pax_global_header00006660000000000000000000000064152707007070014517gustar00rootroot0000000000000052 comment=ad755945ad69008219e661c33ff1d72ff1bbcb51 python-opentelemetry-1.45.2/000077500000000000000000000000001527070070700160235ustar00rootroot00000000000000python-opentelemetry-1.45.2/.agents/000077500000000000000000000000001527070070700173625ustar00rootroot00000000000000python-opentelemetry-1.45.2/.agents/skills/000077500000000000000000000000001527070070700206635ustar00rootroot00000000000000python-opentelemetry-1.45.2/.agents/skills/merge-ruff-120/000077500000000000000000000000001527070070700232225ustar00rootroot00000000000000python-opentelemetry-1.45.2/.agents/skills/merge-ruff-120/SKILL.md000066400000000000000000000065341527070070700244320ustar00rootroot00000000000000--- name: merge-ruff-120 description: "Reformat code on a feature branch and merge main cleanly after the Ruff line-length increase from 79 to 120 (merged August 7, 2026, commit 0110bb1d273dbe848312d429326b844da9e86d93, PR #5514). Relevant for branches created before August 2026." --- # Merging main and Reformatting Code (Ruff 120 Line Length) ## Overview In PR #5514 (merged on August 7, 2026, commit `0110bb1d273dbe848312d429326b844da9e86d93`), OpenTelemetry Python updated its Ruff configuration in `pyproject.toml` to increase `line-length` from 79 to 120 characters and reformatted the entire codebase. Feature branches created prior to August 7, 2026 (before this commit) will encounter extensive merge conflicts if merged directly. This skill instructs an agent on how to detect this situation and perform a clean multi-step merge without formatting conflict markers. ## Detection: When to Use This Skill An agent can test whether a branch needs this reformatting workflow: ```bash # Returns non-zero (exit code 1) if the branch was created BEFORE the Ruff 120 change (August 7, 2026): git merge-base --is-ancestor 0110bb1d273dbe848312d429326b844da9e86d93 HEAD ``` If the commit is **not** an ancestor of `HEAD`, follow the workflow below. --- ## Workflow Follow these steps on the contributor's feature branch: ### Step 1: Identify upstream remote, ensure workspace is clean & sync dependencies First, inspect configured remotes to identify which remote points to the upstream `open-telemetry/opentelemetry-python` repository (typically `upstream` or `origin`): ```bash git remote -v ``` > **Note**: In a fork-based workflow, `upstream` typically points to `open-telemetry/opentelemetry-python` and `origin` points to the contributor's fork. In direct clones, `origin` points to `open-telemetry/opentelemetry-python`. In the steps below, replace `` with the name of the remote pointing to `open-telemetry/opentelemetry-python` (e.g., `upstream` or `origin`). Fetch latest changes and sync dependencies: ```bash uv sync --frozen --all-packages git fetch main ``` ### Step 2: Merge the commit before the line-length change Merge all changes up to the commit immediately *before* the Ruff 120 reformat: ```bash git merge 0110bb1d273dbe848312d429326b844da9e86d93^ ``` Because both sides are still 79-column code, there are zero formatting conflicts. Resolve any real semantic conflicts if present, then commit the merge. ### Step 3: Merge the reformatting commit with `-X ours --no-commit` Merge the reformat commit, automatically choosing the branch's code on any formatting conflicts and pausing before creating the merge commit: ```bash git merge -X ours --no-commit 0110bb1d273dbe848312d429326b844da9e86d93 ``` ### Step 4: Format and lint the branch to 120 columns Ensure `pyproject.toml` has `line-length = 120` (under `[tool.ruff]`). Use file editing tools or Python to avoid macOS vs Linux `sed -i` incompatibilities: ```bash python3 -c "import pathlib; p = pathlib.Path('pyproject.toml'); p.write_text(p.read_text().replace('line-length = 79', 'line-length = 120'))" uv run ruff format uv run ruff check --fix ``` ### Step 5: Finalize the merge commit ```bash git commit -am "chore: merge Ruff 120 reformat and format branch" ``` ### Step 6: Catch up with latest `main` Merge the rest of `main` from the upstream remote: ```bash git merge /main ``` python-opentelemetry-1.45.2/.changelog/000077500000000000000000000000001527070070700200305ustar00rootroot00000000000000python-opentelemetry-1.45.2/.changelog/.gitignore000066400000000000000000000000141527070070700220130ustar00rootroot00000000000000!.gitignore python-opentelemetry-1.45.2/.clomonitor.yml000066400000000000000000000002601527070070700210070ustar00rootroot00000000000000# see https://github.com/cncf/clomonitor/blob/main/docs/checks.md#exemptions exemptions: - check: artifacthub_badge reason: "Artifact Hub doesn't support python packages"python-opentelemetry-1.45.2/.codespellrc000066400000000000000000000003361527070070700203250ustar00rootroot00000000000000[codespell] # skipping auto generated folders skip = ./.tox,./.mypy_cache,./docs/_build,./target,*/LICENSE,./venv,.git,./opentelemetry-semantic-conventions,*-requirements*.txt ignore-words-list = ans,ue,ot,hist,ro,astroid python-opentelemetry-1.45.2/.coveragerc000066400000000000000000000000471527070070700201450ustar00rootroot00000000000000[run] omit = */tests/* */gen/* python-opentelemetry-1.45.2/.git-blame-ignore-revs000066400000000000000000000002401527070070700221170ustar00rootroot00000000000000# Switch to SPDX license headers (#5177) 4d978892d3253da1d6f4e6e6abfbe1e1bee87b05 # Update line length to 120 (#5514) 0110bb1d273dbe848312d429326b844da9e86d93 python-opentelemetry-1.45.2/.gitattributes000066400000000000000000000001521527070070700207140ustar00rootroot00000000000000# tells github that proto code is generated opentelemetry-proto/src/**/*_pb2*.py* linguist-generated=true python-opentelemetry-1.45.2/.github/000077500000000000000000000000001527070070700173635ustar00rootroot00000000000000python-opentelemetry-1.45.2/.github/CODEOWNERS000066400000000000000000000003051527070070700207540ustar00rootroot00000000000000# Code owners file. # This file controls who is tagged for review for any given pull request. # For anything not explicitly taken by someone else: * @open-telemetry/python-approvers python-opentelemetry-1.45.2/.github/ISSUE_TEMPLATE/000077500000000000000000000000001527070070700215465ustar00rootroot00000000000000python-opentelemetry-1.45.2/.github/ISSUE_TEMPLATE/bug_report.yaml000066400000000000000000000066751527070070700246200ustar00rootroot00000000000000--- 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/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc+label%3Abug) to see if your bug is already addressed. If your bug is related to an instrumentation or plugin in [opentelemetry-python-contrib](https://github.com/open-telemetry/opentelemetry-python-contrib) please be sure to file it there. - 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) SDK version: (e.g., 1.25.0) API version: (e.g., 1.25.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). We will close the issue if the repro project you share with us is complex or we cannot reproduce the behavior you are reporting. We cannot investigate custom projects, so don't point us to such, please. 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/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 python-opentelemetry-1.45.2/.github/ISSUE_TEMPLATE/config.yml000066400000000000000000000002411527070070700235330ustar00rootroot00000000000000contact_links: - name: Slack url: https://cloud-native.slack.com/archives/C01PD4HUVBL about: Or the `#otel-python` channel in the CNCF Slack instance. python-opentelemetry-1.45.2/.github/ISSUE_TEMPLATE/feature_request.yaml000066400000000000000000000050331527070070700256360ustar00rootroot00000000000000--- 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: Is your feature request related to a problem? description: Is your feature request related to a problem? If so, provide a concise description of the problem. placeholder: Include the Issue ID from this or other repos. 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/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 python-opentelemetry-1.45.2/.github/dependabot.yml000066400000000000000000000011031527070070700222060ustar00rootroot00000000000000# Keep GitHub Actions up to date with GitHub's Dependabot... # https://docs.github.com/en/code-security/dependabot/working-with-dependabot/keeping-your-actions-up-to-date-with-dependabot # https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#package-ecosystem version: 2 updates: - package-ecosystem: github-actions directory: / groups: github-actions: patterns: - "*" # Group all Actions updates into a single larger pull request schedule: interval: weekly python-opentelemetry-1.45.2/.github/pull_request_template.md000066400000000000000000000034301527070070700243240ustar00rootroot00000000000000# Description 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 Contrib Repo Change? - [ ] Yes. - Link to PR: - [ ] No. # Checklist: - [ ] Followed the style guidelines of this project - [ ] Changelogs have been updated - [ ] Unit tests have been added - [ ] Documentation has been updated python-opentelemetry-1.45.2/.github/renovate.json5000066400000000000000000000046001527070070700221660ustar00rootroot00000000000000{ "$schema": "https://docs.renovatebot.com/renovate-schema.json", extends: [ "config:best-practices", "helpers:pinGitHubActionDigestsToSemver", ":semanticCommits" ], constraints: { python: "==3.10" }, labels: [ "dependencies", "Skip Changelog", ], lockFileMaintenance: { enabled: true, schedule: ["before 8am on Monday"], }, ignorePaths: [ ".github/renovate.json5", "gen-requirements.txt", "docs-requirements.txt", // pyproject.toml files are intentionally excluded: package versions are managed // by the release process and are not auto-updated by Renovate. "**/pyproject.toml", ], // pip-compile manages the oldest|latest requirements files via lockFileMaintenance (re-runs uv pip compile --resolution=lowest|highest). // pip_requirements is disabled for those files to avoid double-processing. "pip-compile": { managerFilePatterns: [ "/(^|/).*-requirements\.(oldest|latest)\.txt$/", ], }, // Also scan GitHub Actions workflow templates written as Jinja files. "github-actions": { managerFilePatterns: [ "/^\\.github/workflows/templates/.*\\.ya?ml\\.j2$/", ], }, packageRules: [ { groupName: "all patch versions", matchUpdateTypes: ["patch"], schedule: ["before 8am every weekday"], }, { matchUpdateTypes: ["minor", "major"], schedule: ["before 8am on Monday"], }, { matchManagers: ["pip_requirements"], matchFileNames: ["**/*-requirements.oldest.txt", "**/*-requirements.latest.txt"], enabled: false, }, { groupName: "docs deps", matchFileNames: ["docs/**"], semanticCommitScope: "docs", }, { groupName: "ci", matchManagers: [ "github-actions", "dockerfile", "docker-compose", ], semanticCommitScope: "ci", }, { matchDepTypes: ["uses-with"], matchDepNames: ["python"], // Disable Renovate for the python version used in GitHub Actions workflows. The version is generated based on tox envs. enabled: false, }, { matchManagers: ["github-actions"], matchDepNames: ["open-telemetry/opentelemetry-python-contrib"], enabled: false, }, ], } python-opentelemetry-1.45.2/.github/rtd-build-instructions.png000066400000000000000000020357431527070070700245370ustar00rootroot00000000000000‰PNG  IHDR~Hf?±sRGB®ÎéDeXIfMM*‡i  ~ H†”K@IDATxì½ ¼eUy§}ιçVc80VQÅ aP!ŠqF[íïë$6ýE£ÆƒvÛ‰ŠFÓjÛ?ã¬4&b4_Ä!fPÓ­QÓ݉ˆ Êä„È<£‚L5ܪ{ÏÐÏó÷½U¢°ŠY»Î=gïµÞõNk­½Öú×»×î_{óMƒÞ`fÒôz“þ´×ëM'ýÉd0™Îôúýþ`2ðcúdÔëOýi¿ÇùƒÉh0�r:"¦7CÆ´7žôú½ñpf2Óïõú”ƒ°?�jÒ›NÉ÷€‡Rr‰=yö&Ã>ef ›�7îÁI.ý>Š@99%ÜòýÑ@YCòz}Tãß`0F‡þx:é Æƒ™A�¢²ù³„å§U�½û3ƒL‘?�b>D”„_XvŠìÞû)JÖÈœ™e:k‚’ºÁìœ9²Å`Nq\Ü›ÎÃsÜ›™À\[0eAm-‡~<ª9dè0žA/~ù›àn“b0©ÐðMÕŒ£úõÑ™ŠêMfÐEõÓúØ«M(‚„)>ሠrCüÐë�(4�.‹ÂŠ£Ú ,›QÏ…‰žˆS,L!ê�c)ÖË%Mf:ì�Reøu0â[4Ñ“’$MuhxKR¿ÓòÚZ T›ª �þ‡yoh£éál MÊ38¢?ÓŸ™p2šG5Z"2,¦~i$Cäö{[fT}fŒkû©jfÏâ)ê:îÀ›£[ƒ£êg&µfëÐå[â8’tõ•pI&ÈËó³ ¹ê™¶<ŽÍT Þák:ãD4ÔŸ6»Þt¨­›©uf­Ú¬z ¤l±­'¦ÑµžØzbë‰mL¤ ´1±�‰ñ@›�:±a²Ã,¡ÍNÛì´ÍNÛ:±­Û:‘UoClîƒc"hˆ8Du±ÆvQ”�ˆ”ø‰P>¨†°ó‘jÁd!Q†Cø'=@åhëD—/vÖ*޶m�˜a¥­ÓEKrmˆM-ßEtH>ÿÜìtÚãú€BDÓ”é€!A«šð,áA°Šƒù¿�t ¥‰5 qpÌsf/Ô#î‹ â¢ørÁ ϰ3H(jÒ[@t3® /B:É OM@2Q…㿘2YËpâŒrÂ1¢NÜ&�ŽƒÈð2"žTb06³¼Ä’ò’%Ìhº@aI’í{A‚“†NH,«,�¿·_øSfÌZõ(5ÌŠ]˜Š?p� WnØjj˜Æ íŸÃ¡Pr7HÜÒPBqù!‹è3äóÅgL Âï 6Iü m3V oêˆQ Å J$ƒü±ð ¡g|PÚŠ‚VRã­íƒ0÷NµT¯ˆ òSæGùþ`f¨ÚÄ j%VsªÿR¥2õ ÓÔÔ¶ k�ü‹n`‘„(ä§9 AëfFèaø ¥�íQ³VÆ”X½ ±–œua|pŠm¨¬bXH ¤G†~›Œ'8�Sa0ÄrÖ ¸(y"©ðÂŒŒgåÀÆ"ÇÄD¢GK_iüâ­pé!Ãè¬1²¡MÄ$ÔÕ‰a?�Ž”È5n¿?Äú†1l¶KòÝà‹-Óq|!ÓíBœ“ê78®@WÝÑz"®k=±õÄÖÛ˜ØÆDdz6&¶Ùi›�¶Ùi›�¶ub['¶1±­ïëˆ Ïkœ(€" ‚×" ß¼€�€CˆL@$‚#Ü&ðaºNžkôš”â %¸(DÂ4”óE°'3 ™áŒ³Å®Äv€.<@u‚£@TGö=B!œF95‘Y¸ùËÁåICBñd�âÏ•D¹hà�ÌRÂ/�@’q`6Nr©90ÃM QüÃ+õèmVŽ˜©¶–ãF¢�bŽòh/ãܲQ"Ña1LKõTG¢8…•&*¡z²L-"�¹öHQquá²Dó]Â;.á  !ªQ�ÅB®¦B­ùǪ¹Ã@¢H—*1¼ð½’¡I[QKj[™ø‰s%[p©Ù>íZIË_xêŒ*,Ð¥:Fæå)Õ°Œú%Eô�G€#2–~ ÕU ”Àm ÇHÑ®pæG6ÖëõUx~©`64ájM‘¤žªœ–Å�†Ã·Ùx©ê%"TÝðv dÑ8Ê ÝE]ŠÊ¥Ó Ì&¯õD\·´žØzbÚ‚Í!m¢õÄ6&vã mÂÛhw{÷*-¤�‰mLl³S{A›�2fÔüŠÑ£»k´Ùi›�2%oëĶNäîàô¡­�HùéVà¬Q9‚ð_0'W ±q‘¯k\þßmÄf(¢â˜,0t¯4ötËEð’1J6Eý­÷ ¿š0‹™d$‘W Gr–KX�^CoãV g»Ó›aQI†¥� ÿЕ$Žò ò²Bƒ͈òLÜÌx%d�T+l’�GpÇtJ±“ $ó&˜‡Ñv�Ò~'¡ÿ¸É2� DÉ ‰vªá\Áù`k,ˆ‚�0F¨®Rx<�p%SP3v,Ú5^š�1WDEqL&¤ØD zµäšKä°+™J˜4I¼žžŽMüGzÅÏ‘ŽèÍînÖ÷9aåÊõH!. „F\¦Z\Ë…g¦dÿU˜_¥Ævö¡Ã;3 øMßèeœL`šÂÃzŠ×ܰÜ�ÖšÆâŒ6Z nK$¾Q½¢Ö3.kç>…� ¶D ùhDÁ�ˆ¸Bµfm#ºR _4·!Z+’™Éoöºƒl�!†@ÒƒÙZÚø2˜܆¾¢éD½©jØõ?�žê6Ú6ûæ¡YuHš.G¡®EqmŒš»S[B)‚%ª�¸Æ·�U_Q¶¿�¢ËQ7 ¾ §“YìÂ41X�Ôzb뉭'¶žè}Ž{Msw®AÌ»qÛ˜Èt„µÍNÛì´ÍNÛì”a²­Û:±­Û˜x_Bl†àZ"UA+%bj+2ãà'èŽDN„e¸ZØv º~–¼)Ár €#!À@…yuA><\Uð-8b–¢ò\kÎ <`bZ�0ÂF¡â”í¨ƒD¯’@)À�bb.¤BA–òÛ¤|,ÁP�ˆÏùÁ‡|1¡€Ñ ód²*ùγ”Y)<’tS�й"…*í ¢ŽðÔV‘·ÒU&ôÐEÀQ19° �ÄfDGh¡t⩚öxh“p* °„ú"©;“:õ('³U®¾°]esÇH(L&8¦v2×AqKƒ¸S—*LÂ))¤¼Ò7ü-k�òÒ ¤L4‚i©ê$,²ä@®Êw¹HÓ(­–Jç:šË¼K)dQBZÔ–¿×‚°k¾w°zÄ\ͶK+ÐBšp.°*IHQ&ÿç1N¬\ k¾p“©­‚_.Ä')›¾mÄß8‰Þ¨¢1)a#Ë™--§”±EQÏüÍ·ƒ}뉭'ÒžZOl=±nymL¬»%7è6&¶1‘±rqx¯‘—!�¡Ÿq¼ÍNqC›�¶Ùi›�vð¶NäÙÖ‰Y�ñÕÖ‰®JY”6ÄÆ&áMBÜÁéÃO±+c÷+ctxWÈ\IÂXµÉ¬ºjdØêY˜ ˜´³Ã7~‚Yº"tH\ÉH%€Î<„bÄÊDÀ#€«€ÆäœL1"‡R"Ñg8Ù nVÄ2TœÅ˜+xq-ÿ¡{¢) ÛJñPÿ™ˆ7Æ éì›F²¥@ AÝ‘åØ7Œ+V6ú_ �‚ŒP\2×ñå­@#– ­� j26Z.¯�E;PÑ#’õ�܈49«hLÅG@?ê€%’(w„×%„³¶é%¢p)¤ye‹øN¬åM�fB ÷)»îùÌ#5à&t²ÈKb)¹VÛ×b} ãH­ Te¨•Ѝ£o„E!g(¢Ÿi³4 ½€±CæÖ@)^ŸÔ Š`Ò4ÁJBÿ?šBÄ¡ÁíÂð›˜×Èÿ¥®tÑ8ëdìfvöÈQœòz‡LþÜ.MÁø_oVµÎ1úÎz��þñß`A‘:Ýi"‡&_ãõ¤ñâ°¾EÔ`6j‚W©ÈÝÄO�Lû³cbç|¯* ,ñ�ʲQÚšP5ð€5¥b8¿ÆÜñ��UÌ™ÕÉ›M9¥½ÕNˆÔj²8Å ‚2õ�Mium=Ñ&‰×y•®e뉭'¶žØÆDoûmLlc"ã'£e›�2sh³S& mvÚf§ÌŸÛ:Ñõ›ëí,%\_´ub['6Äæ§± Œ ªAŸ)aŽ TArÖÁäƒOp‡3Ç;iE†,!Ø‹säBA€àË¡ø¹ R÷'R�"øQÊÀÅ%7`�Jp Ž‘ø€kH%S0o3ÑD…ñ'l GR”‰9„�ZŒ¬(Hв 5pMv¡<° L’‘«† )‰¸"I€#h&^ø&‰‡z‘_6”…S8SA%ÀUîñÒ úU²(šIT‚0+ÔþPãá Z™PË'õ¢Vò$‚U!IÖIa¢®±ZÛpDòÌ÷$æR&¼å&Z¨óHåƒHx~©¦¥�&h„Ÿ� å‘V£±:�œˆRq)ÍöÛ¡¸*ÏW¦²TT¡)Èy]¢Å€¬(MåË)¥‚Ô…™ÒÏ©<]„Ê1°dF8iƒyH¡)ød§* éjYi°Ñªè {É%W«¿¢ÚŸ];Œ¢±”äq.­Ok�Rb²¢qºT5$ôðÔúBD0&‡@'qÑzbü‰_t¹ž�wªòðqë‰6£Ö«Ï´žÈÍ„NÒÆÄÜ`½]ó—ñÆû?÷S‡š6&:Èé˜6&¶Ùi›�:zfÞå,£ÍN3Ÿð«ÍN³$pÐÖ‰µhÉò†ÞÂèáºÆY—§m�È­ƒ›G['6Ć™g fšw±a2×øì¬E"L¼%÷Þé€75ÚëD¸ ¦³3¼r0kñ©A¦³"Ò #Íæåne¾zÓ ,C† Íjpæk_ A[5Ê(‚�OQÃ÷†XE,òf^ü§t0$wÕGO$ œx×'®ˆO¼µ:Êr™=BFÎ3J˜ V±K+Ð�-ç\´ ‡Â6Æa =—ûò3F ` ¹|/ë¬åŒo÷f䈟÷!TAm3é—�Drs� •šÝð ¶ëÖcµÍÚ¢ ±y®¤´}ÜãE¡j èä¶`$“¥º1%o-`E˜p:M+"Œ%™>WStu’ÁÇv"[ ž'é"2Œ7²†³æ¡Ú„ø5ÌÊ›A1I¾ÅMø•�Âÿ;É5œå yÓ¸X.Q¶gA +™ã °2AßpÊæÁú…ÒZPŠâø23ʳ-œ¼JžFà@,¿àŠ8”eµ!=Rúöe3NNˆÎþaýÂA?’˜PH‹ÅËivF@"¦¸�}ùt�ülм½71ŸªŒßQK%�Ó“¢Sø"jÞTWؤ‚”ÅU÷ˆ’¾ým?­'RñS뉭'¶žØÆD×6&¶1±ÍN™ÐÚì´&w̘hµÙi›�¶ub['¶ubClî[ˆ {Í;‚±÷=ËÝŠDáý·“‘,¥;DH€Š($ÒZÉ�!› b˜Tà~IõBÔ@!¤ˆL˜JJƒ—�vÈQ^¢eH²8ç’IDƲ&çÃyÆà°G�+Ÿ ‘Tøð�J4Ѹ8.‘ï© °Ûº±�›¹1 �XªkˆœU„£�€¤U«ÖÖb 9N¨RÝEŽä¨®¿=¬ÈúÇÏ—ãVª{åÙ|j—=•R#òG%XO­'¶žØzbë‰ÜS¸xWjcb¿�‰4Çaÿ˯�‰¸w8¿Ã' ¯ùÌ!|Á=.—i³Ó6;uÖf§mvJOhëĶNlëĆØüt!6ìËß¡ ž�8³áÌÉ�˜�Àƒs�ñ”GBMÈô†5A)–êz6g*@UÌ…ñ˜ -ñ ¤°ƒŸ,+Ü4Ϋ˜/ +‘½(<>‚cT±ŽøRÃwD }DzJÂFÜ΃o>]V¦f@' Ib]‚,Ðúà&k›¬r‚— ‡J¨ò²£B¢³ºHétì.BUI)§²:1“@åw%ÙéÊmDõ Õr²w³{ѬEó´)ºxަªæQZÆÃ¡�\8À�ÂÖ0È›f¼ÆŠTymo=±õÄ4éÖ[OÌ�£n2Þ3½C´11#@¸é¶1±�‰ŽÆŒ©mvš©D›�¶Ùi›�ºLdÎîÇuƒ£§+£¶NôfÉ*„o†Ð¶NlëDzˆ‹Û†Øü‹"6áïJ"ÈÊÿK-$ȇ6ƒ³ä¦Äs—y’’'yE%€šÑJ�¸œß¦FD‡²m¾‘BÆ1%Ü(8 w4â° tç8ˆ…r£.Aâ ÿ+CX‹¥Hò°ç‹qðÚ΀o óN@(¸™Ķ^G t§°É¥'P�³AvS¥£'b!„©x�Qa–D:˜Ê…K—è� f!º]šáfd…¥žñ�@ü#£ždä&®4N£–¼mY@B¯ +)r"Õ{}þ«}°0%üï©Éþ…A!�ÆtÑÊ©­ÝH]�IÚ¡uú @%` ÜóSš%2¤|Þôj½j%8SU‚.±L^îIÝçKµ£&Æô ¶QX‹ˆxCªª$g)TÖS±ÂÖâK»ÃÔ¶y 2•3«V^\d…Ä Èä’Æ�~1®JB SšB­'¶žhcm=‘nÕzb½³¶1±�‰mvÚf§mv򮀦Nd9ÔÆÄ¶Nlˆ ‹ñûü˜8aéP…B>¬ÿNHuŠðÕ b$ÆŸ aiŠh�TwÝ*".à‰4¢Å¾B3^ViNÈŒé’å sÈÊùŽBIʃ‰ÉªìpÉà�À*’Åh¸˜Q•Q@'y¸¦S–*‚éyš•5C¡ÂbˆçDk!3éÉQ.¬ü€wÊ%Af{Ú+.•o…ZÎܸ�Nq÷4™qV%…‚H°’u0Y@â'ˆ—¬äšo’”%bãTV¥$\RýW\b.…õ=¢HÇ'ê"³(í¯e€U9¶¾þ1ºà-KhD%~^!§~J9•6vO~*X¾25ù†¾¤WYp:ÉáK« gÖ�w êk”Æi)êÖpS’c–^¡¼æ€Œ³óä‰%–«´™%œXõ‘1ÕSñˆhgc)�ÊV: èz+(1qá…¬8Døß%_¶Ð¡$)«ø‡^­'¶ž˜ö`¶]Ø2ZOl=‘æàíÚ6ÑÆÄ4o©Þ‡½¥çöŠkÊAÞåIicâÒˆÆ]$®ic"·SúPæ ɹìfñP›�â’6;õÆâýÄiY›�Ú$Ú씥­]íÐ/j å¬ÍNõH}Ú:‘Ñf§iÎHï8;Ž'¼­Ò9G 3bÓxÕ�Æ$(ˆt࿲u™La? ‘K”_cq€.y �ýr΀E¼›à‚0”OÙQÂÈöZFBás¦Jµ`)3l1�B.²Ìw3#å —ÓjŽ„vAC“KÓÄ?9: Q=Bþ`ª4RQWXÌ«CÑgÀ^ügB´”TµàïP%H ó”4$rÜM3ä€\òÇub£òruØ!I}؈Œ· ˆãCg�ôX”GmU ‰ðuyPë©ÚÍ Mk c›©}À�ˆ×\r™çœd§ë³ûÚ`40&‘((âw!]zኗ ÃÏÑê^ëˆ …�¦©L´1x „‰Lð‹½…–üèc áÁ#¼ÓVåÕ†"JíJ�›úØ!Ž–2,HŽ;±EtÉ=×�À®v°D)hy}.9´=ìíõ–Å¢ª«/Dg•˜5 œ•JåÔýêð�®±ER½¾™×˜9çnnØOÍÁ�MØÄÊ€Ñâ”Ãoì ˆV³T�ú¤zl¡ðƒ;µùD²²à?�'YOqEC£)£¼�|,šôØšŽbÄ›^¢9éW4âÏjÂ/­ÊöÖzbë‰nJHÛh=±õDšAÛ˜ØÆD^Þf§L0¸!0ah³Ó6;m³S&àm�ØÖ‰®y¹-ææØÖ‰¬?]XºVÎЛŸ&ÄÆ¨7 ßYÊHP…Á èAXÄ?ª A8'¸B5nÑ¢E`ÂÂ&(øG4)HµêɒΕ$gADäÁÐì*XU÷<£€‰À~Ãü"úÉÅÛ¬Ô’lË/xÅø Ì•:B¡&DRA…ÄN ië¡…ÃÚŠ$’‚ƨh]*A¦$ÅdÃIþBº™]:¢¥ÌÁ”ŒÉÓªè¤lÉ£r´Ê㈬Rጡ›ºT±pÑmf .%1ÅÂË2ªC&rùq$!.ö—³¤J!sR/áÄW¦uŠT½âŸZ×»–UYÊxŸ’@=­ ¤4žôc�‘ç ÇH´FÃ3Άƒ›ªGbøÊ>­`Ñ& S5× ™¦É¨¢‚ÀE•O'€:5ï…ó]âéeQÁ¢©øê¦0± ­'Z­'Únh«­'¶žØÆDo—üyÿäÞéÖÛ§·Í6&ê– J 8(¦�‰ŒÎmvšŽâô¤ÍNÓ1ì&t”6;m³S–Y‹´ubÒ¶Nd*á8ÚÖ‰¸�&�+\�êN\™s&ŽÀ·!6µ¾Ç¸c;›¡µÿé^½ÆF¹åX >¢È¦Xî�–§…�ªBzýyt`Z,®Áÿ¿Àp4$w8+ÛšÅE¶þr%!â!RÁÌö³£cÒ¹ºà4J° Z.m‚³ÿ�ÂÁfû��J–¯-%š ¸,!X‘Î @Q0†¨¯±P� ¦±ç¥Cdô“zÁØ7�B3æmbF´s’p7ô%V ¥¥!tŒ°5_Ý©JB&ž•œJy{Ÿ•ŽñÊíÓ´„ä¡êc]Ɇfh„–bJdXH0Ø`¼ Jx½Ä(Y¹óšïÕ?¬H|a„,u3úBƯÔü“$úEFö1ƒÒ/ö³ÜÄšQOØ‘ÁÈç`ÑNX�ÿc–Ï(ÚrÉ|Ô$y²Àxh`dµþˆLUê-ØLh*‚‹…=ZžF$%´(‡w‰ Ã':šH7è#€«ª£ûcÞ%[ A�xù£-†Á AH)Ã�Fi8KÀ%Õž°H³å¬§¬@­”žêž×5ã!!°ˆÊ…p�†Å¡ÊÊa3F ½Õ1tT,öÃÎÆÊöuÄêMឦ O­!Ÿ„µ±F¶î°y¦ý¤IcXÜd›�3‡^çP'Ê£D뉭'Ò&ZO´[ÐÏZOôæÞÆÄ6&fÔkcb›�¶Ùi›�¶ÙiÛ:1€A['6Äæ>€Ø q.ñÇË`&D�ƒˆƒ™¤æ§ ‚êÒE Bɯˆˆ¤¢r�ú(ªØB ²pàŸq Ö–B�! ƒ3À œ  ]`©D•œ$FfÊ%1²ð„Kt«¢_¢J€™°³š“׊,ˆ�*�•W©‚ ¹¨$¥` l„’�Š�¥8¢—ÈûQL‡bdÑ©¡*#ª(ÎÊE*yÑW^…w9¸¶‰ kþY”BÐÍAv‚I’=ÂüŽƒs&?xÃR�òügy-Ÿr'§  ³ü@S([”Ð#›¥tµ†q''Êcj`0Ý] O(ãª_Åyþ-¬{¯ÉmQK™“ÈÞ–‹€/™ð¯Ž”(þGºÆèÕQ´š\¦l #Vݵ)Ž ?«[ rù– «™V…“VŒÀœ&È?ªX·dHŭа!% ¢•"Î:Ú=æ0^9¨@Ë*¨(ÃÒVVüZOl=±õÄê ­'¶1Q¶n´Þ/oäÜ=Û˜¸8è8ð9íqv\q¤kcb†ü ³¹Ûì43:“Í¥ÍN�erGi³Ó6;]\×0 oëD×(.T2Œ´u"kë¶N´9l(4Ä& Ç]Gl†t)âÎtF  ÎHŒÃÈë$ E¢p`êÍÌç$æ¨p™@‹˜ËÀH12Œ‡Üšì‹jrî7‰Á)›_�6„÷ˆ8*Å ¦!bÄ|±‚¸˜pIaT¡–U _2DÅÚPqþ„Ö„œ�¨“D'XZe )˜ÅÖŒnb•¹ŒÃÛÇZ%¶¿B!@hÞ– òGä’�P �ƒž0-bâЕtÄ&ÞËÖØ�-1{¡ä£vüÃl8 D$WbÛÈñ�Òyì~1”T¼Bo Ÿ.ES–6K½±å‰ûÖ æase¥€º0a±ËÊ$GAzH× #PM 4`¸@!5NŠ ;mŸÀ›'¨«�ÈçÿÖ 7¤ô ï)•Nq#q»>õ­þi-ÕyA‡Óô“Ð.I‰M:ÂýÑkM0ñ#7µSµ™ô¬Oì3ÀÌ ®ÚÔ\�‰^©Y})!ù^"g€|õà�è`›qR”ñœZÅTc÷t&?c.°Ø„ª1ÚLå£L·™±lT•…žS6‰DÏi:¹ÊÔµÜq“YÐÿ7™ŒˆÚ#º%¼_uŒÈ£FâQ¡¼Öñ~ÕiV¶õ×zb뉴†Ö½¡x·icbsÚ˜Øf§Î£Úì”Ñ¡ÍNÛìÔ‰b['ºa✘‹¶NlëĆØüt"6Cþ›X܂ټK]ŸpóTèÇ/®*Ž©¾]³‚�ƒ2d•l_w± ¹Í¤À—ëh¿ &C€·Æ$f%…@ƒ¡² àKÑG�HdAYÄÀ,Š[rY.?�Êâ;ÿB.ä�]„mÁ¡ 1èä†8�…å“}¿J(rÊ:3ä契rêJU@ Óá*?TñS«Cce¤g¢y’Y‚ÿ«W°Ò·–§\)ÌEj…àŒ° ˜oÙÂ>^QG.ãÒÂQ%œÂ?&yæ‡åù+„K1JS)V6Ã(*ò|³Ü ɉ¢t‡óS-šLL&[¶>,l_;�Ži„­'ÚÎñ]ë‰Õ¼hNÕæh4´+[ŽM´õÄÖ¹¿ØFlÜu½KqI icbÛ˜˜ñ—î‘Â=³ºIæXÜDé:u#õnÊ ÁŸŒóžt8›�2Si³Ó´ŒÜ\Ûì´­ëVÑf§mvÚÖ‰µ")ð„ÁÓyh†ÕŒmvêJ6ôŠØðš|ˆÓ¡!�x)¡MÝóp€rNB±ÒËœ³dÃ~l½é¬Û–³îÀë*?ñÉ�Ðà‹@äQýɪûí¾×~köÞ{ß�–‰ŽðnGF0Þr)N2ÃO 4˜…¥73 yÅn\> /Êëõ· Ç`L �Sp-cÖˆyBpÐ}P±¬××¥ê»áöÛÞ}Ê)—^zéø/|á.»®dë/JOˆºë�‰�¢es‡# � ÃO8 9bl¥Ž‘Hh¢^æå±ù˜EEzˆ“ ¢%ZTmŽ£îhŒèbH—FÄ5ÒÈ»8‚ߥoå¼Ìp…w”Ìs81¤Í|’|#©/[u> Q£óŽÁ9xÅgœQLæÅ»>;ɉºR£¬²!jŸ¥<ÔBQ± •÷�USŒÿsRÑ4(æ~h¼½ Œ-C>ñUžDŒ¼UÖQÿ´†7–ÿ€1¢3ëY†êØÓgZEpdõˆ¤Ù— 3ãiÈ#äв«\%Ñ�á3C2RÑ|¦À‘[¬U³�Åɶ6ácD›UJ=D ãÏÌ�R~i7þ¦¼¿Øføßt¢Õ¸i4dûŠV©µ»R|ó¯µK<\GEg ìÄé‡zŸXi‘ƒîÛÙiú”­=É_w¤ŽPßíâ7š¬¸÷z"Rq1’xÒ†¥É­'êÜAÝàÓz¢7„Ö¹5Üccbë‰mLlcb™µÙi¦vmvÚf§mvÊj¤­™xµub['².»—›¡«qƒ‚„D;ä0Õ Eþ…FÅ_@ r±tó-ßûȇÿ*~ä×î»ßïמ}âCþórbØw‰EQ�¯ 4ˆâàÊ;�˜‚Ø�r ÷»N<ÁÞz³Ttñ±È¦š¡ù ÍöDµ·¬j�âIø �ñË'l‹]8Ý+=•õ@9%¾k=Ñ:M ØHüpá?*Ùój0žÛºq]뉺†.ßzâvŒ‰­'¶1±�‰mLäNêÐâ`“™Îâ„¢ÍNÛì4ó�6;­žÑÖ‰Þ"œ‘¶Ù©++ÑÖ‰÷bó¯mv:œ™°ó‘0î¶Æò<ÑN8�Ø7¢TŒoË’_·¸ÞÏÞZ A[¸A»õ7»PÞéqóÍßûã?|ë>³úÅ/ÿí=p15^ø*ßF ��_“ÿã?ÍLPl6`†¡Hîå‰;Í­„r9j9†¦±p%izåW.iuýõ×.ìá:{¤¦yD™À3$ŒÇ„™±�˜ ѹíre$8B9IµÞ• ?¨‘ &ÜÄI< Í%ïÏÞuÊÏø§<ð�¿ÿŽ·�¦¡÷GOŠ¥€’ËkÖÛ|¸äùC’I0,ÎòUØÀ´šÄ‚¢3 ™T…_éÆ¹Ð êcŠ6@¬ ê ]qß„·‘ÂQâ"jn � ÒL]8×½h”CØ,ÜpÚþ7Ö‡$AÄ�6 ‚Ò13ʼn›#™}àbKPUÛ)„P9ƒßh òM5V)™±—ý׺F8D–= ÍÓ1”­êá:†c)à‹=à†wCˆ¹zÏšKûV_‰¼Â Ò´Nã_ßú`@¦?(Ä®ô²)škòôœu�‘˜�@ÜêÂN‡EoL@žŽ0_°s/?¡•ÄV~lÙBâv÷DÙÆï:Ô0¿ ¢kkCw>Z˜uíÏ÷tOD!Cÿð%bQŸÞë=ÑN…s@îqL뉭'¶žØz¢7Â6&¶1±�‰mLl³Ó6;m³Ó¶NlëĶN¼—Ö‰C�(.PA¤ x‚sR–ª‚'9wÕ.¼•«æ¬áùa9/  ÒÐk×®=æØcëâöÛo¿þºë¯¼òŠ�7VÊu×^ó¶7ÿî[þà÷\ûÄ¢0@Šw-Ü¢p<þA5”(qp…XÀ£§’ƒ((�k1 ñ}ôÑGßpÃõ%÷ÐC£´T *¾)JI¹{ΑéÅÅ„]L„Hóâ€Hæ*ÌbâN Äò>Öb“DÓ¯~å+ãñxÓ¦MdËÖí½ÀP¼| ‡m¸‰é€àPƒ¼Î7¥Y<L¢Ðx�ŸØQAy �7Äè„|¢cMÏ¡î±Û+ ä#àä· rЉ¥æ%0¾/™ eÍ{&dJ^ÑŸ°r ÿ"N�·©’“@$~¡ ^ÅÃ�*õ¼ç=ovvxÁvØá'žxâÜ‚³ˆú3È-�ä)–±´�ªÖÝÌw°Îä™T\bðÛ ïô ªÂU€%¦³ÁDRD‚Ô‹cÆõÚÕMêÑË¢d "x0˧<šƒÅPÚ$œ ¼Ù¬�t Dä²ËXa%¤ò%hTsvsŸ("ý4{6²� ”ÌU„èçø ð‘H+â°à‰ßd�šV:.%ÈÓã:‚R£_,H„k°^á㠦%X ƒ4 ÐO‡îhX]ZâfndóÇnUÎP¹**¡!.IàWa°%Ÿÿ‰1‘G”ˆUí=’B»™Ij’à�§7å»h™1’•«îžª¿ïKÃ$ª ±d¢›o��ÏrA’êGº‚ *°gô‡xŸˆ9„b£,84ÈAã…Avų!È»·`â‰Á3{ÀΉø©‡JQŒdƒáînOÄÄW6©ÈqŸ>}¡rúÒÚ±žÒîžî‰è‚P"ß”‹]–šz¯õD¬öÝÍi½!z‡°[à£Ö[Ol=±õÄ6&¶1‘�»�‰mLl³Ó6;m³S¦èmLlcbï¹u"ÏrŸýàãÚ¸N]’ú`µn¢h9|qâÊ9Ä•ju0n�ÀÓÄ£øt]ï‰O|âÞ{íýŠW¼¼hNûÔß?íéÏôjÂÒª€8Q(òø‰$ÕNÙ^€9•+ø€ª\!º"’#%ßË–-{þù/¥âPh¼Ea”pá#U1Â@èY‹›´ŠK€˜l OªÀßüd@ãã“,ßåÆ�H‘¯ä¨#EsÓ÷n A¾$¦�ȇz‚ÔA+Ö‚§-ÀL|§BŒM§ WXæ•O¼ Ðµ©‡0€EZˆÇ;`EkB„x•Æ@4×T ˆ£2õ²rÉZÊÓ64 8`›xgÑ‘k=)oOÅõ8â}ÁÓàì�0±„2¤¢CÊWÌÃ?&"Ceèç£HJI¥¨R-õ¹ðI‘–£ó¼\¥DX‹éèl;*Cò£RѦ#µxe„(šò¸(¥REÕä( ›È‹â(¢¶ÀŒÊëªI¬ÔZV¾´¢ˆ–Âõ)›<µ‡ŽVi¢êê²üÖ»?fOŒWË�¦rãÔÆì¡Ý¹LzêG™J-uùV·¨JrxmoOÄ0ÚŽ�¡ÏGpoõDmµ4�Ä&/4“äÖñ5Òz¢�W¤¶žH§µŸrÃÀ#Þ¸Üþ1±õÄ6&¶1Ñaº�‰s2õ°Epi³Óš¬e–æÌ«ÍN3·Ù©-¡­éÌÞãuLàÛ:7Ø=NîbãTk;î2ž¸v&‰¿¶Ntp¯¬‡šÍ›7ÏÝrûN»î¬‘ UJõgÁÏŒ„V@ÏB!rk’âI¶£0«xìÀ;­#‚U#ÏAòG !L¼Ó— çWì¸l†}µ&“� óÂf†|A#´€)p"˜L�‡>⎭BL� }âa={7"ÛyÛG¡€0c³¹è©.e�±ªP ¿ }n àaßþηI¬ÃELì1Ò Ù¼.T£à*¨JsOt’ÅÉì±¹¯Qí�=À’k×[´ 3Êó£è�“ù¿û?ŸØk�=�~ðCúýèÂ^xºP¨74, ™„ 4F½¡5Öë7cÓÔ&V·æ#“()‚È@.¤k­qq8ÏvàZL³-@t˜í�˜8„ÛÅÑ4, OÁ(Þ«i {B'üÆC—*€‘ÈÂcp3v’C—÷ÇóViN�/t©æË p—À£.$.”Ã<¶¾ãÄHAp,TNä›°ÞÒeîÝfÁXÿißè‡DÚõi²*v/Þ7T qnÕëÏÆ•ÂSˆX“ßÀˆHÑ3l‚)~À‡ü*³Â×¶JUs…ý yé;¾WN¼²W·ŠRÓ„Hň!å·«'ÚgøKSÓKºÉÚvWD¤%ÌOoj.U5f·;Ú˜[סÔ=Öqû:¦Eé$5¼w{¢mLä/-È5ÖúÐñ=Ñu¶ïÏ�Œœm=W·žØz¢7¹^©·žØÆÄ6&¶1±ÍNÛì´ÍNۘȣ­Û:‘…¿¶1è�%¹˜ë$¿<@C@|^OÊT–èžäõ,¦¥äÒÕ~W$å\\³ä ![–ø3[¶,Œ'Sü,è�Ô«®ºòð#Ž„îŒ3þqnÓ¦}÷Y}äÏE¡k¯¹êôÏ|úšë¯g|Ì1Çÿ›' EØ4·éÊË.»òòË®ºòJRö]³fÝA¬Ýÿ�wØQüC=Th�¿õë7þïø_ zÇ=òV­ÜMôÁ…¿úAÈ׆ ®¼ârxî°lùºuûïwÀ�æÆpÁ'd‡ù�r-@‹Aï{ß»åòË/½ô²K®¾êjÞÜzÀû¯ÙÝ~û­ÁT•€?_½ÞÕW_�_€°ñ¦õëy„c†ŸþÌòáQCmæ·œ}î¹×^uÕµ×^»|ÙŠýÖ°fÝ�rà !‚Q&`Õ£=9Ö†!_YÚF£…«¯½úšë®ÙoíºÃŽ<š§ˆ%¢$:‹»„4ðE©¦è)œ–*,Uùî  lÑl¡«@6p��+ £�Q†*…Ó•a²UŽ„døGcÁ–þðª…:�ñÁµh!¢4‘+SHã2H‰el‰PÂÔºˆ#"×2ÈÅ€� ‡:””ÞC9Ñ0t³²¹„‹Ï›ò/&T‹ ÊÑ@QŠ-ö1W]B¯>óZ¹>¥MB—XZs™¥#ƒ3•[”NJfR¬b•òý鉘Ò‡ot¥Uó}= |Ü7žÄ7V �ÚTH4áÛBš«cêWnè-�…òцTµU˜¯b[\±ÄªŽÛ¥Cl!‹Ì#©ÄàYŽh…€r‘iD:hÏÖžh“±V-ˆ~þƒ¦”ˆm¤“iñ´y D×ÐZG*�„H6\3Ð ô%¦©é]î‰â½*EEç7…á_V·žhíà�Ö[Ol=ÑKn.mL\¼e{çÌýS¿´1oØFÚ˜¨8Úì´ÍNÛì´ÍNkÅå ›©~–šm�˜ñò®"6®mÚ:ñ'ºNò0¦kM0!àŠz˜2«FVÓ¬Þ'‰ÝpÏ"Ô%³€S"‘‘FÖ^ÕÐÈ„‰Ý­X¹Û+ìÇßüh´|ÅŠŽ¦×››ß :²qã†?}÷)$ÎÌÌüù>ôéOýýû>pêÍÅ^ð„'ÔC¼cîiŸúÔ_|ོµ`‰`éäWŸõ¬_|ò¿K°–@�Ñbýþm·ÞüÁ÷uÜößÿÝvÝ]¥ G!’ËÞ:þäÇÿöoþú¯—˜pòOÿô�|¯]·ÿ ^ô[{ìµ§![1H4 ©?™ŸÛò'ïzçYgŸ¹m©ÓO÷jõš5'ÿÖËöxàž×]sõ[ÞòÆ[o½e‰fýúõÏ?éyK—ûî»úw÷m8ôæ‹_øÂûßóg„.åžñÅ/p¾ë®+Ÿÿ›/8t’y(>ÏÞ€8aʉ.85M$—ƒ1eÆæ�$^uåk8ðÐÃ�ð} ø~h�å$„v¼•:¯y ò"F–‹lq7¡!Ý'`‰Ú4 Às<DÝÔÝÄ3øAv`®Ô9ÒP8àJàãÒàŒñ;å,IƒB�=š^ ¼f•(@Û™µ„ô1�—PwVËô0Ã�<$Ù𭨇Œ@r†VécIÀYŠRÇÜ¢óú… Æ$Ò©jšèmY­Lƒ6{ü ^úž M%BL.�tlia–²*�ŇlÕ”‰ŽT ü@²±Ñœ°% ~Sw„6H„!Þ„‘[Ũ׶§'Sˆý�ˆXŽú¯êº!†·±‹ß±ÙZG®«ÂE²¢Jì#[‹ˆðëz¢WÚ®e6«0Óz6$jP¤ šê‰ChØY‘ ƒ M®«'bºN ¼‚ëx€Z׌}ﲄøõz"dzZ=È7DS›ùGI Ëj*U­_G[‚Ž;ZÝëÍZmž›!˼"w@R¸+¢‰ôzç=Q€ß0J†*Óá¨õÄr†®d·Jê­õDï¡­'¶žØÆÄ6&¶1‘�ºÍN�¢0P¶Ùi›�¶Ùifêm�ØÖ‰Al~öÖ‰�©²æg�˜�KPW¶ )lB¾V YVÖ.›\Q»,¯àH=\v×A†¦¸Ô 7BÆ.¿ü²EªÞ�ÎÊ·`£K.»x[Ü�ôU»íƪ™„Žöu¯þÀ{ÿì‡ânP~èƒ|õ«_uû܆,¿³¿GÇœF3�‹�¨·þöõ¯yÕ»î¶D zõ²—¼à[^[Ä1`ËAÛ O~Áp·¥R×\}õË^ü¢o|ýëàhÛânKÛžøz‡AÿܳÏz÷)ïØw[¢¹ýöÛ~ïÍoüÊW¾èRô&ð™q~! á†Áäñ�ÂŽ;îXÅ� ®¸ô’ø»�뢋pqà4ÊR^H¥j!RãÙ¬O˜l­B.­v@¦]ˆ�YÖ)‚óü«Uø $Q¹:ˆU$E`$¿î£Î¥¯7PÉ ž¶á3¥Á§�Nÿrï3š˜…PœšçÐZ[$‰q‚û<à¸ñÆù¨G!™ÑåÏÿêÏ/úÖ…•»Ë.»>ýéO?øàƒarÉ¥—|äÃŽ1Tþ@IDAT¾é&ßfpÙe—¾çý§þæ‹_œP¨„~·t´`‹Ð� ¾>�f—_Öá€Çsì³NúA¤_|ÉÅûèÿ<çœsV®\yäƒ,B§a_05ç)G^ÊJâ^+Œ‘FºÁ·÷Þ{=õ©O»âŠ+Ï>û̹¹9Øbî·¾uÁ¥—^|ØáGtÐA\Ç ó6 8à{&Pƒ|4 Œ‚Ôu‡10ÛKÌ‘|füoI\ˆxšƒ¨‚µL†ìEp¸fI0j§]åþG A2â˜ÌN5¸É™ÓkÊ׎m î‰Ö[Æu˜„žräŒSAÁ<ÒG�Àîrð°ih ”ð·!ŽMxg¥Â) o¾¼åPƨ=-ÕkHRO¾ÂØ·ÙB ²9SXXP1ÒÈ0ÜJ™뉯€µ Ÿ�5~q÷=qD%ëj ÙóÂjÃo ¥ä6×2¼ãT¿ù¥/øÝžž8¦_;K9©áôD‚Ñs4«Þ¸ƒoÃöP•ÆgãP�žEcuÖé‰qéñ†Ý‚sâJB¡¦t§÷dÃmôÂÔHj+¡ë‰À¿äPi½ƒuÚ­`ÓÂ;DÿXš;Où):ä7SŠˆ;*€ öâÓtÛ3eOaKx¼%R­r¤£ž ‚Ú؉Óñž4 ï"“˜ÒTçN{¢öÞ±i#\t¬Ù!’·ÙàÑì è�Éh@óh=±õÄÖ[OÌ ±�‰Ü†½'¶1±�‰mvJg`hh³Ó6;m³SW m�ØÖ‰?KëÄìzïR0kb~}ŠÏÕuV¸bY€;p$A–Ö®^ùc kò6P KÏZ[S ^«ðÖ·ý>hTQ}ÔÑ #@9/D´í°Ãoxã=ä°ðtعåö¹k¯½æsŸýL‘u½á oÚqç� !9èÐC÷øã_÷;¯ùÆ7¾ÁYgœñ¤_üÅýØ_Læû4²4 $s\vÉ%gŸ}–g½ÞÓžö´ÿøì9ÁJ€³_xÔ#�:úèç<çÙ›çæˆ\û_ŸøøSþý ¬Õ±úSÿn½¥{†ôß<ñ‰¿ù“yS½Þ~ûí÷Ä'?éKgœ±zß5{î¹kyÀA° v¦+èmÕªUÏ~ö‰80˜…+òÛ6Ì]sÅKA|o}ëÛöÞ{_X¡÷QGÞzë­+vÜ &›ç}ÄW‚z”• YÊæ7¦:·|ùŠuëÖ­]·TñœsΪ`ºñxtþ7¾vÑ…ç~øÏí¿vK'é#þÁ7�QꞪ Seвœ©2DÂV¿mƒCœÉL sø�sy‰¯p­§½ÜZÁRu½'X•™ðB¨Ì LtÂåQJ©†ü’[ÌÔ#iFÙrØÑLŸˆò•�%MªÂRHÄ7A6Ȳy+R2‡£Ä2®„‚c ±Œ€’)?sØAi ^‚…2õ¹H«¡”…ƒþMŽhU¼šKÅ@¤‡·«'‚êtu¦ºwè‰Ñ©*Sø+5Ù) ²�*‚FÑW�EA;µÞ=ÝÈÒñdUu†R´L ¹ì¤.`ò¶ŒxÁt>2¯ösIûZ2�,.¶:Ž2:J½â~�Ótn��q�9wÆ@©¤ ærWl�Yó±'†È–�(ˆÈŸaoáZ?$¢O¡w¥'Ê R¹ó¥,mhSÁDXv!¡õÄÖ½íµžHWk=qñ6ÔÆDoŸÜ7¹…¶1±�‰HÛì”™‹3 '2΢r·à¤ÍNÛì´ÍNkšŸi}M½¹i$Í%Z['z×p!ïr™åN['f~AKqMöZ'ûùO…`,´Rû0K�ÙÁt–ýÀáF¬uU‚ŽBª#KÖ蜵¬#BÃeÜãÊåì†M›¿}Ýõ—]tч?üW E°lÙ²—¼ä· �›�q“¯mŽ×¿á ‡r(c »^1ã�x^­÷þÞ%’Wý·Wí´ÓN¾³aÞýäËfV¬XñŠW¾ò™ÏxFÑüÅ©§¾æModo)Ʀaz`W–•4zÌô`ø¾J$¶îמõ,Vî›æˆÀCáÙáp—]v>é¤ç¾ó”w@sî¹gÿò 'Œ^ú¡¿ü`•Úe×]^ð¢Áhn´0Z³{×ìì̲åÃG=êÑTㆠ›çq¢{£m+¾·°@x‹µ œÞùíë¯/†|ƒÍÍ-l™ŸŸôÆÃÙÁŠfWî¶]åæ�s8Y/³�ùJœ {µù¤eœñRó“éÜüܲå³+–/[wà|.»øôß²e üG£Ñyç}ý /8î¸GÜ�˜;ÞúŠT¢oXu‚+þÊIa“*¼�Îl¶º2”);UYˆ·ÅRsÉI,cB{(±j¢ºújíT3o~Ñœ ñw£nÑœšÇIƪ6„' & |PjïFÅÍËæKkå N(Eé´DÉ�YÂrn¡`]j‘{�¿¨e9 Ñ üˆçtÅ á†åBŒœ¶”Ó<ŽØÕQX"4Y Ô ]Õ¤U¡20&ºj"An Sìq†¼'¯¦ž�%€ÓŽ£‚ñÀÁ·o¨¹ÛÝņô‡‘mDŸ©Pz" F+ý›. ¢hZ=;o*‡50Q×Ð0ЖkÈâò¤hBIôv !mRs>ÛjIbeñ¢¦k·ÕP=Ñ=â0“jäåÃ4<žž8pEúº�gÖ:#,2%‘çpYï˜Õ‰°Òµ†Eâ\®Æú xˆ©4!iÆ/¢Î ÁÑ ðE¶CÚ/²‡ä"ÓÆ‚D«ˬ2SP‚íH&Ì• ·©Ãå�»ÔÕ“‚è¨Wñ† •�Ÿl6™jÖA뉺¶õD勤™Ö[OlcbÛ˜Øf§mvÚÆÄ6;mëĶNìý¬®yÊi¿“ÿüˆ Æ@p,ÜÉbeêR<+ñ€.+yÊŒo×Ã.O]Ä.ŸüØÇø,]þàÉ}ù+V®Zµ~E²VßZöÁyÈ¡‡ºqn3ˆ›Yn­§¨+wˆ{øÃ~¿ÝïO¼º  €Ç˜UøBo·]w}ôcSO³^uùå¨\ r|t€%dÜ»òŠ+*ùIO~2+â¹Ís®åSг……ѱÇ[×^w-�Ìnüη— Ãg>ó™,Åoß4ÇBœ?�ŽG sóƒåË—ñ€1[Ç›»™B]¯/7ó ¼ę̂hÍÚý3{¯yÍk^ô²—í´ãÎøcaËhaºÀ»pËuÌ0lÆzðœ?²c O�ÈÉ$‘ÄýçÆË— W,Ÿ=ààƒ<è Ï}î3õ6Xh0áú믻ߞ{¦¤Pâ3¨ aÁ®d2AT~ƒ,Y¤Lþ Ç$åSŠo®m8+µr*Yx0™a¾�@L'!¢¨Ÿ®-Ia¡’€dÉ;ûaPÅ“HS5ÁÅË‚)W'>MVÇßð—nqN‰²�û˜(Ζ—*¦”ø‡bÂ7ÒSGQù€˜‡¥a…JT…$Ÿ¼T.¾“e*«$êÔð¥yh=” ±'ŠC�ò„BîfOTb´,U5&šÀ‰jR wòøCñÑ­œ‡u�¦éÑ£2ÐJ( ¾þ¨šDœ¤>TÔFZ…lB£…ÔKÒuî³ÿùè¶Ìä–ž,}s]I=åFŠÉü“›x%E`¥-…¢A¤‘®r*Ï j³ZFW¹aÚL/Æßæ©éê‘ÐY†‚6DÏ¢Dy! w§'R z¨¯-†ÛIyÙ¤mdDBººcE%©’[OÄ­'Úôm¡iøiì­'Þ�1±õD�m¨~¼¥¦Oy;r`"§�‰mL¬á�M»‘ÙÙ‰Ãtnz çNCr3v€®iCͰÚìÔ©Ý�­™�ÕŒ'æ´æX:Óɧ×mvª7ÚìԆˆ•ï¶NäÔÖ‰?{ëD¶œb¼�ðB׸ÄaÐâ�”H¢ùW¼“3G V¥Ž?WX°0@dÿ–æuŽM?êàqÑ“_tòÚuë€ÏX‰XøèÚâqü¿qsžCuc6f…dL·Œ6qÉ‘Gµaã\nKˆÌ'3¦ s[Ž:òÈ‚ÞÀ¾ÖßzÛ»®Ä¨‘}·;@Gˆ´a×­[o¹ué1Ï�}ô£_øü2¾Jæ¸�ñ´+V5?Û¦M«vÞé†k¯YJ<òèß¶i�ðV…޾´’ü`4�@×Ö„Þ8Z/¨âÅx,ôFúô¶ôFû°ŽÀz÷›ß<ÿ??çÄcvÜ£ñ˜uô2_  ¾Ã�ÇʈîØÊ>V V¥&FŒ ¨>˜BÌaû¬mÙ2™ÍçºkÏûò—·}‡âö]½F©Úe1™i¸’¨àò©s/ÇAù2IŸ€òN„ñ¿ñPP5„IqÂDt2ÇtM¦µP’fbJ€ [FFU Qoo *¥ò„´daƒÒP"¢’‘}ï-Þóõ¹úØW"øæNYZåBxzµÐ¾ÍHšFÃb`l(ÔHi2�(.%ï– aBÌT€ ïíPiˆ>Pmõ§$û‚©~F‰¡Xh‰nHqï¶yÉñ¾ÝüГâ ÁKü¨#” «ƒ íÄ”t2KÙøÅy8eíÃo”ÞÞž8�1…£eèªn¤–‘‹qƉ!ÜÿN‚‚¸¯E‹ÒØÈ£8�µBÄ`»žè[\a8û°›-;a'7�“­Ù-J¶½Zó,t çæÁ¨P)�RòIˆBhKª1®¾ŒÔÖÀ¾2Jñ(i,›éK=ÑÖI7Ä·‹=QzµƒÜæ~‡žˆèJÖj^h.9.#W í³š¨VÐu üÎ{"wI![[ƒ:ÛJàÝÕ‰Žo=±õÄÖ—ÆÄÖÛ˜ØÆÄ6&24¶Ùi¦#mvÊtÌùv&ƒÌžÚì´ÍNÛ:‘ÿv"6m�Èòö'¼NÓÈZ6þpsËršI° LÖÕ.{MöÂ~_”¸²$-ÉfÝñØ}÷Ý÷ÞgŸµû­=öç>ÑdýM›y8R@p)+Ú*ƒæyÂÔÅ»K`ŸÊêM¯»n+æuÿ<ÀX7²XH»¢¯û18Êôþ÷à’`ö†;à°•‹ ã.YA®{'ä.Qò:…z£ÂRÊž€¾õ&;ÞpݵKY«v¿»ü‹hµèkt��…3|ªÜñ!¸ÀR!2ñQu”Éxíë^÷ÊW¼b‰èܳÎäÃë#ý˜GÿÊÓ~e÷U÷#LŽ‘'aVŒ÷m/ÖEÖý*a Wê@Ù9»öš«Î?ïk[6o^â<;œ}È1ÇvØá[Fã�sóx�Ú*k[|ü1ù|Y>ÜK$)°V_D£2*LQXC)’’w«'V!S˜O5º¨'G55Oe–Ïí‰X¡­#³†Ãh©' vásá1{_9ò=QñÊåW¦ös:¶®ÑI£®`‘0ßt!Á·V¯j}¾/)\™U™p沪¼ܵž¨¬â¡«ùWN�“ Q³8S�HÊ]É*�™dCÿx=QA(‰šùBòV=bj뉶 ÜÒz¢-£õÄÖ½áy³Ê—}ÃÓÜWsod´hc"icbÛì´ÍNk�Øf§ÌWïî:±ÍNÛ:‘ÉE['.!6÷ò:QèÍø)W§@#nˆUk^²ZŠ:Ì3tn†Äü�  V>ª÷Iº²®ãߟpÂI'�”iA7…즔™ö6m\àyRw ãŸì3F€Îâ1bC5E °îG6XÀ¦õ[ñ£+vpÙ­¦¬¸]Ô«ò;ì¸È¦·aýzÒ\Ig ]é¼-ˆ‡ ††‰uÇãŽ?~·Ýv¯9®“[Y›«ù�îºëʲçÚd²aý3r ‡Cã}„ ƒ1, Êu“Àrƒ±¦R‘]Ú7´Â¤�ÏUüÿÍ„÷)¼çÔSßùÎw~õ+_Y¢$(ﳟù,Ÿ¼à…�|ä£|?¦Š»‚ç=‹" bÂçÐD1¬ !-|qýõWŸÿõ¯òR‹%ny¼8õˆ#Žà%‹ÔÇü(‚ EÏ×Ëò3cYg '¬„ݨl2Ÿ€¤4†à\¶JëÄ`"š ¶j�ô°Èr€ ¶Õ/Ô‘Dµ�žV$®¦É+I…a”ïj»<„M6¢ÛT[bÞšs ÔD´@ˆû±¡+MìÓå:§É"#OYu¨{ Ô0Õc«/$¾G(…Ô²»¼QÊØ%~ÄOSâR‰KΑg _‰‹‚J(”°*\¢zЉ¾°FSc¥PE‡¡ ��ø14ƒÊ7JGsÿ2Û qX´x\¾Ý=1­E8£Ã¹ÐÞÖ™ºSS¿åm¶V$úÓ ì3ØÔj@ÛðzbÖ{×~ìÅã‘ïï‰V…õè~nÈr¿E+O§·P’ƒo‘ut%øm^šÞ2tA<~‹îŠÈý väݯiˆ¸6¢Qælþ»;í‰48£-hKºCO$i´‰î!ñ¼55�B´� }»[=ÑF¦¡ÂŽ*å³ËúM÷’Úzbë‰v•Ö[Oô®á}Ê[‡7�6&¶1±�‰5Ѷ[´Ù)^h³Ó6;m³S'óm�øã"6m�Èhâj•V´„ØÜËëDöç¡*À—¿.E\DXœf *²ì�®QÕ…¥£KH>ß8ytÅé2רŠ7láÝÀ' Y+h#«Ú­E]D+Óâ®w{½}öÙg)û»7Ý´ÿA‹\© dÒf¹ÞûîM7.‘­^½†(ÝOw°×«RVo³ÃÚcwü1Ç##i�ùkŽß$öúë7Î�ì½zߎQ¯wóÍ7¯ZµR%àÏ%»ð‡:«6†ðLY°XÎ\˜+I­7Ý´yó^{íõ†7¼�÷™~îsŸûìg>s饗.è�rÊ;Öì·fõ~«á¤cTLË‚÷P¾bª"xÚÿÎ�7œwîYssÝùŠ ~îˆ#ô «Hn™ß²e”•wæEc1,ϧy©^нú•J§¤йú¦\¤HZ­_q“f™S�¥ógøøe–XC•WRÒlQ)Q?êbÙJË?ÍÕ`¡›Ò½ ²„ðP!cT+•!;ZoTDe4öCž˜%”0õGhRa!¢=GR‡ DBʪ×:)zQ4QÔ²]-’ª&Q3Ôa…£wÉ€IxFèa'¨0b;{¢ŒPBOÆË^‰NÊÝžÍ�ýÇdë! ™yšåYã–;öÄÔ”2öÇBZ§@mîz¢œ­4ÿDj-&y¤¥jµ2‰ˆŠá*¨t‹ªT®|¡,ߘž|y*;æéh¹ßyO´˜5(”+œWOT®\­çùªK-c±u¶,°™Âiä›Fׇúûz"_¡ûX)–EL)LJ‡R––Æip­'ê›Jõê÷è4½Í¿ÖÓxm~é ö�ê ­'¶žHoic"=Ãÿì°ƒxw�KrSñË›±w�6&ÒcÚ˜˜öÀ˜N³È@c³IšS&QÚ‹ƒ4­É òm\þËí—¦–áÊd>mvjßÂ?|�ˆÛôjë‰u'Z\'¶žX�‹vÑz"®° ÙQÚ:Ñ».ÿ¼¹â�ŸÁu¢À)+M¬s/#lj†q±.àlÆC0…ù|<¤‰¸…Ž1§;X†Þ27—R²læ‰bXaä;®àN9:™ßÂ{‹åÒÜúÆQáBfZ“=öxÀï:Pßr(–çìIý~›~ûÛß^"ÛgõjÞ&j$ŸŸî�E$ÎÌ̪Ýw#¬¶{»êŠË�zÐÑ[kV�Ï•B,±1€­Â�°vÍêEN=ÞT°Ë.»@ëkÝŸ¬F][žÒ‘šÖ_¶µX�ä�¯}±ÇkZ‡ãMÓ�—/_µrÕ/ÿÒ/óùÎw¿ËËU¿²wÆ—ÎøÕuÏÊ䤠fØŽŒZA�Ýh<=狟çÝ£%‹ÄC;ì!9f8dw,Þ|º°enÄngêLˆ“ïßDE wNCméLÂ!Ñæ ±UN ÈWZ4� �Šà ¨2 Ñ\¶ìòÑ<[ ¯€‚c%gˆ†ˆºNRljªT›¤�›ÀqÊ+Bi&„4Å�cR†WO|a+*USýÄŠ•? Ï£°Ùÿ®¼š�–بÚ-Î6A1bµp–ÍI‘ÈEy”â* *gS° ѸË3¶ý´WæöyJ4µ�—^8Ø5eš‹gÖ~˜³�iÓ­6— ŽTµØá·º{{{¢Õƒæìj‡W¬eÃêÒGÑËZé‹@~ 'ÒØéÕÓÞ�L0UvqT£­=ѪÒ9Þä‹Ò?¬'Jb5b-¨z8†ý6ð%Â#ƒiE¼r�qŸéõ–¹ ,¨»®}ßÖ¡sôNåªT@¼.r¤ÜiOÔ¤Lkvüƒ=Ѱ6"±ÙºÃˆÀRþ#Âz"b2X+ÂAq­:+÷‡öÄÅp\䉪ékk¬^\J$¥�3l=±õÄÖ½ÒÅÇÄÖ¹_´1±�‰mLÌ�éL¬ÍNÛì´ÍN g§g^üÑo^óùïÞvÙx²à„¸Í÷˜f³\¹îð5�>î�§ül¬Ù)žUm{(B4%kPÖ—®2EH\p'“U¯hkeWöݱõÌ„Â\"´¿ÿ<ìa[¾l9‚xá鯹-lï?C…f�c•ÔUre W&Ĭ) HðŠ”Ðç‚bK­E ‹š”Ò#Ö§f»ÐÉJóé>Å¿D‰œ Å>$èdùÔ¦Î�x`ãt¥Š(A´Í†AÙ EUi¤�O*i¦ÛÁgTÒq6syªÌ‚¶òk+)ÀÒ@.ùÒ"7ÜôZ#ÝòZ|Q:Zj„'10ZIhÓ!MðÅˉ"l¶4’È2¯ JÆ§ÊÆÐBÏ” Y{ÝÅžX[^‘üp„\ó#zbQižšG;C|õD¬Ô÷x€dÿqöCz¢>ô!VŠÁ‰C�•ÓQE'pèçüj6Œ¡0ý/p.…å;CÞYáy X´ê¨)ö#z¢b(¥7cà"‡È§tg“|¥³B” {0\ús•¯^œ–tã†ïï‰�ί:ÖVyá/íµ*´žØzâm~4˜Ö[Oìn;¹gy³£Y´1±�‰ÝS÷‰ÙsÓ¨Á³�‰™AµÙ©Óg”Ýôéw‘6;m³Óíœ�^óÝoœþ�÷ÞpËE¹ñ´¯æ�{ÜÀ»7Ür1Ÿ ®þü ûíÝv~à}}�8$Ô†…'�.ÝÌÎÛ5q$Ä{¸èf ÷�€ ÄÀ—4î¼ñ¬¸H–”„Êlõ»÷ú1Q]¬4¹ç»ìäVïZÓe+/§$íñ#b.,<ª¥þoà„Û8Í�'ìÈö7ù)7ÝxãWÎ9瘇>‚¡‚`Þ?ÿüó¯PÍcóX þ€ó"¨"û’¦MÓÉ“žüoß{ê{dxÓMŸ;ý³Çÿøî­÷õu¢�&«a—ÅYÀº¼&–ÉG4íPâ.p]™$X:±ƒ,Ö9ý~_ç?óÅ6øçb˜)_ 1Çãñ¤œÉ|y¯é žöô§ï´ÓN!ïýáÛþàÌ3¾ˆ.á©Þ_ùò9ð¦×Wî²eËžuâ³aŽúZÔ ©LV4Bå§<å)DÒUê©ÿÿŸ~ücSϬÛ1ia~áÓÿÏ=ñÙ/xþ:çÌ/j(�†�Ç'¿ø%U„ïÿþŠ—]qÙåÂ`NÂNSöiûÏÏ=ñÏ?醮í:ÀÈÝV­ª"óóó^ôMdãöÍóÏŸÛÄk†nßðºW¿ê”?úÃïÝz³¨°Ô€GGÇ^|é—¿|n<äàƒÑfñfÁNEüàZò‚ç<ãÏxìc·b‡ÆãÛxoë–-ŽUñ0ȘÅëºÔÂ*htš‚IŠ„ºÂP¶‹ÌÉåà@øp$ÿñ~d‹I.¼�Æëg…Ô„·¢Ò€rYm¤rDQ”(òÁI壔2(L´ö¥â¼$«¼9Læš’ÁQëja„”÷G8–¯²4eŽrA*PyÚ⃤�nQD:5°”Èsaô–<üìÉ>BIµáEm}#«îcº‰æD=ÅH§KÓÎe‰0Û­[ÖL®ïVO¤lzp�Þš+˜©‹2QÑZôL£H*Ñ �D�ê'd‹=1Õj¹­òºØòò4 gi_ǵë‰�u9¬y‹Æ­vñ¯:*w@˜›Kä„Ôêê>ÊÐói)j)é‰6cêȆ‰iþ@õÄÎPd¡d}Û}tv`ánâ W$¥'bF|SU�ä;ï‰Dû©´êPEFORc9"г ü‘ms+Á¡¡X뉭'Ò‘mB­'¶ž˜›dÝ縷Ô�}iü÷^Swo½mLäöÉ}O8á.‡“6&¶1‘®Ñf§‹“*ç&Î~ê㬪ÍN3Çk³Ó;Ÿ�ž}ñßïî¶÷�»ì´r6ÓXšP;šîI ú4¶}Þ•†wÝ­éâ�ß×׉ٸ�4zœKXׂ܃œÅ9maÖÂý9¨Êt:k.ÏF†2�•eQœûöVŸçnΚ’5¬/Ñd)@ظ’'~.…I¡(R"±ÊZÄUt7 Ô 2YQf^ø¢“ßòæ7‘Ímôö?Úýƒ»ï·ßZ.¯¾úêïåYTÎ9~ã7~cÙ²Ù-›ç õ‡p[<˜†z r‰h{éK_öÊW¾¢2?òá¿þ»O~|ÿý÷ßm÷û}÷»ß½ô’‹k'8r?ú?ÿÇ/<üáõÊÃ;œ˜²³Î:“t¢Ø~ç¿¿rõê5<ݹyÓÜ…]xûí··OÿÃß=ëןKÏ줷fõšJäû�¯}íA‡rÃu×óÖ?øØßþ¯¯úÀÞsÉ%ó9óK_|ø/<â€Ã]±bÅ•—^öÙÓN+vß}÷‡?ê‘[¦ózj¼ õ�>Q 5PÆ•\CžƒÎ/Œ7nÚl(uÖš[$ò]–Tç`! 9+ol@!?”$¶p¯äµ xÖgªÓYkÜÇ—ÿëh5ðتaYŽÞŒànª”HDdÉPM�9Zlp!¤ŒÀ'ÙÚ¦ÝKà—ï!5ÑþûßZ wa™46¹äŸ>·ÚR�óe®¨îê‰T•oJÕHn+ùÿš�Ah6]†$ë'¥êYkÈIÐ0BûôñžTó�öD-ÒDZíÎeËUœÕzb뉴ÑnL´‘¶žØzbÓ Ú˜ØÆÄ6;m³Ó6&þôÌN/¼öóÌr‰wsAÔŽæ�{Óƒ> ï;—Ý~þõŸØ!'ܧ׉ìî"Ó%lݬX]Šº ÷ÌÅ¢ëRà ¡L V`"‹I—¤w< tÑ#T¬ å¢�$’™âRå$X*s‰€ˆÏB8LæææñˆG¼úw^ó{¿÷»›çˆëÝü½›ù¤P÷E¼ÛK_úÒG<ò‘·nÜ@É ?wP,¢£íÜÜÜ‘Gù¦7½å�o|=ña°Jã©Õmr¾víÚ×¼öu"QrÃÜæ—¿üò®?ùôiŸ*Êk®¹šÏ¶¥Ž=öØç<ç9£’<í{̱KÕAvÉEß*â¯~õÜÛo¿å¨£Ž>çì³ eûÒÏà³-+Þñ;¯{ípÙpa˼•㢾üáôò^,R–޽}nóh‹X‚'ô.øYÿYäsÂK¨>Á6K˜/ÜÃU¦Yã0ãc‚GjWaò÷'¹�FÔÀÚ+*+ÕÐ0˜ZóÕL´Ðe±�Rm8”²ytç² Îƒ”$$F5ú»ü"'rȶm¨¤9LÕ 9Ý!½:/RåZÂîh1å;µÄŽ;ôD8Û¨Ì+{¹âÔoªÛHŠ(��Øê‰”‹P@1U€$®U6EÒ¤„Ü–S—Vò6ÔùÕºvñšdh”Ÿœªèb»–œ&) Èá¤[/>Km!Æôâ‘,‰a*Ö—‚�®ŠwÖ5±S±õĸbÑÇøRG[Ù$µž˜f—Û} MÇ.Üzbµ�꤭'æn—^C3icâ�1;uÊ�ÛÆÄ6&ÚÚìÔ›J›�¦)ps`(æÛf§ÄÆÜ…uâwn»—ùœi;šîuTÃûÎmW0]¾O¯yù'·n>Ù Œå9�'Cðg8 nñ9Bb’Œ!ÜÄ9Œ÷)‘ "„(çU¬T'ƒ^zézš§1]‚û8U-eåJ:©ì(W¸FJ._>K ŠraŒÀyWlîhÞ›nšŸèCú¾÷½ÿÔSßóõ¯}íÆo,Y|ƒmqÄÏ}ÞZµråæÍ󘢪,ï-»ŠT$Áˆf�l˜ÛrÔQG}à8õ½ï=÷œs¶eæuèa‡žpÂ8î¸ãÐò¶M Æ®,L¶ì°bç¿øÅÇ=츿üË¿¸úê«–‚ã�pøá‡?ýéÏ<æÁæ‰ÑÛ6ovtgª7é½åÍoyÓ›ÞtùÞªê8ôÐÃN~ñÉ;íºËÿóïþßÇ>ö1§¾ç=<^zë­·.æ³;ÛÌãÿø_ÿõ_‡é~á;\ᢠÏà¼T®'H g‚‘²KÉõ¹±ƒ~Mˆ½!t�X3q¶™ÚŒMpÇÖKhS©2 yŠm„9ßDlzÚ³1Ä:l\Rm³*'C/á‚dÊ'ZÎÕ¶±T´%v „ˆTd K¨­^¢Å¸oXt2BÍ×kÀÁà4T 5–~@,! ÂFJN‹ô®ÙÈ- âÈ”…Úfï7øÚ,Ø‘ éØ&ÂÂÎy*ðM1‚M4›=Ô„¥‘ÄYAñ8°~�“Õxê[1í 8K–üédKö)´…£WJöU̶Ö`Þg·D’P7^´ju¿?~O´?Úlú“YNw±'"– HôqW4l±sß±'Z3ê§=Üò4š?;¶ü3² LÕ·ððýžð[ÿñ*Ù %\ê‰ByñÞ�0GhÝ‘�[ …¹)ª>£4ýê‰FÊ5Qƒ¶ºh“ûJT£({ùkhÅB¨÷CÃITW›±Um.™h1$hߌûã¡f¶ý� Ï„”I1sÿùž(ëø£õDïA­'¶žØzbÝܹq“ä§�‰mLt¦ÑÆÄ6;õ†à¤¦ÍNé ÎÂðÅÿeï=à,¯Êûÿ™{ï´íX¶Ñ›5¢E“Xc‚D…X�1/ÿ?E ±ç—(šØð)¢ DD:(EštdAz_v—ζÏÌm¿÷û9wîÎÎÎÎ.l<ß�½÷û=å9ÏyÎóœò¹Ï÷ç��u"³²<;Ývf§�óLcÙDÓä+K`‹J %dñ>¬×‰¾Gå4 ס¢± ÅÖ]\³tbT0pƨˆ]YÇZÕMš²ý%—þ��àÞZuYwOÀR5‰óLÈfcåËS<3ØŒ5ú"2ŠƒÔ»{{ðÜ‚ñ<—ÿ‘-—î²àÉèÑ£?ˆkÌY«Õùóç@Íœ9£ÔV"-$Wtww÷ôŠb×Mq,™ùn\tè k,££‚=eÀýî‘#»>ý©O‘»R®Îb~OOÏø ã&NžøAàK{V™Afxƒ´¾¬{ÅèΑoÜPî�Hˆ³–-[:vì¸I“'··ù2.[¹­\Ñ#dëôîru‡¦þð?Z¼dñsÏ?Ù3¦sŽ)ô–,]Yí­Ž=æ¨£Ž‚Šž;÷ñr¹²ãŒéc8¤Õ¶¨/_¹Š·&åßɺrãÏgfñÂé"&‰6RœϷÙ6☂M^!WõÕV£] "eĉÀ5�ßÀŒæ2‰-a´ˆ�1/¸� *ªM$ØDÀΜaa²ƒlƒ)¹ €6½?(%5 ’Éá,e·žVZ$­æ™ �S¢8HlÑê.Ѱ õ+ªeÎÄئ\ÉéBMlü$x$úˆ@•‰ìÑFÉ9€™bH^ÅI’³{ÉIQ6—wA†œkX¢ª %ë²E–4%Y}KM@#qt¯íC”4k8,II/̨ƒ¼C [b¶Ä¡ÇÄl‰Ù[ò˜è@”ÇÄ<&:ø:;pLϳS¦1÷ȳÓ<;eJÌìsXÍNÈóG–ÀV–€«éá¼NôE/«¬@ùfI«8y­�%{z n>¸ O—7,£=Hlß ’³t骀 ¦ñúBI£,ñ¬k}‚„ÌF>ðÕ%K–»–…š‘RÛ 2ФŸØõV¬â�?0…R§KùJÅ7-Ý6�¯à&FxŸ@dn¸aõîiÛMÝA¢„§b <ËVu‹¦´ù6$Ø ®k$±Q)¸GDFæå-2÷VÊzáÕj�%pHLkA Qj�R¨)>€½ËW”ÜÛ�ò @ Þ¨# ïOâìU¯­ZÒíû… mÂl½œCà‡w5eQCZ%Ý"«Ä¬G |ž¡àS4`õ&Äydlh½�¸Àç�¡ñM!Òñ!ñ€Pû@KŽW‡M)ý”‡B>VÒkƒ$Á¢„²|Æ›‹4¬\‡ÂZJ¢¡Zä· Ú#R@­QwkÒ•§˜Ú!…Z-JÒ’’’YÒ…ì‚)J�*C  CHQ5ã2íÆX¢�RÛWDÚh‰ñû�hw)xA>ë°Dx–mÊ !ÊÁ—mEÝ£=ù–@`TˆÂV„«4 ÒVê¡1Z¢j(dÍG·ˆ¢- ¶eµÄTM H0™É²E¯e‰ÌoäCwñAR+aŠC¼š€ïôäÿ³%*D•”-Q„#¯ÐÑ5ÆÄl‰Ùó˜˜:ú<&:‚D?á\7�‰Œ"v È"ÏNóì4FPtaÝëÄ<;ÝÆf§Ñdù#K`«J€%#åßubIh†%³«c]ÏXÇ…¹ÀºX�2{ôµ@¼N¬d¸Ñ™9•›s¹ €¢»ÈÞ',œÅ$ÀXH“°—’KdÜç"’Ô1õ`}ÍNo$4-¿Y0ˆ LK­,4U-BLŒä�´t± Ö*VÖ7`ò„N‡oÓ±C“  ç!þö²Ë~ð¡?̞ͨƵ´Ìš5«½½£ÌN]N| 'I\Ì�ÌŠ€ ev¤².�&�1Âu‚ÑiŠòåfbí©¬XYà¯â^j¬¹ F>9„e�X Áå‡pB ãuEé‡P¶ž…"™üF•ðN#-E!uø¢1`‹º#m0:ÏúÔ[‡Ï$LVuO0XKÎ~JÍ�Ä`Gȩ̀€0H­—4õT²š”¥·¾j‚‡€n®á¢ºù)Âfj�(<¡`Û‚B=ÜH#¨’2T€²H­ÙÁ Nˆ%ž«Ñ P+©> /þA‰Œx“ !hÈFˆÄ›Z±Š Å/DÄ)6þ%ùÂ"ÆÃÍŽKǯ�2©Œ8¢˜° ”�ê9O�HžS�3ZVšÊ 9›·ïEEå/÷Qˆ‚±�¹`¿ïS‘ŒÃ ÂŒÒô÷,f—Vž­Ý*Ô{‘šÊe3èUI Æ`ÙÃÍñ£Œ ôJK´­ªeK" Ä,6m2EB�×¶DD¦k"²Ü`K¤ÝUª°(•)š…¶XÛEÜeÁ¡bk["ñr-—-‘ÖÓÌ”ˆfãM¶ÄmsLÌ–è¤Ã®]Ícb�ÂV˜�fKÌ–èt'f: ›yvš-ñå4;u2˜¯,�­-–ˆÃz�ˆ×›8�kM–Æ€b<»ÈbòââoSĉHÚˆäD(’wQf¼‹lg¼d‹¯´J8ThŒAD’¬‘.rÅšNG#‹‚ 7¢; þâ�Q„�v@¤Â• a0#-ÿóI¿&B¼R«]zÁ…‹­qþéGÉ.lÖD„�•¹«Ës•Ý "›�¨ÂúÝWc‰ôN˜Ìóà{x�FñÌ _£ú’Q.�0jR®@Ä(J:}…Q uR>  ä‚t<$QÉAÂÈ‚Vp$Ìiò€vøŽf�L"aXò)`‘ÍÁ_È’T”KxjA 1g¬T"E ù…*P]4WäYÉ««#Ò©ÕÛ|­1(jFjËÆ�›$ØàRÆ¢µmQÒEƒKA6q: ^Û"ÌêÒ)ši¬Å…´n|’E‚â”8 $^.%O}Z¢�àh�ÈI¡6¸UST´ŽÉ¸‰"aÒƒ¾FJkeÉ­—©£Ž’LA~RAXHb @ËæG5�àôF¦�©¬ìÛ/+KŒv �S$T6µH4L¶D•0[b¶D»žFoBb—H@t)›nLÌ–˜ÇÄ<&bgŽâ™³‡­2;Í–˜-1[b¶Äêé‡ã:Ñ>4_Y[[ñÑñÈu¿.;�†´êoÆ$¥¦ïU `QG$P7<¿5\83°Ó Yxºá7%l‘Á È�Çð{#aøL §*z$Ÿ+î€ J ‚eö>d¢�­T=⹕àux«( ŽDË º™qQ øŒ�QGGGdö¨ÐÝvÛý³ÿüÏS§M[ºb�™ð›>G¤šØõAƒ 6¢vIû±sØ*<}�Ó3d¡„Ÿ²&¹È±ÒyYY"U¢r\­Uœ !p Ñz V Bs aºJ¹+7t&‘]˜£ï°!#ÿÄ/Ld*‹�ƒ«p!!ºBD|F$A¾�'òÃK‹ G¢lóó™Vö²'6�õ–SNùɲeËps7n¼L´røi´,Š}ÀIÿ‹ó•IÒ²�sY�´ƒ¾ ( b‚> ¢VÑÚ/Ë1P2ÄF5 HabÓ)’€|¢ ³&&{H9Ìžþ‚<KÎmÀÜLà–š ˆ‰R½"vJù½§T¹äl>‚ ö>£.d!QÃÃ1šR´“ŠàôE„“ÿODÀƒ¨ ¥mu#S’Tr¦AS¢tŠÒ’€Á†2²‰qyз1ù° LM3˜EÕA¹<ÒBšDÁ ¢J# x¶ ÞÜãÒ“OFÉcõ!j ƒ¢èh kI¥�McJ`»‰E²Yiâ\,ó$¼®I�ñÚq(´¡!W‰#¶(ÓVŠûˆµl&I­ŒgŒ( Q+R´ß¶sH€�ae‰!Á�¡â¤BþW~V3>¼Â좪eK MÍ–˜-q“Œ‰ÙíAí“é|¼Ícb{Ô†\™50‰q= S›ivš-1[¢]Qž�b}IÚa¶D;G'gʱ`S$.Ž�0ocëÄÔbù3K`ëJ@——”Š=*wÂ>È—F£ù¦!Eªm ±á…SvµâP 414‘„ø ÿ$j‡g”0Mø¥mÕt‰—i�79©£§ØC°—–X„’°ñ]ºŽCleVÃH „´@ —&$RUzœJñU:àþm$ŸK^ƒ€%"qX¬‘V7:‚c¤`Ç; ÁIŒh|þTö0 ‘<Ô˜”Ã8ÍG>d Í|ƒq"­�E†€£–úˆQCüúŒ%I¸™þZ’âSSf8º�ÐÂBÒ‡Ì@nÂ:!9ªglúæ§­Õp�ãg7ÔÌJR¡B먂Båµ¥]/¢Ã6ªYtr«³Ã eÊ»zÞ«Þz¹•hO÷ê#aÔ¹•mQ6\mÄÆ{Ã6¨ìÀêËËV²T¬°`rÔÌNJš-ÑN([b¶ÄÍ<&fKŒ~=�‰yLÜʳÓl‰Ùóìt[X'fK|©–èœ5_Y[]ñqĘ˜ƒÐX€˜†h‰7D _ˆ7Ð�o@‚ '–~1 œÍ¤Ñ& Ó˜Ã×2Eu€!âÝH!,¨É å¸%L,Eß'�~èX ØE@6ÜF1¡’§­­‹—/7“ÐF¼Ñ&’ÏÖÄr£Fö:Þ†"o~ »#&¨ ‚÷È•‚T"¦(â5I«ÛäŸA ‰i’¥²B‚0-¬¢œ#»%[³‘#˜è«júŽÊ™J¸HðËÍýM)×fà]È' ž2ZÎ…wÄ‚Òa­6žùBHÖ-ÊKaÑIL•�ÊÊST_"gQcß(�R$h9Q˜jÃ¥Te-äã+ÃÖQª~øÏÜÆ'9xeXùñ›Ë*rCVÊö–heS¡a‹ñZG„bíè”Y’.I­y¶D”-Q-Á>T•l‰ÈbcÇÄl‰yL´3Îc¢ãŒƒ�#õÖ˜�fKÌ–˜-‘9¯àl‰.ÅðZ'Fš?²¶º\#‡ OĆ×nðÜi)”Aoô=3Á+ݺ­UB±@ÅtJÞ?@jt $=`� ^~€,ˆÛyø ïÒÑXNt¼-mf©WpHsõÞDáD‡D‹8ÆÀ�NEüé¿¥·Ræ5I°F'š“9Ešˆ d‰×6IBJ¨E¼6úÍárc©¢.V’بœŒd€u-‰@”$Üï|CQ0@ö£‚¤Lø‰,%�K�`@‹pXãtMNt öÙA-¾­"®zâtmJ@Å%îNrõ²¢€™Òs1ÁG& ÕäXÁÀáàAÙfô"‹8Mp•WPüÇ}±Î™®äöXT•)�ÀVà€-íðÃÙ¦4dÈÆdº M…>kj˜òõ•@ÓÅVxÂm†È - ‹ˆ†+Ê·hÉäŒÂl4¥Þ€Y`ÂÔÐÒwÌ#c©C­ Î` Öd:"N:¡ ášVÜÛÂk‘ÈØ0MBÖ,��6 Û†A�üÜ“Qª}œ¢‹8õ}‹™6åV)Ç7O�ËR‘6£DÔ,¼Íléúßê�™À˜A•äåà[ö›“F6qÈãP"åy»dÑ×/²ð™jjœJFd_ñÁÎËГ†ÑRÙ³%fKÜŠcb¶Ä<&æ1q[˜�fKÌ–˜-1[â°]'ºÊW–ÀV—@Iw.VëñáM:�R€>xëP\!¡¡ƒ5$$¨xB€%†Fb^-â` æ‡Îçòñ#p¡ÈEAÐ!-Q�FcÂAø.ü˜SâVœ·ÄER‘¬€‹Ìl˜ÑÂì#·<Á�pëK”‰OÙ÷^R~Å_¤æÃà wbA*¢ø@ Ÿ2‘„b¡’ÿiÔ"!+…§ÜF¤Ö½4pË´ÐFæ" J\$âLÊÓà � ½t÷²þÕ üâ<ØVö›>Áï Žñ-ã˜Mj%P$ùÀU ªT¨‚®Y‚ìËæÇ£U-�(™Æá�úaé鯓¯hêI4¹·h‘×W>嘓È'{˜QsèpF+>_ü¸VoCÊ„„à7JÀŠ¢Ô�Mÿ>=ëBÎlbF$¼Ëºüeƒ¸«„%¥ˆø /½@Ëôf¤ÀzG8Oâçå_8.‚Ð9oÁ9 îƒÇ’p®j´1§”¢>D[AŬÅÉ©šò«+rK£lñ$ªŽ`Q�ˆé5#)Õ¦.B‘zíñ×(ÊBm>ÿô›´4À�*ê?ŽçDAÁÙ(+±Bã‘){j.%µ”C›©ê2£F�zÁšêÆŸö`Ål52ºQ!~x±"(´áy'�l‰Ù³%fKÌc¢}&�$}f¥ò˜˜pGÒ<;u:Ê#ÏN±Œ<;ͳSÔ fMy�ÈÚ“%Åæ['º„ÉW–ÀV—À0_'òê¨P’nH ' "@$À‚j m�ÍÕÊÄœØÎÎi��…7ˆî@G|‡©³H†sçIÎSBKÈ%ÚA6aˈT‘�<Â5 ‰Âÿ�à(CܧAUÔƒ@\-¬#Ê’;.¿Ì#wé1E4¢›)|}R$ŲÚé�Geï[B�þÓƒ` %Y¦>íêdI¢k–SŠÄˆ±°è¥8-~ÍlF! n‚d#:NîdMŠXÒ‰ãgøZR¸`!öAV2•hŽB:Ã"6`Kr l™6¿…šÙïh½oTÈTÂLºAÊ�·æaø-¹DÂ@kŸ¤ß”b%ÏTR†" .-+ª¨T3/4X9(�(;¦S”¥“�žr¾Žè+ÄáH"�osFn¾`D~›X^pM,áf‘ƒ@âN’Á‘ ”‹ÿ‰—^’•¤} ²¦�=ÓQ@´|ŠD’ ¯\ʋ˲(Ud ©°È„À¥'ñEjoÌ•-1[bhECWQ’l‰Ùí*øËc"Ã|ê'ó˜˜ÇDÆî<;ezää!ÏNCž�æÙ)«£˜X;�Š9šŠóˆ‚¸&#<¯]w¸Üˆõ "‹³7|�¨©å+K`kK`˜#6¥¢{Ryü�è‡Ðß>à#&¦Âa €«ø·b úJ1ï)h0‰p†bÂßË�Î ¢CVd•ˆŠmÊ ¯3•€Še¥éØ I,ìø&GœŸ €� /t›ì¹ç?÷üÂË,¸Ó¡«Ä6k‚Zš¢CW3œŒâDòDžZJ jï"„ýZì‡�¶¨El5ˆ,‰éx9Y€_»sƒo¿½bE nÉ=îxV Z”›¼˜ða‹T�ÖD1#ª$#£0€x¨D½¥-p ×X8ŸQaOꄪƦ(rpÇdË'd"ªåÆnÈ�ÍÅ?Pð,¶p#cPh3ŸAÊ^5…;�zܪŽw–a{ T,NéEÍDÑfWbÖÆ ãXG<Õ³ž5<Ý;Ž0µ‰$Ût†dï4•Šš±«�°o¤²‚l!rá8ˆ¨ŒÌ.Áß3ÙprÕz›mÌq ”PÒÔŒ·öRT ÐP¬NQI?X$³[†ÏY4ªüÂ|ºá©øï¾zèô`žJÈåX� ¦‡`˜|¸PBŒR ¨R)l2øV©@*·´– êÆŽ{îóg[*“ˆæ²…Øê.ÄSÈ–˜-1[b¶Ä<&æ1ÑaÐQ+�‰Îòì4ÏNóì4ÏN]”Å,Û™¹³mçü.Hó:ѵÂqᶹ׉êa¾²¶²X‹³,g °Ðú?œÞY´ûH’ Ô…ÚøÌ*ˆHþÛáÑÕ%€‚©qI”tDtŒ‚t ‰x·OˆPÇ—÷H „`ü#Yàôœfs9ˆ7 l Z žœˆhÙ3£°/FÏÉÈcvà™èsH\“�ˆ .ÜáMp™î%("G’ôSz𠯦 ÐP˜Ç?>ô4É!–+%ŠžÄ^¼—xDÝ—ÔúD‹à�:ˆ7ñe`Ä(" #EÞb4@Ž™sZ¸ˆÕs*ÒøI…"þ#C$$ÒDX°@�fAI….ƒn¤W�•Ž¡òî£eõ•A®„,KbÜûQí­Õô/’’ÅÔÁ§?Ey!ÖXY‘ ´”0Š• ¯,S6¸åSÞdÍü¤ ÂÐHE’AÄÑê¿�DŒÇ©ò’;húa�¢­Òó�ÿ ÞIŽÛB©-HJ °c„7TÍu�æ¹£è¥f“$(%Ð[ÃõÓ²,Òr Π’N/x»E­H éàV^eFÈOFŃçà)[¢‚�Ü(�hØl‰Ù³%Ò™ä11uðŠ":cûcºéÔÏæ11 JyLtœŽI–ã{ž�æÙ©³¶<;unÌ„*ÏNó:ÑUP¬�Ió•%°-H ÖÀ1£cR§gËðBlJì—Æ¢^%'¥" õrÌPVSŽ`à@Õ˜˜$ˆ‡]ÖD &€k®Sć‚NàfÔfñøP¢É¥¿šŽH�(€”pÒ&îs”Rà KF¢»À ¿ç âÑÞQ/cùrÂŽm,¶é j±Ó?~kN¥+ú<‘ÜÖ_/k#²mY ¦pñ’fàtø7§°ãp‘U$8-Ûkq&¹ëõ’5m鵺¢@ÉoG¤ .øHIrc‹5JÀUË­½Ø²H¸Å<ü‡œü2Eþæ;T^Ü�7}Ý�ü­½ÂHDeoµÀ•Ì-MvÂkaû3Qãkèi¨r╊Ä=ŽŒÅjK›.oœ¬êñRÅxgVBæUÁ¹ÂòKPÅs?™‚¸ ;¤)BH¦´á¬õå÷j14y F£1(ÇW¥J&Û•Úð!ŽÄûÎu�êL¢¯^ .˜ä¦QAAwÿe×´ˆù~4mCømBT� (Ï0J" v“ g¸"-ö24CüÝPɨYuȃW·×C£*$ÅKI⫝̸”Š=CŒÏ&w7”NT[½PgÃB 4_-�D ®P(bh›Ì�L…8èUÍ¡®RQÙ³%¢jÙ³%æ11�‰yLÌc"3Š<&æ11ÏNó:qY'ºÊW–ÀV•�‹ì%X¢?Ä&huáÏZ/à°� �xZªMùž"a  ¤±ÆD’ä%à­SD]Ì%/žHä’œL@bwy³Æ›— ¦ñµBHÝ?C„Jà"’R !�‰­Yœ´¸à b' 9Ð$ƒIeŒþ$ ÏP–=›ÌK[šv ‡”À‡ÅHÕLJÃl£|÷ò²Â`"ä†M—±°­(HnúR²ÈŸJˆxR‰Õré(Â#|d™ˆìV2<[iy &ƒ¤OV"£4›)@2cm­àSJf’ÝDÓ[)¦f%ÐË÷&e�¤IîÔÎIŒp$1b£R¦�÷V-CjÂ…æ•“HœÊaàd&³ÍhWÄòøŽ<ð�êD*÷}Ÿh Í¥¸µ”¨wK+Ç5HTâ’õ ºîœgÁâ¼!Y#””¹T) d²�㆔äKm G«Ž—ýšŠ;? daRHWÐ"Ì&"³*åˆÇqÏ /ѤÊ2 ˆM:Ö˜ô¸mZc2È–¨°‰M¢¤m¥l‰ÙÕ‹l‰vvyL´W@*E£�Ècbô˜SÊ%�‰ª†óÇýì�õ!›<;ECÂl�O(Kt¬ñcnž�ÚÉ"¡<;Õbòì4¯íò•%°•%@_4¬›®M Ô¹JÂÄI¤‚`ÝÙܨ à#fqÖµ"¡2C6.=¢Þs •À�’÷šD}=4͉E!\3 ¿´ÖJ”P¨TðìbxO«ˆHܽd&Só"?FIW)° J—a áæÄS%œ½]Â�ÁÙLÇ.1Sûò+ŽKž[*»pʉ« wVTv(^ºQ1�¥ (x8éÈD…(Ê‘=„"1¿¹t£ Á*7û’a%Ç“5 ,¨´á6f8VÙbux‚‚ÙŠ8Ø(ÏH³ê FšÁ‰8Å ¦ÅG½bË5²˜+Êi¤º.�xú‡“• A"�¨@¢µ^1 ª®ƒ˜/nÖkå`Z.PŠ‚—�ˆSœ©ÊY±P/ÓÈp#“)Ý ¸«Eíá¼þuÁ�ŒÃy�)”&LË”ŽL+9¸ii÷ÀXZ�U"tˆLD‘†Õ¶…Q"ªiÜU­P`×<Ú(ÇûJ´Íçj-ÞQK#—h´HÌ.oTBœPÂeë£'&ŒµÖÚ J°ä�T\Ç%“‰DMŠOh¥ÕƒªmH]ût´E8ÒµAˆD¶>…áu!3uV‡Hlúl‰aÙ³%fKÌcbô§yLÌcbž�æÙiž�æÙi^'¦5Æ–ÓÒ$f l] ø [Ä,%�� ôÈ‚¸FÂ͘èú£ µ} –`^|\XcÍ^¨‹÷�Þ`PüOí"R¸ dVG1ož/ êÿfð ± krÒ{’§à“Fƒ*=ù 0Y‰KÐ…+RƒTE@$BÉjr(“Dòæ51§2$,g8‘,IXNä3…4"”ÄfŽ+å�H0&)X¸Ä$d³<]ú¼�q'†DéBU‰3î”-ˆW¤KWÀIr+F&1“óÈ@N|K) qÓ�G6 g–ø0%€WTL|Õ$}qfi4e¤¹›‹L®œD/ë¦ÔÏÆCrI0*ŒºAÛˆ«QiÒƒÔ ÕE¡zO’¶ÁeNIõV„*&>�ùB¢n¢Zf#ŒšÈ†p :�"PcHš�À+“G�/”j\HˆH±bJ‚V|4dBÝ¡?ÐF‚º¯ñIB˜“VhÏ¡À¡EV9˜$:ÚDÐH ,Q |ÓJÒˆÍJ ÿ’H Bä<‹I¶Ù£õBzÙý�![b¶Ä<&Ú·ÚùFŸ�ÇÄ<&¢胗Fž�2CʳS”"ÏN�fÇ‚#ÏN™·çu"k��\'"Æ|e lu °Üx„�X‰ÇRÑY@„¦ÙabÒÐùÁ¶5&–Ü×JcĨÖVÀ ·#vX«WÝä̪UÕ�Ç}¿DÍ8S“mÆô¦Âeß3ƒü’‰Oȃc6E|P<”T0�¤H°C¸lù'ÄÈ�«V@L’Mùõ×¢hä ­5¶ÛJ¿ñØéºV(‡cÀ$ˆœ�.þLG‘ìðå®p°b5prß¼áǧïÁêàÕŠ^HÂa:3±ËPA>0S{"ƒ°YÉ�Z¸ëÁ€Ío5–xM0\Ó$C¦¡„ðÕJ·lÿEÁ5¼¬`;6s¿3öm“¹�8“·4‹=ňÁC0ÖJ$ø·Zk�È¢|Ò ¤!1ÖS?/eîžv° ¸ÍAl“8]1x¥íÉÜÞ‡8�锈ï�r ~m… ˜-Ç8åY¤ñùò0Pdc‘EõVÊ�ÍyŒ¢ €/rUy7µ6cæ¬Î®NÓÉ‘ð™­ÿ n‚�çù³øBº¶¢,BtESZ c<*x�I†…Õ“àO2hzKeƒ*åKBüµVØ{� Ä鯈$ ¿£H2T¤Àj¬Ú¬ÿ=P€Áž…Ë©Ý]O­aÖÊÔ‹%Äê&‚‚%EÛZ6Ön–‚ åš/{…‚æ”-1[âK°Ä+—=÷ì³1´øZÚÖRÑ5bûvô½ -'FKE%1íè#.[b¶Dµ$ºÙ<&n–1Ñ”Ζ˜ÇDí,ÏN·Öì4[bž�âYs¡l‰¬C^ô:Ñq,_Y[[,ì‡5bÃkœ��$wñÖgà   ¼Çz�)£ šŸ@ þ¸ŠsÅ"*¡“ë7»3²šj`fâ2 N²ŒÈb6a ×€†ˆÐDr)ÄÄÔ5?ññ˶É) 䞨1%_’H¿èn�ͪ˜QL dî|…ÜM–å”2ÄŽÄu‚>$}™R8È*•IX„6¹¤É?þhiÒPŠ/BÑ(¬AJÆ-á)€"|]S~âèÕp/ß¡ ©O’$…HÔC†�– í>ÎCJB3®�ѤæJ|45Z‡`ùLŠ$Ò‹H¥ËoÊ…`¢EÛŽæð]^X*‘G’öqèäx‚ä$n‰’»óÎÙÇw é\ÿþõÿÜm�=Ädû®hJ¢X1°Xï#…À%Ç8ipCØ&�µTŠoCœàhJDÙ%>S9°múÆéTÀ7‚å·ïvR˜Éù§­¿�ém0I×j®þ( é7¤ÕçѼIJ „$S±¤méR•±„w±C¤D‡¡‘’l’A}m™-QAgK\�%ÞvË­?=å4jÀuÜq?š0y2*…–©Ò(aô™IÕ²%b“éRɲ%æ1 Ù5¡N;Â[�¬°A†˜2x*)º¯A«^ �²ÂW�ÌøFY˜ð!&âRÜTœd ‹†Q=Ñ lU�­"ŽR³�û6®”>,ºÝÉ�§�Z_°S¾+…ÈD!)E_CcÈ%³�Ê¥ýæb6t-x¦«…ñ_Î-ÝËŒ‡üÇ™ Á�~ƒl(üÙ«´ôêäòõUe›&6žéKúôÅÙ V .c?¼j�Óu�@-¯•ZKùŒ¢ýâ"s… ¡‚…TP½IÍ+Ä€©Q/‹U딪¢n-y®iè�ÖAþúCãÜq¶)÷´”�CíUO9…íBõŸ·*ŠÏF€pe-Á¸vµÝ}_Û̾ÐL‰èf]¢ºË ÙœnòP\¤åþ²%*ál‰ÉEfƒ-]BÖ¾ÐØ.Ì8trº.:‹æk$š4Šž-1[b·Ì˜Èl…¾=[bóìÔÛÖ›�fK̳ӼNܘuâÚóÌ’%°å%àb}8#6x¾ë~Õ‡µš€¸�K«9>} A0Ì1Sp¡È þÇë—‚{sfÈ+gAðÞ'¯ šMæ8�Ì&�Q¦ÒväBÔ TÅp i9á‹T¢mqHœø‰ï‚z¼ó‹³Hƒ‚…–Ò#D¢œXÝ‚0 ãNa‰–ùá4%ôdõÈrüA*%á Ìz9+ Ë2†œrê º¼R*¿I"UpôM Ò#úDö€xK…; Ò ðIÚ $·¤/â(ŒÆöp/ªJ"¬‘�ï˜K¦£!Äë-¥6¤2ÈÕ.mVÐB `]‚X0!úUK»×Y%J0u;·jq18’UE` SV”@:ZVMEURA„F±¨'<’/¤hi’Œœ²É‡Õ�Œ$MÏFš‚²L&™�˜9hmE™JЧx&IºR E6“HNÚÙÂP8ѸÊÒ&Kí�-1)ÑZ–X¢óì*”ôgzÛêUlm{÷N_*Úe°\ë]YY´¸ûé]ÃÍ iþiÐXÔÄ m"6,Fbc7V‰BD±\Ð~ˆ«íª£„¨‚.G‚VXÐ@H†h&§â � B �Éð±”!:áZxÂ"-L«À=\ŸÃÒ0–ª�ÌLU)4™Á`W±ÜË�ErÅ{ƒ  BÓ*’v™I�é“âEDˆwç{RšÛ [Ö×ÚIŸW/!D&S$¶,P¶ÍO¤D©‹�"GÂ5FAŒ˜”�;+ʇ‚K�A‹ÌþÔGv i\Ê%²[®X–\t¹Ir�ƒÈhZ²(ˆ ÒÓÝýÅÏ~ú…Å‹ûèµ|àý‡òÞCm+˜–RüV!£‚L¤¸ŽøVñÅ ™¼f*ÞÃCä)òþxõÇᓸ‰�A†£CãU&ÉøZ!G§²Ñæ*±Vœh 9þbL¡Òç Snv"Myg MîÖÖZªµËz™Þh}É„ €DÀÙÀ\jž1ꢔ'×’f0˜®×ñŒƒ0¯k±É®_œEJ±„û×RÆ‘¤¥Ð¦‡q�àÔWµÀ Ç,�„UÜØ¬¸Õ1DQàH¯¨Ru œ8†�úé V—gsX+IÎ(ämÏ–6 Ê’–pý%ú:e$ãÖ~ë�ýaE„÷`¸v‰¯ UNk¥u‹�¦Êdˆ ‘®a%€¸×)²øO^‘Wþ« ; ‘ÒÒ#‹F'ÚUþ`—US&íâL}¡h�GâEXˆÔ[:"e…Ä„†’Dˆ$w.�%Àݨj™KiFξ�f‚MAÝÅPµå�¡\Õ‡Ê[ÛÝF ¯7eC€œÁ2I§8Â%…†F"£€ƒI°} B™DàjFæ(Ä65«X¹¼³z\穲ޥ�ã`wÑb�cNñ#ŒBà‹µÄÛo¹e€P±j¡�-q [¢ˆ6í‡ÍÇ·Û Òêåo %žwÎ9¡+ý?臇²Dõr°‹þ ÀB0R5\uÆ€èMÐvô?”=[¢V­@6‰%æ1q7Þ1—M6&Æ�-qKމÙ³%2;Í–¸Åg§Ù_N–8ØLs„1--¯ª^:ûô�øµ&¹WLþ‹+=¡ØÎŠ­¶ÆM­ÒrÀÔöšýè5KõŒnŸØ?pJç^•J½È+pë Ó?ñ¾çM«žå¼¢·AWgiÔ;fý¯™ü®_=ôµ•Õ…”çešhX#6ÌNA‚€£XˆñòÄŸïè±Dó  ¸XÁûp–êó—2…˜Q)ÈK:Üo�) �÷/�—ÐQ ÅD&Q§ˆä.…› jÚHÊ—àeAX.-žL²k•­Jã‘2ѤÚÁ»YÁ€$L@/ 2i¸+{b{ôSõ»Øp ‘?ñUcýmmC  G «%gü¨ o3ðHîü3‰)”Ÿ\†™:ÞÇ%�b�õk4‘)Öº,7ÈÏhF µ‰ÍÏ�m¹‰whý”M˜u!’vS|ÁžÁòí` òJRAàÜÊ?A:µ!Ò“9Hm rf�šˆ�þ£N2¤e[’BAXÅlˆ9U™ÔÄ‘ÇB9é3p)wÁ¬ì'YP›¨!j¼1–8çÑǬ@¿«Íwƒò´ñBtÙ‘ÉfµDµ1zI›"ô[íBcÔ†øÒ»Wu/[¶Ì¼ý/•T -²ê^?K Êýs¤{rØâéÏj£Ú*¸ ™-qóX¢}6ÒS§ýó˜šßÔ~{Å-3&n¤%†ñn²1uÈ–¸…ÇÄl‰1mq( ë÷C+Ì–˜-U°vy´Ef§Ù_V–˜&—›ú“ij¥Z»åÞ˺{V6i·;g�ÚßÞ`kúBµc‡±»ö�üß«~н¼¼²g�Yô³ æWº…3ú§ÜîሃþºW‰'lø5yä¬Ú÷Ô×L~ï¶W¡ ¯ÄƦt9‹) ‡èÈ„‹èl䘃7>&°m!6ÌNKús¹NÀÑIüŠ™j@>þéèäT¹1^ ÏéAÐEôÚ ÏqL8P îEBŽt£ªÇñÕðLƒ¦[d‘@¸ *(”Ö“à€œ2é €{[k•C`÷ ‚ªEüƒDRàŸ4rpD%èz�j)c=�6 a�¥ÄÁ�7H¹à†cZÛØ.Í«)ÐW@Ì2€öV3;/Š©ÔžM  â@EQ0²–j;Ù¶H‘jŽ8I¯q\,uaÏ/¨�I8}Óyx$&±*ŒTi‘¿ø£¦48š‡qº9ä7¤èQ ±¦“Î,2¬HI˜0÷ÑG)¹ÿEªØL�hõQ�.8ŽpEÃ#Bn³šº¢QK…Añ¶ ¢Ò¯¿@¸.…SÛ=„²÷ÖÊ:‚sEÉ ý�»ôÜŸï“F!+k�¤g%Øâ�0åmãY#Z lï~IÕj8êÀƒZ |ôKÔ�Q)ÃôZýÍ@¼Ø/C¬ª¤m}îÑ ÀLZ`›£1"{‚¡æP™T95Vþ5d`닉QjReUÇË ß¶†„`Np£SK”¨ù^Š%Îyt<¯uyž.ÞtÔÏ�Ù‘Èæ±DZ3OEE‰ìwhÚd¹`‰=ú¸­´æ¥Mi‰´fŽÆÊÄoŒ9šª›ÑujüÒ‚&dKÔËtS[¢2�Þ›–ùÛa(þl‰[vLÜHKÜÔc¢Ó«l‰[rLÌ–häÄ” ÀÖœ�fKt´¥%œ~9òþ±ÍN³%¾œ,5Þ¦Q,µ¶w¯»ó’w¼áýÍ"˜ñþ3ﻩ×f`ºá‡Í7ìp¬fø¼§YQ^8ª¥ã´K¿õéC¿å/Í-- —F\DSˆZ�ráÖUá©ð–増[ò&Ç%Ò² ¾�Öb4äÙ `&¸O6V c¸u¹zŽ’(Ù[^åBF²kV(�Ø—ãž/J„k �1©šƒ �ZøDÚ˜Eu¢‚°·�°¸QºS�¼%f¥ßˆ Â…ÖysçE¦5>³µ7iˆ_¢rgHÆ*FzÀƒQ>Êcä4�‘ìâf?¾ò6ø‡8É#.•Õ¿+”`¿‹d ÎáÂtR£48Pçä�˜Z)Úh³Ø\2%óêÁ$ænÁåä=ôÈâdعŽÄ¢ÈàÏ€¯z†ñbrZ„!z)™[aƒ\�EwÍ`Ýõ#ªe,ä-ˆ>åCx-¼åÕG"‚� Œ48 k°D*U4ôÔ‚ ö’,qÞܹrÝï‚=Ñí(™ºeKÜ2–ˆvø‰‹ãªp\èFÒÄÇæ t_5?Z2¤%šf°+@k N` ºC^zãή%ø#.[¢�´©,RX]Ñ9zÓ­8&n¤%ÚuªÑ�nô˜˜¶2È–¸%ÇÄl‰¡¼tðÙíàÓd([¢Ó�-;;Í–øò²DÚsÓ_,ÞÚÚ‹#ǶŸwã‰oßï}͵܎ãw/T»jµîôv³`úµj¥öšßÑ áæüë:j|{[WáŽÇ®þû¯½õu»½uá’gxúæíw],t²f‹ÉyÿÛÄ=ÙÿZ¼äù‹oú9G.j§¾­çX¼—ú�ïýÒ!þ‘¦dèÓÝóß~<ûCž:ùÇwыњ¨—ó´a…Ø0;u[41,|c¨hGl�†‹ûYÑžàO¬ÔØí,ÞÈÓÙ"¶êb".>¡Àø.¢ žH*ÆEO?@ˆ:8ùû0ˆȈn,�·E ì�&&ÀþnPò¥j`±è h—¥â®W ?/êSŠU°þ>”G$Ÿ¥B[ƒCر¢á‹ƒ³y}×5%ƒy#EY²>® Óò4¨S<Ÿ`_X(N¬L!u7ƒÞl� °‡¬lwgqÖ ©�_æÂ31aŒ!îÔ[2ÑØ~ &iÂ¥K–ôöv|²¾~&‘ž[¢I†¢@›�LróTNjÎm2eBL"ÆC!døô¡&þÄNpúGÆþkd ÒV¤)–��åª\^e±–fì’z�z«<ø‡�ù…ÜÀ°ªœ�KÓ[!ø'«ãh)IBpPã+¼áÓmÙĶÈÊÎ|äj'9ÊNÑ(¶oVÓ¡É�4 IKAS—ÇPtKxÑ�q CßTê®RÈ-C,¼ Mr�8Ô&€�àYNJzކV`’#*…x‹¢‹0jR/Ñ/Z<ˆ ªe7{Ë–¸Å,Ñ7À±» ‰ö忽$½ëZâœ9�%^ã3º[µ}r×6H®a‰k$î÷€J‘TTnxCËÔ[ûË–¸,1�‰v ^¨ž£úÖ7Þ7õ˜H¯�-QµØ2cb¶Äl‰hÚ`³Ól‰[tvš-ñeg‰iˆßÄŸLL‹¥BÇÈR½céœ'ØyÚ^©VGoÜñ°›ž;ÝåVÿ«Ö:¹}·Q�ãša=½«îœ{åÔÝÇ´uÇmW]X]yãƒhŒ�Ü9zRG©#M~›ÉWßÙi=‰«ƒ¶êï¦LÚqDû€ V�õrO­ÜSe#¼3.ûîu³/þÞ¿œUd%×Èöqo�öÉkž:i.ÙŸ}jÇc•ÝÞ_Ì/È€h¬?W©µ-ö£g]²E¯*ð�ë,Zn˜!6iv È&Ž`¼¡r‰�Áz1`}‚ \À¢6æq1É F ¾Bä¾0¡(†xÑҌٖù´´Ápƒ{I  I ãþZÎfA-FDÃòxO“< *�ǘߋhQ”RI¢$ !�Ž ¹È²h ñòd,�G?¨‹¯WšDAâQÔ6ÕM‚¦‹r¤í�äVA ¨‘•ÿ:¦„’¦Ž!¡È$Ä)\ÖøuÖ*P’¤ !tŒR‚f'�'"¢ããñÇ3éZ¼Gõ¬¨U–©T_þÁ8‘±°Çcƒ~âÓ,£ùŒj€°ƒnj]R5�”ô×}¥B£Û§Q÷¤E©Fb°o*þBŒÁ¯Í°ºÞf�¬|S®’²–¶©MzÈ'ÿR§ m.’“µKú¥¦62ù cm/!ÆÄßÈ"I�@6Š Xy™J”+žksú¥9"™á“2ÙÞ<óÿ¥Yâ£�= ‘�WCrÙ·œ%††:© Ñ ´µÍ„R®Ï}d¯7UdHKLª6P’fj›²`Ùj/Z§Ú…"“½Ë–¸É,1™±¦Í�²��ý‹ �ÇÄ-8&ªì¡â´ÅK³ÄM;&6,4[¢�Ô–³%Ú÷Äȱug§Ùí‰þˆg§Ù_f–h¾™®B½£³4fB篮=ñóú~³�}w|ûõOý¬PZ;ã ÆwùH3 7¿»óÒÑÚïÚ;‹¯}ÝûÞñ·ÿ+:V�åïß|x±�ì«“³ò:púÇgŒyõØŽ)íE_äähÔ¥½ oyúœû^™RŽkŸöá}ŽMy*µÞ+ÿñÃ/ÜПÈÌѯ÷.G m j�,ºõÒ9ßeªß,£ØÚö¡½ŽÓ1%…üæ±ã~áú~ñÍ„oÀÑ&ì8rĸ6^³y+W{WU—<ßóÌsŸ÷ÛÓÞ÷ö�7óüÉ´¿üÝüÓk­ÝŠ~ÛôOï0z�Ñ“Ú $î­®ZÖ»`ÞÒ»¯ž²øÊ`2yËôOLýб�Û5e²¤wÁãKf_7ÿT—§}‚}Ãö‡í»ÝÁ]¥1EÏ€“þÒžçî_xí­ÏürCêØG饻À¥g†ˆ òáO·5W`lóÎOD¸Uá§Ãò̵BÀ¸ 1~ƒ@è Ô˜�'‘k ÜÛÀ�©T!.ጪˆó�‡çx’ :8áÃÄæBRd PBR¨*8 €ƒßÇ9Ï3 ‚-�]6È(vЉ†"Ù(˜0à6Nñƒm´âœLJZ’ pÀ³¨±?¿G-� e罌( ¡­®‘¨©ào(òʯïXB–Ù'),’Îh N*�WYIO�‘¶@ ±i�§¨@…S!¶ƒ°±êx%ò%†I~î „h¨mò4 )üËb¿&ʤ±bÏ6Z†â‰·Üúãkm±¯”ˆ)á‡*�!jo ¥  ¤�à ‡ÞL„Kê‘ÄîCh5¢¢Ø½4-­®€„è¬Ïõz/é¢ù ¡ýhV)¯uÅ ¶ŽeI�¹üð¶ ãez„EXqW@ï&xÈÍ’ÔH’ªа`ÄR„ŶàU¯¨KÙP¥PWZ YÀ?â$Ð`ä¡ó™b€TØMm&ÍW+rÖ.L s›†ÆÛçRÏ)_EîmVˆ‘€‚ÑÐ_ñ_É©‡¤¬—ÉKLÕðz"!ç(™IĶ”Ç™¹¢­pñ,qΠЛ,á'˜-Ñö mGÄhßf´D�°ÇH*Z£^êLÐ’õY"9Ÿzê)•lÍ ýÚC¥×ÌOTÝÇ‘?žØOs±/äôd-["ÝߦµÄ<&ªåÛÀ˜¸‘–HϼiÇD�8[â–³%fKtvš-q ÏN³%¾Ì,q�‰æ& b\l+tŽ.ÝÿàM«ºWtuŽL„qm›Ò±çâÚñ>�0^D*·ìKÐPÔ›§}ì¡…¿cÝîB“‰c¥ö–iãÈQZZöštà•��ÐÛ²<� ,^;å=“FÌL±|Î[p�óM!†õ]­õŽ…£Ûx��å#T¯·óvmç¨6\áιᇽéý£ºÆôQiÝ{Â;î^|Azí”No÷qo>h—#âÖ—¦þ'vMço¯‰^øðwæ/¿3Õ¢™™¼oϯ�hÛ á™LìšÆß^ßò«¿ö|·ží…ÑÿðÊH }*ûæ÷ª)ï:/êy¼?�ÍqA; GÄ&ÍN=½dÍ̃›h¼n‰¼XŸ #„äªb ¤rç+¨¦—H ??…6 ‘~0†`ë“óçξóVÖ“Ë^X²tÙÒÎÃØ±ã&Mš²ïk_»ë{ädNÉ¡ÍLyc� YC ¤øÖGzhá³ÏcÇ�ßm�Ý[Ù•QÐÃó@/îàÞëo¼ú™gžZ°`ÁŠËG�=i⤽÷yÅ›|ëØ Ø•@‹AÀ¸‰bj’žD£IÔ­–¾u¼%vß}wßuÇìE ,Z´hÙÒe�ãÇ�Ÿ0qÒ+^õªW¾ò•#ºBp…}¤"5*B!¨ÅìÛnëî镺ÞaõgMŸ1cfxJ&éÕ—­XqëÍ7>öøœ…Ï?¿`ÑÂÑ£Gï¾Ën»î²ÛN»ì2aÒ$9Â\•à·%ÐÅP ¹³¡)j=ð ,¬y­ZµjùŠ¥Ê'0 ‘!|êj…‘£GÈ ùíŠ .æ#g!‚º{ºï¸ãö|pñ¢E‹/^¹r刑£ÆŒ7uû^ýš×ìº÷ž ñ䡹]Ø“S¼)Œ!�ç¤k²ãiQ¦G`¾�g¸Eû,¯ÁÕ ÜzëMóçÏ]´páÒ–T+•QcG�7næŒY¯yÍë¦MŸa6íeS %¥ªØ~µ´<óüs?ü$ùŒëêèÜ{Ÿ½ºº:co8Êi}þÙç®ûÝuO>ùijÏ<Ç›¤;ì0uÚôéoù‹·M˜8!ämN¨±7ºôZ[žxbþ-7ÞðÜsÏ>÷üsÈvû©Sgí´ÓŒ™3¦Ï˜9aÂdØñpŒhqJW�-Dþ‚A™³fÔׯ¸Z[æÍ›Ûm³Ÿyæ™%/,|aɬp4¶1nÜî{ìùÊW¿nü¸‰b‰œDòðÃô]ý½ª{å’Këu`ös¤2ßnm­�9pFº»{PN–%Ä9mú´Y;ÎjZb­Z¾úŠËî½ÿÞ'çÏ£Ê&LØy·ÝxÓ›–,XШ@è�7z̘=÷Ú»T(%KTv–%›ƒ:‡£&7¤õoåÊ÷ß_OEÙªñµûž{Lœ8‰ô¡6"·É&¬»¡Ah.†¦â5,Qˆ¯–Ö;n»µ\éQ—‚Æ´i;Μ‰Å)xÒÛUTë·ÞrËwÌ~þùç/XØÛ[;nìÄÉ“÷û“ýÞ°ÿþ]#ùL•‚2Éù×ÝÝ={öï5:Š‹ÿôŒð{¿ò�#» ³±É¤Æ ØTy9µö~Ã+¯+ßwÏ]Ë—/ \Шí¦N™µëNMKœÿØ\ë³æU­V—/e8·Sí ÒÔ…ú»Ú»:;¸±Ä5³4ŸBý�°HòÃ>pÛ­7-xî¹… V«•3fì¼ëÎ\³¦ï\b÷V¨ç|ö·D 2GŸOx‚¥•¢r¨·>1Þ�³g?õÔ“K–,Y¶tIWêÞÇ�Ÿ4eʾ¯yÍ»ïBOaj$§|¤í§BŲ,R©·<òðômC�c&ŒÛ}ϽJîu‘š ^«Vn¹ý¶{ï½÷Ù矅ÿb¡°Ë.»ì´+Ýä®;ΘþÛ4n£Œ—`‰”£¬ä'±°ÇÄ(ÝLj’zK>Dh‹ˆÛõ�‰�†i­:?Q Uû Cc¢!¦Y§%Ry7|L„· ·D ¦ú*Ô›–ÈKÿĪÐLäÑÖP¬£³r|Ç-O¡ælL´Òë¶D%D´”¡Z—vº ̃ÉIÒÅÃ(4Cþ$µd‰” Ÿk�‰‘†ÜÖ—ÉÇœ„4L˜„Yø7Ę8´%*®!ÇÄ ±DX‚·��Œ†Q8ë y«MpÛ-‘(SíÙCR6-ÿ6pL …Ì–{ÎdK´“È–Hoƒ* þÇm¡¶ú˜›ëbÍÜÙÕ6f|ÇÕ·_xðŸþm³˜g}øÜGþ­yØÀÖ¾“ß�@±”fîÓUÛ—u�Íb£Ü;ðÌÐU+*ÅR³‰i#_}ØÞßpÄ]ǵè]Ûó›?¹ã“+—ô>öÌ=»Mm|ã:§ðîg‹~æ`=­GLëOcŸñïš½ðl*ΘËõ]ÇÐŒ]²ráSÏ>5–÷^ý-¼<ø�ýˆ Ó#tµÕqú+Û¹áÍÓ[î¹ê/^H3óî¸óùó…Zê2å}o™ùÑfÔÚ7`…Øë=tÌCK®KœÀíôQû~`¯¯!€¶îýÝÿºíP”ï°½¾=wë_ÊØŽÉ¯›|Èåsÿ+øé³YîC–t"6ÃkvZŠÓîP%ÜxhZºü„5ˆX *·®£B뺯ôáÅkŸÎCùÇÞº“Öã1UÚ±¾rÅŠ‹Î=ïÊ+.aÕº.y_|Á¹ ¹ßþû¿ÿCž2a;rÛQs9Šôjâ×,•kŸþï� ú]ûî»ïç¾üåxkµåö[o=ù¤—.]Ò/¾eù²åÏ<ýô=÷Ü}ö/ÎÚÿ€>öñÙ1ªļœÃ=� arD«¹ë–¨3èÖU+—Ÿ~ÆOn¼þZ÷6\óš�W^qßoùó?ÿÐÇþ¡£³+æðºJI1M¥­�sоü!|ø£÷®ƒ&ÝsÏ/øÙ©?¹ëŽ;Ö,¡åÁûîO!Ó¦MÿüW¾2qÊ$¡Ý˜ôê‚W«?÷Ì3×^}ÕsŸ?o.8ã€ìéñÜ_�ÃßÚQÇ„í8Ò:"„À¶i®ž}晟ž|"˜ÈÚ¹™ÝÒrñEçÓdüÈßôŽƒ˜å;%�IºR³µè�B ƒæ7Hp|¤â‚ç>_$Gh8ýY=‰Üù‡»N;õ'p2�ÆÜrÃ�gŸuæ˜1c?}äg÷Ùç‘Ɖ?R‡¨PjùÃîüÙ)§D‚ÆPìqÇÿ sD‘EþI?<ùÁ×€«ærLÁÍ7�ó˳ßýW‡þÁ«ÌìZÏÅ–[n”,r@IDAT½éÏúŸgŸ]ƒ¥'æÏÿý-·$Òûí·ßÇ?ùIpX2àÑ—x€€‹8<¹®Tˆ˜�×b?Àê%—\|Á,_¶ÆØMno¸îZîwÙu·OñÙ)ÛMZÛŸ~þéß]uÕ¼Ç[¸N8÷Ü_�Û¤Ù¼9îøOž2÷Ê¥‹ÿøøÿj†§›¯ë˜�wÞK¼ã÷·�zÂûsÈ–‚�Ï™sß]w�ÈÅ㘱c�ûÁ0±–z¥Ælý(TÃÕ7a²Õ[jOΟÿ¯_ù2ˆÒ"‡äïÞ}È{Xb½VA¤üïbº“²öCЀIü'ÈÖö²O²ñë'ýh-‹ûû�ôîw¡ø¤]ð«s.>ï¼r¹œr¥OºŽùóæÝyûí§þää÷ýíáyÈ!%Š¢$«áñ¹?üÁjï÷þ¿ñ�ïÍÜigP�m7u´#´kÔ’aŽðg¬­êYù…#�Gû}^ñÊÞòfì}þܹO?õÔ€Ž¢™øŸ>ññæ}óf¿7¼ñ3ÿü/:7eÐŒìS½ý÷·œ|Âñ`èýC©ï ×_ŸB>ò‘�½ó ÷ °ÄÔ¿‹SÚ£#‰BK‘ý1QéÞî+/øåyW^vÙº†ìEçþоâ�oÜÿúàĉ“…ö<£8DJËi$ȃÑÑô /н¿rß×~î‹_e é]¹êg�qåo¯ 0nŠwåèììü⿼÷^»óË[N({nƒ,ÅT+ÜÝ4›ZÅÆ°‡ênÒ1Q­¥ÒþA½�¢Á°BrÌÐã=ÜóCAô¥°5ô˜(ï8øR’¹^Òg¶µÂ†™îä`ÍÖk‰ˆ…ÊÑŒ‰/ÁánÐ1QaS2ƒ;V-ÚÀµà>L®ÌFfC<5<$—¼˜å9�‰½æ˜håýåc=–苨0„¹“,ÙDï,/Ä‘þc"$F¨Pv“ »4s9ÿÑžìôó‹6½þëåîêöcvn–rû×9þoÄEYŶ–®Ñml]7áÃý)�é˜Xé©ÚŠ�­cÞ<ã#ý£-}þá¹÷pê;½jÌÈqͨwîòé‡n»�×ébÖP;xç£úˤ\î}àñ?<½`þ¬vÛmæ+R5/¿çç½+ËÛ�Þ}òÈM:K–/¾ñÎ+Iùê=öÛw�ýÙ„®·Ü}öÍÇŒš$T8@>Í\›ä¦Ppxæ+ 6ÃqvÊût(„†Ïl2f¢¡•ÎUæñm"fT¤+1/C³Œ³ÚF¤d黥péE—üâ̳¬”Œ\ë"ÍM×_Ïß[ßöŽ¿ûè?:˜ß†ˆûQTðÅTÛéïjSIdî¼óNÜÐp·9ûÏ:ÿ¼óÖ¢½FÀM7ÞxÇìÙßøÆ·§N�Ç2Õr¾n)Ô!U—§âƒÝwÌ÷¾�Ê${¸öª«n»ùæOqä«_ûº@|$Æ”¹2+8Lß¹îZ×¥]|ÐAï¾ð‚óÏ>묵"×ÀÓê¨#>}ÄQGýÉo€SºhøÂyðþû.:ïWk$}1¬üä+µ+K ¸mm¹ú·—�ñ³Ÿ®·ÕHpÆi?½æ·WñÙÙ~Ç]n8WçO�Ê_ƒì kcºÞ %R¥‚WËåÓ~~Úo¯ºr�œk”|óGÿé›ÞôûÇ®#WËÑ$ÅÔæ©× zçì;ªõÞSNU¬éÀrê�¯½æª5­õpë­·ÞsÏ=Ÿ |õ«�¤©¨— (“P ME…®·à>yì±Ç<úè£k‘ðè#uä§ûàßsÈ_…tW[âƒÜwñyƒÀjI öÚ'ˆMup]õÛ+fî¼û-7Ýx层 ˆJ�Ûm·=px9 ˆ˜»ä‚ >ð� ¡ UDc¢UŸ¶5ûó?û‹–ˆ~ÀbŒ´¼x¬\£­¡ð[zjðÁrÑÜèØÚ×¥_ô΃^ÕÝý_ÇsßÝw¯� ‚büâ¿ÿ„÷ß¿~tg'"A³Ž³ä›|Ëïm¦L7?;ùÄ£¿ùïeÊ/jÎÿtCv¡¥~ù%—¬�»‘úà÷¼ç¤~„è"ï‹ÿP¢ƒ4b“Pw÷Êþà8¼5›!ƒÞœqÆÏî˜}ûG|näèNØXRæOæù ËM=\í²ó/BDCXP³ÒÜpÃõüýÙÛÞþwþX©/Û�& 5JA=‹Ö¸î¾söÒ–Î}ü‘~pÌÐp�t:ov}ôW9ä¯;ü}¼î4_!Ý�–(�è/B�BOQ]òØzI²›lLŒ>RÒüA›êlj2¡­†!‚=X°E˜†&ž´ë2`L”óñ<7¦^BÊëµD¸J»+¤V¶YI*8$ 3ÖòR,1i $Œ‰A•æ‡yw °lk’˦à˜šø#MMÍ`©Å;&BËŸ‰MlŽ”'4ÌÌ’,QáG- Ý{‡lãKøDvá̘)¦vŠÀ[KOôH˜Ò[¨Y.9-Ô65©.ú<ȧ}ê¿®1ÑúH™?3ògm£xq �‰€¾RphŽÆÕoL4�)]4/.j´†%:n•x'*اIŒ‰„xˆ””üjC,æð^A¨U"•Ÿ-1["š€† -Þ "Ù³%:&Dh‡ªZØ©å1qXŒ‰tö›õJ‡-ºV=2ïžÝf¾2•xôÚí¹û…óYÛ1Ôt¶Lš4jj“�•=+ïòÚ÷[jÓ[G…jü&ÕHâÀÉÈU«°ýß�l_ýNåÉç|ûâΠDú§s¯ýégÿæÛßø×)Ï[vúð üêÖG~óOõ¯6—“¯˜ògóæÞŒ³Øß¾;¼£A½ïkû±;U{ëm@|õúÄŽÝJ@e}×å¿?³´?ö�Ó}á/ꛪ•ÚZ;Gç/º·ÆÎ¶‘åÞj©\ø«½¿Œ šQ—þî—'œýïE~7ƒ£jíèOžòº}Þ”by?ô3�¼|Þ± 寙ô7£:Æ7s=³à‰#¾ó7Ýe^µAdõ cvøÄ¡_|þ…'sÿ)SfŽÙiêš)iˆÿ󟹼g ³†ó¯=µTh?àUoŸ³èžŽI+»Æ�°¹^3צº‰I,×Ù)ÓUæŠÌó{ë-œ’é—@#üö‰ »r¦j|Ð¥½¹˜·ù+0›•ù‡Š C;ÝrB×]­w̱gžñß²0ëß×\yù¿~áÈ/dúf� ÓWì&Wf‡Ä*xõÚk3²ß}×]ßþÆ7Ö‹»¥‚X¡ýÛ¿}éÙ…‹Q~*ÄÜ“ãZ�ø2_¬ð笶R©�ñß?ÿê×þuCp·D–eá±ßùÖñÇ|·ŽðœQ£ØJ…IjßJ`uu-\ô½ï|g½¸[Ê€$Á ®þÍ•rˆt�¸/Æc5ÉqW¤]e4:'(Ö9 uÙw¿þõÓ~rʆ·Úüùó¾ð/Gþú x™¢ÔZôZÞó;ŸúÔ'ÿp÷=îÃ'0ƒ?Yµ‚×Êqð„ʼN>é„ôãõVóÚk~{Å—R“oï?׋»¥2Pƒc¾õÍGî·,iE±ð`·9¿h±Pì˜ù3ŸùÔ†ànMÖq晟ûÌg-XØßµÁ—z•ì™i+D‹žš7÷�ûîZîFÎ)S&vøáƒþ›_ÿºR+WyÙ³½^+.±¡h¡xÍâGô /<ðÀþ†‰Ô›gÎ7¾úUÞZÕA;®WÿöðÚ±ýo{ô‘gžz>ª¥`Km¼âËž~î÷È.0­›êå¿ùMÿ\éž7‹ÿô€ýòºvºu„”Q•@hÚZUN™¾üùÏ­wK)ï¹÷®ÏáL#À;`¶Íäg:‹Póü¡Ò»âûßýæ™§ÿ|½4€å«¯¼â+_ùü⥋ém€è.Šì~‡¿[lj‰2Û%¯u�ô£c¾÷�£‡ÆÝš™Î?ÿ‚ã¾ÿCf&EwaÔ-jhK …öè~8å¸X­Ä ÖÛ~|Ó�‰:séáë„ÑU'½¸m†nc‚}{«ÓÇ5¼öÄí” ¿>frçø©#&N1vRלö*•Õ¯ûì:éõÔ™¼v‡w7©Aê+?üûÒˆÞ‰;Œ˜oÿý­\ðKëõìU·0us!1øuç¼¾ù"®ÿ9ý´žž¥îŸxċȼVÒ�.¡VûéùO|ö“ÿxï=w­•jýg�yÆÃ÷ß/eÈ¥Ö ÁYÒJgÓ@„¯á°k­ÿî†ß}åË_X´hàëxëåÀ=æ›+V,‹õO,‡¨U æjóZ×€× ×Š_ð‹ÿýï__vé½÷ݳ:hîN=ùDÚü|Án¨(ê¬T~rò‰'ÿý [�‘<¿ìq!Ú†%¶÷ûecøZ#‰ÜE€ö¼ÖõÄ“Oüè¸ï­¼:Àè�ï<ˆw WõÝ�’ÌþýllÖú†¦*…Ô6ö-^€š}É×ø~ÿûëî)'Kô%wU‰§XÈ‚+èâbÒUá®cÁ©â%ñRÚÔ8× ÞÒòï_úü ‹×®$ð8ç±9¿øŸ³¨ lS£©;NÛc�=¤I�—ÿú‘?‹·ª é êà‘Ê­-l›8hÑï}ï¡=eðÿÁšaÐ’ÊZhƒ©ûÀôëyÆ™ô’__¬àé¢: ÙR=@Äzå?¾úÝð%]tï_úü?/]±<¤c'¢Ú¤¢ åÁ®{^d§tëM·ÐjÑAJ~K¤4Ñ.tJ_ÉT¾JeSÄÿM5&Ú5Ú;2v;¼ÛȾ�~ôôYbÜñ ,“ eåÊ^2ؘH.ªà;…h=`¤ƒyj§DõQÊSªèœ0S¡2õ5މm‰ð?ø˜¨ÅðÇhÒQä€d/ 3Á“œùgŠtùìô šKѦööf(K´Ó€Œ´|, - ’>xÕNVlÏ,˜zqõÄÆ”Íȉû¸€‘†%j:Ò²Õÿê1‘'„­j©Ò ý‹É1`L Šƒ["X�§öз˜HðÐKyZ5 �ˆÔïúhHÈŠ|ŠÇòiÛuXbp“Àà0¥çY1CëÏŸ$ɽ<‚(H�S¯•b_³À¬Q_>Ö‘B¶DÚ‡6B>!¢l‰Ù±S­Ö+[bí=·¡1Ñ¡os^Œ7]£Š�-øý²•«ß?rû­;€à"³÷Ô†÷Œ`%çßt’ç�v ÷0c(w×ú»w�èõ•�ÿ¹wä¡ßûìßûù÷¥�þÿM·4†­íÇìÎ ž7Üuq³º£;Çj] €³F¿¾¹ÓÜcO¬Þl}ßßÉy•Þê®S^×Ìõ‡‡o9ŽÝ¡/íé7æb Á[»OU"•j¹V­•*cq«k†_}Ûù“¦�˜¶Ç¸{�›¾÷¸©{Œ™°}Çì®o&ÀW®{e¥weuTÇêQç>õpµsÉ»�ž¶÷Xrñ7mϱÛí<pmÜ”ÎÖRáÁùw4)psÔáßûî?�ÿ7oûøNûLž±ÏØé{ŽÞn§Q£Æ·on—7Šö³Ób ïƒöz­³Vo6¯ÕÕ̉;C “8Vè Ößæ]ÎÈ gNÊf‚ $ðjm=óôÓç<öXzziŸ«V®ü¯ï|“_µý%Ò…Ö2‹Wt uÛD×OÌ»öê+¨>NZ±3Ý pÝk;éÄØ/|ˆrÚÛ}W|]×¹¿:ûÙçžvýT©ÖÊ�é&5Ew1ÛÓ…ž‡;@ç;í¶ÛK&Š\]á1.€Ü¯úÎ|}h@ŠŸ†(ëÇW)ãŒDïÇïþ$¡¦¬»ÎL¾ÅOôyÀp ´ïü§æŸø£ã‡(”gÉÃüÏO?…Ck©{æ´xªKt´¡˜CP" �ÿüç?"Á QO?ýÔu×]ë¦ãL”ñ­ä%¸æš«xKwÐ\)�:Šg¥Xö¡¿öÚkš–¸ë®»Ajè(뮋œá’�]�`‰è ?~|±ØvðÁ«'é‹ñûâ‹.i­êì`D!vE�bÑ­ìFÕzÅåƒ@o;í´óŒY³H™,ë„7�…J•½†ýq£ ïq{@=^ø —)ÊÑw 26h¢¨ÖVdÞŸ£—~é%—>5ûÌ•Võ–?|õž¯ý‰^õÛ+ù)©ÈQ²á&– >÷È×jŠ—_6ˆËmý®ƒ^Ù[æ ƒþÔ^Ô="¢³#ÛD×yçœÝ³j%5¶ô¦j³ øý£µzÖ§lmL9 ³Ç}ï[:íp ÒïŠþ"–C¹™÷ÔSOQm@Kؽ�)Ù`–HãðÞŸžKb‚lÕ"¾’b�+Øn›jLôGNœ€Åè’ØV£T-–êm…j{­ÖVA KŒµf…Ý�QgúEã› ò„r“ ¥_{LÓ ’¶> ÜVÊÞ†ÅJQ Y·%RwΖ©Õ8ÿ§·µ¥×ƒ�‰i‰x¦Ùºk�‰ˆ<˜§Tl¹"üBWA=±-\*<ò¨º~Vè"þ{Ž“Ñ ú3#VEÊ_sLd÷Qä½nKD¢vè´q³Ã›‹bQÓ•?L0@¥Ù=ŠÆ,�·GÓ…Qá×Tˆ_¸é•騖ˆüU<0¨5ÇDzB~ádX`Ó\‚Ã]1©c¢DT^÷¸\ÛqJ+*TÜm—�¾´Q›ÿÇÞ™ÀÛ5�ýÿÜ{Ï�§Ì2I̳ª!f%BL¡Eßj«(ZZE_}_5SúVQŠj‹Rm©±¢Bd$ IBd�A¦;Ÿùü¿¿gí{î¹çì}‰¡þŸ³rrî>{¯áYÏz¦õìg­E¯9t^‚V½“Í׉%©DI œÉWG€®´e'rÈ=a� IyŒ¯`>z=«¸v‰_íP'½Æ]‡u$Æ ® ¶i@d œXàÄ�XàÄ'tbö<ñˤ3öÖgv�]ÈQž5=Ëÿõú“Ù�2äÌD<=¬vÿòpÇ´ô½åó‹*"åÕìh’ñ›eò®Q©áTUÆ_ÆÝÚªú=·½óÀGöû*Ÿ]´×‡¶ë=•P7wÞk Ÿ¤¬WK(´KŸ#q®í5 ctë.wïØÈ3¤çN1Î�‹w¯Ù&SdâìG+jX~*ƒ.ssË.P»Ô1¸÷ˆìâÍ‘& ‘AÝvϾ¹|û=úUvÛ¦’øÁºžåݶ©è1 zÅú÷3y¨©Gxp,šÆg™¹¹bÝ’ªˆ¬ïYŽ�s!z ¨ì7¬¶Ï šjœ›ÅÅs?|5ž`‰_GbåïÑ#¿û³¯={ñ÷î<|_Ú*«ü4âû:Zð¿ú²[§8ÐäŠÕKW…š ØŽ$f‘2ƒTlßš$`£Êå?Ùfr³è ÍgÞ ÿø‡?†B¡Q£Fí¼ÓΜÛX^^þÞ{ï.ü÷ÂÓ§ûff«gžybìØ“ÍD6'žk÷@ûM¶ :æ˜c ÀFT¬h{íµ×šÛvúûÈ#�rø¡®R뛸fú´ÉÓgL픯ýg\xÑE¦WÎù˜©ô²eË&<ÿü3O?Ýþ¼ãï]·ßöóë©ßfO3m ïxpÅQ€_=è žÝzpt&KhÙÉ7ã³O<1vÌñ5••Xþƒ‡ ¹öºëÞ]´ˆœDÃ=öØcùEØïÄO´Áq ™éPMm�FNŠÿø»?r\`~Yî|õ«_=ë̳öaªCljþÀïîŸ??÷BÕžøëŸOFFôÐá.ú,Áa>] Ðø¶oð€›§ŸqÆQÇÕ§W/f¡��Íóç/øõ¯ÅŠÅœüìxøáGŒ9Šl¢Tš°ïœlÙ?q|Œ9j¯}öîÙ³ç|ðÔ“On2�ó=�=öXˆ¦OŸÆVƒÙf®ÿôà<è`&Z$´Ð]v¸ÿžßdòd_TVV~ÿ ÷Þk¯ÚÚzî¯Y³fÆÌ™÷Ý{O><¿¿ïÞ=öÜ«¶[5œ8dð``Ñ"BÇ¢‘ 8a܉d,nH컆3€œßa3ØŠ¢9©GÏž­ÑÔÉ'ŸòÌ3>,°hÑ¿9÷ƒó‹5ud(Ñi2*yòÎ;ó›üN–8õ´Ó¢œ{€²�pÿXP‹ßŒŠd¢ n»d2ˆ›ÔÍ$WSw=§òm"q@ÇAÔ»Wï�×~¼pÁ‚É“'çcÛUñø_þ|þ/¥¬{Û{iøË'?ÊN~õÕÃ-$‹¾3ô/¹ÅyÁ³Ú�ãÈë°Ã——µE/½ì²×^y…·<}õÕW—,Y’�Í]Ÿ}öÙ’)ÔǶª»öšKhè2ÕÖÖuôQÇïÀ¸p¢§|øŠGÙO=õäi_—“‘y5 ‡ݘÿÎ;Ÿ×Á2¾ ñ¾ÓÎ;hâý½… Θá/ÞþO?õ÷±'Ž£j9 mÀ6GH"ñŽ8âÈ~ú·´´,~÷Ý<÷œï�½÷Þ{œ‚:j·�ðŸäogN¤/ŠbbŒ$AŸ=çKäd«AÓ¯­×‰4B�´oD¡æ$)S¼6^ûŠb•EË”Ñàr÷…°ï†V1ª|,æÈhÌ`t૤9|©K^)úàlP¢° ¢�9Q]/ñÏ^ê¾W-ÄO‘Û)³³N´Öñi”¼@*ˈ‡‘Ÿ6htE„;§^·übZ¾É,o˜òè®Ó/†KºdóÊjp ËH Á¤ÆÔ†¡×Æ‚ŠhByÀ"â[ÙE!öÒJÙ¬Zy<ÕJ{p¡ãDÆ0Í}Àñd‘2 zFKOí¬2¨_®¶l�èÊM´CŸÅ‰ºx–Ô:N7Q‰†’?”µ$Ô‘xœ­ÝCרµ+¯!Pes"®PÃ�è‡âê€ è®ý‘õ§Ÿºo�Låê·€Òma’_€ q­€å'‚•'Š,DH¢Kþ‰ÌøÍ8±À‰�ˆ¸”ìü�׉ÆÈŸífLiYIU}Ù³oÜ?öà³=Õ �ì{À“ ’_Ý©Óö/�¿rOmÏÒò ^É�?(~С «Ã=üžÞ[½aY8\\Z•\¹öÃþ½‡º|#·9ð�åÖ{7÷³-Ò²²añ+m;Pî0|‚=ÂÃút&!g)k[Ñ´ ¿:,«­OÒòÉô°ž�¢ÞÞ_ζK¡mj¶Í®¿1¾ –UðšM�”…•µá¦h§SûÕOt>ï®±mmu]iY/¸Å¼0¬PŘ©2¶'IÜò§K®8óö¼=�ŠúÕmöž¿˜³ê¹——vZ)œ Õ§y ¿ˆe0R¶xžhÆŠë›â6d¶ðëóáDÂ8‘û #Œ÷·²ì„ ;Í‹_ü”a+Ó¬˜Œ=ñØŸ3²/vß}�«~v»øÛ }z÷sܘ½öÜëÊ+’sv-n‘‡ÿðû /¾ˆlD\ï±Çž|@gsŒ;ñÄñã;6e3ÈÕDs3@¹ AÊ04lضÛß¾¦¦fÆ Àƒÿq�½÷O$BÝ{ô­vÊm?þ9áÙ³Ï>G"¬]Â7¶&.5áŸÏç篮®>àÀ[£ó®A„�(C�I z!ÊDÑ‘HX­±’§@C‹$Ñ ˜E¡á§‹Ü H••·ÞzëÐ!CõÜݘ1Ç�;éäËò“H¤-¿Ð¯ÏHÄ"x”¨6�ŸvÚø{ïým~¶çžzúàÑ£¡9Åç1:@d‹Ëø5õÕI9RÂwÊ)-ñ8ʸ®ºnÌØ±ÒriŽ^�/²p�?}¼º-|ðIñMµMM-VŽ®¸Zý¿Ï<ëL(¡ým^Ñ�‡N9åäk¯½ö-?÷ñÛóÞŸþ:õ\#^¦¹Tè±ò¯:úá%—†x7_y IÊ Þÿû¿ê+Þÿþ·¿sìqåeDÝh¼4ÓøDƒ^ùÓŸî³Ï¾F¯ò“yøácÇùO.çÄ’|¨Þ™;o—�wOø¼©V#‘ʼnÇ+E€�-‚ˆ0 ¼RºD4)¯Ä ĵõ:Q®”‘Y„W¡:­›´Á,‹è#~™dlŒºå÷¡uø‚Ö-ÞLhÀ�â4È Ba�¸6âèPaüà¯ù]Œ�·qˆþ"dó¨ˆ?0h¶ æ�1ˆdɶ#¤£Zx'` jbìÀ“ðŠÒ¸yЉèÌ™åõÜn«•¸ö®S»ÔÍÞ¤†nëo‡N´Wè/Tw'Ó¦qðn9;Ã-ø¡3€â ب:DÖX,ºçt"4ŠÇƒú¹&ƒzªF˜U£Õ¦:ð3ss‹/P¯=Ë·Ýv›]²k˜4ïïxÇV®û0ûf¯}×”¬Ð{MK´^RZÒ­¾“óqÅÚ%7wZê×£®Wx}§­õA—CXዪº²wÍ<çš#.ýî#öçHÓìF¹Þ£ïqKÞ~¯qrÏ9>­Ÿ),ö­˜'~áÖ©¬vÙ˜gæi‘ …åjh“½¥�3Z”è~)»ìXìc[²`Á")|zËm·vÔQ,YŠÇmmQ>lo„)¶Ó¨Q÷Þ{!?ù¥p1¼üâ³ýôª�öí+?£î0/½áÆ=ìPrÅñ¶h,�2)­­«ýÕ¯nÇSà[ìÕMâ>v4ö'½Ÿ9}šïfL»í¾ûé_ÿ}ni‹nljihd'º¦–ÖÖT"¹÷ž{á’˯|â‹p²g k6…ÈÏÆÜ‘÷ÜsÏ€ý™gÅbñÖ¶h$Êž‡)<cO8Á·ÈG+>Ò‘,ãI&›[Û˜Ù‚«Žÿ”f)(å‰%b1ZIŘhÁ? ý=ñ׿ú–ûÖ·ÎÁ­O¥"­›×7¶®oŠ4µr Gˆ5w#FȵŸ“^{íUM0/IæÉÉàý4›S3�*ŸÐ�>â›óþ÷ñ»A*›Û64·¬gÅc¤µ5VQYÁ<<ýéÆ�/~OÂÄ„Ì[Uï“öÚk¯;KG�ž ’úqÄ’©Þ½{ï{ž`Í/3`À€ .º4‘ˆÄ¢m‰8+µŽ8êèÝw÷ôAN‘Õ+Wº;š„„Š×o˜:ù•œ<üÄåôÓŸþwEEEk4ÖÔÒÜÐÜÔ°‘˜°V8¤_¿¾W^qe~‘éÓ¦¦î›$Z#Œ>ƒËÇæ@ù¹™6Zº�s~l2’HF ›üYl«›ø• …ê»u»é¦›ï¸ó®‹xÉ9ßùî�.»ìÆ›o¾ìò+´…}:Ô��\€ùb<¥UŸR`,Þ`·¶4Ïšå³SÔB'2½…€$�$a´hXuhT™@:1 ™cMŒ¡%¾H#ocO”G�ÄGÜýÛß6Œ6àµ1\,‘JÛn»;î¸Ã H’Yí»›ÅâÉ£Ž:�“Ÿó£�>Z²ä€K¦Øb1rúÆï¸ãˆ~:NÄåç‘o•õKOÝ82”1¾ù�ƒh %hì–[~Û‚EK[ bƒÊ++/»ürß¾¬ZµÚPÉÜü²24½pá‚¥Áâýð#�Bžó2 Ò‹´F!9Ú¨�FÝ{ß}8:óCM|áFLÕV²ågsw† Ûö¾û'¿ËÇïQVþ&ɾýû_üƒ‹}K­\í¸Ïx¯=‡ãD­h”+„%o ø•Þ!6ž�¤ÖH‹-׉*MsTcÕiâíº B¹­U¤r¹X;8”¡Üv7)bÏDè]òÌœ’'*/¾2– ѽœ*æp≛Ú{­5C© …+WL•y÷³tâ§Å‰òìèP�Î:Q8±.Z‹üP‚~§pO9jOÜøèÒ%¹�´>•ãN'ê‰ «JYTJ©e›zª:y¢kÚÖˆflΑ’N#¢{yœÀÿEEª�™[Yj¿Úa§&å7œ+¯8Æ>nà:éD„´v®ˆ€±NI“²u¢µkƒ£28T©8ÑÁ-ˆŠE?ò³@T– þ8½=5ç©\º*lÿÕ . Q‰¡ÀÈÛûV„ãDžŠè­ç<7Y ´JÍ“TÊ!Öþª[á@[élÒHÃëµ­ºTI%èGýNƒ¢M!ȈÅ!Ú•±N œhQàDN”<ѧ ¥E2ésÔ‰™6?Û‹âtYEIm�ò¿Lº+»¡±»_,}Öž^Ñ e¬NµWsíwýþ‚,ô81h™‡-­ ÏNyðÙ©>7õ!}¦=ôÜô?þsæCÏ¿þð„7™·ü•ÒJßQù+oÿ-S §Û¡;|#ósâ�Õv/Ÿ¼ð)Ìws‡¾ûàžËdxiö_+jJüÎj÷‚emÁ.‚“wí47\×°fÅÆùpÄÂe�"!ôÚwÖI&¡œûv’ùÉÅ’ÕoGcL ;B út'X×|uaÖ±¦+š®ûýyã.Ùíÿ¼jÑ’·3}w•ïÖëk)6îÎ"ÏìF?­kÏÙŠy"Hû­S<‡ìt†e†y+‹ª Àp.™w²õˆà‚Ÿ— ²Õx×)�ˆõ”½òÊ˾Øüʰ 0ÁéÇ8t’¢(ÆN!%4G%žrÊ)=ôP~Ù×&½<昉ú-ÐÅ^Ñæç …~uûíÆ ÃmÅ4pœ‰WƒxŠ«««N:éäûï¿/¿ÜGK—aT}`NÿÊ¿^ÉÏÃ�Ÿüär^¤7µ1U‡hÕ_ÞV3ÏlŒÅ+Ê´æîñÇ'Ä)»,+ª’‘HIy©\D#x�Î΢kBÿ~pÉ%ÀÐØõf˜¿éâX´¢¼”àš§Ÿò9íaåÊ8Œ„öPnÁ‰Âd-‹äÖn¿©¯%ÖÆ·½ggô´M\XÛÍP\D;uò«ùq3�4îä–æHkZÞuus"\Q¬9ZSYÎêÈ‹.¼0§àìÙ³N<ùThB£õퟨLç¿Ñ‰¢ôŠ®[»6?ߘãÇ ±CCkK"&*#ŠÌÄ“xҭͱúꪳÎ:ûw¿»?§àœ9s¶ßa”:Fhš�’“Áý<ïüï‡Ë+q\¥8Ž ZÅ“aö:à ƒJn½Õ7@é{ß;�Ñihe Å )B5X·[Vqêi§û.;]µjÅÐí†ÑGã«¢Y‹ï.¹ôGø&Ö7°Ò”ˆåÀ5q±h,šJì8r+‘ßœÓiKKÀ{gá‚�FŒ„ä'ÓßžâÄ�é$ Óžé‘�¤’ô& áÜüå¯~Õ—u£Ìíè32gc�N·ÅØèG±38a¡–+:½3¡>|+Ó§M9ðàƒDÖÎR¤ošɯùÐù�?~ls„®ˆÙPˆùü¹o´b™ÍÏDî‚Þ&ÃF‹¹X“àÜgŸýzöè&¥FôŠo¿Ô«W¯;ï¼»¦¶š ÄhTÞjTµF’ñ²¢p¿þý`É9³gç]¶ôÃPè@�Æ~F%%lrç»ÒöùgŸ»à¢ ©Nì"дËãòåË?ÊÃMœzÚøx[¼LÔ­jÍ8„añMC[„×ÔAÅ9�)†Då¼:!ºáŸ_ºå–[‡ ‚×µ%kè¿ÐÆ.Sõµµ‡Œý¯—^Ê)× ß…` �•©ÜÅéW_}9'�û‰x5bT$šÀ�GVí.¥Xxe«©R#Þöï“'½üµ¯�I°yãªNSÂ'A„·ÿúÔµµÆ"±˜Ä€3‘~e%Å{î½¾ÔüÀ7ö댯D/!Z«8Éì6ˆœ-&ÐKmŠ'Äu." “b#ÎËãDŠn�N„oèШÈ;5(Õ©VAO´ìÕ�ºÅHÚÚGÂ;§%èŽAƒ‘‘¥©žóh-¶IÆRq©š$š‰¦m{I�ƒÐà$‹Åe U>�Ç)I% ‘k�è8QDLë pR02Êt$*£8�ƒ�:Q�(0t k'êE =OÊÉ¢M-è«t"0IãÙ€jÅ$¿­°:Î#‘6J’C±zj^5WÌq¢ÂõŒ�Ã'em:·͈€¬'R«Ø�VÃ*a:‘¬t!f >:fœ¨nÒm0g2NêLiD44 C'’?ßò9齫ZTýªQ� s?Ib:�(÷¢zÆ~¹ í‘<ˆh׉r ©ÿ”ýê§j'ª.¿1ÕÀ-¨Ô:Ñp& û }`OŠÐŠy:‘;ê!•ÈJ9ÕÈ>©œˆ©Gë„ê]NM�ŠJ56<° س“f(èFž$‡‘è@gø2µÅa��… œå™N,p¢Y§N4‰QЉÈö/›N4�ó™ÉØ(-ÂiµtÅ[�-ꪻ»&»×ôÉ´�|zú}=·-ÇŸ`V@æ‰Ï…ÂÔKŠ×7®É,Ým‡ýú î*M /;'Í:‰¹«é^Á^o¥lýZÖ¼vãª^Ýúºl{lw¨»`¾´an�~UÉdäã õé¡lݪ{gjㄯž>`ħ³Ú´,T=~çkz× ÊÔnzøÂªúR΄mhd¯yÔ·×™ý¶ûÚ[o<.Ý‘»hç!_Éüj‹µ�¼=šÖöéÞßݺÍÈŠâ6²ÊdëtQª¨ÂõV.+©¨ikÙŸ²àé—çü½wýÀ;¯x¢²¢ÚeîQ9 M–Ë®’•ñY%¶úþ2[§aÌjƒH}ä´Òüë‹MI°µ°¦¼¹€Ok2®<\jjÃϹóÞòE.Ñs%£ªJ%­°šÂŠ�§ZRñãŽ;Î×õ¶lÙ‡±H¤¼²Ü™�š)ù%b:† Û¶¥­5‡PÔùÍÜ”QØÐÔvÔÑGûºÞÖ¬^ã:An.ÞyÛç Ë�wÙ¥®®®¡9‚mMÅ´¯ÿúÒ'OÕ•–ì²Ë.3gÎÌíƒ%‹‡ßQ³:*ÛÙ'ëD OSKVŽi:("Õ´†ƒV«jj˜äç»�ØÅß(™ÌØÎ@Î(É’J<ԤͲéõô|±® I~A6TŠÅprH„: zÔ[!Uˆšm1E˜{jåU‘Ž;HÇÅ_�6ÿRm±�o¿¼íVòç¤çŸ}æ ƒäz“�©vD^øGN6~’­¼ª*ÚU&eeJºóÎ[›››ò3ûÞé?pP¯žÝè™ÈDÄÅŸÄ6g55Õ--mÌ&ÝÚ5}È!KÅ[Óxœ}]oëÖ¬¡::€æ&â o¾¯ëmÆÔiß:çÛ,ͪÜ<<”~éŸòAaÛ5Ü…„pšh`è ‡ˆ+ á!�õÏ¡I—Þ?{ üú¥Ò²²fŽr�³€Êñ"PHMá}hj� :̯�NÔ0p�#…Þ~3H¼ŸŽ@ˆÇÙnNEN 6DKc*vì˜1¾®·eË–F£‘²Š* ©uÎ'•²"²¸8œZCõÖõ„e&ð�w-l—ïzx¼€ÂŠVªÎæDµæüÍ^‹ò@XÃô“1pŽƒNœ(Ù¹E:Ql§ºÅqT#¨… N$®åp´œŠ‰¦©…gB‚aLN)½Í"£WDÂ�òä³8PýpU‰g;q"ìL{T«fQ¿\@*bMè[œh®«†ZU±ÆÙ«.Ô¼û€H2‘t'Ê3DË*¨ƒ“²t¢y“Ȥ¶®Å‰B€à!c'�h}WïO{ ÔiN#??  vèDª¡ºlN4È­{|èDƒÓuN*J4"€bXêM€Zƒ�¨_îŸ=�Ê2| /‚�BC“à×Xfq¢á´C'RU»ÅÀÚ¢=Y`J*()g$Å.…&�°+*¢‘,Nì¤Uæàâ[µR�F‡ ¨Ïö}S;V/wÔ€«�[¨õ��j×+‰-kÏÀåË�d'“m�…Pa×( …ç'�åX§º„¸À–­À‰N t¢ä6Zä ԉȭÏ)±a?�og<~ÒaßÉotÑÒyÅUÑŠš:¢±òŸæÞA͆‹f,xq\ïïºGápé�ÆßùМ‡õÞÐK}j†9ü;ñdÛ“ o*.cꢲT˜ÞX0阯xø@ˆ.÷‚%³9‹ ªŽ÷·å3üëø¯žÅýlÓ¢¥s)[VÎÊ+ÒÞÎ'û»MåÃê÷Ù¯ÿÉ%ÅìÒ‘^˜ö·umKúªc«»òê’�Ö~8 ÷0÷¸WÝ€�úù^ËËî'ªøà¡ß¬*¯Í^¼|žv‚+.Zðá}º�u÷þ´=®~|Ñ•™lå%µÇ ½ñóÜâ›’„5•÷î±ÍÐ^½ß­ŸmI´nŒ7oˆ5o\ýæ¢é_ÙõpWjcóÚD4U^‰üÞª^g`ð½pF­T«†BŒ‹LI ŠXÛ|éóh�õ&ÛÞ‚ÃÓ¼ÿ6¦V`³vz¡]àXUt‹}jtÄ.MujÝÆõì•�"b†ÒÔØ¬ÃÚÌ«Œ%çÞ¡¦R:�§CEUåW¾ò•iÓ¦åŸÿïy»í±YÍcÍÔËÏÃ�&b‘âœ&Ê–eÔÁ…�oè.©)cóïÿûß9e[Z[ØïŒ=aqXöáÒ|'ùqŽLž6U[jN›d³X5¬6²X¤Å¡ y[þ“s}ÃF;“Ѱc³ •ÏIétcc •¢J_à½éU:n´­… �Õ«WçÚ°q¸Ôº(-¢±‰¢:›“+óS¸àÍ4K¼m"£ý½˜êîtúÍyó2ù²/�NSgÎ0ã^·fYÖ.=Z0¤}R,ÒVVÁÔÍ8x%òó¬ {`€æÍ››Ÿ�;ÖmXƪ7›ÄÑ8¯Ï¹ÆYÀàrIå¥y/)ÈÀNmLó€WÔ"ìøÃÐm­JqR�ÜËLrŒ†5$>°OŸmò]oøË+*ãñ´¥ðGcÔ]’ª¬ãå€OÚ¸a£l#JÌ_€O07á{�9c&«…½99Šº5”0—f3ÄàDÈ-ÆNm‚¬'ÚÀäg厘̾ Íqs9}œ»ÿ~û7¶¶qαÇþLÉÚ9‘ê8;iGHâÁ£ùÍoîŠtü¤F–Ÿ¯øpùÀƒ2œøÁÒÅœ ‘ߨ)§žÚÚ!°NÔ4„0 ¾ý‡.¿´î@Ý1FC"óÙ”ˆd­<„6¢È˜^W–J±‹Ó.AˇW¯bè©¿$Ìž˜é=ºûnr‡ ù׿^>ö¨c½‚N€$1yÒ¤|`N:ù$–˜j£ãDÆÒ&›¹:ò³ëN˜°!Æ�ZɨÿÁ‘†�åfFÃþ(k‰Æ*‰P7O­E°RNmAat¼Ï6ïÕL{Ú¸ncß¾CCø6BEë�îþâ}ذm››"œZàÄ­ä‹ñ§úŽN…j*ª8à€©S}ޝY¸ðm­×7‰ý¡‡©ÛðÀÂÕªQ`už)¿8‘µß}ûz/!Û×_Þ ÄìÌ T—v ³¸� tH¾%�¸/Æ : 0©« U§È¦´å:Ñj�‹CR@´ƒX§:5Á‚q?ND¾™NT¢™€Æ¤6ôŠàS¯ †/ŸEÑ»^~Øã ×M^Ûº´¾¼ÏvÝ÷íY5ÐÕòõò›[x¿^yUøÅ9�d\o™f^šóx%‡T–ræé¤·þâ\o™§\LzóI6GÛœ ¼ìRÃú�úéÑOc0¸›D˜ÉpÊK×>øâMÛlW�ï_FiEøÿþöã_œ÷·LÁv¾ôÃ�G®hfZ4¤n�u;dêH&w=yE÷a[Žtóþ£Ž*/£¥m{íúÝšßÏ_ûrCtõ€ÚQ£z�3 …NÙá†Gü¸oõögíüK*l‹7½úáÃs–>ßÜ«exÝ¡ûî0�©¨(ïœ]¿Èa.™òØÜ”t†x~Îö;^ çØÑJ,x{¶~­OxOþð‡ÚË~²¿�� ½++¾>e5¥×š·Ñ…õ~!f”¼öš«Êwu»¹¹YV?3s7eeêÞ KpéM!„ **+/Ë/Eè ACzÙ&Â%¾'´À_“Jú'ª6óy1¾¬%ôuìrPãu×]“ßÜ&ï°(‡K¹©ª&OúäpbP)V€F",ðÑíÎu(—©ÌŒ7îÑGɯêÏ?wîwÎÏpâK/<ŸŸ‡¨¥¡C‡mlj»Ææ4§‘pÿóóûÞA=«o��X‡É¥ )¿¬É0¦Žj„�¤…IœÈ¶oDÑ2.9EñÆZTHÔZÀ¶H”õà¾çKLxî¹c�<Öj•ð|}úôüÀR†CbñXqï<”¨“™8eÚ½ü/uNœ%*ÔS®ì¿d—®*=ÈMš›[)S^-à;ÖY> mŒ×\ÑØÇ~þ_Ê :”ÍÝDªSM˜†¿âmÝ,J³ ‡ÒøºÞx¯@Õ€q¢ëL>(˜Áaå¡\y}'–—ùI*aˆ��¥¥ —�%!‹¦…t{¢‡ªX™RbªáI›‚}B�¨îÈ£¢JT©KN�ŒN4®8i`9»&ð«K]p"˜”ëKÍ)H[BDR”Sû™ ^U (ÓˆøñÛPƒÊtæD-?¤,À©× q hœÛ+7r!‹Tuê¾[iF`‚HÕãÊ«&å äDe£IåÐ¥Ó‰L8ÔÇq"�xl> (K'òHè?ЪË+Á?@IDAT»ê0I_VÈWPféDº“áDÃ@¨¯4­‘à1‚ÐP¤[”UíUòX0óÛ¡Lì#9c‰?}ú0;hlÙÜ¥mþäÝ�¶´…Ù…k”G„%l²íŒÂûpí“Â7)¬hÞæ›ôî=ÍbDP³lìÒÀ¼Ì}YoîA$Ç�wà ~ñz¾-næM¶Ðê•NkÏ_„ Ô‹t\Ÿ^ŠD"ÚrG¸CHã[Ä {ä@�Qª­ÅÒxH6]ñƒ’-æÌM?Á^p�ÆH�â—]´� +U—llhðϳ¥wñ áèĉìyä›`1M™ø£p=¢Ár81(Ì ÷Ëo�2ƒ鬪Ïp"Ó8� wŠÛ±1cOðu½MžüÊ7¿ù�’r½Hሇ)S^͇ñÔñã[¢qzŒàTm™‹!˜~òëP\›iÆ[¢ó¨R‹æÖŒ`H›Ajɉá^ŠPÓh÷î=ò]oòuª�’h&–#™¹£ÿ&wœÕ»pÑü‘;Ž´‰nÑ?ÿé³Àö�Ñ£+ËË8å!Éš“ã7¡Œ›øûÏ�˜Ä®’ HAŽðõÚZD¬3qœªu;m Á|Ìñ„΢2Þè— ÖŒ*>�hЀ•¿¼ÒhM±fÖÜC¸lE1kK›>Á‰|½ëׂÎef—-zî8Ñ77qÒ_ú&r£Ÿü&ê¶�…¿T~ôL„ŠDuœÚ¤ËjA+9!T:G T§à8 ZˆÌ0[¥ �NýŽå¹àŽ÷çD�*œ+›…Aéà¹ÄTÊ êaWœÈsa‡íQ䜢\ NÔ†Kw NíÉ7‰6Q¸›–:R¶3'R•m« “¸”ˆq£?¢Úpp(ÏEÆ8I³9‘̬Ž0¿�"Ï2:·bœ¤�MçurªÇ‰Ô 4mчN:QhDã8Q‡üdïB'æp¢N½ Oö¢Hï[ð¼‰MÁrWœÈæs¢Ua# @Œ�zÌOЇ…ËlNÌèDé.Z’ÑÐÆ_¡Xœ(«‹²Ü`Œ€ C9œú”Â9Ý':�(Ò‚ÆùHàhX5n|é¯Ç‰y:‘ù°‘ sb‡NT—M@QŒªQ‘êq�AF®uZàDGPcŽuZàDX½À‰�˜±Nÿ£t¢Çç–ŠCµÕô,ûˤ;wÞî�Ù;üÆ“5=ËY‘ª‰Íf$¬#2×÷*_ùî‚›þpé%_¿!ç•_:‘ˆßþÌ%ÍÕë«ëJyW_Z^\ݽt΢×ÙãøLæÅ+ÔÔ—•V°¬˜ 5ÝÊß|wê!{ŒÉdXòÑ"vac·¸Í„0S°ë |Ž×Ý{ÑâUoÕoSÁ¶k•µ¥¨,Žƒ¨ï]ñÀ„k[ۚǎ>ÓÍàò«Â!øÐ3¿~aîƒý‡×ãÖĤ«íU1mÑSÝžï}ú1ç•¡vçs—}œüxõ†eÝúù/—¡-D÷í�ýw¬¨¡8\«·ØŸi²½@Ôè—Ó:%" ‹ ;I–­³ÂÀ#„%KŠÙW×üE¦øÈ*„È!a½á˨~Ëu¢znu¨9¡“œ�€êéD¯_Š{ûœ(Ÿ Â^]Þ„NT[Þ7XãaJ#—áD�ªxË(Ǻâ:Æ †Ä2ŠŽôË<:<ÍæDsZ"%ÕOu¦7Á‰ÆÅÀB¡l�Øa;'iÐô€1Ô…�Zh Lîcȧ.uLÔ:hY;éÄlNtä`ÕqCßÔnŠ4‰™¤¼‹üã“щ]p¢fÄGÅvX©mCÁXZ2Nߎß'à!…9À„. œºȉrÀ[V£�‚ÈZ6LQÚAl¢Ád£AÅ®í’;éDÕ)�Ë®À‰][§Â¯#LÐmc7øÒÈ86.pb�OâRA'šÞÊš'¶ËŠÏQ'º¡ø|¾Ñ#Ä”UÕ”~Ô´�Ó2�¢eŸyýÖ¢²"E“¹ßÅ…TRiQM}y÷~•ó–OúÆULŸûr$ÚšS¤¹­ñÙW9óX¸ôuE!At¼Ñ WT—¾8ûÑLf\Q�½|1tDÒ¡¹É€ë™é¿Ë>-ô•yϺ 2G¶.Ñ\K¤ewÕë²_õì…ajSàÕ�T‚[ö·Šû¢K¶_ }5¡ 9»>~‰†9T�™x²P¹t¢(UÆæŸjêÍT\Û[áÎð‡\­i8“v¢ æ1A«5· ®þú å¢]ÙýA œX8ŽˆjÍúö5ÍÁä'&•ÐvY ±´SÛÔ‚‘s~f»ã�TCUT|ÚÔ5x°Âv7B(p£G ¹°Xy¥é¨÷áÄÜÞoMªu …ô…\0²kçDúnKÚä^…"‰È)ãO›4éeeíœþñÜÓGs4T1a‚Oü× cO€†!Í$�5çw&•X³Î•þbÿ:-‹#îC1oª hôrd½œ°Lß蜂Ppnhç?+@6Ñ8�2íbg¡ñ!‡Œ¾ûî»sN7Ä9sf7¶4×ÕÕOxéÅ|ˆ9¯£ÿ �MÑ6›áDÐI´ï¤·6¿œö„”L§ËL'&D¬Ez¹@‡tö)Å-sJØCº«ú¹%A©8ÛñÉ·╤µ(yò×̺"UįŠ2¬qªà]�Ñ8±O±_Ò’[Åë©°ˆ9 ±mH²k‹­y²81 C«›–¯HœÊƒ 'ªÆ�/¤b©ÈboèÉDl&Œ®•—„F)†ˆG¢}ÕôIu¢$¯6šSó´ë|%]q"£5 ¢F´�Ö(3T6ä›Á‰%:Å’ƒpÕ¬ûéD ;PÓ.7Ð7 Hó9ÑzÀ#¤`$(f[ŠÙ 6|T ŸPGq†\ø2=ŸÅ‰F ´%ø…N]�[´Bó:ë<€¹¯ÈÖ‰¢�#ˆ2NTEˆ{�HiáͶ¤ãšž‰ˆŒÄ1N®Nº '’›¾0tÚÕÒùoµ)N¤’8eè¦Ö”{:Ñ$›ª²ÁÉåÄŒNä9½x�]¨„îËýjœ¨²Þ[á�3iå™¶ÑÑ.9Ì«#TM%Øs„Ej\D¥XANÌщÒ�l,ªW§lhHôãD"5¸žNT½d&·ÄL�ƒ­Ó'8±À‰H ëÑ�,èÄëô?E'J}Ž ;�h5–U^pë‘-ÙÀG/u*jJú ©)¯®dEj>,�ÎßgÞqï_oŒ´è$§ú^CvíN6UUU\¿M%s¨õµÞòðbQ¬šðNÛíݳ®÷ºÆ5‹—Ï�D›J9Û¡{E]¯ "Úd†P�Ó *ŠWF>8åŠÝ"-òg±¿[Ï•úh1©t¨eX—^}ÆUûD[9*ÅT||ƒFÔ—¸=½ò¡ÌÜ) ­l|ïÙ¼!š²•;™'™ ™LZíTTFµ}Ë»mSAø^e]!utÊe#ö�}åz ¬ÂÌjX½ô‡¿—Œ§Ë˪v¾«´æ¾7�P¾pYqU}Y�¾U=úWVÖ0çö*«J»õ­d‹¡õ«>üþ-c±d¿^ÃF Ý�¶KW½·®á£py§^]¯JëJ+âo/�zá/&aæo?hgNuˆ&Ûpº­mX‰ï¯º[YÏþ•=±™«€3Ü¢ íaûåµNÃI 8;Ù•˜of‘"¸í ª2`�aöÊòÕµŒY›7ÕÖl6¿~½Í e‚YÅz©§ þQ�UFMÒ`må~U×Ö€S5•"7‹~c¿3Á€ò=[Oóƒ8ïZ¿ÍÝ)ØòѤÓõÝêý*íµ×^lEå=\Ç;Cã°Ñž‰~qjÄvÛ o�F­Ë2 u:Tj@“{þ0벦ü ’ÃÞlm ¢ŸÁ®.U å5ßò©A}qþqwI¢üÄŽ.ËÓþÔf JåìÙþŒ¿•••‡~Ç$:B›,kVï’þ á’¿555ùòð¤“Oö°­b6cÐ_kÓk8·þÞ}úqÄ‘$” âé¥uTÅó-9lÊ¡ §2Ûò°ü̺™“s¡š¢)µîfæzœŸxŽc„w:Ô­ÞŸºjëêŽ:òHŠª¾öÞv´kdø;ìÀÍ­vB«¤¼š HB†žZ�m2d7xŽX”ƒFd•e}ŸÊr­6Í…|àíª¨×ö¬(«ÒNTøƒ(hÊ미ª¶´¤¸º¢ªdãšhkcŒ³8ß_3ë½U2©ÃUÅÝ{TV×»ÿõKiJB§ ãªëS�%Ë[PÊE'Úm¹¢¤öHÅiÎu"Íq…üÝzWpÓË� fæ7í²U\1ͱé¯ó=’ß­_ K\«{”s®By¥­35ð\@ —†8w¢¸´ü4|‰´$’±ÄüeSèAe]QIYEEM)PáG«¨- ÛÎa”%a¸·qM¥šbÍX´ªÂ³I7ñÖLWE”\iI¸´„[×…£-Éå,]÷í*[ï \�ªŸ`5«t -ù�ùôî@öíÂáKi�†‰oà婬#LJß\pf ËH†§±V±»ì3¯KÍšr3?IÀîÝý� lOŽÔ¤”aEï>el¨¬9dW%Xàk?^ç;Ý뻳‹=‘fDZv¿|šoKñ†›”-GøÆlvù+æã ×[}…y!DůâÐ�xÔQG»Gæ¤p€ŽYÎr’!e·Eÿ&yÀÙ`Œ® cšù§ Ä¥Vd2üdvf+e�uí_ŒŸ¢˜¦-f5›^ h�H]†’p†TF<±2´�^sêúº:ßú�;¶·V óPÝHÛsO8 ߃ͣ�¢#ÿ�- 4#mÈWDRŸDÜôŒMMmm¾ë­¶¶îœs¾­ª„Y«Ïj?T­.µ`c®/Ö7ÜÅ[™fóúA}´>(sç¤è BŒÀž(>Ny›Þ€s‹óêœÙýÒ+ å‚ Ðp; |J@’ y Øëª¼¦Ú'C(´ûn»ë;ß¡›éP¹«U "2`úr7½ùÍ£d"Ýà5ÎæDåÍKˆTºÍC è9œhdœWL0Ù²*E¹X`<¥À�ÁÜ´ ‰*R¡Ö¶Øø3¾~ÝϯίkâÄë`´¼ô•¯PS[×k¥#WÐ@LŠpêüá O7I† N†»§ûŒH;›ð”÷&¨S¡Ú_L®m�%:*Ñ‹0.Œ×­[¯ªóRuM 'Ñ¢]ŒÊ´±\,ž{ ¾®·—&NÜ~û‘­­¹qì8[÷ßï€Æ¦ÎvÌæÄù´s >¹¼–½ì5GÜ­ƒ#:®É1üo Ü ØƒzM�ò< Çô™¢tŠÐ.CTÝ9®<»qg¬¤Únݲd®?þx-o Õ¸`¡~ÄCœp$Z0'®_ë�Ònݺ)š.'zƒ�©½ýBÜjFÁ?„ÉesbP)ã ëžÈÅãD* `Q‡m—Bm„µ•@T ð âQ ¥„!�µU:Q£>#JÉw°.8Q-féDrRB:¬(¥½Û6› m0ºÆD¬ Ù-M�„ò%'uèoƒÖF‘‚ÙœèŒ-ß'»bÿø#ÊÑ_ydDJœØ ÚŒýøw‘™$z öCÐÓ‡N¤xל¨jòt¢ˆð é,N,.)…$ru"( ¡¤30ò!�„–v? Љٜ(aÉ~TbŽ0‘�êPç6Á‰lvJ‹Ä .Å‚`!Lo¾¸IÛT”ω°jûÛ먑VÛº#1§ÿú’Yj $ ð²U¿?'5ª" �€¶ä ~j±zPãÄ�Èàa¨Ð;ËuÁ‰ÞkÈÏ Xn“LRdZÜÏŸ'";Y§NôµN œˆÔ,pbA'f[§ÿ9:QjãóM¸ÒØI­xûâdLÛ¢¢ñpáE²µ–¹   ë1 o�L\&„zÕxΠ馰&Ôù•—+wïSI= Å´k,�oeårœñ“FYúÚ­OEeu˜µ_è;ô8ÑsÙIK'ÁSZTÛ½œvUPšÝ/Q 8”„H(âÚÍÍJž²‹jË+ìú¤k`,“¦ =⛮іâÝÚá¢î’RJ•â.Ä+�¦ô‰i®ì°Z¨PKkÕqÎ{-¦§±H’O"&O( Wì|åCååTÞu§s¡Þ²ß²?1ɾ´Ö)Ó'F@!ü1� t£ùv”ìMÊ žêŽî™ê~„êjý]W !ÁÎÓ|¦ÝÆ“�§Ä-Ð.UpÜ�/Þëºu×dDUx0øfÓœŒl2ù”O}à—W†VÒx} Öu¯WóE¡ú€ø¯ùóvø”Ù:ÀU;hp�¨A=�±ë’ldÎIL¥äÐ�€LFqû㜿ò]€î–åðd%‚ É)b曪ּBSÒ 9-¨ÆLE ™%÷sƒTQÐ{óßyû€ƒ¢#2ÅÕEý‡øí– º· šê�ÐýÇ8âÉ šQ�3�æ$Ó[îPëjÛüõ\úá‡9yØãoÝÆ�5µ5ÔB…’œnŒ˜º ënVfõ�ÍqyL›oSD¤+R€~I=©ed(€EdÔ”DïòU¸J‰ñpÅ!I“x%œ,%ÄÆb�–Üó`Á‚ùqò‹çBsP«F÷ÞÉgÌéä~Œ¸Âhœªs9Qmù$…£Ð¯Rèa'úðni†©a‘iÇwÖÁŠã*ÐÅ4ÒhÏ@d÷ñTbï½÷Æ™²1ïìŽéS¦úžÕ{úé§³¢ÜZƒ4GÍl…Ô†æf3úƒœêŸé`€2r³í8ã¨*øÅO¸)yà›˜[Ê¿'&p¸u ¢:@‰´ådø,·b}A ™WUÖŒ>tô¤—'åÙ¸qÃÃÿ>ç&?9 áÀE'šƒŸä(‚œ†Íé̾WdºNº²F†`À'J½ÞÒ®ð„–ÉúxÝò-&¬BÄE!<†>t0ôÇVª}*ö·¡ãÄuküÅo·nÝÕ!Òª÷_-ØÉ­UÞÁ‰¾€qÓsTHFœÌ8Q=—Dᯗ HP¸y˜”A‚Ñ*’âçëDjµ¯¶ù¢aÚ âD2fëÄù¥A¸+ê˜ÍãDº®ü;µÊc£®ð‹±Ñ…`ÐêpÄ ÿ°äÍçDƒ_ü¯ $àÉìq"òÛ(Ñ<9Ž 4P"pj)¨Ù`bÝëЉÚj�Ûþœh]6ÈÅ J¢(¶½ÖNœ¨Ž8HÚu¢²™å &L#8…ÕhøPµ T´ëÄlNt¤AN:=CN½‚°-d¹ȉÀ@WQ²“€ÞÜ ¨A�4(Yœ˜Ñ‰j�<d ¡ P�*Œî!Q#RžKZª„EKB6÷UF-©UAF'rS4í�c?˦b휘£U‹©Ï[oÚ8Ÿ;ëDA( ¡|†ú'‚GM]ó·À‰Bƒ¥'ŠQù_àDð ñ¥OA'šò”ý�Ò‰Ó~ž<�R){®¸fe¦¡=ó±((=9תË*ª0«MÔ¢å òä�þâ“—­ÜÞ§‰¤Ó XÌ->z=K¦…³*Æ�Ä•†_)S§´½N2’¡¼B^­œ YY²ªËº¤¹0Qoee®`Ö“ÜKiVk´‹: ?»9•**˜žIÿÚ K•õÇ›z‚7SFù²x¿h^K^z8É „Ÿœ§ŠÿŽlø+Í ˆ£‡b·�¦¸4aÝÁÀfÌ… %?Ûn»î6göì|0Þš=g·ýöÉzÛÎç;Öª”��yˆI6ìü!8GBHxh‹¼ŒR„L߈ê?ø q ÔÈD<„?Ú‰ÀûBßõ!qD%~#3çÉk `•¥S#FŒ˜;7—Èñ8ïYÅ T»wƒ[äʼnláC ߤƒíœÊ™óåpb`P'ƒïæ¬ô9‡5~ÉfæP4´ {1VqmåÔΉ’±òÇ)ÉÞ@_:‹ŸzÚi÷Ý{oN}ló™³Ó' ´ývÛ·¶EÂŒý�¼˜’“H\õð™é©!Ȫõ81,†± yœèÍ(Û91 ïgi¢$–×Rí1>»9q!åPbÑ¢…¾ÅØ -IWÕ"Ï‘š$B<6þ´ñù®72­X¾,¿ªc�=.–Œrhb' -7jÃ'æ](:Iœü"EQ‚MÈÊŸry®Œ.ô[BE –²æx’dõ/ÆDŸFÐÛ0É Áƒ|à …ï _ªm’ Aè |µ“áÄyy”ïj¾ýpúyœÐo(Ð*DŽ-}щ}!£—À ¼z h(NĹò’‡ÚwM q`\”,ÆYô‚Ë­Ò‰rVû DC²»àÄ�¸€êÎìÄOĉR&6ÐjÛO'rÏ�,3Lì£ìvtùgÉê f_'�ÈhtÁ‰Ð·¯N”,�,ËåD;ú3G'BºÊŒº²AP'ÙÀn�jcxütbgNd?D)í$©R´«p_I£.9ÑùÙÀ<ºÖØf…†iRò#�a."ZI1 ×(r…bpœ«™¤Ôx95J5Ê´#Q¬wÁ‰d˜Ë“RºdjˆÃx;8±³NtJÞE:û•|2hº,ÅíRæ¢ýFðßÎ5ç“òß¼Ô ÔY×µ.k°Rf›­¤¼>qÐ’­„fVw»é±y°}J¹p@lí<ñ µNq,ØŽÝf«š ”ñ'�(;A˜u†§½‰7—)Æš%bÅoÅNOùÈŒD"3§Mcª#3L®Q3ö¬B�– AO¼ÿ~~Y"M ¢¶�NÔLÍ 1˜ó/`‘òQíLù¼4ñÅŽýDzšÙv»í4›¶ �ì¶ÛnY½ËU+W¾ñúLf XÂZŒC­2ݹd×a½Ñ5mÁ]aAÓiõHMs‡^ê6Y’·ùus‡ÒTª0IO¡\ªUÅeû&€Ñ$Ñ9¢¨C?|3ê¦y­bÀéÀ¡z>tèP…ó䥉/NÜÀöLyêå(Z ÇGÓl9ñ@‰C†ë™zi³ ùWÕí¼:³nŒ�¨êCÚM/ëQÇå£�nc-ž{CÈX«ˆÉ7̵ï~ûùVü—?³S˜õD£#|o)'úV/P­wÂ3(ÑT'ú—Ó]¹¾˜ÿ!N XgEÙp³hÕí®$:¡x±�>êhDAp•ON;m¼BÞˆ;b˜”Ɖ†P ¨¤Ä²2EÇØš@È'ãÄŽæ¼+p>æøEt*ä{ÞtÑû[^ÝØu×ÝŒAÑB8€ ÒD·'æ–Ðo • † B'²ÄΣ^ÐàþuÿT8‘!4é&LCÙ&­%…é€>tÓŸí±Æ†dT)� 8¸T>'zÊ9X'ÒˆZN½ª2šøl9Ñ´�Ô¥»æD§ )æ.`7±b]ñ/©6Sœ˜¯ÕC“!ŽÃ½**õ¥Ï¦8�/ '­xÔ@0:b‹-щ`Û °a�²yœhT£~ 0�ŽåÔ—•'òÁô€¬¶X'"Ï áÆÈ�ß¹œh²Hdát"?%î�¯¾kNT_\u’©’³²' †C°í@¸åZ œXàÄ'tâ—K'ftÓÿ7R‡í^�ÿo:å:bâÅ~‡M±é.Rjsâðf97]ç§žC½ÚÚy¢)á/È:-.M¥Jun\¬$ã�³S,S¬qÝN–9¡Àt;iÑM‚ÔìÑÒ¢t©& üî·¯¿sáÉ¿?nR“¹(ÎÉr¼ƒ6£ÓYØwÏýý ßñØ}÷=I-Îb%‡�ìCŸÄ>:å/ aÙ¦ØÕLÕ°íùÈo�|ö©'}Š…B†‹ÛYv:¿+¹ï~ûûf»÷®»bmmÇd%Ê�øJö_`à Ìrë‚&W%‚N-ózE B„ ¯Žq§—pÔ›‹}ȯ^óNM¯ä'"Þ•÷=i2ª]”Y~%�uŠ|ØùŸÃï+‹}ñÉÛÖÜâTM`²ƒÞLëE>�I§£©än»ï‘_ Oåý÷üŽ®€X6¨4ÂTo8ÂMhªŒ=M8�¦ À—è–Š¬Cæ­±ùA~Õd‘‡,.D¡�WTVæg\¹rå‹ÿøg*b )Q}„¤&»\¡ÆÛ ø ØØ~ÆÅey> (…¾ÑþCɦT|Éá¬0°t\¡ÖÀê!3 ùåâ�˜†ÉuÛ·3¬5UäÝjï½ööÍôâ –~´<¶86HÂoþœ˜L†»àDI‘ßDks €nÓ=ó9EЙó‹1LiŽÙ)ŽÓeõƒèMƪƒÙŽ JEC‰áÆ )Þ ‹›(*+ûÚ±ÇùÕ×é#~à!Å"q΄z´¹�áWóZq.ÎFT Á)aÆH“7vL‚âuÔŦ9Ñy.:5i? âx¢„F¯+ï¡ðAåŒ*œ¸zÕªwÞž—_Š;ûî·/“pˆ–­ʈùÄe¬p_2ÕÒ;ýŒ¯û–ʹyÊ)§FÛà9NäÆÁD¤�oÒ"chZÃȉzâŸ$£è´Î¡sè"./&XP\®o!5DÁ{2mKì ÞŸzüoN/Ð!“Ô'>(Þw�q"ŠâŠEâÄÕ%R%|œ�¨ÍæÄ >+†”"ˆ^i(yZ'"MôÖÉ¥A—óD~=|žÚB£ds8q3ub5À‡†…J…oš5‚ž‹Î">Å…H²-àÄ.t"€°Í‡Lëc+tÐ÷gƉA:± NìB'²)m'æèDi` D«Q€¸\±ºÚÞ¬ �˜Í‰‡‹þÓðA×–¶N'J-qbF'Ê#ÿ0„+.5mëq"û¶lŽNôåD§åéÓCï4ØÀ-TšæÍ R!›stb)â2ŦÑ7ß'fëD„+šàÐæ'Y§N,pb�7a�t¢Y§ÿi:Ñ×h,Ü,`àsÆÀVοpë[Œ¹3f!Æ'Ʀh_67U ó)lBûÇ•²è–fYÌaC©c¾ö5_¤¯Zµò�|Àü檰òº`B§Ò“&L˜1•£7|Ò)§œ’ˆ�‹£â�¡OnEÚÚžyòï’ÏJÌâÌWÀ·ýøý=÷675¹gÙßÄ\wüñV€Ž¨w‡v˜o{¥ßuç�Ñhs•¹ÌÓ„›\ª8N‚DêÙgŸ½ø‚ï]xÞw®þÙ•«×}¬œ¡p×�Þü*p0(A7Y†*óAù²À¾½ •¤¥zÿ‚æ=Ñš)&ãxú–°ÿ”}ÆŒér-ðI•àpîùjä)Œ'âãÏ8Ý·‰7ç¼ùÌSÏ"³¡ ™B¨ýç;ÅvlüÃoÏ;çÌK/8ïž_ß·‘Ò;yùÓ‚!§1Á%¨o|•:õÔÓ|axô‘‡¦ócï×Ê�Vþá�ûÈ«6¬^¥Æ‰_|qú´©¾O9õÔ²‹d¦Âö??3+šEF ö´'Zù…Ú»µš –^sH‘é)]—›žMëhn¹a�¨ñãD¡¦”Ï‚ɗˉܲq_Í(³þ Håð³»´À‰¦y;⽫ÿ©½-àDÚöF™¿ŽŸøVe†Z¡c¹`ŒÇ>[N4‰–¯1zÀF>' IØ_'RY'Ú›Â�è°íbj1nôVVýµ­A�b8=ÒN¶ë‚è™� UÖãD¨’�µ%>ñüÔ˜ð¡7ºÕ>$�‰¬\h„ôT-™¨²JLp{4A?�¨ž¹fŒîÔ_ãDAnF¨D]ñ;µöôeåÝ#ÀÔÒT*RøߺæDAK•&Õ_µ.­ƒV� Nž ×ç'8±À‰�ˆ,øéD`…¯¾h  J½©Šì³c’ |,?� †Š³zdÚ|ÑÖiq2ÌËOmÝ%ðàZ( `Š7ÂðZz¶X¥œY—ÄfÅ Ô|ëJ¦"N© x™8qÂÕ?ÿYsc£‡ &R̼£±~{ÏîÏÝJ­‡BÌTGŽÉ D¦¬Vóâù'9‹EWÙé/zøî;oklÞ@n ’+‘nmn»îgW¿òÒËÙ93×g�uv˜ÃH4•VaG ÐìÙ³.½ðÂwÞšk3&"­8OM„55·<þÄSçž{îÃ=¸~ýºÆÆ†÷¿wÝ5W·)V¸á;ij~Ed ° …éѶED`ªZ|^qkÓ¤Îy3¿@8Ó&¦˜ÞD±É ­*¯ôõ;6eÊdâââÉôK/¾xÓÍ7M�1 ŒR¡J„š >|ÄÈ‘™ª³/þü§G~vÅå«>Z.¬Òió‹°ÈfÍêå¿ÿݽ~ÿÜ—'¾À‚âõëÖM�üÚÝwÝÉtÄó1ßmvM�®MNsa>ÃJ†“sN8a¬/ðDúÜzóu¿½ûŽG¦³U ¯®Òóß|ûÖo¸òG—Ì�3›lÄ+ýùO¾:‰±'õÑV\r‘øÓ Æ.‘KY! ¼•NkWJJhêà—¼ªÄl%YÄLöù�ä9Å8"/\Ä™¤çœó-¿ŠÙ«~í�/¾ð¹§Ÿâˆa Eñ81‘ˆOyõ•+ré¯n»yéÒ% zõê•¿øÅõ+V,ÏçD³ñÅ!0cÊ«r’%Š^œ8á–[¯Ÿ1sº&CJàÓùL|࢛¥ÒGXŒùr8‘™1T‡‹ˆíDù¸ó©-Z6Ù£GÏ EÄ™fN8á„D4oà}·Mñwp¢Ð¦ š">\øYÎþiÏH5ât^ÕÁ‰Ðf2U–L–'Y#è?ðÔvý�W=ù̄ۉ3Åœ’+–-ÿñ%/[¶4aöÅw¿wn<+M•ÅJ�ÇF¬€nœ�%ƒDG¦žSOßáTb‚™À\ ;_Úë ›ãD6 +¥çétÿþ2¥²/žyòñµëת+î½ó׿½û—ñX ãB´XI˜cÌ…ŒÂhƒD"ÉIt—܈:æÓ8xüC \Q;~ÝššÚc�=Ö/_è¥ ~~ÕO7nh`jNÞ,:�§ï»ïþø�oÄû¨£hŸq†M'³Ó b_�¤vcÛΉЋù踩4²tp"ݧë`AÛ$ˆ†ÓeBž8Zrš^£×X © °­Õ‰¡¢2μ’û�úж›m+�Óâͯ‘áD‘çÈ/¾Ñ0§†–ýEMŸ˜ÅŒÄܱÙV–NTà3ê(YBä—ê† Ôk¤Àgȉt2TN–•æêDÈá"['"M»Ö‰ jœ˜­câ´°ð»!¡oÞFxAƒ¼œH¤J*�TÑ»BÃÄa wÖ‰ 4›µ‡²¹:±kNôt"NõС½))aK×lN„juܨ�_¶NT\š4ž‚<³91G'š�êÂå‰7V˜={Ñæp¢Â"³t"‘ïñ’bŽºîš;ëDY JdXàÄ'8QÓ©NÖi� :ñË«}íºÂÍ>o lÝ<ñ ·N™ ÈVtæsC947wþAy„ôLwä ¹º0;OÓ:M#Š¢±Ä™gžÉÆjù'9’ñâw/8ÿ»UUU‡ Áé¶aãF|U] Ò\�DiÇüDÇ` ,1uÊd>=zöÜvÛmËÊÊ¿÷ÞêÕ«‚rWWW?vlK$.ß�’÷´µ&8ðé§Ÿ&žËÝÍþ&öíÖ›nd«~ýûoÓ¯ks3õãuÊÎ㮹yçm·ýèŠÿÑœÂ^c°à"?›wG y í” á`ZÈ,ÃM+dÒÊØV>þöíÛoéÒó ýæ®Ûï»÷7ž#zëÍ7‡Ü1|›>ýl C±Hô‚ó/¸øâ‹ò r‡mø.ÿÑ%ý ìÑ£C¶fÍß(ÂéS¦üs‡‘Gq4¾«°‚z Ÿf@5¸ü—}ëœoÿöž»}a˜òÚd>uõõª¬¨â(ãqæâ¹é·÷Ü9hȰÁC‡R3h5ü{œ“ô³ ’À5ÿXeªš“³ã'�ôT, ù“•„gŒ:re®\Ã<˜Ç“;î8bŸ}ö}ýõ™™<Ù�=ò'>={õ0h ÷W¯\¹öãµr²Õ¸‰$ÿp¦ =á—¶?š§*§Ý΢¯ ô·ÇåÓ@ÿÁö�Å¢ï¿ûÞÆ ‚2slëÐaC›ZZÊÔ¼ÚDªký…#±ÄÑG…›‰Ññ­�sÀ_mhj37‹ 4à­'6¥äÚæOQÑÀAƒ}+yñâ]t~Mmm†ÉÑù…±‘Ã…oQ ‘!OøVäeð_Œ ?ùIpáÕà’«8�}ãÌ3'Nœè+ÞßwÑÅçžS‰x<ÞlܰaSâýû±¨ŽgÏæÄ|¼;"a΄ž ºkz|¦ õW™2œH% Ê£�pUP£À!¤CkÖm§ ½ñ¶AuŽÐ 9jRN*ýY=‚Ô€‚XEÕ^íܳ7$dÃ-BšQ5 î ýx$kUÒŒ>Æa†:/W‘YèºC-¾œhà1ØÊíøAŬ ]Ø}Õf=RÝŸ 'Ò´¹�„¦N:Q©^ñØúÝÞmyj¸aã 8…)ë¼r‘Ûè]ø¢0wè¾ûkäÑ¡騔‘=£Õ Q†B}éP¹»gU +Âg;'Ò°�‘P¤öSØWfe¥rÁŠCO Yi§gê’½˜��¨lM•4@,{' µIƒkFÎHn[³–™,²)ÈK器:…ôÑCC XSÖ]‡ƒv�ˆûš‚J€¶#Ä@E¾:Q±øô�:ÕœÇp Æmäh˜+}+ƒhĨ €u­¨ÃjM¿DêV€Jì�.UƒjTKd3 pݦÍYÁ-²N­ U^àDÃ.¸» ²‘uZàDQ/t—щÐb� œ(iôYX§ðÞÿ:QBµ� øB1 ;ËSAòÛ©3`2{I&…*üõ›'~áœÈÊŽã�»64¢ÚÅ,P\CöÎR£wÚ½ÆÛ‰‰.z¶¯Ðõ?™¨«­ûù5×p;(1ß^´`Áâ÷w=1ûöw¾³ã;ÆãQ A'š[SKµœõT9÷q{½ñúëS§¼Ö…ß�lÿõ_ß"6ƒ�¬ôž–±¢§¨aS—ßpã�]ÔÏÔzù²e³fÎX0ÿ_¿›+ûæ¬7׬b1¦_¡2¶ *¨AÑ3"ÅÇéÒzmÑ1f‹æDLш`\»¨À¥šÔÐaCó2z7˜ g;~sÇíÎBCE±Dgå÷¿aPYîoE4ÇM0ùÏLûóókS?ú#º°OP§eWËPO+ò¤·FRÇ?�L~�™;� óßy{Ö¬7Mòõ»¹œžFÛ„Ù¢$FU“¿Äyma¥‡‚2Ó̆?¢uÿ¤-µã hT³ëD`î1fþùÉG@¢¦_„Àj�¶¶Èå—_Ž3¨÷×­];wΛ|V¯Z�=dÙE®|cö¬NDø :4;[öuÜs÷^Ø„‰©ìœ™k™]šbѤ“#ÆNÉY8œB·@†b453Ÿ’�µSÿþý3Uå\œrêi­��¢°¨‚hŠ"6€ÌæDI!ÛçM«µÿrI{+¶‹­BGâÊÇ.}DªÖ„ÁÁ ŽÒ<4`ܳ!ùhÅGÓ_{möÌ×»ð»‘ÿÛç~·)ÓVpyÕ«&N¤ðÎ 'Š÷µ€ 0*7î¤DLQ#¼ö.á£Óa‘§êf†EŠô%]2h |¯A)›qI/|w‘%l–Òæxþ´(‰Â#íòV †7j FÕq¢o[Rå©Pe"TâÙŠ ¸¶fâ½­µõÝ… –,~oÓâ}ÄŽmDuÕ¨­ÌÚ9ÑÎÌ�Ýõ�V5I¸VgN ê5D F4bà´3'�É‚|"Úd¡˜ð¤åYRì®6BÛJ�Xšæ OöKÄÁfÛøÙN‘F°èU·RáÓ �æ¿�‚H²Èc9©ži¸œˆ0Fµ”IÜœ–Ô¾(ƒ~R©4 Açg̉ÄpÇ¡X¼8ÍÖ‰ì‹øÔ¡Æ€¨Š %¨Ì®tâ&91£á1ÂdÁ¯dÒÉ’"¢ñ‚n'bÈ!žJùÒ.¬„@îF&Y¬+ôa›°û©Å¶)À«�-V˜'ÜÊÖ‰p}ל˜Ñ‰¶ÐÕœiÄÓ––ʼn é‡ì(u’ZÊ–»Š®$ÀZà jFÐFšñl�qAö $lUŠ˜Ã‰9:1QŽ+Ô¿kNÌÖ‰Æv&Ϥ† œXàÄ'8QóÄ‚N”uúå׉ÆZávŸ/0J¶fžøEs"ö>þ!³ý�燉 æ—]ctjƒÇMîtÍÇæy\ðPÓ̹H[d×]wýÞl î?∓N:¹-af`ónŸü[ó[S9e9½�}‹ÚZ6eýò:„‰ÜÒ–6lÛË/¿bkš 2~Ö­¾^QiFÁ’¡Y �„~³x�ÊíìWމ3“&íÚE‚òdh�vê©~y}î-^ôïiS& —ª¦¨µ5zì±Ç7fŒOÖ;Å^W×ßp³×Ot‹|�I³aæê…:Kïã±Ä�/ûÉСCËlÆöäúö÷Î¥N/e.Úodþ2Ëv3_ª7¦Y·…'druº¦Å� ÐëÕköÚEGÕ;× æ`‰²òŠëo¸É÷X‰N�uùã´ñã8ð`5ß™O Ø2/¿²wß]4íµÉ0­^w«C>‰1ôF”>œHA0bô#މnà8ˆFã§Ÿq†O¥¡Ð€vØaG¹ÇD»$r¹ÿ†G¾h’[B�–‡é)uZ]B¸˜ŠÕq-HW4@€ÇWci°ø¶ýÉo~ó¿þ«_ÿ¬‰¦Zü}´!o•>6¬šS pb4goÊ ÆŒ9ž /•³ül‚š8øðDÝ¡>ݺw'Ô.¨ªœûwÜùë8 á(¯žû'=å¤7¦A¡ËZÓÿbÜ}$…”§­5²ë.»$˜3~¸Ä;koµ ïE'–6÷“‚�hr81¨ ƒäp®²Yœá¸"F®r¶ EU;͡˭OA'ÊsMµsùŒž~v¨†`âŽ!ÆGœÈåRÕM`üVÎ-âD:HÂC}¸Ýø'V­@mÔ- û9p¢V K¸Ò¹�HŸìCOmQ³gxØMú ½kNT‡Ûu¢äµ‘|kBÄP ±¥b¹®9‘qB91,6T]¨ åÅú¨~ä b…S@´ç’ö¨†N³¡n¯h,Æ ßóÎ?/?çæÜ2dÈoîþí�ÁåaæAD ¯ÿJ2¡ &ÀÂ+5ºóì„k‹jù ™@IDAT†Ã¥ÿ÷«_xÐAå6q›)ýõ7Þ$�§œz ¨Ð‹}´¸_‚° ²�í�S%–Jň0‹ƒ –œúe·{a‘ i2`—±� y$ @8ÖN¡---‘AƒÞ{ï}x  mâöÅ?øÁYßüfEYi>'2ô˜Í&€¿üåQí¾âN>õmÓ&™°œÛ,— üÀ•ÃVqÎ*ÆÔA±Z>G—£ÉÄAãÈZT–gÊ´@š0‹ƒ©móN,+bçIA ¤%òò¾Ôb:Vs6�Ñ›P‡bMl·(Ü©oÌ79™0`à}{pó¬³Ï>õÔñmm1'ë 6S’0¢›HÀ&T¯�éM÷ž½úöí›_ÙÁ‡ÂJL6‡—4•ãÀvAÒœÄÜÔa«Žmº›Œ'þà¢Í”Ÿ„I¾ùæÛ·*�õ¶] —ó)F03rEÅ ‚ºÄ‰`Ù/)¦/Α×e¡¸ûPœL¦’¬&æt𫾥âý[ç\ré�bQì¦3ð/d8Ñ ݳ°"ö,SÀ“pÞ™Í{àSÔ{P£}ò8Ñœô½b¡H¶‡¤|b�Q|à§¢n|{á»ßéxoueZ‡ Oèì'=NÔ¨xî(qšæÈS²eœ(a«=Êþ{çlë]Ö÷½öZ{Ÿs’�H 6&P’ ¨$¡( âøO¥¶´§•Á"èVF«´šÖªup肵fZo c§‚N±šN[ê(:µ .¹ž³ÏÙ—uéçó}Þu²ÏÙûäz®ø¼{íw½ïïòÜ~÷ïzÞßKŸE³©‡ÌØ;o-oarZ¢MDM0+ÜñµóžÐš_Äóm<Þ~ð11£ËƒµÄÝc¢Í Mu„‹}‚^a»%ÒB�Ú˜Og2ÝEym-;¤Ñ#Xq‘ÝöÄo®@Ú±¥þu™ ¡.üì*v�‰ÛÞµi�LPxÁ÷lçä–h"Õ¢ýИñ›œ`KÈ—�!¿øÒnäѬ»ÇDT™´+Ò”éŒö´Ä“ÇDÜŽy.Ρq÷=UKÜ=&Šñ‹R{õ²sì–Ø-±[â®Ùi·Ä/ä1‘a±�¶À9¶Àc\'žóÙ)s1fŠÌèüá�• ³8gåX53:B ˳&>~1ƒcrïÂr4ÚdCòíés¾æ9¿ðÎw½ð…_÷ð‹å†nø—?ùS¯yÍ·±Â;²µ½tÑP§µr Ë=ñ!Â9eOðÉ,eyŽõí?ö£ÌC�²Å{![’d^®:,DÀ§³éæÖôÏüòw¾ëßÝrË-'S9õ=ôqGz÷{ÞóôþæÆÖ¦ëiæšZ0˦Seä‘7Šà`!è2%weþ}óQRˆ¯9J•c[[oøî7œê5š»é<ÿÏÿá}»?‰«7Ÿ•�ÍMüßò-ßrë­·>ý†v'~ðkœÝ¾÷ŸüãŸþÙŸ½ôÒÇÝôVÅ–f9.Üžü>T‡’ª�2M�civøð6|Ë[Þúæ7ÿC¾/r7Õ§?ý†ûç?þ}ß÷&Ìvd{»x׬ßEÓ¾6fñŒâ?Bå°BЧ:R*TÖ‹¤«¤§Î@i"�všˆm#.ø^þÄ'þôÏýÜë¿ó;&¼R"Ý|óÍï|×»¾ñe/Ý™N�mQ¿|jjwK¤|÷¾çáU€üð�¼]…­FûkL æ°áÎR'¶D´ÊGÜÆ¶üfIЬø½î¥ 0 5ÛÙ±«©†·ª�v'´DIÚB×P¤XÂD>–U}øZ®««x6jŸ¿ÈSa‚»S³5Û?û‘·Çë¾c{ÊK8!%OM‘DÞX¶À&ö™*Ÿˆ;?õéÏ~vŸý%ñF<¶½eÏJe mÔ [`ævѧTZLçSžJ~ó÷ÿCVäü¾7½éÙ_ý,º§¬ëŠ˜†Áã!…(Ùj‰»Uß}�v>‡¤ yDq›]vflYHU|á _¸;õƒ_Ó«ü‹Ÿºõ5ßöítïG�ñÔ«‰`Œr/[bÙw!€nÓ�RÈ`0þÉ-qO® V†Ñ>-�(Æ4Êe=·€,ZK–ÊŽøôŒ‰Uè´%q´ä´ÞÀK|$-f9°‚ë´È…b=––î¨ÔÓÔ5ýëàJŸ…–¨±§þc»ÇDµ�ÎÚQT ÐbL|è–˜¢-+ÚÞjHâãÓøz˜-Ñ–jÏ權nIHÝA+UqYM�>aÙeJj9Ý5&>DKT:�!ùÈ(ëô1ŒP'¶Ä$²ï�/Yì†óÍYŽERƒ<¶†è^e¡ª‘è}ZbHPo¬:‘FPPíFK$ƒ´óEîôâÞwKì–hc²YPóº%¦WLcé–XfHŸÙc¢Æp ?ÇÄ¡Äú«-pn-ðØÖ‰ç|vÊ{ÓÖYÑ3ÃÊü*³b´äI|0o¦‘Ì/�Øí€ÿCiMO™Eóë++…ÙáÍ­‹fó'=éIo}Û~ò“Ÿ|ÿo½ïöÛÿðóŸÿü¾Äêñ¹Ï}îK_ö2e"Áöö ô„‘™�Ùßkí†Y4ä·ûÌ÷áGgœS¾ök_ðó?ÿsþç¾yìØIi÷¸K_ò’—¼êU¯¼òª«xÛÀ=G£cVùL¦uœÉŠËG€àˆ’Gg›Óc“+²úegñßüÍ߸ýo?•ü,�Ÿñe_ö¼ç>ïå/ùÁC÷ðæNTî²Í€ Á’ÿ$™–·ìǤ™„Ùt“µV¦ë,ü »ˆépH'sUκ)–‹/>ôïø‰|à?û3?³w7tD½é7½ñ�o|êõO£[¿ÿˆûÜã©Äµbo½èà¡§ßpã­·þ«�üÉŸ¼ÿýïgcµ���¥¤'|Sj_ùU_yó‹nþº¯¿“Quî=vT÷ Å`5­ÀÅ8!Ïò†çé´;\ùÉ�­¹œÒ3=wŒ¹wã襺åÅ/¾ùëoù½ßýà÷ƒòÇ|ª-Ï®¼òÊg=ëYßøÒ—ÝøŒgÀtg>;2Q…œ6�G#¦ç³Ï§5�ì’Ð}¤\ýº.Øç¸ g:bñ±€06•ž�„`à ‰Øÿ0Z]éœEÝ­l㸹yÉÁ¯ÿ®ïzå«_ýþ÷¾÷¿ýþïô£ÝŸÄÊÊõ×?õÙ_ýìW¼âW]yÊÝÚÚØÚÆÛA;ŸØ§;Û¼Æä'ÞñŽT€³¸é¦¿ÿÆÀî~,øï¿ÿ°�S�ÂDX±û÷k‰¸5RI‰«…•�R’Ez£Å/¿çßïÕˆw›€¼à=f™ã£&êð`-Qg0œHª“YYà3B>ef¯#;§ 2žñAËz¡Ò_ ø^wÉãÞòo{ñ7Üþîw¿›Wˆì­Zx#¾ôåû›¾ù›.Z?´Ã¯;Ûúýç =Ìc¤‘ª.*(Ùñ–xÛ¯üÊ^Ž>¡G6lkÖ.ÖŸ>`*nŠØ†î-ÁñŽ|Ñ‹nE½õÖŸúßþð^²ôi¼æ[_ýêõµÉÖöÏ2Sö&«�µ[Aaw7AäU²È0cç<Û©£ ö=ì %i ãUCíÇI’\GyŸë|öä«®|ÛÛÞö »÷ߺýöSv�tÏ¡{éKÙˆÆÀvǶؤªp7¦–:/"AµD,±¿$³ƒ6k�+¬�wµDÈPÜ’Ð�Qñä–X¿yÐEX�) ^ûJRRIPÕñŽB__Ú óÇ6&"¯&r”Y• ÒTojÔ- Ô'Ò°JUºšIßÜGÝuÑJ�ƒž~¦œ¦5Ú*S³Ï|KD|º+5ŒmUÒ1«3PÁ¦eÁݤÁU, ÷«É¥œ²%êY7Œ‰€_(¯ñØ_‘DYsƒ¹ù<œ–¨œB¦Ö=únÒ^S¥¼±í–t§³¡ØŸ‘Þv-K¤eF‹0câý(ÊS´Ä½c¢~1¶Šj'·DøBlÕ7›ZC5%ü…Îņ)âÌ”PiŽ�‰^R»qÙ£ wÃ('´Ä“ÆDˆ+“¥½!wû·ÄÝcb6Eµ²å¯[¢•£[b·Än‰Ç׉=&Òù^¸c¢ƒ_m�smŸdÂø¨×‰ç|vúW÷Ü ²àÑKfj,Bx�=“>&Ž<ôÀÄN¤Š~Â#»¶0¯cÎÎ¤Š„®á\A“kâÏÚ$ä© é [;_tÉÁ<‰Á lqï½÷~úSŸº÷Þûî¾ûnn/½ôÒËŸð„+¯¸òÉW_�¬³H\¯¶ÙW‰è̬ݛ\�_ìy@Ô§xÁìØÍ–x°ƒ_ƒyŠÎ*Ì�GÌL/>¸ÎÓYÎÔWF÷ßsï�wÞÉ{ûP„{–¾¼sóŠ+®ø’kÿ†ScT7œnonÕƒªö;v-ÅôØçŒ.»ô¢LC±N’b;øÎg÷=Fn�ÀŠ]vÑE¸† d‹TL^¢fj�øå:—\|1³cèÝu÷]ŸøÄ'>÷¹ÏwÕUW]{íµ¸Òd~n™òà³vú}àKOË!ìêÚÚäÀ¡uö½VŸ••Ï|úÓŸûüçî¹çžûï»<ƒ@¡ö5×\ ìElé}l{çè6ÏÌfÀ<ßÉ<ÇâàÚøàúAis„^T\9²slgº À‰¬èš‚F¤9t€ƒ=£•‰ÁÇ?vÇ]_¸‹Wânl9tè`ùk¯»î’‹4iÖ‹£x|M§ãu ±TÆFã|‹Ï&UúÉ×<åŠ+¯@cdÛÄ÷u›÷°XÖ´D™  #´ºÚ‘«€’�ßwä�ßówNbÇí¼åžóÕÏå \dZðä’ƒ’y‰ ‘:¬á¤–xð®C�^ëÎ;?ùñ;>NyÑ]÷¥×]ó7¾d}’‡@WV¶v¨/[ CȰ¾F•Z³iu0áÛ®`±qlç½OHS3Ø,¾‚hTJûÚ«ë(”Ü<æNE ‹ÀYU±-�yBÔ—ØË /àÍÅüÒ@" Èû½÷}úÓŸ¶s¼ûnŠæq—]zùåO¸òÊ«®¾úÉ–´&]ll‚¼a¶ÔØp üÉ9-ñò‹��ù9$IHtß}Ûd£Ia,íEò]-ñ²‹MÖÀ^%^•mt÷á#Ö3„¥–$ý¾-áx2sùÜàR*HÉf§uLD|à‹X×í #2]°|0$*ÎÂ6œí$, Ÿ`ÌCš�ª%Òû&{Œ 8”¸ S€Ô>å˜ø�-ñ¤11’µx p,Xã#m‰È3`ïÊk»çËKI[w‘Ø´µq‘C×{ÆDÞ­AÓ"ÏÞ1ÑÉ|+Ã)[âIc¢}0È1í^%÷o‰»ÇDÞ¶Cë¶$ÓÝwKì–Ø-‘V°{vÚ-‘ub�‰ŽLc³NäLÿ_éó}Lü‘_ûfF…§>û‰œûh œ} üå‡ï‚é¾î}¶WR΃˜w=¢uâ9Ÿ�!€@%dêËäJø¢¦ÁöÌÔX|e–é|ŠÒ2 ‹[WÖ¿DòjCæØµ²`¦6ŸÞؘL&¬Yë^v9HÂã±”ínùÏ¥ëŠù|‡ýÆÀBŒÊÖ?Rb K‡;±¼� †ûG76Y}j‰‹ò”ÿX6eši‘5‰]é»[”a£»†UžË¢™vPEŠlá'²‰‡ñ*5°‰°t€ilOaÞ{lk¼Ø¹ôàÁõÉ*�"‰Cæ¢0HÖ„=À05¸êƒ´DÌz@ìr�ÇZùDk4B+µ £}¢‚rÑæVû„ïò„þÂÒ,¸öm‰¤FÖ‘C7�9ðì³*YqÿÓmûøáýtñ¥—Ù>fͽOJÓhü�²%šÄ=â±9™ÐÁjŒ%̉R´–4$bõ+)Ú›º�¨�©=ŽÝ¿99¾¼Ð$}ÕWU¸ÍK­¨žôìÿ|Kù-1ÎDbCŠQ¶$¶ÊÙßY{ß÷¾ß(:»Ïü*ð‚~ÝÑÃÛÕ-jO²VA ‹ô¼µþïi‰[›Û<݉ëÚÚÚõ@°×_�eI [âbe{6ò¦¨·> Š}ò‘ªMË/m„´k9ÃË·²ÚέÞýöñ+tÉÃ%7Z!甑§ùÔº¢qÁÕø˜`q߯1ög<¸¾žîýòË.¿¼ÒTeCø��î= z[ƒMØÛϤdqÒõ'‹óU$›Êi‘”ˆE>^ÙÂ<ܦõiöÝ-1&�3Q~¡.‡õÉÞ/Õ8½Û)Z¢ †4¿¶ð³‡ð—¿Æ |ß…ƒk ~lYŒ«É|¶½`-Íí.[•ßY2„¢†l¢ŒwÎD•‚Kçþ†°R™Î�̶dõ°&ø§Cë!÷\wÊNåfSb§¼TÒR:KÍõ‘{ÝM*º‰àu’5Lœ$kÎ!‡lÁ2—Ç•×$× µX±`¡-vƒ=P†(r’+|’{Äû%·7§HŠÅqåPÝùˆ Ç,Š¥]ð mB� ��‚®‡[3=†Ö}J�Y”*cVnâ˜âƒ®RK3r›E‡2!˜sîmvÍÃñ$v'Eö‰!)&CX™F\xºfÐH©dœé¨#noÑ“ÁôHªAÛô¾Äf6&†Ë2©ˆÛ8¶…£â[,YÚ¹øÂ[“zë·™�Ì¢ žÂæ‹Ñ6<âÕªìH/$§ä?ÌÈú iëþ#>–K=Š ˜a%4­z˜ ©äe°ZŽ���Md[ŸáAk†º„ÇÍŒ§•Ý’ÓkˆJª.¹öØÕqí_Ó p³B–0òAW«‰:Å]«ÓéÊ},ÐRkÓä¬Ì2ÀTÒ�R'µÄR Г²®ƒ¤dù‹¿ø¿wÜñ±eØß<Žsb!�!_¦)5º%¢l Á0´‘ ÁYjS£Ê jªWT|€ù W`Z Ì´Dj–¾@‚¢ÁDÕUÌJzÇ["ôå�ÂjNKä#qmÓÁÙõ?¿ÿ·Nà‘›W½ê[y{Õ‹ºI‹×:a×ö·óT-‘ê¼EgÀoyk ùVíÓè•4BêQÚ$1€5Ÿ•9‰被bÓ\©0((¨¨¶%"·wîÛa‡DknÅð$-Ìü �Ôƒ9�ÚH A\[GŽmÀ�·†Œ†Å(jK±ÌØ+�¾öØ Áì8É ™ðÆÈ1FeY¡é‰k*èmÓ¥%–™”™’[9¢Ñ}›Gm‰J–¸eK4Jú[©¦ìù·­ïÓÉMz’VûU|äLU3£ÑƒÉêÆãÂ<)*|WKÜwL¸¤˜¸êW„Ø¥Û©/[¢ÃhÂ�¸%’ÕqY•D¥Õ\vÕôAêèÓÀÎ6QÆ;òxçåc"¡�°%RgöCTIøWNŒ[ýpŠ5F!Id±fc"†{�–HVÕ, ¤\Šeˆ2�®%ÚzP†A ÆCÛÁ,ÒúeÙÙ!EzNiêg*ª¾É¤¥A%rbK ‹Ä$#‰Ná-SD3ho‰p¦ï-&a³L…áNç¡Z¢AÃÇ¥í??Ú„ð¾-‘šs|LÔÊ£…0"uKì–HuH±=tKì–x|�Øc¢=zŽêãé¤Ïó1q·¿ÚçÖ6œ xvÊoýLT¼ÉÊ)—h†3I”r—7~ugú”5-Z:Ãc½Í´�5ưg�s|– ¢4>„J·QnþË(K'ÂÐWûeäâI  &ÄèeàÌ.nn¼i´–hPäÁŒ@lZr¼{:©¬g<>Jt…ë=BvœÙ:åƒ4ÔKBpä™N”/c¢à€[.ë™%ÊÐt¬MÉκO½=”ßlž¸eã.hT8«Sˆz3,ú™[@ð$“dh�t˜â�_¬F8« (·ð÷œ/C³@Íyš _žQƒ ¿4óx—Ô|\Ñ'_ràrDÎ5¶jCR!944½¿O �`Ï)›À�mU§4”Š˜z鸿k¤äWúlJ§ðêi¦È±€&˜ÎØÓ*)áY, ^?Y £¶tÂÛôpÖ2«5ãsW~IЗ´`F¡XiÈ(á¦S<ÊÆ29^.²¶·‰4øÆ6XŒÃ>>ä&Þ:è}HÌ r™Ûâï&Bù9Dj¤¢Z¸6¡.Ëꘊ¬Ôã8 ¤vPÔ3*\62GuŒfwC Ð5uÜßt�"m ‰,´¾¥X¾ŽÂÀ%VÕCXöÃSÅ e!ì,0³O‚›p1¶šBÂÒCæGÚ©5>›ÄQsTÙR*2Wj W”5Ūü }X-yJ²XÀv ô-n»í?~âqõÕ×Ütã xäa ô‹­eKT~Œ†Mð…MKô±Ì”¿**TZb÷œ¨d :f§%‚G‹ Uý÷;òî¶ÄÿòÛ¿�¯è^+ìÐwl{;}…-1ˆ&lÒDvKDIú!š!°9ýŽujh‰ÿl¶D놕™¾›JBSÍcŒÕ"fsžNµ`Ï.ÄŽU± rµD*e´ *¥†ŸÉ1ñL·DÌ>Œ‰º”¦£ah¥Dìg¨:g«%Ò�Éê<íH}KäU¡irn‹2ö5çaKüb»%>üÙéû%2º3@Õ:f§6&vK|äëÄ�ÏN�L[U¯Ïä:ñQ�‰{ç»Ò8ë8bs¡Œ‰ñÅrÕê?tÖN¬9‹*!ª`¼‹l+�éůì \‹TáÖy)Û²dœ#…ßžB À—Ü ÐK&İsŠn_ãø(òPŒeJ@²›kÏa,¼8“Aù —)6)Õ$`�—&©�4¢Gˆ)=ùeù- ¢D1¤�2=!ÅpŽçVXE-!å&å @"xܱÊ뉗šdù,y q9yÉU&Œ½‡t±‰&ß.�„œXº»„¯ƒ’�„•MJä³ÇŽê±ƒ¶ñ>§H•Ø”³0×Ó>H.Œ© ¡ ZÚ6¹¤; ×-«Hi¤ �d¤•¾â¢Q‰S6ò$[N¹Ž :E@—¸Uüô\Ó(©P,WT;Ðå‚l",*éÊ<ÓÈ��zc-ŽD°SÐ !—¶Å>d�<’(¡¤WâÆÈJCU7–?(Q5ä=„ëp›j01* ñQ¤¢ §dkû4¤¦¨£dŒ iNUˆ•’3"â¹o‰ª_ZDÁ “€p£ÏÜyçŸ~ä#Ê}âñÚ×~ûæ¦;v©}Õ´h©¢ªUÇ`ti?æ–¸¤¹û›ª|¥‘OWKä ïýÕÿ¸›G]ó*ÕC‡.æÝÁÑ×Çn‰Ý©óvù÷dCp÷¬´ ›wý‘úbWCJ;";Ò}ÆDB÷m‰öpé«m\’¶ï¢ÚÔì ÏjKLsî1ѲNQXêÐPv–I ¦¾zLÌç�ŽXå‚»%ZÍ{vš¼g§ga�ØcbÆx*Ü\':fõÑ8÷Œ¸p› Ï'ydå«G’3s§è�¼À€†ÌD· Þ”²²f”(‚3Gž­²›7»>9¯œârD�è=±œìùÃwÖ‰\2çó0 ÎI&H [±•{ºÀ}�È´ïÁ{ ô©ƒ0øÝéü%#¹HÕ'&! aäbý+¦7¾W(�€â::Õù† äÌsQ!&ÑŠ‹Ðˆ7”AG·(ò‹d‘YUg<Ú%Þ¤f<›ˆÐúN¡yVÇ'�«H#VQ†Ë(óps¸Çfq‰4šëA£¤b-¨U>e„¸2Óe�RÐ^OA�dG òC Ñ<ðEÙ!%+/ä—hŠCÏ1…à^Å)–zã¤þeBYì¨l,k/þôçÒJ ‹j²ˆü8hÀFªED1#J ¦ú¢Õaã ²$BQ�¾4Ú˜Ïñb��"Œ%¥¶>œkZõ]¬° ÚG(%®¹FkÄ©E%­§ˆh¤yImœÌ,Î�ÌÈEx„%”ª»¾–X-Ì�Œü:ªW™ÔŽ„ 74±;Dë¡PKÀ”�¥}ðz¹ê•…©RUâçn€ú†*°RŨÖB•SJÚ¤‰_L)Õb§ä<Z"2ù4#bê8†’ÖEªÄ¯ß¶µ¾¾þâ[^¼qtËçŸÏVKŒ÷9QÙNoKüЇþëÑ£G÷rzÍk_wlg“'°,ìn‰Ýé=ÎÀ˜xª–H×ßcb�‰=&:J9 8ã³Ón‰=;íÙéù<;í1ñ4®÷Îx;¤-p, :q#6À,NOD¤rJ–)o"ˆDüp1 VF��@A21ì’„Cl+‚‰ !Á”øƒ¾˜‚ %åÊ”5I `~ÁïÄN$/þ’´…I„ɉ'ºT©üBJN!¾ä*tqœ6iÀDbà]KAÕ¬�lÉ%Š‘tK4Íé`ÅöCþú’kù-›øv„2æðdª¨U|•-¦HšJ§ôKE#‘ †ßʼG$É€ð„X°¼0Š13¼ÓúhÐ$ :m)¥X%ôAK¤`š@?…Bbž¤ÃB²ñ?±&…ªw GÁþ$mMW¥GhÇnfà°ì™ùx‹` –sÈË L¸ �š<*0�郛…º’s†bʰ¬ˆIQù—Ÿ`dÒOèÀ¸bɯU¬kþs@›äR±5p$lx°.ÁdPФ¨Li0”v8cj«Ð „êÅšCv‘=’,I--T_ªDòŽPdJ¶óª%R¦â‡ˆ¯tHKÞ{×]xû(ô‰Ï]ŠØV 5ùPø,´ÄÅXÞY ŽU­E T¥A™žF–$Üã[(ñªT&5<„%„â~@n†Å²+�1 d1ÆÉ%ƒŒ9݋ƙPùF%ƒ", >ø±˜àk¿CIyѲWbœÃÑi¶@è^¢«¸#AÅ�nLãÜ\ Ä+×…ÃVîÌÅm¦ëœñesµ„<æRpH £ºâW(HÉß}°A裮"°gôaæi7½ÛØÏ ìvijɜäMp?¶%Ã# ç9=ìˆB)aJ|ê‚wb’—½¦H•àFâ-‡èJjjL±¦ÄkÆ)"ÆÔ�¢ ÞægµƒþXß=QU_j™�ŽØ/Þ©IV¼ºtÔÊÖyó®h$–º/9¥ ›˜™+픬ÛbYjA´ „T ^sª‹8¸e�?ŒÏ» "áèYF�‰&aR1¯nVˆÅnƒè¢z”z)…Ri]4ñ´h¿ÄTtÔ6z7Ü1\¢]¹ªY=ØÐ]Ùx¹c/ X­•TkÓKLÁô ÄÑ1:Vgïy6¡wç:ÞßÇ‚ÊqgÓ;oe¦Cá*ú TÊ_ß9ˆYà¨ä¥Ä5RÅ[ªçUK¤Ýò^K$vDdÔ\,~ó7~5¶?ùÄÛx«.aȳ×O–âø½ÛÿQô§¥%~øþ�»ïö×'¯}ík7·Ø¢M¹[b·Ä37&îÛí¼é3zLì1±ÇDºf'ÎSÎèì´[¢†îÙiÏN™ŸÓub·Ä³ÐOšñöm[àœX€å´Ëã ±ñYE×üÁª€{X Ö|,™Åx Œ oÄH*Î#�À³ ®ÁÁ9J0ÿfF‚f @ ³É¥èA0ðAñRl�4èÄtN\Ñ™ÏDzö?Øv?Pëé%ô"»A—ȣǜJ'2¡0�– žåÇ Dri¹&¯ r!Щ`"%¯±Š.A’Dép'DE„ N•Ê[ˆ9 %ÅD¨¡8¼Ul(Â%™¹+¯7ˆÐ8&† Â5sd$ "HÂØˆæ lf>²JZ%’2<Ègø((‡/ÂËÈ�4$&HEK¼I\Á“jPü–[ yü*o=01>ƒ)îɃR?¤æ­XÝ,˜¥v•ò'A•5 Áð¬X"U¦H‘XËà(™Áe•2BAÙŒb(U,mIj Ôb2©ct'#D5˜D �ÀrPgS% ‚9£ˆç”@ª?—ŠPÈŽ´„† ¨~Šã¥Æ47!i©,•ÑŽ bC´ó­%‚´•Ò*gmXá=øàïÄ'œ^ÿú×_öøËolöQ-õæs†[â b¿™¯€{ž¶–øÞ_ÿõã”�_Üxã�/ºùE÷oðj·D »[¢-Y;œþ1qoK´ÖÞþ<Ðcbúìn‰=&:~gLí–ȨݳSf.=;uç4Ó©çïì´ÇD—1.^ì™—zùØÆÄÐëS[à\[€‰�㺇køTn.²¤6„¾-£]¢éçqÅê<¹Î1b3ÜaýìTÏpá�8/‚�+ò’F�;yM_€ºÈ±†*8õhýÔÔ.š‘s¾²c6¸‘4—À4„èªÅY§³`_qÉâ„×”B0Š!6t!±X/^ñªW~Í×<Ž»ŽÅ�‹.Z¬‘.PR¼o@Iàp‚°’ϨÏ#ñ'棴([¡“„�?o¦LAª³yqœâÌž)–UΔ“LºŠ ¡‰ AO�«(ym›­Eym¬e¼ØÁA‹}èÔ†;!•(YvVdþÆ!V{÷ è¤ì‘Kæ~!ïÔÜñÅ+›:tª&‚ª“�ð\8à=ÇŽcѱcr¶7#­y8�i`µãKåG ^‰%$¤FU#iyã‡äµ£dš0/æ4äAT•á‰oÕè`KxhQdÄÛòÕXPæjuŠ»²ŠW˜^qšOQ-ÅùšØ^kÊb1À¨eU N‰º’Òw�4ì_‡p æ#Æ#üú°àG4¨R¾Ká”Ë÷n*Ø–ĘÁÇrùVª.�ÜóŸw¹B"sÞ �Ä+cÉ…È•…yŒ´”+îz¶ ÷yÓá‘ײJJ³ì¤—j h_-Ñò Ë°P‡²²¾6þÁú¡»îº[<“É䦛nºöÚkï?rÌR¶Ì,C,+cl.ÏTKüGßû½›'lÁfռ暧LWÖNcK|ãßû‡ûøG­69PîÚk¿èmsgêV¯n‰ÝÓ3S9¨ívx§sLÜÛí©C©”=&ÆòÝ{L<ó³Ón‰=;íÙ)K”s¾Nì–xZâxumƦäÌêk¡7L�û«-pV,�å$•Ð…½ y–w.Æù¾°)Dj,v/ùv­Ê2Á•uöö`&[(�PˆŸJŸäà�%„Sj­abVÙ�b^˜šW ~¹7…^?΄ª=§ Fˆ+ƒm›°ÅW<ó+¾ê+ÿVV²6äWVîß8&�A…Šu!�”s©DÕÒA%ó¬ù OZS’ŠÂCMSGIÔÒTÈ,n�œ|‘–0dž9’ÄHÑŸ¥gà±J´F»Ø˜¸$†+d!/þäÔ–[h¡B,©nŽíÉCz@À[X$Ör¼ä?Äb[˜’I¸(‡NWZð(÷ò’†9Õ: ‹Wi |Ñ*€TÖ Àk8)*‡ÒÈ�H%×:DàÜft+‘ ‰5u±2wțϢ|Mª„Yc–P5'é¥]„A;}º3tÌ,ËÐ2:¾†�¿Ìa&™D6nÓb�½HPæIt 1²1›Ü±©`]D /B45Hq‘Ø«°�R°Q˜¥¢«Lù²)|OÖd÷c¼²˜“:*~è|o‰Z$VÒ±.ê<ïyÏÓúÜzlÍŸÎæ ðE׳Ùo¹å¥Ž4)4^ܽÁo§­%ÞxÓ�7}Ù�¨FyÛĪøæ‹Í­;Ín‰Vt-“öP•‚{»Šn‰ÕUؤñ�U阸·%Zå<ÒBsSú|ì›ÏvK¤Mô˜Øc"#†Ý¤}Á_—11�¯[¢‘ó$ç‚ÌurMgTˆž�Öü©ÇÄÓ·Nì1Ñ%-ŽƒÑþ̬¯ºôiŸ¹÷ÿlž^|™ÛýôÑ8› âÁîªËž–ñõFlتK/W�ÌÙ…ðâM€î_EÆŸ‡îL÷)‘›¼îqÏ›C×gû||?#n¼óѱÖE#­0“~þ¥”ÝßÌåRœÖãâ.ÑLŒ£Ò°ã›k‡¬ìñ~ØXŒŽmnãJcVÀšÈIŒèÐL'3˜°ÄwÕ�2÷¾É°öä²2Üu¿ãnxÖÕ°V6Eå@Ed'�iU>„ŸÁ–ÑA‰F]q(‰š�žÄ3Ÿdƒ2PÄ)�”q&c·´2)qÄŠ!T€��ðíû[Ýb ß ‡�¸×E@&à ã( ¼•u /r²™<¬vH%€…+d´¬QÛòp`½êt‹«#h v‚�V‰²° c m&!>F8ny•U�P^F†z›-”àEÒÚ&е³œTtyñÈ0Tº$¢T-p4¡®iuíʨÙ,HÄ 'ë BAR«)<ÅŒäTnqܘA­³(!ò�ýXŠ)r±®r MËÒòQ?ìDz´I�¡´tÞC -C�÷Ykrâmh—'#ܦ” tȈú ªxrzORòŠ–¦Nmcö³KÝ®@ÒÁáÐr¡f.l8Xë¼o‰F“bí¸*$Åï,·ÐTâ±XJ2˜SgE¤Å¨šF–œ”¤2$urs+‹F ®‚³VÞDKnò3»ôM q(“q=`.*…¼¨ îÄS.2"éK2AEI)"øJÍ"çS2p¯ôf3è›Îy%Eâ4"ZYôùh\þášÃ*à»* .ñM'gÉE©.åQh0±byä£ÖRÆeÔH£þê`UIWVÖ´j $Êef .åþƒ¡IMnh”¨ÜBBíá`ÅàÁjs! <± I‰VEÈ’�Xõ(�-1†V›(� £ÛÙÖ¾u¹["5 [bê‚í€ã4މû¶DÀ°%Úçô˜Ø-±zé¨ÎØì´["s”ž�¦ƒgäwÎݳS¦½T §ÓYd&ˆmë–˜EL&ö®.œ,^HëÄç=ý•ú©Ýwô ô;Ÿºö™—?áÉ5g³ïãLZ8àîÏýÄGî¹ú)×\sù•Ï¿ñÕ£±¤�ð\ ˆ�[�sØ3‚Ú¤O  �\ÅiK|‚KžÍ­¨%¥}*=ªG–šLìxg Þ"€1lý%Fï!±Á ¡ ¶Ò2œNÇeqG•“ûsaÂ)^FP0�HÄÀ@ÿ/¼�t!pe«­}.T¦¦”~iÐâaŠxÆ�ÖnSõzHû7ÈÈÃë+Q5^î�'ÊŠ)4ƒ&9D°€!N�3¥)dÎjϽ ¹qÓ7Üõ¤­³LŸ:­F¨•ÖÕMƒºú’€‰`ñŸ½ôðûŠ;–%Jìvg¼¾o†xöáO‡wÌÞ�|�ëöŽ%˜ä|L¡êH@&“*õD¤‡MçËp$Ç ÃS¦Üù>N`5B°Þunñ'%²æMa¨hf”se£·²v ôàÆïV¦Ò>ë)T¼u_ãJ)s²ÝP°!n™£ ?ÑPˆF°ú³Ážgkœ¯ÉŒ¤ð�¡^¨&šq·Ê>ncq„/5¶B�žtXÓø¯‘»JV'=r`L =µ„ýÖ°ò|uÇÂ% |fëŠÌtz5*ÌÊ—Eþ„¥qG´XÙÇ�[«H£¢¯n~$±ªZÔH³c¢%™ÅT—E4Lãº%RÊ”ŽVÃdU^Ý»%vK¤{è1±ÇÄ C=&:™r:À8ÍxÁ€ëd g§ÃIcÏN{vÚ³S'‘Œš½N<]ëÄW=çM¿ð«o?rÏöŸ}è¯èuûh œ5 l8ôª›ÿ©Ë ±zóp¤v>'r‘ÕüU4Ð[§p R“^\‡pñÝJ†œ|y·$-úA�iÍEO˜}ÜHzâR¦EG›&¥¸Q/&R�~”R ÍÄó~ @”x™HŒx�t N2EKR4óÂÛhF:rr [Sú”Ÿ McpAJa³©e)sh&uðf¸&m2êÈF›ÉÊh(…ð¬P‘ËïºAOˆù#–Óæ‡  ÒL)@ÞBÑJ¨æ0—(¢é|FtÌib(øAþÒ¬(’ lD›äϵ_}‚ Uí0{~åT#þUÓEüˆÎ£À™ëË «X”…ÉSÕ&ž£Ü¦L ›ó{Ð1*­I©pÅ©:bU\§L¡Eˆš¦m¹P2"BÀ°@¤U£aI¤ª‡Ö$ˆäAƒ…Ô{¡IèÊ�?Ú‚¦(´ê�R:üQŸ€óH¤:9ÈMy„>HˆvÑ(âq!!dMûTÒvKÔÞ^·DªI·DÛg·Ä ì£Ó_U_‡ezL´›w|áCÇAïÙcb�‰Îüdâáëá�ã±ÃmÏN�î8“qâka'%¹éÙií?Ò³Ój.ί©*=;uªÿ×uvúÄË®yóßý·üƒÛ>üg¿û©Ïý¿éL§„>ÚgΓñä)W>ý97~ÃK¿öõzbÕÚþBFlø%_$g"®î\ìz°|„Ö’‚��rL€r\xµeö²bê¢Ç\f5œð"¡ƒa� ûf¯è§ClÜ¡vB“Mž½ŠÐ/Iï/°  ™1_ÌÇÐ\RÀUjBJ&¤E>hfMÊ=ÈÅ8Nu¦Ê„Á9¸›‡ñb=­$�Î’³»í“d$È’nfRvº�rñ¹ƒµûyAU”P,Ê-Θ£¬òrÖ¨ª´±d¤ÄŸª˜�"„8¦áÃ¥™Ð‚4’S‹ñTßý¡É£ÑP%>_¼•­Ýdª”ģў/šWw,Œk4":KJµ‡Æ*e&F·†ï”YÙ›�8 !ˆ²\›OI‚BèŒÊ€HjÏr�ÕO=‰ò(a¨Â„­Þ0·Ö–¤�’]ïCo‘]˜/¥ÃI„Œ”dwm‰,Ý»%vKì–HÐcb�‰=&ö˜è¢g§=;uŠÛ³ÓLê�*ק׉çf�ø’ç¿î›Ÿÿzf);./]¾°ø™°Ðr¥»“u¢¡3—7¬(YfíØþÌ".–XyºÊ‚6[t“†þŽí¢Y4é`A¬«W3¦/Ì:‘UÞr�xÜu‚åâˆ-�HUëDV_®¸è=Yˆ²¿�l²ÆØ`¡årŒ“ËT'[Ô(šðவNô¥<Øè‹ኼ¬ý Æ�ßR¼†V;uáÁ'ÖšÙ Ž~ØÝu:+n“ú–½m¨š/Æ,ˆY§ÆEÃPtbÙéXÀ’©$¦±à»Âë<¹,Ê®êõ2q5šE6iµ'KIž[˶áÛj½:™±ÉÎ\¸E Wûz¸õ»Fåé. @” õ‚PqŠá狃)”©Ñî–‚Ñ´ {„YZ.Ù«xÌ«@qmáQ8‹Z|ËÃ-ë]Ფ·t5:p$0¬M¥H„Š´¸þ-ÄÆe÷ �³¥ÑE¢$!ð‹±zCa •Å™Jc`GÕ¶6PPÙŠ„më "Ø<¬6ùE Q+B&Ânæ‰Ý,iCÚ\Z–Ú\ê"™¨@‰;—s'Ê—8¤�'I$Ih¾Ò ŸæMY_Sg%)}óx¶ÀÃU½(bÙg%”·‚�LðE°HÊÜÆ›Ž„^i • žsM@Ú ýI£gkú‡3Í1ÂÅñÓƒœð²Ò–³šR q¡�zmhÄ¥LÕU)äTðޏ� ¤ˆãÒZ�=IeÙ˜˜Puã^Jjio#‚!‘�¤&Q8ôLyTL¬{Ø…z”Þá!úKÓÿ+)¾8ŤÚ8™dm°»ny V¶:Ø> îŠh);õæ_¡4ŠMе Q‹vБh$¡[À”ø ÒH�0tI6T·Å–žh •ꩊÊd8’È[.à¯ô§–]É�RÖò”O*µAòU‹bâ$Â÷#õ,CÂýpAHݘ¯[¢Ö({•)µ3V¶¢UFó»uK´6uKì–H{°çw¶Cká¢ÇDšF�‰ùy¨ÇD†Žš9dÏ ›áÚñ–±„A>ƒŽ§ž�:§tœíÙi&5¯cžÖ³Óž�ÒOôìTÐcÒ—Öz¨×‰.Fz�¨2v°ZkÄÆÙxÆŒBlØ  ¬$BÀTcXÆ‚ÇagÎ+*Gã©H Nm¤à X&zîÈ–Ã`š¢�vÄ ¸ñž=óÅ,Ç#à9 �¼äX's±d8‹Ç’8OW&“X5´¦È,Ä,lcB€* ]Xœ€,¶t°�>Z�eLþÎ?ï\%Êw<ùÆOdåOÿ&ó«4° o²''&|÷¸Æ AŸäÁ€¯bÎ*Avt9V)9¢BÏI­yPLiy‹){«© na|#™#¶ìñÈB\vÔC �HPc‹=­�V|YãÊêšYT6nf†.ÑSu÷µN‰‰/T(†� à꣫²¹Íš c ±*ÅÔŽìî† ˆ{(9ÖŽ*ìËæïJ8$A/‹ƒ{MTÖ „¨U,ÂÈŽüu(ŠZ�s…¦*¹ ° vÓ¦V~£�…µ鵑õÍäÄ#¿ª‹‘CÔÓò 54ƒíQFf:W.ž6@(Ü CJÇð¥nÄ·&𙱇¹|Z–ÒQŸâщˆ1¢•V’°ã‚=-ë“ï-Å@q¼C SOH‚7"¿" ¹õ 9ù²HƦäª[¢Ví–Ø-±[b�‰é{Lì11“�š.™?ôì›ôì4ùٰkpÏN™Söì”Yu¯{�ØëÄFlÎOÄF$"+¾‚h€ˆW ±ò¡çŽMâ à,d"¾„Bà ïBï=�õ¸Ó¾xZ‚)I\P‡… €3LÂ)ÑÞÉ“ƒKá óhIÔ#L Ú“;‰ΉGeKz’€”H¯øJƒA~%†‰› /‘"£TtÝA+ðÓ¾ ,ûµŠ\×-B5Á¡MòH¼x"!B Ô&C€§ˆèk”²#IM‡n¥®¹ÉÁWQ5�aDÒ›¿x�®ÒSþ†ÁÿxF©C@g•Ô‚š–`Ñ4—5×d*E^R’‘ho$�s¾ (ÕI™»%Æä1Q·DªJ·DsšL·D:�éì³Ó·f H?Ûc¢ãT�‰Ž¨‚OÏN­>гÓ嬫g§=;eÊÝëD×@v”YÅè½Àu¯±H¯]O»~†Rï²òÍJØå­ël—·:¹f°Õ©g§ÂûÏNu­Ðžº²ñÒL`¹·Îq`ÓÏH/ܹFîñÆä%‘<¿ípð`K‘ÐJˆ—v‡ žmÂVÁxð¥ÞjkD‚žEÆ6eîÇe㌯Þ@IDATUqÝ¡˜# nAáÈ >H>2Žs±ò<9 ÆÒÿ÷7*‚!ð#ž=Ù€×n •ý �g² Çg¡­o‘»šÁŠ ßa‰Ìà8H·:öyoêOXBéÐ&• ٳ혞}N5›.ÖÔi1%™äu¡0²†û¶WÆ;AÏðŒ2†DZ2šp§¹„jPÎç&•–x,£„0~UÂD¼KV^xâj†3D‚¶éä'! É ïÇEz6S©H‚ZFé¹Í9yì‘2ó™rü}§Cž`×¼n„g%‹sW Aìø)F¡†@(ˆ£´áÍÔŽLkH=<�N¡ KiŽx&NeU“s–<»ÍE4Ø“ÂP¿4ÄÈmÙÈ€#\q—ArSð>doÅHÕ#‰~tü"nv5€-¯tżÉIéZi¬QpXÀúˆ­œRRèÓ4 4-pé¨\0 JyˆRÕˆ"$®•KÓÂú<-Oåö� FÓVñ‹Œå‰ðÐ#®¸jkM·Än‰Ý»%¦+ò·£¡÷°Ÿè1Ñ^”žÕ‘©ÇÄV{vÊÌ!¿Ôõ씉™ó²ž�öì´×‰½Nìu¢kKwÜs9݈ ËëÑêù€ØèõfшN�=0©å?WÄøïAÁ1,Å:€@Ì#Ša© kxÉY('|@TP…CŒ� Ê#¨Æ@./øEñÐÙ+˜ ¹–T æ2©Ì/ŠÂ§€Œ �ÿ¡J§Zˆ /ÅÓ‚²Së#¸2Âþø‘K¨x¸OŽÄ#¥ÄÄÄÌÚÇ QÄE/ˆqÈ×�΃ËB–¤^¢È.^wİ$V4'.´8KÕ9„ð“Ë"aSk‚7OLå@i�ÀosÜfS8ŸE3ØÆ_.ÓZg ¶AòˆDzÛ*=‡\ì/Ü)ÙÈ¥ 7’3_˜,¡F„ê[Œf²øÆU ;D_ÎhÖ# ÂZ@,Ù’8ãnøÈÄXII7é`,%PDæ/ÕêØ¦9#99Ä«™™`E•Õ.O] =bÔ)‰ÕZ&Wr˜!kBo¨n)ÃÂZÒ Cž@ÚÜZãBÅØn‰˜;tK´Q,kc·Än‰ô=&ö˜ÈÕc¢£}Æ^¿zvŠ5|ÍYÏN©=;íÙi-;¨ µNqä´·¨i~™¯ÚëÄZÑÄ:μ1U\-z�ØëD±çÔ þY­òiÄFlà±#6úê`SQ3¬ 8�­�Îpˆ‹ã ÒíëÝoK×�zcm4¥B"ÑÎþâ4`|ÙLïk=„0XFòžò:uI¿à¹Á°0ñ.)o'+ž˜  ìÄeˆU4¿ÐÆé�}–XŠ€J}@ª$^Ê&M–Äf€’˜\p¹¢]ÕÓô+|%§!OüÀqyƒ8á\½¤ó4&OÀÙÁ«b9D摌vq÷‘%8¢ÀÍ+¿‘€õ«š“‰GCàkþ°’yÛ¥!bkR‘«Ì"‰ÀPˆ�þ {Kµê‹"•i !“†#FZ|¢a‡_‚¯²ôKØ,Åì´ Q¢’Ä=œ�C�ÄrÌ7WÜ&2$P‚ŠR•È,%«_ÅL:@«�ióÞ ä{I¬ €(�0ŸT´’V�¸„+ÉRQŠ–D£*‡Êç‚´J©•Èbêj W±tKLÁS4o·Äò ¥¾UåK=I«zÔ-±[b�‰ö¿v¡tÇ=&22eë1‘©„–ÌѳSŒQs§c\:i«É·&J¥!¾g§=;íÙi¯{�h—謢׉ ¤Î³²ÒoÄFp{ìEl&3ܵD¢²d³þ�T§ ÷übÀ�¸¨%XDÿ$"ÀÙÀ+2�Áâäå0 ÄARŸZ—ð &/ ÍØÎ ´aŽ`Š(Xyi�&"gr‚ÂÈh�>b€/jÑ ‹tü˜Cu[6 3§–ã>4@ΡÀ¢ üÉø}˜œ¯P‡üJnr•‰×$Hé^rÎ-²;›1«ã)é§øâñÆOЖU<ý ÷ ¢ôÍS*}–�…ß`_>V¾ €¼T“ S8™Lo+¥Í/µ‚3¼d¢Ye£¶øÍñºQraUbòBXÊd±½qWSž5=Æü™kŠü)½ûpÞÂF)ÏäTJ^éå9’! EŽÔQ}’g�Ð ;+,úˆcTã1%ñbXëDð$©�“˜Õ,nhXV;¦ÀÌ€‘‘L¦+‹5,f¡‰º—(¥BÒ˜¤ JÍÁ›g©ÔO�¢hbPM¶B&:! D!¨„8C%š&ˆaŒÀ …Šˆ`€˜p �¦Þøè(wÀ2"|ø“waží~ÌáÍ"ü#A hò'Ô#¿Dq)B°ð‡‰‘£Ó:45R %+1Ys#3ž=äЋ|ǬrÐ42”#YÌfÂp#Vì.–ȃ²Ä«’�ÉP!Bø 2 hF‹’½—äµàÖR#ˆÄ+åMŸÈ?DɦŒb_Ñ�2OQ”˜¤Ol2(¿âï:ÊÒAQÀ ÊÐÈo,Y†Œ<ù:ÃޮܑtXR+J9)L!¡YóÅË ªF_2X¸V5íų"{ᜠ…1LND€?î¸V"…´~Zµ†âZfI>+ sC˜\ˆM–¢)`¬µ28”á|,> &8˜�âJrÅT§¤Î&CC«ß±§n‰˜Ä?M�=º%b ,a�±²ì:º%vKì1Ñ6Ñcb�‰_1HóéÙ)¦èÙ©ó½ž�ö씕EVG½N¤wìu¢³†^'bGÌ,‰™H±ÀhĸÐríi ÉÚÝ:¨ÉéFl&`8L¹%Áû4�SÜdÈŠŠ>ùO¥ÀÅgÕçG{/#®|�Ü ³‚§3c'5 oÁ@©ôËŠ&Â9 ùÝm‹érö!£#`‚€§^dºk1]� iBp(?qÊÔ·mŠ€%¼¥²¾3-P(Y6÷"©¾xÄG¶�ÄúÊMŒ(Þ&€‘à­WÌG8I‘ýZÃè¾ 6Zžhœª‰[ŠÛ• ê8Ó¯Q'Ò�É ¡Z˜7r…ﮨ5¹jg³9ïDEAL™¬PŠÔzn)!_É£¬8ƒ Í@ƒÆÅ(°˜¨MX&‹»êqÓ*ò¢¤Ì5¾rê Çê•\„cŸ¥…ð§ƒÜlTju’�ÐÐ$¬dÇH‚KšUUÉÅ_=êkŸ.¢ 0†ù}öÉ‘~IcEð}Ÿ–ÝlDÉ`A껼Qå0(ž†ØKçÍEHH…B^R¥•¤l´_;l¡ úXZî$$•¤€ö‚�Q –cl‘“ô�E `l,In”v%,Jr’LU} ‘Á ÔÞ¤:Ⲅãì‡ò(Tü­&Ð^ CdÌðQª=N~˜ëŽËi¬Èky±X·Än‰V/k'§n‰Ý{Lì1±ÇDGÛž�öì´ÇÄž�ö:±×‰½NlÄæBGl&A@ Xï±òwÑçG Kl¡� ЈL}¼‡ÈLЗ‡ ‰ô{¸åždÄ‹G�°€ åŠÄ:ÃÅ2$XÈk17hRÀ�4õ‰è¨qR0�ˆ¹Ô=›9’%Æ2ÚzMUºÅÇr” †’t·DkY·Än‰Ýmöºt1éwè'ª#ë1±ÇÄC:zLÄ �˜¢g§=;íÙ©“Òž�ö:‘I�b¯{�È»á—Ý®í9qºr›ÉÚX�§iœ”¢&ïp@" 5®âüĵ{ˆ­Lp©ò…�BÙû ¯%„ˆeÀ%Éœcq4jŒpjã}“àbîÿC(Œ0X‰„.ø–ˆK«…9»¥-Q£`(šSÞLkÓä‹«hŒ��øH,„$c<’ðÄ[Ʀrºå¢Do5‘ô§@^÷ ã‚+ïYÆøzÑ€ÿY õP;” š¾UÊ‚ Qœ™Ów‘^Ë覙ȉ÷ŠP•Z胅¯Ð\\¶€O°Ùœïè,–&¿À:PÇ �4lná>tÄp=Ä <„“�“ÈBrôóâ@J)(oÞgZs,"¡¹Ä£Ø„ÉMÒB‡‚ƒ­Z¹ƒokEë™’ï ˜¤—ºk¼Hç2h°W›!•Ê€1óRÐ2'¤Ä5Ç6૵š_-´ F‹­T y&ë}£T�ð4e02™R=f)¤ŠÏ£ã›Cžš·‰ÂX:˱áP–ÕK6}ÄDÑ &tÇ7¤7aú<Ê rê¢�¾15€Xê Ö̴ʾzÊå–„èM'‚˜ˆ{´cë6Û‹UÌrå£%Òä,¡Ì‰äšq©¿ùï–Ø-±[b·Äé8é§MzLd¨È& ŽÃ=&:È�š9HÏN�}õ씞‚Ù�u£g§=;u%àj²×‰½Nìub#6Â&çbÃ&oà Nè˜ÍäAM® Y]ø4Ÿ ‡³`$gh[õaÆBÌë@<8H¢Á°@#“e2Æ "':b2!qîB2`Nnœb{Vo3ˆ'€'2“Š–>•«Âd$, ¢ôpôU™¤‚i aâ#Ð$šD¤6”oa¤ Š’é=ѱ�©O„�ÅDêWÄIˆˆ�JvRzU®•Bp�9ÉA[8©h&Íö’&9Øœ > �P¹"žœÔœUáÝ䤛‡L‘ÙtTÍ#24�I õ©CÑ %‹ØW1œžÈ)rÄÄ&±x’ u¸Ê4Á!3’„YJÑô¢NŸC ¬ž ¯Å ŽþÜ“Ê×$YÜS¨=Ö* ùfÌLŠ+côB)Iº8(…�˜xñ”›…lÒJkÁ œ `5®'£9¥ãF‚C8šö[ÿ©4™‘<Ïç‘Yï%DD¨‡>7*•à5ÜS4y8òƒœ1 ¤ÁY;¢±•Ä8¨ì·Ä”*öè–h}é–Ø-1�D�‰t�ô•=&f(Ê`˜Ñ%ã†cH�‰L"zvê|‚Ù„sšž�öì´g§ŽÎÔ�Þ;‹ïubÖ!½N¬õF¯k�Ûˆ �ÃYDl&î9å@�õ?H�³¸xh0áIÀûž9·ÃA'!üƒðÝqyÌVcõ‚B¸J‰å ¤ˆ³ò¡¤†kÉ +&¦Ò'©èþiø 9H0Y0%9fyýä:7zåõœðÒ‘I¡•7Œ*”ò…àn‹59ø®L6+c?8ð(’ ´¨ž;±!úx ¢'Æãs–vÈ¢JŠ®[’8Uî Í�LêˆZ~ÊS¤LlÈl³D¡Ô®€HRO)’!z3/Ü&km_6ÁÝÎ}ÍLŸ—ÈBr[ WÖa1§ìý…w/ÀÀ9�ĺÂ3q£lÄ‘Yo:Ämóo©hrbEu° cöÄP¦õÝ—ÍÞ‘†z{H ›Z<³I}´n•ÀoKs°ÉÕÁ(›aI8Wq„YðJ‹9ñ„„>€ÈÂnbMÖ_RC[Þ0ÆÛ›‹±žvúžÉ‡WkD:¨`AÊ” á”ÎÃBS9+ÊjÌÕ ¨¥ v{¼ÕÔwÎâa˜Â¶ð¬V„à䘮z©1Dà!É'é)òœ2©}W‹ê¤·²mQî 2¢#£µ 2 è¾è”Gf1û&*¢ÑÔ�’˜¢£¢ ÷¡+Ivtê#Š*#kæ�ë–¨,¥”t·Än‰4Hw è–Øc¢ozî1ÑÞ±ÇD‡ÒÌ}zvêªg§=;íÙi¯{�ØëÄFl.Äfºœ à@-~�Ù8¹Éw­…�@€ òˆ^àÑž‚Èʲù8’– Æ’1 X"´cŒ'—ØAÑĦHQ¼¹\r¶+øe™e™M–YŸyBO’•€G",[ÁL¸ æAòÉÊG waXÅõ­â…w±'A¤OTùŽàaˆþ8×E+‘imUæ²à�:SE+jæ…‹Ú@‹¬æ fƒ„zJá2-,=IØJòš ZO4Î\„™Ñï™äþ r¢©9†û’wZ´Øœè„RU�(*X[ n›eD*³1=MÙR ÈÇK}¬€&¤ËC°‘Mp'ÄÉ«oheŽGQ@"ÄôÆ:€ŽrDW:PXâ{ÆAjDŸ*±~v¦pýêáM¾5Š—x`‡··LÖÙ7 Ÿ09Ä5,"F !À”å¤åQM-Ä«Dݧ¨Uv°##O‘‚¾ð^Wm�'‘Rxd'¼ôÕ-¾´ƒÜÂ}¼š×DòØ)ª®+- vpËu)¿-Ã-¼Ë°• ‘—WžbÖ*„R5+‚é0¦Rc�9°&ü¶`‘c^‚'‰ï?%[å�ñÈ¢©b>j :âïÍV8ørÊ&zÖËTG±q¼Óô «�÷Àà‘|D"—Ð4•†|kú‹)(n�V|2·¡¤ïrZC0±¨‹Â«”2Õ‚J¥�@_PJbYaUv¡Óè `Ù…� ¨Ž$�¶xÛ\Ø’ô$ôÚÚK.­%ŠK“°Ð)8«¶ÒYa90[å¦ ÉÔj€R@pÊÎuR¡”ˆ€n‰˜L3iºÅËn‰Ý»%ö˜Øcb�‰ ™0<ôì´g§=;íÙi¯{�ØëDÖÊ=& Cœïˆ ò±Ä}&´?à–…n¹7�8ׯd�0)$Zjq®–CÍ�œ‚|X’hà|šøÎS—Ü‚màƒ&JA H…ìe|Oƒ7â�! ’‘”¡ŒXAÚUÄç^°CID3J)Ê„GAöÅW6¹Ý„�xmÐT51qb0‚À“YIjµðVðJ(ÅT^¡�¸›‘ò1šƒò'“¦òVh­DNÞ<œÈU¥Uf�8 P TÈ`0eAî£"T#ID Så'{X�.aÂ:ˆâ‰+,ÏGú%�j–˜•ZÚþù¬âDéˆaI4VѨʈ!a`ÙrOrfÀ±,áJ5 V²• +<œŒáQfÖËò,oìèÉ#4,µŒ`šZÜ„t$¡*ÆÜJ8P)}"»PÑB}"RhGJp9Ë'Æ­çWU gè£ÛžŠêmç�î"vÑHùpÄÂ^˜Ô𬙑†ü¦õJúÊ–¤Œ>Ü ¹š,¹‹q’–V4¶ð4C·D­Ò-±[¢�¥[b�‰ö—öŠÕ:"Õ�@øîn¾ÇD¸kXuÌÎkíéÙ©“SÍQ³²Œ-Î뜷äÖ™Á8A¡™õìËhgh5oëÙiÏN³&èÙi¯Ó/ôì´g§Ž=;�ˆÇ09s0=ðµ²nÕr@8>CÂ…$Ä'«ÒŠc8ñü"Kbï!ÈRü…@ÁÏ$Ñhêµ€W@gÊDãóƒO–C½gÏÃnüÎgn…å¶rıå"ˆ€‘]Ì à¢��׺ÍýåÌ$û„‰ˆ¸R'iÜ� �S—°ùºšIÈùúÔ/™1Ï@~6ÉþiDéà$è%Ù†1ޑġ7L’@lk°vxkâуoÈs(ƒÂ¼Š9Bžws î°KAÀE¶æÚS4Ê�ïÅÄ&±ˆæ0uÞ•)-KRäO/BÙ4Ëâ§g û‘Pùè=-ÓkQP0TYÙNŠ�Ç… îü]Ý…’tx̓/©åN?:òÑÙ"°î�n%ç^{æ§VèaF2Säq_¶JCËANLޱ,<2â¿ b¥‚ÖÕ9ÛÇY­äh©#’ ‰Õ ¾Ä DþhFÎS9Z‰¬!©¨© –Šð·ù(¤«úIJ4¦ª#8žs£•mŒŽ' ¤ùχ¶Cj˜ Å)½SÑ`EVÌ^GIh4–cyíQ-•™ú"!ÒY¬|fÉKnÙPS³@"È+] Ù-Ñ� »%vKKï–h×cb�‰=&öì´g§=;í1±×‰½NdÁÔëÄFl.Ìu"Л0Š«}–ütèÜdõÏ-×Q*‘<'îcB1“ÀìXo€ éÌ/!þAÀS€³„Æ � —b¦?¹’Qfþ8MdX›MÂð65°�¬|hSqÝR?éMúà>1™Ê!©ƒz_(°3‘Ǫÿ�bÿ0�ôÊU/ô8b©Ë@XR8D ÕB•X&pIK-3qheÊ9·(ÞRPHéIíù"@j9qö›(i@íè÷apŒ-ò¥™ïHáNµÊkˆŠ¹Â¸^Ôàã“Ú²kP6®T�\@F€`*\Q¬ŠDèu“•RK9#R%%X@µW�á¥?T,’DaCi¨Hbªþ:õ€âANidõ¡~RŒ|ûÇP-áH°h,óÀ²¸’¤L‘¥SKœò%ºá¤�^Ð$¹:Vù%™©MRr…ê�|‚åc4g%¹›+ª² oˆ7^D£!o·D ƒ™»%R5¨Ý»%ÒÒÒcT'Òc"mþ8r�‰Ž1ô!ô=&öìÔÉEÏNAµCÚ…ý5ÏëÙ)FéÙ)uƒjÁÒëÄ^'ö:Ñî‘ÞÒ5¬�"=¦�'™_8 mÄ&¶q]ÿ¨›‰�‡{¯á¼„+Ž  [À€@ôFy5AÐ$Á­\�À¤|õ£h�t°£éÁLèÆWÅñ'`6Û.Ÿ• Ï'Š*°Ðê,.>‰1j-hžqb<„òÁ ®¡©£U�‘Cm€§!ÐÑ:AùžPEÆ‹\©ÈMø,C9àft1�°•Jï,¶ù2€ú%TƒO”îq¨%:E\sÌÐ ¨�–»©þYâ/à:èœøñŒw›òÚL|©¸7•¦Åyk3„Õ…/£’¸^°rcþ\\[„\¡xk¦š£�Oç2‰¤¾Gäd{>�ÉÒÄJ1¡bê&•±#f‘ à… IƒiH—@ ™L¸E…LÄGˆ¢(ñP":�æ‘@hxMZó”P ¾•–àRÈ:ƒ–òx’�ì«vD O„&È,ŠSêÀ;G ÅÙ¬¤ÈI-5Y„-—¢K s!ÁÜ*^Á‹´Æg²)¨)—è"îÆQ‰A|p #‚Á/@äéØ8z HD„qQž+M @aà¨yTØ4•‰{Eä­ IU,�NRÑ6²”Í¥›,í&!¢Ë¢”H+…� ù£Ô 7´Kݲ†üâ9˜õ§ ——‰ ’kaËD3(vؘ s«´À\‰‹QIcÊB¢RªbˆXõh bPðR Ñ�›"¦']WjÔ`$ª,r®¸Tí0ð 2¯|) _ª”KðCLq€í¬tÝ5 ì–Ø-±[¢=]F�‰vÂ=&:Fõ˜+Ä6�=42º2˜2æzݳSgÎ\2w9n4MÖ³Óž�öìÔ‰7…ÍÄÕE¯{�HMȧ׉Ì:3¸6bc•pÁ.p¹°Ë°Ï`úñh›É|Ìã’ø™†=�ýÚxl²¼yÔìǾI™4;tÜNe¥C=%—EKbWöžCtËŒ[%À·3åUŽžñV ÜY"¶Dj �ž¨ÅûuÉŽfêfN~H/ØtKì–Ø-±[b�‰öÒô‡vø=&ö˜Ø³Óž�:¹£?ÈOÀ=;íÙi¯{�ØëD’YY6bsÁ!6“¸ø€|P�¢b L€�Äxd!VÄ-`\ínïòþ@rÒ^$ŸÈógpè�c¸Kà &8µ”•²áúc¶¬„9(¹øÖ™Ü%¥£ÜxÔ…g“EÀs: AÃy�ˆŸ‹ ó�õ\`à éø€#DízBHÃbh1�q¾ ùêTÅmÈ$ å±5™ãÿE2wX[»ÅDí56íH; ïR…=pñ®Ãl•Ô>£¿ª4¨,�$D%#ÆòM¯šP«è•‡4ç7ø¶àÖs€z…í‰J9Ò©îRÎù½/òÎØ7 l¢Œ�]ÃÜÅÌMÌWÚA°MÙd¶ˆR8n­‡´ð ߯”šÚ§0Ù– ½·œÑ¹Æ-Q!”ƒ�FG|زsñèl‘ñŸ]ÛrƒÆK:”ÕHR’„Ñ!DjÌ+¾‚Ó#)Ðvì3hIZ¶˜ÌÝÔ�òRhÏdj$a;‰�[郹XìXƒ|n¹­W8 ’³¥†¨Ë*�Ì:8¦Æ CæåLoBdÄÿŽ)=`8YZÝɯ}¦�"ùSWu·ä‡8EÃGo? hË´ꮲbkQ?=ù¸¦×"A4 “dC¼n‰Ý»%vK´cë1Ñž6#P�‰=&öì´g§Ì¨œ:kêÙiÏN{�ØëÄ^'ÚÒ1ú˜_#6 bƒ×›Åe¡‰& Ú(@Çà'Ä—EëV@9 s#† `cÙ›¼H„F,ªÉ%<•€UànÄÝò4‰ÐƒÈ —æ Qüçç¨I {Tz®‚Ò$³aÈ1óŸàòJâµûG)ø‰0ž„áÚ’’Z‰æÎhBaÅÝç%Iqe’pÎÒ«r**|'­ÄÄTòì¥p[óÞG.TE +‡’“.oDsªd„ i"¾—*„t2‘¤öŽäÅP%Q!•�€Tñó,¼CxEÅ'ްh¤ILÀ�^jÊ’¹¡ªj’ Q¤Áh´ðY�J¬JcFTÄ\–¿²’Ï)8'C‚Ð8¶Ø–ã"¦@!9T5"â‚<Ô4ZTW6£É+~È7÷�&¯@.äü(eÙÉ‚ö–ŠkFs$âI�¤H6� GÓJ'5^U6 DòðIjòñµa&ãK$°g@VEµÄëŸ;ë·‘D�»%bŽX/Vê–Ø-1m£["� M£ÇDzË28zLtœÍ@ïD‡a•�?#ªcHÏN31ɧ&:Îz´�šž�öì´g§t¢ôYñdö킢׉ô™n`’^'ÒaÒ[ö:±1�¬ß©N:è<b3™ám$%ç-lVFÇÃŒ… ¶ãÌ0M�›Å¡M¨$-’E1pƒ3à·#Ü”Œæ%�=:Û•é¼Á£Žî½µ:ž€S(«S$º�È‚hoj.  ИòÛžïñ$Vèˆxf˜$‘C  ,`Ä�<³-]ÃT€ìòpr�·Ùys«2û IUCf Eþ£¨Ú‰bIÏ$|ÇHM°µ$[(4RH™”�°-æ×xL"¤ ID¥˜&JƒÀ~(�=e‹D@Œ>Šr€9±N„Q‘Ù#éÉKB(§¨ÊˆFzŠÁ]iÊ2„°ˆÎlakÒ¸ƒ©Åè‚� ™‘ZI) xéM¨™*9_]ŒØN÷ÅU½Ýô,új-'iHâ½Öˆ?ô Œ50¢2dï>öH‰ó Ö›…«‚µ Ý”NÊxxÁ‡1ÎbÅûOáóc¦VT\Š|;�P¡ºCÜŠ€Y°~h%OÔ‡TPjœ¯?…ˆ®‚Ô!c%9¿À°ª(ZOk› ”Â�¾DÌ€)0†ÆÔiNc%#Zd&±Ždw6b±…Ï4cÇ5<%-ŸlzX¶ŠÃ›X& ¢Fv-$“"g/>®ñ�\�† vKì–Ø-±[buZ=&ö˜èÖc"3�ž�2 bîÁÄ‚*Áܦg§=;íÙi¯{�èj³×‰,=™.±ØmÄæüGlÜ�>~<"Wô >6èG$HìAÌÃ!ŸA_`'ÐAæA|‚Qì€Ø<îGRæ’0˜¦�ÿd!H?!‚Xá ™‰“~€Ñ˜Å¯ÜÙ³(œŒ)…½ŒHz,ÑB¥JN©Á]xÈ|É�H.æ g.2“1& ‡t¦ç#ºèb~3™š#B‘Ý‘ÙMôM(Ñ_ ¼Ö  �%)$"µZ™CÒXD¼OË»›¿’ñ‘•b î…"p IW,š²D8$0v%%üé�L¦q- ÈÁ+™M©÷”Q¯ >¤HJ^s•�HÏb7ð¬è!é ¯xQ-ðœÕ%k9 O#S…Ô‹t„@*e-ÃK!<ˆK"_h@J4µf@3ŽÈ•ºUíá?�•ËXE“ºœ¹åc�_Vc¾†$|›-jY6f%zP\™M}®2ÏMY›ƒ€‡lˆ)1TšTüQY±L,Ç�ý€fÒØ„*²ÜÕ‹T-JšRÖ°Ž”¸Ý»%Zïº%vKì1ÑN³ÇÄŒ=&öì”Y‚“Ì€¨Lj>á"3–ž�fªæÛ™˜µØéÿ³÷fÁ–�×}ß½çœÛ�n @b °(šhÊ–D†LhQ–dí(®’‡RÊyHl—ì¼ÄU¶\yÉ[*•‡$v*¥(q’ª8£ä¢mÉ‘bIf©’‚ER¢HQ@ Pz¾÷ ׿ßísÑhtçvŸ;5־瞳÷7¬o}ëÖúþ{ío·uÚÖ©Öx¯{�8¬Æz�؈� ”ewt…ë÷hÓAl€Þð’>@Ë~ž#ä„�®Ä’Àôé› •éòĦnHÉ@ÒYr“–$l{&äc.håÝ’"�~6§À6ò–Gª‡;Zõà\?Ü¡|‘$|&î%d!F{"!˜–b™â•·É¶lñ‡"u�tzÒ¡[Oÿa�ðÚO}¸t²²jü¼`ŒÆauãŸ{^í‘¶5¥@zÔüZ:8Q”�W¶V³ÖD/“G2‚*¨ƒ$õÓ6ƽ˚ ðsne²xü‚Ö†fP‹�`~$DPG¡ðÑ:qGãLâ ØØ‚_Iû›hX¸%Z WvùPe~õýãŒæ\"n6¦¤¸f 5è§ç�yá‰ÆëQ•»Ò Ù<³ZiL;'”㌰ ÚD/©Ô�·„êgç{<76ÙGOÉðžP{i,!t ¾‹ß”Ø$�ïLiL{Òð(?gÖBäôÍ­š”A-‡'>‰’ME½¤Á ��l\ˆ$q-³*c9JÕM`s1Õ�'¯N—WôãÛʼn ¶-OBP&f΋{çn/gV¯´³© -¿ÛžöÃM^ëK~~CËY»7+âó‰Ì‰_àïFx(@®Gb�DúX�ĉ­['¶Nl�ˆ�Ò:±u¢fo[§½NtE–å‚@²K�^'ö:±—ð' ±J—˜¡ãáÕÏ#:̙󞑞sÑ+1˜‚i*·DH*Œët‚á@%:±±©“ÕîÔS8.‰kÀá‹€‰ïߨ‘œmÐ Ö�Œé`RE˜ÈìçÈv.AšŒ|ÝÚr>åÞp–0™ÄBߥ8¯ª„±4œ£PŸë`ðéÆ[lVFya�Iï"Ê TèKG©¥\‚’,¶Ä³ìÏ5�Vy¾ ‹ÄoJ^ÊU Ö"c j’ .¢ã\ö�ÛÜÜŠDô•â$Ò#™/Ý´N²˜ÍÖ “È@t´Ås»“)dgºÒm¸-‰-@(‰3`‰~ ¬cXp<̦4‰PUJD cì7&ô# dzd@2'Tdì›\y *¡ú NEÃú¦+A[È]O­b"räÚ§ê¯G[·V¸T@x‚ˆÀò‡,?c^Ë/gH…·{+ç1[VÖyYèÄžB�иÅ|&'$ç˦†[š…×™âQ§˜‚èÚP‚˜îÐûÆ‚ƒM¬üÌ'Duá´aMœØ Ì úÄe-ïðõݦ±J»cæ�¦pK9¥e‹ÀWŽwÛÚõ“ŸÊ-lXʦîÈÈRYâæGbN‰+ìq¤�ìi «¯LH^%˜³G¢²ì‘Ø#±G¢Ó%óCëDÄÐ:±u"ú1º^5ÛÖi[§m�¶uªéŒ!ÝëÄ^'ö:‘µ1‹âÀ�ØOÄf"ÁŒÅƒr˜öâ 6™ €p@À �~þŒiÉàBž S]iU*VJu#Åä!yÀ¡äÔd"…¿àA~ BB0‰8‚ò#‘y=0´<#C}Aq–hP±9$‡„!aKœ¦0 `‰êƒ‰Á„I@Â9•µæ“«J™ä)>ÉZåYÂmÕªH ØðÑݪHQ¢PYÒ/Ë’ ‹åW½ÜAð"C­ô8Ôºý½ ˆ'M¯d’äY*¡ŠßHXRœð鱌$¢öÖ2²e½‘A€ðIù§‘$Ë…3já�‹äˆ¤W¿¤‚òˆ¶²$NU¬ŒD$Å�xäPeRH¥šÁ„ºæ‘.Uj \e>Käù²3ð—"y!�ÇŒBÑ3:kÐ-“¥êJÚª4P!‡`PWêŸ5®áœÂKpÄ `Ç�E€Àqn½äD~ò`+1“¼¥?RP‘&‹Äm ãK2œVdL+c¬ ¬•ð‡þ ãÔWÎz$"‡#BK›÷H´ã¥{ôHÔÁ·GbÍA™L.tŽLjÆãD•©ÆPµTBÌ“I>³eºTëÄŒª(£Ö‰QKQV­£žYjù¶N3«h±8“´uÚÖi¯Û:uñÒëDÍ- ,ÿ�F[§®q±¡K¼6bSÐ[´é³µ±;åéÉ@¥ Bþñ[¨ßËNüŠ‹žI“ gªXújh×θ 6àÞ‚Y!PÁ˜!Z ?&×–è@Îü«ã�%�ž¤ƒöqÒÆáŠä…åM ’ÕïÊÜ:½aÄ# â’úp¹‹ý&ŽeüA[¨cs‡• —Ì!д<9!!¤‘„>J‚6¦ÎúÅÓ™ô¤ÆŠl<¶ † ð£#½K C6ˆ“u�)a¼ÄJ¼‚™ã;çq%£D´.„B˜|JO(ËJ@iY·[ÄšŠ–� ‚Lu)ñ[.´"GèéADïI ™8Z{Ybg?¸ c¶Äí‹*O 0Ö Lä�8Yá'¾gBM¢pã‰âeo>ª:’oã…]›O’V(à™uçPÖx‡�›‘p‹v²á7ؽO¦|Q(Q”A„Mfå!;î$È >Ž’J¥RAÞ²©ÞæÆ”N½>˜ÖM~iOà·øO&2 i’U7Á…GÃXw¼ýv©¾ °±J¡µìL.ëoíñ’» M�àÇÊËäL9¼ç+–Xl %õ2.hI1äAúd6âúÙ[Ò¦”ð!p'Y)ù–ayât© ¹†„"4ˆð!�?Š6•]ˆúËW�Dš!–0¡(B#K„T"·W*5:E�ĉ=3e382O2FZ':ѶNl�Å•塚5¨Ô²zÅ�Ã&Ú$VF[§m�¶uªýµ´³2ˆ2£äêub Âï¶NA¯kùÖëÄFl˜ 50bQ8.\Õ¢O&Þ`®à<œå>@ÁÒOs]KN¦Ú(à9 ‚ �DØkCÿ'½†´XœyȬ”€ÎÆ¢Þ"À{¯ƒ„€*¹C‡žF$$¹ï)€4€…ÅÈg¸õõ¤à>¹è <§zpÒxN8€øKé>_)µ%ˆ‘Yðª K@�†:‚8BtÎ~hf BtDiP^�º±qʺ�L GˆÊ”Hæqr²Læ#_Ô:g¿6¢¦Ð�<¾cV–º„WÒwó8Hä[ç-<ß”õj–'\«`ÝEaÜš-xþ{sßø 3ìtg<”KøÊ m6@|ßDÞ` ?²TËÔüg 5"PÅx¦)5_^+¶äaóù†N˧¸¥ÁXЊYþ ‚ Y¦Id]&3÷¼#‘!4O’�*�F0’t5ó�‰å”9|Š7M¢4íA"ØŽ$ÖȬ�^½äÌAvæã4x•”â ÿŒ‹¿vÇ0$Q(Xuo7+a7µœ8×q¹ -"S<åý§žã‡¨7"µƒ¤$,'Ñ0¯½Õ‰ò”LínCnw¬˜Âù@IDAT‹®dÍl;Œ¥‹ã) !ㆬÍäYòSÜS ç=‘ˆÒG={$öHÌ„â<áÜ�i�ȬÚ:ѾÐ:Qsu¡ÒÖzQ(jѶNÛ:më´­Ó^'b607ö:±×‰­�›NzÓd "ă1†°€üø¾!:�‚0MÒ±JÎ�„gÁ\™øÿQ‘VbÊ: zSkB‹8„e˜$ˆ³8÷ÀªÌäKÐpE q ©™˜,‚8É ‘ä‡òœ?œqr+d¦]6F›&5±2¤àÒÓV¬×œV´�’áK�11lïJq¼ò”F <&…�e`Ê=ú‹¨.Ìhm-…O@PxX3­�ߦ¶RºW…^D<Ð…q¤Žé¬׿%É3ˆkƒMëh:+"M…˜Ig‹;ãDµ–¹É ?T?ùM.IE«TF^Í0¼êÕ8©Y_É¥Pó‘ÓB ²D«ZumëÊÙ*?ß…‰%:Iª2‘’I ¥ÁX®YíŒþÆ2‡çªekB¹r…¨Òyî4¹¬¡ò[’�½Œ�ÔEÚ°L` „‚¡YxªÔ.ï)íËaŒ|YÁTž¬ ÑŒ�†'§&'NíÑ=iE¥|󉨕:G�DúUú²Ý“£Gb�D&'4;†3ˆý#Ó‰]¤ubëÄÖ‰jŽL˜ªê¶N�Ú:¦Í¡k MÛ:uœ¨:8Ú:-ûSË©D&ö>^·uÚëÄZjªWz�è`axDÉ*‡L#6{¦ø+›‰;TľUø íâ›%Ôás�ÌâÜÃ#À¸[!R<Õô`Òx1á–1õñÐpbʳé@BXèŠ$(Ím¸ôLË̽„œÎlÎÜj zú*¹«áü¨£÷S9, o¤QA;$�§�H‹yáPr™e~m>Ž(?ö„=B/™2}ò�¬(LÀK¶Ê媧Pè3\Ö›R�ÛXÌâútjs,©WïK¥þ¸†´!™ÁòH &CH¡Xõ J´ù§öAf2�Ý(;ØñaZ6¨„Ý,r²Œˆ!€Ž*?Äà‘¢%AQ†&ê ì@ç48ó•ª#öò‡Od…£#T-HH²»ClgÀXÝ´e$.Ÿ¶�´­‡¿Âœ2'º0! ?\C!P•üëSéë>¡n0œg_A ±$óÕ‹U7yi™|Ûîì¢gjiÚKpHWqÔÍdÅ…¿2ÇZ ðh)$ iˆ’Iô#¤_努 ›ìÙHÒÓ8Ç7“w®f­ž6±»ëwÇ%i¸-ñôÝù„ýîè‰ÒŽÖ¡Ñ"ín²!>(mû‡ma1µŸ.‰†'¶G¢-…\íþÈD¡Ðn\öHÌà³o"¡‰ÌD=3ßµND N÷™+Z'2g" u šÙßÖ‰HÁÎá¢ië‚QÒÖ)Bжô¿­Ó ޶N3O”ᎡÎI¯{�¡>íubÌ,$Áшͭ 6nA+-^;zJi§âÏoŸ,pF Ðp¡ºø. zF„V^Ž!�$@q2sqN¶Š—:êŽT  ü’¬)ât�œµ+àÊ”_ƒ¶„wqWäÔÏ©07—ìdòhÇ.¡Ò4�C®¢y©¹ëY^ r +IM†´SH©ŒHŸs⸠™„C¼I�pCù“ —f]þJÍJ(ŸòIoNV…/q*sÂ`úà›)#÷¼˜N2:Ñøß:1S¼ZGÑ1±:K·ND­ÕÊ*`޶NF[§ÎN̦m�¶uªŠíu¢+Ÿ^'ö:1ŠÒ%s#6QšØëCl&ˆu±9b×3ÿ(ÌyQÊ’Èít1‰i 7�–Ü0?‹lÝÐÅMÄ\4s±ÿùËæQL}ƒâç¥ÁÃ0}§Hp ݹ,„�°4‰,ÖrÍíjiq@¢Ñ q£#9”ÉXÖA8@+6IÁAyT·9÷"O©ÑD—<^€)Ž(v¤ž¹¢.&‘ŽåSrèfõÂ^iÚí»z`O¨œ{â!kI(ßzØñS.zñÁ»œë'ÅâhxúT )Â=ÓiJ¡sŽ3•¡Û2yë+û\G:0Ûƒµát(¬h¹–Ž­`&ë CuF 2ÒáŒMìJç«é$‰aQOˆ * 6ÊÆiÔç‘w¿Š þ‰ÖY‘BÇD2E�¿äx‹çR¹ Ar¸�EAÙ‰%¹4\V\Jvj’”8Ä %’j‘Æ&$1��B`R$1uÀ/òÚTÝçrEĺÒNëyNè�½YÁYJ¦_Kÿ±ìÎüÈWû’“—šÒ'|˜”‘d’'sȬDéÇt28àõ±Õ6„ttÎÝÃŽL3Š ·�ä¬ ÿöY(""·jšÓ»wy7­ØÐôSÓÙ#1£¥-éæt�‰=í=�©˜7˜3œð˜âZ'¶NŒæh�Ã.f—6º´­Ó¶NÛ:åI—¶N±®1)]/°¤èu¢ÖC¯B¯]ŸbR 4bã¨qˆÍDep½aDƤO—"q­!8ÀÁ�ñÂ(€‚æª!¬Õ“ ³d‘ÄpF°Ÿ”®¤%ª£{«à4Ί)A„D—UV…ã¤FŠ·©p?B@ú q`ŽL¯šÄ²B7©Ì�ÀD¡?ÌÄXin¡OÖ¡’Ð)èZnü™ø’)P)Â!.Nf°µæ#Ç3¨T­«¡Ig¦¤5ÄdçáÔ’%̰'>3)\…¶ ­±|û©\"¢À`r#1¿øÎ0’Ð�x(¨hÓT8Ö“/ÅÑ6Ò]ð¦‡% Dvþ-vY„…U ·¦¥=ù±Ç&#¿@ˆ�é ù‘˜i„Lya”äòiš¡ˆ�³ë Y‹Óªijl³†hèA™üÊ_DÓpûi¸)ÚKú¥™Œ÷’¦°pB­ë xAÊtÍÔ‹¯pC8m!d–„’0FòÅ4—i-ñM(Àkj›ðDBzu¼"(‹JAJ“”±_Np1”rжGb¤Ö#1Ãî’¿Ò‹ªûrÑ#±G¢³a'¤Ì1™µuJªùË9‡ÑÔ:±ubëDÕm&Ô¶N3{:} sHæŒaÄ8g´uŠÝ†�S¯­S{J[§¬/Ьm�ÆîXN¦™QÛ:uÞìub#6¯Dl&cv{ãfÀÌ_Ÿuã—&Ç{¨±=˜3+NN`iqj"¦±ìª�^x˜>ê2!®øÑð/¶˜‘®Ô\º2™‹j-ä`¤ôä£Ê…¢´¡¨í"È.̤=O~2šËôž!YA¯ä6$¬¤Ú^öåoYózN.ën´… ² —IÄì ¶ü�4¿VGvDé$# yà n ‘W~‡œ’„ˆP3_ú¤¹ÈG1Å ˆ�\wI-½ZžŒ%o:–½Ä�ŒATGQWÅ–ÂS »ñ §rGêøFdyæ…)9‚:WP^´WX 'I£,%$�7 z$*Q$fc ¨ôfEl˜=ÁöHT4={$¶NtæTcd²Èt‘‹Ö‰L£­Û:e\´uê䣫­ÓXžm�ö:Ñ%UþÕ�.‘z�èBiô:±]vŒFlXÂëô(‘¬A]¿|ˆý°>Íjÿõ›Él¶cnÖ·:†l²�•D3 õQ&Ц­5ï²° ’�¶àÎ%°€'�Û‹�^°q–LŽã'¶À·H/+|Žx©ãöF“ñŒjlnžÒ`ÎîWBñ«Ój&P3z§Li2™O×(§†x¬ñNSÀœ%`npcL¡“YJ ôU<’vïUÞBI1If%!ÆA–rf37k—%bS.ÞU ‡çœÜÊl˪Í¡nž` t ®b÷‹òŠ#ß�)`§Þç±L8!̦¾±ŒÊ" dE*XH�p…Ïád"„—Çw‘&$”$©(„�R ^”…[[šeb°är×aBw"OIŒ#¾†àHfÕ*•Ë4H¼…™$fËBƒïY|Ù¨™ØS8ÚÛ8 N©‡[\Ú ¢‚UéúÛ :“‘:%Zî¸viïDLÖD†íÈÑ:n§´frb4g©íB7JÐB�š” :£�÷!ºÞJËîŽ&Aqh Ó¦ytXäŠÖ£¿ènH��C›©`‰®ã+Hy«þƒzíÑÈ@ºT�Ýâ"^ågÃ8EBHA*A>}"é×D àñÖTCÓ|$gáèÆ€Ñ­2ïð$µï‘Ø#±Gb�ÄÖ‰juG&Z¦äÖ‰­Uð1Ú:mëÔ©¡­Ó¶Ncöc_c@÷:±×‰½NÌ:“un�Ø3ÄFPÝ%!€Á´$0*Î?Ú7õoû ëp)ˆ�/R I�²ØÙ FLo/âÌꑗ̤£æÇÙ�‹µ ¹ ^�ĉéÑö·‰=�ëZ'ªRZ'¶Nlë4fC¡­S º¶NÛ:ÕæV;ø‹^ëŠ^'*‡^'ö:±›ã‰ØðTb 3AÑ@ÁAþóL€�!¹@(s�‚ †ø¹D!ú€‘HÐ!“¡³¡‰P>P*ÎÀÇÊ@8‚ÓExMa)Ó«Pˆ �̧°àa™IAá9³®S®¥ =þýñºÙæ­�ñ&³¢)¹®�`™KT «tÒøŠ„íó‡±ÿ“jy*;™ð)½¨+H²!¬BXx 2!‹Ñ–橹]_oÔ PœV…‘—²Þ f6Å ã�Ù! S{‡b&p)0I! 2¯K eÛ:Êö¤":‰‡%Ê”]°)˜áŒd9¨€òæÊDîá¥Ø0žäyj• y%ÝÔ°®sY_Û|T6 ÄsR—žÊ•õ¾ ñ¼bœÒÙÊ#£0bã[áð D¡NÚ4…µb$#p ¦‡" Liiû”ÂWÞí�êJž¢‘ B‚抲¨ds98 ‚Ùw&iRT•Ln²(Tk¢EDsZ=mKÇFZIöHì‘Ø#±u"SeëÄèFiƨ­hTJëĶNc˜Ds¶uªq5V±¶b¶uÚÖi¯ ®‡Ô&½NT�ö:‘•oÛ.ú9Ô!®ëµ3±ÑĪUéz›Iœ‘…ñÊŠ]üA°1(8˜ƒîDÄú�¤.=  ñ}#=íþ4�¹L Òpú‘A‚¸Ø‘nizÇÑ×Ù} Z”J‚Ổ@òº‚IdLgÈ! âµ™€=, Ç#øSæˆw›lâëX7Fø¾‘3è‹E‘ â©GŠ€n(œp "Åï6úÎsp¡/Y‘ì’PVEJ–ù(®¶(�°lþÙø“ùæŠÝ�mˆäÙPù£¢ \æ(Ǻ,ÆìF½�!¥BÇ9LÖð%S#}Åèüœª(’h‹œ"/¸÷”çNQ£yiçd*¬æ•Æ[Câ a3‰r¤t2r„oá €{aENxŒTÊPÉ#+p˜ÐdÄö(ÕäˆÈJ‘¶�UÌ7õM5Š‘ʤK¡d!}0™ªqjd‚¹Ø¡�h¦fùÎTakc,� a´—ÊÀªäC ò€=’� õ!g©pÀߦÁDïÌ'{A^ß`°h{@Kbs‹qŽ.‹o¨!åægl¶øâW¾þä·ž}éÂÅšIÔÇD`n÷Üuö]?ðýßõޮУÍiy­17ÓkâÚ§Œ�n¯qff�7:—½×!�ÕFÍæóßûêÓO~ë;ç.\ ¾ú‘eWs ¨ùØ[í�ôÆØ ·vD…ˆ±cii+q�Ý1ϳÓ>žY�£Éy.ú¼ô»Y÷¿þÔ7Ÿ}‘)n0-ÖÀj“Ø—P@÷Ü}öío}Ëû¾ëÑlß©�,M™Ù…/'bàÑd¶ o‘®$ù¦%�f’U0G†Oþó_ù­Ï|öß|f>_š+ÒícÍ�Ç�>òÖù¡?ý‰ŸúIfûšêi•ÕG¢MŠá^Ž×hîUÌ7´=ž~®æš[ë(È1uß{×ÙGßöÀ÷<öŽ ·¹ýÖÖéQ4ÄmSæÚ­Sæ-úd¬-n–æ¹ †IД€¨îg�߸&øüˆw¹‰½pÎOf< Éþóìƒo¯ 1ô’oSC‘冹7¶-K£Ù'k±=6ðË4ÖØ>«!Mè<7b:-î›Ñ‰Y9BMMJ©XK£TøG¢••ã�ûÕØP.¶0¥²’T%‹ÞP^6Wp±P–›·5Ý`'+à0'ýìßM;kSWuHDDJÎI9k¸@Õ00»¦�÷L]ÀÀ m‘ØJD<’ä`óáy™�‚Ž [÷áF¨ ¬Î0¼·G7‘²÷{Ý�Öw'l ¦.› ›A†ÇŸ($�$Pò·Ï]ü£§ŸýwÿÔûï¾›98袭íŠLcþjXÆÖÒðHGô6_uRÊS±¥¯ž»pù7žøbOqûhƒƒIŠmËTÃç©§ŸûðßïÙ{ÐNL+´( •#j‰æ¶õbÊo¤ K;›p8Åæþö·ŸýoÿÑÿðÔ׿9ä”Èæ“O}ƒÏo=þ™¿õ7ÿƃ=hÓ¬<¡’°É>wáâ¿þí/½Ð¶ÇA¶ÚaÒfêþãsù<õ�ç>ú'¿÷MwÝÅB®­ÓÃl‚Û¬¬õZ§hW@Z1f�¼°s…WP+|y9Ÿ@fLU‚tÄòÍCJ£‘JU°¨"®î™!ED�Y’ј.ÀNª7’ªÏ2!r-M»œÆC¢ 2"´VÓ‰²›äβVÄšì§l30.Éý¬©�ÎÉ]¢¡pÌ:@�ÒÊûa‰™` ’œBLñ)te#‹ÉQ©F¤�LjñRbC͇ú“³�2må2ÅÞ…õÆ7‘r(¿„¥0S.Qzy˜Þ ßµæ!oбX™×ŸËzš’kc8±Ïð©šü) °AmF TÓùO†}X§M°4Ëù;!ðªMko Ì „aŽ,ZÞÒÀðz¨ÛŒ¤#§ŠÕdv]ê ‹ð˜~B(ò’‡u*JÒ±Ÿú!ʸDkuC\‚ÆÜì:‘ü=K†=•C:fz[õ¿0ÇÿÙßýû0 ›¼<‰Á£EÜzaÐdœ8xwº|8Ô¸ICïß™ÿʧžÀ5É‹>ÞØ¸²3}æùÞõÈÛªƒ½ñáÓƒ›ƒÌÌÜ@aÖUÑÝ2/;{«•‘,­g;³_}ü /]¸üÆ–e×~ H‡|é»}wºæBöˆô>&4·}1Þ½îI"æ=�Œ2›d€Ž:�ÏíÓO¯�¡&±& lïL¿ý�{äA7˜D-aÀJÙ‰D»yÄk»ËE[£q9»8åO›�‡&væóÿò¿þo¾þ�§×ÄT“YU/½tî÷¾üå�|ôö[VŒ¯;£Kl>l�ùb~e6ûµO¾æªB?Q阺¿ý�{ÇCŒf­Í¶NOTóCfoÝ:uŽÊr&ÁDº?�¶'�•ë$¥;ÓUÀ µ’‰¢ž4€7p¨" ?²üj&³âïq‚ã ˆ2Ÿ%¤çÖ·á¹?ŠJ¬þ� uð‚ {¯PR›ü’Ê7¡}:0L¾èÁå#> ;O§^û‰ˆù…ÛUê]{Gga‰ñ…‰5ÇŠƒµ¦ÜÔ°3›J#ͨuÙÐÙ¢aA}AŸ�I­Òð+7T�ÍâÊB´¤Ì�ÂU�ä—2ÇÒ‰M¨ˆÝXZúH€Ó8x+gùOŒf¢4œ›wìèíÙ¼ä´T¡4¢iYì¿Ä ¡‰câaÁY͹!½é¾­SëbɰbƒñˆÉ¥ì¤Êa$—Ö(3)Âþ’ò84·Ž \û$†pG ‘HœäØœzPM¶ Ÿå¹™¯'œ6çnñ=á´9ßš» â"T8µb›fŒw¦T¶¸‡êRH›Ú§¬„¥s½ê:ª=iä@« æš­Gâñ‰ã¿ó÷þ~ú½Øé€qä€fzayR!ŒUNiaû|Mœ,æ‹Ïýî>ý웳�–ÀÆÆöÔã�7ß‹ŠâcgRù0K8 ëo?²EŸ-ç\z™xªcñÄïí›Ï¿Ð²l ¬E`4óéô¡·Ü›ÎH¯KGÌía`l�XBZ˜¼˜'vNUZ¾±­ŸÿÒW»C®¥-ÖKdgg¶˜Í¼ÿMšç�’׿›ÐÙ‡+­@ø¶ÁMå…�`wÁ¾Š¿øKŸüÌgŸ0¢�C—À¹sçi‡÷¾û»Ð¬²^{$Úr®UTj“Å|6Ÿý›/ýá3ϽxèŒw�‡$l‰ùlúÐ÷iJ”ýÙÖé!Éþö,æV¬S� —Eü ’e2bNª@Œ´P’¨^�^áTª - |Z¾…ˆ¥?‹eáü²± ýd¦� 4–Ù0[˜ú² utÚ’ÉÁ ªji ²/'®‡ÒWÒ‰<Ö¯2 õ‚ (Ž(¢d£T…²djÕu¢Ëˆ åˆÜP£Áâ ·rïCåÊ®`*Á0“Cß]@¨— ÑàoJUɳ!7ÞÈw™*g < “WšZ¡æ³¤[£“ÖôÞµC™Dºnò-qËÀ„ð+ÒS"ðÆw>dw#DÓÂgcZX8�`M­ B"Þ$¦Ja&Ò2 ëfñ0#gû±NM/›6˜b.Ko“x�I«;PíÆ—†Ž«~ž˜Õ¸XP0åC«ˆ²TE”¬º·MÀ‡pP©kr‘YØÌ‰%‘Z‹x&ÞkÓ ;¦Ò<õ꣧af4z_ëD™J[ØÂ=Âjˆ�ÝÉõH´÷¤?g°ÓÿŽj$Nb÷âOì(ZŒÙ=‡ynæ…£Â9†ÊI‡®¦ç{Ìg—.m?õ̳¹ê¯–À �¯ëÙw?öðÙñÙSsÞþê,N�bO-:šÆ¹V�¨¦"œ¾¤Uâ/ ··Ÿúöó-Ê–À%ðä3Ï¿÷±Gî½ûnì5öòÂDÁ¤À Ènv<�‘a³ ÷ÓF¯™o_¹Òr�m±^R4Í{ßõ¶Ó£3Út�¦%1Ål:6/eªq‚Q¯9Û¸ñ¡Õ3ÍbûÒ•ÇoÜMáÕñ™Ï|îÿ±3g'Q ¯55æ±0i¹[—¿+ÛW¾ñíïç]îáHàÉo=ÿžï~ôγgYe·uz82¿½K¹ië”þGÜ�„A¡è0ĵ�–«{¸±&Â’�~XOÒ¾5\ ˆ ¶Î6·¸ ÷ø\PmN1ƒÑYº®˜ž7f%—;u͙͟BXž äXløÀ>)2ŸÔ‘‰r5a²N³\Ê7÷>t¢t`NoQQÔ¥XŠÌî#LÃû\'’;¶–‚ÀÊõ€øÛrJ—IÖ ‹ñXŒú[ÉѶº{±…Àtk² ~i¬!Bsx]QC2Ûí(×Å)2êîe‘4± †ú%*ŸùL8©üÂ8/ûé)7Ëã#^…7üí‚ÐKr‚&0\\ —zrÙøT ïVÍiw=Æh6&8Õ M"¹ 9$øÍºšR76ØeáÒŽãÍ­Èv_Ö©,_¥)–ü÷~#oÐ0ê´ bUoo ¿Üž¤2ZB˜¹ØMT�¤V!’¿:rÖ3�úh—'ÕîîT•šÛ�é4È?PòÌ€ á7¤Íe�iuc�p‘§â&Lï9-ðÝ�‰N�’d•ub�ĉ·ÇHtÌd¼Ðõ™£˜2^{ü9n�h c¶óŒñGÍóÝóç/]Þvþè£%°'�K;Ó Ïßqæ¶Ù „á¤:ÌÃ(u'ÛÌí%é^Lè1.\ìµ'É>Y‹ì�ç/Ü}æN^ ƒ?-–†��kËÆŒ¯{Áõ­…§ý¼1_l\<é¹çå?Êf}öùï\¼xþÌ™3ñ5â�¯‰Øí®ÖX7ašp³¦»»[SeÓRÙ ð‹.œ½ãŒ&pE[§‡$øÛ¶˜›³Ní}Q0(ç§<ñ¨Œ8ÕRÐÀåÂÛ;†š<ªÈ\ Jvý¶ÄdÌAFUV ðF´t™êøÆHq*ô¿>BxþSFå GDš#{j™:¦tŠÖÁh �â^O'V…²R¢œ™IÍipxå‡âW_'Ê 2qà :)­Ð !�¬)ÁÜ(Œµ�Âbºw¾çy`/g|Ê—) &¯Ìˆ ûðoÅh¿‹„”\qxyóëAr-±FÉ€(ôN*d¡d@(ж֔œ¬r]E'@–a&´|e7úàß2íÀXœse‹”Cȶ ÏC(m)( oóú´æŠÖé5:ÿ²@™ŠcY(K ‚ah…Ò�)Sñ¿©,ǧÓ^ –E’6¡áqHÛúP5k¦¤"/òo+Z &l¡<ð*!²Ð¡ü‰`üFÚÝrá·ÌÚ²g2[éªu"A!ÄW�D„³*b£4{$Ú�èS™nŽÁHôÅ+ 8Ÿ|‡©Ý) w˜‘Ctpn§p ž�fìñ œ‡ç/^ &}´®‘À•‹—7s=}*Tk§Q;yš.¥:p6'ܼܩâµB³ù¹Þ0ûQöå:$páÂ¥ù}S_ÙŒ¢·Ûa@ðÆ)4ý Úy�ëmcÌ^M.ç¸élÑïúX‡ø�Æù‹—¸÷~n×DR÷z5Ú%Ù2T Ï|¼^lKE[„¢ B²×·NlÈ :Ñ>¥,7q÷g=ïX´>B‹ =#&Щ¼�PÆì@jÓ� ÍJÀbŒó%gÊ!àÄ.�†*¸j6™¶ï²/ a„Xs[>< '3äîFolœ`.Üs,�3†ƒÒ@¶ ,/8ÙAÙ‘77D_r†§ëyá�ŠÚõ1"ãúh ¬UtȯVg‚êQ]—aÇsîà H§$Í­òfÎã|¾¸r¥;äZ[bÝÄ®loÓ^[ÚÛ™S4Ãr—Ý&ôÖ®­#M›XûËT#·�ÌQ LÖ0§l6àõG"Á|Hâr]bóíöÀ<ê¦;¤ò¯\Ùv‰ÇÞÜm�’ÔoçbnÊ:unB(Î?ÌF"ÌJì]¨GØËU1b`ÚüxÍ9S[oDkï&ÜGíŒ&A˜Q�5´„Ëæ'‰œñ\‚‘Ä òÃêN2ZÍNŒ)j’ŒþCÑ©þò1Å*:V,0YØq§|¦\(ú@%O–a×K\q$†IJÏè…CkMn8µ ‡  &§U§ªF!u¦ ¼bœ2 ·°‹é­VHÅ rÂg…~‘Ä©2…çLGnA3 •¨(…øø#·šE�h›P>ÅÜM”{fÈ^¨\ ráX‘TIx% WËùX–«kXƒ=" hZ É3·(^1®×²NÓ²t¢  TÂku¹AL2ÏAD¤!eK³lWhñŠP¬d‚-ddW©NI®„Ù�Yd"-*cR~É.«Õ2RÉÕ'LIV„Ó^œ{�„T8BÕ€°µÊ:1¥Ë±Õè‘Ø#1ÝèDŽD÷Á~o}´^%ü׸3!ŒaŸñþ‹J…~­í¬™�[Ñš,™‡£ݵôUÄ: %p«˜óø‚wØP×es{ï s#ÞÞäÔç�:ᄅؙóè¬ZeóÝiOq·*üÍ? ÅŸ#�ÆcyÀMX¬J b}½E›3-M>4zLE oÞÞÝSÍ�¶ÌJÄwfè �ÍýŽÄ˜çŒEÒèýÁ@N®• èD'\>ƒ…Å n¡•ÚÖé oÎcÀþMX§h ñ·;C‹Œf¼©S<Wz�X¿Xiò’šHm - ¦,ž,ÄÈ ï…Ñ̘RQ‘oŒ×Á¾2°�Ï�0ãñ�vóy{âÅqÌÂIl•64n9ÇwIo#&HÊ… ½¶VÕ‰P&—ȇs¬f:³1EmLõ›¹úƒœìo�H¦¼S¡…io„-ÞˆgV¨ÅU ™éÍÁVxÁ�(*…Y rç;i‰˜Œó²®PYƒWX„²ã>«ßÅ©ü7·¬r0_\Ö˜IÈ)…ÑÆd ¡’…È,P<³D×�.�1 ‡_qÒ.òBÉިŦ$}„ž/ËÒâà09Nb¬˜g11ñîF¼Ú! p:‰A!†É@@—È•¬S;Ia_�H‹ñ†:i¸9�Àér¡‡x’ØmT²” Kö[Ö¬‡}?8%è��áÍnlV(]nð”„�Þí¾td壇¹(‚g *‰�ÚBirõ¸ &?—(gôŠËG“‘}´^)�ÜÈ)®–R©)—ýÉäÕwT!é|^rÑ=JÙô±^ èÍ6ô2t¶“7´‹4žˆ`.KÇ‹uKѧ_.»ézÙijk“Î×ÕŽ6–H&vˆ®3[;R½F›Æþ£dÛº•×ÚšàVá~˜Å£Öô�Fbš4†H�SÆjŽ[)·óž ½±¢sm˜¥|·uzRïXòy3Öé˨á;MÕ²UËR—ð¹L ×NêS +D5 HT¾ÕF 5  ždj¯áàB¿(q<ß¼`’ Rvý˜-fã霥džéc-'y¢)‚Ÿ•u¢’s­ÐŸ•„œèŠV’¤Ä\V_'œï6€„KÉx‹³>2ªwI0 !œŠ�ÀrP|ÉSÇuʧ*`މk$�uä'\'6ë ʱ,cŒ3+‡™KÅȼ1,c…¨¬•ƒÖ�ÎKŠÜc°�–ÀH`�•5£ ¢áµ5 v¹¹© F�›lÐõ0í´Y ·1 7óbaCMr-°)µÓhO_Èæ¦dñ@Kk ¦]Ù²™8Ík>Þ€n÷ÚµˆÿÖ‰ðœÃ�—œº¸ÁH¤IõôÐöàÞ¾îÕ[/»)œ ¸Š›nê;Â[�¼Û:=­vŒY¼ ë³a¬…�s¸îaª*„*SQ*‹"òÍ›å…Ä/k$ü®óºÍ-f­Í©.*L\AÔâð¦“qÀÞ»!™ØEŒaLú9îaâ|tÚwþƒ€&³Ûb²h È5”½ÉÚ$!nU�b‡Nœ�ây>Ǭ ñ¸ËxÃs,åï{�?$8Lép¦Ð ˜Zà–Æ�3Áü ·`¨C¶eCS[K½n`S‚ã–&N£À¬ººé M7ÖJÆõ[‘ S"ÈÓ¾ Å&ò‹¢r ]iKÅ‚áÎDµ#J2°E §”c¬c`ÁB42Œ¤'°¾A=q5�ƒ,lŽ^IA]âPH"S[®â€�kkþEkýìÃ:}…N”©èHÞl— 5Ÿmå#`¦|Ý�Ž"=¥Z L¹.Ï(Ÿ×ÀFÅÂaÉ.„›8¼Ùz*\me`A»>’P"qàc§8åI.å¡^¦¾b“t'’¨Üm4BÉ¢Ädm,¸À¹ô•é%ùºëĉ=íz·ÅH¬½ÞœoU臃ÅYxðpÆpØ0D2†2Ô¼ðº�–À5 ³ öõzæäÚE®Ú€Y–™6ÀÉ«š uÞ5„ú²%° ØÇ4bµ ˜¸™Ø¸´‹Òû˜ïô�Ó”‰AB,&„}“yú8ΰ5fcÇÁ¨ÖíšÆ+›/�LƒÓì1¢«6š{}¹X:î\ Üp$jŒ8k}ãðuín¿#o¼ÃaÀq;LÄm�ŽÈoïRœNPû³N•ˆ*”„1!Rà�hZÈ9Š—…BFbá¢q˜® ÖöÀÒ�VŠ…Aj3]Ä5!ª('F)ø�äòÈ­%YÑhf.tŠTé™Â´Rà;øÝþt"„0†$çÌì"SÉ¢±Ø«Œý­S™R"TÈJ¦NNRVbæ"1¦q-�ª!:^‡Ã,‚‘1šN·�ØpÉA#@Œ¡+²«N,ˆ®?I#ÐÔ …!3ž/µqL k™¢è<�›¦"$ÉÊ45XBLPS2¿ô¥AÞ�´>9à*$å:µóÑWêÆ¹im}óÉnÖ>$�4^ß:•D®Ò‰.¡¤%}ªÁû�w µ¸Aü²%Ç Zù)]I”@‘Ë0¹eR|,OžRÂX‡ù€’M&}³/2rTg‚QFH—6�7KS hÂǪëĉ=éJ·ÃHœèñ戠2Œ§ Igs’ÌaºÝÓÁ¤yë�ã ×¹‹Ô}´®'nÌùÖŸ-ç~ïæà_ÄýwêLöÜÚR_ëw\˜‡wG³€#×#Øa-�[‘3�p2çæ Á[­ÜksÃmmã¹ýì½dýpb«×Ç1—€³Ê„mq2�hÇÉjÝ3¬âZ¸cÞ¹ –dî„kMöqäpÚÇÃc‚†»ÁHÔž×n'Zµ¡¹Ï«áÔ(}Üþˆ7ïàÃa^K¡:C[§·ÃX oÂ:Å:ÐC�M&‡2ç V8A¬ eƒ5A*é`:Ós�Yë”7ûx4Æ® '3ŠR¸|Òðà7g·šã„q˜é餑(j�Ì l3Ø,nä†ïó`a.0òD“ž·F“E8c%�HZËÕ³4Æ8¥Ç¹oìë+­Ž¥ÜÄ:Q‡3ýðfy6X"îX>[@­¢‹ŒÔØ£”îZÔ——Ré¼ð¿*âÔãÙšú¹oðèŽþ*ùøRV¡\LÇåÌe­&‘ÂÓ¤PwM#Rsn¾öªÇ¹ t–&#Zž¢lèó“gdi_ ‚›VV’¬)‚¨Ù)¸œSw¸àK*Ð�(X:ÂL„€ü‘56][Õ:­iP"щþÆû j…Náe ‹ô[…B ©oº§÷)©˜™u.Œ´lî,ƬÆ£©" zçI±hËH“kv¬mN©ÓoÀ»Ë‘n›Æä}©Š�Z™„|`uöD�T;1Í"<(9ú¨í¹â:±G""ë‘x{ŒÄÜTpÞpana¨¸Gþ.cÙ‘â8SEp8RºIá”ÁW-�ëIÀîa�âÐ<ÞbÄITÝiשHU§ªCgf?}´@ô¸ô¯ê{N]˜e)a`pÉbŽÙ-GÌ‹Zâ{ð¸i’k•íH+ ê(ÓΠ›l;Í¿¡õM¢ÉÎeÐÕµ2ÑÄnFˑ蚆µÒuG¢–º›Äqk›¦o¦¼ÎsÒ$@닦k�joÓaÚ:=imx¼øÕà³Tì«X§éuµR—ø�ÄpûF“V#’¡9ËáBÑ;>ñIŸå]MÎpôd¿¸›  ‘ Ö‡@…É�¦¤²LIÁPuÒ3T$‹#µ�„Åšqyáµ:±86­ù+:œßP'šŠ�°• >±þä)„Ô~Ö‰’S,ð, …ß�Í wW–@BDÔÊ$ʃ¨ya Î¸ˆ[R°‚C ……•ÒtInÁ XT °3È¥‡yY›Xß�®¦'»J Xëå üTòUÒk‚æ…’’õ˺–abíŒò=ª8¿3“‰vY…œ@Ó)…„½¦uŠ´(ãj�H76ÌÏÀA¶t7‡¬­a™@Aÿ…Ã¥eápã·0îU�ÅçP/5ó’®ÜÁ/ý†¡K„ù+ȇ–U+.ëŠ,U«“1‹Ôé‰*^ÀºÓ#‘Æ£õùOkrÒ#‘Αþu;ŽÄÉŒ »–לðÆ)¯ajá}îyÙq)"9g¤8©f²#¦�–À5` vß&�å¢XÓ­È­=Èíj僂Aí¼n‡Cùõë$¯d_®EÎou o¢RËü·KmWvìËoϤ—2Í1Õq݉7eû8ÎÀ¢ÃÖ˜E7q¨ª5ZhA¢|6Ñeî‰Q£Åçæ0x%$µ9ú8: Ð(Øðì¤ï°¼ÑHô.~Þ»·±£Báåx¼½­�7„\�¹âfˆ·uú†hñƒ®äÍX§¼/³øÊ�`‚®FY ¹õ�ëf-WQس¸QaC°ñæ/oôá¦8ô“¦6‡áñRJÕRÚÉ|eæ¾]túè5W_:F-eÃò|¢ÙBž7”Ë……“|D$PõŠ:‘…mt¢¥³ß–îáp˜½Û½ ‰ÏúsŒ…¾¿u¢ŠØ§7Çz®ínéó¥S •e}À�rÛô5¤©7ÅÖÃWð“Ê›WÅ)[7Ý´6Oq�$§ä�Þi!I‚`´šB9"@|ã�1¾ØA¿5# æ @¥è[9‡—ZŒv ¦§\Ádä…ÚT<îla³(ØÐ« &ik$–ÎVæå=zwY Ir!üS!Ñ*;Jª´ë²WëĬùÈ)>†̶sð@éÐ¥H<ý$]ϵ>¬ 0¤ý�ùUní\0n¯D†[lÅ—×O@B‰“N mæÔÄ÷*­5¦�fb�Ê€�´È‚´ÈœÆ䣨 Q�éQaÅOsÉmú�äáìëĉ=o“‘ÈNëpl1préXt¨1 øÉ·ƒÉ!h"Ç ˆ£¨�–Àõ$Cgav¦·Ð‹ìQt MNU�hÛœãíím±ÚÁáz´:¬%pK`¢ÂtÖß¾¦0-'û¤Ó„ý±s–of8f>¼¥‚;ó!H@kSÛ7âФ[÷‰¤}Áô5Òõœñ’É'­~Ìu¯%Û!#‘ï¡M^5³'G‡@IDAT²]©±<ÊT7´�7ˆÒöm�¾AZû «¹Oë4à‚HBž}T—À �Q„ fB9T¹,B·ˆ•éே@‚ª'8³—ÉÌèœÇÕmPŠQ† Ìs‰g`§d¨´\�ìq¦®ó718U� p•üñµ2‹´’Êô7Ò‰e�'™¶‘ŸüP6„Åä Ž(# Mùd¿î:ÑÈ€exZÉ<Õ‘™¬¬„°ŽI,‹Ú:­ƒÃP5dUiVÞºA)ƒŽ¨œKSFVð¡åFi)¯p&Î#âdÞj,Rå‚fµI@š|H3YxÐ’E­ÏÃR*Àg¡B!J6 ÉáŠ#G^9È9$S“DXjgU-›¶¡Êù&<µ�•—-¥#ÂY’A¢~†D¯´N‹áeŠ$2)Gj Í’Â’¾A!œV‰ËT®äsM–`d(&y3òñ.߈‰å¬p�±1ÅíÔÊÅ¿DX;!’°AÕ‘S% 6’+#$C¸¿éEƦ óH9I—bY'BoÆK3z$*P¤’æöaïùŒx…)0Í.®Bq‘¹Áv`m<�€ DqØ”ž ìê6ÍÊçÉiçfW-2L6Æ`j³à:ãlu¦™�VF‹S¤ŠŽD@â.¾77¶DžáOàIP„Ø ~+êÄ-xA%²5™ãL¥S|¬ÏÙ*Æ… ðÉ>Ö‰r5 .æk¨d/]€­²µÓ£­y´÷.%;¹s ˜¡LxÀO¨s²h»¹é8dáˆ×Àšh®F7)“€ªß5`Ú»úÖHv¨‚¯Ü^Ú·j–Ĩ¦¸Ìõ ;8 µÑŒ®xqƒ+B´,TVì·çã¿lVJ�n᦬ªÖÑ2Dy#^ +í-¾4P ;ŠR/0ß+…³ºuúJ�9dI%b`VPqf/¤Há>jŠËåà¨Çµ‚Ëá™ë1òÖZŒHViABI�x)·ðÎ;eõ‰¬ŽˆãŸ½›¾G§§“Kh”šÛ¾îyh(i1©©çÚ›Íúpd¦8„„QfÉìÕ»Ú:‘ ÐHê‘H­¨{$Ñ8¡U2)8ÆœI2x2$™Ž_çJ§\¸Æj�Øþn \_Þ¸aF¥yØm ±›Ñ£r‘ÛQ ª4̨(Œî^Kiôïº%Àì•Îg7d®³+2ãŘø ?²ô'óMNë%×ëf£é­Y™3Ð[´$³ sˆÖ¾mL‹: ùP1í6hÚ=ͼf>šÜMH �·ÂHŒéá‚“�ïd»‰â:ËI“@µ6#šZ[§'­õŽ'¿7a� w¡\X�žÃ¥€Î¼@'%5PŒ ÏM„‘· \Ò�»ÂÙ 55l°’iq ³˜ßÈâµú¬à“XÅ«�BRX!¥ éÊd¹ñ5Äs½gÌŠ:QÄÈ*Éœl‡7Ó'ìE&%Te#Q�&EoDp(‹°ºŠj(×¢k €(³-„¼TJòàÁÈh†TZü¦�ô²™|ˆ2ò4ÜÈÈžäæˆ€ð$Ê<&ÔbNƒ¡5 Y²Á‰4ÅV¡¥!þ¼ÕĦ¥»Ãë’ o Nì214M½¡í’\9û/CŠ9l qòXžÅóÿ*ë”Bù£©Ñ‰áÅ2m-Q&s�søÅÔ %l9J# gc8; ­L–êQRç¥ À]Q,j& š$Oá~[‹:äÄ+¸—’NÒõòz‚¤iÊ$‡±Êûزò’¨G¢�‹ÅA’ùšTsæP|„�`¹áˆzSi9m×ÔÍ)»K¨õyaÎræ~5Åi Ü´¸OFã¶§¹xÙ` Ð+Ù1Å»hÚª¾Æ +isíï&+íÕ{Ó"?ČڶÌ(1hia¬ÐPÄÏ–9†&FŸ¹bŠ Œ›â„»Éýê=cîYí¢^-V-47Ôo0ušP_¸Eõ§íÒ°¯¦Ö!·—œ®Ûœ.ºÚ:½½š÷Hj³oëT.�>è‹LEzøÐ%Õøø`E¸y˜Ø~ktQö”eŽ"-]–lš´NY™½À2ËpqsUåþ²¢ Ü"—ÞHâ7Ëôt^ÇŽ‰UaPÉâ,ñâ h:2j²`ÊàH7Û�G2iÕ0¬ce�¨á->ÅáT€JØæ¡«Fü¾Ö‰' vôõÓOÌÉ^�'ˆƒ¢g´Ð¤¡ˆ1µÓûoB¡LödÑg+ú�„A™¼¡ž�ÉL÷6Âl tƒD‹ÿÌŠé˜Tó#ÏÆˆ»UD¡²í´Šÿ8ê›VXò1^›7,Ù´ÄÄVoÔ&SPZ?…%¢Ú^)2ž\xŸA–¬Â\x8Ê×.z°:í@4;®øXž¸…­ý¢|H¶Šuz�N„6]o‰b¥/ cø˜„<â±ÚmgEΉRа)U¬‡Zº�i XÔ‘ S‰:òª[PN=Þµ‘.µPà�¢bôPKLSUŠ”{j¾,ÒŽ‰yA,O!¤mV\'û=‘u�ÄÒv:‡êɉnzïèt$ð�™¬fl'<‡'ã‹ùƒ!Ƥ™”Îw™±’¥¿Z¯”€Á¥Š£§D‹™‚îäÜžùÚ.Äüá QaaO ‘œ£ûh ¬[tC'æåG+DËkèŒÃþÅ Jg´kbda웓¦·~ h±ÖÔÒ'w4m5뇊ÊйEÄU Ñô±×ÏKSÜŸÔšÞŽMŽŒD“¨Nò§n±=÷WP§>™( ÁÖöC`TV„½¡­Ó“Ù¬GÈõMX§è �':œzCˆC W{U\‡) EÃYY¶Ë©ª&(Ó‘2‰­´¡‰Qy a‡NŸÆ 9îº(Ó8ŽA\)-R¿ú@Q¤^Z¤Óù ZÂp 9$º’Nƒ¨µ’�çš“%R$‡ZW«ŽDÆß‰7¨ TaJ¾t«âð’…ùñ×.#…ràÅZP;Ç=é8ÏØ'�WbÜ�‘%‰$•?ä"= \O¤©‚i-ìd3pL<ˆ\âNZ|ZR– k¤ª•ÒIU<ÛÚUR¹jI•2]Â`¿ )WýõÈ&É"bIRwYõÊ�Ôì&ƒÈ~t¢÷°¤$z)OÐ)zÐâ“>Våm(‘âJ2a�"µŽ¬rõën¿âÞµ‚¥Öv{«eò¨ôüÍ9‰ª€á� q¨×‡RdJŽ’òÏe()qé™RbÅ?gD/ÓÓ#1óò‰°h[f�X„|=ÄiöH�E´VšØz%`³Ñd›ìù2c(zW–;ãÐÛÊN6¹mīۢØ6xñX™¶ë奩íW1ßpšá¯5Ydº ǘߩ�[†ç~ ìô'Lâå¾&’Aìò‘ÝÖé kÃãÅîMY§t¿©�ÏÇ%'Ô3ÌZVÌ…“:ü£W½Ã·®³Õ1Éj_Q¼âHͲ ]¨KÅå«N1{1=\OC‹i�ü;öô"\¸`ŒßŠLÓ9Îwì·;£hòˆ ³£vKˆÅ·ˆsPZE'baŽãѤʤ†ø‹‘Û0³ß×”JPô¦cÐbøz�u"´¨0ê—-Ä…ƒbñGj|y{l¢Ù•?x&c“:øÐ}P4†Ry¸ŠÍÈÉ,dE‘Xk´‚ò b‰ß–<¹ËšZž|¤�,lêâc%ÚdA뉶‰ä¤Ü8¥ 5i%xÀ•ÜZw‡R¹¢ñ.OÅRí£c"ö¤#-íÌÃ<0Nshç`}?°ÜÜŒ»¾á/|@3»€¶Î:ÃA€œò.}Â÷a�^¥¡i‹ÙnÐÁG�_¨Ë‘˜§ÓR0 Ç‘‡Õwƒ:¥J¯²q�²$£9­·0õšÃ- –Ó-Ý—0¬J˶ˆlíÇ 7×QΖXµ6 jQÐBÒd®L(°ë2VÜ÷�&\q�={$ÒGí]'z$§9G1ôjºÆc(ƒÕê9¨Hà¸tàYi³ôº©ôq dH”báµê¨3lo'y¨Ñ”Q8ô(ƒÐ7Líéo^öÑX¯ÒËèk1¹´´8eËdf¯t‚sjÓX0PQÓpÙg½ìãxJ@{]‹S»žöuf±Ýlc Rô—Z:–0 M4»mìêdzRo®b‚G/Ħ¸îHD9ø„Rš›Cµâ,cô�"§7n=52™—íiz§gþÛ:}ãv‰[­ùþ­Óaî,`¶<ÄÒ«ƒj@ØSéšÎOÑ-*$‚„')\a©ªPN¢Ù¨ÔBÈ,ýZ"œE_•‰ ˆ£MÌ!Ù%b#ZôÏä“Ñ�ÌnãÞHÊÉ>t¢<½"·Õ±§fY°2νûX'Ê8Õñ)Îhd«)NT?èŽ5ç‘×”Ã{D¹§ âcÉÖÈ„& c‘²aB;jy8¥Rš&l2G `Ä–ˆŠ%صԀL AKÚVÏZºQnD' ZÈ  7} ÉàT$VšID“šÈ´)õÄ6‡Å˜Ú:hOúÈ&Àœõ³Bu�*éi“ôu­S¨½R'BPlR`üº6šeØX†Áb=,ÉÕÐe‹KÒ&aÕo`Žžk�’�øÔ‚ZC(B Ód„$fª!¶ºŠ& «§ˆ”ëÓÕ7‡Ü@’ ØEÞ«­ÉPUë‘Ø#Ñžp’Gâ„Û8•ÏQÎóþЯ3¼˜ŒÐ5Ì;¼€„¡ë»PxnJ òogÇB-�«%  =ó­ú�‘Á ÍRI}[„j‡#ó¯·¦œB˜¸é`(]&åîQW˲Ï×  g6>§�âÜU¦Ÿ%PO· ¶‘µëij�ãM;Òç蹆&80Ü“ÝZ,&#=Ùbi–˼ÂÔ2žyÓÙ5…7Ô5/™h|qm·ê�5É>gýB«ˆžÞ`$Æü×’Çîs’Á=Ê» ÷!éšT—!¬‰)êäZ§ãñèÌéÓ¿å¾÷¼ã­?pÿÝwž¡Wž¿tù[Ïýñ—Ÿzæ[Ï¿py{;›:�Pñ߆lßœu 6€ã™Ó_HEÛ Ö+` a¸„©c´ ù`Ö IÒÑèÓÛÌäø aëç´fV~ð×·¦ÀÓ^Ç!@*¸…4Ô_†xÏ›…œ„°3òæh¼…gyˤ¡ Š�¹�¢Ó|%�(M²îiV¸åÕô*>ÂdËâOí¹¿u"$§ºÍ� Éfd†ø][úv ªbxªmÁ — Äè`Ç:9¸j°ê9¨24ô›âpKWð.ª«§™"#?ÁÚs(“ÀkqS”.éøXþwyƒ=Nw®=xdU,”ê*j’NòÖÔÈ’èä±} ÀTç´7vlõ!ž—¤èñšXä-›S}ï äs?©W¶ábfïÐ�ðØœ’6Ëzheë4ä¶t¢2‚¤tðÀ“$â´Îî±§”�3¬âóOI)Î>–õé²û6(%R‡Ãñ5 L–MVÉÇBr(eX6¡õ¥gÓ� )"Ë<Ïv'öpPiWƒ´¸.nÂmsœ¸Ý� ’±ã¸Z_VÇé‘F ¨75ë‘Ø#ñv‰3Ž\f€ÌlŒ+‡Ÿ°|ìüDçÞDƃ_™I×GKà  {èKÞÁÙ�ù—®âý¥ uâö@qxNH)GúŸ³s-�5K€¾G/Ã<Ò2RýÓË0,0A0(K`˜öPM9R©ã“ ;äšÛbÝä4ÙPWZuÏ*.曨´aŽIû²¯MœÄ•¨¿�V¥ ÊÄ¿ÑHÌRPCœÑê%·öc-ï]ú!I fàcb�¢#X9OXýqûyæ«È`}’×8áÔ±7)9/±0�¯ÌáHZõ9$áu1×JভӠ660M®q@‹jç¢uüB yæŠ#*‰©MTˆä�´AÒ52ñyŠ’<ã`` ăÂdVQ7¢ÐX'5 ÒÀJIϤS˳Á„Na+ëD Äm‹øIMÒO5�¨J8M�BU.­f±’ 9$3ѯX'ÚÛCÂþMS×hbò³4@&@*+1¹´Ë r]-aˆPN1ނŘ&üÓv“hP~õj³’""LÙÀ®‹%È�œ˜ÐT| i°@ $Æ·\J̈¤ILˆÙHI 6Mš4­œÈÿ—Ò¤)’¶�‰’e»B’€G8&‰\¬`�^£1T)FW>šK¶K6¡”Ž Ò‚|J0œ måoU…'Ð+P_ƒ*�¢Rìòa mbV«a†ª…ñ©ånn£q^y$$qyÐ[/7¶�M°¾Ïub�DŽìª59KFö6”åJt�De4t½ã7Ù’@„ÞVâ!÷ .PêŒ1ç zjœ¢IåÃòÞæ!] å4qµ®–À·ÉÑ»”w?¡?™ÆíFü²‰]+Ã[]êî`0#�|i”w9ƒ_M¬Ï[k�€¯œrSËY¬#û¡Ÿ¨)¬©)¯áõªJkº±E0Ý2·øÖPt“88 p{{ws‡©d4=�é¨i§µ§ù›†f=¡í‹²"ÐwÐÑä$ÁBÕRíãˆ%0˜ˆ¯9Ý¡�ÖãF:B_,ö6>ޏá¯xwâu†ŒÛ£·NYgò¢¿Ó§·î>s†>{þâå)Ž( ,fŒ—¬™1jê(k†,[“É=wžáäÂå+ÛÛ¨Óž»¤WJà&¬S=¶- \qÚ›æÓ5GH¶ÌIAvbæzƒ5$Ö <Á—ná݃jÒKH_°1z+Zdç³;#NÐHP¥P8—Óé&¢'Kwƹ†3~C”£û¯4²ÇûŽ’vÈ<žžZQ'Ú#- µ dH0l²—f9ö·N¤¦NÕÊMqé/ŵ.OÔ¥ 1“ŒÝ>E…$|_«ªûÔb¤s“÷â ²‘@ØîJ§%§™ Q@:§l�ØØÜ¢DéøiÊR£~2ä’ÄG½|aöæ4®ZBMîÆ‡T±m œEÛᨦ'µ'{Zs&¹ì·=Åf%usƒs®OOR"¸�«`(ÚÇ­Õp,K ÜÃTíV¶¦õàÑ3“ÚØ~d”†½Žuú*�ÁRxp‘Ýêd6ut77Y´â�·”Œ] ÉØuéÌS$dzd”YÌ>Šè’\"òêéØOÅ]ž„cNX­žA€X�é|^š]cËá�·œrJ­H7¡»:×ÁŸ’zýub�Äa î±±ì=�ÑHÉ g0dȯ%cß±'^â?^1h^f;øãô©­·Ü{Ï›î>ËŒýÜ‹çž{á¥ÛÌ�ÿŽÓ§~üCàQ…/?ùôã_ø Cëà…zÀ%ØIP¨/º L°©–ú–¾ãyîÑ8mÛÁÔ:™ß×Íw§y$äºT¹e}tÝȈ¸?òÁïyïc�÷¿óûô™/}w‚«ÿð;}+жÿßþÙ¯_ÙÞ9|ùj_ØÑ?žiÅPˆ1­•¬µex—‚3ËmSŠY†ÁÛ¸ÿÍ÷Ýóíç^xö…—ÔA•tÀtáüã?ò�w=ò`•ó¹/~õ‰ßÿÃCšÇ˜6´§UI~TP¥Î ³ÅµiÚ•†q;À¾�ÛÀÅïz¾Ïž½#ºòZjÛÛÛÌ4׆ÞÚõOýÙýñ�ÿ™—Î�û‡?ÿ ßúöóEì?ú«?ó±�|¸Î?ù+ÿßÿù‹Ÿ|ÝB~âÇþÌ_ùþB%û­Ï|î¿ÿ…ÿåu³¬7MÃHt奢Pe¼z$Ò~ø“蜯Vaɲ^600Š"v~I«P§oŸÚ*Ž6v¦³#¹LùàûàäñÏåkO?» Û'1�fG,+>~c­£ÝáL§pÁ{ðÖ)¢ÞÚšÜyæô½wÝùæ7ÝMŸ}þÅs/])+‚nYøÉÜšœÏd|z‹ßÑ]gN?xÿ›8yáÜ…/\ºxùÊt}æjÛ†1²3�ÞDûÒuâ#|ô¡·TÞßüì±B©ÂM�ºQŠøÀ{Þõ½ï~'ÏÞþê§?îÂ¥¥<¤p»Ì>­Sz`ðÀ˜ÄTH?]ëò� \#FF^å£É‹žIr‚3«¸Oþq `µ;õ‹¦‰u�ÛÔôg¹³Ÿ'«M©EšP ¶tz=t ôæ©eË–ð•ñÅA–÷z:qi ƒÖuð&Y©FÃÂIxÉ\q$J@:¢çÖ1QÁ¡n0ÉÚS�¤¤ú6v ý <7…¬X¸Žô’��ûcj?~™T¨‡\uD|Eò©•¦q‘¶éfD¯Ê–eqTÅC0qKšC9؈ Ω((Éyº4|RC%0p'CÕYRñT—éS´ºÍÅ&‘+êDs›uÐ�2⿲H ,Ã�¼è×2:©ÄEa‘0hèX'0F§]Ÿ¦[‡áH™Ñ•å^‘� ¨ÍƒŠCžnZs²2Yý…4„‹YªIZ™‰¡m¨r7 I(�ók׉$è‘Ø#ñ¶‰Ý˜9�‚æn'àÜGå2!8óÉ;p^Œ†\8DõJ=°ƒ‚~üÃx÷;9[‹ö« Â>þƒ'ŸþWŸý"w¯ >‘§HþoþÌOœÚÒúÛƒ,ÃïüåO=‘éçDV§˜ÎM÷qpu6§ë0Ÿ:1; §«ÑÛ˜Ÿ�ñ™s½?SÇ:k�lúã?òØ#݈( èKßzþÅÏå�¾øO�h±;„çóGºŸUAÕ÷¿ùÓŸÿÒÈ»m6Ñp5ŸÍèØ{¥Ÿ==¹|ù «“½�C8±“añžfL žoš‘ZX$Þ E�ãñæö WXH“žZ`k>�Éÿ{>ü�÷í­Þ«Öð_ýÆ3ÿì7>[6ÜšK=0rÕñy྽Ž÷èC÷}î÷:«|ÇÓ6m�Ç[N/(1®c°kZªÂ¼™«ÖŠã”ü¨·òÞ9S¬óø¿ÿñÏ߈"¢Ïýé§ÿÑÿø�¿ð»_ºQ²ÕÂù/þôò×~¶ÿðþÉ¿úÿÜù éÝ?ð}ßóè#o«ðþÀ÷ÿ“ÿçŸÒ¯A“ùâßóþ½,z6û‡?ÿ?¿v–× vsQøðA`lßh$‚•B\w#­þ ãÚ*ë9NMÆûgÿü­'Ÿ~öÿ¿ùý–¦Dø©�þà^šõø>ý…/ï]î‘:¸xxà¾{~ö«"ÞñÖÿ�_þͯ}óÙÃäáàjw eMüN€7ãósTÖ)²¥‚»½õ-÷³³ÛCo~]ðž»Î>÷¹s.‚²±2õqRt¯”�é0\ï¾ë,÷¿é®·?ü–­Éø¹γûÛ3Ï¿p~~é}ßè?÷—ÿwm÷Äõ?ýâ¯>ûÇ/í]®r‘ù|þØ[¼çî;+ýÃo¾÷K_û·+WɾJŠøáïûî�ýÐ÷WâÇ}ø¿û_ÿéDÝq3Ö©f«&’¬«dX(¡`ÆÀXBl!Ê5׳Ú]¸„b‚O`ߎØ=+{gå-›dÑn>¾÷n¸0Ä,E$ÃSiݾÐbè¶2£õ« J¡°ÎXf "c¨9t"$„WÕ‰�ÀC\pŒ]„ï§XQh@OEz3ëDˆZ­‚Öö>$‚‰ãXÍXT®�Ë¢’÷þ±A™¢Räû 8çÖ™Üài%Ö£—; Oa"L6›ƒ§ø{™¨a¦¿!‡^sSåÕãDÈió”ÂÊ`¦¡›…dÊ[­Ä ¶ (v¸$IÂ'y�q¼älJn#A—œzˆAtuó$)meúLf)›%"ÔiŒGÚjÖé5:QâQ(q’¡h,[z-7‘‚´sPµ’õ°Û‰%+r>îqÌ`ÈtÄä%;YI;jÉé±]›ýo>„Í™]EB%£§(º›¶�ûbÚyÚ’%ÏH� “kªŒ«ƒ}ƒu¢²Ú#ÑÞ¼Ä&]¤Gâq‰Þ¼u¾väd®gàdʼn´æ§’LnC§žö ëúè¾ã­üÅûPR¯.€Û}ïûoçó•'¿ùÿþÚ§_�ऄ0ƒ•\]Í÷¼ó‘Oþúã“ÉVé�“R‘kùŒõà©)„œg¼ÒËP™1˜hé=hRô¹ŠI•—ÙߟuПÍf 7®½á��y³˜Ï‡>ð¾ÿë—ÿ57¥o˜úxGPßíí+³éËeÓéÎÎööé;|Ôå¨x§‡_¹|‰¦ÞcáÊ%Vsœ“%Í&4•÷ŒÑ@ôËØnv¹ºå��Ø<úLRŽØ}¤H¿\¯øAüµÿã÷.—:W‰{ÿco÷Ûßö/?õÛ_øê‰‚‡Ž7{Ùç‚^7ÝÙ>}úŽ—[ýêz®ûܧèN˜!ZÊN"6oÚ™ßZÑ ¼0‹mÓ˜âaam-‹^»N À³gϼ÷Ýßõ_ýÿù³Ï=÷ŸþÝð‹û[$ïѧg~â'þìÞ%{Hý{ÿ·~éŸÿÊ|ΣoØ­Ã1ŸsOA§ ŽeØ+~ᙳ«¦G(ÏgÓÍ-6‘¾~–Wä_ÓÅ^½ÆHtÍ GØý¤ò5rë• ô ·ÙŸ ò«Bï|Ûƒø×¿tþÒu…�©aþ£?ô}{±„|êsŸM¶X«ì®I67$CK}ð½ïº:úßÿ'¾úõgÆ®äoËÃ&?Zë”æå9SüÝÀÝyðþû½¿µ‹CýéS§^8sšÉf2ÞäÞÉ…KW„áxíËx|ï=w�Ûšp?õ¡ûï%K�éèòöj¸~û¬êMô:x¸wƒÌG?ø¾òËŸÚj�í+—1’ö¸Ø¹r³a4ºc-�úÜxû�«ú*@$ï£ø�?xŠñ²WèaŸ8]Ç XÝ:�i*’CÆR/(#bÝäU¤© ó¿tW~sÇG�änàDbá!+8È’à«0$Fö0ŠŒ²¥" ™ùo)¬p"3•œ¯ô›:QtEh5�V…,�µ�´ù³¬b÷¿N¤BŠCo&hóx,,ÏjlÝö,fSÐÈ–+JÖƒ *ë:‚t‚‰‚cØmDÁŸRŠß`Ê´dýÖ„SC6-9…Ú:ðdꀄû–Á�ÒHKúd‘—]¸à%mX3oP6âmde$³nW’BÜó¯Øª|“‹œÖ¥$i››ç†í9eXß0zCëÔ†‘æU:Q1/ïK…*I$3mgÍÃE¤’]?V§NÍìuQØ‹— hAIaZÏ*˜„æŒ$› RD`Ò¡í�ªEð†§½õíSÀÆ[lx[m�¨ù¤n‘”¬x$T¶z$Fþ×"6vT¤Þ#‘Nh—³«äŸ^xD#q2ó%1–ÏPñ-T@ˆ£›i M@´,’bê€Ó&jýÇOÿè¿ç]�®B÷»ßùÈÏý¥?÷ ¿ô«—¯¼Œ8¬’ñ˜¤ƒ¸xáÂÕÌ Òí+WÆwr—ÈñsB�̳̬t%üñé7™hÙ[!·òìIînAçâî…wµírÜT׬ç@’Ø‘ +’»ÿž»þúÏü$üþÿ�þ$ ŸúΦ¯\~Ϙž9º®0åU±jLðAÛYï…ÃëáhúÑŒÏã9öQúœ=Î^kù·[nžf ô9Sç9s�ô*¨¦ø¿•o–dé'?z]Üm�,ÎÿÐ~ç+_;ZwÅ=~^÷¤šøjÜ'ýnzêÔéCi`Š&d‹ûÓîÖŒ†¤ù´¨mYYð]À†«ËÞ{jY\ùÿ·÷Þ;péQ@¬±Ä¨©¦¨ÑM11ÉnÜdÓ6½ìºÙM~nŒ-ÖØ+Š¥w”Þ{‘Þáööÿ|ϼïsŸÛབྷ«¸ùóðòÞç�ræÌ™3gΜ93ƒ/#?ÄJÒiëÃ>ËãéÁædgÿé÷¿üÂ]÷œ>Y«±�ZÖ²¦eÑ¿à+|4‰õr18kÕ!Ê é^p“y�úfÝŽMœnÁ´í£ztì§)ßj—6z¢T¥‘¾ÁìUê:z‰SûÏOˆÄÂÀko¿s÷m·x�Æ^Зõ¼È¨VÌX �“ž‚=ÅK¼qËö#G¥¥gÅÄ6z±Æ 8°¾RQÑÄß9Ï:KD,¶7Ú~%Ÿ˜ðÉr'2Íá\i§˜Ò8ß ‹þn|#¥iÜÙ°¥§$TaG«¯/«¬â47œÌqy#=)ùމ‰Ž‹ŽÆ‡5…ŒXÄ�Ÿ(¯¨¨ª•m\\ß�0zHŸf»æaKo— Øñ’|Ž‚lTWSÓ€èîŒÑøÕU•u5M–?+++ÀuJAÄÛñ·#Ú©D“âU œˆ’ ’Rƒ‹ Ó–<ÌúÉ-–,É,©¹Ê¡ª"±ÌÈòL’×·o›Þ+8:ìKf%ôiF1@ŠDYæ^QÁDE– Jµ "Ú•îÓ—If66Á.¢Ä¦ÕŽ1Üd3Úø¨©–WñQSuµ©{kÏ<\„8û ä¹á\¤�QìZ”ner�Bu÷%«ï”, AŠR ðCÜ“ r©zd'ÕЂÀ—lIt–`€Bª½2ðÑŒ‚i…æµ\&J÷cÐP³¦ÕR,.@ ÑÔCå©yx©y@@m¢ µ¢Ün•!B÷…’íòR…¶ÈA”: �BlÁ¶NAª·�=^¬,×VÆ ´µ°ÕˆMù*ÜjÉM£ÒMøu&íÔ* ÚÁ1Q0Æ·‹Ò!Ž1ΗðZ-š …6ÀY%E*¬JC>#¡~¨I,`Ì!½)\½W„V­l»)¿t¥,…‹z‹0FC©ÏÚu ¡½0âp]5šØÇ¶•â�‡°Q^R`RŘ'ª/„8O¤Èó=‘æQ3Ÿï‰ÿ·{¢d ø_]E¯êOü¦³ÒÀt iÇê£&Ï$øHÖ)ºS$ý„‹µ´»±H¸gï¾í;v±m§Y�I‰ñ_ºî2DX³ð�ÿOÉÞÚºõ›¶9zÌÃöíwfa/a¸ðBþ/¾ØÐcâWí‚Ôµõ> p jã0c FZú|¤D-()ÞI™¥ú-„,_µ&ðY½vï¾ýLXš•vသ—_4Ø!Ó,êãÿS&s_GÐOŸV}Nð§Q›mŸôÚú£ÄGj€œäùÖ"&Œ¨�ä™ÐÞõWJjÒ]MʊزSÚb@wÜ+Ó=h0粕«§Í˜µbõZ?+¾5c&ãN,Ú+ñÃy1¾³1ÄÁW7kõ§` j ’Z¨o¾‚r“†ÕLÂ~j`“êgJ.YlŽ¡ï,¤`&l_~hÛ·ïxõ�I|^ó­ÅK–>rÄ›�•õåÏÝÚ‘öµ�M>ò˜íTYÙk“ÞÂô½›ÇŽ"^–/R±ý8è]ÿ;O·,³•�vôDMâÔ’�Øvôø†ÅËVVV6®ÐôìZ„qÓOkì›c‡õ÷~òòì«o"(xü�ê;ˆ¡!¼1m¦7´òò¤©�VqþP‘ù€ûã �šÄ€È’nÖß±¬aecœàÜØ£'Ë÷:¶{ÿá]ûñÍFÔªjÓ�`Ûi|\LàÀYº—ɸ®Ã¤£p´¨’‚æÇhÀ„Ãú–6Dg(ÅjâgUÌäÀ2† dØò•ÉÓ½ä(ðs½×lñÀ‹ýh^:¦�Š¥½š® ‘Âõdó6Ë�4ÚÇb¤n(“ 8/kusõ!×(™:d±j“Ö())–MÁ&�í·`i “¸T€±n4ƒ¯ìTRÆD�“�ÊÐÒMÚš¦¤QÖZU«óDHÄ~\mÉÕ ¥ÚZ•l¤¡ú¬Í1%H­Ö¢ L9"Š¢UCûQB9DÀ8*òÛÆ@5 %j–*š78$0µ€¤LJ Ø+ýl^T¥ð°{~ðó+Ç�ÊËÉž³pñ®}‡‹ºö -Îì9Ï+óE}¤Vu"êk£†4³ˆÍà*Þà£(뤂`�Ñaµ,óµq%BkD9¾­òß~ñ;c¸Y씓�õÛŸþ /7pB<É/è×ýýµ›Ð¤’Íácþêô IýüX<Íðhöó£@‘sKjc¢£9W‚ÒŘ’kê}Ž!¥EHo0‰fú‚^j”ªSðƒÙ0÷ïQâA«¨¬üâÝßfίÅF±¡þ‹ŸùÔuW_qàÀ¡·¦Í((îª ›ÿgëÓAl›ü~H‘!u¬Á2—Á‡2ê[<¢åð:ió\¨&ÅX“éëDÙz¬äŽ”óÎxÔzM@Íž3÷W¿ÿütŠ!þ«w|ù_¾û¯0ÎeÃ+­]»Ã,¯ôæI“§lÞ´ù“7^ðà¡Çžx**6Žm§6}ðÀ› ¦IAÛuTŸÆ,zsé›6MÒ¹¿êLñvھʇb-{"*½ŽN²(ê/ xɼ ::&!!qöüEW\6ÖU�Fأ˺ìß„cº0^EÝŠ§éÁ5;vï-ìRÊé^é`~ôø‰¯}ïG×NÇÜfò;³kãò‹»6âúôÆ<-¢ž,ÀIçL;ÅIídy÷*p|*;O££#¹-�›>8pôÀ‘c'N–•WéZIhO‚„8¼á’¹]¡ ;==%‰­¦5µJ¼ÿðñÃÇO²/€k%dÍEƒt½FËìîûÞÚ­”ÞjlËô­…trç“K—oÞ¼é²1£ØeÿÖ´Yyg�^k(·3¬#Ú)žAlD�²RÌ„ðêÁ c&&HøúÈ�Ñœ|�X$C‰`B¨ÑÔ@` "Þ›AÝ“™ÛŽMË0'e¹Žp˜I¦µðRV±—- žÄ%C!& Cr¬[Œ;¥Ì+¤1‘œ2¿È› §"„-ƒ*ÈD0”Ê.c\Méíž'š%D¢8bU¢`Ä´ÀŒ9¨?” ùLÚR®NlÃ~I:ŽÇ¡ ©UŽl5²pºRyl#t”±� ò/lN¾3“—òY½(RD,k>ˆ€Z�dUωòËÏWaª6…¹™–°àÐ̲ÉAÒz®äaŸ$^ú¤Û F!­‚ôXÑÕžä$ýB²áÌ€ÿ“ƒ/YCÒN[މX%™ Ló*ÍÚU �¡Pµ/m ÄÕ¢v.xÉ= ÒŠ„<)Ó1-cŒ &iø†ÆQ”Pg›—ä.h±¤V!–Z‡7Ô@]ïª ªÕ€¦f‹ã[Z·Ð� …GÔ2ID ã°!Ôyâùžh¼|¾'þôÄ(sM¦"ˆ>D€ \UK2Êuu uiõ3¾d[¥ùÍAZ¡�ô°¤|ë•cÔgƒÏô™³ÿç/�qXUJZ@ˆb¥—³Ÿýö¿.5â»ß¼;˜0¬_�®3­,¯ÆÁÚÀ‹ÿÐ_Lú�B.Š,‘Q±ñ ñ ‰oL�Áž%Þ3³scb>ÒÓvBÆ· i·@bAÃ[Æ:“^4¨�l,‘-~sanóÁHÚgÆvWÁLèËw}û¾»¾ü¹O\5ÑÕ >}Õ˜ž}«Õ=Gí Â?\Òv1y»褂ÁÔßZâ>z¥‰ ÛÁ�z—^¨�ú¡Ÿ¦9trè–�‘ê{wö¼ˆÈèô¬TØ FÄÍí¹WÞ|æÅ×ââ²ó Å{ÖE¼ô~ioC´7}‡ë”ŒÒ µ$EžF3Å^RFoZ…å uW­nƒc –8"¨“p0p~Xœ–”�¨™¥h3hÝ£�?u×·§¦ ¨ ŸF×’V{Z™I[[ØB¾zý†¿ø•‹‰‹‹×él²)¢û­ßÍëèÚ7ëÏ¡ô"S‹,Muîkh@ãQ.¸‚ƒ¾ý=Ñ¢læFÙ¯ñ¢3gzKNyyòô‰ãÇxm1rh¿5[w73aÐŒÃúõ� >SÞ��œšÆVS›~C;ô7ôNGÊèèèÄä”'O>ñ«´VBRrV~V4jƒ·añqÌdÓZÄ2-~δSíšbÓ\m÷™>rG7í$­�:U^�)mÏÁÃ�çÞRÿÒSe‘å•Õœ6ËV4$Sjrbuuí‘§=� 3õ�—Úç¤÷3xÕÖ ÐËkªEï-»hØîgjJrjb\Yµ;½­¾„Îo­f1P¼CÙ½ß_ÿ;²+%5=>1éÜjVЦ½Ú© E:¾njÐÀé±H$¸SC€_rJ·%Àܘ$´,µ¸^¶AËI5Ó1ÈŒ­K–#g¨ ŠGbPâXvÉG›‘IðIú <±úáÊåyHc`#<�BeÿSÕ U™iËÊ2»c,"ÙPiÏýˆî‡£z‹’j‰«½ºLg<ÌOú•¥%'yÀŽ?ñçGžJÏÊIËÌbÃÂÈæ0àuìÈáKW,]¾rø�A^úK†ö›<ïýÛ®»ŒÅF+KWoâè.‰߃õùk/MŠ�wa¿<•ÃqyGž é]Ú·{÷ôq˜ ?+*«XŸœ69 “€ð¯|rB‚ì²zóÎYKWqßÙ…zd§§B¯‡�¦$îŠ*«¬|æÍÙÒ±|ÙQßG êåÄ÷¸Oš±à÷Þ�“¡mh úî‚÷×lk~¹;÷C•°QMk§Q,³°dº÷Àá·ç/s §À¼ã¦‰q11@ଯ·ç¼·kÿ!¯Lb?1îÂn¶=�ƒHž|c†_AŒ�Žþìµc]uÐzqJ3rypÚõ ]¦sœ?6 ÁQ,¦ÙÀ 2Ì©›cEâ®/~xOxxN~!“ÜLàcí¬­,/;väУO¿pñð¡YY�‹8““‡ôé¶bãÿD Æ;¼¿š!!ž#siÙ§*ö:òΕ^+£aþšK] Ê«ªþþæì*ßžVrÝvýxÎ^&Wé½>s±ŸÚŹ™W�e‹hó/O_pí¥�†Û°c÷ô+8YyüˆA9ið0*ûÉòÊÕ›vÌ_¾ÎäôăF éÛ½$�E{©á'ÇÓ=~jź­ë¶ïn ‡®Sp�‚03‡ÙK×lh#ý˜¡ý{v-à83* ¯?YöòÛ3Û øt‡¡ýºssÞ¬T er2}ÑJ2¶•«ápŸN²2݆ŽËIÅ�T3x: LB ¾¡VÛ`X2£¥`tÂdŽÑñûÑ”å'¦pü¶*^UUQvâxeE3ù”ô M¤› ®Ó ?Šd¼tø¶¶Ò œ1DÞ±·f/E y)Aé4¢lÇžý/N›ïpà{Ü…‹ruÿ2îŽ÷;ÁÝŽ~€d^r3Ó®»üL�°MÇxÕí4ïšÜ„W×s[ª&M¬í´^�Žã�äŸø³E$;«Eº ã7Ñ¡f6NSD¢"£"“’£ì<#èlr¦üÄÉSžé >£+Ñòú/ßúº+âàá#ßøÞ}RãƒÏ·î¾ã’‘#ܯ™sç?ððãlq=jä�¿ÿ]ÀR¯nÿ潦Ãó´ñ·Kqá7¾ú•Ò®]R’“É{êTÙâ÷—q@XÉò³2þùž¯“>)Qn>ìÛ³ÿœy þþÒkmeé@¸f02³éi³'rã�fWLÔ}i_ë¬(­y �ˆŒŒ‹�?±uÇÎî]»¸ÙéqÑlàVöP2ºÇˆÁ�lòöŒyy…]¢0}FDåd^4¨=Õ? 6;Y¶`ÅúM;Gs²\3v¸‹?ñìä9Ùc†õ+ÊÍ‚ÂÈÕ¿<7Ù•Xœ›5þ¢AiÉ t7rq”í‘'¯Ú¸u÷~p SrJÚOî¹ÝáFÈCÏ¿Eð°%<”ÑŠdÜ«ºïA—糫F]PR�ƒ@vEïÜðÍYKywÅyß§AÒKsö/®X´SÚê\i§:ÝÏj�CÊñ²òØã1É ñXß°»qcéaZº©Ý�Zãã|ª¬2{9íÿœ8U~¢¬œŽO ÁÔ %³]$¢r2PO¼\>úD¯îÝÒÓÒ\Èø‹¿>s‰_oñR’·O·"ŽÔÈLã¬:5nyEÕœ¥+¼Þ Qƒzvm䊩ó�ýWŒ‚j o?U¾n뮹ï¯%=gÚM9¤gI>ä'lèèq€ ü„!£cb‡ø©+¾LBÊ+«za²»äê1Ã\_è,õø¡<ÐNPòùa$©KE¤PÈ×Úôxq�ÒÄéMGG‡3ùp Æ·#¢ÿ ý ÕAojóá\¬ÊhDÞzÎuSëi™ÖÛŸ”ªÈÙ �@m-×;@èÊú× ÃRi@ÀR7 š€·±FvMª¢ ôúO°Ì]‘Ú*$H!h§MÇD B'±…x JRQØšÁNHPH a,!+Õ2뚣’ËI8ڼ謎®Y¸ÈY¸ÃÝ|CpØ T—BI hÈ"pêbÙ:s�£’µDF­!¢JÀ#¢Ãë£!‘°Ixóµ=O<ß]C@ÈöYlÎ÷DØêcÖñïÕT=PR€Wº�z±}ÔM%É¥#krÂú%ÂI¿:ã¡+¢©t-lÜôÔÿ~ð‘̬œÌܼ¤äT&¢š&K80�©‰�OdåÿÏ=ñÈÿüơзG—Wß™wøèñÂÜ,±,‹Wn`F&YcriÔÐ~/ÜO.™*++g‚}ëUc˜.ºp¾É’Ççö›&.]³yÆâ•’6‰JǬ`…^ЧÛ�C‡¯3Ì墜øøXÎõpÅ%ÄÇr®2עɎDbVlZ—2í;xˆ�œ¬L49$=-5<|¯{ç› ·^y‰ÿ*+,©I |¸bâ­9KÑ�äSQWGq.×ø{uºÃ�ò˜|sm¢‹ÎÐ>¥hð%„2zXVZŠ‹­;…X-†�\.¼ß XˆÑ€eVÂ]}1иõ´'œ¦%,TÅ€NÓ�ÒÚ‘.ìXt…ôø:'§àlȶ£ûÿ÷á_ÿ˜yoàéZ�óþº-¨Ž®Ý/¿xÐо=‚‘ú‹òŠ ËòN™ûÞš­»à΃‡ŽÄÅê¶;Ð"�wö{«ÝOxopßR�Ú½»%/Zq²¢ÊQ›Ĭ–T©Ù‡Èí¤¼\³^Ч;¦2ÿˆÀ„F_з{qî“oÌ ¥É(ú–+F%9 É…E�fš!ûK_™¾Ðo(ÌHN¼qâ(pPmƒ¬‹Açúñ#6ï,zå�…þr™a|þã½ÎEx•ƒ¥ïüôuÁÜMþ’þ«ŸºŠÝâ^(•‚|¾Z”÷ÞÚMï.ê´û.4¨Ã€ð˜é<ª9Õ—æå„œx'EKÂΞɿ äðpìè ÌVWwäØñüœlb`ßÞŸ¾ñêYK׊5±O†3é�²ÄÅûï8>£X¿-`6¥‚Ì‚®{!6z?¢H„pÃІ8BšQ{RÒÙO#Êàܲ˜#aó¿åŠÑ~äxö+-Êø0kÉJY¼êÛ{ø��ÞèÐ^mðô?­m¥Âº¡Jzºæ"®‘ÉJeÕ ’6&„¤:ò9=г‹EeÌBªH9gUËD=7ØôÀÞ¶};M>©©)iAW¸¸X™)Mxˆ3y/*,ðb‹ A�f¦§{3í¤¤Dl ëËÇŽþþ½ßôRò’œœtù¥cÚJÿ©ë¯¹ëË·yéIÆ ­=ºuåsÝÕWþð¿áÜÕ¶ò¶+œ>gk=S0ZöD†ÓÛÅPvµ�Ú²³˜3&&.)5íÅ×ß¾ï[�~ô— øÎ¢Uĺ‚h¬¤¸˜ŒÔÀXIવë8bâãj¯5tP¯.~”0yÓãn¼ìâ]û¾4}‹d@@È{"=!>;7=ù³×\ª¹ =èè6ô¸›&ŒìQÒ¸­•V`àƒ…nçÞ¯¼³ˆk5H†Æ+ÛPMEÀŠJA¡ŒV€Ö×'ÆÇ¸Q†ïOMY˜ƒ0€’+ºO×¢®ù9�¾2»‰+‘Œ\@z$½å(ɾ8Ä ês �‚v^&+£TZ"Ó$}óY%f¦c'X�ÀŽŸ-k‡N‹95êXd|,œ“ˆÂ@VTJ ÀŒ,üÛ±÷@ËŒ§ 1=a°—àÈÑ£ÜÆ;cÎü›®»ÆöîVR7}ìª^2^ÀÛÙàÞ¥^ â®{‘â^Zrů¿ŒqÜK†ä¿xPïÔÄøYï­¾ý“WøÇØõûî[®ziúüíÈLÌ“”ˆ” Ø ãããä¶dûW¼¾Ð)ê�‡Þ_:¢�ºæ Ÿœû:¯Ø \Ó› 0#‘! cŠ�1jHm2Á?$²ÕeÆ!7I)‘{’F('ÖÜ·A³í‡‚.` Wù6ô}Úù †�Ë®”ʧ�úùÇDjENǤ¨¬Rà�=8^¶{žè擪€ƒ„;ÈY§V1F<–9U{ÇÈIv:†M¡" � ØØkYùMr'l/@IDAT$ß‚'2DÔ¥’pP^*‚¿�GÖ)¥Ç&¦RîZ ™ÃÔ¸8�à c^‚ìZËbîQn•a×X°ZJµ‘qIÖ*{Ô¾z¨X£êhy•ˆwý#  –U‹PKN‚¸ù´SÒAÈà˜è€ð#¸¨Š†©ôNRPf5á+Š‹fÄè—¾M/Ц„Q5HA62 ˜(¦œz@Ëòè� �ŽÇ”/E*“ Æ‘G™B[sæ‰V ÀYz%nsžH±”O¹j¬ó=Ò†6&žï‰bBǹfKt.{"VŒõ¶0A7¬ç<7ú™‰³Mã$ÌÔ»ø¯žZ˼�×;ÕÙÎö^M5÷¯{p¸1jóö=9y©iqfa‚Ê3ûO“q[ÏÊa³Íö]xY˜èT•—½ðÆÛ^zsïn…Z°‡Ëls@�/Á‚¥Ë*Ê˨Ò?}új¿ÝÍKà^†÷ï1¸O7²£§r!ZPðptTÔµãš(.'NœX¿y«—½_÷âß�Íäï]’ïÙÝТرR~ò„Ö¯‚OuU¤�tihÈÏJGéñOzƒ©ô8×�Áþ‚ŠŠ²ùKÞ÷¢Xxgk›«5{dü7»“¦w׈`àeÄ,-j4w¾·ruµó!ãÁlï —âDTÉE ËDu¸5Ô†Õ×Dê ½°(·Z31š ‡åpPa��¤ho¡¡¤GíôxÄM˜pãSÓÒ3rrwï;ÌÅc�¬ŒT\BD¨úz¬ŸÍìn^2^hˆk.½phßî¨Úåe§¶ùxòBä µkû–ù3ìUBB4þt^ìäwçœ:qœã´½¿ÝÍ ä%/+s*Eø›½‹‘åË7^î·»5Kü¯ppøÆcêöO]ÑÌîæÏ¼‹“˼réw|r¢ßîæOÜò�ô7_1Úowk–fP¯n˜~<øÍbÛûí�†¬ «­‰¨®ç®SüصÀ™:SE ¢ Ƈˆüà »UzÃ{g=4Ñ®Ý{üÐFtß�·\ÙE9Ú/†Y8)%511ÉyK‘’òCiI ¤äÁïæÆËGúçBþâxǼ›” —Þ3‹²S§*ÊËó3So»n|["€#÷Á€þ‚bc¹ ­•'^m%Ûiƒ;uØ<)}QÚz$š1&}T˜IKÕN)ï,Y#œX‰×¢ûiŸm$’Ïö“2ŒFääd?ò¿â$è/÷ðä¸7îô!bm bGO"L« l^Vjiqn÷¢¼Òâ¼Ò¢|¾ÛÊØj¸5Y}iI¡ûîœ)陓¦Îò¨�nÃ=e®¥¼dÄ^uÉP¿ÝÍ‹jíEg#ø¹ÂowóÒã<û�[k¨éu—^YA‹1‘CCò€ð’›•¡�oV_LKÙ>sç›ÓgÇ%§7ÖÍŸ­ïZ��ü¶•#d­ 0p� Aú)˜6Lˆÿ4ð%1þÑ>p<†ÃQjF&NI Á�È©ÉI´N}l}·ÂÜ.�ÊʪªÙóîþ`o¯¥£/¾Ðcœ ß_³‘&~wöüî·}ÚU†ä /L|ü„ì˜ü•ëÛ­dæb|⪠³3<>¯ªªÚ¼u+[ýo2ÂB›¶n‡Ã‡ X”ߨµ_2¬ßâÕ¤3´ñ �Ü4q¤?rÍú�ËW®†ÉÇŒÁE. ‹ ÷ O[ (»÷îÇñÎÙ\(wÅšu7mÁQ«Oïž^•'\öÞµŸƒißáýzx-‚ `ËsJjZÙ©“»÷î+.ÈwhŒÒoõæ]pµKI²¸¨È=»zH*ã;3Ñ{tëzõ„ËXBö¢x¡–4q3® :÷ºŒ1,)10Ðã×kéè8ÇÀ� á@¡9ô/-Z³eW PºŸ·!¶¾Y_ppN£Þtæ�AaíÖN=�kC«=KxGLKoàED—;3‚— M„ºqȽB_Ù‰3ùÿ(£’“Rv4( D9Eøö°J‰)C¹I�Äü¦Ñd»1ñhÍífä$ HÕöŒ‰ÔH 0)Kõ2´…�…¨><íê‰Ê#:�q¹&š0Øi©R4)Шg&>›Yý¨¢#‡ (»RRF$£¼p†°€¢ þðŽ1ðn䔥GÅ!Ú4 ±®:„)ÞÈ­F”„1=€ÓØ4F@Ô2H‰¢‘CËý¥ÃÉž¥UÆ,%pIU$?0dòÍW X5âÑOþÈ­=í´Å˜¨’„²Ê¢X*°ØŽÛ"‡Âø#kÅAä D~ ¡À£¼À��¬*ˆ�í7©”ØryïÁ_•$k%ô6(ÆÛ®p+ŸR WRñº�Ü'S¯±²j¡Ž¤:1—�Vdð/Q†žQXiÒr¨ Àùž(¶ò,6F@èt¾'JÒˆIÎ}OŒbw¼¤Œ,%ðº}ÛU=HKš®>¬†®ªó aîZ:…v´ƒ9>»§™í«k„üP â e×˱ÿÐaæDü àðbô‚ŒÖ.Ñ„„}ûú#²ÒÓ¶ÅÇ¿:yú×¾òyžŸ�ʼn<Š‘Šõõc†5.\oÙ¾³¦¶>#=¥Oi‰äÅ×Þ|êùW°&Ð�úëûrß½î89ìYôî:oùz‰3/µ½lÛ¹ëç¿ý/¦¸i©i,; ð`7ä<—Š'ÍZЧ•Ä`xøx¹Ÿõ-¶«`yñCDøŠæÒ±º{»PÉòÏ?úùÎÝ{#¢¢öî?0Ñ’gú3;†ÏLKÁçåÈþ4`φ2¤Oé´E+{ù÷È�:ôéZ¸mßaš±´(—ŸÎ!b 71=Ë qáüŽi¨�ãfb…鎷›xK<‡w ñ¥O«L:Ää$q;XâYdƒÇpÃÛèð‘cž™€M%leÍöúñ�Г'OÝqÏ?c“¢•i£n¯½ñÇßüÜQ 7^~ñc/M^øþª;¿p«È\èÀ±2ª…y¥mÙ\̆WÖ”˜cé[êÕ`õúM±±ñ À~^Ã,õã_ÿaý†Í’uõÿtÇmŸ¸r‚ËBÇaê…уàÏA=»¸]øC�?ýúäi¶ï£á±§žûí¿ÿp@߀ExHßs—®áÀ5Ü÷~ÿç‡?sãÕ²ÿòè“§Êtß+|Ù½[×?þög~¦\Pdë¢õG^àÊ1R_úúwØSMÞ�5÷�·§ýþßäÅòJÜûæ… åÿø—  hþe+×PJ<ó¶´ îìóSÀKß�T"­SKزGW”ôÕð�lÐ>†°h¨*ß7XÕÔˆ…e#WJk% ¥1åÜí}ò›w~©'��­:Lh/è[:iæâM;÷’* ë[¢HŒ�æÞ¼ƒû=òôóãF�X¶bՓϾl�)üñ§_øòço¹ù†O8´°¶ctオז(ËHO�ŒM¸áÚ+¹ÎÏ«Ìß_|å™^c·�ôµ7§æ#ç.]™“W(m×÷0Ñüѯ~¿~ãVˆÜ*¯¶ÜåËݾWVYåeÀB¸&Å6õÑ4…‚­•fúJ#‘H`­„±,iôá=t�’ÂFç¯ ÚôËwÞ½|å*¦ ¤ û&x4¶‰ñ —‘¦îs6yšýlŒ#ý•—�KKk¼Üãwÿy?¦4�R a<øÐë/>×ËL¨� É¡w:tLdÐO�Irx¥©fNýWŽf¥ ÕJcMAS*˜zñp–RS$�5ž†äñd¸F7šb©\<î(âÊUPåÓf|²b•EÀDG¼©¥<…¶¬@VÇí‰b]69„‰(:›M-Žv�;*¥9ñ •°|r¾©\) &α|Ä [)›*š¶æ \CÓN­)§˜K¦sÑÔ\Fd�5f¿RØ7º¢"…‡ÇˆaDPeFI§jÊ¡Š‹J\©¼j ê¢-‰šœ³Ž�Ò*°¯ ”uÖrYJAHeì‹R¦²¡šàÜH¬Œ|Ö¸4ªÊ£'2Žõ´·.”y¢*)ìÏ÷Äó=ñÿ|OTR÷�´Ñ·zR\G=Rc }Ò:­EJø³O+8(ýY~èñl£¡[z“v–¹l$k82Å­ÔË »;ccã¼·‚eENÍ.Ô‹%ªT[[Ý;xê±/OšÂ¹_z–zXŠ|yÒÛ™9¹ùÅ] ŠJÒ3³{úy/–©b´˜å{°²Ýû¯?=Q^Á-Nèï¬@&§¦¿3{ž—„)—ÒÁ�í`àãžC‡�pª1nG L^b^]Ÿ‚Ø*ë…ïÁþqøW–tÍ-,âöÉÓgx±ÝŠòâ.]æ…”v)B]}-68¯D/ö‚~=´wI±Ý½À÷W®†Ì.,}ë4½•±r 6 /�¤½Lp¸$[ý$· gd´±Ó¤¨TÉ}$y襄œÒ«%/­W NC)MRfCG,ŸGŸy£/’•WÀ–ç½û/_%¥Ó=…¹ÙäçÐnÿ>hn5 =!øÐþ=‚ )�m•øÅ³ÜÝ·‹dÚ¬yl²ÆTãO¿äýåÛvìNÉÈÈÈÎ�Pµý±é)l9d£vMÈÊ6.i^(˼1-#+;¯ 37PÿùçEz{À¿KA¹è€;vðßÿî¿ÿò(êɳróÒ²²>؇7\àÄräf¥Ã¬Èa|ž}éÕèØx¦‘ÜÔ •¶îÜãÁ·$bðòŠÆM(pÝ·¾~gaqI^Q ¥à~ˆ³[R2ÜèN l½ÉÚjÊ6Â¥â ]J­0° K¿P—S bõªRP6ôQˆœåþg¿![›¾·òëßûÁΚì<5²è ƒ/;F ²åðØ.!P”ƒ¿*Û3"g/\rßOóìË“SR2²s�žoMŸéÁÄ�ÝjÀoÚ(a~QêÓȱl™zþõÉ©™™p~v~Azvöѧ¦ÏYÀ¥Q1NP{%„-yÅö�{p[>¯ž-Ui©}Ò¥éJO”ª(¥ÞZÕ€Á†#�Z”ÿšT05"ÏGþЭä¯ÿ}ÿàf¥½7&!Oƒ…cÈÓ$8}=í²K/ñÒœ:uêñ'ŸÑy—IÉ IIQ11ë6nôby�$õu˜ñÝ >{Ë-C‡ONNMHN!#*B4-­£W›IK?œö¼;ÑRO$©f48m­2:�–Ö´ ½ië6ùâü\3ìÒWjGíï…#ͧ̚ǘ›œÀåF�ö¸‡Ÿz–+¡ò‹Š Š»dæäÍZ¸Ôsbå²Jyðqðžù·�[v$¥¦ÆplœNúk"Ãð½o–öì @z\jfi�Šˆ‘ ^äa$RHþ7Ô…>Z±É<â籣ǎ¿9}frZº]o•¹c÷>¯ ”‘“™B6#Ñ¥Ÿ ɳm#éŸçT;õ*+ ƒ±mäŽý5l´� %€ø®Ârš©‡œ�A9-9>ÅwÙäwg%¥¤Ä3ô&$Î[²¶ 4IXØø :^"ÚE‘ÏÁ|ÕÚuC‡AŸ„KÙ‚„l÷2Ú‹¦4~®8~âäö]{Ò22QàUÜå¼ôܾ²vÓVØ>=+îDSõ¢8-´¦†€Ž¼`qj Ö¾¾²zÌ2ÝZR¸ÃÚ)rz¢“1‡Ö—Õˆ‘$`Æp–šXC•5ƒM,eB|ÃàD¶ã$˜IÓ+Oépó0Ù{ÀͺTK‰<ÔeÌX©Ä1´ Á>é.¨ÐNÐÚÎ1Q&n3ˆ™ÍIøSÅIÔªŒµ™'jªhmáD=d ´}ûš;–!ìÂ…¼HÃc±š»‘‹oÃû¥É~Ë ^Òè —¦®P—8C”0÷ƒj 9Ì¥ÁZ�d²iª‚HMK4£ Ð(^±4–Z ôWÁT¨ˆ•fy Ñ_•™( @*ÎÕ€7Ũ-…‹},“«˜ Ir‘¹ÃÚ©jf,Am&\�ƒ¡ ‚€UŽj¨®ºâB‘ÀŠ—‘MçY@ ÃQH0ýTµU#£…Í×Ez´0—Q—"à;fsØöèY w(_ˆ)?ì*S™õ…Šò*Ž�Ûµ‘bmWŒÈÚ<ÑŠ8ßÛg±9ß?n=1J{ØéO‘ZZ©ç>8:GsÑ70Ú£#X/‹TÇ¡—ki†ÃèQæºD2u¥³|^Õµu,|yþêl¨¤ªCž~x„çcïØ´mgÁ±y�»ÛF_t¡ ¼ ï+62Ñât·9Žp4†5·æbÌÊ \jI óÞ¿=p¿ú¿Ä.�¤ªþÚ“�™†ÚÉ8 Ðßg_yƒ³6Ò3³9qàhSLk'M›}ã5W §DG³ßd÷þ£Ì¤‡õø>yúL{pÝ«¯¯ à#1ZßP]]íwSbÿì“Ü/¾qˆáoàÛ#PœŸƒÕoêì×_=Ñ�!¯�68h\C»*/¯pç|w)̯®ª¹nÅ�™)3æÅÄq“æÄd›ŽþÕ½Nd —@7^ÙÒ—†?>ˆZ¦¿œýVÎfœãPøjBòŽoùTP‹Šhi-܆LjœÌÆ ¡8mq“lÿ¢F�ÚÂ÷Wç–$j�9f8qìÈÌy‹.̴ؕ‡5^ÌFs.îVRä°/-.¨®^D^ÈîBÞ[¾rèà�Ž=÷í¾bãvº�§R³³iã¶]”ÒlE�•í¬¼Bl7̹ä÷ðÁý¶ÑLªO\tôñS\Ôü¡\Ú:=x“Ñ,Ps„bZf6gù£ÍpºÐ¡{�?îÜŽk;1 ¤ffGFiTf¾{3_`�ã “Úº° 52!6N+Êm¤EÌXð^žîîLÅ„‹Äñ#‡ühÁÞ`¾ió6psD vÂØÑ—�µ{ßÁ…ËÖ,[»‘pÀ�OíÒ²V~p¡½Û‘n,R³7�šoÖ /-*ˆéU’4: ÔìXZV¢ð–\Z™ÍRaJ�ÆC�9-m÷ÍþQzjÒ­7ÝÀ&�L»ÝØŸúSGßÿø+5UU¡ ·f“Q\BRzfNl׆D'&¥ÄÄéHoÜ„b¬º" -“1Ž–w2Î+·‰(Ãß³¡ï /v鲕™Ùy&è€À„¼Æìwñ‰I±ññ•å�†T²œ‘WOUpÜžû¬^ä8«õSüŠåƒ€^‰�Œ´O juHÊ5ª 3ûX‹ÒüMWPÎ ƒÖ2°gφM›] ÛMsssJŠ‹·ó=~ì%))É÷ýôW­eí´0úW]}]~n®qñ{ïÇɽ*V4ÉPi®¯^¼x½~ãÆâ¢À·@<óèCG�›³`ácO?{èÐ!²¸;=}Ù:þŠ6�W½Rjþiz"B[…М4¥=¤ï Éà€Á6¸Ø'%ë²…~çë�ÂÃ.ÜoþŠõH‚ßÎ\vÞaCÓ`f »ýùÉ¿ÜËÌÁ&0` �¦†9Ì#ÈaDŸ?ýÊ5ëÊ«ës‹J“#ñù�ˆ¨­®ÆÌṕv)¹ÿg÷qeäòu›g/Ö…æxçÅÅ%´*”ðéàŽl>¥Ÿq´BW‘¸óòõpÖd�ü±±±ì9<~ìÈ�ÃG ƒÇwÄFE2<Õ×Ö…‚¤ô·&ÕõÓžWMóøÈ;Ÿíd¹-Á¡ì¬f Í¨Åø¯6ž¶:0µ<èQÃ#ÐruX#îrAÒÿž„E2ìŽ1ÜKÚ·°K·(,è± 0äÚ ›ö h˜ý{v›4‹{N±¥ÔsnË-^ÆÙ – HØ%Ú±RO;êä’—”¤…z¿Wäpì2KÝœMŒ ¼yª¬, u"=“²N�<¾uÇ®Âà¾Wî£@7`,hVÇVu°�rÈè€v*Ó�ê CCú–&EÕLP°U±òˆúÀaVšiî@Có!·r�Cä‘}Ez%�˜Y/.DÌ‘³085½bì"›œ—8xɰÇ¡� ž£ÀDš[çÀ�†Š²�F9”hq(ˆÚŪ˜Uà¾�ÆDpg $£l&l1tI1"Ô3™ˆ'Ú7O”XX±”€Iç€%y….f»á®Q;Å•ƒd8k*)]‹¿BE£·6è@W2:´Œª' xt©’ÔÚÊ‚úÇánuª½y6@F•«bqSJ| 4Óv!–š´ïÊx7E½X—sjª,0T{‰Â6îˆ,µíæ¡<á&j*û—œ4ü¦é@΂ll0&’¯¨a^iF(DkÍPµS&pšL©á&UˆŸòIê¤_Æ{²ÓŠþZMzjn.äA2ɨ&hàÀFQ@&9Á&4àÅF8d³›�V (‚:(5E¸Ò);R,ËÊS…ˆ7‹³Œ¬ � µkåc§‚j€Î4OT‘�‚ª†8ߡ™,6b‘ù|O¤|\z"S:½J¢K쯿ôz'¯�QCÏÀ@¬K¤„ÊÒ‰ÏÁ#G‹òº,çã„9/§qÿ&J!;SâS8!î…×§x¦7¦¯ ±Ñe•5£/h\¸ž½`1 †hYMç½þM¯ÍˆæÄO» À^QY““WÀæ8Ì6háxu2&vÃæ­}zvw)Gðô3¸Á»’ µiúÜE…ŨMQUU�ué'œ æ�IG Má_]÷#À;››o«kê1 ¸ëðHÏ-òk·ìÌÒgɲLŸ>uýµJ�™šÄSïÚ)ôA¿Ï/N—u¥3†,4�A�ÜZ-³� î‘„–¤5¡l‚Ti�¥Žò9‡�7çÖÿ±C—ä7ž…ÄáY¨˜©éòÉb͈ÝHýCGOxCöô”¤#Çæ,zÿ¶[>ÉO¢°©áHR”—ƒ¢îRNš2�; »v)æ'ᧆ èå�sðIaŠÅI[^ /˜±P—å\U‡øcáL°…sü�¼‚�%ÕwÞ¡#GS3²0á1&¾ÅéŽ½Šžé�ÓáO¦^”±bèÕíâ!ºد¾{¥Ø™05ùÙ�¦7ò‚$=">1¶dªè?HÞe„.œE„\|ñŸÝóp$©H:Ò©×iy¯ó¨�3#žhLÈ�ÅÅs@ÏC�ÿý~‚sëïúÊçGù¦UÜt‰‡{I{„úxæTÒjkÇŽ< WiFjS¾VjàÍü|q~Q}ÊN�J^KGªý‡�¤gd%KбŸNË¸å‘ q­ªÂSS/4^mîdêÃ¥=¯bÝ4P‘O²…¦S³š¦ìÚ” LZ^3>í)¤½i§½3ã7¿ÿƒ•*|膙¯¿ô|vv@ž 2¸¸0ûŽí…Ü®ô˜c¼~MF|Rà@Œ74�ll6#hÐ÷Û?Ü?~ìo™Šܨzý5WñY»aã�ùÛ'Ë:«c¢EÀEê�t7Ú¤õžhVª¹Z�]š ~V?¨œŒ÷ÐÆm[±&x§Y Ћ |Ç\<Øx+PÄs¯¼É†xLé¾u;⼅Ö¨Ÿµ7+�‘å•Ù¹ù¬‚pÕÂ~òÄñÇþþÒwþé+�‰ÂÂ80t¨a—�ºbÃÖ)óÞ‡Õ‘±þz‡‚õ Åy9^x(£—Tr €—…ðLÉÈÄÈ‚Œ’•MÂÆŽ‚Éˆ¢i‡P�ì<‘jýY˜ê]h~¤Ú鞃GL®0I�ˆ•­‹óN¢Ð*¹–ƒ8î­åÈë¨*™�ÍÈrüd7—c€k¶®æŸæøðO¯n%þ4?¼÷n†o™ZÐkküw"Ñ|ÜK»ùƒD%Æi´—�…öeaŒŽi ÍÁë%ó¿� ™sMRÓm@Æë¯qL!Šp.¦=41÷mCgøˆT[ïå�ÑíTZƒ¬pž‰!�*ª/H+[‡‰&±§Š—LöëÛ+¿3^( ó'2Ëv mÄòCMÊ¥7ç7y·aÈ ]Äà÷ËvX`€“²Êr¢7�õÅu �jx‹ä�ø6ÇD']UO€Ñã”V]�ÿ”iLž'ÊD"Lh˶£š�R(¾ ) ÐRu²:£�)ƒ0á~á@bel#Œ…T£”‚€)ÑÂTmW™�Èe HdÓo£ŒQEÕEGd›‹‘Á ÊT™ô0ª ®þ –ÒŠ´$¾ˆ®úÉfHŒP³0KliIîÈ«fT|`:$úìÚ˜¨r­ôB†‡(,z5¤"Q™b”¢‚ˆç¯C™”ª À‡Fap¡p>0hDRrµ] _Ëa”%Ψ` ­H˜DQO¥ij¨P{B›'žï‰¢»ˆoâÇ(§–§mx4TªyÄ4ÖIDg"�É™ó=jˆçÎ}Od dì_뤉zX=[9¹ˆ’׆°š š§Þ¨Þ+1�u ãp¢isÓçxŸ‚©�HÑ‚¢»­+z1ônò‚%pŒ�am>HB•±*†ÅÅu+*øÔÄQþLï.­©­Žåö¨àäƒ�¨)*q9ÁHý%�Ù+õ=ù|yy¹ÿp%— ^=v8³šE«6ú3žÍ;¶óáE¢I3ÕÂôVä/ë|éxÖRáTð“'f}D•\é¥$tÎ#jDF&$&2/boþ†\b[^Îåw¿ý¯n¾þê/~ö¯¤®ùÙ+Öâ*²X¼\¾|‘£7b |•Û~lníeLóˆöŽÞÃ#ð|Dº:¶<‹ .›B3XãÕf@Bý)vÒ”PG‹·ö¦ :‡õM-ôRþ iv_<­$–>¼ÁkŽ¢jä�‚¥Ž»¿ùí—Ÿ{Æ+cëÖmÛ½Ÿ�ÿBÁÜ´øŸÑ Áb]Y�ÆÝôü�,ÀüØÉ·~þ‹þ¿ÿá*†fXõëÝëÉÿß-_¾“Û€šEuô'ý«†=ÂZ¯o³'rå7sM ™sR­jdZg·ŸšŒ±;5�ó"n¸ú WV§Òâ÷íéÕŽ“";É2”ãPK/œ—²òòÖ”ø,|×Þ}j qeãÃèoÛxñ—ǹ˜~¤C6V¬ÝðÇ¿<ü�;¾„”hLªÎ˜áž™4ÓîÞ)ƒn¨Ó‡‚Å�q´²¡$˜!ø76š ÅÑŒqpn�Oq'# Ù’M+ßî¿�Ú©´Šs¢�†mÚÁ™ œºÇÇÄd¤%æfp=7Ö®²òÊc§Ê¼NšUϯ´¤DnMLˆc�Eé:tì$VQÞiÐf‰OóÂê”íz•võ~¶|=|ÀÆ�ÓOô¹¼ñSš§çr�dh¦1c a¶·Ã+‘ £¢–Û˜H DJúˆh�<éåjùòQÑNé€2�©.Ô…= ?(@EGé¬1õÀ5¢ÍhÙ¦£â…‰(ŽÄ"®þ(‘5ƒ䣧6ˆt m¶6"ÔgQËšOHr“)¬YM³ÔœšêÂ>5 Én3rsÊ0Å*:Ðöª‚jo|rí´É˜HwÅ„˜ÜŒ ªj+½ÿ7°®&1®òDž@YŠGߺåÖ^4#=þ„âx�ö.Ø@�Hb1¶-)Åw޶¤@Å(�ì…Íì™ÐZ‡ys¿/©¥p^DQ+T€¥�)ÞaÒö<àç{¢Ú=¶±úùžŸ!)¬G~z¢D¾º.XY§E¶ð&‘¥æU±áõ)õ=�W7T‚Nxˆâ»|ͺQõÀÝtÕ¥|âUXL=µÅ¾·^3εyûN&ùh±Z£K}sê»_½í3._ÿݰn³}×n¤^t\<Æ©Ý{ö–wâpÄm`"ab¥ððŠÕƒÕi€=| S?>B !aëŽmÞâ9�{véSÚÅË÷üko±ê(G9'/޲Eøötx;Ñž}éµç^~à K§ÆÒ?²cýa團Ø.:eÖ|ÏôÖ¥0/*¨¨q8מýGÈζSçNØ»´$ÝwŸìÔYó܆ÙVññ¡ÖŽ× Å#z«H�#¿Í{‡KbÎ&±Õ+ÚQ@§&¥•?}Íx?Èåk¸R#ló–ãGl¦xDj[ •µß´uaÓ©{ö e¹%`þ’÷oúÄU`ÿ^]KƒûO7oÝÎñÌyK¾ôÙ[]ì°�½‹ò6bL-ËV¯/(éÖªMÖk,¬8~l%‰Eá–Ü+¦¦¤¸¸\.ÙªSüÝOƒÅö7/çž½û9Ï™;ݾp]‚°Å�k(Ž?Þ«{7g!‹Ø�N´÷€—�¹½ŸJ^WòðBÃüdžëÇž~ññg^øì§n3jnAþ4— ë?ÙZ(ì¡ê�mÿ{P?2Y©�aÂ�/û«N' ÒÙQmyã15&( Ú_b+9Ä3‘QqPH7Á ø6b};~ôÈÓfúMoØR¹K°]B ·°ø’ úC7Ál3?|쯃ú÷õ‡·|o"ÊD�z?ÛpÝáÚíûýmáo Í‹m¯¶sæ4: U4œ“3×Zá5SM yiWÙö¥íñÑÌ¥U¶n˜ÒR’XÛÀ€…%”+èÝÇËÊÙܬV¬!Á9Y©™éÜÓ‹Á�ËN•Wœ(+Çó­YâÓü„‘°«ŽÒï4iZFåggrVjUu-çûc òrž¬lÓ˜Ðía‰&\(÷Ûø¥I”¿Ü3¾ÁHD‡¬Þœjë XÚ�¯w‰¡´SCHi°�D¬èÜ®4˜ñÀŒ Hv¨�.|¢!ɯôÀ´Á,o _¢-Ê1X1š‰í! b�änË!B”;iôF”)̲Ö` TJá¥�PeÉšgPøqæ1ë‰}ø™™F¢ÁP0üÍâ!ôÀ3”ž¨Üª H““P¦7¹‘�߯=о¥’F\9 ‰ªºÑتJUˆžÊçÒÂ-VM‘M5nF«¾Œ~$1)V¯X³$˜­T™áDl¥ñÕžº«5!¬@º™ºJ >ئpAÞôpÓAÄÿ¥SË Ã”Ô NõaºÃ'P#ÒÕ�hòªDÕœ_öeIjª=í´É˜h!Z:«(f˜ˆj.$ôJ±b1¡eàx£<™À,%)D KK’XrÕCÙ�ÁT â`S� RÒ¬qÉB,US±ªˆ«�¨¡ «Ó¯•(°QU5UAÆðX >Ö=»¨:BŸU³eŠ­E HA-ÏÜ!�(.3’Ñqyµ63ŸEO„¯ENšJ”¶¡1ÔæFe@MŠ—�:µ6™È£v³tç¢'‚¯8ư7¦‘ä»'bzSw¦©$O$ªÔÕè(µê‰î‡Ú„¸HFc”Zna°6ZYäY|Ñähüï­\‡¯�;� `�rÓÄQ¯Ï\Ü0Æ ÿdzüÂ}æÅì‚bTC¤!Ž�ÂÍ„¹®š3†}qŽ_Åñz(ŸÍzÉ))ïΙ�ïÇŒìíF9rôØAvw–¤« ³¹Tî›N(P{ôLoøG¼>uFĬ4*Ë4‰9G/c¢Øs°Œ‰gù��*w+œ×³fýFŽÈÅÞÁ9_cG]Dlj §5Õ1©lßµ· ¤«ãgýmø‰¯`*És”)ñ––öÕÚ~Ft8fAG3,Yn!@ÉÎÀ¡%(Bܧq;¿øÃ7lÚÂOÄÂãùcFzà05YCý.ߥP)/+£Rm€| @ã'œ†ƒI†6ß5p^ÞãϾôð“ÏÊHûÞ=_د�KÇÜ&).ºœÕA†¢³~¸u DyPLÜQ²T� �˜c$ÕynL¬AòÉŽuoW‘³F‚›ò.èSºzÓöÃÇOÆDÆàYÏñ[¸ÍfV5^ùJI«Ömd¾Õ!€àŠˆ=´qz¶~Óæßÿ÷ÿÂf@£*o<÷7_ÄÍëEË5Š2Åî?xØc›Þ=ºM[°Ü ´¤F PJçõÝZûµšÅ‡`ȯ(B\TÂá)fÖg¥šFs‡œ°ü­ÁL§ÞÐêu,@c]‘"m~šªì‡õÀ´lèÖv0¸Š’�vQQ—^0Ó¸RW®Zƒä©¬hP¸uÇ X�ˆÖ‘#G=Ó[ïž=ü#[«¥°› ñÕ�„PŽX¸xéÜù 韹õ–ÿð>Ï׿O/0×xÔ �…üÔH­÷D­¦Ó\N]Ôe2rF=ã±NÀÀ�W‘š &fý¦-}{õpáýz•z 0o½;w¡;)¹‡òàE1è÷ìÑ}ïÁÃ6ŒƒasTÝȨ$)ÑÍýCMÛö¥d77—Ì`°;5uæ¼ISÞecãí_øôµWNp�¸] ³vébw@7>ˆ7BÖmÚæ‘þŒ£•€m2ëå²�ò}šDc‡‚äÞÃG;‰™¥E ª� �û#ÖN©?'rÐVØUãÑ89„ÓØa•›ÉU¿0‚¶îâÒîKþnÉ 9™©Üå�“žš[^“š¢,¾¬{� v®Õ¼,m½P@r|ŒÿÀVÖP¹lÝc —$0â:_{2vø€)s—ÖrJ\]�×[{t+>´j“µ/©\C7+Ùk}¸—Ò{ñb½^š=.ªYx«�dôÂ?Ü!#P÷öh§ '뉆 ä•’'þ¬ÑQPÀaö¦è6¸TGÚqÇ0ŽfVD3 `e0°ÃÕðtDNJcj©,hp ƒ”ä²5jVªñ«çâOiÍ44S1›(añ¤ÎèL2d 8è¡ÔÐÆD*…v$Ç/İœèäIFù¦#á–fÐT1 z%}[:Â\f]/ZFnίÚK=&#”í[›²ÓnW^ 7gа’û�"ž\¦¢É +y_O&GÆ öBà¤Q,@qP „ÐfÌnÉÃi¨Ö¢ŸA&2ÊôMÙ�¢@ÕMNj)ŒÆ<“ �65µ(ÎoȆ/àEoÕK^`Úo¢jÈp%ë)A´DVýÌ4k÷Ÿ‚±xŠlúÒ›èd”uL¤Mä9Ž¥Wn{aœ¡&e�‡Ý9@@‚p-+«¨ø·Ÿÿîù¿ýÅÛö[QYÁÞÓ‚™;úWºžÆ4á‚…†BE�Ó �ÍI6¡¨yhßöãl¿(á+7N`ÎvÑ Þk7ï˜:9¬¿È‘2âÎÛ.ºÃBBÙ©2ìq«×o Q0Tæp5M€Î@øþ�…}“Ž�ÐHLÄ·¥ñ¡î�Öi k*ʤ&…oÙ¾G—$;#½´8ïá“^7MŠ�ëÓ½xÙº-ì7À¦O ˆÖ R¶› í—©o TÝ�ß®~ ¢#ù±S¿bTç”ë`úÐJiw*4(ä®ðÝ‹öÃø¡Ìš7—ŸkÖoðI|åeã§ÛÙ° vÏî¥ú{iÚùÂ1»K»uu¹° !‘Nê”t‰°¢Â‚Ë.뢼opÀ“;><0Ž¡ä:KÖf^xù•´´Ôï~û—¬ŸºW;/cÇ^´?J¦PØÍžÓôDy ÐcÅ:ôKu]»v¬ø¦¹PúÝÈ¥ç^™ô³ûîm©_ï­Xͽ¦Üþ‹œ×%áõa8»ats)¹ëó?|Úoo¢o3@ì?täƒGXc�y3˜�ÝDüË~ñÈ„„¤ÈœH„íÉÇN�<�OìCO<Û£[×>½{º¼UUœkó—F`äEŒp*E»G+ºKËG4Ö§•~2’ñIuhÔ–àÛbs‘GúÃ9ÐNÁdš’Ã8µ-#-%#%9KjTTZr"Rk@LT$ëg•Uf6¡-ÂqsãŠOLo“Š—œ»+35¥²ªöä©ò²Šª*ú�l,g~ð+?: ÇºÔ_ÿî¿9zÜhÛH^ a õOþõOÞ’í�¾=Þž¹e‹%Yϱ}äÐAó—o`”¡!lpïÞBZš¾ª½¶XÂq‹/¡÷êgo/P/­²–ƒÓ´¶Â›À í‡x²½Ú©ÃZJÔqfI I-Ñ,§› ª¬Ü-�×›¬ üÖüŠ”†%bgCßzÕè„0tRyçz'ʳfË€L•ÉèÅöLõO nJ.c‘{¨|üõÚÑæ´�"\1V\0˜Dr*a 6Ó›Jâ¿*&ᬈ3ΙÌÈ.NÎÞ"ã #˘«F˜áAÖM$ Í (@][F ª&,ìQ Õ‰ñÚ(%ZÊHcu±.šôäQ¹¤f\s»–JfHH¤*@møCfu:uSþëKìh¤€’d7Óš�L¢‚#¤f¾*B“,|VŠ`i$uÍlEQ Œ‹Úh ÒjIòÉj'»«Õ@Tm…M„úÉhÌ$"�¿½ÚipLeŒrâ/�i¬ñQõÒÅ"�cQ„ŠÈÐcµ!TC‘Š�KH=JkµåÕâ\ƒ T2-G¢xGÁ@IDAT¥Ôr|hßÖ``eÃè/˧‰E¡h¹I¬SàGbV–%Ð ƒ¦üõ¬1„ckóÄr­öDj¦hMa” Œ&ÓÓþžH]Äšª§¨(•YQ¦7Noå g�µ(j"Ž6¨†rh|¥•AGDQUé¼p�QQÑ"“l¤²¸©=Ù ­ #‰Œà¢ÿÛê‰)`˜°t8«ºŠ8Ê5�I ²£n£ûñÄ�J ¦�:g=‘Â�ÍU$P-Um‡¦³ª*�®0&F,¨_¢Î?BOŒÒ�&ÔR#1eH¤Ó$ÔÓMzí%¬šíÚp\µ¸ÂDŒ/©©<ûGFİ2üÁ®í“§½{Í—{ ±|}éÆ H¨²ÊJT÷ý^¸d»?Ø3cÁÒ‚â.ÑOnh|Á^|m²gzóÎ[¸„³¨¢cã4ã�Š:^VñþÊÕCƒ—TŽ>„šæ/[wðÐÑì¬ô ôìÑE¾cWŽñÀ3“¸è*”*˜}áQ1›·lë4y8�DSg-àþJLm�‚¦Q‘“ß�{õå—&ï’¿û¶›—¯Ù´|ýÖ²ŠJÌ‘ î“›%7™‘ÃþõùÉô[Ø—#´§Î˜ëLo^}™„oܶ3¿°„âöîÙ…åÑó±riÞ™³�{˜nµ…�*ô­]ÑS4é"0‰ØD½‡+&õ.µ¨Ý8¤QW²a–°€¬½¤ö¤„_ïþÜ�n#XÁKœÀ"Ò5}8ïØ©r6³™7¶ªê…WßüÜÍ7¸$9Y·ß|åì¥k°çe^9z˜g•£G<øØ3‰©Äy“Ƶ+/뇽k÷ž¸x\œ°íNŸ5ߙ޼@˜¿dynQøÇ <ËuÞˆð×ÞžqŸ1øW9hwîúYKVmܶOÌ¡}K9Ð+eí†M¸dÒA�äôÇÉ3$&'££ý 7qG¢—ž†Of%lâîZ\äÂ{•v¹ibøŠ Û³3Rûw/qŒêÏ¢\áá_ûÜuÜÿ°ýƒ}¯¼=ëБ#åe§Jò²<çPzÊ¡C‡Ó2ðŒë„F“ÄȬ� F°õO\.%øà?S5„IEbu þ”Ï”þtBé€`Ð�0r0v7®_�.}»—àûÆl�a± 7“[ð¼’æ.ZŠO.»Rß~wîµ—� E<ðôë§ÊNy˜ÿ2øxy52¡o�®·ÝtµÒ b4"òå7§^>v .ËoºzîÒÕ+7mGu¾ _� ô‚µF èõ§§^ 懓ˆ£Ë"Ñ~Ðn¥Êò0§¤¥òºÁI:­ÔSfPM"¢#k«£ªLJƒÕ•—_VÚ­›ú¶õÄ„Üìlçâê•¶mûN Ãg™«LýqȆÝû�»KK»Í_¼dİ n¹á:ÏÑÌËØ®HðàÃ�Žs‰ËIžyäÁÿ÷ÈßXࢉã/õÄ—ì•.ûÎ×îb«òC{òÕ7'×Õâ¡SwÝ5�,´jõS­!r#Óú!´ã]ÎÏò0�£�žhkº¨§ôâMžðUÀ_An�û@w^ÄŽÛNœ<™’ܸ ßô<,è¤�[Ï" Í÷ij/õKŸu±,ïýäÛwLŸ¿l×¾CI‰qƒzwܧ›ë×/½=gãŽÝŽ<-Ò7^>²Oiñ‘ã'_�6wëö�åÜîÔµk‰—iÅÚõ(BN5÷‘ÕŒ)àÓ®Ñ*<>9ØKhµò”ÁÇ�6ªÁñˆÞª¼8!‘Xï‚ ÝÎMˆ¶A2 @£D²¢;“\® ¦*@“b™“qPñ’Ã�d¶‹ÀUøð„2&J¦àÊ,}ã�d,jPX”ü§X|æ;’-mÏUArÆDÀWœÌbXHPWTTóÁ ‰ZËbÅ©u6»Œ1eïbJ@e $j/iU¬ÑÇt'©&ßƆØ'Öð#/æ9�,Ý‹ƒ­ù ÑÈH«ôßÜÌ$9†Šˆ|Vc#c¤fSžà±ØYWQQž•õ5¶)GK¢Ä’ ¤¢>кZÊ¡Ìj*Róajb÷¨‚%©”Ól®˜æœúh  zÈŽf¢0µµL�˜Ÿ¨µ(ª–“#ÀhÔµÓ¦c" qP`åG T!£V‡ëdøà,QBhh›‘@1�‘HãPR'h…®$†³íI ’~¬–Q}äʇüYtrŸ­P°¥€â �£"ŒLŠSuë8çO–KK.Bê++««ËY´Sk:¸„ãxU…Væ‰*£µž(üÄ.àÃ¥72ÁQ ÕAfPÊTSÓ7ÚÛé]´I\dò<*Fø sÐæ´šÚS•Õ5\\NW¥`S{¼SÅ �=уëjD\')VlmôeQS,WQ…£†‘¥–I@÷Ãu¡õD@-ggE³b�&,Ì€-6ã¤F:0º±é=ÐËãNUu…Ž…Å÷1%?'=Q(ÂL OU:?ý—?,)æÂœ©‰Y²Õ;ÄÃ⛤žÈÞ1*_ü¡OBªI DSÚ«ý¢ó)µN-JìÙ?@FqŒKHâ½'^x ·—ËÆf 8‚ ¦Õ‚¶nÛñ‹ÿüSjzÆwÚ+É`+ {¢¥fÒãÏøÒ¤©É9Œ&¨ ‰zðñ¿þñ×Þ$§KA.ÞAïÊK†â×,¼ÕŸôpŒ€èè/¾1åßùš?Íêu8ÖÃ\«‡yRÚ !1é÷úë/~ø]É/èß“�ïh�ýJ‹×oÿ€ê`pܵsž žý‚›¶nÇ©�cùar<×oÜ2d`ã®4Ô¿Uë6téF'mùl~š P¯6R‚ƒ$úMì˜äTÇBÓÿÄLâ?“¼gSðióÂf]ŠòO“lž{ùõW&¿“_XÌ>^Ì£Ðí­wæLw‰ÇE즼õª1-�Ìœ»àà±…©™4‡£)3æ63½Í^´DÎt2‡>XÍÜ;Éw÷(‡¬áOA#Â?R>:ã¡ ˜ÿ©?=øè÷ïýºIà¸Æ?¨Y 0Ãý<’”–E³;vÜ3|âÊ˨c†ß<efbpþÛ3/ýì¾o{ñWåãýlöBíÆ_4�I áøR}﫟aƒ <+á«×qǼÑ9Òƒ¬ŸÁrò£—dC¼‰ûçM›Òo~ò¥1Áøò,‘€¯X‘Jš>4Z·ˆ–�ÙLôðS/w)�ЉÅù%D!€‰ƒ�ºc”=?ÿ×{¹ñ™ù^³ZË[†„ñ<*:öoO?×—?ïwÌð|üé1^cƒ{wîbàGùn"|Õvª¹4J'M4 ‚,r­.P¿’úOûP‘ÌÌÌàÓV\røÅ;îÒ …½ˆQQó.ºdäÅ.1oýäõ|ÚÊÛŽpÍÂÂ7lÞ²eë¶î¥Ý<øßûÆ?�H\Lô½ÿôUÆ; ¾ß½çkßúÚ]8Ÿ&r[q�¯0"?÷ÂK§�Ю(FÚŽž¨fk»'ºqƒv ôP5±Z»ÓÔf\æ¹laêŒ97_�>ÊÄÑSå…é9:®!¯»bbæ,~ÿÊ —–¸”\ËpãÄÑþ\îýª±®|§‰™–‘�˜³('‹<¿1ñÜqË5˜i˜b@ Ò> á°cמ으fP Ëuøßµk´ÊONo/A²87$$s :ǦCKKA¥íÏ‘vꑚV€çô»j6ƒ�!¶6 ì0ÈJ�¦kÐd$À‡�JK» iJeSnûïÁ=í 0ôè@/×€ä±<†èðy!eyYÙ”™óoùäu*ÇžK/²fãæ—ßœ~Í„q^zލ»©5.õCû~ï€v 54¬@T˜Q3$½ÙGJ¼ rÉþ!&Ù$v•Í.‘&§Ö Ȇ ¤Á)8™b¢,›lþe Ô.šPÌ5V<ò\C—ª4”I›Q6ƒO‹Sˆ~Yj>*+¤1‘Q)ÁS›àT…Ê]M#©ý¶àfóD2’=&6*9)Á)ÈÉ“{*ºâøI®š«ˆ®�ÁK •DE85FQˆLB nÿ©–Q‡"¹–f 9‹tõ5ÈV0&¿�ëêZü°>f�š­cê"±þcvÓÕPIœF<þÅÀØWt�G#N++«¬­Æ)C¶”Qd�mD�!°Ò(-ÈÆ˜{7Ô\€ÚTí`(ÈKÆ7¡)ã©«£3äU“Z¥‰?£vj$TAÁ1Qˆ8ÅÄ}(ˆ(úÀ\pŒœöŒÔ"‹ªâÈýÿ±wr&Õ�þg¦{rÔhwµ­X ‡s¼ÃgÌqã;‡360Éd›dŒ˜�ÉØähÀp6&9³k0°,ió®´ÊÒhf49ß÷½êþ»§'hFvµÐ¥VÏßõW½ªzõêÕ{¯^UÅo°'‰—ñ…SuÑN›ˆ/Q� „«EÎÊØ¢°øpž ¿:øuæ æ[Æ �£’È Ë¼ÌdA¦M·b”úÚ/ðÇ_¥‘}O Ï0‹ûÛ‘y›ðÕgÇÒÔäœ[ŠÑ�:ä#³|4µÞVX{`ùÂP3±”ãuÆÕ[º:–{ü´5ÓÊæÁáÓã£ã-Ì P ]ë’ µ³°p_ã¹Ð\èìl[®=‘�QCžë>¡{ƒ ÝIVxl2+�Dñα<-ÅÎtÆÔ‚ÊwÉ¿‘µ€ê¶n§?=:±£‹…fdž]K"0qÛ�D›–ØšX tI�4#›Êo™ªƒL}!õZѲ‘(é›–í7²£ &`·}i±îKžHàȃbë*ª^éCÒÍø`áÖW¨§·»·ÿµozדžñl¤Ì*²Yá)çuo|ë“þú9Üà ×&@ˆ©2<`5ÁøÅe Õ9Üz5,_¤ÁöÖÒÞÑØÜú7ÏiÍmeÕ¹xæ´¬·~àÃZ:jU…Û™xbN[ǵ7ïçR¹ªäÙ{?øQj…ŒÌ*òÕI$E¶,vôôÞrðÈ?½õ�4sÉë¥?®üú·®üæÕ |(äêêfiu’Ï|é oÌtSj‘û>ýÅ+ªß²§ coí+¶åì"! ]qé Ü\áŒ3®ôòFÚÍKWÃXN›s•¢mLgᜱi§•u‡[zècžô�}œsñ:{ú<§©€õ5¬çÏy1o×€ô¥+¿úOïxow/¹Zé>ä�œ®&*Zúù+¾ŽMÍ Rljì[ßý~5LL'ì6Åä‘hcyåSLÂOÍ[~¿Bð˜Â†ÕÕ×Þøæw¾ãÚ i" '—g¾àe, µtè”÷¶÷.qerwMÙ-+–àX¯¥çµO~eÃ4Ws†c^.ü�…Õ®<†fËj»Ù¼ôµoÿ,l =@tÔÔ"Y¨+p­k,Ü(¸8ÇY$žÚÀ�U,;™?=ÂCÒdYKê,8 œ+A2ËÌÌÎüÃÞñÑO}V‚_=àŽôŒç¼”!Y`Zom]?¸âëW1¦8I°6«Ž 1Ôq”毵Ô�Pu3¡@X÷`|îʯ쓟Í3.øÞu7~ü WÄK^&à fMA©ôêâÎ韘†l¶q�®äã“æœxbJcã‡ò‹s—GÇf9ÅY bܬ�XWØàÀÿyàƒÇ&&´à°bÜØôWû,Œq+f>rôèÒø�—•bâ{iR'iÖcŠÍOzÚ_L.�†ò„üðGòg yŒGÕÆ3ì¹™G¡¬^ü÷/?xø°BPd8W"دo$šŠ£jÀÖ=Ó?È!pgÖ½>òé/Ö ÕüÓÜ]ÎÀ„½ÛÈqWÌ3_ðò›ö(£g…¿,†½ê-ïa3h @’V˜s‘°ªsa¹ á÷dÿ»—¼’E*••«‚x æg1[…2YÉÇ«5#—øù…yÎ]O%]\Ü ¡"i‘ðâÛQ:¥á\Jzš+kFF� Žœã„®S§Çn=>xóÁcGN cùÂÇ «V_Og{F·Ã'NÝtðØAöŸcL‘gÆcƒÃs<¯öÀÄUüÒ=*«ÐÀ'¾pòIWo_ç²OW_®�H¹y‡^¸s cL%¯yÓ;–“Ép©»êÛßËÓç™Çä©’5$Á[bxEHßñXúÊ_-�\Nœš· NªÀ¹ŸµtêÍ`ÉSc‘“m°u‡‰À¶iÏXlš›ošYhš�¡á D$Ê‚J¤N‹ë+1ƒ£8¿ÐL4¬{ƒª/w˜"“ØQHÄ<Ü­¯¯G(á‰Ñ€ªÆéiÈ2Î`ó�Mã‹•B\õçŠó|6>'Î1'RÇQEIØ%´Vp)( :gÎiYQOä¤U\lКَ]=Sä�Îphkiá\+¨~fŽÄ¨+ œŠÇANL€|Â…”–*‚2�‡Â3ÁAtvåBm6ÙÓnµ$ ;zqÌã øÅ™®„à~ÖPHÓ=8ô\5  ‘’úcìXÁîùì omæL•™i�ô^˜Î²™Œ�¸c-à&„_Œ�ù*”DÆô g'êCÿD)ÅìGäCá c¢�Zs¬×¼µž]l°ÚàPCé=MLg%�Ö̉¸|,¶4e­¥ *NŸóÆÕB6ÏÖ¯J‘"ÂÄ�BO‹³–&r ©)òÎêVØÈ¡¹8ÉK·�§‰m•�s´[çã&<ÓØ{F&µ5€i#ò„]i­øø»µ·ãì¶�À΀æ¶fŽ2œžœZ˜‘êÉ ]MOÌGb†×|ŒÄ¶–bÿvÕ·£JÐ0\¶õp±4Ç(ážÆ­60`I‡û®64â½\ªC“W 8xv·cSkÄ™ ·½Y}/$è³ôi$‚$*rí© ñÝÝ\ =5393ÃJÛ"d=#‘R 0:©³£"!Ô_ñg'–çb£Æ£–Ãil¸-G"# j—�'½ßZ2ìá| gY¿Î©4 zlÊ8·‚ÃMØîcü!‰a܇©hGóN@\ÃqGܨ²€*©*’À€ÄÞfd5nýÛ2°­o`Û­G�ÿáÃÿÁ�~]ty(W~íüÈ'|ò‹Wömèß¾Ãëäª.Cdìy`MGçg¿òŸÕæ†ýا�ž±§À—‹ÚÓÚÞÞ׿õàÑ“ôˆÇâ3_à<Ýêâ˜O ½ê oyâ_þÝØÈiFÆ:[ÍœìAKݽŸ¯R†±A91”öÊöÊ!¬ü˜q\ ¯ÐÊ�þ©Ø§¿ôÕÿéÙˆojž†*vËþ[ÿâYÏõß>3£³.‘xP´µµê _ÉS ýêk�½À-Vé9´nh¸rpÞg¿t%a•KÉ3žãCP“ÜSMØÛ¹m%%WÍÇÑg´Y�W˜ê*X9Ç*œ9;8ÄqÝõ7~äŸ~Ö _öاþÍÔÜ´´e`;;pAêzz·Ì,4<ö/ž‰ôôéQÅ¢r@øÆâéÏzþß¿îÍôµ·Ö‚Iƒvd| ¿ùíï–Óf_ýÆUMØ—¢/è_ì¿ÿö±OUÛU?ýå¯""Ÿãù8L0y‰< ßAÔ„ûy?þ¹/?ä1OºöºP«ÓLNNá²÷€‡?îæƒGûúÚÚ:›ŠÍWÿº|Ë;9ɨ:%«3/~åëö¸§ŒWZÄ �ŸfÇ|û{ßøöwOMU²0š®üÏo<øQO|Ë;ß“ã üá*ðÖ÷þÛs_övàæñ© ¦ÈÿØ'òØ'3Lp ‚7UWଟc¸C[L6*„bdóA~Ε† •¬yfâ‡@ã!tdz.µ*cjæ[ßõþ?zÄã¾ð•+1tV½t8c¥ýçüÄfgN1ÇQÃï˜ÀÜ,’ÒÞñ¾O}n‰ÉÈyÂ_<ó¯žû¢TWW æÁNqüìíëÓ»ßÿ´g>oðÔ©êìýÄç¹HpðС{Þû¾ïû—T�qð £áïÿáßýž�+b�¯šÁr~Ð&¾¾‘H ¢ÃøbTºˆ½áÂÖ—�šë‡pQÏs€[ÌÐ܈’ŸÌÀ�Ésu)†|V _óOoá:šÃ)#glÉÍóä›÷ÂÖœƒŽ2ââvÅ7®zÒß<SHMWòöªo÷Á�þó§FpÚ`M\T—K§>g7[!r,­ËZ¿ ·+ÿs]•dÞ¡JkÁZç;zú<�NQÛ¦§g‡FÇsrß±ÁãƒÃÑ;ÅæâCÇGNÓ <8ð,À`Íò&¶ÃÇIpðØà±S¤â{@À3"€.†µnë÷h?ÿß§?—¶M` FЍþà¤Ïv äÛ7¼ó}9ýпx�ç@Õ¯^õØ8"hÅ7i¸sùOxÚ+^÷Æå¦ùuRE^Pvùô.í¥àÉ_·w8ké´LÎR¤– íŠ %*wÒj¥¡Þ“ó‘�è+ÊÅœín•�KÔ‰àqjÔØåôB‹°;å-Ïj¥îÀ ÆL‡õ‘&—‘¨ ¬‡x½±91ìnÐX©*©—\Àb‡¨n�–�Äð b{m#ÖµŠoæÑýÙ Wû™+ñ–Ö·ÝL³µp†›ÛAˆ¢iš�Ëå1$DÚZù @Ô­l~°^–D³4£«<¨¡²#2ùÑrðçæÞàŽÖÚ>R¶ãž¸Ž®Lˆ ÙÈ`é3Zº¿˜é¸FŒñ Ëx¡ ¡‚¿TÕí€Q}`[Ië,]ÐÔ–”MŠá¼B.kb üH†S¤�èã’ÍÕÆF¥ÓÀSÊKI‹L^ÚÝR«­I$.³bâÄG]Ì!NÂÂJŠ5š(Fݨ¨iŽŠE÷0I“G°’/ÅøÒ�m»KùiÇÎnsÌÂ/fµºE_¦¨tB r»z¢·�%L¸.Gus¢f…ü|)Ìì.NNMLÏÌ e£maÎ[çH4]vIN�/{OŽ•j~úT”àgPÐËk ó¬­iÞ³EÑ›ZŠ‘T*ÇzT·}Š#w èŒlj2ƒ`zÃ6ˆ“ûTÎ8é¶šÚ )lÕôt¢¿oó‘éÅXñx–tøí†{åÁ$Â3Šñv-gêoÛ Ó¿?$#qhá~‡.¬ö,FMÄÖ‡B”\^o¡-÷ðO]wÝ Wí/)båœä6;;éO£ÃCCƒ'8fxvz£ò®�ÛwïØÎ;zâ$ûò¸"J¿°ŽN”ðwÃî¦E£2YÆÀäÄøñ#O>411¡!(c€Øµç¶“ä¾÷H6\79|òÄà‰cc§‡1cÁt.¿ü2î=98xãMI¨ÁØ_¶íØÕÞÙ=11và¦ëG‡O¿­£‹Áú·ïb—̨¦õ4ddèÔ¡ý7�:…»ZÎ,».¸ãg²¤78=<|ëÍ×�œ„+´µµmݱ{»;:ÄããcC'Ž�:y|blå�}ªw»Ûå”rìøñýÓ¤pÌs;va[�ßa|ôôá[o9uü(Vx%g6oß}áÖ»ÐôÈ855yòØ‘#LŽ�Â׸ÊnKÿÀî‹.Ñ�’Í wßÓ¿oßemmúJâ¬Æ™«ZëeúJŒ"–dxlbµ„Óf¯ýþMWß´9ÅáÄÐø:ylÈcü2[P"c�oz�~!pö_'Çko€6°Ææ'ñQ_4Þá¡“§Ž;=2”¬ {.ØÕÕÕuäè1ìÂà�^`¿sÿ¶äu#s°B¬K#çÝr#½ÏLB2Þ²±ÛiÀÄøè葃û�åš]êAmÛµ{Û® µúé‘55¼åÆáÁ“t=úóÖ�í;÷\ÂMÔ0•Ùõ§‡‡htµ{ÏÅ=[fg§ï¿™ö’3kßÀöÝ{.a»1M�;}êÄñSƒ'&ÇF©[KkËž .(¯¹îCbì†ýÛ 3òÑà±#ÇŽš=½�Û<ît 3âMûbyDëc€pø:]‰_';svìÆˆÀ˜=qô´:51ÞÙÑqÁ®Ÿ€Ji/ƒezZ£¶U½ðâ¶®.ÐròøQàƒáËö^¼}ÛöÓ§O_wÃMp= {ûÀF®¬e|’÷\ÂÝ/ÙuÙÞ½1Ná`ÓÀ\ÈKž¨"-¸\¥” ïg]Æ¥åÆ™‰©®¿þªý'Ï¥hòÒGà|lt„.„ÍḬ̈Ð!mÛÚË�C78àþ5ˆ°§Fþ{XDÌÇsëd�´|òØa°Êéï¹`Ï…»O�¾áæô&½@/ÃÜ NèpÏ�ö±=||tÿ�×­ÆÊ¨3dF��<99>ÆÏ½—^ÒÞѦÎvã-ì³Âï¹Â7<:ÎIo(Yôx á-ÿÝÈ¡· B< Ø®ž>æôžþ­+ÎŒŒÙÑÑ‘#�NDú�:.¼“ò4œùý’‹÷ŒŽ�ãV(&Ìè �.eÑeô/tŽ„tüÈ¡åTA&ÆÆ�[N;‚,DnV‰.¼d/Ã+Eš[o¡D&^õöoÝ}ñ�P,© Y˜A` Œ‚‹î´yurblCâÍ&Ng!�ºŒ=‡¸qT“I›�ù8U¹“(­H˜ª)Ñž´sn\¤â—ÛàøÌ@H¿!„’òpù#ì1Є§}Í#ã̓ß*© ÒâQ®Ë»˜g¼q*»ó(gj�iÕM‡+ì,ºøk%¢:ë™q1Œ¶Aøk�ŒAêz*º­Ü‘²Pän¢¥z¢M¤å…BgKsw[y‹ÛÉÃÙï]ž��JÏ:�m¿�¿\äÍ:+;™PmÚ[9±¹1–ûEHB’a -A>�66‡™…x0‹³ÅÛ—úž\Lú<‘ž„´•îP}׫�• :õ]œãi ßëÃS`Sc– �Èî½Í¶ ²ßšGF†ñ4Ÿš]à$"Âá¨á,f§Ÿ¾²~;PƒV*dHjÑ:Í áEDQ³á´GùÑÕt0§ ÛB\Þ «-V­|:U_Û,5Pó^—tZ3'R"uÓ †S%®L˜ÛpjÒMHûª<×kCP3x®qêœc|qH™4tšMÙº¬È&2èÇÆ-Ò"ŤpKÕ€L—“-”(è™ .(¡K’“„€Œ[e*®…¿yavü`Âmöà¿ÈûÂìÃoÍžýÐR >?–µy¥Jý�CÇfÆ'8Œ3\Ç "¬Á›p=1‰óT|µd}8[’8£Ùᛳ½¥ãtNž:z ?¸6nVdôÐd[¿æHo�Y{sk_où¨«—>>{ß«Kå¼ú“ÙÏý:ÏxÓíÇ$1x²»¶¶âBS³;F£Mèìwm/6öu—ÏTý÷7eÏ}x ÂCŸ‘=úyqìØI¤=îe3Z3!«R2ƒñ$�üF"òYäšgÊe—Ц7É*º*ÃN@˜1&,É씣—ƒ¬’•¤ç~hF"�Am¤á%)ÂfòF>F'𤧪M׎îx�Pú“~œã7à™ª1¥¡K`A?G à�€C§Y3´D¨²PìíÇ¡§û-è"º‡0¢è'+X €Hwï”",´•S]+NÑU‰éGr\çðÊj�âfnÜ40êÛ:;“óN=š·H‹ÅŽÎNçÂÅEÌ%œÛ…~$5--�ߌqLi½}[(bnv±»CGww,›—Ò“�Ÿd ñƒH[{l†uþhDklØNî‘9— Öºûú§¨¢˜Ê€üÄUZÑÓÛÇÁØ(�4“ Œ]½½à�R q‹B¶e KLæ ¤§¯/=¶¼þTnüIúˆÙÒ.¡jp‡˜‘© ˜“øÅØ èÄó ²Û´š¸üÒĉ³³=è±Â_@HPf[¶wBK`Ý2IM]�R”[P‡Î‰Æ‹®ujxôäð(Ùqy@Z¡ìèlïìdqÃñ¡ :Žâ7Z12. Ó9$iÛmÐS(Æd¢ènw¹ª��1Fjåg®X-â‡nÏ¥·®,aS#%5>ö±2.mïÜl/¹ T   [@A�­mm9-ÆGŒo%ÝÒ‡ñ/=áÃ5q kÌ_&兼ζš/ŠR±–B³,ãIæ7HQ1äEÍ"˜qH€EXÌHI®oNŒ-�œQ êd<«'hãÑ~4.:@äGd¹¢Xëx"g:"r¥P(GX›=%•Tº‘ÄÇ>#*±:ZJ¬�Ø )›—Úe•xÄ÷-H,µ6Á+AåuŒË(gªŒDzó8¸×¥"’8¼JÈnÌGâ:õÄÕFbø&G/…•#�<êƒ-;‰/uÑ®�P%Ù1„HÃ.﨟–°”N†C P|0ÙÎ\jW4nS¾@ ¦4®A7Àm m…ñke1‘#÷ó*¶î1è’‚Üciñ S>‚ ÇlاܽàÁˆÈ"•ô<#±bq@àžô Ê¢Pdnk윪:á�´s×*R+�*ŠMœûP˜×rýaëö]n€F·SmLkᙈABllkmß¶ó‚þ­Ûh‰–ÅÍ#ŽR²×#Ú:»ç9$!*f¹á:�"D[øOÅR€F]qÝBM@Ç–ÐØƒÐ†„„0m yð2ŠÝ……¨?ÅQš¶býó†œÅä¤×5·ü8Í0u¹!‚“•Ê;Ü€§§²ù‹ia€y F…ë�E±K²Ð(º»ÛNü"e+eœç‰D1ÿÑi¡*3óC4½„ª©½"õ`ëlß²e€®GSe‘˜Ž ]½Àà¨�pQS·mßÕ³e+¥[Šw6´ P2?ñê‚8™™Fä-™½Åì}[œéâð¸–Th‹�lÚÚqiKýH=B¶ Ð[u‰`ÀVª¾%ZjN´„*…:å’‹64£Uó—&Â_²°½ØÓÓ§fîU•Ì4¨ ¾NX�+¹ÜÊotLh j‡Nñþ@© ã”èœ[úghõ�”§ÞßÜÒÝÝÇXCèh¢ˆRX‡Ӏ̆ÆnJ hN$�îYVaÍ…‹uðë–ê8l¤:Ý)Q(BÇ•# O²¹%ÄpÖ•¡ ÀVHìÝ]= gÚ _C?ÿ´š VÛÖ‘P–·n&@zºZ„fè5º˜. Œ8 ³ÙþÆ–xŽôÐÖ¾6+KufÉ!ï)#g˜`q §ÊŒÑŽn,nŒVUï+ƒå¬±JF8 ƒ…¹«Ñ ÃøÐ›È-|©D(!¥Û‡ö%–,(Ž?ÁOÀ�¸�ÂéÂöîžÖØø¿"ÈÒÀ’k¸’+7LéJ\šTDô$PSPJà›\­óíp/QL?‹xºÁ¯˜S òcäK;¨¾ÁO]T &Úg÷´rVs3ÀK¹Â>ÐbzÒzH¬g#ðIìþ ²¥”’[�y‰‡­ØÎ FÒê«°"¶êH Y»„/û;4¢Í¡ŸëËDöرó‚YçeÕáuT,Ϥy.R»ekŒ;fƵ“o0|Øœƒº…­X"¶µ¥aµa@@Éû °häØ"S ýi1Öè>¸"�rg–jZÛwì¾шŒpŽ�+Üh�õÏV� ÂF ݰhj]91^ç Ööí;vãOJÉöè¬J}ÎXI9ç¦}7�¡“©Z†|;J§à›>àt �Ãî65=H“`/ß&øÅ g˜ÎÏÏ/Œó›¥$²\1ý !‘ò!>ïyhM[ef$jDJélWb½mÌ/O¡0•ÒgXð]‘Ì}Ó˜ìã~0ÒR÷•©Â V­}ýÛXÑ- R ç:)5!Žöì@f~Âl®¸ µƒÁ> -ÉÀ�҆ś͛2ÎF: = 逿cöR²e^‘ÊQ¦ø… ¡ù›G&¸fêez= ö¾sž¶ÇiŒŽü™7�8IÕ‡ÌTÀáÛŒÉ ™H$f Såàà„díéZl ÿ°¢6¿Åé¨ õ‰Ë×='ªä“XJGø‹QŸ†Y¯Îd„=»eÖ™–:X jE§~©º�¼/y\ö…¥_ÕßÕeÐ)gNCÒvÕ…Lá¬FaæhX@þ�Û0+€ŒF…0/2#Z*¬]”4šKl˜µ]3§ÞˇèKNÔp~¨e i¹Xµv ý(ZìŠmž™…¢€j ²wéA'>>Ö.üí¨›µàŒ«å‘|쥰IM 3ºÅq6)'‘!…€H8J2%’…Ä`¡œe‡á!FwIõm}‚h¶œ�N-6Qzld²²T‚Sõ¡*xÑSÏ™£;Á˜C²¥vJC̵ ‰6À¨ô1ç½g‰8 ¤z‰eƒ³3Qt„0ÝìL¥qNò:0]„¼%Ž ó¼.Kšß Ò;ÚþÌÌ"ç›"Àáü9m£9Í�rôʄ͂’ Zý5e¢V*[Ø3þOöŸ¬)¹Žs5¹#›�£:”1�rOz ¡¨¦] Lê |Q5ñBªr ÓÂ3�5?µ0_œåHÂÙvï(¥�®î…©zN e 8 \ ?k]hžƒ(!ÆH¤ë@t¸r˜ GñAoT>lÊ$[ä¯hªó §WåeÂ�Jíü0UEÄYŽDû‡ƒ9°¨è¤Ã'D‡§®dªµ¯D› >T„èÄ“ãòLu쇗�?¹�ÃOÀ”º­v$b¿4M4j%qSÃly$²±W¬@¨à�’e>ØY59€ža×È ÒëÕí騰�Lë­'󣌈�‘F¢‹ 6Þ³=!u�Oœ”:€|AxB‘C8îE�c  ¾³Þ"„‘?˜€£XPàI»&}QÃA'¾‰FV©É‚¶@•E°M4Ëò\åâ ‹˜B(N êK¡:UjÄÛh)À˧4ÃñŽ£øÀŸÕ¯.Ý48Âtt%ÎWSÃ!êVt Ší¼��DÂJÔT®?YöDΊž¢½KJäÊUgÓÊÅ•alÂ_¨*MïD•Ò‘#1ˆ&F@P(Ír¢éòéqŠ4–®g¡~q¡�¢W &ŽJ„±b“ *6`˜ƒ<Ú¨¨ÿÐRê,í…•ôìkYD~”SE±ôj jUê)kPEVIGg+%•H×b¬kT:Ôë­ôÖ–ÁŽÎr{ù‘ª—g�ã,‹æDKÐSjZÞ�”²Rù6¶@¶´«­+°ÜÍ?j äÅ–¨WÞÈk…›:°bC¨Ô‹›(â'¨“R]Ô 8ÚœVµMcˆ,æ!P“Í ¥L/d� G'hG`äƒ×bÃD!‡Û¼ZТèV1TÎê´ÑpÞ¯Ðð%¹@ãšL€ÑM(<˜LH)Äè6þÒòÖÅvhd*è̬ (öÑS-Ò½ü§!€”*L²³ ÕMé\=¦0ž„¤.PZH”vlÌÅʈ†¨¼ž¢7¥¢kÑ$š)v �Q1V“†ìYx 9L¬¿Óè5eÙDHd@€¯Ð«Ès†h=é�ü’RvãK‰íw:ÝCbH1NéQ“Ð.¬À#›Âe̵ÑÝv8U´N•Z‘asBâùt�üÈ"¬â’‘ˆôMý¬,¯M‘êZšD6§K¡Ð\¹Yg'N„2­(1�š¥ ý%²bvÀ&’ÆcT„�qZi¬ c –.œÐ)ra¡ƒ±— >À(�"†ÀHïh’ÿû£<¢}®ªÏg«TÉðD†ŸÛLbòR¬Ï*sLf=•Lõ9Ço¤ÓЃÕÄŽþ á·�t y2_aCKíbàK“å óöF´¿èGÖYòøõ?5ïhéPˆ�ÃÊ�Ñ&Z%Àš,x¨c; ñÅÊ'вöXÍ8Š+( Á'ó¾ô¼Œ*€¿šL UàW#î&àØþ(P·ß”±qé”fгˆZ€åQÁ¢‚M�?•)¹$pγ?µÊ2PcÙ„Hø_\6ˆwøD¨W¾>¦(((Y€ 1»Å§øÌGéeOÎO™€Â` kŒÙ±‘X¨éÄ2Šž?(™w&æ]”Õ\6'–S2•ÒM‘‡ú8/„°H•€o�ÜyÝÔÙÆÁ†x ”ÃìLöŠ'gx}ù÷’¿äÒæ·+c ‘,¬WQ]QHHb Á ³QœXÕ%ÛJEàÄFò VDÑ.%*�‡„ºÈtçÆÁ”ƒ‡ó³Î!3÷´Ë¬°áQOSzXƒ$ Ý‚Ú‘Ø ^Š”¾îð´©ÛNÐfEÀ\€< öÀ)àÃå{¬-á ÈÄÄÿR�6ø‡jºÖçVJì´,s¡¼»©FºÁ³L+W¢Ñƒ}Êër©K©�c·.·»ñŠZÎáH£m«´îd :Ó­J;4]·UÐ(L±*±ë•M{«´…T-ÍŽŽa�)ÕÁ¼Îʆ¨ªÒriø¯’»M¡”Ìe1é+ r�k�Ð’€§3ç4SY{Üëv–AÆØ×Иt$D �#?76ý/A_BwT6Ð)¾A† ¡¤òÀ1€°¹Q‡Az�¤l4Œ®Ä'|C¡ˆg>Ž»8}Å’ÓZä 8Ö@ÀƘ’F…A+rP«øñ$�ÊÊ¿M»^=±F:µ¸øO[é0Oá`8†�H׸ii¦+̰¢@ûñ 7M… Éž¬´g?ÂÈ償^OrÕðT ïì>ª%rKh§0#×HŒ@°žä†þ&ÛÃA¿TQ5yM´>€d¤Ö!¦X`J`w‰„’_w€­©š?�·«†mÕ×›ôÚÑ" ÑP-7N8pÈIs841änÚ7‰Øƒ¤ðû’J¹)±±Ñ§ç-@EWê+y³6ØÈ²FWa Z2ûꄱZÞµÛ›`&r\»¦dê¡c56µ”˜ÒÃJ-�fá%ÀóÄüŒçÚQ@R± \*…ê\å¸Íù‹³©Ž[tœ¡Bï¾Â߆ “à…ã!]–ô ÐÔËËŒªçX ÓG@ øgÎyY‘É™ví\¥d ´j™Ã©~°«Ø’”ö”œ‘¥ä_3•[�¥úyýUçZç3³¢$‹ÃøLQ?šê'(K‘B>£áÆIñÆY-–’qcùy�¥¬#bRL¹ëHº<‰øt7ÁZ/™2Jñ%ˆíú4T­%ñ/åsêg–~,ý£”¿4ä  ¸$‹Ö¦Xšþ챎KfF¢X­2éVÒ0x›˜!P£t;{d¯«¶�c©~=!S¥%c„Œ5Ýd²3�¸”q;kËᤘ5¯@DQªÏZ³U%åJíôíJNñg¬äJ 77×Ä"?d¢R}þH§y3PÖÆÆ'oà³ÿp¹‰ÑkIËË*M¬Ë^DüÊ$±¼Ê]Â…ÊÐìúµ^-‡´Ê씕H+•/WU®Å9ý= 锹#ü}8í ´„Bí— ¬ŠcÌ*è�ÜÚÉ4Cå<ÐK!‚6꼡r…Ö £­=j¹RGè\ü�l¼áìM¬f8h1o…�ÀYZèð0dÍîAPyã¦ÔÙÄáŽøg0fq�RÀW7ÃUÔyp}s"–åq”{D$&yäb8L];Rwòˆ1¤­ØÒÍ)7©!6&˾ÿŸÙ³œÝ\¹*7EçߤÞ@ø»©xÜS4ƒ�Tè"�…w“S*Ï.p °q†i�IQ�@ ûm/`>âÆ 7‘°�Â�݈Ì»;&§g&ǧ¦&ff´…å¯V|ØÒÛ˧úÕôìì(ùǹœ•»dqœ±“˜ìã°F\…™¹:ÙÑBÙ±ðY�—glîì2Ÿ™Á H}A2ŽmX¸¨%+™Ôc dÒÚ¦h Û¸r7®2 \Ù¯>ÁåJSÓX¥X«wàDC(¹F !»I ­­lyàÜýrÆô—ÝñÜnAØëŽÿ­Š”0œFBÑ“<åGÎ0F*â&é—ŸÌG¼ç¿¹S R—¤bÀ�#�„#C‡UÙc)PÌh¨Æa­JV>ù#AXQ¸°yT1Séå5¹5ò9bPÕÖ5'ZÅÉôC1©vIBYÅZtwBr²p¡È¡È•Y�‹ßôììm/J¸]ó;ª¨fNëêÆ*©¶ÝJ£ï†�(NZâ þ‹>lo²–Ð4ÍVÀã0\,)Þ‡°ò¼‰Ï>‡wqíìÜðé‰Tp”¾Þ¯ŒzÅÞÙ¶Žá‘Ñ l*³ª°R[ ›ì1jW‡AŽOWØOLŽq?ÒÄ”+ͬgãÎ¥I‡âÅž.ÎÊ«µyå1Æ;Û¹»“Ððé1¯ÿôlV,³�œóJvLn’òô5žVPä4˜Žñ‰©Ó4a|"öšRº®3\:ÑÞêéþT¥&cú q�84:::ÁQS-EÎÑ �)A[Äkô’XêÞ”˶Ê5 Ý[Œë‰ïô–ï–ò-Y¶c·ŽõqBMþ²ú�övr”d{û\÷üÐÈK1[z–Ú«S—Ÿ¹´�OoßÔÉ¡S��½]]k;‹q-žÃ¯ i]Ac'g§¶·aðñJ�¹V�&1W… b²¸ØÏ‰[{{º*ƒki0À¥ |ºæfO�ŽLMéÎWI²nT£ˆ³l`Çü’·9±žÕH,u·5!Xp0Iœ¢mÄ[S™*I˜ .@ù`ÀýF®�›<ø™‚^bE¾5Z–{�Ö¤Øh?¢²':´T^ñ’VíhªL3lk& HðšR°´„@ˆ§�à7ÐÍ'õ§’£`ÁÔˆ¥°`iÞtVˆÅU¡ôwTÉ<˜­­ PÌ2ȺD8‹ðqy%ºOìEÿ™ºêÈ1 �á/Ëï\ká‘� .¸K¸Â‚‰\S$iû¬ƒäs0õ‡:6:ÔÈ» :ù‡ç±Ä"ÿ‡ö\‘&^$>&pÁ÷lxu·=ðƒ‚Áʳã- #!¸ÌR¹“ÿxdfóÀg|&1Ö YV"»®ã•Tµêp׉–ÉuÑp}µ‘è‰ô{½˜>xt?G)n�…Ô“Ý‘1À‚ry�Ñm+êÒé¹/Ï�º;SlH:ålXÙPuB)‚¡S%‚Ä‘Ù"ôÎ�.b"ÎÄÌ8ž%ä#f]±I¨£ SSQ±C…ŒS´Cø@ E'D�ÎZ*Ÿê¡DãëñLj´ P¤ÿYþªhâ¦JKÑ:‹9®sN �t€:ŠøÅëh‡ õ� ¤[%PîÞ,�Æñ±*6wu¶cØ:sé^"Tèç6§’³xÕ lÝrúôÄðÐ8~GÊñÊ��BVµyU—B/`þãúÊÆÅ¡±ñ‰éFn­á,¤¶ìr/¦Ø^G ?†FN�àg;=åE(­Ý]ëË�áH†;Õðpadlrnj½ŽŽ¶îîŽÕL]55�Û¶ô57� �ÍLMƒP� ÆÈqë҆׾âʉ¡…+*yïr�ìšo”~^|¹—üXåí]¶dLõôÒ–®N¯:=cà”�~M«gLœt`�mݾ†Ÿx  ww÷bÕ§¥gÚ8ukßPSGžÅmÛ>pÁ…»±¢ž11 Z Å�}[‡GG±‡VÒ¯Õœ3=7ω�FæÀr“Rtxpnp$Ês’ñ5Ôy*ÃÎÈÄ­bHSB>‡…á�É!j²+ šá­£ŸæóЏëpIèG[›1¡‘i¨–F°û0š¸4E–ŠÛ˜+rBÓv]„Ý‘ Ž÷–Uû<ÝÔÀKb䛜�畲|½�tQ65¼©¢žQO¬‘Nåa2dN§t)>ÂTz.\v=`œWžÝÉ1‡¿nü¦hÀ_ëBM¼KA¼XeÉ3�w„O\=Ô1P‹�dp†è )ɱä“66QÄA…’“dƒŠ•#e-¼úï:Î acÓò"ûR®%BÚ”áA¥ªÿø Ü:@�¼â¿ßõpþb€¾cªÇ§¹Þù*¸ IÿÉi¢³á+1“›‚nöEšóÎßfý¨ÔLµRéCGúµF"]æPTvŠq‰øQŸ)~$ˆâp§†CYPxù!F{]:ý‘ �ÍmäYH§ ax ëO�äˆÊ A:©Ääâ³z�¼6›š¢1& ¤ È840ø’Ý�H',^0‡™]qDÿHù‘ÇèxÍØ_ е²ÅØy%xÒP <€æ°<¯1'ZŒÙL¤ÉM­¸)‹0â¥`+v7?g™Ýíoßœ½ã¥+ï<ÅŠG%)‡/á…¦LõŠv9ºQ—­ Õ§4œßS6šŒ±'šÂÕÞMX�¸B—”¥@ª«¿’]ÿ­ìØ�ìÔ±¬G¶ûNÙ…{³ŸüeNÙLiÖöxÊf¦²¿“]û�ìÚoš~`w¶÷dzÿrיּ5eïîö£ÁÁiœÔzz:š[ª¬~”~à:?û¯Éö_›uõf{"»ô®ÙE—gm%‹µØ1šÐ0n†Å=*A.}Ÿ<œÝô½ìÀµÙ-×`ÇÊ.½[v§»i«êÝšÐ�[z{€084ßÕÞÞÓUe«¢ßûš8tcvä–¬o[vñ]2¬]{öeÉÝLì5öõõògxx+OO·× W*ÀÅ,oÌnüvv÷Åáž;{U(Øv�d¡·ÇkµO ͳû•{“ÝaH�¤ã¨è$;˜cʤüDyBÈÃ…û|ÜuI‘í»{þ¼¤&C'²ïþGvøæìà ÙäxvÑ�m ßä-we >ùIƒë% ϸáj‘¸lé[bwƒèŽßšÝzC©¿ÆF죭»²ûÙìò{d=ý R[—ÈUá'‡Ÿ?€±›¾+Ú¯ùzÖÙ“Ýåg̾óâ„.°Ýßß;11™'_íKî¶m-Õv·õôEW×øÄÄY šÝ´qA#Q–C­øËš‰dŽÁJ§üc›G~²‡Hl Qº ãX y¢ä-¤ˆ£NÈ" xOÊÒðæ§n:ªÇ'ùî@X¡™¯H$ií‘Ó—,Qˆ9Îâ‚•XD¤áoâ)$1£•ާ”!µ|(6ÕØQݨ4pS�–è‰`O0J§�JJsÂDF¤ã#P’ÒP(é©>H…p5"‰•°ôü³aÚï\Ÿ 9\ø`™JȼQ8ÝÆQ(ü¬‡:–` fËù54º¿ZÒ‘Ô¥1¾uu©?é¹­œâ5ORS�¢– ³þc30ÀyA¸êçãŠS' ²›órWV<Œ,/f ,ë3Ô 2áá¼ü†Åp]WXdìÁ°Ì¤‰Gy‹“»ä?þÅŸ‘~MË}çeƒ~¤*å�‹¬ž³ÛŠ ‚)�^bÑréHLÚ�Æ`å¨*çW;ÑZëó‡ŸXð RhwM!¢.�þð÷øº…g!�B{Ì%Ê®N(þ⇆1uU)16ô£P��gafâåÕ£æÁÛKí ¾&Õ3Ï)s7¼ “‡B1si/Óž§`L˜ç<7´µFn’ÄãCE�½§ ‡´·)š1#eØ“H “T’2L•×–¹ž91\嬩Ó$§�iïVY剂9n‘7`ÅL²B¸ì'²ç¿Oû¦·ƒ®$ê¡øò¡aúIy–p¨Ø.¡…ÑAšrR6-[ Ç%©>›†“Ò;;Ú*v7¬Tÿ÷õÙ»ÿ~eÿ»‹÷e�znvÏß xÅšEä‡Þœ=ça+¼îèÊû‚ìw"xÛÕÝ>33ÕÙ±Ôîe�Ï®úò Ù‰úã§e�xVÚYæ˜>ø¦•!üÒ}²g¼AS`„Þî®";mÛÛ+‰?öŽì ÏZ¹ù$¹ìÑÏÏ~î×y‹d炞Υv·¿5{Ù²ñÑ Ìü ëճߙݹd&cG$» ‡Æfgfu‚€ùç iœÛ Á¹™Û[éYŒ�yØ}©�¨zÔç»_óËfMÀøøê§g}GMtåçCžž=ð©¹1ÑøçüIöÍ/Vð„æ–M~oürö¿Xy‹‘:yýßTb–?Ýï!Ù£Ÿ§%.œ%—¿/Åp¾áŸß7;u¼6?_øþì§~%Å··sËâ*C¦œsË–>¶%—eÙºû¢£�k!/†õ¡šþ™žâöF6#Q¢N¼i~ƒ#‘ì1R)^ë9hÁšhí¢RÁYƒ,ØU 7²0tn�?’D6&³K9Þå{’Òa=qü»;›=-Â$øíégdàôLŠuÅÞ_õ2�"u³ƒa4¤•¯Â e/ib>¶œŒ@ˆ•Ù쉞róìv½z"ì–P%�R Ü[ ‚ôÆŒËuƒf¨”X4ž ò3*; 5Qü´^ò¹Ô§&�$þ ÞìtoK�¥×¿êXŠ 'F#™^�a%/#{�'†qˆH%Ý%êZ ¥þ«Ž�sÇ€ +>’˜ì,–GdhDÇ©/VqdmL,0@?ç^tÂ1ßÛ�¬8¹X…~(=l/Q’øM2çy¯âõÏí‰� ŒD†#^Þê¾£úÈüA«ó62C]:=�ú㇤*N%&²~éT5PV•Q]•Ò‚°´§©©_…§Ž¥$‘r˜"‰¿‰£xù`øj™“’Œ €¦gšJz­… ;Þ È×ÐNg‘�9-”�g3'Š›�P,±�Y6MJ¥ú½4\piö·oÉÞú5ín«ÛžÞ¥fDK¢œ°a&$Z†õ¦U!~%ÄR õ L�^zÿh±ìq6:”=åþÙ‹·ªáé–벿üýìyËkP[Çñ‘ì¹[ÙîFR¬Qþ#R.&ž¾¾ž¶ö’\63�½èÑÙƒ~zU»ÙÞþâìA?¥oTæ®­}دp+‡÷¿6»ÏÎUín¤úòDz߿Kö±w–3dâ#ÌŽÏüãU›Oì\�»Wö®—¥äÌž[zz±ý•²ŸÌžþ»Ù³º²Ý�DÜžAßõRíúº±|r�ú;Of12ƒZ{—^Ä™n]ÙØ[v:Ë0€Ë%æÎnø²U‡Ó§²ÇÜs-»‰ßúÂìá¿��8T�o­çé*§³o|>ûÝ;ŸÁ5{àÝ+GÔ­ý#oËü³+ØÝHŒ1î‘¿*¶Ë_é©•@ñ¶bwÛx_T€¯ÕlØäކ¸67ñ²³‰ŒJúÝVº&J€­)[cÍÒûLFQÌŒçÙÌ +V}³×'ð]æ^!¼GÆïrØp�”\&Þa  Ub§|‹ ˜ë‡¦É�•UãJŒ,¨Ô:ßweARa¤àEÔ:g=*¼Šh_Y ©CÕàWEO\.�’/ìÐ2Në.c‹¬š¦i•€0W4Ä«4T?æÌt±6˜²åÎ@ÁVêæ`V¼•h©WÇ£€+–ð• ¿zhgÆqÚ@DŒÑDÝnƒ¨ ¬¨Zµ9ø¢êØl Àú¸¬œ;çC‚„Ý—<ƒåÔ°Ï”Á•„áÙúã4•¬ëá|Æ€S�sœçGêD�BL–ôżô1k}L—N°Î»¼��l§nO ÄÉ{84`/U¦)UeéHœ§[•qœš\þD^âH”²êu{V¿^öm€måÊÒqýy]:½ 0þÃ^Ć¥SSò¹-Q,–F·$È"¿¢B)Ùꃂ—ëÊ0+#‡7|»äb"6éLªcêˆÀ@ÞÀ¼…˜Ê•–%,x"ÌV6ëM£NuªƒL\¨]¦$Ö¶äšÎf”­k7¿s"¾jœÄ°Î9‘¢„$0™­ZE £Úôp=1”F›á¿þ¶{ãòm€åµã5}ðxÊñ^P"x76±‚rL!´ƒdº¯ØC`G7>Ú>mWan®©©P¹U`v&ûÓÿ–]» ó2ñSÃoøD†Ñ-ØS.Ø›=ô¯òˆÊƵså'Ù 5þ_ø£ýòof´—[ «7!¾îÙ^_É›ž(}äÔÛ 5yÈÏee¬%¾øïÙ‹[ k&¥Tןú<óAÙ©£Ù=eIâÿøDöÂG/‰Á³m·›g¯»jI+^ñ”ì§~9»ëÏ“¸â08~:ûƒ»Öš�Øaºë"±Z�„W<5;²?{Ê«ÈÎÜßÛ1=1ƵxG¶áÀ±Ð�ž ›5á¬I¿¥À¾×'¾$ã¼³]—–bøsßg».öçå?íw l±|êo/i2�'Ww_vxÿ’-Ì KèË?ZÊÈϯ·2È%�”Tj¸Úº#;yt |Ò`>ûç—gO,+k2ùóKY‰{„— ë)Ràc_¸$ÙÚ?Ϊ/‚§Üõ¡šãá&g§]7p$ò�õK –V¡u�DÒçÕ–ÂÛKß²Äýò Ù††/3ð Šà²µ9�Œ] ÀH‘̰�ã&­iTΰ±a+à L á(�†p!†UrE)yÃR%,½x¢rQ S²¿ I¿5jºãà+ÞÃqäÍÖÊZÊtµ„ƒÓrƒŒ•öxN/F\w°=±F: F‹Ëâ ÿ½<¶g“‚©Éš‘eˆ·yØÑ¬.¥Ë.™ÿìñQz'òJÿÁ©èH¿ƒçGÃë_u ä€@\“C8�Žù‘èHÒwö•’Êtć Âø!ê•c±þ°YÀK™ÉÓn,KÃz#£+‘$Ä ‡„-Bœ²?æ" 98ÝfÕ¤g“1 í>$Äy mÎç1ÕRLíÎs²‹eÒ Þà »ÉU©ƒÛ0®èúHac•‘/Œ Zfg�è<†rtæ†Ë¬g¸#aÁ5„…ÔÛuéôŽÔwçi]e•Na?ˆ! -(°*³Pýdaam€ƒoRŒõ%òƒ3Myúáw0/Ù–ÙIó«,€ :I‘Õ* �o¤b�£•��ˆ&©óÙ}ß©°Ð~1a1 †âhšõω� …ßeÑÁ¦=P­‘ÿQ]¶w1ߖö‰r’ª¿â ÕZ|¡áÚ>ÝOUÀ·Ô½À3-³�pü¨•†m 9Ë8a­rd›«ínXRØÉùbn-Ë<²í…�*ímäçëþ:ûµß]Ë5ï7œýñÓ=�ÀqcoynöžWúœÂߘLoåß8£}${×ßW~b*zλ³Ÿ»U4’=¤oy^ö¾WWà÷ö¯/1S»5ûÛVðôg/Ëî÷'G˜Í>ñÏÙóUI€ýë¿ÿ^¶ã¢JÌ^[yþÕûeöòì‚;•b0f}ò½Kà¿ýEÙ‹þo%=Oo}þ»Û=ÿWöäWz¸óÙMßÑÞ—c˜¶Üÿáéø6ŽŠëìlŸššœ�›nš/´Ä©Aün¬››�ki)û¸ÕØ �ÌÐêM –y:[µí¹ï¶³ å¶C7e/ÿ³ì JiuäP¿t�ܳޞñ!{¾Éôç꥔ùŸ×>#ôá)¯ÔØÑ]ŠWwöʧV°AÏþÉ_g]}KrÕü ÇÙ[ú3¿fŸâû÷µOë?˜à¸ö÷w›m¿°&Óª?϶/*×�ê©iÂÖ¶ÒHÔÚÅ(ߨH”)ÀÇa¨ –Åà�X^Ê.¨U0¾`R²"‡=lW«;/̧X¼+X ´S`^aßM^oR¿…ç?ù©0-"¸Ÿox «žoJYˆ ØKr‚4)º”€ ¦�„ øY k}󅿨˙õĤS+~±O–’ÃÐ"^ÚFkM°$²W«¢v:7kÏÎcžä .çàc6ìb(ç&Œh“ÍL5õPÇÀ2 H] 4®*šsÑŽaÇ[�E‡YH-º·AS2ØÔ0Û˜Íp–Û·9PÐUºz¨c`³1õAbž*õq÷MœK90µBˆzÌ>E¾Äñ×%l`\�ÍTç9PD`ÌENa^hš4æþ"¶šÅyvDÌÌeÜq¤V£*á'&áó¼a? Õc„­c$r2R çØËB‡Î"Pý( §ÞFFs]:­“Á&bଥÓÅ'²á–¥.ÕÈQnø·Á†°EÍfó³\¢×´0ëR‚BlÁóÒJŠ –Ÿ¸+Ïf0A¡p¡Kñ匄‹2²>± í“7ä°ž|'=’”èaFÅ ‡Ræ!n”…êÌ ÜD\vÂ@côÑ�ΉIç¤Q!¼[÷z±Ü�Žõ,ÿÄü¡&ûÚgJ?Ió¼÷VìnÄbú”½à}•ôŸýWÐòpëõæ¡Ögücöû�¯ØÝÈ®þçg¯øK }ÿëK~ÖüÀ3ñÝWg?ÿ%[*~™<“#�ôo{AM¦Už[_¬ vé l;C#ÓãͬR3≞K¶¡‘¨V„‰([DuGÚVa‚é0v“‹Y…Ðv áÑ‚\ž�sanb÷B8Ÿ%v™—â «©_ð>Aè3¬5.dvY ä+À æâBÀ€Q’æ*—‚he‚| ^[gNŒ ±dl°/–9"N¾ƒ÷Ù<~yÁн}4J<=q¹tꥦážÇé‡�Üۨߞí§ņù"ï¨mZWPÀ±æÌZÃz©ªê8‹úÛxB¼Š¦‹ßÔ?u ,À–7HZg”ÄH…¤ã†]‰H¢á}�b:[1Q™ã.‘Û2€u2«cà0 §Š{íaBnÁ»dir1x^P`HÈxM…Ô\âÜuR<¿1à ¥ˆîñÒ®)ƒ‰‰M†Ã”ª�Y /b£P$ *°ûëŸÛ Qp.Î4‘=˜E½˜S�¿èÜó›2ëÕÛ Ä€®K§ujß4 œµtªU �O·H;æ Y `¨šÓT�{œfÔC­Òè¦ÝÍôñÍÛ�4¬–æ+¡A㤠8›¹<ÓZä Ôr- P@ …jd¨¸ñ‹´P3APŸ­�’Ó%åYdDÙ,Þ…™ééÑܯ'^­ç‹ÌúEQ½˜†ÅKTÓoƒ•×K�-bé1úHY®/i–ju{%™é#\±ÔP‚»Vk{éUþ‡Û*ÿ¼Ê1�#ºV ¿pïì鯭5á!K]¥ØB˜‡o~¡b5#ò/ÿ1ÌŸ¿/=Üë2Œzyxé²É±Ò/öÊ gD=êÙÙÝþKž°ò€o׫n®øÜ³o}©ô§¹ê¾(†·]%g<Ýó—,wlƒýõß˸Í3¯þ‹ü1»û¯dzjågþJÁ fÁpLûJi§gkk+ˆ3ܶ0?ʘ{ö4 K;ÓÓ³CC£³¾áÅ–‡ò•²y„DþïpÄÈx�ÿæ�¢ëÿù™JZLc¿õ°ÊÏê'lµà!#ƒùã XfÙëZˆyÌó*qïÝ’}Ç•˞έ/”nÏX½vÒÚ&|�æ]@¦nx P´•fvw´"Üdò¶w§ºø!¦Ý Ôöúwb’P8˜_(p/R�˜tÅà š‚=‡�_2CÒáÇÐ3�Œ;ª:EÕéhS1 yͱÈó[(°”+ßRØs¾àÎ0N€úpÁäK)BMS+H™·u·©²iÀè³¼çå8éÇŒ|„_ÓvýŽ@Ï\º=É’XÅ(æÛ|âÞ´*Õ­%á¬4V‰(›Lˆôs‹Ù¬NZÃaP׿ºq}MèøLòµg»ÐßuéôÚ“çKµ!¨�K§êd!�ª¡D¡Úé_ Œ³�™PÐ ñ€Ó]ž�Bè~n]òÄ4e d¹@A—Y©t;Aœ™œµXLPTU+szRC4/e)ªèæ¡p‚&˜YÄ€Ãdê½”lJþ™…º1Õ™d}s¢¥„(8úSž:zj4u#ÍÌìâôÌØøä,tRÄäää©á‘þ-½[ûÖÜšG^K0„:JÅò…2Š©Hb£kø†€bÎ^ Æ¿�çû ÍÎÜ)|þßJüÁ=­ÊÛˆ9OM ~é¾Qw]”]t¹Nm–Ìd²4pifyo#Š-ïèBSàûXŽ KO·¼”j/-LZwù™RtÜ\¤€r=þÌ%gɸ.»sœqö�+ó,ZÇpp+•kè#¯'.`\/Àu)\óõì'ÙG63bKÖ7Ž'{éã²'¼´Öþˆ7Ö¿Þ˜µvHRլ◇ǼÀ»GW lºüƒ'fozné%7{‚Ò¨^[sKkK¦öÉÁž‹l�Ó ³À~§Ñ±©™™é&G�ú¾¡S“SC§Nuvvvuvà.„–B±¯··„¨}?U)œÍ¡^–ÝýW+1鉭¸÷yPº+¶öÕÚ¿õª�gìZå£_ä*-åæû^«=kàs¸[ !E3"+ÝMg½ö¯*4îçœfj€ä?Ϲ/€tüäàôÌŒt»8?99~úÔpWWo{GW£}°833;4|úÀÁÍí…¶¶ÖBÜRƒ r×EÖ5IßR"c¸Â|²øu)W+kƒMÄ&‡”é€k1n:² bÉÊ ´Iij"ªì�Î�Áj_J®qÊï©zYC&ÀÐÅÏdª’uÈ‚]²ñ0V™&±ÁÈ&hÙ$Ô ø&ö„¢ðí<ßRwôD9ïnyÁ­-pM=±F:¥)ÀôÐ F¬0)ÁÙ�<­¬¿ T� XßËÙœYHf+Ò'ð%&É<@fߨ877SXÑÖnªzøQÄ$�|Íóå@)͹A^Сôã3!%â—äWJžÕ)*!§þ½Y€¢‚ÑAu ¢�ŸʼŒ.æŠx#³”`!Ë`„<êŒÍ‹:‹Û¬®Ød8ölˆ¸i~³gí<þ¦©ÎÙpë�/»t ¿ßäØpïý€ì/)ee'éÃfô´wÝ�7 �j.4ò)6F†Æ8…!ìžR ö ]�ÅÖB[kKsK‹ÕZ¥`¥ñkÝ#Q“—<‚˜Àø:~00xU° ¾ã…ì-"ø+ºüOçû­� Bâ«éÙº†±Œ\‰§™ˆ(Á°à€ºf”Ó¡ë0QŽ –‡~WN¿²|¾e]0WÞxÍ£Ô_Ç 8i$µ‚¢ÄlëÕ�AÒ§Š–«gM³òc“X„’Œå[ç‚§`.ÄÈh^«QtS>»yy›cé¿…BžÎ¡¬Î…Çæ¦Bsa~f¢)¿38PÿúÇÀÂÌn% \A B�’š„UÇ_‡dæjaZÒ†ö´ó.²xX˜Ÿ®SÔ�8mróáQœd‰S¯k:ÂN&7®yZŒUº‚—£Ën!F™»ÜþÏ©.F7Ô r“ûcóÀ9û\÷wÖsµŠÎÃÓ–N+„®äU[DÚ¥»–_Ñ·Ä0a7 lݲÿ@Ýô¶yý±AHý[š œ¯È.¢¬¨:ºÊHdñ”É¥ŠßFz‘…S4¦'ë²Çq~ÇJNáˆÉJ­uéôŽÕ}çamÏN:…ö¼­Œ3�¸Ý¯têµZ"»;äNÜÿÂ,£‰à£ªž>oð5 WЍap2.f4a^Ïá0n#ÆzúqXÚ%É>”D•4€7J`nSÙS�¤^$ñ| Š¥&ªªè”$Õ\GeÎ<'�3”bm|Ñ ~«òÑÕ‚*×ü M…Öf/l+4LÏ65·µ–|ÄlûÔY9±`ÍB(Ãtƒo5We›%Ø:Åj›¤úfÀô†Xħ0‹TÂv§è©©©ÁÁ¡‘‘±Ócãý[ú×W±0ÏEþé© €ÙJÈ÷Z’S˜¯ÜÞ@ÌŽ=¥èi»ÆFOSý‘‘ÓÝ]¶—€/Uʦ«ìª/æqÙ®KÒ3g¤  =y¼µ¥06vºË–Òu¨å&»ùû•Œ÷þ£Šé�XŒVàøØ;‰gÖ/ÞÇ#Û–ü¿ªÃ#¥úWí3`óÀUžåP(x�äôìL±y® KºgzÓsa–n,¶7·.´â7Ù<ë­ \w0¡òûÄ?Ë… sóÍÍdɲ‹ïâQn×|£ ;˾þ9?\å~6„+,ò‹"°JŽŸË¿VNÁÑ|,˜‡nÌ莛¾—áHXMNËÕÄ”·šêMvjèØ¡C;¶mÅÞZ2 W_­°ÓÛfôö4î¾(¶´´´›9±¡¥˜á[ÈèYœõh3Æ-2¯¡¥Ph.¶‚7X#ŒÁGÀ`¸ž‘¾çä MM²Âà°B‹X0 ÍI¤rðÂYàŽó C¡h·e†, Sõ‚fä­Òˆß®fÀÕdŸ~àŒážç1mÀŠ© y¹ÙŒ…" G¬S”× óýOváPkMƒ¬xXHø Ä­˜Æ‚1ïòmœ±ºfœIO\*�–+ ªLiphÊöz0&f ™9¥‡ý §Å-j(^Óköwèãf•y—N†³�ÐÁÃVïÖÆ…ññ±¶Ž•ÌêÖ¾~´009>ÖÑ0Ïåßt!ÿ§ù\²‘Îi *ã%>'òlj…o4Î×)*°TÿÚ AÎAW¸]C’Ìð´‰á…p2˜£,²D‰Î%!ûÁ68‘eu· ±é ìÙÆùæÖ§Xgv/ w¥IÆÃb˜j‹“mðcíU^3ÏÓ³ûöîeÅawÓëVxF  Mûö]VlkE‰t$*n­:ch:péfz)³¥.{œËwä1ÀÚZZ›PÒCrÀ:Q—NïÈ]z;×ý¤SU ˜�âj­<¨W)$„…�_Î/ñ.fœx§úéëddæ’®—,v‘]ó™ìÏ-ªX¤$ŒÿN^°>^ª$>*qÑȘ΢¾¢ )+³œ•SÆ^לú ÅÚ(X1À,=VîÎ-œÒÀòVjüâb[CÖÜØºPÌZ盳"S0�)8S�€MýSkžp£fN”�˜@š�ñI:»Y0QÍ9\»Œd;×.X¨ä ƒ§†{{zûûÎ莗Uïd¯h©Ç¸Õ¾ÙyZʦ6ºrôYs‘…>oòQ¥ ﺸ’|ÙtuòH%rûžôL¦Ø-«j]$†Û ¼–ƒ°ó¢ø_؉òÀÅ/û`öäûç•6NòyõÓ3.¸ÿ#²üyå Õ˜{ªm^+=‘R±³b_mM�Æ–Ís­Ö…X ñ]ï‹¶ì=-lG ë4Á­Á„ý×ôm–MNOãþæ_÷Ùì¯ÿÀÛKk†¿¾ÉŸ¾=-»kÉ ¯B5YÊ?1¤ê\Ÿ.Býç—gzkq–埛ŸçBJL]Œþ9ÖÕ;Lo:“Ç%é7£/XÁl.4ZØPŠ®©½™ýÙ º!›a'(]¥„!”µ,̹¬Èx¢‚¯ll$ÒÍ|"¹1Q&]aÀ—üe`ðÃ]�XEcç�P c]¦ )Â<ÉÌÅ'±]~×”)ÚßÒ`” 7LP,G»š@�£­. ROëŠe8Øe䃛I+ÿ€[ª®•P]Ø€žX-�ÚÀ¨Z©þ%ø‡ãŽR…ùf^ôŽ+%þ×ägc£þ 5´�‘Ô\ã{q½Š‹&™ç:[:·oøÖw¿·8¿³½«|Mo »þõ#ˆ�‰ÑÓ£§Ž\|·Ë»ºÚŠEl³� dS¸£È_ðåF\P°öÊ€h€Ç>ÆwÖý:;:wnïÿæw®YXØÕÑY§¨A"ÚÌ&C�c§Ž\r·»tuuဠS…‹Iv:Z#%@›’gâꬲÀeÂñãféµ³»mûöþ«¾{M�ÅmfÇœ3¬Ô³—Þõ.�míÌNÈs¬ÁÉHì[¦?y�‹ëöoD¦yZE† Ÿ©®ØÖÞ±u``Ïž=·ÜrËÂBÝúvÎ]²È{ö\40°½“c_š¼kаêH´×PL‰"g5tt´mØvä;ß[ìßQ—=1?T_㸭 ¹ø®—wvtàø¦^î¢|]:ý¡êåÛ²1ç"�*¤ÄûÑ‘"B–E’ào‹z"VtÍ¢•Å9Ý*oª¨R<u_ÀÖ0¯ó[ˆÃ1_áØÁY²¨U¬©ªŸ) ›Â)LÊÏJ,2?FBßc“¡\}&PáÐaCÇãOè›Qn#]÷œˆ°ð¸MË¢^éoæl¤¨öbÖ·Eö²ñ—8æØ…Æ™Ù¹†’• \+¸ÐIó”»hÞËäºË^ ºMµ\€ø§2Å)Q $ÚØ¸ôµ‰Ñ%Å•æ±�[P‹3Ó3£££Ýí•Q—dXí‡lF^³v¨9d©ìŠå=Ÿó‹Í…¦Vœ½š4½•ÀtTÝ 05^Š,¶V )»oƒ ú½Pä§ðe”!TiC5þY¿ò[^íúOªRV�Ooò*Ø/|0{Ñ*wAäØÕ$^ÏOnŠàöUzK9+6Lƒ± AêÁcOe,èóTà@ÿÆÅV{”´n©…DIVÀ?©qqrzª«£½äœ_òÁìÍÏÕ:VÓÆ¼Vì¨åÃ¥UGãå/—?pD)7Éç<4ûè;—§9›˜ò…˜ÛèæÆæ"ž¥UÝ]ÕYk—¼}Ao0< Y¡¹©¥­¥½­¹•qZXàžbŽcŸQüÞà7¸æ1þ‚ÑÀO»®|›dN°'›Aw9xÐé îçymi$Âð“£�òŒd’á‹ € Tÿ³sÁaE΃£¹8¨Á<Å£:ô-®W…`¸"UNWÊŒ58Rm~�dQâbÃ,äƒ) À³¨¢S6à8Í`1óò,*EÚŒ\ÌŠr jl£üÏ2‚G²aŠÀ\ 9ÊNÃ@pzâRé4ì¨ –Œ Dipf"à/5qŠ(Õƒ�X¢Ùb-ÜXkÉ{øqT”ÌQ[»…wvlº!ëêè¸p×®ÁÁ“ß¿þšæöžÞ­HÒ]ëZ\=ü°`€Ùnlltøä±é‰á}—]¶k÷ÎöîŽ8ÛZwç[z†²[AaAlP˜\§Õ#‘þt~ ¤ñ ˆFN-é¸pç®Á“ƒ×!{tÔe�Mï¨Û`õßwÙ^xg§»xºÉˆ^—NoŸ®¹ƒ–šSÔ¹H§°Ÿ$Á†~„êÇs+Õ*·:õ¨ø)çB§j¨)µr.b¥ß´²§˜Ò_24Yò¬:`¿|M´eé4&,― ³,ôF2 §9Þ¸¦MJuÙuωÔSµ0$rKà)<303W0bH@ Š(ÅóÉY0_l*ÆIåèƒTmí @(]ßÌäÑêX¢M\ت©ÐÆ%~Ï>6Þ‰C‡s!KaKù,¶ôcPœh†˜‡ûOK±00пmëÖ ÚÝ„E[b**•³òŸê]¨¤À�«T zaß(b8Óé+ÇÊÉÃ8=[KϽ‘%ý(Ûã¤�ÁYiÀ …8§ “€=›zºÉ Úv=F«bÂÀÒ³ÛØ÷øÈ¿Ë¸Xö›Ÿ·!ܦŠÝpyxþ£<€ï‘Ï^þ¦&ÆAE b/~ìÊv·‹÷e—ýTÆ%wýùìÇ~6{ÍÓ³ym �~NO¦H…,YÞ»ëé5¥”Õ�U½×x@µ)}$,ž�Mž¦é¶X¤Róà�êÁÀx\[¢Ý-·#‘ñÞ¢ Žt3À\¿ŽˆÔË2ú™W^bP2E2 iœc¯‰`ýe7£cIRFiè’aÀð'߸ËH¾b ILi©h~’”ßÒJð}yä:Ö”´üç9Š%‹e‘?ªï» 1¥JmsªÂ)Á�iMf»àz ÄQ®|Šp&=1@GJI¾œÇñDÀ™óÈôèyXŸñpxho $�3fT€?ÿ¯Ù-¥[LºÞ÷ªìñ/öù‚½~§ÐÞž=ìoË?6ðwvžý½Óœ ïö�äMz.A°B©ýü•£ZÁ™©D˜1R�pÉìJ\ú‘êb’’C©¥‡•“ $™`é�H™§åW¤ ô”^•þTâi¥ÙKà‰Gx¥gSµø.uƒc¬VŠÔÇÏø®*+mñ1h  '+�kn:::¶lßµ{×¶Ûºuycȇ¨l³Æ?ëB`ôÚ²„1)+%‘Ö`Û]ݗ칤­ÐÞßßüÄñÓã㜟@šÔQÔ!eðâc!œ(Î’k’E!©{à%Ò$ |Û¬  ã…@1ÿŸ½3�Ò4;ëú[õmµõžžÌš™ hd9âBD8Š•¸!Š ŒlFÅ#QLÂr !¸ ›hآǠ‘·#n ¸±$™ž™îžîžžÞ«ªkù–*¿ÿ}«¾ªšî™ff’tÈ}»úûÞ÷¾÷>ÏsŸûÜíÿ=÷Þdßf�‡�B0$ŠŒeúT.GPÜÍÔå¿÷¥ú.Z Iî5ÃÖÄ}ÊA03"é|Ɉ›Ê€’Š(\‰À@ÍziªRk¤@ˆ8~[–‰ç{/ÓË€­ $ F,òóRNF2Á‡GMDJÖïõ8Àüå,'¼ÿ¡ûî¹oaá0¿DÒÓS)ÓFҧ˰”£µEP~‰B ƒIÇ•³³K‡?òÈ+æû'N¿ôÌ¥µÕ$Ú)öÑês‡ÐT•EÏ G¨tMLÅ(…ÚVQ ÍNÌï6—{„³`ù_Þ—×15Û­’&_>økIò�»rM ;ššÁâÒÂ='O>øÀC /K‡ÑÔÐwbªmqDöÒFQÝ �y�€a3þ§R0†g½K÷ðáÃ�>ò0›I?þ²‹—/­­­ÒÔdXC¢Rx–°QY­‰­øÕÚ†jB;QýíúD*mT·Ç Ó—ß{â!Þî9É.Ô]NÖÉ@m×FK“t &Úwñ"Lb²NMŽ9Ô}øáùùÞÉÇž~æòêÍ›lóC¬�zAóQ$‹Åך˜rK�¶Øbç¥ó‰iï”)ßÜ>±Tp6€^\Z¼÷å'_ñÀ4àK‡±ÿ<‚uêèÔ:æWi¯sÛÞÿ:j¢-¡­¼%m ¹<Ú2ØK¾˜Fî¡£S›7Úº1¤VߍЯ�qDúGÃåtÅQT2oÁxiýÊnÙÑai,S ¥µÊÀ¥Lp�ÀÃ-—öÂÜË ˜•¯,)JPÖzJÀÑ…`>*q¡Ÿhaù¼}¢ô™Ò…9cX@A(Ä7¥L�ƒNç‰Î,×2Ò/ÃÛôö¾ÉøÔÂüÂÒaö…÷|ÌÝ$ô¼k›««ë›ÃM‚£pòѶPÑ!ËKÆ“èÑ�›Bo@W�ÞýþüÂ<箲ƒXV¨%æ[sþM ]`LóI»LŸ}cGõìÐg‡¼ú·L¡7vëÏ ¡œ‰÷µë×ù…‰qÅ4wOüÊ”À±œ»Êó+öœ½°sh)Ê9ÌV; ‡zü*5`éªÀ�×ÞÝúOÜ[ÂÚO–4>}Ú•¤;« …“8”¿¯|›îcýOMõ?ÿ}›êÁ�šžãàˆùÅiÈ�;Î%¸¹ìü²sQœ^:¤°08­�BÂ{¤;Çg9ôÅr™*:5Âzi@(jÆm³Kìì½Ç$Œÿ Œ÷>¼Ã¤ix¥ôËЮxkóîïž¾b×9Î^¸“ë¿îqÄ þÖ{vϵ8˜úâÙiH©>Óç=w”×'ünž™k,,,;vtqa0µ·Ý“4ˆñÀ=ï!°ïö¥(‹5œ†CæÝL]ü€v usC]?U5`}ù¥@pÇæ ìÜ/rË}i7,±X/í¨8mAé*�šhVãR…ÝX;\£5h“ÛTI*�‹ù´!�;õúN ÉHˆ¶>•~©m1x¦½å 4yk«ÐZšB#nD¸D@IDATñ²c'Ö¥åÜ7%Îú£MŽ‹ áÖiYàÛ³~Üã+&––)œTZüÂFBˆ¨ïЦP¡“2K�2áˆCÌò¬kd¹TŸ=a–và_²mK‘‚ðç„”º ¶ilçµÄh^(#=QÖ(“æ¨Ï�†ƒGîT ÑߤìMmÓíl�➸P:š×b¶TÑ!Òš ¸›K¡ªNmº  “'ü©@P2+(¾hBj”� ÄV�éoA4ˆ$䥳J€-y :'»ñí4sD�—•PÎTG(ÈHí!e*B«T<¾©&$´K&�¢”,QvP$óTßNg¾7X,¢Ã˜_`VÔ#¹…J«„p$P$ïèRÜY®òO›Pê³ýÇŽô^9wâäñ•նɤr‡�¼TITGª#6+ú l·8Ä -+ŠA’0àÃ¥àë*C ž8fL�‘|&!�?å…‹D„R¨Ä¤½H–´™˜=8¢~NWä»l¿á;~±�¬dšÂŠU›ÒÂÓj\ëN,ú)÷«R!Ì¢ø VM–ùèKmÔ ïmiRe-»¿R&d –ÎY>âj"FÏmý¹y/^<Ä®é‹èë-xô‚Îù]Ūî¯Çù-ˆß(xiû‚2KuÀ4%ŠŒæøüÑùE|1Žß\YoímÒÜU5µÒRæ#+3¶Bç@ò]Z›˜'*Ð6´w¾bD6i†hS¶"l�fÇ€–øÐ1b,˜Z”J@Їª&Ò´°‹ÊàÐÒâáEF¹ó¬w1?Ù÷�sã̪OËcŸ¢ "»÷-eIiÚb&³%+³uøø1\t�Ÿ8ºº|scƒ]H¨¤µ&jÝ¥�±‘·…Ó64scWðBúÄ´`³só½Å#óKìUÑP·è´»;¨‰˜% n6±ˆB™özGŽíÎ 8Éneuyssƒæ™ÿ�÷‰5°1àë%ì¡FU�ë -=´´´0϶R"l§SG§/aM¤ÍD©utJ—çPúŽG§ö†Ux®Ñ¯”n³ÔFö<2ó¡ýiw>c++:#§#0�œ-'c8þÁͺ·£Ìt¹V&!Øq�MqCb¬øðƒ÷;r¸ßï2Jiiüòoþ×ÏOéýæßÑÞïõuQúÐC÷œ8A‡ÕÆeqñÞcöÂj·£öR”çN�à 7h„htå§©anK9m 32òyÞaàT[JÂ:È´ÙÕ�‘óbïiÚ(/1h¢v¶ÇÌS'47™]¤i’„íîn4RFÎl˜•4°‡º|tÉ¥í"b['H–ªÌYqn+A3— ¬ž]¶ŸÈ$çeö[2C~Ì ²r'ͨ?À—!~ÚWR±NžÌti#imY.�DR²d2ó zA0h—mg^à<ñÀè%¡-´-Sa“q©æg‚’�•ÁÌöÙ‚ðâY¥ÒÆZ2QSW¾´"%Kj/Õˆ0êÚìÂ<ÿüöqÞNÜ×ÕÓ ÐRÒ}�†±0=ˆÅNQñ»Å,~‘R€‘É"¢fôí Z"úaXN„† w|iDUìÊ„·`f�B�¦^„þSNÄD éèhX~42Aýq‹•¤-º†"î’HÉ»lãN‘“µ‡ˆ¤B/³ã­>e(D‚{˜g$¿ª‰,#³2 MŠ•�Eg¶{ÄÌ„k&~r@ £Kµ‹«1z"%鑚‚Îð… ± ˆ7‚r„›ÅµNè‰gÜ¡'L.ûW££üdìéD$ë R�«8¯�Hs ídDAŒ$öËé·bÛ¾¡†ÚíEL”•º,ŽD•´é‰;‚ñc†Õzq@ƒ¼¤ z“A…JE&Z;5 %•NÞYìs.Mÿر#š¢%§i’•%èáJQ:V‹ŒYjæÀŠq,KŠ-j‰d^(w³¥úšE8Qå¯e—xTB ™Ò7*”å!ÍĽY>�žyim6ZôÄ¥,1ÑE‚ÕŽM…6�ŒVjB¤Ä¢FiQ8ßørdµ ¤mðY˜`+Fcˆü�mMòÌ‹ˆ /4O´Au‰çÈÉšmdRɺkelýYU£æmÔ'ê–�=Dsš™åâæS´�h� gwBj Ô‚e.ˆ6ݽƒ6®×Ÿ9f YË» k¢=èlö!Õ*0Ú+ÛóT3dÖŽ­ô%õÓvß'ó�ŠíoØmû@Ûm5KÕ§=‰ªLhÞ‰þ!ª‰€/³ýÎ,u†U�Œ’T*û_„-1SƆøÏN6ù*ÞÆÈ¶– ûÙŠ¹{âð±¶Wª5ñØ'²þÄýHì#èÔоEs§5k¤Ã±úQÚ1fzŠ¥y~o;~8¿§8*¸;úÄßð51mÉKß'Žöuf<Õt¶Ë&Æ©utZG§4ÛwÅ蔾Ï>Ñ0CHÒÙdÈÇÏIGfrv<Œ ™j0� –ý~Ùó:x°/6q>�brJ¬�ô‘xí{nyÁƒ¨D‰æ°Ó SÅJàNúÞ'‘’)Év¥¥v8šgÂa‡Ÿjïð2ãø´â·nÏ{› 7"@„•�U|ß&]m°$WÞuŒW·×'~Útá¿üÑæKß\ßp;rdϱ?÷ã; šæÕ¯Ñ…êö—Úµtü~ž‹åŠÿè»Ú8l£Æ®dŸú‡xÄÛîe'ŽíKûo™>²Höã>¥}Ü9¦³}|×Û›¯þvîá?¶G~‚~üïí;vàÓÿX›Wµ]:°³gCo%ÞÞ5•»;Ía$ŸñǦ«8¿ó� Ç5”ëZê;_páèƒr±óšë@ÛËc%˜gij…Á³¦Àe˧jÌMn5IõJ9ò‚$ºó•(Fßîü{o¸XÆû«¿Ø°{ݳ/¨,ìš¼ŒÛ½ºýÝÛfsmzÏÚîÒ`ïtßÛ½?ü¶½OÍsœ�€´¿ò î ÚÛí=|xšðÇÞ>½©üèOœ>Þîî¥( ~žr”/H0^ÔË¢xš´ˆMLÛÈhNŽ8“f·´ldlÇ,f¼ 6BHH5¤uÈD4D�1Û ÁÓ/]EÂŽá#)SôÚƒÕ6—ô`›Ò7ˆO3É?Ú±�h´i^‰Í‡�¤2¼$1dÓΙÇòK°òÈå+Ÿü½@c$*°-&„ùŸœ˜GZ/ó®Ü´¡/jžѽ£Ó¶©æ ö¶éHä}ù‚!àA:ŠH¤…³U�è£y,M/EFBýt:þÚ‚¢ÝE’‚Q ÎÑ O3‰w[6B sØâ˜p*ˆ‚ÐäÛéØMà” 6‹Ó¤'c¼ŒP�Óa¸[–Є£šAkaàòeK�ÀAHír[‘gÌ\ΔÕ!¡%ö¢li€1Ào!†àü*D%‡m@U>u(E‡­ e¶2ô#Pr¨Y 6žãBî¡"ÚIŠ-½ßöÐ_£šÎ¨˜°J& ¹–ñÖp¸ Y(}�724Œž áJbe#A¼çÒtHvãÊí¾[3=\¶·{BoJ`BþààÛîâæ�½øç¹2_¢r�?X%€4øò1nu´w§qDD_‚§’7ƒP¤ÒlP«14N ôæ pY¹Â‡¼Ù‘\öB¼‘É¥ÉdôŸ�’ö@KÔ9<íÇ�Ræd‚Åל†ÂgRx˜_ħ4ˆ“�‰“Ë)9™Ó‚”�Ak•ˆRá´föXƒ�E8„ƒ4#/M’�¨ I|Aá®fC'JG¾ÝŠfXÈdœ"Òúf|ORk¯•£Ò$†¹7CDátžøg%Û#�®k¨ß!æÈ—}$|&³È 쀕ŽÐ/%'çžÅ€fF0Y¨ÖDÚ©ÔDÆ"Äò¥SÛ±½51¾³´[ÖDZ–ímZ›Z)æpŸˆÁeâi½úõÖD'§W)} ×~Ú^¹×õ‰5ñÒ'RgÝ_Üjë Í¦¶ŽNëèôn�2b·Ï±Ol'"8Â;ÄM™M��¦bÜGeï¹wtJOËŸ}o¼¬í¡ü£ë¦«å\Q’;ô£ys‚àè�ÞèJÿÎp…±ãîè´ôs»BæEö‰´¤ô‰™Æ96°Otlô욈¤ª"âs^¼¦fÌ*¢ŽcT¹p2úÙMÿ›ç×}I9*¡Ûé�†VP-p�â·w•À—p2ÚâL$Mðû?O_0V–뛿¤ù¶vpó¯ O6ßþm¾~ߟœÞßæQ)¨Û¼ÜüÚÏn~çgM4øæ×7oÿ©f×£­Dôù�·N㈫T¼pÏѯz›g�–ëGþfóðÇ4¯{}û¸ûÅ>eßý¦Ý§æãÛt›¶ÏøãÍ÷½¹}õºùéx‹äàG?šÍÝJ¼ßôÉSRlúöï~¢}äDÑoøÂæ­?ÒÝ9¢¼Àgðë÷¬W%ðO|eyƒ–V×Ö‡kc6×Ã7R÷ìQŒƒÉ6A/&î+jf“öݱ%ãñ¸WŒ�µ®Ÿõy­s"¯Y üÎ_�;pý·Õüò/LÃX>¼{ÍÍïÞ6ïÿ?ûÖÏâ[ÇÚä]Œòßý³[/Sý?ÔüзM‰p·¶ºïñÀÃWýÞæïÿ§æU¯ÙŒ=ï²¼øšïhØ·îN®W´-+7××77) šþ@E2"rvý,Ć6%õÑW™áØv”Uöm0s¤5âGJd÷Výk1;FÀ4é÷ã�fl5*?ÔÇt["�ç.ˆÃ7#þIS P¶±AàñKâ5—emr>’'Sµü“‹š¬•ûT#ÄÒžˆZ ¯ˆ £Ô›¨R­”ò�8mƒÛߨ5k%“þÑ2ó­Î5BKKfðƒƒ„4ö:%ÅŽMðdá˜î%•ñN\qEf{Ò,ô~㓼[-©¼VaMƒ±uà�èš‚�‚ol}cN¤*Æ{ȸ´ÒaML9YéÍÓÜqY®`\PŸhÊŽ Ä W˜˜L¨ Š̱6ûµ&Æ(4•ëwjË/Ûº¨÷Eö‰/IMt¤[´½ÇÖïº>ñ7|MÔ2>}¢#V7|°µ*…#´Jc…vº©¿utú"kb”i•§˘ÂÖ¿ŽNµéç�¦¡;¥ùI¯’) ¦Û‘8ªàMšN‡²ÞdÐGãg -;Æ��´­Òadâæ[jO¥s²'3�ÖïÐ¥½µÿrjÇs[^â>‘zv‡51BD¬ÛDìÀ2»qØ>ÿo~µ+ËH\.”…“Äæúæd<ÚÞÔ>�£tÆãÉææ°%À åkÿö.1u~þk|ÜŠ‹ÓµgšŸüC•Êå9žo˜Æ¿å�ê- ¾åëý�éoMŸ9Áàõ¿³ù¡omû?zK­\k~ñß6_ý™Í;¿u‡]Æ8»sïõ§ÿ¢K8w¯oþÒæ¾Èp4ã¨üªÞþ5Í›>w÷½7o~çôD ?¾Ý‹äßòeÍã¿Ü€—qm¬)ÃW~ÆÔ¡ŒÀ?þçw£ á}Ù[¦�?ÿ³Íç}|ÃRÊ O:â%ùyOóç^Ûü§Ÿ™Æù´×5õ[Ê#±²²:�gg¨‰™¢=]hÀCò'�Qk¨y‰ï̉9Å_¦�û¬/˜ra]ð~bó¯þ±[¿qÒ\:×üƒoj¾æ³¦qÀ=A*w¯½8%þ†Oôüþ77e½ß¼ãiHüŸúcº÷8…ÿ÷_›·|AóM_²K¬½Ù9Ká`xy†Ë—¿¶ù‰ï—ëûÿwó]©yëM#§6»þ‰ÓÐÛܽè²X^ÁQtöܲ°ï¤²‘¡JMÎàÍQœmN¾)š”˜¸zÚ0 ÎöÉ™Pü°1cæè¬!£Ak¢fãVæ`›†§ƒk@È V�´�á9k'£Üc0å»HȃL#bs•‰¬sWìÆÞB�Ï"µUÔ€ƒ€ÖªBTÚ¶»‘Ê©.ÉKRÉ�X€M®ÎÿJ¾‰ï_¤*YVº?O<0:µXnWÄW0J‰Y øÁNS~ˆ�╆/•f¾‹`ÙmÊ"ñ¿:`5£% òBh² œ¥i*Š‹¶¤ášIÞŽèIô¡B„D¨[_$5.]+Àõ<ÀnX‰hhˆ²¤\r¥ÈxMƒ bäq¹üˆ¢+K©Õî¿Dañ“buuùË‹ðjW™áO¡ kR’ŽÓSÚÝ,¯nÎ¥*2q³ÓD8UnË3=ÕdO\4jq›Îµ¶ÈEìFÕ•�™(§è'IQ¸~j¼� `3‚ðˆ±øÛÐ&RHÈâÁd˜Ç„aê2àO�s™’•¬nf'PL©ê&ê)u¢Ò‰!®MV–‘ДânVT~ââg6¼ýä›\ ©=sAJ®4ÐÃ~ Eü2º)7¤áËúò ¤´½½ILJ…2%«¼$§ŠÓÃå úLèæ…O�xº…šä�¶ÄàCRyÃ-SA~]a®�¾! ak’dmf¨Ù(+tœFGbt‘h ¦»¤y‚Ìõlâ²j#‰Ù!˜8J€2RØöf“˜ü꨺È2¹€ÇÖv?î2Q¦6€º¢ $µ¢PÓcFJO‰Ã!Fr,-˜¿ÿ‰ÚÄ´Öú J,A5›/s¨‚"ZF{÷­‰šz®5Q­5±ÖÄÚ'ÚZ{Ùì×>±ö‰ØA�2Þ¨£Ó::}iG§ªgtêØô¹.· xýµÙ)ÅsEõ]ÏLIØg‹IKuøfÓ)æ[›ë뜌 ØcÄßúÚæ�|©`J¹À¿þÚíýÚÞþ“]ºÚd{¾è;˜?´=Èžð[Þr>æ¾±ùþ·N_~Ï×7üÝòâTÍoy÷Á£ 8)õMß/B·{±r–¿Û]oygóÊ�ß÷’m×¾ø·MáET±«�}ñò𧾦yÍïÚü…µù¹:õC�où3û"ì}øØOj¾ñÇv®_¿¾zc™Ÿk™Y¼Ìð�Õœ£9ÍB‡·�' »L¶Gãáh„#>�ÒÄ‹ðs¿²ùñïié³²õo|þ.¯ƒ7¬Ûý ß±/ðб}gÎâ YÜ!YB B÷EoÚ·ÿÚ{ÃHèÜ’çW삳û(6Í/ýÇIø¶=PæÞ×÷×þ]T°7ìyî_DYp²ÇÍËø×àhä•ùd¶“JÔyâ]ŠØÐ„¥¨œý¥SÜ«ÑrZã±¹ÔUBôD�Wiü þùðÅnËJ‰t¥Ñ X';‰†qĬDŒ¨®¥Q–1MoyÃK‘�TcCñ€d£SâJXœ…ÿâ¶–ù ¹¹*0¡‚ÉŒLÉŽñÁà€[„_JKËw³ÃÓø\¾�¤Ô’ÕäV9�À·/¡ž4¸Ý²h§¥+Gx9+²™&éóH’dtLÁÄTMóψµˆì(J0c¥Ù•3JPá–E)Ò‡  }g¾¸’eE÷ù³Dr¼+ùŒT øzŠÓ^S©I*€~[é]Uš*Ê0“œ µŒRe%Ç ê0V%Š;�]yLi‰Ž&‡îäeié”¶ÏQy±�>b‡˜/%,¢* ‹)é¼ÑÂ#yM|ò’{±cÐ9�94­,J‚éµølô�Äö9±Ë_vÈ,3XPý°VØRÁÔuô¦<îbÃI"›’A¤ÖD”†vŠiךX,¦ÖÄZmJKJå¨}¢­fÛ{Õ>±ö‰T :à::­£S ¡eÑNÚDÔÑi&\ðÑ©ŠŽ«ÔP¦Þ<ÏÅ�ùû‘{²ÃÎü�Ruò5Ùääµõ�)…¿þ÷›¿ö}ÓÇ[Þ�ƒ|ÇOx>À�\±ßIDãp6Â;~º zî ÐêÇþWóêßZbáw1�þ)¿¯ù'¿ì2Òç¾ÈÂwÿëæ³¿ø`,p¥oÿçoù lÇ1.VzþÈ/6öëßâ‘,üí÷ì.ålÓK—®,//s>°FB?§/”SŠ,$Ê|çÖ5„шE’S^x~ê˜>Þîî“?½y×ÿÞu¾›Æzã~0®¼xâWýFKßðÃÓ˜åß:๽¸Û뾸yÏÓÓhlá÷Ô©éãÞ;`\Jäv¥ù�ÿo9ôövQnþÂËbãÊ¥+7——ÙuŽ?—g µ0‘bf_ûÄ»·OìqA!¢ÄÄ(â�dXÀ ªþe¥ñ¤@G€ xöà¬�f’¹�=q ¡öýOh"Kß¹‚€¼p¿+‘Â>pA„òà‹‹=˜øòŸß¹È xC1@ž� YÐR €÷$†48!Æ)é,mÀ?#e-690¥I ÖHøLñB>ýºÚ%v9O¥ÀÍß½ äNU½ªIËI ¤;œxhƒ§›¢kα¥\U62*D ˜ê »åxñW<ï$£J“eŽ–Šª álQË'pž†¨ig`ʲù}òoÙqŇS�y®5½Õšˆ©ÖšXkbíkŸXûÄÚ'Ö>±ŽNïžÑ)93¥ËPÚák{qÕîÕÍ–Çyt Ã.RÌ: ßòÚsûEq(cf™[±òÅDž:¶±9\^¹Ùô9NT2°g)‡¼ã�Ó#G÷Òÿ‚¿Ü¼þo4xEí½sÓ§#{vë7” Êó\d¥²w­ò+Íw]óoÞ½Ç)4@a>ç+šßÿ§Ür‘ö™K—ççæŽ9\B܉ìÿsócßÑüÔ6x{¸X'û'¾¢ùÃ_ºw»µõuΡkõx÷³Lþ®wHÚ>þöÏô‡�ùÄÝ·£áH|¦¸›¡ö¯ú¶æ÷üÑæïü•}'±îÆfM뾩yíëtËÉ"zþâ¥K—¯ôz” ÓíãnÅô‡)¯ï‹ÿÎän=Otîæ4¯·•Õ SðgB¬å?ÓüüÏ4ßûõî×öì ˆós¿ÊE»»G£:Ü)‹Ïü¼fsÃÍïöªpæ}-™?øEÍGBówÿê¾­÷Ê;@4ÎŽø³ojÏvØ»…ß/ý‡æ–G”¾æS›wþ·æmoØw"*Ô�ð¿¾ùòolzí±ˆÇÕ:iî]»s¿/¯¿,˜E^¸xñÒåk½þ'œöçzM2ÃÞ{ºñPúD9;¯ˆ Sñ» ±9íÅߺÑ�QJ¶�@b )5=�(GJRh§*^‹ˆÓC${ÿãØ&¼E`?a`³øQèA;àÄM*$!Á”ñŽÿáM©±±ÂØœ@@…@‡“ºp/nXÈ+÷P*9Œ‚ÅhÉœôŽdËœÕÌ4 A@\ø;�}1§ÙF IDÉ( `gDWÒî ïÅ ðÄ®2åÞH¼äÛRv(@—ìHîùɳ¨—pŒ РYDK†¸Êä±ö‰utZG§wçè4?33c™,- -ìÙç¾^O¿nÞ¼ùø©Ç¯^¾þè«^}ÿý'§'-L£LïVoÞ|ì‰Ó7n,N-Zœ[œëô²·ûxk<š°Ýƒñ…¹¹“'Ž9QÚ{1Aºð¤Ë'Ÿ>Ýœ|@_§û_y§û܇Υ«W?súáû¸÷ž—ï%|à~˜ÍÔ€Œ„7W/6Oþª›õÍËò0Íg¨zåêµ÷�zl~nþU�<²thé …µ•G-²0Út彯˜cf½çZ½¹öÄéÓ=ðÀÑ]ð®¼eDZ dÿW›‹g�Šrè±{¼ÕöÃŽ€5gNŸíºïþûœ7í½ŠÉtXªyß#ͽ7'îÝ™;•Êùgž9õÄ™µ•Õ¥ù……¥ÃóÆbŸ2b²ärG§•Ï3OÄÓf‹3(ËñÖâ`îäÉ-8µ+ ›å¡Æ'­Y_U™'^®0l3·_àå•B/íW#kHY9KéÜó€xå~í5—Ï7çŸlÎ=Þ ê‡^ݼòã<ïâ_lÉ÷Þ_r£7 Ucuû¯k×oZZzƒ§8®^¿~âØ±V.>óÄ“gV—×89xñðÂÂâBqŽÃ+Êi NÀ™MUÄæ®DlØ (x䇪X1A^” ŸÄ2 ¾Ib¡x@8:¸é%%ª%$žÁ�f%šFaÈÌŒ qàé]©ÕpO‹Á­H>‘G®`À@3ø !'«ÿIÉG›Œšîž× vµÕWVa@s¢Å$,‚ó¢™q5=ÂD‡š�Q€ˆÌXØ/ªd©Zb!'w°=ژČ"L$úL$h$¼LF¥ÆKŸ5ãñ�\{¬ÐLbâ®gb²‡´ÒB)K±x¦“‰JÕ¹¿%ÐËNä�Ä2ÕD_S¶ü'm ÑŠezOÜD'»-É3ÈS2M HBØ�#DÀ¼âÓx�އ`pQ˜€™Rkä’ô›¤vk2°/N “uüÇS´êŽeC¶ÙmÎöÄ6zÐÖC+Á¶Øa•@äCQQb¶’Ñ?>Ôœ›k2„~²÷<ÉtG!²)m°<5g|QÃfPq‹x!²P8­Å¢åX"â´ùi¥Î;mÞˆˆÁ“qÿ8�uK":THÕD Œ@‡7ì¶�ªÓ‡S;@‚ÂÒ¢­5±ÖD,¦ÖÄZkŸXûÄÚ'Ö>±ŽN*Ö>ñî�2nÍaf˜_‘tßæZ[[ãÄÒN·3aƒ¯ñø9�°˜q²±æNÏß²»lÇ#晇Õµµ­ñöÉ“G{é¸{1kêzÚµû~cssn0Ø}éÓwcî½�þ3—/?~úô¥g.‘æÅ¿â�cGw|ßJÔ…CÍÇŠ·¹n¬¬<öä“OꕯÖ™Foa·~þn±·ÚÙ3O�:õxgÐc*þÀ}÷îüžO�dáÜÓŸëYY™›_ìÌ/âkŠ×Ëvâñ€˜uâþ|óD¦f™ý±fh{e}}ûʵ{NÛ' p!ÇÅ81v¾®ßX~ïãO,-οúÑGaú@�¿Û]müØóîv‘Ÿ7?5HÝŠººpñéK¯¼êÕÕíÞ˜ÆõïÜùó«7WzàÁ}&weqáÂÅÇO�å° `·¹¹¹Þ`n¶×‡5‘ó�¿2ƒMøªˆÍ݆ØtÁ@3°.JŠTHô'φƒ4”ðÒÀ–"M‘±ð¸H¯®‚yIÈ`Q ÃL¼…ï8H’È Þ ¤Ä«ˆ$„ƒuð$P‚‘ב¨ b]‡#±){•ó ¤!Ã’.|ü!Q(iÂT35»ÒлÊû�œB Q €I 1zKG–F7ŠPYÒ#uîx€ƒGD’%e!'“)¤C"ï”ΨbÖr$ªO†ªœ6=_0ä3�^ y ]Û^ï<@�+%åT‹èÁ”¬¦ü•VÑ»Dµ˺ˆ 'Ò AɶDʃѕ2y!$J΃òÄ>`iVˆc: ðOI¸Ge¢e*DŠ{ ’Gâ¨,“a”f±µ¸ð4š@/oŒ)ì•?©§$“X�Ì€üWˆ’ £(),_s#eÄ!¼dQ!t�ƒi 2y"6Ü´%âÈR¢ªË…ÎHE#�™#$äÅ!NIùmjÍEAD­5±ÖDMBKŽ!sCª5Q=ÔšXûD›YuÓÚ'Ö>1ƒ¾ Âì]ÓH¤ÉäÎ.œŽ¶ŽNÕE�Ún”1˜#´v8F#RÆ’1^ÛáÖÑéó�N™ã3D™Œ·7¶G«Ý�Û9¾e_°K×oÜÀ-g}c}eu ä ÃZj-,¥|æÊåå•KñŽÉ¹” ckVˆ°Ü°‹·Ôõë×F£Íc'ã·“ú¶ß@Nç.\¼~ýÆ«}dañÖ8ȵ7®\½‚Ë«+7–—�=zKr—.^:óäéáÆæ>pâÄq=žïZߨxêÜù³çž…Äå­Û›D;sþéõÍMÜ÷Ø¢ëù4ãñ˜¥…§ÏŸ¿|ùòüÂü¥K—Ødíå'ïyÙñãíIÏI‚Fòêµëçž:ÿäOöú=>ž:wnk4>yòe¸J=gÒöå͵µ³ž>{þÜh}87·0`Ñì<¸…ÉÎI8')-Gñ@Â͈JO%fq)E.�…ƒQ9’ÔÆ@ö±Ü‚ˆh‡xQ!ñNb·Ë8BÙôb‡‰Íï-DÌ;³ÇE®4lr—f}‘_1¢øE õ6«šÈ %U£×”€PXyÈ+ËìY±j±cáäP5uVöƒ³ƒ±ªQ½dOöÄ„ŸSO"»C?ÀðOü¨,9š»²c›uÎ¥¨ž‹Š@&$gj» [ˆ“K‰F2K_lÊìÅß E¨:C2¿;)œ“Š,‹A™%cð_IO@rl‰X°-UKQ�)†*ÔÜ…É¥l"ºêq2� 9ƒ–†¹3«O9)�iȆ˜ÂÉRÉ??…ÁA«C M«$ýê´OU«Í x:â·�uYz‹`øKS|æ�÷$–%ª·ÖªÁa‹(IW9¢KQô_Ct2�ÚWR©1m.ö£ ùŸ@=Y8ä‰'¾T‘|ëJh—£¾hÍWµ&Öš¨ýÖšXkbíkŸXûÄÚ'ÖÑi�Þm£S6FnW77X@Øg©›S‚öbÝ͵kן|ü4xM~n°0J¢ÑÍј�Æð}s¼s�ޱ:ïñ³gÎ]¼ÐŸŸŸ[K˜°�˜ƒkç\ÄuàîÌ‘–ÀõúÚ:®O Z Ìu½rH~ð«è⥫ï?uú½ï{ÿÊÊ “¼~×ÍÅöúX±ß?{—±pïÊ•kƒ¹yxØ"W£m¢íELJn¬,³ØóÔ©Sìfôb¢I¤ÂùnoÌ]!\NxõÚÙ³ç~í×Þ{þüyfeóòvhq”©¿5_½zãæÍ Ñ’~Ÿß fn,«&{ßcï}â‰Sho~0˜Cʹ96åæhËõÍu˜ÌíUæNj¿q3¼tåÊgμÿÔ)4Lâ&ÎÏwg;7W×——×ׇlª3CÙÝŽ>ûÔSï}ìýç/œg:87ÇJÓ¥…ùÁÂÜ 7P”\Sg5ΰžžè¬‰4ÌŒ&“•µ•««›ÃQ9+`¯ü»÷¨buõæ… ;õ8¥€öææ;½ÁìÚæG¥2 ¤›Y»iÈ>nepR¼Ä}fs}s¶Û]¹ÉÞ5È¢°ÑgS.ôIõÄÙ ï}ßcç/\@}ø]Î/QYߺÝ>È4óWK@¬€OZ‹L= pÆ\›»± aºOs)žaYY†…o ãÆO. €"!x\‰œÉQ‰›¶=Ix&4H’ífÜ…ó¼x.Mjù’—$c+>¤Åˆ’þP,q ^D`›p �3‘%qŽ00¾°_„$©d RвoXÙºLyJöƒ=jÁ‚0䒅‰ÉWhµ !á!U^%×Ôª ‹Ò)0ì̈dí\á¶,p$Ø+¤Ì¯È¥7€¼éøÎ¢m‰F�H¬¼´Œ$GE†9z¢¾%‹bKå!dˆ+&Å}¹ˆ•Ä&‡¶äÙ%ÂV\ i-þ‹üÁÞ¨‰”J®à>+§’ú_és±Xú:±q‰ÖùÅš! æ_ÑMI1û†Ë½ø9å #ÊYúÜiˆªIAy•¿ÈJx"‡ª‹8°µˆJ&’DFŒÈ�bÌqY N‘}KyySþË�&Œ¾Ÿ´¾† åQš|TÊ‘—yͳB‰å‡.-¥¤ô3vM"´SkbiV¬N*\Œ¾ÖDÍOc¬5±ÖD �vÂÙúAsSûDª]AíkŸH^G§T‡ òöŒ?ìNíAWÕÑi�ÆB^¢Ñ)�p¾`ç–oœzâÔæúúh4±)›N ngà P’6˜cc~º­Ë+9“q}m´±1ÇcÒìÏÌÙ³óݹ…À¥~\è‹í¯êØ4LÑ9ðöÇîÙ>?T÷AF®^¹~þüÅ~opèÐ÷ýª†�ØÆpxs}•3 Æ6¿ZZZäåòòõ_Y¾�„^“!»òøã=Wg†ã!ççÙyl®7ߎ&Ï\¼töôùµÍ p™ñdÓC"h] 3·¸8~G‚ÙÎ�ë7ž¹z ñ0»ž À¯@¢fÇÛ[ĺ¹¶�ŸÝòò 0»qq8;á÷ûKìˆ�³ž-ƒÞöÆÆðô™s� ×�=ÞØ¢aÚÔ ‡€6kË+×/_¾J®9Jtqq±‡p"‡Déƒ^¢Ì›«7¯\»ökÔææ„>�a°ÇÒÙ\ÛØxæê¥+7@Ç�NU=вÁ`~ðÙÙÏ'ã‹—/Ÿ=ÿÔâüàðâ´=�ÁÖbõ.¯¬^¾rùæÍÕn¿‡·]o€«ÀÝÜîD,7SKÛâñN牥Lq<À•µÍÌ (/ ‹?”°P‹3�e.·²6ŽW®¯œ;wŽEÇh›Œ€e�»Q 3½ÁúÆæã×Ά=zl1²»õø¦˜ÚµÕå•™ÙîãOiF¶f—~fPÌÀx»ÄyÂ"R`¿î�OÀ<�ôÞˆ’,íµ}ñ^Ç4ê4á¤à¹8’IëJË þ¡û�\Lê'1`X`'ï;–‚“F„Üh×ÖТÃVo&Î|P2÷,óˆ´ìVp¡3p–]ΠKTDÐ-âÓc°óZ”…²ç‰äKQÍ¥nqè�;D‡‹n_Z‚Íy$¥ªó"" œQ÷¶‹“ñék`­Ì¥�"ã¤ØÆ_‹`v†‚8>nJÆòy1Rò­ÞD-�b< T™ü%ódÉIi‰™w¢ðý(%åîA-n8'ÓxT  œaIÑð–ÿâLdæ~,î”K[">BÓ· ¾™CÝÄÇ/\ßF¾ÃePÕÅöÜ8Ž.„ÞÁl`W#UÏd3hfÉ!aJV0 õ�I©-µ÷&½´l–,�ȶ¤è‚\ñL M[aÑŸ�æUôÃÛtý°‡¾±éþug´¦¨ Ç£¾MÜÏ8 í[7»ƒ€;ê¡%Gˆ‚‰¢AKÍæ=aò$ÐÃN¥p[k"©5±ÖÄZkŸXûDº¥Ú'Ö>±ö‰utzW�N™Ÿ0“i�ó€¢#‰¦�ù€i1üŸ™ÅŸk¶a*@À±Nž“éÜ`ŸÈÃÑÆúh„K™\ ¢ßTg®Óacgf�c˜kK³“Äv牢 ³‡g£ÑÖp<o7ÇøÞmÁ™0öAs¼Ï<§²75À†G¢vº@EI�|„C†èNÊqw·»�Þ\g¡?˜×/.¡«Ñ*‹‡›Â?“±ë‡Œ º'îÖ‡ZfÌ1pFnptˆ _#¾�—yЊ3ð¸þ  XÑœàV9Ô„( ’ŒqD{d‚'ÂÚiPæ‰dà dFÆÈl›×ésús¢ñÐ YGÎ2xx™tÇof™³è½×Ì¢NÀûãŤ:M Æ’}ðXó[Ôl¢"ŽqJ4ßn¤bÙdZ¡SÅ’b…r€�ˆ‹µ&ÖšXkb­‰6µOÄjŸXûÄ::­£Ó::½ûF§V ˆ€>ΰ„Š}£H\ÓŒ£ÎÎ6ýþ6HÄlÜ t` ç�Ù zàfþÊ¿…»óÉÙ˜ò+¶Ø– �‚?{‹µeºbŸÈ<Á‘3?có3<øSÏŠÙþ¸‹‡8d�ì9¤W8bá8Ä2Ø.À“þYlCr\�8b3MY!Äòœ€SœÉ^õòd¾ÙéŒøùˆgšð‡h SP°@?|�Å9éãžÑßb«1p«fkx°ç|M(¦y�gÓ‘3üøpÓbò@‚$ %ã§jmÍŽ&â(cà¦MþG1EЋÀiññ} ÂBv�@� Á¶=qéŒEzFã>øH¸3ÑLMaã ‡\ >T`~3½9„êöAš˜F ³Ý1buQÝ6ëg�Âô™“ ó–L½„Þ8ÃTÔ�rÄ! Ä(@UfU¤pÖÃ…Ô&p~sGóD¢¢½Í¶¶9&ó!Ÿ„ãñ`¼5l%pVÄ´k? 1@\õ3£$Ùö¯?Ûíáè̬33Áž}, ¼ŠŒO¨D,׋!ð^$®xH@ÞI©S·ÙIxS— ÐTfchÕ&7Ü�¶ìÈ䎒u''²‰C‰{&iدRj_È(üÆ JÚÇ‹Sà5�&®0ãÖôGÞH �ŠÁ—º8ëxÄjS’p› è&+ˆëÖ‰9v0 ވ˧,ÎrX>Š &È;Çüe¯ÕÀ°`2k¶L*bs7!6˜œÍu5&H Ö8ìÞ�Zv€ÑÎŽAOu!Æ-Í­N7îY"µnXLY´ƒuQ^øøPÚº…)@ÑÆ(5”#- ÖL\ë )ð? CÛ7Nº¤½èPψg…¡ÉãWìÉ&Õ4ÞQÙ¬è„C§À5%ÜA‡aê°G蜮Š=j"@½° ÒÀ!•…5ÞÈb¢Ý!±l@ŒÒ’Ä?‰;°Yñ–O#c ×‚7˜5•ɸç=õÖ c®ˆç)�Tvû‹PÃÖlsA�ò†!*_ò.=ù�Žì£Bˆ¶p¶im4lÝ$€®’í´öQµî€¼'9Ñ'¼P‘�mnØ‚k µ�´Ù‘f†x¥š«Z13ò$wƒfÉ¢½‚à(¨O} qO·[¤¶fE4IòG«�P‡3©E¹ÕŒ³eÔiðh!ÃÍCƒÝƳ3=s€¶¡Æ!äéß”:þ€A�BA½À¬44±^ÙHlJ3ê°EQ4ˆHIn¶Ü6ÏöMñcªá‰†¨ß¨�øbŠÄ§TH$NR¼§%fkC2ÊoYxÛ ½J˼Ҵ«u=;û]ëÜ$¿¢`äf)jM¬5±ÖÄZkŸXûÄÚ'Ö>±ŽN˯۵O¼kûÄLi€˜]€M:³¬ÍtÐQ³câ™î@`«ÃÓz`ÍnuÀ&L¸&3#Fò]ÖP1&~ÓbÚæ­+/™ƒdð¾§Oôg|çRÌA™à±ÉWn»?„ˆÚ �%:.(‚¿�‹@Áš8Õ0fXL¹@˜ôêb²ÀŠQ5b6à%³À‡Î%£“RTg²•)¯¡ÔÆ²æˆ 0“ÖÊÉS<¢��œhzw‰ ‘eBG½-ÂyJ°©«ús‰üaP&~äYåŠK]…“xÉD BTÑ>Otž•‰2¨Ú‚FmwÇì´‡?³,ËŸ#&˜j¥8tÿƒ=ö“…v”¬�î“—þ¸k{`ºM¡_ „‰™ÊGxfé¨.ë1uz󵨄>&eÐcÞ"±A½óA_áŒÇ,�b ' lx1JÑtkÁ 3 %r¢‘ýP\,ÖÚy@åN�€V�Ì©È�R ÖœŒŽ&Dˆ; #`t@v\‚{®fëni>j˜Ñf€C-[ìÏÄr9á­ˆ�¶k%±Úze®NiYð”2ÏjĆb´Øü‰ÑŒ4ŸXR±@±j¹è–"è`e¤¡¼�®�¶)±t °Xuj¯uÇÐ’N‚Õ ”r™ ©ÔUØK[ãâ›&„["”glÞø €�a$—#”Ý:¤„†ˆw4&D%ŸâNVk¯ü.ùókJFä+²ðÁ iÌ\ùŠ…‚M`ÒFî¨fÜæD%H~VK Ñ¢S)ÊAmÚ‚ÀŠ\ñL®ù U0»mšÐW·E¿‰T”) öTDL¢JBÂ"(oü4†.ÂÞEg”-fIôÄ,RFZ""Iµ•V »*]x™Öuž–„Ùµò&¢Ne ùÄàµ:¤}Õºð¾$¦`˜™5_Û.$ÕgL„  `¿ÌGµCAûÊ´E/<&Arg�‡d‘\‰˘*HŒSf¼•®_j…{îdÊM�]î¹|]žø A…!вÐ⓬¹ a×3ú,©Áøt_æMK[%&‹�’„¥&£H«`µ&Fá’zñK+*Ì`Äp®¼¯5Q#ª5±ÖÄT‰Ú'Ö>Ѿ¤ö‰ôªéRëè4ýdÂd¼—ž”ñO�:®¨£Ó;�2¾/Àa¾µ5èÎl¯<>â@IDATõfúþjíïçx`V8pàíX\Hl-¿ž3Ôe ¼ígNI2ù!Kç2íkhÛ�cœÇÏj­ QÞ)#¿Ôwz jø‡8‘q• �=dðŸÃWh 9ÞãüøŽ¼xßéöÁƒ™IœnètMél’¡¶Ç„ßÿ}Ç0Ýy¡.ʉ¯Œ]™¥‘X‡ DzØížÐJ™#˜ýœ›Ùäê°¦ƒ¨9@jÜÝH*’„û³ üßÀ.™ˆáÀD Œ&sr oõI*É ©9Ep~èi†ºrݦP‘}½³!„NKï2DJÁe•�fìvFNA,‰¨T�f ™üýLÉih² aBÞša8)L‰^ÚPµ+"ðJ2è‹g‰xóDAƒ, E–ņ,B%è×É)ukÒlØeI8 kÂŽŠ(=ç‘ ÉÖHg0§wJL”Ì„‘“]˜x $ Ḭ́#µVdi´ÙÖP²ÑNúÍÍŠÜj'±.frC,a[Y£‚Ì_STp�.TV¦˜ÈE¶ˆe†XàŒŽ³”•Ì’-TŒªP–ö¬«†(‚!X,³^ðd�a¶‡3+B�2©È¼@u®ê#H�'ªi °hÒ/T¤x¥)Ê]yÿ¡˜'v­ñ^TÑcÍ}ôÞšiÅLÓAÙj¼­Ue ¹›X‘l‰–ûã5™“NbRwuõ‘zê ¹%§±ÛlÇzŠ1—¤Ðà' Í’ï4|ÈôbÕRê´°rEâ‚3Ü‹µ>Lž(T‹ÂœØdŒxœ£i@€ó;Í Å›¹1Óx�’`Å>QiŠy4ò‘XaÒ~&]§¬ÔÌO¥£êÒØ\"ŒaÆóU\¤¢A‚µ#‚õÅ·��ÓªxÆŠgO^§�T0b© @ïi<Ém�47PÒ§nâ/星‰�Ã=Þ ³ëMÈ+Û`»1H(1Ñt33ŸºùûV GÒÊ%½U§Üƒ`ÛÒªrÕj“J]kb­‰š]­‰µ&Ò>Ô>±ö‰µO¤A¬£SÇ0.G �Œ'O̘ÇAJ�2FfPUG§ B?x£ÓÀ �´‚°#³¦Ê>WB!.ÙqþÂLÂ15†J‚E„0ÆÇç‹IÃdÆ ‚1fÀ'ŽÙMœ)&i÷ÍÅ’ Itè‰ÌÛá8°‰ŒÔ™Š°6ÜIYÛË·''Ha ¤é°~Ë)‹;ƒÃAj þ‘Óa½x�oç‚[æ�Î 2%SRýÞ;`#©s/nÛšÈ380qÖ+dÐÂ4�üerCæS�Zt™£õ˜Z²GžSB¼ÒF ¢hÑ›S­¬ûk#©æŽ’q²‚³8rÈ?W¤!鸀{jx€"˜a2EQ'd¸Ñ +yvgmJ >LŽËVêªÉzä<197Gœ=Æ™äp9B¦G¤UµT$A¯w^ÑH¡€b].ñ? G2*V÷™gŠÜ (gB0J _ØÑ i¶'M3ƒÃ%MâÄ Ñ)'lÑù À”J—`†è³’‡™4_Üc�,?“ù,™<èG*Š0N!„8OÄ–41‹š™Õ õZæ‰Ö¤×СÈì}„6Ýä ƒ ‰C‡™S»†Ji�Þ÷,Twc2¶…@)`²øˆªT¦ÑËÉ?ÕGD�¤"6ÚÏÝÙ'ÒB ¤c-Z�OVt R”$¶�a³°„ERíl b3¶DX©É…¥Xñ$ ŠàEr9béñ6a!„ •¼#LÒ”À‹P¥BrËCHHA>X¶5ÉOÅ$t5gA a2+ð½É"•�ˆ·üç²Òð`U·òˆgCk<‚#‚AÞŠ  H׿Y©‰ÉZ%fL}›¬N23¯ GŽTeî$NÛ%Æ“«*‘ø0*þ†ˆµ’¬F6mfÒ|F<[ZXD5ŠlJ‹@^*‰eÍ„Óæ¨î"˜‚[„æ˜ k³& _@µÜ‚œ^†—í¡¶^dÒ¤j"¢©T¨K,á¶k¼à’¨ùõ‰Pñ*n ÜᘂS€„§æ%Í»‘�hJc“árI 1¤�‚Ð܄̤© %GÊît‡�8;’qgf$©µÝWRˆüxⵯvø•Ÿ2øe„7fU ‡Òi*Hùž?ª”C…çŸ1£»e»yû‘\XF�ÚÅ:<¡íÈ]QM�¢•ŒÚ4†âutŠyìŽNÝJ†:¤Ša¿cÌ ð{c|F7TÜØÐ=Ðîd˜;M¬uxÚ©€Zžª¾³š˜q>ÓM•DøE)…£pDs,C&iù§ìõÁ�Q6L®3"wžè”�ÈŽÖ�«@/ˆ ÓÙÄõÃÙ#œ2›wýb* Ø@Ì®V’ĉiÀR‚ίPP›­Ø-g^ s¡.çsºb©‹À‹Ñ™�qÕjJê‘{wžɘw™¥‚ìàüœ`#ñ¢‚IzÚò¨\a"„Ê7Ĥîlƒ눤S’@4/Íï³¾YÀ%QØõ¶‚ѰCW_»Å£6@�€•‚;ËQ©t²-PQš– Ñop W”X툦JD-Ò6E'ëGD‡·ÖiÚìñ£æ×äa ¼êP(^Æ ”ôTiZW¤Ò5*9³ž�)ëØŒW²h1Jt1©…eã;1*ëPª%Bq[®T5¨"­¿æ@«_Á$w°ZB[³àâvµh.g†ƒ©»ˆ“Fów!‹Š�ÒH('tP±6È'±`>ˆ dŒ@ˆE“•„»ˆJ24C8©"äÒëòÒŸ&ʪWì…À,”gÏNb�²ô*ᯎðÎ#ïÂu°–>"3……4!*G™%ð{�fdn”Z é‘M¡j�‘‚2ÒN¢é‚²òåkòÉË‘Ž¸´~”ïÎ%[”œ¤î-‘ óƒ‚ùBKæž_¬ˆÒ¥·Vm¶í—ÅÏ06úzûû¶¤§' ã·wgçBè­ ±#<&ÂOIp´@àCOÆ­M= jM¤üø£|¬˜µ&Ʀ5áØb­‰µ&Ò$Õ>Ñ&¿ö‰ôóµOtôQG§utZG§wÃè´aY%³, ƒi ãs†ÅŽuX[>¶ÙüežØuíŸí¸À-Žô}ùç‰L]œ'²°”1=s+§*Î䌋>^ûç‰ËÍé„Óc2aubGðî<ÑWÆÈ<±LP¤zptÊ̶ež(1'?`-Ìlàê0*7õp‹y¢£]¦ Ld™'r&@&  “§I‘)e/¨[ÏA„œ¸:7Řg0�ÍT—s6 ÍÛÎKÙÈj�5d˜[0fÉ©ä~žèŠRrŒôŠã•¹,gE0ÏÚ™'o–g͵g¯è@ådèla‰C’ãÈ_æ‰eÎèm /â8Ot¦.W牠dÞB½@�O”ÉüX Èò^zŸbH8×yHBÔvËy"»ÂioäHLh²ø–óD¤ÐqD,†,9#vòi¬Ìy¢ÓɊؤl><ç‰8/R¬Ul�Â¥´µ« 2Ú)ÿlw4_ÌL;à­åO0�©yL@�‘± 뮦ÕZKߊaÞ–|Z‘"-“ña°f§m“(ül¼ ‘wËËWJã{Hiøš#ŸF!¦ÑsË“ä¹üL4Úz…�‘$Ú‹Y†·=„o|Ùf�WÔ3E- L/í¡Ñ"–´ ¾ò­@Kˆ '¡m«× )io[þ¼Ml.ôH•¶™…rrÂQiau±&Ò RCñ˜¬ˆ?KÞ¾) ¥°|Gé’¡W„&+dy°“Hh#½"^(Ë«4À69PWJó§„EX¹¨¹ ˆG(x†‘ú¥ÉK|%Œ‚ŒðCmð,�`g$€°h¹7G%P>Ðf*Àµ+”’%)Ç ’(äÉ�ú ¼ Š³5‚ÒÂ%KfÏܨKÞÃr&©Rå)z'²^~&ÿÉ$QŒ(/žé'äó)™hãJÊW$_MÔš¨NQGJQc pvJ‚ð¨R�Öš¨©–Î_cC#šk,QÓCEµ&Öšh«’ê‚I`(¤ö‰µOÔ*jŸXG§ŽIÊHƒ¨£SG—utŠAÔÑ©CP»Î:OT„Ú¨óÄ:Ot"^Èô˜t ™g½@Ć=û@/ !Ã~T2Ö5ŠŽè¼ã´.ƒvŽåeû?ñ¼Â½Gç"9P^àœâPŸÿÁ4Äyüõ€ ý©Á 'BÏBo¦À“ ”X¨Èuý@Ò6wÆ$Pú®|/ÐnüçHì�P!¿bð�¿8 ±u.R@lh�*a' b+Ž.HŠÌ„ ÷·'ôæ‚?hƒ#ë‚ÅiÇ£$ÎdÉ.ÑÙ“•“DV¿À!޳_ŪIŽ "m·&g/6a³Þâ.ÆH xÄ8P00ÞjüH!-ç E-à§³a‰—�ìÈ ž] éÙ+Šo^HF}ªÈf9¹0¥€üXÀ%†å­X�ÛoðvôÕÁ·‚ì!Iº^FJ:Ë5üáÍ�p$Kb‰ª+„\ÊÏ7F÷7°™,LÏ!§¢Ñ4T@þ£b‚U>rhAˆ©‰ð@©°È=MmËEò½ÊÑ,áC¹)I¤ów M°| u7©‹å Wy´kˆþk ?ÂI€ põxÀR÷G�¨NÆ�ÓUÞ0O¼Â‹s~Å3�9-šÃžX<îpصyƒ¼‚™yUCD}Ì•³ÖDSkb­‰µ&Ö>±ôuµO¬}"zÛ::­£Ó::­£Ó:O¬}bíkŸøá>Oß-P’ÀmÀƒ <@,‚0=½DˆÄx‚}ˆ°šÔ†øab>€'ü6ˆ´¢ bSbÂAÀU" %smSð—è$H*cŸŸ}opÈ,í<180á‰Ez!T`•" "ñ†![b�¤I°ƒô¡*:‚t¡o¸™ŒüáC<7CŒf @ŽÄ5)�<&9Ùsªlj?ƒàá¿e®…�¤ÉMÑP+8d 5BÕŠ Ä™QÁ¤J° šK”Çâ)"$BÀ4â[Ñy2)ås˜HG.…‰ÒˀϖiüzÃ� Æ7éGïE1䦤•U‘G™¼'@ZÉT˔̓‰ˆ(†UœÓdj ¹¯"Q‘Jz’‘ lsç-Dx‚ªf²gÈH(b„¤)¹Ê›BVÙ �è’¸b�ÚB7 J¹˜{•‡ø@ŒðŽI”,ÃÇ@ÌÎÔ…‰�aSÏnDõ¥•Ay%–'=¸bèì}*ã¢DH€|ò–ÚdnŒ©µt䉒߇W­‰j\¥«ZtEY£üCµV¹Z1lPõÄkM¬51íŠ5‡6Æ,-ާ´?6µO¬}¢öÀ…uØ~ø¿ö‰êÁ‘D©-éjüp$PG§14ã8³qàÃßîøÅ–†O|ÑÜÐÌ8Ü©£S ªŽNë<Ñu>užh«A‹Zç‰užø5:í–åߌ³ð"²€°ßÔ¯‹‹SjŽ"àìÅŽô=Žòeàž=ßt bÞ›¾„®Õ‰ŸC7ï3Léx@Š $‚»áá'(BÝÿq≩x- =ÀÄzJôÒVI¤p;ݸï*gÙ‰Äv(Drñ<×\#>qäGG/t‚4âì“ØŽ“ˆèZkr&éì‚6³=J¾ƒšdNÓ2þÄ9,�Š�BþH�q…ïH}Ä /¢¯ŠFôä o1›„q�n”øæ.�0¢±5*á,,اþ’«,6o©Ab§âTÉ!•™¤¤Zí8ª19«ÇùõDïª*Èg±èƒF¾³ d¸íàVñ%êÆb¤Šj‘J71ÜøôÉ#‚Dð…—çÃry4ÏDÌ’a„$ÿðŒjÈ8Qñ£4ûfS)îmðEyZ„.“y])íÁ̈zi4|ð†¨x¾hF¬l�:…Ýß •À™pÓÏ•ÐPP a­)Z\y­C?Úĉ1¡Ø084Ô0øDzS9dD;˜& ÔºTdfA«„E™s¹ë )Q\º‘ u[ñBTl3T5 %yÒZýôTÔv ÏUkb­‰X\­‰µ&––˶Æö‹¦ÆÑF¢ö‰þ¶QûDFµO¬£S¨ôIH8PÉ(©ŽNëè´ŽN™þÔy¢k•ê<±Î+bs÷ 6ÝÌ÷΋+è¾Ã0„ ø‘ˆx9ò×÷ˆ(ÜúŠ÷ŸwV@ŸÀ@2HG\Ñ7Æ`Âä¡ì ˜ˆ™¨¤¼”d’4‰œÚC"%^îįÇ÷@k‘ÜØFI¸oÅ?øJÌà$ŠD°Ëü"m™Å˜¸\~ôƒ;þ2†IsaLîdæyiæý,Ú:!ÁNr¶3¯À¸ÈjÔIÓO6¢¯ibõ!ÒãVb$MÓ(ˆln¶ب¨Á=H¨\)#ß+G¦b†¯‚­ä˜•�„‹xž{à’Æ<ò2ï�Ë%—In™9 rþ&èQt–×Î%!¤ä]ÚpYI£líFVÄ\Í•ª(Ñ‘9|Z±h¼äKi"J)a³Ôk”�—*D (žhÉ5±Px2àn ÑB I|VõÂÄ š0˜Â§h–7E U`îÔŽïRd‘Jr…¿%JBHøó­e%¼ÊC“$ÙWàƒ–"Ôa\þZÅÀG™5íc¨„»ükM,Ö€"ü«51Ö­5¥VcXµ&Öšh‚!زpÕ>‘v³ö‰¶—\î½PûD{{úÞÒ…Ó;"(½´÷.”°ê8ÈàC­ÕÑ©�ŒÿÐÚ«£Sm¤ŽN3ðM7CûRG§utꔇ©w�'–©]†´šY1U牙=; ©£ÓÀŽ:ž=:íz¯Ã57l+c”vUŸøÈ èÝ�ÞQŽU8õ“#Šs¬nF*Ù,M܈qžÓB}¯Á)И“F‡5<³7Y\}t›£_çìR/ö.æÑÓ…ô U4³£€$ºoáoå–^œ=ž$?<¹Ä«¶ûMÂhÛ�gÙ† !Dâ™GœŠŠ£Qñl qýµÀ°”ÐÃAlâb„å¬ B%N@$çÑìÆûik»;ñì¶ër<‚&ÂÀ<éÖødî<è’m½D¹" bÁ­æPÒ‘zä´¼Ã3ƒ=™©~A5ÂÙmÌ�é�­mì®cä]©T ‘ÉDö"ãNÛNr]¹Ôb6ØSDþ©$âˆÛ•q¦Ò¼5V¡üänd;Ù»Ž'o-=ÎvqÛ3d·¤<½ЊómQµ�5’‹où²n±ðß‚| &ÖARsY æ�8“ñF‚uzÉq(w—÷:›!ƒ²’'E|.}߸ˆ¸EvU»õIMA´…\Ð-ª°$`Ôt6}²)¤ìÑjô›¼Žçê JD~%�0Q– L“žêÀ%D#ˆ±� üðUÓ)'ò£`h¥›þh†;XÆ� SQ:2�IâØÜãT?–UD®5Ñb¢hÑc­‰µ&ÖšhF�¨}bíkŸè0�Þ²ŽNëè´ŽNµÖÑ)ÃÅ:O¬óÄ:O¬}â‡UŸêá% ÄT—KÜ�Ñ�!BY(`A$ …`bLTšý &#@¬ÄwIh— <áo|{šxHzcXD !.ÚØF¶e É+)e‹“NhÀÍâ/ÀH�dÔ2X2@i"n$#f‘½ÊTܤ¤óKäH¹"�I%°#°¿�!ðN$^zŒ›²’2Ù€†˜Rø'û$7äMèIr·ó”²Á àhü0‚‰˜ùO¶mZå垘F|æ3R)¯²*“¸#¡¼‚Œx‚ˆebj‚Å�Š`Æã/9÷ãð`+kAE´v¥WcÈ&~%¯…¬ïe¡D²�bQFÐ÷ò½:jéøš[19KŠä´lZ (Ã�ã3üü4�‘¢H¥*=“q©¿ù�¬ð�KºæVš)¹ZkMÔF´³ZS—jM¬}bíí%jŸ˜>Öá@FutZG§utZG§¼œtÕyb�'ÖybElî~ÄF—2|t:ºEÑàF à0/Í™N¥=ÆþA&tÜŠ3œ ÛˆW #=ÀL°ê{%LA*~¤ ú„— ÝàÀËø7%¡P¢C`;&³ Ü%SýÝ�E±ö‰utZG§ ëè´ŽN?×yb�'ÖybElèÄ„:> ›`ñÅ1Ä#@ €D?„  -¸ZPÔ@ A†×´ Kp‰§_¤ŸÐE ä’‚m¢S¼†¤ÉPDÀJX'ÊkUGÈ8!¨ˆ®Ä Ys+g¾;��Ò”Á„R–‡QÄ@“ —cÊ·ÈZ$C¹)�BŒb“@û©pÊæaY˜Š4ªÒ‘Âxê:9l)”�¤¸ÈÌŸ�bà^&e°0^jSEqR‰½¨&Ø­ÄJŽŒ””�‹�[šÉ9@,KA}EÌ"±ýBë6´»Zl®&c{ÔTk"$j�ÂU�¥ R@±ïZkMLÕª5±Ô •A©}¢ZÈUûĘDíí9¬$XE�FƒÌÖÑii,4�::µ–ÔÑiÛTЕRA¨(™?8þ¬óÄ:OÔg¢Î+bóÒ!6ìõ&\&@k3ÙVd(Ÿ½ÓD7ØuŒÿ¿2Å!�èb*Î…K{Es…óŽW¢éÛ]ý(®€·›¥å­/‹ãØ„�›¾W€zü°GýÐØ D·5A�!dFÔ�-(’ã82å£Nà„iâfÆ£/1”fl|Œ˜š0úòôQ�yˆÃYäÏéfzöÀIg¶Ä‚”Ê,ÛÔ©‚.ÈŠþcfPU¨06#_ÔM�›¢Å±‹#g-„Ík"+"MžÌ*cıqWxµ1å@gl/G@4P²Ð—­*ÕŠ áºÆ9o Ñ#�¤–WÒEœ.à¤E‡ZP™žbìï†JÙß –]cÎp¨iŠ�½ì²iˆ\Ñ�ó9‰Ê7¥*�Ð6™t(�Ò)çÜˈʎ~¼Tã¨ßYïŠ]°Œ–’ä ¢=oËրɶ9§$¥Û ½ ž;ñ³ÃA f&õWÞrÒ®:(PW±|ë`Ä–tÄÚ…îd­µ�ù’»UÜá8³•h ~ñ�¾Q“—LP-PAäÃo —;äAM™ `¨”Bד,8¬×lG7@d¤÷™?m %Ìâ 9•U`= Àü’Nþ¤ ªXkb­‰‚ºµ&ÖšXûD›cÝÚ'Ö>±ŽNëèÔiI�2欣SØužXç‰užÈ)ÓÐL@+bóá‚Ø¸@”"Ã~-?@€`àÁHÄÎtÎñ%8�"´‰Ò ÊŒ!8„‰°C¬AàF¾I((ÁWð�*’�9$z¾õ“Ä@Q‚q%Tþ…eôð)e&eÿGæÃlñ3Ä5ü$k1··¤ðɘXÌžw(¢aŸ2³çZ6zc(bþ¡ AüØôøsÈFZd�‚  ÉLÇ Å€‹àª>Ù •É…X¦hõŸìºù;ù�˜¸�?ïk{Ë ù Æ[˜B©Õêd ×/(øV«;C”Í5d�¾LAAIbÂÞ}¢I䀂ŠnôÛR§6]©ôºk‰qi7Éi¨+'Éø÷Ê\7l)Jõ¨hx‰ÁÑjâPCÅ<£uߘ­ �o´lÞàï™)c<þ‚_ÍbˆTË {’Lð2‹û÷\òuŽ)mh™_·€ÍDd£@Ôù'¿¼ÇvÉE?PS†(JÜU Mê1ª’7§qªSVBL™7d[:S ´Q–½ÜMu K(ª“%=;‚RŽVZÎ$�žSLV£ZkM¬5±ÖÄÚ'ÒLÖ>Ñ~¡ö‰utZG§utZG§užXûÄ:O¬ˆ c¢oĆåyØ1h™†»Ó§ ¦PžÄ ò Ê Øïa ‹>”@’œÍ)pf¸`Іïù#]âfZÉ+¸±"øÍ_“$Ë%ra .Òê‚D`Òë”—$Ç2¹q ¬U1 2&W¼Žd‘H&éIfJãûŸ�á!éEO'€¤ñìedÂBN>ÒH …€mêP�µÉ�B)þµ¹Ê}ùe5!ÆÛ‘µå#7ø)¤PBo‘M"‰,9a(ž Ãù6™F�¸6bê¢ IK6‰Ã·êŽ¡ÎßDˆ ¶hE´¯a1f›‘�7𖌸õRèÜkà™¨ÖeŸ& 3¾Ø /‘„i2ÜEhRlÛeTÓù—,J!\’k•²çêLn ¨ETY1|‹ä;R]³`PD“R’«ÉQv`<¨K’˜“ÈUÒêCóÑf;µ9 BkÚÂÓä%– €„���·(JÎcÎ�KZ¢‘0�%zÖBjMD騳ÖÄZkM´&؈pÑ8Ô>1�xid‹jÒaØ ÓÄÖ>+©}"æQG§utZG§utšq¼Cù:OdQ牥F´#ª:O¬ˆ�ðD&ëTÄ«,÷›”•repÏg9¾  #e�éÌv/~TÁ�T_¢ì„Æl@€#HI‘]¬Œ$1õ€ÓýHP¢Ó ÁXÜ,Cpü ÿøC8ðHûhó b²+%¯2ŒµˆËïg;#“l•ƒ€ÁÀÙ M'ýä�™ØhÎe˜Ì_xäàJ]¤>øÍé×æÌ¦Àgì>„™ 8Ý—Q·ýŠŒY)œjÄöb:ˆÛÃ9½aÐ'QH)¦>LË™˜";Nœ  ÌXܪƞH!@'®%®DjÒÉ•:â¤Õ‡¸‰ùpß±à6.‡4ˆgDÔw�Tcº™ía×½AV…®>xäJ#ó$�>Šæ\…¸R ¹J!7DÍ&RE^ö1ƒ�uVøT´�ý͈!-9*`¼·²Ò—·&¨¨âCPY¡e† åû²/  *áúÁ5g8KÔïᣳz6âêÈbÒ+¥Ä�n’’ò™Ž42Ó·nËužšZ ^GF�%­®ƒ¼Í‰ ¼Òß1Â¥<¤ŒRLîtÌ“�Û¾A ÿDÕ§òÈZ‚¦ŠÔeÑH¬ÉÑüZ…~Ù,P�{ÖJ"…!ªÂZQVTkb­‰µ&Ö>1MxíkŸ˜žÞþ–½ü¼ç(Ì1 !utZG§ ¶êè´ŽNi2 ªóDZÌ:O¬óÄà0LG+bsW!69Õ€Ñ s~mA ¡?„¨¿Âü ÎÁX‡ÑÁb- +&õâ�€ƒi N&Ò\¢/dFe`‘‚À„¤C�·ú?‘Òwܯœë{Q žÆMF��¤C2%” ØÇHœ0Ä•ÅI ²ãr¦Øõƒ™ã7èƒrme9)(ˆ)‹bxe"I‚ñ²„ðZÎÆLÔ°"ÀPH ®¾ùQE_¼6ó(^²I-²ÄSÂUdB�çTÜÁÜÔe8”Â<°c€ÞŽDÕ-D­‰rpøN ŠJf#*Ì %¤~“ÛˆÍ�Æ÷p¥ì PÍ]RéáX„(9Ó6›Üs E…Šð"q#UIë‹ö�ߊL…™ˆ’�Š-R)&"ñšEªp•/1“éBG­©*Ë …I=Åk\( éB#é२€_±ö‰±{cú]Í£ŽN­'*ÕÑi1ŽüØÊð­ØzüöþïW·v¿ï»ÖZs®½mlå€ ÇH@â„x§BB;±ýgT8þ†’T‰ÚãÀÐ*…ƒJœ¢$N¨‰d—:­+QÕ8n“ ‰Úl?kιx½?×½{—äÄÉ~üìkÌ{ÞcŒëÇ÷×õûsÇ56Ð4“+¤UÆ��ÞÙé��ÖêFYbMåÎN[ÞubõÁÂv�¥Íš6ÿª¦ÜÙ©óElž½sqàÄlÅU+„Å5¤"$–è½ æ¯{D�)ϪXÒ‹æ1CGjŠ»Í Üá Åѽ ‘‡•4õ�E©²;a )C€a˜ã‚÷44­4ÛþM jRd`VÔÞÛ¬+Ü#üˆ@H¾.MîM�4´²êÇ�CâΉnBª5ï_Büü›ªÇÕ©Å·ŠO”¸v—Kmu‹«R3ú´’v>_>µÑRùR}~òæÌ³Þ‹å15�¶½:€ÀmPÏ·7ï@}¶§Xo�ô‘.d��P#fŠ¡õ–¡d-ÃÛÿ�¤’½¾ÙO@‚‡=²Ê'"ˆ©%l¶Y–'O–¦á»×çW±Ï½Â‹R/UÞ+.И¥q.fNw¦õ†Î�ÅÈASÆf%À>RIÓEÍosnK•^‹:ÖIos9Âò–\\‘ #[óÕ�—?÷¼®²ñãÙR7ê),4-‹šSÔêËäHI�D¶ížD«P£K¤Ù2Xk¢Õ9캟Ÿ¾ªª-o‘={y*J^f›ð² ŒqðQ'î rŽk5£`Rùă€Q#íkWDóFÚ6jU”àÝJ«7á:,L»r «ÛoK¬zÞ–x[¢>쎉õ®wLl¼câ��ÞÙéf�wvzg§M©ï:ÑŠä®ï:ÑóË⺅lëÌ‹ØXž·æÿ& 6özƒ ¤Xãy TxšÛm–¤0„À³|unþCHÎUQÁ�)N@� ,ÐÁÞà€Ä¨„+Ä¡ŒÑÏ1­S”ƒ5º\ a33ª.AŒ�#Wß“@(õKh°J‰ö7žË9žØ“ÉÝçuYâ˜"2¸¦ ­mKÓÑ5ªýõ).ìHД.l¡Q.ÑùN”2Kk]@Ÿ‡¸_<ÚJê1ÝÉ%²ƒkB7$2J<yB¦Ç¹Å´®9’3«Û,H•è>´v7€1\Jå#ø'u¦?Ì(„ÈgfÌ4•Àd R¦�Ph1rÈã~OÒ.QÚBÕ$YÞ 9sƒDE�GM‡§�Mª�:  *çáž ~ì�&ÇŒB%hÖT:¢%}•¶´YÙ`Ã,€¼X�#3"ìç*u¯ó(�Bò|)‚$‡æ~)ý̹lI’Gº›/™„O ŠÌâŠcÅý5ËC¤2/hͧ§iq;B¹"Šò°zr‘ôkE·%2óŸrïZ%è¯Ï u[âm‰·%®Ehëu4˜;&ÖQß1ñމ�g¢Ö qg§ÍO²ÄæTæ2wvºé“ÜÙé��6ßl¿Iú¦w�Øccw�x׉Ò‹Ø;­ñ¿=þè›ç—�õ=¡¨{ïd8€c^QÀ³—x:Ìðý®œ‚×ÊQ°ž…3qó°k“ Ì\WŽ«{„Y™ÌÀh(ÐÐ-ŠÌÜùÍ=�¨dLDQe'uŽ€™ié§z ÉIOhj3CÀ[dä[tœSHI«W}T°š«Ý”&¼1+RF˜ŠÒÛ�i‰€'#¹ª g „ Çã…‹Ù)¢¤[“TÚ÷­ÌIVwuŠ3u☠ÁÔW!ÕB¡¥ÉR*ØÛSï´]…�“Ïm‰·%Þ–x[âï˜xÇÄ;&ž=hï˜xÇÄ;&Þ1ñމwL¼câ÷1hKj ŒzáûÀ- …=€/„F "!”"k)ÃNÚ®~¹Ê‚áÛ£›e ŠêqîƒWU.c¾Ó�ùB÷ûðË�f’…o…\,áÀ�CBè$ š¼Cf¤žg@ÐK„NŒçG Óe fÒA`J;ªèXFµYÈÝn%1ÅW¦ èr= Ç…›áfÇ­l†ÈÔeL†0"ÒE!t�ÁaƒÖèàÌjCs–^‡nlœg΄"Óð´‰B‰#�0 ätŠ7î¢+Ò¨<¬Å"@k*�D!@Ù&Õ¡1ÆÅ(y6HdÜ =ÖŒ¢Ì´{›4&ÐÈ�´â‡ODú•ùVK¤xÀY¥§ š3ýÈ'}åXžýÍ"‡l# BG&O* ô2ÜÈgƒ˜Ê½:Ø5v¾pi!=WwÉ$«t9Êñžf Û-Ù¸¤ŠŒ}‚ËSýzûžç­¢k"åº-‘nK¬¦«�·%Þ–¨[¨kÒ(ÖáÜ1±_=áòމÅÚÆ��6Ó8}ĦHM\îì´éܺNêŽ3=;u׉:Å»Nl�lEyKöVýƒ\üÀ›gÎj-ýW@®Lé Lõ…ºñM܇ŸB40T"DNòå*®}´ÓØ�Я(¸må0§·Rj-†Óô‘�`Xˆ;d•EœÇQ¡mtÄÖTÓ¸wwÁW}ö$Ѝ�Ëñ“ÈßÞ:ûç¡7¡oÂ¥cu)<9ãå3Ó½˜R¡fË0*®ôL3qüÊ]¬gg]ÖyKÞQè”pu¤ú^ @§]1P•4Á”Ç.¤}ýBå´ï4[p'¹&Y",ã9/ÐWÌiud,˜'vª®¦<ì~è%sÿS,ÙÜõœég1AK³¢�hy*¹‚+¡ •B¾ð¤XM)A"û¸YfEÜÛIVB�h]¯ú”\úêï)°Ã+=UmD.ì¶ÄÛWoK¼-ñމwLÔÜ1q3—3ð%ïìÔÄáÎNkÍÊÔ‹MÏîìT_qg§LÐ<{SîS9Îr¤…Ä™v?æò~,ÿ#^'žõE‹¤þÍéãgЄî®Ã]'®Þuâ?í–¸%ø·�h�½5ø7¦%>Û0ÍöS,2×]×AÎP§0(þXà >º’zV{=î[ƒ4 w`?ÍKÿqýM™Î€x:œWu-Î!‘Ùû"ùιE½Î`,b`‘Lo6+3'¨D’«VÀ�@·‡vÔó `ãxõäzt~_š%�D¼úu GkÛ霒NjéÝs/H%]2¥I «8%t˜Òà ïøtA¡Lp­ ›ä WË íÉ{-ÓÝýÛÓ{o7}y}bÏ­.É>óþ;oã0$Ænz‹ÅÅk§dQÓ€¹òxc%—Šl¨t½7Ìb8ò ãBg®\…sŒ{•’Ð…5\È6èèÃÞŒÐTF¨Ó‡v¶c§ Æû¬1‹×žH 2¦ÜŽ%�s2T‰ò&ô6U'ñÏÏxf™†¥RžÚ0×- 8n ò�L0G�y¬)ÌßìØ;gyºå°§Fªta¥J¼‚xþ´©ØðS„ ›«šïÛ°wÔ&o…/5�” %žŽ3ÚìÆ¬4}°LEJd¾‘i;…"²cú¡ÌÓs#¸Kd%-òù>:3F�©¤ðùÀ©Q«†Í KD­Õ¹Û•S%u[âm‰·%Þ1ñމwL¼³Ó;&ÞÙé��~“׉-Ì[=`“C‚—Øåèðe™Ò:§CË%«„»Nl©t׉w�øÃŒØxf4)À'¨Ìœ)%ÌiHIˆÀʚˀˆP„pÝËRú ± q¾à>J€…Óׄ7”¶.ì2ÔbhEÃr‡ß`D�•�è¢pbѰ˜¸¥ÆfDºŒKdbù(ïa:Å8!—1MåÜr[è»oIRû «u²ˆ“(àåÈvR/ÓÙ’*Áº+8ñ|X…¿E"ñ2CÞ~;’´#8'æëO€˜ßMyY|w¥‡.1goYS¤ï`¬Y(ö—€)ƒJƒ.c^ù< +V|ÁA{_@ÏD_<ófþitÄÌ^)Vú2eçh•¡W°¨nº�{©»j5Ön�}äˆäø&ŽŒNt©ø"R>¹¼$CÖ6`¢UÌô=‰‚Çb•Å…dm¥X:“+Æã’åk€Ì*'Iµ�J’< 8;ñ¾—’¾J1ýN6Ë)Ï”^„BbÕÊáÐOJ×ø’q™ðHÄfåx2Þ–8ƒVpY:Ü¥êp[bõh•JEº-ñ¶Ä;&Þ1qãæ�Ëwvºi…ÙÄ×È/ãÅÓ‚›ºÜÙé��ÞÙ©9¥^ó4ˆÖšÊi8­N§úÿs�è÷úÍN[,}çßÿwLãüù×þoÿÚÿú¬5>·{�Y½åR³W)3·Ûï:±Jw׉š›…Þf.ÿd-ñ�×:ñY§0 h£§¡¿âãVv…»™>ó­ûXǺѶùï_óú%õÂS¾küŒÊÉ+ªÞfˆ…¾&˜áÃ7é¡J7p�X¯ÊäŽöjG´œµÜõC�7‡6/xêñÐð½ w'€K9OßuºIqÝêTÇÖ3îv“ûðÊMî³ÍåŠÖJæ�Ùõ¢}×›ö¼dxˆ¸q.áK˜GÈ9õ&V¯¤¬Cöy°ÍËnž~¼êêUG‚]êNSr ÃŒnùî­ Om8–]2ñ›7¯JüÜZ) ý3ToÝŒïç¯ h«6«Éü¯‚ßz/çvrËaKDðÔv?“3ç4?¶à™$àÏíf¦G'Õ!it´ýè+´¤’4²þ¼œ•zlÛ¸—££ƒLèå«Õ��´m‡ÇIa¹cÉ¢²A¢–>pïUœr‹\F¡ô¸¡åùÅg­—³Ä‡·7{´á‹!–ª y$$×jØ<Q5Bzwíû§§w«ll¤ŒU”¼½ 4Såž—(h&g²‘©�j Þ>­èrÁ ¶ž(áp¶¡c7¥‹Ë¢�19À[Ƶ¾ rˆªŽÝ+°‘/^*âù¿-ñ¶Äjüm‰·%Þ1Q‡~ÇÄ;&ÞÙé��Þ1ñÎN¿)ëD‹ªïþ�ÿßø·þšYþw~åÿØ’æÝ»ý?ÿïëÏÿÜûgÏv=[d}´Lk}Ð̾_Þw4ó¿ëÄ»N¼ëÄJÄ&Tb(L`—¡ùTÞ5„Eè;ê:ê<‚ e8ß Yöz˜¿Ÿ(Ô¡>&ì"²Ò ½(xAá-Nav% ¨ˆƒc Äu³ÏøN’b±,±á8:r›£KQÐù/^À¡qÂ’r)ú.ËXE¢\CY&ê�Wz±§0d�]o9Ò†É5°æÀ…}•GP ƒ„HB´ìÅc=Ý]&Á±Ç¼X$�-¦SUR¹‹Jìq„T•«^¼üÇØ "‚2�­ä’Leá‘è³�sNmF4Ðjjz‚3ŒL�Ò)K‚øJ³t�3i9öA §¨2IâÆ§ÁqbNåR+�.,YĽ�òS8Mº]ÊAròpèØ-wæ�–‘w \#\d‹‘wéAvqÜá÷«J0Âû:ÒVŠÒU,ç9¾\Mç2dü²¯– �q8ÚĤÁ¹?ÙÒ  2Aúœi�¼G‰ÉXxÿ8M´BnK¼-Q•ð¹-±æ±¦²{[bÕâa]EŸYhýZ]è%“¬o5&Ü1ñމwvzg§ušëœ7�¼³Ó3€ÜÙióõ?ì:±UÙÛç?ñ×ÿÚ× Û¦)ï~üßþŸÿƒÿÑëãw>>=þÎwøØüÆR`žw�Ø笼Խ³ÞÙé®ëœnKüÇi‰&{¼›çÖV4ézZý¯üC£ßò¨Êë*tâàgg®_n`½yÁë*çÙ£ká>·6Ä#.dâ%GDÈÜnsî‘‹ß‘�}Íš…_¨ xç¹§ö¼¯4ø$|+'ª8ÊU¾jdr’ªýÌÆ0l+䄼$l<2ì©Ð÷Ï-ߟø!ù£ê0½91Å<Ô,o+¼Fâýûy-áC‰Øþã¾W–öêÑ´ä×DÿÙãc]ê‰>#Yô$ík -S!XJvPq+)"iÿ1»¬I‘ 1sJ$sÌË»íóçOÀô’&>"Ú¹¬Û´Þç—®jNØ<ñ(å#a¨’÷rÚ%NÇ¿GGS¡»£`|ËHž™ïã\×ü~ÓŽnfjí'G8>gdv.ßÖu¼ËfÔPœ™¬ºß³ÇǶS7µ’½šÒnnŠ?Ã!ÜœgY*�”'kÞ†�JzÊ;SHë'§É=Õ|å�Gú�ƒ¡žyÁ¹Où0¥US,%¢—y{AnìgÂö‰Ëð³žÚâÛ)Y+:ªnlÉQ ²—ÛΜú¸ä‘Z 57Âô]ÖÞ9âÕ­_­îIÂù/I`ˆïla8-£“Â+ÉUÑ©>&ļ-‘ñW·%Þ–x[¢á㎉wL¼câ��š3Ü1ñމwLü�‰­ø>þþå¿úÏ}ñwkeѪãíÿõÿþÝï~zûÑï¶4z{z²õ¶]žÍïï:ñ®ï:±Uû7bÓó†Á aR�„ ¶=L§@øä&xXßÜ´ÔѬÇÃŒ‹¸p(â>ôº0à!ÊþC<ŠˆJ·]†,-xL1Ø ™E]ôœŽ-®…¯´PصXç’ ÖÕÁKÂ1# £ÓP�žj‹ìVÚ,‰ýhÞ8QèMåÒ!•°T˜*Q„¢:%DW> ì&úÉ0QÜœû…¹ ƒI¦(qÙ–aç]Н˜Ë¸¼iÁ¾t–'fGÞ¿ëÕߎ «¶(®Õ�•s¹cSÌ—1Y‡jÿãƒ÷8(¡Š¦O¶q¹Gå‹r‰u„£î¢‡ZRÈ›„·%® Þ–x[b=îm‰�^e½³þ©.IgáSoá_—r® ^?,´¿;&f˜;&²Bu掉Æ×f1_f�ösæ5wvÚ Ä¡ßØ4E�iòrg§ÙDU©¶ì“‘ÝßÙéû:Q=0Ú6ò~ßñ_ø¿üÒïüÎï¨0Ü+žŸ?>½¼:KÔÎ?ºŸÆì»N¼ëÄ»NÔv~H{\�õ~ÞB¹óP¨^_æ!Å™Çd.S€£Xþ¦óa5¥ C8Qß5xi/°7¯'ÌiP•©¯ÔA|ölË«-ðNÀÀ‡WÛ‰ jxÿö•‘85,ôN]ðóÝeâ\z�þWr¹W€í0Ö¦bòq7ª{‹üºªØ¬,Ç©<ŒÈYöC&ÚñzpÊŠ«Q� ?Ð&¯q$Éœ¸è Lkç¶WVíê6×À›,*O/U�¤`+õëÀÓñýç�ïß>5p nm0á‘> ùö9ç0Ž€bO»¾ÌkÊs&É–X(vß{yøa6xgdÑ;6,…‡U¨ÐÈÉ#QÛ4޽£´1�B<ÔÊÚ† {ÌóóËpKá'œ37ãºHrÞa’ÉëÇá‰%¸/L¨¯m5Ü_büþSÌá®*AóÄ\}˜©2ˆ´~!2ë³oaÎkT‰·D²PÐŒ2s«{4{{÷BôH¿�Ë^ª*|õ ÿ Wª x{Ç—�'d>håϤÑ6i«žâKäÊO1ÏeÑ=Tú%�˜lÉŽzm܇YôWµ§zÖ®`ËÓ‚o/Ê4›äÁ·ÒËÔñV�„�» IžÛ+ÞÛoK¼-ñމu‘õwL¼cbÃé&wvzg§wvzg§[ý@׉&iŸ_ÞÿöŸûùÿó?ûÝÿào5Z}9þ«¿ö·þÓ?ósßùîwí ݲÁ"å£5Š¥¥‚¥cK˜Vw�hIu׉w�øC†Øx­@@˜‡ïp&Wm6¿~¡ ï~ʘ›WHAi¥t!{J³0°H0•Tõ?Љ¨²(Š=Qˆ®j0Ç€3YÖæ:Bøàd€ÈB|•1 CvS"ýø?¹š�æ!´«‚=¥H³W'hÕ8…ïu#ñiö�0оžQ\ÿi/aI�úGÒt)©IEºöƒžžÁ‘ K#Ðæ‹e�jøÊ¨¶œxÔ*r�÷J\ÿya"|gNg‰"¡’w‰†ûøT8«Æ‚¢Ø½(¡ä‹_a.7¡ˆÈ&elÒ‹ÊL—¥-yÙFb¬ÊŽ�‚Ü)ò‘ÌÁ´+þ„Œ’�`\6 ÈŠÎ�"=�l ÑP4q# ƒ’ö€[e«$Fy_É3�]’2ÁjQ‰“úkqF5±¤y|¤µ”QXQ«Õ„L×Uæ¯M?ËWÞpCµÉÝ'Hñ{*¹;Á 9ô3õR{£Èмêס^ r•>¬Îåm‰UÔ,²ZÇR•Á *xæ³Ê’ñNa‰gãÌ|[âïWý/Õ÷mí¶ÄÛï˜xÇÄÓ•®K¸câ��6ñimÖ媉Ô��6ñ¸³ÓêÃ��nÉPOiŠy,òevÚrÃzÀbë?üÉ¿ñoüúý¿óòúòjË›×ý€8E¤4>5f™݃lWìAüH*é5j!PÅォK³Zß›mÙã³·ïf8GUÛõŠßˆ1¾-1›ªJŒÊà·%Þ–x[b½Ä� zÔ;&Þ1ñÎNïìôÎNïìô>&š¤žéoþ¥¿ò#ÿÊ_nÿõaýðòéí«OŸ?¾~ncgËË•÷O¯[MÞuâ]'Þu" wü0"6Ï�ÇKmî`áEÃ-Âpòárd§Àaw] ЄJp‚¾HD‚�¶c¾�€£!)Eº/}döé¾pÿaÂQ�0‘-`‚âÒÁI‚†’ì‘$ ©gK—¯Äd#ÁèJ|é™×tÌë”zÜèvø�ŠJ¤¤r„ôµë2”¹`RH°É+}¡‘^ÊÉ&4 F0`¦ïý°4 1_¹ÝM£L Äþi:‘DL‚x]�ZyèŒ|¿½t>_uÆV(ð)�Ýf´…* áè°‰@™OY=¤ M1©Ž:$AS¥ÈbÑ”µx̤qp!„1 ’;Yéè�’È9qŽ˜'S†Evõ?…G›xÀÅI'û(Ä¡Iï3›ˆÁ{>ñ¶Äc›Ê*›3TFë:#KfØÛoK\­¨ß¸-±ßô"wLT'êk£—ußwL¼cb¯mj1Üž¸±eudü†–¯‡š3ÜW‡¾ Ä—)ÄkÃørßÙisž-6Ác®;;­+¾³Óú]Õ¡ÉÚK#õæÉl[¢î ×Ñ=ýæOÿÜŸtýŽûµ¿ó§þî/ÿƒ?ûó//íÔÔø³NFŸ³Ê�â'yÖh¡p׉Վ»NÜ*±…Î]'ZÏ[›g/€<›rö´V˜K@PpF=DŽ8ž ˜÷P¸(¥ÓôOüäZðj0ŸæQ+L¶6!¨o-ÑÛ‡�C¿x$ ©ÏÍ=K’|âÚåªgœ%Ë#ª¡ýFÀ77j¼º¢ÛìHfÛ¯”zªÇ1;ù±Ä$¯»y*yai€’½Å^yCÙ•ÌK0¹‹ýÊÆs€-Q¥N„Àžp–ÞåÙ›L'T}cÏ©ËW;»­·œ)#Pf¢ç+6–Ü¥( £ÔðcbœÞ”¹)H4%“%ƒ¹NQ4]¿rÚûpÅ^XZìõöÌ(M&#ä\¦¸®x$KdŸ·3Z¯'Å.Ç€ ^~4‰+£ôØ¢RÍ›>>²+йÖgRNeñz)—»ÊQE¸R`Ö9îE4³y4¹rë-¶Qx)TU­ð}EýÕFgû¸Á'º/Ççl•¬WÝ:ŽW²ŸßÛÕ.�_‚ר ÑÉÂtÜoþn}²ÍŠíéìD¼„•úçôKuœ¾•àl¡§ù$PüE ¾Ñ�œÎ¾wÅÞuJ¥`ÅÅ>ÅtšÑÔ�¬êM®9ƒ´ècPÉ.EffÁˆ“¤$nºyJ£ÑÜ–8£Ü–¨vÝ–x[â:]Œ®éމܭï˜xÇÄMîìôÎNïì´9è��þ€Ö‰,oÁõþýǧ�¿û½Ÿý‘_ý¾íÞ,•¿z³Æµlí°Ž'NræûJì®ï:ñ®·þáDl†ðì?(­ž¡Î!ÄÀEÝDô¥‰ÿГf1W â†#�Ʋ2 �jHH²�^O)ëˆtbÿ€¢û7bñà!�Î+€n”ˆVNº!_GÊ´ ij@ÔÜI å`1 QKRF7Ak²õ›Ô�á$G[x�ÊIƒD¿ŒBˆ¢S è2ƒ:šaM“’Æ"(ùà\ìnœ†Ñ,9}ףд˜Í_‰ôÜ ŽSŽ”˜q5Ãpvê›AÒ×ñHN”ZD•$SŠ^MøÝe‹é7}ö¤):Ùª<ç ‰àÈì±I°äÉJ§;V´2NƒŽ #÷1BÞn#)¡N>3èlôº0—\ÝƶïD÷YÙɘK9<±ì4Ju¥ (Äb{Ö)¯žokÇѤ:*$Ðv¼&ÒI"}ÿóÜG¬%Ñ2M—îû�aòè�„Lð¾+ªeë©XÒjX«šZÌÛ+ñjÆm‰·%ªZÈm‰õ'uûl´a‘S9vºcâO}x´ƒî��C›„5§º³SÕãÎNïìTƒø'_'š¡n>ÏÉâÃïý á?½ýäßü·~ýÏü싽j4¼œVZkä¡p[â—qû¶ÄÛÿHZ¢^ý�×:‘{#íà+~M½n3Ä‚SP3Ø&÷.L\ìvV=Þ�¼íûërúþüî+° g4Êé„jT-æ ¦{“êÕÓîu==”„Ëi'¬�·§'NH°®QÒ°=þBÃ&PêÛ0ü‚GXaù˜Õ‡�T„gHWAÂPxY%�¥; Âß@N!DíŒÖæua0ˆôîη'¸ ¹w}~÷q“ú^LSÚ„ê: dž[Ó§Yä8ã}7o´ÞÔ饥aLU ç˜H™aÐLd"8ˆ_7mÞÇš_:$CðÎĘSVÁT Qævc¶f–m€AâÐ1&jR”™ÂÍ.¼ÞÂ}ÞU�ü D8©Î @Q±}Þð�|ÿB{Ä&ôÞ6ÒvxñŠPÕ ËÉ…XW2×ÍçäÝ,Ô²—\Ì1í¤C¡a§ �!á ÓMD±È>TU„%EZx#Wñ²?ÌzªFOd–�lÏiþî+”Üó@¤ÑyR”�#,Ÿ;1œZ§‚¤°R�Ùßyé.[”dš¾òD7g ùêz¶š±«’£�,H Vêî»SQ+ì8­Ý”@™?Ùû�Tåéõ·%Þ–X»-ñ¶Ä;&Þ1ñމwL¼³Ó;;½³Óoô:Ñ&ä&û¿÷/üÅwÿöÿ„ÿëãÇ~íWÞ>ÿÞëçïxâôõÕ³W9¡xÐìik)«›»N¼ëÄ»NüáDl<½—ÃMî°õ€� £0-·Ž@—ƒ#˜‘„ /�@›Â$ÀîüÁÁpÀ `W0EPG�ZÁ e0 N È‘Ã×Ä(R+ÆßBô=2Û¤ÿk„g,(Ê8$Id�.ðÇ$œ~%IˆB¶,§° ` ,#ãhwïÂÛF>& †5ÀOˆ¬QN‡3LŽ’ÌÇ ³É!ÉT$"„ê�qR©BÒ8ãF*¬ma™Í½IADO&úCnWÔ—ú3WqÜÙéæ"õ ä 6›°0•AæÎNïìtSûMH«!µ�ÝÜÙé?|�Xïòþýoïç~¢æûŽŸúõ_þûî>½¼~zµQ‚ÍŒšÎ1ïY˜Ü–x׉5­Áw�ؘݚ¼Ñh—¼oB‹Ø÷a) íÛ�ØØ3,ऎ€Ë·ž”l4O¯s~{÷ú•ë·�P’ÀŸc“u-urf œyŒâú“z舵s˜½Ï¸‰µ‰øâÍþ]ùNþ #vý³û©žk^[Çкh¯¸¥ÎE-o)s‚fTïßž]o®Ð¤*Ñ{�iiçIf+3²öLk^u›]<Ж„2ôîã¼¥TøtçztÒ‡cå ÷údW8A�O±¦s¯y�+Èþu­è¥Ÿ´�À½mSš—Uw›pçh5ãÑüŸÍ‰,¢¼Ø›¨½C")lN7Jß©÷~³cX@³p· ìi§¹¬%aV˜ì-ÒŸÛxï3 3ÕêmÛÁ-YSMx+vAŠSYÊdÓ2jrê«ÜŽõ+>¢’,ñCXWúdI§œÆmØ’ÆÎñª :ºNÀ”!O%߇°9–¡ ÉzöUƒö³’}Ð�Ï�±(´ü‚´œrÞ&jRW!&WÌì¶GŒŒ*°JíL¨!‚±:Ge—8äÙdýÓ[ÆRH•%Út5µ©T .Y¢mˆJFiÕ(WȪ·m®¸BË2å~WË)yÆ+ÓÛgOƼ-ñ¶Ä ·%ê`î˜xÇÄ;&ÞÙé��ÞÙ©Ió¿Icâž“yÿüôá÷þüÏ~÷?ø¾íÞ~âó¿ú�ÿÚÏúøéõG,¥¿Û²Á’àý]'Þuâ]'þP#6°¶aõæ7‚5,ÿC@å`Ø2ø' &´Ä¿®¿ï ˆ¤0•:•ð…/IΕx±‡ByCöuð!<è‡E ¶½æcz–sÄíÅ/„T…�U  9%M“DÔ'€eIªß'éàɤ™œe™ëã+…�mÁÏEF2T.Ç(GëLÃVG—\fËDßQé"ÅSm\…MŽÙña&žU¿&ÐKY”'1ޱ£ �@ÈÛîðƒ„‘y@8Ø$)‰ãèq¤HŒœ*‰ä,q¬ý5ؽ(¦ã#]¯9HY&Õ#µ\áOóf>R†/åB–ÙM€0ÅP²ì™Òt#n¿¬žW ôe]$n�3ë²CÏÒ#QF 7¢ä¬W÷P°#LtùŒ\E&‘£ði•Z ÈÝSg¿ô’ß1&Twãt„™8ñ[KÇñ–aÉÆ5©»Àê^¡‹D Ñ¥Hª]ÎÌËs[âm‰êÈm‰õ$kC5�º‡5”ÛëQ²Í:“õ&wLÔ«0wL¬jÜ1q�CŸ¡VdŽ;;Í wvzg§›o¦Ù”õÎN›fœ‰wó‹Ö��µÅˆFóe.¯ùüÿ['úqÝDåûßþKÿÒwÿ•ÿ~tþÀñòòùåíõÓÛ§·7¯t­cBRçäÏwS¾:«#ËÖ2ú-¡$Ù%r�ø9GÜub”áî:±¹�ŠÌÕ¨‹ØüqAlx|½Ùû‘+Ïžeò²ÍOZ{ÅÉGwòuß°âõÕÑKë>¸Né$’ta5†'{zéÛåu:k'×àýÍ ˜ãï#Ûp?M( g¿ìWu¨ñ9_¶ ¯S fÂå½çcuˆksÉ¥®åX&ŽHuNÛ±~½`°Hu1sì•ûãh¾æ%º7�æk©:Ãó×l-†b^_6dïA_iËÓLΧ¡o~‡Æ¸�ÖÚÈKð»ÞâÖ²=åØ�2Qå¨Ló´Ñdø²=šT¢�ßÖzý�7£&áž;½{ùÀ±K�rɘyt�§ˆ™é0ÓºòÄ@9Qa»!RpØùÒyc&—Ìèu*îñƈ<¶f_ž³eúähW²T†p‘ïÐú䞣äS( m Œßƒ%öc�þÈÖKß"²’Íä'›oÆ©�iŠ k&EÖ«¤ªJ=šê�A‚èߨ¸—ﲯ„¬‡é3zR~d*ýÄ<^Ä!¤´~k 2UѤ‚›u«¶|2‘#yS3Ú¸G: LRÙ_”I1cSf®˜¾sÆ$=»$´K"Y·ñà»—g»H•@mè<§Æ¼ú>n‡·%2Z¶ìï¶DÕ«Vp[âm‰ë$®õމwL¼³Ó;;½³Ó;;ýF¬_ß½X´´,þþÃvoê?ùå¿ÿã¿`�è±Óž<c�ñ�-'î:±•V ¯Fw�x׉­Ï[òÀ¾åˆ�Mû-ü{fï ’  é7� Í¢Å_Ѐ4šV‚u�Õñ.ÖzZ+—Ô”gýÂ2!wˆY4$ÔÄPÉÀ‘çVdHØè¼,¼Q<àÑä8�õ�DÅ#Ç ·q~‡yä§NONæ°¼"GJÊsŽ¡(J6(éQ²4Ð S!%Šãò‰›”zo©8±ãé?êOvé2yºßEñè=Z. ŒØê˜ãœL©Ó$ަŽ,"‚R9#–\YòX"¸E ÖXÎŒI—${gÁÂ�\Á‡l%¡lÑJá 3-2g%´à ˜Jº€«Ô ƒuÈS #K�Fëq/^9HØ8%E(bá’óÈ/üpÎ+ £ ™¢(ý�lW‡Ëwòï;éä:Ä!Jå: t¬\ÒA˜E�lrL‘b\£P|°ë’áS®Ì¼ÜúË(G0-3ÂLŸÊ{±f*,oRíX⮲aJÝ–˜½*�Y(ÛÝ–x[âm‰ë.ê…êÁ×BÖÛÜ1ñމúî˜hfrg§&Õ„fgòÔ´ÈGHãj³�3š8›�lÔôÆÑLEÞ2lxg§¬vg§U�;;Õ.þ‘ëD«Ÿù%|þ­Ÿþù?ñ½ŸùÑ_ýÛÙìËa¹õúúòöòÚ‡k€öÕ$¿ã¶ÄŒ°Î©‹–_ë¥tMw�È"°�šr׉Cn¾EˆÍóv-Ò�ûÉhýùÕC{Ð�… ­ fÀݧaÚK*C2øï,ªU@@Ëû÷ÞJ·VÄYë¥w•‚…Ú0=Ÿ"°( “X-zk¹Ïú ½�ADŽyM= ‘@ží¿ßÞn¼¥ŠÍÇ).P'çfÖå~9hºáôÒëRçw&ãK�ï�àý‡´*‰}.‡öÌwŠH=µI¶`8¾c!'dL—vÃß^]lÑ¥ýã`5dh’’¬¶°“5½$I"YYÁREez&Y¤ßÞ‡q �õ‚9cmS·¶ L™¶1—¬�Ùÿõ“œöÞË ¤8f!š�éÔ€:T]Y‘‡ºd ¾ !ói½Ù=µÆÊ}¢ÞÍ?õÜͨ™Ë[dí!—ÿV„Ò©ÒólU€ ß±èª&<ºfmf±^x,ûÓê ¨¶£‹q0¢^ôÔª\×¼Î6)bÇì|Á²ÝþŽÉ&g†á|ñRʽ³õ �b}^²-¸2+Ûø-ÞÕ™cõyQöÝøV[¨ŠØ~p"ÀË©?"‡wµyQ&ì9g÷4¢­‚_ÅÏ+±L™d–¤ y&ùÛk\X­ï웡„jù�F¾íÝÊš<¥� Ý©H…Í•nKd—ÛoK¬™Ý–xÇÄ;&hî˜Øg§wvºæpg§wvú_'Z™¿ÛýÇ<¿5Ø÷?ù7þ�ßø3?û–‹êˇ·7«#),Tî:‘�læmP³ài u׉w�hÉüÃ�Ø@„tõ«ÿ�áiÏ‘öÚß…ÿ�¾ÄÖþC(ÆHH5*á9ºGëAˆè!ÍáF‹h=]{+g„ÂEºÄg€F]’+G × °�”uVÒ J~ÊÒ!ïÁÕÀ."¨S¨C\£á)²’€ ãc-=yÐïkBÈ[²Âˆ ‘WÊ…‡�+ßé°ÓqÂSHêÛB¡à”ˆ¯œÅ³¦‰ž@âRýH÷ ÷£”&}ĸ�Ó#®ÉõÂÊ>z£;I�ËÔÈg´˜HöŤ’Ä…Ž:þ7x'*Á~ºÀôÍÐK—Ì�mì»G"�]qgÿÙ-"јì #"ì×Ö`§Ø•ë”Õá›ü>ž�:sñ“ö!Vꦦã)_:”¡’_²ú糫ޑ.!N¹÷¢‰±"΄KA(”3Yú|­¬Ö /†=õYÔ§c»DßcÔiRÎä éˆTÊ…p™záDéqðWHßfR”qzIé·³òe…%Mõ�Ü*Ûh–�¾Ý–8ƒ2ëm‰«RÕ�ÛoKÔ·Ü1Q—zÇĤ;&Ö96G0Ð7Þ6k3znÀ¾³Ó¦ÍJäÎNïìôÎNÏz¡Yv³óMÖ7[ÿ'Y'z« RÞ,ø[¿øW~äú—ꎾ?öÿÊë§Gïékñ)"Ÿ��_ã~׉[<Ýu⩎¹½Üubë_ÿßZÄæ9tÀès¿ LÑ ó÷ó}3½‡=äÌ£›°7YþOŸ? Ù+òÃ[^d¦}y%<ÁCdå×?x~övÇ`xL[WÎäè³¾¨ð&á+~Âþý|Ðò.ãÈ¡ Ýy†$z#ÌÐŽ'†¹Õs ßî`¼­¸_ù]8t°bÆ? ÀõŠ 1z+hHÍ| RµÓÉ ¾Jp€"ÊÃ|—qéRöé©m툂rèXò‚ÚIHÚ@VH¾zd_lanç�'$N�=ÂÒ\^ºÿ##ÚneÁCH:‡i¥ËÃå¯�©“¨¥Krd댽÷p:õÖÍ-Ù%):ıù„å«6g­&¨üê#ÙI±»r:$WR¸¦ºD¯Ê$óf»Ê&\,â5sKSRÖõFŸ8½”~å»_:0N£AY¤ì] èoƒÀ¡_²©W˜2 "xz©ƒÂøð›½Ñàd¶oi¬žhÞ³¶þÞ¶ÅÊãòUM„ŽêöÑe»ÈáÀÏL™Y+ZNh«4?ÑK‰ªVe‰ ®ˆ Z#YÑ%– >›´´-ŸLH¥º² uæfàè'[™ãn9 !ÇÇÒ‚Õ²#¢ZO`âÑm‰�Öv[âm‰·%Þ1±!掉  !wL4jßÙ©)Æ��6ýÝ¡aÜÙé��þ`׉¯-OZ›ýæ÷þâO<*æïŸ~êÿúKïÏþ§ï|þô]éZÅ~ðŒËuw�x׉w�øCˆØ z³Ä±ô‡@v8}†wÒºÓ�ˆ#r’m«øáX2�£10”#ø XÄô0’P�‘(#$D#Z`‹làDy#ƒüþ Ìo©Qbi –qDC¤Æ¢Øå.A¸Ôˆ Þq - —E¶\蹓£SLÂuÜF&X&A#—8Rÿ=k[Æð9Ë[Σ‡¼£âKÆà|úŒô²=rF~çɃGm1 ¨ð‘�sýÁƒ*€MÐTêô›KD¢½Î!CÉ’ûûÂ}ƕ⨋C”³dØÓ¡C q„â=‹I4|-Ð}qp#õ»˜®ôGzvÒ¡†DFêÑУkI³GÆ9_“9’BvZ¶#òB˜kÂô’wòUì3ï[f‰Ä÷•™pábfAüÃW³ªc•æãÈö‡YÁQή]v¸<§.Ïgu«ˆŠn0ÜI}VâV‹�Žk4ƒo“Í=T‚ª®¯â»®ò”ê¶ÄJÃç¶DfP;´ŽÈª× S…9•ò¶Dv¸-ñމwLÜàa„iôYW±–ñ¥Û0îÜ1‘1μ([e=G}é��nDila— R—jÔ¹³Ó†Ù;;½³ÓæëZÆ÷¯UŒ„þð;?ý3?ú~ßvo6Ìùôj§·7¯¢3Û×Í4÷×7Å¿ëÄ:ßÓÑ´ü©Ó¹ëÄzÞ»NlYß ý-Dlø§5�<´URŸ?|l³Z•?t£†�{�zpÆ`K9žIq@&¹9Í‘L‚—pˆ61 †ñ’O7¶°jf£›‰æÈA_@o 矰b殟p—Q–$~D Š˜ü¿Ì]EA1¸’Oà�¯y%kîKº¿„èuš%LjËòÏinFŸÚ4ä!)晴4 ò#{ñ(o¼‚¾˜èr«µm+:¹<Át’¬9â1P ×ªI’¢D¼^ÃÖ‹æ F ºm[³iŠÃxí¥8§'÷òÎñ<ˇL±¼‹@ŽZþeÍ,@L¢œ<4¼Jp·ò!tuÞôŠo¦™œ1˜¼4Í*!¨NGý ÿþÃÓsbç�­]f¿iD»›T§6T+*="±/Ù`‘^�ë]·Bß}ZkyêÕ¡ý@[îÆåÇQ ¡¶*ië:\˜%ÆB'cË…ôJõ2Ò¬OðÆ­Ê£ÝÕBDðZ1—ãÄ…¤v EJ&dfËùLΜTâçù9Ï·rö6U·ØWॗŒ.eS adÞ±KõbãZ8òö~K÷íløþ¹MàÚCNm­J §¯É†.]¸’&KDsÒ çTü\ ó¡T,™ã¶D¦»-ñ¶ÄÛ3¾c¢aᎉ ð §wvzg§wvzg§&ìŒðÍY'êŸZ'z(ìÃûÿì¿û/ýè¿ö?Øàñõ“ó¯ÿúOý…×·¯,^?Û(ÝZÁâBò»Nl ׉w�øÃ†Ø<Š#øïÊâ¿û†áO^NlÈÄã1K˜x¢)¡ÀðxÇÊë^È¡EÿZÖarÑGŠ!þEµÖGéTtØ€$Ä àô²Ñ¶ßݾ–øŒFÏÛÔêÍæU@ïßœ—Ú`Ÿ:¨¹_µ›šwXroÒ8xåŽ$JhÏÆÙm­¦³F„r„yDêÁHΚ�kd +‘ö�ˆ–œò¦‡¦Ž½ý$ç­ü‰ˆ5ˆ4°i¥2è ©r½·‡ι±q‘"KøU “™°�íT—ö Kþ6•;{¡ÃìL"P ò¾ta]êô’Ñ@o6Ñ¿ÊyT!æôø¢#�­Žm}'ã¯(g²jO6xÿ©ôI™ªˆêÚï.óùëeNQ‘ÍNøñŸÎWùw±æ15¸' ¡, žÙ¦hñÉÿ¬£aÀMWcçþŽ›ß -%È‘ Úç§Oì–m²2#sL‹ùH·#Z[Üs�:5G8œ1Sº˜mžpl“x'uš˜¹­ù0f¯œ-ñ@ÐGTe †#=˜"±>VG2¿¤m>W‰¤Ò’ðÌtîŧ•Öó,† @áÃlËÝ‘_âœÛ0ð5é„ózK®€¼Ä£*GÏf6uÛoR„u¼…U&zÔ(÷ùø‘¼—ÑÎ%±Ÿ±ž-léV•’|´øiY¿Ûú˜Ç\àÛóç·�C5m(g0&Ïm‰·%Þ–x[¢®b]ì×;o¬©ÿ½câUƒ;;Õ?ÜÙ©y[$ÜÙé��þà׉MOVïÞÿÖ÷~áO¨¾ÿø©ßø¥¿ÿßøyàÛçOž¶êw6†öˆË]'nUÙšë®ï:ñ‡±y†k…T ­Ø‹@ƒpvÀú Ý ƒØE°ŒÐBÀØAeÙOíeyÿƉN¦ýÄè~À bäv§}‰G#àÆwàH cµçZwSHÄð˜ªÎ#ŒÈÄ4|BžÐŽÝs·¹Î+¾æø²\}Ë�è1Œ|}Q�`²iÿcÃ"Ç+L»ýF©ó‹†‡1=K9°ðˆò°”©ÀPÚeÑãêˆnH~H_\ú?-Ýäö„Š9œBKzd”¶E‡"p„T}~ç¡Mb¥@92K�ö\•:èj”v–Ðá+3—«Û¦„ÁduzIWž 4³L°òqI`’òX¦øy¤O™zG–g¡¤WY<7à¯(ˆMøG,n)U`ÅrÂÝOòˆÂ»&dÚ/-±£ß½L>ªï¤zˆ†ú ®VÝU ÒPÞ ­„K<ùƽBËPƒ‘öÅ<,V`ˆ¦³›ðÉ$ é;‰W(+�&Æ)³U²]5ÊìRžj”röÏÿ<‹Ý–x[âm‰µÅÛuu-wL<½¥JqÇÄ;&+¿ ïgä5¤ú�ãwvº Ä��ÞÙé��nþZ'¯üjÿô;ßû ?ú«ÿ§ú«/ÇOþ�ó7þÔϾ|zyóÈéKãY ËV6Þ¹ëÄÖxw�x׉f½Ï�ðZ²Š!hSâÕŒÔ:ú�;bVÆq-ïj|†+µôÉWíí#•A2Ç­ˆxÑ»NBïò]0×ÀÒ1סp¥<•93QŽLŒ+['ƒlïÓ4åE†qðZ޶ò�î‘§\®CíáY Êç -÷ÑnO´˜H?·½A%r'¿_Ääㆻ}Ó"*W?2ô“¹½¸R# °êämB ="dzæ¶¾1Š6E“!ͤÂ!ÔÄkTY¦ÓH§:„ Î6Qã?Xðð,ªª, Ÿd IIâûÒË.%D9ݲC |J½£ Ú QäÇ"¥Dý³]÷zæQ ´ ]$žQ)SIË4»LÂð¥Ü¨ îC>[ôÉ—®@>wy0fgà£*‘(Ër~_˜©îáUȯlð¢n©P!‘£ß£eÚÛf“®%æõ’å ¯—­L^ÛÌ.Ç>€\‚gŠá ßâ}¶ªÐs¢A¼uÆÉû®rI-r÷÷áS,38ÙÕ�šç.ˆVÒ(×7ûÞ=œÙ”ˆ\U$’·‰_ùüþãëÞÈ*—šæ-¦‡JYm"1X`-×wÎçÎ�ŠØ•Xž�Œ‘GÝÙ ±Œi-Ê%-8efƒ,ºÜ´i'ÀÛ«£·%Þ–x[âÃ#ûމuûwL¼c¢ñÓhyg§fwvzg§wvúM['6JqŒxÿô[ÿâ_ýÑý›è}p øôòúÕ§Ï?½þŽ…©ÅˆåÖr´D±iuu׉w�x׉¡ßrÄf@0B¨RR_|9èóë 5(†M@6Z9`ƽ”tDPÄ tÁL‡Î�‹ i7b‚‘,‘«~é€"âͶ\ £0â-¥ªÔ5I!*áD DbÂB¢ '�0R‡‘�‚Jà~€Î;¿“My0ŒJlÇäp¤é‚€#$ /±,š‰ó��hY1I‚S\�n•E©•Ù þ’ÓT©¬I"šj sŸÂqÌÚ’‘ªˆÑ(6Å*—¤ŠF Q:¢Št@ÒÑ„8Ó:ÝbqÅw1u›Î&ߌ‘4ÌxB}°Ä'ð+1Ë•~’JöØd4'µT‘j†ìRúäZtß•ˆìa¨ž¯\a%¨@‚†mí8rȲ’6I£ëR®!u#–…²söž‰ˆ<ÏÉ¢ÊYq© Ûb!ñÊ+«¨U dÒj2d^±"™:éœCÿÒ9õPòIU®’OÌ+ÏJ]ôwEd•Q?zŽm©J^Wðd$7·%Ξì’ÉOa"5fA·%žªz[bvXS«õts[âï˜xÇD]�±b«óº‰ºŠ†p}Ä��š²5鸳ÓMWïì´²q†ÑV#wvú‡^'š·kWO­[¾ïø±_û•?ýwéïÿÙŸï5§y$l¢Ò²æ®ï:ñ®èû�µÆ·³–)Š>!¼¥^BäM�u¡ ›Ÿ?>y;䇷°–ž4� é(}0ÒÇܼڭ¬Wo攕Ë�´ÛAM†�,…˜i‹LûßDˆïltƱŠ/~^ú‹; ©]õë­úU ,õWùøëÉØkÂe Îj^eÙr† æ à'2ˆ� 6A7Gq)raÓ€ô¤–“Z:K)t§QýÐ{Y?–��QÜ¥;qÊ;+é¿©›(bã~Øp/Œ+ˆAN(­+>Û¬‘–(|óèÎ÷%ß0/€‘&ÐI–‚=#œ¸Seo=" ÅÍJ3‘ÑÒbÌ'ü¢²@r“ ª}:Sø)œ(¤‹g˜7²ŽrêíÍþ}ÔÚ›A©…ÞâIçÈU ü£KGÄ;Ïåí�§wÛ³¤dô8õØi�èÛ‰zH„°d4)+…¸ÄÎW‘Þ¶pÑ:ü¤¸#D-YÀ¯Š-86.ï?Ø—-?¹ýÊ”-”/ ÙÈ>WȲÍÊ«vy@âˆ(3Ø—/D¯ íí½Š´"g»è$tÄEÑ�a>jA·ÏS)åe‰ù=²·¢’¿ús[¢Â˜�nK¼-ñ¶Ä;&ê¤Áu wL\ÇxÇÄ;;5 º³Ófƒwvzg§ßœu⋯©þ‡¿ýÓ?÷ãßû™ùÕ¿ÝRáËq{}³£ö›…˜µÒ‹%’e†»NÜÀv׉w�ØÒø‡±±×|x†½ïé|<‘è^_1HeËRÀ0#¨H¦�†¥-y3äB"Ð&=.Ú©l]lAfV¿sBò�=„o�N€QŒV�FHÑrŽe)‡È”·à}–=9"< I’ ¨ñé–:¦s9 ¹“«<ìBuÊ(�«¸£5¸dô?|ýPÊ)CW²$ŠÌ‡Kiçe'¨°Dr4Sꬳ=fKʃR¢ ³©°AJŽ�À>¥ÊBl*Œ\ùé:jQ<dÀ2È»ñÜEnJó4KR.Á—„}ª(ºÌ‘ØÛ2--JZA�?Ó»üAãõP¿«ƒq–‰ hbXAcL³­à ×"3òÊS쨬RŒ¡�²D©¤+8 Ése „» úšÝB8]‹Jœ(MbÜ"VE;.feëR•”"U Dú`[5>²Eì˜WÑ%Qy¥^Py�¥'tl«GQ”BÚ¢÷½�êJl)Zü’‘‰È·%2Ím‰·%Þ–XGvÇÄ;&êï˜xg§æ MRîìôÎNïì´ùõ7n�x:j’¾¶€8“þÇœßé'ÿæ_ÿ»?õ³v�¶vj— 륻N¼ëij¼ëÄ/`…¦Ô*ÞÂø,¹¿uˆ ¿#êµg˜žÌ›3s÷ h ð1Øç,Œ"l¯ 1°ÖšrÚò©�.õÆqH§¹t~öÁª›\O³þR_ŒÒPïƒÍ D›§5Á@vbewNbèôkB³Ž�sXã—'=Ø@ “Sk5è˨— K—?. ÍÁàÙ{&£E."‰×ær6·­ƒ¶¸›÷Vï¨üó`Jœ:›üàâ}«T(ët‹ÅA‡Ä3X’p<;:–p“¶lÛìéHå ¢^p¾ÄšŒ0QÛ‚S’<ÏxC•Z…‡î9õë¬ýUY0îm'( ŠåexWAÑø&ÑreÁ†ýæ"ÂÏ…ˆl|°•Ù1^Åð±7�Îï/5©n‰ù¨Èæ¾]ávŽ7-gþ¤mE$côÊSÛ�aóÁ›s&ƒïƒV;åÑxyçúW}�­‘Ã3xSÊL6�'-…sd Êâõ ŸS¦ùJÎ&8&Éx…Õy·q™d¶�[±S*kJö¨ˆ®Ä#D¤°äI])šs¡xJÑ^w]’7{wT�çFÝËqOh{åU}Î.{ •Dâæׯx´½-ñ¶Dž–·%Ö[hí·%Þ1ñý\î˜xg§›IÜÙióÐ;;½³Óoà:1oÓüçÏÿ�_ü«ÿü¿ú—Ö`_?ökçõåí«·×Oo¯žëêá¤×™ž÷Úç®ï:±uè]'Xhmü-FlìËÿ@- ]ÁŒk:�ƒaÁ+‘Åu@2ÊRÊU�E’+ ¨*œ¦-ÿCR4œ‚âp°ü`¯{õÂÐ’‚OêÔ«GÒ��lcì ôìéPÁ¥/Qh[ä—ÏÅ©P�2ÑM‡'­Å­´=¸ ¢Ù2ox % K˜¢Òá–rä÷EúZª#öò%lF,ùø?r~!—)hª—«Ö¬™¹¡LÃ^R«ÜN=mêb@—çH‡ÖÄùØÑ÷Ø—v|Q@Ð�àK<׋—`Á•ŠËŠh™NF—áBYvPV£I‚Î¥²S½ãš¦Þì!,³ðre\䂤a­þŒ2›¯¾!X9&~”eœ b ?:Ä·§C¿¨Tjr½Ö#,%fòL+#¹ðï:Ý{uCñ+»øL$ª³‡D´Ym„¢ˆÀšDœy¦|C™“lÕL¬*ƒ§øQHÉì^ÿ²:1›qöˆ7ð5P7Ó “àX:>UMÁ�ðÃú#u[âm‰«Ò·%Þ–¸žãt2wLÔ=ÖgêL|»Ì@&wL¼câ’ïì´qg§wvzg§-Læ÷1b4hlÑóOºNDÇ’Õßoþô/üó�Dßwüé¿ûËïÇáåõõõåõÙ>?¯=Õ À× -f¶T0õ¿ëÄcš»N¼ëÄZ÷ ±y~î]I€÷ü‹ÔC› ϧSòÜ垤„å{E%@ÍôeŸz-C�:´móƒYòcš»ÑÁ³  ç•—ÛÞLŸ„vÝz�,òâÇ„Y¹"éhîÆáµ�Á@œƒLäS7+ß¶Üëä »49óåÅ_¢‘ûwrb+!¢áuy…•wx€Ñ :€¥«ˆ©Ýó«çñË0’‘Ì2=È>á0ÉÙ$GYÂʈF)C »“ìpÑ#——jY{òÐp9ržzûÄ>éJ ›s…s¶vú)j‚Ò#íÂxHAr nBxÀTj… ÅŸtåš–p¦S©Zž½ÜSÙïˤS¯&�ìDÛ÷¡4­62¥>²!XªDôÓ— I>p®÷�ŠŸjR.œlíþ˜Ýë}„&@¹Ú( ÙÕ�3P%T\U„ Vî9ÿÑNÖ,Ç$Õ)oæE`?™ôn Bɹ<³Á8UW·µ;嫸þ½-ûÈ5ls"8�øÙ$Ñr’+‘³Ô´¨¶x‡QŧM>šáùácJTx3Q2-$‹Ê*‡\”0æf_.†ã Óm‰·%jW*Åm‰·%Þ1ÑøÇÄ;&6g¹³Só�;;=Û ßÙé��6o¦Þlé›¶Nlý¢­¾}çýûßýéŸù‘ÿðû¶{û±ÿè—_þÌ_xý½—¢ä6Ù£ËÖZÔ¹ëD¶¸ëÄÖÍw�¨!1÷±ya©«/ (ò±þœ uÐÁõê„z‡ }00È�"ÔŽA âÚuë$rƒŽ$¥ ;„ÖMÂ!&žb£Bº¢bƒ9"U†}O Aʤýr@0êÜ¥ôhÆ8‡Ñ¬àD´�} S©CWâ•ˆÛØ~Xê‘OÔ´§YT5‚àš>˜èŒñá‹” *¥UÆ<R±]¨ÙwL'r ]~„�Ÿ$q5¡Û=-b²žœAA‚¬"=lü‘MÔQÕ} ŠWˆ 0%&¯/¸$÷w¨L]™³=VbØ$Y"6¡;—¬êøý—cT\)=ü§l�®q�^w™d§#\B绽<¶*tÿû–þp?y "3µ*2*Ÿƒuc‰†òzYå,Bˆ#Ùjqj—UäO½Te,`^ôÆñ°u7€ôk .ʃÆùDXñ q®îVY²é¨D:ú0B!èÏ'n´ðšA‚Ÿ«TÅGÖ™d’,oLø#×m‰·%®>¬W1Ô‹ÛoKÔåÔ]W7êP—Z?\—¾î•iŽ�êå…Ü1ñëM/2ÓÜ1QwºñºÊÒ´£ÛÇÌaº³S&¹³Ó:–ªHÓ²;;­JÜÙi+œØìÔ‚¨_ö­ >¼ÿ­_ü+?ò?ûïUu¾?þkçÅÁçíõt®õÙ÷×óläê›}GÂJ¤N鮯|Z2Ý–x׉ƒpVj*ÿ¨–ø Glž_½hesŽAg|Óæxõþ™CšI,(H8ø ¯m]Öeø$$pïë5\�R´žëCôÏk&Þ”ÊHz©F-`V(c½µ¿¿ˆ|®ÐJ=ôÍ–X¶«¬s2kŽDøÓ‡¯3£ø�«iµc®]ÒèôZšÎÿ©Ñá™ äXû‚%ÒKÜžrg²Y{X°gœØ'et¤:]j Óœ{ÜSï m™#¦�ÐR# ¤pÓû ’ŽØ{9C2v%��Èü¼–]NVÒÓh�Ú&6èÖ¹œõÔÙÍ,ãÎ�íéì†#«µä*®/”"—éý0luñò!Ÿ<, xúðü![zäá§ŸCÅ›°eÞÊH�¢Ó†i 3ir£C0Q} ÈZ¤tÊù¢Ñ^¡/¬=Ê›'¢¡ ,Âø|°Cœšòáç’3']B—Ús »Ú!I(i½>WŒºGßwï¾3�NùT$¬0™âcyxPx5j…ê²ýêX(ÓT#ooæÍg.9ŵ‡ý•j®+£Í„£C‹Zû’ê£"HžO5=Ô6³ÌvÍà ýÏ_ ÎRîT4U‘�x½½³5�’Ç[VR�²+‰ü·âè­VUßnK¼-±M Õ�ÛoKT ô­wL¼câïìÔC‡`Âpg§wvzg§¦ãߨu¢…HËf{žÿñc¿ö+o`·—OŸ>¿¼¼ÿôñÝÓvű^³„¾ëÄ»N¼ëÄЫï·o5b“×›D°-·‹u;å3€è !8gö8 HhÑ`à Œ% aüš´ Ùò 7¥k%)dˆH4L!€°« km±è Ü驎2È8�-7Q­G“$)…HXV6èˆc8æ{èpàÀ�AV‰‘ªa€a=âÀ0ƒ¶d%>‹‹Â¤> Æá4•M’ ©$™Òiñ嘔Ï›8܆Veï’OäI”ç¯#£\òÁ5‹�Jf+*péd)éhu1êÓÿ±DþÙø«TãUÌÊe”|mZËÄ;ôWêY·¼ ›q‡J�œF–;Ò‡!ЕeÂI–E¤Ÿ±%ŸXJI“ýkëa"íÉVì¸÷…f7¬G{Ækã´•8Fÿ%(‹ðxû‚×å—.EÅÎG’#ÿjÁ�b+ Î\wÉ=ÊL�ˆ1‚‹Æ_#�:‰;—ù\�½V6ʺòA73�Hýß–XYÜ–X½Ñ$nK¼-ñމu—þë?õ�õ¬uŸu›wL<û<掉õœwvz¦SU‹;;]�Ñ,­ÞãÎNïìôŸö:Q-SÝÞ}øíïýüŸøÞÏþȯþ-Ö×ÇOý'ÿîúgÿ"ø�;@~Íù;Zõíö®³‡™_“¿»N<6¸ëĦ7[7+›çA[å©í€ˆæo@¶³o™9Œ¹Q䯕wˆCúà£,¡m¼ÿªfRo3ï ]Ç˳Øç�‘=Âü£ÜäŒ5H+Ä#¤ÂÌù�uUìúJ¨Y Ρ۬ÃÝÆº€fýû=Áa³}I¢˜[Sƒê3o6pÙ\°8 7ÌЀç�€1¼¾^ƒz<‚JÇ÷ìù&÷åý”îÍ£Ò¼zÃfm|NæîF^¾b© ×1nk½º3‘Ö}®·MvÉ� ó�üÅ;@IDATìE¸kû´4ü\ÚÞÒ<çáDSZ¢±‘ýr«ó2Ѭ~ØÄ9¾_¥ºÞ!š}¬H §DÆV y%s.µ¿’ÈY©ºÜNRæãeϱò½U2 r" U=Kº`5�Ñy™´nmÁ§$Å=ÜÙ¤A¨bÉýÏDz÷©j¢FpT„‹�<)¸§ñ¤”–p¬ÛÛ€Ä34O7é± ÀM€ÄÉŽï_½KöT„$òòÇj Ô/§AIåqJ �r Ü*ö¹Eå,U¦eé÷W™æCþ‚’p,R¸~jÛ¶íÀ·LBT-ež�¹“q鄲;rUVÛ×ñÕûŒúªB6­„z¶Ê:Þ™£ê¹ú³*M±™©:eGVw$“ü·%f�Û«I·%Ö0nK¬óÒñÝ1ñމÚÃïìôÎNïìôÎN¿9c¢5c @‹“æõÍZþàñ“ÿÛóïþéŸ}}¼Y†¶\xúðñ±€n‰r׉w�x׉CYÌ𾽈M ÐP™Á`„n‡Eœw��CˆC‘Bwªg w8á¡Ké"RÒPŠ@˜Ï=ŠZ´ŒÃ†Ö-¯ôŒã°¶ezpr‘ï a(fP’B8Êñ]Šø,p<—'/¹±)AnÉ« äa*[äÅz*Ñ*æ`EeÄääÆ$áê@†nNP\¨‚Ì °Ì¤ ëÁ¶ÃXDÃ]]ê¤�šÀÙ¡Ñ£äI—„¥ÂEª îD¸q7y£%+ÚÇÀg›¸ì숧ƒ’e ÐÑëã=gFA‘ǬïxWÑ“IŒó ÃZF=| JSÈ\'iÊ6!²ˆÄEެ?øLx– –y¤ä)%B’jUÙØè:-kﵨ}‘2“£]¾ñ|EÄß9–ãлäÏ@SæøÒ%ù�Ëâí(3¶ÉžN3ÔèUÜ¡`’G÷ðˆq¢‰¬(ò˜¬`æR!ÓL”ÊVD‰'xE12BZ✡ÛcŽòñ!‚ å�ÑI9’·%2w&® nK¬že ¸Z«.9 pZ�uêÛm‰·%VSÖýÜ1ñމ�+�twL¬ÿlè~ŒÜwvº™‡A¶¡ãÎNL�²wvzg§_fSÿxëDk�-+>üæ/þä_ý¾íÞŒÊ^púúòòüööúÔ®NKܺì¶Ä»N´8¼ëDÕà[ŽØ<·¸µgÖþ/P…z�ž× ´žÛë$sEj±×Àôîé#8g>G—Tðsù�§˜HU‡ËÎó„¶æ‡}Õ«˜û}€Ä½ˆÜæW¼ÑžÑ~áGÅn˜FˆÑ~¿h’¤+r+3QL˜UU-ýâo5h#ÁýWPAN¸ôcx�M°6ÇBèÀ,í°–w“ ׿�v�µ>r”³¹™ ˜îm©�?žKsáD2æ&=7->qdŽíü½¶ÞÒ1Ã.¥ÄIç�!<¾ ŸòcÇ|ÛĤ&¡çNŠÞºDÞy{€€üÃ&Ul†¥}Tnïß?·oO°¾ú�e:#ê¢fÓ«Â$'†“îéRÉòwÎ>(?âF¨Ä…$puÿcÞg~‚™šEú�Y¤tr?yOiéb·ö¼WÞÉ¿ÚBK\¼ £i ùJ˜/%Bäy¦S†h´ØV&~bW:§4NyÒ/³ x„j¥¦€OþlYBõÞ"Ÿ@¾YG8dð9ÎMªÐ‘£k¥ùTLð|ÌBÂ8Q§t‘ êO6ÏGÔHd¹x ÜkØJnIõ¨3SQÒåÿ÷f›¸Û‡*ÐïW}eqJd Ê»-‘½Ø5£UØ•ßm‰·%ª ·%Ö¡ÔÛÜ1ñŽ‰Æœ;&ÞÙió¨;;5:ÜÙé��6Qü¦¬ Ô{âç7ÿüÏÿÉæ²¿ØîíOÿú/ýƒ?÷ó½náõõù£Õ€� ŽÎæ¾w�x׉w�Ød÷ÛŒØÞ8½}õúæ�sÒ-¶(¨Újá®ë­›ðÎ&À:¢»N¼ëDÕâÝé[¡7hL[c[}²óBl¼f!~:&í/^F ¨<”K\ ÖÖ€ÍYÜÖüÒö±mËøì„;hü˜z¶ÏôŸS�ÚQë݇O€�^Û"?ô n"O~V=sç÷ü�öjÌ^ô’–¿6÷þ©a­&<”÷îýï‘ãëDsº‚s°„ÏW^PÒ7w:'i�ƒLñÌÊÞ½À’jOr¿ðý+ß(Á|îhð9®ckÉÆvÄÉi…Ö˜'†¶‰“~RC´B‹Jï` ôéN–\ºR¢²n�Xì™ä=ÖÒ½·°º„²¿¢#Ä:qæùü–K[ñiÔæe«îΓ³Äð— †qX¥]é–€sóò®Ïçy]%‹äcb柿•N/Óȳ˃'ÿ¯0§$|) ÁÚÍÛ;I�oFlÒ«g•s“}ÃYÏ´†$0 ’/¼ÍÉÙ±íÓö†P.‡åýP‘'$YIŒÎK.Ûˆö÷íýüÒk§Z”�àéË‚áw<Úä ­Â‰]5ª4Ýõ•ÞhÈß™n¹ÿ}~KkfÄMÀ›—:­éCTäèÍ¿•j=¹+dIÔy©j.¾Ÿ³›ThãCöÛYçQãoKT}nK¼-QG¥[¹câï˜xg§wvzÇÄ;&~ãÇDËÓSÿ×Þqºu@ €¯�?ù¿ûë¿þSÿíÞ´`ÎoÅÐ2â,Ìø¶±†épgñp׉lØ6ðw�¨:Üuâ·±yÅðÐøü¶ö8 -bÈA˜B÷C+ ðŒž Lâ ª¨QêÁogÕ%"Ñ%0F`GÀ„#àk(MÑ%|žL¬;BDöêÛ²>……tƒ³¸¹Üs˜€E¢00%*ý/¤ d侨EÀSüÉ¥Á°î/KàÓR¡}�NhN‡t @.:4¥N\àQ¬ûïêPûBå/@ìèy´`ªA6:àÔ„‚•®ë}ͦè·Üû :,° Ф£ðÄ´#þÊ¢´Œ'$¥°ª¬’ÈqN%ÞM)�Dw,Óçé·°ø¸XÌ2ÍT€Œ3 Tì)è�¹RŽQ5kd†hDŽ=í{lUI¤™ ™Ã2.{<�}/ºÐ®YGv—G¸"WH•]hðªQª¤P¸3†…ë•eÿ¬ö¥#>H‘S‘Ða_cä ‘dÑŒŠO |R{Ä(Ÿ´IJß)…£ZÄ«»r”W‚Ƚo» Ýnø‹:Ó¨,…1£f®îoK¼-ñ¶ÄÛï˜xÇÄ wL¼câ��nž¿ùSsÌ;;=“Ç;;m}¦÷ÿÌÖ‰ØYyýæ_þ«?ñ?ùK±ÿrxæ´ÝÞø¾åSÐïùÒ™àçr°©ý]'Þub•å®ëÈ[iû›ç§7;OqÞE….ÌÛ TÀ÷�KRþm[ò Y§qÝ{#‡¶€Ú„¾J¢áL”sÑB†#…gÁ$K¨I�áaj^9øjßy ½àß`Ùñ&Ô›??̨{j//IÚiî° 7&ÔLr¹¦�8ø#9åÊ7œ¤±ŠQw†Bp�¿y±…Û}«ø›“”÷Ÿr3³]ùmD×¶gø&QàÈ�VÌ…&eˆ1'&æâGÂ3Ú+/¿ÏðBJ]M3Ão² c„ ±í€“y`bÓ È”ŸXÌ>ô‚ÖÄ ~‡�&2#È”(Î %õ ²³'S]I€­Fâ}¯ÍõêÍÛHÑä6¦„D¥•ºy¶%‹ëÌK2xÞ†‰ÞIënøßk6<‡=!îÕˆ²ó›l¸ä#šr¯žQ+&\år~Sˆ}ŠñäŠ�½í¿ö¨„Ë+ÜËVQŒXî~�û S†^ì�ë.!uWJSrhƒ¾y³©G¶R¦Íý[”_ª:U+²,“«ªXÒâ²\eAIj`ȬáriLQèò>¬Ážè*Gγ°1×�ä1oÈ&cøÑ‘Á.Ä`DÀÛoKT­nK\³¹-QxÇÄuÀ:oƒHãH?â©.ë¯�%wL¼câ™`°ƒ:qg§wvº©Ü��ÞÙió÷z…–K-.ÿ©®·$hiòáýÓï}ïg¿û««yÌ—ãÇÿ£÷åÏüŽçÔ¦KV½É³…Œí®›÷žn¼µÕJì®ï:±uöé-Í×–ÿø"6ÏO6iãñØñö2î@Õu•¿�ãŒ\{jÝìžÄ ­ÍÀá�d¿Á˜ [ )e8ͼ™¢# „ŠSË«0(Þv¼Ð¡ÓgÈTWÍ¡#æ4“sU]SÌuS8ôê˜wOÞ¦*fŽHÎÒ¯ò–Œq’‰ç[|é54M¢÷Oø†ÚÜ +ey�Pž®ÏöòŠ>øns¯'ï$ýðîSNUL¥7=n�&þ« HŠüŽe«í�±=ŸÊ H»)+šh‰F#×6ò%i¹Qc1ïæ¤Êûw‰�{±v‹úsù2p„£%‰ §§vlãé—Ú9ç­¢�eÉVÀGžVÀ 4§‚G<�íªGþȺ�£ïÔŠfÈ\æWzé"¢ÝÕ2œÔ9˥܈Çi~„í˜[�®4©>Hé߳Ƃ1â�9"8»Ñ_i$͘e³\¨Æñ2CŠ%1Ÿß8DÊ@LÑþ<½õé¥÷ž’JüQÅd‹o²w™üj«*ª_yµE$Ùz{ìl¶A‰?î1úð)>ÍïŸY_eÇ”Ž‘p¤P¥£ò"°]ñ6¶FèSaØóý+úƒ�93$GBÉv["û±Œ únK\;¹-±ŸNj–5 Žú�Ûë¡î˜È ZÕ¸cârä Ñwvzg§wvêy“;;5]øÁ¬[çØMéé·þÅ¿ú_ú׿ÿ™ÓÿøW^?õÈ©u—Ÿ$2³±¸ëÄ»Nl‘Ý8v׉-‰¿­ˆ�ç+f3¿„í2ä#dÞs ¡Ò„ÂdŠÅKÆQp¹Îr¨.®4á!E„�…¤,:ŒKDFqr†Ø”!î"TFkð“ Ë÷àÑüúð K˜‘H†®„ä”2‘Âq¢Ð�¦æcàâSªCˆÖ‹�wÉÐ*·€™",4$$%I–܃ד'\àQÈ×b’QŠ“&ôGºÂ¢GF¥¦`H�ð°–.ƒ;Dz§Â”]˜KÈUÆ*k ¦+±› múÀ�<¯HÐf›éuªôH\€ÔGO ˆïã ¿ýcÂäöÄŠú:.Ý&¿‘‰yJ•~÷Yò\Å��²dŽ F(RÔ!–_ˆä;ÿ%_é§ÒÁFC Ùù°‰%¦¥ŠÕ-Í_4çS6a–æÄ$ !ÍΫmÕ£!Ói5²´‰òùD,ÑÔõ¸¨\+¢SåÙH7‘z ˆB�"ò(¦°ÒJ9þ¥ E,Ó/ïâ’C:Xç2ƒ¨¾å½-‘­nKVF½ ïû޶{{y}{ñÿÊBÕÕÖZíT¯ë©ï:±q¨®zí¾Îi�Ô]'nHk»ëDuâ�í:ñé_þ—ÿÇ Ñ¼ëµ=§>(¦©×óÞ]ÙÆ`Õúºñ�kªÿšÂ3lÊ^Þe:73Ó§:¸áUfìÁ_ 4Rw³ž¤Q1²{Ïh³ÃÆQÍ(o¥z™óY3“ÐTRŠBå9_y$u¹ooI P{ó‚Öƒ„ÙóLs£00Š:ÇE‹G JPMÌžøI�ç&hÛÕî¤V™·š¬š$×î“á×ÞŽúij̧²FVx:”†ý„I_p$Î}³\Ó.JfAQ’Hh~iÏ/S‰èÈ=vê2÷-ú|,aÎ~-`Ê›uvvÍ8ËN ò2J UÒv°ƒ7Í«÷É~°“]æÍ§MN¦ø@…¬û%."Ã4ÝŠØû�M8mÙw> ­DX‹ûÖ’ö[ZÛ�Ä“ŒÒŽvd¯˜êQ¢$¢ §0îzØÑ#u°"BÞ€©98ö{ƒ-a¨g‹�ÀA{C=«îùÖ1}7]—B íIR5•VyöÅÁöGMÞš'—H@S'œÓ¼e4~MõðÿnR´ãRnkôÛTÚ¥¤Ú›°D$ «¤\lwTV¸<é/Ù|"[›¥•ó�bL$æÞzožÖ¾uȰ9M>e©-(Ô­ž%`ýD°å«B{³+oãt’ÿ#f§æ9JѳÍDÍ(ž7££'¥åªŠ¢¸ ʘ�²¶)Ë|8mË`2õÛ˜BªR.{©ÙŽÿïÙ©T�ÝZEó�¯g§½µ0#7Sçj ߜ٩‘¾›ÿQªVËx¼Ì9[TÒªüM_6Oùg<;­F¿¾ãÄrÕƒ,è¬à ÔTVc¯¦Úº¶l S˜¢º³Ó;;­I~Ý�ëDùwÿ‹ÿåý÷ÿ�çÿûß«ûør¼ÿ©ÿæöÏýW>~|úðd›·§ZÙ£�¾ëÄ÷Jw�x׉õößBÄf?€>:ƒ¦ «óf†9Ó¾9f%b#_ƒœQO"c›é’©Æ˜WÌË¿ &KÍQ´k=ÍàzÈÐæÀ4 ¡M Š=³!w¥mÊîÜL眣ö˜1 8ŸµIÓ‚j rþØ{Û’ª^ß§ó�“g``aQ2J~JTD¢ˆ$Ñ' Š 0OD €%ÉC"øžd‰AD‰ d˜&Ï�O‡ÿ÷ýjŸ³O÷í¾aòøßÕ§Ï©]aÕªU«V­Z»‚ƒº‘=_4"‹â'xJ*Å ¡qT!ATÈì¤!úfѧ¬»šHTµU³ˆ`À«¥‰Ô�p"Íd) *à“´ÖZDB‘$Œ`á/õ^>î¬T>‚Ä=K¶àIk¸äËz©ß©O“DË ¿É üRkÚÀVôA` ÂWp°€! výˆ·¿%”¬ Ô#ó…T£LYÄ>D-³ óa¨’ÎÖ±àQ¨JD“8'IJš—jÓŒÒ[²Ê>†ñ1"œµj[„¢À‡DÞ[ ïI*‚?õÌF ¦U€µaÌ@mÚ§p è…ªD2 LP¦64¸äG|D�\–ë.mêÀ‚5O挎NB<–ê�g^rÀ7uÙB˜Âm{b!† ‚H[¹í‰ðMÛí›áˆòÓöÄô’ô$ŒrøšÇ-I†c¢›¡aœùjCaLŒt‹˜UðŬCÃ%¡Æ7ñä½vÆD-7˜d´j0Œ4ö­Q®–˪â`Sp4-•g_ (¾ ÇvÅÈŒïè=1`$Ä 9#,"_#µRˆ%"i8„~ìcbÔ±5ìnbIfÀ8ø¤!@ kÕ‡,cƒðB&éîH«Qu?ÄÈò;Ú,çiøkÕ=‹‚¹Ÿ£/ç@ =€ì¼ÃNXlVeˆ•HW¾'BvØÔòVV<\JS&M¹# —„HbEqµ¼ï"2¸†\x©�äÝ �bK+BWG^^^ºß��˜ZA.êïàLƒ€ET�4*&&¨©ÑŒE}à¢7ëÑ!®DP;Ac;µ Çü¦$†�ih žÄB2ÿB?ÙGã�mÅ—)X×¥v š¶‚T�:lmÊ+SP. ¿8Kñ°L*F‘ |®aí Ê"ò€�Ö–tòªK4P“u°€BÐÕå4 ĵuÉ(¶^}E“á�°jµShÊØ$P«�¦Ï^Ó=‘þ†R(¯Õ½^â×îä½åܛߩ»¼<ÊMy$«Çoù¶×Xé¥~Ù~ÅÙ9Úybh� ‡, }µX8–†l±#aÛybk±�7®_›Þ1¨¹&GýBÚšz c* /Á`e|*.:�eÈøY=6ö6ß-’I®çËÑOåFy!4ÇÀè0 ŽˆŒÍü©ÚÚkÔQ›Ì¡^”A_IãÛW— áå±(®AÇì¢<ÇÙ5Ž8ËÑ?@Áôãû:ä¡U [”$Ê2 C¡ˆóòtS–bŽ—¤žø%ß³Q‡¬Z!´Rë“·§�ƒdTÅŽ®n•Ñ�ܦ \t¶.®SHrià?ÇÊ„Ò5ª¸°šFÒ¿P™4„Ð`bú¬ q)ƒl!'áj!bÍeHš  ÖDx Ê,#c]õ±Å‚‹ ,q®\3!0b@[)®ØsÄÇ� 4̶•YÃ…€¾1è fFЉÔzƒ|*d«’Î2亽XÂ%T;�†çk¼þ' ÚŸJ+}ˆV"ëŠKï@L¥’+Ú\yh°Ê£|# µ2á*�T  ’"Ä�”,W L«�é ¨’“*ꃗ-Á0ÒfNâ=µ™LÚî¶Œõ 3iT@“+M˜Óë Oê$>àÁlB4¬™Ìfä¸$¸SHÖ°j“”t;jãVÛžØöĶ'¶=ñz6&*6•ÞŽQÃÃãÃü¯á•=òtE·Ú]æøÜjw…AXŒ-‚Ø,URô]Ëc"Ò�;5ÞuþùŒh_^Y™Ÿ_Û‰ÉI¢ƒâ°º,.wçæ»œ+ÈéMï ¥ _.�²²Õ˜HytH291Þ�8eèf”©–ºËss ‹‹·Š ¾á t† ãhJÀÇD•™áщNg|„•ŽfcÐq,d{£´µÀ�[¦ ó19>A;YBÉá¸fF¥â„@&¡h�„&ÝÅ•¥¥%ü.�wlÍx—w}4'ªÁ•)dlÛÐÈèhg´3¬­MG©l¼‚ÞK++;Æ;£Mž! sq©;k;-Âr„óW\àÀôS2°ð~r”†îx‹=$)ºËkósKÝŵU*†XGhG^Fp¸b²3:ÞéŒÔ|ê0Î>°…EhÐ]9šÅEùÑ?i¶)åpîIöqZ%˜§N’£ÑÜââÜâýaÌ�°‘ZçÿRöu«�jiëŒu`¤ÒjžÔua~q~~Ñã§\š¨žÍX¤{Bwt kX;Å^ŠM ô&&FG:4º…Ú”uN>PÍ ë¬®ÑFó0zwqh”õ�ô!v¤J`+O¨¶Úi«�"Nà¡kž8ÌžXp¸:ðÈgŸxκãÞ`aÆK:2Pyå ®æ¯�'1{ô1QIÛÎ#›[‹Í ËbãÜ_½‚n¯Î€Þ{Š�ï–´phE N†C ^µ5Ó'‡b‡hCy‹‰ŽXbü ‹*Å·À Ec18@b¶‰Ì1G†KõE‘c,¥EûÓ�¡fKiħD2¼jV´`·€AHdÑJ)”ä| ¡þDRU2rÔ‰}1�3�š®É¤†â<ÙQÀ¥¬— ¸ÈP|5¶ÁÜBÅÝQ¯ÿ Ì#8 Ò„“jR­ “8“ëÄ�`ÔlÒ£R�-YŠ�B/KB½E¢‘êÎD(ìa6;¨ ÑôÅËcAøLœ$RZüzL.ˆ5ö.¨ÑÀ°>¶@ê¦%,–=M4ÄA23¼"VHm]J˧à—ÀÑ Qëê•rLŸv$à—‚¥H 7K4œ¡RÊ6,ô0•qö~ªF꥾æ”Osá t믧 ÁT�t Ô‘8é@\°Ž%J—�ZîrÛ%±4’¾6QÛeÓÂ5mOl{"œp]Œ‰H1&Ĉӱ±Ñ鉱±N'uðk´ÿ€ÙbÿÁCs‹ó+Ël Ī•á ÄéÙˆ9E�°ˆo"íì×̘ȇ ž™œë£W<¬ÁA³É u’` Œ•G¼äý’ãšo„y§„:�êÈÙd&?@iåA‚€­–%Sü,£À UT˜HãU£Ú±2/Ffd6À´z”*šCᢩ¢’&£j^Ç¡WÈ+¾>V—Ñh¦ëù\)LŒÌä¢4õ[|d$ñZB( =,» !긱Ú˺ŠX‚>Šiª†ÇRÊ곑˜®%J)øy‚ 4ÍQ(Ȱéh+”­X¼GÊÄ©öI§R! °¶LÖV8ô‹Ý".¦bŸ@ÑÈE‡×ybÎ.G.e x^—[Iðtý$Sò‘Ð žWêê!ª¨hú6'X��Ö6Ô@í­RÌw°¾FŸôý¦…¹’Z�’€4y¿ ­]­xË·¢¼k"x¦}1Ù ¯‘Ò®{sEa�iŠhÁ›egîèòúyµêpø Tˆ ¸d«àCm(™õk%!äXS?ŒèÜÀåø³Ðf{�G´"R�’ÄSö0†¸D¾�Ð8è/ó` ‰/ëESyiÈ´Ð`WÞ!Cm1·*z†{ 3­W¯´%¼ØHº9�sTÛÙ÷SØfh{"Dà¯í‰mOT2 ×ö˜ÈæEÃÑ¡áí3ã¬åŠ<Ò‹½öìÚ1¹8¶ïàþ…ƒ‹CÃŽåÄlä0‰ˆ½¶ÆDóȶ‘ñ¦žÃL A‰¸Ý­T Ю3,¶š?8_ � £°”é|ÄžÈH£!fbüp»[ìî�3K ‹ss³CkãkÇR‡ z‡“­ÆDÇ­¡á‰±ñcµ»õ*XÊ=Þo(ƒ †ÏÜ<¶§ý|�ŽŽ© °BLÕ }ðøÆÄHòµÎøØ&v·ªÚ÷6s###»¶ïXXìb Cãtñ¢Ã8êNÑCɵmËÖ·»xTgÇ®–sí?p+_Õá0]†_�d¥Ž‡ÛÝúXLMŽœŸ_îÎóJy|[ÇÑ™º«ZÁ\êPê¦v·dfbby©{E÷àÒÚrn¬G_sUˆ"DƒkM;õuúP56Þ¡âýjzÀgûô$ÖÆù…Eê˜Ug(1êÌÁʰç5«�ª¨ U“Ó�c´»Ìá�í3ÛgY²7;WuXˆ …Ñ¿7­vÚj§Qô¯³y¢«›´™#� -ÜúŽã_ýì`w£W±R¼ê.­-Žt‡:H¥áÑÈù—I9µf�í<J´óÄvž¨}à†o±a}ÌôkÆ)�š”¨> È „@„À¡‘q �K~Ä¢ƒ¨C +@£’7¶$`LbÆ’@ó—ó…I!ò�W]„Ÿü;b`cQ¹÷•(ídÂC�¨|7ƒÍ§2†Q)S“‘p¢NU,Õê¢L‹?ekGÁ%ž_€*®Å+øÅ”b™Ð8P´Ìá±’MDTn:Ÿä4I“HuK5Þ' â!©bZ ÖZ�ž ®öˆ2Ï·(Ìý¡©DÆl‚õ[µ(¥ñÄ8ø$ÒÌ8¿Ca©Kò,¥},øôRù‹£c«2’ºQ()ºY ]AJ4„—ØŒÇRõ EÜÔ¡IZ1•°D]Jg6Ñqþd�B}›Ñh±6±€K@Ý«‚sþ¿Â#{œMÄl„ô#R<�ÖŠ’×êlŒ Öp‰¨‘�¢Aж'†ÈÐ¥í‰mO´_ÙEÚžx­�‰yoQaLÚµ}¬·7-]³|­,W—œ_-.T§žY�M DT؉F†Gö­î››u�%2ÙëpâHÄϵ1&R ûk¬@õÐþÚ�gfgƒmw©š;X?ŽvªÉüXåøÌV«Ý¥Åjh¼-‹�Ï[މŒ{l”c_ j3°Ø%ùðbsiiadhœ—cŽ ¼mÈŠ*‡�ÍÆD‚ØXCæåÖÁ}µŸmª@‰Åm¨ ‰Ér‡1‰æëŒoH2鮿‘‹÷^Î2¨Ñ±‰N¯pÏ'ã^Æ*°c�×Fûm¸¢.‹¢gv5å.-Tó³õ#(�»poKÌðHwe¡Z\â𠶆f×fdBoèlÌ�˜)î­!LL•Ja®ã˜�mÙ»š&Ü6؆=æªÅù�{ðCó© V®\\ZP�Ñ«Âá BÏôg÷*]£Î5È`=.êµÚêâÒâÚ¶ vÄÔZTh—Ñ^ùv­i§´–­çÍZIÌÕµîÊ"†É¼<×R©ÁSæšÒNÕQÑ=G‡i—½Af hûnéç�²¸‡°,‘®´°°€¹m¹ã™$êgêzê\üèSÏ#[«�¶Úéµ7OŒâ¯B�Ä\ºÍ�7˜ÞNþû¿øÎÍîÈžS®a)Šoê)Qº‚ªfbħ¬ì|Ï©÷5Ûé$ôžvžØÎeÜÖbC×»z"\D'SŸ¤·i3€Ø£èIŒÀ˜á–}k ;Êß÷¡?dVhrdóî!VŸ9äa©"W±»©³¬uYý£¹¡œ±&$œã¨Ë¡€�) £™Ö!›]˜dÍB$EBáS *X<<šÄœÙ©iÃbø÷\6µdψc©CñkÁ°4¹8ŒØ!Žãm)å±óŸ— .ÆJNÊÓ˜ã1Ì>g˜Fnrä…Æ"ƒÙyáFÔk�+P‚ÐÆ®åŠçnä#)Ø^Qqn é|]U(GÂ券èoâKEæ¥kÊè®õ·ýõn`I,G€_y× aÁ%y%óñmªU¡†Â™æ„N¤ ç€�•Ò’R nÓñ6ßLÚ_ñ„ xØË‰*¬¨Ë™qfbõk“µŒYdG~‹&%€«Â[ 1K«ˆ9rÅѸ ÌäZ5ß©s­-,Í Û¸šo˜«·ñAPþ‚!-I.rKD‹0¾ üf’!$/¥„,‹Y8Ó"™‹„[€FÍÁIÅÊoø:à�ïj :|a @!g¿`Æ”ÇL±Ð0‹ì¸6*½ÅjÃÛ¥lê –ªBzE É]8Qi{"Ôm{bÛéÈF˜¶'^c¢£òj…ÉbçÌz»ÛW>SýÕ+«o|¡úÎ(²Š;ã¬ê¬[W÷}dõã�Q@Çzµk÷®åöXÎ2*ŽUV¤0.²À…pÌkjLDÜ:0ð’)˜ˆàùߪ~Ë‚iuë;ToX}ðÙUÏ~PõàÇW/zSñ3¬®pâ›Ëǵý8bxRÈo2&²f .e@kJÜ,CÒ[Œ°Ä°’°ÃÈBi¬*ÚrLTñaHC”QЇ‹¾[ýÔYI¿_ùžên¬~ï«÷¼¥<ûÎÕ>Ý$xÖý«/¦y<‚ïn÷¯nÏêö÷¨n}§’ ;È ;v--¯œ�ëL²=—õï(oÃcŽaLd!ºC3ƒœ5�;´¯ºß µ÷IÕ.®ýü|àmÕKŸ\?>áÕÓ~ ?Å0®s�*%‡¥� kÖePåÂZÖq©Æ¨šÖ¹.û^õ Õþ;Ü»ú“�?(sv'÷�¢ÒaBTƒv�òo_W½ò9µŸŸ·~¾ºÅíùew5šð�¹Fq-‹Þlë¡Ñp‰JAR ¼â™5„©™ê#{Ñy÷Õnwyq”a~”GtNUxéÚÕNy¾¤^l«Õn‹†°�®.ctîŠ0KûÈ1¬¢K¶kL;-ú¢[Fû|þ‘¿©Îù¹®•$Åbþˆ[UW\R>é…ÕS~?øyIGwq‘~H�qVÔM4°V;mµÓëtžÈ¤öäàêÅÛÜ¥zû6ü\USßú‰c¨U˜eð6ßió_æ|9{qB¡ÔÎÛy¢’VFkjç‰Ì´†Üpç‰ tÅ:`EìÞü¡ÏhYÊ� �ÐQ1PFl´ ÍDVëþöõå×o Å ;صʵ#c«#£¾ÿEÒò½#Š ¼d®¨€ÔNedë'‡û¯b+ogúUˆj∖5I‘�Ó„žpQÆGiÕCØ"7wÐ…³ÞºKKË£î_dœj!¤ª“SIÔÎÁ‹@Pâ›ÁiSíT*ƒ˜Ã2éì`‹”1‘ûRààF “]I§UU#FµÚ‘~�Ùj§­vêœË$×Ñ`ÈkmžOªço«ÞòîŽ{%Ž{»ˆãÞ¸ÿ¯â„„ÒÝ1WiLì¦=±ô/{¢Ó;m;O„Ví<n€ž*eJõÖb!T鮚vz„žèJr¬kËâ@Z޲P.!˜(» ðêÌ´FÕ£èxäÇ6 ¬ìáo ÕÛÍf‡.º âF; Éë:1Ö»¤@ë¤×;øRviqÊÁ;ªÒ¦åKL�Û6‚L�š;}¤àUƒšð“Æ00¯ØB ïöØ› „QÕ1Ê£ÆÀŠ…yrLöBM„PÐáŽÑøÀ–Ðe‘ç2RùÐÚ«¹8�x4…,Í£By£= 5ÓðfV¢ðQŸ8ðC×£^«¼3À"°½ô&E™Hrðmr�àu�h�çz©ÜíD 7»µÓIh>’¡ÛàÔ8g \J½R ÔÄLj‚Jx1¯8g†Jªµ’Ùf Æ”ªmŠ1ÀX³úB†SÒ &ohÔ,“ŒvMý¢2!k©©ÉIB•ºX/  ôfrÃFÊ‚V‚€úDIúXFK+��¢QÔ€…Ö7Ä«P�¸(ÓF€­„£Ö¦ÕçÌÈx%ŒàTÌ«½W—<)Û­©%/¾Ñ!yo“,›<‚d§&095OΉW`�%ÍB)òí’$ƒú–èá‡`�v”Ì4‹˜ˆ#)ÝgŒG>1„ÒÒ3ˆ².Q°ò¤Ûžh�n{"\ÔöĶ'"Ÿ®³1i…„ãü,ì .†Œ'Ü©YrRÿþרu›êÍŸ­~àDrÖN�šŸ_Z\À�—Eo¬@B�^¥1;ŠÃ,ã"ܱ„uÓˆë¢'p)޽‰‡cwì! ÈV F®lí¬Žp+¶±,}¬áRM±d‡ÆŠe¨BÈ÷F§­‹,pÕX{ÏA°ýaxÔ(þװ;Ìr¨ãa,[üúc>üŽêkŸ¯Þü9wùU76LÏNp×(ûd‡Y%Ïš"F4GtOSë`£ug=YFa_ÞÒ*ž�PÚ8ÇNâJ»•åUnT ÅvÆø�¦ÉUå%†”rŽÜöÄ9€3f�T]vT.-×ÇžtFõcÖp\Ü{Þì‚»œ–KÝé s]š—òX¢Ÿw©kCc,íu�o~©úÆ—üÄâçõç�÷î=ÀÖÖ®þå.WÖÉÁ%CœE¹P›3/Ð¥ÁUý\§ÈÍQÐÔà+”„¨J©j—Àµj”ƒ}”ÿ‹Âf› Üz¶/ º*´œÔæ]èƒÂE§Hê×M|´w»..¯vºÕ00Pó9yR«~Ú�Q:¡9ŠŹ CýÐ "j§HûÝETà 5A~3UP+2%\B Ñš¨º-¹º6Zë«›`uä [•þ‡öL ÕýTj³ËFÐЧÕN[íÔ¹¤ŒçŒáZ›':ïc¥´s§¡Å[ßyì«ë Ÿüþ·|÷ûîÔõžÎ[†Mé«t%¾Úy"mÕΕìð,B­�':!Oÿ½áϵi0,eôwD¿³•˘ïXÍø¨°b˜åÛÇòP %ÚxCJ$&Ú�^5R>„;°ª ddUÕp¼M1xU)õ8ôªk&·’'ºTŒ"a<ß±šYyß Æ{�çÃð.h�jÝPŽ|Åg:Z‰4£ñ�žbfæ¿ä§þàŸDð•ìM�@Á7Úˆ05¤jà|T•T‘´!9³°žÁÜø+DXB,ˆÆbƒ‡X²Sך"ètÑ»á41¶83H"ËLÁb×U! !€‰HbÍ S�°õ2M@nY�I­|@È "¥V$©ñÛTèS¶“9ðB)Îõ_–AÉ*jte3 Á%*Ù-Ý\½Ô‰N�öM€k£hÂÇ6B�Mübߪ±3&GiMÂág)+â"AQL@šNhd"˜Ô$´=¤B2ɘ¤¯K€¬QŠ1�d—5$;TS÷Àä2 —"-–*å;-(VV*@Õí-®TÖR ëÊ`± œ<´=Qb´=1¬ÑöĶ'*!ò¹jc"¹ÙW81Ö³^!›^úÄuv·›|õ°§W7¹UÅý�ç}£z÷›Öíj|ᣪ·}©â°-Îh𙿰°Ëöïgç&[ôزæÜéu¥ÆD^5!Ú�³“�æfe��a"Óâ°¼Ü�D5ºh8çU;ÆlŒA”…ù͈�á_Übê‹=LA”Hˆc""£¿ý°äð_‰Ñ Y®hy C!ÉT¬öuŒš2peÒ¦‡;þZíÔ›6ÙÀ¹œ¨Å&>Žít¬a|ÑcªW€0†µSSûzëigÛÈ2+ß4À¨ÀjŽvŽÁŽZØ|HTêÝÉÖ2„ÁÐß+ëÈ @7=b¦òÅÆ<�–lãÌ»@GÝ#»ÐåG}Ê�P.à°¶(4Uõ�'5¦7¨ñù�Ww¼/ '¸\dlâÀ�ùîJwt…epè–æèPv¿¹?ø—MáPøïZ¹¯`ÿþƒ‹s‹ÃC£\1ÁúÁm#˜½0üÑÔê"V œ4$£}  A4é&KQ> �†kÊðÔK·Ô利î"V–î<¶ˆ^ð–¿Žukk'Ÿx‰»vc‹ÁÀæ8“,KËK³¬.[\f­#±Œ(˾óÚæqD<òYÕÞ¶yúùCÕ»^W½ú¹Mì§?Xq®ßmîL¨éÉ©v홚÷¶… C]@R³ŽVDïA¹¢E SŸP¤$™&Ë>ÝÆ&‚ï:©)nkŸj´Í±‘íSãSÛ§&ØÚX¾¶ÎØÄ0P£®‚‡6AšuµÜ ±<5Qß§y—ûWœÑ6Û»pãïÿª˜ÞèS“Sã�Ù¥Å%N¾óÍmk°ÝØèx œ}ŽØ�ûî§Ÿ•Õçý²Þív·½íö�Ó°N?Iñ°éuÒq�èââJ—í¨VÌ�V¹Wbrs2"4MRÌoP|¼ÏFT¶……¥Ù¹yoyó —–b(g!žzê€c¡ß‚©@‘óÛ:hŸ(i¶hˆ”Š„Z´29-2´'7¹Ê«Žfì]m�-ÔËCÞŽ@;¡�c$d£®VÑðµi3óò*µj.yè�b3/ü3ËÎ\ú¬úÛq;+Lɨr¬AÅHN/D?–I[í´ÕN@°¡º~ÞÑ_óDgÊÇ"çù{šãÞ0@/²Ê”%¦®wŽ(##Gz ÝÎÓ½iCÇΈ :|;OdÙZln°ßå­!ÔÝ]†-,ÍYÚ?4+Ä „ w`¦(?4æ(ü"gÌ/YpÔÊ’ˆiµ]§˜6|0–2 ¹ NÊ(À“ Pz�R´Â´[LÉÁ¿6;³™ ù…¸Mjj�@;oðAKO^Ãàb :¢†-¦TI+ N˜Ö,ÕAÒˆéƒéÜlHIøMBJUe OA´ìäL bf‚ Š�¶/Ñ%+yŸë›dàF‚×¼~kï!uYS•‚‘èØ�›Z¤sË“ÉE• y ¤àT/—*Ž �à(ƒJñœ\ ð�hko‘Bæ‹'3¤z”]y[@IDAT*jX‚ñÔ5—ŽÉoæ„gýóƒ¯$¬iù³ºVJšÒ‚S sÀv®‹$7m%ù„"ñ€ʶ<âT¾|Ù¤ñT0<&SBY.¥‘ÁBг(áäOûiŠ¡ÞàkN#™(;"$kÐ4¦€–.¨é±õ„ü]¸M¬Úž(¡pÛÃ1²XÛÛžh—P‚D’D*LŠà5ÐÑÕŽ£ rpá7Nñéÿ•1½¥Ì@ûÄÿ)Pý¾Ë�WOYóX|Œˆ�øÅê»ÿQýõ×QlZŒé�GæîWϮӶ$r93ÅPóHû؈7Ó¶jjllbbƒÈFòÌR¤>ScÝ啹ޡ&Ʀ‰ëÀSn\=è 0 ,±»²»xÚ©§ŸtžíÛ§6Øúé1%ð™aSíÂä!J<𣯧œ|ò�O?ýpY¡—Ã^¾wÿÜü<5›ÀA‡¯Z•|6�6åä:Ϊ|—Ð]‡Ÿ)Ô$ßÒ71]ýì/khû…»7iÞ÷ÅôÆ ÃSO<� O›¨ø°É`öYÛàÐÂ:Ád§³}zbr¢g–ê¥æx¶ŒíyîŒNð^ÂÍáVKžzÚ)'ìÙ35¹øæy6 •Tþbb‹bsOmzãZÒŸyzõ–—×ùþî�Õsÿ¨\±º}jêŠÎèÁ¹C�åÎèÊ(L8<Ì>ì)þéCëVƒrÍH Y¦&'N9éD¹|3ä37¿°úq,ÜÒÒÄôÔî�ÓÍVÖÍ2†fƒ­�ÏR— »“ãã[•‚�r £éô$ü¼ÿÀŒpl£f¯¬€·hŽyã ·Ý{vŸrÒI;¶oW‹ÚÌQ¢ ÇÇX„wèÀµÀuÆÇ'LJ¶OƒÏ†<)qCX±Æ¹-½š:4;?{h¾á“A–&åä´É÷œÒP»×ÈâpÉOUÝüÜùÞ-îÆ PÓÛ1�‰íŒV{vm÷*ó+帔rïþ§œzêMÎÜÜFv8Ô²4<�ê‡PßÉɉOܳ©Ý­$ó™é‰Ë÷ïØÁ2+®WÅê7¼043ÊR¿Ø/¦wTœMÖßz£››‘µ{}7èï£ç¶wÓ´Êz·â¾õ•ò ?4Ʀõ 0!MOL\zÅ^ˆ6}Ò ‡ÛÝjGoëõp{OtquùŒ3otú)'Ãô²­ÿåð2–˜aÛª¸:`§¤¹†Ë«†¶Mr‘HYÏõ€Ç5¦7²ê½^˜Ã1;-Y‹È‚¹ÅîòäË'Sì{ßÜu¿GT½u|4Úö™™&jkcMÜ_z`ivÏÄ®£ÚÝ!alNcŒ8Ìßét0\^¶wß¡ùÙé©-Í—,½Ü{ààØäô§�>3Õ“‡A €z;vN�Œ ¼x®ZêNï»rM=�]X X¯g<ëšRnþƒ½çã-~¨9V/w¹Æî惇æ®8°ffm…E—kì�åÐ,ÜBáÚôµcâqSû0&w�2¸ézu•»­OZ[Û0€�øÁ7}õŒ³Ùn´´¶<¾2�˜^<¢ »þÒj Æ×oÛ�»®¿ÜÕ×N�»NGÏpdFÙ*ÿ•§UïÅÝV�Ûð–-Z ´h)ÐRàÿO`:ݬ,ûæ—›�xlÊ;ó…¬s9ÿ ™Tœqê©Óe–>³k�UÃÍý‰�s±ÛÎØ<7°€ëHZŒYXÜNÙ³{�•#×—þ±úÆ«‹¿[]q±ËaN¿Yu£ï«n{÷z–Î:âõ[ä6/mö@uáwê(ð?ùFøYÉvúi§œ¸gÏ:‹öÙ»²ï¢s+ÞYžzfuÚYÞåŠ9 ŽùÒ§L&Ú½kgIì÷eßóòо»ñ-\ˆ„­‡U@+4”}š€]ìèy·¾Cmz£9Êâ ›Ø)nv›>È#xX¥„± 3‡›ûk¸Èp‡û4¦·K/Øsjßê·óÄê„SI€åƒ�ËvÔ:=§€}çßkÿèXŸg á`ŠoµŽâ @Ž <šÛ³kçž]»Ö¥úú¿Tÿö¹êüoUû/¯ny{—æÝü¶ýv_—r³‡4”§ø±e›¢M ow·ê Ÿªs¼ÿ­Åô¦iifærl‹ËÝÑeZFSc�æàÞêC]ûùaÛuϹ0¿ïæªs¿¦Íˆ æg+ I˜Gi¦Þº-úÇé'Ÿ8r¹ªÖ™€|ñyµÿŒïsýÜòÉ÷V_þGY”ÊÞýÁ°i¿=¹Hw€&|Û¨›Ü²:“Ï-ªO/ë¨õ‰»wqyi�k³†ívïž™žb‡i|i±úÏ“ó©Å…ß©N9Säá|̾Å"VUl>ó´Sgçæš.sî׫n¦KXŠKMá z7x¾‚ß;AMm/e±†Ž=ãu¹'߸ÙÜgéA³2µ‹£]–jk…0–?=­k)pý ÜÈ’ºù%}úM_ö„ H±+¶g3<æ¹þ¥–�7P©}l)ð_†#7;ý4w«dQ­V®Ðuùñå-—–³¯£·…Å_vÖ±eŽD×HÉ[e’p氇ƹý޳YŠ �;WÆÿ@àÇ÷Â>“Ðõ¡.ð«7ì¹DÔr£«;pϪZ�™[åø4á&̽¡xÝ|é¡´¾ fçž²-‡­Û½)|'}¶_æ&usŸbð9XD+ž?–áÛrÑu_ª[zDÄSW@x™K¾¬)\žH*÷<*Z£lQ#ÉI> U™ ºÆ—B8–Jf+£Å†"e‹¢XI.›!+gƒÙy øU¯#Ò=“ÖNšH‡š6.B­©Ê%gæ…¦žÒ—:qšÜ!6i±¤&�I gÖ€R0å;t )ÌîÎòúm�™6„Vž¬a¼z,;0ñ™Ô•=9x.´u�¬ ²ƒ3dÌšW�¢ ùê¦æ¶‡²…M¹D¤åì1ö¥:$2‹¹Mn)À°V·5àykràR$T"’<5'À=½„D’s•WåDs#OJc GÙ¨Í][A‚�-!‘íC,m»”ßd¶Iùá10ö¤ð„ºRšcJ<šð@6<¦†Àd6§¨ó(ðkv@KApæŽÌhIƇ~�¯<ëŸPáZ›’¨ð°ð¥k½¿Ëê[XiÖ€Û¤ HØèu¡ceLdà8e%ŸÅ¿F2•jXø4˜ÈÁV¡?6…ºiO<hê$�¡°•±ƒ™ ˆ%˜§V^Ù �&¼ ][yW´ò*�çx噚)4v™Ÿyš— Ï­N¿)­¸…ù¹½—^–Õ•�ŽËK½xº8H‡ïä];°ÍìØ±}zÚ“¬†Æ8”Ê-§¥oo%¯+ôs„5PPÄ1x¾ëµÕ_þAʼnr‡;ιÊK«{=ÌvŽÅ}áªg?¨NøàÇW/z~J;iÏfJu8v�·üNõÎ?mL�ƒ�ït?/ÄÌ�®ƒÁGñc9zÆ}Öì] K¹'îœâØíщ‰©Ngœ‘ux,CF¡‹�ý­ðû…bg點I¢°©g çlm«––†¸€µ\u²Á�Xm6¸�ÿmõÒ'×aOx�w|ØvÂö™%VƒÅth,ÆÐÇܶNF‹üõ×k??Ø:ûQ»Oª>pqµ™�¶Ø½sÀ^ùÅOV¯}‘× ô›C‹{ñ›Êmý˜¾›ài'ž€�fçŽíS“Ó\y`ã203vì«®òäÆôÆ…§{/­²{÷„];¹£àà�ƒ;víÚ¾}¦1K}äoúEhtÆj¹Áa®}Ý‹«·þî†àúñž?Y=ÿÏJàÃ~Ú&Ù§?P½°Þ»Z½ñÓÕ×þ¥zÅ3šX|�ŽK0ŠÙ—#ùþäùëh2˜””OýÍŠ{Wc&ƒ©êÈ-b÷Î ç3Ú~ü]Õï?«±·BÆòûËX=èñ%ŒþFGk#š!¿úÓìp¬£Õ¼­zùÓÖ1¼qq¿ô»Õ£ŸS:,{Æë@ú;|Î�ɸ¾e¹·ÒÍÀXÉù¥OìÙ>ÓY[ÙÎþØí;XXj—ɆöV¿jõ+THz¹³GTkTÍ<Ê8rʨó ¯±ù ã)¬È­Êó{~²z™ÅöÝöï|ån‡¾}ñ™÷�y§°êO s·r´`e±R‰Vûe4Ê/#bæ)àÝÎÛù`Ñ‹`ùÂy� ÞίßóAf¿êµ¸¢ãÚz6ͨ0Ÿ–4@ÃPÒEжD0µ/"ÈoF0ÒÆ'·bƒyx&â BH2‡³fÒaÜóI£‚0S*sñ)fE�— €n-€Äœ›ÈЂRbYyˆdþ¯ñƒÈ0ˆä^±g 5Ö€€—?ä ÿ‚7y±(hcPRš„ ©ƒ2$©3ÖK¦$ÎÄÅ™ƒ ©’&­N¤ÕzåYþ¹.‚ÜCš%�pò@z6ň‰©•­¡¬ÆÆ$ÌÑ=TOˆ`ãÑÊ£ÕB³šÙDÔh‚“$T”–à•FdÏIÃ¥E<@­jÉ ^žï) uÖ”gµ H½ž‹²MJÙx¶¸ö*È&(µÌz)¡n ÑÖ6�-#F”RPiÚ¤b¤1ÚR[ž'Ž‚X@!É@åc  Iheõç€73Î †TJÀñc}�…JUCöÔ‘{J:i¤Š“Vâc}Ìfö”j‰C�ú�X‚ka�0 xƉIÀ ‚9“\¹Ä"…›2-"ÅX_œÕI *‰IL‘|'ll²zBøÆ:^êQ°,¼*@©K¢‚Jš4)¨]YÔ‚-ØÛn”m¶„Y‚: Û—<©£9ÕD§0̶•eYÝâOT¢Å\|¦†!�Æò *`ûqj-ç’H3?p�–,…?åM@Z@{“‰a~ÛÒ‚ô¶o½[yÕÊ«š3[y¥< ³l-¯J—·;¹BçW¹Bá覆}±zEsè#=ñ„N˜šìmL»ˆh—$O¾XݧÈòuŽ×S:22г¼¢[Óù# +Øs²zÅ37·»‘ä;ÿQ=ÿÕo=)r§”e¾ëk÷~þGª¿xÅæV`sÚ×ãïX½çÍÇQ 6ØÝ^û±¾�EœÉ57»À�BPŒ^Ë€!Ýyqýµ?ýÕ@¬ÂôS\oYbý¸ÅÏÌÌÌÜn–”íjìn$þMW·ú‘-ro îrmÁJ·ÁpcüUznZÿýÿ«ú…ÝÒÆô’'T¯yÁJr8s$@E$+1G¸¤ÓûkûÃÙ½¶.;V§8® aÃf.ç„/�z©ÞýÆž¯ªöÔ¾­¹d�ÛÏýð–v7}üÿV�¾Mõ�ï/éÁ ×ìûH°ÁîF­,xü÷®žx—-iRRþöSª_TÕ0‹÷�æi8Ÿîüô{WÏû™ÍíndäV hþ܇¸"/n€4pYô÷¿T½ø±[ö WÿŠ@0np·üá: ¿4²oƒc}b�VØ�9žNÚÙ6 5‹V¿jõ+U^ÕMz;º§# Ÿëp>ÈÜqhtdùì»l`se;œrö䪷�*Ž¢µ¢ÑyåÄ…aP_ñŃñ¨Àí|0dhçƒò\ÑÎìò×÷ù ožÎäùwêê¨åµfL‡c8¢¨ÌLû±~� –wb£t“y¦Ö²m›w¥bF"�“nà¡g—ù¿³mUF3+/8Ï?k�,_ËÛqd#¥ƒ‰…;!Ç9‡²Wš+EQÈP@9I€ÀI‚w�{+ƒD°OVÒd¡’’·Ë…ª‚ 6Z’,óŠs~óâÓ륨ºòÙ2I!E0)Õ5`hOIM¬�ýç] iv�Oh!«…ÄP`M…+K�º$I(%êÆI¸FYyHz/ib¯NˆEH^7šÙl,‚@‹œTžARjaÉþ‡zåÑÀÇR#¢X$)>7Æú[8AšØòÔžoæX,J “H¨R†@¦!œo“-•[Ti6©4Š™)…K *T ‡EiÀ2*iD”X‡-¥L]nØ€XZ „ÈUF%yÏv3¡eÔ -(ÛS°ÐR“0¡òWx 4 µv]¬°¶ÄBA�ÊJ.ëH:bu–”:‡Å|È»c(ÎÂE/ÍGVUV‡?Iè_�RÃ`,<œ•BKÀg·—4ŽI i` Fñ¥Mh yäR©üZ]Éfqqu-)m-‰={,>–}Xg1JjÌ� PkÁD„�ÁƒbíÿtÚÄPœˆÁÈÁвñ1w„èpƒÙ �‘4Œ‘ t˜‡'ß*¦Y«£SØ H™!š+%°u+Gn·†@° àZ+¯ZyÕÊ««I^!@ØÊ1cöÇÍÓÌC\Ô‰é ÃD�äm¿×¤e’ÙjŠm§Ùˆ |ù3=ðUuÿõ¸}õ³ÕîÔÄ §vT_ý\ =ëÕï¼£º÷Ï4�|þ’ >ÞáÞÕ)7Ö(†ulÐa�eÛ5;y/øÖ:óôGßY�ý‡ÕÏ>w0í‘üôµçÿÌ:TKj6;çk Á dyàWþ©zÇ×*ökoêX¹V¯m[ÙKûþÌ‹>Ý{´‹ûpg{Í®Ž•nÏü¤HuV³«Ë—Z �p}ë¨]¥MŸ)ª‚*Kß©™Ø“ÐZýªÕ¯TÃ%Š5Êkl>¨9[½\é±v»»W_þtŸ'ñì|÷¸iÁ[€úê×p'J0XEë&_;”^NÅœºHç (ÿí|0,ÜÎo`óA7ªÐ!�4:Ô–vž¨x*)2;idr…”“i&ÎQS4yE1tí?SS©ØÐ9œ>ûïd?$aEû!y aï ›ÖØnç¢&‹öoçÀÍⱇù–1Óò’‡³B‚(É4!¤8'ü)+)5‚Q&F0Ð(ÂÚ§T¦B&ôƈ}Þ _±ë©!j’%† m„}¢XmM<¸bëÛ&5%ø €²v�B5nÊ0-ix<7 ›šÈu$Ì+ž¾.侯žóªê„Ó $–Ý—ì¡ëà^ðÈê}ßË›ƒu™Ö=°ê©÷h,Ø ÿøÃÕ­ïØO377wéeœ†¿���Ê<(€ð”6Í”œ°öóÏïg©=÷{ÔÆ�ß竹ƒë‚9aq¡:´¯úϯþü7Ö‘÷ÑÏj6ú­Ë³ÉÈ1•ß$âê š?Týî3×{Áë´v�MØœ+÷’Ç7M¿.�=*LÆò0¨ìàŒNèÐR=ðçÓç¾qtÚ7ÂØGðu°2ÛeŠûàÛj?w †°¾àï­ÇöÉ/®~îW«‰)“`Õú쇪>ºaµÝåþulÈážúÑŠ…`Ÿù{7r²�÷±w6@Î8«:ç/m¸Õ?UýÖ“4&¼G=«:Ûáà×…¼þÅëèÉ�z/z£ `ÕÓÖ<ôíeOi jt�ןãæÓ#;Ìv¿ôû.¥ìŒÛdŸø;÷ÉöÝ_½ªzÔ/­C�~1Ð5LHGxÜóú9ðÐvK‹‹ìžfÏ4¶Q¬£ê.Î:¢”´úœ­¢ÖêW×õ|�ÑÏ9IµŠémýô±o|�‹ŠW¸ †µáªÕê­ö2g—í|‚0º´óÁv>ø_g>èl7p šg°ò¸ÈÊ.¯E€$�hÉ*ë qÕ¦³ü�‡P#&øBCŒV¨Bf©E8ûMJ ®®0óXÀ8sf� 1ñØú9�Âüð¥ $3òäsŽïeâ*¥úœQ—t"CÅ8&枘îðZ c ÐÚáÂV� ›"’09õ©ÛT^ǧn&Cû@5¸¤1ó’h¡ƒ µÔHEâÐÎͨ†ÅÄ’PM8ÊO~uU£  ù‘†�@1Ž`2!ˆ¸XcD(U·dµpSZJ š½¢š›×*©—º¦^ ÓL’4&¦Ä°ÙµIÆŠ‚0ºã´ÅpÞ†A>0@©WÀX>v)üDäV´‘„æ"%xÿ�_*åcŠ.¦/k PÒ ´vR ¨Ò <˜u†X+%•ÅŸYïäÅ!¨kPÓöDrÒƒ.Xcd,M@‰ªÕÎg%7–ÑŠ£ I[*ž–€Ä'm6ô †�In°’ö�Be y-$4çR0Ÿe&ÁmT�(êa²Bkèm�ƒ¹ÍQ°ŒŠ_òaG vÙ‚² ­„Ãü ªÑ7‰ yIf»Q´Uó�ú“YV-uN(FÁÜœtw€ÑáhOÀѼ2c©„ØëIdõ=‚¬Œ}Í6 ®A×F&Â,jÑmùt^wß‚!™Ã„lðæAÌ¡ âHô¬s©¸%ÛGT+¯ZyÕÊ««]^!¸ŽíС9Ll‡šŸŸç\0%ÔÚÚìì¡óÏ?ÿŠË/?ù”“'û[M9íþy¥‹6®·€…,d_X˜·?óž Òÿ¨òJ8¾aÊÜtï%ë&ê÷zhõ–ÏyÖò³8ü¯z³ï’À¿½Žºr?ÿø¾æÂ °Üæ·Þ^ÛÝx¤>÷úéê÷þo'ÇÁ}ëËÕ/ܽ±•`w{íÿ4.ÌÎÎ^pÁ^ø=Nëpw¥[Yi•‘…¹G9Dôð�õª_®î¹}Ýç>»«œV=âªç=l�Ýí'Sý�æ„2ñ(°3&•ñà()¯\4v™¾‰þ©ê§þ»v7 �öO?^mØ.º¡  >„ýŠ¡“ñÉ-ªA,7É‹6¢o}§Šµi}×c!Œnp;WRÔ1¤×ëú©ª Ë±Ê .m{ý'ªÿþ�e�;îöÀ꯾ÜX6©Ú_þ~mSU~Ý'ªg¿²zûW«÷ž_¯’ûüÇš´¯øÛuv7"XJùÊz7«ÉXÈ™qÇâ`ã·¼¼Iø�'Jð¾�¢±ô5­óì& fw¶QÁä Ÿ®n÷£¶Zi²ŸÿµêJaßúÕ@GÖ�Z\X`•(íÇ-´^æâK:E ­­Ž‚òÐêW­~¥ö /Ðé¯Ëù lÉø·pö�ö¹·ï9ó[Ÿué&¯¼ØrêAeF,·óAÚ®�¶óÁÿJóAtV¦ÄÅÈã\·Öœ±˜Ü:3fÌ´?Óh´ed—Û ²IŒ$J2¿]'£ÁE ¯ý…SÐøÅÄo´Óqˆ?Ší*Àq.(r �8V²˜#ìbβ™Ç[´SðÐ[D,­`(&̺•cZ–ôRªßÀÍ’¶:¹V€bhÀ¬Òqóª9‚HðpIåa-Yq{¡uÁ\¡Àró]Ì4VÄ ÖF‰!V:HS•T‹jf.ìèÏüÅYA@PöÐF>˜†âÓ>aMëoÀƒ£¶7 2’$H3ð˜ÔT^x5±ÄWÒŽ\|§ ÅKŒÁP½ßB# ñ¡A©°%–ºŠ™%ù%Õœà@Á1È�8m8ÖÛÄ’×Ò Va"Z¬˜¡z‰‚ �°ŽH¯Õ¥îâ¬N\ZN˜¥ µHš#ô  B�j}ƒœ5 ?9v—²ÀK\hKô1ÈÐkÀðœ øÖªÑÌß,‰4¥EŠžÈLµ$LœŽ áâd*°ty •e3'ö4ቬ); Î|À 1‹9*Ä¥H¶; _ºfqeøN\N±š%•}@´ý’:Fƒ1Ö66pâÃõÐÎ3)Á7Èèíùb 5i¹ ÀZ—*¤£Á�aKê@8_á!¡Ù(»3‰µ)|’…‡4ÒòÇÛ>j™�i â ‘A‹xyPV%¦ÿ )îôša–£³ÔŒ”…Ä£® ÅY¦¨´òJªÔfe[0|­²Ö¨•W²‘ìÒÊ+{_‘ ›È+ÞÎ cÖ^ÅtÑÅ}ó›ßüòW¾ò¹Ï}îÓŸþô¾ð%ì§ßèŒÉr«)P¸´ñ9j×ÂvÈSδ–øt»á@ÿ‘QMI¹ÏˆraÓôß­å•ûÎÏ:–ؾ³’Ó¯žýMª÷¾¥ñ_ ßGÿ¦É„q籿Rdbˆ�½ŸX÷¡­�}\ëùä»­³»aÎèÝŽ ¤ƒž÷Ýï^xáE�1.ºd•ÕX1¾!ü�Ó,ˆà0µÅÅÅl ‡? K<ŽÒzÁŸõ×K‰*ˆºÖ5ä>ðÖð#žYýà]›Ç⃞;°èr}t(ø¦÷÷õ«Œ�Œt* �Y“Z(!Ñäìv>xÚ²& iDãeº[¼zŒ˜—Ù±³d±LÙ Ž×_?‘iÛ\)Ü™#k¹Zˆ4Y$#t>u!À±-3Ñ�„k)�Á·³í' LÀc€`F“V[&ŒÕ:á ˆD�eîͯg—¥wéwŠO…�y&GóëT�däXޱ&(- @ë×$dRš–iˆpЂ *Öê( …¤&D«¢ÁG1 lœòÝx²¡á±™“Å$)µ'O1±È�£a‘@,á´7¬V£¥äÛñ(ÂîWcâ#æR5q)iâ4x,˜�ƒHÌ@@Д#@²âÉw²Sóâ,Œ,´G�(Dµä!JSH¢UÕ£îµærHÂöbV’�$–©_ÎÙ+øJ°@¶Á,Q«Š ±(‰ÖÀÆ4ÂdP2õ 0 j€ÿ ìÈk­pÖ® Äü2T&RË€ Y¢‘˜�Ò@â“Ä �C˜‘¬'-Õ“ùl0R#F-QñŸá[‹’¹ù“Òóò”VD.qó.E%‡”1 / „f¨pE4$Ä“6´Z%Ž¥Œì(­–l¡¢e‰µx–¶³3•ÕvÖÉZ§d|üZxŸÊi,µ{˜"5 "¸A¶ fovªË*CŠÍ—Ü<¤® I&�Ý@¢¿« ƒ=FLø¦v: ´‰Y¼6Tš…ï Í—­9Øü´É{ä”j­¼¢A[yÕÊ«-äk­:‡ÅÑò×!NŽ]òýef¶Í¿cÇŽæ|7 /xøºC¯~ìÁÕÏ5›Â°»:tˆ»™�bEG0)íúéÁvpºçFy¥`³ãˆÕ ÿÏ^Èþ­ áœôä’’õDLæO;³â¢Oîý,³@8®oŒ�ÕwO{©²«ç(±¡ÀWýóÇ«›ÞÊ–¸MÇc=é.�Ý sÆk?îÒ¡žãp·óÎ?ïòË.›˜À¤9Á-�˜ß¸1Ò tÔl•µÓÃkË+„Žˆ/)Èû�••ù¹¹Ù¹C;¶ïèhR®,-�OL²_µûøÙhyÿSª¾¡ºÏÃÉÜ4Á!c²#ÂØ"’ÓÜò{ÌsúéTWúÂ>ÜGþâº]ÏýtŽ`±ú�·öMÞMú¾±úŸoíR›ð#ø>÷‘&òÉç4ëõகÍ[Åq¢Ü3~Ç[MŠûÜG›ûÍQ‡æçÑÏ®vžÐX×jì¿+¡½0[Ò —\zÙ%ˆžô²5Í:Èé“òynh¥Å÷îÝ{ðÀþq:ËÄxÙ̺7Ú�ŒEº(ijV-hõ«V¿RiM/WŠ" TÛ aÅkr>ÈÄö^~Üs‡Ÿû©Âê½oNœÔ¹çM•yˆ�¢³…v>H/VrÛ`/N/Úù T¨Gg©nçƒ7ˆù ç©ehŠÍ�¯³Sþìôµ‚ 3çÍbM6eÓ™”vÔÚŸ/6' Íéœu'I¬T1°ø%“0b«¾0º×¯'îÈ-™– d'Wd(Û$Îå•X`x¬­ $�ÀÞ .ªVêVd©ÛbŒ*QÉN¡¨å©�1³z—n è|´ŸD*× &Þ’P&ÆKÐÒ"|ñÅ¥ƒ}¦dôY(‰Å àñGn( pÕo˜ÛðúV%&ÆR`$›0€lF< °c˜!ÕžNc4¥ÅÑzZ‘�“.–z„-;XA©‡¢©œ‰5ÿ©¯I¡çA|ŽâiÞ1-…À4½ÁÚr¤dMBð‰o)°à#A¬¯é‹0 bžD &2è@�û¬‰ä%Lr"º gàÙkLCSCm¡üÑš\×)�ð}¾¼ 5ˆðÐ-MRp仨ö„,a)¯8±Ç�hiáå>µº'‚¦*ÈZu5>q±uS«RE¡¥‘€…õ×6iIWdòÒ g/çYii `ÞVÙä¥"&*EÀ¢šŠ§]%7É8£�(s¦i­WC”f^R/6åǦFipƒÛÓ€uÁDÂa\gÐúT_tcÙ§À¡|Îwì]@·’.FÕˆØ}�üÚ㤜xJñ`M’V^A©ÛÊ+Ù?ŒÜÊ+ag¹:ä]�I/´õàr:|§3Öe³Ý3Ü‰É ûoq¬Óá¾Ñ¾í€@L Ìó{Vª¹ùùË/¿ÛÛôÌÌè[†¹º ù³gG¶¤õü}'"‚š…½u9þ|öÃÍ;=ã"_|ÑE]rÓ³nºsÇÃXûÃŽ¼«îÎûfc)ÚÝT@²ˆïüóÎã躛žuV}Î[�\"çÓ žï Ö»q DÏíÛ·ï»ØÝ.¿‚-�\;Åÿ{džGi„‘Ò¤£kC]é]æ0¸ù…y융ÇDÍ�Ïs‡"Ø&vîÚyj#p¯ˆú—5J,˜ÚàX´ÈYoç{]0›1©<ï5ÕÞ¶.|‹µ¿kÎ}õŸØ›N»iydÑÓ¹ß=wûÌöSN9¥¶šaüŽ:÷‡þ÷Æ›^¹‹ó=oi`s<Ü ûÞ%9�íðõbƒ‰ÙÚï>œ+·•émÏ©%í Æ €»üDõ±Þ 5®2xÈMÄœ%xw¸�7W¶å6yŽæ;¸oýÖãŸ-àüóÎ?ÿ¼ó/¸É�otúi§aŽ7ü^[Ž«Ng0RÜèæ%–» .8Þ>ùä“ëŠ ˜ä*.ú(n[Å‘_ûÚ×FÒ¸©‰ö>°o‰G\0bÈŸšž£#êXùuÄ(4õˆ|óÕêW­~Uk©2Ðu6¤ïæmÊàÀ&B÷vãoöÒÛ܃{ï¼la„©&›=”WátUjåWÄ,Š´i¸»�ªïH—v>ØÎäùs霒±^ýèúh¿Ò¢¦.¢Rº5Ê.:B¢«8~ÅFÅ,{hhÅ…�…$S…q §¶êj¨�øuZˆ¸Ð8à"©€/tA^XBæÓÅïD;!ˆ%m†}m­“œ<Vìc”Gïn’F½‰œef ‚Y]Ö31 ¡JÉ$"·%� ¸Eލh¯@KUHdɶEsmuÙ%>ˆ† JRšDĬ(V¡nÍe³^11U4QN�‹áÀœÀiHèÄÀÀ";`):‹ƒ@±ÈeâK¾(+ÄÔži%@"j0Ì…¬@0 PG„ýPÔ$QpMò ­0EƒqA]Ób"#"WžV†ä·=*\S–*iw¢½@Ë^ ¹ì6׺}ÄÝ+!BA9/Ùü˜t}¬©FQʼn´¨”´Z©À ¸ÄÂÁrA”òÄUØ…kåU™LF�¾Žïæªl %T÷gñç®pdõ…`mœ(“O*Èø+¾(H (‚ø2UG,ÛèRmÏYZ—f¼Âî¥ÃŠ› ͨ>[#ñ$ •W´¶\.i¡P+¯"õ–0H+¯è8WF^Ñ“L‡YÕ²mw?Ž 7Ç'F1¾•ùv:duÞ7ª_ú‰u&ìn¯þ@5XÈ\†áíŠËÉÈš¬Ñѱ‘!Ž–`aŠï›([É«È*Yº±½±­‹%¥÷¦÷KÝî�:ˆfÇöíµ+è]Åo®Lí;©AŠ8,_ûöîgÔÅþÅl¿ŸäHžÃغô{}ÓK±å]vÙÅ“SÓSãšÝ°»��k©ä "�/e#»õ‡ªNGY�btx¥3Ž!cÅ·oŒ§HrZÚÁkb|’¦Û.¸ÜÊ”†ÕƒUWü¼êŸ>Ôä}ùÓ+%q¸þ±¸¢KÊãM³w -N½I?7¤Û»w ÅîÝKu[ôíMýD=�Ê6·2¼fqdrxáÀv{�ŽÖ/™½Ä ozãÖTÖvÑ }÷±wõ½Ƶ Wy`´í;Ìa�ûáþÓ&:Qß�?�±ˆ‡[\3˜â½üòK»ÝL�5«s9{9»hÌYûy�Ÿ¬îüãîTíqï Ô-ý¬òë;€÷º3]lÿ¾eÿ8¦çé" Pk(¨¿l�º÷úf†ž^»À°‹ó ñ›ØrõDyî�¸GíP5°îiP�Ý9ö‘äÀ�n«Æ@É<Òq}hg´CÕš”ÆFùQ»QYiõ+È¥Z¨r×êW=Ö»ÖçƒQ~i‡¡Å³ï1|ö]†×Ÿlˆ™€Eoô/GÊÛì*º(Ûr^ ­v>ˆòO/‡,�ú¢C˜ôyؽ�n¥_!Ûù ó¦ëÐ~…iË™0s\Ö<1}cÜ*&x¹X ò�Â`°@ÛT|“ î–åI¤M,ù š ¼bÑ 5éJ§ˆ†ÀC †I¦Â¹²Hc+í-”ƒ+?@Y<Å(¢¸±DpEoÒ~€@Ññc> ÇÃ�öøksÁé�„PV $)%›cwA’_UÛXNBŽÔD;FïRÎÞ t^K�~õ@B´$ Y3•dIÃ�hk²u³§þÔ8ÖŠP©„Eg1WOI“«*2MIu!ðÉAIž�šA 2™Ã³) ¤0tÚMç¯Äé y-„PúªqIžˆf)×´!Oˆ ‘݃i�iéØS×mÚKY}æZ3J¡V€¶‰µPr ñbǼ<œëBŒ5“[äx#™ƒ\Ò‰u)s3Écƒš�¹]P‚Šæ Xa<X‘‚¡rfb¾AÏŠxÀÂ!€Ù-Ð\7íU©K/œX�:£Õ"t3�ÊÙ?¢¢ʧ±Â³>·pø¸y´$¥‘ÙÓøÔ5eY _¶�ø…’ilxŽ‹y5+ÚMád³Z¦Œ)'QU‹ÅÃ-i,D¶°‘‹+•L¥ls1¡E¦ëë!(‚´RC”—ZRA’)„×t¢T|XSmw ›œšÙÓ 2‰@�ˆyñƒ¯Ç¤SKR‹måU+¯d�V^]còJûXÄÅ*7’n«FG˜ëì¤Å}å3Õ/þ·usþ;Ü»zù»úudÌ¥—^rÉÅÒeÇ9¸LÃÖ$q¬ c� ¯* /έûŽk݉¹cªñ‰ÑfõÐ�³33ÓSSƒ©®’ÐÜsãzÍKyósó‡β6­9üë¸ ûÍ'Toûb51M&VºíÞ³g~nvd´3>†�!¶¶š"ìëÊW~E°�r,騿uº#+c˜Ý•Œ ~”ÇŽ€ÀJq\è˜ë ϽúýÞ¾7xïäëϱY�Á)š¯!wùE àžGaXtüA¿ÊðO¢�'6)×ûdÔn¼FÌÏl8¸ >ÃôÆ-"\¾Í5`ÅjyÇûVØ[ËÅ|þã!}÷î7ö½ÕCŸÒø‹oКFÈá†×�zÏßýzÏ·þ÷¬æ44´Òƒ³‡v/-ÕŒ5Þü¹ê÷m¶�ö³bãóæ—i›ãˆÇ>·¹!¤ŸfSÏþËš`¶l÷\;ww‰}ÐjEß*QgÞ|Àôö�êö÷èåø=éôò5O}¿‰ô7¡*ú©æÄ+@Ç;X¢‘I¼žŽ>•J!åÅ leÇô¦y.Š—Ì §§�·úUT±V¿’u Âu5„ïYaѨ/ÜŽwç{Þð½[Þ5{N—WWGKiçƒ6´pNÐÎ#Ø2Ê·óAü w>ˆ~VÏnµDÑ‚†â„<‚[� ¼ÂÐÚj¤ ':‹Ã[Íä¸úåäʘÇWYb'ˆ_'ò,ãq…’@4S‰ƒ;™—“ ë�3tž¦épÀ`ö^¬A‹'{¢€Ô;¢í@“\Àгå¦Ó�Œg›ùc’%¬�Խʙ�EŠ$wdךQCÝ0­»e,Te8Yˆ7‰– M@ÁÞ'!ЍoZ�PŠš�†�üþZIV™Õ%E@-( =÷Y¸¿P¬“TÈiµø�½8ŠÆ/FÂÃY][†:ø€Ë²ÄúT(¨"ƒ:V0<'ß©8I(\LW±­ò,m„� G|B ɘ"]_(*={“Y%Dþ~ôh>¢ž&ãŸßbG*XÂV ‡2Ejç˜|3fBs`øƒúb& -Sé�˜TJÎ�Ó ;)\%~dói®|ð`ë,è£õ—»]ÈÄSg Äd¶‰7‡¿ü_§_©Ì0>2>/õ�­#ÕßрáÆÒ¾»àÛkيêuχöcjÏEßÝrŒÏ¬…ÜÔ ì6EÆ8ÀöâCMBÖܽõóvÀ­kâþò•Õc¨úÒ?n•d]øàÕÁ3ìèv£#pkÕ5Å‹›ÞÙdïÖÖ„ßHï,<š€ÌŽÞGq4»­YÏ6>1gOqþáÔì�wÆçkÌ3Þ&'ÆÆ'9ç�S‹€Š²€l!¿¶ú•ÜÞêW‘N!… r�Í•=t�áµ¥Çýʆ0ö�]^âŽÓ.‹ÞÊDBivÞ ¯€‘Þ×Î%ƒ$mçƒ5395k烙¢†"7E9¸~Ù¯FÔ{3;Î0U&ºŒŠNÈóa ㄹ7/–Ë3‡GŽ9%ÏD®òÄcÒ2Ÿ§ºQªx4´yª9ÒÎIs-øÊ¸(¼8M'™™ XM\„‘›gU+Í8ö±@Ĭ�/Szs� 65��eªÌ9¨ÀªûJéQ­Ð©r4»É¨µæ³À"¯ó5¿ ?„;jÓ`kš$)³pbßj²‰½yJêHKP¥Äav]â Z�^yq-òÇÚè&>O¥øÅªi�±lˆ• Én¤ˆY/‘˜ޤ7–ðÔ!¦2=�$ņTf5bh­XŽäûa@—É)MÌ€–(Ú³$d œò0F¥üä-BXM†Fl Ì¤,ˆlöüR–t¶2)Z€ê[À—)j¦£D¨•Š•Ê™˜?Ri®¶q¦"‚0J&Drð_jН$“^üã­¼’�d˜¥•WÇ#¯‹öAœ]¶¸¿zeõÊæœ{Ãÿüê©/uø‰£C_zÉ¥ç�w§51Qfæ<>1Æ¢”Š;FT'èµÈË óu÷W гùA<(>xDǶÅjÇñ[ƒnþPY2FÎ�ÂpâžÝl8íM¿“^YÿžSšœ6ÂF»wíd‘å7ÉŽà»É÷WòÑêÍ¿]½ã5u*Ž$㜬,ÂŽ¶k×.èÂø‹!‰’Á†Â×èW#‘�Îð¯|4 rÀ¬×Écd@þ­.³nk]{m�Uv6i©ÔÊǹ~ƒáÄ´w¿¹Éwѹý³ºš@|ŠæÚ�š£Oß--ô½WƒgÀôÙ?‰°Œ)Ù�¸ù²TmC©ØÚÔ /|ÝlF½Z3Þ±ZŸKÂVVF¸¶¾�âǶzÝ95¨ñÜ?®oøà_6°ü„M®Ùä–‚nº ¬1àÛt«æ@<^Zxšýȃû¾ ÝuRÅ‹ÜCú™Vÿðn?ƒ[P Èòä»U¯z_u×û¯yØSo‡©p ŒB7g Z†ûç_|^bûžÆ¿…/²¸—w‹4Óº°2ƾq?žùæý" jÈr5ôúŠ€Îà®k¥†bȘ­~ÕêW×Çù`„6ixþì›8ŒùÏ<÷Ÿ/ý�»³ëtymyQÄf7ºÀÏ2yE@8¼�¶óA§sŽfŽbí|P¥é†2IJj¼ÌìÊœDǨeƒ¢˜dü*ÆŒb¿±×#XÜB.š�Üh/VÙÁÎÑ/³f¾�y×3À 'ƒ0Ifàe=’ $Vg̼(ؤ`$ÕnQ#“oŠº”Ä~ñ<­%È$s«Èc`0‡ÑyFuœf)ž’Š�ú}“ÙI<PôW#-6‹e–aSŠ�_€äüZ�z¨ýŠ€KÌf$5w�U\K}|ùV‹N’ƒ fçAÀ�²Û8JFHæ«•îèÜ”`oé’Q \ÌcUjäÏâM}H Žª™Á†SJòëœF´C¤¤ ©É¦íŒ2bxñ7“£Ø9ln!¤i�Ä'Î!¤d° B�•‰à²ZŒ_?¢�JXÒQ/�JVNP�% &Kq^ô£U�2\ �¦5Þ–,Ô²öø I z¢I˜mƒ“ Õ¬}¤}IKá–]…"Ÿ“#H/r …ù-…»>Qd`]J_€�¶XM5+û¸më˜ñ<ÔJ™Éˆ…Ó3Ë�`´èÀ øCrÙy@¡i°ÊLªn@pœ¡—4kÅ”mqÁ› zĘb�/$«j]™ÖëßÒö|� ÍåÍAB‡t2…Ä&’…`L c,³Ø–HÙž.ó|¨´ñT�Df,Xºc=xô²Š¸iä?ê'ò> ’”èöÞñ¤ÙœXð”JD›‡æoåU+¯ZyEß ;¤¿^ýò ñ‰çüWÉPÜŸüZõ–—÷òû‚×W?õ ý"]‚Ýíüó9jŒ{Ù>9ÊþÇKY•¾¶ŒX©–#›íè‘W éïñHÄE*¤Ìg”Œð br¦ÙÈãec©32¢qo|’¹y�F z¦À:ðxθY“ãÂsû~ U¬Ä鏨fœÒëpŽ¢< ¬Ÿºx°»½öÖçývõ‘¿©·1õâÇVïø÷bCdñZ®q<ÙÔÊžR~{ШÈ/úU‘„ÐÎ×P Jø”í}rME¢O· ˜ô)…Ë)¾ò•/1æ mãpú¡›Þì¦7:ã Ô™dp�ì÷ì“ß�ßåÁÕp "u‚CõqøëÒ_é‡AÃ+·�ÇPÅVCî{ÕÒØkˆƒ{·.Äå�K4ÃÁtŽH ÆÄÌ•�f76£#³h‚ à› BŽ Ô²Á@*ƒ™S/Å1ÅWšêTIÁ xfÍ·y�êûo6ÍÛx‡©Y#³Áø© Ò„&bT/Šã°[ìPj*ؘº¨š– óª§@Œ0ALZD÷¬!B€*¼H"’€»qÔR!¢EY�RÓ~µòª•W¥—‡ƒZy•Ž“ŽyuÉ+å—bG[º}¶¸?ûŸínôÁA»©ØõÉæ¯›Ýì¬ÛÝîv·ºå-¸ qfûôôöé�ÛgÜɶ±)v‰±yŒ ’ì£?wW9Yº»Ì’#ÅB¨‘W=”â9ͽïz{ú0°dldt [€’§¸s~¶ºópõˆ[T¿úSÕ×ÿ¥Ÿéø<'߸IÏŠ¡Åùò˜½³Éæ:œ:v·ž^Ýk»êÿú#7î�%ÑÙwÕî†c=Ñ‹ÞXçâç’óû§ªAçiîjœ˜XZ\\î²îÁ9ºµã{Æ¢Aý*ÒÕ�Á[0júrŒéÓ¡)i£�âvíÜuç;ÝõŽw¼Ëîp‡¹ã�œtâIõ�•¤Åp3è¶ïnžúF.‚–ûá”Êl’1º¹ô‚~ÔÕà\vn} %±ápj‚%•\IÑk Hº©Cå`tÕš©–”Ç1Ù‘ÇQWý„çhëö,?äI °�½S?Wô‹¸åí=/ŽÆ*¿oüý�ÿ«Ÿmü›ú`žWl³i &¦© RP<š�zÜâöÕãžW½þ“Õ;ÿ£ºÝÝP˜ÒwR7¾Þ¹lufî86�CpæÎ‘¾¡™ëqû!ÙŽ£¯z °ŸÐ[~;Ôc~�ÓájßëQ76 •dô°: H@whõ«PµÕ¯j�jØÑý¿ÞÌ™µ dà\^á¬vÌtŽ{[ZEöW«K+ìœb¦áë§ÍäU¤C´óAÄ@;lçƒtg¯7û«¿2-g¼,¾Â“90âJ™¥‘Á©¯g•aér¹×²6Æy±)³x‡@t@ød*¯�ÀÈ22j� ‡à ˃¢]k¡É¼‰ljDL©A# 麆Õôp䔢³äW}Ò0ðÿ±w.`–eU}?UuoU¿{^080ŽA@dˆ1¨(/ßAÆGðCƒ‘(JT�@TdCú©&Q!DT ¢ "¨Aa‚"ŸÊc˜G?¦{z¦ÓÝõÌï÷_çÖ½U]ÕÓ34ã4îÓÕ÷ž³÷Úëµ×^{íu÷9'©JÝŒ$£¦ Ę,8\ UŸëÐ4�”ÄIÈPl��ó2V¥Q¢}CY€wÄ”_™b•$ytÆ�N«ùZÉ/€"ŠÓ÷Ìd“ø½��`Á“üÐL(¹ ߊDq ¨Õœs%W%Š™¬"ú€žÀ)‡ †„"()\B4™N ©4ÁÂûrïkdŠf*Ø”_š‘4Ç'·ißL“ƒ5½v¨3Ɖœ~‡3ùSIf¡Ð7ő͂4CZßzaäds)XN–ŠAS„N£·ê.ÀïV‚,!CÓ]Y ˆ¼Á£Z™�0FàÞ_ÔC¯¹ ®±·ÿ­öøó%A"åëC¶�B.ì%qs‘F|˜Lò%—Šî¯N·œ ¦4ØcÉ#™•‘î¬.#0Ðö®¶# '’”}ê ‡ùЃ3à=‚Û¦¢vMãM1PÚ!œHÄæ¦·óI—¦Êe„Ê—¶a†>å Ä5\�iýáÄÐ±Ï ¤7e@�©/5¥j´§é‘°mÑÑÊ®ö¡�‚ÞñÎ(µ­Ì�0à¦ËÔHÞ"þE¸u™ðá%lBT|‚2�´èˆà'hþªù+Lûiþê¬û«¬vç’ŒÊßùÕîUÏwxÖÁö–7üM÷ß8ºî¿}iÇùçïæØµû¼óÎ㓜/[ fžk6;çCšNœ˜_šç¥Ü6†/ЭÅÿÔOm£v“�ß]•’z»øâ‹/½ô>Ðíáx0Ù;ÞèùµíÞý–õ¸z 3øš�ëîyŸ1Ü;ßPçdúî}É}.ºè¢¹Ú FéßÀ�lÜšÇõ?ôÞÚÅ6n¸îì«¿µûæ'ŽËÞôòîZ—l)")9=œ˜?qr™§þû«œžš)¢öÇWcü›Ÿíعã|úé¼óí£Ý»¹÷õûÞ±¦Ý¤PËê1ʯÑp×Î�÷¸è"º²¯üë÷¬B�…“‡<|Œ„Çä½ï�uIêÞ—Þç /Ó�¼tÜÆ³HWs’sS�†Æìš�ë숀I}…· 2ÕñˆÇúŽ‚:xm(i¦wÿnÉ×~hõüøñÛÆ­&÷K~ø¯ºkÿnlƒ“ŸúÎîë/ì¾|ªû®u¿¾v?éÐLêÈ:Ròÿ{W÷ä/ï¾bº{Ì¥Ý+~r#ðÎíŠëÞ’1J¥m O)sýdšûÍý-Ò$�ïyñ½.¿ürúz¼Ã·0y\þ É« ÏcÎÖŒ �ù™ò‰ˆN #po¾ƒœ[¢œÆ&-¾jñ1­Óæsެ†åÝ/�^{Ì}ìƒ+þîÂ�ïy�~ÈXYÿ?鯌ž«¨­YJµõ`[žsëAÆ?+d¦1DzYÖÉm»ôÇ—¹Þ'-à«\뛿Ð�Üʤ§ÃË$È58LTˆG|ªÌ½DÎȧRkêĤ�H3ËN¯ ùa€4 áŠõ—.w›ƒ+œÚ 9wûȸ9QÑr°,ÿ¦üƒÐ€s0Y‡?ìø3%‡”à!åƒC,ÁÓœfz¿dÍŒ‚EŸ‘L¦ dê¢PÉjxQ—î-ÂmR@ÆA¤(RFÄZ @ö�eIªM´hŽŒ®EŠˆ‘*Tü³Nº!`N cÜ9+î|2y?¡n$z–h˜43øˆøúxNÓEÜÉ‘‘bBŒzÛôI!`�¬‹Tæ'{È�'RšÀÒ�)E‘…Ž´ZžÑ·¶än)QÉVUHF €Dš·cI>µÈ ýž—?¨Ë�ÆWÐÎÉk˜F­¢ƒ°A'œYE  y8 š‚5»ÏŒR$†#º‚ߦÌgi_ Î) šáÝ(ñ—#öGi�øØƒÍ8ä[ 5øôt�”ilVXö¥*èÁ¨ÞC mðͧd=÷�Dc-¶À‡†«Fv°ù'�|ÓJ² ÇvB­~«ƒ‘%½/}åA1’S‰m´T�¦H½º§p„s¬ÃˆÖ¦OC ¥™Ôƒ®ÖÂ…¨Í/òãl�³e±Z¨¾e祙Jä :iþŠŽRÑöºº‹kµï¢$ô¯Ñ M-ÈÞVÙþ5¥eLÄ®þiû+&\ÇÇÇÿ¦{ÑSëÔOr1¿öÞî <.ÙüÌ[k„nƒKöŽÖé©ã'xi�iL–©›d�Ï5ûÒGŽÎºîí¯ïFï‘y·1«ò¿Ç`÷»¢»÷åãË;zöÕß2nñß¯ìØ ”£rd5|,xë««ÜÏoü.Øé�§ÿÂ8¡ä•ß»ºQŽm�$8 æOÌÏsë#ÚÈäà°Í\U>²æ]«CVg¿.¾:=ýÛ©ýíWô¹Ë‚ã¦ËɇMÞ‹ÊFÏùróÙÜ(+wh÷{¯º*w¨zçyÝ#;nñ«Ï)o�]±�rœâÆØ×¾h ¶öÌŸäâç6‹¯0B~†Âæ¸ë«oºe[7¹ñ�”†·z|ÝwÔ)[ÞŽ=:ÞøvÙÖ¼êá%Oëç¦Õ†«'ÿpu‡ëàm¤ŸwÙjÍf'NÁ5ÓÁÖ³�\ÕCþñ›º·mÜjôN’¾vrãÆ ºîáß:®yõñþîss³¤h±ÿ¾–ÛŠíùcÈÇ|ßêûOÆ…ëÏ0Þ>(X_³öÚé�‰Ÿ_91þ_µø*á Æÿ9°t{G·rìÁ�Xkò^Ý÷“WáIØÎj'ÄfþÊ ®­�üØÚz0^µ­Ï‘õ ¹÷ð”O3—à’•m˜çÊÜ¡�eH¶R±§Ÿ0�ín̉X;ÎC›O$`B"G•¹X¶%+?£CήIúpSŸÚG¿ªwr à8ïë‚?ÄÙ?L¾®ÌÉvôÏfAÒY£@×E‚›ðX:å QÈ:iQ^ÜIU~2©ÃL2 ^0­QgêZæ¦ÌPÑFº`ô¹»¾A°r{Ö‚E¾i™ox‰ùj=C«)@ÏÜ4<1 ý¥LqŸ«[ ø Où]$bO—€”>0cItͧŦ%�° w:NEÉêEåp-69O†ùÁaAeljÏŒ‚&éeê9€ƒ®TEÑÉ„DoÞék>².¨ô(µEÛ^ÚÔ�™mÍå|˜ÌqHÂîÇ~µQwÕ»ºWþt·¸0.©3ö þÄ·� ÙFúè'Œ/79à Æ9¾+¸‘ùž= ›._4Þ…·¦õo¾t| üù÷_nvÆ‹SV·ûóSßѸq=ì­»ŸYÛÑß4±�s=ôêu”Õ«MNð=F Nþ-¾ZÕQ‹¯>փƼyÍ,÷œ>dâfðôóî·½ŸÏ3Xuq÷;ë%z½nãä¶d lëAc³,ÚzЕ¢Úp¹xîä¯|()r¦¸ª€£ÖÓæ0Œôèá¤*Ðü@² YÌû€/s@ùuiÀ]L"ëDTâ-�ËÓ<ØF–ô¦±p#þÐpâUÌ%-ð;×TÇs%C€‰·�"Q|� · ©~"xS‚åÓ¯ÔÉOù.D®„©$¡©"çAap1¤+'‚ÞÊHÌ ' æçÐZ°Ç VB¥ ‚‘�Ê8à*ë¦No›,¦Ìš÷ó´Lâ�ö�< H’ RªœJ³€n>z*ÂøbÛÇì½~€ú 87‡oàP³l(µädÜî2 •Xi‹ËT¦, zÀ‹f“<Ô° äHYá <´G}€çTæë(ßåËÌp£7�BŠKù‚ÃðÃÙŒ]2âQ*°D3´CI˜& .s¯j¬FÌÉ«be 5‘Aô�™iðÁjE*F;”%me'ŒÈÑ})M?“ÒåLáüo—º}L¡c)œÀ­¤‰ð²jÌïÒ§–Ï;|dÊŒYT 4% ñû™m}ÙÚ¨4v"­ä©Í�R"öõSÔt·u61ÙCV@ Ï6ÃåNû+‡¥ãÎã/ÿÐ[)'�ç>iòjÓóŸ~UGv`õ¸êOºŸ›Èã|í·¯Þ›É³ÞNœ<>˜ð#ü=“Žß¶´Ì}©âà>¸ŸøÅî)�èññüø'^Ñý§—w~˜»ðØiõžßë~ñ™ã¤…“ùšU6Îü„GŒýð »Wütß‚›C?zu÷ƒ?Û}Ñ?ï¶ïîö}ÚâoN$YØ™uÅú¥ÔÆÔ¾ù{:î`ý‹ßïkyí)i—‡~—<ÞÇà-q×ÑÂüô"�OÃý1̯P�Ù?Mëƒõ‹ãøJŠ�v’ÜäxñwHqê±p¢ãFÎS�'>½{ÄDz€Ï»o÷°oêþrtGꟽ­{Æcº¯yB÷/mvé½oï^÷¢ñ{$NEx§K¸QƒyW¸ÞõÁNÌ'?ǧ­ñ6Õ¿}_÷ ?æ-Æ›‰W2‘XË<ÚO†Ä¨Ó'™àEQ! O·Ès€Ÿ„oÙÿäCÍûýEй�-o‡¹à‚ã<­�¹Ïr’P¿óÊñ+^ûߺ«þØ—lÜï!f¾xoÛ`ïÞäËXÉv G»É õFŸ0Æ‹ƒû}gìzûÖïë^ÿ’�ylB|Êóº~™#‹îà¹x¿õ²5/«}â36ÂzJol`¬½ —QA¾ãݳ^áp¾ä Ì?~ä}ÝÏ?}œˆ[ÞN¹ýü¼}�áÀíÆu‹‡^Å  -¾jñ•#9Ák‚ÑýZ 3žQ¢Ž�˜Œ`ùô+uP[â(  ŒlÍïÞ¥ëA–Müü\¾ð¤gÍ}hݯ5SäÖyåÎÛžgý)jzAIa¿­í]üm=ˆ&ÚzPc8'óWSÙš¤³bÝ’Œ�ÁÏdsAlB ‡t\áÙ²øŽks OÚ¥–þ6¨T’îMoaµËçøG^9À*‘“ä.D #ù¤lû!¼ÔÒ„û#ÝÁ¤e4×]ŠUI#~«åU9Šç%0ÈÕ ¦5œÒ‡¼$Ìù(�–û û˜•°¤&kÄl.C©¢Þһ⠡*]uì¶·LÐLýÞ� ‹TñÏh×oÃ5)ó)ªòÊï�B¹�¦2 Á Ù&éA8ÜåMPìó@Ù?C¾3“½tæöCÙB ÛæÌXÌj™òR® ²¿�†§àƒ¾Í«Æ·—Pƒ+ºÑ¦± 1º¨õ"ü¡½àÀ¬á< }¯F��ðô¼Ä4bQ6ã*ü¡z±¸ŠZĆKÁ}Y:A‰I5È\‰}‹9h�ŠŠ8t?¬‰pÚºjåãƒK™w7\ö—ÁÊD„±˜ê¤Œ¸!7VG+!”!]uÑü2*ÔvÌÙÙÊÞŽé$ù,GŽ“Œr§}kSjMÛRaZý‚†2m*ÿ€ÆHT ;øÐ …¥ûÔØŒ‘Q(háÀ{„�¢b{pC‡cEfN‡Q,\JU”ü¢àÚNôé¨h-‚ªã0íp£¶xÖì;¸i— ô£¤¿ÒmßEɶmÛ¼‡q �0‘ñ%°úŽØ¨á4ñ~WC�/£^{v†}G£/}D÷´¯mœ«ó#ãÔ$Wsˆ@Ÿ½¢gþ’)¶Õü×ÿýƒŽ¿3?üFŒ‰Dp‘5‹õ2‘ “žkbî9]êSoàÂvÿubÏ%¼²–„oaG�cã¯ô弿ës¥Ÿòðq"˜�zOû†j²Áç·ÿP÷]?¶Aù)Eð|ÛmÇxÈ]L¡3ÑFF{53þ�wwxô)�F<ÖýÛ'ö„ŽŠ7þf»ßü†[öê`WÝ•OÚ’Rž ÷¿´iíÚ FrÂʵ¥\%Ȉb>Ó%¶øªÅWD5ZË9½ìQý®\Û' 6ë?…KïdzlÎŒldDŒDBÁÈ{†$‹Jåp1ÎòŸ¼›IŸ¸Æ'ÏÁKIËKͨʥ>§ìpé,jÓR¡Ü™_‹uhá�z+]á'‰j¢0sPc²ÿ-3ËÇ_RIKÐD$j³91¨@¹²pú„NVÀ#WꂯT€‚$L)2Ëœ;}«tX¥ª"awÿHëÉV$¢ ªR0EɹEi(Va…9—€€ÃMQ°Ÿ?ÓUðªˆÕšÒ\¨ÃbSê"RjûP $æ/ñéÊ|¨GW¶A�²¢<âP­½–£NaèM´cÖúR¤«ý3Ì£þ¸TÝ"I]ûA#’Q¿dEÆü¦J £OÊÜ. =@T7‡/´'jXÒ,õ/âr“¨/kS(ÀU Håv…}\±NÐÉ¿¢¢8ŠŸÍý 5²©4š %ô‚Þ1®T?F£þe;¼ˆ‘6l[£‡}7‡G€T�F7j 4&¨ªÕ>’-Ðç Š=2+„£Èù;:…Š'šWPñémÂlK¥í@&‹øÂ·èé/z„aUNbÇùæŠËHö=…w‡°V*p83ï™–½ XêÒi ã (TT{…Ë£T •4›¿²Wõg1(?Mº6Õü£-c嬸+Ýèø‡ŽÎ>kߺ{o±ÑQàØ{›)š›-ýóUŸ£ã§^ÕýäÚ'»�jÆßÜ+÷’ßíðeã’;}ÆÍƒ¿~u·î¦ËS±‘zøùÿÓ±WèÌ�{}~÷ìWŒÁÉ(½üÙuIGò¾v6ñ¶Ó…Åù¥åEæ/g& ûX§X‡ºÚ8¾šè¾1�3>C�ìw{é[º�w*p,ðZ6$ÖÁ´ÛUÈÏýNÇCâÎîqÑ%Ýë®ZsÓñ©øŸ÷º5/ÇX q¡¾Íâ+j˜€f¦DüöéÏŸuŒžé6Fö„§2#ÕåmÇfgçn;æÙ¸Õ?»¢{Óß­yhÚ¸ýÚ³oûþ¼ŠpmÝ)WSSGŽåÖì¾û|Ù[7y¢ùwÿh÷+ïªÜîDéæ§D ¿ôŽî?n”{]×è_=©{íûÎ3óW‚”uXÖ_5P=WˆdtÑâ«Lÿ-¾:×׃3Sìåž9ñàG.ñþëµ3žw•gëªa ×Öƒm=ØÖƒŸKëÁi3W$ˆH²¶Ï=ëaS3YgòóÅõNƒVõ[s]ø³ŒfHrL¨‚�â&uVòyYª�ylLB"é×ß6”&“Áö˜HT$g„ÞÆ=ÌÐnššZ0W«fâ(MÜ 1÷–y™B®å™?npG²pòæú^ÖÈÚ>«©r`l”µ’OdËee \åJ¤0¶!jŠÈR èeU¦Ä™gmÙ9¬*ƒÙЦEs,ɦ? ©2�ÿ §å5[“ ä47½ÊŸ¹)©R²]`T)€Ä´¾:˜¨°àJáRU°GÜïY/¤íþ9�I‘Ót²_Ê©Ž".�¸0kê?uTÒ÷ôYL'PRóÈÖ¼Ò¾%öYegÀ¤Vy¬ŠV‚èLÃòqªü¹;ÌÖ<+%iE9„GUÖ#§ùJY°~ó€[úð‡roS-U†g9”ߺѡµ(�É.u¶ û ޼Èî2C@q˜Ô"e¥ÚÛÝç¾Ó h")£iÉdÌ„/R_v5 º¬ ™”5]GKhT5,Óˆ„¢¢'mHìv¾Â€£O3‘¥/1U8ÌÉ®&i3ìO-YÓ vΤ aÄ¥˜¢.μÁQRJ£Š<ÅS‘œ Ýê­²@‘ª¤… º’%?ÛS%�f&Í ÕåÄÞªjYåÚÒ¿æ¯2(݇ˆÎ¢»ûUùéKz#VÔüU 4¶SÖœëæ¯ÎÈ_a]ãcÿuãó³x6·µ�9øãèùêý‘Î ®¯›!ËÀ3ÏÊi gÜFúæ�v“ï@˜äê{žÑ½õSÝ£þõdY·mÇøò4�™Ÿ|‡Àj²dF~ù'}ÈRýçWv¿õ¡î¾\m±þd-9¬qíãÿCÇF¤ÕãÍ¿ê‚9xu®ïäÉó <ÿÎGÍg°3ÖÏ(¾ZEéÉäKÖT¬½à)`zh÷ø§tÏ}÷ö=>­l”Iı:t �3ë0 ’›ÀŸz<úqöΣ¿ú(´nç§Bõ%»×nK̦¿¾jâú ïÕý�?ïžõò ’Md?_ü掽Z»7&W&äí¬�Ý ¾Bç3ƒ�qÅÉùöDÑwSN£‡š‘ŽÌݦ‡gg‡üZG±ùm È Ë/{[÷Üÿ¹æ…¡ãê®ûšÇw¯ÿëî9¯ö… ëŽm£7«®-‡i:âð‘#c�@…£zp×�¼ð Ý3~ñt7´NtÄ3\=éYÝ>¼iþÏûõÕ;ÇÇmád÷D&zëöIÖ/LŒÎ':�¾ˆ¨gš´øªÅW˜ÃçÈz1t5>QœØxdü£ïÝo{5�td¦XäG'c=ÿoè¯hÑÖƒ¸týI[¶õ [LŒ&Y¹b­±Å»û實–˜Ë™/Mçðí’×0GþqtÎ~,Ňûƒ´9�…PÙf€Ö32²‘3Ëi�Ç©§f¸û•ø *ŽÁ5�sËÒÑ4EÅÝN+6!Éâ"NH)à Vº&_‰ÊÉ•T™A�žì�ÃÚ¸0yZ&!A2Æ*÷P‹Ñ¢ˆ˜2�ĵÐ7á`:"̺¥)Õ&PLG’ô)pÅd$Û(Á,VÂ9×ÀÐMÏ'AY�ÍØR¿�åT ¿ð“{¡ÐÄÊ(+A&Â,Š<+‘ŠPO6•škk¾¤b=¡ýÚ”êvBX×Ü…Th[Š£Ú4¥²“Œ)�ÒÄç�q ”UQÍŠ¬ÚèxSmðÛ>œð)1ò—%Ô´%V Á/»2ˆtµ›Ñ7x~²ë*“Ÿ1al5&ÍÅ•h"û¢ê·H¨­²BßÅ‘t#è�֜ÀÃO +¹HéJqU_È„ÌÀ”} …üKþÂN‘,üÚ_ê^¤3H�••…i÷�Gj©D ŽLªC‘%J…O�CòP”£Õl?£Æ=â_V&®ÅíðRIV( œ�?h(¤Ô¤R™ã%b§ÒÒNÅåˆgG OS[gs!5ö*è4so«ÆB‡icçH H: A&�Šn-hbDr¬°êUÔæ"Ùsè¨eF¼´àÜÄ¡±šŸ¤…èÄ—¦´ð¾v*Ó“½jú—Ë3MdŠ$¬ê,§€˜ö…]¬”Ò({R‘Ÿ6¡£BE"'Úü•�¥ÉØü×Hš¿jþÊñ¡EÄ_e¦sèÝ7ëíÛ³çÈÑ£»wïÚ¾cÇ–­[I~ð�³ÌÒ>óÌg…ÎÌlÙ²÷¿†¼ážk;î Üû©ŽÄe÷ï.¹¼›�[s§.PűcÇxt×úÖ óݧ?Ú]ó·ÝáC¾hâ󿨻øÒü®²pÃkÐädAðd4²cgõ€Ò[;w®MEñ 3úýúOt÷t—Þ¯û⇮& 7$~Ë­·ì¹ñFx<ï¼ÝÛwìÜ27;`C_â'g/g&…`ªr~c§áÔÔÞ šÊ qRÈ6·ë>}Èwìܹ}ÛV¬vÇŽ]¼ow½–�?é;y¯ý;Jˆ¢îuYwÉ}×ñ\ÉÁõF>AUìß¿Šç_pþ=/¾çÎku$·x_û÷Ý Ÿ�É» .îx-�ç»#B±éñ×4b¬1ÊÐùžk»Ýv÷¼´ûüûu¼0d‽ãÇOlÛÖ§Ô'jƧ ö®¿~×®ÝÝã¢õ$ÆPî:Ü·¯°wçÎ];wï'º�™áuOÞRBWµø [5ªñhñá~¼ž‘1´jÉÈÅ$ï¦ëA<Îòâ2ôœùë?š{æÚŠºîª_y?Óç…Âs[¶2 g•2é¯ ùï’¡­Ûz°­Œˆ»ÿz�™Ì‡Ë3˜ñ[™n�rs©l,s±ÌÈN�YWShM�ÆÏEbWÃ.ÝMø€à%ÃF ÂŽ´”:žæ"ó*n‡I>E•…ƒ,ÝÙÁ„!±×ˆ¤dvñm3¡‚'kGN!’&ÙÊeÝ �tƒ[¥­�XUpxIIÑÝõ'd²ªÁjH¦/ áˆ·(5Úà©7@�}�²Â3êD¯€ñúòäóæÙå5çîœRN¹ÙV. .zµæ&î†Ä¨,�êOcG"MZÅÕ+Ãv—ü³,—vTèÁ&ŠÌM”ÌÚpÁ?ÿ”Oþû• äyøYu5¨i' Eˆˆ›½w"‰Tü—R£ºÝI(“¤è¨¯¢œ§öd÷¤@‘a×|HÀÂQ•i• ‡ ^ ¤¹ÞnÔ“¥3@À¥ý¥ôR)žh¨Š4?Ka‡r$F!è"NóÈ?®”>¥ |»Å"ÄÛB©†„{~O³±Õiáº�æ¾ü—Þ¡ÐÒÅaŒô¹®ô¡&♊Œ„ᎇ@�Ð@Ôa,b‰B)ŠEh zùêIHÙ þGŠh2Í)‡%AimQEk1…ù•�F&KÔQd¡+^�Àf|k¡Í©åƒÜpÒjiÀ�ÐÙÅ–Ä–rEa—4Hš 4ÖˆÆÜYDIfdþn@ÚTRáÈ6´ ’^HÙ@Ü΀€¥°Òüæ5Ä›¿ÂP›¿Â"­gÃ_á¡O“ÈÀRW;Éùiˆm^…Ú8pÞ='�Ÿ`^ضuë^`Òjõ`ëâÿ¥Ÿµƒ~?pð Û@Î?ï¼1xà•ü�Á�*Nž8yÛɓ۷�QšQ�L4N/º ¾3:R'"þBË ØÿÌÒòÄßN!Ú£$Œ�²åmvnnv8œcïÛÉùyÚ³ŽìÕšd„Ok94ÙÓMÈHŠyMÉ `踂êܶ!1ÄöíÛ&ê;“züå}kÊ'.ÈŽmݺ) „:røÈ-‡]|¯{­k¼`�¿MlSÁXA~Ãÿù“ód�å4C€q$À}AøèzØâ+=A‹¯˜½eZCØãYêx°ì©©ãW<êÔ‹>ÿš÷ïùâGlõ ×+K ®/ÙŒ«¿ŠŒ«þª¦ÖBm=ØÖƒXæ ÁC8›áÌc3Ž“,­Úz°üãÉ•hœ‡*J|å¥É�ÓÄW@º`> ù«™Ÿ¹òJ€z†4�¯±‡½d/ree|œ¿*pBn!9v¹t¬T¢«t=@< a31 Ž¥â?⃕¥4sjQMê‹°ÉUñBkÒ* IÝOD÷ž`ê$"0ó«T6ª’Jì§ÐÓ‚¢[ž©?þ‡`®Èuàü0mä• $§-= S¥•—C ]®ÚFl¦a¡÷—4x¢$Ó«� ®kßqÒ�­Q†T¹•M‚@öì%°I L›b¦eú ÖD ¥Ê¨eÛ•RJô’æ/XQ²Ô‘�Etê/8Sj½6Ùü•šP1Í_5UcpÌ3:°Œ;á¯XÞÓø¬dßX“;~cŸŽçµ‡É�ƒÞ|3i�­[Ø.4†CÒRzhǾb°šZ\:vò¨ì4+öUÜ•8tèX×dV!@¾ W”såþ}ûn¹å?ã‘XaÃÍD»�OIìÛ·—½rä7ÔÛí�“ÇŽÏ‘_ÙÓ”É#O,1úÃȘŽj–ŠÍa´±ÛÓHJ•=hGŽݾ�œÞ¶-ìLœž�Îðœ&Œ�Ï»´ ünÿ€Ã=ûHá±inÃÁ‚Š0È7íÇË3T Á—Vè—3¤Â޳={nävUHl&|àÀM{÷î!2æ 3×·;¶gÏõ×~ꓘ#ŽnÚPfûMH°0;œcçáì솒2xÝÌVËCNvØìœ>�»†|в¦Ýû†C‹¯ô¬#Ëmñ¿Çg¸û¯YÀ2»q°%bpõ{¦×>Õaéü‹où¢/g€�ÍŸböqÿ˜{bâ5þŠÞ×tš‚a²3V4.q XbSfÒ¶D.QÚzp_a®îÚz�ÁtšøJ5qœ…üÕÌs¯üš»ff\fñì(f}_i 3Ù1Ü­çœéšqÇÛÀMt 4g o z)dym¶,™ié Ü{f¢ÊV@WÆ!n$2Š�‰�“?€*½Ö/ð¡)/¶#…¤Û©Ð8rJ’µ €ñ‹õbã?TÍa‡…U,Wò¨È)€1Yá´g® *íµRË} 8ÊÊAõ¬&ƒ…�µ2m�FCê­µøãBS&eQB]J/ò)5>à "¶ið ´ÀJ£`Šä•ˆû° è…¾•bƒË§´ÊÿðqÉ©Y/‹&Qt,V.qÀ¹Ò ¢Nõìa;ÙM¢68´‹“óRµ`){݆Sulr”ë@«@Éq¥z•0€pB…ïbà<ÌKÚüUt®9zâW:6k"d°�@amýi�ê�k;ÙF±žPâ’6âUœZê'›EÑ«²*ùìâ… ÷! XÝ$¤0iž&È*l,6 J°xÃêƒ(DÊøÅ>B5Úþf3áªé ·™ñCVxZ…€Cj0U†äX¼,«+£‘°QƒE&©@&TBF‘À&þBì‰8iÑüªQ]Í_õ†Sfæpó¿Æ�vj 4¥F6÷WónD[àfO4v'’ N| ¯}Ü{#«ý#Cn€Ìó˪ŠOØfözlMR„û*Ý æJ׬!Ósz0Ǽÿx��´'+f¸ÒíœrP{ë-·\ÃuŸøø'Ž>ì}jÉæM“qmà+›u\±ÝŒœ×�7Üpô¶cÌÑ£·±Gçu O¡f»xÜ|ðÓŸþôu×]wòäñÊYl„öà�7î�š�2ÁË“×X—4)��ÐÙ·w/ÒoÙ:œÛº}޼ÃÌ€»;ºi|{ñþ—DZ_bi‰­[Щq «·{ >É—ƒ^ûÉO^ãõ 1!Ê-¯Ü%Ë][æP�Þ£n6T] Ÿ“té¾wß>š õd_Àå-‡nI/Mì§ë:257í¿‰ÜÖH_$zü(C$§ÚdÑEÀutÁ¼çÆ=·¾ö‘… j00oÊ` ÊüåÔÛÇWþ~ÆL‹’Ù¥� /-ohiæÝnò@3$[·o݆ñúÄ·DeË‹KG�›Ÿ?I/Àù†Šª¾)·k®¹†41þ‰=aØ�Ce­±�ŠŒØ�{®_˜?¹u+™>òQ³°Î&Jªx,;ð“‚Ov4š9vô()¯OêÚ}{÷;rÈ"±®#n½õÖ÷ì!;F‘‘vâäqö]btè{r¹cíÖ[÷îÙûñ�ìБ›Q*/¾†À$?ý`'ó·çF%6¸G×€®Í¥’D«$[yÐÕv�Žt¼Ì2Œ�Ö'p´ø VÔ⫃ŒYãU´�ý� ëÁr9²Mä|ñ%ƒw¾quXq‚û¾ò1xü%)p¼•Žã?Å_ÈÇ_QßÖƒ† m=¨M¸¶ŠHaN.œÚz°t�JXüãæ¯æ—H9œ]¾šÌ3ìÈÖ ~N[ô £D'&/\›=áî2"9½E¿ Îð¥ïa]GÐX€7O¾'çÁ¥Ó„{e¼uO#pÁlÐÃo‹q—>�‰´„wšŸ0'ç~5¢£ú�Sž€Å_9ûŽš³£D¨“e7>™“ù••¡)9àNÏVj)kô<—Þ)ßå©8³Ø—ˆŒsðºžÅºöS:pæ�}þG/X¾hb2"™°G<~:•2Á<ʣ…à4å0§»TõÆ7ârtð2€«FW$Œ`*ϯ½=Ê)Ö¬Jcu! ¨ŽHÝJ“I:d‰-º¥ˆMŠ™N$6DÇÞ†�”B¦65)²ÌÚ—P×ok›Q0¤¢JÅ \~Ð w�aO’ì{•¼L„T’VÚ0šX¤4ê ²¤€œ¶H­ ‰|åp:³º Ρbž T¤…�ÕLÁ›¶y£Ö,3b¥^âÌðÒÐ<Øt™g-KMfP^l©M͘eD«4÷_HÓ»£ÂMû€†´&9­<>¡„Ñ£z¼äK g&—|Š•Ð_¢G I‰Ÿ»ÈY˜JD^MÊ%¶Ã3j‘e#¨¡ìÈ�½�[�çc-±´eˆòYC£cׄ,P¦±ÊŠåÜe°4 »(Ë€Tš§%-mBÞæ‘EAAÓ«ƒ/ #¯…°î9{^Ó2àúŦ2uPßÍ_¡TSƯÎÒöYóWnìe8h>Í_¡ƒ;䯖¸áñ7‚?zì( /2¤[xïZÜœž‡a8ypUÎcÔ9Ló­§syí„óâ© þ̯Æ…1~YTs·©7¬¶eûÖ-IÑQªã‹¿ZáÍ9K>í‘�xÇaƒ'Ùßvâøüq(;½ÿt膷Å-óNòûo:°8?Ï�ì¬×ùÃS2xQ��¬æÙռͼáOO“-³ú=˜ã°oj–­v€·‡àdœxÜXí¡ ¤ŸHT‘=Ü¿ÿ&„d}DŠ€„a¾yqy‘,•³�êoÇ^Ÿ¹Y3iÃY~æ�³TóÜ~Çýw>PÛ7ÙÙžÌÈì,ϼ#[äŽ*öý gk.Sµ*Z§ºa|…©“qãƒyOº�@b‘?”˜YÕé$ü1>œ1ô:X]{z Ì ’´$ï1Ë=8‡žŸÁÆ5îäÕ«'�ß¹}ò°;Êœ»HÉ�\8rôÈÞ}ä Ë´þ¤ƒÒFGtmH•;G8µ¨¡ðâ„mWP¬*8\‚ØIuÞþ·´{×.vNQ¿ì/Ã"Hí¾õ04Tú‰DÔÀ‡å©Ìí;Üm8çÖ9çŽD�NXkã+ú ÞWóÞ|œlämØýD±±»Ä³"¦7Á·ƒÌÛöí¨Â®Ÿš"ëÙ-ÚöÄÉdÖÈÝž·kçÖÔzS†Ñ‰bpäØQò¿Œ¬ÞÞ†ˆé�jh2º4ÅÙqË™ójžapla°lÝ&Ù9·Œ¡BnL¦§ÁP‰ERTš\)*k¿õÖàPÿ0Qp¥>ïQä&È„ÄS£<ô{¬tÖNY^Æ<·ÎnÙ¾c;5Œøç•ÃÜWË]9Œ™QÛ$óx4}D0bnx ¬nü bQ 'gˆ¤@gée >>Èœg’�ev«)‰øÛw�×t£"\C8ƒ ÑdVê!Z|Õâ«ò]„‰Æ=úÚsc=hػ̔´0ó�?�}Æc1èÉãý/ß®]»xÖáŽí¦õý:Hcàõþб ÃlëÁ¬#œnÚz°­ïÞù+"\óU øòXËS‹õS¦n˜«[8Me(EèeÔ–&á¾�S¡hª…ù…ÊÊ�êÈcà %£LDe¾Èªœ€Ó„œ<$bð3ÏÍOžD´��q•19e”€ƒ J�‚¹&Ï@Š/dG 29sbVJx•O)]ª îËF~rY'p)ÓÖˆ…zU$$ä }b=Á³Ë*£RØv¦„ «©€>Y Òƒ /”á”5‰XC]`Ii§p%®üØL4ßÃd|ßjx2¶Vƒ2a\Þ¨�$ÓŠŽ’ÿàà”^4C¼:…ÉS%±EßxZÊ¢B\¶JÆŠÓh”\$à̦gd9¼¦8³åþË)å¿|ˆDÎ-âÐ6øÂLPÔ Ò0t™¢r.¤e¯ˆÆ§·I4)0Åesª=åöL•œª¨HüüsáƒÝÁ>aC]‡MËh/È£t$5þ«³ ¡VHL�3#!òUä„„cÍeå3ÖÆà¸’ø ¼hÙø2pÀ‚ �‘ÁšL˜ßJ0 A0Ù QåÉtÃð¥û€E *º�ÛèÎþ…+ Rõ"tžvH�4I½Û¢¤¹kdíMÛ R’ðƒ�mÅ–„ P$Éç+¶ëÀ){`„‚¬€7j oÑV2$g ‰Oq„Tr±h¸¥}4…>£Ë毴Jì¡ù«³î¯˜·Xý2fØnš ¹ŠöÍ(d]BF·C_‹ÔYΩþDÏâ³=éNG?;~Æ* ³ÛYh“�Kê�Ì@h'v›áEÌí,Mm›�šwý=œ°Ù‡·� d‹iB_Åë†$W„™�™å÷ZNM»Œdù¹òT<;RàÌö“]xÓwš"Êž ’îb"ÝÀ`(ðDì…En~$ gO7’SS¬�L.û�Dzaf-`�%€bZSd{€™%AAºŠ KGfn‰\ÕܬyÓeL1$‘1m>’ìI·Yd™á-f_ r"eyÕÍâ«ÌÉèÁ-[+[Ñàìì¹C(Ð}ɺùë]õ€]FGÚ}2‰6+eŽɼ Épq‹&4šihefq™[p‡3¾|–ÌcvØ™^QLþ-wdQÍ0¥;(Ô¡£WišîÒÓ{8iøE<ÔÁ<¦Å¥3×2C²†äÛ ´–@‡,¤þ¹5éTÒ5Î äFµËþ×­O¬&ï°…!©L4ª ò�Ì+Ú˜1|À´Ü "ºý+ ºcáäÁ›tØALX ?Ša ¹Ê¿DÌžúÇ;(i ¸è‡CpZDŠJù@Ö ?­£‚URi"„Ëÿxu¿Ì>TL™V•ƒØØÏ¼‚*4A Ï OˆgEˆX¦ô,R�6hÀ)禌c!ÅÐ�©‘å„=ó’‘®�Ê%}ǗĨ§ :�Œ2" —œ¨Û�OÔ©7Mf0õ¢²­sRŒf…E6ªÈ¨x™[~”0VàÌ@¹¼JÐí…‚’"X#íH#ïO¼ð…-´Û¥\„)¦ Y4)Éb&QVßt€ì1Ï”Î8¶·Ì’ ÓƒVޤ ~#�Úˆé&U¥â¨�¢(CÑ ÍäHuaw«A�"“7WH0dó›Ú~éS�»7Εz´!î‰II:+/6*7%)€‘1ÚªØ*5ÁizH…eÈf9N>ΫìJ”Û ¨V[ê¶‘—*ãg�/2ȱöÃáïò¶¨MTûÎ� «§lÅÁ4‘WùZÁ@n �íÔ7jqW[Ì…•J\L¸à"dB4ý©–Dr�Š Öe(vú毚¿jþ*2#ðìù+^lÌânÜ,³È�†®Áqx>kÒA� ˆ#H8 u�s&R*ú�Ä6)d 'ΉgJúNo¡ëá?)�é3L[x¢™2Ú%Ëäjˆø+Ðê‚x玫ðºgvaŽe ©2YÞÀIr&\¸K?ˆ?[1õFrKoê´Ä§).t§8pŸ�4…W¶4‘Op‡;º¨ž[4ãBÍŒ?@IDATù–C”ŠC¿e*ÇD 8!©†ï3‘†Ü¢U&^ÒJr†LŒû·È�èë–f—HŠÍƒ äf5! �,›c ©óH�"pzg_Á‘>´hµ¦¤öèí™p…ÁxÝMÏÒCK CµE¦nA=Ó3ÁŒ:L·�Ý(˜‰�–·dš}îÓçÁ©ù’øôSþ}h³ ‰·:è°_Ñ¥ZuâpiñU9wÌ3 ¹[¯uq8DçËW|Õôß¼··í|�÷¶×Üxÿ‡×¸ÐU[oê¯Òÿñ`ÂpÕÖƒºX=T[:½ÕtßÖƒ ¦#B\'S˜åÔ:ù â+'–íŸqþ*÷‚Ì‘7¨ã´džøÏŒg•|úó}¢Hïx¿s� ƒ2AÌ)XK+ÁøçRÜ›È �R)E"mC j¢É‘‹©})¸¦Op%t(Ò„ƒÜŽŒÙÞº“4s:(ØŸE5 åʇDÄ‚ ¡D–&¨WµsÌ´ M�„Oð<iç’‚âjlA�1 Zâ!�h… Å( “Q"Á¢¦üB›¥ ›®4¤ Zß‘jK®#Jwÿ¤•`•Kp €€€�{Q‚!}¦–£8‹$–0K2 :Ó-+ÌP#Á(§Â\‡TQƒýÅ™œ¥•Q;ïh¥±: &HxI i¸ÎrM¶ íåOìDuëÁì]WÒ€\–JéàK¨£,-Îó^Yâ³ÒÖ¸Àó\äâ°elš÷ià픜eF}ÖBã+:T©Q™¥½Xà‡pR¨wPM«˜­¦A J(É$¼¤ tÊÐÒ†«Œ"6vðM-Y`·~dKÚr¢NÜ*›�Tù§¸†î\sK\s€ù�Å„ *bò_–{¤(RÄâ1DdK&ÑÿªÂ¢J‘rŽ4ÅŽü(¨ÁÅ�ªj\v\Øht½xÅÃqLÌ";‚o{Æ<(çàNÏÙq`a\Àˆ.Ï3 œ&á<4”‰ÜR�!XÕüZD ˜°K5ÕüU|†Áø9Å_1œXÌãsXø.ÎL/±í ‡ÂFaÇšÐ1©¿bXR®ßqÖÐÝPÇ¡¿ 0ã‘ow¿asq™©DCc3æXµO ûÛ4Ö5þJBÝé-Ü39�á ©S zLhŠ“L„¾GA–�À¡0)!¹“4°�I w�ú ýØý±PÏè!ïh² ö@ÔÒòìÊ2X\$Ÿ°´Åà�Ü�ÚÀ_8@Éd 2]BÕmÕ¨…¨%·¸š5ËÃ:„{nMÄ_1² ‰ 0£ )ƒ�39‡U§¦ñÜT¢÷ÓÆWFK¹__8=3Ͻ ³P@?f~hK�8·‰‹I›¨ƒ_Âܧ§ATÆH¡ýƒ•™Î…76oÛdz@ì)‹&eMH"Ȉ€"÷ŠÂ¹Q¿äò@i/IÍYŠ^ã[Ž0yR~I‘nòýTZ¹8U¨%iDÏç¬fŽh/³=§z%COЇ‘Å htj¬BÔBþdB~h1ޝh�HI¥Îr›%/:] ›ý”ÝN› Bàé~v*‰Ì^Ûéô#9±¥å¹ì•4W¨�ixZL¡lt "M¼˜!ÌÍ µWiª¦kÍ1š5{f&TmÈ9]+¢mCõ²Õá î_&Ah…]‰´Q¶Tb0§¸±i€‰B!ÒkIÁÙª:xw©käš–D£%¹+x £iç]ñ.Ï2L`ÈT]â„0á¸"KI3ºú3‹¼ƒ}°43 (œKÜyœd"=î�§žÓýò[7öWèÁ�kXq»@åòÕ \·øªÅW ÍBsÂT4 LD“±ÔØ6SWZ©ƒQ3âŸÎÇA«§ÐÆi—qgc[Ã#xÏ8¾*' ßûì¹g>$«”òk™qw/Ã5SC8Ò‰BO¾ÚzP?ŸÞp®QyéR>Õ‘5Î9Ñ‹—hëÁ¶¼¬Ýõ†Uâ^âot)”0¥&Cäy-_çÚ°Ví:¼à2ú)Çè¯p:Z˜ÒbÝ™±n‰‰”OŒß)’a[³oPâ?¬8Nâá,r¼àìðp”Q—ƒF 4X¤¨Æ5Ú¯ ><�õ™ý!.ê(ë0� cÁæ%S*€ø_8ណJÒX iOÔþà3¸F%¹â ™¨Í7ç=ÝxuE�ÄþÚhÄ'C`W^î9ôÉ-i@[%1X±S€¡¥ ’Ö³Û7ò�‚ƒ¿XÑoH£_©añD/ ¥1?Ëî× ÆBÓâOþKÖÒ^%)’ ØÎ^…ØÌÍå –À‘)¸t�rPÀe˜ ÿ‚@œ”‡¸êØwdWÓ|™Þ£=ª .-l)㚟ÈÓ¦•B!W|:åLaLÑNÑd„‚ˆÁ7”FË (Š�Q�2y;ƒ0MáEÅ𣭙(ŒÎ©O–=¸³Ï°2bT±ÚÔTu”«#8V$0òOcá"(kä[nA½äH�…¡sN]èú倊�hïTi{ðžÖT )ʨ®zõ”ÄL=ÏŸ/K é™�”)•…Á#IXU‹T¦ßé¬À©ˆ|jÔŒ|Í„)®çá�ONµÂl‹ƒÅ1ìÿ/p¬2X$)Fšà‚I šå›óæ¯töêPI¨VavQóWvÚüÕ�÷WØWœ c‡°7€Õ¼ƒ�é^²X£1+xí'’9‰áê�yòéÀwµÏ ^�r0kùøZúˆU eò�/„®yc5¤WM[°ÌçvX¶Á y@élr[qxzE™ƒ)+Àä‰aNNa*~ŽO­dž’2ávXþË ð¸ª<À,^šyî'Ó û�æ¹åäPðWú5ÒàH†'„`’¼‡I&=°âÑÔˆú¡JHm¸c›>i”¡V[u8Àùçž;˜á¿é48/’«²63u*û”øÊ@uÀ�»ÎQ'�¯›‹/5 Ìs{5É'à$Ke�Ê£ŸL¬N9r÷¿ýù€ Õ7Åc|¹ UÅú‰ö¸yˆJ‡Û‹ªÒ^W´^Bm…$,@ëNµN3€˜3á%Y™qÓ uqh˲‡ž²]!ù*É$ßh7NXLv¢!‡åy~ƒ„‡%vW%¯„þ‘¤¦5“ w�×(A“ ÈNÍœˆBÜbF¹Ï¼ �ÄyT:A^•,ãÄ!Šá.¼WÌ�„Cí�;¶¢0jDdB ”&IÔÁ€?j��Ê­W�aIUHPüÚ»žnŠt4ã“À«S#4Ë[€áå©po?†Vt*Î I ¹¯Ôp†##Ħ{“× !¢Fº–ݽgªª1åWtsz|•*d�'ÿéßV¬(ºbgžÇnìQLzify–�¼¸2—‘Ÿ˜!‹XTÎê}˜hñU‹¯›†‚Ž"A,Špn¯ñ;݉?r.CgõcËÇ>xÙ5Wíÿ’G±%t–§0p˜7t|2ôÖû«ø%½|üšÎ @5¥sq΃7ʃd[¢ J•ù-çè'jŽúP"×NRêMskëA´”YDoŽþ˜j L1:êg‡¶ĸʼ6ôWLר�x§÷cNØNéZ––ßÍåÀÍ�Ó3¤]´ �ú6z*Ó´¶¢zF\ü^`¬Ó–”ÈACQ¬ºï5*($C-U°n¬ZÑ@Mû|–Xý˜ÐûÝÚ6u45 “ÆÆrn=#Í –ÖVp�\%D\~=Ï!Þ ÚJÉ?@ù¢FPHÇ+ª êDQgp9eTоÆêh¶jS)O"©¡š‘‰ÊáƒÂ$•>(¤}ÖYaB¥[¬rº4)\–Ø’aiÕˆ{;Ü•°œ£lÖ†VÅ*6rÆGÂj°¸ŽE#Š ÂÈ”±&q»YÐÏîÇŽ°È ,9X•žåC-4@F5a'koÊC ìÒGÍ_5…uhÍ_é6Ξ¿ÊŒäH$ÁÄ@Ä·àôŠŒ=R~°Za¡îÉ¢xc.WýÕtǸeL3ªÝÖÍ··¬e¶¦ˆÑ.ŽrCîÀB`F|hÚ¬H†[R‰› uî�'c!¬¾C/"•ðk{1¤a"„O¨ÓÀ\xˆ- ƒ06ú²¶'Ãæ0˜ϧ+SfZš^›™Ò”»ƒ�L­à‡ãŽ¤Ê‘‡õËGÂt<“{v¸œÁËÁ’ÙÐieU|…ü&NTÔây!%Í~,ÿ™;_¡5�ôLêUMË”Z!–Y 6(@Fã«Ð­yÁQ€²£>Ú$I™ì¡ªQV5î.65 -$UD17ñr­!æ¢Eóà8ö‘p+$mœôíHï!,å¤CPÙl±£›è£]›‹=Z¨ÒÝ…5Í�J>¾Ñ0xLœÊÓ!h±ÐÚ=Wñ›†AºŒp(Vuo/(íD|E©€çFI˜²…’ÌÓ“dŒ$"TÂÍx5¾#å2ZÒ©öuEáv(X€¯Žæ,dø€hÒUy´âp•©/ÛÝŸ q¸isy€ "WíÏCþì�Óž9�—‡4(*ˆø«‡8ÐÓèŒF@@è ó<ñ•99xe`pu…v„MÐvaFZÕ ·jÑ'ºq2㥤íG3Í­n‚ÚÆ=‰2€à£W�…r†_ŶÑ+êiñFå†UyÆxÁ¼µ3ǰ#KË¿˜•š»+Öƒx0Ý3léÁ›ùð_ÊÙè öéÞ¤ÎÃLË8P uÝÚÿzEà ‘@p‘¡�ÈȈ0Pâ¼­�t™ªÊƒo®øf�_ÅúœRÕm=¨Šâm1#çh´Sa›±Q[–¥0Öœ�Ð�¶£!Qà¼Ä˜Ë$§}„¥¸ž´2Ц¤,ÒY8�ãŽÐ05ªŸ€Áˆ¦ VxÈé«\o;?ì�Ò‡jiÍé)Ü,�hÓ’³Î…¾!`NÄÒóp¦fýŒ ¶H‘†K÷¦‹ŒF Å}A›ÿrŸ¨êž¹Ô„?Ñêf9 ¤²“:øÑž ’`™BÙÀG%šÀ¸©€rHž…CXæB¦õàK¢Vå¼aJúð%vÊ�9øJDq0*-ý~1"%èæ<•U{ YU—5(‹ëü@ Û„î=|9œ:f6 :ÂÝú“+j|–*0v¤"Ÿè‡Sª•ÈT‡:1Ê‘k9±Û¤,€1X ÖT„DänÝ»%£i6pÁ>§¢””�…Óàƒœ—èP˜t(�è[Õú¿ÚQ÷š0´N%½î©Šçðc…w„¤ifáN… ƒ»Âl:À•yƒB§#›¥û‚Âv¥´ûA,'¾ A2h£¤„6Àð Y{!òE@Ú"·˜mætõA›–yv¾‰2IÓØ1DÁÕ=¯Yä…ÍŠö3\hÂ�ð±¿%Ú¯‚‹_R>�{5”(”‘T~ó6ÜÕª3¬Ãƒ¢Ë‚) ÚèŒöy²ŸACgK¤ úH&ùO8#gÖ)w¡Loh ê8êíÁQ«³:†À»¶ªfö;XµÕÇ•ýE4¥v›¿ÒÈš¿úìú« ?†§[Nðf¼”É;Þ’ d‡§zFK¤ø•ŒjGlË•~Š1^Î@ükxG~| ÞDs>öŒü•ÓSœ ŽÈ$·Âƒ‚X‚ìô�$ VspÐÎlP~>þŠ2„ÑÑjD¢Š7‚Qs6ä0hËô‹�z{ø«VÈ >?â½hh.¨—„_1$k¤€çgRpÓ¸ KtnRñpÎˉB8ÇJ %£™—Œ¯RÂB åw*¾R±üóÉýJ)w=ºQx~y!…Š{‡‰Ì_*M�0çÃ'> d85¾bLg7R÷Š÷P$ U‹èN�è¡G¢òàGõr„Za Ë¢J .ñ„�ñ`…!ç‚ðŸÛ]± MÔFKQ3X™-Ý™†R‰q�¾ :�ßøb�™×ÁzÆñl×ä’S…Z<½�xÙ•¥jcƒøÊ ÑQ'<‘#2ö± ôªÊµFÓ©äZ3�ZÒ¼§¹áU“´_blˆYãJåd>UMê×X“ƒñÓ™ù3ccÈÊ ˆ•ŠŸF¶ 2I�AÒ¦âR]Ú Ôˆù˜Z"Oæ OÀ£€s$/h.¶2ÖÐZØâ6L©’ Ñœ@N¯¿ã"LŠAx!=·2 a°¸ç‘%$“ò¢Ý¨¸ƒ1+ÿàˆpŸ�_¡µX½ÇyôÖâ«x2G2º‰µêq± ,JÃr�dHÜUñ³DÆŒwmkÖk�ÝoûµëîÿU<1‘;Ï�à£Ç=dû¿­�¨œaé¾¶TX‰vTS”Öì¿Lžm=è`g¢ã&'´ã¬Š‚ÐÐ]œ¿JV…‰œ.˰nj«çø�Pc–%»“žÔkã£,cRÕ«Ó˽A3ÅL¿ô±^ŒÃD ± Ï�èÑèì8µŽO)rÎÀñ‰)\@BøG±!�³¹äSœ*xʤ¬îÂVæysÖø(F‹Œ´R.�šÿa3Ü Wl$æ� Ëõ^!�À\ÇËÉCXOk,%Q'ýh�Ù@~J•øGóÇY¢�Ëy‘JóBP4ÂÚ˜ã4øüežj=n˜J$-:@òÁŽÂjJ`ÉʸEFYp‰�ËžQI#±wRU’.מFÄvѽPiO½ÏÖ….2ÂOÄ·}ËoÝ^fmÆ¥�éá’fv¤–¿™r¡&SÅ…ä€�#„(ÛØ¸]úxôQ¥íè^QVUâ�’Ë 4±"f Dé,—’ ˆ£F|³Må9lGÁD B}ÁLµG1Q„ %çˆ,½²™I[¨Á5‚J\ãÀ’™4Á›+ ò·È8sˀĩ„žP”oèhжBÇõ€á¨qª› €Í¾*Á*/«¶MÓ·²RÆ£$�]ªÿBÇÏêINà̾¡e)Â>îåH“ŒÀ~´„{ñø“!ÿ|…ƒ¼ùg¯‡<¡½Sò[D—Ülê×.hÇâ$ݳMî½ÿU¨‚9 hÄióWh£ù«²l¨ù«³ë¯Ž<² Ã_Æs’=ð„¢|3 ïz#†¦Ï˜âLß?=5ç`öD_ÏIšèåÀVp¢ å¸ÃþJ ú?<€l°›qæñ LÄN.:#|‘Ñ–nžª8Lvñ—˜ÀéC7&uÊ”;5œâ¯”ŸrÜŽ“¼y” >ÁͺI%hÍ€@ñº Êâ‰øJ‚° s…Eˆ]:¤Xï©~ô‹ü³Pä:ãøJ×À_Ô­€p„Ε5X©Nlœ“úI�ìxl_…sêe‡ö<õ,6¢*”^¥@¦U@Ëú Þ«äq�Ðç÷ì)!¿QkÁØÍÏy¬‰¯Ô>Ð Ij3i�Ëv’ªÃ9EJôz¯>{£ÔyÇâ+U$G‘Æ©4 ¥ƒÂœd”Õ£"­n_É”QíͧÉu�!uC=³«¡Ïd|…(L:ÌüZ…<Ñoª¤¬´¬æÛðäF3ùT‘¤:ÅdÒæj"¾Ò–èÀ7 ÖÇWªÏž°/Dn_ƒž‡Ì AÓ§ÆW•+B P÷ËÞ²gìeqgjÔ¡L†™Õõ�P§êAi•”kN¥Øâ+”+hñUô eqlê¯4!MÏh#ÒQ“ƒíps©s0¸î¢õ Ž��åIÏ–Á‰cîcäYŒ¼†˜mo>öÑ7323ôcH�á¡ëv@(\KÆš%JÓÖƒö5šmëAtÐÖƒñg/¾r~â ãŸÃMÇ‚š)·,‘ŠÉü¿f›L`Àöó¢Ãž¬€eúK´j&Î@”…« Ê|Xž ‡Usª~-½K´6ø0$6,X ¦m#2Gƒ³° –4´±´“¢–³&_œ�q†°TÓ³|Ň‹„•„»Â±Ô€sŠ'/d. Ü#Â¥—ù-Hñtº°ä¯pÑAÀ#$uüI€2Ó ÄAfJ1f*DÎï¾°WSkÐâ4åÚH)r ®ï÷‚C¹6š!é)EŠ£ö5§¿R©Ó¥FP¡d9J”¯pW2Dh”­�a)˜l*fñÀ…w‚Tr$?êúC ŠZnä0è§‘?Dxrk‡7Ï�Nª«4 ‘ó%ãÚet]ϰÜ‘Ð�2Jaž_ƒQŽå$4z0q‡Opy³I=ñ_Êá]†ì±Wî�3h†�hM3Ê.-Ô“Ñ‘MÍßø§þdUþ0¿dØÚ9à‚§{)ŽœðI�ùä¡Dê^�&p¹rÅÏß }©”ušÚÊÖN´¨±ÁÀ„X©³‰'2%¹ _›äDX–<²È½Þ3`ÏHرgж‰b¸Ð=P¨f´ ÷¹‚�È´yz Ð�pŸCØiVZÏ=f@ðF<7Ú�’v¸Ð"ñ¤’­ˆêˆK>G¦´ ²z˜«zŒzš¿jþ*æÓüUóWÍ_é³�¶œtœzr0®V�Smñ•J`UÎ@-¾rªÖd’�dVnñU‹¯Z|eªîúøJ==}â!�’…µÇ}¯¹jÞ—‹“€ó¾Sãd—‰¥iÖÖƒm=¨8å·õ óÚ¹’¿š¹ò¹ÏKÊŒŽ3xs6@1•�ãŠ\ ;Úí_áXIS⩽íZ;`Š­¤.ø’ý£�‹w†Kxó3U)‘m XôÓJh�-#ÀÚÛW=Ö�PªØIr–¸ô¯Õ>—!2N•B„È �Jc8`³ ¾\Ø›J*&MUÉŠYõƒ@ɸ•EÌžûÁ£€Ê�t‡b™Ü �z‚oꦒ/ÕáŠ!œ2 -2#åþzhúG+ãÄÏb0•E£�…<5f,êrc;$R¥–]¦‡ú_uet*ÖSÛú4Ú$é#[èä†`†¤€‡6˜!­èˆdÄÔ 8ͦøƒ¬˜e˜QγL&„²ÖP™žË§„©µÇÓ(RJ~L�R^š!”?Û–‚-ø)‹ÎDG©eôK<‚Ž 0C:ê IdÏ­&dCUr¥|›{� Ú  ´w¬?7:ŽÈ!z»Îó ³7åI�aœír± ù/£ñ—L1ÙD¥C�²Á²«å–b=í”Í€Uz¬ðQ€Ü*)d¥$†#FÑä[¶µMãË ¿DÉA…\ø›¡ŸiF-#$Š«¡�Žß¾%j¾2úB‚ÉFpÙ÷PB›Q<ùïIN8j¡–Šh8§’|r塨2e‡ÕhµôÂ�Ôš¿RÍ_5ÕüUüCóWzE½¤@‹¯2I·øÊ‰5s«ók&ð_U\f á?b�_µøŠoaÔùÙ^†Žá7ë‡ÁÕ6½ï:I�Ž¥ .>tÿ‡fg‡^Pœ­cÃgÚö±FËB�‹¶T%µ~PE®XSä@¥Ém=¨PK[j .Q{ƒÁ@\;Cª Ê‡pN@I©C-k`.­¤ä3ˆ¯â_@œ>õܹz²TÓ´^AY’»1Ý-@²&7ý/‰”q '–É#'ò&œKö\G&ʪ\þ¹`â§šJ»ÔÈì³�6@L�œ ;á+ß‚‚ vŸyG«xh®Æ|Š`T`ß'ÝqŸ h!VcÒDé¶HêÕ‰¨dÌz…J¬&a’ΰ¾Ï"±ãG ª$“UÞc‹"h:œ)œ`º%âB%‰<“N"Ë>5Cº"á²Þä®hÅßÕšS2$2Tå‡LpÓœ Ø&I“BÒ'ÒÌI™¬É$øéÈ"äi„v�¥`L†ìµÎ} Užf{I‰H y,ý a�‰"Ó5 à'§ù³ï½  ¯HÙÒ´Ê–)@LŽú³Àà7ÿƒ�Ó�´“Ú‘U�DulÌsçäÄ dCÊã:DR½(I­öò¹ v<Úý‹1¬«7íà—;ÌÉŒ®‚I+­ÌH¨(áL�ò§@´®4²éGtIðª¾#⫨š¿Šª¶tdóWŒ™æ¯3 ³ jΚ¿jþ oé|£ßü�¯*¬kñßhŠ~çÌi·ÅW|´øªÅW‰‘ïNëAïøöñé…ï}Ãuò˜ûèÕyÑÂâÂÂü¢“º±º¼‘³Ñ8m=/­”îÚz�´õ`-�ý\�ß­òW>oŠÃÔ‘«zWÿ²™ D ïZ€;…3älçZÛ -h€XiìlŸÿ”$ufÎß`ë|šªŸ‹'ï~`åRG�(Eá¹ÿ]îçÞÀìèI&ÆÒѱ¼D.hHjC:àˆW¢ÈV+¾:Itâæ€÷e)�RJ*ßð”„ÑÚ"Ï^¦ éáÖŸ¾h`ÒD|‰ø/‡Uðr @ˆ±w‰»0Íyž �š©Þ×H*�)9y ªÈÈÉkhžDs£©ž»‰|&3Ò/Š´rœòk‡M8¤#5)¢vÒ)îZ¤Š6…]J`î=7ÉŇ{;›ö”Ëb0r)Ò${…Á•Z ùÀù¤çí-žj÷‚D Ñ€×Qt²ªE½°ÉŸÁ Ô ¤.hc"ÊpÆ…É¡ž —ä±à§T ¸�RTzVƒ¦çU¤ö‹lCŽK¹SBûFƒˆbéÊ݇eàlÃu Rf *, ²b$p#'`2)D#;…’|JK lN‘Ïj¾’L‰mÔ ¦$§±;Ë|›ÝamR‚ê4ø-O -ÏC#�pþ‹Šj0Y£ú…­<Wêž•ÉD–ï4NgCÇc†±í<È\€[éúB\¨¦ÐŠT2 „½ô˜—Ž ;çÈ¡åD.ùÎïpÙþæ‹Î(67­¨+O´K~-�œFvz: ¤%ªßþ²×üºC%(¨ù+ŒOKÑ1( ÛGbt6åHlþªù+G‡¦îÜàh©kæŒÏ™ø ¯èo‚x]?³2Ã(+-¾jñU‹¯2ØGk½_Ý­Öƒºc“où}\·µzø¸7_pºHìÌz×GjA‰»&H0è�ám=h$˜è/± ú#öa¥á"Ë镹¼ÈJϨ¨­Ûz0&‚}4h-i5Ì—Ú“ kÌ1##I,iõÜ!zçóW3W^ù¼²HDZ¨Ü]Æs°ú5¾P¬à�ANF*Ë~*ùþ…e=W¸MrȨ«í´‡o«øyZ)"ÿ‹£�ÃkárTRˆ"”ÀshÕAµæ‹¦ºöj–*ìõº"-T·ˆ#‹j}<<Ü;Ö*1T¡£Íf[’4ûô–-=Ðo” ´àÙ̈)ù‘‚�Ã�¤d¼â�ºüË.'JÌÍ,ä�:¦É²\'-h–^:jh…ôð£ä¢ãðA΢…1®¢¨f«*¦¬¨¸-Ln”Ym$ðÓ8tÒ_\™d“]Ô}ƒz/D)ÂF¹‡Íoù¾°•XV¶ä1aÄ^ïA oi³V„—€CRIàÇüYÎr÷ip˜ÓRwî”Fc‚)�§ð"*ú)h�+;´15Ï‹:g�ä#Êá[Ód‚Ççèì®7)O¬ÁÝr&O� ÝÊ+I×ÄO� Lð¦u'›8�{SHþK1ÙD¡mí*¤½Š=Ã8µ ØOVJ�dI�b³¶Ói…v;�ôÎ °}0 „&�¤ äÉö0´"rU( ¬KaéÎT ´„иB™ëä<ÅæÃŹ Z)z ˜­È‘ \Œjh¨r믬ÁI%9d’U]-¾jñU†O‹¯âDîšøŠQxâ¢{¹çtÿuE¶>‡üÒ£÷¸tv8; ‡³³,yqdÆÎm=ˆ÷jþJ+iþêóW.NøƒksjîY"gbdf¡SseCRgr€Ñ^Õ|feã”� bÎÚua!ëu3,ÔI©XBC`p>Ãà\é<(3&"•U1Qè b Jv`m‰,‚Û­Da‹¤ ª5T‚ Æ©'³3.|Š–ï’0¸ˆ\pAël̳¿æ°Þk¦ˆ€àäQh�I4ñmÆîø9•û[mî�wäe¥19¾J ‘?RªÔ{—®l˯ KKS‡?JÜîã‹0äVÄäV%KÜÔç‰ùŸÐä+P2Z¹ x¢ ¸Ìcè  DýæÂßL¨,�e/¼$ËE;Í��òãwÈ@Pò¡hÛl%ì1¨½¢ÈMÊÐ]æ †¶˜jÕ'‚ ŠßäóÀË7°8ƒfº6AŽå£(ÊäY(€GáRÈÈD;Z)°‡唿À’0ÔY::õLNƒÔÚØZĤ߸Ÿ–‹ô ¦"úÕŒ¥^+މB»!J¢9è•Y˜è#¶Æ®1Y KR ‹|8B2|EÙ´$ñg/‰\Ô_Tªn8äÇC4àñ�ˆbId¡c¯V¦Î®4î!�É™)ö&M_íÕ¦è�Â`«b5ÿì@,”Ä–§à>Õ^[U®7:�bd*:Ž�`€)5~.üb÷�¾ `â&kÞ÷Šyø5Íì5³Ä#FL�xJoÅróWv¿ÿì/Žæ¯bž˜TóWÍ_5oùO ¾r>Æ.-žú [¹eðÚ,,,ÎÏ/ž<9ïn‘Eg0ˆ�2ë:µøŠáQs±Þ²ÅW„Y�hJ†2-¾jëÁ»,¾"gfe’…@,põc×[_Å;NyÏÜûf¾ÒÖƒ.WjMáB�±ÛÖƒm=¨¸HÌâÈä»Eñew³ü»Þ®tšáŸ7C:¹‚v3vôkÚ^¦Äo!pIŸpÁÒ9_™”J1°ÊH †¨CRî� X³o¬rRAlH`É¢¶�þeåÏ™A…ä©âü—Ssæ:B׉Ejœ18yQ¢»uëAl��r Bû)òJ0ÙŒt€ /_ˆÐXB\gžKD@áFmœæ¨µèÓÖ2ê` ®¸Âo©q ¶Z'Í©.uZ"Ñ´-òRÐ9G©uæBÌ(¡�‚X’²�¦¡¨W+œôDf€àÛÕ+Þ¨EhÛPÊ�2—Çi�°©ŽTŸFµªÀ^ µ„2¦Ôž� dºÒ69�!°ªÏÈkö<|˜ô”.5 ¦||#}OMRÖCÛ 4@Ëa2J‚Ë(ø£Øž Í`O�8ƒÔLpZB¿š[$‡*rJ´.û\dŒq˜f²º<¤!,"”ÂÒçýµíƒ?·•J0pò›SšŽ"HyB\V8ì ¹D"ÈEU¢‰K––ëæfOÍ—ÙbQP{Eq%ÆG}qÅSEúP`e u «$±º’vnÓF•jƒ¾F&é—.$�3ò£ JJ‘AŸvòça ÿ"[X˜™âe¯8j¨ƒs˜²ÿiJ ÷öoPñi4¥ZÒIèSµú»ëG™jÖ†PfóW�v¥I6Õü?@4‡ê‡ÎTž�& ç’s"¾2t™yÍ ¦¯þsD™þÐ_L//ðKÄmÞ›×Ú#CÞyÉ)1Ö\”ÉE™ñ –sædÄâœ_?ZU“#®µÅW™¦£EÓâ+Íà ™2¦_9fÐK[Æ<Îx=躆…Ö=?oðÎ7¢¾ÕcéÂ{íýŠÇ Ùó6;;f<+Zom˜ßüU[¶õà9¸œyΕÏM¤áâ› ŸéÁ çб=Z.³ª7É*�œ A�qáŠpÔܤ%hât½TZÅ'%"5Ï„APĽ]׃7´‰Wã?u¸(ãÂ$„ÂWºzc'¾ø/¶Ü¦}2‡j/H|)[ׂVŒI-q•öÔuóInˆ2ÿŠÁ�äý§T2#Kþ\´ç`7k =€(B˜DnÅ}R LHʈ À«Ö4ƒ¶lóÍV+ùëE�hù£´iÇ'Ñ1�øßç3“¶°¾®¥ÂF¢eÞ��»ÓHÆ�OSƒ0 X´³Èx“ö ”\ ˜FÐëÏ«i8 ªå^¼dIª7lDODæ¤ç:—Œ1*1C=‡²ÐÞôR IÌø 4·nõ}œ£ø#eì².“¡Ÿ–âò~LEX›¯ã�JöYÙZm �U³6ŒøJùT,ÍV!mîG@*„4­õá êŸ™ÉæCS˜pXp¨Â›¤JwÌI-…Y`|ÿì8ÏÌ¡ñ![nÙ”¾ü£qz|ÁQ�õ³ ôí:Ç"Yf±Â­”´8z^’�•Uj”…VH YLO“íBCþ6ˆàƒ `J�·(S†‰ù …_rfzʂ§z,Ù‘ ŒŠ%"S”…_öÿŠ"Šˆò‹5«—g�_K1À„8„ %ñq@-¦E3€±ù+zN%©ªæ¯´QmPÓiþŠ±Â¨t ¨�æ¯ÐCóWN®ùŠëб¢˜øW‡Nl¥÷Æ\$¦« °»,¾ò�òšŸ�yÍ a°Ž™¿wøGoœ^Z8ü€¯Ô®3Í9EgÄ;ëȼ"Œ¾¹¤©Ó-òö#>âµøªB;?þR�5·øÊ±�±8™´øª­?ÓøÊ¸kZ>qá}¶üÆ‹3Æú�ÁÍ{¹çôØE—‘z3û6ò–SŒ.n«ù«¶ÌTÕ‡nØLóÞÖƒ™Ýk®º®k×[b+æ‘åenñr21ì2áÈ�ÉÖŽo03Ã�ÇM( Ål â²æã~Z&íä”Ü&™ª uÄâ‡S7í :¥Â‚CÜV-™:à¢Úfª@R¦#DOø”XP`‘ù²*µ«Ç  À�«þ"¡Hñrá¨ÈH:P)dÂIŸeo`&²ðfMŒ0ß$¥’–âM\F8–H5âYaΦ?ëåLm«Nªø ˆH’DÙâÊôÀëpäG).[™lÕ³IS©-�‹j³Â¹Š ®•A)Ùožçx'q ˜åôR g¥ E• ó_ñ"r4 z�«›Zr"¦2œÊ¡2¢›ô#¿ÔÈ`‘K:‡ I‡� ás5Í«{zžÁOUo@j�ÿ‹Ø†\%yÃçXAî0ñ”mŽH�‰˜éI:K”B…ÿjfåzÔÁ 5ɕα±<žvœIX,UdutT™úìµä�Y¡­ç( ]ñ‡6¼H‰"Rˆ^‘‹ ç¬4§ "€5t’Ù5pÿvwøSÊÄKÎe0I$Hä@–TFo¡é1ê© à±’+ótòÍî$À­yêDìBË)ˆë¿h­¦·àKzŽYd]‡ÍXýd+>CÓ“b#£-` ióW¥(•‘nV뎋›¿ÂÈ0öŠÑîU“ÆŠA¡"l³lþ*úhþªù«*¸l=x¹_܇¾Z÷Íxº›ÅW ³?ú r¹öþí_ízÃK§–Ž}ñÃâ{Pñ•/Ò ô½ËŒãŒäÁÕâ«èGÇéŒÒâ+Õ�€†±Ðâ+c½¶LX‹#ùL׃´Øcm¥\ýžu�{;ö•ßrø¢KM½ Ù÷6`ÉÝ,äÊS7¥‡rTV çUóWÍ_¹@ÕY»¢¾{ù«™+Ÿû<ㇽGs\€±–NÀ%¸SoYu–Ón7Q Šü„±YÀ8e» †ò¶²”ÓdLk¸·¹Û§Î�pÀµ8õ+z4.eÀ{òS ÃOQVQ@â°²–OªÍµ•þHO6Å�\ÈŠž±�I³ë7%g±òòІ~³Sx ŠRa¥ÕŒ²õ¤–€ùMujÑlM8—°]œä®B®˜ Óu,ûÔÿøæ 1·Oðô+›~¢ÿô$‚bc/’¹®(Ea ÂÊŠ‰9$‘+¦hÓºR$ü~’+”®¨…uô.³˜ÀRÍ ‹ IžÕ�‡’á�º~ «ÌY:"Ía‡Ó–b¡éÚÐH IJ¯çjrë§(S-‹í4^)/ú3°cOÔªõd–­§”}i4(‡Âp]ýF¬0H-üxȌ˵Í(0iºŒCÄOÕh  I ­ÔÊ)ƒ£ºÑ¶TMôÅ)Už+úÒôÔÀd(ää€Ï`VS‘$ UN Ù&šƒ(tírÏeO Ë}ÌœÀ8ù­è,rcµ²üöޭɶ«:ÐÌ̽óI†ŠhDq1—~²tDtDSæœCGô»e�û¡ØÕÿ¡Ñ9ê·rvýÜTý€~hG•ÛÐáƒ#à¥í�.H�Ì<ý}ßX;ºár!„¤¹2síµæ÷9ç˜cŽœkí í�Áxï½4oK M€Mâm?¨ä0+¦ŒüŽœu «è‚È�8’KN•¤£tíNNe€Æ`ö.‘êË›À Õe ~÷ÒÉCHp»°Ê(�6#.Uû`æå¯€Û“Òô;µãªNcW[þj�–YþJÿ£gYþŠÇLuÅüÙ+8ýŠÆWµÕÝ“¯þ_'ÿçç¼~¥cÿõ/ïÎ_xîÚ <�~QðÓøªË ÿœ§V|åTª) YDµ+¾Â$ØeÅWŽ”ÆŠs†NÂÿk â;�Dí5Dmk=hØíÈù/Yâ{y¼|q÷Ôßîþúψ‡cÿÝ'¾óñO’v#ï¶Ûï]O÷bï±~ѸÎMG¶ÖƒŽÕµ\þêWØ_±ëíV^ÔÙ¶(ËNKR‰®q %,íÏžøk¬»6&Å1Θ¬À–Ò˜{�7Ïñ&tjº—‰éÚìb@'’ëöŸ%kKuíºŸVÄÓøÌ"n¥j÷óPÕM.�Püõ|é"ØnôÁF'1\½øˆIp•'Wk‡sj1._¢NÍÐ1›‰�¤¥^.ü�ZÑc•Cf/há¨öÈCs£”•1¯‘e„Y¯ìã·1 ‚‰ ³M{N“ºSeL E÷œäV»�rÃ[ë!Såêt)ùà/t)¬²íh22-h©æ´�9�”˜wš°@üAfùP4ô¹#*—Û£dRZg$¡0¹BIAá¢WltI¦ätëÞ¬™¸ª GøØ2Ak„l©å$s([¥J…éZ›Y¦¡ª´§óíЗ(~u…¥Ç*”1Û1•¥g`ã®ÓZŽIÙ¾#<–É�âR«z‚Þ“Ì\k㲜àéÒ Ø¿æ7Š˜„D,÷ZÅ•’"ЕÝl×egC¬”V‰bÁÓ¬”iœrׯDC)AF�!>$•præ2¶œj?C�dl c›ÞÓp@¿˜*}vЀIæ¶Þ*:(^‰0ô©¡c=âú‡¥m£½c^߃M|’‰âkC’ØòWÙxù+;¦XþªÑµüýaù+ç�ê[/¾b&8ï‡y(SÄÉÓ�3ô_~ìÿæ+ïüÂìÎï<ÿ‘›„E#̇Î]ôŽ ¾õˉ$­øJ·±â«_ÙV|E0ñK]â¦uA„Égû/}þÅ®ŒgNÿ¿‡?uz¥7½�’€cl‹ ×zиo–®2¼iÕáúq¬õ ½j­éôß9U·`¡úFå¯v�=öý9ÌwØCé¶#¸¡­v]츑NŽ�JüêDÁ®tŠí¨.ÿ Vç$ *ë�kp €éÚY: JÕ[ gÎʼn}N]Ü;,ø{ÞŽ7˳‰'U K²$—åü˜ñ�TÉ c’I…¡&=Àã§…ȉ‘Žª…A”–û Ëÿ%«y&CØ(ÁA23åÌF¥ÿŠÊBòj†TEOÛ#¿’R² Í ¤Ø'/;–ž%&mÔT`Hž€Ö‰3wÚ©a†¶¦*‚p¹Q ,¹ŠÒ7bk LK�ZY:eZÉ©8Á(µJ6È!8—ÊeÁ aƒÙ1ú=@s—©<#`p½÷€Ž…Æç34 hÌdPŽ‘�ïÇÔÒT õ8&r›&RDeã˜p"á#àÕöÂmñ~�µ@•BŒJRšÈ¯ @éƒBUk -GÀu#®m;ê¡3ߤ<’LÎ BÒbÁuÅÀ6|‡Mjï*—8êKO`[P8.u5E<ˆŠÒÖAA�·Î JMqó¼´×⚃mãCÖ{k YQÔD”,}Ö5}mqù*T¶4… 6¿*Ž_c! xXË‘6#5²²®¹¶h"‘—¿Ò4LkK/ew™.nŸÄ,ã+0ÓòWË_5…,•ËÝü±~ƒÃIB÷ï@y3ÅWNpÎjÿøßýÏþ�ÿóîâY6õy¥ƒíoïøwŸåp?þÈMgçLÁtMìÀd§E(XñÕŠ¯è+¾ZñU^ä—_±VÀ£]üäþ\ýÜϼî aö}ôy_÷vzzõd·ç¿Ò~!\ð:2CkúíZ®õàZ:t[…Ò0* ýL_¹VrÉÔÜÿç¯v·nÝR(ÜÃó@³xij`³8ŸE²ÛYëè€ ,u-nÓo˜ðJ]ßÒår™•%ß }ê&¸1øáR �yô8EDa S›{‘ò@ÙpÉäÃ$8€¦Ê=Xðv4£(S猧 ú¯V×ðÓ©ç†56MÐ[ñ#¤|’‚àð�¸?€h9¢M¥€¢M*éy)sÛ…a²4|j¨Ä�Û?´…¤í Ø0ós᪑*ˆÚ&Säí%¿d+lJ ’lˆ¦ H eü€NgæÝàoà2Ò4 é’¶BÍN"aüÏñ�)[ÕV@.j0�ê?¥bÁh¥Àg¦5Á"�PQŽ”{¿ðTåÂ#H”’†¾0�V=éÙ[ ú¸Î ÛÉP;€¿!…èD �̃IUDÕ6ñ|÷˜¸¢[céVëKR»7v ²L>šKk󘫠ª%HHá>I;ÝÜöä׎;Õ’•OŸ[‘©q4¤­8šŒ=@íîòþ?öö#šÖñ°¡0ÙTò6~Öß¡J;�ð4«™²iµ±�¨  Ò&H8rúÔ®ô$]«*¨} Y}ºˆ¼}G‰² €’–Ñ^Ê 'ì¢U@ââ¬ÄǘÓb@üp³$ÕZ§ßQÁQ\&8Êå�UÃN—Ô²²xå[þ Ã׊4|Ö_þÊÎC÷^þjù«å¯ò¸oµøª`Šmlýž]<óÀÇžüÄïÞstvõÿÙ‰ã•pû‹;Ï=p�J&¦fgFÿÝääÊuUÍzÞWÀ”`S^+¾Â ÎüþÛ­øjÅWÄ k=HØAÈ?[Ir#‡pÚ蚨�3±É+­)ç%!ù�›Ÿ¼:ï{Û�ò¦VË_å\è·[½ Ï*ÊÕ‡Mà!ÀòWk=Èè¤Ô\½¾aþj÷ØíÛγnCú'‹âú.'³#vÚœÆtn¿®Ï-tɯ/élïž?ët ~Z„³‘l`Þëy$$u¸(€\Û+ ?õ*]8µŠ”-‡Â#¶Eš7@¸ó ¼Â" Œ’jòòR–Ñ… š%,�Ü©û<Ì*„ë5‰J€º²‘uÛ›ñWQ²(„£}•ò$×@1àMi|"“v˜tƒ&ÕD>O:²˜V‰·2´ ¨lWrF˜·z>×3G4'mrÀ�8tÉ0妧䋚¦‰¦A«9Ó\)m­¯|é|ÂÕAH’/l7£@S]œKG¡l ¹Ru/m ø“²ÒîÐ¥¬š e5°1²–òDú ÃÝE&ŠaXj*ÄEÀTØ—)­ý4^$‡V´©”hYCsVCUNZSo( QHº¹S™èžˆPò�s]1yó ˜‡"ÍÖõ"ù¨@î»VÊ•üZ^¼ÈÂa—J)‚â2áß\ÁÑåCabªOÂÞ'¦Íè >’Ô\ I[\óBI“ÄY�M"-z}üÕîÑ[·�^Hu]¦œ`ûdòJXxç •cÝš¤5™›:›ô‡Î/1|ÉÝþO)õáÍð ŸÐs™v�1ÌBµ^¤!8÷ F’?@ÃÞz1Çé„vV �Rë�Ž�S¦®ØÛõ“­•¥Ä7ïeîbê7Õ&‚ /Y›P€2!ÞFX@dîÚK•JelˆôÙ/s £Ö˜Z`*°ö¥ÌÃ"ž¼Hyo¨D‘­*žŠa)¤¬¥ÎíD ÍMŸPšT†ô‰TùãQ@ÎKˆ$°yú!e…�¼ølŒÒ¤j&Š‚„~>x¨Üð)ƒÌ‚©HvÆ�V §wð™ºqÏ•©2‚¨;`¡Redl+Vìiï+ľ¢ù—¼ ÁÚÜôÔ‰¥ ÔýbNÊù±Éi!ém45dåƒR;°«CöŽ5�íÏTyRÑ�WüRt…#}PEÊ¢e) &K«Õ[º8#ÚF‰+9¢×‰Ya¾±±@IDATªÚcë P>œkmªèÓ½4{ÂÕí"ÓëlÆáD¿\O"c¼¶§5ø^%ýD€ ”Ú¤’w›±�BLƒ@ÆLé� é~üúDzú/'H³�^ ˆŒ´Óœc¯1§ªû»ü PóÙCüYþjù«å¯–¿Ò?ã€õ zà·||ÕÄC\«�xÅ ñßññ³Þxâ‘ÿå>pßü9ÛßvÇç?~טiŠHšµœz˜]˜—fûé¼³â«â‡&`'iC¦õ_­øÊ0ÐD‡Cÿ蛵Ô(¸~‹·ñ-ʯ¾œÿ˽{òï^þº·o=ü©=Ç•«þîü_?qö¬=–¿ÚÖn->0»&_ëÁ#ƒs­'jRŸµõì¯v�>z[i”æj™ÛÚ<À+ç�]M“L¦È5¹ òq±ìÍK€ùXçræ:]�ÁÔãÂÙ--<ùâý8wŸ\´Sˆ2�Aªk°èˆaI9jafšH/9hPåG%¥ÁF A/¸B€ðN`€À3nJœCTáÒ�“,äWHIp•³”�œB-•œ"ùpÃE'÷-zA ˜¯õJLŠP�dƆ_Es˜~E¼âšwÿ¹&¹Å„f"Ûú$MßXc¹EY³�j‚HžyíÆŸÿF'ê­ÎëþfŸI¨µ¡< '!ó;mõ"¥¦lœCkpjuÚÅž�ˆŠÎÅ!�£<,#(Nݺcm"Y ‹ÐÜĘ˜J%'´‚i;¹ÛRZG¦Ú×™]Òð*^@TX+Rby:#¶ÉG»Oü´\Û$y.ÃÀÊL¥Ñ=ÐÚ”³¼xC³‘'-7ĽR¢Ä§9G¯ä„†¥Q# h1„�'t)à>=¼æò`aÄ8>>ãoVšvÔ’kZ];Š!út�’ɶ[3SP'ž,öÊ ›�Ò¦4Î3 P‚:m=FS¢…dе-¡@2ÛJ´í 2ãÀQ6ú¨¼¤]ãGY¦œ\5{ôlňPƒ°™ÓÖž=­ ©ñì%ʸüÕòWô$;ñôÎå¯Ë_-U/xÄWÎ#Î^wù·Sò®— í{ù÷¼þÄošÜÕo~ÿðòƒÃ�þÉ¿?¹¸óìC7� Š¯ !!§«"Òæ³_­øjÅWÌ+k=h¬�ô:…÷ÆÞ…F¸^tú¯_ÞýÉý¿~Ê3§ßyüÅŽkí¿îÝ>½zÕÀíÄ·¾�ÃZëAW_k=¸ÖƒìCheù«ì¯æ�S×ûx–Å­™]Õۅɰ¢o îê7—3þÅŒ>(ýL˜Ã`QÏÊ�)ª„…+ó6�À¸«ŽLKø&’€be=|7'âÆ ò›Â“Pn­èg",·Ú„ŠË‘½¹>‹ž²’Ç·¹#�’<ÆŠžF!a¡\Š€ åSu¦@Éd¬F;ó_VD›Xã‰8Úʔ G‘ y•B‰ : !GL=¶Zw˜e@8î å¦pHÃU$'¡–|gÇ�"³'Â+Ä�íüŸ-eÞFͲRƒˆQ9xMÍ–’‹ð�D´¼D·Ôí_.'ZlJb/x÷ÊŠj%�!‘ùǨô-´Ä®;I ]ÁÊš[ôò ?e Œ•¢“ÜÜÛšeÁNÞîýÜáwá?±•™[j4DáÓÞ.ÊM$i6µBJAIîRéàI ¢Ê£ü°mtÔ#9Út¯,;òVÙL4@¶2Ó3�V)<±Ãä^ŦHô�àS(¥%ͧ‚ð·Cµ#Q!åªY€7U–èC�åÓ|2¡BÐ ‡ƒl82#èŽ=%� ¨b‡@ÆL=\aKßSÌTY€S”Y«¦ó£ÕÛ‡6}ä(•?Ò�D)¥”ÛG´tP” 'xnv3¡¢B½–õO4tZ@”Ž‚Vä¸ZdùÖÁôd­Ô#2ÖôjX·޴üZþjù+=ÉòWË_åa�øsnÈs¾uã+^áTÄl¼=xÊ&¸ÝÉž/<9yöÚõ'ù½{�.®¾Öó§Î×/üðÚÇ4—ó�“%_í¶Ù·‰ªYÍ8ƒc›Ç¼aãŽY¬éiÅWN嬸ª¾bвâ+† cÇÈÑqF)ˆÜFÒŒ£·ƒ¿Ê¿ègœ­1‚k.¢dά§XúàIÞóþ—¾îíÝïêÆ#{RoW|áÛ¼^Z'´Öƒk=¸ÖƒyØ7Ëzp÷™Û�n[§ó®æÉ18c¢EŽ¡ Ãý¤þÒ�HF¶Àñ³y‹bŽI%´˜Æ�¸ˆÇ't8ã8–¨™µ·™Ê̬¸ÈÅ%?i'"¦`‘‰ZŽb™CrEôò#•Sêþ<-FD#"æ¾¢M:¯µD‰ˆs4 •$6¥ÉVÀ)’ ÎÆ«È‘°ÀŠá™+.ÿõ'±ÀQüm2!k†¦cÒíK²&Tƒ¤N<¬*±¸R/XhÌ()‰”y7§WT sA&‰øÐŽfîTß:U±OÈpMÁ|’Îs²(+¹4‘U¢ñÊ8Hrwà,Wh�ÛÌ\R×…Œç…ý=D¹IŠv(!{_h*d�s*c¥T !ƒ¶Ð Î/å¾Ù~»¦l)Tgå”*—6À�챚}ne5 }­ªßd;:µ]ÛÄkvá4«ía BByj^Õ76ŠÚL v¤DQq°®pB–ÞtCTd&´ÒХϙ*º�ˆ Cõðžô!dè‹@×y/ŸóD‰ËH‡ž7¶. ªK­- ˜š_�ù7ž­q¼GV?‡tì]ÞF  ¬¬ZW +8O—Ûø7:âCì—¿ÂVc'dù«å¯CvŠvÝ/µüU>›Ä�޶Éâ-_S0i892qð,›àÈÅív§§»¬(ÜBÄì1ÕŽ'>lÛ�`ÑÙª, ´Te£År�¤c@%#$h(ÃÓ^<‰öN .·.#_Ù*„#=A�a ŒN-•©æ´üU=:ëôÞºRC“ŽGªW͈°‡Ù�Ñ�ÔCµ¤Qm•TÙ›éôË_-µüU¤ãå�Œ¯òúÌ[¼Yát¼çÁS3€{p¾î™|þô׎Ϯ¼Ê×/ ýþk¾;áy6Ê1¾gâÉgŒ3hèëœûû3fø_­øjÅWÎŒÎ�|¬õ þðŸ¾l¥k ÌŠóäÉ¿Ýýõÿ�/�Óï=ù��ÿNÏ›²—÷ {±21òZný g¬Aì~¬ X²xÞ"¹µÄÎit®µtþ~ãòW»GoßR€ !ì¼³Òõ�7BLØ‹©³ÙÜã‹Î(ʯÖÇ¡`žÀµ íê?}ë0Y$ö”s%™YN%xâRÚBgYnœ<÷ +,Yà˜AK¸ Þ0· ¬€€›A'&z˜½"éÂ|(*÷#4•¡ý�*|œY1–OˆÛòJÊnÊÀnÎý"P…4¹QjdËå˜@>X.5Y “djD…y‰ 64Л!Yø€Ö~®Þ†¬ý@0ħŽXQ(Q\ð¹R\ïÑfiJ'q9Y# ¡Gù@×<ü‚-b&— ÿ†�aéw)¤øñòÆ À»´øoJ©—ÊçQm‚‘ˆšA¡IAø­Hq!”]�¼£©(�sÀãITLë4Jlm •MGe'¡¸(ŠVsuŸ $°f„�˜xéC™_tÀÖ)'9™Øõ,±‚ޝõDµ¥Bt©N&«2ÿ¦Ó¹õÒ[©ë­šœ2Óxàf]O楗ôR« :ò £º…4½ßB ^~Ày´:½%â"Ùèò ¿G¶}É éÝXOëx,X£œ´ÌU¦#çÉ[&»Ôb\*m>�¶‘‚›$ÉÎIêùP84„á§J›Ä3R%m�ž§ŸGÌÀ¡ƒ ÒàCZnù+Ûsù«ú•ý¢ {=ÒŽÂQ·ÞÆ’½�ÞE¢ÿÙí�Û^þJs-µüU^VßìoãCçË8ù•ޝëLŠJê_¿À¶v®No»ÓFGe£Ö'ל7&XÁ³ÆM<ú€9ÍÅÅn+3"N.ú„Ѳ§›Í¤8È‚™ÝBï-v“„äñ/1€!‘ë ÍXµÆöØlà\tËÂo/À1‘d˜D„NÞ¤‚ - ·t’H¢À©©3OA8“µ9kŸQ$ šƒ$âÊKrQÑ*ðÆ�¹„:ŒR¸@Ï]­ðƒð'ž¾Uj)5 ¤.�à}5ÙÒ%ÐÓŠ&8Š €`bzBºš+s;ÞE»hD)*ž± ëö ša8rL¢ Tms“*eò6s�±Ôqè™ îʇ¬8�»éRÊ@ÕÊ'qMk‘uÚ:�BÊŠr±8»±iôS•ËØ¨wôÈ@«ãÃN…ŹÕ]“ã“æf̸Y¥¤%‘}‹?@$×êCÜÙt×¾*æ·M P#qu^Úk4SÄM¥ƒÀ¨Ts§>™Ê–”’Ĩ kð 2çhЖ!!’Ú%å'òKí´«r4q$.uMÍ•2q’Ö`Œ”ʪF@¹à§|ø°ÇÊ�Ü6e¢ylõqWõJµ ¤�°É´š�Î=-Ø’DáÝA·µy›-ãäÈ ®=TiìËЩqÀâ?¥èíp ÂÉ<·Xmù+ ¡µìH4¶Zþª�h¿=ôß寖¿Ê[-¥—~ÆW8Hœ¤'¦"ßûvšÕ}p|<{íÆ“�ü® ¸×úú…/ßóµÿ´êïž}ðÆDœÛ 8~†ÉLÂxà_aŒ¢¡™¦W|e ¤=úXñÆ0ÞÓ"v‘µÄ¯¾l­D4ý�÷èž?ú7ZíEÇéC}ö_~¨Ä�Ñ÷ì– �bqãdÝÓZÎèÛBÁµ¤ÿ¬õ ƒˆu“ƒÄ¥jŽÈ2– ü8¯»öÞ¼Ôëì¯v·ý ëb‚2‡,²¸žkÇ2Ë_myç¦/öã°°™ÕWl%æDŒÕðoÓ(&!q¶Q»âôa.ÖÛŠÁ…ÿ-u1mú¬Ïê7–“)p×YÏT¸+*A¹ŒØHÆ”r)ËϤÙîyf3c!©ÎŸì�ù)â0Siª¬T®aù ^®�ó5oéË.§±DIf ‘¾Nަ.hP}!Ôzf_ª¢çKÓ QÊ­=Õo,Á³D=dˆPîe�m¤^ªèo £ÌŠ.Wm©!ü wŒTèÒñ¾w°ž[ÑɳZg¹ô°º4¶™²+Ij%©U$$¬±[X®`¢*�¤8iû<ƒ!"!ªÿsV~9xpú&’q-dEQ£š3‘âKS®"�îPÑn¥Å\óƒê, ÓA@¯ø�‡é&…2GÅæ5î@£Zù:o£X»1Ì*If{ë:@‘’†µG͇Œ•Pd¤²Þ!c $0 ² ƒ%3’ ͽâS89./)ýƒ·Ä¤×ÚªlQaÂèÓÏ„Jy?ߪ‘êÎ-<0ŽC*Q"AÔL\«ÑN䤲[¬Ó.YBgÑø|m°ˆÚÔT¯½BXroœ’}I‡ [á½Äá#.Uï²íjÌèçòWÁÃnÉ�U=v³LUß³fù+­À±ü•Þxù+]:“‡ŽÄiªQ„;n‚änü°o¶øÊQïÔâGoAq"e·ˆ¡îwÏüÆõÇùÝûŽ_õûOOþþ[;ž?½¸óüƒ75M–À0c¤ °þÅg*卑ž[ŠW|eŸZñÕŠ¯ ÎÌEvc]gìµă\®]J6;ïÿêOOž~û\»ï>ñôÍÿéêž}o§îæu‰³Û–-y(Còµ¬‡åø‹’s@ÌqK�Ï6þÉSc6®ø]ëA=µóÙZ:›¿~þjwë±G§E tÇ­GÎ<é8¦ŸÂþÜîJ&ýŒ³e…«�å/è²úЉpÂ0žå¨×›<¢°‡P�“j™B”ã3+/LþL>ÃfC¤�O²”Ð”Ž¡Àƒó…‘4Ê:ÜŽkò ОB•i¨_A]IûufX™,q�=ÙYñ�¡Z™’àW ‡&%J, aËoYTn£êFEDQj¬E,åsl¼YL-TMž\%_÷õYÇv¯ZEÖ$*q ›(‘ æÄ«(©’SèM€`€ b¥"`#m“2rå$:°ÖdWeî‚K"èÑ.Š'i-¥}80�ÇÁ|ö”–K$I¨˜… *ÇÔ8<",M EÙÒ’uÄ¢Çð§žÀÓ�� Ÿ™Å†‹xìFT)I��"�¥ZüöÆ&iúg—ó¤2äÉÈŽy¬i>ʵ/dDæGËBeûZbTTžT§„óO DIÁ¡Â4›öT)¼�½2 ¶Êp­2c‹ú„¶{Òcó€²÷´kõ*òù9™7Vî&³œëàÀw&ä„W*B„°–;eWõç\L®ŽVäÓF¨3€Š9ÆÉN‰´åíàjRðD–üikÒ½à‚È�Úv9 O<¡@J›$ÜH/$ mí$ÅÁŠÕ.eÓÔ2�GMH¶ÉdXmù+lÑxZþÊAä¸_þjóJ9¶å¯˜8ôÓ:ûÍÝzë_΄nƒÏų¼â+f\¦Ÿ]»×Qa¿»à½o,\éø&àøØ}w_ëëæùÓýÝóçºq_1:›j}kSks<­øJ»8Å¡`­ân8Šú«û ³â«_Ñ+˜‚ÖzPâ� –eX¼ç}§?ûÌéùýï{úÆ#WNy×ÛžÓ NlÖãuðDÙ8¶µ4²áo­YÌT:aNk=h:b[{ÒCÊn0\uü´‚מØëõÈ_ínßzÔ­ò…ÝŒY–À0wAÞœÉM¬-l­Í,è�ÛtñFó¨s*ò•Ò@Ï¡Øj°ô"�¶¬œs-j*Û™˜]uk jîÁjWãÞE3AËöHœâäìpåâ;±Ù)]×�AÇØJÇŸ’áÞø–QŠ¥djBÕü§A¾¬æ‰�2"B޼™(’U1wבNèÿ�"›â©½}ž':}ß›¦¸{r¦¬Ç|O…«)¤\p'Ÿl%¢9ùs-­- '¶ j@•Õd–PX•JÙi¶‘úâˆD„4¶:í›ìÃLËøãA$íKø©0™¢ä X5<æë ,VøsÑ?®2…’Ó¤”ÜC†yHÚ@Ú>AG±A•=k «ëÖT*PÙ™!¼˜h’t(œ½LIø¶X‰%�4Ð¥­^. �”Õ¹5“âÛs©¥ãïЫ¼F´þǯªeФD9? ÐNŠ–_§ÈÿRÙ¼·Å‚#×%7§æ§X1¡c£9µòkäÚ’2¦+ k›™“³k� ¹87U|ù� Ôj(Ρ<~ð´è"ɇÌ>0;dmZéM{%J—ÑŠÖGLJÀq�IÎä¤FIÇ�´Rˈ۸Y¯V¶¦ÝJ’¸êùJú�nk8,…Rˆï®8©¨�O—d†–©�Gëå¯h­å¯ì´ôÃú—Ý�ëº=«§ðëHâºàòWÈ寖¿Ò3ã_qªz`ݯ#dܼÎ['«ëö8\5Œ4�C,?ýf‰¯šÕÐ8m·Çe0©ôÝ W˜OxíÆÿÞ}Çç¯ñü©ÛßÎ^xîÚMÿq¡ô(—ñÕaÎ_ñ•}Iw¼â+C½_éCð&º¼‹#g­Îz0ÿ¢ÑNžü÷ü…\ðvâuo( 2,†æÒó­¸2r¥-?¬mr‚ŒA™¿�§îWÝFˆÀÍe¿ESØaç·Ù™“€˜Èð/Ia“%xEd„ÈIT)ùIîDVæñA9ªÒ(zªDB‰ ò&y¶swÿdË”â 0yêR!Õš Ik­·P qPF¨.æóL@P¥XÜða¦ÓH¾’–N¨VRÙ ‡¹#þÕˆ$”™©�ƒFèÄ�~w–ó燹% MÓHÕ£XÊV‚a¸‰20Ö²;Òo$Pnå÷KÆ2W1é°Ò+ÊCþÊI�¢¹u“�úZN„=ÒpÍwݦ?B 1……ˆ)Hp2A§™Ô@c{‚BÛ¹1øÌ”¡qž"C·ì‘@c5�1!±z(¡<Êz Â#¨œ7s¤¾˜²\�¬ßªÈ (›ß )Wƒ¢mTØOÈbˆL¦ÖH°VÓä[ÎzhÚgHN)ÌHºéÑ’:OÓÊLsz奼©§¨·©]+ ÌYÖákÌ-Q¬Î àE"BpÄ“BͨYÕ úUL°ÔÁÞî¡rªIuô„¨J’�áà‚wó)ÓòWË_Ù‘ìÁó±ü•£J[8 —¿Zþ ¬çÄY®øª©][0ÿ_9©:G¶jå�I{¿•MpÏ<ø±“‡>zþ®÷½Znß×/œðüéC7œ¯Œ’üžñœÐlÿÃìÜ9¡1“ ËœË_9靸ŠÎ1!˜�rÅWMYË_Ó!pÝ…¾x˜ŸÜÿáSž9ýηì(‡ãG7~öÝ>=½bæÍïZð-9ÅÈDƒ« 2‚¶§éˆ<òWk=è°Öƒö‰µÄ&fè¿Üü»ÞnoÃÝE²²Ñj Á� XDzó¥ù°†p·^m±�W› @ôœ-0ÓÅ=:Ó(%ót ø_ hâ#²œ|nñM6úæ“HÝmORâV/ PrW£òp€f²*E§±m_“;HY~î挆c=3cà™&B>[“‚¡9kóŘ²–;|üÔP”#ž‰×­o7ü–‡ðÐX¦ƒ@ŸpW ›»Šˆ�qƒ§æS0ÏZh`”Ý•L øã/ʚà ní”& 7Cd3Zª@\;(.i)‰¤¯RQ*OL$•—Ø(€øÔ stžÆanö2ã;�ôFÄM0یڌ`mGwuoYÒä+Ç>WÝ©[JÓxÜ'¼y°Jîò $4 Äµ;ÏTß6¡,Õd-Yd„�×6 7²�žF¡€Ç–¨)�äLrsHqãk9¶’¶2KÀoذ™!ìü‰¨@aù+­¼ünÓþOÿXþбÁ˜Yþjù«mÕOs4U8LtªùSÜ2§·U|忚˜n˜<°ÿå[ç£üÛ�ßóáï?ð±{_óùÓý׿üŽÏöøìÎó¹¡ùœÙ›ÉНÎüî&¦®_Môà”¿õ²_1þˆ~V|e\‰)Љ;k=èš•�¢7q´”�¥§œ�_\ùÒ¿{ÉëÞöß}ê;ÿä){Þxß›o|cÙ²ýË]ìµl†su6óœ6µ“qïzlB£µÄk=hǨk¸luèýròW»Û�=ÖR–uö¬�u‚®ÌK+¤)ëÛ)¨•pÀ ö<�Ä„5}½ñï:hÞ(_ÎÇ›þŒzL›Øî&±Œ§®ô…;q9ºeÇÉÝöRyíJ¾ INŽ£°X÷»÷ ¤2î,�ªÞÌgí¤$<2G™"ý¾{n,v³œ_�P®CÓ $€…ã/ëøé•ô(àôPj·ýÕŽ@$Ô¨4Ÿ‰‚š�h‘šÏ¤"gÔ ~IQNHãe”3ÒmšhE‹8»;nÝ¢ˆ$@k1ÛFzÞšIâ ¦&LØCd2%â–O‰¸2âÐ7¡!ž½$a»ùÐc6Ø™CËRò²¦ª§‰”k»T~Rh•¾“ú†&µùX ÜÔÒ�’÷‚©©;¤@áìá94ÅñÂ`,i!Ô²YTLã�Ö"©uêÉGb�ã?Æ“aˆ‰gW™…Jh¢K!Ø ÁµíFJ'!ÁÅ6D¶Zy#À´RŒ$ÂVE�•ÒG^-ç0‡•VÈ!NVˆ}×6 Ìr(‘¡´¾Æ±\Å• Ô#03zDŽ-àDT ì‘ckYcO’.ÇÅù¤ ‡¡°›Q¡´*³ùôŸá¡”ºëÏKàüzÙô¤˜{‹wö?­i+ÈVhIr^þjù+:ÓŽUoYþÊ1ã`sÔ0F¿Ë_é8–¿ÊwæàW|Õtl4ÉÔ4ÃÅt=ï�ã‹xr‹TÜÉî^ÿö'~ï¾£Wýúº ¸ÝùÙsÞh¾Ä 1à~_é˜V|…™ôÒ+¾ZñqÛ¸­õ A.S5ÃÂÏ&¨W^êY.Žßóþý—^úÌé·þI·«WøÆæS��Nû.“uD”uÇíZÙ×z#¸üjÖ¢·¬õ ƒ†‡Œ½ÄN§ëÌ×Ñ_ínݺݒwVô²6~ ëÀÒVQÂT˜«@‰@0Ì0?co6C qÐr0Ù’TæoÚ×ÉW´·,Àv“€qe üðcÉ …,a�Nà ˆ:2vn—Ä…¾‡WäñH4ø”š�&³M¥UÛ‰ë9Ô­ˆÍJTï F4–¨ŒÌB"Á!ùL’”"K ”ÝË>™š¾o8ýR4:5¢ª7 ?QÛ ÊúÕ\ÙB£@pËÍ–EI/HQ¸XÈ ¥’•RÀL’BM¹Ò:�p×bè§¡EŒ 8ãG¢ZSíÅŠ¼EUE†¯Ùª� À¤�ª9…@X>ÑŪͦ?Ýš7`ŠÉ´�ø¾Pv˜H#)‘TæÑì¾kMªæ»ìaT(P˜³êƒÔ9|™ …`ŠL+ ;M[HV#(BHÞ?fw‚µk+–Œ§V#^ @…}Ï“lA‚Iêñš½Ë¶×öèÓ?¨ªkPäf=E›‚› ‘à/ô1Yd²UF+­­8Ö‹¶}B 4%à7HÄ vLrì‡�ø˜ †pÞ@‡¿4•!Oö]›ü“šCÌV°í�¤> ¦O‡…Ê düz ,R-�ÔSûVÀ"'áS�µ1ä{LZTÿXÁT*( RÆ­T8Q£å¯–¿¢O4*ê3ö öÔEbË_-µü•±CbÅWÍD3‡_¯8Ó0��|óñS_ t²æÚu¶¿1Sí¾÷$v{ù1_¿p|öÂóÝtÂ#.¾‚~Sñ/Aïlj —äLǹ)nù+fr¢;ƒ…Í`}¬øjÅWv�·±¿ò?ÛFéÿð®ÞûG¿ÿçsú^÷ö�Ó+$ßø²Swíö¯C§ºxÚ¡�`kP=QׄFù+àÁ_­õ sÁXq¥±Öz+¬õ =ÃNñ È_ñ®·[�L;›ÃœÀÀý7 O¸|5$·eRØscÐà(�H#‰ú¦‹sYJH<„Ojâ¹­CÜ$ŠËl™ çz¸N�(ˆ"Tt½s3-á¡)&žÂp‘ÓË-ÇÀÊþ’ªù yIÄ‘ºX #ê¨m„QÒMpV·V,uèG�’»“R\þ¨\~©(Rå$|T•”б™ÔÌŽphnê…á3jÒh™‹”q  Ž¤·´œ—qè‚—(úì©á´\Ìhˆ5H-ÈA­p1�»m”DÌ:Â&O:�²ðÝ£;Å:¨¿΀ÉMs‡°Uù`B—P;4½¹´Û¨y auË#=¢;ah~$ ¹<[á¼+þŽÏíjÚ^-9©:*3¾H*`�n‰ËXŠe1÷LH˜ÆG‰©1Ü®NJ¶˜¤(p¬¢%}þ5sY9D%¤ÄÊÌ¥Í ZŠ[A5'ì@«5Š‚Üç¤0æ,Jd.¼ë9\ $‚_Êv°–/”¸"ŒFÈ+¬É^8P馉–X^ð�!P¹e‚Ao%N-ê©mŒT«;üµÅ¼Ç›óŽ]¨çG”„S°¶Ññ�n¬áâüèL0^l9b§4uýl«ùàña®öcÑGœz‚<­®\#½‚ mƱü•-³üÕòW •†kµaåÀZþ ,53ÏŠ¯œ8˜ßœ[¸`ÒãÉfÝ̽q±ûá{úÆ'î;>»òͯ:¤^é`ûÛ=ý÷Oÿݳ¹é,G䬽ÅWÍh…MsÕ¯ø #­øJ§¼â+ãSWF.Žf+Ã=cDë¬ÎV«˜¬´·øzиÖuíÅþ/yÝÛã¨~y'IËG°CWCÙùI�x RS²’8ËN0�å Œ¹LºŠTÀF(vÖµ QHÓ c“ú,hŒ:8mã2µ±„×nž–„OêfOÊ\ÉêØš¡�¼ôÕ}”Ó/°T»EabWR[–øú#¾6N±€³¿³«5:Š£$�ŒÈR‚•%G{ ¤�®&²êljR1¥¬¿àQl •å¯j´Ú~ù+ûËòWz…† ]ƒ#G`ÑòWË_Ù%zSÎ+¾ÊÌuMŒ[@Ѩñk˜š ØIrÂËßü9åën<ñßzvõUp'ÿíý×þ|þ³|lÂ֦ʂƒ_mG±ÂНŠ+¾šWшïÖzÐh—Ø™—ZO¬k”Ï­‘ôÉ»ûë?c’¿tÙœ¹�l€&îV|åüµâ+:¦ó÷òWË_™¿¢¿"´3@çÙg/ÝÛã¿õ©+WȽ]9==åß8:ãekÃebj=àZê€×z°Éˆ^¶ÖƒN?o|þj÷è­GMZÑ"4�‡+fW×NŽaû-{�¶4�± Î (×ö'ü Q]Z‘­³ H©Ê&› ldçƒ�ÍC¥†Ì18#Áñ6òX1Ää²c`ãÐ<©ÈŒ Æ?-þG‰£Ô]±ïUH‘„”÷�Ìì…[¦¢^ydæÎyÐ(J²(ƒ©„uÕ�ÁÀ*‰ –î19M*@®D�Î"àp(%zL„|ˆjØ$&5§pï²{Èä‡â•¾G²ÀÈöpä›{xQ@²CšjdV<Þ�”5´ÂCÂ=`i$sâú• –¨ŸZHž¦rƒ‚Ú fc šËKË ‰Ì�¢ì'Ÿ<ku!ø Q,5Š+ri“¦`¬ y¹Úö˜0½ìÏs=D¤µ­sŠ�¡¥!/€5DIß!ªM¯Š*ØÐU‰@Ê)*:,«´E©êO-¹ÂÇÑU´#%ƒ�¬]„¤ÃNèÚ煂 –„"^jPÊu³ò‰šÝcëpö{7i"CkK!Ú%$çè:�ÆÕ7Ã,s&ŸÊÀÍöÿüì$jÝ �BsÐpF@5#›˜„®Ïµbzi}0lˆCÆ>¾n+�„Ú'†K2s“|àr TZ ÿòWÚƒ#[` jƒaALD•mkšž£µmAB£¨å¯ìrvØz´nCÖã´“¥Ë_Õ˦ßÔi–¿ZþÊû-_1îmSg,¾p�„ÛüÍŽî“gºù¤_¿ðsž?Ý�ÿ„o ú‡wpÅWÎK+¾ZñÕ„ [ÈmÈá4Ò9jPGüÂïZbFLæ˜Àùî?¾ë×ïùÜÿމ^|œ>ôÑgßõÁ¾ka¾i¡¯[К˜–¿µœ¨×¨N«¬õ a¬Gýƒ“»I¼m%å§wk=ˆ4„k¤×Í_í½}»ç·däˆå†õZ70�w”µà=Àál´qÛ&2îAÛÝìkQ_ÌÕÞ!'0 o90uyÇKuK$Pøã57Ú`:ÍÜçÉ:ŠŒýtÚ^»Vj EEJCu°¨ #©Y¢R¡–èƒÁL9Ú+[A¢V^ª Üá@69@ÐOé*“�*"AÙWd’ˆ�¦2%ÁMq¤ª¼#SOD6_›vKRì}yÇGËB^IEP€‚Š`Ã'¿~ŸyMÒ&hd) „ÑÔÁ‹™![©kT‘å·@’½"Ìv% ˜BÂf q~æ™PÚýXfR$m‡rr9êmç‘£ KsS†"œ�?´­ó‘ëamQzUS¢R c0QæìOËlõ©™Ï6Þj,NÚ#Êå=’Q¥¶•¨õ°©™a@m¯�Cý�èá°F:eñH¿O÷ueh»EН,”C J°ÏXn_3°ák¥j£C¿g]Aðùd]¥C ¥·Ró#ÝÙIO˜Q¬|>J¬ºª£ýÓQ #ÅÁôž¢*¶/ÃCê€`€A§Æ×É¥ò̓uG ä‹§íÓ œ˜!bã»GgŠ#¨µ"¢*'9ØkÅXþJËc m¤AlÀ寖¿²#,µün!× Õs®øªÉc|¥“(ÙÏ‹¯œ5ñ¬$Þ0!gŸ<åK8ö'ÏÆÙŠ„–š YŽòÙ(BÍàuú^=LÙO€"ËÖ? *\òŽt6$8ª¦†SbÔË&Ú•¶KMú³!©„MÃÍÅ�sZ,Н±ƒ›rµS1ðS9÷»ˆô)¨Ýí¥ÚLèÆïìÓ¤€ç‚£%%m±q+ÃÍÄ›f2¹¤=VÁ42ÁâÉd¹¦S2(I�Ã’ê3oÙµ`+Z¡hTTºò™8 Ó« 伌��fkysø¤1$à­Ž jeyM@é PDöå¯l Ú Óca‡m]×ZþÊžhÖDuZîì¯öB~—¿Ò*Ç¥Cwù«å¯Màzf½î¸ôFÌŒG�Þ»iÒ±…3·ÙV¼”—îdÿ¦Ž¯œÒè *§ó óæ·žú W˜�OO÷ß{€ï?=¿ò�¿ÈL¯pâëÞñ…?8¾¸Ã÷Ÿb$Í–…ºÞÉ—B,Ù¬½ü•ó=ÆYñÕŠ¯œ³õ+üy…AZX8bÞFëAûÏ‹¼ç×_òÌéùýï{êÆ'®øM ¤ß®ø½0æÝôWxx=0¶[ëA¦«mnÒ(³q[ëAg¥­Ïät×zp:Jsu“Pî§ÉÛõû/*µ»uûvã”Ó|ˆ–\šÐŒ»v½Ëa åN"d ¥ZãsÆ!Ò§ßunû7][¬mQüµÜÁa¥7•‚Ô¼d\@ßÒ¡4àe”ÈJ8jø‘(ñ�DñD䬣@�ݦ /svû&�ÿˆŸ ¥SÏYi‹*IG¤¿� Þ*=Æý(c�¶÷ÿSb ”r¨¹á�D)J@Ú†lĬ”¥a(”]øŽãèj…?Ç8›î!£–=H%J�˜z |U$H*Ÿ‰:™=�¡—¥Þ_…”AR ¤4E(É$ÈüRé‘h´2tº…– Df`QVP*á‚­ý�¹àÂò'‰¾O‚B“Xd¶¨7j�f�ßôј`¨�´ƒô2þÇ,]mŒ�½ô‡5€pv»ØAU¥*3çOص¹ J¶£D\¦âBÀKynýšYI’ãc¬ÞÈ q•!kŒæ´­4"ÆE6I5ÅÏŸ”ü²��=7ÔÏ ;Ö¤‚€òF;�4%aå±�(�f8ŠÃߌëç�B±§IB™™š‘‚´BEëzˆd…ײ‹Fè[¾‘«;o=NSZ¡…ét¶´  -Ö ~vøàF# h2ž|¨ÄÖt]Á¤´— 2­5è~*T¦IÓ�\þ*ÃØ6Ë_Ñoê£y¥za]{ù«å¯–¿Ò¯ø*?ùψ¯œ·üW†�„‰7@u÷›ß)È÷Ÿ>xãÉOüëž?}õÀ}íËû‹~øÀo꫞êEþªÉsù«b€å¯–¿Zþj‚iœŽþŠ�ÛØ÷øäé¿Ýýñçu!‡cÿý§xæôGÿòÃ$ßtF§ìz¡àý2¢6ôÖymá´Ñ³0k=8fÀ,_X’Lžc­[ j’µÔ FÏ/>µ{ô±Û³€n¤ºç >\ÓEvß‘KàAA#Ö¡«$ÜFe¿ÞžŸø¬˜�™Y0W&ObÌá¨à±Õ?_ eùR4kð.‹DHZ˜qÏŒ¼šð'!L&(ÿ¡VîÍWtX2šm~ËCRÔÊ(à9›bÌ­f$2…õ “\`,K%A.•Eß(QŽîG„~™B[68Y:Ø;Ì•{PD4E+kS6`è}í6 ðH½‘á¾+ˆc1Ì*¼0RPÄîᮢªeOܘ‘äÓRî Më´]T´ÞH�µ‡tìˆ<…aj‘pw](±¨òb|-V*…v㯂ê¹m0©·ûÖ¢<àÊ:Æ÷j’„å5ݳÀ*Ðw(˜fDÏa6\ªŠ"ÕöÓz‹öMÒ�!˜GwìÉ• diüíDÊζ%1AH17» ¢êq·±…¢œ{{qÅCÄ* mVD�\`ýH 1¬Í´¼SFB[”•ý‚D¶Õ”K ê7|øË=h¤Y³�Êͱe¥aBI€°·/+OýU>ÓΪ¤fÁIèm¶Þ$­/-µü•Ý‰Ž²ü•N[pè0–¿Zþ g¼â«™Gš†~‘ñ_¸Ð@3¨õ æ=p|ýµ�]ûèþ;ßÚ}ïÉmòûÙ�ý׿ò/¾øoOÎ~òÜGnž9E:·6o;56«º­øjÅWtÃ"ã*~W|…ÞöëÁ£Þýáý_þéÉw±kyþæÃÏÝÿ!¾iÁïZà±SƒóüÊZf¦µdQ寳Wk=èlû+�¿Úݾõq‹äþèr†Ã L�†�µ… ] t¹´ýD8NÄ f®lÜRe®R�S¯—L…®ÂgÑ��poŽ›Úd#?\ö[dJÇ*:‘£ˆdNÛQ+-!¬Úþ’Âõ=4¨<B¡XìƒçHd7=Ïw¨y’»Eæ¥�TèPL>Œ}uX%K)JÆ+±ø-gfælŒ9’¦ˆ*7Ù$U`ßžl�Bî‚B ¨ãÇLÇ0Sèb´©Q]2z}²Péäni@IDATÆmrPe[ÏŸíeš&ÕqçvV4nºD.d—A¢˜¾R€ItYj«ðèh—”[/fêI+ø¾k'²„¨ ŽÑƒÒfwŽîú²¯¸`?ä×úcr*Ï ÕÊ×G†²Ú’LVþÆX ¹SH™V°Ò'@,9GP3m]ɧŽÌõÿl�\#Q&„'|Ëm©žÕÊ`H™ÕÞ'eƒ 9·ÑÛ;fãä�4Y¬ùŽ�c[Þ<)i‰]Ñ6& Ðd–·2²£jOéP1ŸÔט‘Ô˜�˜M„ÅRÀ:«0ú Î÷ˆŠ¤DlF%§X¦p¿{BëëÀR7Á>ãë;´ƒ $+¤é”pZFS†‘NLŠTCÛ‡í a€žˆæÞ7 R�â’†�Gìe­ÍÀ•rˆiÑòWË_Ñ'èþË_5j0†¶h„4¼ÜÚDZ´ü•.cù+Ü,ÎsÅWv†F|ÅÔé„îhâÊÞ0Kàìw§l‚{þþ~çæ'ï;¾x­çOÿæ+ïøügOÎ_xþ¡ë’ñ»)´_5Ÿ¯øjù«_ïy,… Nîžß=cT\\œ~é�^òº·Óï>õôÇ?ÙÛÞö¼ô�í¸Ìü8%&~WV›¿2À78XëAº”fÁû:ÈŠ�Öz°0ѵÙZš^¡cÔ5<7Òaì*œ~�ù+8½]£ {$}úÜù¼ZôVä¹@™·Æž†b({Œ<®�ft[bRÁcƒðÒbsVvz—ß3¹DO­ô ®ù½J_é>¹.M!kbè ‘21—ºaL·æ°JçEj@Z„ึ¬�ȺÁ‹gSbÛ.í`1}˜hƒ��¨r'E•; R 0YCÚÞ-f‰®�‘N¦VqdÏI¾’wãíP¡RÜlöT-}óÝÞ`ñ¡Õ‡|¹•\+¥¶¤}øÀ¸tÅ coM¼"K=òÓ,VI’û®$¡ ¡F‡‡ºÛ¨õ2ždþÄ¡=¬¢²[ÛgË¥%ÎÈ fÜ-µü•}Â!dO²72Rè)öÛº=gù«å¯t³Ë_­øªÉåŸ_1'ÍTÉÿ ýÚS·¿ù/FîNxíú�øý§W¿ùÕmÎzÙ/€Û]ÜyöÚu‹áŸú+®ðWýSÏInù+&ö_-…Ã~Ûû+Ã;üÃË^÷Æ3§ßzøÓdÿy×ûÞø :ÆÍZ¶ i•°ÖƒšÁõÓ sÌÌ0®êÖzÐ¥§“Œ+Xç_çÝ_Nþj÷™Û·\�ûF-S°ÿé~,ˉ›Ý”9i;W5|x+Œ† à›8à°ª@‘µ6¾†®çÈú��„P$¦$LJÌ›xC]öQšxXÇŸ(þj3.%å˜(²Rpé’Í¥ÙF;)2Ÿ�âÑ@}²�{\'‡Ù¢tk˜p"¦ÃŠ2щ©d}²,HÊ+FŠ¢´¨Cp@Èdñ`hœ!ÔFìÓC¾À ^Y¢Èꂌ «Ža§<(€ÒØ!¿¯T–cfЄþú Ôµ®Ó‰É76‚íH»(Š •²‰å*;¸F€:/Ü0–IäBªp›h&þl½ \x§)¿ñ·]êœý]dA¤Öž!P‚oìÚ iûÆgZ~Pm0KIIÔ¡°u(/´)EiÙ�Üžqpgç,¬m2¡ÌÂÑl¬ƒÝZä`®¼QHÿ\' f%ÂrnÿŒ.¡†î¶µ¢ôZ#Š{êI�ÊY9Ž- /ãXÂÂÚçZÆ+_u=Ÿ¯Ö@ doÓ˜-NÝÓÓ!KÓt(¡QÅMyW$vÏ�œE€·(#”â ÀÉB›Âv°AHa v{©†‚Û¯À©5S[âZÙ€Ã]1×Pä&¬¸Ipd¬±¹�G¤b~µGÒ)+¿Ø¾êÂá?Lâ§Ž´Ç� 26fATäâøŒ%XÊÇEÂZ�׺!ºEs8|XqÍ/ßïieמ½“·f51 ºâh)S4"Oß)Û…Ù}!àT ¬‰(ä L¡= "žmaQ¶­cTyØ«‘AiTºÃ†�šw B[íÕQJë�S‚ºÃA ¹È@ñÎb7ñ¥ìéŒè&E7ý+žð±)ì`êå—kDˤp#I¨»'gj59�=ÎSƒ|ØfÜÃÂÍkòB4zRO^…Z@ì¼é-L[ÉDQWùR£øs-ñ¹2桌ñ¯÷(-à£Ô˶R=?ZLµJÝѪvxlÀ)²}ç†Äà€ø ks˲fŸ|�)š6C4r®¹QŒ.�FÓçuŠ_Ùð!d Ë_eû®SÕ¦uù«å¯k ç寖¿ržo«oͯøJ›ÌoÒa6zi|e‚Õš¼ˆ%XñîwGì?!÷̃{ò“¿wßúÍ×HÀ}ùÞ¯ýÙÉ{ßÿãwÈ ,7­ÏÆ};M: {»â«å¯V|åðpT¾ ý•‘n_\çëÞžþÖÜÎùì¿yï÷¯]w×ÛÕ+³טOÏñRåÒ€c­ëJ®Ûò³õ+¬ÉÖzÐÉǵ˜çµlì Øq3y—Î�#—²Ø©?Ó-]uÅúOÈ_í{ô1ˆºêÕÔÙÛÀ¸ƒ»Vodêœ[–ˆ! ˜[”B0: :©øbxn„00)� ­Å)äF$=©Œ 2 }᪽Œw-òÓ[r�U?‰\º_�”f�aÚb3ù•Q)x�èKÞ0Ò)¥¤(?%á8À¸<�Y’ÑqÈ›Q‹â¦ ÂBŽ%jÌÀ}­a(Ù@7…E©™d‘�ßÏÈ1ð+ƒƒû :•'v´ƒTˆiÀ#y”€§í�(e,¤Žá3– ‰j­—òPP@¶5çžJcKü7åmMûŸa¨”ÄÌ“*Œ©é®%g¡=x£®ÿc9"‚jƒÄšÖ�ƒ†>öü ÃÜ!�…Ë®B~…Çö KòHN<óKy0\ú ˜pi¯ ŸÉD�ò °·Ö‹*•¢Ô·ÚoÈ�9r¤<�ê µµDQë…3Vj"ZQÂDX}ñ‚èÈdOd„'¤ˆö"ØXÕ;;ò’vó¼®²9áf»1¶¹¼D–(·CP`:&²˜tñÞ‘Ñ�M×q× ‚S®ôpƒ�}Cž í:î-Tò:lìùêÈåÀ£E½Dq¦ ’~ê…6 q0»"(”µº—@ù…¡¥Õ¼ú:¦ÞÉ79S¡½Zþjù+z°�²žè˜°ÃØWì“v�寷Œ3�ÂòWË_­8¯0@6w®_m´8a9‹è™Nù^<;½‰;ŸÜ¬ã>ëüpà Ð7G|å{2róÏÃ×¾n}ã ÷ƒ¯ûáOßw|þjÏŸžüý·Oÿä?ìÎò܃7èHÍøJkf¬¬ÇiÅWúá_M¼ÄXq.â�ÓŠ¯ò1ÃII³Ð[ÆiŸ ù üÍ»,®¿{ôžœ~é ªz8Ðê雟¸zÏ›žî÷§t}î„ë-èmŒKXmd­©@q‘âoG†>š(ú‰C&;rO¥ži­ÇHÙÊÁxÙã4™v[ëA;ÇZæyo/Ë_ínó®7G¡‹kº�k\ö¡€` à«~Æo�ާßôk aŒ{q>û*D³×Uã…x(“Iv7M…2rì³3F‚�9 Påî;ò³úiƒO:Ð) ’àÌ%uhF}p÷ZIÚ(ƒbãzù.*PÑþߤ†¼ ÞNì’S !WÁóááºlÆšf”‰"ÿóil Ce½ F¡[:ÆíJj 5/`Ü)Ñ ¬Ä™jŠ>¢JÊ…Ó›"ä’BVÍ+ЬmÛ&a‘B+©§ò�q6Ĥ�$±…¢A²‘ðøa®»§(JRãËS¥#ŸÌb{ —žR#Ù¡騗B  êòU™qEߑי›«¬ ×¶(Dì{µ=ª¢¡Úvk7B£éª’1PÕ2ÚF%² %¥mBCò … Z΃*N6@Zõy¨@øtí;®üöeHå¬nSd©pLîHH[ ,¥ çp°(ÛØ�€ C½93€ÅäÇ8Š1½�— Àt˜ •ò€ÿ°šY•î ù€ -¨^`ôØjy§_'™ é@¶¢¯Q£�f¢/Š©£ˆ\R…,YCŽZ_ʱ¦¾}öwŽú­5š¹Ôyu>Ýy œÔ$å§öËVS*7µH·h¡Ù¦1¢º"ðVìå¯hu›sØ«4)Ö³kj{Ïtm­?Xþjù«å¯–¿ÊU°�z¼t½â«&Xœe3"óQ�Õ+ÇWÍUøÙ³\ðn›‰™õyötG.îø™®ß{töÚÏŸ¾ó pr~çÙ‡nêÄ�ÿ˜g�i‚¦½e+¾ZñÕŠ¯ mÞ†þÊ�»Íû'ÿnÿÇŸÇ—¯{{ü·?ņ7so&ýYDÌK“�´ Ä;Öz_nÜjLÇκá°Ðj²Öƒ.èhk=Ȉq)ÕJóuÊ_í}ìv�Ð�Ë43•¤®ží¥”»PwÝ…÷FJçÖ´y%á‰ù…YäÕ½A"³0_p׆Ñ(�Cèƒa™zz�Ó(¯Þ%Ü(%Ct¦/ñF*" ¨Ö“X«�kPkÉ,ÈÁTê`d©=9 ¡BÃÑy5>8Ê@©õRˆß²c={è“oÃ>Adæ½E ‹ EL ƒÚØvÒ°ïž­T>Õi†e¬AŽ3Pyªï‹W oåmê±õ \ib²�Q7éįºõ|"\˜‘Êl�ÇÐHh ÑN½UÏ[à�'õ$ä«ãòKJ8 ´XüM$.ÝR4ò a# 1ÑøÏ’ (°kçèd}°%ÒZFû`Šd‹p• ƒ)…­;˜/oç*!U„§±‹$ÉdqïqB¦Ñƒ« +jO\/ ) ûA"mFVZ e�BÂX#$N­B‰âXÏvO   ¤`Z§kì‰C²Jî*…îŽ7>ÄêÜ-mH@£Qfõö¡8üJj³¡¦š.EªYΕ8Z�‡:µš[â3„½†6ˆò¯Æ“zÓpˆ‘.»Àv93ȉ§ Y­nÄiŠÕlÍà'PœP Y,Êèï¦ÒБ$ÕZøâN—Š… ÙrRé´wT©Ûhµü•í6¦ÔNgù«zâôŸå¯–¿Ò%áû–¿bH0!0}èQqN +¾ú…ÄWNäÎÂNwºcÞÓAÞ�ïaàAÔÝÑ®Ý|â‘}ßÑÏyþtqçy¶¿5‰Ó86�'š«°Õ�Á–[ñU½W³h­²â«_Ñ:éø{Æ £¥”�ô¦^:îÑçâî ÷àêçþ ·/>ö×>úÃû?tº?Ýùe Wø?E®uXOå·Â nÞÜ â è41bªÂG—:k=ˆUÖzp­_ÇüÕî»uËù}óRŒ½Y”3*ñ_–RÅåLúA1B ßfyïpŹ€z:Ý^!ƒ«@îLP0Ê­4ÿ!¬¹"iA؈E‘á(w ¤Aò„RñzìO óùÁàFÅÍÉÀ/Ôˆ˜Ü#ÀÐ0#¢R”˜&+Ÿ32'àváv°Òññ2ƒH‘ìqžÊãooý`AÍ¿R)ÖŒŸ%–Ɉˆ÷Ðø ÐÜrhÄÎfƒ£èª„¤‰N")! ÿÁ·IÊÖ‰„ C“±ÉW70D ñ”!ˆÎL’Z¿}Š(-¢00Ih4†?Õé…ù¬±#)4•©0’),ü¥Ò6Ï裤²iIÅŠÒלP«À;~#Q¸UEÜ�%YNäÕg Ã'òËÕ*åád®) l8¯!@A½QŠÙÜHÈé.gô¿oAÕè(< +‡OèN�RRS鿣¿p“?¾Øíe7̳Øô.ê!©þ&%� ñ©ãz3wv]~=ì‘ÔÈCþtk>©óÞ}­V™˜“ÑÕ*üb��D73«kTe+¨;Dã¢ÕÒÍ äù]T³Ê,ý wRƒËòW¶†MhÛϧäfù+¦#aìC¬§Ù15?~b­å¯ÆË_-Ųâ+ü¾ÔáÀ$¯7möû¹ñð`éUÜ÷Æá´Ï‹ßNv§l‚ûÑ»?øýÓío¯õý§_~Çç?Ëó§Ïäæ6•ç½òVøôå¯0î DíkhÝúН˜ðÂ+¾r´¼•Öƒnà8:zϯïöuoç÷¿ÿéŸ ïvåêUpÇ~É)þÊ0¹i�á²ÖƒŒÌ¡#7æsÕ¡+�‚¼êZ®õ`ÁÉ„ñ¬ýºå¯v·ný¯§GÎâÍnèUËëz¨WŒ[·5ñÖÿH‡e¶k•̺\¢€ì!ÚtiVäÞί‹h¯b$p‰WÓ^º@�³¼v›ËÆŽÏ¢&W¤D1%ÅŒ;|T ½˜Ù�™ܧcmŒÌ»ù̦.‰ËŽrçr”•6¢ íæ»T�± ©ÞO²,3�J»Ì·–‹‡�TV;))|¢ œ7.åîyÌÉ9MôJ68»‰l6BµER·Ô\Ǩ‹LýÏT…©ÿ!0n"ÜÈò –tæ¾ò((½@†l€¨¼%@aK‰‚� å_6u‘ÉrI%'ðˆU ©;(HɶM¢HmÏú–©8€T]‹­ BLI}ÓC‚2…‘{'9ÄÇ<""¬}XJÜSÐð–!d*絸LaRòé]ÉŒ”\ªªíeܬri“ci`ùë…gS(Øp¦sm¬&‹¦öv}%�02+�4g�Ù*>bŠD¶Yªà aUˆkYy( §tæÄ7òdkÁƒ¬’P–I-'ªüîZ­ä„¬ÕTMz-®†§Ê÷š;½KQ¢¦þ€éWºýLÇ$ !Zl*€ÓÛ�…dèÒÂuW êÎ.t„¡t‡ %eWå“êôµZmI8JÊÎ-?¼SJÀ+¸/UÛ.Uw¤›8vìMÓÉè‘Ów–¿Zþjù+½¨Ù%|®N�Q¢[ŃàLgÐ8v*p5Õ:/¯øŠùLÃ̬«ÝÆŠ®}±ž�‚ÿ°Ã¶ì{s÷Éþô˜÷1=síú·ùô¯ùüéW�Ó^éØýË»‹;Ï=x½¶¨Uh”ƒ¿r"7Bƒ>­ÀB@…&k¾’c.\ñ•¡ÅНìšö»ˆ#Ú¿_e„7ËzÐ…Ýñ1¡üOîÿà½ôû/ö¼îíÊG>ú#ž9eß_³Ì7¼àŒÉwk=ØB¦%ÉZÒá×zPèÔìÜÉ甿"õv눯,FP"‰VìÙç®qi-~˜Ú)`âWjúqAÙœðåýÏI8 !2Á„m(è½klVÝ\oÁœ ŠsC‰º7h�ƒ“‚¬ñ3.Åãƒ')[PÎÁØÃ˜GŠAI©z5î¼²{`ð²ö½”Òö"Â>s/O¥Mî4�uJ±‘`´p'N9+“¹Fâ)8R¨¿¨ “¹R6[«¡Àã-vDoÄH&•Q‘D7�bAú„d^ŒÃâÁ—MÉ3³*£ˆ‚(Qˆ¢RZÙ¨fr©ôRJf#¨YÌÄ"-r‘¨—%?À-`åÐï]^�g� ©”MbÜA<ÑU¼b“c\ÀºNTÖ ÑEZ"¨EÃeª—Ê™Òáz€à]Øq”•K|;?}&ªºÊÑlò`I*qÛlÌ'»ÆöPZ©4NÈ01m«u(§Š<�ÇÌŽ2r§>�ÅF9ª¢-‚Þ¾„•jc±«k�F[ÒŠ=Æâ :"p€–iÐL¹r¡\þð2¤oBv@Xé©ÏJ% ‚„ÁÃCˆÆ 5qÙÀM+ Ä´ˆÙc�xÕ"TeÀ',Q‹]e/  2�×´M’['S_ó‘¾òìüÂñnKkª î`�_Mk%l]=R�Œ ¶¼µ”±�å¯4�†[þÊÞµüÕ¸°F£ã9£,µüÕŠ¯Þ¸øÊ™Ê1ÈñØôæ¼ç·.øÝ Ìfw÷;Ÿ?½‡9’÷%ïIGíËŽ=ÏŸ~ñw³ý­Ù—qsr“a?ØÏÿöÞ5h×ë,ÌÛû{ßooÉ64 ƒOe8X’ƒÁ�`I&0m~¤ÅئÌJÚ@¤ÆùÑ™–¦LK–”NR’’”CgJ2 06L'¥™ )†Ðàxbœ (†NpðAK²­ƒ%íýz]×ý|Û’°ä³-›õ|ï÷¼Ï³Ö}^kÝë^÷ûV|Eœ€V|µâ+æ?GD›—60!¶˜2¬üL^¢‰a37�þÆîzÌ=§Üü²û®}oZèíÊ»ƒCn1>/D_ëAãåµ\ëÁ-$dT´`nQ;9 Ç‹Ðf�&’Y»âtýnÍ'4µûÞÛní×Ëbw×µ8)]U‹ãèÛ¹d‰Kd×ÙãÛô:9>&O Ô_“*0¹E;aΔðŒµ>YBiqîµA“æ‘[R°Z'#d®C—ã"FFPcŽøNñb">``(K𳨟{eÆ+®RQ*þ0qåk‰i %–R’轤•û.#²�J¤æ: d:åQ£. JwI)yÅ•àQ„'õ…nMÖ–ãÙŒ£|±§\Ãnç‚kµu/G•R*J8u fZ5ú—†ªGL-P[ �ÕF��5›Däy†$ˆÝrz¥äE5KD&Fºð³)€¯®¨…MX]††z5¯ÆWtY£"z(.…†£¢Z,”Òp‚twa8• ƒ¼Æ¶NH™±G)6Á=ò[)É»qHzɯ)Àä“ý‘ˆ8›†*0#/]g.ÔÕÄp|ìœØ)bïQÌ)çPãн…Ԕɚ%DGsiÏKUá ¬¡6�Ⴏ~¬9ÄSµØPšpœ©PŠ®Dc3‹`ؾà“çØ\œ�\άóq?’õ*OÓµI¢�”É2d�Šôrâs"âÕ »ó$ày*Óû®»ñ®›_ñ4Þú¶_ƒÂ‡Ü¸üíª·¾ñðö·?pýM°1 ŒÒø+i®ø ;;±/Uˆºâ+;Ãg�¿Ê}à„‰“ÿïŸúc�{»ý®—¾rÏk.ò¦p ‰´­-t³}8¥s1¸fWˆ­Ó_ëÁµ\ëA¥ ç˜ODþj÷š[ne:gaM˜Àž“�Ó˜A,¼ë͘Â+wŽY½7»9*ù7C⨒}A€Ùó§<­KYƒ5cE1Ã~Ö½ÝàU¥K\” ç€�}Æ@ 4¯‰ñpÔ¯LbÖ ÎA”=¸šµT‚Eâ¦E¸�¢¶£Æ»ûNO/«¬šKÁü‘Y:Q(Ì{/9åÈ*mãabHÇÙ¬03ÄÑŽ£ª½Ä7­LÓå<å]­¾m†äBQiè·×ÎBBÌ1ö•® rÅRBª£’h@jLTÌW`% ø –((—dy±´Ö°Œ½?ÑFÐt�tylôÇ.š{�½À8’&J Q+Ѐš_zezej¾-%]U¾lòj5ˆ‰�§`ì<“"4UˆÞQ¾ÇS„ƒ‘õÉÁ!UY@â�\h¨…Ì᜜LcÚ2É ÐP9kd‘•\}5��%8—ŒöP4ˆQ*9ˆ«êSòÁ¡Ús¤š€IÊ'AYS°m="Ö 6ÕÐ<²f’p § YâN"*>š7,À“S4ƒ¶¸J¡l€Yœ”TÅDó©…v~û X ^rC\Š""±:�0Г�Ê[ 7ñdÒˆJÀX c|¢ñG=½“¬8, bà 'Í#œÑÉ©×$ OQbq„��{ù«å¯è+v˜†ÊòWË_-¥C×»â«fç“ùôå<ôi�¯àî¼wà=§äÞxôbpôÞ¼ä/õÓÏ]|‚ûOîzçþ­oâò·ûyÿi“g±‡3ãÄWEªNÜÍÎL—+¾*šHÛàeÅW+¾Â?Ò>ƒ×ƒÄãèÀóŸ{ÜÛkíâg÷œ¾ó^ŃޮòU p0ÄÚ„ìÅÛ”ÿ[ëÁµ\ëÁfÊ®Ö̪ÍΟ²üÕî¶[_S¶¬hÈÉ›S‡8÷z‘ðmÎå­¶Ïõ$h®`›îCF†:Ð]Êãœeásôñ²"n�ß~Ët �/t–Tzi='¥m惤ëI¤GYÉ ¹˜�ç™bŠåB^�y&=UªšÊJÍ €ßÆžt ¤ülÈ6Üãämš“Š4¥¢Xª8„_ d A¥ò:eKÓªVÜ„}³Ë–¬�=ƒjJMŒ ¦²†Õ©äζÒ$œ �ù䘤‰ü±ÅŠ/¤T¡lï‘ ,qù÷{N¤2¨ääÒí=Ê(ÈÀJ•jè%šPfI…¢ÓÚs$�(íe‹Šà :Fç3‘´�i`ñ…�Œ$d™­ Ä¡Œ`Wêq2„/Ó5¢›&ë_ qó�üÀ…öÎÄž¤õlÍZGM°ýè%t„Uî-¥^üÁÜx`C3I|g-�í#R±§�˜À´“˜ÚC _ 2òªgäø¶(µâi±a…)±pPä?Í¡­ŒÚR[ÕE!úÐL"Ši:É£¼ÙRxm’RÂíöWÛÈF`·uIxqœ¤6ç˜#{©GŠÏ!'CO¹Ýl“99¥,ËS:ª¸·ã@›ë�ø6õ_ÈÕœ›Tør®”ét< è:Ï”{*ë寴ÃòWXÁþ°ü•£¶ìùòW¸£å¯œ{òàøW�vÓ¶>”Á¢Kåã£O4TÛŠ¯ÆNÄÎ1Î<ÎîS|4œi¹,‰�¹77oD% wÏ—ÞxõùËŸäõ ¿óæg¼îMÀÝp“&íä¤]Ð94kÍd]ñÕŠ¯V|õÙµ$º#~'νtÍs®ú‰¿9ÞéÊþðúßíó{ÏéÓúú—›¿ÒßédÚñ¥÷“Õ6=¬øÊÁµâ«zÆZB1P>šøjwË­·ÎcPµ´uò6_bB�#b ×às e_,p¬‹Y»»¼g9lþ¤±IˆkŸä@ó\`²8ŸøŸŽÚÊߥù�)ëÖ:›•¾ñ ñ1ß¼ØÂDÏ]ÚKÉ~Þ[©Ê+( i ¹uNPÃw$ÊT+ˆžët²®íÓ ¨ò(°Dq½?RŸ*I—vÀcÏ¥²1ªiÔYr&„¤ÇfžDRbX#Ž*¦Ñ’bÂO%ƒ¤Š ›ª9849¡q‚Öƒ Å i%•wãT,§C–D‹W.û*m0¶rV¦X5dZñ^ Ä‹*éHx2‚IÍ#JÊŒH/ Эݣ›£+Ä š¥£¨–z6�2F�ø°Þnç´Íä‰ÄmÚR)69,0(Ž-Uæ!Ný%Á|)¨ÀÓþjO_6&ÖPÐÚÚXá½Y|-†tm@ûµ!m‡Yî‰_‚€€|@JU£Šnï²lÈ9d¸ ÄÐÄÙr®\J4ì•Dj§Í.r¦ÞÁƒœ)«98Ñ`’†Š “‰ÏdP¯é´A\9EpL…pYÐqY›Œ*Òð¨YZ±aDÑÀg›0eý‘Þ½ÍTw•qú‘ÓU§¹mEúCÀ¦¼u@A8”ãinð†’£�ƒ¬gA@ðÕ(šËs•g³ëú¾`–¿Â$ ƒ-e·!»¾ü•CÃ>Ë_é9–¿¢?àeW|ÅÌó鉯è†<ÆÇD¡»ý¡€»pa÷þënºýå¯öý§O|ÿéî·åàèaÍÃ×<÷CÅWNŒüõž(W|µâ«_}Ö¬ÐÔÇ'ûß|Ããî9=ú¼gÝû‚—p;/[Øï÷Fô.Rìþk=¸Öƒk=ø”Ê_ín½åž6?ë5ãt/R�…-»a;)'WÑ.{Ǹ öÛ¬èYsE‰ ×á`—' ¸…±ySðIÔ@\?»Ü‡ˆ‹è™Fp™`b£`nsUÖçÂùߪ\L|ùå*ݼD°­¿$,J }ó;›��”JUÛ³®TКȇ™¾Qs!\ÝK%€ÂŽ÷q È¬‚Þ å5ÈIé…O¾¦~ÈdHù+ô�À’RŒ`æUT^³–Œè÷ÒjØCSh³™ÇGJ\gJ;Ò$ aæ­õgÅ#¶¥©»N+Á¦ #@Q¨R(Ú„S%äOŔ̜�¬C±;xè0�MT¡œ(ÛðJe)²uŽƒbÖXŒœ“1Ð6ŠÓY¢MX(¦°Æ’ ÿ/ä´ÞŠm•Vuè+Ü'å¿ÒÔ\©¥l½4oFê�b?s9•*KãŒ^T£t'tc (àFZ*5ŠòHF†Û·D=ñYO¥ ”M»=d�2ÇV¤Ø¹xSAËQJ°âH«LÂJÖâl xÑYغhœ0¦ ¦”äAìbZ|�o†n©¾”MªÚ\á�ÀTI#)0æ˜Bû çv`Y`qø?L%M¶àÜê¨M†ÑbëüÀ‰ïTxn^–í™–¿Â:Ë_Ñ‹ìBô“:ÌòW œå¯òzÑ坯™2+¾2šüÌZ\]Á³_ÿ:úö•�6½óæWú°7’odóy¹ Ì-"¬úw}_t Í°pƒžð�ê­Ü�xÒ•‚�7ušÕ0¥£9æ"-–rô„¦^"æ"”ÄM²¨)ýù·¦t‚˜¢¦³$Ï$‘-•â£ó9¥ÅåhF m%ýTD3xùÍYnDRf�NAdK@Ja¥ ¨hj²ÑÙ@ bœ�]‰§¼1IZEYÍîûsÓ_6•ߊ,Òk•áõZª5 %WþÛ8˜ãi^c*Yø—ÂÂyæ·šQ›ž%§¼R³MG –f–¦tÜ[Çq3…:V¯d|lˆ¬æ_Ü�iºi¬"J°a)l�PúRÎ>aZ9b×I 9u±RÑ­D¡9h+_f}�ê3€WË%Ètî�jTJ*@ÙúuŽC”bT3z&�´›%-›æi Ú‘m½1¤2›‘¬”º!ý–œúð74`i8œ9 :¢Rð$9¯ój<þÍb YÁ-©ººSuòÞŠ„œV0ºtÅ¢]&Û„�„ÁK÷ {ø*[Í.|¶µ†ƒHÙ/’CKhY ÂA9ÛÏJŽTÐ�v\þÊî§]µ?6µñ4ÒòWå™—¿j¸:�ÆË_<èI/|/•ãÖo8׿¥W|…ÅŸÔøj¦³öòb¦c¥Ì{ }êî<)¸{¯»ñês—/<Éý§¿ýæÏ}Ý_~ð†›i?=?”œ^·ØÓQßìƒ_a ²â+ûÉŠ¯ìô‡þ4D“ø?K´�k`bÎÂ{�ž*ñU¿_Û…w·ÿáþõ�ÜÛ»^ö*2oó$É|‰Ïuâ)A©– ¡ÜøŠæØeÁ¹sk=ؽm =ÎTKe-zÅZj—~-ÿ Ž�¬åBoÅWaþj÷½í{è\ŽLýK©ºX#цîZª¨EiXy2Ä¥ÄÔ=‘Aãš êŸ4�ÍÚP™e·—tÁa.Ûw(?!ûMƬÁ°·”êñ Ò3ݦç _…\‘‰ “AÒÒ!%¡R®Ó•£e|ÝY†Ì©ò’ä0MsEñ3¡I�ˆá}¥@›DÍút§[ö² '(êc‰+$ÖšXÉùdGw¾y`WþL0OYO€ ©ÊÈÎËÂG;¥ŸV³ÐÛG˜*´¾i¸€g ÛFñØj0'” ×|‘�Ó#]ÐMž ÉNKt^p¦U m>ʦ<*‚4è¯H‰7‚ÂŒ*Éx®q´µ&ä[øš— Bº¦Ÿ#Ì.†úm�t-À%¦AÕ×Ó°<8ûS�Ó“ƒã`T7Ýõ0îzÉMD�d™Äç˜zû^­# z3„´5âÓ2þ�ƒøé; ßkW©…€¶#}MWäÕÒ Ì“ctã[ÛaK   ¼œ%ÒçTRXþh.Ž`7._Xþ_Zn\ʶóòPµ¡ÿª³ôÍh…¨=½+Ü&�ülèq^±°õq¬…}¾Ó Q@J÷JƒÌ—{…Iš ´¹¤¬('ÅÄÄꉶí!T†�«fö;Õ­;qî“܈ÅSäNË<�iRW) nw‘4³mÙÄ^þÊ¢%írË_Ñ)ì(ö-ÿì¥z&¶9§Çph´[-µü•�¡CYþŠÁibsž`89‡m£iÅWz g¨�$¾Ò5Õì͹ùà·yÜîý7ÜôîW|˜ûOyÿéîä2oJ¥!œc�‚šôœûb87nšîËÑŠ¯Â+¾šémÅWNñFÈŸ!ëAƒj•G®}Þþ-o8¸ëœ]Ù.¼ðÅ÷_ó¼=W½°»àƒÚ ¨�pÀaÃݬõàZ6;8-0kÔE曎å2±f&¡ŽåD²Öƒ�Éô–¿Ú}ÏknÅÒ¸Ìì²Øåk¦wqâFKè–JCU`¢UK­gÓqq )Í“­¹\²LšÖ¢}'oVàÚÚ¼‡Y0jâD J°€Í5ϱ H!°Ææ*0¹$G€°±'‰$gŽAÒ†4ç�]A%tbNˆM€t‰.?ËH3¸ãÈÕ»ô*çÀRŠ:7¦š¿ÙùŦÖ~rŠ^c¤J8¸Š€09Áæ³äƒk§4& a8\ER�ñ–ÓæN‡‡L«$ä„V ÆbøÖlˆ>†F åXY™”,ÄÐPÏš°�¥—õ9òl‹mݤ@+j™i8å— ôšÞˆÕéS¨×¹+3t´¡²˜^ Ÿ¥RáìzŠ’zî¨G9žéEAMA’ Q…¦ƒŽ‚¥È"P¦e%ù¾O{è›Ró¥hà‹ç!<ú:jq!fE[�²ª ´Ý1h-­±N#$]}àÛÔðfÏR$Nq?7+Üê$g‹]Å⽟^v¸u3¡Öep[EàÛoCi9 ÈЮŠüEM³ú94À#( jD»¡ F�UDã9Ñà ¯<’´ÒbM­ˆ3¶ÐApë\~Û·…�óÙ²d£P’:,Ùh >"èý m�ÕòWÓå–¿š^dG«+Ò+—¿b|1N–¿ªS,¥«—®uœŒÿÍ ¯øªÙÖI‰Ï|a"§)gMlÕœöɈ¯°?ŒœÎqãÄÒî¹pÅ— š‡»÷˾új‚Ç“ÓÝ=·o^î±_{.ûé¿}prôà o6Ð R2T%¼ažÝBÆZñÕŠ¯èh3òíÛ+¾rd»è1š4`(f á'2Ëü©_! £ßë-Žþ§÷¸·ýÝ·sÏ)·›ò¸· ‡‡Üå]§º—³¸HäQ·Å…Jâqrsk=¨aZÔ¬õàZâf¬}ròW<ëí6Ž?C“qèHœh*ƇSæ� HœÕ­˜´ƒEþ‹Rõü 6×XÃ!Ö\£s–þr5.¢2$ÁBÄ7h S5ô¨4î‘÷Ú…D8CÓzÌDQÈWÁCXéè0A$þpº�°U~B0ˆ E9‘PLÄ’†;#˜Ø¸Ö7-195-¡‡ŽS¸I-ŠvQPe¦žŸS ¥’@›;á,iËúæ²3KG@º"¯ì‚ÅT™çÜÎÞcy�`çHC:‡»¥8œZL¬€#wµpCø®¸JÄ!‡Œ´’B&�ˆÀ%µVܬbÁÙ¡ 7¬,ˆò£ÖBp<ùe<Ò‚X.Ìwvçì˜ hÁµ Wá±Ó �âŽÈZ’Ê,7E^(¾Ê>uˆ$Ä)dó…ž]Ó(4UüÔ' qh²ÛEăªL2i:™ò’"Ç/Í&áv÷_ã{^ú�Wóô·'~ý—¿=ãµ?pp|é×{ÿ©ñûbý;óНV|µüÕ•Àò3Ú_±2àæªg>ûðó¸·ãk¾èÝ7½œg½]¼x/o)ïfìG._0á`£`­ �uºk=Ø k¬õ`s% Q"]×¾³6r¡kf¤¥©1ßc3caS;uþÊ×,Øÿ$u|ž;åüÙMçä2Ì(ªõ˜ÌL¸è6omçt.žKd–×°Gqù¡K’Rz¬·…p[öqD—¥ ÀZ¾ìpK¯Ó“c~æ+¼— ØLô�B�ËÅÌõ ™Vð±pq©¹´™r‚ ³ŒP€$‘kZÈA`H*0û©÷ áIýø bÄä«ë*&qÍ/2 †ù¡Y#ILqºÊH)ºP) .âºä2f2òƒœøÊNMoª¤³²Us8L5itŠ]ê ’l@T ¦aÎ1Å’›´QdØpеöLj–X½@³¢½iZ-<ŒïÛ(4¤íƤX+Y@#‘Ë1¯8 @¯¨ÍŠ]ó,â´.² ó2š¬CEœ¤Œ™'ûR'¹’y`ÛTʈ¥€ñ~XÀÔ^Ùj⎽ºOx;‡щ¢¥68¥r´×IB^aØDõf’†ØÄìxš`3!ÜàÅ›5åJ\ ¬¥Æ¼Ý˜6q¬ÔZ©O‰IKC!C)èÓ –_#,ǨjüÁ±„z¾°ªD!Ã'¦SààP�(ÜÀ±"%%ë†Åf ¹�e‹‘•ÂzZe ÒhÏz¦O±#™«M6Æd£Ò!§Ó�Ôìü ¶ÕÏÐ<£†�µbWgkù+ iŸ²ø`¯å¯°ÄòW3Âô Ë_-…‡Ð¡c=©SÅŠ¯žšñœi’À… Ÿ@œ°Àô·Žñ¸Ýþ½×½ä]/çõ —/¼íC¿~�F&ÇûOï»áFg…¦gC,¦i¦õŽV|¥q³Ž“¦QÑŠ¯V|U@m\jáI^rñO‘°Ö1ÍdHbÑä@¬¬ÛqzÃÃÁ•­ÙôÏ…TªY/¸H ,ˆ—T8*E/–×Ä×Ò9dLs£Ó+�`2›Ç§'pñO© è? <™ HžeLvñÁàÄs›O4ÕÛ¢—8…B‚S€… ëÂ÷ÍÍ‘â)4™„s’uÞŠË3ä¤êi¥hJ'G)�v@ÛÎà¡æi#õ'¼t)…ÛÆÇ2ÉZâ&¿mÄÈz"p,ØMŸ•–ÔµŠUŠ¥ùÄ MüÊ¡75©Y–Õ@®�ÇŽjÀ!mÏcïÁ®ÿb^P%_«I&²2QeÎ6e¯œ©l‚©’ÍK jV»�ìè[Ók´ÖZZ`Ç{ÅýRa:`Ï‘¬ׯJÂQYÒÔ”15õ x4iT3b3HÞM–Ó ÇF–) lF9Ç®CÝSé%¢Œc®1›âP dgÝkwÊÂlBK**R*�å¯0ÅòWŽg6;•�fù+P¦˜a9ƒÈq†�ÚØkù+»ÌòWt »^$G¬�¶Óäv«¨#á¡s½Nߺcªõ×Xb˜¸â+#‰~9r|1Ik ¬ãtÕñ“ÄW]JŸaMÁ1ßî —<8¿7rz/¯_8}²×/þ®÷Ÿò¸®¿™&1^°e˜XáJ0`CëœÐmß_Ñ&F1+¾j¯øjs�OÕõ âq冎š›Ðã_ÜõNõ•íþ›_vÿç=ïâU/ø–ÓüsNh­æšI·š;›,âÙ­õ`Ë;fl¢à³ÖƒvìÁÌù Í_ÍUo­�í‡ö??¦Sxø>ÉÎæhENØ‚à,ˆ+óz¬!ÈØÂÝ5ºÙ* ëã4%Àˆ¸A�ƒ!Eâ|Ü“D„5ÒÃ&ß¶Q°Ná7GŽñ0OËù–ëg#qa¼Z«ÇO’­2ù÷õ<`â’®Ï*Ê€¹꤆0ªFš€»ÃÙ9¸Ç ,„emBJ ê!›4\â£�(QÖ1a %»çÝ úAû�h(ì£á4­1$KªAÄo6m2�r7J^8(!—61Ø‚3ŽÛ˜PI� 9ÉÑ`¤š)P© H«m n™hŠcB/ºª$N"í¤Œ¬¬;…˺õ %•œ½™ J¢·%­à(§¸k”8ÃÕè?"~µiì#ì‰|CÅøÀI‰ÒF‚|(I|kà[—êö’Í Æ^¶ÕÛ_9ìRk(D¿�r �Б¥�!êù€eL¹tm)P”¢tE‘žy^GŽ KD˜ˆ¯`ʉ)°Ú1¹ÉCW ÍýF8q$©1�ã7v”¨§¤Ü¨µÝÐ¥ž¥F™Ë£wBØÒ‡)ŽB}mHçXãœ@o“%(Ó.B)^@©¤Ài8ƒO‹ù $D‘qð6'ŒKÀi“ž�ÑòWË_Ùb[ÿ°‡YfYþªQ7J“8‚–¿ZþŠ>°â+GÃS,¾2ÒdæqoΧì¸ó”g8Qðþën|÷Ëxý÷Ÿ>ÙåoûãKÄ_=󹆳1=;ß2õBÛ ¿¹3¹â«_­øªY±#ƒï8ÞO™õ ×Å š ÌóÇ»Ûß¾û­µ�î¾xÜÛ{¾æ/°îÏï|m2΃ÇF:Þî�=€¹o­1å´´îÖ–§És“k=˜e0ÎZ2�Ö;AFŒÍœŽ¦†§}þjwë÷Þæ�4Cà•2ÛÅ{ËøÍV´³¯sÂÝËm\!SäHæÛ"„šÎ›ËJ8Élédtð»<÷Ño9” ¶pï’Þë÷èø8 €ÕXœÏ ‰þBçîJ�h²Q€¥^&Ðã¼7HÃÊCüGX䇪«¤òù…2À$E("ýÁŒ‡PÚ¿`夌ϮŒ} «_À©BòuÛÒ�úlŒ$X%±D¯)L]˜qbƒ±ZbÉŠRVù¸‰±º;q% ,fA…TI±µœõ&`0ÔœB—ÔRí0ÕšRé³ fâ[¾ÂÏÿ�B ¦íi·³ÁU€Ò:ûOkä¡ÍL£@ˆ‚j©H ލµ‰ý‚‚ÉÎÈ£ÈXjF$O³�)g­®™bãˆûm­”z©ÔzBwŽBG+P˸e@IDAT¨ k!1PhAµ‚$èH_W†ˆWyÚ 6«Ii•·!¼n“MBîÓè;Ä“#«�íå’Ò‚LµQq´¼L±fÄf„Žf³sÑZ¶¦¢{‘ª£_P-æø‘m�[cùÏG »_`oŒ³ª�4Ò:òB¢±§>ŠQ5�`lg�µÌî>•¦-.ZÁŽ.<ÙÊã,Âd6èÀY6gŽ1¡4ŒT'©¥Mó 'º‰³ü•†Ñ|lË_M¯¥gÑa²ÉòW 1§^ºÇòWË_åOW|¥ÓdH4‡m“�3ëÌ­N6NDNoO‰øÊ!Œp>Ý”wžrýÛž54SmÛo5}×Ë¿ãiç�/>Anÿ;o>üç?Ãåo÷]#s„Ú5ó:‘OÌ ý_­øªž±â+[Ë0ìañޝô D.¢Žö?ÿ˜Ç½íï½ã�/û/¹×”&»;8ÄuoÏ À%ÃZº�lm1mLcëôkòµ¬÷»ü\ëAc—ò|Mç`ôOòW»ÛxÖÏktÑoÂ`–Ð\=Õd¥H%œñ¼&!f¥‡gI‚9óq ºÌ¦>:¶Ýní*|ÒVhá¢�B}Íëh–JÓzÇD‹~›�só y§¢{ó qb ŒC‘”¨ _rƒ(»6¥Ff‚¤FDbö^çDµ‘Hy§šá×&…4ÏØHe{ð=?G-LŽÌÁ(½_²&QlÄ´Üœ+†á�yCŽa ?´òlË”Qâuqxm$‡§¦ nÆ…ÈIÝy2ŸB ÒÇ:U¤Xê%ÂÀN£�®"&€VÒ7@ÄX¬.šbl wN°ÙÜa&–ÊJ%¦‘Qœ8š¸ÎzÀ)ÅÖP‘€'°¥%xð‰?évòk’é;4K´7B yî†av¨ÎÝ $î(Á!­WÿÀ’®|gŽJÕÎÚpëm0HxÄ×ÎqKXµñ_šÛ^el³[PR“‰Ö{1!ƒE+;-*¼­ðKÛ2 G *©b §jópäp õ÷z/DÅ,ÚÌâcŸ±1C²csj+Τ…ŒLx‹á?*K`¥C3ƒܘ&á¥;0F—= �»“í`);Ç»D½ÒÏäݨ…„•~q§I5uŸ[q9Æl–TäÎzÎ%O•¨å¯–¿¢{,ÕHkP0*Ø–¿ÊË_ÙðÙ+¾rÙ¶fkfJ>£â+§}›“ùÞ13Oóe"Ž YX[¿÷7>íôèÂïýÚ™ž�ÿæéoŸóÚ<8¹Äý§Îφ½:'T³fXæá™Y±�ê´¢NŒf:[ñÖ1ŠÁ~Z†ÝН°Ã™yñÄZ2 »Db(Ñ; ×:Ÿ”õ O­~øš/ºê'¾ÿqc~wÃWÝíóyÞÛ‰«è.Ǽ¹ß®²Öz°ÆYëÁµü4ä¯v¯¹õÝJ×Ìã'œRr¦´hÇ¡²M @–ÍE´OPoÕ Î 2¨�˜p5"ó ®±ÑyÒòIàþ ±!Wä’Ì¡q@Ã:\—ž‚4¬:A)uk‚mcÈ<„ÄÝ@<¡^TÝMy+Ŧ‘$CíÒ\Í«V)˜Äá0 HJÇ"‰SçSö¡M¹²)ÌÀ ÔFšå Šÿ©Fk“!†=Çò Bh§D%Ð2a‘2Ìg#iCʪMÑ)W Q¤Gx R*#ÉSk ¦’Ä\q¥%=óN\(½r% �gÚH[ž^ &Àâ$bLüA±¿¨ÊAM A°RxËÄ :ìHér,GåR:æ°S¿cg[°Ti°¦LâÚ ˜äâ¾ ÑŽ÷X)@X�¦´qO³l}M’& «e„1çgbMŸ?h‹Û/`À>Q¥§ ½8öSêmCzq ID>&;·!«fj7"ÒßíñÎÐ ¡NáIŸg!ÊS–V…-W(àá€Öu®**3Z MÃLß”š(Hnkv¢†rBAék %§K‘nG GRuàkgÁjTN¢€é#$,Ïp$2â W'¡h8Q4å~©RcF ¾\b´5‘…qK ÙÖ%bÁ‘2K «$�Ë_e½ÍD˜§ƒ�–¿ZþªAç€Zþjù+œ©qDÙy o¿â+GÈS4¾2È9=à2‚4ï$ÛùCn%ãò·Û_ñj.{òÜîäÒ}/øjcÛ¼™»É´9Ûé¹ÖYïÀwónáG+¾ÂYdÅW\•lãB×zÐQò©Ž¯ŒÓYãñ¿ÿ�7ÜùFí•íðîwßy3÷œ^ôyo‡$߸n†�AŒ˜FÒ®‹ø_SlÉZŽ-Öz�¾àÜ@'qáÐBn­?ù+_³ÐèkŽÅÞM¿˜·:ó¯ÃÒ3ö6ŠïUðÙýþüežeK›¸Üwøú{¢ãš�sMîkdjg˜·4ÏiK²² \ØËÀ†.û@0˜sÐ3€`HJÄ'á”Lôð�“‚>É îœº! – YiÝdF­ ?•x«`ŠŠrzT¾aÚ‘JȈO }(o˜Ò¥¿Z;–rÆ1žSµý­�Ïí�Ä·4P%‚¶SÑD6Y©Ä&ì¬G5–�ðŽHl5Їe”ǯJÝ׬3Á$·7´µ-·é©¤,»Qñˆ·ÙD×>2Ì,˜ŒhùJ]X+E•ì âË'ÎsS-¤’Ž:¤¢Ç8j¹CbòsÃŒ:à|j>Jòï¤ G©m‰.0/¶…;DºÇ$»£PÇžJÐF'¤Ä~%€jà‘h6BdOúB_>@¶+WÕ—'6š¨VAvzs Ó KJ&ļÖ.€ZW²Šï1IÅŠ&Œ¢r¬4vñÀê1T2´½¾s¨d›3Á�D›ÙæÊ;’1xí9Q•Ù !ôMGíM¨;.|ãŸ,ܽ×}õÁõ_uôyÏz’ûO?ç§ÿÎÁñ#¾~�Ò¹µà’ÎΗãÿW|µuù™±ËНV|õÔ[º®%þ‚çì_ÿ˜{N�¯ù¢;nzE�{#õÆ}§®L¸„¨{­×zp­Ÿ ù«Ý­·ÞêwÝ.Šg6Àb}Ö ™U³óåôìL�ÖRiÇ!Q…B²†Wêha+&çÅzq‡�caäS(5ç#¼cÚCa0�$ ÚôN¿t< ÛsÃP 6à´o´®Xþ«MÌ¡Ij¼Vˆ" ZÂbh�ºEú0´(VJvý³SÀHŒµ"V/ jº0R-•em€å¯–¿r1Tt§Œ!?lî—¿Zþʾ¡£ÐaÓ7ܱ58×éú©6çd±ón¶r+œ V|56tbúäÅWðpm[0^¹í”ç\ûü{^ð’wùÛ›ŸñºØ_~à…7‰Ø�üÔ]´d‹ãfcÚ-à°€iöüÇ_Õê=E@ƶÐ+¾2}²â+zØt¿µtêCpºO¶­#¸óí»ŸmÖÛv<îí_ÿ—ö‡¼i�?n:ÅO7¸×z“¹nb‡ ×zsØßÖz°rÄxúÔä¯v·Ýz+¶/¢2Úê˜/Sæf\»~›µÎ Ó½!{‹~ûñÌÍI^;"ºõ™å³Á›£#�ü deWéÌýÀDÍrc‹B³Ä6Pðx«ÜHúл„€c}È!YxzíÔ0óf+ÑØ\£P¥›'Š,´˜ÙsÚÀ”jÁ'S£‚Mª,âÞÅ—Nb« ù›¤›<`îÆqõq•'ÊÅUa€�8ªJ2‘¡ØJdGrCSN”Læ¯äœ™“¢%‚”ÙY®í R'*®¥œèÜ?¸Ò=2((tG£N"i¹d­œ–.§ óú«’Bá@ã?„äQzcQ“WÉ€1‘¯ø} ­®‚ÄÎ+æš'„QÑk"Îû¨nõtR­MŠ–ÚºÛ§j*Ì¥˜¥ýÆ –J—¿É4y&¤åéÉH&Î�Nu¤’>�X«–|Û;ÂVyÄÁª� .�¡A%Ž‹ésç�ÌZÆoèA†5LAe?u‚F.A„´«{Š9mÿé ¯5´ìÜe:~pP.ÙÙ¸%ºVƒT¯å…EÚÍp‘ °.¤¤6ÿfîÔ¼QŠSp�ë¬Ç§ZÒ$‰˜cÝ ª#©wNoí©ì]‰ ² Sl; *s¨yHãfbû`+‹¿ÏŽ9~ù+Í^#eòå¯è�½ÏòWË_åUtb¸âå¯t:S¼g‡„Î#MBø[ì¤fÜ8ép˜“<=ÆŠ¯>=ñU-g1�ûCÚ�¸‰ôW¿íÞýMïú†W?ýàÉî?ݽõM»£Ëï¿î&# 'Sh5¯Ò!˜µ™mÙ¬q nªþã«È´Îm:”'¾úà�}öD{ßJ ~Îï+¾Â(Ë_å¥tIvþ—¿ÂÅí>8âÒµÏÛÿÆÏé _õÀµÏ;¼x¡[Òq;âx#yÆ»£Ê†=!ã°[ëA ²Öƒö;…–TÄÓµÔ ÍRÕuö6Ui,Ng�%ź܄a¦ížØ_qÃé-¸2§]cC‘©Ï¬�VŸÏV^E.€­Š©GÃÄ}³5Ži¦×¤º±n)ËãY~o€^“%7Äæ7Ã0ö-�6º¡‡Ö‚/á­?(m£%í í}‡´ )创ÅL–¨Lx%�Z¢¹!#�³E&!æìw^@ŠØ)ÕàùåÏÙÃD”Hœ•š(éù*ug¾ÄÕ±XÁ]/%š¡Ÿ<]û“è�÷*@o@ šQ–®D††âdXÑT’–sñmëF�€ç”[ ë&C[�5�¢j J·¶õ.BЦu(Þq9ÙÉ©@B1ó �÷ŒÕ²ÂäÅ� Gè±(v=Å áÙŽ „DѤ|³ÖÆjHØ ý` ¥Z´«’çÖKlD%V/‘�•Ö »Z»‰ ï,ɞ擤S¡¢°‘`Âו$ˆ‡|l¬¤ x2€—b%B  ¢lý�Å~8Ë: óAîçV�ÙÂ3g§–rAx�ZŽýO[KÕÔÅag¢ HßÄ‘ì@j*e°‰‡~âË>�Âvˆk q¦™T�éd¡bmcVSÄ6œ„“^] i0 šA‹-,*ùS'vžØ·;ö«V$›AèW¢&§(g²/¥I–¿¢{Ø­—¿r”,5.wù+.ÁНÆ;4eë&œ;?Cã+Åg"u.åúCáÞS6n=%ÿÆß½×Ýx•¯_øU§ÉµñþÓÏýé¿ÍûO|áK�æ…��õüF̼ýqÆWÎH^ãÏsDŽ~ó—þÙO\þʯ#À1�™ÀPãJ@°üÕòWöĶ坨Bs¸ÄøãþʱÊZã˜UóáÏÿÔãSo÷Ü~çͯ8äaoxÇé!�ËáØ.“Œž™ Û×zp­×z°9ˆÉ�™ˆáÁÄ䟫)½±E™È‘ÈÙ'.µ»í6RoýÜàæò–Ç,ªýÍ4†ì‘ÂoDiõ>ÎQá\ë â?Ó)%f Ì Ã<€0&&Ì 0Ý–;!{sìíª¨/ä$Ë9;}Š)N]å“‘Àù{œνˆ(éÀØÖüOádLã$´ësEvwöxjU2Ë\D½Œå¥T@Àœ£.&âégÒ\P&% 1¾TÎŽÔ k�EM5%ðüö€‰Ñ‰Š.QÃÍ· ¯¶–â‘) þbìw¼(—¥òÒB™©±�Oc#Öïûˆ½‚4e—‚�e/qn¹•w2 2y7¿8ìXeÏe.\ ·?¯_x嫯>÷aÞzñ·Þ¸¿ãíÞp3Ó Oq~t|Å4<ñ‹]É>•-q4A|.ÓøÁ¯ÿ‹ÝßxÕá÷ýåÝÏýäÁ[Þpú•7=rÍsMí1…íÈ,qœ37¤éðÊ��‡uìˆTV|µâ«µd2 ™Ž6†Ù3Ÿý¸Ç½}þ³î¼ée‡\öFîm8Ëε\ëA×U®ýÆ“¯õ ¦pŽqµÌA‹ÎOMþjwËk¾·™ÍY�„3ïÙ<®@&Pfæãkè•8ôI‹4G6?ðš^Ȥ {0 Éa¨£`¼¼ +T2)ÂW%<Ën[ãs`nÊËd-Ö8%D ¡8RÖ`‹¶Á7à”€— A†nUÂGù¤UBÀè¨áknLöS'—RJ %?æƒd1" nÏ¥PùD È´„el[¢,”ù‰ÄHD­âxÄ$âÃ…rÓ€óe>H=8ã%ž˜ÐÑJE9ÔsÁˆ­9B±øtÕ×@)ÂÆfä„;ƒwfµ vÙ 'Í ÍnÆš3›O.Bó1Øâú‘áE 4®m7©P†S—:£jˆÉjkòØMðz*‚D­Ì2­¬ZNµm,K n=üÂÃ^©<ÕöZ2^ÑŠ%å¿syžòϹÉT8)†D¥)³ ¿Á1¦Cš„à+Ä¥»Ê3\¨ #{ŠòÊŠ&žÎ•�¢$‚´ˆgÈÐãì¢=¡‹ù+¤Ôç):Ò‡ÌíxH Òíä‹$l€@{$iP|4…ÅÉôxÉ�êÀˆÒ£H¢n—þí¦Àñé˜#eñá>æ¥5¯…öIŽÒ+Qm,O5&=–“¿“–j€£˜,"¦�Q‡>¦R ¢üó'íì0R´Ñ—¿Âz´Ÿ½ÐÏòWt}#¶Xþjù+ýÄòWºPg­¡·`dà�ñÀ+¾rB×*Ø%Š¡šÈô!a$N�v DŸ¢ñ•׳0ƒòµ“íÜãA^‹©À—¡¾ÿº—¼ûåßñôsÇÞöëéùøÝÁ{Þéý§'—ÞwÝŸáW¼‰¯fÊ.¾²Û| ñ¾gÿÿ§Ý_ÿŽówüÑY¾ûíïýÚobÚ6ªæ/Óã¶W|å¾ü•qù+Æ\ï‡[⢺æ9Wÿäßúà(;wnï�ûþ̃×>�÷œ’z;ôŠXÖ{nP~­×z°!f˜<½l­‰‰>•ù+wbÄêèüæÚÆ-Ö[Ÿø[“n/àRfòLÆ#Ì™ÍÈþÖîr™j÷bK³»ê6 1UpþôQÏÐ2è1f±kxó-.ûaÅ�\MÉÓ7€;JàC� *Öà.͹0ž{½Þ«D�·Ÿž?. ò€O"K te\±uLÚÂÄŒð/ÿ.Hs-oZÃ^ÈC„¨bPOþIbé3TZÓø#Bú%ùf4™B”ú§ÚüÌg¹Fæ(Á4�ÙÙsiÔ1#Úɲ\´u™B•EH$CªSyÄ2ÇŸ.(³Ë&Ê‹”LXÒ•:ôPP¡3–ãÕœ¼%Å–lb�ࢸ×4؈#„“D™T¨ÂZ {Rk!ë=âfŽš �L«HÏ&RQÚmJT%h³ÀH±È¾”Ôñ|ÑмÛA¾“ó²p¡�¯jàOC%ÍV�qFI¥C.óZÒ&žÖzÒäDŒºy�4æ2¥3&ðtGeHLàiŠ.¾CY»‡¬ÆæS& Hà(î=¶Ò‚ ×(óåÌíùØ.óÙFv·z£¢—>>¡È�öÍrRÈÀZ‰–Ž™¦&•ksDa6±džpÎ"œº)@½ßr¡) ‹vþà¨Zƃà–:p�­9�‘޲oJ#Å>‹iXŒ(]¤™6ÞQR¿Qf ´„êéƒ{åž êªà‚j¼ü•�²ü•pù«å¯–¿b ¬øŠ‰©yåOP|e0ËœjlÂäÏM§>cýéW?íOÛç|Îç¼ë?ù+~Ó_Æ.O´]|ÝÿòœozÞçþÄ÷=üàC—¹ävt阈˛i™Ð®ÁhƒiùÃÄWÌÓ„H�ÝÈñ=ðÀøÀC�<üÈ¥ËGÜ-GhfÔÐtN«9ǯøjÅWk=ø¯1¼HáøE/}ìPsEy|™w{Ñë á×z�0ÁµÞZ®õ`«éOOþŠ«ÞniõËÄjNÃÑÉ$ÛRœyЪæCsT²x®@`@Ç-˜'ÝÃà6õRÈÉ”ù#t0èép‘Œ$t.®§Ö=d,ŒXÖ�´{$S6J  s ³Ì‡ˆå‘ò’œ¨Šc‚Á_:“¸Á‚Ù�m“T�tQ„FÀ“kàß,xñ¤0QÅ�¼Eé~Q@¸¸ è†l †ç*…4S+ö° hgp#;=‚*•|Q-?IéRÖæ ÷a¾ ©Éÿxk&›+qX ²:”9^iUGù´Èp\�y&¥£’æïªˆK$¶‚MëD,ÑÄK9™ZB°#¡Ç&|‚Z3Ì¿:F]`¦>UÚèÁŠª~Ÿ‘¦¤à¯ô› ÓÖ¦ËÖƒL ¾þ¡œùgãì‰â+`šÇOþÙO>žÏõ_õ¾?õì=Ï£Ûï}¸F±øüFjL¼â+£¶_ÕÙŒŒ·nW(ûè¸ü1²ËUÆÚîŽ?ÜýÖ¿zôXÛßýî»n~åž7œrÕ›¯9õ×sc@ãkƒ6‡ic•…«!�«YëÁ…º8k=¸ÖƒŸàüÕî–[nµ[1Iu¹63Õä-lÍ3 8P²uBG( 8Ǫ¥¢òmÂHÉU( Ù›³™Ð޽Ù%¾|Æ›ø¥/ü2ýƒ75[°íñ,¥gô±nЉ4’·bPaXêÀU=PRTX³ àw=’ã¡Ð,vS†øA�š‘™Õ?¿Õ >�rßtÁ%u(!t7±V:Až¦÷¢OÓžI’éåERä;g=ĉëÅvˆªJ³™cŠ•H`2y )­¢°„V�d/>+³£8œZ¥tj!Í*ӊܤÀ¼jXÉ‹*NãK•mÀ–Å”—"Ía.š“¦yÙ*›p>Oíál2U ’RP¡èÑjÕØØªÚYTÅÐzþIQçƒÊì2Au;ƒF€%ü ξËNü)X%FHí£Ð¦)ÍòúÖXÌ %�>rr”Ø……œ g¥¡Ô¨ÉNé©ñ`Š%$Šâ¸ ®*P\T§*P"g%Ý£L½Ä¤¹7ó¦ 7¼Ôš%ê¬ K ­åøœÙ1.“Z“yÏ0Õ™Gšš’Ë"j¯z;T  å¸„“ ,êë«EH–&ŒŒ �V¨ßü ŠÔ¡¸@�Öˆ5“WºèŒY8# œq€'³\`險¿ËEXúPm– ˜?Ñc¸ü­¶üUc–Þ» (GÛòW�Aâ¨Zþ g£{ZþJ·»â+Ægt|źgæç.Tî4ã-¨<éá�å÷Áû¾üÆó׿øøóŸõD÷ŸÎëŽ/=xƒ×Ñ·0ŸºÙ1|b+>ÔÑòdñÛÃ×>‡w/ž¿ó�<9Ûvw¾ó=/}%RqÜž�²'®ø ;¯øªpŽ¶Öƒ�3£XãØ'Z2ªˆ‹ýMþôxÿó¯=d~ïï½ã/{y7žöÆ0ÛS·‚`7#ÚE“ÿZ­õ`K»µœ�¸�e ÃÅå'!µûž[naé\ÇÃð^wBZƒ£&ŸÚÔëdË¿%t–ç÷«àTØw¥c<�¸Ži‹ÜSd �Km=À¤Rt±R3ó@E|ë)™ÛÑ…œ»5¡aXlžÂ¥“þÃ,žd©˜‹ŸæzZ…µ^·‚:Vó)ÉÒ"=±P¨cöÊÐ ç ,ðú>{£ÔbÈÄÍ’%ðªÛ¤�Äeˆ—ì\|¥×âð¤ý'¦0|IBÊEØò ª3ågf›Ä仑V.â¡=ö7ÿfŸ�‚ †Ù �ÒŒäHIÏË? !ô…pŸTðiDÕÊHè(–™óe¶@ÆoOµõf^áªÆİI:\¾gïüqon�ÚÕ€TwxÉY6Jqfc²XÞ•kÎŽÊdL'a2¡¸œx.–-Á¦ŽÈ—ݵ«\HG¤™Þ†ƒ{X�µdº%�Ån±Cc«2Š:gœ~™Àã;á¶V1ùe�!(R{¹H6œã¯/÷CöÀÑÊe‰À^|X=Õòªp®çPÃZÛzB únˆ% ƒ²°ÈÕ›NÔVViÌiÛ'ˆ-(_•�'ÔæRD¦Ölɪ¾ ¬8䨍ÔnÜ«"MÎÍÚ(ìâ]©¶5H‚rjP(3<ƯöNTM´üí±üÕòW ¹å¯–¿Ò…7¿â¦W|Åó'=¾rÊtödÞoÕ@„铞v‡Äˆ»O€º÷/yÚ“¾~áðwÞüŒ×þÀÁñ#ÞpÁ§¾“ÿ_ñ(ÝâwB¹'Œ¯Àp‘ðÏÞ½þ±�{îØ]ÿâû¯yþE2‡¼�ÕÉßøÊŸèŒ”|ÅWqEY+¾2R]ëAFEƒÄ%Ê•õ £Ì�üô¡kž}õO<æqoÔ\xá‹xæóg\bª' ®fqÊBа}­gu”¿ÑcºVÂó¬õàZ².5ºvsÒûdÄW»ïyÍ­Mw¦8`WFÅO�sÍl IËf2gËve*#’Ølx„Š˜-@ WÁ9ðÄÁÎWɤV œ8ÇB›å·ô<�—+òH²£DÝ)z-ÙÍxÌ]w›/‘žPèÚ÷¤ $Åi_•s*1)¥ðTV9–OØ<�«�y ßìÈ ;qÇoV P:QYT…lrI¦r¨ÔcÂÀAÝ‡šˆœ™ô±l*¬‚ÒÒ,Zù€v‡?n[�å ÇšÃlŠm‘ÌÕHÅ3-¤8¡Ø®nöÊ(õD¡ÛƒmNøM�æB¼äK`¶ _Þü×,о¶‘LbYïP _Å…U(N´é�è16¿î£¬ ì7ÛmŠ"7¨�ê�°Úó�;@ù— 5b©öP#Åìqe5ÖѼ%W£ 11m-"dŽ+I"»š[¶PD•á»þeþÅ£dñþS“¯²ª9ü†ÛVo‹x†<7F¥l&ï«eÛÑÞå|±ÀÈ�õÌt†Ó¸8iEx ^þ fÂò€#Ù6v ªåUÄgu¹«„ VÆÖ›¦ë�ô q0ôTÀ—¤HIud„60•G§XGúü�‚a—¿ZþªŽ±ü•£¤ñ7ƒ¦Áà ZþJK,å‰ekºe^äœã_9‰:gÍXÁFͤN¥OñøÊØ¡©ÔU7.[ø”Û<ç©ëï¿þ¦Û_ñê^¿ð$÷ŸþŠ÷Ÿ^®ƒ©•XczÉÖ; ùñ•Ñ�õ=yèóž{ø[o8¸ë�šïlÛ½ç]w¿ô½çôðÁ˜Î½2¿þg ‘›²/.…ÅV|E? Ûðm�ãÑ0où«æsc^»ÉÉá[Þppç;Ιßû»o¿óæWšáöæn/|åºW¬ÈÿZ¶4ÓJ®kǧ»BbYãjÂîF”�ë|Îk=ˆmÖzÐÂö±æ¯víÖÛèRИ‰ïx®›!•¬+Xê©éøW>(DH–Ó‹Oyë X<ŽŒŸáL¸ ) )„PõŸ”sh!�¥à+¯ŒÃ€ºJSikJe-bW2 ë /qêâ/$¡¹Õl’ÉløhXjQ]QÁ#E _ªªaÀV}.n\þJcðO�a¿üÕòWv„寖¿ÒúN&݃>Zg¼â+­‘=š°œ}˜qœŽ�Uã+Å÷�ÙÒzO²?Ï¥f¸I¶ÝÁ½_þÕW“žìî¹�ÎòÇ7ï?}Ýß>8ºüÀ 77ó:ÉÇ@”�“÷‡Š¯˜²1ãé17•ì} Õ›MyÏïøúWùª’ˆ“ÁùÝЃ_a«_-U¸ÌpøðþŠéžåÅùg>gÿú×=z _óEwÜô odß.pƒ7cb ܵÄyaеl]æt¸ÖƒŸÚüÕî–[_Ó"–~˜ýùr`~´#ƒ3� _ÞÐX˜ò3ætÜp^þÔ:\x–ÑÍÌR‹Š$À ª‘¼i†ñ:Ùs·z+v9T4Îñ„E~IVBÀ"JÄÍ×XP\d~Dºò`ÕΙëtÿ%(&d„‡¬�¬ú†¨l²�ŽÔ¨ïDŽÐ�ºQ'"Y¨d€C&‘aQâEEJ�y#Š@H#T‚)É\%ÈQ1Ÿ¼”Ÿ<"¡u%O O:N0PEbÊ(¤á°¦VVÔSF}$‰†©]T�›¢uƒ)ñkQˆÈó †â$õ’V ¾û-Š/ÑMú íH¹Z®6·V‰—ëÚ�HX¿9ÒB|Hʺ”B™%W�pLXœ^˜U"u�T´4 Œ'ÒáxênÛÖÆÒÙˆ%UÛ2бZÚ²Ö�æÈ(óíˆ*6¤¥¬Â¸£•L¥¢H¥ ÚÇ"9Ž4R!l€ 5ÎMt¢)ê$PË`¾Åµê¡N+Q£@Ùj.¦†±dUsD^bþ4�²”Ø*‘Ý!¾Ø‘C”¡# <¡U´Œ€p !AÉóQˆ*÷˜“S‘a ÛÒg*ùˆãÍÅHdç´WŒüÔK̹…+PXõX‹°®” 3ârȆ¥¬ÝòWØfëL‰…i°�¦²ÃÙN6Ÿ½*#fB 4u6ô+8°0»Pì´ýòWË_5Ö—¿Òá-¥[ð_‡á ƒçɯø*‰·p®jÈè|õªNljfZ'ÍùäqéTŸ„øJEYŒÏz œ;ž·ö¾ëo¼ó¥¯|Úéå'zúòîûW.¾õ�û;ÞþÀ \1Ÿ<œìÎãR~l|ÅÄ<Í,îãkÏ>6#ÁÝu_õÀµÏ›ëqÌúÃãô"*W|…�í4Œ%Znë@šk¯øÊh(¹Öƒ�: #�ˆzwûÛÿ¸·{îØsÏéµÏ¿°'óÆûxõ1�«ÖEÓ· îó>ÂO ¥„k=hÛ0þm#\6#¿™c­ýŸ ëÁÝknyM#×$š+ôÙ™­�ôs³–›©›²üàz@z¼0o¶€6°p�\®$Z:³3m@ª{âXØ»I˺˜w´Å*ãâ\ú±êƒÄЛ /a?Tž¡BJ¥�ø–!š�ŽôúÒ£¤PЛ€¡"çjC‡�×&¼`™j`G±ÑAcAª“,1•š)å‰siT!æ}ÄÊs†…­,À^"{ÉÊP•zÁQÞüŠ>JADv›òI†ÈÄBi”WiÂAKoåS›�x­"eUG‚aP¥Èr–F +K„õb jJJÉ.‚îÔH|eºb{Ë%�,]í…V¨±—¥éP@깎|Ž¥äÁ)ÿÂk”…—�"ÕÄNšÇ¯Xj]/©Ôbñœý†Ëý2r×�@„™§¾Wó±›r§<ês (Ëi²SîÎöGß�QyÚg „£’nÜKñDÏ’¡ÄÍË=æ í(#NíÖK!)¯áy˜1ææ&h8h‰iSZ4ÃFh¨q9ƒ0¨ Ü •á-[»«ÆVÂTÒlÌæ²ÚÝ|ÕÙÔ[X9(¶WK(£Ä..Ní3L-×–’E+›X3ž½`„ 4 upÍÉÄM÷aSÐ*²_âüK.LÑ�®³Y`÷a¥*2…V³‹>ÐIéðÞ¤l!$�å¯2ÔòWË_9fb3¶–¿Zþ Oºâ«ÏšøŠ±í¬I°åc߸Ž;Ñv¿÷úoùwtÿé¯;ÿ±mw×»öo}Óîäò}¾ÿ”MæMïdãÀŸ,�®ÄWÎÈÜLzrò�kž{á7þåÁ]ïx4ÉÃ{nçõ‹‡ @nGF€)ÛP™àW|µâ+ú×Zp3�ˆa Û‰j]•=n=èPdÊ>=wéÚçî¹çô±íÁ—¾ì¾kž?y7®|óåÇ ÿï™ço_ëÁI.}4ÅZÎê‰@�`p­]¹º`ýDå¯v¯yÍmÌvÌx$Z)óåÔ<ãýlýÚàoIM#˜¡ 4`ÍäN˜~(uuN‰Æ2mA!䦭š.- ªX h�†’¤NÁ:�8GAAÞE:ø�n�£ìì*³ø+Ñܱ1ñÛkpUìʰ¹ï ‘¬)Ÿ/eçB7dI~Ê«J9¤Œt¨+—®ÚSƒ3bŽÄU^ÿ…¤H3À{êÓ”‚¯I0QM¾C;…3EIÓnmÒ'ñPFfažI²ÜºG¢ÀF"Ifù�ŒDsƹÒY‡8ZÍ›Hçiš~ð›Òðhª‘»»•tkÉ}ôžK¸KwkiF±ouP7ddNl÷éMðê+8"Œ6iM®€¶�Kž=ò£+q�l¸i'æ_3.pÅ4›®b«Î– Š´a¢ }üB?�ýsº�CÇîÍ�jö9³Â©ú˰º´óÀ_t;€+³d,VWþÑ”/*lNšI«ôø?›�³Àæ7\Eq«fNÒ­ö*¶?¦×C]ÐäR&͸“©Ê+¼²0åÌ¡Hý]ѱ"¦cÅ„ L9ç í å¶n OÒ®CNvÚÓÈ q`âÔ^hʸ†Œ”ÃfF„ÅwÏÇ{›i>è(1xRtkŽ$Õ €M-;M¬оu{…¥ž`-µüU”~S'[þjù«å¯ê8Q<ªª�]ñ•S™3‰3 {*½ÿœü Н Æ¾“ [ðñìDÞ”v~ÿÞë^rõéÑ…ß{²§¿}Îë~pwrÄåo’Ò„9b ;ñ•fÕ-Ó Ýsrzú_xø ?ݾí¸ç”ëqîæsv¹ô®` AW|µâ+"<LïZëÁï¯c..‰ÌyÜÛëòü{ÜÛ]/}…/9½p�73Úu³\pl6Cˆë(pmãÈ]ëÁÜØ,Z´ÌН�õök=øñÆW»ï½õVVÁ>RÍvS¥}L#;[ž;ÁÎ’ÝN›>ÑÔ—‰æñ{̰®±mVâœy]NT&äÜ,û…†ôgj9dü(>è‚ÙÑs²–7ßFHÈÙdnêÑêüC 6¹ˆ–�ÛDÁIÖ ÆC“=Œ½ðÊ‚'¯…޶qKEóî…�†3B)Štýããiæ �ãÙ† :�(JµM4ñÜx�q›L6W¸k!áIXø�«”–¸ì�M,*äˉ‡Ð€Ã‘L,à¤b�€nÙrÃ÷8˜®ŠWr›‚•¦ój+¶´@ÛfûŠžj�³ñÌ­@]Yާ0Òô hJ«æ(©hòH^ÈòS©}ÆÓÉÖyù4À${OׇËF :^>ìì’JY†µCZ9ÔÛSLÛıIÖÉHZˆÀ¦nˆAq’Ò¡«ùÐJÈðÔdøŸ˜ÐeÆÈŠ+¶¾ÅE—Q‘2*o17…Ð,&£è°–òî[ZS°¥sä$„6´Jš)¨0‹+ßžRngIKŠ|âÚ¦-LK´yZc…g ­ÒUÔ¦eiI]š‘êÉ袮ë%)‘šèžaˆãqZˆ MËF¾!,€›ÒœÁWBx°á ¢5l …[þjù+ƒÇízYú†jù+ÇÉ™;âxù«å¯š3pä+¾Zñ•A€“´Ó,ñ /&ao§çy—Áq_÷Ÿ¾ëßñôÓã o{²¯_àé@_û\\ïãã«-`ÐECŸi°«òñ¯_<úüg½÷ºy•ùöÆô†‡+¾2ì+hZñÕZØ÷2.>Ìz�Eìé3Ÿ½ÿ…Ç<îmïï|Ù_Ú“xãÊ· ^fê 7\*ÊÞbnéà•ƒq+z ŽØŒ^úbœ­õ Í€©ôlZ¦€k­—¿ryúQå¯v·õš:Ðî<·¶¹ùc)›?�ÚÉœ:~VÙךX•“,aîuŠè/QüqÔg#Úî)‘AîÄÞʩӇ×F“~ÜUV†Š  wCàôôèhèÏSó�¬Qà¢ßSèAÛgZEa¨›mÓm!aNY9HA@T�Sög,Åá2?S@é©)ÄÄ ± OªƒcŽ€P‰±ÙP=6ªR“3£ãQàJ]X#µTD§Èr(IS6SS?3Hâ)}_rì”Ô(–/l¢¦°å£ª Lƒ&&9àE¡hkÛXE §~ë‘ä©7Á¦åÔjXq‘øi/ ÀÔä‘lN,llj¦à-KãN!’ŠK¯€�8nàN£Qçñä…‚µ(މ6(.�äC�&4ã¨�̽ßpúò ¾`  pH†Ùƒ¦J$êÝ{0V ë¬ Îµ™t½ ¥’½¤næÝ6µÊ1âL‡~K:$NÔF™5´øwc.¤�”ÔÆÞ (ñ¦�XÙDi©ž“ô–�EvhÕ”NJVᘟ‰¶.&  2ÆÑÒØ¤w¥ h[Ÿ;Jcޤ¬<ª(ްv·6£Déü‚%©‚âa°Áq>‚:c(PçŸØË_Õ=kÍáߙ햿ZþÊî`§°_äC`Û˜´¬AÌHsh.µü•ÎcÅWL§Í‹ÌLÎZŒ ùê�ãϪøÊPƒÙ”ˆ”Û=v'Üzj Œ›P½0æ€ËßÞåý§Gßö¡ï?=ü�7þóŸÙ_¾ÿú› ¶ü)^kIuîÇ0¡ç´­×`ÿ–_~Ü­pÌ]wÝôJR~>ñ�{^ Ža½ü•½îJÄI¯øŠnT€³Mf/uÅ_9¡ÓaNOºæCä¸_øïÝíó.p»é… ¦Þ¤ìZ4ÔÓèbŒ:ª+WeÐ:vÝkèÀð-æ†ÉD ÜV$ÂB¬ Ãèb­ë¥+¾Zþê#ðW»Ûn»Í°‹ËÍŒ/=Ž@WöIÆ#«þT Òe/Æë.ò¬7ïÔÞc3â:ÈÜcµ�Îo_Ô8 óSQ0`ÄhŸ#ŠÉ]@µ“×Ï8kOz*ZÂòöE/”GV„×ïHcvò-³’¨qçú8Ô-éR*õŒU¿º!)?³ùlÀPNÞ—–`PÖ"l�¦H‘)ÓÏÃ7Ê+Á\o%Oë7­ ™ØJEÞÉ¢>~(PfþæÇ„8åK@%i+.-ÛXRÏõd€j„�?¤V´ðx$”›%lhŠæÅHÈDÒŒÔ�­n•‰ÄÙÚÊpyš™úxIdÍ�ŸÝˆk >žiƒ³Ü+©WÈ!UâʦʣՀíX™0“æv;sá×&u1E=F[{ŒôüËCÁ¡ACV‡ Ì.ÑŒ$�”$™ÓÌfF%öÈ�s§—Íe)\PA Á=¶×ÀÇÞ†^ØÜÔß–Ž÷¾î{äᇯºê*EÑ6JÊ΋äààþûîûþÛ»ïyÏ=wßûž»ßs÷ÝþßûÞ{¯}æ3Çš¶ŒÜ1�2‹ ¨oô´ó˜ÍNtÖVîûî}ÿ¾ýï¸úwß{Ï{î¾ç®»ï¾ëÞ{ß{Í5׃¼Ú»½îŽ{«a¢ùµ�£&>–6G5µƒ‡¶ÅH6‘“,½E"âÂ~*£2p  ¡³mÍy_^I*ë£ |ã>T ÛülˆÂ7Àì=ðSãÖ–u3èYžªå¯´”-H£bÇ*6£ÀR ¸üU]–……f„ѯì^Žß:)!åŒs»»]Юê'õÙí¯pªh½©=Ãͳú+-¡ccÜîœ.ì\3ê»@~œþêô²�÷(Âø"ïz÷·ˆÖ›{YþŠîªµù_ñ•]�M÷怓ãŸÎé�µ°èÁ~[/ÚÌæ´¹â+ÇrVá 3Í?£ WÐckyæ ss`\�f n·ï ^rÕé¥'ýÂ3^û<î/¼¹.ê¬ä_ÙfÏ|Öþq÷œÞ{Çî†?p�/[�%�ßïhGo>-Á)(³¡?Ü.µâ+COúÿy�µt˜yS ‘:18>ðô¤÷;õ“gÛ7}ý}×>ÿð¢É7ǘ«h#iP½Òi{eÙùCÅN öòösF˚΂˜Š ʃêÌ&÷épæh�› —\�3h.xm+*O~è?ò§ÞBLJEr^(ÈyÆ9=Ý_Ø]¼è¼!|ô4žH6í�Öø ÖôƒÔ9K ¢ŒÔ²«M=� KoÜÃÓ§³i%C¶û&y¶¨zÂK¹ñd+Õ!fͤçþé?ùÙù‘yû¾¸/üÂ/üöoÿöW½êUÀ�ýKLÌ%Æ�ÿø�Ìã¶ßÿƒ·!&³É½˜ 9d3Gm ÊDg.)ÔÌ’Þ¥ pz�?÷÷ÿáßû±ûßGüšk®yÓ¯¼I�!À?í‰4MG`Ì6VœžÛ£U++åê9ƒw¨ §öÂPüIÒ¼³›á‘Dˆ—X-©eÈ™_€ÐŸ��V6Ù-¦ŽÑÌâH)¨Ÿ°6çÄ …õTjVÐꥤþ÷µ‰ƒ§RÏ'òWrOj…W ÕúäÀÇŒÈéã–0Û.q�©>Gó¨\�UÐå¯l& šé>‹ü•�”^B´ª¿BG¼¨]ÎNg7°ºÁ`_ì]}õ ;‡þï)坸éJ'‰¤v^þíÅ ÉÇù+4£V-7?Öoßü`£ß³êãôWç!î�K¸~ŸXñȰàù«|XæXþÊÁE¯`g×aà±Õ}ìκb#;GœCòOt|õQù+rmN^úlŸm»c�`Œ„Ø%ÞzÇ·þwÿÇÿõÿ“~ÆÏþ˜ÿPÛ…×þ�Ï==¾÷?ûïO.ïy‘"aÄ~×Cò ¥Û„x}Å×í¿âfÞ”úhŸûý½;®ûšË—�®âÞ¶ããËç¹ëÕ‘_ÃN|µü•!ÊŠ¯V|eü©ëûPñÕžcTº;üâ¯Ù½õM�eÿÎ?ýïºîÏ]>:>6½æÂW/ÉÆPÛÖƒœƒ¿ûÍ7ìþ«?OÅUÿ¯ýßõÀ·ýU/†åy@äë€Å§²ªôå)Mן¸õàGå¯V|U˹À¤3<ÑzpÅWÆŽ–qŸO¹õàÞðš§RšUPNò %[˜3YàS:�ÍŒTÎXƒó˜y³  œûîïùÿÏü�9þ·ë®»þ†®ÿÒ/ýÒ/ù’/yÑ‹^ôEÏz–¹ –0%L\k�ƒÉnœ´&Ù Ëp™œ †["pQ’ð§Sžœsêïª%ûçæ¬¬“Bñš$`Î÷qUå…(нE £lp5# ˆ�*¦SLެeÏ™IÁ8 Ýò8yo¹œ@©†�„ÕD‡cœ*WP²Ql°JVqº‰V±7¡$O§âß‘ÍP!9D+mÑQVb}$Ë&?}€.cE¦³kAtËúæM¡!»…(‚í…\ˆ¢Â ¶ÅÖ£G&D=ne¢MÍ“„/[Õ†Âw~ç_ù¥_ú%ñ?ší+_ô•_öå_ö¥_ö¥/ø²ÜüÒ—^}ÕÅc,hfPý¥mGvZ¡/øÃ.F2]¦M(SHØH£P£ÍjÇéÃc­s{>`2ÒH'–Í�>—10–I�ñ#?üC?ôÃ?<`ìï¸ãŽïÿþïËÂÝ B@IDAT[Þò£?ú£P€ R(•=;{]=;°™2mî·� }h#›Í§,µe6xJP+Tš–4:÷ðÃ�‘|ì÷�ðì?ØCØK©8«ÄÝãŠê]šÂbä'ßaÃh­yEÈE>‰[gÇÿŸÚL”ŠO�3J*ìÀ|¹·ÃI@WäœËç⯤ÝA:)‚)»SÑ‘ªaM—âp;Z™í1âòWÍùì÷WªÉpg¼ÐíÔ^ìiÏ1�ì`¥ÔÍAP7â°Ù¬åßöi÷WˆeQ‘Î<¢Ò)´b3(®ø+î°Wd>Ž’?{«õ'Ä_9¼Ïû«Ç�†ºB(ŒÊ´ü•V¨i4‹í±ü•fp’ðgœ_9–ó.:�’wú¨üU1š¸Žî ,ß¹òŒìÛŽŸ¹mxùöoù®kÿôMOÿ?þ×'zÜÅ×ýнî‡þ ßù¾¿øÝŽN/ìùm{¿ÇizûÝ… —¾ý»öÿÃ7׋·ÝÅßÿõgÿÁ›î~ÑŸ»êòU<~p|äð£`sù+ì‘ÃvjÉè®ki rÚ+¾Ê-Îäô'v=¨'$L>wüâ¯=÷?øèQvÕüÆÑÑÑñåã“Ë—O/\(²í·ÃfÚœ)„Èæøü¯þ?W÷ÿèþÔ?ú�Ëßö]÷ýÅ¿z¼Û^äòÞfl@^4ZøD­ +V|µâ+]ÚŸ”õ s£S�6#‰�Y b³RŒ,GDaÓ¹A¸ÿÎ�Ž^gû� „>ÿGÿöí‚~ÄÛÛÞö{|®€í×}í7ó7ÿ‡þ?â·¹ *NÄ<ä«%¿—Úx{œ¯cDÜ� 4†8;×AÆéÊãF Axºw†JV¤´¦³R3QáÆZc.zR'‚Zè�l‚´}À©Î5“`QP]36”¸&EsZ çPÇj˜(+Á C8¬Ž�q²&×Þƒ OÝ�ÜØ#a@rº› ‘"@±<ðR*DqUÄfS‚&¦ÓæfÑXÉЧ̅ávùqRƒ ªªw�¬‰¤Üêm#CÉ›¿P ‡(ª%W®a(jãeqÊü¾÷½Ï³�rû­·þŸ+Hÿé·~ëþmßvÃuשc‚ÁJQÚøRD%¨¤®�±9žÊ=%\à„™Ö~�pþ¸£R¢ŠíÕ6¶Ïñ¬'û²ºÿæþ¿GçÝ„iûÅ_üÅŸû¹Ÿû†—}èÅb4§¬µiœøS™‘i(K´³’{܆(õ=¯7i–ÕÔjçi#öù˜/ÆVÚv†¨Êh”:ª+(;»·+•1¦b¦¾CZò†ÌFB&¢_“¥Òè––Ìö�ähb9 â•e‚ƒ_±v¦×hj��©:&"„Òl#h#ÈkP“TIÀiü|,þÊ®£Ooœ^ƒ8“Fø�/‡-eØõ¶èlÇVM¥W  \þŠö¡-ë‘ô’Ï E¿dÑKûšîØ.À±ýÈNÊw]Øqd̢֚֯óG–:>]þÊîLÏM�Çøƒ³Ñ®jH­Ìú+VÌeé(¡ ½}Yd›¶¸ ùÆj%  åGã¯ð[\!È@j,/©gîŠÂ寴tÅìxù+; cÊéÁ¾¶â«'Œ¯>FåàÆ¡ù{;ËìËø:8<Þ_ºtxø¾¯úsïþò›¾øgèéO|ùÛU¯û»_øº¿ûÈ_øoïûöïææ¶sÇ\ÄæváÓƒû_ôï?�–{ìvø;ÿúòuöò¥KG.\<< Àg’ÅKe.µâ«_©âôr{NÑþ±øÊ`CˆÝ#/ü®~ìãì‹ÿͯÞù§¿î˜Û›Ž X�eîgxA̵§Ÿüƒ¿ñ8Ô£K�<ôàN/^d�¹c¥mŒ‹!ÜÀv¤&W1³•u¸túˆÖƒ£¿’ºqŒ³ÁН4õ£×ƒ+¾šH�=}ôÃä¯>õñýÖŽ; è¹E‘ÑÅz’íÄe¥cêlAݨdTñC?kªT©4<øØ·7þË7~çó�ßò-ßü»ÿïïÂ8¶ç¸Û !]†¦kÃ-†¹ìãÍí²»ljºã�èÂQ¶Å>Ñ™oiAÁI/äã t]R–Ã.1Ó¹(Š‹#Y,W)z9•ƒ©®MSY–þÊ'„[\`-w6–+Âã¤ôL¡®ÊXý@#]ȪðÉ…® “Ö$Y§v‘.Ž/áPK¿¶!PAT Eã¸Ä(RU>1Ë3%•n‚>&õª,8“ÇÓäûó=ôð=wÞ}ï�ïyÏ]wÝuç]ô\„ò§�¸èÏ>@ÿ�º L”\‰‚�,†ßJ¼M¨„ŠçH¤>Çö3?ýÓßôÊWþÍïûŸüÀ°7Û¤pÒÛk6´áæÌ³¾S�ÝÈ´kÂZ�•i4[Î.“‚5}í£åÑA²e‚�8ý•ý˜»'äw¶ýò/ÿr“ç<=íøhFÔYí£¿¥©m4>¿cN�Ÿ*:µÝÉÆâä˜["©%Bö ]�9|‚�¦:ÙÉÞ›«}˜ƒl öÑI^ð×rÞ𪠋b(Ę̵Þf§ üQ gÿ;b‡dSÍ��nIªDÓ]ω˜îh÷炚z ‚ؑ͑Ï\–6pTDX–ŠáˆMTÊÏùt((1Öýèü•ý´Q&eµákôeà¯KP"¦uaév¼™k ,Qé9Ë_a.{WÐ û)òW¶ÎéÁ¾îB“~2ü•cޱf¯¡ý8w¾£ª:s�=ÓnS×G*‡rCšQúÿ³÷&€šÅÝþ{÷¹³!› 2¢(«àFØ$`Œ~". EÃàú)‹F —¿¨€�Šl.1˜Ac‘]4ìË È0ËÝþÏóës/w9ï0Ë ÿ§çÎûžÓ§»ºªºªºªÞ>ç˜Zòº—žx{…>Á09JÁ�[h¹ ETlJe¯\]  *¸"¢¤£4Ipe¢VÙ^É‚8*™KøÈ�Æ^iÞ§øW�½BL�GE’ò¤÷¯P¯ÿ�ö*^°kc�™°î¾¾žýý3úg̘9sΜÙ÷ì{Ä g^ÿÈÞïrÚ”¾oœ2ç+ŸX´hñâe‹–,fiìêèa;ÝÖ;NêÄk– �yãcƒÀqFYå{¥•…_нëª�CXœšx0vpÌ¿BY�ãM’×Ûì4IËÖøî‡ù·l+ú51¤}÷™ŸÔ‹Ó›÷|×â¥K–q»ÏÐ07�}uØÆ¿ŠfVŒÅƒ�½jì•ê±bñ [¦\çˆÉùÀõöÅ£¸Ú¦OñÄ“˜ÓK/ú暨Þ%eȬ„^¹Aê4”n¸aŸ×¿þ .ò:ây¸VBœ„I 0>%rNRD}8[¹Å4 ꃲøêü¨&Y¸ÌèìëbH»œ å9Ë›¨æ*Þ¯V Ú„gÐB+1·^Fz HhJeÒÇ(³1NÉx"Z !J¸”üã'ð5›B‹2˜Ø™ü)�~+$±æ<é�ƒ˜FVp’eüïÐÕ¹T™H±Œéã²(‚§o5—œd츲pÇÌ”°#0óüóÎ}Ñ‹¶Þi§wÜy§�vÞy�=÷¤¼�·†]¦ÈSÒpòRe(¾�ß;¹jñU¨rÓ1ŒY§¯œuÖÙüö‡þcFOæ¦0RĔы9Å!ÞSˆÏ=�}°4\ñ#@�? …ÈpY°ð#37÷rü;F~³à7©¯ù`[_:¹IésI›vÁx'�\i˜¦V"+¿þ9{”€¡Ö˜?.ª5Û6ðiKž¬yŒ dŒ-Ý*?¬ð?ð¬.CQ«`›cȰtäªýK?ÆVÆ…–çõ©(£àÌs%Ñ‘üdøôä³2%ÀS„h¹r•Sº$›M(si†‹Hœ‚Ñ`ßÉºúöJ‘`?P·ûÅH ¿C#ü:ˆzE ¤+ˆ� 6'fMÔ_)…ékìÕÿŸö*"§Èjcxv%3VŒ 33Mö Aú")Ì·‚¥e8%9B$øSO8U›‘^ト¶Ì!—ä"xV=¡ö*ÂÌý ùåÊ)e…|Ì^E=IôsÅOQ×"çé8 ö (^æLŽÀ(qpmnìò¥‰„?L’‚³”9XÔØ+uϵÇ‹Ô+ KFDêIá_ý/·W¢‡gíͦ�¼�”ç¾õõñ©™³f‘|ãßœ;ßø¡EûÌ/Œ¯ýœñÍÓ6ØgÃÙgšôÛ²�eDý,ôì›YúÖ#&µïûõ5Ýzù€eéË»†ÛƒøçÏÙÖ¨S¸ÐØ+>Â;òslã_±ØÆ dá}Zǃj,1LO×àÛŽš¤eœò¬7qÙàà2ø¦ÍÚÏB Zkr§?¼öƒK—Òchp€ì¹Y‚Æ¿RË¢39lü+ÍtÆ-yÜü•¡�Þ5Z÷—Eo4Z(jÉçŽÆóÓÏ 7l IüKuY#'ëí*ŸuÔQ?ø—1´5 ¢¤ûá0`À·Y‡Qs«°Åö’*ƒžáe”À‚þ9ÔΘ•bÕ Ú|�1²8�pu�+ÂìS—zQèt¯K‰AÍEP{äWØe8&2ÀQ+³æå‚(}ɱr­<Ð_¯‚[K $!Ÿk¦™åu ¢ Žv×7Êa¹WÈRD¤ |Ò¢l.ŒãÄb÷BZ@V’3ü [úŒ|þó“ØïPn�s� vZ Á±+〖!§£ò)� —¥ÀÀŽÎHZÉ!l3�åšk®ùË¿<”Ÿe˜/þœ;8¤Œ¸Ë šù䟳ZFq�$ÆË½U0¸üZäÔÂnx—I°•^Ÿf“Ÿá;l°�Mê /QʘLýÈLY[ªÁŽìhR?&(I41I>{gÇ4�Säma2ûFEþÎ×à¸ä°h.Š?GZG�ü Õ‰-Á·ÛÕ¢XÃeo£tÓ5ÓúU)aÈ®!8çSÑÅ­üÙ˜!T;@óêØä„GbTÈ¿tr€Ëk|m_SÛe@þ;#p°KF:•\›6{¥‚�•cfOŸ¨H�Û½­¸ÂnÈzÉ Î\«4VèÅp߯^©@™Ç'Ò^1¢c"¬±™þjq›.{4Mû¦¶X…1¿R(«Š‚¿Y‡Q6ÿ—Æhs¶�¡B¼4L±WijÌ>ÁöJÕW¢‚œ£Ž�Iö ]S�ÙnŒdj�1=öJk”µPý�sŽ]tÔÆ^¹tMö¯{õTñ¯4ÿëí•zˆ®cÚxh�jãýh½3úúúgÏš9‹íoûþ˳¯ÿãÞ9΢L>ìÿÆ©ÏüÈë{¯¼xé²¥l·Á|<²ÅNƒ[ÿɤvs/üGv½±ímhéߊ¯¡¹œØ®±W�ÕøWhelk&Ž0eB<ˆgÊ{|‚ëÄÒ÷ë«7þÍÏyÑ ê5èÆ7Ü8Ô%Œô~»Î³&ßmÊ~ø¡ÁAîQ-öJç»ñ¯d] ܘ6öJÉŒ?Ç'Ö¾±W˱WÄ÷ r³e ÅB«`å÷n9  ÑËÂǧ·$Rß5)ÇÐn:Ëþïî¹ç>¼q³P:üþç~Rþ �ƒf‰8¹ oNÁ¨F×�j?È­q$}ÌΨÅñrD#.…9K=Z™n7êªõ=2@$Ê¸Ê ú n90r'+×Ú÷ ,Lw$¨O1Mìô’ªÐšf`bø„ùöNùëà1A-þ4}VbjÎÅ…s¦ŸâH¢¥Eö8O�K¤˜Œö–DM†ç2t™3ÓC!õ#?ì8ÒñèâG,XàɸÂ0>b¬ª�r`€ ÝÈQصPÀ78Âh(`e‹®ðÂè´¤�ÆžžÃ«®ºò;çŸ ‚ádÆæHÙN↊Ì�AdxÃÌ�{Á\E„$C„ÍÙé’<¯A¤Ó›üK ž�áŽÁ—½ìeí°ßa‡™Hމ)àÛ4u¦x«Xļ$p¨pDFvj”C‚RæºL^°$5¥d×^0óvÅø^|B*rÜ ‡aTî/ HÓOÈ‚°µ‚¥¦™"Pðh#ï@Ç?ÔBA…Yƒðˆ]1"*a.‚½ú ¨ ')±ÅiC-ü£µS`4_n¾+R-|»ÙÁ«ô ú•&N—½rfU†§ð¥œå�>À-—i¥âI…¨aD•˜ò�Œ›°ã ±Wðã ¶W‘I422Êy¹Ê?�öJU@�éè�F‰gÂS”/”„Wº¤Ò 5VÙP)<  fØŸ¼†þv’ÓR-ÔÇ( þgíZÇP ލM¦Ôº{²½ )Zr5Ò_Ô�âôØ+€%à Hg�XEŠÂ7ö*¢Q§Æ^)J-ÿY€žÄþÕ“Ë^é6Ç@ô°qž »ßúgÌš9svvÀÝõæ]÷ÅkiŸ€ë¾á²™Ç¼™ûOñ*—.]ƽ§K÷û ¶g\é»åê nýiîjXæíï„üC­�Æ^5þbÒøW®Ñ±{1}¬ŽíâA|xþ–mµËÔ;»YϹÝÔä›Ñ"›ØØcJ®rc»ÎþëqY^ý§°¹•N†C=ÜÆ¿‚uñ`c¯{µâöª›uŽ!™*#5Ìû Ýz£¬Âã!ëøQJ¼l¾:á¦.—“‚ñzM™?ÿ°¿x»r�¾î¿ÿþ»S.»ì2^YÓa´êÔSNþħ?Å¢ïàè<#ðsÙ �/ë‹3¦ý"´Ð§ž�˜©ÜÄX#Ú&HF ‚m^DØâ4Ï�¡ž·:¥!é,PPÅì‘,M*E€Žý‘=òp&XhEÐ B®|šåÊVÎý†æ¿€é¸à -ÞœD‡!4Ï#?!:¼.ƒ!Y›ÊÎ¥‰/fŒë€§2ÿÊp—AÝ ”Y¤“ŒNìg†Cy%+DÄ<};Gn[°@& ùóJá|bÐpÔÑÀNº æhä:iiÌfV¯`̉©(S6øØÙaóço÷â��r°xñbž5‡ØÜzë­Ë)êÉ'Ÿü†7¼AFÁQ åË„C†³ayPDDL (‘é± ÿ¤™?ÈÌ+íŠP«k›m¶9à€¾þõ¯�G˜ãm·Ýî-oy »ÂdƒC£‰€cEØÃ®p’(K.%ð7“�€ÄhÃÁVŒN±? 3 €Kx6CËZ¼!JðŽ`¬S–O™ç0É+Àx-ŸñFöÙЮ:}ÐG3»A5dÓ�ÐDtBõ!‘êŒá U´pqà/Í=m ® Åf\…hM(Cò/ÆZ®tÞ|óÍR5±H½DÞ.´M\„•¡Öè“K’Î�…±ž­üsØœÕ}¼hÛmwß}w®0’Ы’�:ZwÜqçÙgŸ}ÖYg�ÖOø~ðÁoºé¦-¶Ørx@Ñ�qrÃ>FZÌ›7Û[2¤!$V0•'ëÞÙ+MÄ Ní0+¼ˆ¼½Á¶£ã¸ãŽç¶Ó“N:i •<�;¦y]äŠkf`¬ÅÄ�;Ý‚o³o6pDWR§ÒCpŒR{ ¡K: øu‡kÖZ; ñ2+@·ãÓ19òrdWŽ”znˆ#ó,ÂŽ™ëùN´Î„zâ¬Ê*æQS$Úû,e!ÓÓÄ„"  ° <暣!ÏÔK–Wå'|€:€HV˜¦AWÚ4R)|M‹½"ÕÉÒÃ{›ü%ô•‘Èh²"â©ÀK¼ïVà\‚0L0•-#AYnx $þ„ÔØ+…ÞF&ø¦üØ«ìê”ïÚ •q"}Î…³1-öJñ!qz�dsõHgFްJ£kE~æ …GÍpG7¦…lV‘ë8M–¬'Ê^AÈ#°üÞÆ—r.‡Ð¶l=“{ÐXÙ+ÍË‹TÑèeÔ–Fª½zn¬²½R¡ä(±þ•ŸÄ {¥PÉE¹øW�½zÊøWOV{ÅÏÒlgÕàqRÄù�$ȺIÀõt/íéº{ßÿ»øµïž÷Ý3æ\øm`]éÿæië ýîuïØâe=7^1¾ÉÜ ¿pÇó·èëâ©RØJüÇnžÆ^Á%™ÀrÓøWXEÖ×®ȧ�å¢Èãq®¹dh«�7êDûs•N} ]q3hÄšáNÆlàmî:bïñ*Æ=§Ü=Êß|Ôk¹k Êì=“ݵ[Þv{{_/™õ^òëÜq^bRo 1�d ÌEã_éÊ1ƃ�ÕăX¦¨ÆãǃFºfÞŒpÁ5i(3Îv 5«»5IKPïOþŠY¥Ï´05�ÌÐh]ui—»¶ë=ÂÚýÚ×¾ö{ßûÞ–[n9¡ù¸vÆ90á ¡‚ ¯ò˼Ψõb©ù¨ât÷–±ÑËÌ…ô�� K8$ÒNT%aG…U´"faî÷ãÀŸ—EÔ'�aˆ�?Cq–}:¢cKc¬€ˆ_H"¤âPÀ»€‚ÈÈ|F4ͨÀ»ìö5RìêàWÄn òsMÍ4ØC´CzæRëðùàâ˜ñtÎC�á’XÑ1…ª(eÓ°W$Ïéµ£‡tídC´Û#7ßT“z“ƒ*g4ðP`Æ ÑHA°Âîmò"/“£t)ãaî JµŸŽ’òÃ1E¸Pôìg?ûØc�eCYmG*qÅ/à !€@‘c2²pi’c±ä„‘hÌ pšŸuá¼Õ!LöÉwØÄdª^Ó›o.uš 2ùˆÏŸ?ŸdåE]ôýïÿW¿úÕ 'œ0kÖ,H*XCò‹|´)¶RdÉý†³‘,c;¼©A'åt HbBÀ2×N‰Š•Nµ#HhfºêÁtxS¹½@+ÀNœÄùf[!ˆuzÑŽË`æd(óE®ðÅ€|q~ª#Èq3M„©âšjâ=á°ÃÙÚò¡~çáz‚ó’÷šÑÁûAý &ª£M“½RXÃá”Qe®Evăc¥E‹„”@õp™2�"Ây¦ÍjlæÈ¢Æ^E¤ü ¨Éá¯lÍì;Ïe¶á•L†Å|z`Yq{E7û 7�§|ZiQzWÓ^1»"�Ê‹!:! x¸‘uÅÕÕÎôK†óo±x±°`á%0´s‘c»æ ´W§›ÊöEA�ƒTgG° Κ…ZBiŽ.Jƒž„öP̵Sµšö ŽÈRæÉq`B\×p+SIe¤£±WræÃw¸c¾Û#Ë¿Æ^Á ÅZ]Cÿ•©ÿåþÕ“Ú^EOYþ:ØóÆ–·¾}3ûgΚ9{ö̹sçιëM‡/<æ‹ËyÿéÌoŸþ·m>ÕâžÓgßúsø¶Ô—0òTM&S“ÔØ+�oØÐøWOKÿJ ¯›9Ü5ÿ•½;÷užy<:2°„›³Ù¼–;Fu4ô?cû P)�¾è®ÞËÆ·ýœäEs!×G"ÇçĆny#.íîéâǽnoÍ&W‚$ôR[ÛøWuñ \lü«Æ^¡ �rã ž7¦]oŽ`2ÙM^$¾Høo ±ŠŸŒŠû´h"iœˆ8ðôœ¤ºãOYAM晩ð‘Íô¥ÆÿÃ#}}}§Ÿ~:©ŠñíÇŽ¯»îzðJô÷JËkˆc6B�Êx¢„]$ÞÊû\¯Á�4¿Õë­r��ÌO¼{²>d¸¾¤T¤]ÃS±¦¥Òèg,Çf46çòv&ˆàO�ꢫŸ†3v ›?o�„mÄ-^�•28MÜ.„ #)e#0¯ÒФJù£�µÓÀÚ‘äIÆ*¨ò"(wÜ”úÁ �žA^ #�Ì Ö!™QÁ88°™`ƆÄ<ú›–!Qè!ET[×ÿ¶O.‚J3Ÿ3–±émøÉ¶!ï35Ûdg©ŒRHIšl¥ž°�šú"Ît—O£ÅS3?L<é û¶öÚk×ö¿ïþû¨gæÅ*K–3-–ÐMøÛmþGŽ'™6‰ÖȧÂs.…¿¢ÎESÌ‘%ˆ6 hêJª%7w_lºé¦/xÁ ¸ûBÄàNˆ¤©!«½ÛƒO É׎<ΰ¬¡Ðí•`e²�ÿܲå8v®äÈÆ5…Æcð¬`€}x�„¨×¬q>Kú@1@t¬•B©VyNOH¥ònTcïB=SBkŸ .ì�e¢•ád7½ÉÔŠ±™×ì*0¼prG–,éäÈqdO‘ Gž.{Å31ZÝ]¸…V—pÈÔ"䠺γµr‡Ö|":No‘SkŠùì—€ÏÈ,LªD ”ûå@BkìLÊ4ˬձWj%Rì¢Â",óŸ¿Õ¶Wef#™êù·(ºo†š_Å“9ލÝ>ö“[S]÷@(Êt•¾'Ô^�òƒšÇ±‰¯2Êì•OøALhsžä³­¶½‚ Ñ@ñ �j!l�[˜ÒØ+¦ê1ÿª±WªŽâV•‰ž¤þÕ“Ý^á©tòxWLÄä¼aFÿ¬Y³gÏšËÛOÿð¢Ýîðý§‡ÆHÔôÞtåÔ k\ð�lÉñiT> ž‡Â b]{ÕøWº‚,YOcÿ Ç¡ë=»•é>û“}gŸÀsx–.Y2°l© “í�ñ*\9[ƒn!ç>Ñ©�{ë»ùJTkpÙÜ>ÎJëÊ�«{æä»MYùÙÐêÛ�¹ãÔäæV'¾ñ¯ôæÛǃ�½jìÕ Ú+ƒeôŸšugÑq÷[dœ™9b[¯â†æñ—§8&FM‘}bÅRéë ®=pãP'þ ¶w ¶l´ÑF{ï=agì”î¿_Ó€Onp z%Ò¡�õîÍÑeÿtÝñp`´î0¶×Ó1È�L©3‡Æ6>v �NwÉù¤y%¨è,¿·%`ØÁ8” U€3œ¡|Ž  K–"†‰QN@®ØÖJA ѤC€â.´¦ßÒ)Sìäo¹I—P/�ŒaJÑ.„ý¹#’ŒË+øctÇ©ñ!_á£Ü ŠÔe$§ÀNA™‘— fï˜à'”Ò“„KÁ€±ƒ‰œRˆLF˜¯eœÌ}ý•Ž‘aP&"2Ž<�yÜI”ØIHÚÛ@ª9ežóËCÖÆuzìð�È€’ ¼�Ð9‚�í¼ 4Ô‚‰"ó°ái! MƒŸ³@Ö‰ §1 ñ ô°¦�`š�˜Ÿ$så§ Ý Î\—=œÝ¶|¸ž|Oz2 3£²92ÒŠ$ˆž#òíðÀur�kIjÏR‘” BqoIéV>ƒFnw †Kµ4´·“ ‰NôéèB,çb’"žôI†Ò.R“í@ôg°AÞY)L�%‰ªB¤„UÓf/Ú©iTÑ œä¼×9�žGGVÎ^É@Þ±•î†Üvöªjí�Rá<0Qò.W¬ó°±WO¤½@Ñ3o ¦<(-‘zdN�R·ZöJQSE�Ì6Ch‚eö“‡åRQ›D”YÅ ¯7âFP ¨<«E³#ÂâÊ—Âü„Ù+Q@ÔÝ`ËaÄÀu¢½’•´,kh~¼B¹]™¦Í^Á0­e§f:zX1D5ö K [ò|�H r’“”›Æ^=9ý«§Œ½Ò1"ƒßéïW½½Ü—Æëfö÷Ïœ9köœ9sîÙ÷¨ëÎüåòߪ™WØø60àßܓË¿t3âa4öÊÅ¢ñ¯”LàÓÊ¿ÒÁê<ó„Ϋ2¦+=ÿtÒܯž¸øÑÅ‹—ø^`4†hÖ‚Á¢¡ÃÃÚí=§c]ÊÁŒ[®Î§jدÖcyé=ë㓚qzõn»­µ§§“ô[n7uï‚?. ¾�¥ç‚çi TâAæ¡ñ¯{¥g¿\{U^N˜  Iy AIñ‹¢1Û¹pÏS,4¯(xü¿HœGðc&­m)HÐßH™èý&Å�4^i�l¾ùæµ}.\¨§Ÿ¡ R¢ùZ;¸ÓŠÏß.øÝý×ý÷ÿ÷>øÀí¯½ÖZk¯µöÖ[o³ýö/[wÝuM%h,üÇ»Ké.Ø(ö½`:¸<ï«#RJ¼ac€_wýµ—_vùƒ<äÿ�ûÎûÕ×Ia‹Óλì¼îºÏŒšiÂ$Q‹W€1J•|—‰Ó¸úà}÷ÿä?rÛonåµ<˜Œ®¯µæšë¬½Îó7Ûlûí·Îsžãòš¼ÏÑ ¡Ä°Þz'h¹Ðºûî{n¹ù&)OÈ?³¯›-·�9»?£»£‚×ÏÜtó¯ò“ŸÜu÷ÝwÝy×Ò¥K7Ûìù›oþÂÍ6{ᦛ>Ÿ7Ã3YÌ!8E8)c„è0­Öo,ȵ ‹-zøáGh'_DÁ�džó1sV¿"#ñÅ4þŠˆ¶_Äic˜™øÎÀØÝ`eì ðÇNœ›L }…!rÂt°-Ñë¼yó~ô£�õ;`™p“–SŒ¬ ê†o¼ï¾{  U­Ö¬þ™²ýö¦¸‚ ùFÛB¿U#·Þvëï~û»LFÖqYs­µ¶Ûކ€" æêW7þê¾{y¯ ñ’,ê\o½g¾ð…[ä)= ]ÞÌXõ%ŒtP’bN‚pTD¡¸ÉZ:nºù–þûo¿ýv^R‚(nòœçl±Åì³ÛìùÏÞóžW5®�̨`@Äÿ²Æ/J(ú=wݽàößRüöößÜv ùZk­‰˜Ï{î¼�vÜià 7$cYÉ5¡7üØÂ%VÆSãÓäÙ›¶2É *kÔ@#—Âo÷ B¥<äR„EdT=ç[òù Žá¨ÇüK�`ƒÉ´Ø+Ъ0…/J[Éè\�çƒ1ÇÛ+¯dÛ ¹çN¶ºÇ’ ù$½@¬h¨³WÙð X!)MDEY$õG4ø·âö XôCPå¹Ó,!žUüu€f*Òžal\k¯0Òe!�©±5Æ’°|{%Ì­:kŠRä�…ì4;``¯ä^Fm†î,�ÈÞSGx{•܄ݢGàËÖ5€ŠžH'áÌ5FSÕ¹²’ö öEêé©Yó‘BÀ˜9ã8�Ö9Ñ^ÒB=’mÐ †ÁOÂ'Ø+Þ@– eŽld`) L&Ù«4a^h(·Mó9MbE® Ð a‚(Õ`fŒ;É^YÁm’¤9¾8ðÿqìÔò‹„bl"]B‹+¿4'Îà4òäqí•ÆQ’ä뉫¾Tal„ §±W²¡±WH«ÚÙØ«éµW1j6o9ç¦4–v´w°k(O‡â9pw¾éƒ‘œ»ñ ¶Û¨œ�W6úÍålµë²Á¥Ý=]$õŒôÑdìPc¯°¼�õôò¯ˆoѰ®³>Þy打TgÆ7O}æ/¶ø€-~Ñn}}½84ÃäÀY› ÙÂZ�‹¡ó9¡ôýúšÁaRÛK‡†fúªnSê™1å íùVt™�n¤ÞúH«£ÚÜ0$0ܳƿjü«Æ¿re*îíªùW䞌†â”k׿@uhãd룡Å1ö ôɉ393áËÞ­� *>ñ„;›U†(®¼ƒj½õ֛أ:c!n9–D�\ ‚´à�;ßúö·ÏüÂÈGÔv/•ög¯<î¸ãÖ]ÿY2b1À:™SÄcР”’�,WGî½÷Þ/|þ çŸ)'/µ)[o½õ{ÞýîÝ_ùJ¡€#Ü “€Ï¡iJ"g#´‘_üâ'�|Ò•W.Ïÿ �ò‰OÜv»íDË)pjñ<à�a‰Ž‘Ö•W\ñ¡Ã?4�M6Ùä /àiLÉ¿üË÷?{Úi &rã§?ýéXûOúÓû¼a'fJ/`⢋~x͵×þú–[xMA»×ξô¥/ƒ3vÀû=O8áx©ÿ²ËÍùR(h$é¦ð÷”]†ŒÆë¸Ü®ÐKªmnd,¹�ÞrÔs.Œ,Y²¤¼yó@…Ž™h`Œ|ùì3/¸àÂI�7™7ï‡]¨Î?ì۾’îßûî÷Î8ãï'õâ¶è«¯¹ÚÉ…–"”­/ù˼dRKÐøá/b¾(Ò¢†ådR»œ2Á’(X¦…#¡—yO:òãK/ýÔ§>Å ^Ç÷¾}Á‚K.¹¤Ôì¾ûîýñ�ƒwm� t1Q±)ÖŽÜrÛ-Ÿüä§HÔÖv,•›l²É¡‡ºÏë÷Á·6æuiW6Qi5?ú#Ï$ÀY/©W†$§-°”ñ8âÖ®Y�°Ãôƒ8Ó‡ ˆ¹¦…�|!$<_ñ‰àw‘Û’;ö7‰3 ö ÈÙÓ°�änðôE‡¢Ð.Ç^zE•Fìþ9:J�¬og¯¸ÃW‚ÜY›A È› ý’ 2vTÀÜ@ºÂöJ”õ‡ähÀÐ*eW`‚˜øºLá<®½žr®U3%L{’c¾„:0ß|-Ç^É’ÐF—¬äĨ¢“g¡›) Ρ°€=�2�y�¦ãìW1 (¶d)!«…ÈÂ>îД#þSjøÇÀQ ¼·Wtp›Ý@Ð% ø˜ëbÌü…-Â.(§)—3#£ö0cK)£(¢ÈÛqÌ^ nÈ/¿ø€ʘ™<‡ ØÇìU,Œ–œ+Ê.óÕä‚MLŠ6p‚™š­ àÀN ÃɪÐhA½ ˜ò Kå„'ÁÊÑ‚C3f±WB¡6 1B€€Í [™.rØù¢5yXx ž¶ng¯”qt¬ kÜÙé—CR)K%Ðüjc¯´+²Û‡5�½jìÕ´Ù«V5 •¿Cp“šòÕ1è^ø}sÞu÷›_¼dɼ ?;û;m_¿ V§Ì½àïÜl‡¾e=݃ÜîÖÕ ð˜­Æ^ŬÁêÆ¿*K™†]cѸiñŸšþê* ÷Õ}ÃåsŽÝ¯g¿,ÜÿÈþýݼ–„_P‰¯b繩gé6¯˜1®}9|Îo®z`ËWð¸7”—¿_™r«i«uÍîowk/9=÷³úN:GÃÿªñ¯ÿ Ó#×^uÿŠÒ4T¢1×â ¡fÅsƤ%hÛÊÓsÒD÷{—KÁ€ó¢ØS? ~ “²Ü�:Å.Ôj²>=eøÎ»îœÚ‘²QØY|%[cQ$ßÚÕ˜�có›ÿ±c�]~Þ ÿþïï²Ë+¾ùÏ_ï$ÑDXd 8b-óË:(fËÄÒ�ÏŸ~Æ®;¿âŸþé«ËÏ»Ñøú믟Øa¤ŸÈƒ Y à¡8R`7ÕŸ|Ê©û°ÿòón@#·²ï~û}â'.úã#t5öN*ƒ‰H¬dĤù—_~[Ü>ømüà'åÝ&5þð‡?|äG>òÈ¢˜]¡�rÊ)_øÇ$•Ó.ï6 θÓÁ{c¦T;Sø\Fyȇp”´2ð5?@EFå±CD9tºq�¶Üÿ!·KJZå—¿üåc]ÆÍ›7/Ó騊Íh—qM<7Àûh2¹,’|òk¸gAšÔ¾œy(ÓJŸÜÜÆ05myƒ! ‰¹ÂÑÔ‰ëfRp&‚h%ÑåObÑ¢GŽ8âˆw¾ó�“ònc}ËÁüÇì¹Ç_ùò—'Õ—S9€:?ÌøðÈþð9Ó?ÿó×,?ïìè£�~Õ¯º‚WXÀRVvm%Éw¨¥HCعƒÏñ¡?ñ¿S)]&k0þˆM¡)‰(ó²+Šƒ9ÐL ‘Á#×ç!ÿý¦ß4Ù+‘ja â ³"öÊ›Í×›PH:¤âˆ(ÖÛ+y' Hî1®s'R.‘Óù[¡"$©¤š‹z{528Lª)¹P™i±µ”Ìh )e™p>F«ä24s!Z`WZijøU¢“†�<%qjäN¶�ÊgäèÖÒWÈäíÚÖ^‘órÖüÜDF|DR¨ËQq šæ’í‚?¤9§TÀ_G· ¡– UMÏÈ.[{�žC¾�‡±zåí•c†Sp™™�“ÛÑ!fùöÊ>Ò&È·dJáª"¶P-8�`}<ô¡àŸØ†'˳WN²ù*GŒ=å;§ŽÆQe¯Èhª�M=ü‡6Ä“Þì}ÊNE†åYŽ’¾ RSíUẼ…Iù, X(N°râÃ�´É]­!ʪ‡,BØ:*´Wæ7¹˜Sßqa[©å}¸f•¿ p•{Å46ö ‰(BÕØ+´�^5{[#có± ,îœa}â³è'Ƙ—Ÿ±º÷Ìèãôþ{¿ÿÿpÅÒ͸i`y…]9ÏþõeË–zãéð÷æâ²ÿ¨±WH0 ŽóÙøWZû8òÅ7r°<õü+œ—%o=zà É·ŽŽiÑŒož¶æ×>ýèâG—.YºÌ(Äq‹»È-¢CÛì4Ö²¬ù�/ùx8¨èÛ²²OhòÐ^o5Äêî&ëæ?î7åmƒ=Y®]„ÿªñ¯"0�¥3AYEÿŠ;gŠ«‹9G K€¯–Sí<¯ûÛð~=æÌø |Ëû¸4 Ÿ.™‘)ºÞé`ðÓ�¿[IÒæ¦_yåÔ²É&›¸3¥†1D’±.þ�‹�8üC�›ð¸�÷� /üìiŸ[gÝu :¸ê’G#Q’ K/~ß>0¶™h|÷å³£mŸ}öa“ÔŽ;ì„Sâo÷`*ðÖ­¿þõ‘Gqà 7,§û¤KgŸýåý׋N;õ³Û¾x;C�„]’�ÙHl2©Gëk_ûú/yw°N¾Pw~á…‚Ïw¾s!o¨Â«afK4Z×öñëpáÃÈYÙI�qcX›©K•ÂSÈ Â.±Q |Òs8…È‚âÏ7 Û€o^B4ssÓM¿ºîºë¦vfWÚË_þrGqCSæþÕ” ipåÈx<¨Ãèt׺’ø›õ'òlÚÕ·”Uú‘hs4,®ƒI»b–)œœytÑ¢w¾ë�HW]§ÉuËÑ“A eb 2¤ ‡H[_uÕU“¡´?çnnÒÇý×'ì¿ÿ!'[Ò2ç¥SƼ–GFnά‚cŽŽ-UÚ‘]›O�Û‘ÞIË·‰º,õ|wuƒ½[e)LkàIŒê°ZöÊqƒœ8¼SÍÁJf{¥-ÔÊRa8Ñ"H]‚ @¾µz´�D¾ck ‡´gf†p&»â¨ñ†L¹õç—‹vöоl2€ pšã°0ˆÉRÆO" ¤¤4!ºD#l2Á^¥XÑY4ÑR~F…Í.ŠpWt‹’ÅQáù{Å|¦KPL2õ%ËÉ íüZËX Fô–M„á¤#9ÏvÆ…Œ1{¥J1(ðh¤¥IÞ‘I` W;;M¤ÛÏAz— qR”Že�k¯Ê€J¸Ü–'AQ�åpùö :¯êb?þ¤¼qiœ½ª˜ˆ*8WÊ$ �ß\9šb¯Ÿ8 ‹5)ò–åv‚½ž3‘\âÞSÉËËÂRf6öŠéNÞJÔÁ d¢(‰ÿ—,Y|À[ÿâzß©ºÒ…ÄÇÁD–d�5æÂ*±mµ.¾øâÃæÏ_iX­[ÏÞýžÃ.þ÷ç!n`¶Ë.Ãߺré¥?®«n[ǪsÏ;ï-ûí_ð¸Lm?î¦$žQpS†8“~~cP.Iè Œç ö›û Ú‚U ™ÂB|=äËH÷/"Ó±páƒïz×»j»~øá³çÌ6F�¬"²Ô¶LeÁ$ùƒHA-]T€úŽä‹f�ñˆðÓ³¾@øíe�#=õíÌÛàš2Ë o´ƒù{ß÷¾Ì»µƒ\꣡DÃp’™áløsŸ=m¥òncð/»ìòý8€Ó.˜ì¼j ’~(S–‰›xú«…“ªÎ“³‚—y‰¤ß+rAhúòaAdÈ‘À;ò•TO¢}£éÌðjÙ+ÆÔˆ•¨¾0ÀÉ ;@ik¯Ü`E;¬O‚ÁÅE©�·Jþ${®RdVÀ*ÓÔE+¡Ôc'] …7ð žœyø¬ L²WÄ *�Ñ}ÐC¶MHÕo“Ðã‡$fi·‚†iPÁH0‚똽âXßÊ�À…BƈÂÚ.p”Ÿ°, D3>}¸ÝCn¤™_L‘ÊO•¨I�V§Û¥$¨y ¾8 =‡[=Ž‚lÉbq¡¤/‘^7®Ã#›+ôÀS6Èq1“fŽÓA6pি€Ï^=ì%%6J!´Â¡ÚÙ+€[Tõœ8ž¥˜9ap0ÒL åÀjØ*åôã#ˆ3Œ³-}¸Jc>�lÛš´‘üA”öUPP2‡‘;ðwö ëè�{HìM_·, ¦ À±WÔäã E´ãÅ­’ œ�Ì7 ¼q<ó—²ÐÆ^1=�½B8{¥¯´½rÁB5Ñ·k~¤iå•^×ü¯žÏ™‘.Rl£EÛ³…—-,e×Û²eCýC=ƒ¼@ÕMÝÚ«˜­Gc¯ä�KJã_¹RÂeã©è_ñàÛaÞ&Ü=2Ü×Ó}Çqÿ¼Þ7Ožyîä'á mÝ7ü|ã7n²d¿Ýr‹ÍmŽo- {\tÑEËÏ»­½öÚ<¬Ý& òYÿÛůÚsOÜvÔG²ÊGgë3Ÿ9yÕònùöß�9Ï€SÅÙ¿ýw¿]~Þ�-ZlÄo·U�úÓNûì1Ç çãò$°I¨\†[ÍÏSO9õu¯Û›·4§£së­¶^þ-�Ëμ BCFL™«ÈFpf íJ¦Ä+iÀgb¦6 íûwhm„P èÉ´>ôÐ9ç|ûÌ3Ϭeï<Ýo¿ý�3Çanƒˆ�±òWS‚!˜3êŠúÑêÁ|mû¨B¡ÐjÌzk $8åºàH)¯švT)â@5¥´:~|É%¼‘£¾ùJÖ}“2¾ƒŸÿáóŸ_IUó£Ž:J(ÂÒâè‘Cf–�KVÕŸÄ1ÍxéÈ,Çô—´0Ë<ÿHŽzÕkr‹ÿüƒh‘V¢Ì¼qÅ'‰¢p‹îÔ®†½êt¿/PøA‚üBùnc¯l:ì{‘Í\Hqº‹ wv»?ÏaÌ^ÙB)H®�f;œ•v>FL1Çøº¥Š‹ÚGšVûªˆN±W¢-ì@3"á��Ö;j’,iã@4As&Œb–B æ\8|ðÏ«A€j”ñTÕ6ŸÂ­ÅÞL-é¥ÌÏŒj(P00“r¼Î51Qƒ C�Sþˆ=Å4‘ã÷dµð\ÆÙO´mrä•J˜CóàE~m&–þqÈ9›Bc8ï°Ö¯¤½r±( FF #Ä¡?µö TÀS &"Ça&Ú¤IÜüñö*¼J^KJOX*ñ–)Síb’(L>Ê‘´†Ir5Ç…³N–Ó˜ÉàrNòÒ;M²Wâà�«b*@þ”$úJgãí•5¾Ù"Ke:ˆ~Rì“OdÆM!ßGkw¶PI»ŒWqÚ…ä,r(Aä6ø6ÒI¿"TLóìé ±¦G4ËLÙ‡^�½‚ ¤räì5ö n4ö* �úW‰’¡}ÐÒTöJqéÙ¹—ƒ'¦lxëe ·ùSîžëÅ: º|*·b¨ñj즯ñ¯\K1øH…òÔô¯²VC#yg^"Ü×?´~kdÖ¹ÿP«‰Ü|úŒÖÈCÕÛÍ­¢>(Ö n³C÷u?ßž»Ã¹×”W£ò¦Ôñõÿž,ð�ÅnzsË8/Jad�ì÷�ÕøW�m¨òW®�«&õ†Nébëéë£ë˜4UM‡…Ô&RÈž›Á6MSéÉÙÜfµEÇ×¥œÖxÏœ¥e¢£;î¸ëoÿöÓ?»ì²ÚŽ<åŠ'»3T¢`ú�ÜsϽï}ïûk¯¿þú'œpÂË^ö²¹sç‚/µüÙÏ~ö±�}ljî¸�}l‡v˜;gŽ•P¢³ƒ'^}å+_©…Lå.»ìÂH·Új«g>ó™¼ì”6i<þ±hóæÍ;ú˜cð ܽcD6üÁ÷×ã ´�~ô£þçÎ{WýᇘOÍ|�uöY¯ù?¯Ùj›mÜ"“å¶^óò ˜¼ùÍoæÅ—LÚo~³àë_ÿú‚ ¦v!{õµ¯}í�‡¼ÝÙéèúè_ýÕN;í¸èÑGiùoÿöoÿùŸ�ý†9Ö$ÇŽÇ6}îsöTTUÆ\N²ÒaxD8V·ÁQ¨@IDAT°]ò4ˆ‚ÒÀõ6å¼óοæêkKxf£ŽÖC=tÏÝ÷Üu×]SY4cç�w>ãŒ3x*¨‰u,—؉ӞgŠ4­¹M‹å‘fæÜYdõ(5cŒˆ¿{@ íÛ¥}¶á(êj[ŠÃ§6›Wš�|òÉ£cNþÞm·Ý^½×^[l¹åÌ™3o½õ¶nüå?}õŸjÓ‘£=³‚›æVày0Ühý„o—‡ròC›G=‚XòzŠs¾õ­±FÇûQÞvª,"‰É”t°ákÄ·|b8 ónøì¦øƒ&õ+�³L@„i§“TÁŒ,ðañ*šÃGÐ}Lâá7Ïãz6j�à4Ù+‘6€ƒã!ÌA²Ö^I 1+�ñDèÆ™Bá/� %�٫‚Bm%Ê—i‚>� ‘ÞÁ±õ<•*@•œÂ!.=f¯àªY x¤‰´§Ý¦w­˜é2|Ø‹t›¶�ËL�TöÊCnÏ3e<É¡°Äß ‰i¶È¤ÌQ·5 ^0þÑ‘ bÀQ±WédÄTȼ�e2­’wË%{�¿Hwˆ–§¢â?¿hj¢y>ÑŶ8‚—‹rF‚‡Ö§Šj òå„+€Ç>Ðx•ì•ìΰ¬cìñ˜ù;)£ø�ðC\fÅ®�E®ˆ 9§´#0‰å7jA¤-îäPòR#‘Ð!T/··W:½Â0ï`M®ê€QJ‰ùãBSšl¯2´¨K„½òYc¯h q" âJ¬ ÄÜ:ʆ³žE!êï‹™`ŸüTl#ØbÌo"shWŠi €±aÖ™·gf»Ü2'ƒ¸Œ)ŒÌ td±iéDI/ƒ5ö nÊZ9ÝØ+øÐØ+ÄA�ˆúÖÛ+�•-ž°²æ÷¾xÿV»ø4ªan;å¾î‹Ã~6öªñ¯”Á²¨º¸ÆaÜ-OIÿŠ¥‹˜ŒÛ‡xêÚpköê¾}�|ÆÛÏ:çôÞ›jžEÓÿÍ϶††xËQCÃ$Ðzô‹ó.•�5¾æï6ÛaƒóOWW^½ÛAn™sÇ[/98þøIÞ²ÇR/ ñ¯âÐ4þ.[ã_©™«v'JAñ0e.¿Ä/z$œs†]ÃEÆ×�Þ96SQI¤Yá¥íÚ¬«mÃä6˜ÜÿÀ¿ûíï.ýñ¥Ÿÿ‡úä=à^øÂîµ×^ô±cFá°3Ïú’CM){ï½÷ßüÍßô÷�m­!GF%™¸Ã;Œ$Âø$)>÷¹Ï~ä#GC ‘ý¼ŒU¶¹¥â’]ò‘�ðhD4‹-cضÅXì«^ˆI¼(²Jk‘ﺞQ�6€s²Ù¥5ºôOnM´ÏšG9ÈÚå6mh/<›Ü’ßu�"ÍZ•¨h˜­�7ßTÿôCÞ¤ñîw¿{ Ã�7Þx÷Ýw;ø ƒ�9æ˜üà“Aç<±­Q¬¾kk¤öVÓ·½ímä©«©mµfÏšM®yû—oø‡>ôÝï~——Zl¾ùæô6IÀj}gßåXö•Y–Dj+bsÆeg£ƒ]1¹;C"HÏLr+3Ë5aDh­2•- qñ%b“VÇ^9ºæ Î0TžçÓÉ-pˆ¯�<(i A®Ì+M’³J@1iÉ,Ä6´!©§ T,À�ýzQÆR ¿p¢"oÒlSÇ’LN@©º "2˜:ÒUãì• ÈÙpÖD£‚͉›Q´˜rw&J”ÊÈL‘Õ>T»IÊ´W|2t÷¨P/>6 QôÉ¢´åÏ,±CÒn&g (æÀ*{E›Èt‹'�ŒÃÃ]B­M�ô™aˆ4GÆa .�öf$°Õ^‘ò‚DŒ�0íU´PÔЉ/åW¥…ÜL8%U�$ÇÐ+n¯²gK™·³C@Ÿåws�ÀÅä`2i!YX4%"©œTk®I€å1{åĘlƒ½*b¬Øµ±Wð¼bGN ½#ÉáF&Ú+'S†Ë’LpH2;É”L°Wöu[ZHg;‚µ¹ö‰ö ‚d¸2+‡‚ÓèTDó5•ô †C Š� ù5ÝžpÅ‹à/ùç2Å(>Õ…¢ PYM3û1˜&Mö!NC¾8TsÜØ+çÒ,7ì“Û2®Q¬RJ{U8¢ "Z�½ÒîhvÂ%å (Kž÷"FD·7ºíŠ…ÛîÆ/©,V Òö±šJ-ÿ‘ØÆ^1Ì�F ˆ 7þ•ò*SžRþ•®`žƒÊs\z»ûpx::Þf×{Ÿ¿ý†ç�:û‚š'/õûŒ�¾}ÆOü燷܅ôÛÒ>8óèÿ¯¼¼Ì„÷˜D©ÆW·Úë@m¼`¡¯¯«§·£«§Õí}Þ,°¬©Å4þ•:çBÚøW�…VD?²N®¬EÀ¥ Æúš‚^ìnÛiÇ¥?p%Ô|‹´ÿþûï»ï~ ÜþìgoÀKœxÏ ÀàcGÇgÔ§O=õTón®ú3†0³Õò$ø.½ôÒI{—n¾éæ[nºi«Í·ƒ‰zªÍ “Èá&|îôÓ·ÚrËttˆ üq_|ñÔÝ·Ýv[Öná5‰‰Š�é¶\]Õ¸Î|È‘D@uDHÖÄFK-¸&‘ÔåÃ^Ô§oÛ�Vü7™ž~Æ<‹T&Œ4Õò”#ÿʵ-%ìo±¶4 jö1ºJ}Gëù!Š:Ä•¼/±ê?©½Y.¹ïÏ ¹¾•�lÆg Ê�GNúÌä}à¶hµÈϾõ­o僟]Qþ9sæ Z¿ÿýïÙ陆? bi(WF†jŸGš;,”mЯÂ1@Gç:ë¬óö·¿½ ë$×MW¦SáXöþ$/ÀU|e£nèà�@=È7Âæß"-£¹,#L4ð”z/Sɧ†Æ'R‡Ü1+a£ÕêÚ+ �i¡ÐåtØwa’XáÑ®­Œ+½£×%¸º«Ó–8Ð<³@@ÀVö*ù,@!›tJøÏ®ZÙ14Vú ?‚?à3tË*äjDCë¹írUÅžb …Xa‡�ZÒðTŽ J« «äux�³mHW9›B‰xËÑA¯š‘Rã¨RêØt(X0yÒD¥&¹&&]�âAO{EþD8»€ØØ«äà3óÌ=è+;´Éˆ 6F#G�Ä5É5cÆUSX^1q¡"Ü‘QŒéd·V–æ"nÛŒ,BpT/9tjR+árX^KcZÛ˜Y‡ÑAÓ†êBéL3òmr¨tqw!lr�`á¢G‡“Æ ö °¾Õ(ãØ8(dj gÚØ+I3ÙêÜ9<²+�€™`¯”DÁ"…ÑÖ£’¥Çx{ÅZÀ#ŠäÃdÙ,`ªGíø:׌YsÔ­ˆ+é^“ñŒc%5yrSºˆBÐäáiÃט½2ËyÍf<.;mЪ*±¤›½”Ù¨ž:g/Ætæ{¯˜¼Æ^)ÐÈTc¯Šò­€½² Ëá¶»túd·Õ-K6ݦ€ ËÆL,zþ‹ÔáVÇ-½˜/|~îtéííêïŸÕÏó'°„ÅÜ ÁQçÆ^5þU‰Y²wü©ï_¹âñß/B¢ôôd�¤òÎ7¾QWçÌ67ŸÎ9vÿî}ßwß¾G,ÞôeÏ™¢µÏ½ý³ÎŸüØ`¢}¼^%dëÙ7 ¥Žvã_éj4þUã__U?•¢ß»ñ /Ôa¯‡ ˆiŒ~ñLÐoÀk֛ŵuã€Ïb"€sjñ õÞI>ùß¶Ônkך VÜ&i¶Î›56‰;.½äǵ]N<ñDî¼ËVyæ?]@ ¤Y¯·Øb ¶¿ñZÏñ}ÉCÝxÓ Ûn½­mZ­IWÇZî½÷ëwÜq§ð×ÜT¨Ö“O†o°£ÕKŒ´é¦Ï¥}¼{â™õÛÿþíu××¼óýï?[ùŒJ°ž^°‘o>fô÷sƒíÑGO~ìÝå?»|«Í·2ØpŽÛ–³Î:{“y›8�CƒÌ'Ó(†×Xc ²„_üâ'uæ�•Ã#¼æfaÛ�PÛLjöØ)ÓA´n˜DSÿ) ½"þ³³€€ a0Çcä "þhdFkþ2Œ,œŽÂ{N÷yýëßûÞ÷¾ñMošÑ×ÇXA‹M. ï¾øÖ®ˆ‹×BºÔKœ7£E¬ÛtTÐI½JíeZ»âe·Ç™¥(ˆÔ7–1^GÉߟþ×O™š©@yˆá¾ûîK}yh~G°§¸ð Ô.öcÖ¦Þ�8©C±;[�þqñTÈÔ\ñ‹+^òÒ—€HþÌ•ˆ 7új +%tr˜eÎKË%æ•G°*U(O‡"¦#=Ðf98íügONFØS@r¡�i÷‚’ÄÕš%_¯)gÂjŽ™Vút:쨀—! úàç>e—¼¿éÐáŒ!IÉq¢â™I<0O™uiAs\ôÃeé5ÁˆJØÑvŒ° ÜL¸lIÊ�o�m¶ šó(ˆVùÁ3§ Å@èCT¢R¸ BcQ N´ ÆÙ÷c�ÔªôÆxlÂY‰‡0�{Aªü($K¥„y1m‚-5±ÜÁF$­é9f¯R:�MRŒ(”zžõií@¼AQàŒY5s8.p5¯ ðR±W4!³tÅN±t’ªÙT£^ÒCöeö´{à‰ñJÙ+ðÏ[b…Y¡ÜM)ƒÂ çN¬}Xrê¤JÑ� by*Ó¤Dù‘J+ù—úñöJ÷¢-´`±&“茔Nas@{¥B)0/iAq¤�c�·W©cî™a8Q|d$ÛŽ·WàŠÁwÝSÞl‹{@ Ïœj‡0�š 1¯tDåÈè kvq9sàØ6,÷X.d×™E1Kò�–Š2ÕIw:8¨p)ãó“ã}Èåg€ DÇÆ^5öJQSñ{µªö*VUC¿¸mMÕîð€§¼?[”5½eC4ÇÌ—]Tz ‹mGÇlRì5]]=Äý}}Ü.×㦴,ªºh8~c¯äB«ñ¯žNþÒ�"xóL—6_hjrï~G®×ê¨}ñ2Òÿ­Ï­;<ôßÿç}[¼¼çÆ ¯F]"]ý§oïGózzúzÍ¿õêGG]ùõ;ÿ SÔøWÈ„Aeã_±.­F<ˆ¯­'Ër‰nñaWîjÜðpõ—ã¾àøšÿqeµ¡¸a ©›âRgQX�ò äÝ6ÝtS—úxû�ìÖ‰Ÿþtâ;’3 yº?Ù~{éG k\öÉh$‚8ˆò¤¶©Éµë®½vÛ­_T…Ÿý¬þas‡r|!ãDz�µŸp%ÿÀðùÓkÐ/(Q¯QÁem yÿ,�M@ ¾ùÓ>°EFZ²ýŸLeÝUW]}È;`Dæ©Mé›Ñ JΤ(9=‰m˜�áð³¦ç]wÝ͋ƌ†7™UúµD8õ™÷ĺ`EàˆC¦‹ÎÚh«Ì\˜_ Dð*|2lå•5جRûÙ0xúé§Âñ{íµ'ƒ»Û*$0²xµ-^2HvFBVFHeÎÛv$zAggEÆÕS¬¬�`ÀDÃ7Щ-A$ÈÊ⫯ºº¶Õ;Þñž¿ Œã°&Æ$6¢Àu%f]8µ…‡ñA¡)ŽYsç¢b“6WÒ甿;¥¯·ïàƒÞŽbé$ñÑqEµÃ°œÁÊe5ÒD<“sÁA'‰{§;4×ì†-l¢Þ0X®s&_hZ!Ý¢+-©¦’oÛš#JãŽ%”Ò)óæÇªÛ«Œ`Ø_@G1‡²Ëdñt(†j¸ùÅ-«sƒ`—­äˆQ³W[˜-a9I N{³]‘"€r2Á^Q˨è�D›| vbBMÁè‰L�(�ÔRÅuç{²½¢«tšyMÅ D©±(ÔUZ×k®Ð8Ù^9Ó/(¦•n´•ž4æâ{e^È&’Ïð¥¹Láÿ{…âðÐ� ²SéÌ8Lí/‚âŸOôB +D84š{5ÒÉkoË ‚àEѯ�ÔP[fF6›uxG‡+#¹�—iI+x Êm§Ø+²�T+SJŽ?"Hho¯”YVø`v��Q&7�ÆÛ+àpÌ_±q$òŸj¢½bmBãi-–ÎY´Ûé¡(*T§¿®ˆd;!< 48•«4Ây§�9±ñöJþÎÇ%•Mí¼òʘ½ÂÊ„«•VÖ”€ sä¾vƒá#ÊX(¦'€(¹ÜØ«Æ^5öjìU6r«šêek`Ëí‡y Öð0›×ÐøGÙ³–ÍÓ\ÿõÆ/Ñ�Ã\˜FCQÚ.�Ù.ÐTi­â‘kË8F-‹ÃndˆEeT•FÔäw}»Ç‚¨Ï_®NÔæ•9ã= <—í ox÷HŸ×`lˆn‰#‰Yj¿ÖZk]yÕUepÖs'¼ÔZh�p]î¼ó®©ˆ<ðÀƒ†Ê## ZX{ìK^ò’-¶Ø|€ûÎ ðŒ.¡V.qf`‘Ý‚z~ð%F.¿üò©ÃQó‡ßÿþŠŸ_¡sB~‹Ÿm €:È;Ǧ”ûï»�&n4tg‡ÓR_œ–æJ)¾´v�i­ÿ¬gÕöZ¸ðÁ�7ÚX7N³§‰Ä[µM©L4=`|é2—Ð]Âíe FÒqeF…¦Q° Tøâ5ˆ„‚„œrr: ¹¤÷½÷}Ÿüä'öyÓ›œu‰7sÁ`méMÚº¸dÕ†¹:…¡¥=£¡]ØveÞ{ߎ ZÝ8Tè%×QÛ8bvŸ¡ŽË/ŸðóÔX—}Þ°�ôò ö‰t ÏÅ&׋G„ÌA¤®5Ì]º?úÑ�ÆÀŽ|êSŸúÆ7¾ÁÉ×¾æ5ë­·žs‡ì(늧JHoSi¨”l“yðˆî ñ®R¡f‚�¨…Js~h‚˜EÀ’ð™*.í8;WÒp©äp䛤³p(Fè†èªYô‡.´ÜjØ+{ûgäˆÿbæàJ+treHÙJ/v ™ÆaSò´O¶!/ëg¯¤ÛŽ1JoÏù” %²¢¢!F^{ß\hóº¦Vô#Â8°»ÿà)CÂ΄æ>aq_¾½]1 G R -' B›u�féå”"©IéÉz1¬ò¬â¤×motyÌ^…._n@g¹Ç§)¼*xLµW0{Ç_¦<$Ù•¶NÉd{¥°yMN«FÑgø6­ö Ú´ÑRa È‚ð éØ9rOædнÀ‘o¯ ›«Š81�.›¤N¾)|ô¦r‚½R- Û«ÏœÉuøPg¯HņLœðü—¾ ;Á^�‚V¾ˆ»sÕ"6Ù^9É ®J @©QQÂhá@]”]aR7ѹAm¬?- ߦګ¼-ƒ„!ûÇéÃÌ1³o[7œºå­��è¬ë,«o|\9 §ÿªñ¯p+ñé� Ô­‰µ6)+biÈá§Î·G|û�瘺„ê[FtGÀ—%¬1*¥¯ae‰3 «ðÉû¾úÕ¯’fw|ÜlúîØÎüˆÑ5¼dñ’©O+£1·æ‘,X…yo¦hgÇu‰9rÇ+x@^˜Âf‚,x\„Ž&@ÒgYe“÷EõFxg->ûp@mýò+I*Óya¤*,©é!J‰’˜’LøÈ&�„JÃ}¼ú³¾0oxhTCv¤J£êZsg“ ‘F.ë?ñOñ\ó_ÂDqµ £ñ¡�¡ª‘Íxz8&_ëË‘GÉÓý'][¶lÙ½÷ÞËûî¸ã60ÖJ]Ž>ú˜g¬¹æî»ý+„ ºÈ¶ÃÖ.Ur2 ÁŒ©V¢kú@uÒNN†‘8�ø¬m(g`Q”I†Ñª0¦¦5ל)£ITîÆ_ÝXÓ¦ÕZ{Ýuœ[s•öD·Œ\6!k›¨N²Ðøµ£µõÖ[צÞ{ûí·ólDÊK_úÒ×¼æµ{ìñªu×]‡ÎxÅÁP”‚½¿0¹3n˜_È ™\v/g ×eB‰ÂÝÛ…pÒ©‘H“È»ž'Ud�Mò.Q6Ýàœ†É^!™‹¦%Qá™{åì Ï©`ÅH*”°Qüð 8$ùÅ7×ìS¶¦1�xJ igb©«’ i´ ‹¸H ©IH)ó&[Õ"[8Œ&–;uSMo² ö jØ\í8Gàƒ˜ˆ;�€` Åô„ÀVÂ^e0 ¤�³a+G<ðOò5q\=ù`]I×$Ë’S�…l½ÊóÕl­½%“Úló6S¡p"DÉ<+ÆÙ+OAÀø-òÂ1ºêCô Síxe$nÑ_v/N£½‚ÓÀÖÎËtF‰áÎÌ¥r¡P©$ÓÙ¡ë⫼B¸´ÐÁÙÌ¶Ó ù알�6‘Ù*DdVÑV®åв^U#™kk CŒÅ«eÊÀ½@%“n¢pŒ©ôÕ¯*ãy)£>9¼(ì7ÈfGµŒ³Wt”@ÀJW`àBÝòìU§{¦}Ïf Š:ÿÃØ"Å^�¹ù=ÑSú(²Càá¹´‡a¥»rÖØ«pª±W�½Zm{¥µÖèväùO=屃}½ƒƒdÌc¤£ôdÏF“fj(ÊÊCÛ²ÚsÏ:÷ƹࡳn€s�Â.“ŒfÇVîwSÁ“´Óˆ°UÎÂŽB{=™Æ^5þUY2²X¹Pž>þÞ#ÊÐÙÕÝ«úpÒÙ=ÒÑÝÙsÏ›�x�~ûößËŽ•,Ü~óFÛÎd·w±ò”7”× §£+rÖöø�ÕøW�5=þ•O­Â§G™90¶7lÐ�å{H-·ÞTFêÔhC@ôÝØT÷× ”ƒØ¿•Tø±æW\qÅ—¾ô¥C=”•Õ%š\q£ì| ?ôû…c�§åà�ü£†¥³ÅÃék>ëYÏ’ð¬ùÆ�@îça_�¾H Qà\ŽJÀ` Õê¸ï¾ûj®ZåÂ… apœŸx!m Ð€…XÄxIÞ‘95RôDO§M7Í»uŒl@žæz8m‹¾³�DЪ8E.Fsì iÀ%€0íh„\x)]‚ƒá==œßvƒpoì¶ÛnWõëûX랈Ç-É'�Tÿ"‚“O:y÷Ýv7¯CQdÛ•°É;�l#Yþqй(ˆ�ŠÈ·)�6´¾Â|ÒƒŠšÂcÌ%dzy&RmbqÞ¼y&+ÄÕh&ã�:WA@BkWUÈmo°¦gÇ_xà×¾öµ©÷œŽÀ«('œpüž{îyÀoÙqÇP¤T¹™W)€g…šŠgîšaÞ9µ1’Á-„ UéXˆ]ZÙ<‰®hà‘ º§ØÊ.‘÷åØ+Ç3 ‡h:Ü¥jDÆüÍO‰/ö h†ë’`²Í( êã¨�wKДP¯4öJ‰hìUW“¦h Ä âÖØ«¶W8þÐÖÕÛÝÓÑßÁ;‡,1¼¾�,\m±C ­$Ÿ>LÂ�æ^ÏRÀ=ãŒ,„Y¡ôYPhìI‰/h†ãʯEO„R‡ËZJ *ÝØ«Æ¿ªÖ‡§»•C]Z]=}Ch {D;»ï}ó‘kn±ýÌo}®÷¦_ÄÜ­è÷�‚-o<^‘ÛXÙˆŠB¢Û&öÐð²T£�P]·¬´EO5§Ó¢á�ÕøW.ºÉ.„Åx0Y¸uÿtŸŸRñ »�üÙ\šun¡˜Åã‚vœkƒ»²tÊ –QÝm×°É+F¢m oØÜa‡¹øàƒ\uõÕíÞÞø™Ï|†·ì²ó.0âíµ‘¡‡®O�µóñ«óFT`ÿá¨mMê ºtâ 9øB}hUbo¾d–¡BDBII@ÒâM¬Ó™%äå•J]PdǬ-ú)<Ÿ/‹¶0EÿEÔê ÍRB‡Î¤Jt»bxƒükkÁƒ‰�d±3ðIVˆÎŠr¢ke;CP> NÕ+yÚ!®ñO�Œ™÷´¢8jçÕ¹sçvØ|n4>æ˜c¦âɽ×\zÉ®»íFh)mKNB`á â Ðé) ¥Â‘kJÜÉ Uˆ”ÄšfTÉ|I¢Hñ¤ìª-´4@•ÖÈC�<\ÛæÙÏ~¶ áÞ(7&;m%‰‘á�åDÍu��Ó¤ë”Ö‘‘µÖ^ëoÿöo9ä�º¶“ë.Jyõ«_ýÑ¿ú+¶ÝÉUþ"&î�qp&C]4AvkA" %¿Ä8c"°åPù°kÄU8¦ d,Ðè’às „dV’æ ·9ŒM“½bêÜs¥<§(™àV~nLXà¹Ënè6h ÐÌý¹|‹»Ý'Û«H‚ —ŠðKy.2Æ6¦P¢ hoù™sP�W®8Æ!&:EÃ0Öñ•'v Ѥ¢Hð—c…¬š¼¢öÊLŠÛÒè*�n­…'ŽªLZí2 êiÃìÒ¨WÅiÑVC˶‚›ý!£(;iÓ`†v²_{{%¤€§¹¢"°t͘@*öJÚ)~ù?²E;ù“9“YÓ`¯Ü@Ç4€ ƒ@0/�åéo@õìø“,1 “˜¦Ðät²›œTƒõf™~1¤Æ™ð¨½B•Ã5´–©®<]¦Ódb�½Š]Ïarö˜}æ*·Ì NUùàe–bŽ–y-?›LµWüø6D-eU§\‡Ì^ñ°Ïx¢� ž¡ŒHïvöŠqa¾ ‚Ÿý9öç…ÂÖ\ C”@¥“˶‘Ë| %5æ+=�¹•µ­ ,{ÕØ+d¥±WE±P•³Wh£F€E¯§¼aПšX‹\¶ÜÉÎæU,1;¿' îC¼n‹¨]¤ËUÃ…&¶v¡°h+yè/êoª£Øƒ7éÊèigP]T™wФµzߨ+Xá œMκ(ùÝøWO3ÿ íA;xâ�ë<ë\gkÚØÝù‡-w»ç#Ûoxþ)³/üêÍŠ•+ÿô ~oðæõ ½=½ÝÝIºáêC~ùßµ]³Áúé�ðE]ŒW=lü+ÍÖ±ñ¯0øO‹x�øÅ�8ò$Ô²RW~ÔÖ{e©õÓµÕ”‚ña ú—?½Ý6eýõŸÅV&ÊË_¾ýüCçóDü6 [GuÔÛӹ'Äý.I~pë�ٮתÕ?÷¹ÏM ß100P Á›4±,2,ø@)[4ä��&(A—,ÑGÀwÀ‘K3f̨¸j•/xÁ Œ6Ü–FóQ[°‚Ìq™sÅ–|fIÿ¥ë±D\MOoz"êL­Çí°o˜Á"G©"YQfzbòÂ15Êm´’TR|Öâ[ÉO{ÔÙÍFü ñáï­¶§ä �ÊáH‹w}îºë®µ ®½æZFeX£@(jS”að]„¬l;£"�•ý6sIƒ$þF{ÔŒa §§èìår[LÊ eÌ�%Kk`µZ$�^„ šh�ðáÊz¤\Ušœªíj¥|ËœÀ¸ë®¯@ yEIÛ/üà?ØãU¯ºý7 ºMè”ôŒ-À€8×�‚}æ[Ü@“�2®é'šú²Á Ï‰ˆ‚røZþàÝm²’Âú-•öw4¾…fÀ,ºM�½*¢!CEÐ#ÁG|e™F °á!)Š”ô'Ï(…\¶7ý§Ø+ÒJäïÂúH�†R‘ïî£)Mê‹FD�ŽªŽ(bB·D³”t†l‘ƒ¤4@n�É/b`G X9{E䣬Š�pA“¼:ëâ¢p ÖQ21ÎIŽÀLi×4b™&4\**{¥*Kb’ôŒ{6³"|hc¯2G@�ÂÙÄè"Pc¯Ô2RNá CÙXì)aÆôØ+Iu!]ñ&9+NÔ0n"Q?8”XgÏÕ…Ö’¢¨‡—ô4%F¿J ‚¡Ò&”´õ"ê$‹xÅ.=Ï”³¶H´«µWÁÅÙ �ê#ÄLajœáÇì•Td:™·<¶™è9Å^eÄ”Éc6µÄôT6,~Ê…7W’ÔùMk¶|{%O\LQn&MjETà2Q$ù€‹°­¯É`\ý{zÐ̹†ÃHX¥G�PHec¯àGc¯Yÿ+“ÊŒ¢ÙØ«µWDà*q>¥»»··oÆŒ¾}ýý3ûûfôód(OgöõösOªÁ{ß oZó=¥ì ÉóÚº:{‰á©2%‡uË{ã:8-äÝÊ”˜Ùq¢Pm~ŸÊ´ÅÒ4öJ–(ÄJrã_¹)«ñ¯\ãöã9°#uF_š8kö¬9sgß½ïá�¼á0iÊC{ˆ²á­··õ6ƒŽïŠÂÇM¾»ñ¯ð.ÿ W°,:g�…5^ÿÊŸåY„¸îÉZ¼“x»è6{"tŒ[hâwÉŒt ²Bâlãq£›cʶo‡8†_{îµ×ßøÆsÏ=wjn‚ûøßüÍI'GÛ#k0Œ w­9÷SÛS3oÞ< Mº”~®T¥p:v\j6ß|ó�wÞ…‡g›±­l´á„ïûï¿9ËƒË \EÞàuHÎqH—<}˜6<²îºëÖÞÍwä‘GÐ å1]J 3J5ð8ðò™ë­Ç[;Í4¤ÆyÀe¬-*íØ_’´æÓÙ!nk×GÌ3ÿ¼óúgô9Ã�ϯà!�pYí–\ª�¤?3Gb•?•Y1 †~4õƒË!ÊÄBæUŽIýíƒL:ŽG|ÓÂHKÅ¿i±Wbï ”Í\’‚¤Â,DH—.ŸaŸ¬r(•I·¢ í•·èSýã‹¶o‚Á S^pM¯Bˆ(J˜Ð �+æàTxE3ŒŽ]8ww_@«`+ü² Ÿ(>02”+d¯hÎððZír!‡ÿ΃™¢L,$0S"QãƒØ)s˰tÑ.Š®ØÉ€9i>ëË4œVØA¶r5×Ëw�½¢%…¶àD_ñ2¯ÉP�g¯ÀMÔ.ÙÅü�‰láVëi²W°¼Lbøï{� ’â4-Ï^)Bê\¯0„"ޏéÙe#n."†Ö�e²T(0—‰(ߌ-ƒŽ‡c%UæÆ ÙáŽËŽ®Üý%Gí·Ì:‚� fÆ–g 1Î^QåS hҢЫöÅ UöŠK\±—8)Eà£ùcÍ…ÈóT{¥PËYó? L:%»¦BHר½âF42º í`|†3á é0š¯ÛÀB®@�§b¥H4öª°Hy•=Jó„ÕØ«JŠ{…=�b¯°Ã:‡ÚíE79mö˜ùÁö–ÚÈušŸ¤ÿÑa40j㥬©×éç×ݨãÛØ«Æ^5þUÑÔ‰åkEý+œ‡AÜf�D\#7œrÿ7Ùí{÷;’÷¯ùñƒµòË-‹6ÛÎ}n=-^)ÜÓÃ�«îru¹¬ÖÒÆ¿*ìs^àlã_5þմă¨iYãï"].žü'.Õÿ�7ŽûŠÿ=äsyô¼©åºécµ=Fï«å´VÅ]c}òŠ2K(7rÌ1G_rÉ%µù)¢ÿÊ=öxõž¯Ê­‹hF¢Õš3wn-hò/óçÏ÷’(û?'´MíhM…'(YÀ}Íg¬9zmÂ7oK°žFöD…G:÷ü"Ø”pÓñ<´!XXsÍ€óæÍ;ôÐ1<ÓH#yuh^!|”×´ªØ]×–:£ á‘p‹Å4«úÔwc63vNÔ"iAkJû$"2D ÐŒ¯,z`‰¾uÏBŒQ®€eEMCÔpR?@ ËßéÓdd˜-=œ¤‘%‘óFÝtÇ;ìÜxã�«¿xƒ ƒµå�${y²{I¿/’‰•g²½í¤ø´4(è)ù™¤‰ä,‰ WDa‹IêÔ4UŒ’féhõÎè[{íµ§ª¯˜JÐ9˜a'°gÇŠ«i¤È©b`ð€V‰mGÊ:ë¬óéOú¨£>|ÁçŸsÎ9µïù“gžuÖ{›î0å 3ðžÁÂ#êýŒ6úíI%›Î›ºR¸æVCéfhm€±ž¥07Ù^*e¥JÉ‘ûŽáÎXRÅÿ•¶Weâøx ‡eš‚CWì•’Î-u¶Ñ/ÌJŸãÑMZHðŒè@Qà*{%Ša mžë£ªF•¿õK4ÁÛ:¡aÇö¤ .p˜^ÇBt5&˜©M¡=©(¬‚½‚0ÕIpt�‰å5ÚÂô˜Ä›iœÉðâà 7-ŒtzÆì•x+›Áß®™ßЍÐÙÆ^ÉWzñœ;4È,˜âHÆ(PðÉzÓ† (Å©˜ /q\Pž{å Ëé�Í·©U¨åCÄ×^e^h- y+‚þ˜w;f¯d©Ä”›ŠdX6¸B$-Ï^Ù°Òž’¦2Ž9P8-ûÓÖ•9†þ€ëy¬÷x{å€ü™Ö4~!@ªÕ[º”¼œue§ÚàƒÑFô©#ð=Á^%K¬ôShóîÔ‚ÑD{•¾•t9ªX3<‘Årlø.Iò–œ¦–ÖÔ¤¶±Wp¢±WZe¤±Wíý«{…énõ²Ðà\¡Ú,üÑ.“ö8hðÔg hýã¯�Ú+u9?à«ä•Aˆ6ư5öªX¦Æ^5þ•k J’ÕÌ•KcíúõøöÊ‹nl¶àiŒø«¨¦I»ÿø¢Ýïûê¯6<÷ÔY´½ù”§¼Ý¼áv³zzgôô÷ôÌ`÷['»Þp<ýA•±ÿJÇCî6þUã_¹ÀM[<ÈF @ó$ÆQ·X­@VF¼í@k¤ÇskuéiÄ1âÈEÂnbÛ´)úåÙ?Î ˆ5æ>ãÄOlÓ¶uôQG=xÿBÀ‘+Èк÷‚”ÄÔ.W]u˜¥^�œ#ÿRQj¹4v�kž6„øÏ™[¿ë�Ԩœ{ãI€J­ÐÝÁÄn‚!\yà$B58, ×[o½©x.X°àá‡3±14²MÎìU!þX?® �Â}LùûoF·+iKkm%°3Ë¢[�U×1¿M’;“¸‰'P[t²`cÖ0ó’ŸC±Óå�yK e%ÿa#ä�çv7×`%ñ�+Nþë6Jœ—øclž²„ÐŽ<Ôæýìš.q™]íV_ÃHTðer�ŸãOJ^›B¦:!“¡¬ -d ¥?¼5î”õ&‰ßÍ´t<ïyÏ›ÚÎÔhªED•ÖZ5.wà©ýRÃD�ª„•¢N‰¬Üê6£V‡rÈ¿þë¿^|ñÅ<>¯vôÒ�ÏŸÿüçU˜nÐ]$Œ‘™…ˆRMLeLÄMYœ�¶ÒbÄ�'l·À ×ÁFÅ Ð\V¢Š~©X´N�ž{U8VlŒÈ+f¾KQ*@lÔ^A&¿1ðÓ¢[“ªÑ�&ÓdhWhùo¯ü„¡H6ˆ t½©p‹c+!=ùéÍwÀ'ñ‘kðÍd±%N…7!ÒO0`„Ѐ�WŒº•¶W �}ëBªdµSÃzNh•}{JXðþ@Óa•zDoïH´Íx{%%VòI•M RX „P5Õ^Eó5»t 3˜߈§¼8uE€é«9�¶æþÉÂF�'Ól¯Ä„‚�Èouõ\A{U¸Ã ‰=ôÉ5É`6 ª`OèΉT9!CËg‹Ç±W2Ön`æÜ‘ÿ’k‚£TöJž0EÔ1R î7SFé*ÛÀ.ås<òD,HqÊb@VGí•?·Ø]—DJ&7,×^”î�/>ì™j¯ÈËnø¡ ØÈß\¯øs,ð¹Ð‘“سȱnìUÅ…N6ö † G�½R0P"Õ†TþU;{UÛ¢Z©X ª{UŒF¶±WŠ”¶ÙÒFc·ÿ žüÏøWº*$ΠY·>²h3fô÷Ϙ9kö]o:|Ñ>Ùöá„L.‹7݆ÛÃyÀOXê˳ÞÜöÆl5þl˜"Ê�Uâ4¼AW Jã_á¯R<È’‰fº$aMqk�ù¨ÑpªÐ6ÁYƒÏáÞRPtªµ$ÞûS;Y¯=§Qñôõ™)1Æög¯zík_[׼ŭmÇ\œC�»³ªw´vØa‡©ío¸á†G?JTJ_¢ýAp2ž *û�ø†è€!ñÀÀ%nj o2ï9SÁRóÓŸþtéâeùýÞè“ £ì04’YiSE™4z¥,§©^d92eà 7ôkbA)|è÷ñnˆNi‘2aŒ}Ûˆ„–�3©à—YT,T•Ìãçlüœw¼ãäàÎ;ï¼—¼ä%ǯή¹æºhÀø`ÎËà1ÁL“°˜¥*¯$8sŠA¹˜§rÑE‘IŒ/Uò�6pu›#•À· ¬<ÿì� ”eUyçoݺÕ@wó~¨4=®EÀG¢Fĉ:Kã“1K¾Hâà(ˆ*†$keEãk’Ñ1d�A£&Á˜8k�:#dŒâŒ†·°hD‡g¤nUÍï÷ÿέ·_4�¦ö¹Uçž³÷·¿×þöwö÷Ý}΀è[Ø�⯠6õÁE¤“oªV-ñWòsù²—€®Iœk�u*Q>ì–·ýD‘{”‘Y ‰6²"(Pˆ¨¼,ï§]¹Pµh­Z톳܅jü• ð@”#X7Ô@gOÒšÄÊöù«ˆ&N•ï8S{Uæþ2Ó…Úsüâ@ƒ�Våâ Wë(…«8”1í CÃiÀ¡CºÏSê¢0€ AñæþJíD 4TOT³Ü_a-&‘ 0F ì\ ˆ…fòþ>t¤pyʺٺ¿Ò~¢+”ŒÔòÊÛEf“$+m)FŒ̺8þÐ*‚×z^i÷qþÊ¡^aØ 9sl!“.éü•EþÙÕs=^è�7œj‘|6óWZ™h¼÷¤˜¹t6óWòHS{›o:šîî ¹’Ùe4à±­]â¯â>‚N57­¼UU \ê¯Æøö\8Žg^*@`j.¯“t®DnþJB-饿¯0œæ¯Ê90šßœ ,¤›_5Õü™‡\Ź\lw<ˆýà�ÛüŠ+•×d~ÅàeOñXåA?OÛcÍê5kö\{óIgÜù²ñÙ·+Ž{%�fÌÛvóAŽ}^GL:Ý_8+ló+gFm~ÕæWNt3ýij9¨ýº/ñ nÁY>S³„  böÊÆÕØ ÈXÝ ¸CÚù³µì!-ùš±'ÚK©5K·`‚ÓÌ¥™)Ó*—úsÏ;wÒƒÞy­âç/¹ 5ŸV©ég<ãKQçü¿ÿ·3�b Ÿø‹ä«àÒ$�å°, ©œbÝ ,þÐ)9Ï‹å®Õ嘹éïsŸ¿œ°! Q—!WÀÈšO,ó^#vjÅÌlL-<õ瞺%<Øž´Zô\ÊËpîMùŒ4CÕÂË€(á½,v¹¬LتI± 7‰5¸ÑÅŒ:�É›½Ãqéw N½vnÌØ9Yˆ`ïtŠ*ME´ÆŒríÃàyk$!™’=ªÆoaÂKUd%àMNbËE\ ˜Æèýðæ›>ò‘�ŒE±nÝÁ‚¡1ÄéOåÝ KÉdÝ}×�ö§Ø [‘eùäd¼ªË”䮢èζ–âç¼Ô) ,ë| �¨ˆx1!+Ê›z£á¥—|j¢¢Eé�Ysô•N1E2?…>`�}ì b *�G[º¬wä‘G~üã?éÄ—c3ø£[-§ñ®í&°VüK±gïiù.P“/9nzå"pZ— ö|[d�¨}Iˆ†-ghÿʃÍÎðW°ÍG¾`[!fìߥþŠ{Ÿè˜0íŒS”ÂL±gpF";N1éomÅ“ §UFý}ðW1IA edtë2œt…©n�1¥ÊyL(ðJWœË™›Ã™ÕZ<@ÈÝÉþª²aZÚ@ˆôb|­ö£ê&ú+ÀòD;INsÁÕ¶yÛè@�«þ«W1ÄŒIÙD�$Y0Ñ_å*”Z o ëȉ?#)ÚŠÆBRÍæŽÞêù�#å¥ß¡¡å?ÎŸÆÆ°YôWŠ ¡Ô»vá–ü•5ÜZ»†2¼%`sÕ¬Â`ɪè";…€Qý‹·\£Láî‘/pQ*œWÙü•�Ùü•ö‹qjžÚ¿ö…ÅäÂ’ß®›¿r`6…6Ÿ_©–毶'ló+¯Ä^¿¼€=0ó+¯á ¨¦µ:ok�DYäÃÛV ßlÅEÁxP 6êtsý&ÛT©¯eˆ²Õ�w¬ÞsÏ=ç�{î½g>óX ‰Äþ>ôðÃÇBþÓߨÅX&fh;Ç«îitïì½W]uÕØV¾ –Èr”'uèŒï�ˆ(œºýÉ#�å#úÛÁ<–ôGÿ⣼®ûB*Ó¨Ü{Š o{ó_ùÊ?|ô£Û–³hÌL4 -¤çP§F3”Dà$fgÉú©§¾z,¶}öÙ'6ÂØJ~In²hSU°š)ýîuJFÓÍÊçQTß¾a-òB*YüoáVv@QN0/«°üVav’¿‚U»ÜÒ=’T^î¯p`.þA{ÑYu˜çRîr´?’.c²K0tàÝ;ÖÊ-êQ¦hF©¡>É_I%ÚØÔ_ÙñnâbÓ±æ›3–)Ñ×Ç1§òUuç¯X³e© é;«èÕ3|ð' øãö½˜¤Ý¶5E‡Ðz¬¿Â³!]?·&3’QD¦vu±úë¯P?® 2ïp£P}Ký•FçháBÛ©a_"îL¥À°¾,r�¡†M4·ä¯ì>ÚXýÓK¬h–ݲÌaÑ_éGõI.qÔ‰ã �åH&ø+p‡+o]öz¦‰ˆ�.ë¯èä¸c(q!JΪ,²#£·b°bYÕAp Òùk4Šã°[4EÔP}\é‡ß§¼:µ¿åþJEPš{ˆs³*#¨ÆKÞVüÄáÝ«Zxàxšë)œØCQ‚s7ÕÑü•vØü•# “hþjâüJ;qˆ7µq~ÕüÕöǃÚO›_=ðó«ÜrÊ£Þ¼�‚÷'¬òÖSV¹¬Y»çÚ«Ïùئ7Ÿ’ #?Ç;‹W±ò�W›ú󽿳¼hÖÖæWm~…5´ùÕ&ù«�±”�Ë­³g}¥—]6Â^®þ Ô`À+²i&놎ç>2ÓµÕØÍÕ^™X{c�èý�`zÁ|ÁñÇ?¶ ¹žsÎyk‹Ó|a~Ý!ëžÿ‚ç�>ãŒ3~øÃ›Œeˆ5ü\¦Ì¢ „z_ÿÚÿ<åÕ¯>ö¸g�|òÉ߸üòŠ” [ã_~åW~e,Ú믿þÜ·Ÿ7;¼7S•Bozêÿýø'g�uÖ±Ç>ó×N~ÏÀ2H€O;mü{�ßñŽw\ñOW ×–çA  Ÿ%ŽŽ毻æê³Î:ó˜'óŠ_;ù“]dT�Ñ—"œñ›ÇÇûÛ2€Tßöò=a#N1dŒ˜Mù¤ÅïcÁ¿ù¿/%H+ÛÑ@mE—°K­€ £0kšǶàS9BAÆmM”sk½#~4À©fw‰«¦@IDAT“èº÷wwÙñ¿ü¼±ï6ž ÌSžúTâ6Û"ÞÂÔ#ùHO–mïùãwÃ�‚Äv1ÝËÂüõ7\÷ŠW¼â²Ë.[ÖÂØÃÂÆhK ˜œŠW132[µØk pÛÞ4g?,<ùÉÇŒêõ°Æ}øCv(A*…q¢à…ÞE}굯}íØV‚i ŽV; ¾æo¿ëö+.¿üÚk®µ€û!c¥þÁ9¿à#ó–m,ˆcqºËJ ²}´RYí(B¶å •Š´YúÁ!¢�ì¾3ÔgˆmGño €îðÒOG0Ò²‘}�/5ŸÎ¢vgù+u"9ölñŽØë¯`GîÒW9Fʨ+¦!] M@€B�×üœQoš³CEq¼¿#ÖUÀ³~!½eÐW+ìø+¥Ä¶Ä�I#6�mæ¯Â‰†ƒÈâØè¯@m˜8±³´u¾(ëb¿-óW& £³j’¥þ Äà§b„IÁÂürÒVmÀˆÓ†Ì�õWòÊ*fõ4«¸Ð­h¥í$%KŒ u¢†HóÄ=AOÑá`’¿²«6zPêÕÓ–Ú2¹5Ë=GNìg¾åßjà”\úé[¹XôW)^B­Ô —œÒd¹¿‚uÈ3šHÌ’%þÊ’XEü¾ÝÛ�åÜäåpQxÌ¿æx¬ËýUX­�Ô³hEo}zjoÊ�þŠU 8Æ_uÍÕˆ¼–ÚZWÍ_imQFóW’ˆÕü•#ÕaÃw<²c³ù«ÍçWꄵä jþJ?­&¶f�µù•jbtåêþ@ίÇü¶6Ý_å£ßXû¶Ç«÷Øsíš›_þæ»^Úŧßyþky›ëÞv›á6.½Ul±i<èU½Í¯2_jó«6¿b(ׇ+BÍÓ�ËïÐüÊ%¦ºQ/¹òæ<× JRÈÜ×H`KÜQîê'»6¢ª ‡ùt|23@²D{ûÛßÎSÕH´-oË[P?ýéOŸø²ºÀ¥×;í´ßøâ¾¸òÊ+ÿùYÏ:öüóßu /$�Å<žûÜì½_ùª«�¾öµ¯U«›o¾ù´W¿æ3—\ºþ°G"sr;ðÀõW•›ì–c¾èSýëÕÿʺƒ:q¹i…ˆäÛç.½”·CÖ›(ÁùÚ×¾îSæQë×£B‚Ñ—¾ô¥ï}ï{—¿§1O~ùɯ:å”7ž~:7áá¡›ÿý+¾uÅ'ÿòѤªÍ"xpÙ ~GÞN„È®ä4ü†¢¦Ç¦’Äbú`ãhUaÖ ñ1‚XÔ1©JS N ´}=““Õ½sýUeœH¯ÁºÀ2[H*ÙF…XŽy‰ØðcS^,‹&L`Ó_2}‚“O„–iUâÒ©üÁÂUA©(Ê P14äžI5…¸ T£Én”¿ÒÏ˪‹~Ð�/}Q y�=õN3yìüUpâDM™UˆEr¦Ó°«Ä™N±5éOÑ�xIǨ‹MüU~ͦ=�ÌR¹ ©Øg"’ͯH›ÃQp$b‰cHó3ëXåxÔ&bÜ©�eåU¾ªt‰¿R”ù�æMþGÎí ÊHñœ!ʘ,V È%k%êVü•ˆWiàqìÉbtEEà³§ao©¨Ô Ø¢4MU�Ÿµ·P=W Ìk:°ó\3qìØ+Áˆ$éôæ¡X”T?ªP±çU5ÄaÏKŠ…þû†è°.61PÊŸ¢ƒ9 ò,�a‰&þ ‡E7yQndë2¶™¿ê`±o„‹0,:ÉZ† j$ hªÈ‘ú`AÔ’2XYìYT<Ò6Ð2&¨å òŠÝ|ñË=©¬bTZ`|.=Òüš±í£æ¯·Z�ûü7åèlþªù+FÆN�ÛüÊ+±ž†kT®Ux¯R^ëê"uίxð†q¶+û3»y 7' nyùû=þi3ÿòõµkÖô§»í¶ûôÌ*2t,—cÙ¹käŒ 7Œ:nó+:йI›_µùÕ΋¹ÿˆ–3¡g¶œwU¢^<ƒ³ZæÊˆ‰v¼Á'¢!eŽîls$ÌÐ¥LÜrÂçlYŽùÎä‡À`á ƒ"ûö–·¼elû·þÎÙOúÏrðÃå°·ð¸Ç>öÄO¼ˆå`ã¶·½íþžð„'üÌÏðŽÈ©k®½fl¦†¤Ão¾þuŸþÔgÖ¬ÞÉðFH¢¡±©7è�yúÓŸNÖéIOzò^{íuÝu×~ë[ß"ݶ) à|ÃoüæEŸüÔÚµkÁÆ«eÎ>ûlÖâm ³x|áŸý üÙG?fÍÚÕ7þðÆ«¾{Õò<ð¿õ[¿õ×_øëG?êg )ü†ã7ÕÊMÀÙElÄôÄ)ñöã[¤Tœ€³ç‹qò¡‡6¶Áßg;üðï¹æš9¤k7áØ7€$æ©Ù½³ÆK™ó#‚ž½/åHÌ_(–íYÎ6iEÛ2Ø¥¨”ªô½zø¶C ößwßg?ûÙ_ùÊW–B÷z_øÂ uäž°ï¾û^ùío³¬l9Ì’ó ÆulªÖ+Ô„.qŒ¨T ¥ €†ÊNý©&ÇÂ[è!ÅØÔT?–�|âm·ÝFÊx±/–ð¹Ùi ÂñÃC݇sgž}öbnñò+®xÉK_òÌg>ó„N1©Iðm a5¼ø3ŸõÛgŠS]åZ0£Ö@·ÏØ‹é�!ÃÆÊ±²›ïÁ\âj1%Ro‡6¶ 7Íõ�@|ñ _ôØÇaTe¤NŸ&¾„zËøo€z¥­5êÄ‚D˜�¾í¿ÿþúЇxÂ÷0Ú©H�{´`å-gœ16õ$¡¾öõ¯o;3^–£jE�ÊDÕÚ[¬I a CvܦÒvb3Ì\èñò_ÖòJÜqà–U–p1ƒ6 ¬+·wU=]ñ™OæÊehÿG¶­ éõX‹÷¾�h<…ϯêT�MS“}�*éÅJâÉ‘ CpÃfõÁñ$1BÞÆñ1: ³x35äÉs€$bIvý3ÇWÊ”“�0Èãì³Íý•U%5rÂRýªKiº�Vü®hEF@VÝÈ)ÿFü• �Ѐµ.Õf²!‡m,WCGAÍÀ*Ý,ú+êjµé`­ ”¤pL1Eoœ ‹ÒúÂ…Ù4؈+•eÎæ/œÐPôÔ’a+ ð>ûH½)3Zr¢üIñ›Î(Ö…„hjÃS‰çh„¹¢..Z¤Âe6<䡤�ÊZh°³QáZê¯4:ÊEÚ©ÙD_|H-ø,:-˜ðWP›æ€ôjüªœo:K©ù¶£c PéaJP>ýà^ÉÍ�ƒ|š&�ýá)Å4òW¢4)W/‘ ¼(ÄÜ©ƒl�¶ n.ÐUæË¡3èCÖh~…²«1‹k«ümæ¯pŸ¦ö´—ê Á•hY´QYrÖ‘¡Ì%„à�#e:X#ö—ߺƒËòtðC‰ìB)#_M†}YŒi†³µÞê }tާ7Û(¬½®�Iæ�v#ÌÈïÔ©YAÕ™†QXeÉö£¡*]Þü•‹L›¿Âb0¬‚ïæ¯-Í_5§Ê¨¶ù•—\:¹¶<¨ó+½/> øáÑëi�™™Áð§³¹j÷¼fa0ã/T^ôÚü •ŸwªÐæWm~¥=0T Nwjû+~b–,›ÃœR²em\¥ë71LNžU0݇�"*Q *T£ÈŒn4µS‰- õ8UM [+;~ #�fw¢$jƒ~õºà�=~Ô$¥ÁŠ µ Õ´Ér)hçÌeÀ<Ñ4QñŒWpScˆ™¥qV©ï°hSX©$=K+ùPn!3)Šä̸´ïÙ`Ϧ&-­f/8 ™Pwª×öâ —¶^ê¯Qb®(fP‚CFÄÃòoÃä8r—ÂØ{ 'ÐÂnÙ_¥7}K—/ZøšžpVÓ�–J™zå ²ã±ô¼9ÙZŽa‹ÿeþŠBйØaü‘¨c,ÄEÓœVÜ&ŽÎøBT!¬§¦”×N¨ß@I™òø+:zd(9 /"&5¶è¯‡O9°'êò­*å®J2Y4¥Áì-“j�T}ÇD‚K "8¢;ÓaEñ_ü¢›�û°" øÂ_Ùnìó‡�a)ga›?ÈQì©&{T~8�s ·µ…Šs¦j¤ù«æ¯´Ì£ù«\MP5Õ C¦ù+ÝGóWÛrAÐ)ëuÛüêAŸ_1iðÚÊ=¥y­Â»ï±zõš5«×®ÝsÏÕ«WÏìæ;è,ºkI<ØæW\ÚüªÍ¯0|sYæÔL›TH<epyæI&Ì£)+à[Œ‰�Ù˜øÖÕ…´8ól‘{ÝÍÏ^‚ ù¹Ÿ5<.‘r Ï ¦…wÕerÌ”Ýií¸Í(ƒgÆÐ�ZvårÌúW©0¿Ÿ?øàGüîïþî¸Ö–½ã�ç_ËÍ€›šÎÏzè¡—\rÉSžò”Ið[.çÁm§žzªJCtSŒ¨�ÇÕÈÉ[ßúÖIw‰n'Ìss(ªÆÃús¼wÍóÞ˜øÃ¯|å+·ÜvRík^óšw¿ûÝ<Ó?¬~qâf¤›øÜ~SS•š0ïŸ;¶!I¿Ð·p¬]i+t4J Õ2¶Å’¾òÊÿõÍoâ¸h�È×è«7í "¡LâdšòfIÀƾ–à¹/§Üžù¥/}éQ�zÔ°’üABzÚûšßô¦7q;ê6R!‘wñg/^¿~ýrxîÅê‘­b)ïØ %¨†¥cAºBÃWnñ'd†ëGÁ"¾-µÚ¼îœsÎùÄ'þŠ¥›{fø ¯é…¼÷ý“~·¼á¦%@%×Wäo`fÎÔMÚÑ ?£q`Þñ¾Kiª7?F‰u©vpò”Ó`Žv†¿JßÀŠöov*Î~äi‹þ e:b°¬�8EC*@Ä¿âÇ_¥��ul1‡J”$“y¹°ï#2¹jn�Ò3°¨ðMÔiÕ#å Vµòa�Ò`ò€ k{Cd®§Ra¬œÊK0DæYŠÍl°ž¹óWrÅøq€ÒD<é$ÛÛGnö²„¬Š=Ód‚S+HÎѦþŠÂÎr±� ķ䯰 X0Íz²>(4i<•¶Ä_éøø­h�Ç�©¡HÞÑ‘QU+%¥ž¹j‘Ú•¶êÎw“†£ô Z¦†½é7})z§¿¢'oVQüeSzµf·‘!ﯨäjì[Çm�E�PJwØUB ­ª›ö2Ôw{ 7¶’ÒqþÊ.Ç&*u=ùEM§67ú+䄆Fãc'X¤°WóeþÊ"•¬†sÐûÊ6ÆózQéÔ!¯«¶%Ð7©qÉ›ðü³ŠM‰FþÊ1$»ÙDœ½œæF ˜|9#rÓØ?ùPÅÙ¦ù«æ¯°,SÐÖYÍ_9Ôš¿òç�æ¯pÑ:M¯m~å ç¡6¿òYnýiÞ§ÀÛLgvãµ§«v[½šo’qýÁ*^vêõ¿Í¯˜OµùU&µm~Åõ‘YÃý:%Æá2‡�ëÍR0Yå÷d¡W#'þöBè–‚™¬S\rK†@”ÎgD‚Ó»éÃÀ â<±¸ÒÜ`ôß'ýó·_üÅ_‹�Âß>óÌá,/‘K‚gnaÿýöçŽ<Ò Û˜!*´¿þë¿þÕ¯~õùÏ~EJ²MØáÕFN«×{ýë_ÏCëׯ__M¶ºÞóž÷¥/~ñä—¿ ÝQšJáînÚ Î;ï¼?ýÓ?Ýö¼Ñï—8ó¬3ñ”CG¨Ü.‚Ä$~ºù¤JU­ô^©VxÅš¸‰’„Oj¯VôV¯wÐ�þñ{Þ³UÝ’ÄùÀ?ø´§=m£H5±'ÈÂ�ìp1?â¶Þª�±‘tãVåw¾ó�k×®Aå‰CHÈ,‰ÞA{ØÅ_Ìûm·JóàpOxüã‘k9plDÛgœÔ�…Ëaª„ÜkFŠcE^ŒLÇ‹màJˆJƒ˜búÚ¦<¢#›MÛ”(ÝÄ‹D^õªWAa,ÏQ9cŽìÞü�9êóŸûÜÞð†­vî" ÞÿË OžûÜç:Ö»>J õò§e™"A46Gù G¼½®ee$tP‰©�3Oša#œ4ž†E Ó3þ@}ôçb×&KÑe~ ÷Ù_9Ç@Ëùp.5Ãz6¹W¶-ù+YŽ…•}ÛÕZ8iÅDsÌäIœ”ª¼7€�ô™©I7qP!Dwn‚ÆÇîËýÅŸé'Dr·™¿¢Y9TíŒ~Áã¨Æ­=„�v6J¡Q”!’–¿¢TV%�ƒâI}Ôn©Ré¨7%/�ILº=’�ƒÞR`´Ú˜cµ{1Ȳ ŒñWÀ˜h¤-ëø|ç„[��x‰¿R9¦hLä˜Ôá<[ˆX×t‰�œìåŽø«ô‘I7µ&Ƙ ‡¾V„cºIÑBÒ=sîX,UìðE™Ke#â›óò€rý\©Ц€±YЋR,!`§Û Î20+mŠ´\Åá„'›ú+ôýØ­&ªDɿ[ôW4­<…U(êUöeþʰMG=Š>d3ÓI… ví¢†äÈ+ì”Ø«8ý•kðŒ0š�zMÛt‚h£ ÔêÃQ 6³qRÔüÚÐfU’_ô€£ÖÃæ¯š¿r´aÍ_5…x!òrÖæW\QÚüÊ˨W .Àm~e¶À©H›_µùÕæù«›_ù~2gÉ^�yº±afÊ5'Ê »|‚ȯœ±ˆòù&cãM¬ñpJ7~K{oaÍÜÜßÁkýиy§Æ4õGó©÷¿ï}¿ÿð·û·K^b$ïû¼âò+žôä§,©…ËfI7œtÒI¤TxÀ¤GÅÑ–§ÈÿüÏÿüK^ò^éÀ)’ ™DDrN)“§ô¼¯Ï]¢¬¢"§Ã6éa¤ÒŽ=öؽèEähÀÀŸ¡‹?È?! Z4ô!´é÷žóœçwÜqÿå/éoþæ²Ë.›ôˆ®#�<’÷9p“éãwD˜21@”’«¡‘”ñÊ„Í@CE£dCõ¦ö €ÇBëßû�ìS•ËxŽË« .¸à‚Ï~ö³Ëi®_¿þ”W½ŠeÎÌÌH™k“$[ˆÊO¦üRl�H�³|×l›¾HBñb�‡=ìaGuÔÑG}ÔQOdµtG „�+¥³ðÃgÁZ°rdÎD2¡Çüëßõ®w‘œâÖÔo|ãËIb'˜©7ªÐüë^÷:Þ§±ämF˜NSÕT qÀ,GE‰1£³á';þù™i9d­A³ãúÚ^Ò**û…g<ãË_þòG>ò‘O~ò“Ë«óH>žp 6ÌÐâ?¿á Aáº�RlŽÅŒˆ¹…¹>?²Q)8�@S€3Ôhæ ÅHÜ6„€Ö”U0¢rœ¬§�/QíUN”buÔÆv!*«[õW´A£¶ ÷ÉÄ•ùg�¾J@T–ÕâŒvc 1 *Y ³\:#[˜ŽaÁ£ +ŒGKЙ�—EyŽ*2Œô„6e›:#kéL1uæoÂKð†nÚQkG¤‹äg™¿JϦOD¾¤‰Á°|Ù+¢€d¡GñTµ·g» ²ï4AZ3N<�oÐŽõWWuhWUØ!,(† ä”.5›ú+²/òËGœòÅŽæyÒ¤EþÙ4¿že¥ßi”¶�à(->œêòW’àØõ Çx …!‰JT$äÈ®ŽBúƾä(+sĈ\;+ÑÆù+»\¢4Ý'^ˆj¤2‡=Á€—Q‰É"¦gƒ¹œÚ]W jS¥¹Æ-£½÷§ŠEÌìGþÊ–ÌdA"â1eSþÍü•=¡â…_Öê…1´á§öjJÞX6²”Ê#Z0�4Öu à°fD‰¿RL­ô Æ ófaT›³=¢3d`Îv õ‡=ÆŸêÒxš¿rØÙ±t�‡vdJè€üñÕü•–”Ë@Ü+¶ƒñ5ÕüUóWxŒ FˆW¿<Ò‹wnDw­gß$¬�‡ió«ÌbÚüª»@cBm~ÕæW™L;¯ÓŸàZjö–yºþ…�ae§Ë™pYvÎÌüOŸCSç5Þ§üÕﯦæÿmèDÜ[F#?B8×E´LiÔùnÅÇÈiU“‡D×¹Õ¬~JGbXº�;™öthL´Žš™£Ç“£ÄîY6iÜiR8©M"L“b¢'èÂÚuüÜóo÷ð€Û~tÛ�üãŸþô§{î¹çÞ{ïµÏ>ûr"ëkåµfRf³Â%ÏVL�g‘‰ò‘:>Òò°écø¯¾úê 6ÜöãÛÓƒ}÷Ùg¯½÷æ� ›$\üe6H’Áœ1h_¯EÅZâšÑ ë5æà“÷™ò†Ê;ïºsÍê5{í Ê}Ö~ø’…HÞ fôMÅ}†ˆ‚—âNÛžÂ] ëxÂ5D3Y=”¨-!Otë®ôk÷%|™ó z,pÀ T²Y~çÌV³³³×]w<‡C4yøá‡sÃ滋Uµ;Í{à ? £:5`Uq cÉí™$¦Ä1ÄÑp¾7CB@yW{~qPýTG©,ˆÔX»¸¥­øxXy¸§¢ íˆ0*F3È5ŸÊâÀÛ*ZÝpà ßûÞ÷n½õÖýöÛ�Ô/—Øÿý ¿V‰d_bg©ƒ2jwœú¾½9ž‘Å¥ìáLfÀ�îÓ^P+©v#q:hE—¥ðh4To3�—Héi0„ùüà7Ýt3–ÃRÉ?ô°ƒHÛ%= µHÌJ-´*nyìý‘~ƒ¼°¤ÃTx6 ˜±F0ß}÷ÝdÜèßCÖ­c!z°×Ë›1©–F» ÄK„ŠåŽ �Ðp tNŠ}¦B0@ÀR ™,Â0ÖzóÃôF‹ËØ‹ìeÑQ©ñ+kX°2 ï³¿Š f~ÌX¡!¸„Ф՗ª×lÉ_ÕÄ#ý0ˆŽ DôKýè¼ËI±µ–Æ‘óð¾U(¡*ÈZ£Í–*by[õWP¡‹ÁW>DCê¶AbšE\”óåàs²¿òW=[Æ ±nÚ®©ÜŽë”Sê»gã(@=Æ_…%E%ÔÓkbUXþØ@>Æ_㈠Ÿ Šáˆ G Ë8nÓtàBo åDî˜ú”]ôoküXp$ä›è«2”,)Ô¹€×œšY&ÔC²ËÁ¨¹‰(ˆFíS tG#%U6zkþ*PF¤Þµ°@Åq y©KÿC�£Ø]\C>v�VÙF…1ÌðkØà ¶I¹ØŠb×,ª<'Buã�S¤îÝë“�¦ü•ÓÒ¸F«1,hzÙæÎUðÛ¡éâ ‡\ $ Àƒ‡ïš``Ù`�EywLJºiÚ©1RîDiƹ-Ô›¿R‹Í_źš¿bä4ÕüU›_y阶ùU®Ñ\c�'à9Ùµùjhó«6¿Ú4ÅÄÓYö¶Ï¯æg‡ÄmƼ‹�™ùŒJ-:Å%ìÀ‰kMs Ÿù¯£ÑÀĹï"Ž:ÜH G\›ðƯ¬¢4“AÜà̺Èwh—ba´Ó°BÉä¼ÛIY¶œxÜD‡Êi7Z1Ö3žCQd}µ!KG'µb Ú uÊ^•ÀQ?(…†FïuïѼϫI ü µÁ”9@}¤¥ÒNÇaªT ñY:H§FªPôlyœLnÒ¤mÅcC�lª�?¢LXáÈÞÛlC‚ÄHV‹xˆ ¸qdÁ/’‹äÑs*ìWU.•‘ÂÓ³R&Ζ(øèhèzçû9ßã…: .‰©Ín –‘ˆ£“à¡0�ÒdBûð,ûR XMƒüS<±j&k˜#ÁÙE™H�óú2��;9á+]ý 92ã�öe-<»Hí-nU ãXÊ31$.By&ÈHñáÞdÇ—J7(E^r#èv`§¨+¹Ê¶‘Ÿâ>avàe u�`²‚vè#�7Lš¶R} 8Å(\KÐVË€�…ùYà¦x£±ánéN‰ÔjvKÌ5‰Lp,TAš™#õ6H憩dâU€i1œû]üz+èÄu3Å)C˜j5ßý•øQ2{P®%a"ãb[ü•Ma>«ŒÊ4ÌuÁp)FåË: Ñœ»9Í!�[K'€Ešë� Ρ߶Ã_IBµH,žhOÇ€H}R,TÊb›üï § ¸´N�cX0¥Q=ÝÏ·ö �V8¬ÓŠá¥9’¢oµo+¶3Ö_>ò;8 hItzƱ¹|iÊvadAÔF•á+gtØ;^mds‡ýU�Y�¸¸>‚ÌdgÈK_ñwÈËBéj¼üZl>ÕªíôWe[H)U �³ó× )NñÓW(J]Ó‹V•VÕWu ¨BîUÒKêP�Ø- –ýF…Xki�zy$ŸÅUÛEm[òWàÁÏ€P'¤¡ã™éðˆÒ" ä4u¯káÑ̵`4ˆ\›ú«ð^fC%wèkô*ðVIм6n“¸u45f~£)›BõvEóW5DOÍ_5ÕüU®m~Åt¡»bza÷’â%V—©×œŸjó+š´ùUYû6¿jñàvͯ†™ gÆkhOãì×Y1—Öˆ“«¥Vã•Lä“Чdåz¨Å A hÁ(Í":£hgûàéˆ& ²©¦Ä[!VäCÖ)uÉ2Bcœ"޹&1<0£÷Y±¾ŒIÞ³ƒ >¶svép™g²tÁSc(¹¨h® dÀãF9 3š’ŒEA]hmÜe0ÐP˜çÖ«Š- @„†S¼¾ îÛ #ZÙ\¤\ÜÂ…”ta2•@HÍ"çtÞ¦Qm@e ‡ì@'úÉh%€Š°B%Ó’PVåÚ�Ù/I—bØž÷’%¶!øXH  /‚¥= 6CèèÄFËŒÕp'qŽ}QCØÞ´Ç}�…b‚– TÎàHýP&Á|+JĨ¦»Š{²'»‘ž ]9¤•’€Iü×:hÓ%WM\D¶(ÕÀÑ]*S:´Ø#B¦3I`RHäíà4Uá¾Ø�¼Ô²�#ù¢´E8‘ºÉOw¤¤œ€Uõ�“´§C™#HÖeʇ-£œ%KE©é,ÛQ¼wxÕ®$ª–Õ§÷å‰K2Ú.eAý"—˦¤ÊPµ€›® ~eÈÞIJ9>‘Gå�ƒ»Í"ÿ¥�d!d,”!3H5†*¯„µØ$•˜' /5‹";(¦õ&rm(ËsVÑÀ³Lí%z äåq SÝ+sÛ诀”uØ}ž…Y÷œŒõWÉQ�¿šFçiŸ†…`䯱2¦ o“¿DÓ r�AÎQ´V]‹áAoÛüU‡¥(’Ý bà >�û^‡KYî¯âÓRqÊ ÚËŽÅ-ú+ñ�’JÑ$àé­4ŠF*Š4¾—ø+; ôF£,qK7G-²‚$Ûë¯âSJð¦ïêØ@‘=JR×P%�PbÓfs*ÿQØöø«t—4 ±®@suMWpSDWlâ¯ÉØ¥Ü XäEäØ'®ó™ƒ×ô˜`p˜ï`…Ë®]8úUh¬@•oÙ_I@®"\ú':ß碋þJ7 ZY†R1 \(R‡÷2™tœí½ Íó T½‡œ_œ±5…Ñ£¶t):õâ=š_Ù(°ù«ŒÙ毚¿ŠßkþªÍ¯œyë5¹Py•Zzqò2Yßm~ÕæW�=` ÌbœQÀI›_9 s¤0ƒkñ F‘©ï’xеLÌÐ’3xAXzaˆfpb�âF±bŸ‡ç Kê¦+4õÄÆ™\æUjþK¹—õ,ßâ�åù»¿-¡CÖO$ ‰—£ˆFògá†!ÄÓòäù@fof2nSÃÊc(©<×aÍùu¥p*ï¶•ÓäH(%ìU #¶Hf‚ËECä–.»„¿2%v”HˈΕYql©ãŽ/"ç fDNyô�àŒ†¤YT©aŒ"£Pi o•{ôK¹™9 ]´’hO9å 6§YSAöl€l u:í—çH»Ê_‹¢cî·r-\L 9çnYcOVG©¸c«J•ƒÆNóÉš¨ õx;"˜l‹˜”zé'óCa€uœ¦‰ià‡ Š®hh¼§9)—6)AHUؠȃ†©44,Tëà·¢³ùö©Ý H''äN‰Òy>®ªðc2Xl2]‰e4Nd´=ߎøM$©Þ’|”ÇþŒ„¼¼O!ež¾fù‰%³j4 Ê3’Š31¤<+gœWS" ¨1)×HáC—À@‘ ŒK)%Õá8R{"‡$dD‚B„iʳ$ÑP`É j»´…Ýh6µŽ4h(É“ZH Z¡Â4­…U»›bqÈ­X;4&ªÑ4ËT(3y-ˆ >ÚÛ‘˜Ÿš;%†\1ZL6Tæ.í¯”ÅžRiØ•ø|DTaŠ Àvù+tæŠSšÚà¾ÏþJK…?{#ù üUÅ^Û‘Y‡y{pýVKå óËàP§ükKÄ»š¿ÊØÉÀeÇïñôÛ¿c¥¾1@‡·f�}xÅ֨ﳿŠì15�KŒ¼ù+†çý;¿jþJWz¿Ì¯|uÓoÜp랯âE¨mMi põäñ>pà#y¤s¯6¿r‚ÍÏ9R&N”œ8yÅf¿$|ðýÕN�Ûüjc<ØæWØýýŸ¿š~Û¹çfk¬’‘G´’U\’7ëÆü™Ág“ÌTo9™.xÁê�qêf “”òÐ¥pçQ8q‚x…§™$½Y&1)sœ¬–•ÚC% (¶ Ì€Ää“z’ °å& çF•ñGz[ñŸ(”£dò“†-­1¾5Ä…�Š-%t²X·MŠ�Ã0l©�4+ÌĘ:î4á«Ú_È��_60 ¤*o70èÒ0ÅHSi iS+ñ…ã¨�W’É©i½$Å"CPÊÏ0¥¹9¸¬ÄEX“ Q5‡|ÄÂN™Â4ý +RH9ù‰ÕMGÑ7Çf)õÒAÂb3ʱT• ä$¨4`c]Nh–TI¨ˆj�5ÛH�ÄcqV<JJ¦£€˜™©žDKMz]x›òP >K=‘º w9�i’*(Õ WhíœB�-i,™¶e”Ø™~' EÑl°EËYwiKµÀ¿ì…gƒkÇ neíQP�qGí\ÓÒ» È‘íá Î�3:ày`Ûó«\)�€ ÃêŸf¡ Å2}mœì{Àv.5û­VÂ’âhœ±%¹D�ªÄOqàQš#Ú¤§ª¥�OôÇ\û}×Ë¢Ãj”ŒX529cûÔȰÔU„ÖÃ)h’¿*|ðM±`m”ʃÊ$êY2ßD=Þ©î©šŽºØÛ²ù«Ø’újþªù+Çêù«æ¯ô�:X¼¤>³Í¯¼p �6¿Òr©½çž{¾}å·o¹å–{ï½Wsi[ÓÀ ÐÖþ“ŸüäG·ýhß}÷�™qj›p‡n‹ Zü% ŽÙY™h8Câž C€ž4¹ ‡©a¹7(ÍcdÊÏÓÕ ØAG5†ƒÜþ»È ÚSíaøÔiù Z€j[ñ„�¦ââ&¨�7sR…Jl7_EEÎJ–DšæÔÅr.€’´çÁæ>:Èïmn fÎ*»ZiØÖ{÷Р�§š™MÕŠ6­ù ¬ß!'ZõLk“ 4'Ê0À0e:2LŠNþ Tj …æ¾]æäˆ">”'¾�Ã� ¸wö¡{�É¥P‰†½y0ùCŠ;¼`A.‘RLÖ[οÙ?î#“¾v'¦v`Ò{!ƒ(H@‰r�‚Tî9²2Ž¡nN Ò–l"¥0¹-2=RÖÌêOæ‚,]²3Øm潋65@:ŽGiÊt˜ØÍ?ÖE6˜èñ±8Ë”T¹U x€Ô”ƒÕV)!OÍÁböIþ%ªH«ÏKÑâÁhƒ—§Í\[Mf•9y…k’ Š,q%ëõó²Ey§¬ZR f&¤ä#PMǧ¼Ê§Ø„c¥Uø ']æ"®Žßn"[Y %�ôtHž)œELbKfÁ©Ÿ(Ó+4•aNÅá¦Éʳ/IJc⧦q �ä”È[©í©bE\ˆÉf‘@` 稜X’¶ÁÔ,©B°ð¼ëê]XHöG``E#üÕ3hÄUdÒOeQ�¹€ »¨[Ñå¯ ¶ Õu¨íH‡8Ü”˜wŽîo”gàhâà Ì&Ñ–�)H–ÔÔÓœ|"F›P dHÉ«I튃  ä$"QœÞò\ž«[JX'eåO$Ý—JAfÃIÍNË!Ímh'# ãˆ::Fù€ 6ø•(‹2eÀÿhº6ãM#4Pº`G é^°P-'Œ#><1Äi’~–—. )Ãa<¤Vk´Ò81Óí5ÀMå }#'ú 6ZÍù¾ÅêKу¶=ˆnÄ@ÎÎ5o>–Ñ�eU!Oj�®QA9D3�r„(¨n7•÷ðŸšæ¯š¿ÂbÇÚYóW •毚¿Ò½zÙð—Ÿ6¿â׿W\’½Œn2¿Ú°aÎóá8Ó î<ýE±mMy ƒU«VaùW_}5£`ݺu9ÙdvÙâAçûZA‹[“Y(xÞÕ|ê�6ªú ”¦ý´SUc£ÜP ï¾RW–'vÛ“ãvp¢r<²MrñæZe B�¤Îû[ʼnÞС$lZ–!i¨•Z•ûK˜ÑÂi]bµ5ñÍÞé‡�%~V:’›ö|ψ^"ì)~þá\ÊZº½¢NH«Éj„  ˜ Rt0AyUÔBâ"Àöª5.8X!•vaô€«QßüUóWšNóWŽ¤æ¯š¿Š/ÍI—«Q¸éFãxQŸr¾uEÀ9ãßsÝjó+.`áùïUÀöÜsÏ»ï¾[«h[ÓÀÊÐYf6,qïºó.æ�N)[ò” a-Vs0J¶�¬ŽIÁagz*)«];¨`¨×þrS•¶/1‹,áþø�ä•(“„šs9‘ù+šå_AÈsy":6š€BÇcYáô€sònp@d¼¼Àí\hæ�¨�ˆb‡eZES… £)_À£o’~´çdá‡õk¦°BþåÇ�_`sY´Ô±¶…£|Hsš^³fßÌ%º¦,ÀÂrÄ1›ö€à Ó88-sGµü&�5EQJÄN "¢#Pe}€Ö²Ì»aIð‰WJtâK5Fè·Ù`·$ÈËocjQ>&f¶ô„#i1+æÒ2{$!�°m×7—%…ØŽâ8*FÌ ˆÔá &”¼k­Z“k ;¡uIÍž­¾ :šRè�§)¯.tèQ1´ ËÈ 52«8áZÍ7Q�Üey©„J^¯s*œ‚t‘êù+: ù+ÇJL°ù«æ¯Í_é5õ¸Î Ê£ë•ñèxQ�¨ü*`8Ó6¿Zñó«\|{<ú*æÑvM+KeùÎDw8¼éÆÞºáVžWje)±IÛ4°+h€‰ÏÚµk<ðÀuÂLÈ“σ—¿Ìõf§ò¨®¤~ Óaʉ›Ú4Æö1gm•ÝÉÌÍlOÖ '+9ÌŒ¸HÉĘ­qe†ù*bŠ,4GQ9\ê@zÀ8"µ!S9¨�©!ê'Zç€öR!ïLrg¤¦lh#W #‡ÔF�<ä6·,¾r ):ÓxT×Òž�f:ZO±‹y „Á‹ÅÐH�åÜ,€“\3UÊG#39³Íb³fdЄÉGÓ3YUè¼79öz|4b[PQì&]¹´�cù0ݦjŠ…®°6G9®˜…б,Àp…ÄR6BIs0€�ærBìj*6ÓWæJ$fÿð�0àOÏ@BÕÀ‘|“›ò[pç²CSó¯|�Tôé­Bè"Æ…)Ùýfu¤”V�*À¬b§Ïs ȲBØ0"Bô]ܘ–}Þ €œK„оáÖhî­“i)�Ý–Þ‡ \€D.YÃh{¹¬�lAFE²r0C‹ØSeÇ‹àðJ¡–ã¼3\þÉHÏžŠ„à¶œŽEw#–ùV$¹ƒRÄU©ZM.ªÉʶ:àØôTÙ, ¢´ºÕ”SG#E²½…=Š:ƒ¾,�Ð$˜•Iäj…&ŽW\¥»í ¡ã)Òep쨋€“¢&¬pdSPÛ%| $fKb´²Gò»ßüUÙzjþ ëhþ*ãÉ¡ÄHiþªù«6¿rNÑæWÛ0¿Òi°üǯ¶5 ¬0 lbù^C¹„no<øo÷ÜsÕU¾¨d…i®‰Û4°‹i€¨öŽ;îäï–[6<ú±�[½jwCÔ/5è/ÌTüÌdÅH ÔÙ �9µ$I=“öó�'N85<Î\—«s¦“ #ª®QVË„‚¡38’½ ¾®è†“k l°Àœawç]VÈ›éçûó<Ð ,¦ Hq¸ú\ý¹>/pL¡Ó.ö y‘°Hyv<Èà¹yï¹4^1µ¡,4$dØOÙŒ,(0Ù ˜¼‘ã„ q¯Vdy®fdˆ"Ø¡Œ›Ið¢“J*¸\¦PX$Ê Ì8I¤ðøUJá{ ×Ù‰ØÔ–´ÜÈ’Áƒ\‹6í“îà8pa‘­ ÐyÿÕ±&+ÑVÅBþJ<Ïycå R¢§€Ø0ÈsO(M¤-U2nÜÏÛ››îͰÔËä mÕâQÁ0€Ò"U^aÅí�ßçMõR (˜Lóy¢¾MPqh�}ƹïÎ'Mc«÷T¨ýH_ðêÞö|¨È°%]·¨Š¾“»C2ü+>˜`ƒÍ,¥�3 —Ž ÏÁ/8åÀÅ>ò®ö¦†.“Kò!nâ÷yrš¸©IIiƒèB1œJÎߌÀâ1÷@NÀä[hÊWêÌ¡•.™ß²’M8ñ q²eAÁ¬Ï“ðKJÎ]°—ÅI/S0ŠÎU%¹™H›â°ÄÊ,éÉ|£µÙQI…?úÃ¥(ÍÑŒÐÂbˆ®þ•,íà ²†EN5qT̨~Ô /UJC:Ðg½iÉ׆0‘Bî)MÚGþX70Q'áKò2WºLP›Jy5ƒsè)[0hðCeNÞM†y·fÑÈ\”ì8Iš7�YLg¤šY’Òk�¦õX‡SB‘aã†Ø’'œŠyl ÓÌšÍ#•@.`²jF3‹œjõ©UùW’l!®jQý„Hª5â)9+ÒŠ1Ó™‘c„?•�|‹êY0!bå���‰¹ºX„ðÆ2b£F ‹ŒlG®…Þ,à êÔâwRsPNÏ©EiÛ÷;Zƒ…ROåÇæAçˆ ~N›¿jþ*Ö–ËEóWŒŒ¿æ¯š¿*­Aà1ñšØFm¹J¶ùƒ¥Í¯¼@{ vfж¦�¦£ \§Îíˆo¸ñÆ;¼ÛÑG}À´¼[3¤¦�·`x2Hª X–©ÞpÃõ™73EJdû€ç¯ˆ‡»1>�.›á:¡o²*F¾†Ä8%Ó]Àï¢ï�„uD—5=½0àÞÈþ¿&f¥®I.þèØ ZŒ^ýI”Ësù˜tÙH %\7§dlŸöf„ "7™^‚>i„¤ EF@&C42Ï uÙÈAxLr&iFjÜH¹gf}Œ÷•J>ä‡É¤8i0‘Xc½™Q“‘J+å&A$+f”fRƒœ“¼¤�yú•Õ&IBŤ£›¹&Y â . â±r�” ]a”'ëÓ>�J|p½À°…€?Ê;f o¢´ àR)·(yšƒˆþ�‡Þ/i3c“©¤ kœp'¯ùYe%EUìGzÊÀcæÉä ÂEâF�)¼çj ºðŒ¬¨+ì|ÞZR82JRªS¸wÐ:}Ô,ÃM¤�¿bJ ¡êäfC1øÍ¶�Œ†ldʬ€Ž·;@”rDZ8èpŒG˲#³â]:®´@µ½¤ ÜøB'º ZßZª`ÖÆ Í)åbÑŒ¬:Í¥[rÛÆQ¨Ô;üp*°,¸´Îû›e�ׂ°Gb!ÃttŽ8JNÜFæ-hnÁ¥)£ñ@«ãV6CÃÞü´‹†?œ„GÐÑD2QÊ ËÓd�cð€±ôÉ£)¥•Š–“lX…ô±@]AH©;zÇõ©|•ÀÍ_©_ÞüUóWiÍ_•wmþ*žϪ7oó«6¿Êµ¿®ÊNA&›Í¯êºÛöM+\8KgT5ÁvŠºMó«[6Ü‚ÞXïÖ’n+Ü~šø»Šª X¸å¶SÂRK‚QStþÊ›7k›°0ºŽÛ¡,^ÈÀ™ xeBç�a¤‡l’lBMñ¬J¨L)Å;÷ãå>)Ìi1§ýæ! ›O»7A—Õ=æùHÖ…\}ÖOAA¬–ð7„â{˜Ül°þ¥’2h“_BŽ<|äÃz†˜}n�Õ”7Öž¸NöÎdÇ$ÇXÆD®„{m�¡kù‚*°IT@ÎJhØ,™]xý€B¡ tHZÅ­C?ôXîåÒ¸°,+~²D�´ƒÓ"Á�2o�æÁ�”N¨ãDE¤iÁ„s;%ÛciP� èµÂÛZ"Ñ{+LݰQ*\w„!Ÿ|l°˜ï’#5Jצ‰÷MSʱ9+U–Ø@^<—ëýYŽ-–;ÈJm+â lÊ–JÌ— Ð*•b�É…†WLr›¼ ´Ÿ4¦(;­I á™&¤’i‘É¡ˆbÂeW2‡�`gÚutÏЪEÆR$IÁ ©yxӮñŒ‡3å"ç$¹ƒ‘B_‚ †b€”ó4*ã˜Ê€R×! õãÒÀç‡ÞÀ©Â­@%ìROY³JÌ� È5†2¢¶ÀæL:ˆêi›Î¿[rž3A,�H6�D³É9ŠVV¬Ö‹d¦�…L©3•4ð'IÚy†q(  S-…‘ÝfÛŒJó«áL,£;ßÅ#ëÍ_¥+›¿Ši5Õü¾¢ù+×]·ù•Ä6¿ÊŸkk.ûÎ ÐÊh~Uó¶oXá0$Üþx�W£¢7î3]áÚkâ7 ìB¨ËÛ½{&F5.æÌ8?Éãæû;5�2f´dÑ-—fN ›�·ùx˜¢`—fÉ" ½ Ü ÉÀjÖó˜Ha3xV€ãm©ƒNä’š•!»5R“¹ª¢g;6RÜ@“A;Ðf “u³t ¶„£˜oÂ/Ž8>ÉÀ1`Fê”%:#AÀ!ð]ú¦cÍTˆ‚p�%JÔ‘'4Ò7ÏDŠì€|d-•<ª$e’©£Lo­Œ,ò£F‹IÑ@׊Sh§ÉáöÄœZöÔU¶FÜ‹Ë#MMψW@¥�-vfXà†~21!'ì”]K-˜I4»RÆ óëMŽ‚e<øÀš¾!ß«2íxÉ‚„„ ó9p‹^Ò©èÜäˆPV¨7ºu5$ rdal¥˜ áÓd()®Ö&mªÃj…U×/Ý·R¼Ø2G¬œJžJ~,¾Ó÷j^‹3‚ÀŠltìPez|©ADß±@™�ã‚,…ˆT ,'7•ljF]e¥XpXFRqÄaî¹ÛxpÙbnÇíMñ PØÑ Pj%ž ±=¦¨r�R“\Qò˜V:,ÖˆxÅ%Ü9¬�=!0G>ü/¹èÚkUá8ȇ#ùM¡pE.½zqÚ4Sû¹4u"ë’¤Æì"K/=„uÇY7#æ´Tjš PùFËÓªù+TÒü•î\3aÀèôVâeþ1w’·Í_9p£�毚¿jó+¯_å6r™Çi0>F·]àPü=c%̯ôœmkhpRiHö°AѸO~¬u’ž�8Í8†�ó܉&Yc{=¦“'|ù áMºÂ‡Á�Š�æj¼ •„± ¦z3É/p`zA‘4ÁºÄ¬Š£Ã(�:¬W$hA>£š0Å­l‘•œË®|¹-ÔÈ%QÁ–AiÆ(d"ÅŠlc¹•bTZ2m¹HDpS‰•`´üð…ûÞà™DŸ­%*FÁÀ©�hA‰”D&€º¦^]ºú+e%·�BipÔ3ÞL+E:Êà›N6‹µG% =©DeR+–†b‡Ë{ºæªÁð'Þ�“ÉJjy×j×.äÑ{>8L¦ƒCixJœ§]�m¼×ÕžU¶@jöJZò�ÁÔ�½ÉAêøv\äE£ BAš�¢œ¥j”S} «¶‚FÛi.ÂáÔ¼·aV ã3`�ž¯§€}»BÊEK-©ä¦PôÊ!5(°{†XÁÛFý‚È®³Þ’¨Qƒzq‡Í¤ÀÊÆ­QZ—qIk°DTÑ{'¨lk r$uy��³…`ù*+ᔵ¯ˆŽp‘`—yÆÐ,< èMû¨|?ü‹SüVCŠ„6èi¦¨\û¥ù«æ¯4zíÎQ¬IhMÍ_é+-Í_5_ªç�êE _ÚæW^±ôl¥˜3¿*¡Û¾i`EkÀ).3J/•N¡�ÐoK<¸¢uÖ„oØÅ5`lôÊ|è�Ï_±î†µ<Ð' göár#ãÙŠdP®~ɤioðäùê ¸‰{�¨Ø¸[ ¤ûªhXK'JhÖÞŒd$Ànè“íx˜—Iï‡ “œ‰ž5 ‰¥lãm’Ö°Hœ0ñGXnÂyM�=‘=؇ÿÀÐÎçh‘€#j§y*µF÷É”ÙÒhÞÛÛXŒ¤ š!Å24Ç`rÀ]à`‰’•@Ë"dÌh!1XÂ9Œª ßeÉ‘ÒÁæNl&b‹I›ð� r”)2ׯàö"@aýmD¡l+ŸATy Î<µå8u5ŸÛ iÂ1ç‚x ’cAŠäWTSìO@a SÔÕ¢¦‰ô¢·‹ G~HÁÃG‡Bò‰ BJSWSé^eš�tÕb•zÿ­'¡,ƒ!˜/e¤‹Ðlú’ÉØ„'X)VÕÅ¥+Ó½"ˆ[Î1lÉ:Ô\´´ g 1U„÷D3Ñ^äó(ýäãÔBP Ú�Jîb–R¹/®‚¯’�®ÔS…ÔöɯJ§èqºf¥PTÉ`àDôA¥žm–"ƒ³…Il«³š!ÄMgÁµVl)ˆ"�¶!±„ÅNï)Ù)h:•©Ê�ˆd"eíh†VÒ¡*2½c‚Ý‘Î͸ª·Ä/»D†±J™ žvc�2û7W‰V؉<™Sj1¹N@+o:5ö¤‘5ÕüfP£¿ù+^óWÍ_µù°6¿š8¿âÂÚ¶¦�¦�Ä Î8�‡; ößÙÄÖâÁ¦º¦�¦�]T] L`ixéˆO[écrƒUâ<Ì(sgæ¯|–Y’Ye^‹i"”iB#á—8 D%‹FÊx©Ê'x6×››[ò‚K×ð¸0ÅÈØàXiH�gÖ»7JöYü Êp]RæU$Q›Þ4Då˜ÎpyRe-À)�|1¥Ø»Ä�Ô1äžJ9ˆTA‰¸I™ð~“”!‘×JBŽ˜_ªÎÚTd øâŸ+Ì&¨$ÿÙl+ik•À{.ª‚*œPÅ­‘°ßµJ}TdÀ­š¤¡ÙA¤"SÄu z_ºÑ£xņ<‚—@…¸›§2KA‹0�ö�má‰í3ô…Á¥_€Q A"�@ñÜΤ-MÒØfQ¬r n[©W’nìm$õ%5ˆÙ¯(”=›‡ 䄞œ?‘AT’1� €:cDs®dƼRl-ÿl$gµÄŒ(¢ѱò&d*è6*|/%*ãW1`Ã9�¬‹ÌàY´v)›ør×¥üD�XqŒyÑ�«Û¤�Äã<+GIR‚xûˆZE T\°ÐÝ@ñi·êá9Q‡ÊaQ+¿ñOÒE� fɨc»2 �æ¯ìz©ù+‡ºæÓüUóWÍ_µùU›_m×üŠkkÛššŒjŠ]ßÛ6¿jzkhØu5�¨Ö0ÿAÉ_y/'k®L“)È ´i$o‚‚£J.Tæ�È:7V>Àœpï"2‚b#÷……<|ÊÄ!¢IÃDñ¹d Ô9å= <†'Ù�M­ëIäØ4€´§ÍüܤåJVšv0±GLüñ¶…x¼g0—äpJKX45aKà 8,ŒFûLߊ¥«DƒIÉIš,2¤qÒœ‹Ò¼ØAzuç–F>>_¾¨RÂ Š†òL$�#زʜˆú,Cß >D¯Ø¦±J6 gÛÈ <0ê…ó$¬|:>¤‚*¤Äª°v‡ÝÕé+ÜZ,ÚÅdˆ\Ú6Â!”ßô0Æ™'‹”JÐ&'1i/&S:&‘…()FÂR¢Z¦}&0s½i:¸ù��Q9e§„òzFið0­¡º±JÐeŽlÜÊܺ|Ï4ZJÕ%íÌ$É…;Ø�/€8#©#jÙs.±ØO0FxX;œÈ àhf¡$¤ÎXƆÈÃÞ<«9EÆ¿ CE!5cO‹I% E^T€r\)Cø KêPvÑ6¶YE$ BšR$€Šfĸaä$%¥¶‹‹0äTbhÿ$ z’WÑ!¢^è…ˆ&ØÉž[¥€�§ëî1^» øT¥,5£†Ô§ˆ´WÅ�‚"-…Tñ/@6ábûÕšƒLPíHq¨>‹¥Î{—B¨ØÂ4¥jU«jjþ*ö‚¥¡”æ¯q“毚¿jó«L,²¿Í¯ÆÍ¯êÂÜöM+\™X:£r’é<”mæW+\kMü¦�]XçÙf ŒüÕÀ<‡Á¸�7w©”ϩЗ™4”�p#„Gà ÍYÌC‚N¿äÞt y ï@$@a…Ó 3¯`Ð<ƒ0Ù¤Dý™xòå§’´ØidÅ¿wOÁŠH€“ÃPg™ ™Ðox!„ž<–½¨q.o­­Ã ˆ­Â²�½âªujƒœr$ÉL˜ u"ÿ�ƒ8© ‰šYà,´Í PP¢Uä/¾8Ö…S&ÒÔË@²fVIŸü (Ôaþ0ß‹àà�ð"ØLœD†²Öﳌìdr/r*  ;5`~2‰�ºiÞìKT E&YƒJ4G“ÌÂ$ónVƒì�·ãeÎÔ�l¹ô6]‹d:±³ˆI9%TÌpìkäÙr UET<¥qt€F±'øVêÍS0ZÀ¼m¡cµ 1 ù§VA- é#¨U§Èš’*ui>°Mcˆb©ŠÕÊFS* ·çB'W%1ΰsºKI`±òBâéÏ“SW”Ï™{:)1ig3Ñ‹œ$·Á#~»6ª¢Æ ½Üø¨‘qsehúTýÑš^êû˜8Qxß7|«X'sYÏ•.Ç#kKzÒ„Y@2gY8'i[G3¡£0Üõ͘«S½¡#…RM¢äôËVøº A© êRœ l½‡ÑYº\4ü))ÿX°C Ñxg ðÁÎ)HdÏWc¨K4½ E½œøø ©n©TãÍ_5…é9R4Ç Ö¡™5•ÒüUóW^ô$ºq/³ñêm~¥6¼¶0LØVîü ´­i`Åk`ÇâÁ¯¶¦€¦�]WµœèÁÊ_±@Íi™z#ecsækLÓ8õ&4óq›13½àl%�¶Á1UY┘¼eë©1¬æÁhtf  ÆšiŠÂ¯‹¡ùìüK ’±×‰ˆÎ…b”|%Ì·žrW<Ém…ëD\µJk°¦ÃíAZA�†96emf]rðd8æç|B›X¹í¶R&E†  �E†‰A#BÕc^D4áÒ*6@]7ThÍVXéǪh•³Ñ>xJ~Ä,œé$.S‹E"¢mr¦V’ÿ„/*ØØÉ+@rR¥VPꆘ¦ªz¾Ð6¸@ïüôkñ‘œÇò)CˆAe¿µ6²L†z«\'–†pš5cœbRðë«<#’¶¤u€Gäbÿ”pDMòi¦^Àa�êö°ë ³ny3¦¥öHDV+]‹4S)H\á8 ˜÷gFašÎôV!ÌœY%Áë3 Ñ­š&»£t�à-¦ÐAri'å‡N‘K`3ËI;s`Éê:ÎÃ_r’ “…Ô¡„üy&vWãXQGá,]nÓ°’q`9-£3,Ì¡ [¶�º_¥LU{eATxTc;Y Øvaœ —w}ОT]®+ý´?iCaÞåw £˜ŠÄÞÿ<²}“*ªø�S«5Þ‹y_[@Xq �@¥||y�,ÔÜñ_D’h³¾hKÞÞ³ºù+´Ðüv‚Á5å iþªù+Ý'þ±Í¯¸Jð׿WcæW5ñ:Ú¶¦�•«&ûL¡ð•Lâ·=\¹új’7 ìú –tbàôàAÈ_ñþFc[CõQô›”§†ÅpFàmjÃäG“`nOîÀ\@â�ô8DANÌ�žŒ�Qq‚u)�Žpmb&@^gǹ|òDå¶M|­T�´°ÂÄW˜2ÆêR]ræ<œDÀUr�†i XcCànsd † õ­6EbOazjTI…û<]d¥?v⦌,£2(-¬Eã8X®rgÚëäW9Hñ˜C?_À&SN3Zæy²Ž¨Û êÚ©ir;3\À†~T!”De'שp&Æ*3jg( �iÇ1õô_¥IÑ”Ü%ìº6¬Ð¥•:°-õ(ÞÇ�Å µQ�°c_ ÈÊi±ª®<³Iê‘£’‚Õü«N¨ÈSÐBŸB–-ª,…Šý-€kdÃÔ¢4µ/Éü©J3ÒF%ì§@r£ªbFjÓ‚ÖÅPF%°<šC4¹¨Z=m¬òD¼>ýo%]kÚR£ Í4î}ù/ a&ì8"”O6ì"öAÛÑÆñRTâu0 YLGRp%¡]-åeEThMrG¿Š‚Óæ¯š¿Â¤°l¨ù+FOóWÍ_ွPfnÔæWj‹X›_åÚêeµmM+^5ÕÝîxpÅë­) i`×Õ€¼Ód¿2G2¤$$%2%¿¿óW,G2löBL›´Dø!˜5o‘¼‰ïý®„M¾SŸ\Jå€Z—þt ;c£a;”dÞÃdÐOpÉ�± ¢«¬Pê6s1f]Lå¶ÒÄê s)˜áCJ.äaGjƒ$fz«¬Ù&‰ÉŠ˜­òFYåz�î¡*“¾Ø¼÷ÒäNú‰ŽDäž½I@ñ—†“9£0i“0Ð%ëÀƒ ýϪ‘¬ tܾS'¢ŠÂDV2Z!ag aŽxäÏò=Šíh$ge›Já �Оƒ¶“BôÐ’cq«3õÇmÆ¡µÀÝÆª³—;3�"àÏ%mt?¥ [„rä1´ åV>nr˜'‹)PÝ·æ–hïö”Ír‹©¬* êN@M®K ƒ1(Ò´¦˜s-®Õ\¤Ihkä]}N¢C Àð!œÜª>¢X:ÄjÎEE¹i+ÝÁ +æ¼ÍZ~êém¨A@›$ñ ÿ6‘rt<Ç`ÆŽ-±;k°�\ÑÐ|¹Q˜%Quª9�žÓ<í#Ù ¢æ@+âÈî²{øŠaX×á‡äÅPå ({;@ÕTAç¼ÃÔa¦ó=äé~€6¥FÕÍ_a¥Í_5Õü®Øë.ØîÁkîôô©é>¥m~Å*õ\½þx ÄVÖü*×ã¶kXñˆßÄnW<¸âµÖÐ4° k€èžè™ÉÁ§s€„˜ÈƒxòWò•°¨Ì³Œ8_3ú Y{f’Ä�W`',¬ ³Ö´‡±‡¦\eaÈ öƒ©³M„³Mæ†É�¡.Dma3Z‘o+>ûÊÉ$p�µ©g~-̹’.­\6BNÈD°�‡/J¢?²™·4<÷?Ù�0æaP{W•àqñ ýâÕ6òFµ�æÂ`aÏL¬É3¼ó84KY9#¡Ì|…Žœ…½òÕ牤�L>‘QpSK²¤Oò HNÌz ‘ Ú²R ȹyÄ]DµƒÂ¯tÅsè¹ûÓd6à*²�$’k!eQ>’ÎR£bfË^(;!Ù“0¡¡Yª¨R2žGGfWlkŠ%ŒGYR4ÏBZ…‡£ £MÑÊ"¾$*k¦¿(aBì99…M3c2”±Có´ |ò's@©Gû·ô@ÓT�gW«£5,ÐùXY¿mƒZÃâí�BmÃ4¸‡*–zryê�Æ ÒtY7çiÑUgITÂQ§P34›C¹Ö­p¢t`:àM—æ©�ŒMŒä£u¯� ˆS�yÏÈ!Œz é ‰IY޹˜ÎB퀰¥¿Š¿jškhØu5@4ë¸7Ìtf�hõ�Ê_1aƒ°)#—Ñ0[IlüLèëÅÙ” ‡fGŠCÙ³‚v¬A¡ Y†ôç`CfÄdœ[›„²é2аÀK� 9 s <‘·}‰ÐJTbØm2Il¡›dSg„`"%m¤îrc`QåËä�©ã/?¾ƒUZIÇÀª¡¾Â›a˜–ÑÂoFHfä JÓœ«4‘�JTkngJÒÄ–päÿøFK(ý„ãäÊÔƒy´¸z@øÏ3æÌ  Cè#¦òs±! 5ú‰œ#Çj4>nß�»íy³ÞJE4ÀE™åш]u} _êDJøíãU¯úxåÕÑÇ+ïE}~EOÈ &7(ïïÜP>�¤ÐGœ‰ñÅhÊ‘”³ÃSu~¥áýèèàŸÈ[Ž»ãººn¨`9ÎIį|»£�¬Ÿ„» À‰ÝœxdNRA±Q7h€(ÓÀ†Äï…^$ă3xÔª,YÂ Ó¤Ï Æ%Z $‰³yko õƒN¥&8ßÜ„ñV¶Ü’ZwÄ„)Ï<ÝæûcÕ[N2T¶í¥N½)äÉFcÎË‘òF_©…od.”§™¡» :m«œÈ‹Ô³?v4k«EîªÎ ¢ÃЉ!.¼Í¶†ŒÂ:_¹†.zÛé륡ÕÑmPRó`Áå\²ÒžŠËâ'Ëm€²'&ê³´AˆI«bé!)-õÇëoS„TÇuoþøQ¾pÛª’DÎU.Cáq[Ū©¡×gt º ü!p#rÚC’°µãˆÈQ¯>ûx•ÞgãÅ#}¼êãU¯úxu¬Ï¯rÊý—»FŸ_­7¿Ê}½tt0‰Úr<ؽÖ=Ð=pÃõ@ ÄNãQD–{ˆ_-#uˆƒ+1`ÆèÍÆÛ¦+&Ž–Ë" ®¯-°>{ëØ÷Ç"8l0,×"BæDCIU_×[8Ù±z á¼18°˜uÁÅœ-AFœž8Û7ÉP–RùË¢³a œ<­álKŒÍ?ñ�„¥ e¥ÅÉ$}ÞÁ Ž´`ƒJ¬? —øC»¥)2–AІ4’}�~¤+Â÷E 9UÝR© Ҭ̃}ÖŠØ£ihYnÊs³\ "Qk°¬X #ÿzÚ ^Cb0‚ :³4T·¹•°P/]5Å’6(ŒžQQ£`D /ƒJyt>ÊŠ ÈÍH‹¸ÕvÁkBê,±PŸ¸Ê«vör•«õ D¥ò¬“Ph«Ð˜Ô¦QÑX8ÍE‹:B‘Xý�¤7ÑÚ½¢–È©·ˆERÐŽÎ?zŸjeQÇM›y- `Jq®N‚áB¸®úòYH£ˆõËM²+Äϼ¾ æTŸNs¦ÍD¸©–í† ¨³ÊC¤¡–ÅáˆO!²ô,öXH©K^í+Ö®ÿ½IG%5öª ^qp�Ÿž°9ã\°(úCà/{xŠ5Ÿ¾ŽWk-¦åè¤cm&˜ñãy ‰qôëE&�Bf\sª¦Ñ!‹¨bššÛ-„E²ùA¹—ûãÐMÂQÈ›dá«â²Šp@—òÔsu‘Mg‡*FäÓlš«£hm]®†j¯J&`ªh*…¹—¤i%›…Xµ±b¯t~Ó/}¼²¿à û}þã�W5ôñª�W}~å­ÄÛ1¿ÞjN¹ù•#à){üÏÿüÏ'|Â'ŒÍûïÿþïøÃ•s£Ýè“>铯¥ë¥�=úÿñUÊc>nr“›¬Gy äc,ýüŒ3Î8lÙ¼ ÌS·n^Àú”W]uÕ‹^ô¢ùòÇ>ö±ç�wÞ|~ÏéèØ1`æÞ¾çøÕÒ°cÏh8X 6�Š$–eÂa´ë¢$g ,1BGBh°c>@Á„-‚½ð�ùì•ÚHܽ€>Š=d,FƒŒ°& “Zâ`D�К×Ô’8a Éç-®€ß@˜�€4Ô¨W�9˜HaGY/‚…<Ê“ŸX_e}nϦS[Q’²�ò(@°—<´5Eù'ð�A?Pf9S,’µQ,XIïãÖ°$ R«¬á­Î}²-D¬HÖ€?øT9}›ŠÒ¡„�‰H&¼€ŸÄ%ôª… |ƒ‰ªL_� �v”l¹ AÐ$}ÆÒã Cuq�žÕ1.JÔÕBiôÃ!U‚¢ù 2¨9š 2ú2*h¢|cü+ôi4tƒ(çàˆÊ÷u¸JCŠ&T “X¥ó•á”`\rä'.°#á„ltQÍ?ùƒà©¶I´Èú½R/æÆ¤�%(°_³põrà¯3tŸí‚�2áÔÊ)Ðf†eÈ…ÖúþÒ÷q¯ÜW@.yÕFx§&Ùzb<3X)•!ReËÕÁ—€£ òl�øŠàdj'‘( ðÁa©(/¬3MwœaXeeÔP¯êãE¡Uí¶¹¿¹Jà%ÛüÆhØKN¿õ[ÕvÈp°–„¡-/bÀw*mYÔÏÕŹîexH évÒ 5ú±`¸-2B¨�…Ã5�]^ÈP/[cúxEcôñŠ®b—²CÔ_¯r]rµy¡q!™ éEÊUÖÇ«>^õùÕé=¿r<Øõã÷~ï÷þýßÿ}M1ø”Où”ÿ—cáž#G޼â¯øÈG>²´´ôЇ>ôw¸CIÕ«^uÝu×Uúì³Ï~ä#¹¦V“Ì÷¼ç=W\qEe¢äSŸúÔ §¼ë-oyËò2/Ws²sÖYgÝêV·š'›Ïyï{ßû÷ÿ÷•¨wÎ9çÌÓìM&¼ò•¯¼æšk‡ò�zÔ£v°EöÆ„íKÉ�‘†Ëí‘ Ä¦âÁí‹Õ¤õÿê¯þj”1$ÿáþa>³çttì”|y&‘6ScÐ=ǯ–\ACÔ ØÀ3ÓˆÕ�ŒeuŠÏð9puP�‚rÊI,ڳɽ!añŠ``�ø� ŠÑµ§Îùå…·ô²`âo<ÍŸ1: °k½“õ ÓXµT)ç­ K�@Sƒ,Õ"–pØ>@{õå$@‚Åhµºèò(íAùËñ\õd䢊a\� ~µPÖ�+±e …ATÚ m"4x òR!ŸgÆÿ� {“q)´À#¬€u$(UÐJ@‹ù³)ä@¥Áj—/¡¤ÀO å{JÔ•Z�(×LÁBwP1Vú~Š\$9¼‘SU_kØj¼š­‹ª¦äЉ^Ñé½}¼êãU¯¬¸zMf`í󫺵y‹Ìý$·¶Ó{~åýÃÛG¾wëƒu=ÿû¿ÿ»1w¦«w¾ó�Ï=÷\�¸�)�[Š9/ùË ã¸íàÁƒµü�ùV«Nz“†ûªùѱf-¸�бª®¯¾úêK.¹ä3>ã3FõÖHJ¾þõ¯o_ð_p×»Þµ�îqâ]ïzWánÈýЇ>tå•W>àØcN–8Ú”%Ò·à­7¾ñ�«û·ké=H<æ1�aq(‚.¿üò†Pï�Ümˆ4ÊSžBEðú¿øÅ êÛ`Ò«t4ð$ç„ '¿Zvpéd‚–ИP—ÉZp ñˆšÅ6#Cn •ÖŠDñ~sŠâ¼‚_F‰ÊCO4=MÃK-7¾3ô1üñë8«Š£A}ÉU·ŽA\Œäj%­a$ªNh≊ E°®TB`œ ûÈ5à{ñ‰0ÞGa«’–‰»²FéÉaU˜H³=�EY½`…ùø2ˆ®á1 &“,àŒ|ë�QAH ½@X‰6¨‹Ä*ÀFÀA�ꓚ,…§ c’ÆÚh·Z´„ž®’�S]EJÌðÈ/Y2„ýá™Í €£P +�dgÛ+FGYÙ¦€XVpVbf¸Å«iEê­ ¦)@ˆ‚œ2+©VP5žuŽ®YôœÕc> P /¿¨X4•gódx(¤|¬‚zUÍä¥Î.c䌭¦ÖG„íŒ,gß�E¢Ð`X59õ8ÀÆCj“Àÿ–"(`Q/+QG»eÁ;ý¶å•Bfù¹Aír†4ƒ—F`Uñ'ŨYš�âœÃÂöóO¹š…š!‚sV IÖ‰ÉIjeéfyø¥¹¨®¡¶´õäÖRD+OáƒÀæXЧ{¤·^ºbI³{º¡®Б¸N+H±VŽŽ­±ØEu;(ð!¥’5ZßÙZÖÅ‘ôùUšË~Ç©TþE›>^Åáx¤�WöV{1ýÆžÛÇ«Œ&}¼êãUŸ_1$x{övÄWnLŒ}~uýüŠ[Èù/ðqäo]q|õW5ëÑŽC½a1+ì wkTÿüÏÿ\ÐA{ËÜü’®�û¸�kµìEkäŸþù¯}ík[áïþîï>èAj§k&ÞñŽwŒó¿ôK¿t|ºÇiනĿýÛ¿ŸžÚiç ˆó˜Olr~µ#>¹_ŽÆê¢‹.úÇüÇvºÛ‰¿ù›¿)ÿõ_ÿµÛ²N�?¸vÃ%�åô£{à=�NxFv©½Ä¯˜¤»âƸ› Î ˜(×hÙp^Dˆ)a7£SuyH@Œ£) 86|®á+ä ”Nø•Ê€Û*€|Šçˆ"e’ ØáF©jbc ¾o•G—YÓB‹Á‚Š®OòPŽ á�Ás9·Å³±2ñ g†UĈL­…‡)hŒß¤ EGÃmóYþCV§¸JÝÌ颕‹™ §éâ=åpðÅ/²x¦�Q¡‚"+个‡ETrdþ!Ÿ'µÉ0Ô¹–÷ÉÆù«¼›£dj‹¾²å$CGõ�×Ñ0˜O"ô*@ž X5±ròç>S²rƶV\‹XOQÁÊ( Ú­$ó¦eÍ®¢ ˆ8BmbÚ ™B ±_i›ÌhZ*‰ä(aèè£sðܨiŽ.‡«l_�Â~$3ôµb&˜-ºwª¨æÂ•RÝöPpšGoAâ‹E kx×f唊A"púEäåY¨t@öoÚE]›C3å¦N&"‹S\ë —åj@”׋ëÓxÁS»‹õ±ÃO‰Š—lËrižF¨hÞÚ4(Ý�m#”¦øªXÑõ‚¥�jñ ‰MÀ;&H²¦N‚ˆæ“BÏ]ŒI Ú0y)ZU/‹Y˜H‰ª!‹?®öV b(8¥(é¨õ_xŒÔ­îžÓ¤r€†ó>^Åë4˜=�ÖìãÝ©�W^b\=}¼êãUŸ_q-ôù•÷é çWÞ?>ÆŽ_ù•_ù×ý×Qê“?ù“'ÕÇ«Š&E;uÊ|è>÷¹Ï˜;IWVšŒsGi¶¶GÈ‘}³›ÝìÖ·¾õ¨|¯“·¸Å-Æ"?ó3?s|zj§3±vR�™Ì33…ÝÄüêÔvJ·®{à”öÀÉů–gP!�}2dgð1¾&Ðw"O0 Úc0^+§Rl$NL\�¨˜(Ùoâ|Âj( çëÛÍcœò#¿²ò…§ÞÍS a‘’[0Î&a!и–2àf‘'G­¢h©'Â@‘!9äË7•QK„ÈÏ�ÖD±Ãšœ¬¸ž 9¡Ñ4¡©!âefh¤—lª-š²7ˆ†‰\° ©–`r)Fq»_ÖK)Sí¢’«ñœ ¡˜FP�Ì–¢P´ ük&¢µŠñº|‚bµ ̲ÔCàÄ/šGí;¶œÀyv²…0 †ãò-v Š; ^IÄs(‡{á&™ò%ä¯åšîŠ& ³ÞJ`FÚj‹•Q{]ªÆàpE¹°Í—VZ€5|é AJDÑwðéxÕBxŠhI-Å|褑úÎv�¹HuC0ÔQøùž‚°Î§{q`‹sÊᶨüäš®LËq"ÿtWeØx|ÃÂÊBS’ì;ººâ]N(0¥ÒÔvGäÉáÌ�Å—@¯iiøB…h¥Kï™kô<8�P/§ðòâD·QÓèOEç¼>Õ*%¥ª©¦róTE±8ÛVãi$@kþXŸWÈ(¨Kõ¨Í�P8ç…É#û� QÎl3qí´[Ä‘§gªí¬ÀŸä¡2©qUšO�N-Ù†šL=Ä"úxUÞÁ7}¼rd°óø×Ç«òá2éãU¯Ú0˸Ì5ÂxëåÂ@ßçWÞé¸ ænê½'S›ÜbN�ù•ÃæÞÿñ饗–LžÅÊDõîw¿»iÁ¿Ÿßüæ7?ìak9[M ‚甽ìe/«Š^x!�’;îŽ×­J™§gÜçþçcN±òîÏþìÏ6ØÄ7¶š*,~úÏÿüÏy¶{–óe_öeôGôOÿôOH¾¤ h =“~r9.æºßR)øû¥mŒãzòx· ØYEÀãÑŽ˜¤eZ¦Áö/bp¬eÚV¨O…ùÌöuã—«gDq¤‚8áPú£ƒôÁr`’ÂTDó@p•¸ÁÊÂ4ÌAœ©'HB…ô! jE~ÆM Cw·q&Â*Derùö?¢‘‘Ö¦hlÁÇ̉%e=’ †ÚD ¸Á"35è®ÄŸû.ÂA¤Ã!šá®„=‚8‘¤&)ã\ö¤#ÃX·Åu(‡¯x쀰”Z*Ñð:yò†ØT& ÎÃC®þè.ÿ¬a~Ñ—–òÐ$õCçCÏèlÕ¬ÔÒÁ ‹yÌ¿ÉêªM!)³×¸yÒ:6‡È–4ü QIœã_ÜBó¥Ýp:Æ»Ø t4y8œœ5ÐÞ"Úzu…e`8&Of‹]J"�­y.'¡ZRk•Àu˜m“» 3Ð�eÈ¡ájo££O 3ÓFDQ<cì»,Úãŧ ½ªUÞ‹0ÜŽ=, ƒ%’¢ªŽ´WÉœ óð…Þg1#Ôú™_•ço&ÄäÔ|*ù U•SW*“� rKžrÆ.®?á<>h0\¡Â^ü!ÒÇ´É®´¡qöÀè$gräE-¹… …ÑY*U\æIÀ+T :¯;ÍKTú‘ó�Ë8“7t@V}—ê4Mš©ÉŒ‡ô¨d£a‹zç/.‰É-yø°úºxUEÏTCµ>^õñÊDßêãU¯úxÕçWÃm®îo}~Åèè]~�ùwSo¨™Tz·?ÁÅ&k»î~÷»ßínwû™Ÿù™&š­Ž'¢€¯†ä¹iý×}æ™gò,-žd¿&Ã53›ë%Ö«ÅFWvŒ6è�ê<=íö·¿ýš|x‘èûßÿþVÄž\^°°—; ›è–`7î3žñ ¶�²X�6ÂÌöþ‡FsÊ&¶~Œxƒ–ú—ù:=�ÏêÅÍlñÞ勯½öZzÎmo{Ûñ¾ì�™ƒJó‰¿û»¿CóOÿôO瑈›|×ðÆl{i÷À–<@4I„O(jdËAº‡øÕ’OÇ8 †ñúo„¾xìhÈE:  —7oÁ3V8Í2ceT6’¦Ø hâBû�.;£¢ôÂÉQˆPÓy@ÅÝA½´×ªáedÍìY” �)Ê니 ~`Я*ÃÊœø.s Ñ8ËûퟬŒôMZd2º( ªZw¥uËT“ˆx¿èePðI¯Q ;ÍȃÛHá8Ïe gŒck¬/1 ÃhWÖ6YaÑ+Ö©®Y�ø)Ô _ÔÀ‹‚ ̘Ä$ðÉ1qh9Åh½…Ú�$J u¯¬äTç|%ËvÕ*[ÝlÕTÑZKešÄZ"Õ�BPz„á ½Ø�\ø ÎŒ«ã„xœþ0‡02§Àcf?p§»ù¢ Šed�|mO¹jù°Ð.˜ˆøÒîš”§ág*à(3ùˆ£á`­Ì3U]¾ÖH?�bцP¨­¦Ò”pÂÅðx«Òl2Yç%+%‘ehå——‚Ü–<™]¶¢tÁE݉) Jq´\éõ¥¯)¨º ÒÑа³]Ø?Ç^[Ï÷/ÔÃá`ïF[¾T•?ÛC#]‹¢•¢e@äº6“ò\ÁC—Ñ SXx�Û0£ÔÜ\�™aÄâžöHïQ-]ƒ’^áz�28'~ªŽÄåbÇOuì€ UÒïà-Ψ¾zÑ>uOáÃUd�M®ú[Ì»¤©Zv¦>^é©>^õñª�W}¼r,pÔïó+ï^™…p{ÉíÛ»Fî~öùÕüüʾ³çO^+™tYVŠÆ“}ê§~j{ *8‰(ÅÔ‚WÁ�>¬wçÚ*ÃøÃ7ºÑ�2+ÛlU…ÛÜæ6m†Ô–Ò5_ñ¾÷½Ï‰ßìøâ/þâ5—˜a $[RzXž¬÷z�µE²;=X�Ȧ׃R€Üã¾aý· ñœHÝ©Þ;qî䕉{&À›�wBòöyp™¼ímo㽺ò'2áÂ2OzÔžð„ù>ð¬g=ëMozÓ„žÓ§?ýéì 4íÇ~ìÇæ)É“ýéŸþi°ãö^2ÙÖ̓þð‡¯Y¥2©òÜç>—Õ šOû´O.Ä#1ÙæŒ ­ޝâ¯úª¯špøŠ¯ø ^1Éì§Ýz ‚{.}'{Œ_-‰ ˆ�° tõæ¤eyßRVµØT¤ïR©Ü ÿ�ëùaйN 9f~FÐæXÀ¢q æ„™ú�y,傿Eœ<ÂnyA&J Œ‚(Ç~™�!%¢þb¼‰„$‘z Z(‹/™ûmgZêâþÚ¨P%Rx†OÒ’¡9ÑÌb2•,™­Tì’O²Äy+uiÎÉ$vešàC©ÈƒãÅNb˜Ü�,ËÙ 1�|+‡þ}m=‰ü?Á›|h;ÕP‚�’£–¤CûÐtÉà {!¥¡d6ƒ†�*¨©…‡j(2]Âìá‡Âp. Õ k$°€ îéÒ©>\h7>�‹Äx–\Ë¥¾â@iv+I @áëûß'üv/³‚­l*ð°¿añ%Õ‘¾~É@ú°µÈ* ©Ÿ¨ì£Ë(Ð AŒø’Z}м@Çt!› ý˺²6ÂDßF3Je¢‰üXî:Ei ÑfÎà—Ø×¤@¬ÐäWÕí ÖFS:—äRVïUŸ�u¤b¶e�¥’Ö OV _ªkcÙñ¾ú©‹@;YœÎØAY|톶 õÓŽXBe¹Â‰‘„Ôâ…LûÌ5HlÕ Ý­»¨WF�"׶ʲBÙ¦ÿ�=¨é%A¨cZN�¢ƒé;}¼¢aôf¯è}¼òBL�Ss@¿@IDATð’ìãÕ0õñÊÿýävá°LªÏ¯¼³è‘>¿Ê½=·Ö=þ˜ìýäyglodÙKC¬Æ€Î _øÂÞßÿþ÷¿Ýín7Öö�ï|çüÁT°QÛ¦ÊëØ;éäçØ1à°Í¿©t€•_ÿ÷ÿ[`ÁOüÄO Ý Í"  .¸à×~í× /Ž˜<®Š&»M©5ÞmŠò‡fÕ^=W8ë¾ð ¿ðV·ºUãL‚uC¿ú«¿J�á,Eù+®¸¢<ƨ‹/¾¸èÉ£ùË¿üKðB^1qÓ›Þôæ7¿9M½€2ÞtyÙe—=~F™C+  Ë‹ž�ˬР¤²ê¶Ïßú­ßªW7|Þç}À íTæXêÑâTd�àü“øØôŠí¬‡š<~®qÞ�„a�ñfB;GÍãϯvC�Íó|þóŸO[¯I\õêW¿š>öÌg>sf±8®]P㺲“|zט ¥Aî~äG~dþ9†ôÕç=ïy¼´÷iO{Úš«Øxïð ^ð‚Ægœ ;ýú¯ÿú-oyˇ<ä!ã|t˜ U:Q•L8Œ+öt÷Àq=à<1ñ§—>�eÀ¥=ïXSÃdd…gnëª S47µ$;säA©ÂŒõ¼�E2pžÏ¬ÎUU.“óSÎôò/„ÄÖ'Ñ1Nø-my ½SI�\Å$+Ä|ø@³Uñmåß“Ñ-L•)÷Z M ñNÿ[Ý„Á°3~ÓOÃZw1ë†JÅÕŠŒl׳yc—޶DÅ)Â&å Ž ¨ec# EH'?®nB¾ ™õôk*ÊQM¸ÅÊ M{Ç`äßDqÔ¤šM®(=]ø[1CwäHæ1tP¡”M<Ð#[/ð—6SÿY!TòÄ;®¦ ˜ôŠÑ=ö”S8ˆgÕÎêðÇɸOÙ’‹”i.,Ëñp�KµE¸¤sÑ¡òxs¹ZjUkàs[_ƒÓƒPÄSàµ3L¡½ú¤‚,í#p°­ô{ñƒˆÞ‚FöF (¤šídý0ð[Ñfʧs;¨ÆÂ>ýÅ©MºÀ~^a�–”X c˜5)êóîžaô$!ãOïÐ^œë~zÆHm };Í‚…à”—R×°)…­¨\,꞊%’ÂR�¢H³ …à"£1ÝÇ«x²�Wv¿ty{žýO¿ôñª�W}¼ò,Ncê^‘«$7ÛÜúüÊ»Ùi?¿ª›ïIÿ$h¿Oó³>볚J¬¯iËÁ&Ð[ÈmsÎjµüyHhFõQßLf�ŠÆÛE)†ÉïüÎï|݆' ÷º×½ÆÐÛïÿþïÏCo°ïå`fªÕ=‘¼h¢½w²¤Jr\sÍ5à€<‘­½›Ï43ÙÜ 2ÝD€výÆoü†³õÙAÅÿ/ê�KÞéNw¢´±-Hä$nFî7 GŽ92ÎÄÞÃ9x�+ËŽÎ:ë¬qikP?Âp·ñ›gaŠ÷çþç¬`WKm/ŠåÙy_þå_>æ¹Ké, Ït|+ñà.)³ ¶àk©À ,]Ä«õú]~Ý×}Ý«^õ*–6žŸó9ŸS�ó«œ~ðƒ•€Ã=tèP«Õ@½,[k§\ªŸýÙŸ jü�| 0²?üÃ?dÝür9Ƹj”¤ÍCi%â.w¹K[õÆ%ÐÒ Ì“•›ä4­zâöÀÿñ3 nÒÀ¿øÅ�~ô£×%6 =iøU /î¶¼pì@à’DôFº†àyÏ$Ñ Á8ç ˜�T€Ä®ˆ¸ à “�È­ìíÏš’'ØÏ~1.Íi—Ä…Z¢¼‚¾CœM 1EIÉÆpœ°|&;�‚¨@q+$§šúpýB%R©Ô+uX&4¨ÌB\ÀìÀ þùâ5c®I<.¤PrS@t[ÁTÈé, tÀùV× òàKp"ÎÑ�¢z‚±·à2˜‰­PþBOUY6ÑS*Dxô��‡* l%¾úQª §~Rl|�ªÅ +zê Y›'Ö£>1©v*#u„ Q�ÒøŠ\NܳÈÒ,µÍy¢=�p½·¢c–®E ô‰ª"[hæ ÄpM ‘«L3ÈH½l™Û|R›$”å€ð.jPÌŸ}3_êøAõzs“ �%É †C‘†€oàP/)på’�%ÚiWF˜Šé}P°ø–ËBP¯GC�¢^ µqø­†QU›¾Õ<ßÍ­§ähÓЉ Ð¥ÔJFŒC‚<Ídw¦z±>-ÈqEaµ'¬hJ†íJ±–ºë%û” èPVŸ’«"á\%ɲý0û¥s{µ®£[Ú›`¦¼¨3üˆŠaNuý/u ³vuâ ß.,c­í”ëb5WUÛé‡ä™eQky(X‡™O'õïãU¯è/^öEÿ“BßèãU¯úxÕçWÜ:úü*·mo˜ëϯ4Oö `4†úЇ6¥†YFÎçA´ñ¹q(ÞÀ)êÍï¹kÌlj—¾ô¥, çl/�Âàm-W><Á¼ÆÌ¿äK¾X�0Ö �]1&#MØ @ÖvÛ�ÍD ǘ¾V9±jœ?Nƒ…½îu¯á;àÕ(ÁVžýìg��³VT ñ—ù—1䋾è‹ZÑØóë!˜ šóä'?¹Ùrå•W6 ¡çŸ~mn™»‘`zÉ�3Ó‰­Äƒ»¡ÊVx²—™•ž~ðƒÇoõÅ¥¯|å+ñN´Ëk^óšÖaȹ(GªÃJ?éIOºÇ=îÑò×LÐKþ羊è]@lw½ë]ëA?õS?õ†7¼�SZ (–…�c&¯ýëë”nƃ'W‹Cy/ÊØŠ"¾üòËVt~ë·~k�ÒÇý³ÑôÄ©í ´—¼ä%@o?ú£?z\K¿ýÛ¿½pºõ ºÄŸÄ½'¿âáK†/,3ãÝ…ÆÛhBwloچȤωª½ RÄÄ’EÈ Œ©ÐšàÚÌ„Î$È„�+™HÌ®cA¨]0•h¾–ö:):á:•; ÉÝ A»| ï]Áå«Tfˆ=wQ7–´H¬nŠºÉU-}ð 4J� �“«�ˆ#¶wÍ�+r`êm÷í;ê.@òÃ\–È*è«ô‘±²Ð”Õ]"UN{ð¡Œd/–`­á]›è/<(q¶ÈÑ *˜ó¡%Ô¢™¶Tü+R£0›Å¤vÇ�‰nåK] iZ1L�i ‘<«jT“›® )ØRQ%TZRÜ`5ÿ"4´”¡˜À�7Ô*á–b=œÙu¨E±áqØÈØ÷æj0´‰—ÉÓ5Rp€® ^¤+þ¤�äKŽ“$Åü"×°H«²¢¡ùu [e4†rÝ øó.[w¦³Ðåøi*¯; )¨8³§¥ý$^ôú`÷#NÐ{BBj©¹ZΟ\”¥7q€ªò%]ÔÐ/ÒÑ@0ŒKg)£ŽÊ¨6Zª ½'('¦ì_Æ7Y¹7«]Ý-Fé$ô�«¬`#[sè¤ä:[á�+QÖvŠ~æPŒîz¯ ¡g‘Äë$Ï�!G’¨œþ�¦éÓÚ�ërš9Gé¹H¯ä‚GC^Ë0ÃæäP£»ý�ÐhåñvZ^L\Cd¼ÚÇ«t»E¯ì)øÁþY—Šk¯úxÕÇ+nO Å\Œ× ¶¹Jœ*ôù•^`Ì8­çWu›ßËO@^`jgûlV´Á�g=ò‘�$ŸD}މ[ú7ó7ǸûpY(ǦBÁ�4FËòZ‰†”�Õ+VÝñŽwµa§p[ègNQ¬hÆ�ô"Uæώ'¶î¸[axÉ%—YÎ;™v¤MéuÕ4Àacèm+¦´¬wkûLyÖÛÁƒÈéSžòöÓ?ɤt½ýéŸþi³Çy|9T&—GãÖÝkz€%l»jÀj£oÇÅÝàO¼{ñ+Þpʴ̹Yà1"u&lÀ7h–=c"O\åÌàØ™ÙèÆ˜ß©ù)³ ì†ØÝµj©a g®Ï[3�1~bÁV—jÞ̆€Q(ufäO}„°ø»^XÊAÈÏŸ{Ce™ðË›šûŠVy _^ )}Ê5P�1P�p�/œ„p€?xŒ{LÅØ=twLp?à EÙ$ìž>=PM#ø~»‹C†ú*ƒm|¢(œ)µ*pB0&Öq‰¶•§A>@é´ÜÆ£±(’ud@ÛO5lþµÔÏ;72TTݨ£O|�µhQ·8Z\Uà;Å©bq®*ÑlÑ2´È¢Åòyv\R]w±À11…͈,¸Ó½ ÆV�_å½ $cˆ¢5]c¨L úPXa51X%êRZ'ë4·Ìâ5áñÀ˜ÂÓlá*1" ¨Uè§rÓ{Hqᥠd9*mbÓ]ðóB£®Z«nKR-ÙØ Zm©ü(bÉeí¬ì¿`U^U¶�–laQøÓ(=”êTÓiœäéuаX¢4‰šË�á4Ä^�Á¶ˆ®ä ißÐEøG²¾TÕÓ%ubYˆ@Û‚Z ‰Wºþ�">ÀÔæb¶'ù§NÚi†+kQ¯t ÛÆõÍÒÇ+¯²>^yr9z÷ñª�W ¡}~åm�ÌqYp‹Ë=%÷ÑLrƒ9MçW {{¬\}õÕkÊ„M»�[“`÷2Û’D0ÇøÎïü΃–86xþÄOü»37)lÌpªmeO c[ Ç)K„7�ˆz®@Éx×íŸøDp·"cÕ›RÙ9X§o|ã¿á¾¡q'x¾>8 ›ÙΉ€†lèk,qúʯüÊ:E.›ABÑ–«9Õ[ç¡;|øp+¼ó�ïüøÇ?¾Á DÂÏyÎsª”›/›v¿ök¿¶�leskæ«û.¼ðÂK/½´•¿6è� Œl­­"Ö9²¥q݇L00DxßtºÙxð…ž`õö�¾5ù°„­ ·¶ïxM²Íg‚�¶‰¼K¡] cßò-ßòÍßüÍä€YÓÇO|«v§ˆ�§ã*=Ý=°%€¸¬¶úÖp·�á9&‡'¿òMŽŒ8ùM$'à±l°ëLŸ8…w²BdÇvº��‘#à‡ÇÄ;ÝOàîøeE1Yl@Ú�rdˆzD"Á³`�¤Ëˆ-z£�µ<‚`…‡Øc#Lùq)jGìï Ê�O*ÊÖŠ*º §TGb°N\ ˆæ1DqÈ,Þ„¯ DêÉ jTâøh¢ÞÄI&Èܨ¢·4#mN¶®U�øQgÀŠ¿üÎt/ŸQ ™Û*|y¡+>+Ѳ­TKWyKÆ" 4ITÐ)6–Ul].I«Z¤³1£ µ‘l\�Ô+9˜‘U­i®ŠÔU_¦‚ò¬§u}¼*Gôñª�W}¼Ê`ÒÇ«üs»Ã'CmŸ_eŒdòáýÄ[GŸ_á‹�¡Tî¯x� 9ÚKµxÀÙøg`@ã`Ë$ÇÝîv·ÍCohžÅÂ76ý5+xmB[4ÙmzßûÞ·žÌ5^ ÷¹Ÿû¹àn@r`gLÖX&vÁ€jH7~VWAâ&7¹É÷|Ï÷´m¹L©>^DÆZB‘‘¼ˆ]Ðs =Ó¶1§�J³Æ¤�S‘LJñ‰&ð-e³ê9çœÓŒbÍx«iÕ½ûÝïþÀ>Ax•O,âùnµì‚S’楖<í°�—<ðÊK–SmiÕaSu« gŸ[�·*cwèq)ø 'Ç/%h¾¥tGÄ‚Ï6>\/,„l§­ÿ´E@Ì ´�æÖ·¾5}ƒSP9Þ�ÂK<è­´'º6ï��Ñ·ÍânæÆ�' ¿r¯ƒŽÿ t:âá^@bC}æ) ~¥bEÓ€fIMT_SâfNëÂã“Øßˆ›FƒŽj*DÐ…2EXVµ;¡/*òym%Zv à9%+ “²¢G…*ºwZIä­žüÚGшa› ¨˜š‰~À,]lÅ"5 ‰¶ ¸•+ä‘Ù™`€ \V€)‚Ã’`!�:<8' -Äl°YЦÄ#vMÀåOcC…Î�¬âŒ:®ªBQsý„OVETÀ°P”E2Oh0ˆe¶¤Yî åÒñ� ¨…›P §ˆxÉÞÕ€•mU-Žù°0&™ˆ—‹Ú*K/B­Ñ²T Á¡²+@´£9h8W$Ì¢³ÒE�lCYøçóÄPX](ã\²·ÝÙã‰X1—EÉTž—š‹"‰¥Á§Ø W8O.V æCç˜Hö5ƒH†Š›ZÂÐR9š‰9\$HÅ$R‹Ó §.°ž²T¤Õ€õ(‡ ÉĤ¦VYETººðÝx‚B´”ùL}SAûä_®“oÖ�ÉÚÁ¹«‘qÞ^t«µAµ beË’E ½AB­‹ÝWQ;Î)ô2ŽTÁþ� í]üyáDœg— 8n&Mwé/ÿ¬ nÏG2ù¦£Š˜\Œ§#§/Z@u¹y ªûr\ÊŠ¥z™ÓÇ+»¹q_¯ÒA¼@ìU}¼ò:ìãU¯˜HôùUŸ_1p¯ö^î¯GîÔ•Ü‹Ïñ CAU˜n4© ljc¡hNËl ôÜXÕõJ7Ο YçŸ>9 îayפúä´©×l¥¼ç=ï9†ÞØýWÒùºöÚk%xÛBk·)�DËc�[m• NÑE‚…�óÛ0yŠ@E±¡*«ÆšD–A�Íá[p=äœ�‘5Kçíªœöl{4aW ¯’ãù\x¬AoÐ`ïcm¶T‚±ÛÄ thðEM:–>ö±�mÕYµ×ŠZæŽ'¸œÍ2iÈ$sÓñàŽ+²5†¸šgþöoÿöÕÚÑ h6S4FŸyrßzïk¬ØÁ=†Þ¸¨¯ºêª*å±n‡⩈¬r…lþ)o�OOtÌ{`=ômó¸áèyxè Ô³X�Ùqè..{¤Ø^YœP*m´™ò+ϰ‘È­i$£•uø°E´=e(CšO‹ “´H訑'ÛA…=Ë)žc©Á—†q Ø"rŸYÓyXgªè¤\SÊΔ'ò¶çê5?$æJ±,¢ìzÖˆ%º'¯Ï…�-É)Æ!•#r¤,úªª *̨ ©½*¥2$=SFC…š¶ì㕾îã•]„ƒ+úxÕÇ«>^1â÷ùUnþsŽ¡Áû 7¾s[ÉŒÁá‚3ïo~žÎó+‡Ï½=Øùƒ?øƒc™U„ñ,éj™lÿd×á<´Ôv6Á¾Ë1C]ß"òA.Æ ÇÆ”¤AÇX+÷îw¿»hx›IÏ<óL–¿�k±´^°€à§V¶ÅÑNçè<ï€<ÖµB×`;Þ¸Ú 7øàmÞ7ÊÁ;UYˆÇ#ÛœÛmxŒ—¤aH\Ú¡Kü`5f°&ô†V¯¶÷*Œë’†? �ñ<¢Ç²&”;xš -£ÄÖâÁT`¬Às/»ì²mTÜ^•ÉÃ�ˤõÀ¢´e³óø ¼Grð’ xá)  ø¸l;A÷@y`}Ûî&°ÃRսǯêU‹LNGƒQ• ƒ�µ¨©ŠóE8•q|&.”h@·Äe�Lvœü‚/|ID0ß©­0*ÊÝ ¾�“"¦w®ä"-êÀGrð#¾UÒ­m%ÔOvÔY3 p.EÐ\0.Õ½NÁœyå¹ÿ|ÍÀ›!–Wy×îh”ërö/s9Ù�(n€œT Ä!t×Tó;ƹ@ª@51ÀTX 8è&i„Å q7ˆ2åãl°Ò<öMæð¤º“BÈ©‹…þ„HÕ£m#uÜJ±-ŠAEh*ªH®� ¦™R £©%ç4*¢2zÎÐ °Ø,cR´†`†Ò¨lJ¯ÃH´$öÚH‚1"CŠM-À!] )¢ åå,=Àr2SÃÆ£2;ôô·­« ÿ�%¦#²¯†TfQ ™Rég y�AlÍ{”£DÕѵҪ¸º}ãAd¼´ÖþA5kcKðVɵŠß™¸\j¢5vR9 °épʨ´Ieú¯–#À�iÌb¢`Pż0Ábæå”…¦žÚEc¬®5´D ú9˜—ªòÓ.µæ*D¼‡íãj Qõ”¼¦5þ… Œƒs¹¬uÜYå' lÛk°W;Ö`rÃ_\+šCI”UAöíڑአÔ`ñš›—‚4ªÃÛ¾÷»»:lÍ×2ùê}PV*‰Ô"¯�W}¼²ëØ¥ûxÕÇ«Œ}¼Ê€ì ËKƒÃ1½Ï¯¸Éq×ðžÒçWvŽº§zƒÍe³GcqtMÁ=èAâ“ݦMv_¶‡µLãºãüJ¯Wºf>™•?~´ÿ“KªüyþKÓÓ=§ z£ˆua˜9^ºÄCÜØ$Hi[z6f²Aš�¢Qÿ£nÍ­£¬¶;|ø0/‹˜p{ík_ËËó˜ÇÔÖ¿‰ÕÅŸ'v�+ÒLó«¨XÆ4€‰VU:É¿·‚V Y>óÜýtÂ'¦xtÓñàîkµ®Ö‡>íiOkÅ@ÕÀ¸<é�ŽáºŠô·½èE�àÄãözæ3Ÿ¹rÚñt–®Ä7}Ó7ñ\vNŠx9ÃÅ_üã?þãíù†‚~Ú=0ï�1úFi½Ï”ÌyÊ5sŒ øe�Ò^ãWKâ(�ˆ«Q@à…€Ù'_ —fÆÑLg…#˜Ä�L`9`‘ûC¡7"7ß¹ž)â|1 †1‚nÍãàÕ›‚ äYEPÎm›Ì‡¬%tœ!)‡/¢sªÄC+5y‚Gô2Sšš¥N+ÆÁ�¸) îb½—2ã*‘}ô’�B#ÊуQ…§Ùí;›t¬@Ã0 +——¥ •èiº\¤FÕõK�ÒmÀúä­‡lµ‰jmÉÃç¯iy¥qiRR]±b€ÓX߇"\©©z�Ï>^ÙëtÎIçèã�…®C/éã•×i¯úxÕçWÞ@¼M9.ôùWÄd~å=dÏ�ñÎ5„³÷¼€g��¡·ñJ«ÝVpü0xP¤y6ø;=yZYÛÍúþ÷¿ë±neÑyç�×7–°�˜4 ‹w °m=Ã!fƒÞƒ˜—9´½µº,s㽬9âá\`p-¿p,;ZïÅÐLžå.9áÀi-Ükù“Ep-’hÆNòOÊ©s[†gÕþ1ëÞÌüꤨZBÙdÚ0P6ÏãÔï{ßûvV=^ÇÑÒ·ozÓ›¶ÓÍ'èÕ xñPòŽw¼£½ù&?ôC?ÄþÙÁ÷›gÚ)Oc4ô ðŸŒÍãnЃœDüÊ�œ¸ÍÿŠ?8k7¢càq4r$"P g,œ‚Æd ,ñ4,×ÖW̬5©f-˜Švxð@|ˆÈáäNÖÓ8¶lXœæÂ3…Z$Ú®P�zð~X?b ÓE2‘gŠ©¨{ë–…™j œÊ ¤ƒÃè=´Ã½¡>±KƒüÉ+ ܾ¨Žh¯}è-KS©«h�2Åu¢„Œ†#@)©ŽÒbâ’.H{k þÖõ¨)�A¾ "f³]µª Ô P Þ©gå-Zz“‚ƒ0OÁ„*åz:ו)Pê`ôPu­A|t%M£ ¥ë•“Öü§0Oú�„ΰD•H* Y 3�O¼†~.•ÂXÉ„¯ødZ�ÒÓTIµ@Q4™ø#“šèpn@t©¯Ü¸F…UH™&¨¦køˆ+‘¼¤0~D�4ÉÑδA¡‡Úg3‹é:È!ò€'tYnG%±(5£6Êù£™èLÊ µ (æK&s@C ¯HT‘ œ/èhþÌÃ0 ­¢=¾ìÕ†µ;™Ç+xì�NŽ�©QŽ„mI‹ñZ_]œ ·éWéqp§:Õ4ø¢$P)ºažH–5ÕízyâºéY:–2ˆÐ4*ÂqT’¹´Ê”ÊO8€I%Jóœ*ÚLYz?νdµ!‹#u‚­[m®Þʴͯa}¼Š�ûxeo´Ù?øìãÎèãU¯êt¹$�ûüÊÂûf&}~UwFÌ“€ €OMѬùö]k& pX{é‰x”�…H¿ôK¿TL€®¸âŠ1ÃûÎ^°@&ø#8]{ª‹Ëx}Á˜x’Þük+YÇ{8Pé=ïy�Ü/+c#!hãÄÿMuF۞܆$6œô–·¼å¸ô‘vÎ$z«ñànÚ¶ñ6g@«ž;�»Q´ñ†åm(>Þ J7˜\5[bÈÙaÊÁËâ¿ë»¾«z>ŸìVfíÞ–¸uâÓÜ…¾á„-ánÐû,+gHÆÓÁ SÉ0B&·È±;øñ,Âr'F�D¹ Æú¤•M©ù*aàëáÅŒšã»»ÑMÌ!“>WÉ1¦bç(KàÑ‘ÎØ9Aûû/Jö°ö²�r¨bpˆÒ–Õy°–D¨Ãr¡ˆ×cf¦@ÁÌ@ ÷u0“Pç_ª2Jææ6Ød– ©³ç-rœ¾òKýR6nàÔ·O;‹“±Zì ¦ú@– òÒžÐé€F¹ò'š¨¢B5nq¥œ !›A/ëì;jË °å‷ "¢µöÉðJyvèY"c-21Â,Ñ…*V2u…jQ‚µA)íürÊR-õôÄ„{ô—yé–T™¤ûS5ÎMkt)f”CƒDÎ^]·^†F—H’ß A&ͤûÁÞÖGYD,£™Î/ÂÆ 4¦!²ÄJ ˆ§“„C–AáY˜ª:ÀS;z€‚mSäƒeÑ×õkÜêùoú-ÂÂ@Qª =‡}¢¸(!]>9ž±›9 ‘4ŸžkŠK,$h=ùA­ i.Ž9¥,­aiŽ`ß ÇTº›-ç_ÜZ¤D`µªòÊô‚¢¶çQ=|Ëv$�0]_¼8‡âB+²³YR~Å,“C/qaî’‘úÙ�´@ù vWq¥ ¹ýè{ÍÐtuÓî˜^I¬ÖH ›…tÁDÖT)ð¼`$ýƒ© •އéÊ2Uålkì[4‰wØ/¸Öh·´¤ï ­µqôh$ 3Ás’ Ï&ˆž2D¡ œ Žé3`!JeK!]«ÕHkÜ®!W-6£[ÁÅ�(*0¡ä°U 4ÙGº5IÉ�Ì'0wÁåÀeR©š!/‹�p™TV›rŠ.ìÃG-ø¸0¼@¦K5źHÖq1B®‘]^@-½E±ªHdË–x^Ìoq?=LéXÇÄSê_óš×°‡tMžëeÕ½Q�—0°M‡óî…11ëÚÆ§ã43<¶qµ ¿«¯¾º�’`Ëêø‘vlE—n# Úø‚¼€wq<ëYϺîºë¶ÁaU¶nCÒñª´5ƒ<†oÚ†óúŽy2¶£Nz×ëÛƒØØ¤L3m@¿¥]Ûcxšqë±{Ö1ÖS¦çŸ`¢ÈáçÉÀ¯–ˆm�üë-„nŒÿÅr·v‹Iñ©DÙNhD BÀ‡÷vJ\ÅYhgOuwò“º ¬Ø‚‚ІÔû�a¬x€1_$ àäK¥„Ö ¿¡W%b{Boà'¢†¤œ Sb¸Ûa© ûAä,�˜¥,-S{ËQ^‚‘ÖS7þ,œ®$íí„C‚Ðthy!m‘*¹'ÕmGa:-âÿ –•î�1 Œ0^Éâ”Z*¯ µ‚m¤+ÅÀ\Ä#ô…€¡ø/ ÿ8V²P c²ÚÇA€,×@Vã�RÉj"òašöµ�ÑL †¢ÔŒBIUF�‘ fõ¹Ä3ò÷/üÐA4ÄÙ.Œ©†…AV¬$jà·ºÔ�ç4˜*°ê*JZ€8º xS؇Ñõ«[ÈI^ò^d9Vš ¬°í�ZÚ¯„³ 6×ɧ>'…^qõy)õñJ/ôñ ôùÕé5¿b4ÜÓDàùÏ~ ´4ÿ̲;Üá„Ùµ�‘'LñöF(sÏ{Þ“ÀþðáÃøÀZþ‰$XMìÅz®b‚ÜŸû¹Ÿ;çœs˜ô‚Gpl›9Æò²…y ¥PŒ hr—»Ü…7½6q¼��Ur<£íÆ7¾1.¼æškJÖÓ=àؤVà†l2Ý` /] d·éx�!Ê€¿Œß¯:áÌ£ñFÛUê¢9ÛWAIX™Åêª!:HµÇ=îq^ûF/zö³Ÿ=¡_ïôf7»Ymöê½ßýîwèСõ(Éç ’èSO|â/¿üò±\òiú7¼á ¼Há¶·½-¥EYŸOxÂÐùa{Eã|Àt0Ö–3aØòI°J”5z¾?÷¹Ïe•ÓüK~Çô=Ý=p|$qÝ_L“4@ØüŠD,ˆ/�ʱgP�u4†Ç�“ˆ‡I; UÜ^:BÁiÖ¨V§�QV¿¸7�%4‚а[Å:$ðùôÇnÃjü¥ä«åBH4�*ðä‹*žÍ Äq@’šhœ¢¯Uølqɘ:fDC9,3¢GÈŸb‘¼`E®\“¥:I%z±o DÏ+ h¢c‚HœíV�ÅJ>M* ”EZ וáÏ!â™÷Eëùl{,~£¢£ŒÚx5¡�¡Ð ºÅDK‘À— …Hji x ë�(UJœ,*"Wr]<§¶æ«§Zõ@ ÛWêPž½IÆÒGºø§”#KÔÄí ‹Ü›ü˜á[Qq�� ‘¹’jÎUGvV§*™.m£[ª‹lJßá;MDMT…�-”â°’©’Ò<1K£´Ð<)×”9ôgš5pŽ\€¬\ÀÉ"k{!}ÏJ°ˆ¥J $*+¦Q™Ò«2¿ú‡w©2WaI#Tõׯ´ëª‹¶¾_Šaž™¶× P¤nÇ•^Fv *Ážú|HB'%O¶a€N‚Ë�¾B…ºZJ×'Ï~%iÄ[ =-V#þr‘Ù1�-ø:M,ðJO•8Ï ŠŠ+#Õ2WàYÇ[ŬP؛̴ðé—;4ö-²î0^—1‚ä1¦ZpCn¯úx•~ëÒÇ«>^õñªÏ¯¸‡ôùwØMί¼¯îíÁÔg9Mäó˜3¢t ¹Ê¿à‚ ®¼òÊî°wŒ`RåOÁ ò�‡4è n@f=A¶TGm  `ÄÉ£Ùx8×üÊ5À¯¯ÿú¯ç‘ùM.˜Èë^÷ºvÚï}ï{AôxAjËY/†îF)šðt°ö€°1=ï]Ýx�þyüã?ÞøÁc&•>÷ÜsÙ–¸ÁžÁù*ó9…U>jƒî�øJºy)“g”NZ·NØìÀéýïðåZ³ö²—½Œw�°*³Ö…QtÑE• `2@®J_rÉ%ô‡³Î:‹ˆlþ¿¸à¸ ÑóéN�iqÝÑ]yÔZu-Po8�9Ð`g,y#“f¢”'²uš�̈5.hìСC㊤‹T&gŸ}6OŽãU! ôðv-ðv‘5òØX±V´– ²ê�çÄÁ çT$ÂNjüÓ({¢{à¸àŠ'L&L=)ø[:Å„p�›}dÁL�,A!Xï ¶†Ð±¼d>}É =aqâh ÉU h#W£dãëDú.q+4Çgs5Y®Ùb10Gðœ#uуo4‰UDx„õè+¦¡ÊV˜a=®”‰s0,!K�:D’ÒÛHžPª%ÔY2b1{gE ƒõÍþý!k««¸h'ÔÜ¢À|¥¨BNT›Œ¼uÁ>TK ý‘Ny(Ï?™Ã™1Dˆ…YY?¹œšØ"�§Då§r¥FÛPªšY¼/‰•H�»Ò*Vj«;@m>J°¶Ö�I1¿à�Ç¡5ç* µh Yª::™Š«Ya—}.À"ÃÒõ  «»ªÚeT}…½Â6Ž¿äàD]/ú64©¬àa¾}гR½¬ÏO24ûjz‘öFz,,ÝDØ7ËHúH–½M@«àyŠp½ ær ë![“ØýÁ§I¤ËÐfÀ Jå¢B|GQ¥*Í-g;5‹é`]—¤^ ”f-f­òÄÍêï—êÓ^2µ¼ÂÔ³Ì%Oƒr x sqârÁP+\Gw>˜íaœ×„|y5FÛòcÜEžj-Î'¿Ò#"Úrf¦ˆ¦UÑÚÀb>PP{âç¸IÅíüä OôñÊ–À}¼¢kôñÊ ½�W}¼b@píó+GÆÇ>¿âÞêÝÛNÁ ‘oæ´Ü^gó+ó¡q^ò1qÜã÷ ¶g9U[„Åš,bìö²‚‰–¬€ÃIë²q>ÛK‰á¿æk¾fM)À_|ñÏþìÏ6Ñëqkã xÁK_úÒqæ}ó‚… (oq‹[<õ©OeEÏ»ù,žô¤'�IMªÃŸœI&«„ o8æX‡J³…‡7-Tõ Aqc » ¿ã;¾ãyÏ{ÞO»ð AÊ&:4žëåA+{+ÌʦVÔXíx"3Ù-ǃ;® i2à×ïþîï.<$«­7?+�]À¬|lÈ,Hñ,Sã�¶ÇÕ˜e† ­CVdžî ô7@X!¶+Ø®�»ã jìzæh§-äGk§k&�þ–·¼¥Þ ŒgÐ�£(¸;ô¶¦Ózæz0ªÌ­Ïà7k¢UÎ å½qš¿[ø•BÅ ”™¸±¢ hƒ1(7g¡ uÊ/ …8鉮 ¢#ïÁ1‡UrÏ÷Ì ÃÝr™‹Ü˜•è½€/N ñ%6îæ›¥rNÈ'ËsõÅQç+Þ: ¼ª"'†òƒßä4¬|L˜€Ô%QHFZ¿Fž¬íôDÐ鉊³ÖIu”áL¸1ÙJäŒLTRjsB®�œdæáL'©¬x�—4˜Ä#†SJaÍŸÌEBØ £zòGÿYϰÁø¥°†ß|À4Û\M‡¸Ç+rU¶ˆ £cN©n[ó|…½l" -Â<A±y‰Ç7~ã7NòÉDÄ$³N‘Ë>Ö_ø…_¿SeMJ‡—¿üå`ñ€Ô›ÑvM&=³{ <À„)Ñ·ñ§¿FÝÉ0EàZ±9%C8½³ó«ýG]ÿ”Û€ÁxVÅ5ÒÖ“B"p´4'‘x˜x�U@äTLí·GÖ1f ‘pï/üƒhÜÕ6NŽâÃÜXÝˆÞ ôB<†%ZÞ�b¦N(ë³z ÂE_|� ¨œ ˜ƒ™báË}’Èä˜ùÔ�µbHaP®xæ[ «b •€8¨%4Ä“ÊJe-V¦µ.cl4™) RA5÷—F(Ù2ˆ]Ði ‚UY½”Ê¢ ¥ XÏ’“<6ó‰5àªøZôÁ¾ý>7 àŠ�¥²ò«èØ…8ÄÄŠC þ…�=±Ø'õ退<§ºŠf[i´�ÖÖÂ!6·Ý!d‰— .ÓTë[‘UfÀZ‚ŽÑºÚi;¨µ=)r°ÑT&)Žg–`v€ÞƒSÔÚ‚·ÂI=´©Œæœ.+¶Ew.’žÈ4"×…‹´—»M×\Vn©LàýG˜(£<ȶG!´´¶#˜jIIëBÑw‹ª|LWÇz�¥11í ¥b�F9+U«ËåXdè(ÞRâZ1Wë¡$^õñªÏ¯¼Õpêó«Ùüêmy¯èÁƒsÏß­6—ySßÄÁJà§ñ[¨Ä“ÈXºE‚gü³™¬ª˜‘ò¸J3oË‚ˆÿ[d^«cІˆ½ HáÍ•Ïg“‚Hñ!ίRÖ 5à�ûI­k�›à‰ø�U³ê§Ö4Í×}h{è°ˆ�ªlÇÃdÔ+‹P€ÌzžO¿#S˜3q«�Êù,$dÓŸè·‚·ÆN€?~k*·�Q9t@V±Bö¢ÛK›ÿáLÛQÔ8ü5`{'k�&niΧ·@ÌVÄj‘y£ÿI”Üsïu¯măoûÛСÁ²;¢ÏV™€u>|˜>‰çÙûɳÒZß*«­ÒóÜ@®¸#GްJ‘+ �þÐÞÿ°&7úÛµ×^K×¢«Ð7 ¦ËÍãhkÖí™Ý;å�«®º VçÝûÞ2�Y $ö¿Z".|ƒ0Ú`šW ‚”XÞ¸žb"m£h"hãicoâ_ãÃ}BiA<8 ¼Ñ¾„>¤?Ñ‘¤CX.À¡$%W‘®PsvœLfHFÞƒ¨˜Ç©@ Ñ”ï 0´r ª°�ˆ€°T¢/ʳ€¤H\¨Gà Xÿ TœH‰p€%Y»F˜/f“`->�K\ß…Aä¤ t¤¨� =ÇŸ†‰Säp–™nò<ˆ’²ÎÕZê, ˆ‹Ô­½“Z˜zÖE&àТ�&›*$ ôÙdy}D¤Ú~¢L6’î‰ZÐXͶ°Žz¦rRRȵ°Àa,"�2{HØPS ôœÈ¶b ¼} 4ÙÉ+ °NvH“ b‹4ðDµÐ¯àu1‡ËK¢JÅ›ÚL(ÀWt ûôe€ÀPp®dDyJ¿Õ,þàFó™”BG‚`ÓŠª”' rŽA¡B“ж‘ž£xˆ8YšÎ.¹Ä8 KQF->QÍohÑ‚Y.M@Q®/¹QÎ*)¥�åvV‹(W Ìu�NÆÀ\P‹wkÍPÌ9]Hl”å T„+¬Ñ‘¶ÃÈ ueÄÞ‘ª§¼Ðè¬Y*ùz(ºR¢N\‘H� t££�‹�«õf Y)¨"|¼‹ÎvzU‰Og/«íiJ#X]P>/V-›ÂQ@>žÚŠä¸ÊQœ7îëãíãU¯¸zúxåÀÖǫܠ-3Jú…WúüŠ;Š÷­>¿rÄý•ÞÁ]ôcà ]@ÇãÙqYˆÒD)# 6€PÖ^U)U H"ÚßÌz· ONí£n >@l P@ERL@>� Æ‘o!‰]cè Ka¸^},à Ð7Ä�Ž�yæ™�$, °iNN+]3�?Ù‘‡Qg€z�Œº(Æ« ZÎÆ €6P£P D¯è«ùðÒšm·1Ï5K1DæàÁƒ”"t&®YåÄ3YFâDy‹ñà‰Ë=qôL^KÂq⬶ÊŒ�+¥.–MÖåR]oíÛ&9t²î��ó€s#‡Sb¨Dñ‰ð÷¿â9KˆB Aó-ŠÀÝyÙû4��[y–0J2Ë—˜�ßI !Wâ±[,s!µdÜl‰°ŸÌs§p±�ULSÛ ]„I’&á#@eâ’ÿV²@ÆP]bŸ¾e¡wP ‡èìÜ;’„ܤ¨»°Lï’±p—U*.(¿Âp¤Õ¼(CAtt*�™iI±Ð“¨„$Yǃ|9HíaU½GÉIFiTŠ\Hn(¢¼X‚~ä[��CšŠ¨†e.é‚>ÙÖç€Biæ»§0DÁûV—m ~i<‹óB$°M¨É¶µ†[�30ÅÓ�¥Ðd"'q'òA‘Rh§Šª*x‘úª°'1†U¹°Ü…d‰)'”|(-vÁ qm™ )˜FVºŒ‘JH‰P‘[³ @IDAT)Éà×þ”J2µøm#E}5‹¾ž©U]�µÏÇÓB­bêE ½ƒ[/âa™FÅCKBK‚FV¯ƒ´†s"¬…v®yÔËò†œª¢Z(ñ—++39|P?íj>–†wš B4Õʱ.r¸Xrè:äT÷ƒB~JÓ�UÑhk’§xóÕTQœ¥{DYñ1'‘¤1üP{ø5 íú8I¬/ü%܉™êgÀ&€mÕ2$ªZOÜÖo•”I„y�˜Ro?u¢5ížøÎk¢öä°²Ð}ïH¶Wh (Б<xÐv÷ñ*ÍoëW÷ôñ W¤CÙOúxÕÇ«>^õù•w0ÇHn%ÜÖNÛù^؃c«ËÄæUrâñht£<ã•k­âd�UËßø ,Þá() ccÐg¼¶qÛd�«-éÚLnÞõªæ;à\,Âdô™ÚÀéŽË™Z ŠðUä»àæá£27æ«ö¾�AXÑèöÑl®?kÏáº>k”b“/Ç(cH‚‚¸a8úSÎìJŽó¥-ǃ»¢IgÚ=Ð=°'0’ô@ØIÀ¯xÃiÅÂ|9¬)˜4ú¯©È–PZã{^¸˜rbtBà!°qc´Ç—a3¨4À|ñK®á^«ØŒ£ ¯YC’¥;<—]L€ÁÏ%:�j³�a<±wj˜«•“¦RS}�çyó£P“:Ï€›M Ì™jS"nÂBä10�P‰Å‘Löú¹Ü­šÕµ',ÙË©/¤D?!µzµ�aíPÇh‘a®ú)©œE¶°˜“;ÞMI‘p”ú$'Ôà4Ή< q‰@—,�§P‰uY4„¨§ï"&ïPE6_àFEµs1Ý”ŸU(Í}¼²'öñª�W}¼êãUŸ_y{á×{Fþ÷Wwjn0Ü6¸uðÕçWÜ…ËCÞXo0РÌn«»7R6cS­1ȸ™*Ѐ‘mïAlkòÜAÅæù£*¨â|þîå0ó6|ãªp†šéèfâÁÝS¨sîèØe€®§r/²1¢%¹wø•ËÑ „µ˜® L–›qcü‰*šŸPZݸ/Õè{è2©aªC(좢¼µ3{ ±Çp?6Á\œÁÐ�ÁM9dˆ dI›UöW„�|±…š39ʤ¾ù„%|’ÇnP˜øÁlGêQ‡Ñ@Kbb"òã QãlعT/§`WLÌ,•7HÅ,žcŸŸxŠÍƒº)W-}æ‰r8À'›ÈBHfìÓRïd½›‰V NSQm‰Þà|�Äûl•H‘œ? ÐDäˆ;óà/ë 9šV2 ÇvgäêDM!õE(_àkùqã-õ²MWAdH¦¶Ô¼‘ÔË"5Nr.€*þ¡NV¤5a�ïDv᪳‰µT¡Wk½®&=¨¯Æz0�<£PõrIø_5ð?q« %¥B´éY�Ñ-ué1Ô×1ø s9QC�º þDÄ6n‚ RÌ·*¨.�e_-2]�Є3•‡_ô³ÁA½í[}¼êã�¢�W}¼êãUŸ_y“éó«m̯ro­z%ûg÷Àié'ÀN?�“2ÁÜl€Kf‹©À<@ab(ßàXj!ÒƒP‹Bu�B°�]œj”‹Û$V<°�ÐE˜EsSÔZU#Œa®Ë œR¢†°@La•Õ‚R®Ô¢ª 7Ô GC%àOs Û€IE�¨),"ª�¨Cj*[hN£ÈÐýᬫü®hëâ9Gm ˲%*ŠÏ@ºœx^VÖ®FÛwì@⇲b^¾Kgz„x�b¼qÁ`o åL·’â Öÿ)>È‹6âöš³ˆ hÀ,ñŠq+>yŽ•ÔÐÐ�\Z•ÑÅæSeá0½€ ¢;ÖP0Z�¤�Ѐú¶Oꊽ™Ôb>ÔaÖ|åAúŠѨÞ CY ;©¤i’ˆ‚…&ÒY€"^yñöñ*ƒI¯úüÊ»�7Š>¿Â ÓùUn±ý£{à4÷Àl2É%’¹æfâÁÓÜeÝüî�¶ K�e]vâŒqOñ+£\p„»Ú… �µJ‰•ÑÉÔ"´3‹2>®HjÕ5FΟuA Œ_ÆäÆýÐuXP¤Ço¢tCnêÊ�í’‰"%ƒ±0³&–Me$@¦~Ò‰[áú|Œ(äÀØ^5`‚e0%².‘Iž6'_•�á0@Š šc“Hª˜¬“gjèØ\"yû©ÐO–[‘Ðz(yû$��8-]ÔJЇ¯ª\â æ7*‹ÌUžv¥ã&sH*] PY=£ õä^§AÐUû'C�\˜ó¸½p¶.J*ŽÄ’ �kÙXÍ«„*AÙt ÷9Ÿ äG)þ´Ð͏}y9.wC§�h±ëcºÆÑD‰â©ªœ¦°Ež”�*Ó+â«RR3R’Ý‘nQ”t8¬A¸š©sÄ,Uq‘${ Ùö3T§yÊpè�Å/:  Ìõ"K/êqžjA½td*Ç´°uä!¤e¯Òñ62 ­¥„Â˦ÒÖT_õ`µcà@ꥊ‘�óJ[O­Õ…€ëL‘öÒ0_[EƲ‘Üõ6öct’b áaŒå<”£jjÁ‡ïp¶-¡‚T*ÖÕ}%<Œ¬#ßP[/�2%ò²Ò¸ =¨¥#5L­-�Š(§~.ˆ¬"®rLhu›;ÖH Pè„ô¬¥¤Lûx…džŽ#õV¯p��Ê@Яìk}¼êãcI¯¼+ôùUݱúüªn¦¹£ö�î�ÓÛÎl�/8­â^Áœóøó«ÓÛcÝúî�¶Œ]O~Å£­R ± •ƒˆ$ÖMèN’|“¦Ag³›ñ¶Ñ óY>ª†gf%à&þµrœÑzBÄZ cL$yÅ÷‰Ý)jF––ÉÖñO Ô ®g8Ì€è¸Æ%> Oˆ%ö`µªìèYP�ªÃ“#LÔ!¢—ZÔ]«~aJr�‘é¾0H=+)$µ=¡Ì•qÔ´À"Õ(Ú’Ë¹ÅÆÉÊÉ>F½Á©ªšWfËLQžÛ)"–3Q° N¬àêºÜ$$² €rfÜt2ÌÔ;ž½dÃXlJ­Ì<ªQï2+u-Ê’½/0äªã‰*z;‚°ß¦*Bg>‡Í‚ž²º)FYþE.uB#êƒTå±®�Ǹ�à«A¹Q.ä`G ¨ù[$íò"¯è ÅÊ&©,½Ë«RíeÊ_ürjŸ´.œm1KËkš�zºÉ”ó­ó¼­K<л²OI:‡ wÊÊP|¯ªÉØê‘ÜÍóÕlN"Y/!K–ä(ƒsU£nZ˜Ê>Š+ê}²Ts3ùÌ%ðŽ=š›LÐ0-ò©wñ(�;\ߨEýXZ ŒL•öÀ Z©ß”£hg› ꫇€l`ϸÔ–5­¨K/ZÈbÁÔ±›xiãjÎU-c gäbj¯ÒÏüà Õûx•~koêã�—œWK¯úxÅðÙçW\\ -½U&íéé;¿ÒqJ�ôÏî�ÓÑuÃÜZíËÛw¨  —³ÈH‚ k~ x(† šDžÌF¶Z…Þ®,²^•Ê¢Ô�‡Àˆ‚55E�¨ëI µ‚DmP³âÒö2@Øþ唩 ”P“@“Fùiyò“ÊRp‚ª)g Ò1(‡zjxÀÞ„!¬âa3 ¡>5duT6û .ˆ‹H(5…¡$£žˆ ⬠œáÞDÏQh�@Ñ–rGYm–…žÉFÀB˜CË™ËTçÄ@ö/»�7ˆ–p�ÎLËZ/:çƒ UŸ Òè!X1xßåN*�‹YÖ¦˜Ôƒi7x  ¦¤ˆõ�8?§*a›êhÜXŠÈ.¹rÕ/¶ÁÍ #LF·` uAyYƧACæJB*Ùªí*ä/6éÚáz‰êQ0FMĨ$Ê#H{•mŽ$èl]>Å;´Å ?ø!üa$»œàÑRt‹Úr‚õL©’A<'3Ѝ|€ÃæàÔÞ�ª¦ø+„.î”W6dÛ –BªNP)9ø†'Z‰– ÃAá×J€¤8D^v{búÕaOu’ôÃÉAš jÕðÍ&lªºô:½!¹\óê ÎU"=MÑRYêK'LÚ$� ŠF¬‡j$úxe7Ç!8# ßÇ«>^õñª�W™78¢úÇYn}~å]¨Ï¯¸OçW³ @ÿî8�=À€É43A 3Ð$�?¿:�Ömï¸Á{€X7°ÐIÁ¯Øp*8BLnÜà`xl�åILݸUã`Ç%þ†e†|ÆËÂɇŠ`™ÊÄø!5;@FÙð”�Yœ².Æ\!‰|ï1/¾ª ÕªŠÕ±à¥4¶B‚pa3@”Ê£¤ˆ»Nq(Éêìr#)",Ñk”“iú“bÌ ºV*8Ÿ5XÁ0/Z+èŒêqÂ/øBÞR RT¦¨ˆrqÏ`Œ>†ûª‹àR�CÞ RƒiÉUÃψ>y8 ªY¢<‚´’ä¥>žB&º¸^B1èuãg :ÆÎC죲š‹—<È?‚©ù­Hx‡‘�­ÚƒÛ_‰É¥ŠØŒl¿Ô,²JáÀtjªš;ãK¦p”EƒáT�îÖÎÔr4a[Ùä–ãmúad¥“ÉVJ/�¤|FZ'h ¿áC ¡R`¦–*Ž9éâäK�´|•n)?¾¹6Ûw©D6M!îWÛcÂ=r² ·GïêÌ«&¸mËHZ0”f©�ü¡S–¼ W'j¸�Uûu‡]P{I³€úœDA›Ý3Å Ï‚8%殺¢5¹ K.Z«¶4zS16]ä• ÚÏ�d¸ÐZá'kÏ凲Ô�Š*+Š£: t’X+™v1º!;½*Šùš_Ë(rB¦ý£�W¹:i =^�ÎmaoéãU®à>^yéye÷ñª�WŒ¡Ž÷Þ-3ÊÈKF�ÁžÙQê–T´ãVÈßçWzN7�bó+{@?ºº¼O:¡Jød±™ùUw[÷@÷À ×L‘�ò ƒE2 â” Š?„|0ðhÁ,GçRŸ~º"Wçç53º5„èD"#÷¾ÆßªèÁâDÜFݪ\#Q2\Xû}¥¢Vª@^gbp5˜ÎÂ+# jdÍ”2|¶»5ýÓRéÕÊþÒ¯AYÓCMõ`Òªª9VãPMÏÉ`tEâòÂ> Nð<â(êåÚ–C  ˆ¹�…xXš ïÓæÕŒxA;#?TfÛŽrsýšªã_Ëàl >9Q)àOå_ò%Bn’Â5T< K�‰´!"áP3)&ì´²å%)I‰²¤H®ÊóÇèQÖøHÓt §Š‹Æ8Ò!lÄýiy•W4@/OÙ™Nóz®õx¤0¨h\]YêRbÐíè*ï“°ãiKò�U�UH•£†ø¥Îû¨æ3æà¥_ ÄX²olq8EY‰¼5|¦mf^êé|â©ð�ÀÊÒ1¥ ìm‡…åѵi$=ƒÏÐH7¹!—ê‚ ³,¼Mȃk ˆ`Ë%{•E—ÁѨ-`)!ø«üLÆ�ÖlÃýÿ?{w¶lIr�w¾*3  ’‡6©ñâ]ëõÚôòF¦¾¦™Ú¬eRƒ-ÌDefõÿ÷yœÐ"�éqöŽÁ}Í>ÄZëxÄ.wÚôÐfŠÎž¶ Úz¼ÀJ&¶ÅDœ’ …c¤€Â›NOU2Çv#fZ'ÀÊD•´dZí+ˆ\\ô#(Aä Qʦ’@�¯ôYsD¢í˜ÝÕ•+¥´£t"ÓÌ6ÁÔžãvR¼X£>´c_AUw¾23‘>Ñ©ÖÈ\žè¿óÕ�¯î|uç«&·Ôë_]ÿÊ}Û�øn×½8Ú¿E<øÑÛíàZà�×¢£Ý—O hE—ÿù«7±w(`m¯¤‡ö pÅÜG¾ç>]´L¶.>¤OÊ“‰ã£yêÞ{T§R"ØD~Q´ø:¸ÖôFGªÄ"É ¬ðÌ dè#k –wU¾CªaY¯�%�ä!~6‘ŽvL6§†©©‹¥#ˆJªaqîbhžL‹&¦Ö” !L ÅZÊ�ŸÜÈÖDué—dðú£¢¼ ªi‘ê…V˹‚ËwYÆDGˤª�‘yÚ´ Ì$ʳ&®Žq¬‘’8.û2% “0å^{Ï?H´1•®*®$]üVÆ¡®ÈüdAF joj{ã1ÚžGNƒ^¥FFFå'q3È�4ñ‹¤DŽÇæ°¢Ýx.õ£C¬5n«²êRÒ¯¬½ÆhÝ€.� b§™8^ò˜±BC*hbŽå´9YÌð¬ó§…|~7`¸3ÒpQªiä]�\%‘–޹ÞYG©•kµ’´µÀ§­ ÌÔ �–— Ù"6–œÈ\9ÑB•d"2;Ô$ Ó•ßý¸Åhé>C¯º¬8D ¦ÇÄ[ÇzÖVR¼ojñílj+ÿÊx·k�kóžÙï^³] \ üñZ@´ù‡‹-x÷ÞO p>ÛDÉbêr ®N¢DåRUV¸¨Xü^i_™ 8.U Žitbð'®² 6#4(%‹ò÷0D§ÁÆÛ[Ъ¬Æt1z¿†:ø.QYx? ÛI_ú"©ÃX€=öx”´(¹PìÓ¾'Ý0úÒÕÄ„JH€?;rV&ÿÐ'ìW^Ì—å;ÌÞ¿–3ËËà?cÐsìI™7ýä^i�¬‘=–±Áþäì&æJá• ’R(™�Nʨ :¿Ñ ‘N&F�âhÛÓ¨Š))ëW+Š£Ì¨P‰2¢ËÔ�ÊOü~€Â�G¤8Kxi»8JN…¹Æ—SQù»ÑŸè3(*º°°4äŒH $i¹„`"fµ O›è÷uÔ\,2I�È]VQZ(Ô�Š–Ã�ÜE¥xgt�rN€Eþ1bv›@šp¬ãñ"#Ò°°–¤!Ú’¾ìÒkÙ^Æ º!Â(÷CÏx´K¶@ºL#Z‚k‰_™GkU T.Ô¯’è~$ &¢¬Œ ¯¿TY­Ôpö“ÌK?Ÿ‰uPT«™¨U,ñ¥,ÒzÐÔEŸÑ¨U´f£lâ3jrºˆâ³¬2‘À’PÓ/1#yz]Ã�ÖÞPŒzBŒI�1&D †%2øšåíx®{¨tÊ>"5�Ùô°XUX:°7J–³®æÎW 6k1-ƒûl—Ñ´QÍÂŽïÎWYâÎWw¾ºóUstSCw“C³üõ¯>vÿÊ=ön×½x°|Î>\Îߨ¿úè w p-ðGk�Âñ© Êlê.„PU²ˆw‡N§ù«7­$‰Ylšyšxb½TŽi¨§È °Wzò•‘Ϊª“j‘W(¤^  Cä�-ÝNj£\ö’,a=ÑyX‹G"4~¡?)ŠWMžÑÛs†]-‡@æÙ‘ß Ÿøä,Û’}Â}²“I\Öb?\}e$¨jÌúåË/½Í…Ú3ÅÛ¥ zâ�rSñ‘R²/¸®VLBçò)ÉÛE å8’$Gï•G=G;¸åi>é¥ad@x¤ÒÏ»Jú½É™fi 0Yˆ-í–Iüáˆï’E��Óí2¶¨—§Kš~ÂRUå‡i”Æ›ÁC¬8„h¤¤I¬p›öÊ”® ¹•Y : Z>(q£fE KÅø¤ewVÖÎÈœíÐê´ØN%:PfÝ­²³0Ò¬ÚŽÔ·¯ÎÐÁ�×hÆFݺªæ`Û0ߥ÷�Íf<¸ýÔ„çO§2ÊKR+ ÅÖU½>2b�¯cú‘(ØÓ¢r,ÚAà·«GhÖbÕ°pP§Ù(€+~6L®Ò©²Ä$8CÔ$‚Í˵6ztÏ€4�u@¹Däú«I 5N*™Åƒßj5M—^Àô/‰â1C|ë!‡Ðx…~É8%&_0šÁ/gh䪒˜�£$.µ÷é»×-<Ï_Ç”‰64²=H– �J]‘ÕÏ/ŒÇÌ Æu…§7ÜùŠ¡2—64tï|uç«;_Ýùª©óÜevÓkzh*5�š&®õ1ûWÝ@ÝBÝbïv-ðÑZÀ‹‘~‹xð£µ×UüZà�ßeWûË9p…¾ÿtù«V½‰—»qÞ-X¬{ÂÚ'öÍÆ Åry q�üI@âí"ðy/'ä+â~I鈹�³W Ž6 qø¾¬W´û è·\C©˜½]ü&¥Æ®xò‰ÂOR!¸Ô(wVáH†Ëƒ$®¸\AÛKx*Áì çG'f }R€dAæè)))”‘� =¯±( ûðHâR gÑ^†A!ýj$Ð 5É® -s5öÄ•ö:†4)Ë~XìO¯Á–P¥_aG)�*z;”]T‘ïIkâ´r嘶—‘Eåþª  6A¹GïúÙÍW‘jßC©ÕKÐV‡È¨ôå–w)Ñ¥ŒMg®”.eäeÿàqÇ sUÄ8që¢Tgˆ]Zœ\M•Éœ Už´ò2QdùÄ­|Ö—ª[HM‘ÃËõªJ¶G¦Æ+ã3ÝG™<ë_â–m%4�fïŽ{Tj8º›çM§sVï· :xr4.2^˜áÖVêÏó¹£ä–êZÊ�^I.¡Æ\GýõKZë:ŒæÑËÉ9# i‹ÉA| ­OŽHåˆK|ãRµTÉ( yJ£—`øy“ªÃàãèï!«/â§ ÛIóé›d+p¯~¥·I�DáQ ÆÀ=7èO®ähþdº~~©{ŸäÅœüyÄ›ûËÊ-,qQD¾&«•™‘R$ çL.&•±mCX`\R†´KU"w»Ù …T’g(‘(›Ö~˜$(c³ÕJ9ù*.™ÑG]LTw.•ÃáŽèØ Ð£@.*õ@ã@ †³v›©˜á4-Ø;à»]jŽF�ÇðSv¤9FZ÷“Í�0Ó¾¦K:Htzl‰ ŒK:Ý)]ªã·)èÍ�ä麖ie©RmzBÊo,ÙûæB·ÌVw<¡œs‡ø™P@zç«5kè5öÎWw¾ÚЫO4ÈÎPnê»óÕ�¯š!®ÕmãúWî/ESÄnöçhƸ۵ÀGk�ÆEÿ±þGǃ­½®â×ü8�å*Õƒ�µËH4ýH#X~Ò&Äîpž ,ËÑ•\‰ð÷Ðݸ3}ÁpuBbZÈæ@õ ‚€¥ ¬”r²4E‚ÚYÇ‘ Á˜�µh¼‚+qVè½äAÇX‘ð[žQñW©WA•?¸yɱ÷Zñ´ z*)^h …ÆÎÚº*¬•·‘dìqÄ~�¹ê{™½7ªI øKÓq™®Sg‰äVÓUéÔµ½LÏ’EØ[î“<›D9rÑl›‹D�€)�ª�Ï:R!QR˜”¢ž"ês0 Be’Ú¶œj¥j¬ËÓΉ`\©Pb÷J¿ä˜\K…˜kyÙ–´Æj #Þ×” ~U•ãÊÌÚ~·Oç2�r.V„êÕ£ÎJRai5Ù-øU�4»v¼Åpû”T:Wž=H´&dKÒÖùfR™Å©ºv,"¨GäÎ;{û®7ú¿÷c ¢Æ@H”HÏÖë«Îs©RèT~è3c²dEñCuºeŠJ£Z穦ôWôÏ"Q@©3JØÎ,§åãá ÝŸÔh`’väŠÉTîì˜|Lã—ÑG8+ÉP+³EUY#RºM¿¨!ÛgÐ#Fç„–hY—XûÏ~•ÕÉÖùcûõÍ ÙÁ«ƒ@÷ “¹x: ÃuäÙìËׯ·\pÜèÔ†Ó9‹TÀúÑ2nÑÑhmTIÜõãØM¾;_­Í²í�¯î|ÕXièÜùj“Ù�¯v0­6WšB¯åÎæNâ^×Áý©{Jcæcô¯Xân×½~Ëxpá§ã¸õ½¯®þç·@6!y…ÉBØ<�Ü‚|džÐXùï3EŠ8H �ÄN,'2T{L¼[á¾�G £wy\+Ìäpš´bv‘zt#ë—:-`.Ü�ºˆzP|Á`'ªÊ ä$.Ç÷T£QfàPä+ÁI¼'=“OÉjã(í³-úäXNk¤Ú–³�hŽ€/à¥$=>lrŠÈÊûw€Dì}iH«é+ËBv¹ŒlrÈMz…c¥�È,U ª‚Žþl•2ûž,gº°gõýŠiI øfË™8 "E]Mt@[Š…Ô¬5X¸ÕƒeŠžÔÍa,ëB&âHÉuÚw·’馀G =9PÙ“£újf¬øL xT7 cÜ—á�5k‡Q£¢mãxº¹°Yç[ÛŒÀiècÞ¥ˆ^¯ùOîóÐ?½!ôšÑ�Y„ É”A-XÓŸF©%#K‚ð&“³žºl�tOܪË� 2&‘OTg JŸ^+å¤f?|1¶~-Å…øhd_ª!N_úØŽ4¾éÕþ0êÕë–�•Ž?C´ˆöÈ!ÁÐ3žÕ}(Jš�æc�u 5ëNFKÜu¬È ²cdåøRÀcŸCO0™¯)±ÃRa�•ôÀT9Îj²Ž¶0…÷yBfEb¨öZ«³²S�飃‰kWä–eyÄžW™ä²fW fHØ_�{–žùÈ -Ï…=ŠITøÖ¼~“4©a÷n­¡ ¬T«ðIíñèù)‹x’œ –B•Í÷T[-ÐSUB�Õö¬leR e")5ì ü8AÌÈ—0�A=-Wå6¼óŒ Õí,µÁ.@±õsÚZYTߤR·J ‚‘¨rL¦áÊ,·­nr~z@¬J±DŒÂ{ùXF—Y2æd¦´¥w‹µŠP¦‘}Ø:ó„Ý>9íX§ªÎ=-ÂÈ4®þbC�TêJ;Ñ®ä�Þ†IÍŠÆ`*A±Æ=,Hm Ã.]eÛŒ±Þ…ýÆ†Š¤jÏ3 Nž�dƒ Ýw­Ž9f&°=Î$ÑÓûqN«ÃI0t§e#¯¸™þ|ôÔª|ô ömcŠ-jì5t^5�¬ójð1Wldnda©åkk\ ëÛ-ÝÚÈJb“¦í¦Øé¼ë=²¨‰•°tž5šWZyÔ‚ñ“¯LìtoæÛï‡,;=À¡¢HWkÜ(=îjÊ?< ÛN –IW›Y+¹€„fkÕàÚfîhë¶§ï|uç«;_ÝùêÎWM�Í–Í�n7MÿæõÝy¯un'‡µûäÝ] |̘Î�üGŃññÃþðoÿöo¿õ­oÝ…osÿ¹ºÿ±X ìH6i¼Þ*ö‡‹‹HÅÉ}‹L?í©ÌVZ}ùöý'?¶JðÎö}$Ž$4ÄÈ ~y(¥ ¥ozþŒ/â-žó›É¢.ºŽ6Q ²tPA|€‹Þ‚@JaõƒŠ¢p×·•r*ù‚‹Z?E3F‹½Ñ=ë€öò,dà.ÇÓ½\;x(2V+GÔ§ ÏFzBoiN6@¼LˆÑé�FóR+¬>ÑZ]h *Ò¥X‰±�Åa†ØˆûŸ H@enö0mO±MäÈÈj0–g$?-3E¸R£9Bš&Üå¤J²'5<-á¶a9UoG‹0ù<~ª!aýˆ06ndžÜJá"& ëã‚!y¾™Ý𤙤¦"W ‚dÇ^”ÿÉ«·Ç°Éfk¯¾vuœÌÓä,GTUM7•}Ú¨f èàì‰,¶ô¬Î˜‘:L‹Ñê9†7õ ?—ÀðÕy´w2Ö>Â;צØ)î9j]"½¶;æÐL_#4€º6•2Žc%®Ö1P�±œñémYEm&"¹ñ¥eä+°ÁÔ%ÃHÄ£Ò°•ÔN`Ò”è«û�É©‚NË;_ySçiË;_é|:ݺå�¯êw¾ºóUÂT{ý+·Žë_¹Óð¯Ükïv-p-�sÉ¿üÇŃÿæýö7¾ñ�üàó7ó_þËñÈÅÝ®®þ§´@óAÚPmÀ6l¿ýío ?ÿpù«7Eõ‹‚ áÄÃJ}Þíù¶ª²â µEÈ�彑–˜?÷„ËMZÃçJK7ˆÇûmÅÍFr"n¬:ó\“]éù ´Ñ’ýy¸ª²tAÄC„_]ÁD ­Á¶³p¢�Ÿ5®¶÷Úv‘}Ðå,)J(;ÞhÅ¥ñUË¡”:Â-~RG½­yŠz 2+= ÷ØcÒx~ÀlÜ2 e:zp’Ò²‘‡}(Ð Š‡çzI@h##Î)"Z‚ŒóDB…‰=ehÁº“[nb©¿#!Ûô•LšOލ%HÜ{×pÜJŒ’x4;IätHX©T½®Íc—Ø®{Ð�•%^Ô£ôr_e`‚›àéºXQSBº’3Hç˜AŒ6bk4Kß"0�1ˆx@�Æ'mJ":<¬äaeqåä˜É(¾^ry‡sjJHJ”1—ÆžX§�fËÓ,Ÿ¼—Á°W¡ÌErW¸|Ð\Î,ΩP†))£˜5#꜉k¼F¡ ï3ÛVa<˜ š³½l%JÄÕUÇtÙ0üwå’”Og²u�(„@Qþ(H…:&YFWFÆøž°Ò3ö£Æ™tøCÞ•±�ì™zûEe9?åQÚŠÕ5XLÚÓê,x¬K$2Íe~·ÂŽñ£ƒ< ƒpB„�J"¤�ûbDZYê.Bë+•Ükñî|•}î|•tñõ�;_$�’g(bgäßùêÎWÍu�FJŸæ]Óìé-ί•9Ø&£|„þÕnàÝé›HYànוø�grlÏçüGǃÿö¯þêÿüÿ¡`þßÿûÿQ™î*{-ðGj�ònõWÿ¶±/.æ6ç0/‹ð¢Ïr2¿×üÕ~”p^{Qpä¨þšŽæÀ;7=MÈMRåݪ+é JÜ\(%ÙÝ×`˜7#IÒKñ °EÔ]È4DúÀ"¼ ]‰´Â¼A;ÀÄèD�Á'4U½Ek9œNãésèÇ2‰óÁ†#±NAûÛh•R@pêT/#V5›O^ldÖR$À•´D�QIˆ8%G´eMH±& mðx´EÐÒav8Þ�6$FçòŒ&c"œ&nrJVY0µtÌúâý¶Ãì]\pìoY¶Ú�ZÔ@i»R%fÜekÆ)nZnd×u/&.§—vñ€ßw‡0ÓC£ùˤÖ^Q¤_£ŒŽðw/F9HHó†› ö‰2ѹ–Û!§:-åçÆ–1%n3¯5’7³ˆFI¾F“…ëN9óÇÁ/cf•™òdœHˆ¦¶ÔNÅýYÆbVõå;WüÏ”ƒ�سù?þ’Êj+•Úí—º ãÆÉ�µF°Ú³þ«X�„Õàˆ5�…]â�¦£,z…N„±_§Â,8îqh¹Û1¢X;s�”ÁŒý8Õ£HÙh“s‹Û¬1žuõ²ZFèþÃY4û£Wʶ°0ªÖü€ï —ºóÑ#‹öËŠ*?yïiöø ‡Ò`dõfRŒuµ3c&z=‚_¾öÃ&HNweÇ“ì�‚Š;_ÝùêÎWw¾ºóUSaFhN¼þÕõ¯ÎÿÎvÓÕ#ºÏþjÿê+_ùÊßoûÚ×¾ðÝ®>* Ô÷Ó·QÀqÿ­âÁ¯|íëÿÛ¿ûwÿ×ü�ÿù?ÿç>�oýQ™ð*{-ðÇa�BÐÏ¿ñ�¿øó?ûö¿þ×¥qÎ:«Å³Ý,óžNì-Nðwß,‚•:ù}å¯,±éf]4ÛÛ–)ø%ŽÔ’3ò ‚8 )óEļh-©‡ÕÙ‰«+/|O�%RŠÔ§°àa"7ÏAˆÕ+:9l—ªYnÜpEápº‚U4ÎPuÍ» õÂÅf¹�ÜÑÊñE»ç0'†«I‚‰,’€ñR]ÙÙòte(ª@üã$¡1ù;1+É  �¤KEø‘pSz —,ÖéEh«L²Ä ?aÂ]v%O¡R6W)èÖõ'÷XQ§”tRn(�“AÊltÐ[<`ê�Â)ž4,ˆ^’”#"Rà!¦#£M�ÞÃ…r›TO�!Ad··o9JR1"ëU�ŽÉ{ å¿Z;ˆç.ÖmÈOB%£*ƒ¢ÇQ’�B=ÊT5j ~ ³lúñ«¸#±:Ïü34{V8â‘¶€M^§ç\‡#Ý3é—à;ý53!EÉ“Êëbd½Æ¤ýÌ(­=3ûˆ<¼à‚Ÿ([EH\j£Xš«3¡”¹§{òhQªL»tIã)Ŕ̃z ’ÝPvuú`bU2ô•³µ–Ó°5cÜô/z”9 Ï¥Fš†‘5fž°£ž�Y²îYG‰‹n=™6–vnHiA]\>?“×âÞ#˜Ån‡d5¸aMþpX°ñfuaì&Y؉Ô�™µ�ºê#ß@3‹�TU™]’`2Ttç«;_ÝùªÑcdøl´ÝùêÎWÍÛ׿ºþUïð¯þÅ7¾Qöáûßÿ~Ù‡ÝÏ}öî¯þù[ ²žŸžßøF¿–�ûÛǃÿ¦GO¿ýíœ×ù¬­DÈ=Ï-ïßÕse¹òâ^tï–zÿÅâÁW½ ‰ï^p”x -HÞ{�ï‹ùî"”*…vùÞ�vƒ.f±Þ‹‘C6ßwÿ?n@ÿ¬Þÿ´£’^Šç�lj/ ÈžïýăœùÂàñ`•^ž9öB \÷�¼ïy]øT“0"ìâ�„ñÏòœtNÿ§¯=rJ8ï•3ðîÈ^ú(о ¥¿‡!(LY4öÁ¿n�H*,Dý`´DŠ6 g«ìl(C¬HAҼă*k Å/I¾¥2s�R6Ó¿ûä]T„A!–ý ,Ó‰h>„°1šøcùNW @’|úE�ãÒšÊö¥Õ»<´¢¤‰§&¹XgvHÏ€«ïœúU´ƒ»äá~ g¨8•+X(¦Õ£—ÖJD†FdºˆvÕi²8YÆÉSƒërªRx`™kR8jÙàă‡õcTdÞbieŒ«û>#ròžŽÒž¸zÁ<ÉY7iØ]Eˆ\ lèZ/ò–xX´ƒA׌ÐIôqE†®b ôõâ�VDªÏ$š>ê,æ„azQH j¤+>¿ËüúZv›ü²ÕKøÑ+ëiä‡?‰gß®¥8ªk8›�LRSHS­ûÓ©?!÷ðÓ ?*ÒÑ©¶p–åë%¯¶ò�Q f öª±1n`dˆªˆ[£JaM,*RüUÙ%Fsƒ!S‘Zâ'?Ë‚í¥WŸöîUgO8|´Fx‡HΙÅ–]XŒSÖ ¥-ªm°Í©e�ä’@ÕÅŨ ú�€Ñ˜+ ‡&õ­¨2u'/RƒOŠ(ê-úh=¾íh€ax¶².N‡ —‰ûcs<ŒQÿü¦˜pÖÓ ñÁL”ŒÇ ‡uâmøP)ñ³*LÅRi雃Ú~Í‘0Í3i2ka|Õʧ&òõŸˆMÐ)Ô8ìN£2Òš#™z UWm ¦2¹¨ªŽË2�yH´NR'eÓ@6áW¹d]FjRо cXo27IÓQ¯‡Ž¶fTRýæÍ:ØÄ˜ÂÁõ‡õZaJ›”ëMUõ�h1Ùë9k]g,$ïêi¬àîðt;ÓJ5ÝaШ={i`s'»éëíH ‚…�ì%MV��u[Â>¦L¶‡ÕŒMÒ1Õ1겯&iþŠvŒ‚™J½9™O¯wÙW+œ›Å!óZ*E©xø�ËLgí÷bCàÎWw¾ºó•ñsç«;_]ÿêúW¿¥õgþç_ýêW¿øâ‹^‚óÓŸþt÷y÷Ý»] ü3¶@ý¼Þ^Ÿ¯ç×ÿÿìÏÿ"eùÈ9Üy»ÚÞxðúW…s׿ú}ùWo,Ñ) . ·˜ËŠWüöõ{oårq²PуT& _f °;¼^„.B.rÞ{¼– /UÐõ§ï–ØLÉ,çÂ,Wzµĉ¾»"ƒR‡mN—'H¢¥-d®•HH&¡?<™ˆ%ÊÐhëåe#’4ÈÉô„9Öåš+YNB¢@ª@ò¥X�òÒ;ä”�/‰ÈT ÙËL™ ×”'fI‘%Œ–w‘yhüGÖû Ù…W‹ÅŽ!¡ŸÒ#-�X‰LF,NÊP%±MW@Â’«Ò>I˜¹¡Y}{߃z®%Ù£%6Ò_~ê‰ÀZà g1ƒŠd;¬µ ÿ«Ô%âUnœQÑsž3üiüÂ>yKZ6~Òͺ"™J+&b¢Ð%N~§‡×/û˜ ­Ñ$®ljÿ<£ˇ6Œ{–ˆ(ûp)&�]|·¸¬“¸¤üÞ40 `™ª^ôÔæ*l=VàËV�³Æ>æîådÉR œ~½lî!ÖFëm²rý£L¿b~†¬2ØàÓ¾¼è—_öŸ£Ú߯¹�öÎWw¾Òs dE'7–*»óƒd‰;_éw¾ºþÕîŒç>Ô�®ûY·7óöÝ[M&�î@»MÖk\i¹Û»þóô¯¾þõÏÿ—¿üËÿû?ý§Ö¾}ï{ß3sÜíZࣱ@y·~èðOþäk Ïô¦„Æû�s!®uãÁÜ꾸Æï¡9b»b�ÿ‘üÕëï|÷»¹ÈY[—©ÚJOE ~ÊŽgzÊW!Bqhœ›³¤¼:êÀXçú­î¼6ʼä"ìµHZ,­pY†*¸;�ŒDüé‹üà¦êC[œûƒPn% -»Ù³—ã‚A’tÈVP�D-Çô!W*ÖÕr¶ÝÊ T:FxA.«Ð’@jLÒUŽšœÙmä“·À °�´ JÜèáR„›!LßRkd¤P�—Ø^Ðv8ŒU³Ë)~ôÃ1J�7¿xYTTá*2C#æ2™LIˆÙ�$ç@%omYõ:CeÒ±X Îiº¨[¯¥³öIÏe<N—âpøÀÚÜÆ4.ø(Yfíƒ,džM,9 .©õU!Û7î¬[�ó1@Š”DëˆÌ">~AwVŠ`Nߪ||§NT"8ziÖ3,Q¦Âyu«fôj\w—ÎtÓPÊÚQ%"@èd”�ô“?H�äË/È&ïö¦”Ö©ªÞ6­«; Á´ Û½<>ë([N‹gmX¥¾„1C.•‰O”¯Û³K£ý¡¤DR¡á£m«0uZö«âþ¤üŽœãƒÉºŽ%È·@À Õ@×eØæ•È›‰` 8œ;_±Eí¥A˜ŒA³Ì�¯ØAÇ»ó•iÁ8z,Ò‰¡5Ûœæ\¯a°;_Ýùªù¼þ`&Ñiú\ÿ꟱õ•¯|öÅ?ûÆçŸ7þ=PÂ;¸ÛµÀ?s t³+éö§úÍ?ÿó¿üúW¿öÍo~“C¹€¡9Ï-Ó Ó=±»âõ¯¸Û?�Û8÷s®uãÁzÂ�9Õ͋Ú1+jœ+}u”¹â[%¨éå�€XËÁàfméJw`îRYŸ¼ïÝgö$^Ó ‡Ó3KYÿb®jæÚ}߯jSX9¼·”§”M±üñð›Ù¨>®Šž+n_›ôzˆ³ßFH lpY–x"®Ø;ä>Ó»(¤Ìþ… ¿p]R ê®B‰@ñ{´÷¾-Kd,Qå°A‘u²÷†x¦XVG2�9€›‘u5öyÖ�1Ø– {f¨.æ#×Ò;’^§qz¶oz'l$O‚#Æ%$—&ð¶]ÙÅÞBDìm÷e1ØlŽ’ÅN’I¸gN+Æ’Ïâ›L‹[ŠÌÝýã×yt¸z"¾«•tˆ&}ã«ì¤tJØì¾´€zðh÷7´ �ór#ªœ÷�)�wôÎÚº´ fà”›¤®hG2¦ »ŸÕ«Á™ñp†2MKgÍX)ŸXLâ—i‡«| ZõH�œóËäÍ’3ß81±ØJG[Þ·¥†‡AH¤ {³PM·žN«¥ÀP›ˆÖUUëg‰Á])swjf ÓÌDÎúÈÓ¹噯Ô\o�´£Ÿ±¢ÕÆóÎWën$sÞùêÎWw¾ºó•{ΙjÏd}ý«ë_ý÷ý«o~ë[­wûWßúÖ7¿õÍnÌÝ^»ÑÎÁÈuä �µ›º›|·ê¼%OX§"W…÷07ä`¶è¿;{�×S}ÿx (OV·þ–¯ŸuúóeÜÜUs@w£Ïyˆ-·¥“œ¾/'ª‚9 ¸sUü7ïÉõ Ä6ù�ˆÿÏ¥ˆ�½Ã ÝþU¥óÆö»m �è-”8ˆs�ç_!ã]/çбIû*Êgá¥ð¸š —ðôQbìz«‚ ™Õ«›ä¡<%Óმ^«{8cË.g,^:AøWdƒÕ7FÇ«¯6C¯ #åàœÊü»*£ÔTiÈ4ÁXôì‰ËÞ†ÓiªèŠ"¼¾@cIc‚ÄeG­Ëîz¾rÅ!%L�#4]×}F/�ÿp-ðñj´H>úyµJBÆŸ‡ˆzßõ ïÒK˜Ö£"©ÒÅ$Òs�øÊgŸ}ë›ßô’™f·êÊõ«ÞxФ%JÝ|å–ò»Î_õÈ¡¹*Â}ãuæÏ&(³¿©q»¦¹Í»Äf\Ók5ÿ鮿µM³*4ß5Ù=!d̰OÆ;œ7#Ž´@Ì´q7c·©—G¢j<ôDëîæ^ûö¹O%ÁÒÞmmÁ<ÌJx&0F=¨H“Mã[—€ì÷+‚�2É @�ݰ3B¼£0ÖdÝuôÔûì×:¼÷ª”CðeáÝ4è§3£Û˜ì[úwXÍ´îX ¼ÇîGýÀwOA*Äê6ø6Þ‡tü¦à‹o98¢${.°¾u'›PÂü`Þ)\ëED‚³Ë3‚[¯ÖÃ8á©Oƒµï:Å–\%Êmùžü(ú—L„˜)Ž톫+O4Ù¢î{uš<4�)ãMj2î5�³gÀ¤Ýv^¦H‰A*“Ít™xãâ^-µº‹=C5p}c>Ä*óÃ’Sá”Xïà›Ílš„kæ·½þLVpÚ$˜§3¤n´fŽÓnúºH¢X™•BÓ�;“xqŸ¼r˜,BV‹Ý*p‹vÇêÒý\n¹æX²ÜƒyìÓcÂá}#Ò–&úÉ3äö‚Õ0–�År¬“äØðˆÇu!¨¾³Èj°[QŒ#d%_Å�=vt‚µ>/¥˜Ö:ÅÚíÅ�5|&ÛR¸Î”;«Î ¦‘挶ÉQ%1߬wÊ¢¬¿JB÷ÅmþñÚþ@IDATê(½óU©«Õ¬“­ŒÚ™HϽóU=¥Þrç«ÆJc÷ÎWw¾ºþ•ÛGß�Ø¿zóæÍ_þÅ_þàG?üÉ�üîínmnú»“¸7_ü‚åÒ�_ÅŒ÷ËþUh¯¹»;ÿ¢îÌÝmÉ�êƒu<„ÝÛ»{¹É£þKþ•Y‹OÐíí âþ[¦ÿ€•KÑZ€Ü½î�ïrÎIý þuSÿêx ô§8ï…˜þ:øðÈ&71ƒPÓ_þUgƒýeÿŠÓ[ñ¯ó¯¦ç?Æ¿B6–“ð4hÒ¼øW�lá^\q@3©C­&¡‰î{ä®Ô¦mÔ:Dòb”(2¥Ãx0¤u•lVƒrçp>ÑB¨r" Ét® tƉ1OZ’>-|Z™fI7d¢[ýêWúmÒ¯ÿËñ/Bx÷>gÿƃšöƃuÆn 5fê74a˜ÝM!Bªßx¾ú5ù«~ÕóÌHŸ¾ëÅùæZóW». ‹�‰ßLi»#î®+SA‡ ¬E•²¡�´kÎ솽ÛdÀUõ/ŠÏ–ãˆVSìþQÖ,êr‹!65w„Ý7úoWS".‰ÒÄÚœ> 70.@­ŒyÒ“`ƒl[?\µn+3ñd‰Àˆò›ñ§‹¤ÄáîЧõs� Ø£Fˆ`¥êÂ# =Éåò')¬DVÞÉ…+°Œ¢{gƒš0ýOIr,åKVap`Q 6Ú?w¤"ÍÃ!® ×…ÈœUæ,Aª¶/ø)7Š^û­ w¥.–¤êäÑ?èª|!im]&™v&R–Nã¨W•á½0«h͉Í^=x\6J#®”…¹¤+9w#EcÀ²»ñ…ó%»ïœ *æ†dÛÏE¥ \%[§Uý"Ö´©¶¢Ó¼Ëß’aÚ¼Þ/cóò¯ÑZƒBG…ÎÑA4| ÖN…™‘xS4÷®´ßšZ&�ÔãЭ롾×R­œ ãó¬_Ä1Êý–‹LäÒ¥ †H„<Ál{9M™ ~þɬQS)Êãö¾Nª6ˆ7|²«Šhí pYʨ8cáÈC»Ç^× g +%Î8r_UIÕGÓýä.\ ‡$š”Xùc›~ÁzOIÿY­hTHÔy‹C�Z.Àéúíb•!IÍÔÉ %�aï|¥?<Ÿ;_ÝùêÎWw¾âlj¾þÕõ¯Ü Ÿ„Ä‚Ûó¯ò¯ú‘Ç?ù“ÏÝfÝ\Ý~r·ÝÝ¿ÇèÜ{ßÎ+š5„îÏü·ê÷–|½ñ”Bæ/sœB㜠»Î©—æ�ÌUò*šÊùW‘ª®MGÂbüòfÜ‚<£Ü‰ˆU¥üƒèTQQóúç¥ Ò€z�›!Dà”æˆpçæ_ñ •·_`lD)qà×±Ýü«`«O\”vüÿ*Oªá–Ë”Ž�¸”,•SJJ…†<,UmFIáÇ¿ê\eÚVÄNæsôü—Ø…7âÆ:ßi�2ù4N?ö—sß+q±:>Òü«€§Wû)%®¨NO;G·~qü«-+ûà�ZcÖž§ ™~NÛä&3©i1ûF“ Þ'÷>�·®ÑÆQÿ¨)?üœàªBy½þ·]€xôß‹"\°\Ö·µE» ½~$NûYÍò�³SfÄ—x0Tí½åHd&½å蹊R`¦¿ñ ¶»ñ {ãÁFè·‘×Åßxðwœ¿zý�¿þŽÙ¼qç.Àä›?‹”wfv«Ììì¾·yËÌ×ýe·Ã5M(¹¢¦°æ@·�C rÃì+ŽËîc¦Ul» ïVPy\Ú ¨Odšd#·Ô ЦèÝ wT'[�Ubd†E¡K‚Go7ýÃ÷¥¯ª— 0ÀPÍɤxœu©­¶ªÄm©O²Nœ‘•ÝÀ7ù£0!X­L\èÙŽc1…[evŒ²w~¹9‘3âP�…2±;ª6ñ] –ˆÒ• ô±ÌÜ÷–ƒ5Dû›Î�nóvˆ5À pÊéÿÜQƒ®šC¤fy¼.ÝÇôe£Ñ�Mºlcÿ54"§,ÁI9òiúóËÖ—Á ÞUøÉÄÛc­*;¸‘«EoÐöƈ¿ð´Ìcæà0p_Ö›D‚PDæá`$Æ8 9²pœ®ÝÑñ!Å“lDy¸OãÎç†L}h¤Bº>†¤¦š8íGOý¸Îí×Ñ&Ç~^u [C0 žÌ9€ö�ׄ Š�Vj’èôë þójm0M�ž®Ö :n;WÄD”ÛOyL Uëϳ}²�ËÈ$ß‹c,)I¦vˆ`[7¹:Œ¹ÖAj`ÓFǪî|¥I²ã�¯î|µÑqç+SÃfŠÝPî|eÒ7›šx¯å–µ{\÷*÷nƒæúWϘÉîÈî´}w¸þƒ\ÿÊ8Ñ!xì;ëœh0Ýx°PG¬wãÁºFîþ�›Io½4P*'HBüý«VŸ© ÆƲœyWÔ›ßË¢nU±ò_/'¸Æ ÐΔ �,#�´$ËvÈ1áD5rõ$¦‘¾‘ÁãŠ}–Ý�©©˜·ù`VAÂe(—�Â5¦C}ºçXUµiþødÃm1Åd„#õiŸ:õ¢ K4QQ ÇTk™¬L³ca ºÇ+íú“Ÿq<¤œœòΖWL�ŽÚ¿O’£ZÝQp†¬šf[ï6‰u¥ZVÿ%æzñšŸ9Ö¯ýSOR;ø‰i'ÅK€”ÒŸXcΚéIL8ï[Á×ÚÌAO2BÙ"{,w:�¤SGýlR+;$r¬ªI¡ð^%ëºËX+ì¯2ò¢ÙƒÓ¤©“®¥ ¬h�—‰ÏDk©*&qHjV™vH]ÆÎ­RI E�Îr%&~i¨bC+ûI#®ßê–qF6ó@rÍš”ϰ¬µwTk†ü@Ñ) J­+ê0_¾Yl¡mUg¸u¬NĬ�’øˆÎoøgï{{׌8Ký¹Î,HŽ©Ã!|¨5u¯§ xGpƒ–9H=ë)Nð¯ÙÊìjÿ•tDoàx6óè£'�÷¤�gÍOûo@C@7D«"ˆÅ&…Ç¡†»ó“3êyÄãÎW –zÜ�¯î|µ‰äÎWü‡ë_]ÿЇÐvý«Ü†ë_]ÿª¡0—’Ó:WØÎC„ë{¼Úë_Ýx°x#ûƃ7ü�ç¯ Äß¿“*¢-ñQO;_÷j�¯‚‚™=â%°é+Ž.®>/@?%Ò%©d»º>ïqÈñ]öJìïž?´Þ¡&½#qÂü‚Çâpáɠμ‚<š„˜NÉjäj.ïrfÎä«4Ü>­j”»ÙË Â ¥Bø¤meO5i*É÷á /ÍBަà�`R\rEh4èZÖ`çË�÷îÜÕUNÚ#÷±çÊFŠNØ5ÖK{¸�$2@²…)O€ 0(#¤Ž»ƒ•Oi“¤îÓwïe:º$Ÿ‚“ VS䱕…g5ykü–ÅȘ'¬0äÜ&\˜mtð’Ú™IUÛ�WC¨“’�,˜×xgûš°²UcP{&l¢C!W!! •R-å¦Éj× žbÅ.Ö,0;ÐÎÅ£Aû–jÊ #(q«äÓÒ‰�N¹aTѧL[\«XwpBÍ*:ef.JÄc�í)¯jK±Ñµöˆ2‚‘YVLî‰Áϱ 6F6¬€4Œþ§žù1Ó��%>ˆàp�[QèÓ |Zv+˜GÚ±b Ôª^)#¸…’³.é BÔt©o4kG¬“¤[Ò½oHT$ý©ßï*½ŒåQ0m¨JÒ)¥ož'ˆS¹m9K?7fªQH"CÂ1Å‘ùQ§ºUú8v‰ñþÎW:S�MÏЫî|¥½ôø;_ÝùêÎWsLê׿Úýc·+w.·âë_Íid“nÁ׿:^Ô0®Èõ¯n Û‚» k™‘Ý–)92>ô{œ´Ç¡O<ò¥Yt׸²Ìz3Éšö� ‡BM™—°„U#¢—Dí…[•õÑ1GV*é–J¬tÛEX-7(`ZI§;¥�ÄËTÔ¤l‹Ò�eÄü"X· e§Æõ -¥·ÿü+ì‹×N8ò%`xq�Šþ4eŒ°;ËPkv^iðíã¡nkú³’F EÖÇûaˆw…#QYö|º-;ÙbXß™qPK¿Ä™3#Á< \/.jš¸Ë>vŽhM~:/�ÒÕNÆ}¯£Ì`=à0Ú#(³iÜ¡VV"°I{ê¡à`ëìÔu,Ç>µòA“Cõùêm£²Æ†¥ü€“)Èõ9ë- Êu:³=^w¾ºó•1°®UÏÐuî|eül¼l ueÛ˜¹ó•¾rç«Çù¹þUSÆõ¯6?˜4ÜmÝvM®W±»ÿõ¯2Ëõ¯v/¹ñ OÔp1@8úÝ\o<øX„ßãÁÍœ&QÑÜ�o<(±²;ªIãòW¯¿û�¿–Æ:÷ÛVÙô÷þ­+Ï}¹# ü«_¼Ë³ðnÒʬ¢©—¹w‹µ;ëEWm:+ê ÛW¬o’ Ž}1ÃÂIO¾…¸�ý0;OH(tHjëÝ{ìÏœH¨%cF_ÛXA$˜ÔrF$“¦ZÕ|^´¶øЩ£6ÅäÝbqäCIž“Þ‹N%Ÿýc€¯oÏ™±_î°†ô–*µ‘D7nè–�¤IÀÊû�þ¼¹+>ÑžqW³T×VÎ=€;D11ÒÛù.Á­‹k>å}ýÎcå]�tØ•ž® f¶=ô¦ñØŒ|uªHµÏkÎC¿ÇÇ óÔmâ“Ò4“I)ý§ °IœquU¶VF¬GÄ, ɯמ¤¤ª�͉@[C7[ {yUÈ‚·uêÄS;`bƒ >j¬2цÉ8¬L]RËZB·2Fåsg- »C\Jp ƒµÔ++²î®»„TÃÂûkogð8vC©T+¡¾¦LY²猭‡Bë ‚¢£N­}Úo¬teÉ3ádN¶ÏÎxhŽCóÄpòF¡ YÍÔ)7<ô'�ŠÓ»µyÌ"0±©è±aŒãêÓ~¤Ž®ŸÈ`_§w¾ºóÕºO]­©+ÞùêÎWw¾2û3›ï+r�;7d³ªyvuöÍð¦û@››U®öúW׿rS¯S\ÿjð:cë .žÍõ¯ê�£xãÁæ�¸ƒô·‰ãƃÅ/ÖY%ÝVÝao<øqǃ’Ë�¸³(¼~£k´Ê[šÜtËH6,É_ÿì¿ëOaXêÃm«Wµ´†gúxr/C¯ÔDïŠÛP Ú2¯½˜ÍQ(ýx}]-r" 2¢)§õåOÇs¬�HÕ3�Ÿ·eâÄè<ä(|GÿP|4˜ØŒµW¢;�DRÒ;àPZUï™b%ÿ§®dDÜ6‚&/v/—{”A­Ó`¹¥9wÃ~õåÛT2½+C5øÚäÂ\&l©ªÈضK?¢É2Ö«áUZj¦h¿k¹vE�7�˜%/¦™·ï9ižaT¦Ï)Ìt öæûUSO†d‚ÈGŠ)S¬Af#Î|hr DyˆåßÇ¥×ñ“Ž­ÆÔÎòº Ê´hµ%^FZòÑ;¾’~R�ïn¬\›�„ �-1%ñú­Öä?ªßéº@ÆK뱘yI_FKZ� ÏI3îh&RÙŸ×ÞZèùMÍìOÅßþ1r”hÖï:T‚öô>»ƒ–Påãs,fÍè ðd¦P …µQ$V 8MeúÚí8iØßH;µë¥^ïF芪!DŠ$Zô†g ®®е×F¼ÄzGz¦:¹óUaÅ>w¾ºó•aXg0Xìï|ÕDqç«Më׿ºþÕîÚî½×¿z<¨Í”;?³'çæúW rý«ë_qêyÞ7lÎl»þÕ‚¿мNH»>¡_üæþÕë¿þëï¸ïX‚´$‚�¶$�Æ©Ðú½Ï ´«ßuÂ…ÙÐÏ*–2•{þ’Nè�1»TœÉ¶ÎÛÏ&tÓ@Ð" 6á9òË/€Øebœ”DUUïïˆ-Fáõ6¶Ê§‰b¿fX¡|µñ–ÊB?ˆ•\}*C!�¢�S±�lHžLŹ}F’çR,„B\ÆAŽ`‰É$¼Ê7t±e)*r!å6^3féŒÆ¢c’ �Í—ïÞŸªå ¼=M{fèI°¼ Ú"Pš³ìM®CØm õÏnxl¯ƒ©ö|º³\RfK¼ƒì¡Áþ  ;£tEueˉ•®-jN~Wäîè·f5‘˜­üWŒ…¢€¬¼ŒßrH3²½üK¾Ú &É¡‘ÐÓ-Zköi‡8ˆ-ƒc´±�š IÇ2¨€úM‰04ĵ2 °Î£ ‡§ýÝÓVã0J‰½Þ˜èkúiœ ؇JŽé°ý ©Äá|=?\]™]ʆŒ�D£ ¤^1;…Ÿ)*ôDk•ìT,–ÂÑ«SKuÎVnhTµY>2:^ÿ*#Ì=r;Í.,sý«ë_m€\ÿŠ·Ùp0B¸²×¿Êü~.;_ýúWasf¶¨Üxp£d=ãÆƒõ‰_›¿zýÝ¿þßO’¡Þc²Ù¾qµ“˜TÈnÎít±¶ ¿`Ùt,ÔëæmÕWþÝ«~qsÕ—Ö/É´EÁP•‚B8:æ³깃Ö/á´¤Õ‰ªÁFn Ø ´ÛNÜÅûKˆÉ%;p÷'íÊŒÿ· %3FCLJEz�„LÛv§™jÍ3~Œ@ùìq(‡z`>¨l©ƒ­ùCt–©v¹¤jýÅ�]ø¸Ž.=³Ëð'»ª9"†Ã\%J(bÞ{R"ËÍàLŠI3PgÒò}¸² ¶‘ˆ¨ œò³ý©Rјâ k%~MH$¾„H4z¥™dælVá!DÆ}B°«Ü Št�áþ­¸-ê �P¬í\²ŒTŠ#ÔiwÁ”:I}Ð;¼E^1áÆ�$jãÃŒÝB£çìˆØIWçGyA>ßD“öU¥:ìÒµ»ŒWFtçY×”B‡# ]ÎD–{U¼":èú¹+bÞ•³Š"ÔûÙ.O#G¼‚`§S" mêµÛ1 „A˜¡¾,Wû˜áq‚¥k @�ìîºÅ˜ê}Yx™Íq#лAOzâØö‘;­³â-‹h*K¡‰23„r”:ò©¶0/*Tn¨T›Ä3@|mzùõµ)«,êKNz‘ž*tž½î9²¹óUvÙ0™ù²ÑcYös•Í2äz‹!¨+Üùjã2ûdšÓÓfªíî|µáv¦ª÷ÜùêÎW<ƒë_¹)çFzý+ÓWäúW׿26n{BSa_ £G¸ÐXóÍ'_9jN üã‘�‰€ãœç? Ã{}Öšƒa—¼ì]oÑ…eñe£IØý¢’š3ˆ÷Þ ç©Óv¨Æ;XgãÑnöá²Æ/â+O{7fÆMž$/C”úô=˜Q�¨%Ü·Iãec¥è²bÊ�iô£# LJ€o{ì@_=/ª�$³HLP#!’p|»ŒQrt¯>ýl¼>PS™Žqd õuãÚ9C£¨ºyhN»ôˆ®VÑo¨±KBõHïCn¼'°£Ó*8âHKò¾ØR ‰©{v˜×‚Õ’­�É Gr̰ aÕ%Åg÷U9nФ‡ÛÈU®¸´ë°„‰xN˜j>­®CEz:'HŸ¸—Úé Õ'‰9mdçã6¾Ä‰ß^ZG°:%Ù:fë5s¬ï|ÅBÙáÎWw¾2tŒ}C¶S#óÎWMPÈ�¯Ü_ë׿êNóÜ[®ÕM×ÍãúWÁ¼É·ºþÕõ¯n<˜Gα¿ñà��#™CyãÁß_þê�ä�!'IÐ:!þÜVqñîÒ½Ó©œTþýK•4Îë2A‹ú“ J—yy?ùÑOþþgÿ³/þþý»w‹©*W»fµ[CåÎß6šÕµÚО¹ÌÂIBÕòj��ÿÄYÁ`=%Õ ¾UËvE³”V'�E⤠V/' >ʨä–Wì²BR�,Ö%'0ëSBPª’i FÉ@x�¤ñW2q°æ°# B')¿3VzgS>)–,;4¢3&R6Ƀ×ÑxÒT[EɈ“ ê2 ß¿•åRÄœ'™Gx´¢l5ãd1,/ã|a¯ñžEUa'ƒ­Ã@®n0'ʱ·9:m¢ó°ÖÎF;-;›vìž^Ò—NÖÑÀ!ÏØtú.r$Kj.Ùœ4!îÆibxN3“’ɺ¨Š”È“ª‹uPD¨0݉´Ì±¢Ö†ß[Þ¾l¹æDÕølñ¾P­ÉY5�² ©úóÈf|t Q÷p¾Æ’ˆqŽªÍéIG1 -m—1 ý‘ÿÔŽm¬2VÝQ”Yª1§p‹A��·ñIC+ÿ"=ì'I;›k¶å‡V!2‘¡O¨<˜d[¨¨3ƒ6f¨×“+îß:D‹‡–ŽmÚ²�bŠoLé>Ú0@S=BqXB6ð^ØH×¾eÜ:1肘b×…žùÊêÈåcÜŒœ}Æ�¹+‰ýû·›Qð§-b+›y¨R²±¨ b*Ñî|uç«õšõÆ:é�¯²Ä�¯î|uý«ë_549^ÿêúW¼úë_Ýx0ן½ÀÂÜÐ&ž(Pâaßx0›,ŒË(3‹0Èé�Ek¿ûxðÄŠ_ûêW¿öµ¯~þùçY:³çÄÿªüÕ F›Íl�´-*l÷*ŒÎRLZ¯n¾y¥šW„P§kÛ"M‘g Û�Úëïþßi¸„°±V‡v TåH€Ú#‘ž„�!Öš0¢þø'?þþîï~ô㟼ýâmÌ ã?T At%®�Öûì*NªÈe€”‹Ï£PÀãù¹¶!^µ¨=‚îÌø¦õ¨"8tªuF}Eˆ²°¼À �,S�"“ña7Ø©‡°öË. 3ø £V+±ÓTÊ9À„”#懆…Š¥ F%8H åбÖDûóW å_–ÏxR$¢Cƒ Áî¿ •‹)*s�?}£¦‡Â=r‘±L¹úΗ_X.²qÔÁtVMû~?Ó T>¬8V ü_¿ÿ_ô£.u"j›«ª1ÕYzcsR�–�™3n…JûÎ¥&€çÌH•sáÙÒñ,ߥ°â`NÞôq5RD¢œ#3‘C¨o?ýäÍÞ6Á¢`*­:�§†çúê0"vŒh�†•¶Yr£ œÖ£VeÖØD€…iBú– 4QL.6øÄ:±–8ðôªš¶,>-”µEj´#7¦jT>Mq8¥3-U9¦øž6—@‰°çb8l'VJŸµCéÎNñÚ|²pHr‰éÃÉaŒ—– dÒ¼¬$”϶!úéΚ »ì¢´9ŸÞIó¾Îl–>쫌q‚Xo8D–�áiò¤¦ˆRYrÚ·0SÿYÝ">ù)f°²¯dR�Sóq2Iš<*bÍT‰¸}‡àd¨ü¹½Ô¸:•ÌÛ™J=©A'"iNýH²$ØóɨûS|¶õ±Ç%Ù_tô~¦‰äiNÒ¤JF‰0¶=ù:µ[𖦕‚:ZÛ´_o\O­Ê­8kŸ†�¥rÀÁ#ʳ롄´NFÂ÷öFv�ÿÇ—Qêo®µôì±VÂ\”±ÔøNB25Bíkâ©«Âk¢ÔZÄÍÐk’h¾b-´ëÊØÅáôóÍW/ý°Ä6æÄÌò�¯2Bvaõ;_­ ™§ëHw¾2Œò&ƒ;_Ýùª‰Â¼|ý+c"K0F·³ë_]ÿêúW׿ºñ`nõqõo<(Îxlá–yãAÚ®ß{<øöÝÛ¿û»ïýìgòù¿üÓÿ_þª8§†™§/«p.;:}‰ � %ßû—Ïà-¹áØ@�n){Ñ‚xA–„/€ö2;œ¤.÷�]öŽzÎBQp%Ö: âý§Ÿ-–t½%’‘SÛÖU�?ø¿÷£ýxO£™e*?�ºa*UPá@]©í ŒAR„¾+ ¯,mè·%``ù÷=y¤‡H«Üã’Î hLJ](QÄ"þ~õÉêÊÏ”@q(bƒhígQN8:œÁx†J…³Ç×Eo¢¬×EQ£ªÛ0ÔÀg‘ÞØ¼´ïL“Ä)l£<Þ!"ѱæ_´³$Å ‚¤ JÃõo‡súRG€¾=Xªê±mOËm7C OK±Y[´f¨ÑUÄ�ò§d¡Ä0�É’‘Jöef"N¤$/Ob†MŸâÃCO\S ¡ªcæñ<4’ —R2[�—ía�69LMRªÔ ¢+|ep§N´è$�H¬êßYľ6|_µ RåI[gV’-kO+pïjøÈ3…SqŸÎU„O†.6�E«!Ä«žQÞz³‹£ÁÑÔ÷ÑéP/�¦á&5ÕaòVÅŠ‘:Oô†|H¡?YR!4�ä�ê3†.Ê?<¥Ö4Z†Ø@tÄ"`S@ó1hñ‰9ò¬¿«d&»3;†£¹¼éHo‡ñ»×æ³~¯t‹Æ8­q'óljag=´óÞáxÌú‹óUù÷´b)Ó.VRÁ$Ò* rç«;_é€:…~uç«;_ÝùjÓï™±}šõ~¸þÕ1†iã|¯Å¹þÕé׿âT^ÿ*ÌÛ¼ñàõ¯NØa~¸þÕÿ þÕø£bæoþ«?= ³ùª—­Š«k¤Î…Òrd‚ìòW‚î¡ Js‡¤$,ú4Ðôñ¾ê‡š^‚oÈÿpþ*.�õ¦€z™©$±þÈ›Î„Øøó3 [½Ù¥{^‡ÉIg(hëån?úÑO, ’µ||³I«v„¸p’'… b× û”¼XHmYÊÂvÅÖ&É™•�Ê,Ë^P¨¿!õ,"fã¡t)“ðº†Ã(®úé™ ¢ËœðýÔ¸M®¬¡Wò¿ÊÁcÔgÁžMú¡ 2@ªð©{xƒ ¯â : lEÇ@òqŠ}¨3 'ƒ:ר© ÌÃbŠ5ŽIå]JÉàQ�I?ùH4Ë4tQg"ظ�5Ī'ãd®F[û�P"wYí‘Eÿ€4“V¹APÔFñ¤Z,1¢}ñ#‹®ù‰�Fr5š{ùÔÎgQd×öGi<Ã%FúBŒ Ë蟘"X·;ë5ƒí e’Æúß ê'[Çõ€µDÔ#ª“�-ˆèb—¡GÝ9¾S—xÙ máA~hGGôÑJ… _$ˆ šz¬½- /[E§Ó"® ßýìAö´z€¢ �4I6€#*gíW zRö™!³Jl�•«Òáô¬á�Kek rTbš:âÒ"Bn ù`ƒ�s;üuŠ‘$8ûY¬×H7à+Õó™ça�¼sÆÇ?០„‘z櫪³Ð�üÅù*(5$§JXDÜ�¯Ø�IŽ]³ð)ºó•™Átpç+£ÏØ»óU£äÎWMÔͦfÛºÅõ¯º£pZLî\Ìrý«ÝQÜI®uý«†„èÕÖ-d‡Î÷¾ñàõ¯׿ú§�ò£å³Ï¾þõÏÙ^<˜CS4/p׿<_ÉÒ • ùæü¸ÅoáI¿�¿kÉß<µ_PÈg��غWöiõ‹à;'còœ#!ȃmЧßÿþš@Z°Vaz˜dͯ8BéÉš„8:ËXäÍ‹ÚU š}¶HÌEo©ÓÈT 2•6};,H�ÇhEµÀÞÅâÇ��  D$á÷3¬ñ’ðò¦,Z�²šÁ;9d±z‰ã1xL’7–‚#¹+ QÁCGê^Æg0ö(µ„aÊi*›®‘“w¾ ÄRFãµø†MªöG‡¸E÷å*ü“ó°'A0¢&;–=“T âåg*Çr$ìZØ!å`œ(k'wrz]Í<ø»4@œ’Of(N1o“<&›JéE�DZ°½”¯.Wa] ÒΤÕ0ÇlúãÔ¦€*u€ ö�'¬, y�ÁXýçŒ�áu¼ð!q–dÄt  ‹§co[>­3F�ˆÊ’l3¬�r$ÜR¹IŽ4ÍÁâz‹é‘ÝHÑxpc ºe\„œøð²V¥ÈÎ)_ß'ájЙ�LÃ[¸ãÔË€ûè‘t â…—ér” N³�îŽdˆoƒ{€†Íˆ�Ƥ’¾={�"–Ù¨ÓÒj³uÅ¡XX”Ñ#­Ý˜qËç,‹õ++S Z û'C”ðX,]�ZÓ¬kl¾ªˆwË<ÿÍ|EÝu�j·aŽý�¯˜æ±9“2~¦©MŒú§ÃÞùjÝYwe˜;_ÝùªArç+Sñ™Å¯å&åÞj2uÿs›½þ•þqý+}ãúWó-r)²wL×8îGô„�›º ‡äƃ¬sãÁžÑ"Þ¹ñàï>üÁ÷¿ÿµ¯}½ü•ÔÀ7ìæžó/<*�q˜n;󕑺élAýÚüU3œ{+Fd­Â‰ô‰%J©7Îþ¥Y…“U­jx‘™¬Ó™@úÓzܲºyàšO鸢8¸c@ÐBägºí½Ng¹ÝøÂ NØ×æ-_´ÄðVy%,iž¬YÀbúxß:ž“‰X9º¥),ü T4“~)Œ¦¾=ÉìV�S=¯i&‘…²Êäè×.I5×VSù–s‹�¡ªf]í±ƒR®J²û‡á@"š@HPŒ«š vñN‚‘ ŠŒ€í;—xPÕ)V³Ôè+&@L©3NxÀK”B¸2 B ÃG ÞUMr¤íHPíj_¾ðIÌS 2¸É*^ú­"LŽHkǘ%ÊLª«õñ·Æ%Aª±Øð`�j�a€4{#õeÉTUÔ®mЦD`¬‡\)´ìˆW×5arÂÏðçt{f¡}ÊñÈ%#PÖ[1Û?�£Äå¡Í.ºQ'�\†…R6Ð&< Z³Fªé>“|�hˆ’²ëa3ô*íCL DŒÖ¾—KÆ�UÇ‘—�ý˜åÌ8œº Ó™’œýKBJïuÊ‚   Ê�«ÒQv¢¡'W Ž­ãmß°ªÝ›€Ú4O€ÍXý )0Þûë—:G ˜¢=QŽ0iä´¼5Ý^Ââe¾šqÌz‡‘}4K’FáÎWÙƒ‰ï|U�Ð…õ¤Îê`I£ðÎW³Ê�¯î|UG¸þ•»Òõ¯rI®Å‹¸þÕõ¯¸PÜÎr®çX;\p/3Î�³C–ésãÁ?T<øöí»Ÿüäǯ^y¼oÁå/ƃÍãuÙ/„‡�—#®W=/ ¨7w^þJ¨~2 ç§\·�ÎŽð?Ï_Õì…Ô ³ÄÍ…ß‚Ïÿ„„j;Xh-‹@dO”Õ‹H>yõÅÏÞÜØŸÈ84ÞÈöíÄ.ûFC ›ÃAÞiav¦#)Ëê=£èº{z¯"t2°l‚f5ò5À_$Žÿ.'÷4\è kö|àOÊ$;Y‰5mC›š›ˆ\ÅÒ9áBj”Ä11²ÃÄJ°H–@x„/Ä/©0›2˜wc�`/Ò;&d¢#JõcÚ^¸/IC¡(ãëUpGÌò R“Î5qû.”�«Ë“ë@¹ò¥3Êò�–Å#\›ìG&|r0Š T„àŸÖéx±…â'ê)x>yŽÖàoµ>‹¦ÛÊž”ŒB˜‰~Μ¸5Cã7+`[âj:™‹§p†JÔÓª¤™;B_÷$€Yj/«[æˆ+Ô)ÕÊ"�˜ù†óY·Nú´¡×,(í¦õd…È¢¡j–N'hØ |޼Ån4´D0µ`L¤¨ŽðDg –R’ª;N;}ƒ&öÕiÍÐ¥¡à듟 ™žõmsqëtÀÉ4±’¤’šé¹î4†8éÛOvdPNU­„ù$¯õ[i2Ë-ç4Dƒyë_LJÀ å§ KÌc/GM§U&,ÎU=b‡×V�2²�ûÓÌ3cMPÿ¯ä*#6âiemè©qˆÆ¯œ¯Ôjöl�Ìzvç �=yœ "¾L޸̎d˜È/ØYâÎWk„;_Ýùjcß@ihmwç«;_íÆnBvØÍa·žþÛ4ì6pý+7wŸu–y”çækøS׿Ú=;+¹W»÷rß®ÕÃLg`Z׿*Ìh.ÑUn<˜Œ“æc…Ó<Ë8¹ñà�›0 ”º—4aüªxðgÿE«ÏN,ùËñ`—æÙtËþµñàë€<7ƒÏ/nÊ_5]™¯âþËóU¹ Û aCüîw¾3\-<¸5Ï©ðõlt(1Ñ‚™–ßE¶¯{Úô¿¶Hgy'�Ia„å/r^ö(B!õ¤»Ñ �]/ŒÑۉî”ob�O¶8IÔ¤¨W†`�Mnlt$%|sþ¶„I¼�Rœ ¿�nÃ#l?ûš‰ƒ‡U]…'cÑYð«"^Ûè{UVÜ�©Ò}"5ν6#âÔ|ÔÑîR�¨rZ*ƒœîkóp¬6cø ‘áeK°VÚYÅ¥Fp«Õ½H„üóù{Çz„‘¦[ýIX¢9^G�™ƒÁƈM¦Ûô{þŸ3ò«×ån4þ¡.‰3!Ai‚,ùiÛiñøLUœ±s‚”A¤8 ²Ã”HÚ¶ƒ"é³à´á:V�k©c °Ìïâ´ïHá Z€1$lk�ze½žv¸W?ÝlŒ\8?p§'Or"Vp’¢yèI¡¬¸j<õ.¬™-„ÓÔ«8õ£@ÎÊ:ÇV‡ åv‘žA ŠôqtˆÈ;Fž*í©Ñ�œ�Wó]$×P•‡ö4,¨ÜÌ1xä8Ô“p‚Êv�½þ…ùjõG²ª~y¾ ´ŸIÒÙÇQ³ºóÕé — ³�ñès…•¡î|¥¯3Næê{ç« sýåÎW›šž™åÔ¸Bó×�¯,ͼnÍ)Îê3&w‘Í9UUwý«y¹¬Ò¨bS�óMÑæžë_P,tý«ë_�)¤IåÆƒ7<³æ3uÞxÐí£û-—äÆƒ‚èùf¹ßøÆç'ì�mîªy%9iÇQëJ`*ž¹Ÿˆ¨CY AÑ)ìzi†Ý�w'rÃæÓ¨ÏòMIh)!¦hZIut& fy‘.P‚}nûMk+¬l|áE¸§0�\B3þTÄ›À2|û;ÇCBÄ‚Ê>� €,¼û0@W�2*·DÜ++źì,ê�M§&]˜ã8©weÑÎÔo/¬?–>æ<�r™ÃPv5¨Ìð6J¬¿ñŽ[ÇÓçA õ³ -€íŸ,ñä°ÑqÂR†t©R®g‰¹úà°<ÏSó–'½(>TDFp†9í#çª!z ø,ð«aôîk¢%Y�Êòt·œ«ºNQ[B¶J¨V”硾ÄôŠ�iŽ»ìqK¯þ’+'ÄzÆ‹d�;e„,q0ÉwlÉÄÕ1c-<ÝÁkåq,ÕÏ:U½41�NŠŒ tÀ�Ó­>ä´S]±¢Ó³ ùN–R� FÇ„;Ÿä]Ë0HÔ ÷ 7Ѓ�z®-äu�‡Ë:@}0ôig{Ø1œ»´fŸc“ =öúPö¡[¸õwT¢Û§¯Þ™ úÙH¿ú䔼´Ä†ÏÃ笃ͪQ3ëÁL›žÒWV-•fÈ™xáÌèbeûee…±œ)Ò‡&]ÕVOgÑ.Ó‹®k6¬™«�È™Œì6üút¹^ñôÙüjôþõˆ²k¡Ol½ †Ëßb¾¢Õ>±$M&=BjJÝùj-d7CÙeè� í»1«ïë+Úa·…ÕÔÀw¾Ò�2ÓúëÎ×¹7 Üùêôª;_™*×M6 JYæô¯ÎdÕþÎW׿ÒKê1›™¯5C\ÿÊrý+þÉ�y¶[ƒ“K~ãÁî™7ÜyãÁпzÿ®LRCÆÇªŒ|¬ò¿6œ¿V¸Êm›ç6ùo«S»MÉݨ­oŠ0°³Ô©Pêõû4_½FÏnù”�À¡ò5Úb­27â/Û,VÙDGJða ¥c‹çEØ<„ãIbªl?ÑÉ(Dl š—çQ�›hA�Úd¯¤å]Š8"­¶Ã¿�ñ¨#¨�NI’4�Þ€L9­2¶¬M¨®lÚ•i@iu#F�J›ðÌô†»¢ S\CÞ(-Ëâ”È�Vþõ¾*t_•¼ŠžÉVm«r¨3k�æ!×xÍ<‡yTÆ$´0Æ·=’úGÍ�¤ë�†‡ÖÇnøÿ·óU•¶ c(ï|•ï|¥ûÜùj#ûÎWM#gÎqrç+sõõ¯v¿Û�Ã�éÿcïM€$éªòý®®þÜý¹ï 3↠‚ ˆ¢¢‚† bˆ"ahh@„†�n(ˆ�†KhAˆŠ(ྠn¸¢ *î+¸ïŠŠþ]¾îêÿóœS}¿œªêšêî™ï›ÞœžªÌ›÷ž{òͬ¬¼O�{ož¯ÐÃïã<_åùªZ=y¾J{0íAîˆÝʨæˆ÷H|°äñ2íA$ª…v#=_Ù’¼¢=HsÙÖ!Œ€sh£Ûfå•üªž í�ÇIå ±p‡æåo›õ ü ôæ£f9Ðߦ^>'\Æú»ñ]ma˜ms!¡$†¶,ðÇf]f•Uoh�×w3èÝv²´Èø(¥ŒuËÞ:Š˜Ù?2ØöÖ}ö¹âÅký.ž8Ž #•UŸ]÷Ý,:ÑŒ€¬]¶€­ëÊQæq ì`^kµaEhÈ!"ú™Ûš°|Ý4Ô@IDATÙ5³é‚WTäGÈþU‰n<꟫R¬U1Õ”žPÐØ;vEòèÕ¡ü6Ceäºf�Š.�—šÀQ²á�h� ؈IèØM~�G¦yʺUv+jŒ � ´ê.ß5.û�%š\¦XóJì ¢RIÙ™]Š0ê�°„T³´×œÉö”DÏ欉—z³Ëa‘&Íã^›k/tÙÒkþ åêš.�(R–úeÉnê„SK)Z•XUÖÒ ~B@—ÇÑÇŽIjÓÁÊMùR€Ã$OÄ;*�ðâÉeS¹ª~& ]{]¥ ’øëm_K r«—W~q}¶>´ÔX— Ü� åõ0<‰U“g kdòÝ£q�'º\Õž®ÀÎýU!ã;¢U&œ*gI×g§?hŽÒÈ÷�ºpõpÌUi}ŒíÙÍȬeÈýuiùQ-T£üY»_µwVOíÓe¥«OI‘EMò±Êýª.jµñª-Á8-k� dÊý*÷«þ´ç~å)÷+¾ ¼­zW­¯%îÂ~måùÊïÂ<_åùÊçŽ<_åùʧ+Çó|e“"íÁ´½3òìÀµp£µg‹Û«=HÚj1ÚXn~5[’Bû“3è¸ÿ�ܦûüy4¤3?¢DÝÂ>+¿:°^m\Å¡z Í\:{!WQj©>tUN[öN`ãÛ]ˇ1}ç5{IÆ'M²ÓÈ,�Ç»‘�.âv¹T‹ñï$ö¨‹jÔÿM(SÞÙB¤t—¯ÝU#e›­¸»„äÂBÅmGɯª^©�MŠs –_úlû³v’ ·d©[M ÄmÈEée£ÕÒåA*—,bÉ¥Í#Y"…+3Ý�½�tÄáñI0…—ÚÝzkÑø€7�”!ŠdGôa¶ŒŠÈ/ ¦#(#â95óˆ ?ŠáÐÙÛgœ`L“ƺ>xšêRd•*õøÄWö÷��Î…áºeØ�„¥¥ëÃ}þ#vÉ­Mݨ¢*T"Z éìkEp’Lœ!½Ó®‹Ú£*ÞµËúÙ}òC‚>pV`"/lº¿ò+-ù«$J¸—•�QÍ‘ÑqôËÓ:מê¥N&÷‰§ð,bNQ=Q¾º¿j“-™YÀ²–Ô¦§©N@eõ¢µ0†J9¹ÉOví{˜u:`™ò[¢*ïÉ�UñëRMîf呪Ÿøe¬{¨˜pÊðƒWM¶«’䪠ŽÇóÄ_-D”±î�À4½Ã™r–V­J¦ò�óá…E:ª’ËÆ�Z 絪ôúd ؉ôuL®ºí§±ä­�eíÕ?fõ©Ð•ÊÀJ{ªd¥¦:5\—‰¦ü‡”ç¥îWìå`ÕÍôv+ç¿ÛK_­�ôܯJ½Ü¯¸”½0s¿òÖ�ûUîW~7x»¬oÚ<_åùʯÒ<_åùªzÁäùÊ�ƒÏ õ@ÙÏ·iò žö —EÚƒ7\{І¹Ï¶Úƒ´«ùM‹‘ò²ýºÊ¯ª±k3Ò6øøU‘3‚nlÏêAÝ26Êæ¯¨ÉG,o%ì" » 40q_/–-n ÕÞ5aÙ´õ;~Þ‹º¹[÷çjOc¯šÊ>ËÕR‚-š;ä²;˜ÿh�ïÓ‰Ìò,úÑ\FuÝÊÌU»åB¬D³^° ¥ð Ê…*¬óRjÉ™Úi �¬Eþp‚T+�{‘аHÑ3�¸sº«­Õi²0Є|Zj ßœ>iÄ8Ôe1²õˆÝ*S'Qíª Þ ŒÂ‘e5dÒ§Š½ó¨KX+u¸GkÔÁa–\hàÁ·9ׄsšfÑö’µrdñ¨´`õ�¯uøx@ñò Œ�»:Ä?/ËåÐv•‡q²(Båof¢QíU…Tª¶U¬΄µêÕ†å{j�\u.ܦrã¹È&o䉄Œ }ä/.íõ0þ]é§E<ñX=ä: ¯n²£±d½eÎó� \lI»vsÖ‘ë°‹×z¯ª(ÅÙKÖ>)½UGW†Ù_7ó”BôO¶†®½ÏGý4@>z -Oi冮–¨Çš\qßù�õÛ¿²²tV×´^¥xá Ùoíu ªì;—N©h¯[V½º3¼¶=,Œy [�‡ËKîS@²î(õz*—kå8y,¯¥ Žë:QZtúZþ0«5=ë#ªAû–Ÿjg—ÊtV*gµîWœ™+ïWVSÇêk!$}2¿/å½Y8k5Ád÷YR�¯œðTÔ¡s¶x÷ NŽ—Ü¥²ä�å~…z¹_� ™+…«ˆ #÷+¯‹Ü¯¼½ä~Uß Üæó|åå�ç«þúõV™ç«<_åùЇ_9} æá3ª¥lÕ[ž¯ò|Å½ÒæWG]iž»=XOd~´”“O}3G{°ºõÕÏ!ÎÛFé¦ö`�2lhúQ¦Ü|•_Ñþ¥¿ñ)”=Ð 6�§s &'ÓÔ˜Mv[æœ_à�`ËÚOñ$–‘þâ\~ur9…×RüæÕR½, ‹Â[¹ÜkŠ[qüféa•ëªþ“„GÖÃ?ìZ%öÙ]�w2©Z¿UÞr»]¢¡]b5KÉÉØXøã'XÁÛ›u¬ëXÒc¬ã®-jMu‚"a‡¥GI2»´Eö± O± ·êãAEæ*¿©¨Ž”Úñ ;uæÉ‘ÕYf 'Jj°/A¦ÙQ�˽�Õy6�£ WL™,a92)œ”ÃXÖ‹ ¨‡^-�µ\µ>üñ$[²èûý×¾iNC¤ deåE'LÖxBa]-;(/Žc7WB{¬ôU¦\Åû"‚ÕÓSã>/™Ò§Ü° ]Ó­ò­k >¯×ŠúÔ!óâs…aQ‚ð¿ê£ëš伖‡†""„.R•ož¢Þt­$ॎH‰HÁ,åk�ìœ\�U�¨ØV© +ä’•RÈ}篫¶�F hó¬V�¾ '[í YQŒÂ‚7¬YÀê¹jIÆÉž2þìÆ­oµi­MŸ¬Å:«êò•.uÌyRµ��”’”jXu‹ML– öbz!LöîÍ�gD¼./“±jÿVu#y„-³Rn ½:"+è}õ óZ¡Hs93óŸÈÝî”y-ù‰ò¼•*u�hªïW~ØVîW”'÷¸_µ]|BŠ>nS¼¨+cîW¹_å~•ûUîWÜ{—Ï)ÜÂ}lðŽ]_N¾qËô>»|(aWž¯úÛ²¾Åò|å—jž¯üȸäù*ÏWý›ö ŸÛn>t§=è­A1lÞ¥=h[ñ.iʯ€'Î×É�êíA?Úʯ8Å<3Øî´­ŒUÖæ¾>TèºÍaÚÌØÀg×1Éá’ÃR=ê– v‘S-VYßsuT! éÊgó%½,Ox¬ÔŒ¦=Ù"”^Ù9ÿú�QÉY0@¾>�Q œòµ{6¯ÛˆËj”£²•?–6¡ÜcÒ™C!àÒ0§Y›LhP*Z™5qOrxì*.A°v2ò.;0GERÔyh¿È+>ðHp¢�UFÏÿ±¦í^ª6˜UA`ÅÓWþƒ0¯9-€ûúD\Rsý£ÞB8¼x©IÕã’ÍÂZ\.ž»�7‰e{Yux½x1ÅÂ9™­.Ô¥�>Už=+ôÖ²,xiýJ�árž5µ®LC'KD¡Wå0³«l¨y'zX^jÈUZ{íY3 ‰оç…ÚJm4QbµÛVë¤"ã_}Õ˜�¼Ø4Y‚äÁY‡åtÔcèO/š+wôÍÿ¾™RŠSÄJÕ‰D®Œ¾^ê„P¸®L!Ç ú³³•«l×vRé‰Zå¿ê�K jœ’~ŠpG娒wŧÞ,`†sl”ËÔr²Ô81;êΠq‹è—2Z¦­ÆbÔ Çn®pŒj�<•�Mµê:Xó¼ùÚvHÆAOXe¶{ê¸=\�Áýuجæ~•û•W£—[½ÖÇK/¹_y[ÈýŠ+$÷+>ÞY}é{.÷RoÃ|nLÍóªäù*ÏWy¾Êó¿§=è÷B=ÍçùŠODÚƒýAcîÆlò 3Úƒ·Ïá"ÕT¤íÉéã¿—1�<<óØ4õ‘Ðq•ª�“mÙÅlÞ³ˆÒ@õ‘Ù6ëQ ÊÅSdÁQÇlåÚð×­QRm˜ÀBŒM-`Ì‚l<[9Ñ%eH$‰ /3Q³�mÇÓÅö’Æ >±½ÍºykaÝ5k韾´Óò¢@ˆQ D�(¢@ˆQ D�(ΡÀÛ¾ÍÝáW ¯íü jú:áW„xœð«Ûæà­%¿â§”ö`¯*Æ úÆÎòÇiãQöé2ÊómnåÄÀÔäKì?à‡Jw,ÃBH§nažèMöÆ>ÃOø“t’ðH~Æš¹ Üý3+Õ’?­¿z••³Ã¡Æ,kdŽFºê“Cao–(¢@ˆQ D�(¢@ˆQ D�(p.ècÕüŠ€3cÁØ(~µ ŒL–œO‰ÍîàW’]Á¯ŠVmæW„½¹£F»7 8VÙ1¹%kÝùËsv÷Z ‡eŽ ð~ŸÍoÀÆ oåñ ÇðÔAÅ„qv^“ëQøvéIX¯,+_Ã`›¥Ü$­¦YÇ»m2>›<�?=è6Ûýïß²D�(¢@ˆQ D�(¢@ˆQ D��xþóŸ¿ÌÛüª@Xc7ú|B§€P2¨u~ÕLl ¿‚d,ù½É�²…!ŠÛÜU0lï�qÒ@fËÁÚöôÁl=Xš>ÌöÓŸ¤ä<>>`´v 1ö!¡v":ÇX––¡vxËqÇLJLN {…úØIÆ#Ò\õ' d't&ÈqôÐR½û'þ-Ê[ˆQ D�(¢@ˆQ D�(¢@8»€° ¿bDïæW`®šƒWò+ •äL„µ¿"�œ¡QÄ ÞjñÝQØ*Mâv’jÏÓ"tMטCµÂÒH«©Ü +Þ¶ ŒÓKÛ¤…3»³Ö–#½iØ~®å6©¢? x åMyFA«Å�OU´–% D�(¢@ˆQ D�(¢@ˆQ œ[� 1E¹\Á¯ b'üJåüʱÑTô75í„_�«ÜÑüjAWRv-ù• ‹rËe6;,ÒUƵOwR‘Wå"@H§V¨Ä©í º¬I.Ø.0D›�c™‡AÀæ„ Ìykµøa›n‰ôغaûâ¼òØLeS¯L3ðÎp»ŠŽ«}Ë¡ïNœÎ{ˆQ D�(¢@ˆQ D�(¢@8�Wò+�×:¿j(u¿‚{Mùý6§üªGklAÍ\*àm”»c2ÈZ‡šÍA^’6lŒ�06(TÉf�ÒÚtå@€F`[Ã:g8µ;ª3¬ÂÜj82é‹5@öØU&è¨Zq28ÿ¹8±*9Šø™�äÃÅŒJt¹�·ò,Q D�(¢@ˆQ D�(¢@ˆQà| )æ”'üª˜ÓîüêxïÐ.žò,"ߊ_ݾػm¯@ev…tÁÊf²>x=Z�Ñ´Ó6q#Ý)\x÷Ÿ�M‡OÇ Ç`pÄ»IâH'ÚM¸Æ& þÆ\«C áFzÎrXŒ G‡U %ÉÅŠÎMY£Ð-Á_UÄa“’'Ž�7KˆQ D�(¢@ˆQ D�(¢@8‡'ü ˆ%™²�çù±hôÿ�I²àVGÇL›`Ô™½GµÈ.i»*ˆMûVÑ�FAg‚.cÔº^èŽìÃÀ(Q³’ê]\-åt¬¸Cèf·S6Šå99ªlľYÞ‰ oü&ú+iÈjµeª|²ær‹q] 8£ *1{çP3E¢@ˆQ D�(¢@ˆQ D�(¢ÀDP[çW�(#È6ð+É„^~U1hŽ©VüÊaܪ°áeLa*RÖdËAܘëTnv’Ã4wÂÀ “Éâ``ŽÀÖ�BÙ¯ |<$¸®ÂÜän¤Hõ–ämò;"r¯h›»äsþ îÚAVìN{âÌ­FŒ# yu¨‡«sͫ唎e‰Q D�(¢@ˆQ D�(¢@ˆçQªUŒjÙ ô„_‰¢ D«}Wð«pLεÄVò+(•£ÁmâW`6ûuþ†A ÑÏ“•æZËþ©ç,¢;MqVÄŽŸoZ·*àÕcrÂê44#ÔŽt†hÃ…e…Ø×=M—ª,c$�Í*¨ØV§3 ®V°½ �«y‰Q D�(¢@ˆQ D�(¢@ˆgV`¿’sº6ð+ÍÓ?Td6áW°îàWÌ|°äW‡Çó#'A¨x2Ù\Q:‹i7;’€-f‡&øW(ÌH6è\sÆr“åŒ'VÛ_h—u×2TŒ›&ÈN’ûÉa-ÂE­d‰Q D�(¢@ˆQ D�(¢@ˆR`�_Á½NáWâ¸ýƒ«ð«Âbůd^06»Žâ¢}8á]°2ÜѾ=O¡mL<*-« ò8ñœc­ÉÞH´ +24«½™µàœpN”À.ÒŠ•Y+û·õM3µi™H=-4WŒÍ}þ+PÇ~öJæ:ü.ì­äÈKˆQ D�(¢@ˆQ D�(¢Ày¨œ‹Sø3�V'LÑÖ:¿ZO;�_¹Ëè´ú�{ÔÔ˜ËΞ̈°O(ZÅ´5L+ÒU„�œEþî Ä6?^¯¦Eá˜É@¶O¼ÜÎJØ«ÃÉШÉÿ±‰Û¤v¥ »zE°×‹kÖÅGGy˜ÜnZÌòY¢@ˆQ D�(¢@ˆQ D�(¢À(~ÔZãWÚVÉ¡6ó+w˜Ça×ÌF`ÚL(¶8>Ä*S”¿òEÆÜ£{G€0f[ …ŠÕW#ƒ`NÀfeDâuQè3=”Ž.GÔÌæû˜"hÎÜu4¹âjò3öi�躂…mxWÁqµE±å¨r’¿c'�À•JÃ5 öVõ·y�Q D�(¢@ˆQ D�(¢@ˆçPœµ�_9òÙ:¿".¬ÂÔÜ5åWÌ;*¿šÃ¯C~Í›_ ¸4¾ÄYîkÆÆ�k†—Ys¹Ê¥ó$ëÖÈÏþ¤¬�Çdoeçàèxq ³ã*&Àx5t›ÅŒªÛ«š÷!€t)õœH�d‚çöÁr‹yÁ:Á(nï°×È‚v-˜ž[:KˆQ D�(¢@ˆQ D�(¢@¸ˆFŒMù¶ö…XküÊà²}f5ØÀ¯`\ö=…_aL¨ESiÎ7ÆhƒÄÁÎ N“›9CÍ; 4s1MàÖ˜¯¢Ð¨~®)cÒª°ÇÍPqÕo”s¬Óg$§-;Ž2û©”Í�7·X±xu8ÕLŽÿº±DŠ–®-’³D�(¢@ˆQ D�(¢@ˆQ D� ) º‚_av¿*šEö3ð+³¶Z5®"ËÄ}B.ª–ó1 ›˜MÎvd�UV©…?Öäbð5Pàl�¬04é¹ !¬#'W+c•Ù!Üì+l£b;ÁÎÄ3vN¶g œV�çFñé]U]õi«ö˜-KˆQ D�(¢@ˆQ D�(¢@¸€;ñ«ÂTÀ«;øUQ±ãwÐ�s�_1´fP4G‘MZ³G*»kä7¢Ö*ë�qnÅåök…Ò4œƒï1ÉÂñÂçHÀïy»#¸Îß\ {è˜9Æ“ÓE�ZŠƒvkŸ!åxÏ¢@ˆQ D�(¢@ˆQ D�(.¢@wMùUE}]ɯDU,r/ø•`‹i¿¢(üŠQØ®àW„§9¶o”TË)BîU ¬i�d»ŽbŽÌU¸Œ¹B^~F¢³¢²Ûš¥i5¡¾íÑYÔÞŠÂuØîán9Ü‚Êé6TÝH¡jå»vô”üŽõV›åØÖ�Ѝ³7ëETMÙ(¢@ˆQ D�(¢@ˆQ D�(P Êt¿’„IÞVø•œŒ04ùqa+üª‘•j²ã„_1C´‘äðn,ŒÏvûÞì6³ÔŒ Gô�Y—/ƺïÝF,”®‹X†�£ù11p³wjÁ»âb¸Xv�€3NŽB2:©Ю§~0CíÔÚ²k)5 ®ŽœÃ2*¯ü©äLpZÊå% D�(¢@ˆQ D�(¢@ˆQà ÈÒX˜È`¿bUðXåèi•ù'üŠ<0+w,ˆrsÒRs€Ï Y³¹Ái]F W ³æ#"Ž„p�â:2cúTâÜ,Ïîë°5˜ßþb± ’®™Tïð�2l;ΜS›2Ž Ý©”Œ°:òâ<ôbe�¼˜ ÖÔ‹ý[qÎlËÉšéµ›åC^¢@ˆQ D�(¢@ˆQ D�(¢Àù‹]Á¯ÀW5Äad˘µ%¿2Nì~ac¸Š^šŒÉÅÓà[Õc”­ú;ÐÂÞCÄQ/¼®Êv¦ÍCÚŽ™ŒÔòeƒ¸9Ø�Ž@ÆHg¬·Ú¨c¾SM±TÀœîaç }²µŽ„ƒV@]ûp¼Ç0t€ÆFn@9ÍüÆ�Z³oR?Mg‰Q D�(¢@ˆQ D�(¢@ˆçV 0¥Oø• Ê`³ üа4y•ycÛùŒ­ù•óŽZFIcÔìÌj'T¹—ñmźŠsQ±{ȶq¤Z;NyÊ E쩊s$¼ ¡l4 —÷<{¸–QܧæKe’+¸†)Gç×…Åa¶K.q">5ç, †óa² ß¹eMÁ(¢@ˆQ D�(¢@ˆQ D�(àÔ£§ò+ä1¾¬øÄK~ÅØkgãWÆ¢iFr··8\ÐETîåtÕMÕ±ãŒjƒ�Ù/”EX&�;Ùñw;r5údm�!³Íar¾³t]wu-ÒF¬œx*ÚgFƒñè�ZìŽ"ÝÖÕRœ^·¢A5'¯ ç% D�(¢@ˆQ D�(¢@ˆQ œWñÔ²ì:¿’± ¸&üêàŒü ë2µ†dr3×rKØÕŒË<7G^›;Bœè Ç;˜»FâN÷æGX!BM|g>,÷ëÇG༢gà=&lpµbë áYswRµ¶žˆ¡‚æ@ûÈ•»º™¨7EÊ¢@ˆQ D�(¢@ˆQ D�(p~˜î`•_-:ø  “b¶‚ÍüÊî›üEVã¦Á¿6ò«‚XR1CÚÀ`FÐaÓ²szxúÞÁk3·–!g®ìÏ»—iA8ò;í*NkRˆŠ¡Ã1þè¤ZÎuÍóÊ&±Àª•§(I„·ÍIð—I`¿‹pÐ6Áà\Í¢@ˆQ D�(¢@ˆQ D�(ί€è Vt’��ð«åDj+¿">íjüÊÙNkBÃ×ìZJ‚#ªUMõ¾pt6:~Z™ïÙÙU¬æÈk2°¦iîcª2�Çš9ì#j/RI‰ŽRGäœzÔª¢ v]e‘»±¶Çܪf¸#O¨”®H�R4°ÓP–(¢@ˆQ D�(¢@ˆQ D�(p>$\Wð«ža´˜Ü”MIÀ¶ó+{‚.©UÇžIÓ$]r4�ƒ©YF€›sô´ ý}c\7IÛÞþ?÷™A�Ê¿œYi¦ë©¯PVjø72•íÊYÃÄnv¸¿Çt¨!É>§ÕeU?d¼ æà¸kŒŸ¥©’ʶ«”Ë¢@ˆQ D�(¢@ˆQ D�(. ÀàWÚ¸‚_$Vs0œð+qqcE®˜ma‰ªè=z¯\ÝÀ¯ª€F,ox[½À¿„\2/â¤Aá€gÕƒUƦO=÷�€ŒìŒõFÇ»àP”¦!¹š%f ñZÁ5«%V5î´N ÇFÁ½²Œ*,cõ‚CdÈ¢@ˆQ D�(¢@ˆQ D�(. @Ã0ÉØ•üªÂÿ|-ù•Ý;�;Xì-ж±´rh¶u~eä™�oð-™d–¾‰¿ –‘Tkì!‹cµQøm™èVáq§õFášµ-ÂØ¬»èùÉ!\kØæXqöm%z®kç$‡¤˜èµCUoù íãŸ.{€Y¢@ˆQ D�(¢@ˆQ D�(¢Àh�¯ £ïç:¿¹5¿š~E&Ç^ëe…_ÑÝSÒV4«æ/�¼�»Hn3‚°#Qœ½@‹Ô9Éi‘4¨ÿ(ël§$ò& Í´& #Á�ßpÕñÝœ¡’¤uÕƒ;EÓ¨K¯1B]õfm´§WøcoÕ.¼ë¨8S³D�(ðÊ ÀüÇÜ2‡ùßÿýß·ß~û-s89�(¢@ˆQ D�(¢ÀM­À•üªza~¦ºƒ_�ºX&üŠ>§2«¢V°/×Y&ü cGÈ�»*.îˆø5QÔÍð³=pÛ‘ íxŸqÙìßé(n,d(bVMžÖ´Ín¢D½¹U•UÞ1S*SxvLü¡�¸¡'¾UÏÒeün4”Þµ¿G"A)ž… x Åìe«›ú’åZ+À¹ûë¿þëaõÍÞìÍ8­Y¢À]©ÀÿüÏÿ|ß÷}ßk¾æk~ÔG}¯w¥+®û%/yÉ _øÂ·y›·yàxac1¢@ˆQ D�(¢@ˆWÌ$/3\ŒX1»h ·ÖøUU4åWæ‡ÅJήäW@9Ò”­,³.F\“‹6Ÿ›@Æj0µbmÐ3fK ¥jT“Õ9 Ííê'ª-Ê‘ÂBÒrš`› @BŒÃöx×.”!ެ=q›ÛջРÛvlu!‹Øe¤8Rp£\«}|¡ü[¿õ[ëF`ŠoôFotéÒ¥»Ýín¯÷z¯·žá–Lù‹¿ø‹Ç<æ1ãОö´§½å[¾åؼVþðÿðw÷w_íÕ^í¾÷½ïë¾îëÞGtMáðÿéŸþ Sïðïð�ø�;Ú|Æ3žñ_ÿõ_d~ÿ÷ÿwz§wÚ±Ô9²ýÒ/ýÒb±xÅ+^ñÇüÇïñï�…ó9|Žª/Räÿ÷Ÿþô§·…OüÄO|­×z-Öý×�{áŸýÙŸ½÷{¿÷ë¼Îë\Ä~ÊF�(¢@ˆQ ܤ üÛ¿ýÛßÿýßÿË¿ü ý!h•¼á¾á›¿ù›¿Ê«¼Ê5<œïüÎïäl Þï~÷{û·ûkhù.7õÏÿüÏ4^xý¿ÿû¿×ý×G½]BFÈüw÷w”zùË_þê¯þê”zÓ7}Ó³>�ßÍ�»ü݌ȫ Yùêªüj<6øØ /#\nɯìáIjE–Øö+.­i—Œ«@Ó9Ôt y€'k³¸pŒu²�JxɆ×iTVÆ]wº½ýê#[8NsÆÌa¡ÜaS¾ÆÑR�'«VycŒkƒ&JÜø_u˜çâËóž÷¼ŸÿùŸßn‡Ïóg}ÖgíŽ$¶[»‘÷Ù5øV]¸AÿÂ/üBÝýÐ}ʧ|Ê­z¤g=®ÃÃC>Ø”â»÷² ¥.Õ_ê»ôâµÄBˆQ D�(¢ÀM¤·<þã?þãŠÏº¼Û»½Û{½×{ñr-HS?«óÐ~-ìÝ6hÐýÜÏýoøI›õù|þ¾ïû¾§Eð+>¿ÿöoÿ6+£`¯Àì>èƒ>¨#_Ùµqs<៩ݴÑTo(cMø}D Zs„5PUá©~Õ ¬ø•=H�gÕQp¯àe'¸Í2ùv lAδ'sªRMC`Ì!õ«sš‘en6"á(,w«Z™|´z®V„�Ö¨Á=Çpœ“@8é›�Vy3ž�Š«Býå(¡y˜¯¼ÖWÞÌ1¡gÇG¾W�¹3—ý×ýò/ÿrÂÁÖo‘w¦©ë‚ ð+ǰÀ7Ðú�wì½ñWþôOÿ”˜D–[›–r"-å—À³þ*µ~ÿó?ÿ“H4tkþµžáz§!ØUüÃ?üCî'×[íØ�Q D�(¢À ¥cû0ŽÊƇ@Ú&¿ù›¿I|À åð å =`~à~`Ê݆{´‰è(ó“?ù“#e¬ ì3ŸùL:ºmlýÑBüžïùâàFþ¬¼r*°Æ¯DN§ó«žß@tV[áW‡„–Á¯ŽgN¿ÀnBß*x«�›$N¬&}X+Ù«$#µau!ô㯆^3òíÈzŒ¢�a\Zv8›1N›ø¯Y@‡iædðضçPt²6þì�ê (OtWÖ–èÎÚIžSé²7þ•ÕòÓÝwâòçþçOxÂ<%YnNøMc8N8÷µúAiؼ3W^ö²—HÅrk_�RBñ[Ø­.¢óßþíߣ‡nü x;ç.ËOjoýÖoÝÅã7~ãÜvR0 D�(¢@ˆQàæR€ß€ŸûÜçŸéðøžïùž÷¹Ï}èØQÁ'î!ªëÅ/~ñÈ“•¡�s§�Õè+Êø-÷¾÷½Ç£59ùqý�þè�F‘^As”ïõÛn»�ȸ÷{¿÷»ç=ïÉD�H{êGôG7‚¹SÙ¼…ØÄ¯a­ó+P×6~u0wþùÙ|¯¸ÎÄd0³VMÂ…Ñ ^³ÙAźÊÓ¸�x)ì4sä· ]ñÁíX#�ìÀ6û¨BÙ ¨ƒšÙOJ&…ã½È õÀï°_NGv›PÑlvgµsjí±¢"‚•E;… Á».Ë×|Í×t¸)4�VúOüÄO¼ô¥/5±þÝßýÝ�|ä#GJVn"¼�^Ãc¬·›ÈóuWo¥?×�n¤p²z�Nß×dÈÅ�¿’�êîœþò/ÿ’ºøÙ“;ëM�€ïÅRKˆQ D�(nèg:øÎƒô K—.�ƒbe"³º;ËïüÎïÐítìÊJ+ðÓ?ýÓ`³�üÈ�½u:½té£öœç<§÷¾à/`ôê!ÚßüÍߌYéCóáþáãÙè •ëÇrz�2&øiýU‡µ¬ÜÚ LøxÊH2¨Ó*¿’—­ñ+'0m~ìÚȯªÿ)ðLÒVÆée ðrh5!›ì‹(3I™!j¯ Õ@mì4®º½3Gã/I�ýUàšà®:‹bD̆'b5ø^Ÿ¬ÊêÁ¼s"+s!�ÅŠ?ù“?¹Èá¤lˆQ D�(¢ÀM¡À_ýÕ_-Û·³èmÝçiÇÉ =¶¢£¼u ¤lãOò—.]Eâ$¥ƒÚXy—wy—ï6rbmt¬¡«ïHÏÊ+¡kü ´Ûį$]küŠn¢•÷~u0¯Îž�-KS™]Qýs»Ò*tMÆœ »Ìö*—íˆYœ\Äæ®d‚1Fo#ÊÍ"º |Û?>:šaA:·w|›”maU{Œg¨…ÝXcÅê*ŠÎ³.ç#Ï5ÿa�¼wâBÄ/÷ÊQ!Ãl�ü _øB†�§ýLÇòŽfC‚W‰•£'ù(2Výñû¿ÿû±ÖÑ7{˜“ª¬�cµ{NŒƒÏrò÷~ï÷¸+a™›¸Ë�zÔ£Þø�ßxÔ>]!žˆ>æyé» ¿¼Ïû¼ÏðOólYðýÔOý¸…ÑÍþ Þà 6f&8ëW~åWÈÌeCXõô~‡b3Ø<4„ ,´Ï+v^ñŠW ‰ã 0P×/ÿò/ó£¸ð5^ã5øÀÒ�´‹pcåg bÃGÑ[¼Å[Œ3Õy^ô¢1Ÿ4ëÜ|§Dµ÷òJ@2gMð o1ò&oò&€’×~í×yÆ aÏü$uùòå®…þÈ¿ÿû¿Ï2ïÿûÿå¹rÆ £Ôö"ÌûòÀ2Fâîw¿;ÑÑ] \‹cÂà¶\�œîÞ‹ ÓŸ€ŸP¸§-G¬ñMÏAm÷�*8¤  6QàíÞîí¦#åm/>öîxŠGþ•.�Náqã§iä'Èœ#¥‡8gŸ™Â¹$Þõ]ß•ƒXáªà!†CëDdºqqrIgÔußûÞ—Ç×6ÓYp:8„I2kҸب‹±Z1ȹæÚ �8Y~L›Ö¸e�‹¹ûÒ¢ð–lÙ¢@ˆQ D�(pk(ÀÃ$]#i)в`Ôéõƒ�÷ìÚ}ÂM23�m@žNy°¤ýB« ŠGcg½Š‘Â,O¹<ëÒ¬ µÅC, Z4S€ÅÃ*�ÁíÌ|ÀŒ²++TÝsŒÒ æ ¼÷òLΔ<¢ã�Öݤ¥í�ô+–§›´q(޵w~çwž¶(Gž©¤�8{ U&µ`/Ãê�ÌÓ•Qpªÿ4Ãöõ]š!S 4hgáIg9ÚŒœ¯i#nä‡3tW'Úé+Mòp-ÑÐf…F íQª›¨ä§R0º4皦GJãô÷¸Gç¼ø9Úå*^õ -D<'Ž„«ŽÖ.×ÌÝîv·•lÓÍ35ϧϺ^0§üŠ 0ágůÀ`›ù•1e07PZa4¶¤UëüŠ&'¡f5¡B¡-{š‚ßö¥[v+……ÙÈF÷RŠ“‰�ñìr:;·ÑÕÈÈÞ½ƒåõ*0$®¢ÜØ%-#/c͹$7;À3ÌX…¦g‡{uHzHFÄ9òÛÜ!åXÌ\Ü­6]%øM»wÚrpy¼ciÄÆ­í)Oy (玵ÆÅñìg?ûgögŸô¤'M/#˜Ñãÿø• îw, Iˆìçþç÷'j÷œ]5Wð—|É— ¦@"Xøà}ú§:],Wœd ɯÿú¯Ÿ&òágF˜�“ÂL³�u4áNÝs*óáùø�ÿø±kº5ãÓE cÜ%9ÀÿñoÖ™ùìa„Û8ãÃ>ìÃø#Ünú¦OnèL@3°9dn^PR~äG~dj0Ç.¾¸×p'·*ú÷qßÁ8·¹ôF‘ç=ïy|i�ªYáþÑc.jÀ _–Ó]¬ƒ½¸qÇ8"î_#ðŽÈá p6ön\ÁŒ ÎØÛF€A0D #rø¸42°ÂìÚcÝ>á>¡7�ª¿ök¿ÖŸœNÁ=4á.¼ñ§¶Î=lÖÙ›œ”ä@øºâ««¯úz¦S|šµ�Þ`§å!üÍe32à0‡É�S6“�ØJ×N“e”úŒÏø Öù0öÅÖ—kÓ±ÎÃ.èöÃö0žføtó6ÊR#çŽÇŽýÐåJé[V0Â��³Ã¥Ë…7¾ò·É®(¢@ˆQ D�›WÔðw‹ÿ °È@£‰¿·äœî¢©Bf ›h�²ð ;Mœ¡½C£o4©ØÅz·zh½Ò³[m�‘~0&Ï®ÇAã9–ÇìnqŒßéF´F�<*·q`ãn�Î#Ã.+´æFƒncþiKpüXNÎA7–"‘6fï:ÇxD»4C¦õþê¯þ*m´i MÝþEŸÂqFd@çá’èÓ˜[?üÑL&3çz´µG•Fßÿð�ét!e\�Þ.~Žv¼ŠÆÁâÃès1Œ”¾ê°C³h$Òì¥_pož£y>ìœc*f/Í;ø!iE¡`SÎT:åWÆ®AÆœZTPÌ’_íuÿÔU~uD”™c¼Ñ+U6æ‹‘j¤ °DµB0'’ºÆÃá\å܂ƚ€9÷(ˆŽ`9*«nªe3ÆÜQZF÷Q�Ü|oÿ6û•’R~ñb†²H¥$“èNs'b(7Ê-��Ôz#áN\Ví|0ˆ/{Ä#±Î݆S�¬/þâ/öØO–'?ùÉ+Üíd�ï܉w³SvÏI~BÏ÷¸ÇM¹[éWÃÕÚ¦‰à›î6Ý»ûú¸‘A:øµd½ ïâ;2Ãq˜ªbÜAx`Ž.Ľ�ù¶ù˜­[#‘�ëôK‚<ŒÆ™Ã 7,¾Ø†AnôÓ©pÖÍv þ×w}Ÿü‘� |Ü8�`,èÞØ»²Â¨¨ˆÐ‰ã®Ç&úãs³Ú•"+›§ip7X G1˜/ªoÓ+·ã#]5²?ëYÏâÎ>.?dxE"_§�e'Üm~×ĤàJ�7Ï}ŠW¬�ï‰ñE¾’�MÜmÅa¼†’ëHY)H +ƒ» éH‡ecvp·é.Î21ˆ¿±ŠõD‚;ñ´s±^$)Q D�(¢@ˆ·¤üÊ;ž´7B®�GM›”vб�ti›Œ¶À´,Ì…FÇ´I5} åÑz¸A¯—¤6ž·§¦XÉK^Òµ`¤›{¤ îÆ36á{#òœLƒw£W+fϱ9}t5^ÕÍ–+#çöû×MíØ ¿÷{¿wÊÝgª<1(´F7¶‚‡…³®Ð­¡.ÛÑ”?G»_E]/�\®ºÁÝhçë7N—ÄXÆ|ÁæùYµ"ƒ¦ÆSÐ*!\ó«ƒSø•tê ~E„™�KŒÆÂî%¿’nÄQ (sƒº�ç°�*��uØž.�ÖÅ †#¼Ø­”ÑÝNàÝlÿ û˜ž°1AžÕ‡Ah+¤f=b¢Re}oî°n:-Ãâì{ZÇ.yä¢Þ•!Q¤�qÞÕñÜ/úŽš¸PèѶÛ¼'$Š%úBÎÇáVHÐe¹è¹@‡ò ÃÇí Cìëó1s¦œdæŠ|êSŸ:Ìr¥ÒÔ¾l€?ºµbø�ßø�t k�¤ûŽïøŽQ„‚ƒˆf"Œ[!�rÓ]Û× ú>ò9![ÿˆ±’~Ñ{¹’FÌ3ð¢%â’¸ßýîÇ=½K¡1_| ¹ ‰*zðƒ¼b�O, ‰üŽÑC!poâÉsÀ ¢œ8œ.ÅãvCpâð€Së›Ô8  g‡.Ã}/€èq;PŽZ`.¨ÁgŽ‘ÐE‡x:¾Ì~æg~¦±#KÐÜö߸ð‡k¬oµØ!LoÄKÝ8 ;âì£>ê£8ƒxÅ•ÖGñ�‡€#-Ç>.!8ƹ¸`óüªâ¬g ‘¸äWÒ/û‚žÆ¯`VÇ�‰_IÚ œ3Þ­FPãÝÏ‚(…šIc)˜—¼K$VþÕ Y¬Ï5TÛÂ`;'A…´aOð–�àÊgCÜ Ñ~ƒìõ6HŽÊÍB™*êt«ätJ‡rnˆ)vrðå„Ô®æ]-o®ë €ãÇ~ìLj›ÖŠMšÜ_ø…_ømßömÜ­ýèG�.ÓÜ@�_#ŠP©Foã®DY.¸¯ûº¯ƒß±ÎD6Ÿú©ŸJW5:?öÝj÷œÿæoþæ�Æ)S×1«xÈ=Žl(ûcûXÖ¿ó;¿“MVzùìÏþl®þ^ÿ¤Oú¤oø†oX ‘;ɸáÙ�÷³�€>â‡W2 dÉÑõo&| @Ê:pmJ0jL)‹ŸìqkØ8¼÷땎Ÿ}«¢7”ÁÝØäÁ7»®í¯TÇÈ_�‡oBsG~ôQáUS’_üÅ_äöÊ­yd莎)®‡ølrtHÝr±ÕoÐpwéÒ¥ÁÝ0NÌ-�Gu\6,âò“…oµ†{LáJ7U¾}§_�œ,¨P�€¼Y?¬NûirQñ%„°Ý­•"|mLE>ñâŽ÷‹ŸâakÀÐ 6v�î7Œ=Á‘ŽÎ Ï.ÜÇI2•‚]\o�êFÁ••ûßÿþLv܉|@0§ßÍû±ÛOX滓©šÏŽN²+ÖÖ7Çï<ƒ0®çIJˆQ D�(¢À­ª�ëüB?=:~k?mH²i¶^ ŒF?Æ�!nxÂç!²Æ¸Þ6é¯\xp¥‰ÊÃí€`??*Ó|è†Eh˜\¾|™tš]üêÜF¨ŽZ¦ÆhަhG t£óÐmpÍñØÌ³4Ë´øµZ§m;°{{²‹qô4y†·»”%ÏŽÍrò´OªÍÒ`$àF„R Í„"h£»Ó„#^u`î.»Ë+Wgvšóšœ£3]EÔ>À(�âÑd&�–5WZ1ÐÈpàâÍóé!ïºN—K™WE{Ù›s�_¡‚_-àWp/'HUMù•Ó¢žÎ¯€_ ¦@u˜öú`REA>ñ×Ie:[öv5„®®r.ôÎ’î¯h9+KÈó„iÂa P¼‘60�Ï‘H­`]Ù- äªý�ð xð8:¬’ñÈ Ÿõ^‡übáz‚¹Ð¢ž‚*naŸù™ŸÙÕ±EÇI:‡îF:WÌ YlöeÄÊdeƒ£Û<›p‚ŠfÚ='e§ã|1¦Ûô:FÒá*9¡{¼²LGsã£8õ–"+ŸÌ.²å•‘{/Õ÷‹‘™(¶q,#Ñ­�ê1>Z£°l|gŒÏçØË ôd…»‘8N·þF]Ó"»¬�j0Êpu$q|OðãÒ8§#+d˜Šß»F|܈A›YY_œ`£þz[Ép¦M¸$t6a�_†��¥F +�¾1>ÚHç‹yÄ… Ð9ö®¬\ðO­õ™å²Üˆ;'ìtp2+d€µå›Ê}|cçÖqÓáèÖ¿6Þǘã}Æ3žÁ/EDÒîxÏm¯Fô/þoüŠº|ù2g¿Õ�M³-l|å&Þé#zkc¶Nä¦Ó´‰8 Ž”Z†[ ži²p€}aP}WН\�c/H®ãÈ®Š–.xŠG�¬ø�?×2”§4'wÄ—�ËØ²Ê éÖuã§ÂÎ78c{u\Q�aŒz°=öF�(¢@ˆQ ÜJ ôÓ>QHü"Þ]pxJg¨ž·éµýHi˜Œ&ÆùzJ®Û癹›Šãyž<ôêVë�Áá1£©;Íx¯†¼Ïð|ð9ýQFˆÉz�O¡»â`|€Ãv±‰àcÚG é³K©]òll† ¡h¯·#0 a 0¥ƒƒ{îRÝö<Ä÷ŒÇÈy½ÏÑÆ«ˆ¦ÙJL×û¶"]'^Ûæùá”—×îiüн5š¨ù­:“jC küªš¶µW|f×O6XezÖªæ¦aôe>U“à`ì.ÖÆ‹kÆÕ‘}Ÿ s†£“̳ål¥�›ž¢V`@û}-׬V·èêqÓ‹±ñêŠC½±ƒ¬G ûŠÀ±j©;q¡Ó5s v—ì•jù,1¬#ae¬°lìqPã"›~�ôeáÃðÀ>�?Ú=çJ”qª+ŽM7¹Ö9+!Kð”ižó­ßóž÷ì>Œ0)nÁÝ7�o‚ÍñeÀwÉÄýMCÿÖ«�v§»V>¨½‹ŸY8/ÃÅ—±Ð,ä$ô‰ ¯�(mj“õñ;Éô¿’‡_�š:q˜+»NÛ\¿Ó�–“tT¢nÓ=d¤s;ñqU�¾2ë#§íÂsns,}t¶3�-n—]jD•o¬ëâ§xÝì9øã žé0×kߘ²vG�»W7Žk”ÝXW£@ˆQ D�(nI4ƒLõ¡ ˆÞêMK2ìÝrÔÓþ+›H[ÊNwÑ¢RÚ´6þÄŽ[ÐÈ¥Ñ[ôõ釨òF+iÚ™‰ˆ1èùYhÒ}„FÍUCX¦Ží²Nsi`AªØ>*Î0Èñ2*zÇ"Ð^c™kèØôÁ~´ ÆÉMªáÌXAçFoÏÂÈv¦•�ì ×ü]õ*â õ‘Ò9@IDATaqªf¼rðèh&ÓìítvMÑÄujž_E=AØv~%7�ig�_ÁVùU³,é–(‹"/Ê’Â*¬Ž$Ám¼5GcÛ5X™õú–~Yί1ÛÂAÍ×@Rbߪ0od-ëX´–ª‰U˜œ¾³­³bcîLÇGB(!Á+ÿ rVeñ¢vmÊík¼pïèv5Q”\"P:!òº^?J<íiOøõ #…áŸøÄÇ?þñ\^#‘P ³A³|îç~nwöÜ=g“㩵-ëD™q¶Goöιºo1²q4úæEgÃþ%„žó^¸œåýý1GÏ4%gX6ìÄzº%ÏØE·Y~W™”“qÙ`‚=àèȹ¾Âo;í*»¶ü&¿ïûà™d_¯î´.¼>Šq… )¨”…¯+º?¯÷=ÍT§si5÷úíù·ï_H}¢OË|mOqG™m¯ñ4Onüôñ�­æÆ÷6F�(¢@ˆQ \?h†0r4Ãõ£/ÝNÎxKuÓ'ü-M˜Ó,ÃA{ ֳ˓6�o`AL]Áß@.J±ÒÆW†‚ÍѶó�’‡Ö Ý¡XhN{VžæÛŽép·1¤c¿0Ló.ánÏyÎs:Ü�¶�À¶#Î]l^5ÏhßmAoã$nt¸j]»d¸Vçh÷«Ø„岡µøô§?�Kˆ¸ }Ò…ht,»«šçGðªù•�` Œ«ò«êät¯lW&e£_+ÔËî¦2¯ãE�ãÖñe çL ’¼e= 02“f�¦Óá´€ŠLO hV²,1™HM–Æ^àœ‡U}[ÉEWV&d�çQÜY\›Öu7U�¡Kma@ëcšªÄØõYèë·å#1êä¾ó_ñc–É‘¾e…;Æ<Ža §½>»È×ý×ïÖ£Ví˜s%žö ¾à :âl£—/_&}|¤;ϺËnOäž…Ãtw%sª6z¿u\ºtiPí©°L€°âÿöZ¶ï,ò]EU؃2ŽèkîªLƒ‡|¼O³0[ît#^ošÿ4›çKç%âKõˆ9@�›5Ñ÷q·û?LQdèÐþ B âs&'ÇÆô—œu ×ösåô½˜ó‚çëÕÝÔ)ãD ¬ySNœ�Q D�(¢@¸ˆüOüZÇ—MÉÚF›ÓN+Ïüó�Dž«‰ùZ & ™ÀCþiM!bPð­[Žô9¥aóô ª2Œ÷ íAxô<7žxÙE|›×dOfÜ�êv´Idß”»1¼ûEWŽzË& ™Öö4…);† á\l1u­v]ð�õ*¢oÑ…ˆO —fõtbIŽˆ«‹¶üˆË™6··(ví›ç²¨âWK•OåWÄ�k6åW2«êÔ)ýšò+ûtžð+ÖÊ2DË!㊤9GŽÙšžéƒ» qD¿Ù%ôTgXÚR+�õQ«ÙG�h=5»Wãõ^tÍDÌS¥;@v†?5gÖþ« Ù&MÿȆY£í\,m‰»rù–où–)w#D‹E¹Å0¨÷”§<å)�#rŠ^«�yÌc¸éFѾgufzª�ãwÉ9� Ü8FÄòÆÚIœŽCÇæî}'O3Øé ¯Öè�#¸Á_F ìʬ|�ú¶&»†è­Ý@ü¾íÒáŸèGPÞôÆ¥ÅWWt¹Ÿv¤#žëzß /×rBRæœvø«^O$´‚€�N Ò±žg=OžýìgbÈønë³ä­—º&)ÄOt›n4©Öͺ1˜¦Õ[/u‘”sŸ£ó]E´mû(¸Èi�²pÕqÝ@Vèí]Õ<_ò«‚RÛùÕþ\ˆ6åWŽÈÆBoN"ÉXÙȯäuäóŸc¶3‰ŸÀ.JQÔwcÕa 7gÝ9’YÓ.õ:éÂÁà0Æ|«b9âçŠù1=aÍÔ  Ðÿ�Œc T²T ²Ç-ÀŸ.™Z^wfvQaE¿QŽâ컫>± �Q;qªÌ%:hàÆÙFfVÈ ¤c�ÁÑÏt Ùôj÷œ´ØùÀŒp¤ù‘¹*zÓt-ôÇd‚›i�·O¯ºÎ­Ÿ¥QTqÐî2+=ÌùtuÄv­P¹«Ö²{0 ¬ª¿øvá³½ñ[¡m û—%"æîsŸû¬WĽr(sMzé®W±’‚·àB¨ÙüÀôo_ÀÊ•<§mì8^öry<ìa»&AUƒc^õ{ýžb.ž&ž|Fn=ô6~Ì:½o¼GSÝúobãúdŒî5®ÏÒ©n®[#+·×‘¾>í슭kxŠn_E+u]dóL‘ù©hcY~,jI9Å×;Žr£IŒQ D�(¢@¸Ó 12zü@Žú—õ•ÚaãÇþé�ó+Ùzƒ#Dn õ3ÍIO¬iÿªÞ5bDxÌ^gO< N-Lשn´­Fd“rNG›™æ_Y'çhËLCÞø•}„à­Ù¸Iƒwp7ÚÎyÈC6f[IÒÕ±þyöf€uüYÉs]7Gg5ØåFV@Î8õÓÆæzí»©ŸãlNϽ~Ú9Z78êÝý*E`ˆ�e¥–»¤y¾px4)Ôv~U£®�‡_вo(öAZ`:Þ¡_¸Zçâ”~ñ/„dófc†½ïÙ$lî%îmÿ@GÉ$bcà'³Ÿè¼‚ØÈGa’iü—U²Z'!$IHï=¼ê †M-æ(‡Ó—òƒÜwݲ÷ô}ß÷}Ÿó9Ÿ㠒뛿ù› ûZw�;Ô£õ(n ‹FŸS˜w¸w�‘¹{›îž“‚DÛ�ÏÆ]òK¿ôK�“È^óp4fèñ/YáwßûÞ—"ŸôIŸÄ¸r])þy÷‰Ÿø‰ül‚Ûü‚±~ƒîm_�Èp+¤872sƒ^ïæI\ƒnö–.·ôw"ŠpC$b Yàƒ»ÏñŒ5zûóÉ_!}Dt·ÏWíÐÇl8ÜÄ…0?ƒLÃ�Ñ`L&KLõô'¦íšœi/t‰¯[~š ‚…1ŠÁÔ¥éq§î9d�†äá;~¿Gµ'|·­é'íô À�"].$~\ê\äëçtÅØ5<Åð´? >àQŸ~¦ƒ»Ñ;•ð—�‡fÔŠµk² %l5r¢ÅÇóÿf û´¶hÑwEô›ö¾¤Qßq!„­@¯.]ºÔÙ8_4ðÏȲû9êêV^Ïq ~¸àâ§ÅÊEÎ>¦aKûšC£̹ëºî’æ¹8ªâƶó+ Õ&~¥çìj~E«+üJf˜;—ÎIšíì"ŽTG|sð·Ù¤t óV;°k¤œyŠš1Ö[×D•¤ï×…ËN;ÂÎØMá1¼Ƕï,•³^ ¨éG{‹Iœ(FP�x.´ó_��îŒÚ'æÎåjƒ �ð(†lcÙ�?ó!¬g¦o`�äµ{NŒð;]VŸüä'ƒÜ’XÆæXyÖ³žÕè�½oû¶o,põÇw|ÇÈvîÎômü ���=ÿÉÆ·BO‘ƒ Ü2¸÷ñQäSǽžûQŸt8óòåË»øÃyܵù&ãFF,4ø ˆsä.ÖÅ›Lm1ÅMqøÆ"eéã‰K8ÆM è3Æ­ã^Ðn1u¾]T„&TM�W¢p¹÷ˆ||FÛ0ÎÞ±Î÷tG¥‘°Ë·8÷èþbrG»tégz¤£¢Qð´J!>(“sAæôG¹žCã´‚�~ O1ß©Xà üçã6 ØÜîÖ½|Ÿõ D}ó5Ì3‰�|ä#·”ºæ»ÆsÈu½æUÄ`ˆQ D�(¢À�£í  \êv- žê¡B<ÆOŸÒi¯M¹Õi‡ÀÞL1×iûbxZÃ1œ÷ÔÚ´8Oø<ç“BÏÇç>÷¹Ý¾ á@lÏÛÓœëë ~ìåùy½—+�¢íØËXlü`OÚY4ÊF#‘G_š'd€%uƒ…uJ]½1”Ðx~¦m4&ÊdeãÒm"v¡ÆànlÒ¬à¨7!‘v!);Æñ�fä´ô>ð�ßó=߃,„5pâ:®‚!h�b”"ÛXgÜIC�"¬¶Â飉JëŒ"§�âiñ�뻟£�ÅI<ÇUDƒ¾6¶-_ ‰@.`.•�x—4Ï÷ 2:5¿ªV3”fœj+¿•1ÌÚ ¿"™ZåWv å:Û;>¹�èÛ#°ÑÔ{ŠÎ|‘�±Éªv)'=s«Æ”ãSdv‘Í�÷Qˆè8ѵA_SË‘‘½,bCàž›õßBŽõÆVYIíyꬆå]�hâ:.�{Üã¾è‹¾hcÌšÌ}dåG>ÿüø°1?‰\[_ù•_Éý‹õÝs¶µ=èA¬ȶåv {Â~ççf÷Õ_ýÕŸ÷yŸ·1ÿcûX8ôùú÷1Ù¸«R×iC¹ñô�8µ¾_ ÔŠV”%éòåËíðU_ùÆê7aºë"s_‰†ÛhnÈ×_xÅ¥Æ�eš6JHó.ß‚ÓR;®óíËQt�Ü{ÓiY~SšF½ñAàg�þÝ oÇtDrq #f�ïÒ±Àm�e˜â(ø™eËùüá?aj”êÌ‚SW7n^«S ëä ¾*ÓŸò6Ö»K"Ï(#ˆo|Ïq¼Óß‘v±s‘<œîh=%ª±™²Q D�(¢@ˆ7¸&>£óÏí+-ž~ù©{im<.šuüèNB·ªÀLÓ0(šf<ó¼Õh¨(GN6ùÉŸeX¦j`Ù´57v�š$½�á†Æ^V7Ãþ°`jÅ-©1%Â40�Ì´\Š1L�]±0ºb{ëÆ‘�v¼ÛÖìË�ݪºNè ³}èC ¶èŽ¥ O–©W�<à+¿ÊÓv£!?Ž�ÊÒ¥�‹Ö­×nôMMm_ßý�fçWmŸh�²Îyéó>j¡‰Äàõ�~ô£¹„H¼ó›çK^FÝò+ûnV�N¬N¡[ù;Oø•¤îˆ�Ò˜Öà ~£Ë'ôM¸E % Y« 2ë YýDy/FGSÊŠ/EÞL!™¨7›ûTJªí~Išà®©™€�Ï”J’.Y«?ñ›�µÒÁk’6�kÊi¬Æð—²Êº×eá¢ßÅ.÷zw>õ©O�b#~gøäOþd~¦ ~êIOzWÏ0Ò"¨í»¾ë»5l\vìåÛþ´Oû´Ç´{ÎaúÆ}ü›¾é›øeêÇÂã�xÄ #ñiO{ÚW}ÕWq7l'qƒx1ú¢r%fê‰O|bѤ£®-+ÜPø1§1F¶ô¤ãƒ�!ª™!_9Ã&s»a¢Š�Ä1ŠÁHYY¡�-Φ»‚ÿ9(LM£yW ®lÂû‚�sÇÍzxÅEÈ/< ¸†)/È+¯èå5yåŽ+·Ö ^¹�cdºeÓ¸<ÿžÚä÷�Óúx$òÓÇôÛ”œ\K˜Ÿ1à(‚LC©Qƒ7ßÁ|Åö-{*lÿúÄm üòÀ(°ÓX?.NÆ‚]÷aå(¦›g:ÅÓ‚+ë�©Fo/{ÙËîw¿ûõ�˜<íðJæ�›ÓÃ$×lq:"øÞºté{W2o4Hâîµo´Àç®O1ߦœ¬�y’¢@ˆQ D�(pë)@Ã�éˈB Æô™Ÿv�£´kxhßý¨)òð‡?œŽD<À·5ž–ùÁžÈƒË—/z6m,`–Z˜!�[Óá\(‚žöA!ÄXñzÚ³î E¡E¹î'ðŽö¿sCèF{Šl�Ÿ^¨8Ð¥:¡g%?®÷Þ«6—¦Z­×{ZÊ´Ý}Zž³¦Ÿ&κ�•–z`tP#~¢égáŒGFãncÓ›F(Mlºˆ=“Æ2�±0H´ÄÊ)Ææö&êîçhýˆ:å¬WÍ[:Sωe™âE—"8¸‘.ןŽ1Ðá9šç§ù¼Sº¨©¡Tó«–²¯MÒ›Dñz~5ß›ÉÏVù•LL¼eRÞìHj¬\uì>‹½xµÃ)oÀ7ÝÁ'&µóhm±zxÌ «{³ÃÃÛ�uæÔÏòÕ*u�œúÅ«µ£c`s*0e^‘xü7td?´¯¸^Y<{™Š8ê"’V/{éŸaêþ÷¿?¯wáîárçúãÒçSqUO•ë >ÅG‘á·`õÝsN+…£óa¨�Kø s}O÷®¬SDOvM`¥ìµÚäóêâÜ"Å5v¬Ž˜U¬Áò9ãξcÙi6¼â7(îƒS8Íp]×ùV¡‚Òˆ(öC{úÂ× W y8Þõ¯nsìåÀÔºÑ<7¾Vr@á�ÙvI¼à)÷7 _<×*FŒŸA^!_ÓßÜv9œ‹ç¡‹wÿÞÁ' ïâc! D�(¢@ˆQà¦S€~€ °lÓœãXh¼4”Ù¥,�†yƦ�pÕÇ0øŒg<£žçÇìÓ&i™1Nk‚MŽî4žH_~§Ñºû0ßÃþM½B3¼#Äîlo°�äù¥êó5/¸²Ë9ÚRÅŽWQŸeì¨ô Ž×ÕÆÐ™ëÚ<ï±öîþ6w‡:Ùú>•_’6øÕ›½é›.ùLmßÖ /§ñ«Å €F¯UCË–af`6ÆvkòEøY�°š&•04¸$Ìh9 ±Å+ŒŽ4øNź+ÀSUt“ÒùrH>ÕnHž;ÍñÖiÈ\&˜ÎzOž¸ŽurU-ö85£eo€4s&:ÃñB¼Kñî9§2pï†dOS¶¬Sý[·d¸vŒ¦äû‚5¡6&ºˆ)¼â;à".RÚµ#ðêIN«‹ *–ÓönOç;àLan[¬]ð‰ù¢½ûL¹Àýš‹vK];îjV¾cæk›��·æn(<EõÚÖkQ D�(¢@ˆ7¸0/–kåättš«Ú¤qÖ&qI£[ÌJŸª�Õ··y‰™`ŒiÊÒ/g£…[8‘Í™ÎRÐÌ¿æá2W=GÛOÁŽW�Gmç´ˆ^wÅ#Ûimù;¡y¾ìïy¿i¹¦üªŽc#¿²?¨3!îFoÒ5~…U©qmÚ'‹ÝR÷ˆ�1Þˆ,c×¼Ê/9œoe²=hø%›ÓÆ¥¦qÇ c»1•‚5ÀÉpw9ñ„m‚à8 ŒȳR&s€ñ1vÝlv4c¦zÆbÕìU¸ é¤\P`çe‰Qà•@Ð[G_ó³ aó7û�Ù¦y»øÏ›7»ñ? D�(¢@ˆQà¦P€¹ÝÚOÎýëþôHaœ~6à9º‚MÓ³~‹)0,í ¢8éÓ¤á3ŸùÌN$ú¤‡¿Ÿf¸ÓÖWùÕ±Xj+¿:\ò«@×ñ|>Û?…Âz­ð+z‰Êñ f :Qزß)쌼ä.nÇáV˜YÁÕ±CÔÀgNp 3;ªUØÙ�¹¤cTÉT©ÆÀ‰ÈŽ€jòµ½ÃÙñm¼íîÏIŽ.§Eç¨HÜW¶vT Ä€!}¼×�‚Dþ³Òy«@^¢@¸uàãΰŒ Ë!²~³¨¿7Ô´»Ìþq³lü�Q D�(¢@¸`:Hö�ðûñÅ�ˆ@'Ƨá�‰.n3nLè·Äì´DQÐb@Æ­ƒ¯Ç5ÀÐäô$m· J¸kûCÚîàW(m2üŠ�5IÙ’_UGÌ=Øœ®‹ÐŒ[»:¿ŠCf1ê’¦Y ºT+W˜æt ´xý''[Ò;“ŽjƒaÛÌ|à4 â²r@{d÷Å4æ�5ž®¬á!?Ó�t“ι�DªæzgÔœ•M§³tZ«�ïçf?�ø¢@ˆQ D�(ðJ¢Àyclî³öTÝ(}á, Ü”^ õ¹•¬{ØÃ~üÇØÊqàFÏåÑy¹�”©˜R�Á£îê?áW0'o»ƒ_Á¾$SůY Äüê°@Ú&~š7¿²ÿ¨8 °E÷PÞ%}¾08ÿñÍö6`žã³9ÉAmÞÈG�PJ/*ªŽÍcÔ€wXÛwŠR+Ñ8¡qû{· Ðz³ãö ¢ íè}Êl d$Nà‰&LÙÇU7<ʪV4Ȭ°’îê3“ú£@¸s`êÕ;§¢ë] �L`t½k‰ý(¢@ˆQ D�(p­``2 Ûµªø.YûZÉ;;*À¤Ä0ísÚ¾üå/‡»1¸�o¤3ë³å^ÃAwtis¶~åÄÆ•mäW¯+ù½?åWõ¶Â¯ì\ÚüJgЙ¼Kˆ&&ǹ­µÙMÈÆ” ’1=€y‘§¨XS¸ÙÞ�%¨iA•ÆÛYBíSŒ¿£#íAÒˆk3tûõO4AEMÛM®ËWqwUy²D�(¢@ˆQ D�(¢@ˆQ D�ó*P JìT±n²°~%5Û̯ cò±Á¯ö˜€¡˜×üЍ·ÅœŒþªb³êê)(sä…ß’°"gÕT*[sâR,CÕìîj.#Þ\±°ÍN¨NÕPS¨2(ÞTž¦¨ÇãY,œm¡*ª”Þ…UzœjßÃ. wlŸÅ´'{£Œ›Y¢@ˆQ D�(¢@ˆQ D�(¢À¹ ^L~UËF~åZ0�©üŠJ fƒ_Åv¿ZܶÜ'¿:‚áɯögÆ Õ?Ë‚µÚ¹{(9ö›Gç3îŽßè4Jæý}ÐÓ¨’ 4ˆ GEx$ôæ* �’NÐq“›‘Àäô^%‰ÙßòxÓ#Kc¬²²¡_x¥KØlgέi F�(¢@ˆQ D�(¢@ˆQ D8áWŽÖùäWü ²5åWÀ­-üÊÞ�Á@{mÂ(5MQªH›g=j#0í›Kk_Ø‚°ÍéŠ*øåQ›l¶î;*7+Ræ;ÿË®ÑnEÕ z¼³_Ÿy—­iØ?^Ü)$7$�À:3ð_[5‡©Y¢@ˆQ D�(¢@ˆQ D�(¢Ày¨qÚŒôÚ̯ Q�ƯàTt#½*¿t‘ËYMg4 ×‚¡Õxc®T6ùWÝ?Ía*ìL&•-=8ZˆäŒXc!Cã7gE%'>�S¤ÆVÅ¿9‰ƒƒÐ9“ªìŽÉRÝUÙ4.Èkó’Ð�ºt—ŒåÀyUM¹(¢@ˆQ D�(¢@ˆQ D�( èB…�üŠtCÈ`R`(ø•œÌŒK~5wÏUù•Y¨áˆéQ1 [¢.gF`Û‰´âK-Õ‹U¸F N�Œ#}^1sDñÊMQh[y]ýLÖÍ:„vX‘£ñ§I…#èG‘6®» šEŠÇKÁJè�ÎPÆbY¢@ˆQ D�(¢@ˆQ D�(¢À êLVeoR;]�j~U‘aͯ*A|5áW0­£¨Á¯˜‰¡ÈœÌЏ6bÚîàW,’çâ;vÁÈÈ¿Œd“Ž5ãr0#Gy»óP3³Ø3•Ýsy+~'3ÃlÀÏå̬vUeq*ÔbsõƬ ²?ÙÜ 4ÔMþ3 ¯¼ëÿ„{ÍáÜÒP–(¢@ˆQ D�(¢@ˆQ D�(p~€Lwð+蓬kɯÍ�R©Íü ²5‡_QÌ äUNÚ ’Ó!·{5%3¤ êf vy—™Á9`^B7 ÐMøQvæ`�áo9ÐCË,×>ÉØô€°:waÆ-�Á¬N�jùÅ‘Æ�S¢’Û;òö¨¾¶y᪜™J÷d‰Q D�(¢@ˆQ D�(¢@ˆçQ`•_U÷ÏÓøUÁ1f �‹¦ ]�µVøÕ1dmʯänò§l0ø›ÀmnÛºUе²æa9l[‡ÍépZHŒ|€2s¾f¨šÆ,íûr‚ˆÚWå=V, E™!øŽ”i‹ö°Å²ÂîÌ'«Ë¢@ˆQ D�(¢@ˆQ D�(. @ñªÓø•üIw2�[ñ+ƒÃvæW…±° ° µ ŠÜÄ`¢¯%ï"ü VGûLUZYy©þ°lYy±À#s53Ó (íøø°JÎ-†·Áªmå&Hƒó±›? ›at½˜T™«W*.¹E0ÞÉþ¼G�(¢@ˆQ D�(¢@ˆQ D�s)’üÊдc¦ Ýίœ§tw~%ì’m9L[.qØlNØ›ýZÙ°3))l»ˆ>c¼6º´.Ã]õ …þù×PM|×Áiv[Å™:�Ûb6yá p“º5xsFë͉àjÍÂÆÀ‰Úð«,éhí6K–(¢@ˆQ D�(¢@ˆQ D�(p.$LçäWÕíÔJ~­ó+ˆÛž¡Œ°æNGccaMÜ%哈AÃ=NòfïÓ}wî7‘ÿF.§QÏU‡Scßœ«”Š1#2›Í˜úÔ�Ü5%+ ]³ãÜÌVUâŽ}JoÔ¤G®X»òfïªØù-£á,Q D�(¢@ˆQ D�(¢@ˆQàÜ  NøýNÏÀ¯¤YÌ·ÀÛlŸaØ4óšò+6ç&ÉÀDa¼’Es~…‚cì>œ-;£šrN�DkÏ•SVÕ„ ¦S ½©�åtŠ�ìÖ<¼Ž‚ØÂ@Õg–,Q D�(¢@ˆQ D�(¢@ˆQà| À¥Š@Qú~U}8‰4[åWR¬"_ÈŠ½É´ªké„_‰±ä]üwŽÓ r#­FpzuК;)kˆK‡´™I¯Y9é_ºOP… —ÁfÖbfLØ ¢«`¸ l£ ômqÔ@ÎÁÝt°jÁÜ‹³"q…ßJŒ¨†`]È“% D�(¢@ˆQ D�(¢@ˆQ œ_�«ò+:‰nãWû'ü À&_›_Á¯Q3h P9AÝé/Œ‘Ü pA¼ß­È�¤„æ_4þ âV=H÷épzL>ƒÚš�¹£~9ö[Ã@¬R·Ø;�¢ž›UŸW*h³DÙ1×*™�xƒÁ»§+X3E²D�(¢@ˆQ D�(¢@ˆQ D�‹)`ÐØv~eÿÏ+ù0Ë!ÜÖø•ã]ɯ¤gEÐ*Î̪ÊR2ÑÉGì§k©&íHZ!n$a©p˜#ÄÍ«›){Žª‚2fŒžŒŒ(;gW°Ür)nV¥+¡HZ÷©5‘rNÖ äÏÞkú÷0èœ.З% D�(¢@ˆQ D�(¢@ˆQ \Dq|lw~%åÚį˜Qô¶�_Í™AÌeä›@ ÎÅèmÆ­iG2ǾZXe¯»ðÊž§º&(Ó™‰Ù欳¢Òô˰뢻Â~•Sšf�ü¯ˆ6÷iÍÍÔôÅ0;mHÛ =N)Aî‚s¬f‰Q D�(¢@ˆQ D�(¢@ˆçU@ÕdlG~âMmàWù•ÐÌP·"bJ&Ýò�Hµy÷ï¤~‰×lvûñѼ³0èŽ\NVgè‹ãL±Ã4�f©!ÜöfL‰ZkîbÍN²¹)¶#MÀX¸ Fh*ë¼8mƒe°„5¼%³D®\Õ�—(¢@ˆQ D�(¢@ˆQ D�(p~NáWp+!Õ•ü &e¸ØÕøUÁ/ò­&z+–f Ûþábqƒ·ËŠ·Uhéð®}Pœo„¬êFÖâcÀ0@Ù>Ño³Å)Xv¶]“—éûÌm#áè;‹c̸êÐnìt↖Åi´É2_aßÚº¬ç�ºß;ì”ñ*ùüç?¿Þó¢@ˆQ D�(¢@ˆQ D�(Ψ@¡µâW¨âWÝ5tN¬Ù6~ußr²ƒ�üªzs ÇŒSj9žó-ðï6¨l^HOâUô€ìò°šBÁdR,(0³ß(kóBcw'EÓÞQ WfÊ*kü³-¶:þͬœ£ÉUEìq0¸Ú±®Øœ[]'ÛY¢@ˆQ D�(¢@ˆQ D�(¢À9 ,¬ˆdÊ�09Y‘¨«ò+Á–Ý5¯Â¯ „5Ñbº„ålU‰å%{²9dxŽòÖ�ÌÞ¡@0é�OçTƒ[{3¦Y�Õ™$È�"Èrx¼w›¼ŒÔÊg1VÊ.oFÒ‘¢Qžk ê6'QÈ^¢æªG«#Ä‘Èþêyª¹½ûßÿþõž—(¢@ˆQ D�(¢@ˆQ D�(°«Ý“~eßKxTÍ[°‰_´šò« @Ûߟ-æWçW‚²‚zÅò cÀ®#C×( h3Æ ˜&w#ßBBæ>)šý?; ûª'ªùÊU Ðm•½s½Ju‘2þY›k&ÏŽiÒ’—iz>/PV^ÑýÔZX‡¼±nh[×5Ð5¬F¨•˜QÇÊ¢ù](.�äméQ¥æ% D�(¢@ˆQ D�(¢@ˆQ œK!Ñc§u~%„vÕDò(þvåWµ¹3üc~„ Z#½CÓ È “€�Ô"[,j~8]Á8ç!Õ½rÓ£ÔUûŠVš“)4?”šÕ² (öÆnòY¹ÑnÔä0s$b‹›•öqÉ÷Ý©Q³øš% D�(¢@ˆQ D�(¢@ˆQ œW�íüª9¶×øqiöç<�_×üj ±äp/yŒž®µ[>†ríÍ´�rGp7Þ }“�ÌXdg ·^ˆŽÃ'Ê,݇Õ50“¢õÈpDæ1š[ÕaMìp>alz'ä}±8²g«0®8!œN?³D�(¢@ˆQ D�(¢@ˆQ D�‹(°�_ɼ–ü Fu¿‚‰í32[uRÝ̯fóŽwxõB`!lt‚„"nµK†Æt§¼ÍöŠ’I¾ mt'µ3é¡s @â î@b&«ÓŒ&–XOòfôš|­þƒÎ‹û’ËÙ�•×9»±LŠ+§¡pf² /�^GzYS6 D�(¢@ˆQ D�(¢@ˆQ ,Y–BlâW »¶‰_ÍåW=qÁUøt $Vü Ä%諔ż 4 KÔÍàq¢4z|j˜LÂUèÛñì@Îf.†w; Ã¨¹Œ‡sÈ9fEÐXÞbO|§­"rô¡%2ÎÚHòì”×¼�X:P]ÏÎZ;÷í…Z4Nï̶Ø'{–(¢@ˆQ D�(¢@ˆQ D�(p¦üªf½ ¿*ĺ’‚ɯz ¶Cæ$=–†‘([›Ñ§”Šn'ÎM8Öl¬‘«5p˜Œ°Ú�X tÆ4 :‡nó˜ap`5HÙþü­Òª"Ru,„¹ÉÐj¨�®cê(½ †ÚU˜­Á µ óÂÝÐ!KˆQ D�(¢@ˆQ D�(¢À…üÊ.š'üªÐ–üÊñϦüêTv%¿ªØ2`U!.AhÌÙŠ_ɲìï)„ƒ£U°™û™>aN�UØgŒÍzjC¤¶¬ôèÈ‘×w$�ÞÚC*!¯¥]Ágöá<¬ÍbGL{ŠÅ¶e ÿq;®‘\ËÉ~ŒXž88Ç�+_ØÏq/óå- D�(¢@ˆQ D�(¢@ˆQ œOÓF~ENÈØ Ÿºƒ_‰ËîÀRìx×de›øUa3àÕÔØmÕcT,WF$nò2»{:À›52A ­È—ÎÕzo;œ”MêÆ, FÂ-»j¨Nª˜3À­¼§ÜñâPfçÔ¦ü7¿c¶øšo"›+—ÊzU¾4œ·(¢@ˆQ D�(¢@ˆQ D�(p^6ð«Âaý«*~EGQ‡cc°µâW¦�ð«“X9;€ÂÎèÿéÐl½eÇ‹#†„ËUìYÍ0ZÐë�ÉÄŽ™ãÔp5£Ø„q&Áá,Y‹ýLl§ µ£åŽâg¸e'Sª&z­ØHäˆÛ­òé–+ë¦%Ò�Ò½Q1p]g^£@ˆQ D�(¢@ˆQ D�(¢À9`X¶~e‡Q U!.ÐÕ’_‰°¨BU]R§ü ˜v¿ˆöÒ�%¿’xÕ*XŒÔ"z´Z-ÂE…`9&Lu463ÀÊ |�Sòg^9c¾±.&Û;²‡«+eÇ9œ\'¯³Ã+K¹@­ 2G®Åþ>Ó²Êï´†½åàoR¾‚}lU@%KËZÏ¢@ˆQ D�(¢@ˆQ D�(Î¥@EƒYÞUüJƶƯdRwð+ÐÔ•üꈰ²0ll6áWì˜Ūôñ1ÅÝ`jò3êĬԋÿö%‰©Jyå3 Ú=´Ye*Ó9Éô…ªÉ̪�³&hH›u¾ô‚Õ^9fæSÀ5i~ù*Þþ-]èÃ6â‘#Ó‘µ¢íÚB^£@ˆQ D�(¢@ˆQ D�(¢ÀyPíÀ¯ÀnÛø•™ÒÏóX]ɯ„{À¶âmìæ nÒ7™›lŸp7§£ì`“ð6G““™§æ?50¯ö¨«Þ¤5vÉ/¼c¯r>ëhLA<†šÄùjÿV÷ZÆ¿*xÂíØÌ¢@ˆQ D�(¢@ˆQ D�(ΧÀõçWB²c1Ü &'+óEBv<‡ç�»`l70#.æÀlÇÆ¸ÑÑÕ¬À3Žov€»üc§[ÅØ V´.®+#mÌŒäî!ê v³„mN·V¦ƒ¨Îª ÜÙÞAv…evnŸOÏ”ŠQ D�(¢@ˆQ D�(¢@ˆKY)x˜=-¯äWÒ-ù¨«_š_-óÙ|ʯ%ÃÂF~å vN½ [Zcaè5ƒÖ\¥‹kœ1Λ•/èj�—}æÔCóøg_Ó2SÉÔh­³#^ɉ9Ýh2Ôc08Ë,œ|¡BÝ<�®G.£øNlYªöißÃ¥ˆÁqY¢@ˆQ D�(¢@ˆQ D�(¢À¹€> ­àUøÕþ’_µŠjI£Š_í]Á¯ YÌTÀ^{šÂ¬&üʹL!Y’6ÿ„xMÓ˜÷ÔÈ2CÝH?Ü3r àµÏ,§ÅÙ˜¿Ôí²•³¾ãÓ,µÄõè6æ$†e¹LE£ˆuZ=Vzlº"m5Ø\9AiKÇuQápuÇÑXO–(¢@ˆQ D�(¢@ˆQ D�(p~š_ ±ŠTIÆ0&‡“‰ÝÁ¯Àaæ{‘È @½š_ÁÅ*úVüjaÁâW”  ö4jD†\�Òt*-,FÁ²Ìì£ gxZû0›;›ÂÿÏÞyKvUgûv9çÌ Ñd0„9HdƒÁPÈ` ã*ʦì€ >À¸À…íSdId,Hd„L"眓 ºÝý=ïZ}×ì9nß0£™Ñ{f¦ï>;¬½ÏÛ§Ï�ýôZ{kêc Ñ5™Ä†ò3º$–o9@Y¼²‹©Ú·á 7uS»›Ê¬FLM} "f©¶\ 5N¦îpX ¡+`¬€°VÀ X+`¬€°VÀ X+°m’_‰7á!¶”_‰uÍåWŠT×jø¬KdKÎp‚_‰Âá`Y:ÙÁÍzÐ3j±KÊ%?3È£MMÅØä7YŸŒÔ…U…Úí4ÑŽpG‘¤£ ŠÂÙ�ÝÆc]«Ê ´7j?ÃiÕ€š]6×~˜ÜSÏ4aT²#Ð(|X+`¬€°VÀ X+`¬€°VÀ X�í* hÕÇÅl¿Räè”_i£ƒ9üJKk·Årl!µµÉpʯ¾)”øÅ¶Ø6AFyÅÿLàMMéD/ :M|£-ä'«ôŽÜd•Ur'4ëȇS1gƒðª£u9Á!NãÖ_�Q<7uƒµÙÚ„ëâf©˜ƒÊaҰVÀ X+`¬€°VÀ X+`¬€ØŽ~%>Õð+Ñ€`òB›‚¨~%x–üj�üŠåØ6ø%4yÃÏÄÚ„ËĶäS&"§ìkaµ _3y›‰Æ…›/`ByÀ1Ô´Öer¡S¯JãÔF“Ø{a2’çnª}�äIGJU4*BZÃ)[™É4¦TŸ< DCSEVÀ X+`¬€°VÀ X+`¬€°V`' åjù^arkøU¬Ž`*øÕ˜…Ü‚U­À¯àX:ä Ç+o�U×”#²&Œ7±-Ö£Nú¶M&넊 [õX±­‡÷M´<àNí…ðDËØÞP&øÖïæ„ð sT)•6ÉÝh¢®pj¤P;Á8ýQRƒ‰ÁÒ:÷wP7>¬€°VÀ X+`¬€°VÀ X+`¬À¶ØÇ¯@Z8�õØî³Ã¯’Zep+…rÎåW¬®6�_A¶0`ã \ RF2–tSg¹ô�É©É6! p³8àu¬ðÇd�”È­äù¦íT?œŽv‚i˜‰RjÂüjÀ€lÐ7â)`UÝ…1]·Æ†aþÈC�Nôχ°VÀ X+`¬€°VÀ X+`¬€ضBNɯ`MÀ¬`Pûó+XÖ~5’£Zñ«l»¿ ËNF!{ cÙ7pZÐ02ÕžÅâÓªºü�ßš€(L§�:Lâú&Ÿµ€p£u¹¿± UuÝ=âJRÎ&º–x/h�Mx.à]iœ�À<ÛFàÀæÉÆF´ä‡òÃ;Oö}X+`¬€°VÀ X+`¬€°VÀ X�í(°”_/¯ŠÍ öñ+ù•‰_±úÚ¤7Xʯ ^p.ù˜¥[[À=*ç4 Œû£>Ü,–sS®hè+À˜6]`5²ÃÅ­?š}ªëΣ€4PfzÊÅV§†,pO†qª‹…Ü”¥Nøp/ZCƒÖÉŽ:ò ‹²êà X+`¬€°VÀ X+`¬€°VÀ lO�%üª/$¨+\Àæð«©sÚ”_­Ïò+еj𮋻Áà‚±±<[îSJ–œÏƬ"'X¯G`jŸ}”¥W0´l¨Â>¤2+àÆž«‚y€¼ˆW•;�H݆¬ƒÓŠsru“‡œ.‹ °…“œ¼÷´â¶Þúrµóa¬€°VÀ X+`¬€°VÀ X+`v¤@‡_%ªÚ�_…_œjs~¥xÐ~…Eú ½hÙšb<åÛF ¢&×2 uB£ !S ê’�“gœA#zU| )x~%¯/ñ§–_%´RÌhñ«þ"~5šáW±·)t ÷´àYý誖5sØÃ(g;Sínšq¦b}Œ‡ÙàmÑš=LËxQmu®jrT‹# œ–‹“=eªp �„Ö´‚›®Ò–çJ’Ãè8§ŽNãªéà X+`¬€°VÀ X+`¬€°VÀ ìH�†_ gqÌãWtn¯X/M|k~q r)ÓªlÚ·4ÜË@wYSL-aš"M©Ä ôM¥ &e…Ü7AKÃá7ŒRµQ�ZZ“[2³µµ¡ðYäò#üèÂ1z퉪«Ó¡ÀS6Z¸K ¨¶xßa£ªf¿Z+`¬€°VÀ X+`¬€°VÀ X�­*ÐáWÂQ¹Ùç üŠ O";~´Ï)~5Q¤èRÑ5üԷȧL¯¹–ÚX1¡Q>Y;› d*'Ç3È—"REÌÂ�-Z.‹&Ãå-,jÃS�ß$G#ó“�ß'=Ä!o½,ˆ(ÍHd} )†¬sí¨êà X+`¬€°VÀ X+`¬€°VÀ ìH!1 €�‚_áî®Ú°Øò+Q«àW£âWòMÛ�_ÊðN4“ÓXo<ÁZ'!T&—7Ønf€ÃMÝÓÔ9]E )%Ô§Vc#È5 ié9¶,•[ìÇ ¦F9#–ç›,Ë]NkÈiÆé²°M ¢[�‚P£Om‘ËÀaX–Ø¥UkÆå Ô³¬ˆêù°Và\¢ÀÏþó#æJýë_Ÿ}öÙGÌåøB¬€°VÀ X+`¬€°‡·ð+…enƯ]ñW˨Íò+1²ÞQ±(›6K€[ÁÌ&lMŠËš¶< �½)2#!4^èWh,pÛ�j - Š†Ç›Îøm4Š®j*¨¦6V¨3ÂÚ·7Q:üß’º©ynyJKF¥®É«‹§6ýS!Vå#Gv¼*åc7à]ûæ7¿Y/ùˇڞÇ8øÍo~sÒI']ð‚<þøãy=G²ó®?þñ�ŸqÆW»ÚÕŽ;î¸�[³+`¬€°VÀ X+`¬€ر ¿Òbmgsø•`Ç~›åW°«>DŒÅ×Ë©nSá0¯@IDATÖˆ0íißR� n oÄÈ¥Æ�cK†ÀiD öÁp5"Qƒ³�…Þ†²+ Â{AàdYno±3¶3��Qao¬§�qUÿ`z�ÝxeHÚYA.GáD¼O4À'x·+Óà�}ìc³¦Æ¥/}é={ö\õªW½øÅ/>[áˆÌùÚ×¾ö¨G=ª.í/xÁ•®t¥:=g�uÖ§>õ©ó�ï|G}ôÅ.v±#àŠvåN>ùäüà˜ºöµ¯ýð+Ú|ÙË^ö«_ýŠÊ·½ím¯{Ýë®ØjÕÞûÞ÷ŽÇã_üâŸÿüçor“›`a{ÞF×;iòÛßþöE/zQZxðƒ|¡ ]ˆô™gžÉãòK_úÒÍnv³‹^ô¢;±ï¶VÀ X+`¬€°VÀ ´ ð?íï|ç;?üáô£1£¿Ô¥.Ťþ²—½l[çK3£C_óýø•ÈÎhé)<%O·}ü ì¡Ú„_ÉÞÚd]ne 2ÅŸ ’¦fâZrW“ ~i“ØUtŒŒèVrQH ütã74¢B±Üd,CUÝhA¥`sôÑ'¸”¦*Ô¿¬\�𓑦ŒAIa7V¥®£"�©TĵÚ¨[ìä8å”SN?ýôå.q‰K<ö±�]I,·v(—ŽFGò*zÐ¥w¿ûÝ©ÿë_ÿú‡?üá‡ò{q0Ƕ¾¾.|¾¶F,äêý‚–²^i«·ÚjÍŸþô§_þò—³Îb™ØÞ€·Úõë× ±S¦°l79§�vÚ 'œ°Ã.ÜÜ X+`¬€°VÀ X�Tà _øÂûÞ÷¾ßýîw%ßÜ“&næØc�%¢«ò�¤DM:¶4•;’صkÑ„Xük¿"ns¿šŒ×YÎmže~E`§°æäiÖ~•Ü,HW_;&à”F!>jlr°uSR‹¢\… âÇv¦švS,—6YV�ª|Øô—œ¢Üßà�MìÇ#ýå ¶T�—†…þrU7¬g¤kî£@90+“¯éè„>¤ÉA<~üã?õ©OÅìûßÿþAìÖ]í²|RÁF8RÕéa—øâ¿ˆO"Ç‘MKy_Š–^á Wع›Ø/ùK<ÑÐ-ù×ÁßñÌN¿÷½ïùyrðõw�VÀ X+`¬€°G¤§žzê»Þõ®–»Õe©óÆ7¾ñ³Ÿýlå8a: ìϯp` —mð+ÔþüJ»ˆ6ãW}ªLùžhp6à¯Ú7AtNÞiüUL§N gìÇ îÆAܨj¨‘m¦ &$&Ç7~&mc+ü§p�Ã.íTD�––KûêCû§‚å`sáp£ÎÔƒ|òrWµQžÆ¥*«UáßÁ>¾úÕ¯>ñ‰OävÇîo—h¿è8ÏyΣû÷°=¾ò•¯àHÅqdß�Òï~÷»ù.´ÚÉ›öío=t›û[y'–WlKØéU®r•¬ü‘�|dÅV®f¬€°VÀ X+`¬À"XSˆ]²@ÇÓžö´\’‰07féo}ë[¿üå/W_¤_úÒ—>ô¡­'#ˆó'j¸Öz;ŒF>;Ô#iÇÏÙ««Þ¬Lô½+K.ò¹.ãçTâ–·¼å׿þuzgK\/k|Niè~­€°VÀ X+`¬@*Àÿ¨?ð�dšE½ð€œÿüçÏSèÛg>ó §pˆ÷¿ÿýw»ÛݲȯV U ù•âC7ø•(Ü\~5žB*lx*ø€*ÖV‹¶ ¿VK~%ǺnÑ�VoKn' 'w4šÐ›ö%…µáê&H'nÆ_|ïÄÍ oLÖÖׯƒÞ@KÇQ‡–j¢Z03u†9ê©3A5‚Kc4�@S�¨ ü�qùŠ*Us9ãÉS.ŠƒÚQ9)^ÔÛý—k]ëZ‰ÞÒô½ï}ï—¿üåÿýßÿ]=±F˜Ñ[©qØ%~/ŽÃnسÎý fó�°œr ç}Û•K;Ð ‘ÿà|Çÿ>÷¹Ï]ç:×Ù•K³+`¬€°VÀ X+p.Tÿ†8îsŸûw˶ƒã¿Ü¹§{/œ õñ%¯¢€n .¿jÍð«ÄZ]~¥= æñ+ñ³©šÐÔKØ-¼ËÞaH’²ÎZú¶Ú­-Øý³X›(™_£‘ª>rŸã¦§@kÂE«¬‹»áÇ`@|:‚ñÄÕ’¹ 0¦‘ÊŽìØKAÌ-(žºà_|–âóÄ YêëàlMxå+_¹úbêN\�¶ ‘ê3ßæÏM³D׊•W¯™±ÎâÜe²ã9|·hHÎßT�E7ᦠ£ ÿG˜­Wwe䇈·à5®q�¼Ö‚Ý•ë²+`¬€°VÀ X+pîT ÷RàÚ/s™Ë\øÂž¡¦óL/VœƒÏqή@ªýøUø¤uø*h›~¬Ä¯ðKK~¥eãDÕ¬ …É1MxLoãa$@fÚá![IkRá5ñÔ¡˜IPXoȹ6Z=‹dI%P‡ /™¯t’©ž (Ck¾i Bnj ,퉚�Åß`zœðqQ¯ïÀOõßøFõÇ2[åƒsÆg° <ógVmÏïÙÁ�ÀòO<ñ¨£Žª&•ÀÙõµ¯}-ÖÒûær—»Láw¸Ãì:V«×Ä8øìu¯{ݧ?ýiž;X <_°ü°‡=ŒPõÞ&ð'ƒïŸøD~@@ßÍo~ó;ÞñŽm�%ißÛßþv*.w³›Ý¬�¢o[ᜅ[/•¹cî|ç;·±u(ö­o}‹Åæ¡!8a!ÇÜ6'ý‹_ü"=„¹‚öñbÛ6L^à8î¸ã#Í&ñ�uÖY\>�Tv±áŠ®xÅ+Ö;•u>üá³Õ4éë_ÿúõ΢|%�wM£Å{QJæ>ÄßñŽw°jØÞ½{³¾oÁŸ™¸Þ‹\ä"(Ï�Ã¥µö7Mò“ŸÌÛËa?ì«_ýêì0� ¹ó—PîFÞî,Eî¨êñYò…ùz‡;M°vãߘ‹ª:stÁµ  ±‰×¼æ5Û•òæ¶šÍ\ñ-žm˜9Ü™ÀMlî§©² WJ„8ï>_sqKÜà7àb« üä'\Zf"KéÆÍÉ›H>ë9ò¹ ¯£�>š7ŽeæØÎ‚·ƒ÷âb»Ø­o}ëºÙèëcûy¯¹7È¿Å-nqÞóž·íqIš›9ciýÍÛ•\d¬€°VÀ X+`6U¿6ט2 Ÿ[™e¾+Ÿiÿù¯Ó6Áì)wxc:Àô¼ã=GMfaúЇˆ\az87r…™o.'ÇHÚÕr&l³Æ¼ŒÑ2Ua†U³¼v™Î©M蔓fÌaçNKg›W“¦3œ‡CæÎ5gÞôÑ�~”iÛÐq½9íÅþòiW™%Á¨˜+噳3Âóâ={ö´ÕÚ4ƒa†ˆDø‘PŸÙ.³¶«^õªm�NzKÓóNÛ-�ŠQuùþ`òY[̯ä'\µ�_Mb]:4¶NUí„_EÉä',&ß3pš¢Nu�ò×7ö]À­M>lIΨKìÚP#“ÉðgS©†©}ÔM�íS!Â~:—m-è&—¶¨°aJUÇÚäA/l(ƒîÇZJNMU#~¬nÓ¶«DlÌ™ŸùÌg‚rÚ"Òܯ~õ«ßùÎw>ýéOoo#˜ÑãÿøŽ“ w*Çé§Ÿ~“›Üä OxB‚Õkf×ÜÁO~ò“‹)�É�°¡G>ò‘÷¿ÿý;ƒ|Ó›Þôìg?»ÍäS÷?q´™KÒhÂS€uøð<ð�œ[jƧ‹"@F}ιÀ·¼å-4¯&|ö0îg~χ°Šxèä `Mâá YÊ%Ã>x‘Þ5­Až¼�cXÉäWgc ÍÙ+³ƒÞhrÊ)§€Tªk<‰ zgžy&`åF7ºQ[Dìŧˆá10®ˆçWUÞq€¹œ%�ÔªO‚ñcq*3�ƒ`ˆ<(Ñ�ËgHU�ûøÔ)º=èAÊS *¿¦òÈbxhcÀU«N¢VCÈ|Þ”äB~ÿ÷ÆÚ©¼ètKoñ"#…Þ`‹ê�þæ¶© ˜ËäƒÆ[V—ÉGŒ·¾ê�à29*çOÿôOIóaÌ›-oפcY‡"èöýîw?1Ÿn~ÍT[z佃zßå.wáN¨ü% Œèq:™përãµÿXÒÊEVÀ X+`¬€°VÀ tØG'³=­o»—oµkcY£-5Y%¼5B¾¿'Á+Œ©ƒð˜HÖôáö·¿}µeNÇ,�)Cå�fHLOöîÝ{Ì1ÇÌÒ.&_Ì£s$ÙJÔà»ßÅ>32ÐA™Zž` <;²X­æãFº¨¶Œ'§½À8®ºtªÂl‚‹ÔÄœ ¤Ó äNwºSõ•maŽ,£ßŠ�õ±ÓÊÈ´÷îw¿{6ÙÆô|vœ«çaÚ&¿ê³+Q£¹Ž†öñ«)¸â‡èšàW6±.rÀ\}�â g ¦‚f½;:6›Q ÚCæ‚Љ¤‰íi~©&áÇ99d0*úŨ2$¼csÖ¨¯nˆ_�õäÔF‹ÐiÉ·1îwò|ÛXNæÂÑ™´ã{…ÙCò�YîVƒ�d=éIOjÁÇ3žñŒw«Ê$¸ËO;í´ÌY½&õq={Üã×r·4’¯0¸ç>÷¹€¶6|ÓánméêiuV†tÀ¹gÂs9yŠª2‡­* “�ðÀå._;餓ø˜ÍZ#“�kûñ¦NRÿ·½ímeW&Täóv™µÖÉaü/yÉKZîÆÃ¢ž¼�`,è^§U�¾â¯@„Ù˧ç5Ô-$àN›ñ«U½ ~5L~£«bŸQÒç~%hHÂhÔÃ9-pM¨‹¢ ‰ÞDì×T‰Æ#±°¤xÃl ¸#tTäÚ·ÆîêšÊ„›R_A¥„‘âE'Ï»t’ƒÒ4Bìˆ1˳N,N�Ê*¢©üît‘˜"#jü€8Ô-KgÜ(ÜýßÀ0.Qðl@xq8H(·PáSšÎ«9\êã à Âèà;Fš%!)]½&•¹#Ÿõ¬g¥M^¹S‰0%– ðGXk}øŸóœç@‡“*u€t/|á « œƒ`çø‚ñ1ÃQ®-ZžœóÉ×»èpökøE–rÃP9­/R">Ÿ|ÀS óëÛž2³'f4I»êÿþïÿ ż•¹gUž›¨ù „Î�1^dB6�&hÎÔ¸&Y¹ùø‘œp ‘†¤Ì³ ¦fpŒr¹Øáô|î°—gN›Ã¯âM/~ÊL´ÖÛ~üjüŠ¥Öb7„}üJì‹&Á¯0ÄÏ‘üÍPXÐLžgÊìL¢ÁËxÍž2h”¨U!2öO‹É+mæÓÅùüPxå+_ùèG?šô‹_übNIäñçþçÜý™þ£?ú£ý×í¸ÈmTœóÃÇ8y?}³è­�%W—n´§\Rg@.ö7Eoåp·gÏžânÇç– Ç?· Gâ³Ìä‰Ùaa ïž÷¼'O[~s€Ïª-oTˆ9È‹‘Ù ¡¨�Ó䦂=!l†µÒÝŠ\Æ+±ó·¸LÕïÈöw^•f‚ÇÖínw;®´òyá­<ÇÉ©›� i¥ ˆû-Q]5ì$ðýf¿ãÌäw³üÑí¾÷½oþ–Â2¿q™L>[A²k³§õ=OÆÙ:αVÀ X+`¬€°V`Û 0(,V"Ût! æzL|„2ÖÖ˜áÖò5œ–§X¦óß~¾¡/¯®‚弯ô‡€„hÙœù s«œ_àiQM@NYªÅ3Ó¼2ëaº�k11<â�Q¥�S¡B~LyZ§–œf}B;+Ö•© óŽŽ©¹§LL‘Ê�†j¸&0 Íé<#dz»wïÞlN¨l&˜×”™Ô¦N:10É-8°óéyv·¥WH•8Ù ¿Šêò+žÃ¯"†s¿J¦%´%ªÑ¢"~lй¾†k8OÐ66!¶F EYê�l³l³6¦žªŸðYäÂ]×c63ÕXAtü 2iLMUV†Lªs…šêèN}„œzP5ÕÑ?9ËEò€‘7ðˆƒû æÂŒºU|ó˜ÇhPkkxl8Iph}NÈäŽ)’ÅiÞF$’þ’àÀ`®'•§p ua¦ÕkÒ¼]ç‹5ÝÚû˜÷¡†JMè^vGL\&xåÑÓŽ–&|Zªt•Ûd5. FÞ6Á‹­®¥ªáÝšuxÔG«ZËêiXŸÏ*%=ép72ë ‚B&êj›¬’®ˆE0J µ’YÏ ÜÙê=­ $¨ÐŠŸEõuGù µM:é¢i`£|Öw*lé®Ä£¹›°À7 e§°Tå�€ôµ¿`²ˆGj}eQ ³mÕ¦wø·¦ò�Õ…UðÄ–»e­ë]ïz™@É$b Z/Ìæz‹»e¥ö<šÜ­Úók#Ó«¼×Y“gE&ŒÞJF'¬€°VÀ X+`¬Àn) «�ætµˆØûÌ;jÕNo™¬åü;0&,0U)ç N+àŒï×kj_!eDµÜ-H æ˜dW“pDVk_Ó ˆ¢ÜÚü6Í|�¬Ì�6¶üŽÌš;“nÇßZØ4Í\‰ÉfËݲ Óùšã´#¬‹ªÉ~uQáMЃÊÜùô¼L­žÀñk÷ùÕ°åW±ñh@Ô¨—0˜XÛd Ñ¨ÉÆ”>J¥ù—Ÿò› ‚&¯8RìE�îqª&W»0)–&È–<>y±�™MËÍm„'”N,NµtÈg8Æi!9ÀUpÍãÍåÍwÀŽ%ì†�ÙSžò”ÙÏLg,å$I~-@V&+?ö±�…øÂ¹ˆ"ì¸,­^“»³ýäàm„›U;˜¢9dæÇž«k/0C\Û&[MÛOÜÀ'¤"ø°CÜ_Z£B9Ö–o*䢞Dm§p–<åêf?Ìs}ÇêÌõ¾ìe/ã¹F,añûÖø¢4ßQdãŸKyöîÝË»ŸjƒÆÚË\d“üŒð%QÞ[K*óÐɧ9q\)½”Kn©Y¸À¼1舘ÇNóÎÝX¥ ¹ô#Û-íð-®IlA–òáÍ-ÿ²ÖøVÓ(SÒÍê–¿€±™¿5W1ΕÕêû±UZ¹Ž°VÀ X+`¬€°›*Àì5¯yMýOϯ'8+dDðˆySN™ºæÜ„p.x‘@ ?¸šŠæ\‰ÌúJÞTóñE“_f9Udò‹©?1‹áXt™s¿¼g&‚›!œ9Tf(Ì÷;Ó[&JÌ1³N9813T¬pQw«ç#r‡šÒ �§\êµÖðÂËÓÎ .3wwzÞö;›^�_…“Ø~üJ¼ªåWø¥ ºü ÙµjP/€o�ÂK Åÿm(ÐøL®o2-‹FصωJU)‹8P˜žŠ9•§[&U˜+±e)Ù²¤·[¨IýÌ÷Ÿ¡È*tP5µ’½ä(³ûayV¬šsÇ;Þ‘=ç®Ë_#Ü·2×Ó@�›¬E¿¸hrðacçJvl¨íê5[ O/ŶçêÀ½ŽÀ—%xÊÜÊ[ʼá o˜1Œ0)ÜÜ26�'H-7é2âiÌrlÜäwvbm[­˜fäzrÿìgyd«üÀ­h�ÇeÖ¬ç¹ wþÏšÕ`‹G ·t™+ö3 v«ÇÕ»«ëª¶+öîjVÀ X+`¬€°VÀ ,Q€Ùè«_ýêr`ƽúÌ—ùx®ÂÆì!n‰ÒXÌ'»#â�™f¢·šð2E*Ò„/[Ö¬=8Íùò’3™í 7é^RŸ±�ö:0/¶rdÃaˆe‹æNHñS©…¡ ‡Lá ñÉ…†–tº¨ˆÙ4“w`¾Yš·• wcqrX¯œ ¥ÓÞ̧¨}ƒÐô¼Òl:æœèº9¿RüéþüŠ0-\Ó€`ãõñ€•Ûæñ+ìÆ¼VD Ø¥~Ô¥XÙÔ M¡¢5ð—�0 Ç~ F�é@¥ãˆGUs(«ÎÓ%Õ£ Vs'Å¥²=îq:Dêäø�uêñ:Q§z”ï\ŽIÐjlê6’âÂ’ój¼(¹E !ò:ÛQÙ/xÁ ŠpÏV¨f×ÿôOÿôøÇ?¾b0³Ô‡Šã/þâ/2Øsõšõñ®^–$íh[ÛdÍYç�%ñpA‡$úæÎ)<›ôÆñNõûõô)GSò Ç"›ä—ëé’:UDØ,̾½:('OLžq·½ímg«! ¨|•ôÞÏs-Ÿ#[’½íhyš/¯¢î$•rð(Ä߸<Ÿ—Û©Rn­$ÂsK«ÚЉröÌ7zQ«Ý}‹¹yèny�‹Frèç×o” æ=ôÇìZ+`¬€°VÀ X�C\�ôw«Y¡‘+-åu1gšŒK §,÷zc¶˜3M¦'‰2ð ·ªV#0i*gŽB-wa^œóš Òõò& ²&hsßÜýp(›ë¹ÂZI\SÅjH¿„¬q0sgú¼|ž^­ðbÖOäÖ¦ó5`€�jÌ_ô¢�ù˜1<,d[ôê=:Ǧç�Ä„0ÂIl¿†M£•–ñ«Að«á£`q­Y~%3ÒŸ¶QÂ;¥áèU`­¯ØR dópNcW… tÚA€ >6¢|MD 2‡™0HQÇ»ä+ä`0ªS‰øU…œöG±?ƒüÛtÄf¬��6\àZÕ@–¢4€£¢'†¢1�ƒX¿rðYÒŸç¿ÿû¿¯]&—Ô¬">´gñ8ö1h£>³Â³ŸýløzB¢kŒË8‰¿ù›¿Y²ýÞ½{©ÓAK³Ãh ®˜æmdÀù¸!Ä=Ñ[qô={öÕn…e„ÎøWìnn5À"Aì8èÂþX”‘w'«�ŸXÛ’¶ uu,´àc‰KWùëµõ;¦vxÊSÿGœ„Q'çøÊ$�B)xÀ@ðüŠ]ð;€&¥C¶B„*â³¢©¬V¿xÊ/i¸»o1wN>‹y_ù’~Ç¢z#–ÿÖ</Íc¶VÀ X+`¬€°çˆÉÝp¿ÊÞYm¹ÆÜA2yLô–mx·å¬¤ÜqàD¹Â5ì‰=ß ti''Ý™.Çé‰'žÈ\ln_m&M²G¦í¬;ß­’†ß�ØVB0g2a>ÈBös§QÌÙYBýŸø#¯Y ]àƒÆ)Ë»cvöÚ×¾¶àfVæ™*Î�Ps]lRGü/£bZÝn,I[|P˜Ëïk§Ûs­ewbz¾”_‰Píǯap+üÇ–ð+üØR:åWá—IËíU#­MJ#GNnÚ„�\<¶1÷â$XX,Ó† òŠDÉDäØA®qTÒhåQ_=ÐX‡7 nza1Œ«Œfª”0rYŒxWa8�ª¡±�“ÇüÇ´Ü -våöeQ3î×g>ó™s‡çQ«�zÔ£¸Ëñt#D6T5‰Ê®çÂ*5Û}01Ò&N»¬ÍM´ëÐQaõØÉ¹Ö*“åÕ½ñÀ)7¿ÈÒή|�2ÞL¶‹è-ûBü|LàgËÖ®å‡SÞôÆ3?ÇüèÖ3º.­åÏ5÷«ƒª¶óÄÞ8x¸#é™gž™ß–pÊ3tîZ›³=âÛÌs6=@¯X2�GpE4ÿ×ýWÍ6\’SAø¯æÙ&»ø×û‚ã–¼ gGuæTH5¿lÁáyHVÀ X+`¬€°VàðR€É5H¨æt¸wUÖ–.„Éc†ˆò`pxleó²FØi¢7Ü&p«ØÒvÆv>þç¿Ê,²‚ÒÊïaKÃf‘à5—¢‚[áôóšk¯ôá@.üÝ`pjÒGùÀ.íPúŠW¼¢€P‚É&¯é¢qòÉ'—m¿ÌmÓ>Sâ�ø“?ù¼‘HÜÿþ÷¯¡¶MΑ鹨Ó2~‡ÚŸ_À„ªb÷Pñ«ÞzP¯yüJ7H:¹© ‘œ :‘/ýò¢¬à_Ô"lT0L˜ ˆÁÏèCþxÂ`�Õ8MÓ«òU Á±Yª†$F'G©|ÝÔ…u6u®ñS#úPˆÍ‘ ï<|è¢#|úÂÌR�"#ôè| Ø-Ed0DƒÏí¿¡üÐV)¸êØc�­ÓrI[½&ˆ½š³´Y눗ù8sþó?ÿ3köç)>P-}c!ÿösÅðžøÄ'–Á-%Šsñ¹ªý Œ#{öìÉü˜ªZ§În�²ÇBšâÝ©§Þ\ã,ê—ù<©ç.6Ĩ†õÀ­œ—`ä<ÈÒþ¢ÇY¹ÚÖ0êþdŒN0#¿�¸E«ælbÖux¼Vþì¶³#»ø—§[îýÚéh‡§�hÜZÛjs¾,JIy‹Wùl«ö]ß X+`¬€°VÀ œ{èp·›ßüæ³SÑ-©Q³ž\¤Œ¶�i k.¥A‚“2Qó ê¨¦Þ¸§ðÿÿÊ_”`^P‹5½ë]ïZTmÓ|âIëÛ}Öt[emúË^ö²5ß\âòF×D¹å˜¬U/5¤¹—YMðæ«¹j5™MœCÓó�ò«Á søÕ: Ø ‹É8èhÀ¿€a×$qjS5!@–u“ó¾v´MÆÙx4%Ó¬>ü‹Ô+(-ˆ)\Ú@r™£@KÃÑaÁÕÔUò50âTã!OùxÖ�6Ð(²lc ûgÇï餓N"2Í2�õW…Wçì€R¬ÕÙ}ô£M,dNûáb€ùªœÑ¦«×¤ácó˜öƒñw÷wOyÊSÞþö·ÓÅë_ÿú?û³?ûË¿üKö`‡‡÷¾÷½ÙñÞÕ#O(x3ËÏáÄÇÒu�|ä#;ÛUsÓϦ|L C2h×l˜'!±ƒ¹­MaÒ>²úüç?¿u*Ü´k ;ט¿måÚ†fÓ€¾[Üâ8¾f[Þ¾ ì/Sì_S›ÉâLËWU´‹ èÒÛÞö¶Ž Ø{e/õ§íÕW(@C}>ø` ¦ë@vÞMîÒW½êUY'mξ‚b; ËzyY“÷nö=íÙÅ·¸¶¹áCÑéeÛ§aë7*•Kã¶Ínµa½Ñå$¸U ®o¬€°VÀ X+`¬ t¸ófÖÌ}f�¹`h®†µT¹ ´Á¤4©àÓúF¿S�:¬Ið Á™‰XEÂf�p(&€,DÐRæ0U)ß&)oyË[ª÷¬À´;4É%á2sîë=îq�Z4 Gœ¶kÈ^;)˜Ö…̵¶‹2Û‰ŠåYA«¼Û&ÅOèLñŸÿùŸlVù⿘(@N�ŒÄKѼšœ#ÓóÍùNnqìǯä£üjŒŠä”_áÇÌ[üJ�£So4fëâaoQÎgŠ%‡J�¯‘pš6WÐ}#’7¢…ÎhÆOeêß0p™¨ “–ƒ)c 6u FˆvË£m/ÆG™<ß²6ù×i`Ó7AÔMgTØØc"z:è/Ümìr�:5Žå£`õ7žÔaÂÏ~³•‰ Ì•¼V¯‰\1ÙõÏxFäSÄQ§•à#Ê“œâ£÷¼ç=¯6Xàîá _XÕ¶�à†‚¾Uÿ<Ýjo.'5\2˜íÑšÐ5¯xáò Çï—~ùz¤ð.le¼ÝoX™^iÔä»V»ãÛ’tf„íòÝÅž={x—¡‡@ºö‰VÖ: Z!>_bð^à€Ù‚ÈÜC£S¿sº‹o1}¬1 ÆÏÇ­¾5êô¸¥S<ÃsÝ:~]±ã8äš›�̇>ô¡[²³ÃÊÅ…Ë×u‡ÝÜ X+`¬€°VÀ œ;`}±–øh«Î�hèÃj<…Õ–È…Ï.5c&ØY©œÙ"u2ô;L»pëÄ<ŽˆTòU¸ì0ÍdºÊD˜U׉Ëú¸z0×K—ÆÆ¬6ûÅ‚…­˜1¿#23-Úuúé§·.5�~9e~} '0ßaÈá¢>ƒd‚™ãaƒDœ*X˜ˆ¹0ÕDžéI¹¿Íš%‡y"³Eà<ÐaÎR™~â^Ǭmn&¤­˜Ìï˜ùr%¸R “w†qÏ{Þ35¿BUbëä)0L;$tV¥/(«åW0€V°³\‡�Ñ\„.BJUFÍÜyAMÈ ˜lDj_La®T†½E¼ÒÛLöä ­¹Å´†�q�}JÇú;^—ÛxÌ@ÊŸ-,¥}uüo ý¢Æ1u�›û¶¤ÌÇ=îq‹z‚ó9ì”–?j'?O¹·þáþ!ÃÅW¯™mï|ç;³l\ëû6Ûìéÿý¿ÿ—ù|�H/ª�S^EùÍÚYžÃf m…Î U„§.ô­¨OI¨uˆî­8xBíÝ»·š,O‡Âw‰ÇÎt@4ÙŠgÇ*ÎÆpCö¦ÉQ1 x  NZÜ 6J,ú"Ý–�pÓR¾u,ö�|€§_GÔ³—éÖë� +Y¦YF˃;k2`a|U� •ã Ès®žãéÀU,sQžì˜â‘Úr7Ì‚S7½*ìÖ[ÌÛQŽoíî׫ŒaQ�v+~Ûq×Q“ë-/èE w1Ÿ·»­[¢º‹]Ø”°VÀ X+`¬€87(Àé·´yàêñ4sŠŒÙJÚÎX;`®ª1‡ªåÌršÉÌÒTÜ�‰ÛqÇ×N3YJ¾¦lLXt;}#Š»�Õî~÷»W‹@‰c7–·âCGMpaÎy™2∙Ã-€±QÊ0X»m‘ÁÌw`$Ó8Ž4ÆÁ4î†å¹X“¹OQ9‚~ðd€ÔŒ5Mq¥,^ϨòôàOÏéX}o 0ô˜ò+ˆ”üÚðuãï¤ø›âʶ¿’—Ø ¿ÂÁ,ùUFr®Oâg—R›êÆ0¢Ÿµ³•7ý ØKF'§4F¡A†»\¬'$G êS ­ –†™¤*± ÙD¨ÒLÜN?Uw¬mÔLÝP 9‰Ó¨'4aw Ýâà&XÅ,(„èÎg=ëY-_Ç+ç�ÿø�q+ý<ýéOçî)S -œÚídÕ°ºí(åææ“ÆBƒE^V¯YÆ¡oødþÛ¿ý€¼×ƒ…ö;ŒL<<ÿñÿ‘[’aà/gÉ6˜:ªi¼¼I«¯% \ð øùàÃÈ’H:>HÐ>ç|AEe“CÊnu«[Ѽ2+´r: �ƒgÝ:ž_õY¥…©¹þŽ…<…÷ñÄä½ÃÁªFÅ�èN\ÃTçé@+rò!5×`eÎ6¬¢Lp�l=‚äöà‘ÚÚäûHåìW(dÓ»-�P“{ Sà³»ÝínQªŽP åù˜¼©6¿ÙàY‰(‹qs$“›“o‡fÇPÆg[z‹g›Wd*)*ðö·¿}þž tùW1ÕœD{™œr?7?ùÉOV*ðqÏž=³•«N'±zï�†yÊç.ßbHh²Î¹Õœi¬€°VÀ X+`¬ÀrÚÉõòš[-eâÀ:N´b6×ñ2ISLUðËèl´iÖ¡-‘CLŽðSÃM¬E̵÷îÝËD¾HV6!ŸM˜2À³:sCfÜÌåé·¦E4Y27Á­σôdˆ¿Í™ó² 1@5çÅ)boŠMy€ƒþµ“æÌ§˜3¢Fú|Ô¨¸Ø\vAlôCˆ+F^ÆÏÒÎX¾Û˜ž§€Û{ú"ðRaÂS~%&¦<Íÿe>¿âÝžáW¸�‰×iûµIÃ[­×ÄOY"›4¯"]½µ£~iÉÅÈgÊÉ¢­ œÖz½õÑÙ€4ðŸ @ÇeʱNãÓÅzÃ~Œª»7àý6éç[ßQ+u2�ÑÅ`èë€8ÆœTN~rkk_úò—ye3^ÏÁÜþàþÃð´éH�Šû >Å„v^›wÌ6\½fÛÏ/B/ùHƒK€åíg²­–iº€h0’Κ‘³5\Ÿ7PwR,QcÅೊ5^¼›>5–ØdT8|�«Z¸¤þîñP¡‚Òø¦búé]пi¸©ÃõÖÓ­êò˜£”{SKHhÕo<øRÉ‚Âmé–Ò;|‹ÁÄù[ú¹[>b|@ø ò ùêüªÛÒ¥m¯2!Þ¹º¿õÂÛž5·²VÀ X+`¬€°Và°P€é*4¦™ÌÎVœ†àÁA„sºÙßN®šI3>,_Å�¶jŠ©(S*Œp-Kæ­Dt¥ßŽ ·¾õ­çöR3¾EáÀtzŽÏ £ºúÕ®.ê¨Úœ_‰]ñòW\È¯Æø¿µüŠèO(8k@/3°—€œOÂi ÚÄÌæiZàM{,h0Z×mÐPIlôÖ†Áñˆy�ˆ¹q†£Ù$°TöÅæ‚°)�åÿõX‹éoG4ª8›Ze—ÔЯC¹úQx( ™-Ñ„! šcÓÁ¯^³5#ßt!üªOËã[«æ�KðøhÉ÷;ÂCm‘¯ï–,3ªòòÝRÃ]© íZxñˆ\2Nœª8¶7$€Ý–ÜÜ–ô²Ã·OÌøÃØçK'ž×Ü´KúZ±(YùŠ•w·ß_%wCár>ßÝ.lÍ X+`¬€°VÀ X�CP¦«åÕµâð n+VÞR5\pVá‹l2]e"�ãQZX„Axå ¸h.¦ç}ÜЄ¦€S@2±'Q/Îôw‹ü fµ¿ïÆ}íë(Ó Õ”±x[šNâ6^ÕMŽo‚aáÍŠcú«`Ò@rä§cS \²;9¬MI[�9­ûF}°žXq*öL“¯›.$Ç8Äç¦U™•íT¨m!@@=9C’T-VÀ é €Þ2ð™oTðè>Ü/·60Âå�ßv‡ûåxüVÀ X+`¬€°VÀ X�E Ýc„ÁUmM"Ùç43ñ>ÉåïÛ -ÝáW¬¦àS^‹_AÁ¦ü ,¶¿Â-ˆXò+|Å:üŠ•Ùä¦Ù€^½ÆÚÌ2–‚ˆö¹ƒ13ÄžV.S *ú’ jß[—3Çp2fû„\+MX.µÃ ?d|2L·6.b´AÙ(ÒtaòŒœKÒ(|è4: RÝê"dχ°ç øÀ³œûØrµGÀ‡?Ê|ç³ÊîçŠ7Øi¬€°VÀ X+`¬ÀªqKlã€ó Ö˜c/Aø³!œÝXšœHÒ¼nœXáîœÔ ¼Â6üÛĦŽ5üŠ0R�O`ªÃ¯´ [@¯àWª2˯È‹íÄ&êµ…Jß´§¼-bCã|<è§Ó𢌱Édà­zƒ«ÑBkÈEc0Z_W@ü‹¾úöBU&�ÙáaH³ªž8 L(î4*Ãë4Ûf«<ÞÒL\¬Ê}X+p®P€°hvºàuïÞ½‡û?èAâ÷ —s¸_ˆÇo¬€°VÀ X+`¬€X®+ Ýï~÷cUV¬£&nlèW{úe[‚pÙR�hÜå¦hi -ªäWbdóøÄJû0,ãW °³Y˜�I…ÁðLYÞ¢!û“øR,«ø]œŽz •—[ô òFuüîßΞŒ‰¥­`v�±!ŽoÚ)A4Û4H££Ài¶TFVÖa}8jŽv“ªºйd…é’>cÀ2HuAAVÀ œ[`ëÕ#ãRù>‡Ýi�ŒkñUX+`¬€°VÀ X+`–+ÀZuø°¥Ã§>õ©Ÿüä'p7wÃñ�|v½cgUviXn᠔ʧM¸i)¿Òf ů U8¦ÍáW¸¤õ1Æ"tTÕ9´Ç(‹§åÞ ârZJ�.è;%oÔÆ'N‘F((œ C~pÈTŒ�øU˜ž:¦¶ØY–`#®@6ÅèTW Ö%<ã¼&$Û)dUeÆESU�²ÇO[,hT#Lø°VÀ X+`¬€°VÀ X+`¬€8´¸ô¥/}(» ¼£šÏ¯²L¯«ð+VMë±oàm(~5�_á³F[ÚSÆÚoò4ƒÎi;t. —ìkWR€‹¹)ÄU°N•U%1™: XGþkSQp¸$i0=,b�Wz>¶MÕµa]0mê0;8ˆ¸‰Béäj'˜¼¹žœÚù°VÀ X+`¬€°VÀ X+`¬€°ÛR À–øUp®ðK·±àW¸Š‰�±[ñ+Á/!6ðÙ ¿R-ñ+A5HW쇀5¨/´mÓ(q5Ë<²U™rf O¸ñô¥-4¤@eS`Æ. Ý•AužÄè4~-ÿ&¿6YÔ«Pa¶ÓEi â{øÕÑ—¹�•¡(¥#¹¹QI!°"pº6VÀ X+`¬€°VÀ X+`¬€°V` ¿¨Òâiûñ«äR�É6øA âWÚÆ`?~5!Fs.¿Š}K1DLão?¢91×›º¯Eï\†èÝ äv¹„±*a¨áí6a›4Àù®Ö”îÆDßëâk,-'„†��öT“ô9Ž&Ú^�~¥ó-ó+­¼&ºµ�_±«~uì¿ !ªPÄŒÍLÙ©49áý¦øQí€ ”ÏÃÿ�JxÃEÛ}Pmm‚ŸœøXŒ³øÉá¦᣺ì &*j6öW ª¦~àuÊÞ)ŸB2e¨îÛ4!¿¸v~ø°VÀ X+`¬€°VÀ X+`¬€°ÛV`ƒ_­vįphƒ\±ýÂþüJ¨Z“�nä©&¶ôZ}£¾þ‘ðêã 'Çi²¯p• 5 uÆì©€«¥á1'r¦ÎðÚÊ �GÚ]›ˆ0›êÆ_2”|ã'':Wwj)áX�N±§ÁþhLÃŽ•ú°VÀ X+`¬€°VÀ X+`¬€°ÛQ Ã¯ä:–0.¡˜�ÕŽø•èØƒÉçLa«‚[b^Ñ‘�\ö(ôEf”R)vA¶AÖ¨¯*ÔÓnÀ™þ�%–FŽ*üÈ"% Ç~DÔÐÆ¥¢tÑ�Æ]ƒúb; Uç±°jMc׆°é+`¬€°VÀ X+`¬€°VÀ X+°mQůrm‡_iKƒ~0ÃQM�â`¼b_ë©ÁÈsâ¶1þo"cá'kü‰&ŽˆY�Mh�ÚƋ¹ªá½¦ºt­ʧyrá“{¯lØD�ê±?*È-ê¨a‚7°=û1ŒY­.û•VÀ X+`¬€°VÀ X+`¬€°V`g ìãW… —Ú¿šz™ ÛŸ_ Ø– ‹óø ;çMø+ð[ò/ª ÃœÚ‰ˆScë±:E±BÒØ-Aû5ˆ°ÅBm o]±)îß6*i×RBpi+3¨È-Æ@/tBÅ@sâø×Å€Ô(ú#~±VÀ X+`¬€°VÀ X+`¬€°ÛV 9Õ”_‰Niiµø•¼Ð©@`]~%ÒF‘b<§|-˜@ Û¹ÈùÂ|äƒÑòŸØÙÚ@Û¥† š~LÖF#<Ýpa£ŠBaiO„œft©X›FKKa36yàUDMÎm�å¯õÂ#..TÄO•Ë!»Š.‚¦ÓöXPʇ°VÀ X+`¬€°VÀ X+`¬€Ø�@¦}üJ¬LÛŠ®Â¯ˆÞ\į W ¯äWÖ„è‚Ä Êå^ăB¼F?*[1€È(:b6¢K¿„糖Á09Ô•bDôŒ,Ög·õiF¸«âDÝT!üàØô�Ú™OIè’±Q0&ìT¨-^•Ê.²�Л+`¬€°VÀ X+`¬€°VÀ X+°C„¬àWÉžVâW­úbiůrù¶9ü*°d�A&� g9�7xœE9ðf“"/~§±×<,Šbj“Þ0h`N¹rl‹DzÞ©&Fc \†<å„Þ¢Á á©Cy¾EcѶ8Èa©7µ§Ö¡J–úÕ X+`¬€°VÀ X+`¬€°VÀ lGXò+sŠJ­À¯D´:üJíæó+¡6Ë"­1N&„ŒBâDÉHÑ«jq˶ 9añ4 ›Êuàõ6MŠ‘ËI][;J}',“K�ƒ2™Cïè‡>up@¥)Qòqœ‹ËQ�t•³ê|X+`¬€°VÀ X+`¬€°VÀ X��(†ÁÒBi‹ù<*ù•hV°©åü ÞØ ~%¾¥ôSÿô*8IQ¯^:œ‘›„ÔèLNhý>ŽR¥þp t˜<ŒÆbj¼ö-K;šÈ¤ºáŸ|æ4a;ºÀÿ�2m£Ô!·= @­ôª´[ñ¾ËdÀ;eú°VÀ X+`¬€°VÀ X+`¬€°ÛV@ø)HÔ2~@JØ«áW£}üŠgSÌ*[�Š_©~:ŸWv�×ú¬Çì‹8RüÞ´G©(™V—˜AÌôWü+è™,ŒÇ#•ѧüÛð�ëÅZoªçr\L–¦eBçxËɨ,a†Zb|éÞF¾ ¨DO¢qœäU¬Ï1θYóa¬€°VÀ X+`¬€°VÀ X+`¶«Àjü 5ï‡ ~5H~%”Ƙõñ9ÍIDATÖåWЪ�KƒŒ‰½õX× /ºØÎ�sm© V§Ï| 8¦Œ Sx¯ ccu"l†7[ÖÕªq‘$n&ÈFC h 9a6mÞ ÊÇ¡!¦ß›r²†®EÔ<~ªŽ+`¬€°VÀ X+`¬€°VÀ X+°mDš¶Ç¯àmûøÕ”pÍáW`,a0A0Å´Ê�¬ÇBnáÄÆ‹<àäêd �39›M$ô¦•É­Ží² n‰%w<æ™^9š=Lä…§.שK# {ð8òÂ1N&�(œBxÚøAé�¿±UjdùÅ X+`¬€°VÀ X+`¬€°VÀ lK`Ô¦üŠ•ÓöçWøžÂÚǯà\`«Ñ,¿<“'Ü€ŸÚñ@M-à»�rˆ°Ñ6Š`aÜÈЊl�Órbrj8Ä@øºQ¯O )y±9ªjrÂÙLYkç‘}ù³©™<ï¢f4ŒêZ n¬29Þ1Òè…Îr¹8�·°VÀ X+`¬€°VÀ X+`¬€Øž}¹œ_)Ðt?~G <¬ÒŸ–_qÚåWmB_¸©�¹äh¦ÆB]4ÖJo2ßÓRpp7òÈdõ6þöEÙ nj"mbx¼¨šÜ×"¸5Òdr®?2П®§4ÅP5Ø¡Êi‚ -W§cÜë�‰OUtª€Ÿ�7ö_H„Õüb¬€°VÀ X+`¬€°VÀ X+`¶§Àt—XÖfü*ˆØfü f5ÚŸ_Éa ¼†ãZ¸®É·�½¬Gx®tKx¦\u%9½Ê� êqÅ ÅÝTJ@©XžÚÇkºÌ…M®(h�à�–”£­bc©;Pä©\à‚%¦¡©ÜúzA»�êÆçà X+`¬€°VÀ X+`¬€°VÀ lO  WáWB^›ð«Q¿~dáÉñÒòk¼Œ´ÌZ ¶ˆUVüTШÂ=é_¾rAãr,Ð3:Œ Uׯ1¶!õ†2ªBy¼(Ÿp3.v˜�–bs ƒÊ±Hœˆ]8Ýáë&.›«RFmoŠ¡!) #lJ”K˜÷‹°VÀ X+`¬€°VÀ X+`¬€ØŽ�¨%ü „%ç°UùÕyfù•ì‡Ûš¨˜ºÒ n $ͳ‰¶ågd“+æE<Ó4°à_d�àäž§ý¨ ÈSÕi+Ãì+:U5Õ†ÓõM•°‚£}Ú‹MT ¦0�†¨¾¦ÔMÜN§¢+`¬€°VÀ X+`¬€°VÀ X+°mDÃø»€_%’ ~,j«üŠ}F£±°Œk‚œØš@Žj 1ØÛ0*iÉ8–e£‰<ÕpF †ÙáH7bu¶ € i•ÿZ4<ÓåÃÓb·`|ñÈÔšq@4q4uKƒø;–=1Á`€AÙÒ–°NÞ€>¬€°VÀ X+`¬€°VÀ X+`¬À¶Ø„_�šò+aªâW€±)¿ÂoMµæò«þzî¯?n{%‹cÕ5Î`q´i£ýÔ †FäRªUØÔ(Àš¢Rárœ:ÐáÞ6ƒö„ÛúZŽ?Ú-‚èT²ä‡ß€M=@÷èj èb¬Ú(µÇ®ªì¼šxQ×´ ß:]N§�vZ¥�°VÀ X+`¬€°VÀ X+`¬€°«+ |< ¯± ~¥Ñ~%ï´†_A¾hüJôl.¿¢ ‡|È(£)Ø´?kR2EÞ[zTz¢MFp4-ׯ¤âxìz¸M5ƒÃ ˆõ§O#ÁN@.Üã‚™)Idjø¸q- Jwã/ .¹›È/N(dSU9ßéRc *.‰Qù°VÀ X+`¬€°VÀ X+`¬€°;Tä$ô_°'È˪Á¾D¿ø»Œ_…sØd2ÔÞ¥-¿‚s u ßMÁ5åU&×6…ƒÊ¡MÛp5uX£­Ó¡�Z³¨5´Õ‚ОpXȦ Â|TÔbn7´S�¤¬dU8õ¤R{ã1HNÕ…Õ=Q¯cŸ¶j�[!o‘Ϋ֮8Î¥²ÇsL&üj¬€°VÀ X+`¬€°VÀ X+`VT #)“c%:Ûǯ´pš¼À~�ÒyïFãÞ¤5ïÒñ,ùÕÐ¥†én&N&<' °�÷ >�¡ xÉÅ-vQ�Ÿ\x£�&=âBҸAò3`�˱ð¦¨Sy¾é:„Ö‚§©ù@åä)4U®m*Tý¬G]N4(µ„ Æ¡È�nE]Í X+`¬€°VÀ X+`¬€°VÀ ÌU‡µàW0©p[į‚Uá‹Öð«Á,¿ÂgMÞeɯ@Xº…t)+¹˜Ò™M·!…ŠQKq¨ð®Ø®4yp ÿ8:•ÅÞšP•Âh,ø]”D—iz™RD’y¾¥(­Wœã .áb`¹™ø�bb)WÌ+!«>¬€°VÀ X+`¬€°VÀ X+`¬ÀNŽ%"–æð«µ1 jØáWøŽõã©?TH71E‡îãW¢y2¨Ÿá‘çS”F:ÜÍÔ\�N+óC Àdà9y¤�ÇÞ\ã f 7EšR¨&�@eS‡5Öm“‘4C’IþŒŽ‚ëMØDÃýd�XR3þp¨[!ÃXFŽÑ§>¬€°VÀ X+`¬€°VÀ X+`¬ÀX�_ê)~¥í@ð«1¬€°VÀ X+`¬€°VÀ X+`¬ÀvØ¿˜’[\ï´«Á~¥(Ö>ñ ø™�€f}j†×š²Â /6¹ŸÅŠpâjlqÚ�Eà§úÁëMXë­¯ ÖØ$Ay8Q°ªœëDóé³Ç &“뉽Aù’§Éc/ˆ¢àÜüMýæp¿#O²>¬€°VÀ X+`¬€°VÀ X+`¬ÀŽß³$XØY�_A·T}­?ØŒ_ÉmM6c| /A8-ö†?þeŠ%U‰¨Ü”ÄqÎ_Ù¥‹Ñx ±Kd'• 8‚‹xذ»AÔ�ºÊ¾¢. ¹‘�7[ÔW¼,œ�JZÚ�V±þ\Ö§’®‰ ±Ä ƨýb¬€°VÀ X+`¬€°VÀ X+`v¤@ð+y}Íò+¹ŒÁ¥~…Oøk~Õ‡_É­,=×”â|$´&vÀKôN›¥&PSl«¨˜Èš¸éÀd줪jŒDxL�N!6EÌ28Ý"U-{@;jP-Oó`kS�Ò¾*ȨàÝ�ÔUê\‘¯¤}X+`¬€°VÀ X+`¬€°VÀ X��)0åWdòõfkø`*ø•0š|Ãä 8N|¬ø•v ¥!›.ˆYmð«q¦5< äò¶% ¦%ð–<-�šBPã�[š<Þ¢^p‘#J•6˜¢&û’æ*áy‡Q9ýeÍ:zŸkçÇèÇ_j*U׆Õ¥qà¸� FrÊÓ.J£Pw>¬€°VÀ X+`¬€°VÀ X+`¬ÀŽH ü ‡¯¿"04ùUø¦¥? Œ÷ãW"rÀ®ýù42¦Ú"_ÉéÀráÔ&¸}“�@m­7À¤†ØG�AÈN'Cá3ÌôYëm@ñH#Ê€UÁ´àd´£ƒø>Sƒ5m£J¹b[5ºvU÷{4RŠÂ„~8Ï Òc’–9õa¬€°VÀ X+`¬€°VÀ X+`¶§€ÐØ~{ŽNùUl(º�_õÙœ@Ѩûó«ˆe`"eê ´¥¿/¶:€{ uiÕpJ£HyÂE!mjÊ1 Há Tê†bpÚK5Špj-oµ@w2O'Äžª™Ò©è›ê‹¿å�‡V§'—Ï)­Þh¤&úçà X+`¬€°VÀ X+`¬€°VÀ l_�EüJ¦rÛ¿ÂïL ŽAmü &§hºÖ_¯ÁÑ(ש'½�‰~É-˜ôLëº ‰ÅP´\Æ›R›kŠE%î5ÉÕ-Œ¨¹éÔ½ŒêÀð¸vÔ*üÚhD ÕÊq¨í8–•£w¼êTâà X+`¬€°VÀ X+`¬€°VÀ l_�Eü*i×b~막”uùÕd<ÒÒoüJ„ O2þ¢…ÿ±§p·)x£nà1JuÀÈ¢� hrŠS7äÈN5h „#œ6–“›°QÜ+{¦â2‡5Ñ7ùÁ©w­ÇA0+ë¿©TfâUåªCVg±Gƒêû°VÀ X+`¬€°VÀ X+`¬€°ÛS`ʯ"H³Ë¯"F´Ë¯ØI4ø °mð+1,pÕ”_õ3øsƒ_É«­7�×{ëAÇ@`¶1ø Ï2™*#˜tŠÞÈOFBe‘ÁËÒ5�‚Éx¨®Ó3nílúW¬vLUPªª RádL[E¬ °iu¹ðßË×é@ñ” ðÆàd†ÎTUg¤ÓmŽ„+`¬€°VÀ X+`¬€°VÀ X+°mØ ØÔáW1šüŠ­€Rt�û!$pKÐ&~¥”Ú~…sõ‹_áv¦FÁºÀmªÎ©˜¾e�8Q/ˆ Oy¦i§Tª ™±¬›¶`­ÑlDÌ(Mc0�ÙpdÓ6Ú?!•åD'¦¦¯õãLcQü•«žúR<ªÜà„êh©áe‡É ®)Šüb¬€°VÀ X+`¬€°VÀ X+`v¢À~%.6åW�²„»‚_ Xíǯz#mİ�_ÑPngü §5‘· -Кÿ²Å6¦žÔ°k²ÎZkì‰Êæ¤ 0x�dJ.m”²ÿ‚\í`z:—1²t^#‰‘ä}yÀ Tå¨%¼§�S5GÄ¿/ªËLŽ^¤��5F˜vs ~µVÀ X+`¬€°VÀ X+`¬€°ÛPÒÕò+á§@c ¿Úp[À¯Dè~Å l¿2ÌÀ¯d‰¢…sÛ€sbH!j UxNÐ�Y¿M5�lrk#'0Y¯þp ICFÃK-Îh=Eda‚pÙÞÚP¥êEd0NÓÈâLàMvô— ªÂ?Œ¨Ãh8íB6¢jú°VÀ X+`¬€°VÀ X+`¬€°;Q@¨+0”(\Ð-±'¼Ä0ª¢Åü —58W¨ÕáW�²’_É�€Ñ>´Mµ1«BÒÂi*Ô_q0~Än l}ª Ó€dÑw¢@Õ‡ÍiXT'½“ âP©†âL0mu*¶&ƆÏÔ0e#¨[\Y2?jÓ±®E%jɨ«ú>¬€°VÀ X+`¬€°VÀ X+`¬À6H~%ÿ²}ü ƒÊPÓÅü*l¯€eÐ;À›~ò�½H„·™è�ãê¶Æ& �ÇCª Xò0Ò±qoåœDìèt%·l‘›+ÈÌúúÚQâs¸Üá7—.utˆ©8QŸB…ùSnr2Næt kÔ|X+`¬€°VÀ X+`¬€°VÀ X�m*°�_a`¿�Ö†x‚uùnküj4YÀ¹xÒ¥)º“ ñ¯ø£a‹·­±�9ôYãM^ta�˜˜NÕp2*ðFUmú@šºÓ…¯šŒÅ.´�Õæ(:®Ã4ÐYr7u!ð†ÛžF3ŠQʈº“iVÀ X+`¬€°VÀ X+`¬€°V`' Lù•v.+~5„LÍáW±}ÁR~º’Ý¿’È›8T‹½OàF“‘v] O¸m<Òž¹Î›¨Zp/:¢D©ßZÕx­è`úãÉ^°´Ñ˜˜Óè@á¯êO5ΪC;7(In_;DĈhªßh6]èMýG’­ÖX’Žaú°VÀ X+`¬€°VÀ X+`¬€°;S`ƒ_á‘¶˜_M´J[ñ+m† 6�_‰­áI¶Á¯hCØèXX�ŸÚ•Vª!8198á¨Ãµµa¿7¤(y+¸á}Ö‡�×”¥mMÙJaŠÓXÎ�á°1ê‰ã�ê rŒ‹"Eª~“g¸Ž#¶_� Þ¤7’§[”$`£ ÕøêÀ§V$|X+`¬€°VÀ X+`¬€°VÀ X�m+0Êí@M ó\‘_ ÷ñ+xÝ :Ÿò+Ñ8"L“wñ N‹ÍƒQ 1¼Ê‚ã…÷\8©õ´WŽö=ÕZïõŽ%ŠËq « Aû´'B’1í� †'_· ÊnqS ZH»OüÖ&:UšºÐaL¨MÃÄٴⳃ–_¬€°VÀ X+`¬€°VÀ X+`¬Àö ‚= >-âW‰¯“é8’_«´8[.Ìžd@+pWñ+ù’‰¼i#Ôô%×Fƒ©Cjª@Tª®­­ ª‰{ �`äi‘8ÎÔÁd}<$¯6­§>ak &u#|T+À �Q[ e�kâŸð¦yäkÀå\�Mh,´á\[2ÀýðÌóa¬€°VÀ X+`¬€°VÀ X+`vC1*!0hÕ2~%ß²åü*Y~Ïš¬ËÄÕø×ë�Ž#‹lzf›;þqÀÓøä,Ø™ ”L¥£Þ`¤š »XN¥A•´hÜÚzÄ—²YH"ÉzpÓ\]tP—ô£¡Ò'Y¼Æ?ø°VÀ l¦ÀúúúÿýßÿmVë°)ÿùÏ~،յVÀ X+`¬€°VÀ Ú ,ãW#¼Êð“oYþ…_Ê’KYø°É=l9¿Â#Ž0R5“·iE�Æix�)¥%àt�fªâˆ8%+w1ÕÞ¤âl˜FNø®‰Ži0½ÿ7ÆÔöÁÏÛ¿ªZ¬üÖ“ó\ Æè6üö(â§–ºpÓâp:SOŒ1Q¡šûØUPø›ßüf™¼üå/?²ÒŸ+px+ðþ÷¿ÿsŸûÜÍnv³›Üä&‡õ•üêW¿zÓ›Þz{ÈCr� \à°¾Þ X+`¬€°VÀ X+ph( ø+ŸÉ' Ž6ï‚SÁ¯TŠÖÔËø•¶dÐF \¬le AÓ†¥k±-)x¦R13Œ'Í“E8j@:u ^z#2U Å1þÈh6Å©Ž4õpvãGF´jTÜçúôEe‹³EW\3-è!zU[ñE?ÒÓJÈ®gœqÆÇ>ö±YSèréK_zÏž=W½êU/~ñ‹ÏV8"s¾öµ¯=êQ�ªK{Á ^p¥+]©N�€ÄYg�õ©O}ê|ç;ßÑG}±‹]츢ö~ó›ß|á _ ç¾0·n[´¥ôøÃüà¼þîw¿»Ä%.q©K]jûë_ÿú»ßý. C €�Ïe/{Ù ^ð‚[꺭Œ@06aLW¼âWCÛ¼Òà*¸Ï¥�|ä#‰Þ¾ÿýï¿þõ¯§ó‡?üáGuTU>¤ï|ç;¿ô¥/1¤+_ùÊw½ë]Ip!?ûÙÏHœ~úéw»ÛÝ©Ñz0VÀ X+`¬€°VàœU€ÿóç;ßa"ó£ýˆÿê3‘ÉYÉ9;ªÚûÉ'ŸÌŒ.®}íkÿÁüÁíëH6{†�R è~üŠ; \¦%×ú¹8Û~üJ4Œ’Mø•À›ÀGxŸÉIMýø‘t â%ªGM<ìåÀuBj¹^›œØä¶ °Ö›òÁurL“íô‡SõÀn ¸ÐᱦÅÝH±kCÀ;ÎÒߎJСƜ¬Ç0(Ç‹/ö7zËñf½�¿žrÊ)Lb—Û�><ö±�=7ÜÇ£Q\�õ”GÒ»ßýî=äær¸^ɼqÿô§?}Ýë^wöÙgSxÑ‹^t{è ‰Þõ®waªzHô3 n}ë[_÷º×­ü6A§ï{ßû>ÿùÏ·™™¾ÚÕ®Æç<ç9ÏlÑ’œo|ãoûÛ‰­:ŸøÄ'HCßàM[õÄ<í´ÓâI³v¹Ë]. ²bdæðz(¯ KÍqþö·¿Í‘_ëZ×:óÌ3I#ÿ¥à?™ïW+`¬€°VÀ X+p.W/f%x”9CÁàØc�e*QùGR‚ISNð„8’®ë _Kx±uø•ÇFð«ädûñ+0–øG´+~E¥a˯äB&ä%`‡5¡0Î}Ê5h�=VñN ¶q‹ h¢Æ¤ù a£�‚u´¤&ŽkÚ U6b,Ú¡�˜V††-–…Ó<.ƒÛDÃ�Ÿ›ŒEšQ‘à�̨=KЩNé6 œF{ÐŽÿøÇO}êSqÃYæ uêŽv]¾)›°ŒC™¹Ô8WL|ë[ßzõ«_�ÜmÅ&³ÕøÍ¼k¹[Õ�ɾ÷½ïýŸÿùŸÊ©¿Û^ò’—ÌånÔùò—¿üâ¿xKë¬}ô£}ë[ßÚr·ê‹wðå/ù/~ñ‹ÊÙ4Á‡q²Úmo{ÛMëoZá=ïyÞ ¯}ík7­y *\èBÂ7-oú�Á�€mZ+`¬€°VÀ X�CP�SO=‚–»Õ ‚yãßøÙÏ~¶rœ°]»lønc"\øœMù•ÜÊh¨Ú”_)„³åWÂXÄ’FH¨ºèM sʼÑÀ RÆßiÐ(^oê—lõuÃN]‹ÝáøN 0þRG7’ *7¹»–bÏ55aãoP4N低“ktêFÍõWýñOÕÕŒ¾pÏ#›Ìƒ}|õ«_}âŸJì®Ýß®(Ð~Ñ�–6î="~‹¼ùÍoÞ!IÄ%»E98ˆ±2Ú-oyË«\å*%ñÈ<Öi&Þò–·ò#,”¸È=èAø¦!Û±ïþ®„å+_ázùý=·»ƒ�y›ÛÜ&{á-;‡q®Ô]X+`¬€°VÀ X�U`M¡ Ö¡2“"o˜DÜô¦7½ä%/YÍñ$È•[*Ç +P (P³áWÚÒ€S«äWÁÃöçW^óù+±íãW£àW„†ÅAà…FT§€¯@´nu,ú¦åÄFX–�TŒÎŽà`J�ÙU5öt€º o#aA2æ�è°µIÒ†\UÆVu•aLp Ãt(¸Ç‰þ q…!Î8'Ø•uätè@xj| ާ=íiø•`ߟoûÛxßà¶S‘~éK_úЇ>´rœ8Œ ΟàÇZëí0ù’¡~àøä'?¹¤ÂŠEïxÇ;ô™ã£ØëÝë^÷ªØÌÝèF8޽æ5¯ÉRºc�²‰h¹‚ÞøÆ7¾Å-n‘E�2–'#Jôƒü 9|š¾þõ¯·¯,´ èa‘2Àè 'œÀ[–àM|‹•+ÙQ� Ë"P­…Nš1CÊ2³ÆÖ©³ÕÓÕ©ßV-¯XŸuôXDï{ßûõQ˜@ບ°VÀ X+`¬€8ò`vÀ$%¯‹E½ð€œÿüçÏSèÛg>ó §L Ø{ÍË%y7À®\‘P×ù´,ù•<ähœü*éVïÀið+V[sã 2ÐMqŸ:ÇENˆ-œÌ„ËðDƒ¼ÉOžj¼PËœ`tq©t˜‘©h/êFÌ)0N–ÄéÔw6¢TÕáDð/¨çñW.sZ3´�“›¢je<¸ÐMÆsdbvä`)¥<˜¨ßûÞ÷~îsŸ{â‰'¶=åêìmŽÓ‡‘¿÷{¿wÿûßÿž÷¼ç®8@�³ÎïÐpq7–ccQÂí œT_•YÜ-­a¶6¥fù¸QZ±œìT0˶nxÃòË/�TÍ%#dÓZmð¾÷½oq·lr‡;ܯºLótnDjÇ8¬•ÈdxàªNé6N¹ü4¸�¶»Øä:×¹NZ[绋}Ù”°VÀ X+`¬€8”àÿü 4ÆûÜç>ÅÝrÌ,W]Ó ¢Få ñØÎAŠ_�Z�_á)ÆD3øèJ!�ɯøY#ü*";¹¬ÀaÂ_”„;µ"(Th,²ƒÄ©•ú^œñž¤Äç´öš2õŠÙñ¨?ŒÔ¿†°Ëû$7=Á3<ëhÂ’rŒQ’ÄãDãT|O+Ìi‘9Šzƒc´Tµ�ë—•­Â‚¬øãÁ~0þ;Õþ;µäyef‚ óš;ùsOA +V^½fv´ ’h‡Dß9îÈÓŽÇé`ÁÖÚÏÊ®?üÃ?ÜöV¼EpàwÐÉÙìÙ³§2Ùr´ÒåòÆÆ•Ù&Šæ¶;mQ' Òª½†£Õ°­Vt�Ê„¦¶EsÓu]{÷î�[a«™8nµÉ�¨�k\ƒ/2°Ì³hîÚ|¢SÛ´VÀ X+`¬€°‡ õþË\æ2ˆÌް¦ó¬@½â|ÖˆsŽl6øîeËùUâ,"D…´æó«ˆE.l7øÄLÞnòcÓÒkxÌ…[x¦eUá:üд1)¼� Ô¦3À<�EáðjÇ­Gæp¬˜Ö( pUH�pÕA^uƒ¹€ª6Þ6®Šd")Ât£ÃÒ©œÞ qü•ƒ�jjQx²l9à?ñS-ÆAg„°ž8ãŒ3Ø4“88–ÁJ‡�+]éJ–ã+‡£ÍìÈpve�v¬å„÷"Â÷pç™]~õšô>c�üOúÓÄ ÒÄ ¬“½ð} ÞX¸�q½D\¢þñ�SĽÊGsÞS*”Áå Œ ErO>DG€ çòëu©°VÀ X+`¬€X¤~mÌø6Óð¹uÚ¹ �¡ýO~[Ÿ©+S{røÿ9ÓóŽ÷ùL$?ô¡1bzXa(­f¾KÄHZ¯¦Ì#ð`à?ðÌÜí¥.u©k^óšW¸ÂÚæmšÁ0C¡ �2õà?üüÿŸ)ÛÜiiÛ°“®Yäp8dî\s=¦6,õƒcs«œ�0íÅþê“JFÅŒ,GÈœ�ù“æÅ­ÓÆì`€H„õ™2ßà7¨%Â;•ótKÓó¹VÌT”§ø‡ðìb~EÔjS~é‚}5üJxK0‹bU6�W�5êˆy…I«Bô!þ…y\൅+]¨S�®?ÀateêBd-x¨ Ê×°“CØ‘-þȨ*ÊKÒ¦3šª :Ы ˼aWݨ6[<|૨M@Íøt‘�ªÏ9ÈÚü‰À²2Ÿ=ŒÀ2ð{"ü¾ÖÔ§”‡N"'*›Øà²¹äŸüä'<ƒÈÁ¬5È“—"žVøF�üRX¬±K°ƒ^ZôF“SN9…þrHùÊ“¢ÇÊbPV=k‹Ha¹óãŠx~U (�ËYòH¥>O·¼À´3·rÌ4zË\¯“ÈÓm¼¢OI4·y«-‚WZå·L¼h•ë$V)ot9u/·O“Vy~•‘N‚ËZ‘‰&�¢Î)¿@f.‰6å¾â¶Ï;9+3Þ»ã�?†›™Ïþó[ûÜr|¸*‡_½·»Ýí8­[‚]€ìu¯r‡soóaÇ&7·kqFŠò6ㆼË]îR“2>7Á%zCLçÖq¦°VÀ X+`¬€8âØǒˬ)Éò­ö`m|«�~Ôd•ðŽM\@Òõ�WÓ”flTbÁÄ<ÏnûÛod¯ñŸ¦ŠüŸ¿rH3$f¦{÷î=æ˜cfi3æÑ­ÇC`ƒïbŸïòk- 2¸(Ñ. V«‰„‘.ªUÍG€q\5`±Š%¸XæP5Ù¡éœÔÀ@ît§;U_i�™ k%µ"d}ì´2â:s÷»ß=›lcz¾h´+æ·üJ€K ­-âW`«­ð+|ËÜöäpñJü¬áº¦�U…‚Òsp´Dc‚`+f³ÖÈäãÚ~¼©“Ôÿmo{[ÄãrWùü·wΚÍÆÿ’—¼„O~UàaQOÞG¾ß—Ti'ñŠW¼¢¸[û”AÆœ¬¶Ó¤Ny´Û­Mª4‰ÉÕµX úV®ÓdwOë·W‡³a9Õõ²Ëj °x¿ÈÉ�JÖ Pm; ~'e÷C)ß©Ãi�?,—æ³ÕÈá÷SæóK´9[óä“O®a·Õ¸%V'Ѽ/ËÜÆy¯¶×Â�àËoxÊ»µ=Â^W‰¢ÍŽ Ñ&€ëôîS+`¬€°VÀ X+`P€Å·ã)Eë‰6+ÿ3¯5¯k[Õ˜ÓÕ¼±™þ%UJ¢ î5c"“è´BN=æ&5ûc`LFZ#¤qâ�%wc*Aýòba‚¨=%: ;§L-kYp¦�µI~ Åݰߎ–I P"gpkí)Ã0´óvRƒ2„¶õñ‡ ~‰�ña%ÝÕQþ‰;œž·½¯˜&ú²åW´�-àWLý~9Óæ£«eü Ìr*Ã�-0ÑHé7¨Ô‚‰¥Q:­�×U/¡=N XÊ�¨Sõ3�0µ#.”šâecêÕ£­<ìÈÔjp¬'kØæŠ"¬õèØÒaê;'KAßÔá-¨›Ðþ`L†ë–¥Onî~¼œÚþ¡'¸DÁáÅYà8 ¡ÜB…Oi:¯f+êãÞB@" ÃÌŸ4KBRºzM*sG>ëYϪ‘p§aJlà�›úð?ç9Ï9úè£3* H÷¾°š�ÀŸ vŽ/3åÚ¢åiÀ9Ÿü¼'�wê}™¥¼ÿTÎR|…R">Ÿ|P ©¾ à)Ããæ®w½kÇŸX2�/¹W&pßXŽ?Y¹ÝdŠÇöš|Ÿpì±ÇvLÍžÒcÑÞ<„óYÑãMLG9zÁ¥–ÒÙæŒ™käû.:C§žzj>¯¹XœænyË[ζªÞ2îN¹gh;û½G½•„%êÃtpn§ÂX+�~ËzŒà@UÞà6ã» ú,° g=F�½žÅ|¾Õ!ÿÿ·w÷:v[€=Ø÷=<¯àÔ)""9ƒ �„DFJBF@à€’%¼ 1od÷Ûµ‡¥r÷™ö10\É·Zö™êú¯ÝÕÕg­³võì\鯙O7¥ÂG?b•bp=¼ÿ¾§šR [ñ®¾�÷øƒ>М¥w&ÉÀ½¬#-î®Í|SȯX~º6ú‘­ÿîöiuÑÃÏâXÍ¢=‘—†ÈׂØ-I+°,°,°,°,°,°,°,°,°,°,Ðð>@Ï7ðc³Ð²µäd¥ €Æ¶]ö |˜k [À®þöÛo�4ÀDF ¸$³6 ŠûªÖAÎ.Â�†| Bq*§üM™QEð±½i¯í.«Îè„`g;\u¼ÏÄnÈŠ†iºúûï¿sƒ[_‰7ƒ³`1á\8!ÎÈÖ ðÉ'ŸtÓÐ2|Î94÷‰x3&L–ÓžFó¾T*ì½½ºÒ:4Óù9·.t%>íé6ÏcW%]•Úr Ì>t–ž¹·ŠÌBžÔ|ˆ·£AáÈçœTlK²õ.]²YfÞ­ "˲æþœëDIlx7©¹@x�¦ºæ"ç„gwÎt5Q¢YƒHÒrM“A@†Ùø�”Ÿ;š¶›óoÂ~N Û2s¯�-^¨š˜xÙTrK§Y-6T¿]ó²:§9Ë·Îzã$ ˆ÷ü�gŽ<Îulúd‘cº)ñ¥øt{~Ƚ”cœØ|-¼[§¢ê"¦ �¾/xã9½±Õ<á7j¦Š:i �kNjSÀyv&~––––––––––pavzi ÄÈ&b–ñ;¸l†·¾r7>UŽI ÀKë㺶x)��öq)óåI�íy 19Ôîݭ„Ñ]óüÙ2 1��0…ÃM—Â6Îü�È`.á¹ÿ�ÿÌO"<àÌ»uAp>Ls3¨€ý4Wžì>$éïÃóÔ~àeþjðn×üêeþªX¬—ù«R˜ÝÌ_ FV¬V‘f(*ÌÆmõyá=¤h/ 5ލÒ*¡6g+þ ±öÂ>p¶ò-²ï^í÷âêÅ(yõâêî]›¸Õ[P‡šNl�\ÙîÎ*¿º+Š“jý@-VÛ,¿Óö>Ud`íªºîgà ºq+Çwã6ûꫯö÷̦IŠÏd3´ÿé§Ÿb|ñ\4DñôîzÎÏivÎwA™ÕÜ™“÷motóÛÅu.òºadçð¦–Ü!y€zBÉam´©ÈެDs£!VŒn3ŸÔŽå6Þ§OŸZ×^ëÍ,Z÷E÷Aÿ›Î˜»$|yyéê·µ‰’æanrΧ¡o¢Y›S7a¨fE-LÖܬøN3‹¢“Ú”½½Sà¬È¸¤öó�›c:/É<ôºÿ¾‡ Ö•²Ž–]Ÿ¡³÷^™sU!ªŽÍ•¶”=y±Rg~wÚÜbÉp0„îü+iÓƒJ6Iý�i.mÈ>‘†ßOÜÜ#›âûÓÜ,,öƸ¯pÅ, , , , , , , , , ¼ð ùçŸ" ü:v…ɨ¹=¶G`6â]‹Œ¸s‡;>«7h¦ƒ �s %WWÇ�ß~á‚Æ>À¯ªfú Îr¤W›@Ds<4A&ÁuW¡xƒ˜@T ¢3€ŸÄw0f¸Â¹¶¿fä&fPƒèS#Ú¸CE¢ì¬Ý üŸ…çÇ#ÂBÝ圉N™¥j'µ+Z)Énæ¯3÷ þª„rÌ®µUÕ³Uï>àeêS4®«fÙ£‘¤aÌ*;º­öp»z«ö¯âWÜ«­ÚîX²5opPÖ¡Âz}ê Ì®¨çºøÈ¥bST¹«ÕèP¾]¼5¨Î3[*¿9Ð|¢<žï¿ÿ~ îa£2IöyÄäñ)|̃¿–ý7m¥l?–6©í4užœ“'#Ïl(5Ÿ™e[XXXXXXXXxƒ-�ÒäGtˆû|ä �÷.l€Æ­©4[§µ¹x¼AšM½ðò¾ ÓDËÖ9É)báÆË9Ý€Ù õöË/¿ì³%FßP{2‘f"² @Éî7'a�J6†Â:@xpl?†Wi}€¦�w@™ÅæsÞ?v�cg|bŽô {Ðñ’æ tKð|îÒ>\Þ—Í_ƒ jcC&V¬Ö™üÚªø±B‘/óWƒ¤–ë.õÚxõh`²–jÎQü1[qi¤jcç¶ò'­ÜèÕJ¢æLå^T:¼Ñ«!]Óœ4 Ï‹ÉC$–ª®zí?ÞíÎóÚÜèêîÕiœÁZ¡bÜ´YªïO Þª[ƒt~+†¥Õ7T”¦VÛÁ Ñç¾=^Ùßÿ}è‰}†ÄФ|ýõ×_|ñE|0; %á¦r|þùçíìy~ÎÜÞiå €×p!æM¸d>V!Ô6'Y\Ø¡}þ¤ýækӏХKÊêcuHÁš¼c¢$fˆôtò”Û,Šg–ÓŠi�{øð!.ÿd©ŽÄÊwW�ðýÖµ^G^Ëìíî“p…M´qn÷´hS¤Ë³_ó¾ì?ƒw‹“²ç�×ì›ð®¹ãyæÝ:ŸH+׃²û'úÇ}myrœ\¯“¦>�ÝoóäÈÚ�JÞŒ@ßt¯¼•ÞŒÁ®Q, , , , , , , , , œcHŠÞ-¨�kä™NK]9&7̱Ý(-6Ò„m›hÇ/ŠÙø�åÅHÄ3¨9þ•Ý÷ùæ3Wp\D'vNÚ„^X;©\±W’¥ v‰?�;ø|ŒÓSŠêç‡Û¨$ñû²Á'BàÇDó![tO ]–@/×èÏK÷µç¯ìñVtTø+;¯1©˜Ž—J®3E\qïô{vÍ_ñ+­]ã®ù«¦¶êª÷g…Õ`Ñnåô)²¬AÜ6^v ðèº�Z­Âòt-ȱÿo¸Ý|JÍl·IEND®B`‚python-opentelemetry-1.45.2/.github/scripts/000077500000000000000000000000001527070070700210525ustar00rootroot00000000000000python-opentelemetry-1.45.2/.github/scripts/update-version-patch.sh000077500000000000000000000004661527070070700254610ustar00rootroot00000000000000#!/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 python-opentelemetry-1.45.2/.github/scripts/update-version.sh000077500000000000000000000003261527070070700243570ustar00rootroot00000000000000#!/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,prerelease python-opentelemetry-1.45.2/.github/scripts/use-cla-approved-github-bot.sh000077500000000000000000000001561527070070700266240ustar00rootroot00000000000000#!/bin/bash -e git config user.name otelbot git config user.email 197425009+otelbot@users.noreply.github.com python-opentelemetry-1.45.2/.github/workflows/000077500000000000000000000000001527070070700214205ustar00rootroot00000000000000python-opentelemetry-1.45.2/.github/workflows/add-to-project.yml000066400000000000000000000017451527070070700247660ustar00rootroot00000000000000name: 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 PR #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 }} python-opentelemetry-1.45.2/.github/workflows/backport.yml000066400000000000000000000037151527070070700237560ustar00rootroot00000000000000name: Backport on: workflow_dispatch: inputs: number: description: "The pull request # to backport" required: true permissions: contents: read jobs: backport: runs-on: ubuntu-latest permissions: contents: write # required for pushing changes 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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: # history is needed to run git cherry-pick below fetch-depth: 0 # zizmor: ignore[artipacked] Credentials are required by the branch push below. persist-credentials: true - 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: 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 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 python-opentelemetry-1.45.2/.github/workflows/benchmarks.yml000066400000000000000000000035061527070070700242640ustar00rootroot00000000000000name: SDK Benchmark Tests on: push: branches: [ main ] permissions: contents: read jobs: sdk-benchmarks: permissions: contents: write # required for pushing to gh-pages runs-on: oracle-bare-metal-64cpu-1024gb-x86-64-ubuntu-24 container: image: python:3.14-slim@sha256:caaf356f40667c496d405780745b9ac25771c189a51dfcc42430d531ea09f8a2 steps: - name: Install Git # since Git isn't available in the container image used above run: | apt-get update apt-get install -y git - name: Make repo safe for Git inside container run: git config --global --add safe.directory "$GITHUB_WORKSPACE" - name: Checkout Core Repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tox run: uvx --with tox-uv tox -e benchmark-opentelemetry-sdk -- --benchmark-json=opentelemetry-sdk/output.json - name: Report on SDK benchmark results uses: benchmark-action/github-action-benchmark@4322e5726e6334590d251fc4f92bec0efafc45dc # v1.22.2 with: name: OpenTelemetry Python SDK Benchmarks tool: pytest output-file-path: opentelemetry-sdk/output.json gh-pages-branch: gh-pages github-token: ${{ secrets.GITHUB_TOKEN }} # Make a commit on `gh-pages` with benchmarks from previous step benchmark-data-dir-path: "benchmarks" auto-push: true max-items-in-chart: 100 # Alert with a commit comment on possible performance regression alert-threshold: '200%' comment-on-alert: true alert-comment-cc-users: "@open-telemetry/python-approvers,@open-telemetry/python-maintainers" python-opentelemetry-1.45.2/.github/workflows/changelog.yml000066400000000000000000000062331527070070700240760ustar00rootroot00000000000000# This action requires that any PR targeting the main branch should add a # changelog fragment file in the .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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false 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') ]] then echo "CHANGELOG.md should not be directly modified." echo "Please add a changelog fragment file to the .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: Install towncrier run: pip install towncrier==25.8.0 - name: Check for changelog fragment run: | if ! towncrier check --compare-with origin/${{ github.base_ref }}; then echo "" echo "No changelog fragment found for this PR." echo "Add a file named .changelog/${{ github.event.pull_request.number }}." echo "where is one of: added, changed, deprecated, removed, fixed" echo "See CONTRIBUTING.md for details." echo "" echo "Or add the \"Skip Changelog\" label if this job should be skipped." false fi - name: Validate fragment filenames env: PR_NUMBER: ${{ github.event.pull_request.number }} run: | fragments=$(git diff --diff-filter=A --name-only origin/${{ github.base_ref }} -- '.changelog/*' | grep -v '/\.gitignore$' || true) [ -z "$fragments" ] && exit 0 invalid=() while IFS= read -r f; do 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 - name: Preview changelog run: towncrier build --draft --version Unreleased python-opentelemetry-1.45.2/.github/workflows/check-links.yml000066400000000000000000000066721527070070700243510ustar00rootroot00000000000000name: 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.event.merge_group.head_sha || github.run_id }} cancel-in-progress: true env: # Diff against merge group SHAs in queue, or PR base branch otherwise 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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false fetch-depth: 0 - name: Get changed markdown files id: changed-files uses: tj-actions/changed-files@9426d40962ed5378910ee2e21d5f8c6fcbf2dd96 # v47.0.6 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 only on pull requests and merge groups 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; } python-opentelemetry-1.45.2/.github/workflows/check_links_config.json000066400000000000000000000004401527070070700261130ustar00rootroot00000000000000{ "ignorePatterns": [ { "pattern": "http(s)?://\\d+\\.\\d+\\.\\d+\\.\\d+" }, { "pattern": "http(s)?://localhost" }, { "pattern": "http(s)?://example.com" } ], "aliveStatusCodes": [429, 200] } python-opentelemetry-1.45.2/.github/workflows/ci.yml000066400000000000000000000030341527070070700225360ustar00rootroot00000000000000# 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 contrib: uses: open-telemetry/opentelemetry-python-contrib/.github/workflows/core_contrib_test.yml@release/v1.45.x-0.66bx with: CORE_REPO_SHA: ${{ github.sha }} CONTRIB_REPO_SHA: ${{ github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'prepare-release') && github.event.pull_request.head.ref || 'release/v1.45.x-0.66bx' }} check: if: always() needs: - misc - lint - tests - contrib runs-on: ubuntu-latest steps: # Skip alls-green check for post-release version bumps in merge_group. # This avoids a deadlock where core and contrib repos wait for each other's releases. # See RELEASING.md for more details. - name: Decide whether the needed jobs succeeded or failed if: ${{ !(github.event_name == 'merge_group' && contains(github.event.merge_group.head_commit.message, 'Update version to')) }} uses: re-actors/alls-green@b5b5b37504aa4183270bd3d855c52a67f212be35 # release/v1 with: jobs: ${{ toJSON(needs) }} python-opentelemetry-1.45.2/.github/workflows/codeql-analysis.yml000066400000000000000000000027171527070070700252420ustar00rootroot00000000000000name: 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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL uses: github/codeql-action/init@3ea06614dafe36dec890db3446326e0d40ce53d4 # v3.38.1 with: languages: python - name: Autobuild uses: github/codeql-action/autobuild@3ea06614dafe36dec890db3446326e0d40ce53d4 # v3.38.1 - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@3ea06614dafe36dec890db3446326e0d40ce53d4 # v3.38.1 python-opentelemetry-1.45.2/.github/workflows/fossa.yml000066400000000000000000000007051527070070700232600ustar00rootroot00000000000000name: FOSSA scanning on: push: branches: - main permissions: contents: read jobs: fossa: runs-on: ubuntu-latest steps: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: persist-credentials: false - uses: fossas/fossa-action@ff70fe9fe17cbd2040648f1c45e8ec4e4884dcf3 # v1.9.0 with: api-key: ${{secrets.FOSSA_API_KEY}} team: OpenTelemetry python-opentelemetry-1.45.2/.github/workflows/generate_workflows.py000066400000000000000000000124541527070070700257070ustar00rootroot00000000000000from 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", "py314t": "3.14t", } 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_misc_job_datas(tox_envs: list) -> list: regex_patterns = [ _tox_test_env_regex, _tox_lint_env_regex, _tox_contrib_env_regex, re_compile(r"benchmark.+"), ] return [tox_env for tox_env in tox_envs if not any(pattern.match(tox_env) for pattern in regex_patterns)] def _generate_workflow( job_datas: list, template_name: str, output_dir: Path, ) -> None: env = Environment(loader=FileSystemLoader(Path(__file__).parent.joinpath("templates"))) with open(output_dir.joinpath(f"{template_name}.yml"), "w") as yml_file: yml_file.write( env.get_template(f"{template_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_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( output_dir: Path, ) -> None: with open(output_dir.joinpath("ci.yml"), "w") as ci_yml_file: ci_yml_file.write( Environment(loader=FileSystemLoader(Path(__file__).parent.joinpath("templates"))) .get_template("ci.yml.j2") .render() ) ci_yml_file.write("\n") if __name__ == "__main__": tox_ini_path = Path(__file__).parent.parent.parent.joinpath("tox.ini") output_dir = Path(__file__).parent generate_test_workflow(tox_ini_path, output_dir, ["ubuntu-latest", "windows-latest"]) generate_lint_workflow(tox_ini_path, output_dir) generate_misc_workflow(tox_ini_path, output_dir) generate_ci_workflow(output_dir) python-opentelemetry-1.45.2/.github/workflows/lint.yml000066400000000000000000000474711527070070700231260ustar00rootroot00000000000000# 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: CORE_REPO_SHA: main CONTRIB_REPO_SHA: ${{ github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'prepare-release') && github.event.pull_request.head.ref || 'release/v1.45.x-0.66bx' }} PIP_EXISTS_ACTION: w jobs: lint-opentelemetry-api: name: opentelemetry-api runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.14 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e lint-opentelemetry-api lint-opentelemetry-proto-gen-latest: name: opentelemetry-proto-gen-latest runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.14 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e lint-opentelemetry-proto-gen-latest lint-opentelemetry-protojson-gen-latest: name: opentelemetry-protojson-gen-latest runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.14 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e lint-opentelemetry-protojson-gen-latest lint-opentelemetry-codegen-json: name: opentelemetry-codegen-json runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.14 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e lint-opentelemetry-codegen-json lint-opentelemetry-sdk: name: opentelemetry-sdk runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.14 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e lint-opentelemetry-sdk lint-opentelemetry-configuration: name: opentelemetry-configuration runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.14 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e lint-opentelemetry-configuration lint-opentelemetry-semantic-conventions: name: opentelemetry-semantic-conventions runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.14 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e lint-opentelemetry-semantic-conventions lint-opentelemetry-getting-started: name: opentelemetry-getting-started runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.14 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e lint-opentelemetry-getting-started lint-opentelemetry-opentracing-shim: name: opentelemetry-opentracing-shim runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.14 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e lint-opentelemetry-opentracing-shim lint-opentelemetry-opencensus-shim: name: opentelemetry-opencensus-shim runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.14 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e lint-opentelemetry-opencensus-shim lint-opentelemetry-exporter-http-transport: name: opentelemetry-exporter-http-transport runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.14 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e lint-opentelemetry-exporter-http-transport lint-opentelemetry-exporter-otlpcommon-latest: name: opentelemetry-exporter-otlpcommon-latest runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.14 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e lint-opentelemetry-exporter-otlpcommon-latest lint-opentelemetry-exporter-opencensus: name: opentelemetry-exporter-opencensus runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.14 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e lint-opentelemetry-exporter-opencensus lint-opentelemetry-exporter-otlp-proto-common: name: opentelemetry-exporter-otlp-proto-common runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.14 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e lint-opentelemetry-exporter-otlp-proto-common lint-opentelemetry-exporter-otlp-json-common: name: opentelemetry-exporter-otlp-json-common runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.14 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e lint-opentelemetry-exporter-otlp-json-common lint-opentelemetry-exporter-otlp-combined: name: opentelemetry-exporter-otlp-combined runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.14 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e lint-opentelemetry-exporter-otlp-combined lint-opentelemetry-exporter-otlp-json-file: name: opentelemetry-exporter-otlp-json-file runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.14 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e lint-opentelemetry-exporter-otlp-json-file lint-opentelemetry-exporter-otlp-json-http: name: opentelemetry-exporter-otlp-json-http runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.14 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e lint-opentelemetry-exporter-otlp-json-http lint-opentelemetry-exporter-otlp-proto-grpc-latest: name: opentelemetry-exporter-otlp-proto-grpc-latest runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.14 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e lint-opentelemetry-exporter-otlp-proto-grpc-latest lint-opentelemetry-exporter-otlp-proto-http: name: opentelemetry-exporter-otlp-proto-http runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.14 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e lint-opentelemetry-exporter-otlp-proto-http lint-opentelemetry-exporter-prometheus: name: opentelemetry-exporter-prometheus runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.14 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e lint-opentelemetry-exporter-prometheus lint-opentelemetry-exporter-zipkin-combined: name: opentelemetry-exporter-zipkin-combined runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.14 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e lint-opentelemetry-exporter-zipkin-combined lint-opentelemetry-exporter-zipkin-proto-http: name: opentelemetry-exporter-zipkin-proto-http runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.14 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e lint-opentelemetry-exporter-zipkin-proto-http lint-opentelemetry-exporter-zipkin-json: name: opentelemetry-exporter-zipkin-json runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.14 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e lint-opentelemetry-exporter-zipkin-json lint-opentelemetry-propagator-b3: name: opentelemetry-propagator-b3 runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.14 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e lint-opentelemetry-propagator-b3 lint-opentelemetry-propagator-jaeger: name: opentelemetry-propagator-jaeger runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.14 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e lint-opentelemetry-propagator-jaeger lint-opentelemetry-test-utils: name: opentelemetry-test-utils runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.14 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e lint-opentelemetry-test-utils 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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.14 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e lint-license-header-check python-opentelemetry-1.45.2/.github/workflows/misc.yml000066400000000000000000000225011527070070700230760ustar00rootroot00000000000000# 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: CORE_REPO_SHA: main CONTRIB_REPO_SHA: ${{ github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'prepare-release') && github.event.pull_request.head.ref || 'release/v1.45.x-0.66bx' }} PIP_EXISTS_ACTION: w CONTRIB_REPO_UTIL_HTTP: ${{ github.workspace }}/opentelemetry-python-contrib/util/opentelemetry-util-http CONTRIB_REPO_INSTRUMENTATION: ${{ github.workspace }}/opentelemetry-python-contrib/opentelemetry-instrumentation CONTRIB_REPO_INSTRUMENTATION_REQUESTS: ${{ github.workspace }}/opentelemetry-python-contrib/instrumentation/opentelemetry-instrumentation-requests CONTRIB_REPO_INSTRUMENTATION_WSGI: ${{ github.workspace }}/opentelemetry-python-contrib/instrumentation/opentelemetry-instrumentation-wsgi CONTRIB_REPO_INSTRUMENTATION_FLASK: ${{ github.workspace }}/opentelemetry-python-contrib/instrumentation/opentelemetry-instrumentation-flask jobs: test-scripts: name: test-scripts runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.11 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.11" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e test-scripts spellcheck: name: spellcheck runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.11 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.11" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e spellcheck tracecontext: name: tracecontext runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Checkout contrib repo @ ${{ env.CONTRIB_REPO_SHA }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false repository: open-telemetry/opentelemetry-python-contrib ref: ${{ env.CONTRIB_REPO_SHA }} path: opentelemetry-python-contrib - name: Set up Python 3.11 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.11" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e tracecontext typecheck: name: typecheck runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.11 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.11" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e typecheck 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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.11 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.11" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e docs docker-tests-otlpexporter: name: docker-tests-otlpexporter runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.11 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.11" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e docker-tests-otlpexporter docker-tests-opencensus: name: docker-tests-opencensus runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.11 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.11" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e docker-tests-opencensus public-symbols-check: name: public-symbols-check runs-on: ubuntu-latest timeout-minutes: 30 if: | !contains(github.event.pull_request.labels.*.name, 'Approve Public API check') && github.actor != 'otelbot[bot]' && github.event_name == 'pull_request' steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false 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 ${{ github.event.pull_request.head.sha }} - name: Set up Python 3.11 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.11" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e public-symbols-check shellcheck: name: shellcheck runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.11 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.11" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e shellcheck 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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.11 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.11" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e generate-workflows - name: Check github 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) precommit: name: precommit runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.11 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.11" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e precommit python-opentelemetry-1.45.2/.github/workflows/ossf-scorecard.yml000066400000000000000000000007421527070070700250630ustar00rootroot00000000000000name: OSSF Scorecard on: push: branches: - main schedule: - cron: "16 11 * * 4" # 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@ad1959372f2919fcf2556d3a3d12ed5265c54209 # v0.16.0 python-opentelemetry-1.45.2/.github/workflows/prepare-patch-release.yml000066400000000000000000000124011527070070700263120ustar00rootroot00000000000000name: Prepare patch release on: workflow_dispatch: permissions: contents: read jobs: prepare-patch-release: permissions: contents: write # required for pushing changes pull-requests: write # required for adding labels to PRs runs-on: ubuntu-latest steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: # zizmor: ignore[artipacked] Credentials are required by the branch push below. persist-credentials: true - name: Install dependencies run: pip install toml towncrier - 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: 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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: ref: main # zizmor: ignore[artipacked] Credentials are required by the branch push below. persist-credentials: true - 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" python-opentelemetry-1.45.2/.github/workflows/prepare-release-branch.yml000066400000000000000000000211121527070070700264470ustar00rootroot00000000000000name: 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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Install toml run: pip install toml - 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: permissions: contents: write # required for pushing changes pull-requests: write # required for adding labels to PRs runs-on: ubuntu-latest needs: prereqs steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: # zizmor: ignore[artipacked] Credentials are required by the branch pushes below. persist-credentials: true - name: Install dependencies run: pip install toml towncrier - 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: 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: permissions: contents: write # required for pushing changes pull-requests: write # required for adding labels to PRs runs-on: ubuntu-latest needs: prereqs steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: # zizmor: ignore[artipacked] Credentials are required by the branch push below. persist-credentials: true - name: Install dependencies run: pip install toml towncrier - 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: 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 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.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" python-opentelemetry-1.45.2/.github/workflows/release.yml000066400000000000000000000123001527070070700235570ustar00rootroot00000000000000name: Release on: workflow_dispatch: permissions: contents: read jobs: release: permissions: contents: write # required for creating GitHub 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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Install toml run: pip install toml - 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 "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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false ref: main # back to the release branch - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false # next few steps publish to pypi - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.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: | # 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 ${STABLE_VERSION}\/${UNSTABLE_VERSION} /d;/^## Version /q;p" CHANGELOG.md \ > /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 - 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$STABLE_VERSION python-opentelemetry-1.45.2/.github/workflows/stale.yml000066400000000000000000000023121527070070700232510ustar00rootroot00000000000000name: 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@1e223db275d687790206a7acac4d1a11bd6fe629 # v10.4.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 python-opentelemetry-1.45.2/.github/workflows/templates/000077500000000000000000000000001527070070700234165ustar00rootroot00000000000000python-opentelemetry-1.45.2/.github/workflows/templates/ci.yml.j2000066400000000000000000000032051527070070700250460ustar00rootroot00000000000000# 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 contrib: uses: open-telemetry/opentelemetry-python-contrib/.github/workflows/core_contrib_test.yml@release/v1.45.x-0.66bx with: CORE_REPO_SHA: ${% raw %}{{ github.sha }}{% endraw %} CONTRIB_REPO_SHA: {% raw %}${{ github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'prepare-release') && github.event.pull_request.head.ref || 'release/v1.45.x-0.66bx' }}{% endraw %} check: if: always() needs: - misc - lint - tests - contrib runs-on: ubuntu-latest steps: # Skip alls-green check for post-release version bumps in merge_group. # This avoids a deadlock where core and contrib repos wait for each other's releases. # See RELEASING.md for more details. - name: Decide whether the needed jobs succeeded or failed if: ${% raw %}{{ !(github.event_name == 'merge_group' && contains(github.event.merge_group.head_commit.message, 'Update version to')) }}{% endraw %} uses: re-actors/alls-green@b5b5b37504aa4183270bd3d855c52a67f212be35 # release/v1 with: jobs: ${% raw %}{{ toJSON(needs) }}{% endraw %} python-opentelemetry-1.45.2/.github/workflows/templates/lint.yml.j2000066400000000000000000000022551527070070700254250ustar00rootroot00000000000000# 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: CORE_REPO_SHA: main CONTRIB_REPO_SHA: {% raw %}${{ github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'prepare-release') && github.event.pull_request.head.ref || 'release/v1.45.x-0.66bx' }}{% endraw %} 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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.14 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e {{ job_data.tox_env }} {%- endfor %} python-opentelemetry-1.45.2/.github/workflows/templates/misc.yml.j2000066400000000000000000000073261527070070700254160ustar00rootroot00000000000000# 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: CORE_REPO_SHA: main CONTRIB_REPO_SHA: {% raw %}${{ github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'prepare-release') && github.event.pull_request.head.ref || 'release/v1.45.x-0.66bx' }}{% endraw %} PIP_EXISTS_ACTION: w CONTRIB_REPO_UTIL_HTTP: ${% raw %}{{ github.workspace }}{% endraw %}/opentelemetry-python-contrib/util/opentelemetry-util-http CONTRIB_REPO_INSTRUMENTATION: ${% raw %}{{ github.workspace }}{% endraw %}/opentelemetry-python-contrib/opentelemetry-instrumentation CONTRIB_REPO_INSTRUMENTATION_REQUESTS: ${% raw %}{{ github.workspace }}{% endraw %}/opentelemetry-python-contrib/instrumentation/opentelemetry-instrumentation-requests CONTRIB_REPO_INSTRUMENTATION_WSGI: ${% raw %}{{ github.workspace }}{% endraw %}/opentelemetry-python-contrib/instrumentation/opentelemetry-instrumentation-wsgi CONTRIB_REPO_INSTRUMENTATION_FLASK: ${% raw %}{{ github.workspace }}{% endraw %}/opentelemetry-python-contrib/instrumentation/opentelemetry-instrumentation-flask 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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false{% if job_data == "public-symbols-check" %} fetch-depth: 0{% endif %} {%- if job_data == "tracecontext" %} - name: Checkout contrib repo @ ${% raw %}{{ env.CONTRIB_REPO_SHA }}{% endraw %} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false repository: open-telemetry/opentelemetry-python-contrib ref: ${% raw %}{{ env.CONTRIB_REPO_SHA }}{% endraw %} path: opentelemetry-python-contrib {%- endif %} {%- if job_data == "public-symbols-check" %} - 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@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.11" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e {{ job_data }} {%- if job_data == "generate-workflows" %} - name: Check github 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 %} {%- endfor %} python-opentelemetry-1.45.2/.github/workflows/templates/test.yml.j2000066400000000000000000000062011527070070700254310ustar00rootroot00000000000000# 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: CORE_REPO_SHA: main CONTRIB_REPO_SHA: {% raw %}${{ github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'prepare-release') && github.event.pull_request.head.ref || 'release/v1.45.x-0.66bx' }}{% endraw %} WEAVER_VERSION: "v0.25.1" PIP_EXISTS_ACTION: w CONTRIB_REPO_UTIL_HTTP: ${% raw %}{{ github.workspace }}{% endraw %}/opentelemetry-python-contrib/util/opentelemetry-util-http CONTRIB_REPO_INSTRUMENTATION: ${% raw %}{{ github.workspace }}{% endraw %}/opentelemetry-python-contrib/opentelemetry-instrumentation CONTRIB_REPO_INSTRUMENTATION_REQUESTS: ${% raw %}{{ github.workspace }}{% endraw %}/opentelemetry-python-contrib/instrumentation/opentelemetry-instrumentation-requests CONTRIB_REPO_INSTRUMENTATION_WSGI: ${% raw %}{{ github.workspace }}{% endraw %}/opentelemetry-python-contrib/instrumentation/opentelemetry-instrumentation-wsgi CONTRIB_REPO_INSTRUMENTATION_FLASK: ${% raw %}{{ github.workspace }}{% endraw %}/opentelemetry-python-contrib/instrumentation/opentelemetry-instrumentation-flask jobs: {%- for job_data in job_datas %} {{ job_data.name }}: name: {{ job_data.ui_name }} runs-on: {{ job_data.os }} timeout-minutes: 30 steps: {%- if job_data.os == "windows-latest" %} - name: Configure git to support long filenames run: git config --system core.longpaths true {%- endif %} - name: Checkout repo @ SHA - ${% raw %}{{ github.sha }}{% endraw %} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false {%- if "getting-started" in job_data.tox_env %} - name: Checkout contrib repo @ ${% raw %}{{ env.CONTRIB_REPO_SHA }}{% endraw %} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false repository: open-telemetry/opentelemetry-python-contrib ref: ${% raw %}{{ env.CONTRIB_REPO_SHA }}{% endraw %} path: opentelemetry-python-contrib {%- endif %} {%- if "test-opentelemetry-test-utils" in job_data.tox_env and job_data.os != "windows-latest" %} - name: Install weaver run: | WEAVER_URL="https://github.com/open-telemetry/weaver/releases/download/${% raw %}{{ env.WEAVER_VERSION }}{% endraw %}/weaver-x86_64-unknown-linux-gnu.tar.xz" curl -sSL "$WEAVER_URL" | tar -xJ -C /tmp weaver-x86_64-unknown-linux-gnu/weaver sudo mv /tmp/weaver-x86_64-unknown-linux-gnu/weaver /usr/local/bin/weaver {%- endif %} - name: Set up Python {{ job_data.python_version }} uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "{{ job_data.python_version }}" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e {{ job_data.tox_env }} -- -ra {%- endfor %} python-opentelemetry-1.45.2/.github/workflows/test.yml000066400000000000000000012320351527070070700231300ustar00rootroot00000000000000# 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: CORE_REPO_SHA: main CONTRIB_REPO_SHA: ${{ github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'prepare-release') && github.event.pull_request.head.ref || 'release/v1.45.x-0.66bx' }} WEAVER_VERSION: "v0.25.1" PIP_EXISTS_ACTION: w CONTRIB_REPO_UTIL_HTTP: ${{ github.workspace }}/opentelemetry-python-contrib/util/opentelemetry-util-http CONTRIB_REPO_INSTRUMENTATION: ${{ github.workspace }}/opentelemetry-python-contrib/opentelemetry-instrumentation CONTRIB_REPO_INSTRUMENTATION_REQUESTS: ${{ github.workspace }}/opentelemetry-python-contrib/instrumentation/opentelemetry-instrumentation-requests CONTRIB_REPO_INSTRUMENTATION_WSGI: ${{ github.workspace }}/opentelemetry-python-contrib/instrumentation/opentelemetry-instrumentation-wsgi CONTRIB_REPO_INSTRUMENTATION_FLASK: ${{ github.workspace }}/opentelemetry-python-contrib/instrumentation/opentelemetry-instrumentation-flask jobs: py310-test-opentelemetry-api_ubuntu-latest: name: opentelemetry-api 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.10 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.10" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py310-test-opentelemetry-api -- -ra py311-test-opentelemetry-api_ubuntu-latest: name: opentelemetry-api 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.11 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.11" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py311-test-opentelemetry-api -- -ra py312-test-opentelemetry-api_ubuntu-latest: name: opentelemetry-api 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.12 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.12" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py312-test-opentelemetry-api -- -ra py313-test-opentelemetry-api_ubuntu-latest: name: opentelemetry-api 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.13 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.13" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py313-test-opentelemetry-api -- -ra py314-test-opentelemetry-api_ubuntu-latest: name: opentelemetry-api 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.14 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py314-test-opentelemetry-api -- -ra py314t-test-opentelemetry-api_ubuntu-latest: name: opentelemetry-api 3.14t Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.14t uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14t" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py314t-test-opentelemetry-api -- -ra pypy3-test-opentelemetry-api_ubuntu-latest: name: opentelemetry-api pypy-3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python pypy-3.10 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "pypy-3.10" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e pypy3-test-opentelemetry-api -- -ra py310-test-opentelemetry-proto-gen-oldest_ubuntu-latest: name: opentelemetry-proto-gen-oldest 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.10 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.10" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py310-test-opentelemetry-proto-gen-oldest -- -ra py310-test-opentelemetry-proto-gen-latest_ubuntu-latest: name: opentelemetry-proto-gen-latest 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.10 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.10" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py310-test-opentelemetry-proto-gen-latest -- -ra py311-test-opentelemetry-proto-gen-oldest_ubuntu-latest: name: opentelemetry-proto-gen-oldest 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.11 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.11" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py311-test-opentelemetry-proto-gen-oldest -- -ra py311-test-opentelemetry-proto-gen-latest_ubuntu-latest: name: opentelemetry-proto-gen-latest 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.11 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.11" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py311-test-opentelemetry-proto-gen-latest -- -ra py312-test-opentelemetry-proto-gen-oldest_ubuntu-latest: name: opentelemetry-proto-gen-oldest 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.12 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.12" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py312-test-opentelemetry-proto-gen-oldest -- -ra py312-test-opentelemetry-proto-gen-latest_ubuntu-latest: name: opentelemetry-proto-gen-latest 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.12 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.12" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py312-test-opentelemetry-proto-gen-latest -- -ra py313-test-opentelemetry-proto-gen-oldest_ubuntu-latest: name: opentelemetry-proto-gen-oldest 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.13 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.13" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py313-test-opentelemetry-proto-gen-oldest -- -ra py313-test-opentelemetry-proto-gen-latest_ubuntu-latest: name: opentelemetry-proto-gen-latest 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.13 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.13" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py313-test-opentelemetry-proto-gen-latest -- -ra py314-test-opentelemetry-proto-gen-oldest_ubuntu-latest: name: opentelemetry-proto-gen-oldest 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.14 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py314-test-opentelemetry-proto-gen-oldest -- -ra py314-test-opentelemetry-proto-gen-latest_ubuntu-latest: name: opentelemetry-proto-gen-latest 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.14 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py314-test-opentelemetry-proto-gen-latest -- -ra py314t-test-opentelemetry-proto-gen-oldest_ubuntu-latest: name: opentelemetry-proto-gen-oldest 3.14t Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.14t uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14t" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py314t-test-opentelemetry-proto-gen-oldest -- -ra py314t-test-opentelemetry-proto-gen-latest_ubuntu-latest: name: opentelemetry-proto-gen-latest 3.14t Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.14t uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14t" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py314t-test-opentelemetry-proto-gen-latest -- -ra pypy3-test-opentelemetry-proto-gen-oldest_ubuntu-latest: name: opentelemetry-proto-gen-oldest pypy-3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python pypy-3.10 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "pypy-3.10" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e pypy3-test-opentelemetry-proto-gen-oldest -- -ra pypy3-test-opentelemetry-proto-gen-latest_ubuntu-latest: name: opentelemetry-proto-gen-latest pypy-3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python pypy-3.10 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "pypy-3.10" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e pypy3-test-opentelemetry-proto-gen-latest -- -ra py310-test-opentelemetry-protojson-gen-oldest_ubuntu-latest: name: opentelemetry-protojson-gen-oldest 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.10 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.10" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py310-test-opentelemetry-protojson-gen-oldest -- -ra py310-test-opentelemetry-protojson-gen-latest_ubuntu-latest: name: opentelemetry-protojson-gen-latest 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.10 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.10" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py310-test-opentelemetry-protojson-gen-latest -- -ra py311-test-opentelemetry-protojson-gen-oldest_ubuntu-latest: name: opentelemetry-protojson-gen-oldest 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.11 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.11" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py311-test-opentelemetry-protojson-gen-oldest -- -ra py311-test-opentelemetry-protojson-gen-latest_ubuntu-latest: name: opentelemetry-protojson-gen-latest 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.11 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.11" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py311-test-opentelemetry-protojson-gen-latest -- -ra py312-test-opentelemetry-protojson-gen-oldest_ubuntu-latest: name: opentelemetry-protojson-gen-oldest 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.12 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.12" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py312-test-opentelemetry-protojson-gen-oldest -- -ra py312-test-opentelemetry-protojson-gen-latest_ubuntu-latest: name: opentelemetry-protojson-gen-latest 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.12 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.12" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py312-test-opentelemetry-protojson-gen-latest -- -ra py313-test-opentelemetry-protojson-gen-oldest_ubuntu-latest: name: opentelemetry-protojson-gen-oldest 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.13 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.13" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py313-test-opentelemetry-protojson-gen-oldest -- -ra py313-test-opentelemetry-protojson-gen-latest_ubuntu-latest: name: opentelemetry-protojson-gen-latest 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.13 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.13" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py313-test-opentelemetry-protojson-gen-latest -- -ra py314-test-opentelemetry-protojson-gen-oldest_ubuntu-latest: name: opentelemetry-protojson-gen-oldest 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.14 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py314-test-opentelemetry-protojson-gen-oldest -- -ra py314-test-opentelemetry-protojson-gen-latest_ubuntu-latest: name: opentelemetry-protojson-gen-latest 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.14 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py314-test-opentelemetry-protojson-gen-latest -- -ra py314t-test-opentelemetry-protojson-gen-oldest_ubuntu-latest: name: opentelemetry-protojson-gen-oldest 3.14t Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.14t uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14t" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py314t-test-opentelemetry-protojson-gen-oldest -- -ra py314t-test-opentelemetry-protojson-gen-latest_ubuntu-latest: name: opentelemetry-protojson-gen-latest 3.14t Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.14t uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14t" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py314t-test-opentelemetry-protojson-gen-latest -- -ra pypy3-test-opentelemetry-protojson-gen-oldest_ubuntu-latest: name: opentelemetry-protojson-gen-oldest pypy-3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python pypy-3.10 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "pypy-3.10" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e pypy3-test-opentelemetry-protojson-gen-oldest -- -ra pypy3-test-opentelemetry-protojson-gen-latest_ubuntu-latest: name: opentelemetry-protojson-gen-latest pypy-3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python pypy-3.10 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "pypy-3.10" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e pypy3-test-opentelemetry-protojson-gen-latest -- -ra py310-test-opentelemetry-codegen-json_ubuntu-latest: name: opentelemetry-codegen-json 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.10 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.10" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py310-test-opentelemetry-codegen-json -- -ra py311-test-opentelemetry-codegen-json_ubuntu-latest: name: opentelemetry-codegen-json 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.11 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.11" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py311-test-opentelemetry-codegen-json -- -ra py312-test-opentelemetry-codegen-json_ubuntu-latest: name: opentelemetry-codegen-json 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.12 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.12" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py312-test-opentelemetry-codegen-json -- -ra py313-test-opentelemetry-codegen-json_ubuntu-latest: name: opentelemetry-codegen-json 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.13 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.13" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py313-test-opentelemetry-codegen-json -- -ra py314-test-opentelemetry-codegen-json_ubuntu-latest: name: opentelemetry-codegen-json 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.14 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py314-test-opentelemetry-codegen-json -- -ra py310-test-opentelemetry-sdk_ubuntu-latest: name: opentelemetry-sdk 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.10 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.10" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py310-test-opentelemetry-sdk -- -ra py311-test-opentelemetry-sdk_ubuntu-latest: name: opentelemetry-sdk 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.11 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.11" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py311-test-opentelemetry-sdk -- -ra py312-test-opentelemetry-sdk_ubuntu-latest: name: opentelemetry-sdk 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.12 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.12" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py312-test-opentelemetry-sdk -- -ra py313-test-opentelemetry-sdk_ubuntu-latest: name: opentelemetry-sdk 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.13 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.13" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py313-test-opentelemetry-sdk -- -ra py314-test-opentelemetry-sdk_ubuntu-latest: name: opentelemetry-sdk 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.14 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py314-test-opentelemetry-sdk -- -ra py314t-test-opentelemetry-sdk_ubuntu-latest: name: opentelemetry-sdk 3.14t Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.14t uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14t" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py314t-test-opentelemetry-sdk -- -ra pypy3-test-opentelemetry-sdk_ubuntu-latest: name: opentelemetry-sdk pypy-3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python pypy-3.10 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "pypy-3.10" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e pypy3-test-opentelemetry-sdk -- -ra py310-test-opentelemetry-configuration_ubuntu-latest: name: opentelemetry-configuration 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.10 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.10" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py310-test-opentelemetry-configuration -- -ra py311-test-opentelemetry-configuration_ubuntu-latest: name: opentelemetry-configuration 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.11 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.11" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py311-test-opentelemetry-configuration -- -ra py312-test-opentelemetry-configuration_ubuntu-latest: name: opentelemetry-configuration 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.12 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.12" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py312-test-opentelemetry-configuration -- -ra py313-test-opentelemetry-configuration_ubuntu-latest: name: opentelemetry-configuration 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.13 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.13" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py313-test-opentelemetry-configuration -- -ra py314-test-opentelemetry-configuration_ubuntu-latest: name: opentelemetry-configuration 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.14 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py314-test-opentelemetry-configuration -- -ra py314t-test-opentelemetry-configuration_ubuntu-latest: name: opentelemetry-configuration 3.14t Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.14t uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14t" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py314t-test-opentelemetry-configuration -- -ra pypy3-test-opentelemetry-configuration_ubuntu-latest: name: opentelemetry-configuration pypy-3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python pypy-3.10 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "pypy-3.10" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e pypy3-test-opentelemetry-configuration -- -ra py310-test-opentelemetry-semantic-conventions_ubuntu-latest: name: opentelemetry-semantic-conventions 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.10 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.10" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py310-test-opentelemetry-semantic-conventions -- -ra py311-test-opentelemetry-semantic-conventions_ubuntu-latest: name: opentelemetry-semantic-conventions 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.11 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.11" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py311-test-opentelemetry-semantic-conventions -- -ra py312-test-opentelemetry-semantic-conventions_ubuntu-latest: name: opentelemetry-semantic-conventions 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.12 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.12" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py312-test-opentelemetry-semantic-conventions -- -ra py313-test-opentelemetry-semantic-conventions_ubuntu-latest: name: opentelemetry-semantic-conventions 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.13 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.13" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py313-test-opentelemetry-semantic-conventions -- -ra py314-test-opentelemetry-semantic-conventions_ubuntu-latest: name: opentelemetry-semantic-conventions 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.14 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py314-test-opentelemetry-semantic-conventions -- -ra py314t-test-opentelemetry-semantic-conventions_ubuntu-latest: name: opentelemetry-semantic-conventions 3.14t Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.14t uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14t" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py314t-test-opentelemetry-semantic-conventions -- -ra pypy3-test-opentelemetry-semantic-conventions_ubuntu-latest: name: opentelemetry-semantic-conventions pypy-3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python pypy-3.10 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "pypy-3.10" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e pypy3-test-opentelemetry-semantic-conventions -- -ra py310-test-opentelemetry-getting-started_ubuntu-latest: name: opentelemetry-getting-started 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Checkout contrib repo @ ${{ env.CONTRIB_REPO_SHA }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false repository: open-telemetry/opentelemetry-python-contrib ref: ${{ env.CONTRIB_REPO_SHA }} path: opentelemetry-python-contrib - name: Set up Python 3.10 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.10" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py310-test-opentelemetry-getting-started -- -ra py311-test-opentelemetry-getting-started_ubuntu-latest: name: opentelemetry-getting-started 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Checkout contrib repo @ ${{ env.CONTRIB_REPO_SHA }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false repository: open-telemetry/opentelemetry-python-contrib ref: ${{ env.CONTRIB_REPO_SHA }} path: opentelemetry-python-contrib - name: Set up Python 3.11 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.11" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py311-test-opentelemetry-getting-started -- -ra py312-test-opentelemetry-getting-started_ubuntu-latest: name: opentelemetry-getting-started 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Checkout contrib repo @ ${{ env.CONTRIB_REPO_SHA }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false repository: open-telemetry/opentelemetry-python-contrib ref: ${{ env.CONTRIB_REPO_SHA }} path: opentelemetry-python-contrib - name: Set up Python 3.12 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.12" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py312-test-opentelemetry-getting-started -- -ra py313-test-opentelemetry-getting-started_ubuntu-latest: name: opentelemetry-getting-started 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Checkout contrib repo @ ${{ env.CONTRIB_REPO_SHA }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false repository: open-telemetry/opentelemetry-python-contrib ref: ${{ env.CONTRIB_REPO_SHA }} path: opentelemetry-python-contrib - name: Set up Python 3.13 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.13" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py313-test-opentelemetry-getting-started -- -ra py314-test-opentelemetry-getting-started_ubuntu-latest: name: opentelemetry-getting-started 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Checkout contrib repo @ ${{ env.CONTRIB_REPO_SHA }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false repository: open-telemetry/opentelemetry-python-contrib ref: ${{ env.CONTRIB_REPO_SHA }} path: opentelemetry-python-contrib - name: Set up Python 3.14 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py314-test-opentelemetry-getting-started -- -ra py310-test-opentelemetry-opentracing-shim_ubuntu-latest: name: opentelemetry-opentracing-shim 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.10 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.10" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py310-test-opentelemetry-opentracing-shim -- -ra py311-test-opentelemetry-opentracing-shim_ubuntu-latest: name: opentelemetry-opentracing-shim 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.11 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.11" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py311-test-opentelemetry-opentracing-shim -- -ra py312-test-opentelemetry-opentracing-shim_ubuntu-latest: name: opentelemetry-opentracing-shim 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.12 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.12" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py312-test-opentelemetry-opentracing-shim -- -ra py313-test-opentelemetry-opentracing-shim_ubuntu-latest: name: opentelemetry-opentracing-shim 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.13 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.13" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py313-test-opentelemetry-opentracing-shim -- -ra py314-test-opentelemetry-opentracing-shim_ubuntu-latest: name: opentelemetry-opentracing-shim 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.14 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py314-test-opentelemetry-opentracing-shim -- -ra py314t-test-opentelemetry-opentracing-shim_ubuntu-latest: name: opentelemetry-opentracing-shim 3.14t Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.14t uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14t" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py314t-test-opentelemetry-opentracing-shim -- -ra pypy3-test-opentelemetry-opentracing-shim_ubuntu-latest: name: opentelemetry-opentracing-shim pypy-3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python pypy-3.10 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "pypy-3.10" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e pypy3-test-opentelemetry-opentracing-shim -- -ra py310-test-opentelemetry-opencensus-shim_ubuntu-latest: name: opentelemetry-opencensus-shim 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.10 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.10" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py310-test-opentelemetry-opencensus-shim -- -ra py311-test-opentelemetry-opencensus-shim_ubuntu-latest: name: opentelemetry-opencensus-shim 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.11 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.11" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py311-test-opentelemetry-opencensus-shim -- -ra py312-test-opentelemetry-opencensus-shim_ubuntu-latest: name: opentelemetry-opencensus-shim 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.12 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.12" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py312-test-opentelemetry-opencensus-shim -- -ra py313-test-opentelemetry-opencensus-shim_ubuntu-latest: name: opentelemetry-opencensus-shim 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.13 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.13" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py313-test-opentelemetry-opencensus-shim -- -ra py314-test-opentelemetry-opencensus-shim_ubuntu-latest: name: opentelemetry-opencensus-shim 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.14 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py314-test-opentelemetry-opencensus-shim -- -ra py310-test-opentelemetry-exporter-http-transport-oldest_ubuntu-latest: name: opentelemetry-exporter-http-transport-oldest 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.10 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.10" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py310-test-opentelemetry-exporter-http-transport-oldest -- -ra py310-test-opentelemetry-exporter-http-transport-latest_ubuntu-latest: name: opentelemetry-exporter-http-transport-latest 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.10 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.10" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py310-test-opentelemetry-exporter-http-transport-latest -- -ra py311-test-opentelemetry-exporter-http-transport-oldest_ubuntu-latest: name: opentelemetry-exporter-http-transport-oldest 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.11 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.11" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py311-test-opentelemetry-exporter-http-transport-oldest -- -ra py311-test-opentelemetry-exporter-http-transport-latest_ubuntu-latest: name: opentelemetry-exporter-http-transport-latest 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.11 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.11" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py311-test-opentelemetry-exporter-http-transport-latest -- -ra py312-test-opentelemetry-exporter-http-transport-oldest_ubuntu-latest: name: opentelemetry-exporter-http-transport-oldest 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.12 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.12" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py312-test-opentelemetry-exporter-http-transport-oldest -- -ra py312-test-opentelemetry-exporter-http-transport-latest_ubuntu-latest: name: opentelemetry-exporter-http-transport-latest 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.12 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.12" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py312-test-opentelemetry-exporter-http-transport-latest -- -ra py313-test-opentelemetry-exporter-http-transport-oldest_ubuntu-latest: name: opentelemetry-exporter-http-transport-oldest 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.13 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.13" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py313-test-opentelemetry-exporter-http-transport-oldest -- -ra py313-test-opentelemetry-exporter-http-transport-latest_ubuntu-latest: name: opentelemetry-exporter-http-transport-latest 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.13 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.13" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py313-test-opentelemetry-exporter-http-transport-latest -- -ra py314-test-opentelemetry-exporter-http-transport-oldest_ubuntu-latest: name: opentelemetry-exporter-http-transport-oldest 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.14 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py314-test-opentelemetry-exporter-http-transport-oldest -- -ra py314-test-opentelemetry-exporter-http-transport-latest_ubuntu-latest: name: opentelemetry-exporter-http-transport-latest 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.14 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py314-test-opentelemetry-exporter-http-transport-latest -- -ra py314t-test-opentelemetry-exporter-http-transport-oldest_ubuntu-latest: name: opentelemetry-exporter-http-transport-oldest 3.14t Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.14t uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14t" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py314t-test-opentelemetry-exporter-http-transport-oldest -- -ra py314t-test-opentelemetry-exporter-http-transport-latest_ubuntu-latest: name: opentelemetry-exporter-http-transport-latest 3.14t Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.14t uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14t" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py314t-test-opentelemetry-exporter-http-transport-latest -- -ra py310-test-opentelemetry-exporter-otlpcommon-oldest_ubuntu-latest: name: opentelemetry-exporter-otlpcommon-oldest 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.10 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.10" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py310-test-opentelemetry-exporter-otlpcommon-oldest -- -ra py310-test-opentelemetry-exporter-otlpcommon-latest_ubuntu-latest: name: opentelemetry-exporter-otlpcommon-latest 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.10 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.10" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py310-test-opentelemetry-exporter-otlpcommon-latest -- -ra py311-test-opentelemetry-exporter-otlpcommon-oldest_ubuntu-latest: name: opentelemetry-exporter-otlpcommon-oldest 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.11 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.11" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py311-test-opentelemetry-exporter-otlpcommon-oldest -- -ra py311-test-opentelemetry-exporter-otlpcommon-latest_ubuntu-latest: name: opentelemetry-exporter-otlpcommon-latest 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.11 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.11" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py311-test-opentelemetry-exporter-otlpcommon-latest -- -ra py312-test-opentelemetry-exporter-otlpcommon-oldest_ubuntu-latest: name: opentelemetry-exporter-otlpcommon-oldest 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.12 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.12" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py312-test-opentelemetry-exporter-otlpcommon-oldest -- -ra py312-test-opentelemetry-exporter-otlpcommon-latest_ubuntu-latest: name: opentelemetry-exporter-otlpcommon-latest 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.12 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.12" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py312-test-opentelemetry-exporter-otlpcommon-latest -- -ra py313-test-opentelemetry-exporter-otlpcommon-oldest_ubuntu-latest: name: opentelemetry-exporter-otlpcommon-oldest 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.13 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.13" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py313-test-opentelemetry-exporter-otlpcommon-oldest -- -ra py313-test-opentelemetry-exporter-otlpcommon-latest_ubuntu-latest: name: opentelemetry-exporter-otlpcommon-latest 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.13 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.13" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py313-test-opentelemetry-exporter-otlpcommon-latest -- -ra py314-test-opentelemetry-exporter-otlpcommon-oldest_ubuntu-latest: name: opentelemetry-exporter-otlpcommon-oldest 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.14 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py314-test-opentelemetry-exporter-otlpcommon-oldest -- -ra py314-test-opentelemetry-exporter-otlpcommon-latest_ubuntu-latest: name: opentelemetry-exporter-otlpcommon-latest 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.14 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py314-test-opentelemetry-exporter-otlpcommon-latest -- -ra py314t-test-opentelemetry-exporter-otlpcommon-oldest_ubuntu-latest: name: opentelemetry-exporter-otlpcommon-oldest 3.14t Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.14t uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14t" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py314t-test-opentelemetry-exporter-otlpcommon-oldest -- -ra py314t-test-opentelemetry-exporter-otlpcommon-latest_ubuntu-latest: name: opentelemetry-exporter-otlpcommon-latest 3.14t Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.14t uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14t" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py314t-test-opentelemetry-exporter-otlpcommon-latest -- -ra py310-test-opentelemetry-exporter-opencensus_ubuntu-latest: name: opentelemetry-exporter-opencensus 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.10 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.10" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py310-test-opentelemetry-exporter-opencensus -- -ra py311-test-opentelemetry-exporter-opencensus_ubuntu-latest: name: opentelemetry-exporter-opencensus 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.11 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.11" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py311-test-opentelemetry-exporter-opencensus -- -ra py312-test-opentelemetry-exporter-opencensus_ubuntu-latest: name: opentelemetry-exporter-opencensus 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.12 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.12" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py312-test-opentelemetry-exporter-opencensus -- -ra py313-test-opentelemetry-exporter-opencensus_ubuntu-latest: name: opentelemetry-exporter-opencensus 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.13 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.13" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py313-test-opentelemetry-exporter-opencensus -- -ra py314-test-opentelemetry-exporter-opencensus_ubuntu-latest: name: opentelemetry-exporter-opencensus 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.14 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py314-test-opentelemetry-exporter-opencensus -- -ra py310-test-opentelemetry-exporter-otlp-proto-common_ubuntu-latest: name: opentelemetry-exporter-otlp-proto-common 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.10 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.10" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py310-test-opentelemetry-exporter-otlp-proto-common -- -ra py311-test-opentelemetry-exporter-otlp-proto-common_ubuntu-latest: name: opentelemetry-exporter-otlp-proto-common 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.11 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.11" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py311-test-opentelemetry-exporter-otlp-proto-common -- -ra py312-test-opentelemetry-exporter-otlp-proto-common_ubuntu-latest: name: opentelemetry-exporter-otlp-proto-common 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.12 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.12" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py312-test-opentelemetry-exporter-otlp-proto-common -- -ra py313-test-opentelemetry-exporter-otlp-proto-common_ubuntu-latest: name: opentelemetry-exporter-otlp-proto-common 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.13 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.13" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py313-test-opentelemetry-exporter-otlp-proto-common -- -ra py314-test-opentelemetry-exporter-otlp-proto-common_ubuntu-latest: name: opentelemetry-exporter-otlp-proto-common 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.14 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py314-test-opentelemetry-exporter-otlp-proto-common -- -ra py314t-test-opentelemetry-exporter-otlp-proto-common_ubuntu-latest: name: opentelemetry-exporter-otlp-proto-common 3.14t Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.14t uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14t" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py314t-test-opentelemetry-exporter-otlp-proto-common -- -ra pypy3-test-opentelemetry-exporter-otlp-proto-common_ubuntu-latest: name: opentelemetry-exporter-otlp-proto-common pypy-3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python pypy-3.10 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "pypy-3.10" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e pypy3-test-opentelemetry-exporter-otlp-proto-common -- -ra py310-test-opentelemetry-exporter-otlp-json-common_ubuntu-latest: name: opentelemetry-exporter-otlp-json-common 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.10 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.10" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py310-test-opentelemetry-exporter-otlp-json-common -- -ra py311-test-opentelemetry-exporter-otlp-json-common_ubuntu-latest: name: opentelemetry-exporter-otlp-json-common 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.11 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.11" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py311-test-opentelemetry-exporter-otlp-json-common -- -ra py312-test-opentelemetry-exporter-otlp-json-common_ubuntu-latest: name: opentelemetry-exporter-otlp-json-common 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.12 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.12" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py312-test-opentelemetry-exporter-otlp-json-common -- -ra py313-test-opentelemetry-exporter-otlp-json-common_ubuntu-latest: name: opentelemetry-exporter-otlp-json-common 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.13 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.13" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py313-test-opentelemetry-exporter-otlp-json-common -- -ra py314-test-opentelemetry-exporter-otlp-json-common_ubuntu-latest: name: opentelemetry-exporter-otlp-json-common 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.14 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py314-test-opentelemetry-exporter-otlp-json-common -- -ra py314t-test-opentelemetry-exporter-otlp-json-common_ubuntu-latest: name: opentelemetry-exporter-otlp-json-common 3.14t Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.14t uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14t" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py314t-test-opentelemetry-exporter-otlp-json-common -- -ra pypy3-test-opentelemetry-exporter-otlp-json-common_ubuntu-latest: name: opentelemetry-exporter-otlp-json-common pypy-3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python pypy-3.10 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "pypy-3.10" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e pypy3-test-opentelemetry-exporter-otlp-json-common -- -ra py310-test-opentelemetry-exporter-otlp-combined_ubuntu-latest: name: opentelemetry-exporter-otlp-combined 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.10 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.10" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py310-test-opentelemetry-exporter-otlp-combined -- -ra py311-test-opentelemetry-exporter-otlp-combined_ubuntu-latest: name: opentelemetry-exporter-otlp-combined 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.11 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.11" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py311-test-opentelemetry-exporter-otlp-combined -- -ra py312-test-opentelemetry-exporter-otlp-combined_ubuntu-latest: name: opentelemetry-exporter-otlp-combined 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.12 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.12" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py312-test-opentelemetry-exporter-otlp-combined -- -ra py313-test-opentelemetry-exporter-otlp-combined_ubuntu-latest: name: opentelemetry-exporter-otlp-combined 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.13 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.13" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py313-test-opentelemetry-exporter-otlp-combined -- -ra py314-test-opentelemetry-exporter-otlp-combined_ubuntu-latest: name: opentelemetry-exporter-otlp-combined 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.14 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py314-test-opentelemetry-exporter-otlp-combined -- -ra py310-test-opentelemetry-exporter-otlp-json-file_ubuntu-latest: name: opentelemetry-exporter-otlp-json-file 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.10 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.10" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py310-test-opentelemetry-exporter-otlp-json-file -- -ra py311-test-opentelemetry-exporter-otlp-json-file_ubuntu-latest: name: opentelemetry-exporter-otlp-json-file 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.11 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.11" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py311-test-opentelemetry-exporter-otlp-json-file -- -ra py312-test-opentelemetry-exporter-otlp-json-file_ubuntu-latest: name: opentelemetry-exporter-otlp-json-file 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.12 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.12" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py312-test-opentelemetry-exporter-otlp-json-file -- -ra py313-test-opentelemetry-exporter-otlp-json-file_ubuntu-latest: name: opentelemetry-exporter-otlp-json-file 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.13 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.13" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py313-test-opentelemetry-exporter-otlp-json-file -- -ra py314-test-opentelemetry-exporter-otlp-json-file_ubuntu-latest: name: opentelemetry-exporter-otlp-json-file 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.14 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py314-test-opentelemetry-exporter-otlp-json-file -- -ra py314t-test-opentelemetry-exporter-otlp-json-file_ubuntu-latest: name: opentelemetry-exporter-otlp-json-file 3.14t Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.14t uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14t" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py314t-test-opentelemetry-exporter-otlp-json-file -- -ra pypy3-test-opentelemetry-exporter-otlp-json-file_ubuntu-latest: name: opentelemetry-exporter-otlp-json-file pypy-3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python pypy-3.10 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "pypy-3.10" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e pypy3-test-opentelemetry-exporter-otlp-json-file -- -ra py310-test-opentelemetry-exporter-otlp-json-http_ubuntu-latest: name: opentelemetry-exporter-otlp-json-http 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.10 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.10" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py310-test-opentelemetry-exporter-otlp-json-http -- -ra py311-test-opentelemetry-exporter-otlp-json-http_ubuntu-latest: name: opentelemetry-exporter-otlp-json-http 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.11 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.11" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py311-test-opentelemetry-exporter-otlp-json-http -- -ra py312-test-opentelemetry-exporter-otlp-json-http_ubuntu-latest: name: opentelemetry-exporter-otlp-json-http 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.12 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.12" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py312-test-opentelemetry-exporter-otlp-json-http -- -ra py313-test-opentelemetry-exporter-otlp-json-http_ubuntu-latest: name: opentelemetry-exporter-otlp-json-http 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.13 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.13" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py313-test-opentelemetry-exporter-otlp-json-http -- -ra py314-test-opentelemetry-exporter-otlp-json-http_ubuntu-latest: name: opentelemetry-exporter-otlp-json-http 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.14 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py314-test-opentelemetry-exporter-otlp-json-http -- -ra py314t-test-opentelemetry-exporter-otlp-json-http_ubuntu-latest: name: opentelemetry-exporter-otlp-json-http 3.14t Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.14t uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14t" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py314t-test-opentelemetry-exporter-otlp-json-http -- -ra pypy3-test-opentelemetry-exporter-otlp-json-http_ubuntu-latest: name: opentelemetry-exporter-otlp-json-http pypy-3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python pypy-3.10 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "pypy-3.10" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e pypy3-test-opentelemetry-exporter-otlp-json-http -- -ra py310-test-opentelemetry-exporter-otlp-proto-grpc-oldest_ubuntu-latest: name: opentelemetry-exporter-otlp-proto-grpc-oldest 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.10 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.10" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py310-test-opentelemetry-exporter-otlp-proto-grpc-oldest -- -ra py310-test-opentelemetry-exporter-otlp-proto-grpc-latest_ubuntu-latest: name: opentelemetry-exporter-otlp-proto-grpc-latest 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.10 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.10" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py310-test-opentelemetry-exporter-otlp-proto-grpc-latest -- -ra py311-test-opentelemetry-exporter-otlp-proto-grpc-oldest_ubuntu-latest: name: opentelemetry-exporter-otlp-proto-grpc-oldest 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.11 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.11" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py311-test-opentelemetry-exporter-otlp-proto-grpc-oldest -- -ra py311-test-opentelemetry-exporter-otlp-proto-grpc-latest_ubuntu-latest: name: opentelemetry-exporter-otlp-proto-grpc-latest 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.11 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.11" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py311-test-opentelemetry-exporter-otlp-proto-grpc-latest -- -ra py312-test-opentelemetry-exporter-otlp-proto-grpc-oldest_ubuntu-latest: name: opentelemetry-exporter-otlp-proto-grpc-oldest 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.12 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.12" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py312-test-opentelemetry-exporter-otlp-proto-grpc-oldest -- -ra py312-test-opentelemetry-exporter-otlp-proto-grpc-latest_ubuntu-latest: name: opentelemetry-exporter-otlp-proto-grpc-latest 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.12 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.12" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py312-test-opentelemetry-exporter-otlp-proto-grpc-latest -- -ra py313-test-opentelemetry-exporter-otlp-proto-grpc-oldest_ubuntu-latest: name: opentelemetry-exporter-otlp-proto-grpc-oldest 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.13 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.13" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py313-test-opentelemetry-exporter-otlp-proto-grpc-oldest -- -ra py313-test-opentelemetry-exporter-otlp-proto-grpc-latest_ubuntu-latest: name: opentelemetry-exporter-otlp-proto-grpc-latest 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.13 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.13" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py313-test-opentelemetry-exporter-otlp-proto-grpc-latest -- -ra py314-test-opentelemetry-exporter-otlp-proto-grpc-oldest_ubuntu-latest: name: opentelemetry-exporter-otlp-proto-grpc-oldest 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.14 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py314-test-opentelemetry-exporter-otlp-proto-grpc-oldest -- -ra py314-test-opentelemetry-exporter-otlp-proto-grpc-latest_ubuntu-latest: name: opentelemetry-exporter-otlp-proto-grpc-latest 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.14 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py314-test-opentelemetry-exporter-otlp-proto-grpc-latest -- -ra py310-test-opentelemetry-exporter-otlp-proto-http_ubuntu-latest: name: opentelemetry-exporter-otlp-proto-http 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.10 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.10" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py310-test-opentelemetry-exporter-otlp-proto-http -- -ra py311-test-opentelemetry-exporter-otlp-proto-http_ubuntu-latest: name: opentelemetry-exporter-otlp-proto-http 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.11 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.11" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py311-test-opentelemetry-exporter-otlp-proto-http -- -ra py312-test-opentelemetry-exporter-otlp-proto-http_ubuntu-latest: name: opentelemetry-exporter-otlp-proto-http 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.12 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.12" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py312-test-opentelemetry-exporter-otlp-proto-http -- -ra py313-test-opentelemetry-exporter-otlp-proto-http_ubuntu-latest: name: opentelemetry-exporter-otlp-proto-http 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.13 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.13" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py313-test-opentelemetry-exporter-otlp-proto-http -- -ra py314-test-opentelemetry-exporter-otlp-proto-http_ubuntu-latest: name: opentelemetry-exporter-otlp-proto-http 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.14 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py314-test-opentelemetry-exporter-otlp-proto-http -- -ra py314t-test-opentelemetry-exporter-otlp-proto-http_ubuntu-latest: name: opentelemetry-exporter-otlp-proto-http 3.14t Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.14t uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14t" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py314t-test-opentelemetry-exporter-otlp-proto-http -- -ra pypy3-test-opentelemetry-exporter-otlp-proto-http_ubuntu-latest: name: opentelemetry-exporter-otlp-proto-http pypy-3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python pypy-3.10 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "pypy-3.10" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e pypy3-test-opentelemetry-exporter-otlp-proto-http -- -ra py310-test-opentelemetry-exporter-prometheus_ubuntu-latest: name: opentelemetry-exporter-prometheus 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.10 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.10" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py310-test-opentelemetry-exporter-prometheus -- -ra py311-test-opentelemetry-exporter-prometheus_ubuntu-latest: name: opentelemetry-exporter-prometheus 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.11 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.11" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py311-test-opentelemetry-exporter-prometheus -- -ra py312-test-opentelemetry-exporter-prometheus_ubuntu-latest: name: opentelemetry-exporter-prometheus 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.12 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.12" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py312-test-opentelemetry-exporter-prometheus -- -ra py313-test-opentelemetry-exporter-prometheus_ubuntu-latest: name: opentelemetry-exporter-prometheus 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.13 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.13" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py313-test-opentelemetry-exporter-prometheus -- -ra py314-test-opentelemetry-exporter-prometheus_ubuntu-latest: name: opentelemetry-exporter-prometheus 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.14 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py314-test-opentelemetry-exporter-prometheus -- -ra py314t-test-opentelemetry-exporter-prometheus_ubuntu-latest: name: opentelemetry-exporter-prometheus 3.14t Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.14t uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14t" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py314t-test-opentelemetry-exporter-prometheus -- -ra pypy3-test-opentelemetry-exporter-prometheus_ubuntu-latest: name: opentelemetry-exporter-prometheus pypy-3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python pypy-3.10 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "pypy-3.10" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e pypy3-test-opentelemetry-exporter-prometheus -- -ra py310-test-opentelemetry-exporter-zipkin-combined_ubuntu-latest: name: opentelemetry-exporter-zipkin-combined 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.10 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.10" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py310-test-opentelemetry-exporter-zipkin-combined -- -ra py311-test-opentelemetry-exporter-zipkin-combined_ubuntu-latest: name: opentelemetry-exporter-zipkin-combined 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.11 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.11" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py311-test-opentelemetry-exporter-zipkin-combined -- -ra py312-test-opentelemetry-exporter-zipkin-combined_ubuntu-latest: name: opentelemetry-exporter-zipkin-combined 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.12 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.12" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py312-test-opentelemetry-exporter-zipkin-combined -- -ra py313-test-opentelemetry-exporter-zipkin-combined_ubuntu-latest: name: opentelemetry-exporter-zipkin-combined 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.13 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.13" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py313-test-opentelemetry-exporter-zipkin-combined -- -ra py314-test-opentelemetry-exporter-zipkin-combined_ubuntu-latest: name: opentelemetry-exporter-zipkin-combined 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.14 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py314-test-opentelemetry-exporter-zipkin-combined -- -ra py314t-test-opentelemetry-exporter-zipkin-combined_ubuntu-latest: name: opentelemetry-exporter-zipkin-combined 3.14t Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.14t uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14t" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py314t-test-opentelemetry-exporter-zipkin-combined -- -ra pypy3-test-opentelemetry-exporter-zipkin-combined_ubuntu-latest: name: opentelemetry-exporter-zipkin-combined pypy-3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python pypy-3.10 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "pypy-3.10" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e pypy3-test-opentelemetry-exporter-zipkin-combined -- -ra py310-test-opentelemetry-exporter-zipkin-proto-http_ubuntu-latest: name: opentelemetry-exporter-zipkin-proto-http 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.10 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.10" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py310-test-opentelemetry-exporter-zipkin-proto-http -- -ra py311-test-opentelemetry-exporter-zipkin-proto-http_ubuntu-latest: name: opentelemetry-exporter-zipkin-proto-http 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.11 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.11" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py311-test-opentelemetry-exporter-zipkin-proto-http -- -ra py312-test-opentelemetry-exporter-zipkin-proto-http_ubuntu-latest: name: opentelemetry-exporter-zipkin-proto-http 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.12 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.12" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py312-test-opentelemetry-exporter-zipkin-proto-http -- -ra py313-test-opentelemetry-exporter-zipkin-proto-http_ubuntu-latest: name: opentelemetry-exporter-zipkin-proto-http 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.13 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.13" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py313-test-opentelemetry-exporter-zipkin-proto-http -- -ra py314-test-opentelemetry-exporter-zipkin-proto-http_ubuntu-latest: name: opentelemetry-exporter-zipkin-proto-http 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.14 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py314-test-opentelemetry-exporter-zipkin-proto-http -- -ra py314t-test-opentelemetry-exporter-zipkin-proto-http_ubuntu-latest: name: opentelemetry-exporter-zipkin-proto-http 3.14t Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.14t uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14t" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py314t-test-opentelemetry-exporter-zipkin-proto-http -- -ra pypy3-test-opentelemetry-exporter-zipkin-proto-http_ubuntu-latest: name: opentelemetry-exporter-zipkin-proto-http pypy-3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python pypy-3.10 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "pypy-3.10" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e pypy3-test-opentelemetry-exporter-zipkin-proto-http -- -ra py310-test-opentelemetry-exporter-zipkin-json_ubuntu-latest: name: opentelemetry-exporter-zipkin-json 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.10 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.10" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py310-test-opentelemetry-exporter-zipkin-json -- -ra py311-test-opentelemetry-exporter-zipkin-json_ubuntu-latest: name: opentelemetry-exporter-zipkin-json 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.11 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.11" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py311-test-opentelemetry-exporter-zipkin-json -- -ra py312-test-opentelemetry-exporter-zipkin-json_ubuntu-latest: name: opentelemetry-exporter-zipkin-json 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.12 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.12" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py312-test-opentelemetry-exporter-zipkin-json -- -ra py313-test-opentelemetry-exporter-zipkin-json_ubuntu-latest: name: opentelemetry-exporter-zipkin-json 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.13 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.13" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py313-test-opentelemetry-exporter-zipkin-json -- -ra py314-test-opentelemetry-exporter-zipkin-json_ubuntu-latest: name: opentelemetry-exporter-zipkin-json 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.14 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py314-test-opentelemetry-exporter-zipkin-json -- -ra py314t-test-opentelemetry-exporter-zipkin-json_ubuntu-latest: name: opentelemetry-exporter-zipkin-json 3.14t Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.14t uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14t" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py314t-test-opentelemetry-exporter-zipkin-json -- -ra pypy3-test-opentelemetry-exporter-zipkin-json_ubuntu-latest: name: opentelemetry-exporter-zipkin-json pypy-3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python pypy-3.10 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "pypy-3.10" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e pypy3-test-opentelemetry-exporter-zipkin-json -- -ra py310-test-opentelemetry-propagator-b3_ubuntu-latest: name: opentelemetry-propagator-b3 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.10 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.10" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py310-test-opentelemetry-propagator-b3 -- -ra py311-test-opentelemetry-propagator-b3_ubuntu-latest: name: opentelemetry-propagator-b3 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.11 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.11" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py311-test-opentelemetry-propagator-b3 -- -ra py312-test-opentelemetry-propagator-b3_ubuntu-latest: name: opentelemetry-propagator-b3 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.12 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.12" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py312-test-opentelemetry-propagator-b3 -- -ra py313-test-opentelemetry-propagator-b3_ubuntu-latest: name: opentelemetry-propagator-b3 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.13 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.13" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py313-test-opentelemetry-propagator-b3 -- -ra py314-test-opentelemetry-propagator-b3_ubuntu-latest: name: opentelemetry-propagator-b3 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.14 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py314-test-opentelemetry-propagator-b3 -- -ra py314t-test-opentelemetry-propagator-b3_ubuntu-latest: name: opentelemetry-propagator-b3 3.14t Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.14t uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14t" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py314t-test-opentelemetry-propagator-b3 -- -ra pypy3-test-opentelemetry-propagator-b3_ubuntu-latest: name: opentelemetry-propagator-b3 pypy-3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python pypy-3.10 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "pypy-3.10" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e pypy3-test-opentelemetry-propagator-b3 -- -ra py310-test-opentelemetry-propagator-jaeger_ubuntu-latest: name: opentelemetry-propagator-jaeger 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.10 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.10" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py310-test-opentelemetry-propagator-jaeger -- -ra py311-test-opentelemetry-propagator-jaeger_ubuntu-latest: name: opentelemetry-propagator-jaeger 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.11 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.11" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py311-test-opentelemetry-propagator-jaeger -- -ra py312-test-opentelemetry-propagator-jaeger_ubuntu-latest: name: opentelemetry-propagator-jaeger 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.12 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.12" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py312-test-opentelemetry-propagator-jaeger -- -ra py313-test-opentelemetry-propagator-jaeger_ubuntu-latest: name: opentelemetry-propagator-jaeger 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.13 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.13" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py313-test-opentelemetry-propagator-jaeger -- -ra py314-test-opentelemetry-propagator-jaeger_ubuntu-latest: name: opentelemetry-propagator-jaeger 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.14 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py314-test-opentelemetry-propagator-jaeger -- -ra py314t-test-opentelemetry-propagator-jaeger_ubuntu-latest: name: opentelemetry-propagator-jaeger 3.14t Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.14t uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14t" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py314t-test-opentelemetry-propagator-jaeger -- -ra pypy3-test-opentelemetry-propagator-jaeger_ubuntu-latest: name: opentelemetry-propagator-jaeger pypy-3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python pypy-3.10 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "pypy-3.10" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e pypy3-test-opentelemetry-propagator-jaeger -- -ra py310-test-opentelemetry-test-utils_ubuntu-latest: name: opentelemetry-test-utils 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Install weaver run: | WEAVER_URL="https://github.com/open-telemetry/weaver/releases/download/${{ env.WEAVER_VERSION }}/weaver-x86_64-unknown-linux-gnu.tar.xz" curl -sSL "$WEAVER_URL" | tar -xJ -C /tmp weaver-x86_64-unknown-linux-gnu/weaver sudo mv /tmp/weaver-x86_64-unknown-linux-gnu/weaver /usr/local/bin/weaver - name: Set up Python 3.10 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.10" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py310-test-opentelemetry-test-utils -- -ra py311-test-opentelemetry-test-utils_ubuntu-latest: name: opentelemetry-test-utils 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Install weaver run: | WEAVER_URL="https://github.com/open-telemetry/weaver/releases/download/${{ env.WEAVER_VERSION }}/weaver-x86_64-unknown-linux-gnu.tar.xz" curl -sSL "$WEAVER_URL" | tar -xJ -C /tmp weaver-x86_64-unknown-linux-gnu/weaver sudo mv /tmp/weaver-x86_64-unknown-linux-gnu/weaver /usr/local/bin/weaver - name: Set up Python 3.11 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.11" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py311-test-opentelemetry-test-utils -- -ra py312-test-opentelemetry-test-utils_ubuntu-latest: name: opentelemetry-test-utils 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Install weaver run: | WEAVER_URL="https://github.com/open-telemetry/weaver/releases/download/${{ env.WEAVER_VERSION }}/weaver-x86_64-unknown-linux-gnu.tar.xz" curl -sSL "$WEAVER_URL" | tar -xJ -C /tmp weaver-x86_64-unknown-linux-gnu/weaver sudo mv /tmp/weaver-x86_64-unknown-linux-gnu/weaver /usr/local/bin/weaver - name: Set up Python 3.12 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.12" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py312-test-opentelemetry-test-utils -- -ra py313-test-opentelemetry-test-utils_ubuntu-latest: name: opentelemetry-test-utils 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Install weaver run: | WEAVER_URL="https://github.com/open-telemetry/weaver/releases/download/${{ env.WEAVER_VERSION }}/weaver-x86_64-unknown-linux-gnu.tar.xz" curl -sSL "$WEAVER_URL" | tar -xJ -C /tmp weaver-x86_64-unknown-linux-gnu/weaver sudo mv /tmp/weaver-x86_64-unknown-linux-gnu/weaver /usr/local/bin/weaver - name: Set up Python 3.13 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.13" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py313-test-opentelemetry-test-utils -- -ra py314-test-opentelemetry-test-utils_ubuntu-latest: name: opentelemetry-test-utils 3.14 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Install weaver run: | WEAVER_URL="https://github.com/open-telemetry/weaver/releases/download/${{ env.WEAVER_VERSION }}/weaver-x86_64-unknown-linux-gnu.tar.xz" curl -sSL "$WEAVER_URL" | tar -xJ -C /tmp weaver-x86_64-unknown-linux-gnu/weaver sudo mv /tmp/weaver-x86_64-unknown-linux-gnu/weaver /usr/local/bin/weaver - name: Set up Python 3.14 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py314-test-opentelemetry-test-utils -- -ra pypy3-test-opentelemetry-test-utils_ubuntu-latest: name: opentelemetry-test-utils pypy-3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Install weaver run: | WEAVER_URL="https://github.com/open-telemetry/weaver/releases/download/${{ env.WEAVER_VERSION }}/weaver-x86_64-unknown-linux-gnu.tar.xz" curl -sSL "$WEAVER_URL" | tar -xJ -C /tmp weaver-x86_64-unknown-linux-gnu/weaver sudo mv /tmp/weaver-x86_64-unknown-linux-gnu/weaver /usr/local/bin/weaver - name: Set up Python pypy-3.10 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "pypy-3.10" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e pypy3-test-opentelemetry-test-utils -- -ra py310-test-opentelemetry-api_windows-latest: name: opentelemetry-api 3.10 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.10 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.10" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py310-test-opentelemetry-api -- -ra py311-test-opentelemetry-api_windows-latest: name: opentelemetry-api 3.11 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.11 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.11" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py311-test-opentelemetry-api -- -ra py312-test-opentelemetry-api_windows-latest: name: opentelemetry-api 3.12 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.12 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.12" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py312-test-opentelemetry-api -- -ra py313-test-opentelemetry-api_windows-latest: name: opentelemetry-api 3.13 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.13 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.13" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py313-test-opentelemetry-api -- -ra py314-test-opentelemetry-api_windows-latest: name: opentelemetry-api 3.14 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.14 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py314-test-opentelemetry-api -- -ra py314t-test-opentelemetry-api_windows-latest: name: opentelemetry-api 3.14t Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.14t uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14t" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py314t-test-opentelemetry-api -- -ra pypy3-test-opentelemetry-api_windows-latest: name: opentelemetry-api pypy-3.10 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python pypy-3.10 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "pypy-3.10" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e pypy3-test-opentelemetry-api -- -ra py310-test-opentelemetry-proto-gen-oldest_windows-latest: name: opentelemetry-proto-gen-oldest 3.10 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.10 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.10" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py310-test-opentelemetry-proto-gen-oldest -- -ra py310-test-opentelemetry-proto-gen-latest_windows-latest: name: opentelemetry-proto-gen-latest 3.10 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.10 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.10" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py310-test-opentelemetry-proto-gen-latest -- -ra py311-test-opentelemetry-proto-gen-oldest_windows-latest: name: opentelemetry-proto-gen-oldest 3.11 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.11 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.11" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py311-test-opentelemetry-proto-gen-oldest -- -ra py311-test-opentelemetry-proto-gen-latest_windows-latest: name: opentelemetry-proto-gen-latest 3.11 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.11 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.11" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py311-test-opentelemetry-proto-gen-latest -- -ra py312-test-opentelemetry-proto-gen-oldest_windows-latest: name: opentelemetry-proto-gen-oldest 3.12 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.12 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.12" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py312-test-opentelemetry-proto-gen-oldest -- -ra py312-test-opentelemetry-proto-gen-latest_windows-latest: name: opentelemetry-proto-gen-latest 3.12 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.12 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.12" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py312-test-opentelemetry-proto-gen-latest -- -ra py313-test-opentelemetry-proto-gen-oldest_windows-latest: name: opentelemetry-proto-gen-oldest 3.13 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.13 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.13" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py313-test-opentelemetry-proto-gen-oldest -- -ra py313-test-opentelemetry-proto-gen-latest_windows-latest: name: opentelemetry-proto-gen-latest 3.13 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.13 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.13" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py313-test-opentelemetry-proto-gen-latest -- -ra py314-test-opentelemetry-proto-gen-oldest_windows-latest: name: opentelemetry-proto-gen-oldest 3.14 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.14 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py314-test-opentelemetry-proto-gen-oldest -- -ra py314-test-opentelemetry-proto-gen-latest_windows-latest: name: opentelemetry-proto-gen-latest 3.14 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.14 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py314-test-opentelemetry-proto-gen-latest -- -ra py314t-test-opentelemetry-proto-gen-oldest_windows-latest: name: opentelemetry-proto-gen-oldest 3.14t Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.14t uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14t" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py314t-test-opentelemetry-proto-gen-oldest -- -ra py314t-test-opentelemetry-proto-gen-latest_windows-latest: name: opentelemetry-proto-gen-latest 3.14t Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.14t uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14t" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py314t-test-opentelemetry-proto-gen-latest -- -ra pypy3-test-opentelemetry-proto-gen-oldest_windows-latest: name: opentelemetry-proto-gen-oldest pypy-3.10 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python pypy-3.10 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "pypy-3.10" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e pypy3-test-opentelemetry-proto-gen-oldest -- -ra pypy3-test-opentelemetry-proto-gen-latest_windows-latest: name: opentelemetry-proto-gen-latest pypy-3.10 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python pypy-3.10 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "pypy-3.10" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e pypy3-test-opentelemetry-proto-gen-latest -- -ra py310-test-opentelemetry-protojson-gen-oldest_windows-latest: name: opentelemetry-protojson-gen-oldest 3.10 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.10 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.10" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py310-test-opentelemetry-protojson-gen-oldest -- -ra py310-test-opentelemetry-protojson-gen-latest_windows-latest: name: opentelemetry-protojson-gen-latest 3.10 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.10 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.10" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py310-test-opentelemetry-protojson-gen-latest -- -ra py311-test-opentelemetry-protojson-gen-oldest_windows-latest: name: opentelemetry-protojson-gen-oldest 3.11 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.11 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.11" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py311-test-opentelemetry-protojson-gen-oldest -- -ra py311-test-opentelemetry-protojson-gen-latest_windows-latest: name: opentelemetry-protojson-gen-latest 3.11 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.11 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.11" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py311-test-opentelemetry-protojson-gen-latest -- -ra py312-test-opentelemetry-protojson-gen-oldest_windows-latest: name: opentelemetry-protojson-gen-oldest 3.12 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.12 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.12" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py312-test-opentelemetry-protojson-gen-oldest -- -ra py312-test-opentelemetry-protojson-gen-latest_windows-latest: name: opentelemetry-protojson-gen-latest 3.12 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.12 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.12" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py312-test-opentelemetry-protojson-gen-latest -- -ra py313-test-opentelemetry-protojson-gen-oldest_windows-latest: name: opentelemetry-protojson-gen-oldest 3.13 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.13 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.13" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py313-test-opentelemetry-protojson-gen-oldest -- -ra py313-test-opentelemetry-protojson-gen-latest_windows-latest: name: opentelemetry-protojson-gen-latest 3.13 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.13 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.13" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py313-test-opentelemetry-protojson-gen-latest -- -ra py314-test-opentelemetry-protojson-gen-oldest_windows-latest: name: opentelemetry-protojson-gen-oldest 3.14 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.14 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py314-test-opentelemetry-protojson-gen-oldest -- -ra py314-test-opentelemetry-protojson-gen-latest_windows-latest: name: opentelemetry-protojson-gen-latest 3.14 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.14 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py314-test-opentelemetry-protojson-gen-latest -- -ra py314t-test-opentelemetry-protojson-gen-oldest_windows-latest: name: opentelemetry-protojson-gen-oldest 3.14t Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.14t uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14t" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py314t-test-opentelemetry-protojson-gen-oldest -- -ra py314t-test-opentelemetry-protojson-gen-latest_windows-latest: name: opentelemetry-protojson-gen-latest 3.14t Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.14t uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14t" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py314t-test-opentelemetry-protojson-gen-latest -- -ra pypy3-test-opentelemetry-protojson-gen-oldest_windows-latest: name: opentelemetry-protojson-gen-oldest pypy-3.10 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python pypy-3.10 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "pypy-3.10" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e pypy3-test-opentelemetry-protojson-gen-oldest -- -ra pypy3-test-opentelemetry-protojson-gen-latest_windows-latest: name: opentelemetry-protojson-gen-latest pypy-3.10 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python pypy-3.10 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "pypy-3.10" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e pypy3-test-opentelemetry-protojson-gen-latest -- -ra py310-test-opentelemetry-codegen-json_windows-latest: name: opentelemetry-codegen-json 3.10 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.10 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.10" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py310-test-opentelemetry-codegen-json -- -ra py311-test-opentelemetry-codegen-json_windows-latest: name: opentelemetry-codegen-json 3.11 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.11 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.11" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py311-test-opentelemetry-codegen-json -- -ra py312-test-opentelemetry-codegen-json_windows-latest: name: opentelemetry-codegen-json 3.12 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.12 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.12" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py312-test-opentelemetry-codegen-json -- -ra py313-test-opentelemetry-codegen-json_windows-latest: name: opentelemetry-codegen-json 3.13 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.13 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.13" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py313-test-opentelemetry-codegen-json -- -ra py314-test-opentelemetry-codegen-json_windows-latest: name: opentelemetry-codegen-json 3.14 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.14 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py314-test-opentelemetry-codegen-json -- -ra py310-test-opentelemetry-sdk_windows-latest: name: opentelemetry-sdk 3.10 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.10 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.10" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py310-test-opentelemetry-sdk -- -ra py311-test-opentelemetry-sdk_windows-latest: name: opentelemetry-sdk 3.11 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.11 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.11" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py311-test-opentelemetry-sdk -- -ra py312-test-opentelemetry-sdk_windows-latest: name: opentelemetry-sdk 3.12 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.12 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.12" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py312-test-opentelemetry-sdk -- -ra py313-test-opentelemetry-sdk_windows-latest: name: opentelemetry-sdk 3.13 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.13 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.13" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py313-test-opentelemetry-sdk -- -ra py314-test-opentelemetry-sdk_windows-latest: name: opentelemetry-sdk 3.14 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.14 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py314-test-opentelemetry-sdk -- -ra py314t-test-opentelemetry-sdk_windows-latest: name: opentelemetry-sdk 3.14t Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.14t uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14t" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py314t-test-opentelemetry-sdk -- -ra pypy3-test-opentelemetry-sdk_windows-latest: name: opentelemetry-sdk pypy-3.10 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python pypy-3.10 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "pypy-3.10" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e pypy3-test-opentelemetry-sdk -- -ra py310-test-opentelemetry-configuration_windows-latest: name: opentelemetry-configuration 3.10 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.10 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.10" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py310-test-opentelemetry-configuration -- -ra py311-test-opentelemetry-configuration_windows-latest: name: opentelemetry-configuration 3.11 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.11 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.11" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py311-test-opentelemetry-configuration -- -ra py312-test-opentelemetry-configuration_windows-latest: name: opentelemetry-configuration 3.12 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.12 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.12" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py312-test-opentelemetry-configuration -- -ra py313-test-opentelemetry-configuration_windows-latest: name: opentelemetry-configuration 3.13 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.13 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.13" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py313-test-opentelemetry-configuration -- -ra py314-test-opentelemetry-configuration_windows-latest: name: opentelemetry-configuration 3.14 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.14 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py314-test-opentelemetry-configuration -- -ra py314t-test-opentelemetry-configuration_windows-latest: name: opentelemetry-configuration 3.14t Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.14t uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14t" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py314t-test-opentelemetry-configuration -- -ra pypy3-test-opentelemetry-configuration_windows-latest: name: opentelemetry-configuration pypy-3.10 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python pypy-3.10 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "pypy-3.10" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e pypy3-test-opentelemetry-configuration -- -ra py310-test-opentelemetry-semantic-conventions_windows-latest: name: opentelemetry-semantic-conventions 3.10 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.10 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.10" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py310-test-opentelemetry-semantic-conventions -- -ra py311-test-opentelemetry-semantic-conventions_windows-latest: name: opentelemetry-semantic-conventions 3.11 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.11 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.11" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py311-test-opentelemetry-semantic-conventions -- -ra py312-test-opentelemetry-semantic-conventions_windows-latest: name: opentelemetry-semantic-conventions 3.12 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.12 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.12" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py312-test-opentelemetry-semantic-conventions -- -ra py313-test-opentelemetry-semantic-conventions_windows-latest: name: opentelemetry-semantic-conventions 3.13 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.13 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.13" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py313-test-opentelemetry-semantic-conventions -- -ra py314-test-opentelemetry-semantic-conventions_windows-latest: name: opentelemetry-semantic-conventions 3.14 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.14 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py314-test-opentelemetry-semantic-conventions -- -ra py314t-test-opentelemetry-semantic-conventions_windows-latest: name: opentelemetry-semantic-conventions 3.14t Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.14t uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14t" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py314t-test-opentelemetry-semantic-conventions -- -ra pypy3-test-opentelemetry-semantic-conventions_windows-latest: name: opentelemetry-semantic-conventions pypy-3.10 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python pypy-3.10 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "pypy-3.10" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e pypy3-test-opentelemetry-semantic-conventions -- -ra py310-test-opentelemetry-getting-started_windows-latest: name: opentelemetry-getting-started 3.10 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Checkout contrib repo @ ${{ env.CONTRIB_REPO_SHA }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false repository: open-telemetry/opentelemetry-python-contrib ref: ${{ env.CONTRIB_REPO_SHA }} path: opentelemetry-python-contrib - name: Set up Python 3.10 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.10" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py310-test-opentelemetry-getting-started -- -ra py311-test-opentelemetry-getting-started_windows-latest: name: opentelemetry-getting-started 3.11 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Checkout contrib repo @ ${{ env.CONTRIB_REPO_SHA }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false repository: open-telemetry/opentelemetry-python-contrib ref: ${{ env.CONTRIB_REPO_SHA }} path: opentelemetry-python-contrib - name: Set up Python 3.11 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.11" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py311-test-opentelemetry-getting-started -- -ra py312-test-opentelemetry-getting-started_windows-latest: name: opentelemetry-getting-started 3.12 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Checkout contrib repo @ ${{ env.CONTRIB_REPO_SHA }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false repository: open-telemetry/opentelemetry-python-contrib ref: ${{ env.CONTRIB_REPO_SHA }} path: opentelemetry-python-contrib - name: Set up Python 3.12 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.12" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py312-test-opentelemetry-getting-started -- -ra py313-test-opentelemetry-getting-started_windows-latest: name: opentelemetry-getting-started 3.13 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Checkout contrib repo @ ${{ env.CONTRIB_REPO_SHA }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false repository: open-telemetry/opentelemetry-python-contrib ref: ${{ env.CONTRIB_REPO_SHA }} path: opentelemetry-python-contrib - name: Set up Python 3.13 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.13" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py313-test-opentelemetry-getting-started -- -ra py314-test-opentelemetry-getting-started_windows-latest: name: opentelemetry-getting-started 3.14 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Checkout contrib repo @ ${{ env.CONTRIB_REPO_SHA }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false repository: open-telemetry/opentelemetry-python-contrib ref: ${{ env.CONTRIB_REPO_SHA }} path: opentelemetry-python-contrib - name: Set up Python 3.14 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py314-test-opentelemetry-getting-started -- -ra py310-test-opentelemetry-opentracing-shim_windows-latest: name: opentelemetry-opentracing-shim 3.10 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.10 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.10" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py310-test-opentelemetry-opentracing-shim -- -ra py311-test-opentelemetry-opentracing-shim_windows-latest: name: opentelemetry-opentracing-shim 3.11 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.11 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.11" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py311-test-opentelemetry-opentracing-shim -- -ra py312-test-opentelemetry-opentracing-shim_windows-latest: name: opentelemetry-opentracing-shim 3.12 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.12 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.12" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py312-test-opentelemetry-opentracing-shim -- -ra py313-test-opentelemetry-opentracing-shim_windows-latest: name: opentelemetry-opentracing-shim 3.13 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.13 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.13" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py313-test-opentelemetry-opentracing-shim -- -ra py314-test-opentelemetry-opentracing-shim_windows-latest: name: opentelemetry-opentracing-shim 3.14 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.14 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py314-test-opentelemetry-opentracing-shim -- -ra py314t-test-opentelemetry-opentracing-shim_windows-latest: name: opentelemetry-opentracing-shim 3.14t Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.14t uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14t" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py314t-test-opentelemetry-opentracing-shim -- -ra pypy3-test-opentelemetry-opentracing-shim_windows-latest: name: opentelemetry-opentracing-shim pypy-3.10 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python pypy-3.10 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "pypy-3.10" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e pypy3-test-opentelemetry-opentracing-shim -- -ra py310-test-opentelemetry-opencensus-shim_windows-latest: name: opentelemetry-opencensus-shim 3.10 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.10 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.10" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py310-test-opentelemetry-opencensus-shim -- -ra py311-test-opentelemetry-opencensus-shim_windows-latest: name: opentelemetry-opencensus-shim 3.11 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.11 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.11" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py311-test-opentelemetry-opencensus-shim -- -ra py312-test-opentelemetry-opencensus-shim_windows-latest: name: opentelemetry-opencensus-shim 3.12 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.12 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.12" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py312-test-opentelemetry-opencensus-shim -- -ra py313-test-opentelemetry-opencensus-shim_windows-latest: name: opentelemetry-opencensus-shim 3.13 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.13 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.13" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py313-test-opentelemetry-opencensus-shim -- -ra py314-test-opentelemetry-opencensus-shim_windows-latest: name: opentelemetry-opencensus-shim 3.14 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.14 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py314-test-opentelemetry-opencensus-shim -- -ra py310-test-opentelemetry-exporter-http-transport-oldest_windows-latest: name: opentelemetry-exporter-http-transport-oldest 3.10 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.10 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.10" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py310-test-opentelemetry-exporter-http-transport-oldest -- -ra py310-test-opentelemetry-exporter-http-transport-latest_windows-latest: name: opentelemetry-exporter-http-transport-latest 3.10 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.10 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.10" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py310-test-opentelemetry-exporter-http-transport-latest -- -ra py311-test-opentelemetry-exporter-http-transport-oldest_windows-latest: name: opentelemetry-exporter-http-transport-oldest 3.11 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.11 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.11" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py311-test-opentelemetry-exporter-http-transport-oldest -- -ra py311-test-opentelemetry-exporter-http-transport-latest_windows-latest: name: opentelemetry-exporter-http-transport-latest 3.11 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.11 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.11" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py311-test-opentelemetry-exporter-http-transport-latest -- -ra py312-test-opentelemetry-exporter-http-transport-oldest_windows-latest: name: opentelemetry-exporter-http-transport-oldest 3.12 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.12 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.12" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py312-test-opentelemetry-exporter-http-transport-oldest -- -ra py312-test-opentelemetry-exporter-http-transport-latest_windows-latest: name: opentelemetry-exporter-http-transport-latest 3.12 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.12 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.12" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py312-test-opentelemetry-exporter-http-transport-latest -- -ra py313-test-opentelemetry-exporter-http-transport-oldest_windows-latest: name: opentelemetry-exporter-http-transport-oldest 3.13 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.13 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.13" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py313-test-opentelemetry-exporter-http-transport-oldest -- -ra py313-test-opentelemetry-exporter-http-transport-latest_windows-latest: name: opentelemetry-exporter-http-transport-latest 3.13 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.13 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.13" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py313-test-opentelemetry-exporter-http-transport-latest -- -ra py314-test-opentelemetry-exporter-http-transport-oldest_windows-latest: name: opentelemetry-exporter-http-transport-oldest 3.14 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.14 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py314-test-opentelemetry-exporter-http-transport-oldest -- -ra py314-test-opentelemetry-exporter-http-transport-latest_windows-latest: name: opentelemetry-exporter-http-transport-latest 3.14 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.14 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py314-test-opentelemetry-exporter-http-transport-latest -- -ra py314t-test-opentelemetry-exporter-http-transport-oldest_windows-latest: name: opentelemetry-exporter-http-transport-oldest 3.14t Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.14t uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14t" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py314t-test-opentelemetry-exporter-http-transport-oldest -- -ra py314t-test-opentelemetry-exporter-http-transport-latest_windows-latest: name: opentelemetry-exporter-http-transport-latest 3.14t Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.14t uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14t" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py314t-test-opentelemetry-exporter-http-transport-latest -- -ra py310-test-opentelemetry-exporter-otlpcommon-oldest_windows-latest: name: opentelemetry-exporter-otlpcommon-oldest 3.10 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.10 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.10" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py310-test-opentelemetry-exporter-otlpcommon-oldest -- -ra py310-test-opentelemetry-exporter-otlpcommon-latest_windows-latest: name: opentelemetry-exporter-otlpcommon-latest 3.10 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.10 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.10" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py310-test-opentelemetry-exporter-otlpcommon-latest -- -ra py311-test-opentelemetry-exporter-otlpcommon-oldest_windows-latest: name: opentelemetry-exporter-otlpcommon-oldest 3.11 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.11 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.11" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py311-test-opentelemetry-exporter-otlpcommon-oldest -- -ra py311-test-opentelemetry-exporter-otlpcommon-latest_windows-latest: name: opentelemetry-exporter-otlpcommon-latest 3.11 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.11 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.11" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py311-test-opentelemetry-exporter-otlpcommon-latest -- -ra py312-test-opentelemetry-exporter-otlpcommon-oldest_windows-latest: name: opentelemetry-exporter-otlpcommon-oldest 3.12 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.12 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.12" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py312-test-opentelemetry-exporter-otlpcommon-oldest -- -ra py312-test-opentelemetry-exporter-otlpcommon-latest_windows-latest: name: opentelemetry-exporter-otlpcommon-latest 3.12 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.12 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.12" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py312-test-opentelemetry-exporter-otlpcommon-latest -- -ra py313-test-opentelemetry-exporter-otlpcommon-oldest_windows-latest: name: opentelemetry-exporter-otlpcommon-oldest 3.13 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.13 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.13" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py313-test-opentelemetry-exporter-otlpcommon-oldest -- -ra py313-test-opentelemetry-exporter-otlpcommon-latest_windows-latest: name: opentelemetry-exporter-otlpcommon-latest 3.13 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.13 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.13" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py313-test-opentelemetry-exporter-otlpcommon-latest -- -ra py314-test-opentelemetry-exporter-otlpcommon-oldest_windows-latest: name: opentelemetry-exporter-otlpcommon-oldest 3.14 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.14 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py314-test-opentelemetry-exporter-otlpcommon-oldest -- -ra py314-test-opentelemetry-exporter-otlpcommon-latest_windows-latest: name: opentelemetry-exporter-otlpcommon-latest 3.14 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.14 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py314-test-opentelemetry-exporter-otlpcommon-latest -- -ra py314t-test-opentelemetry-exporter-otlpcommon-oldest_windows-latest: name: opentelemetry-exporter-otlpcommon-oldest 3.14t Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.14t uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14t" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py314t-test-opentelemetry-exporter-otlpcommon-oldest -- -ra py314t-test-opentelemetry-exporter-otlpcommon-latest_windows-latest: name: opentelemetry-exporter-otlpcommon-latest 3.14t Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.14t uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14t" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py314t-test-opentelemetry-exporter-otlpcommon-latest -- -ra py310-test-opentelemetry-exporter-opencensus_windows-latest: name: opentelemetry-exporter-opencensus 3.10 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.10 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.10" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py310-test-opentelemetry-exporter-opencensus -- -ra py311-test-opentelemetry-exporter-opencensus_windows-latest: name: opentelemetry-exporter-opencensus 3.11 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.11 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.11" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py311-test-opentelemetry-exporter-opencensus -- -ra py312-test-opentelemetry-exporter-opencensus_windows-latest: name: opentelemetry-exporter-opencensus 3.12 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.12 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.12" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py312-test-opentelemetry-exporter-opencensus -- -ra py313-test-opentelemetry-exporter-opencensus_windows-latest: name: opentelemetry-exporter-opencensus 3.13 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.13 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.13" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py313-test-opentelemetry-exporter-opencensus -- -ra py314-test-opentelemetry-exporter-opencensus_windows-latest: name: opentelemetry-exporter-opencensus 3.14 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.14 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py314-test-opentelemetry-exporter-opencensus -- -ra py310-test-opentelemetry-exporter-otlp-proto-common_windows-latest: name: opentelemetry-exporter-otlp-proto-common 3.10 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.10 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.10" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py310-test-opentelemetry-exporter-otlp-proto-common -- -ra py311-test-opentelemetry-exporter-otlp-proto-common_windows-latest: name: opentelemetry-exporter-otlp-proto-common 3.11 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.11 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.11" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py311-test-opentelemetry-exporter-otlp-proto-common -- -ra py312-test-opentelemetry-exporter-otlp-proto-common_windows-latest: name: opentelemetry-exporter-otlp-proto-common 3.12 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.12 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.12" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py312-test-opentelemetry-exporter-otlp-proto-common -- -ra py313-test-opentelemetry-exporter-otlp-proto-common_windows-latest: name: opentelemetry-exporter-otlp-proto-common 3.13 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.13 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.13" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py313-test-opentelemetry-exporter-otlp-proto-common -- -ra py314-test-opentelemetry-exporter-otlp-proto-common_windows-latest: name: opentelemetry-exporter-otlp-proto-common 3.14 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.14 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py314-test-opentelemetry-exporter-otlp-proto-common -- -ra py314t-test-opentelemetry-exporter-otlp-proto-common_windows-latest: name: opentelemetry-exporter-otlp-proto-common 3.14t Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.14t uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14t" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py314t-test-opentelemetry-exporter-otlp-proto-common -- -ra pypy3-test-opentelemetry-exporter-otlp-proto-common_windows-latest: name: opentelemetry-exporter-otlp-proto-common pypy-3.10 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python pypy-3.10 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "pypy-3.10" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e pypy3-test-opentelemetry-exporter-otlp-proto-common -- -ra py310-test-opentelemetry-exporter-otlp-json-common_windows-latest: name: opentelemetry-exporter-otlp-json-common 3.10 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.10 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.10" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py310-test-opentelemetry-exporter-otlp-json-common -- -ra py311-test-opentelemetry-exporter-otlp-json-common_windows-latest: name: opentelemetry-exporter-otlp-json-common 3.11 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.11 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.11" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py311-test-opentelemetry-exporter-otlp-json-common -- -ra py312-test-opentelemetry-exporter-otlp-json-common_windows-latest: name: opentelemetry-exporter-otlp-json-common 3.12 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.12 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.12" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py312-test-opentelemetry-exporter-otlp-json-common -- -ra py313-test-opentelemetry-exporter-otlp-json-common_windows-latest: name: opentelemetry-exporter-otlp-json-common 3.13 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.13 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.13" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py313-test-opentelemetry-exporter-otlp-json-common -- -ra py314-test-opentelemetry-exporter-otlp-json-common_windows-latest: name: opentelemetry-exporter-otlp-json-common 3.14 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.14 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py314-test-opentelemetry-exporter-otlp-json-common -- -ra py314t-test-opentelemetry-exporter-otlp-json-common_windows-latest: name: opentelemetry-exporter-otlp-json-common 3.14t Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.14t uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14t" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py314t-test-opentelemetry-exporter-otlp-json-common -- -ra pypy3-test-opentelemetry-exporter-otlp-json-common_windows-latest: name: opentelemetry-exporter-otlp-json-common pypy-3.10 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python pypy-3.10 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "pypy-3.10" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e pypy3-test-opentelemetry-exporter-otlp-json-common -- -ra py310-test-opentelemetry-exporter-otlp-combined_windows-latest: name: opentelemetry-exporter-otlp-combined 3.10 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.10 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.10" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py310-test-opentelemetry-exporter-otlp-combined -- -ra py311-test-opentelemetry-exporter-otlp-combined_windows-latest: name: opentelemetry-exporter-otlp-combined 3.11 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.11 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.11" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py311-test-opentelemetry-exporter-otlp-combined -- -ra py312-test-opentelemetry-exporter-otlp-combined_windows-latest: name: opentelemetry-exporter-otlp-combined 3.12 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.12 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.12" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py312-test-opentelemetry-exporter-otlp-combined -- -ra py313-test-opentelemetry-exporter-otlp-combined_windows-latest: name: opentelemetry-exporter-otlp-combined 3.13 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.13 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.13" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py313-test-opentelemetry-exporter-otlp-combined -- -ra py314-test-opentelemetry-exporter-otlp-combined_windows-latest: name: opentelemetry-exporter-otlp-combined 3.14 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.14 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py314-test-opentelemetry-exporter-otlp-combined -- -ra py310-test-opentelemetry-exporter-otlp-json-file_windows-latest: name: opentelemetry-exporter-otlp-json-file 3.10 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.10 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.10" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py310-test-opentelemetry-exporter-otlp-json-file -- -ra py311-test-opentelemetry-exporter-otlp-json-file_windows-latest: name: opentelemetry-exporter-otlp-json-file 3.11 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.11 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.11" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py311-test-opentelemetry-exporter-otlp-json-file -- -ra py312-test-opentelemetry-exporter-otlp-json-file_windows-latest: name: opentelemetry-exporter-otlp-json-file 3.12 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.12 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.12" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py312-test-opentelemetry-exporter-otlp-json-file -- -ra py313-test-opentelemetry-exporter-otlp-json-file_windows-latest: name: opentelemetry-exporter-otlp-json-file 3.13 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.13 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.13" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py313-test-opentelemetry-exporter-otlp-json-file -- -ra py314-test-opentelemetry-exporter-otlp-json-file_windows-latest: name: opentelemetry-exporter-otlp-json-file 3.14 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.14 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py314-test-opentelemetry-exporter-otlp-json-file -- -ra py314t-test-opentelemetry-exporter-otlp-json-file_windows-latest: name: opentelemetry-exporter-otlp-json-file 3.14t Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.14t uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14t" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py314t-test-opentelemetry-exporter-otlp-json-file -- -ra pypy3-test-opentelemetry-exporter-otlp-json-file_windows-latest: name: opentelemetry-exporter-otlp-json-file pypy-3.10 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python pypy-3.10 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "pypy-3.10" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e pypy3-test-opentelemetry-exporter-otlp-json-file -- -ra py310-test-opentelemetry-exporter-otlp-json-http_windows-latest: name: opentelemetry-exporter-otlp-json-http 3.10 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.10 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.10" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py310-test-opentelemetry-exporter-otlp-json-http -- -ra py311-test-opentelemetry-exporter-otlp-json-http_windows-latest: name: opentelemetry-exporter-otlp-json-http 3.11 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.11 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.11" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py311-test-opentelemetry-exporter-otlp-json-http -- -ra py312-test-opentelemetry-exporter-otlp-json-http_windows-latest: name: opentelemetry-exporter-otlp-json-http 3.12 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.12 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.12" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py312-test-opentelemetry-exporter-otlp-json-http -- -ra py313-test-opentelemetry-exporter-otlp-json-http_windows-latest: name: opentelemetry-exporter-otlp-json-http 3.13 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.13 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.13" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py313-test-opentelemetry-exporter-otlp-json-http -- -ra py314-test-opentelemetry-exporter-otlp-json-http_windows-latest: name: opentelemetry-exporter-otlp-json-http 3.14 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.14 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py314-test-opentelemetry-exporter-otlp-json-http -- -ra py314t-test-opentelemetry-exporter-otlp-json-http_windows-latest: name: opentelemetry-exporter-otlp-json-http 3.14t Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.14t uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14t" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py314t-test-opentelemetry-exporter-otlp-json-http -- -ra pypy3-test-opentelemetry-exporter-otlp-json-http_windows-latest: name: opentelemetry-exporter-otlp-json-http pypy-3.10 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python pypy-3.10 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "pypy-3.10" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e pypy3-test-opentelemetry-exporter-otlp-json-http -- -ra py310-test-opentelemetry-exporter-otlp-proto-grpc-oldest_windows-latest: name: opentelemetry-exporter-otlp-proto-grpc-oldest 3.10 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.10 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.10" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py310-test-opentelemetry-exporter-otlp-proto-grpc-oldest -- -ra py310-test-opentelemetry-exporter-otlp-proto-grpc-latest_windows-latest: name: opentelemetry-exporter-otlp-proto-grpc-latest 3.10 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.10 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.10" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py310-test-opentelemetry-exporter-otlp-proto-grpc-latest -- -ra py311-test-opentelemetry-exporter-otlp-proto-grpc-oldest_windows-latest: name: opentelemetry-exporter-otlp-proto-grpc-oldest 3.11 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.11 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.11" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py311-test-opentelemetry-exporter-otlp-proto-grpc-oldest -- -ra py311-test-opentelemetry-exporter-otlp-proto-grpc-latest_windows-latest: name: opentelemetry-exporter-otlp-proto-grpc-latest 3.11 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.11 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.11" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py311-test-opentelemetry-exporter-otlp-proto-grpc-latest -- -ra py312-test-opentelemetry-exporter-otlp-proto-grpc-oldest_windows-latest: name: opentelemetry-exporter-otlp-proto-grpc-oldest 3.12 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.12 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.12" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py312-test-opentelemetry-exporter-otlp-proto-grpc-oldest -- -ra py312-test-opentelemetry-exporter-otlp-proto-grpc-latest_windows-latest: name: opentelemetry-exporter-otlp-proto-grpc-latest 3.12 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.12 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.12" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py312-test-opentelemetry-exporter-otlp-proto-grpc-latest -- -ra py313-test-opentelemetry-exporter-otlp-proto-grpc-oldest_windows-latest: name: opentelemetry-exporter-otlp-proto-grpc-oldest 3.13 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.13 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.13" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py313-test-opentelemetry-exporter-otlp-proto-grpc-oldest -- -ra py313-test-opentelemetry-exporter-otlp-proto-grpc-latest_windows-latest: name: opentelemetry-exporter-otlp-proto-grpc-latest 3.13 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.13 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.13" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py313-test-opentelemetry-exporter-otlp-proto-grpc-latest -- -ra py314-test-opentelemetry-exporter-otlp-proto-grpc-oldest_windows-latest: name: opentelemetry-exporter-otlp-proto-grpc-oldest 3.14 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.14 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py314-test-opentelemetry-exporter-otlp-proto-grpc-oldest -- -ra py314-test-opentelemetry-exporter-otlp-proto-grpc-latest_windows-latest: name: opentelemetry-exporter-otlp-proto-grpc-latest 3.14 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.14 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py314-test-opentelemetry-exporter-otlp-proto-grpc-latest -- -ra py310-test-opentelemetry-exporter-otlp-proto-http_windows-latest: name: opentelemetry-exporter-otlp-proto-http 3.10 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.10 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.10" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py310-test-opentelemetry-exporter-otlp-proto-http -- -ra py311-test-opentelemetry-exporter-otlp-proto-http_windows-latest: name: opentelemetry-exporter-otlp-proto-http 3.11 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.11 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.11" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py311-test-opentelemetry-exporter-otlp-proto-http -- -ra py312-test-opentelemetry-exporter-otlp-proto-http_windows-latest: name: opentelemetry-exporter-otlp-proto-http 3.12 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.12 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.12" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py312-test-opentelemetry-exporter-otlp-proto-http -- -ra py313-test-opentelemetry-exporter-otlp-proto-http_windows-latest: name: opentelemetry-exporter-otlp-proto-http 3.13 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.13 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.13" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py313-test-opentelemetry-exporter-otlp-proto-http -- -ra py314-test-opentelemetry-exporter-otlp-proto-http_windows-latest: name: opentelemetry-exporter-otlp-proto-http 3.14 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.14 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py314-test-opentelemetry-exporter-otlp-proto-http -- -ra py314t-test-opentelemetry-exporter-otlp-proto-http_windows-latest: name: opentelemetry-exporter-otlp-proto-http 3.14t Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.14t uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14t" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py314t-test-opentelemetry-exporter-otlp-proto-http -- -ra pypy3-test-opentelemetry-exporter-otlp-proto-http_windows-latest: name: opentelemetry-exporter-otlp-proto-http pypy-3.10 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python pypy-3.10 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "pypy-3.10" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e pypy3-test-opentelemetry-exporter-otlp-proto-http -- -ra py310-test-opentelemetry-exporter-prometheus_windows-latest: name: opentelemetry-exporter-prometheus 3.10 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.10 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.10" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py310-test-opentelemetry-exporter-prometheus -- -ra py311-test-opentelemetry-exporter-prometheus_windows-latest: name: opentelemetry-exporter-prometheus 3.11 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.11 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.11" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py311-test-opentelemetry-exporter-prometheus -- -ra py312-test-opentelemetry-exporter-prometheus_windows-latest: name: opentelemetry-exporter-prometheus 3.12 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.12 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.12" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py312-test-opentelemetry-exporter-prometheus -- -ra py313-test-opentelemetry-exporter-prometheus_windows-latest: name: opentelemetry-exporter-prometheus 3.13 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.13 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.13" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py313-test-opentelemetry-exporter-prometheus -- -ra py314-test-opentelemetry-exporter-prometheus_windows-latest: name: opentelemetry-exporter-prometheus 3.14 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.14 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py314-test-opentelemetry-exporter-prometheus -- -ra py314t-test-opentelemetry-exporter-prometheus_windows-latest: name: opentelemetry-exporter-prometheus 3.14t Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.14t uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14t" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py314t-test-opentelemetry-exporter-prometheus -- -ra pypy3-test-opentelemetry-exporter-prometheus_windows-latest: name: opentelemetry-exporter-prometheus pypy-3.10 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python pypy-3.10 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "pypy-3.10" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e pypy3-test-opentelemetry-exporter-prometheus -- -ra py310-test-opentelemetry-exporter-zipkin-combined_windows-latest: name: opentelemetry-exporter-zipkin-combined 3.10 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.10 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.10" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py310-test-opentelemetry-exporter-zipkin-combined -- -ra py311-test-opentelemetry-exporter-zipkin-combined_windows-latest: name: opentelemetry-exporter-zipkin-combined 3.11 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.11 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.11" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py311-test-opentelemetry-exporter-zipkin-combined -- -ra py312-test-opentelemetry-exporter-zipkin-combined_windows-latest: name: opentelemetry-exporter-zipkin-combined 3.12 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.12 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.12" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py312-test-opentelemetry-exporter-zipkin-combined -- -ra py313-test-opentelemetry-exporter-zipkin-combined_windows-latest: name: opentelemetry-exporter-zipkin-combined 3.13 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.13 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.13" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py313-test-opentelemetry-exporter-zipkin-combined -- -ra py314-test-opentelemetry-exporter-zipkin-combined_windows-latest: name: opentelemetry-exporter-zipkin-combined 3.14 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.14 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py314-test-opentelemetry-exporter-zipkin-combined -- -ra py314t-test-opentelemetry-exporter-zipkin-combined_windows-latest: name: opentelemetry-exporter-zipkin-combined 3.14t Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.14t uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14t" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py314t-test-opentelemetry-exporter-zipkin-combined -- -ra pypy3-test-opentelemetry-exporter-zipkin-combined_windows-latest: name: opentelemetry-exporter-zipkin-combined pypy-3.10 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python pypy-3.10 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "pypy-3.10" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e pypy3-test-opentelemetry-exporter-zipkin-combined -- -ra py310-test-opentelemetry-exporter-zipkin-proto-http_windows-latest: name: opentelemetry-exporter-zipkin-proto-http 3.10 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.10 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.10" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py310-test-opentelemetry-exporter-zipkin-proto-http -- -ra py311-test-opentelemetry-exporter-zipkin-proto-http_windows-latest: name: opentelemetry-exporter-zipkin-proto-http 3.11 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.11 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.11" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py311-test-opentelemetry-exporter-zipkin-proto-http -- -ra py312-test-opentelemetry-exporter-zipkin-proto-http_windows-latest: name: opentelemetry-exporter-zipkin-proto-http 3.12 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.12 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.12" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py312-test-opentelemetry-exporter-zipkin-proto-http -- -ra py313-test-opentelemetry-exporter-zipkin-proto-http_windows-latest: name: opentelemetry-exporter-zipkin-proto-http 3.13 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.13 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.13" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py313-test-opentelemetry-exporter-zipkin-proto-http -- -ra py314-test-opentelemetry-exporter-zipkin-proto-http_windows-latest: name: opentelemetry-exporter-zipkin-proto-http 3.14 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.14 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py314-test-opentelemetry-exporter-zipkin-proto-http -- -ra py314t-test-opentelemetry-exporter-zipkin-proto-http_windows-latest: name: opentelemetry-exporter-zipkin-proto-http 3.14t Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.14t uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14t" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py314t-test-opentelemetry-exporter-zipkin-proto-http -- -ra pypy3-test-opentelemetry-exporter-zipkin-proto-http_windows-latest: name: opentelemetry-exporter-zipkin-proto-http pypy-3.10 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python pypy-3.10 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "pypy-3.10" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e pypy3-test-opentelemetry-exporter-zipkin-proto-http -- -ra py310-test-opentelemetry-exporter-zipkin-json_windows-latest: name: opentelemetry-exporter-zipkin-json 3.10 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.10 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.10" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py310-test-opentelemetry-exporter-zipkin-json -- -ra py311-test-opentelemetry-exporter-zipkin-json_windows-latest: name: opentelemetry-exporter-zipkin-json 3.11 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.11 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.11" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py311-test-opentelemetry-exporter-zipkin-json -- -ra py312-test-opentelemetry-exporter-zipkin-json_windows-latest: name: opentelemetry-exporter-zipkin-json 3.12 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.12 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.12" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py312-test-opentelemetry-exporter-zipkin-json -- -ra py313-test-opentelemetry-exporter-zipkin-json_windows-latest: name: opentelemetry-exporter-zipkin-json 3.13 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.13 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.13" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py313-test-opentelemetry-exporter-zipkin-json -- -ra py314-test-opentelemetry-exporter-zipkin-json_windows-latest: name: opentelemetry-exporter-zipkin-json 3.14 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.14 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py314-test-opentelemetry-exporter-zipkin-json -- -ra py314t-test-opentelemetry-exporter-zipkin-json_windows-latest: name: opentelemetry-exporter-zipkin-json 3.14t Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.14t uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14t" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py314t-test-opentelemetry-exporter-zipkin-json -- -ra pypy3-test-opentelemetry-exporter-zipkin-json_windows-latest: name: opentelemetry-exporter-zipkin-json pypy-3.10 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python pypy-3.10 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "pypy-3.10" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e pypy3-test-opentelemetry-exporter-zipkin-json -- -ra py310-test-opentelemetry-propagator-b3_windows-latest: name: opentelemetry-propagator-b3 3.10 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.10 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.10" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py310-test-opentelemetry-propagator-b3 -- -ra py311-test-opentelemetry-propagator-b3_windows-latest: name: opentelemetry-propagator-b3 3.11 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.11 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.11" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py311-test-opentelemetry-propagator-b3 -- -ra py312-test-opentelemetry-propagator-b3_windows-latest: name: opentelemetry-propagator-b3 3.12 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.12 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.12" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py312-test-opentelemetry-propagator-b3 -- -ra py313-test-opentelemetry-propagator-b3_windows-latest: name: opentelemetry-propagator-b3 3.13 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.13 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.13" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py313-test-opentelemetry-propagator-b3 -- -ra py314-test-opentelemetry-propagator-b3_windows-latest: name: opentelemetry-propagator-b3 3.14 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.14 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py314-test-opentelemetry-propagator-b3 -- -ra py314t-test-opentelemetry-propagator-b3_windows-latest: name: opentelemetry-propagator-b3 3.14t Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.14t uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14t" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py314t-test-opentelemetry-propagator-b3 -- -ra pypy3-test-opentelemetry-propagator-b3_windows-latest: name: opentelemetry-propagator-b3 pypy-3.10 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python pypy-3.10 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "pypy-3.10" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e pypy3-test-opentelemetry-propagator-b3 -- -ra py310-test-opentelemetry-propagator-jaeger_windows-latest: name: opentelemetry-propagator-jaeger 3.10 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.10 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.10" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py310-test-opentelemetry-propagator-jaeger -- -ra py311-test-opentelemetry-propagator-jaeger_windows-latest: name: opentelemetry-propagator-jaeger 3.11 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.11 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.11" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py311-test-opentelemetry-propagator-jaeger -- -ra py312-test-opentelemetry-propagator-jaeger_windows-latest: name: opentelemetry-propagator-jaeger 3.12 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.12 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.12" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py312-test-opentelemetry-propagator-jaeger -- -ra py313-test-opentelemetry-propagator-jaeger_windows-latest: name: opentelemetry-propagator-jaeger 3.13 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.13 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.13" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py313-test-opentelemetry-propagator-jaeger -- -ra py314-test-opentelemetry-propagator-jaeger_windows-latest: name: opentelemetry-propagator-jaeger 3.14 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.14 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py314-test-opentelemetry-propagator-jaeger -- -ra py314t-test-opentelemetry-propagator-jaeger_windows-latest: name: opentelemetry-propagator-jaeger 3.14t Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.14t uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14t" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py314t-test-opentelemetry-propagator-jaeger -- -ra pypy3-test-opentelemetry-propagator-jaeger_windows-latest: name: opentelemetry-propagator-jaeger pypy-3.10 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python pypy-3.10 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "pypy-3.10" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e pypy3-test-opentelemetry-propagator-jaeger -- -ra py310-test-opentelemetry-test-utils_windows-latest: name: opentelemetry-test-utils 3.10 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.10 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.10" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py310-test-opentelemetry-test-utils -- -ra py311-test-opentelemetry-test-utils_windows-latest: name: opentelemetry-test-utils 3.11 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.11 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.11" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py311-test-opentelemetry-test-utils -- -ra py312-test-opentelemetry-test-utils_windows-latest: name: opentelemetry-test-utils 3.12 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.12 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.12" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py312-test-opentelemetry-test-utils -- -ra py313-test-opentelemetry-test-utils_windows-latest: name: opentelemetry-test-utils 3.13 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.13 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.13" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py313-test-opentelemetry-test-utils -- -ra py314-test-opentelemetry-test-utils_windows-latest: name: opentelemetry-test-utils 3.14 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python 3.14 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e py314-test-opentelemetry-test-utils -- -ra pypy3-test-opentelemetry-test-utils_windows-latest: name: opentelemetry-test-utils pypy-3.10 Windows runs-on: windows-latest timeout-minutes: 30 steps: - name: Configure git to support long filenames run: git config --system core.longpaths true - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Python pypy-3.10 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "pypy-3.10" - name: Set up uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Run tests run: uvx --with tox-uv tox -e pypy3-test-opentelemetry-test-utils -- -ra python-opentelemetry-1.45.2/.gitignore000066400000000000000000000012101527070070700200050ustar00rootroot00000000000000*.py[cod] *.sw[op] # C extensions *.so # Packages *.egg *.egg-info dist build eggs parts bin include var sdist develop-eggs .installed.cfg pyvenv.cfg lib share/ lib64 __pycache__ venv*/ .venv*/ # Installer logs pip-log.txt # Unit test / coverage reports coverage.xml .coverage .nox .tox .cache htmlcov .agent/ # Translations *.mo # Mac .DS_Store # Mr Developer .mr.developer.cfg .project .pydevproject # JetBrains .idea # VSCode .vscode # Sphinx _build/ # mypy .mypy_cache/ target # Django example docs/examples/django/db.sqlite3 # Semantic conventions scripts/semconv/semantic-conventions # Benchmark result files *-benchmark.json python-opentelemetry-1.45.2/.pre-commit-config.yaml000066400000000000000000000015611527070070700223070ustar00rootroot00000000000000repos: - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. rev: v0.16.0 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: 0.12.2 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]'] args: ["--report-level", "warning"] python-opentelemetry-1.45.2/.pylintrc000066400000000000000000000364301527070070700176760ustar00rootroot00000000000000[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= # Add list of files or directories to be excluded. They should be base names, not # paths. ignore=CVS,gen,proto,proto_json # Add files or directories matching the regex patterns to be excluded. The # regex matches against base names, not paths. ignore-patterns=^models\.py$ # 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= # When enabled, pylint would attempt to guess common misconfiguration and emit # user-friendly hints instead of false-positive error messages. suggestion-mode=yes # 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, too-many-positional-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 redefined-builtin, cyclic-import, # 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, _agnosticcontextmanager # 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=zipkin_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=120 # 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 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. overgeneral-exceptions=builtins.Exception python-opentelemetry-1.45.2/.readthedocs.yml000066400000000000000000000004301527070070700211060ustar00rootroot00000000000000# Read the Docs configuration file # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details version: 2 build: os: "ubuntu-22.04" tools: python: "3.10" sphinx: configuration: docs/conf.py python: install: - requirements: docs-requirements.txt python-opentelemetry-1.45.2/.rstcheck.cfg000066400000000000000000000001151527070070700203650ustar00rootroot00000000000000[rstcheck] ignore_directives = automodule ignore_roles = scm_web,scm_raw_web python-opentelemetry-1.45.2/AGENTS.md000066400000000000000000000055331527070070700173340ustar00rootroot00000000000000# OpenTelemetry Python 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 the core OpenTelemetry Python API, SDK, and related packages. ## General Rules and Guidelines The OpenTelemetry community has broader guidance on GenAI contributions at https://github.com/open-telemetry/community/blob/main/policies/genai.md — please read it before contributing. 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. 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 threads on their behalf as it is against the rules of this project. ## Structure - `opentelemetry-api/` - the OpenTelemetry API package - `opentelemetry-sdk/` - the OpenTelemetry SDK package - `opentelemetry-semantic-conventions/` - semantic conventions - `opentelemetry-proto/` / `opentelemetry-proto-json/` - protobuf definitions and generated code - `exporter/` - exporters (OTLP, Prometheus, Zipkin, etc.) - `propagator/` - context propagators (B3, Jaeger) - `shim/` - compatibility shims (OpenTracing, OpenCensus) Each package lives under its own directory with a `pyproject.toml` and `tests/`. ## Commands ```sh # Install all packages and dev tools uv sync --frozen --all-packages # Lint (runs ruff via pre-commit) uv run tox -e precommit # Test a specific package uv run tox -e py312-test-opentelemetry-sdk # Lint (pylint) a specific package uv run tox -e lint-opentelemetry-sdk # 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. - Whenever applicable, all code changes should have tests that actually validate the changes. ## 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 ``` python-opentelemetry-1.45.2/CHANGELOG.md000066400000000000000000004723371527070070700176540ustar00rootroot00000000000000# 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). **Breaking changes ongoing** > [!IMPORTANT] > We are working on stabilizing the Log signal that would require making deprecations and breaking changes. We will try to reduce the releases that may require an update to your code, especially for instrumentations or for sdk developers. ## Version 1.45.2/0.66b2 (2026-10-29) ### Added - `opentelemetry-sdk`, `opentelemetry-configuration`, `opentelemetry-exporter-prometheus`: enforce metric cardinality limits (default 2000) with the `otel.metric.overflow` attribute, configurable through `View(aggregation_cardinality_limit=...)`, reader `cardinality_limits` and declarative configuration ([#5809](https://github.com/open-telemetry/opentelemetry-python/pull/5809)) ### Fixed - `opentelemetry-api`: limit `traceparent` header length to 512 characters in `TraceContextTextMapPropagator` during extraction ([#5818](https://github.com/open-telemetry/opentelemetry-python/pull/5818)) ## Version 1.45.1/0.66b1 (2026-10-06) ### Fixed - `opentelemetry-api`: make `AnyValue` and `AttributeValue` explicit type aliases ([#5703](https://github.com/open-telemetry/opentelemetry-python/pull/5703)) - `opentelemetry-sdk`: fix thread leak bug in resource initialization logic for long running resource detectors ([#5706](https://github.com/open-telemetry/opentelemetry-python/pull/5706)) - `opentelemetry-exporter-otlp-proto-http`: restore `requests` as the default HTTP transport so proxy environment variables such as `HTTPS_PROXY` are honored again ([#5714](https://github.com/open-telemetry/opentelemetry-python/pull/5714)) - `opentelemetry-api`: fix quadratic time parsing of the `tracestate` header and discard `tracestate` headers longer than 8192 characters ([#5746](https://github.com/open-telemetry/opentelemetry-python/pull/5746)) ## Version 1.45.0/0.66b0 (2026-09-25) ### Added - `opentelemetry-exporter-prometheus`: add support to configure Resource attributes as metric labels ([#5122](https://github.com/open-telemetry/opentelemetry-python/pull/5122)) - infra: add renovate ([#5202](https://github.com/open-telemetry/opentelemetry-python/pull/5202)) - `opentelemetry-api`, `opentelemetry-sdk`: add support for extended attribute values everywhere. ([#5266](https://github.com/open-telemetry/opentelemetry-python/pull/5266)) - `opentelemetry-sdk`: wire the top-level `log_level` field in declarative configuration — when set, maps the OTel `SeverityNumber` value to a Python logging level and applies it to the `opentelemetry` logger so SDK internal diagnostics respect the configured severity. ([#5351](https://github.com/open-telemetry/opentelemetry-python/pull/5351)) - `opentelemetry-sdk`: add the new stable `AlwaysRecordSampler` ([#5354](https://github.com/open-telemetry/opentelemetry-python/pull/5354)) - `opentelemetry-configuration`, `opentelemetry-sdk`: wire top-level `attribute_limits` into per-signal providers via declarative config; add `log_record_limits` support to `LoggerProvider` ([#5365](https://github.com/open-telemetry/opentelemetry-python/pull/5365)) - `opentelemetry-exporter-otlp-json-http`: add OTLP JSON HTTP exporter package ([#5374](https://github.com/open-telemetry/opentelemetry-python/pull/5374)) - `opentelemetry-api`, `opentelemetry-sdk`: add `enabled()` support to the Logger API, SDK, and `LogRecordProcessor` to let instrumentation skip expensive work when logging is disabled ([#5380](https://github.com/open-telemetry/opentelemetry-python/pull/5380)) - `opentelemetry-exporter-otlp-json-file`: add OTLP JSON file Docker tests ([#5412](https://github.com/open-telemetry/opentelemetry-python/pull/5412)) - `opentelemetry-configuration`: wire the experimental `tracer_configurator/development`, `meter_configurator/development` and `logger_configurator/development` fields into `create_tracer_provider`, `create_meter_provider` and `create_logger_provider`, so per-instrumentation-scope `enabled` overrides declared in the config file are applied to the provider (previously these fields were parsed but silently discarded). The logger `minimum_severity`/`trace_based` fields are not supported by the Python SDK and are ignored with a warning. ([#5418](https://github.com/open-telemetry/opentelemetry-python/pull/5418)) - `docs/examples`: add example on how to manually setup the SDK to get SDK metrics ([#5449](https://github.com/open-telemetry/opentelemetry-python/pull/5449)) - `opentelemetry-docker-tests`: add Prometheus exporter docker tests ([#5457](https://github.com/open-telemetry/opentelemetry-python/pull/5457)) - `opentelemetry-sdk`: count records dropped after shutdown on `otel.sdk.processor.{span,log}.processed` with `error.type=already_shutdown` (batch span/log and simple log processors), which the semantic conventions define as a valid value for this metric. ([#5509](https://github.com/open-telemetry/opentelemetry-python/pull/5509)) - `opentelemetry-semantic-conventions`: update semantic conventions to v1.44.0 ([#5511](https://github.com/open-telemetry/opentelemetry-python/pull/5511)) - `opentelemetry-sdk`: add `host.id` to the host resource detector ([#5653](https://github.com/open-telemetry/opentelemetry-python/pull/5653)) - `opentelemetry-test-utils`: add `CapturingSampler` to record what samplers receive in instrumentation tests ([#5681](https://github.com/open-telemetry/opentelemetry-python/pull/5681)) ### Changed - Enable `PIE` (flake8-pie) ruff rule and fix all violations ([#5150](https://github.com/open-telemetry/opentelemetry-python/pull/5150)) - The public `opentelemetry.util.types.AttributeValue` type in package `opentelemetry-api` is being expanded to include `None`, heterogeneous sequences of primitive types (and nested sequences) as opposed to only homogeneous primitive sequences, and Mappings of strings to any primitive types or sequences/mappings (which themselves must only contain primitive types or sequences/mappings validated the same way). If a `bytes` type is set as an attribute value in the SDK, it will no longer be utf-8 decoded to a string, instead it will be passed along as is in accordance with the OTEL spec, since `bytes` is a valid type in the OTLP proto. ([#5266](https://github.com/open-telemetry/opentelemetry-python/pull/5266)) - `opentelemetry-exporter-otlp-proto-http`: add a `max_request_size` argument to the OTLP HTTP exporters (traces, logs, metrics); serialized requests larger than the limit are dropped before sending, measured before compression. Defaults to 64 MiB (enabled); set to 0 to disable. Mirrors opentelemetry-go#8157. ([#5369](https://github.com/open-telemetry/opentelemetry-python/pull/5369)) - [BREAKING] `opentelemetry-api`: subclasses of `Logger` need to implement the `enabled` method ([#5380](https://github.com/open-telemetry/opentelemetry-python/pull/5380)) - `opentelemetry-exporter-otlp-proto-http`: refactor to use shared opentelemetry-exporter-otlp-common and opentelemetry-exporter-http-transport packages and switch default HTTP backend to urllib3 ([#5389](https://github.com/open-telemetry/opentelemetry-python/pull/5389)) - `opentelemetry-sdk`: unify logging force_flush timeout defaults to 30000ms ([#5438](https://github.com/open-telemetry/opentelemetry-python/pull/5438)) - `opentelemetry-python`: enable Ruff default ruleset and fix auto-fixable lint issues ([#5491](https://github.com/open-telemetry/opentelemetry-python/pull/5491)) - `opentelemetry-sdk`: `SimpleSpanProcessor` now drops spans ended after `shutdown()` instead of passing them to the exporter, and counts them on `otel.sdk.processor.span.processed` with `error.type=already_shutdown`. ([#5512](https://github.com/open-telemetry/opentelemetry-python/pull/5512)) - Bump pytest to 9.0.3 ([#5518](https://github.com/open-telemetry/opentelemetry-python/pull/5518)) - `opentelemetry-exporter-otlp-proto-http`: clarify that the `endpoint=` kwarg requires the full signal path ([#5633](https://github.com/open-telemetry/opentelemetry-python/pull/5633)) - `opentelemetry-sdk`: fix typos in SpanLimits docstring ([#5658](https://github.com/open-telemetry/opentelemetry-python/pull/5658)) ### Fixed - `opentelemetry-configuration`: perform environment variable substitution on scalar values after parsing the configuration file, so `${VAR}` references inside comments and mapping keys are no longer substituted and undefined references in comments no longer abort loading ([#5407](https://github.com/open-telemetry/opentelemetry-python/pull/5407)) - `opentelemetry-configuration`: declarative config environment variable substitution now replaces an unset variable that has no default with an empty value instead of raising an error, per the configuration spec. Resource attributes whose value resolves to null (an unset `${VAR}` with no default) are skipped with a warning instead of being inserted as a null value. ([#5408](https://github.com/open-telemetry/opentelemetry-python/pull/5408)) - 'scripts/build.sh`: add `opentelemetry-configuration` and `opentelemetry-proto-json` to the package to release ([#5425](https://github.com/open-telemetry/opentelemetry-python/pull/5425)) - `opentelemetry-sdk`: fix `View` instrument-name matching so a view configured with an instrument's real (mixed-case) name is applied; matching is now case-insensitive and platform-independent instead of relying on `fnmatch`'s OS-dependent case handling ([#5430](https://github.com/open-telemetry/opentelemetry-python/pull/5430)) - `opentelemetry-sdk`: fix missing f-prefix in exponential histogram error messages ([#5434](https://github.com/open-telemetry/opentelemetry-python/pull/5434)) - `opentelemetry-configuration`: resolve false-positive warning logs for newer schema minor version ([#5436](https://github.com/open-telemetry/opentelemetry-python/pull/5436)) - `opentelemetry-sdk`: make methods on `FixedSizeExemplarReservoirABC` thread safe ([#5437](https://github.com/open-telemetry/opentelemetry-python/pull/5437)) - `opentelemetry-propagator-jaeger`: fix typing issues and enable pyright typechecking for the package `opentelemetry-propagator-jaeger`: skip `uberctx-` baggage headers with an empty value on extraction instead of raising `TypeError` ([#5440](https://github.com/open-telemetry/opentelemetry-python/pull/5440)) - `opentelemetry-sdk`: fix `TypeError` when instantiating a `_BaseConfigurator` subclass whose `__init__` takes arguments ([#5441](https://github.com/open-telemetry/opentelemetry-python/pull/5441)) - `opentelemetry-sdk`: fix `TypeError` in `os.fork()` when a `BatchProcessor` or `PeriodicExportingMetricReader` is garbage collected ([#5453](https://github.com/open-telemetry/opentelemetry-python/pull/5453)) - `opentelemetry-configuration`: a declarative config key present with an empty (null) value on an object-typed node (e.g. `always_on:`, a `- service:` detector, or a metric `console:` exporter) is now treated the same as an explicit empty config (`always_on: {}`) instead of failing type dispatch or silently skipping the node. Both `dict`-typed nodes and dataclasses constructible with no arguments are covered. ([#5454](https://github.com/open-telemetry/opentelemetry-python/pull/5454)) - `opentelemetry-api`: fix copy-pasted log message in `SpanContext.__delattr__` ([#5455](https://github.com/open-telemetry/opentelemetry-python/pull/5455)) - `opentelemetry-sdk`: reject views with `ExponentialBucketHistogramAggregation` for asynchronous instruments instead of silently producing no data ([#5461](https://github.com/open-telemetry/opentelemetry-python/pull/5461)) - `opentelemetry-sdk`: fill every bucket of `SimpleFixedSizeExemplarReservoir` before random sampling ([#5462](https://github.com/open-telemetry/opentelemetry-python/pull/5462)) - `opentelemetry-sdk`: Import code_attributes from stable semconv package ([#5465](https://github.com/open-telemetry/opentelemetry-python/pull/5465)) - `opentelemetry-sdk`: for both the simple and batch span/log processors, count `otel.sdk.processor.{span,log}.processed` when the processor submits records to the exporter instead of after export completes, and stop stamping exporter failures onto this metric as `error.type` ([#5472](https://github.com/open-telemetry/opentelemetry-python/pull/5472)) - `opentelemetry-sdk`: fix misleading instrument name validation error message (name max length is 255, not 63). ([#5513](https://github.com/open-telemetry/opentelemetry-python/pull/5513)) - `opentelemetry-configuration`: add missing process executable name to default service name when available in resource attributes ([#5534](https://github.com/open-telemetry/opentelemetry-python/pull/5534)) - `opentelemetry-sdk`: fix values for `process.executable.name` and `process.executable.path` to match semantic conventions. ([#5535](https://github.com/open-telemetry/opentelemetry-python/pull/5535)) - `opentelemetry-api`: fix `TraceState.update` dropping all entries when adding a new key at the 32-key limit ([#5543](https://github.com/open-telemetry/opentelemetry-python/pull/5543)) - `opentelemetry-sdk`: bound `get_aggregated_resources()` wait to the timeout ([#5545](https://github.com/open-telemetry/opentelemetry-python/pull/5545)) - `opentelemetry-sdk`: don't read `process.executable.name` resource attribute when building default `service.name` ([#5547](https://github.com/open-telemetry/opentelemetry-python/pull/5547)) - `opentelemetry-propagator-jaeger`: enforce baggage limits on both `uberctx-` extract and inject, borrowing the same limits (180 entries, 4096 bytes per entry, 8192 bytes total) the package's core `W3CBaggagePropagator` already uses, so neither an inbound carrier nor an in-process baggage map can produce unbounded work or headers. ([#5556](https://github.com/open-telemetry/opentelemetry-python/pull/5556)) - `opentelemetry-sdk`: keep metric attribute values that Python considers equal but the data model does not, such as `True`, `1` and `1.0`, in separate metric streams ([#5573](https://github.com/open-telemetry/opentelemetry-python/pull/5573)) - `opentelemetry-sdk`: keep `Resource` hashable and serialisable when an attribute value is `bytes`, instead of raising `TypeError` from every OTLP encoder ([#5577](https://github.com/open-telemetry/opentelemetry-python/pull/5577)) - `opentelemetry-sdk`: fix instrumentation scope name matching in the tracer, meter and logger configurators so it is case-sensitive on every platform instead of relying on `fnmatch`'s OS-dependent case handling ([#5584](https://github.com/open-telemetry/opentelemetry-python/pull/5584)) - `opentelemetry-sdk`: fix `TracerProvider()` raising `ValueError` when `OTEL_TRACES_SAMPLER_ARG` is a syntactically valid number outside the `[0.0, 1.0]` range, instead of logging a warning and falling back like other invalid values ([#5594](https://github.com/open-telemetry/opentelemetry-python/pull/5594)) - `opentelemetry-sdk`: retain values from synchronous instruments using last-value aggregation across cumulative collections ([#5637](https://github.com/open-telemetry/opentelemetry-python/pull/5637)) - `opentelemetry-api`: Added guard for negative value on max_value_len ([#5647](https://github.com/open-telemetry/opentelemetry-python/pull/5647)) - `opentelemetry-sdk`: fix overriding of the service.instance.id which has been populated from the user provided values through the resource detectors ([#5660](https://github.com/open-telemetry/opentelemetry-python/pull/5660)) - `opentelemetry-exporter-otlp-proto-grpc`: Fix incorrect default port for OTLP gRPC exporter self-metrics ([#5668](https://github.com/open-telemetry/opentelemetry-python/pull/5668)) - `opentelemetry-api`: update W3CBaggagePropagator to properly handle whitespace ([#5680](https://github.com/open-telemetry/opentelemetry-python/pull/5680)) ## Version 1.44.0/0.65b0 (2026-07-16) ### Added - `opentelemetry-docker-tests`: Refactor Docker tests to properly validate contents of exported telemetry ([#5220](https://github.com/open-telemetry/opentelemetry-python/pull/5220)) - `opentelemetry-exporter-otlp-common`: add shared package for common OTLP utilities ([#5252](https://github.com/open-telemetry/opentelemetry-python/pull/5252)) - `opentelemetry-sdk`: add `MissingDependencyError` exception for declarative configuration and use it for missing optional dependency errors ([#5265](https://github.com/open-telemetry/opentelemetry-python/pull/5265)) - `opentelemetry-sdk`: Add ability to refresh process dependent Resource attributes ([#5280](https://github.com/open-telemetry/opentelemetry-python/pull/5280)) - `opentelemetry-sdk`: add `force_flush` method to `LogRecordExporter` ABC ([#5294](https://github.com/open-telemetry/opentelemetry-python/pull/5294)) - `opentelemetry-sdk`: Make it possible to limit the size of stored spans in InMemorySpanExporter ([#5296](https://github.com/open-telemetry/opentelemetry-python/pull/5296)) - `opentelemetry-sdk`: add log record limits environment variables `OTEL_LOGRECORD_ATTRIBUTE_COUNT_LIMIT` and `OTEL_LOGRECORD_ATTRIBUTE_VALUE_LENGTH_LIMIT` ([#5300](https://github.com/open-telemetry/opentelemetry-python/pull/5300)) - `opentelemetry-sdk`: add support for file exporter with declarative config ([#5311](https://github.com/open-telemetry/opentelemetry-python/pull/5311)) - `opentelemetry-sdk`: expose `SynchronousMultiLogRecordProcessor` and `ConcurrentMultiLogRecordProcessor` publicly ([#5327](https://github.com/open-telemetry/opentelemetry-python/pull/5327)) - `opentelemetry-sdk`: point the declarative configuration package README at the shared [language support status matrix](https://github.com/open-telemetry/opentelemetry-configuration/blob/main/language-support-status.md#python) in the `opentelemetry-configuration` repo, so Python conformance status lives alongside the other languages instead of being duplicated per language SDK. ([#5347](https://github.com/open-telemetry/opentelemetry-python/pull/5347)) - `opentelemetry-sdk`: document that Python-implementation extensions (`OTEL_PYTHON_*` variables) are bypassed when `OTEL_CONFIG_FILE` is set. The env-var initialisation path is skipped entirely in favour of the declarative file; honouring these alongside a config file is tracked as a follow-up. ([#5353](https://github.com/open-telemetry/opentelemetry-python/pull/5353)) - `opentelemetry-sdk`: wire id_generator from declarative configuration to TracerProvider ([#5363](https://github.com/open-telemetry/opentelemetry-python/pull/5363)) - `opentelemetry-sdk`: Add support for activating instrumentors from a declarative configuration file via the `instrumentation/development.python` section. Instrumentors can declare a `configuration` attribute to have their options validated through the same type-coercion pipeline used for SDK component configuration. ([#5372](https://github.com/open-telemetry/opentelemetry-python/pull/5372)) - `opentelemetry-sdk`: add `record_min_max` option to `ExponentialBucketHistogramAggregation`, matching the option already available on `ExplicitBucketHistogramAggregation` and required by the specification ([#5377](https://github.com/open-telemetry/opentelemetry-python/pull/5377)) ### Changed - opentelemetry-sdk: revert BoundedAttributes RLock back to Lock ([#5329](https://github.com/open-telemetry/opentelemetry-python/pull/5329)) - docs: update logs example to use `opentelemetry-instrumentation-logging` ([#5344](https://github.com/open-telemetry/opentelemetry-python/pull/5344)) - `opentelemetry-sdk`: bump declarative configuration schema to v1.1.0 ([#5345](https://github.com/open-telemetry/opentelemetry-python/pull/5345)) - `opentelemetry-configuration`: declarative configuration moves from `opentelemetry.sdk._configuration` into the new public `opentelemetry-configuration` package (`opentelemetry.configuration` namespace), published experimentally. `opentelemetry-sdk[file-configuration]` continues to work as an alias that installs `opentelemetry-configuration` alongside the SDK. ([#5356](https://github.com/open-telemetry/opentelemetry-python/pull/5356)) - `opentelemetry-api`: remove env carrier environment snapshot caching, you now have to pass explicitly the mapping where to get the environment variables from as the `EnvironmentGetter.get` first argument. ([#5366](https://github.com/open-telemetry/opentelemetry-python/pull/5366)) - `opentelemetry-semantic-conventions`: bump to 1.42.0 ([#5410](https://github.com/open-telemetry/opentelemetry-python/pull/5410)) - `opentelemetry-semantic-conventions`: bump to 1.43.0 ([#5413](https://github.com/open-telemetry/opentelemetry-python/pull/5413)) ### Removed - `opentelemetry-api`, `opentelemetry-sdk`: Removed deprecated Events API/SDK. Use `LogRecord` with the `event_name` field set instead. ([#5293](https://github.com/open-telemetry/opentelemetry-python/pull/5293)) ### Fixed - `opentelemetry-sdk`: Optimize `LogRecord` memory in `BatchLogRecordProcessor` by clearing the `Context` object before buffering. This is a potentially breaking change for custom log exporters that access `context` from the exported `ReadableLogRecord`, as the context will now be an empty `Context()` instead of the original. ([#4977](https://github.com/open-telemetry/opentelemetry-python/pull/4977)) - `opentelemetry-sdk`: drop non-finite measurements (NaN and Inf) at the instrument level to prevent permanent aggregation poisoning ([#5336](https://github.com/open-telemetry/opentelemetry-python/pull/5336)) - `opentelemetry-sdk`: raise `ValueError` when `ExplicitBucketHistogramAggregation` boundaries are not strictly increasing or finite ([#5340](https://github.com/open-telemetry/opentelemetry-python/pull/5340)) - `opentelemetry-sdk`: `ProcessResourceDetector` no longer collects or emits `process.command_args` and `process.command_line` by default since the values are not sanitized and may contain sensitive information. Users who depend on these resource attributes must pass `include_command_args=True`. ([#5364](https://github.com/open-telemetry/opentelemetry-python/pull/5364)) - `opentelemetry-exporter-otlp-proto-http`: fix the OTLP HTTP metric exporter self-observability metrics over-counting data points when `max_export_batch_size` splits a batch; each split now reports its own data-point count instead of the whole-batch count. ([#5370](https://github.com/open-telemetry/opentelemetry-python/pull/5370)) - `opentelemetry-api`: Prevent in-place mutation of `Context` via inherited `dict` methods ([#5399](https://github.com/open-telemetry/opentelemetry-python/pull/5399)) ## Version 1.43.0/0.64b0 (2026-06-24) ### Added - `opentelemetry-sdk`: add `add_metric_reader` / `remove_metric_reader` public APIs to register / unregister metric readers at runtime. ([#4863](https://github.com/open-telemetry/opentelemetry-python/pull/4863)) - `opentelemetry-exporter-prometheus`: add support for configuring metric scope labels ([#5123](https://github.com/open-telemetry/opentelemetry-python/pull/5123)) - `opentelemetry-exporter-otlp-proto-grpc`: Add grpc error details to the log message that's written when the grpc call fails. ([#5143](https://github.com/open-telemetry/opentelemetry-python/pull/5143)) - `opentelemetry-exporter-http-transport`: add 'opentelemetry-exporter-http-transport' package for HTTP exporters ([#5194](https://github.com/open-telemetry/opentelemetry-python/pull/5194)) - `opentelemetry-sdk`: Add `composite/development` samplers support to declarative file configuration ([#5201](https://github.com/open-telemetry/opentelemetry-python/pull/5201)) - `opentelemetry-exporter-otlp-json-file`: Add OTLP JSON File exporter implementation ([#5207](https://github.com/open-telemetry/opentelemetry-python/pull/5207)) - `opentelemetry-sdk`: add `_resolve_component` shared utility for declarative config plugin loading, reducing boilerplate in exporter factory functions ([#5215](https://github.com/open-telemetry/opentelemetry-python/pull/5215)) - `opentelemetry-sdk`: add pull metric reader support to declarative file configuration, including Prometheus metric reader via the `prometheus_development` config field ([#5216](https://github.com/open-telemetry/opentelemetry-python/pull/5216)) - `opentelemetry-proto-json`: update to use opentelemetry-proto v1.10.0 ([#5224](https://github.com/open-telemetry/opentelemetry-python/pull/5224)) - `opentelemetry-proto`: bump maximum supported protobuf version to 7.x.x ([#5251](https://github.com/open-telemetry/opentelemetry-python/pull/5251)) - `opentelemetry-sdk`: add `ServiceInstanceIdResourceDetector` for populating `service.instance.id` ([#5259](https://github.com/open-telemetry/opentelemetry-python/pull/5259)) - `opentelemetry-sdk`: declarative config loader now recursively converts parsed dicts into typed dataclass instances, including nested dataclasses, lists of dataclasses, and enum values. End-to-end YAML/JSON → SDK configuration now works via the factory functions. ([#5269](https://github.com/open-telemetry/opentelemetry-python/pull/5269)) - `opentelemetry-sdk`: add `configure_sdk(config)` to the declarative configuration API. Single entry point that takes a parsed `OpenTelemetryConfiguration`, builds the resource, and applies the tracer/meter/logger providers and propagator globally. Honors the top-level `disabled` flag. ([#5270](https://github.com/open-telemetry/opentelemetry-python/pull/5270)) - `opentelemetry-sdk`: the SDK configurator now honors the `OTEL_CONFIG_FILE` environment variable. When set, the SDK loads and applies the referenced declarative configuration file (YAML or JSON) in place of the env-var-based init path. ([#5271](https://github.com/open-telemetry/opentelemetry-python/pull/5271)) - `opentelemetry-sdk`: update declarative config to use `ServiceInstanceIdResourceDetector` ([#5299](https://github.com/open-telemetry/opentelemetry-python/pull/5299)) - `opentelemetry-exporter-otlp-proto-common`, `opentelemetry-exporter-otlp-json-common`: encoders now always accept null, and encode it as an empty `AnyValue` in accordance with the spec. ([#5305](https://github.com/open-telemetry/opentelemetry-python/pull/5305)) - `opentelemetry-sdk`: validate the declarative config `file_format` version — reject an unsupported major version and warn on a newer minor version, per the configuration spec versioning rules ([#5315](https://github.com/open-telemetry/opentelemetry-python/pull/5315)) - `opentelemetry-exporter-http-transport`: enable entry-point loading of transport implementations ([#5320](https://github.com/open-telemetry/opentelemetry-python/pull/5320)) ### Changed - `opentelemetry-sdk`: introduce experimental entry points for OpAMP agent integration ([#4646](https://github.com/open-telemetry/opentelemetry-python/pull/4646)) - `opentelemetry-api`: conditionally import entrypoints for `opentelemetry_context` only if the `OTEL_PYTHON_CONTEXT` env variable is defined, return `ContextVarsRuntimeContext` otherwise ([#5144](https://github.com/open-telemetry/opentelemetry-python/pull/5144)) - `opentelemetry-sdk`: rename "known/unknown" to "built-in/user-defined" terminology in declarative config component loading code ([#5214](https://github.com/open-telemetry/opentelemetry-python/pull/5214)) - `opentelemetry-proto`: regenerate protobuf code from opentelemetry-proto v1.10.0 ([#5223](https://github.com/open-telemetry/opentelemetry-python/pull/5223)) - `opentelemetry-sdk`: remove unnecessary `copy` in Span creation ([#5272](https://github.com/open-telemetry/opentelemetry-python/pull/5272)) - opentelemetry-sdk: remove unnecessary dict in set_attribute method ([#5274](https://github.com/open-telemetry/opentelemetry-python/pull/5274)) - `opentelemetry-sdk`: inline the method `_clean_attribute_value` ([#5275](https://github.com/open-telemetry/opentelemetry-python/pull/5275)) - Remove typing aliases deprecated in python 3.9 and replace all usages of `typing.Union` and `typing.Optional` with `|`. ([#5277](https://github.com/open-telemetry/opentelemetry-python/pull/5277)) - opentelemetry-sdk: remove generator in the accessor for links/events ([#5287](https://github.com/open-telemetry/opentelemetry-python/pull/5287)) - opentelemetry-api: remove unnecessary copy in iterator ([#5288](https://github.com/open-telemetry/opentelemetry-python/pull/5288)) - `opentelemetry-api`: update `EnvironmentGetter` to ignore non-normalized environment variable names ([#5289](https://github.com/open-telemetry/opentelemetry-python/pull/5289)) - opentelemetry-sdk: update iterator for BoundedList ([#5297](https://github.com/open-telemetry/opentelemetry-python/pull/5297)) - opentelemetry-sdk: reduce lock contention in attributes ([#5298](https://github.com/open-telemetry/opentelemetry-python/pull/5298)) - opentelemetry-sdk: merge doesn't need a copy, dict already does this ([#5326](https://github.com/open-telemetry/opentelemetry-python/pull/5326)) - `opentelemetry-api`: normalize empty environment propagation names to `_` in `EnvironmentSetter` and `EnvironmentGetter` ([#5328](https://github.com/open-telemetry/opentelemetry-python/pull/5328)) ### Fixed - Add missing `.rst` files to Sphinx documentation build for SDK logs, propagators, and exporter submodules. ([#5017](https://github.com/open-telemetry/opentelemetry-python/pull/5017)) - Preserve the random trace ID flag when creating child spans instead of always setting the random trace id bit depending on the available trace id generator. ([#5241](https://github.com/open-telemetry/opentelemetry-python/pull/5241)) - `opentelemetry-api`: fix SelectableGroups deprecation warning ([#5250](https://github.com/open-telemetry/opentelemetry-python/pull/5250)) - `opentelemetry-sdk`: make `SynchronousMeasurementConsumer.consume_measurement` lock free to avoid deadlocks ([#5321](https://github.com/open-telemetry/opentelemetry-python/pull/5321)) ## Version 1.42.1/0.63b1 (2026-05-21) ### Fixed - Preserve the random trace ID flag when creating child spans instead of always setting the random trace id bit depending on the available trace id generator. ([#5241](https://github.com/open-telemetry/opentelemetry-python/pull/5241)) ## Version 1.42.0/0.63b0 (2026-05-19) ### Added - `opentelemetry-api`, `opentelemetry-sdk`: add support for 'random-trace-id' flags in W3C traceparent header trace flags. Implementations of `IdGenerator` that do randomly generate the 56 least significant bits, should also implement a `is_trace_id_random` methods that returns `True`. ([#4854](https://github.com/open-telemetry/opentelemetry-python/pull/4854)) - logs: add exception support to Logger emit and LogRecord attributes ([#4908](https://github.com/open-telemetry/opentelemetry-python/pull/4908)) - `opentelemetry-exporter-otlp-proto-grpc`: make retryable gRPC error codes configurable for gRPC exporters ([#4917](https://github.com/open-telemetry/opentelemetry-python/pull/4917)) - `opentelemetry-sdk`: Add `create_logger_provider`/`configure_logger_provider` to declarative file configuration, enabling LoggerProvider instantiation from config files without reading env vars ([#4990](https://github.com/open-telemetry/opentelemetry-python/pull/4990)) - `opentelemetry-exporter-otlp-json-common`: add 'opentelemetry-exporter-otlp-json-common' package for OTLP JSON exporters ([#4996](https://github.com/open-telemetry/opentelemetry-python/pull/4996)) - `opentelemetry-sdk`: Add `service` resource detector support to declarative file configuration via `detection_development.detectors[].service` ([#5003](https://github.com/open-telemetry/opentelemetry-python/pull/5003)) - `opentelemetry-docker-tests`: add docker-tests coverage of `opentelemetry-exporter-otlp-proto-grpc` and `opentelemetry-exporter-otlp-proto-http` metrics export ([#5030](https://github.com/open-telemetry/opentelemetry-python/pull/5030)) - Add `registry` keyword argument to `PrometheusMetricReader` to allow passing a custom Prometheus registry ([#5055](https://github.com/open-telemetry/opentelemetry-python/pull/5055)) - Add WeaverLiveCheck test util ([#5088](https://github.com/open-telemetry/opentelemetry-python/pull/5088)) - `opentelemetry-sdk`: add `load_entry_point` shared utility to declarative file configuration for loading plugins via entry points; refactor propagator loading to use it ([#5093](https://github.com/open-telemetry/opentelemetry-python/pull/5093)) - `opentelemetry-sdk`: add sampler plugin loading to declarative file configuration via the `opentelemetry_sampler` entry point group, matching the spec's PluginComponentProvider mechanism ([#5095](https://github.com/open-telemetry/opentelemetry-python/pull/5095)) - `opentelemetry-sdk`: add propagator plugin loading to declarative file configuration via the `opentelemetry_propagator` entry point group, matching the spec's PluginComponentProvider mechanism ([#5098](https://github.com/open-telemetry/opentelemetry-python/pull/5098)) - `opentelemetry-sdk`: add exporter plugin loading to declarative file configuration for all three signals (traces, metrics, logs) via the `opentelemetry_*_exporter` entry point groups, matching the spec's PluginComponentProvider mechanism ([#5128](https://github.com/open-telemetry/opentelemetry-python/pull/5128)) - `opentelemetry-sdk`: add generic resource detector plugin loading to declarative file configuration via the `opentelemetry_resource_detector` entry point group, matching the spec's PluginComponentProvider mechanism ([#5129](https://github.com/open-telemetry/opentelemetry-python/pull/5129)) - `opentelemetry-sdk`: add `additional_properties` support to generated config models via custom `datamodel-codegen` template, enabling plugin/custom component names to flow through typed dataclasses ([#5131](https://github.com/open-telemetry/opentelemetry-python/pull/5131)) - Add ability to selectively enable exporting of SDK internal metrics with the `OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED` environment variable. ([#5151](https://github.com/open-telemetry/opentelemetry-python/pull/5151)) ### Changed - `opentelemetry-semantic-conventions`: use `X | Y` union annotation ([#5096](https://github.com/open-telemetry/opentelemetry-python/pull/5096)) - `opentelemetry-api`: update `EnvironmentGetter` and `EnvironmentSetter` to use normalized environment variable names ([#5119](https://github.com/open-telemetry/opentelemetry-python/pull/5119)) - Apply fixes for `UP` ruff rule ([#5133](https://github.com/open-telemetry/opentelemetry-python/pull/5133)) - `opentelemetry-sdk`: only load entrypoints for resource detectors if they are configured via `OTEL_EXPERIMENTAL_RESOURCE_DETECTORS` ([#5145](https://github.com/open-telemetry/opentelemetry-python/pull/5145)) - ci: wait for tracecontext server readiness instead of a fixed sleep in `scripts/tracecontext-integration-test.sh` ([#5149](https://github.com/open-telemetry/opentelemetry-python/pull/5149)) - Switch to SPDX license headers and add CI enforcement ([#5177](https://github.com/open-telemetry/opentelemetry-python/pull/5177)) - `opentelemetry-semantic-conventions`: Bump semantic conventions to 1.41.1, this changes the metrics name of `K8S_CONTAINER_CPU_LIMIT_UTILIZATION` and `K8S_CONTAINER_CPU_REQUEST_UTILIZATION`. ([#5200](https://github.com/open-telemetry/opentelemetry-python/pull/5200)) ### Removed - `opentelemetry-api`: remove third-party importlib-metadata in favor of standard library since Python >= 3.10 is now required ([#3234](https://github.com/open-telemetry/opentelemetry-python/pull/3234)) - Drop Python 3.9 support ([#5076](https://github.com/open-telemetry/opentelemetry-python/pull/5076)) ### Fixed - `opentelemetry-sdk`: Allow declarative OTLP HTTP exporters to map `compression: deflate` instead of rejecting it as unsupported. ([#5075](https://github.com/open-telemetry/opentelemetry-python/pull/5075)) - Fix incorrect code example in `create_tracer()` docstring ([#5077](https://github.com/open-telemetry/opentelemetry-python/pull/5077)) - `opentelemetry-sdk`: Fix `ProcessResourceDetector` to use `sys.orig_argv` so that `process.command`, `process.command_line`, and `process.command_args` reflect the original invocation for `python -m ` runs (where `sys.argv[0]` is rewritten to the module path) ([#5083](https://github.com/open-telemetry/opentelemetry-python/pull/5083)) - `opentelemetry-sdk`: fix YAML structure injection via environment variable substitution in declarative file configuration; values containing newlines are now emitted as quoted YAML scalars per spec requirement ([#5091](https://github.com/open-telemetry/opentelemetry-python/pull/5091)) - `opentelemetry-sdk`: Fix mutable attributes reference in metrics, attributes passed to instrument `add`/`record` are now copied so that subsequent mutations do not affect recorded data points ([#5106](https://github.com/open-telemetry/opentelemetry-python/pull/5106)) - `opentelemetry-sdk`: make resource detector ordering deterministic ([#5120](https://github.com/open-telemetry/opentelemetry-python/pull/5120)) - Fix incorrect type annotation on `detectors` parameter of `get_aggregated_resources` ([#5135](https://github.com/open-telemetry/opentelemetry-python/pull/5135)) - `opentelemetry-api`: Enforce W3C Baggage size limits on outbound propagation in `W3CBaggagePropagator.inject()`. Previously only inbound extraction enforced limits; now inject also caps entries at 180, individual pairs at 4096 bytes, and total header at 8192 bytes per the W3C Baggage spec. The extract path max_pairs limit now counts all size-valid entries rather than only successfully parsed ones. ([#5163](https://github.com/open-telemetry/opentelemetry-python/pull/5163)) - `opentelemetry-sdk`: fix multi-processor `force_flush` skipping remaining processors when one returns `None` ([#5179](https://github.com/open-telemetry/opentelemetry-python/pull/5179)) - `opentelemetry-test-utils`: fix weaver live check hanging when weaver log output fills the pipe buffer ([#5208](https://github.com/open-telemetry/opentelemetry-python/pull/5208)) ## Version 1.41.0/0.62b0 (2026-04-09) - `opentelemetry-sdk`: Add `host` resource detector support to declarative file configuration via `detection_development.detectors[].host` ([#5002](https://github.com/open-telemetry/opentelemetry-python/pull/5002)) - `opentelemetry-sdk`: Add `container` resource detector support to declarative file configuration via `detection_development.detectors[].container`, using entry point loading of the `opentelemetry-resource-detector-containerid` contrib package ([#5004](https://github.com/open-telemetry/opentelemetry-python/pull/5004)) - `opentelemetry-sdk`: Add `create_tracer_provider`/`configure_tracer_provider` to declarative file configuration, enabling TracerProvider instantiation from config files without reading env vars ([#4985](https://github.com/open-telemetry/opentelemetry-python/pull/4985)) - Enabled the flake8-tidy-import plugins rules for the ruff linter. These rules throw warnings for relative imports in the modules. ([#5019](https://github.com/open-telemetry/opentelemetry-python/pull/5019)) - `opentelemetry-sdk`: Fix `AttributeError` in `ExplicitBucketHistogramAggregation` when applied to non-Histogram instruments without explicit boundaries ([#5034](https://github.com/open-telemetry/opentelemetry-python/pull/5034)) - Fix `BatchLogRecordProcessor` default `schedule_delay_millis` from 5000ms to 1000ms to comply with the OTel specification. Note: logs may be exported 5x more frequently by default (e.g. for users who don't explicitly set the `OTEL_BLRP_SCHEDULE_DELAY` env var). ([#4998](https://github.com/open-telemetry/opentelemetry-python/pull/4998)) - `opentelemetry-sdk`: Add `process` resource detector support to declarative file configuration via `detection_development.detectors[].process` ([#5001](https://github.com/open-telemetry/opentelemetry-python/pull/5001)) - `opentelemetry-sdk`: Add shared `_parse_headers` helper for declarative config OTLP exporters ([#5021](https://github.com/open-telemetry/opentelemetry-python/pull/5021)) - `opentelemetry-api`: Replace a broad exception in attribute cleaning tests to satisfy pylint in the `lint-opentelemetry-api` CI job - `opentelemetry-sdk`: Add `create_meter_provider`/`configure_meter_provider` to declarative file configuration, enabling MeterProvider instantiation from config files without reading env vars ([#4987](https://github.com/open-telemetry/opentelemetry-python/pull/4987)) - `opentelemetry-sdk`: Add `create_resource` and `create_propagator`/`configure_propagator` to declarative file configuration, enabling Resource and propagator instantiation from config files without reading env vars ([#4979](https://github.com/open-telemetry/opentelemetry-python/pull/4979)) - `opentelemetry-sdk`: Map Python `CRITICAL` log level to OTel `FATAL` severity text per the specification ([#4984](https://github.com/open-telemetry/opentelemetry-python/issues/4984)) - `opentelemetry-sdk`: Add file configuration support with YAML/JSON loading, environment variable substitution, and schema validation against the vendored OTel config JSON schema ([#4898](https://github.com/open-telemetry/opentelemetry-python/pull/4898)) - Fix intermittent CI failures in `getting-started` and `tracecontext` jobs caused by GitHub git CDN SHA propagation lag by installing contrib packages from the already-checked-out local copy instead of a second git clone ([#4958](https://github.com/open-telemetry/opentelemetry-python/pull/4958)) - `opentelemetry-sdk`: fix type annotations on `MetricReader` and related types ([#4938](https://github.com/open-telemetry/opentelemetry-python/pull/4938/)) - `opentelemetry-sdk`: implement log creation metric ([#4935](https://github.com/open-telemetry/opentelemetry-python/pull/4935)) - `opentelemetry-sdk`: implement metric reader metrics ([#4970](https://github.com/open-telemetry/opentelemetry-python/pull/4970)) - `opentelemetry-sdk`: implement processor metrics ([#5012](https://github.com/open-telemetry/opentelemetry-python/pull/5012)) - `opentelemetry-sdk`: upgrade vendored OTel configuration schema from v1.0.0-rc.3 to v1.0.0 ([#4965](https://github.com/open-telemetry/opentelemetry-python/pull/4965)) - `opentelemetry-sdk`: implement exporter metrics ([#4976](https://github.com/open-telemetry/opentelemetry-python/pull/4976)) - improve check-links ci job ([#4978](https://github.com/open-telemetry/opentelemetry-python/pull/4978)) - Resolve some Pyright type errors in Span/ReadableSpan and utility stubs ([#4973](https://github.com/open-telemetry/opentelemetry-python/pull/4973)) - `opentelemetry-exporter-prometheus`: Fix metric name prefix ([#4895](https://github.com/open-telemetry/opentelemetry-python/pull/4895)) - `opentelemetry-api`, `opentelemetry-sdk`: Add deepcopy support for `BoundedAttributes` and `BoundedList` ([#4934](https://github.com/open-telemetry/opentelemetry-python/pull/4934)) - `opentelemetry-proto-json`, `opentelemetry-codegen-json`: Implement custom protoc plugin to generate OTLP JSON class definitions ([#4910](https://github.com/open-telemetry/opentelemetry-python/pull/4910)) - Add configurable `max_export_batch_size` to OTLP HTTP metrics exporter ([#4576](https://github.com/open-telemetry/opentelemetry-python/pull/4576)) - `opentelemetry-sdk`: Implement experimental Meter configurator ([#4966](https://github.com/open-telemetry/opentelemetry-python/pull/4966)) - `opentelemetry-exporter-otlp-proto-http`: use consistent protobuf for export request ([#5015](https://github.com/open-telemetry/opentelemetry-python/pull/5015)) - `opentelemetry-sdk`: cache TracerConfig into the tracer, this changes an internal interface. Only one Tracer with the same instrumentation scope will be created ([#5007](https://github.com/open-telemetry/opentelemetry-python/pull/5007)) - Redo OTLPMetricExporter unit tests of `max_export_batch_size` to use real `export` ([#5036](https://github.com/open-telemetry/opentelemetry-python/pull/5036)) - `opentelemetry-sdk`: Implement experimental Logger configurator ([#4980](https://github.com/open-telemetry/opentelemetry-python/pull/4980)) ## Version 1.40.0/0.61b0 (2026-03-04) - `opentelemetry-sdk`: deprecate `LoggingHandler` in favor of `opentelemetry-instrumentation-logging`, see `opentelemetry-instrumentation-logging` documentation ([#4919](https://github.com/open-telemetry/opentelemetry-python/pull/4919)) - `opentelemetry-sdk`: Clarify log processor error handling expectations in documentation ([#4915](https://github.com/open-telemetry/opentelemetry-python/pull/4915)) - bump semantic-conventions to v1.40.0 ([#4941](https://github.com/open-telemetry/opentelemetry-python/pull/4941)) - Add stale PR GitHub Action ([#4926](https://github.com/open-telemetry/opentelemetry-python/pull/4926)) - `opentelemetry-sdk`: Drop unused Jaeger exporter environment variables (exporter removed in 1.22.0) ([#4918](https://github.com/open-telemetry/opentelemetry-python/issues/4918)) - `opentelemetry-sdk`: Clarify timeout units in environment variable documentation ([#4906](https://github.com/open-telemetry/opentelemetry-python/pull/4906)) - `opentelemetry-exporter-otlp-proto-grpc`: Fix re-initialization of gRPC channel on UNAVAILABLE error ([#4825](https://github.com/open-telemetry/opentelemetry-python/pull/4825)) - `opentelemetry-exporter-prometheus`: Fix duplicate HELP/TYPE declarations for metrics with different label sets ([#4868](https://github.com/open-telemetry/opentelemetry-python/issues/4868)) - Allow loading all resource detectors by setting `OTEL_EXPERIMENTAL_RESOURCE_DETECTORS` to `*` ([#4819](https://github.com/open-telemetry/opentelemetry-python/pull/4819)) - `opentelemetry-sdk`: Fix the type hint of the `_metrics_data` property to allow `None` ([#4837](https://github.com/open-telemetry/opentelemetry-python/pull/4837)). - Regenerate opentelemetry-proto code with v1.9.0 release ([#4840](https://github.com/open-telemetry/opentelemetry-python/pull/4840)) - Add python 3.14 support ([#4798](https://github.com/open-telemetry/opentelemetry-python/pull/4798)) - Silence events API warnings for internal users ([#4847](https://github.com/open-telemetry/opentelemetry-python/pull/4847)) - opentelemetry-sdk: make it possible to override the default processors in the SDK configurator ([#4806](https://github.com/open-telemetry/opentelemetry-python/pull/4806)) - Prevent possible endless recursion from happening in `SimpleLogRecordProcessor.on_emit`, ([#4799](https://github.com/open-telemetry/opentelemetry-python/pull/4799)) and ([#4867](https://github.com/open-telemetry/opentelemetry-python/pull/4867)). - Implement span start/end metrics ([#4880](https://github.com/open-telemetry/opentelemetry-python/pull/4880)) - Add environment variable carriers to API ([#4609](https://github.com/open-telemetry/opentelemetry-python/pull/4609)) - Add experimental composable rule based sampler ([#4882](https://github.com/open-telemetry/opentelemetry-python/pull/4882)) - Make ConcurrentMultiSpanProcessor fork safe ([#4862](https://github.com/open-telemetry/opentelemetry-python/pull/4862)) - `opentelemetry-exporter-otlp-proto-http`: fix retry logic and error handling for connection failures in trace, metric, and log exporters ([#4709](https://github.com/open-telemetry/opentelemetry-python/pull/4709)) - `opentelemetry-sdk`: avoid RuntimeError during iteration of view instrument match dictionary in MetricReaderStorage.collect() ([#4891](https://github.com/open-telemetry/opentelemetry-python/pull/4891)) - Implement experimental TracerConfigurator ([#4861](https://github.com/open-telemetry/opentelemetry-python/pull/4861)) - `opentelemetry-sdk`: Fix instrument creation race condition ([#4913](https://github.com/open-telemetry/opentelemetry-python/pull/4913)) - bump semantic-conventions to v1.39.0 ([#4914](https://github.com/open-telemetry/opentelemetry-python/pull/4914)) - `opentelemetry-sdk`: automatically generate configuration models using OTel config JSON schema ([#4879](https://github.com/open-telemetry/opentelemetry-python/pull/4879)) ## Version 1.39.0/0.60b0 (2025-12-03) - `opentelemetry-api`: Convert objects of any type other than AnyValue in attributes to string to be exportable ([#4808](https://github.com/open-telemetry/opentelemetry-python/pull/4808)) - docs: Added sqlcommenter example ([#4734](https://github.com/open-telemetry/opentelemetry-python/pull/4734)) - build: bump ruff to 0.14.1 ([#4782](https://github.com/open-telemetry/opentelemetry-python/pull/4782)) - Add `opentelemetry-exporter-credential-provider-gcp` as an optional dependency to `opentelemetry-exporter-otlp-proto-grpc` and `opentelemetry-exporter-otlp-proto-http` ([#4760](https://github.com/open-telemetry/opentelemetry-python/pull/4760)) - feat: implement on ending in span processor ([#4775](https://github.com/open-telemetry/opentelemetry-python/pull/4775)) - semantic-conventions: Bump to 1.38.0 ([#4791](https://github.com/open-telemetry/opentelemetry-python/pull/4791)) - [BREAKING] Remove LogData and extend SDK LogRecord to have instrumentation scope ([#4676](https://github.com/open-telemetry/opentelemetry-python/pull/4676)) - [BREAKING] Rename several classes from Log to LogRecord ([#4647](https://github.com/open-telemetry/opentelemetry-python/pull/4647)) **Migration Guide:** `LogData` has been removed. Users should update their code as follows: - **For Log Exporters:** Change from `Sequence[LogData]` to `Sequence[ReadableLogRecord]` ```python # Before from opentelemetry.sdk._logs import LogData def export(self, batch: Sequence[LogData]) -> LogRecordExportResult: ... # After from opentelemetry.sdk._logs import ReadableLogRecord def export(self, batch: Sequence[ReadableLogRecord]) -> LogRecordExportResult: ... ``` - **For Log Processors:** Use `ReadWriteLogRecord` for processing, `ReadableLogRecord` for exporting ```python # Before from opentelemetry.sdk._logs import LogData def on_emit(self, log_data: LogData): ... # After from opentelemetry.sdk._logs import ReadWriteLogRecord, ReadableLogRecord def on_emit(self, log_record: ReadWriteLogRecord): # Convert to ReadableLogRecord before exporting readable = ReadableLogRecord( log_record=log_record.log_record, resource=log_record.resource or Resource.create({}), instrumentation_scope=log_record.instrumentation_scope, limits=log_record.limits, ) ... ``` - **Accessing log data:** Use the same attributes on `ReadableLogRecord`/`ReadWriteLogRecord` - `log_record.log_record` - The API LogRecord (contains body, severity, attributes, etc.) - `log_record.resource` - The Resource - `log_record.instrumentation_scope` - The InstrumentationScope (now included, was in LogData before) - `log_record.limits` - The LogRecordLimits - Mark the Events API/SDK as deprecated. The Logs API/SDK should be used instead, an event is now a `LogRecord` with the `event_name` field set ([#4654](https://github.com/open-telemetry/opentelemetry-python/pull/4654)). - Fix type checking for built-in metric exporters ([#4820](https://github.com/open-telemetry/opentelemetry-python/pull/4820)) ## Version 1.38.0/0.59b0 (2025-10-16) - Add `rstcheck` to pre-commit to stop introducing invalid RST ([#4755](https://github.com/open-telemetry/opentelemetry-python/pull/4755)) - logs: extend Logger.emit to accept separated keyword arguments ([#4737](https://github.com/open-telemetry/opentelemetry-python/pull/4737)) - logs: add warnings for classes that would be deprecated and renamed in 1.39.0 ([#4771](https://github.com/open-telemetry/opentelemetry-python/pull/4771)) ## Version 1.37.0/0.58b0 (2025-09-11) - Add experimental composite samplers ([#4714](https://github.com/open-telemetry/opentelemetry-python/pull/4714)) - Add new environment variables to the SDK `OTEL_PYTHON_EXPORTER_OTLP_{HTTP/GRPC}_{METRICS/TRACES/LOGS}_CREDENTIAL_PROVIDER` that can be used to inject a `requests.Session` or `grpc.ChannelCredentials` object into OTLP exporters created during auto instrumentation [#4689](https://github.com/open-telemetry/opentelemetry-python/pull/4689). - Filter duplicate logs out of some internal `logger`'s logs on the export logs path that might otherwise endlessly log or cause a recursion depth exceeded issue in cases where logging itself results in an exception. ([#4695](https://github.com/open-telemetry/opentelemetry-python/pull/4695)). - docs: linked the examples with their github source code location and added Prometheus example ([#4728](https://github.com/open-telemetry/opentelemetry-python/pull/4728)) - Permit to override default HTTP OTLP exporters headers ([#4634](https://github.com/open-telemetry/opentelemetry-python/pull/4634)) - semantic-conventions: Bump to 1.37.0 ([#4731](https://github.com/open-telemetry/opentelemetry-python/pull/4731)) - opentelemetry-sdk: fix handling of OTEL_ATTRIBUTE_COUNT_LIMIT in logs ([#4677](https://github.com/open-telemetry/opentelemetry-python/pull/4677)) - Performance: Cache `importlib_metadata.entry_points` ([#4735](https://github.com/open-telemetry/opentelemetry-python/pull/4735)) - opentelemetry-sdk: fix calling Logger.emit with an API LogRecord instance ([#4741](https://github.com/open-telemetry/opentelemetry-python/pull/4741)) ## Version 1.36.0/0.57b0 (2025-07-29) - Add missing Prometheus exporter documentation ([#4485](https://github.com/open-telemetry/opentelemetry-python/pull/4485)) - Overwrite logging.config.fileConfig and logging.config.dictConfig to ensure the OTLP `LogHandler` remains attached to the root logger. Fix a bug that can cause a deadlock to occur over `logging._lock` in some cases ([#4636](https://github.com/open-telemetry/opentelemetry-python/pull/4636)). - otlp-http-exporter: set default value for param `timeout_sec` in `_export` method ([#4691](https://github.com/open-telemetry/opentelemetry-python/pull/4691)) - Update OTLP gRPC/HTTP exporters: calling shutdown will now interrupt exporters that are sleeping before a retry attempt, and cause them to return failure immediately. Update BatchSpan/LogRecordProcessors: shutdown will now complete after 30 seconds of trying to finish exporting any buffered telemetry, instead of continuing to export until all telemetry was exported. ([#4638](https://github.com/open-telemetry/opentelemetry-python/pull/4638)). ## Version 1.35.0/0.56b0 (2025-07-11) - Update OTLP proto to v1.7 [#4645](https://github.com/open-telemetry/opentelemetry-python/pull/4645). - Add `event_name` as a top level field in the `LogRecord`. Events are now simply logs with the `event_name` field set, the logs SDK should be used to emit events ([#4652](https://github.com/open-telemetry/opentelemetry-python/pull/4652)). - Update OTLP gRPC/HTTP exporters: the export timeout is now inclusive of all retries and backoffs. A +/-20% jitter was added to all backoffs. A pointless 32 second sleep that occurred after all retries had completed/failed was removed. ([#4564](https://github.com/open-telemetry/opentelemetry-python/pull/4564)). - Update ConsoleLogExporter.export to handle LogRecord's containing bytes type in the body ([#4614](https://github.com/open-telemetry/opentelemetry-python/pull/4614/)). - opentelemetry-sdk: Fix invalid `type: ignore` that causes mypy to ignore the whole file ([#4618](https://github.com/open-telemetry/opentelemetry-python/pull/4618)) - Add `span_exporter` property back to `BatchSpanProcessor` class ([#4621](https://github.com/open-telemetry/opentelemetry-python/pull/4621)) - Fix license field in pyproject.toml files ([#4625](https://github.com/open-telemetry/opentelemetry-python/pull/4625)) - Update logger level to NOTSET in logs example ([#4637](https://github.com/open-telemetry/opentelemetry-python/pull/4637)) - Logging API accepts optional `context`; deprecates `trace_id`, `span_id`, `trace_flags`. ([#4597](https://github.com/open-telemetry/opentelemetry-python/pull/4597)) and ([#4668](https://github.com/open-telemetry/opentelemetry-python/pull/4668)) - sdk: use context instead of trace_id,span_id for initializing LogRecord ([#4653](https://github.com/open-telemetry/opentelemetry-python/pull/4653)) - Rename LogRecordProcessor.emit to on_emit ([#4648](https://github.com/open-telemetry/opentelemetry-python/pull/4648)) - Logging API hide std_to_otel function to convert python logging severity to otel severity ([#4649](https://github.com/open-telemetry/opentelemetry-python/pull/4649)) - proto: relax protobuf version requirement to support v6 ([#4620](https://github.com/open-telemetry/opentelemetry-python/pull/4620)) - Bump semantic-conventions to 1.36.0 ([#4669](https://github.com/open-telemetry/opentelemetry-python/pull/4669)) - Set expected User-Agent in HTTP headers for grpc OTLP exporter ([#4658](https://github.com/open-telemetry/opentelemetry-python/pull/4658)) ## Version 1.34.0/0.55b0 (2025-06-04) - typecheck: add sdk/resources and drop mypy ([#4578](https://github.com/open-telemetry/opentelemetry-python/pull/4578)) - Use PEP702 for marking deprecations ([#4522](https://github.com/open-telemetry/opentelemetry-python/pull/4522)) - Refactor `BatchLogRecordProcessor` and `BatchSpanProcessor` to simplify code and make the control flow more clear ([#4562](https://github.com/open-telemetry/opentelemetry-python/pull/4562/) [#4535](https://github.com/open-telemetry/opentelemetry-python/pull/4535), and [#4580](https://github.com/open-telemetry/opentelemetry-python/pull/4580)). - Remove log messages from `BatchLogRecordProcessor.emit`, this caused the program to crash at shutdown with a max recursion error ([#4586](https://github.com/open-telemetry/opentelemetry-python/pull/4586)). - Configurable max retry timeout for grpc exporter ([#4333](https://github.com/open-telemetry/opentelemetry-python/pull/4333)) - opentelemetry-api: allow importlib-metadata 8.7.0 ([#4593](https://github.com/open-telemetry/opentelemetry-python/pull/4593)) - opentelemetry-test-utils: assert explicit bucket boundaries in histogram metrics ([#4595](https://github.com/open-telemetry/opentelemetry-python/pull/4595)) - Bump semantic conventions to 1.34.0 ([#4599](https://github.com/open-telemetry/opentelemetry-python/pull/4599)) - Drop support for Python 3.8 ([#4520](https://github.com/open-telemetry/opentelemetry-python/pull/4520)) ## Version 1.33.0/0.54b0 (2025-05-09) - Fix intermittent `Connection aborted` error when using otlp/http exporters ([#4477](https://github.com/open-telemetry/opentelemetry-python/pull/4477)) - opentelemetry-sdk: use stable code attributes: `code.function` -> `code.function.name`, `code.lineno` -> `code.line.number`, `code.filepath` -> `code.file.path` ([#4508](https://github.com/open-telemetry/opentelemetry-python/pull/4508)) - Fix serialization of extended attributes for logs signal ([#4342](https://github.com/open-telemetry/opentelemetry-python/pull/4342)) - Handle OTEL_PROPAGATORS contains None ([#4553](https://github.com/open-telemetry/opentelemetry-python/pull/4553)) - docs: updated and added to the metrics and log examples ([#4559](https://github.com/open-telemetry/opentelemetry-python/pull/4559)) - Bump semantic conventions to 1.33.0 ([#4567](https://github.com/open-telemetry/opentelemetry-python/pull/4567)) ## Version 1.32.0/0.53b0 (2025-04-10) - Fix user agent in OTLP HTTP metrics exporter ([#4475](https://github.com/open-telemetry/opentelemetry-python/pull/4475)) - Improve performance of baggage operations ([#4466](https://github.com/open-telemetry/opentelemetry-python/pull/4466)) - sdk: remove duplicated constant definitions for `environment_variables` ([#4491](https://github.com/open-telemetry/opentelemetry-python/pull/4491)) - api: Revert record `BaseException` change in `trace_api.use_span()` ([#4494](https://github.com/open-telemetry/opentelemetry-python/pull/4494)) - Improve CI by cancelling stale runs and setting timeouts ([#4498](https://github.com/open-telemetry/opentelemetry-python/pull/4498)) - Patch logging.basicConfig so OTel logs don't cause console logs to disappear ([#4436](https://github.com/open-telemetry/opentelemetry-python/pull/4436)) - Bump semantic conventions to 1.32.0 ([#4530](https://github.com/open-telemetry/opentelemetry-python/pull/4530)) - Fix ExplicitBucketHistogramAggregation to handle multiple explicit bucket boundaries advisories ([#4521](https://github.com/open-telemetry/opentelemetry-python/pull/4521)) - opentelemetry-sdk: Fix serialization of objects in log handler ([#4528](https://github.com/open-telemetry/opentelemetry-python/pull/4528)) ## Version 1.31.0/0.52b0 (2025-03-12) - semantic-conventions: Bump to 1.31.0 ([#4471](https://github.com/open-telemetry/opentelemetry-python/pull/4471)) - Add type annotations to context's attach & detach ([#4346](https://github.com/open-telemetry/opentelemetry-python/pull/4346)) - Fix OTLP encoders missing instrumentation scope schema url and attributes ([#4359](https://github.com/open-telemetry/opentelemetry-python/pull/4359)) - prometheus-exporter: fix labels out of place for data points with different attribute sets ([#4413](https://github.com/open-telemetry/opentelemetry-python/pull/4413)) - Type indent parameter in to_json ([#4402](https://github.com/open-telemetry/opentelemetry-python/pull/4402)) - Tolerates exceptions when loading resource detectors via `OTEL_EXPERIMENTAL_RESOURCE_DETECTORS` ([#4373](https://github.com/open-telemetry/opentelemetry-python/pull/4373)) - Disconnect gRPC client stub when shutting down `OTLPSpanExporter` ([#4370](https://github.com/open-telemetry/opentelemetry-python/pull/4370)) - opentelemetry-sdk: fix OTLP exporting of Histograms with explicit buckets advisory ([#4434](https://github.com/open-telemetry/opentelemetry-python/pull/4434)) - opentelemetry-exporter-otlp-proto-grpc: better dependency version range for Python 3.13 ([#4444](https://github.com/open-telemetry/opentelemetry-python/pull/4444)) - opentelemetry-exporter-opencensus: better dependency version range for Python 3.13 ([#4444](https://github.com/open-telemetry/opentelemetry-python/pull/4444)) - Updated `tracecontext-integration-test` gitref to `d782773b2cf2fa4afd6a80a93b289d8a74ca894d` ([#4448](https://github.com/open-telemetry/opentelemetry-python/pull/4448)) - Make `trace_api.use_span()` record `BaseException` as well as `Exception` ([#4406](https://github.com/open-telemetry/opentelemetry-python/pull/4406)) - Fix env var error message for TraceLimits/SpanLimits ([#4458](https://github.com/open-telemetry/opentelemetry-python/pull/4458)) - pylint-ci updated python version to 3.13 ([#4450](https://github.com/open-telemetry/opentelemetry-python/pull/4450)) - Fix memory leak in Log & Trace exporter ([#4449](https://github.com/open-telemetry/opentelemetry-python/pull/4449)) ## Version 1.30.0/0.51b0 (2025-02-03) - Always setup logs sdk, OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED only controls python `logging` module handler setup ([#4340](https://github.com/open-telemetry/opentelemetry-python/pull/4340)) - Add `attributes` field in `metrics.get_meter` wrapper function ([#4364](https://github.com/open-telemetry/opentelemetry-python/pull/4364)) - Add Python 3.13 support ([#4353](https://github.com/open-telemetry/opentelemetry-python/pull/4353)) - sdk: don't log or print warnings when the SDK has been disabled ([#4371](https://github.com/open-telemetry/opentelemetry-python/pull/4371)) - Fix span context manager typing by using ParamSpec from typing_extensions ([#4389](https://github.com/open-telemetry/opentelemetry-python/pull/4389)) - Fix serialization of None values in logs body to match 1.31.0+ data model ([#4400](https://github.com/open-telemetry/opentelemetry-python/pull/4400)) - [BREAKING] semantic-conventions: Remove `opentelemetry.semconv.attributes.network_attributes.NETWORK_INTERFACE_NAME` introduced by mistake in the wrong module. ([#4391](https://github.com/open-telemetry/opentelemetry-python/pull/4391)) - Add support for explicit bucket boundaries advisory for Histograms ([#4361](https://github.com/open-telemetry/opentelemetry-python/pull/4361)) - semantic-conventions: Bump to 1.30.0 ([#4337](https://github.com/open-telemetry/opentelemetry-python/pull/4397)) ## Version 1.29.0/0.50b0 (2024-12-11) - Fix crash exporting a log record with None body ([#4276](https://github.com/open-telemetry/opentelemetry-python/pull/4276)) - Fix metrics export with exemplar and no context and filtering observable instruments ([#4251](https://github.com/open-telemetry/opentelemetry-python/pull/4251)) - Fix recursion error with sdk disabled and handler added to root logger ([#4259](https://github.com/open-telemetry/opentelemetry-python/pull/4259)) - sdk: setup EventLogger when OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED is set ([#4270](https://github.com/open-telemetry/opentelemetry-python/pull/4270)) - api: fix logging of duplicate EventLogger setup warning ([#4299](https://github.com/open-telemetry/opentelemetry-python/pull/4299)) - sdk: fix setting of process owner in ProcessResourceDetector ([#4311](https://github.com/open-telemetry/opentelemetry-python/pull/4311)) - sdk: fix serialization of logs severity_number field to int ([#4324](https://github.com/open-telemetry/opentelemetry-python/pull/4324)) - Remove `TestBase.assertEqualSpanInstrumentationInfo` method, use `assertEqualSpanInstrumentationScope` instead ([#4310](https://github.com/open-telemetry/opentelemetry-python/pull/4310)) - sdk: instantiate lazily `ExemplarBucket`s in `ExemplarReservoir`s ([#4260](https://github.com/open-telemetry/opentelemetry-python/pull/4260)) - semantic-conventions: Bump to 1.29.0 ([#4337](https://github.com/open-telemetry/opentelemetry-python/pull/4337)) ## Version 1.28.0/0.49b0 (2024-11-05) - Removed superfluous py.typed markers and added them where they were missing ([#4172](https://github.com/open-telemetry/opentelemetry-python/pull/4172)) - Include metric info in encoding exceptions ([#4154](https://github.com/open-telemetry/opentelemetry-python/pull/4154)) - sdk: Add support for log formatting ([#4137](https://github.com/open-telemetry/opentelemetry-python/pull/4166)) - sdk: Add Host resource detector ([#4182](https://github.com/open-telemetry/opentelemetry-python/pull/4182)) - sdk: Implementation of exemplars ([#4094](https://github.com/open-telemetry/opentelemetry-python/pull/4094)) - Implement events sdk ([#4176](https://github.com/open-telemetry/opentelemetry-python/pull/4176)) - Update semantic conventions to version 1.28.0 ([#4218](https://github.com/open-telemetry/opentelemetry-python/pull/4218)) - Add support to protobuf 5+ and drop support to protobuf 3 and 4 ([#4206](https://github.com/open-telemetry/opentelemetry-python/pull/4206)) - Update environment variable descriptions to match signal ([#4222](https://github.com/open-telemetry/opentelemetry-python/pull/4222)) - Record logger name as the instrumentation scope name ([#4208](https://github.com/open-telemetry/opentelemetry-python/pull/4208)) - Fix memory leak in exporter and reader ([#4224](https://github.com/open-telemetry/opentelemetry-python/pull/4224)) - Drop `OTEL_PYTHON_EXPERIMENTAL_DISABLE_PROMETHEUS_UNIT_NORMALIZATION` environment variable ([#4217](https://github.com/open-telemetry/opentelemetry-python/pull/4217)) - Improve compatibility with other logging libraries that override `LogRecord.getMessage()` in order to customize message formatting ([#4216](https://github.com/open-telemetry/opentelemetry-python/pull/4216)) ## Version 1.27.0/0.48b0 (2024-08-28) - Implementation of Events API ([#4054](https://github.com/open-telemetry/opentelemetry-python/pull/4054)) - Make log sdk add `exception.message` to logRecord for exceptions whose argument is an exception not a string message ([#4122](https://github.com/open-telemetry/opentelemetry-python/pull/4122)) - Fix use of `link.attributes.dropped`, which may not exist ([#4119](https://github.com/open-telemetry/opentelemetry-python/pull/4119)) - Running mypy on SDK resources ([#4053](https://github.com/open-telemetry/opentelemetry-python/pull/4053)) - Added py.typed file to top-level module ([#4084](https://github.com/open-telemetry/opentelemetry-python/pull/4084)) - Drop Final annotation from Enum in semantic conventions ([#4085](https://github.com/open-telemetry/opentelemetry-python/pull/4085)) - Update log export example to not use root logger ([#4090](https://github.com/open-telemetry/opentelemetry-python/pull/4090)) - sdk: Add OS resource detector ([#3992](https://github.com/open-telemetry/opentelemetry-python/pull/3992)) - sdk: Accept non URL-encoded headers in `OTEL_EXPORTER_OTLP_*HEADERS` to match other languages SDKs ([#4103](https://github.com/open-telemetry/opentelemetry-python/pull/4103)) - Update semantic conventions to version 1.27.0 ([#4104](https://github.com/open-telemetry/opentelemetry-python/pull/4104)) - Add support to type bytes for OTLP AnyValue ([#4128](https://github.com/open-telemetry/opentelemetry-python/pull/4128)) - Export ExponentialHistogram and ExponentialHistogramDataPoint ([#4134](https://github.com/open-telemetry/opentelemetry-python/pull/4134)) - Implement Client Key and Certificate File Support for All OTLP Exporters ([#4116](https://github.com/open-telemetry/opentelemetry-python/pull/4116)) - Remove `_start_time_unix_nano` attribute from `_ViewInstrumentMatch` in favor of using `time_ns()` at the moment when the aggregation object is created ([#4137](https://github.com/open-telemetry/opentelemetry-python/pull/4137)) ## Version 1.26.0/0.47b0 (2024-07-25) - Standardizing timeout calculation in measurement consumer collect to nanoseconds ([#4074](https://github.com/open-telemetry/opentelemetry-python/pull/4074)) - optional scope attributes for logger creation ([#4035](https://github.com/open-telemetry/opentelemetry-python/pull/4035)) - optional scope attribute for tracer creation ([#4028](https://github.com/open-telemetry/opentelemetry-python/pull/4028)) - OTLP exporter is encoding invalid span/trace IDs in the logs fix ([#4006](https://github.com/open-telemetry/opentelemetry-python/pull/4006)) - Update sdk process resource detector `process.command_args` attribute to also include the executable itself ([#4032](https://github.com/open-telemetry/opentelemetry-python/pull/4032)) - Fix `start_time_unix_nano` for delta collection for explicit bucket histogram aggregation ([#4009](https://github.com/open-telemetry/opentelemetry-python/pull/4009)) - Fix `start_time_unix_nano` for delta collection for sum aggregation ([#4011](https://github.com/open-telemetry/opentelemetry-python/pull/4011)) - Update opentracing and opencesus docs examples to not use JaegerExporter ([#4023](https://github.com/open-telemetry/opentelemetry-python/pull/4023)) - Do not execute Flask Tests in debug mode ([#3956](https://github.com/open-telemetry/opentelemetry-python/pull/3956)) - When encountering an error encoding metric attributes in the OTLP exporter, log the key that had an error. ([#3838](https://github.com/open-telemetry/opentelemetry-python/pull/3838)) - Fix `ExponentialHistogramAggregation` ([#3978](https://github.com/open-telemetry/opentelemetry-python/pull/3978)) - Log a warning when a `LogRecord` in `sdk/log` has dropped attributes due to reaching limits ([#3946](https://github.com/open-telemetry/opentelemetry-python/pull/3946)) - Fix RandomIdGenerator can generate invalid Span/Trace Ids ([#3949](https://github.com/open-telemetry/opentelemetry-python/pull/3949)) - Add Python 3.12 to tox ([#3616](https://github.com/open-telemetry/opentelemetry-python/pull/3616)) - Improve resource field structure for LogRecords ([#3972](https://github.com/open-telemetry/opentelemetry-python/pull/3972)) - Update Semantic Conventions code generation scripts: - fix namespace exclusion that resulted in dropping `os` and `net` namespaces. - add `Final` decorator to constants to prevent collisions - enable mypy and fix detected issues - allow to drop specific attributes in preparation for Semantic Conventions v1.26.0 ([#3973](https://github.com/open-telemetry/opentelemetry-python/pull/3966)) - Update semantic conventions to version 1.26.0. ([#3964](https://github.com/open-telemetry/opentelemetry-python/pull/3964)) - Use semconv exception attributes for record exceptions in spans ([#3979](https://github.com/open-telemetry/opentelemetry-python/pull/3979)) - Fix _encode_events assumes events.attributes.dropped exists ([#3965](https://github.com/open-telemetry/opentelemetry-python/pull/3965)) - Validate links at span creation ([#3991](https://github.com/open-telemetry/opentelemetry-python/pull/3991)) - Add attributes field in `MeterProvider.get_meter` and `InstrumentationScope` ([#4015](https://github.com/open-telemetry/opentelemetry-python/pull/4015)) - Fix inaccessible `SCHEMA_URL` constants in `opentelemetry-semantic-conventions` ([#4069](https://github.com/open-telemetry/opentelemetry-python/pull/4069)) ## Version 1.25.0/0.46b0 (2024-05-30) - Fix class BoundedAttributes to have RLock rather than Lock ([#3859](https://github.com/open-telemetry/opentelemetry-python/pull/3859)) - Remove thread lock by loading RuntimeContext explicitly. ([#3763](https://github.com/open-telemetry/opentelemetry-python/pull/3763)) - Update proto version to v1.2.0 ([#3844](https://github.com/open-telemetry/opentelemetry-python/pull/3844)) - Add to_json method to ExponentialHistogram ([#3780](https://github.com/open-telemetry/opentelemetry-python/pull/3780)) - Bump mypy to 1.9.0 ([#3795](https://github.com/open-telemetry/opentelemetry-python/pull/3795)) - Fix exponential histograms ([#3798](https://github.com/open-telemetry/opentelemetry-python/pull/3798)) - Fix otlp exporter to export log_record.observed_timestamp ([#3785](https://github.com/open-telemetry/opentelemetry-python/pull/3785)) - Add capture the fully qualified type name for raised exceptions in spans ([#3837](https://github.com/open-telemetry/opentelemetry-python/pull/3837)) - Prometheus exporter sort label keys to prevent duplicate metrics when user input changes order ([#3698](https://github.com/open-telemetry/opentelemetry-python/pull/3698)) - Update semantic conventions to version 1.25.0. Refactor semantic-convention structure: - `SpanAttributes`, `ResourceAttributes`, and `MetricInstruments` are deprecated. - Attribute and metric definitions are now grouped by the namespace. - Stable attributes and metrics are moved to `opentelemetry.semconv.attributes` and `opentelemetry.semconv.metrics` modules. - Stable and experimental attributes and metrics are defined under `opentelemetry.semconv._incubating` import path. ([#3586](https://github.com/open-telemetry/opentelemetry-python/pull/3586)) - Rename test objects to avoid pytest warnings ([#3823] (https://github.com/open-telemetry/opentelemetry-python/pull/3823)) - Add span flags to OTLP spans and links ([#3881](https://github.com/open-telemetry/opentelemetry-python/pull/3881)) - Record links with invalid SpanContext if either attributes or TraceState are not empty ([#3917](https://github.com/open-telemetry/opentelemetry-python/pull/3917/)) - Add OpenTelemetry trove classifiers to PyPI packages ([#3913] (https://github.com/open-telemetry/opentelemetry-python/pull/3913)) - Fix prometheus metric name and unit conversion ([#3924](https://github.com/open-telemetry/opentelemetry-python/pull/3924)) - this is a breaking change to prometheus metric names so they comply with the [specification](https://github.com/open-telemetry/opentelemetry-specification/blob/v1.33.0/specification/compatibility/prometheus_and_openmetrics.md#otlp-metric-points-to-prometheus). - you can temporarily opt-out of the unit normalization by setting the environment variable `OTEL_PYTHON_EXPERIMENTAL_DISABLE_PROMETHEUS_UNIT_NORMALIZATION=true` - common unit abbreviations are converted to Prometheus conventions (`s` -> `seconds`), following the [collector's implementation](https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/c0b51136575aa7ba89326d18edb4549e7e1bbdb9/pkg/translator/prometheus/normalize_name.go#L108) - repeated `_` are replaced with a single `_` - unit annotations (enclosed in curly braces like `{requests}`) are stripped away - units with slash are converted e.g. `m/s` -> `meters_per_second`. - The exporter's API is not changed - Add parameters for Distros and configurators to configure autoinstrumentation in addition to existing environment variables. ([#3864](https://github.com/open-telemetry/opentelemetry-python/pull/3864)) ## Version 1.24.0/0.45b0 (2024-03-28) - Make create_gauge non-abstract method ([#3817](https://github.com/open-telemetry/opentelemetry-python/pull/3817)) - Make `tracer.start_as_current_span()` decorator work with async functions ([#3633](https://github.com/open-telemetry/opentelemetry-python/pull/3633)) - Fix python 3.12 deprecation warning ([#3751](https://github.com/open-telemetry/opentelemetry-python/pull/3751)) - bump mypy to 0.982 ([#3776](https://github.com/open-telemetry/opentelemetry-python/pull/3776)) - Add support for OTEL_SDK_DISABLED environment variable ([#3648](https://github.com/open-telemetry/opentelemetry-python/pull/3648)) - Fix ValueError message for PeriodicExportingMetricsReader ([#3769](https://github.com/open-telemetry/opentelemetry-python/pull/3769)) - Use `BaseException` instead of `Exception` in `record_exception` ([#3354](https://github.com/open-telemetry/opentelemetry-python/pull/3354)) - Make span.record_exception more robust ([#3778](https://github.com/open-telemetry/opentelemetry-python/pull/3778)) - Fix license field in pyproject.toml files ([#3803](https://github.com/open-telemetry/opentelemetry-python/pull/3803)) ## Version 1.23.0/0.44b0 (2024-02-23) - Use Attribute rather than boundattribute in logrecord ([#3567](https://github.com/open-telemetry/opentelemetry-python/pull/3567)) - Fix flush error when no LoggerProvider configured for LoggingHandler ([#3608](https://github.com/open-telemetry/opentelemetry-python/pull/3608)) - Add `Span.add_link()` method to add link after span start ([#3618](https://github.com/open-telemetry/opentelemetry-python/pull/3618)) - Fix `OTLPMetricExporter` ignores `preferred_aggregation` property ([#3603](https://github.com/open-telemetry/opentelemetry-python/pull/3603)) - Logs: set `observed_timestamp` field ([#3565](https://github.com/open-telemetry/opentelemetry-python/pull/3565)) - Add missing Resource SchemaURL in OTLP exporters ([#3652](https://github.com/open-telemetry/opentelemetry-python/pull/3652)) - Fix loglevel warning text ([#3566](https://github.com/open-telemetry/opentelemetry-python/pull/3566)) - Prometheus Exporter string representation for target_info labels ([#3659](https://github.com/open-telemetry/opentelemetry-python/pull/3659)) - Logs: ObservedTimestamp field is missing in console exporter output ([#3564](https://github.com/open-telemetry/opentelemetry-python/pull/3564)) - Fix explicit bucket histogram aggregation ([#3429](https://github.com/open-telemetry/opentelemetry-python/pull/3429)) - Add `code.lineno`, `code.function` and `code.filepath` to all logs ([#3675](https://github.com/open-telemetry/opentelemetry-python/pull/3675)) - Add Synchronous Gauge instrument ([#3462](https://github.com/open-telemetry/opentelemetry-python/pull/3462)) - Drop support for 3.7 ([#3668](https://github.com/open-telemetry/opentelemetry-python/pull/3668)) - Include key in attribute sequence warning ([#3639](https://github.com/open-telemetry/opentelemetry-python/pull/3639)) - Upgrade markupsafe, Flask and related dependencies to dev and test environments ([#3609](https://github.com/open-telemetry/opentelemetry-python/pull/3609)) - Handle HTTP 2XX responses as successful in OTLP exporters ([#3623](https://github.com/open-telemetry/opentelemetry-python/pull/3623)) - Improve Resource Detector timeout messaging ([#3645](https://github.com/open-telemetry/opentelemetry-python/pull/3645)) - Add Proxy classes for logging ([#3575](https://github.com/open-telemetry/opentelemetry-python/pull/3575)) - Remove dependency on 'backoff' library ([#3679](https://github.com/open-telemetry/opentelemetry-python/pull/3679)) ## Version 1.22.0/0.43b0 (2023-12-15) - Prometheus exporter sanitize info metric ([#3572](https://github.com/open-telemetry/opentelemetry-python/pull/3572)) - Remove Jaeger exporters ([#3554](https://github.com/open-telemetry/opentelemetry-python/pull/3554)) - Log stacktrace on `UNKNOWN` status OTLP export error ([#3536](https://github.com/open-telemetry/opentelemetry-python/pull/3536)) - Fix OTLPExporterMixin shutdown timeout period ([#3524](https://github.com/open-telemetry/opentelemetry-python/pull/3524)) - Handle `taskName` `logrecord` attribute ([#3557](https://github.com/open-telemetry/opentelemetry-python/pull/3557)) ## Version 1.21.0/0.42b0 (2023-11-01) - Fix `SumAggregation`  ([#3390](https://github.com/open-telemetry/opentelemetry-python/pull/3390)) - Fix handling of empty metric collection cycles ([#3335](https://github.com/open-telemetry/opentelemetry-python/pull/3335)) - Fix error when no LoggerProvider configured for LoggingHandler ([#3423](https://github.com/open-telemetry/opentelemetry-python/pull/3423)) - Make `opentelemetry_metrics_exporter` entrypoint support pull exporters ([#3428](https://github.com/open-telemetry/opentelemetry-python/pull/3428)) - Allow instrument names to have '/' and up to 255 characters ([#3442](https://github.com/open-telemetry/opentelemetry-python/pull/3442)) - Do not load Resource on sdk import ([#3447](https://github.com/open-telemetry/opentelemetry-python/pull/3447)) - Update semantic conventions to version 1.21.0 ([#3251](https://github.com/open-telemetry/opentelemetry-python/pull/3251)) - Add missing schema_url in global api for logging and metrics ([#3251](https://github.com/open-telemetry/opentelemetry-python/pull/3251)) - Prometheus exporter support for auto instrumentation ([#3413](https://github.com/open-telemetry/opentelemetry-python/pull/3413)) - Implement Process Resource detector ([#3472](https://github.com/open-telemetry/opentelemetry-python/pull/3472)) ## Version 1.20.0/0.41b0 (2023-09-04) - Modify Prometheus exporter to translate non-monotonic Sums into Gauges ([#3306](https://github.com/open-telemetry/opentelemetry-python/pull/3306)) ## Version 1.19.0/0.40b0 (2023-07-13) - Drop `setuptools` runtime requirement. ([#3372](https://github.com/open-telemetry/opentelemetry-python/pull/3372)) - Update the body type in the log ([$3343](https://github.com/open-telemetry/opentelemetry-python/pull/3343)) - Add max_scale option to Exponential Bucket Histogram Aggregation ([#3323](https://github.com/open-telemetry/opentelemetry-python/pull/3323)) - Use BoundedAttributes instead of raw dict to extract attributes from LogRecord ([#3310](https://github.com/open-telemetry/opentelemetry-python/pull/3310)) - Support dropped_attributes_count in LogRecord and exporters ([#3351](https://github.com/open-telemetry/opentelemetry-python/pull/3351)) - Add unit to view instrument selection criteria ([#3341](https://github.com/open-telemetry/opentelemetry-python/pull/3341)) - Upgrade opentelemetry-proto to 0.20 and regen [#3355](https://github.com/open-telemetry/opentelemetry-python/pull/3355)) - Include endpoint in Grpc transient error warning [#3362](https://github.com/open-telemetry/opentelemetry-python/pull/3362)) - Fixed bug where logging export is tracked as trace [#3375](https://github.com/open-telemetry/opentelemetry-python/pull/3375)) - Default LogRecord observed_timestamp to current timestamp [#3377](https://github.com/open-telemetry/opentelemetry-python/pull/3377)) ## Version 1.18.0/0.39b0 (2023-05-19) - Select histogram aggregation with an environment variable ([#3265](https://github.com/open-telemetry/opentelemetry-python/pull/3265)) - Move Protobuf encoding to its own package ([#3169](https://github.com/open-telemetry/opentelemetry-python/pull/3169)) - Add experimental feature to detect resource detectors in auto instrumentation ([#3181](https://github.com/open-telemetry/opentelemetry-python/pull/3181)) - Fix exporting of ExponentialBucketHistogramAggregation from opentelemetry.sdk.metrics.view ([#3240](https://github.com/open-telemetry/opentelemetry-python/pull/3240)) - Fix headers types mismatch for OTLP Exporters ([#3226](https://github.com/open-telemetry/opentelemetry-python/pull/3226)) - Fix suppress instrumentation for log batch processor ([#3223](https://github.com/open-telemetry/opentelemetry-python/pull/3223)) - Add speced out environment variables and arguments for BatchLogRecordProcessor ([#3237](https://github.com/open-telemetry/opentelemetry-python/pull/3237)) - Add benchmark tests for metrics ([#3267](https://github.com/open-telemetry/opentelemetry-python/pull/3267)) ## Version 1.17.0/0.38b0 (2023-03-22) - Implement LowMemory temporality ([#3223](https://github.com/open-telemetry/opentelemetry-python/pull/3223)) - PeriodicExportingMetricReader will continue if collection times out ([#3100](https://github.com/open-telemetry/opentelemetry-python/pull/3100)) - Fix formatting of ConsoleMetricExporter. ([#3197](https://github.com/open-telemetry/opentelemetry-python/pull/3197)) - Fix use of built-in samplers in SDK configuration ([#3176](https://github.com/open-telemetry/opentelemetry-python/pull/3176)) - Implement shutdown procedure forOTLP grpc exporters ([#3138](https://github.com/open-telemetry/opentelemetry-python/pull/3138)) - Add exponential histogram ([#2964](https://github.com/open-telemetry/opentelemetry-python/pull/2964)) - Add OpenCensus trace bridge/shim ([#3210](https://github.com/open-telemetry/opentelemetry-python/pull/3210)) ## Version 1.16.0/0.37b0 (2023-02-17) - Change ``__all__`` to be statically defined. ([#3143](https://github.com/open-telemetry/opentelemetry-python/pull/3143)) - Remove the ability to set a global metric prefix for Prometheus exporter ([#3137](https://github.com/open-telemetry/opentelemetry-python/pull/3137)) - Adds environment variables for log exporter ([#3037](https://github.com/open-telemetry/opentelemetry-python/pull/3037)) - Add attribute name to type warning message. ([3124](https://github.com/open-telemetry/opentelemetry-python/pull/3124)) - Add db metric name to semantic conventions ([#3115](https://github.com/open-telemetry/opentelemetry-python/pull/3115)) - Fix User-Agent header value for OTLP exporters to conform to RFC7231 & RFC7230 ([#3128](https://github.com/open-telemetry/opentelemetry-python/pull/3128)) - Fix validation of baggage values ([#3058](https://github.com/open-telemetry/opentelemetry-python/pull/3058)) - Fix capitalization of baggage keys ([#3151](https://github.com/open-telemetry/opentelemetry-python/pull/3151)) - Bump min required api version for OTLP exporters ([#3156](https://github.com/open-telemetry/opentelemetry-python/pull/3156)) - deprecate jaeger exporters ([#3158](https://github.com/open-telemetry/opentelemetry-python/pull/3158)) - Create a single resource instance ([#3118](https://github.com/open-telemetry/opentelemetry-python/pull/3118)) ## Version 1.15.0/0.36b0 (2022-12-09) - PeriodicExportingMetricsReader with +Inf interval to support explicit metric collection ([#3059](https://github.com/open-telemetry/opentelemetry-python/pull/3059)) - Regenerate opentelemetry-proto to be compatible with protobuf 3 and 4 ([#3070](https://github.com/open-telemetry/opentelemetry-python/pull/3070)) - Rename parse_headers to parse_env_headers and improve error message ([#2376](https://github.com/open-telemetry/opentelemetry-python/pull/2376)) - Add url decode values from OTEL_RESOURCE_ATTRIBUTES ([#3046](https://github.com/open-telemetry/opentelemetry-python/pull/3046)) - Fixed circular dependency issue with custom samplers ([#3026](https://github.com/open-telemetry/opentelemetry-python/pull/3026)) - Add missing entry points for OTLP/HTTP exporter ([#3027](https://github.com/open-telemetry/opentelemetry-python/pull/3027)) - Update logging to include logging api as per specification ([#3038](https://github.com/open-telemetry/opentelemetry-python/pull/3038)) - Fix: Avoid generator in metrics _ViewInstrumentMatch.collect() ([#3035](https://github.com/open-telemetry/opentelemetry-python/pull/3035) - [exporter-otlp-proto-grpc] add user agent string ([#3009](https://github.com/open-telemetry/opentelemetry-python/pull/3009)) ## Version 1.14.0/0.35b0 (2022-11-04) - Add logarithm and exponent mappings ([#2960](https://github.com/open-telemetry/opentelemetry-python/pull/2960)) - Add and use missing metrics environment variables ([#2968](https://github.com/open-telemetry/opentelemetry-python/pull/2968)) - Enabled custom samplers via entry points ([#2972](https://github.com/open-telemetry/opentelemetry-python/pull/2972)) - Update log symbol names ([#2943](https://github.com/open-telemetry/opentelemetry-python/pull/2943)) - Update explicit histogram bucket boundaries ([#2947](https://github.com/open-telemetry/opentelemetry-python/pull/2947)) - `exporter-otlp-proto-http`: add user agent string ([#2959](https://github.com/open-telemetry/opentelemetry-python/pull/2959)) - Add http-metric instrument names to semantic conventions ([#2976](https://github.com/open-telemetry/opentelemetry-python/pull/2976)) - [exporter/opentelemetry-exporter-otlp-proto-http] Add OTLPMetricExporter ([#2891](https://github.com/open-telemetry/opentelemetry-python/pull/2891)) - Add support for py3.11 ([#2997](https://github.com/open-telemetry/opentelemetry-python/pull/2997)) - Fix a bug with exporter retries for with newer versions of the backoff library ([#2980](https://github.com/open-telemetry/opentelemetry-python/pull/2980)) ## Version 1.13.0/0.34b0 (2022-09-26) - Add a configurable max_export_batch_size to the gRPC metrics exporter ([#2809](https://github.com/open-telemetry/opentelemetry-python/pull/2809)) - Remove support for 3.6 ([#2763](https://github.com/open-telemetry/opentelemetry-python/pull/2763)) - Update PeriodicExportingMetricReader to never call export() concurrently ([#2873](https://github.com/open-telemetry/opentelemetry-python/pull/2873)) - Add param for `indent` size to `LogRecord.to_json()` ([#2870](https://github.com/open-telemetry/opentelemetry-python/pull/2870)) - Fix: Remove `LogEmitter.flush()` to align with OTel Log spec ([#2863](https://github.com/open-telemetry/opentelemetry-python/pull/2863)) - Bump minimum required API/SDK version for exporters that support metrics ([#2918](https://github.com/open-telemetry/opentelemetry-python/pull/2918)) - Fix metric reader examples + added `preferred_temporality` and `preferred_aggregation` for `ConsoleMetricExporter` ([#2911](https://github.com/open-telemetry/opentelemetry-python/pull/2911)) - Add support for setting OTLP export protocol with env vars, as defined in the [specifications](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/protocol/exporter.md#specify-protocol) ([#2893](https://github.com/open-telemetry/opentelemetry-python/pull/2893)) - Add force_flush to span exporters ([#2919](https://github.com/open-telemetry/opentelemetry-python/pull/2919)) ## Version 1.12.0/0.33b0 (2022-08-08) - Add `force_flush` method to metrics exporter ([#2852](https://github.com/open-telemetry/opentelemetry-python/pull/2852)) - Change tracing to use `Resource.to_json()` ([#2784](https://github.com/open-telemetry/opentelemetry-python/pull/2784)) - Fix get_log_emitter instrumenting_module_version args typo ([#2830](https://github.com/open-telemetry/opentelemetry-python/pull/2830)) - Fix OTLP gRPC exporter warning message ([#2781](https://github.com/open-telemetry/opentelemetry-python/pull/2781)) - Fix tracing decorator with late configuration ([#2754](https://github.com/open-telemetry/opentelemetry-python/pull/2754)) - Fix --insecure of CLI argument ([#2696](https://github.com/open-telemetry/opentelemetry-python/pull/2696)) - Add temporality and aggregation configuration for metrics exporters, use `OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE` only for OTLP metrics exporter ([#2843](https://github.com/open-telemetry/opentelemetry-python/pull/2843)) - Instrument instances are always created through a Meter ([#2844](https://github.com/open-telemetry/opentelemetry-python/pull/2844)) ## Version 1.12.0rc2/0.32b0 (2022-07-04) - Fix instrument name and unit regexes ([#2796](https://github.com/open-telemetry/opentelemetry-python/pull/2796)) - Add optional sessions parameter to all Exporters leveraging requests.Session ([#2783](https://github.com/open-telemetry/opentelemetry-python/pull/2783) - Add min/max fields to Histogram ([#2759](https://github.com/open-telemetry/opentelemetry-python/pull/2759)) - `opentelemetry-exporter-otlp-proto-http` Add support for OTLP/HTTP log exporter ([#2462](https://github.com/open-telemetry/opentelemetry-python/pull/2462)) - Fix yield of `None`-valued points ([#2745](https://github.com/open-telemetry/opentelemetry-python/pull/2745)) - Add missing `to_json` methods ([#2722](https://github.com/open-telemetry/opentelemetry-python/pull/2722) - Fix type hints for textmap `Getter` and `Setter` ([#2657](https://github.com/open-telemetry/opentelemetry-python/pull/2657)) - Fix LogEmitterProvider.force_flush hanging randomly ([#2714](https://github.com/open-telemetry/opentelemetry-python/pull/2714)) - narrow protobuf dependencies to exclude protobuf >= 4 ([#2720](https://github.com/open-telemetry/opentelemetry-python/pull/2720)) - Specify worker thread names ([#2724](https://github.com/open-telemetry/opentelemetry-python/pull/2724)) - Loosen dependency on `backoff` for newer Python versions ([#2726](https://github.com/open-telemetry/opentelemetry-python/pull/2726)) - fix: frozenset object has no attribute items ([#2727](https://github.com/open-telemetry/opentelemetry-python/pull/2727)) - fix: create suppress HTTP instrumentation key in opentelemetry context ([#2729](https://github.com/open-telemetry/opentelemetry-python/pull/2729)) - Support logs SDK auto instrumentation enable/disable with env ([#2728](https://github.com/open-telemetry/opentelemetry-python/pull/2728)) - fix: update entry point object references for metrics ([#2731](https://github.com/open-telemetry/opentelemetry-python/pull/2731)) - Allow set_status to accept the StatusCode and optional description ([#2735](https://github.com/open-telemetry/opentelemetry-python/pull/2735)) - Configure auto instrumentation to support metrics ([#2705](https://github.com/open-telemetry/opentelemetry-python/pull/2705)) - Add entrypoint for metrics exporter ([#2748](https://github.com/open-telemetry/opentelemetry-python/pull/2748)) - Fix Jaeger propagator usage with NonRecordingSpan ([#2762](https://github.com/open-telemetry/opentelemetry-python/pull/2762)) - Add `opentelemetry.propagate` module and `opentelemetry.propagators` package to the API reference documentation ([#2785](https://github.com/open-telemetry/opentelemetry-python/pull/2785)) ## Version 1.12.0rc1/0.31b0 (2022-05-17) - Fix LoggingHandler to handle LogRecord with exc_info=False ([#2690](https://github.com/open-telemetry/opentelemetry-python/pull/2690)) - Make metrics components public ([#2684](https://github.com/open-telemetry/opentelemetry-python/pull/2684)) - Update to semantic conventions v1.11.0 ([#2669](https://github.com/open-telemetry/opentelemetry-python/pull/2669)) - Update opentelemetry-proto to v0.17.0 ([#2668](https://github.com/open-telemetry/opentelemetry-python/pull/2668)) - Add CallbackOptions to observable instrument callback params ([#2664](https://github.com/open-telemetry/opentelemetry-python/pull/2664)) - Add timeouts to metric SDK ([#2653](https://github.com/open-telemetry/opentelemetry-python/pull/2653)) - Add variadic arguments to metric exporter/reader interfaces ([#2654](https://github.com/open-telemetry/opentelemetry-python/pull/2654)) - Added a `opentelemetry.sdk.resources.ProcessResourceDetector` that adds the 'process.runtime.{name,version,description}' resource attributes when used with the `opentelemetry.sdk.resources.get_aggregated_resources` API ([#2660](https://github.com/open-telemetry/opentelemetry-python/pull/2660)) - Move Metrics API behind internal package ([#2651](https://github.com/open-telemetry/opentelemetry-python/pull/2651)) ## Version 1.11.1/0.30b1 (2022-04-21) - Add parameter to MetricReader constructor to select aggregation per instrument kind ([#2638](https://github.com/open-telemetry/opentelemetry-python/pull/2638)) - Add parameter to MetricReader constructor to select temporality per instrument kind ([#2637](https://github.com/open-telemetry/opentelemetry-python/pull/2637)) - Fix unhandled callback exceptions on async instruments ([#2614](https://github.com/open-telemetry/opentelemetry-python/pull/2614)) - Rename `DefaultCounter`, `DefaultHistogram`, `DefaultObservableCounter`, `DefaultObservableGauge`, `DefaultObservableUpDownCounter`, `DefaultUpDownCounter` instruments to `NoOpCounter`, `NoOpHistogram`, `NoOpObservableCounter`, `NoOpObservableGauge`, `NoOpObservableUpDownCounter`, `NoOpUpDownCounter` ([#2616](https://github.com/open-telemetry/opentelemetry-python/pull/2616)) - Deprecate InstrumentationLibraryInfo and Add InstrumentationScope ([#2583](https://github.com/open-telemetry/opentelemetry-python/pull/2583)) ## Version 1.11.0/0.30b0 (2022-04-18) - Rename API Measurement for async instruments to Observation ([#2617](https://github.com/open-telemetry/opentelemetry-python/pull/2617)) - Add support for zero or more callbacks ([#2602](https://github.com/open-telemetry/opentelemetry-python/pull/2602)) - Fix parsing of trace flags when extracting traceparent ([#2577](https://github.com/open-telemetry/opentelemetry-python/pull/2577)) - Add default aggregation ([#2543](https://github.com/open-telemetry/opentelemetry-python/pull/2543)) - Fix incorrect installation of some exporter “convenienceâ€� packages into “site-packages/srcâ€� ([#2525](https://github.com/open-telemetry/opentelemetry-python/pull/2525)) - Capture exception information as part of log attributes ([#2531](https://github.com/open-telemetry/opentelemetry-python/pull/2531)) - Change OTLPHandler to LoggingHandler ([#2528](https://github.com/open-telemetry/opentelemetry-python/pull/2528)) - Fix delta histogram sum not being reset on collection ([#2533](https://github.com/open-telemetry/opentelemetry-python/pull/2533)) - Add InMemoryMetricReader to metrics SDK ([#2540](https://github.com/open-telemetry/opentelemetry-python/pull/2540)) - Drop the usage of name field from log model in OTLP ([#2565](https://github.com/open-telemetry/opentelemetry-python/pull/2565)) - Update opentelemetry-proto to v0.15.0 ([#2566](https://github.com/open-telemetry/opentelemetry-python/pull/2566)) - Remove `enable_default_view` option from sdk MeterProvider ([#2547](https://github.com/open-telemetry/opentelemetry-python/pull/2547)) - Update otlp-proto-grpc and otlp-proto-http exporters to have more lax requirements for `backoff` lib ([#2575](https://github.com/open-telemetry/opentelemetry-python/pull/2575)) - Add min/max to histogram point ([#2581](https://github.com/open-telemetry/opentelemetry-python/pull/2581)) - Update opentelemetry-proto to v0.16.0 ([#2619](https://github.com/open-telemetry/opentelemetry-python/pull/2619)) ## Version 1.10.0/0.29b0 (2022-03-10) - Docs rework: [non-API docs are moving](https://github.com/open-telemetry/opentelemetry-python/issues/2172) to [opentelemetry.io](https://opentelemetry.io). For details, including a list of pages that have moved, see [#2453](https://github.com/open-telemetry/opentelemetry-python/pull/2453), and [#2498](https://github.com/open-telemetry/opentelemetry-python/pull/2498). - `opentelemetry-exporter-otlp-proto-grpc` update SDK dependency to ~1.9. ([#2442](https://github.com/open-telemetry/opentelemetry-python/pull/2442)) - bugfix(auto-instrumentation): attach OTLPHandler to root logger ([#2450](https://github.com/open-telemetry/opentelemetry-python/pull/2450)) - Bump semantic conventions from 1.6.1 to 1.8.0 ([#2461](https://github.com/open-telemetry/opentelemetry-python/pull/2461)) - fix exception handling in get_aggregated_resources ([#2464](https://github.com/open-telemetry/opentelemetry-python/pull/2464)) - Fix `OTEL_EXPORTER_OTLP_ENDPOINT` usage in OTLP HTTP trace exporter ([#2493](https://github.com/open-telemetry/opentelemetry-python/pull/2493)) - [exporter/opentelemetry-exporter-prometheus] restore package using the new metrics API ([#2321](https://github.com/open-telemetry/opentelemetry-python/pull/2321)) ## Version 1.9.1/0.28b1 (2022-01-29) - Update opentelemetry-proto to v0.12.0. Note that this update removes deprecated status codes. ([#2415](https://github.com/open-telemetry/opentelemetry-python/pull/2415)) ## Version 1.9.0/0.28b0 (2022-01-26) - Fix SpanLimits global span limit defaulting when set to 0 ([#2398](https://github.com/open-telemetry/opentelemetry-python/pull/2398)) - Add Python version support policy ([#2397](https://github.com/open-telemetry/opentelemetry-python/pull/2397)) - Decode URL-encoded headers in environment variables ([#2312](https://github.com/open-telemetry/opentelemetry-python/pull/2312)) - [exporter/opentelemetry-exporter-otlp-proto-grpc] Add OTLPMetricExporter ([#2323](https://github.com/open-telemetry/opentelemetry-python/pull/2323)) - Complete metric exporter format and update OTLP exporter ([#2364](https://github.com/open-telemetry/opentelemetry-python/pull/2364)) - [api] Add `NoOpTracer` and `NoOpTracerProvider`. Marking `_DefaultTracer` and `_DefaultTracerProvider` as deprecated. ([#2363](https://github.com/open-telemetry/opentelemetry-python/pull/2363)) - [exporter/opentelemetry-exporter-otlp-proto-grpc] Add Sum to OTLPMetricExporter ([#2370](https://github.com/open-telemetry/opentelemetry-python/pull/2370)) - [api] Rename `_DefaultMeter` and `_DefaultMeterProvider` to `NoOpMeter` and `NoOpMeterProvider`. ([#2383](https://github.com/open-telemetry/opentelemetry-python/pull/2383)) - [exporter/opentelemetry-exporter-otlp-proto-grpc] Add Gauge to OTLPMetricExporter ([#2408](https://github.com/open-telemetry/opentelemetry-python/pull/2408)) - [logs] prevent None from causing problems ([#2410](https://github.com/open-telemetry/opentelemetry-python/pull/2410)) ## Version 1.8.0/0.27b0 (2021-12-17) - Adds Aggregation and instruments as part of Metrics SDK ([#2234](https://github.com/open-telemetry/opentelemetry-python/pull/2234)) - Update visibility of OTEL_METRICS_EXPORTER environment variable ([#2303](https://github.com/open-telemetry/opentelemetry-python/pull/2303)) - Adding entrypoints for log emitter provider and console, otlp log exporters ([#2253](https://github.com/open-telemetry/opentelemetry-python/pull/2253)) - Rename ConsoleExporter to ConsoleLogExporter ([#2307](https://github.com/open-telemetry/opentelemetry-python/pull/2307)) - Adding OTEL_LOGS_EXPORTER environment variable ([#2320](https://github.com/open-telemetry/opentelemetry-python/pull/2320)) - Add `setuptools` to `install_requires` ([#2334](https://github.com/open-telemetry/opentelemetry-python/pull/2334)) - Add otlp entrypoint for log exporter ([#2322](https://github.com/open-telemetry/opentelemetry-python/pull/2322)) - Support insecure configuration for OTLP gRPC exporter ([#2350](https://github.com/open-telemetry/opentelemetry-python/pull/2350)) ## Version 1.7.1/0.26b1 (2021-11-11) - Add support for Python 3.10 ([#2207](https://github.com/open-telemetry/opentelemetry-python/pull/2207)) - remove `X-B3-ParentSpanId` for B3 propagator as per OpenTelemetry specification ([#2237](https://github.com/open-telemetry/opentelemetry-python/pull/2237)) - Populate `auto.version` in Resource if using auto-instrumentation ([#2243](https://github.com/open-telemetry/opentelemetry-python/pull/2243)) - Return proxy instruments from ProxyMeter ([#2169](https://github.com/open-telemetry/opentelemetry-python/pull/2169)) - Make Measurement a concrete class ([#2153](https://github.com/open-telemetry/opentelemetry-python/pull/2153)) - Add metrics API ([#1887](https://github.com/open-telemetry/opentelemetry-python/pull/1887)) - Make batch processor fork aware and reinit when needed ([#2242](https://github.com/open-telemetry/opentelemetry-python/pull/2242)) - `opentelemetry-sdk` Sanitize env var resource attribute pairs ([#2256](https://github.com/open-telemetry/opentelemetry-python/pull/2256)) - `opentelemetry-test` start releasing to pypi.org ([#2269](https://github.com/open-telemetry/opentelemetry-python/pull/2269)) ## Version 1.6.2/0.25b2 (2021-10-19) - Fix parental trace relationship for opentracing `follows_from` reference ([#2180](https://github.com/open-telemetry/opentelemetry-python/pull/2180)) ## Version 1.6.1/0.25b1 (2021-10-18) - Fix ReadableSpan property types attempting to create a mapping from a list ([#2215](https://github.com/open-telemetry/opentelemetry-python/pull/2215)) - Upgrade GRPC/protobuf related dependency and regenerate otlp protobufs ([#2201](https://github.com/open-telemetry/opentelemetry-python/pull/2201)) - Propagation: only warn about oversized baggage headers when headers exist ([#2212](https://github.com/open-telemetry/opentelemetry-python/pull/2212)) ## Version 1.6.0/0.25b0 (2021-10-13) - Fix race in `set_tracer_provider()` ([#2182](https://github.com/open-telemetry/opentelemetry-python/pull/2182)) - Automatically load OTEL environment variables as options for `opentelemetry-instrument` ([#1969](https://github.com/open-telemetry/opentelemetry-python/pull/1969)) - `opentelemetry-semantic-conventions` Update to semantic conventions v1.6.1 ([#2077](https://github.com/open-telemetry/opentelemetry-python/pull/2077)) - Do not count invalid attributes for dropped ([#2096](https://github.com/open-telemetry/opentelemetry-python/pull/2096)) - Fix propagation bug caused by counting skipped entries ([#2071](https://github.com/open-telemetry/opentelemetry-python/pull/2071)) - Add entry point for exporters with default protocol ([#2093](https://github.com/open-telemetry/opentelemetry-python/pull/2093)) - Renamed entrypoints `otlp_proto_http_span`, `otlp_proto_grpc_span`, `console_span` to remove redundant `_span` suffix. ([#2093](https://github.com/open-telemetry/opentelemetry-python/pull/2093)) - Do not skip sequence attribute on decode error ([#2097](https://github.com/open-telemetry/opentelemetry-python/pull/2097)) - `opentelemetry-test`: Add `HttpTestBase` to allow tests with actual TCP sockets ([#2101](https://github.com/open-telemetry/opentelemetry-python/pull/2101)) - Fix incorrect headers parsing via environment variables ([#2103](https://github.com/open-telemetry/opentelemetry-python/pull/2103)) - Add support for OTEL_ATTRIBUTE_COUNT_LIMIT ([#2139](https://github.com/open-telemetry/opentelemetry-python/pull/2139)) - Attribute limits no longer apply to Resource attributes ([#2138](https://github.com/open-telemetry/opentelemetry-python/pull/2138)) - `opentelemetry-exporter-otlp`: Add `opentelemetry-otlp-proto-http` as dependency ([#2147](https://github.com/open-telemetry/opentelemetry-python/pull/2147)) - Fix validity calculation for trace and span IDs ([#2145](https://github.com/open-telemetry/opentelemetry-python/pull/2145)) - Add `schema_url` to `TracerProvider.get_tracer` ([#2154](https://github.com/open-telemetry/opentelemetry-python/pull/2154)) - Make baggage implementation w3c spec complaint ([#2167](https://github.com/open-telemetry/opentelemetry-python/pull/2167)) - Add name to `BatchSpanProcessor` worker thread ([#2186](https://github.com/open-telemetry/opentelemetry-python/pull/2186)) ## Version 1.5.0/0.24b0 (2021-08-26) - Add pre and post instrumentation entry points ([#1983](https://github.com/open-telemetry/opentelemetry-python/pull/1983)) - Fix documentation on well known exporters and variable OTEL_TRACES_EXPORTER which were misnamed ([#2023](https://github.com/open-telemetry/opentelemetry-python/pull/2023)) - `opentelemetry-sdk` `get_aggregated_resource()` returns default resource and service name whenever called ([#2013](https://github.com/open-telemetry/opentelemetry-python/pull/2013)) - `opentelemetry-distro` & `opentelemetry-sdk` Moved Auto Instrumentation Configurator code to SDK to let distros use its default implementation ([#1937](https://github.com/open-telemetry/opentelemetry-python/pull/1937)) - Add Trace ID validation to meet [TraceID spec](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/overview.md#spancontext) ([#1992](https://github.com/open-telemetry/opentelemetry-python/pull/1992)) - Fixed Python 3.10 incompatibility in `opentelemetry-opentracing-shim` tests ([#2018](https://github.com/open-telemetry/opentelemetry-python/pull/2018)) - `opentelemetry-sdk` added support for `OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT` ([#2044](https://github.com/open-telemetry/opentelemetry-python/pull/2044)) - `opentelemetry-sdk` Fixed bugs (#2041, #2042 & #2045) in Span Limits ([#2044](https://github.com/open-telemetry/opentelemetry-python/pull/2044)) - `opentelemetry-sdk` Add support for `OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT` env var ([#2056](https://github.com/open-telemetry/opentelemetry-python/pull/2056)) - `opentelemetry-sdk` Treat limit even vars set to empty values as unset/unlimited. ([#2054](https://github.com/open-telemetry/opentelemetry-python/pull/2054)) - `opentelemetry-api` Attribute keys must be non-empty strings. ([#2057](https://github.com/open-telemetry/opentelemetry-python/pull/2057)) ## Version 0.23.1 (2021-07-26) ### Changed - Fix opentelemetry-bootstrap dependency script. ([#1987](https://github.com/open-telemetry/opentelemetry-python/pull/1987)) ## Version 1.4.0/0.23b0 (2021-07-21) ### Added - Moved `opentelemetry-instrumentation` to core repository. ([#1959](https://github.com/open-telemetry/opentelemetry-python/pull/1959)) - Add support for OTLP Exporter Protobuf over HTTP ([#1868](https://github.com/open-telemetry/opentelemetry-python/pull/1868)) - Dropped attributes/events/links count available exposed on ReadableSpans. ([#1893](https://github.com/open-telemetry/opentelemetry-python/pull/1893)) - Added dropped count to otlp, jaeger and zipkin exporters. ([#1893](https://github.com/open-telemetry/opentelemetry-python/pull/1893)) ### Added - Give OTLPHandler the ability to process attributes ([#1952](https://github.com/open-telemetry/opentelemetry-python/pull/1952)) - Add global LogEmitterProvider and convenience function get_log_emitter ([#1901](https://github.com/open-telemetry/opentelemetry-python/pull/1901)) - Add OTLPHandler for standard library logging module ([#1903](https://github.com/open-telemetry/opentelemetry-python/pull/1903)) ### Changed - Updated `opentelemetry-opencensus-exporter` to use `service_name` of spans instead of resource ([#1897](https://github.com/open-telemetry/opentelemetry-python/pull/1897)) - Added descriptions to the env variables mentioned in the opentelemetry-specification ([#1898](https://github.com/open-telemetry/opentelemetry-python/pull/1898)) - Ignore calls to `Span.set_status` with `StatusCode.UNSET` and also if previous status already had `StatusCode.OK`. ([#1902](https://github.com/open-telemetry/opentelemetry-python/pull/1902)) - Attributes for `Link` and `Resource` are immutable as they are for `Event`, which means any attempt to modify attributes directly will result in a `TypeError` exception. ([#1909](https://github.com/open-telemetry/opentelemetry-python/pull/1909)) - Added `BoundedAttributes` to the API to make it available for `Link` which is defined in the API. Marked `BoundedDict` in the SDK as deprecated as a result. ([#1915](https://github.com/open-telemetry/opentelemetry-python/pull/1915)) - Fix OTLP SpanExporter to distinguish spans based off Resource and InstrumentationInfo ([#1927](https://github.com/open-telemetry/opentelemetry-python/pull/1927)) - Updating dependency for opentelemetry api/sdk packages to support major version instead of pinning to specific versions. ([#1933](https://github.com/open-telemetry/opentelemetry-python/pull/1933)) - `opentelemetry-semantic-conventions` Generate semconv constants update for OTel Spec 1.5.0 ([#1946](https://github.com/open-telemetry/opentelemetry-python/pull/1946)) ### Fixed - Updated `opentelementry-opentracing-shim` `ScopeShim` to report exceptions in opentelemetry specification format, rather than opentracing spec format. ([#1878](https://github.com/open-telemetry/opentelemetry-python/pull/1878)) ## Version 1.3.0/0.22b0 (2021-06-01) ### Added - Allow span limits to be set programmatically via TracerProvider. ([#1877](https://github.com/open-telemetry/opentelemetry-python/pull/1877)) - Added support for CreateKey functionality. ([#1853](https://github.com/open-telemetry/opentelemetry-python/pull/1853)) ### Changed - Updated get_tracer to return an empty string when passed an invalid name ([#1854](https://github.com/open-telemetry/opentelemetry-python/pull/1854)) - Changed AttributeValue sequences to warn mypy users on adding None values to array ([#1855](https://github.com/open-telemetry/opentelemetry-python/pull/1855)) - Fixed exporter OTLP header parsing to match baggage header formatting. ([#1869](https://github.com/open-telemetry/opentelemetry-python/pull/1869)) - Added optional `schema_url` field to `Resource` class ([#1871](https://github.com/open-telemetry/opentelemetry-python/pull/1871)) - Update protos to latest version release 0.9.0 ([#1873](https://github.com/open-telemetry/opentelemetry-python/pull/1873)) ## Version 1.2.0/0.21b0 (2021-05-11) ### Added - Added example for running Django with auto instrumentation. ([#1803](https://github.com/open-telemetry/opentelemetry-python/pull/1803)) - Added `B3SingleFormat` and `B3MultiFormat` propagators to the `opentelemetry-propagator-b3` package. ([#1823](https://github.com/open-telemetry/opentelemetry-python/pull/1823)) - Added support for OTEL_SERVICE_NAME. ([#1829](https://github.com/open-telemetry/opentelemetry-python/pull/1829)) - Lazily read/configure limits and allow limits to be unset. ([#1839](https://github.com/open-telemetry/opentelemetry-python/pull/1839)) - Added support for OTEL_EXPORTER_JAEGER_TIMEOUT ([#1863](https://github.com/open-telemetry/opentelemetry-python/pull/1863)) ### Changed - Fixed OTLP gRPC exporter silently failing if scheme is not specified in endpoint. ([#1806](https://github.com/open-telemetry/opentelemetry-python/pull/1806)) - Rename CompositeHTTPPropagator to CompositePropagator as per specification. ([#1807](https://github.com/open-telemetry/opentelemetry-python/pull/1807)) - Propagators use the root context as default for `extract` and do not modify the context if extracting from carrier does not work. ([#1811](https://github.com/open-telemetry/opentelemetry-python/pull/1811)) - Fixed `b3` propagator entrypoint to point to `B3SingleFormat` propagator. ([#1823](https://github.com/open-telemetry/opentelemetry-python/pull/1823)) - Added `b3multi` propagator entrypoint to point to `B3MultiFormat` propagator. ([#1823](https://github.com/open-telemetry/opentelemetry-python/pull/1823)) - Improve warning when failing to decode byte attribute ([#1810](https://github.com/open-telemetry/opentelemetry-python/pull/1810)) - Fixed inconsistency in parent_id formatting from the ConsoleSpanExporter ([#1833](https://github.com/open-telemetry/opentelemetry-python/pull/1833)) - Include span parent in Jaeger gRPC export as `CHILD_OF` reference ([#1809])(https://github.com/open-telemetry/opentelemetry-python/pull/1809) - Fixed sequence values in OTLP exporter not translating ([#1818](https://github.com/open-telemetry/opentelemetry-python/pull/1818)) - Update transient errors retry timeout and retryable status codes ([#1842](https://github.com/open-telemetry/opentelemetry-python/pull/1842)) - Apply validation of attributes to `Resource`, move attribute related logic to separate package. ([#1834](https://github.com/open-telemetry/opentelemetry-python/pull/1834)) - Fix start span behavior when excess links and attributes are included ([#1856](https://github.com/open-telemetry/opentelemetry-python/pull/1856)) ### Removed - Moved `opentelemetry-instrumentation` to contrib repository. ([#1797](https://github.com/open-telemetry/opentelemetry-python/pull/1797)) ## Version 1.1.0 (2021-04-20) ### Added - Added `py.typed` file to every package. This should resolve a bunch of mypy errors for users. ([#1720](https://github.com/open-telemetry/opentelemetry-python/pull/1720)) - Add auto generated trace and resource attributes semantic conventions ([#1759](https://github.com/open-telemetry/opentelemetry-python/pull/1759)) - Added `SpanKind` to `should_sample` parameters, suggest using parent span context's tracestate instead of manually passed in tracestate in `should_sample` ([#1764](https://github.com/open-telemetry/opentelemetry-python/pull/1764)) - Added experimental HTTP back propagators. ([#1762](https://github.com/open-telemetry/opentelemetry-python/pull/1762)) - Zipkin exporter: Add support for timeout and implement shutdown ([#1799](https://github.com/open-telemetry/opentelemetry-python/pull/1799)) ### Changed - Adjust `B3Format` propagator to be spec compliant by not modifying context when propagation headers are not present/invalid/empty ([#1728](https://github.com/open-telemetry/opentelemetry-python/pull/1728)) - Silence unnecessary warning when creating a new Status object without description. ([#1721](https://github.com/open-telemetry/opentelemetry-python/pull/1721)) - Update bootstrap cmd to use exact version when installing instrumentation packages. ([#1722](https://github.com/open-telemetry/opentelemetry-python/pull/1722)) - Fix B3 propagator to never return None. ([#1750](https://github.com/open-telemetry/opentelemetry-python/pull/1750)) - Added ProxyTracerProvider and ProxyTracer implementations to allow fetching provider and tracer instances before a global provider is set up. ([#1726](https://github.com/open-telemetry/opentelemetry-python/pull/1726)) - Added `__contains__` to `opentelementry.trace.span.TraceState`. ([#1773](https://github.com/open-telemetry/opentelemetry-python/pull/1773)) - `opentelemetry-opentracing-shim` Fix an issue in the shim where a Span was being wrapped in a NonRecordingSpan when it wasn't necessary. ([#1776](https://github.com/open-telemetry/opentelemetry-python/pull/1776)) - OTLP Exporter now uses the scheme in the endpoint to determine whether to establish a secure connection or not. ([#1771](https://github.com/open-telemetry/opentelemetry-python/pull/1771)) ## Version 1.0.0 (2021-03-26) ### Added - Document how to work with fork process web server models(Gunicorn, uWSGI etc...) ([#1609](https://github.com/open-telemetry/opentelemetry-python/pull/1609)) - Add `max_attr_value_length` support to Jaeger exporter ([#1633](https://github.com/open-telemetry/opentelemetry-python/pull/1633)) - Moved `use_span` from Tracer to `opentelemetry.trace.use_span`. ([#1668](https://github.com/open-telemetry/opentelemetry-python/pull/1668)) - `opentelemetry.trace.use_span()` will now overwrite previously set status on span in case an exception is raised inside the context manager and `set_status_on_exception` is set to `True`. ([#1668](https://github.com/open-telemetry/opentelemetry-python/pull/1668)) - Add `udp_split_oversized_batches` support to jaeger exporter ([#1500](https://github.com/open-telemetry/opentelemetry-python/pull/1500)) ### Changed - remove `service_name` from constructor of jaeger and opencensus exporters and use of env variable `OTEL_PYTHON_SERVICE_NAME` ([#1669])(https://github.com/open-telemetry/opentelemetry-python/pull/1669) - Rename `IdsGenerator` to `IdGenerator` ([#1651](https://github.com/open-telemetry/opentelemetry-python/pull/1651)) - Make TracerProvider's resource attribute private ([#1652](https://github.com/open-telemetry/opentelemetry-python/pull/1652)) - Rename Resource's `create_empty` to `get_empty` ([#1653](https://github.com/open-telemetry/opentelemetry-python/pull/1653)) - Renamed `BatchExportSpanProcessor` to `BatchSpanProcessor` and `SimpleExportSpanProcessor` to `SimpleSpanProcessor` ([#1656](https://github.com/open-telemetry/opentelemetry-python/pull/1656)) - Rename `DefaultSpan` to `NonRecordingSpan` ([#1661](https://github.com/open-telemetry/opentelemetry-python/pull/1661)) - Fixed distro configuration with `OTEL_TRACES_EXPORTER` env var set to `otlp` ([#1657](https://github.com/open-telemetry/opentelemetry-python/pull/1657)) - Moving `Getter`, `Setter` and `TextMapPropagator` out of `opentelemetry.trace.propagation` and into `opentelemetry.propagators` ([#1662](https://github.com/open-telemetry/opentelemetry-python/pull/1662)) - Rename `BaggagePropagator` to `W3CBaggagePropagator` ([#1663](https://github.com/open-telemetry/opentelemetry-python/pull/1663)) - Rename `JaegerSpanExporter` to `JaegerExporter` and rename `ZipkinSpanExporter` to `ZipkinExporter` ([#1664](https://github.com/open-telemetry/opentelemetry-python/pull/1664)) - Expose `StatusCode` from the `opentelemetry.trace` module ([#1681](https://github.com/open-telemetry/opentelemetry-python/pull/1681)) - Status now only sets `description` when `status_code` is set to `StatusCode.ERROR` ([#1673](https://github.com/open-telemetry/opentelemetry-python/pull/1673)) - Update OTLP exporter to use OTLP proto `0.7.0` ([#1674](https://github.com/open-telemetry/opentelemetry-python/pull/1674)) - Remove time_ns from API and add a warning for older versions of Python ([#1602](https://github.com/open-telemetry/opentelemetry-python/pull/1602)) - Hide implementation classes/variables in api/sdk ([#1684](https://github.com/open-telemetry/opentelemetry-python/pull/1684)) - Cleanup OTLP exporter compression options, add tests ([#1671](https://github.com/open-telemetry/opentelemetry-python/pull/1671)) - Initial documentation for environment variables ([#1680](https://github.com/open-telemetry/opentelemetry-python/pull/1680)) - Change Zipkin exporter to obtain service.name from span ([#1696](https://github.com/open-telemetry/opentelemetry-python/pull/1696)) - Split up `opentelemetry-exporter-jaeger` package into `opentelemetry-exporter-jaeger-proto-grpc` and `opentelemetry-exporter-jaeger-thrift` packages to reduce dependencies for each one. ([#1694](https://github.com/open-telemetry/opentelemetry-python/pull/1694)) - Added `opentelemetry-exporter-otlp-proto-grpc` and changed `opentelemetry-exporter-otlp` to install it as a dependency. This will allow for the next package/protocol to also be in its own package. ([#1695](https://github.com/open-telemetry/opentelemetry-python/pull/1695)) - Change Jaeger exporters to obtain service.name from span ([#1703](https://github.com/open-telemetry/opentelemetry-python/pull/1703)) - Fixed an unset `OTEL_TRACES_EXPORTER` resulting in an error ([#1707](https://github.com/open-telemetry/opentelemetry-python/pull/1707)) - Split Zipkin exporter into `opentelemetry-exporter-zipkin-json` and `opentelemetry-exporter-zipkin-proto-http` packages to reduce dependencies. The `opentelemetry-exporter-zipkin` installs both. ([#1699](https://github.com/open-telemetry/opentelemetry-python/pull/1699)) - Make setters and getters optional ([#1690](https://github.com/open-telemetry/opentelemetry-python/pull/1690)) ### Removed - Removed unused `get_hexadecimal_trace_id` and `get_hexadecimal_span_id` methods. ([#1675](https://github.com/open-telemetry/opentelemetry-python/pull/1675)) - Remove `OTEL_EXPORTER_*_ INSECURE` env var ([#1682](https://github.com/open-telemetry/opentelemetry-python/pull/1682)) - Removing support for Python 3.5 ([#1706](https://github.com/open-telemetry/opentelemetry-python/pull/1706)) ## Version 0.19b0 (2021-03-26) ### Changed - remove `service_name` from constructor of jaeger and opencensus exporters and use of env variable `OTEL_PYTHON_SERVICE_NAME` ([#1669])(https://github.com/open-telemetry/opentelemetry-python/pull/1669) - Rename `IdsGenerator` to `IdGenerator` ([#1651](https://github.com/open-telemetry/opentelemetry-python/pull/1651)) ### Removed - Removing support for Python 3.5 ([#1706](https://github.com/open-telemetry/opentelemetry-python/pull/1706)) ## Version 0.18b0 (2021-02-16) ### Added - Add urllib to opentelemetry-bootstrap target list ([#1584](https://github.com/open-telemetry/opentelemetry-python/pull/1584)) ## Version 1.0.0rc1 (2021-02-12) ### Changed - Tracer provider environment variables are now consistent with the rest ([#1571](https://github.com/open-telemetry/opentelemetry-python/pull/1571)) - Rename `TRACE_` to `TRACES_` for environment variables ([#1595](https://github.com/open-telemetry/opentelemetry-python/pull/1595)) - Limits for Span attributes, events and links have been updated to 128 ([1597](https://github.com/open-telemetry/opentelemetry-python/pull/1597)) - Read-only Span attributes have been moved to ReadableSpan class ([#1560](https://github.com/open-telemetry/opentelemetry-python/pull/1560)) - `BatchExportSpanProcessor` flushes export queue when it reaches `max_export_batch_size` ([#1521](https://github.com/open-telemetry/opentelemetry-python/pull/1521)) ### Added - Added `end_on_exit` argument to `start_as_current_span` ([#1519](https://github.com/open-telemetry/opentelemetry-python/pull/1519)) - Add `Span.set_attributes` method to set multiple values with one call ([#1520](https://github.com/open-telemetry/opentelemetry-python/pull/1520)) - Make sure Resources follow semantic conventions ([#1480](https://github.com/open-telemetry/opentelemetry-python/pull/1480)) - Allow missing carrier headers to continue without raising AttributeError ([#1545](https://github.com/open-telemetry/opentelemetry-python/pull/1545)) ### Removed - Remove Configuration ([#1523](https://github.com/open-telemetry/opentelemetry-python/pull/1523)) - Remove Metrics as part of stable, marked as experimental ([#1568](https://github.com/open-telemetry/opentelemetry-python/pull/1568)) ## Version 0.17b0 (2021-01-20) ### Added - Add support for OTLP v0.6.0 ([#1472](https://github.com/open-telemetry/opentelemetry-python/pull/1472)) - Add protobuf via gRPC exporting support for Jaeger ([#1471](https://github.com/open-telemetry/opentelemetry-python/pull/1471)) - Add support for Python 3.9 ([#1441](https://github.com/open-telemetry/opentelemetry-python/pull/1441)) - Added the ability to disable instrumenting libraries specified by OTEL_PYTHON_DISABLED_INSTRUMENTATIONS env variable, when using opentelemetry-instrument command. ([#1461](https://github.com/open-telemetry/opentelemetry-python/pull/1461)) - Add `fields` to propagators ([#1374](https://github.com/open-telemetry/opentelemetry-python/pull/1374)) - Add local/remote samplers to parent based sampler ([#1440](https://github.com/open-telemetry/opentelemetry-python/pull/1440)) - Add support for OTEL*SPAN*{ATTRIBUTE_COUNT_LIMIT,EVENT_COUNT_LIMIT,LINK_COUNT_LIMIT} ([#1377](https://github.com/open-telemetry/opentelemetry-python/pull/1377)) - Return `None` for `DictGetter` if key not found ([#1449](https://github.com/open-telemetry/opentelemetry-python/pull/1449)) - Added support for Jaeger propagator ([#1219](https://github.com/open-telemetry/opentelemetry-python/pull/1219)) - Remove dependency on SDK from `opentelemetry-instrumentation` package. The `opentelemetry-sdk` package now registers an entrypoint `opentelemetry_configurator` to allow `opentelemetry-instrument` to load the configuration for the SDK ([#1420](https://github.com/open-telemetry/opentelemetry-python/pull/1420)) - `opentelemetry-exporter-zipkin` Add support for array attributes in Span and Resource exports ([#1285](https://github.com/open-telemetry/opentelemetry-python/pull/1285)) - Added `__repr__` for `DefaultSpan`, added `trace_flags` to `__repr__` of `SpanContext` ([#1485](https://github.com/open-telemetry/opentelemetry-python/pull/1485)) - `opentelemetry-sdk` Add support for OTEL_TRACE_SAMPLER and OTEL_TRACE_SAMPLER_ARG env variables ([#1496](https://github.com/open-telemetry/opentelemetry-python/pull/1496)) - Adding `opentelemetry-distro` package to add default configuration for span exporter to OTLP ([#1482](https://github.com/open-telemetry/opentelemetry-python/pull/1482)) ### Changed - `opentelemetry-exporter-zipkin` Updated zipkin exporter status code and error tag ([#1486](https://github.com/open-telemetry/opentelemetry-python/pull/1486)) - Recreate span on every run of a `start_as_current_span`-decorated function ([#1451](https://github.com/open-telemetry/opentelemetry-python/pull/1451)) - `opentelemetry-exporter-otlp` Headers are now passed in as tuple as metadata, instead of a string, which was incorrect. ([#1507](https://github.com/open-telemetry/opentelemetry-python/pull/1507)) - `opentelemetry-exporter-jaeger` Updated Jaeger exporter status code tag ([#1488](https://github.com/open-telemetry/opentelemetry-python/pull/1488)) - `opentelemetry-api` `opentelemety-sdk` Moved `idsgenerator` into sdk ([#1514](https://github.com/open-telemetry/opentelemetry-python/pull/1514)) - `opentelemetry-sdk` The B3Format propagator has been moved into its own package: `opentelemetry-propagator-b3` ([#1513](https://github.com/open-telemetry/opentelemetry-python/pull/1513)) - Update default port for OTLP exporter from 55680 to 4317 ([#1516](https://github.com/open-telemetry/opentelemetry-python/pull/1516)) - `opentelemetry-exporter-zipkin` Update boolean attribute value transformation ([#1509](https://github.com/open-telemetry/opentelemetry-python/pull/1509)) - Move opentelemetry-opentracing-shim out of instrumentation folder ([#1533](https://github.com/open-telemetry/opentelemetry-python/pull/1533)) - `opentelemetry-sdk` The JaegerPropagator has been moved into its own package: `opentelemetry-propagator-jaeger` ([#1525](https://github.com/open-telemetry/opentelemetry-python/pull/1525)) - `opentelemetry-exporter-jaeger`, `opentelemetry-exporter-zipkin` Update InstrumentationInfo tag keys for Jaeger and Zipkin exporters ([#1535](https://github.com/open-telemetry/opentelemetry-python/pull/1535)) - `opentelemetry-sdk` Remove rate property setter from TraceIdRatioBasedSampler ([#1536](https://github.com/open-telemetry/opentelemetry-python/pull/1536)) - Fix TraceState to adhere to specs ([#1502](https://github.com/open-telemetry/opentelemetry-python/pull/1502)) - Update Resource `merge` key conflict precedence ([#1544](https://github.com/open-telemetry/opentelemetry-python/pull/1544)) ### Removed - `opentelemetry-api` Remove ThreadLocalRuntimeContext since python3.4 is not supported. ## Version 0.16b1 (2020-11-26) ### Added - Add meter reference to observers ([#1425](https://github.com/open-telemetry/opentelemetry-python/pull/1425)) ## Version 0.16b0 (2020-11-25) ### Added - Add optional parameter to `record_exception` method ([#1314](https://github.com/open-telemetry/opentelemetry-python/pull/1314)) - Add pickle support to SpanContext class ([#1380](https://github.com/open-telemetry/opentelemetry-python/pull/1380)) - Add instrumentation library name and version to OTLP exported metrics ([#1418](https://github.com/open-telemetry/opentelemetry-python/pull/1418)) - Add Gzip compression for exporter ([#1141](https://github.com/open-telemetry/opentelemetry-python/pull/1141)) - Support for v2 api protobuf format ([#1318](https://github.com/open-telemetry/opentelemetry-python/pull/1318)) - Add IDs Generator as Configurable Property of Auto Instrumentation ([#1404](https://github.com/open-telemetry/opentelemetry-python/pull/1404)) - Added support for `OTEL_EXPORTER` to the `opentelemetry-instrument` command ([#1036](https://github.com/open-telemetry/opentelemetry-python/pull/1036)) ### Changed - Change temporality for Counter and UpDownCounter ([#1384](https://github.com/open-telemetry/opentelemetry-python/pull/1384)) - OTLP exporter: Handle error case when no credentials supplied ([#1366](https://github.com/open-telemetry/opentelemetry-python/pull/1366)) - Update protobuf versions ([#1356](https://github.com/open-telemetry/opentelemetry-python/pull/1356)) - Add missing references to instrumented packages ([#1416](https://github.com/open-telemetry/opentelemetry-python/pull/1416)) - Instrumentation Package depends on the OTel SDK ([#1405](https://github.com/open-telemetry/opentelemetry-python/pull/1405)) - Allow samplers to modify tracestate ([#1319](https://github.com/open-telemetry/opentelemetry-python/pull/1319)) - Update exception handling optional parameters, add escaped attribute to record_exception ([#1365](https://github.com/open-telemetry/opentelemetry-python/pull/1365)) - Rename `MetricRecord` to `ExportRecord` ([#1367](https://github.com/open-telemetry/opentelemetry-python/pull/1367)) - Rename `Record` to `Accumulation` ([#1373](https://github.com/open-telemetry/opentelemetry-python/pull/1373)) - Rename `Meter` to `Accumulator` ([#1372](https://github.com/open-telemetry/opentelemetry-python/pull/1372)) - Fix `ParentBased` sampler for implicit parent spans. Fix also `trace_state` erasure for dropped spans or spans sampled by the `TraceIdRatioBased` sampler. ([#1394](https://github.com/open-telemetry/opentelemetry-python/pull/1394)) ## Version 0.15b0 (2020-11-02) ### Added - Add Env variables in OTLP exporter ([#1101](https://github.com/open-telemetry/opentelemetry-python/pull/1101)) - Add support for Jaeger Span Exporter configuration by environment variables and
change JaegerSpanExporter constructor parameters ([#1114](https://github.com/open-telemetry/opentelemetry-python/pull/1114)) ### Changed - Updating status codes to adhere to specs ([#1282](https://github.com/open-telemetry/opentelemetry-python/pull/1282)) - Set initial checkpoint timestamp in aggregators ([#1237](https://github.com/open-telemetry/opentelemetry-python/pull/1237)) - Make `SpanProcessor.on_start` accept parent Context ([#1251](https://github.com/open-telemetry/opentelemetry-python/pull/1251)) - Fix b3 propagator entrypoint ([#1265](https://github.com/open-telemetry/opentelemetry-python/pull/1265)) - Allow None in sequence attributes values ([#998](https://github.com/open-telemetry/opentelemetry-python/pull/998)) - Samplers to accept parent Context ([#1267](https://github.com/open-telemetry/opentelemetry-python/pull/1267)) - Span.is_recording() returns false after span has ended ([#1289](https://github.com/open-telemetry/opentelemetry-python/pull/1289)) - Allow samplers to modify tracestate ([#1319](https://github.com/open-telemetry/opentelemetry-python/pull/1319)) - Remove TracerProvider coupling from Tracer init ([#1295](https://github.com/open-telemetry/opentelemetry-python/pull/1295)) ## Version 0.14b0 (2020-10-13) ### Added - Add optional parameter to `record_exception` method ([#1242](https://github.com/open-telemetry/opentelemetry-python/pull/1242)) - Add support for `OTEL_PROPAGATORS` ([#1123](https://github.com/open-telemetry/opentelemetry-python/pull/1123)) - Add keys method to TextMap propagator Getter ([#1196](https://github.com/open-telemetry/opentelemetry-python/issues/1196)) - Add timestamps to OTLP exporter ([#1199](https://github.com/open-telemetry/opentelemetry-python/pull/1199)) - Add Global Error Handler ([#1080](https://github.com/open-telemetry/opentelemetry-python/pull/1080)) - Add support for `OTEL_BSP_MAX_QUEUE_SIZE`, `OTEL_BSP_SCHEDULE_DELAY_MILLIS`, `OTEL_BSP_MAX_EXPORT_BATCH_SIZE` and `OTEL_BSP_EXPORT_TIMEOUT_MILLIS` environment variables ([#1105](https://github.com/open-telemetry/opentelemetry-python/pull/1120)) - Adding Resource to MeterRecord ([#1209](https://github.com/open-telemetry/opentelemetry-python/pull/1209)) s ### Changed - Store `int`s as `int`s in the global Configuration object ([#1118](https://github.com/open-telemetry/opentelemetry-python/pull/1118)) - Allow for Custom Trace and Span IDs Generation - `IdsGenerator` for TracerProvider ([#1153](https://github.com/open-telemetry/opentelemetry-python/pull/1153)) - Update baggage propagation header ([#1194](https://github.com/open-telemetry/opentelemetry-python/pull/1194)) - Make instances of SpanContext immutable ([#1134](https://github.com/open-telemetry/opentelemetry-python/pull/1134)) - Parent is now always passed in via Context, instead of Span or SpanContext ([#1146](https://github.com/open-telemetry/opentelemetry-python/pull/1146)) - Update OpenTelemetry protos to v0.5.0 ([#1143](https://github.com/open-telemetry/opentelemetry-python/pull/1143)) - Zipkin exporter now accepts a `max_tag_value_length` attribute to customize the maximum allowed size a tag value can have. ([#1151](https://github.com/open-telemetry/opentelemetry-python/pull/1151)) - Fixed OTLP events to Zipkin annotations translation. ([#1161](https://github.com/open-telemetry/opentelemetry-python/pull/1161)) - Fixed bootstrap command to correctly install opentelemetry-instrumentation-falcon instead of opentelemetry-instrumentation-flask. ([#1138](https://github.com/open-telemetry/opentelemetry-python/pull/1138)) - Update sampling result names ([#1128](https://github.com/open-telemetry/opentelemetry-python/pull/1128)) - Event attributes are now immutable ([#1195](https://github.com/open-telemetry/opentelemetry-python/pull/1195)) - Renaming metrics Batcher to Processor ([#1203](https://github.com/open-telemetry/opentelemetry-python/pull/1203)) - Protect access to Span implementation ([#1188](https://github.com/open-telemetry/opentelemetry-python/pull/1188)) - `start_as_current_span` and `use_span` can now optionally auto-record any exceptions raised inside the context manager. ([#1162](https://github.com/open-telemetry/opentelemetry-python/pull/1162)) ## Version 0.13b0 (2020-09-17) ### Added - Add instrumentation info to exported spans ([#1095](https://github.com/open-telemetry/opentelemetry-python/pull/1095)) - Add metric OTLP exporter ([#835](https://github.com/open-telemetry/opentelemetry-python/pull/835)) - Add type hints to OTLP exporter ([#1121](https://github.com/open-telemetry/opentelemetry-python/pull/1121)) - Add support for OTEL_EXPORTER_ZIPKIN_ENDPOINT env var. As part of this change, the configuration of the ZipkinSpanExporter exposes a `url` argument to replace `host_name`, `port`, `protocol`, `endpoint`. This brings this implementation inline with other implementations. ([#1064](https://github.com/open-telemetry/opentelemetry-python/pull/1064)) - Zipkin exporter report instrumentation info. ([#1097](https://github.com/open-telemetry/opentelemetry-python/pull/1097)) - Add status mapping to tags ([#1111](https://github.com/open-telemetry/opentelemetry-python/issues/1111)) - Report instrumentation info ([#1098](https://github.com/open-telemetry/opentelemetry-python/pull/1098)) - Add support for http metrics ([#1116](https://github.com/open-telemetry/opentelemetry-python/pull/1116)) - Populate resource attributes as per semantic conventions ([#1053](https://github.com/open-telemetry/opentelemetry-python/pull/1053)) ### Changed - Refactor `SpanContext.is_valid` from a method to a data attribute ([#1005](https://github.com/open-telemetry/opentelemetry-python/pull/1005)) - Moved samplers from API to SDK ([#1023](https://github.com/open-telemetry/opentelemetry-python/pull/1023)) - Change return value type of `correlationcontext.get_correlations` to immutable `MappingProxyType` ([#1024](https://github.com/open-telemetry/opentelemetry-python/pull/1024)) - Sampling spec changes ([#1034](https://github.com/open-telemetry/opentelemetry-python/pull/1034)) - Remove lazy Event and Link API from Span interface ([#1045](https://github.com/open-telemetry/opentelemetry-python/pull/1045)) - Rename CorrelationContext to Baggage ([#1060](https://github.com/open-telemetry/opentelemetry-python/pull/1060)) - Rename HTTPTextFormat to TextMapPropagator. This change also updates `get_global_httptextformat` and `set_global_httptextformat` to `get_global_textmap` and `set_global_textmap` ([#1085](https://github.com/open-telemetry/opentelemetry-python/pull/1085)) - Fix api/sdk setup.cfg to include missing python files ([#1091](https://github.com/open-telemetry/opentelemetry-python/pull/1091)) - Improve BatchExportSpanProcessor ([#1062](https://github.com/open-telemetry/opentelemetry-python/pull/1062)) - Rename Resource labels to attributes ([#1082](https://github.com/open-telemetry/opentelemetry-python/pull/1082)) - Rename members of `trace.sampling.Decision` enum ([#1115](https://github.com/open-telemetry/opentelemetry-python/pull/1115)) - Merge `OTELResourceDetector` result when creating resources ([#1096](https://github.com/open-telemetry/opentelemetry-python/pull/1096)) ### Removed - Drop support for Python 3.4 ([#1099](https://github.com/open-telemetry/opentelemetry-python/pull/1099)) ## Version 0.12b0 (2020-08-14) ### Added - Implement Views in metrics SDK ([#596](https://github.com/open-telemetry/opentelemetry-python/pull/596)) ### Changed - Update environment variable names, prefix changed from `OPENTELEMETRY` to `OTEL` ([#904](https://github.com/open-telemetry/opentelemetry-python/pull/904)) - Stop TracerProvider and MeterProvider from being overridden ([#959](https://github.com/open-telemetry/opentelemetry-python/pull/959)) - Update default port to 55680 ([#977](https://github.com/open-telemetry/opentelemetry-python/pull/977)) - Add proper length zero padding to hex strings of traceId, spanId, parentId sent on the wire, for compatibility with jaeger-collector ([#908](https://github.com/open-telemetry/opentelemetry-python/pull/908)) - Send start_timestamp and convert labels to strings ([#937](https://github.com/open-telemetry/opentelemetry-python/pull/937)) - Renamed several packages ([#953](https://github.com/open-telemetry/opentelemetry-python/pull/953)) - Thrift URL for Jaeger exporter doesn't allow HTTPS (hardcoded to HTTP) ([#978](https://github.com/open-telemetry/opentelemetry-python/pull/978)) - Change reference names to opentelemetry-instrumentation-opentracing-shim ([#969](https://github.com/open-telemetry/opentelemetry-python/pull/969)) - Changed default Sampler to `ParentOrElse(AlwaysOn)` ([#960](https://github.com/open-telemetry/opentelemetry-python/pull/960)) - Update environment variable names, prefix changed from `OPENTELEMETRY` to `OTEL` ([#904](https://github.com/open-telemetry/opentelemetry-python/pull/904)) - Update environment variable `OTEL_RESOURCE` to `OTEL_RESOURCE_ATTRIBUTES` as per the specification ## Version 0.11b0 (2020-07-28) ### Added - Add support for resources and resource detector ([#853](https://github.com/open-telemetry/opentelemetry-python/pull/853)) ### Changed - Return INVALID_SPAN if no TracerProvider set for get_current_span ([#751](https://github.com/open-telemetry/opentelemetry-python/pull/751)) - Rename record_error to record_exception ([#927](https://github.com/open-telemetry/opentelemetry-python/pull/927)) - Update span exporter to use OpenTelemetry Proto v0.4.0 ([#872](https://github.com/open-telemetry/opentelemetry-python/pull/889)) ## Version 0.10b0 (2020-06-23) ### Changed - Regenerate proto code and add pyi stubs ([#823](https://github.com/open-telemetry/opentelemetry-python/pull/823)) - Rename CounterAggregator -> SumAggregator ([#816](https://github.com/open-telemetry/opentelemetry-python/pull/816)) ## Version 0.9b0 (2020-06-10) ### Added - Adding trace.get_current_span, Removing Tracer.get_current_span ([#552](https://github.com/open-telemetry/opentelemetry-python/pull/552)) - Add SumObserver, UpDownSumObserver and LastValueAggregator in metrics ([#789](https://github.com/open-telemetry/opentelemetry-python/pull/789)) - Add start_pipeline to MeterProvider ([#791](https://github.com/open-telemetry/opentelemetry-python/pull/791)) - Initial release of opentelemetry-ext-otlp, opentelemetry-proto ### Changed - Move stateful & resource from Meter to MeterProvider ([#751](https://github.com/open-telemetry/opentelemetry-python/pull/751)) - Rename Measure to ValueRecorder in metrics ([#761](https://github.com/open-telemetry/opentelemetry-python/pull/761)) - Rename Observer to ValueObserver ([#764](https://github.com/open-telemetry/opentelemetry-python/pull/764)) - Log a warning when replacing the global Tracer/Meter provider ([#856](https://github.com/open-telemetry/opentelemetry-python/pull/856)) - bugfix: byte type attributes are decoded before adding to attributes dict ([#775](https://github.com/open-telemetry/opentelemetry-python/pull/775)) - Rename opentelemetry-auto-instrumentation to opentelemetry-instrumentation, and console script `opentelemetry-auto-instrumentation` to `opentelemetry-instrument` ## Version 0.8b0 (2020-05-27) ### Added - Add a new bootstrap command that enables automatic instrument installations. ([#650](https://github.com/open-telemetry/opentelemetry-python/pull/650)) ### Changed - Handle boolean, integer and float values in Configuration ([#662](https://github.com/open-telemetry/opentelemetry-python/pull/662)) - bugfix: ensure status is always string ([#640](https://github.com/open-telemetry/opentelemetry-python/pull/640)) - Transform resource to tags when exporting ([#707](https://github.com/open-telemetry/opentelemetry-python/pull/707)) - Rename otcollector to opencensus ([#695](https://github.com/open-telemetry/opentelemetry-python/pull/695)) - Transform resource to tags when exporting ([#645](https://github.com/open-telemetry/opentelemetry-python/pull/645)) - `ext/boto`: Could not serialize attribute aws.region to tag when exporting via jaeger Serialize tuple type values by coercing them into a string, since Jaeger does not support tuple types. ([#865](https://github.com/open-telemetry/opentelemetry-python/pull/865)) - Specify to_json indent from arguments ([#718](https://github.com/open-telemetry/opentelemetry-python/pull/718)) - Span.resource will now default to an empty resource ([#724](https://github.com/open-telemetry/opentelemetry-python/pull/724)) - bugfix: Fix error message ([#729](https://github.com/open-telemetry/opentelemetry-python/pull/729)) - deep copy empty attributes ([#714](https://github.com/open-telemetry/opentelemetry-python/pull/714)) ## Version 0.7b1 (2020-05-12) ### Added - Add reset for the global configuration object, for testing purposes ([#636](https://github.com/open-telemetry/opentelemetry-python/pull/636)) - Add support for programmatic instrumentation ([#579](https://github.com/open-telemetry/opentelemetry-python/pull/579)) ### Changed - tracer.get_tracer now optionally accepts a TracerProvider ([#602](https://github.com/open-telemetry/opentelemetry-python/pull/602)) - Configuration object can now be used by any component of opentelemetry, including 3rd party instrumentations ([#563](https://github.com/open-telemetry/opentelemetry-python/pull/563)) - bugfix: configuration object now matches fields in a case-sensitive manner ([#583](https://github.com/open-telemetry/opentelemetry-python/pull/583)) - bugfix: configuration object now accepts all valid python variable names ([#583](https://github.com/open-telemetry/opentelemetry-python/pull/583)) - bugfix: configuration undefined attributes now return None instead of raising an AttributeError. ([#583](https://github.com/open-telemetry/opentelemetry-python/pull/583)) - bugfix: 'debug' field is now correct ([#549](https://github.com/open-telemetry/opentelemetry-python/pull/549)) - bugfix: enable auto-instrumentation command to work for custom entry points (e.g. flask_run) ([#567](https://github.com/open-telemetry/opentelemetry-python/pull/567)) - Exporter API: span parents are now always spancontext ([#548](https://github.com/open-telemetry/opentelemetry-python/pull/548)) - Console span exporter now prints prettier, more legible messages ([#505](https://github.com/open-telemetry/opentelemetry-python/pull/505)) - bugfix: B3 propagation now retrieves parentSpanId correctly ([#621](https://github.com/open-telemetry/opentelemetry-python/pull/621)) - bugfix: a DefaultSpan now longer causes an exception when used with tracer ([#577](https://github.com/open-telemetry/opentelemetry-python/pull/577)) - move last_updated_timestamp into aggregators instead of bound metric instrument ([#522](https://github.com/open-telemetry/opentelemetry-python/pull/522)) - bugfix: suppressing instrumentation in metrics to eliminate an infinite loop of telemetry ([#529](https://github.com/open-telemetry/opentelemetry-python/pull/529)) - bugfix: freezing span attribute sequences, reducing potential user errors ([#529](https://github.com/open-telemetry/opentelemetry-python/pull/529)) ## Version 0.6b0 (2020-03-30) ### Added - Add support for lazy events and links ([#474](https://github.com/open-telemetry/opentelemetry-python/pull/474)) - Adding is_remote flag to SpanContext, indicating when a span is remote ([#516](https://github.com/open-telemetry/opentelemetry-python/pull/516)) - Adding a solution to release metric handles and observers ([#435](https://github.com/open-telemetry/opentelemetry-python/pull/435)) - Initial release: opentelemetry-instrumentation ### Changed - Metrics API no longer uses LabelSet ([#527](https://github.com/open-telemetry/opentelemetry-python/pull/527)) - Allow digit as first char in vendor specific trace state key ([#511](https://github.com/open-telemetry/opentelemetry-python/pull/511)) - Exporting to collector now works ([#508](https://github.com/open-telemetry/opentelemetry-python/pull/508)) ## Version 0.5b0 (2020-03-16) ### Added - Adding Correlation Context API/SDK and propagator ([#471](https://github.com/open-telemetry/opentelemetry-python/pull/471)) - Adding a global configuration module to simplify setting and getting globals ([#466](https://github.com/open-telemetry/opentelemetry-python/pull/466)) - Adding named meters, removing batchers ([#431](https://github.com/open-telemetry/opentelemetry-python/pull/431)) - Adding attach/detach methods as per spec ([#429](https://github.com/open-telemetry/opentelemetry-python/pull/429)) - Adding OT Collector metrics exporter ([#454](https://github.com/open-telemetry/opentelemetry-python/pull/454)) - Initial release opentelemetry-ext-otcollector ### Changed - Rename metric handle to bound metric instrument ([#470](https://github.com/open-telemetry/opentelemetry-python/pull/470)) - Moving resources to sdk ([#464](https://github.com/open-telemetry/opentelemetry-python/pull/464)) - Implementing propagators to API to use context ([#446](https://github.com/open-telemetry/opentelemetry-python/pull/446)) - Renaming TraceOptions to TraceFlags ([#450](https://github.com/open-telemetry/opentelemetry-python/pull/450)) - Renaming TracerSource to TracerProvider ([#441](https://github.com/open-telemetry/opentelemetry-python/pull/441)) - Improve validation of attributes ([#460](https://github.com/open-telemetry/opentelemetry-python/pull/460)) - Re-raise errors caught in opentelemetry.sdk.trace.Tracer.use_span() ([#469](https://github.com/open-telemetry/opentelemetry-python/pull/469)) - Implement observer instrument ([#425](https://github.com/open-telemetry/opentelemetry-python/pull/425)) ## Version 0.4a0 (2020-02-21) ### Added - Added named Tracers ([#301](https://github.com/open-telemetry/opentelemetry-python/pull/301)) - Add int and valid sequenced to AttributeValue type ([#368](https://github.com/open-telemetry/opentelemetry-python/pull/368)) - Add ABC for Metric ([#391](https://github.com/open-telemetry/opentelemetry-python/pull/391)) - Metrics export pipeline, and stdout exporter ([#341](https://github.com/open-telemetry/opentelemetry-python/pull/341)) - Adding Context API Implementation ([#395](https://github.com/open-telemetry/opentelemetry-python/pull/395)) - Adding trace.get_tracer function ([#430](https://github.com/open-telemetry/opentelemetry-python/pull/430)) - Add runtime validation for set_attribute ([#348](https://github.com/open-telemetry/opentelemetry-python/pull/348)) - Add support for B3 ParentSpanID ([#286](https://github.com/open-telemetry/opentelemetry-python/pull/286)) - Implement MinMaxSumCount aggregator ([#422](https://github.com/open-telemetry/opentelemetry-python/pull/422)) - Initial release opentelemetry-ext-zipkin, opentelemetry-ext-prometheus ### Changed - Separate Default classes from interface descriptions ([#311](https://github.com/open-telemetry/opentelemetry-python/pull/311)) - Export span status ([#367](https://github.com/open-telemetry/opentelemetry-python/pull/367)) - Export span kind ([#387](https://github.com/open-telemetry/opentelemetry-python/pull/387)) - Set status for ended spans ([#297](https://github.com/open-telemetry/opentelemetry-python/pull/297) and [#358](https://github.com/open-telemetry/opentelemetry-python/pull/358)) - Use module loggers ([#351](https://github.com/open-telemetry/opentelemetry-python/pull/351)) - Protect start_time and end_time from being set manually by the user ([#363](https://github.com/open-telemetry/opentelemetry-python/pull/363)) - Set status in start_as_current_span ([#377](https://github.com/open-telemetry/opentelemetry-python/pull/377)) - Implement force_flush for span processors ([#389](https://github.com/open-telemetry/opentelemetry-python/pull/389)) - Set sampled flag on sampling trace ([#407](https://github.com/open-telemetry/opentelemetry-python/pull/407)) - Add io and formatter options to console exporter ([#412](https://github.com/open-telemetry/opentelemetry-python/pull/412)) - Clean up ProbabilitySample for 64 bit trace IDs ([#238](https://github.com/open-telemetry/opentelemetry-python/pull/238)) ### Removed - Remove monotonic and absolute metric instruments ([#410](https://github.com/open-telemetry/opentelemetry-python/pull/410)) ## Version 0.3a0 (2019-12-11) ### Added - Add metrics exporters ([#192](https://github.com/open-telemetry/opentelemetry-python/pull/192)) - Implement extract and inject support for HTTP_HEADERS and TEXT_MAP formats ([#256](https://github.com/open-telemetry/opentelemetry-python/pull/256)) ### Changed - Multiple tracing API/SDK changes - Multiple metrics API/SDK changes ### Removed - Remove option to create unstarted spans from API ([#290](https://github.com/open-telemetry/opentelemetry-python/pull/290)) ## Version 0.2a0 (2019-10-29) ### Added - W3C TraceContext fixes and compliance tests ([#228](https://github.com/open-telemetry/opentelemetry-python/pull/228)) - Sampler API/SDK ([#225](https://github.com/open-telemetry/opentelemetry-python/pull/225)) - Initial release: opentelemetry-ext-jaeger, opentelemetry-opentracing-shim ### Changed - Multiple metrics API/SDK changes - Multiple tracing API/SDK changes - Multiple context API changes - Multiple bugfixes and improvements ## Version 0.1a0 (2019-09-30) ### Added - Initial release api/sdk - Use Attribute rather than boundattribute in logrecord ([#3567](https://github.com/open-telemetry/opentelemetry-python/pull/3567)) - Fix flush error when no LoggerProvider configured for LoggingHandler ([#3608](https://github.com/open-telemetry/opentelemetry-python/pull/3608)) - Fix `OTLPMetricExporter` ignores `preferred_aggregation` property ([#3603](https://github.com/open-telemetry/opentelemetry-python/pull/3603)) - Logs: set `observed_timestamp` field ([#3565](https://github.com/open-telemetry/opentelemetry-python/pull/3565)) - Add missing Resource SchemaURL in OTLP exporters ([#3652](https://github.com/open-telemetry/opentelemetry-python/pull/3652)) - Fix loglevel warning text ([#3566](https://github.com/open-telemetry/opentelemetry-python/pull/3566)) - Prometheus Exporter string representation for target_info labels ([#3659](https://github.com/open-telemetry/opentelemetry-python/pull/3659)) - Logs: ObservedTimestamp field is missing in console exporter output ([#3564](https://github.com/open-telemetry/opentelemetry-python/pull/3564)) - Fix explicit bucket histogram aggregation ([#3429](https://github.com/open-telemetry/opentelemetry-python/pull/3429)) - Add `code.lineno`, `code.function` and `code.filepath` to all logs ([#3645](https://github.com/open-telemetry/opentelemetry-python/pull/3645)) - Add Synchronous Gauge instrument ([#3462](https://github.com/open-telemetry/opentelemetry-python/pull/3462)) - Drop support for 3.7 ([#3668](https://github.com/open-telemetry/opentelemetry-python/pull/3668)) - Include key in attribute sequence warning ([#3639](https://github.com/open-telemetry/opentelemetry-python/pull/3639)) - Upgrade markupsafe, Flask and related dependencies to dev and test environments ([#3609](https://github.com/open-telemetry/opentelemetry-python/pull/3609)) - Handle HTTP 2XX responses as successful in OTLP exporters ([#3623](https://github.com/open-telemetry/opentelemetry-python/pull/3623)) - Improve Resource Detector timeout messaging ([#3645](https://github.com/open-telemetry/opentelemetry-python/pull/3645)) - Add Proxy classes for logging ([#3575](https://github.com/open-telemetry/opentelemetry-python/pull/3575)) - Remove dependency on 'backoff' library ([#3679](https://github.com/open-telemetry/opentelemetry-python/pull/3679)) - Make create_gauge non-abstract method ([#3817](https://github.com/open-telemetry/opentelemetry-python/pull/3817)) - Make `tracer.start_as_current_span()` decorator work with async functions ([#3633](https://github.com/open-telemetry/opentelemetry-python/pull/3633)) - Fix python 3.12 deprecation warning ([#3751](https://github.com/open-telemetry/opentelemetry-python/pull/3751)) - bump mypy to 0.982 ([#3776](https://github.com/open-telemetry/opentelemetry-python/pull/3776)) - Add support for OTEL_SDK_DISABLED environment variable ([#3648](https://github.com/open-telemetry/opentelemetry-python/pull/3648)) - Fix ValueError message for PeriodicExportingMetricsReader ([#3769](https://github.com/open-telemetry/opentelemetry-python/pull/3769)) - Use `BaseException` instead of `Exception` in `record_exception` ([#3354](https://github.com/open-telemetry/opentelemetry-python/pull/3354)) - Make span.record_exception more robust ([#3778](https://github.com/open-telemetry/opentelemetry-python/pull/3778)) - Fix license field in pyproject.toml files ([#3803](https://github.com/open-telemetry/opentelemetry-python/pull/3803)) python-opentelemetry-1.45.2/CLAUDE.md000066400000000000000000000000341527070070700172770ustar00rootroot00000000000000See [AGENTS.md](AGENTS.md). python-opentelemetry-1.45.2/CONTRIBUTING.md000066400000000000000000000453341527070070700202650ustar00rootroot00000000000000# Contributing to opentelemetry-python The Python special interest group (SIG) meets weekly on Thursdays at 9AM PST. Check the [OpenTelemetry community calendar](https://groups.google.com/a/opentelemetry.io/g/calendar-python) for specific dates and Zoom meeting links. See the [public meeting notes](https://docs.google.com/document/d/18w8zOBm_mbety0OqlPwxc7dvnfu641EgmrO4AdJef0U/edit?tab=t.0) for a summary description of past meetings. 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. # Find your right repo This is the main repo for OpenTelemetry Python. Nevertheless, there are other repos that are related to this project. Please take a look at this list first, your contributions may belong in one of these repos better: 1. [OpenTelemetry Contrib](https://github.com/open-telemetry/opentelemetry-python-contrib): Instrumentations for third-party libraries and frameworks. ## 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 [channel](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. The Slack channel will be used for introductions and an entry point for external people to be triaged and redirected. For discussions, please open up an issue or a Github [Discussion](https://github.com/open-telemetry/opentelemetry-python/discussions). 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: ```console 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 the API docs - `tox -e opentelemetry-api` and `tox -e opentelemetry-sdk` to run the API and SDK unit tests - `tox -e py314-opentelemetry-api` to e.g. run the API 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 typecheck` to run pyright against entire code base. - `tox -e public-symbols-check` to run public_symbols_checker.py. - `tox -e docker-tests-{otlpexporter,opencensus}` to run tests in both or either one location. - `tox -e tracecontext` to run integration tests for tracecontext. - `tox -e precommit` to run all `pre-commit` actions The `docker-tests-otlpexporter` tests use [inline-snapshot](https://github.com/15r10nk/inline-snapshot) to assert on expected values inline in the test code. If you need to create or update a snapshot, run the tests with `--inline-snapshot=create` or `--inline-snapshot=fix` (see the [inline-snapshot docs](https://15r10nk.github.io/inline-snapshot/latest/) for all options). ### Changelog This project uses [towncrier](https://towncrier.readthedocs.io/) to manage the changelog. Instead of editing `CHANGELOG.md` directly, each PR should include a changelog fragment file in the `.changelog/` directory. **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.added`: ``` `opentelemetry-sdk`: add support for new feature ``` **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 when applicable (e.g. `` `opentelemetry-sdk`: ... ``) - 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` is not directly modified. If your change does not need a changelog entry, add the "Skip Changelog" label to the PR. `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 ``` ### 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. ### Public Symbols We try to keep the amount of _public symbols_ in our code minimal. A public symbol is any Python identifier that does not start with an underscore. Every public symbol is something that has to be kept in order to maintain backwards compatibility, so we try to have as few as possible. To check if your PR is adding public symbols, run `tox -e public-symbols-check`. This will always fail if public symbols are being added/removed. The idea behind this is that every PR that adds/removes public symbols fails in CI, forcing reviewers to check the symbols to make sure they are strictly necessary. If after checking them, it is considered that they are indeed necessary, the PR will be labeled with `Approve Public API check` so that this check is not run. Also, we try to keep our console output as clean as possible. Most of the time this means catching expected log messages in the test cases: ``` python from logging import WARNING ... def test_case(self): with self.assertLogs(level=WARNING): some_function_that_will_log_a_warning_message() ``` Other options can be to disable logging propagation or disabling a logger altogether. A similar approach can be followed to catch warnings: ``` python def test_case(self): with self.assertWarns(DeprecationWarning): some_function_that_will_raise_a_deprecation_warning() ``` See [`tox.ini`](https://github.com/open-telemetry/opentelemetry-python/blob/main/tox.ini) for more detail on available tox commands. ### Contrib repo Some of the `tox` targets install packages from the [OpenTelemetry Python Contrib Repository](https://github.com/open-telemetry/opentelemetry-python.git) 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: ``` CONTRIB_REPO_SHA=dde62cebffe519c35875af6d06fae053b3be65ec tox ``` The continuation integration overrides that environment variable with as per the configuration [here](https://github.com/open-telemetry/opentelemetry-python/blob/main/.github/workflows/test.yml#L14). ### 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. `opentelemetry-sdk/benchmarks/trace/propagation/test_benchmark_b3_format.py`) ## Pull Requests ### How to Structure Pull Requests Smaller PRs get merged faster, improve review quality, and reduce the risk of conflicts. Please keep PRs small and focused: - **Aim for fewer than 500 lines** (excluding deletions, generated files, and test fixtures) - One change per PR -- address a single bug fix, feature, or refactor - If your work requires more extensive changes, consider a series of small, self-contained PRs instead. - If you want the community to understand how your small PR fits into the context of a larger change, consider putting the large change in your personal fork and providing a link to it in your PR description. - Strive to provide clear, incremental value to the repository in your PR, such that the repository is always in a functional, coherent state. ### How to Send Pull Requests Everyone is welcome to contribute code to `opentelemetry-python` via GitHub pull requests (PRs). To create a new PR, fork the project in GitHub and clone the upstream repo: ```console git clone https://github.com/open-telemetry/opentelemetry-python.git cd opentelemetry-python ``` Add your fork as an origin: ```console git remote add fork https://github.com/YOUR_GITHUB_USERNAME/opentelemetry-python.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` 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-sdk: make test_force_flush_late_by_timeout less flaky on pypy/windows" * "opentelemetry-exporter-otlp-proto-http: enable typechecking" * "docs: replace TODO placeholders with API and SDK overview descriptions" * "feat(config): Add TracerProvider support for declarative config" Pull requests are also tested for their compatibility with packages distributed by OpenTelemetry in the [OpenTelemetry Python Contrib Repository](https://github.com/open-telemetry/opentelemetry-python.git). If a pull request (PR) introduces a change that would break the compatibility of these packages with the Core packages in this repo, a separate PR should be opened in the Contrib repo with changes to make the packages compatible. Follow these steps: 1. Open Core repo PR (Contrib Tests will fail) 2. Open Contrib repo PR and modify its `CORE_REPO_SHA` in `.github/workflows/test_x.yml` to equal the commit SHA of the Core repo PR to pass tests 3. Modify the Core repo PR `CONTRIB_REPO_SHA` in `.github/workflows/test_x.yml` to equal the commit SHA of the Contrib repo PR to pass Contrib repo tests (a sanity check for the Maintainers & Approvers) 4. Merge the Contrib repo 5. Restore the Core repo PR `CONTRIB_REPO_SHA` to point to `main` 6. Merge the Core repo PR ### 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 CLA is signed and CI is clear. ### 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. * All tests are passing, including Contrib Repo tests which may require updating the GitHub workflow to reference a PR in the Contrib repo * 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. #### Allow edits from maintainers Something _very important_ is to allow edits from maintainers when opening a PR. This will allow maintainers to rebase your PR against `main` which is necessary in order to merge your PR. You could do it yourself too, but keep in mind that every time another PR gets merged, your PR will require rebasing. Since only maintainers can merge your PR it is almost impossible for maintainers to find your PR just when it has been rebased by you so that it can be merged. Allowing maintainers to edit your PR also allows them to help you get your PR merged by making any minor fixes to solve any issue that while being unrelated to your PR, can still happen. #### Fork from a personal Github account Right now Github [does not allow](https://github.com/orgs/community/discussions/5634) PRs to be edited by maintainers if the corresponding repo fork exists in a Github organization. Please fork this repo in a personal Github account instead. One of the maintainers will merge the PR once it is **ready to merge**. ### 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 ### Environment Variables If you are adding a component that introduces new OpenTelemetry environment variables, put them all in a module, as it is done in `opentelemetry.environment_variables` or in `opentelemetry.sdk.environment_variables`. Keep in mind that any new environment variable must be declared in all caps and must start with `OTEL_PYTHON_`. Register this module with the `opentelemetry_environment_variables` entry point to make your environment variables automatically load as options for the `opentelemetry-instrument` command. ## 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). ## 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 ## Contributions that involve new packages As part of an effort to mitigate namespace squatting on Pypi, please ensure to check whether a package name has been taken already on Pypi before contributing a new package. 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. python-opentelemetry-1.45.2/LICENSE000066400000000000000000000261351527070070700170370ustar00rootroot00000000000000 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. python-opentelemetry-1.45.2/README.md000066400000000000000000000177631527070070700173200ustar00rootroot00000000000000# OpenTelemetry Python [![Slack](https://img.shields.io/badge/slack-@cncf/otel/python-brightgreen.svg?logo=slack)](https://cloud-native.slack.com/archives/C01PD4HUVBL) [![Build Status](https://github.com/open-telemetry/opentelemetry-python/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/open-telemetry/opentelemetry-python/actions/workflows/ci.yml) [![Minimum Python Version](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/) [![Release](https://img.shields.io/github/v/release/open-telemetry/opentelemetry-python?include_prereleases&style=)](https://github.com/open-telemetry/opentelemetry-python/releases/) [![Read the Docs](https://readthedocs.org/projects/opentelemetry-python/badge/?version=latest)](https://opentelemetry-python.readthedocs.io/en/latest/) [![OpenSSF Best Practices](https://www.bestpractices.dev/projects/11060/badge)](https://www.bestpractices.dev/projects/11060) ## Project Status See the [OpenTelemetry Instrumentation for Python](https://opentelemetry.io/docs/instrumentation/python/#status-and-releases). | Signal | Status | Project | | ------- | ------------ | ------- | | Traces | Stable | N/A | | Metrics | Stable | N/A | | Logs | Development* | N/A | Project versioning information and stability guarantees can be found [here](./rationale.md#versioning-and-releasing). ***Breaking Changes** > [!IMPORTANT] > We are working on stabilizing the Log signal which would require making deprecations and breaking changes. We will try to reduce the releases that may require an update to your code, especially for instrumentations or for SDK developers. ## Getting started You can find the getting started guide for OpenTelemetry Python [here](https://opentelemetry.io/docs/languages/python/getting-started/). If you are looking for **examples** on how to use the OpenTelemetry API to instrument your code manually, or how to set up the OpenTelemetry Python SDK, see the [documentation](https://opentelemetry.io/docs/languages/python/instrumentation/). ## Python Version Support This project ensures compatibility with the current supported versions of the Python. As new Python versions are released, support for them is added and as old Python versions reach their end of life, support for them is removed. We add support for new Python versions no later than 3 months after they become stable. We remove support for old Python versions 6 months after they reach their [end of life](https://devguide.python.org/devcycle/#end-of-life-branches). ## Documentation The online documentation is available at https://opentelemetry-python.readthedocs.io/. To access the latest version of the documentation, see https://opentelemetry-python.readthedocs.io/en/latest/. ## Install This repository includes multiple installable packages. The `opentelemetry-api` package includes abstract classes and no-op implementations that comprise the OpenTelemetry API following the [OpenTelemetry specification](https://github.com/open-telemetry/opentelemetry-specification). The `opentelemetry-sdk` package is the reference implementation of the API. 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. The API and SDK packages are available on the Python Package Index (PyPI). You can install them via `pip` with the following commands: ```sh pip install opentelemetry-api pip install opentelemetry-sdk ``` The [`exporter/`](https://github.com/open-telemetry/opentelemetry-python/tree/main/exporter) directory includes OpenTelemetry exporter packages. You can install the packages separately with the following command: ```sh pip install opentelemetry-exporter-{exporter} ``` The [`propagator/`](https://github.com/open-telemetry/opentelemetry-python/tree/main/propagator) directory includes OpenTelemetry propagator packages. You can install the packages separately with the following command: ```sh pip install opentelemetry-propagator-{propagator} ``` To install the development versions of these packages instead, clone or fork this repository and perform an [editable install](https://pip.pypa.io/en/stable/reference/pip_install/#editable-installs): ```sh pip install -e ./opentelemetry-api -e ./opentelemetry-sdk -e ./opentelemetry-semantic-conventions ``` For additional exporter and instrumentation packages, see the [`opentelemetry-python-contrib`](https://github.com/open-telemetry/opentelemetry-python-contrib) repository. ## Contributing For information about contributing to OpenTelemetry Python, see [CONTRIBUTING.md](CONTRIBUTING.md). We meet weekly on Thursdays at 9AM PST. 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 Zoom meeting links. 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 - [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 - [Carlos Alberto Cortez](https://github.com/carlosalberto), Approver - [Chris Kleinknecht](https://github.com/c24t), Maintainer - [Christian Neumüller](https://github.com/Oberon00), Approver - [Jeremy Voss](https://github.com/jeremydvoss), Approver - [Mauricio Vásquez](https://github.com/mauriciovasquezbernal), Approver - [Nathaniel Ruiz Nowell](https://github.com/NathanielRN), Approver - [Nikolay Sokolik](https://github.com/oxeye-nikolay), Approver - [Owais Lone](https://github.com/owais), Maintainer - [Owais Lone](https://github.com/owais), Approver - [Reiley Yang](https://github.com/reyang), Maintainer - [Sanket Mehta](https://github.com/sanketmehta28), Approver - [Shalev Roda](https://github.com/shalevr), Approver - [Srikanth Chekuri](https://github.com/srikanthccv), Maintainer - [Tahir H. Butt](https://github.com/majorgreys), 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 python-opentelemetry-1.45.2/RELEASING.md000066400000000000000000000203451527070070700176620ustar00rootroot00000000000000# Release instructions ## Preparing a new major or minor release * Run the [Prepare release branch workflow](https://github.com/open-telemetry/opentelemetry-python/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-contrib/blob/main/RELEASING.md) for the contrib 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 * Release builds now should pass. * Merge the release PR. * Merge the PR to main (this can be done separately from [making the release](#making-the-release)) * ⚠� **Merge `core` first**: To avoid a merge queue deadlock with `contrib`, you must merge the `core` PR to `main` first. We skip `alls-green` in the core merge queue ([ci.yml](https://github.com/open-telemetry/opentelemetry-python/blob/main/.github/workflows/ci.yml)) for this PR, which unblocks `contrib` once merged. ## 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/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/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 `-core` repo, you should also do an equivalent patch release in `-contrib` repo (even if there's no fix to release), otherwise tests in CI will fail. ### Note on `contrib.yml` Workflow Behavior The [contrib.yml](https://github.com/open-telemetry/opentelemetry-python/blob/main/.github/workflows/contrib.yml) workflow in the core repository references reusable workflows from opentelemetry-python-contrib using the hard-coded `main` branch. Because `uses:` statements cannot receive environment variables and workflows cannot patch or modify other workflows, this reference cannot dynamically follow release branches as we are doing in other workflows. As a result, when preparing a release branch that contains a different set of instrumentations (e.g., older branches without newly added tox environments), CI may attempt to run tests that do not exist on tox in that branch. In this case: * It is safe to merge the release PR even if the contrib workflow fails for this reason, or * Optionally update the contrib.yml workflow to point to the corresponding release branch before running CI. ## Making the release * Run the [Release workflow](https://github.com/open-telemetry/opentelemetry-python/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. * Verify that a new [Github release](https://github.com/open-telemetry/opentelemetry-python/releases) has been created and that the CHANGELOGs look correct. ## After the release * Check PyPI * This should be handled automatically on release by the [publish action](https://github.com/open-telemetry/opentelemetry-python/blob/main/.github/workflows/release.yml). * Check the [action logs](https://github.com/open-telemetry/opentelemetry-python/actions?query=workflow%3APublish) to make sure packages have been uploaded to PyPI * Check the release history (e.g. https://pypi.org/project/opentelemetry-api/#history) on PyPI * If for some reason the action failed, see [Publish failed](#publish-failed) below * Move stable tag and kick-off documentation build * Run the following (TODO automate): ```bash git tag -d stable git tag stable git push --delete origin stable git push origin tag stable ``` * ReadTheDocs will not automatically rebuild on tag changes, so manually kick-off a build of stable: https://readthedocs.org/projects/opentelemetry-python/builds/. ![ReadTheDocs build instructions](.github/rtd-build-instructions.png) * This will ensure that ReadTheDocs for core are pointing at the stable release. ## 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! python-opentelemetry-1.45.2/codegen/000077500000000000000000000000001527070070700174275ustar00rootroot00000000000000python-opentelemetry-1.45.2/codegen/opentelemetry-codegen-json/000077500000000000000000000000001527070070700246745ustar00rootroot00000000000000python-opentelemetry-1.45.2/codegen/opentelemetry-codegen-json/.gitignore000066400000000000000000000000361527070070700266630ustar00rootroot00000000000000tests/generated/otel_test_jsonpython-opentelemetry-1.45.2/codegen/opentelemetry-codegen-json/README.rst000066400000000000000000000005171527070070700263660ustar00rootroot00000000000000OpenTelemetry JSON Code Generator ================================= .. note:: This is an internal package and is not intended for publication to PyPI. This library is a protocol buffer plugin that generates code for the OpenTelemetry protocol in JSON format. References ---------- * `OpenTelemetry `_ python-opentelemetry-1.45.2/codegen/opentelemetry-codegen-json/pyproject.toml000066400000000000000000000027271527070070700276200ustar00rootroot00000000000000[build-system] requires = ["hatchling"] build-backend = "hatchling.build" [project] name = "opentelemetry-codegen-json" version = "0.0.0" # Not published to PyPI description = "Protobuf plugin to generate JSON serializers and deserializers for OpenTelemetry protobuf messages" 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", "Framework :: OpenTelemetry", "Intended Audience :: Developers", "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", "Private :: Do Not Upload", ] dependencies = [ "protobuf>=4.25.3", "types-protobuf>=4.24", ] [project.scripts] protoc-gen-otlp_json = "opentelemetry.codegen.json.plugin:main" [project.urls] Homepage = "https://github.com/open-telemetry/opentelemetry-python/tree/main/codegen/opentelemetry-codegen-json" Repository = "https://github.com/open-telemetry/opentelemetry-python" [tool.hatch.version] path = "src/opentelemetry/codegen/json/version/__init__.py" [tool.hatch.build.targets.sdist] include = [ "/src", ] [tool.hatch.build.targets.wheel] packages = ["src/opentelemetry"] python-opentelemetry-1.45.2/codegen/opentelemetry-codegen-json/src/000077500000000000000000000000001527070070700254635ustar00rootroot00000000000000python-opentelemetry-1.45.2/codegen/opentelemetry-codegen-json/src/opentelemetry/000077500000000000000000000000001527070070700303575ustar00rootroot00000000000000python-opentelemetry-1.45.2/codegen/opentelemetry-codegen-json/src/opentelemetry/codegen/000077500000000000000000000000001527070070700317635ustar00rootroot00000000000000python-opentelemetry-1.45.2/codegen/opentelemetry-codegen-json/src/opentelemetry/codegen/__init__.py000066400000000000000000000000001527070070700340620ustar00rootroot00000000000000python-opentelemetry-1.45.2/codegen/opentelemetry-codegen-json/src/opentelemetry/codegen/json/000077500000000000000000000000001527070070700327345ustar00rootroot00000000000000__init__.py000066400000000000000000000000001527070070700347540ustar00rootroot00000000000000python-opentelemetry-1.45.2/codegen/opentelemetry-codegen-json/src/opentelemetry/codegen/jsongenerator.py000066400000000000000000001042261527070070700352220ustar00rootroot00000000000000python-opentelemetry-1.45.2/codegen/opentelemetry-codegen-json/src/opentelemetry/codegen/json# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # pylint: disable=no-member,invalid-name,too-many-lines from __future__ import annotations import logging from collections import defaultdict from collections.abc import Callable, Iterable from pathlib import Path from typing import Final from google.protobuf import descriptor_pb2 as descriptor from google.protobuf.compiler import plugin_pb2 as plugin from opentelemetry.codegen.json.types import ( get_default_value, get_json_allowed_types, get_python_type, is_bytes_type, is_hex_encoded_field, is_int64_type, to_json_field_name, ) from opentelemetry.codegen.json.writer import CodeWriter _logger = logging.getLogger(__name__) CODEC_MODULE_NAME: Final[str] = "_json_codec" class OtlpJsonGenerator: """ Generates Python dataclasses and JSON serialization/deserialization code from protobuf descriptors. """ def __init__( self, request: plugin.CodeGeneratorRequest, package_transform: Callable[[str], str], version: str, ) -> None: """ Initialize the generator. Args: request: Protobuf compiler plugin request package_transform: A callable that transforms the proto file path. version: Version string for the generated code. """ self._request = request self._package_transform = package_transform self._version = version self._generated_files: dict[str, str] = {} self._common_root: str = "" self._file_to_proto: dict[str, descriptor.FileDescriptorProto] = {f.name: f for f in request.proto_file} self._fqn_to_file: dict[str, str] = {} self._fqn_to_class_path: dict[str, str] = {} self._file_dependencies: dict[str, list[str]] = {f.name: list(f.dependency) for f in request.proto_file} for proto_file in request.proto_file: self._index_file(proto_file) def generate_all(self) -> dict[str, str]: """ Generate Python code for all proto files and support modules. Returns: Dictionary mapping output file paths to generated code """ files_to_generate = self._request.file_to_generate file_to_output = {proto_file: self._transform_proto_path(proto_file) for proto_file in files_to_generate} if not file_to_output: return {} self._common_root = _find_common_root(file_to_output.values()) for proto_file in files_to_generate: file_desc = self._file_to_proto[proto_file] code = self._generate_file(file_desc) output_path = file_to_output[proto_file] self._generated_files[output_path] = code codec_path = f"{self._common_root}/{CODEC_MODULE_NAME}.py" self._generated_files[codec_path] = _load_codec_source() version_init_path = f"{self._common_root}/version/__init__.py" version_writer = CodeWriter(indent_size=4) self._generate_header(version_writer) version_writer.writemany(f'__version__ = "{self._version}"', "") self._generated_files[version_init_path] = version_writer.to_string() self._ensure_init_files() return self._generated_files def _index_file(self, file_desc: descriptor.FileDescriptorProto) -> None: """ Index all messages and enums in the file for usage during generation. Args: file_desc: File descriptor to index """ package = file_desc.package for enum_desc in file_desc.enum_type: fqn = f"{package}.{enum_desc.name}" if package else enum_desc.name self._fqn_to_file[fqn] = file_desc.name self._fqn_to_class_path[fqn] = enum_desc.name for msg_desc in file_desc.message_type: self._index_message(msg_desc, package, file_desc.name, None) def _index_message( self, msg_desc: descriptor.DescriptorProto, package: str, file_name: str, parent_path: str | None, ) -> None: """ Recursively index a message and its nested types. Args: msg_desc: Message descriptor to index package: Protobuf package name for the message file_name: Proto file where the message is defined parent_path: Full parent class path for nested messages """ current_path = f"{parent_path}.{msg_desc.name}" if parent_path else msg_desc.name fqn = f"{package}.{current_path}" if package else current_path self._fqn_to_file[fqn] = file_name self._fqn_to_class_path[fqn] = current_path for enum_desc in msg_desc.enum_type: enum_fqn = f"{fqn}.{enum_desc.name}" self._fqn_to_file[enum_fqn] = file_name self._fqn_to_class_path[enum_fqn] = f"{current_path}.{enum_desc.name}" for nested_msg in msg_desc.nested_type: if not nested_msg.options.map_entry: self._index_message(nested_msg, package, file_name, current_path) def _ensure_init_files(self) -> None: """ Ensure that every directory in the output path contains an __init__.py file. """ dirs = set() for path in self._generated_files: p = Path(path) for parent in p.parents: parent_str = str(parent) # Skip '.', root, and the 'opentelemetry' namespace directory if parent_str in (".", "/", "opentelemetry"): continue dirs.add(parent_str) for d in dirs: init_path = f"{d}/__init__.py" if init_path not in self._generated_files: self._generated_files[init_path] = "" def _get_codec_module_path(self) -> str: """ Get the absolute module path for the codec module. Returns: Absolute module path as a string """ return f"{self._common_root.replace('/', '.')}.{CODEC_MODULE_NAME}" if self._common_root else CODEC_MODULE_NAME def _transform_proto_path(self, proto_path: str) -> str: """ Transform proto file path to output Python file path. Example: 'opentelemetry/proto/trace/v1/trace.proto' -> 'opentelemetry/proto_json/trace/v1/trace.py' Args: proto_path: Original .proto file path Returns: Transformed .py file path """ transformed = self._package_transform(proto_path) if transformed.endswith(".proto"): transformed = transformed[:-6] + ".py" return transformed def _get_module_path(self, proto_file: str) -> str: """ Convert a proto file path to its transformed Python module path. Example: 'opentelemetry/proto/common/v1/common.proto' -> 'opentelemetry.proto_json.common.v1.common' Args: proto_file: Original .proto file path Returns: Python module path (dot-separated) """ transformed = self._transform_proto_path(proto_file) transformed = transformed.removesuffix(".py") return transformed.replace("/", ".") def _generate_file(self, file_desc: descriptor.FileDescriptorProto) -> str: """ Generate complete Python file for a proto file. Args: file_desc: File descriptor Returns: Generated Python code as string """ writer = CodeWriter(indent_size=4) proto_file = file_desc.name self._generate_header(writer, proto_file) self._generate_imports(writer, proto_file, self._file_has_enums(file_desc)) self._generate_enums_for_file(writer, file_desc.enum_type) self._generate_messages_for_file(writer, proto_file, file_desc.message_type) writer.blank_line() return writer.to_string() def _file_has_enums(self, file_desc: descriptor.FileDescriptorProto) -> bool: """ Check if the file or any of its messages (recursively) contain enums. Args: file_desc: File descriptor to check Returns: True if any enums are found, False otherwise """ if file_desc.enum_type: return True for msg in file_desc.message_type: if self._msg_has_enums(msg): return True return False def _msg_has_enums(self, msg_desc: descriptor.DescriptorProto) -> bool: """ Recursively check if the message or any of its nested messages contain enums. Args: msg_desc: Message descriptor to check Returns: True if any enums are found, False otherwise """ if msg_desc.enum_type: return True for nested in msg_desc.nested_type: if self._msg_has_enums(nested): return True return False @classmethod def _generate_header(cls, writer: CodeWriter, proto_file: str = "") -> None: """ Generate file header with license and metadata. Args: writer: Code writer instance proto_file: Original proto file path (optional) """ writer.comment( [ "Copyright The OpenTelemetry Authors", "SPDX-License-Identifier: Apache-2.0", ] ) writer.blank_line() if proto_file: writer.comment(f'AUTO-GENERATED from "{proto_file}"') writer.comment("DO NOT EDIT MANUALLY") writer.blank_line() def _generate_imports( self, writer: CodeWriter, proto_file: str, include_enum: bool, ) -> None: """ Generate all necessary import statements. Args: writer: Code writer instance proto_file: Original proto file path include_enum: Whether to include the enum module import """ writer.import_("__future__", "annotations") writer.blank_line() std_imports = [ "builtins", "dataclasses", "functools", "typing", ] if include_enum: std_imports.append("enum") for module in sorted(std_imports): writer.import_(module) writer.blank_line() writer.assignment( "_dataclass", "functools.partial(dataclasses.dataclass, slots=True)", ) writer.blank_line() # Collect all imports needed imports = self._collect_imports(proto_file) imports.add(f"import {self._get_codec_module_path()}") # Generate cross file imports if imports: for import_info in sorted(imports): writer.writeln(import_info) writer.blank_line() writer.blank_line() def _collect_imports(self, proto_file: str) -> set[str]: """ Collect all import statements needed for cross file references. Args: proto_file: Current proto file path Returns: Set of import statement strings """ return set( "import " + self._get_module_path(dep_file) for dep_file in self._file_dependencies.get(proto_file, []) ) def _generate_enums_for_file( self, writer: CodeWriter, enum_descs: Iterable[descriptor.EnumDescriptorProto], ) -> None: """ Generate all enums for a file (top level and nested). Args: writer: Code writer instance enum_descs: List of top level enums """ for enum_desc in enum_descs: self._generate_enum_class(writer, enum_desc) writer.blank_line() def _generate_messages_for_file( self, writer: CodeWriter, proto_file: str, msg_descs: Iterable[descriptor.DescriptorProto], ) -> None: """ Generate all message classes for a file. Args: writer: Code writer instance proto_file: Original proto file path msg_descs: List of top level messages """ for i, message in enumerate(msg_descs): if i: writer.blank_line(2) self._generate_message_class(writer, proto_file, message) def _generate_message_class( self, writer: CodeWriter, proto_file: str, msg_desc: descriptor.DescriptorProto, parent_path: str | None = None, ) -> None: """ Generate a complete dataclass for a protobuf message. Args: writer: Code writer instance proto_file: Original proto file path msg_desc: Message descriptor parent_path: Full parent class path for nested messages """ current_path = f"{parent_path}.{msg_desc.name}" if parent_path else msg_desc.name codec = self._get_codec_module_path() with writer.dataclass( msg_desc.name, bases=(f"{codec}.JsonMessage",), decorators=("typing.final",), decorator_name="_dataclass", ): if msg_desc.field or msg_desc.nested_type or msg_desc.enum_type: writer.docstring([f"Generated from protobuf message {msg_desc.name}"]) writer.blank_line() for enum_desc in msg_desc.enum_type: self._generate_enum_class(writer, enum_desc) writer.blank_line() for nested_desc in msg_desc.nested_type: if not nested_desc.options.map_entry: self._generate_message_class(writer, proto_file, nested_desc, current_path) writer.blank_line() if msg_desc.field: for field_desc in msg_desc.field: self._generate_field(writer, proto_file, field_desc) else: writer.pass_() writer.blank_line() self._generate_to_dict(writer, msg_desc) writer.blank_line() self._generate_from_dict(writer, proto_file, msg_desc, current_path) @classmethod def _generate_enum_class(cls, writer: CodeWriter, enum_desc: descriptor.EnumDescriptorProto) -> None: """ Generate an IntEnum class for a protobuf enum. Args: writer: Code writer instance enum_desc: Enum descriptor """ with writer.enum( enum_desc.name, enum_type="enum.IntEnum", decorators=("typing.final",), ): writer.docstring([f"Generated from protobuf enum {enum_desc.name}"]) writer.blank_line() if enum_desc.value: for val_desc in enum_desc.value: writer.enum_member(val_desc.name, val_desc.number) else: writer.pass_() def _generate_field( self, writer: CodeWriter, proto_file: str, field_desc: descriptor.FieldDescriptorProto, ) -> None: """ Generate a dataclass field. Args: writer: Code writer instance proto_file: Original proto file path field_desc: Field descriptor """ type_hint = self._get_field_type_hint(proto_file, field_desc) writer.field( field_desc.name, type_hint, default=self._get_field_default(field_desc), ) def _generate_to_dict( self, writer: CodeWriter, msg_desc: descriptor.DescriptorProto, ) -> None: """ Generate a to_dict() method that converts the dataclass instance to a dictionary Args: writer: Code writer instance msg_desc: Message descriptor for the class being generated """ with writer.method( "to_dict", ["self"], return_type="builtins.dict[builtins.str, typing.Any]", ): writer.docstring( [ "Convert this message to a dictionary with lowerCamelCase keys.", "", "Returns:", " Dictionary representation following OTLP JSON encoding", ] ) writer.assignment("_result", "{}") # Separate fields into oneof groups and standalone fields oneof_groups: dict[int, list[descriptor.FieldDescriptorProto]] = defaultdict(list) standalone_fields: list[descriptor.FieldDescriptorProto] = [] for field in msg_desc.field: if field.HasField("oneof_index") and not field.proto3_optional: oneof_groups[field.oneof_index].append(field) else: standalone_fields.append(field) for field in standalone_fields: with writer.if_(f"self.{field.name} is not None" if field.proto3_optional else f"self.{field.name}"): self._generate_serialization_statements(writer, field, "_result") for group_index in sorted(oneof_groups.keys()): group_fields = oneof_groups[group_index] for i, field in enumerate(reversed(group_fields)): condition = f"self.{field.name} is not None" context = writer.elif_(condition) if i else writer.if_(condition) with context: self._generate_serialization_statements(writer, field, "_result") writer.return_("_result") def _generate_from_dict( self, writer: CodeWriter, proto_file: str, msg_desc: descriptor.DescriptorProto, current_path: str, ) -> None: """ Generate a from_dict() class method that creates an instance from a dictionary. Args: writer: Code writer instance proto_file: Original proto file path msg_desc: Message descriptor for the class being generated current_path: Full class path for type hints and return type """ with writer.method( "from_dict", ["cls", "data: builtins.dict[builtins.str, typing.Any]"], decorators=["builtins.classmethod"], return_type=f'"{current_path}"', ): writer.docstring( [ "Create from a dictionary with lowerCamelCase keys.", "", "Args:", " data: Dictionary representation following OTLP JSON encoding", "", "Returns:", f" {msg_desc.name} instance", ] ) codec = self._get_codec_module_path() writer.writeln(f'{codec}.validate_type(data, builtins.dict, "data")') writer.assignment("_args", "{}") writer.blank_line() # Separate fields into oneof groups and standalone fields oneof_groups: dict[int, list[descriptor.FieldDescriptorProto]] = defaultdict(list) standalone_fields: list[descriptor.FieldDescriptorProto] = [] for field in msg_desc.field: if field.HasField("oneof_index") and not field.proto3_optional: oneof_groups[field.oneof_index].append(field) else: standalone_fields.append(field) # Handle standalone fields for field in standalone_fields: json_name = field.json_name if field.json_name else to_json_field_name(field.name) with writer.if_(f'(_value := data.get("{json_name}")) is not None'): self._generate_deserialization_statements(writer, proto_file, field, "_value", "_args") # Handle oneof groups for group_index in sorted(oneof_groups.keys()): group_fields = oneof_groups[group_index] for i, field in enumerate(reversed(group_fields)): json_name = field.json_name if field.json_name else to_json_field_name(field.name) condition = f'(_value := data.get("{json_name}")) is not None' context = writer.elif_(condition) if i else writer.if_(condition) with context: self._generate_deserialization_statements(writer, proto_file, field, "_value", "_args") writer.blank_line() writer.return_("cls(**_args)") def _generate_serialization_statements( self, writer: CodeWriter, field_desc: descriptor.FieldDescriptorProto, target_dict: str, ) -> None: """ Generate statements to serialize a field and assign it to the target dictionary. Args: writer: Code writer instance field_desc: Field descriptor for the field being serialized target_dict: Name of the dictionary variable to assign the serialized value to """ json_name = field_desc.json_name if field_desc.json_name else to_json_field_name(field_desc.name) if field_desc.label == descriptor.FieldDescriptorProto.LABEL_REPEATED: item_expr = self._get_serialization_expr(field_desc, "_v") if item_expr == "_v": writer.assignment(f'{target_dict}["{json_name}"]', f"self.{field_desc.name}") else: codec = self._get_codec_module_path() writer.assignment( f'{target_dict}["{json_name}"]', f"{codec}.encode_repeated(self.{field_desc.name}, lambda _v: {item_expr})", ) else: val_expr = self._get_serialization_expr(field_desc, f"self.{field_desc.name}") writer.assignment(f'{target_dict}["{json_name}"]', val_expr) # pylint: disable-next=too-many-return-statements def _get_serialization_expr(self, field_desc: descriptor.FieldDescriptorProto, var_name: str) -> str: """ Get the Python expression to serialize a value of a given type for JSON output. Args: field_desc: Field descriptor for the value being serialized var_name: Variable name representing the value to serialize """ codec = self._get_codec_module_path() if field_desc.type == descriptor.FieldDescriptorProto.TYPE_MESSAGE: return f"{var_name}.to_dict()" if field_desc.type == descriptor.FieldDescriptorProto.TYPE_ENUM: return f"builtins.int({var_name})" if is_hex_encoded_field(field_desc.name): return f"{codec}.encode_hex({var_name})" if is_int64_type(field_desc.type): return f"{codec}.encode_int64({var_name})" if is_bytes_type(field_desc.type): return f"{codec}.encode_base64({var_name})" if field_desc.type in ( descriptor.FieldDescriptorProto.TYPE_FLOAT, descriptor.FieldDescriptorProto.TYPE_DOUBLE, ): return f"{codec}.encode_float({var_name})" return var_name def _generate_deserialization_statements( self, writer: CodeWriter, proto_file: str, field_desc: descriptor.FieldDescriptorProto, var_name: str, target_dict: str, ) -> None: """ Generate statements to deserialize a field from a JSON value and assign it to the target dictionary. Args: writer: Code writer instance proto_file: Original proto file path field_desc: Field descriptor for the field being deserialized var_name: Variable name representing the JSON value to deserialize target_dict: Name of the dictionary variable to assign the deserialized value to """ codec = self._get_codec_module_path() if field_desc.label == descriptor.FieldDescriptorProto.LABEL_REPEATED: item_expr = self._get_deserialization_expr(proto_file, field_desc, "_v") writer.assignment( f'{target_dict}["{field_desc.name}"]', f'{codec}.decode_repeated({var_name}, lambda _v: {item_expr}, "{field_desc.name}")', ) return if field_desc.type == descriptor.FieldDescriptorProto.TYPE_MESSAGE: msg_type = self._resolve_message_type(field_desc.type_name, proto_file) writer.assignment( f'{target_dict}["{field_desc.name}"]', f"{msg_type}.from_dict({var_name})", ) elif field_desc.type == descriptor.FieldDescriptorProto.TYPE_ENUM: enum_type = self._resolve_enum_type(field_desc.type_name, proto_file) writer.writeln(f'{codec}.validate_type({var_name}, builtins.int, "{field_desc.name}")') writer.assignment( f'{target_dict}["{field_desc.name}"]', f"{enum_type}({var_name})", ) elif is_hex_encoded_field(field_desc.name): writer.assignment( f'{target_dict}["{field_desc.name}"]', f'{codec}.decode_hex({var_name}, "{field_desc.name}")', ) elif is_int64_type(field_desc.type): writer.assignment( f'{target_dict}["{field_desc.name}"]', f'{codec}.decode_int64({var_name}, "{field_desc.name}")', ) elif is_bytes_type(field_desc.type): writer.assignment( f'{target_dict}["{field_desc.name}"]', f'{codec}.decode_base64({var_name}, "{field_desc.name}")', ) elif field_desc.type in ( descriptor.FieldDescriptorProto.TYPE_FLOAT, descriptor.FieldDescriptorProto.TYPE_DOUBLE, ): writer.assignment( f'{target_dict}["{field_desc.name}"]', f'{codec}.decode_float({var_name}, "{field_desc.name}")', ) else: allowed_types = get_json_allowed_types(field_desc.type, field_desc.name) writer.writeln(f'{codec}.validate_type({var_name}, {allowed_types}, "{field_desc.name}")') writer.assignment(f'{target_dict}["{field_desc.name}"]', var_name) # pylint: disable-next=too-many-return-statements def _get_deserialization_expr( self, proto_file: str, field_desc: descriptor.FieldDescriptorProto, var_name: str, ) -> str: """ Get the Python expression to deserialize a value of a given type for JSON input. Args: proto_file: Original proto file path field_desc: Field descriptor for the value being deserialized var_name: Variable name representing the JSON value to deserialize Returns: Python expression string to perform deserialization """ codec = self._get_codec_module_path() if field_desc.type == descriptor.FieldDescriptorProto.TYPE_MESSAGE: msg_type = self._resolve_message_type(field_desc.type_name, proto_file) return f"{msg_type}.from_dict({var_name})" if field_desc.type == descriptor.FieldDescriptorProto.TYPE_ENUM: enum_type = self._resolve_enum_type(field_desc.type_name, proto_file) return f"{enum_type}({var_name})" if is_hex_encoded_field(field_desc.name): return f'{codec}.decode_hex({var_name}, "{field_desc.name}")' if is_int64_type(field_desc.type): return f'{codec}.decode_int64({var_name}, "{field_desc.name}")' if is_bytes_type(field_desc.type): return f'{codec}.decode_base64({var_name}, "{field_desc.name}")' if field_desc.type in ( descriptor.FieldDescriptorProto.TYPE_FLOAT, descriptor.FieldDescriptorProto.TYPE_DOUBLE, ): return f'{codec}.decode_float({var_name}, "{field_desc.name}")' return var_name def _get_field_type_hint(self, proto_file: str, field_desc: descriptor.FieldDescriptorProto) -> str: """ Get the Python type hint for a field. Args: proto_file: Original proto file path field_desc: Field descriptor Returns: Python type hint string """ if field_desc.type == descriptor.FieldDescriptorProto.TYPE_MESSAGE: base_type = self._resolve_message_type(field_desc.type_name, proto_file) elif field_desc.type == descriptor.FieldDescriptorProto.TYPE_ENUM: base_type = self._resolve_enum_type(field_desc.type_name, proto_file) else: base_type = get_python_type(field_desc.type) if field_desc.label == descriptor.FieldDescriptorProto.LABEL_REPEATED: return f"builtins.list[{base_type}]" if field_desc.type == descriptor.FieldDescriptorProto.TYPE_ENUM: return f"{base_type} | builtins.int | None" return f"{base_type} | None" def _resolve_message_type(self, type_name: str, proto_file: str) -> str: """ Resolve a message type name to its Python class path. Args: type_name: Fully qualified proto name proto_file: Current proto file path Returns: Python class reference """ fqn = type_name.lstrip(".") target_file = self._fqn_to_file.get(fqn) if not target_file: _logger.warning("Could not resolve message type: %s", type_name) return "typing.Any" class_path = self._fqn_to_class_path[fqn] # If in same file, use relative class path if target_file == proto_file: return class_path # Cross file reference - use fully qualified module + class path module_path = self._get_module_path(target_file) return f"{module_path}.{class_path}" def _resolve_enum_type(self, type_name: str, proto_file: str) -> str: """ Resolve an enum type name to its Python class path. Args: type_name: Fully qualified proto name proto_file: Current proto file path Returns: Python class reference """ fqn = type_name.lstrip(".") target_file = self._fqn_to_file.get(fqn) if not target_file: _logger.warning("Could not resolve enum type: %s", type_name) return "builtins.int" class_path = self._fqn_to_class_path[fqn] # If in same file, use relative class path if target_file == proto_file: return class_path # Cross file reference - use fully qualified module + class path module_path = self._get_module_path(target_file) return f"{module_path}.{class_path}" @classmethod def _get_field_default(cls, field_desc: descriptor.FieldDescriptorProto) -> str | None: """ Get the default value for a field. Args: field_desc: Field descriptor Returns: Default value string or None """ # Repeated fields default to empty list if field_desc.label == descriptor.FieldDescriptorProto.LABEL_REPEATED: return "dataclasses.field(default_factory=builtins.list)" # Optional fields, Message types, and oneof members default to None if ( field_desc.type == descriptor.FieldDescriptorProto.TYPE_MESSAGE or field_desc.HasField("oneof_index") or field_desc.proto3_optional ): return "None" # Enum types default to 0 if field_desc.type == descriptor.FieldDescriptorProto.TYPE_ENUM: return "0" # Primitive types use proto defaults return get_default_value(field_desc.type) def _load_codec_source() -> str: """ Load the source code for the codec module from its source file. Returns: Source code as a string """ codec_src_path = Path(__file__).parent / "runtime" / "json_codec.py" try: return codec_src_path.read_text(encoding="utf-8") except Exception as e: _logger.error( "Failed to load codec module source from %s: %s", codec_src_path, e, ) raise RuntimeError(f"Failed to load codec module source from {codec_src_path}") from e def _find_common_root(paths: Iterable[str]) -> str: """ Find the longest common directory prefix among the given paths. Args: paths: Iterable of file paths to analyze Returns: Common directory prefix as a string """ if not paths: return "" # Split paths into components split_paths = [p.split("/")[:-1] for p in paths] if not split_paths: return "" # Find common prefix among components common = [] for parts in zip(*split_paths): if all(p == parts[0] for p in parts): common.append(parts[0]) else: break return "/".join(common) def generate_code( request: plugin.CodeGeneratorRequest, package_transform: Callable[[str], str] = lambda p: p.replace("opentelemetry/proto/", "opentelemetry/proto_json/"), ) -> dict[str, str]: """ Main entry point for code generation. Args: request: Protobuf compiler plugin request package_transform: Package transformation string or callable Returns: Dictionary mapping output file paths to generated code """ generator = OtlpJsonGenerator(request, package_transform, version="0.0.0") return generator.generate_all() def generate_plugin_response( request: plugin.CodeGeneratorRequest, package_transform: Callable[[str], str] = lambda p: p.replace("opentelemetry/proto/", "opentelemetry/proto_json/"), ) -> plugin.CodeGeneratorResponse: """ Generate plugin response with all generated files. Args: request: Protobuf compiler plugin request package_transform: Package transformation string Returns: Plugin response with generated files """ response = plugin.CodeGeneratorResponse() # Declare support for optional proto3 fields response.supported_features |= plugin.CodeGeneratorResponse.FEATURE_PROTO3_OPTIONAL response.supported_features |= plugin.CodeGeneratorResponse.FEATURE_SUPPORTS_EDITIONS response.minimum_edition = descriptor.EDITION_LEGACY response.maximum_edition = descriptor.EDITION_2024 # Generate code generated_files = generate_code(request, package_transform) # Create response files for output_path, code in generated_files.items(): file_response = response.file.add() file_response.name = output_path file_response.content = code return response plugin.py000066400000000000000000000030131527070070700345220ustar00rootroot00000000000000python-opentelemetry-1.45.2/codegen/opentelemetry-codegen-json/src/opentelemetry/codegen/json# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # pylint: disable=no-member import logging import sys from collections.abc import Iterator from contextlib import contextmanager from google.protobuf.compiler import plugin_pb2 as plugin from opentelemetry.codegen.json.generator import generate_plugin_response _logger = logging.getLogger(__name__) @contextmanager def code_generation() -> Iterator[tuple[plugin.CodeGeneratorRequest, plugin.CodeGeneratorResponse],]: """ Context manager for handling the code generation process. """ if len(sys.argv) > 1 and sys.argv[1] in ("-V", "--version"): print("opentelemetry-codegen-json 0.0.0") sys.exit(0) data = sys.stdin.buffer.read() request = plugin.CodeGeneratorRequest() request.ParseFromString(data) response = plugin.CodeGeneratorResponse() yield request, response output = response.SerializeToString() sys.stdout.buffer.write(output) def main() -> None: """ Main entry point for the protoc plugin. """ with code_generation() as (request, response): generated_response = generate_plugin_response(request) response.supported_features |= generated_response.supported_features for file in generated_response.file: response.file.add().CopyFrom(file) if __name__ == "__main__": logging.basicConfig( level=logging.DEBUG, stream=sys.stderr, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", ) main() runtime/000077500000000000000000000000001527070070700343405ustar00rootroot00000000000000python-opentelemetry-1.45.2/codegen/opentelemetry-codegen-json/src/opentelemetry/codegen/json__init__.py000066400000000000000000000000001527070070700364370ustar00rootroot00000000000000python-opentelemetry-1.45.2/codegen/opentelemetry-codegen-json/src/opentelemetry/codegen/json/runtimejson_codec.py000066400000000000000000000150451527070070700370250ustar00rootroot00000000000000python-opentelemetry-1.45.2/codegen/opentelemetry-codegen-json/src/opentelemetry/codegen/json/runtime# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations import abc import base64 import collections.abc import json import math import typing from typing_extensions import Self T = typing.TypeVar("T") M = typing.TypeVar("M", bound="JsonMessage") class JsonMessage(abc.ABC): """ Abstract base class for protobuf messages with JSON serialization. """ @abc.abstractmethod def to_dict(self) -> dict[str, typing.Any]: """ Convert this message to a dictionary. """ @classmethod @abc.abstractmethod def from_dict(cls: type[M], data: dict[str, typing.Any]) -> M: """ Create an instance from a dictionary. """ def to_json(self) -> str: """ Serialize this message to a JSON string. """ return json.dumps(self.to_dict()) @classmethod def from_json(cls, data: str | bytes) -> Self: """ Deserialize from a JSON string or bytes. """ return cls.from_dict(json.loads(data)) def encode_hex(value: bytes | None) -> str: """ Encode bytes as hex string. Args: value: The bytes to encode. Returns: Hex string representation of the input bytes. """ return value.hex() if value else "" def encode_base64(value: bytes | None) -> str: """ Encode bytes as base64 string. Standard Proto3 JSON mapping for bytes. Args: value: The bytes to encode. Returns: Base64 string representation of the input bytes. """ return base64.b64encode(value).decode("utf-8") if value else "" def encode_int64(value: int) -> str: """ Encode 64 bit integers as strings. Required for int64, uint64, fixed64, sfixed64 and sint64 per Proto3 JSON spec. Args: value: The integer to encode. Returns: String representation of the input integer. """ return str(value) def encode_float(value: float) -> float | str: """ Encode float/double values. Args: value: The float to encode. Returns: The input value, or a string for special float values (NaN, Infinity). """ if math.isnan(value): return "NaN" if math.isinf(value): return "Infinity" if value > 0 else "-Infinity" return value def encode_repeated( values: list[T] | None, map_fn: collections.abc.Callable[[T], typing.Any], ) -> list[typing.Any]: """ Helper to serialize repeated fields with a mapping function. Args: values: The list of values to encode. map_fn: A function that takes a single value and returns its encoded form. Returns: A list of encoded values, or an empty list if input is None or empty. """ return [map_fn(v) for v in values] if values else [] def decode_hex(value: str | None, field_name: str) -> bytes: """ Decode hex string to bytes. Args: value: The hex string to decode. field_name: The name of the field being decoded (for error messages). Returns: The decoded bytes, or empty bytes if input is None or empty. """ if not value: return b"" validate_type(value, str, field_name) try: return bytes.fromhex(value) except ValueError as error: raise ValueError(f"Invalid hex string for field '{field_name}': {error}") from None def decode_base64(value: str | None, field_name: str) -> bytes: """ Decode base64 string to bytes. Args: value: The base64 string to decode. field_name: The name of the field being decoded (for error messages). Returns: The decoded bytes, or empty bytes if input is None or empty. """ if not value: return b"" validate_type(value, str, field_name) try: return base64.b64decode(value) except Exception as error: raise ValueError(f"Invalid base64 string for field '{field_name}': {error}") from None def decode_int64(value: int | str | None, field_name: str) -> int: """ Parse int64 from number or string. Args: value: The value to decode, which can be an int, a string, or None field_name: The name of the field being decoded (for error messages). Returns: The decoded integer, or 0 if input is None. """ if value is None: return 0 validate_type(value, (int, str), field_name) try: return int(value) except (ValueError, TypeError): raise ValueError(f"Invalid int64 value for field '{field_name}': {value}") from None def decode_float(value: float | str | None, field_name: str) -> float: """ Parse float/double from number or string, handling special values. Args: value: The value to decode, which can be a float, int, string, or None field_name: The name of the field being decoded (for error messages). Returns: The decoded float, or 0.0 if input is None. """ if value is None: return 0.0 validate_type(value, (float, int, str), field_name) if value == "NaN": return math.nan if value == "Infinity": return math.inf if value == "-Infinity": return -math.inf try: return float(value) except (ValueError, TypeError): raise ValueError(f"Invalid float value for field '{field_name}': {value}") from None def decode_repeated( values: list[typing.Any] | None, item_parser: collections.abc.Callable[[typing.Any], T], field_name: str, ) -> list[T]: """ Parse a list of values using the provided item parser function. Args: values: The list of values to decode, or None. item_parser: A function that takes a single value and returns the parsed form. field_name: The name of the field being decoded (for error messages). Returns: A list of parsed values, or an empty list if input is None. """ if values is None: return [] validate_type(values, list, field_name) return [item_parser(v) for v in values] def validate_type( value: typing.Any, expected_types: type | tuple[type, ...], field_name: str, ) -> None: """ Validate that a value is of the expected type(s). Raises TypeError if validation fails. Args: value: The value to validate. expected_types: A type or tuple of types that the value is expected to be. field_name: The name of the field being validated (for error messages). """ if not isinstance(value, expected_types): raise TypeError(f"Field '{field_name}' expected {expected_types}, got {type(value).__name__}") types.py000066400000000000000000000142721527070070700344010ustar00rootroot00000000000000python-opentelemetry-1.45.2/codegen/opentelemetry-codegen-json/src/opentelemetry/codegen/json# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # pylint: disable=no-member from typing import Final from google.protobuf import descriptor_pb2 as descriptor PROTO_TO_PYTHON: Final[dict[int, str]] = { descriptor.FieldDescriptorProto.TYPE_DOUBLE: "builtins.float", descriptor.FieldDescriptorProto.TYPE_FLOAT: "builtins.float", descriptor.FieldDescriptorProto.TYPE_INT64: "builtins.int", descriptor.FieldDescriptorProto.TYPE_UINT64: "builtins.int", descriptor.FieldDescriptorProto.TYPE_INT32: "builtins.int", descriptor.FieldDescriptorProto.TYPE_FIXED64: "builtins.int", descriptor.FieldDescriptorProto.TYPE_FIXED32: "builtins.int", descriptor.FieldDescriptorProto.TYPE_BOOL: "builtins.bool", descriptor.FieldDescriptorProto.TYPE_STRING: "builtins.str", descriptor.FieldDescriptorProto.TYPE_BYTES: "builtins.bytes", descriptor.FieldDescriptorProto.TYPE_UINT32: "builtins.int", descriptor.FieldDescriptorProto.TYPE_SFIXED32: "builtins.int", descriptor.FieldDescriptorProto.TYPE_SFIXED64: "builtins.int", descriptor.FieldDescriptorProto.TYPE_SINT32: "builtins.int", descriptor.FieldDescriptorProto.TYPE_SINT64: "builtins.int", } PROTO_DEFAULTS: Final[dict[int, str]] = { descriptor.FieldDescriptorProto.TYPE_DOUBLE: "0.0", descriptor.FieldDescriptorProto.TYPE_FLOAT: "0.0", descriptor.FieldDescriptorProto.TYPE_INT64: "0", descriptor.FieldDescriptorProto.TYPE_UINT64: "0", descriptor.FieldDescriptorProto.TYPE_INT32: "0", descriptor.FieldDescriptorProto.TYPE_FIXED64: "0", descriptor.FieldDescriptorProto.TYPE_FIXED32: "0", descriptor.FieldDescriptorProto.TYPE_BOOL: "False", descriptor.FieldDescriptorProto.TYPE_STRING: '""', descriptor.FieldDescriptorProto.TYPE_BYTES: 'b""', descriptor.FieldDescriptorProto.TYPE_UINT32: "0", descriptor.FieldDescriptorProto.TYPE_SFIXED32: "0", descriptor.FieldDescriptorProto.TYPE_SFIXED64: "0", descriptor.FieldDescriptorProto.TYPE_SINT32: "0", descriptor.FieldDescriptorProto.TYPE_SINT64: "0", } INT64_TYPES: Final[set[int]] = { descriptor.FieldDescriptorProto.TYPE_INT64, descriptor.FieldDescriptorProto.TYPE_UINT64, descriptor.FieldDescriptorProto.TYPE_FIXED64, descriptor.FieldDescriptorProto.TYPE_SFIXED64, descriptor.FieldDescriptorProto.TYPE_SINT64, } NUMERIC_TYPES: Final[set[int]] = { *INT64_TYPES, descriptor.FieldDescriptorProto.TYPE_DOUBLE, descriptor.FieldDescriptorProto.TYPE_FLOAT, descriptor.FieldDescriptorProto.TYPE_INT32, descriptor.FieldDescriptorProto.TYPE_FIXED32, descriptor.FieldDescriptorProto.TYPE_UINT32, descriptor.FieldDescriptorProto.TYPE_SFIXED32, descriptor.FieldDescriptorProto.TYPE_SINT32, } HEX_ENCODED_FIELDS: Final[set[str]] = { "trace_id", "span_id", "parent_span_id", } def get_python_type(proto_type: int) -> str: """ Get the Python type corresponding to a protobuf field type. Args: proto_type: The protobuf field type as an integer (from FieldDescriptorProto). Returns: A string representing the Python type """ if proto_type not in PROTO_TO_PYTHON: raise ValueError(f"Unknown protobuf type: {proto_type}") return PROTO_TO_PYTHON[proto_type] def get_default_value(proto_type: int) -> str: """ Get the default value for a protobuf field type as a string. Args: proto_type: The protobuf field type as an integer (from FieldDescriptorProto). Returns: A string representing the default value for that type """ return PROTO_DEFAULTS.get(proto_type, "None") def is_int64_type(proto_type: int) -> bool: """ Check if type is a 64-bit integer type (int64, uint64, fixed64, sfixed64, sint64) Args: proto_type: The protobuf field type as an integer (from FieldDescriptorProto). Returns: True if the type is a 64-bit integer type, False otherwise. """ return proto_type in INT64_TYPES def is_bytes_type(proto_type: int) -> bool: """ Check if type is bytes. Args: proto_type: The protobuf field type as an integer (from FieldDescriptorProto). Returns: True if the type is bytes, False otherwise. """ return proto_type == descriptor.FieldDescriptorProto.TYPE_BYTES def is_hex_encoded_field(field_name: str) -> bool: """ Check if field is a hex-encoded field (trace_id, span_id, parent_span_id). Args: field_name: The name of the protobuf field. Returns: True if the field is a hex-encoded field, False otherwise. """ return field_name in HEX_ENCODED_FIELDS def to_json_field_name(snake_name: str) -> str: """ Convert snake_case field name to camelCase for JSON representation. Args: snake_name: The field name in snake_case. Returns: The field name converted to camelCase. """ components = snake_name.split("_") return components[0] + "".join(x.title() for x in components[1:]) def is_numeric_type(proto_type: int) -> bool: """ Check if the protobuf field type is a numeric type (int32, int64, uint32, uint64, float, double). Args: proto_type: The protobuf field type as an integer (from FieldDescriptorProto). Returns: True if the type is a numeric type, False otherwise. """ return proto_type in NUMERIC_TYPES def get_json_allowed_types(proto_type: int, field_name: str = "") -> str: """ Get the Python type(s) allowed for the JSON representation of a field. Args: proto_type: The protobuf field type as an integer (from FieldDescriptorProto). field_name: The name of the protobuf field (used to determine if it's hex-encoded) Returns: A string representing the allowed Python type(s) for the JSON representation of the field. """ if is_hex_encoded_field(field_name): return "builtins.str" if is_int64_type(proto_type): return "(builtins.int, builtins.str)" if is_bytes_type(proto_type): return "builtins.str" if proto_type in ( descriptor.FieldDescriptorProto.TYPE_FLOAT, descriptor.FieldDescriptorProto.TYPE_DOUBLE, ): return "(builtins.float, builtins.int, builtins.str)" return get_python_type(proto_type) writer.py000066400000000000000000000324721527070070700345530ustar00rootroot00000000000000python-opentelemetry-1.45.2/codegen/opentelemetry-codegen-json/src/opentelemetry/codegen/json# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations from collections.abc import Generator, Iterable from contextlib import contextmanager from typing import Any # pylint: disable-next=too-many-public-methods class CodeWriter: def __init__(self, indent_size: int = 4) -> None: """ Initializes a new CodeWriter instance. Args: indent_size: Number of spaces for each indentation level (default: 4) """ self._lines: list[str] = [] self._indent_level: int = 0 self._indent_size: int = indent_size @contextmanager def indent(self) -> Generator[CodeWriter, None, None]: """ Context manager to increase indentation level for a block of code. """ self._indent_level += 1 try: yield self finally: self._indent_level -= 1 def writeln(self, line: str = "") -> CodeWriter: """ Writes a line of code with proper indentation. If the line is empty, it writes a blank line. Args: line: The line of code to write (default: empty string for a blank line) Returns: The CodeWriter instance """ if not line: self._lines.append("") return self indent = " " * (self._indent_level * self._indent_size) self._lines.append(f"{indent}{line}") return self def writemany(self, *lines: str) -> CodeWriter: """ Writes multiple lines of code with proper indentation. Args: *lines: Variable number of lines to write Returns: The CodeWriter instance """ for line in lines: self.writeln(line) return self def comment(self, content: str | Iterable[str]) -> CodeWriter: """ Writes a comment line or block. If content is a string, it writes a single comment line. Args: content: A string for a single comment line or an iterable of strings for multiple comment lines Returns: The CodeWriter instance """ if isinstance(content, str): self.writeln(f"# {content}" if content else "#") return self for line in content: self.writeln(f"# {line}" if line else "#") return self def docstring(self, content: str | Iterable[str]) -> CodeWriter: """ Writes a docstring. If content is a string, it writes a single-line docstring. If content is an iterable of strings, it writes a multi-line docstring. Args: content: A string for a single-line docstring or an iterable of strings for a multi line docstring Returns: The CodeWriter instance """ if isinstance(content, str): self.writeln(f'"""{content}"""') return self self.writeln('"""') for line in content: self.writeln(line) self.writeln('"""') return self def import_(self, module: str, *items: str) -> CodeWriter: """ Writes an import statement. If items are provided, it writes a from-import statement. Otherwise, it writes a regular import statement. Args: module: The module to import *items: Optional items to import from the module (if empty, imports the whole module) Returns: The CodeWriter instance """ if items: self.writeln(f"from {module} import {', '.join(items)}") else: self.writeln(f"import {module}") return self @contextmanager def block(self, header: str) -> Generator[CodeWriter, None, None]: """ Create a generic code block with a header (e.g. if, for, while, try, etc.) Args: header: The header line for the code block """ self.writeln(header) with self.indent(): yield self @contextmanager def class_( self, name: str, bases: Iterable[str] | None = None, decorators: Iterable[str] | None = None, ) -> Generator[CodeWriter, None, None]: """ Generate a class definition with optional base classes and decorators. Args: name: The name of the class bases: Optional iterable of base class names decorators: Optional iterable of decorator names """ if decorators is not None: for dec in decorators: self.writeln(f"@{dec}") bases_str = f"({', '.join(bases)})" if bases else "" self.writeln(f"class {name}{bases_str}:") with self.indent(): yield self @contextmanager def dataclass( self, name: str, bases: Iterable[str] | None = None, decorators: Iterable[str] | None = None, frozen: bool = False, slots: bool = False, decorator_name: str = "dataclasses.dataclass", ) -> Generator[CodeWriter, None, None]: """ Generate a dataclass definition with optional base classes, decorators and dataclass parameters. Args: name: The name of the dataclass bases: Optional iterable of base class names decorators: Optional iterable of additional decorator names frozen: Whether to set frozen=True in the dataclass decorator slots: Whether to set slots=True in the dataclass decorator decorator_name: The full name of the dataclass decorator to use """ dc_params = [] if frozen: dc_params.append("frozen=True") if slots: dc_params.append("slots=True") dc_decorator = f"{decorator_name}({', '.join(dc_params)})" if dc_params else decorator_name all_decorators = [] if decorators is not None: all_decorators.extend(decorators) all_decorators.append(dc_decorator) for dec in all_decorators: self.writeln(f"@{dec}") bases_str = f"({', '.join(bases)})" if bases else "" self.writeln(f"class {name}{bases_str}:") with self.indent(): yield self @contextmanager def enum( self, name: str, enum_type: str = "enum.Enum", bases: Iterable[str] | None = None, decorators: Iterable[str] | None = None, ) -> Generator[CodeWriter, None, None]: """ Generate an enum definition with optional base classes and decorators. Args: name: The name of the enum enum_type: The base enum type to inherit from (default: "enum.Enum") bases: Optional iterable of additional base class names decorators: Optional iterable of decorator names """ if decorators is not None: for dec in decorators: self.writeln(f"@{dec}") all_bases = [enum_type] if bases is not None: all_bases.extend(bases) bases_str = ", ".join(all_bases) self.writeln(f"class {name}({bases_str}):") with self.indent(): yield self def field( self, name: str, type_hint: str, default: Any = None, default_factory: str | None = None, ) -> CodeWriter: """ Write a dataclass field with optional default value or default factory. Args: name: The name of the field type_hint: The type hint for the field default: Optional default value for the field default_factory: Optional default factory for the field """ if default_factory: self.writeln(f"{name}: {type_hint} = dataclasses.field(default_factory={default_factory})") elif default is not None: self.writeln(f"{name}: {type_hint} = {default}") else: self.writeln(f"{name}: {type_hint}") return self def enum_member(self, name: str, value: Any) -> CodeWriter: """ Write an enum member with a specific value. Args: name: The name of the enum member value: The value of the enum member """ self.writeln(f"{name} = {value}") return self @contextmanager def function( self, name: str, params: Iterable[str] | str, decorators: Iterable[str] | None = None, return_type: str | None = None, ) -> Generator[CodeWriter, None, None]: """ Create a function definition with optional decorators and return type. Args: name: The name of the function params: An iterable of parameter strings or a single string for the parameter list decorators: Optional iterable of decorator names return_type: Optional return type hint for the function """ if decorators is not None: for dec in decorators: self.writeln(f"@{dec}") params_str = params if isinstance(params, str) else ", ".join(params) return_annotation = f" -> {return_type}" if return_type else "" self.writeln(f"def {name}({params_str}){return_annotation}:") with self.indent(): yield self @contextmanager def method( self, name: str, params: Iterable[str] | str, decorators: Iterable[str] | None = None, return_type: str | None = None, ) -> Generator[CodeWriter, None, None]: """ Create a method definition within a class with optional decorators and return type. Args: name: The name of the method params: An iterable of parameter strings or a single string for the parameter list decorators: Optional iterable of decorator names return_type: Optional return type hint for the method """ with self.function(name, params, decorators=decorators, return_type=return_type): yield self @contextmanager def if_(self, condition: str) -> Generator[CodeWriter, None, None]: """ Create an if block Args: condition: The condition for the if statement """ self.writeln(f"if {condition}:") with self.indent(): yield self @contextmanager def elif_(self, condition: str) -> Generator[CodeWriter, None, None]: """ Create an elif block Args: condition: The condition for the elif statement """ self.writeln(f"elif {condition}:") with self.indent(): yield self @contextmanager def else_(self) -> Generator[CodeWriter, None, None]: """ Create an else block """ self.writeln("else:") with self.indent(): yield self @contextmanager def for_(self, var: str, iterable: str) -> Generator[CodeWriter, None, None]: """ Create a for loop Args: var: The loop variable iterable: The iterable to loop over """ self.writeln(f"for {var} in {iterable}:") with self.indent(): yield self @contextmanager def while_(self, condition: str) -> Generator[CodeWriter, None, None]: """ Create a while loop Args: condition: The condition for the while loop """ self.writeln(f"while {condition}:") with self.indent(): yield self def assignment(self, var: str, value: str, type_hint: str | None = None) -> CodeWriter: """ Write a variable assignment with optional type hint Args: var: The variable name value: The value to assign to the variable type_hint: Optional type hint for the variable Returns: The CodeWriter instance """ if type_hint: self.writeln(f"{var}: {type_hint} = {value}") else: self.writeln(f"{var} = {value}") return self def return_(self, value: str | None = None) -> CodeWriter: """ Write a return statement with an optional return value Args: value: The value to return (if None, writes a bare return statement) Returns: The CodeWriter instance """ if value: self.writeln(f"return {value}") else: self.writeln("return") return self def pass_(self) -> CodeWriter: """ Write a pass statement Returns: The CodeWriter instance """ self.writeln("pass") return self def blank_line(self, count: int = 1) -> CodeWriter: """ Write one or more blank lines Args: count: The number of blank lines to write Returns: The CodeWriter instance """ self._lines.extend([""] * count) return self def to_string(self) -> str: """ Get the generated code as a single string with newline characters separating lines. Returns: A string containing the entire generated code. """ return "\n".join(self._lines) def to_lines(self) -> list[str]: """ Get the generated code as a list of lines Returns: A list of strings, where each string is a line of generated code. """ return self._lines python-opentelemetry-1.45.2/codegen/opentelemetry-codegen-json/test-requirements.txt000066400000000000000000000002771527070070700311430ustar00rootroot00000000000000iniconfig==2.0.0 packaging==24.0 pluggy==1.5.0 protobuf==6.33.5 pytest==9.0.3 tomli==2.0.1 typing_extensions==4.12.0 grpcio==1.78.0 grpcio-tools==1.78.0 -e codegen/opentelemetry-codegen-json python-opentelemetry-1.45.2/codegen/opentelemetry-codegen-json/tests/000077500000000000000000000000001527070070700260365ustar00rootroot00000000000000python-opentelemetry-1.45.2/codegen/opentelemetry-codegen-json/tests/__init__.py000066400000000000000000000000001527070070700301350ustar00rootroot00000000000000python-opentelemetry-1.45.2/codegen/opentelemetry-codegen-json/tests/conftest.py000066400000000000000000000022741527070070700302420ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # pylint: disable=redefined-outer-name import importlib import subprocess import sys from pathlib import Path import pytest # type: ignore from _pytest.monkeypatch import MonkeyPatch # type: ignore PROTO_PATH = Path(__file__).parent / "proto" GEN_PATH = Path(__file__).parent / "generated" @pytest.fixture(scope="session") def monkeysession(): with pytest.MonkeyPatch.context() as mp: yield mp @pytest.fixture(scope="session", autouse=True) def generate_code(tmp_path_factory: pytest.TempPathFactory, monkeysession: MonkeyPatch) -> None: gen_path = tmp_path_factory.mktemp("generated") protos = list(PROTO_PATH.glob("**/*.proto")) proto_files = [p.relative_to(PROTO_PATH).as_posix() for p in protos] subprocess.check_call( [ sys.executable, "-m", "grpc_tools.protoc", f"-I{PROTO_PATH.as_posix()}", f"--otlp_json_out={gen_path.as_posix()}", f"--python_out={gen_path.as_posix()}", *proto_files, ] ) monkeysession.syspath_prepend(str(gen_path.absolute())) importlib.invalidate_caches() python-opentelemetry-1.45.2/codegen/opentelemetry-codegen-json/tests/proto/000077500000000000000000000000001527070070700272015ustar00rootroot00000000000000python-opentelemetry-1.45.2/codegen/opentelemetry-codegen-json/tests/proto/otel_test_json/000077500000000000000000000000001527070070700322345ustar00rootroot00000000000000python-opentelemetry-1.45.2/codegen/opentelemetry-codegen-json/tests/proto/otel_test_json/common/000077500000000000000000000000001527070070700335245ustar00rootroot00000000000000python-opentelemetry-1.45.2/codegen/opentelemetry-codegen-json/tests/proto/otel_test_json/common/v1/000077500000000000000000000000001527070070700340525ustar00rootroot00000000000000common.proto000066400000000000000000000013271527070070700363530ustar00rootroot00000000000000python-opentelemetry-1.45.2/codegen/opentelemetry-codegen-json/tests/proto/otel_test_json/common/v1// 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. syntax = "proto3"; package otel_test_json.common.v1; message InstrumentationScope { string name = 1; string version = 2; } python-opentelemetry-1.45.2/codegen/opentelemetry-codegen-json/tests/proto/otel_test_json/test/000077500000000000000000000000001527070070700332135ustar00rootroot00000000000000python-opentelemetry-1.45.2/codegen/opentelemetry-codegen-json/tests/proto/otel_test_json/test/v1/000077500000000000000000000000001527070070700335415ustar00rootroot00000000000000complex.proto000066400000000000000000000031531527070070700362200ustar00rootroot00000000000000python-opentelemetry-1.45.2/codegen/opentelemetry-codegen-json/tests/proto/otel_test_json/test/v1// 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. syntax = "proto3"; package otel_test_json.test.v1; import "otel_test_json/common/v1/common.proto"; message NumericTest { double d_val = 1; float f_val = 2; int64 i64_val = 3; uint64 u64_val = 4; int32 i32_val = 5; uint32 u32_val = 6; sint32 si32_val = 7; sint64 si64_val = 8; fixed32 f32_val = 9; fixed64 f64_val = 10; sfixed32 sf32_val = 11; sfixed64 sf64_val = 12; } message OneofSuite { oneof group1 { string g1_string = 1; int32 g1_int = 2; } oneof group2 { otel_test_json.common.v1.InstrumentationScope g2_message = 3; NestedMessage g2_nested = 4; } message NestedMessage { string hint = 1; } } message OptionalScalar { optional string opt_string = 1; optional int32 opt_int = 2; optional bool opt_bool = 3; } message NestedEnumSuite { enum NestedEnum { NESTED_UNSPECIFIED = 0; NESTED_FOO = 1; NESTED_BAR = 2; } NestedEnum nested = 1; repeated NestedEnum repeated_nested = 2; } message DeeplyNested { string value = 1; DeeplyNested next = 2; } test.proto000066400000000000000000000026111527070070700355260ustar00rootroot00000000000000python-opentelemetry-1.45.2/codegen/opentelemetry-codegen-json/tests/proto/otel_test_json/test/v1// 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. syntax = "proto3"; package otel_test_json.test.v1; message TestMessage { enum TestEnum { UNSPECIFIED = 0; SUCCESS = 1; FAILURE = 2; } // Scalar types string name = 1; int32 int_value = 2; bool bool_value = 3; double double_value = 4; // Special OTLP types int64 int64_value = 5; uint64 uint64_value = 6; bytes bytes_value = 7; // Hex encoded fields (per OTLP spec) bytes trace_id = 8; bytes span_id = 9; // Repeated fields repeated string list_strings = 10; repeated int32 list_ints = 11; // Nested message and enum TestEnum enum_value = 12; SubMessage sub_message = 13; repeated SubMessage list_messages = 14; // Oneof group oneof test_oneof { string oneof_string = 15; int32 oneof_int = 16; } } message SubMessage { string content = 1; } python-opentelemetry-1.45.2/codegen/opentelemetry-codegen-json/tests/proto/otel_test_json/trace/000077500000000000000000000000001527070070700333325ustar00rootroot00000000000000python-opentelemetry-1.45.2/codegen/opentelemetry-codegen-json/tests/proto/otel_test_json/trace/v1/000077500000000000000000000000001527070070700336605ustar00rootroot00000000000000trace.proto000066400000000000000000000014341527070070700357660ustar00rootroot00000000000000python-opentelemetry-1.45.2/codegen/opentelemetry-codegen-json/tests/proto/otel_test_json/trace/v1// 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. syntax = "proto3"; package otel_test_json.trace.v1; import "otel_test_json/common/v1/common.proto"; message Span { string name = 1; otel_test_json.common.v1.InstrumentationScope scope = 2; } python-opentelemetry-1.45.2/codegen/opentelemetry-codegen-json/tests/test_json_codec.py000066400000000000000000000110351527070070700315550ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import math import pytest # type: ignore from opentelemetry.codegen.json.runtime.json_codec import ( decode_base64, decode_float, decode_hex, decode_int64, decode_repeated, encode_base64, encode_float, encode_hex, encode_int64, encode_repeated, validate_type, ) @pytest.mark.parametrize( "value, expected", [ (b"\x01\x02\x03", "010203"), (b"", ""), (None, ""), ], ) def test_encode_hex(value: bytes | None, expected: str) -> None: assert encode_hex(value) == expected @pytest.mark.parametrize( "value, expected", [ ("010203", b"\x01\x02\x03"), ("", b""), (None, b""), ], ) def test_decode_hex(value: str | None, expected: bytes) -> None: assert decode_hex(value, "field") == expected def test_decode_hex_errors() -> None: with pytest.raises(TypeError): decode_hex(123, "field") # type: ignore with pytest.raises(ValueError, match="Invalid hex string"): decode_hex("not hex", "field") @pytest.mark.parametrize( "value, expected", [ (b"hello", "aGVsbG8="), (b"", ""), (None, ""), ], ) def test_encode_base64(value: bytes | None, expected: str) -> None: assert encode_base64(value) == expected @pytest.mark.parametrize( "value, expected", [ ("aGVsbG8=", b"hello"), ("", b""), (None, b""), ], ) def test_decode_base64(value: str | None, expected: bytes) -> None: assert decode_base64(value, "field") == expected def test_decode_base64_errors() -> None: with pytest.raises(TypeError): decode_base64(123, "field") # type: ignore @pytest.mark.parametrize( "value, expected", [ (123, "123"), (0, "0"), (-1, "-1"), ], ) def test_encode_int64(value: int, expected: str) -> None: assert encode_int64(value) == expected @pytest.mark.parametrize( "value, expected", [ ("123", 123), (123, 123), (None, 0), ], ) def test_decode_int64(value: int | str | None, expected: int) -> None: assert decode_int64(value, "field") == expected def test_decode_int64_errors() -> None: with pytest.raises(TypeError): decode_int64([], "field") # type: ignore with pytest.raises(ValueError, match="Invalid int64 value"): decode_int64("abc", "field") @pytest.mark.parametrize( "value, expected", [ (1.5, 1.5), (float("nan"), "NaN"), (float("inf"), "Infinity"), (float("-inf"), "-Infinity"), ], ) def test_encode_float(value: float, expected: float | str) -> None: result = encode_float(value) if isinstance(expected, float) and math.isnan(expected): assert math.isnan(result) # type: ignore else: assert result == expected @pytest.mark.parametrize( "value, expected", [ (1.5, 1.5), ("1.5", 1.5), (1, 1.0), ("NaN", math.nan), ("Infinity", math.inf), ("-Infinity", -math.inf), (None, 0.0), ], ) def test_decode_float(value: float | str | None, expected: float) -> None: result = decode_float(value, "field") if math.isnan(expected): assert math.isnan(result) else: assert result == expected def test_decode_float_errors() -> None: with pytest.raises(TypeError): decode_float([], "field") # type: ignore with pytest.raises(ValueError, match="Invalid float value"): decode_float("abc", "field") def test_repeated_fields() -> None: values = [1, 2, 3] assert encode_repeated(values, str) == ["1", "2", "3"] assert not encode_repeated([], str) assert not encode_repeated(None, str) assert decode_repeated(["1", "2"], int, "field") == [1, 2] assert not decode_repeated([], int, "field") assert not decode_repeated(None, int, "field") def test_decode_repeated_errors() -> None: with pytest.raises(TypeError): decode_repeated("not a list", lambda x: x, "field") # type: ignore def test_validate_type() -> None: validate_type("s", str, "field") validate_type(1, int, "field") validate_type(1, (int, str), "field") validate_type("s", (int, str), "field") with pytest.raises(TypeError, match="Field 'field' expected , got str"): validate_type("s", int, "field") with pytest.raises( TypeError, match=r"Field 'field' expected \(, \), got str", ): validate_type("s", (int, float), "field") python-opentelemetry-1.45.2/codegen/opentelemetry-codegen-json/tests/test_protobuf_compatibility.py000066400000000000000000000145431527070070700342470ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # pylint: skip-file # ruff: noqa: PLC0415 import base64 from typing import Any import pytest # type: ignore from google.protobuf import json_format def normalize_otlp_json(data: Any) -> Any: if isinstance(data, list): return [normalize_otlp_json(item) for item in data] if isinstance(data, dict): return { key: ( base64.b64decode(value).hex() if key in {"traceId", "spanId", "parentSpanId"} and isinstance(value, str) else normalize_otlp_json(value) ) for key, value in data.items() } return data @pytest.fixture def test_msg_classes() -> tuple[type[Any], type[Any], type[Any]]: from otel_test_json.test.v1.test import ( # type: ignore SubMessage as JSONSubMessage, # type: ignore ) from otel_test_json.test.v1.test import ( # type: ignore TestMessage as JSONTestMessage, # type: ignore ) from otel_test_json.test.v1.test_pb2 import ( # type: ignore TestMessage as ProtoTestMessage, # type: ignore ) return JSONTestMessage, JSONSubMessage, ProtoTestMessage @pytest.fixture def numeric_msg_classes() -> tuple[type[Any], type[Any]]: from otel_test_json.test.v1.complex import ( # type: ignore NumericTest as JSONNumericTest, # type: ignore ) from otel_test_json.test.v1.complex_pb2 import ( # type: ignore NumericTest as ProtoNumericTest, # type: ignore ) return JSONNumericTest, ProtoNumericTest @pytest.fixture def oneof_msg_classes() -> tuple[type[Any], type[Any], type[Any]]: from otel_test_json.common.v1.common import ( # type: ignore InstrumentationScope as JSONScope, ) from otel_test_json.test.v1.complex import ( # type: ignore OneofSuite as JSONOneofSuite, # type: ignore ) from otel_test_json.test.v1.complex_pb2 import ( # type: ignore OneofSuite as ProtoOneofSuite, ) return JSONOneofSuite, ProtoOneofSuite, JSONScope @pytest.fixture def optional_msg_classes() -> tuple[type[Any], type[Any]]: from otel_test_json.test.v1.complex import ( # type: ignore OptionalScalar as JSONOptionalScalar, ) from otel_test_json.test.v1.complex_pb2 import ( # type: ignore OptionalScalar as ProtoOptionalScalar, ) return JSONOptionalScalar, ProtoOptionalScalar def test_parity_test_message( test_msg_classes: tuple[type[Any], type[Any], type[Any]], ) -> None: JSONTestMessage, JSONSubMessage, ProtoTestMessage = test_msg_classes kwargs: dict[str, Any] = { "name": "test", "int_value": 123, "bool_value": True, "double_value": 1.5, "int64_value": 9223372036854775807, "uint64_value": 18446744073709551615, "bytes_value": b"hello", "trace_id": b"\x00\x01\x02\x03\x04\x05\x06\x07\x08\t\n\x0b\x0c\x0d\x0e\x0f", "span_id": b"\x00\x01\x02\x03\x04\x05\x06\x07", "list_strings": ["a", "b"], "list_ints": [1, 2], "enum_value": JSONTestMessage.TestEnum.SUCCESS, "oneof_string": "oneof", "sub_message": JSONSubMessage(content="sub"), } json_msg = JSONTestMessage(**kwargs) json_dict = json_msg.to_dict() proto_kwargs = kwargs.copy() sub_msg_data = proto_kwargs.pop("sub_message") proto_msg = ProtoTestMessage(**proto_kwargs) proto_msg.sub_message.content = sub_msg_data.content proto_dict = json_format.MessageToDict( proto_msg, preserving_proto_field_name=False, use_integers_for_enums=True, ) assert json_dict == normalize_otlp_json(proto_dict) @pytest.mark.parametrize( "values", [ {"d_val": float("nan")}, {"f_val": float("inf")}, {"f_val": float("-inf")}, {"i64_val": 9223372036854775807}, {"i64_val": -9223372036854775808}, {"u64_val": 18446744073709551615}, {"i32_val": -2147483648}, {"i32_val": 2147483647}, {"si64_val": -9223372036854775808}, {"f64_val": 9223372036854775807}, ], ) def test_parity_numeric_test(numeric_msg_classes: tuple[type[Any], type[Any]], values: dict[str, Any]) -> None: JSONNumericTest, ProtoNumericTest = numeric_msg_classes json_msg = JSONNumericTest(**values) proto_msg = ProtoNumericTest(**values) json_dict = json_msg.to_dict() proto_dict = json_format.MessageToDict( proto_msg, use_integers_for_enums=True, ) assert json_dict == normalize_otlp_json(proto_dict) @pytest.mark.parametrize( "branch_data", [ {"g1_string": "hello", "g2_nested": {"hint": "test"}}, {"g1_int": 42, "g2_message": {"name": "scope"}}, ], ) def test_parity_oneof_suite( oneof_msg_classes: tuple[type[Any], type[Any], type[Any]], branch_data: dict[str, Any], ) -> None: JSONOneofSuite, ProtoOneofSuite, JSONScope = oneof_msg_classes json_kwargs = {} for k, v in branch_data.items(): if k == "g2_nested": json_kwargs[k] = JSONOneofSuite.NestedMessage(**v) elif k == "g2_message": json_kwargs[k] = JSONScope(**v) else: json_kwargs[k] = v json_msg = JSONOneofSuite(**json_kwargs) proto_msg = ProtoOneofSuite() for k, v in branch_data.items(): if k == "g1_string": proto_msg.g1_string = v elif k == "g1_int": proto_msg.g1_int = v elif k == "g2_nested": proto_msg.g2_nested.hint = v["hint"] elif k == "g2_message": proto_msg.g2_message.name = v["name"] assert json_msg.to_dict() == normalize_otlp_json( json_format.MessageToDict( proto_msg, use_integers_for_enums=True, ) ) @pytest.mark.parametrize( "kwargs", [ {"opt_string": "foo", "opt_int": 42, "opt_bool": True}, {}, {"opt_string": "", "opt_int": 0, "opt_bool": False}, ], ) def test_parity_optional_scalars(optional_msg_classes: tuple[type[Any], type[Any]], kwargs: dict[str, Any]) -> None: JSONOptionalScalar, ProtoOptionalScalar = optional_msg_classes json_msg = JSONOptionalScalar(**kwargs) proto_msg = ProtoOptionalScalar(**kwargs) assert json_msg.to_dict() == normalize_otlp_json( json_format.MessageToDict( proto_msg, use_integers_for_enums=True, ) ) python-opentelemetry-1.45.2/codegen/opentelemetry-codegen-json/tests/test_serde.py000066400000000000000000000230531527070070700305540ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # pylint: skip-file # ruff: noqa: PLC0415 import json import math from typing import Any import pytest # type: ignore @pytest.fixture def test_v1_types() -> tuple[type[Any], type[Any]]: from otel_test_json.test.v1.test import ( # type: ignore SubMessage, TestMessage, ) return TestMessage, SubMessage @pytest.fixture def common_v1_types() -> type[Any]: from otel_test_json.common.v1.common import ( # type: ignore InstrumentationScope, # type: ignore ) return InstrumentationScope @pytest.fixture def trace_v1_types() -> type[Any]: from otel_test_json.trace.v1.trace import Span # type: ignore return Span @pytest.fixture def complex_v1_types() -> tuple[type[Any], type[Any], type[Any], type[Any], type[Any]]: from otel_test_json.test.v1.complex import ( # type: ignore DeeplyNested, NestedEnumSuite, NumericTest, OneofSuite, OptionalScalar, ) return ( NumericTest, OneofSuite, OptionalScalar, NestedEnumSuite, DeeplyNested, ) def test_generated_message_roundtrip( test_v1_types: tuple[type[Any], type[Any]], ) -> None: TestMessage, SubMessage = test_v1_types msg = TestMessage( name="test", int_value=123, bool_value=True, double_value=1.5, int64_value=9223372036854775807, uint64_value=18446744073709551615, bytes_value=b"hello", trace_id=b"\x00\x01\x02\x03\x04\x05\x06\x07\x08\t\n\x0b\x0c\x0d\x0e\x0f", span_id=b"\x00\x01\x02\x03\x04\x05\x06", list_strings=["a", "b"], list_ints=[1, 2], enum_value=TestMessage.TestEnum.SUCCESS, sub_message=SubMessage(content="sub"), list_messages=[SubMessage(content="m1"), SubMessage(content="m2")], oneof_string="oneof", ) json_str = msg.to_json() data = json.loads(json_str) assert data["name"] == "test" assert data["intValue"] == 123 assert data["boolValue"] is True assert data["doubleValue"] == 1.5 assert data["int64Value"] == "9223372036854775807" assert data["uint64Value"] == "18446744073709551615" assert data["bytesValue"] == "aGVsbG8=" assert data["traceId"] == "000102030405060708090a0b0c0d0e0f" assert data["spanId"] == "00010203040506" assert data["listStrings"] == ["a", "b"] assert data["listInts"] == [1, 2] assert data["enumValue"] == 1 assert data["subMessage"]["content"] == "sub" assert len(data["listMessages"]) == 2 assert data["oneofString"] == "oneof" assert "oneofInt" not in data new_msg = TestMessage.from_json(json_str) assert new_msg == msg def test_cross_reference(common_v1_types: type[Any], trace_v1_types: type[Any]) -> None: InstrumentationScope = common_v1_types Span = trace_v1_types span = Span( name="my-span", scope=InstrumentationScope(name="my-scope", version="1.0.0"), ) json_str = span.to_json() data = json.loads(json_str) assert data["name"] == "my-span" assert data["scope"]["name"] == "my-scope" assert data["scope"]["version"] == "1.0.0" new_span = Span.from_json(json_str) assert new_span == span @pytest.mark.parametrize( "field, value, expected_json_val", [ ("d_val", float("nan"), "NaN"), ("d_val", float("inf"), "Infinity"), ("d_val", float("-inf"), "-Infinity"), ("d_val", 0.0, None), # Default values are omitted ("d_val", -0.0, None), ("i64_val", 9223372036854775807, "9223372036854775807"), ("i64_val", -9223372036854775808, "-9223372036854775808"), ("u64_val", 18446744073709551615, "18446744073709551615"), ("i32_val", 2147483647, 2147483647), ("i32_val", -2147483648, -2147483648), ("u32_val", 4294967295, 4294967295), ("si32_val", -123, -123), ("si64_val", -456, "-456"), ("f32_val", 789, 789), ("f64_val", 101112, "101112"), ("sf32_val", -131415, -131415), ("sf64_val", -161718, "-161718"), ], ) def test_numeric_types( complex_v1_types: tuple[type[Any], ...], field: str, value: Any, expected_json_val: Any, ) -> None: NumericTest = complex_v1_types[0] msg = NumericTest(**{field: value}) data = msg.to_dict() # Convert snake_case to lowerCamelCase for lookup components = field.split("_") json_field = components[0] + "".join(x.title() for x in components[1:]) if expected_json_val is None: assert json_field not in data else: assert data[json_field] == expected_json_val new_msg = NumericTest.from_dict(data) if isinstance(value, float) and math.isnan(value): assert math.isnan(getattr(new_msg, field)) else: assert getattr(new_msg, field) == value @pytest.mark.parametrize( "kwargs, expected_data", [ ({"g1_string": "hello"}, {"g1String": "hello"}), ({"g1_int": 42}, {"g1Int": 42}), ({"g2_message": {"name": "scope"}}, {"g2Message": {"name": "scope"}}), ({"g2_nested": {"hint": "test"}}, {"g2Nested": {"hint": "test"}}), ], ) def test_oneof_suite_variants( common_v1_types: type[Any], complex_v1_types: tuple[type[Any], ...], kwargs: dict[str, Any], expected_data: dict[str, Any], ) -> None: InstrumentationScope = common_v1_types OneofSuite = complex_v1_types[1] processed_kwargs = {} for k, v in kwargs.items(): if k == "g2_message": processed_kwargs[k] = InstrumentationScope(**v) elif k == "g2_nested": processed_kwargs[k] = OneofSuite.NestedMessage(**v) else: processed_kwargs[k] = v msg = OneofSuite(**processed_kwargs) data = msg.to_dict() assert data == expected_data new_msg = OneofSuite.from_dict(data) for k, v in processed_kwargs.items(): assert getattr(new_msg, k) == v @pytest.mark.parametrize( "kwargs, expected_dict", [ ({}, {}), ({"opt_string": ""}, {"optString": ""}), ({"opt_string": "foo"}, {"optString": "foo"}), ({"opt_int": 0}, {"optInt": 0}), ({"opt_int": 42}, {"optInt": 42}), ({"opt_bool": False}, {"optBool": False}), ({"opt_bool": True}, {"optBool": True}), ], ) def test_optional_scalars( complex_v1_types: tuple[type[Any], ...], kwargs: dict[str, Any], expected_dict: dict[str, Any], ) -> None: OptionalScalar = complex_v1_types[2] msg = OptionalScalar(**kwargs) assert msg.to_dict() == expected_dict assert OptionalScalar.from_dict(expected_dict) == msg def test_nested_enum_suite(complex_v1_types: tuple[type[Any], ...]) -> None: NestedEnumSuite = complex_v1_types[3] msg = NestedEnumSuite( nested=NestedEnumSuite.NestedEnum.NESTED_FOO, repeated_nested=[ NestedEnumSuite.NestedEnum.NESTED_FOO, NestedEnumSuite.NestedEnum.NESTED_BAR, ], ) data = msg.to_dict() assert data["nested"] == 1 assert data["repeatedNested"] == [1, 2] new_msg = NestedEnumSuite.from_dict(data) assert new_msg.nested == NestedEnumSuite.NestedEnum.NESTED_FOO assert new_msg.repeated_nested == msg.repeated_nested def test_deeply_nested(complex_v1_types: tuple[type[Any], ...]) -> None: DeeplyNested = complex_v1_types[4] msg = DeeplyNested( value="1", next=DeeplyNested(value="2", next=DeeplyNested(value="3")), ) data = msg.to_dict() assert data["value"] == "1" assert data["next"]["value"] == "2" assert data["next"]["next"]["value"] == "3" new_msg = DeeplyNested.from_dict(data) assert new_msg.value == "1" assert new_msg.next.value == "2" assert new_msg.next.next.value == "3" @pytest.mark.parametrize( "data, expected_name, expected_int", [ ({"name": None, "intValue": None}, "", 0), ({"name": "test"}, "test", 0), ({"intValue": 42}, "", 42), ], ) def test_defaults_and_none( test_v1_types: tuple[type[Any], type[Any]], data: dict[str, Any], expected_name: str, expected_int: int, ) -> None: TestMessage, _ = test_v1_types msg = TestMessage.from_dict(data) assert msg.name == expected_name assert msg.int_value == expected_int @pytest.mark.parametrize( "data, expected_error, match", [ ({"intValue": "not an int"}, TypeError, "expected "), ({"traceId": "invalid hex"}, ValueError, "Invalid hex string"), ({"listStrings": "not a list"}, TypeError, "expected "), ({"name": 123}, TypeError, "expected "), ({"subMessage": "not a dict"}, TypeError, "expected "), ({"enumValue": "SUCCESS"}, TypeError, "expected "), ({"listMessages": [None]}, TypeError, "expected "), ], ) def test_validation_errors( test_v1_types: tuple[type[Any], type[Any]], data: dict[str, Any], expected_error: type, match: str, ) -> None: TestMessage, _ = test_v1_types with pytest.raises( expected_error, match=match if isinstance(expected_error, TypeError) else None, ): TestMessage.from_dict(data) def test_unknown_fields_ignored( test_v1_types: tuple[type[Any], type[Any]], ) -> None: TestMessage, _ = test_v1_types # Unknown fields should be ignored for forward compatibility data = { "name": "test", "unknownField": "should be ignored", "intValue": 10, } msg = TestMessage.from_dict(data) assert msg.name == "test" assert msg.int_value == 10 python-opentelemetry-1.45.2/codegen/opentelemetry-codegen-json/tests/test_types.py000066400000000000000000000106531527070070700306200ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # pylint: disable=no-member import pytest # type: ignore from google.protobuf import descriptor_pb2 as descriptor from opentelemetry.codegen.json.types import ( get_default_value, get_json_allowed_types, get_python_type, is_bytes_type, is_hex_encoded_field, is_int64_type, is_numeric_type, to_json_field_name, ) @pytest.mark.parametrize( "proto_type, expected", [ (descriptor.FieldDescriptorProto.TYPE_DOUBLE, "builtins.float"), (descriptor.FieldDescriptorProto.TYPE_INT64, "builtins.int"), (descriptor.FieldDescriptorProto.TYPE_BOOL, "builtins.bool"), (descriptor.FieldDescriptorProto.TYPE_STRING, "builtins.str"), (descriptor.FieldDescriptorProto.TYPE_BYTES, "builtins.bytes"), ], ) def test_get_python_type(proto_type: int, expected: str) -> None: assert get_python_type(proto_type) == expected @pytest.mark.parametrize( "proto_type, expected", [ (descriptor.FieldDescriptorProto.TYPE_DOUBLE, "0.0"), (descriptor.FieldDescriptorProto.TYPE_INT64, "0"), (descriptor.FieldDescriptorProto.TYPE_BOOL, "False"), (descriptor.FieldDescriptorProto.TYPE_STRING, '""'), (descriptor.FieldDescriptorProto.TYPE_BYTES, 'b""'), (999, "None"), ], ) def test_get_default_value(proto_type: int, expected: str) -> None: assert get_default_value(proto_type) == expected @pytest.mark.parametrize( "proto_type, expected", [ (descriptor.FieldDescriptorProto.TYPE_INT64, True), (descriptor.FieldDescriptorProto.TYPE_UINT64, True), (descriptor.FieldDescriptorProto.TYPE_FIXED64, True), (descriptor.FieldDescriptorProto.TYPE_SFIXED64, True), (descriptor.FieldDescriptorProto.TYPE_SINT64, True), (descriptor.FieldDescriptorProto.TYPE_INT32, False), (descriptor.FieldDescriptorProto.TYPE_STRING, False), ], ) def test_is_int64_type(proto_type: int, expected: bool) -> None: assert is_int64_type(proto_type) == expected @pytest.mark.parametrize( "proto_type, expected", [ (descriptor.FieldDescriptorProto.TYPE_BYTES, True), (descriptor.FieldDescriptorProto.TYPE_STRING, False), ], ) def test_is_bytes_type(proto_type: int, expected: bool) -> None: assert is_bytes_type(proto_type) == expected @pytest.mark.parametrize( "field_name, expected", [ ("trace_id", True), ("span_id", True), ("parent_span_id", True), ("name", False), ("time_unix_nano", False), ], ) def test_is_hex_encoded_field(field_name: str, expected: bool) -> None: assert is_hex_encoded_field(field_name) == expected @pytest.mark.parametrize( "snake_name, expected", [ ("name", "name"), ("start_time_unix_nano", "startTimeUnixNano"), ("trace_id", "traceId"), ("multiple___underscores", "multipleUnderscores"), ], ) def test_to_json_field_name(snake_name: str, expected: str) -> None: assert to_json_field_name(snake_name) == expected @pytest.mark.parametrize( "proto_type, expected", [ (descriptor.FieldDescriptorProto.TYPE_DOUBLE, True), (descriptor.FieldDescriptorProto.TYPE_INT64, True), (descriptor.FieldDescriptorProto.TYPE_INT32, True), (descriptor.FieldDescriptorProto.TYPE_BOOL, False), (descriptor.FieldDescriptorProto.TYPE_STRING, False), ], ) def test_is_numeric_type(proto_type: int, expected: bool) -> None: assert is_numeric_type(proto_type) == expected @pytest.mark.parametrize( "proto_type, field_name, expected", [ (descriptor.FieldDescriptorProto.TYPE_BYTES, "data", "builtins.str"), ( descriptor.FieldDescriptorProto.TYPE_STRING, "trace_id", "builtins.str", ), ( descriptor.FieldDescriptorProto.TYPE_INT64, "count", "(builtins.int, builtins.str)", ), ( descriptor.FieldDescriptorProto.TYPE_DOUBLE, "value", "(builtins.float, builtins.int, builtins.str)", ), (descriptor.FieldDescriptorProto.TYPE_BOOL, "flag", "builtins.bool"), (descriptor.FieldDescriptorProto.TYPE_INT32, "id", "builtins.int"), ], ) def test_get_json_allowed_types(proto_type: int, field_name: str, expected: str) -> None: assert get_json_allowed_types(proto_type, field_name) == expected python-opentelemetry-1.45.2/codegen/opentelemetry-codegen-json/tests/test_writer.py000066400000000000000000000135621527070070700307720ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from typing import Any import pytest # type: ignore from opentelemetry.codegen.json.writer import CodeWriter def test_initialization() -> None: writer = CodeWriter(indent_size=2) # pylint: disable-next=protected-access assert writer._indent_size == 2 assert not writer.to_lines() assert writer.to_string() == "" def test_writeln_indentation() -> None: writer = CodeWriter(indent_size=4) writer.writeln("line1") with writer.indent(): writer.writeln("line2") with writer.indent(): writer.writeln("line3") writer.writeln("line4") expected = ["line1", " line2", " line3", "line4"] assert writer.to_lines() == expected def test_writemany() -> None: writer = CodeWriter() writer.writemany("a", "b", "c") assert writer.to_lines() == ["a", "b", "c"] @pytest.mark.parametrize( "content, expected", [ ("single line", ["# single line"]), (["line1", "line2"], ["# line1", "# line2"]), ], ) def test_comment(content: str, expected: list[str]) -> None: writer = CodeWriter() writer.comment(content) assert writer.to_lines() == expected @pytest.mark.parametrize( "content, expected", [ ("single line", ['"""single line"""']), (["line1", "line2"], ['"""', "line1", "line2", '"""']), ], ) def test_docstring(content: str, expected: list[str]) -> None: writer = CodeWriter() writer.docstring(content) assert writer.to_lines() == expected @pytest.mark.parametrize( "module, items, expected", [ ("os", [], ["import os"]), ("typing", ["Any", "Optional"], ["from typing import Any, Optional"]), ], ) def test_import(module: str, items: list[str], expected: list[str]) -> None: writer = CodeWriter() writer.import_(module, *items) assert writer.to_lines() == expected def test_suite() -> None: writer = CodeWriter() with writer.block("def foo():"): writer.writeln("pass") assert writer.to_lines() == ["def foo():", " pass"] @pytest.mark.parametrize( "name, bases, decorators, expected", [ ("MyClass", None, None, ["class MyClass:"]), ("MyClass", ["Base"], ["deco"], ["@deco", "class MyClass(Base):"]), ( "MyClass", ["B1", "B2"], ["d1", "d2"], ["@d1", "@d2", "class MyClass(B1, B2):"], ), ], ) def test_class( name: str, bases: str | None, decorators: list[str] | None, expected: list[str], ) -> None: writer = CodeWriter() with writer.class_(name, bases=bases, decorators=decorators): pass assert writer.to_lines() == expected @pytest.mark.parametrize( "kwargs, expected", [ ({"name": "DC"}, ["@dataclasses.dataclass", "class DC:"]), ( {"name": "DC", "frozen": True}, ["@dataclasses.dataclass(frozen=True)", "class DC:"], ), ( {"name": "DC", "slots": True}, ["@dataclasses.dataclass(slots=True)", "class DC:"], ), ( {"name": "DC", "frozen": True, "slots": True}, ["@dataclasses.dataclass(frozen=True, slots=True)", "class DC:"], ), ( {"name": "DC", "decorator_name": "custom.dc"}, ["@custom.dc", "class DC:"], ), ], ) def test_dataclass(kwargs: dict[str, Any], expected: list[str]) -> None: writer = CodeWriter() with writer.dataclass(**kwargs): pass assert writer.to_lines() == expected def test_enum() -> None: writer = CodeWriter() with writer.enum("MyEnum", bases=["IntEnum"]): writer.enum_member("A", 1) writer.enum_member("B", 2) expected = [ "class MyEnum(enum.Enum, IntEnum):", " A = 1", " B = 2", ] assert writer.to_lines() == expected @pytest.mark.parametrize( "name, type_hint, default, default_factory, expected", [ ("x", "int", None, None, ["x: int"]), ("x", "int", 10, None, ["x: int = 10"]), ( "x", "list", None, "list", ["x: list = dataclasses.field(default_factory=list)"], ), ], ) def test_field( name: str, type_hint: str, default: Any | None, default_factory: Any | None, expected: list[str], ) -> None: writer = CodeWriter() writer.field(name, type_hint, default=default, default_factory=default_factory) assert writer.to_lines() == expected def test_function() -> None: writer = CodeWriter() with writer.function("foo", ["a: int", "b: str"], return_type="bool"): writer.return_("True") expected = ["def foo(a: int, b: str) -> bool:", " return True"] assert writer.to_lines() == expected def test_control_flow() -> None: writer = CodeWriter() with writer.if_("a > b"): writer.pass_() with writer.elif_("a == b"): writer.pass_() with writer.else_(): writer.pass_() expected = [ "if a > b:", " pass", "elif a == b:", " pass", "else:", " pass", ] assert writer.to_lines() == expected def test_loops() -> None: writer = CodeWriter() with writer.for_("i", "range(10)"): writer.writeln("print(i)") with writer.while_("True"): writer.writeln("break") expected = [ "for i in range(10):", " print(i)", "while True:", " break", ] assert writer.to_lines() == expected def test_assignment_and_assertions() -> None: writer = CodeWriter() writer.assignment("x", "1", type_hint="int") expected = ["x: int = 1"] assert writer.to_lines() == expected def test_method_alias(): writer = CodeWriter() with writer.method("m", "self"): writer.pass_() assert "def m(self):" in writer.to_string() python-opentelemetry-1.45.2/dev-requirements.txt000066400000000000000000000006751527070070700220730ustar00rootroot00000000000000pylint==3.3.4 griffe==1.15.0 httpretty==1.1.4 inline-snapshot==0.34.2 mocket==3.14.1 pyright==1.1.405 sphinx==8.1.3 sphinx-rtd-theme==3.1.0 sphinx-autodoc-typehints==1.25.2 pytest==9.0.3 pytest-cov==4.1.0 readme-renderer==42.0 markupsafe==2.1.3 bleach==6.4.0 # This dependency was updated to a breaking version. codespell==2.4.3 requests==2.33.0 ruamel.yaml==0.17.21 asgiref==3.7.2 psutil==7.2.2 GitPython==3.1.58 pre-commit==3.7.0 ruff==0.16.1 python-opentelemetry-1.45.2/docs-requirements.txt000066400000000000000000000014741527070070700222430ustar00rootroot00000000000000sphinx==8.1.3 sphinx-rtd-theme==3.1.0 sphinx-autodoc-typehints==1.25.2 # used to generate docs for the website sphinx-jekyll-builder==0.3.0 # Need to install the api/sdk in the venv for autodoc. Modifying sys.path # doesn't work for pkg_resources. ./opentelemetry-api ./opentelemetry-semantic-conventions ./opentelemetry-sdk ./opentelemetry-proto ./opentelemetry-proto-json ./shim/opentelemetry-opencensus-shim ./shim/opentelemetry-opentracing-shim ./exporter/opentelemetry-exporter-otlp-proto-common ./exporter/opentelemetry-exporter-otlp-proto-http[requests] ./exporter/opentelemetry-exporter-otlp-proto-grpc # Required by instrumentation and exporter packages grpcio~=1.27 Deprecated~=1.2 django~=4.2 flask~=2.3 opentracing~=2.2.0 thrift~=0.10 wrapt>=1.0.0,<2.0.0 markupsafe~=2.0 protobuf==5.29.6 prometheus-client~=0.22.1 python-opentelemetry-1.45.2/docs/000077500000000000000000000000001527070070700167535ustar00rootroot00000000000000python-opentelemetry-1.45.2/docs/Makefile000066400000000000000000000011051527070070700204100ustar00rootroot00000000000000# 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) python-opentelemetry-1.45.2/docs/api/000077500000000000000000000000001527070070700175245ustar00rootroot00000000000000python-opentelemetry-1.45.2/docs/api/_logs.rst000066400000000000000000000002701527070070700213600ustar00rootroot00000000000000opentelemetry._logs package ============================= Submodules ---------- .. toctree:: _logs.severity Module contents --------------- .. automodule:: opentelemetry._logs python-opentelemetry-1.45.2/docs/api/_logs.severity.rst000066400000000000000000000001471527070070700232340ustar00rootroot00000000000000opentelemetry._logs.severity ============================ .. automodule:: opentelemetry._logs.severitypython-opentelemetry-1.45.2/docs/api/attributes.rst000066400000000000000000000002251527070070700224430ustar00rootroot00000000000000opentelemetry.attributes ======================== .. automodule:: opentelemetry.attributes :members: :undoc-members: :show-inheritance: python-opentelemetry-1.45.2/docs/api/baggage.propagation.rst000066400000000000000000000002631527070070700241560ustar00rootroot00000000000000opentelemetry.baggage.propagation package ==================================================== Module contents --------------- .. automodule:: opentelemetry.baggage.propagation python-opentelemetry-1.45.2/docs/api/baggage.rst000066400000000000000000000003161527070070700216330ustar00rootroot00000000000000opentelemetry.baggage package ======================================== Subpackages ----------- .. toctree:: baggage.propagation Module contents --------------- .. automodule:: opentelemetry.baggage python-opentelemetry-1.45.2/docs/api/context.rst000066400000000000000000000002721527070070700217430ustar00rootroot00000000000000opentelemetry.context package ============================= Module contents --------------- .. automodule:: opentelemetry.context :members: :undoc-members: :show-inheritance: python-opentelemetry-1.45.2/docs/api/environment_variables.rst000066400000000000000000000002561527070070700246550ustar00rootroot00000000000000opentelemetry.environment_variables package =========================================== Module contents --------------- .. automodule:: opentelemetry.environment_variables python-opentelemetry-1.45.2/docs/api/index.rst000066400000000000000000000013431527070070700213660ustar00rootroot00000000000000OpenTelemetry Python API ======================== The OpenTelemetry Python API provides the core interfaces and no-op implementations for instrumenting applications with traces, metrics, and logs. It defines the abstract classes and data types that library authors and application developers use to collect telemetry data. The API is designed to be lightweight with minimal dependencies so that instrumentation libraries can depend on it without pulling in the full SDK. For the concrete implementation of these interfaces, see the :doc:`OpenTelemetry Python SDK `. .. toctree:: :maxdepth: 1 _logs attributes baggage context propagate propagators trace metrics environment_variables python-opentelemetry-1.45.2/docs/api/metrics.rst000066400000000000000000000002231527070070700217210ustar00rootroot00000000000000opentelemetry.metrics package ============================= .. toctree:: Module contents --------------- .. automodule:: opentelemetry.metrics python-opentelemetry-1.45.2/docs/api/propagate.rst000066400000000000000000000002231527070070700222350ustar00rootroot00000000000000opentelemetry.propagate package ======================================== Module contents --------------- .. automodule:: opentelemetry.propagate python-opentelemetry-1.45.2/docs/api/propagators.composite.rst000066400000000000000000000002571527070070700246240ustar00rootroot00000000000000opentelemetry.propagators.composite ==================================================== Module contents --------------- .. automodule:: opentelemetry.propagators.composite python-opentelemetry-1.45.2/docs/api/propagators.rst000066400000000000000000000002431527070070700226160ustar00rootroot00000000000000opentelemetry.propagators package ======================================== Subpackages ----------- .. toctree:: propagators.textmap propagators.composite python-opentelemetry-1.45.2/docs/api/propagators.textmap.rst000066400000000000000000000002531527070070700243000ustar00rootroot00000000000000opentelemetry.propagators.textmap ==================================================== Module contents --------------- .. automodule:: opentelemetry.propagators.textmap python-opentelemetry-1.45.2/docs/api/trace.rst000066400000000000000000000002641527070070700213560ustar00rootroot00000000000000opentelemetry.trace package =========================== Module contents --------------- .. automodule:: opentelemetry.trace :members: :undoc-members: :show-inheritance: python-opentelemetry-1.45.2/docs/conf.py000066400000000000000000000217631527070070700202630ustar00rootroot00000000000000# 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 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() # Docs-only: resolves the "AnyValue" forward reference wherever # get_type_hints() evaluates it. AnyValue is a recursive alias, so # modules that import it only under TYPE_CHECKING hit a NameError. # TODO: https://github.com/open-telemetry/opentelemetry-python/issues/5304 # this can be removed by importing AnyValue at runtime in the # modules that annotate with it import builtins # noqa: E402 from opentelemetry.util.types import AnyValue # noqa: E402 builtins.AnyValue = AnyValue source_dirs = [ os.path.abspath("../opentelemetry-instrumentation/src/"), ] exp = "../exporter" exp_dirs = [os.path.abspath("/".join(["../exporter", f, "src"])) for f in listdir(exp) if isdir(join(exp, f))] shim = "../shim" shim_dirs = [os.path.abspath("/".join(["../shim", f, "src"])) for f in listdir(shim) if isdir(join(shim, f))] sys.path[:0] = source_dirs + exp_dirs + shim_dirs # -- Project information ----------------------------------------------------- project = "OpenTelemetry Python" 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), "opentracing": ( "https://opentracing-python.readthedocs.io/en/latest/", None, ), "aiohttp": ("https://aiohttp.readthedocs.io/en/stable/", None), "wrapt": ("https://wrapt.readthedocs.io/en/latest/", None), "pymongo": ("https://pymongo.readthedocs.io/en/stable/", None), "grpc": ("https://grpc.github.io/grpc/python/", None), "requests": ("https://requests.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", "ValueT"), ("py:class", "CarrierT"), ("py:obj", "opentelemetry.propagators.textmap.CarrierT"), ("py:obj", "Union"), ("py:data", "typing.Union"), ( "py:class", "opentelemetry.sdk.metrics._internal.instrument._Synchronous", ), ( "py:class", "opentelemetry.sdk.metrics._internal.instrument._Asynchronous", ), # Even if wrapt is added to intersphinx_mapping, sphinx keeps failing # with "class reference target not found: ObjectProxy". ("py:class", "ObjectProxy"), ( "py:class", "opentelemetry.trace._LinkBase", ), ( "py:class", "opentelemetry.proto.collector.trace.v1.trace_service_pb2_grpc.TraceServiceStub", ), ( "py:class", "opentelemetry.proto.collector.metrics.v1.metrics_service_pb2_grpc.MetricsServiceStub", ), ( "py:class", "opentelemetry.proto.collector.logs.v1.logs_service_pb2_grpc.LogsServiceStub", ), ( "py:class", "opentelemetry.exporter.otlp.proto.grpc.exporter.OTLPExporterMixin", ), ( "py:class", "opentelemetry.proto.collector.trace.v1.trace_service_pb2.ExportTraceServiceRequest", ), ( "py:class", "opentelemetry.exporter.otlp.proto.common._internal.metrics_encoder.OTLPMetricExporterMixin", ), ("py:class", "opentelemetry.proto.resource.v1.resource_pb2.Resource"), ( "py:class", "opentelemetry.proto.collector.metrics.v1.metrics_service_pb2.ExportMetricsServiceRequest", ), ("py:class", "opentelemetry.sdk._logs._internal.export.LogRecordExporter"), ( "py:class", "opentelemetry.sdk._logs._internal.export.LogRecordExportResult", ), ( "py:class", "opentelemetry.proto.collector.logs.v1.logs_service_pb2.ExportLogsServiceRequest", ), ( "py:class", "opentelemetry.sdk.metrics._internal.exemplar.exemplar_reservoir.FixedSizeExemplarReservoirABC", ), ( "py:class", "opentelemetry.sdk.metrics._internal.exemplar.exemplar.Exemplar", ), ( "py:class", "opentelemetry.sdk.metrics._internal.aggregation._Aggregation", ), ( "py:class", "_contextvars.Token", ), ( "py:class", "AnyValue", ), ("py:class", "Token"), # ``from os import PathLike`` renders as the bare name ``PathLike`` in the # file exporter type hints, which sphinx cannot resolve to os.PathLike. ("py:class", "PathLike"), ("py:class", "BaseHTTPTransport"), ("py:class", "opentelemetry.exporter.otlp.common.http.Compression"), ( "py:class", "opentelemetry.exporter.http.transport._base.BaseHTTPTransport", ), ] # 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", "examples/fork-process-model/flask-gunicorn", "examples/fork-process-model/flask-uwsgi", "examples/error_handler/error_handler_0", "examples/error_handler/error_handler_1", ] _exclude_members = ["_abc_impl"] autodoc_default_options = { "members": True, "undoc-members": True, "show-inheritance": True, "member-order": "bysource", "exclude-members": ",".join(_exclude_members), } # Napoleon configuration to avoid duplication with autodoc for dataclass fields # Use ivar (instance variable) style for documenting attributes napoleon_use_ivar = True # -- 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/" 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"), } def on_missing_reference(app, env, node, contnode): # FIXME Remove when the API and SDK expose Gauge without the _ prefix. if node["reftarget"] in ( "opentelemetry.metrics.Gauge", "opentelemetry.sdk.metrics._internal.instrument.Gauge", ): return contnode def setup(app): app.connect("missing-reference", on_missing_reference) python-opentelemetry-1.45.2/docs/examples/000077500000000000000000000000001527070070700205715ustar00rootroot00000000000000python-opentelemetry-1.45.2/docs/examples/auto-instrumentation/000077500000000000000000000000001527070070700250025ustar00rootroot00000000000000python-opentelemetry-1.45.2/docs/examples/auto-instrumentation/README.rst000066400000000000000000000004741527070070700264760ustar00rootroot00000000000000Auto-instrumentation ==================== To learn about automatic instrumentation and how to run the example in this :scm_web:`directory `, see `Automatic Instrumentation`_. .. _Automatic Instrumentation: https://opentelemetry.io/docs/instrumentation/python/automatic/example python-opentelemetry-1.45.2/docs/examples/auto-instrumentation/client.py000066400000000000000000000017731527070070700266420ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import sys from requests import get from opentelemetry import trace from opentelemetry.propagate import inject from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import ( BatchSpanProcessor, ConsoleSpanExporter, ) trace.set_tracer_provider(TracerProvider()) tracer = trace.get_tracer_provider().get_tracer(__name__) trace.get_tracer_provider().add_span_processor(BatchSpanProcessor(ConsoleSpanExporter())) # Get parameter from command line argument or use default value "testing" param_value = sys.argv[1] if len(sys.argv) > 1 else "testing" with tracer.start_as_current_span("client"): with tracer.start_as_current_span("client-server"): headers = {} inject(headers) requested = get( "http://localhost:8082/server_request", params={"param": param_value}, headers=headers, ) assert requested.status_code == 200 python-opentelemetry-1.45.2/docs/examples/auto-instrumentation/server_automatic.py000066400000000000000000000004501527070070700307270ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from flask import Flask, request app = Flask(__name__) @app.route("/server_request") def server_request(): print(request.args.get("param")) return "served" if __name__ == "__main__": app.run(port=8082) python-opentelemetry-1.45.2/docs/examples/auto-instrumentation/server_manual.py000066400000000000000000000020361527070070700302200ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from flask import Flask, request from opentelemetry.instrumentation.wsgi import collect_request_attributes from opentelemetry.propagate import extract from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import ( BatchSpanProcessor, ConsoleSpanExporter, ) from opentelemetry.trace import ( SpanKind, get_tracer_provider, set_tracer_provider, ) app = Flask(__name__) set_tracer_provider(TracerProvider()) tracer = get_tracer_provider().get_tracer(__name__) get_tracer_provider().add_span_processor(BatchSpanProcessor(ConsoleSpanExporter())) @app.route("/server_request") def server_request(): with tracer.start_as_current_span( "server_request", context=extract(request.headers), kind=SpanKind.SERVER, attributes=collect_request_attributes(request.environ), ): print(request.args.get("param")) return "served" if __name__ == "__main__": app.run(port=8082) python-opentelemetry-1.45.2/docs/examples/auto-instrumentation/server_programmatic.py000066400000000000000000000015141527070070700314300ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from flask import Flask, request from opentelemetry.instrumentation.flask import FlaskInstrumentor from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import ( BatchSpanProcessor, ConsoleSpanExporter, ) from opentelemetry.trace import get_tracer_provider, set_tracer_provider set_tracer_provider(TracerProvider()) get_tracer_provider().add_span_processor(BatchSpanProcessor(ConsoleSpanExporter())) instrumentor = FlaskInstrumentor() app = Flask(__name__) instrumentor.instrument_app(app) # instrumentor.instrument_app(app, excluded_urls="/server_request") @app.route("/server_request") def server_request(): print(request.args.get("param")) return "served" if __name__ == "__main__": app.run(port=8082) python-opentelemetry-1.45.2/docs/examples/basic_context/000077500000000000000000000000001527070070700234165ustar00rootroot00000000000000python-opentelemetry-1.45.2/docs/examples/basic_context/README.rst000066400000000000000000000020061527070070700251030ustar00rootroot00000000000000Basic Context ============= These examples show how context is propagated through Spans in OpenTelemetry. There are three different examples: * :scm_web:`implicit_context.py `: Shows how starting a span implicitly creates context. * :scm_web:`child_context.py `: Shows how context is propagated through child spans. * :scm_web:`async_context.py `: Shows how context can be shared in another coroutine. The source files of these examples are available :scm_web:`here `. Installation ------------ .. code-block:: sh pip install opentelemetry-api pip install opentelemetry-sdk Run the Example --------------- .. code-block:: sh python .py The output will be shown in the console. Useful links ------------ - OpenTelemetry_ - :doc:`../../api/trace` .. _OpenTelemetry: https://github.com/open-telemetry/opentelemetry-python/ python-opentelemetry-1.45.2/docs/examples/basic_context/async_context.py000066400000000000000000000011201527070070700266430ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import asyncio from opentelemetry import baggage, trace from opentelemetry.sdk.trace import TracerProvider trace.set_tracer_provider(TracerProvider()) tracer = trace.get_tracer(__name__) loop = asyncio.get_event_loop() async def async_span(span): with trace.use_span(span): ctx = baggage.set_baggage("foo", "bar") return ctx async def main(): span = tracer.start_span(name="span") ctx = await async_span(span) print(baggage.get_all(context=ctx)) loop.run_until_complete(main()) python-opentelemetry-1.45.2/docs/examples/basic_context/child_context.py000066400000000000000000000011741527070070700266220ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from opentelemetry import baggage, trace tracer = trace.get_tracer(__name__) global_ctx = baggage.set_baggage("context", "global") with tracer.start_as_current_span(name="root span") as root_span: parent_ctx = baggage.set_baggage("context", "parent") with tracer.start_as_current_span(name="child span", context=parent_ctx) as child_span: child_ctx = baggage.set_baggage("context", "child") print(baggage.get_baggage("context", global_ctx)) print(baggage.get_baggage("context", parent_ctx)) print(baggage.get_baggage("context", child_ctx)) python-opentelemetry-1.45.2/docs/examples/basic_context/implicit_context.py000066400000000000000000000007241527070070700273510ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from opentelemetry import baggage, trace from opentelemetry.sdk.trace import TracerProvider trace.set_tracer_provider(TracerProvider()) tracer = trace.get_tracer(__name__) with tracer.start_span(name="root span") as root_span: ctx = baggage.set_baggage("foo", "bar") print(f"Global context baggage: {baggage.get_all()}") print(f"Span context baggage: {baggage.get_all(context=ctx)}") python-opentelemetry-1.45.2/docs/examples/basic_tracer/000077500000000000000000000000001527070070700232125ustar00rootroot00000000000000python-opentelemetry-1.45.2/docs/examples/basic_tracer/README.rst000066400000000000000000000016061527070070700247040ustar00rootroot00000000000000Basic Trace =========== These examples show how to use OpenTelemetry to create and export Spans. There are two different examples: * :scm_web:`basic_trace.py `: Shows how to configure a SpanProcessor and Exporter, and how to create a tracer and span. * :scm_web:`resources.py `: Shows how to add resource information to a Provider. The source files of these examples are available :scm_web:`here `. Installation ------------ .. code-block:: sh pip install opentelemetry-api pip install opentelemetry-sdk Run the Example --------------- .. code-block:: sh python .py The output will be shown in the console. Useful links ------------ - OpenTelemetry_ - :doc:`../../api/trace` .. _OpenTelemetry: https://github.com/open-telemetry/opentelemetry-python/ python-opentelemetry-1.45.2/docs/examples/basic_tracer/basic_trace.py000066400000000000000000000007571527070070700260340ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from opentelemetry import trace from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import ( BatchSpanProcessor, ConsoleSpanExporter, ) trace.set_tracer_provider(TracerProvider()) trace.get_tracer_provider().add_span_processor(BatchSpanProcessor(ConsoleSpanExporter())) tracer = trace.get_tracer(__name__) with tracer.start_as_current_span("foo"): print("Hello world!") python-opentelemetry-1.45.2/docs/examples/basic_tracer/resources.py000066400000000000000000000012511527070070700255750ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from opentelemetry import trace from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import ( BatchSpanProcessor, ConsoleSpanExporter, ) # Use Resource.create() instead of constructor directly resource = Resource.create({"service.name": "basic_service"}) trace.set_tracer_provider(TracerProvider(resource=resource)) trace.get_tracer_provider().add_span_processor(BatchSpanProcessor(ConsoleSpanExporter())) tracer = trace.get_tracer(__name__) with tracer.start_as_current_span("foo"): print("Hello world!") python-opentelemetry-1.45.2/docs/examples/declarative-config/000077500000000000000000000000001527070070700243175ustar00rootroot00000000000000python-opentelemetry-1.45.2/docs/examples/declarative-config/README.rst000066400000000000000000000052611527070070700260120ustar00rootroot00000000000000Declarative Configuration ========================= .. note:: Declarative configuration support is new in this release and may still have rough edges. If you hit a problem, please open an issue on the `opentelemetry-python tracker `_. This example configures the OpenTelemetry SDK from a single YAML file using :doc:`declarative configuration ` instead of environment variables or hand-written provider setup. The source files of this example are available :scm_web:`here `. Install the declarative-config package, the auto-instrumentation entry point, the OTLP/HTTP exporter, and the ``requests`` instrumentation used by this example: .. code-block:: sh pip install opentelemetry-configuration \ opentelemetry-distro \ opentelemetry-exporter-otlp-proto-http \ opentelemetry-instrumentation-requests Start an OTLP-capable backend locally so there is somewhere to send data. Write the following file: .. code-block:: yaml # otel-collector-config.yaml receivers: otlp: protocols: http: endpoint: 0.0.0.0:4318 exporters: debug: verbosity: detailed service: pipelines: traces: receivers: [otlp] exporters: [debug] metrics: receivers: [otlp] exporters: [debug] logs: receivers: [otlp] exporters: [debug] Then start the Collector: .. code-block:: sh docker run \ -p 4318:4318 \ -v $(pwd)/otel-collector-config.yaml:/etc/otel-collector-config.yaml \ otel/opentelemetry-collector:latest \ --config=/etc/otel-collector-config.yaml Run the example --------------- Point the SDK at ``otel-config.yaml`` with ``OTEL_CONFIG_FILE`` and let auto-instrumentation apply it. No configuration code lives in ``app.py``: .. code-block:: sh export OTEL_CONFIG_FILE=$(pwd)/otel-config.yaml opentelemetry-instrument python app.py You should see the exported span in the Collector's debug output. The ``instrumentation/development.python`` section in ``otel-config.yaml`` activates the ``requests`` instrumentation so outgoing HTTP calls made by ``app.py`` are automatically traced without any code changes. Environment variable substitution ---------------------------------- ``otel-config.yaml`` uses ``${DEPLOYMENT_ENVIRONMENT:-development}`` to read the deployment environment from the environment, defaulting to ``development``. Set it before running to override: .. code-block:: sh export DEPLOYMENT_ENVIRONMENT=staging python-opentelemetry-1.45.2/docs/examples/declarative-config/app.py000066400000000000000000000011401527070070700254450ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 """A minimal application driven by declarative configuration. The SDK is configured entirely from ``otel-config.yaml`` via the ``OTEL_CONFIG_FILE`` environment variable. Run with ``opentelemetry-instrument`` so the configurator picks up the env var: OTEL_CONFIG_FILE=$(pwd)/otel-config.yaml opentelemetry-instrument python app.py """ from opentelemetry import trace tracer = trace.get_tracer("declarative-config-example") with tracer.start_as_current_span("hello"): print("Hello from a declaratively configured SDK!") python-opentelemetry-1.45.2/docs/examples/declarative-config/otel-config.yaml000066400000000000000000000013661527070070700274170ustar00rootroot00000000000000file_format: "1.0" resource: attributes: - name: service.name value: my-service - name: deployment.environment.name value: ${DEPLOYMENT_ENVIRONMENT:-development} tracer_provider: processors: - batch: exporter: otlp_http: endpoint: http://localhost:4318/v1/traces sampler: parent_based: root: always_on: {} meter_provider: readers: - periodic: interval: 60000 exporter: otlp_http: endpoint: http://localhost:4318/v1/metrics logger_provider: processors: - batch: exporter: otlp_http: endpoint: http://localhost:4318/v1/logs instrumentation/development: python: requests: enabled: true python-opentelemetry-1.45.2/docs/examples/django/000077500000000000000000000000001527070070700220335ustar00rootroot00000000000000python-opentelemetry-1.45.2/docs/examples/django/README.rst000066400000000000000000000105131527070070700235220ustar00rootroot00000000000000Django Instrumentation ====================== This shows how to use ``opentelemetry-instrumentation-django`` to automatically instrument a Django app. The source files of these examples are available :scm_web:`here `. Preparation ----------- This example will be executed in a separate virtual environment: .. code-block:: $ mkdir django_auto_instrumentation $ virtualenv django_auto_instrumentation $ source django_auto_instrumentation/bin/activate Installation ------------ .. code-block:: $ pip install opentelemetry-sdk $ pip install opentelemetry-instrumentation-django $ pip install requests Execution --------- Execution of the Django app ........................... This example uses Django features intended for development environment. The ``runserver`` option should not be used for production environments. Set these environment variables first: #. ``export DJANGO_SETTINGS_MODULE=instrumentation_example.settings`` The way to achieve OpenTelemetry instrumentation for your Django app is to use an ``opentelemetry.instrumentation.django.DjangoInstrumentor`` to instrument the app. Clone the ``opentelemetry-python`` repository and go to ``opentelemetry-python/docs/examples/django``. Once there, open the ``manage.py`` file. The call to ``DjangoInstrumentor().instrument()`` in ``main`` is all that is needed to make the app be instrumented. Run the Django app with ``python manage.py runserver --noreload``. The ``--noreload`` flag is needed to avoid Django from running ``main`` twice. Execution of the client ....................... Open up a new console and activate the previous virtual environment there too: ``source django_auto_instrumentation/bin/activate`` Go to ``opentelemetry-python/docs/examples/django``, once there run the client with: ``python client.py hello`` Go to the previous console, where the Django app is running. You should see output similar to this one: .. code-block:: { "name": "home_page_view", "context": { "trace_id": "0xed88755c56d95d05a506f5f70e7849b9", "span_id": "0x0a94c7a60e0650d5", "trace_state": "{}" }, "kind": "SpanKind.SERVER", "parent_id": "0x3096ef92e621c22d", "start_time": "2020-04-26T01:49:57.205833Z", "end_time": "2020-04-26T01:49:57.206214Z", "status": { "status_code": "OK" }, "attributes": { "http.request.method": "GET", "server.address": "localhost", "url.scheme": "http", "server.port": 8000, "url.full": "http://localhost:8000/?param=hello", "server.socket.address": "127.0.0.1", "network.protocol.version": "1.1", "http.response.status_code": 200 }, "events": [], "links": [] } The last output shows spans automatically generated by the OpenTelemetry Django Instrumentation package. Disabling Django Instrumentation -------------------------------- Django's instrumentation can be disabled by setting the following environment variable: ``export OTEL_PYTHON_DJANGO_INSTRUMENT=False`` Auto Instrumentation -------------------- This same example can be run using auto instrumentation. Comment out the call to ``DjangoInstrumentor().instrument()`` in ``main``, then Run the django app with ``opentelemetry-instrument python manage.py runserver --noreload``. Repeat the steps with the client, the result should be the same. Usage with Auto Instrumentation and uWSGI ----------------------------------------- uWSGI and Django can be used together with auto instrumentation. To do so, first install uWSGI in the previous virtual environment: ``pip install uwsgi`` Once that is done, run the server with ``uwsgi`` from the directory that contains ``instrumentation_example``: ``opentelemetry-instrument uwsgi --http :8000 --module instrumentation_example.wsgi`` This should start one uWSGI worker in your console. Open up a browser and point it to ``localhost:8000``. This request should display a span exported in the server console. References ---------- * `Django `_ * `OpenTelemetry Project `_ * `OpenTelemetry Django extension `_ python-opentelemetry-1.45.2/docs/examples/django/client.py000066400000000000000000000015521527070070700236660ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from sys import argv from requests import get from opentelemetry import trace from opentelemetry.propagate import inject from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import ( BatchSpanProcessor, ConsoleSpanExporter, ) trace.set_tracer_provider(TracerProvider()) tracer = trace.get_tracer_provider().get_tracer(__name__) trace.get_tracer_provider().add_span_processor(BatchSpanProcessor(ConsoleSpanExporter())) with tracer.start_as_current_span("client"): with tracer.start_as_current_span("client-server"): headers = {} inject(headers) requested = get( "http://localhost:8000", params={"param": argv[1]}, headers=headers, ) assert requested.status_code == 200 python-opentelemetry-1.45.2/docs/examples/django/instrumentation_example/000077500000000000000000000000001527070070700270115ustar00rootroot00000000000000python-opentelemetry-1.45.2/docs/examples/django/instrumentation_example/__init__.py000066400000000000000000000000001527070070700311100ustar00rootroot00000000000000python-opentelemetry-1.45.2/docs/examples/django/instrumentation_example/asgi.py000066400000000000000000000007631527070070700303140ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 """ ASGI config for instrumentation_example project. It exposes the ASGI callable as a module-level variable named ``application``. For more information on this file, see https://docs.djangoproject.com/en/3.0/howto/deployment/asgi/ """ import os from django.core.asgi import get_asgi_application os.environ.setdefault("DJANGO_SETTINGS_MODULE", "instrumentation_example.settings") application = get_asgi_application() python-opentelemetry-1.45.2/docs/examples/django/instrumentation_example/settings.py000066400000000000000000000062171527070070700312310ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 """ Django settings for instrumentation_example project. Generated by "django-admin startproject" using Django 3.0.4. For more information on this file, see https://docs.djangoproject.com/en/3.0/topics/settings/ For the full list of settings and their values, see https://docs.djangoproject.com/en/3.0/ref/settings/ """ import os # Build paths inside the project like this: os.path.join(BASE_DIR, ...) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/3.0/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! SECRET_KEY = "it%*!=l2(fcawu=!m-06n&#j(iq2j#%$fu6)myi*b9i5ojk+6+" # SECURITY WARNING: don"t run with debug turned on in production! DEBUG = True ALLOWED_HOSTS = [] # Application definition INSTALLED_APPS = [ "django.contrib.admin", "django.contrib.auth", "django.contrib.contenttypes", "django.contrib.sessions", "django.contrib.messages", "django.contrib.staticfiles", ] MIDDLEWARE = [ "django.middleware.security.SecurityMiddleware", "django.contrib.sessions.middleware.SessionMiddleware", "django.middleware.common.CommonMiddleware", "django.middleware.csrf.CsrfViewMiddleware", "django.contrib.auth.middleware.AuthenticationMiddleware", "django.contrib.messages.middleware.MessageMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware", ] ROOT_URLCONF = "instrumentation_example.urls" TEMPLATES = [ { "BACKEND": "django.template.backends.django.DjangoTemplates", "DIRS": [], "APP_DIRS": True, "OPTIONS": { "context_processors": [ "django.template.context_processors.debug", "django.template.context_processors.request", "django.contrib.auth.context_processors.auth", "django.contrib.messages.context_processors.messages", ], }, }, ] WSGI_APPLICATION = "instrumentation_example.wsgi.application" # Database # https://docs.djangoproject.com/en/3.0/ref/settings/#databases DATABASES = { "default": { "ENGINE": "django.db.backends.sqlite3", "NAME": os.path.join(BASE_DIR, "db.sqlite3"), } } # Password validation # https://docs.djangoproject.com/en/3.0/ref/settings/#auth-password-validators AUTH_PASSWORD_VALIDATORS = [ { "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", }, { "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", }, { "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", }, { "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", }, ] # Internationalization # https://docs.djangoproject.com/en/3.0/topics/i18n/ LANGUAGE_CODE = "en-us" TIME_ZONE = "UTC" USE_I18N = True USE_L10N = True USE_TZ = True # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/3.0/howto/static-files/ STATIC_URL = "/static/" python-opentelemetry-1.45.2/docs/examples/django/instrumentation_example/urls.py000066400000000000000000000015701527070070700303530ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 """instrumentation_example URL Configuration The `urlpatterns` list routes URLs to views. For more information please see: https://docs.djangoproject.com/en/3.0/topics/http/urls/ Examples: Function views 1. Add an import: from my_app import views 2. Add a URL to urlpatterns: path("", views.home, name="home") Class-based views 1. Add an import: from other_app.views import Home 2. Add a URL to urlpatterns: path("", Home.as_view(), name="home") Including another URLconf 1. Import the include() function: from django.urls import include, path 2. Add a URL to urlpatterns: path("blog/", include("blog.urls")) """ from django.contrib import admin from django.urls import include, path urlpatterns = [ path("admin/", admin.site.urls), path("", include("pages.urls")), ] python-opentelemetry-1.45.2/docs/examples/django/instrumentation_example/wsgi.py000066400000000000000000000007631527070070700303420ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 """ WSGI config for instrumentation_example project. It exposes the WSGI callable as a module-level variable named ``application``. For more information on this file, see https://docs.djangoproject.com/en/3.0/howto/deployment/wsgi/ """ import os from django.core.wsgi import get_wsgi_application os.environ.setdefault("DJANGO_SETTINGS_MODULE", "instrumentation_example.settings") application = get_wsgi_application() python-opentelemetry-1.45.2/docs/examples/django/manage.py000077500000000000000000000017721527070070700236470ustar00rootroot00000000000000#!/usr/bin/env python # Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 """Django"s command-line utility for administrative tasks.""" import os import sys from opentelemetry.instrumentation.django import DjangoInstrumentor def main(): os.environ.setdefault("DJANGO_SETTINGS_MODULE", "instrumentation_example.settings") # This call is what makes the Django application be instrumented DjangoInstrumentor().instrument() try: from django.core.management import ( # noqa: PLC0415 execute_from_command_line, ) except ImportError as exc: raise ImportError( "Couldn't import Django. Are you sure it's installed and " "available on your PYTHONPATH environment variable? Did you " "forget to activate a virtual environment?" ) from exc execute_from_command_line(sys.argv) if __name__ == "__main__": main() python-opentelemetry-1.45.2/docs/examples/django/pages/000077500000000000000000000000001527070070700231325ustar00rootroot00000000000000python-opentelemetry-1.45.2/docs/examples/django/pages/__init__.py000066400000000000000000000001731527070070700252440ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 default_app_config = "pages.apps.PagesConfig" python-opentelemetry-1.45.2/docs/examples/django/pages/apps.py000066400000000000000000000002411527070070700244440ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from django.apps import AppConfig class PagesConfig(AppConfig): name = "pages" python-opentelemetry-1.45.2/docs/examples/django/pages/migrations/000077500000000000000000000000001527070070700253065ustar00rootroot00000000000000python-opentelemetry-1.45.2/docs/examples/django/pages/migrations/__init__.py000066400000000000000000000000001527070070700274050ustar00rootroot00000000000000python-opentelemetry-1.45.2/docs/examples/django/pages/urls.py000066400000000000000000000003031527070070700244650ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from django.urls import path from .views import home_page_view urlpatterns = [path("", home_page_view, name="home")] python-opentelemetry-1.45.2/docs/examples/django/pages/views.py000066400000000000000000000007641527070070700246500ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from django.http import HttpResponse from opentelemetry import trace from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import ( BatchSpanProcessor, ConsoleSpanExporter, ) trace.set_tracer_provider(TracerProvider()) trace.get_tracer_provider().add_span_processor(BatchSpanProcessor(ConsoleSpanExporter())) def home_page_view(request): return HttpResponse("Hello, world") python-opentelemetry-1.45.2/docs/examples/error_handler/000077500000000000000000000000001527070070700234175ustar00rootroot00000000000000python-opentelemetry-1.45.2/docs/examples/error_handler/README.rst000066400000000000000000000106761527070070700251200ustar00rootroot00000000000000Global Error Handler ==================== Overview -------- This example shows how to use the global error handler. The source files of these examples are available :scm_web:`here `. Preparation ----------- This example will be executed in a separate virtual environment: .. code:: sh $ mkdir global_error_handler $ virtualenv global_error_handler $ source global_error_handler/bin/activate Installation ------------ Here we install first ``opentelemetry-sdk``, the only dependency. Afterwards, 2 error handlers are installed: ``error_handler_0`` will handle ``ZeroDivisionError`` exceptions, ``error_handler_1`` will handle ``IndexError`` and ``KeyError`` exceptions. .. code:: sh $ pip install opentelemetry-sdk $ git clone https://github.com/open-telemetry/opentelemetry-python.git $ pip install -e opentelemetry-python/docs/examples/error_handler/error_handler_0 $ pip install -e opentelemetry-python/docs/examples/error_handler/error_handler_1 Execution --------- An example is provided in the ``opentelemetry-python/docs/examples/error_handler/example.py``. You can just run it, you should get output similar to this one: .. code:: pytb ErrorHandler0 handling a ZeroDivisionError Traceback (most recent call last): File "test.py", line 5, in 1 / 0 ZeroDivisionError: division by zero ErrorHandler1 handling an IndexError Traceback (most recent call last): File "test.py", line 11, in [1][2] IndexError: list index out of range ErrorHandler1 handling a KeyError Traceback (most recent call last): File "test.py", line 17, in {1: 2}[2] KeyError: 2 Error handled by default error handler: Traceback (most recent call last): File "test.py", line 23, in assert False AssertionError No error raised The ``opentelemetry-sdk.error_handler`` module includes documentation that explains how this works. We recommend you read it also, here is just a small summary. In ``example.py`` we use ``GlobalErrorHandler`` as a context manager in several places, for example: .. code:: python with GlobalErrorHandler(): {1: 2}[2] Running that code will raise a ``KeyError`` exception. ``GlobalErrorHandler`` will "capture" that exception and pass it down to the registered error handlers. If there is one that handles ``KeyError`` exceptions then it will handle it. That can be seen in the result of the execution of ``example.py``: .. code:: ErrorHandler1 handling a KeyError Traceback (most recent call last): File "test.py", line 17, in {1: 2}[2] KeyError: 2 There is no registered error handler that can handle ``AssertionError`` exceptions so this kind of errors are handled by the default error handler which just logs the exception to standard logging, as seen here: .. code:: Error handled by default error handler: Traceback (most recent call last): File "test.py", line 23, in assert False AssertionError When no exception is raised, the code inside the scope of ``GlobalErrorHandler`` is executed normally: .. code:: No error raised Users can create Python packages that provide their own custom error handlers and install them in their virtual environments before running their code which instantiates ``GlobalErrorHandler`` context managers. ``error_handler_0`` and ``error_handler_1`` can be used as examples to create these custom error handlers. In order for the error handlers to be registered, they need to create a class that inherits from ``opentelemetry.sdk.error_handler.ErrorHandler`` and at least one ``Exception``-type class. For example, this is an error handler that handles ``ZeroDivisionError`` exceptions: .. code:: python from opentelemetry.sdk.error_handler import ErrorHandler from logging import getLogger logger = getLogger(__name__) class ErrorHandler0(ErrorHandler, ZeroDivisionError): def handle(self, error: Exception, *args, **kwargs): logger.exception("ErrorHandler0 handling a ZeroDivisionError") To register this error handler, use the ``opentelemetry_error_handler`` entry point in the setup of the error handler package: .. code:: [options.entry_points] opentelemetry_error_handler = error_handler_0 = error_handler_0:ErrorHandler0 This entry point should point to the error handler class, ``ErrorHandler0`` in this case. python-opentelemetry-1.45.2/docs/examples/error_handler/error_handler_0/000077500000000000000000000000001527070070700264645ustar00rootroot00000000000000python-opentelemetry-1.45.2/docs/examples/error_handler/error_handler_0/README.rst000066400000000000000000000002611527070070700301520ustar00rootroot00000000000000Error Handler 0 =============== This is just an error handler for this example. The source files are available :scm_web:`here `. python-opentelemetry-1.45.2/docs/examples/error_handler/error_handler_0/pyproject.toml000066400000000000000000000021261527070070700314010ustar00rootroot00000000000000[build-system] requires = ["hatchling"] build-backend = "hatchling.build" [project] name = "error-handler-0" dynamic = ["version"] description = "This is just an error handler example package" 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", "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.3", ] [project.entry-points.opentelemetry_error_handler] error_handler_0 = "error_handler_0:ErrorHandler0" [tool.hatch.version] path = "src/error_handler_0/version/__init__.py" [tool.hatch.build.targets.sdist] include = [ "/src", ] [tool.hatch.build.targets.wheel] packages = ["src/opentelemetry"] python-opentelemetry-1.45.2/docs/examples/error_handler/error_handler_0/src/000077500000000000000000000000001527070070700272535ustar00rootroot00000000000000python-opentelemetry-1.45.2/docs/examples/error_handler/error_handler_0/src/error_handler_0/000077500000000000000000000000001527070070700323205ustar00rootroot00000000000000__init__.py000066400000000000000000000005741527070070700343600ustar00rootroot00000000000000python-opentelemetry-1.45.2/docs/examples/error_handler/error_handler_0/src/error_handler_0# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from logging import getLogger from opentelemetry.sdk.error_handler import ErrorHandler logger = getLogger(__name__) class ErrorHandler0(ErrorHandler, ZeroDivisionError): def _handle(self, error: Exception, *args, **kwargs): logger.exception("ErrorHandler0 handling a ZeroDivisionError") python-opentelemetry-1.45.2/docs/examples/error_handler/error_handler_0/src/error_handler_0/version/000077500000000000000000000000001527070070700340055ustar00rootroot00000000000000__init__.py000066400000000000000000000001471527070070700360410ustar00rootroot00000000000000python-opentelemetry-1.45.2/docs/examples/error_handler/error_handler_0/src/error_handler_0/version# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 __version__ = "0.23.dev0" python-opentelemetry-1.45.2/docs/examples/error_handler/error_handler_1/000077500000000000000000000000001527070070700264655ustar00rootroot00000000000000python-opentelemetry-1.45.2/docs/examples/error_handler/error_handler_1/README.rst000066400000000000000000000002611527070070700301530ustar00rootroot00000000000000Error Handler 1 =============== This is just an error handler for this example. The source files are available :scm_web:`here `. python-opentelemetry-1.45.2/docs/examples/error_handler/error_handler_1/pyproject.toml000066400000000000000000000021261527070070700314020ustar00rootroot00000000000000[build-system] requires = ["hatchling"] build-backend = "hatchling.build" [project] name = "error-handler-1" dynamic = ["version"] description = "This is just an error handler example package" 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", "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.3", ] [project.entry-points.opentelemetry_error_handler] error_handler_1 = "error_handler_1:ErrorHandler1" [tool.hatch.version] path = "src/error_handler_1/version/__init__.py" [tool.hatch.build.targets.sdist] include = [ "/src", ] [tool.hatch.build.targets.wheel] packages = ["src/opentelemetry"] python-opentelemetry-1.45.2/docs/examples/error_handler/error_handler_1/src/000077500000000000000000000000001527070070700272545ustar00rootroot00000000000000python-opentelemetry-1.45.2/docs/examples/error_handler/error_handler_1/src/error_handler_1/000077500000000000000000000000001527070070700323225ustar00rootroot00000000000000__init__.py000066400000000000000000000010711527070070700343530ustar00rootroot00000000000000python-opentelemetry-1.45.2/docs/examples/error_handler/error_handler_1/src/error_handler_1# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from logging import getLogger from opentelemetry.sdk.error_handler import ErrorHandler logger = getLogger(__name__) # pylint: disable=too-many-ancestors class ErrorHandler1(ErrorHandler, IndexError, KeyError): def _handle(self, error: Exception, *args, **kwargs): if isinstance(error, IndexError): logger.exception("ErrorHandler1 handling an IndexError") elif isinstance(error, KeyError): logger.exception("ErrorHandler1 handling a KeyError") python-opentelemetry-1.45.2/docs/examples/error_handler/error_handler_1/src/error_handler_1/version/000077500000000000000000000000001527070070700340075ustar00rootroot00000000000000__init__.py000066400000000000000000000001471527070070700360430ustar00rootroot00000000000000python-opentelemetry-1.45.2/docs/examples/error_handler/error_handler_1/src/error_handler_1/version# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 __version__ = "0.23.dev0" python-opentelemetry-1.45.2/docs/examples/error_handler/example.py000066400000000000000000000011361527070070700254250ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from opentelemetry.sdk.error_handler import GlobalErrorHandler # ZeroDivisionError to be handled by ErrorHandler0 with GlobalErrorHandler(): 1 / 0 print() # IndexError to be handled by ErrorHandler1 with GlobalErrorHandler(): [1][2] print() # KeyError to be handled by ErrorHandler1 with GlobalErrorHandler(): {1: 2}[2] print() # AssertionError to be handled by DefaultErrorHandler with GlobalErrorHandler(): assert False print() # No error raised with GlobalErrorHandler(): print("No error raised") python-opentelemetry-1.45.2/docs/examples/fork-process-model/000077500000000000000000000000001527070070700243045ustar00rootroot00000000000000python-opentelemetry-1.45.2/docs/examples/fork-process-model/README.rst000066400000000000000000000045411527070070700257770ustar00rootroot00000000000000Working With Fork Process Models ================================ The `BatchSpanProcessor` is not fork-safe and doesn't work well with application servers (Gunicorn, uWSGI) which are based on the pre-fork web server model. The `BatchSpanProcessor` spawns a thread to run in the background to export spans to the telemetry backend. During the fork, the child process inherits the lock which is held by the parent process and deadlock occurs. We can use fork hooks to get around this limitation of the span processor. Please see http://bugs.python.org/issue6721 for the problems about Python locks in (multi)threaded context with fork. The source code for the examples with Flask app are available :scm_web:`here `. Gunicorn post_fork hook ----------------------- .. code-block:: python from opentelemetry import trace from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchSpanProcessor def post_fork(server, worker): server.log.info("Worker spawned (pid: %s)", worker.pid) resource = Resource.create(attributes={ "service.name": "api-service" }) trace.set_tracer_provider(TracerProvider(resource=resource)) span_processor = BatchSpanProcessor( OTLPSpanExporter(endpoint="http://localhost:4317") ) trace.get_tracer_provider().add_span_processor(span_processor) uWSGI postfork decorator ------------------------ .. code-block:: python from uwsgidecorators import postfork from opentelemetry import trace from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchSpanProcessor @postfork def init_tracing(): resource = Resource.create(attributes={ "service.name": "api-service" }) trace.set_tracer_provider(TracerProvider(resource=resource)) span_processor = BatchSpanProcessor( OTLPSpanExporter(endpoint="http://localhost:4317") ) trace.get_tracer_provider().add_span_processor(span_processor) python-opentelemetry-1.45.2/docs/examples/fork-process-model/flask-gunicorn/000077500000000000000000000000001527070070700272265ustar00rootroot00000000000000python-opentelemetry-1.45.2/docs/examples/fork-process-model/flask-gunicorn/README.rst000066400000000000000000000005221527070070700307140ustar00rootroot00000000000000Flask + Gunicorn Example ======================== The source files of this example are available :scm_web:`here `. Installation ------------ .. code-block:: sh pip install -rrequirements.txt Run application --------------- .. code-block:: sh gunicorn app -c gunicorn.conf.py python-opentelemetry-1.45.2/docs/examples/fork-process-model/flask-gunicorn/app.py000066400000000000000000000023131527070070700303570ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import flask from flask import request from opentelemetry import trace from opentelemetry.instrumentation.flask import FlaskInstrumentor application = flask.Flask(__name__) FlaskInstrumentor().instrument_app(application) tracer = trace.get_tracer(__name__) def fib_slow(n): if n <= 1: return n return fib_slow(n - 1) + fib_fast(n - 2) def fib_fast(n): nth_fib = [0] * (n + 2) nth_fib[1] = 1 for i in range(2, n + 1): nth_fib[i] = nth_fib[i - 1] + nth_fib[i - 2] return nth_fib[n] @application.route("/fibonacci") def fibonacci(): n = int(request.args.get("n", 1)) with tracer.start_as_current_span("root"): with tracer.start_as_current_span("fib_slow") as slow_span: ans = fib_slow(n) slow_span.set_attribute("n", n) slow_span.set_attribute("nth_fibonacci", ans) with tracer.start_as_current_span("fib_fast") as fast_span: ans = fib_fast(n) fast_span.set_attribute("n", n) fast_span.set_attribute("nth_fibonacci", ans) return f"F({n}) is: ({ans})" if __name__ == "__main__": application.run() python-opentelemetry-1.45.2/docs/examples/fork-process-model/flask-gunicorn/gunicorn.conf.py000066400000000000000000000043141527070070700323520ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from opentelemetry import metrics, trace from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import ( OTLPMetricExporter, ) from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import ( OTLPSpanExporter, ) from opentelemetry.sdk.metrics import MeterProvider from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchSpanProcessor bind = "127.0.0.1:8000" # Sample Worker processes workers = 4 worker_class = "sync" worker_connections = 1000 timeout = 30 keepalive = 2 # Sample logging errorlog = "-" loglevel = "info" accesslog = "-" access_log_format = '%(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s"' def post_fork(server, worker): server.log.info("Worker spawned (pid: %s)", worker.pid) resource = Resource.create( attributes={ "service.name": "api-service", # If workers are not distinguished within attributes, traces and # metrics exported from each worker will be indistinguishable. While # not necessarily an issue for traces, it is confusing for almost # all metric types. A built-in way to identify a worker is by PID # but this may lead to high label cardinality. An alternative # workaround and additional discussion are available here: # https://github.com/benoitc/gunicorn/issues/1352 "worker": worker.pid, } ) trace.set_tracer_provider(TracerProvider(resource=resource)) # This uses insecure connection for the purpose of example. Please see the # OTLP Exporter documentation for other options. span_processor = BatchSpanProcessor(OTLPSpanExporter(endpoint="http://localhost:4317", insecure=True)) trace.get_tracer_provider().add_span_processor(span_processor) reader = PeriodicExportingMetricReader(OTLPMetricExporter(endpoint="http://localhost:4317")) metrics.set_meter_provider( MeterProvider( resource=resource, metric_readers=[reader], ) ) python-opentelemetry-1.45.2/docs/examples/fork-process-model/flask-gunicorn/requirements.txt000066400000000000000000000006741527070070700325210ustar00rootroot00000000000000click==8.1.7 Flask==3.1.3 googleapis-common-protos==1.52.0 grpcio==1.56.2 gunicorn==22.0.0 itsdangerous==2.1.2 Jinja2==3.1.6 MarkupSafe==2.1.3 opentelemetry-api==1.20.0 opentelemetry-exporter-otlp==1.20.0 opentelemetry-instrumentation==0.41b0 opentelemetry-instrumentation-flask==0.41b0 opentelemetry-instrumentation-wsgi==0.41b0 opentelemetry-sdk==1.20.0 protobuf==6.33.5 six==1.15.0 thrift==0.13.0 uWSGI==2.0.22 Werkzeug==3.1.6 wrapt==1.16.0 python-opentelemetry-1.45.2/docs/examples/fork-process-model/flask-uwsgi/000077500000000000000000000000001527070070700265405ustar00rootroot00000000000000python-opentelemetry-1.45.2/docs/examples/fork-process-model/flask-uwsgi/README.rst000066400000000000000000000006001527070070700302230ustar00rootroot00000000000000Flask + uWSGI Example ===================== The source files of this example are available :scm_web:`here `. Installation ------------ .. code-block:: sh pip install -rrequirements.txt Run application --------------- .. code-block:: sh uwsgi --http :8000 --wsgi-file app.py --callable application --master --enable-threads python-opentelemetry-1.45.2/docs/examples/fork-process-model/flask-uwsgi/app.py000066400000000000000000000037151527070070700277000ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import flask from flask import request from uwsgidecorators import postfork from opentelemetry import trace from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import ( OTLPSpanExporter, ) from opentelemetry.instrumentation.flask import FlaskInstrumentor from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchSpanProcessor application = flask.Flask(__name__) FlaskInstrumentor().instrument_app(application) tracer = trace.get_tracer(__name__) @postfork def init_tracing(): resource = Resource.create(attributes={"service.name": "api-service"}) trace.set_tracer_provider(TracerProvider(resource=resource)) # This uses insecure connection for the purpose of example. Please see the # OTLP Exporter documentation for other options. span_processor = BatchSpanProcessor(OTLPSpanExporter(endpoint="http://localhost:4317", insecure=True)) trace.get_tracer_provider().add_span_processor(span_processor) def fib_slow(n): if n <= 1: return n return fib_slow(n - 1) + fib_fast(n - 2) def fib_fast(n): nth_fib = [0] * (n + 2) nth_fib[1] = 1 for i in range(2, n + 1): nth_fib[i] = nth_fib[i - 1] + nth_fib[i - 2] return nth_fib[n] @application.route("/fibonacci") def fibonacci(): n = int(request.args.get("n", 1)) with tracer.start_as_current_span("root"): with tracer.start_as_current_span("fib_slow") as slow_span: ans = fib_slow(n) slow_span.set_attribute("n", n) slow_span.set_attribute("nth_fibonacci", ans) with tracer.start_as_current_span("fib_fast") as fast_span: ans = fib_fast(n) fast_span.set_attribute("n", n) fast_span.set_attribute("nth_fibonacci", ans) return f"F({n}) is: ({ans})" if __name__ == "__main__": application.run() python-opentelemetry-1.45.2/docs/examples/fork-process-model/flask-uwsgi/requirements.txt000066400000000000000000000006531527070070700320300ustar00rootroot00000000000000click==8.1.7 Flask==3.1.3 googleapis-common-protos==1.52.0 grpcio==1.56.2 itsdangerous==2.1.2 Jinja2==3.1.6 MarkupSafe==2.1.3 opentelemetry-api==1.20.0 opentelemetry-exporter-otlp==1.20.0 opentelemetry-instrumentation==0.41b0 opentelemetry-instrumentation-flask==0.41b0 opentelemetry-instrumentation-wsgi==0.41b0 opentelemetry-sdk==1.20.0 protobuf==6.33.5 six==1.15.0 thrift==0.13.0 uWSGI==2.0.22 Werkzeug==3.1.6 wrapt==1.16.0 python-opentelemetry-1.45.2/docs/examples/index.rst000066400000000000000000000001161527070070700224300ustar00rootroot00000000000000:orphan: Examples ======== .. toctree:: :maxdepth: 1 :glob: ** python-opentelemetry-1.45.2/docs/examples/logs/000077500000000000000000000000001527070070700215355ustar00rootroot00000000000000python-opentelemetry-1.45.2/docs/examples/logs/README.rst000066400000000000000000000100561527070070700232260ustar00rootroot00000000000000OpenTelemetry Logs SDK ====================== .. warning:: OpenTelemetry Python logs are in an experimental state. The APIs within :mod:`opentelemetry.sdk._logs` are subject to change in minor/patch releases and make no backward compatibility guarantees at this time. The source files of these examples are available :scm_web:`here `. .. note:: ``LoggingHandler`` has moved from the SDK to the ``opentelemetry-instrumentation-logging`` package in the contrib repo and is deprecated in ``opentelemetry-sdk``. Installation ------------ .. code-block:: sh pip install opentelemetry-api pip install opentelemetry-sdk pip install opentelemetry-exporter-otlp-proto-grpc pip install opentelemetry-instrumentation-logging Run the Example --------------- Start the Collector locally to see data being exported. Write the following file: .. code-block:: yaml # otel-collector-config.yaml receivers: otlp: protocols: grpc: endpoint: 0.0.0.0:4317 exporters: debug: verbosity: detailed service: pipelines: logs: receivers: [otlp] exporters: [debug] traces: receivers: [otlp] exporters: [debug] Then start the Docker container: .. code-block:: sh docker run \ -p 4317:4317 \ -v $(pwd)/otel-collector-config.yaml:/etc/otelcol-contrib/config.yaml \ otel/opentelemetry-collector-contrib:latest .. code-block:: sh $ python example.py The resulting logs will appear in the output from the collector and look similar to this: .. code-block:: sh ResourceLog #0 Resource SchemaURL: Resource attributes: -> telemetry.sdk.language: Str(python) -> telemetry.sdk.name: Str(opentelemetry) -> telemetry.sdk.version: Str(1.33.0.dev0) -> service.name: Str(shoppingcart) -> service.instance.id: Str(instance-12) ScopeLogs #0 ScopeLogs SchemaURL: InstrumentationScope myapp.area2 LogRecord #0 ObservedTimestamp: 2025-04-22 12:16:57.315179 +0000 UTC Timestamp: 2025-04-22 12:16:57.315152896 +0000 UTC SeverityText: WARN SeverityNumber: Warn(13) Body: Str(Jail zesty vixen who grabbed pay from quack.) Attributes: -> code.filepath: Str(/Users/jayclifford/Repos/opentelemetry-python/docs/examples/logs/example.py) -> code.function: Str() -> code.lineno: Int(47) Trace ID: Span ID: Flags: 0 LogRecord #1 ObservedTimestamp: 2025-04-22 12:16:57.31522 +0000 UTC Timestamp: 2025-04-22 12:16:57.315213056 +0000 UTC SeverityText: ERROR SeverityNumber: Error(17) Body: Str(The five boxing wizards jump quickly.) Attributes: -> code.filepath: Str(/Users/jayclifford/Repos/opentelemetry-python/docs/examples/logs/example.py) -> code.function: Str() -> code.lineno: Int(48) Trace ID: Span ID: Flags: 0 LogRecord #2 ObservedTimestamp: 2025-04-22 12:16:57.315445 +0000 UTC Timestamp: 2025-04-22 12:16:57.31543808 +0000 UTC SeverityText: ERROR SeverityNumber: Error(17) Body: Str(Hyderabad, we have a major problem.) Attributes: -> code.filepath: Str(/Users/jayclifford/Repos/opentelemetry-python/docs/examples/logs/example.py) -> code.function: Str() -> code.lineno: Int(61) Trace ID: 8a6739fffce895e694700944e2faf23e Span ID: a45337020100cb63 Flags: 1 ScopeLogs #1 ScopeLogs SchemaURL: InstrumentationScope myapp.area1 LogRecord #0 ObservedTimestamp: 2025-04-22 12:16:57.315242 +0000 UTC Timestamp: 2025-04-22 12:16:57.315234048 +0000 UTC SeverityText: ERROR SeverityNumber: Error(17) Body: Str(I have custom attributes.) Attributes: -> user_id: Str(user-123) -> code.filepath: Str(/Users/jayclifford/Repos/opentelemetry-python/docs/examples/logs/example.py) -> code.function: Str() -> code.lineno: Int(53) Trace ID: Span ID: Flags: 0 python-opentelemetry-1.45.2/docs/examples/logs/example.py000066400000000000000000000047411527070070700235500ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import logging from opentelemetry import trace from opentelemetry._logs import set_logger_provider from opentelemetry.exporter.otlp.proto.grpc._log_exporter import ( OTLPLogExporter, ) # this is available in the opentelemetry-instrumentation-logging package from opentelemetry.instrumentation.logging.handler import LoggingHandler from opentelemetry.sdk._logs import LoggerProvider from opentelemetry.sdk._logs.export import BatchLogRecordProcessor from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import ( BatchSpanProcessor, ConsoleSpanExporter, ) trace.set_tracer_provider(TracerProvider()) trace.get_tracer_provider().add_span_processor(BatchSpanProcessor(ConsoleSpanExporter())) logger_provider = LoggerProvider( resource=Resource.create( { "service.name": "shoppingcart", "service.instance.id": "instance-12", } ), ) set_logger_provider(logger_provider) exporter = OTLPLogExporter(insecure=True) logger_provider.add_log_record_processor(BatchLogRecordProcessor(exporter)) handler = LoggingHandler(level=logging.NOTSET, logger_provider=logger_provider) # Set the root logger level to NOTSET to ensure all messages are captured logging.getLogger().setLevel(logging.NOTSET) # Attach OTLP handler to root logger logging.getLogger().addHandler(handler) # Create different namespaced loggers # It is recommended to not use the root logger with OTLP handler # so telemetry is collected only for the application # Logger.propagate is True by default, ensure it is not set to False so log # records reach the root logger handler. See # https://docs.python.org/3/library/logging.html#logging.Logger.propagate logger1 = logging.getLogger("myapp.area1") logger2 = logging.getLogger("myapp.area2") logger1.debug("Quick zephyrs blow, vexing daft Jim.") logger1.info("How quickly daft jumping zebras vex.") logger2.warning("Jail zesty vixen who grabbed pay from quack.") logger2.error("The five boxing wizards jump quickly.") # Log custom attributes # Custom attributes are added on a per event basis user_id = "user-123" logger1.error("I have custom attributes.", extra={"user_id": user_id}) # Trace context correlation tracer = trace.get_tracer(__name__) with tracer.start_as_current_span("foo"): # Do something logger2.error("Hyderabad, we have a major problem.") logger_provider.shutdown() python-opentelemetry-1.45.2/docs/examples/logs/otel-collector-config.yaml000066400000000000000000000004541527070070700266160ustar00rootroot00000000000000receivers: otlp: protocols: grpc: endpoint: 0.0.0.0:4317 exporters: debug: verbosity: detailed service: pipelines: logs: receivers: [otlp] exporters: [debug] traces: receivers: [otlp] exporters: [debug]python-opentelemetry-1.45.2/docs/examples/metrics/000077500000000000000000000000001527070070700222375ustar00rootroot00000000000000python-opentelemetry-1.45.2/docs/examples/metrics/instruments/000077500000000000000000000000001527070070700246325ustar00rootroot00000000000000python-opentelemetry-1.45.2/docs/examples/metrics/instruments/README.rst000066400000000000000000000040321527070070700263200ustar00rootroot00000000000000OpenTelemetry Metrics SDK ========================= The source files of these examples are available :scm_web:`here `. Start the Collector locally to see data being exported. Write the following file: .. code-block:: yaml # otel-collector-config.yaml receivers: otlp: protocols: grpc: endpoint: 0.0.0.0:4317 exporters: debug: service: pipelines: metrics: receivers: [otlp] exporters: [debug] Then start the Docker container: .. code-block:: sh docker run \ -p 4317:4317 \ -v $(pwd)/otel-collector-config.yaml:/etc/otel/config.yaml \ otel/opentelemetry-collector-contrib:latest .. code-block:: sh $ python example.py The resulting metrics will appear in the output from the collector and look similar to this: .. code-block:: sh ScopeMetrics #0 ScopeMetrics SchemaURL: InstrumentationScope getting-started 0.1.2 Metric #0 Descriptor: -> Name: counter -> Description: -> Unit: -> DataType: Sum -> IsMonotonic: true -> AggregationTemporality: Cumulative NumberDataPoints #0 StartTimestamp: 2024-08-09 11:21:42.145179 +0000 UTC Timestamp: 2024-08-09 11:21:42.145325 +0000 UTC Value: 1 Metric #1 Descriptor: -> Name: updown_counter -> Description: -> Unit: -> DataType: Sum -> IsMonotonic: false -> AggregationTemporality: Cumulative NumberDataPoints #0 StartTimestamp: 2024-08-09 11:21:42.145202 +0000 UTC Timestamp: 2024-08-09 11:21:42.145325 +0000 UTC Value: -4 Metric #2 Descriptor: -> Name: histogram -> Description: -> Unit: -> DataType: Histogram -> AggregationTemporality: Cumulative HistogramDataPoints #0 StartTimestamp: 2024-08-09 11:21:42.145221 +0000 UTC Timestamp: 2024-08-09 11:21:42.145325 +0000 UTC Count: 1 python-opentelemetry-1.45.2/docs/examples/metrics/instruments/example.py000066400000000000000000000035701527070070700266440ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from collections.abc import Iterable from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import ( OTLPMetricExporter, ) from opentelemetry.metrics import ( CallbackOptions, Observation, get_meter_provider, set_meter_provider, ) from opentelemetry.sdk.metrics import MeterProvider from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader exporter = OTLPMetricExporter(insecure=True) reader = PeriodicExportingMetricReader(exporter) provider = MeterProvider(metric_readers=[reader]) set_meter_provider(provider) def observable_counter_func(options: CallbackOptions) -> Iterable[Observation]: yield Observation(1, {}) def observable_up_down_counter_func( options: CallbackOptions, ) -> Iterable[Observation]: yield Observation(-10, {}) def observable_gauge_func(options: CallbackOptions) -> Iterable[Observation]: yield Observation(9, {}) meter = get_meter_provider().get_meter("getting-started", "0.1.2") # Counter counter = meter.create_counter("counter") counter.add(1) # Async Counter observable_counter = meter.create_observable_counter( "observable_counter", [observable_counter_func], ) # UpDownCounter updown_counter = meter.create_up_down_counter("updown_counter") updown_counter.add(1) updown_counter.add(-5) # Async UpDownCounter observable_updown_counter = meter.create_observable_up_down_counter( "observable_updown_counter", [observable_up_down_counter_func] ) # Histogram histogram = meter.create_histogram("histogram") histogram.record(99.9) # Histogram with explicit bucket boundaries advisory histogram = meter.create_histogram( "histogram_with_advisory", explicit_bucket_boundaries_advisory=[0.0, 1.0, 2.0], ) histogram.record(99.9) # Async Gauge gauge = meter.create_observable_gauge("gauge", [observable_gauge_func]) python-opentelemetry-1.45.2/docs/examples/metrics/instruments/otel-collector-config.yaml000066400000000000000000000003371527070070700317130ustar00rootroot00000000000000receivers: otlp: protocols: grpc: endpoint: 0.0.0.0:4317 exporters: debug: service: pipelines: metrics: receivers: [otlp] exporters: [debug] python-opentelemetry-1.45.2/docs/examples/metrics/instruments/requirements.txt000066400000000000000000000001221527070070700301110ustar00rootroot00000000000000opentelemetry-api~=1.25 opentelemetry-sdk~=1.25 opentelemetry-exporter-otlp~=1.25 python-opentelemetry-1.45.2/docs/examples/metrics/prometheus-grafana/000077500000000000000000000000001527070070700260275ustar00rootroot00000000000000python-opentelemetry-1.45.2/docs/examples/metrics/prometheus-grafana/README.rst000066400000000000000000000042531527070070700275220ustar00rootroot00000000000000Prometheus Instrumentation ========================== This shows how to use ``opentelemetry-exporter-prometheus`` to automatically generate Prometheus metrics. The source files of these examples are available :scm_web:`here `. Preparation ----------- This example will be executed in a separate virtual environment: .. code-block:: $ mkdir prometheus_auto_instrumentation $ virtualenv prometheus_auto_instrumentation $ source prometheus_auto_instrumentation/bin/activate Installation ------------ .. code-block:: $ pip install -r requirements.txt Execution --------- .. code-block:: $ python ./prometheus-monitor.py $ Server is running at http://localhost:8000 Now you can visit http://localhost:8000/metrics to see Prometheus metrics. You should see something like: .. code-block:: # HELP python_gc_objects_collected_total Objects collected during gc # TYPE python_gc_objects_collected_total counter python_gc_objects_collected_total{generation="0"} 320.0 python_gc_objects_collected_total{generation="1"} 58.0 python_gc_objects_collected_total{generation="2"} 0.0 # HELP python_gc_objects_uncollectable_total Uncollectable objects found during GC # TYPE python_gc_objects_uncollectable_total counter python_gc_objects_uncollectable_total{generation="0"} 0.0 python_gc_objects_uncollectable_total{generation="1"} 0.0 python_gc_objects_uncollectable_total{generation="2"} 0.0 # HELP python_gc_collections_total Number of times this generation was collected # TYPE python_gc_collections_total counter python_gc_collections_total{generation="0"} 61.0 python_gc_collections_total{generation="1"} 5.0 python_gc_collections_total{generation="2"} 0.0 # HELP python_info Python platform information # TYPE python_info gauge python_info{implementation="CPython",major="3",minor="8",patchlevel="5",version="3.8.5"} 1.0 # HELP MyAppPrefix_my_counter_total # TYPE MyAppPrefix_my_counter_total counter MyAppPrefix_my_counter_total 964.0 ``MyAppPrefix_my_counter_total`` is the custom counter created in the application with the custom prefix ``MyAppPrefix``. python-opentelemetry-1.45.2/docs/examples/metrics/prometheus-grafana/prometheus-monitor.py000066400000000000000000000016111527070070700322600ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import random import time from prometheus_client import start_http_server from opentelemetry.exporter.prometheus import PrometheusMetricReader from opentelemetry.metrics import get_meter_provider, set_meter_provider from opentelemetry.sdk.metrics import MeterProvider # Start Prometheus client start_http_server(port=8000, addr="localhost") # Exporter to export metrics to Prometheus prefix = "MyAppPrefix" reader = PrometheusMetricReader(prefix=prefix) # Meter is responsible for creating and recording metrics set_meter_provider(MeterProvider(metric_readers=[reader])) meter = get_meter_provider().get_meter("view-name-change", "0.1.2") my_counter = meter.create_counter("my.counter") print("Server is running at http://localhost:8000") while 1: my_counter.add(random.randint(1, 10)) time.sleep(random.random()) python-opentelemetry-1.45.2/docs/examples/metrics/prometheus-grafana/requirements.txt000066400000000000000000000000761527070070700313160ustar00rootroot00000000000000opentelemetry-exporter-prometheus==1.12.0rc1 protobuf~=6.33.5 python-opentelemetry-1.45.2/docs/examples/metrics/reader/000077500000000000000000000000001527070070700235015ustar00rootroot00000000000000python-opentelemetry-1.45.2/docs/examples/metrics/reader/README.rst000066400000000000000000000025561527070070700252000ustar00rootroot00000000000000MetricReader configuration scenarios ==================================== These examples show how to customize the metrics that are output by the SDK using configuration on metric readers. There are multiple examples: * :scm_web:`preferred_aggregation.py `: Shows how to configure the preferred aggregation for metric instrument types. * :scm_web:`preferred_temporality.py `: Shows how to configure the preferred temporality for metric instrument types. * :scm_web:`preferred_exemplarfilter.py `: Shows how to configure the exemplar filter. * :scm_web:`synchronous_gauge_read.py `: Shows how to use `PeriodicExportingMetricReader` in a synchronous manner to explicitly control the collection of metrics. The source files of these examples are available :scm_web:`here `. Installation ------------ .. code-block:: sh pip install -r requirements.txt Run the Example --------------- .. code-block:: sh python .py The output will be shown in the console. Useful links ------------ - OpenTelemetry_ - :doc:`../../../api/metrics` .. _OpenTelemetry: https://github.com/open-telemetry/opentelemetry-python/ python-opentelemetry-1.45.2/docs/examples/metrics/reader/preferred_aggregation.py000066400000000000000000000024121527070070700303770ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import time from opentelemetry.metrics import get_meter_provider, set_meter_provider from opentelemetry.sdk.metrics import Counter, MeterProvider from opentelemetry.sdk.metrics.export import ( ConsoleMetricExporter, PeriodicExportingMetricReader, ) from opentelemetry.sdk.metrics.view import LastValueAggregation aggregation_last_value = {Counter: LastValueAggregation()} # Use console exporter for the example exporter = ConsoleMetricExporter( preferred_aggregation=aggregation_last_value, ) # The PeriodicExportingMetricReader takes the preferred aggregation # from the passed in exporter reader = PeriodicExportingMetricReader( exporter, export_interval_millis=5_000, ) provider = MeterProvider(metric_readers=[reader]) set_meter_provider(provider) meter = get_meter_provider().get_meter("preferred-aggregation", "0.1.2") counter = meter.create_counter("my-counter") # A counter normally would have an aggregation type of SumAggregation, # in which it's value would be determined by a cumulative sum. # In this example, the counter is configured with the LastValueAggregation, # which will only hold the most recent value. for x in range(10): counter.add(x) time.sleep(2.0) python-opentelemetry-1.45.2/docs/examples/metrics/reader/preferred_exemplarfilter.py000066400000000000000000000033001527070070700311300ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import time from opentelemetry import trace from opentelemetry.metrics import get_meter_provider, set_meter_provider from opentelemetry.sdk.metrics import MeterProvider from opentelemetry.sdk.metrics._internal.exemplar import AlwaysOnExemplarFilter from opentelemetry.sdk.metrics.export import ( ConsoleMetricExporter, PeriodicExportingMetricReader, ) from opentelemetry.sdk.trace import TracerProvider # Create an ExemplarFilter instance # Available values are AlwaysOffExemplarFilter, AlwaysOnExemplarFilter # and TraceBasedExemplarFilter. # The default value is `TraceBasedExemplarFilter`. # # You can also use the environment variable `OTEL_METRICS_EXEMPLAR_FILTER` # to change the default value. # # You can also define your own filter by implementing the abstract class # `ExemplarFilter` exemplar_filter = AlwaysOnExemplarFilter() exporter = ConsoleMetricExporter() reader = PeriodicExportingMetricReader( exporter, export_interval_millis=5_000, ) # Set up the MeterProvider with the ExemplarFilter provider = MeterProvider( metric_readers=[reader], exemplar_filter=exemplar_filter, # Pass the ExemplarFilter to the MeterProvider ) set_meter_provider(provider) meter = get_meter_provider().get_meter("exemplar-filter-example", "0.1.2") counter = meter.create_counter("my-counter") # Create a trace and span as the default exemplar filter `TraceBasedExemplarFilter` # will only store exemplar if a context exists trace.set_tracer_provider(TracerProvider()) tracer = trace.get_tracer(__name__) with tracer.start_as_current_span("foo"): for value in range(10): counter.add(value) time.sleep(2.0) python-opentelemetry-1.45.2/docs/examples/metrics/reader/preferred_temporality.py000066400000000000000000000035131527070070700304640ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import time from opentelemetry.metrics import get_meter_provider, set_meter_provider from opentelemetry.sdk.metrics import Counter, MeterProvider from opentelemetry.sdk.metrics.export import ( AggregationTemporality, ConsoleMetricExporter, PeriodicExportingMetricReader, ) temporality_cumulative = {Counter: AggregationTemporality.CUMULATIVE} temporality_delta = {Counter: AggregationTemporality.DELTA} # Use console exporters for the example # The metrics that are exported using this exporter will represent a cumulative value exporter = ConsoleMetricExporter( preferred_temporality=temporality_cumulative, ) # The metrics that are exported using this exporter will represent a delta value exporter2 = ConsoleMetricExporter( preferred_temporality=temporality_delta, ) # The PeriodicExportingMetricReader takes the preferred aggregation # from the passed in exporter reader = PeriodicExportingMetricReader( exporter, export_interval_millis=5_000, ) # The PeriodicExportingMetricReader takes the preferred aggregation # from the passed in exporter reader2 = PeriodicExportingMetricReader( exporter2, export_interval_millis=5_000, ) provider = MeterProvider(metric_readers=[reader, reader2]) set_meter_provider(provider) meter = get_meter_provider().get_meter("preferred-temporality", "0.1.2") counter = meter.create_counter("my-counter") # Two metrics are expected to be printed to the console per export interval. # The metric originating from the metric exporter with a preferred temporality # of cumulative will keep a running sum of all values added. # The metric originating from the metric exporter with a preferred temporality # of delta will have the sum value reset each export interval. counter.add(5) time.sleep(10) counter.add(20) python-opentelemetry-1.45.2/docs/examples/metrics/reader/requirements.txt000066400000000000000000000002061527070070700267630ustar00rootroot00000000000000opentelemetry-api==1.15.0 opentelemetry-sdk==1.15.0 opentelemetry-semantic-conventions==0.36b0 typing_extensions==4.5.0 wrapt==1.14.1 python-opentelemetry-1.45.2/docs/examples/metrics/reader/synchronous_gauge_read.py000066400000000000000000000044411527070070700306130ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import math from collections.abc import Iterable from opentelemetry.metrics import ( CallbackOptions, Observation, get_meter_provider, set_meter_provider, ) from opentelemetry.sdk.metrics import MeterProvider from opentelemetry.sdk.metrics.export import ( ConsoleMetricExporter, PeriodicExportingMetricReader, ) temperature = 0.0 humidity = 0.0 # Function called by the gauge to read the temperature def read_temperature(options: CallbackOptions) -> Iterable[Observation]: global temperature yield Observation(value=temperature, attributes={"room": "living-room"}) # Function called by the gauge to read the humidity def read_humidity(options: CallbackOptions) -> Iterable[Observation]: global humidity yield Observation(value=humidity, attributes={"room": "living-room"}) # Use console exporter for the example exporter = ConsoleMetricExporter() # The PeriodicExportingMetricReader If the time interval is set to math.inf # the reader will not invoke periodic collection reader = PeriodicExportingMetricReader( exporter, export_interval_millis=math.inf, ) provider = MeterProvider(metric_readers=[reader]) set_meter_provider(provider) meter = get_meter_provider().get_meter("synchronous_read", "0.1.2") gauge = meter.create_observable_gauge( name="synchronous_gauge_temperature", description="Gauge value captured synchronously", callbacks=[read_temperature], ) # Simulate synchronous reading of temperature print("--- Simulating synchronous reading of temperature ---", flush=True) temperature = 25.0 reader.collect() # Note: The reader will only collect the last value before `collect` is called print("--- Last value only ---", flush=True) temperature = 30.0 temperature = 35.0 reader.collect() # Invoking `collect` will read all measurements assigned to the reader gauge2 = meter.create_observable_gauge( name="synchronous_gauge_humidity", description="Gauge value captured synchronously", callbacks=[read_humidity], ) print("--- Multiple Measurements ---", flush=True) temperature = 20.0 humidity = 50.0 reader.collect() # Invoking `force_flush` will read all measurements assigned to the reader print("--- Invoking force_flush ---", flush=True) provider.force_flush() python-opentelemetry-1.45.2/docs/examples/metrics/sdk-metrics/000077500000000000000000000000001527070070700244645ustar00rootroot00000000000000python-opentelemetry-1.45.2/docs/examples/metrics/sdk-metrics/README.rst000066400000000000000000000023761527070070700261630ustar00rootroot00000000000000SDK Metrics =========== Some SDK components can emit telemetry about their internal state according to the `semantic conventions for OpenTelemetry SDK metrics `_. At the time of writing these semantic conventions are still in development and in order to have them exported you need to set the ``OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED`` environment variable to ``true``. The provided :scm_web:`sdk_metrics.py ` example shows how to setup manually the SDK in order to send them. Installation ------------ .. code-block:: sh pip install -r requirements.txt Run the Example --------------- Start an OTLP HTTP collector or compatible backend listening on the default endpoint, ``http://localhost:4318``. To use a different endpoint, configure ``OTEL_EXPORTER_OTLP_ENDPOINT`` or the signal-specific OTLP exporter environment variables. .. code-block:: sh python sdk_metrics.py The example sends a span, a log record, and the OpenTelemetry SDK health metrics emitted by the configured SDK components. Useful links ------------ - OpenTelemetry_ - :doc:`../../../api/metrics` .. _OpenTelemetry: https://github.com/open-telemetry/opentelemetry-python/ python-opentelemetry-1.45.2/docs/examples/metrics/sdk-metrics/requirements.txt000066400000000000000000000002161527070070700277470ustar00rootroot00000000000000opentelemetry-api==1.44.0 opentelemetry-exporter-otlp-proto-http==1.44.0 opentelemetry-sdk==1.44.0 opentelemetry-semantic-conventions==0.65b0 python-opentelemetry-1.45.2/docs/examples/metrics/sdk-metrics/sdk_metrics.py000066400000000000000000000047511527070070700273540ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import os import time from opentelemetry import _logs, metrics, trace from opentelemetry.exporter.otlp.proto.http._log_exporter import ( OTLPLogExporter, ) from opentelemetry.exporter.otlp.proto.http.metric_exporter import ( OTLPMetricExporter, ) from opentelemetry.exporter.otlp.proto.http.trace_exporter import ( OTLPSpanExporter, ) from opentelemetry.sdk._logs import LoggerProvider from opentelemetry.sdk._logs.export import SimpleLogRecordProcessor from opentelemetry.sdk.metrics import MeterProvider from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import SimpleSpanProcessor os.environ.setdefault("OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED", "true") resource = Resource.create({"service.name": "sdk-metrics-example"}) meter_provider = MeterProvider(resource=resource) metrics.set_meter_provider(meter_provider) # Pass the metrics provider into each SDK component that should report # internal health metrics. metric_exporter = OTLPMetricExporter(meter_provider=meter_provider) metric_reader = PeriodicExportingMetricReader( metric_exporter, export_interval_millis=250, # Intentionally low for example purposes only ) meter_provider.add_metric_reader(metric_reader) tracer_provider = TracerProvider( resource=resource, meter_provider=meter_provider, ) tracer_provider.add_span_processor( SimpleSpanProcessor( OTLPSpanExporter(meter_provider=meter_provider), meter_provider=meter_provider, ) ) trace.set_tracer_provider(tracer_provider) logger_provider = LoggerProvider( resource=resource, meter_provider=meter_provider, ) logger_provider.add_log_record_processor( SimpleLogRecordProcessor( OTLPLogExporter(meter_provider=meter_provider), meter_provider=meter_provider, ) ) _logs.set_logger_provider(logger_provider) tracer = trace.get_tracer(__name__) logger = _logs.get_logger(__name__) with tracer.start_as_current_span("example-span"): logger.emit(body="example log record") # Let the periodic reader export at least once so exporter self-metrics # are available for the final metrics export. time.sleep(0.5) tracer_provider.force_flush() logger_provider.force_flush() meter_provider.force_flush() tracer_provider.shutdown() logger_provider.shutdown() meter_provider.shutdown() python-opentelemetry-1.45.2/docs/examples/metrics/views/000077500000000000000000000000001527070070700233745ustar00rootroot00000000000000python-opentelemetry-1.45.2/docs/examples/metrics/views/README.rst000066400000000000000000000025341527070070700250670ustar00rootroot00000000000000View common scenarios ===================== These examples show how to customize the metrics that are output by the SDK using Views. There are multiple examples: * :scm_web:`change_aggregation.py `: Shows how to configure to change the default aggregation for an instrument. * :scm_web:`change_name.py `: Shows how to change the name of a metric. * :scm_web:`limit_num_of_attrs.py `: Shows how to limit the number of attributes that are output for a metric. * :scm_web:`drop_metrics_from_instrument.py `: Shows how to drop measurements from an instrument. * :scm_web:`change_reservoir_factory.py `: Shows how to use your own ``ExemplarReservoir`` The source files of these examples are available :scm_web:`here `. Installation ------------ .. code-block:: sh pip install -r requirements.txt Run the Example --------------- .. code-block:: sh python .py The output will be shown in the console. Useful links ------------ - OpenTelemetry_ - :doc:`../../../api/metrics` .. _OpenTelemetry: https://github.com/open-telemetry/opentelemetry-python/ python-opentelemetry-1.45.2/docs/examples/metrics/views/change_aggregation.py000066400000000000000000000023011527070070700275360ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import random import time from opentelemetry.metrics import get_meter_provider, set_meter_provider from opentelemetry.sdk.metrics import MeterProvider from opentelemetry.sdk.metrics.export import ( ConsoleMetricExporter, PeriodicExportingMetricReader, ) from opentelemetry.sdk.metrics.view import SumAggregation, View # Create a view matching the histogram instrument name `http.client.request.latency` # and configure the `SumAggregation` for the result metrics stream hist_to_sum_view = View(instrument_name="http.client.request.latency", aggregation=SumAggregation()) # Use console exporter for the example exporter = ConsoleMetricExporter() # Create a metric reader with stdout exporter reader = PeriodicExportingMetricReader(exporter, export_interval_millis=1_000) provider = MeterProvider( metric_readers=[ reader, ], views=[ hist_to_sum_view, ], ) set_meter_provider(provider) meter = get_meter_provider().get_meter("view-change-aggregation", "0.1.2") histogram = meter.create_histogram("http.client.request.latency") while 1: histogram.record(99.9) time.sleep(random.random()) python-opentelemetry-1.45.2/docs/examples/metrics/views/change_name.py000066400000000000000000000023031527070070700261710ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import random import time from opentelemetry.metrics import get_meter_provider, set_meter_provider from opentelemetry.sdk.metrics import Counter, MeterProvider from opentelemetry.sdk.metrics.export import ( ConsoleMetricExporter, PeriodicExportingMetricReader, ) from opentelemetry.sdk.metrics.view import View # Create a view matching the counter instrument `my.counter` # and configure the new name `my.counter.total` for the result metrics stream change_metric_name_view = View( instrument_type=Counter, instrument_name="my.counter", name="my.counter.total", ) # Use console exporter for the example exporter = ConsoleMetricExporter() # Create a metric reader with stdout exporter reader = PeriodicExportingMetricReader(exporter, export_interval_millis=1_000) provider = MeterProvider( metric_readers=[ reader, ], views=[ change_metric_name_view, ], ) set_meter_provider(provider) meter = get_meter_provider().get_meter("view-name-change", "0.1.2") my_counter = meter.create_counter("my.counter") while 1: my_counter.add(random.randint(1, 10)) time.sleep(random.random()) python-opentelemetry-1.45.2/docs/examples/metrics/views/change_reservoir_factory.py000066400000000000000000000047371527070070700310350ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import random import time from opentelemetry import trace from opentelemetry.metrics import get_meter_provider, set_meter_provider from opentelemetry.sdk.metrics import MeterProvider from opentelemetry.sdk.metrics._internal.aggregation import ( DefaultAggregation, _Aggregation, _ExplicitBucketHistogramAggregation, ) from opentelemetry.sdk.metrics._internal.exemplar import ( AlignedHistogramBucketExemplarReservoir, ExemplarReservoirBuilder, SimpleFixedSizeExemplarReservoir, ) from opentelemetry.sdk.metrics.export import ( ConsoleMetricExporter, PeriodicExportingMetricReader, ) from opentelemetry.sdk.metrics.view import View from opentelemetry.sdk.trace import TracerProvider # Create a custom reservoir factory with specified parameters def custom_reservoir_factory( aggregationType: type[_Aggregation], ) -> ExemplarReservoirBuilder: if issubclass(aggregationType, _ExplicitBucketHistogramAggregation): return AlignedHistogramBucketExemplarReservoir else: # Custom reservoir must accept `**kwargs` that may set the `size` for # _ExponentialBucketHistogramAggregation or the `boundaries` for # _ExplicitBucketHistogramAggregation return lambda **kwargs: SimpleFixedSizeExemplarReservoir( size=10, **{k: v for k, v in kwargs.items() if k != "size"}, ) # Create a view with the custom reservoir factory change_reservoir_factory_view = View( instrument_name="my.counter", name="name", aggregation=DefaultAggregation(), exemplar_reservoir_factory=custom_reservoir_factory, ) # Use console exporter for the example exporter = ConsoleMetricExporter() # Create a metric reader with stdout exporter reader = PeriodicExportingMetricReader(exporter, export_interval_millis=1_000) provider = MeterProvider( metric_readers=[ reader, ], views=[ change_reservoir_factory_view, ], ) set_meter_provider(provider) meter = get_meter_provider().get_meter("reservoir-factory-change", "0.1.2") my_counter = meter.create_counter("my.counter") # Create a trace and span as the default exemplar filter `TraceBasedExemplarFilter` # will only store exemplar if a context exists trace.set_tracer_provider(TracerProvider()) tracer = trace.get_tracer(__name__) with tracer.start_as_current_span("foo"): while 1: my_counter.add(random.randint(1, 10)) time.sleep(random.random()) python-opentelemetry-1.45.2/docs/examples/metrics/views/disable_default_aggregation.py000066400000000000000000000022431527070070700314250ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import random import time from opentelemetry.metrics import get_meter_provider, set_meter_provider from opentelemetry.sdk.metrics import MeterProvider from opentelemetry.sdk.metrics.export import ( ConsoleMetricExporter, PeriodicExportingMetricReader, ) from opentelemetry.sdk.metrics.view import ( DropAggregation, SumAggregation, View, ) # disable_default_aggregation. disable_default_aggregation = View(instrument_name="*", aggregation=DropAggregation()) exporter = ConsoleMetricExporter() reader = PeriodicExportingMetricReader(exporter, export_interval_millis=1_000) provider = MeterProvider( metric_readers=[ reader, ], views=[ disable_default_aggregation, View(instrument_name="mycounter", aggregation=SumAggregation()), ], ) set_meter_provider(provider) meter = get_meter_provider().get_meter("view-disable-default-aggregation", "0.1.2") # Create a view to configure aggregation specific for this counter. my_counter = meter.create_counter("mycounter") while 1: my_counter.add(random.randint(1, 10)) time.sleep(random.random()) python-opentelemetry-1.45.2/docs/examples/metrics/views/drop_metrics_from_instrument.py000066400000000000000000000021521527070070700317530ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import random import time from opentelemetry.metrics import get_meter_provider, set_meter_provider from opentelemetry.sdk.metrics import Counter, MeterProvider from opentelemetry.sdk.metrics.export import ( ConsoleMetricExporter, PeriodicExportingMetricReader, ) from opentelemetry.sdk.metrics.view import DropAggregation, View # Create a view matching the counter instrument `my.counter` # and configure the view to drop the aggregation. drop_aggregation_view = View( instrument_type=Counter, instrument_name="my.counter", aggregation=DropAggregation(), ) exporter = ConsoleMetricExporter() reader = PeriodicExportingMetricReader(exporter, export_interval_millis=1_000) provider = MeterProvider( metric_readers=[ reader, ], views=[ drop_aggregation_view, ], ) set_meter_provider(provider) meter = get_meter_provider().get_meter("view-drop-aggregation", "0.1.2") my_counter = meter.create_counter("my.counter") while 1: my_counter.add(random.randint(1, 10)) time.sleep(random.random()) python-opentelemetry-1.45.2/docs/examples/metrics/views/limit_num_of_attrs.py000066400000000000000000000030161527070070700276440ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import random import time from collections.abc import Iterable from opentelemetry.metrics import ( CallbackOptions, Observation, get_meter_provider, set_meter_provider, ) from opentelemetry.sdk.metrics import MeterProvider, ObservableGauge from opentelemetry.sdk.metrics.export import ( ConsoleMetricExporter, PeriodicExportingMetricReader, ) from opentelemetry.sdk.metrics.view import View # Create a view matching the observable gauge instrument `observable_gauge` # and configure the attributes in the result metric stream # to contain only the attributes with keys with `k_3` and `k_5` view_with_attributes_limit = View( instrument_type=ObservableGauge, instrument_name="observable_gauge", attribute_keys={"k_3", "k_5"}, ) exporter = ConsoleMetricExporter() reader = PeriodicExportingMetricReader(exporter, export_interval_millis=1_000) provider = MeterProvider( metric_readers=[ reader, ], views=[ view_with_attributes_limit, ], ) set_meter_provider(provider) meter = get_meter_provider().get_meter("reduce-cardinality-with-view", "0.1.2") def observable_gauge_func(options: CallbackOptions) -> Iterable[Observation]: attrs = {} for i in range(random.randint(1, 100)): attrs[f"k_{i}"] = f"v_{i}" yield Observation(1, attrs) # Async gauge observable_gauge = meter.create_observable_gauge( "observable_gauge", [observable_gauge_func], ) while 1: time.sleep(1) python-opentelemetry-1.45.2/docs/examples/metrics/views/requirements.txt000066400000000000000000000002061527070070700266560ustar00rootroot00000000000000opentelemetry-api==1.12.0 opentelemetry-sdk==1.12.0 opentelemetry-semantic-conventions==0.33b0 typing_extensions==4.5.0 wrapt==1.14.1 python-opentelemetry-1.45.2/docs/examples/multi-destination-exporting/000077500000000000000000000000001527070070700262575ustar00rootroot00000000000000python-opentelemetry-1.45.2/docs/examples/multi-destination-exporting/README.rst000066400000000000000000000045061527070070700277530ustar00rootroot00000000000000Multi-Destination Exporting =========================== This example shows how to export telemetry data to multiple destinations simultaneously. As described in the `OTLP specification `_, each destination should have implemented its own queuing, acknowledgement handling, and retry logic to prevent one slow or unavailable destination from blocking the others. The OpenTelemetry Python SDK achieves this by using a separate processor or reader per destination: * **Traces**: Use one ``BatchSpanProcessor`` per destination, each wrapping its own ``SpanExporter``. Add each processor to the ``TracerProvider`` via ``add_span_processor()``. * **Metrics**: Pass multiple ``MetricReader`` instances (each wrapping its own ``MetricExporter``) to the ``MeterProvider`` constructor via the ``metric_readers`` parameter. * **Logs**: Use one ``BatchLogRecordProcessor`` per destination, each wrapping its own ``LogExporter``. Add each processor to the ``LoggerProvider`` via ``add_log_record_processor()``. .. note:: The **Profiles** signal is not yet supported in the Python SDK. When it becomes available, the same pattern will apply. The source files of these examples are available :scm_web:`here `. Installation ------------ .. code-block:: sh pip install opentelemetry-api pip install opentelemetry-sdk pip install opentelemetry-exporter-otlp-proto-grpc pip install opentelemetry-exporter-otlp-proto-http pip install opentelemetry-instrumentation-logging # For LoggingHandler Run the Example --------------- .. code-block:: sh python multi_destination_traces.py python multi_destination_metrics.py python multi_destination_logs.py The output will be shown in the console for the ``ConsoleSpanExporter``, ``ConsoleMetricExporter``, and ``ConsoleLogRecordExporter`` destinations. The OTLP destinations require a running `collector `_. Useful links ------------ - `OTLP multi-destination exporting specification `_ - OpenTelemetry_ - :doc:`../../api/trace` - :doc:`../../api/metrics` - :doc:`../../api/_logs` .. _OpenTelemetry: https://github.com/open-telemetry/opentelemetry-python/ python-opentelemetry-1.45.2/docs/examples/multi-destination-exporting/multi_destination_logs.py000066400000000000000000000034401527070070700334110ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 """ This example shows how to export logs to multiple destinations. Each BatchLogRecordProcessor has its own queue and retry logic, so destinations do not block each other. """ import logging from opentelemetry._logs import set_logger_provider from opentelemetry.exporter.otlp.proto.grpc._log_exporter import ( OTLPLogExporter as GrpcLogExporter, ) from opentelemetry.exporter.otlp.proto.http._log_exporter import ( OTLPLogExporter as HttpLogExporter, ) # this is available in the opentelemetry-instrumentation-logging package from opentelemetry.instrumentation.logging.handler import LoggingHandler from opentelemetry.sdk._logs import LoggerProvider from opentelemetry.sdk._logs.export import ( BatchLogRecordProcessor, ConsoleLogRecordExporter, ) logger_provider = LoggerProvider() set_logger_provider(logger_provider) # Destination 1: OTLP over gRPC grpc_exporter = GrpcLogExporter(endpoint="http://localhost:4317", insecure=True) logger_provider.add_log_record_processor(BatchLogRecordProcessor(grpc_exporter)) # Destination 2: OTLP over HTTP http_exporter = HttpLogExporter(endpoint="http://localhost:4318/v1/logs") logger_provider.add_log_record_processor(BatchLogRecordProcessor(http_exporter)) # Destination 3: Console (for debugging) logger_provider.add_log_record_processor(BatchLogRecordProcessor(ConsoleLogRecordExporter())) # Bridge Python's logging to OpenTelemetry handler = LoggingHandler(level=logging.NOTSET, logger_provider=logger_provider) logging.getLogger().setLevel(logging.NOTSET) logging.getLogger().addHandler(handler) logger = logging.getLogger("myapp") logger.info("Logs are exported to all three destinations.") logger.warning("This warning also goes everywhere.") logger_provider.shutdown() python-opentelemetry-1.45.2/docs/examples/multi-destination-exporting/multi_destination_metrics.py000066400000000000000000000030301527070070700341060ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 """ This example shows how to export metrics to multiple destinations. Each PeriodicExportingMetricReader has its own collection interval and export queue, so destinations do not block each other. """ from opentelemetry import metrics from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import ( OTLPMetricExporter as GrpcMetricExporter, ) from opentelemetry.exporter.otlp.proto.http.metric_exporter import ( OTLPMetricExporter as HttpMetricExporter, ) from opentelemetry.sdk.metrics import MeterProvider from opentelemetry.sdk.metrics.export import ( ConsoleMetricExporter, PeriodicExportingMetricReader, ) # Destination 1: OTLP over gRPC grpc_reader = PeriodicExportingMetricReader(GrpcMetricExporter(endpoint="http://localhost:4317", insecure=True)) # Destination 2: OTLP over HTTP http_reader = PeriodicExportingMetricReader(HttpMetricExporter(endpoint="http://localhost:4318/v1/metrics")) # Destination 3: Console (for debugging) console_reader = PeriodicExportingMetricReader(ConsoleMetricExporter()) # Pass all readers to the MeterProvider provider = MeterProvider(metric_readers=[grpc_reader, http_reader, console_reader]) metrics.set_meter_provider(provider) meter = metrics.get_meter(__name__) counter = meter.create_counter("request.count", description="Number of requests") counter.add(1, {"endpoint": "/api/users"}) counter.add(1, {"endpoint": "/api/orders"}) print("Metrics are exported to all three destinations.") provider.shutdown() python-opentelemetry-1.45.2/docs/examples/multi-destination-exporting/multi_destination_traces.py000066400000000000000000000026051527070070700337300ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 """ This example shows how to export traces to multiple destinations. Each BatchSpanProcessor has its own queue and retry logic, so destinations do not block each other. """ from opentelemetry import trace from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import ( OTLPSpanExporter as GrpcSpanExporter, ) from opentelemetry.exporter.otlp.proto.http.trace_exporter import ( OTLPSpanExporter as HttpSpanExporter, ) from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import ( BatchSpanProcessor, ConsoleSpanExporter, ) provider = TracerProvider() trace.set_tracer_provider(provider) # Destination 1: OTLP over gRPC grpc_exporter = GrpcSpanExporter(endpoint="http://localhost:4317", insecure=True) provider.add_span_processor(BatchSpanProcessor(grpc_exporter)) # Destination 2: OTLP over HTTP http_exporter = HttpSpanExporter(endpoint="http://localhost:4318/v1/traces") provider.add_span_processor(BatchSpanProcessor(http_exporter)) # Destination 3: Console (for debugging) provider.add_span_processor(BatchSpanProcessor(ConsoleSpanExporter())) tracer = trace.get_tracer(__name__) with tracer.start_as_current_span("example-request"): with tracer.start_as_current_span("fetch-data"): print("Spans are exported to all three destinations.") provider.shutdown() python-opentelemetry-1.45.2/docs/examples/opencensus-exporter-tracer/000077500000000000000000000000001527070070700260775ustar00rootroot00000000000000python-opentelemetry-1.45.2/docs/examples/opencensus-exporter-tracer/README.rst000066400000000000000000000023461527070070700275730ustar00rootroot00000000000000OpenCensus Exporter =================== This example shows how to use the OpenCensus Exporter to export traces to the OpenTelemetry collector. The source files of this example are available :scm_web:`here `. Installation ------------ .. code-block:: sh pip install opentelemetry-api pip install opentelemetry-sdk pip install opentelemetry-exporter-opencensus Run the Example --------------- Before running the example, it's necessary to run the OpenTelemetry collector and Jaeger. The :scm_web:`docker ` folder contains a ``docker-compose`` template with the configuration of those services. .. code-block:: sh pip install docker-compose cd docker docker-compose up Now, the example can be executed: .. code-block:: sh python collector.py The traces are available in the Jaeger UI at http://localhost:16686/. Useful links ------------ - OpenTelemetry_ - `OpenTelemetry Collector`_ - :doc:`../../api/trace` - :doc:`../../exporter/opencensus/opencensus` .. _OpenTelemetry: https://github.com/open-telemetry/opentelemetry-python/ .. _OpenTelemetry Collector: https://github.com/open-telemetry/opentelemetry-collector python-opentelemetry-1.45.2/docs/examples/opencensus-exporter-tracer/collector.py000066400000000000000000000013561527070070700304440ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from opentelemetry import trace from opentelemetry.exporter.opencensus.trace_exporter import ( OpenCensusSpanExporter, ) from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchSpanProcessor exporter = OpenCensusSpanExporter(endpoint="localhost:55678") trace.set_tracer_provider(TracerProvider()) tracer = trace.get_tracer(__name__) span_processor = BatchSpanProcessor(exporter) trace.get_tracer_provider().add_span_processor(span_processor) with tracer.start_as_current_span("foo"), tracer.start_as_current_span("bar"): with tracer.start_as_current_span("baz"): print("Hello world from OpenTelemetry Python!") python-opentelemetry-1.45.2/docs/examples/opencensus-exporter-tracer/docker/000077500000000000000000000000001527070070700273465ustar00rootroot00000000000000python-opentelemetry-1.45.2/docs/examples/opencensus-exporter-tracer/docker/collector-config.yaml000066400000000000000000000005061527070070700334640ustar00rootroot00000000000000receivers: opencensus: endpoint: "0.0.0.0:55678" exporters: jaeger_grpc: endpoint: jaeger-all-in-one:14250 sending_queue: batch: debug: processors: queued_retry: service: pipelines: traces: receivers: [opencensus] exporters: [jaeger_grpc, debug] processors: [queued_retry] python-opentelemetry-1.45.2/docs/examples/opencensus-exporter-tracer/docker/docker-compose.yaml000066400000000000000000000010311527070070700331370ustar00rootroot00000000000000version: "2" services: # Collector collector: image: omnition/opentelemetry-collector-contrib:latest command: ["--config=/conf/collector-config.yaml", "--log-level=DEBUG"] volumes: - ./collector-config.yaml:/conf/collector-config.yaml ports: - "55678:55678" jaeger-all-in-one: image: jaegertracing/jaeger:2.21.0@sha256:3d0ac795ff98aa04d1be04311d2dac6c25b4bfc8322dc02e53bc5b170c5018c3 ports: - "16686:16686" - "6831:6831/udp" - "6832:6832/udp" - "14268" - "14250" python-opentelemetry-1.45.2/docs/examples/opencensus-shim/000077500000000000000000000000001527070070700237115ustar00rootroot00000000000000python-opentelemetry-1.45.2/docs/examples/opencensus-shim/.gitignore000066400000000000000000000000131527070070700256730ustar00rootroot00000000000000example.db python-opentelemetry-1.45.2/docs/examples/opencensus-shim/README.rst000066400000000000000000000046201527070070700254020ustar00rootroot00000000000000OpenCensus Shim ================ This example shows how to use the :doc:`opentelemetry-opencensus-shim package <../../shim/opencensus_shim/opencensus_shim>` to interact with libraries instrumented with `opencensus-python `_. The source files required to run this example are available :scm_web:`here `. Installation ------------ Jaeger ****** Start Jaeger .. code-block:: sh docker run --rm \ -p 4317:4317 \ -p 4318:4318 \ -p 16686:16686 \ jaegertracing/all-in-one:latest \ --log-level=debug Python Dependencies ******************* Install the Python dependencies in :scm_raw_web:`requirements.txt ` .. code-block:: sh pip install -r requirements.txt Alternatively, you can install the Python dependencies separately: .. code-block:: sh pip install \ opentelemetry-api \ opentelemetry-sdk \ opentelemetry-exporter-otlp \ opentelemetry-opencensus-shim \ opentelemetry-instrumentation-sqlite3 \ opencensus \ opencensus-ext-flask \ Flask Run the Application ------------------- Start the application in a terminal. .. code-block:: sh flask --app app run -h 0.0.0.0 Point your browser to the address printed out (probably http://127.0.0.1:5000). Alternatively, just use curl to trigger a request: .. code-block:: sh curl http://127.0.0.1:5000 Jaeger UI ********* Open the Jaeger UI in your browser at ``_ and view traces for the "opencensus-shim-example-flask" service. Click on a span named "span" in the scatter plot. You will see a span tree with the following structure: * ``span`` * ``query movies from db`` * ``SELECT`` * ``build response html`` The root span comes from OpenCensus Flask instrumentation. The children ``query movies from db`` and ``build response html`` come from the manual instrumentation using OpenTelemetry's :meth:`opentelemetry.trace.Tracer.start_as_current_span`. Finally, the ``SELECT`` span is created by OpenTelemetry's SQLite3 instrumentation. Everything is exported to Jaeger using the OpenTelemetry exporter. Useful links ------------ - OpenTelemetry_ - :doc:`../../shim/opencensus_shim/opencensus_shim` .. _OpenTelemetry: https://github.com/open-telemetry/opentelemetry-python/ python-opentelemetry-1.45.2/docs/examples/opencensus-shim/app.py000066400000000000000000000045501527070070700250470ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import sqlite3 from flask import Flask from opencensus.ext.flask.flask_middleware import FlaskMiddleware from opentelemetry import trace from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import ( OTLPSpanExporter, ) from opentelemetry.instrumentation.sqlite3 import SQLite3Instrumentor from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchSpanProcessor from opentelemetry.shim.opencensus import install_shim DB = "example.db" # Set up OpenTelemetry tracer_provider = TracerProvider( resource=Resource.create( { "service.name": "opencensus-shim-example-flask", } ) ) trace.set_tracer_provider(tracer_provider) # Configure OTel to export traces to Jaeger tracer_provider.add_span_processor( BatchSpanProcessor( OTLPSpanExporter( endpoint="localhost:4317", ) ) ) tracer = tracer_provider.get_tracer(__name__) # Install the shim to start bridging spans from OpenCensus to OpenTelemetry install_shim() # Instrument sqlite3 library SQLite3Instrumentor().instrument() # Setup Flask with OpenCensus instrumentation app = Flask(__name__) FlaskMiddleware(app) # Setup the application database def setup_db(): with sqlite3.connect(DB) as con: cur = con.cursor() cur.execute( """ CREATE TABLE IF NOT EXISTS movie( title, year, PRIMARY KEY(title, year) ) """ ) cur.execute( """ INSERT OR IGNORE INTO movie(title, year) VALUES ('Mission Telemetry', 2000), ('Observing the World', 2010), ('The Tracer', 1999), ('The Instrument', 2020) """ ) setup_db() @app.route("/") def hello_world(): lines = [] with ( tracer.start_as_current_span("query movies from db"), sqlite3.connect(DB) as con, ): cur = con.cursor() for title, year in cur.execute("SELECT title, year from movie"): lines.append(f"
  • {title} is from the year {year}
  • ") with tracer.start_as_current_span("build response html"): html = f"
      {''.join(lines)}
    " return html python-opentelemetry-1.45.2/docs/examples/opencensus-shim/requirements.txt000066400000000000000000000002521527070070700271740ustar00rootroot00000000000000opentelemetry-api opentelemetry-sdk opentelemetry-exporter-otlp opentelemetry-opencensus-shim opentelemetry-instrumentation-sqlite3 opencensus opencensus-ext-flask Flask python-opentelemetry-1.45.2/docs/examples/opentracing/000077500000000000000000000000001527070070700231025ustar00rootroot00000000000000python-opentelemetry-1.45.2/docs/examples/opentracing/README.rst000066400000000000000000000047461527070070700246040ustar00rootroot00000000000000OpenTracing Shim ================ This example shows how to use the :doc:`opentelemetry-opentracing-shim package <../../shim/opentracing_shim/opentracing_shim>` to interact with libraries instrumented with `opentracing-python `_. The included ``rediscache`` library creates spans via the OpenTracing Redis integration, `redis_opentracing `_. Spans are exported via the Jaeger exporter, which is attached to the OpenTelemetry tracer. The source files required to run this example are available :scm_web:`here `. Installation ------------ Jaeger ****** Start Jaeger .. code-block:: sh docker run --rm \ -p 4317:4317 \ -p 4318:4318 \ -p 16686:16686 \ jaegertracing/all-in-one:latest \ --log-level=debug Redis ***** Install Redis following the `instructions `_. Make sure that the Redis server is running by executing this: .. code-block:: sh redis-server Python Dependencies ******************* Install the Python dependencies in :scm_raw_web:`requirements.txt ` .. code-block:: sh pip install -r requirements.txt Alternatively, you can install the Python dependencies separately: .. code-block:: sh pip install \ opentelemetry-api \ opentelemetry-sdk \ opentelemetry-exporter-otlp \ opentelemetry-opentracing-shim \ redis \ redis_opentracing Run the Application ------------------- The example script calculates a few Fibonacci numbers and stores the results in Redis. The script, the ``rediscache`` library, and the OpenTracing Redis integration all contribute spans to the trace. To run the script: .. code-block:: sh python main.py After running, you can view the generated trace in the Jaeger UI. Jaeger UI ********* Open the Jaeger UI in your browser at ``_ and view traces for the "OpenTracing Shim Example" service. Each ``main.py`` run should generate a trace, and each trace should include multiple spans that represent calls to Redis. Note that tags and logs (OpenTracing) and attributes and events (OpenTelemetry) from both tracing systems appear in the exported trace. Useful links ------------ - OpenTelemetry_ - :doc:`../../shim/opentracing_shim/opentracing_shim` .. _OpenTelemetry: https://github.com/open-telemetry/opentelemetry-python/ python-opentelemetry-1.45.2/docs/examples/opentracing/__init__.py000066400000000000000000000000001527070070700252010ustar00rootroot00000000000000python-opentelemetry-1.45.2/docs/examples/opentracing/main.py000077500000000000000000000031601527070070700244030ustar00rootroot00000000000000#!/usr/bin/env python # Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from rediscache import RedisCache from opentelemetry import trace from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import ( OTLPSpanExporter, ) from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchSpanProcessor from opentelemetry.shim import opentracing_shim # Configure the tracer using the default implementation trace.set_tracer_provider(TracerProvider()) tracer_provider = trace.get_tracer_provider() # Create an OTLP gRPC span exporter otlp_exporter = OTLPSpanExporter( endpoint="http://localhost:4317", # For insecure connection, useful for testing insecure=True, ) # Add the exporter to the tracer provider trace.get_tracer_provider().add_span_processor(BatchSpanProcessor(otlp_exporter)) # Create an OpenTracing shim. This implements the OpenTracing tracer API, but # forwards calls to the underlying OpenTelemetry tracer. opentracing_tracer = opentracing_shim.create_tracer(tracer_provider) # Our example caching library expects an OpenTracing-compliant tracer. redis_cache = RedisCache(opentracing_tracer) # Application code uses an OpenTelemetry Tracer as usual. tracer = trace.get_tracer(__name__) @redis_cache def fib(number): """Get the Nth Fibonacci number, cache intermediate results in Redis.""" if number < 0: raise ValueError if number in (0, 1): return number return fib(number - 1) + fib(number - 2) with tracer.start_as_current_span("Fibonacci") as span: span.set_attribute("is_example", "yes :)") fib(4) python-opentelemetry-1.45.2/docs/examples/opentracing/rediscache.py000066400000000000000000000041441527070070700255510ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 """ This is an example of a library written to work with opentracing-python. It provides a simple caching decorator backed by Redis, and uses the OpenTracing Redis integration to automatically generate spans for each call to Redis. """ import pickle from functools import wraps # FIXME The pylint disablings are needed here because the code of this # example is being executed against the tox.ini of the main # opentelemetry-python project. Find a way to separate the two. import redis # pylint: disable=import-error import redis_opentracing # pylint: disable=import-error class RedisCache: """Redis-backed caching decorator, using OpenTracing! Args: tracer: an opentracing.tracer.Tracer """ def __init__(self, tracer): redis_opentracing.init_tracing(tracer) self.tracer = tracer self.client = redis.StrictRedis() def __call__(self, func): @wraps(func) def inner(*args, **kwargs): with self.tracer.start_active_span("Caching decorator") as scope1: # Pickle the call args to get a canonical key. Don't do this in # prod! key = pickle.dumps((func.__qualname__, args, kwargs)) pval = self.client.get(key) if pval is not None: val = pickle.loads(pval) scope1.span.log_kv({"msg": "Found cached value", "val": val}) return val scope1.span.log_kv({"msg": "Cache miss, calling function"}) with self.tracer.start_active_span(f'Call "{func.__name__}"') as scope2: scope2.span.set_tag("func", func.__name__) scope2.span.set_tag("args", str(args)) scope2.span.set_tag("kwargs", str(kwargs)) val = func(*args, **kwargs) scope2.span.set_tag("val", str(val)) # Let keys expire after 10 seconds self.client.setex(key, 10, pickle.dumps(val)) return val return inner python-opentelemetry-1.45.2/docs/examples/opentracing/requirements.txt000066400000000000000000000001671527070070700263720ustar00rootroot00000000000000opentelemetry-api opentelemetry-sdk opentelemetry-exporter-otlp opentelemetry-opentracing-shim redis redis_opentracing python-opentelemetry-1.45.2/docs/examples/sqlcommenter/000077500000000000000000000000001527070070700233025ustar00rootroot00000000000000python-opentelemetry-1.45.2/docs/examples/sqlcommenter/README.rst000066400000000000000000000113061527070070700247720ustar00rootroot00000000000000sqlcommenter ============ This is an example of how to use OpenTelemetry Python instrumention with sqlcommenter to enrich database query statements with contextual information. For more information on sqlcommenter concepts, see: * `Semantic Conventions - Database Spans `_ * `sqlcommenter `_ The source files of this example are available :scm_web:`here `. This example uses Docker to manage a database server and OpenTelemetry collector. Run MySQL server ---------------- A running MySQL server with general logs enabled will store query statements with context resulting from the sqlcommenter feature enabled in this example. .. code-block:: sh cd books_database docker build -t books-db . docker run -d --name books-db -p 3366:3306 books-db cd .. Check that the run is working and the general log is available: .. code-block:: sh docker exec -it books-db tail -f /var/log/general.log Run OpenTelemetry Collector --------------------------- Running the OpenTelemetry collector will show the MySQL instrumentor's comment-in-span-attribute feature, which this example has also enabled. .. code-block:: sh docker run \ -p 4317:4317 \ -v $(pwd)/collector-config.yaml:/etc/otel/config.yaml \ otel/opentelemetry-collector-contrib:latest Run the sqlcommenter example ---------------------------- Set up and activate a Python virtual environment. Install these dependencies of the sqlcommenter example: .. code-block:: sh pip install opentelemetry-sdk \ opentelemetry-exporter-otlp-proto-grpc \ opentelemetry-instrumentation-mysql \ mysql-connector-python Then, run this script, which instruments all mysql-connector calls with two sqlcommenter features opted in. .. code-block:: sh python instrumented_query.py Note that OpenTelemetry instrumentation with sqlcommenter is also available for other Python database client drivers/object relation mappers (ORMs). See full list at `instrumentation`_. .. _instrumentation: https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/instrumentation Check MySQL server general log and spans for sqlcomment ------------------------------------------------------- After running the query script, check the MySQL general log contents: .. code-block:: sh docker exec -it books-db tail -f /var/log/general.log For each instrumented ``SELECT`` call, a query was made and logged with a sqlcomment appended. For example: .. code:: 2025-09-02T18:49:06.981980Z 186 Query SELECT * FROM authors WHERE id = 1 /*db_driver='mysql.connector%%3A9.4.0',dbapi_level='2.0',dbapi_threadsafety=1,driver_paramstyle='pyformat',mysql_client_version='9.4.0',traceparent='00-2c45248f2beefdd9688b0a94eb4ac9ee-4f3af9a825aae9b1-01'*/ In the running OpenTelemetry collector, you'll also see one span per ``SELECT`` call. Each of those span's trace ID and span ID will correspond to a query log sqlcomment. With the comment-in-attribute feature enabled, the span's ``db.statement`` attribute will also contain the sqlcomment. For example: .. code:: ScopeSpans #0 ScopeSpans SchemaURL: https://opentelemetry.io/schemas/1.11.0 InstrumentationScope opentelemetry.instrumentation.mysql 0.57b0 Span #0 Trace ID : 2c45248f2beefdd9688b0a94eb4ac9ee Parent ID : ID : 4f3af9a825aae9b1 Name : SELECT Kind : Client Start time : 2025-09-02 18:49:06.982341 +0000 UTC End time : 2025-09-02 18:49:06.98463 +0000 UTC Status code : Unset Status message : Attributes: -> db.system: Str(mysql) -> db.name: Str(books) -> db.statement: Str(SELECT * FROM authors WHERE id = %s /*db_driver='mysql.connector%%3A9.4.0',dbapi_level='2.0',dbapi_threadsafety=1,driver_paramstyle='pyformat',mysql_client_version='9.4.0',traceparent='00-2c45248f2beefdd9688b0a94eb4ac9ee-4f3af9a825aae9b1-01'*/) -> db.user: Str(books) -> net.peer.name: Str(localhost) -> net.peer.port: Int(3366) References ---------- * `OpenTelemetry Project `_ * `OpenTelemetry Collector `_ * `OpenTelemetry MySQL instrumentation `_ * `Semantic Conventions - Database Spans `_ * `sqlcommenter `_python-opentelemetry-1.45.2/docs/examples/sqlcommenter/books_database/000077500000000000000000000000001527070070700262435ustar00rootroot00000000000000python-opentelemetry-1.45.2/docs/examples/sqlcommenter/books_database/Dockerfile000066400000000000000000000017011527070070700302340ustar00rootroot00000000000000FROM mysql:8.4@sha256:0744ee5ef89ce6ccfa13de3e579fe6b9e27f93dd70da9c06d2c908b1b193fb8d ENV MYSQL_ROOT_PASSWORD=root ENV MYSQL_DATABASE=books ADD books.sql /docker-entrypoint-initdb.d/ RUN echo "CREATE USER IF NOT EXISTS 'books'@'%' IDENTIFIED WITH mysql_native_password BY 'books123';" > /docker-entrypoint-initdb.d/01-create-user.sql && \ echo "GRANT ALL PRIVILEGES ON books.* TO 'books'@'%';" >> /docker-entrypoint-initdb.d/01-create-user.sql && \ echo "FLUSH PRIVILEGES;" >> /docker-entrypoint-initdb.d/01-create-user.sql # Prepare general logs RUN mkdir -p /var/log && \ touch /var/log/general.log && \ chown mysql:mysql /var/log/general.log EXPOSE 3306 HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \ CMD mysqladmin ping -p${MYSQL_ROOT_PASSWORD} || exit 1 # Start MySQL with general logging enabled and compatible authentication CMD ["mysqld", "--general-log=1", "--general-log-file=/var/log/general.log"]python-opentelemetry-1.45.2/docs/examples/sqlcommenter/books_database/books.sql000066400000000000000000000044401527070070700301030ustar00rootroot00000000000000-- MySQL dump for Books Database -- Database: books_db -- Generated on: 2025-08-29 SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO"; START TRANSACTION; SET time_zone = "+00:00"; -- Database: `books` CREATE DATABASE IF NOT EXISTS `books` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE `books`; -- -------------------------------------------------------- -- Table structure for table `authors` DROP TABLE IF EXISTS `authors`; CREATE TABLE `authors` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(255) NOT NULL, `home_town` varchar(255) DEFAULT NULL, `birthdate` date DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- Dumping data for table `authors` INSERT INTO `authors` (`id`, `name`, `home_town`, `birthdate`) VALUES (1, 'Frank Herbert', 'Tacoma, Washington', '1920-10-08'), (2, 'Isaac Asimov', 'Petrovichi, Russia', '1920-01-02'), (3, 'Terry Pratchett', 'Beaconsfield, England', '1948-04-28'); -- -------------------------------------------------------- -- Table structure for table `books` DROP TABLE IF EXISTS `books`; CREATE TABLE `books` ( `id` int(11) NOT NULL AUTO_INCREMENT, `title` varchar(255) NOT NULL, `author_id` int(11) NOT NULL, `year_published` int(4) DEFAULT NULL, `genre` varchar(100) DEFAULT NULL, PRIMARY KEY (`id`), KEY `fk_author` (`author_id`), CONSTRAINT `fk_author` FOREIGN KEY (`author_id`) REFERENCES `authors` (`id`) ON DELETE CASCADE ON UPDATE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- Dumping data for table `books` INSERT INTO `books` (`id`, `title`, `author_id`, `year_published`, `genre`) VALUES (1, 'Dune', 1, 1965, 'Science Fiction'), (2, 'Foundation', 2, 1951, 'Science Fiction'), (3, 'The Colour of Magic', 3, 1983, 'Fantasy Comedy'); -- -------------------------------------------------------- -- Additional books to show the many-to-one relationship INSERT INTO `books` (`id`, `title`, `author_id`, `year_published`, `genre`) VALUES (4, 'Dune Messiah', 1, 1969, 'Science Fiction'), (5, 'I, Robot', 2, 1950, 'Science Fiction'), (6, 'Good Omens', 3, 1990, 'Fantasy Comedy'); -- -------------------------------------------------------- -- Auto increment values ALTER TABLE `authors` AUTO_INCREMENT = 4; ALTER TABLE `books` AUTO_INCREMENT = 7; COMMIT;python-opentelemetry-1.45.2/docs/examples/sqlcommenter/collector-config.yaml000066400000000000000000000003431527070070700274170ustar00rootroot00000000000000receivers: otlp: protocols: grpc: endpoint: 0.0.0.0:4317 exporters: debug: verbosity: detailed service: pipelines: traces: receivers: [otlp] processors: [] exporters: [debug] python-opentelemetry-1.45.2/docs/examples/sqlcommenter/instrumented_query.py000066400000000000000000000030061527070070700276210ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from mysql.connector import connect from opentelemetry import trace from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import ( OTLPSpanExporter, ) from opentelemetry.instrumentation.mysql import MySQLInstrumentor from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchSpanProcessor resource = Resource.create( attributes={ "service.name": "sqlcommenter-example", } ) trace.set_tracer_provider(TracerProvider(resource=resource)) span_processor = BatchSpanProcessor(OTLPSpanExporter(endpoint="http://localhost:4317")) trace.get_tracer_provider().add_span_processor(span_processor) cnx = connect( host="localhost", port=3366, user="books", password="books123", database="books", ) # Instruments MySQL queries with sqlcommenter enabled # and comment-in-span-attribute enabled. # Returns wrapped connection to generate traces. cnx = MySQLInstrumentor().instrument_connection( connection=cnx, enable_commenter=True, enable_attribute_commenter=True, ) cursor = cnx.cursor() statement = "SELECT * FROM authors WHERE id = %s" # Each SELECT query generates one mysql log with sqlcomment # and one OTel span with `db.statement` attribute that also # includes sqlcomment. for cid in range(1, 4): cursor.execute(statement, (cid,)) rows = cursor.fetchall() print(f"Found author: {rows[0]}") print("Done.") python-opentelemetry-1.45.2/docs/exporter/000077500000000000000000000000001527070070700206235ustar00rootroot00000000000000python-opentelemetry-1.45.2/docs/exporter/index.rst000066400000000000000000000001201527070070700224550ustar00rootroot00000000000000:orphan: Exporters ========= .. toctree:: :maxdepth: 1 :glob: ** python-opentelemetry-1.45.2/docs/exporter/opencensus/000077500000000000000000000000001527070070700230055ustar00rootroot00000000000000python-opentelemetry-1.45.2/docs/exporter/opencensus/opencensus.rst000066400000000000000000000002241527070070700257170ustar00rootroot00000000000000OpenCensus Exporter =================== .. automodule:: opentelemetry.exporter.opencensus :members: :undoc-members: :show-inheritance: python-opentelemetry-1.45.2/docs/exporter/otlp/000077500000000000000000000000001527070070700216015ustar00rootroot00000000000000python-opentelemetry-1.45.2/docs/exporter/otlp/otlp.rst000066400000000000000000000045671527070070700233250ustar00rootroot00000000000000OpenTelemetry OTLP Exporters ============================ .. automodule:: opentelemetry.exporter.otlp :members: :undoc-members: :show-inheritance: opentelemetry.exporter.otlp.proto.http --------------------------------------- .. automodule:: opentelemetry.exporter.otlp.proto.http :members: :undoc-members: :show-inheritance: .. automodule:: opentelemetry.exporter.otlp.proto.http.trace_exporter :members: :undoc-members: :show-inheritance: .. automodule:: opentelemetry.exporter.otlp.proto.http.metric_exporter :members: :undoc-members: :show-inheritance: .. automodule:: opentelemetry.exporter.otlp.proto.http._log_exporter :members: :undoc-members: :show-inheritance: opentelemetry.exporter.otlp.proto.grpc --------------------------------------- .. automodule:: opentelemetry.exporter.otlp.proto.grpc :members: :undoc-members: :show-inheritance: .. automodule:: opentelemetry.exporter.otlp.proto.grpc.trace_exporter :members: :undoc-members: :show-inheritance: .. automodule:: opentelemetry.exporter.otlp.proto.grpc.metric_exporter :members: :undoc-members: :show-inheritance: .. automodule:: opentelemetry.exporter.otlp.proto.grpc._log_exporter :members: :undoc-members: :show-inheritance: opentelemetry.exporter.otlp.json.file --------------------------------------- .. automodule:: opentelemetry.exporter.otlp.json.file :no-members: :no-undoc-members: .. automodule:: opentelemetry.exporter.otlp.json.file.trace_exporter :members: :undoc-members: :show-inheritance: .. automodule:: opentelemetry.exporter.otlp.json.file.metric_exporter :members: :undoc-members: :show-inheritance: .. automodule:: opentelemetry.exporter.otlp.json.file._log_exporter :members: :undoc-members: :show-inheritance: opentelemetry.exporter.otlp.json.http --------------------------------------- .. automodule:: opentelemetry.exporter.otlp.json.http :no-members: :no-undoc-members: .. automodule:: opentelemetry.exporter.otlp.json.http.trace_exporter :members: :undoc-members: :show-inheritance: .. automodule:: opentelemetry.exporter.otlp.json.http.metric_exporter :members: :undoc-members: :show-inheritance: .. automodule:: opentelemetry.exporter.otlp.json.http._log_exporter :members: :undoc-members: :show-inheritance: python-opentelemetry-1.45.2/docs/exporter/prometheus/000077500000000000000000000000001527070070700230165ustar00rootroot00000000000000python-opentelemetry-1.45.2/docs/exporter/prometheus/prometheus.rst000066400000000000000000000114221527070070700257430ustar00rootroot00000000000000OpenTelemetry Prometheus Exporter ================================= .. automodule:: opentelemetry.exporter.prometheus :members: :undoc-members: :show-inheritance: Installation ------------ The OpenTelemetry Prometheus Exporter package is available on PyPI:: pip install opentelemetry-exporter-prometheus Usage ----- The Prometheus exporter starts an HTTP server that collects metrics and serializes them to Prometheus text format on request:: from prometheus_client import start_http_server from opentelemetry import metrics from opentelemetry.exporter.prometheus import PrometheusMetricReader from opentelemetry.sdk.metrics import MeterProvider from opentelemetry.sdk.resources import SERVICE_NAME, Resource # Service name is required for most backends resource = Resource.create(attributes={ SERVICE_NAME: "your-service-name" }) # Start Prometheus client start_http_server(port=9464, addr="localhost") # Initialize PrometheusMetricReader which pulls metrics from the SDK # on-demand to respond to scrape requests reader = PrometheusMetricReader() provider = MeterProvider(resource=resource, metric_readers=[reader]) metrics.set_meter_provider(provider) Scope labels ------------ By default, the Prometheus exporter adds instrumentation scope information as labels on every exported metric. These labels include ``otel_scope_name``, ``otel_scope_version``, and ``otel_scope_schema_url``. Instrumentation scope attributes are exported with the ``otel_scope_`` prefix:: from prometheus_client import start_http_server from opentelemetry import metrics from opentelemetry.exporter.prometheus import PrometheusMetricReader from opentelemetry.sdk.metrics import MeterProvider start_http_server(port=9464, addr="localhost") reader = PrometheusMetricReader() provider = MeterProvider(metric_readers=[reader]) metrics.set_meter_provider(provider) meter = metrics.get_meter( "checkout", "1.2.3", schema_url="https://opentelemetry.io/schemas/1.21.0", attributes={"region": "us-east-1"}, ) counter = meter.create_counter("orders") counter.add(1, {"environment": "production"}) The exported metric includes labels such as ``otel_scope_name="checkout"``, ``otel_scope_version="1.2.3"``, ``otel_scope_schema_url="https://opentelemetry.io/schemas/1.21.0"``, ``otel_scope_region="us-east-1"``, and ``environment="production"``. To omit instrumentation scope labels from exported metrics, set ``scope_info_enabled`` to ``False``:: reader = PrometheusMetricReader(scope_info_enabled=False) Resource labels --------------- By default, resource attributes are exported on the ``target_info`` metric. To also add selected resource attributes as Prometheus labels on every exported metric, pass a ``resource_attribute_filter`` callback to ``PrometheusMetricReader``. The callback receives the original resource attribute key and returns ``True`` for attributes that should be copied to metric labels:: from prometheus_client import start_http_server from opentelemetry import metrics from opentelemetry.exporter.prometheus import PrometheusMetricReader from opentelemetry.sdk.metrics import MeterProvider from opentelemetry.sdk.resources import SERVICE_NAME, Resource resource = Resource.create( attributes={ SERVICE_NAME: "checkout-service", "service.namespace": "shop", "deployment.environment": "production", } ) start_http_server(port=9464, addr="localhost") included_resource_attrs = {SERVICE_NAME, "service.namespace"} reader = PrometheusMetricReader( resource_attribute_filter=lambda key: key in included_resource_attrs ) provider = MeterProvider(resource=resource, metric_readers=[reader]) metrics.set_meter_provider(provider) meter = metrics.get_meter(__name__) counter = meter.create_counter("orders") counter.add(1) The exported metric includes ``service_name="checkout-service"`` and ``service_namespace="shop"`` labels. Resource attribute keys are sanitized to valid Prometheus label names, and metric attributes with the same sanitized name take precedence over copied resource attributes. Configuration ------------- The following environment variables are supported: * ``OTEL_EXPORTER_PROMETHEUS_HOST`` (default: "localhost"): The host to bind to * ``OTEL_EXPORTER_PROMETHEUS_PORT`` (default: 9464): The port to bind to Limitations ----------- * No multiprocessing support: The Prometheus exporter is not designed to operate in multiprocessing environments (see `#3747 `_). References ---------- * `Prometheus `_ * `OpenTelemetry Project `_ python-opentelemetry-1.45.2/docs/exporter/zipkin/000077500000000000000000000000001527070070700221275ustar00rootroot00000000000000python-opentelemetry-1.45.2/docs/exporter/zipkin/zipkin.rst000066400000000000000000000006061527070070700241670ustar00rootroot00000000000000OpenTelemetry Zipkin Exporters ============================== .. automodule:: opentelemetry.exporter.zipkin :members: :undoc-members: :show-inheritance: .. automodule:: opentelemetry.exporter.zipkin.json :members: :undoc-members: :show-inheritance: .. automodule:: opentelemetry.exporter.zipkin.proto.http :members: :undoc-members: :show-inheritance: python-opentelemetry-1.45.2/docs/getting_started/000077500000000000000000000000001527070070700221425ustar00rootroot00000000000000python-opentelemetry-1.45.2/docs/getting_started/__init__.py000066400000000000000000000000001527070070700242410ustar00rootroot00000000000000python-opentelemetry-1.45.2/docs/getting_started/flask_example.py000066400000000000000000000016141527070070700253310ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # flask_example.py import flask import requests from opentelemetry import trace from opentelemetry.instrumentation.flask import FlaskInstrumentor from opentelemetry.instrumentation.requests import RequestsInstrumentor from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import ( BatchSpanProcessor, ConsoleSpanExporter, ) trace.set_tracer_provider(TracerProvider()) trace.get_tracer_provider().add_span_processor(BatchSpanProcessor(ConsoleSpanExporter())) app = flask.Flask(__name__) FlaskInstrumentor().instrument_app(app) RequestsInstrumentor().instrument() tracer = trace.get_tracer(__name__) @app.route("/") def hello(): with tracer.start_as_current_span("example-request"): requests.get("http://www.example.com", timeout=10) return "hello" app.run(port=5000) python-opentelemetry-1.45.2/docs/getting_started/metrics_example.py000066400000000000000000000034101527070070700256730ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # metrics.py # This is still work in progress as the metrics SDK is being implemented from collections.abc import Iterable from opentelemetry.metrics import ( CallbackOptions, Observation, get_meter_provider, set_meter_provider, ) from opentelemetry.sdk.metrics import MeterProvider from opentelemetry.sdk.metrics.export import ( ConsoleMetricExporter, PeriodicExportingMetricReader, ) exporter = ConsoleMetricExporter() reader = PeriodicExportingMetricReader(exporter) provider = MeterProvider(metric_readers=[reader]) set_meter_provider(provider) def observable_counter_func(options: CallbackOptions) -> Iterable[Observation]: yield Observation(1, {}) def observable_up_down_counter_func( options: CallbackOptions, ) -> Iterable[Observation]: yield Observation(-10, {}) def observable_gauge_func(options: CallbackOptions) -> Iterable[Observation]: yield Observation(9, {}) meter = get_meter_provider().get_meter("getting-started", "0.1.2") # Counter counter = meter.create_counter("counter") counter.add(1) # Async Counter observable_counter = meter.create_observable_counter("observable_counter", [observable_counter_func]) # UpDownCounter updown_counter = meter.create_up_down_counter("updown_counter") updown_counter.add(1) updown_counter.add(-5) # Async UpDownCounter observable_updown_counter = meter.create_observable_up_down_counter( "observable_updown_counter", [observable_up_down_counter_func] ) # Histogram histogram = meter.create_histogram("histogram") histogram.record(99.9) # Async Gauge observable_gauge = meter.create_observable_gauge("observable_gauge", [observable_gauge_func]) # Sync Gauge gauge = meter.create_gauge("gauge") gauge.set(1) python-opentelemetry-1.45.2/docs/getting_started/otlpcollector_example.py000066400000000000000000000015541527070070700271210ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # otcollector.py from opentelemetry import trace from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import ( OTLPSpanExporter, ) from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchSpanProcessor span_exporter = OTLPSpanExporter( # optional # endpoint="myCollectorURL:4317", # credentials=ChannelCredentials(credentials), # headers=(("metadata", "metadata")), ) tracer_provider = TracerProvider() trace.set_tracer_provider(tracer_provider) span_processor = BatchSpanProcessor(span_exporter) tracer_provider.add_span_processor(span_processor) # Configure the tracer to use the collector exporter tracer = trace.get_tracer_provider().get_tracer(__name__) with tracer.start_as_current_span("foo"): print("Hello world!") python-opentelemetry-1.45.2/docs/getting_started/tests/000077500000000000000000000000001527070070700233045ustar00rootroot00000000000000python-opentelemetry-1.45.2/docs/getting_started/tests/__init__.py000066400000000000000000000001141527070070700254110ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 python-opentelemetry-1.45.2/docs/getting_started/tests/requirements.txt000066400000000000000000000011251527070070700265670ustar00rootroot00000000000000asgiref==3.7.2 attrs==23.1.0 certifi==2024.7.4 charset-normalizer==2.0.12 click==8.1.7 Flask==2.3.3 idna==3.15 iniconfig==2.0.0 itsdangerous==2.1.2 Jinja2==3.1.6 MarkupSafe==2.1.3 packaging==24.0 pluggy==1.6.0 py-cpuinfo==9.0.0 pytest==9.0.3 requests==2.33.0 tomli==2.0.1 typing_extensions==4.12.0 urllib3==2.7.0 Werkzeug==3.1.6 wrapt==1.15.0 -e opentelemetry-semantic-conventions -e opentelemetry-proto -e exporter/opentelemetry-exporter-otlp-proto-common -e exporter/opentelemetry-exporter-otlp-common -e exporter/opentelemetry-exporter-otlp-proto-grpc -e opentelemetry-api -e opentelemetry-sdk python-opentelemetry-1.45.2/docs/getting_started/tests/test_flask.py000066400000000000000000000023271527070070700260210ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import os import subprocess import sys import unittest from time import sleep import requests from requests.adapters import HTTPAdapter from requests.packages.urllib3.util.retry import ( # pylint: disable=import-error Retry, ) class TestFlask(unittest.TestCase): def test_flask(self): dirpath = os.path.dirname(os.path.realpath(__file__)) server_script = f"{dirpath}/../flask_example.py" server = subprocess.Popen( # pylint: disable=consider-using-with [sys.executable, server_script], stdout=subprocess.PIPE, ) retry_strategy = Retry(total=10, backoff_factor=1) adapter = HTTPAdapter(max_retries=retry_strategy) http = requests.Session() http.mount("http://", adapter) try: result = http.get("http://localhost:5000") self.assertEqual(result.status_code, 200) sleep(5) finally: server.terminate() output = str(server.stdout.read()) self.assertIn('"name": "GET"', output) self.assertIn('"name": "example-request"', output) self.assertIn('"name": "GET /"', output) python-opentelemetry-1.45.2/docs/getting_started/tests/test_metrics.py000066400000000000000000000054001527070070700263620ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import json import os import subprocess import sys import unittest class TestMetrics(unittest.TestCase): def test_metrics(self): """Test that metrics example produces expected values""" # Run the metrics example test_script = f"{os.path.dirname(os.path.realpath(__file__))}/../metrics_example.py" result = subprocess.run( [sys.executable, test_script], capture_output=True, text=True, timeout=10, check=True, ) # Script should run successfully self.assertEqual(result.returncode, 0) # Parse the JSON output output_data = json.loads(result.stdout) # Get the metrics from the JSON structure metrics = output_data["resource_metrics"][0]["scope_metrics"][0]["metrics"] # Create a lookup dict for easier testing metrics_by_name = {metric["name"]: metric for metric in metrics} # Test Counter: should be 1 (called counter.add(1)) counter_value = metrics_by_name["counter"]["data"]["data_points"][0]["value"] self.assertEqual(counter_value, 1, "Counter should have value 1") # Test UpDownCounter: should be -4 (1 + (-5) = -4) updown_value = metrics_by_name["updown_counter"]["data"]["data_points"][0]["value"] self.assertEqual(updown_value, -4, "UpDownCounter should have value -4") # Test Histogram: should have count=1, sum=99.9 histogram_data = metrics_by_name["histogram"]["data"]["data_points"][0] self.assertEqual(histogram_data["count"], 1, "Histogram should have count 1") self.assertEqual(histogram_data["sum"], 99.9, "Histogram should have sum 99.9") # Test Gauge: should be 1 (last value set) gauge_value = metrics_by_name["gauge"]["data"]["data_points"][0]["value"] self.assertEqual(gauge_value, 1, "Gauge should have value 1") # Test Observable Counter: should be 1 (from callback) obs_counter_value = metrics_by_name["observable_counter"]["data"]["data_points"][0]["value"] self.assertEqual(obs_counter_value, 1, "Observable counter should have value 1") # Test Observable UpDownCounter: should be -10 (from callback) obs_updown_value = metrics_by_name["observable_updown_counter"]["data"]["data_points"][0]["value"] self.assertEqual( obs_updown_value, -10, "Observable updown counter should have value -10", ) # Test Observable Gauge: should be 9 (from callback) obs_gauge_value = metrics_by_name["observable_gauge"]["data"]["data_points"][0]["value"] self.assertEqual(obs_gauge_value, 9, "Observable gauge should have value 9") python-opentelemetry-1.45.2/docs/getting_started/tests/test_otlpcollector.py000066400000000000000000000020731527070070700276040ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # oltpcollector_example.py import os import subprocess import sys import unittest class TestOTLPCollector(unittest.TestCase): def test_otlpcollector(self): """Test that OTLP collector example outputs 'Hello world!'""" dirpath = os.path.dirname(os.path.realpath(__file__)) test_script = f"{dirpath}/../otlpcollector_example.py" # Run the script with a short timeout since it will retry forever with subprocess.Popen( [sys.executable, test_script], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, ) as process: # Wait 2 seconds then kill it (enough time to print "Hello world!") try: stdout, _ = process.communicate(timeout=2) except subprocess.TimeoutExpired: process.kill() stdout, _ = process.communicate() # Check that it printed the expected message self.assertIn("Hello world!", stdout) python-opentelemetry-1.45.2/docs/getting_started/tests/test_tracing.py000066400000000000000000000010551527070070700263450ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import os import subprocess import sys import unittest class TestBasicTracerExample(unittest.TestCase): def test_basic_tracer(self): dirpath = os.path.dirname(os.path.realpath(__file__)) test_script = f"{dirpath}/../tracing_example.py" output = subprocess.check_output((sys.executable, test_script)).decode() self.assertIn('"name": "foo"', output) self.assertIn('"name": "bar"', output) self.assertIn('"name": "baz"', output) python-opentelemetry-1.45.2/docs/getting_started/tracing_example.py000066400000000000000000000012071527070070700256560ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # tracing.py from opentelemetry import trace from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import ( BatchSpanProcessor, ConsoleSpanExporter, ) provider = TracerProvider() processor = BatchSpanProcessor(ConsoleSpanExporter()) provider.add_span_processor(processor) trace.set_tracer_provider(provider) tracer = trace.get_tracer(__name__) with tracer.start_as_current_span("foo"), tracer.start_as_current_span("bar"): with tracer.start_as_current_span("baz"): print("Hello world from OpenTelemetry Python!") python-opentelemetry-1.45.2/docs/index.rst000066400000000000000000000023571527070070700206230ustar00rootroot00000000000000OpenTelemetry-Python API Reference ================================== .. image:: https://img.shields.io/badge/slack-chat-green.svg :target: https://cloud-native.slack.com/archives/C01PD4HUVBL :alt: Slack Chat Welcome to the docs for the `Python OpenTelemetry implementation `_. For an introduction to OpenTelemetry, see the `OpenTelemetry website docs `_. To learn how to instrument your Python code, see `Getting Started `_. For project status, information about releases, installation instructions and more, see `Python `_. Getting Started --------------- * `Getting Started `_ * `Frequently Asked Questions and Cookbook `_ .. toctree:: :maxdepth: 1 :caption: Core Packages :name: packages api/index sdk/index .. toctree:: :maxdepth: 2 :caption: More :glob: exporter/index shim/index examples/index * :ref:`genindex` * :ref:`modindex` * :ref:`search` python-opentelemetry-1.45.2/docs/make.bat000066400000000000000000000014231527070070700203600ustar00rootroot00000000000000@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 python-opentelemetry-1.45.2/docs/sdk/000077500000000000000000000000001527070070700175345ustar00rootroot00000000000000python-opentelemetry-1.45.2/docs/sdk/_logs.export.rst000066400000000000000000000002471527070070700227140ustar00rootroot00000000000000opentelemetry.sdk._logs.export ============================== .. automodule:: opentelemetry.sdk._logs.export :members: :undoc-members: :show-inheritance: python-opentelemetry-1.45.2/docs/sdk/_logs.rst000066400000000000000000000003711527070070700213720ustar00rootroot00000000000000opentelemetry.sdk._logs package =============================== Submodules ---------- .. toctree:: _logs.export Module contents --------------- .. automodule:: opentelemetry.sdk._logs :members: :undoc-members: :show-inheritance: python-opentelemetry-1.45.2/docs/sdk/configuration.rst000066400000000000000000000127401527070070700231410ustar00rootroot00000000000000Declarative Configuration ========================= .. note:: Declarative configuration support is new in this release and may still have rough edges. If you hit a problem, please open an issue on the `opentelemetry-python tracker `_. Declarative configuration lets you configure the OpenTelemetry SDK from a single YAML (or JSON) file instead of setting many individual ``OTEL_*`` environment variables or writing provider-construction code by hand. The file format is defined by the `OpenTelemetry configuration specification `_. A single file describes your resource, providers, processors, exporters, samplers, and propagators. The SDK reads the file, validates it against the configuration schema, and applies it globally. Installing ---------- Declarative configuration lives in a separate, experimental package: .. code-block:: sh pip install opentelemetry-configuration Enabling with an environment variable ------------------------------------- Point the SDK at a file with the ``OTEL_CONFIG_FILE`` environment variable. When it is set, the file is the sole source of SDK construction. Spec-defined ``OTEL_*`` variables with schema equivalents are ignored. Environment variables can still be read via ``${env:VAR}`` substitution inside the file (see `Environment variable substitution`_). .. code-block:: sh export OTEL_CONFIG_FILE=/etc/otel/otel-config.yaml opentelemetry-instrument python app.py Example configuration --------------------- The following file configures traces, metrics, and logs to be exported over OTLP/HTTP. The source is available :scm_web:`here `. .. code-block:: yaml file_format: "1.0" resource: attributes: - name: service.name value: my-service - name: deployment.environment.name value: ${DEPLOYMENT_ENVIRONMENT:-development} tracer_provider: processors: - batch: exporter: otlp_http: endpoint: https://example.com:4318/v1/traces headers: - name: api-key value: ${OTLP_API_KEY} sampler: parent_based: root: always_on: {} meter_provider: readers: - periodic: interval: 60000 exporter: otlp_http: endpoint: https://example.com:4318/v1/metrics headers: - name: api-key value: ${OTLP_API_KEY} logger_provider: processors: - batch: exporter: otlp_http: endpoint: https://example.com:4318/v1/logs headers: - name: api-key value: ${OTLP_API_KEY} Instrumentation --------------- The ``instrumentation/development.python`` section activates Python instrumentors by their ``opentelemetry_instrumentor`` entry-point name. Set ``enabled: false`` to suppress an instrumentor without removing its entry, and pass any other keys as keyword arguments to ``instrument()``: .. code-block:: yaml instrumentation/development: python: requests: enabled: true urllib3: enabled: true max_spans_per_request: 10 If the instrumentor class declares a ``configuration`` class attribute pointing to a dataclass, the options are validated and type-coerced through the same pipeline used for SDK component configuration before being forwarded to ``instrument()``. Instrumentors that are already active (for example because ``opentelemetry-instrument`` ran before the file was applied) are silently skipped. Environment variable substitution ---------------------------------- Values in the file may reference environment variables, which keeps secrets such as API keys out of the file itself. Substitution happens before the file is parsed. * ``${VAR}``: replaced with the value of ``VAR``. If ``VAR`` is unset, loading fails with an error. * ``${VAR:-default}``: replaced with ``VAR`` if set, otherwise ``default``. * ``$$``: a literal ``$``. In the example above, ``${OTLP_API_KEY}`` is required, while ``${DEPLOYMENT_ENVIRONMENT:-development}`` falls back to ``development`` when unset. Behavior notes -------------- * When ``OTEL_CONFIG_FILE`` is set, the file is authoritative for SDK construction; spec-defined ``OTEL_*`` variables with schema equivalents are not consulted. Environment variables can still be read indirectly by components the file enables (for example resource detectors) and via ``${env:VAR}`` substitution. * Python-implementation extensions (``OTEL_PYTHON_*`` variables such as ``OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED`` or ``OTEL_PYTHON_TRACER_CONFIGURATOR``) are **not** applied when ``OTEL_CONFIG_FILE`` is set: the env-var initialisation path is skipped entirely. If your app currently relies on one of these and you are migrating to a config file, plan to capture the equivalent behaviour in the file (or in code) instead. * Sections omitted from the file leave the corresponding global provider unset (a no-op provider), per the specification. * Setting ``disabled: true`` at the top level turns the SDK into a no-op. See also -------- * `OpenTelemetry configuration specification `_ * :doc:`environment_variables`: the environment-variable configuration path python-opentelemetry-1.45.2/docs/sdk/environment_variables.rst000066400000000000000000000011021527070070700246540ustar00rootroot00000000000000opentelemetry.sdk.environment_variables ======================================= The OpenTelemetry Python SDK can be configured through environment variables. These variables control behaviors such as exporter selection, resource attributes, span limits, and trace sampling. They follow the conventions defined by the `OpenTelemetry specification `_. .. toctree:: :maxdepth: 1 .. automodule:: opentelemetry.sdk.environment_variables :members: :undoc-members: :show-inheritance: python-opentelemetry-1.45.2/docs/sdk/error_handler.rst000066400000000000000000000002721527070070700231150ustar00rootroot00000000000000opentelemetry.sdk.error_handler package ======================================= .. automodule:: opentelemetry.sdk.error_handler :members: :undoc-members: :show-inheritance: python-opentelemetry-1.45.2/docs/sdk/index.rst000066400000000000000000000012611527070070700213750ustar00rootroot00000000000000OpenTelemetry Python SDK ======================== The OpenTelemetry Python SDK provides the reference implementation of the :doc:`OpenTelemetry Python API `. It includes the concrete classes for managing and exporting traces, metrics, and logs — such as ``TracerProvider``, ``MeterProvider``, span processors, metric readers, and exporters. The SDK is responsible for sampling, batching, and delivering telemetry data to backends. Install the SDK in your application to configure how telemetry is collected, processed, and exported. .. toctree:: :maxdepth: 1 _logs resources trace metrics error_handler environment_variables configuration python-opentelemetry-1.45.2/docs/sdk/metrics.export.rst000066400000000000000000000002551527070070700232560ustar00rootroot00000000000000opentelemetry.sdk.metrics.export ================================ .. automodule:: opentelemetry.sdk.metrics.export :members: :undoc-members: :show-inheritance: python-opentelemetry-1.45.2/docs/sdk/metrics.rst000066400000000000000000000003611527070070700217340ustar00rootroot00000000000000opentelemetry.sdk.metrics package ================================== Submodules ---------- .. toctree:: metrics.export metrics.view .. automodule:: opentelemetry.sdk.metrics :members: :undoc-members: :show-inheritance: python-opentelemetry-1.45.2/docs/sdk/metrics.view.rst000066400000000000000000000002471527070070700227100ustar00rootroot00000000000000opentelemetry.sdk.metrics.view ============================== .. automodule:: opentelemetry.sdk.metrics.view :members: :undoc-members: :show-inheritance: python-opentelemetry-1.45.2/docs/sdk/resources.rst000066400000000000000000000033711527070070700223040ustar00rootroot00000000000000opentelemetry.sdk.resources package ========================================== Host resource detection ----------------------- The host resource detector populates the attributes defined by the `host resource semantic conventions `_. Enable the host resource detector by setting :envvar:`OTEL_EXPERIMENTAL_RESOURCE_DETECTORS` before starting your application: .. code-block:: sh export OTEL_EXPERIMENTAL_RESOURCE_DETECTORS=host Resources created with :meth:`opentelemetry.sdk.resources.Resource.create` will then include ``host.name``, ``host.arch``, and, when available, ``host.id``. If you already configure other detectors, add ``host`` to the comma-separated list. The detector obtains ``host.id`` using the sources listed for a `non-privileged machine id lookup `_: * Linux: ``/etc/machine-id``, falling back to ``/var/lib/dbus/machine-id``. * BSD: ``/etc/hostid``, falling back to ``/bin/kenv -q smbios.system.uuid``. * macOS: ``IOPlatformUUID`` from ``/usr/sbin/ioreg -rd1 -c IOPlatformExpertDevice``. * Windows: ``MachineGuid`` from ``HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Cryptography``, using the 64-bit registry view. If the lookup fails or the operating system is unsupported, ``host.id`` is omitted by default while ``host.name`` and ``host.arch`` are retained. You can provide an explicit value through :envvar:`OTEL_RESOURCE_ATTRIBUTES`, for example ``OTEL_RESOURCE_ATTRIBUTES=host.id=my-host-id``. With the detector order shown above, this value takes precedence over the detected value. API --- .. automodule:: opentelemetry.sdk.resources :members: :undoc-members: :show-inheritance: python-opentelemetry-1.45.2/docs/sdk/trace.export.rst000066400000000000000000000002621527070070700227040ustar00rootroot00000000000000opentelemetry.sdk.trace.export ========================================== .. automodule:: opentelemetry.sdk.trace.export :members: :undoc-members: :show-inheritance:python-opentelemetry-1.45.2/docs/sdk/trace.id_generator.rst000066400000000000000000000002711527070070700240250ustar00rootroot00000000000000opentelemetry.sdk.trace.id_generator ==================================== .. automodule:: opentelemetry.sdk.trace.id_generator :members: :undoc-members: :show-inheritance: python-opentelemetry-1.45.2/docs/sdk/trace.rst000066400000000000000000000004301527070070700213610ustar00rootroot00000000000000opentelemetry.sdk.trace package =============================== Submodules ---------- .. toctree:: trace.export trace.id_generator trace.sampling util.instrumentation .. automodule:: opentelemetry.sdk.trace :members: :undoc-members: :show-inheritance: python-opentelemetry-1.45.2/docs/sdk/trace.sampling.rst000066400000000000000000000002661527070070700232010ustar00rootroot00000000000000opentelemetry.sdk.trace.sampling ========================================== .. automodule:: opentelemetry.sdk.trace.sampling :members: :undoc-members: :show-inheritance:python-opentelemetry-1.45.2/docs/sdk/util.instrumentation.rst000066400000000000000000000002121527070070700245000ustar00rootroot00000000000000opentelemetry.sdk.util.instrumentation ========================================== .. automodule:: opentelemetry.sdk.util.instrumentation python-opentelemetry-1.45.2/docs/shim/000077500000000000000000000000001527070070700177135ustar00rootroot00000000000000python-opentelemetry-1.45.2/docs/shim/index.rst000066400000000000000000000001101527070070700215440ustar00rootroot00000000000000:orphan: Shims ===== .. toctree:: :maxdepth: 1 :glob: ** python-opentelemetry-1.45.2/docs/shim/opencensus_shim/000077500000000000000000000000001527070070700231155ustar00rootroot00000000000000python-opentelemetry-1.45.2/docs/shim/opencensus_shim/opencensus_shim.rst000066400000000000000000000002151527070070700270470ustar00rootroot00000000000000OpenCensus Shim for OpenTelemetry ================================== .. automodule:: opentelemetry.shim.opencensus :no-show-inheritance: python-opentelemetry-1.45.2/docs/shim/opentracing_shim/000077500000000000000000000000001527070070700232445ustar00rootroot00000000000000python-opentelemetry-1.45.2/docs/shim/opentracing_shim/opentracing_shim.rst000066400000000000000000000002241527070070700273250ustar00rootroot00000000000000OpenTracing Shim for OpenTelemetry ================================== .. automodule:: opentelemetry.shim.opentracing_shim :no-show-inheritance: python-opentelemetry-1.45.2/eachdist.ini000066400000000000000000000025761527070070700203220ustar00rootroot00000000000000# These will be sorted first in that order. # All packages that are depended upon by others should be listed here. [DEFAULT] sortfirst= opentelemetry-api opentelemetry-sdk opentelemetry-proto opentelemetry-distro tests/opentelemetry-test-utils exporter/* [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-common opentelemetry-exporter-otlp-proto-grpc opentelemetry-exporter-otlp-proto-http opentelemetry-exporter-otlp opentelemetry-api [prerelease] version=0.66b2 packages= opentelemetry-opentracing-shim opentelemetry-opencensus-shim opentelemetry-exporter-http-transport opentelemetry-exporter-opencensus opentelemetry-exporter-prometheus opentelemetry-exporter-otlp-json-common opentelemetry-exporter-otlp-json-file opentelemetry-exporter-otlp-json-http opentelemetry-exporter-otlp-common opentelemetry-configuration opentelemetry-proto-json opentelemetry-semantic-conventions opentelemetry-test-utils [lintroots] extraroots=examples/*,scripts/ subglob=*.py,tests/,test/,src/*,examples/* [testroots] extraroots=examples/*,tests/ subglob=tests/,test/ python-opentelemetry-1.45.2/exporter/000077500000000000000000000000001527070070700176735ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-http-transport/000077500000000000000000000000001527070070700274445ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-http-transport/LICENSE000066400000000000000000000261351527070070700304600ustar00rootroot00000000000000 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. python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-http-transport/README.rst000066400000000000000000000020671527070070700311400ustar00rootroot00000000000000OpenTelemetry Exporters HTTP Transport ====================================== |pypi| .. |pypi| image:: https://badge.fury.io/py/opentelemetry-exporter-http-transport.svg :target: https://pypi.org/project/opentelemetry-exporter-http-transport/ This package provides shared HTTP transport abstractions used by OpenTelemetry exporters. The package has **no required dependencies**. The ``requests`` and ``urllib3`` transports are available as optional extras. Installation ------------ Core package (no HTTP backend included):: pip install opentelemetry-exporter-http-transport With the ``requests`` backend:: pip install opentelemetry-exporter-http-transport[requests] With the ``urllib3`` backend:: pip install opentelemetry-exporter-http-transport[urllib3] References ---------- * `OpenTelemetry `_ * `OpenTelemetry Protocol Specification `_ * `requests `_ * `urllib3 `_ python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-http-transport/pyproject.toml000066400000000000000000000032011527070070700323540ustar00rootroot00000000000000[build-system] requires = ["hatchling"] build-backend = "hatchling.build" [project] name = "opentelemetry-exporter-http-transport" dynamic = ["version"] description = "OpenTelemetry Exporters HTTP transport" 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", "Framework :: OpenTelemetry", "Framework :: OpenTelemetry :: Exporters", "Intended Audience :: Developers", "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.15", ] [project.optional-dependencies] urllib3 = [ "urllib3 >= 1.26" ] requests = [ "requests ~= 2.25" ] [project.urls] Homepage = "https://github.com/open-telemetry/opentelemetry-python/tree/main/exporter/opentelemetry-exporter-http-transport" Repository = "https://github.com/open-telemetry/opentelemetry-python" [project.entry-points.opentelemetry_http_transport] urllib3 = "opentelemetry.exporter.http.transport._urllib3:Urllib3HTTPTransport" requests = "opentelemetry.exporter.http.transport._requests:RequestsHTTPTransport" [tool.hatch.version] path = "src/opentelemetry/exporter/http/transport/version/__init__.py" [tool.hatch.build.targets.sdist] include = [ "/src", "/tests", ] [tool.hatch.build.targets.wheel] packages = ["src/opentelemetry"] python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-http-transport/src/000077500000000000000000000000001527070070700302335ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-http-transport/src/opentelemetry/000077500000000000000000000000001527070070700331275ustar00rootroot00000000000000exporter/000077500000000000000000000000001527070070700347205ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-http-transport/src/opentelemetryhttp/000077500000000000000000000000001527070070700356775ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-http-transport/src/opentelemetry/exportertransport/000077500000000000000000000000001527070070700377335ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-http-transport/src/opentelemetry/exporter/http__init__.py000066400000000000000000000047251527070070700420540ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-http-transport/src/opentelemetry/exporter/http/transport# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations from typing import TYPE_CHECKING, cast # pylint: disable-next=import-error from opentelemetry.exporter.http.transport._requests import ( RequestsHTTPTransport as _RequestsHTTPTransport, ) # pylint: disable-next=import-error from opentelemetry.exporter.http.transport._urllib3 import ( Urllib3HTTPTransport as _Urllib3HTTPTransport, ) if TYPE_CHECKING: from typing import Any, Protocol from opentelemetry.exporter.http.transport._base import BaseHTTPTransport class BaseHTTPTransportFactory(Protocol): def __call__( self, *, verify: bool | str, cert: str | tuple[str, str] | None, **kwargs: Any, ) -> BaseHTTPTransport: ... _KNOWN_TRANSPORTS: dict[str, BaseHTTPTransportFactory] = { "requests": _RequestsHTTPTransport, "urllib3": _Urllib3HTTPTransport, } def _load_http_transport_factory(name: str) -> BaseHTTPTransportFactory: """Return the transport factory registered under *name*. Checks the built-in transport registry first to avoid the overhead of an entry-point scan for built-in transports. Falls back to standard entry-point discovery for user supplied transports registered under the ``opentelemetry_http_transport`` group. :param name: Entry point name, e.g. ``"requests"`` or ``"urllib3"``. :returns: A callable with signature ``(*, verify, cert, **kwargs) -> BaseHTTPTransport``. :raises ValueError: If no transport is registered under *name*. :raises TypeError: If the loaded entry point is not callable. """ if name in _KNOWN_TRANSPORTS: return _KNOWN_TRANSPORTS[name] # pylint: disable-next=import-outside-toplevel,import-error from opentelemetry.util._importlib_metadata import ( # noqa: PLC0415 entry_points, ) ep = next( iter(entry_points(group="opentelemetry_http_transport", name=name)), None, ) if not ep: raise ValueError( f"No HTTP transport registered under name {name!r}. " "Install the corresponding extra or register an entry point " "under the 'opentelemetry_http_transport' group." ) factory = ep.load() if not callable(factory): raise TypeError(f"Transport {name!r} loaded from entry point is not callable (got {factory!r}).") return cast("BaseHTTPTransportFactory", factory) _base.py000066400000000000000000000054051527070070700413620ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-http-transport/src/opentelemetry/exporter/http/transport# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations import json from abc import ABC, abstractmethod from dataclasses import dataclass from typing import TYPE_CHECKING if TYPE_CHECKING: from collections.abc import Mapping from typing import Any @dataclass(frozen=True, slots=True) class BaseHTTPResult(ABC): """Outcome of a single HTTP request made by a :class:`BaseHTTPTransport`. Either ``status_code`` and ``reason`` are populated (server responded), or ``error`` is set (request failed before a response was received). """ status_code: int | None = None reason: str | None = None error: Exception | None = None @abstractmethod def content(self) -> bytes: """Return the raw response body. Implementations may raise an exception if the returned content is malformed. """ @abstractmethod def headers(self) -> Mapping[str, str]: """Return the response headers. The returned mapping MUST be case-insensitive with respect to header keys. Headers with multiple values are represented as a single string of comma separated values. Implementations may raise an exception if the returned headers are malformed. """ def text(self) -> str: """Return the response body decoded as UTF-8. Implementations may raise an exception if the returned string is malformed. """ return self.content().decode("utf-8") def json(self) -> Any: """Return the response body parsed as JSON. Implementations may raise an exception if no response content is available or the returned JSON is malformed. """ return json.loads(self.text()) class BaseHTTPTransport(ABC): """Abstract HTTP transport interface used by HTTP exporters.""" @abstractmethod def request( self, method: str, url: str, *, headers: dict[str, str] | None = None, timeout: float | None = None, data: bytes | None = None, ) -> BaseHTTPResult: """Send an HTTP request and return the result. :param method: HTTP method (e.g. ``"POST"``). :param url: Target URL. :param headers: Optional HTTP headers to include in the request. :param timeout: Optional request timeout in seconds. :param data: Optional request body. :returns: A :class:`BaseHTTPResult` describing the outcome. """ @abstractmethod def close(self) -> None: """Release any resources held by the transport.""" @abstractmethod def is_connection_error(self, exception: Exception | None) -> bool: """Return ``True`` if the exception is a transport-level connection error.""" _requests.py000066400000000000000000000064001527070070700423170ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-http-transport/src/opentelemetry/exporter/http/transport# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations import functools from dataclasses import dataclass, field from typing import TYPE_CHECKING # pylint: disable-next=import-error from opentelemetry.exporter.http.transport._base import ( BaseHTTPResult, BaseHTTPTransport, ) if TYPE_CHECKING: from collections.abc import Mapping from typing import Any import requests from requests import Response @functools.cache def _get_connection_error_types() -> tuple[type[Exception], ...]: # pylint: disable-next=import-outside-toplevel import requests.exceptions # noqa: PLC0415 return ( requests.exceptions.ConnectionError, requests.exceptions.ConnectTimeout, requests.exceptions.ReadTimeout, requests.exceptions.Timeout, requests.exceptions.SSLError, requests.exceptions.ProxyError, ) @dataclass(frozen=True, slots=True) class RequestsHTTPResult(BaseHTTPResult): response: Response | None = field(default=None, hash=False, compare=False) def content(self) -> bytes: if self.response is None: return b"" return self.response.content or b"" def text(self) -> str: if self.response is None: return "" return self.response.text or "" def json(self) -> Any: if self.response is None: raise ValueError("No response available.") return self.response.json() def headers(self) -> Mapping[str, str]: if self.response is None: return {} return self.response.headers class RequestsHTTPTransport(BaseHTTPTransport): def __init__( self, *, verify: bool | str = True, cert: str | tuple[str, str] | None = None, session: requests.Session | None = None, **kwargs: Any, ) -> None: # pylint: disable-next=import-outside-toplevel import requests # noqa: PLC0415 self._session = session if session is not None else requests.Session() self._session.verify = verify if cert is not None: self._session.cert = cert def request( self, method: str, url: str, *, headers: dict[str, str] | None = None, timeout: float | None = None, data: bytes | None = None, ) -> BaseHTTPResult: try: response = self._session.request( method=method, url=url, headers=headers, data=data, timeout=timeout, allow_redirects=False, ) # pylint: disable-next=broad-exception-caught except Exception as error: # pylint: disable-next=unexpected-keyword-arg return RequestsHTTPResult(error=error) # pylint: disable-next=unexpected-keyword-arg return RequestsHTTPResult( status_code=response.status_code, reason=response.reason, response=response, ) # pylint: disable-next=no-self-use def is_connection_error(self, exception: Exception | None) -> bool: return isinstance(exception, _get_connection_error_types()) def close(self) -> None: self._session.close() _urllib3.py000066400000000000000000000076321527070070700420300ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-http-transport/src/opentelemetry/exporter/http/transport# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations import functools import json from dataclasses import dataclass, field from typing import TYPE_CHECKING # pylint: disable-next=import-error from opentelemetry.exporter.http.transport._base import ( BaseHTTPResult, BaseHTTPTransport, ) if TYPE_CHECKING: from collections.abc import Mapping from typing import Any from urllib3 import BaseHTTPResponse @functools.cache def _get_connection_error_types() -> tuple[type[Exception], ...]: # pylint: disable-next=import-outside-toplevel import urllib3.exceptions # noqa: PLC0415 types: list[type[Exception]] = [ urllib3.exceptions.ConnectionError, urllib3.exceptions.NewConnectionError, urllib3.exceptions.ConnectTimeoutError, urllib3.exceptions.MaxRetryError, urllib3.exceptions.ProtocolError, ] # NameResolutionError was added in urllib3 2.0 name_resolution_error = getattr(urllib3.exceptions, "NameResolutionError", None) if name_resolution_error is not None: types.append(name_resolution_error) return tuple(types) @dataclass(frozen=True, slots=True) class Urllib3HTTPResult(BaseHTTPResult): response: BaseHTTPResponse | None = field(default=None, hash=False, compare=False) def content(self) -> bytes: if self.response is None: return b"" return self.response.data or b"" def headers(self) -> Mapping[str, str]: if self.response is None: return {} return self.response.headers def json(self) -> Any: if self.response is None: raise ValueError("No response available.") return json.loads(self.content()) class Urllib3HTTPTransport(BaseHTTPTransport): def __init__( self, *, verify: bool | str = True, cert: str | tuple[str, str] | None = None, **kwargs: Any, ) -> None: # pylint: disable-next=import-outside-toplevel import urllib3 # noqa: PLC0415 pool_kwargs: dict[str, object] = { "retries": urllib3.Retry(0, redirect=False), } if verify is False: pool_kwargs["cert_reqs"] = "CERT_NONE" else: pool_kwargs["cert_reqs"] = "CERT_REQUIRED" if isinstance(verify, str): pool_kwargs["ca_certs"] = verify if isinstance(cert, tuple): pool_kwargs["cert_file"] = cert[0] pool_kwargs["key_file"] = cert[1] elif isinstance(cert, str): pool_kwargs["cert_file"] = cert self._pool = urllib3.PoolManager(**pool_kwargs) # type: ignore def request( self, method: str, url: str, *, headers: dict[str, str] | None = None, timeout: float | None = None, data: bytes | None = None, ) -> BaseHTTPResult: # pylint: disable-next=import-outside-toplevel import urllib3 # noqa: PLC0415 try: response = self._pool.request( method=method, url=url, headers=headers, body=data, timeout=urllib3.Timeout(total=timeout) if timeout is not None else None, preload_content=True, ) # pylint: disable-next=broad-exception-caught except Exception as error: # pylint: disable-next=unexpected-keyword-arg return Urllib3HTTPResult(error=error) # pylint: disable-next=unexpected-keyword-arg return Urllib3HTTPResult( status_code=response.status, reason=response.reason, response=response, ) # pylint: disable-next=no-self-use def is_connection_error(self, exception: Exception | None) -> bool: return isinstance(exception, _get_connection_error_types()) def close(self) -> None: self._pool.clear() py.typed000066400000000000000000000000001527070070700414200ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-http-transport/src/opentelemetry/exporter/http/transportversion/000077500000000000000000000000001527070070700414205ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-http-transport/src/opentelemetry/exporter/http/transport__init__.py000066400000000000000000000001441527070070700435300ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-http-transport/src/opentelemetry/exporter/http/transport/version# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 __version__ = "0.66b2" python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-http-transport/test-requirements.in000066400000000000000000000002451527070070700334750ustar00rootroot00000000000000iniconfig==2.3.0 mocket==3.14.1 packaging==26.2 pluggy==1.6.0 pytest==9.0.3 -e opentelemetry-api -e exporter/opentelemetry-exporter-http-transport[urllib3,requests] test-requirements.latest.txt000066400000000000000000000036131527070070700351240ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-http-transport# This file was autogenerated by uv via the following command: # uv pip compile --python-version=3.10 --universal --resolution=highest exporter/opentelemetry-exporter-http-transport/test-requirements.in --output-file=exporter/opentelemetry-exporter-http-transport/test-requirements.latest.txt -e exporter/opentelemetry-exporter-http-transport # via -r exporter/opentelemetry-exporter-http-transport/test-requirements.in -e opentelemetry-api # via # -r exporter/opentelemetry-exporter-http-transport/test-requirements.in # opentelemetry-exporter-http-transport certifi==2026.4.22 # via requests charset-normalizer==3.4.7 # via requests colorama==0.4.6 ; sys_platform == 'win32' # via pytest decorator==5.2.1 # via mocket exceptiongroup==1.3.1 ; python_full_version < '3.11' # via pytest h11==0.16.0 # via mocket idna==3.14 # via requests iniconfig==2.3.0 # via # -r exporter/opentelemetry-exporter-http-transport/test-requirements.in # pytest mocket==3.14.1 # via -r exporter/opentelemetry-exporter-http-transport/test-requirements.in packaging==26.2 # via # -r exporter/opentelemetry-exporter-http-transport/test-requirements.in # pytest pluggy==1.6.0 # via # -r exporter/opentelemetry-exporter-http-transport/test-requirements.in # pytest puremagic==1.30 ; python_full_version < '3.12' # via mocket puremagic==2.2.0 ; python_full_version >= '3.12' # via mocket pygments==2.20.0 # via pytest pytest==9.0.3 # via -r exporter/opentelemetry-exporter-http-transport/test-requirements.in requests==2.34.0 # via opentelemetry-exporter-http-transport tomli==2.4.1 ; python_full_version < '3.11' # via pytest typing-extensions==4.15.0 # via # exceptiongroup # mocket # opentelemetry-api urllib3==2.7.0 # via # mocket # opentelemetry-exporter-http-transport # requests test-requirements.oldest.txt000066400000000000000000000036101527070070700351170ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-http-transport# This file was autogenerated by uv via the following command: # uv pip compile --python-version=3.10 --universal --resolution=lowest-direct exporter/opentelemetry-exporter-http-transport/test-requirements.in --output-file=exporter/opentelemetry-exporter-http-transport/test-requirements.oldest.txt -e exporter/opentelemetry-exporter-http-transport # via -r exporter/opentelemetry-exporter-http-transport/test-requirements.in -e opentelemetry-api # via # -r exporter/opentelemetry-exporter-http-transport/test-requirements.in # opentelemetry-exporter-http-transport certifi==2026.4.22 # via requests chardet==3.0.4 # via requests colorama==0.4.6 ; sys_platform == 'win32' # via pytest decorator==5.2.1 # via mocket exceptiongroup==1.3.1 ; python_full_version < '3.11' # via pytest h11==0.16.0 # via mocket idna==2.10 # via requests iniconfig==2.3.0 # via # -r exporter/opentelemetry-exporter-http-transport/test-requirements.in # pytest mocket==3.14.1 # via -r exporter/opentelemetry-exporter-http-transport/test-requirements.in packaging==26.2 # via # -r exporter/opentelemetry-exporter-http-transport/test-requirements.in # pytest pluggy==1.6.0 # via # -r exporter/opentelemetry-exporter-http-transport/test-requirements.in # pytest puremagic==1.30 ; python_full_version < '3.12' # via mocket puremagic==2.2.0 ; python_full_version >= '3.12' # via mocket pygments==2.20.0 # via pytest pytest==9.0.3 # via -r exporter/opentelemetry-exporter-http-transport/test-requirements.in requests==2.25.0 # via opentelemetry-exporter-http-transport tomli==2.4.1 ; python_full_version < '3.11' # via pytest typing-extensions==4.15.0 # via # exceptiongroup # mocket # opentelemetry-api urllib3==1.26.20 # via # mocket # opentelemetry-exporter-http-transport # requests python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-http-transport/tests/000077500000000000000000000000001527070070700306065ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-http-transport/tests/__init__.py000066400000000000000000000001141527070070700327130ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 test_load_transport.py000066400000000000000000000041571527070070700352020ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-http-transport/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # pylint: disable=import-error import unittest from unittest.mock import MagicMock, patch from opentelemetry.exporter.http.transport import _load_http_transport_factory from opentelemetry.exporter.http.transport._requests import ( RequestsHTTPTransport, ) from opentelemetry.exporter.http.transport._urllib3 import ( Urllib3HTTPTransport, ) _ENTRY_POINTS_TARGET = "opentelemetry.util._importlib_metadata.entry_points" # pylint: disable=no-self-use class TestLoadHTTPTransportFactory(unittest.TestCase): def test_returns_requests_transport(self): self.assertIs(_load_http_transport_factory("requests"), RequestsHTTPTransport) def test_returns_urllib3_transport(self): self.assertIs(_load_http_transport_factory("urllib3"), Urllib3HTTPTransport) def test_known_transport_does_not_call_entry_points(self): with patch(_ENTRY_POINTS_TARGET) as mock_ep: _load_http_transport_factory("requests") _load_http_transport_factory("urllib3") self.assertFalse(mock_ep.called) def test_unknown_transport_calls_entry_points(self): def _custom_factory(*, verify, cert, **kwargs): pass mock_ep = MagicMock() mock_ep.load.return_value = _custom_factory with patch(_ENTRY_POINTS_TARGET, return_value=[mock_ep]) as mock_fn: result = _load_http_transport_factory("custom") self.assertEqual( mock_fn.call_args.kwargs, {"group": "opentelemetry_http_transport", "name": "custom"}, ) self.assertIs(result, _custom_factory) def test_entry_point_non_callable_raises_type_error(self): mock_ep = MagicMock() mock_ep.load.return_value = "not_callable" with patch(_ENTRY_POINTS_TARGET, return_value=[mock_ep]): self.assertRaises(TypeError, _load_http_transport_factory, "bad") def test_unknown_transport_raises_value_error(self): with patch(_ENTRY_POINTS_TARGET, return_value=[]): self.assertRaises(ValueError, _load_http_transport_factory, "nonexistent") test_requests_transport.py000066400000000000000000000232651527070070700361370ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-http-transport/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import unittest from json import JSONDecodeError from unittest.mock import MagicMock, patch import requests import requests.exceptions import requests.structures from mocket import Mocket, Mocketizer, mocketize from mocket.mocks.mockhttp import Entry # pylint: disable-next=import-error from opentelemetry.exporter.http.transport._requests import ( RequestsHTTPResult, RequestsHTTPTransport, ) _TEST_URL = "http://example.test/v1/traces" class TestRequestsHTTPResult(unittest.TestCase): @mocketize def test_content_returns_body(self): Entry.single_register(Entry.POST, _TEST_URL, status=200, body="hello") result = RequestsHTTPTransport().request("POST", _TEST_URL) self.assertEqual(result.content(), b"hello") def test_content_returns_empty_bytes_when_no_response(self): result = RequestsHTTPResult(status_code=200, reason="OK") self.assertEqual(result.content(), b"") @mocketize def test_content_returns_empty_bytes_for_empty_body(self): Entry.single_register(Entry.POST, _TEST_URL, status=204) result = RequestsHTTPTransport().request("POST", _TEST_URL) self.assertEqual(result.content(), b"") @mocketize def test_text_uses_response_text(self): Entry.single_register( Entry.POST, _TEST_URL, status=200, body="hello", ) result = RequestsHTTPTransport().request("POST", _TEST_URL) self.assertEqual(result.text(), "hello") def test_text_returns_empty_string_when_no_response(self): result = RequestsHTTPResult(status_code=200, reason="OK") self.assertEqual(result.text(), "") @mocketize def test_text_returns_empty_string_for_empty_body(self): Entry.single_register(Entry.POST, _TEST_URL, status=204) result = RequestsHTTPTransport().request("POST", _TEST_URL) self.assertEqual(result.text(), "") @mocketize def test_json_uses_response_json(self): Entry.single_register( Entry.POST, _TEST_URL, status=200, body='{"key": "val"}', headers={"Content-Type": "application/json"}, ) result = RequestsHTTPTransport().request("POST", _TEST_URL) self.assertEqual(result.json(), {"key": "val"}) def test_json_raises_when_no_response(self): result = RequestsHTTPResult(status_code=200, reason="OK") self.assertRaises(ValueError, result.json) @mocketize def test_json_raises_for_malformed_json(self): Entry.single_register( Entry.POST, _TEST_URL, status=200, body="not json", headers={"Content-Type": "application/json"}, ) result = RequestsHTTPTransport().request("POST", _TEST_URL) self.assertRaises(JSONDecodeError, result.json) @mocketize def test_headers_returns_response_headers(self): Entry.single_register( Entry.POST, _TEST_URL, status=200, headers={"X-Custom": "value"}, ) result = RequestsHTTPTransport().request("POST", _TEST_URL) self.assertEqual(result.headers()["X-Custom"], "value") def test_headers_returns_empty_dict_when_no_response(self): result = RequestsHTTPResult(status_code=200, reason="OK") self.assertEqual(result.headers(), {}) @mocketize def test_headers_are_case_insensitive(self): Entry.single_register( Entry.POST, _TEST_URL, status=200, headers={"X-Custom": "value"}, ) result = RequestsHTTPTransport().request("POST", _TEST_URL) headers = result.headers() self.assertEqual(headers["x-custom"], "value") self.assertEqual(headers["X-CUSTOM"], "value") self.assertEqual(headers["X-Custom"], "value") # pylint: disable=protected-access,no-self-use class TestRequestsHTTPTransport(unittest.TestCase): def test_request_returns_status_code_and_reason(self): cases = [ (200, "OK"), (400, "Bad Request"), (503, "Service Unavailable"), ] for status_code, reason in cases: with self.subTest(status_code=status_code): with Mocketizer(): Entry.single_register(Entry.POST, _TEST_URL, status=status_code) transport = RequestsHTTPTransport() result = transport.request("POST", _TEST_URL) self.assertEqual(result.status_code, status_code) self.assertEqual(result.reason, reason) self.assertIsNone(result.error) @mocketize def test_request_forwards_headers(self): headers = { "content-type": "application/x-protobuf", "x-custom": "value", } Entry.single_register(Entry.POST, _TEST_URL, status=200) transport = RequestsHTTPTransport() result = transport.request("POST", _TEST_URL, headers=headers) self.assertEqual(result.status_code, 200) req = Mocket.last_request() for key, value in headers.items(): self.assertEqual(req.headers[key], value) @mocketize def test_request_forwards_data(self): Entry.single_register(Entry.POST, _TEST_URL, status=200) transport = RequestsHTTPTransport() result = transport.request("POST", _TEST_URL, data=b"payload") self.assertEqual(result.status_code, 200) self.assertEqual(Mocket.last_request().body, "payload") def test_request_passes_timeout(self): cases = [ 3.5, None, ] for timeout in cases: with self.subTest(timeout=timeout): mock_session = MagicMock(spec=requests.Session) mock_session.request.return_value = MagicMock(status_code=200, reason="OK") transport = RequestsHTTPTransport(session=mock_session) transport.request("POST", _TEST_URL, timeout=timeout) timeout_kwarg = mock_session.request.call_args.kwargs["timeout"] self.assertEqual(timeout_kwarg, timeout) def test_request_catches_exception(self): cases = [ (RuntimeError("unexpected"), False), (requests.exceptions.ConnectionError("failed"), True), ] for error, expected_is_connection_error in cases: with self.subTest(error_type=type(error).__name__): with patch("requests.Session.request", side_effect=error): transport = RequestsHTTPTransport() result = transport.request("POST", _TEST_URL) self.assertIsNone(result.status_code) self.assertIsNone(result.reason) self.assertIs(result.error, error) self.assertEqual( transport.is_connection_error(result.error), expected_is_connection_error, ) def test_is_connection_error(self): cases = [ (requests.exceptions.ConnectionError("error"), True), (requests.exceptions.ConnectTimeout("error"), True), (requests.exceptions.ReadTimeout("error"), True), (requests.exceptions.Timeout("error"), True), (requests.exceptions.SSLError("error"), True), (requests.exceptions.ProxyError("error"), True), (requests.exceptions.HTTPError("error"), False), (requests.exceptions.RequestException("error"), False), (RuntimeError("error"), False), (ValueError("error"), False), (None, False), ] transport = RequestsHTTPTransport(session=MagicMock(spec=requests.Session)) for exception, expected in cases: with self.subTest(error_type=type(exception).__name__): self.assertEqual(transport.is_connection_error(exception), expected) def test_verify_sets_session_verify(self): cases = [ (True, True), (False, False), ("/path/to/ca.pem", "/path/to/ca.pem"), ] for verify, expected in cases: with self.subTest(verify=verify): mock_session = MagicMock(spec=requests.Session) RequestsHTTPTransport(verify=verify, session=mock_session) self.assertEqual(mock_session.verify, expected) def test_cert_none_does_not_set_session_cert(self): mock_session = MagicMock(spec=requests.Session) RequestsHTTPTransport(cert=None, session=mock_session) self.assertFalse(hasattr(mock_session, "cert")) def test_cert_sets_session_cert(self): cases = [ "/path/to/cert.pem", ("/path/to/cert.pem", "/path/to/key.pem"), ] for cert in cases: with self.subTest(cert=cert): mock_session = MagicMock(spec=requests.Session) RequestsHTTPTransport(cert=cert, session=mock_session) self.assertEqual(mock_session.cert, cert) def test_custom_session_is_used(self): mock_session = MagicMock(spec=requests.Session) mock_session.request.return_value = MagicMock(status_code=200, reason="OK") transport = RequestsHTTPTransport(session=mock_session) result = transport.request("POST", _TEST_URL) mock_session.request.assert_called_once() self.assertEqual(result.status_code, 200) self.assertEqual(result.reason, "OK") def test_close_closes_session(self): mock_session = MagicMock(spec=requests.Session) transport = RequestsHTTPTransport(session=mock_session) transport.close() mock_session.close.assert_called_once() test_urllib3_transport.py000066400000000000000000000270241527070070700356350ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-http-transport/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import unittest from json import JSONDecodeError from unittest.mock import MagicMock, patch import urllib3 import urllib3.exceptions from mocket import Mocket, Mocketizer, mocketize from mocket.mocks.mockhttp import Entry from urllib3._collections import HTTPHeaderDict # pylint: disable-next=import-error from opentelemetry.exporter.http.transport._urllib3 import ( Urllib3HTTPResult, Urllib3HTTPTransport, ) _TEST_URL = "http://example.test/v1/traces" class TestUrllib3HTTPResult(unittest.TestCase): @mocketize def test_content_returns_body(self): Entry.single_register(Entry.POST, _TEST_URL, status=200, body="hello") result = Urllib3HTTPTransport().request("POST", _TEST_URL) self.assertEqual(result.content(), b"hello") def test_content_returns_empty_bytes_when_no_response(self): result = Urllib3HTTPResult(status_code=200, reason="OK") self.assertEqual(result.content(), b"") @mocketize def test_content_returns_empty_bytes_for_empty_body(self): Entry.single_register(Entry.POST, _TEST_URL, status=204) result = Urllib3HTTPTransport().request("POST", _TEST_URL) self.assertEqual(result.content(), b"") @mocketize def test_text_decodes_utf8(self): Entry.single_register(Entry.POST, _TEST_URL, status=200, body="hello") result = Urllib3HTTPTransport().request("POST", _TEST_URL) self.assertEqual(result.text(), "hello") def test_text_returns_empty_string_when_no_response(self): result = Urllib3HTTPResult(status_code=200, reason="OK") self.assertEqual(result.text(), "") @mocketize def test_text_returns_empty_string_for_empty_body(self): Entry.single_register(Entry.POST, _TEST_URL, status=204) result = Urllib3HTTPTransport().request("POST", _TEST_URL) self.assertEqual(result.text(), "") def test_text_raises_for_non_utf8_content(self): mock_response = MagicMock() mock_response.data = b"\xff\xfe" result = Urllib3HTTPResult(status_code=200, reason="OK", response=mock_response) self.assertRaises(UnicodeDecodeError, result.text) @mocketize def test_json_parses_dict(self): Entry.single_register( Entry.POST, _TEST_URL, status=200, body='{"key": "val"}', headers={"Content-Type": "application/json"}, ) result = Urllib3HTTPTransport().request("POST", _TEST_URL) self.assertEqual(result.json(), {"key": "val"}) def test_json_raises_when_no_response(self): result = Urllib3HTTPResult(status_code=200, reason="OK") self.assertRaises(ValueError, result.json) @mocketize def test_json_raises_for_malformed_json(self): Entry.single_register( Entry.POST, _TEST_URL, status=200, body="not json", headers={"Content-Type": "application/json"}, ) result = Urllib3HTTPTransport().request("POST", _TEST_URL) self.assertRaises(JSONDecodeError, result.json) @mocketize def test_headers_returns_response_headers(self): Entry.single_register( Entry.POST, _TEST_URL, status=200, headers={"X-Custom": "value"}, ) result = Urllib3HTTPTransport().request("POST", _TEST_URL) self.assertEqual(result.headers()["X-Custom"], "value") def test_headers_returns_empty_dict_when_no_response(self): result = Urllib3HTTPResult(status_code=200, reason="OK") self.assertEqual(result.headers(), {}) @mocketize def test_headers_are_case_insensitive(self): Entry.single_register( Entry.POST, _TEST_URL, status=200, headers={"X-Custom": "value"}, ) result = Urllib3HTTPTransport().request("POST", _TEST_URL) headers = result.headers() self.assertEqual(headers["x-custom"], "value") self.assertEqual(headers["X-CUSTOM"], "value") self.assertEqual(headers["X-Custom"], "value") def test_headers_returns_multiple_values_as_comma_separated(self): mock_response = MagicMock() headers = HTTPHeaderDict() headers.add("X-Multi", "value1") headers.add("X-Multi", "value2") mock_response.headers = headers result = Urllib3HTTPResult(status_code=200, reason="OK", response=mock_response) self.assertEqual(result.headers()["X-Multi"], "value1, value2") # pylint: disable=protected-access,no-self-use class TestUrllib3HTTPTransport(unittest.TestCase): def test_request_returns_status_code_and_reason(self): cases = [ (200, "OK"), (400, "Bad Request"), (503, "Service Unavailable"), ] for status_code, reason in cases: with self.subTest(status_code=status_code): with Mocketizer(): Entry.single_register(Entry.POST, _TEST_URL, status=status_code) transport = Urllib3HTTPTransport() result = transport.request("POST", _TEST_URL) self.assertEqual(result.status_code, status_code) self.assertEqual(result.reason, reason) self.assertIsNone(result.error) @mocketize def test_request_returns_response_content(self): Entry.single_register(Entry.POST, _TEST_URL, status=200) transport = Urllib3HTTPTransport() result = transport.request("POST", _TEST_URL) self.assertIsInstance(result.content(), bytes) @mocketize def test_request_forwards_headers(self): headers = { "content-type": "application/x-protobuf", "x-custom": "value", } Entry.single_register(Entry.POST, _TEST_URL, status=200) transport = Urllib3HTTPTransport() result = transport.request("POST", _TEST_URL, headers=headers) self.assertEqual(result.status_code, 200) req = Mocket.last_request() for key, value in headers.items(): self.assertEqual(req.headers[key], value) @mocketize def test_request_forwards_data(self): Entry.single_register(Entry.POST, _TEST_URL, status=200) transport = Urllib3HTTPTransport() result = transport.request("POST", _TEST_URL, data=b"payload") self.assertEqual(result.status_code, 200) self.assertEqual(Mocket.last_request().body, "payload") def test_request_catches_exception(self): cases = [ (RuntimeError("unexpected"), False), (urllib3.exceptions.ProtocolError("failed"), True), ] for error, expected_is_connection_error in cases: with self.subTest(error_type=type(error).__name__): transport = Urllib3HTTPTransport() with patch.object(transport._pool, "request", side_effect=error): result = transport.request("POST", _TEST_URL) self.assertIsNone(result.status_code) self.assertIsNone(result.reason) self.assertIs(result.error, error) self.assertEqual( transport.is_connection_error(result.error), expected_is_connection_error, ) def test_is_connection_error(self): cases: list[tuple[Exception | None, bool]] = [ (urllib3.exceptions.ProtocolError("error"), True), (urllib3.exceptions.NewConnectionError(None, "error"), True), (urllib3.exceptions.ConnectTimeoutError(None, "error"), True), (urllib3.exceptions.MaxRetryError(None, "http://x"), True), (urllib3.exceptions.HTTPError("error"), False), ( urllib3.exceptions.ReadTimeoutError(None, "http://x", "timeout"), False, ), (RuntimeError("error"), False), (ValueError("error"), False), (None, False), ] name_resolution_error = getattr(urllib3.exceptions, "NameResolutionError", None) if name_resolution_error is not None: cases.append((name_resolution_error("host", None, "error"), True)) transport = Urllib3HTTPTransport() for exception, expected in cases: with self.subTest(error_type=type(exception).__name__): self.assertEqual(transport.is_connection_error(exception), expected) def test_request_passes_timeout(self): cases = [ 3.5, None, ] for timeout in cases: with self.subTest(timeout=timeout): transport = Urllib3HTTPTransport() with patch.object(transport._pool, "request") as mock_request: mock_request.return_value = MagicMock(status=200, reason="OK") transport.request("POST", _TEST_URL, timeout=timeout) timeout_kwarg = mock_request.call_args.kwargs["timeout"] if timeout is not None: self.assertIsInstance(timeout_kwarg, urllib3.Timeout) self.assertEqual(timeout_kwarg.total, timeout) else: self.assertIsNone(timeout_kwarg) def test_verify_sets_pool_manager_kwargs(self): cases = [ (True, "CERT_REQUIRED", None), (False, "CERT_NONE", None), ("/path/to/ca.pem", "CERT_REQUIRED", "/path/to/ca.pem"), ] for verify, expected_cert_reqs, expected_ca_certs in cases: with self.subTest(verify=verify): with patch("urllib3.PoolManager") as mock_pm: Urllib3HTTPTransport(verify=verify) kwargs = mock_pm.call_args.kwargs self.assertEqual(kwargs["cert_reqs"], expected_cert_reqs) if expected_ca_certs is not None: self.assertEqual(kwargs["ca_certs"], expected_ca_certs) else: self.assertNotIn("ca_certs", kwargs) def test_cert_none_does_not_set_cert_file(self): with patch("urllib3.PoolManager") as mock_pm: Urllib3HTTPTransport(cert=None) self.assertNotIn("cert_file", mock_pm.call_args.kwargs) def test_cert_sets_pool_manager_kwargs(self): cases = [ ("/path/to/cert.pem", "/path/to/cert.pem", None), ( ("/path/to/cert.pem", "/path/to/key.pem"), "/path/to/cert.pem", "/path/to/key.pem", ), ] for cert, expected_cert_file, expected_key_file in cases: with self.subTest(cert=cert): with patch("urllib3.PoolManager") as mock_pm: Urllib3HTTPTransport(cert=cert) kwargs = mock_pm.call_args.kwargs self.assertEqual(kwargs["cert_file"], expected_cert_file) if expected_key_file is not None: self.assertEqual(kwargs["key_file"], expected_key_file) else: self.assertNotIn("key_file", kwargs) def test_retries_disabled(self): with patch("urllib3.PoolManager") as mock_pm: Urllib3HTTPTransport() retries = mock_pm.call_args.kwargs["retries"] self.assertIsInstance(retries, urllib3.Retry) self.assertEqual(retries.total, 0) self.assertFalse(retries.redirect) def test_close_clears_pool(self): with patch("urllib3.PoolManager") as mock_pm: transport = Urllib3HTTPTransport() transport.close() mock_pm.return_value.clear.assert_called_once() python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-opencensus/000077500000000000000000000000001527070070700266155ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-opencensus/LICENSE000066400000000000000000000261351527070070700276310ustar00rootroot00000000000000 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. python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-opencensus/README.rst000066400000000000000000000011541527070070700303050ustar00rootroot00000000000000OpenCensus Exporter =================== |pypi| .. |pypi| image:: https://badge.fury.io/py/opentelemetry-exporter-opencensus.svg :target: https://pypi.org/project/opentelemetry-exporter-opencensus/ This library allows to export traces using OpenCensus. Installation ------------ :: pip install opentelemetry-exporter-opencensus References ---------- * `OpenCensus Exporter `_ * `OpenTelemetry Collector `_ * `OpenTelemetry `_ python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-opencensus/pyproject.toml000066400000000000000000000033461527070070700315370ustar00rootroot00000000000000[build-system] requires = ["hatchling"] build-backend = "hatchling.build" [project] name = "opentelemetry-exporter-opencensus" dynamic = ["version"] description = "OpenCensus Exporter" 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", "Framework :: OpenTelemetry", "Framework :: OpenTelemetry :: Exporters", "Intended Audience :: Developers", "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 = [ "grpcio >= 1.63.2, < 2.0.0; python_version < '3.13'", "grpcio >= 1.66.2, < 2.0.0; python_version == '3.13'", "grpcio >= 1.75.1, < 2.0.0; python_version >= '3.14'", "opencensus-proto >= 0.1.0, < 1.0.0", "opentelemetry-api >= 1.45.2", "opentelemetry-sdk >= 1.15", "protobuf ~= 3.13", "setuptools >= 16.0", ] [project.entry-points.opentelemetry_traces_exporter] opencensus = "opentelemetry.exporter.opencensus.trace_exporter:OpenCensusSpanExporter" [project.urls] Homepage = "https://github.com/open-telemetry/opentelemetry-python/tree/main/exporter/opentelemetry-exporter-opencensus" Repository = "https://github.com/open-telemetry/opentelemetry-python" [tool.hatch.version] path = "src/opentelemetry/exporter/opencensus/version/__init__.py" [tool.hatch.build.targets.sdist] include = [ "/src", "/tests", ] [tool.hatch.build.targets.wheel] packages = ["src/opentelemetry"] python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-opencensus/src/000077500000000000000000000000001527070070700274045ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/000077500000000000000000000000001527070070700323005ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/000077500000000000000000000000001527070070700341505ustar00rootroot00000000000000opencensus/000077500000000000000000000000001527070070700362535ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter__init__.py000066400000000000000000000002331527070070700403620ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 """ The **OpenCensus Exporter** allows to export traces using OpenCensus. """ py.typed000066400000000000000000000000001527070070700377400ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensustrace_exporter/000077500000000000000000000000001527070070700413015ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus__init__.py000066400000000000000000000133451527070070700434200ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/trace_exporter# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 """OpenCensus Span Exporter.""" import logging from collections.abc import Sequence import grpc from opencensus.proto.agent.trace.v1 import ( trace_service_pb2, trace_service_pb2_grpc, ) from opencensus.proto.trace.v1 import trace_pb2 import opentelemetry.exporter.opencensus.util as utils from opentelemetry import trace from opentelemetry.sdk.resources import SERVICE_NAME, Resource from opentelemetry.sdk.trace import ReadableSpan from opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult DEFAULT_ENDPOINT = "localhost:55678" logger = logging.getLogger(__name__) # pylint: disable=no-member class OpenCensusSpanExporter(SpanExporter): """OpenCensus Collector span exporter. Args: endpoint: OpenCensus Collector receiver endpoint. host_name: Host name. client: TraceService client stub. """ def __init__( self, endpoint=DEFAULT_ENDPOINT, host_name=None, client=None, ): tracer_provider = trace.get_tracer_provider() service_name = ( tracer_provider.resource.attributes[SERVICE_NAME] if getattr(tracer_provider, "resource", None) else Resource.create().attributes.get(SERVICE_NAME) ) self.endpoint = endpoint if client is None: self.channel = grpc.insecure_channel(self.endpoint) self.client = trace_service_pb2_grpc.TraceServiceStub(channel=self.channel) else: self.client = client self.host_name = host_name self.node = utils.get_node(service_name, host_name) def export(self, spans: Sequence[ReadableSpan]) -> SpanExportResult: # Populate service_name from first span # We restrict any SpanProcessor to be only associated with a single # TracerProvider, so it is safe to assume that all Spans in a single # batch all originate from one TracerProvider (and in turn have all # the same service_name) if spans: service_name = spans[0].resource.attributes.get(SERVICE_NAME) if service_name: self.node = utils.get_node(service_name, self.host_name) try: responses = self.client.Export(self.generate_span_requests(spans)) # Read response for _ in responses: pass except grpc.RpcError: return SpanExportResult.FAILURE return SpanExportResult.SUCCESS def shutdown(self) -> None: pass def generate_span_requests(self, spans): collector_spans = translate_to_collector(spans) service_request = trace_service_pb2.ExportTraceServiceRequest(node=self.node, spans=collector_spans) yield service_request def force_flush(self, timeout_millis: int = 30000) -> bool: return True # pylint: disable=too-many-branches def translate_to_collector(spans: Sequence[ReadableSpan]): collector_spans = [] for span in spans: status = None if span.status is not None: status = trace_pb2.Status( code=span.status.status_code.value, message=span.status.description, ) collector_span = trace_pb2.Span( name=trace_pb2.TruncatableString(value=span.name), kind=utils.get_collector_span_kind(span.kind), trace_id=span.context.trace_id.to_bytes(16, "big"), span_id=span.context.span_id.to_bytes(8, "big"), start_time=utils.proto_timestamp_from_time_ns(span.start_time), end_time=utils.proto_timestamp_from_time_ns(span.end_time), status=status, ) parent_id = 0 if span.parent is not None: parent_id = span.parent.span_id collector_span.parent_span_id = parent_id.to_bytes(8, "big") if span.context.trace_state is not None: for key, value in span.context.trace_state.items(): collector_span.tracestate.entries.add(key=key, value=value) if span.attributes: for key, value in span.attributes.items(): utils.add_proto_attribute_value(collector_span.attributes, key, value) if span.events: for event in span.events: collector_annotation = trace_pb2.Span.TimeEvent.Annotation( description=trace_pb2.TruncatableString(value=event.name) ) if event.attributes: for key, value in event.attributes.items(): utils.add_proto_attribute_value(collector_annotation.attributes, key, value) collector_span.time_events.time_event.add( time=utils.proto_timestamp_from_time_ns(event.timestamp), annotation=collector_annotation, ) if span.links: for link in span.links: collector_span_link = collector_span.links.link.add() collector_span_link.trace_id = link.context.trace_id.to_bytes(16, "big") collector_span_link.span_id = link.context.span_id.to_bytes(8, "big") collector_span_link.type = trace_pb2.Span.Link.Type.TYPE_UNSPECIFIED if span.parent is not None: if link.context.span_id == span.parent.span_id and link.context.trace_id == span.parent.trace_id: collector_span_link.type = trace_pb2.Span.Link.Type.PARENT_LINKED_SPAN if link.attributes: for key, value in link.attributes.items(): utils.add_proto_attribute_value(collector_span_link.attributes, key, value) collector_spans.append(collector_span) return collector_spans util.py000066400000000000000000000054731527070070700376130ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from os import getpid from socket import gethostname from time import time # pylint: disable=wrong-import-position from google.protobuf.timestamp_pb2 import ( # pylint: disable=no-name-in-module Timestamp, ) from opencensus.proto.agent.common.v1 import common_pb2 from opencensus.proto.trace.v1 import trace_pb2 from opentelemetry.exporter.opencensus.version import ( __version__ as opencensusexporter_exporter_version, ) from opentelemetry.trace import SpanKind from opentelemetry.util._importlib_metadata import version OPENTELEMETRY_VERSION = version("opentelemetry-api") def proto_timestamp_from_time_ns(time_ns): """Converts datetime to protobuf timestamp. Args: time_ns: Time in nanoseconds Returns: Returns protobuf timestamp. """ ts = Timestamp() if time_ns is not None: # pylint: disable=no-member ts.FromNanoseconds(time_ns) return ts # pylint: disable=no-member def get_collector_span_kind(kind: SpanKind): if kind is SpanKind.SERVER: return trace_pb2.Span.SpanKind.SERVER if kind is SpanKind.CLIENT: return trace_pb2.Span.SpanKind.CLIENT return trace_pb2.Span.SpanKind.SPAN_KIND_UNSPECIFIED def add_proto_attribute_value(pb_attributes, key, value): """Sets string, int, boolean or float value on protobuf span, link or annotation attributes. Args: pb_attributes: protobuf Span's attributes property. key: attribute key to set. value: attribute value """ if isinstance(value, bool): pb_attributes.attribute_map[key].bool_value = value elif isinstance(value, int): pb_attributes.attribute_map[key].int_value = value elif isinstance(value, str): pb_attributes.attribute_map[key].string_value.value = value elif isinstance(value, float): pb_attributes.attribute_map[key].double_value = value else: pb_attributes.attribute_map[key].string_value.value = str(value) # pylint: disable=no-member def get_node(service_name, host_name): """Generates Node message from params and system information. Args: service_name: Name of Collector service. host_name: Host name. """ return common_pb2.Node( identifier=common_pb2.ProcessIdentifier( host_name=gethostname() if host_name is None else host_name, pid=getpid(), start_timestamp=proto_timestamp_from_time_ns(int(time() * 1e9)), ), library_info=common_pb2.LibraryInfo( language=common_pb2.LibraryInfo.Language.Value("PYTHON"), exporter_version=opencensusexporter_exporter_version, core_library_version=OPENTELEMETRY_VERSION, ), service_info=common_pb2.ServiceInfo(name=service_name), ) version/000077500000000000000000000000001527070070700377405ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus__init__.py000066400000000000000000000001441527070070700420500ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 __version__ = "0.66b2" python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-opencensus/test-requirements.txt000066400000000000000000000005531527070070700330610ustar00rootroot00000000000000asgiref==3.7.2 grpcio==1.75.1 iniconfig==2.0.0 opencensus-proto==0.1.0 packaging==24.0 pluggy==1.6.0 protobuf==3.20.3 py-cpuinfo==9.0.0 pytest==9.0.3 tomli==2.0.1 typing_extensions==4.12.0 wrapt==1.16.0 -e opentelemetry-api -e opentelemetry-sdk -e tests/opentelemetry-test-utils -e opentelemetry-semantic-conventions -e exporter/opentelemetry-exporter-opencensus python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-opencensus/tests/000077500000000000000000000000001527070070700277575ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-opencensus/tests/__init__.py000066400000000000000000000000001527070070700320560ustar00rootroot00000000000000test_otcollector_trace_exporter.py000066400000000000000000000273551527070070700367640ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-opencensus/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import unittest from unittest import mock import grpc from google.protobuf.timestamp_pb2 import ( # pylint: disable=no-name-in-module Timestamp, ) from opencensus.proto.trace.v1 import trace_pb2 import opentelemetry.exporter.opencensus.util as utils from opentelemetry import trace as trace_api from opentelemetry.exporter.opencensus.trace_exporter import ( OpenCensusSpanExporter, translate_to_collector, ) from opentelemetry.sdk import trace from opentelemetry.sdk.resources import SERVICE_NAME, Resource from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import SpanExportResult from opentelemetry.test.globals_test import TraceGlobalsTest from opentelemetry.trace import TraceFlags # pylint: disable=no-member class TestCollectorSpanExporter(TraceGlobalsTest, unittest.TestCase): def test_constructor(self): mock_get_node = mock.Mock() patch = mock.patch( "opentelemetry.exporter.opencensus.util.get_node", side_effect=mock_get_node, ) trace_api.set_tracer_provider(TracerProvider(resource=Resource.create({SERVICE_NAME: "testServiceName"}))) host_name = "testHostName" client = grpc.insecure_channel("") endpoint = "testEndpoint" with patch: exporter = OpenCensusSpanExporter( host_name=host_name, endpoint=endpoint, client=client, ) self.assertIs(exporter.client, client) self.assertEqual(exporter.endpoint, endpoint) mock_get_node.assert_called_with("testServiceName", host_name) def test_get_collector_span_kind(self): result = utils.get_collector_span_kind(trace_api.SpanKind.SERVER) self.assertIs(result, trace_pb2.Span.SpanKind.SERVER) result = utils.get_collector_span_kind(trace_api.SpanKind.CLIENT) self.assertIs(result, trace_pb2.Span.SpanKind.CLIENT) result = utils.get_collector_span_kind(trace_api.SpanKind.CONSUMER) self.assertIs(result, trace_pb2.Span.SpanKind.SPAN_KIND_UNSPECIFIED) result = utils.get_collector_span_kind(trace_api.SpanKind.PRODUCER) self.assertIs(result, trace_pb2.Span.SpanKind.SPAN_KIND_UNSPECIFIED) result = utils.get_collector_span_kind(trace_api.SpanKind.INTERNAL) self.assertIs(result, trace_pb2.Span.SpanKind.SPAN_KIND_UNSPECIFIED) def test_proto_timestamp_from_time_ns(self): result = utils.proto_timestamp_from_time_ns(12345) self.assertIsInstance(result, Timestamp) self.assertEqual(result.nanos, 12345) # pylint: disable=too-many-locals # pylint: disable=too-many-statements def test_translate_to_collector(self): trace_id = 0x6E0C63257DE34C926F9EFCD03927272E span_id = 0x34BF92DEEFC58C92 parent_id = 0x1111111111111111 base_time = 683647322 * 10**9 # in ns start_times = ( base_time, base_time + 150 * 10**6, base_time + 300 * 10**6, ) durations = (50 * 10**6, 100 * 10**6, 200 * 10**6) end_times = ( start_times[0] + durations[0], start_times[1] + durations[1], start_times[2] + durations[2], ) span_context = trace_api.SpanContext( trace_id, span_id, is_remote=False, trace_flags=TraceFlags(TraceFlags.SAMPLED), trace_state=trace_api.TraceState([("testkey", "testvalue")]), ) parent_span_context = trace_api.SpanContext(trace_id, parent_id, is_remote=False) other_context = trace_api.SpanContext(trace_id, span_id, is_remote=False) event_attributes = { "annotation_bool": True, "annotation_string": "annotation_test", "key_float": 0.3, } event_timestamp = base_time + 50 * 10**6 event = trace.Event( name="event0", timestamp=event_timestamp, attributes=event_attributes, ) link_attributes = {"key_bool": True} link_1 = trace_api.Link(context=other_context, attributes=link_attributes) link_2 = trace_api.Link(context=parent_span_context, attributes=link_attributes) span_1 = trace._Span( name="test1", context=span_context, parent=parent_span_context, events=(event,), links=(link_1,), kind=trace_api.SpanKind.CLIENT, ) span_2 = trace._Span( name="test2", context=parent_span_context, parent=None, kind=trace_api.SpanKind.SERVER, ) span_3 = trace._Span( name="test3", context=other_context, links=(link_2,), parent=span_2.get_span_context(), ) otel_spans = [span_1, span_2, span_3] otel_spans[0].start(start_time=start_times[0]) otel_spans[0].set_attribute("key_bool", False) otel_spans[0].set_attribute("key_string", "hello_world") otel_spans[0].set_attribute("key_float", 111.22) otel_spans[0].set_attribute("key_int", 333) otel_spans[0].set_status(trace_api.Status(trace_api.StatusCode.OK)) otel_spans[0].end(end_time=end_times[0]) otel_spans[1].start(start_time=start_times[1]) otel_spans[1].set_status( trace_api.Status( trace_api.StatusCode.ERROR, {"test", "val"}, ) ) otel_spans[1].end(end_time=end_times[1]) otel_spans[2].start(start_time=start_times[2]) otel_spans[2].end(end_time=end_times[2]) output_spans = translate_to_collector(otel_spans) self.assertEqual(len(output_spans), 3) self.assertEqual(output_spans[0].trace_id, b"n\x0cc%}\xe3L\x92o\x9e\xfc\xd09''.") self.assertEqual(output_spans[0].span_id, b"4\xbf\x92\xde\xef\xc5\x8c\x92") self.assertEqual(output_spans[0].name, trace_pb2.TruncatableString(value="test1")) self.assertEqual(output_spans[1].name, trace_pb2.TruncatableString(value="test2")) self.assertEqual(output_spans[2].name, trace_pb2.TruncatableString(value="test3")) self.assertEqual( output_spans[0].start_time.seconds, int(start_times[0] / 1000000000), ) self.assertEqual(output_spans[0].end_time.seconds, int(end_times[0] / 1000000000)) self.assertEqual(output_spans[0].kind, trace_api.SpanKind.CLIENT.value) self.assertEqual(output_spans[1].kind, trace_api.SpanKind.SERVER.value) self.assertEqual(output_spans[0].parent_span_id, b"\x11\x11\x11\x11\x11\x11\x11\x11") self.assertEqual(output_spans[2].parent_span_id, b"\x11\x11\x11\x11\x11\x11\x11\x11") self.assertEqual( output_spans[0].status.code, trace_api.StatusCode.OK.value, ) self.assertEqual(len(output_spans[0].tracestate.entries), 1) self.assertEqual(output_spans[0].tracestate.entries[0].key, "testkey") self.assertEqual(output_spans[0].tracestate.entries[0].value, "testvalue") self.assertEqual( output_spans[0].attributes.attribute_map["key_bool"].bool_value, False, ) self.assertEqual( output_spans[0].attributes.attribute_map["key_string"].string_value.value, "hello_world", ) self.assertEqual( output_spans[0].attributes.attribute_map["key_float"].double_value, 111.22, ) self.assertEqual(output_spans[0].attributes.attribute_map["key_int"].int_value, 333) self.assertEqual(output_spans[0].time_events.time_event[0].time.seconds, 683647322) self.assertEqual( output_spans[0].time_events.time_event[0].annotation.description.value, "event0", ) self.assertEqual( output_spans[0].time_events.time_event[0].annotation.attributes.attribute_map["annotation_bool"].bool_value, True, ) self.assertEqual( output_spans[0] .time_events.time_event[0] .annotation.attributes.attribute_map["annotation_string"] .string_value.value, "annotation_test", ) self.assertEqual( output_spans[0].time_events.time_event[0].annotation.attributes.attribute_map["key_float"].double_value, 0.3, ) self.assertEqual( output_spans[0].links.link[0].trace_id, b"n\x0cc%}\xe3L\x92o\x9e\xfc\xd09''.", ) self.assertEqual( output_spans[0].links.link[0].span_id, b"4\xbf\x92\xde\xef\xc5\x8c\x92", ) self.assertEqual( output_spans[0].links.link[0].type, trace_pb2.Span.Link.Type.TYPE_UNSPECIFIED, ) self.assertEqual( output_spans[1].status.code, trace_api.StatusCode.ERROR.value, ) self.assertEqual( output_spans[2].links.link[0].type, trace_pb2.Span.Link.Type.PARENT_LINKED_SPAN, ) self.assertEqual( output_spans[0].links.link[0].attributes.attribute_map["key_bool"].bool_value, True, ) def test_export(self): mock_client = mock.MagicMock() mock_export = mock.MagicMock() mock_client.Export = mock_export host_name = "testHostName" collector_exporter = OpenCensusSpanExporter(client=mock_client, host_name=host_name) trace_id = 0x6E0C63257DE34C926F9EFCD03927272E span_id = 0x34BF92DEEFC58C92 span_context = trace_api.SpanContext( trace_id, span_id, is_remote=False, trace_flags=TraceFlags(TraceFlags.SAMPLED), ) otel_spans = [ trace._Span( name="test1", context=span_context, kind=trace_api.SpanKind.CLIENT, ) ] result_status = collector_exporter.export(otel_spans) self.assertEqual(SpanExportResult.SUCCESS, result_status) # pylint: disable=unsubscriptable-object export_arg = mock_export.call_args[0] service_request = next(export_arg[0]) output_spans = service_request.spans output_node = service_request.node self.assertEqual(len(output_spans), 1) self.assertIsNotNone(output_node.library_info) self.assertIsNotNone(output_node.service_info) output_identifier = output_node.identifier self.assertEqual(output_identifier.host_name, "testHostName") def test_export_service_name(self): trace_api.set_tracer_provider(TracerProvider(resource=Resource.create({SERVICE_NAME: "testServiceName"}))) mock_client = mock.MagicMock() mock_export = mock.MagicMock() mock_client.Export = mock_export host_name = "testHostName" collector_exporter = OpenCensusSpanExporter(client=mock_client, host_name=host_name) self.assertEqual(collector_exporter.node.service_info.name, "testServiceName") trace_id = 0x6E0C63257DE34C926F9EFCD03927272E span_id = 0x34BF92DEEFC58C92 span_context = trace_api.SpanContext( trace_id, span_id, is_remote=False, trace_flags=TraceFlags(TraceFlags.SAMPLED), ) resource = Resource.create({SERVICE_NAME: "test"}) otel_spans = [ trace._Span( name="test1", context=span_context, kind=trace_api.SpanKind.CLIENT, resource=resource, ) ] result_status = collector_exporter.export(otel_spans) self.assertEqual(SpanExportResult.SUCCESS, result_status) self.assertEqual(collector_exporter.node.service_info.name, "test") python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-common/000077500000000000000000000000001527070070700266775ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-common/LICENSE000066400000000000000000000261351527070070700277130ustar00rootroot00000000000000 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. python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-common/README.rst000066400000000000000000000013461527070070700303720ustar00rootroot00000000000000OpenTelemetry OTLP Utilities =============================== |pypi| .. |pypi| image:: https://badge.fury.io/py/opentelemetry-exporter-otlp-common.svg :target: https://pypi.org/project/opentelemetry-exporter-otlp-common/ OpenTelemetry OTLP export utilities. This package is intended to be used by OpenTelemetry signal exporters (traces, metrics, logs) that send telemetry with OTLP. Currently, all functionality in this package is marked as internal and is not intended for use directly by application developers. Installation ------------ :: pip install opentelemetry-exporter-otlp-common References ---------- * `OpenTelemetry `_ * `OTLP Specification `_ python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-common/pyproject.toml000066400000000000000000000026361527070070700316220ustar00rootroot00000000000000[build-system] requires = ["hatchling"] build-backend = "hatchling.build" [project] name = "opentelemetry-exporter-otlp-common" dynamic = ["version"] description = "OpenTelemetry OTLP HTTP export utilities" 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", "Framework :: OpenTelemetry", "Framework :: OpenTelemetry :: Exporters", "Intended Audience :: Developers", "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.45.2", ] [project.optional-dependencies] http = [ "opentelemetry-exporter-http-transport == 0.66b2", ] [project.urls] Homepage = "https://github.com/open-telemetry/opentelemetry-python/tree/main/exporter/opentelemetry-exporter-otlp-common" Repository = "https://github.com/open-telemetry/opentelemetry-python" [tool.hatch.version] path = "src/opentelemetry/exporter/otlp/common/version/__init__.py" [tool.hatch.build.targets.sdist] include = [ "/src", "/tests", ] [tool.hatch.build.targets.wheel] packages = ["src/opentelemetry"] python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-common/src/000077500000000000000000000000001527070070700274665ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-common/src/opentelemetry/000077500000000000000000000000001527070070700323625ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-common/src/opentelemetry/exporter/000077500000000000000000000000001527070070700342325ustar00rootroot00000000000000otlp/000077500000000000000000000000001527070070700351315ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-common/src/opentelemetry/exportercommon/000077500000000000000000000000001527070070700364215ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-common/src/opentelemetry/exporter/otlp__init__.py000066400000000000000000000001141527070070700405260ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-common/src/opentelemetry/exporter/otlp/common# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 _aggregation.py000066400000000000000000000100651527070070700414230ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-common/src/opentelemetry/exporter/otlp/common# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations import logging import os from opentelemetry.sdk.environment_variables import ( OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION, OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE, ) from opentelemetry.sdk.metrics import ( Counter, Histogram, ObservableCounter, ObservableGauge, ObservableUpDownCounter, UpDownCounter, ) from opentelemetry.sdk.metrics.export import AggregationTemporality from opentelemetry.sdk.metrics.view import ( Aggregation, ExplicitBucketHistogramAggregation, ExponentialBucketHistogramAggregation, ) _logger = logging.getLogger(__name__) def _get_temporality( preferred_temporality: dict[type, AggregationTemporality] | None, ) -> dict[type, AggregationTemporality]: temporality_preference = ( os.environ.get( OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE, "CUMULATIVE", ) .upper() .strip() ) instrument_class_temporality: dict[type, AggregationTemporality] match temporality_preference: case "DELTA": instrument_class_temporality = { Counter: AggregationTemporality.DELTA, UpDownCounter: AggregationTemporality.CUMULATIVE, Histogram: AggregationTemporality.DELTA, ObservableCounter: AggregationTemporality.DELTA, ObservableUpDownCounter: AggregationTemporality.CUMULATIVE, ObservableGauge: AggregationTemporality.CUMULATIVE, } case "LOWMEMORY": instrument_class_temporality = { Counter: AggregationTemporality.DELTA, UpDownCounter: AggregationTemporality.CUMULATIVE, Histogram: AggregationTemporality.DELTA, ObservableCounter: AggregationTemporality.CUMULATIVE, ObservableUpDownCounter: AggregationTemporality.CUMULATIVE, ObservableGauge: AggregationTemporality.CUMULATIVE, } case _: if temporality_preference != "CUMULATIVE": _logger.warning( ("Invalid value for %s: %s, using cumulative temporality aggregation"), OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE, temporality_preference, ) instrument_class_temporality = { Counter: AggregationTemporality.CUMULATIVE, UpDownCounter: AggregationTemporality.CUMULATIVE, Histogram: AggregationTemporality.CUMULATIVE, ObservableCounter: AggregationTemporality.CUMULATIVE, ObservableUpDownCounter: AggregationTemporality.CUMULATIVE, ObservableGauge: AggregationTemporality.CUMULATIVE, } return instrument_class_temporality | (preferred_temporality or {}) def _get_aggregation( preferred_aggregation: dict[type, Aggregation] | None, ) -> dict[type, Aggregation]: default_histogram_aggregation = ( os.environ.get( OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION, "explicit_bucket_histogram", ) .upper() .strip() ) instrument_class_aggregation: dict[type, Aggregation] match default_histogram_aggregation: case "BASE2_EXPONENTIAL_BUCKET_HISTOGRAM": instrument_class_aggregation = { Histogram: ExponentialBucketHistogramAggregation(), } case _: if default_histogram_aggregation != "EXPLICIT_BUCKET_HISTOGRAM": _logger.warning( ("Invalid value for %s: %s, using explicit bucket histogram aggregation"), OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION, default_histogram_aggregation, ) instrument_class_aggregation = { Histogram: ExplicitBucketHistogramAggregation(), } return instrument_class_aggregation | (preferred_aggregation or {}) _exporter_metrics.py000066400000000000000000000123071527070070700425330ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-common/src/opentelemetry/exporter/otlp/common# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations from collections import Counter from collections.abc import Iterator from contextlib import AbstractContextManager, contextmanager from dataclasses import dataclass from time import perf_counter from typing import TYPE_CHECKING, Protocol from urllib.parse import urlparse from opentelemetry.metrics import MeterProvider, get_meter_provider from opentelemetry.semconv._incubating.attributes.otel_attributes import ( OTEL_COMPONENT_NAME, OTEL_COMPONENT_TYPE, OtelComponentTypeValues, ) from opentelemetry.semconv._incubating.metrics.otel_metrics import ( create_otel_sdk_exporter_log_exported, create_otel_sdk_exporter_log_inflight, create_otel_sdk_exporter_metric_data_point_exported, create_otel_sdk_exporter_metric_data_point_inflight, create_otel_sdk_exporter_operation_duration, create_otel_sdk_exporter_span_exported, create_otel_sdk_exporter_span_inflight, ) from opentelemetry.semconv.attributes.error_attributes import ERROR_TYPE from opentelemetry.semconv.attributes.server_attributes import ( SERVER_ADDRESS, SERVER_PORT, ) if TYPE_CHECKING: from typing import Literal from opentelemetry.util.types import AnyValue, Attributes _component_counter = Counter() _GRPC_COMPONENT_TYPES = frozenset( { OtelComponentTypeValues.OTLP_GRPC_SPAN_EXPORTER, OtelComponentTypeValues.OTLP_GRPC_LOG_EXPORTER, OtelComponentTypeValues.OTLP_GRPC_METRIC_EXPORTER, } ) @dataclass class ExportResult: error: Exception | None = None error_attrs: Attributes = None class ExporterMetricsT(Protocol): def export_operation(self, num_items: int) -> AbstractContextManager[ExportResult]: ... class NoOpExporterMetrics: @contextmanager # pylint: disable-next=no-self-use def export_operation(self, num_items: int) -> Iterator[ExportResult]: yield ExportResult() class ExporterMetrics: def __init__( self, component_type: OtelComponentTypeValues | None, signal: Literal["traces", "metrics", "logs"], endpoint: str, meter_provider: MeterProvider | None, ) -> None: if signal == "traces": create_exported = create_otel_sdk_exporter_span_exported create_inflight = create_otel_sdk_exporter_span_inflight elif signal == "logs": create_exported = create_otel_sdk_exporter_log_exported create_inflight = create_otel_sdk_exporter_log_inflight else: create_exported = create_otel_sdk_exporter_metric_data_point_exported create_inflight = create_otel_sdk_exporter_metric_data_point_inflight if not endpoint.startswith("//") and "://" not in endpoint: endpoint = f"//{endpoint}" parsed_endpoint = urlparse(endpoint) port = parsed_endpoint.port if port is None: if component_type in _GRPC_COMPONENT_TYPES or parsed_endpoint.scheme == "https": port = 443 elif parsed_endpoint.scheme == "http": port = 80 component_type_value = component_type.value if component_type else "unknown_otlp_exporter" count = _component_counter[component_type_value] _component_counter[component_type_value] = count + 1 self._standard_attrs: dict[str, AnyValue] = { OTEL_COMPONENT_TYPE: component_type_value, OTEL_COMPONENT_NAME: f"{component_type_value}/{count}", } if parsed_endpoint.hostname: self._standard_attrs[SERVER_ADDRESS] = parsed_endpoint.hostname if port is not None: self._standard_attrs[SERVER_PORT] = port meter_provider = meter_provider or get_meter_provider() meter = meter_provider.get_meter("opentelemetry-sdk") self._inflight = create_inflight(meter) self._exported = create_exported(meter) self._duration = create_otel_sdk_exporter_operation_duration(meter) @contextmanager def export_operation(self, num_items: int) -> Iterator[ExportResult]: start_time = perf_counter() self._inflight.add(num_items, self._standard_attrs) result = ExportResult() try: yield result finally: error = result.error error_attrs = result.error_attrs end_time = perf_counter() self._inflight.add(-num_items, self._standard_attrs) exported_attrs = ( {**self._standard_attrs, ERROR_TYPE: type(error).__qualname__} if error else self._standard_attrs ) self._exported.add(num_items, exported_attrs) duration_attrs = {**exported_attrs, **error_attrs} if error_attrs else exported_attrs self._duration.record(end_time - start_time, duration_attrs) def create_exporter_metrics( component_type: OtelComponentTypeValues | None, signal: Literal["traces", "metrics", "logs"], endpoint: str, meter_provider: MeterProvider | None, enabled: bool, ) -> ExporterMetricsT: if not enabled: return NoOpExporterMetrics() return ExporterMetrics( component_type, signal, endpoint, meter_provider, ) http.py000066400000000000000000000204351527070070700377560ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-common/src/opentelemetry/exporter/otlp/common# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations import enum import gzip import logging import math import random import threading import time import zlib from collections.abc import Mapping from dataclasses import dataclass from datetime import timezone from email.utils import parsedate_to_datetime from http import HTTPStatus from io import BytesIO from typing import TYPE_CHECKING, Final, Literal if TYPE_CHECKING: from opentelemetry.exporter.http.transport._base import ( BaseHTTPResult, BaseHTTPTransport, ) _logger = logging.getLogger(__name__) _MAX_RETRIES: Final[int] = 6 _DEFAULT_TIMEOUT: Final[float] = 10.0 _DEFAULT_JITTER: Final[float] = 0.2 _RETRYABLE_STATUS_CODES: Final[frozenset[int]] = frozenset( { HTTPStatus.TOO_MANY_REQUESTS.value, HTTPStatus.BAD_GATEWAY.value, HTTPStatus.SERVICE_UNAVAILABLE.value, HTTPStatus.GATEWAY_TIMEOUT.value, } ) def _is_retryable(status_code: int | None) -> bool: return status_code in _RETRYABLE_STATUS_CODES def _extract_retry_after(result: BaseHTTPResult) -> float | None: """Parse the ``Retry-After`` header (RFC 7231).""" try: value = result.headers().get("retry-after") # pylint: disable-next=broad-exception-caught except Exception: return None if value is None: return None value = value.strip() # delay-seconds: a non-negative decimal integer. try: seconds = float(value) except ValueError: pass else: return max(seconds, 0.0) if math.isfinite(seconds) else None # HTTP-date: wait until the indicated absolute time. try: retry_at = parsedate_to_datetime(value) except (TypeError, ValueError): return None if retry_at.tzinfo is None: retry_at = retry_at.replace(tzinfo=timezone.utc) return max(retry_at.timestamp() - time.time(), 0.0) class Compression(enum.Enum): NONE = "none" DEFLATE = "deflate" GZIP = "gzip" @staticmethod def from_str(value: str) -> Compression: match value.strip().lower(): case "none": return Compression.NONE case "deflate": return Compression.DEFLATE case "gzip": return Compression.GZIP case _: raise ValueError(f"Invalid compression type: {value!r}. Expected one of: 'none', 'deflate', 'gzip'.") @dataclass(slots=True, frozen=True) class _ExportResult: """Outcome of an OTLP export attempt, including retry exhaustion.""" success: bool status_code: int | None reason: str | None error: Exception | None class _OTLPHTTPClient: """Sends serialized OTLP payloads over HTTP with retry logic. Compression, backoff, and connection-error recovery are handled internally. Callers interact through the :meth:`export` and :meth:`close` methods. """ def __init__( self, transport: BaseHTTPTransport, endpoint: str, kind: Literal["spans", "logs", "metrics"], timeout: float = _DEFAULT_TIMEOUT, compression: Compression = Compression.NONE, headers: Mapping[str, str] | None = None, jitter: float = _DEFAULT_JITTER, logger: logging.Logger | None = None, ) -> None: self._transport = transport self._endpoint = endpoint self._timeout = timeout self._compression = compression self._headers = dict(headers) if headers is not None else {} if self._compression is not Compression.NONE and not any( key.lower() == "content-encoding" for key in self._headers ): self._headers["Content-Encoding"] = self._compression.value self._kind = kind self._jitter = min(max(jitter, 0.0), 1.0) self._logger = logger if logger is not None else _logger self._shutdown = False self._shutdown_event = threading.Event() def _compute_backoff(self, retry: int) -> float: return 2**retry * random.uniform(1 - self._jitter, 1 + self._jitter) def _compress(self, serialized_data: bytes) -> bytes: if self._compression is Compression.GZIP: buf = BytesIO() with gzip.GzipFile(fileobj=buf, mode="w") as gz: gz.write(serialized_data) return buf.getvalue() if self._compression is Compression.DEFLATE: return zlib.compress(serialized_data) return serialized_data def _submit(self, data: bytes, timeout: float) -> BaseHTTPResult: deadline = time.time() + timeout result = self._transport.request( "POST", self._endpoint, headers=self._headers, data=data, timeout=timeout, ) if ( result.error is not None and self._transport.is_connection_error(result.error) and (remaining := deadline - time.time()) > 0 ): # Immediately retry connection errors once without backoff. These # usually indicate a stale pooled connection that the transport will # reestablish on the next attempt. result = self._transport.request( "POST", self._endpoint, headers=self._headers, data=data, timeout=remaining, ) return result def export(self, data: bytes) -> _ExportResult: """Export a serialized payload, retrying on transient failures. :param data: Serialized bytes to send. :returns: An :class:`ExportResult` indicating success or the reason for failure. """ data = self._compress(data) deadline = time.time() + self._timeout for retry in range(_MAX_RETRIES): backoff = self._compute_backoff(retry) status_code: int | None = None reason: str | None = None export_error: Exception | None retryable: bool try: result = self._submit(data, max(deadline - time.time(), 0.0)) # pylint: disable-next=broad-exception-caught except Exception as error: export_error = error retryable = False else: status_code = result.status_code reason = result.reason if status_code is not None and 200 <= status_code < 400: return _ExportResult(True, status_code, reason, None) export_error = result.error retryable = ( _is_retryable(status_code) if status_code else self._transport.is_connection_error(result.error) ) if retryable and status_code is not None and (retry_after := _extract_retry_after(result)) is not None: backoff = retry_after if not retryable: self._logger.error( "Failed to export %s batch code: %s, reason: %s", self._kind, status_code, reason or export_error or "unknown", ) return _ExportResult(False, status_code, reason, export_error) if retry + 1 == _MAX_RETRIES or backoff > (deadline - time.time()) or self._shutdown_event.is_set(): self._logger.error( "Failed to export %s batch due to timeout, max retries or shutdown.", self._kind, ) return _ExportResult(False, status_code, reason, export_error) self._logger.warning( "Transient error %s encountered while exporting %s batch, retrying in %.2fs.", reason or export_error, self._kind, backoff, ) shutdown = self._shutdown_event.wait(backoff) if shutdown: self._logger.warning("Shutdown in progress, aborting retry.") break return _ExportResult(False, None, None, None) def shutdown(self) -> None: """Shutdown the client.""" if self._shutdown: self._logger.warning("OTLP client already shutdown, ignoring call") return self._shutdown = True self._shutdown_event.set() self._transport.close() py.typed000066400000000000000000000000001527070070700401060ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-common/src/opentelemetry/exporter/otlp/commonversion/000077500000000000000000000000001527070070700401065ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-common/src/opentelemetry/exporter/otlp/common__init__.py000066400000000000000000000001441527070070700422160ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-common/src/opentelemetry/exporter/otlp/common/version# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 __version__ = "0.66b2" python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-common/test-requirements.in000066400000000000000000000004171527070070700327310ustar00rootroot00000000000000iniconfig==2.3.0 mocket==3.14.1 packaging==26.2 pluggy==1.6.0 pytest==9.0.3 -e opentelemetry-api -e opentelemetry-sdk -e opentelemetry-semantic-conventions -e exporter/opentelemetry-exporter-http-transport[urllib3,requests] -e exporter/opentelemetry-exporter-otlp-common python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-common/test-requirements.latest.txt000066400000000000000000000046431527070070700344420ustar00rootroot00000000000000# This file was autogenerated by uv via the following command: # uv pip compile --python-version=3.10 --universal --resolution=highest exporter/opentelemetry-exporter-otlp-common/test-requirements.in --output-file=exporter/opentelemetry-exporter-otlp-common/test-requirements.latest.txt -e exporter/opentelemetry-exporter-http-transport # via -r exporter/opentelemetry-exporter-otlp-common/test-requirements.in -e exporter/opentelemetry-exporter-otlp-common # via -r exporter/opentelemetry-exporter-otlp-common/test-requirements.in -e opentelemetry-api # via # -r exporter/opentelemetry-exporter-otlp-common/test-requirements.in # opentelemetry-exporter-http-transport # opentelemetry-sdk # opentelemetry-semantic-conventions -e opentelemetry-sdk # via # -r exporter/opentelemetry-exporter-otlp-common/test-requirements.in # opentelemetry-exporter-otlp-common -e opentelemetry-semantic-conventions # via # -r exporter/opentelemetry-exporter-otlp-common/test-requirements.in # opentelemetry-sdk certifi==2026.5.20 # via requests charset-normalizer==3.4.7 # via requests colorama==0.4.6 ; sys_platform == 'win32' # via pytest decorator==5.3.1 # via mocket exceptiongroup==1.3.1 ; python_full_version < '3.11' # via pytest h11==0.16.0 # via mocket idna==3.16 # via requests iniconfig==2.3.0 # via # -r exporter/opentelemetry-exporter-otlp-common/test-requirements.in # pytest mocket==3.14.1 # via -r exporter/opentelemetry-exporter-otlp-common/test-requirements.in packaging==26.2 # via # -r exporter/opentelemetry-exporter-otlp-common/test-requirements.in # pytest pluggy==1.6.0 # via # -r exporter/opentelemetry-exporter-otlp-common/test-requirements.in # pytest puremagic==1.30 ; python_full_version < '3.12' # via mocket puremagic==2.2.0 ; python_full_version >= '3.12' # via mocket pygments==2.20.0 # via pytest pytest==9.0.3 # via -r exporter/opentelemetry-exporter-otlp-common/test-requirements.in requests==2.33.0 # via opentelemetry-exporter-http-transport tomli==2.4.1 ; python_full_version < '3.11' # via pytest typing-extensions==4.15.0 # via # exceptiongroup # mocket # opentelemetry-api # opentelemetry-sdk # opentelemetry-semantic-conventions urllib3==2.7.0 # via # mocket # opentelemetry-exporter-http-transport # requests python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-common/test-requirements.oldest.txt000066400000000000000000000046371527070070700344430ustar00rootroot00000000000000# This file was autogenerated by uv via the following command: # uv pip compile --python-version=3.10 --universal --resolution=lowest-direct exporter/opentelemetry-exporter-otlp-common/test-requirements.in --output-file=exporter/opentelemetry-exporter-otlp-common/test-requirements.oldest.txt -e exporter/opentelemetry-exporter-http-transport # via -r exporter/opentelemetry-exporter-otlp-common/test-requirements.in -e exporter/opentelemetry-exporter-otlp-common # via -r exporter/opentelemetry-exporter-otlp-common/test-requirements.in -e opentelemetry-api # via # -r exporter/opentelemetry-exporter-otlp-common/test-requirements.in # opentelemetry-exporter-http-transport # opentelemetry-sdk # opentelemetry-semantic-conventions -e opentelemetry-sdk # via # -r exporter/opentelemetry-exporter-otlp-common/test-requirements.in # opentelemetry-exporter-otlp-common -e opentelemetry-semantic-conventions # via # -r exporter/opentelemetry-exporter-otlp-common/test-requirements.in # opentelemetry-sdk certifi==2026.5.20 # via requests chardet==3.0.4 # via requests colorama==0.4.6 ; sys_platform == 'win32' # via pytest decorator==5.3.1 # via mocket exceptiongroup==1.3.1 ; python_full_version < '3.11' # via pytest h11==0.16.0 # via mocket idna==2.10 # via requests iniconfig==2.3.0 # via # -r exporter/opentelemetry-exporter-otlp-common/test-requirements.in # pytest mocket==3.14.1 # via -r exporter/opentelemetry-exporter-otlp-common/test-requirements.in packaging==26.2 # via # -r exporter/opentelemetry-exporter-otlp-common/test-requirements.in # pytest pluggy==1.6.0 # via # -r exporter/opentelemetry-exporter-otlp-common/test-requirements.in # pytest puremagic==1.30 ; python_full_version < '3.12' # via mocket puremagic==2.2.0 ; python_full_version >= '3.12' # via mocket pygments==2.20.0 # via pytest pytest==9.0.3 # via -r exporter/opentelemetry-exporter-otlp-common/test-requirements.in requests==2.25.0 # via opentelemetry-exporter-http-transport tomli==2.4.1 ; python_full_version < '3.11' # via pytest typing-extensions==4.15.0 # via # exceptiongroup # mocket # opentelemetry-api # opentelemetry-sdk # opentelemetry-semantic-conventions urllib3==1.26.0 # via # mocket # opentelemetry-exporter-http-transport # requests python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-common/tests/000077500000000000000000000000001527070070700300415ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-common/tests/__init__.py000066400000000000000000000001141527070070700321460ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-common/tests/test_aggregation.py000066400000000000000000000125301527070070700337420ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # pylint: disable=import-error import unittest from unittest.mock import patch from opentelemetry.exporter.otlp.common._aggregation import ( _get_aggregation, _get_temporality, ) from opentelemetry.sdk.environment_variables import ( OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION, OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE, ) from opentelemetry.sdk.metrics import ( Counter, Histogram, ObservableCounter, ObservableGauge, ObservableUpDownCounter, UpDownCounter, ) from opentelemetry.sdk.metrics.export import AggregationTemporality from opentelemetry.sdk.metrics.view import ( ExplicitBucketHistogramAggregation, ExponentialBucketHistogramAggregation, ) _AGGREGATION_LOGGER_NAME = "opentelemetry.exporter.otlp.common._aggregation" class TestGetTemporality(unittest.TestCase): def test_temporality_default_is_cumulative(self): result = _get_temporality(None) for instrument_class in ( Counter, UpDownCounter, Histogram, ObservableCounter, ObservableUpDownCounter, ObservableGauge, ): with self.subTest(instrument=instrument_class.__name__): self.assertEqual( result[instrument_class], AggregationTemporality.CUMULATIVE, ) def test_temporality_delta_env(self): delta_cases = { Counter: AggregationTemporality.DELTA, UpDownCounter: AggregationTemporality.CUMULATIVE, Histogram: AggregationTemporality.DELTA, ObservableCounter: AggregationTemporality.DELTA, ObservableUpDownCounter: AggregationTemporality.CUMULATIVE, ObservableGauge: AggregationTemporality.CUMULATIVE, } with patch.dict( "os.environ", {OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE: "DELTA"}, ): result = _get_temporality(None) for instrument_class, expected in delta_cases.items(): with self.subTest(instrument=instrument_class.__name__): self.assertEqual(result[instrument_class], expected) def test_temporality_lowmemory_env(self): lowmemory_cases = { Counter: AggregationTemporality.DELTA, UpDownCounter: AggregationTemporality.CUMULATIVE, Histogram: AggregationTemporality.DELTA, ObservableCounter: AggregationTemporality.CUMULATIVE, ObservableUpDownCounter: AggregationTemporality.CUMULATIVE, ObservableGauge: AggregationTemporality.CUMULATIVE, } with patch.dict( "os.environ", {OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE: "LOWMEMORY"}, ): result = _get_temporality(None) for instrument_class, expected in lowmemory_cases.items(): with self.subTest(instrument=instrument_class.__name__): self.assertEqual(result[instrument_class], expected) def test_temporality_invalid_env_logs_warning(self): with ( patch.dict( "os.environ", {OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE: "INVALID"}, ), self.assertLogs(_AGGREGATION_LOGGER_NAME, level="WARNING"), ): result = _get_temporality(None) self.assertEqual( result[Counter], AggregationTemporality.CUMULATIVE, ) def test_temporality_override_takes_precedence(self): with patch.dict( "os.environ", {OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE: "CUMULATIVE"}, ): result = _get_temporality({Counter: AggregationTemporality.DELTA}) self.assertEqual(result[Counter], AggregationTemporality.DELTA) class TestGetAggregation(unittest.TestCase): def test_aggregation_default_is_explicit_bucket(self): result = _get_aggregation(None) self.assertIsInstance( result[Histogram], ExplicitBucketHistogramAggregation, ) def test_aggregation_exponential_env(self): with patch.dict( "os.environ", {OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION: "base2_exponential_bucket_histogram"}, ): result = _get_aggregation(None) self.assertIsInstance( result[Histogram], ExponentialBucketHistogramAggregation, ) def test_aggregation_invalid_env_logs_warning(self): with patch.dict( "os.environ", {OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION: "unknown_aggregation"}, ): with self.assertLogs(_AGGREGATION_LOGGER_NAME, level="WARNING"): result = _get_aggregation(None) self.assertIsInstance( result[Histogram], ExplicitBucketHistogramAggregation, ) def test_aggregation_override_takes_precedence(self): custom_aggregation = ExponentialBucketHistogramAggregation() with patch.dict( "os.environ", {OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION: "explicit_bucket_histogram"}, ): result = _get_aggregation({Histogram: custom_aggregation}) self.assertIs(result[Histogram], custom_aggregation) test_exporter_metrics.py000066400000000000000000000104521527070070700347730ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-common/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations import unittest from unittest.mock import Mock, patch from opentelemetry.exporter.otlp.common._exporter_metrics import ( ExporterMetrics, NoOpExporterMetrics, create_exporter_metrics, ) from opentelemetry.semconv._incubating.attributes.otel_attributes import ( OtelComponentTypeValues, ) from opentelemetry.semconv.attributes.server_attributes import ( SERVER_ADDRESS, SERVER_PORT, ) class TestExporterMetrics(unittest.TestCase): def test_factory_returns_noop_when_disabled(self): meter_provider = Mock() with patch("opentelemetry.exporter.otlp.common._exporter_metrics.get_meter_provider") as get_meter_provider: metrics = create_exporter_metrics( OtelComponentTypeValues.OTLP_HTTP_SPAN_EXPORTER, "traces", "http://localhost:4318/v1/traces", meter_provider, False, ) self.assertIsInstance(metrics, NoOpExporterMetrics) meter_provider.get_meter.assert_not_called() get_meter_provider.assert_not_called() def test_factory_returns_exporter_metrics_when_enabled(self): meter_provider = Mock() meter_provider.get_meter.return_value = Mock() metrics = create_exporter_metrics( OtelComponentTypeValues.OTLP_HTTP_SPAN_EXPORTER, "traces", "http://localhost:4318/v1/traces", meter_provider, True, ) self.assertIsInstance(metrics, ExporterMetrics) meter_provider.get_meter.assert_called_once_with("opentelemetry-sdk") def test_noop_export_operation_yields_result(self): metrics = NoOpExporterMetrics() with metrics.export_operation(1) as result: result.error = RuntimeError("error") self.assertIsInstance(result.error, RuntimeError) def test_grpc_uses_static_port(self): meter_provider = Mock() meter_provider.get_meter.return_value = Mock() cases: list[tuple[OtelComponentTypeValues, str, str, int]] = [ ( OtelComponentTypeValues.OTLP_GRPC_SPAN_EXPORTER, "http://otlp.example.com", "otlp.example.com", 443, ), ( OtelComponentTypeValues.OTLP_GRPC_LOG_EXPORTER, "https://otlp.example.com", "otlp.example.com", 443, ), ( OtelComponentTypeValues.OTLP_GRPC_METRIC_EXPORTER, "otlp.example.com:4317", "otlp.example.com", 4317, ), ( OtelComponentTypeValues.OTLP_GRPC_SPAN_EXPORTER, "http://localhost:4317", "localhost", 4317, ), ] for component_type, endpoint, expected_address, expected_port in cases: with self.subTest(component_type=component_type, endpoint=endpoint): metrics = ExporterMetrics(component_type, "traces", endpoint, meter_provider) # pylint: disable-next=protected-access self.assertEqual(metrics._standard_attrs[SERVER_ADDRESS], expected_address) # pylint: disable-next=protected-access self.assertEqual(metrics._standard_attrs[SERVER_PORT], expected_port) def test_http_port_defaults_by_scheme(self): meter_provider = Mock() meter_provider.get_meter.return_value = Mock() cases: list[tuple[str, int]] = [ ("http://otlp.example.com", 80), ("https://otlp.example.com", 443), ] for endpoint, expected_port in cases: with self.subTest(endpoint=endpoint): metrics = ExporterMetrics( OtelComponentTypeValues.OTLP_HTTP_SPAN_EXPORTER, "traces", endpoint, meter_provider, ) # pylint: disable-next=protected-access self.assertEqual(metrics._standard_attrs[SERVER_ADDRESS], "otlp.example.com") # pylint: disable-next=protected-access self.assertEqual(metrics._standard_attrs[SERVER_PORT], expected_port) python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-common/tests/test_http_client.py000066400000000000000000000450161527070070700337750ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # pylint: disable=unexpected-keyword-arg import gzip import threading import unittest import zlib from collections.abc import Callable, Iterator from contextlib import contextmanager from dataclasses import dataclass, field from datetime import datetime, timezone from email.utils import format_datetime from unittest.mock import Mock, patch # pylint: disable-next=import-error from opentelemetry.exporter.http.transport._base import ( BaseHTTPResult, BaseHTTPTransport, ) # pylint: disable-next=import-error from opentelemetry.exporter.otlp.common.http import ( Compression, _extract_retry_after, _OTLPHTTPClient, ) @contextmanager def _mock_clock( shutdown_event: Mock | None = None, ) -> Iterator[Callable[[float], None]]: _now = [0.0] def advance(delta: float) -> None: _now[0] += delta def get_time() -> float: return _now[0] if shutdown_event is not None: def _wait(duration: float) -> bool: advance(duration) return False shutdown_event.wait.side_effect = _wait with patch( "opentelemetry.exporter.otlp.common.http.time.time", side_effect=get_time, ): yield advance @dataclass(frozen=True, slots=True) class _TestHTTPResult(BaseHTTPResult): response_headers: dict = field(default_factory=dict) # pylint: disable-next=no-self-use def content(self) -> bytes: return b"" def headers(self): return self.response_headers class _TestHTTPTransport(BaseHTTPTransport): def __init__(self, *results, connection_errors=()): self.results = list(results) self.requests = [] self.closed = False self._connection_errors = set(connection_errors) def request( self, method, url, *, headers=None, timeout=None, data=None, ): self.requests.append( { "method": method, "url": url, "headers": headers, "timeout": timeout, "data": data, } ) result = self.results.pop(0) if callable(result): result = result() if isinstance(result, Exception): raise result return result def is_connection_error(self, exception): return exception in self._connection_errors def close(self): self.closed = True class TestOTLPHTTPClient(unittest.TestCase): @staticmethod def _client( transport, *, timeout=5.0, compression=Compression.NONE, jitter=0.0, ): return _OTLPHTTPClient( transport=transport, endpoint="http://example.test/v1/traces", timeout=timeout, compression=compression, headers={"content-type": "application/x-protobuf"}, kind="spans", jitter=jitter, ) def test_export_success_status_codes(self): cases = ( (200, "OK"), (204, "No Content"), (302, "Found"), ) for status_code, reason in cases: with self.subTest(status_code=status_code): transport = _TestHTTPTransport(_TestHTTPResult(status_code=status_code, reason=reason)) client = self._client(transport) result = client.export(b"payload") self.assertTrue(result.success) self.assertEqual(result.status_code, status_code) self.assertEqual(result.reason, reason) self.assertIsNone(result.error) @patch( "opentelemetry.exporter.otlp.common.http.time.time", side_effect=(100.0, 100.0, 100.0), ) def test_export_request_arguments(self, mock_time): transport = _TestHTTPTransport(_TestHTTPResult(status_code=200, reason="OK")) client = self._client(transport, timeout=3.0) client.export(b"payload") self.assertEqual(len(transport.requests), 1) self.assertEqual( transport.requests[0], { "method": "POST", "url": "http://example.test/v1/traces", "headers": {"content-type": "application/x-protobuf"}, "timeout": 3.0, "data": b"payload", }, ) self.assertEqual(mock_time.call_count, 3) def test_export_compresses_payload(self): cases = ( (Compression.NONE, lambda data: data, None), (Compression.GZIP, gzip.decompress, "gzip"), (Compression.DEFLATE, zlib.decompress, "deflate"), ) for compression, decompress, expected_encoding in cases: with self.subTest(compression=compression): transport = _TestHTTPTransport(_TestHTTPResult(status_code=200, reason="OK")) client = self._client(transport, compression=compression) result = client.export(b"payload") self.assertTrue(result.success) self.assertEqual(decompress(transport.requests[0]["data"]), b"payload") headers = transport.requests[0]["headers"] if expected_encoding is None: self.assertNotIn("Content-Encoding", headers) else: self.assertEqual(headers["Content-Encoding"], expected_encoding) def test_export_retryable_status_codes(self): cases = ( (429, "Too Many Requests"), (502, "Bad Gateway"), (503, "Service Unavailable"), (504, "Gateway Timeout"), ) for status_code, reason in cases: with self.subTest(status_code=status_code): shutdown_event = Mock(spec=threading.Event) shutdown_event.is_set.return_value = False shutdown_event.wait.return_value = False transport = _TestHTTPTransport( _TestHTTPResult( status_code=status_code, reason=reason, ), _TestHTTPResult(status_code=200, reason="OK"), ) client = self._client(transport) # pylint: disable-next=protected-access client._shutdown_event = shutdown_event result = client.export(b"payload") self.assertTrue(result.success) self.assertEqual(len(transport.requests), 2) shutdown_event.wait.assert_called_once_with(1.0) def test_export_connection_errors(self): error = RuntimeError("connection failed") transport = _TestHTTPTransport( _TestHTTPResult(error=error), _TestHTTPResult(status_code=200, reason="OK"), connection_errors={error}, ) client = self._client(transport) result = client.export(b"payload") self.assertTrue(result.success) self.assertEqual(len(transport.requests), 2) self.assertAlmostEqual(transport.requests[0]["timeout"], 5.0, 2) self.assertLessEqual( transport.requests[1]["timeout"], transport.requests[0]["timeout"], ) self.assertGreater(transport.requests[1]["timeout"], 0.0) def test_export_non_retryable_errors(self): exception = RuntimeError("request failed") cases = ( ( _TestHTTPResult(status_code=400, reason="Bad Request"), 400, "Bad Request", None, ), ( _TestHTTPResult(status_code=408, reason="Request Timeout"), 408, "Request Timeout", None, ), ( _TestHTTPResult(status_code=500, reason="Internal Server Error"), 500, "Internal Server Error", None, ), ( _TestHTTPResult(error=exception), None, None, exception, ), ( exception, None, None, exception, ), ) for ( response, expected_status_code, expected_reason, expected_error, ) in cases: with self.subTest(response=type(response).__name__): transport = _TestHTTPTransport(response) client = self._client(transport) result = client.export(b"payload") self.assertFalse(result.success) self.assertEqual(result.status_code, expected_status_code) self.assertEqual(result.reason, expected_reason) self.assertIs(result.error, expected_error) def test_export_with_shutdown(self): shutdown_event = Mock(spec=threading.Event) shutdown_event.is_set.return_value = True transport = _TestHTTPTransport(_TestHTTPResult(status_code=503, reason="Service Unavailable")) client = self._client(transport) # pylint: disable-next=protected-access client._shutdown_event = shutdown_event result = client.export(b"payload") self.assertFalse(result.success) self.assertEqual(result.status_code, 503) self.assertEqual(result.reason, "Service Unavailable") shutdown_event.wait.assert_not_called() def test_shutdown_closes_transport(self): transport = _TestHTTPTransport() client = self._client(transport) client.shutdown() self.assertTrue(transport.closed) def test_export_timeout_decreases_per_retry(self): shutdown_event = Mock(spec=threading.Event) shutdown_event.is_set.return_value = False transport = _TestHTTPTransport( _TestHTTPResult(status_code=503, reason="Service Unavailable"), _TestHTTPResult(status_code=503, reason="Service Unavailable"), _TestHTTPResult(status_code=200, reason="OK"), ) client = self._client(transport, timeout=10.0, jitter=0.0) # pylint: disable-next=protected-access client._shutdown_event = shutdown_event with _mock_clock(shutdown_event): result = client.export(b"payload") # retry=0: wait(1.0) -> time=1.0, retry=1: wait(2.0) -> time=3.0, success self.assertTrue(result.success) self.assertAlmostEqual(transport.requests[0]["timeout"], 10.0) self.assertAlmostEqual(transport.requests[1]["timeout"], 9.0) self.assertAlmostEqual(transport.requests[2]["timeout"], 7.0) def test_export_backoff_exhausts_remaining_timeout(self): shutdown_event = Mock(spec=threading.Event) shutdown_event.is_set.return_value = False transport = _TestHTTPTransport( _TestHTTPResult(status_code=503, reason="Service Unavailable"), _TestHTTPResult(status_code=503, reason="Service Unavailable"), ) # timeout=1.5: retry=0 backoff=1.0 fits -> wait(1.0) -> time=1.0 # retry=1 backoff=2.0 > 0.5 remaining -> give up client = self._client(transport, timeout=1.5, jitter=0.0) # pylint: disable-next=protected-access client._shutdown_event = shutdown_event with _mock_clock(shutdown_event): result = client.export(b"payload") self.assertFalse(result.success) self.assertEqual(result.status_code, 503) self.assertEqual(len(transport.requests), 2) shutdown_event.wait.assert_called_once_with(1.0) def test_export_exhausts_max_retries(self): shutdown_event = Mock(spec=threading.Event) shutdown_event.is_set.return_value = False transport = _TestHTTPTransport(*[_TestHTTPResult(status_code=503, reason="Service Unavailable")] * 6) client = self._client(transport, timeout=1000.0, jitter=0.0) # pylint: disable-next=protected-access client._shutdown_event = shutdown_event with _mock_clock(shutdown_event): result = client.export(b"payload") self.assertFalse(result.success) self.assertEqual(len(transport.requests), 6) self.assertEqual(shutdown_event.wait.call_count, 5) self.assertEqual( [call.args[0] for call in shutdown_event.wait.call_args_list], [1.0, 2.0, 4.0, 8.0, 16.0], ) def test_export_connection_error_gets_reduced_timeout(self): stale_error = RuntimeError("stale connection") transport = _TestHTTPTransport( _TestHTTPResult(status_code=200, reason="OK"), connection_errors={stale_error}, ) with _mock_clock() as advance: def _slow_connection_error() -> _TestHTTPResult: advance(2.0) return _TestHTTPResult(error=stale_error) transport.results.insert(0, _slow_connection_error) client = self._client(transport, timeout=5.0) result = client.export(b"payload") # _submit: deadline=0+5=5.0, after first request time=2.0, remaining=3.0 self.assertTrue(result.success) self.assertEqual(len(transport.requests), 2) self.assertAlmostEqual(transport.requests[1]["timeout"], 3.0) def test_export_retry_after_header_used_as_backoff(self): shutdown_event = Mock(spec=threading.Event) shutdown_event.is_set.return_value = False shutdown_event.wait.return_value = False transport = _TestHTTPTransport( _TestHTTPResult( status_code=429, reason="Too Many Requests", response_headers={"retry-after": "5"}, ), _TestHTTPResult(status_code=200, reason="OK"), ) client = self._client(transport, timeout=60.0) # pylint: disable-next=protected-access client._shutdown_event = shutdown_event result = client.export(b"payload") self.assertTrue(result.success) self.assertEqual(len(transport.requests), 2) shutdown_event.wait.assert_called_once_with(5.0) def test_export_retry_after_header_exhausts_timeout(self): shutdown_event = Mock(spec=threading.Event) shutdown_event.is_set.return_value = False transport = _TestHTTPTransport( _TestHTTPResult( status_code=503, reason="Service Unavailable", response_headers={"retry-after": "10"}, ), ) client = self._client(transport, timeout=3.0, jitter=0.0) # pylint: disable-next=protected-access client._shutdown_event = shutdown_event with _mock_clock(shutdown_event): result = client.export(b"payload") self.assertFalse(result.success) self.assertEqual(result.status_code, 503) self.assertEqual(len(transport.requests), 1) shutdown_event.wait.assert_not_called() def test_export_retry_after_http_date(self): base = 1_700_000_000.0 shutdown_event = Mock(spec=threading.Event) shutdown_event.is_set.return_value = False shutdown_event.wait.return_value = False retry_at = format_datetime(datetime.fromtimestamp(base + 30, timezone.utc), usegmt=True) transport = _TestHTTPTransport( _TestHTTPResult( status_code=503, reason="Service Unavailable", response_headers={"retry-after": retry_at}, ), _TestHTTPResult(status_code=200, reason="OK"), ) client = self._client(transport, timeout=120.0) # pylint: disable-next=protected-access client._shutdown_event = shutdown_event with patch( "opentelemetry.exporter.otlp.common.http.time.time", return_value=base, ): result = client.export(b"payload") self.assertTrue(result.success) self.assertEqual(len(transport.requests), 2) shutdown_event.wait.assert_called_once_with(30.0) def test_export_retry_after_http_date_in_past(self): base = 1_700_000_000.0 shutdown_event = Mock(spec=threading.Event) shutdown_event.is_set.return_value = False shutdown_event.wait.return_value = False retry_at = format_datetime(datetime.fromtimestamp(base - 30, timezone.utc), usegmt=True) transport = _TestHTTPTransport( _TestHTTPResult( status_code=429, reason="Too Many Requests", response_headers={"retry-after": retry_at}, ), _TestHTTPResult(status_code=200, reason="OK"), ) client = self._client(transport, timeout=120.0) # pylint: disable-next=protected-access client._shutdown_event = shutdown_event with patch( "opentelemetry.exporter.otlp.common.http.time.time", return_value=base, ): result = client.export(b"payload") self.assertTrue(result.success) self.assertEqual(len(transport.requests), 2) shutdown_event.wait.assert_called_once_with(0.0) def test_export_retry_after_malformed(self): shutdown_event = Mock(spec=threading.Event) shutdown_event.is_set.return_value = False shutdown_event.wait.return_value = False transport = _TestHTTPTransport( _TestHTTPResult( status_code=503, reason="Service Unavailable", response_headers={"retry-after": "not-a-date"}, ), _TestHTTPResult(status_code=200, reason="OK"), ) client = self._client(transport, timeout=60.0, jitter=0.0) # pylint: disable-next=protected-access client._shutdown_event = shutdown_event result = client.export(b"payload") self.assertTrue(result.success) self.assertEqual(len(transport.requests), 2) # No usable Retry-After -> exponential backoff: 2**0 * uniform(1, 1). shutdown_event.wait.assert_called_once_with(1.0) def test_extract_retry_after_edge_cases(self): cases = ( ("5", 5.0), (" 7 ", 7.0), ("-3", 0.0), ("nan", None), ("inf", None), ("garbage", None), ) for value, expected in cases: with self.subTest(value=value): self.assertEqual( _extract_retry_after(_TestHTTPResult(response_headers={"retry-after": value})), expected, ) with self.subTest(value="absent"): self.assertIsNone(_extract_retry_after(_TestHTTPResult())) python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-json-common/000077500000000000000000000000001527070070700276465ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-json-common/LICENSE000066400000000000000000000261351527070070700306620ustar00rootroot00000000000000 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. python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-json-common/README.rst000066400000000000000000000011201527070070700313270ustar00rootroot00000000000000OpenTelemetry JSON Encoding =============================== |pypi| .. |pypi| image:: https://badge.fury.io/py/opentelemetry-exporter-otlp-json-common.svg :target: https://pypi.org/project/opentelemetry-exporter-otlp-json-common/ This library is provided as a convenience to encode to OTLP JSON. Installation ------------ :: pip install opentelemetry-exporter-otlp-json-common References ---------- * `OpenTelemetry `_ * `OpenTelemetry Protocol Specification `_ benchmark-requirements.txt000066400000000000000000000000611527070070700350000ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-json-commonpytest-benchmark==4.0.0 -r test-requirements.txt python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-json-common/benchmarks/000077500000000000000000000000001527070070700317635ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-json-common/benchmarks/conftest.py000066400000000000000000000005371527070070700341670ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import sys from pathlib import Path _pkg_root = str(Path(__file__).resolve().parent.parent) def pytest_configure(config): """Add the package root to sys.path so 'from tests import ...' works.""" if _pkg_root not in sys.path: sys.path.insert(0, _pkg_root) test_benchmark_log_encoder.py000066400000000000000000000013531527070070700376110ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-json-common/benchmarks# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import pytest from opentelemetry._logs import SeverityNumber from opentelemetry.exporter.otlp.json.common._log_encoder import encode_logs from tests import TIME, make_log, make_log_context @pytest.mark.parametrize("batch_size", [1, 10, 100]) def test_benchmark_encode_logs(benchmark, batch_size): ctx = make_log_context() logs = [ make_log( body=f"log message {i}", context=ctx, timestamp=TIME + i, observed_timestamp=TIME + i + 1000, severity_text="INFO", severity_number=SeverityNumber.INFO, ) for i in range(batch_size) ] benchmark(encode_logs, logs) test_benchmark_metrics_encoder.py000066400000000000000000000030721527070070700404760ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-json-common/benchmarks# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from opentelemetry.exporter.otlp.json.common.metrics_encoder import ( encode_metrics, ) from opentelemetry.sdk.metrics import Exemplar from tests import ( SPAN_ID, TIME, TRACE_ID, make_exponential_histogram, make_gauge, make_histogram, make_metrics_data, make_sum, ) def test_benchmark_encode_sum(benchmark): data = make_metrics_data([make_sum()]) benchmark(encode_metrics, data) def test_benchmark_encode_gauge(benchmark): data = make_metrics_data([make_gauge()]) benchmark(encode_metrics, data) def test_benchmark_encode_histogram(benchmark): data = make_metrics_data( [ make_histogram( exemplars=[ Exemplar({"sampled": "true"}, 298.0, TIME, SPAN_ID, TRACE_ID), ], ) ] ) benchmark(encode_metrics, data) def test_benchmark_encode_exponential_histogram(benchmark): data = make_metrics_data([make_exponential_histogram()]) benchmark(encode_metrics, data) def test_benchmark_encode_mixed_metrics(benchmark): data = make_metrics_data( [ make_sum(name="counter"), make_gauge(name="gauge"), make_histogram( name="histogram", exemplars=[ Exemplar({"sampled": "true"}, 298.0, TIME, SPAN_ID, TRACE_ID), ], ), make_exponential_histogram(name="exp_histogram"), ] ) benchmark(encode_metrics, data) test_benchmark_trace_encoder.py000066400000000000000000000027221527070070700401270ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-json-common/benchmarks# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # pylint: disable=protected-access import pytest from opentelemetry.exporter.otlp.json.common.trace_encoder import encode_spans from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.trace import Event, SpanContext from opentelemetry.sdk.util.instrumentation import InstrumentationScope from opentelemetry.trace import Link from opentelemetry.trace.status import Status, StatusCode from tests import BASE_TIME, TRACE_ID, make_span def test_benchmark_encode_span_with_events_and_links(benchmark): link_ctx = SpanContext(TRACE_ID, 0x2222222222222222, is_remote=False) span = make_span( events=tuple( Event( name=f"event_{i}", timestamp=BASE_TIME + i * 10**6, attributes={"event_key": f"val_{i}"}, ) for i in range(5) ), links=tuple(Link(context=link_ctx, attributes={"link_key": True}) for _ in range(3)), resource=Resource({"service.name": "bench-svc"}), instrumentation_scope=InstrumentationScope("bench_lib", "1.0"), ) span._status = Status(StatusCode.ERROR, "benchmark error") benchmark(encode_spans, [span]) @pytest.mark.parametrize("batch_size", [1, 10, 100]) def test_benchmark_encode_spans(benchmark, batch_size): spans = [make_span(name=f"span-{i}", span_id=0x1000 + i) for i in range(batch_size)] benchmark(encode_spans, spans) python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-json-common/pyproject.toml000066400000000000000000000025471527070070700325720ustar00rootroot00000000000000[build-system] requires = ["hatchling"] build-backend = "hatchling.build" [project] name = "opentelemetry-exporter-otlp-json-common" dynamic = ["version"] description = "OpenTelemetry JSON encoding" 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", "Framework :: OpenTelemetry", "Framework :: OpenTelemetry :: Exporters", "Intended Audience :: Developers", "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-proto-json == 0.66b2", "opentelemetry-sdk ~= 1.45.2", ] [project.urls] Homepage = "https://github.com/open-telemetry/opentelemetry-python/tree/main/exporter/opentelemetry-exporter-otlp-json-common" Repository = "https://github.com/open-telemetry/opentelemetry-python" [tool.hatch.version] path = "src/opentelemetry/exporter/otlp/json/common/version/__init__.py" [tool.hatch.build.targets.sdist] include = [ "/src", "/tests", ] [tool.hatch.build.targets.wheel] packages = ["src/opentelemetry"] python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-json-common/src/000077500000000000000000000000001527070070700304355ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-json-common/src/opentelemetry/000077500000000000000000000000001527070070700333315ustar00rootroot00000000000000exporter/000077500000000000000000000000001527070070700351225ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-json-common/src/opentelemetryotlp/000077500000000000000000000000001527070070700361005ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-json-common/src/opentelemetry/exporterjson/000077500000000000000000000000001527070070700370515ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-json-common/src/opentelemetry/exporter/otlpcommon/000077500000000000000000000000001527070070700403415ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-json-common/src/opentelemetry/exporter/otlp/json__init__.py000066400000000000000000000001141527070070700424460ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-json-common/src/opentelemetry/exporter/otlp/json/common# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 _internal/000077500000000000000000000000001527070070700423145ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-json-common/src/opentelemetry/exporter/otlp/json/common__init__.py000066400000000000000000000063531527070070700444340ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-json-common/src/opentelemetry/exporter/otlp/json/common/_internal# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # pyright: reportCallIssue=false from __future__ import annotations import logging from collections.abc import Mapping, Sequence from typing import Any from opentelemetry.proto_json.common.v1.common import AnyValue as JSONAnyValue from opentelemetry.proto_json.common.v1.common import ( ArrayValue as JSONArrayValue, ) from opentelemetry.proto_json.common.v1.common import ( InstrumentationScope as JSONInstrumentationScope, ) from opentelemetry.proto_json.common.v1.common import KeyValue as JSONKeyValue from opentelemetry.proto_json.common.v1.common import ( KeyValueList as JSONKeyValueList, ) from opentelemetry.proto_json.resource.v1.resource import ( Resource as JSONResource, ) from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.util.instrumentation import InstrumentationScope from opentelemetry.util.types import Attributes _logger = logging.getLogger(__name__) def _encode_instrumentation_scope( instrumentation_scope: InstrumentationScope | None, ) -> JSONInstrumentationScope: return ( JSONInstrumentationScope( name=instrumentation_scope.name, version=instrumentation_scope.version, attributes=_encode_attributes(instrumentation_scope.attributes), ) if instrumentation_scope is not None else JSONInstrumentationScope() ) def _encode_resource(resource: Resource) -> JSONResource: return JSONResource(attributes=_encode_attributes(resource.attributes)) # pylint: disable-next=too-many-return-statements def _encode_value(value: Any) -> JSONAnyValue: if value is None: return JSONAnyValue() if isinstance(value, bool): return JSONAnyValue(bool_value=value) if isinstance(value, str): return JSONAnyValue(string_value=value) if isinstance(value, int): return JSONAnyValue(int_value=value) if isinstance(value, float): return JSONAnyValue(double_value=value) if isinstance(value, bytes): return JSONAnyValue(bytes_value=value) if isinstance(value, Sequence): return JSONAnyValue(array_value=JSONArrayValue(values=[_encode_value(v) for v in value])) if isinstance(value, Mapping): return JSONAnyValue( kvlist_value=JSONKeyValueList(values=[_encode_key_value(str(k), v) for k, v in value.items()]) ) raise TypeError(f"Invalid type {type(value)} of value {value}") def _encode_key_value(key: str, value: Any) -> JSONKeyValue: return JSONKeyValue(key=key, value=_encode_value(value)) def _encode_span_id(span_id: int) -> bytes: return span_id.to_bytes(length=8, byteorder="big", signed=False) def _encode_trace_id(trace_id: int) -> bytes: return trace_id.to_bytes(length=16, byteorder="big", signed=False) def _encode_attributes( attributes: Attributes, ) -> list[JSONKeyValue]: if not attributes: return [] json_attributes = [] for key, value in attributes.items(): # pylint: disable=broad-exception-caught try: json_attributes.append(_encode_key_value(key, value)) except Exception as error: _logger.exception("Failed to encode key %s: %s", key, error) return json_attributes _log_encoder/000077500000000000000000000000001527070070700447335ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-json-common/src/opentelemetry/exporter/otlp/json/common/_internal__init__.py000066400000000000000000000064731527070070700470560ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-json-common/src/opentelemetry/exporter/otlp/json/common/_internal/_log_encoder# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # pyright: reportCallIssue=false from __future__ import annotations from collections import defaultdict from collections.abc import Collection from typing import cast from opentelemetry.exporter.otlp.json.common._internal import ( _encode_attributes, _encode_instrumentation_scope, _encode_resource, _encode_span_id, _encode_trace_id, _encode_value, ) from opentelemetry.proto_json.collector.logs.v1.logs_service import ( ExportLogsServiceRequest as JSONExportLogsServiceRequest, ) from opentelemetry.proto_json.logs.v1.logs import LogRecord as JSONLogRecord from opentelemetry.proto_json.logs.v1.logs import ( ResourceLogs as JSONResourceLogs, ) from opentelemetry.proto_json.logs.v1.logs import ( ScopeLogs as JSONScopeLogs, ) from opentelemetry.sdk._logs import ReadableLogRecord from opentelemetry.util.types import Attributes def encode_logs( batch: Collection[ReadableLogRecord], ) -> JSONExportLogsServiceRequest: return JSONExportLogsServiceRequest(resource_logs=_encode_resource_logs(batch)) def _encode_log(readable_log_record: ReadableLogRecord) -> JSONLogRecord: return JSONLogRecord( time_unix_nano=readable_log_record.log_record.timestamp, observed_time_unix_nano=readable_log_record.log_record.observed_timestamp, span_id=_encode_span_id(readable_log_record.log_record.span_id) if readable_log_record.log_record.span_id else None, trace_id=_encode_trace_id(readable_log_record.log_record.trace_id) if readable_log_record.log_record.trace_id else None, flags=int(readable_log_record.log_record.trace_flags), body=_encode_value(readable_log_record.log_record.body), severity_text=readable_log_record.log_record.severity_text, attributes=_encode_attributes(cast(Attributes, readable_log_record.log_record.attributes)), dropped_attributes_count=readable_log_record.dropped_attributes, severity_number=getattr(readable_log_record.log_record.severity_number, "value", None), event_name=readable_log_record.log_record.event_name, ) def _encode_resource_logs( batch: Collection[ReadableLogRecord], ) -> list[JSONResourceLogs]: sdk_resource_logs = defaultdict(lambda: defaultdict(list)) for readable_log in batch: sdk_resource = readable_log.resource sdk_instrumentation = readable_log.instrumentation_scope or None sdk_resource_logs[sdk_resource][sdk_instrumentation].append(_encode_log(readable_log)) json_resource_logs = [] for sdk_resource, sdk_instrumentations in sdk_resource_logs.items(): scope_logs = [] for sdk_instrumentation, json_logs in sdk_instrumentations.items(): scope_logs.append( JSONScopeLogs( scope=(_encode_instrumentation_scope(sdk_instrumentation)), log_records=json_logs, schema_url=sdk_instrumentation.schema_url if sdk_instrumentation else None, ) ) json_resource_logs.append( JSONResourceLogs( resource=_encode_resource(sdk_resource), scope_logs=scope_logs, schema_url=sdk_resource.schema_url, ) ) return json_resource_logs metrics_encoder/000077500000000000000000000000001527070070700454615ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-json-common/src/opentelemetry/exporter/otlp/json/common/_internal__init__.py000066400000000000000000000312351527070070700475760ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-json-common/src/opentelemetry/exporter/otlp/json/common/_internal/metrics_encoder# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # type: ignore[reportArgumentType] # pyright: reportCallIssue=false from __future__ import annotations import logging from collections.abc import Collection, Iterable from dataclasses import replace from typing import TYPE_CHECKING from opentelemetry.exporter.otlp.json.common._internal import ( _encode_attributes, _encode_instrumentation_scope, _encode_span_id, _encode_trace_id, ) from opentelemetry.proto_json.collector.metrics.v1.metrics_service import ( ExportMetricsServiceRequest as JSONExportMetricsServiceRequest, ) from opentelemetry.proto_json.metrics.v1.metrics import ( Exemplar as JSONExemplar, ) from opentelemetry.proto_json.metrics.v1.metrics import ( ExponentialHistogram as JSONExponentialHistogram, ) from opentelemetry.proto_json.metrics.v1.metrics import ( ExponentialHistogramDataPoint as JSONExponentialHistogramDataPoint, ) from opentelemetry.proto_json.metrics.v1.metrics import Gauge as JSONGauge from opentelemetry.proto_json.metrics.v1.metrics import ( Histogram as JSONHistogram, ) from opentelemetry.proto_json.metrics.v1.metrics import ( HistogramDataPoint as JSONHistogramDataPoint, ) from opentelemetry.proto_json.metrics.v1.metrics import Metric as JSONMetric from opentelemetry.proto_json.metrics.v1.metrics import ( NumberDataPoint as JSONNumberDataPoint, ) from opentelemetry.proto_json.metrics.v1.metrics import ( ResourceMetrics as JSONResourceMetrics, ) from opentelemetry.proto_json.metrics.v1.metrics import ( ScopeMetrics as JSONScopeMetrics, ) from opentelemetry.proto_json.metrics.v1.metrics import Sum as JSONSum from opentelemetry.proto_json.resource.v1.resource import ( Resource as JSONResource, ) from opentelemetry.sdk.metrics import ( Exemplar, ) from opentelemetry.sdk.metrics._internal.point import ( ExponentialHistogramDataPoint, HistogramDataPoint, Metric, NumberDataPoint, ResourceMetrics, ScopeMetrics, ) from opentelemetry.sdk.metrics.export import ( ExponentialHistogram, Gauge, Histogram, MetricsData, Sum, ) if TYPE_CHECKING: from typing_extensions import assert_never _logger = logging.getLogger(__name__) _METRIC_DATA_FIELDS = ( "gauge", "sum", "histogram", "exponential_histogram", "summary", ) def encode_metrics(data: MetricsData) -> JSONExportMetricsServiceRequest: return JSONExportMetricsServiceRequest( resource_metrics=[_encode_resource_metrics(rm) for rm in data.resource_metrics] ) # pylint: disable-next=too-many-locals def split_metrics_data( metrics_data: JSONExportMetricsServiceRequest, max_export_batch_size: int | None, ) -> Iterable[JSONExportMetricsServiceRequest]: """Split an ExportMetricsServiceRequest into batches of at most max_export_batch_size data points, preserving resource/scope hierarchy. """ if max_export_batch_size is None: yield metrics_data return batch_size: int = 0 resource_metrics_batch: list[JSONResourceMetrics] = [] scope_metrics_batch: list[JSONScopeMetrics] = [] metrics_batch: list[JSONMetric] = [] for ( resource_metrics, scope_metrics, metric, field_name, data_points, ) in _iter_metric_data_points(metrics_data): if not resource_metrics_batch or resource_metrics_batch[-1].resource is not resource_metrics.resource: scope_metrics_batch = [] resource_metrics_batch.append(replace(resource_metrics, scope_metrics=scope_metrics_batch)) if not scope_metrics_batch or scope_metrics_batch[-1].scope is not scope_metrics.scope: metrics_batch = [] scope_metrics_batch.append(replace(scope_metrics, metrics=metrics_batch)) data_points_batch: list = [] metrics_batch.append(_build_metric_with_data_points(metric, field_name, data_points_batch)) for data_point in data_points: data_points_batch.append(data_point) batch_size += 1 if batch_size >= max_export_batch_size: yield JSONExportMetricsServiceRequest(resource_metrics=resource_metrics_batch) ( resource_metrics_batch, scope_metrics_batch, metrics_batch, data_points_batch, ) = _build_empty_metric_batches(resource_metrics, scope_metrics, metric, field_name) batch_size = 0 if not batch_size: resource_metrics_batch = [] scope_metrics_batch = [] metrics_batch = [] if batch_size: yield JSONExportMetricsServiceRequest(resource_metrics=resource_metrics_batch) def _get_metric_data_field_name(metric: JSONMetric) -> str | None: return next( (f for f in _METRIC_DATA_FIELDS if getattr(metric, f) is not None), None, ) def _iter_metric_data_points( metrics_data: JSONExportMetricsServiceRequest, ) -> Iterable[tuple[JSONResourceMetrics, JSONScopeMetrics, JSONMetric, str, list]]: for resource_metrics in metrics_data.resource_metrics: for scope_metrics in resource_metrics.scope_metrics: for metric in scope_metrics.metrics: field_name = _get_metric_data_field_name(metric) if field_name is None: _logger.warning("Tried to split and export an unsupported metric type. Skipping.") continue dps = getattr(metric, field_name).data_points if not dps: _logger.warning("Unexpected empty metric datapoints. Skipping.") continue yield ( resource_metrics, scope_metrics, metric, field_name, dps, ) def _build_metric_with_data_points( metric: JSONMetric, field_name: str, data_points: list, ) -> JSONMetric: new_data = replace(getattr(metric, field_name), data_points=data_points) return replace(metric, **{field_name: new_data}) def _build_empty_metric_batches( resource_metrics: JSONResourceMetrics, scope_metrics: JSONScopeMetrics, metric: JSONMetric, field_name: str, ) -> tuple[list[JSONResourceMetrics], list[JSONScopeMetrics], list[JSONMetric], list]: data_points_batch = [] metrics_batch = [_build_metric_with_data_points(metric, field_name, data_points_batch)] scope_metrics_batch = [replace(scope_metrics, metrics=metrics_batch)] resource_metrics_batch = [replace(resource_metrics, scope_metrics=scope_metrics_batch)] return ( resource_metrics_batch, scope_metrics_batch, metrics_batch, data_points_batch, ) def _encode_resource_metrics( rm: ResourceMetrics, ) -> JSONResourceMetrics: return JSONResourceMetrics( resource=JSONResource(attributes=_encode_attributes(rm.resource.attributes)), scope_metrics=[_encode_scope_metrics(sm) for sm in rm.scope_metrics], schema_url=rm.resource.schema_url, ) def _encode_scope_metrics( sm: ScopeMetrics, ) -> JSONScopeMetrics: return JSONScopeMetrics( scope=_encode_instrumentation_scope(sm.scope), schema_url=sm.scope.schema_url, metrics=[_encode_metric(m) for m in sm.metrics], ) def _encode_metric(metric: Metric) -> JSONMetric: json_metric = JSONMetric( name=metric.name, description=metric.description, unit=metric.unit, ) if isinstance(metric.data, Gauge): json_metric.gauge = JSONGauge(data_points=[_encode_gauge_data_point(pt) for pt in metric.data.data_points]) elif isinstance(metric.data, Histogram): json_metric.histogram = JSONHistogram( data_points=[_encode_histogram_data_point(pt) for pt in metric.data.data_points], aggregation_temporality=metric.data.aggregation_temporality, ) elif isinstance(metric.data, Sum): json_metric.sum = JSONSum( data_points=[_encode_sum_data_point(pt) for pt in metric.data.data_points], aggregation_temporality=metric.data.aggregation_temporality, is_monotonic=metric.data.is_monotonic, ) elif isinstance(metric.data, ExponentialHistogram): json_metric.exponential_histogram = JSONExponentialHistogram( data_points=[_encode_exponential_histogram_data_point(pt) for pt in metric.data.data_points], aggregation_temporality=metric.data.aggregation_temporality, ) else: if TYPE_CHECKING: assert_never(metric.data) _logger.warning( "unsupported data type %s", metric.data.__class__.__name__, ) return json_metric def _encode_gauge_data_point( data_point: NumberDataPoint, ) -> JSONNumberDataPoint: pt = JSONNumberDataPoint( attributes=_encode_attributes(data_point.attributes), time_unix_nano=data_point.time_unix_nano, exemplars=_encode_exemplars(data_point.exemplars), ) if isinstance(data_point.value, int): pt.as_int = data_point.value else: pt.as_double = data_point.value return pt def _encode_sum_data_point( data_point: NumberDataPoint, ) -> JSONNumberDataPoint: pt = JSONNumberDataPoint( attributes=_encode_attributes(data_point.attributes), start_time_unix_nano=data_point.start_time_unix_nano, time_unix_nano=data_point.time_unix_nano, exemplars=_encode_exemplars(data_point.exemplars), ) if isinstance(data_point.value, int): pt.as_int = data_point.value else: pt.as_double = data_point.value return pt def _encode_histogram_data_point( data_point: HistogramDataPoint, ) -> JSONHistogramDataPoint: return JSONHistogramDataPoint( attributes=_encode_attributes(data_point.attributes), time_unix_nano=data_point.time_unix_nano, start_time_unix_nano=data_point.start_time_unix_nano, exemplars=_encode_exemplars(data_point.exemplars), count=data_point.count, sum=data_point.sum, bucket_counts=list(data_point.bucket_counts), explicit_bounds=list(data_point.explicit_bounds), max=data_point.max, min=data_point.min, ) def _encode_exponential_histogram_data_point( data_point: ExponentialHistogramDataPoint, ) -> JSONExponentialHistogramDataPoint: return JSONExponentialHistogramDataPoint( attributes=_encode_attributes(data_point.attributes), time_unix_nano=data_point.time_unix_nano, start_time_unix_nano=data_point.start_time_unix_nano, exemplars=_encode_exemplars(data_point.exemplars), count=data_point.count, sum=data_point.sum, scale=data_point.scale, zero_count=data_point.zero_count, positive=( JSONExponentialHistogramDataPoint.Buckets( offset=data_point.positive.offset, bucket_counts=list(data_point.positive.bucket_counts), ) if data_point.positive.bucket_counts else None ), negative=( JSONExponentialHistogramDataPoint.Buckets( offset=data_point.negative.offset, bucket_counts=list(data_point.negative.bucket_counts), ) if data_point.negative.bucket_counts else None ), flags=data_point.flags, max=data_point.max, min=data_point.min, ) def _encode_exemplars( sdk_exemplars: Collection[Exemplar], ) -> list[JSONExemplar]: json_exemplars = [] for sdk_exemplar in sdk_exemplars: if sdk_exemplar.span_id is not None and sdk_exemplar.trace_id is not None: json_exemplar = JSONExemplar( time_unix_nano=sdk_exemplar.time_unix_nano, span_id=_encode_span_id(sdk_exemplar.span_id), trace_id=_encode_trace_id(sdk_exemplar.trace_id), filtered_attributes=_encode_attributes(sdk_exemplar.filtered_attributes), ) else: json_exemplar = JSONExemplar( time_unix_nano=sdk_exemplar.time_unix_nano, filtered_attributes=_encode_attributes(sdk_exemplar.filtered_attributes), ) # Assign the value based on its type in the SDK exemplar if isinstance(sdk_exemplar.value, float): json_exemplar.as_double = sdk_exemplar.value elif isinstance(sdk_exemplar.value, int): json_exemplar.as_int = sdk_exemplar.value else: if TYPE_CHECKING: assert_never(sdk_exemplar.value) raise ValueError("Exemplar value must be an int or float") json_exemplars.append(json_exemplar) return json_exemplars trace_encoder/000077500000000000000000000000001527070070700451115ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-json-common/src/opentelemetry/exporter/otlp/json/common/_internal__init__.py000066400000000000000000000127561527070070700472350ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-json-common/src/opentelemetry/exporter/otlp/json/common/_internal/trace_encoder# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # pyright: reportCallIssue=false from __future__ import annotations import logging from collections import defaultdict from collections.abc import Collection from opentelemetry.exporter.otlp.json.common._internal import ( _encode_attributes, _encode_instrumentation_scope, _encode_resource, _encode_span_id, _encode_trace_id, ) from opentelemetry.proto_json.collector.trace.v1.trace_service import ( ExportTraceServiceRequest as JSONExportTraceServiceRequest, ) from opentelemetry.proto_json.trace.v1.trace import ( ResourceSpans as JSONResourceSpans, ) from opentelemetry.proto_json.trace.v1.trace import ( ScopeSpans as JSONScopeSpans, ) from opentelemetry.proto_json.trace.v1.trace import Span as JSONSpan from opentelemetry.proto_json.trace.v1.trace import ( SpanFlags as JSONSpanFlags, ) from opentelemetry.proto_json.trace.v1.trace import Status as JSONStatus from opentelemetry.sdk.trace import Event, ReadableSpan from opentelemetry.trace import Link, SpanKind from opentelemetry.trace.span import SpanContext, Status, TraceState # pylint: disable=E1101 _SPAN_KIND_MAP = { SpanKind.INTERNAL: JSONSpan.SpanKind.SPAN_KIND_INTERNAL, SpanKind.SERVER: JSONSpan.SpanKind.SPAN_KIND_SERVER, SpanKind.CLIENT: JSONSpan.SpanKind.SPAN_KIND_CLIENT, SpanKind.PRODUCER: JSONSpan.SpanKind.SPAN_KIND_PRODUCER, SpanKind.CONSUMER: JSONSpan.SpanKind.SPAN_KIND_CONSUMER, } _logger = logging.getLogger(__name__) def encode_spans( sdk_spans: Collection[ReadableSpan], ) -> JSONExportTraceServiceRequest: return JSONExportTraceServiceRequest(resource_spans=_encode_resource_spans(sdk_spans)) def _encode_resource_spans( sdk_spans: Collection[ReadableSpan], ) -> list[JSONResourceSpans]: sdk_resource_spans = defaultdict(lambda: defaultdict(list)) for sdk_span in sdk_spans: sdk_resource = sdk_span.resource sdk_instrumentation = sdk_span.instrumentation_scope or None sdk_resource_spans[sdk_resource][sdk_instrumentation].append(_encode_span(sdk_span)) json_resource_spans = [] for sdk_resource, sdk_instrumentations in sdk_resource_spans.items(): scope_spans = [] for sdk_instrumentation, json_spans in sdk_instrumentations.items(): scope_spans.append( JSONScopeSpans( scope=(_encode_instrumentation_scope(sdk_instrumentation)), spans=json_spans, schema_url=sdk_instrumentation.schema_url if sdk_instrumentation else None, ) ) json_resource_spans.append( JSONResourceSpans( resource=_encode_resource(sdk_resource), scope_spans=scope_spans, schema_url=sdk_resource.schema_url, ) ) return json_resource_spans def _span_flags(parent_span_context: SpanContext | None) -> int: flags = JSONSpanFlags.SPAN_FLAGS_CONTEXT_HAS_IS_REMOTE_MASK if parent_span_context and parent_span_context.is_remote: flags |= JSONSpanFlags.SPAN_FLAGS_CONTEXT_IS_REMOTE_MASK return int(flags) def _encode_span(sdk_span: ReadableSpan) -> JSONSpan: span_context = sdk_span.get_span_context() return JSONSpan( trace_id=_encode_trace_id(span_context.trace_id if span_context else 0), span_id=_encode_span_id(span_context.span_id if span_context else 0), trace_state=_encode_trace_state(span_context.trace_state if span_context else None), parent_span_id=_encode_context_span_id(sdk_span.parent), name=sdk_span.name, kind=_SPAN_KIND_MAP[sdk_span.kind], start_time_unix_nano=sdk_span.start_time, end_time_unix_nano=sdk_span.end_time, attributes=_encode_attributes(sdk_span.attributes), events=_encode_events(sdk_span.events), links=_encode_links(sdk_span.links), status=_encode_status(sdk_span.status), dropped_attributes_count=sdk_span.dropped_attributes, dropped_events_count=sdk_span.dropped_events, dropped_links_count=sdk_span.dropped_links, flags=_span_flags(sdk_span.parent), ) def _encode_events( events: Collection[Event], ) -> list[JSONSpan.Event]: return [ JSONSpan.Event( name=event.name, time_unix_nano=event.timestamp, attributes=_encode_attributes(event.attributes), dropped_attributes_count=event.dropped_attributes, ) for event in events ] def _encode_links(links: Collection[Link]) -> list[JSONSpan.Link]: return [ JSONSpan.Link( trace_id=_encode_trace_id(link.context.trace_id), span_id=_encode_span_id(link.context.span_id), attributes=_encode_attributes(link.attributes), dropped_attributes_count=link.dropped_attributes, flags=_span_flags(link.context), ) for link in links ] def _encode_status(status: Status | None) -> JSONStatus | None: return ( JSONStatus( code=JSONStatus.StatusCode(status.status_code.value), message=status.description, ) if status is not None else None ) def _encode_trace_state(trace_state: TraceState | None) -> str | None: return ",".join([f"{key}={value}" for key, value in (trace_state.items())]) if trace_state is not None else None def _encode_context_span_id(context: SpanContext | None) -> bytes | None: return _encode_span_id(context.span_id) if context else None _log_encoder.py000066400000000000000000000003101527070070700433240ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-json-common/src/opentelemetry/exporter/otlp/json/common# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from opentelemetry.exporter.otlp.json.common._internal._log_encoder import ( encode_logs, ) __all__ = ["encode_logs"] metrics_encoder.py000066400000000000000000000003211527070070700440540ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-json-common/src/opentelemetry/exporter/otlp/json/common# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from opentelemetry.exporter.otlp.json.common._internal.metrics_encoder import ( encode_metrics, ) __all__ = ["encode_metrics"] py.typed000066400000000000000000000000001527070070700420260ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-json-common/src/opentelemetry/exporter/otlp/json/commontrace_encoder.py000066400000000000000000000003131527070070700435050ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-json-common/src/opentelemetry/exporter/otlp/json/common# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from opentelemetry.exporter.otlp.json.common._internal.trace_encoder import ( encode_spans, ) __all__ = ["encode_spans"] version/000077500000000000000000000000001527070070700420265ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-json-common/src/opentelemetry/exporter/otlp/json/common__init__.py000066400000000000000000000001441527070070700441360ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-json-common/src/opentelemetry/exporter/otlp/json/common/version# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 __version__ = "0.66b2" python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-json-common/test-requirements.txt000066400000000000000000000006621527070070700341130ustar00rootroot00000000000000asgiref==3.7.2 iniconfig==2.0.0 packaging==24.0 pluggy==1.6.0 protobuf==6.33.5 py-cpuinfo==9.0.0 pytest==9.0.3 tomli==2.0.1 typing_extensions==4.12.0 wrapt==1.16.0 -e opentelemetry-api -e opentelemetry-sdk -e opentelemetry-semantic-conventions -e tests/opentelemetry-test-utils -e opentelemetry-proto-json -e opentelemetry-proto -e exporter/opentelemetry-exporter-otlp-json-common -e exporter/opentelemetry-exporter-otlp-proto-common python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-json-common/tests/000077500000000000000000000000001527070070700310105ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-json-common/tests/__init__.py000066400000000000000000000225731527070070700331320ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import dataclasses import unittest from opentelemetry._logs import LogRecord, SeverityNumber from opentelemetry.sdk._logs import ReadableLogRecord from opentelemetry.sdk.metrics.export import ( AggregationTemporality, Buckets, ExponentialHistogram, ExponentialHistogramDataPoint, Gauge, Histogram, HistogramDataPoint, Metric, MetricsData, NumberDataPoint, ResourceMetrics, ScopeMetrics, Sum, ) from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.trace import SpanContext, _Span from opentelemetry.sdk.util.instrumentation import InstrumentationScope from opentelemetry.trace import ( NonRecordingSpan, TraceFlags, set_span_in_context, ) TRACE_ID = 0x3E0C63257DE34C926F9EFCD03927272E SPAN_ID = 0x34BF92DEEFC58C92 PARENT_SPAN_ID = 0x1111111111111111 BASE_TIME = 683647322 * 10**9 START_TIME = 1641946015139533244 TIME = 1641946016139533244 _UNSET = object() def _is_none_equivalent(val_a, val_b): """Check if two values should be treated as equal because one is None and the other is the empty/zero default for that type. Only None and type-matching empty defaults are considered equivalent: None ~= [] (empty list) None ~= "" (empty string) None ~= b"" (empty bytes) None ~= 0 (zero int) None ~= 0.0 (zero float) """ if val_a is None and val_b is None: return True if val_a is None: return val_b == type(val_b)() if val_b is None: return val_a == type(val_a)() return False def assert_proto_json_equal(test_case: unittest.TestCase, obj_a, obj_b, path: str = ""): """Recursively compare two proto_json dataclass objects, treating None as equivalent to the type's empty default ([], "", b"", 0, 0.0).""" if dataclasses.is_dataclass(obj_a) and dataclasses.is_dataclass(obj_b): for field in dataclasses.fields(obj_a): field_path = f"{path}.{field.name}" if path else field.name val_a = getattr(obj_a, field.name) val_b = getattr(obj_b, field.name) assert_proto_json_equal(test_case, val_a, val_b, field_path) elif isinstance(obj_a, list) and isinstance(obj_b, list): test_case.assertEqual( len(obj_a), len(obj_b), f"List length mismatch at {path}: {len(obj_a)} != {len(obj_b)}", ) for idx, (item_a, item_b) in enumerate(zip(obj_a, obj_b)): assert_proto_json_equal(test_case, item_a, item_b, f"{path}[{idx}]") elif _is_none_equivalent(obj_a, obj_b): pass else: test_case.assertEqual(obj_a, obj_b, f"Mismatch at {path}: {obj_a!r} != {obj_b!r}") def make_span_unended( name="test-span", trace_id=TRACE_ID, span_id=SPAN_ID, parent=None, resource=None, instrumentation_scope=None, events=(), links=(), start_time=BASE_TIME, ): if resource is None: resource = Resource({}) span = _Span( name=name, context=SpanContext( trace_id, span_id, is_remote=False, trace_flags=TraceFlags(TraceFlags.SAMPLED), ), parent=parent, resource=resource, instrumentation_scope=instrumentation_scope, events=events, links=links, ) span.start(start_time=start_time) return span def make_span( name="test-span", trace_id=TRACE_ID, span_id=SPAN_ID, parent=None, resource=None, instrumentation_scope=None, events=(), links=(), start_time=BASE_TIME, end_time=BASE_TIME + 50 * 10**6, ): span = make_span_unended( name=name, trace_id=trace_id, span_id=span_id, parent=parent, resource=resource, instrumentation_scope=instrumentation_scope, events=events, links=links, start_time=start_time, ) span.end(end_time=end_time) return span def make_log_context(trace_id=TRACE_ID, span_id=SPAN_ID): return set_span_in_context(NonRecordingSpan(SpanContext(trace_id, span_id, False, TraceFlags(0x01)))) def make_log( body="test log message", severity_text="INFO", severity_number=SeverityNumber.INFO, attributes=None, timestamp=TIME, observed_timestamp=TIME + 1000, resource=None, instrumentation_scope=_UNSET, event_name=None, context=None, limits=None, ): kwargs = { "timestamp": timestamp, "observed_timestamp": observed_timestamp, "severity_text": severity_text, "severity_number": severity_number, "body": body, "attributes": attributes or {}, "event_name": event_name, } if context is not None: kwargs["context"] = context rkwargs = { "resource": resource or Resource({}), "instrumentation_scope": InstrumentationScope("test_scope", "1.0") if instrumentation_scope is _UNSET else instrumentation_scope, } if limits is not None: rkwargs["limits"] = limits return ReadableLogRecord(LogRecord(**kwargs), **rkwargs) def make_metrics_data( metrics, resource_attrs=None, resource_schema_url=None, scope_name="test_scope", scope_version="1.0", scope_schema_url=None, ): return MetricsData( resource_metrics=[ ResourceMetrics( resource=Resource( attributes=resource_attrs or {}, schema_url=resource_schema_url, ), scope_metrics=[ ScopeMetrics( scope=InstrumentationScope( name=scope_name, version=scope_version, schema_url=scope_schema_url, ), metrics=metrics, schema_url=scope_schema_url, ) ], schema_url=resource_schema_url, ) ] ) def make_sum( name="test_sum", value=33, attributes=None, temporality=AggregationTemporality.CUMULATIVE, is_monotonic=True, description="desc", unit="s", ): return Metric( name=name, description=description, unit=unit, data=Sum( data_points=[ NumberDataPoint( attributes=attributes or {"a": 1}, start_time_unix_nano=START_TIME, time_unix_nano=TIME, value=value, ) ], aggregation_temporality=temporality, is_monotonic=is_monotonic, ), ) def make_gauge( name="test_gauge", value=9000, attributes=None, description="desc", unit="1", ): return Metric( name=name, description=description, unit=unit, data=Gauge( data_points=[ NumberDataPoint( attributes=attributes or {"a": 1}, start_time_unix_nano=None, time_unix_nano=TIME, value=value, ) ], ), ) def make_histogram( name="test_histogram", attributes=None, count=5, sum_value=67, bucket_counts=None, explicit_bounds=None, min_value=8, max_value=18, exemplars=None, temporality=AggregationTemporality.DELTA, description="desc", unit="ms", ): return Metric( name=name, description=description, unit=unit, data=Histogram( data_points=[ HistogramDataPoint( attributes=attributes or {"a": 1}, start_time_unix_nano=START_TIME, time_unix_nano=TIME, count=count, sum=sum_value, bucket_counts=bucket_counts or [1, 4], explicit_bounds=explicit_bounds or [10.0, 20.0], min=min_value, max=max_value, exemplars=exemplars or [], ) ], aggregation_temporality=temporality, ), ) def make_exponential_histogram( name="test_exp_hist", attributes=None, count=10, sum_value=100.5, scale=1, zero_count=2, positive=None, negative=None, flags=0, min_value=1.0, max_value=50.0, exemplars=None, temporality=AggregationTemporality.CUMULATIVE, description="desc", unit="s", ): return Metric( name=name, description=description, unit=unit, data=ExponentialHistogram( data_points=[ ExponentialHistogramDataPoint( attributes=attributes or {"a": 1}, start_time_unix_nano=START_TIME, time_unix_nano=TIME, count=count, sum=sum_value, scale=scale, zero_count=zero_count, positive=positive or Buckets(offset=0, bucket_counts=[1, 2, 3]), negative=negative or Buckets(offset=1, bucket_counts=[1]), flags=flags, min=min_value, max=max_value, exemplars=exemplars or [], ) ], aggregation_temporality=temporality, ), ) test_common_encoder.py000066400000000000000000000256161527070070700353430ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-json-common/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # pylint: disable=protected-access import base64 import unittest from logging import ERROR from opentelemetry.exporter.otlp.json.common._internal import ( _encode_attributes, _encode_instrumentation_scope, _encode_key_value, _encode_resource, _encode_span_id, _encode_trace_id, _encode_value, ) from opentelemetry.proto_json.common.v1.common import AnyValue as JSONAnyValue from opentelemetry.proto_json.common.v1.common import ( ArrayValue as JSONArrayValue, ) from opentelemetry.proto_json.common.v1.common import ( InstrumentationScope as JSONInstrumentationScope, ) from opentelemetry.proto_json.common.v1.common import KeyValue as JSONKeyValue from opentelemetry.proto_json.common.v1.common import ( KeyValueList as JSONKeyValueList, ) from opentelemetry.proto_json.resource.v1.resource import ( Resource as JSONResource, ) from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.util.instrumentation import InstrumentationScope _COMMON_LOGGER_NAME = "opentelemetry.exporter.otlp.json.common._internal" class CallingStrRaisesException: def __str__(self): raise ValueError("Cannot encode") class TestCommonEncoder(unittest.TestCase): def test_encode_value(self): cases = [ ( "string", "hello", JSONAnyValue(string_value="hello"), {"stringValue": "hello"}, ), ( "bool_true", True, JSONAnyValue(bool_value=True), {"boolValue": True}, ), ( "bool_false", False, JSONAnyValue(bool_value=False), {"boolValue": False}, ), ( "int", 42, JSONAnyValue(int_value=42), {"intValue": "42"}, ), ( "int_zero", 0, JSONAnyValue(int_value=0), {"intValue": "0"}, ), ( "int_negative", -7, JSONAnyValue(int_value=-7), {"intValue": "-7"}, ), ( "float", 3.14, JSONAnyValue(double_value=3.14), {"doubleValue": 3.14}, ), ( "bytes", b"\x01\x02\x03", JSONAnyValue(bytes_value=b"\x01\x02\x03"), {"bytesValue": base64.b64encode(b"\x01\x02\x03").decode("utf-8")}, ), ] for name, value, expected_obj, expected_dict in cases: with self.subTest(name=name): result = _encode_value(value) self.assertEqual(result, expected_obj) self.assertEqual(result.to_dict(), expected_dict) def test_encode_value_sequence(self): result = _encode_value([1, 2, 3]) expected = JSONAnyValue( array_value=JSONArrayValue( values=[ JSONAnyValue(int_value=1), JSONAnyValue(int_value=2), JSONAnyValue(int_value=3), ] ) ) self.assertEqual(result, expected) result_dict = result.to_dict() self.assertIn("arrayValue", result_dict) self.assertEqual(len(result_dict["arrayValue"]["values"]), 3) def test_encode_value_mapping(self): result = _encode_value({"key": "val", "num": 1}) expected = JSONAnyValue( kvlist_value=JSONKeyValueList( values=[ JSONKeyValue(key="key", value=JSONAnyValue(string_value="val")), JSONKeyValue(key="num", value=JSONAnyValue(int_value=1)), ] ) ) self.assertEqual(result, expected) result_dict = result.to_dict() self.assertIn("kvlistValue", result_dict) def test_encode_null(self): result = _encode_value(None) self.assertEqual(result, JSONAnyValue()) def test_encode_array_with_nulls(self): result = _encode_value([1, None, 2]) expected = JSONAnyValue( array_value=JSONArrayValue( values=[ JSONAnyValue(int_value=1), JSONAnyValue(), JSONAnyValue(int_value=2), ] ) ) self.assertEqual(result, expected) self.assertEqual(result.array_value.values[1].to_dict(), {}) def test_encode_key_value(self): result = _encode_key_value("mykey", "myval") expected = JSONKeyValue(key="mykey", value=JSONAnyValue(string_value="myval")) self.assertEqual(result, expected) self.assertEqual( result.to_dict(), {"key": "mykey", "value": {"stringValue": "myval"}}, ) def test_encode_attributes_all_types(self): result = _encode_attributes( { "a": 1, "b": 3.14, "c": False, "hello": "world", "greet": ["hola", "bonjour"], "data": [1, 2], "data_granular": [1.4, 2.4], "binary_data": b"x00\x01\x02", } ) self.assertEqual( result, [ JSONKeyValue(key="a", value=JSONAnyValue(int_value=1)), JSONKeyValue(key="b", value=JSONAnyValue(double_value=3.14)), JSONKeyValue(key="c", value=JSONAnyValue(bool_value=False)), JSONKeyValue(key="hello", value=JSONAnyValue(string_value="world")), JSONKeyValue( key="greet", value=JSONAnyValue( array_value=JSONArrayValue( values=[ JSONAnyValue(string_value="hola"), JSONAnyValue(string_value="bonjour"), ] ) ), ), JSONKeyValue( key="data", value=JSONAnyValue( array_value=JSONArrayValue( values=[ JSONAnyValue(int_value=1), JSONAnyValue(int_value=2), ] ) ), ), JSONKeyValue( key="data_granular", value=JSONAnyValue( array_value=JSONArrayValue( values=[ JSONAnyValue(double_value=1.4), JSONAnyValue(double_value=2.4), ] ) ), ), JSONKeyValue( key="binary_data", value=JSONAnyValue(bytes_value=b"x00\x01\x02"), ), ], ) def test_encode_attributes_empty(self): self.assertEqual(_encode_attributes({}), []) self.assertEqual(_encode_attributes(None), []) def test_encode_attributes_error_skips_bad_key(self): with self.assertLogs(level=ERROR) as error: result = _encode_attributes({"a": 1, "bad_key": CallingStrRaisesException(), "b": 2}) self.assertEqual(len(error.records), 1) self.assertEqual(error.records[0].msg, "Failed to encode key %s: %s") self.assertEqual(error.records[0].args[0], "bad_key") self.assertIsInstance(error.records[0].args[1], Exception) self.assertEqual( result, [ JSONKeyValue(key="a", value=JSONAnyValue(int_value=1)), JSONKeyValue(key="b", value=JSONAnyValue(int_value=2)), ], ) def test_encode_attributes_error_list_unencodable_item(self): with self.assertLogs(level=ERROR) as error: result = _encode_attributes( { "a": 1, "bad_key": ["test", CallingStrRaisesException(), "test"], "b": 2, } ) self.assertEqual(len(error.records), 1) self.assertEqual(error.records[0].msg, "Failed to encode key %s: %s") self.assertEqual(error.records[0].args[0], "bad_key") self.assertIsInstance(error.records[0].args[1], Exception) self.assertEqual( result, [ JSONKeyValue(key="a", value=JSONAnyValue(int_value=1)), JSONKeyValue(key="b", value=JSONAnyValue(int_value=2)), ], ) def test_encode_span_id(self): span_id = 0x1234567890ABCDEF result = _encode_span_id(span_id) self.assertEqual(result, b"\x12\x34\x56\x78\x90\xab\xcd\xef") self.assertEqual(len(result), 8) def test_encode_trace_id(self): trace_id = 0x3E0C63257DE34C926F9EFCD03927272E result = _encode_trace_id(trace_id) self.assertEqual( result, b"\x3e\x0c\x63\x25\x7d\xe3\x4c\x92\x6f\x9e\xfc\xd0\x39\x27\x27\x2e", ) self.assertEqual(len(result), 16) def test_encode_resource(self): resource = Resource({"key": "val"}) result = _encode_resource(resource) expected = JSONResource(attributes=[JSONKeyValue(key="key", value=JSONAnyValue(string_value="val"))]) self.assertEqual(result, expected) result_dict = result.to_dict() self.assertIn("attributes", result_dict) self.assertEqual(len(result_dict["attributes"]), 1) self.assertEqual( result_dict["attributes"][0], {"key": "key", "value": {"stringValue": "val"}}, ) def test_encode_resource_empty(self): resource = Resource({}) result = _encode_resource(resource) self.assertEqual(result, JSONResource()) self.assertEqual(result.to_dict(), {}) def test_encode_instrumentation_scope(self): scope = InstrumentationScope( name="my_lib", version="1.0.0", attributes={"k": 1}, ) result = _encode_instrumentation_scope(scope) expected = JSONInstrumentationScope( name="my_lib", version="1.0.0", attributes=[JSONKeyValue(key="k", value=JSONAnyValue(int_value=1))], ) self.assertEqual(result, expected) result_dict = result.to_dict() self.assertEqual(result_dict["name"], "my_lib") self.assertEqual(result_dict["version"], "1.0.0") self.assertIn("attributes", result_dict) def test_encode_instrumentation_scope_none(self): result = _encode_instrumentation_scope(None) self.assertEqual(result, JSONInstrumentationScope()) self.assertEqual(result.to_dict(), {}) test_log_encoder.py000066400000000000000000000256221527070070700346310ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-json-common/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import unittest from opentelemetry._logs import LogRecord, SeverityNumber from opentelemetry.exporter.otlp.proto.common._internal import ( _encode_attributes, _encode_span_id, _encode_trace_id, _encode_value, ) from opentelemetry.exporter.otlp.proto.common._log_encoder import encode_logs from opentelemetry.proto.collector.logs.v1.logs_service_pb2 import ( ExportLogsServiceRequest, ) from opentelemetry.proto.common.v1.common_pb2 import AnyValue as PB2AnyValue from opentelemetry.proto.common.v1.common_pb2 import ( InstrumentationScope as PB2InstrumentationScope, ) from opentelemetry.proto.common.v1.common_pb2 import KeyValue as PB2KeyValue from opentelemetry.proto.logs.v1.logs_pb2 import LogRecord as PB2LogRecord from opentelemetry.proto.logs.v1.logs_pb2 import ( ResourceLogs as PB2ResourceLogs, ) from opentelemetry.proto.logs.v1.logs_pb2 import ScopeLogs as PB2ScopeLogs from opentelemetry.proto.resource.v1.resource_pb2 import ( Resource as PB2Resource, ) from opentelemetry.sdk._logs import LogRecordLimits, ReadWriteLogRecord from opentelemetry.sdk.resources import Resource as SDKResource from opentelemetry.sdk.util.instrumentation import InstrumentationScope from opentelemetry.trace import ( NonRecordingSpan, SpanContext, TraceFlags, set_span_in_context, ) _CONTEXT_LOG = set_span_in_context( NonRecordingSpan( SpanContext( 89564621134313219400156819398935297684, 1312458408527513268, False, TraceFlags(0x01), ) ) ) class TestOTLPLogEncoder(unittest.TestCase): def test_encode_basic_log_record(self): basic_log_record = ReadWriteLogRecord( LogRecord( timestamp=1644650195189786880, observed_timestamp=1644650195189786881, context=_CONTEXT_LOG, severity_text="WARN", severity_number=SeverityNumber.WARN, body="Do not go gentle into that good night. Rage, rage against the dying of the light", attributes={"a": 1, "b": "c"}, ), resource=SDKResource( {"first_resource": "value"}, "resource_schema_url", ), instrumentation_scope=InstrumentationScope("first_name", "first_version"), ) pb2_service_request = ExportLogsServiceRequest( resource_logs=[ PB2ResourceLogs( resource=PB2Resource( attributes=[ PB2KeyValue( key="first_resource", value=PB2AnyValue(string_value="value"), ) ] ), scope_logs=[ PB2ScopeLogs( scope=PB2InstrumentationScope(name="first_name", version="first_version"), log_records=[ PB2LogRecord( time_unix_nano=1644650195189786880, observed_time_unix_nano=1644650195189786881, trace_id=_encode_trace_id(89564621134313219400156819398935297684), span_id=_encode_span_id(1312458408527513268), flags=int(TraceFlags(0x01)), severity_text="WARN", severity_number=SeverityNumber.WARN.value, body=_encode_value( "Do not go gentle into that good night. Rage, rage against the dying of the light" ), attributes=_encode_attributes({"a": 1, "b": "c"}), ) ], ), ], schema_url="resource_schema_url", ) ] ) self.assertEqual(encode_logs([basic_log_record]), pb2_service_request) def test_encode_log_record_with_no_instrumentation_scope_and_dict_body( self, ): log_record_with_no_instrumentation_scope_and_dict_body = ReadWriteLogRecord( LogRecord( timestamp=1644650427658989056, observed_timestamp=1644650427658989057, context=_CONTEXT_LOG, severity_text="DEBUG", severity_number=SeverityNumber.DEBUG, body={"error": None, "array_with_nones": [1, None, 2]}, attributes={"a": 1, "b": "c"}, ), resource=SDKResource({"second_resource": "CASE"}), instrumentation_scope=None, ) pb2_resource_logs = PB2ResourceLogs( resource=PB2Resource( attributes=[ PB2KeyValue( key="second_resource", value=PB2AnyValue(string_value="CASE"), ) ] ), scope_logs=[ PB2ScopeLogs( scope=PB2InstrumentationScope(), log_records=[ PB2LogRecord( time_unix_nano=1644650427658989056, observed_time_unix_nano=1644650427658989057, trace_id=_encode_trace_id(89564621134313219400156819398935297684), span_id=_encode_span_id(1312458408527513268), flags=int(TraceFlags(0x01)), severity_text="DEBUG", severity_number=SeverityNumber.DEBUG.value, body=_encode_value( { "error": None, "array_with_nones": [1, None, 2], } ), attributes=_encode_attributes({"a": 1, "b": "c"}), ) ], ) ], ) self.assertEqual( encode_logs([log_record_with_no_instrumentation_scope_and_dict_body]), ExportLogsServiceRequest(resource_logs=[pb2_resource_logs]), ) def test_encode_log_record_with_empty_resource_and_dict_attribute_value( self, ): log_record_with_empty_resource_and_dict_attribute_value = ReadWriteLogRecord( LogRecord( timestamp=1644650584292683033, observed_timestamp=1644650584292683033, context=_CONTEXT_LOG, severity_text="FATAL", severity_number=SeverityNumber.FATAL, body="This instrumentation scope has a schema url and attributes", attributes={"extended": {"sequence": [{"inner": "mapping", "none": None}]}}, ), resource=SDKResource({}), instrumentation_scope=InstrumentationScope( "scope_with_attributes", "scope_with_attributes_version", "instrumentation_schema_url", {"one": 1, "two": "2"}, ), ) pb2_resource_logs = PB2ResourceLogs( resource=PB2Resource(attributes=[]), scope_logs=[ PB2ScopeLogs( scope=PB2InstrumentationScope( name="scope_with_attributes", version="scope_with_attributes_version", attributes=_encode_attributes({"one": 1, "two": "2"}), ), log_records=[ PB2LogRecord( time_unix_nano=1644650584292683033, observed_time_unix_nano=1644650584292683033, trace_id=_encode_trace_id(89564621134313219400156819398935297684), span_id=_encode_span_id(1312458408527513268), flags=int(TraceFlags(0x01)), severity_text="FATAL", severity_number=SeverityNumber.FATAL.value, body=_encode_value("This instrumentation scope has a schema url and attributes"), attributes=_encode_attributes( {"extended": {"sequence": [{"inner": "mapping", "none": None}]}} ), ) ], schema_url="instrumentation_schema_url", ) ], ) self.assertEqual( encode_logs([log_record_with_empty_resource_and_dict_attribute_value]), ExportLogsServiceRequest(resource_logs=[pb2_resource_logs]), ) def test_dropped_attributes_count(self): sdk_logs = self._get_test_logs_dropped_attributes() encoded_logs = encode_logs(sdk_logs) self.assertTrue(hasattr(sdk_logs[0], "dropped_attributes")) self.assertEqual( # pylint:disable=no-member encoded_logs.resource_logs[0].scope_logs[0].log_records[0].dropped_attributes_count, 2, ) @staticmethod def _get_test_logs_dropped_attributes() -> list[ReadWriteLogRecord]: ctx_log1 = set_span_in_context( NonRecordingSpan( SpanContext( 89564621134313219400156819398935297684, 1312458408527513268, False, TraceFlags(0x01), ) ) ) log1 = ReadWriteLogRecord( LogRecord( timestamp=1644650195189786880, context=ctx_log1, severity_text="WARN", severity_number=SeverityNumber.WARN, body="Do not go gentle into that good night. Rage, rage against the dying of the light", attributes={"a": 1, "b": "c", "user_id": "B121092"}, ), resource=SDKResource({"first_resource": "value"}), limits=LogRecordLimits(max_attributes=1), instrumentation_scope=InstrumentationScope("first_name", "first_version"), ) ctx_log2 = set_span_in_context(NonRecordingSpan(SpanContext(0, 0, False))) log2 = ReadWriteLogRecord( LogRecord( timestamp=1644650249738562048, context=ctx_log2, severity_text="WARN", severity_number=SeverityNumber.WARN, body="Cooper, this is no time for caution!", attributes={}, ), resource=SDKResource({"second_resource": "CASE"}), instrumentation_scope=InstrumentationScope("second_name", "second_version"), ) return [log1, log2] test_metrics_encoder.py000066400000000000000000000221051527070070700355070ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-json-common/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # pylint: disable=protected-access,unsubscriptable-object import json import unittest from logging import WARNING from opentelemetry.exporter.otlp.json.common.metrics_encoder import ( encode_metrics, ) from opentelemetry.proto_json.collector.metrics.v1.metrics_service import ( ExportMetricsServiceRequest as JSONExportMetricsServiceRequest, ) from opentelemetry.sdk.metrics import Exemplar from opentelemetry.sdk.metrics.export import ( AggregationTemporality, Buckets, Metric, ) from tests import ( SPAN_ID, TIME, TRACE_ID, assert_proto_json_equal, make_exponential_histogram, make_gauge, make_histogram, make_metrics_data, make_sum, ) def _get_first_metric(result): return result.resource_metrics[0].scope_metrics[0].metrics[0] class TestOTLPMetricsEncoder(unittest.TestCase): def test_encode_sum(self): cases = [ ("int_value", 33, True), ("double_value", 2.98, False), ] for name, value, is_int in cases: with self.subTest(name=name): result = encode_metrics(make_metrics_data([make_sum(value=value)])) encoded = _get_first_metric(result) self.assertIsNotNone(encoded.sum) self.assertIsNone(encoded.gauge) self.assertEqual( encoded.sum.aggregation_temporality, AggregationTemporality.CUMULATIVE, ) self.assertTrue(encoded.sum.is_monotonic) dp = encoded.sum.data_points[0] self.assertIsNotNone(dp.start_time_unix_nano) self.assertIsNotNone(dp.time_unix_nano) if is_int: self.assertEqual(dp.as_int, value) self.assertIsNone(dp.as_double) else: self.assertEqual(dp.as_double, value) self.assertIsNone(dp.as_int) def test_encode_gauge(self): cases = [ ("int_value", 9000, True), ("double_value", 52.028, False), ] for name, value, is_int in cases: with self.subTest(name=name): result = encode_metrics(make_metrics_data([make_gauge(value=value)])) encoded = _get_first_metric(result) self.assertIsNotNone(encoded.gauge) self.assertIsNone(encoded.sum) dp = encoded.gauge.data_points[0] self.assertIsNotNone(dp.time_unix_nano) if is_int: self.assertEqual(dp.as_int, value) else: self.assertEqual(dp.as_double, value) def test_encode_histogram(self): metric = make_histogram( exemplars=[ Exemplar({"filtered": "banana"}, 298.0, TIME, SPAN_ID, TRACE_ID), ], ) result = encode_metrics(make_metrics_data([metric])) encoded = _get_first_metric(result) self.assertIsNotNone(encoded.histogram) self.assertEqual( encoded.histogram.aggregation_temporality, AggregationTemporality.DELTA, ) dp = encoded.histogram.data_points[0] self.assertEqual(dp.count, 5) self.assertEqual(dp.sum, 67) self.assertEqual(dp.bucket_counts, [1, 4]) self.assertEqual(dp.explicit_bounds, [10.0, 20.0]) self.assertEqual(dp.min, 8) self.assertEqual(dp.max, 18) self.assertEqual(len(dp.exemplars), 1) def test_encode_exponential_histogram(self): result = encode_metrics(make_metrics_data([make_exponential_histogram()])) encoded = _get_first_metric(result) self.assertIsNotNone(encoded.exponential_histogram) dp = encoded.exponential_histogram.data_points[0] self.assertEqual(dp.count, 10) self.assertEqual(dp.sum, 100.5) self.assertEqual(dp.scale, 1) self.assertEqual(dp.zero_count, 2) self.assertIsNotNone(dp.positive) self.assertEqual(dp.positive.offset, 0) self.assertEqual(dp.positive.bucket_counts, [1, 2, 3]) self.assertIsNotNone(dp.negative) self.assertEqual(dp.negative.offset, 1) self.assertEqual(dp.negative.bucket_counts, [1]) def test_encode_exponential_histogram_empty_buckets(self): metric = make_exponential_histogram( attributes={}, count=0, sum_value=0.0, zero_count=0, positive=Buckets(offset=0, bucket_counts=[]), negative=Buckets(offset=0, bucket_counts=[]), min_value=0.0, max_value=0.0, ) result = encode_metrics(make_metrics_data([metric])) dp = _get_first_metric(result).exponential_histogram.data_points[0] self.assertIsNone(dp.positive) self.assertIsNone(dp.negative) def test_encode_exemplars(self): cases = [ ( "float_with_ids", Exemplar({"f": "banana"}, 298.0, TIME, SPAN_ID, TRACE_ID), True, True, ), ( "float_without_ids", Exemplar({"f": "banana"}, 298.0, TIME, None, None), False, True, ), ( "int_with_ids", Exemplar({"f": "banana"}, 42, TIME, SPAN_ID, TRACE_ID), True, False, ), ] for name, exemplar, has_ids, is_float in cases: with self.subTest(name=name): metric = make_histogram( attributes={}, count=1, sum_value=exemplar.value, bucket_counts=[1], explicit_bounds=[], min_value=exemplar.value, max_value=exemplar.value, exemplars=[exemplar], ) result = encode_metrics(make_metrics_data([metric])) enc_ex = _get_first_metric(result).histogram.data_points[0].exemplars[0] if has_ids: self.assertTrue(enc_ex.span_id) self.assertTrue(enc_ex.trace_id) else: self.assertFalse(enc_ex.span_id) self.assertFalse(enc_ex.trace_id) if is_float: self.assertEqual(enc_ex.as_double, exemplar.value) else: self.assertEqual(enc_ex.as_int, exemplar.value) def test_encode_metrics_resource_and_scope(self): result = encode_metrics( make_metrics_data( [make_gauge(value=1, attributes={})], resource_attrs={"service.name": "test-svc"}, resource_schema_url="res_schema", scope_name="my_lib", scope_version="2.0", scope_schema_url="scope_schema", ) ) rm = result.resource_metrics[0] self.assertEqual(rm.schema_url, "res_schema") self.assertEqual(rm.resource.attributes[0].key, "service.name") sm = rm.scope_metrics[0] self.assertEqual(sm.schema_url, "scope_schema") self.assertEqual(sm.scope.name, "my_lib") self.assertEqual(sm.scope.version, "2.0") def test_encode_metrics_to_dict(self): result = encode_metrics(make_metrics_data([make_sum(name="sum_int", value=33)])) result_dict = result.to_dict() self.assertIn("resourceMetrics", result_dict) rm = result_dict["resourceMetrics"][0] self.assertIn("scopeMetrics", rm) dp = rm["scopeMetrics"][0]["metrics"][0]["sum"]["dataPoints"][0] self.assertIsInstance(dp["startTimeUnixNano"], str) self.assertIsInstance(dp["timeUnixNano"], str) self.assertIsInstance(dp["asInt"], str) self.assertEqual(dp["asInt"], "33") def test_encode_metrics_json_roundtrip(self): metrics = [ make_sum(name="s", value=33), make_gauge(name="g", value=52.028), make_histogram( name="h", exemplars=[ Exemplar({"f": "b"}, 298.0, TIME, SPAN_ID, TRACE_ID), ], ), make_exponential_histogram(name="eh"), ] result = encode_metrics(make_metrics_data(metrics)) json_str = result.to_json() roundtripped = JSONExportMetricsServiceRequest.from_dict(json.loads(json_str)) assert_proto_json_equal(self, result, roundtripped) def test_unsupported_metric_type(self): metric = Metric( name="unsupported", description="foo", unit="s", data=None, ) with self.assertLogs(level=WARNING): result = encode_metrics(make_metrics_data([metric])) encoded = _get_first_metric(result) self.assertEqual(encoded.name, "unsupported") self.assertIsNone(encoded.gauge) self.assertIsNone(encoded.sum) self.assertIsNone(encoded.histogram) self.assertIsNone(encoded.exponential_histogram) test_metrics_split.py000066400000000000000000000540231527070070700352270ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-json-common/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # pylint: disable=protected-access,unsubscriptable-object import unittest from logging import WARNING from opentelemetry.exporter.otlp.json.common._internal.metrics_encoder import ( _METRIC_DATA_FIELDS, _get_metric_data_field_name, split_metrics_data, ) from opentelemetry.exporter.otlp.json.common.metrics_encoder import ( encode_metrics, ) from opentelemetry.proto_json.collector.metrics.v1.metrics_service import ( ExportMetricsServiceRequest as JSONExportMetricsServiceRequest, ) from opentelemetry.proto_json.metrics.v1.metrics import ( NumberDataPoint as JSONNumberDataPoint, ) from opentelemetry.sdk.metrics.export import ( AggregationTemporality, Buckets, ExponentialHistogram, ExponentialHistogramDataPoint, Gauge, Histogram, HistogramDataPoint, Metric, MetricsData, NumberDataPoint, ResourceMetrics, ScopeMetrics, Sum, ) from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.util.instrumentation import InstrumentationScope from . import ( START_TIME, TIME, assert_proto_json_equal, make_metrics_data, make_sum, ) def _get_first_metric(result: JSONExportMetricsServiceRequest) -> Metric: return result.resource_metrics[0].scope_metrics[0].metrics[0] def _number_dp(value: int) -> NumberDataPoint: return NumberDataPoint( attributes={"a": 1}, start_time_unix_nano=START_TIME, time_unix_nano=TIME, value=value, ) def _histogram_dp(count: int) -> HistogramDataPoint: return HistogramDataPoint( attributes={"a": 1}, start_time_unix_nano=START_TIME, time_unix_nano=TIME, count=count, sum=count, bucket_counts=[count], explicit_bounds=[], min=0, max=count, exemplars=[], ) def _exponential_histogram_dp(count: int) -> ExponentialHistogramDataPoint: return ExponentialHistogramDataPoint( attributes={"a": 1}, start_time_unix_nano=START_TIME, time_unix_nano=TIME, count=count, sum=float(count), scale=1, zero_count=0, positive=Buckets(offset=0, bucket_counts=[count]), negative=Buckets(offset=0, bucket_counts=[]), flags=0, min=0.0, max=float(count), exemplars=[], ) def _metric_of_type(field_name: str, values: list[int], name: str | None = None) -> Metric: match field_name: case "gauge": data = Gauge(data_points=[_number_dp(v) for v in values]) case "sum": data = Sum( data_points=[_number_dp(v) for v in values], aggregation_temporality=AggregationTemporality.CUMULATIVE, is_monotonic=True, ) case "histogram": data = Histogram( data_points=[_histogram_dp(v) for v in values], aggregation_temporality=AggregationTemporality.DELTA, ) case _: # exponential_histogram data = ExponentialHistogram( data_points=[_exponential_histogram_dp(v) for v in values], aggregation_temporality=AggregationTemporality.CUMULATIVE, ) return Metric(name=name or field_name, description="desc", unit="u", data=data) def _scope_metrics(name: str, metrics: list[Metric]) -> ScopeMetrics: return ScopeMetrics( scope=InstrumentationScope(name=name, version="1.0", schema_url="scope_url"), metrics=metrics, schema_url="scope_url", ) def _resource_metrics(index: int, scope_metrics_list: list[ScopeMetrics]) -> ResourceMetrics: return ResourceMetrics( resource=Resource(attributes={"r": index}, schema_url=f"res_url_{index}"), scope_metrics=scope_metrics_list, schema_url=f"res_url_{index}", ) def _data_point_value(data_point: JSONNumberDataPoint) -> int | float: return data_point.as_int if data_point.as_int is not None else data_point.as_double def _data_point_values( request: JSONExportMetricsServiceRequest, ) -> list[int | float]: values = [] for resource_metrics in request.resource_metrics: for scope_metrics in resource_metrics.scope_metrics: for metric in scope_metrics.metrics: field_name = _get_metric_data_field_name(metric) for dp in getattr(metric, field_name).data_points: values.append(_data_point_value(dp)) return values def _count_data_points(request: JSONExportMetricsServiceRequest) -> int: return sum( len(getattr(metric, _get_metric_data_field_name(metric)).data_points) for resource_metrics in request.resource_metrics for scope_metrics in resource_metrics.scope_metrics for metric in scope_metrics.metrics ) def _flatten_with_context( request: JSONExportMetricsServiceRequest, ) -> dict[int, dict]: context = {} for resource_metrics in request.resource_metrics: for scope_metrics in resource_metrics.scope_metrics: for metric in scope_metrics.metrics: field_name = _get_metric_data_field_name(metric) for dp in getattr(metric, field_name).data_points: context[id(dp)] = { "data_point": dp, "resource": resource_metrics.resource, "resource_schema_url": resource_metrics.schema_url, "scope": scope_metrics.scope, "scope_schema_url": scope_metrics.schema_url, "metric_name": metric.name, "metric_unit": metric.unit, "metric_description": metric.description, "field_name": field_name, } return context def _assert_no_empty_metrics( batch: JSONExportMetricsServiceRequest, ) -> None: for resource_metrics in batch.resource_metrics: assert resource_metrics.scope_metrics, "empty scope_metrics list" for scope_metrics in resource_metrics.scope_metrics: assert scope_metrics.metrics, "empty metrics list" for metric in scope_metrics.metrics: field_name = _get_metric_data_field_name(metric) data_points = getattr(metric, field_name).data_points assert data_points, f"metric {metric.name!r} has no data points" class TestSplitMetricsData(unittest.TestCase): def test_none_batch_size_yields_original(self): request = encode_metrics(make_metrics_data([make_sum(value=1)])) batches = list(split_metrics_data(request, None)) self.assertEqual(len(batches), 1) self.assertIs(batches[0], request) def test_split_batch_sizes(self): cases = [ ("even", [0, 1, 2, 3], 2, [[0, 1], [2, 3]]), ("uneven", [0, 1, 2, 3, 4], 2, [[0, 1], [2, 3], [4]]), ("batch_size_one", [7, 8, 9], 1, [[7], [8], [9]]), ("batch_larger_than_total", [0, 1, 2], 100, [[0, 1, 2]]), ] for label, values, batch_size, expected_batches in cases: with self.subTest(case=label): request = encode_metrics(make_metrics_data([_metric_of_type("sum", values, "s")])) batches = list(split_metrics_data(request, batch_size)) self.assertEqual(len(batches), len(expected_batches)) self.assertEqual( [_data_point_values(b) for b in batches], expected_batches, ) def test_split_preserves_metadata(self): request = encode_metrics(make_metrics_data([_metric_of_type("sum", [0, 1, 2, 3, 4], "s")])) for batch in split_metrics_data(request, 2): metric = _get_first_metric(batch) self.assertEqual(metric.name, "s") self.assertEqual(metric.unit, "u") self.assertEqual(metric.description, "desc") self.assertIsNotNone(metric.sum) self.assertEqual( metric.sum.aggregation_temporality, AggregationTemporality.CUMULATIVE, ) self.assertTrue(metric.sum.is_monotonic) def test_split_with_empty_metric(self): empty_metric = Metric( name="empty", description="d", unit="u", data=Sum( data_points=[], aggregation_temporality=AggregationTemporality.CUMULATIVE, is_monotonic=True, ), ) request = encode_metrics( make_metrics_data( [ _metric_of_type("sum", [0, 1], "a"), empty_metric, _metric_of_type("sum", [2, 3], "b"), ] ) ) batches = list(split_metrics_data(request, 100)) self.assertEqual(len(batches), 1) _assert_no_empty_metrics(batches[0]) names = [m.name for rm in batches[0].resource_metrics for sm in rm.scope_metrics for m in sm.metrics] self.assertEqual(names, ["a", "b"]) self.assertEqual(_data_point_values(batches[0]), [0, 1, 2, 3]) def test_split_across_metrics(self): request = encode_metrics( make_metrics_data( [ _metric_of_type("sum", [0, 1], "a"), _metric_of_type("sum", [2, 3], "b"), ] ) ) batches = list(split_metrics_data(request, 3)) self.assertEqual(len(batches), 2) first_metrics = batches[0].resource_metrics[0].scope_metrics[0].metrics self.assertEqual([m.name for m in first_metrics], ["a", "b"]) self.assertEqual(_data_point_values(batches[0]), [0, 1, 2]) second_metrics = batches[1].resource_metrics[0].scope_metrics[0].metrics self.assertEqual([m.name for m in second_metrics], ["b"]) self.assertEqual(_data_point_values(batches[1]), [3]) def test_split_across_scopes(self): request = encode_metrics( MetricsData( resource_metrics=[ _resource_metrics( 0, [ _scope_metrics( "s0", [_metric_of_type("sum", [0, 1], "a")], ), _scope_metrics( "s1", [_metric_of_type("sum", [2, 3], "b")], ), ], ) ] ) ) batches = list(split_metrics_data(request, 3)) self.assertEqual(len(batches), 2) self.assertEqual(len(batches[0].resource_metrics[0].scope_metrics), 2) self.assertEqual(_data_point_values(batches[0]), [0, 1, 2]) self.assertEqual(len(batches[1].resource_metrics[0].scope_metrics), 1) self.assertEqual(batches[1].resource_metrics[0].scope_metrics[0].scope.name, "s1") self.assertEqual(_data_point_values(batches[1]), [3]) def test_split_across_resources(self): request = encode_metrics( MetricsData( resource_metrics=[ _resource_metrics( 0, [ _scope_metrics( "s0", [_metric_of_type("sum", [0, 1], "a")], ) ], ), _resource_metrics( 1, [ _scope_metrics( "s1", [_metric_of_type("sum", [2, 3], "b")], ) ], ), ] ) ) batches = list(split_metrics_data(request, 3)) self.assertEqual(len(batches), 2) self.assertEqual(len(batches[0].resource_metrics), 2) self.assertEqual(_data_point_values(batches[0]), [0, 1, 2]) self.assertEqual(len(batches[1].resource_metrics), 1) self.assertEqual(_data_point_values(batches[1]), [3]) def test_split_all_metric_types(self): for field_name in _METRIC_DATA_FIELDS: # Exclude "summary" metrics (not generated by the SDK) if field_name == "summary": continue with self.subTest(field_name=field_name): request = encode_metrics(make_metrics_data([_metric_of_type(field_name, [1, 2, 3])])) batches = list(split_metrics_data(request, 2)) self.assertEqual(len(batches), 2) self.assertEqual(_count_data_points(batches[0]), 2) self.assertEqual(_count_data_points(batches[1]), 1) for batch in batches: metric = _get_first_metric(batch) self.assertEqual(_get_metric_data_field_name(metric), field_name) def test_split_preserves_data_points(self): request = encode_metrics( MetricsData( resource_metrics=[ _resource_metrics( 0, [ _scope_metrics( "s0", [ _metric_of_type("sum", [0, 1, 2], "a"), _metric_of_type("sum", [3, 4], "b"), ], ), _scope_metrics( "s1", [_metric_of_type("sum", [5, 6, 7], "c")], ), ], ), _resource_metrics( 1, [ _scope_metrics( "s2", [_metric_of_type("sum", [8, 9], "d")], ) ], ), ] ) ) expected = _data_point_values(request) self.assertEqual(expected, list(range(10))) for batch_size in (1, 2, 3, 4, 7, 1000): with self.subTest(batch_size=batch_size): batches = list(split_metrics_data(request, batch_size)) flattened = [v for b in batches for v in _data_point_values(b)] self.assertEqual(flattened, expected) self.assertEqual(sum(_count_data_points(b) for b in batches), len(expected)) for batch in batches: self.assertLessEqual(_count_data_points(batch), batch_size) _assert_no_empty_metrics(batch) def test_split_preserves_hierarchy_and_attributes(self): cases = [ ( "single_metric", encode_metrics(make_metrics_data([_metric_of_type("sum", [0, 1, 2, 3, 4], "only")])), 5, ), ( "multi_scope_same_resource", encode_metrics( MetricsData( resource_metrics=[ _resource_metrics( 0, [ _scope_metrics( "s0", [_metric_of_type("gauge", [1, 2, 3])], ), _scope_metrics( "s1", [_metric_of_type("histogram", [1, 2])], ), ], ) ] ) ), 5, ), ( "multi_resource_mixed_types", encode_metrics( MetricsData( resource_metrics=[ _resource_metrics( 0, [ _scope_metrics( "s0", [ _metric_of_type("sum", [0, 1, 2], "a"), _metric_of_type("histogram", [1, 2]), ], ), _scope_metrics( "s1", [_metric_of_type("gauge", [1, 2, 3])], ), ], ), _resource_metrics( 1, [ _scope_metrics( "s2", [ _metric_of_type( "exponential_histogram", [1, 2], ) ], ) ], ), ] ) ), 10, ), ( "empty_metric_interspersed", encode_metrics( make_metrics_data( [ _metric_of_type("sum", [0, 1], "a"), Metric( name="empty", description="d", unit="u", data=Sum( data_points=[], aggregation_temporality=(AggregationTemporality.CUMULATIVE), is_monotonic=True, ), ), _metric_of_type("sum", [2, 3], "b"), ] ) ), 4, ), ] for label, request, expected_count in cases: with self.subTest(case=label): ground_truth = _flatten_with_context(request) self.assertEqual(len(ground_truth), expected_count) for batch_size in (1, 2, 3, 4, 7, 1000): with self.subTest(case=label, batch_size=batch_size): seen: dict[int, dict] = {} for batch in split_metrics_data(request, batch_size): _assert_no_empty_metrics(batch) batch_context = _flatten_with_context(batch) for dp_id in batch_context: self.assertNotIn( dp_id, seen, "data point yielded in more than one batch", ) seen.update(batch_context) self.assertEqual(set(seen), set(ground_truth)) for dp_id, ctx in seen.items(): expected = ground_truth[dp_id] self.assertIs(ctx["data_point"], expected["data_point"]) assert_proto_json_equal(self, ctx["resource"], expected["resource"]) self.assertEqual( ctx["resource_schema_url"], expected["resource_schema_url"], ) assert_proto_json_equal(self, ctx["scope"], expected["scope"]) self.assertEqual( ctx["scope_schema_url"], expected["scope_schema_url"], ) self.assertEqual(ctx["metric_name"], expected["metric_name"]) self.assertEqual(ctx["metric_unit"], expected["metric_unit"]) self.assertEqual( ctx["metric_description"], expected["metric_description"], ) self.assertEqual(ctx["field_name"], expected["field_name"]) def test_split_skips_unsupported_metric(self): request = encode_metrics( make_metrics_data( [ _metric_of_type("sum", [0, 1], "good"), Metric(name="bad", description="d", unit="u", data=None), ] ) ) with self.assertLogs(level=WARNING) as log_ctx: batches = list(split_metrics_data(request, 5)) self.assertTrue(any("unsupported metric type" in m for m in log_ctx.output)) self.assertEqual(len(batches), 1) self.assertEqual(_data_point_values(batches[0]), [0, 1]) names = [m.name for b in batches for rm in b.resource_metrics for sm in rm.scope_metrics for m in sm.metrics] self.assertEqual(names, ["good"]) def test_split_empty_request(self): request = JSONExportMetricsServiceRequest(resource_metrics=[]) self.assertEqual(list(split_metrics_data(request, 5)), []) def test_split_with_no_data_points(self): metric = Metric( name="empty", description="d", unit="u", data=Sum( data_points=[], aggregation_temporality=AggregationTemporality.CUMULATIVE, is_monotonic=True, ), ) request = encode_metrics(make_metrics_data([metric])) self.assertEqual(list(split_metrics_data(request, 5)), []) test_proto_json_compatibility.py000066400000000000000000000226101527070070700374700ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-json-common/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 """Tests verifying compatibility between opentelemetry-exporter-otlp-json-common and protobuf's built-in JSON serialization (MessageToDict / ParseDict). OTLP JSON deviations from standard proto3 JSON mapping: - traceId/spanId/parentSpanId: hex-encoded (not base64) - Enum fields: integer values only - Keys: lowerCamelCase - 64-bit integers: string-encoded """ import base64 import unittest from typing import Any from google.protobuf.json_format import MessageToDict, ParseDict from opentelemetry._logs import SeverityNumber from opentelemetry.exporter.otlp.json.common._log_encoder import ( encode_logs as json_encode_logs, ) from opentelemetry.exporter.otlp.json.common.metrics_encoder import ( encode_metrics as json_encode_metrics, ) from opentelemetry.exporter.otlp.json.common.trace_encoder import ( encode_spans as json_encode_spans, ) from opentelemetry.exporter.otlp.proto.common._log_encoder import ( encode_logs as proto_encode_logs, ) from opentelemetry.exporter.otlp.proto.common.metrics_encoder import ( encode_metrics as proto_encode_metrics, ) from opentelemetry.exporter.otlp.proto.common.trace_encoder import ( encode_spans as proto_encode_spans, ) from opentelemetry.proto.collector.logs.v1.logs_service_pb2 import ( ExportLogsServiceRequest as PB2ExportLogsServiceRequest, ) from opentelemetry.proto.collector.metrics.v1.metrics_service_pb2 import ( ExportMetricsServiceRequest as PB2ExportMetricsServiceRequest, ) from opentelemetry.proto.collector.trace.v1.trace_service_pb2 import ( ExportTraceServiceRequest as PB2ExportTraceServiceRequest, ) from opentelemetry.sdk.metrics import Exemplar from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.trace import Event, SpanContext from opentelemetry.sdk.util.instrumentation import InstrumentationScope from opentelemetry.trace import Link from tests import ( BASE_TIME, PARENT_SPAN_ID, SPAN_ID, TIME, TRACE_ID, make_exponential_histogram, make_gauge, make_histogram, make_log, make_log_context, make_metrics_data, make_span, make_sum, ) _HEX_ID_KEYS = frozenset({"traceId", "spanId", "parentSpanId"}) def _normalize_otlp_json(data: Any) -> Any: """Convert proto3 standard JSON (base64 IDs) to OTLP JSON (hex IDs).""" if isinstance(data, list): return [_normalize_otlp_json(item) for item in data] if isinstance(data, dict): return { key: ( base64.b64decode(value).hex() if key in _HEX_ID_KEYS and isinstance(value, str) else _normalize_otlp_json(value) ) for key, value in data.items() } return data def _denormalize_otlp_json(data: Any) -> Any: """Convert OTLP JSON (hex IDs) back to proto3 standard JSON (base64 IDs).""" if isinstance(data, list): return [_denormalize_otlp_json(item) for item in data] if isinstance(data, dict): return { key: ( base64.b64encode(bytes.fromhex(value)).decode("utf-8") if key in _HEX_ID_KEYS and isinstance(value, str) else _denormalize_otlp_json(value) ) for key, value in data.items() } return data def _make_spans(): parent_ctx = SpanContext(TRACE_ID, PARENT_SPAN_ID, is_remote=True) link_ctx = SpanContext(TRACE_ID, 0x2222222222222222, is_remote=False) span1 = make_span( name="span-with-everything", parent=parent_ctx, events=( Event( name="event1", timestamp=BASE_TIME + 10 * 10**6, attributes={"event_key": "event_val", "event_int": 42}, ), ), links=(Link(context=link_ctx, attributes={"link_bool": True}),), resource=Resource( {"service.name": "test-service"}, schema_url="resource_schema", ), instrumentation_scope=InstrumentationScope( name="test_lib", version="1.0.0", schema_url="scope_schema", ), ) span2 = make_span( name="bare-span", span_id=0xBBBBBBBBBBBBBBBB, parent=None, resource=Resource({"service.name": "test-service"}), start_time=BASE_TIME + 100 * 10**6, end_time=BASE_TIME + 200 * 10**6, ) return [span1, span2] def _make_test_metrics_data(): return make_metrics_data( [ make_sum( name="counter", value=100, attributes={"method": "GET"}, description="a counter", unit="1", ), make_gauge( name="gauge_float", value=52.5, attributes={"host": "localhost"}, description="a gauge", unit="ms", ), make_histogram( name="histogram", attributes={"path": "/api"}, count=10, sum_value=500.0, bucket_counts=[2, 5, 3], explicit_bounds=[100.0, 250.0], min_value=10.0, max_value=450.0, exemplars=[ Exemplar( {"sampled": "true"}, 298.0, TIME, SPAN_ID, TRACE_ID, ), ], description="a histogram", unit="ms", ), make_exponential_histogram( name="exp_histogram", attributes={}, count=8, sum_value=80.0, scale=2, zero_count=1, min_value=1.0, max_value=30.0, description="exponential", ), ], resource_attrs={"service.name": "metrics-svc"}, resource_schema_url="resource_schema", scope_name="metrics_lib", scope_version="2.0", scope_schema_url="scope_schema", ) def _make_logs(): ctx = make_log_context() return [ make_log( body="something went wrong", severity_text="ERROR", severity_number=SeverityNumber.ERROR, attributes={"error.code": 500, "path": "/api"}, context=ctx, resource=Resource({"service.name": "log-svc"}, "resource_schema"), instrumentation_scope=InstrumentationScope("log_lib", "1.0", "scope_schema"), ), make_log( body="healthy", timestamp=TIME + 2000, observed_timestamp=TIME + 3000, resource=Resource({"service.name": "log-svc"}), instrumentation_scope=InstrumentationScope("log_lib", "1.0"), ), ] class TestProtoJsonDictCompatibility(unittest.TestCase): """Compare .to_dict() from JSON encoder with MessageToDict from proto encoder (after normalizing IDs).""" def test_trace_dict_compatibility(self): spans = _make_spans() json_dict = json_encode_spans(spans).to_dict() proto_dict = MessageToDict( proto_encode_spans(spans), preserving_proto_field_name=False, use_integers_for_enums=True, ) self.assertEqual(json_dict, _normalize_otlp_json(proto_dict)) def test_metrics_dict_compatibility(self): data = _make_test_metrics_data() json_dict = json_encode_metrics(data).to_dict() proto_dict = MessageToDict( proto_encode_metrics(data), preserving_proto_field_name=False, use_integers_for_enums=True, ) self.assertEqual(json_dict, _normalize_otlp_json(proto_dict)) def test_log_dict_compatibility(self): logs = _make_logs() json_dict = json_encode_logs(logs).to_dict() proto_dict = MessageToDict( proto_encode_logs(logs), preserving_proto_field_name=False, use_integers_for_enums=True, ) self.assertEqual(json_dict, _normalize_otlp_json(proto_dict)) class TestProtoJsonParseCompatibility(unittest.TestCase): """Encode SDK objects to dict via JSON encoder, denormalize (hex→base64 IDs), parse into protobuf via ParseDict, compare with proto encoder.""" def test_trace_parse_compatibility(self): spans = _make_spans() json_dict = json_encode_spans(spans).to_dict() proto_expected = proto_encode_spans(spans) denormalized = _denormalize_otlp_json(json_dict) proto_parsed = ParseDict(denormalized, PB2ExportTraceServiceRequest()) self.assertEqual(proto_parsed, proto_expected) def test_metrics_parse_compatibility(self): data = _make_test_metrics_data() json_dict = json_encode_metrics(data).to_dict() proto_expected = proto_encode_metrics(data) denormalized = _denormalize_otlp_json(json_dict) proto_parsed = ParseDict(denormalized, PB2ExportMetricsServiceRequest()) self.assertEqual(proto_parsed, proto_expected) def test_log_parse_compatibility(self): logs = _make_logs() json_dict = json_encode_logs(logs).to_dict() proto_expected = proto_encode_logs(logs) denormalized = _denormalize_otlp_json(json_dict) proto_parsed = ParseDict(denormalized, PB2ExportLogsServiceRequest()) self.assertEqual(proto_parsed, proto_expected) test_trace_encoder.py000066400000000000000000000313761527070070700351510ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-json-common/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # pylint: disable=protected-access,unsubscriptable-object import json import unittest from opentelemetry.exporter.otlp.json.common._internal import ( _encode_span_id, _encode_trace_id, ) from opentelemetry.exporter.otlp.json.common._internal.trace_encoder import ( _SPAN_KIND_MAP, _encode_context_span_id, _encode_events, _encode_links, _encode_status, _encode_trace_state, _span_flags, ) from opentelemetry.exporter.otlp.json.common.trace_encoder import encode_spans from opentelemetry.proto_json.collector.trace.v1.trace_service import ( ExportTraceServiceRequest as JSONExportTraceServiceRequest, ) from opentelemetry.proto_json.common.v1.common import AnyValue as JSONAnyValue from opentelemetry.proto_json.trace.v1.trace import Span as JSONSpan from opentelemetry.proto_json.trace.v1.trace import ( SpanFlags as JSONSpanFlags, ) from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.trace import Event, SpanContext from opentelemetry.sdk.util.instrumentation import InstrumentationScope from opentelemetry.trace import Link, SpanKind from opentelemetry.trace.span import TraceState from opentelemetry.trace.status import Status, StatusCode from tests import ( BASE_TIME, PARENT_SPAN_ID, SPAN_ID, TRACE_ID, assert_proto_json_equal, make_span, make_span_unended, ) def _get_span(result, rs_idx=0, ss_idx=0, s_idx=0): return result.resource_spans[rs_idx].scope_spans[ss_idx].spans[s_idx] # pylint: disable=too-many-public-methods class TestOTLPTraceEncoder(unittest.TestCase): def test_encode_single_span(self): span = make_span() result = encode_spans([span]) self.assertEqual(len(result.resource_spans), 1) self.assertEqual(len(result.resource_spans[0].scope_spans), 1) self.assertEqual(len(result.resource_spans[0].scope_spans[0].spans), 1) encoded = _get_span(result) self.assertEqual(encoded.name, "test-span") self.assertEqual(encoded.kind, JSONSpan.SpanKind.SPAN_KIND_INTERNAL) self.assertEqual(encoded.start_time_unix_nano, BASE_TIME) self.assertEqual(encoded.end_time_unix_nano, BASE_TIME + 50 * 10**6) self.assertEqual(encoded.trace_id, _encode_trace_id(TRACE_ID)) self.assertEqual(encoded.span_id, _encode_span_id(SPAN_ID)) def test_encode_span_attributes(self): span = make_span() span._attributes = {"key_bool": False, "key_str": "hello"} result = encode_spans([span]) encoded = _get_span(result) attr_dict = {kv.key: kv.value for kv in encoded.attributes} self.assertEqual(attr_dict["key_bool"], JSONAnyValue(bool_value=False)) self.assertEqual(attr_dict["key_str"], JSONAnyValue(string_value="hello")) def test_encode_span_events(self): event = Event( name="my-event", timestamp=BASE_TIME + 10 * 10**6, attributes={"event_key": "event_val"}, ) span = make_span(events=(event,)) result = encode_spans([span]) encoded = _get_span(result) self.assertEqual(len(encoded.events), 1) self.assertEqual(encoded.events[0].name, "my-event") self.assertEqual(encoded.events[0].time_unix_nano, BASE_TIME + 10 * 10**6) self.assertEqual(encoded.events[0].attributes[0].key, "event_key") def test_encode_span_links(self): link_ctx = SpanContext(TRACE_ID, 0x2222222222222222, is_remote=False) link = Link(context=link_ctx, attributes={"link_key": True}) span = make_span(links=(link,)) result = encode_spans([span]) encoded = _get_span(result) self.assertEqual(len(encoded.links), 1) self.assertEqual(encoded.links[0].trace_id, _encode_trace_id(TRACE_ID)) self.assertEqual( encoded.links[0].span_id, _encode_span_id(0x2222222222222222), ) self.assertEqual(encoded.links[0].attributes[0].key, "link_key") self.assertEqual( encoded.links[0].flags, int(JSONSpanFlags.SPAN_FLAGS_CONTEXT_HAS_IS_REMOTE_MASK), ) def test_encode_span_status(self): cases = [ ("unset", StatusCode.UNSET, None, 0), ("ok", StatusCode.OK, None, 1), ("error", StatusCode.ERROR, "Something broke", 2), ] for name, code, description, expected_code in cases: with self.subTest(name=name): span = make_span_unended() span.set_status(Status(code, description)) span.end(end_time=BASE_TIME + 50 * 10**6) result = encode_spans([span]) encoded = _get_span(result) self.assertEqual(int(encoded.status.code), expected_code) if description: self.assertEqual(encoded.status.message, description) def test_encode_span_parent(self): parent_ctx = SpanContext(TRACE_ID, PARENT_SPAN_ID, is_remote=True) span = make_span(parent=parent_ctx) result = encode_spans([span]) encoded = _get_span(result) self.assertEqual( encoded.parent_span_id, _encode_span_id(PARENT_SPAN_ID), ) self.assertEqual( encoded.flags, int(JSONSpanFlags.SPAN_FLAGS_CONTEXT_HAS_IS_REMOTE_MASK | JSONSpanFlags.SPAN_FLAGS_CONTEXT_IS_REMOTE_MASK), ) def test_encode_span_no_parent(self): span = make_span(parent=None) result = encode_spans([span]) encoded = _get_span(result) self.assertIsNone(encoded.parent_span_id) self.assertEqual( encoded.flags, int(JSONSpanFlags.SPAN_FLAGS_CONTEXT_HAS_IS_REMOTE_MASK), ) def test_encode_span_grouping_by_resource(self): r1 = Resource({"svc": "svc1"}) r2 = Resource({"svc": "svc2"}) span1 = make_span(name="s1", resource=r1, span_id=0xAAAA) span2 = make_span(name="s2", resource=r2, span_id=0xBBBB) result = encode_spans([span1, span2]) self.assertEqual(len(result.resource_spans), 2) groups = {} # pylint: disable-next=not-an-iterable for rs in result.resource_spans: svc_val = rs.resource.attributes[0].value.string_value span_names = [s.name for ss in rs.scope_spans for s in ss.spans] groups[svc_val] = span_names self.assertEqual(groups["svc1"], ["s1"]) self.assertEqual(groups["svc2"], ["s2"]) def test_encode_span_grouping_by_scope(self): resource = Resource({"svc": "test"}) scope1 = InstrumentationScope(name="lib1", version="1.0") scope2 = InstrumentationScope(name="lib2", version="2.0") span1 = make_span( name="s1", resource=resource, instrumentation_scope=scope1, span_id=0xAAAA, ) span2 = make_span( name="s2", resource=resource, instrumentation_scope=scope2, span_id=0xBBBB, ) result = encode_spans([span1, span2]) self.assertEqual(len(result.resource_spans), 1) scope_spans = result.resource_spans[0].scope_spans self.assertEqual(len(scope_spans), 2) groups = {ss.scope.name: [s.name for s in ss.spans] for ss in scope_spans} self.assertEqual(groups["lib1"], ["s1"]) self.assertEqual(groups["lib2"], ["s2"]) self.assertEqual(scope_spans[0].scope.version, "1.0") self.assertEqual(scope_spans[1].scope.version, "2.0") def test_encode_span_schema_urls(self): resource = Resource({"key": "val"}, schema_url="resource_schema") scope = InstrumentationScope( name="lib", version="1.0", schema_url="scope_schema", ) span = make_span(resource=resource, instrumentation_scope=scope) result = encode_spans([span]) self.assertEqual(result.resource_spans[0].schema_url, "resource_schema") self.assertEqual( result.resource_spans[0].scope_spans[0].schema_url, "scope_schema", ) def test_encode_span_scope_attributes(self): scope = InstrumentationScope( name="lib", version="1.0", attributes={"scope_key": "scope_val"}, ) span = make_span(instrumentation_scope=scope) result = encode_spans([span]) encoded_scope = result.resource_spans[0].scope_spans[0].scope self.assertEqual(encoded_scope.name, "lib") self.assertEqual(encoded_scope.version, "1.0") self.assertEqual(len(encoded_scope.attributes), 1) self.assertEqual(encoded_scope.attributes[0].key, "scope_key") def test_span_kind_map(self): expected = [ (SpanKind.INTERNAL, JSONSpan.SpanKind.SPAN_KIND_INTERNAL), (SpanKind.SERVER, JSONSpan.SpanKind.SPAN_KIND_SERVER), (SpanKind.CLIENT, JSONSpan.SpanKind.SPAN_KIND_CLIENT), (SpanKind.PRODUCER, JSONSpan.SpanKind.SPAN_KIND_PRODUCER), (SpanKind.CONSUMER, JSONSpan.SpanKind.SPAN_KIND_CONSUMER), ] for sdk_kind, json_kind in expected: with self.subTest(kind=sdk_kind.name): self.assertEqual(_SPAN_KIND_MAP[sdk_kind], json_kind) def test_span_flags(self): cases = [ ( "remote_parent", SpanContext(TRACE_ID, 0x1111, is_remote=True), int( JSONSpanFlags.SPAN_FLAGS_CONTEXT_HAS_IS_REMOTE_MASK | JSONSpanFlags.SPAN_FLAGS_CONTEXT_IS_REMOTE_MASK ), ), ( "local_parent", SpanContext(TRACE_ID, 0x2222, is_remote=False), int(JSONSpanFlags.SPAN_FLAGS_CONTEXT_HAS_IS_REMOTE_MASK), ), ( "no_parent", None, int(JSONSpanFlags.SPAN_FLAGS_CONTEXT_HAS_IS_REMOTE_MASK), ), ] for name, context, expected_flags in cases: with self.subTest(name=name): self.assertEqual(_span_flags(context), expected_flags) def test_encode_status_translations(self): cases = [ ("unset", StatusCode.UNSET, None), ("ok", StatusCode.OK, None), ("error", StatusCode.ERROR, "desc"), ] for name, code, desc in cases: with self.subTest(name=name): result = _encode_status(Status(code, desc)) self.assertEqual( int(result.code), code.value, ) if desc: self.assertEqual(result.message, desc) def test_encode_trace_state(self): ts = TraceState([("key1", "val1"), ("key2", "val2")]) self.assertEqual(_encode_trace_state(ts), "key1=val1,key2=val2") def test_encode_trace_state_none(self): self.assertIsNone(_encode_trace_state(None)) def test_encode_events_empty(self): self.assertEqual(_encode_events(()), []) self.assertEqual(_encode_events([]), []) def test_encode_links_empty(self): self.assertEqual(_encode_links(()), []) self.assertEqual(_encode_links([]), []) def test_encode_parent_id(self): ctx = SpanContext(TRACE_ID, 0xABCDEF1234567890, is_remote=True) self.assertEqual( _encode_context_span_id(ctx), _encode_span_id(0xABCDEF1234567890), ) def test_encode_parent_id_none(self): self.assertIsNone(_encode_context_span_id(None)) def test_encode_spans_to_dict(self): span = make_span() span.set_attribute("key", "value") result = encode_spans([span]) result_dict = result.to_dict() self.assertIn("resourceSpans", result_dict) sp = result_dict["resourceSpans"][0]["scopeSpans"][0]["spans"][0] self.assertIsInstance(sp["traceId"], str) self.assertEqual(len(sp["traceId"]), 32) self.assertIsInstance(sp["spanId"], str) self.assertEqual(len(sp["spanId"]), 16) self.assertIsInstance(sp["startTimeUnixNano"], str) self.assertIsInstance(sp["endTimeUnixNano"], str) def test_encode_spans_json_roundtrip(self): parent_ctx = SpanContext(TRACE_ID, PARENT_SPAN_ID, is_remote=True) spans = [ make_span(name="span1", parent=parent_ctx), make_span( name="span2", span_id=0xBBBB, resource=Resource({"r": "v"}), instrumentation_scope=InstrumentationScope("lib", "1.0", attributes={"sk": 1}), ), ] result = encode_spans(spans) json_str = result.to_json() roundtripped = JSONExportTraceServiceRequest.from_dict(json.loads(json_str)) assert_proto_json_equal(self, result, roundtripped) python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-json-file/000077500000000000000000000000001527070070700272755ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-json-file/LICENSE000066400000000000000000000261351527070070700303110ustar00rootroot00000000000000 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. python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-json-file/README.rst000066400000000000000000000016271527070070700307720ustar00rootroot00000000000000OpenTelemetry JSON File Exporter ================================ |pypi| .. |pypi| image:: https://badge.fury.io/py/opentelemetry-exporter-otlp-json-file.svg :target: https://pypi.org/project/opentelemetry-exporter-otlp-json-file/ This library exports telemetry as OTLP JSON to a file-like text stream, such as a file or stdout. The exporter writes newline delimited OTLP JSON records for file-based collection workflows. Installation ------------ :: pip install opentelemetry-exporter-otlp-json-file References ---------- * `OpenTelemetry Project `_ * `OpenTelemetry Protocol File Exporter `_ * `OTLP Specification `_ * `OTLP JSON Encoding Specification `_ * `JSON Lines `_ python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-json-file/pyproject.toml000066400000000000000000000035241527070070700322150ustar00rootroot00000000000000[build-system] requires = ["hatchling"] build-backend = "hatchling.build" [project] name = "opentelemetry-exporter-otlp-json-file" dynamic = ["version"] description = "OpenTelemetry JSON File Exporter" 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", "Framework :: OpenTelemetry", "Framework :: OpenTelemetry :: Exporters", "Intended Audience :: Developers", "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-exporter-otlp-common == 0.66b2", "opentelemetry-exporter-otlp-json-common == 0.66b2", "opentelemetry-sdk ~= 1.45.2", ] [project.entry-points.opentelemetry_traces_exporter] otlp_json_file = "opentelemetry.exporter.otlp.json.file.trace_exporter:FileSpanExporter" [project.entry-points.opentelemetry_metrics_exporter] otlp_json_file = "opentelemetry.exporter.otlp.json.file.metric_exporter:FileMetricExporter" [project.entry-points.opentelemetry_logs_exporter] otlp_json_file = "opentelemetry.exporter.otlp.json.file._log_exporter:FileLogExporter" [project.urls] Homepage = "https://github.com/open-telemetry/opentelemetry-python/tree/main/exporter/opentelemetry-exporter-otlp-json-file" Repository = "https://github.com/open-telemetry/opentelemetry-python" [tool.hatch.version] path = "src/opentelemetry/exporter/otlp/json/file/version/__init__.py" [tool.hatch.build.targets.sdist] include = [ "/src", "/tests", ] [tool.hatch.build.targets.wheel] packages = ["src/opentelemetry"] python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-json-file/src/000077500000000000000000000000001527070070700300645ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-json-file/src/opentelemetry/000077500000000000000000000000001527070070700327605ustar00rootroot00000000000000exporter/000077500000000000000000000000001527070070700345515ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-json-file/src/opentelemetryotlp/000077500000000000000000000000001527070070700355275ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-json-file/src/opentelemetry/exporterjson/000077500000000000000000000000001527070070700365005ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-json-file/src/opentelemetry/exporter/otlpfile/000077500000000000000000000000001527070070700374175ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-json-file/src/opentelemetry/exporter/otlp/json__init__.py000066400000000000000000000043231527070070700415320ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-json-file/src/opentelemetry/exporter/otlp/json/file# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 """ This library allows exporting OpenTelemetry telemetry to a file or stream in OTLP JSON (JSON Lines) format. Usage ----- The **OTLP JSON File Exporter** writes `OpenTelemetry`_ traces, metrics, and logs as compact JSON Lines to a file, an arbitrary ``IO[str]`` stream or ``sys.stdout``. Three exporters are provided: - :class:`~opentelemetry.exporter.otlp.json.file.trace_exporter.FileSpanExporter` - traces - :class:`~opentelemetry.exporter.otlp.json.file.metric_exporter.FileMetricExporter` - metrics - :class:`~opentelemetry.exporter.otlp.json.file._log_exporter.FileLogExporter` - logs Each exporter accepts a destination in one of three ways: - ``path`` - a file path opened in append mode (UTF-8) - ``stream`` - any ``IO[str]`` text stream - neither - defaults to ``sys.stdout`` .. _OpenTelemetry: https://github.com/open-telemetry/opentelemetry-python/ .. code:: python from opentelemetry import trace from opentelemetry.exporter.otlp.json.file import FileSpanExporter from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchSpanProcessor # Write traces to a file (append mode, UTF-8) exporter = FileSpanExporter("/var/log/otel/traces.jsonl") # Alternatively, write to an explicit stream: # exporter = FileSpanExporter(stream=open("traces.jsonl", "a")) # Or write to sys.stdout (default when no path or stream is given): # exporter = FileSpanExporter() trace.set_tracer_provider(TracerProvider()) trace.get_tracer_provider().add_span_processor( BatchSpanProcessor(exporter) ) tracer = trace.get_tracer(__name__) with tracer.start_as_current_span("foo"): print("Hello world!") The metric exporter supports the following environment variables: - :envvar:`OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE` - :envvar:`OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION` API --- """ from opentelemetry.exporter.otlp.json.file.metric_exporter import ( FileMetricExporter, ) from opentelemetry.exporter.otlp.json.file.trace_exporter import ( FileSpanExporter, ) __all__ = [ "FileMetricExporter", "FileSpanExporter", ] _internal.py000066400000000000000000000050521527070070700417460ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-json-file/src/opentelemetry/exporter/otlp/json/file# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import json import logging import sys import threading from collections.abc import Callable from os import PathLike from typing import IO, Generic, Literal, TypeVar _logger = logging.getLogger(__name__) T = TypeVar("T") class _FileExporter(Generic[T]): def __init__( self, encode: Callable[[T], dict | None], kind: Literal["spans", "logs", "metrics"], logger: logging.Logger | None = None, path: str | PathLike[str] | None = None, stream: IO[str] | None = None, ) -> None: if path is not None and stream is not None: raise ValueError("Cannot specify both 'path' and 'stream'") if path is not None: self._stream: IO[str] = open( # pylint: disable=consider-using-with path, "a", encoding="utf-8" ) self._owns_stream = True elif stream is not None: self._stream = stream self._owns_stream = False else: self._stream = sys.stdout self._owns_stream = False self._shutdown = False self._encode = encode self._kind = kind self._logger = logger if logger is not None else _logger self._lock = threading.Lock() def export(self, data: T) -> bool: if self._shutdown: self._logger.warning("Exporter already shutdown, ignoring call") return False try: encoded = self._encode(data) with self._lock: if self._stream.closed: self._logger.warning("Stream is closed, ignoring %s export call", self._kind) return False if encoded is not None: self._stream.write(_format_line(encoded)) self._stream.flush() # pylint: disable-next=broad-exception-caught except Exception as error: self._logger.exception( "Failed to write %s batch to stream: %s: %s", self._kind, type(error).__name__, error, ) return False return True def shutdown(self) -> None: if self._shutdown: self._logger.warning("Exporter already shutdown, ignoring call") return self._shutdown = True if self._owns_stream: with self._lock: self._stream.close() def _format_line(entry: dict) -> str: return json.dumps(entry, separators=(",", ":")) + "\n" _log_exporter.py000066400000000000000000000035451527070070700426500ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-json-file/src/opentelemetry/exporter/otlp/json/file# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import logging import os from collections.abc import Sequence from typing import IO, Any, overload from opentelemetry.exporter.otlp.json.common._log_encoder import encode_logs from opentelemetry.exporter.otlp.json.file._internal import _FileExporter from opentelemetry.sdk._logs import ReadableLogRecord from opentelemetry.sdk._logs.export import ( LogRecordExporter, LogRecordExportResult, ) from opentelemetry.sdk._shared_internal import DuplicateFilter _logger = logging.getLogger(__name__) _logger.addFilter(DuplicateFilter()) def _encode_logs_to_dict( batch: Sequence[ReadableLogRecord], ) -> dict[str, Any] | None: data = encode_logs(batch) return data.to_dict() if data.resource_logs else None class FileLogExporter(LogRecordExporter): @overload def __init__( self, path: str | os.PathLike[str], ) -> None: ... @overload def __init__( self, *, stream: IO[str], ) -> None: ... @overload def __init__( self, ) -> None: ... def __init__( self, path: str | os.PathLike[str] | None = None, *, stream: IO[str] | None = None, ) -> None: self._exporter: _FileExporter[Sequence[ReadableLogRecord]] = _FileExporter( encode=_encode_logs_to_dict, kind="logs", logger=_logger, path=path, stream=stream, ) def export(self, batch: Sequence[ReadableLogRecord]) -> LogRecordExportResult: return LogRecordExportResult.SUCCESS if self._exporter.export(batch) else LogRecordExportResult.FAILURE def shutdown(self) -> None: self._exporter.shutdown() # pylint: disable-next=no-self-use def force_flush(self, timeout_millis: int = 10_000) -> bool: return True metric_exporter.py000066400000000000000000000054161527070070700432120ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-json-file/src/opentelemetry/exporter/otlp/json/file# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import logging import os from typing import IO, Any, overload from opentelemetry.exporter.otlp.common._aggregation import ( _get_aggregation, _get_temporality, ) from opentelemetry.exporter.otlp.json.common.metrics_encoder import ( encode_metrics, ) from opentelemetry.exporter.otlp.json.file._internal import _FileExporter from opentelemetry.sdk.metrics.export import ( AggregationTemporality, MetricExporter, MetricExportResult, MetricsData, ) from opentelemetry.sdk.metrics.view import Aggregation _logger = logging.getLogger(__name__) def _encode_metrics_to_dict( metrics_data: MetricsData, ) -> dict[str, Any] | None: data = encode_metrics(metrics_data) return data.to_dict() if data.resource_metrics else None class FileMetricExporter(MetricExporter): @overload def __init__( self, path: str | os.PathLike[str], *, preferred_temporality: dict[type, AggregationTemporality] | None = None, preferred_aggregation: dict[type, Aggregation] | None = None, ) -> None: ... @overload def __init__( self, *, stream: IO[str], preferred_temporality: dict[type, AggregationTemporality] | None = None, preferred_aggregation: dict[type, Aggregation] | None = None, ) -> None: ... @overload def __init__( self, *, preferred_temporality: dict[type, AggregationTemporality] | None = None, preferred_aggregation: dict[type, Aggregation] | None = None, ) -> None: ... def __init__( self, path: str | os.PathLike[str] | None = None, *, stream: IO[str] | None = None, preferred_temporality: dict[type, AggregationTemporality] | None = None, preferred_aggregation: dict[type, Aggregation] | None = None, ) -> None: MetricExporter.__init__( self, preferred_temporality=_get_temporality(preferred_temporality), preferred_aggregation=_get_aggregation(preferred_aggregation), ) self._exporter: _FileExporter[MetricsData] = _FileExporter( encode=_encode_metrics_to_dict, kind="metrics", logger=_logger, path=path, stream=stream, ) def export( self, metrics_data: MetricsData, timeout_millis: float = 10_000, **kwargs, ) -> MetricExportResult: return MetricExportResult.SUCCESS if self._exporter.export(metrics_data) else MetricExportResult.FAILURE def shutdown(self, timeout_millis: float = 30_000, **kwargs) -> None: self._exporter.shutdown() def force_flush(self, timeout_millis: float = 10_000) -> bool: return True py.typed000066400000000000000000000000001527070070700411040ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-json-file/src/opentelemetry/exporter/otlp/json/filetrace_exporter.py000066400000000000000000000033141527070070700430200ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-json-file/src/opentelemetry/exporter/otlp/json/file# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import logging import os from collections.abc import Sequence from typing import IO, Any, overload from opentelemetry.exporter.otlp.json.common.trace_encoder import encode_spans from opentelemetry.exporter.otlp.json.file._internal import _FileExporter from opentelemetry.sdk.trace import ReadableSpan from opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult _logger = logging.getLogger(__name__) def _encode_spans_to_dict( spans: Sequence[ReadableSpan], ) -> dict[str, Any] | None: data = encode_spans(spans) return data.to_dict() if data.resource_spans else None class FileSpanExporter(SpanExporter): @overload def __init__( self, path: str | os.PathLike[str], ) -> None: ... @overload def __init__( self, *, stream: IO[str], ) -> None: ... @overload def __init__( self, ) -> None: ... def __init__( self, path: str | os.PathLike[str] | None = None, *, stream: IO[str] | None = None, ) -> None: self._exporter: _FileExporter[Sequence[ReadableSpan]] = _FileExporter( encode=_encode_spans_to_dict, kind="spans", logger=_logger, path=path, stream=stream, ) def export(self, spans: Sequence[ReadableSpan]) -> SpanExportResult: return SpanExportResult.SUCCESS if self._exporter.export(spans) else SpanExportResult.FAILURE def shutdown(self, timeout_millis: float = 30_000, **kwargs) -> None: self._exporter.shutdown() def force_flush(self, timeout_millis: int = 30000) -> bool: return True version/000077500000000000000000000000001527070070700411045ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-json-file/src/opentelemetry/exporter/otlp/json/file__init__.py000066400000000000000000000001441527070070700432140ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-json-file/src/opentelemetry/exporter/otlp/json/file/version# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 __version__ = "0.66b2" python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-json-file/test-requirements.txt000066400000000000000000000007361527070070700335440ustar00rootroot00000000000000asgiref==3.7.2 importlib-metadata==6.11.0 iniconfig==2.0.0 packaging==24.0 pluggy==1.6.0 py-cpuinfo==9.0.0 pytest==9.0.3 tomli==2.0.1 typing_extensions==4.10.0 wrapt==1.16.0 zipp==3.19.2 -e opentelemetry-api -e opentelemetry-sdk -e opentelemetry-semantic-conventions -e tests/opentelemetry-test-utils -e opentelemetry-proto-json -e exporter/opentelemetry-exporter-otlp-common -e exporter/opentelemetry-exporter-otlp-json-common -e exporter/opentelemetry-exporter-otlp-json-file python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-json-file/tests/000077500000000000000000000000001527070070700304375ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-json-file/tests/__init__.py000066400000000000000000000001141527070070700325440ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-json-file/tests/test_internal.py000066400000000000000000000015721527070070700336710ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import json import unittest from opentelemetry.exporter.otlp.json.file._internal import _format_line class TestFormatLine(unittest.TestCase): def test_produces_valid_json(self): result = _format_line({"a": 1, "b": "hello"}) parsed = json.loads(result.strip()) self.assertEqual(parsed, {"a": 1, "b": "hello"}) def test_newline_terminated(self): result = _format_line({"x": 0}) self.assertTrue(result.endswith("\n")) def test_compact_no_spaces(self): result = _format_line({"a": 1, "b": 2}) self.assertNotIn(" ", result) def test_nested_structure(self): entry = {"outer": {"inner": [1, 2, 3]}, "flag": True} result = _format_line(entry) parsed = json.loads(result.strip()) self.assertEqual(parsed, entry) test_log_exporter.py000066400000000000000000000162321527070070700345060ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-json-file/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # pylint: disable=unsubscriptable-object import io import os import sys import tempfile import unittest from unittest.mock import Mock from opentelemetry._logs import LogRecord, SeverityNumber from opentelemetry.exporter.otlp.json.common._log_encoder import encode_logs from opentelemetry.exporter.otlp.json.file._internal import _format_line from opentelemetry.exporter.otlp.json.file._log_exporter import ( FileLogExporter, ) from opentelemetry.proto_json.logs.v1.logs import LogsData from opentelemetry.sdk._logs import ( LoggerProvider, ReadableLogRecord, ) from opentelemetry.sdk._logs.export import ( InMemoryLogRecordExporter, LogRecordExportResult, SimpleLogRecordProcessor, ) from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.util.instrumentation import InstrumentationScope _LOGGER_NAME = "opentelemetry.exporter.otlp.json.file._log_exporter" def _make_log_record( body: str = "test log message", resource_attrs: dict | None = None, ) -> ReadableLogRecord: return ReadableLogRecord( LogRecord( body=body, severity_text="INFO", severity_number=SeverityNumber.INFO, ), resource=Resource(resource_attrs or {"service.name": "test"}), instrumentation_scope=InstrumentationScope("test-scope", "1.0"), ) class TestFileLogExporter(unittest.TestCase): def setUp(self): self._stream = io.StringIO() self._exporter = FileLogExporter(stream=self._stream) def test_export_empty_sequence(self): result = self._exporter.export([]) self.assertEqual(result, LogRecordExportResult.SUCCESS) self.assertEqual(self._stream.getvalue(), "") def test_export_single_log(self): result = self._exporter.export([_make_log_record("hello from test")]) self.assertEqual(result, LogRecordExportResult.SUCCESS) lines = self._stream.getvalue().splitlines() self.assertEqual(len(lines), 1) data = LogsData.from_json(lines[0]) resource_logs = data.resource_logs[0] scope_logs = resource_logs.scope_logs[0] log_record = scope_logs.log_records[0] self.assertEqual( log_record.body.string_value, # type: ignore "hello from test", ) def test_export_multiple_logs_same_resource(self): logs = [ _make_log_record("first"), _make_log_record("second"), ] self._exporter.export(logs) lines = self._stream.getvalue().splitlines() self.assertEqual(len(lines), 1) data = LogsData.from_json(lines[0]) total_logs = sum( len(sl.log_records) for rl in data.resource_logs # pylint: disable=not-an-iterable for sl in rl.scope_logs ) self.assertEqual(total_logs, 2) def test_export_logs_different_resources(self): logs = [ _make_log_record("from-a", resource_attrs={"host": "a"}), _make_log_record("from-b", resource_attrs={"host": "b"}), ] self._exporter.export(logs) lines = self._stream.getvalue().splitlines() self.assertEqual(len(lines), 1) data = LogsData.from_json(lines[0]) self.assertEqual(len(data.resource_logs), 2) bodies = { log_record.body.string_value # type: ignore for rl in data.resource_logs # pylint: disable=not-an-iterable for sl in rl.scope_logs for log_record in sl.log_records } self.assertEqual(bodies, {"from-a", "from-b"}) # pylint: disable-next=no-self-use def test_stream_flushed_after_export(self): mock_stream = Mock() mock_stream.closed = False exporter = FileLogExporter(stream=mock_stream) exporter.export([_make_log_record()]) mock_stream.flush.assert_called_once() def test_export_after_shutdown(self): self._exporter.shutdown() with self.assertLogs(_LOGGER_NAME, level="WARNING"): result = self._exporter.export([_make_log_record()]) self.assertEqual(result, LogRecordExportResult.FAILURE) self.assertEqual(self._stream.getvalue(), "") def test_shutdown_idempotent(self): self._exporter.shutdown() with self.assertLogs(_LOGGER_NAME, level="WARNING"): self._exporter.shutdown() def test_force_flush_returns_true(self): self.assertTrue(self._exporter.force_flush()) def test_export_stream_error(self): mock_stream = Mock() mock_stream.closed = False mock_stream.write.side_effect = OSError("disk full") exporter = FileLogExporter(stream=mock_stream) with self.assertLogs(_LOGGER_NAME, level="ERROR"): result = exporter.export([_make_log_record()]) self.assertEqual(result, LogRecordExportResult.FAILURE) def test_export_with_path(self): with tempfile.TemporaryDirectory() as tmp_dir_name: path = os.path.join(tmp_dir_name, "output.jsonl") exporter = FileLogExporter(path) exporter.export([_make_log_record("hello from path")]) exporter.shutdown() with open(path, encoding="utf-8") as fh: data = LogsData.from_json(fh.read().splitlines()[0]) resource_logs = data.resource_logs[0] scope_logs = resource_logs.scope_logs[0] log_record = scope_logs.log_records[0] self.assertEqual( log_record.body.string_value, # type: ignore "hello from path", ) def test_path_and_stream_raises(self): with self.assertRaises(ValueError): FileLogExporter("output.jsonl", stream=self._stream) # type: ignore def test_default_stream_is_stdout(self): exporter = FileLogExporter() # pylint: disable-next=protected-access self.assertIs(exporter._exporter._stream, sys.stdout) class TestFileLogExporterRoundTrip(unittest.TestCase): def setUp(self): self._stream = io.StringIO() self._file_exporter = FileLogExporter(stream=self._stream) self._in_memory = InMemoryLogRecordExporter() provider = LoggerProvider() provider.add_log_record_processor(SimpleLogRecordProcessor(self._file_exporter)) provider.add_log_record_processor(SimpleLogRecordProcessor(self._in_memory)) self._logger = provider.get_logger("test.integration") def _expected(self) -> str: return "".join(_format_line(encode_logs([record]).to_dict()) for record in self._in_memory.get_finished_logs()) def test_single_log_matches_in_memory(self): self._logger.emit( LogRecord( body="hello from integration", severity_number=SeverityNumber.INFO, ) ) self.assertEqual(self._stream.getvalue(), self._expected()) def test_multiple_logs_match_in_memory(self): self._logger.emit(LogRecord(body="first message", severity_number=SeverityNumber.INFO)) self._logger.emit(LogRecord(body="second message", severity_number=SeverityNumber.WARN)) self.assertEqual(self._stream.getvalue(), self._expected()) test_metric_exporter.py000066400000000000000000000202171527070070700352060ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-json-file/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # pylint: disable=unsubscriptable-object import io import os import sys import tempfile import unittest from typing import IO from unittest.mock import Mock from opentelemetry.exporter.otlp.json.common.metrics_encoder import ( encode_metrics, ) from opentelemetry.exporter.otlp.json.file._internal import _format_line from opentelemetry.exporter.otlp.json.file.metric_exporter import ( FileMetricExporter, ) from opentelemetry.metrics import Observation from opentelemetry.proto_json.metrics.v1.metrics import ( MetricsData as OtlpMetricsData, ) from opentelemetry.sdk.metrics import MeterProvider from opentelemetry.sdk.metrics._internal.export import MetricExportResult from opentelemetry.sdk.metrics._internal.point import ( MetricsData, ResourceMetrics, ScopeMetrics, ) from opentelemetry.sdk.metrics.export import ( PeriodicExportingMetricReader, ) from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.util.instrumentation import InstrumentationScope from opentelemetry.test.metrictestutil import _generate_gauge, _generate_sum _LOGGER_NAME = "opentelemetry.exporter.otlp.json.file.metric_exporter" class _CapturingMetricExporter(FileMetricExporter): def __init__(self, *, stream: IO[str]) -> None: super().__init__(stream=stream) self.last_metrics_data: MetricsData | None = None def export( self, metrics_data: MetricsData, timeout_millis: float = 10_000, **kwargs, ) -> MetricExportResult: self.last_metrics_data = metrics_data return super().export(metrics_data, timeout_millis=timeout_millis, **kwargs) def _make_metrics_data() -> MetricsData: return MetricsData( resource_metrics=[ ResourceMetrics( resource=Resource({"service.name": "test-service"}), scope_metrics=[ ScopeMetrics( scope=InstrumentationScope("test-scope", "1.0"), metrics=[_generate_sum("requests", 42)], schema_url="", ) ], schema_url="", ) ] ) class TestFileMetricExporter(unittest.TestCase): def setUp(self): self._stream = io.StringIO() self._exporter = FileMetricExporter(stream=self._stream) def test_export_metrics(self): result = self._exporter.export(_make_metrics_data()) self.assertEqual(result, MetricExportResult.SUCCESS) lines = self._stream.getvalue().splitlines() self.assertEqual(len(lines), 1) data = OtlpMetricsData.from_json(lines[0]) resource_metrics = data.resource_metrics[0] scope_metrics = resource_metrics.scope_metrics[0] self.assertEqual( scope_metrics.metrics[0].name, "requests", ) # pylint: disable-next=no-self-use def test_stream_flushed_after_export(self): mock_stream = Mock() mock_stream.closed = False exporter = FileMetricExporter(stream=mock_stream) exporter.export(_make_metrics_data()) mock_stream.flush.assert_called_once() def test_export_multiple_resource_metrics(self): data = MetricsData( resource_metrics=[ ResourceMetrics( resource=Resource({"host": "a"}), scope_metrics=[ ScopeMetrics( scope=InstrumentationScope("s", "1"), metrics=[_generate_sum("counter_a", 1)], schema_url="", ) ], schema_url="", ), ResourceMetrics( resource=Resource({"host": "b"}), scope_metrics=[ ScopeMetrics( scope=InstrumentationScope("s", "1"), metrics=[_generate_gauge("gauge_b", 2)], schema_url="", ) ], schema_url="", ), ] ) self._exporter.export(data) lines = self._stream.getvalue().splitlines() self.assertEqual(len(lines), 1) data = OtlpMetricsData.from_json(lines[0]) self.assertEqual(len(data.resource_metrics), 2) names = { metric.name for rm in data.resource_metrics # pylint: disable=not-an-iterable for sm in rm.scope_metrics for metric in sm.metrics } self.assertEqual(names, {"counter_a", "gauge_b"}) def test_export_after_shutdown(self): self._exporter.shutdown() with self.assertLogs(_LOGGER_NAME, level="WARNING"): result = self._exporter.export(_make_metrics_data()) self.assertEqual(result, MetricExportResult.FAILURE) self.assertEqual(self._stream.getvalue(), "") def test_shutdown_idempotent(self): self._exporter.shutdown() with self.assertLogs(_LOGGER_NAME, level="WARNING"): self._exporter.shutdown() def test_force_flush_returns_true(self): self.assertTrue(self._exporter.force_flush()) def test_export_stream_error(self): mock_stream = Mock() mock_stream.closed = False mock_stream.write.side_effect = OSError("disk full") exporter = FileMetricExporter(stream=mock_stream) with self.assertLogs(_LOGGER_NAME, level="ERROR"): result = exporter.export(_make_metrics_data()) self.assertEqual(result, MetricExportResult.FAILURE) def test_export_with_path(self): with tempfile.TemporaryDirectory() as tmp_dir_name: path = os.path.join(tmp_dir_name, "output.jsonl") exporter = FileMetricExporter(path) exporter.export(_make_metrics_data()) exporter.shutdown() with open(path, encoding="utf-8") as fh: data = OtlpMetricsData.from_json(fh.read().splitlines()[0]) resource_metrics = data.resource_metrics[0] scope_metrics = resource_metrics.scope_metrics[0] self.assertEqual( scope_metrics.metrics[0].name, "requests", ) def test_path_and_stream_raises(self): with self.assertRaises(ValueError): FileMetricExporter("output.jsonl", stream=self._stream) # type: ignore def test_default_stream_is_stdout(self): exporter = FileMetricExporter() # pylint: disable-next=protected-access self.assertIs(exporter._exporter._stream, sys.stdout) class TestFileMetricExporterRoundTrip(unittest.TestCase): def setUp(self): self._stream = io.StringIO() self._exporter = _CapturingMetricExporter(stream=self._stream) self._provider = MeterProvider( metric_readers=[ PeriodicExportingMetricReader(self._exporter, export_interval_millis=100_000), ] ) self._meter = self._provider.get_meter(__name__) def tearDown(self): self._provider.shutdown() def _expected(self) -> str: return _format_line( encode_metrics(self._exporter.last_metrics_data).to_dict() # type: ignore ) def test_synchronous_instruments_match_in_memory(self): self._meter.create_counter("req.count").add(10) self._meter.create_up_down_counter("queue.depth").add(-3) self._meter.create_histogram("req.duration").record(1.5) self._meter.create_gauge("cpu.temp").set(72.0) self._provider.force_flush() self.assertEqual(self._stream.getvalue(), self._expected()) def test_observable_instruments_match_in_memory(self): self._meter.create_observable_counter("obs.req.count", callbacks=[lambda _: [Observation(20)]]) self._meter.create_observable_up_down_counter("obs.queue.depth", callbacks=[lambda _: [Observation(-7)]]) self._meter.create_observable_gauge("obs.cpu.temp", callbacks=[lambda _: [Observation(55.5)]]) self._provider.force_flush() self.assertEqual(self._stream.getvalue(), self._expected()) test_trace_exporter.py000066400000000000000000000204251527070070700350220ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-json-file/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # pylint: disable=unsubscriptable-object import io import os import sys import tempfile import unittest from unittest.mock import Mock from opentelemetry.exporter.otlp.json.common.trace_encoder import encode_spans from opentelemetry.exporter.otlp.json.file._internal import _format_line from opentelemetry.exporter.otlp.json.file.trace_exporter import ( FileSpanExporter, ) from opentelemetry.proto_json.trace.v1.trace import TracesData from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import ( SimpleSpanProcessor, SpanExportResult, ) from opentelemetry.sdk.trace.export.in_memory_span_exporter import ( InMemorySpanExporter, ) from opentelemetry.trace import Link, SpanContext, StatusCode, TraceFlags _LOGGER_NAME = "opentelemetry.exporter.otlp.json.file.trace_exporter" class TestFileSpanExporter(unittest.TestCase): def setUp(self): self._stream = io.StringIO() self._exporter = FileSpanExporter(stream=self._stream) self._in_memory = InMemorySpanExporter() provider = TracerProvider() provider.add_span_processor(SimpleSpanProcessor(self._in_memory)) self._tracer = provider.get_tracer(__name__) def _finished_spans(self): return list(self._in_memory.get_finished_spans()) def _make_span(self, name: str = "test-span"): with self._tracer.start_as_current_span(name): pass return self._finished_spans() def test_export_empty_sequence(self): result = self._exporter.export([]) self.assertEqual(result, SpanExportResult.SUCCESS) self.assertEqual(self._stream.getvalue(), "") def test_export_single_span(self): result = self._exporter.export(self._make_span("my-span")) self.assertEqual(result, SpanExportResult.SUCCESS) lines = self._stream.getvalue().splitlines() self.assertEqual(len(lines), 1) data = TracesData.from_json(lines[0]) resource_spans = data.resource_spans[0] scope_spans = resource_spans.scope_spans[0] self.assertEqual(scope_spans.spans[0].name, "my-span") def test_export_multiple_spans_same_resource(self): with self._tracer.start_as_current_span("first"): pass with self._tracer.start_as_current_span("second"): pass self._exporter.export(self._finished_spans()) lines = self._stream.getvalue().splitlines() self.assertEqual(len(lines), 1) data = TracesData.from_json(lines[0]) total_spans = sum( len(ss.spans) for rs in data.resource_spans # pylint: disable=not-an-iterable for ss in rs.scope_spans ) self.assertEqual(total_spans, 2) def test_export_spans_different_resources(self): spans = [] for name, host in (("from-a", "a"), ("from-b", "b")): exporter = InMemorySpanExporter() provider = TracerProvider(resource=Resource({"host": host})) provider.add_span_processor(SimpleSpanProcessor(exporter)) tracer = provider.get_tracer(__name__) with tracer.start_as_current_span(name): pass spans.extend(exporter.get_finished_spans()) self._exporter.export(spans) lines = self._stream.getvalue().splitlines() self.assertEqual(len(lines), 1) data = TracesData.from_json(lines[0]) self.assertEqual(len(data.resource_spans), 2) names = { span.name for rs in data.resource_spans # pylint: disable=not-an-iterable for ss in rs.scope_spans for span in ss.spans } self.assertEqual(names, {"from-a", "from-b"}) def test_stream_flushed_after_export(self): mock_stream = Mock() mock_stream.closed = False exporter = FileSpanExporter(stream=mock_stream) exporter.export(self._make_span()) mock_stream.flush.assert_called_once() def test_export_after_shutdown(self): self._exporter.shutdown() with self.assertLogs(_LOGGER_NAME, level="WARNING"): result = self._exporter.export(self._make_span()) self.assertEqual(result, SpanExportResult.FAILURE) self.assertEqual(self._stream.getvalue(), "") def test_shutdown_idempotent(self): self._exporter.shutdown() with self.assertLogs(_LOGGER_NAME, level="WARNING"): self._exporter.shutdown() def test_force_flush_returns_true(self): self.assertTrue(self._exporter.force_flush()) self._exporter.export(self._make_span()) self.assertTrue(self._exporter.force_flush()) def test_export_stream_error(self): mock_stream = Mock() mock_stream.closed = False mock_stream.write.side_effect = OSError("disk full") exporter = FileSpanExporter(stream=mock_stream) with self.assertLogs(_LOGGER_NAME, level="ERROR"): result = exporter.export(self._make_span()) self.assertEqual(result, SpanExportResult.FAILURE) def test_export_with_path(self): with tempfile.TemporaryDirectory() as tmp_dir_name: path = os.path.join(tmp_dir_name, "output.jsonl") exporter = FileSpanExporter(path) exporter.export(self._make_span("path-span")) exporter.shutdown() with open(path, encoding="utf-8") as fh: data = TracesData.from_json(fh.read().splitlines()[0]) resource_spans = data.resource_spans[0] scope_spans = resource_spans.scope_spans[0] self.assertEqual(scope_spans.spans[0].name, "path-span") def test_path_and_stream_raises(self): with self.assertRaises(ValueError): FileSpanExporter("output.jsonl", stream=self._stream) # type: ignore def test_default_stream_is_stdout(self): exporter = FileSpanExporter() # pylint: disable-next=protected-access self.assertIs(exporter._exporter._stream, sys.stdout) class TestFileSpanExporterRoundTrip(unittest.TestCase): def setUp(self): self._stream = io.StringIO() self._file_exporter = FileSpanExporter(stream=self._stream) self._in_memory = InMemorySpanExporter() provider = TracerProvider() provider.add_span_processor(SimpleSpanProcessor(self._file_exporter)) provider.add_span_processor(SimpleSpanProcessor(self._in_memory)) self._tracer = provider.get_tracer(__name__) def _expected(self) -> str: return "".join(_format_line(encode_spans([span]).to_dict()) for span in self._in_memory.get_finished_spans()) def test_single_span_matches_in_memory(self): link_ctx = SpanContext( trace_id=0x000000000000000000000000DEADBEEF, span_id=0x00000000DEADBEF0, is_remote=True, trace_flags=TraceFlags(0x01), ) with self._tracer.start_as_current_span( "rich-span", links=[Link(link_ctx, {"link.order": 1})], ) as span: span.set_attributes( { "http.method": "GET", "http.status_code": 200, "http.retried": False, } ) span.add_event("cache-miss", {"cache.key": "user:42"}) span.set_status(StatusCode.OK) self.assertEqual(self._stream.getvalue(), self._expected()) def test_multiple_spans_match_in_memory(self): with self._tracer.start_as_current_span("parent-op", attributes={"phase": "request"}) as parent: parent.add_event("processing-started") with self._tracer.start_as_current_span("child-op") as child: child.set_attribute("attempt", 1) child.set_status(StatusCode.ERROR, "timeout") self.assertEqual(self._stream.getvalue(), self._expected()) def test_recorded_exception_matches_in_memory(self): with self._tracer.start_as_current_span("failing-op") as span: try: raise ValueError("something went wrong") except ValueError as exc: span.record_exception(exc) span.set_status(StatusCode.ERROR, str(exc)) self.assertEqual(self._stream.getvalue(), self._expected()) python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-json-http/000077500000000000000000000000001527070070700273355ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-json-http/LICENSE000066400000000000000000000261351527070070700303510ustar00rootroot00000000000000 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. python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-json-http/README.rst000066400000000000000000000034461527070070700310330ustar00rootroot00000000000000OpenTelemetry Collector JSON over HTTP Exporter ================================================ |pypi| .. |pypi| image:: https://badge.fury.io/py/opentelemetry-exporter-otlp-json-http.svg :target: https://pypi.org/project/opentelemetry-exporter-otlp-json-http/ This library exports OpenTelemetry traces, metrics and logs using the OpenTelemetry Protocol (OTLP) JSON encoding over HTTP. It can be used to send data to any backend that accepts OTLP JSON such as the OpenTelemetry Collector. Installation ------------ :: pip install opentelemetry-exporter-otlp-json-http Usage ----- .. code:: python from opentelemetry import trace from opentelemetry.exporter.otlp.json.http import OTLPSpanExporter from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchSpanProcessor resource = Resource.create({ "service.name": "service" }) trace.set_tracer_provider(TracerProvider(resource=resource)) tracer = trace.get_tracer(__name__) otlp_exporter = OTLPSpanExporter() span_processor = BatchSpanProcessor(otlp_exporter) trace.get_tracer_provider().add_span_processor(span_processor) with tracer.start_as_current_span("foo"): print("Hello world!") References ---------- * `OpenTelemetry OTLP Exporters `_ * `OpenTelemetry Collector `_ * `OpenTelemetry `_ * `OpenTelemetry Protocol Specification `_ * `OTLP JSON Encoding Specification `_ python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-json-http/pyproject.toml000066400000000000000000000036721527070070700322610ustar00rootroot00000000000000[build-system] requires = ["hatchling"] build-backend = "hatchling.build" [project] name = "opentelemetry-exporter-otlp-json-http" dynamic = ["version"] description = "OpenTelemetry OTLP JSON Exporter" 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", "Framework :: OpenTelemetry", "Framework :: OpenTelemetry :: Exporters", "Intended Audience :: Developers", "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.45.2", "opentelemetry-proto-json == 0.66b2", "opentelemetry-exporter-otlp-common == 0.66b2", "opentelemetry-exporter-otlp-json-common == 0.66b2", "opentelemetry-exporter-http-transport[urllib3] == 0.66b2", ] [project.entry-points.opentelemetry_traces_exporter] otlp_json_http = "opentelemetry.exporter.otlp.json.http.trace_exporter:OTLPSpanExporter" [project.entry-points.opentelemetry_metrics_exporter] otlp_json_http = "opentelemetry.exporter.otlp.json.http.metric_exporter:OTLPMetricExporter" [project.entry-points.opentelemetry_logs_exporter] otlp_json_http = "opentelemetry.exporter.otlp.json.http._log_exporter:OTLPLogExporter" [project.urls] Homepage = "https://github.com/open-telemetry/opentelemetry-python/tree/main/exporter/opentelemetry-exporter-otlp-json-http" Repository = "https://github.com/open-telemetry/opentelemetry-python" [tool.hatch.version] path = "src/opentelemetry/exporter/otlp/json/http/version/__init__.py" [tool.hatch.build.targets.sdist] include = [ "/src", "/tests", ] [tool.hatch.build.targets.wheel] packages = ["src/opentelemetry"] python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-json-http/src/000077500000000000000000000000001527070070700301245ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-json-http/src/opentelemetry/000077500000000000000000000000001527070070700330205ustar00rootroot00000000000000exporter/000077500000000000000000000000001527070070700346115ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-json-http/src/opentelemetryotlp/000077500000000000000000000000001527070070700355675ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-json-http/src/opentelemetry/exporterjson/000077500000000000000000000000001527070070700365405ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-json-http/src/opentelemetry/exporter/otlphttp/000077500000000000000000000000001527070070700375175ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-json-http/src/opentelemetry/exporter/otlp/json__init__.py000066400000000000000000000047571527070070700416450ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-json-http/src/opentelemetry/exporter/otlp/json/http# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 """ This library allows exporting OpenTelemetry telemetry using the OTLP JSON over HTTP wire format to any backend that accepts OTLP JSON. Usage ----- The **OTLP JSON HTTP Exporter** exports `OpenTelemetry`_ traces, metrics, and logs via HTTP to any `OTLP`_-compatible backend using OTLP JSON encoding instead of protobuf. Three exporters are provided: - :class:`~opentelemetry.exporter.otlp.json.http.trace_exporter.OTLPSpanExporter` - traces - :class:`~opentelemetry.exporter.otlp.json.http.metric_exporter.OTLPMetricExporter` - metrics - :class:`~opentelemetry.exporter.otlp.json.http._log_exporter.OTLPLogExporter` - logs You can configure each exporter with the following environment variables, using the appropriate per-signal prefix (``TRACES``, ``METRICS``, or ``LOGS``) or the generic variant that applies to all three: - :envvar:`OTEL_EXPORTER_OTLP_ENDPOINT` - :envvar:`OTEL_EXPORTER_OTLP_HEADERS` - :envvar:`OTEL_EXPORTER_OTLP_TIMEOUT` - :envvar:`OTEL_EXPORTER_OTLP_COMPRESSION` - :envvar:`OTEL_EXPORTER_OTLP_CERTIFICATE` - :envvar:`OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE` - :envvar:`OTEL_EXPORTER_OTLP_CLIENT_KEY` The metric exporter also supports the following environment variables: - :envvar:`OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE` - :envvar:`OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION` .. _OTLP: https://github.com/open-telemetry/opentelemetry-collector/ .. _OpenTelemetry: https://github.com/open-telemetry/opentelemetry-python/ .. code:: python from opentelemetry import trace from opentelemetry.exporter.otlp.json.http import OTLPSpanExporter from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchSpanProcessor resource = Resource.create({ "service.name": "service" }) trace.set_tracer_provider(TracerProvider(resource=resource)) tracer = trace.get_tracer(__name__) otlp_exporter = OTLPSpanExporter() span_processor = BatchSpanProcessor(otlp_exporter) trace.get_tracer_provider().add_span_processor(span_processor) with tracer.start_as_current_span("foo"): print("Hello world!") API --- """ from opentelemetry.exporter.otlp.json.http.metric_exporter import ( OTLPMetricExporter, ) from opentelemetry.exporter.otlp.json.http.trace_exporter import ( OTLPSpanExporter, ) __all__ = [ "OTLPMetricExporter", "OTLPSpanExporter", ] _internal.py000066400000000000000000000076331527070070700420550ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-json-http/src/opentelemetry/exporter/otlp/json/http# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations import logging import os from collections.abc import Mapping from typing import TYPE_CHECKING, Literal from opentelemetry.exporter.http.transport._urllib3 import Urllib3HTTPTransport from opentelemetry.exporter.otlp.common.http import Compression from opentelemetry.exporter.otlp.json.http.version import __version__ from opentelemetry.sdk.environment_variables import ( OTEL_EXPORTER_OTLP_CERTIFICATE, OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE, OTEL_EXPORTER_OTLP_CLIENT_KEY, OTEL_EXPORTER_OTLP_COMPRESSION, OTEL_EXPORTER_OTLP_ENDPOINT, OTEL_EXPORTER_OTLP_HEADERS, OTEL_EXPORTER_OTLP_TIMEOUT, ) from opentelemetry.util.re import parse_env_headers if TYPE_CHECKING: from opentelemetry.exporter.http.transport import BaseHTTPTransportFactory from opentelemetry.exporter.http.transport._base import BaseHTTPTransport _DEFAULT_ENDPOINT = "http://localhost:4318" _DEFAULT_TIMEOUT = 10 _logger = logging.getLogger(__name__) def _resolve_endpoint( endpoint_env_var: str, default_path: Literal["v1/traces", "v1/metrics", "v1/logs"], ) -> str: if endpoint := os.environ.get(endpoint_env_var): return endpoint base_endpoint = os.environ.get(OTEL_EXPORTER_OTLP_ENDPOINT) or _DEFAULT_ENDPOINT return f"{base_endpoint.removesuffix('/')}/{default_path}" def _resolve_headers( headers: Mapping[str, str] | None, headers_env_var: str, ) -> dict[str, str]: headers_ = { "content-type": "application/json", "user-agent": "OTel-OTLP-JSON-Exporter-Python/" + __version__, } env_headers = parse_env_headers( os.environ.get(headers_env_var) or os.environ.get(OTEL_EXPORTER_OTLP_HEADERS, ""), liberal=True, ) headers_.update(env_headers) if headers: headers_.update({key.lower(): value for key, value in headers.items()}) return headers_ def _resolve_timeout( timeout_env_var: str, ) -> float: raw = os.environ.get(timeout_env_var) or os.environ.get(OTEL_EXPORTER_OTLP_TIMEOUT) or _DEFAULT_TIMEOUT try: return float(raw) except ValueError: _logger.warning( "Invalid timeout value %r, using default of %s seconds", raw, _DEFAULT_TIMEOUT, ) return float(_DEFAULT_TIMEOUT) def _resolve_compression(compression_env_var: str) -> Compression: val = ( ( os.environ.get( compression_env_var, ) or os.environ.get(OTEL_EXPORTER_OTLP_COMPRESSION) or "none" ) .lower() .strip() ) try: return Compression.from_str(val) except ValueError: _logger.warning("Unsupported compression type: %s", val) return Compression.NONE def _build_transport( certificate_file: str | None, client_key_file: str | None, client_certificate_file: str | None, certificate_env_var: str, client_key_env_var: str, client_certificate_env_var: str, transport_factory: BaseHTTPTransportFactory = Urllib3HTTPTransport, ) -> BaseHTTPTransport: verify: bool | str = ( certificate_file or os.environ.get( certificate_env_var, ) or os.environ.get(OTEL_EXPORTER_OTLP_CERTIFICATE) or True ) client_key_file = ( client_key_file or os.environ.get( client_key_env_var, ) or os.environ.get(OTEL_EXPORTER_OTLP_CLIENT_KEY) ) client_certificate_file = ( client_certificate_file or os.environ.get( client_certificate_env_var, ) or os.environ.get(OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE) ) return transport_factory( verify=verify, cert=(client_certificate_file, client_key_file) if client_certificate_file and client_key_file else client_certificate_file, ) _log_exporter.py000066400000000000000000000112761527070070700427500ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-json-http/src/opentelemetry/exporter/otlp/json/http# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import logging from collections.abc import Mapping, Sequence from typing import overload from opentelemetry.exporter.http.transport._base import BaseHTTPTransport from opentelemetry.exporter.otlp.common.http import ( Compression, _OTLPHTTPClient, ) from opentelemetry.exporter.otlp.json.common._internal._log_encoder import ( encode_logs, ) from opentelemetry.exporter.otlp.json.http._internal import ( _build_transport, _resolve_compression, _resolve_endpoint, _resolve_headers, _resolve_timeout, ) from opentelemetry.sdk._logs import ReadableLogRecord from opentelemetry.sdk._logs.export import ( LogRecordExporter, LogRecordExportResult, ) from opentelemetry.sdk._shared_internal import DuplicateFilter from opentelemetry.sdk.environment_variables import ( OTEL_EXPORTER_OTLP_LOGS_CERTIFICATE, OTEL_EXPORTER_OTLP_LOGS_CLIENT_CERTIFICATE, OTEL_EXPORTER_OTLP_LOGS_CLIENT_KEY, OTEL_EXPORTER_OTLP_LOGS_COMPRESSION, OTEL_EXPORTER_OTLP_LOGS_ENDPOINT, OTEL_EXPORTER_OTLP_LOGS_HEADERS, OTEL_EXPORTER_OTLP_LOGS_TIMEOUT, ) _DEFAULT_LOGS_EXPORT_PATH = "v1/logs" _logger = logging.getLogger(__name__) _logger.addFilter(DuplicateFilter()) class OTLPLogExporter(LogRecordExporter): @overload def __init__( self, endpoint: str | None = None, certificate_file: str | None = None, client_key_file: str | None = None, client_certificate_file: str | None = None, headers: Mapping[str, str] | None = None, timeout: float | None = None, compression: Compression | None = None, ) -> None: ... @overload def __init__( self, endpoint: str | None = None, certificate_file: None = None, client_key_file: None = None, client_certificate_file: None = None, headers: Mapping[str, str] | None = None, timeout: float | None = None, compression: Compression | None = None, *, _transport: BaseHTTPTransport, ) -> None: ... def __init__( self, endpoint: str | None = None, certificate_file: str | None = None, client_key_file: str | None = None, client_certificate_file: str | None = None, headers: Mapping[str, str] | None = None, timeout: float | None = None, compression: Compression | None = None, *, _transport: BaseHTTPTransport | None = None, ) -> None: """OTLP HTTP JSON log exporter. Args: endpoint: Full URL of the OTLP/HTTP signal endpoint, including the signal path. Example: ``http://collector:4318/v1/logs``. For a base URL without a signal path, set the ``OTEL_EXPORTER_OTLP_ENDPOINT`` environment variable. """ transport = _transport or _build_transport( certificate_file, client_key_file, client_certificate_file, OTEL_EXPORTER_OTLP_LOGS_CERTIFICATE, OTEL_EXPORTER_OTLP_LOGS_CLIENT_KEY, OTEL_EXPORTER_OTLP_LOGS_CLIENT_CERTIFICATE, ) self._client = _OTLPHTTPClient( transport=transport, endpoint=endpoint or _resolve_endpoint(OTEL_EXPORTER_OTLP_LOGS_ENDPOINT, _DEFAULT_LOGS_EXPORT_PATH), kind="logs", timeout=timeout if timeout is not None else _resolve_timeout(OTEL_EXPORTER_OTLP_LOGS_TIMEOUT), compression=compression if compression is not None else _resolve_compression(OTEL_EXPORTER_OTLP_LOGS_COMPRESSION), headers=_resolve_headers(headers, OTEL_EXPORTER_OTLP_LOGS_HEADERS), logger=_logger, ) self._shutdown = False def export(self, batch: Sequence[ReadableLogRecord]) -> LogRecordExportResult: if self._shutdown: _logger.warning("Exporter already shutdown, ignoring batch") return LogRecordExportResult.FAILURE try: body = encode_logs(batch).to_json().encode() # pylint: disable-next=broad-exception-caught except Exception as error: _logger.error("Failed to encode logs: %s", error) return LogRecordExportResult.FAILURE export_result = self._client.export(body) return LogRecordExportResult.SUCCESS if export_result.success else LogRecordExportResult.FAILURE def shutdown(self) -> None: if self._shutdown: _logger.warning("Exporter already shutdown, ignoring call") return self._shutdown = True self._client.shutdown() # pylint: disable-next=no-self-use def force_flush(self, timeout_millis: int = 10_000) -> bool: return True metric_exporter.py000066400000000000000000000136741527070070700433170ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-json-http/src/opentelemetry/exporter/otlp/json/http# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import logging from collections.abc import Mapping from typing import overload from opentelemetry.exporter.http.transport._base import BaseHTTPTransport from opentelemetry.exporter.otlp.common._aggregation import ( _get_aggregation, _get_temporality, ) from opentelemetry.exporter.otlp.common.http import ( Compression, _OTLPHTTPClient, ) from opentelemetry.exporter.otlp.json.common._internal.metrics_encoder import ( encode_metrics, split_metrics_data, ) from opentelemetry.exporter.otlp.json.http._internal import ( _build_transport, _resolve_compression, _resolve_endpoint, _resolve_headers, _resolve_timeout, ) from opentelemetry.sdk.environment_variables import ( OTEL_EXPORTER_OTLP_METRICS_CERTIFICATE, OTEL_EXPORTER_OTLP_METRICS_CLIENT_CERTIFICATE, OTEL_EXPORTER_OTLP_METRICS_CLIENT_KEY, OTEL_EXPORTER_OTLP_METRICS_COMPRESSION, OTEL_EXPORTER_OTLP_METRICS_ENDPOINT, OTEL_EXPORTER_OTLP_METRICS_HEADERS, OTEL_EXPORTER_OTLP_METRICS_TIMEOUT, ) from opentelemetry.sdk.metrics.export import ( AggregationTemporality, MetricExporter, MetricExportResult, MetricsData, ) from opentelemetry.sdk.metrics.view import Aggregation _DEFAULT_METRICS_EXPORT_PATH = "v1/metrics" _logger = logging.getLogger(__name__) class OTLPMetricExporter(MetricExporter): @overload def __init__( self, endpoint: str | None = None, certificate_file: str | None = None, client_key_file: str | None = None, client_certificate_file: str | None = None, headers: Mapping[str, str] | None = None, timeout: float | None = None, compression: Compression | None = None, preferred_temporality: dict[type, AggregationTemporality] | None = None, preferred_aggregation: dict[type, Aggregation] | None = None, max_export_batch_size: int | None = None, ) -> None: ... @overload def __init__( self, endpoint: str | None = None, certificate_file: None = None, client_key_file: None = None, client_certificate_file: None = None, headers: Mapping[str, str] | None = None, timeout: float | None = None, compression: Compression | None = None, preferred_temporality: dict[type, AggregationTemporality] | None = None, preferred_aggregation: dict[type, Aggregation] | None = None, max_export_batch_size: int | None = None, *, _transport: BaseHTTPTransport, ) -> None: ... def __init__( self, endpoint: str | None = None, certificate_file: str | None = None, client_key_file: str | None = None, client_certificate_file: str | None = None, headers: Mapping[str, str] | None = None, timeout: float | None = None, compression: Compression | None = None, preferred_temporality: dict[type, AggregationTemporality] | None = None, preferred_aggregation: dict[type, Aggregation] | None = None, max_export_batch_size: int | None = None, *, _transport: BaseHTTPTransport | None = None, ) -> None: """OTLP HTTP JSON metric exporter. Args: endpoint: Full URL of the OTLP/HTTP signal endpoint, including the signal path. Example: ``http://collector:4318/v1/metrics``. For a base URL without a signal path, set the ``OTEL_EXPORTER_OTLP_ENDPOINT`` environment variable. """ MetricExporter.__init__( self, preferred_temporality=_get_temporality(preferred_temporality), preferred_aggregation=_get_aggregation(preferred_aggregation), ) transport = _transport or _build_transport( certificate_file, client_key_file, client_certificate_file, OTEL_EXPORTER_OTLP_METRICS_CERTIFICATE, OTEL_EXPORTER_OTLP_METRICS_CLIENT_KEY, OTEL_EXPORTER_OTLP_METRICS_CLIENT_CERTIFICATE, ) self._client = _OTLPHTTPClient( transport=transport, endpoint=endpoint or _resolve_endpoint( OTEL_EXPORTER_OTLP_METRICS_ENDPOINT, _DEFAULT_METRICS_EXPORT_PATH, ), kind="metrics", timeout=timeout if timeout is not None else _resolve_timeout(OTEL_EXPORTER_OTLP_METRICS_TIMEOUT), compression=compression if compression is not None else _resolve_compression(OTEL_EXPORTER_OTLP_METRICS_COMPRESSION), headers=_resolve_headers(headers, OTEL_EXPORTER_OTLP_METRICS_HEADERS), logger=_logger, ) self._max_export_batch_size = max_export_batch_size self._shutdown = False def export( self, metrics_data: MetricsData, timeout_millis: float = 10_000, **kwargs, ) -> MetricExportResult: if self._shutdown: _logger.warning("Exporter already shutdown, ignoring batch") return MetricExportResult.FAILURE try: export_request = encode_metrics(metrics_data) # pylint: disable-next=broad-exception-caught except Exception as error: _logger.error("Failed to encode metrics: %s", error) return MetricExportResult.FAILURE for request in split_metrics_data(export_request, self._max_export_batch_size): export_result = self._client.export(request.to_json().encode()) if not export_result.success: return MetricExportResult.FAILURE return MetricExportResult.SUCCESS def shutdown(self, timeout_millis: float = 30_000, **kwargs) -> None: if self._shutdown: _logger.warning("Exporter already shutdown, ignoring call") return self._shutdown = True self._client.shutdown() def force_flush(self, timeout_millis: float = 10_000) -> bool: return True py.typed000066400000000000000000000000001527070070700412040ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-json-http/src/opentelemetry/exporter/otlp/json/httptrace_exporter.py000066400000000000000000000111101527070070700431110ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-json-http/src/opentelemetry/exporter/otlp/json/http# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import logging from collections.abc import Mapping, Sequence from typing import overload from opentelemetry.exporter.http.transport._base import BaseHTTPTransport from opentelemetry.exporter.otlp.common.http import ( Compression, _OTLPHTTPClient, ) from opentelemetry.exporter.otlp.json.common._internal.trace_encoder import ( encode_spans, ) from opentelemetry.exporter.otlp.json.http._internal import ( _build_transport, _resolve_compression, _resolve_endpoint, _resolve_headers, _resolve_timeout, ) from opentelemetry.sdk.environment_variables import ( OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE, OTEL_EXPORTER_OTLP_TRACES_CLIENT_CERTIFICATE, OTEL_EXPORTER_OTLP_TRACES_CLIENT_KEY, OTEL_EXPORTER_OTLP_TRACES_COMPRESSION, OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, OTEL_EXPORTER_OTLP_TRACES_HEADERS, OTEL_EXPORTER_OTLP_TRACES_TIMEOUT, ) from opentelemetry.sdk.trace import ReadableSpan from opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult _DEFAULT_TRACES_EXPORT_PATH = "v1/traces" _logger = logging.getLogger(__name__) class OTLPSpanExporter(SpanExporter): @overload def __init__( self, endpoint: str | None = None, certificate_file: str | None = None, client_key_file: str | None = None, client_certificate_file: str | None = None, headers: Mapping[str, str] | None = None, timeout: float | None = None, compression: Compression | None = None, ) -> None: ... @overload def __init__( self, endpoint: str | None = None, certificate_file: None = None, client_key_file: None = None, client_certificate_file: None = None, headers: Mapping[str, str] | None = None, timeout: float | None = None, compression: Compression | None = None, *, _transport: BaseHTTPTransport, ) -> None: ... def __init__( self, endpoint: str | None = None, certificate_file: str | None = None, client_key_file: str | None = None, client_certificate_file: str | None = None, headers: Mapping[str, str] | None = None, timeout: float | None = None, compression: Compression | None = None, *, _transport: BaseHTTPTransport | None = None, ) -> None: """OTLP HTTP JSON span exporter. Args: endpoint: Full URL of the OTLP/HTTP signal endpoint, including the signal path. Example: ``http://collector:4318/v1/traces``. For a base URL without a signal path, set the ``OTEL_EXPORTER_OTLP_ENDPOINT`` environment variable. """ transport = _transport or _build_transport( certificate_file, client_key_file, client_certificate_file, OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE, OTEL_EXPORTER_OTLP_TRACES_CLIENT_KEY, OTEL_EXPORTER_OTLP_TRACES_CLIENT_CERTIFICATE, ) self._client = _OTLPHTTPClient( transport=transport, endpoint=endpoint or _resolve_endpoint(OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, _DEFAULT_TRACES_EXPORT_PATH), kind="spans", timeout=timeout if timeout is not None else _resolve_timeout(OTEL_EXPORTER_OTLP_TRACES_TIMEOUT), compression=compression if compression is not None else _resolve_compression(OTEL_EXPORTER_OTLP_TRACES_COMPRESSION), headers=_resolve_headers(headers, OTEL_EXPORTER_OTLP_TRACES_HEADERS), logger=_logger, ) self._shutdown = False def export(self, spans: Sequence[ReadableSpan]) -> SpanExportResult: if self._shutdown: _logger.warning("Exporter already shutdown, ignoring batch") return SpanExportResult.FAILURE try: body = encode_spans(spans).to_json().encode() # pylint: disable-next=broad-exception-caught except Exception as error: _logger.error("Failed to encode spans: %s", error) return SpanExportResult.FAILURE export_result = self._client.export(body) return SpanExportResult.SUCCESS if export_result.success else SpanExportResult.FAILURE def shutdown(self, timeout_millis: float = 30_000, **kwargs) -> None: if self._shutdown: _logger.warning("Exporter already shutdown, ignoring call") return self._shutdown = True self._client.shutdown() def force_flush(self, timeout_millis: int = 30000) -> bool: return True version/000077500000000000000000000000001527070070700412045ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-json-http/src/opentelemetry/exporter/otlp/json/http__init__.py000066400000000000000000000001441527070070700433140ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-json-http/src/opentelemetry/exporter/otlp/json/http/version# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 __version__ = "0.66b2" python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-json-http/test-requirements.txt000066400000000000000000000012531527070070700335770ustar00rootroot00000000000000asgiref==3.7.2 decorator==5.2.1 h11==0.16.0 importlib-metadata==6.11.0 iniconfig==2.0.0 mocket==3.14.1 packaging==24.0 pluggy==1.6.0 puremagic==1.30 ; python_full_version < '3.12' puremagic==2.2.0 ; python_full_version >= '3.12' py-cpuinfo==9.0.0 pytest==9.0.3 tomli==2.0.1 typing_extensions==4.15.0 urllib3==2.7.0 wrapt==1.16.0 zipp==3.19.2 -e opentelemetry-api -e opentelemetry-sdk -e opentelemetry-semantic-conventions -e tests/opentelemetry-test-utils -e opentelemetry-proto-json -e exporter/opentelemetry-exporter-http-transport -e exporter/opentelemetry-exporter-otlp-common -e exporter/opentelemetry-exporter-otlp-json-common -e exporter/opentelemetry-exporter-otlp-json-http python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-json-http/tests/000077500000000000000000000000001527070070700304775ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-json-http/tests/__init__.py000066400000000000000000000015301527070070700326070ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from collections.abc import Callable, Iterator from contextlib import contextmanager from unittest.mock import Mock, patch # Helper context manager to mock blocking wait operations and calls to # get the current time in order to avoid wasting wall clock time. @contextmanager def _mock_clock( shutdown_event: Mock, ) -> Iterator[Callable[[float], None]]: _now = 0.0 def advance(delta: float) -> None: nonlocal _now _now += delta def get_time() -> float: return _now def _wait(duration: float) -> bool: advance(duration) return False shutdown_event.wait.side_effect = _wait with patch( "opentelemetry.exporter.otlp.common.http.time.time", side_effect=get_time, ): yield advance python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-json-http/tests/test_internal.py000066400000000000000000000352241527070070700337320ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # pylint: disable=protected-access import os import unittest from logging import WARNING from unittest.mock import MagicMock, patch from opentelemetry.exporter.http.transport._urllib3 import Urllib3HTTPTransport from opentelemetry.exporter.otlp.common.http import Compression from opentelemetry.exporter.otlp.json.http._internal import ( _DEFAULT_TIMEOUT, _build_transport, _resolve_compression, _resolve_endpoint, _resolve_headers, _resolve_timeout, ) from opentelemetry.exporter.otlp.json.http.version import __version__ from opentelemetry.sdk.environment_variables import ( OTEL_EXPORTER_OTLP_CERTIFICATE, OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE, OTEL_EXPORTER_OTLP_CLIENT_KEY, OTEL_EXPORTER_OTLP_COMPRESSION, OTEL_EXPORTER_OTLP_ENDPOINT, OTEL_EXPORTER_OTLP_HEADERS, OTEL_EXPORTER_OTLP_TIMEOUT, OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE, OTEL_EXPORTER_OTLP_TRACES_CLIENT_CERTIFICATE, OTEL_EXPORTER_OTLP_TRACES_CLIENT_KEY, OTEL_EXPORTER_OTLP_TRACES_COMPRESSION, OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, OTEL_EXPORTER_OTLP_TRACES_HEADERS, OTEL_EXPORTER_OTLP_TRACES_TIMEOUT, ) _USER_AGENT = "OTel-OTLP-JSON-Exporter-Python/" + __version__ _BASE_HEADERS = {"content-type": "application/json", "user-agent": _USER_AGENT} class TestResolveInternal(unittest.TestCase): def test_resolve_endpoint(self): cases = [ # per-signal wins over base and is returned verbatim (no path added) ( "per_signal_verbatim", { OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: "http://per-signal:4318/v1/traces", OTEL_EXPORTER_OTLP_ENDPOINT: "http://base:4318", }, "v1/traces", "http://per-signal:4318/v1/traces", ), ( "base_no_trailing_slash", {OTEL_EXPORTER_OTLP_ENDPOINT: "http://base:4318"}, "v1/traces", "http://base:4318/v1/traces", ), ( "base_trailing_slash_normalized", {OTEL_EXPORTER_OTLP_ENDPOINT: "http://base:4318/"}, "v1/metrics", "http://base:4318/v1/metrics", ), ( "empty_per_signal_falls_back", { OTEL_EXPORTER_OTLP_ENDPOINT: "http://base:4318", OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: "", }, "v1/traces", "http://base:4318/v1/traces", ), ( "empty_base_falls_back_to_default", { OTEL_EXPORTER_OTLP_ENDPOINT: "", }, "v1/traces", "http://localhost:4318/v1/traces", ), ( "default_traces", {}, "v1/traces", "http://localhost:4318/v1/traces", ), ( "default_logs", {}, "v1/logs", "http://localhost:4318/v1/logs", ), ] for label, env, default_path, expected in cases: with self.subTest(label), patch.dict(os.environ, env, clear=True): self.assertEqual( _resolve_endpoint(OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, default_path), expected, ) def test_resolve_headers(self): cases = [ ("defaults_only", {}, None, _BASE_HEADERS), ( "general_env_merged", {OTEL_EXPORTER_OTLP_HEADERS: "k1=v1,k2=v2"}, None, {**_BASE_HEADERS, "k1": "v1", "k2": "v2"}, ), # per-signal var is used instead of (not merged with) the general one ( "per_signal_overrides_general", { OTEL_EXPORTER_OTLP_HEADERS: "api-key=general,shared=g", OTEL_EXPORTER_OTLP_TRACES_HEADERS: "api-key=per-signal", }, None, {**_BASE_HEADERS, "api-key": "per-signal"}, ), # empty per-signal var falls back to (inherits) the general one ( "empty_per_signal_falls_back_to_general", { OTEL_EXPORTER_OTLP_HEADERS: "api-key=general", OTEL_EXPORTER_OTLP_TRACES_HEADERS: "", }, None, {**_BASE_HEADERS, "api-key": "general"}, ), # explicit arg wins over env and base, by exact key ( "explicit_arg_overrides", {OTEL_EXPORTER_OTLP_HEADERS: "api-key=from-env"}, {"api-key": "explicit", "Content-Type": "text/plain"}, { "content-type": "text/plain", "user-agent": _USER_AGENT, "api-key": "explicit", }, ), # env override of a default header must replace it ( "env_overrides_default_header_case_insensitively", {OTEL_EXPORTER_OTLP_HEADERS: "user-agent=custom-agent"}, None, { "content-type": "application/json", "user-agent": "custom-agent", }, ), ( "explicit_arg_overrides_default_header_case_insensitively", {}, {"User-Agent": "explicit-agent"}, { "content-type": "application/json", "user-agent": "explicit-agent", }, ), ] for label, env, headers_arg, expected in cases: with self.subTest(label), patch.dict(os.environ, env, clear=True): self.assertEqual( _resolve_headers(headers_arg, OTEL_EXPORTER_OTLP_TRACES_HEADERS), expected, ) def test_resolve_timeout(self): cases = [ ( "per_signal_wins", { OTEL_EXPORTER_OTLP_TRACES_TIMEOUT: "5", OTEL_EXPORTER_OTLP_TIMEOUT: "7", }, 5.0, False, ), ( "falls_back_to_general", {OTEL_EXPORTER_OTLP_TIMEOUT: "7"}, 7.0, False, ), ( "fractional", {OTEL_EXPORTER_OTLP_TRACES_TIMEOUT: "2.5"}, 2.5, False, ), ("default", {}, float(_DEFAULT_TIMEOUT), False), ( "empty_per_signal_falls_back_to_general", { OTEL_EXPORTER_OTLP_TRACES_TIMEOUT: "", OTEL_EXPORTER_OTLP_TIMEOUT: "7", }, 7.0, False, ), ( "empty_falls_back_to_default", {OTEL_EXPORTER_OTLP_TRACES_TIMEOUT: ""}, float(_DEFAULT_TIMEOUT), False, ), ( "invalid_value_logs_and_defaults", {OTEL_EXPORTER_OTLP_TRACES_TIMEOUT: "not-a-number"}, float(_DEFAULT_TIMEOUT), True, ), ] for label, env, expected, errors in cases: with self.subTest(label), patch.dict(os.environ, env, clear=True): if errors: with self.assertLogs(level=WARNING): result = _resolve_timeout(OTEL_EXPORTER_OTLP_TRACES_TIMEOUT) else: result = _resolve_timeout(OTEL_EXPORTER_OTLP_TRACES_TIMEOUT) self.assertEqual(result, expected) self.assertIsInstance(result, float) def test_resolve_compression(self): cases = [ ("gzip", {"COMP": "gzip"}, "COMP", Compression.GZIP, False), ( "deflate", {"COMP": "deflate"}, "COMP", Compression.DEFLATE, False, ), ("none", {"COMP": "none"}, "COMP", Compression.NONE, False), ( "case_and_whitespace", {"COMP": " GzIp "}, "COMP", Compression.GZIP, False, ), ( "falls_back_to_general", {OTEL_EXPORTER_OTLP_COMPRESSION: "deflate"}, OTEL_EXPORTER_OTLP_TRACES_COMPRESSION, Compression.DEFLATE, False, ), ( "per_signal_wins", { OTEL_EXPORTER_OTLP_TRACES_COMPRESSION: "gzip", OTEL_EXPORTER_OTLP_COMPRESSION: "deflate", }, OTEL_EXPORTER_OTLP_TRACES_COMPRESSION, Compression.GZIP, False, ), ( "empty_per_signal_falls_back_to_general", { OTEL_EXPORTER_OTLP_TRACES_COMPRESSION: "", OTEL_EXPORTER_OTLP_COMPRESSION: "gzip", }, OTEL_EXPORTER_OTLP_TRACES_COMPRESSION, Compression.GZIP, False, ), ( "default", {}, OTEL_EXPORTER_OTLP_TRACES_COMPRESSION, Compression.NONE, False, ), ( "invalid_warns", {OTEL_EXPORTER_OTLP_TRACES_COMPRESSION: "bogus"}, OTEL_EXPORTER_OTLP_TRACES_COMPRESSION, Compression.NONE, True, ), ] for label, env, env_var, expected, warns in cases: with self.subTest(label), patch.dict(os.environ, env, clear=True): if warns: with self.assertLogs(level=WARNING): result = _resolve_compression(env_var) else: result = _resolve_compression(env_var) self.assertEqual(result, expected) class TestBuildTransport(unittest.TestCase): def test_default_transport_factory_is_urllib3(self): with patch.dict(os.environ, {}, clear=True): result = _build_transport( None, None, None, OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE, OTEL_EXPORTER_OTLP_TRACES_CLIENT_KEY, OTEL_EXPORTER_OTLP_TRACES_CLIENT_CERTIFICATE, ) self.assertIsInstance(result, Urllib3HTTPTransport) def test_build_transport(self): cases = [ ( "explicit_args_win", { OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE: "env-cert.pem", OTEL_EXPORTER_OTLP_TRACES_CLIENT_KEY: "env-key.pem", OTEL_EXPORTER_OTLP_TRACES_CLIENT_CERTIFICATE: "env-cert2.pem", }, "arg-cert.pem", "arg-key.pem", "arg-cert2.pem", "arg-cert.pem", ("arg-cert2.pem", "arg-key.pem"), ), ( "per_signal_env_wins_over_general", { OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE: "per-signal-cert.pem", OTEL_EXPORTER_OTLP_CERTIFICATE: "general-cert.pem", OTEL_EXPORTER_OTLP_TRACES_CLIENT_KEY: "per-signal-key.pem", OTEL_EXPORTER_OTLP_CLIENT_KEY: "general-key.pem", OTEL_EXPORTER_OTLP_TRACES_CLIENT_CERTIFICATE: "per-signal-cert2.pem", OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE: "general-cert2.pem", }, None, None, None, "per-signal-cert.pem", ("per-signal-cert2.pem", "per-signal-key.pem"), ), ( "falls_back_to_general_env", { OTEL_EXPORTER_OTLP_CERTIFICATE: "general-cert.pem", OTEL_EXPORTER_OTLP_CLIENT_KEY: "general-key.pem", OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE: "general-cert2.pem", }, None, None, None, "general-cert.pem", ("general-cert2.pem", "general-key.pem"), ), ( "empty_per_signal_certificate_falls_back_to_general", { OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE: "", OTEL_EXPORTER_OTLP_CERTIFICATE: "general-cert.pem", }, None, None, None, "general-cert.pem", None, ), ( "empty_certificate_at_every_level_falls_back_to_default_true", { OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE: "", OTEL_EXPORTER_OTLP_CERTIFICATE: "", }, None, None, None, True, None, ), ( "defaults_verify_true_no_cert", {}, None, None, None, True, None, ), ( "only_cert_file_not_tupled", { OTEL_EXPORTER_OTLP_TRACES_CLIENT_CERTIFICATE: "cert2.pem", }, None, None, None, True, "cert2.pem", ), ] for ( label, env, certificate_file, client_key_file, client_certificate_file, expected_verify, expected_cert, ) in cases: with self.subTest(label), patch.dict(os.environ, env, clear=True): mock_factory = MagicMock() result = _build_transport( certificate_file, client_key_file, client_certificate_file, OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE, OTEL_EXPORTER_OTLP_TRACES_CLIENT_KEY, OTEL_EXPORTER_OTLP_TRACES_CLIENT_CERTIFICATE, transport_factory=mock_factory, ) mock_factory.assert_called_once_with(verify=expected_verify, cert=expected_cert) self.assertIs(result, mock_factory.return_value) test_log_exporter.py000066400000000000000000000463301527070070700345500ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-json-http/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # pylint: disable=protected-access,too-many-public-methods import gzip import json import os import threading import unittest import zlib from datetime import datetime, timezone from email.utils import format_datetime from unittest.mock import Mock, patch import urllib3.exceptions from mocket import Mocket, Mocketizer, mocketize from mocket.mocks.mockhttp import Entry, Response from opentelemetry._logs import LogRecord, SeverityNumber from opentelemetry.exporter.http.transport._urllib3 import ( Urllib3HTTPTransport, ) from opentelemetry.exporter.otlp.common.http import Compression from opentelemetry.exporter.otlp.json.common._log_encoder import encode_logs from opentelemetry.exporter.otlp.json.http._internal import _build_transport from opentelemetry.exporter.otlp.json.http._log_exporter import ( OTLPLogExporter, ) from opentelemetry.sdk._logs import LoggerProvider from opentelemetry.sdk._logs.export import ( InMemoryLogRecordExporter, LogRecordExportResult, SimpleLogRecordProcessor, ) from opentelemetry.sdk.environment_variables import ( OTEL_EXPORTER_OTLP_ENDPOINT, OTEL_EXPORTER_OTLP_HEADERS, OTEL_EXPORTER_OTLP_LOGS_CERTIFICATE, OTEL_EXPORTER_OTLP_LOGS_CLIENT_CERTIFICATE, OTEL_EXPORTER_OTLP_LOGS_CLIENT_KEY, OTEL_EXPORTER_OTLP_LOGS_ENDPOINT, OTEL_EXPORTER_OTLP_LOGS_HEADERS, OTEL_EXPORTER_OTLP_LOGS_TIMEOUT, OTEL_EXPORTER_OTLP_TIMEOUT, ) from opentelemetry.sdk.resources import Resource from opentelemetry.trace import ( NonRecordingSpan, SpanContext, TraceFlags, set_span_in_context, ) from . import _mock_clock _TEST_ENDPOINT = "http://localhost:4318/v1/logs" _LOGGER_NAME = "opentelemetry.exporter.otlp.json.http._log_exporter" class TestOTLPLogExporter(unittest.TestCase): def setUp(self): env_patcher = patch.dict(os.environ, {}, clear=True) env_patcher.start() self.addCleanup(env_patcher.stop) self._in_memory = InMemoryLogRecordExporter() provider = LoggerProvider() provider.add_log_record_processor(SimpleLogRecordProcessor(self._in_memory)) self._logger = provider.get_logger(__name__) def _finished_logs(self): return list(self._in_memory.get_finished_logs()) def _make_log(self, body: str = "test-log"): self._logger.emit(LogRecord(body=body, severity_number=SeverityNumber.INFO)) return self._finished_logs() @staticmethod def _mocked_shutdown_event() -> Mock: shutdown_event = Mock(spec=threading.Event) shutdown_event.is_set.return_value = False return shutdown_event @mocketize def test_export_single_log(self): Entry.single_register(Entry.POST, _TEST_ENDPOINT, status=200) exporter = OTLPLogExporter(endpoint=_TEST_ENDPOINT) logs = self._make_log("my-log") result = exporter.export(logs) self.assertEqual(result, LogRecordExportResult.SUCCESS) request = Mocket.last_request() self.assertEqual(request.method, "POST") self.assertEqual(request.path, "/v1/logs") self.assertEqual(json.loads(request.body), encode_logs(logs).to_dict()) @mocketize def test_export_multiple_logs_same_resource(self): Entry.single_register(Entry.POST, _TEST_ENDPOINT, status=200) exporter = OTLPLogExporter(endpoint=_TEST_ENDPOINT) self._logger.emit(LogRecord(body="first", severity_number=SeverityNumber.INFO)) self._logger.emit(LogRecord(body="second", severity_number=SeverityNumber.INFO)) logs = self._finished_logs() result = exporter.export(logs) self.assertEqual(result, LogRecordExportResult.SUCCESS) self.assertEqual(len(Mocket.request_list()), 1) body = json.loads(Mocket.last_request().body) self.assertEqual(body, encode_logs(logs).to_dict()) total_logs = sum(len(sl["logRecords"]) for rl in body["resourceLogs"] for sl in rl["scopeLogs"]) self.assertEqual(total_logs, 2) @mocketize def test_export_logs_different_resources(self): Entry.single_register(Entry.POST, _TEST_ENDPOINT, status=200) exporter = OTLPLogExporter(endpoint=_TEST_ENDPOINT) logs = [] for body, host in (("from-a", "a"), ("from-b", "b")): in_memory = InMemoryLogRecordExporter() provider = LoggerProvider(resource=Resource({"host": host})) provider.add_log_record_processor(SimpleLogRecordProcessor(in_memory)) logger = provider.get_logger(__name__) logger.emit(LogRecord(body=body, severity_number=SeverityNumber.INFO)) logs.extend(in_memory.get_finished_logs()) result = exporter.export(logs) self.assertEqual(result, LogRecordExportResult.SUCCESS) body = json.loads(Mocket.last_request().body) self.assertEqual(body, encode_logs(logs).to_dict()) self.assertEqual(len(body["resourceLogs"]), 2) @mocketize def test_export_rich_log(self): Entry.single_register(Entry.POST, _TEST_ENDPOINT, status=200) exporter = OTLPLogExporter(endpoint=_TEST_ENDPOINT) ctx = set_span_in_context( NonRecordingSpan( SpanContext( trace_id=0x000000000000000000000000DEADBEEF, span_id=0x00000000DEADBEF0, is_remote=True, trace_flags=TraceFlags(0x01), ) ) ) self._logger.emit( LogRecord( body="rich-log", severity_text="ERROR", severity_number=SeverityNumber.ERROR, attributes={"http.method": "GET", "http.status_code": 500}, context=ctx, ) ) logs = self._finished_logs() result = exporter.export(logs) self.assertEqual(result, LogRecordExportResult.SUCCESS) body = json.loads(Mocket.last_request().body) self.assertEqual(body, encode_logs(logs).to_dict()) @mocketize def test_export_empty_sequence(self): Entry.single_register(Entry.POST, _TEST_ENDPOINT, status=200) exporter = OTLPLogExporter(endpoint=_TEST_ENDPOINT) result = exporter.export([]) self.assertEqual(result, LogRecordExportResult.SUCCESS) body = json.loads(Mocket.last_request().body) self.assertEqual(body, encode_logs([]).to_dict()) @mocketize def test_default_endpoint_and_headers(self): Entry.single_register(Entry.POST, "http://localhost:4318/v1/logs", status=200) exporter = OTLPLogExporter() result = exporter.export(self._make_log()) self.assertEqual(result, LogRecordExportResult.SUCCESS) headers = Mocket.last_request().headers self.assertEqual(headers["content-type"], "application/json") self.assertTrue(headers["user-agent"].startswith("OTel-OTLP-JSON-Exporter-Python/")) def test_custom_endpoint(self): url = "http://custom.example:9999/v1/logs" cases = ( ("constructor", {}, {"endpoint": url}), ( "generic_env", {OTEL_EXPORTER_OTLP_ENDPOINT: "http://custom.example:9999"}, {}, ), ( "per_signal_env", {OTEL_EXPORTER_OTLP_LOGS_ENDPOINT: url}, {}, ), ) for label, env, kwargs in cases: with ( self.subTest(label), patch.dict(os.environ, env, clear=True), Mocketizer(), ): Entry.single_register(Entry.POST, url, status=200) exporter = OTLPLogExporter(**kwargs) self._in_memory.clear() result = exporter.export(self._make_log()) self.assertEqual(result, LogRecordExportResult.SUCCESS) def test_custom_headers(self): cases = ( ("constructor", {}, {"headers": {"x-api-key": "secret"}}), ( "generic_env", {OTEL_EXPORTER_OTLP_HEADERS: "x-api-key=secret"}, {}, ), ( "per_signal_env", {OTEL_EXPORTER_OTLP_LOGS_HEADERS: "x-api-key=secret"}, {}, ), ) for label, env, kwargs in cases: with ( self.subTest(label), patch.dict(os.environ, env, clear=True), Mocketizer(), ): Entry.single_register(Entry.POST, _TEST_ENDPOINT, status=200) exporter = OTLPLogExporter(endpoint=_TEST_ENDPOINT, **kwargs) self._in_memory.clear() exporter.export(self._make_log()) headers = Mocket.last_request().headers self.assertEqual(headers["x-api-key"], "secret") self.assertEqual(headers["content-type"], "application/json") self.assertTrue(headers["user-agent"].startswith("OTel-OTLP-JSON-Exporter-Python/")) @mocketize def test_custom_transport(self): Entry.single_register(Entry.POST, _TEST_ENDPOINT, status=200) custom_transport = Urllib3HTTPTransport() with patch("opentelemetry.exporter.otlp.json.http._log_exporter._build_transport") as mock_build_transport: exporter = OTLPLogExporter(endpoint=_TEST_ENDPOINT, _transport=custom_transport) mock_build_transport.assert_not_called() self.assertIs(exporter._client._transport, custom_transport) result = exporter.export(self._make_log()) self.assertEqual(result, LogRecordExportResult.SUCCESS) self.assertEqual(len(Mocket.request_list()), 1) def test_custom_timeout(self): cases = ( ("constructor", {}, {"timeout": 7.5}), ("generic_env", {OTEL_EXPORTER_OTLP_TIMEOUT: "7.5"}, {}), ( "per_signal_env", {OTEL_EXPORTER_OTLP_LOGS_TIMEOUT: "7.5"}, {}, ), ) for label, env, kwargs in cases: with ( self.subTest(label), patch.dict(os.environ, env, clear=True), Mocketizer(), ): Entry.single_register(Entry.POST, _TEST_ENDPOINT, status=200) exporter = OTLPLogExporter(endpoint=_TEST_ENDPOINT, **kwargs) self._in_memory.clear() with patch.object( exporter._client._transport, "request", wraps=exporter._client._transport.request, ) as mock_request: result = exporter.export(self._make_log()) self.assertEqual(result, LogRecordExportResult.SUCCESS) self.assertAlmostEqual(mock_request.call_args.kwargs["timeout"], 7.5, delta=0.5) @mocketize def test_certificate_args(self): Entry.single_register(Entry.POST, _TEST_ENDPOINT, status=200) with patch( "opentelemetry.exporter.otlp.json.http._log_exporter._build_transport", wraps=_build_transport, ) as mock_build_transport: exporter = OTLPLogExporter( endpoint=_TEST_ENDPOINT, certificate_file="ca.pem", client_key_file="client-key.pem", client_certificate_file="client-cert.pem", ) mock_build_transport.assert_called_once_with( "ca.pem", "client-key.pem", "client-cert.pem", OTEL_EXPORTER_OTLP_LOGS_CERTIFICATE, OTEL_EXPORTER_OTLP_LOGS_CLIENT_KEY, OTEL_EXPORTER_OTLP_LOGS_CLIENT_CERTIFICATE, ) result = exporter.export(self._make_log()) self.assertEqual(result, LogRecordExportResult.SUCCESS) def test_compression_options(self): cases = ( (Compression.NONE, None, lambda data: data), (Compression.GZIP, "gzip", gzip.decompress), (Compression.DEFLATE, "deflate", zlib.decompress), ) for compression, expected_encoding, decompress in cases: with self.subTest(compression=compression), Mocketizer(): Entry.single_register(Entry.POST, _TEST_ENDPOINT, status=200) exporter = OTLPLogExporter(endpoint=_TEST_ENDPOINT, compression=compression) transport = exporter._client._transport self._in_memory.clear() logs = self._make_log() with patch.object(transport, "request", wraps=transport.request) as mock_request: result = exporter.export(logs) self.assertEqual(result, LogRecordExportResult.SUCCESS) sent_headers = mock_request.call_args.kwargs["headers"] if expected_encoding is None: self.assertNotIn("Content-Encoding", sent_headers) else: self.assertEqual(sent_headers["Content-Encoding"], expected_encoding) sent_data = mock_request.call_args.kwargs["data"] decompressed = decompress(sent_data) self.assertEqual(json.loads(decompressed), encode_logs(logs).to_dict()) def test_export_retryable_status_codes(self): for status_code in (429, 502, 503, 504): with self.subTest(status_code=status_code), Mocketizer(): Entry.register( Entry.POST, _TEST_ENDPOINT, Response(status=status_code), Response(status=200), ) exporter = OTLPLogExporter(endpoint=_TEST_ENDPOINT, timeout=30.0) shutdown_event = self._mocked_shutdown_event() exporter._client._shutdown_event = shutdown_event self._in_memory.clear() with _mock_clock(shutdown_event): result = exporter.export(self._make_log()) self.assertEqual(result, LogRecordExportResult.SUCCESS) self.assertEqual(len(Mocket.request_list()), 2) shutdown_event.wait.assert_called_once() (wait_arg,) = shutdown_event.wait.call_args.args self.assertGreaterEqual(wait_arg, 0.8) self.assertLessEqual(wait_arg, 1.2) def test_export_non_retryable_status_codes(self): for status_code in (400, 401, 403, 404, 408, 500, 501): with self.subTest(status_code=status_code), Mocketizer(): Entry.single_register(Entry.POST, _TEST_ENDPOINT, status=status_code) exporter = OTLPLogExporter(endpoint=_TEST_ENDPOINT) self._in_memory.clear() result = exporter.export(self._make_log()) self.assertEqual(result, LogRecordExportResult.FAILURE) self.assertEqual(len(Mocket.request_list()), 1) @mocketize def test_export_max_retries(self): Entry.register( Entry.POST, _TEST_ENDPOINT, *[Response(status=503)] * 6, ) exporter = OTLPLogExporter(endpoint=_TEST_ENDPOINT, timeout=1000.0) shutdown_event = self._mocked_shutdown_event() exporter._client._shutdown_event = shutdown_event with _mock_clock(shutdown_event): result = exporter.export(self._make_log()) self.assertEqual(result, LogRecordExportResult.FAILURE) self.assertEqual(len(Mocket.request_list()), 6) self.assertEqual(shutdown_event.wait.call_count, 5) @mocketize def test_export_retry_after_header(self): Entry.register( Entry.POST, _TEST_ENDPOINT, Response(status=429, headers={"Retry-After": "5"}), Response(status=200), ) exporter = OTLPLogExporter(endpoint=_TEST_ENDPOINT, timeout=60.0) shutdown_event = self._mocked_shutdown_event() shutdown_event.wait.return_value = False exporter._client._shutdown_event = shutdown_event result = exporter.export(self._make_log()) self.assertEqual(result, LogRecordExportResult.SUCCESS) self.assertEqual(len(Mocket.request_list()), 2) shutdown_event.wait.assert_called_once_with(5.0) @mocketize def test_export_retry_after_header_http_date(self): base = 1_700_000_000.0 retry_at = format_datetime(datetime.fromtimestamp(base + 30, timezone.utc), usegmt=True) Entry.register( Entry.POST, _TEST_ENDPOINT, Response(status=503, headers={"Retry-After": retry_at}), Response(status=200), ) exporter = OTLPLogExporter(endpoint=_TEST_ENDPOINT, timeout=120.0) shutdown_event = self._mocked_shutdown_event() shutdown_event.wait.return_value = False exporter._client._shutdown_event = shutdown_event with patch( "opentelemetry.exporter.otlp.common.http.time.time", return_value=base, ): result = exporter.export(self._make_log()) self.assertEqual(result, LogRecordExportResult.SUCCESS) self.assertEqual(len(Mocket.request_list()), 2) shutdown_event.wait.assert_called_once_with(30.0) @mocketize def test_export_connection_error(self): Entry.register( Entry.POST, _TEST_ENDPOINT, urllib3.exceptions.ProtocolError("simulated reset"), Response(status=200), ) exporter = OTLPLogExporter(endpoint=_TEST_ENDPOINT, timeout=5.0) result = exporter.export(self._make_log()) self.assertEqual(result, LogRecordExportResult.SUCCESS) self.assertEqual(len(Mocket.request_list()), 2) @mocketize def test_export_after_shutdown(self): exporter = OTLPLogExporter(endpoint=_TEST_ENDPOINT) exporter.shutdown() with self.assertLogs(_LOGGER_NAME, level="WARNING"): result = exporter.export(self._make_log()) self.assertEqual(result, LogRecordExportResult.FAILURE) self.assertIsNone(Mocket.last_request()) def test_shutdown_idempotent(self): exporter = OTLPLogExporter(endpoint=_TEST_ENDPOINT) exporter.shutdown() with self.assertLogs(_LOGGER_NAME, level="WARNING"): exporter.shutdown() # pylint: disable-next=no-self-use def test_shutdown_closes_transport(self): exporter = OTLPLogExporter(endpoint=_TEST_ENDPOINT) with patch.object(exporter._client._transport, "close") as mock_close: exporter.shutdown() mock_close.assert_called_once() @mocketize def test_force_flush(self): Entry.single_register(Entry.POST, _TEST_ENDPOINT, status=200) exporter = OTLPLogExporter(endpoint=_TEST_ENDPOINT) self.assertTrue(exporter.force_flush()) exporter.export(self._make_log()) self.assertTrue(exporter.force_flush()) @mocketize def test_export_encoding_failure(self): exporter = OTLPLogExporter(endpoint=_TEST_ENDPOINT) with ( patch( "opentelemetry.exporter.otlp.json.http._log_exporter.encode_logs", side_effect=ValueError("boom"), ), self.assertLogs(_LOGGER_NAME, level="ERROR"), ): result = exporter.export(self._make_log()) self.assertEqual(result, LogRecordExportResult.FAILURE) self.assertIsNone(Mocket.last_request()) test_metric_exporter.py000066400000000000000000000534251527070070700352550ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-json-http/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # pylint: disable=protected-access,too-many-public-methods import gzip import json import os import threading import unittest import zlib from datetime import datetime, timezone from email.utils import format_datetime from unittest.mock import Mock, patch import urllib3.exceptions from mocket import Mocket, Mocketizer, mocketize from mocket.mocks.mockhttp import Entry, Response from opentelemetry.attributes import BoundedAttributes from opentelemetry.exporter.http.transport._urllib3 import ( Urllib3HTTPTransport, ) from opentelemetry.exporter.otlp.common.http import Compression from opentelemetry.exporter.otlp.json.common._internal.metrics_encoder import ( split_metrics_data, ) from opentelemetry.exporter.otlp.json.common.metrics_encoder import ( encode_metrics, ) from opentelemetry.exporter.otlp.json.http._internal import _build_transport from opentelemetry.exporter.otlp.json.http.metric_exporter import ( OTLPMetricExporter, ) from opentelemetry.sdk.environment_variables import ( OTEL_EXPORTER_OTLP_ENDPOINT, OTEL_EXPORTER_OTLP_HEADERS, OTEL_EXPORTER_OTLP_METRICS_CERTIFICATE, OTEL_EXPORTER_OTLP_METRICS_CLIENT_CERTIFICATE, OTEL_EXPORTER_OTLP_METRICS_CLIENT_KEY, OTEL_EXPORTER_OTLP_METRICS_ENDPOINT, OTEL_EXPORTER_OTLP_METRICS_HEADERS, OTEL_EXPORTER_OTLP_METRICS_TIMEOUT, OTEL_EXPORTER_OTLP_TIMEOUT, ) from opentelemetry.sdk.metrics.export import ( AggregationTemporality, Metric, MetricExportResult, MetricsData, NumberDataPoint, ResourceMetrics, ScopeMetrics, Sum, ) from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.util.instrumentation import InstrumentationScope from opentelemetry.test.metrictestutil import ( _generate_gauge, _generate_histogram, _generate_sum, ) from . import _mock_clock _TEST_ENDPOINT = "http://localhost:4318/v1/metrics" _LOGGER_NAME = "opentelemetry.exporter.otlp.json.http.metric_exporter" def _make_metrics_data( metric: Metric | None = None, resource_attrs: dict | None = None, ) -> MetricsData: return MetricsData( resource_metrics=[ ResourceMetrics( resource=Resource(resource_attrs or {"service.name": "test"}), scope_metrics=[ ScopeMetrics( scope=InstrumentationScope("test-scope", "1.0"), metrics=[metric or _generate_sum("requests", 42)], schema_url="", ) ], schema_url="", ) ] ) class TestOTLPMetricExporter(unittest.TestCase): def setUp(self): env_patcher = patch.dict(os.environ, {}, clear=True) env_patcher.start() self.addCleanup(env_patcher.stop) @staticmethod def _mocked_shutdown_event() -> Mock: shutdown_event = Mock(spec=threading.Event) shutdown_event.is_set.return_value = False return shutdown_event @mocketize def test_export_single_metric(self): Entry.single_register(Entry.POST, _TEST_ENDPOINT, status=200) exporter = OTLPMetricExporter(endpoint=_TEST_ENDPOINT) metrics_data = _make_metrics_data() result = exporter.export(metrics_data) self.assertEqual(result, MetricExportResult.SUCCESS) request = Mocket.last_request() self.assertEqual(request.method, "POST") self.assertEqual(request.path, "/v1/metrics") self.assertEqual(json.loads(request.body), encode_metrics(metrics_data).to_dict()) @mocketize def test_export_multiple_metrics_same_resource(self): Entry.single_register(Entry.POST, _TEST_ENDPOINT, status=200) exporter = OTLPMetricExporter(endpoint=_TEST_ENDPOINT) metrics_data = MetricsData( resource_metrics=[ ResourceMetrics( resource=Resource({"service.name": "test"}), scope_metrics=[ ScopeMetrics( scope=InstrumentationScope("test-scope", "1.0"), metrics=[ _generate_sum("counter", 1), _generate_gauge("gauge", 2), ], schema_url="", ) ], schema_url="", ) ] ) result = exporter.export(metrics_data) self.assertEqual(result, MetricExportResult.SUCCESS) self.assertEqual(len(Mocket.request_list()), 1) body = json.loads(Mocket.last_request().body) self.assertEqual(body, encode_metrics(metrics_data).to_dict()) total_metrics = sum(len(sm["metrics"]) for rm in body["resourceMetrics"] for sm in rm["scopeMetrics"]) self.assertEqual(total_metrics, 2) @mocketize def test_export_metrics_different_resources(self): Entry.single_register(Entry.POST, _TEST_ENDPOINT, status=200) exporter = OTLPMetricExporter(endpoint=_TEST_ENDPOINT) metrics_data = MetricsData( resource_metrics=[ ResourceMetrics( resource=Resource({"host": "a"}), scope_metrics=[ ScopeMetrics( scope=InstrumentationScope("test-scope", "1.0"), metrics=[_generate_sum("from-a", 1)], schema_url="", ) ], schema_url="", ), ResourceMetrics( resource=Resource({"host": "b"}), scope_metrics=[ ScopeMetrics( scope=InstrumentationScope("test-scope", "1.0"), metrics=[_generate_sum("from-b", 1)], schema_url="", ) ], schema_url="", ), ] ) result = exporter.export(metrics_data) self.assertEqual(result, MetricExportResult.SUCCESS) body = json.loads(Mocket.last_request().body) self.assertEqual(body, encode_metrics(metrics_data).to_dict()) self.assertEqual(len(body["resourceMetrics"]), 2) @mocketize def test_export_rich_metrics(self): Entry.single_register(Entry.POST, _TEST_ENDPOINT, status=200) exporter = OTLPMetricExporter(endpoint=_TEST_ENDPOINT) metrics_data = _make_metrics_data( _generate_histogram( "req.duration", attributes=BoundedAttributes( attributes={ "http.method": "GET", "http.status_code": 200, } ), ) ) result = exporter.export(metrics_data) self.assertEqual(result, MetricExportResult.SUCCESS) body = json.loads(Mocket.last_request().body) self.assertEqual(body, encode_metrics(metrics_data).to_dict()) @mocketize def test_export_empty_metrics_data(self): Entry.single_register(Entry.POST, _TEST_ENDPOINT, status=200) exporter = OTLPMetricExporter(endpoint=_TEST_ENDPOINT) metrics_data = MetricsData(resource_metrics=[]) result = exporter.export(metrics_data) self.assertEqual(result, MetricExportResult.SUCCESS) body = json.loads(Mocket.last_request().body) self.assertEqual(body, encode_metrics(metrics_data).to_dict()) @mocketize def test_default_endpoint_and_headers(self): Entry.single_register(Entry.POST, "http://localhost:4318/v1/metrics", status=200) exporter = OTLPMetricExporter() result = exporter.export(_make_metrics_data()) self.assertEqual(result, MetricExportResult.SUCCESS) headers = Mocket.last_request().headers self.assertEqual(headers["content-type"], "application/json") self.assertTrue(headers["user-agent"].startswith("OTel-OTLP-JSON-Exporter-Python/")) def test_custom_endpoint(self): url = "http://custom.example:9999/v1/metrics" cases = ( ("constructor", {}, {"endpoint": url}), ( "generic_env", {OTEL_EXPORTER_OTLP_ENDPOINT: "http://custom.example:9999"}, {}, ), ( "per_signal_env", {OTEL_EXPORTER_OTLP_METRICS_ENDPOINT: url}, {}, ), ) for label, env, kwargs in cases: with ( self.subTest(label), patch.dict(os.environ, env, clear=True), Mocketizer(), ): Entry.single_register(Entry.POST, url, status=200) exporter = OTLPMetricExporter(**kwargs) result = exporter.export(_make_metrics_data()) self.assertEqual(result, MetricExportResult.SUCCESS) def test_custom_headers(self): cases = ( ("constructor", {}, {"headers": {"x-api-key": "secret"}}), ( "generic_env", {OTEL_EXPORTER_OTLP_HEADERS: "x-api-key=secret"}, {}, ), ( "per_signal_env", {OTEL_EXPORTER_OTLP_METRICS_HEADERS: "x-api-key=secret"}, {}, ), ) for label, env, kwargs in cases: with ( self.subTest(label), patch.dict(os.environ, env, clear=True), Mocketizer(), ): Entry.single_register(Entry.POST, _TEST_ENDPOINT, status=200) exporter = OTLPMetricExporter(endpoint=_TEST_ENDPOINT, **kwargs) exporter.export(_make_metrics_data()) headers = Mocket.last_request().headers self.assertEqual(headers["x-api-key"], "secret") self.assertEqual(headers["content-type"], "application/json") self.assertTrue(headers["user-agent"].startswith("OTel-OTLP-JSON-Exporter-Python/")) @mocketize def test_custom_transport(self): Entry.single_register(Entry.POST, _TEST_ENDPOINT, status=200) custom_transport = Urllib3HTTPTransport() with patch("opentelemetry.exporter.otlp.json.http.metric_exporter._build_transport") as mock_build_transport: exporter = OTLPMetricExporter(endpoint=_TEST_ENDPOINT, _transport=custom_transport) mock_build_transport.assert_not_called() self.assertIs(exporter._client._transport, custom_transport) result = exporter.export(_make_metrics_data()) self.assertEqual(result, MetricExportResult.SUCCESS) self.assertEqual(len(Mocket.request_list()), 1) def test_custom_timeout(self): cases = ( ("constructor", {}, {"timeout": 7.5}), ("generic_env", {OTEL_EXPORTER_OTLP_TIMEOUT: "7.5"}, {}), ( "per_signal_env", {OTEL_EXPORTER_OTLP_METRICS_TIMEOUT: "7.5"}, {}, ), ) for label, env, kwargs in cases: with ( self.subTest(label), patch.dict(os.environ, env, clear=True), Mocketizer(), ): Entry.single_register(Entry.POST, _TEST_ENDPOINT, status=200) exporter = OTLPMetricExporter(endpoint=_TEST_ENDPOINT, **kwargs) with patch.object( exporter._client._transport, "request", wraps=exporter._client._transport.request, ) as mock_request: result = exporter.export(_make_metrics_data()) self.assertEqual(result, MetricExportResult.SUCCESS) self.assertAlmostEqual(mock_request.call_args.kwargs["timeout"], 7.5, delta=0.5) @mocketize def test_certificate_args(self): Entry.single_register(Entry.POST, _TEST_ENDPOINT, status=200) with patch( "opentelemetry.exporter.otlp.json.http.metric_exporter._build_transport", wraps=_build_transport, ) as mock_build_transport: exporter = OTLPMetricExporter( endpoint=_TEST_ENDPOINT, certificate_file="ca.pem", client_key_file="client-key.pem", client_certificate_file="client-cert.pem", ) mock_build_transport.assert_called_once_with( "ca.pem", "client-key.pem", "client-cert.pem", OTEL_EXPORTER_OTLP_METRICS_CERTIFICATE, OTEL_EXPORTER_OTLP_METRICS_CLIENT_KEY, OTEL_EXPORTER_OTLP_METRICS_CLIENT_CERTIFICATE, ) result = exporter.export(_make_metrics_data()) self.assertEqual(result, MetricExportResult.SUCCESS) def test_compression_options(self): cases = ( (Compression.NONE, None, lambda data: data), (Compression.GZIP, "gzip", gzip.decompress), (Compression.DEFLATE, "deflate", zlib.decompress), ) for compression, expected_encoding, decompress in cases: with self.subTest(compression=compression), Mocketizer(): Entry.single_register(Entry.POST, _TEST_ENDPOINT, status=200) exporter = OTLPMetricExporter(endpoint=_TEST_ENDPOINT, compression=compression) transport = exporter._client._transport metrics_data = _make_metrics_data() with patch.object(transport, "request", wraps=transport.request) as mock_request: result = exporter.export(metrics_data) self.assertEqual(result, MetricExportResult.SUCCESS) sent_headers = mock_request.call_args.kwargs["headers"] if expected_encoding is None: self.assertNotIn("Content-Encoding", sent_headers) else: self.assertEqual(sent_headers["Content-Encoding"], expected_encoding) sent_data = mock_request.call_args.kwargs["data"] decompressed = decompress(sent_data) self.assertEqual( json.loads(decompressed), encode_metrics(metrics_data).to_dict(), ) @mocketize def test_export_batch_splitting(self): Entry.register( Entry.POST, _TEST_ENDPOINT, Response(status=200), Response(status=200), Response(status=200), ) exporter = OTLPMetricExporter(endpoint=_TEST_ENDPOINT, max_export_batch_size=2) data_points = [ NumberDataPoint( attributes=BoundedAttributes(attributes={"i": i}), start_time_unix_nano=1641946015139533244, time_unix_nano=1641946016139533244, value=i, ) for i in range(5) ] metric = Metric( name="requests", description="foo", unit="s", data=Sum( data_points=data_points, aggregation_temporality=AggregationTemporality.CUMULATIVE, is_monotonic=True, ), ) metrics_data = _make_metrics_data(metric) expected_batches = [batch.to_dict() for batch in split_metrics_data(encode_metrics(metrics_data), 2)] result = exporter.export(metrics_data) self.assertEqual(result, MetricExportResult.SUCCESS) requests = Mocket.request_list() self.assertEqual(len(requests), len(expected_batches)) self.assertEqual( [json.loads(request.body) for request in requests], expected_batches, ) def test_export_retryable_status_codes(self): for status_code in (429, 502, 503, 504): with self.subTest(status_code=status_code), Mocketizer(): Entry.register( Entry.POST, _TEST_ENDPOINT, Response(status=status_code), Response(status=200), ) exporter = OTLPMetricExporter(endpoint=_TEST_ENDPOINT, timeout=30.0) shutdown_event = self._mocked_shutdown_event() exporter._client._shutdown_event = shutdown_event with _mock_clock(shutdown_event): result = exporter.export(_make_metrics_data()) self.assertEqual(result, MetricExportResult.SUCCESS) self.assertEqual(len(Mocket.request_list()), 2) shutdown_event.wait.assert_called_once() (wait_arg,) = shutdown_event.wait.call_args.args self.assertGreaterEqual(wait_arg, 0.8) self.assertLessEqual(wait_arg, 1.2) def test_export_non_retryable_status_codes(self): for status_code in (400, 401, 403, 404, 408, 500, 501): with self.subTest(status_code=status_code), Mocketizer(): Entry.single_register(Entry.POST, _TEST_ENDPOINT, status=status_code) exporter = OTLPMetricExporter(endpoint=_TEST_ENDPOINT) result = exporter.export(_make_metrics_data()) self.assertEqual(result, MetricExportResult.FAILURE) self.assertEqual(len(Mocket.request_list()), 1) @mocketize def test_export_max_retries(self): Entry.register( Entry.POST, _TEST_ENDPOINT, *[Response(status=503)] * 6, ) exporter = OTLPMetricExporter(endpoint=_TEST_ENDPOINT, timeout=1000.0) shutdown_event = self._mocked_shutdown_event() exporter._client._shutdown_event = shutdown_event with _mock_clock(shutdown_event): result = exporter.export(_make_metrics_data()) self.assertEqual(result, MetricExportResult.FAILURE) self.assertEqual(len(Mocket.request_list()), 6) self.assertEqual(shutdown_event.wait.call_count, 5) @mocketize def test_export_retry_after_header(self): Entry.register( Entry.POST, _TEST_ENDPOINT, Response(status=429, headers={"Retry-After": "5"}), Response(status=200), ) exporter = OTLPMetricExporter(endpoint=_TEST_ENDPOINT, timeout=60.0) shutdown_event = self._mocked_shutdown_event() shutdown_event.wait.return_value = False exporter._client._shutdown_event = shutdown_event result = exporter.export(_make_metrics_data()) self.assertEqual(result, MetricExportResult.SUCCESS) self.assertEqual(len(Mocket.request_list()), 2) shutdown_event.wait.assert_called_once_with(5.0) @mocketize def test_export_retry_after_header_http_date(self): base = 1_700_000_000.0 retry_at = format_datetime(datetime.fromtimestamp(base + 30, timezone.utc), usegmt=True) Entry.register( Entry.POST, _TEST_ENDPOINT, Response(status=503, headers={"Retry-After": retry_at}), Response(status=200), ) exporter = OTLPMetricExporter(endpoint=_TEST_ENDPOINT, timeout=120.0) shutdown_event = self._mocked_shutdown_event() shutdown_event.wait.return_value = False exporter._client._shutdown_event = shutdown_event with patch( "opentelemetry.exporter.otlp.common.http.time.time", return_value=base, ): result = exporter.export(_make_metrics_data()) self.assertEqual(result, MetricExportResult.SUCCESS) self.assertEqual(len(Mocket.request_list()), 2) shutdown_event.wait.assert_called_once_with(30.0) @mocketize def test_export_connection_error(self): Entry.register( Entry.POST, _TEST_ENDPOINT, urllib3.exceptions.ProtocolError("simulated reset"), Response(status=200), ) exporter = OTLPMetricExporter(endpoint=_TEST_ENDPOINT, timeout=5.0) result = exporter.export(_make_metrics_data()) self.assertEqual(result, MetricExportResult.SUCCESS) self.assertEqual(len(Mocket.request_list()), 2) @mocketize def test_export_after_shutdown(self): exporter = OTLPMetricExporter(endpoint=_TEST_ENDPOINT) exporter.shutdown() with self.assertLogs(_LOGGER_NAME, level="WARNING"): result = exporter.export(_make_metrics_data()) self.assertEqual(result, MetricExportResult.FAILURE) self.assertIsNone(Mocket.last_request()) def test_shutdown_idempotent(self): exporter = OTLPMetricExporter(endpoint=_TEST_ENDPOINT) exporter.shutdown() with self.assertLogs(_LOGGER_NAME, level="WARNING"): exporter.shutdown() # pylint: disable-next=no-self-use def test_shutdown_closes_transport(self): exporter = OTLPMetricExporter(endpoint=_TEST_ENDPOINT) with patch.object(exporter._client._transport, "close") as mock_close: exporter.shutdown() mock_close.assert_called_once() @mocketize def test_force_flush(self): Entry.single_register(Entry.POST, _TEST_ENDPOINT, status=200) exporter = OTLPMetricExporter(endpoint=_TEST_ENDPOINT) self.assertTrue(exporter.force_flush()) exporter.export(_make_metrics_data()) self.assertTrue(exporter.force_flush()) @mocketize def test_export_encoding_failure(self): exporter = OTLPMetricExporter(endpoint=_TEST_ENDPOINT) with ( patch( "opentelemetry.exporter.otlp.json.http.metric_exporter.encode_metrics", side_effect=ValueError("boom"), ), self.assertLogs(_LOGGER_NAME, level="ERROR"), ): result = exporter.export(_make_metrics_data()) self.assertEqual(result, MetricExportResult.FAILURE) self.assertIsNone(Mocket.last_request()) test_trace_exporter.py000066400000000000000000000462451527070070700350720ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-json-http/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # pylint: disable=protected-access,too-many-public-methods import gzip import json import os import threading import unittest import zlib from datetime import datetime, timezone from email.utils import format_datetime from unittest.mock import Mock, patch import urllib3.exceptions from mocket import Mocket, Mocketizer, mocketize from mocket.mocks.mockhttp import Entry, Response from opentelemetry.exporter.http.transport._urllib3 import ( Urllib3HTTPTransport, ) from opentelemetry.exporter.otlp.common.http import Compression from opentelemetry.exporter.otlp.json.common.trace_encoder import encode_spans from opentelemetry.exporter.otlp.json.http._internal import _build_transport from opentelemetry.exporter.otlp.json.http.trace_exporter import ( OTLPSpanExporter, ) from opentelemetry.sdk.environment_variables import ( OTEL_EXPORTER_OTLP_ENDPOINT, OTEL_EXPORTER_OTLP_HEADERS, OTEL_EXPORTER_OTLP_TIMEOUT, OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE, OTEL_EXPORTER_OTLP_TRACES_CLIENT_CERTIFICATE, OTEL_EXPORTER_OTLP_TRACES_CLIENT_KEY, OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, OTEL_EXPORTER_OTLP_TRACES_HEADERS, OTEL_EXPORTER_OTLP_TRACES_TIMEOUT, ) from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import ( SimpleSpanProcessor, SpanExportResult, ) from opentelemetry.sdk.trace.export.in_memory_span_exporter import ( InMemorySpanExporter, ) from opentelemetry.trace import Link, SpanContext, StatusCode, TraceFlags from . import _mock_clock _TEST_ENDPOINT = "http://localhost:4318/v1/traces" _LOGGER_NAME = "opentelemetry.exporter.otlp.json.http.trace_exporter" class TestOTLPSpanExporter(unittest.TestCase): def setUp(self): env_patcher = patch.dict(os.environ, {}, clear=True) env_patcher.start() self.addCleanup(env_patcher.stop) self._in_memory = InMemorySpanExporter() provider = TracerProvider() provider.add_span_processor(SimpleSpanProcessor(self._in_memory)) self._tracer = provider.get_tracer(__name__) def _finished_spans(self): return list(self._in_memory.get_finished_spans()) def _make_span(self, name: str = "test-span"): with self._tracer.start_as_current_span(name): pass return self._finished_spans() @staticmethod def _mocked_shutdown_event() -> Mock: shutdown_event = Mock(spec=threading.Event) shutdown_event.is_set.return_value = False return shutdown_event @mocketize def test_export_single_span(self): Entry.single_register(Entry.POST, _TEST_ENDPOINT, status=200) exporter = OTLPSpanExporter(endpoint=_TEST_ENDPOINT) spans = self._make_span("my-span") result = exporter.export(spans) self.assertEqual(result, SpanExportResult.SUCCESS) request = Mocket.last_request() self.assertEqual(request.method, "POST") self.assertEqual(request.path, "/v1/traces") self.assertEqual(json.loads(request.body), encode_spans(spans).to_dict()) @mocketize def test_export_multiple_spans_same_resource(self): Entry.single_register(Entry.POST, _TEST_ENDPOINT, status=200) exporter = OTLPSpanExporter(endpoint=_TEST_ENDPOINT) with self._tracer.start_as_current_span("first"): pass with self._tracer.start_as_current_span("second"): pass spans = self._finished_spans() result = exporter.export(spans) self.assertEqual(result, SpanExportResult.SUCCESS) self.assertEqual(len(Mocket.request_list()), 1) body = json.loads(Mocket.last_request().body) self.assertEqual(body, encode_spans(spans).to_dict()) total_spans = sum(len(ss["spans"]) for rs in body["resourceSpans"] for ss in rs["scopeSpans"]) self.assertEqual(total_spans, 2) @mocketize def test_export_spans_different_resources(self): Entry.single_register(Entry.POST, _TEST_ENDPOINT, status=200) exporter = OTLPSpanExporter(endpoint=_TEST_ENDPOINT) spans = [] for name, host in (("from-a", "a"), ("from-b", "b")): in_memory = InMemorySpanExporter() provider = TracerProvider(resource=Resource({"host": host})) provider.add_span_processor(SimpleSpanProcessor(in_memory)) tracer = provider.get_tracer(__name__) with tracer.start_as_current_span(name): pass spans.extend(in_memory.get_finished_spans()) result = exporter.export(spans) self.assertEqual(result, SpanExportResult.SUCCESS) body = json.loads(Mocket.last_request().body) self.assertEqual(body, encode_spans(spans).to_dict()) self.assertEqual(len(body["resourceSpans"]), 2) @mocketize def test_export_rich_span(self): Entry.single_register(Entry.POST, _TEST_ENDPOINT, status=200) exporter = OTLPSpanExporter(endpoint=_TEST_ENDPOINT) link_ctx = SpanContext( trace_id=0x000000000000000000000000DEADBEEF, span_id=0x00000000DEADBEF0, is_remote=True, trace_flags=TraceFlags(0x01), ) with self._tracer.start_as_current_span( "rich-span", links=[Link(link_ctx, {"link.order": 1})], ) as span: span.set_attributes( { "http.method": "GET", "http.status_code": 200, "http.retried": False, } ) span.add_event("cache-miss", {"cache.key": "user:42"}) span.set_status(StatusCode.ERROR, "boom") spans = self._finished_spans() result = exporter.export(spans) self.assertEqual(result, SpanExportResult.SUCCESS) body = json.loads(Mocket.last_request().body) self.assertEqual(body, encode_spans(spans).to_dict()) @mocketize def test_export_empty_sequence(self): Entry.single_register(Entry.POST, _TEST_ENDPOINT, status=200) exporter = OTLPSpanExporter(endpoint=_TEST_ENDPOINT) result = exporter.export([]) self.assertEqual(result, SpanExportResult.SUCCESS) body = json.loads(Mocket.last_request().body) self.assertEqual(body, encode_spans([]).to_dict()) @mocketize def test_default_endpoint_and_headers(self): Entry.single_register(Entry.POST, "http://localhost:4318/v1/traces", status=200) exporter = OTLPSpanExporter() result = exporter.export(self._make_span()) self.assertEqual(result, SpanExportResult.SUCCESS) headers = Mocket.last_request().headers self.assertEqual(headers["content-type"], "application/json") self.assertTrue(headers["user-agent"].startswith("OTel-OTLP-JSON-Exporter-Python/")) def test_custom_endpoint(self): url = "http://custom.example:9999/v1/traces" cases = ( ("constructor", {}, {"endpoint": url}), ( "generic_env", {OTEL_EXPORTER_OTLP_ENDPOINT: "http://custom.example:9999"}, {}, ), ( "per_signal_env", {OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: url}, {}, ), ) for label, env, kwargs in cases: with ( self.subTest(label), patch.dict(os.environ, env, clear=True), Mocketizer(), ): Entry.single_register(Entry.POST, url, status=200) exporter = OTLPSpanExporter(**kwargs) self._in_memory.clear() result = exporter.export(self._make_span()) self.assertEqual(result, SpanExportResult.SUCCESS) def test_custom_headers(self): cases = ( ("constructor", {}, {"headers": {"x-api-key": "secret"}}), ( "generic_env", {OTEL_EXPORTER_OTLP_HEADERS: "x-api-key=secret"}, {}, ), ( "per_signal_env", {OTEL_EXPORTER_OTLP_TRACES_HEADERS: "x-api-key=secret"}, {}, ), ) for label, env, kwargs in cases: with ( self.subTest(label), patch.dict(os.environ, env, clear=True), Mocketizer(), ): Entry.single_register(Entry.POST, _TEST_ENDPOINT, status=200) exporter = OTLPSpanExporter(endpoint=_TEST_ENDPOINT, **kwargs) self._in_memory.clear() exporter.export(self._make_span()) headers = Mocket.last_request().headers self.assertEqual(headers["x-api-key"], "secret") self.assertEqual(headers["content-type"], "application/json") self.assertTrue(headers["user-agent"].startswith("OTel-OTLP-JSON-Exporter-Python/")) @mocketize def test_custom_transport(self): Entry.single_register(Entry.POST, _TEST_ENDPOINT, status=200) custom_transport = Urllib3HTTPTransport() with patch("opentelemetry.exporter.otlp.json.http.trace_exporter._build_transport") as mock_build_transport: exporter = OTLPSpanExporter(endpoint=_TEST_ENDPOINT, _transport=custom_transport) mock_build_transport.assert_not_called() self.assertIs(exporter._client._transport, custom_transport) result = exporter.export(self._make_span()) self.assertEqual(result, SpanExportResult.SUCCESS) self.assertEqual(len(Mocket.request_list()), 1) def test_custom_timeout(self): cases = ( ("constructor", {}, {"timeout": 7.5}), ("generic_env", {OTEL_EXPORTER_OTLP_TIMEOUT: "7.5"}, {}), ( "per_signal_env", {OTEL_EXPORTER_OTLP_TRACES_TIMEOUT: "7.5"}, {}, ), ) for label, env, kwargs in cases: with ( self.subTest(label), patch.dict(os.environ, env, clear=True), Mocketizer(), ): Entry.single_register(Entry.POST, _TEST_ENDPOINT, status=200) exporter = OTLPSpanExporter(endpoint=_TEST_ENDPOINT, **kwargs) self._in_memory.clear() with patch.object( exporter._client._transport, "request", wraps=exporter._client._transport.request, ) as mock_request: result = exporter.export(self._make_span()) self.assertEqual(result, SpanExportResult.SUCCESS) self.assertAlmostEqual(mock_request.call_args.kwargs["timeout"], 7.5, delta=0.5) @mocketize def test_certificate_args(self): Entry.single_register(Entry.POST, _TEST_ENDPOINT, status=200) with patch( "opentelemetry.exporter.otlp.json.http.trace_exporter._build_transport", wraps=_build_transport, ) as mock_build_transport: exporter = OTLPSpanExporter( endpoint=_TEST_ENDPOINT, certificate_file="ca.pem", client_key_file="client-key.pem", client_certificate_file="client-cert.pem", ) mock_build_transport.assert_called_once_with( "ca.pem", "client-key.pem", "client-cert.pem", OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE, OTEL_EXPORTER_OTLP_TRACES_CLIENT_KEY, OTEL_EXPORTER_OTLP_TRACES_CLIENT_CERTIFICATE, ) result = exporter.export(self._make_span()) self.assertEqual(result, SpanExportResult.SUCCESS) def test_compression_options(self): cases = ( (Compression.NONE, None, lambda data: data), (Compression.GZIP, "gzip", gzip.decompress), (Compression.DEFLATE, "deflate", zlib.decompress), ) for compression, expected_encoding, decompress in cases: with self.subTest(compression=compression), Mocketizer(): Entry.single_register(Entry.POST, _TEST_ENDPOINT, status=200) exporter = OTLPSpanExporter(endpoint=_TEST_ENDPOINT, compression=compression) transport = exporter._client._transport self._in_memory.clear() spans = self._make_span() with patch.object(transport, "request", wraps=transport.request) as mock_request: result = exporter.export(spans) self.assertEqual(result, SpanExportResult.SUCCESS) sent_headers = mock_request.call_args.kwargs["headers"] if expected_encoding is None: self.assertNotIn("Content-Encoding", sent_headers) else: self.assertEqual(sent_headers["Content-Encoding"], expected_encoding) sent_data = mock_request.call_args.kwargs["data"] decompressed = decompress(sent_data) self.assertEqual(json.loads(decompressed), encode_spans(spans).to_dict()) def test_export_retryable_status_codes(self): for status_code in (429, 502, 503, 504): with self.subTest(status_code=status_code), Mocketizer(): Entry.register( Entry.POST, _TEST_ENDPOINT, Response(status=status_code), Response(status=200), ) exporter = OTLPSpanExporter(endpoint=_TEST_ENDPOINT, timeout=30.0) shutdown_event = self._mocked_shutdown_event() exporter._client._shutdown_event = shutdown_event self._in_memory.clear() with _mock_clock(shutdown_event): result = exporter.export(self._make_span()) self.assertEqual(result, SpanExportResult.SUCCESS) self.assertEqual(len(Mocket.request_list()), 2) shutdown_event.wait.assert_called_once() (wait_arg,) = shutdown_event.wait.call_args.args self.assertGreaterEqual(wait_arg, 0.8) self.assertLessEqual(wait_arg, 1.2) def test_export_non_retryable_status_codes(self): for status_code in (400, 401, 403, 404, 408, 500, 501): with self.subTest(status_code=status_code), Mocketizer(): Entry.single_register(Entry.POST, _TEST_ENDPOINT, status=status_code) exporter = OTLPSpanExporter(endpoint=_TEST_ENDPOINT) self._in_memory.clear() result = exporter.export(self._make_span()) self.assertEqual(result, SpanExportResult.FAILURE) self.assertEqual(len(Mocket.request_list()), 1) @mocketize def test_export_max_retries(self): Entry.register( Entry.POST, _TEST_ENDPOINT, *[Response(status=503)] * 6, ) exporter = OTLPSpanExporter(endpoint=_TEST_ENDPOINT, timeout=1000.0) shutdown_event = self._mocked_shutdown_event() exporter._client._shutdown_event = shutdown_event with _mock_clock(shutdown_event): result = exporter.export(self._make_span()) self.assertEqual(result, SpanExportResult.FAILURE) self.assertEqual(len(Mocket.request_list()), 6) self.assertEqual(shutdown_event.wait.call_count, 5) @mocketize def test_export_retry_after_header(self): Entry.register( Entry.POST, _TEST_ENDPOINT, Response(status=429, headers={"Retry-After": "5"}), Response(status=200), ) exporter = OTLPSpanExporter(endpoint=_TEST_ENDPOINT, timeout=60.0) shutdown_event = self._mocked_shutdown_event() shutdown_event.wait.return_value = False exporter._client._shutdown_event = shutdown_event result = exporter.export(self._make_span()) self.assertEqual(result, SpanExportResult.SUCCESS) self.assertEqual(len(Mocket.request_list()), 2) shutdown_event.wait.assert_called_once_with(5.0) @mocketize def test_export_retry_after_header_http_date(self): base = 1_700_000_000.0 retry_at = format_datetime(datetime.fromtimestamp(base + 30, timezone.utc), usegmt=True) Entry.register( Entry.POST, _TEST_ENDPOINT, Response(status=503, headers={"Retry-After": retry_at}), Response(status=200), ) exporter = OTLPSpanExporter(endpoint=_TEST_ENDPOINT, timeout=120.0) shutdown_event = self._mocked_shutdown_event() shutdown_event.wait.return_value = False exporter._client._shutdown_event = shutdown_event with patch( "opentelemetry.exporter.otlp.common.http.time.time", return_value=base, ): result = exporter.export(self._make_span()) self.assertEqual(result, SpanExportResult.SUCCESS) self.assertEqual(len(Mocket.request_list()), 2) shutdown_event.wait.assert_called_once_with(30.0) @mocketize def test_export_connection_error(self): Entry.register( Entry.POST, _TEST_ENDPOINT, urllib3.exceptions.ProtocolError("simulated reset"), Response(status=200), ) exporter = OTLPSpanExporter(endpoint=_TEST_ENDPOINT, timeout=5.0) result = exporter.export(self._make_span()) self.assertEqual(result, SpanExportResult.SUCCESS) self.assertEqual(len(Mocket.request_list()), 2) @mocketize def test_export_after_shutdown(self): exporter = OTLPSpanExporter(endpoint=_TEST_ENDPOINT) exporter.shutdown() with self.assertLogs(_LOGGER_NAME, level="WARNING"): result = exporter.export(self._make_span()) self.assertEqual(result, SpanExportResult.FAILURE) self.assertIsNone(Mocket.last_request()) def test_shutdown_idempotent(self): exporter = OTLPSpanExporter(endpoint=_TEST_ENDPOINT) exporter.shutdown() with self.assertLogs(_LOGGER_NAME, level="WARNING"): exporter.shutdown() # pylint: disable-next=no-self-use def test_shutdown_closes_transport(self): exporter = OTLPSpanExporter(endpoint=_TEST_ENDPOINT) with patch.object(exporter._client._transport, "close") as mock_close: exporter.shutdown() mock_close.assert_called_once() @mocketize def test_force_flush(self): Entry.single_register(Entry.POST, _TEST_ENDPOINT, status=200) exporter = OTLPSpanExporter(endpoint=_TEST_ENDPOINT) self.assertTrue(exporter.force_flush()) exporter.export(self._make_span()) self.assertTrue(exporter.force_flush()) @mocketize def test_export_encoding_failure(self): exporter = OTLPSpanExporter(endpoint=_TEST_ENDPOINT) with ( patch( "opentelemetry.exporter.otlp.json.http.trace_exporter.encode_spans", side_effect=ValueError("boom"), ), self.assertLogs(_LOGGER_NAME, level="ERROR"), ): result = exporter.export(self._make_span()) self.assertEqual(result, SpanExportResult.FAILURE) self.assertIsNone(Mocket.last_request()) python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-proto-common/000077500000000000000000000000001527070070700300405ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-proto-common/LICENSE000066400000000000000000000261351527070070700310540ustar00rootroot00000000000000 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. python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-proto-common/README.rst000066400000000000000000000012751527070070700315340ustar00rootroot00000000000000OpenTelemetry Protobuf Encoding =============================== |pypi| .. |pypi| image:: https://badge.fury.io/py/opentelemetry-exporter-otlp-proto-common.svg :target: https://pypi.org/project/opentelemetry-exporter-otlp-proto-common/ This library is provided as a convenience to encode to Protobuf. Currently used by: * opentelemetry-exporter-otlp-proto-grpc * opentelemetry-exporter-otlp-proto-http Installation ------------ :: pip install opentelemetry-exporter-otlp-proto-common References ---------- * `OpenTelemetry `_ * `OpenTelemetry Protocol Specification `_ python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-proto-common/pyproject.toml000066400000000000000000000025251527070070700327600ustar00rootroot00000000000000[build-system] requires = ["hatchling"] build-backend = "hatchling.build" [project] name = "opentelemetry-exporter-otlp-proto-common" dynamic = ["version"] description = "OpenTelemetry Protobuf encoding" 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", "Framework :: OpenTelemetry", "Framework :: OpenTelemetry :: Exporters", "Intended Audience :: Developers", "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-proto == 1.45.2", ] [project.urls] Homepage = "https://github.com/open-telemetry/opentelemetry-python/tree/main/exporter/opentelemetry-exporter-otlp-proto-common" Repository = "https://github.com/open-telemetry/opentelemetry-python" [tool.hatch.version] path = "src/opentelemetry/exporter/otlp/proto/common/version/__init__.py" [tool.hatch.build.targets.sdist] include = [ "/src", "/tests", ] [tool.hatch.build.targets.wheel] packages = ["src/opentelemetry"] python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-proto-common/src/000077500000000000000000000000001527070070700306275ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/000077500000000000000000000000001527070070700335235ustar00rootroot00000000000000exporter/000077500000000000000000000000001527070070700353145ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetryotlp/000077500000000000000000000000001527070070700362725ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporterproto/000077500000000000000000000000001527070070700374355ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlpcommon/000077500000000000000000000000001527070070700407255ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto__init__.py000066400000000000000000000002621527070070700430360ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from opentelemetry.exporter.otlp.proto.common.version import __version__ __all__ = ["__version__"] _internal/000077500000000000000000000000001527070070700427005ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common__init__.py000066400000000000000000000075101527070070700450140ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/_internal# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations import logging from collections.abc import Callable, Mapping, Sequence from typing import ( Any, TypeVar, ) from opentelemetry.proto.common.v1.common_pb2 import AnyValue as PB2AnyValue from opentelemetry.proto.common.v1.common_pb2 import ( ArrayValue as PB2ArrayValue, ) from opentelemetry.proto.common.v1.common_pb2 import ( InstrumentationScope as PB2InstrumentationScope, ) from opentelemetry.proto.common.v1.common_pb2 import KeyValue as PB2KeyValue from opentelemetry.proto.common.v1.common_pb2 import ( KeyValueList as PB2KeyValueList, ) from opentelemetry.proto.resource.v1.resource_pb2 import ( Resource as PB2Resource, ) from opentelemetry.sdk.trace import Resource from opentelemetry.sdk.util.instrumentation import InstrumentationScope from opentelemetry.util.types import Attributes _logger = logging.getLogger(__name__) _TypingResourceT = TypeVar("_TypingResourceT") _ResourceDataT = TypeVar("_ResourceDataT") def _encode_instrumentation_scope( instrumentation_scope: InstrumentationScope, ) -> PB2InstrumentationScope: if instrumentation_scope is None: return PB2InstrumentationScope() return PB2InstrumentationScope( name=instrumentation_scope.name, version=instrumentation_scope.version, attributes=_encode_attributes(instrumentation_scope.attributes), ) def _encode_resource(resource: Resource) -> PB2Resource: return PB2Resource(attributes=_encode_attributes(resource.attributes)) def _encode_value(value: Any) -> PB2AnyValue: if value is None: return PB2AnyValue() if isinstance(value, bool): return PB2AnyValue(bool_value=value) if isinstance(value, str): return PB2AnyValue(string_value=value) if isinstance(value, int): return PB2AnyValue(int_value=value) if isinstance(value, float): return PB2AnyValue(double_value=value) if isinstance(value, bytes): return PB2AnyValue(bytes_value=value) if isinstance(value, Sequence): return PB2AnyValue(array_value=PB2ArrayValue(values=[_encode_value(v) for v in value])) if isinstance(value, Mapping): return PB2AnyValue( kvlist_value=PB2KeyValueList(values=[_encode_key_value(str(k), v) for k, v in value.items()]) ) raise Exception(f"Invalid type {type(value)} of value {value}") def _encode_key_value(key: str, value: Any) -> PB2KeyValue: return PB2KeyValue(key=key, value=_encode_value(value)) def _encode_span_id(span_id: int) -> bytes: return span_id.to_bytes(length=8, byteorder="big", signed=False) def _encode_trace_id(trace_id: int) -> bytes: return trace_id.to_bytes(length=16, byteorder="big", signed=False) def _encode_attributes(attributes: Attributes) -> list[PB2KeyValue]: if not attributes: return [] pb2_attributes = [] for key, value in attributes.items(): # pylint: disable=broad-exception-caught try: pb2_attributes.append(_encode_key_value(key, value)) except Exception as error: _logger.exception("Failed to encode key %s: %s", key, error) return pb2_attributes def _get_resource_data( sdk_resource_scope_data: dict[Resource, _ResourceDataT], resource_class: Callable[..., _TypingResourceT], name: str, ) -> list[_TypingResourceT]: resource_data = [] for ( sdk_resource, scope_data, ) in sdk_resource_scope_data.items(): collector_resource = PB2Resource(attributes=_encode_attributes(sdk_resource.attributes)) resource_data.append( resource_class( **{ "resource": collector_resource, f"scope_{name}": scope_data.values(), } ) ) return resource_data _log_encoder/000077500000000000000000000000001527070070700453175ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/_internal__init__.py000066400000000000000000000061371527070070700474370ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/_internal/_log_encoder# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from collections import defaultdict from collections.abc import Sequence from opentelemetry.exporter.otlp.proto.common._internal import ( _encode_attributes, _encode_instrumentation_scope, _encode_resource, _encode_span_id, _encode_trace_id, _encode_value, ) from opentelemetry.proto.collector.logs.v1.logs_service_pb2 import ( ExportLogsServiceRequest, ) from opentelemetry.proto.logs.v1.logs_pb2 import LogRecord as PB2LogRecord from opentelemetry.proto.logs.v1.logs_pb2 import ( ResourceLogs, ScopeLogs, ) from opentelemetry.sdk._logs import ReadableLogRecord def encode_logs( batch: Sequence[ReadableLogRecord], ) -> ExportLogsServiceRequest: return ExportLogsServiceRequest(resource_logs=_encode_resource_logs(batch)) def _encode_log(readable_log_record: ReadableLogRecord) -> PB2LogRecord: span_id = ( None if readable_log_record.log_record.span_id == 0 else _encode_span_id(readable_log_record.log_record.span_id) ) trace_id = ( None if readable_log_record.log_record.trace_id == 0 else _encode_trace_id(readable_log_record.log_record.trace_id) ) return PB2LogRecord( time_unix_nano=readable_log_record.log_record.timestamp, observed_time_unix_nano=readable_log_record.log_record.observed_timestamp, span_id=span_id, trace_id=trace_id, flags=int(readable_log_record.log_record.trace_flags), body=_encode_value(readable_log_record.log_record.body), severity_text=readable_log_record.log_record.severity_text, attributes=_encode_attributes(readable_log_record.log_record.attributes), dropped_attributes_count=readable_log_record.dropped_attributes, severity_number=getattr(readable_log_record.log_record.severity_number, "value", None), event_name=readable_log_record.log_record.event_name, ) def _encode_resource_logs( batch: Sequence[ReadableLogRecord], ) -> list[ResourceLogs]: sdk_resource_logs = defaultdict(lambda: defaultdict(list)) for readable_log in batch: sdk_resource = readable_log.resource sdk_instrumentation = readable_log.instrumentation_scope or None pb2_log = _encode_log(readable_log) sdk_resource_logs[sdk_resource][sdk_instrumentation].append(pb2_log) pb2_resource_logs = [] for sdk_resource, sdk_instrumentations in sdk_resource_logs.items(): scope_logs = [] for sdk_instrumentation, pb2_logs in sdk_instrumentations.items(): scope_logs.append( ScopeLogs( scope=(_encode_instrumentation_scope(sdk_instrumentation)), log_records=pb2_logs, schema_url=sdk_instrumentation.schema_url if sdk_instrumentation else None, ) ) pb2_resource_logs.append( ResourceLogs( resource=_encode_resource(sdk_resource), scope_logs=scope_logs, schema_url=sdk_resource.schema_url, ) ) return pb2_resource_logs metrics_encoder/000077500000000000000000000000001527070070700460455ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/_internal__init__.py000066400000000000000000000324671527070070700501720ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/_internal/metrics_encoder# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations import logging from os import environ from opentelemetry.exporter.otlp.proto.common._internal import ( _encode_attributes, _encode_instrumentation_scope, _encode_span_id, _encode_trace_id, ) from opentelemetry.proto.collector.metrics.v1.metrics_service_pb2 import ( ExportMetricsServiceRequest, ) from opentelemetry.proto.metrics.v1 import metrics_pb2 as pb2 from opentelemetry.proto.resource.v1.resource_pb2 import ( Resource as PB2Resource, ) from opentelemetry.sdk.environment_variables import ( OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION, OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE, ) from opentelemetry.sdk.metrics import ( Counter, Exemplar, Histogram, ObservableCounter, ObservableGauge, ObservableUpDownCounter, UpDownCounter, ) from opentelemetry.sdk.metrics.export import ( AggregationTemporality, Gauge, MetricExporter, MetricsData, Sum, ) from opentelemetry.sdk.metrics.export import ( ExponentialHistogram as ExponentialHistogramType, ) from opentelemetry.sdk.metrics.export import ( Histogram as HistogramType, ) from opentelemetry.sdk.metrics.view import ( Aggregation, ExplicitBucketHistogramAggregation, ExponentialBucketHistogramAggregation, ) _logger = logging.getLogger(__name__) class OTLPMetricExporterMixin: def _common_configuration( self, preferred_temporality: dict[type, AggregationTemporality] | None = None, preferred_aggregation: dict[type, Aggregation] | None = None, ) -> None: MetricExporter.__init__( self, preferred_temporality=self._get_temporality(preferred_temporality), preferred_aggregation=self._get_aggregation(preferred_aggregation), ) def _get_temporality( self, preferred_temporality: dict[type, AggregationTemporality] ) -> dict[type, AggregationTemporality]: otel_exporter_otlp_metrics_temporality_preference = ( environ.get( OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE, "CUMULATIVE", ) .upper() .strip() ) if otel_exporter_otlp_metrics_temporality_preference == "DELTA": instrument_class_temporality = { Counter: AggregationTemporality.DELTA, UpDownCounter: AggregationTemporality.CUMULATIVE, Histogram: AggregationTemporality.DELTA, ObservableCounter: AggregationTemporality.DELTA, ObservableUpDownCounter: AggregationTemporality.CUMULATIVE, ObservableGauge: AggregationTemporality.CUMULATIVE, } elif otel_exporter_otlp_metrics_temporality_preference == "LOWMEMORY": instrument_class_temporality = { Counter: AggregationTemporality.DELTA, UpDownCounter: AggregationTemporality.CUMULATIVE, Histogram: AggregationTemporality.DELTA, ObservableCounter: AggregationTemporality.CUMULATIVE, ObservableUpDownCounter: AggregationTemporality.CUMULATIVE, ObservableGauge: AggregationTemporality.CUMULATIVE, } else: if otel_exporter_otlp_metrics_temporality_preference != ("CUMULATIVE"): _logger.warning( "Unrecognized OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE value found: %s, using CUMULATIVE", otel_exporter_otlp_metrics_temporality_preference, ) instrument_class_temporality = { Counter: AggregationTemporality.CUMULATIVE, UpDownCounter: AggregationTemporality.CUMULATIVE, Histogram: AggregationTemporality.CUMULATIVE, ObservableCounter: AggregationTemporality.CUMULATIVE, ObservableUpDownCounter: AggregationTemporality.CUMULATIVE, ObservableGauge: AggregationTemporality.CUMULATIVE, } instrument_class_temporality.update(preferred_temporality or {}) return instrument_class_temporality def _get_aggregation( self, preferred_aggregation: dict[type, Aggregation], ) -> dict[type, Aggregation]: otel_exporter_otlp_metrics_default_histogram_aggregation = environ.get( OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION, "explicit_bucket_histogram", ) if otel_exporter_otlp_metrics_default_histogram_aggregation == ("base2_exponential_bucket_histogram"): instrument_class_aggregation = { Histogram: ExponentialBucketHistogramAggregation(), } else: if otel_exporter_otlp_metrics_default_histogram_aggregation != ("explicit_bucket_histogram"): _logger.warning( ("Invalid value for %s: %s, using explicit bucket histogram aggregation"), OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION, otel_exporter_otlp_metrics_default_histogram_aggregation, ) instrument_class_aggregation = { Histogram: ExplicitBucketHistogramAggregation(), } instrument_class_aggregation.update(preferred_aggregation or {}) return instrument_class_aggregation class EncodingException(Exception): """ Raised by encode_metrics() when an exception is caught during encoding. Contains the problematic metric so the misbehaving metric name and details can be logged during exception handling. """ def __init__(self, original_exception, metric): super().__init__() self.original_exception = original_exception self.metric = metric def __str__(self): return f"{self.metric}\n{self.original_exception}" def encode_metrics(data: MetricsData) -> ExportMetricsServiceRequest: resource_metrics_dict = {} for resource_metrics in data.resource_metrics: _encode_resource_metrics(resource_metrics, resource_metrics_dict) resource_data = [] for ( sdk_resource, scope_data, ) in resource_metrics_dict.items(): resource_data.append( pb2.ResourceMetrics( resource=PB2Resource(attributes=_encode_attributes(sdk_resource.attributes)), scope_metrics=scope_data.values(), schema_url=sdk_resource.schema_url, ) ) return ExportMetricsServiceRequest(resource_metrics=resource_data) def _encode_resource_metrics(resource_metrics, resource_metrics_dict): resource = resource_metrics.resource # It is safe to assume that each entry in data.resource_metrics is # associated with an unique resource. scope_metrics_dict = {} resource_metrics_dict[resource] = scope_metrics_dict for scope_metrics in resource_metrics.scope_metrics: instrumentation_scope = scope_metrics.scope # The SDK groups metrics in instrumentation scopes already so # there is no need to check for existing instrumentation scopes # here. pb2_scope_metrics = pb2.ScopeMetrics( scope=_encode_instrumentation_scope(instrumentation_scope), schema_url=instrumentation_scope.schema_url, ) scope_metrics_dict[instrumentation_scope] = pb2_scope_metrics for metric in scope_metrics.metrics: pb2_metric = pb2.Metric( name=metric.name, description=metric.description, unit=metric.unit, ) try: _encode_metric(metric, pb2_metric) except Exception as ex: # `from None` so we don't get "During handling of the above exception, another exception occurred:" raise EncodingException(ex, metric) from None pb2_scope_metrics.metrics.append(pb2_metric) def _encode_metric(metric, pb2_metric): if isinstance(metric.data, Gauge): for data_point in metric.data.data_points: pt = pb2.NumberDataPoint( attributes=_encode_attributes(data_point.attributes), time_unix_nano=data_point.time_unix_nano, exemplars=_encode_exemplars(data_point.exemplars), ) if isinstance(data_point.value, int): pt.as_int = data_point.value else: pt.as_double = data_point.value pb2_metric.gauge.data_points.append(pt) elif isinstance(metric.data, HistogramType): for data_point in metric.data.data_points: pt = pb2.HistogramDataPoint( attributes=_encode_attributes(data_point.attributes), time_unix_nano=data_point.time_unix_nano, start_time_unix_nano=data_point.start_time_unix_nano, exemplars=_encode_exemplars(data_point.exemplars), count=data_point.count, sum=data_point.sum, bucket_counts=data_point.bucket_counts, explicit_bounds=data_point.explicit_bounds, max=data_point.max, min=data_point.min, ) pb2_metric.histogram.aggregation_temporality = metric.data.aggregation_temporality pb2_metric.histogram.data_points.append(pt) elif isinstance(metric.data, Sum): for data_point in metric.data.data_points: pt = pb2.NumberDataPoint( attributes=_encode_attributes(data_point.attributes), start_time_unix_nano=data_point.start_time_unix_nano, time_unix_nano=data_point.time_unix_nano, exemplars=_encode_exemplars(data_point.exemplars), ) if isinstance(data_point.value, int): pt.as_int = data_point.value else: pt.as_double = data_point.value # note that because sum is a message type, the # fields must be set individually rather than # instantiating a pb2.Sum and setting it once pb2_metric.sum.aggregation_temporality = metric.data.aggregation_temporality pb2_metric.sum.is_monotonic = metric.data.is_monotonic pb2_metric.sum.data_points.append(pt) elif isinstance(metric.data, ExponentialHistogramType): for data_point in metric.data.data_points: if data_point.positive.bucket_counts: positive = pb2.ExponentialHistogramDataPoint.Buckets( offset=data_point.positive.offset, bucket_counts=data_point.positive.bucket_counts, ) else: positive = None if data_point.negative.bucket_counts: negative = pb2.ExponentialHistogramDataPoint.Buckets( offset=data_point.negative.offset, bucket_counts=data_point.negative.bucket_counts, ) else: negative = None pt = pb2.ExponentialHistogramDataPoint( attributes=_encode_attributes(data_point.attributes), time_unix_nano=data_point.time_unix_nano, start_time_unix_nano=data_point.start_time_unix_nano, exemplars=_encode_exemplars(data_point.exemplars), count=data_point.count, sum=data_point.sum, scale=data_point.scale, zero_count=data_point.zero_count, positive=positive, negative=negative, flags=data_point.flags, max=data_point.max, min=data_point.min, ) pb2_metric.exponential_histogram.aggregation_temporality = metric.data.aggregation_temporality pb2_metric.exponential_histogram.data_points.append(pt) else: _logger.warning( "unsupported data type %s", metric.data.__class__.__name__, ) def _encode_exemplars(sdk_exemplars: list[Exemplar]) -> list[pb2.Exemplar]: """ Converts a list of SDK Exemplars into a list of protobuf Exemplars. Args: sdk_exemplars (list): The list of exemplars from the OpenTelemetry SDK. Returns: list: A list of protobuf exemplars. """ pb_exemplars = [] for sdk_exemplar in sdk_exemplars: if sdk_exemplar.span_id is not None and sdk_exemplar.trace_id is not None: pb_exemplar = pb2.Exemplar( time_unix_nano=sdk_exemplar.time_unix_nano, span_id=_encode_span_id(sdk_exemplar.span_id), trace_id=_encode_trace_id(sdk_exemplar.trace_id), filtered_attributes=_encode_attributes(sdk_exemplar.filtered_attributes), ) else: pb_exemplar = pb2.Exemplar( time_unix_nano=sdk_exemplar.time_unix_nano, filtered_attributes=_encode_attributes(sdk_exemplar.filtered_attributes), ) # Assign the value based on its type in the SDK exemplar if isinstance(sdk_exemplar.value, float): pb_exemplar.as_double = sdk_exemplar.value elif isinstance(sdk_exemplar.value, int): pb_exemplar.as_int = sdk_exemplar.value else: raise ValueError("Exemplar value must be an int or float") pb_exemplars.append(pb_exemplar) return pb_exemplars trace_encoder/000077500000000000000000000000001527070070700454755ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/_internal__init__.py000066400000000000000000000140021527070070700476030ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/_internal/trace_encoder# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import logging from collections import defaultdict from collections.abc import Sequence from opentelemetry.exporter.otlp.proto.common._internal import ( _encode_attributes, _encode_instrumentation_scope, _encode_resource, _encode_span_id, _encode_trace_id, ) from opentelemetry.proto.collector.trace.v1.trace_service_pb2 import ( ExportTraceServiceRequest as PB2ExportTraceServiceRequest, ) from opentelemetry.proto.trace.v1.trace_pb2 import ( ResourceSpans as PB2ResourceSpans, ) from opentelemetry.proto.trace.v1.trace_pb2 import ScopeSpans as PB2ScopeSpans from opentelemetry.proto.trace.v1.trace_pb2 import Span as PB2SPan from opentelemetry.proto.trace.v1.trace_pb2 import SpanFlags as PB2SpanFlags from opentelemetry.proto.trace.v1.trace_pb2 import Status as PB2Status from opentelemetry.sdk.trace import Event, ReadableSpan from opentelemetry.trace import Link, SpanKind from opentelemetry.trace.span import SpanContext, Status, TraceState # pylint: disable=E1101 _SPAN_KIND_MAP = { SpanKind.INTERNAL: PB2SPan.SpanKind.SPAN_KIND_INTERNAL, SpanKind.SERVER: PB2SPan.SpanKind.SPAN_KIND_SERVER, SpanKind.CLIENT: PB2SPan.SpanKind.SPAN_KIND_CLIENT, SpanKind.PRODUCER: PB2SPan.SpanKind.SPAN_KIND_PRODUCER, SpanKind.CONSUMER: PB2SPan.SpanKind.SPAN_KIND_CONSUMER, } _logger = logging.getLogger(__name__) def encode_spans( sdk_spans: Sequence[ReadableSpan], ) -> PB2ExportTraceServiceRequest: return PB2ExportTraceServiceRequest(resource_spans=_encode_resource_spans(sdk_spans)) def _encode_resource_spans( sdk_spans: Sequence[ReadableSpan], ) -> list[PB2ResourceSpans]: # We need to inspect the spans and group + structure them as: # # Resource # Instrumentation Library # Spans # # First loop organizes the SDK spans in this structure. Protobuf messages # are not hashable so we stick with SDK data in this phase. # # Second loop encodes the data into Protobuf format. # sdk_resource_spans = defaultdict(lambda: defaultdict(list)) for sdk_span in sdk_spans: sdk_resource = sdk_span.resource sdk_instrumentation = sdk_span.instrumentation_scope or None pb2_span = _encode_span(sdk_span) sdk_resource_spans[sdk_resource][sdk_instrumentation].append(pb2_span) pb2_resource_spans = [] for sdk_resource, sdk_instrumentations in sdk_resource_spans.items(): scope_spans = [] for sdk_instrumentation, pb2_spans in sdk_instrumentations.items(): scope_spans.append( PB2ScopeSpans( scope=(_encode_instrumentation_scope(sdk_instrumentation)), spans=pb2_spans, schema_url=sdk_instrumentation.schema_url if sdk_instrumentation else None, ) ) pb2_resource_spans.append( PB2ResourceSpans( resource=_encode_resource(sdk_resource), scope_spans=scope_spans, schema_url=sdk_resource.schema_url, ) ) return pb2_resource_spans def _span_flags(parent_span_context: SpanContext | None) -> int: flags = PB2SpanFlags.SPAN_FLAGS_CONTEXT_HAS_IS_REMOTE_MASK if parent_span_context and parent_span_context.is_remote: flags |= PB2SpanFlags.SPAN_FLAGS_CONTEXT_IS_REMOTE_MASK return flags def _encode_span(sdk_span: ReadableSpan) -> PB2SPan: span_context = sdk_span.get_span_context() return PB2SPan( trace_id=_encode_trace_id(span_context.trace_id), span_id=_encode_span_id(span_context.span_id), trace_state=_encode_trace_state(span_context.trace_state), parent_span_id=_encode_parent_id(sdk_span.parent), name=sdk_span.name, kind=_SPAN_KIND_MAP[sdk_span.kind], start_time_unix_nano=sdk_span.start_time, end_time_unix_nano=sdk_span.end_time, attributes=_encode_attributes(sdk_span.attributes), events=_encode_events(sdk_span.events), links=_encode_links(sdk_span.links), status=_encode_status(sdk_span.status), dropped_attributes_count=sdk_span.dropped_attributes, dropped_events_count=sdk_span.dropped_events, dropped_links_count=sdk_span.dropped_links, flags=_span_flags(sdk_span.parent), ) def _encode_events( events: Sequence[Event], ) -> list[PB2SPan.Event] | None: pb2_events = None if events: pb2_events = [] for event in events: encoded_event = PB2SPan.Event( name=event.name, time_unix_nano=event.timestamp, attributes=_encode_attributes(event.attributes), dropped_attributes_count=event.dropped_attributes, ) pb2_events.append(encoded_event) return pb2_events def _encode_links(links: Sequence[Link]) -> Sequence[PB2SPan.Link]: pb2_links = None if links: pb2_links = [] for link in links: encoded_link = PB2SPan.Link( trace_id=_encode_trace_id(link.context.trace_id), span_id=_encode_span_id(link.context.span_id), attributes=_encode_attributes(link.attributes), dropped_attributes_count=link.dropped_attributes, flags=_span_flags(link.context), ) pb2_links.append(encoded_link) return pb2_links def _encode_status(status: Status) -> PB2Status | None: pb2_status = None if status is not None: pb2_status = PB2Status( code=status.status_code.value, message=status.description, ) return pb2_status def _encode_trace_state(trace_state: TraceState) -> str | None: pb2_trace_state = None if trace_state is not None: pb2_trace_state = ",".join([f"{key}={value}" for key, value in (trace_state.items())]) return pb2_trace_state def _encode_parent_id(context: SpanContext | None) -> bytes | None: if context: return _encode_span_id(context.span_id) return None _log_encoder.py000066400000000000000000000003121527070070700437120ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from opentelemetry.exporter.otlp.proto.common._internal._log_encoder import ( encode_logs, ) __all__ = ["encode_logs"] metrics_encoder.py000066400000000000000000000003231527070070700444420ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from opentelemetry.exporter.otlp.proto.common._internal.metrics_encoder import ( encode_metrics, ) __all__ = ["encode_metrics"] py.typed000066400000000000000000000000001527070070700424120ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/commontrace_encoder.py000066400000000000000000000003151527070070700440730ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from opentelemetry.exporter.otlp.proto.common._internal.trace_encoder import ( encode_spans, ) __all__ = ["encode_spans"] version/000077500000000000000000000000001527070070700424125ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common__init__.py000066400000000000000000000001441527070070700445220ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/version# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 __version__ = "1.45.2" python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-proto-common/test-requirements.txt000066400000000000000000000005421527070070700343020ustar00rootroot00000000000000asgiref==3.7.2 iniconfig==2.0.0 packaging==24.0 pluggy==1.6.0 protobuf==7.35.0 py-cpuinfo==9.0.0 pytest==9.0.3 tomli==2.0.1 typing_extensions==4.12.0 wrapt==1.16.0 -e opentelemetry-api -e opentelemetry-sdk -e opentelemetry-semantic-conventions -e tests/opentelemetry-test-utils -e opentelemetry-proto -e exporter/opentelemetry-exporter-otlp-proto-common python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-proto-common/tests/000077500000000000000000000000001527070070700312025ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-proto-common/tests/__init__.py000066400000000000000000000000001527070070700333010ustar00rootroot00000000000000test_attribute_encoder.py000066400000000000000000000067741527070070700362540ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-proto-common/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import unittest from logging import ERROR from opentelemetry.exporter.otlp.proto.common._internal import ( _encode_attributes, ) from opentelemetry.proto.common.v1.common_pb2 import AnyValue as PB2AnyValue from opentelemetry.proto.common.v1.common_pb2 import ( ArrayValue as PB2ArrayValue, ) from opentelemetry.proto.common.v1.common_pb2 import KeyValue as PB2KeyValue class CallingStrRaisesException: def __str__(self): raise ValueError("Cannot encode") class TestOTLPAttributeEncoder(unittest.TestCase): def test_encode_attributes_all_kinds(self): result = _encode_attributes( { "a": 1, # int "b": 3.14, # float "c": False, # bool "hello": "world", # str "greet": ["hola", "bonjour"], # Sequence[str] "data": [1, 2], # Sequence[int] "data_granular": [1.4, 2.4], # Sequence[float] "binary_data": b"x00\x01\x02", # bytes } ) self.assertEqual( result, [ PB2KeyValue(key="a", value=PB2AnyValue(int_value=1)), PB2KeyValue(key="b", value=PB2AnyValue(double_value=3.14)), PB2KeyValue(key="c", value=PB2AnyValue(bool_value=False)), PB2KeyValue(key="hello", value=PB2AnyValue(string_value="world")), PB2KeyValue( key="greet", value=PB2AnyValue( array_value=PB2ArrayValue( values=[ PB2AnyValue(string_value="hola"), PB2AnyValue(string_value="bonjour"), ] ) ), ), PB2KeyValue( key="data", value=PB2AnyValue( array_value=PB2ArrayValue( values=[ PB2AnyValue(int_value=1), PB2AnyValue(int_value=2), ] ) ), ), PB2KeyValue( key="data_granular", value=PB2AnyValue( array_value=PB2ArrayValue( values=[ PB2AnyValue(double_value=1.4), PB2AnyValue(double_value=2.4), ] ) ), ), PB2KeyValue( key="binary_data", value=PB2AnyValue(bytes_value=b"x00\x01\x02"), ), ], ) def test_encode_attributes_error_logs_key(self): with self.assertLogs(level=ERROR) as error: result = _encode_attributes({"a": 1, "bad_key": CallingStrRaisesException(), "b": 2}) self.assertEqual(len(error.records), 1) self.assertEqual(error.records[0].msg, "Failed to encode key %s: %s") self.assertEqual(error.records[0].args[0], "bad_key") self.assertIsInstance(error.records[0].args[1], Exception) self.assertEqual( result, [ PB2KeyValue(key="a", value=PB2AnyValue(int_value=1)), PB2KeyValue(key="b", value=PB2AnyValue(int_value=2)), ], ) test_bytes_resource.py000066400000000000000000000047151527070070700356000ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-proto-common/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 """Every OTLP encoder groups telemetry by using the Resource as a dict key, so a Resource carrying a bytes attribute must survive the whole export path. """ import unittest from opentelemetry.exporter.otlp.proto.common._log_encoder import encode_logs from opentelemetry.exporter.otlp.proto.common.metrics_encoder import ( encode_metrics, ) from opentelemetry.exporter.otlp.proto.common.trace_encoder import encode_spans from opentelemetry.sdk._logs import LoggerProvider from opentelemetry.sdk._logs.export import ( InMemoryLogExporter, SimpleLogRecordProcessor, ) from opentelemetry.sdk.metrics import MeterProvider from opentelemetry.sdk.metrics.export import InMemoryMetricReader from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import SimpleSpanProcessor from opentelemetry.sdk.trace.export.in_memory_span_exporter import ( InMemorySpanExporter, ) _BYTES_RESOURCE = {"service.name": "svc", "build.id": b"\x01\x02\x03"} class TestBytesResourceExports(unittest.TestCase): def setUp(self): self.resource = Resource.create(_BYTES_RESOURCE) def test_encode_spans(self): exporter = InMemorySpanExporter() provider = TracerProvider(resource=self.resource, shutdown_on_exit=False) provider.add_span_processor(SimpleSpanProcessor(exporter)) with provider.get_tracer(__name__).start_as_current_span("span"): pass request = encode_spans(exporter.get_finished_spans()) self.assertEqual(len(request.resource_spans), 1) provider.shutdown() def test_encode_metrics(self): reader = InMemoryMetricReader() provider = MeterProvider(metric_readers=[reader], resource=self.resource) provider.get_meter(__name__).create_counter("counter").add(1) request = encode_metrics(reader.get_metrics_data()) self.assertEqual(len(request.resource_metrics), 1) provider.shutdown() def test_encode_logs(self): exporter = InMemoryLogExporter() provider = LoggerProvider(resource=self.resource, shutdown_on_exit=False) provider.add_log_record_processor(SimpleLogRecordProcessor(exporter)) provider.get_logger(__name__).emit(body="body") request = encode_logs(exporter.get_finished_logs()) self.assertEqual(len(request.resource_logs), 1) provider.shutdown() test_log_encoder.py000066400000000000000000000256221527070070700350230ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-proto-common/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import unittest from opentelemetry._logs import LogRecord, SeverityNumber from opentelemetry.exporter.otlp.proto.common._internal import ( _encode_attributes, _encode_span_id, _encode_trace_id, _encode_value, ) from opentelemetry.exporter.otlp.proto.common._log_encoder import encode_logs from opentelemetry.proto.collector.logs.v1.logs_service_pb2 import ( ExportLogsServiceRequest, ) from opentelemetry.proto.common.v1.common_pb2 import AnyValue as PB2AnyValue from opentelemetry.proto.common.v1.common_pb2 import ( InstrumentationScope as PB2InstrumentationScope, ) from opentelemetry.proto.common.v1.common_pb2 import KeyValue as PB2KeyValue from opentelemetry.proto.logs.v1.logs_pb2 import LogRecord as PB2LogRecord from opentelemetry.proto.logs.v1.logs_pb2 import ( ResourceLogs as PB2ResourceLogs, ) from opentelemetry.proto.logs.v1.logs_pb2 import ScopeLogs as PB2ScopeLogs from opentelemetry.proto.resource.v1.resource_pb2 import ( Resource as PB2Resource, ) from opentelemetry.sdk._logs import LogRecordLimits, ReadWriteLogRecord from opentelemetry.sdk.resources import Resource as SDKResource from opentelemetry.sdk.util.instrumentation import InstrumentationScope from opentelemetry.trace import ( NonRecordingSpan, SpanContext, TraceFlags, set_span_in_context, ) _CONTEXT_LOG = set_span_in_context( NonRecordingSpan( SpanContext( 89564621134313219400156819398935297684, 1312458408527513268, False, TraceFlags(0x01), ) ) ) class TestOTLPLogEncoder(unittest.TestCase): def test_encode_basic_log_record(self): basic_log_record = ReadWriteLogRecord( LogRecord( timestamp=1644650195189786880, observed_timestamp=1644650195189786881, context=_CONTEXT_LOG, severity_text="WARN", severity_number=SeverityNumber.WARN, body="Do not go gentle into that good night. Rage, rage against the dying of the light", attributes={"a": 1, "b": "c"}, ), resource=SDKResource( {"first_resource": "value"}, "resource_schema_url", ), instrumentation_scope=InstrumentationScope("first_name", "first_version"), ) pb2_service_request = ExportLogsServiceRequest( resource_logs=[ PB2ResourceLogs( resource=PB2Resource( attributes=[ PB2KeyValue( key="first_resource", value=PB2AnyValue(string_value="value"), ) ] ), scope_logs=[ PB2ScopeLogs( scope=PB2InstrumentationScope(name="first_name", version="first_version"), log_records=[ PB2LogRecord( time_unix_nano=1644650195189786880, observed_time_unix_nano=1644650195189786881, trace_id=_encode_trace_id(89564621134313219400156819398935297684), span_id=_encode_span_id(1312458408527513268), flags=int(TraceFlags(0x01)), severity_text="WARN", severity_number=SeverityNumber.WARN.value, body=_encode_value( "Do not go gentle into that good night. Rage, rage against the dying of the light" ), attributes=_encode_attributes({"a": 1, "b": "c"}), ) ], ), ], schema_url="resource_schema_url", ) ] ) self.assertEqual(encode_logs([basic_log_record]), pb2_service_request) def test_encode_log_record_with_no_instrumentation_scope_and_dict_body( self, ): log_record_with_no_instrumentation_scope_and_dict_body = ReadWriteLogRecord( LogRecord( timestamp=1644650427658989056, observed_timestamp=1644650427658989057, context=_CONTEXT_LOG, severity_text="DEBUG", severity_number=SeverityNumber.DEBUG, body={"error": None, "array_with_nones": [1, None, 2]}, attributes={"a": 1, "b": "c"}, ), resource=SDKResource({"second_resource": "CASE"}), instrumentation_scope=None, ) pb2_resource_logs = PB2ResourceLogs( resource=PB2Resource( attributes=[ PB2KeyValue( key="second_resource", value=PB2AnyValue(string_value="CASE"), ) ] ), scope_logs=[ PB2ScopeLogs( scope=PB2InstrumentationScope(), log_records=[ PB2LogRecord( time_unix_nano=1644650427658989056, observed_time_unix_nano=1644650427658989057, trace_id=_encode_trace_id(89564621134313219400156819398935297684), span_id=_encode_span_id(1312458408527513268), flags=int(TraceFlags(0x01)), severity_text="DEBUG", severity_number=SeverityNumber.DEBUG.value, body=_encode_value( { "error": None, "array_with_nones": [1, None, 2], } ), attributes=_encode_attributes({"a": 1, "b": "c"}), ) ], ) ], ) self.assertEqual( encode_logs([log_record_with_no_instrumentation_scope_and_dict_body]), ExportLogsServiceRequest(resource_logs=[pb2_resource_logs]), ) def test_encode_log_record_with_empty_resource_and_dict_attribute_value( self, ): log_record_with_empty_resource_and_dict_attribute_value = ReadWriteLogRecord( LogRecord( timestamp=1644650584292683033, observed_timestamp=1644650584292683033, context=_CONTEXT_LOG, severity_text="FATAL", severity_number=SeverityNumber.FATAL, body="This instrumentation scope has a schema url and attributes", attributes={"extended": {"sequence": [{"inner": "mapping", "none": None}]}}, ), resource=SDKResource({}), instrumentation_scope=InstrumentationScope( "scope_with_attributes", "scope_with_attributes_version", "instrumentation_schema_url", {"one": 1, "two": "2"}, ), ) pb2_resource_logs = PB2ResourceLogs( resource=PB2Resource(attributes=[]), scope_logs=[ PB2ScopeLogs( scope=PB2InstrumentationScope( name="scope_with_attributes", version="scope_with_attributes_version", attributes=_encode_attributes({"one": 1, "two": "2"}), ), log_records=[ PB2LogRecord( time_unix_nano=1644650584292683033, observed_time_unix_nano=1644650584292683033, trace_id=_encode_trace_id(89564621134313219400156819398935297684), span_id=_encode_span_id(1312458408527513268), flags=int(TraceFlags(0x01)), severity_text="FATAL", severity_number=SeverityNumber.FATAL.value, body=_encode_value("This instrumentation scope has a schema url and attributes"), attributes=_encode_attributes( {"extended": {"sequence": [{"inner": "mapping", "none": None}]}} ), ) ], schema_url="instrumentation_schema_url", ) ], ) self.assertEqual( encode_logs([log_record_with_empty_resource_and_dict_attribute_value]), ExportLogsServiceRequest(resource_logs=[pb2_resource_logs]), ) def test_dropped_attributes_count(self): sdk_logs = self._get_test_logs_dropped_attributes() encoded_logs = encode_logs(sdk_logs) self.assertTrue(hasattr(sdk_logs[0], "dropped_attributes")) self.assertEqual( # pylint:disable=no-member encoded_logs.resource_logs[0].scope_logs[0].log_records[0].dropped_attributes_count, 2, ) @staticmethod def _get_test_logs_dropped_attributes() -> list[ReadWriteLogRecord]: ctx_log1 = set_span_in_context( NonRecordingSpan( SpanContext( 89564621134313219400156819398935297684, 1312458408527513268, False, TraceFlags(0x01), ) ) ) log1 = ReadWriteLogRecord( LogRecord( timestamp=1644650195189786880, context=ctx_log1, severity_text="WARN", severity_number=SeverityNumber.WARN, body="Do not go gentle into that good night. Rage, rage against the dying of the light", attributes={"a": 1, "b": "c", "user_id": "B121092"}, ), resource=SDKResource({"first_resource": "value"}), limits=LogRecordLimits(max_attributes=1), instrumentation_scope=InstrumentationScope("first_name", "first_version"), ) ctx_log2 = set_span_in_context(NonRecordingSpan(SpanContext(0, 0, False))) log2 = ReadWriteLogRecord( LogRecord( timestamp=1644650249738562048, context=ctx_log2, severity_text="WARN", severity_number=SeverityNumber.WARN, body="Cooper, this is no time for caution!", attributes={}, ), resource=SDKResource({"second_resource": "CASE"}), instrumentation_scope=InstrumentationScope("second_name", "second_version"), ) return [log1, log2] test_metrics_encoder.py000066400000000000000000001425011527070070700357040ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-proto-common/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # pylint: disable=protected-access,too-many-lines import unittest from opentelemetry.exporter.otlp.proto.common._internal.metrics_encoder import ( EncodingException, ) from opentelemetry.exporter.otlp.proto.common.metrics_encoder import ( encode_metrics, ) from opentelemetry.proto.collector.metrics.v1.metrics_service_pb2 import ( ExportMetricsServiceRequest, ) from opentelemetry.proto.common.v1.common_pb2 import ( AnyValue, InstrumentationScope, KeyValue, ) from opentelemetry.proto.metrics.v1 import metrics_pb2 as pb2 from opentelemetry.proto.resource.v1.resource_pb2 import ( Resource as OTLPResource, ) from opentelemetry.sdk.metrics import Exemplar from opentelemetry.sdk.metrics.export import ( AggregationTemporality, Buckets, ExponentialHistogramDataPoint, HistogramDataPoint, Metric, MetricsData, ResourceMetrics, ScopeMetrics, ) from opentelemetry.sdk.metrics.export import ( ExponentialHistogram as ExponentialHistogramType, ) from opentelemetry.sdk.metrics.export import Histogram as HistogramType from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.util.instrumentation import ( InstrumentationScope as SDKInstrumentationScope, ) from opentelemetry.test.metrictestutil import _generate_gauge, _generate_sum class TestOTLPMetricsEncoder(unittest.TestCase): span_id = int("6e0c63257de34c92", 16) trace_id = int("d4cda95b652f4a1592b449d5929fda1b", 16) histogram = Metric( name="histogram", description="foo", unit="s", data=HistogramType( data_points=[ HistogramDataPoint( attributes={"a": 1, "b": True}, start_time_unix_nano=1641946016139533244, time_unix_nano=1641946016139533244, exemplars=[ Exemplar( {"filtered": "banana"}, 298.0, 1641946016139533400, span_id, trace_id, ), Exemplar( {"filtered": "banana"}, 298.0, 1641946016139533400, None, None, ), ], count=5, sum=67, bucket_counts=[1, 4], explicit_bounds=[10.0, 20.0], min=8, max=18, ) ], aggregation_temporality=AggregationTemporality.DELTA, ), ) def test_encode_sum_int(self): metrics_data = MetricsData( resource_metrics=[ ResourceMetrics( resource=Resource( attributes={"a": 1, "b": False}, schema_url="resource_schema_url", ), scope_metrics=[ ScopeMetrics( scope=SDKInstrumentationScope( name="first_name", version="first_version", schema_url="instrumentation_scope_schema_url", ), metrics=[_generate_sum("sum_int", 33)], schema_url="instrumentation_scope_schema_url", ) ], schema_url="resource_schema_url", ) ] ) expected = ExportMetricsServiceRequest( resource_metrics=[ pb2.ResourceMetrics( schema_url="resource_schema_url", resource=OTLPResource( attributes=[ KeyValue(key="a", value=AnyValue(int_value=1)), KeyValue(key="b", value=AnyValue(bool_value=False)), ] ), scope_metrics=[ pb2.ScopeMetrics( scope=InstrumentationScope(name="first_name", version="first_version"), schema_url="instrumentation_scope_schema_url", metrics=[ pb2.Metric( name="sum_int", unit="s", description="foo", sum=pb2.Sum( data_points=[ pb2.NumberDataPoint( attributes=[ KeyValue( key="a", value=AnyValue(int_value=1), ), KeyValue( key="b", value=AnyValue(bool_value=True), ), ], start_time_unix_nano=1641946015139533244, time_unix_nano=1641946016139533244, as_int=33, ) ], aggregation_temporality=AggregationTemporality.CUMULATIVE, is_monotonic=True, ), ) ], ) ], ) ] ) actual = encode_metrics(metrics_data) self.assertEqual(expected, actual) def test_encode_sum_double(self): metrics_data = MetricsData( resource_metrics=[ ResourceMetrics( resource=Resource( attributes={"a": 1, "b": False}, schema_url="resource_schema_url", ), scope_metrics=[ ScopeMetrics( scope=SDKInstrumentationScope( name="first_name", version="first_version", schema_url="instrumentation_scope_schema_url", ), metrics=[_generate_sum("sum_double", 2.98)], schema_url="instrumentation_scope_schema_url", ) ], schema_url="resource_schema_url", ) ] ) expected = ExportMetricsServiceRequest( resource_metrics=[ pb2.ResourceMetrics( schema_url="resource_schema_url", resource=OTLPResource( attributes=[ KeyValue(key="a", value=AnyValue(int_value=1)), KeyValue(key="b", value=AnyValue(bool_value=False)), ] ), scope_metrics=[ pb2.ScopeMetrics( scope=InstrumentationScope(name="first_name", version="first_version"), schema_url="instrumentation_scope_schema_url", metrics=[ pb2.Metric( name="sum_double", unit="s", description="foo", sum=pb2.Sum( data_points=[ pb2.NumberDataPoint( attributes=[ KeyValue( key="a", value=AnyValue(int_value=1), ), KeyValue( key="b", value=AnyValue(bool_value=True), ), ], start_time_unix_nano=1641946015139533244, time_unix_nano=1641946016139533244, as_double=2.98, ) ], aggregation_temporality=AggregationTemporality.CUMULATIVE, is_monotonic=True, ), ) ], ) ], ) ] ) actual = encode_metrics(metrics_data) self.assertEqual(expected, actual) def test_encode_gauge_int(self): metrics_data = MetricsData( resource_metrics=[ ResourceMetrics( resource=Resource( attributes={"a": 1, "b": False}, schema_url="resource_schema_url", ), scope_metrics=[ ScopeMetrics( scope=SDKInstrumentationScope( name="first_name", version="first_version", schema_url="instrumentation_scope_schema_url", ), metrics=[_generate_gauge("gauge_int", 9000)], schema_url="instrumentation_scope_schema_url", ) ], schema_url="resource_schema_url", ) ] ) expected = ExportMetricsServiceRequest( resource_metrics=[ pb2.ResourceMetrics( schema_url="resource_schema_url", resource=OTLPResource( attributes=[ KeyValue(key="a", value=AnyValue(int_value=1)), KeyValue(key="b", value=AnyValue(bool_value=False)), ] ), scope_metrics=[ pb2.ScopeMetrics( scope=InstrumentationScope(name="first_name", version="first_version"), schema_url="instrumentation_scope_schema_url", metrics=[ pb2.Metric( name="gauge_int", unit="s", description="foo", gauge=pb2.Gauge( data_points=[ pb2.NumberDataPoint( attributes=[ KeyValue( key="a", value=AnyValue(int_value=1), ), KeyValue( key="b", value=AnyValue(bool_value=True), ), ], time_unix_nano=1641946016139533244, start_time_unix_nano=0, as_int=9000, ) ], ), ) ], ) ], ) ] ) actual = encode_metrics(metrics_data) self.assertEqual(expected, actual) def test_encode_gauge_double(self): metrics_data = MetricsData( resource_metrics=[ ResourceMetrics( resource=Resource( attributes={"a": 1, "b": False}, schema_url="resource_schema_url", ), scope_metrics=[ ScopeMetrics( scope=SDKInstrumentationScope( name="first_name", version="first_version", schema_url="instrumentation_scope_schema_url", ), metrics=[_generate_gauge("gauge_double", 52.028)], schema_url="instrumentation_scope_schema_url", ) ], schema_url="resource_schema_url", ) ] ) expected = ExportMetricsServiceRequest( resource_metrics=[ pb2.ResourceMetrics( schema_url="resource_schema_url", resource=OTLPResource( attributes=[ KeyValue(key="a", value=AnyValue(int_value=1)), KeyValue(key="b", value=AnyValue(bool_value=False)), ] ), scope_metrics=[ pb2.ScopeMetrics( scope=InstrumentationScope(name="first_name", version="first_version"), schema_url="instrumentation_scope_schema_url", metrics=[ pb2.Metric( name="gauge_double", unit="s", description="foo", gauge=pb2.Gauge( data_points=[ pb2.NumberDataPoint( attributes=[ KeyValue( key="a", value=AnyValue(int_value=1), ), KeyValue( key="b", value=AnyValue(bool_value=True), ), ], time_unix_nano=1641946016139533244, as_double=52.028, ) ], ), ) ], ) ], ) ] ) actual = encode_metrics(metrics_data) self.assertEqual(expected, actual) def test_encode_histogram(self): metrics_data = MetricsData( resource_metrics=[ ResourceMetrics( resource=Resource( attributes={"a": 1, "b": False}, schema_url="resource_schema_url", ), scope_metrics=[ ScopeMetrics( scope=SDKInstrumentationScope( name="first_name", version="first_version", schema_url="instrumentation_scope_schema_url", ), metrics=[self.histogram], schema_url="instrumentation_scope_schema_url", ) ], schema_url="resource_schema_url", ) ] ) expected = ExportMetricsServiceRequest( resource_metrics=[ pb2.ResourceMetrics( schema_url="resource_schema_url", resource=OTLPResource( attributes=[ KeyValue(key="a", value=AnyValue(int_value=1)), KeyValue(key="b", value=AnyValue(bool_value=False)), ] ), scope_metrics=[ pb2.ScopeMetrics( scope=InstrumentationScope(name="first_name", version="first_version"), schema_url="instrumentation_scope_schema_url", metrics=[ pb2.Metric( name="histogram", unit="s", description="foo", histogram=pb2.Histogram( data_points=[ pb2.HistogramDataPoint( attributes=[ KeyValue( key="a", value=AnyValue(int_value=1), ), KeyValue( key="b", value=AnyValue(bool_value=True), ), ], start_time_unix_nano=1641946016139533244, time_unix_nano=1641946016139533244, count=5, sum=67, bucket_counts=[1, 4], explicit_bounds=[10.0, 20.0], exemplars=[ pb2.Exemplar( time_unix_nano=1641946016139533400, as_double=298, span_id=b"n\x0cc%}\xe3L\x92", trace_id=b"\xd4\xcd\xa9[e/J\x15\x92\xb4I\xd5\x92\x9f\xda\x1b", filtered_attributes=[ KeyValue( key="filtered", value=AnyValue(string_value="banana"), ) ], ), pb2.Exemplar( time_unix_nano=1641946016139533400, as_double=298, filtered_attributes=[ KeyValue( key="filtered", value=AnyValue(string_value="banana"), ) ], ), ], max=18.0, min=8.0, ) ], aggregation_temporality=AggregationTemporality.DELTA, ), ) ], ) ], ) ] ) actual = encode_metrics(metrics_data) self.assertEqual(expected, actual) def test_encode_multiple_scope_histogram(self): metrics_data = MetricsData( resource_metrics=[ ResourceMetrics( resource=Resource( attributes={"a": 1, "b": False}, schema_url="resource_schema_url", ), scope_metrics=[ ScopeMetrics( scope=SDKInstrumentationScope( name="first_name", version="first_version", schema_url="instrumentation_scope_schema_url", ), metrics=[self.histogram, self.histogram], schema_url="instrumentation_scope_schema_url", ), ScopeMetrics( scope=SDKInstrumentationScope( name="second_name", version="second_version", schema_url="instrumentation_scope_schema_url", ), metrics=[self.histogram], schema_url="instrumentation_scope_schema_url", ), ScopeMetrics( scope=SDKInstrumentationScope( name="third_name", version="third_version", schema_url="instrumentation_scope_schema_url", ), metrics=[self.histogram], schema_url="instrumentation_scope_schema_url", ), ], schema_url="resource_schema_url", ) ] ) expected = ExportMetricsServiceRequest( resource_metrics=[ pb2.ResourceMetrics( schema_url="resource_schema_url", resource=OTLPResource( attributes=[ KeyValue(key="a", value=AnyValue(int_value=1)), KeyValue(key="b", value=AnyValue(bool_value=False)), ] ), scope_metrics=[ pb2.ScopeMetrics( scope=InstrumentationScope(name="first_name", version="first_version"), schema_url="instrumentation_scope_schema_url", metrics=[ pb2.Metric( name="histogram", unit="s", description="foo", histogram=pb2.Histogram( data_points=[ pb2.HistogramDataPoint( attributes=[ KeyValue( key="a", value=AnyValue(int_value=1), ), KeyValue( key="b", value=AnyValue(bool_value=True), ), ], start_time_unix_nano=1641946016139533244, time_unix_nano=1641946016139533244, count=5, sum=67, bucket_counts=[1, 4], explicit_bounds=[10.0, 20.0], exemplars=[ pb2.Exemplar( time_unix_nano=1641946016139533400, as_double=298, span_id=b"n\x0cc%}\xe3L\x92", trace_id=b"\xd4\xcd\xa9[e/J\x15\x92\xb4I\xd5\x92\x9f\xda\x1b", filtered_attributes=[ KeyValue( key="filtered", value=AnyValue(string_value="banana"), ) ], ), pb2.Exemplar( time_unix_nano=1641946016139533400, as_double=298, filtered_attributes=[ KeyValue( key="filtered", value=AnyValue(string_value="banana"), ) ], ), ], max=18.0, min=8.0, ) ], aggregation_temporality=AggregationTemporality.DELTA, ), ), pb2.Metric( name="histogram", unit="s", description="foo", histogram=pb2.Histogram( data_points=[ pb2.HistogramDataPoint( attributes=[ KeyValue( key="a", value=AnyValue(int_value=1), ), KeyValue( key="b", value=AnyValue(bool_value=True), ), ], start_time_unix_nano=1641946016139533244, time_unix_nano=1641946016139533244, count=5, sum=67, bucket_counts=[1, 4], explicit_bounds=[10.0, 20.0], exemplars=[ pb2.Exemplar( time_unix_nano=1641946016139533400, as_double=298, span_id=b"n\x0cc%}\xe3L\x92", trace_id=b"\xd4\xcd\xa9[e/J\x15\x92\xb4I\xd5\x92\x9f\xda\x1b", filtered_attributes=[ KeyValue( key="filtered", value=AnyValue(string_value="banana"), ) ], ), pb2.Exemplar( time_unix_nano=1641946016139533400, as_double=298, filtered_attributes=[ KeyValue( key="filtered", value=AnyValue(string_value="banana"), ) ], ), ], max=18.0, min=8.0, ) ], aggregation_temporality=AggregationTemporality.DELTA, ), ), ], ), pb2.ScopeMetrics( scope=InstrumentationScope(name="second_name", version="second_version"), schema_url="instrumentation_scope_schema_url", metrics=[ pb2.Metric( name="histogram", unit="s", description="foo", histogram=pb2.Histogram( data_points=[ pb2.HistogramDataPoint( attributes=[ KeyValue( key="a", value=AnyValue(int_value=1), ), KeyValue( key="b", value=AnyValue(bool_value=True), ), ], start_time_unix_nano=1641946016139533244, time_unix_nano=1641946016139533244, count=5, sum=67, bucket_counts=[1, 4], explicit_bounds=[10.0, 20.0], exemplars=[ pb2.Exemplar( time_unix_nano=1641946016139533400, as_double=298, span_id=b"n\x0cc%}\xe3L\x92", trace_id=b"\xd4\xcd\xa9[e/J\x15\x92\xb4I\xd5\x92\x9f\xda\x1b", filtered_attributes=[ KeyValue( key="filtered", value=AnyValue(string_value="banana"), ) ], ), pb2.Exemplar( time_unix_nano=1641946016139533400, as_double=298, filtered_attributes=[ KeyValue( key="filtered", value=AnyValue(string_value="banana"), ) ], ), ], max=18.0, min=8.0, ) ], aggregation_temporality=AggregationTemporality.DELTA, ), ) ], ), pb2.ScopeMetrics( scope=InstrumentationScope(name="third_name", version="third_version"), schema_url="instrumentation_scope_schema_url", metrics=[ pb2.Metric( name="histogram", unit="s", description="foo", histogram=pb2.Histogram( data_points=[ pb2.HistogramDataPoint( attributes=[ KeyValue( key="a", value=AnyValue(int_value=1), ), KeyValue( key="b", value=AnyValue(bool_value=True), ), ], start_time_unix_nano=1641946016139533244, time_unix_nano=1641946016139533244, count=5, sum=67, bucket_counts=[1, 4], explicit_bounds=[10.0, 20.0], exemplars=[ pb2.Exemplar( time_unix_nano=1641946016139533400, as_double=298, span_id=b"n\x0cc%}\xe3L\x92", trace_id=b"\xd4\xcd\xa9[e/J\x15\x92\xb4I\xd5\x92\x9f\xda\x1b", filtered_attributes=[ KeyValue( key="filtered", value=AnyValue(string_value="banana"), ) ], ), pb2.Exemplar( time_unix_nano=1641946016139533400, as_double=298, filtered_attributes=[ KeyValue( key="filtered", value=AnyValue(string_value="banana"), ) ], ), ], max=18.0, min=8.0, ) ], aggregation_temporality=AggregationTemporality.DELTA, ), ) ], ), ], ) ] ) actual = encode_metrics(metrics_data) self.assertEqual(expected, actual) def test_encode_exponential_histogram(self): exponential_histogram = Metric( name="exponential_histogram", description="description", unit="unit", data=ExponentialHistogramType( data_points=[ ExponentialHistogramDataPoint( attributes={"a": 1, "b": True}, start_time_unix_nano=0, time_unix_nano=1, count=2, sum=3, scale=4, zero_count=5, positive=Buckets(offset=6, bucket_counts=[7, 8]), negative=Buckets(offset=9, bucket_counts=[10, 11]), flags=12, min=13.0, max=14.0, ) ], aggregation_temporality=AggregationTemporality.DELTA, ), ) metrics_data = MetricsData( resource_metrics=[ ResourceMetrics( resource=Resource( attributes={"a": 1, "b": False}, schema_url="resource_schema_url", ), scope_metrics=[ ScopeMetrics( scope=SDKInstrumentationScope( name="first_name", version="first_version", schema_url="instrumentation_scope_schema_url", ), metrics=[exponential_histogram], schema_url="instrumentation_scope_schema_url", ) ], schema_url="resource_schema_url", ) ] ) expected = ExportMetricsServiceRequest( resource_metrics=[ pb2.ResourceMetrics( schema_url="resource_schema_url", resource=OTLPResource( attributes=[ KeyValue(key="a", value=AnyValue(int_value=1)), KeyValue(key="b", value=AnyValue(bool_value=False)), ] ), scope_metrics=[ pb2.ScopeMetrics( scope=InstrumentationScope(name="first_name", version="first_version"), schema_url="instrumentation_scope_schema_url", metrics=[ pb2.Metric( name="exponential_histogram", unit="unit", description="description", exponential_histogram=pb2.ExponentialHistogram( data_points=[ pb2.ExponentialHistogramDataPoint( attributes=[ KeyValue( key="a", value=AnyValue(int_value=1), ), KeyValue( key="b", value=AnyValue(bool_value=True), ), ], start_time_unix_nano=0, time_unix_nano=1, count=2, sum=3, scale=4, zero_count=5, positive=pb2.ExponentialHistogramDataPoint.Buckets( offset=6, bucket_counts=[7, 8], ), negative=pb2.ExponentialHistogramDataPoint.Buckets( offset=9, bucket_counts=[10, 11], ), flags=12, exemplars=[], min=13.0, max=14.0, ) ], aggregation_temporality=AggregationTemporality.DELTA, ), ) ], ) ], ) ] ) # pylint: disable=protected-access actual = encode_metrics(metrics_data) self.assertEqual(expected, actual) def test_encoding_exception_reraise(self): # this number is too big to fit in a signed 64-bit proto field and causes a ValueError big_number = 2**63 metrics_data = MetricsData( resource_metrics=[ ResourceMetrics( resource=Resource( attributes={}, schema_url="resource_schema_url", ), scope_metrics=[ ScopeMetrics( scope=SDKInstrumentationScope( name="first_name", version="first_version", schema_url="instrumentation_scope_schema_url", ), metrics=[_generate_sum("sum_double", big_number)], schema_url="instrumentation_scope_schema_url", ) ], schema_url="resource_schema_url", ) ] ) with self.assertRaises(EncodingException) as context: encode_metrics(metrics_data) # assert that the EncodingException wraps the metric and original exception assert isinstance(context.exception.metric, Metric) assert isinstance(context.exception.original_exception, ValueError) def test_encode_scope_with_attributes(self): metrics_data = MetricsData( resource_metrics=[ ResourceMetrics( resource=Resource( attributes=None, schema_url="resource_schema_url", ), scope_metrics=[ ScopeMetrics( scope=SDKInstrumentationScope( name="first_name", version="first_version", schema_url="instrumentation_scope_schema_url", attributes={"one": 1, "two": "2"}, ), metrics=[_generate_sum("sum_int", 88)], schema_url="instrumentation_scope_schema_url", ) ], schema_url="resource_schema_url", ) ] ) expected = ExportMetricsServiceRequest( resource_metrics=[ pb2.ResourceMetrics( schema_url="resource_schema_url", resource=OTLPResource(), scope_metrics=[ pb2.ScopeMetrics( scope=InstrumentationScope( name="first_name", version="first_version", attributes=[ KeyValue(key="one", value=AnyValue(int_value=1)), KeyValue( key="two", value=AnyValue(string_value="2"), ), ], ), schema_url="instrumentation_scope_schema_url", metrics=[ pb2.Metric( name="sum_int", unit="s", description="foo", sum=pb2.Sum( data_points=[ pb2.NumberDataPoint( attributes=[ KeyValue( key="a", value=AnyValue(int_value=1), ), KeyValue( key="b", value=AnyValue(bool_value=True), ), ], start_time_unix_nano=1641946015139533244, time_unix_nano=1641946016139533244, as_int=88, ) ], aggregation_temporality=AggregationTemporality.CUMULATIVE, is_monotonic=True, ), ) ], ) ], ) ] ) actual = encode_metrics(metrics_data) self.assertEqual(expected, actual) test_trace_encoder.py000066400000000000000000000451121527070070700353340ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-proto-common/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # pylint: disable=protected-access import unittest from opentelemetry.exporter.otlp.proto.common._internal import ( _encode_span_id, _encode_trace_id, ) from opentelemetry.exporter.otlp.proto.common._internal.trace_encoder import ( _SPAN_KIND_MAP, _encode_status, ) from opentelemetry.exporter.otlp.proto.common.trace_encoder import encode_spans from opentelemetry.proto.collector.trace.v1.trace_service_pb2 import ( ExportTraceServiceRequest as PB2ExportTraceServiceRequest, ) from opentelemetry.proto.common.v1.common_pb2 import AnyValue as PB2AnyValue from opentelemetry.proto.common.v1.common_pb2 import ( InstrumentationScope as PB2InstrumentationScope, ) from opentelemetry.proto.common.v1.common_pb2 import KeyValue as PB2KeyValue from opentelemetry.proto.resource.v1.resource_pb2 import ( Resource as PB2Resource, ) from opentelemetry.proto.trace.v1.trace_pb2 import ( ResourceSpans as PB2ResourceSpans, ) from opentelemetry.proto.trace.v1.trace_pb2 import ScopeSpans as PB2ScopeSpans from opentelemetry.proto.trace.v1.trace_pb2 import Span as PB2SPan from opentelemetry.proto.trace.v1.trace_pb2 import Status as PB2Status from opentelemetry.sdk.trace import Event as SDKEvent from opentelemetry.sdk.trace import Resource as SDKResource from opentelemetry.sdk.trace import SpanContext as SDKSpanContext from opentelemetry.sdk.trace import _Span as SDKSpan from opentelemetry.sdk.util.instrumentation import ( InstrumentationScope as SDKInstrumentationScope, ) from opentelemetry.trace import Link as SDKLink from opentelemetry.trace import SpanKind as SDKSpanKind from opentelemetry.trace import TraceFlags as SDKTraceFlags from opentelemetry.trace.status import Status as SDKStatus from opentelemetry.trace.status import StatusCode as SDKStatusCode class TestOTLPTraceEncoder(unittest.TestCase): def test_encode_spans(self): otel_spans, expected_encoding = self.get_exhaustive_test_spans() self.assertEqual(encode_spans(otel_spans), expected_encoding) @staticmethod def get_exhaustive_otel_span_list() -> list[SDKSpan]: trace_id = 0x3E0C63257DE34C926F9EFCD03927272E base_time = 683647322 * 10**9 # in ns start_times = ( base_time, base_time + 150 * 10**6, base_time + 300 * 10**6, base_time + 400 * 10**6, base_time + 500 * 10**6, base_time + 600 * 10**6, ) end_times = ( start_times[0] + (50 * 10**6), start_times[1] + (100 * 10**6), start_times[2] + (200 * 10**6), start_times[3] + (300 * 10**6), start_times[4] + (400 * 10**6), start_times[5] + (500 * 10**6), ) parent_span_context = SDKSpanContext(trace_id, 0x1111111111111111, is_remote=True) other_context = SDKSpanContext(trace_id, 0x2222222222222222, is_remote=False) span1 = SDKSpan( name="test-span-1", context=SDKSpanContext( trace_id, 0x34BF92DEEFC58C92, is_remote=False, trace_flags=SDKTraceFlags(SDKTraceFlags.SAMPLED), ), parent=parent_span_context, events=( SDKEvent( name="event0", timestamp=base_time + 50 * 10**6, attributes={ "annotation_bool": True, "annotation_string": "annotation_test", "key_float": 0.3, }, ), ), links=(SDKLink(context=other_context, attributes={"key_bool": True}),), resource=SDKResource({}, "resource_schema_url"), ) span1.start(start_time=start_times[0]) span1.set_attribute("key_bool", False) span1.set_attribute("key_string", "hello_world") span1.set_attribute("key_float", 111.22) span1.set_status(SDKStatus(SDKStatusCode.ERROR, "Example description")) span1.end(end_time=end_times[0]) span2 = SDKSpan( name="test-span-2", context=parent_span_context, parent=None, resource=SDKResource(attributes={"key_resource": "some_resource"}), ) span2.start(start_time=start_times[1]) span2.end(end_time=end_times[1]) span3 = SDKSpan( name="test-span-3", context=other_context, parent=None, resource=SDKResource(attributes={"key_resource": "some_resource"}), ) span3.start(start_time=start_times[2]) span3.set_attribute("key_string", "hello_world") span3.end(end_time=end_times[2]) span4 = SDKSpan( name="test-span-4", context=other_context, parent=None, resource=SDKResource({}, "resource_schema_url"), instrumentation_scope=SDKInstrumentationScope(name="name", version="version"), ) span4.start(start_time=start_times[3]) span4.end(end_time=end_times[3]) span5 = SDKSpan( name="test-span-5", context=other_context, parent=None, resource=SDKResource( attributes={"key_resource": "another_resource"}, schema_url="resource_schema_url", ), instrumentation_scope=SDKInstrumentationScope( name="scope_1_name", version="scope_1_version", schema_url="scope_1_schema_url", ), ) span5.start(start_time=start_times[4]) span5.end(end_time=end_times[4]) span6 = SDKSpan( name="test-span-6", context=other_context, parent=None, resource=SDKResource( attributes={"key_resource": "another_resource"}, schema_url="resource_schema_url", ), instrumentation_scope=SDKInstrumentationScope( name="scope_2_name", version="scope_2_version", schema_url="scope_2_schema_url", attributes={"one": "1", "two": 2}, ), ) span6.start(start_time=start_times[5]) span6.end(end_time=end_times[5]) return [span1, span2, span3, span4, span5, span6] def get_exhaustive_test_spans( self, ) -> tuple[list[SDKSpan], PB2ExportTraceServiceRequest]: otel_spans = self.get_exhaustive_otel_span_list() trace_id = _encode_trace_id(otel_spans[0].context.trace_id) span_kind = _SPAN_KIND_MAP[SDKSpanKind.INTERNAL] pb2_service_request = PB2ExportTraceServiceRequest( resource_spans=[ PB2ResourceSpans( schema_url="resource_schema_url", resource=PB2Resource(), scope_spans=[ PB2ScopeSpans( scope=PB2InstrumentationScope(), spans=[ PB2SPan( trace_id=trace_id, span_id=_encode_span_id(otel_spans[0].context.span_id), trace_state=None, parent_span_id=_encode_span_id(otel_spans[0].parent.span_id), name=otel_spans[0].name, kind=span_kind, start_time_unix_nano=otel_spans[0].start_time, end_time_unix_nano=otel_spans[0].end_time, attributes=[ PB2KeyValue( key="key_bool", value=PB2AnyValue(bool_value=False), ), PB2KeyValue( key="key_string", value=PB2AnyValue(string_value="hello_world"), ), PB2KeyValue( key="key_float", value=PB2AnyValue(double_value=111.22), ), ], events=[ PB2SPan.Event( name="event0", time_unix_nano=otel_spans[0].events[0].timestamp, attributes=[ PB2KeyValue( key="annotation_bool", value=PB2AnyValue(bool_value=True), ), PB2KeyValue( key="annotation_string", value=PB2AnyValue(string_value="annotation_test"), ), PB2KeyValue( key="key_float", value=PB2AnyValue(double_value=0.3), ), ], ) ], links=[ PB2SPan.Link( trace_id=_encode_trace_id(otel_spans[0].links[0].context.trace_id), span_id=_encode_span_id(otel_spans[0].links[0].context.span_id), attributes=[ PB2KeyValue( key="key_bool", value=PB2AnyValue(bool_value=True), ), ], flags=0x100, ) ], status=PB2Status( code=SDKStatusCode.ERROR.value, message="Example description", ), flags=0x300, ) ], ), PB2ScopeSpans( scope=PB2InstrumentationScope( name="name", version="version", ), spans=[ PB2SPan( trace_id=trace_id, span_id=_encode_span_id(otel_spans[3].context.span_id), trace_state=None, parent_span_id=None, name=otel_spans[3].name, kind=span_kind, start_time_unix_nano=otel_spans[3].start_time, end_time_unix_nano=otel_spans[3].end_time, attributes=None, events=None, links=None, status={}, flags=0x100, ) ], ), ], ), PB2ResourceSpans( resource=PB2Resource( attributes=[ PB2KeyValue( key="key_resource", value=PB2AnyValue(string_value="some_resource"), ) ] ), scope_spans=[ PB2ScopeSpans( scope=PB2InstrumentationScope(), spans=[ PB2SPan( trace_id=trace_id, span_id=_encode_span_id(otel_spans[1].context.span_id), trace_state=None, parent_span_id=None, name=otel_spans[1].name, kind=span_kind, start_time_unix_nano=otel_spans[1].start_time, end_time_unix_nano=otel_spans[1].end_time, attributes=None, events=None, links=None, status={}, flags=0x100, ), PB2SPan( trace_id=trace_id, span_id=_encode_span_id(otel_spans[2].context.span_id), trace_state=None, parent_span_id=None, name=otel_spans[2].name, kind=span_kind, start_time_unix_nano=otel_spans[2].start_time, end_time_unix_nano=otel_spans[2].end_time, attributes=[ PB2KeyValue( key="key_string", value=PB2AnyValue(string_value="hello_world"), ), ], events=None, links=None, status={}, flags=0x100, ), ], ) ], ), PB2ResourceSpans( resource=PB2Resource( attributes=[ PB2KeyValue( key="key_resource", value=PB2AnyValue(string_value="another_resource"), ), ], ), schema_url="resource_schema_url", scope_spans=[ PB2ScopeSpans( scope=PB2InstrumentationScope(name="scope_1_name", version="scope_1_version"), schema_url="scope_1_schema_url", spans=[ PB2SPan( trace_id=trace_id, span_id=_encode_span_id(otel_spans[4].context.span_id), trace_state=None, parent_span_id=None, name=otel_spans[4].name, kind=span_kind, start_time_unix_nano=otel_spans[4].start_time, end_time_unix_nano=otel_spans[4].end_time, attributes=None, events=None, links=None, status={}, flags=0x100, ), ], ), PB2ScopeSpans( scope=PB2InstrumentationScope( name="scope_2_name", version="scope_2_version", attributes=[ PB2KeyValue( key="one", value=PB2AnyValue(string_value="1"), ), PB2KeyValue( key="two", value=PB2AnyValue(int_value=2), ), ], ), schema_url="scope_2_schema_url", spans=[ PB2SPan( trace_id=trace_id, span_id=_encode_span_id(otel_spans[5].context.span_id), trace_state=None, parent_span_id=None, name=otel_spans[5].name, kind=span_kind, start_time_unix_nano=otel_spans[5].start_time, end_time_unix_nano=otel_spans[5].end_time, attributes=None, events=None, links=None, status={}, flags=0x100, ), ], ), ], ), ] ) return otel_spans, pb2_service_request def test_encode_status_code_translations(self): self.assertEqual( _encode_status(SDKStatus(status_code=SDKStatusCode.UNSET)), PB2Status( code=SDKStatusCode.UNSET.value, ), ) self.assertEqual( _encode_status(SDKStatus(status_code=SDKStatusCode.OK)), PB2Status( code=SDKStatusCode.OK.value, ), ) self.assertEqual( _encode_status(SDKStatus(status_code=SDKStatusCode.ERROR)), PB2Status( code=SDKStatusCode.ERROR.value, ), ) python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-proto-grpc/000077500000000000000000000000001527070070700275035ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-proto-grpc/LICENSE000066400000000000000000000261351527070070700305170ustar00rootroot00000000000000 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. python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-proto-grpc/README.rst000066400000000000000000000016041527070070700311730ustar00rootroot00000000000000OpenTelemetry Collector Protobuf over gRPC Exporter =================================================== |pypi| .. |pypi| image:: https://badge.fury.io/py/opentelemetry-exporter-otlp-proto-grpc.svg :target: https://pypi.org/project/opentelemetry-exporter-otlp-proto-grpc/ This library allows to export data to the OpenTelemetry Collector using the OpenTelemetry Protocol using Protobuf over gRPC. Installation ------------ :: pip install opentelemetry-exporter-otlp-proto-grpc References ---------- * `OpenTelemetry Collector Exporter `_ * `OpenTelemetry Collector `_ * `OpenTelemetry `_ * `OpenTelemetry Protocol Specification `_ benchmark-requirements.txt000066400000000000000000000000301527070070700346310ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-proto-grpcpytest-benchmark==4.0.0 python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-proto-grpc/benchmarks/000077500000000000000000000000001527070070700316205ustar00rootroot00000000000000test_benchmark_trace_exporter.py000066400000000000000000000043461527070070700402210ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-proto-grpc/benchmarks# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # pylint: disable=invalid-name from unittest.mock import patch from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import ( OTLPSpanExporter, ) from opentelemetry.sdk.trace import TracerProvider, sampling from opentelemetry.sdk.trace.export import ( BatchSpanProcessor, SimpleSpanProcessor, ) def get_tracer_with_processor(span_processor_class): span_processor = span_processor_class(OTLPSpanExporter()) tracer = TracerProvider( active_span_processor=span_processor, sampler=sampling.DEFAULT_ON, ).get_tracer("pipeline_benchmark_tracer") return tracer class MockTraceServiceStub: def __init__(self, channel): self.Export = lambda *args, **kwargs: None @patch( "opentelemetry.exporter.otlp.proto.grpc.trace_exporter.TraceServiceStub", new=MockTraceServiceStub, ) def test_simple_span_processor(benchmark): tracer = get_tracer_with_processor(SimpleSpanProcessor) def create_spans_to_be_exported(): span = tracer.start_span( "benchmarkedSpan", ) for i in range(10): span.set_attribute( f"benchmarkAttribute_{i}", f"benchmarkAttrValue_{i}", ) span.end() benchmark(create_spans_to_be_exported) @patch( "opentelemetry.exporter.otlp.proto.grpc.trace_exporter.TraceServiceStub", new=MockTraceServiceStub, ) def test_batch_span_processor(benchmark): """Runs benchmark tests using BatchSpanProcessor. One particular call by pytest-benchmark will be much more expensive since the batch export thread will activate and consume a lot of CPU to process all the spans. For this reason, focus on the average measurement. Do not focus on the min/max measurements which will be misleading. """ tracer = get_tracer_with_processor(BatchSpanProcessor) def create_spans_to_be_exported(): span = tracer.start_span( "benchmarkedSpan", ) for i in range(10): span.set_attribute( f"benchmarkAttribute_{i}", f"benchmarkAttrValue_{i}", ) span.end() benchmark(create_spans_to_be_exported) python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-proto-grpc/pyproject.toml000066400000000000000000000044361527070070700324260ustar00rootroot00000000000000[build-system] requires = ["hatchling"] build-backend = "hatchling.build" [project] name = "opentelemetry-exporter-otlp-proto-grpc" dynamic = ["version"] description = "OpenTelemetry Collector Protobuf over gRPC Exporter" 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", "Framework :: OpenTelemetry", "Framework :: OpenTelemetry :: Exporters", "Intended Audience :: Developers", "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 = [ "googleapis-common-protos ~= 1.57", "grpcio >= 1.63.2, < 2.0.0; python_version < '3.13'", "grpcio >= 1.66.2, < 2.0.0; python_version == '3.13'", "grpcio >= 1.75.1, < 2.0.0; python_version >= '3.14'", "opentelemetry-api ~= 1.15", "opentelemetry-proto == 1.45.2", "opentelemetry-sdk ~= 1.45.2", "opentelemetry-exporter-otlp-proto-common == 1.45.2", "opentelemetry-exporter-otlp-common == 0.66b2", "typing-extensions >= 4.6.0", ] [project.entry-points.opentelemetry_logs_exporter] otlp_proto_grpc = "opentelemetry.exporter.otlp.proto.grpc._log_exporter:OTLPLogExporter" [project.entry-points.opentelemetry_metrics_exporter] otlp_proto_grpc = "opentelemetry.exporter.otlp.proto.grpc.metric_exporter:OTLPMetricExporter" [project.entry-points.opentelemetry_traces_exporter] otlp_proto_grpc = "opentelemetry.exporter.otlp.proto.grpc.trace_exporter:OTLPSpanExporter" [project.urls] Homepage = "https://github.com/open-telemetry/opentelemetry-python/tree/main/exporter/opentelemetry-exporter-otlp-proto-grpc" Repository = "https://github.com/open-telemetry/opentelemetry-python" [project.optional-dependencies] gcp-auth = [ "opentelemetry-exporter-credential-provider-gcp >= 0.59b0", ] [tool.hatch.version] path = "src/opentelemetry/exporter/otlp/proto/grpc/version/__init__.py" [tool.hatch.build.targets.sdist] include = [ "/src", "/tests", ] [tool.hatch.build.targets.wheel] packages = ["src/opentelemetry"] python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-proto-grpc/src/000077500000000000000000000000001527070070700302725ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/000077500000000000000000000000001527070070700331665ustar00rootroot00000000000000exporter/000077500000000000000000000000001527070070700347575ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetryotlp/000077500000000000000000000000001527070070700357355ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporterproto/000077500000000000000000000000001527070070700371005ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlpgrpc/000077500000000000000000000000001527070070700400335ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto__init__.py000066400000000000000000000042101527070070700421410ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 """ This library allows to export tracing data to an OTLP collector. Usage ----- The **OTLP Span Exporter** allows to export `OpenTelemetry`_ traces to the `OTLP`_ collector. You can configure the exporter with the following environment variables: - :envvar:`OTEL_EXPORTER_OTLP_TRACES_TIMEOUT` - :envvar:`OTEL_EXPORTER_OTLP_TRACES_PROTOCOL` - :envvar:`OTEL_EXPORTER_OTLP_TRACES_HEADERS` - :envvar:`OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` - :envvar:`OTEL_EXPORTER_OTLP_TRACES_COMPRESSION` - :envvar:`OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE` - :envvar:`OTEL_EXPORTER_OTLP_TIMEOUT` - :envvar:`OTEL_EXPORTER_OTLP_PROTOCOL` - :envvar:`OTEL_EXPORTER_OTLP_HEADERS` - :envvar:`OTEL_EXPORTER_OTLP_ENDPOINT` - :envvar:`OTEL_EXPORTER_OTLP_COMPRESSION` - :envvar:`OTEL_EXPORTER_OTLP_CERTIFICATE` .. _OTLP: https://github.com/open-telemetry/opentelemetry-collector/ .. _OpenTelemetry: https://github.com/open-telemetry/opentelemetry-python/ .. code:: python from opentelemetry import trace from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchSpanProcessor # Resource can be required for some backends, e.g. Jaeger # If resource wouldn't be set - traces wouldn't appears in Jaeger resource = Resource.create({ "service.name": "service" }) trace.set_tracer_provider(TracerProvider(resource=resource)) tracer = trace.get_tracer(__name__) otlp_exporter = OTLPSpanExporter(endpoint="http://localhost:4317", insecure=True) span_processor = BatchSpanProcessor(otlp_exporter) trace.get_tracer_provider().add_span_processor(span_processor) with tracer.start_as_current_span("foo"): print("Hello world!") API --- """ from .version import __version__ _USER_AGENT_HEADER_VALUE = "OTel-OTLP-Exporter-Python/" + __version__ _OTLP_GRPC_CHANNEL_OPTIONS = [ # this will appear in the http User-Agent header ("grpc.primary_user_agent", _USER_AGENT_HEADER_VALUE) ] _log_exporter/000077500000000000000000000000001527070070700427035ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc__init__.py000066400000000000000000000110631527070070700450150ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/_log_exporter# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from collections.abc import Iterable, Sequence from collections.abc import Sequence as TypingSequence from os import environ from typing import Literal from grpc import ChannelCredentials, Compression, StatusCode from opentelemetry.exporter.otlp.proto.common._log_encoder import encode_logs from opentelemetry.exporter.otlp.proto.grpc.exporter import ( OTLPExporterMixin, _get_credentials, environ_to_compression, ) from opentelemetry.metrics import MeterProvider from opentelemetry.proto.collector.logs.v1.logs_service_pb2 import ( ExportLogsServiceRequest, ) from opentelemetry.proto.collector.logs.v1.logs_service_pb2_grpc import ( LogsServiceStub, ) from opentelemetry.sdk._logs import ReadableLogRecord from opentelemetry.sdk._logs.export import ( LogRecordExporter, LogRecordExportResult, ) from opentelemetry.sdk.environment_variables import ( _OTEL_PYTHON_EXPORTER_OTLP_GRPC_LOGS_CREDENTIAL_PROVIDER, OTEL_EXPORTER_OTLP_LOGS_CERTIFICATE, OTEL_EXPORTER_OTLP_LOGS_CLIENT_CERTIFICATE, OTEL_EXPORTER_OTLP_LOGS_CLIENT_KEY, OTEL_EXPORTER_OTLP_LOGS_COMPRESSION, OTEL_EXPORTER_OTLP_LOGS_ENDPOINT, OTEL_EXPORTER_OTLP_LOGS_HEADERS, OTEL_EXPORTER_OTLP_LOGS_INSECURE, OTEL_EXPORTER_OTLP_LOGS_TIMEOUT, ) from opentelemetry.semconv._incubating.attributes.otel_attributes import ( OtelComponentTypeValues, ) class OTLPLogExporter( LogRecordExporter, OTLPExporterMixin[ Sequence[ReadableLogRecord], ExportLogsServiceRequest, LogRecordExportResult, LogsServiceStub, ], ): def __init__( self, endpoint: str | None = None, insecure: bool | None = None, credentials: ChannelCredentials | None = None, headers: TypingSequence[tuple[str, str]] | dict[str, str] | str | None = None, timeout: float | None = None, compression: Compression | None = None, channel_options: tuple[tuple[str, str]] | None = None, retryable_error_codes: Iterable[StatusCode] | None = None, *, meter_provider: MeterProvider | None = None, ): insecure_logs = environ.get(OTEL_EXPORTER_OTLP_LOGS_INSECURE) if insecure is None and insecure_logs is not None: insecure = insecure_logs.lower() == "true" if not insecure and environ.get(OTEL_EXPORTER_OTLP_LOGS_CERTIFICATE) is not None: credentials = _get_credentials( credentials, _OTEL_PYTHON_EXPORTER_OTLP_GRPC_LOGS_CREDENTIAL_PROVIDER, OTEL_EXPORTER_OTLP_LOGS_CERTIFICATE, OTEL_EXPORTER_OTLP_LOGS_CLIENT_KEY, OTEL_EXPORTER_OTLP_LOGS_CLIENT_CERTIFICATE, ) environ_timeout = environ.get(OTEL_EXPORTER_OTLP_LOGS_TIMEOUT) environ_timeout = float(environ_timeout) if environ_timeout is not None else None compression = ( environ_to_compression(OTEL_EXPORTER_OTLP_LOGS_COMPRESSION) if compression is None else compression ) OTLPExporterMixin.__init__( self, endpoint=endpoint or environ.get(OTEL_EXPORTER_OTLP_LOGS_ENDPOINT), insecure=insecure, credentials=credentials, headers=headers or environ.get(OTEL_EXPORTER_OTLP_LOGS_HEADERS), timeout=timeout or environ_timeout, compression=compression, stub=LogsServiceStub, result=LogRecordExportResult, channel_options=channel_options, retryable_error_codes=retryable_error_codes, component_type=OtelComponentTypeValues.OTLP_GRPC_LOG_EXPORTER, signal="logs", meter_provider=meter_provider, ) def _translate_data(self, data: Sequence[ReadableLogRecord]) -> ExportLogsServiceRequest: return encode_logs(data) def _count_data(self, data: Sequence[ReadableLogRecord]): return len(data) def export( # type: ignore [reportIncompatibleMethodOverride] self, batch: Sequence[ReadableLogRecord], ) -> Literal[LogRecordExportResult.SUCCESS, LogRecordExportResult.FAILURE]: return OTLPExporterMixin._export(self, batch) def shutdown(self, timeout_millis: float = 30_000, **kwargs) -> None: OTLPExporterMixin.shutdown(self, timeout_millis=timeout_millis) def force_flush(self, timeout_millis: int = 10_000) -> bool: """Nothing is buffered in this exporter, so this method does nothing.""" return True @property def _exporting(self) -> str: return "logs" exporter.py000066400000000000000000000466351527070070700422730ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 """OTLP Exporter This module provides a mixin class for OTLP exporters that send telemetry data to an OTLP-compatible receiver via gRPC. It includes a configurable reconnection logic to handle transient collector outages. """ import os import random import threading from abc import ABC, abstractmethod from collections.abc import ( Callable, Iterable, Sequence, # noqa: F401 ) from collections.abc import Sequence as TypingSequence from logging import getLogger from os import environ from time import time from typing import ( # noqa: F401 Any, Generic, Literal, NewType, Optional, TypeVar, ) from urllib.parse import urlparse from google.rpc.error_details_pb2 import RetryInfo from typing_extensions import deprecated from grpc import ( ChannelCredentials, Compression, RpcError, StatusCode, insecure_channel, secure_channel, ssl_channel_credentials, ) from opentelemetry.exporter.otlp.common._exporter_metrics import ( create_exporter_metrics, ) from opentelemetry.exporter.otlp.proto.common._internal import ( _get_resource_data, ) from opentelemetry.exporter.otlp.proto.grpc import ( _OTLP_GRPC_CHANNEL_OPTIONS, ) from opentelemetry.metrics import MeterProvider from opentelemetry.proto.collector.logs.v1.logs_service_pb2 import ( ExportLogsServiceRequest, ) from opentelemetry.proto.collector.logs.v1.logs_service_pb2_grpc import ( LogsServiceStub, ) from opentelemetry.proto.collector.metrics.v1.metrics_service_pb2 import ( ExportMetricsServiceRequest, ) from opentelemetry.proto.collector.metrics.v1.metrics_service_pb2_grpc import ( MetricsServiceStub, ) from opentelemetry.proto.collector.trace.v1.trace_service_pb2 import ( ExportTraceServiceRequest, ) from opentelemetry.proto.collector.trace.v1.trace_service_pb2_grpc import ( TraceServiceStub, ) from opentelemetry.proto.common.v1.common_pb2 import ( # noqa: F401 AnyValue, ArrayValue, KeyValue, ) from opentelemetry.proto.resource.v1.resource_pb2 import Resource # noqa: F401 from opentelemetry.sdk._logs import ReadableLogRecord from opentelemetry.sdk._logs.export import LogRecordExportResult from opentelemetry.sdk._shared_internal import DuplicateFilter from opentelemetry.sdk.environment_variables import ( _OTEL_PYTHON_EXPORTER_OTLP_GRPC_CREDENTIAL_PROVIDER, _OTEL_PYTHON_EXPORTER_OTLP_GRPC_RETRYABLE_ERROR_CODES, OTEL_EXPORTER_OTLP_CERTIFICATE, OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE, OTEL_EXPORTER_OTLP_CLIENT_KEY, OTEL_EXPORTER_OTLP_COMPRESSION, OTEL_EXPORTER_OTLP_ENDPOINT, OTEL_EXPORTER_OTLP_HEADERS, OTEL_EXPORTER_OTLP_INSECURE, OTEL_EXPORTER_OTLP_TIMEOUT, OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED, ) from opentelemetry.sdk.metrics.export import MetricExportResult, MetricsData from opentelemetry.sdk.resources import Resource as SDKResource from opentelemetry.sdk.trace import ReadableSpan from opentelemetry.sdk.trace.export import SpanExportResult from opentelemetry.semconv._incubating.attributes.otel_attributes import ( OtelComponentTypeValues, ) from opentelemetry.semconv._incubating.attributes.rpc_attributes import ( RPC_RESPONSE_STATUS_CODE, ) from opentelemetry.util._importlib_metadata import entry_points from opentelemetry.util.re import parse_env_headers _RETRYABLE_ERROR_CODES = frozenset( [ StatusCode.CANCELLED, StatusCode.DEADLINE_EXCEEDED, StatusCode.RESOURCE_EXHAUSTED, StatusCode.ABORTED, StatusCode.OUT_OF_RANGE, StatusCode.UNAVAILABLE, StatusCode.DATA_LOSS, ] ) _MAX_RETRYS = 6 logger = getLogger(__name__) # This prevents logs generated when a log fails to be written to generate another log which fails to be written etc. etc. logger.addFilter(DuplicateFilter()) SDKDataT = TypeVar( "SDKDataT", TypingSequence[ReadableLogRecord], MetricsData, TypingSequence[ReadableSpan], ) ResourceDataT = TypeVar("ResourceDataT") TypingResourceT = TypeVar("TypingResourceT") ExportServiceRequestT = TypeVar( "ExportServiceRequestT", ExportTraceServiceRequest, ExportMetricsServiceRequest, ExportLogsServiceRequest, ) ExportResultT = TypeVar( "ExportResultT", LogRecordExportResult, MetricExportResult, SpanExportResult, ) ExportStubT = TypeVar("ExportStubT", TraceServiceStub, MetricsServiceStub, LogsServiceStub) _ENVIRON_TO_COMPRESSION = { None: None, "gzip": Compression.Gzip, } class InvalidCompressionValueException(Exception): def __init__(self, environ_key: str, environ_value: str): super().__init__(f'Invalid value "{environ_value}" for compression envvar {environ_key}') def environ_to_compression(environ_key: str) -> Compression | None: environ_value = environ[environ_key].lower().strip() if environ_key in environ else None if environ_value not in _ENVIRON_TO_COMPRESSION and environ_value is not None: raise InvalidCompressionValueException(environ_key, environ_value) return _ENVIRON_TO_COMPRESSION[environ_value] @deprecated( "Use one of the encoders from opentelemetry-exporter-otlp-proto-common instead. Deprecated since version 1.18.0.", ) def get_resource_data( sdk_resource_scope_data: dict[SDKResource, ResourceDataT], resource_class: Callable[..., TypingResourceT], name: str, ) -> list[TypingResourceT]: return _get_resource_data(sdk_resource_scope_data, resource_class, name) def _read_file(file_path: str) -> bytes | None: try: with open(file_path, "rb") as file: return file.read() except FileNotFoundError as e: logger.exception( "Failed to read file: %s. Please check if the file exists and is accessible.", e.filename, ) return None def _load_credentials( certificate_file: str | None, client_key_file: str | None, client_certificate_file: str | None, ) -> ChannelCredentials: root_certificates = _read_file(certificate_file) if certificate_file else None private_key = _read_file(client_key_file) if client_key_file else None certificate_chain = _read_file(client_certificate_file) if client_certificate_file else None return ssl_channel_credentials( root_certificates=root_certificates, private_key=private_key, certificate_chain=certificate_chain, ) def _get_credentials( creds: ChannelCredentials | None, credential_entry_point_env_key: str, certificate_file_env_key: str, client_key_file_env_key: str, client_certificate_file_env_key: str, ) -> ChannelCredentials: if creds is not None: return creds _credential_env = environ.get(credential_entry_point_env_key) if _credential_env: try: maybe_channel_creds = next( iter( entry_points( group="opentelemetry_otlp_credential_provider", name=_credential_env, ) ) ).load()() except StopIteration: raise RuntimeError( f"Requested component '{_credential_env}' not found in " f"entry point 'opentelemetry_otlp_credential_provider'" ) if isinstance(maybe_channel_creds, ChannelCredentials): return maybe_channel_creds else: raise RuntimeError( f"Requested component '{_credential_env}' is of type {type(maybe_channel_creds)}" f" must be of type `grpc.ChannelCredentials`." ) certificate_file = environ.get(certificate_file_env_key) if certificate_file: client_key_file = environ.get(client_key_file_env_key) client_certificate_file = environ.get(client_certificate_file_env_key) credentials = _load_credentials(certificate_file, client_key_file, client_certificate_file) if credentials is not None: return credentials return ssl_channel_credentials() # pylint: disable=no-member class OTLPExporterMixin(ABC, Generic[SDKDataT, ExportServiceRequestT, ExportResultT, ExportStubT]): """OTLP gRPC exporter mixin. This class provides the base functionality for OTLP exporters that send telemetry data (spans or metrics) to an OTLP-compatible receiver via gRPC. It includes a configurable reconnection mechanism to handle transient receiver outages. Args: endpoint: OTLP-compatible receiver endpoint insecure: Connection type credentials: ChannelCredentials object for server authentication headers: Headers to send when exporting timeout: Backend request timeout in seconds compression: gRPC compression method to use channel_options: gRPC channel options """ def __init__( self, stub: ExportStubT, result: ExportResultT, endpoint: str | None = None, insecure: bool | None = None, credentials: ChannelCredentials | None = None, headers: TypingSequence[tuple[str, str]] | dict[str, str] | str | None = None, timeout: float | None = None, compression: Compression | None = None, channel_options: tuple[tuple[str, str]] | None = None, retryable_error_codes: Iterable[StatusCode] | None = None, *, component_type: OtelComponentTypeValues | None = None, signal: Literal["traces", "metrics", "logs"] = "traces", meter_provider: MeterProvider | None = None, ): super().__init__() self._result = result self._stub = stub self._endpoint = endpoint or environ.get(OTEL_EXPORTER_OTLP_ENDPOINT, "http://localhost:4317") parsed_url = urlparse(self._endpoint) if parsed_url.scheme == "https": insecure = False insecure_exporter = environ.get(OTEL_EXPORTER_OTLP_INSECURE) if insecure is None: if insecure_exporter is not None: insecure = insecure_exporter.lower() == "true" else: insecure = parsed_url.scheme == "http" if parsed_url.netloc: self._endpoint = parsed_url.netloc self._insecure = insecure self._credentials = credentials self._headers = headers or environ.get(OTEL_EXPORTER_OTLP_HEADERS) if isinstance(self._headers, str): temp_headers = parse_env_headers(self._headers, liberal=True) self._headers = tuple(temp_headers.items()) elif isinstance(self._headers, dict): self._headers = tuple(self._headers.items()) if self._headers is None: self._headers = tuple() if channel_options: # merge the default channel options with the one passed as parameter overridden_options = {opt_name for (opt_name, _) in channel_options} default_options = tuple( (opt_name, opt_value) for opt_name, opt_value in _OTLP_GRPC_CHANNEL_OPTIONS if opt_name not in overridden_options ) self._channel_options = default_options + channel_options else: self._channel_options = tuple(_OTLP_GRPC_CHANNEL_OPTIONS) self._timeout = timeout or float(environ.get(OTEL_EXPORTER_OTLP_TIMEOUT, 10)) self._collector_kwargs = None self._compression = ( environ_to_compression(OTEL_EXPORTER_OTLP_COMPRESSION) if compression is None else compression ) or Compression.NoCompression self._retryable_error_codes = retryable_error_codes or os.environ.get( _OTEL_PYTHON_EXPORTER_OTLP_GRPC_RETRYABLE_ERROR_CODES ) if isinstance(self._retryable_error_codes, str): self._retryable_error_codes = frozenset( StatusCode[code.strip().upper()] for code in self._retryable_error_codes.split(",") if code.strip() ) elif self._retryable_error_codes is not None: self._retryable_error_codes = frozenset(self._retryable_error_codes) else: self._retryable_error_codes = _RETRYABLE_ERROR_CODES self._channel = None self._client = None self._shutdown_in_progress = threading.Event() self._shutdown = False if not self._insecure: self._credentials = _get_credentials( self._credentials, _OTEL_PYTHON_EXPORTER_OTLP_GRPC_CREDENTIAL_PROVIDER, OTEL_EXPORTER_OTLP_CERTIFICATE, OTEL_EXPORTER_OTLP_CLIENT_KEY, OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE, ) self._component_type = component_type self._signal: Literal["traces", "metrics", "logs"] = signal self._metrics = create_exporter_metrics( self._component_type, signal, self._endpoint, meter_provider, os.environ.get(OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED, "").strip().lower() == "true", ) self._initialize_channel_and_stub() def _initialize_channel_and_stub(self): """ Create a new gRPC channel and stub. This method is used during initialization and by the reconnection mechanism to reinitialize the channel on transient errors. """ if self._insecure: self._channel = insecure_channel( self._endpoint, compression=self._compression, options=self._channel_options, ) else: assert self._credentials is not None self._channel = secure_channel( self._endpoint, self._credentials, compression=self._compression, options=self._channel_options, ) self._client = self._stub(self._channel) # type: ignore [reportCallIssue] @abstractmethod def _translate_data( self, data: SDKDataT, ) -> ExportServiceRequestT: pass @abstractmethod def _count_data( self, data: SDKDataT, ) -> int: pass def _export( self, data: SDKDataT, ) -> ExportResultT: if self._shutdown: logger.warning("Exporter already shutdown, ignoring batch") return self._result.FAILURE # type: ignore [reportReturnType] with self._metrics.export_operation(self._count_data(data)) as result: # FIXME remove this check if the export type for traces # gets updated to a class that represents the proto # TracesData and use the code below instead. deadline_sec = time() + self._timeout for retry_num in range(_MAX_RETRYS): try: if self._client is None: return self._result.FAILURE self._client.Export( request=self._translate_data(data), metadata=self._headers, timeout=deadline_sec - time(), ) return self._result.SUCCESS # type: ignore [reportReturnType] except RpcError as error: retry_info_bin = dict(error.trailing_metadata()).get( # type: ignore [reportAttributeAccessIssue] "google.rpc.retryinfo-bin" # type: ignore [reportArgumentType] ) # multiplying by a random number between .8 and 1.2 introduces a +/20% jitter to each backoff. backoff_seconds = 2**retry_num * random.uniform(0.8, 1.2) if retry_info_bin is not None: retry_info = RetryInfo() retry_info.ParseFromString(retry_info_bin) backoff_seconds = retry_info.retry_delay.seconds + retry_info.retry_delay.nanos / 1.0e9 # For UNAVAILABLE errors, reinitialize the channel to force reconnection if error.code() == StatusCode.UNAVAILABLE and retry_num == 0: # type: ignore logger.debug( "Reinitializing gRPC channel for %s exporter due to UNAVAILABLE error", self._exporting, ) try: if self._channel: self._channel.close() except Exception as e: logger.debug( "Error closing channel for %s exporter to %s: %s", self._exporting, self._endpoint, str(e), ) # Enable channel reconnection for subsequent calls self._initialize_channel_and_stub() if ( error.code() not in self._retryable_error_codes # type: ignore [reportAttributeAccessIssue] or retry_num + 1 == _MAX_RETRYS or backoff_seconds > (deadline_sec - time()) or self._shutdown ): logger.error( "Failed to export %s to %s, error code: %s, error details: %s", self._exporting, self._endpoint, error.code(), # type: ignore [reportAttributeAccessIssue] error.details(), exc_info=error.code() == StatusCode.UNKNOWN, # type: ignore [reportAttributeAccessIssue] ) result.error = error result.error_attrs = {RPC_RESPONSE_STATUS_CODE: error.code().name} return self._result.FAILURE # type: ignore [reportReturnType] logger.warning( "Transient error %s encountered while exporting %s to %s, retrying in %.2fs. Error details: %s", error.code(), # type: ignore [reportAttributeAccessIssue] self._exporting, self._endpoint, backoff_seconds, error.details(), ) shutdown = self._shutdown_in_progress.wait(backoff_seconds) if shutdown: logger.warning("Shutdown in progress, aborting retry.") break # Not possible to reach here but the linter is complaining. return self._result.FAILURE # type: ignore [reportReturnType] def shutdown(self, timeout_millis: float = 30_000, **kwargs) -> None: """ Shut down the exporter. Args: timeout_millis: Timeout in milliseconds for shutting down the exporter. """ if self._shutdown: logger.warning("Exporter already shutdown, ignoring call") return self._shutdown = True self._shutdown_in_progress.set() if self._channel: self._channel.close() @property @abstractmethod def _exporting(self) -> str: """ Returns a string that describes the overall exporter, to be used in warning messages. """ def _set_meter_provider(self, meter_provider: MeterProvider) -> None: self._metrics = create_exporter_metrics( self._component_type, self._signal, self._endpoint, meter_provider, os.environ.get(OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED, "").strip().lower() == "true", ) metric_exporter/000077500000000000000000000000001527070070700432465ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc__init__.py000066400000000000000000000245041527070070700453640ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/metric_exporter# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations from collections.abc import Iterable from collections.abc import Sequence as TypingSequence from dataclasses import replace from logging import getLogger from os import environ from grpc import ChannelCredentials, Compression, StatusCode from opentelemetry.exporter.otlp.proto.common._internal.metrics_encoder import ( OTLPMetricExporterMixin, ) from opentelemetry.exporter.otlp.proto.common.metrics_encoder import ( encode_metrics, ) from opentelemetry.exporter.otlp.proto.grpc.exporter import ( # noqa: F401 OTLPExporterMixin, _get_credentials, environ_to_compression, get_resource_data, ) from opentelemetry.metrics import MeterProvider from opentelemetry.proto.collector.metrics.v1.metrics_service_pb2 import ( ExportMetricsServiceRequest, ) from opentelemetry.proto.collector.metrics.v1.metrics_service_pb2_grpc import ( MetricsServiceStub, ) from opentelemetry.proto.common.v1.common_pb2 import ( # noqa: F401 InstrumentationScope, ) from opentelemetry.proto.metrics.v1 import metrics_pb2 as pb2 # noqa: F401 from opentelemetry.sdk.environment_variables import ( _OTEL_PYTHON_EXPORTER_OTLP_GRPC_METRICS_CREDENTIAL_PROVIDER, OTEL_EXPORTER_OTLP_METRICS_CERTIFICATE, OTEL_EXPORTER_OTLP_METRICS_CLIENT_CERTIFICATE, OTEL_EXPORTER_OTLP_METRICS_CLIENT_KEY, OTEL_EXPORTER_OTLP_METRICS_COMPRESSION, OTEL_EXPORTER_OTLP_METRICS_ENDPOINT, OTEL_EXPORTER_OTLP_METRICS_HEADERS, OTEL_EXPORTER_OTLP_METRICS_INSECURE, OTEL_EXPORTER_OTLP_METRICS_TIMEOUT, ) from opentelemetry.sdk.metrics._internal.aggregation import Aggregation from opentelemetry.sdk.metrics.export import ( # noqa: F401 AggregationTemporality, DataPointT, Gauge, Metric, MetricExporter, MetricExportResult, MetricsData, ResourceMetrics, ScopeMetrics, Sum, ) from opentelemetry.sdk.metrics.export import ( # noqa: F401 ExponentialHistogram as ExponentialHistogramType, ) from opentelemetry.sdk.metrics.export import ( # noqa: F401 Histogram as HistogramType, ) from opentelemetry.semconv._incubating.attributes.otel_attributes import ( OtelComponentTypeValues, ) _logger = getLogger(__name__) class OTLPMetricExporter( MetricExporter, OTLPExporterMixin[ MetricsData, ExportMetricsServiceRequest, MetricExportResult, MetricsServiceStub, ], OTLPMetricExporterMixin, ): """OTLP metric exporter Args: endpoint: Target URL to which the exporter is going to send metrics max_export_batch_size: Maximum number of data points to export in a single request. This is to deal with gRPC's 4MB message size limit. If not set there is no limit to the number of data points in a request. If it is set and the number of data points exceeds the max, the request will be split. """ def __init__( self, endpoint: str | None = None, insecure: bool | None = None, credentials: ChannelCredentials | None = None, headers: TypingSequence[tuple[str, str]] | dict[str, str] | str | None = None, timeout: float | None = None, compression: Compression | None = None, preferred_temporality: dict[type, AggregationTemporality] | None = None, preferred_aggregation: dict[type, Aggregation] | None = None, max_export_batch_size: int | None = None, channel_options: tuple[tuple[str, str]] | None = None, retryable_error_codes: Iterable[StatusCode] | None = None, *, meter_provider: MeterProvider | None = None, ): insecure_metrics = environ.get(OTEL_EXPORTER_OTLP_METRICS_INSECURE) if insecure is None and insecure_metrics is not None: insecure = insecure_metrics.lower() == "true" if not insecure and environ.get(OTEL_EXPORTER_OTLP_METRICS_CERTIFICATE) is not None: credentials = _get_credentials( credentials, _OTEL_PYTHON_EXPORTER_OTLP_GRPC_METRICS_CREDENTIAL_PROVIDER, OTEL_EXPORTER_OTLP_METRICS_CERTIFICATE, OTEL_EXPORTER_OTLP_METRICS_CLIENT_KEY, OTEL_EXPORTER_OTLP_METRICS_CLIENT_CERTIFICATE, ) environ_timeout = environ.get(OTEL_EXPORTER_OTLP_METRICS_TIMEOUT) environ_timeout = float(environ_timeout) if environ_timeout is not None else None compression = ( environ_to_compression(OTEL_EXPORTER_OTLP_METRICS_COMPRESSION) if compression is None else compression ) self._common_configuration(preferred_temporality, preferred_aggregation) OTLPExporterMixin.__init__( self, stub=MetricsServiceStub, result=MetricExportResult, endpoint=endpoint or environ.get(OTEL_EXPORTER_OTLP_METRICS_ENDPOINT), insecure=insecure, credentials=credentials, headers=headers or environ.get(OTEL_EXPORTER_OTLP_METRICS_HEADERS), timeout=timeout or environ_timeout, compression=compression, channel_options=channel_options, retryable_error_codes=retryable_error_codes, component_type=OtelComponentTypeValues.OTLP_GRPC_METRIC_EXPORTER, signal="metrics", meter_provider=meter_provider, ) self._max_export_batch_size: int | None = max_export_batch_size def _translate_data( # type: ignore [reportIncompatibleMethodOverride] self, data: MetricsData ) -> ExportMetricsServiceRequest: return encode_metrics(data) def _count_data(self, data: MetricsData): num_items = 0 for resource_metrics in data.resource_metrics: for scope_metrics in resource_metrics.scope_metrics: for metric in scope_metrics.metrics: num_items += len(metric.data.data_points) return num_items def export( self, metrics_data: MetricsData, timeout_millis: float = 10_000, **kwargs, ) -> MetricExportResult: # TODO(#2663): OTLPExporterMixin should pass timeout to gRPC if self._max_export_batch_size is None: return self._export(data=metrics_data) export_result = MetricExportResult.SUCCESS for split_metrics_data in self._split_metrics_data(metrics_data): split_export_result = self._export(data=split_metrics_data) if split_export_result is MetricExportResult.FAILURE: export_result = MetricExportResult.FAILURE return export_result def _split_metrics_data( self, metrics_data: MetricsData, ) -> Iterable[MetricsData]: assert self._max_export_batch_size is not None batch_size: int = 0 split_resource_metrics: list[ResourceMetrics] = [] for resource_metrics in metrics_data.resource_metrics: split_scope_metrics: list[ScopeMetrics] = [] split_resource_metrics.append( replace( resource_metrics, scope_metrics=split_scope_metrics, ) ) for scope_metrics in resource_metrics.scope_metrics: split_metrics: list[Metric] = [] split_scope_metrics.append( replace( scope_metrics, metrics=split_metrics, ) ) for metric in scope_metrics.metrics: split_data_points: list[DataPointT] = [] split_metrics.append( replace( metric, data=replace( metric.data, data_points=split_data_points, ), ) ) for data_point in metric.data.data_points: split_data_points.append(data_point) batch_size += 1 if batch_size >= self._max_export_batch_size: yield MetricsData(resource_metrics=split_resource_metrics) # Reset all the variables batch_size = 0 split_data_points = [] split_metrics = [ replace( metric, data=replace( metric.data, data_points=split_data_points, ), ) ] split_scope_metrics = [ replace( scope_metrics, metrics=split_metrics, ) ] split_resource_metrics = [ replace( resource_metrics, scope_metrics=split_scope_metrics, ) ] if not split_data_points: # If data_points is empty remove the whole metric split_metrics.pop() if not split_metrics: # If metrics is empty remove the whole scope_metrics split_scope_metrics.pop() if not split_scope_metrics: # If scope_metrics is empty remove the whole resource_metrics split_resource_metrics.pop() if batch_size > 0: yield MetricsData(resource_metrics=split_resource_metrics) def shutdown(self, timeout_millis: float = 30_000, **kwargs) -> None: OTLPExporterMixin.shutdown(self, timeout_millis=timeout_millis) def set_meter_provider(self, meter_provider: MeterProvider): return self._set_meter_provider(meter_provider) @property def _exporting(self) -> str: return "metrics" def force_flush(self, timeout_millis: float = 10_000) -> bool: """Nothing is buffered in this exporter, so this method does nothing.""" return True py.typed000066400000000000000000000000001527070070700415200ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpctrace_exporter/000077500000000000000000000000001527070070700430615ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc__init__.py000066400000000000000000000123341527070070700451750ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/trace_exporter# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 """OTLP Span Exporter""" import logging from collections.abc import Iterable, Sequence from collections.abc import Sequence as TypingSequence from os import environ from grpc import ChannelCredentials, Compression, StatusCode from opentelemetry.exporter.otlp.proto.common.trace_encoder import ( encode_spans, ) from opentelemetry.exporter.otlp.proto.grpc.exporter import ( # noqa: F401 OTLPExporterMixin, _get_credentials, environ_to_compression, get_resource_data, ) from opentelemetry.metrics import MeterProvider from opentelemetry.proto.collector.trace.v1.trace_service_pb2 import ( ExportTraceServiceRequest, ) from opentelemetry.proto.collector.trace.v1.trace_service_pb2_grpc import ( TraceServiceStub, ) from opentelemetry.proto.common.v1.common_pb2 import ( # noqa: F401 InstrumentationScope, ) from opentelemetry.proto.trace.v1.trace_pb2 import ( # noqa: F401 ResourceSpans, ScopeSpans, Status, ) from opentelemetry.proto.trace.v1.trace_pb2 import ( # noqa: F401 Span as CollectorSpan, ) from opentelemetry.sdk.environment_variables import ( _OTEL_PYTHON_EXPORTER_OTLP_GRPC_TRACES_CREDENTIAL_PROVIDER, OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE, OTEL_EXPORTER_OTLP_TRACES_CLIENT_CERTIFICATE, OTEL_EXPORTER_OTLP_TRACES_CLIENT_KEY, OTEL_EXPORTER_OTLP_TRACES_COMPRESSION, OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, OTEL_EXPORTER_OTLP_TRACES_HEADERS, OTEL_EXPORTER_OTLP_TRACES_INSECURE, OTEL_EXPORTER_OTLP_TRACES_TIMEOUT, ) from opentelemetry.sdk.trace import ReadableSpan from opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult from opentelemetry.semconv._incubating.attributes.otel_attributes import ( OtelComponentTypeValues, ) logger = logging.getLogger(__name__) # pylint: disable=no-member class OTLPSpanExporter( SpanExporter, OTLPExporterMixin[ Sequence[ReadableSpan], ExportTraceServiceRequest, SpanExportResult, TraceServiceStub, ], ): # pylint: disable=unsubscriptable-object """OTLP span exporter Args: endpoint: OpenTelemetry Collector receiver endpoint insecure: Connection type credentials: Credentials object for server authentication headers: Headers to send when exporting timeout: Backend request timeout in seconds compression: gRPC compression method to use """ def __init__( self, endpoint: str | None = None, insecure: bool | None = None, credentials: ChannelCredentials | None = None, headers: TypingSequence[tuple[str, str]] | dict[str, str] | str | None = None, timeout: float | None = None, compression: Compression | None = None, channel_options: tuple[tuple[str, str]] | None = None, retryable_error_codes: Iterable[StatusCode] | None = None, *, meter_provider: MeterProvider | None = None, ): insecure_spans = environ.get(OTEL_EXPORTER_OTLP_TRACES_INSECURE) if insecure is None and insecure_spans is not None: insecure = insecure_spans.lower() == "true" if not insecure and environ.get(OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE) is not None: credentials = _get_credentials( credentials, _OTEL_PYTHON_EXPORTER_OTLP_GRPC_TRACES_CREDENTIAL_PROVIDER, OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE, OTEL_EXPORTER_OTLP_TRACES_CLIENT_KEY, OTEL_EXPORTER_OTLP_TRACES_CLIENT_CERTIFICATE, ) environ_timeout = environ.get(OTEL_EXPORTER_OTLP_TRACES_TIMEOUT) environ_timeout = float(environ_timeout) if environ_timeout is not None else None compression = ( environ_to_compression(OTEL_EXPORTER_OTLP_TRACES_COMPRESSION) if compression is None else compression ) OTLPExporterMixin.__init__( self, stub=TraceServiceStub, result=SpanExportResult, endpoint=endpoint or environ.get(OTEL_EXPORTER_OTLP_TRACES_ENDPOINT), insecure=insecure, credentials=credentials, headers=headers or environ.get(OTEL_EXPORTER_OTLP_TRACES_HEADERS), timeout=timeout or environ_timeout, compression=compression, channel_options=channel_options, retryable_error_codes=retryable_error_codes, component_type=OtelComponentTypeValues.OTLP_GRPC_SPAN_EXPORTER, signal="traces", meter_provider=meter_provider, ) def _translate_data(self, data: Sequence[ReadableSpan]) -> ExportTraceServiceRequest: return encode_spans(data) def _count_data(self, data: Sequence[ReadableSpan]): return len(data) def export(self, spans: Sequence[ReadableSpan]) -> SpanExportResult: return self._export(spans) def shutdown(self, timeout_millis: float = 30_000, **kwargs) -> None: OTLPExporterMixin.shutdown(self, timeout_millis=timeout_millis) def force_flush(self, timeout_millis: int = 30000) -> bool: """Nothing is buffered in this exporter, so this method does nothing.""" return True @property def _exporting(self): return "traces" version/000077500000000000000000000000001527070070700415205ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc__init__.py000066400000000000000000000001441527070070700436300ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 __version__ = "1.45.2" python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-proto-grpc/test-requirements.in000066400000000000000000000005761527070070700335430ustar00rootroot00000000000000colorama>=0.4.6 iniconfig>=2.0.0 packaging>=24.0 pluggy>=1.6.0 protobuf>=5.29.5 pytest>=9.0.3 -e opentelemetry-api -e tests/opentelemetry-test-utils -e exporter/opentelemetry-exporter-otlp-proto-common -e exporter/opentelemetry-exporter-otlp-common -e opentelemetry-proto -e opentelemetry-sdk -e opentelemetry-semantic-conventions -e exporter/opentelemetry-exporter-otlp-proto-grpc test-requirements.latest.txt000066400000000000000000000065731527070070700351730ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-proto-grpc# This file was autogenerated by uv via the following command: # uv pip compile --python-version=3.10 --universal --constraints=dev-requirements.txt exporter/opentelemetry-exporter-otlp-proto-grpc/test-requirements.in --output-file=exporter/opentelemetry-exporter-otlp-proto-grpc/test-requirements.latest.txt -e exporter/opentelemetry-exporter-otlp-common # via # -r exporter/opentelemetry-exporter-otlp-proto-grpc/test-requirements.in # opentelemetry-exporter-otlp-proto-grpc -e exporter/opentelemetry-exporter-otlp-proto-common # via # -r exporter/opentelemetry-exporter-otlp-proto-grpc/test-requirements.in # opentelemetry-exporter-otlp-proto-grpc -e exporter/opentelemetry-exporter-otlp-proto-grpc # via -r exporter/opentelemetry-exporter-otlp-proto-grpc/test-requirements.in -e opentelemetry-api # via # -r exporter/opentelemetry-exporter-otlp-proto-grpc/test-requirements.in # opentelemetry-exporter-otlp-proto-grpc # opentelemetry-sdk # opentelemetry-semantic-conventions # opentelemetry-test-utils -e opentelemetry-proto # via # -r exporter/opentelemetry-exporter-otlp-proto-grpc/test-requirements.in # opentelemetry-exporter-otlp-proto-common # opentelemetry-exporter-otlp-proto-grpc -e opentelemetry-sdk # via # -r exporter/opentelemetry-exporter-otlp-proto-grpc/test-requirements.in # opentelemetry-exporter-otlp-common # opentelemetry-exporter-otlp-proto-grpc # opentelemetry-test-utils -e opentelemetry-semantic-conventions # via # -r exporter/opentelemetry-exporter-otlp-proto-grpc/test-requirements.in # opentelemetry-sdk -e tests/opentelemetry-test-utils # via -r exporter/opentelemetry-exporter-otlp-proto-grpc/test-requirements.in asgiref==3.7.2 # via # -c dev-requirements.txt # opentelemetry-test-utils certifi==2026.5.20 # via requests charset-normalizer==3.4.7 # via requests colorama==0.4.6 # via # -r exporter/opentelemetry-exporter-otlp-proto-grpc/test-requirements.in # pytest exceptiongroup==1.3.1 ; python_full_version < '3.11' # via pytest googleapis-common-protos==1.75.0 # via opentelemetry-exporter-otlp-proto-grpc grpcio==1.80.0 # via opentelemetry-exporter-otlp-proto-grpc idna==3.16 # via requests iniconfig==2.3.0 # via # -r exporter/opentelemetry-exporter-otlp-proto-grpc/test-requirements.in # pytest packaging==26.2 # via # -r exporter/opentelemetry-exporter-otlp-proto-grpc/test-requirements.in # pytest pluggy==1.6.0 # via # -r exporter/opentelemetry-exporter-otlp-proto-grpc/test-requirements.in # pytest protobuf==7.35.0 # via # -r exporter/opentelemetry-exporter-otlp-proto-grpc/test-requirements.in # googleapis-common-protos # opentelemetry-proto pygments==2.20.0 # via pytest pytest==9.0.3 # via # -c dev-requirements.txt # -r exporter/opentelemetry-exporter-otlp-proto-grpc/test-requirements.in requests==2.33.0 # via # -c dev-requirements.txt # opentelemetry-test-utils tomli==2.4.1 ; python_full_version < '3.11' # via pytest typing-extensions==4.15.0 # via # asgiref # exceptiongroup # grpcio # opentelemetry-api # opentelemetry-exporter-otlp-proto-grpc # opentelemetry-sdk # opentelemetry-semantic-conventions urllib3==2.7.0 # via requests test-requirements.oldest.txt000066400000000000000000000075071527070070700351670ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-proto-grpc# This file was autogenerated by uv via the following command: # uv pip compile --python-version=3.10 --universal --resolution=lowest --constraints=dev-requirements.txt exporter/opentelemetry-exporter-otlp-proto-grpc/test-requirements.in --output-file=exporter/opentelemetry-exporter-otlp-proto-grpc/test-requirements.oldest.txt -e exporter/opentelemetry-exporter-otlp-common # via # -r exporter/opentelemetry-exporter-otlp-proto-grpc/test-requirements.in # opentelemetry-exporter-otlp-proto-grpc -e exporter/opentelemetry-exporter-otlp-proto-common # via # -r exporter/opentelemetry-exporter-otlp-proto-grpc/test-requirements.in # opentelemetry-exporter-otlp-proto-grpc -e exporter/opentelemetry-exporter-otlp-proto-grpc # via -r exporter/opentelemetry-exporter-otlp-proto-grpc/test-requirements.in -e opentelemetry-api # via # -r exporter/opentelemetry-exporter-otlp-proto-grpc/test-requirements.in # opentelemetry-exporter-otlp-proto-grpc # opentelemetry-sdk # opentelemetry-semantic-conventions # opentelemetry-test-utils -e opentelemetry-proto # via # -r exporter/opentelemetry-exporter-otlp-proto-grpc/test-requirements.in # opentelemetry-exporter-otlp-proto-common # opentelemetry-exporter-otlp-proto-grpc -e opentelemetry-sdk # via # -r exporter/opentelemetry-exporter-otlp-proto-grpc/test-requirements.in # opentelemetry-exporter-otlp-common # opentelemetry-exporter-otlp-proto-grpc # opentelemetry-test-utils -e opentelemetry-semantic-conventions # via # -r exporter/opentelemetry-exporter-otlp-proto-grpc/test-requirements.in # opentelemetry-sdk -e tests/opentelemetry-test-utils # via -r exporter/opentelemetry-exporter-otlp-proto-grpc/test-requirements.in asgiref==3.7.2 # via # -c dev-requirements.txt # opentelemetry-test-utils certifi==2023.5.7 # via requests charset-normalizer==2.0.0 # via requests colorama==0.4.6 # via # -r exporter/opentelemetry-exporter-otlp-proto-grpc/test-requirements.in # pytest exceptiongroup==1.0.0 ; python_full_version < '3.11' # via pytest googleapis-common-protos==1.63.1 # via opentelemetry-exporter-otlp-proto-grpc grpcio==1.63.2 ; python_full_version < '3.13' # via opentelemetry-exporter-otlp-proto-grpc grpcio==1.66.2 ; python_full_version == '3.13.*' # via opentelemetry-exporter-otlp-proto-grpc grpcio==1.75.1 ; python_full_version >= '3.14' # via opentelemetry-exporter-otlp-proto-grpc idna==2.5 # via requests iniconfig==2.0.0 # via # -r exporter/opentelemetry-exporter-otlp-proto-grpc/test-requirements.in # pytest packaging==24.0 # via # -r exporter/opentelemetry-exporter-otlp-proto-grpc/test-requirements.in # pytest pluggy==1.6.0 # via # -r exporter/opentelemetry-exporter-otlp-proto-grpc/test-requirements.in # pytest protobuf==5.29.5 # via # -r exporter/opentelemetry-exporter-otlp-proto-grpc/test-requirements.in # googleapis-common-protos # opentelemetry-proto pygments==2.7.2 # via pytest pytest==9.0.3 # via # -c dev-requirements.txt # -r exporter/opentelemetry-exporter-otlp-proto-grpc/test-requirements.in requests==2.33.0 # via # -c dev-requirements.txt # opentelemetry-test-utils tomli==1.0.0 ; python_full_version < '3.11' # via pytest typing-extensions==4.6.0 ; python_full_version < '3.14' # via # asgiref # opentelemetry-api # opentelemetry-exporter-otlp-proto-grpc # opentelemetry-sdk # opentelemetry-semantic-conventions typing-extensions==4.12.0 ; python_full_version >= '3.14' # via # grpcio # opentelemetry-api # opentelemetry-exporter-otlp-proto-grpc # opentelemetry-sdk # opentelemetry-semantic-conventions urllib3==1.26.0 # via requests python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/000077500000000000000000000000001527070070700306455ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/__init__.py000066400000000000000000000000001527070070700327440ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/fixtures/000077500000000000000000000000001527070070700325165ustar00rootroot00000000000000test-client-cert.pem000066400000000000000000000000001527070070700363160ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/fixturestest-client-key.pem000066400000000000000000000000001527070070700361510ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/fixturespython-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/fixtures/test.cert000066400000000000000000000000001527070070700343420ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/logs/000077500000000000000000000000001527070070700316115ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/logs/__init__.py000066400000000000000000000000001527070070700337100ustar00rootroot00000000000000test_otlp_logs_exporter.py000066400000000000000000000502721527070070700371030ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/logs# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # pylint: disable=too-many-lines import time from os.path import dirname from unittest import TestCase from unittest.mock import Mock, patch from google.protobuf.json_format import MessageToDict from grpc import ChannelCredentials, Compression from opentelemetry._logs import LogRecord, SeverityNumber from opentelemetry.exporter.otlp.proto.common._internal import _encode_value from opentelemetry.exporter.otlp.proto.grpc._log_exporter import ( OTLPLogExporter, ) from opentelemetry.proto.collector.logs.v1.logs_service_pb2 import ( ExportLogsServiceRequest, ) from opentelemetry.proto.common.v1.common_pb2 import AnyValue, KeyValue from opentelemetry.proto.common.v1.common_pb2 import ( InstrumentationScope as PB2InstrumentationScope, ) from opentelemetry.proto.logs.v1.logs_pb2 import LogRecord as PB2LogRecord from opentelemetry.proto.logs.v1.logs_pb2 import ResourceLogs, ScopeLogs from opentelemetry.proto.resource.v1.resource_pb2 import ( Resource as OTLPResource, ) from opentelemetry.sdk._logs import ReadWriteLogRecord from opentelemetry.sdk.environment_variables import ( OTEL_EXPORTER_OTLP_LOGS_CERTIFICATE, OTEL_EXPORTER_OTLP_LOGS_CLIENT_CERTIFICATE, OTEL_EXPORTER_OTLP_LOGS_CLIENT_KEY, OTEL_EXPORTER_OTLP_LOGS_COMPRESSION, OTEL_EXPORTER_OTLP_LOGS_ENDPOINT, OTEL_EXPORTER_OTLP_LOGS_HEADERS, OTEL_EXPORTER_OTLP_LOGS_TIMEOUT, ) from opentelemetry.sdk.resources import Resource as SDKResource from opentelemetry.sdk.util.instrumentation import InstrumentationScope from opentelemetry.trace import ( NonRecordingSpan, SpanContext, TraceFlags, set_span_in_context, ) THIS_DIR = dirname(__file__) class TestOTLPLogExporter(TestCase): def setUp(self): self.exporter = OTLPLogExporter() ctx_log_data_1 = set_span_in_context( NonRecordingSpan( SpanContext( 2604504634922341076776623263868986797, 5213367945872657620, False, TraceFlags(0x01), ) ) ) self.log_data_1 = ReadWriteLogRecord( LogRecord( timestamp=int(time.time() * 1e9), context=ctx_log_data_1, severity_text="WARNING", severity_number=SeverityNumber.WARN, body="Zhengzhou, We have a heaviest rains in 1000 years", attributes={"a": 1, "b": "c"}, ), resource=SDKResource({"key": "value"}), instrumentation_scope=InstrumentationScope("first_name", "first_version"), ) ctx_log_data_2 = set_span_in_context( NonRecordingSpan( SpanContext( 2604504634922341076776623263868986799, 5213367945872657623, False, TraceFlags(0x01), ) ) ) self.log_data_2 = ReadWriteLogRecord( LogRecord( timestamp=int(time.time() * 1e9), context=ctx_log_data_2, severity_text="INFO", severity_number=SeverityNumber.INFO2, body="Sydney, Opera House is closed", attributes={"custom_attr": [1, 2, 3]}, ), resource=SDKResource({"key": "value"}), instrumentation_scope=InstrumentationScope("second_name", "second_version"), ) ctx_log_data_3 = set_span_in_context( NonRecordingSpan( SpanContext( 2604504634922341076776623263868986800, 5213367945872657628, False, TraceFlags(0x01), ) ) ) self.log_data_3 = ReadWriteLogRecord( LogRecord( timestamp=int(time.time() * 1e9), context=ctx_log_data_3, severity_text="ERROR", severity_number=SeverityNumber.WARN, body="Mumbai, Boil water before drinking", ), resource=SDKResource({"service": "myapp"}), instrumentation_scope=InstrumentationScope("third_name", "third_version"), ) ctx_log_data_4 = set_span_in_context( NonRecordingSpan(SpanContext(0, 5213367945872657629, False, TraceFlags(0x01))) ) self.log_data_4 = ReadWriteLogRecord( LogRecord( timestamp=int(time.time() * 1e9), context=ctx_log_data_4, severity_text="ERROR", severity_number=SeverityNumber.WARN, body="Invalid trace id check", ), resource=SDKResource({"service": "myapp"}), instrumentation_scope=InstrumentationScope("fourth_name", "fourth_version"), ) ctx_log_data_5 = set_span_in_context( NonRecordingSpan( SpanContext( 2604504634922341076776623263868986801, 0, False, TraceFlags(0x01), ) ) ) self.log_data_5 = ReadWriteLogRecord( LogRecord( timestamp=int(time.time() * 1e9), context=ctx_log_data_5, severity_text="ERROR", severity_number=SeverityNumber.WARN, body="Invalid span id check", ), resource=SDKResource({"service": "myapp"}), instrumentation_scope=InstrumentationScope("fifth_name", "fifth_version"), ) def test_exporting(self): # pylint: disable=protected-access self.assertEqual(self.exporter._exporting, "logs") @patch.dict( "os.environ", { OTEL_EXPORTER_OTLP_LOGS_ENDPOINT: "logs:4317", OTEL_EXPORTER_OTLP_LOGS_HEADERS: " key1=value1,KEY2 = VALUE=2", OTEL_EXPORTER_OTLP_LOGS_TIMEOUT: "10", OTEL_EXPORTER_OTLP_LOGS_COMPRESSION: "gzip", }, ) @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.OTLPExporterMixin.__init__") def test_env_variables(self, mock_exporter_mixin): OTLPLogExporter() self.assertTrue(len(mock_exporter_mixin.call_args_list) == 1) _, kwargs = mock_exporter_mixin.call_args_list[0] self.assertEqual(kwargs["endpoint"], "logs:4317") self.assertEqual(kwargs["headers"], " key1=value1,KEY2 = VALUE=2") self.assertEqual(kwargs["timeout"], 10) self.assertEqual(kwargs["compression"], Compression.Gzip) self.assertIsNone(kwargs["credentials"]) # Create a new test method specifically for client certificates @patch.dict( "os.environ", { OTEL_EXPORTER_OTLP_LOGS_ENDPOINT: "logs:4317", OTEL_EXPORTER_OTLP_LOGS_CERTIFICATE: THIS_DIR + "/../fixtures/test.cert", OTEL_EXPORTER_OTLP_LOGS_CLIENT_CERTIFICATE: THIS_DIR + "/../fixtures/test-client-cert.pem", OTEL_EXPORTER_OTLP_LOGS_CLIENT_KEY: THIS_DIR + "/../fixtures/test-client-key.pem", OTEL_EXPORTER_OTLP_LOGS_HEADERS: " key1=value1,KEY2 = VALUE=2", OTEL_EXPORTER_OTLP_LOGS_TIMEOUT: "10", OTEL_EXPORTER_OTLP_LOGS_COMPRESSION: "gzip", }, ) @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.OTLPExporterMixin.__init__") def test_env_variables_with_client_certificates(self, mock_exporter_mixin): OTLPLogExporter() self.assertTrue(len(mock_exporter_mixin.call_args_list) == 1) _, kwargs = mock_exporter_mixin.call_args_list[0] self.assertEqual(kwargs["endpoint"], "logs:4317") self.assertEqual(kwargs["headers"], " key1=value1,KEY2 = VALUE=2") self.assertEqual(kwargs["timeout"], 10) self.assertEqual(kwargs["compression"], Compression.Gzip) self.assertIsNotNone(kwargs["credentials"]) self.assertIsInstance(kwargs["credentials"], ChannelCredentials) @patch.dict( "os.environ", { OTEL_EXPORTER_OTLP_LOGS_ENDPOINT: "logs:4317", OTEL_EXPORTER_OTLP_LOGS_CERTIFICATE: THIS_DIR + "/../fixtures/test.cert", OTEL_EXPORTER_OTLP_LOGS_HEADERS: " key1=value1,KEY2 = VALUE=2", OTEL_EXPORTER_OTLP_LOGS_TIMEOUT: "10", OTEL_EXPORTER_OTLP_LOGS_COMPRESSION: "gzip", }, ) @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.OTLPExporterMixin.__init__") @patch("logging.Logger.error") def test_env_variables_with_only_certificate(self, mock_logger_error, mock_exporter_mixin): OTLPLogExporter() self.assertTrue(len(mock_exporter_mixin.call_args_list) == 1) _, kwargs = mock_exporter_mixin.call_args_list[0] self.assertEqual(kwargs["endpoint"], "logs:4317") self.assertEqual(kwargs["headers"], " key1=value1,KEY2 = VALUE=2") self.assertEqual(kwargs["timeout"], 10) self.assertEqual(kwargs["compression"], Compression.Gzip) self.assertIsNotNone(kwargs["credentials"]) self.assertIsInstance(kwargs["credentials"], ChannelCredentials) mock_logger_error.assert_not_called() @patch.dict( "os.environ", { OTEL_EXPORTER_OTLP_LOGS_ENDPOINT: "logs:4317", OTEL_EXPORTER_OTLP_LOGS_CERTIFICATE: THIS_DIR + "/../fixtures/test.cert", OTEL_EXPORTER_OTLP_LOGS_HEADERS: " key1=value1,KEY2 = VALUE=2", OTEL_EXPORTER_OTLP_LOGS_TIMEOUT: "10", OTEL_EXPORTER_OTLP_LOGS_COMPRESSION: "gzip", }, ) @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.OTLPExporterMixin.__init__") @patch("logging.Logger.error") def test_kwargs_have_precedence_over_env_variables(self, mock_logger_error, mock_exporter_mixin): credentials_mock = Mock() OTLPLogExporter( endpoint="logs:4318", headers=(("an", "header"),), timeout=20, credentials=credentials_mock, compression=Compression.NoCompression, channel_options=(("some", "options"),), ) self.assertTrue(len(mock_exporter_mixin.call_args_list) == 1) _, kwargs = mock_exporter_mixin.call_args_list[0] self.assertEqual(kwargs["endpoint"], "logs:4318") self.assertEqual(kwargs["headers"], (("an", "header"),)) self.assertEqual(kwargs["timeout"], 20) self.assertEqual(kwargs["compression"], Compression.NoCompression) self.assertEqual(kwargs["credentials"], credentials_mock) self.assertEqual(kwargs["channel_options"], (("some", "options"),)) mock_logger_error.assert_not_called() def export_log_and_deserialize(self, log_data): # pylint: disable=protected-access translated_data = self.exporter._translate_data([log_data]) request_dict = MessageToDict(translated_data) log_records = request_dict.get("resourceLogs")[0].get("scopeLogs")[0].get("logRecords") return log_records def test_exported_log_without_trace_id(self): log_records = self.export_log_and_deserialize(self.log_data_4) if log_records: log_record = log_records[0] self.assertIn("spanId", log_record) self.assertNotIn( "traceId", log_record, "traceId should not be present in the log record", ) else: self.fail("No log records found") def test_exported_log_without_span_id(self): log_records = self.export_log_and_deserialize(self.log_data_5) if log_records: log_record = log_records[0] self.assertIn("traceId", log_record) self.assertNotIn( "spanId", log_record, "spanId should not be present in the log record", ) else: self.fail("No log records found") def test_translate_log_data(self): expected = ExportLogsServiceRequest( resource_logs=[ ResourceLogs( resource=OTLPResource( attributes=[ KeyValue(key="key", value=AnyValue(string_value="value")), ] ), scope_logs=[ ScopeLogs( scope=PB2InstrumentationScope(name="first_name", version="first_version"), log_records=[ PB2LogRecord( # pylint: disable=no-member time_unix_nano=self.log_data_1.log_record.timestamp, observed_time_unix_nano=self.log_data_1.log_record.observed_timestamp, severity_number=self.log_data_1.log_record.severity_number.value, severity_text="WARNING", span_id=int.to_bytes(5213367945872657620, 8, "big"), trace_id=int.to_bytes( 2604504634922341076776623263868986797, 16, "big", ), body=_encode_value("Zhengzhou, We have a heaviest rains in 1000 years"), attributes=[ KeyValue( key="a", value=AnyValue(int_value=1), ), KeyValue( key="b", value=AnyValue(string_value="c"), ), ], flags=int(self.log_data_1.log_record.trace_flags), ) ], ) ], ), ] ) # pylint: disable=protected-access self.assertEqual(expected, self.exporter._translate_data([self.log_data_1])) def test_count_log_data(self): # pylint: disable=protected-access self.assertEqual(1, self.exporter._count_data([self.log_data_1])) def test_translate_multiple_logs(self): expected = ExportLogsServiceRequest( resource_logs=[ ResourceLogs( resource=OTLPResource( attributes=[ KeyValue(key="key", value=AnyValue(string_value="value")), ] ), scope_logs=[ ScopeLogs( scope=PB2InstrumentationScope(name="first_name", version="first_version"), log_records=[ PB2LogRecord( # pylint: disable=no-member time_unix_nano=self.log_data_1.log_record.timestamp, observed_time_unix_nano=self.log_data_1.log_record.observed_timestamp, severity_number=self.log_data_1.log_record.severity_number.value, severity_text="WARNING", span_id=int.to_bytes(5213367945872657620, 8, "big"), trace_id=int.to_bytes( 2604504634922341076776623263868986797, 16, "big", ), body=_encode_value("Zhengzhou, We have a heaviest rains in 1000 years"), attributes=[ KeyValue( key="a", value=AnyValue(int_value=1), ), KeyValue( key="b", value=AnyValue(string_value="c"), ), ], flags=int(self.log_data_1.log_record.trace_flags), ) ], ), ScopeLogs( scope=PB2InstrumentationScope(name="second_name", version="second_version"), log_records=[ PB2LogRecord( # pylint: disable=no-member time_unix_nano=self.log_data_2.log_record.timestamp, observed_time_unix_nano=self.log_data_2.log_record.observed_timestamp, severity_number=self.log_data_2.log_record.severity_number.value, severity_text="INFO", span_id=int.to_bytes(5213367945872657623, 8, "big"), trace_id=int.to_bytes( 2604504634922341076776623263868986799, 16, "big", ), body=_encode_value("Sydney, Opera House is closed"), attributes=[ KeyValue( key="custom_attr", value=_encode_value([1, 2, 3]), ), ], flags=int(self.log_data_2.log_record.trace_flags), ) ], ), ], ), ResourceLogs( resource=OTLPResource( attributes=[ KeyValue( key="service", value=AnyValue(string_value="myapp"), ), ] ), scope_logs=[ ScopeLogs( scope=PB2InstrumentationScope(name="third_name", version="third_version"), log_records=[ PB2LogRecord( # pylint: disable=no-member time_unix_nano=self.log_data_3.log_record.timestamp, observed_time_unix_nano=self.log_data_3.log_record.observed_timestamp, severity_number=self.log_data_3.log_record.severity_number.value, severity_text="ERROR", span_id=int.to_bytes(5213367945872657628, 8, "big"), trace_id=int.to_bytes( 2604504634922341076776623263868986800, 16, "big", ), body=_encode_value("Mumbai, Boil water before drinking"), attributes=[], flags=int(self.log_data_3.log_record.trace_flags), ) ], ) ], ), ] ) # pylint: disable=protected-access self.assertEqual( expected, self.exporter._translate_data([self.log_data_1, self.log_data_2, self.log_data_3]), ) def test_count_multiple_logs(self): self.assertEqual( 3, # pylint: disable=protected-access self.exporter._count_data([self.log_data_1, self.log_data_2, self.log_data_3]), ) test_otlp_exporter_mixin.py000066400000000000000000000660061527070070700363210ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-proto-grpc/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import threading import time import unittest from collections.abc import Sequence from concurrent.futures import ( # pylint: disable=no-name-in-module ThreadPoolExecutor, ) from logging import WARNING, getLogger from platform import system from typing import Any from unittest import TestCase from unittest.mock import Mock, patch import grpc from google.protobuf.duration_pb2 import ( # pylint: disable=no-name-in-module Duration, ) from google.rpc.error_details_pb2 import ( # pylint: disable=no-name-in-module RetryInfo, ) from grpc import ChannelCredentials, Compression, StatusCode, server from opentelemetry.exporter.otlp.proto.common.trace_encoder import ( encode_spans, ) from opentelemetry.exporter.otlp.proto.grpc.exporter import ( _RETRYABLE_ERROR_CODES, InvalidCompressionValueException, OTLPExporterMixin, environ_to_compression, ) from opentelemetry.exporter.otlp.proto.grpc.version import __version__ from opentelemetry.proto.collector.trace.v1.trace_service_pb2 import ( ExportTraceServiceRequest, ExportTraceServiceResponse, ) from opentelemetry.proto.collector.trace.v1.trace_service_pb2_grpc import ( TraceServiceServicer, TraceServiceStub, add_TraceServiceServicer_to_server, ) from opentelemetry.sdk.environment_variables import ( _OTEL_PYTHON_EXPORTER_OTLP_GRPC_CREDENTIAL_PROVIDER, OTEL_EXPORTER_OTLP_COMPRESSION, OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED, ) from opentelemetry.sdk.metrics import MeterProvider from opentelemetry.sdk.metrics.export import InMemoryMetricReader from opentelemetry.sdk.trace import ReadableSpan, _Span from opentelemetry.sdk.trace.export import ( SpanExporter, SpanExportResult, ) from opentelemetry.semconv._incubating.attributes.otel_attributes import ( OtelComponentTypeValues, ) from opentelemetry.test.mock_test_classes import IterEntryPoint logger = getLogger(__name__) # The below tests use this test SpanExporter and Spans, but are testing the # underlying behavior in the mixin. A MetricExporter or LogRecordExporter could # just as easily be used. class OTLPSpanExporterForTesting( SpanExporter, OTLPExporterMixin[ ReadableSpan, ExportTraceServiceRequest, SpanExportResult, TraceServiceStub, ], ): def __init__(self, **kwargs): super().__init__( TraceServiceStub, SpanExportResult, component_type=OtelComponentTypeValues.OTLP_GRPC_SPAN_EXPORTER, signal="traces", meter_provider=kwargs.pop("meter_provider", None), **kwargs, ) def _translate_data(self, data: Sequence[ReadableSpan]) -> ExportTraceServiceRequest: return encode_spans(data) def _count_data(self, data: Sequence[ReadableSpan]) -> int: return len(data) def export(self, spans: Sequence[ReadableSpan]) -> SpanExportResult: return self._export(spans) @property def _exporting(self): return "traces" def shutdown(self, timeout_millis: float = 30_000, **kwargs): return OTLPExporterMixin.shutdown(self, timeout_millis, **kwargs) class TraceServiceServicerWithExportParams(TraceServiceServicer): def __init__( self, export_result: StatusCode, optional_retry_nanos: int | None = None, optional_export_sleep: float | None = None, optional_error_details: str | None = None, ): self.export_result = export_result self.optional_export_sleep = optional_export_sleep self.optional_retry_nanos = optional_retry_nanos self.num_requests = 0 self.optional_error_details = optional_error_details # pylint: disable=invalid-name,unused-argument def Export(self, request, context): self.num_requests += 1 if self.optional_export_sleep: time.sleep(self.optional_export_sleep) if self.export_result != StatusCode.OK and self.optional_retry_nanos: context.set_trailing_metadata( ( ( "google.rpc.retryinfo-bin", RetryInfo(retry_delay=Duration(nanos=self.optional_retry_nanos)).SerializeToString(), ), ) ) context.set_code(self.export_result) if self.optional_error_details: context.set_details(self.optional_error_details) return ExportTraceServiceResponse() class ThreadWithReturnValue(threading.Thread): def __init__( self, target=None, args=(), ): super().__init__(target=target, args=args) self._return = None def run(self): try: if self._target is not None: # type: ignore self._return = self._target(*self._args, **self._kwargs) # type: ignore finally: # Avoid a refcycle if the thread is running a function with # an argument that has a member that points to the thread. del self._target, self._args, self._kwargs # type: ignore def join(self, timeout: float | None = None) -> Any: super().join(timeout=timeout) return self._return # pylint: disable-next=too-many-public-methods class TestOTLPExporterMixin(TestCase): def setUp(self): self.server = server(ThreadPoolExecutor(max_workers=10)) self.server.add_insecure_port("127.0.0.1:4317") self.server.start() self.metric_reader = InMemoryMetricReader() self.meter_provider = MeterProvider(metric_readers=[self.metric_reader]) self.exporter = OTLPSpanExporterForTesting(insecure=True, meter_provider=self.meter_provider) self.span = _Span( "a", context=Mock( trace_state={"a": "b", "c": "d"}, span_id=10217189687419569865, trace_id=67545097771067222548457157018666467027, ), ) def tearDown(self): self.server.stop(None) @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.insecure_channel") @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.secure_channel") def test_otlp_exporter_endpoint(self, mock_secure, mock_insecure): expected_endpoint = "localhost:4317" endpoints = [ ( "http://localhost:4317", None, mock_insecure, ), ( "localhost:4317", None, mock_secure, ), ( "http://localhost:4317", True, mock_insecure, ), ( "localhost:4317", True, mock_insecure, ), ( "http://localhost:4317", False, mock_secure, ), ( "localhost:4317", False, mock_secure, ), ( "https://localhost:4317", False, mock_secure, ), ( "https://localhost:4317", None, mock_secure, ), ( "https://localhost:4317", True, mock_secure, ), ] for endpoint, insecure, mock_method in endpoints: OTLPSpanExporterForTesting(endpoint=endpoint, insecure=insecure) self.assertEqual( 1, mock_method.call_count, f"expected {mock_method} to be called for {endpoint} {insecure}", ) self.assertEqual( expected_endpoint, mock_method.call_args[0][0], f"expected {expected_endpoint} got {mock_method.call_args[0][0]} {endpoint}", ) mock_method.reset_mock() def test_environ_to_compression(self): with patch.dict( "os.environ", { "test_gzip": "gzip", "test_gzip_caseinsensitive_with_whitespace": " GzIp ", "test_invalid": "some invalid compression", }, ): self.assertEqual(environ_to_compression("test_gzip"), Compression.Gzip) self.assertEqual( environ_to_compression("test_gzip_caseinsensitive_with_whitespace"), Compression.Gzip, ) self.assertIsNone( environ_to_compression("missing_key"), ) with self.assertRaises(InvalidCompressionValueException): environ_to_compression("test_invalid") # pylint: disable=no-self-use @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.insecure_channel") @patch.dict("os.environ", {}) def test_otlp_exporter_otlp_compression_unspecified(self, mock_insecure_channel): """No env or kwarg should be NoCompression""" OTLPSpanExporterForTesting(insecure=True) mock_insecure_channel.assert_called_once_with( "localhost:4317", compression=Compression.NoCompression, options=( ( "grpc.primary_user_agent", "OTel-OTLP-Exporter-Python/" + __version__, ), ), ) @patch.dict( "os.environ", {_OTEL_PYTHON_EXPORTER_OTLP_GRPC_CREDENTIAL_PROVIDER: "credential_provider"}, ) @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.entry_points") def test_that_credential_gets_passed_to_exporter(self, mock_entry_points): credential = ChannelCredentials(None) def f(): return credential mock_entry_points.configure_mock(return_value=[IterEntryPoint("custom_credential", f)]) exporter = OTLPSpanExporterForTesting(insecure=False) # pylint: disable=protected-access assert exporter._credentials is credential @patch.dict( "os.environ", {_OTEL_PYTHON_EXPORTER_OTLP_GRPC_CREDENTIAL_PROVIDER: "credential_provider"}, ) def test_that_missing_entry_point_raises_exception(self): with self.assertRaises(RuntimeError): OTLPSpanExporterForTesting(insecure=False) @patch.dict( "os.environ", {_OTEL_PYTHON_EXPORTER_OTLP_GRPC_CREDENTIAL_PROVIDER: "credential_provider"}, ) @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.entry_points") def test_that_entry_point_returning_bad_type_raises_exception(self, mock_entry_points): def f(): return 1 mock_entry_points.configure_mock(return_value=[IterEntryPoint("custom_credential", f)]) with self.assertRaises(RuntimeError): OTLPSpanExporterForTesting(insecure=False) # pylint: disable=no-self-use, disable=unused-argument @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.ssl_channel_credentials") @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.secure_channel") @patch.dict("os.environ", {}) def test_no_credentials_ssl_channel_called(self, secure_channel, mock_ssl_channel): OTLPSpanExporterForTesting(insecure=False) self.assertTrue(mock_ssl_channel.called) # pylint: disable=no-self-use @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.insecure_channel") @patch.dict("os.environ", {OTEL_EXPORTER_OTLP_COMPRESSION: "gzip"}) def test_otlp_exporter_otlp_compression_envvar(self, mock_insecure_channel): """Just OTEL_EXPORTER_OTLP_COMPRESSION should work""" OTLPSpanExporterForTesting(insecure=True) mock_insecure_channel.assert_called_once_with( "localhost:4317", compression=Compression.Gzip, options=( ( "grpc.primary_user_agent", "OTel-OTLP-Exporter-Python/" + __version__, ), ), ) @patch.dict("os.environ", {OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED: " true "}) def test_shutdown(self): add_TraceServiceServicer_to_server( TraceServiceServicerWithExportParams(StatusCode.OK), self.server, ) exporter = OTLPSpanExporterForTesting(insecure=True, meter_provider=self.meter_provider) self.assertEqual(exporter.export([self.span]), SpanExportResult.SUCCESS) metrics_data = self.metric_reader.get_metrics_data() scope_metrics = metrics_data.resource_metrics[0].scope_metrics[0] self.assertEqual(scope_metrics.scope.name, "opentelemetry-sdk") metrics = sorted(scope_metrics.metrics, key=lambda m: m.name) self.assertEqual(len(metrics), 3) self.assertEqual(metrics[0].name, "otel.sdk.exporter.operation.duration") self.assert_standard_metric_attrs(metrics[0].data.data_points[0].attributes) self.assertEqual(metrics[1].name, "otel.sdk.exporter.span.exported") self.assert_standard_metric_attrs(metrics[1].data.data_points[0].attributes) self.assertEqual(metrics[2].name, "otel.sdk.exporter.span.inflight") self.assert_standard_metric_attrs(metrics[2].data.data_points[0].attributes) exporter.shutdown() with self.assertLogs(level=WARNING) as warning: self.assertEqual(exporter.export([self.span]), SpanExportResult.FAILURE) self.assertEqual( warning.records[0].message, "Exporter already shutdown, ignoring batch", ) @unittest.skipIf( system() == "Windows", "For gRPC + windows there's some added delay in the RPCs which breaks the assertion over amount of time passed.", ) def test_shutdown_interrupts_export_retry_backoff(self): add_TraceServiceServicer_to_server( TraceServiceServicerWithExportParams( StatusCode.UNAVAILABLE, ), self.server, ) export_thread = ThreadWithReturnValue(target=self.exporter.export, args=([self.span],)) with self.assertLogs(level=WARNING) as warning: begin_wait = time.time() export_thread.start() # Wait a bit for export to fail and the backoff sleep to start time.sleep(0.05) # The code should now be in a 1 second backoff. # pylint: disable=protected-access self.assertFalse(self.exporter._shutdown_in_progress.is_set()) self.exporter.shutdown() self.assertTrue(self.exporter._shutdown_in_progress.is_set()) export_result = export_thread.join() end_wait = time.time() self.assertEqual(export_result, SpanExportResult.FAILURE) # Shutdown should have interrupted the sleep. self.assertTrue(end_wait - begin_wait < 0.2) self.assertEqual( warning.records[1].message, "Shutdown in progress, aborting retry.", ) def test_export_over_closed_grpc_channel(self): # pylint: disable=protected-access add_TraceServiceServicer_to_server( TraceServiceServicerWithExportParams(StatusCode.OK), self.server, ) self.exporter.export([self.span]) self.exporter.shutdown() data = self.exporter._translate_data([self.span]) with self.assertRaises(ValueError) as err: self.exporter._client.Export(request=data) self.assertEqual(str(err.exception), "Cannot invoke RPC on closed channel!") @unittest.skipIf( system() == "Windows", "For gRPC + windows there's some added delay in the RPCs which breaks the assertion over amount of time passed.", ) @patch.dict("os.environ", {OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED: "true"}) def test_retry_info_is_respected(self): mock_trace_service = TraceServiceServicerWithExportParams( StatusCode.UNAVAILABLE, optional_retry_nanos=200000000, # .2 seconds ) add_TraceServiceServicer_to_server( mock_trace_service, self.server, ) exporter = OTLPSpanExporterForTesting(insecure=True, timeout=10, meter_provider=self.meter_provider) before = time.time() self.assertEqual( exporter.export([self.span]), SpanExportResult.FAILURE, ) after = time.time() self.assertEqual(mock_trace_service.num_requests, 6) # 1 second plus wiggle room so the test passes consistently. self.assertAlmostEqual(after - before, 1, delta=0.5) metrics_data = self.metric_reader.get_metrics_data() scope_metrics = metrics_data.resource_metrics[0].scope_metrics[0] self.assertEqual(scope_metrics.scope.name, "opentelemetry-sdk") metrics = sorted(scope_metrics.metrics, key=lambda m: m.name) self.assertEqual(len(metrics), 3) self.assertEqual(metrics[0].name, "otel.sdk.exporter.operation.duration") self.assert_standard_metric_attrs(metrics[0].data.data_points[0].attributes) self.assertEqual( metrics[0].data.data_points[0].attributes["error.type"], "_InactiveRpcError", ) self.assertEqual( metrics[0].data.data_points[0].attributes["rpc.response.status_code"], "UNAVAILABLE", ) self.assertEqual(metrics[1].name, "otel.sdk.exporter.span.exported") self.assert_standard_metric_attrs(metrics[1].data.data_points[0].attributes) self.assertEqual( metrics[1].data.data_points[0].attributes["error.type"], "_InactiveRpcError", ) self.assertNotIn( "rpc.response.status_code", metrics[1].data.data_points[0].attributes, ) self.assertEqual(metrics[2].name, "otel.sdk.exporter.span.inflight") self.assert_standard_metric_attrs(metrics[2].data.data_points[0].attributes) self.assertNotIn("error.type", metrics[2].data.data_points[0].attributes) self.assertNotIn( "rpc.response.status_code", metrics[2].data.data_points[0].attributes, ) @unittest.skipIf( system() == "Windows", "For gRPC + windows there's some added delay in the RPCs which breaks the assertion over amount of time passed.", ) def test_retry_not_made_if_would_exceed_timeout(self): mock_trace_service = TraceServiceServicerWithExportParams(StatusCode.UNAVAILABLE) add_TraceServiceServicer_to_server( mock_trace_service, self.server, ) exporter = OTLPSpanExporterForTesting(insecure=True, timeout=4) before = time.time() self.assertEqual( exporter.export([self.span]), SpanExportResult.FAILURE, ) after = time.time() # Our retry starts with a 1 second backoff then doubles. # First call at time 0, second at time 1, third at time 3, fourth would exceed timeout. self.assertEqual(mock_trace_service.num_requests, 3) # There's a +/-20% jitter on each backoff. self.assertTrue(2.35 < after - before < 3.65) @unittest.skipIf( system() == "Windows", "For gRPC + windows there's some added delay in the RPCs which breaks the assertion over amount of time passed.", ) def test_timeout_set_correctly(self): mock_trace_service = TraceServiceServicerWithExportParams(StatusCode.UNAVAILABLE, optional_export_sleep=0.25) add_TraceServiceServicer_to_server( mock_trace_service, self.server, ) exporter = OTLPSpanExporterForTesting(insecure=True, timeout=1.4) # Should timeout after 1.4 seconds. First attempt takes .25 seconds # Then a 1 second sleep, then deadline exceeded after .15 seconds, # mid way through second call. with self.assertLogs(level=WARNING) as warning: before = time.time() # Eliminate the jitter. with patch("random.uniform", return_value=1): self.assertEqual( exporter.export([self.span]), SpanExportResult.FAILURE, ) after = time.time() self.assertIn( "error code: StatusCode.DEADLINE_EXCEEDED", warning.records[-1].message, ) self.assertEqual(mock_trace_service.num_requests, 2) self.assertAlmostEqual(after - before, 1.4, delta=0.5) def test_channel_options_set_correctly(self): """Test that gRPC channel options are set correctly for keepalive and reconnection""" # This test verifies that the channel is created with the right options # We patch grpc.insecure_channel to ensure it is called without errors with patch("opentelemetry.exporter.otlp.proto.grpc.exporter.insecure_channel") as mock_channel: OTLPSpanExporterForTesting(insecure=True) self.assertTrue(mock_channel.called) def test_otlp_headers_from_env(self): # pylint: disable=protected-access # This ensures that there is no other header than standard user-agent. self.assertEqual( self.exporter._headers, (), ) @patch.dict("os.environ", {OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED: "true"}) def test_permanent_failure(self): exporter = OTLPSpanExporterForTesting(insecure=True, meter_provider=self.meter_provider) with self.assertLogs(level=WARNING) as warning: add_TraceServiceServicer_to_server( TraceServiceServicerWithExportParams( StatusCode.ALREADY_EXISTS, optional_error_details="This already exists.", ), self.server, ) self.assertEqual(exporter.export([self.span]), SpanExportResult.FAILURE) self.assertEqual( warning.records[-1].message, "Failed to export traces to localhost:4317, error code: StatusCode.ALREADY_EXISTS, error details: This already exists.", ) metrics_data = self.metric_reader.get_metrics_data() scope_metrics = metrics_data.resource_metrics[0].scope_metrics[0] self.assertEqual(scope_metrics.scope.name, "opentelemetry-sdk") metrics = sorted(scope_metrics.metrics, key=lambda m: m.name) self.assertEqual(len(metrics), 3) self.assertEqual(metrics[0].name, "otel.sdk.exporter.operation.duration") self.assert_standard_metric_attrs(metrics[0].data.data_points[0].attributes) self.assertEqual( metrics[0].data.data_points[0].attributes["error.type"], "_InactiveRpcError", ) self.assertEqual( metrics[0].data.data_points[0].attributes["rpc.response.status_code"], "ALREADY_EXISTS", ) self.assertEqual(metrics[1].name, "otel.sdk.exporter.span.exported") self.assert_standard_metric_attrs(metrics[1].data.data_points[0].attributes) self.assertEqual( metrics[1].data.data_points[0].attributes["error.type"], "_InactiveRpcError", ) self.assertNotIn( "rpc.response.status_code", metrics[1].data.data_points[0].attributes, ) self.assertEqual(metrics[2].name, "otel.sdk.exporter.span.inflight") self.assert_standard_metric_attrs(metrics[2].data.data_points[0].attributes) self.assertNotIn("error.type", metrics[2].data.data_points[0].attributes) self.assertNotIn( "rpc.response.status_code", metrics[2].data.data_points[0].attributes, ) def test_unavailable_reconnects(self): """Test that the exporter reconnects on UNAVAILABLE error""" add_TraceServiceServicer_to_server( TraceServiceServicerWithExportParams(StatusCode.UNAVAILABLE), self.server, ) # Spy on grpc.insecure_channel to verify it's called for reconnection with patch( "opentelemetry.exporter.otlp.proto.grpc.exporter.insecure_channel", side_effect=grpc.insecure_channel, ) as mock_insecure_channel: # Mock sleep to avoid waiting with patch("time.sleep"): # We expect FAILURE because the server keeps returning UNAVAILABLE # but we want to verify reconnection attempts happened self.exporter.export([self.span]) # Verify that we attempted to reinitialize the channel (called insecure_channel) # Since the initial channel was created in setUp (unpatched), this call # must be from the reconnection logic. self.assertTrue(mock_insecure_channel.called) def test_retryable_error_codes_initialization(self): # pylint: disable=protected-access self.assertEqual(self.exporter._retryable_error_codes, _RETRYABLE_ERROR_CODES) custom_codes = [StatusCode.INTERNAL, StatusCode.UNKNOWN] exporter = OTLPSpanExporterForTesting(insecure=True, retryable_error_codes=custom_codes) self.assertEqual(exporter._retryable_error_codes, frozenset(custom_codes)) @patch.dict( "os.environ", {"OTEL_PYTHON_EXPORTER_OTLP_GRPC_RETRYABLE_ERROR_CODES": ",INTERNAL, unknown,,,dEAdline_Exceeded "}, ) def test_retryable_error_codes_initialization_from_env(self): expected_codes = frozenset( { StatusCode.INTERNAL, StatusCode.UNKNOWN, StatusCode.DEADLINE_EXCEEDED, } ) exporter = OTLPSpanExporterForTesting() # pylint: disable=protected-access self.assertEqual(exporter._retryable_error_codes, expected_codes) @unittest.skipIf( system() == "Windows", "For gRPC + windows there's some added delay in the RPCs which breaks the assertion over amount of time passed.", ) def test_retryable_error_codes_custom(self): # Test that a custom error code is retried if specified custom_codes = [StatusCode.INTERNAL] mock_trace_service = TraceServiceServicerWithExportParams( StatusCode.INTERNAL, optional_retry_nanos=200000000, # .2 seconds ) add_TraceServiceServicer_to_server( mock_trace_service, self.server, ) exporter = OTLPSpanExporterForTesting(insecure=True, retryable_error_codes=custom_codes, timeout=10) self.assertEqual( exporter.export([self.span]), SpanExportResult.FAILURE, ) self.assertEqual(mock_trace_service.num_requests, 6) # Test that a default retryable code is NOT retried if not in custom_codes mock_trace_service.num_requests = 0 mock_trace_service.export_result = StatusCode.UNAVAILABLE self.assertEqual( exporter.export([self.span]), SpanExportResult.FAILURE, ) self.assertEqual(mock_trace_service.num_requests, 1) def assert_standard_metric_attrs(self, attributes): self.assertEqual(attributes["otel.component.type"], "otlp_grpc_span_exporter") self.assertTrue(attributes["otel.component.name"].startswith("otlp_grpc_span_exporter/")) self.assertEqual(attributes["server.address"], "localhost") self.assertEqual(attributes["server.port"], 4317) test_otlp_metrics_exporter.py000066400000000000000000000736501527070070700366460ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-proto-grpc/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # pylint: disable=too-many-lines from logging import WARNING from os import environ from os.path import dirname from unittest import TestCase from unittest.mock import patch from grpc import ChannelCredentials, Compression from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import ( OTLPMetricExporter, ) from opentelemetry.exporter.otlp.proto.grpc.version import __version__ from opentelemetry.proto.common.v1.common_pb2 import InstrumentationScope from opentelemetry.sdk.environment_variables import ( OTEL_EXPORTER_OTLP_COMPRESSION, OTEL_EXPORTER_OTLP_METRICS_CERTIFICATE, OTEL_EXPORTER_OTLP_METRICS_CLIENT_CERTIFICATE, OTEL_EXPORTER_OTLP_METRICS_CLIENT_KEY, OTEL_EXPORTER_OTLP_METRICS_COMPRESSION, OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION, OTEL_EXPORTER_OTLP_METRICS_ENDPOINT, OTEL_EXPORTER_OTLP_METRICS_HEADERS, OTEL_EXPORTER_OTLP_METRICS_INSECURE, OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE, OTEL_EXPORTER_OTLP_METRICS_TIMEOUT, ) from opentelemetry.sdk.metrics import ( Counter, Histogram, ObservableCounter, ObservableGauge, ObservableUpDownCounter, UpDownCounter, ) from opentelemetry.sdk.metrics.export import ( AggregationTemporality, Gauge, Metric, MetricsData, NumberDataPoint, ResourceMetrics, ScopeMetrics, ) from opentelemetry.sdk.metrics.view import ( ExplicitBucketHistogramAggregation, ExponentialBucketHistogramAggregation, ) from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.util.instrumentation import ( InstrumentationScope as SDKInstrumentationScope, ) from opentelemetry.test.metrictestutil import _generate_sum THIS_DIR = dirname(__file__) class TestOTLPMetricExporter(TestCase): # pylint: disable=too-many-public-methods def setUp(self): self.exporter = OTLPMetricExporter() self.metrics = { "sum_int": MetricsData( resource_metrics=[ ResourceMetrics( resource=Resource( attributes={"a": 1, "b": False}, schema_url="resource_schema_url", ), scope_metrics=[ ScopeMetrics( scope=SDKInstrumentationScope( name="first_name", version="first_version", schema_url="insrumentation_scope_schema_url", ), metrics=[_generate_sum("sum_int", 33)], schema_url="instrumentation_scope_schema_url", ) ], schema_url="resource_schema_url", ) ] ) } def test_exporting(self): # pylint: disable=protected-access self.assertEqual(self.exporter._exporting, "metrics") @patch.dict( "os.environ", {OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE: "DELTA"}, ) def test_preferred_temporality(self): # pylint: disable=protected-access exporter = OTLPMetricExporter(preferred_temporality={Counter: AggregationTemporality.CUMULATIVE}) self.assertEqual( exporter._preferred_temporality[Counter], AggregationTemporality.CUMULATIVE, ) self.assertEqual( exporter._preferred_temporality[UpDownCounter], AggregationTemporality.CUMULATIVE, ) self.assertEqual( exporter._preferred_temporality[Histogram], AggregationTemporality.DELTA, ) self.assertEqual( exporter._preferred_temporality[ObservableCounter], AggregationTemporality.DELTA, ) self.assertEqual( exporter._preferred_temporality[ObservableUpDownCounter], AggregationTemporality.CUMULATIVE, ) self.assertEqual( exporter._preferred_temporality[ObservableGauge], AggregationTemporality.CUMULATIVE, ) @patch.dict( "os.environ", { OTEL_EXPORTER_OTLP_METRICS_ENDPOINT: "collector:4317", OTEL_EXPORTER_OTLP_METRICS_HEADERS: " key1=value1,KEY2 = value=2", OTEL_EXPORTER_OTLP_METRICS_TIMEOUT: "10", OTEL_EXPORTER_OTLP_METRICS_COMPRESSION: "gzip", }, ) @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.OTLPExporterMixin.__init__") def test_env_variables(self, mock_exporter_mixin): OTLPMetricExporter() self.assertTrue(len(mock_exporter_mixin.call_args_list) == 1) _, kwargs = mock_exporter_mixin.call_args_list[0] self.assertEqual(kwargs["endpoint"], "collector:4317") self.assertEqual(kwargs["headers"], " key1=value1,KEY2 = value=2") self.assertEqual(kwargs["timeout"], 10) self.assertEqual(kwargs["compression"], Compression.Gzip) self.assertIsNone(kwargs["credentials"]) @patch.dict( "os.environ", { OTEL_EXPORTER_OTLP_METRICS_ENDPOINT: "collector:4317", OTEL_EXPORTER_OTLP_METRICS_CERTIFICATE: THIS_DIR + "/fixtures/test.cert", OTEL_EXPORTER_OTLP_METRICS_CLIENT_CERTIFICATE: THIS_DIR + "/fixtures/test-client-cert.pem", OTEL_EXPORTER_OTLP_METRICS_CLIENT_KEY: THIS_DIR + "/fixtures/test-client-key.pem", OTEL_EXPORTER_OTLP_METRICS_HEADERS: " key1=value1,KEY2 = value=2", OTEL_EXPORTER_OTLP_METRICS_TIMEOUT: "10", OTEL_EXPORTER_OTLP_METRICS_COMPRESSION: "gzip", }, ) @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.OTLPExporterMixin.__init__") def test_env_variables_with_client_certificates(self, mock_exporter_mixin): OTLPMetricExporter() self.assertTrue(len(mock_exporter_mixin.call_args_list) == 1) _, kwargs = mock_exporter_mixin.call_args_list[0] self.assertEqual(kwargs["endpoint"], "collector:4317") self.assertEqual(kwargs["headers"], " key1=value1,KEY2 = value=2") self.assertEqual(kwargs["timeout"], 10) self.assertEqual(kwargs["compression"], Compression.Gzip) self.assertIsNotNone(kwargs["credentials"]) self.assertIsInstance(kwargs["credentials"], ChannelCredentials) @patch.dict( "os.environ", { OTEL_EXPORTER_OTLP_METRICS_ENDPOINT: "collector:4317", OTEL_EXPORTER_OTLP_METRICS_CERTIFICATE: THIS_DIR + "/fixtures/test.cert", OTEL_EXPORTER_OTLP_METRICS_HEADERS: " key1=value1,KEY2 = value=2", OTEL_EXPORTER_OTLP_METRICS_TIMEOUT: "10", OTEL_EXPORTER_OTLP_METRICS_COMPRESSION: "gzip", }, ) @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.OTLPExporterMixin.__init__") @patch("logging.Logger.error") def test_env_variables_with_only_certificate(self, mock_logger_error, mock_exporter_mixin): OTLPMetricExporter() self.assertTrue(len(mock_exporter_mixin.call_args_list) == 1) _, kwargs = mock_exporter_mixin.call_args_list[0] self.assertEqual(kwargs["endpoint"], "collector:4317") self.assertEqual(kwargs["headers"], " key1=value1,KEY2 = value=2") self.assertEqual(kwargs["timeout"], 10) self.assertEqual(kwargs["compression"], Compression.Gzip) self.assertIsNotNone(kwargs["credentials"]) self.assertIsInstance(kwargs["credentials"], ChannelCredentials) mock_logger_error.assert_not_called() @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.ssl_channel_credentials") @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.secure_channel") # pylint: disable=unused-argument def test_no_credentials_error(self, mock_ssl_channel, mock_secure): OTLPMetricExporter(insecure=False) self.assertTrue(mock_ssl_channel.called) @patch.dict( "os.environ", {OTEL_EXPORTER_OTLP_METRICS_HEADERS: " key1=value1,KEY2 = VALUE=2 "}, ) @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.ssl_channel_credentials") @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.secure_channel") # pylint: disable=unused-argument def test_otlp_headers_from_env(self, mock_ssl_channel, mock_secure): exporter = OTLPMetricExporter() # pylint: disable=protected-access self.assertEqual( exporter._headers, ( ("key1", "value1"), ("key2", "VALUE=2"), ), ) exporter = OTLPMetricExporter(headers=(("key3", "value3"), ("key4", "value4"))) # pylint: disable=protected-access self.assertEqual( exporter._headers, ( ("key3", "value3"), ("key4", "value4"), ), ) @patch.dict( "os.environ", {OTEL_EXPORTER_OTLP_METRICS_INSECURE: "True"}, ) @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.insecure_channel") # pylint: disable=unused-argument def test_otlp_insecure_from_env(self, mock_insecure): OTLPMetricExporter() # pylint: disable=protected-access self.assertTrue(mock_insecure.called) self.assertEqual( 1, mock_insecure.call_count, f"expected {mock_insecure} to be called", ) # pylint: disable=no-self-use @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.insecure_channel") @patch.dict("os.environ", {OTEL_EXPORTER_OTLP_COMPRESSION: "gzip"}) def test_otlp_exporter_otlp_compression_kwarg(self, mock_insecure_channel): """Specifying kwarg should take precedence over env""" OTLPMetricExporter(insecure=True, compression=Compression.NoCompression) mock_insecure_channel.assert_called_once_with( "localhost:4317", compression=Compression.NoCompression, options=( ( "grpc.primary_user_agent", "OTel-OTLP-Exporter-Python/" + __version__, ), ), ) # pylint: disable=no-self-use @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.insecure_channel") def test_otlp_exporter_otlp_channel_options_kwarg(self, mock_insecure_channel): OTLPMetricExporter(insecure=True, channel_options=(("some", "options"),)) mock_insecure_channel.assert_called_once_with( "localhost:4317", compression=Compression.NoCompression, options=( ( "grpc.primary_user_agent", "OTel-OTLP-Exporter-Python/" + __version__, ), ("some", "options"), ), ) def test_split_metrics_data_many_data_points(self): # GIVEN metrics_data = MetricsData( resource_metrics=[ _resource_metrics( index=1, scope_metrics=[ _scope_metrics( index=1, metrics=[ _gauge( index=1, data_points=[ _number_data_point(11), _number_data_point(12), _number_data_point(13), ], ), ], ), ], ), ] ) # WHEN split_metrics_data: list[MetricsData] = list( # pylint: disable=protected-access OTLPMetricExporter(max_export_batch_size=2)._split_metrics_data( metrics_data=metrics_data, ) ) # THEN self.assertEqual( [ MetricsData( resource_metrics=[ _resource_metrics( index=1, scope_metrics=[ _scope_metrics( index=1, metrics=[ _gauge( index=1, data_points=[ _number_data_point(11), _number_data_point(12), ], ), ], ), ], ), ] ), MetricsData( resource_metrics=[ _resource_metrics( index=1, scope_metrics=[ _scope_metrics( index=1, metrics=[ _gauge( index=1, data_points=[ _number_data_point(13), ], ), ], ), ], ), ] ), ], split_metrics_data, ) def test_split_metrics_data_nb_data_points_equal_batch_size(self): # GIVEN metrics_data = MetricsData( resource_metrics=[ _resource_metrics( index=1, scope_metrics=[ _scope_metrics( index=1, metrics=[ _gauge( index=1, data_points=[ _number_data_point(11), _number_data_point(12), _number_data_point(13), ], ), ], ), ], ), ] ) # WHEN split_metrics_data: list[MetricsData] = list( # pylint: disable=protected-access OTLPMetricExporter(max_export_batch_size=3)._split_metrics_data( metrics_data=metrics_data, ) ) # THEN self.assertEqual( [ MetricsData( resource_metrics=[ _resource_metrics( index=1, scope_metrics=[ _scope_metrics( index=1, metrics=[ _gauge( index=1, data_points=[ _number_data_point(11), _number_data_point(12), _number_data_point(13), ], ), ], ), ], ), ] ), ], split_metrics_data, ) def test_split_metrics_data_many_resources_scopes_metrics(self): # GIVEN metrics_data = MetricsData( resource_metrics=[ _resource_metrics( index=1, scope_metrics=[ _scope_metrics( index=1, metrics=[ _gauge( index=1, data_points=[ _number_data_point(11), ], ), _gauge( index=2, data_points=[ _number_data_point(12), ], ), ], ), _scope_metrics( index=2, metrics=[ _gauge( index=3, data_points=[ _number_data_point(13), ], ), ], ), ], ), _resource_metrics( index=2, scope_metrics=[ _scope_metrics( index=3, metrics=[ _gauge( index=4, data_points=[ _number_data_point(14), ], ), ], ), ], ), ] ) # WHEN split_metrics_data: list[MetricsData] = list( # pylint: disable=protected-access OTLPMetricExporter(max_export_batch_size=2)._split_metrics_data( metrics_data=metrics_data, ) ) # THEN self.assertEqual( [ MetricsData( resource_metrics=[ _resource_metrics( index=1, scope_metrics=[ _scope_metrics( index=1, metrics=[ _gauge( index=1, data_points=[ _number_data_point(11), ], ), _gauge( index=2, data_points=[ _number_data_point(12), ], ), ], ), ], ), ] ), MetricsData( resource_metrics=[ _resource_metrics( index=1, scope_metrics=[ _scope_metrics( index=2, metrics=[ _gauge( index=3, data_points=[ _number_data_point(13), ], ), ], ), ], ), _resource_metrics( index=2, scope_metrics=[ _scope_metrics( index=3, metrics=[ _gauge( index=4, data_points=[ _number_data_point(14), ], ), ], ), ], ), ] ), ], split_metrics_data, ) def test_count_metrics_data(self): # GIVEN metrics_data = MetricsData( resource_metrics=[ _resource_metrics( index=1, scope_metrics=[ _scope_metrics( index=1, metrics=[ _gauge( index=1, data_points=[ _number_data_point(11), ], ), _gauge( index=2, data_points=[ _number_data_point(12), ], ), ], ), _scope_metrics( index=2, metrics=[ _gauge( index=3, data_points=[ _number_data_point(13), ], ), ], ), ], ), _resource_metrics( index=2, scope_metrics=[ _scope_metrics( index=3, metrics=[ _gauge( index=4, data_points=[ _number_data_point(14), ], ), ], ), ], ), ] ) # WHEN # pylint: disable=protected-access count = OTLPMetricExporter(max_export_batch_size=2)._count_data( metrics_data, ) # THEN self.assertEqual(count, 4) @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.secure_channel") def test_insecure_https_endpoint(self, mock_secure_channel): OTLPMetricExporter(endpoint="https://ab.c:123", insecure=True) mock_secure_channel.assert_called() def test_aggregation_temporality(self): # pylint: disable=protected-access otlp_metric_exporter = OTLPMetricExporter() for temporality in otlp_metric_exporter._preferred_temporality.values(): self.assertEqual(temporality, AggregationTemporality.CUMULATIVE) with patch.dict( environ, {OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE: "CUMULATIVE"}, ): otlp_metric_exporter = OTLPMetricExporter() for temporality in otlp_metric_exporter._preferred_temporality.values(): self.assertEqual(temporality, AggregationTemporality.CUMULATIVE) with patch.dict(environ, {OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE: "ABC"}): with self.assertLogs(level=WARNING): otlp_metric_exporter = OTLPMetricExporter() for temporality in otlp_metric_exporter._preferred_temporality.values(): self.assertEqual(temporality, AggregationTemporality.CUMULATIVE) with patch.dict( environ, {OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE: "DELTA"}, ): otlp_metric_exporter = OTLPMetricExporter() self.assertEqual( otlp_metric_exporter._preferred_temporality[Counter], AggregationTemporality.DELTA, ) self.assertEqual( otlp_metric_exporter._preferred_temporality[UpDownCounter], AggregationTemporality.CUMULATIVE, ) self.assertEqual( otlp_metric_exporter._preferred_temporality[Histogram], AggregationTemporality.DELTA, ) self.assertEqual( otlp_metric_exporter._preferred_temporality[ObservableCounter], AggregationTemporality.DELTA, ) self.assertEqual( otlp_metric_exporter._preferred_temporality[ObservableUpDownCounter], AggregationTemporality.CUMULATIVE, ) self.assertEqual( otlp_metric_exporter._preferred_temporality[ObservableGauge], AggregationTemporality.CUMULATIVE, ) with patch.dict( environ, {OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE: "LOWMEMORY"}, ): otlp_metric_exporter = OTLPMetricExporter() self.assertEqual( otlp_metric_exporter._preferred_temporality[Counter], AggregationTemporality.DELTA, ) self.assertEqual( otlp_metric_exporter._preferred_temporality[UpDownCounter], AggregationTemporality.CUMULATIVE, ) self.assertEqual( otlp_metric_exporter._preferred_temporality[Histogram], AggregationTemporality.DELTA, ) self.assertEqual( otlp_metric_exporter._preferred_temporality[ObservableCounter], AggregationTemporality.CUMULATIVE, ) self.assertEqual( otlp_metric_exporter._preferred_temporality[ObservableUpDownCounter], AggregationTemporality.CUMULATIVE, ) self.assertEqual( otlp_metric_exporter._preferred_temporality[ObservableGauge], AggregationTemporality.CUMULATIVE, ) def test_exponential_explicit_bucket_histogram(self): self.assertIsInstance( # pylint: disable=protected-access OTLPMetricExporter()._preferred_aggregation[Histogram], ExplicitBucketHistogramAggregation, ) with patch.dict( environ, {OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION: "base2_exponential_bucket_histogram"}, ): self.assertIsInstance( # pylint: disable=protected-access OTLPMetricExporter()._preferred_aggregation[Histogram], ExponentialBucketHistogramAggregation, ) with patch.dict( environ, {OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION: "abc"}, ): with self.assertLogs(level=WARNING) as log: self.assertIsInstance( # pylint: disable=protected-access OTLPMetricExporter()._preferred_aggregation[Histogram], ExplicitBucketHistogramAggregation, ) self.assertIn( ( "Invalid value for OTEL_EXPORTER_OTLP_METRICS_DEFAULT_" "HISTOGRAM_AGGREGATION: abc, using explicit bucket " "histogram aggregation" ), log.output[0], ) with patch.dict( environ, {OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION: "explicit_bucket_histogram"}, ): self.assertIsInstance( # pylint: disable=protected-access OTLPMetricExporter()._preferred_aggregation[Histogram], ExplicitBucketHistogramAggregation, ) def test_preferred_aggregation_override(self): histogram_aggregation = ExplicitBucketHistogramAggregation( boundaries=[0.05, 0.1, 0.5, 1, 5, 10], ) exporter = OTLPMetricExporter( preferred_aggregation={ Histogram: histogram_aggregation, }, ) self.assertEqual( # pylint: disable=protected-access exporter._preferred_aggregation[Histogram], histogram_aggregation, ) def _resource_metrics(index: int, scope_metrics: list[ScopeMetrics]) -> ResourceMetrics: return ResourceMetrics( resource=Resource( attributes={"a": index}, schema_url=f"resource_url_{index}", ), schema_url=f"resource_url_{index}", scope_metrics=scope_metrics, ) def _scope_metrics(index: int, metrics: list[Metric]) -> ScopeMetrics: return ScopeMetrics( scope=InstrumentationScope(name=f"scope_{index}"), schema_url=f"scope_url_{index}", metrics=metrics, ) def _gauge(index: int, data_points: list[NumberDataPoint]) -> Metric: return Metric( name=f"gauge_{index}", description="description", unit="unit", data=Gauge(data_points=data_points), ) def _number_data_point(value: int) -> NumberDataPoint: return NumberDataPoint( attributes={"a": 1, "b": True}, start_time_unix_nano=1641946015139533244, time_unix_nano=1641946016139533244, value=value, ) test_otlp_trace_exporter.py000066400000000000000000000707621527070070700362770ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-proto-grpc/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # pylint: disable=too-many-lines import os from unittest import TestCase from unittest.mock import Mock, PropertyMock, patch from grpc import ChannelCredentials, Compression from opentelemetry.attributes import BoundedAttributes from opentelemetry.exporter.otlp.proto.common._internal import ( _encode_key_value, ) from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import ( OTLPSpanExporter, ) from opentelemetry.exporter.otlp.proto.grpc.version import __version__ from opentelemetry.proto.collector.trace.v1.trace_service_pb2 import ( ExportTraceServiceRequest, ) from opentelemetry.proto.common.v1.common_pb2 import ( AnyValue, ArrayValue, KeyValue, ) from opentelemetry.proto.common.v1.common_pb2 import ( InstrumentationScope as PB2InstrumentationScope, ) from opentelemetry.proto.resource.v1.resource_pb2 import ( Resource as OTLPResource, ) from opentelemetry.proto.trace.v1.trace_pb2 import ( ResourceSpans, ScopeSpans, Status, ) from opentelemetry.proto.trace.v1.trace_pb2 import Span as OTLPSpan from opentelemetry.sdk.environment_variables import ( OTEL_EXPORTER_OTLP_COMPRESSION, OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE, OTEL_EXPORTER_OTLP_TRACES_CLIENT_CERTIFICATE, OTEL_EXPORTER_OTLP_TRACES_CLIENT_KEY, OTEL_EXPORTER_OTLP_TRACES_COMPRESSION, OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, OTEL_EXPORTER_OTLP_TRACES_HEADERS, OTEL_EXPORTER_OTLP_TRACES_INSECURE, OTEL_EXPORTER_OTLP_TRACES_TIMEOUT, ) from opentelemetry.sdk.resources import Resource as SDKResource from opentelemetry.sdk.trace import Status as SDKStatus from opentelemetry.sdk.trace import StatusCode as SDKStatusCode from opentelemetry.sdk.trace import TracerProvider, _Span from opentelemetry.sdk.trace.export import ( SimpleSpanProcessor, ) from opentelemetry.sdk.util.instrumentation import InstrumentationScope from opentelemetry.test.spantestutil import ( get_span_with_dropped_attributes_events_links, ) THIS_DIR = os.path.dirname(__file__) class TestOTLPSpanExporter(TestCase): # pylint: disable=too-many-public-methods def setUp(self): tracer_provider = TracerProvider() self.exporter = OTLPSpanExporter(insecure=True) tracer_provider.add_span_processor(SimpleSpanProcessor(self.exporter)) self.tracer = tracer_provider.get_tracer(__name__) event_mock = Mock( timestamp=1591240820506462784, attributes=BoundedAttributes(attributes={"a": 1, "b": False}), ) type(event_mock).name = PropertyMock(return_value="a") type(event_mock).dropped_attributes = PropertyMock(return_value=0) self.span = _Span( "a", context=Mock( trace_state={"a": "b", "c": "d"}, span_id=10217189687419569865, trace_id=67545097771067222548457157018666467027, ), resource=SDKResource({"a": 1, "b": False}), parent=Mock(span_id=12345), attributes=BoundedAttributes(attributes={"a": 1, "b": True}), events=[event_mock], links=[ Mock( **{ "context.trace_id": 1, "context.span_id": 2, "attributes": BoundedAttributes(attributes={"a": 1, "b": False}), "dropped_attributes": 0, "kind": OTLPSpan.SpanKind.SPAN_KIND_INTERNAL, # pylint: disable=no-member } ) ], instrumentation_scope=InstrumentationScope(name="name", version="version"), ) self.span2 = _Span( "b", context=Mock( trace_state={"a": "b", "c": "d"}, span_id=10217189687419569865, trace_id=67545097771067222548457157018666467027, ), resource=SDKResource({"a": 2, "b": False}), parent=Mock(span_id=12345), instrumentation_scope=InstrumentationScope(name="name", version="version"), ) self.span3 = _Span( "c", context=Mock( trace_state={"a": "b", "c": "d"}, span_id=10217189687419569865, trace_id=67545097771067222548457157018666467027, ), resource=SDKResource({"a": 1, "b": False}), parent=Mock(span_id=12345), instrumentation_scope=InstrumentationScope(name="name2", version="version2"), ) self.span.start() self.span.end() self.span2.start() self.span2.end() self.span3.start() self.span3.end() def test_exporting(self): # pylint: disable=protected-access self.assertEqual(self.exporter._exporting, "traces") @patch.dict( "os.environ", { OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: "collector:4317", OTEL_EXPORTER_OTLP_TRACES_HEADERS: " key1=value1,KEY2 = value=2", OTEL_EXPORTER_OTLP_TRACES_TIMEOUT: "10", OTEL_EXPORTER_OTLP_TRACES_COMPRESSION: "gzip", }, ) @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.OTLPExporterMixin.__init__") def test_env_variables(self, mock_exporter_mixin): OTLPSpanExporter() self.assertTrue(len(mock_exporter_mixin.call_args_list) == 1) _, kwargs = mock_exporter_mixin.call_args_list[0] self.assertEqual(kwargs["endpoint"], "collector:4317") self.assertEqual(kwargs["headers"], " key1=value1,KEY2 = value=2") self.assertEqual(kwargs["timeout"], 10) self.assertEqual(kwargs["compression"], Compression.Gzip) self.assertIsNone(kwargs["credentials"]) @patch.dict( "os.environ", { OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: "collector:4317", OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE: THIS_DIR + "/fixtures/test.cert", OTEL_EXPORTER_OTLP_TRACES_CLIENT_CERTIFICATE: THIS_DIR + "/fixtures/test-client-cert.pem", OTEL_EXPORTER_OTLP_TRACES_CLIENT_KEY: THIS_DIR + "/fixtures/test-client-key.pem", OTEL_EXPORTER_OTLP_TRACES_HEADERS: " key1=value1,KEY2 = value=2", OTEL_EXPORTER_OTLP_TRACES_TIMEOUT: "10", OTEL_EXPORTER_OTLP_TRACES_COMPRESSION: "gzip", }, ) @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.OTLPExporterMixin.__init__") def test_env_variables_with_client_certificates(self, mock_exporter_mixin): OTLPSpanExporter() self.assertTrue(len(mock_exporter_mixin.call_args_list) == 1) _, kwargs = mock_exporter_mixin.call_args_list[0] self.assertEqual(kwargs["endpoint"], "collector:4317") self.assertEqual(kwargs["headers"], " key1=value1,KEY2 = value=2") self.assertEqual(kwargs["timeout"], 10) self.assertEqual(kwargs["compression"], Compression.Gzip) self.assertIsNotNone(kwargs["credentials"]) self.assertIsInstance(kwargs["credentials"], ChannelCredentials) @patch.dict( "os.environ", { OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: "collector:4317", OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE: THIS_DIR + "/fixtures/test.cert", OTEL_EXPORTER_OTLP_TRACES_HEADERS: " key1=value1,KEY2 = value=2", OTEL_EXPORTER_OTLP_TRACES_TIMEOUT: "10", OTEL_EXPORTER_OTLP_TRACES_COMPRESSION: "gzip", }, ) @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.OTLPExporterMixin.__init__") @patch("logging.Logger.error") def test_env_variables_with_only_certificate(self, mock_logger_error, mock_exporter_mixin): OTLPSpanExporter() self.assertTrue(len(mock_exporter_mixin.call_args_list) == 1) _, kwargs = mock_exporter_mixin.call_args_list[0] self.assertEqual(kwargs["endpoint"], "collector:4317") self.assertEqual(kwargs["headers"], " key1=value1,KEY2 = value=2") self.assertEqual(kwargs["timeout"], 10) self.assertEqual(kwargs["compression"], Compression.Gzip) self.assertIsNotNone(kwargs["credentials"]) self.assertIsInstance(kwargs["credentials"], ChannelCredentials) mock_logger_error.assert_not_called() @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.ssl_channel_credentials") @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.secure_channel") # pylint: disable=unused-argument def test_no_credentials_error(self, mock_ssl_channel, mock_secure): OTLPSpanExporter(insecure=False) self.assertTrue(mock_ssl_channel.called) @patch.dict( "os.environ", {OTEL_EXPORTER_OTLP_TRACES_HEADERS: " key1=value1,KEY2 = VALUE=2 "}, ) @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.ssl_channel_credentials") @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.secure_channel") # pylint: disable=unused-argument def test_otlp_headers_from_env(self, mock_ssl_channel, mock_secure): exporter = OTLPSpanExporter() # pylint: disable=protected-access self.assertEqual( exporter._headers, ( ("key1", "value1"), ("key2", "VALUE=2"), ), ) exporter = OTLPSpanExporter(headers=(("key3", "value3"), ("key4", "value4"))) # pylint: disable=protected-access self.assertEqual( exporter._headers, ( ("key3", "value3"), ("key4", "value4"), ), ) exporter = OTLPSpanExporter(headers={"key5": "value5", "key6": "value6"}) # pylint: disable=protected-access self.assertEqual( exporter._headers, ( ("key5", "value5"), ("key6", "value6"), ), ) @patch.dict( "os.environ", {OTEL_EXPORTER_OTLP_TRACES_INSECURE: "True"}, ) @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.insecure_channel") # pylint: disable=unused-argument def test_otlp_insecure_from_env(self, mock_insecure): OTLPSpanExporter() # pylint: disable=protected-access self.assertTrue(mock_insecure.called) self.assertEqual( 1, mock_insecure.call_count, f"expected {mock_insecure} to be called", ) # pylint: disable=no-self-use @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.insecure_channel") @patch.dict("os.environ", {OTEL_EXPORTER_OTLP_COMPRESSION: "gzip"}) def test_otlp_exporter_otlp_compression_kwarg(self, mock_insecure_channel): """Specifying kwarg should take precedence over env""" OTLPSpanExporter(insecure=True, compression=Compression.NoCompression) mock_insecure_channel.assert_called_once_with( "localhost:4317", compression=Compression.NoCompression, options=( ( "grpc.primary_user_agent", "OTel-OTLP-Exporter-Python/" + __version__, ), ), ) # pylint: disable=no-self-use @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.insecure_channel") @patch.dict( "os.environ", {OTEL_EXPORTER_OTLP_TRACES_COMPRESSION: "gzip"}, ) def test_otlp_exporter_otlp_compression_precendence(self, mock_insecure_channel): """OTEL_EXPORTER_OTLP_TRACES_COMPRESSION as higher priority than OTEL_EXPORTER_OTLP_COMPRESSION """ OTLPSpanExporter(insecure=True) mock_insecure_channel.assert_called_once_with( "localhost:4317", compression=Compression.Gzip, options=( ( "grpc.primary_user_agent", "OTel-OTLP-Exporter-Python/" + __version__, ), ), ) # pylint: disable=no-self-use @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.insecure_channel") def test_otlp_exporter_otlp_channel_options_kwarg(self, mock_insecure_channel): OTLPSpanExporter(insecure=True, channel_options=(("some", "options"),)) mock_insecure_channel.assert_called_once_with( "localhost:4317", compression=Compression.NoCompression, options=( ( "grpc.primary_user_agent", "OTel-OTLP-Exporter-Python/" + __version__, ), ("some", "options"), ), ) def test_translate_spans(self): expected = ExportTraceServiceRequest( resource_spans=[ ResourceSpans( resource=OTLPResource( attributes=[ KeyValue(key="a", value=AnyValue(int_value=1)), KeyValue(key="b", value=AnyValue(bool_value=False)), ] ), scope_spans=[ ScopeSpans( scope=PB2InstrumentationScope(name="name", version="version"), spans=[ OTLPSpan( # pylint: disable=no-member name="a", start_time_unix_nano=self.span.start_time, end_time_unix_nano=self.span.end_time, trace_state="a=b,c=d", span_id=int.to_bytes(10217189687419569865, 8, "big"), trace_id=int.to_bytes( 67545097771067222548457157018666467027, 16, "big", ), parent_span_id=(b"\000\000\000\000\000\00009"), kind=(OTLPSpan.SpanKind.SPAN_KIND_INTERNAL), attributes=[ KeyValue( key="a", value=AnyValue(int_value=1), ), KeyValue( key="b", value=AnyValue(bool_value=True), ), ], events=[ OTLPSpan.Event( name="a", time_unix_nano=1591240820506462784, attributes=[ KeyValue( key="a", value=AnyValue(int_value=1), ), KeyValue( key="b", value=AnyValue(bool_value=False), ), ], ) ], status=Status(code=0, message=""), links=[ OTLPSpan.Link( trace_id=int.to_bytes(1, 16, "big"), span_id=int.to_bytes(2, 8, "big"), attributes=[ KeyValue( key="a", value=AnyValue(int_value=1), ), KeyValue( key="b", value=AnyValue(bool_value=False), ), ], flags=0x300, ) ], flags=0x300, ) ], ) ], ), ] ) # pylint: disable=protected-access self.assertEqual(expected, self.exporter._translate_data([self.span])) def test_count_spans(self): # pylint: disable=protected-access self.assertEqual(1, self.exporter._count_data([self.span])) def test_translate_spans_multi(self): expected = ExportTraceServiceRequest( resource_spans=[ ResourceSpans( resource=OTLPResource( attributes=[ KeyValue(key="a", value=AnyValue(int_value=1)), KeyValue(key="b", value=AnyValue(bool_value=False)), ] ), scope_spans=[ ScopeSpans( scope=PB2InstrumentationScope(name="name", version="version"), spans=[ OTLPSpan( # pylint: disable=no-member name="a", start_time_unix_nano=self.span.start_time, end_time_unix_nano=self.span.end_time, trace_state="a=b,c=d", span_id=int.to_bytes(10217189687419569865, 8, "big"), trace_id=int.to_bytes( 67545097771067222548457157018666467027, 16, "big", ), parent_span_id=(b"\000\000\000\000\000\00009"), kind=(OTLPSpan.SpanKind.SPAN_KIND_INTERNAL), attributes=[ KeyValue( key="a", value=AnyValue(int_value=1), ), KeyValue( key="b", value=AnyValue(bool_value=True), ), ], events=[ OTLPSpan.Event( name="a", time_unix_nano=1591240820506462784, attributes=[ KeyValue( key="a", value=AnyValue(int_value=1), ), KeyValue( key="b", value=AnyValue(bool_value=False), ), ], ) ], status=Status(code=0, message=""), links=[ OTLPSpan.Link( trace_id=int.to_bytes(1, 16, "big"), span_id=int.to_bytes(2, 8, "big"), attributes=[ KeyValue( key="a", value=AnyValue(int_value=1), ), KeyValue( key="b", value=AnyValue(bool_value=False), ), ], flags=0x300, ) ], flags=0x300, ) ], ), ScopeSpans( scope=PB2InstrumentationScope(name="name2", version="version2"), spans=[ OTLPSpan( # pylint: disable=no-member name="c", start_time_unix_nano=self.span3.start_time, end_time_unix_nano=self.span3.end_time, trace_state="a=b,c=d", span_id=int.to_bytes(10217189687419569865, 8, "big"), trace_id=int.to_bytes( 67545097771067222548457157018666467027, 16, "big", ), parent_span_id=(b"\000\000\000\000\000\00009"), kind=(OTLPSpan.SpanKind.SPAN_KIND_INTERNAL), status=Status(code=0, message=""), flags=0x300, ) ], ), ], ), ResourceSpans( resource=OTLPResource( attributes=[ KeyValue(key="a", value=AnyValue(int_value=2)), KeyValue(key="b", value=AnyValue(bool_value=False)), ] ), scope_spans=[ ScopeSpans( scope=PB2InstrumentationScope(name="name", version="version"), spans=[ OTLPSpan( # pylint: disable=no-member name="b", start_time_unix_nano=self.span2.start_time, end_time_unix_nano=self.span2.end_time, trace_state="a=b,c=d", span_id=int.to_bytes(10217189687419569865, 8, "big"), trace_id=int.to_bytes( 67545097771067222548457157018666467027, 16, "big", ), parent_span_id=(b"\000\000\000\000\000\00009"), kind=(OTLPSpan.SpanKind.SPAN_KIND_INTERNAL), status=Status(code=0, message=""), flags=0x300, ) ], ) ], ), ] ) # pylint: disable=protected-access self.assertEqual( expected, self.exporter._translate_data([self.span, self.span2, self.span3]), ) def test_count_spans_multi(self): self.assertEqual( # pylint: disable=protected-access 3, self.exporter._count_data([self.span, self.span2, self.span3]), ) def _check_translated_status( self, translated: ExportTraceServiceRequest, code_expected: Status, ): status = translated.resource_spans[0].scope_spans[0].spans[0].status self.assertEqual( status.code, code_expected, ) def test_span_status_translate(self): # pylint: disable=protected-access,no-member unset = SDKStatus(status_code=SDKStatusCode.UNSET) ok = SDKStatus(status_code=SDKStatusCode.OK) error = SDKStatus(status_code=SDKStatusCode.ERROR) unset_translated = self.exporter._translate_data([_create_span_with_status(unset)]) ok_translated = self.exporter._translate_data([_create_span_with_status(ok)]) error_translated = self.exporter._translate_data([_create_span_with_status(error)]) self._check_translated_status( unset_translated, Status.STATUS_CODE_UNSET, ) self._check_translated_status( ok_translated, Status.STATUS_CODE_OK, ) self._check_translated_status( error_translated, Status.STATUS_CODE_ERROR, ) # pylint:disable=no-member def test_translate_key_values(self): bool_value = _encode_key_value("bool_type", False) self.assertTrue(isinstance(bool_value, KeyValue)) self.assertEqual(bool_value.key, "bool_type") self.assertTrue(isinstance(bool_value.value, AnyValue)) self.assertFalse(bool_value.value.bool_value) str_value = _encode_key_value("str_type", "str") self.assertTrue(isinstance(str_value, KeyValue)) self.assertEqual(str_value.key, "str_type") self.assertTrue(isinstance(str_value.value, AnyValue)) self.assertEqual(str_value.value.string_value, "str") int_value = _encode_key_value("int_type", 2) self.assertTrue(isinstance(int_value, KeyValue)) self.assertEqual(int_value.key, "int_type") self.assertTrue(isinstance(int_value.value, AnyValue)) self.assertEqual(int_value.value.int_value, 2) double_value = _encode_key_value("double_type", 3.2) self.assertTrue(isinstance(double_value, KeyValue)) self.assertEqual(double_value.key, "double_type") self.assertTrue(isinstance(double_value.value, AnyValue)) self.assertEqual(double_value.value.double_value, 3.2) seq_value = _encode_key_value("seq_type", ["asd", "123"]) self.assertTrue(isinstance(seq_value, KeyValue)) self.assertEqual(seq_value.key, "seq_type") self.assertTrue(isinstance(seq_value.value, AnyValue)) self.assertTrue(isinstance(seq_value.value.array_value, ArrayValue)) arr_value = seq_value.value.array_value self.assertTrue(isinstance(arr_value.values[0], AnyValue)) self.assertEqual(arr_value.values[0].string_value, "asd") self.assertTrue(isinstance(arr_value.values[1], AnyValue)) self.assertEqual(arr_value.values[1].string_value, "123") def test_dropped_values(self): span = get_span_with_dropped_attributes_events_links() # pylint:disable=protected-access translated = self.exporter._translate_data([span]) self.assertEqual( 1, translated.resource_spans[0].scope_spans[0].spans[0].dropped_links_count, ) self.assertEqual( 2, translated.resource_spans[0].scope_spans[0].spans[0].dropped_attributes_count, ) self.assertEqual( 3, translated.resource_spans[0].scope_spans[0].spans[0].dropped_events_count, ) self.assertEqual( 2, translated.resource_spans[0].scope_spans[0].spans[0].links[0].dropped_attributes_count, ) self.assertEqual( 2, translated.resource_spans[0].scope_spans[0].spans[0].events[0].dropped_attributes_count, ) def _create_span_with_status(status: SDKStatus): span = _Span( "a", context=Mock( trace_state={"a": "b", "c": "d"}, span_id=10217189687419569865, trace_id=67545097771067222548457157018666467027, ), parent=Mock(span_id=12345), instrumentation_scope=InstrumentationScope(name="name", version="version"), ) span.set_status(status) return span python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-proto-http/000077500000000000000000000000001527070070700275275ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-proto-http/LICENSE000066400000000000000000000261351527070070700305430ustar00rootroot00000000000000 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. python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-proto-http/README.rst000066400000000000000000000022621527070070700312200ustar00rootroot00000000000000OpenTelemetry Collector Protobuf over HTTP Exporter =================================================== |pypi| .. |pypi| image:: https://badge.fury.io/py/opentelemetry-exporter-otlp-proto-http.svg :target: https://pypi.org/project/opentelemetry-exporter-otlp-proto-http/ This library allows to export data to the OpenTelemetry Collector using the OpenTelemetry Protocol using Protobuf over HTTP. Installation ------------ :: pip install opentelemetry-exporter-otlp-proto-http To use a custom ``requests.Session`` (for example to configure authentication), pass it to the exporter: .. code-block:: python import requests from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter exporter = OTLPSpanExporter(session=requests.Session()) References ---------- * `OpenTelemetry Collector Exporter `_ * `OpenTelemetry Collector `_ * `OpenTelemetry `_ * `OpenTelemetry Protocol Specification `_ python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-proto-http/pyproject.toml000066400000000000000000000044531527070070700324510ustar00rootroot00000000000000[build-system] requires = ["hatchling"] build-backend = "hatchling.build" [project] name = "opentelemetry-exporter-otlp-proto-http" dynamic = ["version"] description = "OpenTelemetry Collector Protobuf over HTTP Exporter" 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", "Framework :: OpenTelemetry", "Framework :: OpenTelemetry :: Exporters", "Intended Audience :: Developers", "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 = [ "googleapis-common-protos ~= 1.52", "opentelemetry-api ~= 1.15", "opentelemetry-proto == 1.45.2", "opentelemetry-sdk ~= 1.45.2", "opentelemetry-exporter-otlp-proto-common == 1.45.2", "opentelemetry-exporter-otlp-common == 0.66b2", "opentelemetry-exporter-http-transport[requests] == 0.66b2", "requests ~= 2.7", "typing-extensions >= 4.5.0", ] [project.entry-points.opentelemetry_traces_exporter] otlp_proto_http = "opentelemetry.exporter.otlp.proto.http.trace_exporter:OTLPSpanExporter" [project.entry-points.opentelemetry_metrics_exporter] otlp_proto_http = "opentelemetry.exporter.otlp.proto.http.metric_exporter:OTLPMetricExporter" [project.entry-points.opentelemetry_logs_exporter] otlp_proto_http = "opentelemetry.exporter.otlp.proto.http._log_exporter:OTLPLogExporter" [project.urls] Homepage = "https://github.com/open-telemetry/opentelemetry-python/tree/main/exporter/opentelemetry-exporter-otlp-proto-http" Repository = "https://github.com/open-telemetry/opentelemetry-python" [project.optional-dependencies] gcp-auth = [ "opentelemetry-exporter-credential-provider-gcp >= 0.59b0", ] requests = [ "requests ~= 2.7", "opentelemetry-exporter-http-transport[requests] == 0.66b2", ] [tool.hatch.version] path = "src/opentelemetry/exporter/otlp/proto/http/version/__init__.py" [tool.hatch.build.targets.sdist] include = [ "/src", "/tests", ] [tool.hatch.build.targets.wheel] packages = ["src/opentelemetry"] python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-proto-http/src/000077500000000000000000000000001527070070700303165ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/000077500000000000000000000000001527070070700332125ustar00rootroot00000000000000exporter/000077500000000000000000000000001527070070700350035ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetryotlp/000077500000000000000000000000001527070070700357615ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporterproto/000077500000000000000000000000001527070070700371245ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlphttp/000077500000000000000000000000001527070070700401035ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto__init__.py000066400000000000000000000041721527070070700422200ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 """ This library allows to export tracing data to an OTLP collector. Usage ----- The **OTLP Span Exporter** allows to export `OpenTelemetry`_ traces to the `OTLP`_ collector. You can configure the exporter with the following environment variables: - :envvar:`OTEL_EXPORTER_OTLP_TRACES_TIMEOUT` - :envvar:`OTEL_EXPORTER_OTLP_TRACES_PROTOCOL` - :envvar:`OTEL_EXPORTER_OTLP_TRACES_HEADERS` - :envvar:`OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` - :envvar:`OTEL_EXPORTER_OTLP_TRACES_COMPRESSION` - :envvar:`OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE` - :envvar:`OTEL_EXPORTER_OTLP_TIMEOUT` - :envvar:`OTEL_EXPORTER_OTLP_PROTOCOL` - :envvar:`OTEL_EXPORTER_OTLP_HEADERS` - :envvar:`OTEL_EXPORTER_OTLP_ENDPOINT` - :envvar:`OTEL_EXPORTER_OTLP_COMPRESSION` - :envvar:`OTEL_EXPORTER_OTLP_CERTIFICATE` .. _OTLP: https://github.com/open-telemetry/opentelemetry-collector/ .. _OpenTelemetry: https://github.com/open-telemetry/opentelemetry-python/ .. code:: python from opentelemetry import trace from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchSpanProcessor # Resource can be required for some backends, e.g. Jaeger # If resource wouldn't be set - traces wouldn't appears in Jaeger resource = Resource.create({ "service.name": "service" }) trace.set_tracer_provider(TracerProvider(resource=resource)) tracer = trace.get_tracer(__name__) otlp_exporter = OTLPSpanExporter() span_processor = BatchSpanProcessor(otlp_exporter) trace.get_tracer_provider().add_span_processor(span_processor) with tracer.start_as_current_span("foo"): print("Hello world!") API --- """ import enum from .version import __version__ _OTLP_HTTP_HEADERS = { "Content-Type": "application/x-protobuf", "User-Agent": "OTel-OTLP-Exporter-Python/" + __version__, } class Compression(enum.Enum): NoCompression = "none" Deflate = "deflate" Gzip = "gzip" _common/000077500000000000000000000000001527070070700415325ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http__init__.py000066400000000000000000000152641527070070700436530ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_common# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations import logging import os from collections.abc import Mapping from os import environ from typing import TYPE_CHECKING, Literal from opentelemetry.exporter.http.transport._requests import ( RequestsHTTPTransport, ) from opentelemetry.exporter.otlp.common import http as _http from opentelemetry.exporter.otlp.proto.http import ( _OTLP_HTTP_HEADERS, Compression, ) from opentelemetry.sdk.environment_variables import ( _OTEL_PYTHON_EXPORTER_OTLP_HTTP_CREDENTIAL_PROVIDER, OTEL_EXPORTER_OTLP_CERTIFICATE, OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE, OTEL_EXPORTER_OTLP_CLIENT_KEY, OTEL_EXPORTER_OTLP_COMPRESSION, OTEL_EXPORTER_OTLP_ENDPOINT, OTEL_EXPORTER_OTLP_HEADERS, OTEL_EXPORTER_OTLP_TIMEOUT, ) from opentelemetry.util._importlib_metadata import entry_points from opentelemetry.util.re import parse_env_headers if TYPE_CHECKING: import requests from opentelemetry.exporter.http.transport._base import BaseHTTPTransport _CredentialEnvVar = Literal[ "OTEL_PYTHON_EXPORTER_OTLP_HTTP_LOGS_CREDENTIAL_PROVIDER", "OTEL_PYTHON_EXPORTER_OTLP_HTTP_TRACES_CREDENTIAL_PROVIDER", "OTEL_PYTHON_EXPORTER_OTLP_HTTP_METRICS_CREDENTIAL_PROVIDER", ] _logger = logging.getLogger(__name__) _DEFAULT_ENDPOINT = "http://localhost:4318" _DEFAULT_TIMEOUT = 10 # 64 MiB, in bytes. _DEFAULT_MAX_REQUEST_SIZE = 64 * 1024 * 1024 class RequestPayloadTooLargeError(Exception): """A serialized OTLP request exceeded the configured ``max_request_size``. The class name is emitted as the ``error.type`` attribute on the exporter's failed-export metric, so renaming it changes observable telemetry. """ def _is_request_too_large(serialized_data: bytes, max_request_size: int) -> bool: """Return True if the serialized request exceeds a positive size limit. The size is measured on the uncompressed serialized request, matching the OTLP specification's "before compression" request-size limit. A ``max_request_size`` of ``0`` (or any non-positive value) disables the check. """ return 0 < max_request_size < len(serialized_data) def _load_session_from_envvar( cred_envvar: _CredentialEnvVar, ) -> requests.Session | None: _credential_env = environ.get(_OTEL_PYTHON_EXPORTER_OTLP_HTTP_CREDENTIAL_PROVIDER) or environ.get(cred_envvar) if _credential_env: try: # pylint: disable-next=import-outside-toplevel import requests # noqa: PLC0415 except ImportError as exc: raise ImportError( "The 'requests' package is required to load a credential " "provider session but is not installed. Install it with " "`pip install requests`." ) from exc try: maybe_session = next( iter( entry_points( group="opentelemetry_otlp_credential_provider", name=_credential_env, ) ) ).load()() except StopIteration: raise RuntimeError( f"Requested component '{_credential_env}' not found in " f"entry point 'opentelemetry_otlp_credential_provider'" ) if isinstance(maybe_session, requests.Session): return maybe_session else: raise RuntimeError( f"Requested component '{_credential_env}' is of type {type(maybe_session)}" f" must be of type `requests.Session`." ) return None def _normalize_compression( compression: Compression | _http.Compression | None, ) -> _http.Compression | None: match compression: case Compression.NoCompression: return _http.Compression.NONE case Compression.Deflate: return _http.Compression.DEFLATE case Compression.Gzip: return _http.Compression.GZIP case _: return compression def _resolve_endpoint( endpoint_env_var: str, default_path: Literal["v1/traces", "v1/metrics", "v1/logs"], ) -> str: if endpoint := os.environ.get(endpoint_env_var): return endpoint base_endpoint = os.environ.get(OTEL_EXPORTER_OTLP_ENDPOINT) or _DEFAULT_ENDPOINT return f"{base_endpoint.removesuffix('/')}/{default_path}" def _resolve_headers( headers: Mapping[str, str] | None, headers_env_var: str, ) -> dict[str, str]: headers_ = {k.lower(): v for k, v in _OTLP_HTTP_HEADERS.items()} env_headers = parse_env_headers( os.environ.get(headers_env_var) or os.environ.get(OTEL_EXPORTER_OTLP_HEADERS, ""), liberal=True, ) headers_.update(env_headers) if headers: headers_.update({key.lower(): value for key, value in headers.items()}) return headers_ def _resolve_timeout( timeout_env_var: str, ) -> float: raw = os.environ.get(timeout_env_var) or os.environ.get(OTEL_EXPORTER_OTLP_TIMEOUT) or _DEFAULT_TIMEOUT try: return float(raw) except ValueError: _logger.warning( "Invalid timeout value %r, using default of %s seconds", raw, _DEFAULT_TIMEOUT, ) return float(_DEFAULT_TIMEOUT) def _resolve_compression(compression_env_var: str) -> _http.Compression: value = ( (os.environ.get(compression_env_var) or os.environ.get(OTEL_EXPORTER_OTLP_COMPRESSION) or "none") .lower() .strip() ) try: return _http.Compression.from_str(value) except ValueError: _logger.warning("Unsupported compression type: %s", value) return _http.Compression.NONE def _build_transport( certificate_file: str | None, client_key_file: str | None, client_certificate_file: str | None, certificate_env_var: str, client_key_env_var: str, client_certificate_env_var: str, session: requests.Session | None, ) -> BaseHTTPTransport: verify: bool | str = ( certificate_file or os.environ.get(certificate_env_var) or os.environ.get(OTEL_EXPORTER_OTLP_CERTIFICATE) or True ) client_key_file = ( client_key_file or os.environ.get(client_key_env_var) or os.environ.get(OTEL_EXPORTER_OTLP_CLIENT_KEY) ) client_certificate_file = ( client_certificate_file or os.environ.get(client_certificate_env_var) or os.environ.get(OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE) ) cert = ( (client_certificate_file, client_key_file) if client_certificate_file and client_key_file else client_certificate_file ) return RequestsHTTPTransport(verify=verify, cert=cert, session=session) _log_exporter/000077500000000000000000000000001527070070700427535ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http__init__.py000066400000000000000000000220371527070070700450700ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_log_exporter# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations import logging import os from collections.abc import Mapping, Sequence from typing import TYPE_CHECKING, overload from opentelemetry.exporter.otlp.common import http as _http from opentelemetry.exporter.otlp.common._exporter_metrics import ( create_exporter_metrics, ) from opentelemetry.exporter.otlp.proto.common._log_encoder import encode_logs from opentelemetry.exporter.otlp.proto.http import Compression from opentelemetry.exporter.otlp.proto.http._common import ( _DEFAULT_MAX_REQUEST_SIZE, RequestPayloadTooLargeError, _build_transport, _is_request_too_large, _load_session_from_envvar, _normalize_compression, _resolve_compression, _resolve_endpoint, _resolve_headers, _resolve_timeout, ) from opentelemetry.metrics import MeterProvider from opentelemetry.sdk._logs import ReadableLogRecord from opentelemetry.sdk._logs.export import ( LogRecordExporter, LogRecordExportResult, ) from opentelemetry.sdk._shared_internal import DuplicateFilter from opentelemetry.sdk.environment_variables import ( _OTEL_PYTHON_EXPORTER_OTLP_HTTP_LOGS_CREDENTIAL_PROVIDER, OTEL_EXPORTER_OTLP_LOGS_CERTIFICATE, OTEL_EXPORTER_OTLP_LOGS_CLIENT_CERTIFICATE, OTEL_EXPORTER_OTLP_LOGS_CLIENT_KEY, OTEL_EXPORTER_OTLP_LOGS_COMPRESSION, OTEL_EXPORTER_OTLP_LOGS_ENDPOINT, OTEL_EXPORTER_OTLP_LOGS_HEADERS, OTEL_EXPORTER_OTLP_LOGS_TIMEOUT, OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED, ) from opentelemetry.semconv._incubating.attributes.otel_attributes import ( OtelComponentTypeValues, ) from opentelemetry.semconv.attributes.http_attributes import ( HTTP_RESPONSE_STATUS_CODE, ) if TYPE_CHECKING: import requests from opentelemetry.exporter.http.transport._base import BaseHTTPTransport _logger = logging.getLogger(__name__) # This prevents logs generated when a log fails to be written to generate another log which fails to be written etc. etc. _logger.addFilter(DuplicateFilter()) DEFAULT_COMPRESSION = Compression.NoCompression DEFAULT_ENDPOINT = "http://localhost:4318/" DEFAULT_LOGS_EXPORT_PATH = "v1/logs" DEFAULT_TIMEOUT = 10 # in seconds class OTLPLogExporter(LogRecordExporter): @overload def __init__( self, endpoint: str | None = None, certificate_file: str | None = None, client_key_file: str | None = None, client_certificate_file: str | None = None, headers: Mapping[str, str] | None = None, timeout: float | None = None, compression: Compression | _http.Compression | None = None, session: requests.Session | None = None, *, max_request_size: int | None = None, meter_provider: MeterProvider | None = None, ) -> None: ... @overload def __init__( self, endpoint: str | None = None, certificate_file: None = None, client_key_file: None = None, client_certificate_file: None = None, headers: Mapping[str, str] | None = None, timeout: float | None = None, compression: Compression | _http.Compression | None = None, session: requests.Session | None = None, *, max_request_size: int | None = None, meter_provider: MeterProvider | None = None, _transport: BaseHTTPTransport, ) -> None: ... def __init__( self, endpoint: str | None = None, certificate_file: str | None = None, client_key_file: str | None = None, client_certificate_file: str | None = None, headers: Mapping[str, str] | None = None, timeout: float | None = None, compression: Compression | _http.Compression | None = None, session: requests.Session | None = None, *, max_request_size: int | None = None, meter_provider: MeterProvider | None = None, _transport: BaseHTTPTransport | None = None, ) -> None: """OTLP HTTP log exporter. Args: endpoint: Full URL of the OTLP/HTTP signal endpoint, including the signal path. Example: ``http://collector:4318/v1/logs``. For a base URL without a signal path, set the ``OTEL_EXPORTER_OTLP_ENDPOINT`` environment variable. certificate_file: Path to the CA certificate file for TLS. client_key_file: Path to the client key file for mTLS. client_certificate_file: Path to the client certificate file for mTLS. headers: Headers to send with each export request. timeout: Timeout in seconds for each export request. compression: Compression to use; one of none, gzip, deflate. session: Requests session to use at export. max_request_size: Maximum size in bytes of a serialized request, measured before compression. A request exceeding this size is dropped before being sent. Defaults to 64 MiB; a value of 0 (or any non-positive value) disables the limit. Batch processors group log records by count rather than serialized size, so a batch whose serialized request exceeds this limit is dropped as a whole and recorded as a failed export; reduce the processor's ``max_export_batch_size`` (or raise/disable this limit) if batches may approach it. meter_provider: MeterProvider used for the exporter's own metrics. """ self._endpoint = endpoint or _resolve_endpoint(OTEL_EXPORTER_OTLP_LOGS_ENDPOINT, DEFAULT_LOGS_EXPORT_PATH) self._compression = _normalize_compression(compression) or _resolve_compression( OTEL_EXPORTER_OTLP_LOGS_COMPRESSION ) transport = _transport or _build_transport( certificate_file, client_key_file, client_certificate_file, OTEL_EXPORTER_OTLP_LOGS_CERTIFICATE, OTEL_EXPORTER_OTLP_LOGS_CLIENT_KEY, OTEL_EXPORTER_OTLP_LOGS_CLIENT_CERTIFICATE, session=session or _load_session_from_envvar(_OTEL_PYTHON_EXPORTER_OTLP_HTTP_LOGS_CREDENTIAL_PROVIDER), ) self._max_request_size = _DEFAULT_MAX_REQUEST_SIZE if max_request_size is None else max_request_size self._client = _http._OTLPHTTPClient( transport=transport, endpoint=self._endpoint, kind="logs", timeout=timeout if timeout is not None else _resolve_timeout(OTEL_EXPORTER_OTLP_LOGS_TIMEOUT), compression=self._compression, headers=_resolve_headers(headers, OTEL_EXPORTER_OTLP_LOGS_HEADERS), logger=_logger, ) self._shutdown = False self._metrics = create_exporter_metrics( OtelComponentTypeValues.OTLP_HTTP_LOG_EXPORTER, "logs", self._endpoint, meter_provider, os.environ.get(OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED, "").strip().lower() == "true", ) def export(self, batch: Sequence[ReadableLogRecord]) -> LogRecordExportResult: if self._shutdown: _logger.warning("Exporter already shutdown, ignoring batch") return LogRecordExportResult.FAILURE with self._metrics.export_operation(len(batch)) as result: try: serialized_data = encode_logs(batch).SerializeToString() # pylint: disable-next=broad-exception-caught except Exception as error: _logger.error("Failed to encode logs batch: %s", error) result.error = error return LogRecordExportResult.FAILURE if _is_request_too_large(serialized_data, self._max_request_size): _logger.warning( "Dropping logs batch: serialized size %d bytes exceeds max_request_size %d bytes.", len(serialized_data), self._max_request_size, ) result.error = RequestPayloadTooLargeError( f"Serialized logs request size {len(serialized_data)} " f"bytes exceeds max_request_size " f"{self._max_request_size} bytes." ) return LogRecordExportResult.FAILURE export_result = self._client.export(serialized_data) if not export_result.success: result.error = export_result.error result.error_attrs = ( {HTTP_RESPONSE_STATUS_CODE: export_result.status_code} if export_result.status_code is not None else None ) return LogRecordExportResult.FAILURE return LogRecordExportResult.SUCCESS def force_flush(self, timeout_millis: int = 10_000) -> bool: """Nothing is buffered in this exporter, so this method does nothing.""" return True def shutdown(self): if self._shutdown: _logger.warning("Exporter already shutdown, ignoring call") return self._shutdown = True self._client.shutdown() metric_exporter/000077500000000000000000000000001527070070700433165ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http__init__.py000066400000000000000000000632171527070070700454400ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/metric_exporter# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations import logging import os from collections.abc import Callable, Iterable, Mapping from typing import ( # noqa: F401 TYPE_CHECKING, Any, Optional, overload, ) from typing_extensions import deprecated from opentelemetry.exporter.otlp.common import http as _http from opentelemetry.exporter.otlp.common._aggregation import ( _get_aggregation, _get_temporality, ) from opentelemetry.exporter.otlp.common._exporter_metrics import ( create_exporter_metrics, ) from opentelemetry.exporter.otlp.proto.common._internal import ( _get_resource_data, ) from opentelemetry.exporter.otlp.proto.common.metrics_encoder import ( encode_metrics, ) from opentelemetry.exporter.otlp.proto.http import Compression from opentelemetry.exporter.otlp.proto.http._common import ( _DEFAULT_MAX_REQUEST_SIZE, RequestPayloadTooLargeError, _build_transport, _is_request_too_large, _load_session_from_envvar, _normalize_compression, _resolve_compression, _resolve_endpoint, _resolve_headers, _resolve_timeout, ) from opentelemetry.metrics import MeterProvider from opentelemetry.proto.collector.metrics.v1.metrics_service_pb2 import ( ExportMetricsServiceRequest, ) from opentelemetry.proto.common.v1.common_pb2 import ( # noqa: F401 AnyValue, ArrayValue, InstrumentationScope, KeyValue, KeyValueList, ) from opentelemetry.proto.metrics.v1 import metrics_pb2 as pb2 from opentelemetry.proto.resource.v1.resource_pb2 import Resource # noqa: F401 from opentelemetry.proto.resource.v1.resource_pb2 import ( Resource as PB2Resource, ) from opentelemetry.sdk.environment_variables import ( _OTEL_PYTHON_EXPORTER_OTLP_HTTP_METRICS_CREDENTIAL_PROVIDER, OTEL_EXPORTER_OTLP_METRICS_CERTIFICATE, OTEL_EXPORTER_OTLP_METRICS_CLIENT_CERTIFICATE, OTEL_EXPORTER_OTLP_METRICS_CLIENT_KEY, OTEL_EXPORTER_OTLP_METRICS_COMPRESSION, OTEL_EXPORTER_OTLP_METRICS_ENDPOINT, OTEL_EXPORTER_OTLP_METRICS_HEADERS, OTEL_EXPORTER_OTLP_METRICS_TIMEOUT, OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED, ) from opentelemetry.sdk.metrics._internal.aggregation import Aggregation from opentelemetry.sdk.metrics.export import ( # noqa: F401 AggregationTemporality, Gauge, MetricExporter, MetricExportResult, MetricsData, Sum, ) from opentelemetry.sdk.metrics.export import ( # noqa: F401 Histogram as HistogramType, ) from opentelemetry.sdk.resources import Resource as SDKResource from opentelemetry.semconv._incubating.attributes.otel_attributes import ( OtelComponentTypeValues, ) from opentelemetry.semconv.attributes.http_attributes import ( HTTP_RESPONSE_STATUS_CODE, ) if TYPE_CHECKING: import requests from opentelemetry.exporter.http.transport._base import BaseHTTPTransport _logger = logging.getLogger(__name__) DEFAULT_COMPRESSION = Compression.NoCompression DEFAULT_ENDPOINT = "http://localhost:4318/" DEFAULT_METRICS_EXPORT_PATH = "v1/metrics" DEFAULT_TIMEOUT = 10 # in seconds class OTLPMetricExporter(MetricExporter): @overload def __init__( self, endpoint: str | None = None, certificate_file: str | None = None, client_key_file: str | None = None, client_certificate_file: str | None = None, headers: Mapping[str, str] | None = None, timeout: float | None = None, compression: Compression | _http.Compression | None = None, session: requests.Session | None = None, preferred_temporality: dict[type, AggregationTemporality] | None = None, preferred_aggregation: dict[type, Aggregation] | None = None, max_export_batch_size: int | None = None, *, max_request_size: int | None = None, meter_provider: MeterProvider | None = None, ) -> None: ... @overload def __init__( self, endpoint: str | None = None, certificate_file: None = None, client_key_file: None = None, client_certificate_file: None = None, headers: Mapping[str, str] | None = None, timeout: float | None = None, compression: Compression | _http.Compression | None = None, session: requests.Session | None = None, preferred_temporality: dict[type, AggregationTemporality] | None = None, preferred_aggregation: dict[type, Aggregation] | None = None, max_export_batch_size: int | None = None, *, max_request_size: int | None = None, meter_provider: MeterProvider | None = None, _transport: BaseHTTPTransport, ) -> None: ... def __init__( self, endpoint: str | None = None, certificate_file: str | None = None, client_key_file: str | None = None, client_certificate_file: str | None = None, headers: Mapping[str, str] | None = None, timeout: float | None = None, compression: Compression | _http.Compression | None = None, session: requests.Session | None = None, preferred_temporality: dict[type, AggregationTemporality] | None = None, preferred_aggregation: dict[type, Aggregation] | None = None, max_export_batch_size: int | None = None, *, max_request_size: int | None = None, meter_provider: MeterProvider | None = None, _transport: BaseHTTPTransport | None = None, ) -> None: """OTLP HTTP metrics exporter Args: endpoint: Full URL of the OTLP/HTTP signal endpoint, including the signal path. Example: ``http://collector:4318/v1/metrics``. For a base URL without a signal path, set the ``OTEL_EXPORTER_OTLP_ENDPOINT`` environment variable. certificate_file: Path to the certificate file to use for any TLS client_key_file: Path to the client key file to use for any TLS client_certificate_file: Path to the client certificate file to use for any TLS headers: Headers to be sent with HTTP requests at export timeout: Timeout in seconds for export compression: Compression to use; one of none, gzip, deflate session: Requests session to use at export preferred_temporality: Map of preferred temporality for each metric type. See `opentelemetry.sdk.metrics.export.MetricReader` for more details on what preferred temporality is. preferred_aggregation: Map of preferred aggregation for each metric type. See `opentelemetry.sdk.metrics.export.MetricReader` for more details on what preferred aggregation is. max_export_batch_size: Maximum number of data points to export in a single request. If not set there is no limit to the number of data points in a request. If it is set and the number of data points exceeds the max, the request will be split. max_request_size: Maximum size in bytes of a serialized request, measured before compression. A request exceeding this size is dropped before being sent. Defaults to 64 MiB; a value of 0 (or any non-positive value) disables the limit. Requests are sized after any ``max_export_batch_size`` splitting; a batch whose serialized request still exceeds this limit is dropped as a whole and recorded as a failed export. Reduce ``max_export_batch_size`` (or raise/disable this limit) if batches may approach it. meter_provider: MeterProvider used for the exporter's own metrics. """ MetricExporter.__init__( self, preferred_temporality=_get_temporality(preferred_temporality), preferred_aggregation=_get_aggregation(preferred_aggregation), ) self._endpoint = endpoint or _resolve_endpoint(OTEL_EXPORTER_OTLP_METRICS_ENDPOINT, DEFAULT_METRICS_EXPORT_PATH) self._compression = _normalize_compression(compression) or _resolve_compression( OTEL_EXPORTER_OTLP_METRICS_COMPRESSION ) transport = _transport or _build_transport( certificate_file, client_key_file, client_certificate_file, OTEL_EXPORTER_OTLP_METRICS_CERTIFICATE, OTEL_EXPORTER_OTLP_METRICS_CLIENT_KEY, OTEL_EXPORTER_OTLP_METRICS_CLIENT_CERTIFICATE, session=session or _load_session_from_envvar(_OTEL_PYTHON_EXPORTER_OTLP_HTTP_METRICS_CREDENTIAL_PROVIDER), ) self._client = _http._OTLPHTTPClient( transport=transport, endpoint=self._endpoint, kind="metrics", timeout=timeout if timeout is not None else _resolve_timeout(OTEL_EXPORTER_OTLP_METRICS_TIMEOUT), compression=self._compression, headers=_resolve_headers(headers, OTEL_EXPORTER_OTLP_METRICS_HEADERS), logger=_logger, ) self._max_export_batch_size: int | None = max_export_batch_size self._max_request_size = _DEFAULT_MAX_REQUEST_SIZE if max_request_size is None else max_request_size self._shutdown = False self._metrics = create_exporter_metrics( OtelComponentTypeValues.OTLP_HTTP_METRIC_EXPORTER, "metrics", self._endpoint, meter_provider, os.environ.get(OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED, "").strip().lower() == "true", ) def _export_batch( self, export_request: ExportMetricsServiceRequest, ) -> MetricExportResult: with self._metrics.export_operation(_count_data_points(export_request)) as result: serialized_data = export_request.SerializeToString() if _is_request_too_large(serialized_data, self._max_request_size): _logger.warning( "Dropping metrics batch: serialized size %d bytes exceeds max_request_size %d bytes.", len(serialized_data), self._max_request_size, ) result.error = RequestPayloadTooLargeError( f"Serialized metrics request size {len(serialized_data)} " f"bytes exceeds max_request_size " f"{self._max_request_size} bytes." ) return MetricExportResult.FAILURE export_result = self._client.export(serialized_data) if not export_result.success: result.error = export_result.error result.error_attrs = ( {HTTP_RESPONSE_STATUS_CODE: export_result.status_code} if export_result.status_code is not None else None ) return MetricExportResult.FAILURE return MetricExportResult.SUCCESS def export( self, metrics_data: MetricsData, timeout_millis: float | None = 10000, **kwargs, ) -> MetricExportResult: if self._shutdown: _logger.warning("Exporter already shutdown, ignoring batch") return MetricExportResult.FAILURE try: export_request = encode_metrics(metrics_data) # pylint: disable-next=broad-exception-caught except Exception as error: _logger.error("Failed to encode metrics batch: %s", error) return MetricExportResult.FAILURE # If no batch size configured, export as single batch with retries as configured if self._max_export_batch_size is None: return self._export_batch(export_request) for batch in _split_metrics_data(export_request, self._max_export_batch_size): export_result = self._export_batch(batch) if export_result != MetricExportResult.SUCCESS: return MetricExportResult.FAILURE # Only returns SUCCESS if all batches succeeded return MetricExportResult.SUCCESS def shutdown(self, timeout_millis: float = 30_000, **kwargs) -> None: if self._shutdown: _logger.warning("Exporter already shutdown, ignoring call") return self._shutdown = True self._client.shutdown() def force_flush(self, timeout_millis: float = 10_000) -> bool: """Nothing is buffered in this exporter, so this method does nothing.""" return True def set_meter_provider(self, meter_provider: MeterProvider) -> None: self._metrics = create_exporter_metrics( OtelComponentTypeValues.OTLP_HTTP_METRIC_EXPORTER, "metrics", self._endpoint, meter_provider, os.environ.get(OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED, "").strip().lower() == "true", ) def _count_data_points(export_request: ExportMetricsServiceRequest) -> int: """Count the number of data points in an encoded metrics export request.""" count = 0 for resource_metrics in export_request.resource_metrics: for scope_metrics in resource_metrics.scope_metrics: for metric in scope_metrics.metrics: field_name = metric.WhichOneof("data") if field_name: count += len(getattr(metric, field_name).data_points) return count def _split_metrics_data( metrics_data: ExportMetricsServiceRequest, max_export_batch_size: int | None = None, ) -> Iterable[ExportMetricsServiceRequest]: """Splits metrics data into several ExportMetricsServiceRequest (copies protobuf originals), based on configured data point max export batch size. Args: metrics_data: metrics object based on HTTP protocol buffer definition Returns: Iterable[ExportMetricsServiceRequest]: An iterable of ExportMetricsServiceRequest objects containing ExportMetricsServiceRequest.ResourceMetrics, ExportMetricsServiceRequest.ScopeMetrics, ExportMetricsServiceRequest.Metrics, and data points """ if not max_export_batch_size: return metrics_data batch_size: int = 0 # Stores split metrics data as editable references # used to write batched pb2 objects for export when finalized split_resource_metrics = [] for resource_metrics in metrics_data.resource_metrics: split_scope_metrics = [] split_resource_metrics.append( { "resource": resource_metrics.resource, "schema_url": resource_metrics.schema_url, "scope_metrics": split_scope_metrics, } ) for scope_metrics in resource_metrics.scope_metrics: split_metrics = [] split_scope_metrics.append( { "scope": scope_metrics.scope, "schema_url": scope_metrics.schema_url, "metrics": split_metrics, } ) for metric in scope_metrics.metrics: split_data_points = [] field_name = metric.WhichOneof("data") if not field_name: _logger.warning("Tried to split and export an unsupported metric type. Skipping.") continue # Get data container using field name # and build metric dictionary dynamically for conciseness data_container = getattr(metric, field_name) metric_dict = { "name": metric.name, "description": metric.description, "unit": metric.unit, field_name: { "data_points": split_data_points, }, } if hasattr(data_container, "aggregation_temporality"): metric_dict[field_name]["aggregation_temporality"] = data_container.aggregation_temporality if hasattr(data_container, "is_monotonic"): metric_dict[field_name]["is_monotonic"] = data_container.is_monotonic split_metrics.append(metric_dict) current_data_points = data_container.data_points for data_point in current_data_points: split_data_points.append(data_point) batch_size += 1 if batch_size >= max_export_batch_size: yield ExportMetricsServiceRequest( resource_metrics=_get_split_resource_metrics_pb2(split_resource_metrics) ) # Reset all the reference variables with current metrics_data position # minus yielded data_points. Need to clear data_points and keep metric # to avoid duplicate data_point export batch_size = 0 split_data_points = [] # Rebuild metric dict generically using same approach as initial creation field_name = metric.WhichOneof("data") if field_name is None: _logger.warning("Tried to split and export an unsupported metric type. Skipping.") continue data_container = getattr(metric, field_name) metric_dict = { "name": metric.name, "description": metric.description, "unit": metric.unit, field_name: { "data_points": split_data_points, }, } if hasattr(data_container, "aggregation_temporality"): metric_dict[field_name]["aggregation_temporality"] = data_container.aggregation_temporality if hasattr(data_container, "is_monotonic"): metric_dict[field_name]["is_monotonic"] = data_container.is_monotonic split_metrics = [metric_dict] split_scope_metrics = [ { "scope": scope_metrics.scope, "schema_url": scope_metrics.schema_url, "metrics": split_metrics, } ] split_resource_metrics = [ { "resource": resource_metrics.resource, "schema_url": resource_metrics.schema_url, "scope_metrics": split_scope_metrics, } ] if not split_data_points: # If data_points is empty remove the whole metric split_metrics.pop() if not split_metrics: # If metrics is empty remove the whole scope_metrics split_scope_metrics.pop() if not split_scope_metrics: # If scope_metrics is empty remove the whole resource_metrics split_resource_metrics.pop() if batch_size > 0: yield ExportMetricsServiceRequest(resource_metrics=_get_split_resource_metrics_pb2(split_resource_metrics)) def _get_split_resource_metrics_pb2( split_resource_metrics: list[dict], ) -> list[pb2.ResourceMetrics]: """Helper that returns a list of pb2.ResourceMetrics objects based on split_resource_metrics. Example input: ```python [ { "resource": , "schema_url": "http://foo-bar", "scope_metrics": [ "scope": , "schema_url": "http://foo-baz", "metrics": [ { "name": "apples", "description": "number of apples purchased", "sum": { "aggregation_temporality": 1, "is_monotonic": "false", "data_points": [ { start_time_unix_nano: 1000 time_unix_nano: 1001 exemplars { time_unix_nano: 1002 span_id: "foo-span" trace_id: "foo-trace" as_int: 5 } as_int: 5 } ] } }, ], ], }, ] ``` Args: split_resource_metrics: A list of dict representations of ResourceMetrics, ScopeMetrics, Metrics, and data points. Returns: List[pb2.ResourceMetrics]: A list of pb2.ResourceMetrics objects containing pb2.ScopeMetrics, pb2.Metrics, and data points """ split_resource_metrics_pb = [] for resource_metrics in split_resource_metrics: new_resource_metrics = pb2.ResourceMetrics( resource=resource_metrics.get("resource"), scope_metrics=[], schema_url=resource_metrics.get("schema_url") or "", ) for scope_metrics in resource_metrics.get("scope_metrics", []): new_scope_metrics = pb2.ScopeMetrics( scope=scope_metrics.get("scope"), metrics=[], schema_url=scope_metrics.get("schema_url") or "", ) for metric in scope_metrics.get("metrics", []): new_metric = None data_points = [] if "sum" in metric: new_metric = pb2.Metric( name=metric.get("name"), description=metric.get("description"), unit=metric.get("unit"), sum=pb2.Sum( data_points=[], aggregation_temporality=metric.get("sum").get("aggregation_temporality"), is_monotonic=metric.get("sum").get("is_monotonic"), ), ) data_points = metric.get("sum").get("data_points") elif "histogram" in metric: new_metric = pb2.Metric( name=metric.get("name"), description=metric.get("description"), unit=metric.get("unit"), histogram=pb2.Histogram( data_points=[], aggregation_temporality=metric.get("histogram").get("aggregation_temporality"), ), ) data_points = metric.get("histogram").get("data_points") elif "exponential_histogram" in metric: new_metric = pb2.Metric( name=metric.get("name"), description=metric.get("description"), unit=metric.get("unit"), exponential_histogram=pb2.ExponentialHistogram( data_points=[], aggregation_temporality=metric.get("exponential_histogram").get("aggregation_temporality"), ), ) data_points = metric.get("exponential_histogram").get("data_points") elif "gauge" in metric: new_metric = pb2.Metric( name=metric.get("name"), description=metric.get("description"), unit=metric.get("unit"), gauge=pb2.Gauge( data_points=[], ), ) data_points = metric.get("gauge").get("data_points") elif "summary" in metric: new_metric = pb2.Metric( name=metric.get("name"), description=metric.get("description"), unit=metric.get("unit"), summary=pb2.Summary( data_points=[], ), ) data_points = metric.get("summary").get("data_points") else: _logger.warning("Tried to split and export an unsupported metric type. Skipping.") continue # Append data points generically using the field name from the metric dict for field_name in [ "sum", "histogram", "exponential_histogram", "gauge", "summary", ]: if field_name in metric: metric_data_container = getattr(new_metric, field_name) for data_point in data_points: metric_data_container.data_points.append(data_point) break new_scope_metrics.metrics.append(new_metric) new_resource_metrics.scope_metrics.append(new_scope_metrics) split_resource_metrics_pb.append(new_resource_metrics) return split_resource_metrics_pb @deprecated( "Use one of the encoders from opentelemetry-exporter-otlp-proto-common instead. Deprecated since version 1.18.0.", ) def get_resource_data( sdk_resource_scope_data: dict[SDKResource, Any], # ResourceDataT? resource_class: Callable[..., PB2Resource], name: str, ) -> list[PB2Resource]: return _get_resource_data(sdk_resource_scope_data, resource_class, name) py.typed000066400000000000000000000000001527070070700415700ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/httptrace_exporter/000077500000000000000000000000001527070070700431315ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http__init__.py000066400000000000000000000214671527070070700452540ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations import logging import os from collections.abc import Mapping, Sequence from typing import TYPE_CHECKING, overload from opentelemetry.exporter.otlp.common import http as _http from opentelemetry.exporter.otlp.common._exporter_metrics import ( create_exporter_metrics, ) from opentelemetry.exporter.otlp.proto.common.trace_encoder import ( encode_spans, ) from opentelemetry.exporter.otlp.proto.http import Compression from opentelemetry.exporter.otlp.proto.http._common import ( _DEFAULT_MAX_REQUEST_SIZE, RequestPayloadTooLargeError, _build_transport, _is_request_too_large, _load_session_from_envvar, _normalize_compression, _resolve_compression, _resolve_endpoint, _resolve_headers, _resolve_timeout, ) from opentelemetry.metrics import MeterProvider from opentelemetry.sdk.environment_variables import ( _OTEL_PYTHON_EXPORTER_OTLP_HTTP_TRACES_CREDENTIAL_PROVIDER, OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE, OTEL_EXPORTER_OTLP_TRACES_CLIENT_CERTIFICATE, OTEL_EXPORTER_OTLP_TRACES_CLIENT_KEY, OTEL_EXPORTER_OTLP_TRACES_COMPRESSION, OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, OTEL_EXPORTER_OTLP_TRACES_HEADERS, OTEL_EXPORTER_OTLP_TRACES_TIMEOUT, OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED, ) from opentelemetry.sdk.trace import ReadableSpan from opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult from opentelemetry.semconv._incubating.attributes.otel_attributes import ( OtelComponentTypeValues, ) from opentelemetry.semconv.attributes.http_attributes import ( HTTP_RESPONSE_STATUS_CODE, ) if TYPE_CHECKING: import requests from opentelemetry.exporter.http.transport._base import BaseHTTPTransport _logger = logging.getLogger(__name__) DEFAULT_COMPRESSION = Compression.NoCompression DEFAULT_ENDPOINT = "http://localhost:4318/" DEFAULT_TRACES_EXPORT_PATH = "v1/traces" DEFAULT_TIMEOUT = 10 # in seconds class OTLPSpanExporter(SpanExporter): @overload def __init__( self, endpoint: str | None = None, certificate_file: str | None = None, client_key_file: str | None = None, client_certificate_file: str | None = None, headers: Mapping[str, str] | None = None, timeout: float | None = None, compression: Compression | _http.Compression | None = None, session: requests.Session | None = None, *, max_request_size: int | None = None, meter_provider: MeterProvider | None = None, ) -> None: ... @overload def __init__( self, endpoint: str | None = None, certificate_file: None = None, client_key_file: None = None, client_certificate_file: None = None, headers: Mapping[str, str] | None = None, timeout: float | None = None, compression: Compression | _http.Compression | None = None, session: requests.Session | None = None, *, max_request_size: int | None = None, meter_provider: MeterProvider | None = None, _transport: BaseHTTPTransport, ) -> None: ... def __init__( self, endpoint: str | None = None, certificate_file: str | None = None, client_key_file: str | None = None, client_certificate_file: str | None = None, headers: Mapping[str, str] | None = None, timeout: float | None = None, compression: Compression | _http.Compression | None = None, session: requests.Session | None = None, *, max_request_size: int | None = None, meter_provider: MeterProvider | None = None, _transport: BaseHTTPTransport | None = None, ) -> None: """OTLP HTTP span exporter. Args: endpoint: Full URL of the OTLP/HTTP signal endpoint, including the signal path. Example: ``http://collector:4318/v1/traces``. For a base URL without a signal path, set the ``OTEL_EXPORTER_OTLP_ENDPOINT`` environment variable. certificate_file: Path to the CA certificate file for TLS. client_key_file: Path to the client key file for mTLS. client_certificate_file: Path to the client certificate file for mTLS. headers: Headers to send with each export request. timeout: Timeout in seconds for each export request. compression: Compression to use; one of none, gzip, deflate. session: Requests session to use at export. max_request_size: Maximum size in bytes of a serialized request, measured before compression. A request exceeding this size is dropped before being sent. Defaults to 64 MiB; a value of 0 (or any non-positive value) disables the limit. Batch processors group spans by count rather than serialized size, so a batch whose serialized request exceeds this limit is dropped as a whole and recorded as a failed export; reduce the processor's ``max_export_batch_size`` (or raise/disable this limit) if batches may approach it. meter_provider: MeterProvider used for the exporter's own metrics. """ self._endpoint = endpoint or _resolve_endpoint(OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, DEFAULT_TRACES_EXPORT_PATH) self._compression = _normalize_compression(compression) or _resolve_compression( OTEL_EXPORTER_OTLP_TRACES_COMPRESSION ) transport = _transport or _build_transport( certificate_file, client_key_file, client_certificate_file, OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE, OTEL_EXPORTER_OTLP_TRACES_CLIENT_KEY, OTEL_EXPORTER_OTLP_TRACES_CLIENT_CERTIFICATE, session=session or _load_session_from_envvar(_OTEL_PYTHON_EXPORTER_OTLP_HTTP_TRACES_CREDENTIAL_PROVIDER), ) self._max_request_size = _DEFAULT_MAX_REQUEST_SIZE if max_request_size is None else max_request_size self._client = _http._OTLPHTTPClient( transport=transport, endpoint=self._endpoint, kind="spans", timeout=timeout if timeout is not None else _resolve_timeout(OTEL_EXPORTER_OTLP_TRACES_TIMEOUT), compression=self._compression, headers=_resolve_headers(headers, OTEL_EXPORTER_OTLP_TRACES_HEADERS), logger=_logger, ) self._shutdown = False self._metrics = create_exporter_metrics( OtelComponentTypeValues.OTLP_HTTP_SPAN_EXPORTER, "traces", self._endpoint, meter_provider, os.environ.get(OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED, "").strip().lower() == "true", ) def export(self, spans: Sequence[ReadableSpan]) -> SpanExportResult: if self._shutdown: _logger.warning("Exporter already shutdown, ignoring batch") return SpanExportResult.FAILURE with self._metrics.export_operation(len(spans)) as result: try: serialized_data = encode_spans(spans).SerializePartialToString() # pylint: disable-next=broad-exception-caught except Exception as error: _logger.error("Failed to encode span batch: %s", error) result.error = error return SpanExportResult.FAILURE if _is_request_too_large(serialized_data, self._max_request_size): _logger.warning( "Dropping span batch: serialized size %d bytes exceeds max_request_size %d bytes.", len(serialized_data), self._max_request_size, ) result.error = RequestPayloadTooLargeError( f"Serialized span request size {len(serialized_data)} " f"bytes exceeds max_request_size " f"{self._max_request_size} bytes." ) return SpanExportResult.FAILURE export_result = self._client.export(serialized_data) if not export_result.success: result.error = export_result.error result.error_attrs = ( {HTTP_RESPONSE_STATUS_CODE: export_result.status_code} if export_result.status_code is not None else None ) return SpanExportResult.FAILURE return SpanExportResult.SUCCESS def shutdown(self): if self._shutdown: _logger.warning("Exporter already shutdown, ignoring call") return self._shutdown = True self._client.shutdown() def force_flush(self, timeout_millis: int = 30000) -> bool: """Nothing is buffered in this exporter, so this method does nothing.""" return True encoder/000077500000000000000000000000001527070070700445505ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter__init__.py000066400000000000000000000033641527070070700466670ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/encoder# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import logging # noqa: F401 from collections import abc # noqa: F401 from collections.abc import Sequence # noqa: F401 from opentelemetry.proto.collector.trace.v1.trace_service_pb2 import ( # noqa: F401 ExportTraceServiceRequest as PB2ExportTraceServiceRequest, ) from opentelemetry.proto.common.v1.common_pb2 import ( # noqa: F401 AnyValue as PB2AnyValue, ) from opentelemetry.proto.common.v1.common_pb2 import ( # noqa: F401 ArrayValue as PB2ArrayValue, ) from opentelemetry.proto.common.v1.common_pb2 import ( # noqa: F401 InstrumentationScope as PB2InstrumentationScope, ) from opentelemetry.proto.common.v1.common_pb2 import ( # noqa: F401 KeyValue as PB2KeyValue, ) from opentelemetry.proto.resource.v1.resource_pb2 import ( # noqa: F401 Resource as PB2Resource, ) from opentelemetry.proto.trace.v1.trace_pb2 import ( # noqa: F401 ResourceSpans as PB2ResourceSpans, ) from opentelemetry.proto.trace.v1.trace_pb2 import ( # noqa: F401 ScopeSpans as PB2ScopeSpans, ) from opentelemetry.proto.trace.v1.trace_pb2 import ( # noqa: F401 Span as PB2SPan, ) from opentelemetry.proto.trace.v1.trace_pb2 import ( # noqa: F401 Status as PB2Status, ) from opentelemetry.sdk.trace import ( Event, # noqa: F401 Resource, # noqa: F401 ) from opentelemetry.sdk.trace import Span as SDKSpan # noqa: F401 from opentelemetry.sdk.util.instrumentation import ( # noqa: F401 InstrumentationScope, ) from opentelemetry.trace import ( Link, # noqa: F401 SpanKind, # noqa: F401 ) from opentelemetry.trace.span import ( # noqa: F401 SpanContext, Status, TraceState, ) from opentelemetry.util.types import Attributes # noqa: F401 version/000077500000000000000000000000001527070070700415705ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http__init__.py000066400000000000000000000001441527070070700437000ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/version# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 __version__ = "1.45.2" python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-proto-http/test-requirements.txt000066400000000000000000000012611527070070700337700ustar00rootroot00000000000000asgiref==3.7.2 certifi==2024.12.14 charset-normalizer==3.3.2 googleapis-common-protos==1.75.0 idna==3.15 iniconfig==2.0.0 mocket==3.14.1 packaging==24.0 pluggy==1.6.0 protobuf==7.35.0 py-cpuinfo==9.0.0 pytest==9.0.3 PyYAML==6.0.1 requests==2.33.0 responses==0.24.1 tomli==2.0.1 typing_extensions==4.12.0 urllib3==2.7.0 wrapt==1.16.0 -e opentelemetry-api -e tests/opentelemetry-test-utils -e exporter/opentelemetry-exporter-otlp-proto-common -e exporter/opentelemetry-exporter-otlp-common -e exporter/opentelemetry-exporter-http-transport[requests,urllib3] -e opentelemetry-proto -e opentelemetry-sdk -e opentelemetry-semantic-conventions -e exporter/opentelemetry-exporter-otlp-proto-http python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-proto-http/tests/000077500000000000000000000000001527070070700306715ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-proto-http/tests/__init__.py000066400000000000000000000014211527070070700330000ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from collections.abc import Callable, Iterator from contextlib import contextmanager from unittest.mock import Mock, patch @contextmanager def _mock_clock( shutdown_event: Mock | None = None, ) -> Iterator[Callable[[float], None]]: _now = 0.0 def advance(delta: float) -> None: nonlocal _now _now += delta def get_time() -> float: return _now if shutdown_event is not None: def _wait(duration: float) -> bool: advance(duration) return False shutdown_event.wait.side_effect = _wait with patch( "opentelemetry.exporter.otlp.common.http.time.time", side_effect=get_time, ): yield advance python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-proto-http/tests/metrics/000077500000000000000000000000001527070070700323375ustar00rootroot00000000000000__init__.py000066400000000000000000000000001527070070700343570ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-proto-http/tests/metricstest_otlp_metrics_exporter.py000066400000000000000000001507341527070070700403370ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-proto-http/tests/metrics# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # pylint: disable=too-many-lines,protected-access,too-many-public-methods import threading import time from logging import WARNING from os import environ from unittest import TestCase from unittest.mock import Mock, patch import requests from mocket import Mocket, Mocketizer, mocketize from mocket.mocks.mockhttp import Entry, Response from opentelemetry.exporter.http.transport._requests import ( RequestsHTTPTransport, ) from opentelemetry.exporter.http.transport._urllib3 import ( Urllib3HTTPTransport, ) from opentelemetry.exporter.otlp.common import http as _http from opentelemetry.exporter.otlp.proto.common.metrics_encoder import ( encode_metrics, ) from opentelemetry.exporter.otlp.proto.http import Compression from opentelemetry.exporter.otlp.proto.http._common import ( _DEFAULT_TIMEOUT, _build_transport, ) from opentelemetry.exporter.otlp.proto.http.metric_exporter import ( DEFAULT_ENDPOINT, DEFAULT_METRICS_EXPORT_PATH, OTLPMetricExporter, _count_data_points, _get_split_resource_metrics_pb2, _split_metrics_data, ) from opentelemetry.exporter.otlp.proto.http.version import __version__ from opentelemetry.proto.collector.metrics.v1.metrics_service_pb2 import ( ExportMetricsServiceRequest, ) from opentelemetry.proto.common.v1.common_pb2 import ( InstrumentationScope, KeyValue, ) from opentelemetry.proto.metrics.v1 import metrics_pb2 as pb2 from opentelemetry.proto.resource.v1.resource_pb2 import ( Resource as Pb2Resource, ) from opentelemetry.sdk.environment_variables import ( _OTEL_PYTHON_EXPORTER_OTLP_HTTP_METRICS_CREDENTIAL_PROVIDER, OTEL_EXPORTER_OTLP_COMPRESSION, OTEL_EXPORTER_OTLP_ENDPOINT, OTEL_EXPORTER_OTLP_HEADERS, OTEL_EXPORTER_OTLP_METRICS_CERTIFICATE, OTEL_EXPORTER_OTLP_METRICS_CLIENT_CERTIFICATE, OTEL_EXPORTER_OTLP_METRICS_CLIENT_KEY, OTEL_EXPORTER_OTLP_METRICS_COMPRESSION, OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION, OTEL_EXPORTER_OTLP_METRICS_ENDPOINT, OTEL_EXPORTER_OTLP_METRICS_HEADERS, OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE, OTEL_EXPORTER_OTLP_METRICS_TIMEOUT, OTEL_EXPORTER_OTLP_TIMEOUT, OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED, ) from opentelemetry.sdk.metrics import ( Counter, Histogram, MeterProvider, ObservableCounter, ObservableGauge, ObservableUpDownCounter, UpDownCounter, ) from opentelemetry.sdk.metrics.export import ( AggregationTemporality, InMemoryMetricReader, MetricExportResult, MetricsData, ResourceMetrics, ScopeMetrics, ) from opentelemetry.sdk.metrics.view import ( ExplicitBucketHistogramAggregation, ExponentialBucketHistogramAggregation, ) from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.util.instrumentation import ( InstrumentationScope as SDKInstrumentationScope, ) from opentelemetry.test.metrictestutil import _generate_sum from opentelemetry.test.mock_test_classes import IterEntryPoint from .. import _mock_clock # noqa: TID252 _TEST_ENDPOINT = "http://localhost:4318/v1/metrics" _USER_AGENT = "OTel-OTLP-Exporter-Python/" + __version__ _BASE_HEADERS = { "content-type": "application/x-protobuf", "user-agent": _USER_AGENT, } def _decode_body(body: bytes) -> ExportMetricsServiceRequest: return ExportMetricsServiceRequest.FromString(body) # pylint: disable=protected-access,too-many-public-methods class TestOTLPMetricExporter(TestCase): def setUp(self): env_patcher = patch.dict("os.environ", {}, clear=True) env_patcher.start() self.addCleanup(env_patcher.stop) self.metric_reader = InMemoryMetricReader() self.meter_provider = MeterProvider(metric_readers=[self.metric_reader]) self.metrics = { "sum_int": MetricsData( resource_metrics=[ ResourceMetrics( resource=Resource( attributes={"a": 1, "b": False}, schema_url="resource_schema_url", ), scope_metrics=[ ScopeMetrics( scope=SDKInstrumentationScope( name="first_name", version="first_version", schema_url="insrumentation_scope_schema_url", ), metrics=[_generate_sum("sum_int", 33)], schema_url="instrumentation_scope_schema_url", ) ], schema_url="resource_schema_url", ) ] ), } def assert_standard_metric_attrs(self, attributes): self.assertEqual(attributes["otel.component.type"], "otlp_http_metric_exporter") self.assertTrue(attributes["otel.component.name"].startswith("otlp_http_metric_exporter/")) self.assertEqual(attributes["server.address"], "localhost") self.assertEqual(attributes["server.port"], 4318) @staticmethod def _create_metrics_data_multiple_data_points( num_data_points: int, ) -> MetricsData: """Helper to create MetricsData with specified number of data points for testing batch splitting.""" metrics = [] for idx in range(num_data_points): metrics.append(_generate_sum(f"sum_int_{idx}", 33)) return MetricsData( resource_metrics=[ ResourceMetrics( resource=Resource( attributes={"a": 1, "b": False}, schema_url="resource_schema_url", ), scope_metrics=[ ScopeMetrics( scope=SDKInstrumentationScope( name="first_name", version="first_version", schema_url="insrumentation_scope_schema_url", ), metrics=metrics, schema_url="instrumentation_scope_schema_url", ) ], schema_url="resource_schema_url", ) ] ) def test_max_request_size_default(self): self.assertEqual(OTLPMetricExporter()._max_request_size, 64 * 1024 * 1024) @mocketize def test_oversized_payload_dropped_before_send(self): exporter = OTLPMetricExporter(endpoint=_TEST_ENDPOINT, max_request_size=1) result = exporter.export(self.metrics["sum_int"]) self.assertEqual(result, MetricExportResult.FAILURE) self.assertEqual(len(Mocket.request_list()), 0) @mocketize def test_max_request_size_zero_disables(self): Entry.single_register(Entry.POST, _TEST_ENDPOINT, status=200) exporter = OTLPMetricExporter(endpoint=_TEST_ENDPOINT, max_request_size=0) result = exporter.export(self.metrics["sum_int"]) self.assertEqual(result, MetricExportResult.SUCCESS) @mocketize def test_negative_max_request_size_disables_limit(self): Entry.single_register(Entry.POST, _TEST_ENDPOINT, status=200) exporter = OTLPMetricExporter(endpoint=_TEST_ENDPOINT, max_request_size=-1) result = exporter.export(self.metrics["sum_int"]) self.assertEqual(result, MetricExportResult.SUCCESS) self.assertEqual(len(Mocket.request_list()), 1) @mocketize def test_oversized_payload_dropped_with_batch_splitting(self): exporter = OTLPMetricExporter( endpoint=_TEST_ENDPOINT, max_request_size=1, max_export_batch_size=1, ) result = exporter.export(self.metrics["sum_int"]) self.assertEqual(result, MetricExportResult.FAILURE) self.assertEqual(len(Mocket.request_list()), 0) @patch.dict("os.environ", {OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED: "true"}) @mocketize def test_oversized_payload_records_failure_metric(self): exporter = OTLPMetricExporter( endpoint=_TEST_ENDPOINT, max_request_size=1, meter_provider=self.meter_provider, ) result = exporter.export(self.metrics["sum_int"]) self.assertEqual(result, MetricExportResult.FAILURE) self.assertEqual(len(Mocket.request_list()), 0) metrics_data = self.metric_reader.get_metrics_data() scope_metrics = metrics_data.resource_metrics[0].scope_metrics[0] exported = next( metric for metric in scope_metrics.metrics if metric.name == "otel.sdk.exporter.metric_data_point.exported" ) self.assertEqual( exported.data.data_points[0].attributes["error.type"], "RequestPayloadTooLargeError", ) @patch.dict("os.environ", {OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED: "true"}) @mocketize def test_split_export_records_per_split_data_point_count(self): Entry.register( Entry.POST, _TEST_ENDPOINT, Response(status=200), Response(status=200), Response(status=200), ) metrics_data = MetricsData( resource_metrics=[ ResourceMetrics( resource=Resource(attributes={"a": 1}, schema_url="resource_schema_url"), scope_metrics=[ ScopeMetrics( scope=SDKInstrumentationScope(name="name", version="version"), metrics=[ _generate_sum("s1", 1), _generate_sum("s2", 2), _generate_sum("s3", 3), ], schema_url="scope_schema_url", ) ], schema_url="resource_schema_url", ) ] ) exporter = OTLPMetricExporter( endpoint=_TEST_ENDPOINT, max_export_batch_size=1, meter_provider=self.meter_provider, ) result = exporter.export(metrics_data) self.assertEqual(result, MetricExportResult.SUCCESS) self.assertEqual(len(Mocket.request_list()), 3) internal = self.metric_reader.get_metrics_data() scope_metrics = internal.resource_metrics[0].scope_metrics[0] exported = next( metric for metric in scope_metrics.metrics if metric.name == "otel.sdk.exporter.metric_data_point.exported" ) total = sum(dp.value for dp in exported.data.data_points) self.assertEqual(total, 3) def test_default_transport_is_requests(self): exporter = OTLPMetricExporter() self.assertEqual(exporter._endpoint, DEFAULT_ENDPOINT + DEFAULT_METRICS_EXPORT_PATH) self.assertIs(exporter._compression, _http.Compression.NONE) self.assertIsInstance(exporter._client._transport, RequestsHTTPTransport) def test_session_uses_requests_transport(self): session = requests.Session() exporter = OTLPMetricExporter(session=session) self.assertIsInstance(exporter._client._transport, RequestsHTTPTransport) self.assertIs(exporter._client._transport._session, session) @patch.dict( environ, { _OTEL_PYTHON_EXPORTER_OTLP_HTTP_METRICS_CREDENTIAL_PROVIDER: "custom_credential", }, ) @patch("opentelemetry.exporter.otlp.proto.http._common.entry_points") def test_credential_provider_uses_requests(self, mock_entry_point): credential = requests.Session() mock_entry_point.configure_mock(return_value=[IterEntryPoint("custom_credential", lambda: credential)]) exporter = OTLPMetricExporter() self.assertIsInstance(exporter._client._transport, RequestsHTTPTransport) self.assertIs(exporter._client._transport._session, credential) @patch.dict( environ, { _OTEL_PYTHON_EXPORTER_OTLP_HTTP_METRICS_CREDENTIAL_PROVIDER: "bad_credential", }, ) @patch("opentelemetry.exporter.otlp.proto.http._common.entry_points") def test_entrypoint_wrong_type_raises(self, mock_entry_point): mock_entry_point.configure_mock(return_value=[IterEntryPoint("bad_credential", lambda: 1)]) with self.assertRaises(RuntimeError): OTLPMetricExporter() def test_compression_dual_enum_acceptance(self): cases = ( (Compression.NoCompression, _http.Compression.NONE), (Compression.Deflate, _http.Compression.DEFLATE), (Compression.Gzip, _http.Compression.GZIP), (_http.Compression.NONE, _http.Compression.NONE), (_http.Compression.DEFLATE, _http.Compression.DEFLATE), (_http.Compression.GZIP, _http.Compression.GZIP), ) for compression, expected in cases: with self.subTest(compression=compression): exporter = OTLPMetricExporter(compression=compression) self.assertIs(exporter._compression, expected) self.assertIs(exporter._client._compression, expected) @patch.dict(environ, {OTEL_EXPORTER_OTLP_METRICS_ENDPOINT: "os.env.base"}) def test_exporter_env_endpoint_without_slash(self): exporter = OTLPMetricExporter() self.assertEqual(exporter._endpoint, "os.env.base") def test_configuration_precedence(self): common_env = { OTEL_EXPORTER_OTLP_ENDPOINT: "http://common.example:4318", OTEL_EXPORTER_OTLP_HEADERS: "common-key=common-value", OTEL_EXPORTER_OTLP_COMPRESSION: "gzip", OTEL_EXPORTER_OTLP_TIMEOUT: "5", } signal_env = { OTEL_EXPORTER_OTLP_METRICS_ENDPOINT: "http://signal.example:4318/v1/metrics", OTEL_EXPORTER_OTLP_METRICS_HEADERS: "signal-key=signal-value", OTEL_EXPORTER_OTLP_METRICS_COMPRESSION: "deflate", OTEL_EXPORTER_OTLP_METRICS_TIMEOUT: "7", } explicit_kwargs = { "endpoint": "http://explicit.example:4318/v1/metrics", "headers": {"explicit-key": "explicit-value"}, "compression": _http.Compression.NONE, "timeout": 9, } cases = ( ( "defaults", {}, {}, { "endpoint": DEFAULT_ENDPOINT + DEFAULT_METRICS_EXPORT_PATH, "compression": _http.Compression.NONE, "headers": _BASE_HEADERS, "timeout": float(_DEFAULT_TIMEOUT), }, ), ( "common_env", common_env, {}, { "endpoint": "http://common.example:4318/v1/metrics", "compression": _http.Compression.GZIP, "headers": { **_BASE_HEADERS, "common-key": "common-value", "Content-Encoding": "gzip", }, "timeout": 5.0, }, ), ( "signal_env_overrides_common", {**common_env, **signal_env}, {}, { "endpoint": "http://signal.example:4318/v1/metrics", "compression": _http.Compression.DEFLATE, "headers": { **_BASE_HEADERS, "signal-key": "signal-value", "Content-Encoding": "deflate", }, "timeout": 7.0, }, ), ( "constructor_overrides_all", {**common_env, **signal_env}, explicit_kwargs, { "endpoint": "http://explicit.example:4318/v1/metrics", "compression": _http.Compression.NONE, "headers": { **_BASE_HEADERS, "signal-key": "signal-value", "explicit-key": "explicit-value", }, "timeout": 9.0, }, ), ) for label, env, kwargs, expected in cases: with self.subTest(label), patch.dict(environ, env, clear=True): exporter = OTLPMetricExporter(**kwargs) self.assertEqual(exporter._endpoint, expected["endpoint"]) self.assertIs(exporter._compression, expected["compression"]) self.assertEqual(exporter._client._headers, expected["headers"]) self.assertEqual(exporter._client._timeout, expected["timeout"]) @mocketize @patch( "opentelemetry.exporter.otlp.proto.http.metric_exporter._build_transport", wraps=_build_transport, ) def test_certificate_args(self, mock_build_transport): Entry.single_register(Entry.POST, _TEST_ENDPOINT, status=200) exporter = OTLPMetricExporter( endpoint=_TEST_ENDPOINT, certificate_file="ca.pem", client_key_file="client-key.pem", client_certificate_file="client-cert.pem", ) mock_build_transport.assert_called_once_with( "ca.pem", "client-key.pem", "client-cert.pem", OTEL_EXPORTER_OTLP_METRICS_CERTIFICATE, OTEL_EXPORTER_OTLP_METRICS_CLIENT_KEY, OTEL_EXPORTER_OTLP_METRICS_CLIENT_CERTIFICATE, session=None, ) transport_session = exporter._client._transport._session self.assertEqual(transport_session.verify, "ca.pem") self.assertEqual(transport_session.cert, ("client-cert.pem", "client-key.pem")) @mocketize @patch("opentelemetry.exporter.otlp.proto.http.metric_exporter._build_transport") def test_custom_transport(self, mock_build_transport): Entry.single_register(Entry.POST, _TEST_ENDPOINT, status=200) custom_transport = Urllib3HTTPTransport() exporter = OTLPMetricExporter(endpoint=_TEST_ENDPOINT, _transport=custom_transport) mock_build_transport.assert_not_called() self.assertIs(exporter._client._transport, custom_transport) result = exporter.export(self.metrics["sum_int"]) self.assertEqual(result, MetricExportResult.SUCCESS) @mocketize def test_serialization(self): Entry.single_register(Entry.POST, _TEST_ENDPOINT, status=200) exporter = OTLPMetricExporter(endpoint=_TEST_ENDPOINT) transport = exporter._client._transport with patch.object(transport, "request", wraps=transport.request) as mock_request: self.assertEqual( exporter.export(self.metrics["sum_int"]), MetricExportResult.SUCCESS, ) serialized_data = encode_metrics(self.metrics["sum_int"]) sent_data = mock_request.call_args.kwargs["data"] self.assertEqual(_decode_body(sent_data), serialized_data) @patch.dict("os.environ", {OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED: " true "}) @mocketize def test_success_records_metrics(self): Entry.single_register(Entry.POST, _TEST_ENDPOINT, status=200) exporter = OTLPMetricExporter(endpoint=_TEST_ENDPOINT, meter_provider=self.meter_provider) self.assertEqual( exporter.export(self.metrics["sum_int"]), MetricExportResult.SUCCESS, ) metrics_data = self.metric_reader.get_metrics_data() scope_metrics = metrics_data.resource_metrics[0].scope_metrics[0] metrics = sorted(scope_metrics.metrics, key=lambda m: m.name) self.assertEqual(len(metrics), 3) names = [m.name for m in metrics] self.assertIn("otel.sdk.exporter.metric_data_point.exported", names) self.assertIn("otel.sdk.exporter.metric_data_point.inflight", names) self.assertIn("otel.sdk.exporter.operation.duration", names) for metric in metrics: self.assert_standard_metric_attrs(metric.data.data_points[0].attributes) @patch.dict("os.environ", {OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED: "true"}) @mocketize def test_failure_records_metrics(self): Entry.single_register(Entry.POST, _TEST_ENDPOINT, status=401) exporter = OTLPMetricExporter(endpoint=_TEST_ENDPOINT, meter_provider=self.meter_provider) self.assertEqual( exporter.export(self.metrics["sum_int"]), MetricExportResult.FAILURE, ) metrics_data = self.metric_reader.get_metrics_data() scope_metrics = metrics_data.resource_metrics[0].scope_metrics[0] metrics = sorted(scope_metrics.metrics, key=lambda m: m.name) duration_metric = next(m for m in metrics if m.name == "otel.sdk.exporter.operation.duration") self.assertEqual( duration_metric.data.data_points[0].attributes["http.response.status_code"], 401, ) @mocketize def test_export_max_export_batch_size_single_batch_integration(self): Entry.single_register(Entry.POST, _TEST_ENDPOINT, status=200) # 2 data points, batch size of 3: fits in one batch metrics_data = self._create_metrics_data_multiple_data_points(2) exporter = OTLPMetricExporter(endpoint=_TEST_ENDPOINT, max_export_batch_size=3) transport = exporter._client._transport with patch.object(transport, "request", wraps=transport.request) as mock_request: result = exporter.export(metrics_data) self.assertEqual(result, MetricExportResult.SUCCESS) self.assertEqual(mock_request.call_count, 1) request = _decode_body(mock_request.call_args.kwargs["data"]) self.assertEqual(len(request.resource_metrics), 1) metrics = request.resource_metrics[0].scope_metrics[0].metrics metric_names = {metric.name for metric in metrics} self.assertEqual(metric_names, {"sum_int_0", "sum_int_1"}) @mocketize def test_export_max_export_batch_size_multiple_batches_integration(self): Entry.register( Entry.POST, _TEST_ENDPOINT, Response(status=200), Response(status=200), ) # 3 data points, batch size of 2: requires 2 batches metrics_data = self._create_metrics_data_multiple_data_points(3) exporter = OTLPMetricExporter(endpoint=_TEST_ENDPOINT, max_export_batch_size=2) transport = exporter._client._transport with patch.object(transport, "request", wraps=transport.request) as mock_request: result = exporter.export(metrics_data) self.assertEqual(result, MetricExportResult.SUCCESS) self.assertEqual(mock_request.call_count, 2) first_request = _decode_body(mock_request.call_args_list[0].kwargs["data"]) first_metrics = first_request.resource_metrics[0].scope_metrics[0].metrics self.assertEqual({m.name for m in first_metrics}, {"sum_int_0", "sum_int_1"}) second_request = _decode_body(mock_request.call_args_list[1].kwargs["data"]) second_metrics = second_request.resource_metrics[0].scope_metrics[0].metrics self.assertEqual(len(second_metrics), 1) self.assertEqual(second_metrics[0].name, "sum_int_2") @mocketize def test_export_max_export_batch_size_retry_scenarios_integration(self): Entry.register( Entry.POST, _TEST_ENDPOINT, Response(status=200), Response(status=400), ) # 3 data points, batch size of 2: requires 2 batches metrics_data = self._create_metrics_data_multiple_data_points(3) exporter = OTLPMetricExporter(endpoint=_TEST_ENDPOINT, max_export_batch_size=2) result = exporter.export(metrics_data) self.assertEqual(result, MetricExportResult.FAILURE) self.assertEqual(len(Mocket.request_list()), 2) @mocketize def test_export_max_export_batch_size_retryable_failure_integration(self): Entry.register( Entry.POST, _TEST_ENDPOINT, Response(status=200), Response(status=503), Response(status=200), ) # 3 data points, batch size of 2: requires 2 batches metrics_data = self._create_metrics_data_multiple_data_points(3) exporter = OTLPMetricExporter(endpoint=_TEST_ENDPOINT, max_export_batch_size=2, timeout=2.0) result = exporter.export(metrics_data) self.assertEqual(result, MetricExportResult.SUCCESS) # First batch + retry of second batch self.assertEqual(len(Mocket.request_list()), 3) def test_count_data_points(self): request = encode_metrics(self._create_metrics_data_multiple_data_points(5)) self.assertEqual(_count_data_points(request), 5) self.assertEqual(_count_data_points(ExportMetricsServiceRequest()), 0) @mocketize def test_export_records_per_batch_data_point_count(self): Entry.register( Entry.POST, _TEST_ENDPOINT, Response(status=200), Response(status=200), ) # 3 data points, batch size of 2: requires 2 batches metrics_data = self._create_metrics_data_multiple_data_points(3) exporter = OTLPMetricExporter(endpoint=_TEST_ENDPOINT, max_export_batch_size=2) with patch.object( exporter._metrics, "export_operation", wraps=exporter._metrics.export_operation, ) as mock_op: result = exporter.export(metrics_data) self.assertEqual(result, MetricExportResult.SUCCESS) # Each batch reports only its own data points (sums to 3), rather # than the total (3) being reported for every batch (== 6). counts = [call.args[0] for call in mock_op.call_args_list] self.assertEqual(counts, [2, 1]) @mocketize def test_export_records_total_data_point_count_single_batch(self): Entry.single_register(Entry.POST, _TEST_ENDPOINT, status=200) metrics_data = self._create_metrics_data_multiple_data_points(2) exporter = OTLPMetricExporter(endpoint=_TEST_ENDPOINT) with patch.object( exporter._metrics, "export_operation", wraps=exporter._metrics.export_operation, ) as mock_op: result = exporter.export(metrics_data) self.assertEqual(result, MetricExportResult.SUCCESS) counts = [call.args[0] for call in mock_op.call_args_list] self.assertEqual(counts, [2]) def test_aggregation_temporality(self): cumulative_all = { Counter: AggregationTemporality.CUMULATIVE, UpDownCounter: AggregationTemporality.CUMULATIVE, Histogram: AggregationTemporality.CUMULATIVE, ObservableCounter: AggregationTemporality.CUMULATIVE, ObservableUpDownCounter: AggregationTemporality.CUMULATIVE, ObservableGauge: AggregationTemporality.CUMULATIVE, } cases = ( ("unset", None, False, cumulative_all), ("cumulative", "CUMULATIVE", False, cumulative_all), ("invalid_falls_back_to_cumulative", "ABC", True, cumulative_all), ( "delta", "DELTA", False, { Counter: AggregationTemporality.DELTA, UpDownCounter: AggregationTemporality.CUMULATIVE, Histogram: AggregationTemporality.DELTA, ObservableCounter: AggregationTemporality.DELTA, ObservableUpDownCounter: AggregationTemporality.CUMULATIVE, ObservableGauge: AggregationTemporality.CUMULATIVE, }, ), ( "low_memory", "LOWMEMORY", False, { Counter: AggregationTemporality.DELTA, UpDownCounter: AggregationTemporality.CUMULATIVE, Histogram: AggregationTemporality.DELTA, ObservableCounter: AggregationTemporality.CUMULATIVE, ObservableUpDownCounter: AggregationTemporality.CUMULATIVE, ObservableGauge: AggregationTemporality.CUMULATIVE, }, ), ) for label, env_value, expect_warning, expected in cases: env = {} if env_value is None else {OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE: env_value} with self.subTest(label), patch.dict(environ, env): if expect_warning: with self.assertLogs(level=WARNING): otlp_metric_exporter = OTLPMetricExporter() else: otlp_metric_exporter = OTLPMetricExporter() for ( instrument_type, expected_temporality, ) in expected.items(): self.assertEqual( otlp_metric_exporter._preferred_temporality[instrument_type], expected_temporality, ) def test_exponential_explicit_bucket_histogram(self): cases = ( ("unset", None, ExplicitBucketHistogramAggregation, None), ( "base2_exponential_bucket_histogram", "base2_exponential_bucket_histogram", ExponentialBucketHistogramAggregation, None, ), ( "invalid_falls_back_to_explicit_bucket", "abc", ExplicitBucketHistogramAggregation, ( "Invalid value for OTEL_EXPORTER_OTLP_METRICS_DEFAULT_" "HISTOGRAM_AGGREGATION: ABC, using explicit bucket " "histogram aggregation" ), ), ( "explicit_bucket_histogram", "explicit_bucket_histogram", ExplicitBucketHistogramAggregation, None, ), ) for label, env_value, expected_type, expected_warning in cases: env = {} if env_value is None else {OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION: env_value} with self.subTest(label), patch.dict(environ, env): if expected_warning: with self.assertLogs(level=WARNING) as log: self.assertIsInstance( OTLPMetricExporter()._preferred_aggregation[Histogram], expected_type, ) self.assertIn(expected_warning, log.output[0]) else: self.assertIsInstance( OTLPMetricExporter()._preferred_aggregation[Histogram], expected_type, ) def test_preferred_aggregation_override(self): histogram_aggregation = ExplicitBucketHistogramAggregation( boundaries=[0.05, 0.1, 0.5, 1, 5, 10], ) exporter = OTLPMetricExporter( preferred_aggregation={ Histogram: histogram_aggregation, }, ) self.assertEqual(exporter._preferred_aggregation[Histogram], histogram_aggregation) @mocketize def test_2xx_status_code(self): Entry.single_register(Entry.POST, _TEST_ENDPOINT, status=200) self.assertEqual( OTLPMetricExporter(endpoint=_TEST_ENDPOINT).export(self.metrics["sum_int"]), MetricExportResult.SUCCESS, ) @mocketize def test_exporter_metrics_disabled_after_set_meter_provider(self): Entry.single_register(Entry.POST, _TEST_ENDPOINT, status=200) exporter = OTLPMetricExporter(endpoint=_TEST_ENDPOINT) exporter.set_meter_provider(self.meter_provider) self.assertEqual( exporter.export(self.metrics["sum_int"]), MetricExportResult.SUCCESS, ) self.assertIsNone(self.metric_reader.get_metrics_data()) def test_export_retryable_status_codes(self): for status_code in (429, 502, 503, 504): with self.subTest(status_code=status_code), Mocketizer(): Entry.register( Entry.POST, _TEST_ENDPOINT, Response(status=status_code), Response(status=200), ) exporter = OTLPMetricExporter(endpoint=_TEST_ENDPOINT, timeout=30.0) shutdown_event = Mock(spec=threading.Event) shutdown_event.is_set.return_value = False exporter._client._shutdown_event = shutdown_event with _mock_clock(shutdown_event): result = exporter.export(self.metrics["sum_int"]) self.assertEqual(result, MetricExportResult.SUCCESS) self.assertEqual(len(Mocket.request_list()), 2) def test_export_non_retryable_status_codes(self): for status_code in (400, 401, 403, 404, 408, 500, 501): with self.subTest(status_code=status_code), Mocketizer(): Entry.single_register(Entry.POST, _TEST_ENDPOINT, status=status_code) exporter = OTLPMetricExporter(endpoint=_TEST_ENDPOINT) result = exporter.export(self.metrics["sum_int"]) self.assertEqual(result, MetricExportResult.FAILURE) self.assertEqual(len(Mocket.request_list()), 1) @mocketize def test_shutdown_interrupts_retry_backoff(self): Entry.register( Entry.POST, _TEST_ENDPOINT, *[Response(status=503)] * 6, ) exporter = OTLPMetricExporter(endpoint=_TEST_ENDPOINT, timeout=1.5) thread = threading.Thread(target=exporter.export, args=(self.metrics["sum_int"],)) before = time.time() thread.start() time.sleep(0.05) exporter.shutdown() thread.join() after = time.time() self.assertLess(after - before, 0.5) def test_split_metrics_data_many_data_points(self): metrics_data = ExportMetricsServiceRequest( resource_metrics=[ _resource_metrics( index=1, scope_metrics=[ _scope_metrics( index=1, metrics=[ _gauge( index=1, data_points=[ _number_data_point(11), _number_data_point(12), _number_data_point(13), ], ), ], ), ], ), ] ) split_metrics_data: list[ExportMetricsServiceRequest] = list( _split_metrics_data( metrics_data=metrics_data, max_export_batch_size=2, ) ) self.assertEqual( [ ExportMetricsServiceRequest( resource_metrics=[ _resource_metrics( index=1, scope_metrics=[ _scope_metrics( index=1, metrics=[ _gauge( index=1, data_points=[ _number_data_point(11), _number_data_point(12), ], ), ], ), ], ), ] ), ExportMetricsServiceRequest( resource_metrics=[ _resource_metrics( index=1, scope_metrics=[ _scope_metrics( index=1, metrics=[ _gauge( index=1, data_points=[ _number_data_point(13), ], ), ], ), ], ), ] ), ], split_metrics_data, ) def test_split_metrics_data_nb_data_points_equal_batch_size(self): metrics_data = ExportMetricsServiceRequest( resource_metrics=[ _resource_metrics( index=1, scope_metrics=[ _scope_metrics( index=1, metrics=[ _gauge( index=1, data_points=[ _number_data_point(11), _number_data_point(12), _number_data_point(13), ], ), ], ), ], ), ] ) split_metrics_data: list[ExportMetricsServiceRequest] = list( _split_metrics_data( metrics_data=metrics_data, max_export_batch_size=3, ) ) self.assertEqual( [ ExportMetricsServiceRequest( resource_metrics=[ _resource_metrics( index=1, scope_metrics=[ _scope_metrics( index=1, metrics=[ _gauge( index=1, data_points=[ _number_data_point(11), _number_data_point(12), _number_data_point(13), ], ), ], ), ], ), ] ), ], split_metrics_data, ) def test_split_metrics_data_many_resources_scopes_metrics(self): metrics_data = ExportMetricsServiceRequest( resource_metrics=[ _resource_metrics( index=1, scope_metrics=[ _scope_metrics( index=1, metrics=[ _gauge( index=1, data_points=[ _number_data_point(11), ], ), _gauge( index=2, data_points=[ _number_data_point(12), ], ), ], ), _scope_metrics( index=2, metrics=[ _gauge( index=3, data_points=[ _number_data_point(13), ], ), ], ), ], ), _resource_metrics( index=2, scope_metrics=[ _scope_metrics( index=3, metrics=[ _gauge( index=4, data_points=[ _number_data_point(14), ], ), ], ), ], ), ] ) split_metrics_data: list[ExportMetricsServiceRequest] = list( _split_metrics_data( metrics_data=metrics_data, max_export_batch_size=2, ) ) self.assertEqual( [ ExportMetricsServiceRequest( resource_metrics=[ _resource_metrics( index=1, scope_metrics=[ _scope_metrics( index=1, metrics=[ _gauge( index=1, data_points=[ _number_data_point(11), ], ), _gauge( index=2, data_points=[ _number_data_point(12), ], ), ], ), ], ), ] ), ExportMetricsServiceRequest( resource_metrics=[ _resource_metrics( index=1, scope_metrics=[ _scope_metrics( index=2, metrics=[ _gauge( index=3, data_points=[ _number_data_point(13), ], ), ], ), ], ), _resource_metrics( index=2, scope_metrics=[ _scope_metrics( index=3, metrics=[ _gauge( index=4, data_points=[ _number_data_point(14), ], ), ], ), ], ), ] ), ], split_metrics_data, ) def test_get_split_resource_metrics_pb2_one_of_each(self): split_resource_metrics = [ { "resource": Pb2Resource( attributes=[KeyValue(key="foo", value={"string_value": "bar"})], ), "schema_url": "http://foo-bar", "scope_metrics": [ { "scope": InstrumentationScope(name="foo-scope", version="1.0.0"), "schema_url": "http://foo-baz", "metrics": [ { "name": "foo-metric", "description": "foo-description", "unit": "foo-unit", "sum": { "aggregation_temporality": 1, "is_monotonic": True, "data_points": [ pb2.NumberDataPoint( attributes=[ KeyValue( key="dp_key", value={"string_value": "dp_value"}, ) ], start_time_unix_nano=12345, time_unix_nano=12350, as_double=42.42, ) ], }, } ], } ], } ] result = _get_split_resource_metrics_pb2(split_resource_metrics) self.assertEqual(len(result), 1) self.assertIsInstance(result[0], pb2.ResourceMetrics) self.assertEqual(result[0].schema_url, "http://foo-bar") self.assertEqual(len(result[0].scope_metrics), 1) self.assertEqual(result[0].scope_metrics[0].scope.name, "foo-scope") self.assertEqual(len(result[0].scope_metrics[0].metrics), 1) self.assertEqual(result[0].scope_metrics[0].metrics[0].name, "foo-metric") self.assertEqual(result[0].scope_metrics[0].metrics[0].sum.is_monotonic, True) def test_get_split_resource_metrics_pb2_multiples(self): split_resource_metrics = [ { "resource": Pb2Resource( attributes=[KeyValue(key="foo1", value={"string_value": "bar2"})], ), "schema_url": "http://foo-bar-1", "scope_metrics": [ { "scope": InstrumentationScope(name="foo-scope-1", version="1.0.0"), "schema_url": "http://foo-baz-1", "metrics": [ { "name": "foo-metric-1", "description": "foo-description-1", "unit": "foo-unit-1", "gauge": { "data_points": [ pb2.NumberDataPoint( attributes=[ KeyValue( key="dp_key", value={"string_value": "dp_value"}, ) ], start_time_unix_nano=12345, time_unix_nano=12350, as_double=42.42, ) ], }, } ], } ], }, { "resource": Pb2Resource( attributes=[KeyValue(key="foo2", value={"string_value": "bar2"})], ), "schema_url": "http://foo-bar-2", "scope_metrics": [ { "scope": InstrumentationScope(name="foo-scope-2", version="2.0.0"), "schema_url": "http://foo-baz-2", "metrics": [ { "name": "foo-metric-2", "description": "foo-description-2", "unit": "foo-unit-2", "histogram": { "aggregation_temporality": 2, "data_points": [ pb2.HistogramDataPoint( attributes=[ KeyValue( key="dp_key", value={"string_value": "dp_value"}, ) ], start_time_unix_nano=12345, time_unix_nano=12350, ) ], }, } ], } ], }, ] result = _get_split_resource_metrics_pb2(split_resource_metrics) self.assertEqual(len(result), 2) self.assertEqual(result[0].schema_url, "http://foo-bar-1") self.assertEqual(result[1].schema_url, "http://foo-bar-2") self.assertEqual(len(result[0].scope_metrics), 1) self.assertEqual(len(result[1].scope_metrics), 1) self.assertEqual(result[0].scope_metrics[0].scope.name, "foo-scope-1") self.assertEqual(result[1].scope_metrics[0].scope.name, "foo-scope-2") self.assertEqual(result[0].scope_metrics[0].metrics[0].name, "foo-metric-1") self.assertEqual(result[1].scope_metrics[0].metrics[0].name, "foo-metric-2") def test_get_split_resource_metrics_pb2_unsupported_metric_type(self): split_resource_metrics = [ { "resource": Pb2Resource( attributes=[KeyValue(key="foo", value={"string_value": "bar"})], ), "schema_url": "http://foo-bar", "scope_metrics": [ { "scope": InstrumentationScope(name="foo", version="1.0.0"), "schema_url": "http://foo-baz", "metrics": [ { "name": "unsupported-metric", "description": "foo-bar", "unit": "foo-bar", "unsupported_metric_type": {}, } ], } ], } ] with self.assertLogs(level="WARNING") as log: result = _get_split_resource_metrics_pb2(split_resource_metrics) self.assertEqual(len(result), 1) self.assertIn( "Tried to split and export an unsupported metric type", log.output[0], ) def _resource_metrics(index: int, scope_metrics: list[pb2.ScopeMetrics]) -> pb2.ResourceMetrics: return pb2.ResourceMetrics( resource={ "attributes": [KeyValue(key="a", value={"int_value": index})], }, schema_url=f"resource_url_{index}", scope_metrics=scope_metrics, ) def _scope_metrics(index: int, metrics: list[pb2.Metric]) -> pb2.ScopeMetrics: return pb2.ScopeMetrics( scope=InstrumentationScope(name=f"scope_{index}"), schema_url=f"scope_url_{index}", metrics=metrics, ) def _gauge(index: int, data_points: list[pb2.NumberDataPoint]) -> pb2.Metric: return pb2.Metric( name=f"gauge_{index}", description="description", unit="unit", gauge=pb2.Gauge(data_points=data_points), ) def _number_data_point(value: int) -> pb2.NumberDataPoint: return pb2.NumberDataPoint( attributes=[ KeyValue(key="a", value={"int_value": 1}), KeyValue(key="b", value={"bool_value": True}), ], start_time_unix_nano=1641946015139533244, time_unix_nano=1641946016139533244, as_int=value, ) python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_common.py000066400000000000000000000443061527070070700336010ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # pylint: disable=protected-access import os import sys import unittest from logging import WARNING from unittest.mock import patch import requests from opentelemetry.exporter.http.transport._requests import ( RequestsHTTPTransport, ) from opentelemetry.exporter.otlp.common import http as _http from opentelemetry.exporter.otlp.proto.http import Compression from opentelemetry.exporter.otlp.proto.http._common import ( _DEFAULT_TIMEOUT, _build_transport, _load_session_from_envvar, _normalize_compression, _resolve_compression, _resolve_endpoint, _resolve_headers, _resolve_timeout, ) from opentelemetry.exporter.otlp.proto.http.version import __version__ from opentelemetry.sdk.environment_variables import ( _OTEL_PYTHON_EXPORTER_OTLP_HTTP_CREDENTIAL_PROVIDER, _OTEL_PYTHON_EXPORTER_OTLP_HTTP_METRICS_CREDENTIAL_PROVIDER, OTEL_EXPORTER_OTLP_CERTIFICATE, OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE, OTEL_EXPORTER_OTLP_CLIENT_KEY, OTEL_EXPORTER_OTLP_COMPRESSION, OTEL_EXPORTER_OTLP_ENDPOINT, OTEL_EXPORTER_OTLP_HEADERS, OTEL_EXPORTER_OTLP_TIMEOUT, OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE, OTEL_EXPORTER_OTLP_TRACES_CLIENT_CERTIFICATE, OTEL_EXPORTER_OTLP_TRACES_CLIENT_KEY, OTEL_EXPORTER_OTLP_TRACES_COMPRESSION, OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, OTEL_EXPORTER_OTLP_TRACES_HEADERS, OTEL_EXPORTER_OTLP_TRACES_TIMEOUT, ) _USER_AGENT = "OTel-OTLP-Exporter-Python/" + __version__ _BASE_HEADERS = { "content-type": "application/x-protobuf", "user-agent": _USER_AGENT, } class TestResolveCommon(unittest.TestCase): def test_resolve_endpoint(self): cases = [ # per-signal wins over base and is returned verbatim (no path added) ( "per_signal_verbatim", { OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: "http://per-signal:4318/v1/traces", OTEL_EXPORTER_OTLP_ENDPOINT: "http://base:4318", }, "v1/traces", "http://per-signal:4318/v1/traces", ), ( "base_no_trailing_slash", {OTEL_EXPORTER_OTLP_ENDPOINT: "http://base:4318"}, "v1/traces", "http://base:4318/v1/traces", ), ( "base_trailing_slash_normalized", {OTEL_EXPORTER_OTLP_ENDPOINT: "http://base:4318/"}, "v1/metrics", "http://base:4318/v1/metrics", ), ( "empty_per_signal_falls_back", { OTEL_EXPORTER_OTLP_ENDPOINT: "http://base:4318", OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: "", }, "v1/traces", "http://base:4318/v1/traces", ), ( "empty_base_falls_back_to_default", {OTEL_EXPORTER_OTLP_ENDPOINT: ""}, "v1/traces", "http://localhost:4318/v1/traces", ), ( "default_traces", {}, "v1/traces", "http://localhost:4318/v1/traces", ), ( "default_logs", {}, "v1/logs", "http://localhost:4318/v1/logs", ), ] for label, env, default_path, expected in cases: with self.subTest(label), patch.dict(os.environ, env, clear=True): self.assertEqual( _resolve_endpoint(OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, default_path), expected, ) def test_resolve_headers(self): cases = [ ("defaults_only", {}, None, _BASE_HEADERS), ( "general_env_merged", {OTEL_EXPORTER_OTLP_HEADERS: "k1=v1,k2=v2"}, None, {**_BASE_HEADERS, "k1": "v1", "k2": "v2"}, ), # per-signal var is used instead of (not merged with) the general one ( "per_signal_overrides_general", { OTEL_EXPORTER_OTLP_HEADERS: "api-key=general,shared=g", OTEL_EXPORTER_OTLP_TRACES_HEADERS: "api-key=per-signal", }, None, {**_BASE_HEADERS, "api-key": "per-signal"}, ), # empty per-signal var falls back to (inherits) the general one ( "empty_per_signal_falls_back_to_general", { OTEL_EXPORTER_OTLP_HEADERS: "api-key=general", OTEL_EXPORTER_OTLP_TRACES_HEADERS: "", }, None, {**_BASE_HEADERS, "api-key": "general"}, ), # explicit arg wins over env and base, by exact key ( "explicit_arg_overrides", {OTEL_EXPORTER_OTLP_HEADERS: "api-key=from-env"}, {"api-key": "explicit", "Content-Type": "text/plain"}, { "content-type": "text/plain", "user-agent": _USER_AGENT, "api-key": "explicit", }, ), # env override of a default header must replace it ( "env_overrides_default_header_case_insensitively", {OTEL_EXPORTER_OTLP_HEADERS: "user-agent=custom-agent"}, None, { "content-type": "application/x-protobuf", "user-agent": "custom-agent", }, ), ( "explicit_arg_overrides_default_header_case_insensitively", {}, {"User-Agent": "explicit-agent"}, { "content-type": "application/x-protobuf", "user-agent": "explicit-agent", }, ), ] for label, env, headers_arg, expected in cases: with self.subTest(label), patch.dict(os.environ, env, clear=True): self.assertEqual( _resolve_headers(headers_arg, OTEL_EXPORTER_OTLP_TRACES_HEADERS), expected, ) def test_resolve_timeout(self): cases = [ ( "per_signal_wins", { OTEL_EXPORTER_OTLP_TRACES_TIMEOUT: "5", OTEL_EXPORTER_OTLP_TIMEOUT: "7", }, 5.0, False, ), ( "falls_back_to_general", {OTEL_EXPORTER_OTLP_TIMEOUT: "7"}, 7.0, False, ), ( "fractional", {OTEL_EXPORTER_OTLP_TRACES_TIMEOUT: "2.5"}, 2.5, False, ), ("default", {}, float(_DEFAULT_TIMEOUT), False), ( "empty_per_signal_falls_back_to_general", { OTEL_EXPORTER_OTLP_TRACES_TIMEOUT: "", OTEL_EXPORTER_OTLP_TIMEOUT: "7", }, 7.0, False, ), ( "empty_falls_back_to_default", {OTEL_EXPORTER_OTLP_TRACES_TIMEOUT: ""}, float(_DEFAULT_TIMEOUT), False, ), ( "invalid_value_logs_and_defaults", {OTEL_EXPORTER_OTLP_TRACES_TIMEOUT: "not-a-number"}, float(_DEFAULT_TIMEOUT), True, ), ] for label, env, expected, errors in cases: with self.subTest(label), patch.dict(os.environ, env, clear=True): if errors: with self.assertLogs(level=WARNING): result = _resolve_timeout(OTEL_EXPORTER_OTLP_TRACES_TIMEOUT) else: result = _resolve_timeout(OTEL_EXPORTER_OTLP_TRACES_TIMEOUT) self.assertEqual(result, expected) self.assertIsInstance(result, float) def test_resolve_compression(self): cases = [ ( "gzip", {OTEL_EXPORTER_OTLP_TRACES_COMPRESSION: "gzip"}, _http.Compression.GZIP, False, ), ( "deflate", {OTEL_EXPORTER_OTLP_TRACES_COMPRESSION: "deflate"}, _http.Compression.DEFLATE, False, ), ( "none", {OTEL_EXPORTER_OTLP_TRACES_COMPRESSION: "none"}, _http.Compression.NONE, False, ), ( "case_and_whitespace", {OTEL_EXPORTER_OTLP_TRACES_COMPRESSION: " GzIp "}, _http.Compression.GZIP, False, ), ( "falls_back_to_general", {OTEL_EXPORTER_OTLP_COMPRESSION: "deflate"}, _http.Compression.DEFLATE, False, ), ( "per_signal_wins", { OTEL_EXPORTER_OTLP_TRACES_COMPRESSION: "gzip", OTEL_EXPORTER_OTLP_COMPRESSION: "deflate", }, _http.Compression.GZIP, False, ), ( "empty_per_signal_falls_back_to_general", { OTEL_EXPORTER_OTLP_TRACES_COMPRESSION: "", OTEL_EXPORTER_OTLP_COMPRESSION: "gzip", }, _http.Compression.GZIP, False, ), ("default", {}, _http.Compression.NONE, False), ( "invalid_warns", {OTEL_EXPORTER_OTLP_TRACES_COMPRESSION: "bogus"}, _http.Compression.NONE, True, ), ] for label, env, expected, warns in cases: with self.subTest(label), patch.dict(os.environ, env, clear=True): if warns: with self.assertLogs(level=WARNING): result = _resolve_compression(OTEL_EXPORTER_OTLP_TRACES_COMPRESSION) else: result = _resolve_compression(OTEL_EXPORTER_OTLP_TRACES_COMPRESSION) self.assertEqual(result, expected) def test_normalize_compression(self): cases = [ ("none_passthrough", None, None), ( "legacy_no_compression", Compression.NoCompression, _http.Compression.NONE, ), ("legacy_deflate", Compression.Deflate, _http.Compression.DEFLATE), ("legacy_gzip", Compression.Gzip, _http.Compression.GZIP), ( "common_passthrough_none", _http.Compression.NONE, _http.Compression.NONE, ), ( "common_passthrough_deflate", _http.Compression.DEFLATE, _http.Compression.DEFLATE, ), ( "common_passthrough_gzip", _http.Compression.GZIP, _http.Compression.GZIP, ), ] for label, given, expected in cases: with self.subTest(label): self.assertEqual(_normalize_compression(given), expected) class TestBuildTransport(unittest.TestCase): @patch.dict(os.environ, {}, clear=True) def test_default_transport_is_requests(self): result = _build_transport( None, None, None, OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE, OTEL_EXPORTER_OTLP_TRACES_CLIENT_KEY, OTEL_EXPORTER_OTLP_TRACES_CLIENT_CERTIFICATE, session=None, ) self.assertIsInstance(result, RequestsHTTPTransport) @patch.dict(os.environ, {"HTTPS_PROXY": "http://proxy.invalid:3129"}, clear=True) def test_default_transport_honors_proxy_env(self): result = _build_transport( None, None, None, OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE, OTEL_EXPORTER_OTLP_TRACES_CLIENT_KEY, OTEL_EXPORTER_OTLP_TRACES_CLIENT_CERTIFICATE, session=None, ) # pylint: disable-next=protected-access settings = result._session.merge_environment_settings( "https://collector.invalid/v1/traces", {}, None, None, None ) self.assertEqual(settings["proxies"]["https"], "http://proxy.invalid:3129") @patch.dict(os.environ, {}, clear=True) def test_session_forces_requests_transport(self): session = requests.Session() result = _build_transport( None, None, None, OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE, OTEL_EXPORTER_OTLP_TRACES_CLIENT_KEY, OTEL_EXPORTER_OTLP_TRACES_CLIENT_CERTIFICATE, session=session, ) self.assertIsInstance(result, RequestsHTTPTransport) # pylint: disable-next=protected-access self.assertIs(result._session, session) def test_build_transport_verify_and_cert(self): cases = [ ( "explicit_args_win", { OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE: "env-cert.pem", OTEL_EXPORTER_OTLP_TRACES_CLIENT_KEY: "env-key.pem", OTEL_EXPORTER_OTLP_TRACES_CLIENT_CERTIFICATE: "env-cert2.pem", }, "arg-cert.pem", "arg-key.pem", "arg-cert2.pem", "arg-cert.pem", ("arg-cert2.pem", "arg-key.pem"), ), ( "per_signal_env_wins_over_general", { OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE: "per-signal-cert.pem", OTEL_EXPORTER_OTLP_CERTIFICATE: "general-cert.pem", OTEL_EXPORTER_OTLP_TRACES_CLIENT_KEY: "per-signal-key.pem", OTEL_EXPORTER_OTLP_CLIENT_KEY: "general-key.pem", OTEL_EXPORTER_OTLP_TRACES_CLIENT_CERTIFICATE: "per-signal-cert2.pem", OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE: "general-cert2.pem", }, None, None, None, "per-signal-cert.pem", ("per-signal-cert2.pem", "per-signal-key.pem"), ), ( "falls_back_to_general_env", { OTEL_EXPORTER_OTLP_CERTIFICATE: "general-cert.pem", OTEL_EXPORTER_OTLP_CLIENT_KEY: "general-key.pem", OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE: "general-cert2.pem", }, None, None, None, "general-cert.pem", ("general-cert2.pem", "general-key.pem"), ), ( "empty_certificate_at_every_level_falls_back_to_default_true", { OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE: "", OTEL_EXPORTER_OTLP_CERTIFICATE: "", }, None, None, None, True, None, ), ( "defaults_verify_true_no_cert", {}, None, None, None, True, None, ), ( "only_cert_file_not_tupled", { OTEL_EXPORTER_OTLP_TRACES_CLIENT_CERTIFICATE: "cert2.pem", }, None, None, None, True, "cert2.pem", ), ] for ( label, env, certificate_file, client_key_file, client_certificate_file, expected_verify, expected_cert, ) in cases: with self.subTest(label), patch.dict(os.environ, env, clear=True): with patch("opentelemetry.exporter.otlp.proto.http._common.RequestsHTTPTransport") as mock_transport: result = _build_transport( certificate_file, client_key_file, client_certificate_file, OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE, OTEL_EXPORTER_OTLP_TRACES_CLIENT_KEY, OTEL_EXPORTER_OTLP_TRACES_CLIENT_CERTIFICATE, session=None, ) mock_transport.assert_called_once_with(verify=expected_verify, cert=expected_cert, session=None) self.assertIs(result, mock_transport.return_value) def test_build_transport_passes_verify_and_cert_to_requests(self): session = requests.Session() with ( patch.dict( os.environ, {OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE: "cert.pem"}, clear=True, ), patch("opentelemetry.exporter.otlp.proto.http._common.RequestsHTTPTransport") as mock_transport, ): result = _build_transport( None, None, None, OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE, OTEL_EXPORTER_OTLP_TRACES_CLIENT_KEY, OTEL_EXPORTER_OTLP_TRACES_CLIENT_CERTIFICATE, session=session, ) mock_transport.assert_called_once_with(verify="cert.pem", cert=None, session=session) self.assertIs(result, mock_transport.return_value) class TestLoadSessionFromEnvvar(unittest.TestCase): @patch.dict( os.environ, {_OTEL_PYTHON_EXPORTER_OTLP_HTTP_CREDENTIAL_PROVIDER: "custom"}, ) @patch.dict(sys.modules, {"requests": None}) def test_missing_requests_raises_helpful_error(self): with self.assertRaises(ImportError) as cm: _load_session_from_envvar(_OTEL_PYTHON_EXPORTER_OTLP_HTTP_METRICS_CREDENTIAL_PROVIDER) self.assertIn("pip install requests", str(cm.exception)) test_proto_log_exporter.py000066400000000000000000000606651527070070700361740ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-proto-http/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # pylint: disable=protected-access,too-many-public-methods import gzip import os import threading import time import unittest import zlib from unittest.mock import Mock, patch import requests import urllib3.exceptions from mocket import Mocket, Mocketizer, mocketize from mocket.mocks.mockhttp import Entry, Response from opentelemetry._logs import LogRecord, SeverityNumber from opentelemetry.exporter.http.transport._requests import ( RequestsHTTPTransport, ) from opentelemetry.exporter.http.transport._urllib3 import ( Urllib3HTTPTransport, ) from opentelemetry.exporter.otlp.common import http as _http from opentelemetry.exporter.otlp.proto.common._log_encoder import encode_logs from opentelemetry.exporter.otlp.proto.http import Compression from opentelemetry.exporter.otlp.proto.http._common import ( _DEFAULT_TIMEOUT, _build_transport, ) from opentelemetry.exporter.otlp.proto.http._log_exporter import ( DEFAULT_ENDPOINT, DEFAULT_LOGS_EXPORT_PATH, OTLPLogExporter, ) from opentelemetry.exporter.otlp.proto.http.version import __version__ from opentelemetry.proto.collector.logs.v1.logs_service_pb2 import ( ExportLogsServiceRequest, ) from opentelemetry.sdk._logs import ReadWriteLogRecord from opentelemetry.sdk._logs.export import LogRecordExportResult from opentelemetry.sdk.environment_variables import ( _OTEL_PYTHON_EXPORTER_OTLP_HTTP_LOGS_CREDENTIAL_PROVIDER, OTEL_EXPORTER_OTLP_COMPRESSION, OTEL_EXPORTER_OTLP_ENDPOINT, OTEL_EXPORTER_OTLP_HEADERS, OTEL_EXPORTER_OTLP_LOGS_CERTIFICATE, OTEL_EXPORTER_OTLP_LOGS_CLIENT_CERTIFICATE, OTEL_EXPORTER_OTLP_LOGS_CLIENT_KEY, OTEL_EXPORTER_OTLP_LOGS_COMPRESSION, OTEL_EXPORTER_OTLP_LOGS_ENDPOINT, OTEL_EXPORTER_OTLP_LOGS_HEADERS, OTEL_EXPORTER_OTLP_LOGS_TIMEOUT, OTEL_EXPORTER_OTLP_TIMEOUT, OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED, ) from opentelemetry.sdk.metrics import MeterProvider from opentelemetry.sdk.metrics.export import InMemoryMetricReader from opentelemetry.sdk.resources import Resource as SDKResource from opentelemetry.sdk.util.instrumentation import InstrumentationScope from opentelemetry.test.mock_test_classes import IterEntryPoint from . import _mock_clock _TEST_ENDPOINT = "http://localhost:4318/v1/logs" _LOGGER_NAME = "opentelemetry.exporter.otlp.proto.http._log_exporter" _USER_AGENT = "OTel-OTLP-Exporter-Python/" + __version__ _BASE_HEADERS = { "content-type": "application/x-protobuf", "user-agent": _USER_AGENT, } def _decode_body(body: bytes) -> ExportLogsServiceRequest: return ExportLogsServiceRequest.FromString(body) def _make_log_record() -> ReadWriteLogRecord: return ReadWriteLogRecord( LogRecord( timestamp=1644650195189786182, severity_text="WARN", severity_number=SeverityNumber.WARN, body="a log message", attributes={"a": 1, "b": "c"}, ), resource=SDKResource({"first_resource": "value"}), instrumentation_scope=InstrumentationScope("name", "version"), ) # pylint: disable=too-many-public-methods class TestOTLPHTTPLogExporter(unittest.TestCase): def setUp(self): env_patcher = patch.dict(os.environ, {}, clear=True) env_patcher.start() self.addCleanup(env_patcher.stop) self.metric_reader = InMemoryMetricReader() self.meter_provider = MeterProvider(metric_readers=[self.metric_reader]) @staticmethod def _mocked_shutdown_event() -> Mock: shutdown_event = Mock(spec=threading.Event) shutdown_event.is_set.return_value = False return shutdown_event def test_max_request_size_default(self): self.assertEqual(OTLPLogExporter()._max_request_size, 64 * 1024 * 1024) @mocketize def test_oversized_payload_dropped_before_send(self): exporter = OTLPLogExporter(endpoint=_TEST_ENDPOINT, max_request_size=1) result = exporter.export([_make_log_record()]) self.assertEqual(result, LogRecordExportResult.FAILURE) self.assertEqual(len(Mocket.request_list()), 0) @mocketize def test_max_request_size_zero_disables(self): Entry.single_register(Entry.POST, _TEST_ENDPOINT, status=200) exporter = OTLPLogExporter(endpoint=_TEST_ENDPOINT, max_request_size=0) result = exporter.export([_make_log_record()]) self.assertEqual(result, LogRecordExportResult.SUCCESS) @mocketize def test_negative_max_request_size_disables_limit(self): Entry.single_register(Entry.POST, _TEST_ENDPOINT, status=200) exporter = OTLPLogExporter(endpoint=_TEST_ENDPOINT, max_request_size=-1) result = exporter.export([_make_log_record()]) self.assertEqual(result, LogRecordExportResult.SUCCESS) self.assertEqual(len(Mocket.request_list()), 1) @patch.dict("os.environ", {OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED: "true"}) @mocketize def test_oversized_payload_records_failure_metric(self): exporter = OTLPLogExporter( endpoint=_TEST_ENDPOINT, max_request_size=1, meter_provider=self.meter_provider, ) result = exporter.export([_make_log_record()]) self.assertEqual(result, LogRecordExportResult.FAILURE) self.assertEqual(len(Mocket.request_list()), 0) metrics_data = self.metric_reader.get_metrics_data() scope_metrics = metrics_data.resource_metrics[0].scope_metrics[0] exported = next(metric for metric in scope_metrics.metrics if metric.name == "otel.sdk.exporter.log.exported") self.assertEqual( exported.data.data_points[0].attributes["error.type"], "RequestPayloadTooLargeError", ) def assert_standard_metric_attrs(self, attributes): self.assertEqual(attributes["otel.component.type"], "otlp_http_log_exporter") self.assertTrue(attributes["otel.component.name"].startswith("otlp_http_log_exporter/")) self.assertEqual(attributes["server.address"], "localhost") self.assertEqual(attributes["server.port"], 4318) def test_default_transport_is_requests(self): exporter = OTLPLogExporter() self.assertEqual(exporter._endpoint, DEFAULT_ENDPOINT + DEFAULT_LOGS_EXPORT_PATH) self.assertIs(exporter._compression, _http.Compression.NONE) self.assertIsInstance(exporter._client._transport, RequestsHTTPTransport) def test_session_uses_requests_transport(self): session = requests.Session() exporter = OTLPLogExporter(session=session) self.assertIsInstance(exporter._client._transport, RequestsHTTPTransport) self.assertIs(exporter._client._transport._session, session) @patch.dict( os.environ, { _OTEL_PYTHON_EXPORTER_OTLP_HTTP_LOGS_CREDENTIAL_PROVIDER: "custom_credential", }, ) @patch("opentelemetry.exporter.otlp.proto.http._common.entry_points") def test_credential_provider_uses_requests(self, mock_entry_point): credential = requests.Session() mock_entry_point.configure_mock(return_value=[IterEntryPoint("custom_credential", lambda: credential)]) exporter = OTLPLogExporter() self.assertIsInstance(exporter._client._transport, RequestsHTTPTransport) self.assertIs(exporter._client._transport._session, credential) @patch.dict( os.environ, { _OTEL_PYTHON_EXPORTER_OTLP_HTTP_LOGS_CREDENTIAL_PROVIDER: "bad_credential", }, ) @patch("opentelemetry.exporter.otlp.proto.http._common.entry_points") def test_entrypoint_wrong_type_raises(self, mock_entry_point): mock_entry_point.configure_mock(return_value=[IterEntryPoint("bad_credential", lambda: 1)]) with self.assertRaises(RuntimeError): OTLPLogExporter() @patch.dict( os.environ, { _OTEL_PYTHON_EXPORTER_OTLP_HTTP_LOGS_CREDENTIAL_PROVIDER: "missing", }, ) @patch("opentelemetry.exporter.otlp.proto.http._common.entry_points") def test_entrypoint_missing_raises(self, mock_entry_point): mock_entry_point.configure_mock(return_value=[]) with self.assertRaises(RuntimeError): OTLPLogExporter() def test_compression_dual_enum_acceptance(self): cases = ( (Compression.NoCompression, _http.Compression.NONE), (Compression.Deflate, _http.Compression.DEFLATE), (Compression.Gzip, _http.Compression.GZIP), (_http.Compression.NONE, _http.Compression.NONE), (_http.Compression.DEFLATE, _http.Compression.DEFLATE), (_http.Compression.GZIP, _http.Compression.GZIP), ) for compression, expected in cases: with self.subTest(compression=compression): exporter = OTLPLogExporter(compression=compression) self.assertIs(exporter._compression, expected) self.assertIs(exporter._client._compression, expected) @mocketize def test_export_single_log(self): Entry.single_register(Entry.POST, _TEST_ENDPOINT, status=200) exporter = OTLPLogExporter(endpoint=_TEST_ENDPOINT) logs = [_make_log_record()] transport = exporter._client._transport with patch.object(transport, "request", wraps=transport.request) as mock_request: result = exporter.export(logs) self.assertEqual(result, LogRecordExportResult.SUCCESS) request = Mocket.last_request() self.assertEqual(request.method, "POST") self.assertEqual(request.path, "/v1/logs") sent_data = mock_request.call_args.kwargs["data"] self.assertEqual(_decode_body(sent_data), encode_logs(logs)) @mocketize def test_default_endpoint_and_headers(self): Entry.single_register(Entry.POST, "http://localhost:4318/v1/logs", status=200) exporter = OTLPLogExporter() result = exporter.export([_make_log_record()]) self.assertEqual(result, LogRecordExportResult.SUCCESS) headers = Mocket.last_request().headers self.assertEqual(headers["content-type"], "application/x-protobuf") self.assertTrue(headers["user-agent"].startswith("OTel-OTLP-Exporter-Python/")) def test_custom_endpoint(self): url = "http://custom.example:9999/v1/logs" cases = ( ("constructor", {}, {"endpoint": url}), ( "generic_env", {OTEL_EXPORTER_OTLP_ENDPOINT: "http://custom.example:9999"}, {}, ), ("per_signal_env", {OTEL_EXPORTER_OTLP_LOGS_ENDPOINT: url}, {}), ) for label, env, kwargs in cases: with ( self.subTest(label), patch.dict(os.environ, env, clear=True), Mocketizer(), ): Entry.single_register(Entry.POST, url, status=200) exporter = OTLPLogExporter(**kwargs) result = exporter.export([_make_log_record()]) self.assertEqual(result, LogRecordExportResult.SUCCESS) def test_custom_headers(self): cases = ( ("constructor", {}, {"headers": {"x-api-key": "secret"}}), ( "generic_env", {OTEL_EXPORTER_OTLP_HEADERS: "x-api-key=secret"}, {}, ), ( "per_signal_env", {OTEL_EXPORTER_OTLP_LOGS_HEADERS: "x-api-key=secret"}, {}, ), ) for label, env, kwargs in cases: with ( self.subTest(label), patch.dict(os.environ, env, clear=True), Mocketizer(), ): Entry.single_register(Entry.POST, _TEST_ENDPOINT, status=200) exporter = OTLPLogExporter(endpoint=_TEST_ENDPOINT, **kwargs) exporter.export([_make_log_record()]) headers = Mocket.last_request().headers self.assertEqual(headers["x-api-key"], "secret") def test_configuration_precedence(self): common_env = { OTEL_EXPORTER_OTLP_ENDPOINT: "http://common.example:4318", OTEL_EXPORTER_OTLP_HEADERS: "common-key=common-value", OTEL_EXPORTER_OTLP_COMPRESSION: "gzip", OTEL_EXPORTER_OTLP_TIMEOUT: "5", } signal_env = { OTEL_EXPORTER_OTLP_LOGS_ENDPOINT: "http://signal.example:4318/v1/logs", OTEL_EXPORTER_OTLP_LOGS_HEADERS: "signal-key=signal-value", OTEL_EXPORTER_OTLP_LOGS_COMPRESSION: "deflate", OTEL_EXPORTER_OTLP_LOGS_TIMEOUT: "7", } explicit_kwargs = { "endpoint": "http://explicit.example:4318/v1/logs", "headers": {"explicit-key": "explicit-value"}, "compression": _http.Compression.NONE, "timeout": 9, } cases = ( ( "defaults", {}, {}, { "endpoint": DEFAULT_ENDPOINT + DEFAULT_LOGS_EXPORT_PATH, "compression": _http.Compression.NONE, "headers": _BASE_HEADERS, "timeout": float(_DEFAULT_TIMEOUT), }, ), ( "common_env", common_env, {}, { "endpoint": "http://common.example:4318/v1/logs", "compression": _http.Compression.GZIP, "headers": { **_BASE_HEADERS, "common-key": "common-value", "Content-Encoding": "gzip", }, "timeout": 5.0, }, ), ( "signal_env_overrides_common", {**common_env, **signal_env}, {}, { "endpoint": "http://signal.example:4318/v1/logs", "compression": _http.Compression.DEFLATE, "headers": { **_BASE_HEADERS, "signal-key": "signal-value", "Content-Encoding": "deflate", }, "timeout": 7.0, }, ), ( "constructor_overrides_all", {**common_env, **signal_env}, explicit_kwargs, { "endpoint": "http://explicit.example:4318/v1/logs", "compression": _http.Compression.NONE, "headers": { **_BASE_HEADERS, "signal-key": "signal-value", "explicit-key": "explicit-value", }, "timeout": 9.0, }, ), ) for label, env, kwargs, expected in cases: with self.subTest(label), patch.dict(os.environ, env, clear=True): exporter = OTLPLogExporter(**kwargs) self.assertEqual(exporter._endpoint, expected["endpoint"]) self.assertIs(exporter._compression, expected["compression"]) self.assertEqual(exporter._client._headers, expected["headers"]) self.assertEqual(exporter._client._timeout, expected["timeout"]) @mocketize @patch("opentelemetry.exporter.otlp.proto.http._log_exporter._build_transport") def test_custom_transport(self, mock_build_transport): Entry.single_register(Entry.POST, _TEST_ENDPOINT, status=200) custom_transport = Urllib3HTTPTransport() exporter = OTLPLogExporter(endpoint=_TEST_ENDPOINT, _transport=custom_transport) mock_build_transport.assert_not_called() self.assertIs(exporter._client._transport, custom_transport) result = exporter.export([_make_log_record()]) self.assertEqual(result, LogRecordExportResult.SUCCESS) @mocketize @patch( "opentelemetry.exporter.otlp.proto.http._log_exporter._build_transport", wraps=_build_transport, ) def test_certificate_args(self, mock_build_transport): Entry.single_register(Entry.POST, _TEST_ENDPOINT, status=200) exporter = OTLPLogExporter( endpoint=_TEST_ENDPOINT, certificate_file="ca.pem", client_key_file="client-key.pem", client_certificate_file="client-cert.pem", ) mock_build_transport.assert_called_once_with( "ca.pem", "client-key.pem", "client-cert.pem", OTEL_EXPORTER_OTLP_LOGS_CERTIFICATE, OTEL_EXPORTER_OTLP_LOGS_CLIENT_KEY, OTEL_EXPORTER_OTLP_LOGS_CLIENT_CERTIFICATE, session=None, ) transport_session = exporter._client._transport._session self.assertEqual(transport_session.verify, "ca.pem") self.assertEqual(transport_session.cert, ("client-cert.pem", "client-key.pem")) def test_compression_options(self): cases = ( (Compression.NoCompression, None, lambda data: data), (Compression.Gzip, "gzip", gzip.decompress), (Compression.Deflate, "deflate", zlib.decompress), (_http.Compression.NONE, None, lambda data: data), (_http.Compression.GZIP, "gzip", gzip.decompress), (_http.Compression.DEFLATE, "deflate", zlib.decompress), ) for compression, expected_encoding, decompress in cases: with self.subTest(compression=compression), Mocketizer(): Entry.single_register(Entry.POST, _TEST_ENDPOINT, status=200) exporter = OTLPLogExporter(endpoint=_TEST_ENDPOINT, compression=compression) transport = exporter._client._transport logs = [_make_log_record()] with patch.object(transport, "request", wraps=transport.request) as mock_request: result = exporter.export(logs) self.assertEqual(result, LogRecordExportResult.SUCCESS) sent_headers = mock_request.call_args.kwargs["headers"] if expected_encoding is None: self.assertNotIn("Content-Encoding", sent_headers) else: self.assertEqual(sent_headers["Content-Encoding"], expected_encoding) sent_data = mock_request.call_args.kwargs["data"] decompressed = decompress(sent_data) self.assertEqual(_decode_body(decompressed), encode_logs(logs)) def test_export_retryable_status_codes(self): for status_code in (429, 502, 503, 504): with self.subTest(status_code=status_code), Mocketizer(): Entry.register( Entry.POST, _TEST_ENDPOINT, Response(status=status_code), Response(status=200), ) exporter = OTLPLogExporter(endpoint=_TEST_ENDPOINT, timeout=30.0) shutdown_event = self._mocked_shutdown_event() exporter._client._shutdown_event = shutdown_event with _mock_clock(shutdown_event): result = exporter.export([_make_log_record()]) self.assertEqual(result, LogRecordExportResult.SUCCESS) self.assertEqual(len(Mocket.request_list()), 2) def test_export_non_retryable_status_codes(self): for status_code in (400, 401, 403, 404, 408, 500, 501): with self.subTest(status_code=status_code), Mocketizer(): Entry.single_register(Entry.POST, _TEST_ENDPOINT, status=status_code) exporter = OTLPLogExporter(endpoint=_TEST_ENDPOINT) result = exporter.export([_make_log_record()]) self.assertEqual(result, LogRecordExportResult.FAILURE) self.assertEqual(len(Mocket.request_list()), 1) @mocketize def test_export_connection_error(self): Entry.register( Entry.POST, _TEST_ENDPOINT, urllib3.exceptions.ProtocolError("simulated reset"), Response(status=200), ) exporter = OTLPLogExporter(endpoint=_TEST_ENDPOINT, timeout=5.0) result = exporter.export([_make_log_record()]) self.assertEqual(result, LogRecordExportResult.SUCCESS) self.assertEqual(len(Mocket.request_list()), 2) @mocketize def test_shutdown_interrupts_retry_backoff(self): Entry.register( Entry.POST, _TEST_ENDPOINT, *[Response(status=503)] * 6, ) exporter = OTLPLogExporter(endpoint=_TEST_ENDPOINT, timeout=1.5) thread = threading.Thread(target=exporter.export, args=([_make_log_record()],)) before = time.time() thread.start() time.sleep(0.05) exporter.shutdown() thread.join() after = time.time() self.assertLess(after - before, 0.5) @mocketize def test_exporter_metrics_disabled_by_default(self): Entry.single_register(Entry.POST, _TEST_ENDPOINT, status=200) exporter = OTLPLogExporter(endpoint=_TEST_ENDPOINT, meter_provider=self.meter_provider) self.assertEqual( exporter.export([_make_log_record()]), LogRecordExportResult.SUCCESS, ) self.assertIsNone(self.metric_reader.get_metrics_data()) @patch.dict("os.environ", {OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED: "true"}) def test_retry_timeout_records_metrics(self): with Mocketizer(): Entry.register( Entry.POST, _TEST_ENDPOINT, Response(status=503), Response(status=503), ) exporter = OTLPLogExporter( endpoint=_TEST_ENDPOINT, timeout=1.5, meter_provider=self.meter_provider, ) shutdown_event = self._mocked_shutdown_event() exporter._client._shutdown_event = shutdown_event with _mock_clock(shutdown_event): result = exporter.export([_make_log_record()]) self.assertEqual(result, LogRecordExportResult.FAILURE) metrics_data = self.metric_reader.get_metrics_data() scope_metrics = metrics_data.resource_metrics[0].scope_metrics[0] metrics = sorted(scope_metrics.metrics, key=lambda m: m.name) self.assertEqual(len(metrics), 3) names = [m.name for m in metrics] self.assertIn("otel.sdk.exporter.log.exported", names) self.assertIn("otel.sdk.exporter.log.inflight", names) self.assertIn("otel.sdk.exporter.operation.duration", names) duration_metric = next(m for m in metrics if m.name == "otel.sdk.exporter.operation.duration") self.assert_standard_metric_attrs(duration_metric.data.data_points[0].attributes) self.assertEqual( duration_metric.data.data_points[0].attributes["http.response.status_code"], 503, ) @mocketize def test_export_after_shutdown(self): exporter = OTLPLogExporter(endpoint=_TEST_ENDPOINT) exporter.shutdown() with self.assertLogs(_LOGGER_NAME, level="WARNING"): result = exporter.export([_make_log_record()]) self.assertEqual(result, LogRecordExportResult.FAILURE) self.assertIsNone(Mocket.last_request()) def test_shutdown_idempotent(self): exporter = OTLPLogExporter(endpoint=_TEST_ENDPOINT) exporter.shutdown() with self.assertLogs(_LOGGER_NAME, level="WARNING"): exporter.shutdown() # pylint: disable-next=no-self-use def test_shutdown_closes_transport(self): exporter = OTLPLogExporter(endpoint=_TEST_ENDPOINT) with patch.object(exporter._client._transport, "close") as mock_close: exporter.shutdown() mock_close.assert_called_once() @mocketize def test_force_flush(self): Entry.single_register(Entry.POST, _TEST_ENDPOINT, status=200) exporter = OTLPLogExporter(endpoint=_TEST_ENDPOINT) self.assertTrue(exporter.force_flush()) exporter.export([_make_log_record()]) self.assertTrue(exporter.force_flush()) @mocketize @patch( "opentelemetry.exporter.otlp.proto.http._log_exporter.encode_logs", side_effect=ValueError("boom"), ) def test_export_encoding_failure(self, mock_encode_logs): exporter = OTLPLogExporter(endpoint=_TEST_ENDPOINT) with self.assertLogs(_LOGGER_NAME, level="ERROR"): result = exporter.export([_make_log_record()]) self.assertEqual(result, LogRecordExportResult.FAILURE) self.assertIsNone(Mocket.last_request()) test_proto_span_exporter.py000066400000000000000000000743561527070070700363560ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp-proto-http/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # pylint: disable=protected-access,too-many-public-methods import gzip import os import threading import time import unittest import zlib from datetime import datetime, timezone from email.utils import format_datetime from unittest.mock import Mock, patch import requests import urllib3.exceptions from mocket import Mocket, Mocketizer, mocketize from mocket.mocks.mockhttp import Entry, Response from opentelemetry.exporter.http.transport._requests import ( RequestsHTTPTransport, ) from opentelemetry.exporter.http.transport._urllib3 import ( Urllib3HTTPTransport, ) from opentelemetry.exporter.otlp.common import http as _http from opentelemetry.exporter.otlp.proto.common.trace_encoder import ( encode_spans, ) from opentelemetry.exporter.otlp.proto.http import Compression from opentelemetry.exporter.otlp.proto.http._common import ( _DEFAULT_TIMEOUT, _build_transport, ) from opentelemetry.exporter.otlp.proto.http.trace_exporter import ( DEFAULT_ENDPOINT, DEFAULT_TRACES_EXPORT_PATH, OTLPSpanExporter, ) from opentelemetry.exporter.otlp.proto.http.version import __version__ from opentelemetry.proto.collector.trace.v1.trace_service_pb2 import ( ExportTraceServiceRequest, ) from opentelemetry.sdk.environment_variables import ( _OTEL_PYTHON_EXPORTER_OTLP_HTTP_TRACES_CREDENTIAL_PROVIDER, OTEL_EXPORTER_OTLP_COMPRESSION, OTEL_EXPORTER_OTLP_ENDPOINT, OTEL_EXPORTER_OTLP_HEADERS, OTEL_EXPORTER_OTLP_TIMEOUT, OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE, OTEL_EXPORTER_OTLP_TRACES_CLIENT_CERTIFICATE, OTEL_EXPORTER_OTLP_TRACES_CLIENT_KEY, OTEL_EXPORTER_OTLP_TRACES_COMPRESSION, OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, OTEL_EXPORTER_OTLP_TRACES_HEADERS, OTEL_EXPORTER_OTLP_TRACES_TIMEOUT, OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED, ) from opentelemetry.sdk.metrics import MeterProvider from opentelemetry.sdk.metrics.export import InMemoryMetricReader from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import ( SimpleSpanProcessor, SpanExportResult, ) from opentelemetry.sdk.trace.export.in_memory_span_exporter import ( InMemorySpanExporter, ) from opentelemetry.test.mock_test_classes import IterEntryPoint from . import _mock_clock _TEST_ENDPOINT = "http://localhost:4318/v1/traces" _LOGGER_NAME = "opentelemetry.exporter.otlp.proto.http.trace_exporter" _USER_AGENT = "OTel-OTLP-Exporter-Python/" + __version__ _BASE_HEADERS = { "content-type": "application/x-protobuf", "user-agent": _USER_AGENT, } def _decode_body(body: bytes) -> ExportTraceServiceRequest: return ExportTraceServiceRequest.FromString(body) class TestOTLPSpanExporter(unittest.TestCase): def setUp(self): env_patcher = patch.dict(os.environ, {}, clear=True) env_patcher.start() self.addCleanup(env_patcher.stop) self._in_memory = InMemorySpanExporter() provider = TracerProvider() provider.add_span_processor(SimpleSpanProcessor(self._in_memory)) self._tracer = provider.get_tracer(__name__) self.metric_reader = InMemoryMetricReader() self.meter_provider = MeterProvider(metric_readers=[self.metric_reader]) def _finished_spans(self): return list(self._in_memory.get_finished_spans()) def _make_span(self, name: str = "test-span"): with self._tracer.start_as_current_span(name): pass return self._finished_spans() @staticmethod def _mocked_shutdown_event() -> Mock: shutdown_event = Mock(spec=threading.Event) shutdown_event.is_set.return_value = False return shutdown_event def test_max_request_size_default(self): exporter = OTLPSpanExporter() self.assertEqual(exporter._max_request_size, 64 * 1024 * 1024) @mocketize def test_oversized_payload_dropped_before_send(self): exporter = OTLPSpanExporter(endpoint=_TEST_ENDPOINT, max_request_size=1) result = exporter.export(self._make_span()) self.assertEqual(result, SpanExportResult.FAILURE) self.assertEqual(len(Mocket.request_list()), 0) @mocketize def test_max_request_size_zero_disables(self): Entry.single_register(Entry.POST, _TEST_ENDPOINT, status=200) exporter = OTLPSpanExporter(endpoint=_TEST_ENDPOINT, max_request_size=0) result = exporter.export(self._make_span()) self.assertEqual(result, SpanExportResult.SUCCESS) @mocketize def test_negative_max_request_size_disables_limit(self): Entry.single_register(Entry.POST, _TEST_ENDPOINT, status=200) exporter = OTLPSpanExporter(endpoint=_TEST_ENDPOINT, max_request_size=-1) result = exporter.export(self._make_span()) self.assertEqual(result, SpanExportResult.SUCCESS) self.assertEqual(len(Mocket.request_list()), 1) @mocketize def test_oversized_payload_measured_before_compression(self): # The limit applies to the uncompressed serialized request. Build a # highly compressible batch whose gzip size is below a limit that the # uncompressed size still exceeds, then assert it is still dropped -- # which can only hold if size is measured before compression. for _ in range(200): with self._tracer.start_as_current_span("test-span"): pass spans = self._finished_spans() uncompressed = encode_spans(spans).SerializePartialToString() compressed = gzip.compress(uncompressed) limit = (len(compressed) + len(uncompressed)) // 2 # Guard the discriminating condition: compressed < limit < uncompressed. self.assertLess(len(compressed), limit) self.assertLess(limit, len(uncompressed)) exporter = OTLPSpanExporter( endpoint=_TEST_ENDPOINT, max_request_size=limit, compression=Compression.Gzip, ) result = exporter.export(spans) self.assertEqual(result, SpanExportResult.FAILURE) self.assertEqual(len(Mocket.request_list()), 0) @patch.dict("os.environ", {OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED: "true"}) @mocketize def test_oversized_payload_records_failure_metric(self): exporter = OTLPSpanExporter( endpoint=_TEST_ENDPOINT, max_request_size=1, meter_provider=self.meter_provider, ) result = exporter.export(self._make_span()) self.assertEqual(result, SpanExportResult.FAILURE) self.assertEqual(len(Mocket.request_list()), 0) metrics_data = self.metric_reader.get_metrics_data() scope_metrics = metrics_data.resource_metrics[0].scope_metrics[0] exported = next(metric for metric in scope_metrics.metrics if metric.name == "otel.sdk.exporter.span.exported") self.assertEqual( exported.data.data_points[0].attributes["error.type"], "RequestPayloadTooLargeError", ) def assert_standard_metric_attrs(self, attributes): self.assertEqual(attributes["otel.component.type"], "otlp_http_span_exporter") self.assertTrue(attributes["otel.component.name"].startswith("otlp_http_span_exporter/")) self.assertEqual(attributes["server.address"], "localhost") self.assertEqual(attributes["server.port"], 4318) def test_default_transport_is_requests(self): exporter = OTLPSpanExporter() self.assertEqual(exporter._endpoint, DEFAULT_ENDPOINT + DEFAULT_TRACES_EXPORT_PATH) self.assertIs(exporter._compression, _http.Compression.NONE) self.assertIsInstance(exporter._client._transport, RequestsHTTPTransport) def test_session_uses_requests_transport(self): session = requests.Session() exporter = OTLPSpanExporter(session=session) self.assertIsInstance(exporter._client._transport, RequestsHTTPTransport) self.assertIs(exporter._client._transport._session, session) @patch.dict( os.environ, { _OTEL_PYTHON_EXPORTER_OTLP_HTTP_TRACES_CREDENTIAL_PROVIDER: "custom_credential", }, ) @patch("opentelemetry.exporter.otlp.proto.http._common.entry_points") def test_credential_provider_uses_requests(self, mock_entry_point): credential = requests.Session() mock_entry_point.configure_mock(return_value=[IterEntryPoint("custom_credential", lambda: credential)]) exporter = OTLPSpanExporter() self.assertIsInstance(exporter._client._transport, RequestsHTTPTransport) self.assertIs(exporter._client._transport._session, credential) @patch.dict( os.environ, { _OTEL_PYTHON_EXPORTER_OTLP_HTTP_TRACES_CREDENTIAL_PROVIDER: "bad_credential", }, ) @patch("opentelemetry.exporter.otlp.proto.http._common.entry_points") def test_entrypoint_wrong_type_raises(self, mock_entry_point): mock_entry_point.configure_mock(return_value=[IterEntryPoint("bad_credential", lambda: 1)]) with self.assertRaises(RuntimeError): OTLPSpanExporter() @patch.dict( os.environ, { _OTEL_PYTHON_EXPORTER_OTLP_HTTP_TRACES_CREDENTIAL_PROVIDER: "missing", }, ) @patch("opentelemetry.exporter.otlp.proto.http._common.entry_points") def test_entrypoint_missing_raises(self, mock_entry_point): mock_entry_point.configure_mock(return_value=[]) with self.assertRaises(RuntimeError): OTLPSpanExporter() def test_compression_dual_enum_acceptance(self): cases = ( (Compression.NoCompression, _http.Compression.NONE), (Compression.Deflate, _http.Compression.DEFLATE), (Compression.Gzip, _http.Compression.GZIP), (_http.Compression.NONE, _http.Compression.NONE), (_http.Compression.DEFLATE, _http.Compression.DEFLATE), (_http.Compression.GZIP, _http.Compression.GZIP), ) for compression, expected in cases: with self.subTest(compression=compression): exporter = OTLPSpanExporter(compression=compression) self.assertIs(exporter._compression, expected) self.assertIs(exporter._client._compression, expected) @mocketize def test_export_single_span(self): Entry.single_register(Entry.POST, _TEST_ENDPOINT, status=200) exporter = OTLPSpanExporter(endpoint=_TEST_ENDPOINT) spans = self._make_span("my-span") transport = exporter._client._transport with patch.object(transport, "request", wraps=transport.request) as mock_request: result = exporter.export(spans) self.assertEqual(result, SpanExportResult.SUCCESS) request = Mocket.last_request() self.assertEqual(request.method, "POST") self.assertEqual(request.path, "/v1/traces") sent_data = mock_request.call_args.kwargs["data"] self.assertEqual(_decode_body(sent_data), encode_spans(spans)) @mocketize def test_export_spans_different_resources(self): Entry.single_register(Entry.POST, _TEST_ENDPOINT, status=200) exporter = OTLPSpanExporter(endpoint=_TEST_ENDPOINT) transport = exporter._client._transport spans = [] for name, host in (("from-a", "a"), ("from-b", "b")): in_memory = InMemorySpanExporter() provider = TracerProvider(resource=Resource({"host": host})) provider.add_span_processor(SimpleSpanProcessor(in_memory)) tracer = provider.get_tracer(__name__) with tracer.start_as_current_span(name): pass spans.extend(in_memory.get_finished_spans()) with patch.object(transport, "request", wraps=transport.request) as mock_request: result = exporter.export(spans) self.assertEqual(result, SpanExportResult.SUCCESS) sent_data = mock_request.call_args.kwargs["data"] body = _decode_body(sent_data) self.assertEqual(body, encode_spans(spans)) self.assertEqual(len(body.resource_spans), 2) @mocketize def test_default_endpoint_and_headers(self): Entry.single_register(Entry.POST, "http://localhost:4318/v1/traces", status=200) exporter = OTLPSpanExporter() result = exporter.export(self._make_span()) self.assertEqual(result, SpanExportResult.SUCCESS) headers = Mocket.last_request().headers self.assertEqual(headers["content-type"], "application/x-protobuf") self.assertTrue(headers["user-agent"].startswith("OTel-OTLP-Exporter-Python/")) def test_custom_endpoint(self): url = "http://custom.example:9999/v1/traces" cases = ( ("constructor", {}, {"endpoint": url}), ( "generic_env", {OTEL_EXPORTER_OTLP_ENDPOINT: "http://custom.example:9999"}, {}, ), ("per_signal_env", {OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: url}, {}), ) for label, env, kwargs in cases: with ( self.subTest(label), patch.dict(os.environ, env, clear=True), Mocketizer(), ): Entry.single_register(Entry.POST, url, status=200) exporter = OTLPSpanExporter(**kwargs) self._in_memory.clear() result = exporter.export(self._make_span()) self.assertEqual(result, SpanExportResult.SUCCESS) def test_custom_headers(self): cases = ( ("constructor", {}, {"headers": {"x-api-key": "secret"}}), ( "generic_env", {OTEL_EXPORTER_OTLP_HEADERS: "x-api-key=secret"}, {}, ), ( "per_signal_env", {OTEL_EXPORTER_OTLP_TRACES_HEADERS: "x-api-key=secret"}, {}, ), ) for label, env, kwargs in cases: with ( self.subTest(label), patch.dict(os.environ, env, clear=True), Mocketizer(), ): Entry.single_register(Entry.POST, _TEST_ENDPOINT, status=200) exporter = OTLPSpanExporter(endpoint=_TEST_ENDPOINT, **kwargs) self._in_memory.clear() exporter.export(self._make_span()) headers = Mocket.last_request().headers self.assertEqual(headers["x-api-key"], "secret") def test_configuration_precedence(self): common_env = { OTEL_EXPORTER_OTLP_ENDPOINT: "http://common.example:4318", OTEL_EXPORTER_OTLP_HEADERS: "common-key=common-value", OTEL_EXPORTER_OTLP_COMPRESSION: "gzip", OTEL_EXPORTER_OTLP_TIMEOUT: "5", } signal_env = { OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: "http://signal.example:4318/v1/traces", OTEL_EXPORTER_OTLP_TRACES_HEADERS: "signal-key=signal-value", OTEL_EXPORTER_OTLP_TRACES_COMPRESSION: "deflate", OTEL_EXPORTER_OTLP_TRACES_TIMEOUT: "7", } explicit_kwargs = { "endpoint": "http://explicit.example:4318/v1/traces", "headers": {"explicit-key": "explicit-value"}, "compression": _http.Compression.NONE, "timeout": 9, } cases = ( ( "defaults", {}, {}, { "endpoint": DEFAULT_ENDPOINT + DEFAULT_TRACES_EXPORT_PATH, "compression": _http.Compression.NONE, "headers": _BASE_HEADERS, "timeout": float(_DEFAULT_TIMEOUT), }, ), ( "common_env", common_env, {}, { "endpoint": "http://common.example:4318/v1/traces", "compression": _http.Compression.GZIP, "headers": { **_BASE_HEADERS, "common-key": "common-value", "Content-Encoding": "gzip", }, "timeout": 5.0, }, ), ( "signal_env_overrides_common", {**common_env, **signal_env}, {}, { "endpoint": "http://signal.example:4318/v1/traces", "compression": _http.Compression.DEFLATE, "headers": { **_BASE_HEADERS, "signal-key": "signal-value", "Content-Encoding": "deflate", }, "timeout": 7.0, }, ), ( "constructor_overrides_all", {**common_env, **signal_env}, explicit_kwargs, { "endpoint": "http://explicit.example:4318/v1/traces", "compression": _http.Compression.NONE, "headers": { **_BASE_HEADERS, "signal-key": "signal-value", "explicit-key": "explicit-value", }, "timeout": 9.0, }, ), ) for label, env, kwargs, expected in cases: with self.subTest(label), patch.dict(os.environ, env, clear=True): exporter = OTLPSpanExporter(**kwargs) self.assertEqual(exporter._endpoint, expected["endpoint"]) self.assertIs(exporter._compression, expected["compression"]) self.assertEqual(exporter._client._headers, expected["headers"]) self.assertEqual(exporter._client._timeout, expected["timeout"]) @mocketize @patch("opentelemetry.exporter.otlp.proto.http.trace_exporter._build_transport") def test_custom_transport(self, mock_build_transport): Entry.single_register(Entry.POST, _TEST_ENDPOINT, status=200) custom_transport = Urllib3HTTPTransport() exporter = OTLPSpanExporter(endpoint=_TEST_ENDPOINT, _transport=custom_transport) mock_build_transport.assert_not_called() self.assertIs(exporter._client._transport, custom_transport) result = exporter.export(self._make_span()) self.assertEqual(result, SpanExportResult.SUCCESS) @mocketize @patch( "opentelemetry.exporter.otlp.proto.http.trace_exporter._build_transport", wraps=_build_transport, ) def test_certificate_args(self, mock_build_transport): Entry.single_register(Entry.POST, _TEST_ENDPOINT, status=200) exporter = OTLPSpanExporter( endpoint=_TEST_ENDPOINT, certificate_file="ca.pem", client_key_file="client-key.pem", client_certificate_file="client-cert.pem", ) mock_build_transport.assert_called_once_with( "ca.pem", "client-key.pem", "client-cert.pem", OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE, OTEL_EXPORTER_OTLP_TRACES_CLIENT_KEY, OTEL_EXPORTER_OTLP_TRACES_CLIENT_CERTIFICATE, session=None, ) transport_session = exporter._client._transport._session self.assertEqual(transport_session.verify, "ca.pem") self.assertEqual(transport_session.cert, ("client-cert.pem", "client-key.pem")) def test_compression_options(self): cases = ( (Compression.NoCompression, None, lambda data: data), (Compression.Gzip, "gzip", gzip.decompress), (Compression.Deflate, "deflate", zlib.decompress), (_http.Compression.NONE, None, lambda data: data), (_http.Compression.GZIP, "gzip", gzip.decompress), (_http.Compression.DEFLATE, "deflate", zlib.decompress), ) for compression, expected_encoding, decompress in cases: with self.subTest(compression=compression), Mocketizer(): Entry.single_register(Entry.POST, _TEST_ENDPOINT, status=200) exporter = OTLPSpanExporter(endpoint=_TEST_ENDPOINT, compression=compression) transport = exporter._client._transport self._in_memory.clear() spans = self._make_span() with patch.object(transport, "request", wraps=transport.request) as mock_request: result = exporter.export(spans) self.assertEqual(result, SpanExportResult.SUCCESS) sent_headers = mock_request.call_args.kwargs["headers"] if expected_encoding is None: self.assertNotIn("Content-Encoding", sent_headers) else: self.assertEqual(sent_headers["Content-Encoding"], expected_encoding) sent_data = mock_request.call_args.kwargs["data"] decompressed = decompress(sent_data) self.assertEqual(_decode_body(decompressed), encode_spans(spans)) def test_export_retryable_status_codes(self): for status_code in (429, 502, 503, 504): with self.subTest(status_code=status_code), Mocketizer(): Entry.register( Entry.POST, _TEST_ENDPOINT, Response(status=status_code), Response(status=200), ) exporter = OTLPSpanExporter(endpoint=_TEST_ENDPOINT, timeout=30.0) shutdown_event = self._mocked_shutdown_event() exporter._client._shutdown_event = shutdown_event self._in_memory.clear() with _mock_clock(shutdown_event): result = exporter.export(self._make_span()) self.assertEqual(result, SpanExportResult.SUCCESS) self.assertEqual(len(Mocket.request_list()), 2) def test_export_non_retryable_status_codes(self): for status_code in (400, 401, 403, 404, 408, 500, 501): with self.subTest(status_code=status_code), Mocketizer(): Entry.single_register(Entry.POST, _TEST_ENDPOINT, status=status_code) exporter = OTLPSpanExporter(endpoint=_TEST_ENDPOINT) self._in_memory.clear() result = exporter.export(self._make_span()) self.assertEqual(result, SpanExportResult.FAILURE) self.assertEqual(len(Mocket.request_list()), 1) @mocketize def test_export_max_retries(self): Entry.register( Entry.POST, _TEST_ENDPOINT, *[Response(status=503)] * 6, ) exporter = OTLPSpanExporter(endpoint=_TEST_ENDPOINT, timeout=1000.0) shutdown_event = self._mocked_shutdown_event() exporter._client._shutdown_event = shutdown_event with _mock_clock(shutdown_event): result = exporter.export(self._make_span()) self.assertEqual(result, SpanExportResult.FAILURE) self.assertEqual(len(Mocket.request_list()), 6) @mocketize def test_export_retry_after_header(self): Entry.register( Entry.POST, _TEST_ENDPOINT, Response(status=429, headers={"Retry-After": "5"}), Response(status=200), ) exporter = OTLPSpanExporter(endpoint=_TEST_ENDPOINT, timeout=60.0) shutdown_event = self._mocked_shutdown_event() shutdown_event.wait.return_value = False exporter._client._shutdown_event = shutdown_event result = exporter.export(self._make_span()) self.assertEqual(result, SpanExportResult.SUCCESS) self.assertEqual(len(Mocket.request_list()), 2) shutdown_event.wait.assert_called_once_with(5.0) @mocketize def test_export_retry_after_header_http_date(self): base = 1_700_000_000.0 retry_at = format_datetime(datetime.fromtimestamp(base + 30, timezone.utc), usegmt=True) Entry.register( Entry.POST, _TEST_ENDPOINT, Response(status=503, headers={"Retry-After": retry_at}), Response(status=200), ) exporter = OTLPSpanExporter(endpoint=_TEST_ENDPOINT, timeout=120.0) shutdown_event = self._mocked_shutdown_event() shutdown_event.wait.return_value = False exporter._client._shutdown_event = shutdown_event with patch( "opentelemetry.exporter.otlp.common.http.time.time", return_value=base, ): result = exporter.export(self._make_span()) self.assertEqual(result, SpanExportResult.SUCCESS) self.assertEqual(len(Mocket.request_list()), 2) shutdown_event.wait.assert_called_once_with(30.0) @mocketize def test_export_connection_error(self): Entry.register( Entry.POST, _TEST_ENDPOINT, urllib3.exceptions.ProtocolError("simulated reset"), Response(status=200), ) exporter = OTLPSpanExporter(endpoint=_TEST_ENDPOINT, timeout=5.0) result = exporter.export(self._make_span()) self.assertEqual(result, SpanExportResult.SUCCESS) self.assertEqual(len(Mocket.request_list()), 2) @mocketize def test_shutdown_interrupts_retry_backoff(self): Entry.register( Entry.POST, _TEST_ENDPOINT, *[Response(status=503)] * 6, ) exporter = OTLPSpanExporter(endpoint=_TEST_ENDPOINT, timeout=1.5) thread = threading.Thread(target=exporter.export, args=(self._make_span(),)) before = time.time() thread.start() time.sleep(0.05) exporter.shutdown() thread.join() after = time.time() self.assertLess(after - before, 0.5) @mocketize def test_exporter_metrics_disabled_by_default(self): Entry.single_register(Entry.POST, _TEST_ENDPOINT, status=200) exporter = OTLPSpanExporter(endpoint=_TEST_ENDPOINT, meter_provider=self.meter_provider) self.assertEqual(exporter.export(self._make_span()), SpanExportResult.SUCCESS) self.assertIsNone(self.metric_reader.get_metrics_data()) @patch.dict("os.environ", {OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED: " true "}) def test_retry_timeout_records_metrics(self): with Mocketizer(): Entry.register( Entry.POST, _TEST_ENDPOINT, Response(status=503), Response(status=503), ) exporter = OTLPSpanExporter( endpoint=_TEST_ENDPOINT, timeout=1.5, meter_provider=self.meter_provider, ) shutdown_event = self._mocked_shutdown_event() exporter._client._shutdown_event = shutdown_event with _mock_clock(shutdown_event): result = exporter.export(self._make_span()) self.assertEqual(result, SpanExportResult.FAILURE) metrics_data = self.metric_reader.get_metrics_data() scope_metrics = metrics_data.resource_metrics[0].scope_metrics[0] metrics = sorted(scope_metrics.metrics, key=lambda m: m.name) self.assertEqual(len(metrics), 3) self.assertEqual(metrics[0].name, "otel.sdk.exporter.operation.duration") self.assert_standard_metric_attrs(metrics[0].data.data_points[0].attributes) self.assertEqual( metrics[0].data.data_points[0].attributes["http.response.status_code"], 503, ) self.assertEqual(metrics[1].name, "otel.sdk.exporter.span.exported") self.assertEqual(metrics[2].name, "otel.sdk.exporter.span.inflight") @patch.dict("os.environ", {OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED: "true"}) @mocketize def test_export_connection_error_records_metrics(self): Entry.register( Entry.POST, _TEST_ENDPOINT, *([urllib3.exceptions.ProtocolError("boom")] * 6), ) exporter = OTLPSpanExporter( endpoint=_TEST_ENDPOINT, timeout=1000.0, meter_provider=self.meter_provider, ) shutdown_event = self._mocked_shutdown_event() exporter._client._shutdown_event = shutdown_event with _mock_clock(shutdown_event): result = exporter.export(self._make_span()) self.assertEqual(result, SpanExportResult.FAILURE) metrics_data = self.metric_reader.get_metrics_data() scope_metrics = metrics_data.resource_metrics[0].scope_metrics[0] metrics = sorted(scope_metrics.metrics, key=lambda m: m.name) self.assertEqual(metrics[0].name, "otel.sdk.exporter.operation.duration") self.assertIn("error.type", metrics[0].data.data_points[0].attributes) self.assertNotIn( "http.response.status_code", metrics[0].data.data_points[0].attributes, ) @mocketize def test_export_after_shutdown(self): exporter = OTLPSpanExporter(endpoint=_TEST_ENDPOINT) exporter.shutdown() with self.assertLogs(_LOGGER_NAME, level="WARNING"): result = exporter.export(self._make_span()) self.assertEqual(result, SpanExportResult.FAILURE) self.assertIsNone(Mocket.last_request()) def test_shutdown_idempotent(self): exporter = OTLPSpanExporter(endpoint=_TEST_ENDPOINT) exporter.shutdown() with self.assertLogs(_LOGGER_NAME, level="WARNING"): exporter.shutdown() # pylint: disable-next=no-self-use def test_shutdown_closes_transport(self): exporter = OTLPSpanExporter(endpoint=_TEST_ENDPOINT) with patch.object(exporter._client._transport, "close") as mock_close: exporter.shutdown() mock_close.assert_called_once() @mocketize def test_force_flush(self): Entry.single_register(Entry.POST, _TEST_ENDPOINT, status=200) exporter = OTLPSpanExporter(endpoint=_TEST_ENDPOINT) self.assertTrue(exporter.force_flush()) exporter.export(self._make_span()) self.assertTrue(exporter.force_flush()) @mocketize @patch( "opentelemetry.exporter.otlp.proto.http.trace_exporter.encode_spans", side_effect=ValueError("boom"), ) def test_export_encoding_failure(self, mock_encode_spans): exporter = OTLPSpanExporter(endpoint=_TEST_ENDPOINT) with self.assertLogs(_LOGGER_NAME, level="ERROR"): result = exporter.export(self._make_span()) self.assertEqual(result, SpanExportResult.FAILURE) self.assertIsNone(Mocket.last_request()) python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp/000077500000000000000000000000001527070070700254115ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp/LICENSE000066400000000000000000000261351527070070700264250ustar00rootroot00000000000000 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. python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp/README.rst000066400000000000000000000022121527070070700270750ustar00rootroot00000000000000OpenTelemetry Collector Exporters ================================= |pypi| .. |pypi| image:: https://badge.fury.io/py/opentelemetry-exporter-otlp.svg :target: https://pypi.org/project/opentelemetry-exporter-otlp/ This library is provided as a convenience to install all supported OpenTelemetry Collector Exporters. Currently it installs: * opentelemetry-exporter-otlp-proto-grpc * opentelemetry-exporter-otlp-proto-http In the future, additional packages will be available: * opentelemetry-exporter-otlp-json-http To avoid unnecessary dependencies, users should install the specific package once they've determined their preferred serialization and protocol method. Installation ------------ :: pip install opentelemetry-exporter-otlp References ---------- * `OpenTelemetry Collector Exporter `_ * `OpenTelemetry Collector `_ * `OpenTelemetry `_ * `OpenTelemetry Protocol Specification `_ python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp/pyproject.toml000066400000000000000000000034401527070070700303260ustar00rootroot00000000000000[build-system] requires = ["hatchling"] build-backend = "hatchling.build" [project] name = "opentelemetry-exporter-otlp" dynamic = ["version"] description = "OpenTelemetry Collector Exporters" 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", "Framework :: OpenTelemetry", "Framework :: OpenTelemetry :: Exporters", "Intended Audience :: Developers", "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-exporter-otlp-proto-grpc == 1.45.2", "opentelemetry-exporter-otlp-proto-http == 1.45.2", ] [project.entry-points.opentelemetry_logs_exporter] otlp = "opentelemetry.exporter.otlp.proto.grpc._log_exporter:OTLPLogExporter" [project.entry-points.opentelemetry_metrics_exporter] otlp = "opentelemetry.exporter.otlp.proto.grpc.metric_exporter:OTLPMetricExporter" [project.entry-points.opentelemetry_traces_exporter] otlp = "opentelemetry.exporter.otlp.proto.grpc.trace_exporter:OTLPSpanExporter" [project.urls] Homepage = "https://github.com/open-telemetry/opentelemetry-python/tree/main/exporter/opentelemetry-exporter-otlp" Repository = "https://github.com/open-telemetry/opentelemetry-python" [tool.hatch.version] path = "src/opentelemetry/exporter/otlp/version/__init__.py" [tool.hatch.build.targets.sdist] include = [ "/src", "/tests", ] [tool.hatch.build.targets.wheel] packages = ["src/opentelemetry"] python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp/src/000077500000000000000000000000001527070070700262005ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp/src/opentelemetry/000077500000000000000000000000001527070070700310745ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/000077500000000000000000000000001527070070700327445ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/000077500000000000000000000000001527070070700337225ustar00rootroot00000000000000py.typed000066400000000000000000000000001527070070700353300ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlpversion/000077500000000000000000000000001527070070700353305ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp__init__.py000066400000000000000000000001441527070070700374400ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 __version__ = "1.45.2" python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp/test-requirements.txt000066400000000000000000000011221527070070700316460ustar00rootroot00000000000000asgiref==3.7.2 iniconfig==2.0.0 packaging==24.0 pluggy==1.6.0 py-cpuinfo==9.0.0 pytest==9.0.3 tomli==2.0.1 typing_extensions==4.12.0 wrapt==1.16.0 -e opentelemetry-api -e tests/opentelemetry-test-utils -e exporter/opentelemetry-exporter-otlp-common -e exporter/opentelemetry-exporter-otlp-proto-common -e exporter/opentelemetry-exporter-otlp-proto-grpc -e exporter/opentelemetry-exporter-otlp-proto-http -e exporter/opentelemetry-exporter-http-transport[urllib3,requests] -e opentelemetry-proto -e opentelemetry-sdk -e opentelemetry-semantic-conventions -e exporter/opentelemetry-exporter-otlp python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp/tests/000077500000000000000000000000001527070070700265535ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp/tests/__init__.py000066400000000000000000000000001527070070700306520ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-otlp/tests/test_otlp.py000066400000000000000000000014541527070070700311460ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 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.exporter.otlp.proto.http.trace_exporter import ( OTLPSpanExporter as HTTPSpanExporter, ) from opentelemetry.test import TestCase class TestOTLPExporters(TestCase): def test_constructors(self): for exporter in [ OTLPSpanExporter, HTTPSpanExporter, OTLPLogExporter, OTLPMetricExporter, ]: with self.assertNotRaises(Exception): exporter() python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-prometheus/000077500000000000000000000000001527070070700266265ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-prometheus/LICENSE000066400000000000000000000261351527070070700276420ustar00rootroot00000000000000 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. python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-prometheus/README.rst000066400000000000000000000015531527070070700303210ustar00rootroot00000000000000OpenTelemetry Prometheus Exporter ================================= |pypi| .. |pypi| image:: https://badge.fury.io/py/opentelemetry-exporter-prometheus.svg :target: https://pypi.org/project/opentelemetry-exporter-prometheus/ This library allows to export metrics data to `Prometheus `_. Installation ------------ :: pip install opentelemetry-exporter-prometheus Limitations ----------- * No multiprocessing support: The Prometheus exporter is not designed to operate in multiprocessing environments (see `#3747 `_). References ---------- * `OpenTelemetry Prometheus Exporter `_ * `Prometheus `_ * `OpenTelemetry Project `_ python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-prometheus/pyproject.toml000066400000000000000000000031251527070070700315430ustar00rootroot00000000000000[build-system] requires = ["hatchling"] build-backend = "hatchling.build" [project] name = "opentelemetry-exporter-prometheus" dynamic = ["version"] description = "Prometheus Metric 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", "Framework :: OpenTelemetry", "Framework :: OpenTelemetry :: Exporters", "Intended Audience :: Developers", "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", # DONOTMERGE: confirm that this will becomes ~= 1.21 in the next release "opentelemetry-sdk ~= 1.45.2", "prometheus_client >= 0.5.0, < 1.0.0", ] [project.entry-points.opentelemetry_metrics_exporter] prometheus = "opentelemetry.exporter.prometheus:_AutoPrometheusMetricReader" [project.urls] Homepage = "https://github.com/open-telemetry/opentelemetry-python/tree/main/exporter/opentelemetry-exporter-prometheus" Repository = "https://github.com/open-telemetry/opentelemetry-python" [tool.hatch.version] path = "src/opentelemetry/exporter/prometheus/version/__init__.py" [tool.hatch.build.targets.sdist] include = [ "/src", "/tests", ] [tool.hatch.build.targets.wheel] packages = ["src/opentelemetry"] python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-prometheus/src/000077500000000000000000000000001527070070700274155ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/000077500000000000000000000000001527070070700323115ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/000077500000000000000000000000001527070070700341615ustar00rootroot00000000000000prometheus/000077500000000000000000000000001527070070700362755ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter__init__.py000066400000000000000000000442221527070070700404120ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 """ This library allows export of metrics data to `Prometheus `_. Usage ----- The **OpenTelemetry Prometheus Exporter** allows export of `OpenTelemetry`_ metrics to `Prometheus`_. .. _Prometheus: https://prometheus.io/ .. _OpenTelemetry: https://github.com/open-telemetry/opentelemetry-python/ .. code:: python from prometheus_client import start_http_server from opentelemetry.exporter.prometheus import PrometheusMetricReader from opentelemetry.metrics import get_meter_provider, set_meter_provider from opentelemetry.sdk.metrics import MeterProvider # Start Prometheus client start_http_server(port=8000, addr="localhost") # Exporter to export metrics to Prometheus prefix = "MyAppPrefix" reader = PrometheusMetricReader(prefix=prefix) # Meter is responsible for creating and recording metrics set_meter_provider(MeterProvider(metric_readers=[reader])) meter = get_meter_provider().get_meter("myapp", "0.1.2") counter = meter.create_counter( "requests", "requests", "number of requests", ) # Labels are used to identify key-values that are associated with a specific # metric that you want to record. These are useful for pre-aggregation and can # be used to store custom dimensions pertaining to a metric labels = {"environment": "staging"} counter.add(25, labels) input("Press any key to exit...") API --- """ from collections import deque from collections.abc import Callable, Iterable, Sequence from itertools import chain from json import dumps from logging import getLogger from os import environ from typing import ( Any, TypeVar, ) from prometheus_client import CollectorRegistry, start_http_server from prometheus_client.core import ( REGISTRY, CounterMetricFamily, GaugeMetricFamily, HistogramMetricFamily, InfoMetricFamily, ) from prometheus_client.core import Metric as PrometheusMetric from opentelemetry.exporter.prometheus._mapping import ( map_unit, sanitize_attribute, sanitize_full_name, ) from opentelemetry.sdk.environment_variables import ( OTEL_EXPORTER_PROMETHEUS_HOST, OTEL_EXPORTER_PROMETHEUS_PORT, ) from opentelemetry.sdk.metrics import ( Counter, ObservableCounter, ObservableGauge, ObservableUpDownCounter, UpDownCounter, ) from opentelemetry.sdk.metrics import Histogram as HistogramInstrument from opentelemetry.sdk.metrics.export import ( AggregationTemporality, CardinalityLimits, DataT, Gauge, Histogram, HistogramDataPoint, Metric, MetricReader, MetricsData, Sum, ) from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.util.instrumentation import InstrumentationScope from opentelemetry.semconv._incubating.attributes.otel_attributes import ( OtelComponentTypeValues, ) from opentelemetry.util.types import AnyValue, Attributes _logger = getLogger(__name__) _TARGET_INFO_NAME = "target" _TARGET_INFO_DESCRIPTION = "Target metadata" _OTEL_SCOPE_NAME_LABEL = "otel_scope_name" _OTEL_SCOPE_VERSION_LABEL = "otel_scope_version" _OTEL_SCOPE_SCHEMA_URL_LABEL = "otel_scope_schema_url" _OTEL_SCOPE_ATTR_PREFIX = "otel_scope_" def _convert_buckets(bucket_counts: Sequence[int], explicit_bounds: Sequence[float]) -> Sequence[tuple[str, int]]: buckets = [] total_count = 0 for upper_bound, count in zip( chain(explicit_bounds, ["+Inf"]), bucket_counts, ): total_count += count buckets.append((f"{upper_bound}", total_count)) return buckets def _should_convert_sum_to_gauge(metric: Metric) -> bool: # The Prometheus compatibility spec requires cumulative non-monotonic Sums # to be exported as Gauges. if not isinstance(metric.data, Sum): return False return not metric.data.is_monotonic and metric.data.aggregation_temporality == AggregationTemporality.CUMULATIVE _FamilyT = TypeVar("_FamilyT", bound=PrometheusMetric) def _get_or_create_family( registry: dict[str, PrometheusMetric], family_id: str, factory: Callable[..., _FamilyT], *, name: str, documentation: str, labels: Sequence[str], unit: str, ) -> _FamilyT: if family_id not in registry: registry[family_id] = factory( name=name, documentation=documentation, labels=labels, unit=unit, ) return registry[family_id] def _populate_counter_family( registry: dict[str, PrometheusMetric], per_metric_family_id: str, metric_name: str, description: str, unit: str, label_keys: Sequence[str], label_rows: Sequence[Sequence[str]], values: Sequence[float], ) -> None: family_id = "|".join([per_metric_family_id, CounterMetricFamily.__name__]) family = _get_or_create_family( registry, family_id, CounterMetricFamily, name=metric_name, documentation=description, labels=label_keys, unit=unit, ) for label_values, value in zip(label_rows, values): family.add_metric(labels=label_values, value=value) def _populate_gauge_family( registry: dict[str, PrometheusMetric], per_metric_family_id: str, metric_name: str, description: str, unit: str, label_keys: Sequence[str], label_rows: Sequence[Sequence[str]], values: Sequence[float], ) -> None: family_id = "|".join([per_metric_family_id, GaugeMetricFamily.__name__]) family = _get_or_create_family( registry, family_id, GaugeMetricFamily, name=metric_name, documentation=description, labels=label_keys, unit=unit, ) for label_values, value in zip(label_rows, values): family.add_metric(labels=label_values, value=value) def _populate_histogram_family( registry: dict[str, PrometheusMetric], per_metric_family_id: str, metric_name: str, description: str, unit: str, label_keys: Sequence[str], label_rows: Sequence[Sequence[str]], values: Sequence[dict[str, Any]], ) -> None: family_id = "|".join([per_metric_family_id, HistogramMetricFamily.__name__]) family = _get_or_create_family( registry, family_id, HistogramMetricFamily, name=metric_name, documentation=description, labels=label_keys, unit=unit, ) for label_values, value in zip(label_rows, values): family.add_metric( labels=label_values, buckets=_convert_buckets(value["bucket_counts"], value["explicit_bounds"]), sum_value=value["sum"], ) class PrometheusMetricReader(MetricReader): """Prometheus metric exporter for OpenTelemetry. Args: disable_target_info: Whether to disable the ``target_info`` metric. prefix: Prefix added to exported Prometheus metric names. scope_info_enabled: Whether to include instrumentation scope labels on exported metrics. Scope labels are exported by default. resource_attribute_filter: Optional callback to select resource attributes that are copied as labels on exported metrics. The callback receives the original resource attribute key. Selected keys are sanitized to valid Prometheus label names. cardinality_limits: A mapping between instrument classes and the maximum number of metric points a metric stream of that class may export per collection cycle. Classes not included in the mapping use a limit of 2000. See :class:`~opentelemetry.sdk.metrics.export.MetricReader` for details. """ def __init__( self, disable_target_info: bool = False, prefix: str = "", scope_info_enabled: bool = True, *, resource_attribute_filter: Callable[[str], bool] | None = None, registry: CollectorRegistry = REGISTRY, cardinality_limits: CardinalityLimits | None = None, ) -> None: super().__init__( preferred_temporality={ Counter: AggregationTemporality.CUMULATIVE, UpDownCounter: AggregationTemporality.CUMULATIVE, HistogramInstrument: AggregationTemporality.CUMULATIVE, ObservableCounter: AggregationTemporality.CUMULATIVE, ObservableUpDownCounter: AggregationTemporality.CUMULATIVE, ObservableGauge: AggregationTemporality.CUMULATIVE, }, cardinality_limits=cardinality_limits, otel_component_type=OtelComponentTypeValues.PROMETHEUS_HTTP_TEXT_METRIC_EXPORTER, ) self._collector = _CustomCollector( disable_target_info=disable_target_info, prefix=prefix, scope_info_enabled=scope_info_enabled, resource_attribute_filter=resource_attribute_filter, ) self._registry = registry self._registry.register(self._collector) self._collector._callback = self.collect self._prefix = prefix def _receive_metrics( self, metrics_data: MetricsData, timeout_millis: float = 10_000, **kwargs, ) -> None: if metrics_data is None: return self._collector.add_metrics_data(metrics_data) def shutdown(self, timeout_millis: float = 30_000, **kwargs) -> None: self._registry.unregister(self._collector) class _CustomCollector: """_CustomCollector represents the Prometheus Collector object See more: https://github.com/prometheus/client_python#custom-collectors """ def __init__( self, disable_target_info: bool = False, prefix: str = "", scope_info_enabled: bool = True, resource_attribute_filter: Callable[[str], bool] | None = None, ): self._callback = None self._metrics_datas: deque[MetricsData] = deque() self._disable_target_info = disable_target_info self._target_info = None self._prefix = prefix self._scope_info_enabled = scope_info_enabled self._resource_attribute_filter = resource_attribute_filter def add_metrics_data(self, metrics_data: MetricsData) -> None: """Add metrics to Prometheus data""" self._metrics_datas.append(metrics_data) def collect(self) -> Iterable[PrometheusMetric]: """Collect fetches the metrics from OpenTelemetry and delivers them as Prometheus Metrics. Collect is invoked every time a ``prometheus.Gatherer`` is run for example when the HTTP endpoint is invoked by Prometheus. """ if self._callback is not None: self._callback() metric_family_id_metric_family = {} if len(self._metrics_datas): if not self._disable_target_info: if self._target_info is None: attributes: Attributes = {} for res in self._metrics_datas[0].resource_metrics: attributes = {**attributes, **res.resource.attributes} self._target_info = self._create_info_metric( _TARGET_INFO_NAME, _TARGET_INFO_DESCRIPTION, attributes ) metric_family_id_metric_family[_TARGET_INFO_NAME] = self._target_info while self._metrics_datas: self._translate_to_prometheus(self._metrics_datas.popleft(), metric_family_id_metric_family) if metric_family_id_metric_family: yield from metric_family_id_metric_family.values() def _translate_to_prometheus( self, metrics_data: MetricsData, metric_family_id_metric_family: dict[str, PrometheusMetric], ): for rm in metrics_data.resource_metrics: resource_attrs = self._build_resource_attrs(rm.resource) for sm in rm.scope_metrics: scope_attrs = self._build_scope_attrs(sm.scope) for metric in sm.metrics: self._translate_metric( metric=metric, scope_attrs=scope_attrs, resource_attrs=resource_attrs, metric_family_id_metric_family=metric_family_id_metric_family, ) def _translate_metric( self, metric: Metric, scope_attrs: dict[str, AnyValue], resource_attrs: dict[str, AnyValue], metric_family_id_metric_family: dict[str, PrometheusMetric], ) -> None: metric_name = self._resolve_metric_name(metric.name) description = metric.description or "" unit = map_unit(metric.unit or "") label_keys, label_rows, values = self._collect_data_points(metric.data, scope_attrs, resource_attrs) per_metric_family_id = "|".join((metric_name, description, unit)) convert_sum_to_gauge = _should_convert_sum_to_gauge(metric) if isinstance(metric.data, Sum) and not convert_sum_to_gauge: _populate_counter_family( registry=metric_family_id_metric_family, per_metric_family_id=per_metric_family_id, metric_name=metric_name, description=description, unit=unit, label_keys=label_keys, label_rows=label_rows, values=values, ) elif isinstance(metric.data, Gauge) or convert_sum_to_gauge: _populate_gauge_family( registry=metric_family_id_metric_family, per_metric_family_id=per_metric_family_id, metric_name=metric_name, description=description, unit=unit, label_keys=label_keys, label_rows=label_rows, values=values, ) elif isinstance(metric.data, Histogram): _populate_histogram_family( registry=metric_family_id_metric_family, per_metric_family_id=per_metric_family_id, metric_name=metric_name, description=description, unit=unit, label_keys=label_keys, label_rows=label_rows, values=values, ) else: _logger.warning("Unsupported metric data. %s", type(metric.data)) def _build_scope_attrs(self, scope: InstrumentationScope) -> dict[str, AnyValue]: if not self._scope_info_enabled: return {} attrs: dict[str, AnyValue] = {} if scope.attributes: for key, value in scope.attributes.items(): attrs[_OTEL_SCOPE_ATTR_PREFIX + key] = value attrs[_OTEL_SCOPE_NAME_LABEL] = scope.name or "" attrs[_OTEL_SCOPE_VERSION_LABEL] = scope.version or "" attrs[_OTEL_SCOPE_SCHEMA_URL_LABEL] = scope.schema_url or "" return attrs def _build_resource_attrs(self, resource: Resource) -> dict[str, AnyValue]: if not self._resource_attribute_filter: return {} return {key: value for key, value in resource.attributes.items() if self._resource_attribute_filter(key)} def _resolve_metric_name(self, name: str) -> str: if self._prefix: name = self._prefix + "_" + name return sanitize_full_name(name) def _collect_data_points( self, metric_data: DataT, scope_attrs: dict[str, AnyValue], resource_attrs: dict[str, AnyValue], ) -> tuple[list[str], list[list[str]], list[float | dict[str, Any]]]: keys: set[str] = set() rows: list[dict[str, str]] = [] values: list[float | dict[str, Any]] = [] for point in metric_data.data_points: labels: dict[str, str] = {} for key, value in chain( resource_attrs.items(), scope_attrs.items(), point.attributes.items(), ): label = sanitize_attribute(key) keys.add(label) labels[label] = self._check_value(value) rows.append(labels) if isinstance(point, HistogramDataPoint): values.append( { "bucket_counts": point.bucket_counts, "explicit_bounds": point.explicit_bounds, "sum": point.sum, } ) else: values.append(point.value) label_keys = sorted(keys) # Backfill missing labels with "" so every data point exposes the # full label set expected by the Prometheus family. label_rows = [[labels.get(k, "") for k in label_keys] for labels in rows] return label_keys, label_rows, values # pylint: disable=no-self-use def _check_value(self, value: float | str | Sequence) -> str: """Check the label value and return is appropriate representation""" if not isinstance(value, str): return dumps(value, default=str) return str(value) def _create_info_metric(self, name: str, description: str, attributes: dict[str, str]) -> InfoMetricFamily: """Create an Info Metric Family with list of attributes""" # sanitize the attribute names according to Prometheus rule attributes = {sanitize_attribute(key): self._check_value(value) for key, value in attributes.items()} info = InfoMetricFamily(name, description, labels=attributes) info.add_metric(labels=list(attributes.keys()), value=attributes) return info class _AutoPrometheusMetricReader(PrometheusMetricReader): """Thin wrapper around PrometheusMetricReader used for the opentelemetry_metrics_exporter entry point. This allows users to use the prometheus exporter with opentelemetry-instrument. It handles starting the Prometheus http server on the the correct port and host. """ def __init__(self) -> None: super().__init__() # Default values are specified in # https://github.com/open-telemetry/opentelemetry-specification/blob/v1.24.0/specification/configuration/sdk-environment-variables.md#prometheus-exporter start_http_server( port=int(environ.get(OTEL_EXPORTER_PROMETHEUS_PORT, "9464")), addr=environ.get(OTEL_EXPORTER_PROMETHEUS_HOST, "localhost"), ) _mapping.py000066400000000000000000000107131527070070700404430ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from re import UNICODE, compile _SANITIZE_NAME_RE = compile(r"[^a-zA-Z0-9:]+", UNICODE) # Same as name, but doesn't allow ":" _SANITIZE_ATTRIBUTE_KEY_RE = compile(r"[^a-zA-Z0-9]+", UNICODE) # UCUM style annotations which are text enclosed in curly braces https://ucum.org/ucum#para-6. # This regex is more permissive than UCUM allows and matches any character within curly braces. _UNIT_ANNOTATION = compile(r"{.*}") # Remaps common UCUM and SI units to prometheus conventions. Copied from # https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/v0.101.0/pkg/translator/prometheus/normalize_name.go#L19 # See specification: # https://github.com/open-telemetry/opentelemetry-specification/blob/v1.33.0/specification/compatibility/prometheus_and_openmetrics.md#metric-metadata-1 _UNIT_MAPPINGS = { # Time "d": "days", "h": "hours", "min": "minutes", "s": "seconds", "ms": "milliseconds", "us": "microseconds", "ns": "nanoseconds", # Bytes "By": "bytes", "KiBy": "kibibytes", "MiBy": "mebibytes", "GiBy": "gibibytes", "TiBy": "tibibytes", "KBy": "kilobytes", "MBy": "megabytes", "GBy": "gigabytes", "TBy": "terabytes", # SI "m": "meters", "V": "volts", "A": "amperes", "J": "joules", "W": "watts", "g": "grams", # Misc "Cel": "celsius", "Hz": "hertz", # TODO(https://github.com/open-telemetry/opentelemetry-specification/issues/4058): the # specification says to normalize "1" to ratio but that may change. Update this mapping or # remove TODO once a decision is made. "1": "", "%": "percent", } # Similar to _UNIT_MAPPINGS, but for "per" unit denominator. # Example: s => per second (singular) # Copied from https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/80317ce83ed87a2dff0c316bb939afbfaa823d5e/pkg/translator/prometheus/normalize_name.go#L58 _PER_UNIT_MAPPINGS = { "s": "second", "m": "minute", "h": "hour", "d": "day", "w": "week", "mo": "month", "y": "year", } def sanitize_full_name(name: str) -> str: """sanitize the given metric name according to Prometheus rule, including sanitizing leading digits https://github.com/open-telemetry/opentelemetry-specification/blob/v1.33.0/specification/compatibility/prometheus_and_openmetrics.md#metric-metadata-1 """ # Leading number special case if name and name[0].isdigit(): name = "_" + name[1:] return _sanitize_name(name) def _sanitize_name(name: str) -> str: """sanitize the given metric name according to Prometheus rule, but does not handle sanitizing a leading digit.""" return _SANITIZE_NAME_RE.sub("_", name) def sanitize_attribute(key: str) -> str: """sanitize the given metric attribute key according to Prometheus rule. https://github.com/open-telemetry/opentelemetry-specification/blob/v1.33.0/specification/compatibility/prometheus_and_openmetrics.md#metric-attributes """ # Leading number special case if key and key[0].isdigit(): key = "_" + key[1:] return _SANITIZE_ATTRIBUTE_KEY_RE.sub("_", key) def map_unit(unit: str) -> str: """Maps unit to common prometheus metric names if available and sanitizes any invalid characters See: - https://github.com/open-telemetry/opentelemetry-specification/blob/v1.33.0/specification/compatibility/prometheus_and_openmetrics.md#metric-metadata-1 - https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/v0.101.0/pkg/translator/prometheus/normalize_name.go#L108 """ # remove curly brace unit annotations unit = _UNIT_ANNOTATION.sub("", unit) if unit in _UNIT_MAPPINGS: return _UNIT_MAPPINGS[unit] # replace "/" with "per" units like m/s -> meters_per_second ratio_unit_subparts = unit.split("/", maxsplit=1) if len(ratio_unit_subparts) == 2: bottom = _sanitize_name(ratio_unit_subparts[1]) if bottom: top = _sanitize_name(ratio_unit_subparts[0]) top = _UNIT_MAPPINGS.get(top, top) bottom = _PER_UNIT_MAPPINGS.get(bottom, bottom) return f"{top}_per_{bottom}" if top else f"per_{bottom}" return ( # since units end up as a metric name suffix, they must be sanitized _sanitize_name(unit) # strip surrounding "_" chars since it will lead to consecutive underscores in the # metric name .strip("_") ) py.typed000066400000000000000000000000001527070070700377620ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheusversion/000077500000000000000000000000001527070070700377625ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus__init__.py000066400000000000000000000001441527070070700420720ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/version# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 __version__ = "0.66b2" python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-prometheus/test-requirements.txt000066400000000000000000000005151527070070700330700ustar00rootroot00000000000000asgiref==3.7.2 iniconfig==2.0.0 packaging==24.0 pluggy==1.6.0 prometheus_client==0.20.0 py-cpuinfo==9.0.0 pytest==9.0.3 tomli==2.0.1 typing_extensions==4.10.0 wrapt==1.16.0 -e opentelemetry-api -e opentelemetry-sdk -e tests/opentelemetry-test-utils -e opentelemetry-semantic-conventions -e exporter/opentelemetry-exporter-prometheus python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-prometheus/tests/000077500000000000000000000000001527070070700277705ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-prometheus/tests/__init__.py000066400000000000000000000001141527070070700320750ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-prometheus/tests/test_entrypoints.py000066400000000000000000000037351527070070700340070ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # pylint: disable=no-self-use import os from unittest import TestCase from unittest.mock import ANY, Mock, patch from opentelemetry.exporter.prometheus import _AutoPrometheusMetricReader from opentelemetry.sdk._configuration import _import_exporters from opentelemetry.sdk.environment_variables import ( OTEL_EXPORTER_PROMETHEUS_HOST, OTEL_EXPORTER_PROMETHEUS_PORT, ) class TestEntrypoints(TestCase): def test_import_exporters(self) -> None: """ Tests that the entrypoint can be loaded and doesn't have a typo in the name """ ( _trace_exporters, metric_exporters, _logs_exporters, ) = _import_exporters( trace_exporter_names=[], metric_exporter_names=["prometheus"], log_exporter_names=[], ) self.assertIs( metric_exporters["prometheus"], _AutoPrometheusMetricReader, ) @patch("opentelemetry.exporter.prometheus.start_http_server") @patch.dict(os.environ) def test_starts_http_server_defaults(self, mock_start_http_server: Mock) -> None: _AutoPrometheusMetricReader() mock_start_http_server.assert_called_once_with(port=9464, addr="localhost") @patch("opentelemetry.exporter.prometheus.start_http_server") @patch.dict(os.environ, {OTEL_EXPORTER_PROMETHEUS_HOST: "1.2.3.4"}) def test_starts_http_server_host_envvar(self, mock_start_http_server: Mock) -> None: _AutoPrometheusMetricReader() mock_start_http_server.assert_called_once_with(port=ANY, addr="1.2.3.4") @patch("opentelemetry.exporter.prometheus.start_http_server") @patch.dict(os.environ, {OTEL_EXPORTER_PROMETHEUS_PORT: "9999"}) def test_starts_http_server_port_envvar(self, mock_start_http_server: Mock) -> None: _AutoPrometheusMetricReader() mock_start_http_server.assert_called_once_with(port=9999, addr=ANY) python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-prometheus/tests/test_mapping.py000066400000000000000000000075061527070070700330440ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from unittest import TestCase from opentelemetry.exporter.prometheus._mapping import ( map_unit, sanitize_attribute, sanitize_full_name, ) class TestMapping(TestCase): def test_sanitize_full_name(self): self.assertEqual(sanitize_full_name("valid_metric_name"), "valid_metric_name") self.assertEqual(sanitize_full_name("VALID_METRIC_NAME"), "VALID_METRIC_NAME") self.assertEqual(sanitize_full_name("_valid_metric_name"), "_valid_metric_name") self.assertEqual(sanitize_full_name("valid:metric_name"), "valid:metric_name") self.assertEqual(sanitize_full_name("valid_1_metric_name"), "valid_1_metric_name") self.assertEqual(sanitize_full_name("1leading_digit"), "_leading_digit") self.assertEqual( sanitize_full_name("consective_____underscores"), "consective_underscores", ) self.assertEqual( sanitize_full_name("1_~#consective_underscores"), "_consective_underscores", ) self.assertEqual( sanitize_full_name("1!2@3#4$5%6^7&8*9(0)_-"), "_2_3_4_5_6_7_8_9_0_", ) self.assertEqual(sanitize_full_name("foo,./?;:[]{}bar"), "foo_:_bar") self.assertEqual(sanitize_full_name("TestString"), "TestString") self.assertEqual(sanitize_full_name("aAbBcC_12_oi"), "aAbBcC_12_oi") def test_sanitize_attribute(self): self.assertEqual(sanitize_attribute("valid_attr_key"), "valid_attr_key") self.assertEqual(sanitize_attribute("VALID_attr_key"), "VALID_attr_key") self.assertEqual(sanitize_attribute("_valid_attr_key"), "_valid_attr_key") self.assertEqual(sanitize_attribute("valid_1_attr_key"), "valid_1_attr_key") self.assertEqual(sanitize_attribute("sanitize:colons"), "sanitize_colons") self.assertEqual(sanitize_attribute("1leading_digit"), "_leading_digit") self.assertEqual( sanitize_attribute("1_~#consective_underscores"), "_consective_underscores", ) self.assertEqual( sanitize_attribute("1!2@3#4$5%6^7&8*9(0)_-"), "_2_3_4_5_6_7_8_9_0_", ) self.assertEqual(sanitize_attribute("foo,./?;:[]{}bar"), "foo_bar") self.assertEqual(sanitize_attribute("TestString"), "TestString") self.assertEqual(sanitize_attribute("aAbBcC_12_oi"), "aAbBcC_12_oi") def test_map_unit(self): # select hardcoded mappings self.assertEqual(map_unit("s"), "seconds") self.assertEqual(map_unit("By"), "bytes") self.assertEqual(map_unit("m"), "meters") # should work with UCUM annotations as well self.assertEqual(map_unit("g{dogfood}"), "grams") # UCUM "default unit" aka unity and equivalent UCUM annotations should be stripped self.assertEqual(map_unit("1"), "") self.assertEqual(map_unit("{}"), "") self.assertEqual(map_unit("{request}"), "") self.assertEqual(map_unit("{{{;@#$}}}"), "") self.assertEqual(map_unit("{unit with space}"), "") # conversion of per units self.assertEqual(map_unit("km/h"), "km_per_hour") self.assertEqual(map_unit("m/s"), "meters_per_second") self.assertEqual(map_unit("{foo}/s"), "per_second") self.assertEqual(map_unit("foo/bar"), "foo_per_bar") self.assertEqual(map_unit("2fer/store"), "2fer_per_store") # should be sanitized to become part of the metric name without surrounding "_" self.assertEqual(map_unit("____"), "") self.assertEqual(map_unit("____"), "") self.assertEqual(map_unit("1:foo#@!"), "1:foo") # should not be interpreted as a per unit since there is no denominator self.assertEqual(map_unit("m/"), "m") self.assertEqual(map_unit("m/{bar}"), "m") test_prometheus_exporter.py000066400000000000000000001170061527070070700354520ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-prometheus/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # pylint: disable=too-many-lines from collections.abc import Callable from textwrap import dedent from unittest import TestCase from unittest.mock import Mock, patch from prometheus_client import CollectorRegistry, generate_latest from prometheus_client.core import ( CounterMetricFamily, GaugeMetricFamily, InfoMetricFamily, ) from opentelemetry.exporter.prometheus import ( _OTEL_SCOPE_ATTR_PREFIX, _OTEL_SCOPE_NAME_LABEL, _OTEL_SCOPE_SCHEMA_URL_LABEL, _OTEL_SCOPE_VERSION_LABEL, PrometheusMetricReader, _CustomCollector, ) from opentelemetry.metrics import NoOpMeterProvider from opentelemetry.sdk.metrics import Counter as CounterInstrument from opentelemetry.sdk.metrics import MeterProvider from opentelemetry.sdk.metrics.export import ( AggregationTemporality, Histogram, HistogramDataPoint, Metric, MetricsData, ResourceMetrics, ScopeMetrics, ) from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.util.instrumentation import InstrumentationScope from opentelemetry.test.metrictestutil import ( _generate_gauge, _generate_histogram, _generate_sum, _generate_unsupported_metric, ) class TestPrometheusMetricReader(TestCase): # pylint: disable=too-many-public-methods def setUp(self): self._mock_registry_register = Mock() self._registry_register_patch = patch( "prometheus_client.core.REGISTRY.register", side_effect=self._mock_registry_register, ) def test_custom_registry(self): with self._registry_register_patch: custom_registry = CollectorRegistry() reader = PrometheusMetricReader(registry=custom_registry) # global REGISTRY should NOT be used self._mock_registry_register.assert_not_called() # check custom_registry was registered self.assertIn( reader._collector, # pylint: disable=protected-access custom_registry._collector_to_names, # pylint: disable=protected-access ) reader.shutdown() def verify_text_format( self, metric: Metric, expect_prometheus_text: str, prefix: str = "", scope: InstrumentationScope | None = None, scope_info_enabled: bool = False, resource: Resource | None = None, resource_attr_filter: Callable[[str], bool] | None = None, ) -> None: metrics_data = MetricsData( resource_metrics=[ ResourceMetrics( resource=resource or Mock(), scope_metrics=[ ScopeMetrics( scope=scope or Mock(), metrics=[metric], schema_url="schema_url", ) ], schema_url="schema_url", ) ] ) collector = _CustomCollector( disable_target_info=True, scope_info_enabled=scope_info_enabled, prefix=prefix, resource_attribute_filter=resource_attr_filter, ) collector.add_metrics_data(metrics_data) result_bytes = generate_latest(collector) result = result_bytes.decode("utf-8") self.assertEqual(result, expect_prometheus_text) # pylint: disable=protected-access def test_constructor(self): """Test the constructor.""" with self._registry_register_patch: _ = PrometheusMetricReader() self.assertTrue(self._mock_registry_register.called) def test_cardinality_limits(self): registry = CollectorRegistry() reader = PrometheusMetricReader( registry=registry, cardinality_limits={CounterInstrument: 3}, ) provider = MeterProvider(metric_readers=[reader]) counter = provider.get_meter("test").create_counter("requests") for index in range(5): counter.add(1, {"k": str(index)}) output = generate_latest(registry).decode() self.assertEqual(output.count("requests_total{"), 4) self.assertIn('otel_metric_overflow="true"', output) def test_shutdown(self): with patch("prometheus_client.core.REGISTRY.unregister") as registry_unregister_patch: exporter = PrometheusMetricReader() exporter.shutdown() self.assertTrue(registry_unregister_patch.called) def test_histogram_to_prometheus(self): metric = Metric( name="test@name", description="foo", unit="s", data=Histogram( data_points=[ HistogramDataPoint( attributes={"histo": 1}, start_time_unix_nano=1641946016139533244, time_unix_nano=1641946016139533244, count=6, sum=579.0, bucket_counts=[1, 3, 2], explicit_bounds=[123.0, 456.0], min=1, max=457, ) ], aggregation_temporality=AggregationTemporality.DELTA, ), ) self.verify_text_format( metric, dedent( """\ # HELP test_name_seconds foo # TYPE test_name_seconds histogram test_name_seconds_bucket{histo="1",le="123.0"} 1.0 test_name_seconds_bucket{histo="1",le="456.0"} 4.0 test_name_seconds_bucket{histo="1",le="+Inf"} 6.0 test_name_seconds_count{histo="1"} 6.0 test_name_seconds_sum{histo="1"} 579.0 """ ), ) def test_monotonic_sum_to_prometheus(self): labels = {"environment@": "staging", "os": "Windows"} metric = _generate_sum( "test@sum_monotonic", 123, attributes=labels, description="testdesc", unit="testunit", ) metrics_data = MetricsData( resource_metrics=[ ResourceMetrics( resource=Mock(), scope_metrics=[ ScopeMetrics( scope=Mock(), metrics=[metric], schema_url="schema_url", ) ], schema_url="schema_url", ) ] ) collector = _CustomCollector(disable_target_info=True, scope_info_enabled=False) collector.add_metrics_data(metrics_data) for prometheus_metric in collector.collect(): self.assertEqual(type(prometheus_metric), CounterMetricFamily) self.assertEqual(prometheus_metric.name, "test_sum_monotonic_testunit") self.assertEqual(prometheus_metric.documentation, "testdesc") self.assertTrue(len(prometheus_metric.samples) == 1) self.assertEqual(prometheus_metric.samples[0].value, 123) self.assertTrue(len(prometheus_metric.samples[0].labels) == 2) self.assertEqual(prometheus_metric.samples[0].labels["environment_"], "staging") self.assertEqual(prometheus_metric.samples[0].labels["os"], "Windows") def test_non_monotonic_sum_to_prometheus(self): labels = {"environment@": "staging", "os": "Windows"} metric = _generate_sum( "test@sum_nonmonotonic", 123, attributes=labels, description="testdesc", unit="testunit", is_monotonic=False, ) metrics_data = MetricsData( resource_metrics=[ ResourceMetrics( resource=Mock(), scope_metrics=[ ScopeMetrics( scope=Mock(), metrics=[metric], schema_url="schema_url", ) ], schema_url="schema_url", ) ] ) collector = _CustomCollector(disable_target_info=True, scope_info_enabled=False) collector.add_metrics_data(metrics_data) for prometheus_metric in collector.collect(): self.assertEqual(type(prometheus_metric), GaugeMetricFamily) self.assertEqual(prometheus_metric.name, "test_sum_nonmonotonic_testunit") self.assertEqual(prometheus_metric.documentation, "testdesc") self.assertTrue(len(prometheus_metric.samples) == 1) self.assertEqual(prometheus_metric.samples[0].value, 123) self.assertTrue(len(prometheus_metric.samples[0].labels) == 2) self.assertEqual(prometheus_metric.samples[0].labels["environment_"], "staging") self.assertEqual(prometheus_metric.samples[0].labels["os"], "Windows") def test_gauge_to_prometheus(self): labels = {"environment@": "dev", "os": "Unix"} metric = _generate_gauge( "test@gauge", 123, attributes=labels, description="testdesc", unit="testunit", ) metrics_data = MetricsData( resource_metrics=[ ResourceMetrics( resource=Mock(), scope_metrics=[ ScopeMetrics( scope=Mock(), metrics=[metric], schema_url="schema_url", ) ], schema_url="schema_url", ) ] ) collector = _CustomCollector(disable_target_info=True, scope_info_enabled=False) collector.add_metrics_data(metrics_data) for prometheus_metric in collector.collect(): self.assertEqual(type(prometheus_metric), GaugeMetricFamily) self.assertEqual(prometheus_metric.name, "test_gauge_testunit") self.assertEqual(prometheus_metric.documentation, "testdesc") self.assertTrue(len(prometheus_metric.samples) == 1) self.assertEqual(prometheus_metric.samples[0].value, 123) self.assertTrue(len(prometheus_metric.samples[0].labels) == 2) self.assertEqual(prometheus_metric.samples[0].labels["environment_"], "dev") self.assertEqual(prometheus_metric.samples[0].labels["os"], "Unix") def test_invalid_metric(self): labels = {"environment": "staging"} record = _generate_unsupported_metric( "tesname", attributes=labels, description="testdesc", unit="testunit", ) collector = _CustomCollector() collector.add_metrics_data([record]) collector.collect() self.assertLogs("opentelemetry.exporter.prometheus", level="WARNING") def test_list_labels(self): labels = {"environment@": ["1", "2", "3"], "os": "Unix"} metric = _generate_gauge( "test@gauge", 123, attributes=labels, description="testdesc", unit="testunit", ) metrics_data = MetricsData( resource_metrics=[ ResourceMetrics( resource=Mock(), scope_metrics=[ ScopeMetrics( scope=Mock(), metrics=[metric], schema_url="schema_url", ) ], schema_url="schema_url", ) ] ) collector = _CustomCollector(disable_target_info=True, scope_info_enabled=False) collector.add_metrics_data(metrics_data) for prometheus_metric in collector.collect(): self.assertEqual(type(prometheus_metric), GaugeMetricFamily) self.assertEqual(prometheus_metric.name, "test_gauge_testunit") self.assertEqual(prometheus_metric.documentation, "testdesc") self.assertTrue(len(prometheus_metric.samples) == 1) self.assertEqual(prometheus_metric.samples[0].value, 123) self.assertTrue(len(prometheus_metric.samples[0].labels) == 2) self.assertEqual( prometheus_metric.samples[0].labels["environment_"], '["1", "2", "3"]', ) self.assertEqual(prometheus_metric.samples[0].labels["os"], "Unix") def test_check_value(self): collector = _CustomCollector() self.assertEqual(collector._check_value(1), "1") self.assertEqual(collector._check_value(1.0), "1.0") self.assertEqual(collector._check_value("a"), "a") self.assertEqual(collector._check_value([1, 2]), "[1, 2]") self.assertEqual(collector._check_value((1, 2)), "[1, 2]") self.assertEqual(collector._check_value(["a", 2]), '["a", 2]') self.assertEqual(collector._check_value(True), "true") self.assertEqual(collector._check_value(False), "false") self.assertEqual(collector._check_value(None), "null") def test_multiple_collection_calls(self): metric_reader = PrometheusMetricReader() provider = MeterProvider(metric_readers=[metric_reader]) # Disable SDK metrics since they are not constant across collections metric_reader._set_meter_provider(NoOpMeterProvider()) meter = provider.get_meter("getting-started", "0.1.2") counter = meter.create_counter("counter") counter.add(1) result_0 = list(metric_reader._collector.collect()) result_1 = list(metric_reader._collector.collect()) result_2 = list(metric_reader._collector.collect()) self.assertEqual(result_0, result_1) self.assertEqual(result_1, result_2) def test_target_info_enabled_by_default(self): metric_reader = PrometheusMetricReader() provider = MeterProvider( metric_readers=[metric_reader], resource=Resource({"os": "Unix", "version": "1.2.3"}), ) meter = provider.get_meter("getting-started", "0.1.2") counter = meter.create_counter("counter") counter.add(1) result = list(metric_reader._collector.collect()) self.assertEqual(len(result), 2) prometheus_metric = result[0] self.assertEqual(type(prometheus_metric), InfoMetricFamily) self.assertEqual(prometheus_metric.name, "target") self.assertEqual(prometheus_metric.documentation, "Target metadata") self.assertTrue(len(prometheus_metric.samples) == 1) self.assertEqual(prometheus_metric.samples[0].value, 1) self.assertTrue(len(prometheus_metric.samples[0].labels) == 2) self.assertEqual(prometheus_metric.samples[0].labels["os"], "Unix") self.assertEqual(prometheus_metric.samples[0].labels["version"], "1.2.3") def test_target_info_disabled(self): metric_reader = PrometheusMetricReader(disable_target_info=True) provider = MeterProvider( metric_readers=[metric_reader], resource=Resource({"os": "Unix", "version": "1.2.3"}), ) meter = provider.get_meter("getting-started", "0.1.2") counter = meter.create_counter("counter") counter.add(1) result = list(metric_reader._collector.collect()) for prometheus_metric in result: self.assertNotEqual(type(prometheus_metric), InfoMetricFamily) self.assertNotEqual(prometheus_metric.name, "target") self.assertNotEqual(prometheus_metric.documentation, "Target metadata") self.assertNotIn("os", prometheus_metric.samples[0].labels) self.assertNotIn("version", prometheus_metric.samples[0].labels) def test_target_info_sanitize(self): metric_reader = PrometheusMetricReader() provider = MeterProvider( metric_readers=[metric_reader], resource=Resource( { "system.os": "Unix", "system.name": "Prometheus Target Sanitize", "histo": 1, "ratio": 0.1, } ), ) meter = provider.get_meter("getting-started", "0.1.2") counter = meter.create_counter("counter") counter.add(1) prometheus_metric = list(metric_reader._collector.collect())[0] self.assertEqual(type(prometheus_metric), InfoMetricFamily) self.assertEqual(prometheus_metric.name, "target") self.assertEqual(prometheus_metric.documentation, "Target metadata") self.assertTrue(len(prometheus_metric.samples) == 1) self.assertEqual(prometheus_metric.samples[0].value, 1) self.assertTrue(len(prometheus_metric.samples[0].labels) == 4) self.assertTrue("system_os" in prometheus_metric.samples[0].labels) self.assertEqual(prometheus_metric.samples[0].labels["system_os"], "Unix") self.assertTrue("system_name" in prometheus_metric.samples[0].labels) self.assertEqual( prometheus_metric.samples[0].labels["system_name"], "Prometheus Target Sanitize", ) self.assertTrue("histo" in prometheus_metric.samples[0].labels) self.assertEqual( prometheus_metric.samples[0].labels["histo"], "1", ) self.assertTrue("ratio" in prometheus_metric.samples[0].labels) self.assertEqual( prometheus_metric.samples[0].labels["ratio"], "0.1", ) def test_label_order_does_not_matter(self): metric_reader = PrometheusMetricReader() provider = MeterProvider(metric_readers=[metric_reader]) meter = provider.get_meter("getting-started", "0.1.2") counter = meter.create_counter("counter") counter.add(1, {"cause": "cause1", "reason": "reason1"}) counter.add(1, {"reason": "reason2", "cause": "cause2"}) prometheus_output = generate_latest().decode() # All labels are mapped correctly self.assertIn('cause="cause1"', prometheus_output) self.assertIn('cause="cause2"', prometheus_output) self.assertIn('reason="reason1"', prometheus_output) self.assertIn('reason="reason2"', prometheus_output) # Only one metric is generated metric_count = prometheus_output.count("# HELP counter_total") self.assertEqual(metric_count, 1) def test_metric_name(self): self.verify_text_format( _generate_sum(name="test_counter", value=1, unit=""), dedent( """\ # HELP test_counter_total foo # TYPE test_counter_total counter test_counter_total{a="1",b="true"} 1.0 """ ), ) self.verify_text_format( _generate_sum(name="test_counter_w_prefix", value=1, unit=""), dedent( """\ # HELP foo_test_counter_w_prefix_total foo # TYPE foo_test_counter_w_prefix_total counter foo_test_counter_w_prefix_total{a="1",b="true"} 1.0 """ ), prefix="foo", ) self.verify_text_format( _generate_sum(name="test_counter_w_invalid_chars_prefix", value=1, unit=""), dedent( """\ # HELP _foo_test_counter_w_invalid_chars_prefix_total foo # TYPE _foo_test_counter_w_invalid_chars_prefix_total counter _foo_test_counter_w_invalid_chars_prefix_total{a="1",b="true"} 1.0 """ ), prefix="#foo", ) self.verify_text_format( _generate_sum(name="1leading_digit", value=1, unit=""), dedent( """\ # HELP _leading_digit_total foo # TYPE _leading_digit_total counter _leading_digit_total{a="1",b="true"} 1.0 """ ), ) self.verify_text_format( _generate_sum(name="!@#counter_invalid_chars", value=1, unit=""), dedent( """\ # HELP _counter_invalid_chars_total foo # TYPE _counter_invalid_chars_total counter _counter_invalid_chars_total{a="1",b="true"} 1.0 """ ), ) def test_metric_name_with_unit(self): self.verify_text_format( _generate_gauge(name="test.metric.no_unit", value=1, unit=""), dedent( """\ # HELP test_metric_no_unit foo # TYPE test_metric_no_unit gauge test_metric_no_unit{a="1",b="true"} 1.0 """ ), ) self.verify_text_format( _generate_gauge(name="test.metric.spaces", value=1, unit=" \t "), dedent( """\ # HELP test_metric_spaces foo # TYPE test_metric_spaces gauge test_metric_spaces{a="1",b="true"} 1.0 """ ), ) # UCUM annotations should be stripped self.verify_text_format( _generate_sum(name="test_counter", value=1, unit="{requests}"), dedent( """\ # HELP test_counter_total foo # TYPE test_counter_total counter test_counter_total{a="1",b="true"} 1.0 """ ), ) # slash converts to "per" self.verify_text_format( _generate_gauge(name="test_gauge", value=1, unit="m/s"), dedent( """\ # HELP test_gauge_meters_per_second foo # TYPE test_gauge_meters_per_second gauge test_gauge_meters_per_second{a="1",b="true"} 1.0 """ ), ) # invalid characters in name are sanitized before being passed to prom client, which # would throw errors self.verify_text_format( _generate_sum(name="test_counter", value=1, unit="%{foo}@?"), dedent( """\ # HELP test_counter_total foo # TYPE test_counter_total counter test_counter_total{a="1",b="true"} 1.0 """ ), ) def test_semconv(self): """Tests that a few select semconv metrics get converted to the expected prometheus text format""" self.verify_text_format( _generate_sum( name="system.filesystem.usage", value=1, is_monotonic=False, unit="By", ), dedent( """\ # HELP system_filesystem_usage_bytes foo # TYPE system_filesystem_usage_bytes gauge system_filesystem_usage_bytes{a="1",b="true"} 1.0 """ ), ) self.verify_text_format( _generate_sum( name="system.network.dropped", value=1, unit="{packets}", ), dedent( """\ # HELP system_network_dropped_total foo # TYPE system_network_dropped_total counter system_network_dropped_total{a="1",b="true"} 1.0 """ ), ) self.verify_text_format( _generate_histogram( name="http.server.request.duration", unit="s", ), dedent( """\ # HELP http_server_request_duration_seconds foo # TYPE http_server_request_duration_seconds histogram http_server_request_duration_seconds_bucket{a="1",b="true",le="123.0"} 1.0 http_server_request_duration_seconds_bucket{a="1",b="true",le="456.0"} 4.0 http_server_request_duration_seconds_bucket{a="1",b="true",le="+Inf"} 6.0 http_server_request_duration_seconds_count{a="1",b="true"} 6.0 http_server_request_duration_seconds_sum{a="1",b="true"} 579.0 """ ), ) self.verify_text_format( _generate_sum( name="http.server.active_requests", value=1, unit="{request}", is_monotonic=False, ), dedent( """\ # HELP http_server_active_requests foo # TYPE http_server_active_requests gauge http_server_active_requests{a="1",b="true"} 1.0 """ ), ) # if the metric name already contains the unit, it shouldn't be added again self.verify_text_format( _generate_sum( name="metric_name_with_myunit", value=1, unit="myunit", ), dedent( """\ # HELP metric_name_with_myunit_total foo # TYPE metric_name_with_myunit_total counter metric_name_with_myunit_total{a="1",b="true"} 1.0 """ ), ) self.verify_text_format( _generate_gauge( name="metric_name_percent", value=1, unit="%", ), dedent( """\ # HELP metric_name_percent foo # TYPE metric_name_percent gauge metric_name_percent{a="1",b="true"} 1.0 """ ), ) def test_scope_info_labels_default(self): scope = InstrumentationScope( name="library.test", version="1.2.3", schema_url="schema_url", ) metric = _generate_gauge( "test_gauge", 42, attributes={"env": "prod"}, description="testdesc", unit="", ) metrics_data = MetricsData( resource_metrics=[ ResourceMetrics( resource=Resource({}), scope_metrics=[ ScopeMetrics( scope=scope, metrics=[metric], schema_url="schema_url", ) ], schema_url="schema_url", ) ] ) collector = _CustomCollector() collector.add_metrics_data(metrics_data) for prometheus_metric in collector.collect(): if isinstance(prometheus_metric, InfoMetricFamily): continue labels = prometheus_metric.samples[0].labels self.assertEqual(labels[_OTEL_SCOPE_NAME_LABEL], "library.test") self.assertEqual(labels[_OTEL_SCOPE_VERSION_LABEL], "1.2.3") self.assertEqual( labels[_OTEL_SCOPE_SCHEMA_URL_LABEL], "schema_url", ) self.assertEqual(labels["env"], "prod") def test_scope_info_labels_text_format(self): scope = InstrumentationScope( name="library.test", version="1.2.3", ) self.verify_text_format( _generate_gauge( "test_gauge", 42, attributes={"env": "prod"}, description="testdesc", unit="", ), dedent( """\ # HELP test_gauge testdesc # TYPE test_gauge gauge test_gauge{env="prod",otel_scope_name="library.test",otel_scope_schema_url="",otel_scope_version="1.2.3"} 42.0 """ ), scope=scope, scope_info_enabled=True, ) def test_scope_attributes_text_format(self): scope = InstrumentationScope( name="library.test", version="1.0", schema_url="schema_url", attributes={"region": "us-east-1"}, ) self.verify_text_format( _generate_gauge( "test_gauge", 7, attributes={}, description="testdesc", unit="", ), dedent( """\ # HELP test_gauge testdesc # TYPE test_gauge gauge test_gauge{otel_scope_name="library.test",otel_scope_region="us-east-1",otel_scope_schema_url="schema_url",otel_scope_version="1.0"} 7.0 """ ), scope=scope, scope_info_enabled=True, ) def test_scope_info_disabled(self): scope = InstrumentationScope(name="library.test", version="1.2.3") metric = _generate_gauge( "test_gauge", 42, attributes={"env": "prod"}, description="testdesc", unit="", ) metrics_data = MetricsData( resource_metrics=[ ResourceMetrics( resource=Mock(), scope_metrics=[ ScopeMetrics( scope=scope, metrics=[metric], schema_url="schema_url", ) ], schema_url="schema_url", ) ] ) collector = _CustomCollector(disable_target_info=True, scope_info_enabled=False) collector.add_metrics_data(metrics_data) for prometheus_metric in collector.collect(): labels = prometheus_metric.samples[0].labels self.assertNotIn(_OTEL_SCOPE_NAME_LABEL, labels) self.assertNotIn(_OTEL_SCOPE_VERSION_LABEL, labels) self.assertNotIn(_OTEL_SCOPE_SCHEMA_URL_LABEL, labels) self.assertNotIn(_OTEL_SCOPE_ATTR_PREFIX + "region", labels) def test_scope_attributes_labels(self): scope = InstrumentationScope( name="library.test", version="1.0", schema_url="schema_url", attributes={ "region": "us-east-1", "name": "should-be-dropped", "version": "should-be-dropped", "schema_url": "should-be-dropped", }, ) metric = _generate_gauge( "test_gauge", 7, attributes={}, description="testdesc", unit="", ) metrics_data = MetricsData( resource_metrics=[ ResourceMetrics( resource=Mock(), scope_metrics=[ ScopeMetrics( scope=scope, metrics=[metric], schema_url="schema_url", ) ], schema_url="schema_url", ) ] ) collector = _CustomCollector(disable_target_info=True) collector.add_metrics_data(metrics_data) for prometheus_metric in collector.collect(): labels = prometheus_metric.samples[0].labels self.assertEqual(labels[_OTEL_SCOPE_ATTR_PREFIX + "region"], "us-east-1") self.assertEqual(labels[_OTEL_SCOPE_NAME_LABEL], "library.test") self.assertEqual(labels[_OTEL_SCOPE_VERSION_LABEL], "1.0") self.assertEqual(labels[_OTEL_SCOPE_SCHEMA_URL_LABEL], "schema_url") def test_resource_attr_filter_default(self): metric = _generate_gauge( "test_gauge", 42, attributes={"env": "prod"}, description="testdesc", unit="", ) metrics_data = MetricsData( resource_metrics=[ ResourceMetrics( resource=Resource({"service.name": "my-service"}), scope_metrics=[ ScopeMetrics( scope=Mock(), metrics=[metric], schema_url="schema_url", ) ], schema_url="schema_url", ) ] ) collector = _CustomCollector(disable_target_info=True, scope_info_enabled=False) collector.add_metrics_data(metrics_data) for prometheus_metric in collector.collect(): labels = prometheus_metric.samples[0].labels self.assertNotIn("service_name", labels) self.assertEqual(labels["env"], "prod") def test_resource_attr_filter_labels(self): metric = _generate_gauge( "test_gauge", 42, attributes={"env": "prod"}, description="testdesc", unit="", ) metrics_data = MetricsData( resource_metrics=[ ResourceMetrics( resource=Resource({"service.name": "my-service", "host.name": "myhost"}), scope_metrics=[ ScopeMetrics( scope=Mock(), metrics=[metric], schema_url="schema_url", ) ], schema_url="schema_url", ) ] ) collector = _CustomCollector( disable_target_info=True, scope_info_enabled=False, resource_attribute_filter=lambda key: key == "service.name", ) collector.add_metrics_data(metrics_data) for prometheus_metric in collector.collect(): labels = prometheus_metric.samples[0].labels self.assertEqual(labels["service_name"], "my-service") self.assertEqual(labels["env"], "prod") self.assertNotIn("host_name", labels) def test_resource_attr_filter_receives_raw_keys(self): metric = _generate_gauge( "test_gauge", 42, attributes={}, description="testdesc", unit="", ) metrics_data = MetricsData( resource_metrics=[ ResourceMetrics( resource=Resource({"service.name": "my-service", "hostname": "myhost"}), scope_metrics=[ ScopeMetrics( scope=Mock(), metrics=[metric], schema_url="schema_url", ) ], schema_url="schema_url", ) ] ) collector = _CustomCollector( disable_target_info=True, scope_info_enabled=False, resource_attribute_filter=lambda key: "." in key, ) collector.add_metrics_data(metrics_data) for prometheus_metric in collector.collect(): labels = prometheus_metric.samples[0].labels self.assertEqual(labels["service_name"], "my-service") self.assertNotIn("hostname", labels) def test_resource_attr_filter_text_format(self): self.verify_text_format( _generate_gauge( "test_gauge", 42, attributes={"env": "prod"}, description="testdesc", unit="", ), dedent( """\ # HELP test_gauge testdesc # TYPE test_gauge gauge test_gauge{env="prod",service_name="my-service"} 42.0 """ ), resource=Resource({"service.name": "my-service"}), resource_attr_filter=lambda key: True, ) def test_resource_attr_filter_with_scope_labels_text_format(self): scope = InstrumentationScope(name="library.test", version="1.0") self.verify_text_format( _generate_gauge( "test_gauge", 42, attributes={}, description="testdesc", unit="", ), dedent( """\ # HELP test_gauge testdesc # TYPE test_gauge gauge test_gauge{otel_scope_name="library.test",otel_scope_schema_url="",otel_scope_version="1.0",service_name="my-service"} 42.0 """ ), scope=scope, scope_info_enabled=True, resource=Resource({"service.name": "my-service"}), resource_attr_filter=lambda key: True, ) def test_resource_attr_filter_point_attr_precedence_text_format(self): self.verify_text_format( _generate_gauge( "test_gauge", 42, attributes={"env": "point-env"}, description="testdesc", unit="", ), dedent( """\ # HELP test_gauge testdesc # TYPE test_gauge gauge test_gauge{env="point-env"} 42.0 """ ), resource=Resource({"env": "resource-env"}), resource_attr_filter=lambda key: True, ) def test_multiple_data_points_with_different_label_sets(self): hist_point_1 = HistogramDataPoint( attributes={"http_target": "/foobar", "net_host_port": 8080}, start_time_unix_nano=1641946016139533244, time_unix_nano=1641946016139533244, count=6, sum=579.0, bucket_counts=[1, 3, 2], explicit_bounds=[123.0, 456.0], min=1, max=457, ) hist_point_2 = HistogramDataPoint( attributes={"net_host_port": 8080}, start_time_unix_nano=1641946016139533245, time_unix_nano=1641946016139533245, count=7, sum=579.0, bucket_counts=[1, 3, 3], explicit_bounds=[123.0, 456.0], min=1, max=457, ) metric = Metric( name="http.server.request.duration", description="test multiple label sets", unit="s", data=Histogram( data_points=[hist_point_1, hist_point_2], aggregation_temporality=AggregationTemporality.CUMULATIVE, ), ) self.verify_text_format( metric, dedent( """\ # HELP http_server_request_duration_seconds test multiple label sets # TYPE http_server_request_duration_seconds histogram http_server_request_duration_seconds_bucket{http_target="/foobar",le="123.0",net_host_port="8080"} 1.0 http_server_request_duration_seconds_bucket{http_target="/foobar",le="456.0",net_host_port="8080"} 4.0 http_server_request_duration_seconds_bucket{http_target="/foobar",le="+Inf",net_host_port="8080"} 6.0 http_server_request_duration_seconds_count{http_target="/foobar",net_host_port="8080"} 6.0 http_server_request_duration_seconds_sum{http_target="/foobar",net_host_port="8080"} 579.0 http_server_request_duration_seconds_bucket{http_target="",le="123.0",net_host_port="8080"} 1.0 http_server_request_duration_seconds_bucket{http_target="",le="456.0",net_host_port="8080"} 4.0 http_server_request_duration_seconds_bucket{http_target="",le="+Inf",net_host_port="8080"} 7.0 http_server_request_duration_seconds_count{http_target="",net_host_port="8080"} 7.0 http_server_request_duration_seconds_sum{http_target="",net_host_port="8080"} 579.0 """ ), ) python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-zipkin-json/000077500000000000000000000000001527070070700267065ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-zipkin-json/CHANGELOG.md000066400000000000000000000000001527070070700305050ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-zipkin-json/LICENSE000066400000000000000000000261351527070070700277220ustar00rootroot00000000000000 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. python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-zipkin-json/README.rst000066400000000000000000000012221527070070700303720ustar00rootroot00000000000000OpenTelemetry Zipkin JSON Exporter ================================== |pypi| .. |pypi| image:: https://badge.fury.io/py/opentelemetry-exporter-zipkin-json.svg :target: https://pypi.org/project/opentelemetry-exporter-zipkin-json/ This library allows export of tracing data to `Zipkin `_ using JSON for serialization. Installation ------------ :: pip install opentelemetry-exporter-zipkin-json References ---------- * `OpenTelemetry Zipkin Exporter `_ * `Zipkin `_ * `OpenTelemetry Project `_ python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-zipkin-json/pyproject.toml000066400000000000000000000030131527070070700316170ustar00rootroot00000000000000[build-system] requires = ["hatchling"] build-backend = "hatchling.build" [project] name = "opentelemetry-exporter-zipkin-json" dynamic = ["version"] description = "Zipkin Span JSON 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 :: 5 - Production/Stable", "Framework :: OpenTelemetry", "Framework :: OpenTelemetry :: Exporters", "Intended Audience :: Developers", "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.3", "opentelemetry-sdk ~= 1.11", "requests ~= 2.7", ] [project.entry-points.opentelemetry_traces_exporter] zipkin_json = "opentelemetry.exporter.zipkin.json:ZipkinExporter" [project.urls] Homepage = "https://github.com/open-telemetry/opentelemetry-python/tree/main/exporter/opentelemetry-exporter-zipkin-json" Repository = "https://github.com/open-telemetry/opentelemetry-python" [tool.hatch.version] path = "src/opentelemetry/exporter/zipkin/json/version/__init__.py" [tool.hatch.build.targets.sdist] include = [ "/src", "/tests", ] [tool.hatch.build.targets.wheel] packages = ["src/opentelemetry"] python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-zipkin-json/src/000077500000000000000000000000001527070070700274755ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/000077500000000000000000000000001527070070700323715ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/000077500000000000000000000000001527070070700342415ustar00rootroot00000000000000zipkin/000077500000000000000000000000001527070070700354665ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporterencoder/000077500000000000000000000000001527070070700371055ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin__init__.py000066400000000000000000000215101527070070700412150ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/encoder# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 """Zipkin Exporter Transport Encoder Base module and abstract class for concrete transport encoders to extend. """ import abc import json import logging from collections.abc import Sequence from enum import Enum from typing import Any, TypeVar from opentelemetry.exporter.zipkin.node_endpoint import NodeEndpoint from opentelemetry.sdk.trace import Event from opentelemetry.trace import ( Span, SpanContext, StatusCode, format_span_id, format_trace_id, ) EncodedLocalEndpointT = TypeVar("EncodedLocalEndpointT") DEFAULT_MAX_TAG_VALUE_LENGTH = 128 NAME_KEY = "otel.library.name" VERSION_KEY = "otel.library.version" _SCOPE_NAME_KEY = "otel.scope.name" _SCOPE_VERSION_KEY = "otel.scope.version" logger = logging.getLogger(__name__) class Protocol(Enum): """Enum of supported protocol formats. Values are human-readable strings so that they can be easily used by the OS environ var OTEL_EXPORTER_ZIPKIN_PROTOCOL (reserved for future usage). """ V1 = "v1" V2 = "v2" # pylint: disable=W0223 class Encoder(abc.ABC): """Base class for encoders that are used by the exporter. Args: max_tag_value_length: maximum length of an exported tag value. Values will be truncated to conform. Since values are serialized to a JSON list string, max_tag_value_length is honored at the element boundary. """ def __init__(self, max_tag_value_length: int = DEFAULT_MAX_TAG_VALUE_LENGTH): self.max_tag_value_length = max_tag_value_length @staticmethod @abc.abstractmethod def content_type() -> str: pass @abc.abstractmethod def serialize(self, spans: Sequence[Span], local_endpoint: NodeEndpoint) -> str: pass @abc.abstractmethod def _encode_span(self, span: Span, encoded_local_endpoint: EncodedLocalEndpointT) -> Any: """ Per spec Zipkin fields that can be absent SHOULD be omitted from the payload when they are empty in the OpenTelemetry Span. https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/trace/sdk_exporters/zipkin.md#request-payload """ @staticmethod @abc.abstractmethod def _encode_local_endpoint( local_endpoint: NodeEndpoint, ) -> EncodedLocalEndpointT: pass @staticmethod def _encode_debug(span_context) -> Any: return span_context.trace_flags.sampled @staticmethod @abc.abstractmethod def _encode_span_id(span_id: int) -> Any: pass @staticmethod @abc.abstractmethod def _encode_trace_id(trace_id: int) -> Any: pass @staticmethod def _get_parent_id(span_context) -> int | None: if isinstance(span_context, Span): parent_id = span_context.parent.span_id elif isinstance(span_context, SpanContext): parent_id = span_context.span_id else: parent_id = None return parent_id def _extract_tags_from_dict(self, tags_dict: dict | None) -> dict[str, str]: tags = {} if not tags_dict: return tags for attribute_key, attribute_value in tags_dict.items(): if isinstance(attribute_value, bool): value = str(attribute_value).lower() elif isinstance(attribute_value, (int, float, str)): value = str(attribute_value) elif isinstance(attribute_value, Sequence): value = self._extract_tag_value_string_from_sequence(attribute_value) if not value: logger.warning("Could not serialize tag %s", attribute_key) continue else: logger.warning("Could not serialize tag %s", attribute_key) continue if self.max_tag_value_length is not None and self.max_tag_value_length > 0: value = value[: self.max_tag_value_length] tags[attribute_key] = value return tags def _extract_tag_value_string_from_sequence(self, sequence: Sequence): if self.max_tag_value_length and self.max_tag_value_length == 1: return None tag_value_elements = [] running_string_length = 2 # accounts for array brackets in output string defined_max_tag_value_length = self.max_tag_value_length is not None and self.max_tag_value_length > 0 for element in sequence: if isinstance(element, bool): tag_value_element = str(element).lower() elif isinstance(element, (int, float, str)): tag_value_element = str(element) elif element is None: tag_value_element = None else: continue if defined_max_tag_value_length: if tag_value_element is None: running_string_length += 4 # null with no quotes else: # + 2 accounts for string quotation marks running_string_length += len(tag_value_element) + 2 if tag_value_elements: # accounts for ',' item separator running_string_length += 1 if running_string_length > self.max_tag_value_length: break tag_value_elements.append(tag_value_element) return json.dumps(tag_value_elements, separators=(",", ":")) def _extract_tags_from_span(self, span: Span) -> dict[str, str]: tags = self._extract_tags_from_dict(span.attributes) if span.resource: tags.update(self._extract_tags_from_dict(span.resource.attributes)) if span.instrumentation_scope is not None: tags.update( { NAME_KEY: span.instrumentation_scope.name, VERSION_KEY: span.instrumentation_scope.version, _SCOPE_NAME_KEY: span.instrumentation_scope.name, _SCOPE_VERSION_KEY: span.instrumentation_scope.version, } ) if span.status.status_code is not StatusCode.UNSET: tags.update({"otel.status_code": span.status.status_code.name}) if span.status.status_code is StatusCode.ERROR: tags.update({"error": span.status.description or ""}) if span.dropped_attributes: tags.update({"otel.dropped_attributes_count": str(span.dropped_attributes)}) if span.dropped_events: tags.update({"otel.dropped_events_count": str(span.dropped_events)}) if span.dropped_links: tags.update({"otel.dropped_links_count": str(span.dropped_links)}) return tags def _extract_annotations_from_events(self, events: list[Event] | None) -> list[dict] | None: if not events: return None annotations = [] for event in events: attrs = {} for key, value in event.attributes.items(): if isinstance(value, str) and self.max_tag_value_length is not None and self.max_tag_value_length > 0: value = value[: self.max_tag_value_length] attrs[key] = value annotations.append( { "timestamp": self._nsec_to_usec_round(event.timestamp), "value": json.dumps({event.name: attrs}, sort_keys=True), } ) return annotations @staticmethod def _nsec_to_usec_round(nsec: int) -> int: """Round nanoseconds to microseconds Timestamp in zipkin spans is int of microseconds. See: https://zipkin.io/pages/instrumenting.html """ return (nsec + 500) // 10**3 class JsonEncoder(Encoder): @staticmethod def content_type(): return "application/json" def serialize(self, spans: Sequence[Span], local_endpoint: NodeEndpoint) -> str: encoded_local_endpoint = self._encode_local_endpoint(local_endpoint) encoded_spans = [] for span in spans: encoded_spans.append(self._encode_span(span, encoded_local_endpoint)) return json.dumps(encoded_spans) @staticmethod def _encode_local_endpoint(local_endpoint: NodeEndpoint) -> dict: encoded_local_endpoint = {"serviceName": local_endpoint.service_name} if local_endpoint.ipv4 is not None: encoded_local_endpoint["ipv4"] = str(local_endpoint.ipv4) if local_endpoint.ipv6 is not None: encoded_local_endpoint["ipv6"] = str(local_endpoint.ipv6) if local_endpoint.port is not None: encoded_local_endpoint["port"] = local_endpoint.port return encoded_local_endpoint @staticmethod def _encode_span_id(span_id: int) -> str: return format_span_id(span_id) @staticmethod def _encode_trace_id(trace_id: int) -> str: return format_trace_id(trace_id) json/000077500000000000000000000000001527070070700364375ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin__init__.py000066400000000000000000000143451527070070700405570ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 """ OpenTelemetry Zipkin JSON Exporter ---------------------------------- This library allows to export tracing data to `Zipkin `_. Usage ----- The **OpenTelemetry Zipkin JSON Exporter** allows exporting of `OpenTelemetry`_ traces to `Zipkin`_. This exporter sends traces to the configured Zipkin collector endpoint using JSON over HTTP and supports multiple versions (v1, v2). .. _Zipkin: https://zipkin.io/ .. _OpenTelemetry: https://github.com/open-telemetry/opentelemetry-python/ .. _Specification: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/sdk-environment-variables.md#zipkin-exporter .. code:: python import requests from opentelemetry import trace from opentelemetry.exporter.zipkin.json import ZipkinExporter from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchSpanProcessor trace.set_tracer_provider(TracerProvider()) tracer = trace.get_tracer(__name__) # create a ZipkinExporter zipkin_exporter = ZipkinExporter( # version=Protocol.V2 # optional: # endpoint="http://localhost:9411/api/v2/spans", # local_node_ipv4="192.168.0.1", # local_node_ipv6="2001:db8::c001", # local_node_port=31313, # max_tag_value_length=256, # timeout=5 (in seconds), # session=requests.Session(), ) # Create a BatchSpanProcessor and add the exporter to it span_processor = BatchSpanProcessor(zipkin_exporter) # add to the tracer trace.get_tracer_provider().add_span_processor(span_processor) with tracer.start_as_current_span("foo"): print("Hello world!") The exporter supports the following environment variable for configuration: - :envvar:`OTEL_EXPORTER_ZIPKIN_ENDPOINT` - :envvar:`OTEL_EXPORTER_ZIPKIN_TIMEOUT` API --- """ import logging from collections.abc import Sequence from os import environ import requests from opentelemetry.exporter.zipkin.encoder import Protocol from opentelemetry.exporter.zipkin.json.v1 import JsonV1Encoder from opentelemetry.exporter.zipkin.json.v2 import JsonV2Encoder from opentelemetry.exporter.zipkin.node_endpoint import IpInput, NodeEndpoint from opentelemetry.sdk.environment_variables import ( OTEL_EXPORTER_ZIPKIN_ENDPOINT, OTEL_EXPORTER_ZIPKIN_TIMEOUT, ) from opentelemetry.sdk.resources import SERVICE_NAME from opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult from opentelemetry.trace import Span DEFAULT_ENDPOINT = "http://localhost:9411/api/v2/spans" REQUESTS_SUCCESS_STATUS_CODES = (200, 202) logger = logging.getLogger(__name__) class ZipkinExporter(SpanExporter): def __init__( self, version: Protocol = Protocol.V2, endpoint: str | None = None, local_node_ipv4: IpInput = None, local_node_ipv6: IpInput = None, local_node_port: int | None = None, max_tag_value_length: int | None = None, timeout: int | None = None, session: requests.Session | None = None, ): """Zipkin exporter. Args: version: The protocol version to be used. endpoint: The endpoint of the Zipkin collector. local_node_ipv4: Primary IPv4 address associated with this connection. local_node_ipv6: Primary IPv6 address associated with this connection. local_node_port: Depending on context, this could be a listen port or the client-side of a socket. max_tag_value_length: Max length string attribute values can have. timeout: Maximum time the Zipkin exporter will wait for each batch export. The default value is 10s. session: Connection session to the Zipkin collector endpoint. The tuple (local_node_ipv4, local_node_ipv6, local_node_port) is used to represent the network context of a node in the service graph. """ self.local_node = NodeEndpoint(local_node_ipv4, local_node_ipv6, local_node_port) if endpoint is None: endpoint = environ.get(OTEL_EXPORTER_ZIPKIN_ENDPOINT) or DEFAULT_ENDPOINT self.endpoint = endpoint if version == Protocol.V1: self.encoder = JsonV1Encoder(max_tag_value_length) elif version == Protocol.V2: self.encoder = JsonV2Encoder(max_tag_value_length) self.session = session or requests.Session() self.session.headers.update({"Content-Type": self.encoder.content_type()}) self._closed = False self.timeout = timeout or int(environ.get(OTEL_EXPORTER_ZIPKIN_TIMEOUT, 10)) def export(self, spans: Sequence[Span]) -> SpanExportResult: # After the call to Shutdown subsequent calls to Export are # not allowed and should return a Failure result if self._closed: logger.warning("Exporter already shutdown, ignoring batch") return SpanExportResult.FAILURE # Populate service_name from first span # We restrict any SpanProcessor to be only associated with a single # TracerProvider, so it is safe to assume that all Spans in a single # batch all originate from one TracerProvider (and in turn have all # the same service.name) if spans: service_name = spans[0].resource.attributes.get(SERVICE_NAME) if service_name: self.local_node.service_name = service_name result = self.session.post( url=self.endpoint, data=self.encoder.serialize(spans, self.local_node), timeout=self.timeout, ) if result.status_code not in REQUESTS_SUCCESS_STATUS_CODES: logger.error( "Traces cannot be uploaded; status code: %s, message %s", result.status_code, result.text, ) return SpanExportResult.FAILURE return SpanExportResult.SUCCESS def shutdown(self) -> None: if self._closed: logger.warning("Exporter already shutdown, ignoring call") return self.session.close() self._closed = True def force_flush(self, timeout_millis: int = 30000) -> bool: return True v1/000077500000000000000000000000001527070070700367655ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json__init__.py000066400000000000000000000044231527070070700411010ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/v1# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 """Zipkin Export Encoders for JSON formats""" from opentelemetry.exporter.zipkin.encoder import Encoder, JsonEncoder from opentelemetry.trace import Span # pylint: disable=W0223 class V1Encoder(Encoder): def _extract_binary_annotations(self, span: Span, encoded_local_endpoint: dict) -> list[dict]: binary_annotations = [] for tag_key, tag_value in self._extract_tags_from_span(span).items(): if isinstance(tag_value, str) and self.max_tag_value_length > 0: tag_value = tag_value[: self.max_tag_value_length] binary_annotations.append( { "key": tag_key, "value": tag_value, "endpoint": encoded_local_endpoint, } ) return binary_annotations class JsonV1Encoder(JsonEncoder, V1Encoder): """Zipkin Export Encoder for JSON v1 API API spec: https://github.com/openzipkin/zipkin-api/blob/master/zipkin-api.yaml """ def _encode_span(self, span: Span, encoded_local_endpoint: dict) -> dict: context = span.get_span_context() encoded_span = { "traceId": self._encode_trace_id(context.trace_id), "id": self._encode_span_id(context.span_id), "name": span.name, "timestamp": self._nsec_to_usec_round(span.start_time), "duration": self._nsec_to_usec_round(span.end_time - span.start_time), } encoded_annotations = self._extract_annotations_from_events(span.events) if encoded_annotations is not None: for annotation in encoded_annotations: annotation["endpoint"] = encoded_local_endpoint encoded_span["annotations"] = encoded_annotations binary_annotations = self._extract_binary_annotations(span, encoded_local_endpoint) if binary_annotations: encoded_span["binaryAnnotations"] = binary_annotations debug = self._encode_debug(context) if debug: encoded_span["debug"] = debug parent_id = self._get_parent_id(span.parent) if parent_id is not None: encoded_span["parentId"] = self._encode_span_id(parent_id) return encoded_span v2/000077500000000000000000000000001527070070700367665ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json__init__.py000066400000000000000000000033051527070070700411000ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/v2# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 """Zipkin Export Encoders for JSON formats""" from opentelemetry.exporter.zipkin.encoder import JsonEncoder from opentelemetry.trace import Span, SpanKind class JsonV2Encoder(JsonEncoder): """Zipkin Export Encoder for JSON v2 API API spec: https://github.com/openzipkin/zipkin-api/blob/master/zipkin2-api.yaml """ SPAN_KIND_MAP = { SpanKind.INTERNAL: None, SpanKind.SERVER: "SERVER", SpanKind.CLIENT: "CLIENT", SpanKind.PRODUCER: "PRODUCER", SpanKind.CONSUMER: "CONSUMER", } def _encode_span(self, span: Span, encoded_local_endpoint: dict) -> dict: context = span.get_span_context() encoded_span = { "traceId": self._encode_trace_id(context.trace_id), "id": self._encode_span_id(context.span_id), "name": span.name, "timestamp": self._nsec_to_usec_round(span.start_time), "duration": self._nsec_to_usec_round(span.end_time - span.start_time), "localEndpoint": encoded_local_endpoint, "kind": self.SPAN_KIND_MAP[span.kind], } tags = self._extract_tags_from_span(span) if tags: encoded_span["tags"] = tags annotations = self._extract_annotations_from_events(span.events) if annotations: encoded_span["annotations"] = annotations debug = self._encode_debug(context) if debug: encoded_span["debug"] = debug parent_id = self._get_parent_id(span.parent) if parent_id is not None: encoded_span["parentId"] = self._encode_span_id(parent_id) return encoded_span version/000077500000000000000000000000001527070070700401245ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json__init__.py000066400000000000000000000001441527070070700422340ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 __version__ = "1.45.2" node_endpoint.py000066400000000000000000000037771527070070700407030ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 """Zipkin Exporter Endpoints""" import ipaddress from opentelemetry import trace from opentelemetry.sdk.resources import SERVICE_NAME, Resource IpInput = str | int | None class NodeEndpoint: """The network context of a node in the service graph. Args: ipv4: Primary IPv4 address associated with this connection. ipv6: Primary IPv6 address associated with this connection. port: Depending on context, this could be a listen port or the client-side of a socket. None if unknown. """ def __init__( self, ipv4: IpInput = None, ipv6: IpInput = None, port: int | None = None, ): self.ipv4 = ipv4 self.ipv6 = ipv6 self.port = port tracer_provider = trace.get_tracer_provider() if hasattr(tracer_provider, "resource"): resource = tracer_provider.resource else: resource = Resource.create() self.service_name = resource.attributes[SERVICE_NAME] @property def ipv4(self) -> ipaddress.IPv4Address | None: return self._ipv4 @ipv4.setter def ipv4(self, address: IpInput) -> None: if address is None: self._ipv4 = None else: ipv4_address = ipaddress.ip_address(address) if not isinstance(ipv4_address, ipaddress.IPv4Address): raise ValueError(f"{address!r} does not appear to be an IPv4 address") self._ipv4 = ipv4_address @property def ipv6(self) -> ipaddress.IPv6Address | None: return self._ipv6 @ipv6.setter def ipv6(self, address: IpInput) -> None: if address is None: self._ipv6 = None else: ipv6_address = ipaddress.ip_address(address) if not isinstance(ipv6_address, ipaddress.IPv6Address): raise ValueError(f"{address!r} does not appear to be an IPv6 address") self._ipv6 = ipv6_address py.typed000066400000000000000000000000001527070070700371530ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkinpython-opentelemetry-1.45.2/exporter/opentelemetry-exporter-zipkin-json/test-requirements.txt000066400000000000000000000006151527070070700331510ustar00rootroot00000000000000asgiref==3.7.2 certifi==2024.12.14 charset-normalizer==3.3.2 idna==3.15 iniconfig==2.0.0 packaging==24.0 pluggy==1.6.0 py-cpuinfo==9.0.0 pytest==9.0.3 requests==2.33.0 tomli==2.0.1 typing_extensions==4.10.0 urllib3==2.7.0 wrapt==1.16.0 -e opentelemetry-api -e opentelemetry-sdk -e opentelemetry-semantic-conventions -e tests/opentelemetry-test-utils -e exporter/opentelemetry-exporter-zipkin-json python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-zipkin-json/tests/000077500000000000000000000000001527070070700300505ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-zipkin-json/tests/__init__.py000066400000000000000000000001141527070070700321550ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-zipkin-json/tests/encoder/000077500000000000000000000000001527070070700314675ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-zipkin-json/tests/encoder/__init__.py000066400000000000000000000000001527070070700335660ustar00rootroot00000000000000common_tests.py000066400000000000000000000432201527070070700344750ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-zipkin-json/tests/encoder# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import abc from opentelemetry import trace as trace_api from opentelemetry.exporter.zipkin.encoder import ( DEFAULT_MAX_TAG_VALUE_LENGTH, Encoder, ) from opentelemetry.exporter.zipkin.node_endpoint import NodeEndpoint from opentelemetry.sdk import trace from opentelemetry.sdk.resources import SERVICE_NAME, Resource from opentelemetry.sdk.util.instrumentation import InstrumentationScope from opentelemetry.test.test_base import TestBase from opentelemetry.trace import TraceFlags from opentelemetry.trace.status import Status, StatusCode TEST_SERVICE_NAME = "test_service" # pylint: disable=protected-access,too-many-public-methods class CommonEncoderTestCases: class CommonEncoderTest(TestBase): @staticmethod def create_tracer_provider(**kwargs): return TestBase.create_tracer_provider(resource=Resource({SERVICE_NAME: TEST_SERVICE_NAME}), **kwargs) @staticmethod @abc.abstractmethod def get_encoder(*args, **kwargs) -> Encoder: pass @classmethod def get_encoder_default(cls) -> Encoder: return cls.get_encoder() @abc.abstractmethod def test_encode_trace_id(self): pass @abc.abstractmethod def test_encode_span_id(self): pass @abc.abstractmethod def test_encode_local_endpoint_default(self): pass @abc.abstractmethod def test_encode_local_endpoint_explicits(self): pass @abc.abstractmethod def _test_encode_max_tag_length(self, max_tag_value_length: int): pass def test_encode_max_tag_length_2(self): self._test_encode_max_tag_length(2) def test_encode_max_tag_length_5(self): self._test_encode_max_tag_length(5) def test_encode_max_tag_length_9(self): self._test_encode_max_tag_length(9) def test_encode_max_tag_length_10(self): self._test_encode_max_tag_length(10) def test_encode_max_tag_length_11(self): self._test_encode_max_tag_length(11) def test_encode_max_tag_length_128(self): self._test_encode_max_tag_length(128) def test_constructor_default(self): encoder = self.get_encoder() self.assertEqual(DEFAULT_MAX_TAG_VALUE_LENGTH, encoder.max_tag_value_length) def test_constructor_max_tag_value_length(self): max_tag_value_length = 123456 encoder = self.get_encoder(max_tag_value_length) self.assertEqual(max_tag_value_length, encoder.max_tag_value_length) def test_nsec_to_usec_round(self): base_time_nsec = 683647322 * 10**9 for nsec in ( base_time_nsec, base_time_nsec + 150 * 10**6, base_time_nsec + 300 * 10**6, base_time_nsec + 400 * 10**6, ): self.assertEqual( (nsec + 500) // 10**3, self.get_encoder_default()._nsec_to_usec_round(nsec), ) def test_encode_debug(self): self.assertFalse( self.get_encoder_default()._encode_debug( trace_api.SpanContext( trace_id=0x000000000000000000000000DEADBEEF, span_id=0x00000000DEADBEF0, is_remote=False, trace_flags=TraceFlags(TraceFlags.DEFAULT), ) ) ) self.assertTrue( self.get_encoder_default()._encode_debug( trace_api.SpanContext( trace_id=0x000000000000000000000000DEADBEEF, span_id=0x00000000DEADBEF0, is_remote=False, trace_flags=TraceFlags(TraceFlags.SAMPLED), ) ) ) def test_get_parent_id_from_span(self): parent_id = 0x00000000DEADBEF0 self.assertEqual( parent_id, self.get_encoder_default()._get_parent_id( trace._Span( name="test-span", context=trace_api.SpanContext( 0x000000000000000000000000DEADBEEF, 0x04BF92DEEFC58C92, is_remote=False, ), parent=trace_api.SpanContext( 0x0000000000000000000000AADEADBEEF, parent_id, is_remote=False, ), ) ), ) def test_get_parent_id_from_span_context(self): parent_id = 0x00000000DEADBEF0 self.assertEqual( parent_id, self.get_encoder_default()._get_parent_id( trace_api.SpanContext( trace_id=0x000000000000000000000000DEADBEEF, span_id=parent_id, is_remote=False, ), ), ) @staticmethod def get_data_for_max_tag_length_test( max_tag_length: int, ) -> (trace._Span, dict): start_time = 683647322 * 10**9 # in ns duration = 50 * 10**6 end_time = start_time + duration span = trace._Span( name=TEST_SERVICE_NAME, context=trace_api.SpanContext( 0x0E0C63257DE34C926F9EFCD03927272E, 0x04BF92DEEFC58C92, is_remote=False, trace_flags=TraceFlags(TraceFlags.SAMPLED), ), resource=trace.Resource({}), ) span.start(start_time=start_time) span.set_attribute("string1", "v" * 500) span.set_attribute("string2", "v" * 50) span.set_attribute("list1", ["a"] * 25) span.set_attribute("list2", ["a"] * 10) span.set_attribute("list3", [2] * 25) span.set_attribute("list4", [2] * 10) span.set_attribute("list5", [True] * 25) span.set_attribute("list6", [True] * 10) span.set_attribute("tuple1", ("a",) * 25) span.set_attribute("tuple2", ("a",) * 10) span.set_attribute("tuple3", (2,) * 25) span.set_attribute("tuple4", (2,) * 10) span.set_attribute("tuple5", (True,) * 25) span.set_attribute("tuple6", (True,) * 10) span.set_attribute("range1", range(25)) span.set_attribute("range2", range(10)) span.set_attribute("empty_list", []) span.set_attribute("none_list", ["hello", None, "world"]) span.end(end_time=end_time) expected_outputs = { 2: { "string1": "vv", "string2": "vv", "list1": "[]", "list2": "[]", "list3": "[]", "list4": "[]", "list5": "[]", "list6": "[]", "tuple1": "[]", "tuple2": "[]", "tuple3": "[]", "tuple4": "[]", "tuple5": "[]", "tuple6": "[]", "range1": "[]", "range2": "[]", "empty_list": "[]", "none_list": "[]", }, 5: { "string1": "vvvvv", "string2": "vvvvv", "list1": '["a"]', "list2": '["a"]', "list3": '["2"]', "list4": '["2"]', "list5": "[]", "list6": "[]", "tuple1": '["a"]', "tuple2": '["a"]', "tuple3": '["2"]', "tuple4": '["2"]', "tuple5": "[]", "tuple6": "[]", "range1": '["0"]', "range2": '["0"]', "empty_list": "[]", "none_list": "[]", }, 9: { "string1": "vvvvvvvvv", "string2": "vvvvvvvvv", "list1": '["a","a"]', "list2": '["a","a"]', "list3": '["2","2"]', "list4": '["2","2"]', "list5": '["true"]', "list6": '["true"]', "tuple1": '["a","a"]', "tuple2": '["a","a"]', "tuple3": '["2","2"]', "tuple4": '["2","2"]', "tuple5": '["true"]', "tuple6": '["true"]', "range1": '["0","1"]', "range2": '["0","1"]', "empty_list": "[]", "none_list": '["hello"]', }, 10: { "string1": "vvvvvvvvvv", "string2": "vvvvvvvvvv", "list1": '["a","a"]', "list2": '["a","a"]', "list3": '["2","2"]', "list4": '["2","2"]', "list5": '["true"]', "list6": '["true"]', "tuple1": '["a","a"]', "tuple2": '["a","a"]', "tuple3": '["2","2"]', "tuple4": '["2","2"]', "tuple5": '["true"]', "tuple6": '["true"]', "range1": '["0","1"]', "range2": '["0","1"]', "empty_list": "[]", "none_list": '["hello"]', }, 11: { "string1": "vvvvvvvvvvv", "string2": "vvvvvvvvvvv", "list1": '["a","a"]', "list2": '["a","a"]', "list3": '["2","2"]', "list4": '["2","2"]', "list5": '["true"]', "list6": '["true"]', "tuple1": '["a","a"]', "tuple2": '["a","a"]', "tuple3": '["2","2"]', "tuple4": '["2","2"]', "tuple5": '["true"]', "tuple6": '["true"]', "range1": '["0","1"]', "range2": '["0","1"]', "empty_list": "[]", "none_list": '["hello"]', }, 128: { "string1": "v" * 128, "string2": "v" * 50, "list1": '["a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a"]', "list2": '["a","a","a","a","a","a","a","a","a","a"]', "list3": '["2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2"]', "list4": '["2","2","2","2","2","2","2","2","2","2"]', "list5": '["true","true","true","true","true","true","true","true","true","true","true","true","true","true","true","true","true","true"]', "list6": '["true","true","true","true","true","true","true","true","true","true"]', "tuple1": '["a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a"]', "tuple2": '["a","a","a","a","a","a","a","a","a","a"]', "tuple3": '["2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2"]', "tuple4": '["2","2","2","2","2","2","2","2","2","2"]', "tuple5": '["true","true","true","true","true","true","true","true","true","true","true","true","true","true","true","true","true","true"]', "tuple6": '["true","true","true","true","true","true","true","true","true","true"]', "range1": '["0","1","2","3","4","5","6","7","8","9","10","11","12","13","14","15","16","17","18","19","20","21","22","23","24"]', "range2": '["0","1","2","3","4","5","6","7","8","9"]', "empty_list": "[]", "none_list": '["hello",null,"world"]', }, } return span, expected_outputs[max_tag_length] @staticmethod def get_exhaustive_otel_span_list() -> list[trace._Span]: trace_id = 0x6E0C63257DE34C926F9EFCD03927272E base_time = 683647322 * 10**9 # in ns start_times = ( base_time, base_time + 150 * 10**6, base_time + 300 * 10**6, base_time + 400 * 10**6, ) end_times = ( start_times[0] + (50 * 10**6), start_times[1] + (100 * 10**6), start_times[2] + (200 * 10**6), start_times[3] + (300 * 10**6), ) parent_span_context = trace_api.SpanContext(trace_id, 0x1111111111111111, is_remote=False) other_context = trace_api.SpanContext(trace_id, 0x2222222222222222, is_remote=False) span1 = trace._Span( name="test-span-1", context=trace_api.SpanContext( trace_id, 0x34BF92DEEFC58C92, is_remote=False, trace_flags=TraceFlags(TraceFlags.SAMPLED), ), parent=parent_span_context, events=( trace.Event( name="event0", timestamp=base_time + 50 * 10**6, attributes={ "annotation_bool": True, "annotation_string": "annotation_test", "key_float": 0.3, }, ), ), links=(trace_api.Link(context=other_context, attributes={"key_bool": True}),), resource=trace.Resource({}), ) span1.start(start_time=start_times[0]) span1.set_attribute("key_bool", False) span1.set_attribute("key_string", "hello_world") span1.set_attribute("key_float", 111.22) span1.set_status(Status(StatusCode.OK)) span1.end(end_time=end_times[0]) span2 = trace._Span( name="test-span-2", context=parent_span_context, parent=None, resource=trace.Resource(attributes={"key_resource": "some_resource"}), ) span2.start(start_time=start_times[1]) span2.set_status(Status(StatusCode.ERROR, "Example description")) span2.end(end_time=end_times[1]) span3 = trace._Span( name="test-span-3", context=other_context, parent=None, resource=trace.Resource(attributes={"key_resource": "some_resource"}), ) span3.start(start_time=start_times[2]) span3.set_attribute("key_string", "hello_world") span3.end(end_time=end_times[2]) span4 = trace._Span( name="test-span-3", context=other_context, parent=None, resource=trace.Resource({}), instrumentation_scope=InstrumentationScope(name="name", version="version"), ) span4.start(start_time=start_times[3]) span4.end(end_time=end_times[3]) return [span1, span2, span3, span4] # pylint: disable=W0223 class CommonJsonEncoderTest(CommonEncoderTest, abc.ABC): def test_encode_trace_id(self): for trace_id in (1, 1024, 2**32, 2**64, 2**65): self.assertEqual( format(trace_id, "032x"), self.get_encoder_default()._encode_trace_id(trace_id), ) def test_encode_span_id(self): for span_id in (1, 1024, 2**8, 2**16, 2**32, 2**64): self.assertEqual( format(span_id, "016x"), self.get_encoder_default()._encode_span_id(span_id), ) def test_encode_local_endpoint_default(self): self.assertEqual( self.get_encoder_default()._encode_local_endpoint(NodeEndpoint()), {"serviceName": TEST_SERVICE_NAME}, ) def test_encode_local_endpoint_explicits(self): ipv4 = "192.168.0.1" ipv6 = "2001:db8::c001" port = 414120 self.assertEqual( self.get_encoder_default()._encode_local_endpoint(NodeEndpoint(ipv4, ipv6, port)), { "serviceName": TEST_SERVICE_NAME, "ipv4": ipv4, "ipv6": ipv6, "port": port, }, ) @staticmethod def pop_and_sort(source_list, source_index, sort_key): """ Convenience method that will pop a specified index from a list, sort it by a given key and then return it. """ popped_item = source_list.pop(source_index, None) if popped_item is not None: popped_item = sorted(popped_item, key=lambda x: x[sort_key]) return popped_item def assert_equal_encoded_spans(self, expected_spans, actual_spans): self.assertEqual(expected_spans, actual_spans) test_v1_json.py000066400000000000000000000223671527070070700344120ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-zipkin-json/tests/encoder# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import json from opentelemetry import trace as trace_api from opentelemetry.exporter.zipkin.encoder import ( _SCOPE_NAME_KEY, _SCOPE_VERSION_KEY, NAME_KEY, VERSION_KEY, ) from opentelemetry.exporter.zipkin.json.v1 import JsonV1Encoder from opentelemetry.exporter.zipkin.node_endpoint import NodeEndpoint from opentelemetry.sdk import trace from opentelemetry.test.spantestutil import ( get_span_with_dropped_attributes_events_links, ) from opentelemetry.trace import TraceFlags, format_span_id, format_trace_id from .common_tests import ( # pylint: disable=import-error TEST_SERVICE_NAME, CommonEncoderTestCases, ) # pylint: disable=protected-access class TestV1JsonEncoder(CommonEncoderTestCases.CommonJsonEncoderTest): @staticmethod def get_encoder(*args, **kwargs) -> JsonV1Encoder: return JsonV1Encoder(*args, **kwargs) def test_encode(self): local_endpoint = {"serviceName": TEST_SERVICE_NAME} otel_spans = self.get_exhaustive_otel_span_list() trace_id = JsonV1Encoder._encode_trace_id(otel_spans[0].context.trace_id) expected_output = [ { "traceId": trace_id, "id": JsonV1Encoder._encode_span_id(otel_spans[0].context.span_id), "name": otel_spans[0].name, "timestamp": otel_spans[0].start_time // 10**3, "duration": (otel_spans[0].end_time // 10**3) - (otel_spans[0].start_time // 10**3), "annotations": [ { "timestamp": otel_spans[0].events[0].timestamp // 10**3, "value": json.dumps( { "event0": { "annotation_bool": True, "annotation_string": "annotation_test", "key_float": 0.3, } }, sort_keys=True, ), "endpoint": local_endpoint, } ], "binaryAnnotations": [ { "key": "key_bool", "value": "false", "endpoint": local_endpoint, }, { "key": "key_string", "value": "hello_world", "endpoint": local_endpoint, }, { "key": "key_float", "value": "111.22", "endpoint": local_endpoint, }, { "key": "otel.status_code", "value": "OK", "endpoint": local_endpoint, }, ], "debug": True, "parentId": JsonV1Encoder._encode_span_id(otel_spans[0].parent.span_id), }, { "traceId": trace_id, "id": JsonV1Encoder._encode_span_id(otel_spans[1].context.span_id), "name": otel_spans[1].name, "timestamp": otel_spans[1].start_time // 10**3, "duration": (otel_spans[1].end_time // 10**3) - (otel_spans[1].start_time // 10**3), "binaryAnnotations": [ { "key": "key_resource", "value": "some_resource", "endpoint": local_endpoint, }, { "key": "otel.status_code", "value": "ERROR", "endpoint": local_endpoint, }, { "key": "error", "value": "Example description", "endpoint": local_endpoint, }, ], }, { "traceId": trace_id, "id": JsonV1Encoder._encode_span_id(otel_spans[2].context.span_id), "name": otel_spans[2].name, "timestamp": otel_spans[2].start_time // 10**3, "duration": (otel_spans[2].end_time // 10**3) - (otel_spans[2].start_time // 10**3), "binaryAnnotations": [ { "key": "key_string", "value": "hello_world", "endpoint": local_endpoint, }, { "key": "key_resource", "value": "some_resource", "endpoint": local_endpoint, }, ], }, { "traceId": trace_id, "id": JsonV1Encoder._encode_span_id(otel_spans[3].context.span_id), "name": otel_spans[3].name, "timestamp": otel_spans[3].start_time // 10**3, "duration": (otel_spans[3].end_time // 10**3) - (otel_spans[3].start_time // 10**3), "binaryAnnotations": [ { "key": NAME_KEY, "value": "name", "endpoint": local_endpoint, }, { "key": VERSION_KEY, "value": "version", "endpoint": local_endpoint, }, { "key": _SCOPE_NAME_KEY, "value": "name", "endpoint": local_endpoint, }, { "key": _SCOPE_VERSION_KEY, "value": "version", "endpoint": local_endpoint, }, ], }, ] self.assert_equal_encoded_spans( json.dumps(expected_output), JsonV1Encoder().serialize(otel_spans, NodeEndpoint()), ) def test_encode_id_zero_padding(self): trace_id = 0x0E0C63257DE34C926F9EFCD03927272E span_id = 0x04BF92DEEFC58C92 parent_id = 0x0AAAAAAAAAAAAAAA start_time = 683647322 * 10**9 # in ns duration = 50 * 10**6 end_time = start_time + duration otel_span = trace._Span( name=TEST_SERVICE_NAME, context=trace_api.SpanContext( trace_id, span_id, is_remote=False, trace_flags=TraceFlags(TraceFlags.SAMPLED), ), parent=trace_api.SpanContext(trace_id, parent_id, is_remote=False), resource=trace.Resource({}), ) otel_span.start(start_time=start_time) otel_span.end(end_time=end_time) expected_output = [ { "traceId": format_trace_id(trace_id), "id": format_span_id(span_id), "name": TEST_SERVICE_NAME, "timestamp": JsonV1Encoder._nsec_to_usec_round(start_time), "duration": JsonV1Encoder._nsec_to_usec_round(duration), "debug": True, "parentId": format_span_id(parent_id), } ] self.assertEqual( json.dumps(expected_output), JsonV1Encoder().serialize([otel_span], NodeEndpoint()), ) def _test_encode_max_tag_length(self, max_tag_value_length: int): otel_span, expected_tag_output = self.get_data_for_max_tag_length_test(max_tag_value_length) service_name = otel_span.name binary_annotations = [] for tag_key, tag_expected_value in expected_tag_output.items(): binary_annotations.append( { "key": tag_key, "value": tag_expected_value, "endpoint": {"serviceName": service_name}, } ) expected_output = [ { "traceId": JsonV1Encoder._encode_trace_id(otel_span.context.trace_id), "id": JsonV1Encoder._encode_span_id(otel_span.context.span_id), "name": service_name, "timestamp": JsonV1Encoder._nsec_to_usec_round(otel_span.start_time), "duration": JsonV1Encoder._nsec_to_usec_round(otel_span.end_time - otel_span.start_time), "binaryAnnotations": binary_annotations, "debug": True, } ] self.assert_equal_encoded_spans( json.dumps(expected_output), JsonV1Encoder(max_tag_value_length).serialize([otel_span], NodeEndpoint()), ) def test_dropped_span_attributes(self): otel_span = get_span_with_dropped_attributes_events_links() annotations = JsonV1Encoder()._encode_span(otel_span, "test")["binaryAnnotations"] annotations = {annotation["key"]: annotation["value"] for annotation in annotations} self.assertEqual("1", annotations["otel.dropped_links_count"]) self.assertEqual("2", annotations["otel.dropped_attributes_count"]) self.assertEqual("3", annotations["otel.dropped_events_count"]) test_v2_json.py000066400000000000000000000170361527070070700344100ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-zipkin-json/tests/encoder# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import json from opentelemetry import trace as trace_api from opentelemetry.exporter.zipkin.encoder import ( _SCOPE_NAME_KEY, _SCOPE_VERSION_KEY, NAME_KEY, VERSION_KEY, ) from opentelemetry.exporter.zipkin.json.v2 import JsonV2Encoder from opentelemetry.exporter.zipkin.node_endpoint import NodeEndpoint from opentelemetry.sdk import trace from opentelemetry.test.spantestutil import ( get_span_with_dropped_attributes_events_links, ) from opentelemetry.trace import SpanKind, TraceFlags from .common_tests import ( # pylint: disable=import-error TEST_SERVICE_NAME, CommonEncoderTestCases, ) # pylint: disable=protected-access class TestV2JsonEncoder(CommonEncoderTestCases.CommonJsonEncoderTest): @staticmethod def get_encoder(*args, **kwargs) -> JsonV2Encoder: return JsonV2Encoder(*args, **kwargs) def test_encode(self): local_endpoint = {"serviceName": TEST_SERVICE_NAME} span_kind = JsonV2Encoder.SPAN_KIND_MAP[SpanKind.INTERNAL] otel_spans = self.get_exhaustive_otel_span_list() trace_id = JsonV2Encoder._encode_trace_id(otel_spans[0].context.trace_id) expected_output = [ { "traceId": trace_id, "id": JsonV2Encoder._encode_span_id(otel_spans[0].context.span_id), "name": otel_spans[0].name, "timestamp": otel_spans[0].start_time // 10**3, "duration": (otel_spans[0].end_time // 10**3) - (otel_spans[0].start_time // 10**3), "localEndpoint": local_endpoint, "kind": span_kind, "tags": { "key_bool": "false", "key_string": "hello_world", "key_float": "111.22", "otel.status_code": "OK", }, "annotations": [ { "timestamp": otel_spans[0].events[0].timestamp // 10**3, "value": json.dumps( { "event0": { "annotation_bool": True, "annotation_string": "annotation_test", "key_float": 0.3, } }, sort_keys=True, ), } ], "debug": True, "parentId": JsonV2Encoder._encode_span_id(otel_spans[0].parent.span_id), }, { "traceId": trace_id, "id": JsonV2Encoder._encode_span_id(otel_spans[1].context.span_id), "name": otel_spans[1].name, "timestamp": otel_spans[1].start_time // 10**3, "duration": (otel_spans[1].end_time // 10**3) - (otel_spans[1].start_time // 10**3), "localEndpoint": local_endpoint, "kind": span_kind, "tags": { "key_resource": "some_resource", "otel.status_code": "ERROR", "error": "Example description", }, }, { "traceId": trace_id, "id": JsonV2Encoder._encode_span_id(otel_spans[2].context.span_id), "name": otel_spans[2].name, "timestamp": otel_spans[2].start_time // 10**3, "duration": (otel_spans[2].end_time // 10**3) - (otel_spans[2].start_time // 10**3), "localEndpoint": local_endpoint, "kind": span_kind, "tags": { "key_string": "hello_world", "key_resource": "some_resource", }, }, { "traceId": trace_id, "id": JsonV2Encoder._encode_span_id(otel_spans[3].context.span_id), "name": otel_spans[3].name, "timestamp": otel_spans[3].start_time // 10**3, "duration": (otel_spans[3].end_time // 10**3) - (otel_spans[3].start_time // 10**3), "localEndpoint": local_endpoint, "kind": span_kind, "tags": { NAME_KEY: "name", VERSION_KEY: "version", _SCOPE_NAME_KEY: "name", _SCOPE_VERSION_KEY: "version", }, }, ] self.assert_equal_encoded_spans( json.dumps(expected_output), JsonV2Encoder().serialize(otel_spans, NodeEndpoint()), ) def test_encode_id_zero_padding(self): trace_id = 0x0E0C63257DE34C926F9EFCD03927272E span_id = 0x04BF92DEEFC58C92 parent_id = 0x0AAAAAAAAAAAAAAA start_time = 683647322 * 10**9 # in ns duration = 50 * 10**6 end_time = start_time + duration otel_span = trace._Span( name=TEST_SERVICE_NAME, context=trace_api.SpanContext( trace_id, span_id, is_remote=False, trace_flags=TraceFlags(TraceFlags.SAMPLED), ), parent=trace_api.SpanContext(trace_id, parent_id, is_remote=False), resource=trace.Resource({}), ) otel_span.start(start_time=start_time) otel_span.end(end_time=end_time) expected_output = [ { "traceId": format(trace_id, "032x"), "id": format(span_id, "016x"), "name": TEST_SERVICE_NAME, "timestamp": JsonV2Encoder._nsec_to_usec_round(start_time), "duration": JsonV2Encoder._nsec_to_usec_round(duration), "localEndpoint": {"serviceName": TEST_SERVICE_NAME}, "kind": JsonV2Encoder.SPAN_KIND_MAP[SpanKind.INTERNAL], "debug": True, "parentId": format(parent_id, "016x"), } ] self.assert_equal_encoded_spans( json.dumps(expected_output), JsonV2Encoder().serialize([otel_span], NodeEndpoint()), ) def _test_encode_max_tag_length(self, max_tag_value_length: int): otel_span, expected_tag_output = self.get_data_for_max_tag_length_test(max_tag_value_length) service_name = otel_span.name expected_output = [ { "traceId": JsonV2Encoder._encode_trace_id(otel_span.context.trace_id), "id": JsonV2Encoder._encode_span_id(otel_span.context.span_id), "name": service_name, "timestamp": JsonV2Encoder._nsec_to_usec_round(otel_span.start_time), "duration": JsonV2Encoder._nsec_to_usec_round(otel_span.end_time - otel_span.start_time), "localEndpoint": {"serviceName": service_name}, "kind": JsonV2Encoder.SPAN_KIND_MAP[SpanKind.INTERNAL], "tags": expected_tag_output, "debug": True, } ] self.assert_equal_encoded_spans( json.dumps(expected_output), JsonV2Encoder(max_tag_value_length).serialize([otel_span], NodeEndpoint()), ) def test_dropped_span_attributes(self): otel_span = get_span_with_dropped_attributes_events_links() tags = JsonV2Encoder()._encode_span(otel_span, "test")["tags"] self.assertEqual("1", tags["otel.dropped_links_count"]) self.assertEqual("2", tags["otel.dropped_attributes_count"]) self.assertEqual("3", tags["otel.dropped_events_count"]) test_zipkin_exporter.py000066400000000000000000000201531527070070700346370ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-zipkin-json/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import ipaddress import os import unittest from unittest.mock import patch import requests from opentelemetry import trace from opentelemetry.exporter.zipkin.encoder import Protocol from opentelemetry.exporter.zipkin.json import DEFAULT_ENDPOINT, ZipkinExporter from opentelemetry.exporter.zipkin.json.v2 import JsonV2Encoder from opentelemetry.exporter.zipkin.node_endpoint import NodeEndpoint from opentelemetry.sdk.environment_variables import ( OTEL_EXPORTER_ZIPKIN_ENDPOINT, OTEL_EXPORTER_ZIPKIN_TIMEOUT, ) from opentelemetry.sdk.resources import SERVICE_NAME, Resource from opentelemetry.sdk.trace import TracerProvider, _Span from opentelemetry.sdk.trace.export import SpanExportResult TEST_SERVICE_NAME = "test_service" class MockResponse: def __init__(self, status_code): self.status_code = status_code self.text = status_code class TestZipkinExporter(unittest.TestCase): @classmethod def setUpClass(cls): trace.set_tracer_provider(TracerProvider(resource=Resource({SERVICE_NAME: TEST_SERVICE_NAME}))) def tearDown(self): os.environ.pop(OTEL_EXPORTER_ZIPKIN_ENDPOINT, None) os.environ.pop(OTEL_EXPORTER_ZIPKIN_TIMEOUT, None) def test_constructor_default(self): exporter = ZipkinExporter() self.assertIsInstance(exporter.encoder, JsonV2Encoder) self.assertIsInstance(exporter.session, requests.Session) self.assertEqual(exporter.endpoint, DEFAULT_ENDPOINT) self.assertEqual(exporter.local_node.service_name, TEST_SERVICE_NAME) self.assertEqual(exporter.local_node.ipv4, None) self.assertEqual(exporter.local_node.ipv6, None) self.assertEqual(exporter.local_node.port, None) def test_constructor_env_vars(self): os_endpoint = "https://foo:9911/path" os.environ[OTEL_EXPORTER_ZIPKIN_ENDPOINT] = os_endpoint os.environ[OTEL_EXPORTER_ZIPKIN_TIMEOUT] = "15" exporter = ZipkinExporter() self.assertEqual(exporter.endpoint, os_endpoint) self.assertEqual(exporter.local_node.service_name, TEST_SERVICE_NAME) self.assertEqual(exporter.local_node.ipv4, None) self.assertEqual(exporter.local_node.ipv6, None) self.assertEqual(exporter.local_node.port, None) self.assertEqual(exporter.timeout, 15) def test_constructor_protocol_endpoint(self): """Test the constructor for the common usage of providing the protocol and endpoint arguments.""" endpoint = "https://opentelemetry.io:15875/myapi/traces?format=zipkin" exporter = ZipkinExporter(endpoint=endpoint) self.assertIsInstance(exporter.encoder, JsonV2Encoder) self.assertIsInstance(exporter.session, requests.Session) self.assertEqual(exporter.endpoint, endpoint) self.assertEqual(exporter.local_node.service_name, TEST_SERVICE_NAME) self.assertEqual(exporter.local_node.ipv4, None) self.assertEqual(exporter.local_node.ipv6, None) self.assertEqual(exporter.local_node.port, None) def test_constructor_all_params_and_env_vars(self): """Test the scenario where all params are provided and all OS env vars are set. Explicit params should take precedence. """ os_endpoint = "https://os.env.param:9911/path" os.environ[OTEL_EXPORTER_ZIPKIN_ENDPOINT] = os_endpoint os.environ[OTEL_EXPORTER_ZIPKIN_TIMEOUT] = "15" constructor_param_version = Protocol.V2 constructor_param_endpoint = "https://constructor.param:9911/path" local_node_ipv4 = "192.168.0.1" local_node_ipv6 = "2001:db8::1000" local_node_port = 30301 max_tag_value_length = 56 timeout_param = 20 session_param = requests.Session() exporter = ZipkinExporter( constructor_param_version, constructor_param_endpoint, local_node_ipv4, local_node_ipv6, local_node_port, max_tag_value_length, timeout_param, session_param, ) self.assertIsInstance(exporter.encoder, JsonV2Encoder) self.assertIsInstance(exporter.session, requests.Session) self.assertEqual(exporter.endpoint, constructor_param_endpoint) self.assertEqual(exporter.local_node.service_name, TEST_SERVICE_NAME) self.assertEqual(exporter.local_node.ipv4, ipaddress.IPv4Address(local_node_ipv4)) self.assertEqual(exporter.local_node.ipv6, ipaddress.IPv6Address(local_node_ipv6)) self.assertEqual(exporter.local_node.port, local_node_port) # Assert timeout passed in constructor is prioritized over env # when both are set. self.assertEqual(exporter.timeout, 20) @patch("requests.Session.post") def test_export_success(self, mock_post): mock_post.return_value = MockResponse(200) spans = [] exporter = ZipkinExporter() status = exporter.export(spans) self.assertEqual(SpanExportResult.SUCCESS, status) @patch("requests.Session.post") def test_export_invalid_response(self, mock_post): mock_post.return_value = MockResponse(404) spans = [] exporter = ZipkinExporter() status = exporter.export(spans) self.assertEqual(SpanExportResult.FAILURE, status) @patch("requests.Session.post") def test_export_span_service_name(self, mock_post): mock_post.return_value = MockResponse(200) resource = Resource.create({SERVICE_NAME: "test"}) context = trace.SpanContext( trace_id=0x000000000000000000000000DEADBEEF, span_id=0x00000000DEADBEF0, is_remote=False, ) span = _Span("test_span", context=context, resource=resource) span.start() span.end() exporter = ZipkinExporter() exporter.export([span]) self.assertEqual(exporter.local_node.service_name, "test") @patch("requests.Session.post") def test_export_shutdown(self, mock_post): mock_post.return_value = MockResponse(200) spans = [] exporter = ZipkinExporter() status = exporter.export(spans) self.assertEqual(SpanExportResult.SUCCESS, status) exporter.shutdown() # Any call to .export() post shutdown should return failure status = exporter.export(spans) self.assertEqual(SpanExportResult.FAILURE, status) @patch("requests.Session.post") def test_export_timeout(self, mock_post): mock_post.return_value = MockResponse(200) spans = [] exporter = ZipkinExporter(timeout=2) status = exporter.export(spans) self.assertEqual(SpanExportResult.SUCCESS, status) mock_post.assert_called_with(url="http://localhost:9411/api/v2/spans", data="[]", timeout=2) class TestZipkinNodeEndpoint(unittest.TestCase): def test_constructor_default(self): node_endpoint = NodeEndpoint() self.assertEqual(node_endpoint.ipv4, None) self.assertEqual(node_endpoint.ipv6, None) self.assertEqual(node_endpoint.port, None) self.assertEqual(node_endpoint.service_name, TEST_SERVICE_NAME) def test_constructor_explicits(self): ipv4 = "192.168.0.1" ipv6 = "2001:db8::c001" port = 414120 node_endpoint = NodeEndpoint(ipv4, ipv6, port) self.assertEqual(node_endpoint.ipv4, ipaddress.IPv4Address(ipv4)) self.assertEqual(node_endpoint.ipv6, ipaddress.IPv6Address(ipv6)) self.assertEqual(node_endpoint.port, port) self.assertEqual(node_endpoint.service_name, TEST_SERVICE_NAME) def test_ipv4_invalid_raises_error(self): with self.assertRaises(ValueError): NodeEndpoint(ipv4="invalid-ipv4-address") def test_ipv4_passed_ipv6_raises_error(self): with self.assertRaises(ValueError): NodeEndpoint(ipv4="2001:db8::c001") def test_ipv6_invalid_raises_error(self): with self.assertRaises(ValueError): NodeEndpoint(ipv6="invalid-ipv6-address") def test_ipv6_passed_ipv4_raises_error(self): with self.assertRaises(ValueError): NodeEndpoint(ipv6="192.168.0.1") python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-zipkin-proto-http/000077500000000000000000000000001527070070700300555ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-zipkin-proto-http/CHANGELOG.md000066400000000000000000000000001527070070700316540ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-zipkin-proto-http/LICENSE000066400000000000000000000261351527070070700310710ustar00rootroot00000000000000 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. python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-zipkin-proto-http/README.rst000066400000000000000000000012601527070070700315430ustar00rootroot00000000000000OpenTelemetry Zipkin Protobuf Exporter ====================================== |pypi| .. |pypi| image:: https://badge.fury.io/py/opentelemetry-exporter-zipkin-proto-http.svg :target: https://pypi.org/project/opentelemetry-exporter-zipkin-proto-http/ This library allows export of tracing data to `Zipkin `_ using Protobuf for serialization. Installation ------------ :: pip install opentelemetry-exporter-zipkin-proto-http References ---------- * `OpenTelemetry Zipkin Exporter `_ * `Zipkin `_ * `OpenTelemetry Project `_ python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-zipkin-proto-http/pyproject.toml000066400000000000000000000031601527070070700327710ustar00rootroot00000000000000[build-system] requires = ["hatchling"] build-backend = "hatchling.build" [project] name = "opentelemetry-exporter-zipkin-proto-http" dynamic = ["version"] description = "Zipkin Span Protobuf 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 :: 5 - Production/Stable", "Framework :: OpenTelemetry", "Framework :: OpenTelemetry :: Exporters", "Intended Audience :: Developers", "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.3", "opentelemetry-exporter-zipkin-json == 1.45.2", "opentelemetry-sdk ~= 1.11", "protobuf ~= 3.12", "requests ~= 2.7", ] [project.entry-points.opentelemetry_traces_exporter] zipkin_proto = "opentelemetry.exporter.zipkin.proto.http:ZipkinExporter" [project.urls] Homepage = "https://github.com/open-telemetry/opentelemetry-python/tree/main/exporter/opentelemetry-exporter-zipkin-proto-http" Repository = "https://github.com/open-telemetry/opentelemetry-python" [tool.hatch.version] path = "src/opentelemetry/exporter/zipkin/proto/http/version/__init__.py" [tool.hatch.build.targets.sdist] include = [ "/src", "/tests", ] [tool.hatch.build.targets.wheel] packages = ["src/opentelemetry"] python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-zipkin-proto-http/src/000077500000000000000000000000001527070070700306445ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/000077500000000000000000000000001527070070700335405ustar00rootroot00000000000000exporter/000077500000000000000000000000001527070070700353315ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetryzipkin/000077500000000000000000000000001527070070700366355ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporterproto/000077500000000000000000000000001527070070700400005ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkinhttp/000077500000000000000000000000001527070070700407575ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto__init__.py000066400000000000000000000136201527070070700430720ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 """ OpenTelemetry Zipkin Protobuf Exporter -------------------------------------- This library allows to export tracing data to `Zipkin `_. Usage ----- The **OpenTelemetry Zipkin Exporter** allows exporting of `OpenTelemetry`_ traces to `Zipkin`_. This exporter sends traces to the configured Zipkin collector endpoint using HTTP and supports v2 protobuf. .. _Zipkin: https://zipkin.io/ .. _OpenTelemetry: https://github.com/open-telemetry/opentelemetry-python/ .. _Specification: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/sdk-environment-variables.md#zipkin-exporter .. code:: python import requests from opentelemetry import trace from opentelemetry.exporter.zipkin.proto.http import ZipkinExporter from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchSpanProcessor trace.set_tracer_provider(TracerProvider()) tracer = trace.get_tracer(__name__) # create a ZipkinExporter zipkin_exporter = ZipkinExporter( # optional: # endpoint="http://localhost:9411/api/v2/spans", # local_node_ipv4="192.168.0.1", # local_node_ipv6="2001:db8::c001", # local_node_port=31313, # max_tag_value_length=256, # timeout=5 (in seconds), # session=requests.Session() ) # Create a BatchSpanProcessor and add the exporter to it span_processor = BatchSpanProcessor(zipkin_exporter) # add to the tracer trace.get_tracer_provider().add_span_processor(span_processor) with tracer.start_as_current_span("foo"): print("Hello world!") The exporter supports the following environment variable for configuration: - :envvar:`OTEL_EXPORTER_ZIPKIN_ENDPOINT` - :envvar:`OTEL_EXPORTER_ZIPKIN_TIMEOUT` API --- """ import logging from collections.abc import Sequence from os import environ import requests from opentelemetry.exporter.zipkin.node_endpoint import IpInput, NodeEndpoint from opentelemetry.exporter.zipkin.proto.http.v2 import ProtobufEncoder from opentelemetry.sdk.environment_variables import ( OTEL_EXPORTER_ZIPKIN_ENDPOINT, OTEL_EXPORTER_ZIPKIN_TIMEOUT, ) from opentelemetry.sdk.resources import SERVICE_NAME from opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult from opentelemetry.trace import Span DEFAULT_ENDPOINT = "http://localhost:9411/api/v2/spans" REQUESTS_SUCCESS_STATUS_CODES = (200, 202) logger = logging.getLogger(__name__) class ZipkinExporter(SpanExporter): def __init__( self, endpoint: str | None = None, local_node_ipv4: IpInput = None, local_node_ipv6: IpInput = None, local_node_port: int | None = None, max_tag_value_length: int | None = None, timeout: int | None = None, session: requests.Session | None = None, ): """Zipkin exporter. Args: version: The protocol version to be used. endpoint: The endpoint of the Zipkin collector. local_node_ipv4: Primary IPv4 address associated with this connection. local_node_ipv6: Primary IPv6 address associated with this connection. local_node_port: Depending on context, this could be a listen port or the client-side of a socket. max_tag_value_length: Max length string attribute values can have. timeout: Maximum time the Zipkin exporter will wait for each batch export. The default value is 10s. session: Connection session to the Zipkin collector endpoint. The tuple (local_node_ipv4, local_node_ipv6, local_node_port) is used to represent the network context of a node in the service graph. """ self.local_node = NodeEndpoint(local_node_ipv4, local_node_ipv6, local_node_port) if endpoint is None: endpoint = environ.get(OTEL_EXPORTER_ZIPKIN_ENDPOINT) or DEFAULT_ENDPOINT self.endpoint = endpoint self.encoder = ProtobufEncoder(max_tag_value_length) self.session = session or requests.Session() self.session.headers.update({"Content-Type": self.encoder.content_type()}) self._closed = False self.timeout = timeout or int(environ.get(OTEL_EXPORTER_ZIPKIN_TIMEOUT, 10)) def export(self, spans: Sequence[Span]) -> SpanExportResult: # After the call to Shutdown subsequent calls to Export are # not allowed and should return a Failure result if self._closed: logger.warning("Exporter already shutdown, ignoring batch") return SpanExportResult.FAILURE # Populate service_name from first span # We restrict any SpanProcessor to be only associated with a single # TracerProvider, so it is safe to assume that all Spans in a single # batch all originate from one TracerProvider (and in turn have all # the same service.name) if spans: service_name = spans[0].resource.attributes.get(SERVICE_NAME) if service_name: self.local_node.service_name = service_name result = self.session.post( url=self.endpoint, data=self.encoder.serialize(spans, self.local_node), timeout=self.timeout, ) if result.status_code not in REQUESTS_SUCCESS_STATUS_CODES: logger.error( "Traces cannot be uploaded; status code: %s, message %s", result.status_code, result.text, ) return SpanExportResult.FAILURE return SpanExportResult.SUCCESS def shutdown(self) -> None: if self._closed: logger.warning("Exporter already shutdown, ignoring call") return self.session.close() self._closed = True def force_flush(self, timeout_millis: int = 30000) -> bool: return True py.typed000066400000000000000000000000001527070070700424440ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/httpv2/000077500000000000000000000000001527070070700413065ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http__init__.py000066400000000000000000000100731527070070700434200ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/v2# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 """Zipkin Export Encoder for Protobuf API spec: https://github.com/openzipkin/zipkin-api/blob/master/zipkin.proto """ from collections.abc import Sequence from opentelemetry.exporter.zipkin.encoder import Encoder from opentelemetry.exporter.zipkin.node_endpoint import NodeEndpoint from opentelemetry.exporter.zipkin.proto.http.v2.gen import zipkin_pb2 from opentelemetry.sdk.trace import Event from opentelemetry.trace import Span, SpanKind class ProtobufEncoder(Encoder): """Zipkin Export Encoder for Protobuf API spec: https://github.com/openzipkin/zipkin-api/blob/master/zipkin.proto """ SPAN_KIND_MAP = { SpanKind.INTERNAL: zipkin_pb2.Span.Kind.SPAN_KIND_UNSPECIFIED, SpanKind.SERVER: zipkin_pb2.Span.Kind.SERVER, SpanKind.CLIENT: zipkin_pb2.Span.Kind.CLIENT, SpanKind.PRODUCER: zipkin_pb2.Span.Kind.PRODUCER, SpanKind.CONSUMER: zipkin_pb2.Span.Kind.CONSUMER, } @staticmethod def content_type(): return "application/x-protobuf" def serialize(self, spans: Sequence[Span], local_endpoint: NodeEndpoint) -> bytes: encoded_local_endpoint = self._encode_local_endpoint(local_endpoint) # pylint: disable=no-member encoded_spans = zipkin_pb2.ListOfSpans() for span in spans: encoded_spans.spans.append(self._encode_span(span, encoded_local_endpoint)) return encoded_spans.SerializeToString() def _encode_span(self, span: Span, encoded_local_endpoint: zipkin_pb2.Endpoint) -> zipkin_pb2.Span: context = span.get_span_context() # pylint: disable=no-member encoded_span = zipkin_pb2.Span( trace_id=self._encode_trace_id(context.trace_id), id=self._encode_span_id(context.span_id), name=span.name, timestamp=self._nsec_to_usec_round(span.start_time), duration=self._nsec_to_usec_round(span.end_time - span.start_time), local_endpoint=encoded_local_endpoint, kind=self.SPAN_KIND_MAP[span.kind], ) tags = self._extract_tags_from_span(span) if tags: encoded_span.tags.update(tags) annotations = self._encode_annotations(span.events) if annotations: encoded_span.annotations.extend(annotations) debug = self._encode_debug(context) if debug: encoded_span.debug = debug parent_id = self._get_parent_id(span.parent) if parent_id is not None: encoded_span.parent_id = self._encode_span_id(parent_id) return encoded_span def _encode_annotations(self, span_events: list[Event] | None) -> list | None: annotations = self._extract_annotations_from_events(span_events) if annotations is None: encoded_annotations = None else: encoded_annotations = [] for annotation in annotations: encoded_annotations.append( zipkin_pb2.Annotation( timestamp=annotation["timestamp"], value=annotation["value"], ) ) return encoded_annotations @staticmethod def _encode_local_endpoint( local_endpoint: NodeEndpoint, ) -> zipkin_pb2.Endpoint: encoded_local_endpoint = zipkin_pb2.Endpoint( service_name=local_endpoint.service_name, ) if local_endpoint.ipv4 is not None: encoded_local_endpoint.ipv4 = local_endpoint.ipv4.packed if local_endpoint.ipv6 is not None: encoded_local_endpoint.ipv6 = local_endpoint.ipv6.packed if local_endpoint.port is not None: encoded_local_endpoint.port = local_endpoint.port return encoded_local_endpoint @staticmethod def _encode_span_id(span_id: int) -> bytes: return span_id.to_bytes(length=8, byteorder="big", signed=False) @staticmethod def _encode_trace_id(trace_id: int) -> bytes: return trace_id.to_bytes(length=16, byteorder="big", signed=False) gen/000077500000000000000000000000001527070070700420575ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/v2__init__.py000066400000000000000000000000001527070070700441560ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/v2/genzipkin_pb2.py000066400000000000000000000435241527070070700445100ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/v2/gen# Generated by the protocol buffer compiler. DO NOT EDIT! # source: zipkin.proto """Generated protocol buffer code.""" 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='zipkin.proto', package='zipkin.proto3', syntax='proto3', serialized_options=b'\n\016zipkin2.proto3P\001', create_key=_descriptor._internal_create_key, serialized_pb=b'\n\x0czipkin.proto\x12\rzipkin.proto3\"\xf5\x03\n\x04Span\x12\x10\n\x08trace_id\x18\x01 \x01(\x0c\x12\x11\n\tparent_id\x18\x02 \x01(\x0c\x12\n\n\x02id\x18\x03 \x01(\x0c\x12&\n\x04kind\x18\x04 \x01(\x0e\x32\x18.zipkin.proto3.Span.Kind\x12\x0c\n\x04name\x18\x05 \x01(\t\x12\x11\n\ttimestamp\x18\x06 \x01(\x06\x12\x10\n\x08\x64uration\x18\x07 \x01(\x04\x12/\n\x0elocal_endpoint\x18\x08 \x01(\x0b\x32\x17.zipkin.proto3.Endpoint\x12\x30\n\x0fremote_endpoint\x18\t \x01(\x0b\x32\x17.zipkin.proto3.Endpoint\x12.\n\x0b\x61nnotations\x18\n \x03(\x0b\x32\x19.zipkin.proto3.Annotation\x12+\n\x04tags\x18\x0b \x03(\x0b\x32\x1d.zipkin.proto3.Span.TagsEntry\x12\r\n\x05\x64\x65\x62ug\x18\x0c \x01(\x08\x12\x0e\n\x06shared\x18\r \x01(\x08\x1a+\n\tTagsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"U\n\x04Kind\x12\x19\n\x15SPAN_KIND_UNSPECIFIED\x10\x00\x12\n\n\x06\x43LIENT\x10\x01\x12\n\n\x06SERVER\x10\x02\x12\x0c\n\x08PRODUCER\x10\x03\x12\x0c\n\x08\x43ONSUMER\x10\x04\"J\n\x08\x45ndpoint\x12\x14\n\x0cservice_name\x18\x01 \x01(\t\x12\x0c\n\x04ipv4\x18\x02 \x01(\x0c\x12\x0c\n\x04ipv6\x18\x03 \x01(\x0c\x12\x0c\n\x04port\x18\x04 \x01(\x05\".\n\nAnnotation\x12\x11\n\ttimestamp\x18\x01 \x01(\x06\x12\r\n\x05value\x18\x02 \x01(\t\"1\n\x0bListOfSpans\x12\"\n\x05spans\x18\x01 \x03(\x0b\x32\x13.zipkin.proto3.Span\"\x10\n\x0eReportResponse2T\n\x0bSpanService\x12\x45\n\x06Report\x12\x1a.zipkin.proto3.ListOfSpans\x1a\x1d.zipkin.proto3.ReportResponse\"\x00\x42\x12\n\x0ezipkin2.proto3P\x01\x62\x06proto3' ) _SPAN_KIND = _descriptor.EnumDescriptor( name='Kind', full_name='zipkin.proto3.Span.Kind', filename=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key, values=[ _descriptor.EnumValueDescriptor( name='SPAN_KIND_UNSPECIFIED', index=0, number=0, serialized_options=None, type=None, create_key=_descriptor._internal_create_key), _descriptor.EnumValueDescriptor( name='CLIENT', index=1, number=1, serialized_options=None, type=None, create_key=_descriptor._internal_create_key), _descriptor.EnumValueDescriptor( name='SERVER', index=2, number=2, serialized_options=None, type=None, create_key=_descriptor._internal_create_key), _descriptor.EnumValueDescriptor( name='PRODUCER', index=3, number=3, serialized_options=None, type=None, create_key=_descriptor._internal_create_key), _descriptor.EnumValueDescriptor( name='CONSUMER', index=4, number=4, serialized_options=None, type=None, create_key=_descriptor._internal_create_key), ], containing_type=None, serialized_options=None, serialized_start=448, serialized_end=533, ) _sym_db.RegisterEnumDescriptor(_SPAN_KIND) _SPAN_TAGSENTRY = _descriptor.Descriptor( name='TagsEntry', full_name='zipkin.proto3.Span.TagsEntry', filename=None, file=DESCRIPTOR, containing_type=None, create_key=_descriptor._internal_create_key, fields=[ _descriptor.FieldDescriptor( name='key', full_name='zipkin.proto3.Span.TagsEntry.key', index=0, number=1, 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, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='value', full_name='zipkin.proto3.Span.TagsEntry.value', 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, create_key=_descriptor._internal_create_key), ], extensions=[ ], nested_types=[], enum_types=[ ], serialized_options=b'8\001', is_extendable=False, syntax='proto3', extension_ranges=[], oneofs=[ ], serialized_start=403, serialized_end=446, ) _SPAN = _descriptor.Descriptor( name='Span', full_name='zipkin.proto3.Span', filename=None, file=DESCRIPTOR, containing_type=None, create_key=_descriptor._internal_create_key, fields=[ _descriptor.FieldDescriptor( name='trace_id', full_name='zipkin.proto3.Span.trace_id', index=0, number=1, type=12, cpp_type=9, label=1, has_default_value=False, default_value=b"", message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='parent_id', full_name='zipkin.proto3.Span.parent_id', index=1, number=2, type=12, cpp_type=9, label=1, has_default_value=False, default_value=b"", message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='id', full_name='zipkin.proto3.Span.id', index=2, number=3, type=12, cpp_type=9, label=1, has_default_value=False, default_value=b"", message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='kind', full_name='zipkin.proto3.Span.kind', index=3, number=4, type=14, cpp_type=8, 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, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='name', full_name='zipkin.proto3.Span.name', index=4, number=5, 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, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='timestamp', full_name='zipkin.proto3.Span.timestamp', index=5, number=6, type=6, cpp_type=4, 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, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='duration', full_name='zipkin.proto3.Span.duration', index=6, number=7, type=4, cpp_type=4, 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, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='local_endpoint', full_name='zipkin.proto3.Span.local_endpoint', index=7, number=8, type=11, cpp_type=10, label=1, has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='remote_endpoint', full_name='zipkin.proto3.Span.remote_endpoint', index=8, number=9, type=11, cpp_type=10, label=1, has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='annotations', full_name='zipkin.proto3.Span.annotations', index=9, number=10, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='tags', full_name='zipkin.proto3.Span.tags', index=10, number=11, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='debug', full_name='zipkin.proto3.Span.debug', index=11, number=12, type=8, cpp_type=7, label=1, has_default_value=False, default_value=False, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='shared', full_name='zipkin.proto3.Span.shared', index=12, number=13, type=8, cpp_type=7, label=1, has_default_value=False, default_value=False, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), ], extensions=[ ], nested_types=[_SPAN_TAGSENTRY, ], enum_types=[ _SPAN_KIND, ], serialized_options=None, is_extendable=False, syntax='proto3', extension_ranges=[], oneofs=[ ], serialized_start=32, serialized_end=533, ) _ENDPOINT = _descriptor.Descriptor( name='Endpoint', full_name='zipkin.proto3.Endpoint', filename=None, file=DESCRIPTOR, containing_type=None, create_key=_descriptor._internal_create_key, fields=[ _descriptor.FieldDescriptor( name='service_name', full_name='zipkin.proto3.Endpoint.service_name', index=0, number=1, 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, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='ipv4', full_name='zipkin.proto3.Endpoint.ipv4', index=1, number=2, type=12, cpp_type=9, label=1, has_default_value=False, default_value=b"", message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='ipv6', full_name='zipkin.proto3.Endpoint.ipv6', index=2, number=3, type=12, cpp_type=9, label=1, has_default_value=False, default_value=b"", message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='port', full_name='zipkin.proto3.Endpoint.port', index=3, number=4, type=5, cpp_type=1, 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, create_key=_descriptor._internal_create_key), ], extensions=[ ], nested_types=[], enum_types=[ ], serialized_options=None, is_extendable=False, syntax='proto3', extension_ranges=[], oneofs=[ ], serialized_start=535, serialized_end=609, ) _ANNOTATION = _descriptor.Descriptor( name='Annotation', full_name='zipkin.proto3.Annotation', filename=None, file=DESCRIPTOR, containing_type=None, create_key=_descriptor._internal_create_key, fields=[ _descriptor.FieldDescriptor( name='timestamp', full_name='zipkin.proto3.Annotation.timestamp', index=0, number=1, type=6, cpp_type=4, 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, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='value', full_name='zipkin.proto3.Annotation.value', 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, create_key=_descriptor._internal_create_key), ], extensions=[ ], nested_types=[], enum_types=[ ], serialized_options=None, is_extendable=False, syntax='proto3', extension_ranges=[], oneofs=[ ], serialized_start=611, serialized_end=657, ) _LISTOFSPANS = _descriptor.Descriptor( name='ListOfSpans', full_name='zipkin.proto3.ListOfSpans', filename=None, file=DESCRIPTOR, containing_type=None, create_key=_descriptor._internal_create_key, fields=[ _descriptor.FieldDescriptor( name='spans', full_name='zipkin.proto3.ListOfSpans.spans', index=0, number=1, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), ], extensions=[ ], nested_types=[], enum_types=[ ], serialized_options=None, is_extendable=False, syntax='proto3', extension_ranges=[], oneofs=[ ], serialized_start=659, serialized_end=708, ) _REPORTRESPONSE = _descriptor.Descriptor( name='ReportResponse', full_name='zipkin.proto3.ReportResponse', filename=None, file=DESCRIPTOR, containing_type=None, create_key=_descriptor._internal_create_key, fields=[ ], extensions=[ ], nested_types=[], enum_types=[ ], serialized_options=None, is_extendable=False, syntax='proto3', extension_ranges=[], oneofs=[ ], serialized_start=710, serialized_end=726, ) _SPAN_TAGSENTRY.containing_type = _SPAN _SPAN.fields_by_name['kind'].enum_type = _SPAN_KIND _SPAN.fields_by_name['local_endpoint'].message_type = _ENDPOINT _SPAN.fields_by_name['remote_endpoint'].message_type = _ENDPOINT _SPAN.fields_by_name['annotations'].message_type = _ANNOTATION _SPAN.fields_by_name['tags'].message_type = _SPAN_TAGSENTRY _SPAN_KIND.containing_type = _SPAN _LISTOFSPANS.fields_by_name['spans'].message_type = _SPAN DESCRIPTOR.message_types_by_name['Span'] = _SPAN DESCRIPTOR.message_types_by_name['Endpoint'] = _ENDPOINT DESCRIPTOR.message_types_by_name['Annotation'] = _ANNOTATION DESCRIPTOR.message_types_by_name['ListOfSpans'] = _LISTOFSPANS DESCRIPTOR.message_types_by_name['ReportResponse'] = _REPORTRESPONSE _sym_db.RegisterFileDescriptor(DESCRIPTOR) Span = _reflection.GeneratedProtocolMessageType('Span', (_message.Message,), { 'TagsEntry' : _reflection.GeneratedProtocolMessageType('TagsEntry', (_message.Message,), { 'DESCRIPTOR' : _SPAN_TAGSENTRY, '__module__' : 'zipkin_pb2' # @@protoc_insertion_point(class_scope:zipkin.proto3.Span.TagsEntry) }) , 'DESCRIPTOR' : _SPAN, '__module__' : 'zipkin_pb2' # @@protoc_insertion_point(class_scope:zipkin.proto3.Span) }) _sym_db.RegisterMessage(Span) _sym_db.RegisterMessage(Span.TagsEntry) Endpoint = _reflection.GeneratedProtocolMessageType('Endpoint', (_message.Message,), { 'DESCRIPTOR' : _ENDPOINT, '__module__' : 'zipkin_pb2' # @@protoc_insertion_point(class_scope:zipkin.proto3.Endpoint) }) _sym_db.RegisterMessage(Endpoint) Annotation = _reflection.GeneratedProtocolMessageType('Annotation', (_message.Message,), { 'DESCRIPTOR' : _ANNOTATION, '__module__' : 'zipkin_pb2' # @@protoc_insertion_point(class_scope:zipkin.proto3.Annotation) }) _sym_db.RegisterMessage(Annotation) ListOfSpans = _reflection.GeneratedProtocolMessageType('ListOfSpans', (_message.Message,), { 'DESCRIPTOR' : _LISTOFSPANS, '__module__' : 'zipkin_pb2' # @@protoc_insertion_point(class_scope:zipkin.proto3.ListOfSpans) }) _sym_db.RegisterMessage(ListOfSpans) ReportResponse = _reflection.GeneratedProtocolMessageType('ReportResponse', (_message.Message,), { 'DESCRIPTOR' : _REPORTRESPONSE, '__module__' : 'zipkin_pb2' # @@protoc_insertion_point(class_scope:zipkin.proto3.ReportResponse) }) _sym_db.RegisterMessage(ReportResponse) DESCRIPTOR._options = None _SPAN_TAGSENTRY._options = None _SPANSERVICE = _descriptor.ServiceDescriptor( name='SpanService', full_name='zipkin.proto3.SpanService', file=DESCRIPTOR, index=0, serialized_options=None, create_key=_descriptor._internal_create_key, serialized_start=728, serialized_end=812, methods=[ _descriptor.MethodDescriptor( name='Report', full_name='zipkin.proto3.SpanService.Report', index=0, containing_service=None, input_type=_LISTOFSPANS, output_type=_REPORTRESPONSE, serialized_options=None, create_key=_descriptor._internal_create_key, ), ]) _sym_db.RegisterServiceDescriptor(_SPANSERVICE) DESCRIPTOR.services_by_name['SpanService'] = _SPANSERVICE # @@protoc_insertion_point(module_scope) zipkin_pb2.pyi000066400000000000000000000212001527070070700446440ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/v2/gen# @generated by generate_proto_mypy_stubs.py. Do not edit! import sys from google.protobuf.descriptor import ( Descriptor as google___protobuf___descriptor___Descriptor, EnumDescriptor as google___protobuf___descriptor___EnumDescriptor, FileDescriptor as google___protobuf___descriptor___FileDescriptor, ) from google.protobuf.internal.containers import ( RepeatedCompositeFieldContainer as google___protobuf___internal___containers___RepeatedCompositeFieldContainer, ) from google.protobuf.message import ( Message as google___protobuf___message___Message, ) from typing import ( Iterable as typing___Iterable, List as typing___List, Mapping as typing___Mapping, MutableMapping as typing___MutableMapping, NewType as typing___NewType, Optional as typing___Optional, Text as typing___Text, Tuple as typing___Tuple, Union as typing___Union, cast as typing___cast, ) from typing_extensions import ( Literal as typing_extensions___Literal, ) builtin___bool = bool builtin___bytes = bytes builtin___float = float builtin___int = int builtin___str = str DESCRIPTOR: google___protobuf___descriptor___FileDescriptor = ... class Span(google___protobuf___message___Message): DESCRIPTOR: google___protobuf___descriptor___Descriptor = ... KindValue = typing___NewType('KindValue', builtin___int) type___KindValue = KindValue class Kind(object): DESCRIPTOR: google___protobuf___descriptor___EnumDescriptor = ... @classmethod def Name(cls, number: builtin___int) -> builtin___str: ... @classmethod def Value(cls, name: builtin___str) -> Span.KindValue: ... @classmethod def keys(cls) -> typing___List[builtin___str]: ... @classmethod def values(cls) -> typing___List[Span.KindValue]: ... @classmethod def items(cls) -> typing___List[typing___Tuple[builtin___str, Span.KindValue]]: ... SPAN_KIND_UNSPECIFIED = typing___cast(Span.KindValue, 0) CLIENT = typing___cast(Span.KindValue, 1) SERVER = typing___cast(Span.KindValue, 2) PRODUCER = typing___cast(Span.KindValue, 3) CONSUMER = typing___cast(Span.KindValue, 4) SPAN_KIND_UNSPECIFIED = typing___cast(Span.KindValue, 0) CLIENT = typing___cast(Span.KindValue, 1) SERVER = typing___cast(Span.KindValue, 2) PRODUCER = typing___cast(Span.KindValue, 3) CONSUMER = typing___cast(Span.KindValue, 4) type___Kind = Kind class TagsEntry(google___protobuf___message___Message): DESCRIPTOR: google___protobuf___descriptor___Descriptor = ... key: typing___Text = ... value: typing___Text = ... def __init__(self, *, key : typing___Optional[typing___Text] = None, value : typing___Optional[typing___Text] = None, ) -> None: ... if sys.version_info >= (3,): @classmethod def FromString(cls, s: builtin___bytes) -> Span.TagsEntry: ... else: @classmethod def FromString(cls, s: typing___Union[builtin___bytes, builtin___buffer, builtin___unicode]) -> Span.TagsEntry: ... def ClearField(self, field_name: typing_extensions___Literal[u"key",b"key",u"value",b"value"]) -> None: ... type___TagsEntry = TagsEntry trace_id: builtin___bytes = ... parent_id: builtin___bytes = ... id: builtin___bytes = ... kind: type___Span.KindValue = ... name: typing___Text = ... timestamp: builtin___int = ... duration: builtin___int = ... debug: builtin___bool = ... shared: builtin___bool = ... @property def local_endpoint(self) -> type___Endpoint: ... @property def remote_endpoint(self) -> type___Endpoint: ... @property def annotations(self) -> google___protobuf___internal___containers___RepeatedCompositeFieldContainer[type___Annotation]: ... @property def tags(self) -> typing___MutableMapping[typing___Text, typing___Text]: ... def __init__(self, *, trace_id : typing___Optional[builtin___bytes] = None, parent_id : typing___Optional[builtin___bytes] = None, id : typing___Optional[builtin___bytes] = None, kind : typing___Optional[type___Span.KindValue] = None, name : typing___Optional[typing___Text] = None, timestamp : typing___Optional[builtin___int] = None, duration : typing___Optional[builtin___int] = None, local_endpoint : typing___Optional[type___Endpoint] = None, remote_endpoint : typing___Optional[type___Endpoint] = None, annotations : typing___Optional[typing___Iterable[type___Annotation]] = None, tags : typing___Optional[typing___Mapping[typing___Text, typing___Text]] = None, debug : typing___Optional[builtin___bool] = None, shared : typing___Optional[builtin___bool] = None, ) -> None: ... if sys.version_info >= (3,): @classmethod def FromString(cls, s: builtin___bytes) -> Span: ... else: @classmethod def FromString(cls, s: typing___Union[builtin___bytes, builtin___buffer, builtin___unicode]) -> Span: ... def HasField(self, field_name: typing_extensions___Literal[u"local_endpoint",b"local_endpoint",u"remote_endpoint",b"remote_endpoint"]) -> builtin___bool: ... def ClearField(self, field_name: typing_extensions___Literal[u"annotations",b"annotations",u"debug",b"debug",u"duration",b"duration",u"id",b"id",u"kind",b"kind",u"local_endpoint",b"local_endpoint",u"name",b"name",u"parent_id",b"parent_id",u"remote_endpoint",b"remote_endpoint",u"shared",b"shared",u"tags",b"tags",u"timestamp",b"timestamp",u"trace_id",b"trace_id"]) -> None: ... type___Span = Span class Endpoint(google___protobuf___message___Message): DESCRIPTOR: google___protobuf___descriptor___Descriptor = ... service_name: typing___Text = ... ipv4: builtin___bytes = ... ipv6: builtin___bytes = ... port: builtin___int = ... def __init__(self, *, service_name : typing___Optional[typing___Text] = None, ipv4 : typing___Optional[builtin___bytes] = None, ipv6 : typing___Optional[builtin___bytes] = None, port : typing___Optional[builtin___int] = None, ) -> None: ... if sys.version_info >= (3,): @classmethod def FromString(cls, s: builtin___bytes) -> Endpoint: ... else: @classmethod def FromString(cls, s: typing___Union[builtin___bytes, builtin___buffer, builtin___unicode]) -> Endpoint: ... def ClearField(self, field_name: typing_extensions___Literal[u"ipv4",b"ipv4",u"ipv6",b"ipv6",u"port",b"port",u"service_name",b"service_name"]) -> None: ... type___Endpoint = Endpoint class Annotation(google___protobuf___message___Message): DESCRIPTOR: google___protobuf___descriptor___Descriptor = ... timestamp: builtin___int = ... value: typing___Text = ... def __init__(self, *, timestamp : typing___Optional[builtin___int] = None, value : typing___Optional[typing___Text] = None, ) -> None: ... if sys.version_info >= (3,): @classmethod def FromString(cls, s: builtin___bytes) -> Annotation: ... else: @classmethod def FromString(cls, s: typing___Union[builtin___bytes, builtin___buffer, builtin___unicode]) -> Annotation: ... def ClearField(self, field_name: typing_extensions___Literal[u"timestamp",b"timestamp",u"value",b"value"]) -> None: ... type___Annotation = Annotation class ListOfSpans(google___protobuf___message___Message): DESCRIPTOR: google___protobuf___descriptor___Descriptor = ... @property def spans(self) -> google___protobuf___internal___containers___RepeatedCompositeFieldContainer[type___Span]: ... def __init__(self, *, spans : typing___Optional[typing___Iterable[type___Span]] = None, ) -> None: ... if sys.version_info >= (3,): @classmethod def FromString(cls, s: builtin___bytes) -> ListOfSpans: ... else: @classmethod def FromString(cls, s: typing___Union[builtin___bytes, builtin___buffer, builtin___unicode]) -> ListOfSpans: ... def ClearField(self, field_name: typing_extensions___Literal[u"spans",b"spans"]) -> None: ... type___ListOfSpans = ListOfSpans class ReportResponse(google___protobuf___message___Message): DESCRIPTOR: google___protobuf___descriptor___Descriptor = ... def __init__(self, ) -> None: ... if sys.version_info >= (3,): @classmethod def FromString(cls, s: builtin___bytes) -> ReportResponse: ... else: @classmethod def FromString(cls, s: typing___Union[builtin___bytes, builtin___buffer, builtin___unicode]) -> ReportResponse: ... type___ReportResponse = ReportResponse version/000077500000000000000000000000001527070070700424445ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http__init__.py000066400000000000000000000001441527070070700445540ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 __version__ = "1.45.2" python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-zipkin-proto-http/test-requirements.txt000066400000000000000000000007231527070070700343200ustar00rootroot00000000000000asgiref==3.7.2 certifi==2024.12.14 charset-normalizer==3.3.2 idna==3.15 iniconfig==2.0.0 packaging==24.0 pluggy==1.6.0 protobuf==3.20.3 py-cpuinfo==9.0.0 pytest==9.0.3 requests==2.33.0 tomli==2.0.1 typing_extensions==4.10.0 urllib3==2.7.0 wrapt==1.16.0 -e opentelemetry-api -e exporter/opentelemetry-exporter-zipkin-json -e opentelemetry-sdk -e tests/opentelemetry-test-utils -e opentelemetry-semantic-conventions -e exporter/opentelemetry-exporter-zipkin-proto-http python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-zipkin-proto-http/tests/000077500000000000000000000000001527070070700312175ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-zipkin-proto-http/tests/__init__.py000066400000000000000000000001141527070070700333240ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-zipkin-proto-http/tests/encoder/000077500000000000000000000000001527070070700326365ustar00rootroot00000000000000__init__.py000066400000000000000000000000001527070070700346560ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-zipkin-proto-http/tests/encodercommon_tests.py000066400000000000000000000432201527070070700356440ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-zipkin-proto-http/tests/encoder# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import abc from opentelemetry import trace as trace_api from opentelemetry.exporter.zipkin.encoder import ( DEFAULT_MAX_TAG_VALUE_LENGTH, Encoder, ) from opentelemetry.exporter.zipkin.node_endpoint import NodeEndpoint from opentelemetry.sdk import trace from opentelemetry.sdk.resources import SERVICE_NAME, Resource from opentelemetry.sdk.util.instrumentation import InstrumentationScope from opentelemetry.test.test_base import TestBase from opentelemetry.trace import TraceFlags from opentelemetry.trace.status import Status, StatusCode TEST_SERVICE_NAME = "test_service" # pylint: disable=protected-access,too-many-public-methods class CommonEncoderTestCases: class CommonEncoderTest(TestBase): @staticmethod def create_tracer_provider(**kwargs): return TestBase.create_tracer_provider(resource=Resource({SERVICE_NAME: TEST_SERVICE_NAME}), **kwargs) @staticmethod @abc.abstractmethod def get_encoder(*args, **kwargs) -> Encoder: pass @classmethod def get_encoder_default(cls) -> Encoder: return cls.get_encoder() @abc.abstractmethod def test_encode_trace_id(self): pass @abc.abstractmethod def test_encode_span_id(self): pass @abc.abstractmethod def test_encode_local_endpoint_default(self): pass @abc.abstractmethod def test_encode_local_endpoint_explicits(self): pass @abc.abstractmethod def _test_encode_max_tag_length(self, max_tag_value_length: int): pass def test_encode_max_tag_length_2(self): self._test_encode_max_tag_length(2) def test_encode_max_tag_length_5(self): self._test_encode_max_tag_length(5) def test_encode_max_tag_length_9(self): self._test_encode_max_tag_length(9) def test_encode_max_tag_length_10(self): self._test_encode_max_tag_length(10) def test_encode_max_tag_length_11(self): self._test_encode_max_tag_length(11) def test_encode_max_tag_length_128(self): self._test_encode_max_tag_length(128) def test_constructor_default(self): encoder = self.get_encoder() self.assertEqual(DEFAULT_MAX_TAG_VALUE_LENGTH, encoder.max_tag_value_length) def test_constructor_max_tag_value_length(self): max_tag_value_length = 123456 encoder = self.get_encoder(max_tag_value_length) self.assertEqual(max_tag_value_length, encoder.max_tag_value_length) def test_nsec_to_usec_round(self): base_time_nsec = 683647322 * 10**9 for nsec in ( base_time_nsec, base_time_nsec + 150 * 10**6, base_time_nsec + 300 * 10**6, base_time_nsec + 400 * 10**6, ): self.assertEqual( (nsec + 500) // 10**3, self.get_encoder_default()._nsec_to_usec_round(nsec), ) def test_encode_debug(self): self.assertFalse( self.get_encoder_default()._encode_debug( trace_api.SpanContext( trace_id=0x000000000000000000000000DEADBEEF, span_id=0x00000000DEADBEF0, is_remote=False, trace_flags=TraceFlags(TraceFlags.DEFAULT), ) ) ) self.assertTrue( self.get_encoder_default()._encode_debug( trace_api.SpanContext( trace_id=0x000000000000000000000000DEADBEEF, span_id=0x00000000DEADBEF0, is_remote=False, trace_flags=TraceFlags(TraceFlags.SAMPLED), ) ) ) def test_get_parent_id_from_span(self): parent_id = 0x00000000DEADBEF0 self.assertEqual( parent_id, self.get_encoder_default()._get_parent_id( trace._Span( name="test-span", context=trace_api.SpanContext( 0x000000000000000000000000DEADBEEF, 0x04BF92DEEFC58C92, is_remote=False, ), parent=trace_api.SpanContext( 0x0000000000000000000000AADEADBEEF, parent_id, is_remote=False, ), ) ), ) def test_get_parent_id_from_span_context(self): parent_id = 0x00000000DEADBEF0 self.assertEqual( parent_id, self.get_encoder_default()._get_parent_id( trace_api.SpanContext( trace_id=0x000000000000000000000000DEADBEEF, span_id=parent_id, is_remote=False, ), ), ) @staticmethod def get_data_for_max_tag_length_test( max_tag_length: int, ) -> (trace._Span, dict): start_time = 683647322 * 10**9 # in ns duration = 50 * 10**6 end_time = start_time + duration span = trace._Span( name=TEST_SERVICE_NAME, context=trace_api.SpanContext( 0x0E0C63257DE34C926F9EFCD03927272E, 0x04BF92DEEFC58C92, is_remote=False, trace_flags=TraceFlags(TraceFlags.SAMPLED), ), resource=trace.Resource({}), ) span.start(start_time=start_time) span.set_attribute("string1", "v" * 500) span.set_attribute("string2", "v" * 50) span.set_attribute("list1", ["a"] * 25) span.set_attribute("list2", ["a"] * 10) span.set_attribute("list3", [2] * 25) span.set_attribute("list4", [2] * 10) span.set_attribute("list5", [True] * 25) span.set_attribute("list6", [True] * 10) span.set_attribute("tuple1", ("a",) * 25) span.set_attribute("tuple2", ("a",) * 10) span.set_attribute("tuple3", (2,) * 25) span.set_attribute("tuple4", (2,) * 10) span.set_attribute("tuple5", (True,) * 25) span.set_attribute("tuple6", (True,) * 10) span.set_attribute("range1", range(25)) span.set_attribute("range2", range(10)) span.set_attribute("empty_list", []) span.set_attribute("none_list", ["hello", None, "world"]) span.end(end_time=end_time) expected_outputs = { 2: { "string1": "vv", "string2": "vv", "list1": "[]", "list2": "[]", "list3": "[]", "list4": "[]", "list5": "[]", "list6": "[]", "tuple1": "[]", "tuple2": "[]", "tuple3": "[]", "tuple4": "[]", "tuple5": "[]", "tuple6": "[]", "range1": "[]", "range2": "[]", "empty_list": "[]", "none_list": "[]", }, 5: { "string1": "vvvvv", "string2": "vvvvv", "list1": '["a"]', "list2": '["a"]', "list3": '["2"]', "list4": '["2"]', "list5": "[]", "list6": "[]", "tuple1": '["a"]', "tuple2": '["a"]', "tuple3": '["2"]', "tuple4": '["2"]', "tuple5": "[]", "tuple6": "[]", "range1": '["0"]', "range2": '["0"]', "empty_list": "[]", "none_list": "[]", }, 9: { "string1": "vvvvvvvvv", "string2": "vvvvvvvvv", "list1": '["a","a"]', "list2": '["a","a"]', "list3": '["2","2"]', "list4": '["2","2"]', "list5": '["true"]', "list6": '["true"]', "tuple1": '["a","a"]', "tuple2": '["a","a"]', "tuple3": '["2","2"]', "tuple4": '["2","2"]', "tuple5": '["true"]', "tuple6": '["true"]', "range1": '["0","1"]', "range2": '["0","1"]', "empty_list": "[]", "none_list": '["hello"]', }, 10: { "string1": "vvvvvvvvvv", "string2": "vvvvvvvvvv", "list1": '["a","a"]', "list2": '["a","a"]', "list3": '["2","2"]', "list4": '["2","2"]', "list5": '["true"]', "list6": '["true"]', "tuple1": '["a","a"]', "tuple2": '["a","a"]', "tuple3": '["2","2"]', "tuple4": '["2","2"]', "tuple5": '["true"]', "tuple6": '["true"]', "range1": '["0","1"]', "range2": '["0","1"]', "empty_list": "[]", "none_list": '["hello"]', }, 11: { "string1": "vvvvvvvvvvv", "string2": "vvvvvvvvvvv", "list1": '["a","a"]', "list2": '["a","a"]', "list3": '["2","2"]', "list4": '["2","2"]', "list5": '["true"]', "list6": '["true"]', "tuple1": '["a","a"]', "tuple2": '["a","a"]', "tuple3": '["2","2"]', "tuple4": '["2","2"]', "tuple5": '["true"]', "tuple6": '["true"]', "range1": '["0","1"]', "range2": '["0","1"]', "empty_list": "[]", "none_list": '["hello"]', }, 128: { "string1": "v" * 128, "string2": "v" * 50, "list1": '["a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a"]', "list2": '["a","a","a","a","a","a","a","a","a","a"]', "list3": '["2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2"]', "list4": '["2","2","2","2","2","2","2","2","2","2"]', "list5": '["true","true","true","true","true","true","true","true","true","true","true","true","true","true","true","true","true","true"]', "list6": '["true","true","true","true","true","true","true","true","true","true"]', "tuple1": '["a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a"]', "tuple2": '["a","a","a","a","a","a","a","a","a","a"]', "tuple3": '["2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2"]', "tuple4": '["2","2","2","2","2","2","2","2","2","2"]', "tuple5": '["true","true","true","true","true","true","true","true","true","true","true","true","true","true","true","true","true","true"]', "tuple6": '["true","true","true","true","true","true","true","true","true","true"]', "range1": '["0","1","2","3","4","5","6","7","8","9","10","11","12","13","14","15","16","17","18","19","20","21","22","23","24"]', "range2": '["0","1","2","3","4","5","6","7","8","9"]', "empty_list": "[]", "none_list": '["hello",null,"world"]', }, } return span, expected_outputs[max_tag_length] @staticmethod def get_exhaustive_otel_span_list() -> list[trace._Span]: trace_id = 0x6E0C63257DE34C926F9EFCD03927272E base_time = 683647322 * 10**9 # in ns start_times = ( base_time, base_time + 150 * 10**6, base_time + 300 * 10**6, base_time + 400 * 10**6, ) end_times = ( start_times[0] + (50 * 10**6), start_times[1] + (100 * 10**6), start_times[2] + (200 * 10**6), start_times[3] + (300 * 10**6), ) parent_span_context = trace_api.SpanContext(trace_id, 0x1111111111111111, is_remote=False) other_context = trace_api.SpanContext(trace_id, 0x2222222222222222, is_remote=False) span1 = trace._Span( name="test-span-1", context=trace_api.SpanContext( trace_id, 0x34BF92DEEFC58C92, is_remote=False, trace_flags=TraceFlags(TraceFlags.SAMPLED), ), parent=parent_span_context, events=( trace.Event( name="event0", timestamp=base_time + 50 * 10**6, attributes={ "annotation_bool": True, "annotation_string": "annotation_test", "key_float": 0.3, }, ), ), links=(trace_api.Link(context=other_context, attributes={"key_bool": True}),), resource=trace.Resource({}), ) span1.start(start_time=start_times[0]) span1.set_attribute("key_bool", False) span1.set_attribute("key_string", "hello_world") span1.set_attribute("key_float", 111.22) span1.set_status(Status(StatusCode.OK)) span1.end(end_time=end_times[0]) span2 = trace._Span( name="test-span-2", context=parent_span_context, parent=None, resource=trace.Resource(attributes={"key_resource": "some_resource"}), ) span2.start(start_time=start_times[1]) span2.set_status(Status(StatusCode.ERROR, "Example description")) span2.end(end_time=end_times[1]) span3 = trace._Span( name="test-span-3", context=other_context, parent=None, resource=trace.Resource(attributes={"key_resource": "some_resource"}), ) span3.start(start_time=start_times[2]) span3.set_attribute("key_string", "hello_world") span3.end(end_time=end_times[2]) span4 = trace._Span( name="test-span-3", context=other_context, parent=None, resource=trace.Resource({}), instrumentation_scope=InstrumentationScope(name="name", version="version"), ) span4.start(start_time=start_times[3]) span4.end(end_time=end_times[3]) return [span1, span2, span3, span4] # pylint: disable=W0223 class CommonJsonEncoderTest(CommonEncoderTest, abc.ABC): def test_encode_trace_id(self): for trace_id in (1, 1024, 2**32, 2**64, 2**65): self.assertEqual( format(trace_id, "032x"), self.get_encoder_default()._encode_trace_id(trace_id), ) def test_encode_span_id(self): for span_id in (1, 1024, 2**8, 2**16, 2**32, 2**64): self.assertEqual( format(span_id, "016x"), self.get_encoder_default()._encode_span_id(span_id), ) def test_encode_local_endpoint_default(self): self.assertEqual( self.get_encoder_default()._encode_local_endpoint(NodeEndpoint()), {"serviceName": TEST_SERVICE_NAME}, ) def test_encode_local_endpoint_explicits(self): ipv4 = "192.168.0.1" ipv6 = "2001:db8::c001" port = 414120 self.assertEqual( self.get_encoder_default()._encode_local_endpoint(NodeEndpoint(ipv4, ipv6, port)), { "serviceName": TEST_SERVICE_NAME, "ipv4": ipv4, "ipv6": ipv6, "port": port, }, ) @staticmethod def pop_and_sort(source_list, source_index, sort_key): """ Convenience method that will pop a specified index from a list, sort it by a given key and then return it. """ popped_item = source_list.pop(source_index, None) if popped_item is not None: popped_item = sorted(popped_item, key=lambda x: x[sort_key]) return popped_item def assert_equal_encoded_spans(self, expected_spans, actual_spans): self.assertEqual(expected_spans, actual_spans) test_v2_protobuf.py000066400000000000000000000203131527070070700364360ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-zipkin-proto-http/tests/encoder# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import ipaddress import json from opentelemetry.exporter.zipkin.encoder import ( _SCOPE_NAME_KEY, _SCOPE_VERSION_KEY, NAME_KEY, VERSION_KEY, ) from opentelemetry.exporter.zipkin.node_endpoint import NodeEndpoint from opentelemetry.exporter.zipkin.proto.http.v2 import ProtobufEncoder from opentelemetry.exporter.zipkin.proto.http.v2.gen import zipkin_pb2 from opentelemetry.test.spantestutil import ( get_span_with_dropped_attributes_events_links, ) from opentelemetry.trace import SpanKind from .common_tests import ( # pylint: disable=import-error TEST_SERVICE_NAME, CommonEncoderTestCases, ) # pylint: disable=protected-access class TestProtobufEncoder(CommonEncoderTestCases.CommonEncoderTest): @staticmethod def get_encoder(*args, **kwargs) -> ProtobufEncoder: return ProtobufEncoder(*args, **kwargs) def test_encode_trace_id(self): for trace_id in (1, 1024, 2**32, 2**64, 2**127): self.assertEqual( self.get_encoder_default()._encode_trace_id(trace_id), trace_id.to_bytes(length=16, byteorder="big", signed=False), ) def test_encode_span_id(self): for span_id in (1, 1024, 2**8, 2**16, 2**32, 2**63): self.assertEqual( self.get_encoder_default()._encode_span_id(span_id), span_id.to_bytes(length=8, byteorder="big", signed=False), ) def test_encode_local_endpoint_default(self): self.assertEqual( ProtobufEncoder()._encode_local_endpoint(NodeEndpoint()), zipkin_pb2.Endpoint(service_name=TEST_SERVICE_NAME), ) def test_encode_local_endpoint_explicits(self): ipv4 = "192.168.0.1" ipv6 = "2001:db8::c001" port = 414120 self.assertEqual( ProtobufEncoder()._encode_local_endpoint(NodeEndpoint(ipv4, ipv6, port)), zipkin_pb2.Endpoint( service_name=TEST_SERVICE_NAME, ipv4=ipaddress.ip_address(ipv4).packed, ipv6=ipaddress.ip_address(ipv6).packed, port=port, ), ) def test_encode(self): local_endpoint = zipkin_pb2.Endpoint(service_name=TEST_SERVICE_NAME) span_kind = ProtobufEncoder.SPAN_KIND_MAP[SpanKind.INTERNAL] otel_spans = self.get_exhaustive_otel_span_list() trace_id = ProtobufEncoder._encode_trace_id(otel_spans[0].context.trace_id) expected_output = zipkin_pb2.ListOfSpans( spans=[ zipkin_pb2.Span( trace_id=trace_id, id=ProtobufEncoder._encode_span_id(otel_spans[0].context.span_id), name=otel_spans[0].name, timestamp=ProtobufEncoder._nsec_to_usec_round(otel_spans[0].start_time), duration=(ProtobufEncoder._nsec_to_usec_round(otel_spans[0].end_time - otel_spans[0].start_time)), local_endpoint=local_endpoint, kind=span_kind, tags={ "key_bool": "false", "key_string": "hello_world", "key_float": "111.22", "otel.status_code": "OK", }, debug=True, parent_id=ProtobufEncoder._encode_span_id(otel_spans[0].parent.span_id), annotations=[ zipkin_pb2.Annotation( timestamp=ProtobufEncoder._nsec_to_usec_round(otel_spans[0].events[0].timestamp), value=json.dumps( { "event0": { "annotation_bool": True, "annotation_string": "annotation_test", "key_float": 0.3, } }, sort_keys=True, ), ), ], ), zipkin_pb2.Span( trace_id=trace_id, id=ProtobufEncoder._encode_span_id(otel_spans[1].context.span_id), name=otel_spans[1].name, timestamp=ProtobufEncoder._nsec_to_usec_round(otel_spans[1].start_time), duration=(ProtobufEncoder._nsec_to_usec_round(otel_spans[1].end_time - otel_spans[1].start_time)), local_endpoint=local_endpoint, kind=span_kind, tags={ "key_resource": "some_resource", "otel.status_code": "ERROR", "error": "Example description", }, debug=False, ), zipkin_pb2.Span( trace_id=trace_id, id=ProtobufEncoder._encode_span_id(otel_spans[2].context.span_id), name=otel_spans[2].name, timestamp=ProtobufEncoder._nsec_to_usec_round(otel_spans[2].start_time), duration=(ProtobufEncoder._nsec_to_usec_round(otel_spans[2].end_time - otel_spans[2].start_time)), local_endpoint=local_endpoint, kind=span_kind, tags={ "key_string": "hello_world", "key_resource": "some_resource", }, debug=False, ), zipkin_pb2.Span( trace_id=trace_id, id=ProtobufEncoder._encode_span_id(otel_spans[3].context.span_id), name=otel_spans[3].name, timestamp=ProtobufEncoder._nsec_to_usec_round(otel_spans[3].start_time), duration=(ProtobufEncoder._nsec_to_usec_round(otel_spans[3].end_time - otel_spans[3].start_time)), local_endpoint=local_endpoint, kind=span_kind, tags={ NAME_KEY: "name", VERSION_KEY: "version", _SCOPE_NAME_KEY: "name", _SCOPE_VERSION_KEY: "version", }, debug=False, ), ], ) actual_output = zipkin_pb2.ListOfSpans.FromString(ProtobufEncoder().serialize(otel_spans, NodeEndpoint())) self.assertEqual(actual_output, expected_output) def _test_encode_max_tag_length(self, max_tag_value_length: int): otel_span, expected_tag_output = self.get_data_for_max_tag_length_test(max_tag_value_length) service_name = otel_span.name expected_output = zipkin_pb2.ListOfSpans( spans=[ zipkin_pb2.Span( trace_id=ProtobufEncoder._encode_trace_id(otel_span.context.trace_id), id=ProtobufEncoder._encode_span_id(otel_span.context.span_id), name=service_name, timestamp=ProtobufEncoder._nsec_to_usec_round(otel_span.start_time), duration=ProtobufEncoder._nsec_to_usec_round(otel_span.end_time - otel_span.start_time), local_endpoint=zipkin_pb2.Endpoint(service_name=service_name), kind=ProtobufEncoder.SPAN_KIND_MAP[SpanKind.INTERNAL], tags=expected_tag_output, annotations=None, debug=True, ) ] ) actual_output = zipkin_pb2.ListOfSpans.FromString( ProtobufEncoder(max_tag_value_length).serialize([otel_span], NodeEndpoint()) ) self.assertEqual(actual_output, expected_output) def test_dropped_span_attributes(self): otel_span = get_span_with_dropped_attributes_events_links() # pylint: disable=no-member tags = ProtobufEncoder()._encode_span(otel_span, zipkin_pb2.Endpoint()).tags self.assertEqual("1", tags["otel.dropped_links_count"]) self.assertEqual("2", tags["otel.dropped_attributes_count"]) self.assertEqual("3", tags["otel.dropped_events_count"]) test_zipkin_exporter.py000066400000000000000000000177601527070070700360200ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-zipkin-proto-http/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import ipaddress import os import unittest from unittest.mock import patch import requests from opentelemetry import trace from opentelemetry.exporter.zipkin.node_endpoint import NodeEndpoint from opentelemetry.exporter.zipkin.proto.http import ( DEFAULT_ENDPOINT, ZipkinExporter, ) from opentelemetry.exporter.zipkin.proto.http.v2 import ProtobufEncoder from opentelemetry.sdk.environment_variables import ( OTEL_EXPORTER_ZIPKIN_ENDPOINT, OTEL_EXPORTER_ZIPKIN_TIMEOUT, ) from opentelemetry.sdk.resources import SERVICE_NAME, Resource from opentelemetry.sdk.trace import TracerProvider, _Span from opentelemetry.sdk.trace.export import SpanExportResult TEST_SERVICE_NAME = "test_service" class MockResponse: def __init__(self, status_code): self.status_code = status_code self.text = status_code class TestZipkinExporter(unittest.TestCase): @classmethod def setUpClass(cls): trace.set_tracer_provider(TracerProvider(resource=Resource({SERVICE_NAME: TEST_SERVICE_NAME}))) def tearDown(self): os.environ.pop(OTEL_EXPORTER_ZIPKIN_ENDPOINT, None) os.environ.pop(OTEL_EXPORTER_ZIPKIN_TIMEOUT, None) def test_constructor_default(self): exporter = ZipkinExporter() self.assertIsInstance(exporter.encoder, ProtobufEncoder) self.assertIsInstance(exporter.session, requests.Session) self.assertEqual(exporter.endpoint, DEFAULT_ENDPOINT) self.assertEqual(exporter.local_node.service_name, TEST_SERVICE_NAME) self.assertEqual(exporter.local_node.ipv4, None) self.assertEqual(exporter.local_node.ipv6, None) self.assertEqual(exporter.local_node.port, None) def test_constructor_env_vars(self): os_endpoint = "https://foo:9911/path" os.environ[OTEL_EXPORTER_ZIPKIN_ENDPOINT] = os_endpoint os.environ[OTEL_EXPORTER_ZIPKIN_TIMEOUT] = "15" exporter = ZipkinExporter() self.assertEqual(exporter.endpoint, os_endpoint) self.assertEqual(exporter.local_node.service_name, TEST_SERVICE_NAME) self.assertEqual(exporter.local_node.ipv4, None) self.assertEqual(exporter.local_node.ipv6, None) self.assertEqual(exporter.local_node.port, None) self.assertEqual(exporter.timeout, 15) def test_constructor_protocol_endpoint(self): """Test the constructor for the common usage of providing the protocol and endpoint arguments.""" endpoint = "https://opentelemetry.io:15875/myapi/traces?format=zipkin" exporter = ZipkinExporter(endpoint) self.assertIsInstance(exporter.encoder, ProtobufEncoder) self.assertIsInstance(exporter.session, requests.Session) self.assertEqual(exporter.endpoint, endpoint) self.assertEqual(exporter.local_node.service_name, TEST_SERVICE_NAME) self.assertEqual(exporter.local_node.ipv4, None) self.assertEqual(exporter.local_node.ipv6, None) self.assertEqual(exporter.local_node.port, None) def test_constructor_all_params_and_env_vars(self): """Test the scenario where all params are provided and all OS env vars are set. Explicit params should take precedence. """ os_endpoint = "https://os.env.param:9911/path" os.environ[OTEL_EXPORTER_ZIPKIN_ENDPOINT] = os_endpoint os.environ[OTEL_EXPORTER_ZIPKIN_TIMEOUT] = "15" constructor_param_endpoint = "https://constructor.param:9911/path" local_node_ipv4 = "192.168.0.1" local_node_ipv6 = "2001:db8::1000" local_node_port = 30301 max_tag_value_length = 56 timeout_param = 20 session_param = requests.Session() exporter = ZipkinExporter( constructor_param_endpoint, local_node_ipv4, local_node_ipv6, local_node_port, max_tag_value_length, timeout_param, session_param, ) self.assertIsInstance(exporter.encoder, ProtobufEncoder) self.assertIsInstance(exporter.session, requests.Session) self.assertEqual(exporter.endpoint, constructor_param_endpoint) self.assertEqual(exporter.local_node.service_name, TEST_SERVICE_NAME) self.assertEqual(exporter.local_node.ipv4, ipaddress.IPv4Address(local_node_ipv4)) self.assertEqual(exporter.local_node.ipv6, ipaddress.IPv6Address(local_node_ipv6)) self.assertEqual(exporter.local_node.port, local_node_port) # Assert timeout passed in constructor is prioritized over env # when both are set. self.assertEqual(exporter.timeout, 20) @patch("requests.Session.post") def test_export_success(self, mock_post): mock_post.return_value = MockResponse(200) spans = [] exporter = ZipkinExporter() status = exporter.export(spans) self.assertEqual(SpanExportResult.SUCCESS, status) @patch("requests.Session.post") def test_export_invalid_response(self, mock_post): mock_post.return_value = MockResponse(404) spans = [] exporter = ZipkinExporter() status = exporter.export(spans) self.assertEqual(SpanExportResult.FAILURE, status) @patch("requests.Session.post") def test_export_span_service_name(self, mock_post): mock_post.return_value = MockResponse(200) resource = Resource.create({SERVICE_NAME: "test"}) context = trace.SpanContext( trace_id=0x000000000000000000000000DEADBEEF, span_id=0x00000000DEADBEF0, is_remote=False, ) span = _Span("test_span", context=context, resource=resource) span.start() span.end() exporter = ZipkinExporter() exporter.export([span]) self.assertEqual(exporter.local_node.service_name, "test") @patch("requests.Session.post") def test_export_shutdown(self, mock_post): mock_post.return_value = MockResponse(200) spans = [] exporter = ZipkinExporter() status = exporter.export(spans) self.assertEqual(SpanExportResult.SUCCESS, status) exporter.shutdown() # Any call to .export() post shutdown should return failure status = exporter.export(spans) self.assertEqual(SpanExportResult.FAILURE, status) @patch("requests.Session.post") def test_export_timeout(self, mock_post): mock_post.return_value = MockResponse(200) spans = [] exporter = ZipkinExporter(timeout=2) status = exporter.export(spans) self.assertEqual(SpanExportResult.SUCCESS, status) mock_post.assert_called_with(url="http://localhost:9411/api/v2/spans", data=b"", timeout=2) class TestZipkinNodeEndpoint(unittest.TestCase): def test_constructor_default(self): node_endpoint = NodeEndpoint() self.assertEqual(node_endpoint.ipv4, None) self.assertEqual(node_endpoint.ipv6, None) self.assertEqual(node_endpoint.port, None) self.assertEqual(node_endpoint.service_name, TEST_SERVICE_NAME) def test_constructor_explicits(self): ipv4 = "192.168.0.1" ipv6 = "2001:db8::c001" port = 414120 node_endpoint = NodeEndpoint(ipv4, ipv6, port) self.assertEqual(node_endpoint.ipv4, ipaddress.IPv4Address(ipv4)) self.assertEqual(node_endpoint.ipv6, ipaddress.IPv6Address(ipv6)) self.assertEqual(node_endpoint.port, port) self.assertEqual(node_endpoint.service_name, TEST_SERVICE_NAME) def test_ipv4_invalid_raises_error(self): with self.assertRaises(ValueError): NodeEndpoint(ipv4="invalid-ipv4-address") def test_ipv4_passed_ipv6_raises_error(self): with self.assertRaises(ValueError): NodeEndpoint(ipv4="2001:db8::c001") def test_ipv6_invalid_raises_error(self): with self.assertRaises(ValueError): NodeEndpoint(ipv6="invalid-ipv6-address") def test_ipv6_passed_ipv4_raises_error(self): with self.assertRaises(ValueError): NodeEndpoint(ipv6="192.168.0.1") python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-zipkin/000077500000000000000000000000001527070070700257375ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-zipkin/LICENSE000066400000000000000000000261351527070070700267530ustar00rootroot00000000000000 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. python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-zipkin/README.rst000066400000000000000000000017011527070070700274250ustar00rootroot00000000000000OpenTelemetry Zipkin Exporter ============================= |pypi| .. |pypi| image:: https://badge.fury.io/py/opentelemetry-exporter-zipkin.svg :target: https://pypi.org/project/opentelemetry-exporter-zipkin/ This library is provided as a convenience to install all supported OpenTelemetry Zipkin Exporters. Currently it installs: * opentelemetry-exporter-zipkin-json * opentelemetry-exporter-zipkin-proto-http In the future, additional packages may be available: * opentelemetry-exporter-zipkin-thrift To avoid unnecessary dependencies, users should install the specific package once they've determined their preferred serialization method. Installation ------------ :: pip install opentelemetry-exporter-zipkin References ---------- * `OpenTelemetry Zipkin Exporter `_ * `Zipkin `_ * `OpenTelemetry Project `_ python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-zipkin/pyproject.toml000066400000000000000000000030211527070070700306470ustar00rootroot00000000000000[build-system] requires = ["hatchling"] build-backend = "hatchling.build" [project] name = "opentelemetry-exporter-zipkin" dynamic = ["version"] description = "Zipkin Span Exporters 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", "Framework :: OpenTelemetry", "Framework :: OpenTelemetry :: Exporters", "Intended Audience :: Developers", "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-exporter-zipkin-json == 1.45.2", "opentelemetry-exporter-zipkin-proto-http == 1.45.2", ] [project.entry-points.opentelemetry_traces_exporter] zipkin = "opentelemetry.exporter.zipkin.proto.http:ZipkinExporter" [project.urls] Homepage = "https://github.com/open-telemetry/opentelemetry-python/tree/main/exporter/opentelemetry-exporter-zipkin" Repository = "https://github.com/open-telemetry/opentelemetry-python" [tool.hatch.version] path = "src/opentelemetry/exporter/zipkin/version/__init__.py" [tool.hatch.build.targets.sdist] include = [ "/src", "/tests", ] [tool.hatch.build.targets.wheel] packages = ["src/opentelemetry"] python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-zipkin/src/000077500000000000000000000000001527070070700265265ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/000077500000000000000000000000001527070070700314225ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/000077500000000000000000000000001527070070700332725ustar00rootroot00000000000000zipkin/000077500000000000000000000000001527070070700345175ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporterpy.typed000066400000000000000000000000001527070070700362040ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkinversion/000077500000000000000000000000001527070070700362045ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin__init__.py000066400000000000000000000001441527070070700403140ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 __version__ = "1.45.2" python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-zipkin/test-requirements.txt000066400000000000000000000005611527070070700322020ustar00rootroot00000000000000asgiref==3.7.2 iniconfig==2.0.0 packaging==24.0 pluggy==1.6.0 py-cpuinfo==9.0.0 pytest==9.0.3 tomli==2.0.1 typing_extensions==4.10.0 wrapt==1.16.0 -e opentelemetry-api -e exporter/opentelemetry-exporter-zipkin-json -e exporter/opentelemetry-exporter-zipkin-proto-http -e opentelemetry-sdk -e opentelemetry-semantic-conventions -e exporter/opentelemetry-exporter-zipkin python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-zipkin/tests/000077500000000000000000000000001527070070700271015ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-zipkin/tests/__init__.py000066400000000000000000000000001527070070700312000ustar00rootroot00000000000000python-opentelemetry-1.45.2/exporter/opentelemetry-exporter-zipkin/tests/test_zipkin.py000066400000000000000000000007211527070070700320160ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import unittest from opentelemetry.exporter.zipkin import json from opentelemetry.exporter.zipkin.proto import http class TestZipkinExporter(unittest.TestCase): def test_constructors(self): try: json.ZipkinExporter() http.ZipkinExporter() except Exception as exc: # pylint: disable=broad-exception-caught self.assertIsNone(exc) python-opentelemetry-1.45.2/gen-requirements.txt000066400000000000000000000005401527070070700220550ustar00rootroot00000000000000# 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 python-opentelemetry-1.45.2/opentelemetry-api/000077500000000000000000000000001527070070700214665ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-api/LICENSE000066400000000000000000000261351527070070700225020ustar00rootroot00000000000000 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. python-opentelemetry-1.45.2/opentelemetry-api/README.rst000066400000000000000000000005711527070070700231600ustar00rootroot00000000000000OpenTelemetry Python API ============================================================================ |pypi| .. |pypi| image:: https://badge.fury.io/py/opentelemetry-api.svg :target: https://pypi.org/project/opentelemetry-api/ Installation ------------ :: pip install opentelemetry-api References ---------- * `OpenTelemetry Project `_ python-opentelemetry-1.45.2/opentelemetry-api/pyproject.toml000066400000000000000000000036711527070070700244110ustar00rootroot00000000000000[build-system] requires = ["hatchling"] build-backend = "hatchling.build" [project] name = "opentelemetry-api" description = "OpenTelemetry Python API" 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", "Framework :: OpenTelemetry", "Intended Audience :: Developers", "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 = [ "typing-extensions >= 4.5.0", ] dynamic = [ "version", ] [project.entry-points.opentelemetry_context] contextvars_context = "opentelemetry.context.contextvars_context:ContextVarsRuntimeContext" [project.entry-points.opentelemetry_environment_variables] api = "opentelemetry.environment_variables" [project.entry-points.opentelemetry_meter_provider] default_meter_provider = "opentelemetry.metrics:NoOpMeterProvider" [project.entry-points.opentelemetry_propagator] baggage = "opentelemetry.baggage.propagation:W3CBaggagePropagator" tracecontext = "opentelemetry.trace.propagation.tracecontext:TraceContextTextMapPropagator" [project.entry-points.opentelemetry_tracer_provider] default_tracer_provider = "opentelemetry.trace:NoOpTracerProvider" [project.urls] Homepage = "https://github.com/open-telemetry/opentelemetry-python/tree/main/opentelemetry-api" Repository = "https://github.com/open-telemetry/opentelemetry-python" [tool.hatch.version] path = "src/opentelemetry/version/__init__.py" [tool.hatch.build.targets.sdist] include = [ "/src", "/tests", ] [tool.hatch.build.targets.wheel] packages = ["src/opentelemetry"] python-opentelemetry-1.45.2/opentelemetry-api/src/000077500000000000000000000000001527070070700222555ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-api/src/opentelemetry/000077500000000000000000000000001527070070700251515ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-api/src/opentelemetry/_logs/000077500000000000000000000000001527070070700262545ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-api/src/opentelemetry/_logs/__init__.py000066400000000000000000000025261527070070700303720ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 """ The OpenTelemetry logging API describes the classes used to generate logs and events. The :class:`.LoggerProvider` provides users access to the :class:`.Logger`. This module provides abstract (i.e. unimplemented) classes required for logging, and a concrete no-op implementation :class:`.NoOpLogger` that allows applications to use the API package alone without a supporting implementation. To get a logger, you need to provide the package name from which you are calling the logging APIs to OpenTelemetry by calling `LoggerProvider.get_logger` with the calling module name and the version of your package. The following code shows how to obtain a logger using the global :class:`.LoggerProvider`:: from opentelemetry._logs import get_logger logger = get_logger("example-logger") .. versionadded:: 1.15.0 """ from opentelemetry._logs._internal import ( Logger, LoggerProvider, LogRecord, NoOpLogger, NoOpLoggerProvider, get_logger, get_logger_provider, set_logger_provider, ) from opentelemetry._logs.severity import SeverityNumber __all__ = [ "LogRecord", "Logger", "LoggerProvider", "NoOpLogger", "NoOpLoggerProvider", "SeverityNumber", "get_logger", "get_logger_provider", "set_logger_provider", ] python-opentelemetry-1.45.2/opentelemetry-api/src/opentelemetry/_logs/_internal/000077500000000000000000000000001527070070700302275ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-api/src/opentelemetry/_logs/_internal/__init__.py000066400000000000000000000361551527070070700323520ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 """ The OpenTelemetry logging API describes the classes used to generate logs and events. The :class:`.LoggerProvider` provides users access to the :class:`.Logger`. This module provides abstract (i.e. unimplemented) classes required for logging, and a concrete no-op implementation :class:`.NoOpLogger` that allows applications to use the API package alone without a supporting implementation. To get a logger, you need to provide the package name from which you are calling the logging APIs to OpenTelemetry by calling `LoggerProvider.get_logger` with the calling module name and the version of your package. The following code shows how to obtain a logger using the global :class:`.LoggerProvider`:: from opentelemetry._logs import get_logger logger = get_logger("example-logger") .. versionadded:: 1.15.0 """ from __future__ import annotations from abc import ABC, abstractmethod from logging import getLogger from os import environ from time import time_ns from typing import cast, overload from typing_extensions import deprecated from opentelemetry._logs.severity import SeverityNumber from opentelemetry.context import get_current from opentelemetry.context.context import Context from opentelemetry.environment_variables import _OTEL_PYTHON_LOGGER_PROVIDER from opentelemetry.trace import get_current_span from opentelemetry.trace.span import TraceFlags from opentelemetry.util._once import Once from opentelemetry.util._providers import _load_provider from opentelemetry.util.types import AnyValue, Attributes _logger = getLogger(__name__) class LogRecord(ABC): """A LogRecord instance represents an event being logged. LogRecord instances are created and emitted via `Logger` every time something is logged. They contain all the information pertinent to the event being logged. """ @overload def __init__( self, *, timestamp: int | None = None, observed_timestamp: int | None = None, context: Context | None = None, severity_text: str | None = None, severity_number: SeverityNumber | None = None, body: AnyValue = None, attributes: Attributes = None, event_name: str | None = None, exception: BaseException | None = None, ) -> None: ... @overload @deprecated( "LogRecord init with `trace_id`, `span_id`, and/or `trace_flags` is deprecated since 1.35.0. Use `context` instead." ) def __init__( self, *, timestamp: int | None = None, observed_timestamp: int | None = None, trace_id: int | None = None, span_id: int | None = None, trace_flags: TraceFlags | None = None, severity_text: str | None = None, severity_number: SeverityNumber | None = None, body: AnyValue = None, attributes: Attributes = None, ) -> None: ... def __init__( self, *, timestamp: int | None = None, observed_timestamp: int | None = None, context: Context | None = None, trace_id: int | None = None, span_id: int | None = None, trace_flags: TraceFlags | None = None, severity_text: str | None = None, severity_number: SeverityNumber | None = None, body: AnyValue = None, attributes: Attributes = None, event_name: str | None = None, exception: BaseException | None = None, ) -> None: if not context: context = get_current() span_context = get_current_span(context).get_span_context() self.timestamp = timestamp if observed_timestamp is None: observed_timestamp = time_ns() self.observed_timestamp = observed_timestamp self.context = context self.trace_id = trace_id or span_context.trace_id self.span_id = span_id or span_context.span_id self.trace_flags = trace_flags or span_context.trace_flags self.severity_text = severity_text self.severity_number = severity_number self.body = body self.attributes = attributes self.event_name = event_name self.exception = exception class Logger(ABC): """Handles emitting events and logs via `LogRecord`.""" def __init__( self, name: str, version: str | None = None, schema_url: str | None = None, attributes: Attributes = None, ) -> None: super().__init__() self._name = name self._version = version self._schema_url = schema_url self._attributes = attributes @overload def emit( self, *, timestamp: int | None = None, observed_timestamp: int | None = None, context: Context | None = None, severity_number: SeverityNumber | None = None, severity_text: str | None = None, body: AnyValue | None = None, attributes: Attributes = None, event_name: str | None = None, exception: BaseException | None = None, ) -> None: ... @overload def emit( self, record: LogRecord, ) -> None: ... @abstractmethod def emit( self, record: LogRecord | None = None, *, timestamp: int | None = None, observed_timestamp: int | None = None, context: Context | None = None, severity_number: SeverityNumber | None = None, severity_text: str | None = None, body: AnyValue | None = None, attributes: Attributes = None, event_name: str | None = None, exception: BaseException | None = None, ) -> None: """Emits a :class:`LogRecord` representing a log to the processing pipeline.""" @abstractmethod def enabled( self, *, context: Context | None = None, severity_number: SeverityNumber | None = None, event_name: str | None = None, ) -> bool: """Returns whether the logger is enabled for the given arguments. Instrumentation should call this before performing expensive work to construct a log record, and skip that work if ``False`` is returned. The returned value may change over time and should be checked each time before emitting a log record. """ class NoOpLogger(Logger): """The default Logger used when no Logger implementation is available. All operations are no-op. """ @overload def emit( self, *, timestamp: int | None = None, observed_timestamp: int | None = None, context: Context | None = None, severity_number: SeverityNumber | None = None, severity_text: str | None = None, body: AnyValue | None = None, attributes: Attributes = None, event_name: str | None = None, exception: BaseException | None = None, ) -> None: ... @overload def emit( # pylint:disable=arguments-differ self, record: LogRecord, ) -> None: ... def emit( self, record: LogRecord | None = None, *, timestamp: int | None = None, observed_timestamp: int | None = None, context: Context | None = None, severity_number: SeverityNumber | None = None, severity_text: str | None = None, body: AnyValue | None = None, attributes: Attributes = None, event_name: str | None = None, exception: BaseException | None = None, ) -> None: pass def enabled( self, *, context: Context | None = None, severity_number: SeverityNumber | None = None, event_name: str | None = None, ) -> bool: return False class ProxyLogger(Logger): def __init__( # pylint: disable=super-init-not-called self, name: str, version: str | None = None, schema_url: str | None = None, attributes: Attributes = None, ): self._name = name self._version = version self._schema_url = schema_url self._attributes = attributes self._real_logger: Logger | None = None self._noop_logger = NoOpLogger(name) @property def _logger(self) -> Logger: if self._real_logger: return self._real_logger if _LOGGER_PROVIDER: self._real_logger = _LOGGER_PROVIDER.get_logger( self._name, self._version, self._schema_url, self._attributes, ) return self._real_logger return self._noop_logger @overload def emit( self, *, timestamp: int | None = None, observed_timestamp: int | None = None, context: Context | None = None, severity_number: SeverityNumber | None = None, severity_text: str | None = None, body: AnyValue | None = None, attributes: Attributes = None, event_name: str | None = None, exception: BaseException | None = None, ) -> None: ... @overload def emit( # pylint:disable=arguments-differ self, record: LogRecord, ) -> None: ... def emit( self, record: LogRecord | None = None, *, timestamp: int | None = None, observed_timestamp: int | None = None, context: Context | None = None, severity_number: SeverityNumber | None = None, severity_text: str | None = None, body: AnyValue | None = None, attributes: Attributes = None, event_name: str | None = None, exception: BaseException | None = None, ) -> None: if record: self._logger.emit(record) else: self._logger.emit( timestamp=timestamp, observed_timestamp=observed_timestamp, context=context, severity_number=severity_number, severity_text=severity_text, body=body, attributes=attributes, event_name=event_name, exception=exception, ) def enabled( self, *, context: Context | None = None, severity_number: SeverityNumber | None = None, event_name: str | None = None, ) -> bool: return self._logger.enabled( context=context, severity_number=severity_number, event_name=event_name, ) class LoggerProvider(ABC): """ LoggerProvider is the entry point of the API. It provides access to Logger instances. """ @abstractmethod def get_logger( self, name: str, version: str | None = None, schema_url: str | None = None, attributes: Attributes = None, ) -> Logger: """Returns a `Logger` for use by the given instrumentation library. For any two calls with identical parameters, it is undefined whether the same or different `Logger` instances are returned. This function may return different `Logger` types (e.g. a no-op logger vs. a functional logger). Args: name: The name of the instrumenting module, package or class. This should *not* be the name of the module, package or class that is instrumented but the name of the code doing the instrumentation. E.g., instead of ``"requests"``, use ``"opentelemetry.instrumentation.requests"``. For log sources which define a logger name (e.g. logging.Logger.name) the Logger Name should be recorded as the instrumentation scope name. version: Optional. The version string of the instrumenting library. Usually this should be the same as ``importlib.metadata.version(instrumenting_library_name)``. schema_url: Optional. Specifies the Schema URL of the emitted telemetry. attributes: Optional. Specifies the instrumentation scope attributes to associate with emitted telemetry. """ class NoOpLoggerProvider(LoggerProvider): """The default LoggerProvider used when no LoggerProvider implementation is available.""" def get_logger( self, name: str, version: str | None = None, schema_url: str | None = None, attributes: Attributes = None, ) -> Logger: """Returns a NoOpLogger.""" return NoOpLogger(name, version=version, schema_url=schema_url, attributes=attributes) class ProxyLoggerProvider(LoggerProvider): def get_logger( self, name: str, version: str | None = None, schema_url: str | None = None, attributes: Attributes = None, ) -> Logger: if _LOGGER_PROVIDER: return _LOGGER_PROVIDER.get_logger( name, version=version, schema_url=schema_url, attributes=attributes, ) return ProxyLogger( name, version=version, schema_url=schema_url, attributes=attributes, ) _LOGGER_PROVIDER_SET_ONCE = Once() _LOGGER_PROVIDER: LoggerProvider | None = None _PROXY_LOGGER_PROVIDER = ProxyLoggerProvider() def get_logger_provider() -> LoggerProvider: """Gets the current global :class:`~.LoggerProvider` object.""" global _LOGGER_PROVIDER # pylint: disable=global-variable-not-assigned if _LOGGER_PROVIDER is None: if _OTEL_PYTHON_LOGGER_PROVIDER not in environ: return _PROXY_LOGGER_PROVIDER logger_provider: LoggerProvider = _load_provider( # type: ignore _OTEL_PYTHON_LOGGER_PROVIDER, "logger_provider" ) _set_logger_provider(logger_provider, log=False) # _LOGGER_PROVIDER will have been set by one thread return cast("LoggerProvider", _LOGGER_PROVIDER) def _set_logger_provider(logger_provider: LoggerProvider, log: bool) -> None: def set_lp() -> None: global _LOGGER_PROVIDER # pylint: disable=global-statement _LOGGER_PROVIDER = logger_provider did_set = _LOGGER_PROVIDER_SET_ONCE.do_once(set_lp) if log and not did_set: _logger.warning("Overriding of current LoggerProvider is not allowed") def set_logger_provider(logger_provider: LoggerProvider) -> None: """Sets the current global :class:`~.LoggerProvider` object. This can only be done once, a warning will be logged if any further attempt is made. """ _set_logger_provider(logger_provider, log=True) def get_logger( instrumenting_module_name: str, instrumenting_library_version: str = "", logger_provider: LoggerProvider | None = None, schema_url: str | None = None, attributes: Attributes = None, ) -> Logger: """Returns a `Logger` for use within a python process. This function is a convenience wrapper for opentelemetry.sdk._logs.LoggerProvider.get_logger. If logger_provider param is omitted the current configured one is used. """ if logger_provider is None: logger_provider = get_logger_provider() return logger_provider.get_logger( instrumenting_module_name, instrumenting_library_version, schema_url, attributes, ) python-opentelemetry-1.45.2/opentelemetry-api/src/opentelemetry/_logs/py.typed000066400000000000000000000000001527070070700277410ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-api/src/opentelemetry/_logs/severity/000077500000000000000000000000001527070070700301265ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-api/src/opentelemetry/_logs/severity/__init__.py000066400000000000000000000017571527070070700322510ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import enum class SeverityNumber(enum.Enum): """Numerical value of severity. Smaller numerical values correspond to less severe events (such as debug events), larger numerical values correspond to more severe events (such as errors and critical events). See the `Log Data Model`_ spec for more info and how to map the severity from source format to OTLP Model. .. _Log Data Model: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/logs/data-model.md#field-severitynumber """ UNSPECIFIED = 0 TRACE = 1 TRACE2 = 2 TRACE3 = 3 TRACE4 = 4 DEBUG = 5 DEBUG2 = 6 DEBUG3 = 7 DEBUG4 = 8 INFO = 9 INFO2 = 10 INFO3 = 11 INFO4 = 12 WARN = 13 WARN2 = 14 WARN3 = 15 WARN4 = 16 ERROR = 17 ERROR2 = 18 ERROR3 = 19 ERROR4 = 20 FATAL = 21 FATAL2 = 22 FATAL3 = 23 FATAL4 = 24 python-opentelemetry-1.45.2/opentelemetry-api/src/opentelemetry/attributes/000077500000000000000000000000001527070070700273375ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-api/src/opentelemetry/attributes/__init__.py000066400000000000000000000224371527070070700314600ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # pyright: reportUnnecessaryIsInstance=false import copy import logging import threading from collections.abc import Mapping, MutableMapping, Sequence from types import NoneType from typing import TYPE_CHECKING, Any, overload from typing_extensions import assert_never, deprecated from opentelemetry.util import types _logger = logging.getLogger(__name__) # Calling str(x) will use an object's `__str__` method if it exists, otherwise it will use it's `__repr__` method. # If neither is defined it uses the base class's `object.__repr__` method, which returns a string that is hard to understand. # So in that case we drop the key/value pair. def _is_non_custom_str(key: Any) -> bool: return type(key).__str__ is not object.__str__ or type(key).__repr__ is not object.__repr__ @overload def _clean_attribute_value( value: Mapping[str, types.AnyValue], max_string_value_length: int | None, ) -> Mapping[str, types.AnyValue]: ... @overload def _clean_attribute_value( value: types.AnyValue, max_string_value_length: int | None, ) -> types.AnyValue: ... def _clean_attribute_value( value: types.AnyValue, max_string_value_length: int | None, ) -> types.AnyValue: """Recursively checks if an attribute value is valid and cleans it if required. String values are truncated to max_string_value_length if provided. Anything that isn't of `types.AnyValue`, we attempt to cast to `str`. If this fails, the value is replaced with None. Sequence's are converted to tuples and mappings are copied into new dicts. Returns: The recursively cleaned AnyValue. """ if isinstance(value, (NoneType, bool, int, float, bytes)): return value if isinstance(value, str): if max_string_value_length is not None and len(value) > max_string_value_length: _logger.warning( "String attribute value exceeds max length of %d, truncating.", max_string_value_length, ) value = value[:max_string_value_length] return value if isinstance(value, Sequence): return tuple(_clean_attribute_value(v, max_string_value_length) for v in value) if isinstance(value, Mapping): cleaned_mapping: dict[str, types.AnyValue] = {} for key, val in value.items(): if not key: _logger.warning( "invalid attribute key `%s`. must be non-empty string. Dropping key from attributes.", key, ) continue # Spec says to convert unknown types to strings if possible (here and below too). if not isinstance(key, str): _logger.warning( "Invalid type `%s` for attribute key `%s`, must be a str. Key's `__str__/__repr__` method will be called if it exists, otherwise the key/value pair will be dropped.", type(key), key, ) if _is_non_custom_str(key): key = str(key) else: continue cleaned_mapping[key] = _clean_attribute_value(val, max_string_value_length) return cleaned_mapping if TYPE_CHECKING: assert_never(value) _logger.warning( "Invalid type `%s` for attribute value. Expected one of bool, str, None, bytes, int, float or a " "Mapping or Sequence of those types. Value's __str__ method will be called if it exists, otherwise the value will be replaced with None.", type(value), ) if _is_non_custom_str(value): return str(value) return None class BoundedAttributes(MutableMapping[str, types.AnyValue]): """A dict with a fixed max capacity which cleans and potentially drops values to ensure they are valid attribute values. Args: maxlen: The maximum number of attributes to store, use None for no limit. attributes: The initial attributes to store. immutable: Defaults to true. Whether to allow adding/removing of attributes after the initialisation of the instance. max_value_len: The maximum length of string values, use None for no limit. extended_attributes: Deprecated. Kept for backwards compatibility. Extended attributes are now always used for attributes everywhere. When the dict is full and a new element is added, the oldest element is dropped. Attributes are made to be immutable when set in this container. So passing a mutable list as an attribute value, and then mutating it after will not change it's value in this container. """ @overload def __init__( self, maxlen: int | None = None, attributes: types.Attributes = None, immutable: bool = True, max_value_len: int | None = None, ) -> None: ... @overload @deprecated( "Creating BoundedAttributes with `extended_attributes` set is deprecated. " "The `extended_attributes` param is no longer used and will be removed " "in a future release. Extended attributes are now always used for attributes everywhere." ) def __init__( self, maxlen: int | None = None, attributes: types.Attributes = None, immutable: bool = True, max_value_len: int | None = None, extended_attributes: bool = False, ) -> None: ... def __init__( self, maxlen: int | None = None, attributes: types.Attributes = None, immutable: bool = True, max_value_len: int | None = None, extended_attributes: bool = False, ) -> None: if maxlen is not None and maxlen < 0: raise ValueError("maxlen must be valid int greater or equal to 0") if max_value_len is not None and max_value_len < 0: raise ValueError("max_value_len must be valid int greater or equal to 0") self._dict: dict[str, types.AnyValue] = {} self.maxlen = maxlen self.dropped = 0 self.max_value_len = max_value_len self._lock = threading.Lock() # setting False before adding items self._immutable = False if attributes: self._set_items(attributes) self._immutable = immutable def __repr__(self) -> str: return f"{dict(self._dict)}" def __getitem__(self, key: str) -> types.AnyValue: return self._dict[key] def _raise_if_immutable(self) -> None: if self._immutable: raise TypeError("Cannot mutate immutable BoundedAttributes") def __setitem__(self, key: str, value: types.AnyValue) -> None: self._raise_if_immutable() if self.maxlen is not None and self.maxlen == 0: with self._lock: self.dropped += 1 return if not key or not isinstance(key, str): _logger.warning( "invalid key `%s`. must be non-empty string. Dropping key from attributes.", key, ) with self._lock: self.dropped += 1 return cleaned = _clean_attribute_value(value, self.max_value_len) with self._lock: self._setitem_locked(key, cleaned) def _set_items(self, attributes: Mapping[str, types.AnyValue]) -> None: self._raise_if_immutable() if self.maxlen is not None and self.maxlen == 0: with self._lock: self.dropped += len(attributes) return cleaned_attributes: Mapping[str, types.AnyValue] = _clean_attribute_value(attributes, self.max_value_len) with self._lock: self.dropped += len(attributes) - len(cleaned_attributes) for key, value in cleaned_attributes.items(): self._setitem_locked(key, value) def _setitem_locked(self, key: str, value: types.AnyValue) -> None: if key in self._dict: del self._dict[key] if self.maxlen is not None and len(self._dict) >= self.maxlen: _logger.warning( "Attributes dict is full. Dropping the oldest key-value pair from attributes to make space for the new key-value pair.", ) # Dictionaries are insertion ordered in Python, this is the recommended way to get the oldest value. del self._dict[next(iter(self._dict.keys()))] self.dropped += 1 self._dict[key] = value def __delitem__(self, key: str) -> None: self._raise_if_immutable() del self._dict[key] def __iter__(self): if self._immutable: return iter(self._dict) with self._lock: return iter(list(self._dict)) def __len__(self) -> int: return len(self._dict) def __deepcopy__(self, memo: dict[int, Any]) -> "BoundedAttributes": copy_ = BoundedAttributes( maxlen=self.maxlen, immutable=self._immutable, max_value_len=self.max_value_len, ) memo[id(self)] = copy_ with self._lock: # Assign _dict directly to avoid re-cleaning already clean values # and to bypass the immutability guard in __setitem__ copy_._dict = copy.deepcopy(self._dict, memo) copy_.dropped = self.dropped return copy_ def copy(self) -> MutableMapping[str, types.AnyValue]: return self._dict.copy() python-opentelemetry-1.45.2/opentelemetry-api/src/opentelemetry/attributes/py.typed000066400000000000000000000000001527070070700310240ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-api/src/opentelemetry/baggage/000077500000000000000000000000001527070070700265265ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-api/src/opentelemetry/baggage/__init__.py000066400000000000000000000067541527070070700306530ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from collections.abc import Mapping from logging import getLogger from re import compile from types import MappingProxyType from opentelemetry.context import create_key, get_value, set_value from opentelemetry.context.context import Context from opentelemetry.util.re import ( _BAGGAGE_PROPERTY_FORMAT, _KEY_FORMAT, _VALUE_FORMAT, ) _BAGGAGE_KEY = create_key("baggage") _logger = getLogger(__name__) _KEY_PATTERN = compile(_KEY_FORMAT) _VALUE_PATTERN = compile(_VALUE_FORMAT) _PROPERT_PATTERN = compile(_BAGGAGE_PROPERTY_FORMAT) def get_all( context: Context | None = None, ) -> Mapping[str, object]: """Returns the name/value pairs in the Baggage Args: context: The Context to use. If not set, uses current Context Returns: The name/value pairs in the Baggage """ return MappingProxyType(_get_baggage_value(context=context)) def get_baggage(name: str, context: Context | None = None) -> object | None: """Provides access to the value for a name/value pair in the Baggage Args: name: The name of the value to retrieve context: The Context to use. If not set, uses current Context Returns: The value associated with the given name, or null if the given name is not present. """ return _get_baggage_value(context=context).get(name) def set_baggage(name: str, value: object, context: Context | None = None) -> Context: """Sets a value in the Baggage Args: name: The name of the value to set value: The value to set context: The Context to use. If not set, uses current Context Returns: A Context with the value updated """ baggage = _get_baggage_value(context=context).copy() baggage[name] = value return set_value(_BAGGAGE_KEY, baggage, context=context) def remove_baggage(name: str, context: Context | None = None) -> Context: """Removes a value from the Baggage Args: name: The name of the value to remove context: The Context to use. If not set, uses current Context Returns: A Context with the name/value removed """ baggage = _get_baggage_value(context=context).copy() baggage.pop(name, None) return set_value(_BAGGAGE_KEY, baggage, context=context) def clear(context: Context | None = None) -> Context: """Removes all values from the Baggage Args: context: The Context to use. If not set, uses current Context Returns: A Context with all baggage entries removed """ return set_value(_BAGGAGE_KEY, {}, context=context) def _get_baggage_value(context: Context | None = None) -> dict[str, object]: baggage = get_value(_BAGGAGE_KEY, context=context) if isinstance(baggage, dict): return baggage return {} def _is_valid_key(name: str) -> bool: return _KEY_PATTERN.fullmatch(str(name)) is not None def _is_valid_value(value: object) -> bool: parts = str(value).split(";") # W3C Baggage allows optional whitespace between the value and ";" is_valid_value = _VALUE_PATTERN.fullmatch(parts[0].rstrip(" \t")) is not None if len(parts) > 1: # one or more properties metadata for property in parts[1:]: if _PROPERT_PATTERN.fullmatch(property) is None: is_valid_value = False break return is_valid_value def _is_valid_pair(key: str, value: str) -> bool: return _is_valid_key(key) and _is_valid_value(value) python-opentelemetry-1.45.2/opentelemetry-api/src/opentelemetry/baggage/propagation/000077500000000000000000000000001527070070700310515ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-api/src/opentelemetry/baggage/propagation/__init__.py000066400000000000000000000127471527070070700331750ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # from collections.abc import Iterable, Iterator, Mapping from logging import getLogger from re import split from urllib.parse import quote_plus, unquote_plus from opentelemetry.baggage import _is_valid_pair, get_all, set_baggage from opentelemetry.context import get_current from opentelemetry.context.context import Context from opentelemetry.propagators import textmap from opentelemetry.util.re import _DELIMITER_PATTERN _logger = getLogger(__name__) def _filter_valid_entries( entries: Iterable[str], max_pair_length: int, ) -> Iterator[str]: for entry in entries: if not entry: continue if not entry.isascii(): _logger.warning( "Baggage entry with key `%s` contains non-ASCII characters", entry.split("=", 1)[0], ) continue if len(entry) > max_pair_length: _logger.warning( "Baggage entry with key `%s` exceeded the maximum number of bytes per list-member with length %d", entry.split("=", 1)[0], len(entry), ) continue yield entry def _apply_baggage_limits( entries: Iterable[str], max_pairs: int, max_pair_length: int, max_header_length: int, ) -> Iterator[str]: """Apply W3C Baggage size limits to a sequence of baggage entries. Yields entries that fit within the W3C specification limits. Logs warnings when entries are dropped. """ length = 0 for index, entry in enumerate(_filter_valid_entries(entries, max_pair_length)): if index >= max_pairs: _logger.warning("Baggage exceeded the maximum number of list-members") return length += (1 if index > 0 else 0) + len(entry) if length > max_header_length: _logger.warning("Baggage exceeded the maximum number of bytes per baggage-string") return yield entry class W3CBaggagePropagator(textmap.TextMapPropagator): """Extracts and injects Baggage which is used to annotate telemetry.""" _MAX_HEADER_LENGTH = 8192 _MAX_PAIR_LENGTH = 4096 _MAX_PAIRS = 180 _BAGGAGE_HEADER_NAME = "baggage" def extract( self, carrier: textmap.CarrierT, context: Context | None = None, getter: textmap.Getter[textmap.CarrierT] = textmap.default_getter, ) -> Context: """Extract Baggage from the carrier. See `opentelemetry.propagators.textmap.TextMapPropagator.extract` """ if context is None: context = get_current() header = _extract_first_element(getter.get(carrier, self._BAGGAGE_HEADER_NAME)) if not header: return context if len(header.encode()) > self._MAX_HEADER_LENGTH: _logger.warning( "Baggage header `%s` exceeded the maximum number of bytes per baggage-string", header, ) return context baggage_entries = split(_DELIMITER_PATTERN, header) for entry in _apply_baggage_limits( baggage_entries, max_pairs=self._MAX_PAIRS, max_pair_length=self._MAX_PAIR_LENGTH, max_header_length=self._MAX_HEADER_LENGTH, ): try: name, value = entry.split("=", 1) except Exception: # pylint: disable=broad-exception-caught _logger.warning("Baggage list-member `%s` doesn't match the format", entry) continue # W3C Baggage allows optional whitespace around "=" name = name.strip(" \t") value = value.strip(" \t") if not _is_valid_pair(name, value): _logger.warning("Invalid baggage entry: `%s`", entry) continue name = unquote_plus(name).strip() value = unquote_plus(value).strip() context = set_baggage( name, value, context=context, ) return context def inject( self, carrier: textmap.CarrierT, context: Context | None = None, setter: textmap.Setter[textmap.CarrierT] = textmap.default_setter, ) -> None: """Injects Baggage into the carrier. See `opentelemetry.propagators.textmap.TextMapPropagator.inject` """ baggage_entries = get_all(context=context) if not baggage_entries: return baggage_string = ",".join( _apply_baggage_limits( _encode_baggage_pairs(baggage_entries), max_pairs=self._MAX_PAIRS, max_pair_length=self._MAX_PAIR_LENGTH, max_header_length=self._MAX_HEADER_LENGTH, ) ) if baggage_string: setter.set(carrier, self._BAGGAGE_HEADER_NAME, baggage_string) @property def fields(self) -> set[str]: """Returns a set with the fields set in `inject`.""" return {self._BAGGAGE_HEADER_NAME} def _encode_baggage_pairs( baggage_entries: Mapping[str, object], ) -> Iterator[str]: """Yield URL-encoded 'key=value' pairs from baggage entries.""" for key, value in baggage_entries.items(): yield quote_plus(str(key)) + "=" + quote_plus(str(value)) def _extract_first_element( items: Iterable[textmap.CarrierT] | None, ) -> textmap.CarrierT | None: if items is None: return None return next(iter(items), None) python-opentelemetry-1.45.2/opentelemetry-api/src/opentelemetry/baggage/py.typed000066400000000000000000000000001527070070700302130ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-api/src/opentelemetry/context/000077500000000000000000000000001527070070700266355ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-api/src/opentelemetry/context/__init__.py000066400000000000000000000111461527070070700307510ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations import logging import os from contextvars import Token from uuid import uuid4 # pylint: disable=wrong-import-position from opentelemetry.context.context import Context, _RuntimeContext from opentelemetry.context.contextvars_context import ContextVarsRuntimeContext from opentelemetry.environment_variables import OTEL_PYTHON_CONTEXT logger = logging.getLogger(__name__) def _load_runtime_context() -> _RuntimeContext: """Initialize the RuntimeContext Returns: An instance of RuntimeContext. """ configured_context = os.environ.get(OTEL_PYTHON_CONTEXT) if not configured_context: return ContextVarsRuntimeContext() # pylint: disable=import-outside-toplevel,no-name-in-module from opentelemetry.util._importlib_metadata import ( # noqa: PLC0415 entry_points, ) try: return next(iter(entry_points(group="opentelemetry_context", name=configured_context))).load()() except Exception: # pylint: disable=broad-exception-caught logger.exception( "Failed to load context: %s, falling back to contextvars_context", configured_context, ) return ContextVarsRuntimeContext() _RUNTIME_CONTEXT = _load_runtime_context() def create_key(keyname: str) -> str: """To allow cross-cutting concern to control access to their local state, the RuntimeContext API provides a function which takes a keyname as input, and returns a unique key. Args: keyname: The key name is for debugging purposes and is not required to be unique. Returns: A unique string representing the newly created key. """ return keyname + "-" + str(uuid4()) def get_value(key: str, context: Context | None = None) -> object: """To access the local state of a concern, the RuntimeContext API provides a function which takes a context and a key as input, and returns a value. Args: key: The key of the value to retrieve. context: The context from which to retrieve the value, if None, the current context is used. Returns: The value associated with the key. """ return context.get(key) if context is not None else get_current().get(key) def set_value(key: str, value: object, context: Context | None = None) -> Context: """To record the local state of a cross-cutting concern, the RuntimeContext API provides a function which takes a context, a key, and a value as input, and returns an updated context which contains the new value. Args: key: The key of the entry to set. value: The value of the entry to set. context: The context to copy, if None, the current context is used. Returns: A new `Context` containing the value set. """ if context is None: context = get_current() new_values = context.copy() new_values[key] = value return Context(new_values) def get_current() -> Context: """To access the context associated with program execution, the Context API provides a function which takes no arguments and returns a Context. Returns: The current `Context` object. """ return _RUNTIME_CONTEXT.get_current() def attach(context: Context) -> Token[Context]: """Associates a Context with the caller's current execution unit. Returns a token that can be used to restore the previous Context. Args: context: The Context to set as current. Returns: A token that can be used with `detach` to reset the context. """ return _RUNTIME_CONTEXT.attach(context) def detach(token: Token[Context]) -> None: """Resets the Context associated with the caller's current execution unit to the value it had before attaching a specified Context. Args: token: The Token that was returned by a previous call to attach a Context. """ try: _RUNTIME_CONTEXT.detach(token) except Exception: # pylint: disable=broad-exception-caught logger.exception("Failed to detach context") # FIXME This is a temporary location for the suppress instrumentation key. # Once the decision around how to suppress instrumentation is made in the # spec, this key should be moved accordingly. _ON_EMIT_RECURSION_COUNT_KEY = create_key("on_emit_recursion_count") _SUPPRESS_INSTRUMENTATION_KEY = create_key("suppress_instrumentation") _SUPPRESS_HTTP_INSTRUMENTATION_KEY = create_key("suppress_http_instrumentation") __all__ = [ "Context", "attach", "create_key", "detach", "get_current", "get_value", "set_value", ] python-opentelemetry-1.45.2/opentelemetry-api/src/opentelemetry/context/context.py000066400000000000000000000032721527070070700306770ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations from abc import ABC, abstractmethod from contextvars import Token class Context(dict[str, object]): def __setitem__(self, key: str, value: object) -> None: raise ValueError def __delitem__(self, key: str) -> None: raise ValueError def setdefault(self, key: str, default: object = None) -> object: raise ValueError def pop(self, key: str, *args: object) -> object: raise ValueError def popitem(self) -> tuple[str, object]: raise ValueError def clear(self) -> None: raise ValueError def update(self, *args: object, **kwargs: object) -> None: raise ValueError def __ior__(self, other: object) -> Context: raise ValueError class _RuntimeContext(ABC): """The RuntimeContext interface provides a wrapper for the different mechanisms that are used to propagate context in Python. Implementations can be made available via entry_points and selected through environment variables. """ @abstractmethod def attach(self, context: Context) -> Token[Context]: """Sets the current `Context` object. Returns a token that can be used to reset to the previous `Context`. Args: context: The Context to set. """ @abstractmethod def get_current(self) -> Context: """Returns the current `Context` object.""" @abstractmethod def detach(self, token: Token[Context]) -> None: """Resets Context to a previous value Args: token: A reference to a previous Context. """ __all__ = ["Context"] python-opentelemetry-1.45.2/opentelemetry-api/src/opentelemetry/context/contextvars_context.py000066400000000000000000000024231527070070700333340ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations from contextvars import ContextVar, Token from opentelemetry.context.context import Context, _RuntimeContext class ContextVarsRuntimeContext(_RuntimeContext): """An implementation of the RuntimeContext interface which wraps ContextVar under the hood. This is the preferred implementation for usage with Python 3.5+ """ _CONTEXT_KEY = "current_context" def __init__(self) -> None: self._current_context = ContextVar(self._CONTEXT_KEY, default=Context()) def attach(self, context: Context) -> Token[Context]: """Sets the current `Context` object. Returns a token that can be used to reset to the previous `Context`. Args: context: The Context to set. """ return self._current_context.set(context) def get_current(self) -> Context: """Returns the current `Context` object.""" return self._current_context.get() def detach(self, token: Token[Context]) -> None: """Resets Context to a previous value Args: token: A reference to a previous Context. """ self._current_context.reset(token) __all__ = ["ContextVarsRuntimeContext"] python-opentelemetry-1.45.2/opentelemetry-api/src/opentelemetry/context/py.typed000066400000000000000000000000001527070070700303220ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-api/src/opentelemetry/environment_variables/000077500000000000000000000000001527070070700315455ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-api/src/opentelemetry/environment_variables/__init__.py000066400000000000000000000035031527070070700336570ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 OTEL_LOGS_EXPORTER = "OTEL_LOGS_EXPORTER" """ .. envvar:: OTEL_LOGS_EXPORTER """ OTEL_METRICS_EXPORTER = "OTEL_METRICS_EXPORTER" """ .. envvar:: OTEL_METRICS_EXPORTER Specifies which exporter is used for metrics. See `General SDK Configuration `_. **Default value:** ``"otlp"`` **Example:** ``export OTEL_METRICS_EXPORTER="prometheus"`` Accepted values for ``OTEL_METRICS_EXPORTER`` are: - ``"otlp"`` - ``"prometheus"`` - ``"none"``: No automatically configured exporter for metrics. .. note:: Exporter packages may add entry points for group ``opentelemetry_metrics_exporter`` which can then be used with this environment variable by name. The entry point should point to either a `opentelemetry.sdk.metrics.export.MetricExporter` (push exporter) or `opentelemetry.sdk.metrics.export.MetricReader` (pull exporter) subclass; it must be constructable without any required arguments. This mechanism is considered experimental and may change in subsequent releases. """ OTEL_PROPAGATORS = "OTEL_PROPAGATORS" """ .. envvar:: OTEL_PROPAGATORS """ OTEL_PYTHON_CONTEXT = "OTEL_PYTHON_CONTEXT" """ .. envvar:: OTEL_PYTHON_CONTEXT """ OTEL_PYTHON_ID_GENERATOR = "OTEL_PYTHON_ID_GENERATOR" """ .. envvar:: OTEL_PYTHON_ID_GENERATOR """ OTEL_TRACES_EXPORTER = "OTEL_TRACES_EXPORTER" """ .. envvar:: OTEL_TRACES_EXPORTER """ OTEL_PYTHON_TRACER_PROVIDER = "OTEL_PYTHON_TRACER_PROVIDER" """ .. envvar:: OTEL_PYTHON_TRACER_PROVIDER """ OTEL_PYTHON_METER_PROVIDER = "OTEL_PYTHON_METER_PROVIDER" """ .. envvar:: OTEL_PYTHON_METER_PROVIDER """ _OTEL_PYTHON_LOGGER_PROVIDER = "OTEL_PYTHON_LOGGER_PROVIDER" """ .. envvar:: OTEL_PYTHON_LOGGER_PROVIDER """ python-opentelemetry-1.45.2/opentelemetry-api/src/opentelemetry/environment_variables/py.typed000066400000000000000000000000001527070070700332320ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-api/src/opentelemetry/metrics/000077500000000000000000000000001527070070700266175ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-api/src/opentelemetry/metrics/__init__.py000066400000000000000000000057741527070070700307450ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 """ The OpenTelemetry metrics API describes the classes used to generate metrics. The :class:`.MeterProvider` provides users access to the :class:`.Meter` which in turn is used to create :class:`.Instrument` objects. The :class:`.Instrument` objects are used to record measurements. This module provides abstract (i.e. unimplemented) classes required for metrics, and a concrete no-op implementation :class:`.NoOpMeter` that allows applications to use the API package alone without a supporting implementation. To get a meter, you need to provide the package name from which you are calling the meter APIs to OpenTelemetry by calling `MeterProvider.get_meter` with the calling instrumentation name and the version of your package. The following code shows how to obtain a meter using the global :class:`.MeterProvider`:: from opentelemetry.metrics import get_meter meter = get_meter("example-meter") counter = meter.create_counter("example-counter") .. versionadded:: 1.10.0 .. versionchanged:: 1.12.0rc """ from opentelemetry.metrics._internal import ( Meter, MeterProvider, NoOpMeter, NoOpMeterProvider, get_meter, get_meter_provider, set_meter_provider, ) from opentelemetry.metrics._internal.instrument import ( Asynchronous, CallbackOptions, CallbackT, Counter, Histogram, Instrument, NoOpCounter, NoOpHistogram, NoOpObservableCounter, NoOpObservableGauge, NoOpObservableUpDownCounter, NoOpUpDownCounter, ObservableCounter, ObservableGauge, ObservableUpDownCounter, Synchronous, UpDownCounter, ) from opentelemetry.metrics._internal.instrument import Gauge as _Gauge from opentelemetry.metrics._internal.instrument import NoOpGauge as _NoOpGauge from opentelemetry.metrics._internal.observation import Observation for obj in [ Counter, Synchronous, Asynchronous, CallbackOptions, _Gauge, _NoOpGauge, get_meter_provider, get_meter, Histogram, Meter, MeterProvider, Instrument, NoOpCounter, NoOpHistogram, NoOpMeter, NoOpMeterProvider, NoOpObservableCounter, NoOpObservableGauge, NoOpObservableUpDownCounter, NoOpUpDownCounter, ObservableCounter, ObservableGauge, ObservableUpDownCounter, Observation, set_meter_provider, UpDownCounter, ]: obj.__module__ = __name__ __all__ = [ "Asynchronous", "CallbackOptions", "CallbackT", "Counter", "Histogram", "Instrument", "Meter", "MeterProvider", "NoOpCounter", "NoOpHistogram", "NoOpMeter", "NoOpMeterProvider", "NoOpObservableCounter", "NoOpObservableGauge", "NoOpObservableUpDownCounter", "NoOpUpDownCounter", "ObservableCounter", "ObservableGauge", "ObservableUpDownCounter", "Observation", "Synchronous", "UpDownCounter", "_Gauge", "_NoOpGauge", "get_meter", "get_meter_provider", "set_meter_provider", ] python-opentelemetry-1.45.2/opentelemetry-api/src/opentelemetry/metrics/_internal/000077500000000000000000000000001527070070700305725ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-api/src/opentelemetry/metrics/_internal/__init__.py000066400000000000000000000742231527070070700327130ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # pylint: disable=too-many-ancestors """ The OpenTelemetry metrics API describes the classes used to generate metrics. The :class:`.MeterProvider` provides users access to the :class:`.Meter` which in turn is used to create :class:`.Instrument` objects. The :class:`.Instrument` objects are used to record measurements. This module provides abstract (i.e. unimplemented) classes required for metrics, and a concrete no-op implementation :class:`.NoOpMeter` that allows applications to use the API package alone without a supporting implementation. To get a meter, you need to provide the package name from which you are calling the meter APIs to OpenTelemetry by calling `MeterProvider.get_meter` with the calling instrumentation name and the version of your package. The following code shows how to obtain a meter using the global :class:`.MeterProvider`:: from opentelemetry.metrics import get_meter meter = get_meter("example-meter") counter = meter.create_counter("example-counter") .. versionadded:: 1.10.0 """ import warnings from abc import ABC, abstractmethod from collections.abc import Sequence from dataclasses import dataclass from logging import getLogger from os import environ from threading import Lock from typing import cast from opentelemetry.environment_variables import OTEL_PYTHON_METER_PROVIDER from opentelemetry.metrics._internal.instrument import ( CallbackT, Counter, Gauge, Histogram, NoOpCounter, NoOpGauge, NoOpHistogram, NoOpObservableCounter, NoOpObservableGauge, NoOpObservableUpDownCounter, NoOpUpDownCounter, ObservableCounter, ObservableGauge, ObservableUpDownCounter, UpDownCounter, _MetricsHistogramAdvisory, _ProxyCounter, _ProxyGauge, _ProxyHistogram, _ProxyObservableCounter, _ProxyObservableGauge, _ProxyObservableUpDownCounter, _ProxyUpDownCounter, ) from opentelemetry.util._once import Once from opentelemetry.util._providers import _load_provider from opentelemetry.util.types import ( Attributes, ) _logger = getLogger(__name__) # pylint: disable=invalid-name _ProxyInstrumentT = ( _ProxyCounter | _ProxyHistogram | _ProxyGauge | _ProxyObservableCounter | _ProxyObservableGauge | _ProxyObservableUpDownCounter | _ProxyUpDownCounter ) class MeterProvider(ABC): """ MeterProvider is the entry point of the API. It provides access to `Meter` instances. """ @abstractmethod def get_meter( self, name: str, version: str | None = None, schema_url: str | None = None, attributes: Attributes = None, ) -> "Meter": """Returns a `Meter` for use by the given instrumentation library. For any two calls it is undefined whether the same or different `Meter` instances are returned, even for different library names. This function may return different `Meter` types (e.g. a no-op meter vs. a functional meter). Args: name: The name of the instrumenting module. ``__name__`` should be avoided as this can result in different meter names if the meters are in different files. It is better to use a fixed string that can be imported where needed and used consistently as the name of the meter. This should *not* be the name of the module that is instrumented but the name of the module doing the instrumentation. E.g., instead of ``"requests"``, use ``"opentelemetry.instrumentation.requests"``. version: Optional. The version string of the instrumenting library. Usually this should be the same as ``importlib.metadata.version(instrumenting_library_name)``. schema_url: Optional. Specifies the Schema URL of the emitted telemetry. attributes: Optional. Attributes that are associated with the emitted telemetry. """ class NoOpMeterProvider(MeterProvider): """The default MeterProvider used when no MeterProvider implementation is available.""" def get_meter( self, name: str, version: str | None = None, schema_url: str | None = None, attributes: Attributes = None, ) -> "Meter": """Returns a NoOpMeter.""" return NoOpMeter(name, version=version, schema_url=schema_url) class _ProxyMeterProvider(MeterProvider): def __init__(self) -> None: self._lock = Lock() self._meters: list[_ProxyMeter] = [] self._real_meter_provider: MeterProvider | None = None def get_meter( self, name: str, version: str | None = None, schema_url: str | None = None, attributes: Attributes = None, ) -> "Meter": with self._lock: if self._real_meter_provider is not None: return self._real_meter_provider.get_meter(name, version, schema_url) meter = _ProxyMeter(name, version=version, schema_url=schema_url) self._meters.append(meter) return meter def on_set_meter_provider(self, meter_provider: MeterProvider) -> None: with self._lock: self._real_meter_provider = meter_provider for meter in self._meters: meter.on_set_meter_provider(meter_provider) @dataclass class _InstrumentRegistrationStatus: instrument_id: str already_registered: bool conflict: bool current_advisory: _MetricsHistogramAdvisory | None class Meter(ABC): """Handles instrument creation. This class provides methods for creating instruments which are then used to produce measurements. """ def __init__( self, name: str, version: str | None = None, schema_url: str | None = None, ) -> None: super().__init__() self._name = name self._version = version self._schema_url = schema_url self._instrument_ids: dict[str, _MetricsHistogramAdvisory | None] = {} self._instrument_ids_lock = Lock() @property def name(self) -> str: """ The name of the instrumenting module. """ return self._name @property def version(self) -> str | None: """ The version string of the instrumenting library. """ return self._version @property def schema_url(self) -> str | None: """ Specifies the Schema URL of the emitted telemetry """ return self._schema_url def _register_instrument( self, name: str, type_: type, unit: str, description: str, advisory: _MetricsHistogramAdvisory | None = None, ) -> _InstrumentRegistrationStatus: """ Register an instrument with the name, type, unit and description as identifying keys and the advisory as value. Returns a tuple. The first value is the instrument id. The second value is an `_InstrumentRegistrationStatus` where `already_registered` is `True` if the instrument has been registered already. If `conflict` is set to True the `current_advisory` attribute contains the registered instrument advisory. """ instrument_id = ",".join([name.strip().lower(), type_.__name__, unit, description]) already_registered = False conflict = False current_advisory = None with self._instrument_ids_lock: # we are not using get because None is a valid value already_registered = instrument_id in self._instrument_ids if already_registered: current_advisory = self._instrument_ids[instrument_id] conflict = current_advisory != advisory else: self._instrument_ids[instrument_id] = advisory return _InstrumentRegistrationStatus( instrument_id=instrument_id, already_registered=already_registered, conflict=conflict, current_advisory=current_advisory, ) @staticmethod def _log_instrument_registration_conflict( name: str, instrumentation_type: str, unit: str, description: str, status: _InstrumentRegistrationStatus, ) -> None: _logger.warning( "An instrument with name %s, type %s, unit %s and " "description %s has been created already with a " "different advisory value %s and will be used instead.", name, instrumentation_type, unit, description, status.current_advisory, ) @abstractmethod def create_counter( self, name: str, unit: str = "", description: str = "", ) -> Counter: """Creates a `Counter` instrument Args: name: The name of the instrument to be created unit: The unit for observations this instrument reports. For example, ``By`` for bytes. UCUM units are recommended. description: A description for this instrument and what it measures. """ @abstractmethod def create_up_down_counter( self, name: str, unit: str = "", description: str = "", ) -> UpDownCounter: """Creates an `UpDownCounter` instrument Args: name: The name of the instrument to be created unit: The unit for observations this instrument reports. For example, ``By`` for bytes. UCUM units are recommended. description: A description for this instrument and what it measures. """ @abstractmethod def create_observable_counter( self, name: str, callbacks: Sequence[CallbackT] | None = None, unit: str = "", description: str = "", ) -> ObservableCounter: """Creates an `ObservableCounter` instrument An observable counter observes a monotonically increasing count by calling provided callbacks which accept a :class:`~opentelemetry.metrics.CallbackOptions` and return multiple :class:`~opentelemetry.metrics.Observation`. For example, an observable counter could be used to report system CPU time periodically. Here is a basic implementation:: def cpu_time_callback( options: CallbackOptions, ) -> Iterable[Observation]: observations = [] with open("/proc/stat") as procstat: procstat.readline() # skip the first line for line in procstat: if not line.startswith("cpu"): break cpu, *states = line.split() observations.append( Observation( int(states[0]) // 100, {"cpu": cpu, "state": "user"}, ) ) observations.append( Observation( int(states[1]) // 100, {"cpu": cpu, "state": "nice"}, ) ) observations.append( Observation( int(states[2]) // 100, {"cpu": cpu, "state": "system"}, ) ) # ... other states return observations meter.create_observable_counter( "system.cpu.time", callbacks=[cpu_time_callback], unit="s", description="CPU time", ) To reduce memory usage, you can use generator callbacks instead of building the full list:: def cpu_time_callback( options: CallbackOptions, ) -> Iterable[Observation]: with open("/proc/stat") as procstat: procstat.readline() # skip the first line for line in procstat: if not line.startswith("cpu"): break cpu, *states = line.split() yield Observation( int(states[0]) // 100, {"cpu": cpu, "state": "user"}, ) yield Observation( int(states[1]) // 100, {"cpu": cpu, "state": "nice"}, ) # ... other states Alternatively, you can pass a sequence of generators directly instead of a sequence of callbacks, which each should return iterables of :class:`~opentelemetry.metrics.Observation`:: def cpu_time_callback( states_to_include: set[str], ) -> Iterable[Iterable[Observation]]: # accept options sent in from OpenTelemetry options = yield while True: observations = [] with open("/proc/stat") as procstat: procstat.readline() # skip the first line for line in procstat: if not line.startswith("cpu"): break cpu, *states = line.split() if "user" in states_to_include: observations.append( Observation( int(states[0]) // 100, {"cpu": cpu, "state": "user"}, ) ) if "nice" in states_to_include: observations.append( Observation( int(states[1]) // 100, {"cpu": cpu, "state": "nice"}, ) ) # ... other states # yield the observations and receive the options for next iteration options = yield observations meter.create_observable_counter( "system.cpu.time", callbacks=[cpu_time_callback({"user", "system"})], unit="s", description="CPU time", ) The :class:`~opentelemetry.metrics.CallbackOptions` contain a timeout which the callback should respect. For example if the callback does asynchronous work, like making HTTP requests, it should respect the timeout:: def scrape_http_callback( options: CallbackOptions, ) -> Iterable[Observation]: r = requests.get( "http://scrapethis.com", timeout=options.timeout_millis / 10**3, ) for value in r.json(): yield Observation(value) Args: name: The name of the instrument to be created callbacks: A sequence of callbacks that return an iterable of :class:`~opentelemetry.metrics.Observation`. Alternatively, can be a sequence of generators that each yields iterables of :class:`~opentelemetry.metrics.Observation`. unit: The unit for observations this instrument reports. For example, ``By`` for bytes. UCUM units are recommended. description: A description for this instrument and what it measures. """ @abstractmethod def create_histogram( self, name: str, unit: str = "", description: str = "", *, explicit_bucket_boundaries_advisory: Sequence[float] | None = None, ) -> Histogram: """Creates a :class:`~opentelemetry.metrics.Histogram` instrument Args: name: The name of the instrument to be created unit: The unit for observations this instrument reports. For example, ``By`` for bytes. UCUM units are recommended. description: A description for this instrument and what it measures. """ def create_gauge( # type: ignore # pylint: disable=no-self-use self, name: str, unit: str = "", description: str = "", ) -> Gauge: # pyright: ignore[reportReturnType] """Creates a ``Gauge`` instrument Args: name: The name of the instrument to be created unit: The unit for observations this instrument reports. For example, ``By`` for bytes. UCUM units are recommended. description: A description for this instrument and what it measures. """ warnings.warn("create_gauge() is not implemented and will be a no-op") @abstractmethod def create_observable_gauge( self, name: str, callbacks: Sequence[CallbackT] | None = None, unit: str = "", description: str = "", ) -> ObservableGauge: """Creates an `ObservableGauge` instrument Args: name: The name of the instrument to be created callbacks: A sequence of callbacks that return an iterable of :class:`~opentelemetry.metrics.Observation`. Alternatively, can be a generator that yields iterables of :class:`~opentelemetry.metrics.Observation`. unit: The unit for observations this instrument reports. For example, ``By`` for bytes. UCUM units are recommended. description: A description for this instrument and what it measures. """ @abstractmethod def create_observable_up_down_counter( self, name: str, callbacks: Sequence[CallbackT] | None = None, unit: str = "", description: str = "", ) -> ObservableUpDownCounter: """Creates an `ObservableUpDownCounter` instrument Args: name: The name of the instrument to be created callbacks: A sequence of callbacks that return an iterable of :class:`~opentelemetry.metrics.Observation`. Alternatively, can be a generator that yields iterables of :class:`~opentelemetry.metrics.Observation`. unit: The unit for observations this instrument reports. For example, ``By`` for bytes. UCUM units are recommended. description: A description for this instrument and what it measures. """ class _ProxyMeter(Meter): def __init__( self, name: str, version: str | None = None, schema_url: str | None = None, ) -> None: super().__init__(name, version=version, schema_url=schema_url) self._lock = Lock() self._instruments: list[_ProxyInstrumentT] = [] self._real_meter: Meter | None = None def on_set_meter_provider(self, meter_provider: MeterProvider) -> None: """Called when a real meter provider is set on the creating _ProxyMeterProvider Creates a real backing meter for this instance and notifies all created instruments so they can create real backing instruments. """ real_meter = meter_provider.get_meter(self._name, self._version, self._schema_url) with self._lock: self._real_meter = real_meter # notify all proxy instruments of the new meter so they can create # real instruments to back themselves for instrument in self._instruments: instrument.on_meter_set(real_meter) def create_counter( self, name: str, unit: str = "", description: str = "", ) -> Counter: with self._lock: if self._real_meter: return self._real_meter.create_counter(name, unit, description) proxy = _ProxyCounter(name, unit, description) self._instruments.append(proxy) return proxy def create_up_down_counter( self, name: str, unit: str = "", description: str = "", ) -> UpDownCounter: with self._lock: if self._real_meter: return self._real_meter.create_up_down_counter(name, unit, description) proxy = _ProxyUpDownCounter(name, unit, description) self._instruments.append(proxy) return proxy def create_observable_counter( self, name: str, callbacks: Sequence[CallbackT] | None = None, unit: str = "", description: str = "", ) -> ObservableCounter: with self._lock: if self._real_meter: return self._real_meter.create_observable_counter(name, callbacks, unit, description) proxy = _ProxyObservableCounter(name, callbacks, unit=unit, description=description) self._instruments.append(proxy) return proxy def create_histogram( self, name: str, unit: str = "", description: str = "", *, explicit_bucket_boundaries_advisory: Sequence[float] | None = None, ) -> Histogram: with self._lock: if self._real_meter: return self._real_meter.create_histogram( name, unit, description, explicit_bucket_boundaries_advisory=explicit_bucket_boundaries_advisory, ) proxy = _ProxyHistogram(name, unit, description, explicit_bucket_boundaries_advisory) self._instruments.append(proxy) return proxy def create_gauge( self, name: str, unit: str = "", description: str = "", ) -> Gauge: with self._lock: if self._real_meter: return self._real_meter.create_gauge(name, unit, description) proxy = _ProxyGauge(name, unit, description) self._instruments.append(proxy) return proxy def create_observable_gauge( self, name: str, callbacks: Sequence[CallbackT] | None = None, unit: str = "", description: str = "", ) -> ObservableGauge: with self._lock: if self._real_meter: return self._real_meter.create_observable_gauge(name, callbacks, unit, description) proxy = _ProxyObservableGauge(name, callbacks, unit=unit, description=description) self._instruments.append(proxy) return proxy def create_observable_up_down_counter( self, name: str, callbacks: Sequence[CallbackT] | None = None, unit: str = "", description: str = "", ) -> ObservableUpDownCounter: with self._lock: if self._real_meter: return self._real_meter.create_observable_up_down_counter( name, callbacks, unit, description, ) proxy = _ProxyObservableUpDownCounter(name, callbacks, unit=unit, description=description) self._instruments.append(proxy) return proxy class NoOpMeter(Meter): """The default Meter used when no Meter implementation is available. All operations are no-op. """ def create_counter( self, name: str, unit: str = "", description: str = "", ) -> Counter: """Returns a no-op Counter.""" status = self._register_instrument(name, NoOpCounter, unit, description) if status.conflict: self._log_instrument_registration_conflict( name, Counter.__name__, unit, description, status, ) return NoOpCounter(name, unit=unit, description=description) def create_gauge( self, name: str, unit: str = "", description: str = "", ) -> Gauge: """Returns a no-op Gauge.""" status = self._register_instrument(name, NoOpGauge, unit, description) if status.conflict: self._log_instrument_registration_conflict( name, Gauge.__name__, unit, description, status, ) return NoOpGauge(name, unit=unit, description=description) def create_up_down_counter( self, name: str, unit: str = "", description: str = "", ) -> UpDownCounter: """Returns a no-op UpDownCounter.""" status = self._register_instrument(name, NoOpUpDownCounter, unit, description) if status.conflict: self._log_instrument_registration_conflict( name, UpDownCounter.__name__, unit, description, status, ) return NoOpUpDownCounter(name, unit=unit, description=description) def create_observable_counter( self, name: str, callbacks: Sequence[CallbackT] | None = None, unit: str = "", description: str = "", ) -> ObservableCounter: """Returns a no-op ObservableCounter.""" status = self._register_instrument(name, NoOpObservableCounter, unit, description) if status.conflict: self._log_instrument_registration_conflict( name, ObservableCounter.__name__, unit, description, status, ) return NoOpObservableCounter( name, callbacks, unit=unit, description=description, ) def create_histogram( self, name: str, unit: str = "", description: str = "", *, explicit_bucket_boundaries_advisory: Sequence[float] | None = None, ) -> Histogram: """Returns a no-op Histogram.""" status = self._register_instrument( name, NoOpHistogram, unit, description, _MetricsHistogramAdvisory(explicit_bucket_boundaries=explicit_bucket_boundaries_advisory), ) if status.conflict: self._log_instrument_registration_conflict( name, Histogram.__name__, unit, description, status, ) return NoOpHistogram( name, unit=unit, description=description, explicit_bucket_boundaries_advisory=explicit_bucket_boundaries_advisory, ) def create_observable_gauge( self, name: str, callbacks: Sequence[CallbackT] | None = None, unit: str = "", description: str = "", ) -> ObservableGauge: """Returns a no-op ObservableGauge.""" status = self._register_instrument(name, NoOpObservableGauge, unit, description) if status.conflict: self._log_instrument_registration_conflict( name, ObservableGauge.__name__, unit, description, status, ) return NoOpObservableGauge( name, callbacks, unit=unit, description=description, ) def create_observable_up_down_counter( self, name: str, callbacks: Sequence[CallbackT] | None = None, unit: str = "", description: str = "", ) -> ObservableUpDownCounter: """Returns a no-op ObservableUpDownCounter.""" status = self._register_instrument(name, NoOpObservableUpDownCounter, unit, description) if status.conflict: self._log_instrument_registration_conflict( name, ObservableUpDownCounter.__name__, unit, description, status, ) return NoOpObservableUpDownCounter( name, callbacks, unit=unit, description=description, ) _METER_PROVIDER_SET_ONCE = Once() _METER_PROVIDER: MeterProvider | None = None _PROXY_METER_PROVIDER = _ProxyMeterProvider() def get_meter( name: str, version: str = "", meter_provider: MeterProvider | None = None, schema_url: str | None = None, attributes: Attributes = None, ) -> "Meter": """Returns a `Meter` for use by the given instrumentation library. This function is a convenience wrapper for `opentelemetry.metrics.MeterProvider.get_meter`. If meter_provider is omitted the current configured one is used. """ if meter_provider is None: meter_provider = get_meter_provider() return meter_provider.get_meter(name, version, schema_url, attributes) def _set_meter_provider(meter_provider: MeterProvider, log: bool) -> None: def set_mp() -> None: global _METER_PROVIDER # pylint: disable=global-statement _METER_PROVIDER = meter_provider # gives all proxies real instruments off the newly set meter provider _PROXY_METER_PROVIDER.on_set_meter_provider(meter_provider) did_set = _METER_PROVIDER_SET_ONCE.do_once(set_mp) if log and not did_set: _logger.warning("Overriding of current MeterProvider is not allowed") def set_meter_provider(meter_provider: MeterProvider) -> None: """Sets the current global :class:`~.MeterProvider` object. This can only be done once, a warning will be logged if any further attempt is made. """ _set_meter_provider(meter_provider, log=True) def get_meter_provider() -> MeterProvider: """Gets the current global :class:`~.MeterProvider` object.""" if _METER_PROVIDER is None: if OTEL_PYTHON_METER_PROVIDER not in environ: return _PROXY_METER_PROVIDER meter_provider: MeterProvider = _load_provider( # type: ignore OTEL_PYTHON_METER_PROVIDER, "meter_provider" ) _set_meter_provider(meter_provider, log=False) # _METER_PROVIDER will have been set by one thread return cast("MeterProvider", _METER_PROVIDER) python-opentelemetry-1.45.2/opentelemetry-api/src/opentelemetry/metrics/_internal/instrument.py000066400000000000000000000371551527070070700333670ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # pylint: disable=too-many-ancestors from abc import ABC, abstractmethod from collections.abc import Callable, Generator, Iterable, Sequence from dataclasses import dataclass from logging import getLogger from re import compile as re_compile from typing import ( Generic, TypeVar, ) # pylint: disable=unused-import; needed for typing and sphinx from opentelemetry import metrics from opentelemetry.context import Context from opentelemetry.metrics._internal.observation import Observation from opentelemetry.util.types import ( Attributes, ) _logger = getLogger(__name__) _name_regex = re_compile(r"[a-zA-Z][-_./a-zA-Z0-9]{0,254}") _unit_regex = re_compile(r"[\x00-\x7F]{0,63}") @dataclass(frozen=True) class _MetricsHistogramAdvisory: explicit_bucket_boundaries: Sequence[float] | None = None @dataclass(frozen=True) class CallbackOptions: """Options for the callback Args: timeout_millis: Timeout for the callback's execution. If the callback does asynchronous work (e.g. HTTP requests), it should respect this timeout. """ timeout_millis: float = 10_000 InstrumentT = TypeVar("InstrumentT", bound="Instrument") # pylint: disable=invalid-name CallbackT = Callable[[CallbackOptions], Iterable[Observation]] | Generator[Iterable[Observation], CallbackOptions, None] class Instrument(ABC): """Abstract class that serves as base for all instruments.""" @abstractmethod def __init__( self, name: str, unit: str = "", description: str = "", ) -> None: pass @staticmethod def _check_name_unit_description(name: str, unit: str, description: str) -> dict[str, str | None]: """ Checks the following instrument name, unit and description for compliance with the spec. Returns a dict with keys "name", "unit" and "description", the corresponding values will be the checked strings or `None` if the value is invalid. If valid, the checked strings should be used instead of the original values. """ result: dict[str, str | None] = {} if _name_regex.fullmatch(name) is not None: result["name"] = name else: result["name"] = None if unit is None: unit = "" if _unit_regex.fullmatch(unit) is not None: result["unit"] = unit else: result["unit"] = None if description is None: result["description"] = "" else: result["description"] = description return result class _ProxyInstrument(ABC, Generic[InstrumentT]): def __init__( self, name: str, unit: str = "", description: str = "", ) -> None: self._name = name self._unit = unit self._description = description self._real_instrument: InstrumentT | None = None def on_meter_set(self, meter: "metrics.Meter") -> None: """Called when a real meter is set on the creating _ProxyMeter""" # We don't need any locking on proxy instruments because it's OK if some # measurements get dropped while a real backing instrument is being # created. self._real_instrument = self._create_real_instrument(meter) @abstractmethod def _create_real_instrument(self, meter: "metrics.Meter") -> InstrumentT: """Create an instance of the real instrument. Implement this.""" class _ProxyAsynchronousInstrument(_ProxyInstrument[InstrumentT]): def __init__( self, name: str, callbacks: Sequence[CallbackT] | None = None, unit: str = "", description: str = "", ) -> None: super().__init__(name, unit, description) self._callbacks = callbacks class Synchronous(Instrument): """Base class for all synchronous instruments""" class Asynchronous(Instrument): """Base class for all asynchronous instruments""" @abstractmethod def __init__( self, name: str, callbacks: Sequence[CallbackT] | None = None, unit: str = "", description: str = "", ) -> None: super().__init__(name, unit=unit, description=description) class Counter(Synchronous): """A Counter is a synchronous `Instrument` which supports non-negative increments.""" @abstractmethod def add( self, amount: float, attributes: Attributes = None, context: Context | None = None, ) -> None: """Records an increment to the counter. Args: amount: The amount to increment the counter by. Must be non-negative. attributes: Optional set of attributes to associate with the measurement. context: Optional context to associate with the measurement. If not provided, the current context is used. """ class NoOpCounter(Counter): """No-op implementation of `Counter`.""" def __init__( self, name: str, unit: str = "", description: str = "", ) -> None: super().__init__(name, unit=unit, description=description) def add( self, amount: float, attributes: Attributes = None, context: Context | None = None, ) -> None: return super().add(amount, attributes=attributes, context=context) class _ProxyCounter(_ProxyInstrument[Counter], Counter): def add( self, amount: float, attributes: Attributes = None, context: Context | None = None, ) -> None: if self._real_instrument: self._real_instrument.add(amount, attributes, context) def _create_real_instrument(self, meter: "metrics.Meter") -> Counter: return meter.create_counter( self._name, self._unit, self._description, ) class UpDownCounter(Synchronous): """An UpDownCounter is a synchronous `Instrument` which supports increments and decrements.""" @abstractmethod def add( self, amount: float, attributes: Attributes = None, context: Context | None = None, ) -> None: """Records an increment or decrement to the counter. Unlike `Counter`, the ``amount`` may be negative, allowing the instrument to track values that go up and down (e.g. number of active requests, queue depth). Args: amount: The amount to add to the counter. May be positive or negative. attributes: Optional set of attributes to associate with the measurement. context: Optional context to associate with the measurement. If not provided, the current context is used. """ class NoOpUpDownCounter(UpDownCounter): """No-op implementation of `UpDownCounter`.""" def __init__( self, name: str, unit: str = "", description: str = "", ) -> None: super().__init__(name, unit=unit, description=description) def add( self, amount: float, attributes: Attributes = None, context: Context | None = None, ) -> None: return super().add(amount, attributes=attributes, context=context) class _ProxyUpDownCounter(_ProxyInstrument[UpDownCounter], UpDownCounter): def add( self, amount: float, attributes: Attributes = None, context: Context | None = None, ) -> None: if self._real_instrument: self._real_instrument.add(amount, attributes, context) def _create_real_instrument(self, meter: "metrics.Meter") -> UpDownCounter: return meter.create_up_down_counter( self._name, self._unit, self._description, ) class ObservableCounter(Asynchronous): """An ObservableCounter is an asynchronous `Instrument` which reports monotonically increasing value(s) when the instrument is being observed. """ class NoOpObservableCounter(ObservableCounter): """No-op implementation of `ObservableCounter`.""" def __init__( self, name: str, callbacks: Sequence[CallbackT] | None = None, unit: str = "", description: str = "", ) -> None: super().__init__( name, callbacks, unit=unit, description=description, ) class _ProxyObservableCounter(_ProxyAsynchronousInstrument[ObservableCounter], ObservableCounter): def _create_real_instrument(self, meter: "metrics.Meter") -> ObservableCounter: return meter.create_observable_counter( self._name, self._callbacks, self._unit, self._description, ) class ObservableUpDownCounter(Asynchronous): """An ObservableUpDownCounter is an asynchronous `Instrument` which reports additive value(s) (e.g. the process heap size - it makes sense to report the heap size from multiple processes and sum them up, so we get the total heap usage) when the instrument is being observed. """ class NoOpObservableUpDownCounter(ObservableUpDownCounter): """No-op implementation of `ObservableUpDownCounter`.""" def __init__( self, name: str, callbacks: Sequence[CallbackT] | None = None, unit: str = "", description: str = "", ) -> None: super().__init__( name, callbacks, unit=unit, description=description, ) class _ProxyObservableUpDownCounter( _ProxyAsynchronousInstrument[ObservableUpDownCounter], ObservableUpDownCounter, ): def _create_real_instrument(self, meter: "metrics.Meter") -> ObservableUpDownCounter: return meter.create_observable_up_down_counter( self._name, self._callbacks, self._unit, self._description, ) class Histogram(Synchronous): """Histogram is a synchronous `Instrument` which can be used to report arbitrary values that are likely to be statistically meaningful. It is intended for statistics such as histograms, summaries, and percentile. """ @abstractmethod def __init__( self, name: str, unit: str = "", description: str = "", explicit_bucket_boundaries_advisory: Sequence[float] | None = None, ) -> None: pass @abstractmethod def record( self, amount: float, attributes: Attributes = None, context: Context | None = None, ) -> None: """Records a measurement. Used to report measurements that are likely to be statistically meaningful, such as request durations, payload sizes, or any value for which a distribution (e.g. percentiles) is useful. Args: amount: The measurement to record. Should be non-negative in most cases; negative values are only meaningful when the histogram is used to track signed deltas. attributes: Optional set of attributes to associate with the measurement. context: Optional context to associate with the measurement. If not provided, the current context is used. """ class NoOpHistogram(Histogram): """No-op implementation of `Histogram`.""" def __init__( self, name: str, unit: str = "", description: str = "", explicit_bucket_boundaries_advisory: Sequence[float] | None = None, ) -> None: super().__init__( name, unit=unit, description=description, explicit_bucket_boundaries_advisory=explicit_bucket_boundaries_advisory, ) def record( self, amount: float, attributes: Attributes = None, context: Context | None = None, ) -> None: return super().record(amount, attributes=attributes, context=context) class _ProxyHistogram(_ProxyInstrument[Histogram], Histogram): def __init__( self, name: str, unit: str = "", description: str = "", explicit_bucket_boundaries_advisory: Sequence[float] | None = None, ) -> None: super().__init__(name, unit=unit, description=description) self._explicit_bucket_boundaries_advisory = explicit_bucket_boundaries_advisory def record( self, amount: float, attributes: Attributes = None, context: Context | None = None, ) -> None: if self._real_instrument: self._real_instrument.record(amount, attributes, context) def _create_real_instrument(self, meter: "metrics.Meter") -> Histogram: return meter.create_histogram( self._name, self._unit, self._description, explicit_bucket_boundaries_advisory=self._explicit_bucket_boundaries_advisory, ) class ObservableGauge(Asynchronous): """Asynchronous Gauge is an asynchronous `Instrument` which reports non-additive value(s) (e.g. the room temperature - it makes no sense to report the temperature value from multiple rooms and sum them up) when the instrument is being observed. """ class NoOpObservableGauge(ObservableGauge): """No-op implementation of `ObservableGauge`.""" def __init__( self, name: str, callbacks: Sequence[CallbackT] | None = None, unit: str = "", description: str = "", ) -> None: super().__init__( name, callbacks, unit=unit, description=description, ) class _ProxyObservableGauge( _ProxyAsynchronousInstrument[ObservableGauge], ObservableGauge, ): def _create_real_instrument(self, meter: "metrics.Meter") -> ObservableGauge: return meter.create_observable_gauge( self._name, self._callbacks, self._unit, self._description, ) class Gauge(Synchronous): """A Gauge is a synchronous `Instrument` which can be used to record non-additive values as they occur.""" @abstractmethod def set( self, amount: float, attributes: Attributes = None, context: Context | None = None, ) -> None: """Records the current value of the gauge. The gauge reports the last recorded value when observed. It is intended for non-additive measurements where only the current value matters (e.g. CPU utilisation percentage, room temperature). Args: amount: The current value to record. attributes: Optional set of attributes to associate with the measurement. context: Optional context to associate with the measurement. If not provided, the current context is used. """ class NoOpGauge(Gauge): """No-op implementation of ``Gauge``.""" def __init__( self, name: str, unit: str = "", description: str = "", ) -> None: super().__init__(name, unit=unit, description=description) def set( self, amount: float, attributes: Attributes = None, context: Context | None = None, ) -> None: return super().set(amount, attributes=attributes, context=context) class _ProxyGauge( _ProxyInstrument[Gauge], Gauge, ): def set( self, amount: float, attributes: Attributes = None, context: Context | None = None, ) -> None: if self._real_instrument: self._real_instrument.set(amount, attributes, context) def _create_real_instrument(self, meter: "metrics.Meter") -> Gauge: return meter.create_gauge( self._name, self._unit, self._description, ) python-opentelemetry-1.45.2/opentelemetry-api/src/opentelemetry/metrics/_internal/observation.py000066400000000000000000000025421527070070700335020ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from opentelemetry.context import Context from opentelemetry.util.types import Attributes class Observation: """A measurement observed in an asynchronous instrument Return/yield instances of this class from asynchronous instrument callbacks. Args: value: The float or int measured value attributes: The measurement's attributes context: The measurement's context """ def __init__( self, value: float, attributes: Attributes = None, context: Context | None = None, ) -> None: self._value = value self._attributes = attributes self._context = context @property def value(self) -> float | int: return self._value @property def attributes(self) -> Attributes: return self._attributes @property def context(self) -> Context | None: return self._context def __eq__(self, other: object) -> bool: return ( isinstance(other, Observation) and self.value == other.value and self.attributes == other.attributes and self.context == other.context ) def __repr__(self) -> str: return f"Observation(value={self.value}, attributes={self.attributes}, context={self.context})" python-opentelemetry-1.45.2/opentelemetry-api/src/opentelemetry/metrics/py.typed000066400000000000000000000000001527070070700303040ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-api/src/opentelemetry/propagate/000077500000000000000000000000001527070070700271335ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-api/src/opentelemetry/propagate/__init__.py000066400000000000000000000142371527070070700312530ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 """ API for propagation of context. The propagators for the ``opentelemetry.propagators.composite.CompositePropagator`` can be defined via configuration in the ``OTEL_PROPAGATORS`` environment variable. This variable should be set to a comma-separated string of names of values for the ``opentelemetry_propagator`` entry point. For example, setting ``OTEL_PROPAGATORS`` to ``tracecontext,baggage`` (which is the default value) would instantiate ``opentelemetry.propagators.composite.CompositePropagator`` with 2 propagators, one of type ``opentelemetry.trace.propagation.tracecontext.TraceContextTextMapPropagator`` and other of type ``opentelemetry.baggage.propagation.W3CBaggagePropagator``. Notice that these propagator classes are defined as ``opentelemetry_propagator`` entry points in the ``pyproject.toml`` file of ``opentelemetry``. Example:: import flask import requests from opentelemetry import propagate PROPAGATOR = propagate.get_global_textmap() def get_header_from_flask_request(request, key): return request.headers.get_all(key) def set_header_into_requests_request(request: requests.Request, key: str, value: str): request.headers[key] = value def example_route(): context = PROPAGATOR.extract(get_header_from_flask_request, flask.request) request_to_downstream = requests.Request("GET", "http://httpbin.org/get") PROPAGATOR.inject( set_header_into_requests_request, request_to_downstream, context=context, ) session = requests.Session() session.send(request_to_downstream.prepare()) .. _Propagation API Specification: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/context/api-propagators.md """ from logging import getLogger from os import environ from opentelemetry.context.context import Context from opentelemetry.environment_variables import OTEL_PROPAGATORS from opentelemetry.propagators import composite, textmap logger = getLogger(__name__) def extract( carrier: textmap.CarrierT, context: Context | None = None, getter: textmap.Getter[textmap.CarrierT] = textmap.default_getter, ) -> Context: """Uses the configured propagator to extract a Context from the carrier. Args: 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. carrier: and 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: an optional Context to use. Defaults to root context if not set. """ return get_global_textmap().extract(carrier, context, getter=getter) def inject( carrier: textmap.CarrierT, context: Context | None = None, setter: textmap.Setter[textmap.CarrierT] = textmap.default_setter, ) -> None: """Uses the configured propagator to inject a Context into the carrier. Args: carrier: the medium used by Propagators to read values from and write values to. Should be paired with setter, which should know how to set header values on the carrier. context: An optional Context to use. Defaults to current context if not set. setter: An optional `Setter` object that can set values on the carrier. """ get_global_textmap().inject(carrier, context=context, setter=setter) def _load_propagators() -> textmap.TextMapPropagator: configured = environ.get(OTEL_PROPAGATORS) if not configured: # pylint: disable=import-outside-toplevel,no-name-in-module from opentelemetry.baggage.propagation import ( # noqa: PLC0415 W3CBaggagePropagator, ) # pylint: disable=import-outside-toplevel,no-name-in-module from opentelemetry.trace.propagation.tracecontext import ( # noqa: PLC0415 TraceContextTextMapPropagator, ) return composite.CompositePropagator([TraceContextTextMapPropagator(), W3CBaggagePropagator()]) # pylint: disable=import-outside-toplevel,no-name-in-module from opentelemetry.util._importlib_metadata import ( # noqa: PLC0415 entry_points, ) _propagators: list[textmap.TextMapPropagator] = [] for _propagator in configured.split(","): _propagator = _propagator.strip() if _propagator.lower() == "none": logger.debug("OTEL_PROPAGATORS environment variable contains none, removing all propagators") return composite.CompositePropagator([]) try: _propagators.append( next( # type: ignore iter( # type: ignore entry_points( # type: ignore[misc] group="opentelemetry_propagator", name=_propagator, ) ) ).load()() ) except StopIteration: raise ValueError(f"Propagator {_propagator} not found. It is either misspelled or not installed.") except Exception: # pylint: disable=broad-exception-caught logger.exception("Failed to load propagator: %s", _propagator) raise return composite.CompositePropagator(_propagators) _HTTP_TEXT_FORMAT: textmap.TextMapPropagator = _load_propagators() # Deprecated: propagators, environ_propagators and propagator names were never intendended to be part of the public API. propagators: list[textmap.TextMapPropagator] = [] environ_propagators = environ.get(OTEL_PROPAGATORS, "tracecontext,baggage") for propagator in environ_propagators.split(","): # type: ignore[assignment] propagator = propagator.strip() def get_global_textmap() -> textmap.TextMapPropagator: return _HTTP_TEXT_FORMAT def set_global_textmap( http_text_format: textmap.TextMapPropagator, ) -> None: global _HTTP_TEXT_FORMAT # pylint:disable=global-statement _HTTP_TEXT_FORMAT = http_text_format python-opentelemetry-1.45.2/opentelemetry-api/src/opentelemetry/propagate/py.typed000066400000000000000000000000001527070070700306200ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-api/src/opentelemetry/propagators/000077500000000000000000000000001527070070700275125ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-api/src/opentelemetry/propagators/_envcarrier.py000066400000000000000000000060551527070070700323710ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 """Environment variable carriers for text map propagators. Use :class:`EnvironmentGetter` with the environment mapping to extract from, usually ``os.environ`` during application or child-process initialization. Use :class:`EnvironmentSetter` with a mutable environment copy when preparing the environment for a child process. """ import re from collections.abc import Mapping, MutableMapping from opentelemetry.propagators.textmap import Getter, Setter def _normalize_key(key: str) -> str: if not key: return "_" result = re.sub(r"[^A-Za-z0-9_]", "_", key).upper() if result and result[0].isdigit(): result = "_" + result return result def _is_normalized_key(key: str) -> bool: if not key: return False if "0" <= key[0] <= "9": return False return all("A" <= char <= "Z" or "0" <= char <= "9" or char == "_" for char in key) class EnvironmentGetter(Getter[Mapping[str, str]]): """Getter implementation for extracting context and baggage from environment variables. EnvironmentGetter reads from the mapping provided as the carrier, normalizes requested keys, and provides simple data access without validation. Example usage: getter = EnvironmentGetter() traceparent = getter.get(os.environ, "traceparent") """ def get(self, carrier: Mapping[str, str], key: str) -> list[str] | None: """Get a value from the environment carrier for the given key. Args: carrier: Mapping to read environment variables from key: The key to look up (will be normalized) Returns: A list with a single string value if the key exists, None otherwise. """ val = carrier.get(_normalize_key(key)) if val is None: return None return [val] def keys(self, carrier: Mapping[str, str]) -> list[str]: """Get all keys from the environment carrier. Args: carrier: Mapping to read environment variable keys from Returns: List of all already-normalized environment variable keys. """ return [key for key in carrier.keys() if _is_normalized_key(key)] class EnvironmentSetter(Setter[MutableMapping[str, str]]): """Setter implementation for building environment variable dictionaries. EnvironmentSetter builds a dictionary of environment variables that can be passed to utilities like subprocess.run() Example usage: setter = EnvironmentSetter() env_vars = {} setter.set(env_vars, "traceparent", "00-trace-id-span-id-01") subprocess.run(myCommand, env=env_vars) """ def set(self, carrier: MutableMapping[str, str], key: str, value: str) -> None: """Set a value in the carrier dictionary for the given key. Args: carrier: Dictionary to store environment variables key: The key to set (normalized) value: The value to set """ carrier[_normalize_key(key)] = value python-opentelemetry-1.45.2/opentelemetry-api/src/opentelemetry/propagators/composite.py000066400000000000000000000052441527070070700320730ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import collections.abc import logging from typing_extensions import deprecated from opentelemetry.context.context import Context from opentelemetry.propagators import textmap logger = logging.getLogger(__name__) class CompositePropagator(textmap.TextMapPropagator): """CompositePropagator provides a mechanism for combining multiple propagators into a single one. Args: propagators: the list of propagators to use """ def __init__(self, propagators: collections.abc.Sequence[textmap.TextMapPropagator]) -> None: self._propagators = propagators def extract( self, carrier: textmap.CarrierT, context: Context | None = None, getter: textmap.Getter[textmap.CarrierT] = textmap.default_getter, ) -> Context: """Run each of the configured propagators with the given context and carrier. Propagators are run in the order they are configured, if multiple propagators write the same context key, the propagator later in the list will override previous propagators. See `opentelemetry.propagators.textmap.TextMapPropagator.extract` """ for propagator in self._propagators: context = propagator.extract(carrier, context, getter=getter) return context # type: ignore def inject( self, carrier: textmap.CarrierT, context: Context | None = None, setter: textmap.Setter[textmap.CarrierT] = textmap.default_setter, ) -> None: """Run each of the configured propagators with the given context and carrier. Propagators are run in the order they are configured, if multiple propagators write the same carrier key, the propagator later in the list will override previous propagators. See `opentelemetry.propagators.textmap.TextMapPropagator.inject` """ for propagator in self._propagators: propagator.inject(carrier, context, setter=setter) @property def fields(self) -> set[str]: """Returns a set with the fields set in `inject`. See `opentelemetry.propagators.textmap.TextMapPropagator.fields` """ composite_fields = set() for propagator in self._propagators: for field in propagator.fields: composite_fields.add(field) return composite_fields @deprecated("You should use CompositePropagator. Deprecated since version 1.2.0.") class CompositeHTTPPropagator(CompositePropagator): """CompositeHTTPPropagator provides a mechanism for combining multiple propagators into a single one. """ python-opentelemetry-1.45.2/opentelemetry-api/src/opentelemetry/propagators/py.typed000066400000000000000000000000001527070070700311770ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-api/src/opentelemetry/propagators/textmap.py000066400000000000000000000136011527070070700315470ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import abc import typing from collections.abc import Iterable, Mapping, MutableMapping from opentelemetry.context.context import Context CarrierT = typing.TypeVar("CarrierT") # pylint: disable=invalid-name CarrierValT = list[str] | str class Getter(abc.ABC, typing.Generic[CarrierT]): """This class implements a Getter that enables extracting propagated fields from a carrier. """ @abc.abstractmethod def get(self, carrier: CarrierT, key: str) -> list[str] | None: """Function that can retrieve zero or more values from the carrier. In the case that the value does not exist, returns None. Args: carrier: An object which contains values that are used to construct a Context. key: key of a field in carrier. Returns: first value of the propagation key or None if the key doesn't exist. """ @abc.abstractmethod def keys(self, carrier: CarrierT) -> list[str]: """Function that can retrieve all the keys in a carrier object. Args: carrier: An object which contains values that are used to construct a Context. Returns: list of keys from the carrier. """ class Setter(abc.ABC, typing.Generic[CarrierT]): """This class implements a Setter that enables injecting propagated fields into a carrier. """ @abc.abstractmethod def set(self, carrier: CarrierT, key: str, value: str) -> None: """Function that can set a value into a carrier"" Args: carrier: An object which contains values that are used to construct a Context. key: key of a field in carrier. value: value for a field in carrier. """ class DefaultGetter(Getter[Mapping[str, CarrierValT]]): def get(self, carrier: Mapping[str, CarrierValT], key: str) -> list[str] | None: """Getter implementation to retrieve a value from a dictionary. Args: carrier: dictionary in which to get value key: the key used to get the value Returns: A list with a single string with the value if it exists, else None. """ val = carrier.get(key, None) if val is None: return None if isinstance(val, Iterable) and not isinstance(val, str): return list(val) return [val] def keys(self, carrier: Mapping[str, CarrierValT]) -> list[str]: """Keys implementation that returns all keys from a dictionary.""" return list(carrier.keys()) default_getter: Getter[CarrierT] = DefaultGetter() # type: ignore class DefaultSetter(Setter[MutableMapping[str, CarrierValT]]): def set( self, carrier: MutableMapping[str, CarrierValT], key: str, value: CarrierValT, ) -> None: """Setter implementation to set a value into a dictionary. Args: carrier: dictionary in which to set value key: the key used to set the value value: the value to set """ carrier[key] = value default_setter: Setter[CarrierT] = DefaultSetter() # type: ignore class TextMapPropagator(abc.ABC): """This class provides an interface that enables extracting and injecting context into headers of HTTP requests. HTTP frameworks and clients can integrate with TextMapPropagator by providing the object containing the headers, and a getter and setter function for the extraction and injection of values, respectively. """ @abc.abstractmethod def extract( self, carrier: CarrierT, context: Context | None = None, getter: Getter[CarrierT] = default_getter, ) -> Context: """Create a Context from values in the carrier. The extract function should retrieve values from the carrier object using getter, and use values to populate a Context value and return it. Args: getter: a function that can retrieve zero or more values from the carrier. In the case that the value does not exist, return an empty list. carrier: and 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: an optional Context to use. Defaults to root context if not set. Returns: A Context with configuration found in the carrier. """ @abc.abstractmethod def inject( self, carrier: CarrierT, context: Context | None = None, setter: Setter[CarrierT] = default_setter, ) -> None: """Inject values from a Context into a carrier. inject enables the propagation of values into HTTP clients or other objects which perform an HTTP request. Implementations should use the `Setter` 's set method to set values on the carrier. Args: carrier: An object that a place to define HTTP headers. Should be paired with setter, which should know how to set header values on the carrier. context: an optional Context to use. Defaults to current context if not set. setter: An optional `Setter` object that can set values on the carrier. """ @property @abc.abstractmethod def fields(self) -> set[str]: """ Gets the fields set in the carrier by the `inject` method. If the carrier is reused, its fields that correspond with the ones present in this attribute should be deleted before calling `inject`. Returns: A set with the fields set in `inject`. """ python-opentelemetry-1.45.2/opentelemetry-api/src/opentelemetry/py.typed000066400000000000000000000000001527070070700266360ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-api/src/opentelemetry/trace/000077500000000000000000000000001527070070700262475ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-api/src/opentelemetry/trace/__init__.py000066400000000000000000000552241527070070700303700ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 """ The OpenTelemetry tracing API describes the classes used to generate distributed traces. The :class:`.Tracer` class controls access to the execution context, and manages span creation. Each operation in a trace is represented by a :class:`.Span`, which records the start, end time, and metadata associated with the operation. This module provides abstract (i.e. unimplemented) classes required for tracing, and a concrete no-op :class:`.NonRecordingSpan` that allows applications to use the API package alone without a supporting implementation. To get a tracer, you need to provide the package name from which you are calling the tracer APIs to OpenTelemetry by calling `TracerProvider.get_tracer` with the calling module name and the version of your package. The tracer supports creating spans that are "attached" or "detached" from the context. New spans are "attached" to the context in that they are created as children of the currently active span, and the newly-created span can optionally become the new active span:: from opentelemetry import trace tracer = trace.get_tracer(__name__) # Create a new root span, set it as the current span in context with tracer.start_as_current_span("parent"): # Attach a new child and update the current span with tracer.start_as_current_span("child"): do_work(): # Close child span, set parent as current # Close parent span, set default span as current When creating a span that's "detached" from the context the active span doesn't change, and the caller is responsible for managing the span's lifetime:: # Explicit parent span assignment is done via the Context from opentelemetry.trace import set_span_in_context context = set_span_in_context(parent) child = tracer.start_span("child", context=context) try: do_work(span=child) finally: child.end() Applications should generally use a single global TracerProvider, and use either implicit or explicit context propagation consistently throughout. .. versionadded:: 0.1.0 .. versionchanged:: 0.3.0 `TracerProvider` was introduced and the global ``tracer`` getter was replaced by ``tracer_provider``. .. versionchanged:: 0.5.0 ``tracer_provider`` was replaced by `get_tracer_provider`, ``set_preferred_tracer_provider_implementation`` was replaced by `set_tracer_provider`. """ import os from abc import ABC, abstractmethod from collections.abc import Iterator, Sequence from enum import Enum from logging import getLogger from typing import cast from typing_extensions import deprecated from opentelemetry import context as context_api from opentelemetry.attributes import BoundedAttributes from opentelemetry.context.context import Context from opentelemetry.environment_variables import OTEL_PYTHON_TRACER_PROVIDER from opentelemetry.trace.propagation import ( _SPAN_KEY, get_current_span, set_span_in_context, ) from opentelemetry.trace.span import ( DEFAULT_TRACE_OPTIONS, DEFAULT_TRACE_STATE, INVALID_SPAN, INVALID_SPAN_CONTEXT, INVALID_SPAN_ID, INVALID_TRACE_ID, NonRecordingSpan, Span, SpanContext, TraceFlags, TraceState, format_span_id, format_trace_id, ) from opentelemetry.trace.status import Status, StatusCode from opentelemetry.util import types from opentelemetry.util._decorator import _agnosticcontextmanager from opentelemetry.util._once import Once from opentelemetry.util._providers import _load_provider logger = getLogger(__name__) class _LinkBase(ABC): def __init__(self, context: "SpanContext") -> None: self._context = context @property def context(self) -> "SpanContext": return self._context @property @abstractmethod def attributes(self) -> types.Attributes: pass class Link(_LinkBase): """A link to a `Span`. The attributes of a Link are immutable. Args: context: `SpanContext` of the `Span` to link to. attributes: Link's attributes. """ def __init__( self, context: "SpanContext", attributes: types.Attributes = None, ) -> None: super().__init__(context) self._attributes = attributes @property def attributes(self) -> types.Attributes: return self._attributes @property def dropped_attributes(self) -> int: if isinstance(self._attributes, BoundedAttributes): return self._attributes.dropped return 0 _Links = Sequence[Link] | None class SpanKind(Enum): """Specifies additional details on how this span relates to its parent span. Note that this enumeration is experimental and likely to change. See https://github.com/open-telemetry/opentelemetry-specification/pull/226. """ #: Default value. Indicates that the span is used internally in the # application. INTERNAL = 0 #: Indicates that the span describes an operation that handles a remote # request. SERVER = 1 #: Indicates that the span describes a request to some remote service. CLIENT = 2 #: Indicates that the span describes a producer sending a message to a #: broker. Unlike client and server, there is usually no direct critical #: path latency relationship between producer and consumer spans. PRODUCER = 3 #: Indicates that the span describes a consumer receiving a message from a #: broker. Unlike client and server, there is usually no direct critical #: path latency relationship between producer and consumer spans. CONSUMER = 4 class TracerProvider(ABC): @abstractmethod def get_tracer( self, instrumenting_module_name: str, instrumenting_library_version: str | None = None, schema_url: str | None = None, attributes: types.Attributes = None, ) -> "Tracer": """Returns a `Tracer` for use by the given instrumentation library. For any two calls it is undefined whether the same or different `Tracer` instances are returned, even for different library names. This function may return different `Tracer` types (e.g. a no-op tracer vs. a functional tracer). Args: instrumenting_module_name: The uniquely identifiable name for instrumentation scope, such as instrumentation library, package, module or class name. ``__name__`` should be avoided as this can result in different tracer names if the tracers are in different files. It is better to use a fixed string that can be imported where needed and used consistently as the name of the tracer. This should *not* be the name of the module that is instrumented but the name of the module doing the instrumentation. E.g., instead of ``"requests"``, use ``"opentelemetry.instrumentation.requests"``. instrumenting_library_version: Optional. The version string of the instrumenting library. Usually this should be the same as ``importlib.metadata.version(instrumenting_library_name)``. schema_url: Optional. Specifies the Schema URL of the emitted telemetry. attributes: Optional. Specifies the attributes of the emitted telemetry. """ class NoOpTracerProvider(TracerProvider): """The default TracerProvider, used when no implementation is available. All operations are no-op. """ def get_tracer( self, instrumenting_module_name: str, instrumenting_library_version: str | None = None, schema_url: str | None = None, attributes: types.Attributes = None, ) -> "Tracer": # pylint:disable=no-self-use,unused-argument return NoOpTracer() @deprecated("You should use NoOpTracerProvider. Deprecated since version 1.9.0.") class _DefaultTracerProvider(NoOpTracerProvider): """The default TracerProvider, used when no implementation is available. All operations are no-op. """ class ProxyTracerProvider(TracerProvider): def get_tracer( self, instrumenting_module_name: str, instrumenting_library_version: str | None = None, schema_url: str | None = None, attributes: types.Attributes = None, ) -> "Tracer": if _TRACER_PROVIDER: return _TRACER_PROVIDER.get_tracer( instrumenting_module_name, instrumenting_library_version, schema_url, attributes, ) return ProxyTracer( instrumenting_module_name, instrumenting_library_version, schema_url, attributes, ) class Tracer(ABC): """Handles span creation and in-process context propagation. This class provides methods for manipulating the context, creating spans, and controlling spans' lifecycles. """ @abstractmethod def start_span( self, name: str, context: Context | None = None, kind: SpanKind = SpanKind.INTERNAL, attributes: types.Attributes = None, links: _Links = None, start_time: int | None = None, record_exception: bool = True, set_status_on_exception: bool = True, ) -> "Span": """Starts a span. Create a new span. Start the span without setting it as the current span in the context. To start the span and use the context in a single method, see :meth:`start_as_current_span`. By default the current span in the context will be used as parent, but an explicit context can also be specified, by passing in a `Context` containing a current `Span`. If there is no current span in the global `Context` or in the specified context, the created span will be a root span. The span can be used as a context manager. On exiting the context manager, the span's end() method will be called. Example:: # trace.get_current_span() will be used as the implicit parent. # If none is found, the created span will be a root instance. with tracer.start_span("one") as child: child.add_event("child's event") Args: name: The name of the span to be created. context: An optional Context containing the span's parent. Defaults to the global context. kind: The span's kind (relationship to parent). Note that is meaningful even if there is no parent. attributes: The span's attributes. links: Links span to other spans start_time: Sets the start time of a span record_exception: Whether to record any exceptions raised within the context as error event on the span. set_status_on_exception: Only relevant if the returned span is used in a with/context manager. Defines whether the span status will be automatically set to ERROR when an uncaught exception is raised in the span with block. The span status won't be set by this mechanism if it was previously set manually. Returns: The newly-created span. """ @_agnosticcontextmanager @abstractmethod def start_as_current_span( self, name: str, context: Context | None = None, kind: SpanKind = SpanKind.INTERNAL, attributes: types.Attributes = None, links: _Links = None, start_time: int | None = None, record_exception: bool = True, set_status_on_exception: bool = True, end_on_exit: bool = True, ) -> Iterator["Span"]: """Context manager for creating a new span and set it as the current span in this tracer's context. Exiting the context manager will call the span's end method, as well as return the current span to its previous value by returning to the previous context. Example:: with tracer.start_as_current_span("one") as parent: parent.add_event("parent's event") with tracer.start_as_current_span("two") as child: child.add_event("child's event") trace.get_current_span() # returns child trace.get_current_span() # returns parent trace.get_current_span() # returns previously active span This is a convenience method for creating spans attached to the tracer's context. Applications that need more control over the span lifetime should use :meth:`start_span` instead. For example:: with tracer.start_as_current_span(name) as span: do_work() is equivalent to:: span = tracer.start_span(name) with opentelemetry.trace.use_span(span, end_on_exit=True): do_work() This can also be used as a decorator:: @tracer.start_as_current_span("name") def function(): ... function() Args: name: The name of the span to be created. context: An optional Context containing the span's parent. Defaults to the global context. kind: The span's kind (relationship to parent). Note that is meaningful even if there is no parent. attributes: The span's attributes. links: Links span to other spans start_time: Sets the start time of a span record_exception: Whether to record any exceptions raised within the context as error event on the span. set_status_on_exception: Only relevant if the returned span is used in a with/context manager. Defines whether the span status will be automatically set to ERROR when an uncaught exception is raised in the span with block. The span status won't be set by this mechanism if it was previously set manually. end_on_exit: Whether to end the span automatically when leaving the context manager. Yields: The newly-created span. """ class ProxyTracer(Tracer): # pylint: disable=W0222,signature-differs def __init__( self, instrumenting_module_name: str, instrumenting_library_version: str | None = None, schema_url: str | None = None, attributes: types.Attributes = None, ): self._instrumenting_module_name = instrumenting_module_name self._instrumenting_library_version = instrumenting_library_version self._schema_url = schema_url self._attributes = attributes self._real_tracer: Tracer | None = None self._noop_tracer = NoOpTracer() @property def _tracer(self) -> Tracer: if self._real_tracer: return self._real_tracer if _TRACER_PROVIDER: self._real_tracer = _TRACER_PROVIDER.get_tracer( self._instrumenting_module_name, self._instrumenting_library_version, self._schema_url, self._attributes, ) return self._real_tracer return self._noop_tracer def start_span(self, *args, **kwargs) -> Span: # type: ignore return self._tracer.start_span(*args, **kwargs) # type: ignore @_agnosticcontextmanager # type: ignore def start_as_current_span(self, *args, **kwargs) -> Iterator[Span]: with self._tracer.start_as_current_span(*args, **kwargs) as span: # type: ignore yield span class NoOpTracer(Tracer): """The default Tracer, used when no Tracer implementation is available. All operations are no-op. """ def start_span( self, name: str, context: Context | None = None, kind: SpanKind = SpanKind.INTERNAL, attributes: types.Attributes = None, links: _Links = None, start_time: int | None = None, record_exception: bool = True, set_status_on_exception: bool = True, ) -> "Span": current_span = get_current_span(context) if isinstance(current_span, NonRecordingSpan): return current_span parent_span_context = current_span.get_span_context() if parent_span_context is not None and not isinstance(parent_span_context, SpanContext): logger.warning( "Invalid span context for %s: %s", current_span, parent_span_context, ) return INVALID_SPAN return NonRecordingSpan(context=parent_span_context) @_agnosticcontextmanager def start_as_current_span( self, name: str, context: Context | None = None, kind: SpanKind = SpanKind.INTERNAL, attributes: types.Attributes = None, links: _Links = None, start_time: int | None = None, record_exception: bool = True, set_status_on_exception: bool = True, end_on_exit: bool = True, ) -> Iterator["Span"]: span = self.start_span( name=name, context=context, kind=kind, attributes=attributes, links=links, start_time=start_time, record_exception=record_exception, set_status_on_exception=set_status_on_exception, ) with use_span( span, end_on_exit=end_on_exit, record_exception=record_exception, set_status_on_exception=set_status_on_exception, ) as span: yield span @deprecated("You should use NoOpTracer. Deprecated since version 1.9.0.") class _DefaultTracer(NoOpTracer): """The default Tracer, used when no Tracer implementation is available. All operations are no-op. """ _TRACER_PROVIDER_SET_ONCE = Once() _TRACER_PROVIDER: TracerProvider | None = None _PROXY_TRACER_PROVIDER = ProxyTracerProvider() def get_tracer( instrumenting_module_name: str, instrumenting_library_version: str | None = None, tracer_provider: TracerProvider | None = None, schema_url: str | None = None, attributes: types.Attributes = None, ) -> "Tracer": """Returns a `Tracer` for use by the given instrumentation library. This function is a convenience wrapper for opentelemetry.trace.TracerProvider.get_tracer. If tracer_provider is omitted the current configured one is used. """ if tracer_provider is None: tracer_provider = get_tracer_provider() return tracer_provider.get_tracer( instrumenting_module_name, instrumenting_library_version, schema_url, attributes, ) def _set_tracer_provider(tracer_provider: TracerProvider, log: bool) -> None: def set_tp() -> None: global _TRACER_PROVIDER # pylint: disable=global-statement _TRACER_PROVIDER = tracer_provider did_set = _TRACER_PROVIDER_SET_ONCE.do_once(set_tp) if log and not did_set: logger.warning("Overriding of current TracerProvider is not allowed") def set_tracer_provider(tracer_provider: TracerProvider) -> None: """Sets the current global :class:`~.TracerProvider` object. This can only be done once, a warning will be logged if any further attempt is made. """ _set_tracer_provider(tracer_provider, log=True) def get_tracer_provider() -> TracerProvider: """Gets the current global :class:`~.TracerProvider` object.""" if _TRACER_PROVIDER is None: # if a global tracer provider has not been set either via code or env # vars, return a proxy tracer provider if OTEL_PYTHON_TRACER_PROVIDER not in os.environ: return _PROXY_TRACER_PROVIDER tracer_provider: TracerProvider = _load_provider(OTEL_PYTHON_TRACER_PROVIDER, "tracer_provider") _set_tracer_provider(tracer_provider, log=False) # _TRACER_PROVIDER will have been set by one thread return cast("TracerProvider", _TRACER_PROVIDER) @_agnosticcontextmanager def use_span( span: Span, end_on_exit: bool = False, record_exception: bool = True, set_status_on_exception: bool = True, ) -> Iterator[Span]: """Takes a non-active span and activates it in the current context. Args: span: The span that should be activated in the current context. end_on_exit: Whether to end the span automatically when leaving the context manager scope. record_exception: Whether to record any exceptions raised within the context as error event on the span. set_status_on_exception: Only relevant if the returned span is used in a with/context manager. Defines whether the span status will be automatically set to ERROR when an uncaught exception is raised in the span with block. The span status won't be set by this mechanism if it was previously set manually. """ try: token = context_api.attach(context_api.set_value(_SPAN_KEY, span)) try: yield span finally: context_api.detach(token) # Record only exceptions that inherit Exception class but not BaseException, because # classes that directly inherit BaseException are not technically errors, e.g. GeneratorExit. # See https://github.com/open-telemetry/opentelemetry-python/issues/4484 except Exception as exc: # pylint: disable=broad-exception-caught if isinstance(span, Span) and span.is_recording(): # Record the exception as an event if record_exception: span.record_exception(exc) # Set status in case exception was raised if set_status_on_exception: span.set_status( Status( status_code=StatusCode.ERROR, description=f"{type(exc).__name__}: {exc}", ) ) # This causes parent spans to set their status to ERROR and to record # an exception as an event if a child span raises an exception even if # such child span was started with both record_exception and # set_status_on_exception attributes set to False. raise finally: if end_on_exit: span.end() __all__ = [ "DEFAULT_TRACE_OPTIONS", "DEFAULT_TRACE_STATE", "INVALID_SPAN", "INVALID_SPAN_CONTEXT", "INVALID_SPAN_ID", "INVALID_TRACE_ID", "Link", "NonRecordingSpan", "Span", "SpanContext", "SpanKind", "Status", "StatusCode", "TraceFlags", "TraceState", "Tracer", "TracerProvider", "format_span_id", "format_trace_id", "get_current_span", "get_tracer", "get_tracer_provider", "set_span_in_context", "set_tracer_provider", "use_span", ] python-opentelemetry-1.45.2/opentelemetry-api/src/opentelemetry/trace/propagation/000077500000000000000000000000001527070070700305725ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-api/src/opentelemetry/trace/propagation/__init__.py000066400000000000000000000021601527070070700327020ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from opentelemetry.context import create_key, get_value, set_value from opentelemetry.context.context import Context from opentelemetry.trace.span import INVALID_SPAN, Span SPAN_KEY = "current-span" _SPAN_KEY = create_key("current-span") def set_span_in_context(span: Span, context: Context | None = None) -> Context: """Set the span in the given context. Args: span: The Span to set. context: a Context object. if one is not passed, the default current context is used instead. """ ctx = set_value(_SPAN_KEY, span, context=context) return ctx def get_current_span(context: Context | None = None) -> Span: """Retrieve the current span. Args: context: A Context object. If one is not passed, the default current context is used instead. Returns: The Span set in the context if it exists. INVALID_SPAN otherwise. """ span = get_value(_SPAN_KEY, context=context) if span is None or not isinstance(span, Span): return INVALID_SPAN return span python-opentelemetry-1.45.2/opentelemetry-api/src/opentelemetry/trace/propagation/tracecontext.py000066400000000000000000000106321527070070700336510ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # import logging import re 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 from opentelemetry.trace.span import ( _TRACECONTEXT_MAXIMUM_TRACESTATE_LENGTH, TraceState, ) _logger = logging.getLogger(__name__) class TraceContextTextMapPropagator(textmap.TextMapPropagator): """Extracts and injects using w3c TraceContext's headers.""" _TRACEPARENT_HEADER_NAME = "traceparent" _TRACESTATE_HEADER_NAME = "tracestate" _TRACEPARENT_HEADER_FORMAT = ( "^[ \t]*([0-9a-f]{2})-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})" + "(?:(-.*)|[ \t]*)$" ) _TRACEPARENT_HEADER_FORMAT_RE = re.compile(_TRACEPARENT_HEADER_FORMAT) _TRACEPARENT_MAXIMUM_LENGTH = 512 # pylint: disable-next=too-many-return-statements def extract( self, carrier: textmap.CarrierT, context: Context | None = None, getter: textmap.Getter[textmap.CarrierT] = textmap.default_getter, ) -> Context: """Extracts SpanContext from the carrier. See `opentelemetry.propagators.textmap.TextMapPropagator.extract` """ if context is None: context = Context() header = getter.get(carrier, self._TRACEPARENT_HEADER_NAME) if not header: return context if len(header[0]) > self._TRACEPARENT_MAXIMUM_LENGTH: _logger.warning( "traceparent header exceeded the maximum length of %d characters", self._TRACEPARENT_MAXIMUM_LENGTH, ) return context match = re.search(self._TRACEPARENT_HEADER_FORMAT_RE, header[0]) if not match: return context version: str = match.group(1) trace_id: str = match.group(2) span_id: str = match.group(3) trace_flags: str = match.group(4) if trace_id == "0" * 32 or span_id == "0" * 16: return context if version == "00": if match.group(5): # type: ignore return context if version == "ff": return context tracestate_headers = getter.get(carrier, self._TRACESTATE_HEADER_NAME) if tracestate_headers is None: tracestate = None elif ( # multiple header fields are combined with "," (RFC 9110, section 5.3) sum(len(header) for header in tracestate_headers) + len(tracestate_headers) - 1 > _TRACECONTEXT_MAXIMUM_TRACESTATE_LENGTH ): _logger.warning( "tracestate header exceeded the maximum length of %d characters", _TRACECONTEXT_MAXIMUM_TRACESTATE_LENGTH, ) tracestate = None else: tracestate = TraceState.from_header(tracestate_headers) span_context = trace.SpanContext( trace_id=int(trace_id, 16), span_id=int(span_id, 16), is_remote=True, trace_flags=trace.TraceFlags(int(trace_flags, 16)), trace_state=tracestate, ) return trace.set_span_in_context(trace.NonRecordingSpan(span_context), context) def inject( self, carrier: textmap.CarrierT, context: Context | None = None, setter: textmap.Setter[textmap.CarrierT] = textmap.default_setter, ) -> None: """Injects SpanContext into the carrier. See `opentelemetry.propagators.textmap.TextMapPropagator.inject` """ span = trace.get_current_span(context) span_context = span.get_span_context() if span_context == trace.INVALID_SPAN_CONTEXT: return traceparent_string = f"00-{format_trace_id(span_context.trace_id)}-{format_span_id(span_context.span_id)}-{span_context.trace_flags:02x}" setter.set(carrier, self._TRACEPARENT_HEADER_NAME, traceparent_string) if span_context.trace_state: tracestate_string = span_context.trace_state.to_header() setter.set(carrier, self._TRACESTATE_HEADER_NAME, tracestate_string) @property def fields(self) -> set[str]: """Returns a set with the fields set in `inject`. See `opentelemetry.propagators.textmap.TextMapPropagator.fields` """ return {self._TRACEPARENT_HEADER_NAME, self._TRACESTATE_HEADER_NAME} python-opentelemetry-1.45.2/opentelemetry-api/src/opentelemetry/trace/py.typed000066400000000000000000000000001527070070700277340ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-api/src/opentelemetry/trace/span.py000066400000000000000000000500771527070070700275730ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations import abc import logging import re import types as python_types import typing import warnings from collections.abc import Iterator, Mapping, Sequence from opentelemetry.trace.status import Status, StatusCode from opentelemetry.util import types # The key MUST begin with a lowercase letter or a digit, # and can only contain lowercase letters (a-z), digits (0-9), # underscores (_), dashes (-), asterisks (*), and forward slashes (/). # For multi-tenant vendor scenarios, an at sign (@) can be used to # prefix the vendor name. Vendors SHOULD set the tenant ID # at the beginning of the key. # key = ( lcalpha ) 0*255( lcalpha / DIGIT / "_" / "-"/ "*" / "/" ) # key = ( lcalpha / DIGIT ) 0*240( lcalpha / DIGIT / "_" / "-"/ "*" / "/" ) "@" lcalpha 0*13( lcalpha / DIGIT / "_" / "-"/ "*" / "/" ) # lcalpha = %x61-7A ; a-z _KEY_FORMAT = ( r"[a-z][_0-9a-z\-\*\/]{0,255}|" r"[a-z0-9][_0-9a-z\-\*\/]{0,240}@[a-z][_0-9a-z\-\*\/]{0,13}" ) _KEY_PATTERN = re.compile(_KEY_FORMAT) # The value is an opaque string containing up to 256 printable # ASCII [RFC0020] characters (i.e., the range 0x20 to 0x7E) # except comma (,) and (=). # value = 0*255(chr) nblk-chr # nblk-chr = %x21-2B / %x2D-3C / %x3E-7E # chr = %x20 / nblk-chr _VALUE_FORMAT = r"[\x20-\x2b\x2d-\x3c\x3e-\x7e]{0,255}[\x21-\x2b\x2d-\x3c\x3e-\x7e]" _VALUE_PATTERN = re.compile(_VALUE_FORMAT) _TRACECONTEXT_MAXIMUM_TRACESTATE_KEYS = 32 _TRACECONTEXT_MAXIMUM_TRACESTATE_LENGTH = 8192 _member_pattern = re.compile(f"({_KEY_FORMAT})=({_VALUE_FORMAT})") _logger = logging.getLogger(__name__) def _is_valid_pair(key: str, value: str) -> bool: return ( isinstance(key, str) and _KEY_PATTERN.fullmatch(key) is not None and isinstance(value, str) and _VALUE_PATTERN.fullmatch(value) is not None ) class Span(abc.ABC): """A span represents a single operation within a trace.""" @abc.abstractmethod def end(self, end_time: int | None = None) -> None: """Sets the current time as the span's end time. The span's end time is the wall time at which the operation finished. Only the first call to `end` should modify the span, and implementations are free to ignore or raise on further calls. """ @abc.abstractmethod def get_span_context(self) -> SpanContext: """Gets the span's SpanContext. Get an immutable, serializable identifier for this span that can be used to create new child spans. Returns: A :class:`opentelemetry.trace.SpanContext` with a copy of this span's immutable state. """ @abc.abstractmethod def set_attributes(self, attributes: Mapping[str, types.AnyValue]) -> None: """Sets Attributes. Sets Attributes with the key and value passed as arguments dict. Note: The behavior of `None` value attributes is undefined, and hence strongly discouraged. It is also preferred to set attributes at span creation, instead of calling this method later since samplers can only consider information already present during span creation. """ @abc.abstractmethod def set_attribute(self, key: str, value: types.AnyValue) -> None: """Sets an Attribute. Sets a single Attribute with the key and value passed as arguments. Note: The behavior of `None` value attributes is undefined, and hence strongly discouraged. It is also preferred to set attributes at span creation, instead of calling this method later since samplers can only consider information already present during span creation. """ @abc.abstractmethod def add_event( self, name: str, attributes: types.Attributes = None, timestamp: int | None = None, ) -> None: """Adds an `Event`. Adds a single `Event` with the name and, optionally, a timestamp and attributes passed as arguments. Implementations should generate a timestamp if the `timestamp` argument is omitted. """ def add_link( # pylint: disable=no-self-use self, context: SpanContext, attributes: types.Attributes = None, ) -> None: """Adds a `Link`. Adds a single `Link` with the `SpanContext` of the span to link to and, optionally, attributes passed as arguments. Implementations may ignore calls with an invalid span context if both attributes and TraceState are empty. Note: It is preferred to add links at span creation, instead of calling this method later since samplers can only consider information already present during span creation. """ warnings.warn( "Span.add_link() not implemented and will be a no-op. " "Use opentelemetry-sdk >= 1.23 to add links after span creation" ) @abc.abstractmethod def update_name(self, name: str) -> None: """Updates the `Span` name. This will override the name provided via :func:`opentelemetry.trace.Tracer.start_span`. Upon this update, any sampling behavior based on Span name will depend on the implementation. """ @abc.abstractmethod def is_recording(self) -> bool: """Returns whether this span will be recorded. Returns true if this Span is active and recording information like events with the add_event operation and attributes using set_attribute. """ @abc.abstractmethod def set_status( self, status: Status | StatusCode, description: str | None = None, ) -> None: """Sets the Status of the Span. If used, this will override the default Span status. """ @abc.abstractmethod def record_exception( self, exception: BaseException, attributes: types.Attributes = None, timestamp: int | None = None, escaped: bool = False, ) -> None: """Records an exception as a span event.""" def __enter__(self) -> Span: """Invoked when `Span` is used as a context manager. Returns the `Span` itself. """ return self def __exit__( self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: python_types.TracebackType | None, ) -> None: """Ends context manager and calls `end` on the `Span`.""" self.end() class TraceFlags(int): """A bitmask that represents options specific to the trace. Supported flags: - "sampled" (``0x01``): Indicates the trace may have been sampled upstream. - "random-trace-id" (``0x02``): Indicates the trace ID was generated randomly, with at least the 7 rightmost bytes (56 bits) selected with uniform distribution. See the `W3C Trace Context - Traceparent`_ spec for details. .. _W3C Trace Context - Traceparent: https://www.w3.org/TR/trace-context-2/#trace-flags """ DEFAULT = 0x00 SAMPLED = 0x01 RANDOM_TRACE_ID = 0x02 @classmethod def get_default(cls) -> TraceFlags: return cls(cls.DEFAULT) @property def sampled(self) -> bool: return bool(self & TraceFlags.SAMPLED) @property def random_trace_id(self) -> bool: return bool(self & TraceFlags.RANDOM_TRACE_ID) DEFAULT_TRACE_OPTIONS = TraceFlags.get_default() class TraceState(Mapping[str, str]): """A list of key-value pairs representing vendor-specific trace info. Keys and values are strings of up to 256 printable US-ASCII characters. Implementations should conform to the `W3C Trace Context - Tracestate`_ spec, which describes additional restrictions on valid field values. .. _W3C Trace Context - Tracestate: https://www.w3.org/TR/trace-context/#tracestate-field """ def __init__( self, entries: Sequence[tuple[str, str]] | None = None, ) -> None: self._dict = {} # type: dict[str, str] if entries is None: return if len(entries) > _TRACECONTEXT_MAXIMUM_TRACESTATE_KEYS: _logger.warning( "There can't be more than %s key/value pairs.", _TRACECONTEXT_MAXIMUM_TRACESTATE_KEYS, ) return for key, value in entries: if _is_valid_pair(key, value): if key in self._dict: _logger.warning("Duplicate key: %s found.", key) continue self._dict[key] = value else: _logger.warning("Invalid key/value pair (%s, %s) found.", key, value) def __contains__(self, item: object) -> bool: return item in self._dict def __getitem__(self, key: str) -> str: return self._dict[key] def __iter__(self) -> Iterator[str]: return iter(self._dict) def __len__(self) -> int: return len(self._dict) def __repr__(self) -> str: pairs = [f"{{key={key}, value={value}}}" for key, value in self._dict.items()] return str(pairs) def add(self, key: str, value: str) -> TraceState: """Adds a key-value pair to tracestate. The provided pair should adhere to w3c tracestate identifiers format. Args: key: A valid tracestate key to add value: A valid tracestate value to add Returns: A new TraceState with the modifications applied. If the provided key-value pair is invalid or results in tracestate that violates tracecontext specification, they are discarded and same tracestate will be returned. """ if not _is_valid_pair(key, value): _logger.warning("Invalid key/value pair (%s, %s) found.", key, value) return self # There can be a maximum of 32 pairs if len(self) >= _TRACECONTEXT_MAXIMUM_TRACESTATE_KEYS: _logger.warning("There can't be more 32 key/value pairs.") return self # Duplicate entries are not allowed if key in self._dict: _logger.warning("The provided key %s already exists.", key) return self new_state = [(key, value)] + list(self._dict.items()) return TraceState(new_state) def update(self, key: str, value: str) -> TraceState: """Updates a key-value pair in tracestate. The provided pair should adhere to w3c tracestate identifiers format. Note: This method performs an "upsert": if ``key`` is not already present it is added (when the tracestate is below the 32-entry limit), otherwise its value is updated. This upsert behaviour is intentional but goes beyond what the OpenTelemetry specification defines for ``update`` (https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/api.md#tracestate), and is kept for backwards compatibility with callers that rely on it. Args: key: A valid tracestate key to update value: A valid tracestate value to update for key Returns: A new TraceState with the modifications applied. If the provided pair is invalid, or adding a new key would exceed the maximum of 32 key/value pairs, they are discarded and the same tracestate is returned unchanged. Updating an existing key is always allowed, even at the maximum. """ if not _is_valid_pair(key, value): _logger.warning("Invalid key/value pair (%s, %s) found.", key, value) return self # Adding a new key at the maximum would push the tracestate over the # limit and cause the constructor to drop every entry. Return unchanged # instead of silently discarding existing state. if key not in self._dict and len(self._dict) >= _TRACECONTEXT_MAXIMUM_TRACESTATE_KEYS: _logger.warning("There can't be more 32 key/value pairs.") return self prev_state = self._dict.copy() prev_state.pop(key, None) new_state = [(key, value), *prev_state.items()] return TraceState(new_state) def delete(self, key: str) -> TraceState: """Deletes a key-value from tracestate. Args: key: A valid tracestate key to remove key-value pair from tracestate Returns: A new TraceState with the modifications applied. If the provided key-value pair is invalid or results in tracestate that violates tracecontext specification, they are discarded and same tracestate will be returned. """ if key not in self._dict: _logger.warning("The provided key %s doesn't exist.", key) return self prev_state = self._dict.copy() prev_state.pop(key) new_state = list(prev_state.items()) return TraceState(new_state) def to_header(self) -> str: """Creates a w3c tracestate header from a TraceState. Returns: A string that adheres to the w3c tracestate header format. """ return ",".join(key + "=" + value for key, value in self._dict.items()) @classmethod def from_header(cls, header_list: list[str]) -> TraceState: """Parses one or more w3c tracestate header into a TraceState. Args: header_list: one or more w3c tracestate headers. Returns: A valid TraceState that contains values extracted from the tracestate header. If the format of one headers is illegal, all values will be discarded and an empty tracestate will be returned. If the number of keys is beyond the maximum, all values will be discarded and an empty tracestate will be returned. """ pairs = {} # type: dict[str, str] for header in header_list: for member in header.split(","): # remove optional whitespace (RFC 9110, section 5.6.3) member = member.strip(" \t") # empty members are valid, but no need to process further. if not member: continue match = _member_pattern.fullmatch(member) if not match: _logger.warning( "Member doesn't match the w3c identifiers format %s", member, ) return cls() key, value = match.groups() # duplicate keys are not legal in header if key in pairs: return cls() pairs[key] = value return cls(list(pairs.items())) @classmethod def get_default(cls) -> TraceState: return cls() def keys(self) -> typing.KeysView[str]: return self._dict.keys() def items(self) -> typing.ItemsView[str, str]: return self._dict.items() def values(self) -> typing.ValuesView[str]: return self._dict.values() DEFAULT_TRACE_STATE = TraceState.get_default() _TRACE_ID_MAX_VALUE = 2**128 - 1 _SPAN_ID_MAX_VALUE = 2**64 - 1 class SpanContext(tuple[int, int, bool, "TraceFlags", "TraceState", bool]): """The state of a Span to propagate between processes. This class includes the immutable attributes of a :class:`.Span` that must be propagated to a span's children and across process boundaries. Args: trace_id: The ID of the trace that this span belongs to. span_id: This span's ID. is_remote: True if propagated from a remote parent. trace_flags: Trace options to propagate. trace_state: Tracing-system-specific info to propagate. """ def __new__( cls, trace_id: int, span_id: int, is_remote: bool, trace_flags: TraceFlags | None = DEFAULT_TRACE_OPTIONS, trace_state: TraceState | None = DEFAULT_TRACE_STATE, ) -> SpanContext: if trace_flags is None: trace_flags = DEFAULT_TRACE_OPTIONS if trace_state is None: trace_state = DEFAULT_TRACE_STATE is_valid = ( INVALID_TRACE_ID < trace_id <= _TRACE_ID_MAX_VALUE and INVALID_SPAN_ID < span_id <= _SPAN_ID_MAX_VALUE ) return tuple.__new__( cls, (trace_id, span_id, is_remote, trace_flags, trace_state, is_valid), ) def __getnewargs__( self, ) -> tuple[int, int, bool, TraceFlags, TraceState]: return ( self.trace_id, self.span_id, self.is_remote, self.trace_flags, self.trace_state, ) @property def trace_id(self) -> int: return self[0] # pylint: disable=unsubscriptable-object @property def span_id(self) -> int: return self[1] # pylint: disable=unsubscriptable-object @property def is_remote(self) -> bool: return self[2] # pylint: disable=unsubscriptable-object @property def trace_flags(self) -> TraceFlags: return self[3] # pylint: disable=unsubscriptable-object @property def trace_state(self) -> TraceState: return self[4] # pylint: disable=unsubscriptable-object @property def is_valid(self) -> bool: return self[5] # pylint: disable=unsubscriptable-object def __setattr__(self, *args: str) -> None: _logger.debug("Immutable type, ignoring call to set attribute", stack_info=True) def __delattr__(self, *args: str) -> None: _logger.debug( "Immutable type, ignoring call to delete attribute", stack_info=True, ) def __repr__(self) -> str: return f"{type(self).__name__}(trace_id=0x{format_trace_id(self.trace_id)}, span_id=0x{format_span_id(self.span_id)}, trace_flags=0x{self.trace_flags:02x}, trace_state={self.trace_state!r}, is_remote={self.is_remote})" class NonRecordingSpan(Span): """The Span that is used when no Span implementation is available. All operations are no-op except context propagation. """ def __init__(self, context: SpanContext) -> None: self._context = context def get_span_context(self) -> SpanContext: return self._context def is_recording(self) -> bool: return False def end(self, end_time: int | None = None) -> None: pass def set_attributes(self, attributes: Mapping[str, types.AnyValue]) -> None: pass def set_attribute(self, key: str, value: types.AnyValue) -> None: pass def add_event( self, name: str, attributes: types.Attributes = None, timestamp: int | None = None, ) -> None: pass def add_link( self, context: SpanContext, attributes: types.Attributes = None, ) -> None: pass def update_name(self, name: str) -> None: pass def set_status( self, status: Status | StatusCode, description: str | None = None, ) -> None: pass def record_exception( self, exception: BaseException, attributes: types.Attributes = None, timestamp: int | None = None, escaped: bool = False, ) -> None: pass def __repr__(self) -> str: return f"NonRecordingSpan({self._context!r})" INVALID_SPAN_ID = 0x0000000000000000 INVALID_TRACE_ID = 0x00000000000000000000000000000000 INVALID_SPAN_CONTEXT = SpanContext( trace_id=INVALID_TRACE_ID, span_id=INVALID_SPAN_ID, is_remote=False, trace_flags=DEFAULT_TRACE_OPTIONS, trace_state=DEFAULT_TRACE_STATE, ) INVALID_SPAN = NonRecordingSpan(INVALID_SPAN_CONTEXT) def format_trace_id(trace_id: int) -> str: """Convenience trace ID formatting method Args: trace_id: Trace ID int Returns: The trace ID (16 bytes) cast to a 32-character hexadecimal string """ return format(trace_id, "032x") def format_span_id(span_id: int) -> str: """Convenience span ID formatting method Args: span_id: Span ID int Returns: The span ID (8 bytes) cast to a 16-character hexadecimal string """ return format(span_id, "016x") python-opentelemetry-1.45.2/opentelemetry-api/src/opentelemetry/trace/status.py000066400000000000000000000036471527070070700301560ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import enum import logging logger = logging.getLogger(__name__) class StatusCode(enum.Enum): """Represents the canonical set of status codes of a finished Span.""" UNSET = 0 """The default status.""" OK = 1 """The operation has been validated by an Application developer or Operator to have completed successfully.""" ERROR = 2 """The operation contains an error.""" class Status: """Represents the status of a finished Span. Args: status_code: The canonical status code that describes the result status of the operation. description: An optional description of the status. """ def __init__( self, status_code: StatusCode = StatusCode.UNSET, description: str | None = None, ): self._status_code = status_code self._description = None if description: if not isinstance(description, str): logger.warning("Invalid status description type, expected str") return if status_code is not StatusCode.ERROR: logger.warning("description should only be set when status_code is set to StatusCode.ERROR") return self._description = description @property def status_code(self) -> StatusCode: """Represents the canonical status code of a finished Span.""" return self._status_code @property def description(self) -> str | None: """Status description""" return self._description @property def is_ok(self) -> bool: """Returns false if this represents an error, true otherwise.""" return self.is_unset or self._status_code is StatusCode.OK @property def is_unset(self) -> bool: """Returns true if unset, false otherwise.""" return self._status_code is StatusCode.UNSET python-opentelemetry-1.45.2/opentelemetry-api/src/opentelemetry/util/000077500000000000000000000000001527070070700261265ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-api/src/opentelemetry/util/_decorator.py000066400000000000000000000056571527070070700306360ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import contextlib import functools import inspect from collections.abc import Callable, Iterator from typing import TYPE_CHECKING, Generic, TypeVar V = TypeVar("V") R = TypeVar("R") # Return type Pargs = TypeVar("Pargs") # Generic type for arguments Pkwargs = TypeVar("Pkwargs") # Generic type for arguments # We don't actually depend on typing_extensions but we can use it in CI with this conditional # import. ParamSpec can be imported directly from typing after python 3.9 is dropped # https://peps.python.org/pep-0612/. if TYPE_CHECKING: from typing_extensions import ParamSpec P = ParamSpec("P") # Generic type for all arguments class _AgnosticContextManager( contextlib._GeneratorContextManager[R], Generic[R], ): # pylint: disable=protected-access """Context manager that can decorate both async and sync functions. This is an overridden version of the contextlib._GeneratorContextManager class that will decorate async functions with an async context manager to end the span AFTER the entire async function coroutine finishes. Else it will report near zero spans durations for async functions. We are overriding the contextlib._GeneratorContextManager class as reimplementing it is a lot of code to maintain and this class (even if it's marked as protected) doesn't seems like to be evolving a lot. For more information, see: https://github.com/open-telemetry/opentelemetry-python/pull/3633 """ def __enter__(self) -> R: """Reimplementing __enter__ to avoid the type error. The original __enter__ method returns Any type, but we want to return R. """ del self.args, self.kwds, self.func # type: ignore try: return next(self.gen) # type: ignore except StopIteration: raise RuntimeError("generator didn't yield") from None def __call__(self, func: V) -> V: # pyright: ignore [reportIncompatibleMethodOverride] if inspect.iscoroutinefunction(func): @functools.wraps(func) # type: ignore async def async_wrapper(*args: Pargs, **kwargs: Pkwargs) -> R: # pyright: ignore [reportInvalidTypeVarUse] with self._recreate_cm(): # type: ignore return await func(*args, **kwargs) # type: ignore return async_wrapper # type: ignore return super().__call__(func) # type: ignore def _agnosticcontextmanager( func: "Callable[P, Iterator[R]]", ) -> "Callable[P, _AgnosticContextManager[R]]": @functools.wraps(func) def helper(*args: Pargs, **kwargs: Pkwargs) -> _AgnosticContextManager[R]: # pyright: ignore [reportInvalidTypeVarUse] return _AgnosticContextManager(func, args, kwargs) # pyright: ignore [reportArgumentType] # Ignoring the type to keep the original signature of the function return helper # type: ignore[return-value] python-opentelemetry-1.45.2/opentelemetry-api/src/opentelemetry/util/_importlib_metadata.py000066400000000000000000000034511527070070700325030ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 """ Caching and compatibility wrapper for standard library ``importlib.metadata``. This module caches ``entry_points()`` to avoid reloading entry points from disk on every call. It also normalizes minor differences across python versions 3.10+. References to issues: - https://github.com/open-telemetry/opentelemetry-python/pull/4735 - https://github.com/open-telemetry/opentelemetry-python/pull/5203 """ import itertools from functools import cache from importlib.metadata import ( Distribution, EntryPoint, EntryPoints, PackageNotFoundError, distributions, requires, version, ) from importlib.metadata import entry_points as original_entry_points from typing import Any def _as_entry_points(eps: Any) -> EntryPoints: # Python versions greater than 3.11 return EntryPoints. if isinstance(eps, EntryPoints): return eps # In Python 3.10 and 3.11 entry_points() returns # a dict-like SelectableGroups object. # Use dict.values() instead of eps.values() to avoid the DeprecationWarning # that SelectableGroups raises when calling .values(). if isinstance(eps, dict): return EntryPoints(itertools.chain.from_iterable(dict.values(eps))) # This case should be unreachable, but is included as a fallback. return EntryPoints(ep for group in eps.groups for ep in eps.select(group=group)) @cache def _original_entry_points_cached() -> EntryPoints: return _as_entry_points(original_entry_points()) def entry_points(**params) -> EntryPoints: return _original_entry_points_cached().select(**params) __all__ = [ "Distribution", "EntryPoint", "EntryPoints", "PackageNotFoundError", "distributions", "entry_points", "requires", "version", ] python-opentelemetry-1.45.2/opentelemetry-api/src/opentelemetry/util/_once.py000066400000000000000000000016551527070070700275720ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from collections.abc import Callable from threading import Lock class Once: """Execute a function exactly once and block all callers until the function returns Same as golang's `sync.Once `_ """ def __init__(self) -> None: self._lock = Lock() self._done = False def do_once(self, func: Callable[[], None]) -> bool: """Execute ``func`` if it hasn't been executed or return. Will block until ``func`` has been called by one thread. Returns: Whether or not ``func`` was executed in this call """ # fast path, try to avoid locking if self._done: return False with self._lock: if not self._done: func() self._done = True return True return False python-opentelemetry-1.45.2/opentelemetry-api/src/opentelemetry/util/_providers.py000066400000000000000000000024451527070070700306610ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from logging import getLogger from os import environ from typing import TYPE_CHECKING, TypeVar, cast if TYPE_CHECKING: from opentelemetry.metrics import MeterProvider from opentelemetry.trace import TracerProvider Provider = TypeVar("Provider", "TracerProvider", "MeterProvider") logger = getLogger(__name__) def _load_provider(provider_environment_variable: str, provider: str) -> Provider: # type: ignore[type-var] # pylint: disable=import-outside-toplevel,no-name-in-module from opentelemetry.util._importlib_metadata import ( # noqa: PLC0415 entry_points, ) try: provider_name = cast( str, environ.get(provider_environment_variable, f"default_{provider}"), ) return cast( Provider, next( # type: ignore iter( # type: ignore entry_points( # type: ignore group=f"opentelemetry_{provider}", name=provider_name, ) ) ).load()(), ) except Exception: # pylint: disable=broad-exception-caught logger.exception("Failed to load configured provider %s", provider) raise python-opentelemetry-1.45.2/opentelemetry-api/src/opentelemetry/util/py.typed000066400000000000000000000000001527070070700276130ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-api/src/opentelemetry/util/re.py000066400000000000000000000077441527070070700271220ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from collections.abc import Mapping from logging import getLogger from re import compile, split from urllib.parse import unquote from typing_extensions import deprecated _logger = getLogger(__name__) # The following regexes reference this spec: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/protocol/exporter.md#specifying-headers-via-environment-variables # Optional whitespace _OWS = r"[ \t]*" # A key contains printable US-ASCII characters except: SP and "(),/:;<=>?@[\]{} _KEY_FORMAT = r"[\x21\x23-\x27\x2a\x2b\x2d\x2e\x30-\x39\x41-\x5a\x5e-\x7a\x7c\x7e]+" # A value contains a URL-encoded UTF-8 string. The encoded form can contain any # printable US-ASCII characters (0x20-0x7f) other than SP, DEL, and ",;/ _VALUE_FORMAT = r"[\x21\x23-\x2b\x2d-\x3a\x3c-\x5b\x5d-\x7e]*" # Like above with SP included _LIBERAL_VALUE_FORMAT = r"[\x20\x21\x23-\x2b\x2d-\x3a\x3c-\x5b\x5d-\x7e]*" # A key-value is key=value, with optional whitespace surrounding key and value _KEY_VALUE_FORMAT = rf"{_OWS}{_KEY_FORMAT}{_OWS}={_OWS}{_VALUE_FORMAT}{_OWS}" _HEADER_PATTERN = compile(_KEY_VALUE_FORMAT) _LIBERAL_HEADER_PATTERN = compile(rf"{_OWS}{_KEY_FORMAT}{_OWS}={_OWS}{_LIBERAL_VALUE_FORMAT}{_OWS}") _DELIMITER_PATTERN = compile(r"[ \t]*,[ \t]*") _BAGGAGE_PROPERTY_FORMAT = rf"{_KEY_VALUE_FORMAT}|{_OWS}{_KEY_FORMAT}{_OWS}" _INVALID_HEADER_ERROR_MESSAGE_STRICT_TEMPLATE = ( "Header format invalid! Header values in environment variables must be " "URL encoded per the OpenTelemetry Protocol Exporter specification: %s" ) _INVALID_HEADER_ERROR_MESSAGE_LIBERAL_TEMPLATE = ( "Header format invalid! Header values in environment variables must be " "URL encoded per the OpenTelemetry Protocol Exporter specification or " "a comma separated list of name=value occurrences: %s" ) # pylint: disable=invalid-name @deprecated("You should use parse_env_headers. Deprecated since version 1.15.0.") def parse_headers(s: str) -> Mapping[str, str]: return parse_env_headers(s) def parse_env_headers(s: str, liberal: bool = False) -> Mapping[str, str]: """ Parse ``s``, which is a ``str`` instance containing HTTP headers encoded for use in ENV variables per the W3C Baggage HTTP header format at https://www.w3.org/TR/baggage/#baggage-http-header-format, except that additional semi-colon delimited metadata is not supported. If ``liberal`` is True we try to parse ``s`` anyway to be more compatible with other languages SDKs that accept non URL-encoded headers by default. """ headers: dict[str, str] = {} headers_list: list[str] = split(_DELIMITER_PATTERN, s) for header in headers_list: if not header: # empty string continue header_match = _HEADER_PATTERN.fullmatch(header.strip()) if not header_match and not liberal: _logger.warning(_INVALID_HEADER_ERROR_MESSAGE_STRICT_TEMPLATE, header) continue if header_match: match_string: str = header_match.string # value may contain any number of `=` name, value = match_string.split("=", 1) name = unquote(name).strip().lower() value = unquote(value).strip() headers[name] = value else: # this is not url-encoded and does not match the spec but we decided to be # liberal in what we accept to match other languages SDKs behaviour liberal_header_match = _LIBERAL_HEADER_PATTERN.fullmatch(header.strip()) if not liberal_header_match: _logger.warning(_INVALID_HEADER_ERROR_MESSAGE_LIBERAL_TEMPLATE, header) continue liberal_match_string: str = liberal_header_match.string # value may contain any number of `=` name, value = liberal_match_string.split("=", 1) name = name.strip().lower() value = value.strip() headers[name] = value return headers python-opentelemetry-1.45.2/opentelemetry-api/src/opentelemetry/util/types.py000066400000000000000000000016361527070070700276520ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from collections.abc import Mapping, Sequence from typing import TypeAlias # This is the implementation of the "Any" type as specified by the specifications of OpenTelemetry data model for logs. # For more details, refer to the OTel specification: # https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/logs/data-model.md#type-any AnyValue: TypeAlias = str | bool | int | float | bytes | Sequence["AnyValue"] | Mapping[str, "AnyValue"] | None AttributeValue: TypeAlias = AnyValue Attributes = Mapping[str, AnyValue] | None # Deprecated type, do not use. AttributesAsKey = tuple[ tuple[ str, str | bool | int | float | tuple[str | None, ...] | tuple[bool | None, ...] | tuple[int | None, ...] | tuple[float | None, ...], ], ..., ] python-opentelemetry-1.45.2/opentelemetry-api/src/opentelemetry/version/000077500000000000000000000000001527070070700266365ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-api/src/opentelemetry/version/__init__.py000066400000000000000000000001441527070070700307460ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 __version__ = "1.45.2" python-opentelemetry-1.45.2/opentelemetry-api/src/opentelemetry/version/py.typed000066400000000000000000000000001527070070700303230ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-api/test-requirements.txt000066400000000000000000000004221527070070700257250ustar00rootroot00000000000000asgiref==3.7.2 iniconfig==2.0.0 packaging==24.0 pluggy==1.6.0 py-cpuinfo==9.0.0 pytest==9.0.3 tomli==2.0.1 typing_extensions==4.10.0 wrapt==1.16.0 zipp==3.20.2 -e opentelemetry-sdk -e opentelemetry-semantic-conventions -e opentelemetry-api -e tests/opentelemetry-test-utils python-opentelemetry-1.45.2/opentelemetry-api/tests/000077500000000000000000000000001527070070700226305ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-api/tests/__init__.py000066400000000000000000000001141527070070700247350ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 python-opentelemetry-1.45.2/opentelemetry-api/tests/attributes/000077500000000000000000000000001527070070700250165ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-api/tests/attributes/test_attributes.py000066400000000000000000000223711527070070700306220ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # type: ignore import copy import logging import threading import unittest import unittest.mock from opentelemetry.attributes import ( BoundedAttributes, _clean_attribute_value, ) class _NoStrNoReprObject: def __init__(self): pass class TestBoundedAttributes(unittest.TestCase): # pylint: disable=consider-using-dict-items base = { "name": "Firulais", "age": 7, "weight": 13, "vaccinated": True, } def test_clean_attribute_value_with_various_params(self): # A python object that isn't a primitive and has no string/repr method is converted to None. with self.assertLogs("opentelemetry", level="WARNING") as cm: self.assertEqual( _clean_attribute_value(_NoStrNoReprObject(), None), None, ) self.assertEqual(len(cm.output), 1) self.assertIn("Expected one of bool, str, None, bytes, int, float", cm.output[0]) valid_primitive_sequence = [1, 2.2, None, "cookie"] self.assertEqual( _clean_attribute_value(valid_primitive_sequence, None), tuple(valid_primitive_sequence), ) for valid_primitive in valid_primitive_sequence: self.assertEqual(_clean_attribute_value(valid_primitive, None), valid_primitive) self.assertEqual(_clean_attribute_value(b"hello", 4), b"hello") with self.assertLogs("opentelemetry", level="WARNING") as cm: # String is truncated. self.assertEqual(_clean_attribute_value("a" * 1000, 5), "aaaaa") self.assertEqual(len(cm.output), 1) self.assertIn("String attribute value exceeds max length", cm.output[0]) # Sequence of different types of values. List converted to tuple. self.assertEqual( _clean_attribute_value(["a", 2, _NoStrNoReprObject(), None, b"\xff"], None), ("a", 2, None, None, b"\xff"), ) # non-str key in map... will be converted to string with self.assertLogs("opentelemetry", level="WARNING") as cm: self.assertEqual(_clean_attribute_value({2.2: 4.4}, None), {"2.2": 4.4}) self.assertEqual(len(cm.output), 1) self.assertIn( "Invalid type", cm.output[0], ) # Mapping of values.. # Non string key without a string conversion method is dropped. self.assertEqual( _clean_attribute_value( { "a": 1, _NoStrNoReprObject(): 2, "c": 3, "d": [2, 3], "bytes": b"\xff", }, None, ), {"a": 1, "c": 3, "d": (2, 3), "bytes": b"\xff"}, ) def test_same_key_value_overwritten(self): bdict = BoundedAttributes(1, {"name": "Firulais"}, immutable=False) self.assertEqual(bdict["name"], "Firulais") self.assertEqual(bdict.dropped, 0) bdict["name"] = "Bruno" self.assertEqual(bdict["name"], "Bruno") self.assertEqual(bdict.dropped, 0) def test_invalid_key_not_used(self): bdict = BoundedAttributes(50, {}, immutable=False) with self.assertLogs("opentelemetry", level="WARNING") as cm: bdict[1] = 2 self.assertEqual(len(cm.output), 1) self.assertIn("invalid key", cm.output[0]) self.assertNotIn(1, bdict) self.assertEqual(bdict.dropped, 1) def test_maxlen_reached(self): bdict = BoundedAttributes(1, {"first": "value"}, immutable=False) with self.assertLogs("opentelemetry", level="WARNING") as cm: bdict["second"] = "another" self.assertIn("Attributes dict is full. Dropping the oldest", cm.output[0]) self.assertNotIn("first", bdict) self.assertEqual(bdict["second"], "another") self.assertEqual(bdict.dropped, 1) def test_negative_maxlen_not_allowed(self): with self.assertRaises(ValueError): BoundedAttributes(-1) def test_negative_max_value_len_not_allowed(self): with self.assertRaises(ValueError): BoundedAttributes(1, {"first": "value"}, immutable=False, max_value_len=-1) def test_base_copy_isolated_and_len_works(self): dic_len = len(self.base) base_copy = self.base.copy() bdict = BoundedAttributes(dic_len, base_copy) self.assertEqual(len(bdict), dic_len) # modify base_copy and test that bdict is not changed base_copy["name"] = "Bruno" base_copy["age"] = 3 for key in self.base: self.assertEqual(bdict[key], self.base[key]) # test that iter yields the correct number of elements self.assertEqual(len(tuple(bdict)), dic_len) def test_basic_insertion_update_iteration_and_deletion(self): # create empty dict dic_len = len(self.base) bdict = BoundedAttributes(dic_len, immutable=False) self.assertEqual(len(bdict), 0) # fill dict for key in self.base: bdict[key] = self.base[key] self.assertEqual(len(bdict), dic_len) self.assertEqual(bdict.dropped, 0) for key in self.base: self.assertEqual(bdict[key], self.base[key]) # test __iter__ in BoundedAttributes for key in bdict: self.assertEqual(bdict[key], self.base[key]) # updating an existing element should not drop bdict["name"] = "Bruno" self.assertEqual(bdict.dropped, 0) # try to append more elements, old elements should be dropped for key in self.base: bdict["new-" + key] = self.base[key] self.assertEqual(len(bdict), dic_len) self.assertEqual(bdict.dropped, dic_len) # test that elements in the dict are the new ones for key in self.base: self.assertEqual(bdict["new-" + key], self.base[key]) # delete an element del bdict["new-name"] self.assertEqual(len(bdict), dic_len - 1) with self.assertRaises(KeyError): _ = bdict["new-name"] def test_immutable(self): bdict = BoundedAttributes() with self.assertRaises(TypeError): bdict["should-not-work"] = "dict immutable" def test_no_deadlock_on_reentrant_logging(self): """Regression test for #3858. The deadlock scenario: a logging handler intercepts the warning emitted by _clean_attribute for an invalid value and calls __setitem__ on the same BoundedAttributes instance from the same thread. With _clean_attribute called inside the lock this caused a deadlock. With _clean_attribute called before the lock is acquired, no deadlock occurs. """ bdict = BoundedAttributes(immutable=False, max_value_len=20) class ReentrantHandler(logging.Handler): def emit(self, _record): # Simulates Sentry intercepting the OTel warning and writing # back into the same BoundedAttributes on the same thread. bdict["reentrant.key"] = "set_by_handler" otel_logger = logging.getLogger("opentelemetry.attributes") handler = ReentrantHandler() otel_logger.addHandler(handler) try: completed = threading.Event() def run(): # A string value > 20 triggers a warning log in _clean_attribute, which fires the ReentrantHandler above. bdict["trigger.key"] = "a" * 21 completed.set() thread = threading.Thread(target=run, daemon=True) thread.start() thread.join(timeout=2.0) self.assertTrue( completed.is_set(), "Deadlock detected: __setitem__ did not complete within 2s", ) self.assertEqual(bdict.get("reentrant.key"), "set_by_handler") finally: otel_logger.removeHandler(handler) def test_wsgi_request_conversion_to_string(self): """Test that WSGI request objects are converted to strings when _clean_attribute_value is called.""" class DummyWSGIRequest: def __str__(self): return "" self.assertEqual( "", _clean_attribute_value(DummyWSGIRequest(), None), ) def test_deepcopy(self): bdict = BoundedAttributes(4, self.base, immutable=False) bdict.dropped = 10 bdict_copy = copy.deepcopy(bdict) for key in bdict_copy: self.assertEqual(bdict_copy[key], bdict[key]) self.assertEqual(bdict_copy.dropped, bdict.dropped) self.assertEqual(bdict_copy.maxlen, bdict.maxlen) self.assertEqual(bdict_copy.max_value_len, bdict.max_value_len) bdict_copy["name"] = "Bob" self.assertNotEqual(bdict_copy["name"], bdict["name"]) bdict["age"] = 99 self.assertNotEqual(bdict["age"], bdict_copy["age"]) def test_deepcopy_preserves_immutability(self): bdict = BoundedAttributes(maxlen=4, attributes=self.base, immutable=True) bdict_copy = copy.deepcopy(bdict) with self.assertRaises(TypeError): bdict_copy["invalid"] = "invalid" python-opentelemetry-1.45.2/opentelemetry-api/tests/baggage/000077500000000000000000000000001527070070700242055ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-api/tests/baggage/propagation/000077500000000000000000000000001527070070700265305ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-api/tests/baggage/propagation/test_propagation.py000066400000000000000000000013611527070070700324650ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # # type: ignore from unittest import TestCase from opentelemetry.baggage import get_baggage, set_baggage from opentelemetry.baggage.propagation import W3CBaggagePropagator class TestBaggageManager(TestCase): def test_propagate_baggage(self): carrier = {} propagator = W3CBaggagePropagator() ctx = set_baggage("Test1", "value1") ctx = set_baggage("test2", "value2", context=ctx) propagator.inject(carrier, ctx) ctx_propagated = propagator.extract(carrier) self.assertEqual(get_baggage("Test1", context=ctx_propagated), "value1") self.assertEqual(get_baggage("test2", context=ctx_propagated), "value2") python-opentelemetry-1.45.2/opentelemetry-api/tests/baggage/test_baggage.py000066400000000000000000000051341527070070700271760ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # type: ignore from unittest import TestCase from opentelemetry.baggage import ( _is_valid_value, clear, get_all, get_baggage, remove_baggage, set_baggage, ) from opentelemetry.context import attach, detach class TestBaggageManager(TestCase): def test_set_baggage(self): self.assertEqual({}, get_all()) ctx = set_baggage("test", "value") self.assertEqual(get_baggage("test", context=ctx), "value") ctx = set_baggage("test", "value2", context=ctx) self.assertEqual(get_baggage("test", context=ctx), "value2") def test_baggages_current_context(self): token = attach(set_baggage("test", "value")) self.assertEqual(get_baggage("test"), "value") detach(token) self.assertEqual(get_baggage("test"), None) def test_set_multiple_baggage_entries(self): ctx = set_baggage("test", "value") ctx = set_baggage("test2", "value2", context=ctx) self.assertEqual(get_baggage("test", context=ctx), "value") self.assertEqual(get_baggage("test2", context=ctx), "value2") self.assertEqual( get_all(context=ctx), {"test": "value", "test2": "value2"}, ) def test_modifying_baggage(self): ctx = set_baggage("test", "value") self.assertEqual(get_baggage("test", context=ctx), "value") baggage_entries = get_all(context=ctx) with self.assertRaises(TypeError): baggage_entries["test"] = "mess-this-up" self.assertEqual(get_baggage("test", context=ctx), "value") def test_remove_baggage_entry(self): self.assertEqual({}, get_all()) ctx = set_baggage("test", "value") ctx = set_baggage("test2", "value2", context=ctx) ctx = remove_baggage("test", context=ctx) self.assertEqual(get_baggage("test", context=ctx), None) self.assertEqual(get_baggage("test2", context=ctx), "value2") def test_clear_baggage(self): self.assertEqual({}, get_all()) ctx = set_baggage("test", "value") self.assertEqual(get_baggage("test", context=ctx), "value") ctx = clear(context=ctx) self.assertEqual(get_all(context=ctx), {}) def test__is_valid_value(self): for value, expected in ( ("GET%20%2Fapi%2F%2Freport", True), ("val1 ;prop=1", True), ("val1\t; p1 = 1 ; p2", True), ("val 1;prop", False), ): with self.subTest(value=value): self.assertEqual(_is_valid_value(value), expected) python-opentelemetry-1.45.2/opentelemetry-api/tests/context/000077500000000000000000000000001527070070700243145ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-api/tests/context/__init__.py000066400000000000000000000000001527070070700264130ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-api/tests/context/base_context.py000066400000000000000000000046021527070070700273460ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import unittest from logging import ERROR from opentelemetry import context def do_work() -> None: context.attach(context.set_value("say", "bar")) class ContextTestCases: class BaseTest(unittest.TestCase): def setUp(self) -> None: self.previous_context = context.get_current() def tearDown(self) -> None: context.attach(self.previous_context) def test_context(self): self.assertIsNone(context.get_value("say")) empty = context.get_current() second = context.set_value("say", "foo") self.assertEqual(context.get_value("say", context=second), "foo") do_work() self.assertEqual(context.get_value("say"), "bar") third = context.get_current() self.assertIsNone(context.get_value("say", context=empty)) self.assertEqual(context.get_value("say", context=second), "foo") self.assertEqual(context.get_value("say", context=third), "bar") def test_set_value(self): first = context.set_value("a", "yyy") second = context.set_value("a", "zzz") third = context.set_value("a", "---", first) self.assertEqual("yyy", context.get_value("a", context=first)) self.assertEqual("zzz", context.get_value("a", context=second)) self.assertEqual("---", context.get_value("a", context=third)) self.assertEqual(None, context.get_value("a")) def test_attach(self): context.attach(context.set_value("a", "yyy")) token = context.attach(context.set_value("a", "zzz")) self.assertEqual("zzz", context.get_value("a")) context.detach(token) self.assertEqual("yyy", context.get_value("a")) with self.assertLogs(level=ERROR): context.detach(token) def test_detach_out_of_order(self): t1 = context.attach(context.set_value("c", 1)) self.assertEqual(context.get_current(), {"c": 1}) t2 = context.attach(context.set_value("c", 2)) self.assertEqual(context.get_current(), {"c": 2}) context.detach(t1) self.assertEqual(context.get_current(), {}) context.detach(t2) self.assertEqual(context.get_current(), {"c": 1}) python-opentelemetry-1.45.2/opentelemetry-api/tests/context/propagation/000077500000000000000000000000001527070070700266375ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-api/tests/context/propagation/__init__.py000066400000000000000000000000001527070070700307360ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-api/tests/context/test_context.py000066400000000000000000000074111527070070700274140ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import unittest from unittest.mock import patch from opentelemetry import context from opentelemetry.context.context import Context from opentelemetry.context.contextvars_context import ContextVarsRuntimeContext from opentelemetry.environment_variables import OTEL_PYTHON_CONTEXT def _do_work() -> str: key = context.create_key("say") context.attach(context.set_value(key, "bar")) return key class TestContext(unittest.TestCase): def setUp(self): context.attach(Context()) def test_context_key(self): key1 = context.create_key("say") key2 = context.create_key("say") self.assertNotEqual(key1, key2) first = context.set_value(key1, "foo") second = context.set_value(key2, "bar") self.assertEqual(context.get_value(key1, context=first), "foo") self.assertEqual(context.get_value(key2, context=second), "bar") def test_context(self): key1 = context.create_key("say") self.assertIsNone(context.get_value(key1)) empty = context.get_current() second = context.set_value(key1, "foo") self.assertEqual(context.get_value(key1, context=second), "foo") key2 = _do_work() self.assertEqual(context.get_value(key2), "bar") third = context.get_current() self.assertIsNone(context.get_value(key1, context=empty)) self.assertEqual(context.get_value(key1, context=second), "foo") self.assertEqual(context.get_value(key2, context=third), "bar") def test_set_value(self): first = context.set_value("a", "yyy") second = context.set_value("a", "zzz") third = context.set_value("a", "---", first) self.assertEqual("yyy", context.get_value("a", context=first)) self.assertEqual("zzz", context.get_value("a", context=second)) self.assertEqual("---", context.get_value("a", context=third)) self.assertEqual(None, context.get_value("a")) def test_context_is_immutable(self): ctx = context.get_current() with self.assertRaises(ValueError): ctx["test"] = "cant-change-immutable" with self.assertRaises(ValueError): del ctx["test"] with self.assertRaises(ValueError): ctx.pop("test") with self.assertRaises(ValueError): ctx.popitem() with self.assertRaises(ValueError): ctx.clear() with self.assertRaises(ValueError): ctx.update({"test": "cant-change-immutable"}) with self.assertRaises(ValueError): ctx.setdefault("test", "cant-change-immutable") with self.assertRaises(ValueError): ctx |= {"test": "cant-change-immutable"} def test_set_current(self): context.attach(context.set_value("a", "yyy")) token = context.attach(context.set_value("a", "zzz")) self.assertEqual("zzz", context.get_value("a")) context.detach(token) self.assertEqual("yyy", context.get_value("a")) class TestInitContext(unittest.TestCase): def test_load_runtime_context_default(self): ctx = context._load_runtime_context() # pylint: disable=W0212 self.assertIsInstance(ctx, ContextVarsRuntimeContext) @patch.dict("os.environ", {OTEL_PYTHON_CONTEXT: "contextvars_context"}) def test_load_runtime_context(self): # type: ignore[misc] ctx = context._load_runtime_context() # pylint: disable=W0212 self.assertIsInstance(ctx, ContextVarsRuntimeContext) @patch.dict("os.environ", {OTEL_PYTHON_CONTEXT: "foo"}) def test_load_runtime_context_fallback(self): # type: ignore[misc] ctx = context._load_runtime_context() # pylint: disable=W0212 self.assertIsInstance(ctx, ContextVarsRuntimeContext) python-opentelemetry-1.45.2/opentelemetry-api/tests/context/test_contextvars_context.py000066400000000000000000000014211527070070700320470ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from unittest.mock import patch from opentelemetry import context from opentelemetry.context.contextvars_context import ContextVarsRuntimeContext # pylint: disable=import-error,no-name-in-module from tests.context.base_context import ContextTestCases class TestContextVarsContext(ContextTestCases.BaseTest): # pylint: disable=invalid-name def setUp(self) -> None: super().setUp() self.mock_runtime = patch.object( context, "_RUNTIME_CONTEXT", ContextVarsRuntimeContext(), ) self.mock_runtime.start() # pylint: disable=invalid-name def tearDown(self) -> None: super().tearDown() self.mock_runtime.stop() python-opentelemetry-1.45.2/opentelemetry-api/tests/distributedcontext/000077500000000000000000000000001527070070700265575ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-api/tests/distributedcontext/__init__.py000066400000000000000000000001141527070070700306640ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 python-opentelemetry-1.45.2/opentelemetry-api/tests/logs/000077500000000000000000000000001527070070700235745ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-api/tests/logs/test_log_record.py000066400000000000000000000012671527070070700273320ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import unittest from unittest.mock import patch from opentelemetry._logs import LogRecord OBSERVED_TIMESTAMP = "OBSERVED_TIMESTAMP" class TestLogRecord(unittest.TestCase): @patch("opentelemetry._logs._internal.time_ns") def test_log_record_observed_timestamp_default(self, time_ns_mock): # type: ignore time_ns_mock.return_value = OBSERVED_TIMESTAMP self.assertEqual(LogRecord().observed_timestamp, OBSERVED_TIMESTAMP) def test_log_record_exception(self): exc = ValueError("boom") log_record = LogRecord(exception=exc) self.assertIs(log_record.exception, exc) python-opentelemetry-1.45.2/opentelemetry-api/tests/logs/test_logger_provider.py000066400000000000000000000032361527070070700304020ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # type:ignore import unittest from unittest.mock import Mock, patch import opentelemetry._logs._internal as logs_internal from opentelemetry._logs import get_logger_provider, set_logger_provider from opentelemetry.environment_variables import _OTEL_PYTHON_LOGGER_PROVIDER from opentelemetry.test.globals_test import reset_logging_globals class TestGlobals(unittest.TestCase): def setUp(self): super().tearDown() reset_logging_globals() def tearDown(self): super().tearDown() reset_logging_globals() def test_set_logger_provider(self): lp_mock = Mock() # pylint: disable=protected-access self.assertIsNone(logs_internal._LOGGER_PROVIDER) set_logger_provider(lp_mock) self.assertIs(logs_internal._LOGGER_PROVIDER, lp_mock) self.assertIs(get_logger_provider(), lp_mock) def test_get_logger_provider(self): # pylint: disable=protected-access self.assertIsNone(logs_internal._LOGGER_PROVIDER) self.assertIsInstance(get_logger_provider(), logs_internal.ProxyLoggerProvider) logs_internal._LOGGER_PROVIDER = None with ( patch.dict( "os.environ", {_OTEL_PYTHON_LOGGER_PROVIDER: "test_logger_provider"}, ), patch("opentelemetry._logs._internal._load_provider", Mock()), ): with patch( "opentelemetry._logs._internal.cast", Mock(return_value="test_logger_provider"), ): self.assertEqual(get_logger_provider(), "test_logger_provider") python-opentelemetry-1.45.2/opentelemetry-api/tests/logs/test_proxy.py000066400000000000000000000064451527070070700263770ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # pylint: disable=W0212,W0222,W0221 import unittest from unittest.mock import Mock import opentelemetry._logs._internal as _logs_internal from opentelemetry import _logs from opentelemetry.test.globals_test import LoggingGlobalsTest from opentelemetry.util.types import Attributes class TestProvider(_logs.NoOpLoggerProvider): def get_logger( self, name: str, version: str | None = None, schema_url: str | None = None, attributes: Attributes = None, ) -> _logs.Logger: return LoggerTest(name) class LoggerTest(_logs.NoOpLogger): def emit( self, record: _logs.LogRecord | None = None, *, timestamp=None, observed_timestamp=None, context=None, severity_number=None, severity_text=None, body=None, attributes=None, event_name=None, exception: BaseException | None = None, ) -> None: pass class TestProxy(LoggingGlobalsTest, unittest.TestCase): def test_proxy_logger(self): provider = _logs.get_logger_provider() # proxy provider self.assertIsInstance(provider, _logs_internal.ProxyLoggerProvider) # provider returns proxy logger logger = provider.get_logger("proxy-test") self.assertIsInstance(logger, _logs_internal.ProxyLogger) # set a real provider _logs.set_logger_provider(TestProvider()) # get_logger_provider() now returns the real provider self.assertIsInstance(_logs.get_logger_provider(), TestProvider) # logger provider now returns real instance self.assertIsInstance(_logs.get_logger_provider().get_logger("fresh"), LoggerTest) # references to the old provider still work but return real logger now real_logger = provider.get_logger("proxy-test") self.assertIsInstance(real_logger, LoggerTest) def test_proxy_logger_forwards_record_with_exception(self): logger = _logs_internal.ProxyLogger("proxy-test") logger._real_logger = Mock(spec=LoggerTest("proxy-test")) record = _logs.LogRecord(exception=ValueError("boom")) self.assertIsNotNone(logger._real_logger) logger.emit(record) logger._real_logger.emit.assert_called_once_with(record) def test_proxy_logger_enabled_delegates_to_real_logger(self): logger = _logs_internal.ProxyLogger("proxy-test") real_logger = Mock(spec=LoggerTest("proxy-test")) real_logger.enabled.return_value = True logger._real_logger = real_logger result = logger.enabled(severity_number=_logs.SeverityNumber.INFO, event_name="test") self.assertTrue(result) real_logger.enabled.assert_called_once_with( context=None, severity_number=_logs.SeverityNumber.INFO, event_name="test", ) def test_proxy_logger_enabled_falls_back_to_noop(self): logger = _logs_internal.ProxyLogger("proxy-test") self.assertFalse(logger.enabled()) def test_noop_logger_enabled_returns_false(self): logger = _logs.NoOpLogger("noop-test") self.assertFalse(logger.enabled()) self.assertFalse(logger.enabled(severity_number=_logs.SeverityNumber.ERROR, event_name="e")) python-opentelemetry-1.45.2/opentelemetry-api/tests/metrics/000077500000000000000000000000001527070070700242765ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-api/tests/metrics/test_instruments.py000066400000000000000000000521611527070070700303070ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # type: ignore from inspect import Signature, isabstract, signature from unittest import TestCase from opentelemetry.metrics import ( Counter, Histogram, Instrument, Meter, NoOpCounter, NoOpHistogram, NoOpMeter, NoOpUpDownCounter, ObservableCounter, ObservableGauge, ObservableUpDownCounter, UpDownCounter, _Gauge, ) # FIXME Test that the instrument methods can be called concurrently safely. class ChildInstrument(Instrument): # pylint: disable=useless-parent-delegation def __init__(self, name, *args, unit="", description="", **kwargs): super().__init__(name, *args, unit=unit, description=description, **kwargs) class TestCounter(TestCase): def test_create_counter(self): """ Test that the Counter can be created with create_counter. """ self.assertTrue(isinstance(NoOpMeter("name").create_counter("name"), Counter)) def test_api_counter_abstract(self): """ Test that the API Counter is an abstract class. """ self.assertTrue(isabstract(Counter)) def test_create_counter_api(self): """ Test that the API for creating a counter accepts the name of the instrument. Test that the API for creating a counter accepts the unit of the instrument. Test that the API for creating a counter accepts the description of the """ create_counter_signature = signature(Meter.create_counter) self.assertIn("name", create_counter_signature.parameters.keys()) self.assertIs( create_counter_signature.parameters["name"].default, Signature.empty, ) create_counter_signature = signature(Meter.create_counter) self.assertIn("unit", create_counter_signature.parameters.keys()) self.assertIs(create_counter_signature.parameters["unit"].default, "") create_counter_signature = signature(Meter.create_counter) self.assertIn("description", create_counter_signature.parameters.keys()) self.assertIs(create_counter_signature.parameters["description"].default, "") def test_counter_add_method(self): """ Test that the counter has an add method. Test that the add method returns None. Test that the add method accepts optional attributes. Test that the add method accepts the increment amount. Test that the add method accepts only positive amounts. """ self.assertTrue(hasattr(Counter, "add")) self.assertIsNone(NoOpCounter("name").add(1)) add_signature = signature(Counter.add) self.assertIn("attributes", add_signature.parameters.keys()) self.assertIs(add_signature.parameters["attributes"].default, None) self.assertIn("amount", add_signature.parameters.keys()) self.assertIs(add_signature.parameters["amount"].default, Signature.empty) class TestObservableCounter(TestCase): def test_create_observable_counter(self): """ Test that the ObservableCounter can be created with create_observable_counter. """ def callback(): yield self.assertTrue( isinstance( NoOpMeter("name").create_observable_counter("name", callbacks=[callback()]), ObservableCounter, ) ) def test_api_observable_counter_abstract(self): """ Test that the API ObservableCounter is an abstract class. """ self.assertTrue(isabstract(ObservableCounter)) def test_create_observable_counter_api(self): """ Test that the API for creating a observable_counter accepts the name of the instrument. Test that the API for creating a observable_counter accepts a sequence of callbacks. Test that the API for creating a observable_counter accepts the unit of the instrument. Test that the API for creating a observable_counter accepts the description of the instrument """ create_observable_counter_signature = signature(Meter.create_observable_counter) self.assertIn("name", create_observable_counter_signature.parameters.keys()) self.assertIs( create_observable_counter_signature.parameters["name"].default, Signature.empty, ) create_observable_counter_signature = signature(Meter.create_observable_counter) self.assertIn("callbacks", create_observable_counter_signature.parameters.keys()) self.assertIs( create_observable_counter_signature.parameters["callbacks"].default, None, ) create_observable_counter_signature = signature(Meter.create_observable_counter) self.assertIn("unit", create_observable_counter_signature.parameters.keys()) self.assertIs(create_observable_counter_signature.parameters["unit"].default, "") create_observable_counter_signature = signature(Meter.create_observable_counter) self.assertIn( "description", create_observable_counter_signature.parameters.keys(), ) self.assertIs( create_observable_counter_signature.parameters["description"].default, "", ) def test_observable_counter_generator(self): """ Test that the API for creating a asynchronous counter accepts a generator. Test that the generator function reports iterable of measurements. Test that there is a way to pass state to the generator. Test that the instrument accepts positive measurements. Test that the instrument does not accept negative measurements. """ create_observable_counter_signature = signature(Meter.create_observable_counter) self.assertIn("callbacks", create_observable_counter_signature.parameters.keys()) self.assertIs( create_observable_counter_signature.parameters["name"].default, Signature.empty, ) class TestHistogram(TestCase): def test_create_histogram(self): """ Test that the Histogram can be created with create_histogram. """ self.assertTrue(isinstance(NoOpMeter("name").create_histogram("name"), Histogram)) def test_api_histogram_abstract(self): """ Test that the API Histogram is an abstract class. """ self.assertTrue(isabstract(Histogram)) def test_create_histogram_api(self): """ Test that the API for creating a histogram accepts the name of the instrument. Test that the API for creating a histogram accepts the unit of the instrument. Test that the API for creating a histogram accepts the description of the """ create_histogram_signature = signature(Meter.create_histogram) self.assertIn("name", create_histogram_signature.parameters.keys()) self.assertIs( create_histogram_signature.parameters["name"].default, Signature.empty, ) create_histogram_signature = signature(Meter.create_histogram) self.assertIn("unit", create_histogram_signature.parameters.keys()) self.assertIs(create_histogram_signature.parameters["unit"].default, "") create_histogram_signature = signature(Meter.create_histogram) self.assertIn("description", create_histogram_signature.parameters.keys()) self.assertIs(create_histogram_signature.parameters["description"].default, "") def test_histogram_record_method(self): """ Test that the histogram has an record method. Test that the record method returns None. Test that the record method accepts optional attributes. Test that the record method accepts the increment amount. Test that the record method returns None. """ self.assertTrue(hasattr(Histogram, "record")) self.assertIsNone(NoOpHistogram("name").record(1)) record_signature = signature(Histogram.record) self.assertIn("attributes", record_signature.parameters.keys()) self.assertIs(record_signature.parameters["attributes"].default, None) self.assertIn("amount", record_signature.parameters.keys()) self.assertIs(record_signature.parameters["amount"].default, Signature.empty) self.assertIsNone(NoOpHistogram("name").record(1)) class TestGauge(TestCase): def test_create_gauge(self): """ Test that the Gauge can be created with create_gauge. """ self.assertTrue(isinstance(NoOpMeter("name").create_gauge("name"), _Gauge)) def test_api_gauge_abstract(self): """ Test that the API Gauge is an abstract class. """ self.assertTrue(isabstract(_Gauge)) def test_create_gauge_api(self): """ Test that the API for creating a gauge accepts the name of the instrument. Test that the API for creating a gauge accepts a sequence of callbacks. Test that the API for creating a gauge accepts the unit of the instrument. Test that the API for creating a gauge accepts the description of the instrument """ create_gauge_signature = signature(Meter.create_gauge) self.assertIn("name", create_gauge_signature.parameters.keys()) self.assertIs( create_gauge_signature.parameters["name"].default, Signature.empty, ) create_gauge_signature = signature(Meter.create_gauge) create_gauge_signature = signature(Meter.create_gauge) self.assertIn("unit", create_gauge_signature.parameters.keys()) self.assertIs(create_gauge_signature.parameters["unit"].default, "") create_gauge_signature = signature(Meter.create_gauge) self.assertIn("description", create_gauge_signature.parameters.keys()) self.assertIs( create_gauge_signature.parameters["description"].default, "", ) class TestObservableGauge(TestCase): def test_create_observable_gauge(self): """ Test that the ObservableGauge can be created with create_observable_gauge. """ def callback(): yield self.assertTrue( isinstance( NoOpMeter("name").create_observable_gauge("name", [callback()]), ObservableGauge, ) ) def test_api_observable_gauge_abstract(self): """ Test that the API ObservableGauge is an abstract class. """ self.assertTrue(isabstract(ObservableGauge)) def test_create_observable_gauge_api(self): """ Test that the API for creating a observable_gauge accepts the name of the instrument. Test that the API for creating a observable_gauge accepts a sequence of callbacks. Test that the API for creating a observable_gauge accepts the unit of the instrument. Test that the API for creating a observable_gauge accepts the description of the instrument """ create_observable_gauge_signature = signature(Meter.create_observable_gauge) self.assertIn("name", create_observable_gauge_signature.parameters.keys()) self.assertIs( create_observable_gauge_signature.parameters["name"].default, Signature.empty, ) create_observable_gauge_signature = signature(Meter.create_observable_gauge) self.assertIn("callbacks", create_observable_gauge_signature.parameters.keys()) self.assertIs( create_observable_gauge_signature.parameters["callbacks"].default, None, ) create_observable_gauge_signature = signature(Meter.create_observable_gauge) self.assertIn("unit", create_observable_gauge_signature.parameters.keys()) self.assertIs(create_observable_gauge_signature.parameters["unit"].default, "") create_observable_gauge_signature = signature(Meter.create_observable_gauge) self.assertIn("description", create_observable_gauge_signature.parameters.keys()) self.assertIs( create_observable_gauge_signature.parameters["description"].default, "", ) def test_observable_gauge_callback(self): """ Test that the API for creating a asynchronous gauge accepts a sequence of callbacks. Test that the callback function reports measurements. Test that there is a way to pass state to the callback. """ create_observable_gauge_signature = signature(Meter.create_observable_gauge) self.assertIn("callbacks", create_observable_gauge_signature.parameters.keys()) self.assertIs( create_observable_gauge_signature.parameters["name"].default, Signature.empty, ) class TestUpDownCounter(TestCase): def test_create_up_down_counter(self): """ Test that the UpDownCounter can be created with create_up_down_counter. """ self.assertTrue( isinstance( NoOpMeter("name").create_up_down_counter("name"), UpDownCounter, ) ) def test_api_up_down_counter_abstract(self): """ Test that the API UpDownCounter is an abstract class. """ self.assertTrue(isabstract(UpDownCounter)) def test_create_up_down_counter_api(self): """ Test that the API for creating a up_down_counter accepts the name of the instrument. Test that the API for creating a up_down_counter accepts the unit of the instrument. Test that the API for creating a up_down_counter accepts the description of the """ create_up_down_counter_signature = signature(Meter.create_up_down_counter) self.assertIn("name", create_up_down_counter_signature.parameters.keys()) self.assertIs( create_up_down_counter_signature.parameters["name"].default, Signature.empty, ) create_up_down_counter_signature = signature(Meter.create_up_down_counter) self.assertIn("unit", create_up_down_counter_signature.parameters.keys()) self.assertIs(create_up_down_counter_signature.parameters["unit"].default, "") create_up_down_counter_signature = signature(Meter.create_up_down_counter) self.assertIn("description", create_up_down_counter_signature.parameters.keys()) self.assertIs( create_up_down_counter_signature.parameters["description"].default, "", ) def test_up_down_counter_add_method(self): """ Test that the up_down_counter has an add method. Test that the add method returns None. Test that the add method accepts optional attributes. Test that the add method accepts the increment or decrement amount. Test that the add method accepts positive and negative amounts. """ self.assertTrue(hasattr(UpDownCounter, "add")) self.assertIsNone(NoOpUpDownCounter("name").add(1)) add_signature = signature(UpDownCounter.add) self.assertIn("attributes", add_signature.parameters.keys()) self.assertIs(add_signature.parameters["attributes"].default, None) self.assertIn("amount", add_signature.parameters.keys()) self.assertIs(add_signature.parameters["amount"].default, Signature.empty) class TestObservableUpDownCounter(TestCase): # pylint: disable=protected-access def test_create_observable_up_down_counter(self): """ Test that the ObservableUpDownCounter can be created with create_observable_up_down_counter. """ def callback(): yield self.assertTrue( isinstance( NoOpMeter("name").create_observable_up_down_counter("name", [callback()]), ObservableUpDownCounter, ) ) def test_api_observable_up_down_counter_abstract(self): """ Test that the API ObservableUpDownCounter is an abstract class. """ self.assertTrue(isabstract(ObservableUpDownCounter)) def test_create_observable_up_down_counter_api(self): """ Test that the API for creating a observable_up_down_counter accepts the name of the instrument. Test that the API for creating a observable_up_down_counter accepts a sequence of callbacks. Test that the API for creating a observable_up_down_counter accepts the unit of the instrument. Test that the API for creating a observable_up_down_counter accepts the description of the instrument """ create_observable_up_down_counter_signature = signature(Meter.create_observable_up_down_counter) self.assertIn( "name", create_observable_up_down_counter_signature.parameters.keys(), ) self.assertIs( create_observable_up_down_counter_signature.parameters["name"].default, Signature.empty, ) create_observable_up_down_counter_signature = signature(Meter.create_observable_up_down_counter) self.assertIn( "callbacks", create_observable_up_down_counter_signature.parameters.keys(), ) self.assertIs( create_observable_up_down_counter_signature.parameters["callbacks"].default, None, ) create_observable_up_down_counter_signature = signature(Meter.create_observable_up_down_counter) self.assertIn( "unit", create_observable_up_down_counter_signature.parameters.keys(), ) self.assertIs( create_observable_up_down_counter_signature.parameters["unit"].default, "", ) create_observable_up_down_counter_signature = signature(Meter.create_observable_up_down_counter) self.assertIn( "description", create_observable_up_down_counter_signature.parameters.keys(), ) self.assertIs( create_observable_up_down_counter_signature.parameters["description"].default, "", ) def test_observable_up_down_counter_callback(self): """ Test that the API for creating a asynchronous up_down_counter accepts a sequence of callbacks. Test that the callback function reports measurements. Test that there is a way to pass state to the callback. Test that the instrument accepts positive and negative values. """ create_observable_up_down_counter_signature = signature(Meter.create_observable_up_down_counter) self.assertIn( "callbacks", create_observable_up_down_counter_signature.parameters.keys(), ) self.assertIs( create_observable_up_down_counter_signature.parameters["name"].default, Signature.empty, ) def test_name_check(self): instrument = ChildInstrument("name") self.assertEqual( instrument._check_name_unit_description("a" * 255, "unit", "description")["name"], "a" * 255, ) self.assertEqual( instrument._check_name_unit_description("a.", "unit", "description")["name"], "a.", ) self.assertEqual( instrument._check_name_unit_description("a-", "unit", "description")["name"], "a-", ) self.assertEqual( instrument._check_name_unit_description("a_", "unit", "description")["name"], "a_", ) self.assertEqual( instrument._check_name_unit_description("a/", "unit", "description")["name"], "a/", ) # the old max length self.assertIsNotNone(instrument._check_name_unit_description("a" * 64, "unit", "description")["name"]) self.assertIsNone(instrument._check_name_unit_description("a" * 256, "unit", "description")["name"]) self.assertIsNone(instrument._check_name_unit_description("Ñ", "unit", "description")["name"]) self.assertIsNone(instrument._check_name_unit_description("_a", "unit", "description")["name"]) self.assertIsNone(instrument._check_name_unit_description("1a", "unit", "description")["name"]) self.assertIsNone(instrument._check_name_unit_description("", "unit", "description")["name"]) def test_unit_check(self): instrument = ChildInstrument("name") self.assertEqual( instrument._check_name_unit_description("name", "a" * 63, "description")["unit"], "a" * 63, ) self.assertEqual( instrument._check_name_unit_description("name", "{a}", "description")["unit"], "{a}", ) self.assertIsNone(instrument._check_name_unit_description("name", "a" * 64, "description")["unit"]) self.assertIsNone(instrument._check_name_unit_description("name", "Ñ", "description")["unit"]) self.assertEqual( instrument._check_name_unit_description("name", None, "description")["unit"], "", ) def test_description_check(self): instrument = ChildInstrument("name") self.assertEqual( instrument._check_name_unit_description("name", "unit", "description")["description"], "description", ) self.assertEqual( instrument._check_name_unit_description("name", "unit", None)["description"], "", ) python-opentelemetry-1.45.2/opentelemetry-api/tests/metrics/test_meter.py000066400000000000000000000133041527070070700270240ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # type: ignore from logging import WARNING from unittest import TestCase from unittest.mock import Mock from opentelemetry.metrics import Meter, NoOpMeter # FIXME Test that the meter methods can be called concurrently safely. class ChildMeter(Meter): # pylint: disable=signature-differs def create_counter(self, name, unit="", description=""): super().create_counter(name, unit=unit, description=description) def create_up_down_counter(self, name, unit="", description=""): super().create_up_down_counter(name, unit=unit, description=description) def create_observable_counter(self, name, callbacks, unit="", description=""): super().create_observable_counter( name, callbacks, unit=unit, description=description, ) def create_histogram( self, name, unit="", description="", *, explicit_bucket_boundaries_advisory=None, ): super().create_histogram( name, unit=unit, description=description, explicit_bucket_boundaries_advisory=explicit_bucket_boundaries_advisory, ) def create_gauge(self, name, unit="", description=""): super().create_gauge(name, unit=unit, description=description) def create_observable_gauge(self, name, callbacks, unit="", description=""): super().create_observable_gauge( name, callbacks, unit=unit, description=description, ) def create_observable_up_down_counter(self, name, callbacks, unit="", description=""): super().create_observable_up_down_counter( name, callbacks, unit=unit, description=description, ) class TestMeter(TestCase): # pylint: disable=no-member def test_repeated_instrument_names(self): try: test_meter = NoOpMeter("name") test_meter.create_counter("counter") test_meter.create_up_down_counter("up_down_counter") test_meter.create_observable_counter("observable_counter", Mock()) test_meter.create_histogram("histogram") test_meter.create_gauge("gauge") test_meter.create_observable_gauge("observable_gauge", Mock()) test_meter.create_observable_up_down_counter("observable_up_down_counter", Mock()) except Exception as error: # pylint: disable=broad-exception-caught self.fail(f"Unexpected exception raised {error}") for instrument_name in [ "counter", "up_down_counter", "histogram", "gauge", ]: with self.assertNoLogs("opentelemetry.metrics._internal", level="WARNING"): getattr(test_meter, f"create_{instrument_name}")(instrument_name) for instrument_name in [ "observable_counter", "observable_gauge", "observable_up_down_counter", ]: with self.assertNoLogs("opentelemetry.metrics._internal", level="WARNING"): getattr(test_meter, f"create_{instrument_name}")(instrument_name, Mock()) def test_repeated_instrument_names_with_different_advisory(self): try: test_meter = NoOpMeter("name") test_meter.create_histogram("histogram", explicit_bucket_boundaries_advisory=[1.0]) except Exception as error: # pylint: disable=broad-exception-caught self.fail(f"Unexpected exception raised {error}") for instrument_name in [ "histogram", ]: with self.assertLogs(level=WARNING): getattr(test_meter, f"create_{instrument_name}")( instrument_name, ) def test_create_counter(self): """ Test that the meter provides a function to create a new Counter """ self.assertTrue(hasattr(Meter, "create_counter")) self.assertTrue(Meter.create_counter.__isabstractmethod__) def test_create_up_down_counter(self): """ Test that the meter provides a function to create a new UpDownCounter """ self.assertTrue(hasattr(Meter, "create_up_down_counter")) self.assertTrue(Meter.create_up_down_counter.__isabstractmethod__) def test_create_observable_counter(self): """ Test that the meter provides a function to create a new ObservableCounter """ self.assertTrue(hasattr(Meter, "create_observable_counter")) self.assertTrue(Meter.create_observable_counter.__isabstractmethod__) def test_create_histogram(self): """ Test that the meter provides a function to create a new Histogram """ self.assertTrue(hasattr(Meter, "create_histogram")) self.assertTrue(Meter.create_histogram.__isabstractmethod__) def test_create_gauge(self): """ Test that the meter provides a function to create a new Gauge """ self.assertTrue(hasattr(Meter, "create_gauge")) def test_create_observable_gauge(self): """ Test that the meter provides a function to create a new ObservableGauge """ self.assertTrue(hasattr(Meter, "create_observable_gauge")) self.assertTrue(Meter.create_observable_gauge.__isabstractmethod__) def test_create_observable_up_down_counter(self): """ Test that the meter provides a function to create a new ObservableUpDownCounter """ self.assertTrue(hasattr(Meter, "create_observable_up_down_counter")) self.assertTrue(Meter.create_observable_up_down_counter.__isabstractmethod__) python-opentelemetry-1.45.2/opentelemetry-api/tests/metrics/test_meter_provider.py000066400000000000000000000300401527070070700307320ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # type: ignore # pylint: disable=protected-access from unittest import TestCase from unittest.mock import Mock, patch from pytest import fixture import opentelemetry.metrics._internal as metrics_internal from opentelemetry import metrics from opentelemetry.environment_variables import OTEL_PYTHON_METER_PROVIDER from opentelemetry.metrics import ( NoOpMeter, NoOpMeterProvider, get_meter_provider, set_meter_provider, ) from opentelemetry.metrics._internal import ( _ProxyMeter, _ProxyMeterProvider, get_meter, ) from opentelemetry.metrics._internal.instrument import ( _ProxyCounter, _ProxyGauge, _ProxyHistogram, _ProxyObservableCounter, _ProxyObservableGauge, _ProxyObservableUpDownCounter, _ProxyUpDownCounter, ) from opentelemetry.test.globals_test import ( MetricsGlobalsTest, reset_metrics_globals, ) # FIXME Test that the instrument methods can be called concurrently safely. @fixture def reset_meter_provider(): print(f"calling reset_metrics_globals() {reset_metrics_globals}") reset_metrics_globals() yield print("teardown - calling reset_metrics_globals()") reset_metrics_globals() # pylint: disable=redefined-outer-name def test_set_meter_provider(reset_meter_provider): """ Test that the API provides a way to set a global default MeterProvider """ mock = Mock() assert metrics_internal._METER_PROVIDER is None set_meter_provider(mock) assert metrics_internal._METER_PROVIDER is mock assert get_meter_provider() is mock def test_set_meter_provider_calls_proxy_provider(reset_meter_provider): with patch("opentelemetry.metrics._internal._PROXY_METER_PROVIDER") as mock_proxy_mp: assert metrics_internal._PROXY_METER_PROVIDER is mock_proxy_mp mock_real_mp = Mock() set_meter_provider(mock_real_mp) mock_proxy_mp.on_set_meter_provider.assert_called_once_with(mock_real_mp) def test_get_meter_provider(reset_meter_provider): """ Test that the API provides a way to get a global default MeterProvider """ assert metrics_internal._METER_PROVIDER is None assert isinstance(get_meter_provider(), _ProxyMeterProvider) metrics._METER_PROVIDER = None with ( patch.dict("os.environ", {OTEL_PYTHON_METER_PROVIDER: "test_meter_provider"}), patch("opentelemetry.metrics._internal._load_provider", Mock()), patch( "opentelemetry.metrics._internal.cast", Mock(return_value="test_meter_provider"), ), ): assert get_meter_provider() == "test_meter_provider" class TestGetMeter(TestCase): def test_get_meter_parameters(self): """ Test that get_meter accepts name, version and schema_url """ try: NoOpMeterProvider().get_meter("name", version="version", schema_url="schema_url") except Exception as error: # pylint: disable=broad-exception-caught self.fail(f"Unexpected exception raised: {error}") def test_invalid_name(self): """ Test that when an invalid name is specified a working meter implementation is returned as a fallback. Test that the fallback meter name property keeps its original invalid value. Test that a message is logged reporting the specified value for the fallback meter is invalid. """ meter = NoOpMeterProvider().get_meter("") self.assertTrue(isinstance(meter, NoOpMeter)) self.assertEqual(meter.name, "") meter = NoOpMeterProvider().get_meter(None) self.assertTrue(isinstance(meter, NoOpMeter)) self.assertEqual(meter.name, None) def test_get_meter_wrapper(self): """ `metrics._internal.get_meter` called with valid parameters and a NoOpMeterProvider should return a NoOpMeter with the same parameters. """ meter = get_meter( "name", version="version", meter_provider=NoOpMeterProvider(), schema_url="schema_url", attributes={"key": "value", "key2": 5, "key3": "value3"}, ) self.assertIsInstance(meter, NoOpMeter) self.assertEqual(meter.name, "name") self.assertEqual(meter.version, "version") self.assertEqual(meter.schema_url, "schema_url") class TestProxy(MetricsGlobalsTest, TestCase): def test_global_proxy_meter_provider(self): # Global get_meter_provider() should initially be a _ProxyMeterProvider # singleton proxy_meter_provider: _ProxyMeterProvider = get_meter_provider() self.assertIsInstance(proxy_meter_provider, _ProxyMeterProvider) self.assertIs(get_meter_provider(), proxy_meter_provider) def test_proxy_provider(self): proxy_meter_provider = _ProxyMeterProvider() # Should return a proxy meter when no real MeterProvider is set name = "foo" version = "1.2" schema_url = "schema_url" proxy_meter: _ProxyMeter = proxy_meter_provider.get_meter(name, version=version, schema_url=schema_url) self.assertIsInstance(proxy_meter, _ProxyMeter) # After setting a real meter provider on the proxy, it should notify # it's _ProxyMeters which should create their own real Meters mock_real_mp = Mock() proxy_meter_provider.on_set_meter_provider(mock_real_mp) mock_real_mp.get_meter.assert_called_once_with(name, version, schema_url) # After setting a real meter provider on the proxy, it should now return # new meters directly from the set real meter another_name = "bar" meter2 = proxy_meter_provider.get_meter(another_name) self.assertIsInstance(meter2, Mock) mock_real_mp.get_meter.assert_called_with(another_name, None, None) # pylint: disable=too-many-locals,too-many-statements def test_proxy_meter(self): meter_name = "foo" proxy_meter: _ProxyMeter = _ProxyMeterProvider().get_meter(meter_name) self.assertIsInstance(proxy_meter, _ProxyMeter) # Should be able to create proxy instruments name = "foo" unit = "s" description = "Foobar" callback = Mock() proxy_counter = proxy_meter.create_counter(name, unit=unit, description=description) proxy_updowncounter = proxy_meter.create_up_down_counter(name, unit=unit, description=description) proxy_histogram = proxy_meter.create_histogram(name, unit=unit, description=description) proxy_gauge = proxy_meter.create_gauge(name, unit=unit, description=description) proxy_observable_counter = proxy_meter.create_observable_counter( name, callbacks=[callback], unit=unit, description=description ) proxy_observable_updowncounter = proxy_meter.create_observable_up_down_counter( name, callbacks=[callback], unit=unit, description=description ) proxy_overvable_gauge = proxy_meter.create_observable_gauge( name, callbacks=[callback], unit=unit, description=description ) self.assertIsInstance(proxy_counter, _ProxyCounter) self.assertIsInstance(proxy_updowncounter, _ProxyUpDownCounter) self.assertIsInstance(proxy_histogram, _ProxyHistogram) self.assertIsInstance(proxy_gauge, _ProxyGauge) self.assertIsInstance(proxy_observable_counter, _ProxyObservableCounter) self.assertIsInstance(proxy_observable_updowncounter, _ProxyObservableUpDownCounter) self.assertIsInstance(proxy_overvable_gauge, _ProxyObservableGauge) # Synchronous proxy instruments should be usable amount = 12 attributes = {"foo": "bar"} proxy_counter.add(amount, attributes=attributes) proxy_updowncounter.add(amount, attributes=attributes) proxy_histogram.record(amount, attributes=attributes) proxy_gauge.set(amount, attributes=attributes) # Calling _ProxyMeterProvider.on_set_meter_provider() should cascade down # to the _ProxyInstruments which should create their own real instruments # from the real Meter to back their calls real_meter_provider = Mock() proxy_meter.on_set_meter_provider(real_meter_provider) real_meter_provider.get_meter.assert_called_once_with(meter_name, None, None) real_meter: Mock = real_meter_provider.get_meter() real_meter.create_counter.assert_called_once_with(name, unit, description) real_meter.create_up_down_counter.assert_called_once_with(name, unit, description) real_meter.create_histogram.assert_called_once_with( name, unit, description, explicit_bucket_boundaries_advisory=None ) real_meter.create_gauge.assert_called_once_with(name, unit, description) real_meter.create_observable_counter.assert_called_once_with(name, [callback], unit, description) real_meter.create_observable_up_down_counter.assert_called_once_with(name, [callback], unit, description) real_meter.create_observable_gauge.assert_called_once_with(name, [callback], unit, description) # The synchronous instrument measurement methods should call through to # the real instruments real_counter: Mock = real_meter.create_counter() real_updowncounter: Mock = real_meter.create_up_down_counter() real_histogram: Mock = real_meter.create_histogram() real_gauge: Mock = real_meter.create_gauge() real_counter.assert_not_called() real_updowncounter.assert_not_called() real_histogram.assert_not_called() real_gauge.assert_not_called() proxy_counter.add(amount, attributes=attributes) real_counter.add.assert_called_once_with(amount, attributes, None) proxy_updowncounter.add(amount, attributes=attributes) real_updowncounter.add.assert_called_once_with(amount, attributes, None) proxy_histogram.record(amount, attributes=attributes) real_histogram.record.assert_called_once_with(amount, attributes, None) proxy_gauge.set(amount, attributes=attributes) real_gauge.set.assert_called_once_with(amount, attributes, None) def test_proxy_meter_with_real_meter(self) -> None: # Creating new instruments on the _ProxyMeter with a real meter set # should create real instruments instead of proxies meter_name = "foo" proxy_meter: _ProxyMeter = _ProxyMeterProvider().get_meter(meter_name) self.assertIsInstance(proxy_meter, _ProxyMeter) real_meter_provider = Mock() proxy_meter.on_set_meter_provider(real_meter_provider) name = "foo" unit = "s" description = "Foobar" callback = Mock() counter = proxy_meter.create_counter(name, unit=unit, description=description) updowncounter = proxy_meter.create_up_down_counter(name, unit=unit, description=description) histogram = proxy_meter.create_histogram(name, unit=unit, description=description) gauge = proxy_meter.create_gauge(name, unit=unit, description=description) observable_counter = proxy_meter.create_observable_counter( name, callbacks=[callback], unit=unit, description=description ) observable_updowncounter = proxy_meter.create_observable_up_down_counter( name, callbacks=[callback], unit=unit, description=description ) observable_gauge = proxy_meter.create_observable_gauge( name, callbacks=[callback], unit=unit, description=description ) real_meter: Mock = real_meter_provider.get_meter() self.assertIs(counter, real_meter.create_counter()) self.assertIs(updowncounter, real_meter.create_up_down_counter()) self.assertIs(histogram, real_meter.create_histogram()) self.assertIs(gauge, real_meter.create_gauge()) self.assertIs(observable_counter, real_meter.create_observable_counter()) self.assertIs( observable_updowncounter, real_meter.create_observable_up_down_counter(), ) self.assertIs(observable_gauge, real_meter.create_observable_gauge()) python-opentelemetry-1.45.2/opentelemetry-api/tests/metrics/test_observation.py000066400000000000000000000017371527070070700302520ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from unittest import TestCase from opentelemetry.metrics import Observation class TestObservation(TestCase): def test_measurement_init(self): try: # int Observation(321, {"hello": "world"}) # float Observation(321.321, {"hello": "world"}) except Exception: # pylint: disable=broad-exception-caught self.fail("Unexpected exception raised when instantiating Observation") def test_measurement_equality(self): self.assertEqual( Observation(321, {"hello": "world"}), Observation(321, {"hello": "world"}), ) self.assertNotEqual( Observation(321, {"hello": "world"}), Observation(321.321, {"hello": "world"}), ) self.assertNotEqual( Observation(321, {"baz": "world"}), Observation(321, {"hello": "world"}), ) python-opentelemetry-1.45.2/opentelemetry-api/tests/metrics/test_subclass_instantiation.py000066400000000000000000000123571527070070700325020ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # type: ignore # NOTE: The tests in this file are intended to test the semver compatibility of the public API. # Any tests that fail here indicate that the public API has changed in a way that is not backwards compatible. # Either bump the major version of the API, or make the necessary changes to the API to remain semver compatible. # pylint: disable=useless-parent-delegation,arguments-differ from opentelemetry.metrics import ( Asynchronous, Counter, Histogram, Instrument, Meter, MeterProvider, ObservableCounter, ObservableGauge, ObservableUpDownCounter, Synchronous, UpDownCounter, _Gauge, ) class MeterProviderImplTest(MeterProvider): def get_meter( self, name: str, version: str | None = None, schema_url: str | None = None, ) -> Meter: return super().get_meter(name, version, schema_url) def test_meter_provider_subclass_instantiation(): meter_provider = MeterProviderImplTest() assert isinstance(meter_provider, MeterProvider) class MeterImplTest(Meter): def create_counter(self, name, description, **kwargs): pass def create_up_down_counter(self, name, description, **kwargs): pass def create_observable_counter(self, name, description, **kwargs): pass def create_histogram(self, name, description, **kwargs): pass def create_observable_gauge(self, name, description, **kwargs): pass def create_observable_up_down_counter(self, name, description, **kwargs): pass def test_meter_subclass_instantiation(): meter = MeterImplTest("subclass_test") assert isinstance(meter, Meter) class SynchronousImplTest(Synchronous): def __init__(self, name: str, unit: str = "", description: str = "") -> None: super().__init__(name, unit, description) def test_synchronous_subclass_instantiation(): synchronous = SynchronousImplTest("subclass_test") assert isinstance(synchronous, Synchronous) class AsynchronousImplTest(Asynchronous): def __init__(self, name: str, unit: str = "", description: str = "") -> None: super().__init__(name, unit, description) def test_asynchronous_subclass_instantiation(): asynchronous = AsynchronousImplTest("subclass_test") assert isinstance(asynchronous, Asynchronous) class CounterImplTest(Counter): def __init__(self, name: str, unit: str = "", description: str = "") -> None: super().__init__(name, unit, description) def add(self, amount: int, **kwargs): pass def test_counter_subclass_instantiation(): counter = CounterImplTest("subclass_test") assert isinstance(counter, Counter) class UpDownCounterImplTest(UpDownCounter): def __init__(self, name: str, unit: str = "", description: str = "") -> None: super().__init__(name, unit, description) def add(self, amount: int, **kwargs): pass def test_up_down_counter_subclass_instantiation(): up_down_counter = UpDownCounterImplTest("subclass_test") assert isinstance(up_down_counter, UpDownCounter) class ObservableCounterImplTest(ObservableCounter): def __init__(self, name: str, unit: str = "", description: str = "") -> None: super().__init__(name, unit, description) def test_observable_counter_subclass_instantiation(): observable_counter = ObservableCounterImplTest("subclass_test") assert isinstance(observable_counter, ObservableCounter) class HistogramImplTest(Histogram): def __init__(self, name: str, unit: str = "", description: str = "") -> None: super().__init__(name, unit, description) def record(self, amount: int, **kwargs): pass def test_histogram_subclass_instantiation(): histogram = HistogramImplTest("subclass_test") assert isinstance(histogram, Histogram) class GaugeImplTest(_Gauge): def __init__(self, name: str, unit: str = "", description: str = "") -> None: super().__init__(name, unit, description) def set(self, amount: int, **kwargs): pass def test_gauge_subclass_instantiation(): gauge = GaugeImplTest("subclass_test") assert isinstance(gauge, _Gauge) class InstrumentImplTest(Instrument): def __init__(self, name: str, unit: str = "", description: str = "") -> None: super().__init__(name, unit, description) def test_instrument_subclass_instantiation(): instrument = InstrumentImplTest("subclass_test") assert isinstance(instrument, Instrument) class ObservableGaugeImplTest(ObservableGauge): def __init__(self, name: str, unit: str = "", description: str = "") -> None: super().__init__(name, unit, description) def test_observable_gauge_subclass_instantiation(): observable_gauge = ObservableGaugeImplTest("subclass_test") assert isinstance(observable_gauge, ObservableGauge) class ObservableUpDownCounterImplTest(ObservableUpDownCounter): def __init__(self, name: str, unit: str = "", description: str = "") -> None: super().__init__(name, unit, description) def test_observable_up_down_counter_subclass_instantiation(): observable_up_down_counter = ObservableUpDownCounterImplTest("subclass_test") assert isinstance(observable_up_down_counter, ObservableUpDownCounter) python-opentelemetry-1.45.2/opentelemetry-api/tests/mypysmoke.py000066400000000000000000000003501527070070700252350ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import opentelemetry.trace def dummy_check_mypy_returntype() -> opentelemetry.trace.TracerProvider: return opentelemetry.trace.get_tracer_provider() python-opentelemetry-1.45.2/opentelemetry-api/tests/propagators/000077500000000000000000000000001527070070700251715ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-api/tests/propagators/test__envcarrier.py000066400000000000000000000567111527070070700311130ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # type: ignore import os import unittest from unittest.mock import Mock, patch from opentelemetry import trace from opentelemetry.baggage import get_all, set_baggage from opentelemetry.baggage.propagation import W3CBaggagePropagator from opentelemetry.context import Context, get_current from opentelemetry.propagators._envcarrier import ( EnvironmentGetter, EnvironmentSetter, _is_normalized_key, _normalize_key, ) from opentelemetry.trace.propagation.tracecontext import ( TraceContextTextMapPropagator, ) class TestNormalizeKey(unittest.TestCase): """Unit tests for _normalize_key.""" def test_lowercase(self): self.assertEqual(_normalize_key("traceparent"), "TRACEPARENT") def test_hyphen_replaced(self): self.assertEqual(_normalize_key("trace-parent"), "TRACE_PARENT") def test_non_ascii_replaced(self): self.assertEqual(_normalize_key("héllo"), "H_LLO") def test_leading_digit_prefixed(self): self.assertEqual(_normalize_key("1abc"), "_1ABC") def test_already_valid(self): self.assertEqual(_normalize_key("ALREADY_VALID"), "ALREADY_VALID") def test_empty_string(self): self.assertEqual(_normalize_key(""), "_") class TestIsNormalizedKey(unittest.TestCase): """Unit tests for _is_normalized_key.""" def test_normalized_keys(self): self.assertTrue(_is_normalized_key("TRACEPARENT")) self.assertTrue(_is_normalized_key("X_B3_TRACEID")) self.assertTrue(_is_normalized_key("H_LLO")) self.assertTrue(_is_normalized_key("_1ABC")) def test_non_normalized_keys(self): self.assertFalse(_is_normalized_key("")) self.assertFalse(_is_normalized_key("traceparent")) self.assertFalse(_is_normalized_key("TraceParent")) self.assertFalse(_is_normalized_key("X-B3-TRACEID")) self.assertFalse(_is_normalized_key("1ABC")) self.assertFalse(_is_normalized_key("héllo")) class TestEnvironmentGetter(unittest.TestCase): """Unit tests for EnvironmentGetter.""" def test_get_existing_env_var(self): """Test retrieving an existing environment variable.""" getter = EnvironmentGetter() result = getter.get({"TEST_KEY": "test_value"}, "test_key") self.assertEqual(result, ["test_value"]) def test_get_normalizes_requested_key_case(self): """Test that requested key case is normalized.""" getter = EnvironmentGetter() carrier = {"TEST_KEY": "test_value"} self.assertEqual(getter.get(carrier, "test_key"), ["test_value"]) self.assertEqual(getter.get(carrier, "TEST_KEY"), ["test_value"]) self.assertEqual(getter.get(carrier, "Test_Key"), ["test_value"]) def test_get_nonexistent_env_var(self): """Test retrieving a non-existent environment variable.""" getter = EnvironmentGetter() result = getter.get({}, "nonexistent_key") self.assertIsNone(result) def test_get_empty_value(self): """Test retrieving an environment variable with empty value.""" getter = EnvironmentGetter() result = getter.get({"EMPTY_KEY": ""}, "empty_key") self.assertEqual(result, [""]) def test_get_empty_key_maps_to_underscore(self): """Test empty key lookup uses the normalized underscore name.""" getter = EnvironmentGetter() result = getter.get({"_": "underscore_value"}, "") self.assertEqual(result, ["underscore_value"]) def test_get_with_special_characters(self): """Test environment variables with special characters.""" getter = EnvironmentGetter() result = getter.get({"TEST_KEY": "value with spaces and !@#$%"}, "test_key") self.assertEqual(result, ["value with spaces and !@#$%"]) def test_get_ignores_non_normalized_env_var_name(self): """Test that non-normalized environment variable names are ignored.""" getter = EnvironmentGetter() carrier = {"X-B3-TRACEID": "ignored"} self.assertIsNone(getter.get(carrier, "x-b3-traceid")) self.assertIsNone(getter.get(carrier, "X_B3_TRACEID")) def test_get_prefers_normalized_env_var_name(self): """Test deterministic lookup when normalized names collide.""" getter = EnvironmentGetter() carrier = {"X_B3_TRACEID": "expected", "X-B3-TRACEID": "ignored"} self.assertEqual(getter.get(carrier, "x-b3-traceid"), ["expected"]) def test_keys(self): """Test getting all environment variable keys.""" test_env = {"KEY1": "value1", "KEY2": "value2", "KEY3": "value3"} getter = EnvironmentGetter() keys = getter.keys(test_env) expected_keys = {"KEY1", "KEY2", "KEY3"} self.assertEqual(set(keys), expected_keys) def test_keys_ignores_non_normalized_env_var_names(self): """Test that keys returns only already-normalized names.""" test_env = { "KEY1": "value1", "X-B3-TRACEID": "ignored", "1START": "ignored", "_1START": "value2", } getter = EnvironmentGetter() keys = getter.keys(test_env) self.assertEqual(set(keys), {"KEY1", "_1START"}) def test_keys_empty_environment(self): """Test getting keys when environment is empty.""" getter = EnvironmentGetter() keys = getter.keys({}) self.assertEqual(keys, []) def test_uses_carrier_parameter(self): """Test that getter reads from the supplied carrier.""" getter = EnvironmentGetter() result = getter.get({"TEST_KEY": "test_value"}, "test_key") self.assertEqual(result, ["test_value"]) self.assertIsNone(getter.get({}, "test_key")) def test_reads_current_carrier_without_snapshot(self): """Test that getter sees current carrier values.""" getter = EnvironmentGetter() carrier = {} self.assertIsNone(getter.get(carrier, "test_key")) carrier["TEST_KEY"] = "new_value" self.assertEqual(getter.get(carrier, "test_key"), ["new_value"]) def test_extracts_from_os_environ_when_provided_as_carrier(self): """Test using os.environ directly as the carrier.""" with patch.dict(os.environ, {}, clear=True): getter = EnvironmentGetter() self.assertIsNone(getter.get(os.environ, "test_key")) os.environ["TEST_KEY"] = "new_value" self.assertEqual(getter.get(os.environ, "test_key"), ["new_value"]) class TestEnvironmentSetter(unittest.TestCase): """Unit tests for EnvironmentSetter.""" def test_set_basic(self): """Test setting a value in carrier dictionary.""" setter = EnvironmentSetter() carrier = {} setter.set(carrier, "test_key", "test_value") self.assertEqual(carrier, {"TEST_KEY": "test_value"}) def test_set_multiple_values(self): """Test setting multiple values in the same carrier.""" setter = EnvironmentSetter() carrier = {} setter.set(carrier, "key1", "value1") setter.set(carrier, "key2", "value2") setter.set(carrier, "key3", "value3") expected = {"KEY1": "value1", "KEY2": "value2", "KEY3": "value3"} self.assertEqual(carrier, expected) def test_set_overwrites_existing(self): """Test that setting a key overwrites existing value.""" setter = EnvironmentSetter() carrier = {"TEST_KEY": "old_value"} setter.set(carrier, "test_key", "new_value") self.assertEqual(carrier, {"TEST_KEY": "new_value"}) def test_set_uppercases_keys(self): """Test that keys are normalized to uppercase.""" setter = EnvironmentSetter() carrier = {} setter.set(carrier, "lowercase_key", "value1") setter.set(carrier, "UPPERCASE_KEY", "value2") setter.set(carrier, "MiXeD_cAsE_kEy", "value3") expected = { "LOWERCASE_KEY": "value1", "UPPERCASE_KEY": "value2", "MIXED_CASE_KEY": "value3", } self.assertEqual(carrier, expected) def test_set_special_characters_in_value(self): """Test setting values with special characters.""" setter = EnvironmentSetter() carrier = {} setter.set(carrier, "test_key", "value with spaces and !@#$%^&*()") self.assertEqual(carrier, {"TEST_KEY": "value with spaces and !@#$%^&*()"}) def test_set_empty_value(self): """Test setting an empty value.""" setter = EnvironmentSetter() carrier = {} setter.set(carrier, "empty_key", "") self.assertEqual(carrier, {"EMPTY_KEY": ""}) def test_set_empty_key_maps_to_underscore(self): """Test setting an empty key uses the normalized underscore name.""" setter = EnvironmentSetter() carrier = {} setter.set(carrier, "", "value") self.assertEqual(carrier, {"_": "value"}) def test_does_not_modify_os_environ(self): """Test that setter does not modify os.environ.""" setter = EnvironmentSetter() carrier = {} original_environ = dict(os.environ) setter.set(carrier, "test_key", "test_value") self.assertEqual(dict(os.environ), original_environ) self.assertEqual(carrier, {"TEST_KEY": "test_value"}) class TestEnvironmentCarrierWithTraceContext(unittest.TestCase): """Integration tests with W3C TraceContext propagator.""" TRACE_ID = 0x4BF92F3577B34DA6A3CE929D0E0E4736 SPAN_ID = 0x00F067AA0BA902B7 def setUp(self): self.propagator = TraceContextTextMapPropagator() def _extract_with_env(self, env_vars): """Helper: Extract context from environment variables.""" with patch.dict(os.environ, env_vars, clear=True): getter = EnvironmentGetter() return self.propagator.extract(os.environ, getter=getter) def _inject_to_env(self, context): """Helper: Inject context into environment dict.""" setter = EnvironmentSetter() env_dict = {} self.propagator.inject(env_dict, context=context, setter=setter) return env_dict def test_extract_valid_traceparent(self): """Test extracting valid traceparent from environment.""" traceparent = f"00-{self.TRACE_ID:032x}-{self.SPAN_ID:016x}-01" ctx = self._extract_with_env({"TRACEPARENT": traceparent}) span_context = trace.get_current_span(ctx).get_span_context() self.assertEqual(span_context.trace_id, self.TRACE_ID) self.assertEqual(span_context.span_id, self.SPAN_ID) self.assertTrue(span_context.trace_flags.sampled) self.assertTrue(span_context.is_remote) def test_extract_traceparent_not_sampled(self): """Test extracting traceparent with sampled=false.""" traceparent = f"00-{self.TRACE_ID:032x}-{self.SPAN_ID:016x}-00" ctx = self._extract_with_env({"TRACEPARENT": traceparent}) span_context = trace.get_current_span(ctx).get_span_context() self.assertFalse(span_context.trace_flags.sampled) def test_extract_with_tracestate(self): """Test extracting traceparent with tracestate.""" traceparent = f"00-{self.TRACE_ID:032x}-{self.SPAN_ID:016x}-01" tracestate = "vendor1=value1,vendor2=value2" ctx = self._extract_with_env({"TRACEPARENT": traceparent, "TRACESTATE": tracestate}) span_context = trace.get_current_span(ctx).get_span_context() self.assertEqual(span_context.trace_state.get("vendor1"), "value1") self.assertEqual(span_context.trace_state.get("vendor2"), "value2") @unittest.skipIf( os.name == "nt", "Windows environment variable names are case-insensitive", ) def test_extract_ignores_lowercase_trace_context_names(self): """Test extraction ignores non-normalized trace context env names.""" traceparent = f"00-{self.TRACE_ID:032x}-{self.SPAN_ID:016x}-01" ctx = self._extract_with_env({"traceparent": traceparent, "tracestate": "vendor=value"}) span_context = trace.get_current_span(ctx).get_span_context() self.assertFalse(span_context.is_valid) def test_extract_invalid_traceparent(self): """Test that invalid traceparent formats are handled gracefully. Per W3C Trace Context spec, invalid traceparent should be ignored. """ invalid_traceparents = [ "invalid-format", "00-00000000000000000000000000000000-1234567890123456-00", # zero trace_id "00-12345678901234567890123456789012-0000000000000000-00", # zero span_id "00-12345678901234567890123456789012-1234567890123456-00-extra", # extra data ] for invalid_tp in invalid_traceparents: with self.subTest(traceparent=invalid_tp): ctx = self._extract_with_env({"TRACEPARENT": invalid_tp}) span = trace.get_current_span(ctx) self.assertEqual(span.get_span_context(), trace.INVALID_SPAN_CONTEXT) def test_extract_missing_traceparent(self): """Test extraction with missing TRACEPARENT.""" ctx = self._extract_with_env({}) span = trace.get_current_span(ctx) self.assertIsInstance(span.get_span_context(), trace.SpanContext) def test_extract_preserves_context_on_invalid_traceparent(self): """Test that invalid traceparent preserves original context.""" orig_ctx = Context({"my_key": "my_value"}) with patch.dict(os.environ, {"TRACEPARENT": "invalid"}, clear=True): getter = EnvironmentGetter() ctx = self.propagator.extract(os.environ, context=orig_ctx, getter=getter) self.assertDictEqual(ctx, orig_ctx) def test_inject_valid_span_context(self): """Test injecting valid span context to environment dict.""" span_context = trace.SpanContext( trace_id=self.TRACE_ID, span_id=self.SPAN_ID, is_remote=False, trace_flags=trace.TraceFlags(0x01), ) ctx = trace.set_span_in_context(trace.NonRecordingSpan(span_context)) env_dict = self._inject_to_env(ctx) expected_traceparent = f"00-{self.TRACE_ID:032x}-{self.SPAN_ID:016x}-01" self.assertEqual(env_dict["TRACEPARENT"], expected_traceparent) def test_inject_does_not_include_empty_tracestate(self): """Test that empty tracestate is not injected. Per W3C spec: vendors SHOULD avoid sending empty tracestate. """ span_context = trace.SpanContext( trace_id=self.TRACE_ID, span_id=self.SPAN_ID, is_remote=False, ) ctx = trace.set_span_in_context(trace.NonRecordingSpan(span_context)) env_dict = self._inject_to_env(ctx) self.assertIn("TRACEPARENT", env_dict) self.assertNotIn("TRACESTATE", env_dict) def test_inject_invalid_context(self): """Test that invalid context is not propagated.""" ctx = trace.set_span_in_context(trace.INVALID_SPAN) env_dict = self._inject_to_env(ctx) self.assertNotIn("TRACEPARENT", env_dict) def test_roundtrip_preserves_traceparent(self): """Test that traceparent survives extract->inject->extract cycle.""" original_traceparent = f"00-{self.TRACE_ID:032x}-{self.SPAN_ID:016x}-01" # Extract from environment ctx1 = self._extract_with_env({"TRACEPARENT": original_traceparent}) # Inject to new environment dict env_dict = self._inject_to_env(ctx1) self.assertEqual(env_dict["TRACEPARENT"], original_traceparent) # Extract again from injected dict ctx2 = self._extract_with_env(env_dict) # Verify both contexts have same span context span1 = trace.get_current_span(ctx1).get_span_context() span2 = trace.get_current_span(ctx2).get_span_context() self.assertEqual(span1.trace_id, span2.trace_id) self.assertEqual(span1.span_id, span2.span_id) self.assertEqual(span1.trace_flags, span2.trace_flags) def test_roundtrip_preserves_tracestate(self): """Test that tracestate survives roundtrip.""" env_vars = { "TRACEPARENT": f"00-{self.TRACE_ID:032x}-{self.SPAN_ID:016x}-01", "TRACESTATE": "vendor1=value1,vendor2=value2", } ctx1 = self._extract_with_env(env_vars) env_dict = self._inject_to_env(ctx1) self.assertIn("TRACESTATE", env_dict) self.assertIn("vendor1=value1", env_dict["TRACESTATE"]) self.assertIn("vendor2=value2", env_dict["TRACESTATE"]) def test_case_handling(self): """Test case handling for W3C headers.""" test_env = { "TRACEPARENT": f"00-{self.TRACE_ID:032x}-{self.SPAN_ID:016x}-01", "TRACESTATE": "vendor=value", } with patch.dict(os.environ, test_env, clear=True): getter = EnvironmentGetter() # Propagator uses lowercase keys self.assertIsNotNone(getter.get(os.environ, "traceparent")) self.assertIsNotNone(getter.get(os.environ, "tracestate")) @patch("opentelemetry.trace.INVALID_SPAN_CONTEXT") @patch("opentelemetry.trace.get_current_span") def test_fields(self, mock_get_current_span, mock_invalid_span_context): """Test that propagator.fields matches injected keys.""" # pylint: disable=import-outside-toplevel from opentelemetry.trace.span import TraceState # noqa: PLC0415 mock_get_current_span.configure_mock( return_value=Mock( **{ "get_span_context.return_value": Mock( trace_id=1, span_id=2, trace_flags=3, trace_state=TraceState([("a", "b")]), ) } ) ) mock_setter = Mock() self.propagator.inject({}, setter=mock_setter) inject_fields = set() for mock_call in mock_setter.mock_calls: inject_fields.add(mock_call[1][1]) self.assertEqual(inject_fields, self.propagator.fields) class TestEnvironmentCarrierWithBaggage(unittest.TestCase): """Integration tests with W3C Baggage propagator.""" def setUp(self): self.propagator = W3CBaggagePropagator() def _extract_with_env(self, env_vars): """Helper: Extract baggage from environment variables.""" with patch.dict(os.environ, env_vars, clear=True): getter = EnvironmentGetter() return self.propagator.extract(os.environ, getter=getter) def _inject_to_env(self, context): """Helper: Inject baggage into environment dict.""" setter = EnvironmentSetter() env_dict = {} self.propagator.inject(env_dict, context=context, setter=setter) return env_dict def test_extract_baggage(self): """Test extracting baggage from BAGGAGE environment variable.""" ctx = self._extract_with_env({"BAGGAGE": "key1=value1,key2=value2,key3=value3"}) baggage = get_all(ctx) self.assertEqual(baggage, {"key1": "value1", "key2": "value2", "key3": "value3"}) def test_extract_empty_baggage(self): """Test extracting empty baggage.""" ctx = self._extract_with_env({"BAGGAGE": ""}) baggage = get_all(ctx) self.assertEqual(baggage, {}) def test_extract_missing_baggage(self): """Test extraction with missing BAGGAGE.""" ctx = self._extract_with_env({}) baggage = get_all(ctx) self.assertEqual(baggage, {}) def test_inject_baggage(self): """Test injecting baggage into environment dict.""" ctx = get_current() ctx = set_baggage("deployment", "production", context=ctx) ctx = set_baggage("service", "api-gateway", context=ctx) env_dict = self._inject_to_env(ctx) self.assertIn("BAGGAGE", env_dict) baggage_value = env_dict["BAGGAGE"] self.assertIn("deployment=production", baggage_value) self.assertIn("service=api-gateway", baggage_value) def test_inject_no_baggage(self): """Test that empty baggage is not injected.""" ctx = get_current() env_dict = self._inject_to_env(ctx) self.assertNotIn("BAGGAGE", env_dict) def test_roundtrip_baggage(self): """Test baggage roundtrip.""" ctx1 = get_current() ctx1 = set_baggage("user_id", "12345", context=ctx1) ctx1 = set_baggage("session_id", "abc-def", context=ctx1) env_dict = self._inject_to_env(ctx1) ctx2 = self._extract_with_env(env_dict) baggage1 = get_all(ctx1) baggage2 = get_all(ctx2) self.assertEqual(baggage1, baggage2) @patch("opentelemetry.baggage.propagation.get_all") def test_fields(self, mock_get_all): """Test that propagator.fields matches injected keys.""" mock_get_all.return_value = {"key": "value"} mock_setter = Mock() self.propagator.inject({}, setter=mock_setter) inject_fields = set() for mock_call in mock_setter.mock_calls: inject_fields.add(mock_call[1][1]) self.assertEqual(inject_fields, self.propagator.fields) class TestEnvironmentCarrierWithCompositePropagator(unittest.TestCase): """Integration tests with multiple propagators.""" TRACE_ID = 0x4BF92F3577B34DA6A3CE929D0E0E4736 SPAN_ID = 0x00F067AA0BA902B7 def setUp(self): # pylint: disable=import-outside-toplevel from opentelemetry.propagators.composite import ( # noqa: PLC0415 CompositePropagator, ) self.propagator = CompositePropagator([TraceContextTextMapPropagator(), W3CBaggagePropagator()]) def test_extract_all_w3c_headers(self): """Test extracting both traceparent and baggage.""" env_vars = { "TRACEPARENT": f"00-{self.TRACE_ID:032x}-{self.SPAN_ID:016x}-01", "TRACESTATE": "vendor=value", "BAGGAGE": "user_id=12345,session_id=abc-def", } with patch.dict(os.environ, env_vars, clear=True): getter = EnvironmentGetter() ctx = self.propagator.extract(os.environ, getter=getter) # Verify traceparent was extracted span_context = trace.get_current_span(ctx).get_span_context() self.assertEqual(span_context.trace_id, self.TRACE_ID) self.assertEqual(span_context.span_id, self.SPAN_ID) # Verify baggage was extracted baggage = get_all(ctx) self.assertEqual(baggage["user_id"], "12345") self.assertEqual(baggage["session_id"], "abc-def") def test_inject_all_w3c_headers(self): """Test injecting both traceparent and baggage.""" span_context = trace.SpanContext( trace_id=self.TRACE_ID, span_id=self.SPAN_ID, is_remote=False, trace_flags=trace.TraceFlags(0x01), ) ctx = trace.set_span_in_context(trace.NonRecordingSpan(span_context)) ctx = set_baggage("deployment", "production", context=ctx) setter = EnvironmentSetter() env_dict = {} self.propagator.inject(env_dict, context=ctx, setter=setter) # Verify both were injected with uppercase keys self.assertIn("TRACEPARENT", env_dict) self.assertIn("BAGGAGE", env_dict) self.assertIn("deployment=production", env_dict["BAGGAGE"]) def test_empty_environment(self): """Test behavior with completely empty environment.""" with patch.dict(os.environ, {}, clear=True): getter = EnvironmentGetter() ctx = self.propagator.extract(os.environ, getter=getter) # Should not crash, return valid context self.assertIsInstance(ctx, Context) python-opentelemetry-1.45.2/opentelemetry-api/tests/propagators/test_composite.py000066400000000000000000000072231527070070700306100ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # type: ignore import unittest from unittest.mock import Mock from opentelemetry.propagators.composite import CompositePropagator def get_as_list(dict_object, key): value = dict_object.get(key) return [value] if value is not None else [] def mock_inject(name, value="data"): def wrapped(carrier=None, context=None, setter=None): carrier[name] = value setter.set({}, f"inject_field_{name}_0", None) setter.set({}, f"inject_field_{name}_1", None) return wrapped def mock_extract(name, value="context"): def wrapped(carrier=None, context=None, getter=None): new_context = context.copy() new_context[name] = value return new_context return wrapped def mock_fields(name): return {f"inject_field_{name}_0", f"inject_field_{name}_1"} class TestCompositePropagator(unittest.TestCase): @classmethod def setUpClass(cls): cls.mock_propagator_0 = Mock( inject=mock_inject("mock-0"), extract=mock_extract("mock-0"), fields=mock_fields("mock-0"), ) cls.mock_propagator_1 = Mock( inject=mock_inject("mock-1"), extract=mock_extract("mock-1"), fields=mock_fields("mock-1"), ) cls.mock_propagator_2 = Mock( inject=mock_inject("mock-0", value="data2"), extract=mock_extract("mock-0", value="context2"), fields=mock_fields("mock-0"), ) def test_no_propagators(self): propagator = CompositePropagator([]) new_carrier = {} propagator.inject(new_carrier) self.assertEqual(new_carrier, {}) context = propagator.extract(carrier=new_carrier, context={}, getter=get_as_list) self.assertEqual(context, {}) def test_single_propagator(self): propagator = CompositePropagator([self.mock_propagator_0]) new_carrier = {} propagator.inject(new_carrier) self.assertEqual(new_carrier, {"mock-0": "data"}) context = propagator.extract(carrier=new_carrier, context={}, getter=get_as_list) self.assertEqual(context, {"mock-0": "context"}) def test_multiple_propagators(self): propagator = CompositePropagator([self.mock_propagator_0, self.mock_propagator_1]) new_carrier = {} propagator.inject(new_carrier) self.assertEqual(new_carrier, {"mock-0": "data", "mock-1": "data"}) context = propagator.extract(carrier=new_carrier, context={}, getter=get_as_list) self.assertEqual(context, {"mock-0": "context", "mock-1": "context"}) def test_multiple_propagators_same_key(self): # test that when multiple propagators extract/inject the same # key, the later propagator values are extracted/injected propagator = CompositePropagator([self.mock_propagator_0, self.mock_propagator_2]) new_carrier = {} propagator.inject(new_carrier) self.assertEqual(new_carrier, {"mock-0": "data2"}) context = propagator.extract(carrier=new_carrier, context={}, getter=get_as_list) self.assertEqual(context, {"mock-0": "context2"}) def test_fields(self): propagator = CompositePropagator( [ self.mock_propagator_0, self.mock_propagator_1, self.mock_propagator_2, ] ) mock_setter = Mock() propagator.inject({}, setter=mock_setter) inject_fields = set() for mock_call in mock_setter.mock_calls: inject_fields.add(mock_call[1][1]) self.assertEqual(inject_fields, propagator.fields) python-opentelemetry-1.45.2/opentelemetry-api/tests/propagators/test_global_httptextformat.py000066400000000000000000000036421527070070700332240ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # type: ignore import unittest from opentelemetry import baggage, trace from opentelemetry.propagate import extract, inject from opentelemetry.trace import get_current_span, set_span_in_context from opentelemetry.trace.span import format_span_id, format_trace_id class TestDefaultGlobalPropagator(unittest.TestCase): """Test ensures the default global composite propagator works as intended""" TRACE_ID = int("12345678901234567890123456789012", 16) # type:int SPAN_ID = int("1234567890123456", 16) # type:int def test_propagation(self): traceparent_value = f"00-{format_trace_id(self.TRACE_ID)}-{format_span_id(self.SPAN_ID)}-00" tracestate_value = "foo=1,bar=2,baz=3" headers = { "baggage": ["key1=val1,key2=val2"], "traceparent": [traceparent_value], "tracestate": [tracestate_value], } ctx = extract(headers) baggage_entries = baggage.get_all(context=ctx) expected = {"key1": "val1", "key2": "val2"} self.assertEqual(baggage_entries, expected) span_context = get_current_span(context=ctx).get_span_context() self.assertEqual(span_context.trace_id, self.TRACE_ID) self.assertEqual(span_context.span_id, self.SPAN_ID) span = trace.NonRecordingSpan(span_context) ctx = baggage.set_baggage("key3", "val3") ctx = baggage.set_baggage("key4", "val4", context=ctx) ctx = set_span_in_context(span, context=ctx) output = {} inject(output, context=ctx) self.assertEqual(traceparent_value, output["traceparent"]) self.assertIn("key3=val3", output["baggage"]) self.assertIn("key4=val4", output["baggage"]) self.assertIn("foo=1", output["tracestate"]) self.assertIn("bar=2", output["tracestate"]) self.assertIn("baz=3", output["tracestate"]) python-opentelemetry-1.45.2/opentelemetry-api/tests/propagators/test_propagators.py000066400000000000000000000216531527070070700311520ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # type: ignore from importlib import reload from os import environ from unittest import TestCase from unittest.mock import Mock, patch from opentelemetry import trace from opentelemetry.baggage.propagation import W3CBaggagePropagator from opentelemetry.context.context import Context from opentelemetry.environment_variables import OTEL_PROPAGATORS from opentelemetry.trace.propagation.tracecontext import ( TraceContextTextMapPropagator, ) class TestPropagators(TestCase): @patch("opentelemetry.propagators.composite.CompositePropagator") def test_default_composite_propagators(self, mock_compositehttppropagator): def test_propagators(propagators): propagators = {propagator.__class__ for propagator in propagators} self.assertEqual(len(propagators), 2) self.assertEqual( propagators, {TraceContextTextMapPropagator, W3CBaggagePropagator}, ) mock_compositehttppropagator.configure_mock(side_effect=test_propagators) # pylint: disable=import-outside-toplevel import opentelemetry.propagate # noqa: PLC0415 reload(opentelemetry.propagate) @patch.dict(environ, {OTEL_PROPAGATORS: "None"}) @patch("opentelemetry.propagators.composite.CompositePropagator") def test_none_propagators(self, mock_compositehttppropagator): def test_propagators(propagators): propagators = {propagator.__class__ for propagator in propagators} self.assertEqual(len(propagators), 0) self.assertEqual( propagators, set(), ) mock_compositehttppropagator.configure_mock(side_effect=test_propagators) # pylint: disable=import-outside-toplevel import opentelemetry.propagate # noqa: PLC0415 reload(opentelemetry.propagate) @patch.dict(environ, {OTEL_PROPAGATORS: "tracecontext, None"}) @patch("opentelemetry.propagators.composite.CompositePropagator") def test_multiple_propagators_with_none(self, mock_compositehttppropagator): def test_propagators(propagators): propagators = {propagator.__class__ for propagator in propagators} self.assertEqual(len(propagators), 0) self.assertEqual( propagators, set(), ) mock_compositehttppropagator.configure_mock(side_effect=test_propagators) # pylint: disable=import-outside-toplevel import opentelemetry.propagate # noqa: PLC0415 reload(opentelemetry.propagate) @patch.dict(environ, {OTEL_PROPAGATORS: "a, b, c "}) @patch("opentelemetry.propagators.composite.CompositePropagator") @patch("opentelemetry.util._importlib_metadata.entry_points") def test_non_default_propagators(self, mock_entry_points, mock_compositehttppropagator): mock_entry_points.configure_mock( side_effect=[ [ Mock(**{"load.return_value": Mock(return_value="a")}), ], [Mock(**{"load.return_value": Mock(return_value="b")})], [Mock(**{"load.return_value": Mock(return_value="c")})], ] ) def test_propagators(propagators): self.assertEqual(propagators, ["a", "b", "c"]) mock_compositehttppropagator.configure_mock(side_effect=test_propagators) # pylint: disable=import-outside-toplevel import opentelemetry.propagate # noqa: PLC0415 reload(opentelemetry.propagate) @patch.dict(environ, {OTEL_PROPAGATORS: "tracecontext , unknown , baggage"}) def test_composite_propagators_error(self): with self.assertRaises(ValueError) as cm: # pylint: disable=import-outside-toplevel import opentelemetry.propagate # noqa: PLC0415 reload(opentelemetry.propagate) self.assertEqual( str(cm.exception), "Propagator unknown not found. It is either misspelled or not installed.", ) class TestTraceContextTextMapPropagator(TestCase): def setUp(self): self.propagator = TraceContextTextMapPropagator() def traceparent_helper( self, carrier, ): # We purposefully start with an empty context so we can test later if anything is added to it. initial_context = Context() context = self.propagator.extract(carrier, context=initial_context) self.assertIsNotNone(context) self.assertIsInstance(context, Context) return context def traceparent_helper_generator( self, version=0x00, trace_id=0x00000000000000000000000000000001, span_id=0x0000000000000001, trace_flags=0x00, suffix="", ): traceparent = f"{version:02x}-{trace_id:032x}-{span_id:016x}-{trace_flags:02x}{suffix}" carrier = {"traceparent": traceparent} return self.traceparent_helper(carrier) def valid_traceparent_helper( self, version=0x00, trace_id=0x00000000000000000000000000000001, span_id=0x0000000000000001, trace_flags=0x00, suffix="", assert_context_msg="A valid traceparent was provided, so the context should be non-empty.", ): context = self.traceparent_helper_generator( version=version, trace_id=trace_id, span_id=span_id, trace_flags=trace_flags, suffix=suffix, ) self.assertNotEqual( context, Context(), assert_context_msg, ) span = trace.get_current_span(context) self.assertIsNotNone(span) self.assertIsInstance(span, trace.span.Span) span_context = span.get_span_context() self.assertIsNotNone(span_context) self.assertIsInstance(span_context, trace.span.SpanContext) # Note: No version in SpanContext, it is only used locally in TraceContextTextMapPropagator self.assertEqual(span_context.trace_id, trace_id) self.assertEqual(span_context.span_id, span_id) self.assertEqual(span_context.trace_flags, trace_flags) self.assertIsInstance(span_context.trace_state, trace.TraceState) self.assertCountEqual(span_context.trace_state, []) self.assertEqual(span_context.is_remote, True) return context, span, span_context def invalid_traceparent_helper( self, version=0x00, trace_id=0x00000000000000000000000000000001, span_id=0x0000000000000001, trace_flags=0x00, suffix="", assert_context_msg="An invalid traceparent was provided, so the context should still be empty.", ): context = self.traceparent_helper_generator( version=version, trace_id=trace_id, span_id=span_id, trace_flags=trace_flags, suffix=suffix, ) self.assertEqual( context, Context(), assert_context_msg, ) return context def test_extract_nothing(self): context = self.traceparent_helper(carrier={}) self.assertEqual( context, {}, "We didn't provide a valid traceparent, so we should still have an empty Context.", ) def test_extract_simple_traceparent(self): self.valid_traceparent_helper() # https://www.w3.org/TR/trace-context/#version def test_extract_version_forbidden_ff(self): self.invalid_traceparent_helper( version=0xFF, assert_context_msg="We provided ann invalid traceparent with a forbidden version=0xff, so the context should still be empty.", ) # https://www.w3.org/TR/trace-context/#version-format def test_extract_version_00_with_unsupported_suffix(self): self.invalid_traceparent_helper( suffix="-f00", assert_context_msg="We provided an invalid traceparent with version=0x00 and suffix information which is not supported in this version, so the context should still be empty.", ) # https://www.w3.org/TR/trace-context/#versioning-of-traceparent # See the parsing of the sampled bit of flags. def test_extract_future_version_with_future_suffix_data(self): self.valid_traceparent_helper( version=0x99, suffix="-f00", assert_context_msg="We provided a traceparent that is possibly valid in the future with version=0x99 and suffix information, so the context be non-empty.", ) # https://www.w3.org/TR/trace-context/#trace-id def test_extract_trace_id_invalid_all_zeros(self): self.invalid_traceparent_helper(trace_id=0) # https://www.w3.org/TR/trace-context/#parent-id def test_extract_span_id_invalid_all_zeros(self): self.invalid_traceparent_helper(span_id=0) def test_extract_non_decimal_trace_flags(self): self.valid_traceparent_helper(trace_flags=0xA0) python-opentelemetry-1.45.2/opentelemetry-api/tests/propagators/test_w3cbaggagepropagator.py000066400000000000000000000331661527070070700327040ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # # type: ignore from logging import WARNING from unittest import TestCase from unittest.mock import Mock, patch from urllib.parse import quote_plus from opentelemetry.baggage import get_all, set_baggage from opentelemetry.baggage.propagation import W3CBaggagePropagator from opentelemetry.context import get_current class TestW3CBaggagePropagator(TestCase): # pylint: disable=protected-access # pylint: disable=too-many-public-methods def setUp(self): self.propagator = W3CBaggagePropagator() def _extract(self, header_value): """Test helper""" header = {"baggage": [header_value]} return get_all(self.propagator.extract(header)) def _inject(self, values): """Test helper""" ctx = get_current() for k, v in values.items(): # pylint: disable=invalid-name ctx = set_baggage(k, v, context=ctx) output = {} self.propagator.inject(output, context=ctx) return output.get("baggage") def test_no_context_header(self): baggage_entries = get_all(self.propagator.extract({})) self.assertEqual(baggage_entries, {}) def test_empty_context_header(self): header = "" self.assertEqual(self._extract(header), {}) def test_valid_header(self): header = "key1=val1,key2=val2" expected = {"key1": "val1", "key2": "val2"} self.assertEqual(self._extract(header), expected) def test_valid_header_with_whitespace(self): for header, expected in ( ( "key1 = val1, key2 =val2 ", {"key1": "val1", "key2": "val2"}, ), ("some_name = some_value", {"some_name": "some_value"}), ("key1\t=\tval1", {"key1": "val1"}), ("key1 = val1;prop=1", {"key1": "val1;prop=1"}), ( "key1 = val1 ; prop1 = 1 ; prop2 ;prop3= 2", {"key1": "val1 ; prop1 = 1 ; prop2 ;prop3= 2"}, ), ( "key1=val1, key2 = val2;prop = 1;prop2", {"key1": "val1", "key2": "val2;prop = 1;prop2"}, ), ): with self.subTest(header=header): self.assertEqual(self._extract(header), expected) def test_invalid_header_with_inner_whitespace(self): for header in ( "key 1=val1", "key1=val 1", "key1=val1;pr op=1", " = ", ): with self.subTest(header=header): self.assertEqual(self._extract(header), {}) def test_valid_header_with_properties(self): header = "key1=val1,key2=val2;prop=1;prop2;prop3=2" expected = {"key1": "val1", "key2": "val2;prop=1;prop2;prop3=2"} self.assertEqual(self._extract(header), expected) def test_valid_header_with_url_escaped_values(self): header = "key1=val1,key2=val2%3Aval3,key3=val4%40%23%24val5" expected = { "key1": "val1", "key2": "val2:val3", "key3": "val4@#$val5", } self.assertEqual(self._extract(header), expected) def test_header_with_invalid_value(self): header = "key1=val1,key2=val2,a,val3" with self.assertLogs(level=WARNING) as warning: self._extract(header) self.assertIn( "Baggage list-member `a` doesn't match the format", warning.output[0], ) def test_valid_header_with_empty_value(self): header = "key1=,key2=val2" expected = {"key1": "", "key2": "val2"} self.assertEqual(self._extract(header), expected) def test_invalid_header(self): self.assertEqual(self._extract("header1"), {}) self.assertEqual(self._extract(" = "), {}) def test_header_too_long(self): long_value = "s" * (W3CBaggagePropagator._MAX_HEADER_LENGTH + 1) header = f"key1={long_value}" expected = {} with self.assertLogs(level=WARNING) as warning: self.assertEqual(self._extract(header), expected) self.assertIn( "exceeded the maximum number of bytes per baggage-string", warning.output[0], ) def test_extract_non_ascii_header(self): """Extract should skip non-ASCII entries but keep valid ones.""" header = "key1=val1,key2=test\u00e9" with self.assertLogs(level=WARNING) as warning: result = self._extract(header) self.assertEqual(result, {"key1": "val1"}) self.assertIn( "contains non-ASCII characters", warning.output[0], ) def test_extract_non_ascii_header_exceeds_byte_limit(self): """A header under the char limit but over the byte limit when encoded should be rejected.""" # Each \u00e9 encodes to 2 bytes in UTF-8, so we need fewer chars to exceed the byte limit non_ascii_value = "\u00e9" * (W3CBaggagePropagator._MAX_HEADER_LENGTH) header = f"key1={non_ascii_value}" with self.assertLogs(level=WARNING) as warning: result = self._extract(header) self.assertEqual(result, {}) self.assertIn( "exceeded the maximum number of bytes per baggage-string", warning.output[0], ) def test_header_contains_too_many_entries(self): header = ",".join([f"key{k}=val" for k in range(W3CBaggagePropagator._MAX_PAIRS + 1)]) with self.assertLogs(level=WARNING): self.assertEqual(len(self._extract(header)), W3CBaggagePropagator._MAX_PAIRS) def test_header_contains_pair_too_long(self): long_value = "s" * (W3CBaggagePropagator._MAX_PAIR_LENGTH + 1) header = f"key1=value1,key2={long_value},key3=value3" expected = {"key1": "value1", "key3": "value3"} with self.assertLogs(level=WARNING) as warning: self.assertEqual(self._extract(header), expected) self.assertIn( "exceeded the maximum number of bytes per list-member", warning.output[0], ) def test_extract_unquote_plus(self): self.assertEqual(self._extract("keykey=value%5Evalue"), {"keykey": "value^value"}) self.assertEqual( self._extract("key%23key=value%23value"), {"key#key": "value#value"}, ) def test_header_max_entries_skip_invalid_entry(self): # 181 entries where index 2 is too long: skipping it leaves exactly 180 valid entries with self.assertLogs(level=WARNING) as warning: self.assertEqual( self._extract( ",".join( [ ( f"key{index}=value{index}" if index != 2 else (f"key{index}=value{'s' * (W3CBaggagePropagator._MAX_PAIR_LENGTH + 1)}") ) for index in range(W3CBaggagePropagator._MAX_PAIRS + 1) ] ) ), {f"key{index}": f"value{index}" for index in range(W3CBaggagePropagator._MAX_PAIRS + 1) if index != 2}, ) self.assertTrue( any("exceeded the maximum number of bytes per list-member" in msg for msg in warning.output) ) # 181 entries where index 2 is malformed (no '='): _apply_baggage_limits # accepts the first 180 entries (indices 0-179), then the malformed entry # at index 2 is skipped during parsing with self.assertLogs(level=WARNING) as warning: self.assertEqual( self._extract( ",".join( [ (f"key{index}=value{index}" if index != 2 else f"key{index}xvalue{index}") for index in range(W3CBaggagePropagator._MAX_PAIRS + 1) ] ) ), {f"key{index}": f"value{index}" for index in range(W3CBaggagePropagator._MAX_PAIRS) if index != 2}, ) self.assertTrue(any("exceeded the maximum number of list-members" in msg for msg in warning.output)) self.assertTrue(any("doesn't match the format" in msg for msg in warning.output)) def test_inject_no_baggage_entries(self): values = {} output = self._inject(values) self.assertEqual(None, output) def test_inject_space_entries(self): self.assertEqual("key=val+ue", self._inject({"key": "val ue"})) def test_inject(self): values = { "key1": "val1", "key2": "val2", } output = self._inject(values) self.assertIn("key1=val1", output) self.assertIn("key2=val2", output) def test_inject_escaped_values(self): values = { "key1": "val1,val2", "key2": "val3=4", } output = self._inject(values) self.assertIn("key2=val3%3D4", output) def test_inject_non_string_values(self): values = { "key1": True, "key2": 123, "key3": 123.567, } output = self._inject(values) self.assertIn("key1=True", output) self.assertIn("key2=123", output) self.assertIn("key3=123.567", output) @patch("opentelemetry.baggage.propagation.get_all") def test_fields(self, mock_baggage): mock_baggage.return_value = {"key": "value"} mock_setter = Mock() self.propagator.inject({}, setter=mock_setter) inject_fields = set() for mock_call in mock_setter.mock_calls: inject_fields.add(mock_call[1][1]) self.assertEqual(inject_fields, self.propagator.fields) def test_encode_baggage_pairs(self): def _format_baggage(entries): return ",".join(quote_plus(str(k)) + "=" + quote_plus(str(v)) for k, v in entries.items()) self.assertEqual(_format_baggage({"key key": "value value"}), "key+key=value+value") self.assertEqual( _format_baggage({"key/key": "value/value"}), "key%2Fkey=value%2Fvalue", ) def test_inject_too_many_entries(self): """Inject should drop entries exceeding _MAX_PAIRS.""" values = {f"key{i}": f"val{i}" for i in range(self.propagator._MAX_PAIRS + 10)} ctx = get_current() for key, val in values.items(): ctx = set_baggage(key, val, context=ctx) output = {} with self.assertLogs(level=WARNING) as warning: self.propagator.inject(output, context=ctx) self.assertIn( "exceeded the maximum number of list-members", warning.output[0], ) baggage_str = output.get("baggage", "") pairs = baggage_str.split(",") self.assertEqual(len(pairs), self.propagator._MAX_PAIRS) self.assertNotIn( f"key{self.propagator._MAX_PAIRS}=", baggage_str, ) def test_inject_entry_too_long(self): """Inject should skip individual entries that exceed _MAX_PAIR_LENGTH.""" long_value = "x" * self.propagator._MAX_PAIR_LENGTH values = {"key1": "val1", "big": long_value, "key3": "val3"} ctx = get_current() for key, val in values.items(): ctx = set_baggage(key, val, context=ctx) output = {} with self.assertLogs(level=WARNING) as warning: self.propagator.inject(output, context=ctx) self.assertIn( "exceeded the maximum number of bytes per list-member", warning.output[0], ) baggage_str = output.get("baggage", "") self.assertIn("key1=val1", baggage_str) self.assertIn("key3=val3", baggage_str) self.assertNotIn("big=", baggage_str) def test_inject_total_header_too_long(self): """Inject should stop adding entries when total header would exceed _MAX_HEADER_LENGTH.""" # Create entries that individually fit but collectively exceed the max header length # Each entry "kNNN=" with value ~200 chars; 50 of these > 8192 value = "v" * 200 values = {f"k{i:03d}": value for i in range(50)} ctx = get_current() for key, val in values.items(): ctx = set_baggage(key, val, context=ctx) output = {} with self.assertLogs(level=WARNING) as warning: self.propagator.inject(output, context=ctx) self.assertIn( "exceeded the maximum number of bytes per baggage-string", warning.output[0], ) baggage_str = output.get("baggage", "") self.assertLessEqual(len(baggage_str), self.propagator._MAX_HEADER_LENGTH) def test_inject_empty_after_all_dropped(self): """If all entries are too long, nothing should be injected.""" long_value = "x" * self.propagator._MAX_PAIR_LENGTH values = {"big1": long_value, "big2": long_value} ctx = get_current() for key, val in values.items(): ctx = set_baggage(key, val, context=ctx) output = {} self.propagator.inject(output, context=ctx) self.assertNotIn("baggage", output) @patch("opentelemetry.baggage._BAGGAGE_KEY", new="abc") def test_inject_extract(self): carrier = {} context = set_baggage("transaction", "string with spaces", context=get_current()) self.propagator.inject(carrier, context) context = self.propagator.extract(carrier) self.assertEqual(carrier, {"baggage": "transaction=string+with+spaces"}) self.assertEqual(context, {"abc": {"transaction": "string with spaces"}}) python-opentelemetry-1.45.2/opentelemetry-api/tests/test_implementation.py000066400000000000000000000102051527070070700272640ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import unittest from opentelemetry import trace class RecordingSpan(trace.Span): def __init__(self, context: trace.SpanContext) -> None: self._context = context def get_span_context(self) -> trace.SpanContext: return self._context def is_recording(self) -> bool: return True def end(self, end_time=None) -> None: pass def set_attributes(self, attributes) -> None: pass def set_attribute(self, key, value) -> None: pass def add_event( self, name: str, attributes=None, timestamp=None, ) -> None: pass def add_link( self, context, attributes=None, ) -> None: pass def update_name(self, name) -> None: pass def set_status( self, status, description=None, ) -> None: pass def record_exception( self, exception, attributes=None, timestamp=None, escaped=False, ) -> None: pass def __repr__(self) -> str: return f"RecordingSpan({self._context!r})" class TestAPIOnlyImplementation(unittest.TestCase): """ This test is in place to ensure the API is returning values that are valid. The same tests have been added to the SDK with different expected results. See issue for more details: https://github.com/open-telemetry/opentelemetry-python/issues/142 """ # TRACER def test_tracer(self): with self.assertRaises(TypeError): # pylint: disable=abstract-class-instantiated trace.TracerProvider() # type:ignore def test_default_tracer(self): tracer_provider = trace.NoOpTracerProvider() tracer = tracer_provider.get_tracer(__name__) with tracer.start_span("test") as span: self.assertFalse(span.get_span_context().is_valid) self.assertIs(span.is_recording(), False) with tracer.start_span("test2") as span2: self.assertFalse(span2.get_span_context().is_valid) self.assertIs(span2.is_recording(), False) def test_default_tracer_context_propagation_recording_span(self): tracer_provider = trace.NoOpTracerProvider() tracer = tracer_provider.get_tracer(__name__) span_context = trace.SpanContext( 2604504634922341076776623263868986797, 5213367945872657620, False, trace.TraceFlags(0x01), ) ctx = trace.set_span_in_context(RecordingSpan(context=span_context)) with tracer.start_span("test", context=ctx) as span: self.assertTrue(span.get_span_context().is_valid) self.assertEqual(span.get_span_context(), span_context) self.assertIs(span.is_recording(), False) def test_default_tracer_context_propagation_non_recording_span(self): tracer_provider = trace.NoOpTracerProvider() tracer = tracer_provider.get_tracer(__name__) ctx = trace.set_span_in_context(trace.INVALID_SPAN) with tracer.start_span("test", context=ctx) as span: self.assertFalse(span.get_span_context().is_valid) self.assertIs(span, trace.INVALID_SPAN) def test_default_tracer_context_propagation_with_invalid_context(self): tracer_provider = trace.NoOpTracerProvider() tracer = tracer_provider.get_tracer(__name__) ctx = trace.set_span_in_context( RecordingSpan(context="invalid_context") # type: ignore[reportArgumentType] ) with tracer.start_span("test", context=ctx) as span: self.assertFalse(span.get_span_context().is_valid) self.assertIs(span, trace.INVALID_SPAN) def test_span(self): with self.assertRaises(TypeError): # pylint: disable=abstract-class-instantiated trace.Span() # type:ignore def test_default_span(self): span = trace.NonRecordingSpan(trace.INVALID_SPAN_CONTEXT) self.assertEqual(span.get_span_context(), trace.INVALID_SPAN_CONTEXT) self.assertIs(span.is_recording(), False) python-opentelemetry-1.45.2/opentelemetry-api/tests/trace/000077500000000000000000000000001527070070700237265ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-api/tests/trace/__init__.py000066400000000000000000000001141527070070700260330ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 python-opentelemetry-1.45.2/opentelemetry-api/tests/trace/propagation/000077500000000000000000000000001527070070700262515ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-api/tests/trace/propagation/test_textmap.py000066400000000000000000000015511527070070700313460ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # type: ignore import unittest from opentelemetry.propagators.textmap import DefaultGetter class TestDefaultGetter(unittest.TestCase): def test_get_none(self): getter = DefaultGetter() carrier = {} val = getter.get(carrier, "test") self.assertIsNone(val) def test_get_str(self): getter = DefaultGetter() carrier = {"test": "val"} val = getter.get(carrier, "test") self.assertEqual(val, ["val"]) def test_get_iter(self): getter = DefaultGetter() carrier = {"test": ["val"]} val = getter.get(carrier, "test") self.assertEqual(val, ["val"]) def test_keys(self): getter = DefaultGetter() keys = getter.keys({"test": "val"}) self.assertEqual(keys, ["test"]) test_tracecontexthttptextformat.py000066400000000000000000000341331527070070700353300ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-api/tests/trace/propagation# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # type: ignore import unittest from unittest.mock import Mock, patch from opentelemetry import trace from opentelemetry.context import Context from opentelemetry.trace.propagation import tracecontext from opentelemetry.trace.span import TraceState FORMAT = tracecontext.TraceContextTextMapPropagator() # pylint: disable-next=too-many-public-methods class TestTraceContextFormat(unittest.TestCase): TRACE_ID = int("12345678901234567890123456789012", 16) # type:int SPAN_ID = int("1234567890123456", 16) # type:int def test_no_traceparent_header(self): """When tracecontext headers are not present, a new SpanContext should be created. RFC 4.2.2: If no traceparent header is received, the vendor creates a new trace-id and parent-id that represents the current request. """ output: dict[str, list[str]] = {} span = trace.get_current_span(FORMAT.extract(output)) self.assertIsInstance(span.get_span_context(), trace.SpanContext) def test_headers_with_tracestate(self): """When there is a traceparent and tracestate header, data from both should be added to the SpanContext. """ traceparent_value = f"00-{format(self.TRACE_ID, '032x')}-{format(self.SPAN_ID, '016x')}-00" tracestate_value = "foo=1,bar=2,baz=3" span_context = trace.get_current_span( FORMAT.extract( { "traceparent": [traceparent_value], "tracestate": [tracestate_value], }, ) ).get_span_context() self.assertEqual(span_context.trace_id, self.TRACE_ID) self.assertEqual(span_context.span_id, self.SPAN_ID) self.assertEqual(span_context.trace_state, {"foo": "1", "bar": "2", "baz": "3"}) self.assertTrue(span_context.is_remote) output: dict[str, str] = {} span = trace.NonRecordingSpan(span_context) ctx = trace.set_span_in_context(span) FORMAT.inject(output, context=ctx) self.assertEqual(output["traceparent"], traceparent_value) for pair in ["foo=1", "bar=2", "baz=3"]: self.assertIn(pair, output["tracestate"]) self.assertEqual(output["tracestate"].count(","), 2) def test_invalid_trace_id(self): """If the trace id is invalid, we must ignore the full traceparent header, and return a random, valid trace. Also ignore any tracestate. RFC 3.2.2.3 If the trace-id value is invalid (for example if it contains non-allowed characters or all zeros), vendors MUST ignore the traceparent. RFC 3.3 If the vendor failed to parse traceparent, it MUST NOT attempt to parse tracestate. Note that the opposite is not true: failure to parse tracestate MUST NOT affect the parsing of traceparent. """ span = trace.get_current_span( FORMAT.extract( { "traceparent": ["00-00000000000000000000000000000000-1234567890123456-00"], "tracestate": ["foo=1,bar=2,foo=3"], }, ) ) self.assertEqual(span.get_span_context(), trace.INVALID_SPAN_CONTEXT) def test_invalid_parent_id(self): """If the parent id is invalid, we must ignore the full traceparent header. Also ignore any tracestate. RFC 3.2.2.3 Vendors MUST ignore the traceparent when the parent-id is invalid (for example, if it contains non-lowercase hex characters). RFC 3.3 If the vendor failed to parse traceparent, it MUST NOT attempt to parse tracestate. Note that the opposite is not true: failure to parse tracestate MUST NOT affect the parsing of traceparent. """ span = trace.get_current_span( FORMAT.extract( { "traceparent": ["00-00000000000000000000000000000000-0000000000000000-00"], "tracestate": ["foo=1,bar=2,foo=3"], }, ) ) self.assertEqual(span.get_span_context(), trace.INVALID_SPAN_CONTEXT) def test_no_send_empty_tracestate(self): """If the tracestate is empty, do not set the header. RFC 3.3.1.1 Empty and whitespace-only list members are allowed. Vendors MUST accept empty tracestate headers but SHOULD avoid sending them. """ output: dict[str, str] = {} span = trace.NonRecordingSpan(trace.SpanContext(self.TRACE_ID, self.SPAN_ID, is_remote=False)) ctx = trace.set_span_in_context(span) FORMAT.inject(output, context=ctx) self.assertTrue("traceparent" in output) self.assertFalse("tracestate" in output) def test_format_not_supported(self): """If the traceparent does not adhere to the supported format, discard it and create a new tracecontext. RFC 4.3 If the version cannot be parsed, return an invalid trace header. """ span = trace.get_current_span( FORMAT.extract( { "traceparent": ["00-12345678901234567890123456789012-1234567890123456-00-residue"], "tracestate": ["foo=1,bar=2,foo=3"], }, ) ) self.assertEqual(span.get_span_context(), trace.INVALID_SPAN_CONTEXT) def test_propagate_invalid_context(self): """Do not propagate invalid trace context.""" output: dict[str, str] = {} ctx = trace.set_span_in_context(trace.INVALID_SPAN) FORMAT.inject(output, context=ctx) self.assertFalse("traceparent" in output) def test_tracestate_empty_header(self): """Test tracestate with an additional empty header (should be ignored)""" span = trace.get_current_span( FORMAT.extract( { "traceparent": ["00-12345678901234567890123456789012-1234567890123456-00"], "tracestate": ["foo=1", ""], }, ) ) self.assertEqual(span.get_span_context().trace_state["foo"], "1") def test_tracestate_header_with_trailing_comma(self): """Do not propagate invalid trace context.""" span = trace.get_current_span( FORMAT.extract( { "traceparent": ["00-12345678901234567890123456789012-1234567890123456-00"], "tracestate": ["foo=1,"], }, ) ) self.assertEqual(span.get_span_context().trace_state["foo"], "1") def test_tracestate_header_exceeds_maximum_length(self): """Discard tracestate exceeding the maximum combined length, but keep traceparent.""" cases = { "single header": ["foo=1" + " " * 8188], "combined headers": ["foo=1" + " " * 4091, "bar=2" + " " * 4091], } for name, tracestate in cases.items(): with self.subTest(name): with self.assertLogs(tracecontext.__name__, level="WARNING"): span = trace.get_current_span( FORMAT.extract( { "traceparent": ["00-12345678901234567890123456789012-1234567890123456-00"], "tracestate": tracestate, }, ) ) span_context = span.get_span_context() self.assertEqual(span_context.trace_id, self.TRACE_ID) self.assertEqual(span_context.span_id, self.SPAN_ID) self.assertEqual(len(span_context.trace_state), 0) def test_tracestate_header_at_maximum_length(self): """Accept tracestate with a combined length equal to the maximum.""" cases = { "single header": ["foo=1" + " " * 8187], "combined headers": ["foo=1" + " " * 4090, "bar=2" + " " * 4091], } for name, tracestate in cases.items(): with self.subTest(name): span = trace.get_current_span( FORMAT.extract( { "traceparent": ["00-12345678901234567890123456789012-1234567890123456-00"], "tracestate": tracestate, }, ) ) self.assertEqual(span.get_span_context().trace_state["foo"], "1") def test_traceparent_header_exceeds_maximum_length(self): """Discard a traceparent longer than the maximum length.""" prefix = "01-12345678901234567890123456789012-1234567890123456-00-" traceparent = prefix + "a" * (513 - len(prefix)) with self.assertLogs(tracecontext.__name__, level="WARNING"): span = trace.get_current_span(FORMAT.extract({"traceparent": [traceparent]})) self.assertEqual(span.get_span_context(), trace.INVALID_SPAN_CONTEXT) def test_traceparent_header_at_maximum_length(self): """Accept a future-version traceparent with a length equal to the maximum.""" prefix = "01-12345678901234567890123456789012-1234567890123456-00-" traceparent = prefix + "a" * (512 - len(prefix)) span = trace.get_current_span(FORMAT.extract({"traceparent": [traceparent]})) span_context = span.get_span_context() self.assertEqual(span_context.trace_id, self.TRACE_ID) self.assertEqual(span_context.span_id, self.SPAN_ID) def test_traceparent_whitespace_and_suffix(self): base = "-12345678901234567890123456789012-1234567890123456-01" cases = [ ("00" + base, True), ("00" + base + " \t ", True), ("00" + base + "-x", False), ("00" + base + "-" + " " * 100, False), ("01" + base + "-x", True), ("01" + base + "-" + " " * 100, True), ("01" + base + " -x", False), ("01" + base + "-" + " " * 100 + "\nx", False), ] for traceparent, valid in cases: with self.subTest(traceparent=traceparent): span = trace.get_current_span(FORMAT.extract({"traceparent": [traceparent]})) span_context = span.get_span_context() self.assertEqual(span_context.is_valid, valid) if valid: self.assertEqual(span_context.trace_id, self.TRACE_ID) self.assertEqual(span_context.span_id, self.SPAN_ID) self.assertEqual(span_context.trace_flags, trace.TraceFlags.SAMPLED) def test_tracestate_keys(self): """Test for valid key patterns in the tracestate""" tracestate_value = ",".join( [ "1a-2f@foo=bar1", "1a-_*/2b@foo=bar2", "foo=bar3", "foo-_*/bar=bar4", ] ) span = trace.get_current_span( FORMAT.extract( { "traceparent": ["00-12345678901234567890123456789012-1234567890123456-00"], "tracestate": [tracestate_value], }, ) ) self.assertEqual(span.get_span_context().trace_state["1a-2f@foo"], "bar1") self.assertEqual(span.get_span_context().trace_state["1a-_*/2b@foo"], "bar2") self.assertEqual(span.get_span_context().trace_state["foo"], "bar3") self.assertEqual(span.get_span_context().trace_state["foo-_*/bar"], "bar4") @patch("opentelemetry.trace.INVALID_SPAN_CONTEXT") @patch("opentelemetry.trace.get_current_span") def test_fields(self, mock_get_current_span, mock_invalid_span_context): mock_get_current_span.configure_mock( return_value=Mock( **{ "get_span_context.return_value": Mock( trace_id=1, span_id=2, trace_flags=3, trace_state=TraceState([("a", "b")]), ) } ) ) mock_setter = Mock() FORMAT.inject({}, setter=mock_setter) inject_fields = set() for mock_call in mock_setter.mock_calls: inject_fields.add(mock_call[1][1]) self.assertEqual(inject_fields, FORMAT.fields) def test_extract_no_trace_parent_to_explicit_ctx(self): carrier = {"tracestate": ["foo=1"]} orig_ctx = Context({"k1": "v1"}) ctx = FORMAT.extract(carrier, orig_ctx) self.assertDictEqual(orig_ctx, ctx) def test_extract_no_trace_parent_to_implicit_ctx(self): carrier = {"tracestate": ["foo=1"]} ctx = FORMAT.extract(carrier) self.assertDictEqual(Context(), ctx) def test_extract_invalid_trace_parent_to_explicit_ctx(self): trace_parent_headers = [ "invalid", "00-00000000000000000000000000000000-1234567890123456-00", "00-12345678901234567890123456789012-0000000000000000-00", "00-12345678901234567890123456789012-1234567890123456-00-residue", ] for trace_parent in trace_parent_headers: with self.subTest(trace_parent=trace_parent): carrier = { "traceparent": [trace_parent], "tracestate": ["foo=1"], } orig_ctx = Context({"k1": "v1"}) ctx = FORMAT.extract(carrier, orig_ctx) self.assertDictEqual(orig_ctx, ctx) def test_extract_invalid_trace_parent_to_implicit_ctx(self): trace_parent_headers = [ "invalid", "00-00000000000000000000000000000000-1234567890123456-00", "00-12345678901234567890123456789012-0000000000000000-00", "00-12345678901234567890123456789012-1234567890123456-00-residue", ] for trace_parent in trace_parent_headers: with self.subTest(trace_parent=trace_parent): carrier = { "traceparent": [trace_parent], "tracestate": ["foo=1"], } ctx = FORMAT.extract(carrier) self.assertDictEqual(Context(), ctx) python-opentelemetry-1.45.2/opentelemetry-api/tests/trace/test_defaultspan.py000066400000000000000000000013441527070070700276470ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import unittest from opentelemetry import trace class TestNonRecordingSpan(unittest.TestCase): def test_ctor(self): context = trace.SpanContext( 1, 1, is_remote=False, trace_flags=trace.DEFAULT_TRACE_OPTIONS, trace_state=trace.DEFAULT_TRACE_STATE, ) span = trace.NonRecordingSpan(context) self.assertEqual(context, span.get_span_context()) def test_invalid_span(self): self.assertIsNotNone(trace.INVALID_SPAN) self.assertIsNotNone(trace.INVALID_SPAN.get_span_context()) self.assertFalse(trace.INVALID_SPAN.get_span_context().is_valid) python-opentelemetry-1.45.2/opentelemetry-api/tests/trace/test_globals.py000066400000000000000000000140651527070070700267700ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import unittest from unittest.mock import Mock, patch from opentelemetry import context, trace from opentelemetry.test.concurrency_test import ConcurrencyTestBase, MockFunc from opentelemetry.test.globals_test import TraceGlobalsTest from opentelemetry.trace.status import Status, StatusCode class SpanTest(trace.NonRecordingSpan): has_ended = False recorded_exception = None recorded_status = Status(status_code=StatusCode.UNSET) def set_status(self, status, description=None): if isinstance(status, Status): self.recorded_status = status else: self.recorded_status = Status(status_code=status, description=description) def end(self, end_time=None): self.has_ended = True def is_recording(self): return not self.has_ended def record_exception(self, exception, attributes=None, timestamp=None, escaped=False): self.recorded_exception = exception class TestGlobals(TraceGlobalsTest, unittest.TestCase): @staticmethod @patch("opentelemetry.trace._TRACER_PROVIDER") def test_get_tracer(mock_tracer_provider): # type: ignore """trace.get_tracer should proxy to the global tracer provider.""" trace.get_tracer("foo", "var") mock_tracer_provider.get_tracer.assert_called_with("foo", "var", None, None) mock_provider = Mock() trace.get_tracer("foo", "var", mock_provider) mock_provider.get_tracer.assert_called_with("foo", "var", None, None) class TestGlobalsConcurrency(TraceGlobalsTest, ConcurrencyTestBase): @patch("opentelemetry.trace.logger") def test_set_tracer_provider_many_threads(self, mock_logger) -> None: # type: ignore mock_logger.warning = MockFunc() def do_concurrently() -> Mock: # first get a proxy tracer proxy_tracer = trace.ProxyTracerProvider().get_tracer("foo") # try to set the global tracer provider mock_tracer_provider = Mock(get_tracer=MockFunc()) trace.set_tracer_provider(mock_tracer_provider) # start a span through the proxy which will call through to the mock provider proxy_tracer.start_span("foo") return mock_tracer_provider num_threads = 100 mock_tracer_providers = self.run_with_many_threads( do_concurrently, num_threads=num_threads, ) # despite trying to set tracer provider many times, only one of the # mock_tracer_providers should have stuck and been called from # proxy_tracer.start_span() mock_tps_with_any_call = [mock for mock in mock_tracer_providers if mock.get_tracer.call_count > 0] self.assertEqual(len(mock_tps_with_any_call), 1) self.assertEqual(mock_tps_with_any_call[0].get_tracer.call_count, num_threads) # should have warned every time except for the successful set self.assertEqual(mock_logger.warning.call_count, num_threads - 1) class TestTracer(unittest.TestCase): def setUp(self): self.tracer = trace.NoOpTracer() def test_get_current_span(self): """NoOpTracer's start_span will also be retrievable via get_current_span """ self.assertEqual(trace.get_current_span(), trace.INVALID_SPAN) span = trace.NonRecordingSpan(trace.INVALID_SPAN_CONTEXT) ctx = trace.set_span_in_context(span) token = context.attach(ctx) try: self.assertIs(trace.get_current_span(), span) finally: context.detach(token) self.assertEqual(trace.get_current_span(), trace.INVALID_SPAN) class TestUseTracer(unittest.TestCase): def test_use_span(self): self.assertEqual(trace.get_current_span(), trace.INVALID_SPAN) span = trace.NonRecordingSpan(trace.INVALID_SPAN_CONTEXT) with trace.use_span(span): self.assertIs(trace.get_current_span(), span) self.assertEqual(trace.get_current_span(), trace.INVALID_SPAN) def test_use_span_end_on_exit(self): test_span = SpanTest(trace.INVALID_SPAN_CONTEXT) with trace.use_span(test_span): pass self.assertFalse(test_span.has_ended) with trace.use_span(test_span, end_on_exit=True): pass self.assertTrue(test_span.has_ended) def test_use_span_exception(self): class TestUseSpanException(Exception): pass test_span = SpanTest(trace.INVALID_SPAN_CONTEXT) exception = TestUseSpanException("test exception") with self.assertRaises(TestUseSpanException): with trace.use_span(test_span): raise exception self.assertEqual(test_span.recorded_exception, exception) def test_use_span_set_status(self): class TestUseSpanException(Exception): pass test_span = SpanTest(trace.INVALID_SPAN_CONTEXT) with self.assertRaises(TestUseSpanException): with trace.use_span(test_span): raise TestUseSpanException("test error") self.assertEqual( test_span.recorded_status.status_code, StatusCode.ERROR, ) self.assertEqual( test_span.recorded_status.description, "TestUseSpanException: test error", ) def test_use_span_base_exceptions(self): base_exception_classes = [ BaseException, GeneratorExit, SystemExit, KeyboardInterrupt, ] for exc_cls in base_exception_classes: with self.subTest(exc=exc_cls.__name__): test_span = SpanTest(trace.INVALID_SPAN_CONTEXT) with self.assertRaises(exc_cls): with trace.use_span(test_span): raise exc_cls() self.assertEqual( test_span.recorded_status.status_code, StatusCode.UNSET, ) self.assertIsNone(test_span.recorded_status.description) self.assertIsNone(test_span.recorded_exception) python-opentelemetry-1.45.2/opentelemetry-api/tests/trace/test_immutablespancontext.py000066400000000000000000000032071527070070700316070ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import unittest from opentelemetry import trace from opentelemetry.trace import TraceFlags, TraceState class TestImmutableSpanContext(unittest.TestCase): def test_ctor(self): context = trace.SpanContext( 1, 1, is_remote=False, trace_flags=trace.DEFAULT_TRACE_OPTIONS, trace_state=trace.DEFAULT_TRACE_STATE, ) self.assertEqual(context.trace_id, 1) self.assertEqual(context.span_id, 1) self.assertEqual(context.is_remote, False) self.assertEqual(context.trace_flags, trace.DEFAULT_TRACE_OPTIONS) self.assertEqual(context.trace_state, trace.DEFAULT_TRACE_STATE) def test_attempt_change_attributes(self): context = trace.SpanContext( 1, 2, is_remote=False, trace_flags=trace.DEFAULT_TRACE_OPTIONS, trace_state=trace.DEFAULT_TRACE_STATE, ) # attempt to change the attribute values context.trace_id = 2 # type: ignore context.span_id = 3 # type: ignore context.is_remote = True # type: ignore context.trace_flags = TraceFlags(3) # type: ignore context.trace_state = TraceState([("test", "test")]) # type: ignore # check if attributes changed self.assertEqual(context.trace_id, 1) self.assertEqual(context.span_id, 2) self.assertEqual(context.is_remote, False) self.assertEqual(context.trace_flags, trace.DEFAULT_TRACE_OPTIONS) self.assertEqual(context.trace_state, trace.DEFAULT_TRACE_STATE) python-opentelemetry-1.45.2/opentelemetry-api/tests/trace/test_proxy.py000066400000000000000000000061741527070070700265300ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # pylint: disable=W0212,W0222,W0221 import unittest from opentelemetry import trace from opentelemetry.test.globals_test import TraceGlobalsTest from opentelemetry.trace.span import ( INVALID_SPAN_CONTEXT, NonRecordingSpan, Span, ) from opentelemetry.util._decorator import _agnosticcontextmanager from opentelemetry.util.types import Attributes class TestProvider(trace.NoOpTracerProvider): def get_tracer( self, instrumenting_module_name: str, instrumenting_library_version: str | None = None, schema_url: str | None = None, attributes: Attributes = None, ) -> trace.Tracer: return TestTracer() class TestTracer(trace.NoOpTracer): def start_span(self, *args, **kwargs): return SpanTest(INVALID_SPAN_CONTEXT) @_agnosticcontextmanager # pylint: disable=protected-access def start_as_current_span(self, *args, **kwargs): # type: ignore with trace.use_span(self.start_span(*args, **kwargs)) as span: # type: ignore yield span class SpanTest(NonRecordingSpan): pass class TestProxy(TraceGlobalsTest, unittest.TestCase): def test_proxy_tracer(self): provider = trace.get_tracer_provider() # proxy provider self.assertIsInstance(provider, trace.ProxyTracerProvider) # provider returns proxy tracer tracer = provider.get_tracer("proxy-test") self.assertIsInstance(tracer, trace.ProxyTracer) with tracer.start_span("span1") as span: self.assertIsInstance(span, trace.NonRecordingSpan) with tracer.start_as_current_span("span2") as span: self.assertIsInstance(span, trace.NonRecordingSpan) # set a real provider trace.set_tracer_provider(TestProvider()) # get_tracer_provider() now returns the real provider self.assertIsInstance(trace.get_tracer_provider(), TestProvider) # tracer provider now returns real instance self.assertIsInstance(trace.get_tracer_provider(), TestProvider) # references to the old provider still work but return real tracer now real_tracer = provider.get_tracer("proxy-test") self.assertIsInstance(real_tracer, TestTracer) # reference to old proxy tracer now delegates to a real tracer and # creates real spans with tracer.start_span("") as span: self.assertIsInstance(span, SpanTest) def test_late_config(self): # get a tracer and instrument a function as we would at the # root of a module tracer = trace.get_tracer("test") @tracer.start_as_current_span("span") def my_function() -> Span: return trace.get_current_span() # call function before configuring tracing provider, should # return NonRecordingSpan from the NoOpTracer self.assertFalse(my_function().is_recording()) # configure tracing provider trace.set_tracer_provider(TestProvider()) # call function again, we should now be getting a TestSpan self.assertIsInstance(my_function(), SpanTest) python-opentelemetry-1.45.2/opentelemetry-api/tests/trace/test_span_context.py000066400000000000000000000046261527070070700300540ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import pickle import unittest from opentelemetry import trace class TestSpanContext(unittest.TestCase): def test_span_context_pickle(self): """ SpanContext needs to be pickleable to support multiprocessing so span can start as parent from the new spawned process """ sc = trace.SpanContext( 1, 2, is_remote=False, trace_flags=trace.DEFAULT_TRACE_OPTIONS, trace_state=trace.DEFAULT_TRACE_STATE, ) pickle_sc = pickle.loads(pickle.dumps(sc)) self.assertEqual(sc.trace_id, pickle_sc.trace_id) self.assertEqual(sc.span_id, pickle_sc.span_id) invalid_sc = trace.SpanContext( 9999999999999999999999999999999999999999999999999999999999999999999999999999, 9, is_remote=False, trace_flags=trace.DEFAULT_TRACE_OPTIONS, trace_state=trace.DEFAULT_TRACE_STATE, ) self.assertFalse(invalid_sc.is_valid) def test_trace_id_validity(self): trace_id_max_value = int("f" * 32, 16) span_id = 1 # valid trace IDs sc = trace.SpanContext(trace_id_max_value, span_id, is_remote=False) self.assertTrue(sc.is_valid) sc = trace.SpanContext(1, span_id, is_remote=False) self.assertTrue(sc.is_valid) # invalid trace IDs sc = trace.SpanContext(0, span_id, is_remote=False) self.assertFalse(sc.is_valid) sc = trace.SpanContext(-1, span_id, is_remote=False) self.assertFalse(sc.is_valid) sc = trace.SpanContext(trace_id_max_value + 1, span_id, is_remote=False) self.assertFalse(sc.is_valid) def test_span_id_validity(self): span_id_max = int("f" * 16, 16) trace_id = 1 # valid span IDs sc = trace.SpanContext(trace_id, span_id_max, is_remote=False) self.assertTrue(sc.is_valid) sc = trace.SpanContext(trace_id, 1, is_remote=False) self.assertTrue(sc.is_valid) # invalid span IDs sc = trace.SpanContext(trace_id, 0, is_remote=False) self.assertFalse(sc.is_valid) sc = trace.SpanContext(trace_id, -1, is_remote=False) self.assertFalse(sc.is_valid) sc = trace.SpanContext(trace_id, span_id_max + 1, is_remote=False) self.assertFalse(sc.is_valid) python-opentelemetry-1.45.2/opentelemetry-api/tests/trace/test_status.py000066400000000000000000000043171527070070700266670ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import unittest from logging import WARNING from opentelemetry.trace.status import Status, StatusCode class TestStatus(unittest.TestCase): def test_constructor(self): status = Status() self.assertIs(status.status_code, StatusCode.UNSET) self.assertIsNone(status.description) status = Status(StatusCode.ERROR, "unavailable") self.assertIs(status.status_code, StatusCode.ERROR) self.assertEqual(status.description, "unavailable") def test_invalid_description(self): with self.assertLogs(level=WARNING) as warning: status = Status( status_code=StatusCode.ERROR, description={"test": "val"}, # type: ignore ) self.assertIs(status.status_code, StatusCode.ERROR) self.assertEqual(status.description, None) self.assertIn( "Invalid status description type, expected str", warning.output[0], # type: ignore ) def test_description_and_non_error_status(self): with self.assertLogs(level=WARNING) as warning: status = Status(status_code=StatusCode.OK, description="status description") self.assertIs(status.status_code, StatusCode.OK) self.assertEqual(status.description, None) self.assertIn( "description should only be set when status_code is set to StatusCode.ERROR", warning.output[0], # type: ignore ) with self.assertLogs(level=WARNING) as warning: status = Status(status_code=StatusCode.UNSET, description="status description") self.assertIs(status.status_code, StatusCode.UNSET) self.assertEqual(status.description, None) self.assertIn( "description should only be set when status_code is set to StatusCode.ERROR", warning.output[0], # type: ignore ) status = Status(status_code=StatusCode.ERROR, description="status description") self.assertIs(status.status_code, StatusCode.ERROR) self.assertEqual(status.description, "status description") python-opentelemetry-1.45.2/opentelemetry-api/tests/trace/test_tracer.py000066400000000000000000000040751527070070700266250ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import asyncio from unittest import TestCase from opentelemetry.trace import ( INVALID_SPAN, NoOpTracer, Span, Tracer, _agnosticcontextmanager, get_current_span, ) class TestTracer(TestCase): def setUp(self): self.tracer = NoOpTracer() def test_start_span(self): with self.tracer.start_span("") as span: self.assertIsInstance(span, Span) def test_start_as_current_span_context_manager(self): with self.tracer.start_as_current_span("") as span: self.assertIsInstance(span, Span) def test_start_as_current_span_decorator(self): # using a list to track the mock call order calls = [] class MockTracer(Tracer): def start_span(self, *args, **kwargs): return INVALID_SPAN @_agnosticcontextmanager # pylint: disable=protected-access def start_as_current_span(self, *args, **kwargs): # type: ignore calls.append(1) yield INVALID_SPAN calls.append(9) mock_tracer = MockTracer() # test 1 : sync function @mock_tracer.start_as_current_span("name") def function_sync(data: str) -> int: calls.append(5) return len(data) calls = [] res = function_sync("123") self.assertEqual(res, 3) self.assertEqual(calls, [1, 5, 9]) # test 2 : async function @mock_tracer.start_as_current_span("name") async def function_async(data: str) -> int: calls.append(5) return len(data) calls = [] res = asyncio.run(function_async("123")) self.assertEqual(res, 3) self.assertEqual(calls, [1, 5, 9]) def test_get_current_span(self): with self.tracer.start_as_current_span("test") as span: get_current_span().set_attribute("test", "test") self.assertFalse(span.is_recording()) self.assertFalse(hasattr("span", "attributes")) python-opentelemetry-1.45.2/opentelemetry-api/tests/trace/test_tracestate.py000066400000000000000000000142571527070070700275070ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # pylint: disable=no-member import time import unittest from opentelemetry.trace.span import TraceState class TestTraceContextFormat(unittest.TestCase): def test_empty_tracestate(self): state = TraceState() self.assertEqual(len(state), 0) self.assertEqual(state.to_header(), "") def test_tracestate_valid_pairs(self): pairs = [("1a-2f@foo", "bar1"), ("foo-_*/bar", "bar4")] state = TraceState(pairs) self.assertEqual(len(state), 2) self.assertIsNotNone(state.get("foo-_*/bar")) self.assertEqual(state.get("foo-_*/bar"), "bar4") self.assertEqual(state.to_header(), "1a-2f@foo=bar1,foo-_*/bar=bar4") self.assertIsNone(state.get("random")) def test_tracestate_add_valid(self): state = TraceState() new_state = state.add("1a-2f@foo", "bar4") self.assertEqual(len(new_state), 1) self.assertEqual(new_state.get("1a-2f@foo"), "bar4") def test_tracestate_add_invalid(self): state = TraceState() new_state = state.add("%%%nsasa", "val") self.assertEqual(len(new_state), 0) new_state = new_state.add("key", "====val====") self.assertEqual(len(new_state), 0) self.assertEqual(new_state.to_header(), "") def test_tracestate_update_valid(self): state = TraceState([("a", "1")]) new_state = state.update("a", "2") self.assertEqual(new_state.get("a"), "2") new_state = new_state.add("b", "3") self.assertNotEqual(state, new_state) def test_tracestate_update_invalid(self): state = TraceState([("a", "1")]) new_state = state.update("a", "2=/") self.assertNotEqual(new_state.get("a"), "2=/") new_state = new_state.update("a", ",,2,,f") self.assertNotEqual(new_state.get("a"), ",,2,,f") self.assertEqual(new_state.get("a"), "1") def test_tracestate_update_new_key_added_below_capacity(self): # update() keeps its upsert behavior: a key that is not present is added # as long as there is room below the 32-entry limit. small_state = TraceState([("a", "1")]) new_state = small_state.update("b", "2") self.assertEqual(new_state.get("b"), "2") self.assertEqual(new_state.get("a"), "1") def test_tracestate_update_at_capacity_new_key_preserved(self): # Guards the previous bug: adding a new key at the 32-entry limit used to # push the list to 33 entries and cause the constructor to wipe them all. # Now the tracestate is returned unchanged, preserving existing entries. pairs = [(f"key{i}", f"value{i}") for i in range(32)] state = TraceState(pairs) new_state = state.update("newkey", "newvalue") self.assertEqual(len(new_state), 32) self.assertIsNone(new_state.get("newkey")) self.assertEqual(new_state.get("key0"), "value0") def test_tracestate_update_existing_key_at_capacity(self): # Updating an existing key while at the limit stays within the limit. pairs = [(f"key{i}", f"value{i}") for i in range(32)] state = TraceState(pairs) new_state = state.update("key0", "changed") self.assertEqual(len(new_state), 32) self.assertEqual(new_state.get("key0"), "changed") def test_tracestate_delete_preserved(self): state = TraceState([("a", "1"), ("b", "2"), ("c", "3")]) new_state = state.delete("b") self.assertIsNone(new_state.get("b")) entries = list(new_state.items()) a_place = entries.index(("a", "1")) c_place = entries.index(("c", "3")) self.assertLessEqual(a_place, c_place) def test_tracestate_from_header(self): entries = [ "1a-2f@foo=bar1", "1a-_*/2b@foo=bar2", "foo=bar3", "foo-_*/bar=bar4", ] header_list = [",".join(entries)] state = TraceState.from_header(header_list) self.assertEqual(state.to_header(), ",".join(entries)) def test_tracestate_order_changed(self): entries = [ "1a-2f@foo=bar1", "1a-_*/2b@foo=bar2", "foo=bar3", "foo-_*/bar=bar4", ] header_list = [",".join(entries)] state = TraceState.from_header(header_list) new_state = state.update("foo", "bar33") entries = list(new_state.items()) # type: ignore foo_place = entries.index(("foo", "bar33")) # type: ignore prev_first_place = entries.index(("1a-2f@foo", "bar1")) # type: ignore self.assertLessEqual(foo_place, prev_first_place) def test_trace_contains(self): entries = [ "1a-2f@foo=bar1", "1a-_*/2b@foo=bar2", "foo=bar3", "foo-_*/bar=bar4", ] header_list = [",".join(entries)] state = TraceState.from_header(header_list) self.assertTrue("foo" in state) self.assertFalse("bar" in state) self.assertIsNone(state.get("bar")) with self.assertRaises(KeyError): state["bar"] # pylint:disable=W0104 def test_tracestate_from_header_optional_whitespace(self): cases = [ ("foo=1 , bar=2", {"foo": "1", "bar": "2"}), ("\tfoo=1,\tbar=2\t", {"foo": "1", "bar": "2"}), (" foo=1", {"foo": "1"}), ("foo=1, ,bar=2", {"foo": "1", "bar": "2"}), ("foo=1,", {"foo": "1"}), ("foo=a b", {"foo": "a b"}), ] for header, expected in cases: with self.subTest(header=header): state = TraceState.from_header([header]) self.assertEqual(dict(state), expected) def test_tracestate_from_header_long_whitespace_run(self): for whitespace in (" ", "\t"): with self.subTest(whitespace=repr(whitespace)): header = "a=b" + whitespace * 100_000 + "x" start = time.perf_counter() with self.assertLogs(level="WARNING"): state = TraceState.from_header([header]) elapsed = time.perf_counter() - start self.assertEqual(len(state), 0) self.assertLess(elapsed, 1) python-opentelemetry-1.45.2/opentelemetry-api/tests/util/000077500000000000000000000000001527070070700236055ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-api/tests/util/test__importlib_metadata.py000066400000000000000000000117771527070070700312330ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import unittest import warnings from unittest import TestCase try: # pylint: disable-next=no-name-in-module from importlib.metadata import SelectableGroups as _SelectableGroups except ImportError: _SelectableGroups = None from opentelemetry.metrics import MeterProvider from opentelemetry.util._importlib_metadata import ( EntryPoint, EntryPoints, _as_entry_points, version, ) from opentelemetry.util._importlib_metadata import ( entry_points as importlib_metadata_entry_points, ) class TestEntryPoints(TestCase): def test_entry_points(self): self.assertIsInstance( next( iter( importlib_metadata_entry_points( group="opentelemetry_meter_provider", name="default_meter_provider", ) ) ).load()(), MeterProvider, ) def test_uniform_behavior(self): """ Test that entry_points behaves the same regardless of the Python version. """ entry_points = importlib_metadata_entry_points() self.assertIsInstance(entry_points, EntryPoints) entry_points = entry_points.select(group="opentelemetry_propagator") self.assertIsInstance(entry_points, EntryPoints) entry_points = entry_points.select(name="baggage") self.assertIsInstance(entry_points, EntryPoints) entry_point = next(iter(entry_points)) self.assertIsInstance(entry_point, EntryPoint) self.assertEqual(entry_point.name, "baggage") self.assertEqual(entry_point.group, "opentelemetry_propagator") self.assertEqual( entry_point.value, "opentelemetry.baggage.propagation:W3CBaggagePropagator", ) entry_points = importlib_metadata_entry_points(group="opentelemetry_propagator") self.assertIsInstance(entry_points, EntryPoints) entry_points = entry_points.select(name="baggage") self.assertIsInstance(entry_points, EntryPoints) entry_point = next(iter(entry_points)) self.assertIsInstance(entry_point, EntryPoint) self.assertEqual(entry_point.name, "baggage") self.assertEqual(entry_point.group, "opentelemetry_propagator") self.assertEqual( entry_point.value, "opentelemetry.baggage.propagation:W3CBaggagePropagator", ) entry_points = importlib_metadata_entry_points(name="baggage") self.assertIsInstance(entry_points, EntryPoints) entry_point = next(iter(entry_points)) self.assertIsInstance(entry_point, EntryPoint) self.assertEqual(entry_point.name, "baggage") self.assertEqual(entry_point.group, "opentelemetry_propagator") self.assertEqual( entry_point.value, "opentelemetry.baggage.propagation:W3CBaggagePropagator", ) entry_points = importlib_metadata_entry_points(group="abc") self.assertIsInstance(entry_points, EntryPoints) self.assertEqual(len(entry_points), 0) entry_points = importlib_metadata_entry_points(group="opentelemetry_propagator", name="abc") self.assertIsInstance(entry_points, EntryPoints) self.assertEqual(len(entry_points), 0) entry_points = importlib_metadata_entry_points(group="abc", name="abc") self.assertIsInstance(entry_points, EntryPoints) self.assertEqual(len(entry_points), 0) self.assertIsInstance(version("opentelemetry-api"), str) def test_as_entry_points_selectable_groups_compat(self): """Test that _as_entry_points correctly normalizes dict-like SelectableGroups (returned by importlib.metadata.entry_points() in Python 3.10) into EntryPoints. On Python 3.11+, entry_points() returns EntryPoints directly, which is handled by the fast-path in _as_entry_points. """ ep1 = EntryPoint(name="foo", value="bar:baz", group="gp") ep2 = EntryPoint(name="foo2", value="bar2:baz2", group="gp") selectable_groups = {"gp": [ep1, ep2]} normalized = _as_entry_points(selectable_groups) self.assertIsInstance(normalized, EntryPoints) self.assertEqual(len(normalized), 2) self.assertEqual(list(normalized), [ep1, ep2]) @unittest.skipIf( _SelectableGroups is None, "SelectableGroups not available on this Python version", ) def test_as_entry_points_selectable_groups_no_deprecation_warning(self): ep1 = EntryPoint(name="foo", value="bar:baz", group="gp") ep2 = EntryPoint(name="foo2", value="bar2:baz2", group="gp") sg = _SelectableGroups({"gp": [ep1, ep2]}) # type: ignore with warnings.catch_warnings(): warnings.simplefilter("error") normalized = _as_entry_points(sg) self.assertIsInstance(normalized, EntryPoints) self.assertEqual(len(normalized), 2) self.assertEqual(list(normalized), [ep1, ep2]) python-opentelemetry-1.45.2/opentelemetry-api/tests/util/test__providers.py000066400000000000000000000017711527070070700274000ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from importlib import reload from os import environ from unittest import TestCase from unittest.mock import Mock, patch from opentelemetry.util import _providers class Test_Providers(TestCase): # pylint: disable=invalid-name @patch.dict( environ, { # type: ignore "provider_environment_variable": "mock_provider_environment_variable" }, ) @patch("opentelemetry.util._importlib_metadata.entry_points") def test__providers(self, mock_entry_points): reload(_providers) mock_entry_points.configure_mock( side_effect=[ [ Mock(**{"load.return_value": Mock(return_value="a")}), ], ] ) self.assertEqual( _providers._load_provider( # pylint: disable=protected-access "provider_environment_variable", "provider" ), "a", ) python-opentelemetry-1.45.2/opentelemetry-api/tests/util/test_contextmanager.py000066400000000000000000000031471527070070700302420ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import asyncio import unittest from collections.abc import Callable, Iterator from opentelemetry.util._decorator import _agnosticcontextmanager @_agnosticcontextmanager def cm() -> Iterator[int]: yield 3 @_agnosticcontextmanager def cm_call_when_done(f: Callable[[], None]) -> Iterator[int]: yield 3 f() class TestContextManager(unittest.TestCase): def test_sync_with(self): with cm() as val: self.assertEqual(val, 3) def test_decorate_sync_func(self): @cm() def sync_func(a: str) -> str: return a + a res = sync_func("a") self.assertEqual(res, "aa") def test_decorate_async_func(self): # Test that a universal context manager decorating an async function runs it's cleanup # code after the entire async function coroutine finishes. This silently fails when # using the normal @contextmanager decorator, which runs it's __exit__() after the # un-started coroutine is returned. # # To see this behavior, change cm_call_when_done() to # be decorated with @contextmanager. events = [] @cm_call_when_done(lambda: events.append("cm_done")) async def async_func(a: str) -> str: events.append("start_async_func") await asyncio.sleep(0) events.append("finish_sleep") return a + a res = asyncio.run(async_func("a")) self.assertEqual(res, "aa") self.assertEqual(events, ["start_async_func", "finish_sleep", "cm_done"]) python-opentelemetry-1.45.2/opentelemetry-api/tests/util/test_once.py000066400000000000000000000023021527070070700261370ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from opentelemetry.test.concurrency_test import ConcurrencyTestBase, MockFunc from opentelemetry.util._once import Once class TestOnce(ConcurrencyTestBase): def test_once_single_thread(self): once_func = MockFunc() once = Once() self.assertEqual(once_func.call_count, 0) # first call should run called = once.do_once(once_func) # type: ignore[reportArgumentType] self.assertTrue(called) self.assertEqual(once_func.call_count, 1) # subsequent calls do nothing called = once.do_once(once_func) # type: ignore[reportArgumentType] self.assertFalse(called) self.assertEqual(once_func.call_count, 1) def test_once_many_threads(self): once_func = MockFunc() once = Once() def run_concurrently() -> bool: return once.do_once(once_func) # type: ignore[reportArgumentType] results = self.run_with_many_threads(run_concurrently, num_threads=100) self.assertEqual(once_func.call_count, 1) # check that only one of the threads got True self.assertEqual(results.count(True), 1) python-opentelemetry-1.45.2/opentelemetry-api/tests/util/test_re.py000066400000000000000000000075261527070070700256360ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # type: ignore import unittest from opentelemetry.util.re import parse_env_headers class TestParseHeaders(unittest.TestCase): @staticmethod def _common_test_cases(): return [ # invalid header name ("=value", [], True), ("}key=value", [], True), ("@key()=value", [], True), ("/key=value", [], True), # invalid header value ("name=\\", [], True), ('name=value"', [], True), ("name=;value", [], True), # different header values ("name=", [("name", "")], False), ("name===value=", [("name", "==value=")], False), # url-encoded headers ("key=value%20with%20space", [("key", "value with space")], False), ("key%21=value", [("key!", "value")], False), ("%20key%20=%20value%20", [("key", "value")], False), # header name case normalization ("Key=Value", [("key", "Value")], False), # mix of valid and invalid headers ( "name1=value1,invalidName, name2 = value2 , name3=value3==", [ ( "name1", "value1", ), ("name2", "value2"), ("name3", "value3=="), ], True, ), ( "=name=valu3; key1; key2, content = application, red=\tvelvet; cake", [("content", "application")], True, ), ] def test_parse_env_headers(self): inp = self._common_test_cases() + [ # invalid header value ("key=value othervalue", [], True), ] for case_ in inp: headers, expected, warn = case_ with self.subTest(headers=headers): if warn: with self.assertLogs(level="WARNING") as cm: self.assertEqual(parse_env_headers(headers), dict(expected)) self.assertTrue( "Header format invalid! Header values in environment " "variables must be URL encoded per the OpenTelemetry " "Protocol Exporter specification:" in cm.records[0].message, ) else: self.assertEqual(parse_env_headers(headers), dict(expected)) def test_parse_env_headers_liberal(self): inp = self._common_test_cases() + [ # valid header value ("key=value othervalue", [("key", "value othervalue")], False), ( "key=value Other_Value==", [("key", "value Other_Value==")], False, ), ] for case_ in inp: headers, expected, warn = case_ with self.subTest(headers=headers): if warn: with self.assertLogs(level="WARNING") as cm: self.assertEqual( parse_env_headers(headers, liberal=True), dict(expected), ) self.assertTrue( "Header format invalid! Header values in environment " "variables must be URL encoded per the OpenTelemetry " "Protocol Exporter specification or a comma separated " "list of name=value occurrences:" in cm.records[0].message, ) else: self.assertEqual( parse_env_headers(headers, liberal=True), dict(expected), ) python-opentelemetry-1.45.2/opentelemetry-configuration/000077500000000000000000000000001527070070700235645ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-configuration/README.rst000066400000000000000000000031161527070070700252540ustar00rootroot00000000000000OpenTelemetry Python SDK Declarative Configuration =================================================== |pypi| .. |pypi| image:: https://badge.fury.io/py/opentelemetry-configuration.svg :target: https://pypi.org/project/opentelemetry-configuration/ .. warning:: This package is **experimental**. The API surface, type names, and behaviour may change between minor versions. Pin a specific version in production. This package implements the `OpenTelemetry declarative configuration specification `_ for the Python SDK. It parses a YAML or JSON configuration file (or programmatically-constructed configuration model) into typed dataclasses and applies the result to the global SDK providers. The standard activation path is the ``OTEL_CONFIG_FILE`` environment variable, which the SDK's configurator picks up automatically. Set the variable and run your app under ``opentelemetry-instrument``; no code change is required. For programmatic use: .. code-block:: python from opentelemetry.configuration import configure_sdk, load_config_file config = load_config_file("otel-config.yaml") configure_sdk(config) Installation ------------ :: pip install opentelemetry-configuration References ---------- * `OpenTelemetry declarative configuration specification `_ * `Language support status matrix `_ * `OpenTelemetry Project `_ python-opentelemetry-1.45.2/opentelemetry-configuration/codegen/000077500000000000000000000000001527070070700251705ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-configuration/codegen/README.md000066400000000000000000000020651527070070700264520ustar00rootroot00000000000000# Code Generation Templates Custom [datamodel-code-generator](https://github.com/koxudaxi/datamodel-code-generator) templates used when generating `models.py` from the OpenTelemetry configuration JSON schema. ## `dataclass.jinja2` Extends the default dataclass template to support `additionalProperties` from the JSON Schema. Schema types that allow additional properties (e.g. `Sampler`, `SpanExporter`, `TextMapPropagator`) get: - `@_additional_properties` decorator: captures user-defined constructor kwargs - `additional_properties: ClassVar[dict[str, Any]]` annotation: satisfies type checkers without creating a dataclass field This enables plugin/custom component names to flow through typed dataclasses without a post-processing step. ## Usage Templates are applied automatically when regenerating models: ```sh tox -e generate-config-from-jsonschema ``` The template directory is configured in `pyproject.toml` under `[tool.datamodel-codegen]`. See `opentelemetry-configuration/src/opentelemetry/configuration/README.md` for the full schema update workflow. python-opentelemetry-1.45.2/opentelemetry-configuration/codegen/dataclass.jinja2000066400000000000000000000050251527070070700302300ustar00rootroot00000000000000{# Custom dataclass template for OpenTelemetry configuration models. Extends the default datamodel-codegen dataclass template to support JSON Schema additionalProperties. When a schema type allows additional properties (e.g. Sampler, SpanExporter), this template adds: - @_additional_properties decorator (captures user-defined kwargs) - additional_properties: ClassVar[dict[str, Any]] annotation (for type checkers) The template checks two context variables set by datamodel-codegen: - additionalProperties: set when the schema value is a boolean (true/false) - additionalPropertiesType: set when the schema value is a type object (e.g. {"type": ["object", "null"]}), which is how the OTel config schema defines it for plugin-capable types. See TestGeneratedModelsHaveAdditionalProperties in test_common.py for regression tests that guard against changes in these context variables. -#} {% for decorator in decorators -%} {{ decorator }} {% endfor -%} {%- set args = [] %} {%- for k, v in (dataclass_arguments or {}).items() %} {%- if v is not none and v is not false %} {%- set _ = args.append(k ~ '=' ~ (v|pprint)) %} {%- endif %} {%- endfor %} {%- set has_additional = (additionalProperties is defined and additionalProperties != false) or (additionalPropertiesType is defined) %} {%- if has_additional %} @_additional_properties {%- endif %} {%- if args %} @dataclass({{ args | join(', ') }}) {%- else %} @dataclass {%- endif %} {%- if base_class %} class {{ class_name }}({{ base_class }}): {%- else %} class {{ class_name }}: {%- endif %} {%- if description %} """ {{ description | escape_docstring | indent(4) }} """ {%- endif %} {%- if not fields and not description and not has_additional %} pass {%- endif %} {%- for field in fields -%} {%- if field.field %} {{ field.name }}: {{ field.type_hint }} = {{ field.field }} {%- else %} {{ field.name }}: {{ field.type_hint }} {%- if not (field.required or (field.represented_default == 'None' and field.strip_default_none)) %} = {{ field.represented_default }} {%- endif -%} {%- endif %} {%- if field.docstring %} """ {{ field.docstring | escape_docstring | indent(4) }} """ {%- if field.use_inline_field_description and not loop.last %} {% endif %} {%- elif field.inline_field_docstring %} {{ field.inline_field_docstring }} {%- if not loop.last %} {% endif %} {%- endif %} {%- endfor -%} {%- if has_additional %} additional_properties: ClassVar[dict[str, Any]] {%- endif -%} python-opentelemetry-1.45.2/opentelemetry-configuration/pyproject.toml000066400000000000000000000025501527070070700265020ustar00rootroot00000000000000[build-system] requires = ["hatchling"] build-backend = "hatchling.build" [project] name = "opentelemetry-configuration" dynamic = ["version"] description = "OpenTelemetry Python Declarative Configuration (experimental)" 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 :: 3 - Alpha", "Framework :: OpenTelemetry", "Intended Audience :: Developers", "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.45.2", "opentelemetry-sdk == 1.45.2", "pyyaml >= 5.4", "jsonschema >= 4.0", ] [project.urls] Homepage = "https://github.com/open-telemetry/opentelemetry-python/tree/main/opentelemetry-configuration" Repository = "https://github.com/open-telemetry/opentelemetry-python" [tool.hatch.version] path = "src/opentelemetry/configuration/version/__init__.py" [tool.hatch.build.targets.sdist] include = [ "/src", "/tests", ] [tool.hatch.build.targets.wheel] packages = ["src/opentelemetry"] python-opentelemetry-1.45.2/opentelemetry-configuration/src/000077500000000000000000000000001527070070700243535ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-configuration/src/opentelemetry/000077500000000000000000000000001527070070700272475ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-configuration/src/opentelemetry/configuration/000077500000000000000000000000001527070070700321165ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-configuration/src/opentelemetry/configuration/README.md000066400000000000000000000034541527070070700334030ustar00rootroot00000000000000# SDK Declarative Configuration This package implements [OpenTelemetry file-based configuration](https://opentelemetry.io/docs/specs/otel/configuration). ## Files - `schema.json`: vendored copy of the [OpenTelemetry configuration JSON schema](https://github.com/open-telemetry/opentelemetry-configuration) - `models.py`: Python dataclasses generated from `schema.json` by [datamodel-code-generator](https://github.com/koxudaxi/datamodel-code-generator), using a custom template from `opentelemetry-configuration/codegen/` (see that directory's README for details) ## Spec conformance Which fields of the configuration schema the Python SDK actually applies at runtime is tracked in the shared [language support status matrix](https://github.com/open-telemetry/opentelemetry-configuration/blob/main/language-support-status.md#python) in the `opentelemetry-configuration` repository. The Python column is the source of truth; update [`schema/meta_schema_language_python.yaml`](https://github.com/open-telemetry/opentelemetry-configuration/blob/main/schema/meta_schema_language_python.yaml) there when wiring lands or changes, and the matrix regenerates. ## Updating the schema 1. Pick the desired release tag from the [opentelemetry-configuration releases](https://github.com/open-telemetry/opentelemetry-configuration/releases) and overwrite `opentelemetry-configuration/src/opentelemetry/configuration/schema.json` with the `opentelemetry_configuration.json` published on that release. 2. Regenerate `models.py`: ```sh tox -e generate-config-from-jsonschema ``` 3. Bump `_SUPPORTED_SCHEMA_MAJOR` / `_SUPPORTED_SCHEMA_MINOR` in `file/_loader.py` to the version of the vendored schema. 4. Update any version string references in tests and source: ```sh grep -r "OLD_VERSION" opentelemetry-configuration/ ``` python-opentelemetry-1.45.2/opentelemetry-configuration/src/opentelemetry/configuration/__init__.py000066400000000000000000000026541527070070700342360ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 """OpenTelemetry SDK Declarative Configuration. This package implements the OpenTelemetry declarative configuration specification for the Python SDK. Load a YAML or JSON configuration file (or build a configuration model programmatically) and apply it to the process-global SDK providers. The standard activation path is the ``OTEL_CONFIG_FILE`` environment variable, which the SDK's configurator picks up automatically. For programmatic use: >>> from opentelemetry.configuration import ( ... load_config_file, ... configure_sdk, ... ) >>> config = load_config_file("otel-config.yaml") >>> configure_sdk(config) Construct a configuration programmatically and apply it: >>> from opentelemetry.configuration import ( ... OpenTelemetryConfiguration, ... configure_sdk, ... ) >>> configure_sdk(OpenTelemetryConfiguration(file_format="1.0-rc.1")) This package is **experimental**. The API surface, type names, and behaviour may change between minor versions. """ from opentelemetry.configuration._exceptions import ConfigurationError from opentelemetry.configuration._sdk import configure_sdk from opentelemetry.configuration.file._loader import load_config_file from opentelemetry.configuration.models import OpenTelemetryConfiguration __all__ = [ "ConfigurationError", "OpenTelemetryConfiguration", "configure_sdk", "load_config_file", ] python-opentelemetry-1.45.2/opentelemetry-configuration/src/opentelemetry/configuration/_common.py000066400000000000000000000211211527070070700341140ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations import dataclasses import inspect import logging from collections.abc import Callable from typing import Any, Protocol from urllib.parse import urlparse from opentelemetry.configuration._exceptions import ConfigurationError from opentelemetry.util._importlib_metadata import entry_points _logger = logging.getLogger(__name__) def _additional_properties(cls): """Decorator for dataclasses whose JSON Schema sets additionalProperties. Wraps the dataclass-generated ``__init__`` so that extra keyword arguments are captured into an ``additional_properties`` instance attribute instead of raising ``TypeError``. This lets user-defined component names flow through the config pipeline without modifying the codegen output for built-in fields. Applied automatically by the custom template in ``opentelemetry-sdk/codegen/`` when ``additionalPropertiesType`` is present in the template context (set by ``datamodel-codegen`` for schema types with ``additionalProperties``). """ original_init = cls.__init__ original_sig = inspect.signature(original_init) known_fields = frozenset(f.name for f in dataclasses.fields(cls)) def _init(self, **kwargs): known = {k: v for k, v in kwargs.items() if k in known_fields} extra = {k: v for k, v in kwargs.items() if k not in known_fields} original_init(self, **known) self.additional_properties = extra # Preserve the original parameter list for IDE autocompletion and # inspect.signature(), adding **kwargs to signal extras are accepted. # setattr used because pyright rejects direct __signature__ assignment. params = list(original_sig.parameters.values()) params.append(inspect.Parameter("kwargs", inspect.Parameter.VAR_KEYWORD)) setattr(_init, "__signature__", original_sig.replace(parameters=params)) # noqa: B010 cls.__init__ = _init return cls def load_entry_point(group: str, name: str) -> type: """Load a plugin class from an entry point group by name. Returns the loaded class — callers are responsible for instantiation with whatever arguments their config requires. Raises: ConfigurationError: If the entry point is not found or fails to load. """ try: ep = next(iter(entry_points(group=group, name=name)), None) if ep is None: raise ConfigurationError( f"Plugin '{name}' not found in group '{group}'. " "Make sure the package providing this plugin is installed." ) return ep.load() except ConfigurationError: raise except Exception as exc: raise ConfigurationError(f"Failed to load plugin '{name}' from group '{group}': {exc}") from exc class _ComponentConfig(Protocol): """Protocol for config dataclasses decorated with @_additional_properties. Values in ``additional_properties`` are nested config dicts (suitable for ``**kwargs`` splatting to the user-defined component class) or ``None`` (when the YAML uses ``my_plugin:`` or ``my_plugin: null``). Note: the generated models declare ``additional_properties`` as a ``ClassVar`` even though the decorator assigns it as an instance attribute at runtime. This is tolerated by pyright in ``standard`` mode but flagged in ``strict`` mode. See #5268. """ additional_properties: dict[str, dict[str, Any] | None] def _resolve_component( config: _ComponentConfig, registry: dict[str, Callable[[Any], Any]], entry_point_group: str, component_type: str, ) -> Any: """Resolve a config dataclass to a component instance. Checks built-in factories in ``registry`` first (by matching typed field names on ``config``), then falls back to entry point loading for plugin components found in ``config.additional_properties``. The JSON schema enforces exactly one component per config block (``minProperties: 1, maxProperties: 1``). If multiple typed fields or ``additional_properties`` entries are set (e.g. when schema validation is bypassed), the first registry match wins. Args: config: A dataclass with ``additional_properties`` (from the ``@_additional_properties`` decorator). registry: Mapping of built-in component names to factory callables. Each factory receives the field value from config. entry_point_group: The entry point group name for plugin loading. component_type: Human-readable name for error messages (e.g. "span exporter"). Returns: The resolved component instance. Raises: ConfigurationError: If no component type is specified in config. """ for name, factory in registry.items(): value = getattr(config, name, None) if value is not None: return factory(value) if config.additional_properties: name, plugin_config = next(iter(config.additional_properties.items())) return load_entry_point(entry_point_group, name)(**(plugin_config or {})) raise ConfigurationError(f"No {component_type} type specified in config.") def _parse_headers( headers: list | None, headers_list: str | None, ) -> dict[str, str] | None: """Merge headers struct and headers_list into a dict. Returns None if neither is set, letting the exporter read env vars. headers struct takes priority over headers_list for the same key. """ if headers is None and headers_list is None: return None result: dict[str, str] = {} if headers_list: for item in headers_list.split(","): item = item.strip() if "=" in item: key, value = item.split("=", 1) result[key.strip()] = value.strip() elif item: _logger.warning( "Invalid header pair in headers_list (missing '='): %s", item, ) if headers: for pair in headers: if isinstance(pair, dict): result[pair["name"]] = pair.get("value") or "" else: result[pair.name] = pair.value or "" return result def _map_compression( value: str | None, compression_enum: type, *, allow_deflate: bool = False, ) -> object | None: """Map a compression string to the given Compression enum value.""" if value is None: return None value_lower = value.lower() supports_deflate = hasattr(compression_enum, "Deflate") if value_lower == "none": return None if value_lower == "gzip": return compression_enum.Gzip # type: ignore[attr-defined] if value_lower == "deflate" and allow_deflate and supports_deflate: return compression_enum.Deflate # type: ignore[attr-defined] supported_values = ["'gzip'", "'none'"] if allow_deflate and supports_deflate: supported_values.insert(1, "'deflate'") raise ConfigurationError( f"Unsupported compression value '{value}'. Supported values: {', '.join(supported_values)}." ) def _parse_otlp_file_output_stream(output_stream: str | None) -> str | None: """Resolve an output_stream value to a file path, or None for stdout. Per the OTel file exporter spec, output_stream is "stdout" (or None, which means the same), or a "file://" URI giving a path. """ if output_stream is None or output_stream == "stdout": return None try: parsed = urlparse(output_stream) except ValueError as exc: raise ConfigurationError( f"Failed to parse output_stream '{output_stream}' for otlp_file_development exporter: {exc}" ) from exc is_local_file_uri = parsed.scheme == "file" and parsed.netloc in ("", "localhost") and bool(parsed.path) has_extra_components = parsed.params or parsed.query or parsed.fragment if is_local_file_uri and not has_extra_components: path = parsed.path if not path.startswith("/"): raise ConfigurationError( f"Unsupported output_stream '{output_stream}' for " "otlp_file_development exporter. Path must be absolute." ) if path.endswith("/"): raise ConfigurationError( f"Unsupported output_stream '{output_stream}' for " "otlp_file_development exporter. Path must be a file, " "not a directory." ) return path raise ConfigurationError( f"Unsupported output_stream '{output_stream}' for otlp_file_development " "exporter. Supported values: stdout, file://." ) _conversion.py000066400000000000000000000131151527070070700347360ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-configuration/src/opentelemetry/configuration# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 """Recursive dict-to-dataclass conversion for parsed config data. The YAML/JSON loader produces nested dicts. Factory functions expect typed dataclass instances (e.g. ``TracerProvider``, ``SpanProcessor``). This module walks each field's type annotation and converts nested dicts into their corresponding dataclass types. """ from __future__ import annotations from collections.abc import Mapping from dataclasses import MISSING, fields, is_dataclass from enum import Enum from types import UnionType from typing import Any, TypeVar, Union, get_args, get_origin, get_type_hints _T = TypeVar("_T") def _unwrap_optional(type_hint: Any) -> Any: """Strip ``None`` from a ``X | None`` / ``Optional[X]`` annotation. Returns the unwrapped type, or the original hint if not a union with None. """ origin = get_origin(type_hint) if origin is UnionType or origin is Union: non_none = [t for t in get_args(type_hint) if t is not type(None)] if len(non_none) == 1: return non_none[0] return type_hint def _is_empty_constructible_dataclass(unwrapped: Any) -> bool: """True if ``unwrapped`` is a dataclass type instantiable with no args. A dataclass with only optional fields (all have a default or default_factory) can be built as ``cls()``; one with a required field cannot, and coercing a present null into it would raise ``TypeError``. """ return ( isinstance(unwrapped, type) and is_dataclass(unwrapped) and all(field.default is not MISSING or field.default_factory is not MISSING for field in fields(unwrapped)) ) def _convert_value(value: Any, type_hint: Any) -> Any: """Convert a value according to its type hint. Recursively converts dicts to dataclasses and lists of dicts to lists of dataclasses. Other values (primitives, enums, ``dict[str, Any]`` aliases) pass through unchanged. """ unwrapped = _unwrap_optional(type_hint) origin = get_origin(unwrapped) if value is None: # A mapping key present with an empty (null) YAML value parses to # ``None``, which is otherwise indistinguishable from an absent key. # For object-typed nodes the declarative-config spec treats a present # null as "select this with an empty config" — e.g. ``always_on:`` is # equivalent to ``always_on: {}`` and a metric ``console:`` exporter to # ``console: {}``. Substitute an empty mapping and let the dict/ # dataclass handling below build it, so downstream ``is not None`` type # dispatch selects it. Scalar fields — and dataclasses with required # fields, which cannot be defaulted — keep ``None``, so an absent # optional section stays unset. if origin is dict or _is_empty_constructible_dataclass(unwrapped): value = {} else: return None # list[X] — recurse on each element if origin is list and isinstance(value, list): args = get_args(unwrapped) if args: item_type = args[0] return [_convert_value(item, item_type) for item in value] return value # Direct dataclass type — recurse if isinstance(unwrapped, type) and is_dataclass(unwrapped) and isinstance(value, dict): return _dict_to_dataclass(value, unwrapped) # Enum type — coerce string/value to the Enum member if isinstance(unwrapped, type) and issubclass(unwrapped, Enum) and not isinstance(value, unwrapped): return unwrapped(value) return value def _dict_to_dataclass(data: Mapping[str, Any], cls: type[_T]) -> _T: """Recursively convert a mapping to a dataclass instance. For each key in ``data``: - If it matches a known dataclass field, the value is converted according to that field's type annotation (recursing for nested dataclasses). - Unknown keys are passed through as kwargs; classes decorated with ``@_additional_properties`` will capture them on the instance's ``additional_properties`` attribute. ``ClassVar`` fields (e.g. the ``additional_properties`` annotation on decorated dataclasses) are ignored as expected. Raises: TypeError: If ``cls`` is not a dataclass type. """ if not is_dataclass(cls): raise TypeError(f"{cls.__name__} is not a dataclass") # Annotated as ``dict[str, Any]`` so astroid stops tracing into # ``get_type_hints`` — under pylint 3.x that path leads into # Python 3.14's ``annotationlib`` (which uses t-strings) and crashes. hints: dict[str, Any] = dict(get_type_hints(cls, include_extras=False)) known_fields = {f.name for f in fields(cls)} kwargs: dict[str, Any] = {} for key, value in data.items(): # The OTel configuration schema uses "/" as a namespace separator for # development/experimental features (e.g. "otlp_file/development", # "instrumentation/development"). Python identifiers cannot contain # "/", so the corresponding dataclass fields use "_" instead (e.g. # "otlp_file_development"). Without this normalisation the key would # not match any known field and would fall through to # additional_properties, causing the factory lookup to fail silently. field_key = key.replace("/", "_") if field_key in known_fields: type_hint = hints.get(field_key) kwargs[field_key] = _convert_value(value, type_hint) else: # Unknown key — @_additional_properties decorator will capture it. kwargs[key] = value return cls(**kwargs) _exceptions.py000066400000000000000000000034531527070070700347360ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-configuration/src/opentelemetry/configuration# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 class ConfigurationError(Exception): """Raised when configuration loading, parsing, validation, or instantiation fails. This includes errors from: - File not found or inaccessible - Invalid YAML/JSON syntax - Schema validation failures - Environment variable substitution errors - Missing required SDK extensions (e.g., propagator packages not installed) """ class MissingDependencyError(ConfigurationError, ImportError): """Raised when an optional dependency is not installed. Inherits from both :class:`ConfigurationError` and :class:`ImportError` to maintain backwards compatibility with callers that catch ``ImportError``. Args: package: The name of the missing package. feature: Optional description of the feature that requires the package. install_name: Optional package name used in the pip install command (defaults to ``package``). extras: Optional extras string for the pip install command. """ def __init__( self, package: str, feature: str | None = None, install_name: str | None = None, extras: str | None = None, ) -> None: self.package = package self.feature = feature self.install_name = install_name or package self.extras = extras if extras: install_cmd = f'pip install "{self.install_name}[{extras}]"' else: install_cmd = f"pip install {self.install_name}" if feature: message = f"{feature} requires '{package}'. Install it with: {install_cmd}" else: message = f"'{package}' is required but not installed. Install it with: {install_cmd}" super().__init__(message) _logger_provider.py000066400000000000000000000341701527070070700357460ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-configuration/src/opentelemetry/configuration# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations import logging from opentelemetry._logs import set_logger_provider from opentelemetry.configuration._common import ( _map_compression, _parse_headers, _parse_otlp_file_output_stream, load_entry_point, ) from opentelemetry.configuration._exceptions import ( ConfigurationError, MissingDependencyError, ) from opentelemetry.configuration.models import ( AttributeLimits, ) from opentelemetry.configuration.models import ( BatchLogRecordProcessor as BatchLogRecordProcessorConfig, ) from opentelemetry.configuration.models import ( ExperimentalLoggerConfig as LoggerConfigConfig, ) from opentelemetry.configuration.models import ( ExperimentalLoggerConfigurator as LoggerConfiguratorConfig, ) from opentelemetry.configuration.models import ( ExperimentalOtlpFileExporter as ExperimentalOtlpFileExporterConfig, ) from opentelemetry.configuration.models import ( LoggerProvider as LoggerProviderConfig, ) from opentelemetry.configuration.models import ( LogRecordExporter as LogRecordExporterConfig, ) from opentelemetry.configuration.models import ( LogRecordLimits as LogRecordLimitsConfig, ) from opentelemetry.configuration.models import ( LogRecordProcessor as LogRecordProcessorConfig, ) from opentelemetry.configuration.models import ( OtlpGrpcExporter as OtlpGrpcExporterConfig, ) from opentelemetry.configuration.models import ( OtlpHttpExporter as OtlpHttpExporterConfig, ) from opentelemetry.configuration.models import ( SimpleLogRecordProcessor as SimpleLogRecordProcessorConfig, ) from opentelemetry.sdk._logs import LoggerProvider from opentelemetry.sdk._logs._internal import ( LogRecordLimits, _LoggerConfig, _RuleBasedLoggerConfigurator, ) from opentelemetry.sdk._logs._internal.export import ( BatchLogRecordProcessor, ConsoleLogRecordExporter, LogRecordExporter, SimpleLogRecordProcessor, ) from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.util.instrumentation import _scope_name_matches_glob _logger = logging.getLogger(__name__) _DEFAULT_OTEL_LOG_ATTRIBUTE_COUNT_LIMIT = 128 # BatchLogRecordProcessor defaults per OTel spec (milliseconds). _DEFAULT_SCHEDULE_DELAY_MILLIS = 1000 _DEFAULT_EXPORT_TIMEOUT_MILLIS = 30000 _DEFAULT_MAX_QUEUE_SIZE = 2048 _DEFAULT_MAX_EXPORT_BATCH_SIZE = 512 def _create_console_log_exporter() -> ConsoleLogRecordExporter: """Create a ConsoleLogRecordExporter.""" return ConsoleLogRecordExporter() def _create_otlp_http_log_exporter( config: OtlpHttpExporterConfig, ) -> LogRecordExporter: """Create an OTLP HTTP log exporter from config.""" try: # pylint: disable=import-outside-toplevel,no-name-in-module from opentelemetry.exporter.otlp.proto.http import ( # noqa: PLC0415 # type: ignore[import-untyped] Compression, ) from opentelemetry.exporter.otlp.proto.http._log_exporter import ( # noqa: PLC0415 # type: ignore[import-untyped] OTLPLogExporter, ) except ImportError as exc: raise MissingDependencyError( package="opentelemetry-exporter-otlp-proto-http", feature="otlp_http log exporter", ) from exc compression = _map_compression(config.compression, Compression, allow_deflate=True) headers = _parse_headers(config.headers, config.headers_list) timeout = (config.timeout / 1000.0) if config.timeout is not None else None return OTLPLogExporter( # type: ignore[return-value] endpoint=config.endpoint, headers=headers, timeout=timeout, compression=compression, # type: ignore[arg-type] ) def _create_otlp_grpc_log_exporter( config: OtlpGrpcExporterConfig, ) -> LogRecordExporter: """Create an OTLP gRPC log exporter from config.""" try: # pylint: disable=import-outside-toplevel,no-name-in-module import grpc # noqa: PLC0415 # type: ignore[import-untyped] from opentelemetry.exporter.otlp.proto.grpc._log_exporter import ( # noqa: PLC0415 # type: ignore[import-untyped] OTLPLogExporter, ) except ImportError as exc: raise MissingDependencyError( package="opentelemetry-exporter-otlp-proto-grpc", feature="otlp_grpc log exporter", ) from exc compression = _map_compression(config.compression, grpc.Compression) headers = _parse_headers(config.headers, config.headers_list) timeout = (config.timeout / 1000.0) if config.timeout is not None else None return OTLPLogExporter( # type: ignore[return-value] endpoint=config.endpoint, headers=headers, timeout=timeout, compression=compression, # type: ignore[arg-type] ) def _create_otlp_file_development_log_exporter( config: ExperimentalOtlpFileExporterConfig, ) -> LogRecordExporter: """Create an OTLP file (JSON Lines) log exporter from config.""" try: # pylint: disable=import-outside-toplevel,no-name-in-module from opentelemetry.exporter.otlp.json.file._log_exporter import ( # noqa: PLC0415 # type: ignore[import-untyped] FileLogExporter, ) except ImportError as exc: raise ConfigurationError( "otlp_file_development log exporter requires 'opentelemetry-exporter-otlp-json-file'. " "Install it with: pip install opentelemetry-exporter-otlp-json-file" ) from exc path = _parse_otlp_file_output_stream(config.output_stream) return FileLogExporter(path) if path is not None else FileLogExporter() _LOG_EXPORTER_REGISTRY: dict = { "otlp_http": _create_otlp_http_log_exporter, "otlp_grpc": _create_otlp_grpc_log_exporter, "console": lambda _: ConsoleLogRecordExporter(), "otlp_file_development": _create_otlp_file_development_log_exporter, } def _create_log_record_exporter( config: LogRecordExporterConfig, ) -> LogRecordExporter: """Create a log record exporter from config. Known exporter types are checked via typed fields on the LogRecordExporter dataclass. Unknown exporter names captured in additional_properties by the @_additional_properties decorator are loaded via the ``opentelemetry_logs_exporter`` entry point group. """ for name, factory in _LOG_EXPORTER_REGISTRY.items(): value = getattr(config, name, None) if value is not None: return factory(value) if config.additional_properties: name, plugin_config = next(iter(config.additional_properties.items())) return load_entry_point("opentelemetry_logs_exporter", name)(**(plugin_config or {})) raise ConfigurationError( "No exporter type specified in log record exporter config. " "Supported types: console, otlp_http, otlp_grpc, otlp_file_development." ) def _create_batch_log_record_processor( config: BatchLogRecordProcessorConfig, ) -> BatchLogRecordProcessor: """Create a BatchLogRecordProcessor from config. Passes explicit defaults to suppress OTEL_BLRP_* env var reading. """ exporter = _create_log_record_exporter(config.exporter) schedule_delay = config.schedule_delay if config.schedule_delay is not None else _DEFAULT_SCHEDULE_DELAY_MILLIS export_timeout = config.export_timeout if config.export_timeout is not None else _DEFAULT_EXPORT_TIMEOUT_MILLIS max_queue_size = config.max_queue_size if config.max_queue_size is not None else _DEFAULT_MAX_QUEUE_SIZE max_export_batch_size = ( config.max_export_batch_size if config.max_export_batch_size is not None else _DEFAULT_MAX_EXPORT_BATCH_SIZE ) return BatchLogRecordProcessor( exporter=exporter, schedule_delay_millis=float(schedule_delay), export_timeout_millis=float(export_timeout), max_queue_size=max_queue_size, max_export_batch_size=max_export_batch_size, ) def _create_simple_log_record_processor( config: SimpleLogRecordProcessorConfig, ) -> SimpleLogRecordProcessor: """Create a SimpleLogRecordProcessor from config.""" exporter = _create_log_record_exporter(config.exporter) return SimpleLogRecordProcessor(exporter) def _create_log_record_processor( config: LogRecordProcessorConfig, ) -> BatchLogRecordProcessor | SimpleLogRecordProcessor: """Create a log record processor from config.""" if config.batch is not None: return _create_batch_log_record_processor(config.batch) if config.simple is not None: return _create_simple_log_record_processor(config.simple) raise ConfigurationError( "No processor type specified in log record processor config. Supported types: batch, simple." ) def _to_logger_config(config: LoggerConfigConfig | None) -> _LoggerConfig: """Map an experimental per-logger config to an SDK ``_LoggerConfig``. Only ``enabled`` is honored. ``minimum_severity`` and ``trace_based`` are accepted by the config schema but not supported by the Python SDK ``_LoggerConfig``; the ones that are set are ignored with a warning naming them. An absent ``enabled`` leaves the logger enabled. """ if config is None: return _LoggerConfig.default() unsupported_fields = [ field_name for field_name in ("minimum_severity", "trace_based") if getattr(config, field_name) is not None ] if unsupported_fields: _logger.warning( "Ignoring logger_configurator fields that are not supported by the Python SDK LoggerProvider: %s", ", ".join(unsupported_fields), ) if config.enabled is None: return _LoggerConfig.default() return _LoggerConfig(is_enabled=config.enabled) def _create_logger_configurator( config: LoggerConfiguratorConfig, ) -> _RuleBasedLoggerConfigurator: """Build a rule-based logger configurator from experimental config. Each entry in ``loggers`` maps an instrumentation-scope name glob to a per-logger config; ``default_config`` applies to scopes matching no glob. Rules are evaluated in order, so earlier entries take precedence. """ rules = [ ( _scope_name_matches_glob(matcher.name), _to_logger_config(matcher.config), ) for matcher in (config.loggers or []) ] return _RuleBasedLoggerConfigurator( rules=rules, default_config=_to_logger_config(config.default_config), ) def _create_log_record_limits( config: LogRecordLimitsConfig, global_limits: AttributeLimits | None = None, ) -> LogRecordLimits: """Create LogRecordLimits from config. Absent fields fall back to global_limits (if provided), then to OTel spec defaults (128 for counts, unlimited for lengths). Explicit values suppress env-var reading — matching Java SDK behavior. """ attribute_count_limit = config.attribute_count_limit if attribute_count_limit is None and global_limits is not None: attribute_count_limit = global_limits.attribute_count_limit attribute_value_length_limit = config.attribute_value_length_limit if attribute_value_length_limit is None and global_limits is not None: attribute_value_length_limit = global_limits.attribute_value_length_limit max_attributes = ( attribute_count_limit if attribute_count_limit is not None else _DEFAULT_OTEL_LOG_ATTRIBUTE_COUNT_LIMIT ) max_attribute_length = ( attribute_value_length_limit if attribute_value_length_limit is not None else LogRecordLimits.UNSET ) # The log-record-specific fields are the ones the SDK enforces, so they are # set explicitly too. Leaving them absent would let OTEL_LOGRECORD_ATTRIBUTE_* # override the configured values. return LogRecordLimits( max_attributes=max_attributes, max_attribute_length=max_attribute_length, max_log_record_attributes=max_attributes, max_log_record_attribute_length=max_attribute_length, ) def create_logger_provider( config: LoggerProviderConfig | None, resource: Resource | None = None, global_attribute_limits: AttributeLimits | None = None, ) -> LoggerProvider: """Create an SDK LoggerProvider from declarative config. Does NOT read OTEL_BLRP_* or other env vars for values explicitly controlled by the config. Absent config values use OTel spec defaults. Args: config: LoggerProvider config from the parsed config file, or None. resource: Resource to attach to the provider. global_attribute_limits: Top-level attribute_limits from the root config, used as a fallback when per-signal limits are not specified. Returns: A configured LoggerProvider. """ logger_configurator = ( _create_logger_configurator(config.logger_configurator_development) if config is not None and config.logger_configurator_development is not None else None ) if config is not None and config.limits is not None: limits = config.limits else: limits = LogRecordLimitsConfig() log_record_limits = _create_log_record_limits(limits, global_attribute_limits) provider = LoggerProvider( resource=resource, log_record_limits=log_record_limits, _logger_configurator=logger_configurator, ) if config is None: return provider for processor_config in config.processors: provider.add_log_record_processor(_create_log_record_processor(processor_config)) return provider def configure_logger_provider( config: LoggerProviderConfig | None, resource: Resource | None = None, global_attribute_limits: AttributeLimits | None = None, ) -> None: """Configure the global LoggerProvider from declarative config. When config is None (logger_provider section absent from config file), the global is not set — matching Java/JS SDK behavior. Args: config: LoggerProvider config from the parsed config file, or None. resource: Resource to attach to the provider. global_attribute_limits: Top-level attribute_limits from the root config, used as a fallback when per-signal limits are not specified. """ if config is None: return set_logger_provider(create_logger_provider(config, resource, global_attribute_limits)) _meter_provider.py000066400000000000000000000615341527070070700356070ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-configuration/src/opentelemetry/configuration# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations import logging from opentelemetry import metrics from opentelemetry.configuration._common import ( _map_compression, _parse_headers, _parse_otlp_file_output_stream, load_entry_point, ) from opentelemetry.configuration._exceptions import ( ConfigurationError, MissingDependencyError, ) from opentelemetry.configuration.models import ( Aggregation as AggregationConfig, ) from opentelemetry.configuration.models import ( CardinalityLimits as CardinalityLimitsConfig, ) from opentelemetry.configuration.models import ( ConsoleMetricExporter as ConsoleMetricExporterConfig, ) from opentelemetry.configuration.models import ( ExemplarFilter as ExemplarFilterConfig, ) from opentelemetry.configuration.models import ( ExperimentalMeterConfig as MeterConfigConfig, ) from opentelemetry.configuration.models import ( ExperimentalMeterConfigurator as MeterConfiguratorConfig, ) from opentelemetry.configuration.models import ( ExperimentalOtlpFileMetricExporter as ExperimentalOtlpFileMetricExporterConfig, ) from opentelemetry.configuration.models import ( ExperimentalPrometheusMetricExporter as PrometheusMetricExporterConfig, ) from opentelemetry.configuration.models import ( ExporterDefaultHistogramAggregation, ExporterTemporalityPreference, InstrumentType, ) from opentelemetry.configuration.models import ( MeterProvider as MeterProviderConfig, ) from opentelemetry.configuration.models import ( MetricReader as MetricReaderConfig, ) from opentelemetry.configuration.models import ( OtlpGrpcMetricExporter as OtlpGrpcMetricExporterConfig, ) from opentelemetry.configuration.models import ( OtlpHttpMetricExporter as OtlpHttpMetricExporterConfig, ) from opentelemetry.configuration.models import ( PeriodicMetricReader as PeriodicMetricReaderConfig, ) from opentelemetry.configuration.models import ( PullMetricExporter as PullMetricExporterConfig, ) from opentelemetry.configuration.models import ( PullMetricReader as PullMetricReaderConfig, ) from opentelemetry.configuration.models import ( PushMetricExporter as PushMetricExporterConfig, ) from opentelemetry.configuration.models import ( View as ViewConfig, ) from opentelemetry.sdk.metrics import ( AlwaysOffExemplarFilter, AlwaysOnExemplarFilter, Counter, Histogram, MeterProvider, ObservableCounter, ObservableGauge, ObservableUpDownCounter, TraceBasedExemplarFilter, UpDownCounter, _Gauge, ) from opentelemetry.sdk.metrics._internal import ( _MeterConfig, _RuleBasedMeterConfigurator, ) from opentelemetry.sdk.metrics.export import ( AggregationTemporality, CardinalityLimits, ConsoleMetricExporter, MetricExporter, MetricReader, PeriodicExportingMetricReader, ) from opentelemetry.sdk.metrics.view import ( Aggregation, DefaultAggregation, DropAggregation, ExplicitBucketHistogramAggregation, ExponentialBucketHistogramAggregation, LastValueAggregation, SumAggregation, View, ) from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.util.instrumentation import _scope_name_matches_glob _logger = logging.getLogger(__name__) # Default interval/timeout per OTel spec (milliseconds). _DEFAULT_EXPORT_INTERVAL_MILLIS = 60000 _DEFAULT_EXPORT_TIMEOUT_MILLIS = 30000 # Instrument type → SDK instrument class mapping (for View selectors). _INSTRUMENT_TYPE_MAP: dict[InstrumentType, type] = { InstrumentType.counter: Counter, InstrumentType.up_down_counter: UpDownCounter, InstrumentType.histogram: Histogram, InstrumentType.gauge: _Gauge, InstrumentType.observable_counter: ObservableCounter, InstrumentType.observable_gauge: ObservableGauge, InstrumentType.observable_up_down_counter: ObservableUpDownCounter, } def _map_temporality( pref: ExporterTemporalityPreference | None, ) -> dict[type, AggregationTemporality]: """Map a temporality preference to an explicit preferred_temporality dict. Always returns an explicit dict to suppress OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE. Default (None or cumulative) → all instruments CUMULATIVE. """ if pref is None or pref == ExporterTemporalityPreference.cumulative: return { Counter: AggregationTemporality.CUMULATIVE, UpDownCounter: AggregationTemporality.CUMULATIVE, Histogram: AggregationTemporality.CUMULATIVE, ObservableCounter: AggregationTemporality.CUMULATIVE, ObservableUpDownCounter: AggregationTemporality.CUMULATIVE, ObservableGauge: AggregationTemporality.CUMULATIVE, } if pref == ExporterTemporalityPreference.delta: return { Counter: AggregationTemporality.DELTA, UpDownCounter: AggregationTemporality.CUMULATIVE, Histogram: AggregationTemporality.DELTA, ObservableCounter: AggregationTemporality.DELTA, ObservableUpDownCounter: AggregationTemporality.CUMULATIVE, ObservableGauge: AggregationTemporality.CUMULATIVE, } if pref == ExporterTemporalityPreference.low_memory: return { Counter: AggregationTemporality.DELTA, UpDownCounter: AggregationTemporality.CUMULATIVE, Histogram: AggregationTemporality.DELTA, ObservableCounter: AggregationTemporality.CUMULATIVE, ObservableUpDownCounter: AggregationTemporality.CUMULATIVE, ObservableGauge: AggregationTemporality.CUMULATIVE, } raise ConfigurationError( f"Unsupported temporality preference '{pref}'. Supported values: cumulative, delta, low_memory." ) def _map_histogram_aggregation( pref: ExporterDefaultHistogramAggregation | None, ) -> dict[type, Aggregation]: """Map a histogram aggregation preference to an explicit preferred_aggregation dict. Always returns an explicit dict to suppress OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION. Default (None or explicit_bucket_histogram) → ExplicitBucketHistogramAggregation. """ if pref is None or pref == ExporterDefaultHistogramAggregation.explicit_bucket_histogram: return {Histogram: ExplicitBucketHistogramAggregation()} if pref == ExporterDefaultHistogramAggregation.base2_exponential_bucket_histogram: return {Histogram: ExponentialBucketHistogramAggregation()} raise ConfigurationError( f"Unsupported default histogram aggregation '{pref}'. " "Supported values: explicit_bucket_histogram, base2_exponential_bucket_histogram." ) def _create_aggregation(config: AggregationConfig) -> Aggregation: """Create an SDK Aggregation from config, passing through detail parameters.""" if config.default is not None: return DefaultAggregation() if config.drop is not None: return DropAggregation() if config.explicit_bucket_histogram is not None: return ExplicitBucketHistogramAggregation( boundaries=config.explicit_bucket_histogram.boundaries, record_min_max=( config.explicit_bucket_histogram.record_min_max if config.explicit_bucket_histogram.record_min_max is not None else True ), ) if config.base2_exponential_bucket_histogram is not None: kwargs = {} if config.base2_exponential_bucket_histogram.max_size is not None: kwargs["max_size"] = config.base2_exponential_bucket_histogram.max_size if config.base2_exponential_bucket_histogram.max_scale is not None: kwargs["max_scale"] = config.base2_exponential_bucket_histogram.max_scale if config.base2_exponential_bucket_histogram.record_min_max is not None: kwargs["record_min_max"] = config.base2_exponential_bucket_histogram.record_min_max return ExponentialBucketHistogramAggregation(**kwargs) if config.last_value is not None: return LastValueAggregation() if config.sum is not None: return SumAggregation() raise ConfigurationError( f"Unknown or unsupported aggregation type in config: {config!r}. " "Supported types: default, drop, explicit_bucket_histogram, " "base2_exponential_bucket_histogram, last_value, sum." ) def _create_view(config: ViewConfig) -> View: """Create an SDK View from config.""" selector = config.selector stream = config.stream instrument_type = None if selector.instrument_type is not None: instrument_type = _INSTRUMENT_TYPE_MAP.get(selector.instrument_type) if instrument_type is None: raise ConfigurationError(f"Unknown instrument type: {selector.instrument_type!r}") attribute_keys: set[str] | None = None if stream.attribute_keys is not None: if stream.attribute_keys.excluded: _logger.warning( "attribute_keys.excluded is not supported by the Python SDK View; the exclusion list will be ignored." ) if stream.attribute_keys.included is not None: attribute_keys = set(stream.attribute_keys.included) aggregation = None if stream.aggregation is not None: aggregation = _create_aggregation(stream.aggregation) return View( instrument_type=instrument_type, instrument_name=selector.instrument_name, meter_name=selector.meter_name, meter_version=selector.meter_version, meter_schema_url=selector.meter_schema_url, instrument_unit=selector.unit, name=stream.name, description=stream.description, attribute_keys=attribute_keys, aggregation=aggregation, aggregation_cardinality_limit=stream.aggregation_cardinality_limit, ) def _create_console_metric_exporter( config: ConsoleMetricExporterConfig, ) -> MetricExporter: """Create a ConsoleMetricExporter from config.""" preferred_temporality = _map_temporality(config.temporality_preference) preferred_aggregation = _map_histogram_aggregation(config.default_histogram_aggregation) return ConsoleMetricExporter( preferred_temporality=preferred_temporality, preferred_aggregation=preferred_aggregation, ) def _create_otlp_http_metric_exporter( config: OtlpHttpMetricExporterConfig, ) -> MetricExporter: """Create an OTLP HTTP metric exporter from config.""" try: # pylint: disable=import-outside-toplevel,no-name-in-module from opentelemetry.exporter.otlp.proto.http import ( # noqa: PLC0415 # type: ignore[import-untyped] Compression, ) from opentelemetry.exporter.otlp.proto.http.metric_exporter import ( # noqa: PLC0415 # type: ignore[import-untyped] OTLPMetricExporter, ) except ImportError as exc: raise MissingDependencyError( package="opentelemetry-exporter-otlp-proto-http", feature="otlp_http metric exporter", ) from exc compression = _map_compression(config.compression, Compression, allow_deflate=True) headers = _parse_headers(config.headers, config.headers_list) timeout = (config.timeout / 1000.0) if config.timeout is not None else None preferred_temporality = _map_temporality(config.temporality_preference) preferred_aggregation = _map_histogram_aggregation(config.default_histogram_aggregation) return OTLPMetricExporter( # type: ignore[return-value] endpoint=config.endpoint, headers=headers, timeout=timeout, compression=compression, # type: ignore[arg-type] preferred_temporality=preferred_temporality, preferred_aggregation=preferred_aggregation, ) def _create_otlp_grpc_metric_exporter( config: OtlpGrpcMetricExporterConfig, ) -> MetricExporter: """Create an OTLP gRPC metric exporter from config.""" try: # pylint: disable=import-outside-toplevel,no-name-in-module import grpc # noqa: PLC0415 # type: ignore[import-untyped] from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import ( # noqa: PLC0415 # type: ignore[import-untyped] OTLPMetricExporter, ) except ImportError as exc: raise MissingDependencyError( package="opentelemetry-exporter-otlp-proto-grpc", feature="otlp_grpc metric exporter", ) from exc compression = _map_compression(config.compression, grpc.Compression) headers = _parse_headers(config.headers, config.headers_list) timeout = (config.timeout / 1000.0) if config.timeout is not None else None preferred_temporality = _map_temporality(config.temporality_preference) preferred_aggregation = _map_histogram_aggregation(config.default_histogram_aggregation) return OTLPMetricExporter( # type: ignore[return-value] endpoint=config.endpoint, headers=headers, timeout=timeout, compression=compression, # type: ignore[arg-type] preferred_temporality=preferred_temporality, preferred_aggregation=preferred_aggregation, ) def _create_otlp_file_development_metric_exporter( config: ExperimentalOtlpFileMetricExporterConfig, ) -> MetricExporter: """Create an OTLP file (JSON Lines) metric exporter from config.""" try: # pylint: disable=import-outside-toplevel,no-name-in-module from opentelemetry.exporter.otlp.json.file.metric_exporter import ( # noqa: PLC0415 # type: ignore[import-untyped] FileMetricExporter, ) except ImportError as exc: raise ConfigurationError( "otlp_file_development metric exporter requires 'opentelemetry-exporter-otlp-json-file'. " "Install it with: pip install opentelemetry-exporter-otlp-json-file" ) from exc path = _parse_otlp_file_output_stream(config.output_stream) preferred_temporality = _map_temporality(config.temporality_preference) preferred_aggregation = _map_histogram_aggregation(config.default_histogram_aggregation) return ( FileMetricExporter( path, preferred_temporality=preferred_temporality, preferred_aggregation=preferred_aggregation, ) if path is not None else FileMetricExporter( preferred_temporality=preferred_temporality, preferred_aggregation=preferred_aggregation, ) ) _METRIC_EXPORTER_REGISTRY: dict = { "otlp_http": _create_otlp_http_metric_exporter, "otlp_grpc": _create_otlp_grpc_metric_exporter, "console": _create_console_metric_exporter, "otlp_file_development": _create_otlp_file_development_metric_exporter, } def _create_push_metric_exporter( config: PushMetricExporterConfig, ) -> MetricExporter: """Create a push metric exporter from config. Known exporter types are checked via typed fields on the PushMetricExporter dataclass. Unknown exporter names captured in additional_properties by the @_additional_properties decorator are loaded via the ``opentelemetry_metrics_exporter`` entry point group. """ for name, factory in _METRIC_EXPORTER_REGISTRY.items(): value = getattr(config, name, None) if value is not None: return factory(value) if config.additional_properties: name, plugin_config = next(iter(config.additional_properties.items())) return load_entry_point("opentelemetry_metrics_exporter", name)(**(plugin_config or {})) raise ConfigurationError( "No exporter type specified in push metric exporter config. " "Supported types: console, otlp_http, otlp_grpc, otlp_file_development." ) def _create_cardinality_limits(config: CardinalityLimitsConfig | None) -> CardinalityLimits | None: """Create the SDK per-instrument cardinality limits from config. Each instrument type uses its own limit and falls back to ``default``. Instrument types left out of the result use the SDK default limit. """ if config is None: return None limits: CardinalityLimits = {} for instrument_class, limit in ( (Counter, config.counter), (UpDownCounter, config.up_down_counter), (Histogram, config.histogram), (_Gauge, config.gauge), (ObservableCounter, config.observable_counter), (ObservableUpDownCounter, config.observable_up_down_counter), (ObservableGauge, config.observable_gauge), ): limit = limit if limit is not None else config.default if limit is not None: limits[instrument_class] = limit return limits def _create_periodic_metric_reader( config: PeriodicMetricReaderConfig, ) -> PeriodicExportingMetricReader: """Create a PeriodicExportingMetricReader from config. Passes explicit interval/timeout defaults to suppress env var reading. """ exporter = _create_push_metric_exporter(config.exporter) interval = config.interval if config.interval is not None else _DEFAULT_EXPORT_INTERVAL_MILLIS timeout = config.timeout if config.timeout is not None else _DEFAULT_EXPORT_TIMEOUT_MILLIS return PeriodicExportingMetricReader( exporter=exporter, export_interval_millis=float(interval), export_timeout_millis=float(timeout), cardinality_limits=_create_cardinality_limits(config.cardinality_limits), ) def _create_prometheus_metric_reader( config: PrometheusMetricExporterConfig, cardinality_limits: CardinalityLimits | None = None, ) -> MetricReader: """Create a PrometheusMetricReader from config. Dynamically imports the prometheus exporter package to avoid a hard dependency. Maps config fields to constructor parameters and starts the HTTP server. """ try: # pylint: disable=import-outside-toplevel,no-name-in-module from opentelemetry.exporter.prometheus import ( # noqa: PLC0415 # type: ignore[import-untyped] PrometheusMetricReader, start_http_server, ) except ImportError as exc: raise MissingDependencyError( package="opentelemetry-exporter-prometheus", feature="prometheus pull metric exporter", ) from exc disable_target_info = ( config.target_info_enabled_development is not None and not config.target_info_enabled_development ) if config.scope_info_enabled is not None: _logger.warning("scope_info_enabled is not yet supported for Prometheus metric exporter and will be ignored.") if config.resource_constant_labels is not None: _logger.warning( "resource_constant_labels is not yet supported for Prometheus metric exporter and will be ignored." ) reader = PrometheusMetricReader( disable_target_info=disable_target_info, cardinality_limits=cardinality_limits, ) port = config.port if config.port is not None else 9464 host = config.host if config.host is not None else "localhost" start_http_server(port=port, addr=host) return reader def _create_pull_metric_exporter( config: PullMetricExporterConfig, cardinality_limits: CardinalityLimits | None = None, ) -> MetricReader: """Create a pull metric exporter (which is itself a MetricReader) from config. Pull metric exporters like Prometheus are combined reader+exporter objects: the "exporter" IS the reader. The config schema models them as separate exporter configs, but the factory returns a MetricReader. Plugin pull exporters are loaded via the ``opentelemetry_pull_metric_exporter`` entry point group. """ if config.prometheus_development is not None: return _create_prometheus_metric_reader(config.prometheus_development, cardinality_limits) if config.additional_properties: name, plugin_config = next(iter(config.additional_properties.items())) if cardinality_limits is not None: _logger.warning( "cardinality_limits is not yet supported for pull metric exporter plugins and will be ignored." ) return load_entry_point("opentelemetry_pull_metric_exporter", name)(**(plugin_config or {})) raise ConfigurationError( "No exporter type specified in pull metric exporter config. Supported types: prometheus_development." ) def _create_pull_metric_reader( config: PullMetricReaderConfig, ) -> MetricReader: """Create a pull MetricReader from config. The pull reader's exporter is itself a MetricReader (combined reader+exporter). producers are not yet supported. """ if config.producers: _logger.warning( "MetricProducer configuration is not yet supported for pull metric readers and will be ignored." ) return _create_pull_metric_exporter(config.exporter, _create_cardinality_limits(config.cardinality_limits)) def _create_metric_reader(config: MetricReaderConfig) -> MetricReader: """Create a MetricReader from config.""" if config.periodic is not None: return _create_periodic_metric_reader(config.periodic) if config.pull is not None: return _create_pull_metric_reader(config.pull) raise ConfigurationError("No reader type specified in metric reader config. Supported types: periodic, pull.") def _create_exemplar_filter( value: ExemplarFilterConfig, ) -> object: """Create an SDK exemplar filter from config enum value.""" if value == ExemplarFilterConfig.always_on: return AlwaysOnExemplarFilter() if value == ExemplarFilterConfig.always_off: return AlwaysOffExemplarFilter() if value == ExemplarFilterConfig.trace_based: return TraceBasedExemplarFilter() raise ConfigurationError( f"Unknown exemplar filter value: {value!r}. Supported values: always_on, always_off, trace_based." ) def _to_meter_config(config: MeterConfigConfig | None) -> _MeterConfig: """Map an experimental per-meter config to an SDK ``_MeterConfig``. Only ``enabled`` is honored — it is the sole field the SDK ``_MeterConfig`` exposes. An absent ``enabled`` leaves the meter enabled. """ if config is None or config.enabled is None: return _MeterConfig.default() return _MeterConfig(is_enabled=config.enabled) def _create_meter_configurator( config: MeterConfiguratorConfig, ) -> _RuleBasedMeterConfigurator: """Build a rule-based meter configurator from experimental config. Each entry in ``meters`` maps an instrumentation-scope name glob to a per-meter config; ``default_config`` applies to scopes matching no glob. Rules are evaluated in order, so earlier entries take precedence. """ rules = [ ( _scope_name_matches_glob(matcher.name), _to_meter_config(matcher.config), ) for matcher in (config.meters or []) ] return _RuleBasedMeterConfigurator( rules=rules, default_config=_to_meter_config(config.default_config), ) def create_meter_provider( config: MeterProviderConfig | None, resource: Resource | None = None, ) -> MeterProvider: """Create an SDK MeterProvider from declarative config. Does NOT read OTEL_METRIC_EXPORT_INTERVAL, OTEL_METRICS_EXEMPLAR_FILTER, or any other env vars for values explicitly controlled by the config. Absent config values use OTel spec defaults, matching Java SDK behavior. Args: config: MeterProvider config from the parsed config file, or None. resource: Resource to attach to the provider. Returns: A configured MeterProvider. """ # Always pass an explicit exemplar filter to suppress env var reading. # Spec default is trace_based. exemplar_filter: object = TraceBasedExemplarFilter() if config is not None and config.exemplar_filter is not None: exemplar_filter = _create_exemplar_filter(config.exemplar_filter) readers: list[MetricReader] = [] views: list[View] = [] if config is not None: for reader_config in config.readers: readers.append(_create_metric_reader(reader_config)) if config.views: for view_config in config.views: views.append(_create_view(view_config)) meter_configurator = ( _create_meter_configurator(config.meter_configurator_development) if config is not None and config.meter_configurator_development is not None else None ) return MeterProvider( resource=resource, metric_readers=readers, exemplar_filter=exemplar_filter, # type: ignore[arg-type] views=views, _meter_configurator=meter_configurator, ) def configure_meter_provider( config: MeterProviderConfig | None, resource: Resource | None = None, ) -> None: """Configure the global MeterProvider from declarative config. When config is None (meter_provider section absent from config file), the global is not set — matching Java/JS SDK behavior. Args: config: MeterProvider config from the parsed config file, or None. resource: Resource to attach to the provider. """ if config is None: return metrics.set_meter_provider(create_meter_provider(config, resource)) _propagator.py000066400000000000000000000074511527070070700347350ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-configuration/src/opentelemetry/configuration# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations from opentelemetry.baggage.propagation import W3CBaggagePropagator from opentelemetry.configuration._common import load_entry_point from opentelemetry.configuration.models import ( Propagator as PropagatorConfig, ) from opentelemetry.configuration.models import ( TextMapPropagator as TextMapPropagatorConfig, ) from opentelemetry.propagate import set_global_textmap from opentelemetry.propagators.composite import CompositePropagator from opentelemetry.propagators.textmap import TextMapPropagator from opentelemetry.trace.propagation.tracecontext import ( TraceContextTextMapPropagator, ) # Propagators bundled with the SDK — no entry point lookup needed. _PROPAGATOR_REGISTRY: dict[str, type[TextMapPropagator]] = { "tracecontext": TraceContextTextMapPropagator, "baggage": W3CBaggagePropagator, } def _propagators_from_textmap_config( config: TextMapPropagatorConfig, ) -> list[TextMapPropagator]: """Resolve a TextMapPropagator config to a list of propagators. Known names (tracecontext, baggage) are bootstrapped directly via _PROPAGATOR_REGISTRY. Known schema fields not in the registry (b3, b3multi) and unknown plugin names from additional_properties are loaded via the ``opentelemetry_propagator`` entry point group. """ result: list[TextMapPropagator] = [] for name, cls in _PROPAGATOR_REGISTRY.items(): if getattr(config, name, None) is not None: result.append(cls()) # Known schema fields not in registry (b3, b3multi) — loaded via entry point for name in ("b3", "b3multi"): if getattr(config, name, None) is not None: result.append(load_entry_point("opentelemetry_propagator", name)()) # Plugin propagators from additional_properties for name, plugin_config in config.additional_properties.items(): result.append(load_entry_point("opentelemetry_propagator", name)(**(plugin_config or {}))) return result def create_propagator( config: PropagatorConfig | None, ) -> CompositePropagator: """Create a CompositePropagator from declarative config. If config is None or has no propagators defined, returns an empty CompositePropagator (no-op), ensuring "what you see is what you get" semantics — the env-var-based default propagators are not used. Args: config: Propagator config from the parsed config file, or None. Returns: A CompositePropagator wrapping all configured propagators. """ if config is None: return CompositePropagator([]) propagators: dict[type[TextMapPropagator], TextMapPropagator] = {} # Process structured composite list if config.composite: for entry in config.composite: for propagator in _propagators_from_textmap_config(entry): propagators.setdefault(type(propagator), propagator) # Process composite_list (comma-separated propagator names via entry_points) if config.composite_list: for name in config.composite_list.split(","): name = name.strip() if not name or name.lower() == "none": continue propagator = load_entry_point("opentelemetry_propagator", name)() propagators.setdefault(type(propagator), propagator) return CompositePropagator(list(propagators.values())) def configure_propagator(config: PropagatorConfig | None) -> None: """Configure the global text map propagator from declarative config. Always calls set_global_textmap to override any defaults (including the env-var-based tracecontext+baggage default set by the SDK). Args: config: Propagator config from the parsed config file, or None. """ set_global_textmap(create_propagator(config)) python-opentelemetry-1.45.2/opentelemetry-configuration/src/opentelemetry/configuration/_resource.py000066400000000000000000000202641527070070700344620ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations import dataclasses import fnmatch import logging import os import sys from collections.abc import Callable from typing import Any from urllib import parse from opentelemetry.configuration._common import load_entry_point from opentelemetry.configuration.models import ( AttributeNameValue, AttributeType, ExperimentalResourceDetector, IncludeExclude, ) from opentelemetry.configuration.models import Resource as ResourceConfig from opentelemetry.sdk.environment_variables import OTEL_SERVICE_NAME from opentelemetry.sdk.resources import ( _DEFAULT_RESOURCE, SERVICE_NAME, ProcessResourceDetector, Resource, ServiceInstanceIdResourceDetector, _HostResourceDetector, ) from opentelemetry.util.types import AnyValue _logger = logging.getLogger(__name__) def _coerce_bool(value: object) -> bool: if isinstance(value, str): return value.lower() not in ("false", "0", "") return bool(value) def _array(coerce: Callable) -> Callable: return lambda value: [coerce(item) for item in value] # Dispatch table mapping AttributeType to its coercion callable _COERCIONS = { AttributeType.string: str, AttributeType.int: int, AttributeType.double: float, AttributeType.bool: _coerce_bool, AttributeType.string_array: _array(str), AttributeType.int_array: _array(int), AttributeType.double_array: _array(float), AttributeType.bool_array: _array(_coerce_bool), } def _coerce_attribute_value(attr: AttributeNameValue) -> object: """Coerce an attribute value to the correct Python type based on AttributeType.""" coerce = _COERCIONS.get(attr.type) # type: ignore[arg-type] return coerce(attr.value) if coerce is not None else attr.value # type: ignore[operator] def _parse_attributes_list(attributes_list: str) -> dict[str, str]: """Parse a comma-separated key=value string into a dict. Format is the same as OTEL_RESOURCE_ATTRIBUTES: key=value,key=value Values are always strings (no type coercion). """ result: dict[str, str] = {} for item in attributes_list.split(","): item = item.strip() if not item: continue if "=" not in item: _logger.warning( "Invalid resource attribute pair in attributes_list: %s", item, ) continue key, value = item.split("=", maxsplit=1) result[key.strip()] = parse.unquote(value.strip()) return result def create_resource(config: ResourceConfig | None) -> Resource: """Create an SDK Resource from declarative config. Does NOT read OTEL_RESOURCE_ATTRIBUTES. Resource detectors are only run when explicitly listed under detection_development.detectors in the config. Starts from SDK telemetry defaults (telemetry.sdk.*), merges any detected attributes, then merges explicit config attributes on top (highest priority). Args: config: Resource config from the parsed config file, or None. Returns: A Resource with SDK defaults, optional detector attributes, and any config-specified attributes merged in priority order. """ # Spec requires service.name to always be present; detectors and explicit # config attributes can override this default. executable_name = os.path.basename(sys.executable) if sys.executable else None default_service_name = f"unknown_service:{executable_name}" if executable_name else "unknown_service" base = _DEFAULT_RESOURCE.merge(Resource({SERVICE_NAME: default_service_name})) if config is None: return base # attributes_list is lower priority; explicit attributes overwrite conflicts. config_attrs: dict[str, object] = {} if config.attributes_list: config_attrs.update(_parse_attributes_list(config.attributes_list)) if config.attributes: for attr in config.attributes: # An unset ${VAR} with no default substitutes an empty value that # the YAML parser reads as null. Skip such attributes rather than # inserting a None (or coercing it into garbage like "None"/0). if attr.value is None: _logger.warning( "Ignoring resource attribute '%s' with empty value", attr.name, ) continue config_attrs[attr.name] = _coerce_attribute_value(attr) schema_url = config.schema_url # Run detectors only if detection_development is configured. Collect all # detected attributes, apply the include/exclude filter, then merge before # config attributes so explicit values always win. result = base if config.detection_development: detected_attrs: dict[str, object] = {} if config.detection_development.detectors: for detector_config in config.detection_development.detectors: _run_detectors(detector_config, detected_attrs) filtered = _filter_attributes(detected_attrs, config.detection_development.attributes) if filtered: result = result.merge(Resource(filtered)) # type: ignore[arg-type] config_resource = Resource(config_attrs, schema_url) # type: ignore[arg-type] return result.merge(config_resource) def _detect_service(_config: Any) -> dict[str, AnyValue]: """Service detector: generates instance ID and reads OTEL_SERVICE_NAME.""" attrs: dict[str, AnyValue] = dict(ServiceInstanceIdResourceDetector().detect().attributes) if service_name := os.environ.get(OTEL_SERVICE_NAME): attrs[SERVICE_NAME] = service_name return attrs _RESOURCE_DETECTOR_REGISTRY: dict[str, Callable[[Any], dict[str, AnyValue]]] = { "service": _detect_service, "host": lambda _: dict(_HostResourceDetector().detect().attributes), "process": lambda _: dict(ProcessResourceDetector().detect().attributes), } def _run_detectors( detector_config: ExperimentalResourceDetector, detected_attrs: dict[str, object], ) -> None: """Run detectors present in a single detector config entry. Known detectors (service, host, process) are handled directly via _RESOURCE_DETECTOR_REGISTRY. All other detectors — including known schema fields like container that require contrib packages, and unknown plugin detectors captured in additional_properties — are loaded via the ``opentelemetry_resource_detector`` entry point group. The detected_attrs dict is updated in-place; later detectors overwrite earlier ones for the same key. """ for name in dataclasses.fields(detector_config): value = getattr(detector_config, name.name, None) if value is None: continue if name.name in _RESOURCE_DETECTOR_REGISTRY: detected_attrs.update(_RESOURCE_DETECTOR_REGISTRY[name.name](value)) else: cls = load_entry_point("opentelemetry_resource_detector", name.name) detected_attrs.update(cls(**(value or {})).detect().attributes) for name, plugin_config in detector_config.additional_properties.items(): cls = load_entry_point("opentelemetry_resource_detector", name) detected_attrs.update(cls(**(plugin_config or {})).detect().attributes) def _filter_attributes(attrs: dict[str, object], filter_config: IncludeExclude | None) -> dict[str, object]: """Filter detected attribute keys using include/exclude glob patterns. Mirrors other SDK IncludeExcludePredicate.createPatternMatching behaviour: - No filter config (attributes absent) → include all detected attributes. - included patterns are checked first; excluded patterns are applied after. - An empty included list is treated as "include everything". """ if filter_config is None: return attrs included = filter_config.included excluded = filter_config.excluded if not included and not excluded: return attrs result: dict[str, object] = {} for key, value in attrs.items(): if included and not any(fnmatch.fnmatch(key, pat) for pat in included): continue if excluded and any(fnmatch.fnmatch(key, pat) for pat in excluded): continue result[key] = value return result python-opentelemetry-1.45.2/opentelemetry-configuration/src/opentelemetry/configuration/_sdk.py000066400000000000000000000077541527070070700334250ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 """Top-level orchestrator for declarative SDK configuration. Takes a parsed ``OpenTelemetryConfiguration`` and applies it by calling each per-signal ``configure_*`` factory in order. This is the single entry point for "apply this config" on the declarative path. """ from __future__ import annotations from logging import CRITICAL, DEBUG, ERROR, INFO, WARNING, getLogger from opentelemetry.configuration._logger_provider import ( configure_logger_provider, ) from opentelemetry.configuration._meter_provider import ( configure_meter_provider, ) from opentelemetry.configuration._propagator import configure_propagator from opentelemetry.configuration._resource import create_resource from opentelemetry.configuration._tracer_provider import ( configure_tracer_provider, ) from opentelemetry.configuration.instrumentation import ( configure_instrumentation, ) from opentelemetry.configuration.models import ( OpenTelemetryConfiguration, SeverityNumber, ) _logger = getLogger(__name__) # Maps OTel SeverityNumber groups to Python logging levels. # The numbered variants (debug2, info3, …) are sub-levels within the same # Python tier, so they collapse to the same level constant. _SEVERITY_TO_LOGGING_LEVEL: dict[SeverityNumber, int] = { SeverityNumber.trace: DEBUG, SeverityNumber.trace2: DEBUG, SeverityNumber.trace3: DEBUG, SeverityNumber.trace4: DEBUG, SeverityNumber.debug: DEBUG, SeverityNumber.debug2: DEBUG, SeverityNumber.debug3: DEBUG, SeverityNumber.debug4: DEBUG, SeverityNumber.info: INFO, SeverityNumber.info2: INFO, SeverityNumber.info3: INFO, SeverityNumber.info4: INFO, SeverityNumber.warn: WARNING, SeverityNumber.warn2: WARNING, SeverityNumber.warn3: WARNING, SeverityNumber.warn4: WARNING, SeverityNumber.error: ERROR, SeverityNumber.error2: ERROR, SeverityNumber.error3: ERROR, SeverityNumber.error4: ERROR, SeverityNumber.fatal: CRITICAL, SeverityNumber.fatal2: CRITICAL, SeverityNumber.fatal3: CRITICAL, SeverityNumber.fatal4: CRITICAL, } def configure_sdk(config: OpenTelemetryConfiguration) -> None: """Configure the global SDK from a parsed declarative configuration. Builds a :class:`Resource` from ``config.resource`` and applies it to each signal provider. Sets the global tracer provider, meter provider, logger provider, and text map propagator from their respective config sections. Sections absent from the config (``None``) leave the corresponding global untouched — matching the spec's "noop default" behavior. Honors the top-level ``disabled`` flag: when true, the function returns early without setting any globals. The ``log_level`` field, when present and not disabled, is applied to the internal ``opentelemetry`` logger to control the verbosity of SDK self-diagnostics. Args: config: Parsed ``OpenTelemetryConfiguration`` (typically from ``load_config_file``). Example: >>> from opentelemetry.configuration.file import ( ... load_config_file, ... configure_sdk, ... ) >>> config = load_config_file("otel-config.yaml") >>> configure_sdk(config) """ if config.disabled: _logger.warning("Declarative configuration has disabled=true; skipping SDK setup.") return if config.log_level is not None: level = _SEVERITY_TO_LOGGING_LEVEL.get(config.log_level, INFO) getLogger("opentelemetry").setLevel(level) global_attribute_limits = config.attribute_limits resource = create_resource(config.resource) configure_tracer_provider(config.tracer_provider, resource, global_attribute_limits) configure_meter_provider(config.meter_provider, resource) configure_logger_provider(config.logger_provider, resource, global_attribute_limits) configure_propagator(config.propagator) configure_instrumentation(config.instrumentation_development) _tracer_provider.py000066400000000000000000000470731527070070700357550ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-configuration/src/opentelemetry/configuration# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations import logging from opentelemetry import trace from opentelemetry.configuration._common import ( _map_compression, _parse_headers, _parse_otlp_file_output_stream, load_entry_point, ) from opentelemetry.configuration._exceptions import ( ConfigurationError, MissingDependencyError, ) from opentelemetry.configuration.models import ( AttributeLimits, ) from opentelemetry.configuration.models import ( ExperimentalComposableRuleBasedSampler as RuleBasedSamplerConfig, ) from opentelemetry.configuration.models import ( ExperimentalComposableRuleBasedSamplerRule as RuleBasedSamplerRuleConfig, ) from opentelemetry.configuration.models import ( ExperimentalComposableSampler as ComposableSamplerConfig, ) from opentelemetry.configuration.models import ( ExperimentalOtlpFileExporter as ExperimentalOtlpFileExporterConfig, ) from opentelemetry.configuration.models import ( ExperimentalTracerConfig as TracerConfigConfig, ) from opentelemetry.configuration.models import ( ExperimentalTracerConfigurator as TracerConfiguratorConfig, ) from opentelemetry.configuration.models import ( IdGenerator as IdGeneratorConfig, ) from opentelemetry.configuration.models import ( OtlpGrpcExporter as OtlpGrpcExporterConfig, ) from opentelemetry.configuration.models import ( OtlpHttpExporter as OtlpHttpExporterConfig, ) from opentelemetry.configuration.models import ( ParentBasedSampler as ParentBasedSamplerConfig, ) from opentelemetry.configuration.models import ( Sampler as SamplerConfig, ) from opentelemetry.configuration.models import ( SpanExporter as SpanExporterConfig, ) from opentelemetry.configuration.models import ( SpanLimits as SpanLimitsConfig, ) from opentelemetry.configuration.models import ( SpanProcessor as SpanProcessorConfig, ) from opentelemetry.configuration.models import ( TracerProvider as TracerProviderConfig, ) from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.trace import ( _DEFAULT_OTEL_EVENT_ATTRIBUTE_COUNT_LIMIT, _DEFAULT_OTEL_LINK_ATTRIBUTE_COUNT_LIMIT, _DEFAULT_OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT, _DEFAULT_OTEL_SPAN_EVENT_COUNT_LIMIT, _DEFAULT_OTEL_SPAN_LINK_COUNT_LIMIT, SpanLimits, TracerProvider, _RuleBasedTracerConfigurator, _TracerConfig, ) from opentelemetry.sdk.trace._sampling_experimental import ( ComposableSampler, composable_always_off, composable_always_on, composable_parent_threshold, composable_rule_based, composable_traceid_ratio_based, composite_sampler, ) from opentelemetry.sdk.trace._sampling_experimental._rule_based import ( AllPredicate, AlwaysMatchPredicate, AttributePatternsPredicate, AttributeValuesPredicate, ParentPredicate, PredicateT, RulesT, SpanKindPredicate, ) from opentelemetry.sdk.trace.export import ( BatchSpanProcessor, ConsoleSpanExporter, SimpleSpanProcessor, SpanExporter, ) from opentelemetry.sdk.trace.id_generator import IdGenerator, RandomIdGenerator from opentelemetry.sdk.trace.sampling import ( ALWAYS_OFF, ALWAYS_ON, ParentBased, Sampler, TraceIdRatioBased, ) from opentelemetry.sdk.util.instrumentation import _scope_name_matches_glob from opentelemetry.trace import SpanKind as TraceSpanKind _logger = logging.getLogger(__name__) # Default sampler per the OTel spec: parent_based with always_on root. _DEFAULT_SAMPLER = ParentBased(root=ALWAYS_ON) def _create_otlp_http_span_exporter( config: OtlpHttpExporterConfig, ) -> SpanExporter: """Create an OTLP HTTP span exporter from config.""" try: # pylint: disable=import-outside-toplevel,no-name-in-module from opentelemetry.exporter.otlp.proto.http import ( # noqa: PLC0415 # type: ignore[import-untyped] Compression, ) from opentelemetry.exporter.otlp.proto.http.trace_exporter import ( # noqa: PLC0415 # type: ignore[import-untyped] OTLPSpanExporter, ) except ImportError as exc: raise MissingDependencyError( package="opentelemetry-exporter-otlp-proto-http", feature="otlp_http span exporter", ) from exc compression = _map_compression(config.compression, Compression, allow_deflate=True) headers = _parse_headers(config.headers, config.headers_list) timeout = (config.timeout / 1000.0) if config.timeout is not None else None return OTLPSpanExporter( # type: ignore[return-value] endpoint=config.endpoint, headers=headers, timeout=timeout, compression=compression, # type: ignore[arg-type] ) def _create_otlp_grpc_span_exporter( config: OtlpGrpcExporterConfig, ) -> SpanExporter: """Create an OTLP gRPC span exporter from config.""" try: # pylint: disable=import-outside-toplevel,no-name-in-module import grpc # noqa: PLC0415 # type: ignore[import-untyped] from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import ( # noqa: PLC0415 # type: ignore[import-untyped] OTLPSpanExporter, ) except ImportError as exc: raise MissingDependencyError( package="opentelemetry-exporter-otlp-proto-grpc", feature="otlp_grpc span exporter", ) from exc compression = _map_compression(config.compression, grpc.Compression) headers = _parse_headers(config.headers, config.headers_list) timeout = (config.timeout / 1000.0) if config.timeout is not None else None return OTLPSpanExporter( # type: ignore[return-value] endpoint=config.endpoint, headers=headers, timeout=timeout, compression=compression, # type: ignore[arg-type] ) def _create_otlp_file_development_span_exporter( config: ExperimentalOtlpFileExporterConfig, ) -> SpanExporter: """Create an OTLP file (JSON Lines) span exporter from config.""" try: # pylint: disable=import-outside-toplevel,no-name-in-module from opentelemetry.exporter.otlp.json.file.trace_exporter import ( # noqa: PLC0415 # type: ignore[import-untyped] FileSpanExporter, ) except ImportError as exc: raise ConfigurationError( "otlp_file_development span exporter requires 'opentelemetry-exporter-otlp-json-file'. " "Install it with: pip install opentelemetry-exporter-otlp-json-file" ) from exc path = _parse_otlp_file_output_stream(config.output_stream) return FileSpanExporter(path) if path is not None else FileSpanExporter() _SPAN_EXPORTER_REGISTRY: dict = { "otlp_http": _create_otlp_http_span_exporter, "otlp_grpc": _create_otlp_grpc_span_exporter, "console": lambda _: ConsoleSpanExporter(), "otlp_file_development": _create_otlp_file_development_span_exporter, } def _create_span_exporter(config: SpanExporterConfig) -> SpanExporter: """Create a span exporter from config. Known exporter types are checked via typed fields on the SpanExporter dataclass. Unknown exporter names captured in additional_properties by the @_additional_properties decorator are loaded via the ``opentelemetry_traces_exporter`` entry point group. """ for name, factory in _SPAN_EXPORTER_REGISTRY.items(): value = getattr(config, name, None) if value is not None: return factory(value) if config.additional_properties: name, plugin_config = next(iter(config.additional_properties.items())) return load_entry_point("opentelemetry_traces_exporter", name)(**(plugin_config or {})) raise ConfigurationError( "No exporter type specified in span exporter config. " "Supported types: otlp_http, otlp_grpc, console, otlp_file_development." ) def _create_span_processor( config: SpanProcessorConfig, ) -> BatchSpanProcessor | SimpleSpanProcessor: """Create a span processor from config.""" if config.batch is not None: exporter = _create_span_exporter(config.batch.exporter) return BatchSpanProcessor( exporter, max_queue_size=config.batch.max_queue_size, schedule_delay_millis=config.batch.schedule_delay, max_export_batch_size=config.batch.max_export_batch_size, export_timeout_millis=config.batch.export_timeout, ) if config.simple is not None: return SimpleSpanProcessor(_create_span_exporter(config.simple.exporter)) raise ConfigurationError("No processor type specified in span processor config. Supported types: batch, simple.") def _create_experimental_composable_sampler( config: ComposableSamplerConfig, ) -> ComposableSampler: """Create an experimental composable sampler from config""" if config.always_on is not None: return composable_always_on() if config.always_off is not None: return composable_always_off() if config.parent_threshold is not None: return composable_parent_threshold(_create_experimental_composable_sampler(config.parent_threshold.root)) if config.probability is not None: ratio = config.probability.ratio return composable_traceid_ratio_based(ratio if ratio is not None else 1.0) if config.rule_based is not None: return composable_rule_based(_create_rule_based_sampler_rules(config.rule_based)) raise ConfigurationError( f"Unknown or unsupported experimental composable sampler type in config: {config!r}. " "Supported types: always_on, always_off, parent_threshold, probability, rule_based." ) def _create_rule_based_sampler_rules( config: RuleBasedSamplerConfig, ) -> RulesT: if config.rules is None: return [] return [ ( _create_rule_based_sampler_rule_predicate(rule), _create_experimental_composable_sampler(rule.sampler), ) for rule in config.rules ] def _create_rule_based_sampler_rule_predicate( config: RuleBasedSamplerRuleConfig, ) -> PredicateT: predicates: list[PredicateT] = [] if config.attribute_values is not None: predicates.append( AttributeValuesPredicate( config.attribute_values.key, config.attribute_values.values, ) ) if config.attribute_patterns is not None: predicates.append( AttributePatternsPredicate( config.attribute_patterns.key, config.attribute_patterns.included, config.attribute_patterns.excluded, ) ) if config.span_kinds is not None: predicates.append( SpanKindPredicate([TraceSpanKind[span_kind.value.upper()] for span_kind in config.span_kinds]) ) if config.parent is not None: predicates.append(ParentPredicate([parent.value for parent in config.parent])) if not predicates: return AlwaysMatchPredicate() if len(predicates) == 1: return predicates[0] return AllPredicate(predicates) def _create_sampler(config: SamplerConfig) -> Sampler: """Create a sampler from config. Built-in sampler types are checked via typed fields on the Sampler dataclass. User-defined sampler names captured in additional_properties by the @_additional_properties decorator are loaded via the ``opentelemetry_sampler`` entry point group. """ if config.always_on is not None: return ALWAYS_ON if config.always_off is not None: return ALWAYS_OFF if config.trace_id_ratio_based is not None: ratio = config.trace_id_ratio_based.ratio return TraceIdRatioBased(ratio if ratio is not None else 1.0) if config.composite_development is not None: return composite_sampler(_create_experimental_composable_sampler(config.composite_development)) if config.parent_based is not None: return _create_parent_based_sampler(config.parent_based) if config.additional_properties: name = next(iter(config.additional_properties)) return load_entry_point("opentelemetry_sampler", name)() raise ConfigurationError( f"Unsupported sampler type in config: {config!r}. " "Supported types: always_on, always_off, composite_development, " "trace_id_ratio_based, parent_based." ) def _create_id_generator(config: IdGeneratorConfig) -> IdGenerator: """Create an IdGenerator from config. Built-in ``random`` resolves to ``RandomIdGenerator``; unknown names load from the ``opentelemetry_id_generator`` entry point group (the same group ``OTEL_PYTHON_ID_GENERATOR`` uses today). """ if config.random is not None: return RandomIdGenerator() if config.additional_properties: name, plugin_config = next(iter(config.additional_properties.items())) return load_entry_point("opentelemetry_id_generator", name)(**(plugin_config or {})) raise ConfigurationError("No id_generator type specified in config. Supported built-in types: random.") def _create_parent_based_sampler(config: ParentBasedSamplerConfig) -> Sampler: """Create a ParentBased sampler from config, applying SDK defaults for absent delegates.""" root = _create_sampler(config.root) if config.root is not None else ALWAYS_ON kwargs: dict = {"root": root} if config.remote_parent_sampled is not None: kwargs["remote_parent_sampled"] = _create_sampler(config.remote_parent_sampled) if config.remote_parent_not_sampled is not None: kwargs["remote_parent_not_sampled"] = _create_sampler(config.remote_parent_not_sampled) if config.local_parent_sampled is not None: kwargs["local_parent_sampled"] = _create_sampler(config.local_parent_sampled) if config.local_parent_not_sampled is not None: kwargs["local_parent_not_sampled"] = _create_sampler(config.local_parent_not_sampled) return ParentBased(**kwargs) def _create_span_limits( config: SpanLimitsConfig, global_limits: AttributeLimits | None = None, ) -> SpanLimits: """Create SpanLimits from config. Absent fields fall back to global_limits (if provided), then to OTel spec defaults (128 for counts, unlimited for lengths). Explicit values suppress env-var reading — matching Java SDK behavior. """ attribute_count_limit = config.attribute_count_limit if attribute_count_limit is None and global_limits is not None: attribute_count_limit = global_limits.attribute_count_limit attribute_value_length_limit = config.attribute_value_length_limit if attribute_value_length_limit is None and global_limits is not None: attribute_value_length_limit = global_limits.attribute_value_length_limit return SpanLimits( max_span_attributes=( attribute_count_limit if attribute_count_limit is not None else _DEFAULT_OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT ), max_events=( config.event_count_limit if config.event_count_limit is not None else _DEFAULT_OTEL_SPAN_EVENT_COUNT_LIMIT ), max_links=( config.link_count_limit if config.link_count_limit is not None else _DEFAULT_OTEL_SPAN_LINK_COUNT_LIMIT ), max_event_attributes=( config.event_attribute_count_limit if config.event_attribute_count_limit is not None else _DEFAULT_OTEL_EVENT_ATTRIBUTE_COUNT_LIMIT ), max_link_attributes=( config.link_attribute_count_limit if config.link_attribute_count_limit is not None else _DEFAULT_OTEL_LINK_ATTRIBUTE_COUNT_LIMIT ), max_attribute_length=( attribute_value_length_limit if attribute_value_length_limit is not None else SpanLimits.UNSET ), max_span_attribute_length=( attribute_value_length_limit if attribute_value_length_limit is not None else SpanLimits.UNSET ), ) def _to_tracer_config(config: TracerConfigConfig | None) -> _TracerConfig: """Map an experimental per-tracer config to an SDK ``_TracerConfig``. Only ``enabled`` is honored — it is the sole field the SDK ``_TracerConfig`` exposes. An absent ``enabled`` leaves the tracer enabled. """ if config is None or config.enabled is None: return _TracerConfig.default() return _TracerConfig(is_enabled=config.enabled) def _create_tracer_configurator( config: TracerConfiguratorConfig, ) -> _RuleBasedTracerConfigurator: """Build a rule-based tracer configurator from experimental config. Each entry in ``tracers`` maps an instrumentation-scope name glob to a per-tracer config; ``default_config`` applies to scopes matching no glob. Rules are evaluated in order, so earlier entries take precedence. """ rules = [ ( _scope_name_matches_glob(matcher.name), _to_tracer_config(matcher.config), ) for matcher in (config.tracers or []) ] return _RuleBasedTracerConfigurator( rules=rules, default_config=_to_tracer_config(config.default_config), ) def create_tracer_provider( config: TracerProviderConfig | None, resource: Resource | None = None, global_attribute_limits: AttributeLimits | None = None, ) -> TracerProvider: """Create an SDK TracerProvider from declarative config. Does NOT read OTEL_TRACES_SAMPLER, OTEL_SPAN_*_LIMIT, or any other env vars for values that are explicitly controlled by the config. Absent config values use OTel spec defaults (not env vars), matching Java SDK behavior. Args: config: TracerProvider config from the parsed config file, or None. resource: Resource to attach to the provider. global_attribute_limits: Top-level attribute_limits from the root config, used as a fallback when per-signal limits are not specified. Returns: A configured TracerProvider. """ sampler = _create_sampler(config.sampler) if config is not None and config.sampler is not None else _DEFAULT_SAMPLER id_generator = ( _create_id_generator(config.id_generator) if config is not None and config.id_generator is not None else None ) if config is not None and config.limits is not None: limits = config.limits else: limits = SpanLimitsConfig() span_limits = _create_span_limits(limits, global_attribute_limits) tracer_configurator = ( _create_tracer_configurator(config.tracer_configurator_development) if config is not None and config.tracer_configurator_development is not None else None ) provider = TracerProvider( resource=resource, sampler=sampler, span_limits=span_limits, id_generator=id_generator, _tracer_configurator=tracer_configurator, ) if config is not None: for proc_config in config.processors: provider.add_span_processor(_create_span_processor(proc_config)) return provider def configure_tracer_provider( config: TracerProviderConfig | None, resource: Resource | None = None, global_attribute_limits: AttributeLimits | None = None, ) -> None: """Configure the global TracerProvider from declarative config. When config is None (tracer_provider section absent from config file), the global is not set — matching Java/JS SDK behavior and the spec's "a noop tracer provider is used" default. Args: config: TracerProvider config from the parsed config file, or None. resource: Resource to attach to the provider. global_attribute_limits: Top-level attribute_limits from the root config, used as a fallback when per-signal limits are not specified. """ if config is None: return trace.set_tracer_provider(create_tracer_provider(config, resource, global_attribute_limits)) python-opentelemetry-1.45.2/opentelemetry-configuration/src/opentelemetry/configuration/file/000077500000000000000000000000001527070070700330355ustar00rootroot00000000000000__init__.py000066400000000000000000000032261527070070700350720ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-configuration/src/opentelemetry/configuration/file# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 """OpenTelemetry SDK File Configuration. This module provides support for configuring the OpenTelemetry SDK using declarative configuration files (YAML or JSON). Example: >>> from opentelemetry.configuration.file import load_config_file >>> config = load_config_file("otel-config.yaml") >>> print(config.file_format) '1.0' """ from opentelemetry.configuration._exceptions import ( ConfigurationError, MissingDependencyError, ) from opentelemetry.configuration._logger_provider import ( configure_logger_provider, create_logger_provider, ) from opentelemetry.configuration._meter_provider import ( configure_meter_provider, create_meter_provider, ) from opentelemetry.configuration._propagator import ( configure_propagator, create_propagator, ) from opentelemetry.configuration._resource import create_resource from opentelemetry.configuration._sdk import configure_sdk from opentelemetry.configuration._tracer_provider import ( configure_tracer_provider, create_tracer_provider, ) from opentelemetry.configuration.file._env_substitution import ( substitute_env_vars, ) from opentelemetry.configuration.file._loader import load_config_file __all__ = [ "ConfigurationError", "MissingDependencyError", "configure_logger_provider", "configure_meter_provider", "configure_propagator", "configure_sdk", "configure_tracer_provider", "create_logger_provider", "create_meter_provider", "create_propagator", "create_resource", "create_tracer_provider", "load_config_file", "substitute_env_vars", ] _env_substitution.py000066400000000000000000000061301527070070700371130ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-configuration/src/opentelemetry/configuration/file# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 """Environment variable substitution for configuration files.""" import os import re def substitute_env_vars(text: str) -> str: """Substitute environment variables within a configuration value. A configuration value is a single value from the parsed configuration file (the value of one key or one list item), never a key, a comment, or a whole mapping/list. Substitution is applied per configuration value after the file has been parsed, so comments and mapping keys are never touched. For example, given the YAML:: service_name: ${SERVICE_NAME} endpoint: http://${HOST}:${PORT} the configuration values are ``${SERVICE_NAME}`` and ``http://${HOST}:${PORT}``; this function is called once with each, and never with the keys ``service_name`` or ``endpoint``. Supports the following syntax: - ${VAR}: Substitute with environment variable VAR, or an empty value if VAR is not set. - ${VAR:-default}: Substitute with VAR if set, otherwise use default value. - $$: Escape sequence for literal $. Per the declarative configuration specification, a referenced environment variable that is not set and has no default is replaced with an empty value (which the YAML parser then interprets as null). This matches the behavior of the Java and Node.js implementations and lets configuration files be shared across languages. Args: text: A configuration value with potential ${VAR} placeholders. Returns: The configuration value with environment variables substituted. Examples: >>> os.environ["SERVICE_NAME"] = "my-service" >>> substitute_env_vars("${SERVICE_NAME}") 'my-service' >>> substitute_env_vars("${MISSING:-default}") 'default' >>> substitute_env_vars("$$100") '$100' """ # Pattern matches $$ (escape sequence) or ${VAR_NAME} / ${VAR_NAME:-default_value} # Handling both in a single pass ensures $$ followed by ${VAR} works correctly pattern = r"\$\$|\$\{([A-Za-z_][A-Za-z0-9_]*)(:-([^}]*))?\}" def replace_var(match) -> str: if match.group(1) is None: # Matched $$, return literal $ return "$" var_name = match.group(1) has_default = match.group(2) is not None default_value = match.group(3) if has_default else None value = os.environ.get(var_name) if value is None: # An unset variable is replaced with its default if one is # provided, otherwise with an empty value, per the spec. return default_value or "" # Substitution runs on an already-parsed configuration value, so the # result cannot inject new YAML structure regardless of its contents # (a newline in the value stays a literal character within this one # value). Type interpretation for standalone references is handled by # the loader, which re-resolves the node tag after substitution. return value return re.sub(pattern, replace_var, text) _loader.py000066400000000000000000000367361527070070700347540ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-configuration/src/opentelemetry/configuration/file# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 """Configuration file loading and parsing.""" import importlib.resources import json import logging import os import re from pathlib import Path from typing import Any from opentelemetry.configuration._conversion import _dict_to_dataclass from opentelemetry.configuration._exceptions import ( ConfigurationError, MissingDependencyError, ) from opentelemetry.configuration.file._env_substitution import ( substitute_env_vars, ) from opentelemetry.configuration.models import OpenTelemetryConfiguration try: import yaml except ImportError as exc: raise MissingDependencyError( package="pyyaml", feature="File configuration", install_name="opentelemetry-sdk", extras="file-configuration", ) from exc try: import jsonschema except ImportError as exc: raise MissingDependencyError( package="jsonschema", feature="File configuration", install_name="opentelemetry-sdk", extras="file-configuration", ) from exc # Schema version vendored in schema.json. ``file_format`` values are accepted # per the configuration spec's versioning rules: the major version must match, # and a minor version newer than the one this SDK targets is accepted with a # warning. See # https://github.com/open-telemetry/opentelemetry-configuration/blob/main/VERSIONING.md _SUPPORTED_SCHEMA_MAJOR = 1 _SUPPORTED_SCHEMA_MINOR = 1 _schema_cache: list[dict] = [] def _get_schema() -> dict: if not _schema_cache: schema_path = importlib.resources.files("opentelemetry.configuration") / "schema.json" _schema_cache.append(json.loads(schema_path.read_text(encoding="utf-8"))) return _schema_cache[0] _logger = logging.getLogger(__name__) _YAML_STR_TAG = "tag:yaml.org,2002:str" # A configuration value whose entire content is a single ``${VAR}`` / # ``${VAR:-default}`` reference. Only such standalone references (when # unquoted) have their type re-interpreted after substitution; embedded or # multiple references resolve to a string per the configuration spec. A # leading ``$$`` escape does not match, so ``$${VAR}`` is treated as an # embedded (string) value. _STANDALONE_ENV_REF = re.compile(r"\A\$\{[A-Za-z_][A-Za-z0-9_]*(?::-[^}]*)?\}\Z") def _substitute_env_in_yaml_node( node: yaml.Node, loader: yaml.SafeLoader, visited: set[int] | None = None, ): """Apply env-var substitution to string configuration values in a node tree. Substitution runs after parsing on configuration values only, so comments and mapping keys are never candidates (per the configuration spec). For an unquoted standalone ``${VAR}`` reference the node's type tag is re-resolved from the substituted value so YAML type coercion still applies (e.g. ``${LIMIT}`` -> int); quoted or embedded references stay strings. Args: node: Root of the node tree to walk. Substitution happens in place. loader: The loader that composed ``node``; used to re-resolve tags. visited: Ids of nodes already processed in this traversal. Callers pass nothing; the walker creates and threads one set through the whole tree. """ # Every anchor/alias pair shares one composed node, so the same node is # reached once per reference to it. Each node must be substituted exactly # once for two reasons: # # 1. Substituting twice re-reads the output of the first pass. An # anchored ``$${TOKEN}`` becomes the escaped literal ``${TOKEN}`` on # the first visit, which the second visit then resolves as a real # variable -- silently defeating the ``$$`` escape. # 2. A cyclic alias (``a: &anchor {self: *anchor}``) makes the node tree # a graph with a loop, so an unguarded walk recurses until it raises # ``RecursionError``. # # Tracking ``id(node)`` is safe here because ``root_node`` keeps every node # in the tree alive for the whole traversal, so no id can be reused. if visited is None: visited = set() if id(node) in visited: return visited.add(id(node)) # Worked example: a node value that makes all three checks below true. # # YAML (with environment LIMIT=100): # attribute_limits: # attribute_count_limit: ${LIMIT} # # The parser hands this function the value node of attribute_count_limit, # which has value="${LIMIT}", tag=str, style=None (plain/unquoted). Then: # # 1. isinstance(node, yaml.ScalarNode) -> True: it is a leaf value. # 2. node.tag == _YAML_STR_TAG -> True: "${LIMIT}" is not a number or # bool, so the parser tagged it str. We substitute in place, giving # node.value = "100". # 3. node.style is None and _STANDALONE_ENV_REF.match(raw) -> True: # it was unquoted and the whole value is a single reference. We # re-resolve the tag: resolve("100") -> int, and construct_document() # later builds the integer 100 (not the string "100"). # # Counter-cases that reach check 3 but stop there: "${LIMIT}" quoted fails # the style test; x${LIMIT} fails the regex. Both keep the str tag. if isinstance(node, yaml.ScalarNode): # A ``ScalarNode`` is a leaf -- one configuration value. Only a # ``str``-tagged one can hold a ``${VAR}`` reference (nodes the parser # already typed as int, float, bool or null cannot), so those are the # only substitution candidates. if node.tag == _YAML_STR_TAG: # ``node.value`` is the raw text of this one value (never a key or # comment). Replace any ${VAR} / ${VAR:-default} / $$ in it. raw = node.value node.value = substitute_env_vars(raw) # The spec says node types are interpreted *after* substitution, # but only for a value that is exactly one reference and written # unquoted (``style is None``). Example: ``count: ${N}`` with # ``N=42`` must yield the int 42, not the string "42". In that case # re-run YAML's implicit type resolution on the substituted text # and retag the node. A quoted (``"${N}"``) or embedded # (``foo-${N}``) value keeps its ``str`` tag and stays a string. if node.style is None and _STANDALONE_ENV_REF.match(raw): # ``resolve`` returns the implicit tag (int/float/bool/null/ # str) YAML would assign to ``node.value``; the ``(True, False)`` # implicit-flag pair marks it as a plain scalar so that # type-guessing applies, exactly as during normal parsing. node.tag = loader.resolve(yaml.ScalarNode, node.value, (True, False)) elif isinstance(node, yaml.SequenceNode): # A sequence (list): each item is itself a value node -- recurse. for item in node.value: _substitute_env_in_yaml_node(item, loader, visited) elif isinstance(node, yaml.MappingNode): # A mapping (dict): recurse into values only; keys are not # substitution candidates per the spec. A merge key (``<<: *base``) # appears here as an ordinary pair whose value node is the anchored # mapping itself, so ``visited`` also covers the merged-in values. for _key_node, value_node in node.value: _substitute_env_in_yaml_node(value_node, loader, visited) def _substitute_env_in_json_value(value: Any) -> Any: """Recursively apply env-var substitution to string values in JSON data. JSON has explicit types and no comments, so substitution applies only to string values (the result stays a string) and mapping keys are left as-is. """ if isinstance(value, str): return substitute_env_vars(value) if isinstance(value, list): return [_substitute_env_in_json_value(item) for item in value] if isinstance(value, dict): return {key: _substitute_env_in_json_value(val) for key, val in value.items()} return value def _parse_config_content(content: str, suffix: str, file_path: str | os.PathLike[str]) -> Any: """Parse configuration text and substitute environment variables. Parsing happens first, so ``${VAR}`` references in comments and mapping keys are never substitution candidates; substitution then runs on configuration values, with YAML node types re-resolved for standalone references. Raises: ConfigurationError: If the content cannot be parsed or substitution of a required environment variable fails. """ try: if suffix == ".json": return _substitute_env_in_json_value(json.loads(content)) yaml_loader = yaml.SafeLoader(content) try: root_node = yaml_loader.get_single_node() if root_node is None: return None _substitute_env_in_yaml_node(root_node, yaml_loader) return yaml_loader.construct_document(root_node) finally: yaml_loader.dispose() except yaml.YAMLError as exc: _logger.exception("Failed to parse YAML from %s", file_path) raise ConfigurationError(f"Failed to parse YAML: {exc}") from exc except json.JSONDecodeError as exc: _logger.exception("Failed to parse JSON from %s", file_path) raise ConfigurationError(f"Failed to parse JSON: {exc}") from exc def load_config_file( file_path: str | os.PathLike[str], ) -> OpenTelemetryConfiguration: """Load and parse an OpenTelemetry configuration file. Supports YAML and JSON formats. Environment variable substitution is performed after parsing, on configuration values only, so ``${VAR}`` references in comments or mapping keys are left untouched. Args: file_path: Path to the configuration file (.yaml, .yml, or .json). Returns: Parsed OpenTelemetryConfiguration object. Raises: ConfigurationError: If file cannot be read, parsed, or validated. Examples: >>> config = load_config_file("otel-config.yaml") >>> print(config.tracer_provider) """ path = Path(file_path) if not path.exists(): _logger.error("Configuration file not found: %s", file_path) raise ConfigurationError(f"Configuration file not found: {file_path}") if not path.is_file(): _logger.error("Configuration path is not a file: %s", file_path) raise ConfigurationError(f"Configuration path is not a file: {file_path}") try: with open(path, encoding="utf-8") as config_file: content = config_file.read() except OSError as exc: _logger.exception("Failed to read configuration file: %s", file_path) raise ConfigurationError(f"Failed to read configuration file: {file_path}") from exc # Parse the file, then substitute env vars in configuration values only. # Parsing first means comments and mapping keys are never substitution # candidates, and node types are still resolved after substitution. suffix = path.suffix.lower() if suffix not in (".yaml", ".yml", ".json"): _logger.error("Unsupported file format: %s", suffix) raise ConfigurationError(f"Unsupported file format: {suffix}. Use .yaml, .yml, or .json") data = _parse_config_content(content, suffix, file_path) if data is None: _logger.error("Configuration file is empty: %s", file_path) raise ConfigurationError("Configuration file is empty") if not isinstance(data, dict): _logger.error( "Configuration must be a mapping/object, got %s", type(data).__name__, ) raise ConfigurationError(f"Configuration must be a mapping/object, got {type(data).__name__}") _validate_schema(data) _validate_file_format(data) # Convert to OpenTelemetryConfiguration model try: config = _dict_to_model(data) except Exception as exc: _logger.exception("Failed to validate configuration from %s", file_path) raise ConfigurationError(f"Failed to validate configuration: {exc}") from exc return config def _validate_schema(data: dict) -> None: """Validate configuration dict against the OTel configuration JSON schema. Raises: ConfigurationError: If the data does not conform to the schema. """ try: jsonschema.validate( instance=data, schema=_get_schema(), cls=jsonschema.Draft202012Validator, ) except jsonschema.ValidationError as exc: raise ConfigurationError( f"Configuration does not match schema: {exc.message} (at {' -> '.join(str(p) for p in exc.absolute_path)})" if exc.absolute_path else f"Configuration does not match schema: {exc.message}" ) from exc except jsonschema.SchemaError as exc: raise ConfigurationError(f"Invalid configuration schema: {exc.message}") from exc def _validate_file_format(data: dict) -> None: """Validate the ``file_format`` version per the configuration spec. The spec requires implementations to fail on an unsupported major version (a breaking-change boundary) and to accept, with a warning, a minor version newer than the one this SDK targets. See https://github.com/open-telemetry/opentelemetry-configuration/blob/main/VERSIONING.md Raises: ConfigurationError: If ``file_format`` is malformed or its major version is not supported. """ file_format = data.get("file_format") # file_format is required and typed as a string by the schema, which is # validated before this runs; guard defensively regardless. if not isinstance(file_format, str): raise ConfigurationError(f"Invalid file_format: expected a version string, got {file_format!r}") # Drop any pre-release / meta tag, e.g. "1.0-rc.2" -> "1.0". version = file_format.split("-", 1)[0] parts = version.split(".") try: major = int(parts[0]) minor = int(parts[1]) if len(parts) > 1 else 0 except ValueError as exc: raise ConfigurationError(f"Invalid file_format '{file_format}': expected MAJOR.MINOR version numbers") from exc if major != _SUPPORTED_SCHEMA_MAJOR: raise ConfigurationError( f"Unsupported file_format '{file_format}': this SDK supports schema version {_SUPPORTED_SCHEMA_MAJOR}.x" ) if minor > _SUPPORTED_SCHEMA_MINOR: _logger.warning( "Configuration file_format '%s' has a newer minor version than " "this SDK supports (%d.%d); some settings may be ignored.", file_format, _SUPPORTED_SCHEMA_MAJOR, _SUPPORTED_SCHEMA_MINOR, ) def _dict_to_model(data: dict[str, Any]) -> OpenTelemetryConfiguration: """Convert a parsed config dictionary to the full typed model tree. Walks each field's type annotation, recursively converting nested dicts to their corresponding dataclass types. The resulting ``OpenTelemetryConfiguration`` is fully typed end-to-end, so factory functions can rely on typed attribute access (e.g. ``config.sampler``, ``config.processors[0].batch.exporter``). Args: data: Parsed configuration dictionary. Returns: OpenTelemetryConfiguration instance. Raises: TypeError: If data doesn't match expected structure. ValueError: If values are invalid. """ try: return _dict_to_dataclass(data, OpenTelemetryConfiguration) except TypeError as exc: raise TypeError( f"Configuration structure is invalid. Check that all required fields are present and correctly typed: {exc}" ) from exc instrumentation.py000066400000000000000000000060531527070070700356600ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-configuration/src/opentelemetry/configuration# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations from dataclasses import fields, is_dataclass from inspect import isclass from logging import getLogger from opentelemetry.configuration._common import load_entry_point from opentelemetry.configuration._conversion import _dict_to_dataclass from opentelemetry.configuration._exceptions import ConfigurationError from opentelemetry.configuration.models import ExperimentalInstrumentation _logger = getLogger(__name__) def configure_instrumentation( configuration: ExperimentalInstrumentation | None, ) -> None: """Activate instrumentors listed under ``instrumentation/development.python``. For each entry in ``configuration.python`` the matching ``opentelemetry_instrumentor`` entry point is loaded. If the instrumentor class exposes a ``configuration`` attribute that is a dataclass type, the raw options are validated through ``_dict_to_dataclass`` before being forwarded to ``instrument()``. An ``enabled: false`` value suppresses instrumentation without raising. If an instrumentor is already active (e.g. ``opentelemetry-instrument`` ran before the SDK was configured from the file) its ``instrument()`` call is skipped to avoid a double-instrumentation warning. Absent or unknown entry points are logged as warnings; runtime errors from an instrumentor are logged as exceptions. Neither stops the remaining instrumentors from being applied. """ if configuration is None or configuration.python is None: return for name, options in configuration.python.items(): options = dict(options) if not options.pop("enabled", True): _logger.debug( "Instrumentation '%s' is disabled in declarative config; skipping", name, ) continue try: cls = load_entry_point("opentelemetry_instrumentor", name) configuration_cls = getattr(cls, "configuration", None) if isclass(configuration_cls) and is_dataclass(configuration_cls): configuration_obj = _dict_to_dataclass(options, configuration_cls) options = { f.name: value for f in fields(configuration_obj) if (value := getattr(configuration_obj, f.name)) is not None } instance = cls() if getattr(instance, "is_instrumented_by_opentelemetry", False): _logger.debug("Skipping '%s': already instrumented", name) else: instance.instrument(**options) _logger.debug("Instrumented '%s' via declarative config", name) except ConfigurationError as exc: _logger.warning( "Skipping instrumentation '%s' in declarative config: %s", name, exc, ) except Exception: # pylint: disable=broad-except _logger.exception("Failed to instrument '%s' via declarative config", name) python-opentelemetry-1.45.2/opentelemetry-configuration/src/opentelemetry/configuration/models.py000066400000000000000000000523151527070070700337610ustar00rootroot00000000000000# generated by datamodel-codegen: # filename: schema.json # timestamp: 2026-06-23T15:17:37+00:00 from __future__ import annotations from dataclasses import dataclass from enum import Enum from typing import Any, ClassVar, TypeAlias from opentelemetry.configuration._common import _additional_properties AlwaysOffSampler: TypeAlias = dict[str, Any] | None AlwaysOnSampler: TypeAlias = dict[str, Any] | None @dataclass class AttributeLimits: attribute_value_length_limit: int | None = None attribute_count_limit: int | None = None Value: TypeAlias = list[str] Value1: TypeAlias = list[bool] Value2: TypeAlias = list[float] class AttributeType(Enum): string = "string" bool = "bool" int = "int" double = "double" string_array = "string_array" bool_array = "bool_array" int_array = "int_array" double_array = "double_array" B3MultiPropagator: TypeAlias = dict[str, Any] | None B3Propagator: TypeAlias = dict[str, Any] | None BaggagePropagator: TypeAlias = dict[str, Any] | None @dataclass class Base2ExponentialBucketHistogramAggregation: max_scale: int | None = None max_size: int | None = None record_min_max: bool | None = None @dataclass class CardinalityLimits: default: int | None = None counter: int | None = None gauge: int | None = None histogram: int | None = None observable_counter: int | None = None observable_gauge: int | None = None observable_up_down_counter: int | None = None up_down_counter: int | None = None ConsoleExporter: TypeAlias = dict[str, Any] | None DefaultAggregation: TypeAlias = dict[str, Any] | None Distribution: TypeAlias = dict[str, dict[str, Any]] DropAggregation: TypeAlias = dict[str, Any] | None class ExemplarFilter(Enum): always_on = "always_on" always_off = "always_off" trace_based = "trace_based" ExperimentalComposableAlwaysOffSampler: TypeAlias = dict[str, Any] | None ExperimentalComposableAlwaysOnSampler: TypeAlias = dict[str, Any] | None @dataclass class ExperimentalComposableProbabilitySampler: ratio: float | None = None @dataclass class ExperimentalComposableRuleBasedSamplerRuleAttributePatterns: key: str included: list[str] | None = None excluded: list[str] | None = None @dataclass class ExperimentalComposableRuleBasedSamplerRuleAttributeValues: key: str values: list[str] ExperimentalContainerResourceDetector: TypeAlias = dict[str, Any] | None ExperimentalEventToSpanEventBridgeLogRecordProcessor: TypeAlias = dict[str, Any] | None ExperimentalHostResourceDetector: TypeAlias = dict[str, Any] | None @dataclass class ExperimentalHttpClientInstrumentation: request_captured_headers: list[str] | None = None response_captured_headers: list[str] | None = None known_methods: list[str] | None = None @dataclass class ExperimentalHttpServerInstrumentation: request_captured_headers: list[str] | None = None response_captured_headers: list[str] | None = None known_methods: list[str] | None = None ExperimentalLanguageSpecificInstrumentation: TypeAlias = dict[str, dict[str, Any]] @dataclass class ExperimentalMeterConfig: enabled: bool | None = None @dataclass class ExperimentalMeterMatcherAndConfig: name: str config: ExperimentalMeterConfig @dataclass class ExperimentalOtlpFileExporter: output_stream: str | None = None @dataclass class ExperimentalProbabilitySampler: ratio: float | None = None ExperimentalProcessResourceDetector: TypeAlias = dict[str, Any] | None class ExperimentalPrometheusTranslationStrategy(Enum): underscore_escaping_with_suffixes = "underscore_escaping_with_suffixes" underscore_escaping_without_suffixes_development = "underscore_escaping_without_suffixes/development" no_utf8_escaping_with_suffixes_development = "no_utf8_escaping_with_suffixes/development" no_translation_development = "no_translation/development" @dataclass class ExperimentalSemconvConfig: version: int | None = None experimental: bool | None = None dual_emit: bool | None = None ExperimentalServiceResourceDetector: TypeAlias = dict[str, Any] | None class ExperimentalSpanParent(Enum): none = "none" remote = "remote" local = "local" @dataclass class ExperimentalTracerConfig: enabled: bool | None = None @dataclass class ExperimentalTracerMatcherAndConfig: name: str config: ExperimentalTracerConfig @dataclass class ExperimentalUrlSanitization: sensitive_query_parameters: list[str] | None = None @dataclass class ExplicitBucketHistogramAggregation: boundaries: list[float] | None = None record_min_max: bool | None = None class ExporterDefaultHistogramAggregation(Enum): explicit_bucket_histogram = "explicit_bucket_histogram" base2_exponential_bucket_histogram = "base2_exponential_bucket_histogram" class ExporterTemporalityPreference(Enum): cumulative = "cumulative" delta = "delta" low_memory = "low_memory" @dataclass class GrpcTls: ca_file: str | None = None key_file: str | None = None cert_file: str | None = None insecure: bool | None = None @dataclass class HttpTls: ca_file: str | None = None key_file: str | None = None cert_file: str | None = None @dataclass class IncludeExclude: included: list[str] | None = None excluded: list[str] | None = None class InstrumentType(Enum): counter = "counter" gauge = "gauge" histogram = "histogram" observable_counter = "observable_counter" observable_gauge = "observable_gauge" observable_up_down_counter = "observable_up_down_counter" up_down_counter = "up_down_counter" LastValueAggregation: TypeAlias = dict[str, Any] | None @dataclass class LogRecordLimits: attribute_value_length_limit: int | None = None attribute_count_limit: int | None = None @dataclass class NameStringValuePair: name: str value: str | None OpenCensusMetricProducer: TypeAlias = dict[str, Any] | None @dataclass class OtlpGrpcExporter: endpoint: str | None = None tls: GrpcTls | None = None headers: list[NameStringValuePair] | None = None headers_list: str | None = None compression: str | None = None timeout: int | None = None @dataclass class OtlpGrpcMetricExporter: endpoint: str | None = None tls: GrpcTls | None = None headers: list[NameStringValuePair] | None = None headers_list: str | None = None compression: str | None = None timeout: int | None = None temporality_preference: ExporterTemporalityPreference | None = None default_histogram_aggregation: ExporterDefaultHistogramAggregation | None = None class OtlpHttpEncoding(Enum): protobuf = "protobuf" json = "json" @dataclass class OtlpHttpExporter: endpoint: str | None = None tls: HttpTls | None = None headers: list[NameStringValuePair] | None = None headers_list: str | None = None compression: str | None = None timeout: int | None = None encoding: OtlpHttpEncoding | None = None @dataclass class OtlpHttpMetricExporter: endpoint: str | None = None tls: HttpTls | None = None headers: list[NameStringValuePair] | None = None headers_list: str | None = None compression: str | None = None timeout: int | None = None encoding: OtlpHttpEncoding | None = None temporality_preference: ExporterTemporalityPreference | None = None default_histogram_aggregation: ExporterDefaultHistogramAggregation | None = None RandomIdGenerator: TypeAlias = dict[str, Any] | None class SeverityNumber(Enum): trace = "trace" trace2 = "trace2" trace3 = "trace3" trace4 = "trace4" debug = "debug" debug2 = "debug2" debug3 = "debug3" debug4 = "debug4" info = "info" info2 = "info2" info3 = "info3" info4 = "info4" warn = "warn" warn2 = "warn2" warn3 = "warn3" warn4 = "warn4" error = "error" error2 = "error2" error3 = "error3" error4 = "error4" fatal = "fatal" fatal2 = "fatal2" fatal3 = "fatal3" fatal4 = "fatal4" @_additional_properties @dataclass class SpanExporter: otlp_http: OtlpHttpExporter | None = None otlp_grpc: OtlpGrpcExporter | None = None otlp_file_development: ExperimentalOtlpFileExporter | None = None console: ConsoleExporter | None = None additional_properties: ClassVar[dict[str, Any]] class SpanKind(Enum): internal = "internal" server = "server" client = "client" producer = "producer" consumer = "consumer" @dataclass class SpanLimits: attribute_value_length_limit: int | None = None attribute_count_limit: int | None = None event_count_limit: int | None = None link_count_limit: int | None = None event_attribute_count_limit: int | None = None link_attribute_count_limit: int | None = None SumAggregation: TypeAlias = dict[str, Any] | None TraceContextPropagator: TypeAlias = dict[str, Any] | None @dataclass class TraceIdRatioBasedSampler: ratio: float | None = None @dataclass class ViewSelector: instrument_name: str | None = None instrument_type: InstrumentType | None = None unit: str | None = None meter_name: str | None = None meter_version: str | None = None meter_schema_url: str | None = None @dataclass class Aggregation: default: DefaultAggregation | None = None drop: DropAggregation | None = None explicit_bucket_histogram: ExplicitBucketHistogramAggregation | None = None base2_exponential_bucket_histogram: Base2ExponentialBucketHistogramAggregation | None = None last_value: LastValueAggregation | None = None sum: SumAggregation | None = None @dataclass class AttributeNameValue: name: str value: str | float | bool | Value | Value1 | Value2 | None type: AttributeType | None = None @dataclass class BatchSpanProcessor: exporter: SpanExporter schedule_delay: int | None = None export_timeout: int | None = None max_queue_size: int | None = None max_export_batch_size: int | None = None @dataclass class ConsoleMetricExporter: temporality_preference: ExporterTemporalityPreference | None = None default_histogram_aggregation: ExporterDefaultHistogramAggregation | None = None @dataclass class ExperimentalCodeInstrumentation: semconv: ExperimentalSemconvConfig | None = None @dataclass class ExperimentalDbInstrumentation: semconv: ExperimentalSemconvConfig | None = None @dataclass class ExperimentalGenAiInstrumentation: semconv: ExperimentalSemconvConfig | None = None @dataclass class ExperimentalHttpInstrumentation: semconv: ExperimentalSemconvConfig | None = None client: ExperimentalHttpClientInstrumentation | None = None server: ExperimentalHttpServerInstrumentation | None = None @dataclass class ExperimentalLoggerConfig: enabled: bool | None = None minimum_severity: SeverityNumber | None = None trace_based: bool | None = None @dataclass class ExperimentalLoggerMatcherAndConfig: name: str config: ExperimentalLoggerConfig @dataclass class ExperimentalMessagingInstrumentation: semconv: ExperimentalSemconvConfig | None = None @dataclass class ExperimentalMeterConfigurator: default_config: ExperimentalMeterConfig | None = None meters: list[ExperimentalMeterMatcherAndConfig] | None = None @dataclass class ExperimentalOtlpFileMetricExporter: output_stream: str | None = None temporality_preference: ExporterTemporalityPreference | None = None default_histogram_aggregation: ExporterDefaultHistogramAggregation | None = None @dataclass class ExperimentalPrometheusMetricExporter: host: str | None = None port: int | None = None scope_info_enabled: bool | None = None target_info_enabled_development: bool | None = None resource_constant_labels: IncludeExclude | None = None translation_strategy: ExperimentalPrometheusTranslationStrategy | None = None @_additional_properties @dataclass class ExperimentalResourceDetector: container: ExperimentalContainerResourceDetector | None = None host: ExperimentalHostResourceDetector | None = None process: ExperimentalProcessResourceDetector | None = None service: ExperimentalServiceResourceDetector | None = None additional_properties: ClassVar[dict[str, Any]] @dataclass class ExperimentalRpcInstrumentation: semconv: ExperimentalSemconvConfig | None = None @dataclass class ExperimentalSanitization: url: ExperimentalUrlSanitization | None = None @dataclass class ExperimentalTracerConfigurator: default_config: ExperimentalTracerConfig | None = None tracers: list[ExperimentalTracerMatcherAndConfig] | None = None @_additional_properties @dataclass class IdGenerator: random: RandomIdGenerator | None = None additional_properties: ClassVar[dict[str, Any]] @_additional_properties @dataclass class LogRecordExporter: otlp_http: OtlpHttpExporter | None = None otlp_grpc: OtlpGrpcExporter | None = None otlp_file_development: ExperimentalOtlpFileExporter | None = None console: ConsoleExporter | None = None additional_properties: ClassVar[dict[str, Any]] @_additional_properties @dataclass class MetricProducer: opencensus: OpenCensusMetricProducer | None = None additional_properties: ClassVar[dict[str, Any]] @_additional_properties @dataclass class PullMetricExporter: prometheus_development: ExperimentalPrometheusMetricExporter | None = None additional_properties: ClassVar[dict[str, Any]] @dataclass class PullMetricReader: exporter: PullMetricExporter producers: list[MetricProducer] | None = None cardinality_limits: CardinalityLimits | None = None @_additional_properties @dataclass class PushMetricExporter: otlp_http: OtlpHttpMetricExporter | None = None otlp_grpc: OtlpGrpcMetricExporter | None = None otlp_file_development: ExperimentalOtlpFileMetricExporter | None = None console: ConsoleMetricExporter | None = None additional_properties: ClassVar[dict[str, Any]] @dataclass class SimpleLogRecordProcessor: exporter: LogRecordExporter @dataclass class SimpleSpanProcessor: exporter: SpanExporter @_additional_properties @dataclass class SpanProcessor: batch: BatchSpanProcessor | None = None simple: SimpleSpanProcessor | None = None additional_properties: ClassVar[dict[str, Any]] @_additional_properties @dataclass class TextMapPropagator: tracecontext: TraceContextPropagator | None = None baggage: BaggagePropagator | None = None b3: B3Propagator | None = None b3multi: B3MultiPropagator | None = None additional_properties: ClassVar[dict[str, Any]] @dataclass class ViewStream: name: str | None = None description: str | None = None aggregation: Aggregation | None = None aggregation_cardinality_limit: int | None = None attribute_keys: IncludeExclude | None = None @dataclass class BatchLogRecordProcessor: exporter: LogRecordExporter schedule_delay: int | None = None export_timeout: int | None = None max_queue_size: int | None = None max_export_batch_size: int | None = None @dataclass class ExperimentalGeneralInstrumentation: http: ExperimentalHttpInstrumentation | None = None code: ExperimentalCodeInstrumentation | None = None db: ExperimentalDbInstrumentation | None = None gen_ai: ExperimentalGenAiInstrumentation | None = None messaging: ExperimentalMessagingInstrumentation | None = None rpc: ExperimentalRpcInstrumentation | None = None sanitization: ExperimentalSanitization | None = None stability_opt_in_list: str | None = None @dataclass class ExperimentalInstrumentation: general: ExperimentalGeneralInstrumentation | None = None cpp: ExperimentalLanguageSpecificInstrumentation | None = None dotnet: ExperimentalLanguageSpecificInstrumentation | None = None erlang: ExperimentalLanguageSpecificInstrumentation | None = None go: ExperimentalLanguageSpecificInstrumentation | None = None java: ExperimentalLanguageSpecificInstrumentation | None = None js: ExperimentalLanguageSpecificInstrumentation | None = None php: ExperimentalLanguageSpecificInstrumentation | None = None python: ExperimentalLanguageSpecificInstrumentation | None = None ruby: ExperimentalLanguageSpecificInstrumentation | None = None rust: ExperimentalLanguageSpecificInstrumentation | None = None swift: ExperimentalLanguageSpecificInstrumentation | None = None @dataclass class ExperimentalLoggerConfigurator: default_config: ExperimentalLoggerConfig | None = None loggers: list[ExperimentalLoggerMatcherAndConfig] | None = None @dataclass class ExperimentalResourceDetection: attributes: IncludeExclude | None = None detectors: list[ExperimentalResourceDetector] | None = None @_additional_properties @dataclass class LogRecordProcessor: batch: BatchLogRecordProcessor | None = None simple: SimpleLogRecordProcessor | None = None event_to_span_event_bridge_development: ExperimentalEventToSpanEventBridgeLogRecordProcessor | None = None additional_properties: ClassVar[dict[str, Any]] @dataclass class PeriodicMetricReader: exporter: PushMetricExporter interval: int | None = None timeout: int | None = None max_export_batch_size_development: int | None = None producers: list[MetricProducer] | None = None cardinality_limits: CardinalityLimits | None = None @dataclass class Propagator: composite: list[TextMapPropagator] | None = None composite_list: str | None = None @dataclass class Resource: attributes: list[AttributeNameValue] | None = None detection_development: ExperimentalResourceDetection | None = None schema_url: str | None = None attributes_list: str | None = None @dataclass class View: selector: ViewSelector stream: ViewStream @dataclass class LoggerProvider: processors: list[LogRecordProcessor] limits: LogRecordLimits | None = None logger_configurator_development: ExperimentalLoggerConfigurator | None = None @dataclass class MetricReader: periodic: PeriodicMetricReader | None = None pull: PullMetricReader | None = None @dataclass class MeterProvider: readers: list[MetricReader] views: list[View] | None = None exemplar_filter: ExemplarFilter | None = None meter_configurator_development: ExperimentalMeterConfigurator | None = None @_additional_properties @dataclass class OpenTelemetryConfiguration: file_format: str disabled: bool | None = None log_level: SeverityNumber | None = None attribute_limits: AttributeLimits | None = None logger_provider: LoggerProvider | None = None meter_provider: MeterProvider | None = None propagator: Propagator | None = None tracer_provider: TracerProvider | None = None resource: Resource | None = None instrumentation_development: ExperimentalInstrumentation | None = None distribution: Distribution | None = None additional_properties: ClassVar[dict[str, Any]] @dataclass class ExperimentalComposableParentThresholdSampler: root: ExperimentalComposableSampler @dataclass class ExperimentalComposableRuleBasedSampler: rules: list[ExperimentalComposableRuleBasedSamplerRule] | None = None @dataclass class ExperimentalComposableRuleBasedSamplerRule: """ A rule for ExperimentalComposableRuleBasedSampler. A rule can have multiple match conditions - the sampler will be applied if all match. If no conditions are specified, the rule matches all spans that reach it. """ sampler: ExperimentalComposableSampler attribute_values: ExperimentalComposableRuleBasedSamplerRuleAttributeValues | None = None attribute_patterns: ExperimentalComposableRuleBasedSamplerRuleAttributePatterns | None = None span_kinds: list[SpanKind] | None = None parent: list[ExperimentalSpanParent] | None = None @_additional_properties @dataclass class ExperimentalComposableSampler: always_off: ExperimentalComposableAlwaysOffSampler | None = None always_on: ExperimentalComposableAlwaysOnSampler | None = None parent_threshold: ExperimentalComposableParentThresholdSampler | None = None probability: ExperimentalComposableProbabilitySampler | None = None rule_based: ExperimentalComposableRuleBasedSampler | None = None additional_properties: ClassVar[dict[str, Any]] @dataclass class ExperimentalJaegerRemoteSampler: endpoint: str initial_sampler: Sampler interval: int | None = None @dataclass class ParentBasedSampler: root: Sampler | None = None remote_parent_sampled: Sampler | None = None remote_parent_not_sampled: Sampler | None = None local_parent_sampled: Sampler | None = None local_parent_not_sampled: Sampler | None = None @_additional_properties @dataclass class Sampler: always_off: AlwaysOffSampler | None = None always_on: AlwaysOnSampler | None = None composite_development: ExperimentalComposableSampler | None = None jaeger_remote_development: ExperimentalJaegerRemoteSampler | None = None parent_based: ParentBasedSampler | None = None probability_development: ExperimentalProbabilitySampler | None = None trace_id_ratio_based: TraceIdRatioBasedSampler | None = None additional_properties: ClassVar[dict[str, Any]] @dataclass class TracerProvider: processors: list[SpanProcessor] limits: SpanLimits | None = None sampler: Sampler | None = None id_generator: IdGenerator | None = None tracer_configurator_development: ExperimentalTracerConfigurator | None = None python-opentelemetry-1.45.2/opentelemetry-configuration/src/opentelemetry/configuration/schema.json000066400000000000000000003164371527070070700342670ustar00rootroot00000000000000{ "$schema": "https://json-schema.org/draft/2020-12/schema", "title": "OpenTelemetryConfiguration", "type": "object", "additionalProperties": true, "properties": { "file_format": { "type": "string", "description": "The file format version.\nRepresented as a string including the semver major, minor version numbers (and optionally the meta tag). For example: \"0.4\", \"1.0-rc.2\", \"1.0\" (after stable release).\nSee https://github.com/open-telemetry/opentelemetry-configuration/blob/main/VERSIONING.md for more details.\nThe yaml format is documented at https://github.com/open-telemetry/opentelemetry-configuration/tree/main/schema\nProperty is required and must be non-null.\n" }, "disabled": { "type": [ "boolean", "null" ], "description": "Configure if the SDK is disabled or not.\nIf omitted or null, false is used.\n" }, "log_level": { "$ref": "#/$defs/SeverityNumber", "description": "Configure the log level of the internal logger used by the SDK.\nValues include:\n* debug: debug, severity number 5.\n* debug2: debug2, severity number 6.\n* debug3: debug3, severity number 7.\n* debug4: debug4, severity number 8.\n* error: error, severity number 17.\n* error2: error2, severity number 18.\n* error3: error3, severity number 19.\n* error4: error4, severity number 20.\n* fatal: fatal, severity number 21.\n* fatal2: fatal2, severity number 22.\n* fatal3: fatal3, severity number 23.\n* fatal4: fatal4, severity number 24.\n* info: info, severity number 9.\n* info2: info2, severity number 10.\n* info3: info3, severity number 11.\n* info4: info4, severity number 12.\n* trace: trace, severity number 1.\n* trace2: trace2, severity number 2.\n* trace3: trace3, severity number 3.\n* trace4: trace4, severity number 4.\n* warn: warn, severity number 13.\n* warn2: warn2, severity number 14.\n* warn3: warn3, severity number 15.\n* warn4: warn4, severity number 16.\nIf omitted, INFO is used.\n" }, "attribute_limits": { "$ref": "#/$defs/AttributeLimits", "description": "Configure general attribute limits. See also tracer_provider.limits, logger_provider.limits.\nIf omitted, default values as described in AttributeLimits are used.\n" }, "logger_provider": { "$ref": "#/$defs/LoggerProvider", "description": "Configure logger provider.\nIf omitted, a noop logger provider is used.\n" }, "meter_provider": { "$ref": "#/$defs/MeterProvider", "description": "Configure meter provider.\nIf omitted, a noop meter provider is used.\n" }, "propagator": { "$ref": "#/$defs/Propagator", "description": "Configure text map context propagators.\nIf omitted, a noop propagator is used.\n" }, "tracer_provider": { "$ref": "#/$defs/TracerProvider", "description": "Configure tracer provider.\nIf omitted, a noop tracer provider is used.\n" }, "resource": { "$ref": "#/$defs/Resource", "description": "Configure resource for all signals.\nIf omitted, the default resource is used.\n" }, "instrumentation/development": { "$ref": "#/$defs/ExperimentalInstrumentation", "description": "Configure instrumentation.\nIf omitted, instrumentation defaults are used.\n" }, "distribution": { "$ref": "#/$defs/Distribution", "description": "Defines configuration parameters specific to a particular OpenTelemetry distribution or vendor.\nThis section provides a standardized location for distribution-specific settings\nthat are not part of the OpenTelemetry configuration model.\nIt allows vendors to expose their own extensions and general configuration options.\nIf omitted, distribution defaults are used.\n" } }, "required": [ "file_format" ], "$defs": { "Aggregation": { "type": "object", "additionalProperties": false, "minProperties": 1, "maxProperties": 1, "properties": { "default": { "$ref": "#/$defs/DefaultAggregation", "description": "Configures the stream to use the instrument kind to select an aggregation and advisory parameters to influence aggregation configuration parameters. See https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk.md#default-aggregation for details.\nIf omitted, ignore.\n" }, "drop": { "$ref": "#/$defs/DropAggregation", "description": "Configures the stream to ignore/drop all instrument measurements. See https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk.md#drop-aggregation for details.\nIf omitted, ignore.\n" }, "explicit_bucket_histogram": { "$ref": "#/$defs/ExplicitBucketHistogramAggregation", "description": "Configures the stream to collect data for the histogram metric point using a set of explicit boundary values for histogram bucketing. See https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk.md#explicit-bucket-histogram-aggregation for details\nIf omitted, ignore.\n" }, "base2_exponential_bucket_histogram": { "$ref": "#/$defs/Base2ExponentialBucketHistogramAggregation", "description": "Configures the stream to collect data for the exponential histogram metric point, which uses a base-2 exponential formula to determine bucket boundaries and an integer scale parameter to control resolution. See https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk.md#base2-exponential-bucket-histogram-aggregation for details.\nIf omitted, ignore.\n" }, "last_value": { "$ref": "#/$defs/LastValueAggregation", "description": "Configures the stream to collect data using the last measurement. See https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk.md#last-value-aggregation for details.\nIf omitted, ignore.\n" }, "sum": { "$ref": "#/$defs/SumAggregation", "description": "Configures the stream to collect the arithmetic sum of measurement values. See https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk.md#sum-aggregation for details.\nIf omitted, ignore.\n" } } }, "AlwaysOffSampler": { "type": [ "object", "null" ], "additionalProperties": false }, "AlwaysOnSampler": { "type": [ "object", "null" ], "additionalProperties": false }, "AttributeLimits": { "type": "object", "additionalProperties": false, "properties": { "attribute_value_length_limit": { "type": [ "integer", "null" ], "minimum": 0, "description": "Configure max attribute value size. \nValue must be non-negative.\nIf omitted or null, there is no limit.\n" }, "attribute_count_limit": { "type": [ "integer", "null" ], "minimum": 0, "description": "Configure max attribute count. \nValue must be non-negative.\nIf omitted or null, 128 is used.\n" } } }, "AttributeNameValue": { "type": "object", "additionalProperties": false, "properties": { "name": { "type": "string", "description": "The attribute name.\nProperty is required and must be non-null.\n" }, "value": { "oneOf": [ { "type": "string" }, { "type": "number" }, { "type": "boolean" }, { "type": "null" }, { "type": "array", "items": { "type": "string" }, "minItems": 1 }, { "type": "array", "items": { "type": "boolean" }, "minItems": 1 }, { "type": "array", "items": { "type": "number" }, "minItems": 1 } ], "description": "The attribute value.\nThe type of value must match .type.\nProperty must be present, but if null the entry is ignored.\n" }, "type": { "$ref": "#/$defs/AttributeType", "description": "The attribute type.\nValues include:\n* bool: Boolean attribute value.\n* bool_array: Boolean array attribute value.\n* double: Double attribute value.\n* double_array: Double array attribute value.\n* int: Integer attribute value.\n* int_array: Integer array attribute value.\n* string: String attribute value.\n* string_array: String array attribute value.\nIf omitted, string is used.\n" } }, "required": [ "name", "value" ] }, "AttributeType": { "type": [ "string", "null" ], "enum": [ "string", "bool", "int", "double", "string_array", "bool_array", "int_array", "double_array" ] }, "B3MultiPropagator": { "type": [ "object", "null" ], "additionalProperties": false }, "B3Propagator": { "type": [ "object", "null" ], "additionalProperties": false }, "BaggagePropagator": { "type": [ "object", "null" ], "additionalProperties": false }, "Base2ExponentialBucketHistogramAggregation": { "type": [ "object", "null" ], "additionalProperties": false, "properties": { "max_scale": { "type": [ "integer", "null" ], "minimum": -10, "maximum": 20, "description": "Configure the max scale factor.\nIf omitted or null, 20 is used.\n" }, "max_size": { "type": [ "integer", "null" ], "minimum": 2, "description": "Configure the maximum number of buckets in each of the positive and negative ranges, not counting the special zero bucket.\nIf omitted or null, 160 is used.\n" }, "record_min_max": { "type": [ "boolean", "null" ], "description": "Configure whether or not to record min and max.\nIf omitted or null, true is used.\n" } } }, "BatchLogRecordProcessor": { "type": "object", "additionalProperties": false, "properties": { "schedule_delay": { "type": [ "integer", "null" ], "minimum": 0, "description": "Configure delay interval (in milliseconds) between two consecutive exports. \nValue must be non-negative.\nIf omitted or null, 1000 is used.\n" }, "export_timeout": { "type": [ "integer", "null" ], "minimum": 0, "description": "Configure maximum allowed time (in milliseconds) to export data. \nValue must be non-negative. A value of 0 indicates no limit (infinity).\nIf omitted or null, 30000 is used.\n" }, "max_queue_size": { "type": [ "integer", "null" ], "exclusiveMinimum": 0, "description": "Configure maximum queue size. Value must be positive.\nIf omitted or null, 2048 is used.\n" }, "max_export_batch_size": { "type": [ "integer", "null" ], "exclusiveMinimum": 0, "description": "Configure maximum batch size. Value must be positive.\nIf omitted or null, 512 is used.\n" }, "exporter": { "$ref": "#/$defs/LogRecordExporter", "description": "Configure exporter.\nProperty is required and must be non-null.\n" } }, "required": [ "exporter" ] }, "BatchSpanProcessor": { "type": "object", "additionalProperties": false, "properties": { "schedule_delay": { "type": [ "integer", "null" ], "minimum": 0, "description": "Configure delay interval (in milliseconds) between two consecutive exports. \nValue must be non-negative.\nIf omitted or null, 5000 is used.\n" }, "export_timeout": { "type": [ "integer", "null" ], "minimum": 0, "description": "Configure maximum allowed time (in milliseconds) to export data. \nValue must be non-negative. A value of 0 indicates no limit (infinity).\nIf omitted or null, 30000 is used.\n" }, "max_queue_size": { "type": [ "integer", "null" ], "exclusiveMinimum": 0, "description": "Configure maximum queue size. Value must be positive.\nIf omitted or null, 2048 is used.\n" }, "max_export_batch_size": { "type": [ "integer", "null" ], "exclusiveMinimum": 0, "description": "Configure maximum batch size. Value must be positive.\nIf omitted or null, 512 is used.\n" }, "exporter": { "$ref": "#/$defs/SpanExporter", "description": "Configure exporter.\nProperty is required and must be non-null.\n" } }, "required": [ "exporter" ] }, "CardinalityLimits": { "type": "object", "additionalProperties": false, "properties": { "default": { "type": [ "integer", "null" ], "exclusiveMinimum": 0, "description": "Configure default cardinality limit for all instrument types.\nInstrument-specific cardinality limits take priority.\nIf omitted or null, 2000 is used.\n" }, "counter": { "type": [ "integer", "null" ], "exclusiveMinimum": 0, "description": "Configure default cardinality limit for counter instruments.\nIf omitted or null, the value from .default is used.\n" }, "gauge": { "type": [ "integer", "null" ], "exclusiveMinimum": 0, "description": "Configure default cardinality limit for gauge instruments.\nIf omitted or null, the value from .default is used.\n" }, "histogram": { "type": [ "integer", "null" ], "exclusiveMinimum": 0, "description": "Configure default cardinality limit for histogram instruments.\nIf omitted or null, the value from .default is used.\n" }, "observable_counter": { "type": [ "integer", "null" ], "exclusiveMinimum": 0, "description": "Configure default cardinality limit for observable_counter instruments.\nIf omitted or null, the value from .default is used.\n" }, "observable_gauge": { "type": [ "integer", "null" ], "exclusiveMinimum": 0, "description": "Configure default cardinality limit for observable_gauge instruments.\nIf omitted or null, the value from .default is used.\n" }, "observable_up_down_counter": { "type": [ "integer", "null" ], "exclusiveMinimum": 0, "description": "Configure default cardinality limit for observable_up_down_counter instruments.\nIf omitted or null, the value from .default is used.\n" }, "up_down_counter": { "type": [ "integer", "null" ], "exclusiveMinimum": 0, "description": "Configure default cardinality limit for up_down_counter instruments.\nIf omitted or null, the value from .default is used.\n" } } }, "ConsoleExporter": { "type": [ "object", "null" ], "additionalProperties": false }, "ConsoleMetricExporter": { "type": [ "object", "null" ], "additionalProperties": false, "properties": { "temporality_preference": { "$ref": "#/$defs/ExporterTemporalityPreference", "description": "Configure temporality preference.\nValues include:\n* cumulative: Use cumulative aggregation temporality for all instrument types.\n* delta: Use delta aggregation for all instrument types except up down counter and asynchronous up down counter.\n* low_memory: Use delta aggregation temporality for counter and histogram instrument types. Use cumulative aggregation temporality for all other instrument types.\nIf omitted, cumulative is used.\n" }, "default_histogram_aggregation": { "$ref": "#/$defs/ExporterDefaultHistogramAggregation", "description": "Configure default histogram aggregation.\nValues include:\n* base2_exponential_bucket_histogram: Use base2 exponential histogram as the default aggregation for histogram instruments.\n* explicit_bucket_histogram: Use explicit bucket histogram as the default aggregation for histogram instruments.\nIf omitted, explicit_bucket_histogram is used.\n" } } }, "DefaultAggregation": { "type": [ "object", "null" ], "additionalProperties": false }, "Distribution": { "type": "object", "additionalProperties": { "type": "object" }, "minProperties": 1 }, "DropAggregation": { "type": [ "object", "null" ], "additionalProperties": false }, "ExemplarFilter": { "type": [ "string", "null" ], "enum": [ "always_on", "always_off", "trace_based" ] }, "ExperimentalCodeInstrumentation": { "type": "object", "additionalProperties": false, "properties": { "semconv": { "$ref": "#/$defs/ExperimentalSemconvConfig", "description": "Configure code semantic convention version and migration behavior.\n\nThis property takes precedence over the .instrumentation/development.general.stability_opt_in_list setting.\n\nSee code semantic conventions: https://opentelemetry.io/docs/specs/semconv/registry/attributes/code/\nIf omitted, uses the general stability_opt_in_list setting, or instrumentations continue emitting their default semantic convention version if not set.\n" } } }, "ExperimentalComposableAlwaysOffSampler": { "type": [ "object", "null" ], "additionalProperties": false }, "ExperimentalComposableAlwaysOnSampler": { "type": [ "object", "null" ], "additionalProperties": false }, "ExperimentalComposableParentThresholdSampler": { "type": [ "object" ], "additionalProperties": false, "properties": { "root": { "$ref": "#/$defs/ExperimentalComposableSampler", "description": "Sampler to use when there is no parent.\nProperty is required and must be non-null.\n" } }, "required": [ "root" ] }, "ExperimentalComposableProbabilitySampler": { "type": [ "object", "null" ], "additionalProperties": false, "properties": { "ratio": { "type": [ "number", "null" ], "minimum": 0, "maximum": 1, "description": "Configure ratio.\nIf omitted or null, 1.0 is used.\n" } } }, "ExperimentalComposableRuleBasedSampler": { "type": [ "object", "null" ], "additionalProperties": false, "properties": { "rules": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/ExperimentalComposableRuleBasedSamplerRule" }, "description": "The rules for the sampler, matched in order.\nEach rule can have multiple match conditions. All conditions must match for the rule to match.\nIf no conditions are specified, the rule matches all spans that reach it.\nIf no rules match, the span is not sampled.\nIf omitted, no span is sampled.\n" } } }, "ExperimentalComposableRuleBasedSamplerRule": { "type": "object", "description": "A rule for ExperimentalComposableRuleBasedSampler. A rule can have multiple match conditions - the sampler will be applied if all match. \nIf no conditions are specified, the rule matches all spans that reach it.\n", "additionalProperties": false, "properties": { "attribute_values": { "$ref": "#/$defs/ExperimentalComposableRuleBasedSamplerRuleAttributeValues", "description": "Values to match against a single attribute. Non-string attributes are matched using their string representation:\nfor example, a value of \"404\" would match the http.response.status_code 404. For array attributes, if any\nitem matches, it is considered a match.\nIf omitted, ignore.\n" }, "attribute_patterns": { "$ref": "#/$defs/ExperimentalComposableRuleBasedSamplerRuleAttributePatterns", "description": "Patterns to match against a single attribute. Non-string attributes are matched using their string representation:\nfor example, a pattern of \"4*\" would match any http.response.status_code in 400-499. For array attributes, if any\nitem matches, it is considered a match.\nIf omitted, ignore.\n" }, "span_kinds": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/SpanKind" }, "description": "The span kinds to match. If the span's kind matches any of these, it matches.\nValues include:\n* client: client, a client span.\n* consumer: consumer, a consumer span.\n* internal: internal, an internal span.\n* producer: producer, a producer span.\n* server: server, a server span.\nIf omitted, ignore.\n" }, "parent": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/ExperimentalSpanParent" }, "description": "The parent span types to match.\nValues include:\n* local: local, a local parent.\n* none: none, no parent, i.e., the trace root.\n* remote: remote, a remote parent.\nIf omitted, ignore.\n" }, "sampler": { "$ref": "#/$defs/ExperimentalComposableSampler", "description": "The sampler to use for matching spans.\nProperty is required and must be non-null.\n" } }, "required": [ "sampler" ] }, "ExperimentalComposableRuleBasedSamplerRuleAttributePatterns": { "type": "object", "additionalProperties": false, "properties": { "key": { "type": "string", "description": "The attribute key to match against.\nProperty is required and must be non-null.\n" }, "included": { "type": "array", "minItems": 1, "items": { "type": "string" }, "description": "Configure list of value patterns to include.\nMatching is case-sensitive. Values are evaluated to match as follows:\n * If the value exactly matches.\n * If the value matches the wildcard pattern, where '?' matches any single character and '*' matches any number of characters including none.\nIf omitted, all values are included.\n" }, "excluded": { "type": "array", "minItems": 1, "items": { "type": "string" }, "description": "Configure list of value patterns to exclude. Applies after .included (i.e. excluded has higher priority than included).\nMatching is case-sensitive. Values are evaluated to match as follows:\n * If the value exactly matches.\n * If the value matches the wildcard pattern, where '?' matches any single character and '*' matches any number of characters including none.\nIf omitted, .included attributes are included.\n" } }, "required": [ "key" ] }, "ExperimentalComposableRuleBasedSamplerRuleAttributeValues": { "type": "object", "additionalProperties": false, "properties": { "key": { "type": "string", "description": "The attribute key to match against.\nProperty is required and must be non-null.\n" }, "values": { "type": "array", "minItems": 1, "items": { "type": "string" }, "description": "The attribute values to match against. If the attribute's value matches any of these, it matches.\nProperty is required and must be non-null.\n" } }, "required": [ "key", "values" ] }, "ExperimentalComposableSampler": { "type": "object", "additionalProperties": { "type": [ "object", "null" ] }, "minProperties": 1, "maxProperties": 1, "properties": { "always_off": { "$ref": "#/$defs/ExperimentalComposableAlwaysOffSampler", "description": "Configure sampler to be always_off.\nIf omitted, ignore.\n" }, "always_on": { "$ref": "#/$defs/ExperimentalComposableAlwaysOnSampler", "description": "Configure sampler to be always_on.\nIf omitted, ignore.\n" }, "parent_threshold": { "$ref": "#/$defs/ExperimentalComposableParentThresholdSampler", "description": "Configure sampler to be parent_threshold.\nIf omitted, ignore.\n" }, "probability": { "$ref": "#/$defs/ExperimentalComposableProbabilitySampler", "description": "Configure sampler to be probability.\nIf omitted, ignore.\n" }, "rule_based": { "$ref": "#/$defs/ExperimentalComposableRuleBasedSampler", "description": "Configure sampler to be rule_based.\nIf omitted, ignore.\n" } } }, "ExperimentalContainerResourceDetector": { "type": [ "object", "null" ], "additionalProperties": false }, "ExperimentalDbInstrumentation": { "type": "object", "additionalProperties": false, "properties": { "semconv": { "$ref": "#/$defs/ExperimentalSemconvConfig", "description": "Configure database semantic convention version and migration behavior.\n\nThis property takes precedence over the .instrumentation/development.general.stability_opt_in_list setting.\n\nSee database migration: https://opentelemetry.io/docs/specs/semconv/database/\nIf omitted, uses the general stability_opt_in_list setting, or instrumentations continue emitting their default semantic convention version if not set.\n" } } }, "ExperimentalEventToSpanEventBridgeLogRecordProcessor": { "type": [ "object", "null" ], "additionalProperties": false }, "ExperimentalGenAiInstrumentation": { "type": "object", "additionalProperties": false, "properties": { "semconv": { "$ref": "#/$defs/ExperimentalSemconvConfig", "description": "Configure GenAI semantic convention version and migration behavior.\n\nThis property takes precedence over the .instrumentation/development.general.stability_opt_in_list setting.\n\nSee GenAI semantic conventions: https://opentelemetry.io/docs/specs/semconv/gen-ai/\nIf omitted, uses the general stability_opt_in_list setting, or instrumentations continue emitting their default semantic convention version if not set.\n" } } }, "ExperimentalGeneralInstrumentation": { "type": "object", "additionalProperties": false, "properties": { "http": { "$ref": "#/$defs/ExperimentalHttpInstrumentation", "description": "Configure instrumentations following the http semantic conventions.\nSee http semantic conventions: https://opentelemetry.io/docs/specs/semconv/http/\nIf omitted, defaults as described in ExperimentalHttpInstrumentation are used.\n" }, "code": { "$ref": "#/$defs/ExperimentalCodeInstrumentation", "description": "Configure instrumentations following the code semantic conventions.\nSee code semantic conventions: https://opentelemetry.io/docs/specs/semconv/registry/attributes/code/\nIf omitted, defaults as described in ExperimentalCodeInstrumentation are used.\n" }, "db": { "$ref": "#/$defs/ExperimentalDbInstrumentation", "description": "Configure instrumentations following the database semantic conventions.\nSee database semantic conventions: https://opentelemetry.io/docs/specs/semconv/database/\nIf omitted, defaults as described in ExperimentalDbInstrumentation are used.\n" }, "gen_ai": { "$ref": "#/$defs/ExperimentalGenAiInstrumentation", "description": "Configure instrumentations following the GenAI semantic conventions.\nSee GenAI semantic conventions: https://opentelemetry.io/docs/specs/semconv/gen-ai/\nIf omitted, defaults as described in ExperimentalGenAiInstrumentation are used.\n" }, "messaging": { "$ref": "#/$defs/ExperimentalMessagingInstrumentation", "description": "Configure instrumentations following the messaging semantic conventions.\nSee messaging semantic conventions: https://opentelemetry.io/docs/specs/semconv/messaging/\nIf omitted, defaults as described in ExperimentalMessagingInstrumentation are used.\n" }, "rpc": { "$ref": "#/$defs/ExperimentalRpcInstrumentation", "description": "Configure instrumentations following the RPC semantic conventions.\nSee RPC semantic conventions: https://opentelemetry.io/docs/specs/semconv/rpc/\nIf omitted, defaults as described in ExperimentalRpcInstrumentation are used.\n" }, "sanitization": { "$ref": "#/$defs/ExperimentalSanitization", "description": "Configure general sanitization options.\nIf omitted, defaults as described in ExperimentalSanitization are used.\n" }, "stability_opt_in_list": { "type": [ "string", "null" ], "description": "Configure semantic convention stability opt-in as a comma-separated list.\nThis property follows the format and semantics of the OTEL_SEMCONV_STABILITY_OPT_IN environment variable.\nControls the emission of stable vs. experimental semantic conventions for instrumentation.\nThis setting is only intended for migrating from experimental to stable semantic conventions.\n\nKnown values include:\n- http: Emit stable HTTP and networking conventions only\n- http/dup: Emit both old and stable HTTP and networking conventions (for phased migration)\n- database: Emit stable database conventions only\n- database/dup: Emit both old and stable database conventions (for phased migration)\n- rpc: Emit stable RPC conventions only\n- rpc/dup: Emit both experimental and stable RPC conventions (for phased migration)\n- messaging: Emit stable messaging conventions only\n- messaging/dup: Emit both old and stable messaging conventions (for phased migration)\n- code: Emit stable code conventions only\n- code/dup: Emit both old and stable code conventions (for phased migration)\n\nMultiple values can be specified as a comma-separated list (e.g., \"http,database/dup\").\nAdditional signal types may be supported in future versions.\n\nDomain-specific semconv properties (e.g., .instrumentation/development.general.db.semconv) take precedence over this general setting.\n\nSee:\n- HTTP migration: https://opentelemetry.io/docs/specs/semconv/non-normative/http-migration/\n- Database migration: https://opentelemetry.io/docs/specs/semconv/database/\n- RPC: https://opentelemetry.io/docs/specs/semconv/rpc/\n- Messaging: https://opentelemetry.io/docs/specs/semconv/messaging/messaging-spans/\nIf omitted or null, no opt-in is configured and instrumentations continue emitting their default semantic convention version.\n" } } }, "ExperimentalHostResourceDetector": { "type": [ "object", "null" ], "additionalProperties": false }, "ExperimentalHttpClientInstrumentation": { "type": "object", "additionalProperties": false, "properties": { "request_captured_headers": { "type": "array", "minItems": 1, "items": { "type": "string" }, "description": "Configure headers to capture for outbound http requests.\nIf omitted, no outbound request headers are captured.\n" }, "response_captured_headers": { "type": "array", "minItems": 1, "items": { "type": "string" }, "description": "Configure headers to capture for inbound http responses.\nIf omitted, no inbound response headers are captured.\n" }, "known_methods": { "type": "array", "minItems": 0, "items": { "type": "string" }, "description": "Override the default list of known HTTP methods.\nKnown methods are case-sensitive.\nThis is a full override of the default known methods, not a list of known methods in addition to the defaults.\nIf omitted, HTTP methods GET, HEAD, POST, PUT, DELETE, CONNECT, OPTIONS, TRACE, PATCH are known.\n" } } }, "ExperimentalHttpInstrumentation": { "type": "object", "additionalProperties": false, "properties": { "semconv": { "$ref": "#/$defs/ExperimentalSemconvConfig", "description": "Configure HTTP semantic convention version and migration behavior.\n\nThis property takes precedence over the .instrumentation/development.general.stability_opt_in_list setting.\n\nSee HTTP migration: https://opentelemetry.io/docs/specs/semconv/non-normative/http-migration/\nIf omitted, uses the general stability_opt_in_list setting, or instrumentations continue emitting their default semantic convention version if not set.\n" }, "client": { "$ref": "#/$defs/ExperimentalHttpClientInstrumentation", "description": "Configure instrumentations following the http client semantic conventions.\nIf omitted, defaults as described in ExperimentalHttpClientInstrumentation are used.\n" }, "server": { "$ref": "#/$defs/ExperimentalHttpServerInstrumentation", "description": "Configure instrumentations following the http server semantic conventions.\nIf omitted, defaults as described in ExperimentalHttpServerInstrumentation are used.\n" } } }, "ExperimentalHttpServerInstrumentation": { "type": "object", "additionalProperties": false, "properties": { "request_captured_headers": { "type": "array", "minItems": 1, "items": { "type": "string" }, "description": "Configure headers to capture for inbound http requests.\nIf omitted, no request headers are captured.\n" }, "response_captured_headers": { "type": "array", "minItems": 1, "items": { "type": "string" }, "description": "Configure headers to capture for outbound http responses.\nIf omitted, no response headers are captures.\n" }, "known_methods": { "type": "array", "minItems": 0, "items": { "type": "string" }, "description": "Override the default list of known HTTP methods.\nKnown methods are case-sensitive.\nThis is a full override of the default known methods, not a list of known methods in addition to the defaults.\nIf omitted, HTTP methods GET, HEAD, POST, PUT, DELETE, CONNECT, OPTIONS, TRACE, PATCH are known.\n" } } }, "ExperimentalInstrumentation": { "type": "object", "additionalProperties": false, "properties": { "general": { "$ref": "#/$defs/ExperimentalGeneralInstrumentation", "description": "Configure general SemConv options that may apply to multiple languages and instrumentations.\nInstrumenation may merge general config options with the language specific configuration at .instrumentation..\nIf omitted, default values as described in ExperimentalGeneralInstrumentation are used.\n" }, "cpp": { "$ref": "#/$defs/ExperimentalLanguageSpecificInstrumentation", "description": "Configure C++ language-specific instrumentation libraries.\nIf omitted, instrumentation defaults are used.\n" }, "dotnet": { "$ref": "#/$defs/ExperimentalLanguageSpecificInstrumentation", "description": "Configure .NET language-specific instrumentation libraries.\nEach entry's key identifies a particular instrumentation library. The corresponding value configures it.\nIf omitted, instrumentation defaults are used.\n" }, "erlang": { "$ref": "#/$defs/ExperimentalLanguageSpecificInstrumentation", "description": "Configure Erlang language-specific instrumentation libraries.\nEach entry's key identifies a particular instrumentation library. The corresponding value configures it.\nIf omitted, instrumentation defaults are used.\n" }, "go": { "$ref": "#/$defs/ExperimentalLanguageSpecificInstrumentation", "description": "Configure Go language-specific instrumentation libraries.\nEach entry's key identifies a particular instrumentation library. The corresponding value configures it.\nIf omitted, instrumentation defaults are used.\n" }, "java": { "$ref": "#/$defs/ExperimentalLanguageSpecificInstrumentation", "description": "Configure Java language-specific instrumentation libraries.\nEach entry's key identifies a particular instrumentation library. The corresponding value configures it.\nIf omitted, instrumentation defaults are used.\n" }, "js": { "$ref": "#/$defs/ExperimentalLanguageSpecificInstrumentation", "description": "Configure JavaScript language-specific instrumentation libraries.\nEach entry's key identifies a particular instrumentation library. The corresponding value configures it.\nIf omitted, instrumentation defaults are used.\n" }, "php": { "$ref": "#/$defs/ExperimentalLanguageSpecificInstrumentation", "description": "Configure PHP language-specific instrumentation libraries.\nEach entry's key identifies a particular instrumentation library. The corresponding value configures it.\nIf omitted, instrumentation defaults are used.\n" }, "python": { "$ref": "#/$defs/ExperimentalLanguageSpecificInstrumentation", "description": "Configure Python language-specific instrumentation libraries.\nEach entry's key identifies a particular instrumentation library. The corresponding value configures it.\nIf omitted, instrumentation defaults are used.\n" }, "ruby": { "$ref": "#/$defs/ExperimentalLanguageSpecificInstrumentation", "description": "Configure Ruby language-specific instrumentation libraries.\nEach entry's key identifies a particular instrumentation library. The corresponding value configures it.\nIf omitted, instrumentation defaults are used.\n" }, "rust": { "$ref": "#/$defs/ExperimentalLanguageSpecificInstrumentation", "description": "Configure Rust language-specific instrumentation libraries.\nEach entry's key identifies a particular instrumentation library. The corresponding value configures it.\nIf omitted, instrumentation defaults are used.\n" }, "swift": { "$ref": "#/$defs/ExperimentalLanguageSpecificInstrumentation", "description": "Configure Swift language-specific instrumentation libraries.\nEach entry's key identifies a particular instrumentation library. The corresponding value configures it.\nIf omitted, instrumentation defaults are used.\n" } } }, "ExperimentalJaegerRemoteSampler": { "type": [ "object", "null" ], "additionalProperties": false, "properties": { "endpoint": { "type": [ "string" ], "description": "Configure the endpoint of the jaeger remote sampling service.\nProperty is required and must be non-null.\n" }, "interval": { "type": [ "integer", "null" ], "minimum": 0, "description": "Configure the polling interval (in milliseconds) to fetch from the remote sampling service.\nIf omitted or null, 60000 is used.\n" }, "initial_sampler": { "$ref": "#/$defs/Sampler", "description": "Configure the initial sampler used before first configuration is fetched.\nProperty is required and must be non-null.\n" } }, "required": [ "endpoint", "initial_sampler" ] }, "ExperimentalLanguageSpecificInstrumentation": { "type": "object", "additionalProperties": { "type": "object" } }, "ExperimentalLoggerConfig": { "type": [ "object" ], "additionalProperties": false, "properties": { "enabled": { "type": [ "boolean", "null" ], "description": "Configure if the logger is enabled or not.\nIf omitted or null, true is used.\n" }, "minimum_severity": { "$ref": "#/$defs/SeverityNumber", "description": "Configure severity filtering.\nLog records with an non-zero (i.e. unspecified) severity number which is less than minimum_severity are not processed.\nValues include:\n* debug: debug, severity number 5.\n* debug2: debug2, severity number 6.\n* debug3: debug3, severity number 7.\n* debug4: debug4, severity number 8.\n* error: error, severity number 17.\n* error2: error2, severity number 18.\n* error3: error3, severity number 19.\n* error4: error4, severity number 20.\n* fatal: fatal, severity number 21.\n* fatal2: fatal2, severity number 22.\n* fatal3: fatal3, severity number 23.\n* fatal4: fatal4, severity number 24.\n* info: info, severity number 9.\n* info2: info2, severity number 10.\n* info3: info3, severity number 11.\n* info4: info4, severity number 12.\n* trace: trace, severity number 1.\n* trace2: trace2, severity number 2.\n* trace3: trace3, severity number 3.\n* trace4: trace4, severity number 4.\n* warn: warn, severity number 13.\n* warn2: warn2, severity number 14.\n* warn3: warn3, severity number 15.\n* warn4: warn4, severity number 16.\nIf omitted, severity filtering is not applied.\n" }, "trace_based": { "type": [ "boolean", "null" ], "description": "Configure trace based filtering.\nIf true, log records associated with unsampled trace contexts traces are not processed. If false, or if a log record is not associated with a trace context, trace based filtering is not applied.\nIf omitted or null, trace based filtering is not applied.\n" } } }, "ExperimentalLoggerConfigurator": { "type": [ "object" ], "additionalProperties": false, "properties": { "default_config": { "$ref": "#/$defs/ExperimentalLoggerConfig", "description": "Configure the default logger config used there is no matching entry in .logger_configurator/development.loggers.\nIf omitted, unmatched .loggers use default values as described in ExperimentalLoggerConfig.\n" }, "loggers": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/ExperimentalLoggerMatcherAndConfig" }, "description": "Configure loggers.\nIf omitted, all loggers use .default_config.\n" } } }, "ExperimentalLoggerMatcherAndConfig": { "type": [ "object" ], "additionalProperties": false, "properties": { "name": { "type": [ "string" ], "description": "Configure logger names to match. Matching is case-sensitive, evaluated as follows:\n\n * If the logger name exactly matches.\n * If the logger name matches the wildcard pattern, where '?' matches any single character and '*' matches any number of characters including none.\nProperty is required and must be non-null.\n" }, "config": { "$ref": "#/$defs/ExperimentalLoggerConfig", "description": "The logger config.\nProperty is required and must be non-null.\n" } }, "required": [ "name", "config" ] }, "ExperimentalMessagingInstrumentation": { "type": "object", "additionalProperties": false, "properties": { "semconv": { "$ref": "#/$defs/ExperimentalSemconvConfig", "description": "Configure messaging semantic convention version and migration behavior.\n\nThis property takes precedence over the .instrumentation/development.general.stability_opt_in_list setting.\n\nSee messaging semantic conventions: https://opentelemetry.io/docs/specs/semconv/messaging/\nIf omitted, uses the general stability_opt_in_list setting, or instrumentations continue emitting their default semantic convention version if not set.\n" } } }, "ExperimentalMeterConfig": { "type": [ "object" ], "additionalProperties": false, "properties": { "enabled": { "type": [ "boolean" ], "description": "Configure if the meter is enabled or not.\nIf omitted, true is used.\n" } } }, "ExperimentalMeterConfigurator": { "type": [ "object" ], "additionalProperties": false, "properties": { "default_config": { "$ref": "#/$defs/ExperimentalMeterConfig", "description": "Configure the default meter config used there is no matching entry in .meter_configurator/development.meters.\nIf omitted, unmatched .meters use default values as described in ExperimentalMeterConfig.\n" }, "meters": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/ExperimentalMeterMatcherAndConfig" }, "description": "Configure meters.\nIf omitted, all meters used .default_config.\n" } } }, "ExperimentalMeterMatcherAndConfig": { "type": [ "object" ], "additionalProperties": false, "properties": { "name": { "type": [ "string" ], "description": "Configure meter names to match. Matching is case-sensitive, evaluated as follows:\n\n * If the meter name exactly matches.\n * If the meter name matches the wildcard pattern, where '?' matches any single character and '*' matches any number of characters including none.\nProperty is required and must be non-null.\n" }, "config": { "$ref": "#/$defs/ExperimentalMeterConfig", "description": "The meter config.\nProperty is required and must be non-null.\n" } }, "required": [ "name", "config" ] }, "ExperimentalOtlpFileExporter": { "type": [ "object", "null" ], "additionalProperties": false, "properties": { "output_stream": { "type": [ "string", "null" ], "description": "Configure output stream. \nValues include stdout, or scheme+destination. For example: file:///path/to/file.jsonl.\nIf omitted or null, stdout is used.\n" } } }, "ExperimentalOtlpFileMetricExporter": { "type": [ "object", "null" ], "additionalProperties": false, "properties": { "output_stream": { "type": [ "string", "null" ], "description": "Configure output stream. \nValues include stdout, or scheme+destination. For example: file:///path/to/file.jsonl.\nIf omitted or null, stdout is used.\n" }, "temporality_preference": { "$ref": "#/$defs/ExporterTemporalityPreference", "description": "Configure temporality preference.\nValues include:\n* cumulative: Use cumulative aggregation temporality for all instrument types.\n* delta: Use delta aggregation for all instrument types except up down counter and asynchronous up down counter.\n* low_memory: Use delta aggregation temporality for counter and histogram instrument types. Use cumulative aggregation temporality for all other instrument types.\nIf omitted, cumulative is used.\n" }, "default_histogram_aggregation": { "$ref": "#/$defs/ExporterDefaultHistogramAggregation", "description": "Configure default histogram aggregation.\nValues include:\n* base2_exponential_bucket_histogram: Use base2 exponential histogram as the default aggregation for histogram instruments.\n* explicit_bucket_histogram: Use explicit bucket histogram as the default aggregation for histogram instruments.\nIf omitted, explicit_bucket_histogram is used.\n" } } }, "ExperimentalProbabilitySampler": { "type": [ "object", "null" ], "additionalProperties": false, "properties": { "ratio": { "type": [ "number", "null" ], "minimum": 0, "maximum": 1, "description": "Configure ratio.\nIf omitted or null, 1.0 is used.\n" } } }, "ExperimentalProcessResourceDetector": { "type": [ "object", "null" ], "additionalProperties": false }, "ExperimentalPrometheusMetricExporter": { "type": [ "object", "null" ], "additionalProperties": false, "properties": { "host": { "type": [ "string", "null" ], "description": "Configure host.\nIf omitted or null, localhost is used.\n" }, "port": { "type": [ "integer", "null" ], "description": "Configure port.\nIf omitted or null, 9464 is used.\n" }, "scope_info_enabled": { "type": [ "boolean", "null" ], "description": "Configure Prometheus Exporter to produce metrics with scope labels.\nIf omitted or null, true is used.\n" }, "target_info_enabled/development": { "type": [ "boolean", "null" ], "description": "Configure Prometheus Exporter to produce metrics with a target info metric for the resource.\nIf omitted or null, true is used.\n" }, "resource_constant_labels": { "$ref": "#/$defs/IncludeExclude", "description": "Configure Prometheus Exporter to add resource attributes as metrics attributes, where the resource attribute keys match the patterns.\nIf omitted, no resource attributes are added.\n" }, "translation_strategy": { "$ref": "#/$defs/ExperimentalPrometheusTranslationStrategy", "description": "Configure how metric names are translated to Prometheus metric names.\nValues include:\n* no_translation/development: Special character escaping is disabled. Type and unit suffixes are disabled. Metric names are unaltered.\n* no_utf8_escaping_with_suffixes/development: Special character escaping is disabled. Type and unit suffixes are enabled.\n* underscore_escaping_with_suffixes: Special character escaping is enabled. Type and unit suffixes are enabled.\n* underscore_escaping_without_suffixes/development: Special character escaping is enabled. Type and unit suffixes are disabled. This represents classic Prometheus metric name compatibility.\nIf omitted, underscore_escaping_with_suffixes is used.\n" } } }, "ExperimentalPrometheusTranslationStrategy": { "type": [ "string", "null" ], "enum": [ "underscore_escaping_with_suffixes", "underscore_escaping_without_suffixes/development", "no_utf8_escaping_with_suffixes/development", "no_translation/development" ] }, "ExperimentalResourceDetection": { "type": "object", "additionalProperties": false, "properties": { "attributes": { "$ref": "#/$defs/IncludeExclude", "description": "Configure attributes provided by resource detectors.\nIf omitted, all attributes from resource detectors are added.\n" }, "detectors": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/ExperimentalResourceDetector" }, "description": "Configure resource detectors.\nResource detector names are dependent on the SDK language ecosystem. Please consult documentation for each respective language. \nIf omitted, no resource detectors are enabled.\n" } } }, "ExperimentalResourceDetector": { "type": "object", "additionalProperties": { "type": [ "object", "null" ] }, "minProperties": 1, "maxProperties": 1, "properties": { "container": { "$ref": "#/$defs/ExperimentalContainerResourceDetector", "description": "Enable the container resource detector, which populates container.* attributes.\nIf omitted, ignore.\n" }, "host": { "$ref": "#/$defs/ExperimentalHostResourceDetector", "description": "Enable the host resource detector, which populates host.* and os.* attributes.\nIf omitted, ignore.\n" }, "process": { "$ref": "#/$defs/ExperimentalProcessResourceDetector", "description": "Enable the process resource detector, which populates process.* attributes.\nIf omitted, ignore.\n" }, "service": { "$ref": "#/$defs/ExperimentalServiceResourceDetector", "description": "Enable the service detector, which populates service.name based on the OTEL_SERVICE_NAME environment variable and service.instance.id.\nIf omitted, ignore.\n" } } }, "ExperimentalRpcInstrumentation": { "type": "object", "additionalProperties": false, "properties": { "semconv": { "$ref": "#/$defs/ExperimentalSemconvConfig", "description": "Configure RPC semantic convention version and migration behavior.\n\nThis property takes precedence over the .instrumentation/development.general.stability_opt_in_list setting.\n\nSee RPC semantic conventions: https://opentelemetry.io/docs/specs/semconv/rpc/\nIf omitted, uses the general stability_opt_in_list setting, or instrumentations continue emitting their default semantic convention version if not set.\n" } } }, "ExperimentalSanitization": { "type": "object", "additionalProperties": false, "properties": { "url": { "$ref": "#/$defs/ExperimentalUrlSanitization", "description": "Configure URL sanitization options.\nIf omitted, defaults as described in ExperimentalUrlSanitization are used.\n" } } }, "ExperimentalSemconvConfig": { "type": "object", "additionalProperties": false, "properties": { "version": { "type": [ "integer", "null" ], "minimum": 0, "description": "The target semantic convention version for this domain (e.g., 1).\nIf omitted or null, the latest stable version is used, or if no stable version is available and .experimental is true then the latest experimental version is used.\n" }, "experimental": { "type": [ "boolean", "null" ], "description": "Use latest experimental semantic conventions (before stable is available or to enable experimental features on top of stable conventions).\nIf omitted or null, false is used.\n" }, "dual_emit": { "type": [ "boolean", "null" ], "description": "When true, also emit the previous major version alongside the target version.\nFor version=1, the previous version refers to the pre-stable conventions that the instrumentation emitted before the first stable semantic convention version was defined.\nFor version=2 and above, the previous version is the prior stable major version (e.g., version=2, dual_emit=true emits both v2 and v1).\nEnables dual-emit for phased migration between versions.\nIf omitted or null, false is used.\n" } } }, "ExperimentalServiceResourceDetector": { "type": [ "object", "null" ], "additionalProperties": false }, "ExperimentalSpanParent": { "type": [ "string", "null" ], "enum": [ "none", "remote", "local" ] }, "ExperimentalTracerConfig": { "type": [ "object" ], "additionalProperties": false, "properties": { "enabled": { "type": [ "boolean" ], "description": "Configure if the tracer is enabled or not.\nIf omitted, true is used.\n" } } }, "ExperimentalTracerConfigurator": { "type": [ "object" ], "additionalProperties": false, "properties": { "default_config": { "$ref": "#/$defs/ExperimentalTracerConfig", "description": "Configure the default tracer config used there is no matching entry in .tracer_configurator/development.tracers.\nIf omitted, unmatched .tracers use default values as described in ExperimentalTracerConfig.\n" }, "tracers": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/ExperimentalTracerMatcherAndConfig" }, "description": "Configure tracers.\nIf omitted, all tracers use .default_config.\n" } } }, "ExperimentalTracerMatcherAndConfig": { "type": [ "object" ], "additionalProperties": false, "properties": { "name": { "type": [ "string" ], "description": "Configure tracer names to match. Matching is case-sensitive, evaluated as follows:\n\n * If the tracer name exactly matches.\n * If the tracer name matches the wildcard pattern, where '?' matches any single character and '*' matches any number of characters including none.\nProperty is required and must be non-null.\n" }, "config": { "$ref": "#/$defs/ExperimentalTracerConfig", "description": "The tracer config.\nProperty is required and must be non-null.\n" } }, "required": [ "name", "config" ] }, "ExperimentalUrlSanitization": { "type": "object", "additionalProperties": false, "properties": { "sensitive_query_parameters": { "type": "array", "minItems": 0, "items": { "type": "string" }, "description": "List of query parameter names whose values should be redacted from URLs.\nQuery parameter names are case-sensitive.\nThis is a full override of the default sensitive query parameter keys, it is not a list of keys in addition to the defaults.\nSet to an empty array to disable query parameter redaction.\nIf omitted, the default sensitive query parameter list as defined by the url semantic conventions (https://github.com/open-telemetry/semantic-conventions/blob/main/docs/registry/attributes/url.md) is used.\n" } } }, "ExplicitBucketHistogramAggregation": { "type": [ "object", "null" ], "additionalProperties": false, "properties": { "boundaries": { "type": "array", "minItems": 0, "items": { "type": "number" }, "description": "Configure bucket boundaries.\nIf omitted, [0, 5, 10, 25, 50, 75, 100, 250, 500, 750, 1000, 2500, 5000, 7500, 10000] is used.\n" }, "record_min_max": { "type": [ "boolean", "null" ], "description": "Configure record min and max.\nIf omitted or null, true is used.\n" } } }, "ExporterDefaultHistogramAggregation": { "type": [ "string", "null" ], "enum": [ "explicit_bucket_histogram", "base2_exponential_bucket_histogram" ] }, "ExporterTemporalityPreference": { "type": [ "string", "null" ], "enum": [ "cumulative", "delta", "low_memory" ] }, "GrpcTls": { "type": [ "object", "null" ], "additionalProperties": false, "properties": { "ca_file": { "type": [ "string", "null" ], "description": "Configure certificate used to verify a server's TLS credentials. \nAbsolute path to certificate file in PEM format.\nIf omitted or null, system default certificate verification is used for secure connections.\n" }, "key_file": { "type": [ "string", "null" ], "description": "Configure mTLS private client key. \nAbsolute path to client key file in PEM format. If set, .client_certificate must also be set.\nIf omitted or null, mTLS is not used.\n" }, "cert_file": { "type": [ "string", "null" ], "description": "Configure mTLS client certificate. \nAbsolute path to client certificate file in PEM format. If set, .client_key must also be set.\nIf omitted or null, mTLS is not used.\n" }, "insecure": { "type": [ "boolean", "null" ], "description": "Configure client transport security for the exporter's connection. \nOnly applicable when .endpoint is provided without http or https scheme. Implementations may choose to ignore .insecure.\nIf omitted or null, false is used.\n" } } }, "HttpTls": { "type": [ "object", "null" ], "additionalProperties": false, "properties": { "ca_file": { "type": [ "string", "null" ], "description": "Configure certificate used to verify a server's TLS credentials. \nAbsolute path to certificate file in PEM format.\nIf omitted or null, system default certificate verification is used for secure connections.\n" }, "key_file": { "type": [ "string", "null" ], "description": "Configure mTLS private client key. \nAbsolute path to client key file in PEM format. If set, .client_certificate must also be set.\nIf omitted or null, mTLS is not used.\n" }, "cert_file": { "type": [ "string", "null" ], "description": "Configure mTLS client certificate. \nAbsolute path to client certificate file in PEM format. If set, .client_key must also be set.\nIf omitted or null, mTLS is not used.\n" } } }, "IdGenerator": { "type": "object", "additionalProperties": { "type": [ "object", "null" ] }, "minProperties": 1, "maxProperties": 1, "properties": { "random": { "$ref": "#/$defs/RandomIdGenerator", "description": "Configure the ID generator to randomly generate TraceIds and SpanIds (spec default).\nIf omitted, ignore.\n" } } }, "IncludeExclude": { "type": "object", "additionalProperties": false, "properties": { "included": { "type": "array", "minItems": 1, "items": { "type": "string" }, "description": "Configure list of value patterns to include.\nMatching is case-sensitive. Values are evaluated to match as follows:\n * If the value exactly matches.\n * If the value matches the wildcard pattern, where '?' matches any single character and '*' matches any number of characters including none.\nIf omitted, all values are included.\n" }, "excluded": { "type": "array", "minItems": 1, "items": { "type": "string" }, "description": "Configure list of value patterns to exclude. Applies after .included (i.e. excluded has higher priority than included).\nMatching is case-sensitive. Values are evaluated to match as follows:\n * If the value exactly matches.\n * If the value matches the wildcard pattern, where '?' matches any single character and '*' matches any number of characters including none.\nIf omitted, .included attributes are included.\n" } } }, "InstrumentType": { "type": [ "string", "null" ], "enum": [ "counter", "gauge", "histogram", "observable_counter", "observable_gauge", "observable_up_down_counter", "up_down_counter" ] }, "LastValueAggregation": { "type": [ "object", "null" ], "additionalProperties": false }, "LoggerProvider": { "type": "object", "additionalProperties": false, "properties": { "processors": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/LogRecordProcessor" }, "description": "Configure log record processors.\nProperty is required and must be non-null.\n" }, "limits": { "$ref": "#/$defs/LogRecordLimits", "description": "Configure log record limits. See also attribute_limits.\nIf omitted, default values as described in LogRecordLimits are used.\n" }, "logger_configurator/development": { "$ref": "#/$defs/ExperimentalLoggerConfigurator", "description": "Configure loggers.\nIf omitted, all loggers use default values as described in ExperimentalLoggerConfig.\n" } }, "required": [ "processors" ] }, "LogRecordExporter": { "type": "object", "additionalProperties": { "type": [ "object", "null" ] }, "minProperties": 1, "maxProperties": 1, "properties": { "otlp_http": { "$ref": "#/$defs/OtlpHttpExporter", "description": "Configure exporter to be OTLP with HTTP transport.\nIf omitted, ignore.\n" }, "otlp_grpc": { "$ref": "#/$defs/OtlpGrpcExporter", "description": "Configure exporter to be OTLP with gRPC transport.\nIf omitted, ignore.\n" }, "otlp_file/development": { "$ref": "#/$defs/ExperimentalOtlpFileExporter", "description": "Configure exporter to be OTLP with file transport.\nIf omitted, ignore.\n" }, "console": { "$ref": "#/$defs/ConsoleExporter", "description": "Configure exporter to be console.\nIf omitted, ignore.\n" } } }, "LogRecordLimits": { "type": "object", "additionalProperties": false, "properties": { "attribute_value_length_limit": { "type": [ "integer", "null" ], "minimum": 0, "description": "Configure max attribute value size. Overrides .attribute_limits.attribute_value_length_limit. \nValue must be non-negative.\nIf omitted or null, there is no limit.\n" }, "attribute_count_limit": { "type": [ "integer", "null" ], "minimum": 0, "description": "Configure max attribute count. Overrides .attribute_limits.attribute_count_limit. \nValue must be non-negative.\nIf omitted or null, 128 is used.\n" } } }, "LogRecordProcessor": { "type": "object", "additionalProperties": { "type": [ "object", "null" ] }, "minProperties": 1, "maxProperties": 1, "properties": { "batch": { "$ref": "#/$defs/BatchLogRecordProcessor", "description": "Configure a batch log record processor.\nIf omitted, ignore.\n" }, "simple": { "$ref": "#/$defs/SimpleLogRecordProcessor", "description": "Configure a simple log record processor.\nIf omitted, ignore.\n" }, "event_to_span_event_bridge/development": { "$ref": "#/$defs/ExperimentalEventToSpanEventBridgeLogRecordProcessor", "description": "Configure an event to span event bridge log record processor.\nIf omitted, ignore.\n" } } }, "MeterProvider": { "type": "object", "additionalProperties": false, "properties": { "readers": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/MetricReader" }, "description": "Configure metric readers.\nProperty is required and must be non-null.\n" }, "views": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/View" }, "description": "Configure views. \nEach view has a selector which determines the instrument(s) it applies to, and a configuration for the resulting stream(s).\nIf omitted, no views are registered.\n" }, "exemplar_filter": { "$ref": "#/$defs/ExemplarFilter", "description": "Configure the exemplar filter.\nValues include:\n* always_off: ExemplarFilter which makes no measurements eligible for being an Exemplar.\n* always_on: ExemplarFilter which makes all measurements eligible for being an Exemplar.\n* trace_based: ExemplarFilter which makes measurements recorded in the context of a sampled parent span eligible for being an Exemplar.\nIf omitted, trace_based is used.\n" }, "meter_configurator/development": { "$ref": "#/$defs/ExperimentalMeterConfigurator", "description": "Configure meters.\nIf omitted, all meters use default values as described in ExperimentalMeterConfig.\n" } }, "required": [ "readers" ] }, "MetricProducer": { "type": "object", "additionalProperties": { "type": [ "object", "null" ] }, "minProperties": 1, "maxProperties": 1, "properties": { "opencensus": { "$ref": "#/$defs/OpenCensusMetricProducer", "description": "Configure metric producer to be opencensus.\nIf omitted, ignore.\n" } } }, "MetricReader": { "type": "object", "additionalProperties": false, "minProperties": 1, "maxProperties": 1, "properties": { "periodic": { "$ref": "#/$defs/PeriodicMetricReader", "description": "Configure a periodic metric reader.\nIf omitted, ignore.\n" }, "pull": { "$ref": "#/$defs/PullMetricReader", "description": "Configure a pull based metric reader.\nIf omitted, ignore.\n" } } }, "NameStringValuePair": { "type": "object", "additionalProperties": false, "properties": { "name": { "type": "string", "description": "The name of the pair.\nProperty is required and must be non-null.\n" }, "value": { "type": [ "string", "null" ], "description": "The value of the pair.\nProperty must be present, but if null the behavior is dependent on usage context.\n" } }, "required": [ "name", "value" ] }, "OpenCensusMetricProducer": { "type": [ "object", "null" ], "additionalProperties": false }, "OtlpGrpcExporter": { "type": [ "object", "null" ], "additionalProperties": false, "properties": { "endpoint": { "type": [ "string", "null" ], "description": "Configure endpoint.\nIf omitted or null, http://localhost:4317 is used.\n" }, "tls": { "$ref": "#/$defs/GrpcTls", "description": "Configure TLS settings for the exporter.\nIf omitted, system default TLS settings are used.\n" }, "headers": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/NameStringValuePair" }, "description": "Configure headers. Entries have higher priority than entries from .headers_list.\nIf an entry's .value is null, the entry is ignored.\nIf omitted, no headers are added.\n" }, "headers_list": { "type": [ "string", "null" ], "description": "Configure headers. Entries have lower priority than entries from .headers.\nThe value is a list of comma separated key-value pairs matching the format of OTEL_EXPORTER_OTLP_HEADERS. See https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/protocol/exporter.md#configuration-options for details.\nIf omitted or null, no headers are added.\n" }, "compression": { "type": [ "string", "null" ], "description": "Configure compression.\nKnown values include: gzip, none. Implementations may support other compression algorithms.\nIf omitted or null, none is used.\n" }, "timeout": { "type": [ "integer", "null" ], "minimum": 0, "description": "Configure max time (in milliseconds) to wait for each export.\nValue must be non-negative. A value of 0 indicates no limit (infinity).\nIf omitted or null, 10000 is used.\n" } } }, "OtlpGrpcMetricExporter": { "type": [ "object", "null" ], "additionalProperties": false, "properties": { "endpoint": { "type": [ "string", "null" ], "description": "Configure endpoint.\nIf omitted or null, http://localhost:4317 is used.\n" }, "tls": { "$ref": "#/$defs/GrpcTls", "description": "Configure TLS settings for the exporter.\nIf omitted, system default TLS settings are used.\n" }, "headers": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/NameStringValuePair" }, "description": "Configure headers. Entries have higher priority than entries from .headers_list.\nIf an entry's .value is null, the entry is ignored.\nIf omitted, no headers are added.\n" }, "headers_list": { "type": [ "string", "null" ], "description": "Configure headers. Entries have lower priority than entries from .headers.\nThe value is a list of comma separated key-value pairs matching the format of OTEL_EXPORTER_OTLP_HEADERS. See https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/protocol/exporter.md#configuration-options for details.\nIf omitted or null, no headers are added.\n" }, "compression": { "type": [ "string", "null" ], "description": "Configure compression.\nKnown values include: gzip, none. Implementations may support other compression algorithms.\nIf omitted or null, none is used.\n" }, "timeout": { "type": [ "integer", "null" ], "minimum": 0, "description": "Configure max time (in milliseconds) to wait for each export.\nValue must be non-negative. A value of 0 indicates no limit (infinity).\nIf omitted or null, 10000 is used.\n" }, "temporality_preference": { "$ref": "#/$defs/ExporterTemporalityPreference", "description": "Configure temporality preference.\nValues include:\n* cumulative: Use cumulative aggregation temporality for all instrument types.\n* delta: Use delta aggregation for all instrument types except up down counter and asynchronous up down counter.\n* low_memory: Use delta aggregation temporality for counter and histogram instrument types. Use cumulative aggregation temporality for all other instrument types.\nIf omitted, cumulative is used.\n" }, "default_histogram_aggregation": { "$ref": "#/$defs/ExporterDefaultHistogramAggregation", "description": "Configure default histogram aggregation.\nValues include:\n* base2_exponential_bucket_histogram: Use base2 exponential histogram as the default aggregation for histogram instruments.\n* explicit_bucket_histogram: Use explicit bucket histogram as the default aggregation for histogram instruments.\nIf omitted, explicit_bucket_histogram is used.\n" } } }, "OtlpHttpEncoding": { "type": [ "string", "null" ], "enum": [ "protobuf", "json" ] }, "OtlpHttpExporter": { "type": [ "object", "null" ], "additionalProperties": false, "properties": { "endpoint": { "type": [ "string", "null" ], "description": "Configure endpoint, including the signal specific path.\nIf omitted or null, the http://localhost:4318/v1/{signal} (where signal is 'traces', 'logs', or 'metrics') is used.\n" }, "tls": { "$ref": "#/$defs/HttpTls", "description": "Configure TLS settings for the exporter.\nIf omitted, system default TLS settings are used.\n" }, "headers": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/NameStringValuePair" }, "description": "Configure headers. Entries have higher priority than entries from .headers_list.\nIf an entry's .value is null, the entry is ignored.\nIf omitted, no headers are added.\n" }, "headers_list": { "type": [ "string", "null" ], "description": "Configure headers. Entries have lower priority than entries from .headers.\nThe value is a list of comma separated key-value pairs matching the format of OTEL_EXPORTER_OTLP_HEADERS. See https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/protocol/exporter.md#configuration-options for details.\nIf omitted or null, no headers are added.\n" }, "compression": { "type": [ "string", "null" ], "description": "Configure compression.\nKnown values include: gzip, none. Implementations may support other compression algorithms.\nIf omitted or null, none is used.\n" }, "timeout": { "type": [ "integer", "null" ], "minimum": 0, "description": "Configure max time (in milliseconds) to wait for each export.\nValue must be non-negative. A value of 0 indicates no limit (infinity).\nIf omitted or null, 10000 is used.\n" }, "encoding": { "$ref": "#/$defs/OtlpHttpEncoding", "description": "Configure the encoding used for messages. \nImplementations may not support json.\nValues include:\n* json: Protobuf JSON encoding.\n* protobuf: Protobuf binary encoding.\nIf omitted, protobuf is used.\n" } } }, "OtlpHttpMetricExporter": { "type": [ "object", "null" ], "additionalProperties": false, "properties": { "endpoint": { "type": [ "string", "null" ], "description": "Configure endpoint.\nIf omitted or null, http://localhost:4318/v1/metrics is used.\n" }, "tls": { "$ref": "#/$defs/HttpTls", "description": "Configure TLS settings for the exporter.\nIf omitted, system default TLS settings are used.\n" }, "headers": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/NameStringValuePair" }, "description": "Configure headers. Entries have higher priority than entries from .headers_list.\nIf an entry's .value is null, the entry is ignored.\nIf omitted, no headers are added.\n" }, "headers_list": { "type": [ "string", "null" ], "description": "Configure headers. Entries have lower priority than entries from .headers.\nThe value is a list of comma separated key-value pairs matching the format of OTEL_EXPORTER_OTLP_HEADERS. See https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/protocol/exporter.md#configuration-options for details.\nIf omitted or null, no headers are added.\n" }, "compression": { "type": [ "string", "null" ], "description": "Configure compression.\nKnown values include: gzip, none. Implementations may support other compression algorithms.\nIf omitted or null, none is used.\n" }, "timeout": { "type": [ "integer", "null" ], "minimum": 0, "description": "Configure max time (in milliseconds) to wait for each export.\nValue must be non-negative. A value of 0 indicates no limit (infinity).\nIf omitted or null, 10000 is used.\n" }, "encoding": { "$ref": "#/$defs/OtlpHttpEncoding", "description": "Configure the encoding used for messages. \nImplementations may not support json.\nValues include:\n* json: Protobuf JSON encoding.\n* protobuf: Protobuf binary encoding.\nIf omitted, protobuf is used.\n" }, "temporality_preference": { "$ref": "#/$defs/ExporterTemporalityPreference", "description": "Configure temporality preference.\nValues include:\n* cumulative: Use cumulative aggregation temporality for all instrument types.\n* delta: Use delta aggregation for all instrument types except up down counter and asynchronous up down counter.\n* low_memory: Use delta aggregation temporality for counter and histogram instrument types. Use cumulative aggregation temporality for all other instrument types.\nIf omitted, cumulative is used.\n" }, "default_histogram_aggregation": { "$ref": "#/$defs/ExporterDefaultHistogramAggregation", "description": "Configure default histogram aggregation.\nValues include:\n* base2_exponential_bucket_histogram: Use base2 exponential histogram as the default aggregation for histogram instruments.\n* explicit_bucket_histogram: Use explicit bucket histogram as the default aggregation for histogram instruments.\nIf omitted, explicit_bucket_histogram is used.\n" } } }, "ParentBasedSampler": { "type": [ "object", "null" ], "additionalProperties": false, "properties": { "root": { "$ref": "#/$defs/Sampler", "description": "Configure root sampler.\nIf omitted, always_on is used.\n" }, "remote_parent_sampled": { "$ref": "#/$defs/Sampler", "description": "Configure remote_parent_sampled sampler.\nIf omitted, always_on is used.\n" }, "remote_parent_not_sampled": { "$ref": "#/$defs/Sampler", "description": "Configure remote_parent_not_sampled sampler.\nIf omitted, always_off is used.\n" }, "local_parent_sampled": { "$ref": "#/$defs/Sampler", "description": "Configure local_parent_sampled sampler.\nIf omitted, always_on is used.\n" }, "local_parent_not_sampled": { "$ref": "#/$defs/Sampler", "description": "Configure local_parent_not_sampled sampler.\nIf omitted, always_off is used.\n" } } }, "PeriodicMetricReader": { "type": "object", "additionalProperties": false, "properties": { "interval": { "type": [ "integer", "null" ], "minimum": 0, "description": "Configure delay interval (in milliseconds) between start of two consecutive exports. \nValue must be non-negative.\nIf omitted or null, 60000 is used.\n" }, "timeout": { "type": [ "integer", "null" ], "minimum": 0, "description": "Configure maximum allowed time (in milliseconds) to export data. \nValue must be non-negative. A value of 0 indicates no limit (infinity).\nIf omitted or null, 30000 is used.\n" }, "max_export_batch_size/development": { "type": [ "integer", "null" ], "minimum": 1, "description": "Configure maximum export batch size.\nIf omitted or null, no limit is used.\n" }, "exporter": { "$ref": "#/$defs/PushMetricExporter", "description": "Configure exporter.\nProperty is required and must be non-null.\n" }, "producers": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/MetricProducer" }, "description": "Configure metric producers.\nIf omitted, no metric producers are added.\n" }, "cardinality_limits": { "$ref": "#/$defs/CardinalityLimits", "description": "Configure cardinality limits.\nIf omitted, default values as described in CardinalityLimits are used.\n" } }, "required": [ "exporter" ] }, "Propagator": { "type": "object", "additionalProperties": false, "properties": { "composite": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/TextMapPropagator" }, "description": "Configure the propagators in the composite text map propagator. Entries from .composite_list are appended to the list here with duplicates filtered out.\nBuilt-in propagator keys include: tracecontext, baggage, b3, b3multi. Known third party keys include: xray.\nIf omitted, and .composite_list is omitted or null, a noop propagator is used.\n" }, "composite_list": { "type": [ "string", "null" ], "description": "Configure the propagators in the composite text map propagator. Entries are appended to .composite with duplicates filtered out.\nThe value is a comma separated list of propagator identifiers matching the format of OTEL_PROPAGATORS. See https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/configuration/sdk-environment-variables.md#general-sdk-configuration for details.\nBuilt-in propagator identifiers include: tracecontext, baggage, b3, b3multi. Known third party identifiers include: xray.\nIf omitted or null, and .composite is omitted or null, a noop propagator is used.\n" } } }, "PullMetricExporter": { "type": "object", "additionalProperties": { "type": [ "object", "null" ] }, "minProperties": 1, "maxProperties": 1, "properties": { "prometheus/development": { "$ref": "#/$defs/ExperimentalPrometheusMetricExporter", "description": "Configure exporter to be prometheus.\nIf omitted, ignore.\n" } } }, "PullMetricReader": { "type": "object", "additionalProperties": false, "properties": { "exporter": { "$ref": "#/$defs/PullMetricExporter", "description": "Configure exporter.\nProperty is required and must be non-null.\n" }, "producers": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/MetricProducer" }, "description": "Configure metric producers.\nIf omitted, no metric producers are added.\n" }, "cardinality_limits": { "$ref": "#/$defs/CardinalityLimits", "description": "Configure cardinality limits.\nIf omitted, default values as described in CardinalityLimits are used.\n" } }, "required": [ "exporter" ] }, "PushMetricExporter": { "type": "object", "additionalProperties": { "type": [ "object", "null" ] }, "minProperties": 1, "maxProperties": 1, "properties": { "otlp_http": { "$ref": "#/$defs/OtlpHttpMetricExporter", "description": "Configure exporter to be OTLP with HTTP transport.\nIf omitted, ignore.\n" }, "otlp_grpc": { "$ref": "#/$defs/OtlpGrpcMetricExporter", "description": "Configure exporter to be OTLP with gRPC transport.\nIf omitted, ignore.\n" }, "otlp_file/development": { "$ref": "#/$defs/ExperimentalOtlpFileMetricExporter", "description": "Configure exporter to be OTLP with file transport.\nIf omitted, ignore.\n" }, "console": { "$ref": "#/$defs/ConsoleMetricExporter", "description": "Configure exporter to be console.\nIf omitted, ignore.\n" } } }, "RandomIdGenerator": { "type": [ "object", "null" ], "additionalProperties": false }, "Resource": { "type": "object", "additionalProperties": false, "properties": { "attributes": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/AttributeNameValue" }, "description": "Configure resource attributes. Entries have higher priority than entries from .resource.attributes_list.\nIf omitted, no resource attributes are added.\n" }, "detection/development": { "$ref": "#/$defs/ExperimentalResourceDetection", "description": "Configure resource detection.\nIf omitted, resource detection is disabled.\n" }, "schema_url": { "type": [ "string", "null" ], "description": "Configure resource schema URL.\nIf omitted or null, no schema URL is used.\n" }, "attributes_list": { "type": [ "string", "null" ], "description": "Configure resource attributes. Entries have lower priority than entries from .resource.attributes.\nThe value is a list of comma separated key-value pairs matching the format of OTEL_RESOURCE_ATTRIBUTES. See https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/configuration/sdk-environment-variables.md#general-sdk-configuration for details.\nIf omitted or null, no resource attributes are added.\n" } } }, "Sampler": { "type": "object", "additionalProperties": { "type": [ "object", "null" ] }, "minProperties": 1, "maxProperties": 1, "properties": { "always_off": { "$ref": "#/$defs/AlwaysOffSampler", "description": "Configure sampler to be always_off.\nIf omitted, ignore.\n" }, "always_on": { "$ref": "#/$defs/AlwaysOnSampler", "description": "Configure sampler to be always_on.\nIf omitted, ignore.\n" }, "composite/development": { "$ref": "#/$defs/ExperimentalComposableSampler", "description": "Configure sampler to be composite.\nIf omitted, ignore.\n" }, "jaeger_remote/development": { "$ref": "#/$defs/ExperimentalJaegerRemoteSampler", "description": "Configure sampler to be jaeger_remote.\nIf omitted, ignore.\n" }, "parent_based": { "$ref": "#/$defs/ParentBasedSampler", "description": "Configure sampler to be parent_based.\nIf omitted, ignore.\n" }, "probability/development": { "$ref": "#/$defs/ExperimentalProbabilitySampler", "description": "Configure sampler to be probability.\nIf omitted, ignore.\n" }, "trace_id_ratio_based": { "$ref": "#/$defs/TraceIdRatioBasedSampler", "description": "Configure sampler to be trace_id_ratio_based.\nIf omitted, ignore.\n" } } }, "SeverityNumber": { "type": [ "string", "null" ], "enum": [ "trace", "trace2", "trace3", "trace4", "debug", "debug2", "debug3", "debug4", "info", "info2", "info3", "info4", "warn", "warn2", "warn3", "warn4", "error", "error2", "error3", "error4", "fatal", "fatal2", "fatal3", "fatal4" ] }, "SimpleLogRecordProcessor": { "type": "object", "additionalProperties": false, "properties": { "exporter": { "$ref": "#/$defs/LogRecordExporter", "description": "Configure exporter.\nProperty is required and must be non-null.\n" } }, "required": [ "exporter" ] }, "SimpleSpanProcessor": { "type": "object", "additionalProperties": false, "properties": { "exporter": { "$ref": "#/$defs/SpanExporter", "description": "Configure exporter.\nProperty is required and must be non-null.\n" } }, "required": [ "exporter" ] }, "SpanExporter": { "type": "object", "additionalProperties": { "type": [ "object", "null" ] }, "minProperties": 1, "maxProperties": 1, "properties": { "otlp_http": { "$ref": "#/$defs/OtlpHttpExporter", "description": "Configure exporter to be OTLP with HTTP transport.\nIf omitted, ignore.\n" }, "otlp_grpc": { "$ref": "#/$defs/OtlpGrpcExporter", "description": "Configure exporter to be OTLP with gRPC transport.\nIf omitted, ignore.\n" }, "otlp_file/development": { "$ref": "#/$defs/ExperimentalOtlpFileExporter", "description": "Configure exporter to be OTLP with file transport.\nIf omitted, ignore.\n" }, "console": { "$ref": "#/$defs/ConsoleExporter", "description": "Configure exporter to be console.\nIf omitted, ignore.\n" } } }, "SpanKind": { "type": [ "string", "null" ], "enum": [ "internal", "server", "client", "producer", "consumer" ] }, "SpanLimits": { "type": "object", "additionalProperties": false, "properties": { "attribute_value_length_limit": { "type": [ "integer", "null" ], "minimum": 0, "description": "Configure max attribute value size. Overrides .attribute_limits.attribute_value_length_limit. \nValue must be non-negative.\nIf omitted or null, there is no limit.\n" }, "attribute_count_limit": { "type": [ "integer", "null" ], "minimum": 0, "description": "Configure max attribute count. Overrides .attribute_limits.attribute_count_limit. \nValue must be non-negative.\nIf omitted or null, 128 is used.\n" }, "event_count_limit": { "type": [ "integer", "null" ], "minimum": 0, "description": "Configure max span event count. \nValue must be non-negative.\nIf omitted or null, 128 is used.\n" }, "link_count_limit": { "type": [ "integer", "null" ], "minimum": 0, "description": "Configure max span link count. \nValue must be non-negative.\nIf omitted or null, 128 is used.\n" }, "event_attribute_count_limit": { "type": [ "integer", "null" ], "minimum": 0, "description": "Configure max attributes per span event. \nValue must be non-negative.\nIf omitted or null, 128 is used.\n" }, "link_attribute_count_limit": { "type": [ "integer", "null" ], "minimum": 0, "description": "Configure max attributes per span link. \nValue must be non-negative.\nIf omitted or null, 128 is used.\n" } } }, "SpanProcessor": { "type": "object", "additionalProperties": { "type": [ "object", "null" ] }, "minProperties": 1, "maxProperties": 1, "properties": { "batch": { "$ref": "#/$defs/BatchSpanProcessor", "description": "Configure a batch span processor.\nIf omitted, ignore.\n" }, "simple": { "$ref": "#/$defs/SimpleSpanProcessor", "description": "Configure a simple span processor.\nIf omitted, ignore.\n" } } }, "SumAggregation": { "type": [ "object", "null" ], "additionalProperties": false }, "TextMapPropagator": { "type": "object", "additionalProperties": { "type": [ "object", "null" ] }, "minProperties": 1, "maxProperties": 1, "properties": { "tracecontext": { "$ref": "#/$defs/TraceContextPropagator", "description": "Include the w3c trace context propagator.\nIf omitted, ignore.\n" }, "baggage": { "$ref": "#/$defs/BaggagePropagator", "description": "Include the w3c baggage propagator.\nIf omitted, ignore.\n" }, "b3": { "$ref": "#/$defs/B3Propagator", "description": "Include the zipkin b3 propagator.\nIf omitted, ignore.\n" }, "b3multi": { "$ref": "#/$defs/B3MultiPropagator", "description": "Include the zipkin b3 multi propagator.\nIf omitted, ignore.\n" } } }, "TraceContextPropagator": { "type": [ "object", "null" ], "additionalProperties": false }, "TraceIdRatioBasedSampler": { "type": [ "object", "null" ], "additionalProperties": false, "properties": { "ratio": { "type": [ "number", "null" ], "minimum": 0, "maximum": 1, "description": "Configure trace_id_ratio.\nIf omitted or null, 1.0 is used.\n" } } }, "TracerProvider": { "type": "object", "additionalProperties": false, "properties": { "processors": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/SpanProcessor" }, "description": "Configure span processors.\nProperty is required and must be non-null.\n" }, "limits": { "$ref": "#/$defs/SpanLimits", "description": "Configure span limits. See also attribute_limits.\nIf omitted, default values as described in SpanLimits are used.\n" }, "sampler": { "$ref": "#/$defs/Sampler", "description": "Configure the sampler.\nIf omitted, parent based sampler with a root of always_on is used.\n" }, "id_generator": { "$ref": "#/$defs/IdGenerator", "description": "Configure the trace and span ID generator.\nIf omitted, RandomIdGenerator is used.\n" }, "tracer_configurator/development": { "$ref": "#/$defs/ExperimentalTracerConfigurator", "description": "Configure tracers.\nIf omitted, all tracers use default values as described in ExperimentalTracerConfig.\n" } }, "required": [ "processors" ] }, "View": { "type": "object", "additionalProperties": false, "properties": { "selector": { "$ref": "#/$defs/ViewSelector", "description": "Configure view selector. \nSelection criteria is additive as described in https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk.md#instrument-selection-criteria.\nProperty is required and must be non-null.\n" }, "stream": { "$ref": "#/$defs/ViewStream", "description": "Configure view stream.\nProperty is required and must be non-null.\n" } }, "required": [ "selector", "stream" ] }, "ViewSelector": { "type": "object", "additionalProperties": false, "properties": { "instrument_name": { "type": [ "string", "null" ], "description": "Configure instrument name selection criteria.\nIf omitted or null, all instrument names match.\n" }, "instrument_type": { "$ref": "#/$defs/InstrumentType", "description": "Configure instrument type selection criteria.\nValues include:\n* counter: Synchronous counter instruments.\n* gauge: Synchronous gauge instruments.\n* histogram: Synchronous histogram instruments.\n* observable_counter: Asynchronous counter instruments.\n* observable_gauge: Asynchronous gauge instruments.\n* observable_up_down_counter: Asynchronous up down counter instruments.\n* up_down_counter: Synchronous up down counter instruments.\nIf omitted, all instrument types match.\n" }, "unit": { "type": [ "string", "null" ], "description": "Configure the instrument unit selection criteria.\nIf omitted or null, all instrument units match.\n" }, "meter_name": { "type": [ "string", "null" ], "description": "Configure meter name selection criteria.\nIf omitted or null, all meter names match.\n" }, "meter_version": { "type": [ "string", "null" ], "description": "Configure meter version selection criteria.\nIf omitted or null, all meter versions match.\n" }, "meter_schema_url": { "type": [ "string", "null" ], "description": "Configure meter schema url selection criteria.\nIf omitted or null, all meter schema URLs match.\n" } } }, "ViewStream": { "type": "object", "additionalProperties": false, "properties": { "name": { "type": [ "string", "null" ], "description": "Configure metric name of the resulting stream(s).\nIf omitted or null, the instrument's original name is used.\n" }, "description": { "type": [ "string", "null" ], "description": "Configure metric description of the resulting stream(s).\nIf omitted or null, the instrument's origin description is used.\n" }, "aggregation": { "$ref": "#/$defs/Aggregation", "description": "Configure aggregation of the resulting stream(s).\nIf omitted, default is used.\n" }, "aggregation_cardinality_limit": { "type": [ "integer", "null" ], "exclusiveMinimum": 0, "description": "Configure the aggregation cardinality limit.\nIf omitted or null, the metric reader's default cardinality limit is used.\n" }, "attribute_keys": { "$ref": "#/$defs/IncludeExclude", "description": "Configure attribute keys retained in the resulting stream(s).\nIf omitted, all attribute keys are retained.\n" } } } } }python-opentelemetry-1.45.2/opentelemetry-configuration/src/opentelemetry/configuration/version/000077500000000000000000000000001527070070700336035ustar00rootroot00000000000000__init__.py000066400000000000000000000001441527070070700356340ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-configuration/src/opentelemetry/configuration/version# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 __version__ = "0.66b2" python-opentelemetry-1.45.2/opentelemetry-configuration/test-requirements.txt000066400000000000000000000007011527070070700300230ustar00rootroot00000000000000asgiref==3.7.2 flaky==3.8.1 iniconfig==2.0.0 packaging==24.0 pluggy==1.6.0 psutil==7.2.2 py-cpuinfo==9.0.0 pytest==9.0.3 jsonschema==4.25.1 rpds-py==0.23.1; python_version == "3.10" rpds-py==0.30.0; python_version >= "3.11" pyyaml==6.0.3 tomli==2.0.1 typing_extensions==4.12.0 wrapt==1.16.0 zipp==3.19.2 -e opentelemetry-api -e tests/opentelemetry-test-utils -e opentelemetry-semantic-conventions -e opentelemetry-sdk -e opentelemetry-configuration python-opentelemetry-1.45.2/opentelemetry-configuration/tests/000077500000000000000000000000001527070070700247265ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-configuration/tests/__init__.py000066400000000000000000000001141527070070700270330ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 python-opentelemetry-1.45.2/opentelemetry-configuration/tests/file/000077500000000000000000000000001527070070700256455ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-configuration/tests/file/__init__.py000066400000000000000000000001141527070070700277520ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 python-opentelemetry-1.45.2/opentelemetry-configuration/tests/file/data/000077500000000000000000000000001527070070700265565ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-configuration/tests/file/data/config_with_env_vars.yaml000066400000000000000000000002441527070070700336450ustar00rootroot00000000000000file_format: "1.0" resource: attributes: - name: service.name value: ${SERVICE_NAME} - name: deployment.environment value: ${ENV:-production} python-opentelemetry-1.45.2/opentelemetry-configuration/tests/file/data/configurator_config.yaml000066400000000000000000000013261527070070700334730ustar00rootroot00000000000000file_format: "1.0" tracer_provider: processors: - batch: exporter: console: {} tracer_configurator/development: default_config: enabled: true tracers: - name: "noisy.*" config: enabled: false meter_provider: readers: - periodic: exporter: console: {} meter_configurator/development: default_config: enabled: false meters: - name: "keep.*" config: enabled: true logger_provider: processors: - batch: exporter: console: {} logger_configurator/development: default_config: enabled: true loggers: - name: "noisy.*" config: enabled: false python-opentelemetry-1.45.2/opentelemetry-configuration/tests/file/data/invalid_yaml.yaml000066400000000000000000000000651527070070700321130ustar00rootroot00000000000000file_format: "1.0" invalid: indentation: - bad python-opentelemetry-1.45.2/opentelemetry-configuration/tests/file/data/minimal_config.json000066400000000000000000000000331527070070700324200ustar00rootroot00000000000000{ "file_format": "1.0" } python-opentelemetry-1.45.2/opentelemetry-configuration/tests/file/data/minimal_config.yaml000066400000000000000000000000231527070070700324100ustar00rootroot00000000000000file_format: "1.0" python-opentelemetry-1.45.2/opentelemetry-configuration/tests/file/test_configurator_yaml.py000066400000000000000000000076131527070070700330110ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # Tests access private members of SDK providers to assert wired configuration. # pylint: disable=protected-access import unittest from pathlib import Path from opentelemetry.configuration._logger_provider import ( create_logger_provider, ) from opentelemetry.configuration._meter_provider import ( create_meter_provider, ) from opentelemetry.configuration._tracer_provider import ( create_tracer_provider, ) from opentelemetry.configuration.file import load_config_file from opentelemetry.configuration.models import ( ExperimentalLoggerConfig, ExperimentalLoggerConfigurator, ExperimentalLoggerMatcherAndConfig, ExperimentalMeterConfig, ExperimentalMeterConfigurator, ExperimentalMeterMatcherAndConfig, ExperimentalTracerConfig, ExperimentalTracerConfigurator, ExperimentalTracerMatcherAndConfig, ) from opentelemetry.sdk.util.instrumentation import InstrumentationScope class TestConfiguratorYaml(unittest.TestCase): """Verify the tracer/meter/logger configurator nodes are parsed from YAML and wired into the providers they configure.""" @classmethod def setUpClass(cls): config_path = Path(__file__).parent / "data" / "configurator_config.yaml" cls.config = load_config_file(str(config_path)) def test_tracer_configurator_parsed_from_yaml(self): self.assertEqual( self.config.tracer_provider.tracer_configurator_development, ExperimentalTracerConfigurator( default_config=ExperimentalTracerConfig(enabled=True), tracers=[ ExperimentalTracerMatcherAndConfig( name="noisy.*", config=ExperimentalTracerConfig(enabled=False), ) ], ), ) def test_meter_configurator_parsed_from_yaml(self): self.assertEqual( self.config.meter_provider.meter_configurator_development, ExperimentalMeterConfigurator( default_config=ExperimentalMeterConfig(enabled=False), meters=[ ExperimentalMeterMatcherAndConfig( name="keep.*", config=ExperimentalMeterConfig(enabled=True), ) ], ), ) def test_logger_configurator_parsed_from_yaml(self): self.assertEqual( self.config.logger_provider.logger_configurator_development, ExperimentalLoggerConfigurator( default_config=ExperimentalLoggerConfig(enabled=True), loggers=[ ExperimentalLoggerMatcherAndConfig( name="noisy.*", config=ExperimentalLoggerConfig(enabled=False), ) ], ), ) def test_tracer_configurator_from_yaml_is_wired(self): provider = create_tracer_provider(self.config.tracer_provider) self.assertFalse(provider._apply_tracer_configurator(InstrumentationScope("noisy.http")).is_enabled) self.assertTrue(provider._apply_tracer_configurator(InstrumentationScope("app.service")).is_enabled) def test_meter_configurator_from_yaml_is_wired(self): provider = create_meter_provider(self.config.meter_provider) self.assertTrue(provider._apply_meter_configurator(InstrumentationScope("keep.me")).is_enabled) self.assertFalse(provider._apply_meter_configurator(InstrumentationScope("other")).is_enabled) def test_logger_configurator_from_yaml_is_wired(self): provider = create_logger_provider(self.config.logger_provider) self.assertFalse(provider._apply_logger_configurator(InstrumentationScope("noisy.http")).is_enabled) self.assertTrue(provider._apply_logger_configurator(InstrumentationScope("app.service")).is_enabled) python-opentelemetry-1.45.2/opentelemetry-configuration/tests/file/test_env_substitution.py000066400000000000000000000134311527070070700327040ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import os import unittest from unittest.mock import patch import yaml from opentelemetry.configuration.file import substitute_env_vars class TestEnvSubstitution(unittest.TestCase): """Test environment variable substitution.""" def test_simple_substitution(self): """Test basic ${VAR} substitution.""" with patch.dict(os.environ, {"SERVICE_NAME": "my-service"}): result = substitute_env_vars("name: ${SERVICE_NAME}") self.assertEqual(result, "name: my-service") def test_multiple_substitutions(self): """Test multiple ${VAR} in same string.""" with patch.dict(os.environ, {"HOST": "localhost", "PORT": "8080"}): result = substitute_env_vars("url: http://${HOST}:${PORT}") self.assertEqual(result, "url: http://localhost:8080") def test_substitution_with_default(self): """Test ${VAR:-default} with missing variable.""" with patch.dict(os.environ, {}, clear=True): result = substitute_env_vars("name: ${SERVICE_NAME:-default}") self.assertEqual(result, "name: default") def test_substitution_with_default_override(self): """Test ${VAR:-default} with present variable.""" with patch.dict(os.environ, {"SERVICE_NAME": "actual"}): result = substitute_env_vars("name: ${SERVICE_NAME:-default}") self.assertEqual(result, "name: actual") def test_missing_variable_without_default_substitutes_empty(self): """An unset ${VAR} without a default is replaced with an empty value. Per the declarative configuration spec, unset variables without defaults are replaced with an empty value, which YAML then reads as null. This matches the Java and Node.js implementations. """ with patch.dict(os.environ, {}, clear=True): result = substitute_env_vars("name: ${MISSING_VAR}") self.assertEqual(result, "name: ") self.assertIsNone(yaml.safe_load(result)["name"]) def test_dollar_sign_escape(self): """Test $$ escapes to literal $.""" result = substitute_env_vars("price: $$100") self.assertEqual(result, "price: $100") def test_dollar_sign_escape_with_substitution(self): """Test $$ with ${VAR} in same string.""" with patch.dict(os.environ, {"AMOUNT": "50"}): result = substitute_env_vars("price: $$${AMOUNT}") self.assertEqual(result, "price: $50") def test_empty_default(self): """Test ${VAR:-} with empty default value.""" with patch.dict(os.environ, {}, clear=True): result = substitute_env_vars("name: ${VAR:-}") self.assertEqual(result, "name: ") def test_default_with_special_chars(self): """Test default value containing special characters.""" with patch.dict(os.environ, {}, clear=True): result = substitute_env_vars("url: ${URL:-http://localhost:8080}") self.assertEqual(result, "url: http://localhost:8080") def test_no_substitution_needed(self): """Test string without any variables.""" result = substitute_env_vars("plain text without variables") self.assertEqual(result, "plain text without variables") def test_variable_name_with_underscores(self): """Test variable names with underscores.""" with patch.dict(os.environ, {"MY_VAR_NAME": "value"}): result = substitute_env_vars("key: ${MY_VAR_NAME}") self.assertEqual(result, "key: value") def test_variable_name_with_numbers(self): """Test variable names with numbers.""" with patch.dict(os.environ, {"VAR_123": "value"}): result = substitute_env_vars("key: ${VAR_123}") self.assertEqual(result, "key: value") def test_multiline_substitution(self): """Test substitution across multiple lines.""" with patch.dict(os.environ, {"VAR1": "value1", "VAR2": "value2"}): text = """line1: ${VAR1} line2: ${VAR2}""" result = substitute_env_vars(text) expected = """line1: value1 line2: value2""" self.assertEqual(result, expected) def test_empty_string(self): """Test empty string returns empty string.""" result = substitute_env_vars("") self.assertEqual(result, "") def test_only_dollar_signs(self): """Test string with only escaped dollar signs.""" result = substitute_env_vars("$$$$") self.assertEqual(result, "$$") def test_newline_in_value_returned_verbatim(self): """A newline in a value is returned as-is. Substitution runs per configuration value after parsing, so it does not escape or quote newlines; YAML injection is prevented structurally (see the loader tests), not by rewriting the value here. """ with patch.dict(os.environ, {"MULTI": "line1\nline2"}): result = substitute_env_vars("${MULTI}") self.assertEqual(result, "line1\nline2") def test_carriage_return_in_value_returned_verbatim(self): """A carriage return in a value is returned as-is, not escaped.""" with patch.dict(os.environ, {"VAL": "text\r\nmore"}): result = substitute_env_vars("${VAL}") self.assertEqual(result, "text\r\nmore") def test_type_coercion_preserved_for_simple_values(self): """Simple values without newlines still undergo YAML type coercion per spec.""" with patch.dict(os.environ, {"BOOL_VAL": "true", "INT_VAL": "42"}): bool_result = yaml.safe_load(substitute_env_vars("key: ${BOOL_VAL}")) int_result = yaml.safe_load(substitute_env_vars("key: ${INT_VAL}")) self.assertIs(bool_result["key"], True) self.assertEqual(int_result["key"], 42) python-opentelemetry-1.45.2/opentelemetry-configuration/tests/file/test_loader.py000066400000000000000000000544701527070070700305360ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import os import tempfile import unittest from pathlib import Path from unittest.mock import patch import yaml from opentelemetry.configuration._tracer_provider import ( create_tracer_provider, ) from opentelemetry.configuration.file import ( ConfigurationError, load_config_file, ) from opentelemetry.configuration.file._loader import ( _SUPPORTED_SCHEMA_MAJOR, _SUPPORTED_SCHEMA_MINOR, _substitute_env_in_json_value, _substitute_env_in_yaml_node, ) from opentelemetry.configuration.models import ( BatchSpanProcessor as BatchSpanProcessorConfig, ) from opentelemetry.configuration.models import OpenTelemetryConfiguration from opentelemetry.configuration.models import ( ParentBasedSampler as ParentBasedSamplerConfig, ) from opentelemetry.configuration.models import ( SpanProcessor as SpanProcessorConfig, ) from opentelemetry.configuration.models import ( TracerProvider as TracerProviderConfig, ) from opentelemetry.sdk.trace import TracerProvider as SdkTracerProvider from opentelemetry.sdk.trace.export import ( BatchSpanProcessor, ConsoleSpanExporter, ) from opentelemetry.sdk.trace.sampling import ParentBased, TraceIdRatioBased class TestConfigLoader(unittest.TestCase): """Test configuration file loading.""" def setUp(self): """Set up test fixtures.""" self.test_data_dir = Path(__file__).parent / "data" def test_load_minimal_yaml(self): """Test loading minimal YAML configuration.""" config_path = self.test_data_dir / "minimal_config.yaml" config = load_config_file(str(config_path)) self.assertIsInstance(config, OpenTelemetryConfiguration) self.assertEqual(config.file_format, "1.0") def test_load_minimal_json(self): """Test loading minimal JSON configuration.""" config_path = self.test_data_dir / "minimal_config.json" config = load_config_file(str(config_path)) self.assertIsInstance(config, OpenTelemetryConfiguration) self.assertEqual(config.file_format, "1.0") def test_load_config_with_env_vars(self): """Test loading configuration with environment variable substitution.""" config_path = self.test_data_dir / "config_with_env_vars.yaml" with patch.dict(os.environ, {"SERVICE_NAME": "test-service"}): config = load_config_file(str(config_path)) self.assertIsInstance(config, OpenTelemetryConfiguration) def test_file_not_found(self): """Test error when file doesn't exist.""" with self.assertRaises(ConfigurationError) as ctx: load_config_file("nonexistent_file.yaml") self.assertIn("not found", str(ctx.exception)) def test_directory_instead_of_file(self): """Test error when path is a directory.""" with self.assertRaises(ConfigurationError) as ctx: load_config_file(str(self.test_data_dir)) self.assertIn("not a file", str(ctx.exception)) def test_invalid_yaml_syntax(self): """Test error on invalid YAML syntax.""" config_path = self.test_data_dir / "invalid_yaml.yaml" with self.assertRaises(ConfigurationError) as ctx: load_config_file(str(config_path)) self.assertIn("yaml", str(ctx.exception).lower()) def test_invalid_file_extension(self): """Test error on unsupported file extension.""" with tempfile.NamedTemporaryFile(suffix=".txt", delete=False) as temp_file: temp_file.write(b"file_format: 1.0") temp_path = temp_file.name try: with self.assertRaises(ConfigurationError) as ctx: load_config_file(temp_path) self.assertIn("Unsupported file format", str(ctx.exception)) finally: os.unlink(temp_path) def test_empty_file(self): """Test error on empty file.""" with tempfile.NamedTemporaryFile(suffix=".yaml", delete=False, mode="w") as temp_file: temp_path = temp_file.name try: with self.assertRaises(ConfigurationError) as ctx: load_config_file(temp_path) self.assertIn("empty", str(ctx.exception).lower()) finally: os.unlink(temp_path) def test_non_dict_root(self): """Test error when root is not a mapping.""" with tempfile.NamedTemporaryFile(suffix=".yaml", delete=False, mode="w") as temp_file: temp_file.write("- item1\n- item2") temp_path = temp_file.name try: with self.assertRaises(ConfigurationError) as ctx: load_config_file(temp_path) self.assertIn("mapping", str(ctx.exception).lower()) finally: os.unlink(temp_path) def test_unset_env_var_without_default_substitutes_empty(self): """An unset env var without a default resolves to an empty value. Per the declarative configuration spec, an unset ``${VAR}`` reference with no default is replaced with an empty value rather than raising, so the file still loads. A default (``${ENV:-production}``) is still applied when its variable is unset. """ config_path = self.test_data_dir / "config_with_env_vars.yaml" with patch.dict(os.environ, {}, clear=True): config = load_config_file(str(config_path)) attributes = {attribute.name: attribute.value for attribute in config.resource.attributes} self.assertIsNone(attributes["service.name"]) self.assertEqual(attributes["deployment.environment"], "production") def test_yml_extension(self): """Test .yml extension is supported.""" with tempfile.NamedTemporaryFile(suffix=".yml", delete=False, mode="w") as temp_file: temp_file.write('file_format: "1.0"') temp_path = temp_file.name try: config = load_config_file(temp_path) self.assertEqual(config.file_format, "1.0") finally: os.unlink(temp_path) def test_json_syntax_error(self): """Test error on invalid JSON syntax.""" with tempfile.NamedTemporaryFile(suffix=".json", delete=False, mode="w") as temp_file: temp_file.write('{"file_format": invalid}') temp_path = temp_file.name try: with self.assertRaises(ConfigurationError) as ctx: load_config_file(temp_path) self.assertIn("json", str(ctx.exception).lower()) finally: os.unlink(temp_path) def test_schema_validation_wrong_type(self): """Test error when field has wrong type.""" with tempfile.NamedTemporaryFile(suffix=".yaml", delete=False, mode="w") as temp_file: # disabled must be a boolean, not a string temp_file.write('file_format: "1.0"\ndisabled: "yes"') temp_path = temp_file.name try: with self.assertRaises(ConfigurationError) as ctx: load_config_file(temp_path) self.assertIn("schema", str(ctx.exception).lower()) finally: os.unlink(temp_path) def test_schema_validation_missing_file_format(self): """Test error when required file_format field is missing.""" with tempfile.NamedTemporaryFile(suffix=".yaml", delete=False, mode="w") as temp_file: temp_file.write("disabled: false") temp_path = temp_file.name try: with self.assertRaises(ConfigurationError) as ctx: load_config_file(temp_path) self.assertIn("schema", str(ctx.exception).lower()) finally: os.unlink(temp_path) def test_schema_validation_nested_path_in_error(self): """Test that error message includes field path for nested violations.""" with tempfile.NamedTemporaryFile(suffix=".yaml", delete=False, mode="w") as temp_file: temp_file.write('file_format: "1.0"\nattribute_limits:\n attribute_count_limit: "not-a-number"\n') temp_path = temp_file.name try: with self.assertRaises(ConfigurationError) as ctx: load_config_file(temp_path) error = str(ctx.exception) self.assertIn("schema", error.lower()) self.assertIn("attribute_limits", error) self.assertIn("attribute_count_limit", error) finally: os.unlink(temp_path) def test_schema_validation_invalid_enum(self): """Test error when field value is not a valid enum value.""" with tempfile.NamedTemporaryFile(suffix=".yaml", delete=False, mode="w") as temp_file: temp_file.write('file_format: "1.0"\nlog_level: INVALID_LEVEL') temp_path = temp_file.name try: with self.assertRaises(ConfigurationError) as ctx: load_config_file(temp_path) self.assertIn("schema", str(ctx.exception).lower()) finally: os.unlink(temp_path) class TestConfigLoaderEndToEnd(unittest.TestCase): """Smoke-test the full YAML -> typed config -> SDK object pipeline. Unit tests in test_conversion.py exercise the dict-to-dataclass conversion in isolation; these tests verify it composes with the real loader and downstream factory functions on a representative nested configuration. """ _YAML = """ file_format: '1.0' tracer_provider: processors: - batch: exporter: console: {} sampler: parent_based: root: trace_id_ratio_based: {ratio: 0.5} """ def _load(self, yaml_content: str | None = None) -> OpenTelemetryConfiguration: with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as fh: fh.write(self._YAML if yaml_content is None else yaml_content) path = fh.name try: return load_config_file(path) finally: os.unlink(path) def test_nested_fields_are_typed_dataclasses(self): config = self._load() self.assertIsInstance(config.tracer_provider, TracerProviderConfig) self.assertIsInstance( config.tracer_provider.sampler.parent_based, ParentBasedSamplerConfig, ) # Lists of dataclasses are converted element-wise. self.assertIsInstance(config.tracer_provider.processors[0], SpanProcessorConfig) self.assertIsInstance( config.tracer_provider.processors[0].batch, BatchSpanProcessorConfig, ) # pylint: disable=protected-access def test_typed_config_feeds_factory_function(self): config = self._load() provider = create_tracer_provider(config.tracer_provider) self.assertIsInstance(provider, SdkTracerProvider) # Sampler wiring from the YAML: parent_based(trace_id_ratio_based(0.5)). self.assertIsInstance(provider.sampler, ParentBased) self.assertIsInstance(provider.sampler._root, TraceIdRatioBased) self.assertEqual(provider.sampler._root.rate, 0.5) # Span processor wiring from the YAML: batch(console). processors = provider._active_span_processor._span_processors self.assertEqual(len(processors), 1) self.assertIsInstance(processors[0], BatchSpanProcessor) self.assertIsInstance(processors[0].span_exporter, ConsoleSpanExporter) def test_null_valued_required_field_node_survives_conversion(self): # Regression test for #5451: ``jaeger_remote_development`` is nullable # in the schema (so ``jaeger_remote_development:`` passes validation and # is NOT rejected before conversion), yet its model has required # fields. Coercing the present null into it would raise TypeError, so # it must be left as None instead. The rest of the config still loads. config = self._load( """ file_format: '1.0' tracer_provider: processors: - batch: exporter: console: sampler: jaeger_remote_development: """ ) # Schema accepted the null, and conversion left the required-field # node unset rather than crashing. self.assertIsNone(config.tracer_provider.sampler.jaeger_remote_development) # A sibling nullable dict-typed node (console:) was still coerced. self.assertEqual(config.tracer_provider.processors[0].batch.exporter.console, {}) class TestFileFormatValidation(unittest.TestCase): """Validate the file_format version per the configuration spec.""" _SUPPORTED = f"{_SUPPORTED_SCHEMA_MAJOR}.{_SUPPORTED_SCHEMA_MINOR}" @staticmethod def _load(file_format: str) -> OpenTelemetryConfiguration: with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as fh: fh.write(f'file_format: "{file_format}"') path = fh.name try: return load_config_file(path) finally: os.unlink(path) def test_supported_version_is_accepted(self): with self.assertNoLogs("opentelemetry.configuration.file._loader", level="WARNING"): config = self._load(self._SUPPORTED) self.assertEqual(config.file_format, self._SUPPORTED) def test_pre_release_meta_tag_is_accepted(self): # The meta tag is stripped, so the version is read as the supported one. version = f"{self._SUPPORTED}-rc.2" config = self._load(version) self.assertEqual(config.file_format, version) @unittest.skipIf( _SUPPORTED_SCHEMA_MINOR == 0, "No older minor version to validate against", ) def test_older_minor_is_accepted(self): version = f"{_SUPPORTED_SCHEMA_MAJOR}.{_SUPPORTED_SCHEMA_MINOR - 1}" with self.assertNoLogs("opentelemetry.configuration.file._loader", level="WARNING"): config = self._load(version) self.assertEqual(config.file_format, version) def test_newer_minor_is_accepted_with_warning(self): version = f"{_SUPPORTED_SCHEMA_MAJOR}.{_SUPPORTED_SCHEMA_MINOR + 1}" with self.assertLogs("opentelemetry.configuration.file._loader", level="WARNING") as logs: config = self._load(version) self.assertEqual(config.file_format, version) self.assertTrue(any("newer minor version" in message for message in logs.output)) def test_unsupported_major_is_rejected(self): versions = ["0.4", f"{_SUPPORTED_SCHEMA_MAJOR + 1}.0"] for version in versions: with self.subTest(version=version): with self.assertRaises(ConfigurationError) as ctx: self._load(version) self.assertIn("file_format", str(ctx.exception)) def test_malformed_version_is_rejected(self): with self.assertRaises(ConfigurationError) as ctx: self._load("not-a-version") self.assertIn("file_format", str(ctx.exception)) def _substitute_yaml(text: str): """Parse ``text``, run the node walker over it, and build the document. This is what ``_parse_config_content`` does for YAML, without the schema validation, so walker behavior can be asserted on any document shape. """ loader = yaml.SafeLoader(text) try: node = loader.get_single_node() _substitute_env_in_yaml_node(node, loader) return loader.construct_document(node) finally: loader.dispose() class TestEnvVarSubstitutionScope(unittest.TestCase): """Substitution applies only to configuration values, per the config spec. These exercise the YAML node walker directly so the spec's type and scope rules can be asserted without the JSON schema constraining shape. """ def test_unquoted_standalone_reference_is_type_coerced(self): with patch.dict(os.environ, {"N": "42", "FLAG": "true"}): result = _substitute_yaml("count: ${N}\nflag: ${FLAG}") self.assertEqual(result["count"], 42) self.assertIsInstance(result["count"], int) self.assertIs(result["flag"], True) def test_quoted_reference_stays_string(self): with patch.dict(os.environ, {"N": "42"}): result = _substitute_yaml('count: "${N}"') self.assertEqual(result["count"], "42") def test_embedded_reference_resolves_to_string(self): with patch.dict(os.environ, {"N": "42"}): result = _substitute_yaml("name: svc-${N}") self.assertEqual(result["name"], "svc-42") def test_mapping_key_is_not_substituted(self): # A ${VAR} in a key position is left verbatim and triggers no lookup, # so an undefined variable there does not raise. with patch.dict(os.environ, {}, clear=True): result = _substitute_yaml("${UNDEFINED_KEY}: value") self.assertEqual(result, {"${UNDEFINED_KEY}": "value"}) def test_escape_sequence_is_not_a_reference(self): with patch.dict(os.environ, {}, clear=True): result = _substitute_yaml("literal: $${NOT_A_VAR}") self.assertEqual(result["literal"], "${NOT_A_VAR}") def test_value_newline_cannot_inject_mapping_keys(self): with patch.dict(os.environ, {"VAL": "legit\nmalicious_key: injected"}): result = _substitute_yaml("service_name: ${VAL}") self.assertEqual(list(result), ["service_name"]) self.assertEqual(result["service_name"], "legit\nmalicious_key: injected") class TestEnvVarSubstitutionWithAliases(unittest.TestCase): """An anchored value is substituted once, however many aliases reach it. An alias resolves to the very same composed node as its anchor, so the walker reaches one node once per reference. Substituting it more than once would re-read the first pass's output and defeat the ``$$`` escape, and a cyclic alias would recurse without end. """ def test_aliased_reference_resolves_for_every_alias(self): with patch.dict(os.environ, {"TOKEN": "resolved"}): result = _substitute_yaml("a: &anchor ${TOKEN}\nb: *anchor\nc: *anchor") self.assertEqual(result, {"a": "resolved", "b": "resolved", "c": "resolved"}) def test_aliased_escape_stays_literal(self): # Without a visited set the first visit turns $${TOKEN} into the # literal ${TOKEN} and the second resolves it, leaking TOKEN's value. with patch.dict(os.environ, {"TOKEN": "leaked"}): result = _substitute_yaml("a: &anchor $${TOKEN}\nb: *anchor\nc: *anchor") self.assertEqual(result, {"a": "${TOKEN}", "b": "${TOKEN}", "c": "${TOKEN}"}) def test_aliased_reference_is_type_coerced_once(self): with patch.dict(os.environ, {"N": "42"}): result = _substitute_yaml("a: &anchor ${N}\nb: *anchor") self.assertEqual(result["a"], 42) self.assertIsInstance(result["a"], int) self.assertEqual(result["b"], 42) def test_merge_key_escape_stays_literal(self): # A merge key's value node is the anchored mapping itself, so the # merged-in values are reached twice as well. text = "base: &base\n x: $${TOKEN}\nderived:\n <<: *base\n y: 1\n" with patch.dict(os.environ, {"TOKEN": "leaked"}): result = _substitute_yaml(text) self.assertEqual(result, {"base": {"x": "${TOKEN}"}, "derived": {"x": "${TOKEN}", "y": 1}}) def test_merge_key_resolves_reference(self): text = "base: &base\n x: ${TOKEN}\nderived:\n <<: *base\n y: 1\n" with patch.dict(os.environ, {"TOKEN": "resolved"}): result = _substitute_yaml(text) self.assertEqual(result, {"base": {"x": "resolved"}, "derived": {"x": "resolved", "y": 1}}) def test_cyclic_alias_terminates(self): # A mapping that aliases itself makes the node tree a graph with a # loop; the walk must end instead of raising RecursionError. with patch.dict(os.environ, {"TOKEN": "resolved"}): result = _substitute_yaml("a: &anchor\n self: *anchor\n value: ${TOKEN}") self.assertIs(result["a"]["self"], result["a"]) self.assertEqual(result["a"]["value"], "resolved") def test_cyclic_alias_in_sequence_terminates(self): with patch.dict(os.environ, {"TOKEN": "resolved"}): result = _substitute_yaml("a: &anchor\n - *anchor\n - ${TOKEN}") self.assertIs(result["a"][0], result["a"]) self.assertEqual(result["a"][1], "resolved") class TestJsonEnvVarSubstitution(unittest.TestCase): """JSON substitution touches only string values, not keys or non-strings.""" def test_string_values_substituted_keys_untouched(self): with patch.dict(os.environ, {"V": "resolved"}): result = _substitute_env_in_json_value({"${KEY}": "${V}", "nested": ["${V}", 1, True, None]}) self.assertEqual( result, {"${KEY}": "resolved", "nested": ["resolved", 1, True, None]}, ) class TestEnvVarSubstitutionEndToEnd(unittest.TestCase): """End-to-end loader behavior for issue #5406.""" @staticmethod def _load_yaml(text: str) -> OpenTelemetryConfiguration: with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as fh: fh.write(text) path = fh.name try: return load_config_file(path) finally: os.unlink(path) def test_undefined_variable_in_comment_does_not_crash(self): # The reported bug: a ${VAR} inside a comment must be ignored, so an # undefined variable there no longer aborts loading. text = "file_format: '1.0'\n# documented default uses ${UNDEFINED_VAR} - not substituted\ndisabled: false\n" with patch.dict(os.environ, {}, clear=True): config = self._load_yaml(text) self.assertEqual(config.file_format, "1.0") self.assertIs(config.disabled, False) def test_standalone_reference_coerces_type_for_schema(self): # An integer field populated from ${VAR} must be an int so it passes # JSON-schema validation. text = "file_format: '1.0'\nattribute_limits:\n attribute_count_limit: ${LIMIT}\n" with patch.dict(os.environ, {"LIMIT": "100"}): config = self._load_yaml(text) self.assertEqual(config.attribute_limits.attribute_count_limit, 100) def test_quoted_reference_for_int_field_fails_schema(self): # Quoting forces a string, which is invalid for an integer field. text = "file_format: '1.0'\nattribute_limits:\n attribute_count_limit: \"${LIMIT}\"\n" with patch.dict(os.environ, {"LIMIT": "100"}): with self.assertRaises(ConfigurationError) as ctx: self._load_yaml(text) self.assertIn("schema", str(ctx.exception).lower()) python-opentelemetry-1.45.2/opentelemetry-configuration/tests/test_common.py000066400000000000000000000401431527070070700276310ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import inspect import unittest from dataclasses import dataclass from types import SimpleNamespace from typing import Any, ClassVar from unittest.mock import MagicMock, patch from opentelemetry.configuration._common import ( _additional_properties, _map_compression, _parse_headers, _parse_otlp_file_output_stream, _resolve_component, load_entry_point, ) from opentelemetry.configuration._exceptions import ConfigurationError from opentelemetry.configuration.models import ( ExperimentalResourceDetector, LogRecordExporter, PushMetricExporter, Sampler, SpanExporter, TextMapPropagator, ) class TestParseHeaders(unittest.TestCase): def test_both_none_returns_none(self): self.assertIsNone(_parse_headers(None, None)) def test_empty_headers_list_returns_empty_dict(self): self.assertEqual(_parse_headers(None, ""), {}) def test_headers_list_single_pair(self): self.assertEqual( _parse_headers(None, "x-api-key=secret"), {"x-api-key": "secret"}, ) def test_headers_list_multiple_pairs(self): self.assertEqual( _parse_headers(None, "x-api-key=secret,env=prod"), {"x-api-key": "secret", "env": "prod"}, ) def test_headers_list_strips_whitespace(self): self.assertEqual( _parse_headers(None, " x-api-key = secret , env = prod "), {"x-api-key": "secret", "env": "prod"}, ) def test_headers_list_value_with_equals(self): # value contains '=' — only split on the first one self.assertEqual( _parse_headers(None, "auth=Bearer tok=en"), {"auth": "Bearer tok=en"}, ) def test_headers_list_invalid_pair_ignored(self): # malformed entry (no '=') should be skipped with a warning result = _parse_headers(None, "bad,x-key=val") self.assertEqual(result, {"x-key": "val"}) def test_struct_headers_only(self): headers = [ SimpleNamespace(name="x-api-key", value="secret"), SimpleNamespace(name="env", value="prod"), ] self.assertEqual( _parse_headers(headers, None), {"x-api-key": "secret", "env": "prod"}, ) def test_struct_header_none_value_becomes_empty_string(self): headers = [SimpleNamespace(name="x-key", value=None)] self.assertEqual(_parse_headers(headers, None), {"x-key": ""}) def test_struct_headers_override_headers_list(self): # struct takes priority over headers_list for same key headers = [SimpleNamespace(name="x-api-key", value="from-struct")] self.assertEqual( _parse_headers(headers, "x-api-key=from-list,env=prod"), {"x-api-key": "from-struct", "env": "prod"}, ) def test_both_empty_struct_and_none_list_returns_empty_dict(self): self.assertEqual(_parse_headers([], None), {}) class TestLoadEntryPoint(unittest.TestCase): def test_returns_loaded_class(self): mock_class = MagicMock() mock_ep = MagicMock() mock_ep.load.return_value = mock_class with patch( "opentelemetry.configuration._common.entry_points", return_value=[mock_ep], ): result = load_entry_point("some_group", "some_name") self.assertIs(result, mock_class) def test_raises_when_not_found(self): with ( patch( "opentelemetry.configuration._common.entry_points", return_value=[], ), self.assertRaises(ConfigurationError) as ctx, ): load_entry_point("some_group", "missing") self.assertIn("missing", str(ctx.exception)) self.assertIn("some_group", str(ctx.exception)) def test_wraps_load_exception_in_configuration_error(self): mock_ep = MagicMock() mock_ep.load.side_effect = ImportError("bad import") with ( patch( "opentelemetry.configuration._common.entry_points", return_value=[mock_ep], ), self.assertRaises(ConfigurationError) as ctx, ): load_entry_point("some_group", "some_name") self.assertIn("bad import", str(ctx.exception)) def test_instantiation_error_not_wrapped(self): """load_entry_point returns the class; instantiation is the caller's responsibility. Errors from calling the returned class are NOT wrapped in ConfigurationError — they propagate as-is.""" mock_class = MagicMock(side_effect=TypeError("bad init")) mock_ep = MagicMock() mock_ep.load.return_value = mock_class with patch( "opentelemetry.configuration._common.entry_points", return_value=[mock_ep], ): cls = load_entry_point("some_group", "some_name") # load_entry_point itself succeeds self.assertIs(cls, mock_class) # Calling the returned class raises the original error, not # ConfigurationError with self.assertRaises(TypeError, msg="bad init"): cls() class _CompressionWithDeflate: Gzip = "gzip" Deflate = "deflate" class _CompressionWithoutDeflate: Gzip = "gzip" class TestMapCompression(unittest.TestCase): def test_none_returns_none(self): self.assertIsNone(_map_compression(None, _CompressionWithDeflate)) def test_none_string_returns_none(self): self.assertIsNone(_map_compression("none", _CompressionWithDeflate)) def test_gzip_maps_to_gzip(self): self.assertEqual(_map_compression("gzip", _CompressionWithDeflate), "gzip") def test_deflate_maps_when_enabled(self): self.assertEqual( _map_compression("deflate", _CompressionWithDeflate, allow_deflate=True), "deflate", ) def test_deflate_raises_by_default(self): with self.assertRaises(ConfigurationError) as ctx: _map_compression("deflate", _CompressionWithDeflate) self.assertEqual( str(ctx.exception), "Unsupported compression value 'deflate'. Supported values: 'gzip', 'none'.", ) def test_deflate_raises_when_http_enum_lacks_support(self): with self.assertRaises(ConfigurationError) as ctx: _map_compression("deflate", _CompressionWithoutDeflate, allow_deflate=True) self.assertEqual( str(ctx.exception), "Unsupported compression value 'deflate'. Supported values: 'gzip', 'none'.", ) def test_http_error_message_includes_deflate(self): with self.assertRaises(ConfigurationError) as ctx: _map_compression("brotli", _CompressionWithDeflate, allow_deflate=True) self.assertEqual( str(ctx.exception), "Unsupported compression value 'brotli'. Supported values: 'gzip', 'deflate', 'none'.", ) class TestParseOtlpFileOutputStream(unittest.TestCase): def test_none_returns_none(self): self.assertIsNone(_parse_otlp_file_output_stream(None)) def test_stdout_returns_none(self): self.assertIsNone(_parse_otlp_file_output_stream("stdout")) def test_file_uri_returns_path(self): self.assertEqual( _parse_otlp_file_output_stream("file:///tmp/traces.jsonl"), "/tmp/traces.jsonl", ) def test_file_uri_localhost_host_returns_path(self): self.assertEqual( _parse_otlp_file_output_stream("file://localhost/tmp/traces.jsonl"), "/tmp/traces.jsonl", ) def test_file_uri_with_other_host_raises(self): with self.assertRaises(ConfigurationError) as ctx: _parse_otlp_file_output_stream("file://otherhost/tmp/traces.jsonl") self.assertEqual( str(ctx.exception), "Unsupported output_stream 'file://otherhost/tmp/traces.jsonl' " "for otlp_file_development exporter. Supported values: stdout, " "file://.", ) def test_file_uri_empty_path_raises(self): with self.assertRaises(ConfigurationError): _parse_otlp_file_output_stream("file://") def test_file_uri_with_query_raises(self): with self.assertRaises(ConfigurationError): _parse_otlp_file_output_stream("file:///tmp/traces.jsonl?foo=bar") def test_file_uri_with_fragment_raises(self): with self.assertRaises(ConfigurationError): _parse_otlp_file_output_stream("file:///tmp/traces.jsonl#frag") def test_unsupported_scheme_raises(self): with self.assertRaises(ConfigurationError) as ctx: _parse_otlp_file_output_stream("http://example") self.assertEqual( str(ctx.exception), "Unsupported output_stream 'http://example' for " "otlp_file_development exporter. Supported values: stdout, " "file://.", ) def test_malformed_uri_raises_configuration_error(self): with self.assertRaises(ConfigurationError) as ctx: _parse_otlp_file_output_stream("file://[::1") self.assertIn( "Failed to parse output_stream 'file://[::1' for otlp_file_development exporter", str(ctx.exception), ) def test_relative_path_raises(self): with self.assertRaises(ConfigurationError) as ctx: _parse_otlp_file_output_stream("file:traces.jsonl") self.assertEqual( str(ctx.exception), "Unsupported output_stream 'file:traces.jsonl' for otlp_file_development exporter. Path must be absolute.", ) def test_trailing_slash_path_raises(self): with self.assertRaises(ConfigurationError) as ctx: _parse_otlp_file_output_stream("file:///tmp/output/") self.assertEqual( str(ctx.exception), "Unsupported output_stream 'file:///tmp/output/' for " "otlp_file_development exporter. Path must be a file, " "not a directory.", ) class TestAdditionalPropertiesSupport(unittest.TestCase): def setUp(self): @_additional_properties @dataclass class _SampleConfig: known_field: dict | None = None another_field: str | None = None additional_properties: ClassVar[dict[str, Any]] self.cls = _SampleConfig def test_known_fields_work_normally(self): obj = self.cls(known_field={}, another_field="val") self.assertEqual(obj.known_field, {}) self.assertEqual(obj.another_field, "val") self.assertEqual(obj.additional_properties, {}) def test_unknown_kwargs_captured_in_additional_properties(self): # pylint: disable=unexpected-keyword-arg obj = self.cls(my_plugin={"key": "val"}) self.assertIsNone(obj.known_field) self.assertEqual(obj.additional_properties, {"my_plugin": {"key": "val"}}) def test_mixed_known_and_unknown_kwargs(self): # pylint: disable=unexpected-keyword-arg obj = self.cls(known_field={}, my_plugin={}) self.assertEqual(obj.known_field, {}) self.assertEqual(obj.additional_properties, {"my_plugin": {}}) def test_no_args_creates_empty_additional_properties(self): obj = self.cls() self.assertIsNone(obj.known_field) self.assertEqual(obj.additional_properties, {}) def test_signature_preserves_known_fields_and_adds_kwargs(self): sig = inspect.signature(self.cls) param_names = list(sig.parameters.keys()) self.assertIn("known_field", param_names) self.assertIn("another_field", param_names) # **kwargs signals that extras are accepted kwargs_param = sig.parameters.get("kwargs") self.assertIsNotNone(kwargs_param) self.assertEqual(kwargs_param.kind, inspect.Parameter.VAR_KEYWORD) class TestGeneratedModelsHaveAdditionalProperties(unittest.TestCase): """Guards against regressions in the custom datamodel-codegen template. The codegen/dataclass.jinja2 template conditionally applies the @_additional_properties decorator based on the additionalPropertiesType template variable. If datamodel-codegen changes how it passes this variable, these tests will fail. """ def _assert_supports_additional_properties(self, model_cls): # pylint: disable=unexpected-keyword-arg obj = model_cls(_test_plugin_key={}) self.assertTrue( hasattr(obj, "additional_properties"), f"{model_cls.__name__} missing additional_properties attribute", ) self.assertIn("_test_plugin_key", obj.additional_properties) def test_sampler(self): self._assert_supports_additional_properties(Sampler) def test_span_exporter(self): self._assert_supports_additional_properties(SpanExporter) def test_text_map_propagator(self): self._assert_supports_additional_properties(TextMapPropagator) def test_resource_detector(self): self._assert_supports_additional_properties(ExperimentalResourceDetector) def test_log_record_exporter(self): self._assert_supports_additional_properties(LogRecordExporter) def test_push_metric_exporter(self): self._assert_supports_additional_properties(PushMetricExporter) class TestResolveComponent(unittest.TestCase): def setUp(self): @_additional_properties @dataclass class _Config: builtin_a: dict | None = None builtin_b: str | None = None additional_properties: ClassVar[dict[str, Any]] self.cls = _Config self.registry = { "builtin_a": lambda v: ("resolved_a", v), "builtin_b": lambda v: ("resolved_b", v), } def test_resolves_builtin_from_registry(self): config = self.cls(builtin_a={"key": "val"}) result = _resolve_component(config, self.registry, "test_group", "test component") self.assertEqual(result, ("resolved_a", {"key": "val"})) def test_resolves_plugin_via_entry_point(self): mock_instance = MagicMock() mock_class = MagicMock(return_value=mock_instance) with patch( "opentelemetry.configuration._common.entry_points", return_value=[MagicMock(**{"load.return_value": mock_class})], ): # pylint: disable=unexpected-keyword-arg config = self.cls(my_plugin={"opt": "val"}) result = _resolve_component(config, self.registry, "test_group", "test component") self.assertIs(result, mock_instance) mock_class.assert_called_once_with(opt="val") def test_plugin_with_empty_config(self): mock_instance = MagicMock() mock_class = MagicMock(return_value=mock_instance) with patch( "opentelemetry.configuration._common.entry_points", return_value=[MagicMock(**{"load.return_value": mock_class})], ): # pylint: disable=unexpected-keyword-arg config = self.cls(my_plugin={}) _resolve_component(config, self.registry, "test_group", "test component") mock_class.assert_called_once_with() def test_no_component_raises_configuration_error(self): config = self.cls() with self.assertRaises(ConfigurationError): _resolve_component(config, self.registry, "test_group", "test component") def test_plugin_not_found_raises_configuration_error(self): with patch( "opentelemetry.configuration._common.entry_points", return_value=[], ): # pylint: disable=unexpected-keyword-arg config = self.cls(missing_plugin={}) with self.assertRaises(ConfigurationError): _resolve_component(config, self.registry, "test_group", "test component") def test_first_registry_match_wins_when_multiple_set(self): """When multiple built-in fields are set (which the schema should prevent), the first registry match wins.""" config = self.cls(builtin_a={"a": 1}, builtin_b="b") result = _resolve_component(config, self.registry, "test_group", "test component") # builtin_a comes first in the registry dict self.assertEqual(result, ("resolved_a", {"a": 1})) python-opentelemetry-1.45.2/opentelemetry-configuration/tests/test_conversion.py000066400000000000000000000127541527070070700305350ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # Tests access private members of SDK classes to assert correct configuration. # pylint: disable=protected-access import unittest from dataclasses import dataclass from typing import Any, ClassVar from opentelemetry.configuration._common import _additional_properties from opentelemetry.configuration._conversion import _dict_to_dataclass from opentelemetry.configuration.models import ExemplarFilter from opentelemetry.configuration.models import Sampler as SamplerConfig @dataclass class _Inner: value: int | None = None @dataclass class _Middle: inner: _Inner | None = None items: list[_Inner] | None = None @dataclass class _Outer: middle: _Middle | None = None name: str | None = None @_additional_properties @dataclass class _WithExtras: known: str | None = None additional_properties: ClassVar[dict[str, Any]] @dataclass class _WithEnum: filter: ExemplarFilter | None = None @dataclass class _WithMapping: # ``dict[str, Any]``-typed node, like the sampler/detector leaf configs. option: dict[str, Any] | None = None name: str | None = None class TestDictToDataclass(unittest.TestCase): def test_raises_on_non_dataclass(self): # _dict_to_dataclass is internal and assumes cls is a dataclass. with self.assertRaises(TypeError) as ctx: _dict_to_dataclass({"x": 1}, dict) self.assertIn("not a dataclass", str(ctx.exception)) def test_converts_flat_dict(self): result = _dict_to_dataclass({"value": 42}, _Inner) self.assertIsInstance(result, _Inner) self.assertEqual(result.value, 42) def test_converts_nested_dataclass(self): result = _dict_to_dataclass({"middle": {"inner": {"value": 7}}}, _Outer) self.assertIsInstance(result, _Outer) self.assertIsInstance(result.middle, _Middle) self.assertIsInstance(result.middle.inner, _Inner) self.assertEqual(result.middle.inner.value, 7) def test_converts_list_of_dataclasses(self): result = _dict_to_dataclass({"middle": {"items": [{"value": 1}, {"value": 2}]}}, _Outer) self.assertEqual(len(result.middle.items), 2) self.assertIsInstance(result.middle.items[0], _Inner) self.assertEqual(result.middle.items[0].value, 1) self.assertEqual(result.middle.items[1].value, 2) def test_present_null_dataclass_coerced_to_empty_instance(self): # A present-null value on a dataclass-typed node that can be built with # no arguments (e.g. a metric ``console:`` exporter) becomes a # defaulted instance, not None. Regression test for #5451. result = _dict_to_dataclass({"middle": None, "name": "test"}, _Outer) self.assertIsInstance(result.middle, _Middle) self.assertIsNone(result.middle.inner) self.assertEqual(result.name, "test") def test_present_null_dataclass_with_required_field_stays_none(self): # ``jaeger_remote_development`` is nullable in the schema but its model # (ExperimentalJaegerRemoteSampler) has required fields, so it cannot # be defaulted. A present null must stay None rather than raising a # TypeError trying to instantiate it. Regression test for #5451. result = _dict_to_dataclass({"jaeger_remote_development": None}, SamplerConfig) self.assertIsNone(result.jaeger_remote_development) def test_missing_optional_fields_default_to_none(self): result = _dict_to_dataclass({}, _Outer) self.assertIsNone(result.middle) self.assertIsNone(result.name) def test_unknown_keys_routed_to_additional_properties(self): result = _dict_to_dataclass({"known": "yes", "my_plugin": {"opt": True}}, _WithExtras) self.assertEqual(result.known, "yes") self.assertEqual(result.additional_properties, {"my_plugin": {"opt": True}}) def test_primitive_values_pass_through(self): result = _dict_to_dataclass({"name": "hello"}, _Outer) self.assertEqual(result.name, "hello") def test_empty_list_converted(self): result = _dict_to_dataclass({"middle": {"items": []}}, _Outer) self.assertEqual(result.middle.items, []) def test_enum_value_coerced_from_string(self): result = _dict_to_dataclass({"filter": "always_on"}, _WithEnum) self.assertIs(result.filter, ExemplarFilter.always_on) def test_enum_value_already_enum_passes_through(self): result = _dict_to_dataclass({"filter": ExemplarFilter.trace_based}, _WithEnum) self.assertIs(result.filter, ExemplarFilter.trace_based) def test_present_null_mapping_coerced_to_empty_dict(self): # A key present with a null YAML value (e.g. ``option:``) on a # dict-typed node means "select with empty config", so it must become # an empty mapping rather than None. Regression test for #5451. result = _dict_to_dataclass({"option": None}, _WithMapping) self.assertEqual(result.option, {}) def test_explicit_empty_mapping_preserved(self): result = _dict_to_dataclass({"option": {}}, _WithMapping) self.assertEqual(result.option, {}) def test_absent_mapping_stays_none(self): # An omitted optional section must remain unset, unlike a present null. result = _dict_to_dataclass({"name": "x"}, _WithMapping) self.assertIsNone(result.option) def test_present_null_scalar_stays_none(self): result = _dict_to_dataclass({"name": None}, _WithMapping) self.assertIsNone(result.name) python-opentelemetry-1.45.2/opentelemetry-configuration/tests/test_exceptions.py000066400000000000000000000051001527070070700305140ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import unittest from opentelemetry.configuration._exceptions import ( ConfigurationError, MissingDependencyError, ) class TestMissingDependencyError(unittest.TestCase): def test_is_configuration_error_subclass(self): self.assertTrue(issubclass(MissingDependencyError, ConfigurationError)) def test_minimal_constructor(self): exc = MissingDependencyError(package="foo") self.assertEqual(exc.package, "foo") self.assertIsNone(exc.feature) self.assertEqual(exc.install_name, "foo") self.assertIsNone(exc.extras) self.assertIn("'foo'", str(exc)) self.assertIn("pip install foo", str(exc)) def test_with_feature(self): exc = MissingDependencyError(package="bar", feature="Baz exporter") self.assertEqual(exc.package, "bar") self.assertEqual(exc.feature, "Baz exporter") self.assertIn("Baz exporter requires 'bar'", str(exc)) self.assertIn("pip install bar", str(exc)) def test_with_custom_install_name(self): exc = MissingDependencyError( package="pyyaml", install_name="opentelemetry-sdk", extras="file-configuration", ) self.assertEqual(exc.install_name, "opentelemetry-sdk") self.assertEqual(exc.extras, "file-configuration") self.assertIn('pip install "opentelemetry-sdk[file-configuration]"', str(exc)) def test_with_feature_and_extras(self): exc = MissingDependencyError( package="jsonschema", feature="File configuration", install_name="opentelemetry-sdk", extras="file-configuration", ) self.assertIn("File configuration requires 'jsonschema'", str(exc)) self.assertIn('pip install "opentelemetry-sdk[file-configuration]"', str(exc)) def test_can_be_caught_as_configuration_error(self): with self.assertRaises(ConfigurationError): raise MissingDependencyError(package="test") def test_can_be_caught_as_exception(self): with self.assertRaises(Exception): raise MissingDependencyError(package="test") def test_can_be_caught_as_import_error(self): with self.assertRaises(ImportError): raise MissingDependencyError(package="test") def test_is_import_error_subclass(self): self.assertTrue(issubclass(MissingDependencyError, ImportError)) def test_issubclass_import_error(self): self.assertIsInstance(MissingDependencyError(package="test"), ImportError) python-opentelemetry-1.45.2/opentelemetry-configuration/tests/test_instrumentation.py000066400000000000000000000165351527070070700316140ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from dataclasses import dataclass from logging import ERROR, WARNING from unittest import TestCase from unittest.mock import MagicMock, patch from opentelemetry.configuration._exceptions import ConfigurationError from opentelemetry.configuration.instrumentation import ( configure_instrumentation, ) from opentelemetry.configuration.models import ExperimentalInstrumentation _LOAD_EP = "opentelemetry.configuration.instrumentation.load_entry_point" def _make_instrumentor_class(instance, configuration=None): """Return a class mock whose constructor returns ``instance``.""" cls = MagicMock(return_value=instance) cls.configuration = configuration instance.is_instrumented_by_opentelemetry = False return cls class TestConfigureInstrumentation(TestCase): # pylint: disable=no-self-use def test_none_config_is_noop(self): configure_instrumentation(None) def test_none_python_is_noop(self): configure_instrumentation(ExperimentalInstrumentation()) @patch(_LOAD_EP, side_effect=ConfigurationError("not found")) def test_unknown_instrumentor_logs_warning(self, _mock_load): with self.assertLogs( "opentelemetry.configuration.instrumentation", level=WARNING, ) as cm: configure_instrumentation(ExperimentalInstrumentation(python={"unknown_lib": {}})) self.assertTrue( any("unknown_lib" in msg for msg in cm.output), f"Expected warning mentioning 'unknown_lib', got: {cm.output}", ) @patch(_LOAD_EP) def test_instruments_listed_library_with_no_opts(self, mock_load): instrumentor = MagicMock() mock_load.return_value = _make_instrumentor_class(instrumentor) configure_instrumentation(ExperimentalInstrumentation(python={"requests": {}})) mock_load.assert_called_once_with("opentelemetry_instrumentor", "requests") instrumentor.instrument.assert_called_once_with() @patch(_LOAD_EP) def test_forwards_kwargs_to_instrumentor_without_configuration(self, mock_load): instrumentor = MagicMock() mock_load.return_value = _make_instrumentor_class(instrumentor) configure_instrumentation( ExperimentalInstrumentation(python={"flask": {"excluded_urls": "/healthz", "foo": "bar"}}) ) instrumentor.instrument.assert_called_once_with(excluded_urls="/healthz", foo="bar") @patch(_LOAD_EP) def test_enabled_false_skips_instrumentation(self, mock_load): instrumentor = MagicMock() mock_load.return_value = _make_instrumentor_class(instrumentor) configure_instrumentation(ExperimentalInstrumentation(python={"requests": {"enabled": False}})) mock_load.assert_not_called() instrumentor.instrument.assert_not_called() @patch(_LOAD_EP) def test_enabled_key_not_forwarded_to_instrumentor(self, mock_load): instrumentor = MagicMock() mock_load.return_value = _make_instrumentor_class(instrumentor) configure_instrumentation( ExperimentalInstrumentation(python={"flask": {"enabled": True, "excluded_urls": "/ok"}}) ) instrumentor.instrument.assert_called_once_with(excluded_urls="/ok") @patch(_LOAD_EP) def test_multiple_instrumentors_all_called(self, mock_load): flask_inst = MagicMock() requests_inst = MagicMock() def _side_effect(_group, name): return _make_instrumentor_class(flask_inst if name == "flask" else requests_inst) mock_load.side_effect = _side_effect configure_instrumentation(ExperimentalInstrumentation(python={"flask": {}, "requests": {"foo": "bar"}})) flask_inst.instrument.assert_called_once_with() requests_inst.instrument.assert_called_once_with(foo="bar") @patch(_LOAD_EP) def test_instrumentor_exception_does_not_stop_others(self, mock_load): broken_inst = MagicMock() broken_inst.instrument.side_effect = RuntimeError("boom") ok_inst = MagicMock() call_count = 0 def _side_effect(_group, _name): nonlocal call_count call_count += 1 return _make_instrumentor_class(broken_inst if call_count == 1 else ok_inst) mock_load.side_effect = _side_effect with self.assertLogs( "opentelemetry.configuration.instrumentation", level=ERROR, ): configure_instrumentation(ExperimentalInstrumentation(python={"broken": {}, "ok": {}})) ok_inst.instrument.assert_called_once_with() @patch(_LOAD_EP) def test_skips_already_instrumented(self, mock_load): instrumentor = MagicMock() mock_load.return_value = _make_instrumentor_class(instrumentor) instrumentor.is_instrumented_by_opentelemetry = True configure_instrumentation(ExperimentalInstrumentation(python={"requests": {}})) instrumentor.instrument.assert_not_called() @patch(_LOAD_EP) def test_non_dataclass_configuration_attribute_ignored(self, mock_load): instrumentor = MagicMock() mock_load.return_value = _make_instrumentor_class(instrumentor, configuration="not-a-dataclass") configure_instrumentation(ExperimentalInstrumentation(python={"requests": {"foo": "bar"}})) instrumentor.instrument.assert_called_once_with(foo="bar") @patch(_LOAD_EP) def test_configuration_coerces_opts(self, mock_load): @dataclass class RequestsConfig: excluded_urls: str | None = None capture_headers: bool | None = None instrumentor = MagicMock() mock_load.return_value = _make_instrumentor_class(instrumentor, configuration=RequestsConfig) configure_instrumentation( ExperimentalInstrumentation( python={ "requests": { "excluded_urls": "/health", "capture_headers": True, } } ) ) instrumentor.instrument.assert_called_once_with(excluded_urls="/health", capture_headers=True) @patch(_LOAD_EP) def test_configuration_none_fields_not_forwarded(self, mock_load): @dataclass class FlaskConfig: excluded_urls: str | None = None propagate_headers: bool | None = None instrumentor = MagicMock() mock_load.return_value = _make_instrumentor_class(instrumentor, configuration=FlaskConfig) configure_instrumentation(ExperimentalInstrumentation(python={"flask": {"excluded_urls": "/ok"}})) # propagate_headers was not set, so it must not appear in the call. instrumentor.instrument.assert_called_once_with(excluded_urls="/ok") @patch(_LOAD_EP) def test_configuration_unknown_field_raises(self, mock_load): @dataclass class StrictConfig: excluded_urls: str | None = None instrumentor = MagicMock() mock_load.return_value = _make_instrumentor_class(instrumentor, configuration=StrictConfig) with self.assertLogs( "opentelemetry.configuration.instrumentation", level=ERROR, ): configure_instrumentation( ExperimentalInstrumentation(python={"mylib": {"excluded_urls": "/ok", "typo_field": "bad"}}) ) instrumentor.instrument.assert_not_called() python-opentelemetry-1.45.2/opentelemetry-configuration/tests/test_logger_provider.py000066400000000000000000000642131527070070700315360ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # Tests access private members of SDK classes to assert correct configuration. # pylint: disable=protected-access import os import sys import unittest from unittest.mock import MagicMock, patch from opentelemetry._logs import get_logger_provider from opentelemetry.configuration._logger_provider import ( _DEFAULT_EXPORT_TIMEOUT_MILLIS, _DEFAULT_MAX_EXPORT_BATCH_SIZE, _DEFAULT_MAX_QUEUE_SIZE, _DEFAULT_SCHEDULE_DELAY_MILLIS, _create_batch_log_record_processor, _create_log_record_exporter, _create_log_record_processor, _create_simple_log_record_processor, configure_logger_provider, create_logger_provider, ) from opentelemetry.configuration.file._loader import ConfigurationError from opentelemetry.configuration.models import ( AttributeLimits, NameStringValuePair, SeverityNumber, ) from opentelemetry.configuration.models import ( BatchLogRecordProcessor as BatchLogRecordProcessorConfig, ) from opentelemetry.configuration.models import ( ExperimentalLoggerConfig as LoggerConfigConfig, ) from opentelemetry.configuration.models import ( ExperimentalLoggerConfigurator as LoggerConfiguratorConfig, ) from opentelemetry.configuration.models import ( ExperimentalLoggerMatcherAndConfig as LoggerMatcherAndConfig, ) from opentelemetry.configuration.models import ( ExperimentalOtlpFileExporter as ExperimentalOtlpFileExporterConfig, ) from opentelemetry.configuration.models import ( LoggerProvider as LoggerProviderConfig, ) from opentelemetry.configuration.models import ( LogRecordExporter as LogRecordExporterConfig, ) from opentelemetry.configuration.models import ( LogRecordLimits as LogRecordLimitsConfig, ) from opentelemetry.configuration.models import ( LogRecordProcessor as LogRecordProcessorConfig, ) from opentelemetry.configuration.models import ( OtlpGrpcExporter as OtlpGrpcExporterConfig, ) from opentelemetry.configuration.models import ( OtlpHttpExporter as OtlpHttpExporterConfig, ) from opentelemetry.configuration.models import ( SimpleLogRecordProcessor as SimpleLogRecordProcessorConfig, ) from opentelemetry.sdk._logs import LoggerProvider from opentelemetry.sdk._logs._internal.export import ( BatchLogRecordProcessor, ConsoleLogRecordExporter, SimpleLogRecordProcessor, ) from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.util.instrumentation import InstrumentationScope class TestCreateLoggerProviderBasic(unittest.TestCase): def test_none_config_returns_provider(self): provider = create_logger_provider(None) self.assertIsInstance(provider, LoggerProvider) def test_none_config_uses_supplied_resource(self): resource = Resource({"service.name": "svc"}) provider = create_logger_provider(None, resource) self.assertIs(provider.resource, resource) def test_config_with_no_processors(self): config = LoggerProviderConfig(processors=[]) provider = create_logger_provider(config) self.assertIsInstance(provider, LoggerProvider) def test_configure_none_is_noop(self): original = get_logger_provider() configure_logger_provider(None) self.assertIs(get_logger_provider(), original) class TestCreateLogRecordProcessors(unittest.TestCase): @staticmethod def _make_batch_config( exporter_config=None, schedule_delay=None, export_timeout=None, max_queue_size=None, max_export_batch_size=None, ): if exporter_config is None: exporter_config = LogRecordExporterConfig(console={}) return BatchLogRecordProcessorConfig( exporter=exporter_config, schedule_delay=schedule_delay, export_timeout=export_timeout, max_queue_size=max_queue_size, max_export_batch_size=max_export_batch_size, ) def test_batch_processor_default_schedule_delay(self): processor = _create_batch_log_record_processor(self._make_batch_config()) self.assertEqual( processor._batch_processor._schedule_delay_millis, _DEFAULT_SCHEDULE_DELAY_MILLIS, ) def test_batch_processor_default_export_timeout(self): processor = _create_batch_log_record_processor(self._make_batch_config()) self.assertEqual( processor._batch_processor._export_timeout_millis, _DEFAULT_EXPORT_TIMEOUT_MILLIS, ) def test_batch_processor_default_max_queue_size(self): processor = _create_batch_log_record_processor(self._make_batch_config()) self.assertEqual( processor._batch_processor._max_queue_size, _DEFAULT_MAX_QUEUE_SIZE, ) def test_batch_processor_default_max_export_batch_size(self): processor = _create_batch_log_record_processor(self._make_batch_config()) self.assertEqual( processor._batch_processor._max_export_batch_size, _DEFAULT_MAX_EXPORT_BATCH_SIZE, ) def test_batch_processor_explicit_schedule_delay(self): processor = _create_batch_log_record_processor(self._make_batch_config(schedule_delay=2000)) self.assertEqual(processor._batch_processor._schedule_delay_millis, 2000.0) def test_batch_processor_explicit_export_timeout(self): processor = _create_batch_log_record_processor(self._make_batch_config(export_timeout=5000)) self.assertEqual(processor._batch_processor._export_timeout_millis, 5000.0) def test_batch_processor_explicit_max_queue_size(self): processor = _create_batch_log_record_processor(self._make_batch_config(max_queue_size=512)) self.assertEqual(processor._batch_processor._max_queue_size, 512) def test_batch_processor_explicit_max_export_batch_size(self): processor = _create_batch_log_record_processor(self._make_batch_config(max_export_batch_size=128)) self.assertEqual(processor._batch_processor._max_export_batch_size, 128) def test_batch_processor_uses_console_exporter(self): processor = _create_batch_log_record_processor(self._make_batch_config()) self.assertIsInstance(processor._batch_processor._exporter, ConsoleLogRecordExporter) def test_simple_processor_uses_console_exporter(self): config = SimpleLogRecordProcessorConfig(exporter=LogRecordExporterConfig(console={})) processor = _create_simple_log_record_processor(config) self.assertIsInstance(processor, SimpleLogRecordProcessor) self.assertIsInstance(processor._exporter, ConsoleLogRecordExporter) def test_batch_processor_dispatched_from_processor_config(self): config = LogRecordProcessorConfig(batch=self._make_batch_config()) processor = _create_log_record_processor(config) self.assertIsInstance(processor, BatchLogRecordProcessor) def test_simple_processor_dispatched_from_processor_config(self): config = LogRecordProcessorConfig( simple=SimpleLogRecordProcessorConfig(exporter=LogRecordExporterConfig(console={})) ) processor = _create_log_record_processor(config) self.assertIsInstance(processor, SimpleLogRecordProcessor) def test_no_processor_type_raises(self): config = LogRecordProcessorConfig() with self.assertRaises(ConfigurationError): _create_log_record_processor(config) def test_batch_processor_suppresses_env_var(self): """schedule_delay default must not read OTEL_BLRP_SCHEDULE_DELAY.""" with patch.dict("os.environ", {"OTEL_BLRP_SCHEDULE_DELAY": "9999"}): processor = _create_batch_log_record_processor(self._make_batch_config()) self.assertEqual( processor._batch_processor._schedule_delay_millis, _DEFAULT_SCHEDULE_DELAY_MILLIS, ) class TestCreateLogRecordExporters(unittest.TestCase): def test_console_exporter(self): config = LogRecordExporterConfig(console={}) exporter = _create_log_record_exporter(config) self.assertIsInstance(exporter, ConsoleLogRecordExporter) def test_no_exporter_type_raises(self): config = LogRecordExporterConfig() with self.assertRaises(ConfigurationError): _create_log_record_exporter(config) def test_plugin_log_exporter_loaded_via_entry_point(self): mock_exporter = MagicMock() mock_class = MagicMock(return_value=mock_exporter) with patch( "opentelemetry.configuration._common.entry_points", return_value=[MagicMock(**{"load.return_value": mock_class})], ): # pylint: disable=unexpected-keyword-arg result = _create_log_record_exporter(LogRecordExporterConfig(my_custom_exporter={})) self.assertIs(result, mock_exporter) def test_unknown_log_exporter_raises_configuration_error(self): with ( patch( "opentelemetry.configuration._common.entry_points", return_value=[], ), self.assertRaises(ConfigurationError), ): # pylint: disable=unexpected-keyword-arg _create_log_record_exporter(LogRecordExporterConfig(no_such_exporter={})) def test_otlp_http_missing_package_raises(self): config = LogRecordExporterConfig(otlp_http=OtlpHttpExporterConfig(endpoint="http://localhost:4318")) with ( patch.dict( sys.modules, { "opentelemetry.exporter.otlp.proto.http": None, "opentelemetry.exporter.otlp.proto.http._log_exporter": None, }, ), self.assertRaises(ConfigurationError), ): _create_log_record_exporter(config) def test_otlp_grpc_missing_package_raises(self): config = LogRecordExporterConfig(otlp_grpc=OtlpGrpcExporterConfig(endpoint="http://localhost:4317")) with ( patch.dict( sys.modules, { "grpc": None, "opentelemetry.exporter.otlp.proto.grpc._log_exporter": None, }, ), self.assertRaises(ConfigurationError), ): _create_log_record_exporter(config) def test_otlp_file_development_missing_package_raises(self): config = LogRecordExporterConfig(otlp_file_development=ExperimentalOtlpFileExporterConfig()) with ( patch.dict( sys.modules, { "opentelemetry.exporter.otlp.json.file._log_exporter": None, }, ), self.assertRaises(ConfigurationError) as ctx, ): _create_log_record_exporter(config) self.assertIn("opentelemetry-exporter-otlp-json-file", str(ctx.exception)) def test_otlp_file_development_default_stdout(self): mock_exporter_cls = MagicMock() mock_module = MagicMock() mock_module.FileLogExporter = mock_exporter_cls with patch.dict( sys.modules, { "opentelemetry.exporter.otlp.json.file._log_exporter": mock_module, }, ): config = LogRecordExporterConfig(otlp_file_development=ExperimentalOtlpFileExporterConfig()) _create_log_record_exporter(config) mock_exporter_cls.assert_called_once() self.assertEqual(mock_exporter_cls.call_args.args, ()) self.assertEqual(mock_exporter_cls.call_args.kwargs, {}) def test_otlp_file_development_file_uri(self): mock_exporter_cls = MagicMock() mock_module = MagicMock() mock_module.FileLogExporter = mock_exporter_cls with patch.dict( sys.modules, { "opentelemetry.exporter.otlp.json.file._log_exporter": mock_module, }, ): config = LogRecordExporterConfig( otlp_file_development=ExperimentalOtlpFileExporterConfig(output_stream="file:///tmp/logs.jsonl") ) _create_log_record_exporter(config) mock_exporter_cls.assert_called_once() self.assertEqual(mock_exporter_cls.call_args.args, ("/tmp/logs.jsonl",)) def test_otlp_file_development_unsupported_output_stream_raises(self): mock_exporter_cls = MagicMock() mock_module = MagicMock() mock_module.FileLogExporter = mock_exporter_cls with patch.dict( sys.modules, { "opentelemetry.exporter.otlp.json.file._log_exporter": mock_module, }, ): config = LogRecordExporterConfig( otlp_file_development=ExperimentalOtlpFileExporterConfig(output_stream="http://example") ) with self.assertRaises(ConfigurationError) as ctx: _create_log_record_exporter(config) self.assertIn("output_stream", str(ctx.exception)) mock_exporter_cls.assert_not_called() def test_otlp_http_exporter_endpoint(self): mock_exporter_cls = MagicMock() mock_compression_cls = MagicMock() mock_compression_cls.Gzip = "gzip" mock_module = MagicMock() mock_module.Compression = mock_compression_cls mock_log_module = MagicMock() mock_log_module.OTLPLogExporter = mock_exporter_cls with patch.dict( sys.modules, { "opentelemetry.exporter.otlp.proto.http": mock_module, "opentelemetry.exporter.otlp.proto.http._log_exporter": mock_log_module, }, ): config = LogRecordExporterConfig( otlp_http=OtlpHttpExporterConfig( endpoint="http://collector:4318", timeout=5000, ) ) _create_log_record_exporter(config) mock_exporter_cls.assert_called_once() call_kwargs = mock_exporter_cls.call_args.kwargs self.assertEqual(call_kwargs["endpoint"], "http://collector:4318") self.assertAlmostEqual(call_kwargs["timeout"], 5.0) def test_otlp_http_exporter_headers(self): mock_exporter_cls = MagicMock() mock_compression_cls = MagicMock() mock_module = MagicMock() mock_module.Compression = mock_compression_cls mock_log_module = MagicMock() mock_log_module.OTLPLogExporter = mock_exporter_cls with patch.dict( sys.modules, { "opentelemetry.exporter.otlp.proto.http": mock_module, "opentelemetry.exporter.otlp.proto.http._log_exporter": mock_log_module, }, ): config = LogRecordExporterConfig( otlp_http=OtlpHttpExporterConfig(headers=[NameStringValuePair(name="x-api-key", value="secret")]) ) _create_log_record_exporter(config) call_kwargs = mock_exporter_cls.call_args.kwargs self.assertEqual(call_kwargs["headers"], {"x-api-key": "secret"}) def test_otlp_http_exporter_deflate_compression(self): mock_exporter_cls = MagicMock() mock_compression_cls = MagicMock() mock_compression_cls.Deflate = "deflate" mock_module = MagicMock() mock_module.Compression = mock_compression_cls mock_log_module = MagicMock() mock_log_module.OTLPLogExporter = mock_exporter_cls with patch.dict( sys.modules, { "opentelemetry.exporter.otlp.proto.http": mock_module, "opentelemetry.exporter.otlp.proto.http._log_exporter": mock_log_module, }, ): config = LogRecordExporterConfig(otlp_http=OtlpHttpExporterConfig(compression="deflate")) _create_log_record_exporter(config) call_kwargs = mock_exporter_cls.call_args.kwargs self.assertEqual(call_kwargs["compression"], "deflate") def test_otlp_grpc_exporter_endpoint(self): mock_exporter_cls = MagicMock() mock_grpc = MagicMock() mock_grpc.Compression = MagicMock() mock_grpc_log_module = MagicMock() mock_grpc_log_module.OTLPLogExporter = mock_exporter_cls with patch.dict( sys.modules, { "grpc": mock_grpc, "opentelemetry.exporter.otlp.proto.grpc._log_exporter": mock_grpc_log_module, }, ): config = LogRecordExporterConfig( otlp_grpc=OtlpGrpcExporterConfig( endpoint="http://collector:4317", timeout=10000, ) ) _create_log_record_exporter(config) mock_exporter_cls.assert_called_once() call_kwargs = mock_exporter_cls.call_args.kwargs self.assertEqual(call_kwargs["endpoint"], "http://collector:4317") self.assertAlmostEqual(call_kwargs["timeout"], 10.0) class TestLogRecordLimits(unittest.TestCase): def test_default_limits(self): provider = create_logger_provider(None) self.assertEqual(provider._log_record_limits.max_attributes, 128) self.assertIsNone(provider._log_record_limits.max_attribute_length) self.assertEqual(provider._log_record_limits.max_log_record_attributes, 128) self.assertIsNone(provider._log_record_limits.max_log_record_attribute_length) def test_default_limits_do_not_read_env_vars(self): with patch.dict( os.environ, { "OTEL_ATTRIBUTE_COUNT_LIMIT": "1", "OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT": "2", }, ): provider = create_logger_provider(None) self.assertEqual(provider._log_record_limits.max_attributes, 128) self.assertIsNone(provider._log_record_limits.max_attribute_length) self.assertEqual(provider._log_record_limits.max_log_record_attributes, 128) self.assertIsNone(provider._log_record_limits.max_log_record_attribute_length) def test_limits_from_config(self): config = LoggerProviderConfig( processors=[], limits=LogRecordLimitsConfig( attribute_count_limit=64, attribute_value_length_limit=256, ), ) provider = create_logger_provider(config) self.assertEqual(provider._log_record_limits.max_attributes, 64) self.assertEqual(provider._log_record_limits.max_attribute_length, 256) self.assertEqual(provider._log_record_limits.max_log_record_attributes, 64) self.assertEqual(provider._log_record_limits.max_log_record_attribute_length, 256) def test_config_limits_are_not_overridden_by_log_record_env_vars(self): config = LoggerProviderConfig( processors=[], limits=LogRecordLimitsConfig( attribute_count_limit=64, attribute_value_length_limit=256, ), ) with patch.dict( os.environ, { "OTEL_LOGRECORD_ATTRIBUTE_COUNT_LIMIT": "5", "OTEL_LOGRECORD_ATTRIBUTE_VALUE_LENGTH_LIMIT": "3", }, ): provider = create_logger_provider(config) self.assertEqual(provider._log_record_limits.max_log_record_attributes, 64) self.assertEqual(provider._log_record_limits.max_log_record_attribute_length, 256) def test_default_limits_are_not_overridden_by_log_record_env_vars(self): with patch.dict( os.environ, { "OTEL_LOGRECORD_ATTRIBUTE_COUNT_LIMIT": "5", "OTEL_LOGRECORD_ATTRIBUTE_VALUE_LENGTH_LIMIT": "3", }, ): provider = create_logger_provider(None) self.assertEqual(provider._log_record_limits.max_log_record_attributes, 128) self.assertIsNone(provider._log_record_limits.max_log_record_attribute_length) @staticmethod def test_no_limits_no_warning(): config = LoggerProviderConfig(processors=[]) with patch("opentelemetry.configuration._logger_provider._logger") as mock_logger: create_logger_provider(config) mock_logger.warning.assert_not_called() def test_global_attribute_count_limit_used_when_no_per_signal_limits(self): global_limits = AttributeLimits(attribute_count_limit=42) provider = create_logger_provider(None, global_attribute_limits=global_limits) self.assertEqual(provider._log_record_limits.max_attributes, 42) self.assertEqual(provider._log_record_limits.max_log_record_attributes, 42) def test_global_attribute_value_length_limit_used_when_no_per_signal_limits( self, ): global_limits = AttributeLimits(attribute_value_length_limit=64) provider = create_logger_provider(None, global_attribute_limits=global_limits) self.assertEqual(provider._log_record_limits.max_attribute_length, 64) self.assertEqual(provider._log_record_limits.max_log_record_attribute_length, 64) def test_per_signal_limits_override_global(self): global_limits = AttributeLimits(attribute_count_limit=100, attribute_value_length_limit=200) config = LoggerProviderConfig( processors=[], limits=LogRecordLimitsConfig( attribute_count_limit=7, attribute_value_length_limit=16, ), ) provider = create_logger_provider(config, global_attribute_limits=global_limits) self.assertEqual(provider._log_record_limits.max_attributes, 7) self.assertEqual(provider._log_record_limits.max_attribute_length, 16) self.assertEqual(provider._log_record_limits.max_log_record_attributes, 7) self.assertEqual(provider._log_record_limits.max_log_record_attribute_length, 16) # Configurator tests access the SDK LoggerProvider private # _apply_logger_configurator to assert the wired config takes effect. # pylint: disable=protected-access class TestLoggerConfigurator(unittest.TestCase): @staticmethod def _enabled(provider, name): return provider._apply_logger_configurator(InstrumentationScope(name)).is_enabled def test_no_configurator_leaves_loggers_enabled(self): provider = create_logger_provider(LoggerProviderConfig(processors=[])) self.assertTrue(self._enabled(provider, "any.scope")) def test_matching_glob_disables_logger(self): config = LoggerProviderConfig( processors=[], logger_configurator_development=LoggerConfiguratorConfig( default_config=LoggerConfigConfig(enabled=True), loggers=[ LoggerMatcherAndConfig( name="noisy.*", config=LoggerConfigConfig(enabled=False), ) ], ), ) provider = create_logger_provider(config) self.assertFalse(self._enabled(provider, "noisy.http")) self.assertTrue(self._enabled(provider, "app.service")) def test_default_config_applies_to_unmatched_scopes(self): config = LoggerProviderConfig( processors=[], logger_configurator_development=LoggerConfiguratorConfig( default_config=LoggerConfigConfig(enabled=False), loggers=[ LoggerMatcherAndConfig( name="keep.*", config=LoggerConfigConfig(enabled=True), ) ], ), ) provider = create_logger_provider(config) self.assertTrue(self._enabled(provider, "keep.me")) self.assertFalse(self._enabled(provider, "other")) def test_first_matching_rule_wins(self): config = LoggerProviderConfig( processors=[], logger_configurator_development=LoggerConfiguratorConfig( loggers=[ LoggerMatcherAndConfig( name="a.*", config=LoggerConfigConfig(enabled=False), ), LoggerMatcherAndConfig( name="a.b", config=LoggerConfigConfig(enabled=True), ), ], ), ) provider = create_logger_provider(config) self.assertFalse(self._enabled(provider, "a.b")) def test_absent_enabled_defaults_to_enabled(self): config = LoggerProviderConfig( processors=[], logger_configurator_development=LoggerConfiguratorConfig( default_config=LoggerConfigConfig(), ), ) provider = create_logger_provider(config) self.assertTrue(self._enabled(provider, "any.scope")) def test_unsupported_minimum_severity_logs_warning(self): config = LoggerProviderConfig( processors=[], logger_configurator_development=LoggerConfiguratorConfig( default_config=LoggerConfigConfig( enabled=True, minimum_severity=SeverityNumber.warn, ), ), ) with self.assertLogs( "opentelemetry.configuration._logger_provider", level="WARNING", ) as cm: create_logger_provider(config) self.assertTrue( any("minimum_severity" in msg for msg in cm.output), "Expected warning about unsupported minimum_severity", ) self.assertFalse( any("trace_based" in msg for msg in cm.output), "Warning must not name trace_based when it is not set", ) def test_unsupported_trace_based_logs_warning(self): config = LoggerProviderConfig( processors=[], logger_configurator_development=LoggerConfiguratorConfig( default_config=LoggerConfigConfig( enabled=True, trace_based=True, ), ), ) with self.assertLogs( "opentelemetry.configuration._logger_provider", level="WARNING", ) as cm: create_logger_provider(config) self.assertTrue( any("trace_based" in msg for msg in cm.output), "Expected warning about unsupported trace_based", ) self.assertFalse( any("minimum_severity" in msg for msg in cm.output), "Warning must not name minimum_severity when it is not set", ) if __name__ == "__main__": unittest.main() python-opentelemetry-1.45.2/opentelemetry-configuration/tests/test_meter_provider.py000066400000000000000000001210241527070070700313650ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # Tests access private members of SDK classes to assert correct configuration. # pylint: disable=protected-access,too-many-lines import os import sys import unittest from unittest.mock import MagicMock, patch from opentelemetry.configuration._conversion import _dict_to_dataclass from opentelemetry.configuration._meter_provider import ( configure_meter_provider, create_meter_provider, ) from opentelemetry.configuration.file._loader import ConfigurationError from opentelemetry.configuration.models import ( Aggregation as AggregationConfig, ) from opentelemetry.configuration.models import ( Base2ExponentialBucketHistogramAggregation as Base2Config, ) from opentelemetry.configuration.models import ( CardinalityLimits as CardinalityLimitsConfig, ) from opentelemetry.configuration.models import ( ConsoleMetricExporter as ConsoleMetricExporterConfig, ) from opentelemetry.configuration.models import ( ExperimentalMeterConfig as MeterConfigConfig, ) from opentelemetry.configuration.models import ( ExperimentalMeterConfigurator as MeterConfiguratorConfig, ) from opentelemetry.configuration.models import ( ExperimentalMeterMatcherAndConfig as MeterMatcherAndConfig, ) from opentelemetry.configuration.models import ( ExperimentalOtlpFileMetricExporter as ExperimentalOtlpFileMetricExporterConfig, ) from opentelemetry.configuration.models import ( ExperimentalPrometheusMetricExporter as PrometheusMetricExporterConfig, ) from opentelemetry.configuration.models import ( ExplicitBucketHistogramAggregation as ExplicitBucketConfig, ) from opentelemetry.configuration.models import ( ExporterDefaultHistogramAggregation, ExporterTemporalityPreference, IncludeExclude, InstrumentType, ViewSelector, ViewStream, ) from opentelemetry.configuration.models import ( MeterProvider as MeterProviderConfig, ) from opentelemetry.configuration.models import ( MetricReader as MetricReaderConfig, ) from opentelemetry.configuration.models import ( OtlpGrpcMetricExporter as OtlpGrpcMetricExporterConfig, ) from opentelemetry.configuration.models import ( OtlpHttpMetricExporter as OtlpHttpMetricExporterConfig, ) from opentelemetry.configuration.models import ( PeriodicMetricReader as PeriodicMetricReaderConfig, ) from opentelemetry.configuration.models import ( PullMetricExporter as PullMetricExporterConfig, ) from opentelemetry.configuration.models import ( PullMetricReader as PullMetricReaderConfig, ) from opentelemetry.configuration.models import ( PushMetricExporter as PushMetricExporterConfig, ) from opentelemetry.configuration.models import ( View as ViewConfig, ) from opentelemetry.sdk.metrics import ( Counter, Histogram, MeterProvider, ObservableCounter, ObservableGauge, ObservableUpDownCounter, TraceBasedExemplarFilter, UpDownCounter, ) from opentelemetry.sdk.metrics._internal.instrument import ( _Counter, _Gauge, _Histogram, _ObservableCounter, _ObservableGauge, _ObservableUpDownCounter, _UpDownCounter, ) from opentelemetry.sdk.metrics.export import ( AggregationTemporality, ConsoleMetricExporter, PeriodicExportingMetricReader, ) from opentelemetry.sdk.metrics.view import ( DefaultAggregation, DropAggregation, ExplicitBucketHistogramAggregation, ExponentialBucketHistogramAggregation, LastValueAggregation, SumAggregation, View, ) from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.util.instrumentation import InstrumentationScope class TestCreateMeterProviderBasic(unittest.TestCase): def test_none_config_returns_provider(self): provider = create_meter_provider(None) self.assertIsInstance(provider, MeterProvider) def test_none_config_uses_supplied_resource(self): resource = Resource({"service.name": "svc"}) provider = create_meter_provider(None, resource) self.assertIs(provider._sdk_config.resource, resource) def test_none_config_no_readers(self): provider = create_meter_provider(None) self.assertEqual(len(provider._metric_readers), 0) def test_none_config_uses_trace_based_exemplar_filter(self): provider = create_meter_provider(None) self.assertIsInstance(provider._sdk_config.exemplar_filter, TraceBasedExemplarFilter) def test_none_config_does_not_read_exemplar_filter_env_var(self): with patch.dict(os.environ, {"OTEL_METRICS_EXEMPLAR_FILTER": "always_on"}): provider = create_meter_provider(None) self.assertIsInstance(provider._sdk_config.exemplar_filter, TraceBasedExemplarFilter) def test_none_config_does_not_read_interval_env_var(self): config = MeterProviderConfig( readers=[ MetricReaderConfig( periodic=PeriodicMetricReaderConfig( exporter=PushMetricExporterConfig(console=ConsoleMetricExporterConfig()) ) ) ] ) with patch.dict(os.environ, {"OTEL_METRIC_EXPORT_INTERVAL": "999999"}): provider = create_meter_provider(config) reader = provider._metric_readers[0] self.assertIsInstance(reader, PeriodicExportingMetricReader) self.assertEqual(reader._export_interval_millis, 60000.0) def test_configure_none_does_not_set_global(self): original = __import__("opentelemetry.metrics", fromlist=["get_meter_provider"]).get_meter_provider() configure_meter_provider(None) after = __import__("opentelemetry.metrics", fromlist=["get_meter_provider"]).get_meter_provider() self.assertIs(original, after) def test_configure_with_config_sets_global(self): config = MeterProviderConfig(readers=[]) with patch("opentelemetry.configuration._meter_provider.metrics.set_meter_provider") as mock_set: configure_meter_provider(config) mock_set.assert_called_once() arg = mock_set.call_args[0][0] self.assertIsInstance(arg, MeterProvider) class TestCreateMetricReaders(unittest.TestCase): @staticmethod def _make_periodic_config(exporter_config, interval=None, timeout=None): return MeterProviderConfig( readers=[ MetricReaderConfig( periodic=PeriodicMetricReaderConfig( exporter=exporter_config, interval=interval, timeout=timeout, ) ) ] ) def test_cardinality_limits_default_and_per_type(self): config = MeterProviderConfig( readers=[ MetricReaderConfig( periodic=PeriodicMetricReaderConfig( exporter=PushMetricExporterConfig(console=ConsoleMetricExporterConfig()), cardinality_limits=CardinalityLimitsConfig(default=10, histogram=20, observable_gauge=30), ) ) ] ) reader = create_meter_provider(config)._metric_readers[0] limits = reader._instrument_class_cardinality_limit expected = { _Counter: 10, _UpDownCounter: 10, _Histogram: 20, _Gauge: 10, _ObservableCounter: 10, _ObservableUpDownCounter: 10, _ObservableGauge: 30, } for instrument_class, limit in expected.items(): with self.subTest(instrument_class=instrument_class): self.assertEqual(limits[instrument_class], limit) def test_cardinality_limits_omitted_uses_sdk_default(self): config = self._make_periodic_config(PushMetricExporterConfig(console=ConsoleMetricExporterConfig())) reader = create_meter_provider(config)._metric_readers[0] self.assertEqual(set(reader._instrument_class_cardinality_limit.values()), {2000}) def test_console_exporter(self): config = self._make_periodic_config(PushMetricExporterConfig(console=ConsoleMetricExporterConfig())) provider = create_meter_provider(config) reader = provider._metric_readers[0] self.assertIsInstance(reader, PeriodicExportingMetricReader) self.assertIsInstance(reader._exporter, ConsoleMetricExporter) def test_null_valued_console_exporter_from_parsed_config(self): # Regression test for #5451: a dataclass-typed exporter written as # ``console:`` (present, null) must behave like ``console: {}`` rather # than requiring an explicit empty mapping. config = _dict_to_dataclass( { "readers": [ {"periodic": {"exporter": {"console": None}}}, ] }, MeterProviderConfig, ) provider = create_meter_provider(config) reader = provider._metric_readers[0] self.assertIsInstance(reader, PeriodicExportingMetricReader) self.assertIsInstance(reader._exporter, ConsoleMetricExporter) def test_periodic_reader_default_interval(self): config = self._make_periodic_config(PushMetricExporterConfig(console=ConsoleMetricExporterConfig())) provider = create_meter_provider(config) reader = provider._metric_readers[0] self.assertEqual(reader._export_interval_millis, 60000.0) def test_periodic_reader_default_timeout(self): config = self._make_periodic_config(PushMetricExporterConfig(console=ConsoleMetricExporterConfig())) provider = create_meter_provider(config) reader = provider._metric_readers[0] self.assertEqual(reader._export_timeout_millis, 30000.0) def test_periodic_reader_explicit_interval(self): config = self._make_periodic_config( PushMetricExporterConfig(console=ConsoleMetricExporterConfig()), interval=5000, ) provider = create_meter_provider(config) reader = provider._metric_readers[0] self.assertEqual(reader._export_interval_millis, 5000.0) def test_periodic_reader_explicit_timeout(self): config = self._make_periodic_config( PushMetricExporterConfig(console=ConsoleMetricExporterConfig()), timeout=10000, ) provider = create_meter_provider(config) reader = provider._metric_readers[0] self.assertEqual(reader._export_timeout_millis, 10000.0) def test_otlp_http_missing_package_raises(self): config = self._make_periodic_config(PushMetricExporterConfig(otlp_http=OtlpHttpMetricExporterConfig())) with ( patch.dict( sys.modules, { "opentelemetry.exporter.otlp.proto.http.metric_exporter": None, "opentelemetry.exporter.otlp.proto.http": None, }, ), self.assertRaises(ConfigurationError) as ctx, ): create_meter_provider(config) self.assertIn("otlp-proto-http", str(ctx.exception)) def test_otlp_http_created_with_endpoint(self): mock_exporter_cls = MagicMock() mock_compression_cls = MagicMock() mock_http_module = MagicMock() mock_http_module.Compression = mock_compression_cls mock_module = MagicMock() mock_module.OTLPMetricExporter = mock_exporter_cls with patch.dict( sys.modules, { "opentelemetry.exporter.otlp.proto.http.metric_exporter": mock_module, "opentelemetry.exporter.otlp.proto.http": mock_http_module, }, ): config = self._make_periodic_config( PushMetricExporterConfig(otlp_http=OtlpHttpMetricExporterConfig(endpoint="http://localhost:4318")) ) create_meter_provider(config) _, kwargs = mock_exporter_cls.call_args self.assertEqual(kwargs["endpoint"], "http://localhost:4318") self.assertIsNone(kwargs["headers"]) self.assertIsNone(kwargs["timeout"]) self.assertIsNone(kwargs["compression"]) def test_otlp_http_created_with_deflate_compression(self): mock_exporter_cls = MagicMock() mock_compression_cls = MagicMock() mock_compression_cls.Deflate = "deflate_val" mock_http_module = MagicMock() mock_http_module.Compression = mock_compression_cls mock_module = MagicMock() mock_module.OTLPMetricExporter = mock_exporter_cls with patch.dict( sys.modules, { "opentelemetry.exporter.otlp.proto.http.metric_exporter": mock_module, "opentelemetry.exporter.otlp.proto.http": mock_http_module, }, ): config = self._make_periodic_config( PushMetricExporterConfig(otlp_http=OtlpHttpMetricExporterConfig(compression="deflate")) ) create_meter_provider(config) _, kwargs = mock_exporter_cls.call_args self.assertEqual(kwargs["compression"], "deflate_val") def test_otlp_grpc_missing_package_raises(self): config = self._make_periodic_config(PushMetricExporterConfig(otlp_grpc=OtlpGrpcMetricExporterConfig())) with ( patch.dict( sys.modules, { "opentelemetry.exporter.otlp.proto.grpc.metric_exporter": None, "grpc": None, }, ), self.assertRaises(ConfigurationError) as ctx, ): create_meter_provider(config) self.assertIn("otlp-proto-grpc", str(ctx.exception)) def test_otlp_file_development_missing_package_raises(self): config = self._make_periodic_config( PushMetricExporterConfig(otlp_file_development=ExperimentalOtlpFileMetricExporterConfig()) ) with ( patch.dict( sys.modules, { "opentelemetry.exporter.otlp.json.file.metric_exporter": None, }, ), self.assertRaises(ConfigurationError) as ctx, ): create_meter_provider(config) self.assertIn("opentelemetry-exporter-otlp-json-file", str(ctx.exception)) def test_otlp_file_development_default_stdout(self): mock_exporter_cls = MagicMock() mock_module = MagicMock() mock_module.FileMetricExporter = mock_exporter_cls with patch.dict( sys.modules, { "opentelemetry.exporter.otlp.json.file.metric_exporter": mock_module, }, ): config = self._make_periodic_config( PushMetricExporterConfig(otlp_file_development=ExperimentalOtlpFileMetricExporterConfig()) ) create_meter_provider(config) args, kwargs = mock_exporter_cls.call_args self.assertEqual(args, ()) self.assertEqual( kwargs["preferred_temporality"], { Counter: AggregationTemporality.CUMULATIVE, UpDownCounter: AggregationTemporality.CUMULATIVE, Histogram: AggregationTemporality.CUMULATIVE, ObservableCounter: AggregationTemporality.CUMULATIVE, ObservableUpDownCounter: AggregationTemporality.CUMULATIVE, ObservableGauge: AggregationTemporality.CUMULATIVE, }, ) self.assertIsInstance( kwargs["preferred_aggregation"][Histogram], ExplicitBucketHistogramAggregation, ) def test_otlp_file_development_file_uri(self): mock_exporter_cls = MagicMock() mock_module = MagicMock() mock_module.FileMetricExporter = mock_exporter_cls with patch.dict( sys.modules, { "opentelemetry.exporter.otlp.json.file.metric_exporter": mock_module, }, ): config = self._make_periodic_config( PushMetricExporterConfig( otlp_file_development=ExperimentalOtlpFileMetricExporterConfig( output_stream="file:///tmp/metrics.jsonl" ) ) ) create_meter_provider(config) args, kwargs = mock_exporter_cls.call_args self.assertEqual(args, ("/tmp/metrics.jsonl",)) self.assertIn("preferred_temporality", kwargs) self.assertIn("preferred_aggregation", kwargs) def test_otlp_file_development_unsupported_output_stream_raises(self): mock_exporter_cls = MagicMock() mock_module = MagicMock() mock_module.FileMetricExporter = mock_exporter_cls with patch.dict( sys.modules, { "opentelemetry.exporter.otlp.json.file.metric_exporter": mock_module, }, ): config = self._make_periodic_config( PushMetricExporterConfig( otlp_file_development=ExperimentalOtlpFileMetricExporterConfig(output_stream="http://example") ) ) with self.assertRaises(ConfigurationError) as ctx: create_meter_provider(config) self.assertIn("output_stream", str(ctx.exception)) mock_exporter_cls.assert_not_called() class TestCreatePullMetricReaders(unittest.TestCase): def test_pull_prometheus_creates_reader(self): mock_reader_cls = MagicMock() mock_start_server = MagicMock() mock_module = MagicMock() mock_module.PrometheusMetricReader = mock_reader_cls mock_module.start_http_server = mock_start_server with patch.dict( sys.modules, {"opentelemetry.exporter.prometheus": mock_module}, ): config = MeterProviderConfig( readers=[ MetricReaderConfig( pull=PullMetricReaderConfig( exporter=PullMetricExporterConfig( prometheus_development=PrometheusMetricExporterConfig( host="0.0.0.0", port=9090, target_info_enabled_development=False, ) ) ) ) ] ) provider = create_meter_provider(config) mock_reader_cls.assert_called_once_with(disable_target_info=True, cardinality_limits=None) mock_start_server.assert_called_once_with(port=9090, addr="0.0.0.0") self.assertEqual(len(provider._metric_readers), 1) def test_pull_prometheus_defaults(self): mock_reader_cls = MagicMock() mock_start_server = MagicMock() mock_module = MagicMock() mock_module.PrometheusMetricReader = mock_reader_cls mock_module.start_http_server = mock_start_server with patch.dict( sys.modules, {"opentelemetry.exporter.prometheus": mock_module}, ): config = MeterProviderConfig( readers=[ MetricReaderConfig( pull=PullMetricReaderConfig( exporter=PullMetricExporterConfig(prometheus_development=PrometheusMetricExporterConfig()) ) ) ] ) provider = create_meter_provider(config) mock_reader_cls.assert_called_once_with(disable_target_info=False, cardinality_limits=None) mock_start_server.assert_called_once_with(port=9464, addr="localhost") self.assertEqual(len(provider._metric_readers), 1) def test_pull_prometheus_missing_package_raises(self): with patch.dict( sys.modules, {"opentelemetry.exporter.prometheus": None}, ): config = MeterProviderConfig( readers=[ MetricReaderConfig( pull=PullMetricReaderConfig( exporter=PullMetricExporterConfig(prometheus_development=PrometheusMetricExporterConfig()) ) ) ] ) with self.assertRaises(ConfigurationError): create_meter_provider(config) def test_pull_no_exporter_raises(self): config = MeterProviderConfig( readers=[MetricReaderConfig(pull=PullMetricReaderConfig(exporter=PullMetricExporterConfig()))] ) with self.assertRaises(ConfigurationError): create_meter_provider(config) def test_pull_plugin_loads_via_entry_point(self): mock_reader = MagicMock() mock_class = MagicMock(return_value=mock_reader) mock_entry_points = MagicMock(return_value=[MagicMock(**{"load.return_value": mock_class})]) with patch( "opentelemetry.configuration._common.entry_points", mock_entry_points, ): config = MeterProviderConfig( readers=[ MetricReaderConfig( pull=PullMetricReaderConfig( # pylint: disable=unexpected-keyword-arg exporter=PullMetricExporterConfig(my_custom_reader={"port": 8080}) ) ) ] ) provider = create_meter_provider(config) self.assertEqual(len(provider._metric_readers), 1) mock_class.assert_called_once_with(port=8080) mock_entry_points.assert_called_once_with( group="opentelemetry_pull_metric_exporter", name="my_custom_reader", ) def test_pull_plugin_cardinality_limits_warns(self): mock_class = MagicMock() mock_entry_points = MagicMock(return_value=[MagicMock(**{"load.return_value": mock_class})]) with patch( "opentelemetry.configuration._common.entry_points", mock_entry_points, ): config = MeterProviderConfig( readers=[ MetricReaderConfig( pull=PullMetricReaderConfig( # pylint: disable=unexpected-keyword-arg exporter=PullMetricExporterConfig(my_custom_reader={"port": 8080}), cardinality_limits=CardinalityLimitsConfig(default=5), ) ) ] ) with self.assertLogs( "opentelemetry.configuration._meter_provider", level="WARNING", ) as cm: create_meter_provider(config) self.assertTrue(any("cardinality_limits" in msg for msg in cm.output)) mock_class.assert_called_once_with(port=8080) def test_pull_plugin_not_found_raises(self): with patch( "opentelemetry.configuration._common.entry_points", return_value=[], ): config = MeterProviderConfig( readers=[ MetricReaderConfig( pull=PullMetricReaderConfig( # pylint: disable=unexpected-keyword-arg exporter=PullMetricExporterConfig(no_such_reader={}) ) ) ] ) with self.assertRaises(ConfigurationError): create_meter_provider(config) def test_pull_producers_warns(self): mock_module = MagicMock() with patch.dict( sys.modules, {"opentelemetry.exporter.prometheus": mock_module}, ): config = MeterProviderConfig( readers=[ MetricReaderConfig( pull=PullMetricReaderConfig( exporter=PullMetricExporterConfig(prometheus_development=PrometheusMetricExporterConfig()), producers=[MagicMock()], ) ) ] ) with self.assertLogs( "opentelemetry.configuration._meter_provider", level="WARNING", ) as cm: create_meter_provider(config) self.assertTrue(any("MetricProducer" in msg for msg in cm.output)) def test_pull_cardinality_limits_passed_to_prometheus_reader(self): mock_module = MagicMock() with patch.dict( sys.modules, {"opentelemetry.exporter.prometheus": mock_module}, ): config = MeterProviderConfig( readers=[ MetricReaderConfig( pull=PullMetricReaderConfig( exporter=PullMetricExporterConfig(prometheus_development=PrometheusMetricExporterConfig()), cardinality_limits=CardinalityLimitsConfig(default=5, counter=7), ) ) ] ) create_meter_provider(config) limits = mock_module.PrometheusMetricReader.call_args.kwargs["cardinality_limits"] self.assertEqual(limits[Counter], 7) self.assertEqual(limits[Histogram], 5) def test_pull_without_cardinality_limits(self): mock_module = MagicMock() with patch.dict( sys.modules, {"opentelemetry.exporter.prometheus": mock_module}, ): config = MeterProviderConfig( readers=[ MetricReaderConfig( pull=PullMetricReaderConfig( exporter=PullMetricExporterConfig(prometheus_development=PrometheusMetricExporterConfig()), ) ) ] ) create_meter_provider(config) self.assertIsNone(mock_module.PrometheusMetricReader.call_args.kwargs["cardinality_limits"]) class TestCreateMetricReadersGeneral(unittest.TestCase): @staticmethod def _make_periodic_config(exporter_config): return MeterProviderConfig( readers=[MetricReaderConfig(periodic=PeriodicMetricReaderConfig(exporter=exporter_config))] ) def test_no_reader_type_raises(self): config = MeterProviderConfig(readers=[MetricReaderConfig()]) with self.assertRaises(ConfigurationError): create_meter_provider(config) def test_no_exporter_type_raises(self): config = self._make_periodic_config(PushMetricExporterConfig()) with self.assertRaises(ConfigurationError): create_meter_provider(config) def test_plugin_metric_exporter_loaded_via_entry_point(self): mock_exporter = MagicMock() mock_class = MagicMock(return_value=mock_exporter) with patch( "opentelemetry.configuration._common.entry_points", return_value=[MagicMock(**{"load.return_value": mock_class})], ): # pylint: disable=unexpected-keyword-arg config = self._make_periodic_config(PushMetricExporterConfig(my_custom_exporter={})) provider = create_meter_provider(config) self.assertEqual(len(provider._metric_readers), 1) def test_unknown_metric_exporter_raises_configuration_error(self): with patch( "opentelemetry.configuration._common.entry_points", return_value=[], ): # pylint: disable=unexpected-keyword-arg config = self._make_periodic_config(PushMetricExporterConfig(no_such_exporter={})) with self.assertRaises(ConfigurationError): create_meter_provider(config) def test_multiple_readers(self): config = MeterProviderConfig( readers=[ MetricReaderConfig( periodic=PeriodicMetricReaderConfig( exporter=PushMetricExporterConfig(console=ConsoleMetricExporterConfig()) ) ), MetricReaderConfig( periodic=PeriodicMetricReaderConfig( exporter=PushMetricExporterConfig(console=ConsoleMetricExporterConfig()) ) ), ] ) provider = create_meter_provider(config) self.assertEqual(len(provider._metric_readers), 2) class TestTemporalityAndAggregation(unittest.TestCase): @staticmethod def _make_console_config(temporality=None, histogram_agg=None): return MeterProviderConfig( readers=[ MetricReaderConfig( periodic=PeriodicMetricReaderConfig( exporter=PushMetricExporterConfig( console=ConsoleMetricExporterConfig( temporality_preference=temporality, default_histogram_aggregation=histogram_agg, ) ) ) ) ] ) @staticmethod def _get_exporter(config): provider = create_meter_provider(config) return provider._metric_readers[0]._exporter def test_default_temporality_is_cumulative(self): exporter = self._get_exporter(self._make_console_config()) for instrument_type in ( Counter, UpDownCounter, Histogram, ObservableCounter, ObservableGauge, ObservableUpDownCounter, ): self.assertEqual( exporter._preferred_temporality[instrument_type], AggregationTemporality.CUMULATIVE, ) def test_cumulative_temporality(self): exporter = self._get_exporter(self._make_console_config(temporality=ExporterTemporalityPreference.cumulative)) self.assertEqual( exporter._preferred_temporality[Counter], AggregationTemporality.CUMULATIVE, ) def test_delta_temporality(self): exporter = self._get_exporter(self._make_console_config(temporality=ExporterTemporalityPreference.delta)) self.assertEqual( exporter._preferred_temporality[Counter], AggregationTemporality.DELTA, ) self.assertEqual( exporter._preferred_temporality[Histogram], AggregationTemporality.DELTA, ) self.assertEqual( exporter._preferred_temporality[UpDownCounter], AggregationTemporality.CUMULATIVE, ) self.assertEqual( exporter._preferred_temporality[ObservableCounter], AggregationTemporality.DELTA, ) def test_low_memory_temporality(self): exporter = self._get_exporter(self._make_console_config(temporality=ExporterTemporalityPreference.low_memory)) self.assertEqual( exporter._preferred_temporality[Counter], AggregationTemporality.DELTA, ) self.assertEqual( exporter._preferred_temporality[ObservableCounter], AggregationTemporality.CUMULATIVE, ) def test_default_histogram_aggregation_is_explicit(self): exporter = self._get_exporter(self._make_console_config()) self.assertIsInstance( exporter._preferred_aggregation[Histogram], ExplicitBucketHistogramAggregation, ) def test_explicit_histogram_aggregation(self): exporter = self._get_exporter( self._make_console_config(histogram_agg=ExporterDefaultHistogramAggregation.explicit_bucket_histogram) ) self.assertIsInstance( exporter._preferred_aggregation[Histogram], ExplicitBucketHistogramAggregation, ) def test_base2_exponential_histogram_aggregation(self): exporter = self._get_exporter( self._make_console_config( histogram_agg=ExporterDefaultHistogramAggregation.base2_exponential_bucket_histogram ) ) self.assertIsInstance( exporter._preferred_aggregation[Histogram], ExponentialBucketHistogramAggregation, ) def test_temporality_suppresses_env_var(self): with patch.dict( os.environ, {"OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE": "DELTA"}, ): exporter = self._get_exporter(self._make_console_config()) # Config has no preference → default cumulative, env var ignored self.assertEqual( exporter._preferred_temporality[Counter], AggregationTemporality.CUMULATIVE, ) class TestCreateViews(unittest.TestCase): @staticmethod def _make_view_config(selector_kwargs=None, stream_kwargs=None): selector = ViewSelector(**(selector_kwargs or {"instrument_name": "*"})) stream = ViewStream(**(stream_kwargs or {})) return MeterProviderConfig( readers=[], views=[ViewConfig(selector=selector, stream=stream)], ) @staticmethod def _get_view(config): provider = create_meter_provider(config) return provider._sdk_config.views[0] def test_view_created(self): config = self._make_view_config() provider = create_meter_provider(config) self.assertEqual(len(provider._sdk_config.views), 1) self.assertIsInstance(provider._sdk_config.views[0], View) def test_selector_instrument_name(self): view = self._get_view(self._make_view_config({"instrument_name": "my.metric"})) self.assertEqual(view._instrument_name, "my.metric") def test_selector_instrument_type(self): view = self._get_view(self._make_view_config({"instrument_type": InstrumentType.counter})) self.assertIs(view._instrument_type, Counter) def test_selector_meter_name(self): view = self._get_view(self._make_view_config({"meter_name": "my.meter"})) self.assertEqual(view._meter_name, "my.meter") def test_stream_name(self): view = self._get_view( self._make_view_config( {"instrument_name": "my.metric"}, stream_kwargs={"name": "renamed"}, ) ) self.assertEqual(view._name, "renamed") def test_stream_aggregation_cardinality_limit(self): view = self._get_view(self._make_view_config(stream_kwargs={"aggregation_cardinality_limit": 42})) self.assertEqual(view._aggregation_cardinality_limit, 42) def test_stream_aggregation_cardinality_limit_default(self): view = self._get_view(self._make_view_config()) self.assertIsNone(view._aggregation_cardinality_limit) def test_stream_description(self): view = self._get_view(self._make_view_config(stream_kwargs={"description": "a description"})) self.assertEqual(view._description, "a description") def test_stream_attribute_keys_included(self): view = self._get_view( self._make_view_config(stream_kwargs={"attribute_keys": IncludeExclude(included=["key1", "key2"])}) ) self.assertEqual(view._attribute_keys, {"key1", "key2"}) def test_stream_attribute_keys_excluded_logs_warning(self): config = self._make_view_config(stream_kwargs={"attribute_keys": IncludeExclude(excluded=["key1"])}) with self.assertLogs("opentelemetry.configuration._meter_provider", level="WARNING") as log: create_meter_provider(config) self.assertTrue(any("excluded" in msg for msg in log.output)) def test_stream_aggregation_drop(self): view = self._get_view(self._make_view_config(stream_kwargs={"aggregation": AggregationConfig(drop={})})) self.assertIsInstance(view._aggregation, DropAggregation) def test_stream_aggregation_explicit_bucket_histogram_with_boundaries( self, ): view = self._get_view( self._make_view_config( stream_kwargs={ "aggregation": AggregationConfig( explicit_bucket_histogram=ExplicitBucketConfig(boundaries=[1.0, 5.0, 10.0]) ) } ) ) self.assertIsInstance(view._aggregation, ExplicitBucketHistogramAggregation) self.assertEqual(list(view._aggregation._boundaries), [1.0, 5.0, 10.0]) def test_stream_aggregation_base2_exponential_with_params(self): view = self._get_view( self._make_view_config( stream_kwargs={ "aggregation": AggregationConfig( base2_exponential_bucket_histogram=Base2Config(max_size=64, max_scale=5) ) } ) ) self.assertIsInstance(view._aggregation, ExponentialBucketHistogramAggregation) def test_stream_aggregation_base2_exponential_record_min_max(self): for record_min_max, expected in [ (True, True), (False, False), (None, True), ]: with self.subTest(record_min_max=record_min_max): view = self._get_view( self._make_view_config( stream_kwargs={ "aggregation": AggregationConfig( base2_exponential_bucket_histogram=Base2Config(record_min_max=record_min_max) ) } ) ) self.assertIsInstance(view._aggregation, ExponentialBucketHistogramAggregation) self.assertEqual(view._aggregation._record_min_max, expected) def test_stream_aggregation_last_value(self): view = self._get_view(self._make_view_config(stream_kwargs={"aggregation": AggregationConfig(last_value={})})) self.assertIsInstance(view._aggregation, LastValueAggregation) def test_stream_aggregation_sum(self): view = self._get_view(self._make_view_config(stream_kwargs={"aggregation": AggregationConfig(sum={})})) self.assertIsInstance(view._aggregation, SumAggregation) def test_stream_aggregation_default(self): view = self._get_view(self._make_view_config(stream_kwargs={"aggregation": AggregationConfig(default={})})) self.assertIsInstance(view._aggregation, DefaultAggregation) class TestMeterConfigurator(unittest.TestCase): @staticmethod def _enabled(provider, name): return provider._apply_meter_configurator(InstrumentationScope(name)).is_enabled def test_no_configurator_leaves_meters_enabled(self): provider = create_meter_provider(MeterProviderConfig(readers=[])) self.assertTrue(self._enabled(provider, "any.scope")) def test_matching_glob_disables_meter(self): config = MeterProviderConfig( readers=[], meter_configurator_development=MeterConfiguratorConfig( default_config=MeterConfigConfig(enabled=True), meters=[ MeterMatcherAndConfig( name="noisy.*", config=MeterConfigConfig(enabled=False), ) ], ), ) provider = create_meter_provider(config) self.assertFalse(self._enabled(provider, "noisy.http")) self.assertTrue(self._enabled(provider, "app.service")) def test_default_config_applies_to_unmatched_scopes(self): config = MeterProviderConfig( readers=[], meter_configurator_development=MeterConfiguratorConfig( default_config=MeterConfigConfig(enabled=False), meters=[ MeterMatcherAndConfig( name="keep.*", config=MeterConfigConfig(enabled=True), ) ], ), ) provider = create_meter_provider(config) self.assertTrue(self._enabled(provider, "keep.me")) self.assertFalse(self._enabled(provider, "other")) def test_first_matching_rule_wins(self): config = MeterProviderConfig( readers=[], meter_configurator_development=MeterConfiguratorConfig( meters=[ MeterMatcherAndConfig( name="a.*", config=MeterConfigConfig(enabled=False), ), MeterMatcherAndConfig( name="a.b", config=MeterConfigConfig(enabled=True), ), ], ), ) provider = create_meter_provider(config) self.assertFalse(self._enabled(provider, "a.b")) def test_absent_enabled_defaults_to_enabled(self): config = MeterProviderConfig( readers=[], meter_configurator_development=MeterConfiguratorConfig( default_config=MeterConfigConfig(), ), ) provider = create_meter_provider(config) self.assertTrue(self._enabled(provider, "any.scope")) python-opentelemetry-1.45.2/opentelemetry-configuration/tests/test_meter_provider_exemplar_filter.py000066400000000000000000000032071527070070700346310ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # Tests access private members of SDK classes to assert correct configuration. # pylint: disable=protected-access import unittest from opentelemetry.configuration._meter_provider import ( create_meter_provider, ) from opentelemetry.configuration.models import ( ExemplarFilter as ExemplarFilterConfig, ) from opentelemetry.configuration.models import ( MeterProvider as MeterProviderConfig, ) from opentelemetry.sdk.metrics import ( AlwaysOffExemplarFilter, AlwaysOnExemplarFilter, TraceBasedExemplarFilter, ) class TestExemplarFilter(unittest.TestCase): @staticmethod def _make_config(exemplar_filter): return MeterProviderConfig(readers=[], exemplar_filter=exemplar_filter) def test_always_on(self): provider = create_meter_provider(self._make_config(ExemplarFilterConfig.always_on)) self.assertIsInstance(provider._sdk_config.exemplar_filter, AlwaysOnExemplarFilter) def test_always_off(self): provider = create_meter_provider(self._make_config(ExemplarFilterConfig.always_off)) self.assertIsInstance(provider._sdk_config.exemplar_filter, AlwaysOffExemplarFilter) def test_trace_based(self): provider = create_meter_provider(self._make_config(ExemplarFilterConfig.trace_based)) self.assertIsInstance(provider._sdk_config.exemplar_filter, TraceBasedExemplarFilter) def test_absent_defaults_to_trace_based(self): provider = create_meter_provider(MeterProviderConfig(readers=[])) self.assertIsInstance(provider._sdk_config.exemplar_filter, TraceBasedExemplarFilter) python-opentelemetry-1.45.2/opentelemetry-configuration/tests/test_models.py000066400000000000000000000007071527070070700276260ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import unittest class TestModelsImport(unittest.TestCase): def test_models_import(self): # pylint: disable=no-self-use """Verify generated models import successfully across all Python versions""" from opentelemetry.configuration import ( # pylint: disable=import-outside-toplevel,unused-import models, # noqa: F401 # type: ignore ) python-opentelemetry-1.45.2/opentelemetry-configuration/tests/test_propagator.py000066400000000000000000000265171527070070700305300ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import unittest from os import environ from unittest.mock import MagicMock, patch # CompositePropagator stores its propagators in _propagators (private). # We access it here to assert composition correctness. # pylint: disable=protected-access from opentelemetry.baggage.propagation import W3CBaggagePropagator from opentelemetry.configuration._exceptions import ConfigurationError from opentelemetry.configuration._propagator import ( configure_propagator, create_propagator, ) from opentelemetry.configuration.models import ( Propagator as PropagatorConfig, ) from opentelemetry.configuration.models import ( TextMapPropagator as TextMapPropagatorConfig, ) from opentelemetry.environment_variables import OTEL_PROPAGATORS from opentelemetry.propagators.composite import CompositePropagator from opentelemetry.trace.propagation.tracecontext import ( TraceContextTextMapPropagator, ) class TestCreatePropagator(unittest.TestCase): def test_none_config_returns_empty_composite(self): result = create_propagator(None) self.assertIsInstance(result, CompositePropagator) self.assertEqual(result._propagators, []) # type: ignore[attr-defined] def test_empty_config_returns_empty_composite(self): result = create_propagator(PropagatorConfig()) self.assertIsInstance(result, CompositePropagator) self.assertEqual(result._propagators, []) # type: ignore[attr-defined] def test_tracecontext_only(self): config = PropagatorConfig(composite=[TextMapPropagatorConfig(tracecontext={})]) result = create_propagator(config) self.assertEqual(len(result._propagators), 1) # type: ignore[attr-defined] self.assertIsInstance( result._propagators[0], TraceContextTextMapPropagator, # type: ignore[attr-defined] ) def test_baggage_only(self): config = PropagatorConfig(composite=[TextMapPropagatorConfig(baggage={})]) result = create_propagator(config) self.assertEqual(len(result._propagators), 1) # type: ignore[attr-defined] self.assertIsInstance(result._propagators[0], W3CBaggagePropagator) # type: ignore[attr-defined] def test_tracecontext_and_baggage(self): config = PropagatorConfig( composite=[ TextMapPropagatorConfig(tracecontext={}), TextMapPropagatorConfig(baggage={}), ] ) result = create_propagator(config) self.assertEqual(len(result._propagators), 2) # type: ignore[attr-defined] self.assertIsInstance( result._propagators[0], TraceContextTextMapPropagator, # type: ignore[attr-defined] ) self.assertIsInstance(result._propagators[1], W3CBaggagePropagator) # type: ignore[attr-defined] def test_b3_via_entry_point(self): mock_propagator = MagicMock() mock_ep = MagicMock() mock_ep.load.return_value = lambda: mock_propagator with patch( "opentelemetry.configuration._common.entry_points", return_value=[mock_ep], ): config = PropagatorConfig(composite=[TextMapPropagatorConfig(b3={})]) result = create_propagator(config) self.assertEqual(len(result._propagators), 1) # type: ignore[attr-defined] self.assertIs(result._propagators[0], mock_propagator) # type: ignore[attr-defined] def test_b3multi_via_entry_point(self): mock_propagator = MagicMock() mock_ep = MagicMock() mock_ep.load.return_value = lambda: mock_propagator with patch( "opentelemetry.configuration._common.entry_points", return_value=[mock_ep], ): config = PropagatorConfig(composite=[TextMapPropagatorConfig(b3multi={})]) result = create_propagator(config) self.assertEqual(len(result._propagators), 1) # type: ignore[attr-defined] def test_b3_not_installed_raises_configuration_error(self): with patch( "opentelemetry.configuration._common.entry_points", return_value=[], ): config = PropagatorConfig(composite=[TextMapPropagatorConfig(b3={})]) with self.assertRaises(ConfigurationError) as ctx: create_propagator(config) self.assertIn("b3", str(ctx.exception)) def test_composite_list_tracecontext(self): config = PropagatorConfig(composite_list="tracecontext") mock_tc = TraceContextTextMapPropagator() mock_ep = MagicMock() mock_ep.load.return_value = lambda: mock_tc with patch( "opentelemetry.configuration._common.entry_points", return_value=[mock_ep], ): result = create_propagator(config) self.assertEqual(len(result._propagators), 1) # type: ignore[attr-defined] def test_composite_list_multiple(self): mock_tc = TraceContextTextMapPropagator() mock_baggage = W3CBaggagePropagator() mock_ep_tc = MagicMock() mock_ep_tc.load.return_value = lambda: mock_tc mock_ep_baggage = MagicMock() mock_ep_baggage.load.return_value = lambda: mock_baggage def fake_entry_points(group, name): if name == "tracecontext": return [mock_ep_tc] if name == "baggage": return [mock_ep_baggage] return [] with patch( "opentelemetry.configuration._common.entry_points", side_effect=fake_entry_points, ): config = PropagatorConfig(composite_list="tracecontext,baggage") result = create_propagator(config) self.assertEqual(len(result._propagators), 2) # type: ignore[attr-defined] def test_composite_list_none_entry_skipped(self): config = PropagatorConfig(composite_list="none") result = create_propagator(config) self.assertEqual(result._propagators, []) # type: ignore[attr-defined] def test_composite_list_empty_entries_skipped(self): config = PropagatorConfig(composite_list=",, ,") result = create_propagator(config) self.assertEqual(result._propagators, []) # type: ignore[attr-defined] def test_composite_list_whitespace_around_names(self): mock_tc = TraceContextTextMapPropagator() mock_ep = MagicMock() mock_ep.load.return_value = lambda: mock_tc with patch( "opentelemetry.configuration._common.entry_points", return_value=[mock_ep], ): config = PropagatorConfig(composite_list=" tracecontext ") result = create_propagator(config) self.assertEqual(len(result._propagators), 1) # type: ignore[attr-defined] def test_entry_point_load_exception_raises_configuration_error(self): mock_ep = MagicMock() mock_ep.load.side_effect = RuntimeError("package broken") with patch( "opentelemetry.configuration._common.entry_points", return_value=[mock_ep], ): config = PropagatorConfig(composite_list="broken-prop") with self.assertRaises(ConfigurationError) as ctx: create_propagator(config) self.assertIn("broken-prop", str(ctx.exception)) def test_deduplication_across_composite_and_composite_list(self): """Same propagator type from both composite and composite_list is deduplicated.""" mock_tc = TraceContextTextMapPropagator() mock_ep = MagicMock() mock_ep.load.return_value = lambda: mock_tc with patch( "opentelemetry.configuration._common.entry_points", return_value=[mock_ep], ): config = PropagatorConfig( composite=[TextMapPropagatorConfig(tracecontext={})], composite_list="tracecontext", ) result = create_propagator(config) # Only one TraceContextTextMapPropagator despite being in both tc_count = sum( 1 for p in result._propagators # type: ignore[attr-defined] if isinstance(p, TraceContextTextMapPropagator) ) self.assertEqual(tc_count, 1) def test_unknown_composite_list_propagator_raises(self): with patch( "opentelemetry.configuration._common.entry_points", return_value=[], ): config = PropagatorConfig(composite_list="nonexistent") with self.assertRaises(ConfigurationError): create_propagator(config) def test_plugin_propagator_via_entry_point(self): mock_propagator = MagicMock() mock_ep = MagicMock() mock_ep.load.return_value = lambda: mock_propagator with patch( "opentelemetry.configuration._common.entry_points", return_value=[mock_ep], ): config = PropagatorConfig( composite=[ # pylint: disable=unexpected-keyword-arg TextMapPropagatorConfig(my_custom_propagator={}) ] ) result = create_propagator(config) self.assertEqual(len(result._propagators), 1) # type: ignore[attr-defined] self.assertIs(result._propagators[0], mock_propagator) # type: ignore[attr-defined] def test_unknown_composite_propagator_raises(self): with patch( "opentelemetry.configuration._common.entry_points", return_value=[], ): config = PropagatorConfig( composite=[ # pylint: disable=unexpected-keyword-arg TextMapPropagatorConfig(nonexistent={}) ] ) with self.assertRaises(ConfigurationError): create_propagator(config) class TestConfigurePropagator(unittest.TestCase): def test_configure_propagator_calls_set_global_textmap(self): with patch("opentelemetry.configuration._propagator.set_global_textmap") as mock_set: configure_propagator(None) mock_set.assert_called_once() arg = mock_set.call_args[0][0] self.assertIsInstance(arg, CompositePropagator) def test_configure_propagator_with_config(self): config = PropagatorConfig(composite=[TextMapPropagatorConfig(tracecontext={})]) with patch("opentelemetry.configuration._propagator.set_global_textmap") as mock_set: configure_propagator(config) mock_set.assert_called_once() propagator = mock_set.call_args[0][0] self.assertIsInstance(propagator, CompositePropagator) self.assertEqual(len(propagator._propagators), 1) # type: ignore[attr-defined] @patch.dict(environ, {OTEL_PROPAGATORS: "baggage"}) def test_otel_propagators_env_var_ignored(self): """OTEL_PROPAGATORS env var must not influence configure_propagator output.""" config = PropagatorConfig(composite=[TextMapPropagatorConfig(tracecontext={})]) with patch("opentelemetry.configuration._propagator.set_global_textmap") as mock_set: configure_propagator(config) propagator = mock_set.call_args[0][0] self.assertEqual(len(propagator._propagators), 1) # type: ignore[attr-defined] self.assertIsInstance( propagator._propagators[0], TraceContextTextMapPropagator, # type: ignore[attr-defined] ) python-opentelemetry-1.45.2/opentelemetry-configuration/tests/test_resource.py000066400000000000000000000631301527070070700301710ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import os import socket import sys import unittest from unittest.mock import MagicMock, patch from opentelemetry.configuration._conversion import _dict_to_dataclass from opentelemetry.configuration._exceptions import ConfigurationError from opentelemetry.configuration._resource import create_resource from opentelemetry.configuration.models import ( AttributeNameValue, AttributeType, ExperimentalResourceDetection, ExperimentalResourceDetector, IncludeExclude, ) from opentelemetry.configuration.models import Resource as ResourceConfig from opentelemetry.sdk.resources import ( CONTAINER_ID, HOST_ARCH, HOST_NAME, PROCESS_PID, PROCESS_RUNTIME_NAME, SERVICE_INSTANCE_ID, SERVICE_NAME, TELEMETRY_SDK_LANGUAGE, TELEMETRY_SDK_NAME, TELEMETRY_SDK_VERSION, Resource, ) class TestCreateResourceDefaults(unittest.TestCase): @patch("sys.executable", "/usr/bin/python3") def test_none_config_returns_sdk_defaults(self): resource = create_resource(None) self.assertIsInstance(resource, Resource) self.assertEqual(resource.attributes[TELEMETRY_SDK_LANGUAGE], "python") self.assertEqual(resource.attributes[TELEMETRY_SDK_NAME], "opentelemetry") self.assertIn(TELEMETRY_SDK_VERSION, resource.attributes) self.assertEqual(resource.attributes[SERVICE_NAME], "unknown_service:python3") @patch("sys.executable", "/usr/bin/python3") def test_none_config_does_not_read_env_vars(self): with patch.dict( os.environ, { "OTEL_RESOURCE_ATTRIBUTES": "foo=bar", "OTEL_SERVICE_NAME": "my-service", }, ): resource = create_resource(None) self.assertNotIn("foo", resource.attributes) self.assertEqual(resource.attributes[SERVICE_NAME], "unknown_service:python3") @patch("sys.executable", "/usr/bin/python3") def test_empty_resource_config(self): resource = create_resource(ResourceConfig()) self.assertEqual(resource.attributes[TELEMETRY_SDK_LANGUAGE], "python") self.assertEqual(resource.attributes[SERVICE_NAME], "unknown_service:python3") @patch("sys.executable", "/usr/bin/python3") def test_service_name_default_added_when_missing(self): config = ResourceConfig(attributes=[AttributeNameValue(name="env", value="staging")]) resource = create_resource(config) self.assertEqual(resource.attributes[SERVICE_NAME], "unknown_service:python3") def test_service_name_not_overridden_when_set(self): config = ResourceConfig(attributes=[AttributeNameValue(name="service.name", value="my-app")]) resource = create_resource(config) self.assertEqual(resource.attributes[SERVICE_NAME], "my-app") @patch("sys.executable", None) def test_default_service_name_without_sys_executable_returns_plain_unkwnon_service(self): resource = create_resource(None) self.assertEqual(resource.attributes[SERVICE_NAME], "unknown_service") def test_env_vars_not_read(self): """OTEL_RESOURCE_ATTRIBUTES must not affect declarative config resource.""" with patch.dict( os.environ, {"OTEL_RESOURCE_ATTRIBUTES": "injected=true"}, ): config = ResourceConfig(attributes=[AttributeNameValue(name="k", value="v")]) resource = create_resource(config) self.assertNotIn("injected", resource.attributes) def test_schema_url(self): config = ResourceConfig(schema_url="https://opentelemetry.io/schemas/1.24.0") resource = create_resource(config) self.assertEqual(resource.schema_url, "https://opentelemetry.io/schemas/1.24.0") def test_schema_url_none(self): resource = create_resource(ResourceConfig()) self.assertEqual(resource.schema_url, "") class TestCreateResourceAttributes(unittest.TestCase): def test_attributes_plain(self): config = ResourceConfig( attributes=[ AttributeNameValue(name="service.name", value="my-service"), AttributeNameValue(name="env", value="production"), ] ) resource = create_resource(config) self.assertEqual(resource.attributes["service.name"], "my-service") self.assertEqual(resource.attributes["env"], "production") # SDK defaults still present self.assertEqual(resource.attributes[TELEMETRY_SDK_LANGUAGE], "python") def test_attribute_type_string(self): config = ResourceConfig(attributes=[AttributeNameValue(name="k", value=42, type=AttributeType.string)]) resource = create_resource(config) self.assertEqual(resource.attributes["k"], "42") self.assertIsInstance(resource.attributes["k"], str) def test_attribute_type_int(self): config = ResourceConfig(attributes=[AttributeNameValue(name="k", value=3.0, type=AttributeType.int)]) resource = create_resource(config) self.assertEqual(resource.attributes["k"], 3) self.assertIsInstance(resource.attributes["k"], int) def test_attribute_type_double(self): config = ResourceConfig(attributes=[AttributeNameValue(name="k", value="1.5", type=AttributeType.double)]) resource = create_resource(config) self.assertAlmostEqual(resource.attributes["k"], 1.5) # type: ignore[arg-type] self.assertIsInstance(resource.attributes["k"], float) def test_attribute_type_bool(self): config = ResourceConfig(attributes=[AttributeNameValue(name="k", value="true", type=AttributeType.bool)]) resource = create_resource(config) self.assertTrue(resource.attributes["k"]) def test_attribute_type_bool_false_string(self): config = ResourceConfig(attributes=[AttributeNameValue(name="k", value="false", type=AttributeType.bool)]) resource = create_resource(config) self.assertFalse(resource.attributes["k"]) def test_attribute_type_string_array(self): config = ResourceConfig( attributes=[ AttributeNameValue( name="k", value=["a", "b"], type=AttributeType.string_array, ) ] ) resource = create_resource(config) self.assertEqual(list(resource.attributes["k"]), ["a", "b"]) # type: ignore[arg-type] def test_attribute_type_int_array(self): config = ResourceConfig( attributes=[ AttributeNameValue( name="k", value=[1.0, 2.0], type=AttributeType.int_array, ) ] ) resource = create_resource(config) self.assertEqual(list(resource.attributes["k"]), [1, 2]) # type: ignore[arg-type] def test_attribute_type_double_array(self): config = ResourceConfig( attributes=[ AttributeNameValue( name="k", value=[1, 2], type=AttributeType.double_array, ) ] ) resource = create_resource(config) self.assertEqual(list(resource.attributes["k"]), [1.0, 2.0]) # type: ignore[arg-type] def test_attribute_type_bool_array(self): config = ResourceConfig( attributes=[ AttributeNameValue( name="k", value=[True, False], type=AttributeType.bool_array, ) ] ) resource = create_resource(config) self.assertEqual(list(resource.attributes["k"]), [True, False]) # type: ignore[arg-type] def test_none_value_attribute_skipped_with_warning(self): """An unset ${VAR} with no default yields a null value; it must be skipped (not inserted as None or coerced) and a warning logged.""" with self.assertLogs("opentelemetry.configuration._resource", level="WARNING") as cm: config = ResourceConfig( attributes=[ AttributeNameValue(name="empty", value=None), AttributeNameValue(name="env", value="production"), ] ) resource = create_resource(config) self.assertNotIn("empty", resource.attributes) self.assertEqual(resource.attributes["env"], "production") self.assertTrue(any("empty" in msg for msg in cm.output)) def test_none_value_typed_attribute_skipped(self): """A null value with a declared type must be skipped, not coerced (int(None)/str(None) would raise or produce garbage).""" with self.assertLogs("opentelemetry.configuration._resource", level="WARNING"): config = ResourceConfig(attributes=[AttributeNameValue(name="k", value=None, type=AttributeType.int)]) resource = create_resource(config) self.assertNotIn("k", resource.attributes) def test_attribute_type_bool_array_string_values(self): """bool_array must use _coerce_bool, not plain bool() — 'false' must be False.""" config = ResourceConfig( attributes=[ AttributeNameValue( name="k", value=["true", "false"], type=AttributeType.bool_array, ) ] ) resource = create_resource(config) self.assertEqual(list(resource.attributes["k"]), [True, False]) # type: ignore[arg-type] class TestCreateResourceAttributesList(unittest.TestCase): def test_attributes_list_parsed(self): config = ResourceConfig(attributes_list="service.name=my-svc,region=us-east-1") resource = create_resource(config) self.assertEqual(resource.attributes["service.name"], "my-svc") self.assertEqual(resource.attributes["region"], "us-east-1") def test_attributes_list_does_not_override_attributes(self): """Explicit attributes take precedence over attributes_list.""" config = ResourceConfig( attributes=[AttributeNameValue(name="service.name", value="explicit")], attributes_list="service.name=from-list,extra=val", ) resource = create_resource(config) self.assertEqual(resource.attributes["service.name"], "explicit") self.assertEqual(resource.attributes["extra"], "val") def test_attributes_list_only_includes_sdk_defaults(self): """attributes_list alone should still include telemetry.sdk.* defaults.""" config = ResourceConfig(attributes_list="env=prod") resource = create_resource(config) self.assertEqual(resource.attributes["env"], "prod") self.assertEqual(resource.attributes[TELEMETRY_SDK_LANGUAGE], "python") def test_attributes_list_value_containing_equals(self): """Values containing '=' should be preserved intact.""" config = ResourceConfig(attributes_list="token=abc=def=ghi") resource = create_resource(config) self.assertEqual(resource.attributes["token"], "abc=def=ghi") def test_attributes_list_empty_pairs_skipped(self): config = ResourceConfig(attributes_list=",foo=bar,,") resource = create_resource(config) self.assertEqual(resource.attributes["foo"], "bar") def test_attributes_list_url_decoded(self): config = ResourceConfig(attributes_list="service.namespace=my%20namespace,region=us-east-1") resource = create_resource(config) self.assertEqual(resource.attributes["service.namespace"], "my namespace") def test_attributes_list_invalid_pair_skipped(self): with self.assertLogs("opentelemetry.configuration._resource", level="WARNING") as cm: config = ResourceConfig(attributes_list="no-equals,foo=bar") resource = create_resource(config) self.assertEqual(resource.attributes["foo"], "bar") self.assertNotIn("no-equals", resource.attributes) self.assertTrue(any("no-equals" in msg for msg in cm.output)) class TestServiceResourceDetector(unittest.TestCase): @staticmethod def _config_with_service() -> ResourceConfig: return ResourceConfig( detection_development=ExperimentalResourceDetection(detectors=[ExperimentalResourceDetector(service={})]) ) def test_service_detector_adds_instance_id(self): resource = create_resource(self._config_with_service()) self.assertIn(SERVICE_INSTANCE_ID, resource.attributes) def test_service_instance_id_is_stable_within_process(self): r1 = create_resource(self._config_with_service()) r2 = create_resource(self._config_with_service()) self.assertEqual( r1.attributes[SERVICE_INSTANCE_ID], r2.attributes[SERVICE_INSTANCE_ID], ) def test_service_detector_reads_otel_service_name_env_var(self): with patch.dict(os.environ, {"OTEL_SERVICE_NAME": "my-service"}): resource = create_resource(self._config_with_service()) self.assertEqual(resource.attributes[SERVICE_NAME], "my-service") @patch("sys.executable", "/usr/bin/python3") def test_service_detector_no_env_var_leaves_default_service_name(self): with patch.dict(os.environ, {}, clear=True): resource = create_resource(self._config_with_service()) self.assertEqual(resource.attributes[SERVICE_NAME], "unknown_service:python3") def test_explicit_service_name_overrides_env_var(self): """Config attributes win over the service detector's env-var value.""" config = ResourceConfig( attributes=[AttributeNameValue(name="service.name", value="explicit-svc")], detection_development=ExperimentalResourceDetection(detectors=[ExperimentalResourceDetector(service={})]), ) with patch.dict(os.environ, {"OTEL_SERVICE_NAME": "env-svc"}): resource = create_resource(config) self.assertEqual(resource.attributes[SERVICE_NAME], "explicit-svc") def test_null_valued_detector_from_parsed_config(self): # Regression test for #5451: a detector written as ``- service:`` # (present, null) is parsed to ``{"service": None}``. It must run the # detector just like ``- service: {}`` rather than being skipped. config = _dict_to_dataclass( {"detection_development": {"detectors": [{"service": None}]}}, ResourceConfig, ) resource = create_resource(config) self.assertIn(SERVICE_INSTANCE_ID, resource.attributes) def test_service_detector_not_run_when_absent(self): resource = create_resource(ResourceConfig()) self.assertNotIn(SERVICE_INSTANCE_ID, resource.attributes) def test_service_detector_not_run_when_detection_development_is_none(self): resource = create_resource(ResourceConfig(detection_development=None)) self.assertNotIn(SERVICE_INSTANCE_ID, resource.attributes) def test_service_detector_also_includes_sdk_defaults(self): resource = create_resource(self._config_with_service()) self.assertEqual(resource.attributes[TELEMETRY_SDK_LANGUAGE], "python") self.assertIn(TELEMETRY_SDK_VERSION, resource.attributes) @patch("sys.executable", "/usr/bin/python3") def test_included_filter_limits_service_attributes(self): config = ResourceConfig( detection_development=ExperimentalResourceDetection( detectors=[ExperimentalResourceDetector(service={})], attributes=IncludeExclude(included=["service.instance.id"]), ) ) with patch.dict(os.environ, {"OTEL_SERVICE_NAME": "my-service"}): resource = create_resource(config) self.assertIn(SERVICE_INSTANCE_ID, resource.attributes) # service.name comes from the filter-excluded detector output, but the # default "unknown_service" is still added by create_resource directly self.assertEqual(resource.attributes[SERVICE_NAME], "unknown_service:python3") class TestHostResourceDetector(unittest.TestCase): @staticmethod def _config_with_host() -> ResourceConfig: return ResourceConfig( detection_development=ExperimentalResourceDetection(detectors=[ExperimentalResourceDetector(host={})]) ) def test_host_detector_adds_host_attributes(self): resource = create_resource(self._config_with_host()) self.assertIn(HOST_NAME, resource.attributes) self.assertEqual(resource.attributes[HOST_NAME], socket.gethostname()) self.assertIn(HOST_ARCH, resource.attributes) def test_host_detector_also_includes_sdk_defaults(self): resource = create_resource(self._config_with_host()) self.assertEqual(resource.attributes[TELEMETRY_SDK_LANGUAGE], "python") self.assertIn(TELEMETRY_SDK_VERSION, resource.attributes) def test_host_detector_not_run_when_absent(self): resource = create_resource(ResourceConfig()) self.assertNotIn(HOST_NAME, resource.attributes) self.assertNotIn(HOST_ARCH, resource.attributes) def test_host_detector_not_run_when_detection_development_is_none(self): resource = create_resource(ResourceConfig(detection_development=None)) self.assertNotIn(HOST_NAME, resource.attributes) def test_host_detector_not_run_when_detectors_list_empty(self): config = ResourceConfig(detection_development=ExperimentalResourceDetection(detectors=[])) resource = create_resource(config) self.assertNotIn(HOST_NAME, resource.attributes) def test_explicit_attributes_override_host_detector(self): config = ResourceConfig( attributes=[AttributeNameValue(name="host.name", value="custom-host")], detection_development=ExperimentalResourceDetection(detectors=[ExperimentalResourceDetector(host={})]), ) resource = create_resource(config) self.assertEqual(resource.attributes[HOST_NAME], "custom-host") def test_included_filter_limits_host_attributes(self): config = ResourceConfig( detection_development=ExperimentalResourceDetection( detectors=[ExperimentalResourceDetector(host={})], attributes=IncludeExclude(included=["host.name"]), ) ) resource = create_resource(config) self.assertIn(HOST_NAME, resource.attributes) self.assertNotIn(HOST_ARCH, resource.attributes) def test_excluded_filter_removes_host_attributes(self): config = ResourceConfig( detection_development=ExperimentalResourceDetection( detectors=[ExperimentalResourceDetector(host={})], attributes=IncludeExclude(excluded=["host.name"]), ) ) resource = create_resource(config) self.assertNotIn(HOST_NAME, resource.attributes) self.assertIn(HOST_ARCH, resource.attributes) class TestContainerResourceDetector(unittest.TestCase): @staticmethod def _config_with_container() -> ResourceConfig: return ResourceConfig( detection_development=ExperimentalResourceDetection(detectors=[ExperimentalResourceDetector(container={})]) ) def test_container_detector_not_run_when_absent(self): resource = create_resource(ResourceConfig()) self.assertNotIn(CONTAINER_ID, resource.attributes) def test_container_detector_not_run_when_detection_development_is_none( self, ): resource = create_resource(ResourceConfig(detection_development=None)) self.assertNotIn(CONTAINER_ID, resource.attributes) def test_container_detector_not_run_when_detectors_list_empty(self): config = ResourceConfig(detection_development=ExperimentalResourceDetection(detectors=[])) resource = create_resource(config) self.assertNotIn(CONTAINER_ID, resource.attributes) def test_container_detector_raises_when_package_missing(self): """ConfigurationError is raised when the contrib entry point is not found.""" with ( patch( "opentelemetry.configuration._common.entry_points", return_value=[], ), self.assertRaises(ConfigurationError), ): create_resource(self._config_with_container()) def test_container_detector_uses_contrib_when_available(self): """When the contrib entry point is registered, container.id is detected.""" mock_resource = Resource({CONTAINER_ID: "abc123"}) mock_detector = MagicMock() mock_detector.return_value.detect.return_value = mock_resource mock_ep = MagicMock() mock_ep.load.return_value = mock_detector with patch( "opentelemetry.configuration._common.entry_points", return_value=[mock_ep], ): resource = create_resource(self._config_with_container()) self.assertEqual(resource.attributes[CONTAINER_ID], "abc123") def test_explicit_attributes_override_container_detector(self): """Config attributes win over detector-provided values.""" mock_resource = Resource({CONTAINER_ID: "detected-id"}) mock_detector = MagicMock() mock_detector.return_value.detect.return_value = mock_resource mock_ep = MagicMock() mock_ep.load.return_value = mock_detector config = ResourceConfig( attributes=[AttributeNameValue(name="container.id", value="explicit-id")], detection_development=ExperimentalResourceDetection(detectors=[ExperimentalResourceDetector(container={})]), ) with patch( "opentelemetry.configuration._common.entry_points", return_value=[mock_ep], ): resource = create_resource(config) self.assertEqual(resource.attributes[CONTAINER_ID], "explicit-id") class TestProcessResourceDetector(unittest.TestCase): @staticmethod def _config_with_process() -> ResourceConfig: return ResourceConfig( detection_development=ExperimentalResourceDetection(detectors=[ExperimentalResourceDetector(process={})]) ) def test_process_detector_adds_process_attributes(self): resource = create_resource(self._config_with_process()) self.assertIn(PROCESS_PID, resource.attributes) self.assertEqual(resource.attributes[PROCESS_PID], os.getpid()) self.assertEqual( resource.attributes[PROCESS_RUNTIME_NAME], sys.implementation.name, ) def test_process_detector_also_includes_sdk_defaults(self): resource = create_resource(self._config_with_process()) self.assertEqual(resource.attributes[TELEMETRY_SDK_LANGUAGE], "python") self.assertIn(TELEMETRY_SDK_VERSION, resource.attributes) def test_process_detector_not_run_when_absent(self): resource = create_resource(ResourceConfig()) self.assertNotIn(PROCESS_PID, resource.attributes) def test_process_detector_not_run_when_detection_development_is_none(self): resource = create_resource(ResourceConfig(detection_development=None)) self.assertNotIn(PROCESS_PID, resource.attributes) def test_process_detector_not_run_when_detectors_list_empty(self): config = ResourceConfig(detection_development=ExperimentalResourceDetection(detectors=[])) resource = create_resource(config) self.assertNotIn(PROCESS_PID, resource.attributes) def test_explicit_attributes_override_process_detector(self): """Config attributes win over detector-provided values.""" config = ResourceConfig( attributes=[AttributeNameValue(name="process.pid", value=99999, type=AttributeType.int)], detection_development=ExperimentalResourceDetection(detectors=[ExperimentalResourceDetector(process={})]), ) resource = create_resource(config) self.assertEqual(resource.attributes[PROCESS_PID], 99999) def test_multiple_detector_entries_run_process_once(self): """Multiple detector list entries each with process={} should still work.""" config = ResourceConfig( detection_development=ExperimentalResourceDetection( detectors=[ ExperimentalResourceDetector(process={}), ExperimentalResourceDetector(process={}), ] ) ) resource = create_resource(config) self.assertEqual(resource.attributes[PROCESS_PID], os.getpid()) class TestPluginResourceDetector(unittest.TestCase): def test_plugin_detector_loaded_via_entry_point(self): mock_resource = Resource({"custom.attr": "value"}) mock_detector = MagicMock() mock_detector.return_value.detect.return_value = mock_resource mock_ep = MagicMock() mock_ep.load.return_value = mock_detector config = ResourceConfig( detection_development=ExperimentalResourceDetection( # pylint: disable=unexpected-keyword-arg detectors=[ExperimentalResourceDetector(my_custom_detector={})] ) ) with patch( "opentelemetry.configuration._common.entry_points", return_value=[mock_ep], ): resource = create_resource(config) self.assertEqual(resource.attributes["custom.attr"], "value") def test_unknown_detector_raises_configuration_error(self): config = ResourceConfig( detection_development=ExperimentalResourceDetection( # pylint: disable=unexpected-keyword-arg detectors=[ExperimentalResourceDetector(no_such_detector={})] ) ) with ( patch( "opentelemetry.configuration._common.entry_points", return_value=[], ), self.assertRaises(ConfigurationError), ): create_resource(config) python-opentelemetry-1.45.2/opentelemetry-configuration/tests/test_sdk.py000066400000000000000000000214771527070070700271330ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # Tests access private members of SDK classes to assert correct configuration. # pylint: disable=protected-access import logging import unittest from unittest.mock import patch from opentelemetry.configuration._sdk import configure_sdk from opentelemetry.configuration.models import ( OpenTelemetryConfiguration, SeverityNumber, ) from opentelemetry.configuration.models import ( Propagator as PropagatorConfig, ) from opentelemetry.configuration.models import ( Resource as ResourceConfig, ) from opentelemetry.configuration.models import ( SimpleSpanProcessor as SimpleSpanProcessorConfig, ) from opentelemetry.configuration.models import ( SpanExporter as SpanExporterConfig, ) from opentelemetry.configuration.models import ( SpanProcessor as SpanProcessorConfig, ) from opentelemetry.configuration.models import ( TracerProvider as TracerProviderConfig, ) from opentelemetry.sdk.trace import TracerProvider as SdkTracerProvider _MIN_CONFIG_KWARGS = {"file_format": "1.0"} def _config(**kwargs) -> OpenTelemetryConfiguration: return OpenTelemetryConfiguration(**{**_MIN_CONFIG_KWARGS, **kwargs}) class TestConfigureSdk(unittest.TestCase): @patch("opentelemetry.configuration._sdk.configure_propagator") @patch("opentelemetry.configuration._sdk.configure_logger_provider") @patch("opentelemetry.configuration._sdk.configure_meter_provider") @patch("opentelemetry.configuration._sdk.configure_tracer_provider") @patch("opentelemetry.configuration._sdk.create_resource") # pylint: disable=no-self-use def test_calls_each_signal_with_resource( self, mock_create_resource, mock_tracer, mock_meter, mock_logger, mock_propagator, ): sentinel_resource = object() mock_create_resource.return_value = sentinel_resource resource_cfg = ResourceConfig() tracer_cfg = TracerProviderConfig(processors=[]) propagator_cfg = PropagatorConfig() config = _config( resource=resource_cfg, tracer_provider=tracer_cfg, propagator=propagator_cfg, ) configure_sdk(config) mock_create_resource.assert_called_once_with(resource_cfg) mock_tracer.assert_called_once_with(tracer_cfg, sentinel_resource, None) mock_meter.assert_called_once_with(None, sentinel_resource) mock_logger.assert_called_once_with(None, sentinel_resource, None) mock_propagator.assert_called_once_with(propagator_cfg) @patch("opentelemetry.configuration._sdk.configure_propagator") @patch("opentelemetry.configuration._sdk.configure_logger_provider") @patch("opentelemetry.configuration._sdk.configure_meter_provider") @patch("opentelemetry.configuration._sdk.configure_tracer_provider") @patch("opentelemetry.configuration._sdk.create_resource") # pylint: disable=no-self-use def test_disabled_skips_everything( self, mock_create_resource, mock_tracer, mock_meter, mock_logger, mock_propagator, ): config = _config( disabled=True, tracer_provider=TracerProviderConfig(processors=[]), ) configure_sdk(config) mock_create_resource.assert_not_called() mock_tracer.assert_not_called() mock_meter.assert_not_called() mock_logger.assert_not_called() mock_propagator.assert_not_called() @patch("opentelemetry.configuration._sdk.configure_propagator") @patch("opentelemetry.configuration._sdk.configure_logger_provider") @patch("opentelemetry.configuration._sdk.configure_meter_provider") @patch("opentelemetry.configuration._sdk.configure_tracer_provider") @patch("opentelemetry.configuration._sdk.create_resource") def test_absent_sections_pass_none( self, mock_create_resource, mock_tracer, mock_meter, mock_logger, mock_propagator, ): configure_sdk(_config()) # Each configure_* is called exactly once, with config=None. self.assertEqual(mock_tracer.call_args.args[0], None) self.assertEqual(mock_meter.call_args.args[0], None) self.assertEqual(mock_logger.call_args.args[0], None) self.assertEqual(mock_propagator.call_args.args[0], None) class TestConfigureSdkLogLevel(unittest.TestCase): def setUp(self): # Preserve whatever level was set before this test so we can # restore it in tearDown, keeping tests isolated from each other # and from the ambient logging configuration. self._original_level = logging.getLogger("opentelemetry").level def tearDown(self): logging.getLogger("opentelemetry").setLevel(self._original_level) @patch("opentelemetry.configuration._sdk.configure_propagator") @patch("opentelemetry.configuration._sdk.configure_logger_provider") @patch("opentelemetry.configuration._sdk.configure_meter_provider") @patch("opentelemetry.configuration._sdk.configure_tracer_provider") @patch("opentelemetry.configuration._sdk.create_resource") def test_sets_opentelemetry_logger_level(self, *_mocks): configure_sdk(_config(log_level=SeverityNumber.warn)) self.assertEqual(logging.getLogger("opentelemetry").level, logging.WARNING) @patch("opentelemetry.configuration._sdk.configure_propagator") @patch("opentelemetry.configuration._sdk.configure_logger_provider") @patch("opentelemetry.configuration._sdk.configure_meter_provider") @patch("opentelemetry.configuration._sdk.configure_tracer_provider") @patch("opentelemetry.configuration._sdk.create_resource") def test_absent_log_level_leaves_logger_unchanged(self, *_mocks): logging.getLogger("opentelemetry").setLevel(logging.ERROR) configure_sdk(_config()) self.assertEqual(logging.getLogger("opentelemetry").level, logging.ERROR) @patch("opentelemetry.configuration._sdk.configure_propagator") @patch("opentelemetry.configuration._sdk.configure_logger_provider") @patch("opentelemetry.configuration._sdk.configure_meter_provider") @patch("opentelemetry.configuration._sdk.configure_tracer_provider") @patch("opentelemetry.configuration._sdk.create_resource") def test_severity_number_variants_map_correctly(self, *_mocks): cases = [ (SeverityNumber.trace, logging.DEBUG), (SeverityNumber.trace2, logging.DEBUG), (SeverityNumber.trace3, logging.DEBUG), (SeverityNumber.trace4, logging.DEBUG), (SeverityNumber.debug, logging.DEBUG), (SeverityNumber.debug2, logging.DEBUG), (SeverityNumber.debug3, logging.DEBUG), (SeverityNumber.debug4, logging.DEBUG), (SeverityNumber.info, logging.INFO), (SeverityNumber.info2, logging.INFO), (SeverityNumber.info3, logging.INFO), (SeverityNumber.info4, logging.INFO), (SeverityNumber.warn, logging.WARNING), (SeverityNumber.warn2, logging.WARNING), (SeverityNumber.warn3, logging.WARNING), (SeverityNumber.warn4, logging.WARNING), (SeverityNumber.error, logging.ERROR), (SeverityNumber.error2, logging.ERROR), (SeverityNumber.error3, logging.ERROR), (SeverityNumber.error4, logging.ERROR), (SeverityNumber.fatal, logging.CRITICAL), (SeverityNumber.fatal2, logging.CRITICAL), (SeverityNumber.fatal3, logging.CRITICAL), (SeverityNumber.fatal4, logging.CRITICAL), ] for severity, expected_level in cases: with self.subTest(severity=severity): configure_sdk(_config(log_level=severity)) self.assertEqual( logging.getLogger("opentelemetry").level, expected_level, ) def test_log_level_not_applied_when_disabled(self): logging.getLogger("opentelemetry").setLevel(logging.WARNING) configure_sdk(_config(disabled=True, log_level=SeverityNumber.error)) self.assertEqual(logging.getLogger("opentelemetry").level, logging.WARNING) class TestConfigureSdkIntegration(unittest.TestCase): """End-to-end: build a real OpenTelemetryConfiguration and apply it.""" @patch("opentelemetry.configuration._tracer_provider.trace.set_tracer_provider") def test_applies_tracer_provider_globally(self, mock_set_tracer): config = _config( tracer_provider=TracerProviderConfig( processors=[ SpanProcessorConfig(simple=SimpleSpanProcessorConfig(exporter=SpanExporterConfig(console={}))) ] ) ) configure_sdk(config) mock_set_tracer.assert_called_once() self.assertIsInstance(mock_set_tracer.call_args[0][0], SdkTracerProvider) python-opentelemetry-1.45.2/opentelemetry-configuration/tests/test_tracer_provider.py000066400000000000000000001146641527070070700315450ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # Tests access private members of SDK classes to assert correct configuration. # pylint: disable=protected-access # pylint: disable=too-many-lines import os import sys import unittest from unittest.mock import MagicMock, patch from opentelemetry import trace as trace_api from opentelemetry.configuration._conversion import _dict_to_dataclass from opentelemetry.configuration._tracer_provider import ( configure_tracer_provider, create_tracer_provider, ) from opentelemetry.configuration.file._loader import ConfigurationError from opentelemetry.configuration.models import ( AttributeLimits, ) from opentelemetry.configuration.models import ( BatchSpanProcessor as BatchSpanProcessorConfig, ) from opentelemetry.configuration.models import ( ExperimentalComposableProbabilitySampler as ComposableProbabilityConfig, ) from opentelemetry.configuration.models import ( ExperimentalComposableRuleBasedSampler as RuleBasedSamplerConfig, ) from opentelemetry.configuration.models import ( ExperimentalComposableRuleBasedSamplerRule as RuleBasedSamplerRuleConfig, ) from opentelemetry.configuration.models import ( ExperimentalComposableRuleBasedSamplerRuleAttributePatterns as RuleAttributePatternsConfig, ) from opentelemetry.configuration.models import ( ExperimentalComposableRuleBasedSamplerRuleAttributeValues as RuleAttributeValuesConfig, ) from opentelemetry.configuration.models import ( ExperimentalComposableSampler as ComposableSamplerConfig, ) from opentelemetry.configuration.models import ( ExperimentalOtlpFileExporter as ExperimentalOtlpFileExporterConfig, ) from opentelemetry.configuration.models import ( ExperimentalSpanParent as SpanParentConfig, ) from opentelemetry.configuration.models import ( ExperimentalTracerConfig as TracerConfigConfig, ) from opentelemetry.configuration.models import ( ExperimentalTracerConfigurator as TracerConfiguratorConfig, ) from opentelemetry.configuration.models import ( ExperimentalTracerMatcherAndConfig as TracerMatcherAndConfig, ) from opentelemetry.configuration.models import ( IdGenerator as IdGeneratorConfig, ) from opentelemetry.configuration.models import ( OtlpGrpcExporter as OtlpGrpcExporterConfig, ) from opentelemetry.configuration.models import ( OtlpHttpExporter as OtlpHttpExporterConfig, ) from opentelemetry.configuration.models import ( ParentBasedSampler as ParentBasedSamplerConfig, ) from opentelemetry.configuration.models import ( Sampler as SamplerConfig, ) from opentelemetry.configuration.models import ( SimpleSpanProcessor as SimpleSpanProcessorConfig, ) from opentelemetry.configuration.models import ( SpanExporter as SpanExporterConfig, ) from opentelemetry.configuration.models import ( SpanKind as SpanKindConfig, ) from opentelemetry.configuration.models import ( SpanLimits as SpanLimitsConfig, ) from opentelemetry.configuration.models import ( SpanProcessor as SpanProcessorConfig, ) from opentelemetry.configuration.models import ( TraceIdRatioBasedSampler as TraceIdRatioBasedConfig, ) from opentelemetry.configuration.models import ( TracerProvider as TracerProviderConfig, ) from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import ( BatchSpanProcessor, ConsoleSpanExporter, SimpleSpanProcessor, ) from opentelemetry.sdk.trace.id_generator import RandomIdGenerator from opentelemetry.sdk.trace.sampling import ( ALWAYS_OFF, ALWAYS_ON, Decision, ParentBased, Sampler, TraceIdRatioBased, ) from opentelemetry.sdk.util.instrumentation import InstrumentationScope from opentelemetry.trace import SpanContext, TraceFlags from opentelemetry.trace import SpanKind as TraceSpanKind TRACE_ID = int("00112233445566778800000000000000", 16) SPAN_ID = int("0123456789abcdef", 16) class TestCreateTracerProviderBasic(unittest.TestCase): def test_none_config_returns_provider(self): resource = Resource({"service.name": "test"}) provider = create_tracer_provider(None, resource) self.assertIsInstance(provider, TracerProvider) def test_none_config_uses_supplied_resource(self): resource = Resource({"service.name": "svc"}) provider = create_tracer_provider(None, resource) self.assertIs(provider._resource, resource) def test_none_config_uses_default_sampler(self): provider = create_tracer_provider(None) self.assertIsInstance(provider.sampler, ParentBased) def test_none_config_no_processors(self): provider = create_tracer_provider(None) self.assertEqual(len(provider._active_span_processor._span_processors), 0) def test_none_config_does_not_read_sampler_env_var(self): with patch.dict(os.environ, {"OTEL_TRACES_SAMPLER": "always_off"}): provider = create_tracer_provider(None) self.assertIsInstance(provider.sampler, ParentBased) def test_none_config_does_not_read_span_limit_env_var(self): with patch.dict(os.environ, {"OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT": "1"}): provider = create_tracer_provider(None) self.assertEqual(provider._span_limits.max_span_attributes, 128) def test_configure_none_does_not_set_global(self): original = __import__("opentelemetry.trace", fromlist=["get_tracer_provider"]).get_tracer_provider() configure_tracer_provider(None) after = __import__("opentelemetry.trace", fromlist=["get_tracer_provider"]).get_tracer_provider() self.assertIs(original, after) def test_configure_with_config_sets_global(self): config = TracerProviderConfig(processors=[]) with patch("opentelemetry.configuration._tracer_provider.trace.set_tracer_provider") as mock_set: configure_tracer_provider(config) mock_set.assert_called_once() arg = mock_set.call_args[0][0] self.assertIsInstance(arg, TracerProvider) def test_processors_added_in_order(self): mock_proc_a = MagicMock() mock_proc_b = MagicMock() config = TracerProviderConfig(processors=[]) provider = create_tracer_provider(config) provider.add_span_processor(mock_proc_a) provider.add_span_processor(mock_proc_b) procs = provider._active_span_processor._span_processors self.assertIs(procs[0], mock_proc_a) self.assertIs(procs[1], mock_proc_b) def test_span_limits_from_config(self): config = TracerProviderConfig( processors=[], limits=SpanLimitsConfig( attribute_count_limit=5, event_count_limit=10, link_count_limit=3, ), ) provider = create_tracer_provider(config) self.assertEqual(provider._span_limits.max_span_attributes, 5) self.assertEqual(provider._span_limits.max_events, 10) self.assertEqual(provider._span_limits.max_links, 3) class TestCreateSampler(unittest.TestCase): @staticmethod def _make_provider(sampler_config): return create_tracer_provider(TracerProviderConfig(processors=[], sampler=sampler_config)) def test_always_on(self): provider = self._make_provider(SamplerConfig(always_on={})) self.assertIs(provider.sampler, ALWAYS_ON) def test_always_off(self): provider = self._make_provider(SamplerConfig(always_off={})) self.assertIs(provider.sampler, ALWAYS_OFF) def test_trace_id_ratio_based(self): provider = self._make_provider(SamplerConfig(trace_id_ratio_based=TraceIdRatioBasedConfig(ratio=0.5))) self.assertIsInstance(provider.sampler, TraceIdRatioBased) self.assertAlmostEqual(provider.sampler._rate, 0.5) def test_trace_id_ratio_based_none_ratio_defaults_to_1(self): provider = self._make_provider(SamplerConfig(trace_id_ratio_based=TraceIdRatioBasedConfig())) self.assertIsInstance(provider.sampler, TraceIdRatioBased) self.assertAlmostEqual(provider.sampler._rate, 1.0) def test_parent_based_with_root(self): provider = self._make_provider( SamplerConfig(parent_based=ParentBasedSamplerConfig(root=SamplerConfig(always_on={}))) ) self.assertIsInstance(provider.sampler, ParentBased) def test_parent_based_no_root_defaults_to_always_on(self): provider = self._make_provider(SamplerConfig(parent_based=ParentBasedSamplerConfig())) self.assertIsInstance(provider.sampler, ParentBased) self.assertIs(provider.sampler._root, ALWAYS_ON) def test_parent_based_with_delegate_samplers(self): provider = self._make_provider( SamplerConfig( parent_based=ParentBasedSamplerConfig( root=SamplerConfig(always_on={}), remote_parent_sampled=SamplerConfig(always_on={}), remote_parent_not_sampled=SamplerConfig(always_off={}), local_parent_sampled=SamplerConfig(always_on={}), local_parent_not_sampled=SamplerConfig(always_off={}), ) ) ) sampler = provider.sampler self.assertIsInstance(sampler, ParentBased) self.assertIs(sampler._remote_parent_sampled, ALWAYS_ON) self.assertIs(sampler._remote_parent_not_sampled, ALWAYS_OFF) self.assertIs(sampler._local_parent_sampled, ALWAYS_ON) self.assertIs(sampler._local_parent_not_sampled, ALWAYS_OFF) def test_no_sampler_raises_configuration_error(self): with self.assertRaises(ConfigurationError): self._make_provider(SamplerConfig()) def test_null_valued_always_on_from_parsed_config(self): # Regression test for #5451: a leaf sampler written as ``always_on:`` # (present, null) is parsed to ``None`` by the YAML loader. It must be # treated the same as ``always_on: {}`` rather than failing type # dispatch. sampler_config = _dict_to_dataclass({"always_on": None}, SamplerConfig) provider = self._make_provider(sampler_config) self.assertIs(provider.sampler, ALWAYS_ON) def test_null_valued_parent_based_delegates_from_parsed_config(self): # Regression test for #5451: the exact parent_based config from the # issue, where every leaf delegate is written with a null value. sampler_config = _dict_to_dataclass( { "parent_based": { "root": {"always_on": None}, "remote_parent_sampled": {"always_on": None}, "remote_parent_not_sampled": {"always_off": None}, "local_parent_sampled": {"always_on": None}, "local_parent_not_sampled": {"always_off": None}, } }, SamplerConfig, ) provider = self._make_provider(sampler_config) sampler = provider.sampler self.assertIsInstance(sampler, ParentBased) self.assertIs(sampler._root, ALWAYS_ON) self.assertIs(sampler._remote_parent_sampled, ALWAYS_ON) self.assertIs(sampler._remote_parent_not_sampled, ALWAYS_OFF) self.assertIs(sampler._local_parent_sampled, ALWAYS_ON) self.assertIs(sampler._local_parent_not_sampled, ALWAYS_OFF) def test_user_defined_sampler_loaded_via_entry_point(self): mock_sampler = MagicMock(spec=Sampler) mock_class = MagicMock(return_value=mock_sampler) with patch( "opentelemetry.configuration._common.entry_points", return_value=[MagicMock(**{"load.return_value": mock_class})], ): # pylint: disable=unexpected-keyword-arg provider = self._make_provider(SamplerConfig(my_custom_sampler={})) self.assertIs(provider.sampler, mock_sampler) def test_user_defined_sampler_not_found_raises_configuration_error(self): with ( patch( "opentelemetry.configuration._common.entry_points", return_value=[], ), self.assertRaises(ConfigurationError), ): # pylint: disable=unexpected-keyword-arg self._make_provider(SamplerConfig(no_such_sampler={})) class TestCreateCompositeRuleBasedSampler(unittest.TestCase): @staticmethod def _make_provider(rule_based_config): return create_tracer_provider( TracerProviderConfig( processors=[], sampler=SamplerConfig(composite_development=ComposableSamplerConfig(rule_based=rule_based_config)), ) ) @staticmethod def _rule(sampler, **kwargs): return RuleBasedSamplerRuleConfig(sampler=sampler, **kwargs) @staticmethod def _always_on(): return ComposableSamplerConfig(always_on={}) @staticmethod def _always_off(): return ComposableSamplerConfig(always_off={}) @staticmethod def _attribute_values(key, values): return RuleAttributeValuesConfig(key=key, values=values) @staticmethod def _attribute_patterns(key, included=None, excluded=None): return RuleAttributePatternsConfig( key=key, included=included, excluded=excluded, ) def _decision( self, rule_based_config, *, name="span", kind=None, attributes=None, parent_context=None, ): provider = self._make_provider(rule_based_config) return provider.sampler.should_sample( parent_context, TRACE_ID, name, kind, attributes, None, ).decision def test_composite_rule_based_no_rules_drops(self): decision = self._decision(RuleBasedSamplerConfig()) self.assertEqual(decision, Decision.DROP) def test_composite_rule_based_no_condition_rule_matches(self): decision = self._decision(RuleBasedSamplerConfig(rules=[self._rule(self._always_on())])) self.assertEqual(decision, Decision.RECORD_AND_SAMPLE) def test_composite_rule_based_first_match_wins(self): rule_based = RuleBasedSamplerConfig( rules=[ self._rule( self._always_off(), attribute_values=self._attribute_values("http.route", ["/health"]), ), self._rule( self._always_on(), attribute_values=self._attribute_values("http.route", ["/health"]), ), ] ) decision = self._decision(rule_based, attributes={"http.route": "/health"}) self.assertEqual(decision, Decision.DROP) def test_composite_rule_based_attribute_values_stringifies_values(self): decision = self._decision( RuleBasedSamplerConfig( rules=[ self._rule( self._always_on(), attribute_values=self._attribute_values("http.response.status_code", ["404"]), ) ] ), attributes={"http.response.status_code": 404}, ) self.assertEqual(decision, Decision.RECORD_AND_SAMPLE) def test_composite_rule_based_attribute_values_match_array_item(self): decision = self._decision( RuleBasedSamplerConfig( rules=[ self._rule( self._always_on(), attribute_values=self._attribute_values("http.request.method", ["POST"]), ) ] ), attributes={"http.request.method": ("GET", "POST")}, ) self.assertEqual(decision, Decision.RECORD_AND_SAMPLE) def test_composite_rule_based_attribute_patterns_include_exclude(self): rule_based = RuleBasedSamplerConfig( rules=[ self._rule( self._always_on(), attribute_patterns=self._attribute_patterns( "http.route", ["/api/*"], ["/api/private/*"], ), ) ] ) included = self._decision(rule_based, attributes={"http.route": "/api/users"}) excluded = self._decision(rule_based, attributes={"http.route": "/api/private/user"}) case_mismatch = self._decision(rule_based, attributes={"http.route": "/API/users"}) self.assertEqual(included, Decision.RECORD_AND_SAMPLE) self.assertEqual(excluded, Decision.DROP) self.assertEqual(case_mismatch, Decision.DROP) def test_composite_rule_based_span_kind(self): rule_based = RuleBasedSamplerConfig( rules=[ self._rule( self._always_on(), span_kinds=[SpanKindConfig.server], ) ] ) server = self._decision(rule_based, kind=TraceSpanKind.SERVER) client = self._decision(rule_based, kind=TraceSpanKind.CLIENT) self.assertEqual(server, Decision.RECORD_AND_SAMPLE) self.assertEqual(client, Decision.DROP) def test_composite_rule_based_parent(self): local_parent_context = trace_api.set_span_in_context( trace_api.NonRecordingSpan( SpanContext( TRACE_ID, SPAN_ID, is_remote=False, trace_flags=TraceFlags.get_default(), ) ) ) remote_parent_context = trace_api.set_span_in_context( trace_api.NonRecordingSpan( SpanContext( TRACE_ID, SPAN_ID, is_remote=True, trace_flags=TraceFlags.get_default(), ) ) ) rule_based = RuleBasedSamplerConfig( rules=[ self._rule( self._always_on(), parent=[SpanParentConfig.local], ) ] ) local = self._decision(rule_based, parent_context=local_parent_context) remote = self._decision(rule_based, parent_context=remote_parent_context) no_parent = self._decision(rule_based) self.assertEqual(local, Decision.RECORD_AND_SAMPLE) self.assertEqual(remote, Decision.DROP) self.assertEqual(no_parent, Decision.DROP) def test_composite_rule_based_multiple_conditions_are_anded(self): rule_based = RuleBasedSamplerConfig( rules=[ self._rule( self._always_on(), attribute_values=self._attribute_values("http.route", ["/users"]), span_kinds=[SpanKindConfig.server], ) ] ) matching = self._decision( rule_based, kind=TraceSpanKind.SERVER, attributes={"http.route": "/users"}, ) wrong_kind = self._decision( rule_based, kind=TraceSpanKind.CLIENT, attributes={"http.route": "/users"}, ) self.assertEqual(matching, Decision.RECORD_AND_SAMPLE) self.assertEqual(wrong_kind, Decision.DROP) def test_composite_rule_based_nested_probability_sampler(self): provider = self._make_provider( RuleBasedSamplerConfig( rules=[self._rule(ComposableSamplerConfig(probability=ComposableProbabilityConfig(ratio=0.0)))] ) ) expected = "ComposableRuleBased{[(AlwaysMatch:ComposableTraceIDRatioBased{threshold=max, ratio=0.0})]}" self.assertEqual( provider.sampler.get_description(), expected, ) class TestCreateSpanExporterAndProcessor(unittest.TestCase): # pylint: disable=no-self-use @staticmethod def _make_batch_config(exporter_config): return TracerProviderConfig( processors=[SpanProcessorConfig(batch=BatchSpanProcessorConfig(exporter=exporter_config))] ) @staticmethod def _make_simple_config(exporter_config): return TracerProviderConfig( processors=[SpanProcessorConfig(simple=SimpleSpanProcessorConfig(exporter=exporter_config))] ) def test_console_exporter_batch(self): config = self._make_batch_config(SpanExporterConfig(console={})) provider = create_tracer_provider(config) procs = provider._active_span_processor._span_processors self.assertEqual(len(procs), 1) self.assertIsInstance(procs[0], BatchSpanProcessor) self.assertIsInstance(procs[0].span_exporter, ConsoleSpanExporter) def test_console_exporter_simple(self): config = self._make_simple_config(SpanExporterConfig(console={})) provider = create_tracer_provider(config) procs = provider._active_span_processor._span_processors self.assertIsInstance(procs[0], SimpleSpanProcessor) self.assertIsInstance(procs[0].span_exporter, ConsoleSpanExporter) def test_otlp_http_missing_package_raises(self): config = self._make_batch_config(SpanExporterConfig(otlp_http=OtlpHttpExporterConfig())) with ( patch.dict( sys.modules, { "opentelemetry.exporter.otlp.proto.http.trace_exporter": None, "opentelemetry.exporter.otlp.proto.http": None, }, ), self.assertRaises(ConfigurationError) as ctx, ): create_tracer_provider(config) self.assertIn("otlp-proto-http", str(ctx.exception)) def test_otlp_http_created_with_endpoint(self): mock_exporter_cls = MagicMock() mock_compression_cls = MagicMock() mock_compression_cls.Gzip = "gzip_val" mock_module = MagicMock() mock_module.OTLPSpanExporter = mock_exporter_cls mock_http_module = MagicMock() mock_http_module.Compression = mock_compression_cls with patch.dict( sys.modules, { "opentelemetry.exporter.otlp.proto.http.trace_exporter": mock_module, "opentelemetry.exporter.otlp.proto.http": mock_http_module, }, ): config = self._make_batch_config( SpanExporterConfig(otlp_http=OtlpHttpExporterConfig(endpoint="http://localhost:4318")) ) create_tracer_provider(config) mock_exporter_cls.assert_called_once_with( endpoint="http://localhost:4318", headers=None, timeout=None, compression=None, ) def test_otlp_http_created_with_deflate_compression(self): mock_exporter_cls = MagicMock() mock_compression_cls = MagicMock() mock_compression_cls.Deflate = "deflate_val" mock_module = MagicMock() mock_module.OTLPSpanExporter = mock_exporter_cls mock_http_module = MagicMock() mock_http_module.Compression = mock_compression_cls with patch.dict( sys.modules, { "opentelemetry.exporter.otlp.proto.http.trace_exporter": mock_module, "opentelemetry.exporter.otlp.proto.http": mock_http_module, }, ): config = self._make_batch_config( SpanExporterConfig(otlp_http=OtlpHttpExporterConfig(compression="deflate")) ) create_tracer_provider(config) _, kwargs = mock_exporter_cls.call_args self.assertEqual(kwargs["compression"], "deflate_val") def test_otlp_http_headers_list(self): mock_exporter_cls = MagicMock() mock_http_module = MagicMock() mock_module = MagicMock() mock_module.OTLPSpanExporter = mock_exporter_cls with patch.dict( sys.modules, { "opentelemetry.exporter.otlp.proto.http.trace_exporter": mock_module, "opentelemetry.exporter.otlp.proto.http": mock_http_module, }, ): config = self._make_batch_config( SpanExporterConfig(otlp_http=OtlpHttpExporterConfig(headers_list="x-api-key=secret,env=prod")) ) create_tracer_provider(config) _, kwargs = mock_exporter_cls.call_args self.assertEqual(kwargs["headers"], {"x-api-key": "secret", "env": "prod"}) def test_otlp_file_development_missing_package_raises(self): config = self._make_batch_config(SpanExporterConfig(otlp_file_development=ExperimentalOtlpFileExporterConfig())) with ( patch.dict( sys.modules, { "opentelemetry.exporter.otlp.json.file.trace_exporter": None, }, ), self.assertRaises(ConfigurationError) as ctx, ): create_tracer_provider(config) self.assertIn("opentelemetry-exporter-otlp-json-file", str(ctx.exception)) def test_otlp_file_development_default_stdout(self): mock_exporter_cls = MagicMock() mock_module = MagicMock() mock_module.FileSpanExporter = mock_exporter_cls with patch.dict( sys.modules, { "opentelemetry.exporter.otlp.json.file.trace_exporter": mock_module, }, ): config = self._make_batch_config( SpanExporterConfig(otlp_file_development=ExperimentalOtlpFileExporterConfig()) ) create_tracer_provider(config) mock_exporter_cls.assert_called_once_with() def test_otlp_file_development_file_uri(self): mock_exporter_cls = MagicMock() mock_module = MagicMock() mock_module.FileSpanExporter = mock_exporter_cls with patch.dict( sys.modules, { "opentelemetry.exporter.otlp.json.file.trace_exporter": mock_module, }, ): config = self._make_batch_config( SpanExporterConfig( otlp_file_development=ExperimentalOtlpFileExporterConfig(output_stream="file:///tmp/traces.jsonl") ) ) create_tracer_provider(config) mock_exporter_cls.assert_called_once_with("/tmp/traces.jsonl") def test_otlp_file_development_unsupported_output_stream_raises(self): mock_exporter_cls = MagicMock() mock_module = MagicMock() mock_module.FileSpanExporter = mock_exporter_cls with patch.dict( sys.modules, { "opentelemetry.exporter.otlp.json.file.trace_exporter": mock_module, }, ): config = self._make_batch_config( SpanExporterConfig( otlp_file_development=ExperimentalOtlpFileExporterConfig(output_stream="http://example") ) ) with self.assertRaises(ConfigurationError) as ctx: create_tracer_provider(config) self.assertIn("output_stream", str(ctx.exception)) mock_exporter_cls.assert_not_called() def test_otlp_grpc_missing_package_raises(self): config = self._make_batch_config(SpanExporterConfig(otlp_grpc=OtlpGrpcExporterConfig())) with ( patch.dict( sys.modules, { "opentelemetry.exporter.otlp.proto.grpc.trace_exporter": None, "grpc": None, }, ), self.assertRaises(ConfigurationError) as ctx, ): create_tracer_provider(config) self.assertIn("otlp-proto-grpc", str(ctx.exception)) def test_no_processor_type_raises(self): config = TracerProviderConfig(processors=[SpanProcessorConfig()]) with self.assertRaises(ConfigurationError): create_tracer_provider(config) def test_no_exporter_type_raises(self): config = self._make_batch_config(SpanExporterConfig()) with self.assertRaises(ConfigurationError): create_tracer_provider(config) def test_plugin_span_exporter_loaded_via_entry_point(self): mock_exporter = MagicMock() mock_class = MagicMock(return_value=mock_exporter) with patch( "opentelemetry.configuration._common.entry_points", return_value=[MagicMock(**{"load.return_value": mock_class})], ): config = self._make_batch_config( # pylint: disable=unexpected-keyword-arg SpanExporterConfig(my_custom_exporter={}) ) provider = create_tracer_provider(config) self.assertEqual(len(provider._active_span_processor._span_processors), 1) def test_unknown_span_exporter_raises_configuration_error(self): with patch( "opentelemetry.configuration._common.entry_points", return_value=[], ): config = self._make_batch_config( # pylint: disable=unexpected-keyword-arg SpanExporterConfig(no_such_exporter={}) ) with self.assertRaises(ConfigurationError): create_tracer_provider(config) class TestCreateSpanLimits(unittest.TestCase): # pylint: disable=no-self-use @staticmethod def _create_with_limits(limits_config): return create_tracer_provider(TracerProviderConfig(processors=[], limits=limits_config)) def test_explicit_attribute_count_limit(self): provider = self._create_with_limits(SpanLimitsConfig(attribute_count_limit=10)) self.assertEqual(provider._span_limits.max_span_attributes, 10) def test_explicit_event_count_limit(self): provider = self._create_with_limits(SpanLimitsConfig(event_count_limit=5)) self.assertEqual(provider._span_limits.max_events, 5) def test_explicit_link_count_limit(self): provider = self._create_with_limits(SpanLimitsConfig(link_count_limit=2)) self.assertEqual(provider._span_limits.max_links, 2) def test_explicit_attribute_value_length_limit(self): provider = self._create_with_limits(SpanLimitsConfig(attribute_value_length_limit=64)) self.assertEqual(provider._span_limits.max_attribute_length, 64) def test_absent_limits_use_spec_defaults(self): provider = self._create_with_limits(SpanLimitsConfig()) self.assertEqual(provider._span_limits.max_span_attributes, 128) self.assertEqual(provider._span_limits.max_events, 128) self.assertEqual(provider._span_limits.max_links, 128) def test_absent_limits_do_not_read_env_vars(self): with patch.dict( os.environ, { "OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT": "1", "OTEL_SPAN_EVENT_COUNT_LIMIT": "2", "OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT": "3", "OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT": "4", }, ): provider = self._create_with_limits(SpanLimitsConfig()) self.assertEqual(provider._span_limits.max_span_attributes, 128) self.assertEqual(provider._span_limits.max_events, 128) self.assertIsNone(provider._span_limits.max_attribute_length) self.assertIsNone(provider._span_limits.max_span_attribute_length) class TestCreateIdGenerator(unittest.TestCase): """Tests for _create_id_generator and id_generator wiring in create_tracer_provider.""" @staticmethod def _make_provider(id_generator_config): return create_tracer_provider(TracerProviderConfig(processors=[], id_generator=id_generator_config)) def test_absent_id_generator_uses_sdk_default(self): """When id_generator is omitted, the SDK's default RandomIdGenerator is used.""" provider = create_tracer_provider(TracerProviderConfig(processors=[])) self.assertIsInstance(provider.id_generator, RandomIdGenerator) def test_builtin_random_id_generator(self): """Built-in 'random' id_generator resolves to RandomIdGenerator.""" provider = self._make_provider(IdGeneratorConfig(random={})) self.assertIsInstance(provider.id_generator, RandomIdGenerator) def test_plugin_id_generator_loaded_via_entry_point(self): """Unknown id_generator name is loaded from opentelemetry_id_generator entry point group.""" mock_generator = MagicMock() mock_class = MagicMock(return_value=mock_generator) with patch( "opentelemetry.configuration._common.entry_points", return_value=[MagicMock(**{"load.return_value": mock_class})], ): # pylint: disable=unexpected-keyword-arg provider = self._make_provider(IdGeneratorConfig(my_custom_generator={})) self.assertIs(provider.id_generator, mock_generator) def test_unknown_id_generator_raises_configuration_error(self): """Unknown id_generator name with no matching entry point raises ConfigurationError.""" with ( patch( "opentelemetry.configuration._common.entry_points", return_value=[], ), self.assertRaises(ConfigurationError), ): # pylint: disable=unexpected-keyword-arg self._make_provider(IdGeneratorConfig(no_such_generator={})) def test_empty_id_generator_raises_configuration_error(self): """Empty IdGenerator config (no type specified) raises ConfigurationError.""" with self.assertRaises(ConfigurationError): self._make_provider(IdGeneratorConfig()) # Configurator tests access the SDK TracerProvider private # _apply_tracer_configurator to assert the wired config takes effect. class TestTracerConfigurator(unittest.TestCase): @staticmethod def _enabled(provider, name): return provider._apply_tracer_configurator(InstrumentationScope(name)).is_enabled def test_no_configurator_leaves_tracers_enabled(self): provider = create_tracer_provider(TracerProviderConfig(processors=[])) self.assertTrue(self._enabled(provider, "any.scope")) def test_matching_glob_disables_tracer(self): config = TracerProviderConfig( processors=[], tracer_configurator_development=TracerConfiguratorConfig( default_config=TracerConfigConfig(enabled=True), tracers=[ TracerMatcherAndConfig( name="noisy.*", config=TracerConfigConfig(enabled=False), ) ], ), ) provider = create_tracer_provider(config) self.assertFalse(self._enabled(provider, "noisy.http")) self.assertTrue(self._enabled(provider, "app.service")) def test_default_config_applies_to_unmatched_scopes(self): config = TracerProviderConfig( processors=[], tracer_configurator_development=TracerConfiguratorConfig( default_config=TracerConfigConfig(enabled=False), tracers=[ TracerMatcherAndConfig( name="keep.*", config=TracerConfigConfig(enabled=True), ) ], ), ) provider = create_tracer_provider(config) self.assertTrue(self._enabled(provider, "keep.me")) self.assertFalse(self._enabled(provider, "other")) def test_first_matching_rule_wins(self): config = TracerProviderConfig( processors=[], tracer_configurator_development=TracerConfiguratorConfig( tracers=[ TracerMatcherAndConfig( name="a.*", config=TracerConfigConfig(enabled=False), ), TracerMatcherAndConfig( name="a.b", config=TracerConfigConfig(enabled=True), ), ], ), ) provider = create_tracer_provider(config) self.assertFalse(self._enabled(provider, "a.b")) def test_absent_enabled_defaults_to_enabled(self): config = TracerProviderConfig( processors=[], tracer_configurator_development=TracerConfiguratorConfig( default_config=TracerConfigConfig(), ), ) provider = create_tracer_provider(config) self.assertTrue(self._enabled(provider, "any.scope")) class TestGlobalAttributeLimitsFallback(unittest.TestCase): # pylint: disable=no-self-use def test_global_attribute_count_limit_used_when_no_per_signal_limits(self): global_limits = AttributeLimits(attribute_count_limit=42) provider = create_tracer_provider( TracerProviderConfig(processors=[]), global_attribute_limits=global_limits, ) self.assertEqual(provider._span_limits.max_span_attributes, 42) def test_global_attribute_value_length_limit_used_when_no_per_signal_limits( self, ): global_limits = AttributeLimits(attribute_value_length_limit=64) provider = create_tracer_provider( TracerProviderConfig(processors=[]), global_attribute_limits=global_limits, ) self.assertEqual(provider._span_limits.max_attribute_length, 64) def test_per_signal_limits_take_precedence_over_global(self): global_limits = AttributeLimits( attribute_count_limit=99, attribute_value_length_limit=99, ) provider = create_tracer_provider( TracerProviderConfig( processors=[], limits=SpanLimitsConfig( attribute_count_limit=7, attribute_value_length_limit=16, ), ), global_attribute_limits=global_limits, ) self.assertEqual(provider._span_limits.max_span_attributes, 7) self.assertEqual(provider._span_limits.max_attribute_length, 16) def test_global_limits_absent_uses_sdk_defaults(self): provider = create_tracer_provider( TracerProviderConfig(processors=[]), ) self.assertEqual(provider._span_limits.max_span_attributes, 128) self.assertIsNone(provider._span_limits.max_attribute_length) def test_global_limits_absent_does_not_read_env_vars(self): with patch.dict( os.environ, { "OTEL_ATTRIBUTE_COUNT_LIMIT": "1", "OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT": "2", "OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT": "3", }, ): provider = create_tracer_provider( TracerProviderConfig(processors=[]), ) self.assertEqual(provider._span_limits.max_span_attributes, 128) self.assertIsNone(provider._span_limits.max_attribute_length) self.assertIsNone(provider._span_limits.max_span_attribute_length) python-opentelemetry-1.45.2/opentelemetry-proto-json/000077500000000000000000000000001527070070700230275ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-proto-json/LICENSE000066400000000000000000000261351527070070700240430ustar00rootroot00000000000000 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. python-opentelemetry-1.45.2/opentelemetry-proto-json/README.rst000066400000000000000000000031421527070070700245160ustar00rootroot00000000000000OpenTelemetry Python Proto JSON ================================ |pypi| .. |pypi| image:: https://badge.fury.io/py/opentelemetry-proto-json.svg :target: https://pypi.org/project/opentelemetry-proto-json/ This library contains the generated code for OpenTelemetry protobuf data model with JSON encoding support. The code in the current package was generated using the v1.10.0 release_ of opentelemetry-proto and includes definitions for the OpenTelemetry JSON Protobuf encoding specification. .. _release: https://github.com/open-telemetry/opentelemetry-proto/releases/tag/v1.10.0 Installation ------------ :: pip install opentelemetry-proto-json Code Generation --------------- These files were generated automatically using the custom protoc plugin opentelemetry-codegen-json_ from code in opentelemetry-proto_. To regenerate the code, run ``../scripts/proto_codegen_json.sh``. To build against a new release or specific commit of opentelemetry-proto_, update the ``PROTO_REPO_BRANCH_OR_COMMIT`` variable in ``../scripts/proto_codegen_json.sh``. Then run the script and commit the changes as well as any fixes needed in the OTLP exporter. .. _opentelemetry-codegen-json: https://github.com/open-telemetry/opentelemetry-python/tree/main/codegen/opentelemetry-codegen-json .. _opentelemetry-proto: https://github.com/open-telemetry/opentelemetry-proto References ---------- * `OpenTelemetry Project `_ * `OpenTelemetry Proto `_ * `OTLP JSON Encoding Specification `_ python-opentelemetry-1.45.2/opentelemetry-proto-json/pyproject.toml000066400000000000000000000022761527070070700257520ustar00rootroot00000000000000[build-system] requires = ["hatchling"] build-backend = "hatchling.build" [project] name = "opentelemetry-proto-json" dynamic = ["version"] description = "OpenTelemetry Python Json Proto" 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", "Framework :: OpenTelemetry", "Intended Audience :: Developers", "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 = [] [project.urls] Homepage = "https://github.com/open-telemetry/opentelemetry-python/tree/main/opentelemetry-proto-json" Repository = "https://github.com/open-telemetry/opentelemetry-python" [tool.hatch.version] path = "src/opentelemetry/proto_json/version/__init__.py" [tool.hatch.build.targets.sdist] include = [ "/src", "/tests", ] [tool.hatch.build.targets.wheel] packages = ["src/opentelemetry"] python-opentelemetry-1.45.2/opentelemetry-proto-json/src/000077500000000000000000000000001527070070700236165ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-proto-json/src/opentelemetry/000077500000000000000000000000001527070070700265125ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-proto-json/src/opentelemetry/proto_json/000077500000000000000000000000001527070070700307065ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-proto-json/src/opentelemetry/proto_json/__init__.py000066400000000000000000000000001527070070700330050ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-proto-json/src/opentelemetry/proto_json/_json_codec.py000066400000000000000000000152431527070070700335320ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations import abc import base64 import collections.abc import json import math import typing from typing_extensions import Self T = typing.TypeVar("T") M = typing.TypeVar("M", bound="JsonMessage") class JsonMessage(abc.ABC): """ Abstract base class for protobuf messages with JSON serialization. """ @abc.abstractmethod def to_dict(self) -> dict[str, typing.Any]: """ Convert this message to a dictionary. """ @classmethod @abc.abstractmethod def from_dict(cls: type[M], data: dict[str, typing.Any]) -> M: """ Create an instance from a dictionary. """ def to_json(self) -> str: """ Serialize this message to a JSON string. """ return json.dumps(self.to_dict()) @classmethod def from_json(cls, data: str | bytes) -> Self: """ Deserialize from a JSON string or bytes. """ return cls.from_dict(json.loads(data)) def encode_hex(value: bytes | None) -> str: """ Encode bytes as hex string. Args: value: The bytes to encode. Returns: Hex string representation of the input bytes. """ return value.hex() if value else "" def encode_base64(value: bytes | None) -> str: """ Encode bytes as base64 string. Standard Proto3 JSON mapping for bytes. Args: value: The bytes to encode. Returns: Base64 string representation of the input bytes. """ return base64.b64encode(value).decode("utf-8") if value else "" def encode_int64(value: int) -> str: """ Encode 64 bit integers as strings. Required for int64, uint64, fixed64, sfixed64 and sint64 per Proto3 JSON spec. Args: value: The integer to encode. Returns: String representation of the input integer. """ return str(value) def encode_float(value: float) -> float | str: """ Encode float/double values. Args: value: The float to encode. Returns: The input value, or a string for special float values (NaN, Infinity). """ if math.isnan(value): return "NaN" if math.isinf(value): return "Infinity" if value > 0 else "-Infinity" return value def encode_repeated( values: list[T] | None, map_fn: collections.abc.Callable[[T], typing.Any], ) -> list[typing.Any]: """ Helper to serialize repeated fields with a mapping function. Args: values: The list of values to encode. map_fn: A function that takes a single value and returns its encoded form. Returns: A list of encoded values, or an empty list if input is None or empty. """ return [map_fn(v) for v in values] if values else [] def decode_hex(value: str | None, field_name: str) -> bytes: """ Decode hex string to bytes. Args: value: The hex string to decode. field_name: The name of the field being decoded (for error messages). Returns: The decoded bytes, or empty bytes if input is None or empty. """ if not value: return b"" validate_type(value, str, field_name) try: return bytes.fromhex(value) except ValueError as error: raise ValueError( f"Invalid hex string for field '{field_name}': {error}" ) from None def decode_base64(value: str | None, field_name: str) -> bytes: """ Decode base64 string to bytes. Args: value: The base64 string to decode. field_name: The name of the field being decoded (for error messages). Returns: The decoded bytes, or empty bytes if input is None or empty. """ if not value: return b"" validate_type(value, str, field_name) try: return base64.b64decode(value) except Exception as error: raise ValueError( f"Invalid base64 string for field '{field_name}': {error}" ) from None def decode_int64(value: int | str | None, field_name: str) -> int: """ Parse int64 from number or string. Args: value: The value to decode, which can be an int, a string, or None field_name: The name of the field being decoded (for error messages). Returns: The decoded integer, or 0 if input is None. """ if value is None: return 0 validate_type(value, (int, str), field_name) try: return int(value) except (ValueError, TypeError): raise ValueError( f"Invalid int64 value for field '{field_name}': {value}" ) from None def decode_float(value: float | str | None, field_name: str) -> float: """ Parse float/double from number or string, handling special values. Args: value: The value to decode, which can be a float, int, string, or None field_name: The name of the field being decoded (for error messages). Returns: The decoded float, or 0.0 if input is None. """ if value is None: return 0.0 validate_type(value, (float, int, str), field_name) if value == "NaN": return math.nan if value == "Infinity": return math.inf if value == "-Infinity": return -math.inf try: return float(value) except (ValueError, TypeError): raise ValueError( f"Invalid float value for field '{field_name}': {value}" ) from None def decode_repeated( values: list[typing.Any] | None, item_parser: collections.abc.Callable[[typing.Any], T], field_name: str, ) -> list[T]: """ Parse a list of values using the provided item parser function. Args: values: The list of values to decode, or None. item_parser: A function that takes a single value and returns the parsed form. field_name: The name of the field being decoded (for error messages). Returns: A list of parsed values, or an empty list if input is None. """ if values is None: return [] validate_type(values, list, field_name) return [item_parser(v) for v in values] def validate_type( value: typing.Any, expected_types: type | tuple[type, ...], field_name: str, ) -> None: """ Validate that a value is of the expected type(s). Raises TypeError if validation fails. Args: value: The value to validate. expected_types: A type or tuple of types that the value is expected to be. field_name: The name of the field being validated (for error messages). """ if not isinstance(value, expected_types): raise TypeError( f"Field '{field_name}' expected {expected_types}, " f"got {type(value).__name__}" ) python-opentelemetry-1.45.2/opentelemetry-proto-json/src/opentelemetry/proto_json/collector/000077500000000000000000000000001527070070700326745ustar00rootroot00000000000000__init__.py000066400000000000000000000000001527070070700347140ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-proto-json/src/opentelemetry/proto_json/collectorpython-opentelemetry-1.45.2/opentelemetry-proto-json/src/opentelemetry/proto_json/collector/logs/000077500000000000000000000000001527070070700336405ustar00rootroot00000000000000__init__.py000066400000000000000000000000001527070070700356600ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-proto-json/src/opentelemetry/proto_json/collector/logspython-opentelemetry-1.45.2/opentelemetry-proto-json/src/opentelemetry/proto_json/collector/logs/v1/000077500000000000000000000000001527070070700341665ustar00rootroot00000000000000__init__.py000066400000000000000000000000001527070070700362060ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-proto-json/src/opentelemetry/proto_json/collector/logs/v1logs_service.py000066400000000000000000000115661527070070700371560ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-proto-json/src/opentelemetry/proto_json/collector/logs/v1# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # AUTO-GENERATED from "opentelemetry/proto/collector/logs/v1/logs_service.proto" # DO NOT EDIT MANUALLY from __future__ import annotations import builtins import dataclasses import functools import typing _dataclass = functools.partial(dataclasses.dataclass, slots=True) import opentelemetry.proto_json._json_codec import opentelemetry.proto_json.logs.v1.logs @typing.final @_dataclass class ExportLogsServiceRequest(opentelemetry.proto_json._json_codec.JsonMessage): """ Generated from protobuf message ExportLogsServiceRequest """ resource_logs: builtins.list[opentelemetry.proto_json.logs.v1.logs.ResourceLogs] = dataclasses.field(default_factory=builtins.list) def to_dict(self) -> builtins.dict[builtins.str, typing.Any]: """ Convert this message to a dictionary with lowerCamelCase keys. Returns: Dictionary representation following OTLP JSON encoding """ _result = {} if self.resource_logs: _result["resourceLogs"] = opentelemetry.proto_json._json_codec.encode_repeated(self.resource_logs, lambda _v: _v.to_dict()) return _result @builtins.classmethod def from_dict(cls, data: builtins.dict[builtins.str, typing.Any]) -> ExportLogsServiceRequest: """ Create from a dictionary with lowerCamelCase keys. Args: data: Dictionary representation following OTLP JSON encoding Returns: ExportLogsServiceRequest instance """ opentelemetry.proto_json._json_codec.validate_type(data, builtins.dict, "data") _args = {} if (_value := data.get("resourceLogs")) is not None: _args["resource_logs"] = opentelemetry.proto_json._json_codec.decode_repeated(_value, lambda _v: opentelemetry.proto_json.logs.v1.logs.ResourceLogs.from_dict(_v), "resource_logs") return cls(**_args) @typing.final @_dataclass class ExportLogsServiceResponse(opentelemetry.proto_json._json_codec.JsonMessage): """ Generated from protobuf message ExportLogsServiceResponse """ partial_success: ExportLogsPartialSuccess | None = None def to_dict(self) -> builtins.dict[builtins.str, typing.Any]: """ Convert this message to a dictionary with lowerCamelCase keys. Returns: Dictionary representation following OTLP JSON encoding """ _result = {} if self.partial_success: _result["partialSuccess"] = self.partial_success.to_dict() return _result @builtins.classmethod def from_dict(cls, data: builtins.dict[builtins.str, typing.Any]) -> ExportLogsServiceResponse: """ Create from a dictionary with lowerCamelCase keys. Args: data: Dictionary representation following OTLP JSON encoding Returns: ExportLogsServiceResponse instance """ opentelemetry.proto_json._json_codec.validate_type(data, builtins.dict, "data") _args = {} if (_value := data.get("partialSuccess")) is not None: _args["partial_success"] = ExportLogsPartialSuccess.from_dict(_value) return cls(**_args) @typing.final @_dataclass class ExportLogsPartialSuccess(opentelemetry.proto_json._json_codec.JsonMessage): """ Generated from protobuf message ExportLogsPartialSuccess """ rejected_log_records: builtins.int | None = 0 error_message: builtins.str | None = "" def to_dict(self) -> builtins.dict[builtins.str, typing.Any]: """ Convert this message to a dictionary with lowerCamelCase keys. Returns: Dictionary representation following OTLP JSON encoding """ _result = {} if self.rejected_log_records: _result["rejectedLogRecords"] = opentelemetry.proto_json._json_codec.encode_int64(self.rejected_log_records) if self.error_message: _result["errorMessage"] = self.error_message return _result @builtins.classmethod def from_dict(cls, data: builtins.dict[builtins.str, typing.Any]) -> ExportLogsPartialSuccess: """ Create from a dictionary with lowerCamelCase keys. Args: data: Dictionary representation following OTLP JSON encoding Returns: ExportLogsPartialSuccess instance """ opentelemetry.proto_json._json_codec.validate_type(data, builtins.dict, "data") _args = {} if (_value := data.get("rejectedLogRecords")) is not None: _args["rejected_log_records"] = opentelemetry.proto_json._json_codec.decode_int64(_value, "rejected_log_records") if (_value := data.get("errorMessage")) is not None: opentelemetry.proto_json._json_codec.validate_type(_value, builtins.str, "error_message") _args["error_message"] = _value return cls(**_args) python-opentelemetry-1.45.2/opentelemetry-proto-json/src/opentelemetry/proto_json/collector/metrics/000077500000000000000000000000001527070070700343425ustar00rootroot00000000000000__init__.py000066400000000000000000000000001527070070700363620ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-proto-json/src/opentelemetry/proto_json/collector/metricsv1/000077500000000000000000000000001527070070700346115ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-proto-json/src/opentelemetry/proto_json/collector/metrics__init__.py000066400000000000000000000000001527070070700367100ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-proto-json/src/opentelemetry/proto_json/collector/metrics/v1metrics_service.py000066400000000000000000000117231527070070700403550ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-proto-json/src/opentelemetry/proto_json/collector/metrics/v1# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # AUTO-GENERATED from "opentelemetry/proto/collector/metrics/v1/metrics_service.proto" # DO NOT EDIT MANUALLY from __future__ import annotations import builtins import dataclasses import functools import typing _dataclass = functools.partial(dataclasses.dataclass, slots=True) import opentelemetry.proto_json._json_codec import opentelemetry.proto_json.metrics.v1.metrics @typing.final @_dataclass class ExportMetricsServiceRequest(opentelemetry.proto_json._json_codec.JsonMessage): """ Generated from protobuf message ExportMetricsServiceRequest """ resource_metrics: builtins.list[opentelemetry.proto_json.metrics.v1.metrics.ResourceMetrics] = dataclasses.field(default_factory=builtins.list) def to_dict(self) -> builtins.dict[builtins.str, typing.Any]: """ Convert this message to a dictionary with lowerCamelCase keys. Returns: Dictionary representation following OTLP JSON encoding """ _result = {} if self.resource_metrics: _result["resourceMetrics"] = opentelemetry.proto_json._json_codec.encode_repeated(self.resource_metrics, lambda _v: _v.to_dict()) return _result @builtins.classmethod def from_dict(cls, data: builtins.dict[builtins.str, typing.Any]) -> ExportMetricsServiceRequest: """ Create from a dictionary with lowerCamelCase keys. Args: data: Dictionary representation following OTLP JSON encoding Returns: ExportMetricsServiceRequest instance """ opentelemetry.proto_json._json_codec.validate_type(data, builtins.dict, "data") _args = {} if (_value := data.get("resourceMetrics")) is not None: _args["resource_metrics"] = opentelemetry.proto_json._json_codec.decode_repeated(_value, lambda _v: opentelemetry.proto_json.metrics.v1.metrics.ResourceMetrics.from_dict(_v), "resource_metrics") return cls(**_args) @typing.final @_dataclass class ExportMetricsServiceResponse(opentelemetry.proto_json._json_codec.JsonMessage): """ Generated from protobuf message ExportMetricsServiceResponse """ partial_success: ExportMetricsPartialSuccess | None = None def to_dict(self) -> builtins.dict[builtins.str, typing.Any]: """ Convert this message to a dictionary with lowerCamelCase keys. Returns: Dictionary representation following OTLP JSON encoding """ _result = {} if self.partial_success: _result["partialSuccess"] = self.partial_success.to_dict() return _result @builtins.classmethod def from_dict(cls, data: builtins.dict[builtins.str, typing.Any]) -> ExportMetricsServiceResponse: """ Create from a dictionary with lowerCamelCase keys. Args: data: Dictionary representation following OTLP JSON encoding Returns: ExportMetricsServiceResponse instance """ opentelemetry.proto_json._json_codec.validate_type(data, builtins.dict, "data") _args = {} if (_value := data.get("partialSuccess")) is not None: _args["partial_success"] = ExportMetricsPartialSuccess.from_dict(_value) return cls(**_args) @typing.final @_dataclass class ExportMetricsPartialSuccess(opentelemetry.proto_json._json_codec.JsonMessage): """ Generated from protobuf message ExportMetricsPartialSuccess """ rejected_data_points: builtins.int | None = 0 error_message: builtins.str | None = "" def to_dict(self) -> builtins.dict[builtins.str, typing.Any]: """ Convert this message to a dictionary with lowerCamelCase keys. Returns: Dictionary representation following OTLP JSON encoding """ _result = {} if self.rejected_data_points: _result["rejectedDataPoints"] = opentelemetry.proto_json._json_codec.encode_int64(self.rejected_data_points) if self.error_message: _result["errorMessage"] = self.error_message return _result @builtins.classmethod def from_dict(cls, data: builtins.dict[builtins.str, typing.Any]) -> ExportMetricsPartialSuccess: """ Create from a dictionary with lowerCamelCase keys. Args: data: Dictionary representation following OTLP JSON encoding Returns: ExportMetricsPartialSuccess instance """ opentelemetry.proto_json._json_codec.validate_type(data, builtins.dict, "data") _args = {} if (_value := data.get("rejectedDataPoints")) is not None: _args["rejected_data_points"] = opentelemetry.proto_json._json_codec.decode_int64(_value, "rejected_data_points") if (_value := data.get("errorMessage")) is not None: opentelemetry.proto_json._json_codec.validate_type(_value, builtins.str, "error_message") _args["error_message"] = _value return cls(**_args) profiles/000077500000000000000000000000001527070070700344405ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-proto-json/src/opentelemetry/proto_json/collector__init__.py000066400000000000000000000000001527070070700365370ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-proto-json/src/opentelemetry/proto_json/collector/profilesv1development/000077500000000000000000000000001527070070700372315ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-proto-json/src/opentelemetry/proto_json/collector/profiles__init__.py000066400000000000000000000000001527070070700413300ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-proto-json/src/opentelemetry/proto_json/collector/profiles/v1developmentprofiles_service.py000066400000000000000000000126121527070070700431500ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-proto-json/src/opentelemetry/proto_json/collector/profiles/v1development# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # AUTO-GENERATED from "opentelemetry/proto/collector/profiles/v1development/profiles_service.proto" # DO NOT EDIT MANUALLY from __future__ import annotations import builtins import dataclasses import functools import typing _dataclass = functools.partial(dataclasses.dataclass, slots=True) import opentelemetry.proto_json._json_codec import opentelemetry.proto_json.profiles.v1development.profiles @typing.final @_dataclass class ExportProfilesServiceRequest(opentelemetry.proto_json._json_codec.JsonMessage): """ Generated from protobuf message ExportProfilesServiceRequest """ resource_profiles: builtins.list[opentelemetry.proto_json.profiles.v1development.profiles.ResourceProfiles] = dataclasses.field(default_factory=builtins.list) dictionary: opentelemetry.proto_json.profiles.v1development.profiles.ProfilesDictionary | None = None def to_dict(self) -> builtins.dict[builtins.str, typing.Any]: """ Convert this message to a dictionary with lowerCamelCase keys. Returns: Dictionary representation following OTLP JSON encoding """ _result = {} if self.resource_profiles: _result["resourceProfiles"] = opentelemetry.proto_json._json_codec.encode_repeated(self.resource_profiles, lambda _v: _v.to_dict()) if self.dictionary: _result["dictionary"] = self.dictionary.to_dict() return _result @builtins.classmethod def from_dict(cls, data: builtins.dict[builtins.str, typing.Any]) -> ExportProfilesServiceRequest: """ Create from a dictionary with lowerCamelCase keys. Args: data: Dictionary representation following OTLP JSON encoding Returns: ExportProfilesServiceRequest instance """ opentelemetry.proto_json._json_codec.validate_type(data, builtins.dict, "data") _args = {} if (_value := data.get("resourceProfiles")) is not None: _args["resource_profiles"] = opentelemetry.proto_json._json_codec.decode_repeated(_value, lambda _v: opentelemetry.proto_json.profiles.v1development.profiles.ResourceProfiles.from_dict(_v), "resource_profiles") if (_value := data.get("dictionary")) is not None: _args["dictionary"] = opentelemetry.proto_json.profiles.v1development.profiles.ProfilesDictionary.from_dict(_value) return cls(**_args) @typing.final @_dataclass class ExportProfilesServiceResponse(opentelemetry.proto_json._json_codec.JsonMessage): """ Generated from protobuf message ExportProfilesServiceResponse """ partial_success: ExportProfilesPartialSuccess | None = None def to_dict(self) -> builtins.dict[builtins.str, typing.Any]: """ Convert this message to a dictionary with lowerCamelCase keys. Returns: Dictionary representation following OTLP JSON encoding """ _result = {} if self.partial_success: _result["partialSuccess"] = self.partial_success.to_dict() return _result @builtins.classmethod def from_dict(cls, data: builtins.dict[builtins.str, typing.Any]) -> ExportProfilesServiceResponse: """ Create from a dictionary with lowerCamelCase keys. Args: data: Dictionary representation following OTLP JSON encoding Returns: ExportProfilesServiceResponse instance """ opentelemetry.proto_json._json_codec.validate_type(data, builtins.dict, "data") _args = {} if (_value := data.get("partialSuccess")) is not None: _args["partial_success"] = ExportProfilesPartialSuccess.from_dict(_value) return cls(**_args) @typing.final @_dataclass class ExportProfilesPartialSuccess(opentelemetry.proto_json._json_codec.JsonMessage): """ Generated from protobuf message ExportProfilesPartialSuccess """ rejected_profiles: builtins.int | None = 0 error_message: builtins.str | None = "" def to_dict(self) -> builtins.dict[builtins.str, typing.Any]: """ Convert this message to a dictionary with lowerCamelCase keys. Returns: Dictionary representation following OTLP JSON encoding """ _result = {} if self.rejected_profiles: _result["rejectedProfiles"] = opentelemetry.proto_json._json_codec.encode_int64(self.rejected_profiles) if self.error_message: _result["errorMessage"] = self.error_message return _result @builtins.classmethod def from_dict(cls, data: builtins.dict[builtins.str, typing.Any]) -> ExportProfilesPartialSuccess: """ Create from a dictionary with lowerCamelCase keys. Args: data: Dictionary representation following OTLP JSON encoding Returns: ExportProfilesPartialSuccess instance """ opentelemetry.proto_json._json_codec.validate_type(data, builtins.dict, "data") _args = {} if (_value := data.get("rejectedProfiles")) is not None: _args["rejected_profiles"] = opentelemetry.proto_json._json_codec.decode_int64(_value, "rejected_profiles") if (_value := data.get("errorMessage")) is not None: opentelemetry.proto_json._json_codec.validate_type(_value, builtins.str, "error_message") _args["error_message"] = _value return cls(**_args) python-opentelemetry-1.45.2/opentelemetry-proto-json/src/opentelemetry/proto_json/collector/trace/000077500000000000000000000000001527070070700337725ustar00rootroot00000000000000__init__.py000066400000000000000000000000001527070070700360120ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-proto-json/src/opentelemetry/proto_json/collector/tracev1/000077500000000000000000000000001527070070700342415ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-proto-json/src/opentelemetry/proto_json/collector/trace__init__.py000066400000000000000000000000001527070070700363400ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-proto-json/src/opentelemetry/proto_json/collector/trace/v1trace_service.py000066400000000000000000000115551527070070700374400ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-proto-json/src/opentelemetry/proto_json/collector/trace/v1# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # AUTO-GENERATED from "opentelemetry/proto/collector/trace/v1/trace_service.proto" # DO NOT EDIT MANUALLY from __future__ import annotations import builtins import dataclasses import functools import typing _dataclass = functools.partial(dataclasses.dataclass, slots=True) import opentelemetry.proto_json._json_codec import opentelemetry.proto_json.trace.v1.trace @typing.final @_dataclass class ExportTraceServiceRequest(opentelemetry.proto_json._json_codec.JsonMessage): """ Generated from protobuf message ExportTraceServiceRequest """ resource_spans: builtins.list[opentelemetry.proto_json.trace.v1.trace.ResourceSpans] = dataclasses.field(default_factory=builtins.list) def to_dict(self) -> builtins.dict[builtins.str, typing.Any]: """ Convert this message to a dictionary with lowerCamelCase keys. Returns: Dictionary representation following OTLP JSON encoding """ _result = {} if self.resource_spans: _result["resourceSpans"] = opentelemetry.proto_json._json_codec.encode_repeated(self.resource_spans, lambda _v: _v.to_dict()) return _result @builtins.classmethod def from_dict(cls, data: builtins.dict[builtins.str, typing.Any]) -> ExportTraceServiceRequest: """ Create from a dictionary with lowerCamelCase keys. Args: data: Dictionary representation following OTLP JSON encoding Returns: ExportTraceServiceRequest instance """ opentelemetry.proto_json._json_codec.validate_type(data, builtins.dict, "data") _args = {} if (_value := data.get("resourceSpans")) is not None: _args["resource_spans"] = opentelemetry.proto_json._json_codec.decode_repeated(_value, lambda _v: opentelemetry.proto_json.trace.v1.trace.ResourceSpans.from_dict(_v), "resource_spans") return cls(**_args) @typing.final @_dataclass class ExportTraceServiceResponse(opentelemetry.proto_json._json_codec.JsonMessage): """ Generated from protobuf message ExportTraceServiceResponse """ partial_success: ExportTracePartialSuccess | None = None def to_dict(self) -> builtins.dict[builtins.str, typing.Any]: """ Convert this message to a dictionary with lowerCamelCase keys. Returns: Dictionary representation following OTLP JSON encoding """ _result = {} if self.partial_success: _result["partialSuccess"] = self.partial_success.to_dict() return _result @builtins.classmethod def from_dict(cls, data: builtins.dict[builtins.str, typing.Any]) -> ExportTraceServiceResponse: """ Create from a dictionary with lowerCamelCase keys. Args: data: Dictionary representation following OTLP JSON encoding Returns: ExportTraceServiceResponse instance """ opentelemetry.proto_json._json_codec.validate_type(data, builtins.dict, "data") _args = {} if (_value := data.get("partialSuccess")) is not None: _args["partial_success"] = ExportTracePartialSuccess.from_dict(_value) return cls(**_args) @typing.final @_dataclass class ExportTracePartialSuccess(opentelemetry.proto_json._json_codec.JsonMessage): """ Generated from protobuf message ExportTracePartialSuccess """ rejected_spans: builtins.int | None = 0 error_message: builtins.str | None = "" def to_dict(self) -> builtins.dict[builtins.str, typing.Any]: """ Convert this message to a dictionary with lowerCamelCase keys. Returns: Dictionary representation following OTLP JSON encoding """ _result = {} if self.rejected_spans: _result["rejectedSpans"] = opentelemetry.proto_json._json_codec.encode_int64(self.rejected_spans) if self.error_message: _result["errorMessage"] = self.error_message return _result @builtins.classmethod def from_dict(cls, data: builtins.dict[builtins.str, typing.Any]) -> ExportTracePartialSuccess: """ Create from a dictionary with lowerCamelCase keys. Args: data: Dictionary representation following OTLP JSON encoding Returns: ExportTracePartialSuccess instance """ opentelemetry.proto_json._json_codec.validate_type(data, builtins.dict, "data") _args = {} if (_value := data.get("rejectedSpans")) is not None: _args["rejected_spans"] = opentelemetry.proto_json._json_codec.decode_int64(_value, "rejected_spans") if (_value := data.get("errorMessage")) is not None: opentelemetry.proto_json._json_codec.validate_type(_value, builtins.str, "error_message") _args["error_message"] = _value return cls(**_args) python-opentelemetry-1.45.2/opentelemetry-proto-json/src/opentelemetry/proto_json/common/000077500000000000000000000000001527070070700321765ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-proto-json/src/opentelemetry/proto_json/common/__init__.py000066400000000000000000000000001527070070700342750ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-proto-json/src/opentelemetry/proto_json/common/v1/000077500000000000000000000000001527070070700325245ustar00rootroot00000000000000__init__.py000066400000000000000000000000001527070070700345440ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-proto-json/src/opentelemetry/proto_json/common/v1common.py000066400000000000000000000314651527070070700343200ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-proto-json/src/opentelemetry/proto_json/common/v1# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # AUTO-GENERATED from "opentelemetry/proto/common/v1/common.proto" # DO NOT EDIT MANUALLY from __future__ import annotations import builtins import dataclasses import functools import typing _dataclass = functools.partial(dataclasses.dataclass, slots=True) import opentelemetry.proto_json._json_codec @typing.final @_dataclass class AnyValue(opentelemetry.proto_json._json_codec.JsonMessage): """ Generated from protobuf message AnyValue """ string_value: builtins.str | None = None bool_value: builtins.bool | None = None int_value: builtins.int | None = None double_value: builtins.float | None = None array_value: ArrayValue | None = None kvlist_value: KeyValueList | None = None bytes_value: builtins.bytes | None = None string_value_strindex: builtins.int | None = None def to_dict(self) -> builtins.dict[builtins.str, typing.Any]: """ Convert this message to a dictionary with lowerCamelCase keys. Returns: Dictionary representation following OTLP JSON encoding """ _result = {} if self.string_value_strindex is not None: _result["stringValueStrindex"] = self.string_value_strindex elif self.bytes_value is not None: _result["bytesValue"] = opentelemetry.proto_json._json_codec.encode_base64(self.bytes_value) elif self.kvlist_value is not None: _result["kvlistValue"] = self.kvlist_value.to_dict() elif self.array_value is not None: _result["arrayValue"] = self.array_value.to_dict() elif self.double_value is not None: _result["doubleValue"] = opentelemetry.proto_json._json_codec.encode_float(self.double_value) elif self.int_value is not None: _result["intValue"] = opentelemetry.proto_json._json_codec.encode_int64(self.int_value) elif self.bool_value is not None: _result["boolValue"] = self.bool_value elif self.string_value is not None: _result["stringValue"] = self.string_value return _result @builtins.classmethod def from_dict(cls, data: builtins.dict[builtins.str, typing.Any]) -> AnyValue: """ Create from a dictionary with lowerCamelCase keys. Args: data: Dictionary representation following OTLP JSON encoding Returns: AnyValue instance """ opentelemetry.proto_json._json_codec.validate_type(data, builtins.dict, "data") _args = {} if (_value := data.get("stringValueStrindex")) is not None: opentelemetry.proto_json._json_codec.validate_type(_value, builtins.int, "string_value_strindex") _args["string_value_strindex"] = _value elif (_value := data.get("bytesValue")) is not None: _args["bytes_value"] = opentelemetry.proto_json._json_codec.decode_base64(_value, "bytes_value") elif (_value := data.get("kvlistValue")) is not None: _args["kvlist_value"] = KeyValueList.from_dict(_value) elif (_value := data.get("arrayValue")) is not None: _args["array_value"] = ArrayValue.from_dict(_value) elif (_value := data.get("doubleValue")) is not None: _args["double_value"] = opentelemetry.proto_json._json_codec.decode_float(_value, "double_value") elif (_value := data.get("intValue")) is not None: _args["int_value"] = opentelemetry.proto_json._json_codec.decode_int64(_value, "int_value") elif (_value := data.get("boolValue")) is not None: opentelemetry.proto_json._json_codec.validate_type(_value, builtins.bool, "bool_value") _args["bool_value"] = _value elif (_value := data.get("stringValue")) is not None: opentelemetry.proto_json._json_codec.validate_type(_value, builtins.str, "string_value") _args["string_value"] = _value return cls(**_args) @typing.final @_dataclass class ArrayValue(opentelemetry.proto_json._json_codec.JsonMessage): """ Generated from protobuf message ArrayValue """ values: builtins.list[AnyValue] = dataclasses.field(default_factory=builtins.list) def to_dict(self) -> builtins.dict[builtins.str, typing.Any]: """ Convert this message to a dictionary with lowerCamelCase keys. Returns: Dictionary representation following OTLP JSON encoding """ _result = {} if self.values: _result["values"] = opentelemetry.proto_json._json_codec.encode_repeated(self.values, lambda _v: _v.to_dict()) return _result @builtins.classmethod def from_dict(cls, data: builtins.dict[builtins.str, typing.Any]) -> ArrayValue: """ Create from a dictionary with lowerCamelCase keys. Args: data: Dictionary representation following OTLP JSON encoding Returns: ArrayValue instance """ opentelemetry.proto_json._json_codec.validate_type(data, builtins.dict, "data") _args = {} if (_value := data.get("values")) is not None: _args["values"] = opentelemetry.proto_json._json_codec.decode_repeated(_value, lambda _v: AnyValue.from_dict(_v), "values") return cls(**_args) @typing.final @_dataclass class KeyValueList(opentelemetry.proto_json._json_codec.JsonMessage): """ Generated from protobuf message KeyValueList """ values: builtins.list[KeyValue] = dataclasses.field(default_factory=builtins.list) def to_dict(self) -> builtins.dict[builtins.str, typing.Any]: """ Convert this message to a dictionary with lowerCamelCase keys. Returns: Dictionary representation following OTLP JSON encoding """ _result = {} if self.values: _result["values"] = opentelemetry.proto_json._json_codec.encode_repeated(self.values, lambda _v: _v.to_dict()) return _result @builtins.classmethod def from_dict(cls, data: builtins.dict[builtins.str, typing.Any]) -> KeyValueList: """ Create from a dictionary with lowerCamelCase keys. Args: data: Dictionary representation following OTLP JSON encoding Returns: KeyValueList instance """ opentelemetry.proto_json._json_codec.validate_type(data, builtins.dict, "data") _args = {} if (_value := data.get("values")) is not None: _args["values"] = opentelemetry.proto_json._json_codec.decode_repeated(_value, lambda _v: KeyValue.from_dict(_v), "values") return cls(**_args) @typing.final @_dataclass class KeyValue(opentelemetry.proto_json._json_codec.JsonMessage): """ Generated from protobuf message KeyValue """ key: builtins.str | None = "" value: AnyValue | None = None key_strindex: builtins.int | None = 0 def to_dict(self) -> builtins.dict[builtins.str, typing.Any]: """ Convert this message to a dictionary with lowerCamelCase keys. Returns: Dictionary representation following OTLP JSON encoding """ _result = {} if self.key: _result["key"] = self.key if self.value: _result["value"] = self.value.to_dict() if self.key_strindex: _result["keyStrindex"] = self.key_strindex return _result @builtins.classmethod def from_dict(cls, data: builtins.dict[builtins.str, typing.Any]) -> KeyValue: """ Create from a dictionary with lowerCamelCase keys. Args: data: Dictionary representation following OTLP JSON encoding Returns: KeyValue instance """ opentelemetry.proto_json._json_codec.validate_type(data, builtins.dict, "data") _args = {} if (_value := data.get("key")) is not None: opentelemetry.proto_json._json_codec.validate_type(_value, builtins.str, "key") _args["key"] = _value if (_value := data.get("value")) is not None: _args["value"] = AnyValue.from_dict(_value) if (_value := data.get("keyStrindex")) is not None: opentelemetry.proto_json._json_codec.validate_type(_value, builtins.int, "key_strindex") _args["key_strindex"] = _value return cls(**_args) @typing.final @_dataclass class InstrumentationScope(opentelemetry.proto_json._json_codec.JsonMessage): """ Generated from protobuf message InstrumentationScope """ name: builtins.str | None = "" version: builtins.str | None = "" attributes: builtins.list[KeyValue] = dataclasses.field(default_factory=builtins.list) dropped_attributes_count: builtins.int | None = 0 def to_dict(self) -> builtins.dict[builtins.str, typing.Any]: """ Convert this message to a dictionary with lowerCamelCase keys. Returns: Dictionary representation following OTLP JSON encoding """ _result = {} if self.name: _result["name"] = self.name if self.version: _result["version"] = self.version if self.attributes: _result["attributes"] = opentelemetry.proto_json._json_codec.encode_repeated(self.attributes, lambda _v: _v.to_dict()) if self.dropped_attributes_count: _result["droppedAttributesCount"] = self.dropped_attributes_count return _result @builtins.classmethod def from_dict(cls, data: builtins.dict[builtins.str, typing.Any]) -> InstrumentationScope: """ Create from a dictionary with lowerCamelCase keys. Args: data: Dictionary representation following OTLP JSON encoding Returns: InstrumentationScope instance """ opentelemetry.proto_json._json_codec.validate_type(data, builtins.dict, "data") _args = {} if (_value := data.get("name")) is not None: opentelemetry.proto_json._json_codec.validate_type(_value, builtins.str, "name") _args["name"] = _value if (_value := data.get("version")) is not None: opentelemetry.proto_json._json_codec.validate_type(_value, builtins.str, "version") _args["version"] = _value if (_value := data.get("attributes")) is not None: _args["attributes"] = opentelemetry.proto_json._json_codec.decode_repeated(_value, lambda _v: KeyValue.from_dict(_v), "attributes") if (_value := data.get("droppedAttributesCount")) is not None: opentelemetry.proto_json._json_codec.validate_type(_value, builtins.int, "dropped_attributes_count") _args["dropped_attributes_count"] = _value return cls(**_args) @typing.final @_dataclass class EntityRef(opentelemetry.proto_json._json_codec.JsonMessage): """ Generated from protobuf message EntityRef """ schema_url: builtins.str | None = "" type: builtins.str | None = "" id_keys: builtins.list[builtins.str] = dataclasses.field(default_factory=builtins.list) description_keys: builtins.list[builtins.str] = dataclasses.field(default_factory=builtins.list) def to_dict(self) -> builtins.dict[builtins.str, typing.Any]: """ Convert this message to a dictionary with lowerCamelCase keys. Returns: Dictionary representation following OTLP JSON encoding """ _result = {} if self.schema_url: _result["schemaUrl"] = self.schema_url if self.type: _result["type"] = self.type if self.id_keys: _result["idKeys"] = self.id_keys if self.description_keys: _result["descriptionKeys"] = self.description_keys return _result @builtins.classmethod def from_dict(cls, data: builtins.dict[builtins.str, typing.Any]) -> EntityRef: """ Create from a dictionary with lowerCamelCase keys. Args: data: Dictionary representation following OTLP JSON encoding Returns: EntityRef instance """ opentelemetry.proto_json._json_codec.validate_type(data, builtins.dict, "data") _args = {} if (_value := data.get("schemaUrl")) is not None: opentelemetry.proto_json._json_codec.validate_type(_value, builtins.str, "schema_url") _args["schema_url"] = _value if (_value := data.get("type")) is not None: opentelemetry.proto_json._json_codec.validate_type(_value, builtins.str, "type") _args["type"] = _value if (_value := data.get("idKeys")) is not None: _args["id_keys"] = opentelemetry.proto_json._json_codec.decode_repeated(_value, lambda _v: _v, "id_keys") if (_value := data.get("descriptionKeys")) is not None: _args["description_keys"] = opentelemetry.proto_json._json_codec.decode_repeated(_value, lambda _v: _v, "description_keys") return cls(**_args) python-opentelemetry-1.45.2/opentelemetry-proto-json/src/opentelemetry/proto_json/logs/000077500000000000000000000000001527070070700316525ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-proto-json/src/opentelemetry/proto_json/logs/__init__.py000066400000000000000000000000001527070070700337510ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-proto-json/src/opentelemetry/proto_json/logs/v1/000077500000000000000000000000001527070070700322005ustar00rootroot00000000000000__init__.py000066400000000000000000000000001527070070700342200ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-proto-json/src/opentelemetry/proto_json/logs/v1python-opentelemetry-1.45.2/opentelemetry-proto-json/src/opentelemetry/proto_json/logs/v1/logs.py000066400000000000000000000272531527070070700335270ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # AUTO-GENERATED from "opentelemetry/proto/logs/v1/logs.proto" # DO NOT EDIT MANUALLY from __future__ import annotations import builtins import dataclasses import enum import functools import typing _dataclass = functools.partial(dataclasses.dataclass, slots=True) import opentelemetry.proto_json._json_codec import opentelemetry.proto_json.common.v1.common import opentelemetry.proto_json.resource.v1.resource @typing.final class SeverityNumber(enum.IntEnum): """ Generated from protobuf enum SeverityNumber """ SEVERITY_NUMBER_UNSPECIFIED = 0 SEVERITY_NUMBER_TRACE = 1 SEVERITY_NUMBER_TRACE2 = 2 SEVERITY_NUMBER_TRACE3 = 3 SEVERITY_NUMBER_TRACE4 = 4 SEVERITY_NUMBER_DEBUG = 5 SEVERITY_NUMBER_DEBUG2 = 6 SEVERITY_NUMBER_DEBUG3 = 7 SEVERITY_NUMBER_DEBUG4 = 8 SEVERITY_NUMBER_INFO = 9 SEVERITY_NUMBER_INFO2 = 10 SEVERITY_NUMBER_INFO3 = 11 SEVERITY_NUMBER_INFO4 = 12 SEVERITY_NUMBER_WARN = 13 SEVERITY_NUMBER_WARN2 = 14 SEVERITY_NUMBER_WARN3 = 15 SEVERITY_NUMBER_WARN4 = 16 SEVERITY_NUMBER_ERROR = 17 SEVERITY_NUMBER_ERROR2 = 18 SEVERITY_NUMBER_ERROR3 = 19 SEVERITY_NUMBER_ERROR4 = 20 SEVERITY_NUMBER_FATAL = 21 SEVERITY_NUMBER_FATAL2 = 22 SEVERITY_NUMBER_FATAL3 = 23 SEVERITY_NUMBER_FATAL4 = 24 @typing.final class LogRecordFlags(enum.IntEnum): """ Generated from protobuf enum LogRecordFlags """ LOG_RECORD_FLAGS_DO_NOT_USE = 0 LOG_RECORD_FLAGS_TRACE_FLAGS_MASK = 255 @typing.final @_dataclass class LogsData(opentelemetry.proto_json._json_codec.JsonMessage): """ Generated from protobuf message LogsData """ resource_logs: builtins.list[ResourceLogs] = dataclasses.field(default_factory=builtins.list) def to_dict(self) -> builtins.dict[builtins.str, typing.Any]: """ Convert this message to a dictionary with lowerCamelCase keys. Returns: Dictionary representation following OTLP JSON encoding """ _result = {} if self.resource_logs: _result["resourceLogs"] = opentelemetry.proto_json._json_codec.encode_repeated(self.resource_logs, lambda _v: _v.to_dict()) return _result @builtins.classmethod def from_dict(cls, data: builtins.dict[builtins.str, typing.Any]) -> LogsData: """ Create from a dictionary with lowerCamelCase keys. Args: data: Dictionary representation following OTLP JSON encoding Returns: LogsData instance """ opentelemetry.proto_json._json_codec.validate_type(data, builtins.dict, "data") _args = {} if (_value := data.get("resourceLogs")) is not None: _args["resource_logs"] = opentelemetry.proto_json._json_codec.decode_repeated(_value, lambda _v: ResourceLogs.from_dict(_v), "resource_logs") return cls(**_args) @typing.final @_dataclass class ResourceLogs(opentelemetry.proto_json._json_codec.JsonMessage): """ Generated from protobuf message ResourceLogs """ resource: opentelemetry.proto_json.resource.v1.resource.Resource | None = None scope_logs: builtins.list[ScopeLogs] = dataclasses.field(default_factory=builtins.list) schema_url: builtins.str | None = "" def to_dict(self) -> builtins.dict[builtins.str, typing.Any]: """ Convert this message to a dictionary with lowerCamelCase keys. Returns: Dictionary representation following OTLP JSON encoding """ _result = {} if self.resource: _result["resource"] = self.resource.to_dict() if self.scope_logs: _result["scopeLogs"] = opentelemetry.proto_json._json_codec.encode_repeated(self.scope_logs, lambda _v: _v.to_dict()) if self.schema_url: _result["schemaUrl"] = self.schema_url return _result @builtins.classmethod def from_dict(cls, data: builtins.dict[builtins.str, typing.Any]) -> ResourceLogs: """ Create from a dictionary with lowerCamelCase keys. Args: data: Dictionary representation following OTLP JSON encoding Returns: ResourceLogs instance """ opentelemetry.proto_json._json_codec.validate_type(data, builtins.dict, "data") _args = {} if (_value := data.get("resource")) is not None: _args["resource"] = opentelemetry.proto_json.resource.v1.resource.Resource.from_dict(_value) if (_value := data.get("scopeLogs")) is not None: _args["scope_logs"] = opentelemetry.proto_json._json_codec.decode_repeated(_value, lambda _v: ScopeLogs.from_dict(_v), "scope_logs") if (_value := data.get("schemaUrl")) is not None: opentelemetry.proto_json._json_codec.validate_type(_value, builtins.str, "schema_url") _args["schema_url"] = _value return cls(**_args) @typing.final @_dataclass class ScopeLogs(opentelemetry.proto_json._json_codec.JsonMessage): """ Generated from protobuf message ScopeLogs """ scope: opentelemetry.proto_json.common.v1.common.InstrumentationScope | None = None log_records: builtins.list[LogRecord] = dataclasses.field(default_factory=builtins.list) schema_url: builtins.str | None = "" def to_dict(self) -> builtins.dict[builtins.str, typing.Any]: """ Convert this message to a dictionary with lowerCamelCase keys. Returns: Dictionary representation following OTLP JSON encoding """ _result = {} if self.scope: _result["scope"] = self.scope.to_dict() if self.log_records: _result["logRecords"] = opentelemetry.proto_json._json_codec.encode_repeated(self.log_records, lambda _v: _v.to_dict()) if self.schema_url: _result["schemaUrl"] = self.schema_url return _result @builtins.classmethod def from_dict(cls, data: builtins.dict[builtins.str, typing.Any]) -> ScopeLogs: """ Create from a dictionary with lowerCamelCase keys. Args: data: Dictionary representation following OTLP JSON encoding Returns: ScopeLogs instance """ opentelemetry.proto_json._json_codec.validate_type(data, builtins.dict, "data") _args = {} if (_value := data.get("scope")) is not None: _args["scope"] = opentelemetry.proto_json.common.v1.common.InstrumentationScope.from_dict(_value) if (_value := data.get("logRecords")) is not None: _args["log_records"] = opentelemetry.proto_json._json_codec.decode_repeated(_value, lambda _v: LogRecord.from_dict(_v), "log_records") if (_value := data.get("schemaUrl")) is not None: opentelemetry.proto_json._json_codec.validate_type(_value, builtins.str, "schema_url") _args["schema_url"] = _value return cls(**_args) @typing.final @_dataclass class LogRecord(opentelemetry.proto_json._json_codec.JsonMessage): """ Generated from protobuf message LogRecord """ time_unix_nano: builtins.int | None = 0 observed_time_unix_nano: builtins.int | None = 0 severity_number: SeverityNumber | builtins.int | None = 0 severity_text: builtins.str | None = "" body: opentelemetry.proto_json.common.v1.common.AnyValue | None = None attributes: builtins.list[opentelemetry.proto_json.common.v1.common.KeyValue] = dataclasses.field(default_factory=builtins.list) dropped_attributes_count: builtins.int | None = 0 flags: builtins.int | None = 0 trace_id: builtins.bytes | None = b"" span_id: builtins.bytes | None = b"" event_name: builtins.str | None = "" def to_dict(self) -> builtins.dict[builtins.str, typing.Any]: """ Convert this message to a dictionary with lowerCamelCase keys. Returns: Dictionary representation following OTLP JSON encoding """ _result = {} if self.time_unix_nano: _result["timeUnixNano"] = opentelemetry.proto_json._json_codec.encode_int64(self.time_unix_nano) if self.observed_time_unix_nano: _result["observedTimeUnixNano"] = opentelemetry.proto_json._json_codec.encode_int64(self.observed_time_unix_nano) if self.severity_number: _result["severityNumber"] = builtins.int(self.severity_number) if self.severity_text: _result["severityText"] = self.severity_text if self.body: _result["body"] = self.body.to_dict() if self.attributes: _result["attributes"] = opentelemetry.proto_json._json_codec.encode_repeated(self.attributes, lambda _v: _v.to_dict()) if self.dropped_attributes_count: _result["droppedAttributesCount"] = self.dropped_attributes_count if self.flags: _result["flags"] = self.flags if self.trace_id: _result["traceId"] = opentelemetry.proto_json._json_codec.encode_hex(self.trace_id) if self.span_id: _result["spanId"] = opentelemetry.proto_json._json_codec.encode_hex(self.span_id) if self.event_name: _result["eventName"] = self.event_name return _result @builtins.classmethod def from_dict(cls, data: builtins.dict[builtins.str, typing.Any]) -> LogRecord: """ Create from a dictionary with lowerCamelCase keys. Args: data: Dictionary representation following OTLP JSON encoding Returns: LogRecord instance """ opentelemetry.proto_json._json_codec.validate_type(data, builtins.dict, "data") _args = {} if (_value := data.get("timeUnixNano")) is not None: _args["time_unix_nano"] = opentelemetry.proto_json._json_codec.decode_int64(_value, "time_unix_nano") if (_value := data.get("observedTimeUnixNano")) is not None: _args["observed_time_unix_nano"] = opentelemetry.proto_json._json_codec.decode_int64(_value, "observed_time_unix_nano") if (_value := data.get("severityNumber")) is not None: opentelemetry.proto_json._json_codec.validate_type(_value, builtins.int, "severity_number") _args["severity_number"] = SeverityNumber(_value) if (_value := data.get("severityText")) is not None: opentelemetry.proto_json._json_codec.validate_type(_value, builtins.str, "severity_text") _args["severity_text"] = _value if (_value := data.get("body")) is not None: _args["body"] = opentelemetry.proto_json.common.v1.common.AnyValue.from_dict(_value) if (_value := data.get("attributes")) is not None: _args["attributes"] = opentelemetry.proto_json._json_codec.decode_repeated(_value, lambda _v: opentelemetry.proto_json.common.v1.common.KeyValue.from_dict(_v), "attributes") if (_value := data.get("droppedAttributesCount")) is not None: opentelemetry.proto_json._json_codec.validate_type(_value, builtins.int, "dropped_attributes_count") _args["dropped_attributes_count"] = _value if (_value := data.get("flags")) is not None: opentelemetry.proto_json._json_codec.validate_type(_value, builtins.int, "flags") _args["flags"] = _value if (_value := data.get("traceId")) is not None: _args["trace_id"] = opentelemetry.proto_json._json_codec.decode_hex(_value, "trace_id") if (_value := data.get("spanId")) is not None: _args["span_id"] = opentelemetry.proto_json._json_codec.decode_hex(_value, "span_id") if (_value := data.get("eventName")) is not None: opentelemetry.proto_json._json_codec.validate_type(_value, builtins.str, "event_name") _args["event_name"] = _value return cls(**_args) python-opentelemetry-1.45.2/opentelemetry-proto-json/src/opentelemetry/proto_json/metrics/000077500000000000000000000000001527070070700323545ustar00rootroot00000000000000__init__.py000066400000000000000000000000001527070070700343740ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-proto-json/src/opentelemetry/proto_json/metricspython-opentelemetry-1.45.2/opentelemetry-proto-json/src/opentelemetry/proto_json/metrics/v1/000077500000000000000000000000001527070070700327025ustar00rootroot00000000000000__init__.py000066400000000000000000000000001527070070700347220ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-proto-json/src/opentelemetry/proto_json/metrics/v1metrics.py000066400000000000000000001260601527070070700346500ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-proto-json/src/opentelemetry/proto_json/metrics/v1# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # AUTO-GENERATED from "opentelemetry/proto/metrics/v1/metrics.proto" # DO NOT EDIT MANUALLY from __future__ import annotations import builtins import dataclasses import enum import functools import typing _dataclass = functools.partial(dataclasses.dataclass, slots=True) import opentelemetry.proto_json._json_codec import opentelemetry.proto_json.common.v1.common import opentelemetry.proto_json.resource.v1.resource @typing.final class AggregationTemporality(enum.IntEnum): """ Generated from protobuf enum AggregationTemporality """ AGGREGATION_TEMPORALITY_UNSPECIFIED = 0 AGGREGATION_TEMPORALITY_DELTA = 1 AGGREGATION_TEMPORALITY_CUMULATIVE = 2 @typing.final class DataPointFlags(enum.IntEnum): """ Generated from protobuf enum DataPointFlags """ DATA_POINT_FLAGS_DO_NOT_USE = 0 DATA_POINT_FLAGS_NO_RECORDED_VALUE_MASK = 1 @typing.final @_dataclass class MetricsData(opentelemetry.proto_json._json_codec.JsonMessage): """ Generated from protobuf message MetricsData """ resource_metrics: builtins.list[ResourceMetrics] = dataclasses.field(default_factory=builtins.list) def to_dict(self) -> builtins.dict[builtins.str, typing.Any]: """ Convert this message to a dictionary with lowerCamelCase keys. Returns: Dictionary representation following OTLP JSON encoding """ _result = {} if self.resource_metrics: _result["resourceMetrics"] = opentelemetry.proto_json._json_codec.encode_repeated(self.resource_metrics, lambda _v: _v.to_dict()) return _result @builtins.classmethod def from_dict(cls, data: builtins.dict[builtins.str, typing.Any]) -> MetricsData: """ Create from a dictionary with lowerCamelCase keys. Args: data: Dictionary representation following OTLP JSON encoding Returns: MetricsData instance """ opentelemetry.proto_json._json_codec.validate_type(data, builtins.dict, "data") _args = {} if (_value := data.get("resourceMetrics")) is not None: _args["resource_metrics"] = opentelemetry.proto_json._json_codec.decode_repeated(_value, lambda _v: ResourceMetrics.from_dict(_v), "resource_metrics") return cls(**_args) @typing.final @_dataclass class ResourceMetrics(opentelemetry.proto_json._json_codec.JsonMessage): """ Generated from protobuf message ResourceMetrics """ resource: opentelemetry.proto_json.resource.v1.resource.Resource | None = None scope_metrics: builtins.list[ScopeMetrics] = dataclasses.field(default_factory=builtins.list) schema_url: builtins.str | None = "" def to_dict(self) -> builtins.dict[builtins.str, typing.Any]: """ Convert this message to a dictionary with lowerCamelCase keys. Returns: Dictionary representation following OTLP JSON encoding """ _result = {} if self.resource: _result["resource"] = self.resource.to_dict() if self.scope_metrics: _result["scopeMetrics"] = opentelemetry.proto_json._json_codec.encode_repeated(self.scope_metrics, lambda _v: _v.to_dict()) if self.schema_url: _result["schemaUrl"] = self.schema_url return _result @builtins.classmethod def from_dict(cls, data: builtins.dict[builtins.str, typing.Any]) -> ResourceMetrics: """ Create from a dictionary with lowerCamelCase keys. Args: data: Dictionary representation following OTLP JSON encoding Returns: ResourceMetrics instance """ opentelemetry.proto_json._json_codec.validate_type(data, builtins.dict, "data") _args = {} if (_value := data.get("resource")) is not None: _args["resource"] = opentelemetry.proto_json.resource.v1.resource.Resource.from_dict(_value) if (_value := data.get("scopeMetrics")) is not None: _args["scope_metrics"] = opentelemetry.proto_json._json_codec.decode_repeated(_value, lambda _v: ScopeMetrics.from_dict(_v), "scope_metrics") if (_value := data.get("schemaUrl")) is not None: opentelemetry.proto_json._json_codec.validate_type(_value, builtins.str, "schema_url") _args["schema_url"] = _value return cls(**_args) @typing.final @_dataclass class ScopeMetrics(opentelemetry.proto_json._json_codec.JsonMessage): """ Generated from protobuf message ScopeMetrics """ scope: opentelemetry.proto_json.common.v1.common.InstrumentationScope | None = None metrics: builtins.list[Metric] = dataclasses.field(default_factory=builtins.list) schema_url: builtins.str | None = "" def to_dict(self) -> builtins.dict[builtins.str, typing.Any]: """ Convert this message to a dictionary with lowerCamelCase keys. Returns: Dictionary representation following OTLP JSON encoding """ _result = {} if self.scope: _result["scope"] = self.scope.to_dict() if self.metrics: _result["metrics"] = opentelemetry.proto_json._json_codec.encode_repeated(self.metrics, lambda _v: _v.to_dict()) if self.schema_url: _result["schemaUrl"] = self.schema_url return _result @builtins.classmethod def from_dict(cls, data: builtins.dict[builtins.str, typing.Any]) -> ScopeMetrics: """ Create from a dictionary with lowerCamelCase keys. Args: data: Dictionary representation following OTLP JSON encoding Returns: ScopeMetrics instance """ opentelemetry.proto_json._json_codec.validate_type(data, builtins.dict, "data") _args = {} if (_value := data.get("scope")) is not None: _args["scope"] = opentelemetry.proto_json.common.v1.common.InstrumentationScope.from_dict(_value) if (_value := data.get("metrics")) is not None: _args["metrics"] = opentelemetry.proto_json._json_codec.decode_repeated(_value, lambda _v: Metric.from_dict(_v), "metrics") if (_value := data.get("schemaUrl")) is not None: opentelemetry.proto_json._json_codec.validate_type(_value, builtins.str, "schema_url") _args["schema_url"] = _value return cls(**_args) @typing.final @_dataclass class Metric(opentelemetry.proto_json._json_codec.JsonMessage): """ Generated from protobuf message Metric """ name: builtins.str | None = "" description: builtins.str | None = "" unit: builtins.str | None = "" gauge: Gauge | None = None sum: Sum | None = None histogram: Histogram | None = None exponential_histogram: ExponentialHistogram | None = None summary: Summary | None = None metadata: builtins.list[opentelemetry.proto_json.common.v1.common.KeyValue] = dataclasses.field(default_factory=builtins.list) def to_dict(self) -> builtins.dict[builtins.str, typing.Any]: """ Convert this message to a dictionary with lowerCamelCase keys. Returns: Dictionary representation following OTLP JSON encoding """ _result = {} if self.name: _result["name"] = self.name if self.description: _result["description"] = self.description if self.unit: _result["unit"] = self.unit if self.metadata: _result["metadata"] = opentelemetry.proto_json._json_codec.encode_repeated(self.metadata, lambda _v: _v.to_dict()) if self.summary is not None: _result["summary"] = self.summary.to_dict() elif self.exponential_histogram is not None: _result["exponentialHistogram"] = self.exponential_histogram.to_dict() elif self.histogram is not None: _result["histogram"] = self.histogram.to_dict() elif self.sum is not None: _result["sum"] = self.sum.to_dict() elif self.gauge is not None: _result["gauge"] = self.gauge.to_dict() return _result @builtins.classmethod def from_dict(cls, data: builtins.dict[builtins.str, typing.Any]) -> Metric: """ Create from a dictionary with lowerCamelCase keys. Args: data: Dictionary representation following OTLP JSON encoding Returns: Metric instance """ opentelemetry.proto_json._json_codec.validate_type(data, builtins.dict, "data") _args = {} if (_value := data.get("name")) is not None: opentelemetry.proto_json._json_codec.validate_type(_value, builtins.str, "name") _args["name"] = _value if (_value := data.get("description")) is not None: opentelemetry.proto_json._json_codec.validate_type(_value, builtins.str, "description") _args["description"] = _value if (_value := data.get("unit")) is not None: opentelemetry.proto_json._json_codec.validate_type(_value, builtins.str, "unit") _args["unit"] = _value if (_value := data.get("metadata")) is not None: _args["metadata"] = opentelemetry.proto_json._json_codec.decode_repeated(_value, lambda _v: opentelemetry.proto_json.common.v1.common.KeyValue.from_dict(_v), "metadata") if (_value := data.get("summary")) is not None: _args["summary"] = Summary.from_dict(_value) elif (_value := data.get("exponentialHistogram")) is not None: _args["exponential_histogram"] = ExponentialHistogram.from_dict(_value) elif (_value := data.get("histogram")) is not None: _args["histogram"] = Histogram.from_dict(_value) elif (_value := data.get("sum")) is not None: _args["sum"] = Sum.from_dict(_value) elif (_value := data.get("gauge")) is not None: _args["gauge"] = Gauge.from_dict(_value) return cls(**_args) @typing.final @_dataclass class Gauge(opentelemetry.proto_json._json_codec.JsonMessage): """ Generated from protobuf message Gauge """ data_points: builtins.list[NumberDataPoint] = dataclasses.field(default_factory=builtins.list) def to_dict(self) -> builtins.dict[builtins.str, typing.Any]: """ Convert this message to a dictionary with lowerCamelCase keys. Returns: Dictionary representation following OTLP JSON encoding """ _result = {} if self.data_points: _result["dataPoints"] = opentelemetry.proto_json._json_codec.encode_repeated(self.data_points, lambda _v: _v.to_dict()) return _result @builtins.classmethod def from_dict(cls, data: builtins.dict[builtins.str, typing.Any]) -> Gauge: """ Create from a dictionary with lowerCamelCase keys. Args: data: Dictionary representation following OTLP JSON encoding Returns: Gauge instance """ opentelemetry.proto_json._json_codec.validate_type(data, builtins.dict, "data") _args = {} if (_value := data.get("dataPoints")) is not None: _args["data_points"] = opentelemetry.proto_json._json_codec.decode_repeated(_value, lambda _v: NumberDataPoint.from_dict(_v), "data_points") return cls(**_args) @typing.final @_dataclass class Sum(opentelemetry.proto_json._json_codec.JsonMessage): """ Generated from protobuf message Sum """ data_points: builtins.list[NumberDataPoint] = dataclasses.field(default_factory=builtins.list) aggregation_temporality: AggregationTemporality | builtins.int | None = 0 is_monotonic: builtins.bool | None = False def to_dict(self) -> builtins.dict[builtins.str, typing.Any]: """ Convert this message to a dictionary with lowerCamelCase keys. Returns: Dictionary representation following OTLP JSON encoding """ _result = {} if self.data_points: _result["dataPoints"] = opentelemetry.proto_json._json_codec.encode_repeated(self.data_points, lambda _v: _v.to_dict()) if self.aggregation_temporality: _result["aggregationTemporality"] = builtins.int(self.aggregation_temporality) if self.is_monotonic: _result["isMonotonic"] = self.is_monotonic return _result @builtins.classmethod def from_dict(cls, data: builtins.dict[builtins.str, typing.Any]) -> Sum: """ Create from a dictionary with lowerCamelCase keys. Args: data: Dictionary representation following OTLP JSON encoding Returns: Sum instance """ opentelemetry.proto_json._json_codec.validate_type(data, builtins.dict, "data") _args = {} if (_value := data.get("dataPoints")) is not None: _args["data_points"] = opentelemetry.proto_json._json_codec.decode_repeated(_value, lambda _v: NumberDataPoint.from_dict(_v), "data_points") if (_value := data.get("aggregationTemporality")) is not None: opentelemetry.proto_json._json_codec.validate_type(_value, builtins.int, "aggregation_temporality") _args["aggregation_temporality"] = AggregationTemporality(_value) if (_value := data.get("isMonotonic")) is not None: opentelemetry.proto_json._json_codec.validate_type(_value, builtins.bool, "is_monotonic") _args["is_monotonic"] = _value return cls(**_args) @typing.final @_dataclass class Histogram(opentelemetry.proto_json._json_codec.JsonMessage): """ Generated from protobuf message Histogram """ data_points: builtins.list[HistogramDataPoint] = dataclasses.field(default_factory=builtins.list) aggregation_temporality: AggregationTemporality | builtins.int | None = 0 def to_dict(self) -> builtins.dict[builtins.str, typing.Any]: """ Convert this message to a dictionary with lowerCamelCase keys. Returns: Dictionary representation following OTLP JSON encoding """ _result = {} if self.data_points: _result["dataPoints"] = opentelemetry.proto_json._json_codec.encode_repeated(self.data_points, lambda _v: _v.to_dict()) if self.aggregation_temporality: _result["aggregationTemporality"] = builtins.int(self.aggregation_temporality) return _result @builtins.classmethod def from_dict(cls, data: builtins.dict[builtins.str, typing.Any]) -> Histogram: """ Create from a dictionary with lowerCamelCase keys. Args: data: Dictionary representation following OTLP JSON encoding Returns: Histogram instance """ opentelemetry.proto_json._json_codec.validate_type(data, builtins.dict, "data") _args = {} if (_value := data.get("dataPoints")) is not None: _args["data_points"] = opentelemetry.proto_json._json_codec.decode_repeated(_value, lambda _v: HistogramDataPoint.from_dict(_v), "data_points") if (_value := data.get("aggregationTemporality")) is not None: opentelemetry.proto_json._json_codec.validate_type(_value, builtins.int, "aggregation_temporality") _args["aggregation_temporality"] = AggregationTemporality(_value) return cls(**_args) @typing.final @_dataclass class ExponentialHistogram(opentelemetry.proto_json._json_codec.JsonMessage): """ Generated from protobuf message ExponentialHistogram """ data_points: builtins.list[ExponentialHistogramDataPoint] = dataclasses.field(default_factory=builtins.list) aggregation_temporality: AggregationTemporality | builtins.int | None = 0 def to_dict(self) -> builtins.dict[builtins.str, typing.Any]: """ Convert this message to a dictionary with lowerCamelCase keys. Returns: Dictionary representation following OTLP JSON encoding """ _result = {} if self.data_points: _result["dataPoints"] = opentelemetry.proto_json._json_codec.encode_repeated(self.data_points, lambda _v: _v.to_dict()) if self.aggregation_temporality: _result["aggregationTemporality"] = builtins.int(self.aggregation_temporality) return _result @builtins.classmethod def from_dict(cls, data: builtins.dict[builtins.str, typing.Any]) -> ExponentialHistogram: """ Create from a dictionary with lowerCamelCase keys. Args: data: Dictionary representation following OTLP JSON encoding Returns: ExponentialHistogram instance """ opentelemetry.proto_json._json_codec.validate_type(data, builtins.dict, "data") _args = {} if (_value := data.get("dataPoints")) is not None: _args["data_points"] = opentelemetry.proto_json._json_codec.decode_repeated(_value, lambda _v: ExponentialHistogramDataPoint.from_dict(_v), "data_points") if (_value := data.get("aggregationTemporality")) is not None: opentelemetry.proto_json._json_codec.validate_type(_value, builtins.int, "aggregation_temporality") _args["aggregation_temporality"] = AggregationTemporality(_value) return cls(**_args) @typing.final @_dataclass class Summary(opentelemetry.proto_json._json_codec.JsonMessage): """ Generated from protobuf message Summary """ data_points: builtins.list[SummaryDataPoint] = dataclasses.field(default_factory=builtins.list) def to_dict(self) -> builtins.dict[builtins.str, typing.Any]: """ Convert this message to a dictionary with lowerCamelCase keys. Returns: Dictionary representation following OTLP JSON encoding """ _result = {} if self.data_points: _result["dataPoints"] = opentelemetry.proto_json._json_codec.encode_repeated(self.data_points, lambda _v: _v.to_dict()) return _result @builtins.classmethod def from_dict(cls, data: builtins.dict[builtins.str, typing.Any]) -> Summary: """ Create from a dictionary with lowerCamelCase keys. Args: data: Dictionary representation following OTLP JSON encoding Returns: Summary instance """ opentelemetry.proto_json._json_codec.validate_type(data, builtins.dict, "data") _args = {} if (_value := data.get("dataPoints")) is not None: _args["data_points"] = opentelemetry.proto_json._json_codec.decode_repeated(_value, lambda _v: SummaryDataPoint.from_dict(_v), "data_points") return cls(**_args) @typing.final @_dataclass class NumberDataPoint(opentelemetry.proto_json._json_codec.JsonMessage): """ Generated from protobuf message NumberDataPoint """ attributes: builtins.list[opentelemetry.proto_json.common.v1.common.KeyValue] = dataclasses.field(default_factory=builtins.list) start_time_unix_nano: builtins.int | None = 0 time_unix_nano: builtins.int | None = 0 as_double: builtins.float | None = None as_int: builtins.int | None = None exemplars: builtins.list[Exemplar] = dataclasses.field(default_factory=builtins.list) flags: builtins.int | None = 0 def to_dict(self) -> builtins.dict[builtins.str, typing.Any]: """ Convert this message to a dictionary with lowerCamelCase keys. Returns: Dictionary representation following OTLP JSON encoding """ _result = {} if self.attributes: _result["attributes"] = opentelemetry.proto_json._json_codec.encode_repeated(self.attributes, lambda _v: _v.to_dict()) if self.start_time_unix_nano: _result["startTimeUnixNano"] = opentelemetry.proto_json._json_codec.encode_int64(self.start_time_unix_nano) if self.time_unix_nano: _result["timeUnixNano"] = opentelemetry.proto_json._json_codec.encode_int64(self.time_unix_nano) if self.exemplars: _result["exemplars"] = opentelemetry.proto_json._json_codec.encode_repeated(self.exemplars, lambda _v: _v.to_dict()) if self.flags: _result["flags"] = self.flags if self.as_int is not None: _result["asInt"] = opentelemetry.proto_json._json_codec.encode_int64(self.as_int) elif self.as_double is not None: _result["asDouble"] = opentelemetry.proto_json._json_codec.encode_float(self.as_double) return _result @builtins.classmethod def from_dict(cls, data: builtins.dict[builtins.str, typing.Any]) -> NumberDataPoint: """ Create from a dictionary with lowerCamelCase keys. Args: data: Dictionary representation following OTLP JSON encoding Returns: NumberDataPoint instance """ opentelemetry.proto_json._json_codec.validate_type(data, builtins.dict, "data") _args = {} if (_value := data.get("attributes")) is not None: _args["attributes"] = opentelemetry.proto_json._json_codec.decode_repeated(_value, lambda _v: opentelemetry.proto_json.common.v1.common.KeyValue.from_dict(_v), "attributes") if (_value := data.get("startTimeUnixNano")) is not None: _args["start_time_unix_nano"] = opentelemetry.proto_json._json_codec.decode_int64(_value, "start_time_unix_nano") if (_value := data.get("timeUnixNano")) is not None: _args["time_unix_nano"] = opentelemetry.proto_json._json_codec.decode_int64(_value, "time_unix_nano") if (_value := data.get("exemplars")) is not None: _args["exemplars"] = opentelemetry.proto_json._json_codec.decode_repeated(_value, lambda _v: Exemplar.from_dict(_v), "exemplars") if (_value := data.get("flags")) is not None: opentelemetry.proto_json._json_codec.validate_type(_value, builtins.int, "flags") _args["flags"] = _value if (_value := data.get("asInt")) is not None: _args["as_int"] = opentelemetry.proto_json._json_codec.decode_int64(_value, "as_int") elif (_value := data.get("asDouble")) is not None: _args["as_double"] = opentelemetry.proto_json._json_codec.decode_float(_value, "as_double") return cls(**_args) @typing.final @_dataclass class HistogramDataPoint(opentelemetry.proto_json._json_codec.JsonMessage): """ Generated from protobuf message HistogramDataPoint """ attributes: builtins.list[opentelemetry.proto_json.common.v1.common.KeyValue] = dataclasses.field(default_factory=builtins.list) start_time_unix_nano: builtins.int | None = 0 time_unix_nano: builtins.int | None = 0 count: builtins.int | None = 0 sum: builtins.float | None = None bucket_counts: builtins.list[builtins.int] = dataclasses.field(default_factory=builtins.list) explicit_bounds: builtins.list[builtins.float] = dataclasses.field(default_factory=builtins.list) exemplars: builtins.list[Exemplar] = dataclasses.field(default_factory=builtins.list) flags: builtins.int | None = 0 min: builtins.float | None = None max: builtins.float | None = None def to_dict(self) -> builtins.dict[builtins.str, typing.Any]: """ Convert this message to a dictionary with lowerCamelCase keys. Returns: Dictionary representation following OTLP JSON encoding """ _result = {} if self.attributes: _result["attributes"] = opentelemetry.proto_json._json_codec.encode_repeated(self.attributes, lambda _v: _v.to_dict()) if self.start_time_unix_nano: _result["startTimeUnixNano"] = opentelemetry.proto_json._json_codec.encode_int64(self.start_time_unix_nano) if self.time_unix_nano: _result["timeUnixNano"] = opentelemetry.proto_json._json_codec.encode_int64(self.time_unix_nano) if self.count: _result["count"] = opentelemetry.proto_json._json_codec.encode_int64(self.count) if self.sum is not None: _result["sum"] = opentelemetry.proto_json._json_codec.encode_float(self.sum) if self.bucket_counts: _result["bucketCounts"] = opentelemetry.proto_json._json_codec.encode_repeated(self.bucket_counts, lambda _v: opentelemetry.proto_json._json_codec.encode_int64(_v)) if self.explicit_bounds: _result["explicitBounds"] = opentelemetry.proto_json._json_codec.encode_repeated(self.explicit_bounds, lambda _v: opentelemetry.proto_json._json_codec.encode_float(_v)) if self.exemplars: _result["exemplars"] = opentelemetry.proto_json._json_codec.encode_repeated(self.exemplars, lambda _v: _v.to_dict()) if self.flags: _result["flags"] = self.flags if self.min is not None: _result["min"] = opentelemetry.proto_json._json_codec.encode_float(self.min) if self.max is not None: _result["max"] = opentelemetry.proto_json._json_codec.encode_float(self.max) return _result @builtins.classmethod def from_dict(cls, data: builtins.dict[builtins.str, typing.Any]) -> HistogramDataPoint: """ Create from a dictionary with lowerCamelCase keys. Args: data: Dictionary representation following OTLP JSON encoding Returns: HistogramDataPoint instance """ opentelemetry.proto_json._json_codec.validate_type(data, builtins.dict, "data") _args = {} if (_value := data.get("attributes")) is not None: _args["attributes"] = opentelemetry.proto_json._json_codec.decode_repeated(_value, lambda _v: opentelemetry.proto_json.common.v1.common.KeyValue.from_dict(_v), "attributes") if (_value := data.get("startTimeUnixNano")) is not None: _args["start_time_unix_nano"] = opentelemetry.proto_json._json_codec.decode_int64(_value, "start_time_unix_nano") if (_value := data.get("timeUnixNano")) is not None: _args["time_unix_nano"] = opentelemetry.proto_json._json_codec.decode_int64(_value, "time_unix_nano") if (_value := data.get("count")) is not None: _args["count"] = opentelemetry.proto_json._json_codec.decode_int64(_value, "count") if (_value := data.get("sum")) is not None: _args["sum"] = opentelemetry.proto_json._json_codec.decode_float(_value, "sum") if (_value := data.get("bucketCounts")) is not None: _args["bucket_counts"] = opentelemetry.proto_json._json_codec.decode_repeated(_value, lambda _v: opentelemetry.proto_json._json_codec.decode_int64(_v, "bucket_counts"), "bucket_counts") if (_value := data.get("explicitBounds")) is not None: _args["explicit_bounds"] = opentelemetry.proto_json._json_codec.decode_repeated(_value, lambda _v: opentelemetry.proto_json._json_codec.decode_float(_v, "explicit_bounds"), "explicit_bounds") if (_value := data.get("exemplars")) is not None: _args["exemplars"] = opentelemetry.proto_json._json_codec.decode_repeated(_value, lambda _v: Exemplar.from_dict(_v), "exemplars") if (_value := data.get("flags")) is not None: opentelemetry.proto_json._json_codec.validate_type(_value, builtins.int, "flags") _args["flags"] = _value if (_value := data.get("min")) is not None: _args["min"] = opentelemetry.proto_json._json_codec.decode_float(_value, "min") if (_value := data.get("max")) is not None: _args["max"] = opentelemetry.proto_json._json_codec.decode_float(_value, "max") return cls(**_args) @typing.final @_dataclass class ExponentialHistogramDataPoint(opentelemetry.proto_json._json_codec.JsonMessage): """ Generated from protobuf message ExponentialHistogramDataPoint """ @typing.final @_dataclass class Buckets(opentelemetry.proto_json._json_codec.JsonMessage): """ Generated from protobuf message Buckets """ offset: builtins.int | None = 0 bucket_counts: builtins.list[builtins.int] = dataclasses.field(default_factory=builtins.list) def to_dict(self) -> builtins.dict[builtins.str, typing.Any]: """ Convert this message to a dictionary with lowerCamelCase keys. Returns: Dictionary representation following OTLP JSON encoding """ _result = {} if self.offset: _result["offset"] = self.offset if self.bucket_counts: _result["bucketCounts"] = opentelemetry.proto_json._json_codec.encode_repeated(self.bucket_counts, lambda _v: opentelemetry.proto_json._json_codec.encode_int64(_v)) return _result @builtins.classmethod def from_dict(cls, data: builtins.dict[builtins.str, typing.Any]) -> ExponentialHistogramDataPoint.Buckets: """ Create from a dictionary with lowerCamelCase keys. Args: data: Dictionary representation following OTLP JSON encoding Returns: Buckets instance """ opentelemetry.proto_json._json_codec.validate_type(data, builtins.dict, "data") _args = {} if (_value := data.get("offset")) is not None: opentelemetry.proto_json._json_codec.validate_type(_value, builtins.int, "offset") _args["offset"] = _value if (_value := data.get("bucketCounts")) is not None: _args["bucket_counts"] = opentelemetry.proto_json._json_codec.decode_repeated(_value, lambda _v: opentelemetry.proto_json._json_codec.decode_int64(_v, "bucket_counts"), "bucket_counts") return cls(**_args) attributes: builtins.list[opentelemetry.proto_json.common.v1.common.KeyValue] = dataclasses.field(default_factory=builtins.list) start_time_unix_nano: builtins.int | None = 0 time_unix_nano: builtins.int | None = 0 count: builtins.int | None = 0 sum: builtins.float | None = None scale: builtins.int | None = 0 zero_count: builtins.int | None = 0 positive: ExponentialHistogramDataPoint.Buckets | None = None negative: ExponentialHistogramDataPoint.Buckets | None = None flags: builtins.int | None = 0 exemplars: builtins.list[Exemplar] = dataclasses.field(default_factory=builtins.list) min: builtins.float | None = None max: builtins.float | None = None zero_threshold: builtins.float | None = 0.0 def to_dict(self) -> builtins.dict[builtins.str, typing.Any]: """ Convert this message to a dictionary with lowerCamelCase keys. Returns: Dictionary representation following OTLP JSON encoding """ _result = {} if self.attributes: _result["attributes"] = opentelemetry.proto_json._json_codec.encode_repeated(self.attributes, lambda _v: _v.to_dict()) if self.start_time_unix_nano: _result["startTimeUnixNano"] = opentelemetry.proto_json._json_codec.encode_int64(self.start_time_unix_nano) if self.time_unix_nano: _result["timeUnixNano"] = opentelemetry.proto_json._json_codec.encode_int64(self.time_unix_nano) if self.count: _result["count"] = opentelemetry.proto_json._json_codec.encode_int64(self.count) if self.sum is not None: _result["sum"] = opentelemetry.proto_json._json_codec.encode_float(self.sum) if self.scale: _result["scale"] = self.scale if self.zero_count: _result["zeroCount"] = opentelemetry.proto_json._json_codec.encode_int64(self.zero_count) if self.positive: _result["positive"] = self.positive.to_dict() if self.negative: _result["negative"] = self.negative.to_dict() if self.flags: _result["flags"] = self.flags if self.exemplars: _result["exemplars"] = opentelemetry.proto_json._json_codec.encode_repeated(self.exemplars, lambda _v: _v.to_dict()) if self.min is not None: _result["min"] = opentelemetry.proto_json._json_codec.encode_float(self.min) if self.max is not None: _result["max"] = opentelemetry.proto_json._json_codec.encode_float(self.max) if self.zero_threshold: _result["zeroThreshold"] = opentelemetry.proto_json._json_codec.encode_float(self.zero_threshold) return _result @builtins.classmethod def from_dict(cls, data: builtins.dict[builtins.str, typing.Any]) -> ExponentialHistogramDataPoint: """ Create from a dictionary with lowerCamelCase keys. Args: data: Dictionary representation following OTLP JSON encoding Returns: ExponentialHistogramDataPoint instance """ opentelemetry.proto_json._json_codec.validate_type(data, builtins.dict, "data") _args = {} if (_value := data.get("attributes")) is not None: _args["attributes"] = opentelemetry.proto_json._json_codec.decode_repeated(_value, lambda _v: opentelemetry.proto_json.common.v1.common.KeyValue.from_dict(_v), "attributes") if (_value := data.get("startTimeUnixNano")) is not None: _args["start_time_unix_nano"] = opentelemetry.proto_json._json_codec.decode_int64(_value, "start_time_unix_nano") if (_value := data.get("timeUnixNano")) is not None: _args["time_unix_nano"] = opentelemetry.proto_json._json_codec.decode_int64(_value, "time_unix_nano") if (_value := data.get("count")) is not None: _args["count"] = opentelemetry.proto_json._json_codec.decode_int64(_value, "count") if (_value := data.get("sum")) is not None: _args["sum"] = opentelemetry.proto_json._json_codec.decode_float(_value, "sum") if (_value := data.get("scale")) is not None: opentelemetry.proto_json._json_codec.validate_type(_value, builtins.int, "scale") _args["scale"] = _value if (_value := data.get("zeroCount")) is not None: _args["zero_count"] = opentelemetry.proto_json._json_codec.decode_int64(_value, "zero_count") if (_value := data.get("positive")) is not None: _args["positive"] = ExponentialHistogramDataPoint.Buckets.from_dict(_value) if (_value := data.get("negative")) is not None: _args["negative"] = ExponentialHistogramDataPoint.Buckets.from_dict(_value) if (_value := data.get("flags")) is not None: opentelemetry.proto_json._json_codec.validate_type(_value, builtins.int, "flags") _args["flags"] = _value if (_value := data.get("exemplars")) is not None: _args["exemplars"] = opentelemetry.proto_json._json_codec.decode_repeated(_value, lambda _v: Exemplar.from_dict(_v), "exemplars") if (_value := data.get("min")) is not None: _args["min"] = opentelemetry.proto_json._json_codec.decode_float(_value, "min") if (_value := data.get("max")) is not None: _args["max"] = opentelemetry.proto_json._json_codec.decode_float(_value, "max") if (_value := data.get("zeroThreshold")) is not None: _args["zero_threshold"] = opentelemetry.proto_json._json_codec.decode_float(_value, "zero_threshold") return cls(**_args) @typing.final @_dataclass class SummaryDataPoint(opentelemetry.proto_json._json_codec.JsonMessage): """ Generated from protobuf message SummaryDataPoint """ @typing.final @_dataclass class ValueAtQuantile(opentelemetry.proto_json._json_codec.JsonMessage): """ Generated from protobuf message ValueAtQuantile """ quantile: builtins.float | None = 0.0 value: builtins.float | None = 0.0 def to_dict(self) -> builtins.dict[builtins.str, typing.Any]: """ Convert this message to a dictionary with lowerCamelCase keys. Returns: Dictionary representation following OTLP JSON encoding """ _result = {} if self.quantile: _result["quantile"] = opentelemetry.proto_json._json_codec.encode_float(self.quantile) if self.value: _result["value"] = opentelemetry.proto_json._json_codec.encode_float(self.value) return _result @builtins.classmethod def from_dict(cls, data: builtins.dict[builtins.str, typing.Any]) -> SummaryDataPoint.ValueAtQuantile: """ Create from a dictionary with lowerCamelCase keys. Args: data: Dictionary representation following OTLP JSON encoding Returns: ValueAtQuantile instance """ opentelemetry.proto_json._json_codec.validate_type(data, builtins.dict, "data") _args = {} if (_value := data.get("quantile")) is not None: _args["quantile"] = opentelemetry.proto_json._json_codec.decode_float(_value, "quantile") if (_value := data.get("value")) is not None: _args["value"] = opentelemetry.proto_json._json_codec.decode_float(_value, "value") return cls(**_args) attributes: builtins.list[opentelemetry.proto_json.common.v1.common.KeyValue] = dataclasses.field(default_factory=builtins.list) start_time_unix_nano: builtins.int | None = 0 time_unix_nano: builtins.int | None = 0 count: builtins.int | None = 0 sum: builtins.float | None = 0.0 quantile_values: builtins.list[SummaryDataPoint.ValueAtQuantile] = dataclasses.field(default_factory=builtins.list) flags: builtins.int | None = 0 def to_dict(self) -> builtins.dict[builtins.str, typing.Any]: """ Convert this message to a dictionary with lowerCamelCase keys. Returns: Dictionary representation following OTLP JSON encoding """ _result = {} if self.attributes: _result["attributes"] = opentelemetry.proto_json._json_codec.encode_repeated(self.attributes, lambda _v: _v.to_dict()) if self.start_time_unix_nano: _result["startTimeUnixNano"] = opentelemetry.proto_json._json_codec.encode_int64(self.start_time_unix_nano) if self.time_unix_nano: _result["timeUnixNano"] = opentelemetry.proto_json._json_codec.encode_int64(self.time_unix_nano) if self.count: _result["count"] = opentelemetry.proto_json._json_codec.encode_int64(self.count) if self.sum: _result["sum"] = opentelemetry.proto_json._json_codec.encode_float(self.sum) if self.quantile_values: _result["quantileValues"] = opentelemetry.proto_json._json_codec.encode_repeated(self.quantile_values, lambda _v: _v.to_dict()) if self.flags: _result["flags"] = self.flags return _result @builtins.classmethod def from_dict(cls, data: builtins.dict[builtins.str, typing.Any]) -> SummaryDataPoint: """ Create from a dictionary with lowerCamelCase keys. Args: data: Dictionary representation following OTLP JSON encoding Returns: SummaryDataPoint instance """ opentelemetry.proto_json._json_codec.validate_type(data, builtins.dict, "data") _args = {} if (_value := data.get("attributes")) is not None: _args["attributes"] = opentelemetry.proto_json._json_codec.decode_repeated(_value, lambda _v: opentelemetry.proto_json.common.v1.common.KeyValue.from_dict(_v), "attributes") if (_value := data.get("startTimeUnixNano")) is not None: _args["start_time_unix_nano"] = opentelemetry.proto_json._json_codec.decode_int64(_value, "start_time_unix_nano") if (_value := data.get("timeUnixNano")) is not None: _args["time_unix_nano"] = opentelemetry.proto_json._json_codec.decode_int64(_value, "time_unix_nano") if (_value := data.get("count")) is not None: _args["count"] = opentelemetry.proto_json._json_codec.decode_int64(_value, "count") if (_value := data.get("sum")) is not None: _args["sum"] = opentelemetry.proto_json._json_codec.decode_float(_value, "sum") if (_value := data.get("quantileValues")) is not None: _args["quantile_values"] = opentelemetry.proto_json._json_codec.decode_repeated(_value, lambda _v: SummaryDataPoint.ValueAtQuantile.from_dict(_v), "quantile_values") if (_value := data.get("flags")) is not None: opentelemetry.proto_json._json_codec.validate_type(_value, builtins.int, "flags") _args["flags"] = _value return cls(**_args) @typing.final @_dataclass class Exemplar(opentelemetry.proto_json._json_codec.JsonMessage): """ Generated from protobuf message Exemplar """ filtered_attributes: builtins.list[opentelemetry.proto_json.common.v1.common.KeyValue] = dataclasses.field(default_factory=builtins.list) time_unix_nano: builtins.int | None = 0 as_double: builtins.float | None = None as_int: builtins.int | None = None span_id: builtins.bytes | None = b"" trace_id: builtins.bytes | None = b"" def to_dict(self) -> builtins.dict[builtins.str, typing.Any]: """ Convert this message to a dictionary with lowerCamelCase keys. Returns: Dictionary representation following OTLP JSON encoding """ _result = {} if self.filtered_attributes: _result["filteredAttributes"] = opentelemetry.proto_json._json_codec.encode_repeated(self.filtered_attributes, lambda _v: _v.to_dict()) if self.time_unix_nano: _result["timeUnixNano"] = opentelemetry.proto_json._json_codec.encode_int64(self.time_unix_nano) if self.span_id: _result["spanId"] = opentelemetry.proto_json._json_codec.encode_hex(self.span_id) if self.trace_id: _result["traceId"] = opentelemetry.proto_json._json_codec.encode_hex(self.trace_id) if self.as_int is not None: _result["asInt"] = opentelemetry.proto_json._json_codec.encode_int64(self.as_int) elif self.as_double is not None: _result["asDouble"] = opentelemetry.proto_json._json_codec.encode_float(self.as_double) return _result @builtins.classmethod def from_dict(cls, data: builtins.dict[builtins.str, typing.Any]) -> Exemplar: """ Create from a dictionary with lowerCamelCase keys. Args: data: Dictionary representation following OTLP JSON encoding Returns: Exemplar instance """ opentelemetry.proto_json._json_codec.validate_type(data, builtins.dict, "data") _args = {} if (_value := data.get("filteredAttributes")) is not None: _args["filtered_attributes"] = opentelemetry.proto_json._json_codec.decode_repeated(_value, lambda _v: opentelemetry.proto_json.common.v1.common.KeyValue.from_dict(_v), "filtered_attributes") if (_value := data.get("timeUnixNano")) is not None: _args["time_unix_nano"] = opentelemetry.proto_json._json_codec.decode_int64(_value, "time_unix_nano") if (_value := data.get("spanId")) is not None: _args["span_id"] = opentelemetry.proto_json._json_codec.decode_hex(_value, "span_id") if (_value := data.get("traceId")) is not None: _args["trace_id"] = opentelemetry.proto_json._json_codec.decode_hex(_value, "trace_id") if (_value := data.get("asInt")) is not None: _args["as_int"] = opentelemetry.proto_json._json_codec.decode_int64(_value, "as_int") elif (_value := data.get("asDouble")) is not None: _args["as_double"] = opentelemetry.proto_json._json_codec.decode_float(_value, "as_double") return cls(**_args) python-opentelemetry-1.45.2/opentelemetry-proto-json/src/opentelemetry/proto_json/profiles/000077500000000000000000000000001527070070700325315ustar00rootroot00000000000000__init__.py000066400000000000000000000000001527070070700345510ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-proto-json/src/opentelemetry/proto_json/profilesv1development/000077500000000000000000000000001527070070700352435ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-proto-json/src/opentelemetry/proto_json/profiles__init__.py000066400000000000000000000000001527070070700373420ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-proto-json/src/opentelemetry/proto_json/profiles/v1developmentprofiles.py000066400000000000000000001022561527070070700374460ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-proto-json/src/opentelemetry/proto_json/profiles/v1development# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # AUTO-GENERATED from "opentelemetry/proto/profiles/v1development/profiles.proto" # DO NOT EDIT MANUALLY from __future__ import annotations import builtins import dataclasses import functools import typing _dataclass = functools.partial(dataclasses.dataclass, slots=True) import opentelemetry.proto_json._json_codec import opentelemetry.proto_json.common.v1.common import opentelemetry.proto_json.resource.v1.resource @typing.final @_dataclass class ProfilesDictionary(opentelemetry.proto_json._json_codec.JsonMessage): """ Generated from protobuf message ProfilesDictionary """ mapping_table: builtins.list[Mapping] = dataclasses.field(default_factory=builtins.list) location_table: builtins.list[Location] = dataclasses.field(default_factory=builtins.list) function_table: builtins.list[Function] = dataclasses.field(default_factory=builtins.list) link_table: builtins.list[Link] = dataclasses.field(default_factory=builtins.list) string_table: builtins.list[builtins.str] = dataclasses.field(default_factory=builtins.list) attribute_table: builtins.list[KeyValueAndUnit] = dataclasses.field(default_factory=builtins.list) stack_table: builtins.list[Stack] = dataclasses.field(default_factory=builtins.list) def to_dict(self) -> builtins.dict[builtins.str, typing.Any]: """ Convert this message to a dictionary with lowerCamelCase keys. Returns: Dictionary representation following OTLP JSON encoding """ _result = {} if self.mapping_table: _result["mappingTable"] = opentelemetry.proto_json._json_codec.encode_repeated(self.mapping_table, lambda _v: _v.to_dict()) if self.location_table: _result["locationTable"] = opentelemetry.proto_json._json_codec.encode_repeated(self.location_table, lambda _v: _v.to_dict()) if self.function_table: _result["functionTable"] = opentelemetry.proto_json._json_codec.encode_repeated(self.function_table, lambda _v: _v.to_dict()) if self.link_table: _result["linkTable"] = opentelemetry.proto_json._json_codec.encode_repeated(self.link_table, lambda _v: _v.to_dict()) if self.string_table: _result["stringTable"] = self.string_table if self.attribute_table: _result["attributeTable"] = opentelemetry.proto_json._json_codec.encode_repeated(self.attribute_table, lambda _v: _v.to_dict()) if self.stack_table: _result["stackTable"] = opentelemetry.proto_json._json_codec.encode_repeated(self.stack_table, lambda _v: _v.to_dict()) return _result @builtins.classmethod def from_dict(cls, data: builtins.dict[builtins.str, typing.Any]) -> ProfilesDictionary: """ Create from a dictionary with lowerCamelCase keys. Args: data: Dictionary representation following OTLP JSON encoding Returns: ProfilesDictionary instance """ opentelemetry.proto_json._json_codec.validate_type(data, builtins.dict, "data") _args = {} if (_value := data.get("mappingTable")) is not None: _args["mapping_table"] = opentelemetry.proto_json._json_codec.decode_repeated(_value, lambda _v: Mapping.from_dict(_v), "mapping_table") if (_value := data.get("locationTable")) is not None: _args["location_table"] = opentelemetry.proto_json._json_codec.decode_repeated(_value, lambda _v: Location.from_dict(_v), "location_table") if (_value := data.get("functionTable")) is not None: _args["function_table"] = opentelemetry.proto_json._json_codec.decode_repeated(_value, lambda _v: Function.from_dict(_v), "function_table") if (_value := data.get("linkTable")) is not None: _args["link_table"] = opentelemetry.proto_json._json_codec.decode_repeated(_value, lambda _v: Link.from_dict(_v), "link_table") if (_value := data.get("stringTable")) is not None: _args["string_table"] = opentelemetry.proto_json._json_codec.decode_repeated(_value, lambda _v: _v, "string_table") if (_value := data.get("attributeTable")) is not None: _args["attribute_table"] = opentelemetry.proto_json._json_codec.decode_repeated(_value, lambda _v: KeyValueAndUnit.from_dict(_v), "attribute_table") if (_value := data.get("stackTable")) is not None: _args["stack_table"] = opentelemetry.proto_json._json_codec.decode_repeated(_value, lambda _v: Stack.from_dict(_v), "stack_table") return cls(**_args) @typing.final @_dataclass class ProfilesData(opentelemetry.proto_json._json_codec.JsonMessage): """ Generated from protobuf message ProfilesData """ resource_profiles: builtins.list[ResourceProfiles] = dataclasses.field(default_factory=builtins.list) dictionary: ProfilesDictionary | None = None def to_dict(self) -> builtins.dict[builtins.str, typing.Any]: """ Convert this message to a dictionary with lowerCamelCase keys. Returns: Dictionary representation following OTLP JSON encoding """ _result = {} if self.resource_profiles: _result["resourceProfiles"] = opentelemetry.proto_json._json_codec.encode_repeated(self.resource_profiles, lambda _v: _v.to_dict()) if self.dictionary: _result["dictionary"] = self.dictionary.to_dict() return _result @builtins.classmethod def from_dict(cls, data: builtins.dict[builtins.str, typing.Any]) -> ProfilesData: """ Create from a dictionary with lowerCamelCase keys. Args: data: Dictionary representation following OTLP JSON encoding Returns: ProfilesData instance """ opentelemetry.proto_json._json_codec.validate_type(data, builtins.dict, "data") _args = {} if (_value := data.get("resourceProfiles")) is not None: _args["resource_profiles"] = opentelemetry.proto_json._json_codec.decode_repeated(_value, lambda _v: ResourceProfiles.from_dict(_v), "resource_profiles") if (_value := data.get("dictionary")) is not None: _args["dictionary"] = ProfilesDictionary.from_dict(_value) return cls(**_args) @typing.final @_dataclass class ResourceProfiles(opentelemetry.proto_json._json_codec.JsonMessage): """ Generated from protobuf message ResourceProfiles """ resource: opentelemetry.proto_json.resource.v1.resource.Resource | None = None scope_profiles: builtins.list[ScopeProfiles] = dataclasses.field(default_factory=builtins.list) schema_url: builtins.str | None = "" def to_dict(self) -> builtins.dict[builtins.str, typing.Any]: """ Convert this message to a dictionary with lowerCamelCase keys. Returns: Dictionary representation following OTLP JSON encoding """ _result = {} if self.resource: _result["resource"] = self.resource.to_dict() if self.scope_profiles: _result["scopeProfiles"] = opentelemetry.proto_json._json_codec.encode_repeated(self.scope_profiles, lambda _v: _v.to_dict()) if self.schema_url: _result["schemaUrl"] = self.schema_url return _result @builtins.classmethod def from_dict(cls, data: builtins.dict[builtins.str, typing.Any]) -> ResourceProfiles: """ Create from a dictionary with lowerCamelCase keys. Args: data: Dictionary representation following OTLP JSON encoding Returns: ResourceProfiles instance """ opentelemetry.proto_json._json_codec.validate_type(data, builtins.dict, "data") _args = {} if (_value := data.get("resource")) is not None: _args["resource"] = opentelemetry.proto_json.resource.v1.resource.Resource.from_dict(_value) if (_value := data.get("scopeProfiles")) is not None: _args["scope_profiles"] = opentelemetry.proto_json._json_codec.decode_repeated(_value, lambda _v: ScopeProfiles.from_dict(_v), "scope_profiles") if (_value := data.get("schemaUrl")) is not None: opentelemetry.proto_json._json_codec.validate_type(_value, builtins.str, "schema_url") _args["schema_url"] = _value return cls(**_args) @typing.final @_dataclass class ScopeProfiles(opentelemetry.proto_json._json_codec.JsonMessage): """ Generated from protobuf message ScopeProfiles """ scope: opentelemetry.proto_json.common.v1.common.InstrumentationScope | None = None profiles: builtins.list[Profile] = dataclasses.field(default_factory=builtins.list) schema_url: builtins.str | None = "" def to_dict(self) -> builtins.dict[builtins.str, typing.Any]: """ Convert this message to a dictionary with lowerCamelCase keys. Returns: Dictionary representation following OTLP JSON encoding """ _result = {} if self.scope: _result["scope"] = self.scope.to_dict() if self.profiles: _result["profiles"] = opentelemetry.proto_json._json_codec.encode_repeated(self.profiles, lambda _v: _v.to_dict()) if self.schema_url: _result["schemaUrl"] = self.schema_url return _result @builtins.classmethod def from_dict(cls, data: builtins.dict[builtins.str, typing.Any]) -> ScopeProfiles: """ Create from a dictionary with lowerCamelCase keys. Args: data: Dictionary representation following OTLP JSON encoding Returns: ScopeProfiles instance """ opentelemetry.proto_json._json_codec.validate_type(data, builtins.dict, "data") _args = {} if (_value := data.get("scope")) is not None: _args["scope"] = opentelemetry.proto_json.common.v1.common.InstrumentationScope.from_dict(_value) if (_value := data.get("profiles")) is not None: _args["profiles"] = opentelemetry.proto_json._json_codec.decode_repeated(_value, lambda _v: Profile.from_dict(_v), "profiles") if (_value := data.get("schemaUrl")) is not None: opentelemetry.proto_json._json_codec.validate_type(_value, builtins.str, "schema_url") _args["schema_url"] = _value return cls(**_args) @typing.final @_dataclass class Profile(opentelemetry.proto_json._json_codec.JsonMessage): """ Generated from protobuf message Profile """ sample_type: ValueType | None = None samples: builtins.list[Sample] = dataclasses.field(default_factory=builtins.list) time_unix_nano: builtins.int | None = 0 duration_nano: builtins.int | None = 0 period_type: ValueType | None = None period: builtins.int | None = 0 profile_id: builtins.bytes | None = b"" dropped_attributes_count: builtins.int | None = 0 original_payload_format: builtins.str | None = "" original_payload: builtins.bytes | None = b"" attribute_indices: builtins.list[builtins.int] = dataclasses.field(default_factory=builtins.list) def to_dict(self) -> builtins.dict[builtins.str, typing.Any]: """ Convert this message to a dictionary with lowerCamelCase keys. Returns: Dictionary representation following OTLP JSON encoding """ _result = {} if self.sample_type: _result["sampleType"] = self.sample_type.to_dict() if self.samples: _result["samples"] = opentelemetry.proto_json._json_codec.encode_repeated(self.samples, lambda _v: _v.to_dict()) if self.time_unix_nano: _result["timeUnixNano"] = opentelemetry.proto_json._json_codec.encode_int64(self.time_unix_nano) if self.duration_nano: _result["durationNano"] = opentelemetry.proto_json._json_codec.encode_int64(self.duration_nano) if self.period_type: _result["periodType"] = self.period_type.to_dict() if self.period: _result["period"] = opentelemetry.proto_json._json_codec.encode_int64(self.period) if self.profile_id: _result["profileId"] = opentelemetry.proto_json._json_codec.encode_base64(self.profile_id) if self.dropped_attributes_count: _result["droppedAttributesCount"] = self.dropped_attributes_count if self.original_payload_format: _result["originalPayloadFormat"] = self.original_payload_format if self.original_payload: _result["originalPayload"] = opentelemetry.proto_json._json_codec.encode_base64(self.original_payload) if self.attribute_indices: _result["attributeIndices"] = self.attribute_indices return _result @builtins.classmethod def from_dict(cls, data: builtins.dict[builtins.str, typing.Any]) -> Profile: """ Create from a dictionary with lowerCamelCase keys. Args: data: Dictionary representation following OTLP JSON encoding Returns: Profile instance """ opentelemetry.proto_json._json_codec.validate_type(data, builtins.dict, "data") _args = {} if (_value := data.get("sampleType")) is not None: _args["sample_type"] = ValueType.from_dict(_value) if (_value := data.get("samples")) is not None: _args["samples"] = opentelemetry.proto_json._json_codec.decode_repeated(_value, lambda _v: Sample.from_dict(_v), "samples") if (_value := data.get("timeUnixNano")) is not None: _args["time_unix_nano"] = opentelemetry.proto_json._json_codec.decode_int64(_value, "time_unix_nano") if (_value := data.get("durationNano")) is not None: _args["duration_nano"] = opentelemetry.proto_json._json_codec.decode_int64(_value, "duration_nano") if (_value := data.get("periodType")) is not None: _args["period_type"] = ValueType.from_dict(_value) if (_value := data.get("period")) is not None: _args["period"] = opentelemetry.proto_json._json_codec.decode_int64(_value, "period") if (_value := data.get("profileId")) is not None: _args["profile_id"] = opentelemetry.proto_json._json_codec.decode_base64(_value, "profile_id") if (_value := data.get("droppedAttributesCount")) is not None: opentelemetry.proto_json._json_codec.validate_type(_value, builtins.int, "dropped_attributes_count") _args["dropped_attributes_count"] = _value if (_value := data.get("originalPayloadFormat")) is not None: opentelemetry.proto_json._json_codec.validate_type(_value, builtins.str, "original_payload_format") _args["original_payload_format"] = _value if (_value := data.get("originalPayload")) is not None: _args["original_payload"] = opentelemetry.proto_json._json_codec.decode_base64(_value, "original_payload") if (_value := data.get("attributeIndices")) is not None: _args["attribute_indices"] = opentelemetry.proto_json._json_codec.decode_repeated(_value, lambda _v: _v, "attribute_indices") return cls(**_args) @typing.final @_dataclass class Link(opentelemetry.proto_json._json_codec.JsonMessage): """ Generated from protobuf message Link """ trace_id: builtins.bytes | None = b"" span_id: builtins.bytes | None = b"" def to_dict(self) -> builtins.dict[builtins.str, typing.Any]: """ Convert this message to a dictionary with lowerCamelCase keys. Returns: Dictionary representation following OTLP JSON encoding """ _result = {} if self.trace_id: _result["traceId"] = opentelemetry.proto_json._json_codec.encode_hex(self.trace_id) if self.span_id: _result["spanId"] = opentelemetry.proto_json._json_codec.encode_hex(self.span_id) return _result @builtins.classmethod def from_dict(cls, data: builtins.dict[builtins.str, typing.Any]) -> Link: """ Create from a dictionary with lowerCamelCase keys. Args: data: Dictionary representation following OTLP JSON encoding Returns: Link instance """ opentelemetry.proto_json._json_codec.validate_type(data, builtins.dict, "data") _args = {} if (_value := data.get("traceId")) is not None: _args["trace_id"] = opentelemetry.proto_json._json_codec.decode_hex(_value, "trace_id") if (_value := data.get("spanId")) is not None: _args["span_id"] = opentelemetry.proto_json._json_codec.decode_hex(_value, "span_id") return cls(**_args) @typing.final @_dataclass class ValueType(opentelemetry.proto_json._json_codec.JsonMessage): """ Generated from protobuf message ValueType """ type_strindex: builtins.int | None = 0 unit_strindex: builtins.int | None = 0 def to_dict(self) -> builtins.dict[builtins.str, typing.Any]: """ Convert this message to a dictionary with lowerCamelCase keys. Returns: Dictionary representation following OTLP JSON encoding """ _result = {} if self.type_strindex: _result["typeStrindex"] = self.type_strindex if self.unit_strindex: _result["unitStrindex"] = self.unit_strindex return _result @builtins.classmethod def from_dict(cls, data: builtins.dict[builtins.str, typing.Any]) -> ValueType: """ Create from a dictionary with lowerCamelCase keys. Args: data: Dictionary representation following OTLP JSON encoding Returns: ValueType instance """ opentelemetry.proto_json._json_codec.validate_type(data, builtins.dict, "data") _args = {} if (_value := data.get("typeStrindex")) is not None: opentelemetry.proto_json._json_codec.validate_type(_value, builtins.int, "type_strindex") _args["type_strindex"] = _value if (_value := data.get("unitStrindex")) is not None: opentelemetry.proto_json._json_codec.validate_type(_value, builtins.int, "unit_strindex") _args["unit_strindex"] = _value return cls(**_args) @typing.final @_dataclass class Sample(opentelemetry.proto_json._json_codec.JsonMessage): """ Generated from protobuf message Sample """ stack_index: builtins.int | None = 0 attribute_indices: builtins.list[builtins.int] = dataclasses.field(default_factory=builtins.list) link_index: builtins.int | None = 0 values: builtins.list[builtins.int] = dataclasses.field(default_factory=builtins.list) timestamps_unix_nano: builtins.list[builtins.int] = dataclasses.field(default_factory=builtins.list) def to_dict(self) -> builtins.dict[builtins.str, typing.Any]: """ Convert this message to a dictionary with lowerCamelCase keys. Returns: Dictionary representation following OTLP JSON encoding """ _result = {} if self.stack_index: _result["stackIndex"] = self.stack_index if self.attribute_indices: _result["attributeIndices"] = self.attribute_indices if self.link_index: _result["linkIndex"] = self.link_index if self.values: _result["values"] = opentelemetry.proto_json._json_codec.encode_repeated(self.values, lambda _v: opentelemetry.proto_json._json_codec.encode_int64(_v)) if self.timestamps_unix_nano: _result["timestampsUnixNano"] = opentelemetry.proto_json._json_codec.encode_repeated(self.timestamps_unix_nano, lambda _v: opentelemetry.proto_json._json_codec.encode_int64(_v)) return _result @builtins.classmethod def from_dict(cls, data: builtins.dict[builtins.str, typing.Any]) -> Sample: """ Create from a dictionary with lowerCamelCase keys. Args: data: Dictionary representation following OTLP JSON encoding Returns: Sample instance """ opentelemetry.proto_json._json_codec.validate_type(data, builtins.dict, "data") _args = {} if (_value := data.get("stackIndex")) is not None: opentelemetry.proto_json._json_codec.validate_type(_value, builtins.int, "stack_index") _args["stack_index"] = _value if (_value := data.get("attributeIndices")) is not None: _args["attribute_indices"] = opentelemetry.proto_json._json_codec.decode_repeated(_value, lambda _v: _v, "attribute_indices") if (_value := data.get("linkIndex")) is not None: opentelemetry.proto_json._json_codec.validate_type(_value, builtins.int, "link_index") _args["link_index"] = _value if (_value := data.get("values")) is not None: _args["values"] = opentelemetry.proto_json._json_codec.decode_repeated(_value, lambda _v: opentelemetry.proto_json._json_codec.decode_int64(_v, "values"), "values") if (_value := data.get("timestampsUnixNano")) is not None: _args["timestamps_unix_nano"] = opentelemetry.proto_json._json_codec.decode_repeated(_value, lambda _v: opentelemetry.proto_json._json_codec.decode_int64(_v, "timestamps_unix_nano"), "timestamps_unix_nano") return cls(**_args) @typing.final @_dataclass class Mapping(opentelemetry.proto_json._json_codec.JsonMessage): """ Generated from protobuf message Mapping """ memory_start: builtins.int | None = 0 memory_limit: builtins.int | None = 0 file_offset: builtins.int | None = 0 filename_strindex: builtins.int | None = 0 attribute_indices: builtins.list[builtins.int] = dataclasses.field(default_factory=builtins.list) def to_dict(self) -> builtins.dict[builtins.str, typing.Any]: """ Convert this message to a dictionary with lowerCamelCase keys. Returns: Dictionary representation following OTLP JSON encoding """ _result = {} if self.memory_start: _result["memoryStart"] = opentelemetry.proto_json._json_codec.encode_int64(self.memory_start) if self.memory_limit: _result["memoryLimit"] = opentelemetry.proto_json._json_codec.encode_int64(self.memory_limit) if self.file_offset: _result["fileOffset"] = opentelemetry.proto_json._json_codec.encode_int64(self.file_offset) if self.filename_strindex: _result["filenameStrindex"] = self.filename_strindex if self.attribute_indices: _result["attributeIndices"] = self.attribute_indices return _result @builtins.classmethod def from_dict(cls, data: builtins.dict[builtins.str, typing.Any]) -> Mapping: """ Create from a dictionary with lowerCamelCase keys. Args: data: Dictionary representation following OTLP JSON encoding Returns: Mapping instance """ opentelemetry.proto_json._json_codec.validate_type(data, builtins.dict, "data") _args = {} if (_value := data.get("memoryStart")) is not None: _args["memory_start"] = opentelemetry.proto_json._json_codec.decode_int64(_value, "memory_start") if (_value := data.get("memoryLimit")) is not None: _args["memory_limit"] = opentelemetry.proto_json._json_codec.decode_int64(_value, "memory_limit") if (_value := data.get("fileOffset")) is not None: _args["file_offset"] = opentelemetry.proto_json._json_codec.decode_int64(_value, "file_offset") if (_value := data.get("filenameStrindex")) is not None: opentelemetry.proto_json._json_codec.validate_type(_value, builtins.int, "filename_strindex") _args["filename_strindex"] = _value if (_value := data.get("attributeIndices")) is not None: _args["attribute_indices"] = opentelemetry.proto_json._json_codec.decode_repeated(_value, lambda _v: _v, "attribute_indices") return cls(**_args) @typing.final @_dataclass class Stack(opentelemetry.proto_json._json_codec.JsonMessage): """ Generated from protobuf message Stack """ location_indices: builtins.list[builtins.int] = dataclasses.field(default_factory=builtins.list) def to_dict(self) -> builtins.dict[builtins.str, typing.Any]: """ Convert this message to a dictionary with lowerCamelCase keys. Returns: Dictionary representation following OTLP JSON encoding """ _result = {} if self.location_indices: _result["locationIndices"] = self.location_indices return _result @builtins.classmethod def from_dict(cls, data: builtins.dict[builtins.str, typing.Any]) -> Stack: """ Create from a dictionary with lowerCamelCase keys. Args: data: Dictionary representation following OTLP JSON encoding Returns: Stack instance """ opentelemetry.proto_json._json_codec.validate_type(data, builtins.dict, "data") _args = {} if (_value := data.get("locationIndices")) is not None: _args["location_indices"] = opentelemetry.proto_json._json_codec.decode_repeated(_value, lambda _v: _v, "location_indices") return cls(**_args) @typing.final @_dataclass class Location(opentelemetry.proto_json._json_codec.JsonMessage): """ Generated from protobuf message Location """ mapping_index: builtins.int | None = 0 address: builtins.int | None = 0 lines: builtins.list[Line] = dataclasses.field(default_factory=builtins.list) attribute_indices: builtins.list[builtins.int] = dataclasses.field(default_factory=builtins.list) def to_dict(self) -> builtins.dict[builtins.str, typing.Any]: """ Convert this message to a dictionary with lowerCamelCase keys. Returns: Dictionary representation following OTLP JSON encoding """ _result = {} if self.mapping_index: _result["mappingIndex"] = self.mapping_index if self.address: _result["address"] = opentelemetry.proto_json._json_codec.encode_int64(self.address) if self.lines: _result["lines"] = opentelemetry.proto_json._json_codec.encode_repeated(self.lines, lambda _v: _v.to_dict()) if self.attribute_indices: _result["attributeIndices"] = self.attribute_indices return _result @builtins.classmethod def from_dict(cls, data: builtins.dict[builtins.str, typing.Any]) -> Location: """ Create from a dictionary with lowerCamelCase keys. Args: data: Dictionary representation following OTLP JSON encoding Returns: Location instance """ opentelemetry.proto_json._json_codec.validate_type(data, builtins.dict, "data") _args = {} if (_value := data.get("mappingIndex")) is not None: opentelemetry.proto_json._json_codec.validate_type(_value, builtins.int, "mapping_index") _args["mapping_index"] = _value if (_value := data.get("address")) is not None: _args["address"] = opentelemetry.proto_json._json_codec.decode_int64(_value, "address") if (_value := data.get("lines")) is not None: _args["lines"] = opentelemetry.proto_json._json_codec.decode_repeated(_value, lambda _v: Line.from_dict(_v), "lines") if (_value := data.get("attributeIndices")) is not None: _args["attribute_indices"] = opentelemetry.proto_json._json_codec.decode_repeated(_value, lambda _v: _v, "attribute_indices") return cls(**_args) @typing.final @_dataclass class Line(opentelemetry.proto_json._json_codec.JsonMessage): """ Generated from protobuf message Line """ function_index: builtins.int | None = 0 line: builtins.int | None = 0 column: builtins.int | None = 0 def to_dict(self) -> builtins.dict[builtins.str, typing.Any]: """ Convert this message to a dictionary with lowerCamelCase keys. Returns: Dictionary representation following OTLP JSON encoding """ _result = {} if self.function_index: _result["functionIndex"] = self.function_index if self.line: _result["line"] = opentelemetry.proto_json._json_codec.encode_int64(self.line) if self.column: _result["column"] = opentelemetry.proto_json._json_codec.encode_int64(self.column) return _result @builtins.classmethod def from_dict(cls, data: builtins.dict[builtins.str, typing.Any]) -> Line: """ Create from a dictionary with lowerCamelCase keys. Args: data: Dictionary representation following OTLP JSON encoding Returns: Line instance """ opentelemetry.proto_json._json_codec.validate_type(data, builtins.dict, "data") _args = {} if (_value := data.get("functionIndex")) is not None: opentelemetry.proto_json._json_codec.validate_type(_value, builtins.int, "function_index") _args["function_index"] = _value if (_value := data.get("line")) is not None: _args["line"] = opentelemetry.proto_json._json_codec.decode_int64(_value, "line") if (_value := data.get("column")) is not None: _args["column"] = opentelemetry.proto_json._json_codec.decode_int64(_value, "column") return cls(**_args) @typing.final @_dataclass class Function(opentelemetry.proto_json._json_codec.JsonMessage): """ Generated from protobuf message Function """ name_strindex: builtins.int | None = 0 system_name_strindex: builtins.int | None = 0 filename_strindex: builtins.int | None = 0 start_line: builtins.int | None = 0 def to_dict(self) -> builtins.dict[builtins.str, typing.Any]: """ Convert this message to a dictionary with lowerCamelCase keys. Returns: Dictionary representation following OTLP JSON encoding """ _result = {} if self.name_strindex: _result["nameStrindex"] = self.name_strindex if self.system_name_strindex: _result["systemNameStrindex"] = self.system_name_strindex if self.filename_strindex: _result["filenameStrindex"] = self.filename_strindex if self.start_line: _result["startLine"] = opentelemetry.proto_json._json_codec.encode_int64(self.start_line) return _result @builtins.classmethod def from_dict(cls, data: builtins.dict[builtins.str, typing.Any]) -> Function: """ Create from a dictionary with lowerCamelCase keys. Args: data: Dictionary representation following OTLP JSON encoding Returns: Function instance """ opentelemetry.proto_json._json_codec.validate_type(data, builtins.dict, "data") _args = {} if (_value := data.get("nameStrindex")) is not None: opentelemetry.proto_json._json_codec.validate_type(_value, builtins.int, "name_strindex") _args["name_strindex"] = _value if (_value := data.get("systemNameStrindex")) is not None: opentelemetry.proto_json._json_codec.validate_type(_value, builtins.int, "system_name_strindex") _args["system_name_strindex"] = _value if (_value := data.get("filenameStrindex")) is not None: opentelemetry.proto_json._json_codec.validate_type(_value, builtins.int, "filename_strindex") _args["filename_strindex"] = _value if (_value := data.get("startLine")) is not None: _args["start_line"] = opentelemetry.proto_json._json_codec.decode_int64(_value, "start_line") return cls(**_args) @typing.final @_dataclass class KeyValueAndUnit(opentelemetry.proto_json._json_codec.JsonMessage): """ Generated from protobuf message KeyValueAndUnit """ key_strindex: builtins.int | None = 0 value: opentelemetry.proto_json.common.v1.common.AnyValue | None = None unit_strindex: builtins.int | None = 0 def to_dict(self) -> builtins.dict[builtins.str, typing.Any]: """ Convert this message to a dictionary with lowerCamelCase keys. Returns: Dictionary representation following OTLP JSON encoding """ _result = {} if self.key_strindex: _result["keyStrindex"] = self.key_strindex if self.value: _result["value"] = self.value.to_dict() if self.unit_strindex: _result["unitStrindex"] = self.unit_strindex return _result @builtins.classmethod def from_dict(cls, data: builtins.dict[builtins.str, typing.Any]) -> KeyValueAndUnit: """ Create from a dictionary with lowerCamelCase keys. Args: data: Dictionary representation following OTLP JSON encoding Returns: KeyValueAndUnit instance """ opentelemetry.proto_json._json_codec.validate_type(data, builtins.dict, "data") _args = {} if (_value := data.get("keyStrindex")) is not None: opentelemetry.proto_json._json_codec.validate_type(_value, builtins.int, "key_strindex") _args["key_strindex"] = _value if (_value := data.get("value")) is not None: _args["value"] = opentelemetry.proto_json.common.v1.common.AnyValue.from_dict(_value) if (_value := data.get("unitStrindex")) is not None: opentelemetry.proto_json._json_codec.validate_type(_value, builtins.int, "unit_strindex") _args["unit_strindex"] = _value return cls(**_args) python-opentelemetry-1.45.2/opentelemetry-proto-json/src/opentelemetry/proto_json/py.typed000066400000000000000000000000001527070070700323730ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-proto-json/src/opentelemetry/proto_json/resource/000077500000000000000000000000001527070070700325355ustar00rootroot00000000000000__init__.py000066400000000000000000000000001527070070700345550ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-proto-json/src/opentelemetry/proto_json/resourcepython-opentelemetry-1.45.2/opentelemetry-proto-json/src/opentelemetry/proto_json/resource/v1/000077500000000000000000000000001527070070700330635ustar00rootroot00000000000000__init__.py000066400000000000000000000000001527070070700351030ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-proto-json/src/opentelemetry/proto_json/resource/v1resource.py000066400000000000000000000054321527070070700352110ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-proto-json/src/opentelemetry/proto_json/resource/v1# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # AUTO-GENERATED from "opentelemetry/proto/resource/v1/resource.proto" # DO NOT EDIT MANUALLY from __future__ import annotations import builtins import dataclasses import functools import typing _dataclass = functools.partial(dataclasses.dataclass, slots=True) import opentelemetry.proto_json._json_codec import opentelemetry.proto_json.common.v1.common @typing.final @_dataclass class Resource(opentelemetry.proto_json._json_codec.JsonMessage): """ Generated from protobuf message Resource """ attributes: builtins.list[opentelemetry.proto_json.common.v1.common.KeyValue] = dataclasses.field(default_factory=builtins.list) dropped_attributes_count: builtins.int | None = 0 entity_refs: builtins.list[opentelemetry.proto_json.common.v1.common.EntityRef] = dataclasses.field(default_factory=builtins.list) def to_dict(self) -> builtins.dict[builtins.str, typing.Any]: """ Convert this message to a dictionary with lowerCamelCase keys. Returns: Dictionary representation following OTLP JSON encoding """ _result = {} if self.attributes: _result["attributes"] = opentelemetry.proto_json._json_codec.encode_repeated(self.attributes, lambda _v: _v.to_dict()) if self.dropped_attributes_count: _result["droppedAttributesCount"] = self.dropped_attributes_count if self.entity_refs: _result["entityRefs"] = opentelemetry.proto_json._json_codec.encode_repeated(self.entity_refs, lambda _v: _v.to_dict()) return _result @builtins.classmethod def from_dict(cls, data: builtins.dict[builtins.str, typing.Any]) -> Resource: """ Create from a dictionary with lowerCamelCase keys. Args: data: Dictionary representation following OTLP JSON encoding Returns: Resource instance """ opentelemetry.proto_json._json_codec.validate_type(data, builtins.dict, "data") _args = {} if (_value := data.get("attributes")) is not None: _args["attributes"] = opentelemetry.proto_json._json_codec.decode_repeated(_value, lambda _v: opentelemetry.proto_json.common.v1.common.KeyValue.from_dict(_v), "attributes") if (_value := data.get("droppedAttributesCount")) is not None: opentelemetry.proto_json._json_codec.validate_type(_value, builtins.int, "dropped_attributes_count") _args["dropped_attributes_count"] = _value if (_value := data.get("entityRefs")) is not None: _args["entity_refs"] = opentelemetry.proto_json._json_codec.decode_repeated(_value, lambda _v: opentelemetry.proto_json.common.v1.common.EntityRef.from_dict(_v), "entity_refs") return cls(**_args) python-opentelemetry-1.45.2/opentelemetry-proto-json/src/opentelemetry/proto_json/trace/000077500000000000000000000000001527070070700320045ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-proto-json/src/opentelemetry/proto_json/trace/__init__.py000066400000000000000000000000001527070070700341030ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-proto-json/src/opentelemetry/proto_json/trace/v1/000077500000000000000000000000001527070070700323325ustar00rootroot00000000000000__init__.py000066400000000000000000000000001527070070700343520ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-proto-json/src/opentelemetry/proto_json/trace/v1python-opentelemetry-1.45.2/opentelemetry-proto-json/src/opentelemetry/proto_json/trace/v1/trace.py000066400000000000000000000507001527070070700340040ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # AUTO-GENERATED from "opentelemetry/proto/trace/v1/trace.proto" # DO NOT EDIT MANUALLY from __future__ import annotations import builtins import dataclasses import enum import functools import typing _dataclass = functools.partial(dataclasses.dataclass, slots=True) import opentelemetry.proto_json._json_codec import opentelemetry.proto_json.common.v1.common import opentelemetry.proto_json.resource.v1.resource @typing.final class SpanFlags(enum.IntEnum): """ Generated from protobuf enum SpanFlags """ SPAN_FLAGS_DO_NOT_USE = 0 SPAN_FLAGS_TRACE_FLAGS_MASK = 255 SPAN_FLAGS_CONTEXT_HAS_IS_REMOTE_MASK = 256 SPAN_FLAGS_CONTEXT_IS_REMOTE_MASK = 512 @typing.final @_dataclass class TracesData(opentelemetry.proto_json._json_codec.JsonMessage): """ Generated from protobuf message TracesData """ resource_spans: builtins.list[ResourceSpans] = dataclasses.field(default_factory=builtins.list) def to_dict(self) -> builtins.dict[builtins.str, typing.Any]: """ Convert this message to a dictionary with lowerCamelCase keys. Returns: Dictionary representation following OTLP JSON encoding """ _result = {} if self.resource_spans: _result["resourceSpans"] = opentelemetry.proto_json._json_codec.encode_repeated(self.resource_spans, lambda _v: _v.to_dict()) return _result @builtins.classmethod def from_dict(cls, data: builtins.dict[builtins.str, typing.Any]) -> TracesData: """ Create from a dictionary with lowerCamelCase keys. Args: data: Dictionary representation following OTLP JSON encoding Returns: TracesData instance """ opentelemetry.proto_json._json_codec.validate_type(data, builtins.dict, "data") _args = {} if (_value := data.get("resourceSpans")) is not None: _args["resource_spans"] = opentelemetry.proto_json._json_codec.decode_repeated(_value, lambda _v: ResourceSpans.from_dict(_v), "resource_spans") return cls(**_args) @typing.final @_dataclass class ResourceSpans(opentelemetry.proto_json._json_codec.JsonMessage): """ Generated from protobuf message ResourceSpans """ resource: opentelemetry.proto_json.resource.v1.resource.Resource | None = None scope_spans: builtins.list[ScopeSpans] = dataclasses.field(default_factory=builtins.list) schema_url: builtins.str | None = "" def to_dict(self) -> builtins.dict[builtins.str, typing.Any]: """ Convert this message to a dictionary with lowerCamelCase keys. Returns: Dictionary representation following OTLP JSON encoding """ _result = {} if self.resource: _result["resource"] = self.resource.to_dict() if self.scope_spans: _result["scopeSpans"] = opentelemetry.proto_json._json_codec.encode_repeated(self.scope_spans, lambda _v: _v.to_dict()) if self.schema_url: _result["schemaUrl"] = self.schema_url return _result @builtins.classmethod def from_dict(cls, data: builtins.dict[builtins.str, typing.Any]) -> ResourceSpans: """ Create from a dictionary with lowerCamelCase keys. Args: data: Dictionary representation following OTLP JSON encoding Returns: ResourceSpans instance """ opentelemetry.proto_json._json_codec.validate_type(data, builtins.dict, "data") _args = {} if (_value := data.get("resource")) is not None: _args["resource"] = opentelemetry.proto_json.resource.v1.resource.Resource.from_dict(_value) if (_value := data.get("scopeSpans")) is not None: _args["scope_spans"] = opentelemetry.proto_json._json_codec.decode_repeated(_value, lambda _v: ScopeSpans.from_dict(_v), "scope_spans") if (_value := data.get("schemaUrl")) is not None: opentelemetry.proto_json._json_codec.validate_type(_value, builtins.str, "schema_url") _args["schema_url"] = _value return cls(**_args) @typing.final @_dataclass class ScopeSpans(opentelemetry.proto_json._json_codec.JsonMessage): """ Generated from protobuf message ScopeSpans """ scope: opentelemetry.proto_json.common.v1.common.InstrumentationScope | None = None spans: builtins.list[Span] = dataclasses.field(default_factory=builtins.list) schema_url: builtins.str | None = "" def to_dict(self) -> builtins.dict[builtins.str, typing.Any]: """ Convert this message to a dictionary with lowerCamelCase keys. Returns: Dictionary representation following OTLP JSON encoding """ _result = {} if self.scope: _result["scope"] = self.scope.to_dict() if self.spans: _result["spans"] = opentelemetry.proto_json._json_codec.encode_repeated(self.spans, lambda _v: _v.to_dict()) if self.schema_url: _result["schemaUrl"] = self.schema_url return _result @builtins.classmethod def from_dict(cls, data: builtins.dict[builtins.str, typing.Any]) -> ScopeSpans: """ Create from a dictionary with lowerCamelCase keys. Args: data: Dictionary representation following OTLP JSON encoding Returns: ScopeSpans instance """ opentelemetry.proto_json._json_codec.validate_type(data, builtins.dict, "data") _args = {} if (_value := data.get("scope")) is not None: _args["scope"] = opentelemetry.proto_json.common.v1.common.InstrumentationScope.from_dict(_value) if (_value := data.get("spans")) is not None: _args["spans"] = opentelemetry.proto_json._json_codec.decode_repeated(_value, lambda _v: Span.from_dict(_v), "spans") if (_value := data.get("schemaUrl")) is not None: opentelemetry.proto_json._json_codec.validate_type(_value, builtins.str, "schema_url") _args["schema_url"] = _value return cls(**_args) @typing.final @_dataclass class Span(opentelemetry.proto_json._json_codec.JsonMessage): """ Generated from protobuf message Span """ @typing.final class SpanKind(enum.IntEnum): """ Generated from protobuf enum SpanKind """ SPAN_KIND_UNSPECIFIED = 0 SPAN_KIND_INTERNAL = 1 SPAN_KIND_SERVER = 2 SPAN_KIND_CLIENT = 3 SPAN_KIND_PRODUCER = 4 SPAN_KIND_CONSUMER = 5 @typing.final @_dataclass class Event(opentelemetry.proto_json._json_codec.JsonMessage): """ Generated from protobuf message Event """ time_unix_nano: builtins.int | None = 0 name: builtins.str | None = "" attributes: builtins.list[opentelemetry.proto_json.common.v1.common.KeyValue] = dataclasses.field(default_factory=builtins.list) dropped_attributes_count: builtins.int | None = 0 def to_dict(self) -> builtins.dict[builtins.str, typing.Any]: """ Convert this message to a dictionary with lowerCamelCase keys. Returns: Dictionary representation following OTLP JSON encoding """ _result = {} if self.time_unix_nano: _result["timeUnixNano"] = opentelemetry.proto_json._json_codec.encode_int64(self.time_unix_nano) if self.name: _result["name"] = self.name if self.attributes: _result["attributes"] = opentelemetry.proto_json._json_codec.encode_repeated(self.attributes, lambda _v: _v.to_dict()) if self.dropped_attributes_count: _result["droppedAttributesCount"] = self.dropped_attributes_count return _result @builtins.classmethod def from_dict(cls, data: builtins.dict[builtins.str, typing.Any]) -> Span.Event: """ Create from a dictionary with lowerCamelCase keys. Args: data: Dictionary representation following OTLP JSON encoding Returns: Event instance """ opentelemetry.proto_json._json_codec.validate_type(data, builtins.dict, "data") _args = {} if (_value := data.get("timeUnixNano")) is not None: _args["time_unix_nano"] = opentelemetry.proto_json._json_codec.decode_int64(_value, "time_unix_nano") if (_value := data.get("name")) is not None: opentelemetry.proto_json._json_codec.validate_type(_value, builtins.str, "name") _args["name"] = _value if (_value := data.get("attributes")) is not None: _args["attributes"] = opentelemetry.proto_json._json_codec.decode_repeated(_value, lambda _v: opentelemetry.proto_json.common.v1.common.KeyValue.from_dict(_v), "attributes") if (_value := data.get("droppedAttributesCount")) is not None: opentelemetry.proto_json._json_codec.validate_type(_value, builtins.int, "dropped_attributes_count") _args["dropped_attributes_count"] = _value return cls(**_args) @typing.final @_dataclass class Link(opentelemetry.proto_json._json_codec.JsonMessage): """ Generated from protobuf message Link """ trace_id: builtins.bytes | None = b"" span_id: builtins.bytes | None = b"" trace_state: builtins.str | None = "" attributes: builtins.list[opentelemetry.proto_json.common.v1.common.KeyValue] = dataclasses.field(default_factory=builtins.list) dropped_attributes_count: builtins.int | None = 0 flags: builtins.int | None = 0 def to_dict(self) -> builtins.dict[builtins.str, typing.Any]: """ Convert this message to a dictionary with lowerCamelCase keys. Returns: Dictionary representation following OTLP JSON encoding """ _result = {} if self.trace_id: _result["traceId"] = opentelemetry.proto_json._json_codec.encode_hex(self.trace_id) if self.span_id: _result["spanId"] = opentelemetry.proto_json._json_codec.encode_hex(self.span_id) if self.trace_state: _result["traceState"] = self.trace_state if self.attributes: _result["attributes"] = opentelemetry.proto_json._json_codec.encode_repeated(self.attributes, lambda _v: _v.to_dict()) if self.dropped_attributes_count: _result["droppedAttributesCount"] = self.dropped_attributes_count if self.flags: _result["flags"] = self.flags return _result @builtins.classmethod def from_dict(cls, data: builtins.dict[builtins.str, typing.Any]) -> Span.Link: """ Create from a dictionary with lowerCamelCase keys. Args: data: Dictionary representation following OTLP JSON encoding Returns: Link instance """ opentelemetry.proto_json._json_codec.validate_type(data, builtins.dict, "data") _args = {} if (_value := data.get("traceId")) is not None: _args["trace_id"] = opentelemetry.proto_json._json_codec.decode_hex(_value, "trace_id") if (_value := data.get("spanId")) is not None: _args["span_id"] = opentelemetry.proto_json._json_codec.decode_hex(_value, "span_id") if (_value := data.get("traceState")) is not None: opentelemetry.proto_json._json_codec.validate_type(_value, builtins.str, "trace_state") _args["trace_state"] = _value if (_value := data.get("attributes")) is not None: _args["attributes"] = opentelemetry.proto_json._json_codec.decode_repeated(_value, lambda _v: opentelemetry.proto_json.common.v1.common.KeyValue.from_dict(_v), "attributes") if (_value := data.get("droppedAttributesCount")) is not None: opentelemetry.proto_json._json_codec.validate_type(_value, builtins.int, "dropped_attributes_count") _args["dropped_attributes_count"] = _value if (_value := data.get("flags")) is not None: opentelemetry.proto_json._json_codec.validate_type(_value, builtins.int, "flags") _args["flags"] = _value return cls(**_args) trace_id: builtins.bytes | None = b"" span_id: builtins.bytes | None = b"" trace_state: builtins.str | None = "" parent_span_id: builtins.bytes | None = b"" flags: builtins.int | None = 0 name: builtins.str | None = "" kind: Span.SpanKind | builtins.int | None = 0 start_time_unix_nano: builtins.int | None = 0 end_time_unix_nano: builtins.int | None = 0 attributes: builtins.list[opentelemetry.proto_json.common.v1.common.KeyValue] = dataclasses.field(default_factory=builtins.list) dropped_attributes_count: builtins.int | None = 0 events: builtins.list[Span.Event] = dataclasses.field(default_factory=builtins.list) dropped_events_count: builtins.int | None = 0 links: builtins.list[Span.Link] = dataclasses.field(default_factory=builtins.list) dropped_links_count: builtins.int | None = 0 status: Status | None = None def to_dict(self) -> builtins.dict[builtins.str, typing.Any]: """ Convert this message to a dictionary with lowerCamelCase keys. Returns: Dictionary representation following OTLP JSON encoding """ _result = {} if self.trace_id: _result["traceId"] = opentelemetry.proto_json._json_codec.encode_hex(self.trace_id) if self.span_id: _result["spanId"] = opentelemetry.proto_json._json_codec.encode_hex(self.span_id) if self.trace_state: _result["traceState"] = self.trace_state if self.parent_span_id: _result["parentSpanId"] = opentelemetry.proto_json._json_codec.encode_hex(self.parent_span_id) if self.flags: _result["flags"] = self.flags if self.name: _result["name"] = self.name if self.kind: _result["kind"] = builtins.int(self.kind) if self.start_time_unix_nano: _result["startTimeUnixNano"] = opentelemetry.proto_json._json_codec.encode_int64(self.start_time_unix_nano) if self.end_time_unix_nano: _result["endTimeUnixNano"] = opentelemetry.proto_json._json_codec.encode_int64(self.end_time_unix_nano) if self.attributes: _result["attributes"] = opentelemetry.proto_json._json_codec.encode_repeated(self.attributes, lambda _v: _v.to_dict()) if self.dropped_attributes_count: _result["droppedAttributesCount"] = self.dropped_attributes_count if self.events: _result["events"] = opentelemetry.proto_json._json_codec.encode_repeated(self.events, lambda _v: _v.to_dict()) if self.dropped_events_count: _result["droppedEventsCount"] = self.dropped_events_count if self.links: _result["links"] = opentelemetry.proto_json._json_codec.encode_repeated(self.links, lambda _v: _v.to_dict()) if self.dropped_links_count: _result["droppedLinksCount"] = self.dropped_links_count if self.status: _result["status"] = self.status.to_dict() return _result @builtins.classmethod def from_dict(cls, data: builtins.dict[builtins.str, typing.Any]) -> Span: """ Create from a dictionary with lowerCamelCase keys. Args: data: Dictionary representation following OTLP JSON encoding Returns: Span instance """ opentelemetry.proto_json._json_codec.validate_type(data, builtins.dict, "data") _args = {} if (_value := data.get("traceId")) is not None: _args["trace_id"] = opentelemetry.proto_json._json_codec.decode_hex(_value, "trace_id") if (_value := data.get("spanId")) is not None: _args["span_id"] = opentelemetry.proto_json._json_codec.decode_hex(_value, "span_id") if (_value := data.get("traceState")) is not None: opentelemetry.proto_json._json_codec.validate_type(_value, builtins.str, "trace_state") _args["trace_state"] = _value if (_value := data.get("parentSpanId")) is not None: _args["parent_span_id"] = opentelemetry.proto_json._json_codec.decode_hex(_value, "parent_span_id") if (_value := data.get("flags")) is not None: opentelemetry.proto_json._json_codec.validate_type(_value, builtins.int, "flags") _args["flags"] = _value if (_value := data.get("name")) is not None: opentelemetry.proto_json._json_codec.validate_type(_value, builtins.str, "name") _args["name"] = _value if (_value := data.get("kind")) is not None: opentelemetry.proto_json._json_codec.validate_type(_value, builtins.int, "kind") _args["kind"] = Span.SpanKind(_value) if (_value := data.get("startTimeUnixNano")) is not None: _args["start_time_unix_nano"] = opentelemetry.proto_json._json_codec.decode_int64(_value, "start_time_unix_nano") if (_value := data.get("endTimeUnixNano")) is not None: _args["end_time_unix_nano"] = opentelemetry.proto_json._json_codec.decode_int64(_value, "end_time_unix_nano") if (_value := data.get("attributes")) is not None: _args["attributes"] = opentelemetry.proto_json._json_codec.decode_repeated(_value, lambda _v: opentelemetry.proto_json.common.v1.common.KeyValue.from_dict(_v), "attributes") if (_value := data.get("droppedAttributesCount")) is not None: opentelemetry.proto_json._json_codec.validate_type(_value, builtins.int, "dropped_attributes_count") _args["dropped_attributes_count"] = _value if (_value := data.get("events")) is not None: _args["events"] = opentelemetry.proto_json._json_codec.decode_repeated(_value, lambda _v: Span.Event.from_dict(_v), "events") if (_value := data.get("droppedEventsCount")) is not None: opentelemetry.proto_json._json_codec.validate_type(_value, builtins.int, "dropped_events_count") _args["dropped_events_count"] = _value if (_value := data.get("links")) is not None: _args["links"] = opentelemetry.proto_json._json_codec.decode_repeated(_value, lambda _v: Span.Link.from_dict(_v), "links") if (_value := data.get("droppedLinksCount")) is not None: opentelemetry.proto_json._json_codec.validate_type(_value, builtins.int, "dropped_links_count") _args["dropped_links_count"] = _value if (_value := data.get("status")) is not None: _args["status"] = Status.from_dict(_value) return cls(**_args) @typing.final @_dataclass class Status(opentelemetry.proto_json._json_codec.JsonMessage): """ Generated from protobuf message Status """ @typing.final class StatusCode(enum.IntEnum): """ Generated from protobuf enum StatusCode """ STATUS_CODE_UNSET = 0 STATUS_CODE_OK = 1 STATUS_CODE_ERROR = 2 message: builtins.str | None = "" code: Status.StatusCode | builtins.int | None = 0 def to_dict(self) -> builtins.dict[builtins.str, typing.Any]: """ Convert this message to a dictionary with lowerCamelCase keys. Returns: Dictionary representation following OTLP JSON encoding """ _result = {} if self.message: _result["message"] = self.message if self.code: _result["code"] = builtins.int(self.code) return _result @builtins.classmethod def from_dict(cls, data: builtins.dict[builtins.str, typing.Any]) -> Status: """ Create from a dictionary with lowerCamelCase keys. Args: data: Dictionary representation following OTLP JSON encoding Returns: Status instance """ opentelemetry.proto_json._json_codec.validate_type(data, builtins.dict, "data") _args = {} if (_value := data.get("message")) is not None: opentelemetry.proto_json._json_codec.validate_type(_value, builtins.str, "message") _args["message"] = _value if (_value := data.get("code")) is not None: opentelemetry.proto_json._json_codec.validate_type(_value, builtins.int, "code") _args["code"] = Status.StatusCode(_value) return cls(**_args) python-opentelemetry-1.45.2/opentelemetry-proto-json/src/opentelemetry/proto_json/version/000077500000000000000000000000001527070070700323735ustar00rootroot00000000000000__init__.py000066400000000000000000000001441527070070700344240ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-proto-json/src/opentelemetry/proto_json/version# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 __version__ = "0.66b2" python-opentelemetry-1.45.2/opentelemetry-proto-json/test-requirements.in000066400000000000000000000001511527070070700270540ustar00rootroot00000000000000colorama>=0.4.6 iniconfig>=2.0.0 packaging>=24.0 pytest>=9.0.3 pluggy>=1.6.0 -e opentelemetry-proto-json python-opentelemetry-1.45.2/opentelemetry-proto-json/test-requirements.latest.txt000066400000000000000000000021201527070070700305560ustar00rootroot00000000000000# This file was autogenerated by uv via the following command: # uv pip compile --python-version=3.10 --universal --constraints=dev-requirements.txt opentelemetry-proto-json/test-requirements.in --output-file=opentelemetry-proto-json/test-requirements.latest.txt -e opentelemetry-proto-json # via -r opentelemetry-proto-json/test-requirements.in colorama==0.4.6 # via # -r opentelemetry-proto-json/test-requirements.in # pytest exceptiongroup==1.3.1 ; python_full_version < '3.11' # via pytest iniconfig==2.3.0 # via # -r opentelemetry-proto-json/test-requirements.in # pytest packaging==26.0 # via # -r opentelemetry-proto-json/test-requirements.in # pytest pluggy==1.6.0 # via # -r opentelemetry-proto-json/test-requirements.in # pytest pygments==2.20.0 # via pytest pytest==9.0.3 # via # -c dev-requirements.txt # -r opentelemetry-proto-json/test-requirements.in tomli==2.4.1 ; python_full_version < '3.11' # via pytest typing-extensions==4.15.0 ; python_full_version < '3.11' # via exceptiongroup python-opentelemetry-1.45.2/opentelemetry-proto-json/test-requirements.oldest.txt000066400000000000000000000020211527070070700305540ustar00rootroot00000000000000# This file was autogenerated by uv via the following command: # uv pip compile --python-version=3.10 --universal --resolution=lowest --constraints=dev-requirements.txt opentelemetry-proto-json/test-requirements.in --output-file=opentelemetry-proto-json/test-requirements.oldest.txt -e opentelemetry-proto-json # via -r opentelemetry-proto-json/test-requirements.in colorama==0.4.6 # via # -r opentelemetry-proto-json/test-requirements.in # pytest exceptiongroup==1.0.0 ; python_full_version < '3.11' # via pytest iniconfig==2.0.0 # via # -r opentelemetry-proto-json/test-requirements.in # pytest packaging==24.0 # via # -r opentelemetry-proto-json/test-requirements.in # pytest pluggy==1.6.0 # via # -r opentelemetry-proto-json/test-requirements.in # pytest pygments==2.7.2 # via pytest pytest==9.0.3 # via # -c dev-requirements.txt # -r opentelemetry-proto-json/test-requirements.in tomli==1.0.0 ; python_full_version < '3.11' # via pytest python-opentelemetry-1.45.2/opentelemetry-proto-json/tests/000077500000000000000000000000001527070070700241715ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-proto-json/tests/__init__.py000066400000000000000000000000001527070070700262700ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-proto-json/tests/test_proto.py000066400000000000000000000005271527070070700267510ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # type: ignore from importlib.util import find_spec from unittest import TestCase class TestInstrumentor(TestCase): def test_proto(self): if find_spec("opentelemetry.proto_json") is None: self.fail("opentelemetry-proto-json not installed") python-opentelemetry-1.45.2/opentelemetry-proto/000077500000000000000000000000001527070070700220605ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-proto/LICENSE000066400000000000000000000261351527070070700230740ustar00rootroot00000000000000 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. python-opentelemetry-1.45.2/opentelemetry-proto/README.rst000066400000000000000000000024411527070070700235500ustar00rootroot00000000000000OpenTelemetry Python Proto ========================== |pypi| .. |pypi| image:: https://badge.fury.io/py/opentelemetry-proto.svg :target: https://pypi.org/project/opentelemetry-proto/ This library contains the generated code for OpenTelemetry protobuf data model. The code in the current package was generated using the v1.10.0 release_ of opentelemetry-proto. .. _release: https://github.com/open-telemetry/opentelemetry-proto/releases/tag/v1.10.0 Installation ------------ :: pip install opentelemetry-proto Code Generation --------------- These files were generated automatically from code in opentelemetry-proto_. To regenerate the code, run ``../scripts/proto_codegen.sh``. To build against a new release or specific commit of opentelemetry-proto_, update the ``PROTO_REPO_BRANCH_OR_COMMIT`` variable in ``../scripts/proto_codegen.sh``. Then run the script and commit the changes as well as any fixes needed in the OTLP exporter. .. _opentelemetry-proto: https://github.com/open-telemetry/opentelemetry-proto References ---------- * `OpenTelemetry Project `_ * `OpenTelemetry Proto `_ * `proto_codegen.sh script `_ python-opentelemetry-1.45.2/opentelemetry-proto/pyproject.toml000066400000000000000000000023221527070070700247730ustar00rootroot00000000000000[build-system] requires = ["hatchling"] build-backend = "hatchling.build" [project] name = "opentelemetry-proto" dynamic = ["version"] description = "OpenTelemetry Python Proto" 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", "Framework :: OpenTelemetry", "Intended Audience :: Developers", "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.0, < 8.0", ] [project.urls] Homepage = "https://github.com/open-telemetry/opentelemetry-python/tree/main/opentelemetry-proto" Repository = "https://github.com/open-telemetry/opentelemetry-python" [tool.hatch.version] path = "src/opentelemetry/proto/version/__init__.py" [tool.hatch.build.targets.sdist] include = [ "/src", "/tests", ] [tool.hatch.build.targets.wheel] packages = ["src/opentelemetry"] python-opentelemetry-1.45.2/opentelemetry-proto/src/000077500000000000000000000000001527070070700226475ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-proto/src/opentelemetry/000077500000000000000000000000001527070070700255435ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-proto/src/opentelemetry/proto/000077500000000000000000000000001527070070700267065ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-proto/src/opentelemetry/proto/__init__.py000066400000000000000000000000001527070070700310050ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-proto/src/opentelemetry/proto/collector/000077500000000000000000000000001527070070700306745ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-proto/src/opentelemetry/proto/collector/__init__.py000066400000000000000000000000001527070070700327730ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-proto/src/opentelemetry/proto/collector/logs/000077500000000000000000000000001527070070700316405ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-proto/src/opentelemetry/proto/collector/logs/v1/000077500000000000000000000000001527070070700321665ustar00rootroot00000000000000logs_service_pb2.py000066400000000000000000000051311527070070700357100ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-proto/src/opentelemetry/proto/collector/logs/v1# -*- coding: utf-8 -*- # Generated by the protocol buffer compiler. DO NOT EDIT! # source: opentelemetry/proto/collector/logs/v1/logs_service.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 opentelemetry.proto.logs.v1 import logs_pb2 as opentelemetry_dot_proto_dot_logs_dot_v1_dot_logs__pb2 DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n8opentelemetry/proto/collector/logs/v1/logs_service.proto\x12%opentelemetry.proto.collector.logs.v1\x1a&opentelemetry/proto/logs/v1/logs.proto\"\\\n\x18\x45xportLogsServiceRequest\x12@\n\rresource_logs\x18\x01 \x03(\x0b\x32).opentelemetry.proto.logs.v1.ResourceLogs\"u\n\x19\x45xportLogsServiceResponse\x12X\n\x0fpartial_success\x18\x01 \x01(\x0b\x32?.opentelemetry.proto.collector.logs.v1.ExportLogsPartialSuccess\"O\n\x18\x45xportLogsPartialSuccess\x12\x1c\n\x14rejected_log_records\x18\x01 \x01(\x03\x12\x15\n\rerror_message\x18\x02 \x01(\t2\x9d\x01\n\x0bLogsService\x12\x8d\x01\n\x06\x45xport\x12?.opentelemetry.proto.collector.logs.v1.ExportLogsServiceRequest\x1a@.opentelemetry.proto.collector.logs.v1.ExportLogsServiceResponse\"\x00\x42\x98\x01\n(io.opentelemetry.proto.collector.logs.v1B\x10LogsServiceProtoP\x01Z0go.opentelemetry.io/proto/otlp/collector/logs/v1\xaa\x02%OpenTelemetry.Proto.Collector.Logs.V1b\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'opentelemetry.proto.collector.logs.v1.logs_service_pb2', _globals) if not _descriptor._USE_C_DESCRIPTORS: _globals['DESCRIPTOR']._loaded_options = None _globals['DESCRIPTOR']._serialized_options = b'\n(io.opentelemetry.proto.collector.logs.v1B\020LogsServiceProtoP\001Z0go.opentelemetry.io/proto/otlp/collector/logs/v1\252\002%OpenTelemetry.Proto.Collector.Logs.V1' _globals['_EXPORTLOGSSERVICEREQUEST']._serialized_start=139 _globals['_EXPORTLOGSSERVICEREQUEST']._serialized_end=231 _globals['_EXPORTLOGSSERVICERESPONSE']._serialized_start=233 _globals['_EXPORTLOGSSERVICERESPONSE']._serialized_end=350 _globals['_EXPORTLOGSPARTIALSUCCESS']._serialized_start=352 _globals['_EXPORTLOGSPARTIALSUCCESS']._serialized_end=431 _globals['_LOGSSERVICE']._serialized_start=434 _globals['_LOGSSERVICE']._serialized_end=591 # @@protoc_insertion_point(module_scope) logs_service_pb2.pyi000066400000000000000000000115161527070070700360650ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-proto/src/opentelemetry/proto/collector/logs/v1""" @generated by mypy-protobuf. Do not edit manually! isort:skip_file Copyright 2020, 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. """ import builtins import collections.abc import google.protobuf.descriptor import google.protobuf.internal.containers import google.protobuf.message import opentelemetry.proto.logs.v1.logs_pb2 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 ExportLogsServiceRequest(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor RESOURCE_LOGS_FIELD_NUMBER: builtins.int @property def resource_logs(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[opentelemetry.proto.logs.v1.logs_pb2.ResourceLogs]: """An array of ResourceLogs. For data coming from a single resource this array will typically contain one element. Intermediary nodes (such as OpenTelemetry Collector) that receive data from multiple origins typically batch the data before forwarding further and in that case this array will contain multiple elements. """ def __init__( self, *, resource_logs: collections.abc.Iterable[opentelemetry.proto.logs.v1.logs_pb2.ResourceLogs] | None = ..., ) -> None: ... def ClearField(self, field_name: typing_extensions.Literal["resource_logs", b"resource_logs"]) -> None: ... global___ExportLogsServiceRequest = ExportLogsServiceRequest @typing_extensions.final class ExportLogsServiceResponse(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor PARTIAL_SUCCESS_FIELD_NUMBER: builtins.int @property def partial_success(self) -> global___ExportLogsPartialSuccess: """The details of a partially successful export request. If the request is only partially accepted (i.e. when the server accepts only parts of the data and rejects the rest) the server MUST initialize the `partial_success` field and MUST set the `rejected_` with the number of items it rejected. Servers MAY also make use of the `partial_success` field to convey warnings/suggestions to senders even when the request was fully accepted. In such cases, the `rejected_` MUST have a value of `0` and the `error_message` MUST be non-empty. A `partial_success` message with an empty value (rejected_ = 0 and `error_message` = "") is equivalent to it not being set/present. Senders SHOULD interpret it the same way as in the full success case. """ def __init__( self, *, partial_success: global___ExportLogsPartialSuccess | None = ..., ) -> None: ... def HasField(self, field_name: typing_extensions.Literal["partial_success", b"partial_success"]) -> builtins.bool: ... def ClearField(self, field_name: typing_extensions.Literal["partial_success", b"partial_success"]) -> None: ... global___ExportLogsServiceResponse = ExportLogsServiceResponse @typing_extensions.final class ExportLogsPartialSuccess(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor REJECTED_LOG_RECORDS_FIELD_NUMBER: builtins.int ERROR_MESSAGE_FIELD_NUMBER: builtins.int rejected_log_records: builtins.int """The number of rejected log records. A `rejected_` field holding a `0` value indicates that the request was fully accepted. """ error_message: builtins.str """A developer-facing human-readable message in English. It should be used either to explain why the server rejected parts of the data during a partial success or to convey warnings/suggestions during a full success. The message should offer guidance on how users can address such issues. error_message is an optional field. An error_message with an empty value is equivalent to it not being set. """ def __init__( self, *, rejected_log_records: builtins.int = ..., error_message: builtins.str = ..., ) -> None: ... def ClearField(self, field_name: typing_extensions.Literal["error_message", b"error_message", "rejected_log_records", b"rejected_log_records"]) -> None: ... global___ExportLogsPartialSuccess = ExportLogsPartialSuccess logs_service_pb2_grpc.py000066400000000000000000000112521527070070700367240ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-proto/src/opentelemetry/proto/collector/logs/v1# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! """Client and server classes corresponding to protobuf-defined services.""" import grpc import warnings from opentelemetry.proto.collector.logs.v1 import logs_service_pb2 as opentelemetry_dot_proto_dot_collector_dot_logs_dot_v1_dot_logs__service__pb2 GRPC_GENERATED_VERSION = '1.63.2' GRPC_VERSION = grpc.__version__ EXPECTED_ERROR_RELEASE = '1.65.0' SCHEDULED_RELEASE_DATE = 'June 25, 2024' _version_not_supported = False try: from grpc._utilities import first_version_is_lower _version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION) except ImportError: _version_not_supported = True if _version_not_supported: warnings.warn( f'The grpc package installed is at version {GRPC_VERSION},' + f' but the generated code in opentelemetry/proto/collector/logs/v1/logs_service_pb2_grpc.py depends on' + f' grpcio>={GRPC_GENERATED_VERSION}.' + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}' + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.' + f' This warning will become an error in {EXPECTED_ERROR_RELEASE},' + f' scheduled for release on {SCHEDULED_RELEASE_DATE}.', RuntimeWarning ) class LogsServiceStub(object): """Service that can be used to push logs between one Application instrumented with OpenTelemetry and an collector, or between an collector and a central collector (in this case logs are sent/received to/from multiple Applications). """ def __init__(self, channel): """Constructor. Args: channel: A grpc.Channel. """ self.Export = channel.unary_unary( '/opentelemetry.proto.collector.logs.v1.LogsService/Export', request_serializer=opentelemetry_dot_proto_dot_collector_dot_logs_dot_v1_dot_logs__service__pb2.ExportLogsServiceRequest.SerializeToString, response_deserializer=opentelemetry_dot_proto_dot_collector_dot_logs_dot_v1_dot_logs__service__pb2.ExportLogsServiceResponse.FromString, _registered_method=True) class LogsServiceServicer(object): """Service that can be used to push logs between one Application instrumented with OpenTelemetry and an collector, or between an collector and a central collector (in this case logs are sent/received to/from multiple Applications). """ def Export(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 add_LogsServiceServicer_to_server(servicer, server): rpc_method_handlers = { 'Export': grpc.unary_unary_rpc_method_handler( servicer.Export, request_deserializer=opentelemetry_dot_proto_dot_collector_dot_logs_dot_v1_dot_logs__service__pb2.ExportLogsServiceRequest.FromString, response_serializer=opentelemetry_dot_proto_dot_collector_dot_logs_dot_v1_dot_logs__service__pb2.ExportLogsServiceResponse.SerializeToString, ), } generic_handler = grpc.method_handlers_generic_handler( 'opentelemetry.proto.collector.logs.v1.LogsService', rpc_method_handlers) server.add_generic_rpc_handlers((generic_handler,)) # This class is part of an EXPERIMENTAL API. class LogsService(object): """Service that can be used to push logs between one Application instrumented with OpenTelemetry and an collector, or between an collector and a central collector (in this case logs are sent/received to/from multiple Applications). """ @staticmethod def Export(request, target, options=(), channel_credentials=None, call_credentials=None, insecure=False, compression=None, wait_for_ready=None, timeout=None, metadata=None): return grpc.experimental.unary_unary( request, target, '/opentelemetry.proto.collector.logs.v1.LogsService/Export', opentelemetry_dot_proto_dot_collector_dot_logs_dot_v1_dot_logs__service__pb2.ExportLogsServiceRequest.SerializeToString, opentelemetry_dot_proto_dot_collector_dot_logs_dot_v1_dot_logs__service__pb2.ExportLogsServiceResponse.FromString, options, channel_credentials, insecure, call_credentials, compression, wait_for_ready, timeout, metadata, _registered_method=True) python-opentelemetry-1.45.2/opentelemetry-proto/src/opentelemetry/proto/collector/metrics/000077500000000000000000000000001527070070700323425ustar00rootroot00000000000000__init__.py000066400000000000000000000000001527070070700343620ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-proto/src/opentelemetry/proto/collector/metricspython-opentelemetry-1.45.2/opentelemetry-proto/src/opentelemetry/proto/collector/metrics/v1/000077500000000000000000000000001527070070700326705ustar00rootroot00000000000000__init__.py000066400000000000000000000000001527070070700347100ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-proto/src/opentelemetry/proto/collector/metrics/v1metrics_service_pb2.py000066400000000000000000000053411527070070700371170ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-proto/src/opentelemetry/proto/collector/metrics/v1# -*- coding: utf-8 -*- # Generated by the protocol buffer compiler. DO NOT EDIT! # source: opentelemetry/proto/collector/metrics/v1/metrics_service.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 opentelemetry.proto.metrics.v1 import metrics_pb2 as opentelemetry_dot_proto_dot_metrics_dot_v1_dot_metrics__pb2 DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n>opentelemetry/proto/collector/metrics/v1/metrics_service.proto\x12(opentelemetry.proto.collector.metrics.v1\x1a,opentelemetry/proto/metrics/v1/metrics.proto\"h\n\x1b\x45xportMetricsServiceRequest\x12I\n\x10resource_metrics\x18\x01 \x03(\x0b\x32/.opentelemetry.proto.metrics.v1.ResourceMetrics\"~\n\x1c\x45xportMetricsServiceResponse\x12^\n\x0fpartial_success\x18\x01 \x01(\x0b\x32\x45.opentelemetry.proto.collector.metrics.v1.ExportMetricsPartialSuccess\"R\n\x1b\x45xportMetricsPartialSuccess\x12\x1c\n\x14rejected_data_points\x18\x01 \x01(\x03\x12\x15\n\rerror_message\x18\x02 \x01(\t2\xac\x01\n\x0eMetricsService\x12\x99\x01\n\x06\x45xport\x12\x45.opentelemetry.proto.collector.metrics.v1.ExportMetricsServiceRequest\x1a\x46.opentelemetry.proto.collector.metrics.v1.ExportMetricsServiceResponse\"\x00\x42\xa4\x01\n+io.opentelemetry.proto.collector.metrics.v1B\x13MetricsServiceProtoP\x01Z3go.opentelemetry.io/proto/otlp/collector/metrics/v1\xaa\x02(OpenTelemetry.Proto.Collector.Metrics.V1b\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'opentelemetry.proto.collector.metrics.v1.metrics_service_pb2', _globals) if not _descriptor._USE_C_DESCRIPTORS: _globals['DESCRIPTOR']._loaded_options = None _globals['DESCRIPTOR']._serialized_options = b'\n+io.opentelemetry.proto.collector.metrics.v1B\023MetricsServiceProtoP\001Z3go.opentelemetry.io/proto/otlp/collector/metrics/v1\252\002(OpenTelemetry.Proto.Collector.Metrics.V1' _globals['_EXPORTMETRICSSERVICEREQUEST']._serialized_start=154 _globals['_EXPORTMETRICSSERVICEREQUEST']._serialized_end=258 _globals['_EXPORTMETRICSSERVICERESPONSE']._serialized_start=260 _globals['_EXPORTMETRICSSERVICERESPONSE']._serialized_end=386 _globals['_EXPORTMETRICSPARTIALSUCCESS']._serialized_start=388 _globals['_EXPORTMETRICSPARTIALSUCCESS']._serialized_end=470 _globals['_METRICSSERVICE']._serialized_start=473 _globals['_METRICSSERVICE']._serialized_end=645 # @@protoc_insertion_point(module_scope) metrics_service_pb2.pyi000066400000000000000000000116311527070070700372670ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-proto/src/opentelemetry/proto/collector/metrics/v1""" @generated by mypy-protobuf. Do not edit manually! isort:skip_file Copyright 2019, 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. """ import builtins import collections.abc import google.protobuf.descriptor import google.protobuf.internal.containers import google.protobuf.message import opentelemetry.proto.metrics.v1.metrics_pb2 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 ExportMetricsServiceRequest(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor RESOURCE_METRICS_FIELD_NUMBER: builtins.int @property def resource_metrics(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[opentelemetry.proto.metrics.v1.metrics_pb2.ResourceMetrics]: """An array of ResourceMetrics. For data coming from a single resource this array will typically contain one element. Intermediary nodes (such as OpenTelemetry Collector) that receive data from multiple origins typically batch the data before forwarding further and in that case this array will contain multiple elements. """ def __init__( self, *, resource_metrics: collections.abc.Iterable[opentelemetry.proto.metrics.v1.metrics_pb2.ResourceMetrics] | None = ..., ) -> None: ... def ClearField(self, field_name: typing_extensions.Literal["resource_metrics", b"resource_metrics"]) -> None: ... global___ExportMetricsServiceRequest = ExportMetricsServiceRequest @typing_extensions.final class ExportMetricsServiceResponse(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor PARTIAL_SUCCESS_FIELD_NUMBER: builtins.int @property def partial_success(self) -> global___ExportMetricsPartialSuccess: """The details of a partially successful export request. If the request is only partially accepted (i.e. when the server accepts only parts of the data and rejects the rest) the server MUST initialize the `partial_success` field and MUST set the `rejected_` with the number of items it rejected. Servers MAY also make use of the `partial_success` field to convey warnings/suggestions to senders even when the request was fully accepted. In such cases, the `rejected_` MUST have a value of `0` and the `error_message` MUST be non-empty. A `partial_success` message with an empty value (rejected_ = 0 and `error_message` = "") is equivalent to it not being set/present. Senders SHOULD interpret it the same way as in the full success case. """ def __init__( self, *, partial_success: global___ExportMetricsPartialSuccess | None = ..., ) -> None: ... def HasField(self, field_name: typing_extensions.Literal["partial_success", b"partial_success"]) -> builtins.bool: ... def ClearField(self, field_name: typing_extensions.Literal["partial_success", b"partial_success"]) -> None: ... global___ExportMetricsServiceResponse = ExportMetricsServiceResponse @typing_extensions.final class ExportMetricsPartialSuccess(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor REJECTED_DATA_POINTS_FIELD_NUMBER: builtins.int ERROR_MESSAGE_FIELD_NUMBER: builtins.int rejected_data_points: builtins.int """The number of rejected data points. A `rejected_` field holding a `0` value indicates that the request was fully accepted. """ error_message: builtins.str """A developer-facing human-readable message in English. It should be used either to explain why the server rejected parts of the data during a partial success or to convey warnings/suggestions during a full success. The message should offer guidance on how users can address such issues. error_message is an optional field. An error_message with an empty value is equivalent to it not being set. """ def __init__( self, *, rejected_data_points: builtins.int = ..., error_message: builtins.str = ..., ) -> None: ... def ClearField(self, field_name: typing_extensions.Literal["error_message", b"error_message", "rejected_data_points", b"rejected_data_points"]) -> None: ... global___ExportMetricsPartialSuccess = ExportMetricsPartialSuccess metrics_service_pb2_grpc.py000066400000000000000000000111071527070070700401270ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-proto/src/opentelemetry/proto/collector/metrics/v1# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! """Client and server classes corresponding to protobuf-defined services.""" import grpc import warnings from opentelemetry.proto.collector.metrics.v1 import metrics_service_pb2 as opentelemetry_dot_proto_dot_collector_dot_metrics_dot_v1_dot_metrics__service__pb2 GRPC_GENERATED_VERSION = '1.63.2' GRPC_VERSION = grpc.__version__ EXPECTED_ERROR_RELEASE = '1.65.0' SCHEDULED_RELEASE_DATE = 'June 25, 2024' _version_not_supported = False try: from grpc._utilities import first_version_is_lower _version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION) except ImportError: _version_not_supported = True if _version_not_supported: warnings.warn( f'The grpc package installed is at version {GRPC_VERSION},' + f' but the generated code in opentelemetry/proto/collector/metrics/v1/metrics_service_pb2_grpc.py depends on' + f' grpcio>={GRPC_GENERATED_VERSION}.' + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}' + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.' + f' This warning will become an error in {EXPECTED_ERROR_RELEASE},' + f' scheduled for release on {SCHEDULED_RELEASE_DATE}.', RuntimeWarning ) class MetricsServiceStub(object): """Service that can be used to push metrics between one Application instrumented with OpenTelemetry and a collector, or between a collector and a central collector. """ def __init__(self, channel): """Constructor. Args: channel: A grpc.Channel. """ self.Export = channel.unary_unary( '/opentelemetry.proto.collector.metrics.v1.MetricsService/Export', request_serializer=opentelemetry_dot_proto_dot_collector_dot_metrics_dot_v1_dot_metrics__service__pb2.ExportMetricsServiceRequest.SerializeToString, response_deserializer=opentelemetry_dot_proto_dot_collector_dot_metrics_dot_v1_dot_metrics__service__pb2.ExportMetricsServiceResponse.FromString, _registered_method=True) class MetricsServiceServicer(object): """Service that can be used to push metrics between one Application instrumented with OpenTelemetry and a collector, or between a collector and a central collector. """ def Export(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 add_MetricsServiceServicer_to_server(servicer, server): rpc_method_handlers = { 'Export': grpc.unary_unary_rpc_method_handler( servicer.Export, request_deserializer=opentelemetry_dot_proto_dot_collector_dot_metrics_dot_v1_dot_metrics__service__pb2.ExportMetricsServiceRequest.FromString, response_serializer=opentelemetry_dot_proto_dot_collector_dot_metrics_dot_v1_dot_metrics__service__pb2.ExportMetricsServiceResponse.SerializeToString, ), } generic_handler = grpc.method_handlers_generic_handler( 'opentelemetry.proto.collector.metrics.v1.MetricsService', rpc_method_handlers) server.add_generic_rpc_handlers((generic_handler,)) # This class is part of an EXPERIMENTAL API. class MetricsService(object): """Service that can be used to push metrics between one Application instrumented with OpenTelemetry and a collector, or between a collector and a central collector. """ @staticmethod def Export(request, target, options=(), channel_credentials=None, call_credentials=None, insecure=False, compression=None, wait_for_ready=None, timeout=None, metadata=None): return grpc.experimental.unary_unary( request, target, '/opentelemetry.proto.collector.metrics.v1.MetricsService/Export', opentelemetry_dot_proto_dot_collector_dot_metrics_dot_v1_dot_metrics__service__pb2.ExportMetricsServiceRequest.SerializeToString, opentelemetry_dot_proto_dot_collector_dot_metrics_dot_v1_dot_metrics__service__pb2.ExportMetricsServiceResponse.FromString, options, channel_credentials, insecure, call_credentials, compression, wait_for_ready, timeout, metadata, _registered_method=True) python-opentelemetry-1.45.2/opentelemetry-proto/src/opentelemetry/proto/collector/profiles/000077500000000000000000000000001527070070700325175ustar00rootroot00000000000000v1development/000077500000000000000000000000001527070070700352315ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-proto/src/opentelemetry/proto/collector/profilesprofiles_service_pb2.py000066400000000000000000000060711527070070700417150ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-proto/src/opentelemetry/proto/collector/profiles/v1development# -*- coding: utf-8 -*- # Generated by the protocol buffer compiler. DO NOT EDIT! # source: opentelemetry/proto/collector/profiles/v1development/profiles_service.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 opentelemetry.proto.profiles.v1development import profiles_pb2 as opentelemetry_dot_proto_dot_profiles_dot_v1development_dot_profiles__pb2 DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\nKopentelemetry/proto/collector/profiles/v1development/profiles_service.proto\x12\x34opentelemetry.proto.collector.profiles.v1development\x1a\x39opentelemetry/proto/profiles/v1development/profiles.proto\"\xcb\x01\n\x1c\x45xportProfilesServiceRequest\x12W\n\x11resource_profiles\x18\x01 \x03(\x0b\x32<.opentelemetry.proto.profiles.v1development.ResourceProfiles\x12R\n\ndictionary\x18\x02 \x01(\x0b\x32>.opentelemetry.proto.profiles.v1development.ProfilesDictionary\"\x8c\x01\n\x1d\x45xportProfilesServiceResponse\x12k\n\x0fpartial_success\x18\x01 \x01(\x0b\x32R.opentelemetry.proto.collector.profiles.v1development.ExportProfilesPartialSuccess\"P\n\x1c\x45xportProfilesPartialSuccess\x12\x19\n\x11rejected_profiles\x18\x01 \x01(\x03\x12\x15\n\rerror_message\x18\x02 \x01(\t2\xc7\x01\n\x0fProfilesService\x12\xb3\x01\n\x06\x45xport\x12R.opentelemetry.proto.collector.profiles.v1development.ExportProfilesServiceRequest\x1aS.opentelemetry.proto.collector.profiles.v1development.ExportProfilesServiceResponse\"\x00\x42\xc9\x01\n7io.opentelemetry.proto.collector.profiles.v1developmentB\x14ProfilesServiceProtoP\x01Z?go.opentelemetry.io/proto/otlp/collector/profiles/v1development\xaa\x02\x34OpenTelemetry.Proto.Collector.Profiles.V1Developmentb\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'opentelemetry.proto.collector.profiles.v1development.profiles_service_pb2', _globals) if not _descriptor._USE_C_DESCRIPTORS: _globals['DESCRIPTOR']._loaded_options = None _globals['DESCRIPTOR']._serialized_options = b'\n7io.opentelemetry.proto.collector.profiles.v1developmentB\024ProfilesServiceProtoP\001Z?go.opentelemetry.io/proto/otlp/collector/profiles/v1development\252\0024OpenTelemetry.Proto.Collector.Profiles.V1Development' _globals['_EXPORTPROFILESSERVICEREQUEST']._serialized_start=193 _globals['_EXPORTPROFILESSERVICEREQUEST']._serialized_end=396 _globals['_EXPORTPROFILESSERVICERESPONSE']._serialized_start=399 _globals['_EXPORTPROFILESSERVICERESPONSE']._serialized_end=539 _globals['_EXPORTPROFILESPARTIALSUCCESS']._serialized_start=541 _globals['_EXPORTPROFILESPARTIALSUCCESS']._serialized_end=621 _globals['_PROFILESSERVICE']._serialized_start=624 _globals['_PROFILESSERVICE']._serialized_end=823 # @@protoc_insertion_point(module_scope) profiles_service_pb2.pyi000066400000000000000000000127041527070070700420660ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-proto/src/opentelemetry/proto/collector/profiles/v1development""" @generated by mypy-protobuf. Do not edit manually! isort:skip_file Copyright 2023, 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. """ import builtins import collections.abc import google.protobuf.descriptor import google.protobuf.internal.containers import google.protobuf.message import opentelemetry.proto.profiles.v1development.profiles_pb2 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 ExportProfilesServiceRequest(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor RESOURCE_PROFILES_FIELD_NUMBER: builtins.int DICTIONARY_FIELD_NUMBER: builtins.int @property def resource_profiles(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[opentelemetry.proto.profiles.v1development.profiles_pb2.ResourceProfiles]: """An array of ResourceProfiles. For data coming from a single resource this array will typically contain one element. Intermediary nodes (such as OpenTelemetry Collector) that receive data from multiple origins typically batch the data before forwarding further and in that case this array will contain multiple elements. """ @property def dictionary(self) -> opentelemetry.proto.profiles.v1development.profiles_pb2.ProfilesDictionary: """The reference table containing all data shared by profiles across the message being sent.""" def __init__( self, *, resource_profiles: collections.abc.Iterable[opentelemetry.proto.profiles.v1development.profiles_pb2.ResourceProfiles] | None = ..., dictionary: opentelemetry.proto.profiles.v1development.profiles_pb2.ProfilesDictionary | None = ..., ) -> None: ... def HasField(self, field_name: typing_extensions.Literal["dictionary", b"dictionary"]) -> builtins.bool: ... def ClearField(self, field_name: typing_extensions.Literal["dictionary", b"dictionary", "resource_profiles", b"resource_profiles"]) -> None: ... global___ExportProfilesServiceRequest = ExportProfilesServiceRequest @typing_extensions.final class ExportProfilesServiceResponse(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor PARTIAL_SUCCESS_FIELD_NUMBER: builtins.int @property def partial_success(self) -> global___ExportProfilesPartialSuccess: """The details of a partially successful export request. If the request is only partially accepted (i.e. when the server accepts only parts of the data and rejects the rest) the server MUST initialize the `partial_success` field and MUST set the `rejected_` with the number of items it rejected. Servers MAY also make use of the `partial_success` field to convey warnings/suggestions to senders even when the request was fully accepted. In such cases, the `rejected_` MUST have a value of `0` and the `error_message` MUST be non-empty. A `partial_success` message with an empty value (rejected_ = 0 and `error_message` = "") is equivalent to it not being set/present. Senders SHOULD interpret it the same way as in the full success case. """ def __init__( self, *, partial_success: global___ExportProfilesPartialSuccess | None = ..., ) -> None: ... def HasField(self, field_name: typing_extensions.Literal["partial_success", b"partial_success"]) -> builtins.bool: ... def ClearField(self, field_name: typing_extensions.Literal["partial_success", b"partial_success"]) -> None: ... global___ExportProfilesServiceResponse = ExportProfilesServiceResponse @typing_extensions.final class ExportProfilesPartialSuccess(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor REJECTED_PROFILES_FIELD_NUMBER: builtins.int ERROR_MESSAGE_FIELD_NUMBER: builtins.int rejected_profiles: builtins.int """The number of rejected profiles. A `rejected_` field holding a `0` value indicates that the request was fully accepted. """ error_message: builtins.str """A developer-facing human-readable message in English. It should be used either to explain why the server rejected parts of the data during a partial success or to convey warnings/suggestions during a full success. The message should offer guidance on how users can address such issues. error_message is an optional field. An error_message with an empty value is equivalent to it not being set. """ def __init__( self, *, rejected_profiles: builtins.int = ..., error_message: builtins.str = ..., ) -> None: ... def ClearField(self, field_name: typing_extensions.Literal["error_message", b"error_message", "rejected_profiles", b"rejected_profiles"]) -> None: ... global___ExportProfilesPartialSuccess = ExportProfilesPartialSuccess profiles_service_pb2_grpc.py000066400000000000000000000113441527070070700427270ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-proto/src/opentelemetry/proto/collector/profiles/v1development# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! """Client and server classes corresponding to protobuf-defined services.""" import grpc import warnings from opentelemetry.proto.collector.profiles.v1development import profiles_service_pb2 as opentelemetry_dot_proto_dot_collector_dot_profiles_dot_v1development_dot_profiles__service__pb2 GRPC_GENERATED_VERSION = '1.63.2' GRPC_VERSION = grpc.__version__ EXPECTED_ERROR_RELEASE = '1.65.0' SCHEDULED_RELEASE_DATE = 'June 25, 2024' _version_not_supported = False try: from grpc._utilities import first_version_is_lower _version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION) except ImportError: _version_not_supported = True if _version_not_supported: warnings.warn( f'The grpc package installed is at version {GRPC_VERSION},' + f' but the generated code in opentelemetry/proto/collector/profiles/v1development/profiles_service_pb2_grpc.py depends on' + f' grpcio>={GRPC_GENERATED_VERSION}.' + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}' + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.' + f' This warning will become an error in {EXPECTED_ERROR_RELEASE},' + f' scheduled for release on {SCHEDULED_RELEASE_DATE}.', RuntimeWarning ) class ProfilesServiceStub(object): """Service that can be used to push profiles between one Application instrumented with OpenTelemetry and a collector, or between a collector and a central collector. """ def __init__(self, channel): """Constructor. Args: channel: A grpc.Channel. """ self.Export = channel.unary_unary( '/opentelemetry.proto.collector.profiles.v1development.ProfilesService/Export', request_serializer=opentelemetry_dot_proto_dot_collector_dot_profiles_dot_v1development_dot_profiles__service__pb2.ExportProfilesServiceRequest.SerializeToString, response_deserializer=opentelemetry_dot_proto_dot_collector_dot_profiles_dot_v1development_dot_profiles__service__pb2.ExportProfilesServiceResponse.FromString, _registered_method=True) class ProfilesServiceServicer(object): """Service that can be used to push profiles between one Application instrumented with OpenTelemetry and a collector, or between a collector and a central collector. """ def Export(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 add_ProfilesServiceServicer_to_server(servicer, server): rpc_method_handlers = { 'Export': grpc.unary_unary_rpc_method_handler( servicer.Export, request_deserializer=opentelemetry_dot_proto_dot_collector_dot_profiles_dot_v1development_dot_profiles__service__pb2.ExportProfilesServiceRequest.FromString, response_serializer=opentelemetry_dot_proto_dot_collector_dot_profiles_dot_v1development_dot_profiles__service__pb2.ExportProfilesServiceResponse.SerializeToString, ), } generic_handler = grpc.method_handlers_generic_handler( 'opentelemetry.proto.collector.profiles.v1development.ProfilesService', rpc_method_handlers) server.add_generic_rpc_handlers((generic_handler,)) # This class is part of an EXPERIMENTAL API. class ProfilesService(object): """Service that can be used to push profiles between one Application instrumented with OpenTelemetry and a collector, or between a collector and a central collector. """ @staticmethod def Export(request, target, options=(), channel_credentials=None, call_credentials=None, insecure=False, compression=None, wait_for_ready=None, timeout=None, metadata=None): return grpc.experimental.unary_unary( request, target, '/opentelemetry.proto.collector.profiles.v1development.ProfilesService/Export', opentelemetry_dot_proto_dot_collector_dot_profiles_dot_v1development_dot_profiles__service__pb2.ExportProfilesServiceRequest.SerializeToString, opentelemetry_dot_proto_dot_collector_dot_profiles_dot_v1development_dot_profiles__service__pb2.ExportProfilesServiceResponse.FromString, options, channel_credentials, insecure, call_credentials, compression, wait_for_ready, timeout, metadata, _registered_method=True) python-opentelemetry-1.45.2/opentelemetry-proto/src/opentelemetry/proto/collector/trace/000077500000000000000000000000001527070070700317725ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-proto/src/opentelemetry/proto/collector/trace/__init__.py000066400000000000000000000000001527070070700340710ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-proto/src/opentelemetry/proto/collector/trace/v1/000077500000000000000000000000001527070070700323205ustar00rootroot00000000000000__init__.py000066400000000000000000000000001527070070700343400ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-proto/src/opentelemetry/proto/collector/trace/v1trace_service_pb2.py000066400000000000000000000052121527070070700361740ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-proto/src/opentelemetry/proto/collector/trace/v1# -*- coding: utf-8 -*- # Generated by the protocol buffer compiler. DO NOT EDIT! # source: opentelemetry/proto/collector/trace/v1/trace_service.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 opentelemetry.proto.trace.v1 import trace_pb2 as opentelemetry_dot_proto_dot_trace_dot_v1_dot_trace__pb2 DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n:opentelemetry/proto/collector/trace/v1/trace_service.proto\x12&opentelemetry.proto.collector.trace.v1\x1a(opentelemetry/proto/trace/v1/trace.proto\"`\n\x19\x45xportTraceServiceRequest\x12\x43\n\x0eresource_spans\x18\x01 \x03(\x0b\x32+.opentelemetry.proto.trace.v1.ResourceSpans\"x\n\x1a\x45xportTraceServiceResponse\x12Z\n\x0fpartial_success\x18\x01 \x01(\x0b\x32\x41.opentelemetry.proto.collector.trace.v1.ExportTracePartialSuccess\"J\n\x19\x45xportTracePartialSuccess\x12\x16\n\x0erejected_spans\x18\x01 \x01(\x03\x12\x15\n\rerror_message\x18\x02 \x01(\t2\xa2\x01\n\x0cTraceService\x12\x91\x01\n\x06\x45xport\x12\x41.opentelemetry.proto.collector.trace.v1.ExportTraceServiceRequest\x1a\x42.opentelemetry.proto.collector.trace.v1.ExportTraceServiceResponse\"\x00\x42\x9c\x01\n)io.opentelemetry.proto.collector.trace.v1B\x11TraceServiceProtoP\x01Z1go.opentelemetry.io/proto/otlp/collector/trace/v1\xaa\x02&OpenTelemetry.Proto.Collector.Trace.V1b\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'opentelemetry.proto.collector.trace.v1.trace_service_pb2', _globals) if not _descriptor._USE_C_DESCRIPTORS: _globals['DESCRIPTOR']._loaded_options = None _globals['DESCRIPTOR']._serialized_options = b'\n)io.opentelemetry.proto.collector.trace.v1B\021TraceServiceProtoP\001Z1go.opentelemetry.io/proto/otlp/collector/trace/v1\252\002&OpenTelemetry.Proto.Collector.Trace.V1' _globals['_EXPORTTRACESERVICEREQUEST']._serialized_start=144 _globals['_EXPORTTRACESERVICEREQUEST']._serialized_end=240 _globals['_EXPORTTRACESERVICERESPONSE']._serialized_start=242 _globals['_EXPORTTRACESERVICERESPONSE']._serialized_end=362 _globals['_EXPORTTRACEPARTIALSUCCESS']._serialized_start=364 _globals['_EXPORTTRACEPARTIALSUCCESS']._serialized_end=438 _globals['_TRACESERVICE']._serialized_start=441 _globals['_TRACESERVICE']._serialized_end=603 # @@protoc_insertion_point(module_scope) trace_service_pb2.pyi000066400000000000000000000115031527070070700363450ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-proto/src/opentelemetry/proto/collector/trace/v1""" @generated by mypy-protobuf. Do not edit manually! isort:skip_file Copyright 2019, 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. """ import builtins import collections.abc import google.protobuf.descriptor import google.protobuf.internal.containers import google.protobuf.message import opentelemetry.proto.trace.v1.trace_pb2 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 ExportTraceServiceRequest(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor RESOURCE_SPANS_FIELD_NUMBER: builtins.int @property def resource_spans(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[opentelemetry.proto.trace.v1.trace_pb2.ResourceSpans]: """An array of ResourceSpans. For data coming from a single resource this array will typically contain one element. Intermediary nodes (such as OpenTelemetry Collector) that receive data from multiple origins typically batch the data before forwarding further and in that case this array will contain multiple elements. """ def __init__( self, *, resource_spans: collections.abc.Iterable[opentelemetry.proto.trace.v1.trace_pb2.ResourceSpans] | None = ..., ) -> None: ... def ClearField(self, field_name: typing_extensions.Literal["resource_spans", b"resource_spans"]) -> None: ... global___ExportTraceServiceRequest = ExportTraceServiceRequest @typing_extensions.final class ExportTraceServiceResponse(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor PARTIAL_SUCCESS_FIELD_NUMBER: builtins.int @property def partial_success(self) -> global___ExportTracePartialSuccess: """The details of a partially successful export request. If the request is only partially accepted (i.e. when the server accepts only parts of the data and rejects the rest) the server MUST initialize the `partial_success` field and MUST set the `rejected_` with the number of items it rejected. Servers MAY also make use of the `partial_success` field to convey warnings/suggestions to senders even when the request was fully accepted. In such cases, the `rejected_` MUST have a value of `0` and the `error_message` MUST be non-empty. A `partial_success` message with an empty value (rejected_ = 0 and `error_message` = "") is equivalent to it not being set/present. Senders SHOULD interpret it the same way as in the full success case. """ def __init__( self, *, partial_success: global___ExportTracePartialSuccess | None = ..., ) -> None: ... def HasField(self, field_name: typing_extensions.Literal["partial_success", b"partial_success"]) -> builtins.bool: ... def ClearField(self, field_name: typing_extensions.Literal["partial_success", b"partial_success"]) -> None: ... global___ExportTraceServiceResponse = ExportTraceServiceResponse @typing_extensions.final class ExportTracePartialSuccess(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor REJECTED_SPANS_FIELD_NUMBER: builtins.int ERROR_MESSAGE_FIELD_NUMBER: builtins.int rejected_spans: builtins.int """The number of rejected spans. A `rejected_` field holding a `0` value indicates that the request was fully accepted. """ error_message: builtins.str """A developer-facing human-readable message in English. It should be used either to explain why the server rejected parts of the data during a partial success or to convey warnings/suggestions during a full success. The message should offer guidance on how users can address such issues. error_message is an optional field. An error_message with an empty value is equivalent to it not being set. """ def __init__( self, *, rejected_spans: builtins.int = ..., error_message: builtins.str = ..., ) -> None: ... def ClearField(self, field_name: typing_extensions.Literal["error_message", b"error_message", "rejected_spans", b"rejected_spans"]) -> None: ... global___ExportTracePartialSuccess = ExportTracePartialSuccess trace_service_pb2_grpc.py000066400000000000000000000113141527070070700372070ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-proto/src/opentelemetry/proto/collector/trace/v1# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! """Client and server classes corresponding to protobuf-defined services.""" import grpc import warnings from opentelemetry.proto.collector.trace.v1 import trace_service_pb2 as opentelemetry_dot_proto_dot_collector_dot_trace_dot_v1_dot_trace__service__pb2 GRPC_GENERATED_VERSION = '1.63.2' GRPC_VERSION = grpc.__version__ EXPECTED_ERROR_RELEASE = '1.65.0' SCHEDULED_RELEASE_DATE = 'June 25, 2024' _version_not_supported = False try: from grpc._utilities import first_version_is_lower _version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION) except ImportError: _version_not_supported = True if _version_not_supported: warnings.warn( f'The grpc package installed is at version {GRPC_VERSION},' + f' but the generated code in opentelemetry/proto/collector/trace/v1/trace_service_pb2_grpc.py depends on' + f' grpcio>={GRPC_GENERATED_VERSION}.' + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}' + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.' + f' This warning will become an error in {EXPECTED_ERROR_RELEASE},' + f' scheduled for release on {SCHEDULED_RELEASE_DATE}.', RuntimeWarning ) class TraceServiceStub(object): """Service that can be used to push spans between one Application instrumented with OpenTelemetry and a collector, or between a collector and a central collector (in this case spans are sent/received to/from multiple Applications). """ def __init__(self, channel): """Constructor. Args: channel: A grpc.Channel. """ self.Export = channel.unary_unary( '/opentelemetry.proto.collector.trace.v1.TraceService/Export', request_serializer=opentelemetry_dot_proto_dot_collector_dot_trace_dot_v1_dot_trace__service__pb2.ExportTraceServiceRequest.SerializeToString, response_deserializer=opentelemetry_dot_proto_dot_collector_dot_trace_dot_v1_dot_trace__service__pb2.ExportTraceServiceResponse.FromString, _registered_method=True) class TraceServiceServicer(object): """Service that can be used to push spans between one Application instrumented with OpenTelemetry and a collector, or between a collector and a central collector (in this case spans are sent/received to/from multiple Applications). """ def Export(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 add_TraceServiceServicer_to_server(servicer, server): rpc_method_handlers = { 'Export': grpc.unary_unary_rpc_method_handler( servicer.Export, request_deserializer=opentelemetry_dot_proto_dot_collector_dot_trace_dot_v1_dot_trace__service__pb2.ExportTraceServiceRequest.FromString, response_serializer=opentelemetry_dot_proto_dot_collector_dot_trace_dot_v1_dot_trace__service__pb2.ExportTraceServiceResponse.SerializeToString, ), } generic_handler = grpc.method_handlers_generic_handler( 'opentelemetry.proto.collector.trace.v1.TraceService', rpc_method_handlers) server.add_generic_rpc_handlers((generic_handler,)) # This class is part of an EXPERIMENTAL API. class TraceService(object): """Service that can be used to push spans between one Application instrumented with OpenTelemetry and a collector, or between a collector and a central collector (in this case spans are sent/received to/from multiple Applications). """ @staticmethod def Export(request, target, options=(), channel_credentials=None, call_credentials=None, insecure=False, compression=None, wait_for_ready=None, timeout=None, metadata=None): return grpc.experimental.unary_unary( request, target, '/opentelemetry.proto.collector.trace.v1.TraceService/Export', opentelemetry_dot_proto_dot_collector_dot_trace_dot_v1_dot_trace__service__pb2.ExportTraceServiceRequest.SerializeToString, opentelemetry_dot_proto_dot_collector_dot_trace_dot_v1_dot_trace__service__pb2.ExportTraceServiceResponse.FromString, options, channel_credentials, insecure, call_credentials, compression, wait_for_ready, timeout, metadata, _registered_method=True) python-opentelemetry-1.45.2/opentelemetry-proto/src/opentelemetry/proto/common/000077500000000000000000000000001527070070700301765ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-proto/src/opentelemetry/proto/common/__init__.py000066400000000000000000000000001527070070700322750ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-proto/src/opentelemetry/proto/common/v1/000077500000000000000000000000001527070070700305245ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-proto/src/opentelemetry/proto/common/v1/__init__.py000066400000000000000000000000001527070070700326230ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-proto/src/opentelemetry/proto/common/v1/common_pb2.py000066400000000000000000000062541527070070700331400ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Generated by the protocol buffer compiler. DO NOT EDIT! # source: opentelemetry/proto/common/v1/common.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*opentelemetry/proto/common/v1/common.proto\x12\x1dopentelemetry.proto.common.v1\"\xad\x02\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).opentelemetry.proto.common.v1.ArrayValueH\x00\x12\x43\n\x0ckvlist_value\x18\x06 \x01(\x0b\x32+.opentelemetry.proto.common.v1.KeyValueListH\x00\x12\x15\n\x0b\x62ytes_value\x18\x07 \x01(\x0cH\x00\x12\x1f\n\x15string_value_strindex\x18\x08 \x01(\x05H\x00\x42\x07\n\x05value\"E\n\nArrayValue\x12\x37\n\x06values\x18\x01 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.AnyValue\"G\n\x0cKeyValueList\x12\x37\n\x06values\x18\x01 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\"e\n\x08KeyValue\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x36\n\x05value\x18\x02 \x01(\x0b\x32\'.opentelemetry.proto.common.v1.AnyValue\x12\x14\n\x0ckey_strindex\x18\x03 \x01(\x05\"\x94\x01\n\x14InstrumentationScope\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07version\x18\x02 \x01(\t\x12;\n\nattributes\x18\x03 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12 \n\x18\x64ropped_attributes_count\x18\x04 \x01(\r\"X\n\tEntityRef\x12\x12\n\nschema_url\x18\x01 \x01(\t\x12\x0c\n\x04type\x18\x02 \x01(\t\x12\x0f\n\x07id_keys\x18\x03 \x03(\t\x12\x18\n\x10\x64\x65scription_keys\x18\x04 \x03(\tB{\n io.opentelemetry.proto.common.v1B\x0b\x43ommonProtoP\x01Z(go.opentelemetry.io/proto/otlp/common/v1\xaa\x02\x1dOpenTelemetry.Proto.Common.V1b\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'opentelemetry.proto.common.v1.common_pb2', _globals) if not _descriptor._USE_C_DESCRIPTORS: _globals['DESCRIPTOR']._loaded_options = None _globals['DESCRIPTOR']._serialized_options = b'\n io.opentelemetry.proto.common.v1B\013CommonProtoP\001Z(go.opentelemetry.io/proto/otlp/common/v1\252\002\035OpenTelemetry.Proto.Common.V1' _globals['_ANYVALUE']._serialized_start=78 _globals['_ANYVALUE']._serialized_end=379 _globals['_ARRAYVALUE']._serialized_start=381 _globals['_ARRAYVALUE']._serialized_end=450 _globals['_KEYVALUELIST']._serialized_start=452 _globals['_KEYVALUELIST']._serialized_end=523 _globals['_KEYVALUE']._serialized_start=525 _globals['_KEYVALUE']._serialized_end=626 _globals['_INSTRUMENTATIONSCOPE']._serialized_start=629 _globals['_INSTRUMENTATIONSCOPE']._serialized_end=777 _globals['_ENTITYREF']._serialized_start=779 _globals['_ENTITYREF']._serialized_end=867 # @@protoc_insertion_point(module_scope) python-opentelemetry-1.45.2/opentelemetry-proto/src/opentelemetry/proto/common/v1/common_pb2.pyi000066400000000000000000000277341527070070700333170ustar00rootroot00000000000000""" @generated by mypy-protobuf. Do not edit manually! isort:skip_file Copyright 2019, 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. """ 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): """Represents 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_STRINDEX_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 string_value_strindex: builtins.int """Reference to the string value in ProfilesDictionary.string_table. Note: This is currently used exclusively in the Profiling signal. Implementers of OTLP receivers for signals other than Profiling should treat the presence of this value as a non-fatal issue. Log an error or warning indicating an unexpected field intended for the Profiling signal and process the data as if this value were absent or empty, ignoring its semantic content for the non-Profiling signal. Status: [Development] """ 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 = ..., string_value_strindex: builtins.int = ..., ) -> 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", "string_value_strindex", b"string_value_strindex", "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", "string_value_strindex", b"string_value_strindex", "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", "string_value_strindex"] | 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). The keys MUST be unique (it is not allowed to have more than one value with the same key). The behavior of software that receives duplicated keys can be unpredictable. """ 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): """Represents 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_STRINDEX_FIELD_NUMBER: builtins.int key: builtins.str """The key name of the pair. key_ref MUST NOT be set if key is used. """ @property def value(self) -> global___AnyValue: """The value of the pair.""" key_strindex: builtins.int """Reference to the string key in ProfilesDictionary.string_table. key MUST NOT be set if key_strindex is used. Note: This is currently used exclusively in the Profiling signal. Implementers of OTLP receivers for signals other than Profiling should treat the presence of this key as a non-fatal issue. Log an error or warning indicating an unexpected field intended for the Profiling signal and process the data as if this value were absent or empty, ignoring its semantic content for the non-Profiling signal. Status: [Development] """ def __init__( self, *, key: builtins.str = ..., value: global___AnyValue | None = ..., key_strindex: builtins.int = ..., ) -> 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", "key_strindex", b"key_strindex", "value", b"value"]) -> None: ... global___KeyValue = KeyValue @typing_extensions.final class InstrumentationScope(google.protobuf.message.Message): """InstrumentationScope is a message representing the instrumentation scope information such as the fully qualified name and version. """ DESCRIPTOR: google.protobuf.descriptor.Descriptor NAME_FIELD_NUMBER: builtins.int VERSION_FIELD_NUMBER: builtins.int ATTRIBUTES_FIELD_NUMBER: builtins.int DROPPED_ATTRIBUTES_COUNT_FIELD_NUMBER: builtins.int name: builtins.str """A name denoting the Instrumentation scope. An empty instrumentation scope name means the name is unknown. """ version: builtins.str """Defines the version of the instrumentation scope. An empty instrumentation scope version means the version is unknown. """ @property def attributes(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___KeyValue]: """Additional attributes that describe the scope. [Optional]. Attribute keys MUST be unique (it is not allowed to have more than one attribute with the same key). The behavior of software that receives duplicated keys can be unpredictable. """ dropped_attributes_count: builtins.int """The number of attributes that were discarded. Attributes can be discarded because their keys are too long or because there are too many attributes. If this value is 0, then no attributes were dropped. """ def __init__( self, *, name: builtins.str = ..., version: builtins.str = ..., attributes: collections.abc.Iterable[global___KeyValue] | None = ..., dropped_attributes_count: builtins.int = ..., ) -> None: ... def ClearField(self, field_name: typing_extensions.Literal["attributes", b"attributes", "dropped_attributes_count", b"dropped_attributes_count", "name", b"name", "version", b"version"]) -> None: ... global___InstrumentationScope = InstrumentationScope @typing_extensions.final class EntityRef(google.protobuf.message.Message): """A reference to an Entity. Entity represents an object of interest associated with produced telemetry: e.g spans, metrics, profiles, or logs. Status: [Development] """ DESCRIPTOR: google.protobuf.descriptor.Descriptor SCHEMA_URL_FIELD_NUMBER: builtins.int TYPE_FIELD_NUMBER: builtins.int ID_KEYS_FIELD_NUMBER: builtins.int DESCRIPTION_KEYS_FIELD_NUMBER: builtins.int schema_url: builtins.str """The Schema URL, if known. This is the identifier of the Schema that the entity data is recorded in. To learn more about Schema URL see https://opentelemetry.io/docs/specs/otel/schemas/#schema-url This schema_url applies to the data in this message and to the Resource attributes referenced by id_keys and description_keys. TODO: discuss if we are happy with this somewhat complicated definition of what the schema_url applies to. This field obsoletes the schema_url field in ResourceMetrics/ResourceSpans/ResourceLogs. """ type: builtins.str """Defines the type of the entity. MUST not change during the lifetime of the entity. For example: "service" or "host". This field is required and MUST not be empty for valid entities. """ @property def id_keys(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.str]: """Attribute Keys that identify the entity. MUST not change during the lifetime of the entity. The Id must contain at least one attribute. These keys MUST exist in the containing {message}.attributes. """ @property def description_keys(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.str]: """Descriptive (non-identifying) attribute keys of the entity. MAY change over the lifetime of the entity. MAY be empty. These attribute keys are not part of entity's identity. These keys MUST exist in the containing {message}.attributes. """ def __init__( self, *, schema_url: builtins.str = ..., type: builtins.str = ..., id_keys: collections.abc.Iterable[builtins.str] | None = ..., description_keys: collections.abc.Iterable[builtins.str] | None = ..., ) -> None: ... def ClearField(self, field_name: typing_extensions.Literal["description_keys", b"description_keys", "id_keys", b"id_keys", "schema_url", b"schema_url", "type", b"type"]) -> None: ... global___EntityRef = EntityRef python-opentelemetry-1.45.2/opentelemetry-proto/src/opentelemetry/proto/logs/000077500000000000000000000000001527070070700276525ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-proto/src/opentelemetry/proto/logs/v1/000077500000000000000000000000001527070070700302005ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-proto/src/opentelemetry/proto/logs/v1/logs_pb2.py000066400000000000000000000111451527070070700322630ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Generated by the protocol buffer compiler. DO NOT EDIT! # source: opentelemetry/proto/logs/v1/logs.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 opentelemetry.proto.common.v1 import common_pb2 as opentelemetry_dot_proto_dot_common_dot_v1_dot_common__pb2 from opentelemetry.proto.resource.v1 import resource_pb2 as opentelemetry_dot_proto_dot_resource_dot_v1_dot_resource__pb2 DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n&opentelemetry/proto/logs/v1/logs.proto\x12\x1bopentelemetry.proto.logs.v1\x1a*opentelemetry/proto/common/v1/common.proto\x1a.opentelemetry/proto/resource/v1/resource.proto\"L\n\x08LogsData\x12@\n\rresource_logs\x18\x01 \x03(\x0b\x32).opentelemetry.proto.logs.v1.ResourceLogs\"\xa3\x01\n\x0cResourceLogs\x12;\n\x08resource\x18\x01 \x01(\x0b\x32).opentelemetry.proto.resource.v1.Resource\x12:\n\nscope_logs\x18\x02 \x03(\x0b\x32&.opentelemetry.proto.logs.v1.ScopeLogs\x12\x12\n\nschema_url\x18\x03 \x01(\tJ\x06\x08\xe8\x07\x10\xe9\x07\"\xa0\x01\n\tScopeLogs\x12\x42\n\x05scope\x18\x01 \x01(\x0b\x32\x33.opentelemetry.proto.common.v1.InstrumentationScope\x12;\n\x0blog_records\x18\x02 \x03(\x0b\x32&.opentelemetry.proto.logs.v1.LogRecord\x12\x12\n\nschema_url\x18\x03 \x01(\t\"\x83\x03\n\tLogRecord\x12\x16\n\x0etime_unix_nano\x18\x01 \x01(\x06\x12\x1f\n\x17observed_time_unix_nano\x18\x0b \x01(\x06\x12\x44\n\x0fseverity_number\x18\x02 \x01(\x0e\x32+.opentelemetry.proto.logs.v1.SeverityNumber\x12\x15\n\rseverity_text\x18\x03 \x01(\t\x12\x35\n\x04\x62ody\x18\x05 \x01(\x0b\x32\'.opentelemetry.proto.common.v1.AnyValue\x12;\n\nattributes\x18\x06 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12 \n\x18\x64ropped_attributes_count\x18\x07 \x01(\r\x12\r\n\x05\x66lags\x18\x08 \x01(\x07\x12\x10\n\x08trace_id\x18\t \x01(\x0c\x12\x0f\n\x07span_id\x18\n \x01(\x0c\x12\x12\n\nevent_name\x18\x0c \x01(\tJ\x04\x08\x04\x10\x05*\xc3\x05\n\x0eSeverityNumber\x12\x1f\n\x1bSEVERITY_NUMBER_UNSPECIFIED\x10\x00\x12\x19\n\x15SEVERITY_NUMBER_TRACE\x10\x01\x12\x1a\n\x16SEVERITY_NUMBER_TRACE2\x10\x02\x12\x1a\n\x16SEVERITY_NUMBER_TRACE3\x10\x03\x12\x1a\n\x16SEVERITY_NUMBER_TRACE4\x10\x04\x12\x19\n\x15SEVERITY_NUMBER_DEBUG\x10\x05\x12\x1a\n\x16SEVERITY_NUMBER_DEBUG2\x10\x06\x12\x1a\n\x16SEVERITY_NUMBER_DEBUG3\x10\x07\x12\x1a\n\x16SEVERITY_NUMBER_DEBUG4\x10\x08\x12\x18\n\x14SEVERITY_NUMBER_INFO\x10\t\x12\x19\n\x15SEVERITY_NUMBER_INFO2\x10\n\x12\x19\n\x15SEVERITY_NUMBER_INFO3\x10\x0b\x12\x19\n\x15SEVERITY_NUMBER_INFO4\x10\x0c\x12\x18\n\x14SEVERITY_NUMBER_WARN\x10\r\x12\x19\n\x15SEVERITY_NUMBER_WARN2\x10\x0e\x12\x19\n\x15SEVERITY_NUMBER_WARN3\x10\x0f\x12\x19\n\x15SEVERITY_NUMBER_WARN4\x10\x10\x12\x19\n\x15SEVERITY_NUMBER_ERROR\x10\x11\x12\x1a\n\x16SEVERITY_NUMBER_ERROR2\x10\x12\x12\x1a\n\x16SEVERITY_NUMBER_ERROR3\x10\x13\x12\x1a\n\x16SEVERITY_NUMBER_ERROR4\x10\x14\x12\x19\n\x15SEVERITY_NUMBER_FATAL\x10\x15\x12\x1a\n\x16SEVERITY_NUMBER_FATAL2\x10\x16\x12\x1a\n\x16SEVERITY_NUMBER_FATAL3\x10\x17\x12\x1a\n\x16SEVERITY_NUMBER_FATAL4\x10\x18*Y\n\x0eLogRecordFlags\x12\x1f\n\x1bLOG_RECORD_FLAGS_DO_NOT_USE\x10\x00\x12&\n!LOG_RECORD_FLAGS_TRACE_FLAGS_MASK\x10\xff\x01\x42s\n\x1eio.opentelemetry.proto.logs.v1B\tLogsProtoP\x01Z&go.opentelemetry.io/proto/otlp/logs/v1\xaa\x02\x1bOpenTelemetry.Proto.Logs.V1b\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'opentelemetry.proto.logs.v1.logs_pb2', _globals) if not _descriptor._USE_C_DESCRIPTORS: _globals['DESCRIPTOR']._loaded_options = None _globals['DESCRIPTOR']._serialized_options = b'\n\036io.opentelemetry.proto.logs.v1B\tLogsProtoP\001Z&go.opentelemetry.io/proto/otlp/logs/v1\252\002\033OpenTelemetry.Proto.Logs.V1' _globals['_SEVERITYNUMBER']._serialized_start=961 _globals['_SEVERITYNUMBER']._serialized_end=1668 _globals['_LOGRECORDFLAGS']._serialized_start=1670 _globals['_LOGRECORDFLAGS']._serialized_end=1759 _globals['_LOGSDATA']._serialized_start=163 _globals['_LOGSDATA']._serialized_end=239 _globals['_RESOURCELOGS']._serialized_start=242 _globals['_RESOURCELOGS']._serialized_end=405 _globals['_SCOPELOGS']._serialized_start=408 _globals['_SCOPELOGS']._serialized_end=568 _globals['_LOGRECORD']._serialized_start=571 _globals['_LOGRECORD']._serialized_end=958 # @@protoc_insertion_point(module_scope) python-opentelemetry-1.45.2/opentelemetry-proto/src/opentelemetry/proto/logs/v1/logs_pb2.pyi000066400000000000000000000421401527070070700324330ustar00rootroot00000000000000""" @generated by mypy-protobuf. Do not edit manually! isort:skip_file Copyright 2020, 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. """ 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 opentelemetry.proto.common.v1.common_pb2 import opentelemetry.proto.resource.v1.resource_pb2 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 _SeverityNumber: ValueType = typing.NewType("ValueType", builtins.int) V: typing_extensions.TypeAlias = ValueType class _SeverityNumberEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_SeverityNumber.ValueType], builtins.type): DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor SEVERITY_NUMBER_UNSPECIFIED: _SeverityNumber.ValueType # 0 SEVERITY_NUMBER_TRACE: _SeverityNumber.ValueType # 1 SEVERITY_NUMBER_TRACE2: _SeverityNumber.ValueType # 2 SEVERITY_NUMBER_TRACE3: _SeverityNumber.ValueType # 3 SEVERITY_NUMBER_TRACE4: _SeverityNumber.ValueType # 4 SEVERITY_NUMBER_DEBUG: _SeverityNumber.ValueType # 5 SEVERITY_NUMBER_DEBUG2: _SeverityNumber.ValueType # 6 SEVERITY_NUMBER_DEBUG3: _SeverityNumber.ValueType # 7 SEVERITY_NUMBER_DEBUG4: _SeverityNumber.ValueType # 8 SEVERITY_NUMBER_INFO: _SeverityNumber.ValueType # 9 SEVERITY_NUMBER_INFO2: _SeverityNumber.ValueType # 10 SEVERITY_NUMBER_INFO3: _SeverityNumber.ValueType # 11 SEVERITY_NUMBER_INFO4: _SeverityNumber.ValueType # 12 SEVERITY_NUMBER_WARN: _SeverityNumber.ValueType # 13 SEVERITY_NUMBER_WARN2: _SeverityNumber.ValueType # 14 SEVERITY_NUMBER_WARN3: _SeverityNumber.ValueType # 15 SEVERITY_NUMBER_WARN4: _SeverityNumber.ValueType # 16 SEVERITY_NUMBER_ERROR: _SeverityNumber.ValueType # 17 SEVERITY_NUMBER_ERROR2: _SeverityNumber.ValueType # 18 SEVERITY_NUMBER_ERROR3: _SeverityNumber.ValueType # 19 SEVERITY_NUMBER_ERROR4: _SeverityNumber.ValueType # 20 SEVERITY_NUMBER_FATAL: _SeverityNumber.ValueType # 21 SEVERITY_NUMBER_FATAL2: _SeverityNumber.ValueType # 22 SEVERITY_NUMBER_FATAL3: _SeverityNumber.ValueType # 23 SEVERITY_NUMBER_FATAL4: _SeverityNumber.ValueType # 24 class SeverityNumber(_SeverityNumber, metaclass=_SeverityNumberEnumTypeWrapper): """Possible values for LogRecord.SeverityNumber.""" SEVERITY_NUMBER_UNSPECIFIED: SeverityNumber.ValueType # 0 SEVERITY_NUMBER_TRACE: SeverityNumber.ValueType # 1 SEVERITY_NUMBER_TRACE2: SeverityNumber.ValueType # 2 SEVERITY_NUMBER_TRACE3: SeverityNumber.ValueType # 3 SEVERITY_NUMBER_TRACE4: SeverityNumber.ValueType # 4 SEVERITY_NUMBER_DEBUG: SeverityNumber.ValueType # 5 SEVERITY_NUMBER_DEBUG2: SeverityNumber.ValueType # 6 SEVERITY_NUMBER_DEBUG3: SeverityNumber.ValueType # 7 SEVERITY_NUMBER_DEBUG4: SeverityNumber.ValueType # 8 SEVERITY_NUMBER_INFO: SeverityNumber.ValueType # 9 SEVERITY_NUMBER_INFO2: SeverityNumber.ValueType # 10 SEVERITY_NUMBER_INFO3: SeverityNumber.ValueType # 11 SEVERITY_NUMBER_INFO4: SeverityNumber.ValueType # 12 SEVERITY_NUMBER_WARN: SeverityNumber.ValueType # 13 SEVERITY_NUMBER_WARN2: SeverityNumber.ValueType # 14 SEVERITY_NUMBER_WARN3: SeverityNumber.ValueType # 15 SEVERITY_NUMBER_WARN4: SeverityNumber.ValueType # 16 SEVERITY_NUMBER_ERROR: SeverityNumber.ValueType # 17 SEVERITY_NUMBER_ERROR2: SeverityNumber.ValueType # 18 SEVERITY_NUMBER_ERROR3: SeverityNumber.ValueType # 19 SEVERITY_NUMBER_ERROR4: SeverityNumber.ValueType # 20 SEVERITY_NUMBER_FATAL: SeverityNumber.ValueType # 21 SEVERITY_NUMBER_FATAL2: SeverityNumber.ValueType # 22 SEVERITY_NUMBER_FATAL3: SeverityNumber.ValueType # 23 SEVERITY_NUMBER_FATAL4: SeverityNumber.ValueType # 24 global___SeverityNumber = SeverityNumber class _LogRecordFlags: ValueType = typing.NewType("ValueType", builtins.int) V: typing_extensions.TypeAlias = ValueType class _LogRecordFlagsEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_LogRecordFlags.ValueType], builtins.type): DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor LOG_RECORD_FLAGS_DO_NOT_USE: _LogRecordFlags.ValueType # 0 """The zero value for the enum. Should not be used for comparisons. Instead use bitwise "and" with the appropriate mask as shown above. """ LOG_RECORD_FLAGS_TRACE_FLAGS_MASK: _LogRecordFlags.ValueType # 255 """Bits 0-7 are used for trace flags.""" class LogRecordFlags(_LogRecordFlags, metaclass=_LogRecordFlagsEnumTypeWrapper): """LogRecordFlags represents constants used to interpret the LogRecord.flags field, which is protobuf 'fixed32' type and is to be used as bit-fields. Each non-zero value defined in this enum is a bit-mask. To extract the bit-field, for example, use an expression like: (logRecord.flags & LOG_RECORD_FLAGS_TRACE_FLAGS_MASK) """ LOG_RECORD_FLAGS_DO_NOT_USE: LogRecordFlags.ValueType # 0 """The zero value for the enum. Should not be used for comparisons. Instead use bitwise "and" with the appropriate mask as shown above. """ LOG_RECORD_FLAGS_TRACE_FLAGS_MASK: LogRecordFlags.ValueType # 255 """Bits 0-7 are used for trace flags.""" global___LogRecordFlags = LogRecordFlags @typing_extensions.final class LogsData(google.protobuf.message.Message): """LogsData represents the logs data that can be stored in a persistent storage, OR can be embedded by other protocols that transfer OTLP logs data but do not implement the OTLP protocol. The main difference between this message and collector protocol is that in this message there will not be any "control" or "metadata" specific to OTLP protocol. When new fields are added into this message, the OTLP request MUST be updated as well. """ DESCRIPTOR: google.protobuf.descriptor.Descriptor RESOURCE_LOGS_FIELD_NUMBER: builtins.int @property def resource_logs(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___ResourceLogs]: """An array of ResourceLogs. For data coming from a single resource this array will typically contain one element. Intermediary nodes that receive data from multiple origins typically batch the data before forwarding further and in that case this array will contain multiple elements. """ def __init__( self, *, resource_logs: collections.abc.Iterable[global___ResourceLogs] | None = ..., ) -> None: ... def ClearField(self, field_name: typing_extensions.Literal["resource_logs", b"resource_logs"]) -> None: ... global___LogsData = LogsData @typing_extensions.final class ResourceLogs(google.protobuf.message.Message): """A collection of ScopeLogs from a Resource.""" DESCRIPTOR: google.protobuf.descriptor.Descriptor RESOURCE_FIELD_NUMBER: builtins.int SCOPE_LOGS_FIELD_NUMBER: builtins.int SCHEMA_URL_FIELD_NUMBER: builtins.int @property def resource(self) -> opentelemetry.proto.resource.v1.resource_pb2.Resource: """The resource for the logs in this message. If this field is not set then resource info is unknown. """ @property def scope_logs(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___ScopeLogs]: """A list of ScopeLogs that originate from a resource.""" schema_url: builtins.str """The Schema URL, if known. This is the identifier of the Schema that the resource data is recorded in. Notably, the last part of the URL path is the version number of the schema: http[s]://server[:port]/path/. To learn more about Schema URL see https://opentelemetry.io/docs/specs/otel/schemas/#schema-url This schema_url applies to the data in the "resource" field. It does not apply to the data in the "scope_logs" field which have their own schema_url field. """ def __init__( self, *, resource: opentelemetry.proto.resource.v1.resource_pb2.Resource | None = ..., scope_logs: collections.abc.Iterable[global___ScopeLogs] | None = ..., schema_url: builtins.str = ..., ) -> None: ... def HasField(self, field_name: typing_extensions.Literal["resource", b"resource"]) -> builtins.bool: ... def ClearField(self, field_name: typing_extensions.Literal["resource", b"resource", "schema_url", b"schema_url", "scope_logs", b"scope_logs"]) -> None: ... global___ResourceLogs = ResourceLogs @typing_extensions.final class ScopeLogs(google.protobuf.message.Message): """A collection of Logs produced by a Scope.""" DESCRIPTOR: google.protobuf.descriptor.Descriptor SCOPE_FIELD_NUMBER: builtins.int LOG_RECORDS_FIELD_NUMBER: builtins.int SCHEMA_URL_FIELD_NUMBER: builtins.int @property def scope(self) -> opentelemetry.proto.common.v1.common_pb2.InstrumentationScope: """The instrumentation scope information for the logs in this message. Semantically when InstrumentationScope isn't set, it is equivalent with an empty instrumentation scope name (unknown). """ @property def log_records(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___LogRecord]: """A list of log records.""" schema_url: builtins.str """The Schema URL, if known. This is the identifier of the Schema that the log data is recorded in. Notably, the last part of the URL path is the version number of the schema: http[s]://server[:port]/path/. To learn more about Schema URL see https://opentelemetry.io/docs/specs/otel/schemas/#schema-url This schema_url applies to the data in the "scope" field and all logs in the "log_records" field. """ def __init__( self, *, scope: opentelemetry.proto.common.v1.common_pb2.InstrumentationScope | None = ..., log_records: collections.abc.Iterable[global___LogRecord] | None = ..., schema_url: builtins.str = ..., ) -> None: ... def HasField(self, field_name: typing_extensions.Literal["scope", b"scope"]) -> builtins.bool: ... def ClearField(self, field_name: typing_extensions.Literal["log_records", b"log_records", "schema_url", b"schema_url", "scope", b"scope"]) -> None: ... global___ScopeLogs = ScopeLogs @typing_extensions.final class LogRecord(google.protobuf.message.Message): """A log record according to OpenTelemetry Log Data Model: https://github.com/open-telemetry/oteps/blob/main/text/logs/0097-log-data-model.md """ DESCRIPTOR: google.protobuf.descriptor.Descriptor TIME_UNIX_NANO_FIELD_NUMBER: builtins.int OBSERVED_TIME_UNIX_NANO_FIELD_NUMBER: builtins.int SEVERITY_NUMBER_FIELD_NUMBER: builtins.int SEVERITY_TEXT_FIELD_NUMBER: builtins.int BODY_FIELD_NUMBER: builtins.int ATTRIBUTES_FIELD_NUMBER: builtins.int DROPPED_ATTRIBUTES_COUNT_FIELD_NUMBER: builtins.int FLAGS_FIELD_NUMBER: builtins.int TRACE_ID_FIELD_NUMBER: builtins.int SPAN_ID_FIELD_NUMBER: builtins.int EVENT_NAME_FIELD_NUMBER: builtins.int time_unix_nano: builtins.int """time_unix_nano is the time when the event occurred. Value is UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January 1970. Value of 0 indicates unknown or missing timestamp. """ observed_time_unix_nano: builtins.int """Time when the event was observed by the collection system. For events that originate in OpenTelemetry (e.g. using OpenTelemetry Logging SDK) this timestamp is typically set at the generation time and is equal to Timestamp. For events originating externally and collected by OpenTelemetry (e.g. using Collector) this is the time when OpenTelemetry's code observed the event measured by the clock of the OpenTelemetry code. This field MUST be set once the event is observed by OpenTelemetry. For converting OpenTelemetry log data to formats that support only one timestamp or when receiving OpenTelemetry log data by recipients that support only one timestamp internally the following logic is recommended: - Use time_unix_nano if it is present, otherwise use observed_time_unix_nano. Value is UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January 1970. Value of 0 indicates unknown or missing timestamp. """ severity_number: global___SeverityNumber.ValueType """Numerical value of the severity, normalized to values described in Log Data Model. [Optional]. """ severity_text: builtins.str """The severity text (also known as log level). The original string representation as it is known at the source. [Optional]. """ @property def body(self) -> opentelemetry.proto.common.v1.common_pb2.AnyValue: """A value containing the body of the log record. Can be for example a human-readable string message (including multi-line) describing the event in a free form or it can be a structured data composed of arrays and maps of other values. [Optional]. """ @property def attributes(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[opentelemetry.proto.common.v1.common_pb2.KeyValue]: """Additional attributes that describe the specific event occurrence. [Optional]. Attribute keys MUST be unique (it is not allowed to have more than one attribute with the same key). The behavior of software that receives duplicated keys can be unpredictable. """ dropped_attributes_count: builtins.int flags: builtins.int """Flags, a bit field. 8 least significant bits are the trace flags as defined in W3C Trace Context specification. 24 most significant bits are reserved and must be set to 0. Readers must not assume that 24 most significant bits will be zero and must correctly mask the bits when reading 8-bit trace flag (use flags & LOG_RECORD_FLAGS_TRACE_FLAGS_MASK). [Optional]. """ trace_id: builtins.bytes """A unique identifier for a trace. All logs from the same trace share the same `trace_id`. The ID is a 16-byte array. An ID with all zeroes OR of length other than 16 bytes is considered invalid (empty string in OTLP/JSON is zero-length and thus is also invalid). This field is optional. The receivers SHOULD assume that the log record is not associated with a trace if any of the following is true: - the field is not present, - the field contains an invalid value. """ span_id: builtins.bytes """A unique identifier for a span within a trace, assigned when the span is created. The ID is an 8-byte array. An ID with all zeroes OR of length other than 8 bytes is considered invalid (empty string in OTLP/JSON is zero-length and thus is also invalid). This field is optional. If the sender specifies a valid span_id then it SHOULD also specify a valid trace_id. The receivers SHOULD assume that the log record is not associated with a span if any of the following is true: - the field is not present, - the field contains an invalid value. """ event_name: builtins.str """A unique identifier of event category/type. All events with the same event_name are expected to conform to the same schema for both their attributes and their body. Recommended to be fully qualified and short (no longer than 256 characters). Presence of event_name on the log record identifies this record as an event. [Optional]. """ def __init__( self, *, time_unix_nano: builtins.int = ..., observed_time_unix_nano: builtins.int = ..., severity_number: global___SeverityNumber.ValueType = ..., severity_text: builtins.str = ..., body: opentelemetry.proto.common.v1.common_pb2.AnyValue | None = ..., attributes: collections.abc.Iterable[opentelemetry.proto.common.v1.common_pb2.KeyValue] | None = ..., dropped_attributes_count: builtins.int = ..., flags: builtins.int = ..., trace_id: builtins.bytes = ..., span_id: builtins.bytes = ..., event_name: builtins.str = ..., ) -> None: ... def HasField(self, field_name: typing_extensions.Literal["body", b"body"]) -> builtins.bool: ... def ClearField(self, field_name: typing_extensions.Literal["attributes", b"attributes", "body", b"body", "dropped_attributes_count", b"dropped_attributes_count", "event_name", b"event_name", "flags", b"flags", "observed_time_unix_nano", b"observed_time_unix_nano", "severity_number", b"severity_number", "severity_text", b"severity_text", "span_id", b"span_id", "time_unix_nano", b"time_unix_nano", "trace_id", b"trace_id"]) -> None: ... global___LogRecord = LogRecord python-opentelemetry-1.45.2/opentelemetry-proto/src/opentelemetry/proto/metrics/000077500000000000000000000000001527070070700303545ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-proto/src/opentelemetry/proto/metrics/__init__.py000066400000000000000000000000001527070070700324530ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-proto/src/opentelemetry/proto/metrics/v1/000077500000000000000000000000001527070070700307025ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-proto/src/opentelemetry/proto/metrics/v1/__init__.py000066400000000000000000000000001527070070700330010ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-proto/src/opentelemetry/proto/metrics/v1/metrics_pb2.py000066400000000000000000000223741527070070700334750ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Generated by the protocol buffer compiler. DO NOT EDIT! # source: opentelemetry/proto/metrics/v1/metrics.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 opentelemetry.proto.common.v1 import common_pb2 as opentelemetry_dot_proto_dot_common_dot_v1_dot_common__pb2 from opentelemetry.proto.resource.v1 import resource_pb2 as opentelemetry_dot_proto_dot_resource_dot_v1_dot_resource__pb2 DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n,opentelemetry/proto/metrics/v1/metrics.proto\x12\x1eopentelemetry.proto.metrics.v1\x1a*opentelemetry/proto/common/v1/common.proto\x1a.opentelemetry/proto/resource/v1/resource.proto\"X\n\x0bMetricsData\x12I\n\x10resource_metrics\x18\x01 \x03(\x0b\x32/.opentelemetry.proto.metrics.v1.ResourceMetrics\"\xaf\x01\n\x0fResourceMetrics\x12;\n\x08resource\x18\x01 \x01(\x0b\x32).opentelemetry.proto.resource.v1.Resource\x12\x43\n\rscope_metrics\x18\x02 \x03(\x0b\x32,.opentelemetry.proto.metrics.v1.ScopeMetrics\x12\x12\n\nschema_url\x18\x03 \x01(\tJ\x06\x08\xe8\x07\x10\xe9\x07\"\x9f\x01\n\x0cScopeMetrics\x12\x42\n\x05scope\x18\x01 \x01(\x0b\x32\x33.opentelemetry.proto.common.v1.InstrumentationScope\x12\x37\n\x07metrics\x18\x02 \x03(\x0b\x32&.opentelemetry.proto.metrics.v1.Metric\x12\x12\n\nschema_url\x18\x03 \x01(\t\"\xcd\x03\n\x06Metric\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x13\n\x0b\x64\x65scription\x18\x02 \x01(\t\x12\x0c\n\x04unit\x18\x03 \x01(\t\x12\x36\n\x05gauge\x18\x05 \x01(\x0b\x32%.opentelemetry.proto.metrics.v1.GaugeH\x00\x12\x32\n\x03sum\x18\x07 \x01(\x0b\x32#.opentelemetry.proto.metrics.v1.SumH\x00\x12>\n\thistogram\x18\t \x01(\x0b\x32).opentelemetry.proto.metrics.v1.HistogramH\x00\x12U\n\x15\x65xponential_histogram\x18\n \x01(\x0b\x32\x34.opentelemetry.proto.metrics.v1.ExponentialHistogramH\x00\x12:\n\x07summary\x18\x0b \x01(\x0b\x32\'.opentelemetry.proto.metrics.v1.SummaryH\x00\x12\x39\n\x08metadata\x18\x0c \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValueB\x06\n\x04\x64\x61taJ\x04\x08\x04\x10\x05J\x04\x08\x06\x10\x07J\x04\x08\x08\x10\t\"M\n\x05Gauge\x12\x44\n\x0b\x64\x61ta_points\x18\x01 \x03(\x0b\x32/.opentelemetry.proto.metrics.v1.NumberDataPoint\"\xba\x01\n\x03Sum\x12\x44\n\x0b\x64\x61ta_points\x18\x01 \x03(\x0b\x32/.opentelemetry.proto.metrics.v1.NumberDataPoint\x12W\n\x17\x61ggregation_temporality\x18\x02 \x01(\x0e\x32\x36.opentelemetry.proto.metrics.v1.AggregationTemporality\x12\x14\n\x0cis_monotonic\x18\x03 \x01(\x08\"\xad\x01\n\tHistogram\x12G\n\x0b\x64\x61ta_points\x18\x01 \x03(\x0b\x32\x32.opentelemetry.proto.metrics.v1.HistogramDataPoint\x12W\n\x17\x61ggregation_temporality\x18\x02 \x01(\x0e\x32\x36.opentelemetry.proto.metrics.v1.AggregationTemporality\"\xc3\x01\n\x14\x45xponentialHistogram\x12R\n\x0b\x64\x61ta_points\x18\x01 \x03(\x0b\x32=.opentelemetry.proto.metrics.v1.ExponentialHistogramDataPoint\x12W\n\x17\x61ggregation_temporality\x18\x02 \x01(\x0e\x32\x36.opentelemetry.proto.metrics.v1.AggregationTemporality\"P\n\x07Summary\x12\x45\n\x0b\x64\x61ta_points\x18\x01 \x03(\x0b\x32\x30.opentelemetry.proto.metrics.v1.SummaryDataPoint\"\x86\x02\n\x0fNumberDataPoint\x12;\n\nattributes\x18\x07 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12\x1c\n\x14start_time_unix_nano\x18\x02 \x01(\x06\x12\x16\n\x0etime_unix_nano\x18\x03 \x01(\x06\x12\x13\n\tas_double\x18\x04 \x01(\x01H\x00\x12\x10\n\x06\x61s_int\x18\x06 \x01(\x10H\x00\x12;\n\texemplars\x18\x05 \x03(\x0b\x32(.opentelemetry.proto.metrics.v1.Exemplar\x12\r\n\x05\x66lags\x18\x08 \x01(\rB\x07\n\x05valueJ\x04\x08\x01\x10\x02\"\xe6\x02\n\x12HistogramDataPoint\x12;\n\nattributes\x18\t \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12\x1c\n\x14start_time_unix_nano\x18\x02 \x01(\x06\x12\x16\n\x0etime_unix_nano\x18\x03 \x01(\x06\x12\r\n\x05\x63ount\x18\x04 \x01(\x06\x12\x10\n\x03sum\x18\x05 \x01(\x01H\x00\x88\x01\x01\x12\x15\n\rbucket_counts\x18\x06 \x03(\x06\x12\x17\n\x0f\x65xplicit_bounds\x18\x07 \x03(\x01\x12;\n\texemplars\x18\x08 \x03(\x0b\x32(.opentelemetry.proto.metrics.v1.Exemplar\x12\r\n\x05\x66lags\x18\n \x01(\r\x12\x10\n\x03min\x18\x0b \x01(\x01H\x01\x88\x01\x01\x12\x10\n\x03max\x18\x0c \x01(\x01H\x02\x88\x01\x01\x42\x06\n\x04_sumB\x06\n\x04_minB\x06\n\x04_maxJ\x04\x08\x01\x10\x02\"\xda\x04\n\x1d\x45xponentialHistogramDataPoint\x12;\n\nattributes\x18\x01 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12\x1c\n\x14start_time_unix_nano\x18\x02 \x01(\x06\x12\x16\n\x0etime_unix_nano\x18\x03 \x01(\x06\x12\r\n\x05\x63ount\x18\x04 \x01(\x06\x12\x10\n\x03sum\x18\x05 \x01(\x01H\x00\x88\x01\x01\x12\r\n\x05scale\x18\x06 \x01(\x11\x12\x12\n\nzero_count\x18\x07 \x01(\x06\x12W\n\x08positive\x18\x08 \x01(\x0b\x32\x45.opentelemetry.proto.metrics.v1.ExponentialHistogramDataPoint.Buckets\x12W\n\x08negative\x18\t \x01(\x0b\x32\x45.opentelemetry.proto.metrics.v1.ExponentialHistogramDataPoint.Buckets\x12\r\n\x05\x66lags\x18\n \x01(\r\x12;\n\texemplars\x18\x0b \x03(\x0b\x32(.opentelemetry.proto.metrics.v1.Exemplar\x12\x10\n\x03min\x18\x0c \x01(\x01H\x01\x88\x01\x01\x12\x10\n\x03max\x18\r \x01(\x01H\x02\x88\x01\x01\x12\x16\n\x0ezero_threshold\x18\x0e \x01(\x01\x1a\x30\n\x07\x42uckets\x12\x0e\n\x06offset\x18\x01 \x01(\x11\x12\x15\n\rbucket_counts\x18\x02 \x03(\x04\x42\x06\n\x04_sumB\x06\n\x04_minB\x06\n\x04_max\"\xc5\x02\n\x10SummaryDataPoint\x12;\n\nattributes\x18\x07 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12\x1c\n\x14start_time_unix_nano\x18\x02 \x01(\x06\x12\x16\n\x0etime_unix_nano\x18\x03 \x01(\x06\x12\r\n\x05\x63ount\x18\x04 \x01(\x06\x12\x0b\n\x03sum\x18\x05 \x01(\x01\x12Y\n\x0fquantile_values\x18\x06 \x03(\x0b\x32@.opentelemetry.proto.metrics.v1.SummaryDataPoint.ValueAtQuantile\x12\r\n\x05\x66lags\x18\x08 \x01(\r\x1a\x32\n\x0fValueAtQuantile\x12\x10\n\x08quantile\x18\x01 \x01(\x01\x12\r\n\x05value\x18\x02 \x01(\x01J\x04\x08\x01\x10\x02\"\xc1\x01\n\x08\x45xemplar\x12\x44\n\x13\x66iltered_attributes\x18\x07 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12\x16\n\x0etime_unix_nano\x18\x02 \x01(\x06\x12\x13\n\tas_double\x18\x03 \x01(\x01H\x00\x12\x10\n\x06\x61s_int\x18\x06 \x01(\x10H\x00\x12\x0f\n\x07span_id\x18\x04 \x01(\x0c\x12\x10\n\x08trace_id\x18\x05 \x01(\x0c\x42\x07\n\x05valueJ\x04\x08\x01\x10\x02*\x8c\x01\n\x16\x41ggregationTemporality\x12\'\n#AGGREGATION_TEMPORALITY_UNSPECIFIED\x10\x00\x12!\n\x1d\x41GGREGATION_TEMPORALITY_DELTA\x10\x01\x12&\n\"AGGREGATION_TEMPORALITY_CUMULATIVE\x10\x02*^\n\x0e\x44\x61taPointFlags\x12\x1f\n\x1b\x44\x41TA_POINT_FLAGS_DO_NOT_USE\x10\x00\x12+\n\'DATA_POINT_FLAGS_NO_RECORDED_VALUE_MASK\x10\x01\x42\x7f\n!io.opentelemetry.proto.metrics.v1B\x0cMetricsProtoP\x01Z)go.opentelemetry.io/proto/otlp/metrics/v1\xaa\x02\x1eOpenTelemetry.Proto.Metrics.V1b\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'opentelemetry.proto.metrics.v1.metrics_pb2', _globals) if not _descriptor._USE_C_DESCRIPTORS: _globals['DESCRIPTOR']._loaded_options = None _globals['DESCRIPTOR']._serialized_options = b'\n!io.opentelemetry.proto.metrics.v1B\014MetricsProtoP\001Z)go.opentelemetry.io/proto/otlp/metrics/v1\252\002\036OpenTelemetry.Proto.Metrics.V1' _globals['_AGGREGATIONTEMPORALITY']._serialized_start=3546 _globals['_AGGREGATIONTEMPORALITY']._serialized_end=3686 _globals['_DATAPOINTFLAGS']._serialized_start=3688 _globals['_DATAPOINTFLAGS']._serialized_end=3782 _globals['_METRICSDATA']._serialized_start=172 _globals['_METRICSDATA']._serialized_end=260 _globals['_RESOURCEMETRICS']._serialized_start=263 _globals['_RESOURCEMETRICS']._serialized_end=438 _globals['_SCOPEMETRICS']._serialized_start=441 _globals['_SCOPEMETRICS']._serialized_end=600 _globals['_METRIC']._serialized_start=603 _globals['_METRIC']._serialized_end=1064 _globals['_GAUGE']._serialized_start=1066 _globals['_GAUGE']._serialized_end=1143 _globals['_SUM']._serialized_start=1146 _globals['_SUM']._serialized_end=1332 _globals['_HISTOGRAM']._serialized_start=1335 _globals['_HISTOGRAM']._serialized_end=1508 _globals['_EXPONENTIALHISTOGRAM']._serialized_start=1511 _globals['_EXPONENTIALHISTOGRAM']._serialized_end=1706 _globals['_SUMMARY']._serialized_start=1708 _globals['_SUMMARY']._serialized_end=1788 _globals['_NUMBERDATAPOINT']._serialized_start=1791 _globals['_NUMBERDATAPOINT']._serialized_end=2053 _globals['_HISTOGRAMDATAPOINT']._serialized_start=2056 _globals['_HISTOGRAMDATAPOINT']._serialized_end=2414 _globals['_EXPONENTIALHISTOGRAMDATAPOINT']._serialized_start=2417 _globals['_EXPONENTIALHISTOGRAMDATAPOINT']._serialized_end=3019 _globals['_EXPONENTIALHISTOGRAMDATAPOINT_BUCKETS']._serialized_start=2947 _globals['_EXPONENTIALHISTOGRAMDATAPOINT_BUCKETS']._serialized_end=2995 _globals['_SUMMARYDATAPOINT']._serialized_start=3022 _globals['_SUMMARYDATAPOINT']._serialized_end=3347 _globals['_SUMMARYDATAPOINT_VALUEATQUANTILE']._serialized_start=3291 _globals['_SUMMARYDATAPOINT_VALUEATQUANTILE']._serialized_end=3341 _globals['_EXEMPLAR']._serialized_start=3350 _globals['_EXEMPLAR']._serialized_end=3543 # @@protoc_insertion_point(module_scope) python-opentelemetry-1.45.2/opentelemetry-proto/src/opentelemetry/proto/metrics/v1/metrics_pb2.pyi000066400000000000000000001567021527070070700336510ustar00rootroot00000000000000""" @generated by mypy-protobuf. Do not edit manually! isort:skip_file Copyright 2019, 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. """ 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 opentelemetry.proto.common.v1.common_pb2 import opentelemetry.proto.resource.v1.resource_pb2 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 _AggregationTemporality: ValueType = typing.NewType("ValueType", builtins.int) V: typing_extensions.TypeAlias = ValueType class _AggregationTemporalityEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_AggregationTemporality.ValueType], builtins.type): DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor AGGREGATION_TEMPORALITY_UNSPECIFIED: _AggregationTemporality.ValueType # 0 """UNSPECIFIED is the default AggregationTemporality, it MUST not be used.""" AGGREGATION_TEMPORALITY_DELTA: _AggregationTemporality.ValueType # 1 """DELTA is an AggregationTemporality for a metric aggregator which reports changes since last report time. Successive metrics contain aggregation of values from continuous and non-overlapping intervals. The values for a DELTA metric are based only on the time interval associated with one measurement cycle. There is no dependency on previous measurements like is the case for CUMULATIVE metrics. For example, consider a system measuring the number of requests that it receives and reports the sum of these requests every second as a DELTA metric: 1. The system starts receiving at time=t_0. 2. A request is received, the system measures 1 request. 3. A request is received, the system measures 1 request. 4. A request is received, the system measures 1 request. 5. The 1 second collection cycle ends. A metric is exported for the number of requests received over the interval of time t_0 to t_0+1 with a value of 3. 6. A request is received, the system measures 1 request. 7. A request is received, the system measures 1 request. 8. The 1 second collection cycle ends. A metric is exported for the number of requests received over the interval of time t_0+1 to t_0+2 with a value of 2. """ AGGREGATION_TEMPORALITY_CUMULATIVE: _AggregationTemporality.ValueType # 2 """CUMULATIVE is an AggregationTemporality for a metric aggregator which reports changes since a fixed start time. This means that current values of a CUMULATIVE metric depend on all previous measurements since the start time. Because of this, the sender is required to retain this state in some form. If this state is lost or invalidated, the CUMULATIVE metric values MUST be reset and a new fixed start time following the last reported measurement time sent MUST be used. For example, consider a system measuring the number of requests that it receives and reports the sum of these requests every second as a CUMULATIVE metric: 1. The system starts receiving at time=t_0. 2. A request is received, the system measures 1 request. 3. A request is received, the system measures 1 request. 4. A request is received, the system measures 1 request. 5. The 1 second collection cycle ends. A metric is exported for the number of requests received over the interval of time t_0 to t_0+1 with a value of 3. 6. A request is received, the system measures 1 request. 7. A request is received, the system measures 1 request. 8. The 1 second collection cycle ends. A metric is exported for the number of requests received over the interval of time t_0 to t_0+2 with a value of 5. 9. The system experiences a fault and loses state. 10. The system recovers and resumes receiving at time=t_1. 11. A request is received, the system measures 1 request. 12. The 1 second collection cycle ends. A metric is exported for the number of requests received over the interval of time t_1 to t_0+1 with a value of 1. Note: Even though, when reporting changes since last report time, using CUMULATIVE is valid, it is not recommended. This may cause problems for systems that do not use start_time to determine when the aggregation value was reset (e.g. Prometheus). """ class AggregationTemporality(_AggregationTemporality, metaclass=_AggregationTemporalityEnumTypeWrapper): """AggregationTemporality defines how a metric aggregator reports aggregated values. It describes how those values relate to the time interval over which they are aggregated. """ AGGREGATION_TEMPORALITY_UNSPECIFIED: AggregationTemporality.ValueType # 0 """UNSPECIFIED is the default AggregationTemporality, it MUST not be used.""" AGGREGATION_TEMPORALITY_DELTA: AggregationTemporality.ValueType # 1 """DELTA is an AggregationTemporality for a metric aggregator which reports changes since last report time. Successive metrics contain aggregation of values from continuous and non-overlapping intervals. The values for a DELTA metric are based only on the time interval associated with one measurement cycle. There is no dependency on previous measurements like is the case for CUMULATIVE metrics. For example, consider a system measuring the number of requests that it receives and reports the sum of these requests every second as a DELTA metric: 1. The system starts receiving at time=t_0. 2. A request is received, the system measures 1 request. 3. A request is received, the system measures 1 request. 4. A request is received, the system measures 1 request. 5. The 1 second collection cycle ends. A metric is exported for the number of requests received over the interval of time t_0 to t_0+1 with a value of 3. 6. A request is received, the system measures 1 request. 7. A request is received, the system measures 1 request. 8. The 1 second collection cycle ends. A metric is exported for the number of requests received over the interval of time t_0+1 to t_0+2 with a value of 2. """ AGGREGATION_TEMPORALITY_CUMULATIVE: AggregationTemporality.ValueType # 2 """CUMULATIVE is an AggregationTemporality for a metric aggregator which reports changes since a fixed start time. This means that current values of a CUMULATIVE metric depend on all previous measurements since the start time. Because of this, the sender is required to retain this state in some form. If this state is lost or invalidated, the CUMULATIVE metric values MUST be reset and a new fixed start time following the last reported measurement time sent MUST be used. For example, consider a system measuring the number of requests that it receives and reports the sum of these requests every second as a CUMULATIVE metric: 1. The system starts receiving at time=t_0. 2. A request is received, the system measures 1 request. 3. A request is received, the system measures 1 request. 4. A request is received, the system measures 1 request. 5. The 1 second collection cycle ends. A metric is exported for the number of requests received over the interval of time t_0 to t_0+1 with a value of 3. 6. A request is received, the system measures 1 request. 7. A request is received, the system measures 1 request. 8. The 1 second collection cycle ends. A metric is exported for the number of requests received over the interval of time t_0 to t_0+2 with a value of 5. 9. The system experiences a fault and loses state. 10. The system recovers and resumes receiving at time=t_1. 11. A request is received, the system measures 1 request. 12. The 1 second collection cycle ends. A metric is exported for the number of requests received over the interval of time t_1 to t_0+1 with a value of 1. Note: Even though, when reporting changes since last report time, using CUMULATIVE is valid, it is not recommended. This may cause problems for systems that do not use start_time to determine when the aggregation value was reset (e.g. Prometheus). """ global___AggregationTemporality = AggregationTemporality class _DataPointFlags: ValueType = typing.NewType("ValueType", builtins.int) V: typing_extensions.TypeAlias = ValueType class _DataPointFlagsEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_DataPointFlags.ValueType], builtins.type): DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor DATA_POINT_FLAGS_DO_NOT_USE: _DataPointFlags.ValueType # 0 """The zero value for the enum. Should not be used for comparisons. Instead use bitwise "and" with the appropriate mask as shown above. """ DATA_POINT_FLAGS_NO_RECORDED_VALUE_MASK: _DataPointFlags.ValueType # 1 """This DataPoint is valid but has no recorded value. This value SHOULD be used to reflect explicitly missing data in a series, as for an equivalent to the Prometheus "staleness marker". """ class DataPointFlags(_DataPointFlags, metaclass=_DataPointFlagsEnumTypeWrapper): """DataPointFlags is defined as a protobuf 'uint32' type and is to be used as a bit-field representing 32 distinct boolean flags. Each flag defined in this enum is a bit-mask. To test the presence of a single flag in the flags of a data point, for example, use an expression like: (point.flags & DATA_POINT_FLAGS_NO_RECORDED_VALUE_MASK) == DATA_POINT_FLAGS_NO_RECORDED_VALUE_MASK """ DATA_POINT_FLAGS_DO_NOT_USE: DataPointFlags.ValueType # 0 """The zero value for the enum. Should not be used for comparisons. Instead use bitwise "and" with the appropriate mask as shown above. """ DATA_POINT_FLAGS_NO_RECORDED_VALUE_MASK: DataPointFlags.ValueType # 1 """This DataPoint is valid but has no recorded value. This value SHOULD be used to reflect explicitly missing data in a series, as for an equivalent to the Prometheus "staleness marker". """ global___DataPointFlags = DataPointFlags @typing_extensions.final class MetricsData(google.protobuf.message.Message): """MetricsData represents the metrics data that can be stored in a persistent storage, OR can be embedded by other protocols that transfer OTLP metrics data but do not implement the OTLP protocol. MetricsData └─── ResourceMetrics ├── Resource ├── SchemaURL └── ScopeMetrics ├── Scope ├── SchemaURL └── Metric ├── Name ├── Description ├── Unit └── data ├── Gauge ├── Sum ├── Histogram ├── ExponentialHistogram └── Summary The main difference between this message and collector protocol is that in this message there will not be any "control" or "metadata" specific to OTLP protocol. When new fields are added into this message, the OTLP request MUST be updated as well. """ DESCRIPTOR: google.protobuf.descriptor.Descriptor RESOURCE_METRICS_FIELD_NUMBER: builtins.int @property def resource_metrics(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___ResourceMetrics]: """An array of ResourceMetrics. For data coming from a single resource this array will typically contain one element. Intermediary nodes that receive data from multiple origins typically batch the data before forwarding further and in that case this array will contain multiple elements. """ def __init__( self, *, resource_metrics: collections.abc.Iterable[global___ResourceMetrics] | None = ..., ) -> None: ... def ClearField(self, field_name: typing_extensions.Literal["resource_metrics", b"resource_metrics"]) -> None: ... global___MetricsData = MetricsData @typing_extensions.final class ResourceMetrics(google.protobuf.message.Message): """A collection of ScopeMetrics from a Resource.""" DESCRIPTOR: google.protobuf.descriptor.Descriptor RESOURCE_FIELD_NUMBER: builtins.int SCOPE_METRICS_FIELD_NUMBER: builtins.int SCHEMA_URL_FIELD_NUMBER: builtins.int @property def resource(self) -> opentelemetry.proto.resource.v1.resource_pb2.Resource: """The resource for the metrics in this message. If this field is not set then no resource info is known. """ @property def scope_metrics(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___ScopeMetrics]: """A list of metrics that originate from a resource.""" schema_url: builtins.str """The Schema URL, if known. This is the identifier of the Schema that the resource data is recorded in. Notably, the last part of the URL path is the version number of the schema: http[s]://server[:port]/path/. To learn more about Schema URL see https://opentelemetry.io/docs/specs/otel/schemas/#schema-url This schema_url applies to the data in the "resource" field. It does not apply to the data in the "scope_metrics" field which have their own schema_url field. """ def __init__( self, *, resource: opentelemetry.proto.resource.v1.resource_pb2.Resource | None = ..., scope_metrics: collections.abc.Iterable[global___ScopeMetrics] | None = ..., schema_url: builtins.str = ..., ) -> None: ... def HasField(self, field_name: typing_extensions.Literal["resource", b"resource"]) -> builtins.bool: ... def ClearField(self, field_name: typing_extensions.Literal["resource", b"resource", "schema_url", b"schema_url", "scope_metrics", b"scope_metrics"]) -> None: ... global___ResourceMetrics = ResourceMetrics @typing_extensions.final class ScopeMetrics(google.protobuf.message.Message): """A collection of Metrics produced by an Scope.""" DESCRIPTOR: google.protobuf.descriptor.Descriptor SCOPE_FIELD_NUMBER: builtins.int METRICS_FIELD_NUMBER: builtins.int SCHEMA_URL_FIELD_NUMBER: builtins.int @property def scope(self) -> opentelemetry.proto.common.v1.common_pb2.InstrumentationScope: """The instrumentation scope information for the metrics in this message. Semantically when InstrumentationScope isn't set, it is equivalent with an empty instrumentation scope name (unknown). """ @property def metrics(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___Metric]: """A list of metrics that originate from an instrumentation library.""" schema_url: builtins.str """The Schema URL, if known. This is the identifier of the Schema that the metric data is recorded in. Notably, the last part of the URL path is the version number of the schema: http[s]://server[:port]/path/. To learn more about Schema URL see https://opentelemetry.io/docs/specs/otel/schemas/#schema-url This schema_url applies to the data in the "scope" field and all metrics in the "metrics" field. """ def __init__( self, *, scope: opentelemetry.proto.common.v1.common_pb2.InstrumentationScope | None = ..., metrics: collections.abc.Iterable[global___Metric] | None = ..., schema_url: builtins.str = ..., ) -> None: ... def HasField(self, field_name: typing_extensions.Literal["scope", b"scope"]) -> builtins.bool: ... def ClearField(self, field_name: typing_extensions.Literal["metrics", b"metrics", "schema_url", b"schema_url", "scope", b"scope"]) -> None: ... global___ScopeMetrics = ScopeMetrics @typing_extensions.final class Metric(google.protobuf.message.Message): """Defines a Metric which has one or more timeseries. The following is a brief summary of the Metric data model. For more details, see: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/data-model.md The data model and relation between entities is shown in the diagram below. Here, "DataPoint" is the term used to refer to any one of the specific data point value types, and "points" is the term used to refer to any one of the lists of points contained in the Metric. - Metric is composed of a metadata and data. - Metadata part contains a name, description, unit. - Data is one of the possible types (Sum, Gauge, Histogram, Summary). - DataPoint contains timestamps, attributes, and one of the possible value type fields. Metric +------------+ |name | |description | |unit | +------------------------------------+ |data |---> |Gauge, Sum, Histogram, Summary, ... | +------------+ +------------------------------------+ Data [One of Gauge, Sum, Histogram, Summary, ...] +-----------+ |... | // Metadata about the Data. |points |--+ +-----------+ | | +---------------------------+ | |DataPoint 1 | v |+------+------+ +------+ | +-----+ ||label |label |...|label | | | 1 |-->||value1|value2|...|valueN| | +-----+ |+------+------+ +------+ | | . | |+-----+ | | . | ||value| | | . | |+-----+ | | . | +---------------------------+ | . | . | . | . | . | . | . | +---------------------------+ | . | |DataPoint M | +-----+ |+------+------+ +------+ | | M |-->||label |label |...|label | | +-----+ ||value1|value2|...|valueN| | |+------+------+ +------+ | |+-----+ | ||value| | |+-----+ | +---------------------------+ Each distinct type of DataPoint represents the output of a specific aggregation function, the result of applying the DataPoint's associated function of to one or more measurements. All DataPoint types have three common fields: - Attributes includes key-value pairs associated with the data point - TimeUnixNano is required, set to the end time of the aggregation - StartTimeUnixNano is optional, but strongly encouraged for DataPoints having an AggregationTemporality field, as discussed below. Both TimeUnixNano and StartTimeUnixNano values are expressed as UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January 1970. # TimeUnixNano This field is required, having consistent interpretation across DataPoint types. TimeUnixNano is the moment corresponding to when the data point's aggregate value was captured. Data points with the 0 value for TimeUnixNano SHOULD be rejected by consumers. # StartTimeUnixNano StartTimeUnixNano in general allows detecting when a sequence of observations is unbroken. This field indicates to consumers the start time for points with cumulative and delta AggregationTemporality, and it should be included whenever possible to support correct rate calculation. Although it may be omitted when the start time is truly unknown, setting StartTimeUnixNano is strongly encouraged. """ DESCRIPTOR: google.protobuf.descriptor.Descriptor NAME_FIELD_NUMBER: builtins.int DESCRIPTION_FIELD_NUMBER: builtins.int UNIT_FIELD_NUMBER: builtins.int GAUGE_FIELD_NUMBER: builtins.int SUM_FIELD_NUMBER: builtins.int HISTOGRAM_FIELD_NUMBER: builtins.int EXPONENTIAL_HISTOGRAM_FIELD_NUMBER: builtins.int SUMMARY_FIELD_NUMBER: builtins.int METADATA_FIELD_NUMBER: builtins.int name: builtins.str """The name of the metric.""" description: builtins.str """A description of the metric, which can be used in documentation.""" unit: builtins.str """The unit in which the metric value is reported. Follows the format described by https://unitsofmeasure.org/ucum.html. """ @property def gauge(self) -> global___Gauge: ... @property def sum(self) -> global___Sum: ... @property def histogram(self) -> global___Histogram: ... @property def exponential_histogram(self) -> global___ExponentialHistogram: ... @property def summary(self) -> global___Summary: ... @property def metadata(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[opentelemetry.proto.common.v1.common_pb2.KeyValue]: """Additional metadata attributes that describe the metric. [Optional]. Attributes are non-identifying. Consumers SHOULD NOT need to be aware of these attributes. These attributes MAY be used to encode information allowing for lossless roundtrip translation to / from another data model. Attribute keys MUST be unique (it is not allowed to have more than one attribute with the same key). The behavior of software that receives duplicated keys can be unpredictable. """ def __init__( self, *, name: builtins.str = ..., description: builtins.str = ..., unit: builtins.str = ..., gauge: global___Gauge | None = ..., sum: global___Sum | None = ..., histogram: global___Histogram | None = ..., exponential_histogram: global___ExponentialHistogram | None = ..., summary: global___Summary | None = ..., metadata: collections.abc.Iterable[opentelemetry.proto.common.v1.common_pb2.KeyValue] | None = ..., ) -> None: ... def HasField(self, field_name: typing_extensions.Literal["data", b"data", "exponential_histogram", b"exponential_histogram", "gauge", b"gauge", "histogram", b"histogram", "sum", b"sum", "summary", b"summary"]) -> builtins.bool: ... def ClearField(self, field_name: typing_extensions.Literal["data", b"data", "description", b"description", "exponential_histogram", b"exponential_histogram", "gauge", b"gauge", "histogram", b"histogram", "metadata", b"metadata", "name", b"name", "sum", b"sum", "summary", b"summary", "unit", b"unit"]) -> None: ... def WhichOneof(self, oneof_group: typing_extensions.Literal["data", b"data"]) -> typing_extensions.Literal["gauge", "sum", "histogram", "exponential_histogram", "summary"] | None: ... global___Metric = Metric @typing_extensions.final class Gauge(google.protobuf.message.Message): """Gauge represents the type of a scalar metric that always exports the "current value" for every data point. It should be used for an "unknown" aggregation. A Gauge does not support different aggregation temporalities. Given the aggregation is unknown, points cannot be combined using the same aggregation, regardless of aggregation temporalities. Therefore, AggregationTemporality is not included. Consequently, this also means "StartTimeUnixNano" is ignored for all data points. """ DESCRIPTOR: google.protobuf.descriptor.Descriptor DATA_POINTS_FIELD_NUMBER: builtins.int @property def data_points(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___NumberDataPoint]: """The time series data points. Note: Multiple time series may be included (same timestamp, different attributes). """ def __init__( self, *, data_points: collections.abc.Iterable[global___NumberDataPoint] | None = ..., ) -> None: ... def ClearField(self, field_name: typing_extensions.Literal["data_points", b"data_points"]) -> None: ... global___Gauge = Gauge @typing_extensions.final class Sum(google.protobuf.message.Message): """Sum represents the type of a scalar metric that is calculated as a sum of all reported measurements over a time interval. """ DESCRIPTOR: google.protobuf.descriptor.Descriptor DATA_POINTS_FIELD_NUMBER: builtins.int AGGREGATION_TEMPORALITY_FIELD_NUMBER: builtins.int IS_MONOTONIC_FIELD_NUMBER: builtins.int @property def data_points(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___NumberDataPoint]: """The time series data points. Note: Multiple time series may be included (same timestamp, different attributes). """ aggregation_temporality: global___AggregationTemporality.ValueType """aggregation_temporality describes if the aggregator reports delta changes since last report time, or cumulative changes since a fixed start time. """ is_monotonic: builtins.bool """Represents whether the sum is monotonic.""" def __init__( self, *, data_points: collections.abc.Iterable[global___NumberDataPoint] | None = ..., aggregation_temporality: global___AggregationTemporality.ValueType = ..., is_monotonic: builtins.bool = ..., ) -> None: ... def ClearField(self, field_name: typing_extensions.Literal["aggregation_temporality", b"aggregation_temporality", "data_points", b"data_points", "is_monotonic", b"is_monotonic"]) -> None: ... global___Sum = Sum @typing_extensions.final class Histogram(google.protobuf.message.Message): """Histogram represents the type of a metric that is calculated by aggregating as a Histogram of all reported measurements over a time interval. """ DESCRIPTOR: google.protobuf.descriptor.Descriptor DATA_POINTS_FIELD_NUMBER: builtins.int AGGREGATION_TEMPORALITY_FIELD_NUMBER: builtins.int @property def data_points(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___HistogramDataPoint]: """The time series data points. Note: Multiple time series may be included (same timestamp, different attributes). """ aggregation_temporality: global___AggregationTemporality.ValueType """aggregation_temporality describes if the aggregator reports delta changes since last report time, or cumulative changes since a fixed start time. """ def __init__( self, *, data_points: collections.abc.Iterable[global___HistogramDataPoint] | None = ..., aggregation_temporality: global___AggregationTemporality.ValueType = ..., ) -> None: ... def ClearField(self, field_name: typing_extensions.Literal["aggregation_temporality", b"aggregation_temporality", "data_points", b"data_points"]) -> None: ... global___Histogram = Histogram @typing_extensions.final class ExponentialHistogram(google.protobuf.message.Message): """ExponentialHistogram represents the type of a metric that is calculated by aggregating as a ExponentialHistogram of all reported double measurements over a time interval. """ DESCRIPTOR: google.protobuf.descriptor.Descriptor DATA_POINTS_FIELD_NUMBER: builtins.int AGGREGATION_TEMPORALITY_FIELD_NUMBER: builtins.int @property def data_points(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___ExponentialHistogramDataPoint]: """The time series data points. Note: Multiple time series may be included (same timestamp, different attributes). """ aggregation_temporality: global___AggregationTemporality.ValueType """aggregation_temporality describes if the aggregator reports delta changes since last report time, or cumulative changes since a fixed start time. """ def __init__( self, *, data_points: collections.abc.Iterable[global___ExponentialHistogramDataPoint] | None = ..., aggregation_temporality: global___AggregationTemporality.ValueType = ..., ) -> None: ... def ClearField(self, field_name: typing_extensions.Literal["aggregation_temporality", b"aggregation_temporality", "data_points", b"data_points"]) -> None: ... global___ExponentialHistogram = ExponentialHistogram @typing_extensions.final class Summary(google.protobuf.message.Message): """Summary metric data are used to convey quantile summaries, a Prometheus (see: https://prometheus.io/docs/concepts/metric_types/#summary) and OpenMetrics (see: https://github.com/prometheus/OpenMetrics/blob/4dbf6075567ab43296eed941037c12951faafb92/protos/prometheus.proto#L45) data type. These data points cannot always be merged in a meaningful way. While they can be useful in some applications, histogram data points are recommended for new applications. Summary metrics do not have an aggregation temporality field. This is because the count and sum fields of a SummaryDataPoint are assumed to be cumulative values. """ DESCRIPTOR: google.protobuf.descriptor.Descriptor DATA_POINTS_FIELD_NUMBER: builtins.int @property def data_points(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___SummaryDataPoint]: """The time series data points. Note: Multiple time series may be included (same timestamp, different attributes). """ def __init__( self, *, data_points: collections.abc.Iterable[global___SummaryDataPoint] | None = ..., ) -> None: ... def ClearField(self, field_name: typing_extensions.Literal["data_points", b"data_points"]) -> None: ... global___Summary = Summary @typing_extensions.final class NumberDataPoint(google.protobuf.message.Message): """NumberDataPoint is a single data point in a timeseries that describes the time-varying scalar value of a metric. """ DESCRIPTOR: google.protobuf.descriptor.Descriptor ATTRIBUTES_FIELD_NUMBER: builtins.int START_TIME_UNIX_NANO_FIELD_NUMBER: builtins.int TIME_UNIX_NANO_FIELD_NUMBER: builtins.int AS_DOUBLE_FIELD_NUMBER: builtins.int AS_INT_FIELD_NUMBER: builtins.int EXEMPLARS_FIELD_NUMBER: builtins.int FLAGS_FIELD_NUMBER: builtins.int @property def attributes(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[opentelemetry.proto.common.v1.common_pb2.KeyValue]: """The set of key/value pairs that uniquely identify the timeseries from where this point belongs. The list may be empty (may contain 0 elements). Attribute keys MUST be unique (it is not allowed to have more than one attribute with the same key). The behavior of software that receives duplicated keys can be unpredictable. """ start_time_unix_nano: builtins.int """StartTimeUnixNano is optional but strongly encouraged, see the the detailed comments above Metric. Value is UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January 1970. """ time_unix_nano: builtins.int """TimeUnixNano is required, see the detailed comments above Metric. Value is UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January 1970. """ as_double: builtins.float as_int: builtins.int @property def exemplars(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___Exemplar]: """(Optional) List of exemplars collected from measurements that were used to form the data point """ flags: builtins.int """Flags that apply to this specific data point. See DataPointFlags for the available flags and their meaning. """ def __init__( self, *, attributes: collections.abc.Iterable[opentelemetry.proto.common.v1.common_pb2.KeyValue] | None = ..., start_time_unix_nano: builtins.int = ..., time_unix_nano: builtins.int = ..., as_double: builtins.float = ..., as_int: builtins.int = ..., exemplars: collections.abc.Iterable[global___Exemplar] | None = ..., flags: builtins.int = ..., ) -> None: ... def HasField(self, field_name: typing_extensions.Literal["as_double", b"as_double", "as_int", b"as_int", "value", b"value"]) -> builtins.bool: ... def ClearField(self, field_name: typing_extensions.Literal["as_double", b"as_double", "as_int", b"as_int", "attributes", b"attributes", "exemplars", b"exemplars", "flags", b"flags", "start_time_unix_nano", b"start_time_unix_nano", "time_unix_nano", b"time_unix_nano", "value", b"value"]) -> None: ... def WhichOneof(self, oneof_group: typing_extensions.Literal["value", b"value"]) -> typing_extensions.Literal["as_double", "as_int"] | None: ... global___NumberDataPoint = NumberDataPoint @typing_extensions.final class HistogramDataPoint(google.protobuf.message.Message): """HistogramDataPoint is a single data point in a timeseries that describes the time-varying values of a Histogram. A Histogram contains summary statistics for a population of values, it may optionally contain the distribution of those values across a set of buckets. If the histogram contains the distribution of values, then both "explicit_bounds" and "bucket counts" fields must be defined. If the histogram does not contain the distribution of values, then both "explicit_bounds" and "bucket_counts" must be omitted and only "count" and "sum" are known. """ DESCRIPTOR: google.protobuf.descriptor.Descriptor ATTRIBUTES_FIELD_NUMBER: builtins.int START_TIME_UNIX_NANO_FIELD_NUMBER: builtins.int TIME_UNIX_NANO_FIELD_NUMBER: builtins.int COUNT_FIELD_NUMBER: builtins.int SUM_FIELD_NUMBER: builtins.int BUCKET_COUNTS_FIELD_NUMBER: builtins.int EXPLICIT_BOUNDS_FIELD_NUMBER: builtins.int EXEMPLARS_FIELD_NUMBER: builtins.int FLAGS_FIELD_NUMBER: builtins.int MIN_FIELD_NUMBER: builtins.int MAX_FIELD_NUMBER: builtins.int @property def attributes(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[opentelemetry.proto.common.v1.common_pb2.KeyValue]: """The set of key/value pairs that uniquely identify the timeseries from where this point belongs. The list may be empty (may contain 0 elements). Attribute keys MUST be unique (it is not allowed to have more than one attribute with the same key). The behavior of software that receives duplicated keys can be unpredictable. """ start_time_unix_nano: builtins.int """StartTimeUnixNano is optional but strongly encouraged, see the the detailed comments above Metric. Value is UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January 1970. """ time_unix_nano: builtins.int """TimeUnixNano is required, see the detailed comments above Metric. Value is UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January 1970. """ count: builtins.int """count is the number of values in the population. Must be non-negative. This value must be equal to the sum of the "count" fields in buckets if a histogram is provided. """ sum: builtins.float """sum of the values in the population. If count is zero then this field must be zero. Note: Sum should only be filled out when measuring non-negative discrete events, and is assumed to be monotonic over the values of these events. Negative events *can* be recorded, but sum should not be filled out when doing so. This is specifically to enforce compatibility w/ OpenMetrics, see: https://github.com/prometheus/OpenMetrics/blob/v1.0.0/specification/OpenMetrics.md#histogram """ @property def bucket_counts(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.int]: """bucket_counts is an optional field contains the count values of histogram for each bucket. The sum of the bucket_counts must equal the value in the count field. The number of elements in bucket_counts array must be by one greater than the number of elements in explicit_bounds array. The exception to this rule is when the length of bucket_counts is 0, then the length of explicit_bounds must also be 0. """ @property def explicit_bounds(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.float]: """explicit_bounds specifies buckets with explicitly defined bounds for values. The boundaries for bucket at index i are: (-infinity, explicit_bounds[i]] for i == 0 (explicit_bounds[i-1], explicit_bounds[i]] for 0 < i < size(explicit_bounds) (explicit_bounds[i-1], +infinity) for i == size(explicit_bounds) The values in the explicit_bounds array must be strictly increasing. Histogram buckets are inclusive of their upper boundary, except the last bucket where the boundary is at infinity. This format is intentionally compatible with the OpenMetrics histogram definition. If bucket_counts length is 0 then explicit_bounds length must also be 0, otherwise the data point is invalid. """ @property def exemplars(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___Exemplar]: """(Optional) List of exemplars collected from measurements that were used to form the data point """ flags: builtins.int """Flags that apply to this specific data point. See DataPointFlags for the available flags and their meaning. """ min: builtins.float """min is the minimum value over (start_time, end_time].""" max: builtins.float """max is the maximum value over (start_time, end_time].""" def __init__( self, *, attributes: collections.abc.Iterable[opentelemetry.proto.common.v1.common_pb2.KeyValue] | None = ..., start_time_unix_nano: builtins.int = ..., time_unix_nano: builtins.int = ..., count: builtins.int = ..., sum: builtins.float | None = ..., bucket_counts: collections.abc.Iterable[builtins.int] | None = ..., explicit_bounds: collections.abc.Iterable[builtins.float] | None = ..., exemplars: collections.abc.Iterable[global___Exemplar] | None = ..., flags: builtins.int = ..., min: builtins.float | None = ..., max: builtins.float | None = ..., ) -> None: ... def HasField(self, field_name: typing_extensions.Literal["_max", b"_max", "_min", b"_min", "_sum", b"_sum", "max", b"max", "min", b"min", "sum", b"sum"]) -> builtins.bool: ... def ClearField(self, field_name: typing_extensions.Literal["_max", b"_max", "_min", b"_min", "_sum", b"_sum", "attributes", b"attributes", "bucket_counts", b"bucket_counts", "count", b"count", "exemplars", b"exemplars", "explicit_bounds", b"explicit_bounds", "flags", b"flags", "max", b"max", "min", b"min", "start_time_unix_nano", b"start_time_unix_nano", "sum", b"sum", "time_unix_nano", b"time_unix_nano"]) -> None: ... @typing.overload def WhichOneof(self, oneof_group: typing_extensions.Literal["_max", b"_max"]) -> typing_extensions.Literal["max"] | None: ... @typing.overload def WhichOneof(self, oneof_group: typing_extensions.Literal["_min", b"_min"]) -> typing_extensions.Literal["min"] | None: ... @typing.overload def WhichOneof(self, oneof_group: typing_extensions.Literal["_sum", b"_sum"]) -> typing_extensions.Literal["sum"] | None: ... global___HistogramDataPoint = HistogramDataPoint @typing_extensions.final class ExponentialHistogramDataPoint(google.protobuf.message.Message): """ExponentialHistogramDataPoint is a single data point in a timeseries that describes the time-varying values of a ExponentialHistogram of double values. A ExponentialHistogram contains summary statistics for a population of values, it may optionally contain the distribution of those values across a set of buckets. """ DESCRIPTOR: google.protobuf.descriptor.Descriptor @typing_extensions.final class Buckets(google.protobuf.message.Message): """Buckets are a set of bucket counts, encoded in a contiguous array of counts. """ DESCRIPTOR: google.protobuf.descriptor.Descriptor OFFSET_FIELD_NUMBER: builtins.int BUCKET_COUNTS_FIELD_NUMBER: builtins.int offset: builtins.int """The bucket index of the first entry in the bucket_counts array. Note: This uses a varint encoding as a simple form of compression. """ @property def bucket_counts(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.int]: """An array of count values, where bucket_counts[i] carries the count of the bucket at index (offset+i). bucket_counts[i] is the count of values greater than base^(offset+i) and less than or equal to base^(offset+i+1). Note: By contrast, the explicit HistogramDataPoint uses fixed64. This field is expected to have many buckets, especially zeros, so uint64 has been selected to ensure varint encoding. """ def __init__( self, *, offset: builtins.int = ..., bucket_counts: collections.abc.Iterable[builtins.int] | None = ..., ) -> None: ... def ClearField(self, field_name: typing_extensions.Literal["bucket_counts", b"bucket_counts", "offset", b"offset"]) -> None: ... ATTRIBUTES_FIELD_NUMBER: builtins.int START_TIME_UNIX_NANO_FIELD_NUMBER: builtins.int TIME_UNIX_NANO_FIELD_NUMBER: builtins.int COUNT_FIELD_NUMBER: builtins.int SUM_FIELD_NUMBER: builtins.int SCALE_FIELD_NUMBER: builtins.int ZERO_COUNT_FIELD_NUMBER: builtins.int POSITIVE_FIELD_NUMBER: builtins.int NEGATIVE_FIELD_NUMBER: builtins.int FLAGS_FIELD_NUMBER: builtins.int EXEMPLARS_FIELD_NUMBER: builtins.int MIN_FIELD_NUMBER: builtins.int MAX_FIELD_NUMBER: builtins.int ZERO_THRESHOLD_FIELD_NUMBER: builtins.int @property def attributes(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[opentelemetry.proto.common.v1.common_pb2.KeyValue]: """The set of key/value pairs that uniquely identify the timeseries from where this point belongs. The list may be empty (may contain 0 elements). Attribute keys MUST be unique (it is not allowed to have more than one attribute with the same key). The behavior of software that receives duplicated keys can be unpredictable. """ start_time_unix_nano: builtins.int """StartTimeUnixNano is optional but strongly encouraged, see the the detailed comments above Metric. Value is UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January 1970. """ time_unix_nano: builtins.int """TimeUnixNano is required, see the detailed comments above Metric. Value is UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January 1970. """ count: builtins.int """The number of values in the population. Must be non-negative. This value must be equal to the sum of the "bucket_counts" values in the positive and negative Buckets plus the "zero_count" field. """ sum: builtins.float """The sum of the values in the population. If count is zero then this field must be zero. Note: Sum should only be filled out when measuring non-negative discrete events, and is assumed to be monotonic over the values of these events. Negative events *can* be recorded, but sum should not be filled out when doing so. This is specifically to enforce compatibility w/ OpenMetrics, see: https://github.com/prometheus/OpenMetrics/blob/v1.0.0/specification/OpenMetrics.md#histogram """ scale: builtins.int """scale describes the resolution of the histogram. Boundaries are located at powers of the base, where: base = (2^(2^-scale)) The histogram bucket identified by `index`, a signed integer, contains values that are greater than (base^index) and less than or equal to (base^(index+1)). The positive and negative ranges of the histogram are expressed separately. Negative values are mapped by their absolute value into the negative range using the same scale as the positive range. scale is not restricted by the protocol, as the permissible values depend on the range of the data. """ zero_count: builtins.int """The count of values that are either exactly zero or within the region considered zero by the instrumentation at the tolerated degree of precision. This bucket stores values that cannot be expressed using the standard exponential formula as well as values that have been rounded to zero. Implementations MAY consider the zero bucket to have probability mass equal to (zero_count / count). """ @property def positive(self) -> global___ExponentialHistogramDataPoint.Buckets: """positive carries the positive range of exponential bucket counts.""" @property def negative(self) -> global___ExponentialHistogramDataPoint.Buckets: """negative carries the negative range of exponential bucket counts.""" flags: builtins.int """Flags that apply to this specific data point. See DataPointFlags for the available flags and their meaning. """ @property def exemplars(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___Exemplar]: """(Optional) List of exemplars collected from measurements that were used to form the data point """ min: builtins.float """The minimum value over (start_time, end_time].""" max: builtins.float """The maximum value over (start_time, end_time].""" zero_threshold: builtins.float """ZeroThreshold may be optionally set to convey the width of the zero region. Where the zero region is defined as the closed interval [-ZeroThreshold, ZeroThreshold]. When ZeroThreshold is 0, zero count bucket stores values that cannot be expressed using the standard exponential formula as well as values that have been rounded to zero. """ def __init__( self, *, attributes: collections.abc.Iterable[opentelemetry.proto.common.v1.common_pb2.KeyValue] | None = ..., start_time_unix_nano: builtins.int = ..., time_unix_nano: builtins.int = ..., count: builtins.int = ..., sum: builtins.float | None = ..., scale: builtins.int = ..., zero_count: builtins.int = ..., positive: global___ExponentialHistogramDataPoint.Buckets | None = ..., negative: global___ExponentialHistogramDataPoint.Buckets | None = ..., flags: builtins.int = ..., exemplars: collections.abc.Iterable[global___Exemplar] | None = ..., min: builtins.float | None = ..., max: builtins.float | None = ..., zero_threshold: builtins.float = ..., ) -> None: ... def HasField(self, field_name: typing_extensions.Literal["_max", b"_max", "_min", b"_min", "_sum", b"_sum", "max", b"max", "min", b"min", "negative", b"negative", "positive", b"positive", "sum", b"sum"]) -> builtins.bool: ... def ClearField(self, field_name: typing_extensions.Literal["_max", b"_max", "_min", b"_min", "_sum", b"_sum", "attributes", b"attributes", "count", b"count", "exemplars", b"exemplars", "flags", b"flags", "max", b"max", "min", b"min", "negative", b"negative", "positive", b"positive", "scale", b"scale", "start_time_unix_nano", b"start_time_unix_nano", "sum", b"sum", "time_unix_nano", b"time_unix_nano", "zero_count", b"zero_count", "zero_threshold", b"zero_threshold"]) -> None: ... @typing.overload def WhichOneof(self, oneof_group: typing_extensions.Literal["_max", b"_max"]) -> typing_extensions.Literal["max"] | None: ... @typing.overload def WhichOneof(self, oneof_group: typing_extensions.Literal["_min", b"_min"]) -> typing_extensions.Literal["min"] | None: ... @typing.overload def WhichOneof(self, oneof_group: typing_extensions.Literal["_sum", b"_sum"]) -> typing_extensions.Literal["sum"] | None: ... global___ExponentialHistogramDataPoint = ExponentialHistogramDataPoint @typing_extensions.final class SummaryDataPoint(google.protobuf.message.Message): """SummaryDataPoint is a single data point in a timeseries that describes the time-varying values of a Summary metric. The count and sum fields represent cumulative values. """ DESCRIPTOR: google.protobuf.descriptor.Descriptor @typing_extensions.final class ValueAtQuantile(google.protobuf.message.Message): """Represents the value at a given quantile of a distribution. To record Min and Max values following conventions are used: - The 1.0 quantile is equivalent to the maximum value observed. - The 0.0 quantile is equivalent to the minimum value observed. See the following issue for more context: https://github.com/open-telemetry/opentelemetry-proto/issues/125 """ DESCRIPTOR: google.protobuf.descriptor.Descriptor QUANTILE_FIELD_NUMBER: builtins.int VALUE_FIELD_NUMBER: builtins.int quantile: builtins.float """The quantile of a distribution. Must be in the interval [0.0, 1.0]. """ value: builtins.float """The value at the given quantile of a distribution. Quantile values must NOT be negative. """ def __init__( self, *, quantile: builtins.float = ..., value: builtins.float = ..., ) -> None: ... def ClearField(self, field_name: typing_extensions.Literal["quantile", b"quantile", "value", b"value"]) -> None: ... ATTRIBUTES_FIELD_NUMBER: builtins.int START_TIME_UNIX_NANO_FIELD_NUMBER: builtins.int TIME_UNIX_NANO_FIELD_NUMBER: builtins.int COUNT_FIELD_NUMBER: builtins.int SUM_FIELD_NUMBER: builtins.int QUANTILE_VALUES_FIELD_NUMBER: builtins.int FLAGS_FIELD_NUMBER: builtins.int @property def attributes(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[opentelemetry.proto.common.v1.common_pb2.KeyValue]: """The set of key/value pairs that uniquely identify the timeseries from where this point belongs. The list may be empty (may contain 0 elements). Attribute keys MUST be unique (it is not allowed to have more than one attribute with the same key). The behavior of software that receives duplicated keys can be unpredictable. """ start_time_unix_nano: builtins.int """StartTimeUnixNano is optional but strongly encouraged, see the the detailed comments above Metric. Value is UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January 1970. """ time_unix_nano: builtins.int """TimeUnixNano is required, see the detailed comments above Metric. Value is UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January 1970. """ count: builtins.int """count is the number of values in the population. Must be non-negative.""" sum: builtins.float """sum of the values in the population. If count is zero then this field must be zero. Note: Sum should only be filled out when measuring non-negative discrete events, and is assumed to be monotonic over the values of these events. Negative events *can* be recorded, but sum should not be filled out when doing so. This is specifically to enforce compatibility w/ OpenMetrics, see: https://github.com/prometheus/OpenMetrics/blob/v1.0.0/specification/OpenMetrics.md#summary """ @property def quantile_values(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___SummaryDataPoint.ValueAtQuantile]: """(Optional) list of values at different quantiles of the distribution calculated from the current snapshot. The quantiles must be strictly increasing. """ flags: builtins.int """Flags that apply to this specific data point. See DataPointFlags for the available flags and their meaning. """ def __init__( self, *, attributes: collections.abc.Iterable[opentelemetry.proto.common.v1.common_pb2.KeyValue] | None = ..., start_time_unix_nano: builtins.int = ..., time_unix_nano: builtins.int = ..., count: builtins.int = ..., sum: builtins.float = ..., quantile_values: collections.abc.Iterable[global___SummaryDataPoint.ValueAtQuantile] | None = ..., flags: builtins.int = ..., ) -> None: ... def ClearField(self, field_name: typing_extensions.Literal["attributes", b"attributes", "count", b"count", "flags", b"flags", "quantile_values", b"quantile_values", "start_time_unix_nano", b"start_time_unix_nano", "sum", b"sum", "time_unix_nano", b"time_unix_nano"]) -> None: ... global___SummaryDataPoint = SummaryDataPoint @typing_extensions.final class Exemplar(google.protobuf.message.Message): """A representation of an exemplar, which is a sample input measurement. Exemplars also hold information about the environment when the measurement was recorded, for example the span and trace ID of the active span when the exemplar was recorded. """ DESCRIPTOR: google.protobuf.descriptor.Descriptor FILTERED_ATTRIBUTES_FIELD_NUMBER: builtins.int TIME_UNIX_NANO_FIELD_NUMBER: builtins.int AS_DOUBLE_FIELD_NUMBER: builtins.int AS_INT_FIELD_NUMBER: builtins.int SPAN_ID_FIELD_NUMBER: builtins.int TRACE_ID_FIELD_NUMBER: builtins.int @property def filtered_attributes(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[opentelemetry.proto.common.v1.common_pb2.KeyValue]: """The set of key/value pairs that were filtered out by the aggregator, but recorded alongside the original measurement. Only key/value pairs that were filtered out by the aggregator should be included """ time_unix_nano: builtins.int """time_unix_nano is the exact time when this exemplar was recorded Value is UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January 1970. """ as_double: builtins.float as_int: builtins.int span_id: builtins.bytes """(Optional) Span ID of the exemplar trace. span_id may be missing if the measurement is not recorded inside a trace or if the trace is not sampled. """ trace_id: builtins.bytes """(Optional) Trace ID of the exemplar trace. trace_id may be missing if the measurement is not recorded inside a trace or if the trace is not sampled. """ def __init__( self, *, filtered_attributes: collections.abc.Iterable[opentelemetry.proto.common.v1.common_pb2.KeyValue] | None = ..., time_unix_nano: builtins.int = ..., as_double: builtins.float = ..., as_int: builtins.int = ..., span_id: builtins.bytes = ..., trace_id: builtins.bytes = ..., ) -> None: ... def HasField(self, field_name: typing_extensions.Literal["as_double", b"as_double", "as_int", b"as_int", "value", b"value"]) -> builtins.bool: ... def ClearField(self, field_name: typing_extensions.Literal["as_double", b"as_double", "as_int", b"as_int", "filtered_attributes", b"filtered_attributes", "span_id", b"span_id", "time_unix_nano", b"time_unix_nano", "trace_id", b"trace_id", "value", b"value"]) -> None: ... def WhichOneof(self, oneof_group: typing_extensions.Literal["value", b"value"]) -> typing_extensions.Literal["as_double", "as_int"] | None: ... global___Exemplar = Exemplar python-opentelemetry-1.45.2/opentelemetry-proto/src/opentelemetry/proto/profiles/000077500000000000000000000000001527070070700305315ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-proto/src/opentelemetry/proto/profiles/v1development/000077500000000000000000000000001527070070700333225ustar00rootroot00000000000000profiles_pb2.py000066400000000000000000000153051527070070700362070ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-proto/src/opentelemetry/proto/profiles/v1development# -*- coding: utf-8 -*- # Generated by the protocol buffer compiler. DO NOT EDIT! # source: opentelemetry/proto/profiles/v1development/profiles.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 opentelemetry.proto.common.v1 import common_pb2 as opentelemetry_dot_proto_dot_common_dot_v1_dot_common__pb2 from opentelemetry.proto.resource.v1 import resource_pb2 as opentelemetry_dot_proto_dot_resource_dot_v1_dot_resource__pb2 DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n9opentelemetry/proto/profiles/v1development/profiles.proto\x12*opentelemetry.proto.profiles.v1development\x1a*opentelemetry/proto/common/v1/common.proto\x1a.opentelemetry/proto/resource/v1/resource.proto\"\xf6\x03\n\x12ProfilesDictionary\x12J\n\rmapping_table\x18\x01 \x03(\x0b\x32\x33.opentelemetry.proto.profiles.v1development.Mapping\x12L\n\x0elocation_table\x18\x02 \x03(\x0b\x32\x34.opentelemetry.proto.profiles.v1development.Location\x12L\n\x0e\x66unction_table\x18\x03 \x03(\x0b\x32\x34.opentelemetry.proto.profiles.v1development.Function\x12\x44\n\nlink_table\x18\x04 \x03(\x0b\x32\x30.opentelemetry.proto.profiles.v1development.Link\x12\x14\n\x0cstring_table\x18\x05 \x03(\t\x12T\n\x0f\x61ttribute_table\x18\x06 \x03(\x0b\x32;.opentelemetry.proto.profiles.v1development.KeyValueAndUnit\x12\x46\n\x0bstack_table\x18\x07 \x03(\x0b\x32\x31.opentelemetry.proto.profiles.v1development.Stack\"\xbb\x01\n\x0cProfilesData\x12W\n\x11resource_profiles\x18\x01 \x03(\x0b\x32<.opentelemetry.proto.profiles.v1development.ResourceProfiles\x12R\n\ndictionary\x18\x02 \x01(\x0b\x32>.opentelemetry.proto.profiles.v1development.ProfilesDictionary\"\xbe\x01\n\x10ResourceProfiles\x12;\n\x08resource\x18\x01 \x01(\x0b\x32).opentelemetry.proto.resource.v1.Resource\x12Q\n\x0escope_profiles\x18\x02 \x03(\x0b\x32\x39.opentelemetry.proto.profiles.v1development.ScopeProfiles\x12\x12\n\nschema_url\x18\x03 \x01(\tJ\x06\x08\xe8\x07\x10\xe9\x07\"\xae\x01\n\rScopeProfiles\x12\x42\n\x05scope\x18\x01 \x01(\x0b\x32\x33.opentelemetry.proto.common.v1.InstrumentationScope\x12\x45\n\x08profiles\x18\x02 \x03(\x0b\x32\x33.opentelemetry.proto.profiles.v1development.Profile\x12\x12\n\nschema_url\x18\x03 \x01(\t\"\xb1\x03\n\x07Profile\x12J\n\x0bsample_type\x18\x01 \x01(\x0b\x32\x35.opentelemetry.proto.profiles.v1development.ValueType\x12\x43\n\x07samples\x18\x02 \x03(\x0b\x32\x32.opentelemetry.proto.profiles.v1development.Sample\x12\x16\n\x0etime_unix_nano\x18\x03 \x01(\x06\x12\x15\n\rduration_nano\x18\x04 \x01(\x04\x12J\n\x0bperiod_type\x18\x05 \x01(\x0b\x32\x35.opentelemetry.proto.profiles.v1development.ValueType\x12\x0e\n\x06period\x18\x06 \x01(\x03\x12\x12\n\nprofile_id\x18\x07 \x01(\x0c\x12 \n\x18\x64ropped_attributes_count\x18\x08 \x01(\r\x12\x1f\n\x17original_payload_format\x18\t \x01(\t\x12\x18\n\x10original_payload\x18\n \x01(\x0c\x12\x19\n\x11\x61ttribute_indices\x18\x0b \x03(\x05\")\n\x04Link\x12\x10\n\x08trace_id\x18\x01 \x01(\x0c\x12\x0f\n\x07span_id\x18\x02 \x01(\x0c\"9\n\tValueType\x12\x15\n\rtype_strindex\x18\x01 \x01(\x05\x12\x15\n\runit_strindex\x18\x02 \x01(\x05\"z\n\x06Sample\x12\x13\n\x0bstack_index\x18\x01 \x01(\x05\x12\x19\n\x11\x61ttribute_indices\x18\x02 \x03(\x05\x12\x12\n\nlink_index\x18\x03 \x01(\x05\x12\x0e\n\x06values\x18\x04 \x03(\x03\x12\x1c\n\x14timestamps_unix_nano\x18\x05 \x03(\x06\"\x80\x01\n\x07Mapping\x12\x14\n\x0cmemory_start\x18\x01 \x01(\x04\x12\x14\n\x0cmemory_limit\x18\x02 \x01(\x04\x12\x13\n\x0b\x66ile_offset\x18\x03 \x01(\x04\x12\x19\n\x11\x66ilename_strindex\x18\x04 \x01(\x05\x12\x19\n\x11\x61ttribute_indices\x18\x05 \x03(\x05\"!\n\x05Stack\x12\x18\n\x10location_indices\x18\x01 \x03(\x05\"\x8e\x01\n\x08Location\x12\x15\n\rmapping_index\x18\x01 \x01(\x05\x12\x0f\n\x07\x61\x64\x64ress\x18\x02 \x01(\x04\x12?\n\x05lines\x18\x03 \x03(\x0b\x32\x30.opentelemetry.proto.profiles.v1development.Line\x12\x19\n\x11\x61ttribute_indices\x18\x04 \x03(\x05\"<\n\x04Line\x12\x16\n\x0e\x66unction_index\x18\x01 \x01(\x05\x12\x0c\n\x04line\x18\x02 \x01(\x03\x12\x0e\n\x06\x63olumn\x18\x03 \x01(\x03\"n\n\x08\x46unction\x12\x15\n\rname_strindex\x18\x01 \x01(\x05\x12\x1c\n\x14system_name_strindex\x18\x02 \x01(\x05\x12\x19\n\x11\x66ilename_strindex\x18\x03 \x01(\x05\x12\x12\n\nstart_line\x18\x04 \x01(\x03\"v\n\x0fKeyValueAndUnit\x12\x14\n\x0ckey_strindex\x18\x01 \x01(\x05\x12\x36\n\x05value\x18\x02 \x01(\x0b\x32\'.opentelemetry.proto.common.v1.AnyValue\x12\x15\n\runit_strindex\x18\x03 \x01(\x05\x42\xa4\x01\n-io.opentelemetry.proto.profiles.v1developmentB\rProfilesProtoP\x01Z5go.opentelemetry.io/proto/otlp/profiles/v1development\xaa\x02*OpenTelemetry.Proto.Profiles.V1Developmentb\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'opentelemetry.proto.profiles.v1development.profiles_pb2', _globals) if not _descriptor._USE_C_DESCRIPTORS: _globals['DESCRIPTOR']._loaded_options = None _globals['DESCRIPTOR']._serialized_options = b'\n-io.opentelemetry.proto.profiles.v1developmentB\rProfilesProtoP\001Z5go.opentelemetry.io/proto/otlp/profiles/v1development\252\002*OpenTelemetry.Proto.Profiles.V1Development' _globals['_PROFILESDICTIONARY']._serialized_start=198 _globals['_PROFILESDICTIONARY']._serialized_end=700 _globals['_PROFILESDATA']._serialized_start=703 _globals['_PROFILESDATA']._serialized_end=890 _globals['_RESOURCEPROFILES']._serialized_start=893 _globals['_RESOURCEPROFILES']._serialized_end=1083 _globals['_SCOPEPROFILES']._serialized_start=1086 _globals['_SCOPEPROFILES']._serialized_end=1260 _globals['_PROFILE']._serialized_start=1263 _globals['_PROFILE']._serialized_end=1696 _globals['_LINK']._serialized_start=1698 _globals['_LINK']._serialized_end=1739 _globals['_VALUETYPE']._serialized_start=1741 _globals['_VALUETYPE']._serialized_end=1798 _globals['_SAMPLE']._serialized_start=1800 _globals['_SAMPLE']._serialized_end=1922 _globals['_MAPPING']._serialized_start=1925 _globals['_MAPPING']._serialized_end=2053 _globals['_STACK']._serialized_start=2055 _globals['_STACK']._serialized_end=2088 _globals['_LOCATION']._serialized_start=2091 _globals['_LOCATION']._serialized_end=2233 _globals['_LINE']._serialized_start=2235 _globals['_LINE']._serialized_end=2295 _globals['_FUNCTION']._serialized_start=2297 _globals['_FUNCTION']._serialized_end=2407 _globals['_KEYVALUEANDUNIT']._serialized_start=2409 _globals['_KEYVALUEANDUNIT']._serialized_end=2527 # @@protoc_insertion_point(module_scope) profiles_pb2.pyi000066400000000000000000001121751527070070700363630ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-proto/src/opentelemetry/proto/profiles/v1development""" @generated by mypy-protobuf. Do not edit manually! isort:skip_file Copyright 2023, 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 file includes work covered by the following copyright and permission notices: Copyright 2016 Google Inc. All Rights Reserved. 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. """ import builtins import collections.abc import google.protobuf.descriptor import google.protobuf.internal.containers import google.protobuf.message import opentelemetry.proto.common.v1.common_pb2 import opentelemetry.proto.resource.v1.resource_pb2 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 ProfilesDictionary(google.protobuf.message.Message): """ Relationships Diagram ┌──────────────────� LEGEND │ ProfilesData │ ─────� └──────────────────┘ │ ─────▶ embedded │ │ │ 1-n │ ─────▷ referenced by index ▼ ▼ ┌──────────────────� ┌────────────────────� │ ResourceProfiles │ │ ProfilesDictionary │ └──────────────────┘ └────────────────────┘ │ │ 1-n ▼ ┌──────────────────� │ ScopeProfiles │ └──────────────────┘ │ │ 1-n ▼ ┌──────────────────� │ Profile │ └──────────────────┘ │ n-1 │ 1-n ┌───────────────────────────────────────� ▼ │ ▽ ┌──────────────────� 1-n ┌─────────────────� ┌──────────� │ Sample │ ──────▷ │ KeyValueAndUnit │ │ Link │ └──────────────────┘ └─────────────────┘ └──────────┘ │ △ △ │ n-1 │ │ 1-n ▽ │ │ ┌──────────────────� │ │ │ Stack │ │ │ └──────────────────┘ │ │ │ 1-n │ │ │ 1-n ┌────────────────┘ │ ▽ │ │ ┌──────────────────� n-1 ┌─────────────� │ Location │ ──────▷ │ Mapping │ └──────────────────┘ └─────────────┘ │ │ 1-n ▼ ┌──────────────────� │ Line │ └──────────────────┘ │ │ 1-1 ▽ ┌──────────────────� │ Function │ └──────────────────┘ ProfilesDictionary represents the profiles data shared across the entire message being sent. The following applies to all fields in this message: - A dictionary is an array of dictionary items. Users of the dictionary compactly reference the items using the index within the array. - A dictionary MUST have a zero value encoded as the first element. This allows for _index fields pointing into the dictionary to use a 0 pointer value to indicate 'null' / 'not set'. Unless otherwise defined, a 'zero value' message value is one with all default field values, so as to minimize wire encoded size. - There SHOULD NOT be dupes in a dictionary. The identity of dictionary items is based on their value, recursively as needed. If a particular implementation does emit duplicated items, it MUST NOT attempt to give them meaning based on the index or order. A profile processor may remove duplicate items and this MUST NOT have any observable effects for consumers. - There SHOULD NOT be orphaned (unreferenced) items in a dictionary. A profile processor may remove ("garbage-collect") orphaned items and this MUST NOT have any observable effects for consumers. """ DESCRIPTOR: google.protobuf.descriptor.Descriptor MAPPING_TABLE_FIELD_NUMBER: builtins.int LOCATION_TABLE_FIELD_NUMBER: builtins.int FUNCTION_TABLE_FIELD_NUMBER: builtins.int LINK_TABLE_FIELD_NUMBER: builtins.int STRING_TABLE_FIELD_NUMBER: builtins.int ATTRIBUTE_TABLE_FIELD_NUMBER: builtins.int STACK_TABLE_FIELD_NUMBER: builtins.int @property def mapping_table(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___Mapping]: """Mappings from address ranges to the image/binary/library mapped into that address range referenced by locations via Location.mapping_index. mapping_table[0] must always be zero value (Mapping{}) and present. """ @property def location_table(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___Location]: """Locations referenced by samples via Stack.location_indices. location_table[0] must always be zero value (Location{}) and present. """ @property def function_table(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___Function]: """Functions referenced by locations via Line.function_index. function_table[0] must always be zero value (Function{}) and present. """ @property def link_table(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___Link]: """Links referenced by samples via Sample.link_index. link_table[0] must always be zero value (Link{}) and present. """ @property def string_table(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.str]: """A common table for strings referenced by various messages. string_table[0] must always be "" and present. """ @property def attribute_table(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___KeyValueAndUnit]: """A common table for attributes referenced by the Profile, Sample, Mapping and Location messages below through attribute_indices field. Each entry is a key/value pair with an optional unit. Since this is a dictionary table, multiple entries with the same key may be present, unlike direct attribute tables like Resource.attributes. The referencing attribute_indices fields, though, do maintain the key uniqueness requirement. It's recommended to use attributes for variables with bounded cardinality, such as categorical variables (https://en.wikipedia.org/wiki/Categorical_variable). Using an attribute of a floating point type (e.g., CPU time) in a sample can quickly make every attribute value unique, defeating the purpose of the dictionary and impractically increasing the profile size. Examples of attributes: "/http/user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36" "abc.com/myattribute": true "allocation_size": 128 bytes attribute_table[0] must always be zero value (KeyValueAndUnit{}) and present. """ @property def stack_table(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___Stack]: """Stacks referenced by samples via Sample.stack_index. stack_table[0] must always be zero value (Stack{}) and present. """ def __init__( self, *, mapping_table: collections.abc.Iterable[global___Mapping] | None = ..., location_table: collections.abc.Iterable[global___Location] | None = ..., function_table: collections.abc.Iterable[global___Function] | None = ..., link_table: collections.abc.Iterable[global___Link] | None = ..., string_table: collections.abc.Iterable[builtins.str] | None = ..., attribute_table: collections.abc.Iterable[global___KeyValueAndUnit] | None = ..., stack_table: collections.abc.Iterable[global___Stack] | None = ..., ) -> None: ... def ClearField(self, field_name: typing_extensions.Literal["attribute_table", b"attribute_table", "function_table", b"function_table", "link_table", b"link_table", "location_table", b"location_table", "mapping_table", b"mapping_table", "stack_table", b"stack_table", "string_table", b"string_table"]) -> None: ... global___ProfilesDictionary = ProfilesDictionary @typing_extensions.final class ProfilesData(google.protobuf.message.Message): """ProfilesData represents the profiles data that can be stored in persistent storage, OR can be embedded by other protocols that transfer OTLP profiles data but do not implement the OTLP protocol. The main difference between this message and collector protocol is that in this message there will not be any "control" or "metadata" specific to OTLP protocol. When new fields are added into this message, the OTLP request MUST be updated as well. """ DESCRIPTOR: google.protobuf.descriptor.Descriptor RESOURCE_PROFILES_FIELD_NUMBER: builtins.int DICTIONARY_FIELD_NUMBER: builtins.int @property def resource_profiles(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___ResourceProfiles]: """An array of ResourceProfiles. For data coming from an SDK profiler, this array will typically contain one element. Host-level profilers will usually create one ResourceProfile per container, as well as one additional ResourceProfile grouping all samples from non-containerized processes. Other resource groupings are possible as well and clarified via Resource.attributes and semantic conventions. Tools that visualize profiles should prefer displaying resources_profiles[0].scope_profiles[0].profiles[0] by default. """ @property def dictionary(self) -> global___ProfilesDictionary: """One instance of ProfilesDictionary""" def __init__( self, *, resource_profiles: collections.abc.Iterable[global___ResourceProfiles] | None = ..., dictionary: global___ProfilesDictionary | None = ..., ) -> None: ... def HasField(self, field_name: typing_extensions.Literal["dictionary", b"dictionary"]) -> builtins.bool: ... def ClearField(self, field_name: typing_extensions.Literal["dictionary", b"dictionary", "resource_profiles", b"resource_profiles"]) -> None: ... global___ProfilesData = ProfilesData @typing_extensions.final class ResourceProfiles(google.protobuf.message.Message): """A collection of ScopeProfiles from a Resource.""" DESCRIPTOR: google.protobuf.descriptor.Descriptor RESOURCE_FIELD_NUMBER: builtins.int SCOPE_PROFILES_FIELD_NUMBER: builtins.int SCHEMA_URL_FIELD_NUMBER: builtins.int @property def resource(self) -> opentelemetry.proto.resource.v1.resource_pb2.Resource: """The resource for the profiles in this message. If this field is not set then no resource info is known. """ @property def scope_profiles(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___ScopeProfiles]: """A list of ScopeProfiles that originate from a resource.""" schema_url: builtins.str """The Schema URL, if known. This is the identifier of the Schema that the resource data is recorded in. Notably, the last part of the URL path is the version number of the schema: http[s]://server[:port]/path/. To learn more about Schema URL see https://opentelemetry.io/docs/specs/otel/schemas/#schema-url This schema_url applies to the data in the "resource" field. It does not apply to the data in the "scope_profiles" field which have their own schema_url field. """ def __init__( self, *, resource: opentelemetry.proto.resource.v1.resource_pb2.Resource | None = ..., scope_profiles: collections.abc.Iterable[global___ScopeProfiles] | None = ..., schema_url: builtins.str = ..., ) -> None: ... def HasField(self, field_name: typing_extensions.Literal["resource", b"resource"]) -> builtins.bool: ... def ClearField(self, field_name: typing_extensions.Literal["resource", b"resource", "schema_url", b"schema_url", "scope_profiles", b"scope_profiles"]) -> None: ... global___ResourceProfiles = ResourceProfiles @typing_extensions.final class ScopeProfiles(google.protobuf.message.Message): """A collection of Profiles produced by an InstrumentationScope.""" DESCRIPTOR: google.protobuf.descriptor.Descriptor SCOPE_FIELD_NUMBER: builtins.int PROFILES_FIELD_NUMBER: builtins.int SCHEMA_URL_FIELD_NUMBER: builtins.int @property def scope(self) -> opentelemetry.proto.common.v1.common_pb2.InstrumentationScope: """The instrumentation scope information for the profiles in this message. Semantically when InstrumentationScope isn't set, it is equivalent with an empty instrumentation scope name (unknown). """ @property def profiles(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___Profile]: """A list of Profiles that originate from an instrumentation scope.""" schema_url: builtins.str """The Schema URL, if known. This is the identifier of the Schema that the profile data is recorded in. Notably, the last part of the URL path is the version number of the schema: http[s]://server[:port]/path/. To learn more about Schema URL see https://opentelemetry.io/docs/specs/otel/schemas/#schema-url This schema_url applies to the data in the "scope" field and all profiles in the "profiles" field. """ def __init__( self, *, scope: opentelemetry.proto.common.v1.common_pb2.InstrumentationScope | None = ..., profiles: collections.abc.Iterable[global___Profile] | None = ..., schema_url: builtins.str = ..., ) -> None: ... def HasField(self, field_name: typing_extensions.Literal["scope", b"scope"]) -> builtins.bool: ... def ClearField(self, field_name: typing_extensions.Literal["profiles", b"profiles", "schema_url", b"schema_url", "scope", b"scope"]) -> None: ... global___ScopeProfiles = ScopeProfiles @typing_extensions.final class Profile(google.protobuf.message.Message): """Profile is a common stacktrace profile format. Measurements represented with this format should follow the following conventions: - Consumers should treat unset optional fields as if they had been set with their default value. - When possible, measurements should be stored in "unsampled" form that is most useful to humans. There should be enough information present to determine the original sampled values. - The profile is represented as a set of samples, where each sample references a stack trace which is a list of locations, each belonging to a mapping. - There is a N->1 relationship from Stack.location_indices entries to locations. For every Stack.location_indices entry there must be a unique Location with that index. - There is an optional N->1 relationship from locations to mappings. For every nonzero Location.mapping_id there must be a unique Mapping with that index. Represents a complete profile, including sample types, samples, mappings to binaries, stacks, locations, functions, string table, and additional metadata. It modifies and annotates pprof Profile with OpenTelemetry specific fields. Note that whilst fields in this message retain the name and field id from pprof in most cases for ease of understanding data migration, it is not intended that pprof:Profile and OpenTelemetry:Profile encoding be wire compatible. """ DESCRIPTOR: google.protobuf.descriptor.Descriptor SAMPLE_TYPE_FIELD_NUMBER: builtins.int SAMPLES_FIELD_NUMBER: builtins.int TIME_UNIX_NANO_FIELD_NUMBER: builtins.int DURATION_NANO_FIELD_NUMBER: builtins.int PERIOD_TYPE_FIELD_NUMBER: builtins.int PERIOD_FIELD_NUMBER: builtins.int PROFILE_ID_FIELD_NUMBER: builtins.int DROPPED_ATTRIBUTES_COUNT_FIELD_NUMBER: builtins.int ORIGINAL_PAYLOAD_FORMAT_FIELD_NUMBER: builtins.int ORIGINAL_PAYLOAD_FIELD_NUMBER: builtins.int ATTRIBUTE_INDICES_FIELD_NUMBER: builtins.int @property def sample_type(self) -> global___ValueType: """The type and unit of all Sample.values in this profile. For a cpu or off-cpu profile this might be: ["cpu","nanoseconds"] or ["off_cpu","nanoseconds"] For a heap profile, this might be: ["allocated_objects","count"] or ["allocated_space","bytes"], """ @property def samples(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___Sample]: """The set of samples recorded in this profile.""" time_unix_nano: builtins.int """The following fields 3-12 are informational, do not affect interpretation of results. Time of collection. Value is UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January 1970. """ duration_nano: builtins.int """Duration of the profile. For instant profiles like live heap snapshot, the duration can be zero but it may be preferable to set time_unix_nano to the process start time and duration_nano to the relative time when the profile was gathered. This ensures Sample.timestamps_unix_nano values such as allocation timestamp fall into the profile time range. """ @property def period_type(self) -> global___ValueType: """The kind of events between sampled occurrences. e.g [ "cpu","cycles" ] or [ "heap","bytes" ] """ period: builtins.int """The number of events between sampled occurrences.""" profile_id: builtins.bytes """A globally unique identifier for a profile. The ID is a 16-byte array. An ID with all zeroes is considered invalid. It may be used for deduplication and signal correlation purposes. It is acceptable to treat two profiles with different values in this field as not equal, even if they represented the same object at an earlier time. This field is optional; an ID may be assigned to an ID-less profile in a later step. """ dropped_attributes_count: builtins.int """The number of attributes that were discarded. Attributes can be discarded because their keys are too long or because there are too many attributes. If this value is 0, then no attributes were dropped. """ original_payload_format: builtins.str """The original payload format. See also original_payload. Optional, but the format and the bytes must be set or unset together. The allowed values for the format string are defined by the OpenTelemetry specification. Some examples are "jfr", "pprof", "linux_perf". The original payload may be optionally provided when the conversion to the OLTP format was done from a different format with some loss of the fidelity and the receiver may want to store the original payload to allow future lossless export or reinterpretation. Some examples of the original format are JFR (Java Flight Recorder), pprof, Linux perf. Even when the original payload is in a format that is semantically close to OTLP, such as pprof, a conversion may still be lossy in some cases (e.g. if the pprof file contains custom extensions or conventions). The original payload can be large in size, so including the original payload should be configurable by the profiler or collector options. The default behavior should be to not include the original payload. """ original_payload: builtins.bytes """The original payload bytes. See also original_payload_format. Optional, but format and the bytes must be set or unset together. """ @property def attribute_indices(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.int]: """References to attributes in attribute_table. [optional]""" def __init__( self, *, sample_type: global___ValueType | None = ..., samples: collections.abc.Iterable[global___Sample] | None = ..., time_unix_nano: builtins.int = ..., duration_nano: builtins.int = ..., period_type: global___ValueType | None = ..., period: builtins.int = ..., profile_id: builtins.bytes = ..., dropped_attributes_count: builtins.int = ..., original_payload_format: builtins.str = ..., original_payload: builtins.bytes = ..., attribute_indices: collections.abc.Iterable[builtins.int] | None = ..., ) -> None: ... def HasField(self, field_name: typing_extensions.Literal["period_type", b"period_type", "sample_type", b"sample_type"]) -> builtins.bool: ... def ClearField(self, field_name: typing_extensions.Literal["attribute_indices", b"attribute_indices", "dropped_attributes_count", b"dropped_attributes_count", "duration_nano", b"duration_nano", "original_payload", b"original_payload", "original_payload_format", b"original_payload_format", "period", b"period", "period_type", b"period_type", "profile_id", b"profile_id", "sample_type", b"sample_type", "samples", b"samples", "time_unix_nano", b"time_unix_nano"]) -> None: ... global___Profile = Profile @typing_extensions.final class Link(google.protobuf.message.Message): """A pointer from a profile Sample to a trace Span. Connects a profile sample to a trace span, identified by unique trace and span IDs. """ DESCRIPTOR: google.protobuf.descriptor.Descriptor TRACE_ID_FIELD_NUMBER: builtins.int SPAN_ID_FIELD_NUMBER: builtins.int trace_id: builtins.bytes """A unique identifier of a trace that this linked span is part of. The ID is a 16-byte array. """ span_id: builtins.bytes """A unique identifier for the linked span. The ID is an 8-byte array.""" def __init__( self, *, trace_id: builtins.bytes = ..., span_id: builtins.bytes = ..., ) -> None: ... def ClearField(self, field_name: typing_extensions.Literal["span_id", b"span_id", "trace_id", b"trace_id"]) -> None: ... global___Link = Link @typing_extensions.final class ValueType(google.protobuf.message.Message): """ValueType describes the type and units of a value.""" DESCRIPTOR: google.protobuf.descriptor.Descriptor TYPE_STRINDEX_FIELD_NUMBER: builtins.int UNIT_STRINDEX_FIELD_NUMBER: builtins.int type_strindex: builtins.int """Index into ProfilesDictionary.string_table.""" unit_strindex: builtins.int """Index into ProfilesDictionary.string_table.""" def __init__( self, *, type_strindex: builtins.int = ..., unit_strindex: builtins.int = ..., ) -> None: ... def ClearField(self, field_name: typing_extensions.Literal["type_strindex", b"type_strindex", "unit_strindex", b"unit_strindex"]) -> None: ... global___ValueType = ValueType @typing_extensions.final class Sample(google.protobuf.message.Message): """Each Sample records values encountered in some program context. The program context is typically a stack trace, perhaps augmented with auxiliary information like the thread-id, some indicator of a higher level request being handled etc. A Sample MUST have have at least one values or timestamps_unix_nano entry. If both fields are populated, they MUST contain the same number of elements, and the elements at the same index MUST refer to the same event. For the purposes of efficiently representing aggregated data observations, a Sample is regarded as having a shared identity and an associated collection of per-observation data points. Samples having the same identity SHOULD be combined by inserting timestamps and values to the data arrays. Examples of different ways ('shapes') of representing a sample with the total value of 10: Report of a stacktrace at 10 timestamps (consumers must assume the value is 1 for each point): values: [] timestamps_unix_nano: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] Report of a stacktrace with an aggregated value without timestamps: values: [10] timestamps_unix_nano: [] Report of a stacktrace at 4 timestamps where each point records a specific value: values: [2, 2, 3, 3] timestamps_unix_nano: [1, 2, 3, 4] All Samples for a Profile SHOULD have the same shape, i.e. all data observation series should consistently adopt the same data recording style. """ DESCRIPTOR: google.protobuf.descriptor.Descriptor STACK_INDEX_FIELD_NUMBER: builtins.int ATTRIBUTE_INDICES_FIELD_NUMBER: builtins.int LINK_INDEX_FIELD_NUMBER: builtins.int VALUES_FIELD_NUMBER: builtins.int TIMESTAMPS_UNIX_NANO_FIELD_NUMBER: builtins.int stack_index: builtins.int """A Sample's identity (i.e. 'primary key') is the tuple of {stack_index, set_of(attribute_indices), link_index} Reference to stack in ProfilesDictionary.stack_table. """ @property def attribute_indices(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.int]: """References to attributes in ProfilesDictionary.attribute_table. [optional]""" link_index: builtins.int """Reference to link in ProfilesDictionary.link_table. [optional] It can be unset / set to 0 if no link exists, as link_table[0] is always a 'null' default value. """ @property def values(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.int]: """The following fields may contain per-observation data and do not form part of the Sample's identity. The type and unit of each value is defined by Profile.sample_type. """ @property def timestamps_unix_nano(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.int]: """Timestamps associated with Sample. Value is UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January 1970. The timestamps should fall within the [Profile.time_unix_nano, Profile.time_unix_nano + Profile.duration_nano) time range. """ def __init__( self, *, stack_index: builtins.int = ..., attribute_indices: collections.abc.Iterable[builtins.int] | None = ..., link_index: builtins.int = ..., values: collections.abc.Iterable[builtins.int] | None = ..., timestamps_unix_nano: collections.abc.Iterable[builtins.int] | None = ..., ) -> None: ... def ClearField(self, field_name: typing_extensions.Literal["attribute_indices", b"attribute_indices", "link_index", b"link_index", "stack_index", b"stack_index", "timestamps_unix_nano", b"timestamps_unix_nano", "values", b"values"]) -> None: ... global___Sample = Sample @typing_extensions.final class Mapping(google.protobuf.message.Message): """Describes the mapping of a binary in memory, including its address range, file offset, and metadata like build ID """ DESCRIPTOR: google.protobuf.descriptor.Descriptor MEMORY_START_FIELD_NUMBER: builtins.int MEMORY_LIMIT_FIELD_NUMBER: builtins.int FILE_OFFSET_FIELD_NUMBER: builtins.int FILENAME_STRINDEX_FIELD_NUMBER: builtins.int ATTRIBUTE_INDICES_FIELD_NUMBER: builtins.int memory_start: builtins.int """Address at which the binary (or DLL) is loaded into memory.""" memory_limit: builtins.int """The limit of the address range occupied by this mapping.""" file_offset: builtins.int """Offset in the binary that corresponds to the first mapped address.""" filename_strindex: builtins.int """The object this entry is loaded from. This can be a filename on disk for the main binary and shared libraries, or virtual abstractions like "[vdso]". Index into ProfilesDictionary.string_table. """ @property def attribute_indices(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.int]: """References to attributes in ProfilesDictionary.attribute_table. [optional]""" def __init__( self, *, memory_start: builtins.int = ..., memory_limit: builtins.int = ..., file_offset: builtins.int = ..., filename_strindex: builtins.int = ..., attribute_indices: collections.abc.Iterable[builtins.int] | None = ..., ) -> None: ... def ClearField(self, field_name: typing_extensions.Literal["attribute_indices", b"attribute_indices", "file_offset", b"file_offset", "filename_strindex", b"filename_strindex", "memory_limit", b"memory_limit", "memory_start", b"memory_start"]) -> None: ... global___Mapping = Mapping @typing_extensions.final class Stack(google.protobuf.message.Message): """A Stack represents a stack trace as a list of locations.""" DESCRIPTOR: google.protobuf.descriptor.Descriptor LOCATION_INDICES_FIELD_NUMBER: builtins.int @property def location_indices(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.int]: """References to locations in ProfilesDictionary.location_table. The first location is the leaf frame. """ def __init__( self, *, location_indices: collections.abc.Iterable[builtins.int] | None = ..., ) -> None: ... def ClearField(self, field_name: typing_extensions.Literal["location_indices", b"location_indices"]) -> None: ... global___Stack = Stack @typing_extensions.final class Location(google.protobuf.message.Message): """Describes function and line table debug information.""" DESCRIPTOR: google.protobuf.descriptor.Descriptor MAPPING_INDEX_FIELD_NUMBER: builtins.int ADDRESS_FIELD_NUMBER: builtins.int LINES_FIELD_NUMBER: builtins.int ATTRIBUTE_INDICES_FIELD_NUMBER: builtins.int mapping_index: builtins.int """Reference to mapping in ProfilesDictionary.mapping_table. It can be unset / set to 0 if the mapping is unknown or not applicable for this profile type, as mapping_table[0] is always a 'null' default mapping. """ address: builtins.int """The instruction address for this location, if available. It should be within [Mapping.memory_start...Mapping.memory_limit] for the corresponding mapping. A non-leaf address may be in the middle of a call instruction. It is up to display tools to find the beginning of the instruction if necessary. """ @property def lines(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___Line]: """Multiple line indicates this location has inlined functions, where the last entry represents the caller into which the preceding entries were inlined. E.g., if memcpy() is inlined into printf: lines[0].function_name == "memcpy" lines[1].function_name == "printf" """ @property def attribute_indices(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.int]: """References to attributes in ProfilesDictionary.attribute_table. [optional]""" def __init__( self, *, mapping_index: builtins.int = ..., address: builtins.int = ..., lines: collections.abc.Iterable[global___Line] | None = ..., attribute_indices: collections.abc.Iterable[builtins.int] | None = ..., ) -> None: ... def ClearField(self, field_name: typing_extensions.Literal["address", b"address", "attribute_indices", b"attribute_indices", "lines", b"lines", "mapping_index", b"mapping_index"]) -> None: ... global___Location = Location @typing_extensions.final class Line(google.protobuf.message.Message): """Details a specific line in a source code, linked to a function.""" DESCRIPTOR: google.protobuf.descriptor.Descriptor FUNCTION_INDEX_FIELD_NUMBER: builtins.int LINE_FIELD_NUMBER: builtins.int COLUMN_FIELD_NUMBER: builtins.int function_index: builtins.int """Reference to function in ProfilesDictionary.function_table.""" line: builtins.int """Line number in source code. 0 means unset.""" column: builtins.int """Column number in source code. 0 means unset.""" def __init__( self, *, function_index: builtins.int = ..., line: builtins.int = ..., column: builtins.int = ..., ) -> None: ... def ClearField(self, field_name: typing_extensions.Literal["column", b"column", "function_index", b"function_index", "line", b"line"]) -> None: ... global___Line = Line @typing_extensions.final class Function(google.protobuf.message.Message): """Describes a function, including its human-readable name, system name, source file, and starting line number in the source. """ DESCRIPTOR: google.protobuf.descriptor.Descriptor NAME_STRINDEX_FIELD_NUMBER: builtins.int SYSTEM_NAME_STRINDEX_FIELD_NUMBER: builtins.int FILENAME_STRINDEX_FIELD_NUMBER: builtins.int START_LINE_FIELD_NUMBER: builtins.int name_strindex: builtins.int """The function name. Empty string if not available.""" system_name_strindex: builtins.int """Function name, as identified by the system. For instance, it can be a C++ mangled name. Empty string if not available. """ filename_strindex: builtins.int """Source file containing the function. Empty string if not available.""" start_line: builtins.int """Line number in source file. 0 means unset.""" def __init__( self, *, name_strindex: builtins.int = ..., system_name_strindex: builtins.int = ..., filename_strindex: builtins.int = ..., start_line: builtins.int = ..., ) -> None: ... def ClearField(self, field_name: typing_extensions.Literal["filename_strindex", b"filename_strindex", "name_strindex", b"name_strindex", "start_line", b"start_line", "system_name_strindex", b"system_name_strindex"]) -> None: ... global___Function = Function @typing_extensions.final class KeyValueAndUnit(google.protobuf.message.Message): """A custom 'dictionary native' style of encoding attributes which is more convenient for profiles than opentelemetry.proto.common.v1.KeyValue Specifically, uses the string table for keys and allows optional unit information. """ DESCRIPTOR: google.protobuf.descriptor.Descriptor KEY_STRINDEX_FIELD_NUMBER: builtins.int VALUE_FIELD_NUMBER: builtins.int UNIT_STRINDEX_FIELD_NUMBER: builtins.int key_strindex: builtins.int """The index into the string table for the attribute's key.""" @property def value(self) -> opentelemetry.proto.common.v1.common_pb2.AnyValue: """The value of the attribute.""" unit_strindex: builtins.int """The index into the string table for the attribute's unit. zero indicates implicit (by semconv) or non-defined unit. """ def __init__( self, *, key_strindex: builtins.int = ..., value: opentelemetry.proto.common.v1.common_pb2.AnyValue | None = ..., unit_strindex: builtins.int = ..., ) -> None: ... def HasField(self, field_name: typing_extensions.Literal["value", b"value"]) -> builtins.bool: ... def ClearField(self, field_name: typing_extensions.Literal["key_strindex", b"key_strindex", "unit_strindex", b"unit_strindex", "value", b"value"]) -> None: ... global___KeyValueAndUnit = KeyValueAndUnit python-opentelemetry-1.45.2/opentelemetry-proto/src/opentelemetry/proto/py.typed000066400000000000000000000000001527070070700303730ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-proto/src/opentelemetry/proto/resource/000077500000000000000000000000001527070070700305355ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-proto/src/opentelemetry/proto/resource/__init__.py000066400000000000000000000000001527070070700326340ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-proto/src/opentelemetry/proto/resource/v1/000077500000000000000000000000001527070070700310635ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-proto/src/opentelemetry/proto/resource/v1/__init__.py000066400000000000000000000000001527070070700331620ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-proto/src/opentelemetry/proto/resource/v1/resource_pb2.py000066400000000000000000000035101527070070700340260ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Generated by the protocol buffer compiler. DO NOT EDIT! # source: opentelemetry/proto/resource/v1/resource.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 opentelemetry.proto.common.v1 import common_pb2 as opentelemetry_dot_proto_dot_common_dot_v1_dot_common__pb2 DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n.opentelemetry/proto/resource/v1/resource.proto\x12\x1fopentelemetry.proto.resource.v1\x1a*opentelemetry/proto/common/v1/common.proto\"\xa8\x01\n\x08Resource\x12;\n\nattributes\x18\x01 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12 \n\x18\x64ropped_attributes_count\x18\x02 \x01(\r\x12=\n\x0b\x65ntity_refs\x18\x03 \x03(\x0b\x32(.opentelemetry.proto.common.v1.EntityRefB\x83\x01\n\"io.opentelemetry.proto.resource.v1B\rResourceProtoP\x01Z*go.opentelemetry.io/proto/otlp/resource/v1\xaa\x02\x1fOpenTelemetry.Proto.Resource.V1b\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'opentelemetry.proto.resource.v1.resource_pb2', _globals) if not _descriptor._USE_C_DESCRIPTORS: _globals['DESCRIPTOR']._loaded_options = None _globals['DESCRIPTOR']._serialized_options = b'\n\"io.opentelemetry.proto.resource.v1B\rResourceProtoP\001Z*go.opentelemetry.io/proto/otlp/resource/v1\252\002\037OpenTelemetry.Proto.Resource.V1' _globals['_RESOURCE']._serialized_start=128 _globals['_RESOURCE']._serialized_end=296 # @@protoc_insertion_point(module_scope) python-opentelemetry-1.45.2/opentelemetry-proto/src/opentelemetry/proto/resource/v1/resource_pb2.pyi000066400000000000000000000053621527070070700342060ustar00rootroot00000000000000""" @generated by mypy-protobuf. Do not edit manually! isort:skip_file Copyright 2019, 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. """ import builtins import collections.abc import google.protobuf.descriptor import google.protobuf.internal.containers import google.protobuf.message import opentelemetry.proto.common.v1.common_pb2 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 Resource(google.protobuf.message.Message): """Resource information.""" DESCRIPTOR: google.protobuf.descriptor.Descriptor ATTRIBUTES_FIELD_NUMBER: builtins.int DROPPED_ATTRIBUTES_COUNT_FIELD_NUMBER: builtins.int ENTITY_REFS_FIELD_NUMBER: builtins.int @property def attributes(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[opentelemetry.proto.common.v1.common_pb2.KeyValue]: """Set of attributes that describe the resource. Attribute keys MUST be unique (it is not allowed to have more than one attribute with the same key). The behavior of software that receives duplicated keys can be unpredictable. """ dropped_attributes_count: builtins.int """The number of dropped attributes. If the value is 0, then no attributes were dropped. """ @property def entity_refs(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[opentelemetry.proto.common.v1.common_pb2.EntityRef]: """Set of entities that participate in this Resource. Note: keys in the references MUST exist in attributes of this message. Status: [Development] """ def __init__( self, *, attributes: collections.abc.Iterable[opentelemetry.proto.common.v1.common_pb2.KeyValue] | None = ..., dropped_attributes_count: builtins.int = ..., entity_refs: collections.abc.Iterable[opentelemetry.proto.common.v1.common_pb2.EntityRef] | None = ..., ) -> None: ... def ClearField(self, field_name: typing_extensions.Literal["attributes", b"attributes", "dropped_attributes_count", b"dropped_attributes_count", "entity_refs", b"entity_refs"]) -> None: ... global___Resource = Resource python-opentelemetry-1.45.2/opentelemetry-proto/src/opentelemetry/proto/trace/000077500000000000000000000000001527070070700300045ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-proto/src/opentelemetry/proto/trace/__init__.py000066400000000000000000000000001527070070700321030ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-proto/src/opentelemetry/proto/trace/v1/000077500000000000000000000000001527070070700303325ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-proto/src/opentelemetry/proto/trace/v1/__init__.py000066400000000000000000000000001527070070700324310ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_pb2.py000066400000000000000000000125651527070070700325560ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Generated by the protocol buffer compiler. DO NOT EDIT! # source: opentelemetry/proto/trace/v1/trace.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 opentelemetry.proto.common.v1 import common_pb2 as opentelemetry_dot_proto_dot_common_dot_v1_dot_common__pb2 from opentelemetry.proto.resource.v1 import resource_pb2 as opentelemetry_dot_proto_dot_resource_dot_v1_dot_resource__pb2 DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n(opentelemetry/proto/trace/v1/trace.proto\x12\x1copentelemetry.proto.trace.v1\x1a*opentelemetry/proto/common/v1/common.proto\x1a.opentelemetry/proto/resource/v1/resource.proto\"Q\n\nTracesData\x12\x43\n\x0eresource_spans\x18\x01 \x03(\x0b\x32+.opentelemetry.proto.trace.v1.ResourceSpans\"\xa7\x01\n\rResourceSpans\x12;\n\x08resource\x18\x01 \x01(\x0b\x32).opentelemetry.proto.resource.v1.Resource\x12=\n\x0bscope_spans\x18\x02 \x03(\x0b\x32(.opentelemetry.proto.trace.v1.ScopeSpans\x12\x12\n\nschema_url\x18\x03 \x01(\tJ\x06\x08\xe8\x07\x10\xe9\x07\"\x97\x01\n\nScopeSpans\x12\x42\n\x05scope\x18\x01 \x01(\x0b\x32\x33.opentelemetry.proto.common.v1.InstrumentationScope\x12\x31\n\x05spans\x18\x02 \x03(\x0b\x32\".opentelemetry.proto.trace.v1.Span\x12\x12\n\nschema_url\x18\x03 \x01(\t\"\x84\x08\n\x04Span\x12\x10\n\x08trace_id\x18\x01 \x01(\x0c\x12\x0f\n\x07span_id\x18\x02 \x01(\x0c\x12\x13\n\x0btrace_state\x18\x03 \x01(\t\x12\x16\n\x0eparent_span_id\x18\x04 \x01(\x0c\x12\r\n\x05\x66lags\x18\x10 \x01(\x07\x12\x0c\n\x04name\x18\x05 \x01(\t\x12\x39\n\x04kind\x18\x06 \x01(\x0e\x32+.opentelemetry.proto.trace.v1.Span.SpanKind\x12\x1c\n\x14start_time_unix_nano\x18\x07 \x01(\x06\x12\x1a\n\x12\x65nd_time_unix_nano\x18\x08 \x01(\x06\x12;\n\nattributes\x18\t \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12 \n\x18\x64ropped_attributes_count\x18\n \x01(\r\x12\x38\n\x06\x65vents\x18\x0b \x03(\x0b\x32(.opentelemetry.proto.trace.v1.Span.Event\x12\x1c\n\x14\x64ropped_events_count\x18\x0c \x01(\r\x12\x36\n\x05links\x18\r \x03(\x0b\x32\'.opentelemetry.proto.trace.v1.Span.Link\x12\x1b\n\x13\x64ropped_links_count\x18\x0e \x01(\r\x12\x34\n\x06status\x18\x0f \x01(\x0b\x32$.opentelemetry.proto.trace.v1.Status\x1a\x8c\x01\n\x05\x45vent\x12\x16\n\x0etime_unix_nano\x18\x01 \x01(\x06\x12\x0c\n\x04name\x18\x02 \x01(\t\x12;\n\nattributes\x18\x03 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12 \n\x18\x64ropped_attributes_count\x18\x04 \x01(\r\x1a\xac\x01\n\x04Link\x12\x10\n\x08trace_id\x18\x01 \x01(\x0c\x12\x0f\n\x07span_id\x18\x02 \x01(\x0c\x12\x13\n\x0btrace_state\x18\x03 \x01(\t\x12;\n\nattributes\x18\x04 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12 \n\x18\x64ropped_attributes_count\x18\x05 \x01(\r\x12\r\n\x05\x66lags\x18\x06 \x01(\x07\"\x99\x01\n\x08SpanKind\x12\x19\n\x15SPAN_KIND_UNSPECIFIED\x10\x00\x12\x16\n\x12SPAN_KIND_INTERNAL\x10\x01\x12\x14\n\x10SPAN_KIND_SERVER\x10\x02\x12\x14\n\x10SPAN_KIND_CLIENT\x10\x03\x12\x16\n\x12SPAN_KIND_PRODUCER\x10\x04\x12\x16\n\x12SPAN_KIND_CONSUMER\x10\x05\"\xae\x01\n\x06Status\x12\x0f\n\x07message\x18\x02 \x01(\t\x12=\n\x04\x63ode\x18\x03 \x01(\x0e\x32/.opentelemetry.proto.trace.v1.Status.StatusCode\"N\n\nStatusCode\x12\x15\n\x11STATUS_CODE_UNSET\x10\x00\x12\x12\n\x0eSTATUS_CODE_OK\x10\x01\x12\x15\n\x11STATUS_CODE_ERROR\x10\x02J\x04\x08\x01\x10\x02*\x9c\x01\n\tSpanFlags\x12\x19\n\x15SPAN_FLAGS_DO_NOT_USE\x10\x00\x12 \n\x1bSPAN_FLAGS_TRACE_FLAGS_MASK\x10\xff\x01\x12*\n%SPAN_FLAGS_CONTEXT_HAS_IS_REMOTE_MASK\x10\x80\x02\x12&\n!SPAN_FLAGS_CONTEXT_IS_REMOTE_MASK\x10\x80\x04\x42w\n\x1fio.opentelemetry.proto.trace.v1B\nTraceProtoP\x01Z\'go.opentelemetry.io/proto/otlp/trace/v1\xaa\x02\x1cOpenTelemetry.Proto.Trace.V1b\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'opentelemetry.proto.trace.v1.trace_pb2', _globals) if not _descriptor._USE_C_DESCRIPTORS: _globals['DESCRIPTOR']._loaded_options = None _globals['DESCRIPTOR']._serialized_options = b'\n\037io.opentelemetry.proto.trace.v1B\nTraceProtoP\001Z\'go.opentelemetry.io/proto/otlp/trace/v1\252\002\034OpenTelemetry.Proto.Trace.V1' _globals['_SPANFLAGS']._serialized_start=1782 _globals['_SPANFLAGS']._serialized_end=1938 _globals['_TRACESDATA']._serialized_start=166 _globals['_TRACESDATA']._serialized_end=247 _globals['_RESOURCESPANS']._serialized_start=250 _globals['_RESOURCESPANS']._serialized_end=417 _globals['_SCOPESPANS']._serialized_start=420 _globals['_SCOPESPANS']._serialized_end=571 _globals['_SPAN']._serialized_start=574 _globals['_SPAN']._serialized_end=1602 _globals['_SPAN_EVENT']._serialized_start=1131 _globals['_SPAN_EVENT']._serialized_end=1271 _globals['_SPAN_LINK']._serialized_start=1274 _globals['_SPAN_LINK']._serialized_end=1446 _globals['_SPAN_SPANKIND']._serialized_start=1449 _globals['_SPAN_SPANKIND']._serialized_end=1602 _globals['_STATUS']._serialized_start=1605 _globals['_STATUS']._serialized_end=1779 _globals['_STATUS_STATUSCODE']._serialized_start=1695 _globals['_STATUS_STATUSCODE']._serialized_end=1773 # @@protoc_insertion_point(module_scope) python-opentelemetry-1.45.2/opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_pb2.pyi000066400000000000000000000671541527070070700327330ustar00rootroot00000000000000""" @generated by mypy-protobuf. Do not edit manually! isort:skip_file Copyright 2019, 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. """ 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 opentelemetry.proto.common.v1.common_pb2 import opentelemetry.proto.resource.v1.resource_pb2 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 _SpanFlags: ValueType = typing.NewType("ValueType", builtins.int) V: typing_extensions.TypeAlias = ValueType class _SpanFlagsEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_SpanFlags.ValueType], builtins.type): DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor SPAN_FLAGS_DO_NOT_USE: _SpanFlags.ValueType # 0 """The zero value for the enum. Should not be used for comparisons. Instead use bitwise "and" with the appropriate mask as shown above. """ SPAN_FLAGS_TRACE_FLAGS_MASK: _SpanFlags.ValueType # 255 """Bits 0-7 are used for trace flags.""" SPAN_FLAGS_CONTEXT_HAS_IS_REMOTE_MASK: _SpanFlags.ValueType # 256 """Bits 8 and 9 are used to indicate that the parent span or link span is remote. Bit 8 (`HAS_IS_REMOTE`) indicates whether the value is known. Bit 9 (`IS_REMOTE`) indicates whether the span or link is remote. """ SPAN_FLAGS_CONTEXT_IS_REMOTE_MASK: _SpanFlags.ValueType # 512 class SpanFlags(_SpanFlags, metaclass=_SpanFlagsEnumTypeWrapper): """SpanFlags represents constants used to interpret the Span.flags field, which is protobuf 'fixed32' type and is to be used as bit-fields. Each non-zero value defined in this enum is a bit-mask. To extract the bit-field, for example, use an expression like: (span.flags & SPAN_FLAGS_TRACE_FLAGS_MASK) See https://www.w3.org/TR/trace-context-2/#trace-flags for the flag definitions. Note that Span flags were introduced in version 1.1 of the OpenTelemetry protocol. Older Span producers do not set this field, consequently consumers should not rely on the absence of a particular flag bit to indicate the presence of a particular feature. """ SPAN_FLAGS_DO_NOT_USE: SpanFlags.ValueType # 0 """The zero value for the enum. Should not be used for comparisons. Instead use bitwise "and" with the appropriate mask as shown above. """ SPAN_FLAGS_TRACE_FLAGS_MASK: SpanFlags.ValueType # 255 """Bits 0-7 are used for trace flags.""" SPAN_FLAGS_CONTEXT_HAS_IS_REMOTE_MASK: SpanFlags.ValueType # 256 """Bits 8 and 9 are used to indicate that the parent span or link span is remote. Bit 8 (`HAS_IS_REMOTE`) indicates whether the value is known. Bit 9 (`IS_REMOTE`) indicates whether the span or link is remote. """ SPAN_FLAGS_CONTEXT_IS_REMOTE_MASK: SpanFlags.ValueType # 512 global___SpanFlags = SpanFlags @typing_extensions.final class TracesData(google.protobuf.message.Message): """TracesData represents the traces data that can be stored in a persistent storage, OR can be embedded by other protocols that transfer OTLP traces data but do not implement the OTLP protocol. The main difference between this message and collector protocol is that in this message there will not be any "control" or "metadata" specific to OTLP protocol. When new fields are added into this message, the OTLP request MUST be updated as well. """ DESCRIPTOR: google.protobuf.descriptor.Descriptor RESOURCE_SPANS_FIELD_NUMBER: builtins.int @property def resource_spans(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___ResourceSpans]: """An array of ResourceSpans. For data coming from a single resource this array will typically contain one element. Intermediary nodes that receive data from multiple origins typically batch the data before forwarding further and in that case this array will contain multiple elements. """ def __init__( self, *, resource_spans: collections.abc.Iterable[global___ResourceSpans] | None = ..., ) -> None: ... def ClearField(self, field_name: typing_extensions.Literal["resource_spans", b"resource_spans"]) -> None: ... global___TracesData = TracesData @typing_extensions.final class ResourceSpans(google.protobuf.message.Message): """A collection of ScopeSpans from a Resource.""" DESCRIPTOR: google.protobuf.descriptor.Descriptor RESOURCE_FIELD_NUMBER: builtins.int SCOPE_SPANS_FIELD_NUMBER: builtins.int SCHEMA_URL_FIELD_NUMBER: builtins.int @property def resource(self) -> opentelemetry.proto.resource.v1.resource_pb2.Resource: """The resource for the spans in this message. If this field is not set then no resource info is known. """ @property def scope_spans(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___ScopeSpans]: """A list of ScopeSpans that originate from a resource.""" schema_url: builtins.str """The Schema URL, if known. This is the identifier of the Schema that the resource data is recorded in. Notably, the last part of the URL path is the version number of the schema: http[s]://server[:port]/path/. To learn more about Schema URL see https://opentelemetry.io/docs/specs/otel/schemas/#schema-url This schema_url applies to the data in the "resource" field. It does not apply to the data in the "scope_spans" field which have their own schema_url field. """ def __init__( self, *, resource: opentelemetry.proto.resource.v1.resource_pb2.Resource | None = ..., scope_spans: collections.abc.Iterable[global___ScopeSpans] | None = ..., schema_url: builtins.str = ..., ) -> None: ... def HasField(self, field_name: typing_extensions.Literal["resource", b"resource"]) -> builtins.bool: ... def ClearField(self, field_name: typing_extensions.Literal["resource", b"resource", "schema_url", b"schema_url", "scope_spans", b"scope_spans"]) -> None: ... global___ResourceSpans = ResourceSpans @typing_extensions.final class ScopeSpans(google.protobuf.message.Message): """A collection of Spans produced by an InstrumentationScope.""" DESCRIPTOR: google.protobuf.descriptor.Descriptor SCOPE_FIELD_NUMBER: builtins.int SPANS_FIELD_NUMBER: builtins.int SCHEMA_URL_FIELD_NUMBER: builtins.int @property def scope(self) -> opentelemetry.proto.common.v1.common_pb2.InstrumentationScope: """The instrumentation scope information for the spans in this message. Semantically when InstrumentationScope isn't set, it is equivalent with an empty instrumentation scope name (unknown). """ @property def spans(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___Span]: """A list of Spans that originate from an instrumentation scope.""" schema_url: builtins.str """The Schema URL, if known. This is the identifier of the Schema that the span data is recorded in. Notably, the last part of the URL path is the version number of the schema: http[s]://server[:port]/path/. To learn more about Schema URL see https://opentelemetry.io/docs/specs/otel/schemas/#schema-url This schema_url applies to the data in the "scope" field and all spans and span events in the "spans" field. """ def __init__( self, *, scope: opentelemetry.proto.common.v1.common_pb2.InstrumentationScope | None = ..., spans: collections.abc.Iterable[global___Span] | None = ..., schema_url: builtins.str = ..., ) -> None: ... def HasField(self, field_name: typing_extensions.Literal["scope", b"scope"]) -> builtins.bool: ... def ClearField(self, field_name: typing_extensions.Literal["schema_url", b"schema_url", "scope", b"scope", "spans", b"spans"]) -> None: ... global___ScopeSpans = ScopeSpans @typing_extensions.final class Span(google.protobuf.message.Message): """A Span represents a single operation performed by a single component of the system. The next available field id is 17. """ DESCRIPTOR: google.protobuf.descriptor.Descriptor class _SpanKind: ValueType = typing.NewType("ValueType", builtins.int) V: typing_extensions.TypeAlias = ValueType class _SpanKindEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[Span._SpanKind.ValueType], builtins.type): DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor SPAN_KIND_UNSPECIFIED: Span._SpanKind.ValueType # 0 """Unspecified. Do NOT use as default. Implementations MAY assume SpanKind to be INTERNAL when receiving UNSPECIFIED. """ SPAN_KIND_INTERNAL: Span._SpanKind.ValueType # 1 """Indicates that the span represents an internal operation within an application, as opposed to an operation happening at the boundaries. Default value. """ SPAN_KIND_SERVER: Span._SpanKind.ValueType # 2 """Indicates that the span covers server-side handling of an RPC or other remote network request. """ SPAN_KIND_CLIENT: Span._SpanKind.ValueType # 3 """Indicates that the span describes a request to some remote service.""" SPAN_KIND_PRODUCER: Span._SpanKind.ValueType # 4 """Indicates that the span describes a producer sending a message to a broker. Unlike CLIENT and SERVER, there is often no direct critical path latency relationship between producer and consumer spans. A PRODUCER span ends when the message was accepted by the broker while the logical processing of the message might span a much longer time. """ SPAN_KIND_CONSUMER: Span._SpanKind.ValueType # 5 """Indicates that the span describes consumer receiving a message from a broker. Like the PRODUCER kind, there is often no direct critical path latency relationship between producer and consumer spans. """ class SpanKind(_SpanKind, metaclass=_SpanKindEnumTypeWrapper): """SpanKind is the type of span. Can be used to specify additional relationships between spans in addition to a parent/child relationship. """ SPAN_KIND_UNSPECIFIED: Span.SpanKind.ValueType # 0 """Unspecified. Do NOT use as default. Implementations MAY assume SpanKind to be INTERNAL when receiving UNSPECIFIED. """ SPAN_KIND_INTERNAL: Span.SpanKind.ValueType # 1 """Indicates that the span represents an internal operation within an application, as opposed to an operation happening at the boundaries. Default value. """ SPAN_KIND_SERVER: Span.SpanKind.ValueType # 2 """Indicates that the span covers server-side handling of an RPC or other remote network request. """ SPAN_KIND_CLIENT: Span.SpanKind.ValueType # 3 """Indicates that the span describes a request to some remote service.""" SPAN_KIND_PRODUCER: Span.SpanKind.ValueType # 4 """Indicates that the span describes a producer sending a message to a broker. Unlike CLIENT and SERVER, there is often no direct critical path latency relationship between producer and consumer spans. A PRODUCER span ends when the message was accepted by the broker while the logical processing of the message might span a much longer time. """ SPAN_KIND_CONSUMER: Span.SpanKind.ValueType # 5 """Indicates that the span describes consumer receiving a message from a broker. Like the PRODUCER kind, there is often no direct critical path latency relationship between producer and consumer spans. """ @typing_extensions.final class Event(google.protobuf.message.Message): """Event is a time-stamped annotation of the span, consisting of user-supplied text description and key-value pairs. """ DESCRIPTOR: google.protobuf.descriptor.Descriptor TIME_UNIX_NANO_FIELD_NUMBER: builtins.int NAME_FIELD_NUMBER: builtins.int ATTRIBUTES_FIELD_NUMBER: builtins.int DROPPED_ATTRIBUTES_COUNT_FIELD_NUMBER: builtins.int time_unix_nano: builtins.int """The time the event occurred.""" name: builtins.str """The name of the event. This field is semantically required to be set to non-empty string. """ @property def attributes(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[opentelemetry.proto.common.v1.common_pb2.KeyValue]: """A collection of attribute key/value pairs on the event. Attribute keys MUST be unique (it is not allowed to have more than one attribute with the same key). The behavior of software that receives duplicated keys can be unpredictable. """ dropped_attributes_count: builtins.int """The number of dropped attributes. If the value is 0, then no attributes were dropped. """ def __init__( self, *, time_unix_nano: builtins.int = ..., name: builtins.str = ..., attributes: collections.abc.Iterable[opentelemetry.proto.common.v1.common_pb2.KeyValue] | None = ..., dropped_attributes_count: builtins.int = ..., ) -> None: ... def ClearField(self, field_name: typing_extensions.Literal["attributes", b"attributes", "dropped_attributes_count", b"dropped_attributes_count", "name", b"name", "time_unix_nano", b"time_unix_nano"]) -> None: ... @typing_extensions.final class Link(google.protobuf.message.Message): """A pointer from the current span to another span in the same trace or in a different trace. For example, this can be used in batching operations, where a single batch handler processes multiple requests from different traces or when the handler receives a request from a different project. """ DESCRIPTOR: google.protobuf.descriptor.Descriptor TRACE_ID_FIELD_NUMBER: builtins.int SPAN_ID_FIELD_NUMBER: builtins.int TRACE_STATE_FIELD_NUMBER: builtins.int ATTRIBUTES_FIELD_NUMBER: builtins.int DROPPED_ATTRIBUTES_COUNT_FIELD_NUMBER: builtins.int FLAGS_FIELD_NUMBER: builtins.int trace_id: builtins.bytes """A unique identifier of a trace that this linked span is part of. The ID is a 16-byte array. """ span_id: builtins.bytes """A unique identifier for the linked span. The ID is an 8-byte array.""" trace_state: builtins.str """The trace_state associated with the link.""" @property def attributes(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[opentelemetry.proto.common.v1.common_pb2.KeyValue]: """A collection of attribute key/value pairs on the link. Attribute keys MUST be unique (it is not allowed to have more than one attribute with the same key). The behavior of software that receives duplicated keys can be unpredictable. """ dropped_attributes_count: builtins.int """The number of dropped attributes. If the value is 0, then no attributes were dropped. """ flags: builtins.int """Flags, a bit field. Bits 0-7 (8 least significant bits) are the trace flags as defined in W3C Trace Context specification. To read the 8-bit W3C trace flag, use `flags & SPAN_FLAGS_TRACE_FLAGS_MASK`. See https://www.w3.org/TR/trace-context-2/#trace-flags for the flag definitions. Bits 8 and 9 represent the 3 states of whether the link is remote. The states are (unknown, is not remote, is remote). To read whether the value is known, use `(flags & SPAN_FLAGS_CONTEXT_HAS_IS_REMOTE_MASK) != 0`. To read whether the link is remote, use `(flags & SPAN_FLAGS_CONTEXT_IS_REMOTE_MASK) != 0`. Readers MUST NOT assume that bits 10-31 (22 most significant bits) will be zero. When creating new spans, bits 10-31 (most-significant 22-bits) MUST be zero. [Optional]. """ def __init__( self, *, trace_id: builtins.bytes = ..., span_id: builtins.bytes = ..., trace_state: builtins.str = ..., attributes: collections.abc.Iterable[opentelemetry.proto.common.v1.common_pb2.KeyValue] | None = ..., dropped_attributes_count: builtins.int = ..., flags: builtins.int = ..., ) -> None: ... def ClearField(self, field_name: typing_extensions.Literal["attributes", b"attributes", "dropped_attributes_count", b"dropped_attributes_count", "flags", b"flags", "span_id", b"span_id", "trace_id", b"trace_id", "trace_state", b"trace_state"]) -> None: ... TRACE_ID_FIELD_NUMBER: builtins.int SPAN_ID_FIELD_NUMBER: builtins.int TRACE_STATE_FIELD_NUMBER: builtins.int PARENT_SPAN_ID_FIELD_NUMBER: builtins.int FLAGS_FIELD_NUMBER: builtins.int NAME_FIELD_NUMBER: builtins.int KIND_FIELD_NUMBER: builtins.int START_TIME_UNIX_NANO_FIELD_NUMBER: builtins.int END_TIME_UNIX_NANO_FIELD_NUMBER: builtins.int ATTRIBUTES_FIELD_NUMBER: builtins.int DROPPED_ATTRIBUTES_COUNT_FIELD_NUMBER: builtins.int EVENTS_FIELD_NUMBER: builtins.int DROPPED_EVENTS_COUNT_FIELD_NUMBER: builtins.int LINKS_FIELD_NUMBER: builtins.int DROPPED_LINKS_COUNT_FIELD_NUMBER: builtins.int STATUS_FIELD_NUMBER: builtins.int trace_id: builtins.bytes """A unique identifier for a trace. All spans from the same trace share the same `trace_id`. The ID is a 16-byte array. An ID with all zeroes OR of length other than 16 bytes is considered invalid (empty string in OTLP/JSON is zero-length and thus is also invalid). This field is required. """ span_id: builtins.bytes """A unique identifier for a span within a trace, assigned when the span is created. The ID is an 8-byte array. An ID with all zeroes OR of length other than 8 bytes is considered invalid (empty string in OTLP/JSON is zero-length and thus is also invalid). This field is required. """ trace_state: builtins.str """trace_state conveys information about request position in multiple distributed tracing graphs. It is a trace_state in w3c-trace-context format: https://www.w3.org/TR/trace-context/#tracestate-header See also https://github.com/w3c/distributed-tracing for more details about this field. """ parent_span_id: builtins.bytes """The `span_id` of this span's parent span. If this is a root span, then this field must be empty. The ID is an 8-byte array. """ flags: builtins.int """Flags, a bit field. Bits 0-7 (8 least significant bits) are the trace flags as defined in W3C Trace Context specification. To read the 8-bit W3C trace flag, use `flags & SPAN_FLAGS_TRACE_FLAGS_MASK`. See https://www.w3.org/TR/trace-context-2/#trace-flags for the flag definitions. Bits 8 and 9 represent the 3 states of whether a span's parent is remote. The states are (unknown, is not remote, is remote). To read whether the value is known, use `(flags & SPAN_FLAGS_CONTEXT_HAS_IS_REMOTE_MASK) != 0`. To read whether the span is remote, use `(flags & SPAN_FLAGS_CONTEXT_IS_REMOTE_MASK) != 0`. When creating span messages, if the message is logically forwarded from another source with an equivalent flags fields (i.e., usually another OTLP span message), the field SHOULD be copied as-is. If creating from a source that does not have an equivalent flags field (such as a runtime representation of an OpenTelemetry span), the high 22 bits MUST be set to zero. Readers MUST NOT assume that bits 10-31 (22 most significant bits) will be zero. [Optional]. """ name: builtins.str """A description of the span's operation. For example, the name can be a qualified method name or a file name and a line number where the operation is called. A best practice is to use the same display name at the same call point in an application. This makes it easier to correlate spans in different traces. This field is semantically required to be set to non-empty string. Empty value is equivalent to an unknown span name. This field is required. """ kind: global___Span.SpanKind.ValueType """Distinguishes between spans generated in a particular context. For example, two spans with the same name may be distinguished using `CLIENT` (caller) and `SERVER` (callee) to identify queueing latency associated with the span. """ start_time_unix_nano: builtins.int """The start time of the span. On the client side, this is the time kept by the local machine where the span execution starts. On the server side, this is the time when the server's application handler starts running. Value is UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January 1970. This field is semantically required and it is expected that end_time >= start_time. """ end_time_unix_nano: builtins.int """The end time of the span. On the client side, this is the time kept by the local machine where the span execution ends. On the server side, this is the time when the server application handler stops running. Value is UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January 1970. This field is semantically required and it is expected that end_time >= start_time. """ @property def attributes(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[opentelemetry.proto.common.v1.common_pb2.KeyValue]: """A collection of key/value pairs. Note, global attributes like server name can be set using the resource API. Examples of attributes: "/http/user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36" "/http/server_latency": 300 "example.com/myattribute": true "example.com/score": 10.239 Attribute keys MUST be unique (it is not allowed to have more than one attribute with the same key). The behavior of software that receives duplicated keys can be unpredictable. """ dropped_attributes_count: builtins.int """The number of attributes that were discarded. Attributes can be discarded because their keys are too long or because there are too many attributes. If this value is 0, then no attributes were dropped. """ @property def events(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___Span.Event]: """A collection of Event items.""" dropped_events_count: builtins.int """The number of dropped events. If the value is 0, then no events were dropped. """ @property def links(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___Span.Link]: """A collection of Links, which are references from this span to a span in the same or different trace. """ dropped_links_count: builtins.int """The number of dropped links after the maximum size was enforced. If this value is 0, then no links were dropped. """ @property def status(self) -> global___Status: """An optional final status for this span. Semantically when Status isn't set, it means span's status code is unset, i.e. assume STATUS_CODE_UNSET (code = 0). """ def __init__( self, *, trace_id: builtins.bytes = ..., span_id: builtins.bytes = ..., trace_state: builtins.str = ..., parent_span_id: builtins.bytes = ..., flags: builtins.int = ..., name: builtins.str = ..., kind: global___Span.SpanKind.ValueType = ..., start_time_unix_nano: builtins.int = ..., end_time_unix_nano: builtins.int = ..., attributes: collections.abc.Iterable[opentelemetry.proto.common.v1.common_pb2.KeyValue] | None = ..., dropped_attributes_count: builtins.int = ..., events: collections.abc.Iterable[global___Span.Event] | None = ..., dropped_events_count: builtins.int = ..., links: collections.abc.Iterable[global___Span.Link] | None = ..., dropped_links_count: builtins.int = ..., status: global___Status | None = ..., ) -> None: ... def HasField(self, field_name: typing_extensions.Literal["status", b"status"]) -> builtins.bool: ... def ClearField(self, field_name: typing_extensions.Literal["attributes", b"attributes", "dropped_attributes_count", b"dropped_attributes_count", "dropped_events_count", b"dropped_events_count", "dropped_links_count", b"dropped_links_count", "end_time_unix_nano", b"end_time_unix_nano", "events", b"events", "flags", b"flags", "kind", b"kind", "links", b"links", "name", b"name", "parent_span_id", b"parent_span_id", "span_id", b"span_id", "start_time_unix_nano", b"start_time_unix_nano", "status", b"status", "trace_id", b"trace_id", "trace_state", b"trace_state"]) -> None: ... global___Span = Span @typing_extensions.final class Status(google.protobuf.message.Message): """The Status type defines a logical error model that is suitable for different programming environments, including REST APIs and RPC APIs. """ DESCRIPTOR: google.protobuf.descriptor.Descriptor class _StatusCode: ValueType = typing.NewType("ValueType", builtins.int) V: typing_extensions.TypeAlias = ValueType class _StatusCodeEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[Status._StatusCode.ValueType], builtins.type): DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor STATUS_CODE_UNSET: Status._StatusCode.ValueType # 0 """The default status.""" STATUS_CODE_OK: Status._StatusCode.ValueType # 1 """The Span has been validated by an Application developer or Operator to have completed successfully. """ STATUS_CODE_ERROR: Status._StatusCode.ValueType # 2 """The Span contains an error.""" class StatusCode(_StatusCode, metaclass=_StatusCodeEnumTypeWrapper): """For the semantics of status codes see https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/api.md#set-status """ STATUS_CODE_UNSET: Status.StatusCode.ValueType # 0 """The default status.""" STATUS_CODE_OK: Status.StatusCode.ValueType # 1 """The Span has been validated by an Application developer or Operator to have completed successfully. """ STATUS_CODE_ERROR: Status.StatusCode.ValueType # 2 """The Span contains an error.""" MESSAGE_FIELD_NUMBER: builtins.int CODE_FIELD_NUMBER: builtins.int message: builtins.str """A developer-facing human readable error message.""" code: global___Status.StatusCode.ValueType """The status code.""" def __init__( self, *, message: builtins.str = ..., code: global___Status.StatusCode.ValueType = ..., ) -> None: ... def ClearField(self, field_name: typing_extensions.Literal["code", b"code", "message", b"message"]) -> None: ... global___Status = Status python-opentelemetry-1.45.2/opentelemetry-proto/src/opentelemetry/proto/version/000077500000000000000000000000001527070070700303735ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-proto/src/opentelemetry/proto/version/__init__.py000066400000000000000000000001441527070070700325030ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 __version__ = "1.45.2" python-opentelemetry-1.45.2/opentelemetry-proto/test-requirements.in000066400000000000000000000001471527070070700261120ustar00rootroot00000000000000colorama>=0.4.6 iniconfig>=2.0.0 packaging>=24.0 protobuf>=5.29.6 pytest>=9.0.3 -e opentelemetry-proto python-opentelemetry-1.45.2/opentelemetry-proto/test-requirements.latest.txt000066400000000000000000000021221527070070700276110ustar00rootroot00000000000000# This file was autogenerated by uv via the following command: # uv pip compile --python-version=3.10 --universal --constraints=dev-requirements.txt opentelemetry-proto/test-requirements.in --output-file=opentelemetry-proto/test-requirements.latest.txt -e opentelemetry-proto # via -r opentelemetry-proto/test-requirements.in colorama==0.4.6 # via # -r opentelemetry-proto/test-requirements.in # pytest exceptiongroup==1.3.1 ; python_full_version < '3.11' # via pytest iniconfig==2.3.0 # via # -r opentelemetry-proto/test-requirements.in # pytest packaging==26.0 # via # -r opentelemetry-proto/test-requirements.in # pytest pluggy==1.6.0 # via pytest protobuf==7.35.0 # via # -r opentelemetry-proto/test-requirements.in # opentelemetry-proto pygments==2.20.0 # via pytest pytest==9.0.3 # via # -c dev-requirements.txt # -r opentelemetry-proto/test-requirements.in tomli==2.4.1 ; python_full_version < '3.11' # via pytest typing-extensions==4.15.0 ; python_full_version < '3.11' # via exceptiongroup python-opentelemetry-1.45.2/opentelemetry-proto/test-requirements.oldest.txt000066400000000000000000000020231527070070700276070ustar00rootroot00000000000000# This file was autogenerated by uv via the following command: # uv pip compile --python-version=3.10 --universal --resolution=lowest --constraints=dev-requirements.txt opentelemetry-proto/test-requirements.in --output-file=opentelemetry-proto/test-requirements.oldest.txt -e opentelemetry-proto # via -r opentelemetry-proto/test-requirements.in colorama==0.4.6 # via # -r opentelemetry-proto/test-requirements.in # pytest exceptiongroup==1.0.0 ; python_full_version < '3.11' # via pytest iniconfig==2.0.0 # via # -r opentelemetry-proto/test-requirements.in # pytest packaging==24.0 # via # -r opentelemetry-proto/test-requirements.in # pytest pluggy==1.6.0 # via pytest protobuf==5.29.6 # via # -r opentelemetry-proto/test-requirements.in # opentelemetry-proto pygments==2.7.2 # via pytest pytest==9.0.3 # via # -c dev-requirements.txt # -r opentelemetry-proto/test-requirements.in tomli==1.0.0 ; python_full_version < '3.11' # via pytest python-opentelemetry-1.45.2/opentelemetry-proto/tests/000077500000000000000000000000001527070070700232225ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-proto/tests/__init__.py000066400000000000000000000000001527070070700253210ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-proto/tests/test_proto.py000066400000000000000000000005151527070070700257770ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # type: ignore from importlib.util import find_spec from unittest import TestCase class TestInstrumentor(TestCase): def test_proto(self): if find_spec("opentelemetry.proto") is None: self.fail("opentelemetry-proto not installed") python-opentelemetry-1.45.2/opentelemetry-sdk/000077500000000000000000000000001527070070700214765ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-sdk/LICENSE000066400000000000000000000261351527070070700225120ustar00rootroot00000000000000 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. python-opentelemetry-1.45.2/opentelemetry-sdk/README.rst000066400000000000000000000005711527070070700231700ustar00rootroot00000000000000OpenTelemetry Python SDK ============================================================================ |pypi| .. |pypi| image:: https://badge.fury.io/py/opentelemetry-sdk.svg :target: https://pypi.org/project/opentelemetry-sdk/ Installation ------------ :: pip install opentelemetry-sdk References ---------- * `OpenTelemetry Project `_ python-opentelemetry-1.45.2/opentelemetry-sdk/benchmark-requirements.txt000066400000000000000000000000301527070070700267030ustar00rootroot00000000000000pytest-benchmark==4.0.0 python-opentelemetry-1.45.2/opentelemetry-sdk/benchmarks/000077500000000000000000000000001527070070700236135ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-sdk/benchmarks/logs/000077500000000000000000000000001527070070700245575ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-sdk/benchmarks/logs/test_benchmark_logging_handler.py000066400000000000000000000022361527070070700333300ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import logging import pytest from opentelemetry.sdk._logs import LoggerProvider, LoggingHandler 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) python-opentelemetry-1.45.2/opentelemetry-sdk/benchmarks/logs/test_benchmark_logs.py000066400000000000000000000042651527070070700311550ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import pytest from opentelemetry._logs import SeverityNumber from opentelemetry.sdk._logs import LoggerProvider from opentelemetry.sdk._logs.export import ( BatchLogRecordProcessor, InMemoryLogRecordExporter, SimpleLogRecordProcessor, ) from opentelemetry.sdk.resources import Resource resource = Resource( { "service.name": "A123456789", "service.version": "1.34567890", "service.instance.id": "123ab456-a123-12ab-12ab-12340a1abc12", } ) simple_exporter = InMemoryLogRecordExporter() simple_provider = LoggerProvider(resource=resource) simple_provider.add_log_record_processor(SimpleLogRecordProcessor(simple_exporter)) simple_logger = simple_provider.get_logger("simple_logger") batch_exporter = InMemoryLogRecordExporter() batch_provider = LoggerProvider(resource=resource) batch_provider.add_log_record_processor(BatchLogRecordProcessor(batch_exporter)) batch_logger = batch_provider.get_logger("batch_logger") @pytest.mark.parametrize("num_attributes", [0, 1, 3, 5, 10]) def test_simple_log_record_processor(benchmark, num_attributes): attributes = {f"key{i}": f"value{i}" for i in range(num_attributes)} def benchmark_emit(): simple_logger.emit( severity_number=SeverityNumber.INFO, body="benchmark log message", attributes=attributes, event_name="test.event", ) benchmark(benchmark_emit) @pytest.mark.parametrize("num_attributes", [0, 1, 3, 5, 10]) def test_batch_log_record_processor(benchmark, num_attributes): attributes = {f"key{i}": f"value{i}" for i in range(num_attributes)} def benchmark_emit(): batch_logger.emit( severity_number=SeverityNumber.INFO, body="benchmark log message", attributes=attributes, event_name="test.event", ) benchmark(benchmark_emit) def test_get_logger(benchmark): def benchmark_get_logger(): simple_provider.get_logger( "test_logger", version="1.0.0", schema_url="https://opentelemetry.io/schemas/1.38.0", ) benchmark(benchmark_get_logger) python-opentelemetry-1.45.2/opentelemetry-sdk/benchmarks/metrics/000077500000000000000000000000001527070070700252615ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-sdk/benchmarks/metrics/test_benchmark_metrics.py000066400000000000000000000066611527070070700323630ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import pytest from opentelemetry.sdk.metrics import Counter, MeterProvider from opentelemetry.sdk.metrics._internal import ( _default_meter_configurator, _disable_meter_configurator, _MeterConfig, _RuleBasedMeterConfigurator, ) from opentelemetry.sdk.metrics.export import ( AggregationTemporality, InMemoryMetricReader, ) from opentelemetry.sdk.util.instrumentation import _scope_name_matches_glob reader_cumulative = InMemoryMetricReader() reader_delta = InMemoryMetricReader( preferred_temporality={ Counter: AggregationTemporality.DELTA, }, ) provider_reader_cumulative = MeterProvider( metric_readers=[reader_cumulative], ) provider_reader_delta = MeterProvider(metric_readers=[reader_delta]) meter_cumulative = provider_reader_cumulative.get_meter("sdk_meter_provider") meter_delta = provider_reader_delta.get_meter("sdk_meter_provider_delta") counter_cumulative = meter_cumulative.create_counter("test_counter") counter_delta = meter_delta.create_counter("test_counter2") udcounter = meter_cumulative.create_up_down_counter("test_udcounter") @pytest.mark.parametrize( ("num_labels", "temporality"), [ (0, "delta"), (1, "delta"), (3, "delta"), (5, "delta"), (10, "delta"), (0, "cumulative"), (1, "cumulative"), (3, "cumulative"), (5, "cumulative"), (10, "cumulative"), ], ) def test_counter_add(benchmark, num_labels, temporality): labels = {} # pylint: disable=invalid-name for i in range(num_labels): labels = {f"Key{i}": f"Value{i}" for i in range(num_labels)} def benchmark_counter_add(): if temporality == "cumulative": counter_cumulative.add(1, labels) else: counter_delta.add(1, labels) benchmark(benchmark_counter_add) @pytest.mark.parametrize("num_labels", [0, 1, 3, 5, 10]) def test_up_down_counter_add(benchmark, num_labels): labels = {} # pylint: disable=invalid-name for i in range(num_labels): labels = {f"Key{i}": f"Value{i}" for i in range(num_labels)} def benchmark_up_down_counter_add(): udcounter.add(1, labels) benchmark(benchmark_up_down_counter_add) @pytest.fixture(params=[None, 0, 1, 10, 50]) def num_meter_configurator_rules(request): return request.param # pylint: disable=protected-access,redefined-outer-name def test_counter_add_with_meter_configurator_rules(benchmark, num_meter_configurator_rules): def benchmark_counter_add(): counter_cumulative.add(1, {}) if num_meter_configurator_rules is None: provider_reader_cumulative._set_meter_configurator(meter_configurator=_disable_meter_configurator) else: def meter_configurator(meter_scope): return _RuleBasedMeterConfigurator( rules=[ ( _scope_name_matches_glob(glob_pattern=str(i)), _MeterConfig(is_enabled=True), ) for i in range(num_meter_configurator_rules) ], default_config=_MeterConfig(is_enabled=True), )(meter_scope) provider_reader_cumulative._set_meter_configurator(meter_configurator=meter_configurator) benchmark(benchmark_counter_add) provider_reader_cumulative._set_meter_configurator(meter_configurator=_default_meter_configurator) python-opentelemetry-1.45.2/opentelemetry-sdk/benchmarks/metrics/test_benchmark_metrics_histogram.py000066400000000000000000000116521527070070700344340ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # pylint: disable=invalid-name import json import random import pytest from opentelemetry.sdk.metrics import MeterProvider from opentelemetry.sdk.metrics.export import InMemoryMetricReader from opentelemetry.sdk.metrics.view import ( ExplicitBucketHistogramAggregation, View, ) MAX_BOUND_VALUE = 10000 def _generate_bounds(bound_count): bounds = [] for i in range(bound_count): bounds.append(i * MAX_BOUND_VALUE / bound_count) return bounds hist_view_10 = View( instrument_name="test_histogram_10_bound", aggregation=ExplicitBucketHistogramAggregation(_generate_bounds(10)), ) hist_view_49 = View( instrument_name="test_histogram_49_bound", aggregation=ExplicitBucketHistogramAggregation(_generate_bounds(49)), ) hist_view_50 = View( instrument_name="test_histogram_50_bound", aggregation=ExplicitBucketHistogramAggregation(_generate_bounds(50)), ) hist_view_1000 = View( instrument_name="test_histogram_1000_bound", aggregation=ExplicitBucketHistogramAggregation(_generate_bounds(1000)), ) reader = InMemoryMetricReader() provider = MeterProvider( metric_readers=[reader], views=[ hist_view_10, hist_view_49, hist_view_50, hist_view_1000, ], ) meter = provider.get_meter("sdk_meter_provider") hist = meter.create_histogram("test_histogram_default") hist10 = meter.create_histogram("test_histogram_10_bound") hist49 = meter.create_histogram("test_histogram_49_bound") hist50 = meter.create_histogram("test_histogram_50_bound") hist1000 = meter.create_histogram("test_histogram_1000_bound") @pytest.mark.parametrize("num_labels", [0, 1, 3, 5, 7]) def test_histogram_record(benchmark, num_labels): labels = {} for i in range(num_labels): labels[f"Key{i}"] = "Value{i}" def benchmark_histogram_record(): hist.record(random.random() * MAX_BOUND_VALUE) benchmark(benchmark_histogram_record) @pytest.mark.parametrize("num_labels", [0, 1, 3, 5, 7]) def test_histogram_record_10(benchmark, num_labels): labels = {} for i in range(num_labels): labels[f"Key{i}"] = "Value{i}" def benchmark_histogram_record_10(): hist10.record(random.random() * MAX_BOUND_VALUE) benchmark(benchmark_histogram_record_10) @pytest.mark.parametrize("num_labels", [1, 3]) def test_histogram_record_10_small_mapping_attrs(benchmark, num_labels): def test_histogram_record_10_attrs(): hist10.record( random.random() * MAX_BOUND_VALUE, attributes={f"Key{i}": {"k1": "v1"} for i in range(num_labels)}, ) benchmark(test_histogram_record_10_attrs) @pytest.mark.parametrize("num_labels", [1, 3]) def test_histogram_record_10_complex_attrs(benchmark, num_labels): def test_histogram_record_10_attrs(): hist10.record( random.random() * MAX_BOUND_VALUE, attributes={f"Key{i}": {"k1": "v1", "k2": {"k3": "v3", "k4": [1, 2, 3]}} for i in range(num_labels)}, ) benchmark(test_histogram_record_10_attrs) @pytest.mark.parametrize("num_labels", [1, 3]) def test_histogram_record_10_array_attrs(benchmark, num_labels): def test_histogram_record_10_attrs(): hist10.record( random.random() * MAX_BOUND_VALUE, attributes={f"Key{i}": ["k1", "v1", "k2", "k3", "v3", "k4", "1", "2", "3"] for i in range(num_labels)}, ) benchmark(test_histogram_record_10_attrs) @pytest.mark.parametrize("num_labels", [1, 3]) def test_histogram_record_10_json_string_attrs(benchmark, num_labels): def test_histogram_record_10_attrs(): hist10.record( random.random() * MAX_BOUND_VALUE, attributes={ f"Key{i}": json.dumps({"k1": "v1", "k2": {"k3": "v3", "k4": [1, 2, 3]}}) for i in range(num_labels) }, ) benchmark(test_histogram_record_10_attrs) @pytest.mark.parametrize("num_labels", [0, 1, 3, 5, 7]) def test_histogram_record_49(benchmark, num_labels): def benchmark_histogram_record_49(): hist49.record( random.random() * MAX_BOUND_VALUE, attributes={f"Key{i}": f"Value{i}" for i in range(num_labels)}, ) benchmark(benchmark_histogram_record_49) @pytest.mark.parametrize("num_labels", [0, 1, 3, 5, 7]) def test_histogram_record_50(benchmark, num_labels): def benchmark_histogram_record_50(): hist50.record( random.random() * MAX_BOUND_VALUE, attributes={f"Key{i}": f"Value{i}" for i in range(num_labels)}, ) benchmark(benchmark_histogram_record_50) @pytest.mark.parametrize("num_labels", [0, 1, 3, 5, 7]) def test_histogram_record_1000(benchmark, num_labels): def benchmark_histogram_record_1000(): hist1000.record( random.random() * MAX_BOUND_VALUE, attributes={f"Key{i}": f"Value{i}" for i in range(num_labels)}, ) benchmark(benchmark_histogram_record_1000) test_benchmark_metrics_histogram_steady.py000066400000000000000000000052671527070070700357330ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-sdk/benchmarks/metrics# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # pylint: disable=invalid-name import itertools from opentelemetry.sdk.metrics import MeterProvider from opentelemetry.sdk.metrics.export import InMemoryMetricReader from opentelemetry.sdk.metrics.view import ( ExplicitBucketHistogramAggregation, View, ) MAX_BOUND_VALUE = 10000 def _generate_bounds(bound_count): bounds = [] for i in range(bound_count): bounds.append(i * MAX_BOUND_VALUE / bound_count) return bounds hist_view_10 = View( instrument_name="test_histogram_10_bound", aggregation=ExplicitBucketHistogramAggregation(_generate_bounds(10)), ) hist_view_49 = View( instrument_name="test_histogram_49_bound", aggregation=ExplicitBucketHistogramAggregation(_generate_bounds(49)), ) hist_view_50 = View( instrument_name="test_histogram_50_bound", aggregation=ExplicitBucketHistogramAggregation(_generate_bounds(50)), ) hist_view_1000 = View( instrument_name="test_histogram_1000_bound", aggregation=ExplicitBucketHistogramAggregation(_generate_bounds(1000)), ) reader = InMemoryMetricReader() provider = MeterProvider( metric_readers=[reader], views=[ hist_view_10, hist_view_49, hist_view_50, hist_view_1000, ], ) meter = provider.get_meter("sdk_meter_provider") hist = meter.create_histogram("test_histogram_default") hist10 = meter.create_histogram("test_histogram_10_bound") hist49 = meter.create_histogram("test_histogram_49_bound") hist50 = meter.create_histogram("test_histogram_50_bound") hist1000 = meter.create_histogram("test_histogram_1000_bound") def test_histogram_record(benchmark): values = itertools.cycle(_generate_bounds(10)) def benchmark_histogram_record(): hist.record(next(values)) benchmark(benchmark_histogram_record) def test_histogram_record_10(benchmark): values = itertools.cycle(_generate_bounds(10)) def benchmark_histogram_record_10(): hist10.record(next(values)) benchmark(benchmark_histogram_record_10) def test_histogram_record_49(benchmark): values = itertools.cycle(_generate_bounds(49)) def benchmark_histogram_record_49(): hist49.record(next(values)) benchmark(benchmark_histogram_record_49) def test_histogram_record_50(benchmark): values = itertools.cycle(_generate_bounds(50)) def benchmark_histogram_record_50(): hist50.record(next(values)) benchmark(benchmark_histogram_record_50) def test_histogram_record_1000(benchmark): values = itertools.cycle(_generate_bounds(1000)) def benchmark_histogram_record_1000(): hist1000.record(next(values)) benchmark(benchmark_histogram_record_1000) python-opentelemetry-1.45.2/opentelemetry-sdk/benchmarks/test_baggage.py000066400000000000000000000035451527070070700266100ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # pylint: disable=redefined-outer-name, invalid-name import pytest from opentelemetry import trace from opentelemetry.baggage import ( clear, get_all, get_baggage, remove_baggage, set_baggage, ) tracer = trace.get_tracer(__name__) @pytest.fixture(params=[10, 100, 1000, 10000]) def baggage_size(request): return request.param def set_baggage_operation(size=10): with tracer.start_span(name="root span"): ctx = get_all() for i in range(size): ctx = set_baggage(f"foo{i}", f"bar{i}", context=ctx) return ctx def test_set_baggage(benchmark, baggage_size): ctx = benchmark(set_baggage_operation, baggage_size) result = get_all(ctx) assert len(result) == baggage_size def test_get_baggage(benchmark, baggage_size): ctx = set_baggage_operation(baggage_size) def get_baggage_operation(): return [get_baggage(f"foo{i}", ctx) for i in range(baggage_size)] result = benchmark(get_baggage_operation) assert result == [f"bar{i}" for i in range(baggage_size)] def test_remove_baggage(benchmark, baggage_size): ctx = set_baggage_operation(baggage_size) def remove_operation(): tmp_ctx = ctx for i in range(baggage_size): tmp_ctx = remove_baggage(f"foo{i}", tmp_ctx) return tmp_ctx cleared_context = benchmark(remove_operation) result = get_all(cleared_context) # After removing all baggage items, it should be empty. assert len(result) == 0 def test_clear_baggage(benchmark, baggage_size): ctx = set_baggage_operation(baggage_size) def clear_operation(): return clear(ctx) cleared_context = benchmark(clear_operation) result = get_all(cleared_context) # After clearing the baggage should be empty. assert len(result) == 0 python-opentelemetry-1.45.2/opentelemetry-sdk/benchmarks/trace/000077500000000000000000000000001527070070700247115ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-sdk/benchmarks/trace/test_benchmark_trace.py000066400000000000000000000215721527070070700314410ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import threading import tracemalloc from functools import lru_cache import pytest from opentelemetry.attributes import BoundedAttributes from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.trace import ( ReadableSpan, SpanProcessor, TracerProvider, _default_tracer_configurator, _RuleBasedTracerConfigurator, _TracerConfig, sampling, ) from opentelemetry.sdk.trace.export import BatchSpanProcessor from opentelemetry.sdk.trace.export.in_memory_span_exporter import ( InMemorySpanExporter, ) from opentelemetry.sdk.util import BoundedList from opentelemetry.sdk.util.instrumentation import _scope_name_matches_glob from opentelemetry.trace import SpanContext, TraceFlags tracer_provider = TracerProvider( sampler=sampling.DEFAULT_ON, resource=Resource( { "service.name": "A123456789", "service.version": "1.34567890", "service.instance.id": "123ab456-a123-12ab-12ab-12340a1abc12", } ), ) tracer = tracer_provider.get_tracer("sdk_tracer_provider") @pytest.fixture(params=[0, 1, 10, 50]) def num_tracer_configurator_rules(request): return request.param def test_simple_start_span(benchmark): def benchmark_start_span(): span = tracer.start_span( "benchmarkedSpan", attributes={"long.attribute": -10000000001000000000}, ) span.add_event("benchmarkEvent") span.end() benchmark(benchmark_start_span) @pytest.mark.parametrize("num_attrs", [0, 1, 10, 50, 128]) def test_start_span_with_attributes(benchmark, num_attrs): attrs = {f"key{i}": f"value{i}" for i in range(num_attrs)} def benchmark_start_span(): span = tracer.start_span("benchmarkedSpan", attributes=attrs) span.end() benchmark(benchmark_start_span) # pylint: disable=protected-access,redefined-outer-name def test_simple_start_span_with_tracer_configurator_rules(benchmark, num_tracer_configurator_rules): def benchmark_start_span(): span = tracer.start_span( "benchmarkedSpan", attributes={"long.attribute": -10000000001000000000}, ) span.add_event("benchmarkEvent") span.end() @lru_cache def tracer_configurator(tracer_scope): # this is testing 100 rules that is an extreme case return _RuleBasedTracerConfigurator( rules=[ ( _scope_name_matches_glob(glob_pattern=str(i)), _TracerConfig(is_enabled=True), ) for i in range(num_tracer_configurator_rules) ], default_config=_TracerConfig(is_enabled=True), )(tracer_scope) tracer_provider._set_tracer_configurator(tracer_configurator=tracer_configurator) benchmark(benchmark_start_span) tracer_provider._set_tracer_configurator(tracer_configurator=_default_tracer_configurator) @pytest.mark.parametrize("num_attrs", [1, 10, 50, 128]) def test_set_attribute(benchmark, num_attrs): attrs = {f"key{i}": f"value{i}" for i in range(num_attrs)} def benchmark_set_attribute(): span = tracer.start_span("benchmarkedSpan") for key, value in attrs.items(): span.set_attribute(key, value) span.end() benchmark(benchmark_set_attribute) @pytest.mark.parametrize( "attr_type,value", [ ("bool", True), ("int", 42), ("float", 3.14), ("str", "hello world"), ("bytes", b"hello world"), ("seq_bool", (True, False, True)), ("seq_int", (1, 2, 3, 4, 5)), ("seq_float", (1.1, 2.2, 3.3)), ("seq_str", ("a", "b", "c", "d", "e")), ("seq_bytes", (b"a", b"b", b"c")), ], ) def test_set_attribute_types(benchmark, attr_type, value): attrs = {f"key{i}": value for i in range(128)} def benchmark_set_attribute(): for _ in range(5_000): span = tracer.start_span("benchmarkedSpan") for key, val in attrs.items(): span.set_attribute(key, val) span.end() benchmark(benchmark_set_attribute) def test_simple_start_as_current_span(benchmark): def benchmark_start_as_current_span(): with tracer.start_as_current_span( "benchmarkedSpan", attributes={"long.attribute": -10000000001000000000}, ) as span: span.add_event("benchmarkEvent") benchmark(benchmark_start_as_current_span) class _EventsReadingProcessor(SpanProcessor): def on_end(self, span: ReadableSpan) -> None: _ = span.events class _LinksReadingProcessor(SpanProcessor): def on_end(self, span: ReadableSpan) -> None: _ = span.links _link_context = SpanContext( trace_id=0x000000000000000000000000DEADBEEF, span_id=0x00000000DEADBEF0, is_remote=True, trace_flags=TraceFlags(TraceFlags.SAMPLED), ) @pytest.mark.parametrize("num_events", [1, 10, 50, 128]) def test_read_events(benchmark, num_events): provider = TracerProvider(sampler=sampling.DEFAULT_ON) provider.add_span_processor(_EventsReadingProcessor()) tp = provider.get_tracer("bench") peaks = [] for _ in range(200): span = tp.start_span("benchmarkedSpan") for event in range(num_events): span.add_event(f"event{event}", {"k": "v"}) tracemalloc.start() span.end() _, peak = tracemalloc.get_traced_memory() tracemalloc.stop() peaks.append(peak) benchmark.extra_info["mean_alloc_bytes"] = sum(peaks) / len(peaks) def benchmark_read_events(): span = tp.start_span("benchmarkedSpan") for event in range(num_events): span.add_event(f"event{event}", {"k": "v"}) span.end() benchmark(benchmark_read_events) @pytest.mark.parametrize("num_links", [1, 10, 50, 128]) def test_read_links(benchmark, num_links): provider = TracerProvider(sampler=sampling.DEFAULT_ON) provider.add_span_processor(_LinksReadingProcessor()) tp = provider.get_tracer("bench") peaks = [] for _ in range(200): span = tp.start_span("benchmarkedSpan") for _ in range(num_links): span.add_link(_link_context) tracemalloc.start() span.end() _, peak = tracemalloc.get_traced_memory() tracemalloc.stop() peaks.append(peak) benchmark.extra_info["mean_alloc_bytes"] = sum(peaks) / len(peaks) def benchmark_read_links(): span = tp.start_span("benchmarkedSpan") for _ in range(num_links): span.add_link(_link_context) span.end() benchmark(benchmark_read_links) @pytest.mark.parametrize("num_attrs", [1, 10, 50, 128]) def test_bounded_attribute_iterator(benchmark, num_attrs): attrs = BoundedAttributes(attributes={f"key{i}": f"value{i}" for i in range(num_attrs)}) peaks = [] for _ in range(200): tracemalloc.start() list(attrs) _, peak = tracemalloc.get_traced_memory() tracemalloc.stop() peaks.append(peak) benchmark.extra_info["mean_alloc_bytes"] = sum(peaks) / len(peaks) def benchmark_iter(): list(attrs) benchmark(benchmark_iter) @pytest.mark.parametrize("num_items", [1, 10, 50, 128]) def test_bounded_list_iterator(benchmark, num_items): blist = BoundedList.from_seq(num_items, range(num_items)) peaks = [] for _ in range(200): tracemalloc.start() list(blist) _, peak = tracemalloc.get_traced_memory() tracemalloc.stop() peaks.append(peak) benchmark.extra_info["mean_alloc_bytes"] = sum(peaks) / len(peaks) def benchmark_iter(): list(blist) benchmark(benchmark_iter) # Total span work is fixed at TOTAL_SPANS regardless of thread count so that # ideal (GIL-free) parallelism would produce a flat wall-clock time across # thread counts. Any increase instead reveals GIL + lock contention. _TOTAL_SPANS = 128 _ATTRS_PER_SPAN = {f"key{i}": f"value{i}" for i in range(50)} @pytest.mark.parametrize("num_threads", [1, 2, 4, 8]) def test_gil_contention_batch_processor(benchmark, num_threads): exporter = InMemorySpanExporter() provider = TracerProvider(sampler=sampling.DEFAULT_ON) # max_export_batch_size=16 ensures the export threshold is crossed # during the benchmark so _worker_awaken.set() contention is exercised. provider.add_span_processor(BatchSpanProcessor(exporter, max_export_batch_size=16)) tracer = provider.get_tracer("bench") spans_per_thread = _TOTAL_SPANS // num_threads def worker(): for _ in range(spans_per_thread): span = tracer.start_span("s") span.set_attributes(_ATTRS_PER_SPAN) span.end() def benchmark_fn(): threads = [threading.Thread(target=worker) for _ in range(num_threads)] for thread in threads: thread.start() for thread in threads: thread.join() benchmark(benchmark_fn) python-opentelemetry-1.45.2/opentelemetry-sdk/pyproject.toml000066400000000000000000000066661527070070700244300ustar00rootroot00000000000000[build-system] requires = ["hatchling"] build-backend = "hatchling.build" [project] name = "opentelemetry-sdk" dynamic = ["version"] description = "OpenTelemetry Python SDK" 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", "Framework :: OpenTelemetry", "Intended Audience :: Developers", "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.45.2", "opentelemetry-semantic-conventions == 0.66b2", "typing-extensions >= 4.5.0", ] [project.optional-dependencies] # Deprecated alias: declarative configuration lives in the # `opentelemetry-configuration` package. This extras group is kept so that # existing `opentelemetry-sdk[file-configuration]` install lines keep # working; new users should install `opentelemetry-configuration` directly. file-configuration = [ "opentelemetry-configuration == 0.66b2", ] [project.entry-points.opentelemetry_environment_variables] sdk = "opentelemetry.sdk.environment_variables" [project.entry-points.opentelemetry_id_generator] random = "opentelemetry.sdk.trace.id_generator:RandomIdGenerator" [project.entry-points.opentelemetry_traces_sampler] always_on = "opentelemetry.sdk.trace.sampling:_AlwaysOn" always_off = "opentelemetry.sdk.trace.sampling:_AlwaysOff" parentbased_always_on = "opentelemetry.sdk.trace.sampling:_ParentBasedAlwaysOn" parentbased_always_off = "opentelemetry.sdk.trace.sampling:_ParentBasedAlwaysOff" traceidratio = "opentelemetry.sdk.trace.sampling:TraceIdRatioBased" parentbased_traceidratio = "opentelemetry.sdk.trace.sampling:ParentBasedTraceIdRatio" [project.entry-points.opentelemetry_logger_provider] sdk_logger_provider = "opentelemetry.sdk._logs:LoggerProvider" [project.entry-points.opentelemetry_logs_exporter] console = "opentelemetry.sdk._logs.export:ConsoleLogRecordExporter" [project.entry-points.opentelemetry_meter_provider] sdk_meter_provider = "opentelemetry.sdk.metrics:MeterProvider" [project.entry-points.opentelemetry_metrics_exporter] console = "opentelemetry.sdk.metrics.export:ConsoleMetricExporter" [project.entry-points.opentelemetry_tracer_provider] sdk_tracer_provider = "opentelemetry.sdk.trace:TracerProvider" [project.entry-points.opentelemetry_traces_exporter] console = "opentelemetry.sdk.trace.export:ConsoleSpanExporter" [project.entry-points.opentelemetry_resource_detector] otel = "opentelemetry.sdk.resources:OTELResourceDetector" process = "opentelemetry.sdk.resources:ProcessResourceDetector" os = "opentelemetry.sdk.resources:OsResourceDetector" host = "opentelemetry.sdk.resources:_HostResourceDetector" service_instance = "opentelemetry.sdk.resources:ServiceInstanceIdResourceDetector" [project.urls] Homepage = "https://github.com/open-telemetry/opentelemetry-python/tree/main/opentelemetry-sdk" Repository = "https://github.com/open-telemetry/opentelemetry-python" [tool.hatch.version] path = "src/opentelemetry/sdk/version/__init__.py" [tool.hatch.build.targets.sdist] include = [ "/src", "/tests", ] [tool.hatch.build.targets.wheel] packages = ["src/opentelemetry"] python-opentelemetry-1.45.2/opentelemetry-sdk/src/000077500000000000000000000000001527070070700222655ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-sdk/src/opentelemetry/000077500000000000000000000000001527070070700251615ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-sdk/src/opentelemetry/sdk/000077500000000000000000000000001527070070700257425ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-sdk/src/opentelemetry/sdk/__init__.pyi000066400000000000000000000012351527070070700302250ustar00rootroot00000000000000# 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. """ The OpenTelemetry SDK package is an implementation of the OpenTelemetry API """ python-opentelemetry-1.45.2/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/000077500000000000000000000000001527070070700307505ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py000066400000000000000000000600541527070070700330660ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # """ OpenTelemetry SDK Configurator for Easy Instrumentation with Distros """ from __future__ import annotations import logging import logging.config import os import warnings from abc import ABC, abstractmethod from collections.abc import Callable, Mapping, Sequence from os import environ from typing import Any, Literal, Protocol from opentelemetry._logs import set_logger_provider from opentelemetry.environment_variables import ( OTEL_LOGS_EXPORTER, OTEL_METRICS_EXPORTER, OTEL_PYTHON_ID_GENERATOR, OTEL_TRACES_EXPORTER, ) from opentelemetry.metrics import set_meter_provider from opentelemetry.sdk._logs import ( LoggerProvider, LoggingHandler, LogRecordProcessor, ) from opentelemetry.sdk._logs._internal import _LoggerConfiguratorT from opentelemetry.sdk._logs.export import ( BatchLogRecordProcessor, LogRecordExporter, ) from opentelemetry.sdk.environment_variables import ( _OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED, OTEL_CONFIG_FILE, OTEL_EXPORTER_OTLP_LOGS_PROTOCOL, OTEL_EXPORTER_OTLP_METRICS_PROTOCOL, OTEL_EXPORTER_OTLP_PROTOCOL, OTEL_EXPORTER_OTLP_TRACES_PROTOCOL, OTEL_PYTHON_LOGGER_CONFIGURATOR, OTEL_PYTHON_METER_CONFIGURATOR, OTEL_PYTHON_TRACER_CONFIGURATOR, OTEL_TRACES_SAMPLER, OTEL_TRACES_SAMPLER_ARG, ) from opentelemetry.sdk.metrics import MeterProvider from opentelemetry.sdk.metrics._internal import _MeterConfiguratorT from opentelemetry.sdk.metrics.export import ( MetricExporter, MetricReader, PeriodicExportingMetricReader, ) from opentelemetry.sdk.resources import Attributes, Resource from opentelemetry.sdk.trace import ( SpanProcessor, TracerProvider, _TracerConfiguratorT, ) from opentelemetry.sdk.trace.export import BatchSpanProcessor, SpanExporter from opentelemetry.sdk.trace.id_generator import IdGenerator from opentelemetry.sdk.trace.sampling import Sampler from opentelemetry.semconv.resource import ResourceAttributes from opentelemetry.trace import set_tracer_provider from opentelemetry.util._importlib_metadata import entry_points _EXPORTER_OTLP = "otlp" _EXPORTER_OTLP_PROTO_GRPC = "otlp_proto_grpc" _EXPORTER_OTLP_PROTO_HTTP = "otlp_proto_http" _EXPORTER_BY_OTLP_PROTOCOL = { "grpc": _EXPORTER_OTLP_PROTO_GRPC, "http/protobuf": _EXPORTER_OTLP_PROTO_HTTP, } _EXPORTER_ENV_BY_SIGNAL_TYPE = { "traces": OTEL_TRACES_EXPORTER, "metrics": OTEL_METRICS_EXPORTER, "logs": OTEL_LOGS_EXPORTER, } _PROTOCOL_ENV_BY_SIGNAL_TYPE = { "traces": OTEL_EXPORTER_OTLP_TRACES_PROTOCOL, "metrics": OTEL_EXPORTER_OTLP_METRICS_PROTOCOL, "logs": OTEL_EXPORTER_OTLP_LOGS_PROTOCOL, } _RANDOM_ID_GENERATOR = "random" _DEFAULT_ID_GENERATOR = _RANDOM_ID_GENERATOR _OTEL_SAMPLER_ENTRY_POINT_GROUP = "opentelemetry_traces_sampler" _logger = logging.getLogger(__name__) ExporterArgsMap = Mapping[ type[SpanExporter] | type[MetricExporter] | type[MetricReader] | type[LogRecordExporter], Mapping[str, Any], ] class _ConfigurationExporterSpanProcessorT(Protocol): def __call__(self, span_exporter: SpanExporter, *args, **kwargs) -> SpanProcessor: ... class _ConfigurationExporterLogRecordProcessorT(Protocol): def __call__(self, exporter: LogRecordExporter, *args, **kwargs) -> LogRecordProcessor: ... def _import_config_components(selected_components: Sequence[str], entry_point_name: str) -> list[tuple[str, type]]: component_implementations = [] for selected_component in selected_components: try: component_implementations.append( ( selected_component, next(iter(entry_points(group=entry_point_name, name=selected_component))).load(), ) ) except KeyError: raise RuntimeError(f"Requested entry point '{entry_point_name}' not found") except StopIteration: raise RuntimeError( f"Requested component '{selected_component}' not found in entry point '{entry_point_name}'" ) return component_implementations def _get_sampler() -> str | None: return environ.get(OTEL_TRACES_SAMPLER, None) def _get_id_generator() -> str: return environ.get(OTEL_PYTHON_ID_GENERATOR, _DEFAULT_ID_GENERATOR) def _get_tracer_configurator() -> str | None: return environ.get(OTEL_PYTHON_TRACER_CONFIGURATOR, None) def _get_meter_configurator() -> str | None: return environ.get(OTEL_PYTHON_METER_CONFIGURATOR, None) def _get_logger_configurator() -> str | None: return environ.get(OTEL_PYTHON_LOGGER_CONFIGURATOR, None) def _get_exporter_entry_point(exporter_name: str, signal_type: Literal["traces", "metrics", "logs"]): if exporter_name not in ( _EXPORTER_OTLP, _EXPORTER_OTLP_PROTO_GRPC, _EXPORTER_OTLP_PROTO_HTTP, ): return exporter_name # Checking env vars for OTLP protocol (grpc/http). otlp_protocol = environ.get(_PROTOCOL_ENV_BY_SIGNAL_TYPE[signal_type]) or environ.get(OTEL_EXPORTER_OTLP_PROTOCOL) if not otlp_protocol: if exporter_name == _EXPORTER_OTLP: return _EXPORTER_OTLP_PROTO_GRPC return exporter_name otlp_protocol = otlp_protocol.strip() if exporter_name == _EXPORTER_OTLP: if otlp_protocol not in _EXPORTER_BY_OTLP_PROTOCOL: # Invalid value was set by the env var raise RuntimeError(f"Unsupported OTLP protocol '{otlp_protocol}' is configured") return _EXPORTER_BY_OTLP_PROTOCOL[otlp_protocol] # grpc/http already specified by exporter_name, only add a warning in case # of a conflict. exporter_name_by_env = _EXPORTER_BY_OTLP_PROTOCOL.get(otlp_protocol) if exporter_name_by_env and exporter_name != exporter_name_by_env: _logger.warning( "Conflicting values for %s OTLP exporter protocol, using '%s'", signal_type, exporter_name, ) return exporter_name def _get_exporter_names( signal_type: Literal["traces", "metrics", "logs"], ) -> list[str]: names = environ.get(_EXPORTER_ENV_BY_SIGNAL_TYPE.get(signal_type, "")) if not names or names.lower().strip() == "none": return [] return [_get_exporter_entry_point(_exporter.strip(), signal_type) for _exporter in names.split(",")] def _init_tracing( exporters: dict[str, type[SpanExporter]], id_generator: IdGenerator | None = None, sampler: Sampler | None = None, resource: Resource | None = None, exporter_args_map: ExporterArgsMap | None = None, span_processors: Sequence[SpanProcessor] | None = None, export_span_processor: _ConfigurationExporterSpanProcessorT | None = None, tracer_configurator: _TracerConfiguratorT | None = None, ): provider = TracerProvider( id_generator=id_generator, sampler=sampler, resource=resource, _tracer_configurator=tracer_configurator, ) set_tracer_provider(provider) exporter_args_map = exporter_args_map or {} export_processor = export_span_processor or BatchSpanProcessor span_processors = span_processors or [] for span_processor in span_processors: provider.add_span_processor(span_processor) for _, exporter_class in exporters.items(): exporter_args = exporter_args_map.get(exporter_class, {}) provider.add_span_processor(export_processor(exporter_class(**exporter_args))) def _init_metrics( exporters_or_readers: dict[str, type[MetricExporter | MetricReader]], resource: Resource | None = None, exporter_args_map: ExporterArgsMap | None = None, meter_configurator: _MeterConfiguratorT | None = None, ): metric_readers = [] exporter_args_map = exporter_args_map or {} for _, exporter_or_reader_class in exporters_or_readers.items(): exporter_args = exporter_args_map.get(exporter_or_reader_class, {}) if issubclass(exporter_or_reader_class, MetricReader): metric_readers.append(exporter_or_reader_class(**exporter_args)) else: metric_readers.append(PeriodicExportingMetricReader(exporter_or_reader_class(**exporter_args))) provider = MeterProvider( resource=resource, metric_readers=metric_readers, _meter_configurator=meter_configurator, ) set_meter_provider(provider) # pylint: disable-next=too-many-locals def _init_logging( exporters: dict[str, type[LogRecordExporter]], resource: Resource | None = None, setup_logging_handler: bool = True, exporter_args_map: ExporterArgsMap | None = None, log_record_processors: Sequence[LogRecordProcessor] | None = None, export_log_record_processor: _ConfigurationExporterLogRecordProcessorT | None = None, logger_configurator: _LoggerConfiguratorT | None = None, ): provider = LoggerProvider(resource=resource, _logger_configurator=logger_configurator) set_logger_provider(provider) exporter_args_map = exporter_args_map or {} export_processor = export_log_record_processor or BatchLogRecordProcessor log_record_processors = log_record_processors or [] for log_record_processor in log_record_processors: provider.add_log_record_processor(log_record_processor) for _, exporter_class in exporters.items(): exporter_args = exporter_args_map.get(exporter_class, {}) provider.add_log_record_processor(export_processor(exporter_class(**exporter_args))) if setup_logging_handler: warnings.warn( "The `OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED` environment variable " "and the `LoggingHandler` in `opentelemetry-sdk` that it controls are deprecated." "Install `opentelemetry-instrumentation-logging` package instead.", DeprecationWarning, ) # Add OTel handler handler = LoggingHandler(level=logging.NOTSET, logger_provider=provider) logging.getLogger().addHandler(handler) _overwrite_logging_config_fns(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) def _import_logger_configurator( logger_configurator_name: str | None, ) -> _LoggerConfiguratorT | None: if not logger_configurator_name: return None try: _, logger_configurator_impl = _import_config_components( [logger_configurator_name.strip()], "_opentelemetry_logger_configurator", )[0] except Exception as exc: # pylint: disable=broad-exception-caught _logger.warning( "Using default logger configurator. Failed to load logger configurator, %s: %s", logger_configurator_name, exc, ) return None return logger_configurator_impl def _import_tracer_configurator( tracer_configurator_name: str | None, ) -> _TracerConfiguratorT | None: if not tracer_configurator_name: return None try: _, tracer_configurator_impl = _import_config_components( [tracer_configurator_name.strip()], "_opentelemetry_tracer_configurator", )[0] except Exception as exc: # pylint: disable=broad-exception-caught _logger.warning( "Using default tracer configurator. Failed to load tracer configurator, %s: %s", tracer_configurator_name, exc, ) return None return tracer_configurator_impl def _import_meter_configurator( meter_configurator_name: str | None, ) -> _MeterConfiguratorT | None: if not meter_configurator_name: return None try: _, meter_configurator_impl = _import_config_components( [meter_configurator_name.strip()], "_opentelemetry_meter_configurator", )[0] except Exception as exc: # pylint: disable=broad-exception-caught _logger.warning( "Using default meter configurator. Failed to load meter configurator, %s: %s", meter_configurator_name, exc, ) return None return meter_configurator_impl def _import_exporters( trace_exporter_names: Sequence[str], metric_exporter_names: Sequence[str], log_exporter_names: Sequence[str], ) -> tuple[ dict[str, type[SpanExporter]], dict[str, type[MetricExporter | MetricReader]], dict[str, type[LogRecordExporter]], ]: trace_exporters = {} metric_exporters = {} log_exporters = {} for ( exporter_name, exporter_impl, ) in _import_config_components(trace_exporter_names, "opentelemetry_traces_exporter"): if issubclass(exporter_impl, SpanExporter): trace_exporters[exporter_name] = exporter_impl else: raise RuntimeError(f"{exporter_name} is not a trace exporter") for ( exporter_name, exporter_impl, ) in _import_config_components(metric_exporter_names, "opentelemetry_metrics_exporter"): # The metric exporter components may be push MetricExporter or pull exporters which # subclass MetricReader directly if issubclass(exporter_impl, (MetricExporter, MetricReader)): metric_exporters[exporter_name] = exporter_impl else: raise RuntimeError(f"{exporter_name} is not a metric exporter") for ( exporter_name, exporter_impl, ) in _import_config_components(log_exporter_names, "opentelemetry_logs_exporter"): if issubclass(exporter_impl, LogRecordExporter): log_exporters[exporter_name] = exporter_impl else: raise RuntimeError(f"{exporter_name} is not a log exporter") return trace_exporters, metric_exporters, log_exporters def _import_sampler_factory( sampler_name: str, ) -> Callable[[float | str | None], Sampler]: _, sampler_impl = _import_config_components([sampler_name.strip()], _OTEL_SAMPLER_ENTRY_POINT_GROUP)[0] return sampler_impl def _import_sampler(sampler_name: str | None) -> Sampler | None: if not sampler_name: return None try: sampler_factory = _import_sampler_factory(sampler_name) arg = None if sampler_name in ("traceidratio", "parentbased_traceidratio"): try: rate = float(os.getenv(OTEL_TRACES_SAMPLER_ARG, "")) except (ValueError, TypeError): _logger.warning("Could not convert TRACES_SAMPLER_ARG to float. Using default value 1.0.") rate = 1.0 arg = rate else: arg = os.getenv(OTEL_TRACES_SAMPLER_ARG) sampler = sampler_factory(arg) if not isinstance(sampler, Sampler): message = f"Sampler factory, {sampler_factory}, produced output, {sampler}, which is not a Sampler." _logger.warning(message) raise ValueError(message) return sampler except Exception as exc: # pylint: disable=broad-exception-caught _logger.warning( "Using default sampler. Failed to initialize sampler, %s: %s", sampler_name, exc, ) return None def _import_id_generator(id_generator_name: str) -> IdGenerator: id_generator_name, id_generator_impl = _import_config_components( [id_generator_name.strip()], "opentelemetry_id_generator" )[0] if issubclass(id_generator_impl, IdGenerator): return id_generator_impl() raise RuntimeError(f"{id_generator_name} is not an IdGenerator") def _import_opamp( name: Literal["pre_sdk_init_function", "post_sdk_init_function"], ) -> Callable[[Resource], None] | None: """Helper for OpAMP entry points loading This in development, at the moment we are looking for a callable that takes the resource and instantiate an OpAMP agent. Since configuration is not specified every implementer may have its own. Refer to the opentelemetry-opamp-client package on how to setup the OpAMP agent. """ entry_point = None try: entry_point = next(iter(entry_points(group="_opentelemetry_opamp", name=name))) return entry_point.load() except StopIteration: _logger.debug("No OpAMP init function found") except AttributeError as exc: _logger.warning( "Failed to load OpAMP init function from entry point, %s: %s", entry_point, exc, ) return None def _initialize_components( auto_instrumentation_version: str | None = None, trace_exporter_names: list[str] | None = None, metric_exporter_names: list[str] | None = None, log_exporter_names: list[str] | None = None, sampler: Sampler | None = None, resource_attributes: Attributes | None = None, id_generator: IdGenerator | None = None, setup_logging_handler: bool | None = None, exporter_args_map: ExporterArgsMap | None = None, span_processors: Sequence[SpanProcessor] | None = None, export_span_processor: _ConfigurationExporterSpanProcessorT | None = None, log_record_processors: Sequence[LogRecordProcessor] | None = None, export_log_record_processor: _ConfigurationExporterLogRecordProcessorT | None = None, tracer_configurator: _TracerConfiguratorT | None = None, meter_configurator: _MeterConfiguratorT | None = None, logger_configurator: _LoggerConfiguratorT | None = None, ): # pylint: disable=too-many-locals,too-many-branches if resource_attributes is None: resource_attributes = {} # populate version if using auto-instrumentation if auto_instrumentation_version: resource_attributes[ResourceAttributes.TELEMETRY_AUTO_VERSION] = ( # type: ignore[reportIndexIssue] auto_instrumentation_version ) # if env var OTEL_RESOURCE_ATTRIBUTES is given, it will read the service_name # from the env variable else defaults to "unknown_service" resource = Resource.create(resource_attributes) # OpAMP is a system created to configure OpenTelemetry SDKs with a remote config. # This is different than other init helpers because setting up OpAMP requires distro # provided code as it's not strictly specified. We have two entry points for OpAMP: # one called early for people that want it blocking to get an updated config before # setting up the rest of the SDK and the other after for people that want the # SDK already setup. _init_opamp = _import_opamp("pre_sdk_init_function") if _init_opamp is not None: _init_opamp(resource) if trace_exporter_names is None: trace_exporter_names = [] if metric_exporter_names is None: metric_exporter_names = [] if log_exporter_names is None: log_exporter_names = [] span_exporters, metric_exporters, log_exporters = _import_exporters( trace_exporter_names + _get_exporter_names("traces"), metric_exporter_names + _get_exporter_names("metrics"), log_exporter_names + _get_exporter_names("logs"), ) if sampler is None: sampler_name = _get_sampler() sampler = _import_sampler(sampler_name) if id_generator is None: id_generator_name = _get_id_generator() id_generator = _import_id_generator(id_generator_name) if tracer_configurator is None: tracer_configurator_name = _get_tracer_configurator() tracer_configurator = _import_tracer_configurator(tracer_configurator_name) if meter_configurator is None: meter_configurator_name = _get_meter_configurator() meter_configurator = _import_meter_configurator(meter_configurator_name) if logger_configurator is None: logger_configurator_name = _get_logger_configurator() logger_configurator = _import_logger_configurator(logger_configurator_name) _init_tracing( exporters=span_exporters, id_generator=id_generator, sampler=sampler, resource=resource, exporter_args_map=exporter_args_map, span_processors=span_processors, export_span_processor=export_span_processor, tracer_configurator=tracer_configurator, ) _init_metrics( exporters_or_readers=metric_exporters, resource=resource, exporter_args_map=exporter_args_map, meter_configurator=meter_configurator, ) if setup_logging_handler is None: setup_logging_handler = ( os.getenv(_OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED, "false").strip().lower() == "true" ) _init_logging( log_exporters, resource, setup_logging_handler, exporter_args_map=exporter_args_map, log_record_processors=log_record_processors, export_log_record_processor=export_log_record_processor, logger_configurator=logger_configurator, ) _init_opamp = _import_opamp("post_sdk_init_function") if _init_opamp is not None: _init_opamp(resource) class _BaseConfigurator(ABC): """An ABC for configurators Configurators are used to configure SDKs (i.e. TracerProvider, MeterProvider, Processors...) to reduce the amount of manual configuration required. """ _instance = None _is_instrumented = False def __new__(cls, *args, **kwargs): if cls._instance is None: cls._instance = super().__new__(cls) return cls._instance @abstractmethod def _configure(self, **kwargs): """Configure the SDK""" def configure(self, **kwargs): """Configure the SDK""" self._configure(**kwargs) class _OTelSDKConfigurator(_BaseConfigurator): """A basic Configurator by OTel Python for initializing OTel SDK components Initializes several crucial OTel SDK components (i.e. TracerProvider, MeterProvider, Processors...) according to a default implementation. Other Configurators can subclass and slightly alter this initialization. NOTE: This class should not be instantiated nor should it become an entry point on the `opentelemetry-sdk` package. Instead, distros should subclass this Configurator and enhance it as needed. """ def _configure(self, **kwargs): if config_file := environ.get(OTEL_CONFIG_FILE): # Declarative configuration lives in the separate # ``opentelemetry-configuration`` package. Import lazily so the # SDK has no runtime dependency on it; users who don't set # ``OTEL_CONFIG_FILE`` never pay the import cost. try: # opentelemetry-configuration is an optional runtime dep # and is not installed in this package's lint env, so # silence the static-analysis no-name-in-module on the # conditional import. # pylint: disable=import-outside-toplevel,no-name-in-module from opentelemetry.configuration import ( # noqa: PLC0415 configure_sdk, load_config_file, ) except ImportError as exc: raise RuntimeError( f"{OTEL_CONFIG_FILE} is set but " "opentelemetry-configuration is not installed. " "Install it with: pip install opentelemetry-configuration" ) from exc if kwargs: _logger.warning( "%s is set; ignoring configurator kwargs: %s", OTEL_CONFIG_FILE, sorted(kwargs), ) configure_sdk(load_config_file(config_file)) return _initialize_components(**kwargs) python-opentelemetry-1.45.2/opentelemetry-sdk/src/opentelemetry/sdk/_logs/000077500000000000000000000000001527070070700270455ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-sdk/src/opentelemetry/sdk/_logs/__init__.py000066400000000000000000000014221527070070700311550ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from opentelemetry.sdk._logs._internal import ( ConcurrentMultiLogRecordProcessor, LogDroppedAttributesWarning, Logger, LoggerProvider, LoggingHandler, LogLimits, LogRecordDroppedAttributesWarning, LogRecordLimits, LogRecordProcessor, ReadableLogRecord, ReadWriteLogRecord, SynchronousMultiLogRecordProcessor, ) __all__ = [ "ConcurrentMultiLogRecordProcessor", "LogDroppedAttributesWarning", "LogLimits", "LogRecordDroppedAttributesWarning", "LogRecordLimits", "LogRecordProcessor", "Logger", "LoggerProvider", "LoggingHandler", "ReadWriteLogRecord", "ReadableLogRecord", "SynchronousMultiLogRecordProcessor", ] python-opentelemetry-1.45.2/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/000077500000000000000000000000001527070070700310205ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py000066400000000000000000001156311527070070700331400ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations import abc import atexit import base64 import concurrent.futures import json import logging import os import threading import traceback import warnings from collections.abc import Callable, Mapping, Sequence from dataclasses import dataclass, field from os import environ from threading import Lock from time import time_ns from types import NoneType from typing import ( # noqa Any, Tuple, Union, cast, overload, ) from weakref import WeakMethod, WeakSet from typing_extensions import deprecated from opentelemetry._logs import Logger as APILogger from opentelemetry._logs import LoggerProvider as APILoggerProvider from opentelemetry._logs import ( LogRecord, NoOpLogger, SeverityNumber, get_logger, get_logger_provider, ) from opentelemetry.attributes import BoundedAttributes from opentelemetry.context import get_current from opentelemetry.context.context import Context from opentelemetry.metrics import MeterProvider, get_meter_provider from opentelemetry.sdk._logs._internal._exceptions import ( _copy_log_record_with_exception, _create_log_record_with_exception, _set_log_record_exception_attributes, ) from opentelemetry.sdk._logs._internal._logger_metrics import ( LoggerMetricsT, create_logger_metrics, ) from opentelemetry.sdk.environment_variables import ( OTEL_ATTRIBUTE_COUNT_LIMIT, OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT, OTEL_LOGRECORD_ATTRIBUTE_COUNT_LIMIT, OTEL_LOGRECORD_ATTRIBUTE_VALUE_LENGTH_LIMIT, OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED, OTEL_SDK_DISABLED, ) from opentelemetry.sdk.environment_variables._internal import ( parse_boolean_environment_variable, ) from opentelemetry.sdk.resources import ( Resource, _get_process_dependent_resource, ) from opentelemetry.sdk.util import ns_to_iso_str from opentelemetry.sdk.util._configurator import RuleBasedConfigurator from opentelemetry.sdk.util.instrumentation import ( InstrumentationScope, ) from opentelemetry.semconv.attributes import ( code_attributes, exception_attributes, ) from opentelemetry.trace import ( format_span_id, format_trace_id, ) from opentelemetry.util.types import AnyValue, Attributes # pylint: disable=too-many-lines _DEFAULT_OTEL_ATTRIBUTE_COUNT_LIMIT = 128 _ENV_VALUE_UNSET = "" _logger = logging.getLogger(__name__) class BytesEncoder(json.JSONEncoder): def default(self, o): if isinstance(o, bytes): return base64.b64encode(o).decode() return super().default(o) class LogRecordDroppedAttributesWarning(UserWarning): """Custom warning to indicate dropped log attributes due to limits. This class is used to filter and handle these specific warnings separately from other warnings, ensuring that they are only shown once without interfering with default user warnings. """ warnings.simplefilter("once", LogRecordDroppedAttributesWarning) @deprecated( "Use LogRecordDroppedAttributesWarning. Since logs are not stable yet this WILL be removed in future releases." ) class LogDroppedAttributesWarning(LogRecordDroppedAttributesWarning): pass class LogRecordLimits: """This class is based on a SpanLimits class in the Tracing module. This class represents the limits that should be enforced on recorded data such as events, links, attributes etc. This class does not enforce any limits itself. It only provides a way to read limits from env, default values and from user provided arguments. All limit arguments must be either a non-negative integer, ``None`` or ``LogRecordLimits.UNSET``. - All limit arguments are optional. - If a limit argument is not set, the class will try to read its value from the corresponding environment variable. - If the environment variable is not set, the default value, if any, will be used. Limit precedence: - If a model specific limit is set, it will be used. - Else if the corresponding global limit is set, it will be used. - Else if the model specific limit has a default value, the default value will be used. - Else if the global limit has a default value, the default value will be used. Args: max_attributes: Maximum number of attributes that can be added to a log record (global fallback). Environment variable: ``OTEL_ATTRIBUTE_COUNT_LIMIT`` Default: {_DEFAULT_OTEL_ATTRIBUTE_COUNT_LIMIT} max_attribute_length: Maximum length an attribute value can have (global fallback). Values longer than the specified length will be truncated. max_log_record_attributes: Maximum number of attributes that can be added to a log record. Environment variable: ``OTEL_LOGRECORD_ATTRIBUTE_COUNT_LIMIT`` Falls back to ``max_attributes`` when unset. max_log_record_attribute_length: Maximum length a log record attribute value can have. Environment variable: ``OTEL_LOGRECORD_ATTRIBUTE_VALUE_LENGTH_LIMIT`` Falls back to ``max_attribute_length`` when unset. """ UNSET = -1 def __init__( self, max_attributes: int | None = None, max_attribute_length: int | None = None, max_log_record_attributes: int | None = None, max_log_record_attribute_length: int | None = None, ): # global attribute count global_max_attributes = self._from_env_if_absent(max_attributes, OTEL_ATTRIBUTE_COUNT_LIMIT) self.max_attributes = ( global_max_attributes if global_max_attributes is not None else _DEFAULT_OTEL_ATTRIBUTE_COUNT_LIMIT ) # global attribute length self.max_attribute_length = self._from_env_if_absent( max_attribute_length, OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT, ) self.max_log_record_attributes = self._from_env_if_absent( max_log_record_attributes, OTEL_LOGRECORD_ATTRIBUTE_COUNT_LIMIT, default=self.max_attributes, ) self.max_log_record_attribute_length = self._from_env_if_absent( max_log_record_attribute_length, OTEL_LOGRECORD_ATTRIBUTE_VALUE_LENGTH_LIMIT, default=self.max_attribute_length, ) def __repr__(self): return ( f"{type(self).__name__}(" f"max_attributes={self.max_attributes}, " f"max_attribute_length={self.max_attribute_length}, " f"max_log_record_attributes={self.max_log_record_attributes}, " f"max_log_record_attribute_length={self.max_log_record_attribute_length})" ) @classmethod def _from_env_if_absent(cls, value: int | None, env_var: str, default: int | None = None) -> int | None: if value == cls.UNSET: return None err_msg = "{} must be a non-negative integer but got {}" # if no value is provided for the limit, try to load it from env if value is None: # return default value if env var is not set if env_var not in environ: return default str_value = environ.get(env_var, "").strip().lower() if str_value == _ENV_VALUE_UNSET: return None try: value = int(str_value) except ValueError: raise ValueError(err_msg.format(env_var, str_value)) if value < 0: raise ValueError(err_msg.format(env_var, value)) return value @deprecated("Use LogRecordLimits. Since logs are not stable yet this WILL be removed in future releases.") class LogLimits(LogRecordLimits): pass @dataclass(frozen=True) class ReadableLogRecord: """Readable LogRecord should be kept exactly in-sync with ReadWriteLogRecord, only difference is the frozen=True param.""" log_record: LogRecord resource: Resource instrumentation_scope: InstrumentationScope | None = None limits: LogRecordLimits | None = None @property def dropped_attributes(self) -> int: if isinstance(self.log_record.attributes, BoundedAttributes): return self.log_record.attributes.dropped return 0 def to_json(self, indent: int | None = 4) -> str: return json.dumps( { "body": self.log_record.body, "severity_number": self.log_record.severity_number.value if self.log_record.severity_number is not None else None, "severity_text": self.log_record.severity_text, "attributes": (dict(self.log_record.attributes) if bool(self.log_record.attributes) else None), "dropped_attributes": self.dropped_attributes, "timestamp": ns_to_iso_str(self.log_record.timestamp) if self.log_record.timestamp is not None else None, "observed_timestamp": ns_to_iso_str(self.log_record.observed_timestamp), "trace_id": ( f"0x{format_trace_id(self.log_record.trace_id)}" if self.log_record.trace_id is not None else "" ), "span_id": ( f"0x{format_span_id(self.log_record.span_id)}" if self.log_record.span_id is not None else "" ), "trace_flags": self.log_record.trace_flags, "resource": json.loads(self.resource.to_json()), "event_name": self.log_record.event_name if self.log_record.event_name else "", }, indent=indent, cls=BytesEncoder, ) @dataclass class ReadWriteLogRecord: """A ReadWriteLogRecord instance represents an event being logged. ReadWriteLogRecord instances are created and emitted via `Logger` every time something is logged. They contain all the information pertinent to the event being logged. """ log_record: LogRecord resource: Resource | None = Resource.create({}) instrumentation_scope: InstrumentationScope | None = None limits: LogRecordLimits = field(default_factory=LogRecordLimits) def __post_init__(self): self.log_record.attributes = BoundedAttributes( maxlen=self.limits.max_log_record_attributes, attributes=self.log_record.attributes if self.log_record.attributes else None, immutable=False, max_value_len=self.limits.max_log_record_attribute_length, extended_attributes=True, ) if self.dropped_attributes > 0: warnings.warn( "Log record attributes were dropped due to limits", LogRecordDroppedAttributesWarning, stacklevel=2, ) def __eq__(self, other: object) -> bool: if not isinstance(other, ReadWriteLogRecord): return NotImplemented return self.__dict__ == other.__dict__ @property def dropped_attributes(self) -> int: if isinstance(self.log_record.attributes, BoundedAttributes): return self.log_record.attributes.dropped return 0 @classmethod def _from_api_log_record( cls, *, record: LogRecord, resource: Resource, instrumentation_scope: InstrumentationScope | None = None, limits: LogRecordLimits | None = None, ) -> ReadWriteLogRecord: return cls( log_record=record, resource=resource, instrumentation_scope=instrumentation_scope, **({} if limits is None else {"limits": limits}), ) class LogRecordProcessor(abc.ABC): """Interface to hook the log record emitting action. Log processors can be registered directly using :func:`LoggerProvider.add_log_record_processor` and they are invoked in the same order as they were registered. Implementers of custom log processors should be aware of the following: Error Handling -------------- According to the OpenTelemetry error handling principles, the SDK should not throw unhandled exceptions at runtime. When implementing a custom ``LogRecordProcessor``, it is the **processor's responsibility** to handle any exceptions that may be raised by the exporter's ``export()`` method. The ``LogRecordExporter.export()`` method may raise exceptions (e.g., network errors, timeouts). If these exceptions are not caught, they will propagate up and potentially crash the application. Custom processor implementations should wrap exporter calls in a try/except block. See ``SimpleLogRecordProcessor`` for a reference implementation:: def on_emit(self, log_record: ReadWriteLogRecord): try: self._exporter.export((log_record,)) except Exception: # pylint: disable=broad-exception-caught logger.exception("Exception while exporting logs.") The ``BatchLogRecordProcessor`` handles this implicitly since export operations occur in a background thread where exceptions cannot bubble up to the caller. """ @abc.abstractmethod def on_emit(self, log_record: ReadWriteLogRecord) -> None: """Emits the ``ReadWriteLogRecord``. Implementers should handle any exceptions raised during log processing to prevent application crashes. See the class docstring for details on error handling expectations. """ def enabled( # pylint: disable=no-self-use,unused-argument self, *, context: Context | None = None, instrumentation_scope: InstrumentationScope | None = None, severity_number: SeverityNumber | None = None, event_name: str | None = None, ) -> bool: """Returns whether this processor would emit a record for the given arguments. Processors that support filtering MAY override this method. The default implementation returns ``True``. """ return True @abc.abstractmethod def shutdown(self) -> None: """Called when a :class:`opentelemetry.sdk._logs.Logger` is shutdown""" @abc.abstractmethod def force_flush(self, timeout_millis: int = 30000) -> bool: """Export all the received logs to the configured Exporter that have not yet been exported. Args: timeout_millis: The maximum amount of time to wait for logs to be exported. Returns: False if the timeout is exceeded, True otherwise. """ # Temporary fix until https://github.com/PyCQA/pylint/issues/4098 is resolved # pylint:disable=no-member class SynchronousMultiLogRecordProcessor(LogRecordProcessor): """Implementation of class:`LogRecordProcessor` that forwards all received events to a list of log processors sequentially. The underlying log processors are called in sequential order as they were added. """ def __init__(self): # use a tuple to avoid race conditions when adding a new log and # iterating through it on "emit". self._log_record_processors = () # type: tuple[LogRecordProcessor, ...] self._lock = threading.Lock() def add_log_record_processor(self, log_record_processor: LogRecordProcessor) -> None: """Adds a Logprocessor to the list of log processors handled by this instance""" with self._lock: self._log_record_processors += (log_record_processor,) def on_emit(self, log_record: ReadWriteLogRecord) -> None: for lp in self._log_record_processors: lp.on_emit(log_record) def enabled( self, *, context: Context | None = None, instrumentation_scope: InstrumentationScope | None = None, severity_number: SeverityNumber | None = None, event_name: str | None = None, ) -> bool: processors = self._log_record_processors return any( lp.enabled( context=context, instrumentation_scope=instrumentation_scope, severity_number=severity_number, event_name=event_name, ) for lp in processors ) def shutdown(self) -> None: """Shutdown the log processors one by one""" for lp in self._log_record_processors: lp.shutdown() def force_flush(self, timeout_millis: int = 30000) -> bool: """Force flush the log processors one by one Args: timeout_millis: The maximum amount of time to wait for logs to be exported. If the first n log processors exceeded the timeout then remaining log processors will not be flushed. Returns: True if all the log processors flushes the logs within timeout, False otherwise. """ deadline_ns = time_ns() + timeout_millis * 1000000 all_flushed = True for lp in self._log_record_processors: current_ts = time_ns() if current_ts >= deadline_ns: return False if lp.force_flush((deadline_ns - current_ts) // 1000000) is False: all_flushed = False return all_flushed class ConcurrentMultiLogRecordProcessor(LogRecordProcessor): """Implementation of :class:`LogRecordProcessor` that forwards all received events to a list of log processors in parallel. Calls to the underlying log processors are forwarded in parallel by submitting them to a thread pool executor and waiting until each log processor finished its work. Args: max_workers: The number of threads managed by the thread pool executor and thus defining how many log processors can work in parallel. """ def __init__(self, max_workers: int = 2): # use a tuple to avoid race conditions when adding a new log and # iterating through it on "emit". self._log_record_processors = () # type: tuple[LogRecordProcessor, ...] self._lock = threading.Lock() self._executor = concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) def add_log_record_processor(self, log_record_processor: LogRecordProcessor): with self._lock: self._log_record_processors += (log_record_processor,) def _submit_and_wait( self, func: Callable[[LogRecordProcessor], Callable[..., None]], *args: Any, **kwargs: Any, ): futures = [] for lp in self._log_record_processors: future = self._executor.submit(func(lp), *args, **kwargs) futures.append(future) for future in futures: future.result() def on_emit(self, log_record: ReadWriteLogRecord) -> None: self._submit_and_wait(lambda lp: lp.on_emit, log_record) def enabled( self, *, context: Context | None = None, instrumentation_scope: InstrumentationScope | None = None, severity_number: SeverityNumber | None = None, event_name: str | None = None, ) -> bool: processors = self._log_record_processors return any( lp.enabled( context=context, instrumentation_scope=instrumentation_scope, severity_number=severity_number, event_name=event_name, ) for lp in processors ) def shutdown(self) -> None: self._submit_and_wait(lambda lp: lp.shutdown) def force_flush(self, timeout_millis: int = 30000) -> bool: """Force flush the log processors in parallel. Args: timeout_millis: The maximum amount of time to wait for logs to be exported. Returns: True if all the log processors flushes the logs within timeout, False otherwise. """ futures = [] for lp in self._log_record_processors: future = self._executor.submit(lp.force_flush, timeout_millis) futures.append(future) done_futures, not_done_futures = concurrent.futures.wait(futures, timeout_millis / 1e3) if not_done_futures: return False for future in done_futures: if future.result() is False: return False return True # 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 """ def __init__( self, level: int = logging.NOTSET, logger_provider: APILoggerProvider | None = None, ) -> None: super().__init__(level=level) self._logger_provider = logger_provider or get_logger_provider() warnings.warn( "`LoggingHandler` in `opentelemetry-sdk` is deprecated. Use the " "handler from `opentelemetry-instrumentation-logging` instead.", DeprecationWarning, ) @staticmethod def _get_attributes(record: logging.LogRecord) -> Attributes: attributes = {k: v for k, v in vars(record).items() if k not in _RESERVED_ATTRS} # 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 def _translate(self, record: logging.LogRecord) -> LogRecord: timestamp = int(record.created * 1e9) observered_timestamp = time_ns() attributes = self._get_attributes(record) severity_number = std_to_otel(record.levelno) if self.formatter: body = self.format(record) else: # `record.getMessage()` uses `record.msg` as a template to format # `record.args` into. There is a special case in `record.getMessage()` # where it will only attempt formatting if args are provided, # otherwise, it just stringifies `record.msg`. # # Since the OTLP body field has a type of 'any' and the logging module # is sometimes used in such a way that objects incorrectly end up # set as record.msg, in those cases we would like to bypass # `record.getMessage()` completely and set the body to the object # itself instead of its string representation. # For more background, see: https://github.com/open-telemetry/opentelemetry-python/pull/4216 if not record.args and not isinstance(record.msg, str): # if record.msg is not a value we can export, cast it to string # TODO: https://github.com/open-telemetry/opentelemetry-python/issues/5304 - do something better # than just casting to a string. if not isinstance( record.msg, ( NoneType, bool, bytes, int, float, str, Sequence, Mapping, ), ): body = str(record.msg) else: body = record.msg else: body = record.getMessage() # 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 "WARNING" -> OTel "WARN" (see #3548) # Python "CRITICAL" -> OTel "FATAL" (see #4984) _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, ) 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. """ logger = get_logger(record.name, logger_provider=self._logger_provider) if not isinstance(logger, NoOpLogger): logger.emit(self._translate(record)) 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() @dataclass class _LoggerConfig: is_enabled: bool = True @classmethod def default(cls) -> _LoggerConfig: return _LoggerConfig() class Logger(APILogger): def __init__( self, resource: Resource, multi_log_record_processor: SynchronousMultiLogRecordProcessor | ConcurrentMultiLogRecordProcessor, instrumentation_scope: InstrumentationScope, *, logger_metrics: LoggerMetricsT, log_record_limits: LogRecordLimits | None = None, _logger_config: _LoggerConfig, ): super().__init__( instrumentation_scope.name, instrumentation_scope.version, instrumentation_scope.schema_url, instrumentation_scope.attributes, ) self._resource = resource self._multi_log_record_processor = multi_log_record_processor self._instrumentation_scope = instrumentation_scope self._logger_metrics = logger_metrics self._logger_config = _logger_config self._log_record_limits = log_record_limits or LogRecordLimits() def _is_enabled(self) -> bool: return self._logger_config.is_enabled def enabled( self, *, context: Context | None = None, severity_number: SeverityNumber | None = None, event_name: str | None = None, ) -> bool: if not self._is_enabled(): return False return self._multi_log_record_processor.enabled( context=context, instrumentation_scope=self._instrumentation_scope, severity_number=severity_number, event_name=event_name, ) def _set_logger_config(self, logger_config: _LoggerConfig) -> None: self._logger_config = logger_config def _set_resource(self, resource: Resource) -> None: self._resource = resource @property def instrumentation_scope(self): return self._instrumentation_scope @property def resource(self): return self._resource # pylint: disable=arguments-differ def emit( self, record: LogRecord | None = None, *, timestamp: int | None = None, observed_timestamp: int | None = None, context: Context | None = None, severity_number: SeverityNumber | None = None, severity_text: str | None = None, body: AnyValue | None = None, attributes: Attributes = None, event_name: str | None = None, exception: BaseException | None = None, ) -> None: """Emits the :class:`ReadWriteLogRecord` by setting instrumentation scope and forwarding to the processor. """ if not self._is_enabled(): return # If a record is provided, use it directly if record is not None: if not isinstance(record, ReadWriteLogRecord): if record.exception is not None: record = _copy_log_record_with_exception(record) # pylint:disable=protected-access writable_record = ReadWriteLogRecord._from_api_log_record( record=record, resource=self._resource, instrumentation_scope=self._instrumentation_scope, limits=self._log_record_limits, ) else: _set_log_record_exception_attributes(record.log_record) writable_record = record else: # Create a record from individual parameters log_record = _create_log_record_with_exception( timestamp=timestamp, observed_timestamp=observed_timestamp, context=context, severity_number=severity_number, severity_text=severity_text, body=body, attributes=attributes, event_name=event_name, exception=exception, ) # pylint:disable=protected-access writable_record = ReadWriteLogRecord._from_api_log_record( record=log_record, resource=self._resource, instrumentation_scope=self._instrumentation_scope, limits=self._log_record_limits, ) self._logger_metrics.emit_log() self._multi_log_record_processor.on_emit(writable_record) _LoggerConfiguratorT = Callable[[InstrumentationScope], _LoggerConfig] _RuleBasedLoggerConfigurator = RuleBasedConfigurator[_LoggerConfig] def _default_logger_configurator( _logger_scope: InstrumentationScope, ) -> _LoggerConfig: return _LoggerConfig.default() def _disable_logger_configurator( _logger_scope: InstrumentationScope, ) -> _LoggerConfig: return _LoggerConfig(is_enabled=False) class LoggerProvider(APILoggerProvider): def __init__( self, resource: Resource | None = None, shutdown_on_exit: bool = True, multi_log_record_processor: SynchronousMultiLogRecordProcessor | ConcurrentMultiLogRecordProcessor | None = None, *, meter_provider: MeterProvider | None = None, log_record_limits: LogRecordLimits | None = None, _logger_configurator: _LoggerConfiguratorT | None = None, ): if resource is None: self._resource = Resource.create({}) else: self._resource = resource self._multi_log_record_processor = multi_log_record_processor or SynchronousMultiLogRecordProcessor() self._logger_metrics = create_logger_metrics( meter_provider or get_meter_provider(), parse_boolean_environment_variable(OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED), ) disabled = environ.get(OTEL_SDK_DISABLED, "") self._disabled = disabled.lower().strip() == "true" self._logger_configurator = _logger_configurator or _default_logger_configurator self._log_record_limits = log_record_limits or LogRecordLimits() self._at_exit_handler = None if shutdown_on_exit: self._at_exit_handler = atexit.register(self.shutdown) self._logger_cache = {} self._logger_cache_lock = Lock() self._active_loggers: WeakSet[Logger] = WeakSet() self._active_loggers_lock = Lock() if hasattr(os, "register_at_fork"): weak_at_fork = WeakMethod(self._handle_fork) def _after_in_child() -> None: if at_fork := weak_at_fork(): at_fork() os.register_at_fork(after_in_child=_after_in_child) def _handle_fork(self) -> None: self._logger_cache_lock = Lock() self._active_loggers_lock = Lock() self._update_resource(_get_process_dependent_resource()) @property def resource(self): return self._resource def _update_resource(self, resource: Resource) -> None: with self._active_loggers_lock: self._resource = self._resource.merge(resource) for logger in list(self._active_loggers): # pylint: disable-next=protected-access logger._set_resource(self._resource) def _get_logger_no_cache( self, name: str, version: str | None = None, schema_url: str | None = None, attributes: Attributes = None, ) -> Logger: scope = InstrumentationScope(name, version, schema_url, attributes) return Logger( self._resource, self._multi_log_record_processor, scope, logger_metrics=self._logger_metrics, _logger_config=self._apply_logger_configurator(scope), log_record_limits=self._log_record_limits, ) def _get_logger_cached( self, name: str, version: str | None = None, schema_url: str | None = None, ) -> Logger: with self._logger_cache_lock: key = (name, version, schema_url) if key in self._logger_cache: return self._logger_cache[key] self._logger_cache[key] = self._get_logger_no_cache(name, version, schema_url) return self._logger_cache[key] def get_logger( self, name: str, version: str | None = None, schema_url: str | None = None, attributes: Attributes = None, ) -> APILogger: if self._disabled: return NoOpLogger( name, version=version, schema_url=schema_url, attributes=attributes, ) logger = ( self._get_logger_cached(name, version, schema_url) if attributes is None else self._get_logger_no_cache(name, version, schema_url, attributes) ) with self._active_loggers_lock: self._active_loggers.add(logger) return logger def add_log_record_processor(self, log_record_processor: LogRecordProcessor): """Registers a new :class:`LogRecordProcessor` for this `LoggerProvider` instance. The log processors are invoked in the same order they are registered. """ self._multi_log_record_processor.add_log_record_processor(log_record_processor) def _set_logger_configurator(self, *, logger_configurator: _LoggerConfiguratorT): """Set a new LoggerConfigurator for this LoggerProvider. Setting a new LoggerConfigurator will result in the configurator being called for each outstanding Logger and for any newly created loggers thereafter. Therefore, it is important that the provided function returns quickly. """ self._logger_configurator = logger_configurator with self._active_loggers_lock: for logger in list(self._active_loggers): # pylint: disable-next=protected-access logger._set_logger_config(self._apply_logger_configurator(logger.instrumentation_scope)) def _apply_logger_configurator(self, instrumentation_scope: InstrumentationScope) -> _LoggerConfig: try: return self._logger_configurator(instrumentation_scope) # pylint: disable-next=broad-exception-caught except Exception: _logger.exception( "logger configurator failed for scope '%s', using default config", instrumentation_scope.name, ) return _LoggerConfig.default() def shutdown(self) -> None: """Shuts down the log processors.""" self._multi_log_record_processor.shutdown() if self._at_exit_handler is not None: atexit.unregister(self._at_exit_handler) self._at_exit_handler = None def force_flush(self, timeout_millis: int = 30000) -> bool: """Force flush the log processors. Args: timeout_millis: The maximum amount of time to wait for logs to be exported. Returns: True if all the log processors flushes the logs within timeout, False otherwise. """ return self._multi_log_record_processor.force_flush(timeout_millis) _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 python log levelno as defined in https://docs.python.org/3/library/logging.html#logging-levels to OTel log severity number as defined here: 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] python-opentelemetry-1.45.2/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/_exceptions.py000066400000000000000000000071131527070070700337140ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations import traceback from opentelemetry._logs import LogRecord from opentelemetry.attributes import BoundedAttributes from opentelemetry.semconv.attributes import exception_attributes from opentelemetry.util.types import AnyValue, Attributes def _get_exception_attributes( exception: BaseException, ) -> dict[str, AnyValue]: stacktrace = "".join(traceback.format_exception(type(exception), value=exception, tb=exception.__traceback__)) module = type(exception).__module__ qualname = type(exception).__qualname__ exception_type = f"{module}.{qualname}" if module and module != "builtins" else qualname return { exception_attributes.EXCEPTION_TYPE: exception_type, exception_attributes.EXCEPTION_MESSAGE: str(exception), exception_attributes.EXCEPTION_STACKTRACE: stacktrace, } def _get_attributes_with_exception( attributes: Attributes, exception: BaseException | None, ) -> Attributes: if exception is None: return attributes exception_attributes_map = _get_exception_attributes(exception) if attributes is None: attributes_map: Attributes = {} else: attributes_map = attributes if isinstance(attributes_map, BoundedAttributes): bounded_attributes = attributes_map merged = BoundedAttributes( maxlen=bounded_attributes.maxlen, attributes=bounded_attributes, immutable=False, max_value_len=bounded_attributes.max_value_len, ) merged.dropped = bounded_attributes.dropped for key, value in exception_attributes_map.items(): if key not in merged: merged[key] = value return merged return exception_attributes_map | dict(attributes_map.items()) def _copy_log_record( record: LogRecord, attributes: Attributes, ) -> LogRecord: copied_record = LogRecord( timestamp=record.timestamp, observed_timestamp=record.observed_timestamp, context=record.context, severity_text=record.severity_text, severity_number=record.severity_number, body=record.body, attributes=attributes, event_name=record.event_name, exception=getattr(record, "exception", None), ) copied_record.trace_id = record.trace_id copied_record.span_id = record.span_id copied_record.trace_flags = record.trace_flags return copied_record def _copy_log_record_with_exception(record: LogRecord) -> LogRecord: return _copy_log_record( record, _get_attributes_with_exception(record.attributes, record.exception), ) def _set_log_record_exception_attributes(record: LogRecord) -> None: record.attributes = _get_attributes_with_exception( record.attributes, record.exception, ) def _create_log_record_with_exception( *, timestamp: int | None = None, observed_timestamp: int | None = None, context=None, severity_number=None, severity_text: str | None = None, body: AnyValue | None = None, attributes: Attributes = None, event_name: str | None = None, exception: BaseException | None = None, ) -> LogRecord: return LogRecord( timestamp=timestamp, observed_timestamp=observed_timestamp, context=context, severity_number=severity_number, severity_text=severity_text, body=body, attributes=_get_attributes_with_exception(attributes, exception), event_name=event_name, exception=exception, ) _logger_metrics.py000066400000000000000000000016111527070070700344560ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from typing import Protocol from opentelemetry import metrics as metrics_api from opentelemetry.semconv._incubating.metrics.otel_metrics import ( create_otel_sdk_log_created, ) class LoggerMetricsT(Protocol): def emit_log(self) -> None: ... class NoOpLoggerMetrics: def emit_log(self) -> None: pass class LoggerMetrics: def __init__(self, meter_provider: metrics_api.MeterProvider) -> None: meter = meter_provider.get_meter("opentelemetry-sdk") self._created_logs = create_otel_sdk_log_created(meter) def emit_log(self) -> None: self._created_logs.add(1) def create_logger_metrics( meter_provider: metrics_api.MeterProvider, enabled: bool, ) -> LoggerMetricsT: if not enabled: return NoOpLoggerMetrics() return LoggerMetrics(meter_provider) python-opentelemetry-1.45.2/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/export/000077500000000000000000000000001527070070700323415ustar00rootroot00000000000000__init__.py000066400000000000000000000340321527070070700343750ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/export# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations import abc import copy import enum import logging import sys from collections.abc import Callable, Sequence from os import environ, linesep from typing import IO from typing_extensions import deprecated from opentelemetry.context import ( _ON_EMIT_RECURSION_COUNT_KEY, _SUPPRESS_INSTRUMENTATION_KEY, Context, attach, detach, get_value, set_value, ) from opentelemetry.metrics import MeterProvider, get_meter_provider from opentelemetry.sdk._logs import ( LogRecordProcessor, ReadableLogRecord, ReadWriteLogRecord, ) from opentelemetry.sdk._shared_internal import ( BatchProcessor, DuplicateFilter, ) from opentelemetry.sdk._shared_internal._processor_metrics import ( create_processor_metrics, ) from opentelemetry.sdk.environment_variables import ( OTEL_BLRP_EXPORT_TIMEOUT, OTEL_BLRP_MAX_EXPORT_BATCH_SIZE, OTEL_BLRP_MAX_QUEUE_SIZE, OTEL_BLRP_SCHEDULE_DELAY, OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED, ) from opentelemetry.sdk.environment_variables._internal import ( parse_boolean_environment_variable, ) from opentelemetry.sdk.resources import Resource from opentelemetry.semconv._incubating.attributes.otel_attributes import ( OtelComponentTypeValues, ) _DEFAULT_SCHEDULE_DELAY_MILLIS = 1000 _DEFAULT_MAX_EXPORT_BATCH_SIZE = 512 _DEFAULT_EXPORT_TIMEOUT_MILLIS = 30000 _DEFAULT_MAX_QUEUE_SIZE = 2048 _ENV_VAR_INT_VALUE_ERROR_MESSAGE = "Unable to parse value for %s as integer. Defaulting to %s." _logger = logging.getLogger(__name__) _logger.addFilter(DuplicateFilter()) _propagate_false_logger = logging.getLogger(__name__ + ".propagate.false") _propagate_false_logger.propagate = False class LogRecordExportResult(enum.Enum): SUCCESS = 0 FAILURE = 1 @deprecated("Use LogRecordExportResult. Since logs are not stable yet this WILL be removed in future releases.") class LogExportResult(enum.Enum): SUCCESS = 0 FAILURE = 1 class LogRecordExporter(abc.ABC): """Interface for exporting logs. Interface to be implemented by services that want to export logs received in their own format. To export data this MUST be registered to the :class:`opentelemetry.sdk._logs.Logger` using a log processor. Important --------- The ``export()`` method may raise exceptions (e.g., network errors, timeouts, serialization errors). It is the responsibility of the ``LogRecordProcessor`` calling this exporter to handle these exceptions appropriately to prevent application crashes. See ``LogRecordProcessor`` for guidance on implementing proper error handling. """ @abc.abstractmethod def export(self, batch: Sequence[ReadableLogRecord]) -> LogRecordExportResult: """Exports a batch of logs. Args: batch: The list of ``ReadableLogRecord`` objects to be exported. Returns: The result of the export. Raises: Exception: This method may raise exceptions on network errors, timeouts, or other failures. Callers (i.e., log processors) should handle these exceptions to comply with OpenTelemetry error handling principles. """ @abc.abstractmethod def shutdown(self): """Shuts down the exporter. Called when the SDK is shut down. """ @abc.abstractmethod def force_flush(self, timeout_millis: int = 30000) -> bool: """Hint to ensure that the export of any ``ReadableLogRecord`` objects the exporter has received prior to the call to ``force_flush`` SHOULD be completed as soon as possible, preferably before returning from this method. Args: timeout_millis: The maximum amount of time to wait for the flush to complete, in milliseconds. Returns: ``True`` if the flush completed successfully within the timeout, ``False`` otherwise. """ @deprecated("Use LogRecordExporter. Since logs are not stable yet this WILL be removed in future releases.") class LogExporter(LogRecordExporter): pass class ConsoleLogRecordExporter(LogRecordExporter): """Implementation of :class:`LogRecordExporter` that prints log records to the console. This class can be used for diagnostic purposes. It prints the exported log records to the console STDOUT. """ def __init__( self, out: IO = sys.stdout, formatter: Callable[[ReadableLogRecord], str] = lambda record: record.to_json() + linesep, ): self.out = out self.formatter = formatter def export(self, batch: Sequence[ReadableLogRecord]): for log_record in batch: self.out.write(self.formatter(log_record)) self.out.flush() return LogRecordExportResult.SUCCESS def shutdown(self): pass def force_flush(self, timeout_millis: int = 30000) -> bool: return True @deprecated("Use ConsoleLogRecordExporter. Since logs are not stable yet this WILL be removed in future releases.") class ConsoleLogExporter(ConsoleLogRecordExporter): pass class SimpleLogRecordProcessor(LogRecordProcessor): """Implementation of LogRecordProcessor that exports logs synchronously. This processor passes received logs directly to the configured ``LogRecordExporter`` as soon as they are emitted. This class serves as a reference implementation for custom log processors, demonstrating proper error handling. Note how the ``on_emit`` method wraps the exporter call in a try/except block to prevent exceptions from propagating to the application. """ def __init__( self, exporter: LogRecordExporter, *, meter_provider: MeterProvider | None = None, ): self._exporter = exporter self._shutdown = False self._metrics = create_processor_metrics( "logs", OtelComponentTypeValues.SIMPLE_LOG_PROCESSOR, meter_provider or get_meter_provider(), enabled=parse_boolean_environment_variable(OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED), ) def on_emit(self, log_record: ReadWriteLogRecord): # Prevent entering a recursive loop. cnt = get_value(_ON_EMIT_RECURSION_COUNT_KEY) or 0 # Recursive depth of 3 is sort of arbitrary. It's possible that an Exporter.export call # emits a log which returns us to this function, but when we call Exporter.export again the log # is no longer emitted and we exit this recursive loop naturally, a depth of >3 allows 3 # recursive log calls but exits after because it's likely endless. if cnt > 3: # pyright: ignore[reportOperatorIssue] _propagate_false_logger.warning( "SimpleLogRecordProcessor.on_emit has entered a recursive loop. Dropping log and exiting the loop." ) return token = attach( set_value( _SUPPRESS_INSTRUMENTATION_KEY, True, set_value(_ON_EMIT_RECURSION_COUNT_KEY, cnt + 1), # pyright: ignore[reportOperatorIssue] ) ) try: if self._shutdown: _logger.warning("Processor is already shutdown, ignoring call") self._metrics.drop_items(1, "already_shutdown") return # Convert ReadWriteLogRecord to ReadableLogRecord before exporting # Note: resource should not be None at this point as it's set during Logger.emit() resource = log_record.resource if log_record.resource is not None else Resource.create({}) readable_log_record = ReadableLogRecord( log_record=log_record.log_record, resource=resource, instrumentation_scope=log_record.instrumentation_scope, limits=log_record.limits, ) # Record on submission to the exporter. self._metrics.finish_items(1) self._exporter.export((readable_log_record,)) except Exception: # pylint: disable=broad-exception-caught _logger.exception("Exception while exporting logs.") finally: detach(token) def shutdown(self): self._shutdown = True self._exporter.shutdown() def force_flush(self, timeout_millis: int = 30000) -> bool: # pylint: disable=no-self-use return True class BatchLogRecordProcessor(LogRecordProcessor): """This is an implementation of LogRecordProcessor which creates batches of received logs and sends them to the configured LogRecordExporter. `BatchLogRecordProcessor` is configurable with the following environment variables which correspond to constructor parameters: - :envvar:`OTEL_BLRP_SCHEDULE_DELAY` - :envvar:`OTEL_BLRP_MAX_QUEUE_SIZE` - :envvar:`OTEL_BLRP_MAX_EXPORT_BATCH_SIZE` - :envvar:`OTEL_BLRP_EXPORT_TIMEOUT` All the logic for emitting logs, shutting down etc. resides in the BatchProcessor class. """ def __init__( self, exporter: LogRecordExporter, schedule_delay_millis: float | None = None, max_export_batch_size: int | None = None, export_timeout_millis: float | None = None, max_queue_size: int | None = None, *, meter_provider: MeterProvider | None = None, ): if max_queue_size is None: max_queue_size = BatchLogRecordProcessor._default_max_queue_size() if schedule_delay_millis is None: schedule_delay_millis = BatchLogRecordProcessor._default_schedule_delay_millis() if max_export_batch_size is None: max_export_batch_size = BatchLogRecordProcessor._default_max_export_batch_size() # Not used. No way currently to pass timeout to export. if export_timeout_millis is None: export_timeout_millis = BatchLogRecordProcessor._default_export_timeout_millis() BatchLogRecordProcessor._validate_arguments(max_queue_size, schedule_delay_millis, max_export_batch_size) # Initializes BatchProcessor self._batch_processor = BatchProcessor( exporter, schedule_delay_millis, max_export_batch_size, export_timeout_millis, max_queue_size, "Log", create_processor_metrics( "logs", OtelComponentTypeValues.BATCHING_LOG_PROCESSOR, meter_provider or get_meter_provider(), capacity=max_queue_size, enabled=parse_boolean_environment_variable(OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED), ), ) def on_emit(self, log_record: ReadWriteLogRecord) -> None: # Convert ReadWriteLogRecord to ReadableLogRecord before passing to BatchProcessor # Note: resource should not be None at this point as it's set during Logger.emit() resource = log_record.resource if log_record.resource is not None else Resource.create({}) # Shallow copy the API log record to break the reference to the potentially large context # while keeping the original context intact for other processors. api_log_record = copy.copy(log_record.log_record) api_log_record.context = Context() readable_log_record = ReadableLogRecord( log_record=api_log_record, resource=resource, instrumentation_scope=log_record.instrumentation_scope, limits=log_record.limits, ) return self._batch_processor.emit(readable_log_record) def shutdown(self): return self._batch_processor.shutdown() def force_flush(self, timeout_millis: int = 30000) -> bool: return self._batch_processor.force_flush(timeout_millis) @staticmethod def _default_max_queue_size(): try: return int(environ.get(OTEL_BLRP_MAX_QUEUE_SIZE, _DEFAULT_MAX_QUEUE_SIZE)) except ValueError: _logger.exception( _ENV_VAR_INT_VALUE_ERROR_MESSAGE, OTEL_BLRP_MAX_QUEUE_SIZE, _DEFAULT_MAX_QUEUE_SIZE, ) return _DEFAULT_MAX_QUEUE_SIZE @staticmethod def _default_schedule_delay_millis(): try: return int(environ.get(OTEL_BLRP_SCHEDULE_DELAY, _DEFAULT_SCHEDULE_DELAY_MILLIS)) except ValueError: _logger.exception( _ENV_VAR_INT_VALUE_ERROR_MESSAGE, OTEL_BLRP_SCHEDULE_DELAY, _DEFAULT_SCHEDULE_DELAY_MILLIS, ) return _DEFAULT_SCHEDULE_DELAY_MILLIS @staticmethod def _default_max_export_batch_size(): try: return int( environ.get( OTEL_BLRP_MAX_EXPORT_BATCH_SIZE, _DEFAULT_MAX_EXPORT_BATCH_SIZE, ) ) except ValueError: _logger.exception( _ENV_VAR_INT_VALUE_ERROR_MESSAGE, OTEL_BLRP_MAX_EXPORT_BATCH_SIZE, _DEFAULT_MAX_EXPORT_BATCH_SIZE, ) return _DEFAULT_MAX_EXPORT_BATCH_SIZE @staticmethod def _default_export_timeout_millis(): try: return int(environ.get(OTEL_BLRP_EXPORT_TIMEOUT, _DEFAULT_EXPORT_TIMEOUT_MILLIS)) except ValueError: _logger.exception( _ENV_VAR_INT_VALUE_ERROR_MESSAGE, OTEL_BLRP_EXPORT_TIMEOUT, _DEFAULT_EXPORT_TIMEOUT_MILLIS, ) return _DEFAULT_EXPORT_TIMEOUT_MILLIS @staticmethod def _validate_arguments(max_queue_size, schedule_delay_millis, max_export_batch_size): if max_queue_size <= 0: raise ValueError("max_queue_size must be a positive integer.") if schedule_delay_millis <= 0: raise ValueError("schedule_delay_millis must be positive.") if max_export_batch_size <= 0: raise ValueError("max_export_batch_size must be a positive integer.") if max_export_batch_size > max_queue_size: raise ValueError("max_export_batch_size must be less than or equal to max_queue_size.") in_memory_log_exporter.py000066400000000000000000000030421527070070700374220ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/export# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import collections.abc import threading from typing_extensions import deprecated from opentelemetry.sdk._logs import ReadableLogRecord from opentelemetry.sdk._logs.export import ( LogRecordExporter, LogRecordExportResult, ) class InMemoryLogRecordExporter(LogRecordExporter): """Implementation of :class:`.LogRecordExporter` that stores logs in memory. This class can be used for testing purposes. It stores the exported logs in a list in memory that can be retrieved using the :meth:`.get_finished_logs` method. """ def __init__(self): self._logs = [] self._lock = threading.Lock() self._stopped = False def clear(self) -> None: with self._lock: self._logs.clear() def get_finished_logs(self) -> tuple[ReadableLogRecord, ...]: with self._lock: return tuple(self._logs) def export(self, batch: collections.abc.Sequence[ReadableLogRecord]) -> LogRecordExportResult: if self._stopped: return LogRecordExportResult.FAILURE with self._lock: self._logs.extend(batch) return LogRecordExportResult.SUCCESS def shutdown(self) -> None: self._stopped = True def force_flush(self, timeout_millis: int = 30000) -> bool: return True @deprecated("Use InMemoryLogRecordExporter. Since logs are not stable yet this WILL be removed in future releases.") class InMemoryLogExporter(InMemoryLogRecordExporter): pass python-opentelemetry-1.45.2/opentelemetry-sdk/src/opentelemetry/sdk/_logs/export/000077500000000000000000000000001527070070700303665ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-sdk/src/opentelemetry/sdk/_logs/export/__init__.py000066400000000000000000000015121527070070700324760ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from opentelemetry.sdk._logs._internal.export import ( BatchLogRecordProcessor, ConsoleLogExporter, ConsoleLogRecordExporter, LogExporter, LogExportResult, LogRecordExporter, LogRecordExportResult, SimpleLogRecordProcessor, ) # The point module is not in the export directory to avoid a circular import. from opentelemetry.sdk._logs._internal.export.in_memory_log_exporter import ( InMemoryLogExporter, InMemoryLogRecordExporter, ) __all__ = [ "BatchLogRecordProcessor", "ConsoleLogExporter", "ConsoleLogRecordExporter", "InMemoryLogExporter", "InMemoryLogRecordExporter", "LogExportResult", "LogExporter", "LogRecordExportResult", "LogRecordExporter", "SimpleLogRecordProcessor", ] python-opentelemetry-1.45.2/opentelemetry-sdk/src/opentelemetry/sdk/_shared_internal/000077500000000000000000000000001527070070700312435ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-sdk/src/opentelemetry/sdk/_shared_internal/__init__.py000066400000000000000000000222271527070070700333610ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations import collections import enum import inspect import logging import os import threading import time import weakref from abc import abstractmethod from typing import ( Generic, Protocol, TypeVar, ) from opentelemetry.context import ( _SUPPRESS_INSTRUMENTATION_KEY, attach, detach, set_value, ) from opentelemetry.sdk._shared_internal._processor_metrics import ( ProcessorMetricsT, ) from opentelemetry.util._once import Once class DuplicateFilter(logging.Filter): """Filter that can be applied to internal `logger`'s. Currently applied to `logger`s on the export logs path to prevent endlessly logging the same log in cases where logging itself is failing.""" def filter(self, record): current_log = ( record.module, record.levelno, record.msg, # We need to pick a time longer than the OTLP LogExporter timeout # which defaults to 10 seconds, but not pick something so long that # it filters out useful logs. time.time() // 20, ) if current_log != getattr(self, "last_log", None): self.last_log = current_log # pylint: disable=attribute-defined-outside-init return True # False means python's `logging` module will no longer process this log. return False class BatchExportStrategy(enum.Enum): EXPORT_ALL = 0 EXPORT_WHILE_BATCH_EXCEEDS_THRESHOLD = 1 EXPORT_AT_LEAST_ONE_BATCH = 2 Telemetry = TypeVar("Telemetry") class Exporter(Protocol[Telemetry]): @abstractmethod def export(self, batch: list[Telemetry], /): raise NotImplementedError @abstractmethod def shutdown(self): raise NotImplementedError _logger = logging.getLogger(__name__) _logger.addFilter(DuplicateFilter()) class BatchProcessor(Generic[Telemetry]): """This class can be used with exporter's that implement the above Exporter interface to buffer and send telemetry in batch through the exporter.""" def __init__( self, exporter: Exporter[Telemetry], schedule_delay_millis: float, max_export_batch_size: int, export_timeout_millis: float, max_queue_size: int, exporting: str, metrics: ProcessorMetricsT, ): self._bsp_reset_once = Once() self._exporter = exporter self._max_queue_size = max_queue_size self._schedule_delay_millis = schedule_delay_millis self._schedule_delay = schedule_delay_millis / 1e3 self._max_export_batch_size = max_export_batch_size # Not used. No way currently to pass timeout to export. # TODO(https://github.com/open-telemetry/opentelemetry-python/issues/4555): figure out what this should do. self._export_timeout_millis = export_timeout_millis # Deque is thread safe. self._queue = collections.deque([], max_queue_size) self._worker_thread = threading.Thread( name=f"OtelBatch{exporting}RecordProcessor", target=self.worker, daemon=True, ) self._exporting = exporting self._shutdown = False self._shutdown_timeout_exceeded = False self._export_lock = threading.Lock() self._worker_awaken = threading.Event() self._worker_thread.start() if hasattr(os, "register_at_fork"): weak_reinit = weakref.WeakMethod(self._at_fork_reinit) def _after_in_child() -> None: if reinit := weak_reinit(): reinit() os.register_at_fork(after_in_child=_after_in_child) self._pid = os.getpid() metrics.register_queue_size(lambda: len(self._queue)) self._metrics = metrics def _should_export_batch(self, batch_strategy: BatchExportStrategy, num_iterations: int) -> bool: if not self._queue or self._shutdown_timeout_exceeded: return False # Always continue to export while queue length exceeds max batch size. if len(self._queue) >= self._max_export_batch_size: return True if batch_strategy is BatchExportStrategy.EXPORT_ALL: return True if batch_strategy is BatchExportStrategy.EXPORT_AT_LEAST_ONE_BATCH: return num_iterations == 0 return False def _at_fork_reinit(self): self._export_lock = threading.Lock() self._worker_awaken = threading.Event() self._queue.clear() self._worker_thread = threading.Thread( name=f"OtelBatch{self._exporting}RecordProcessor", target=self.worker, daemon=True, ) self._worker_thread.start() self._pid = os.getpid() def worker(self): while not self._shutdown: # Lots of strategies in the spec for setting next timeout. # https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/sdk.md#batching-processor. # Shutdown will interrupt this sleep. Emit will interrupt this sleep only if the queue is bigger then threshold. sleep_interrupted = self._worker_awaken.wait(self._schedule_delay) if self._shutdown: break self._export( BatchExportStrategy.EXPORT_WHILE_BATCH_EXCEEDS_THRESHOLD if sleep_interrupted else BatchExportStrategy.EXPORT_AT_LEAST_ONE_BATCH ) self._worker_awaken.clear() self._export(BatchExportStrategy.EXPORT_ALL) def _export(self, batch_strategy: BatchExportStrategy) -> None: with self._export_lock: iteration = 0 # We could see concurrent export calls from worker and force_flush. We call _should_export_batch # once the lock is obtained to see if we still need to make the requested export. while self._should_export_batch(batch_strategy, iteration): iteration += 1 token = attach(set_value(_SUPPRESS_INSTRUMENTATION_KEY, True)) count = min( self._max_export_batch_size, len(self._queue), ) # Oldest records are at the back, so pop from there. batch = [self._queue.pop() for _ in range(count)] # Record on submission to the exporter. self._metrics.finish_items(count) try: self._exporter.export(batch) except Exception: # pylint: disable=broad-exception-caught _logger.exception("Exception while exporting %s.", self._exporting) detach(token) def emit(self, data: Telemetry) -> None: if self._shutdown: _logger.info("Shutdown called, ignoring %s.", self._exporting) self._metrics.drop_items(1, "already_shutdown") return if self._pid != os.getpid(): self._bsp_reset_once.do_once(self._at_fork_reinit) if len(self._queue) == self._max_queue_size: _logger.warning("Queue full, dropping %s.", self._exporting) self._metrics.drop_items(1) # This will drop a log from the right side if the queue is at _max_queue_size. self._queue.appendleft(data) if len(self._queue) >= self._max_export_batch_size and not self._worker_awaken.is_set(): self._worker_awaken.set() def shutdown(self, timeout_millis: int = 30000): if self._shutdown: return shutdown_should_end = time.time() + (timeout_millis / 1000) # Causes emit to reject telemetry and makes force_flush a no-op. self._shutdown = True # Interrupts sleep in the worker if it's sleeping. self._worker_awaken.set() self._worker_thread.join(timeout_millis / 1000) # Stops worker thread from calling export again if queue is still not empty. self._shutdown_timeout_exceeded = True # We want to shutdown immediately only if we already waited `timeout_secs`. # Otherwise we pass the remaining timeout to the exporter. # Some exporter's shutdown support a timeout param. if "timeout_millis" in inspect.getfullargspec(self._exporter.shutdown).args: remaining_millis = (shutdown_should_end - time.time()) * 1000 self._exporter.shutdown(timeout_millis=max(0, remaining_millis)) # type: ignore else: self._exporter.shutdown() # Worker thread **should** be finished at this point, because we called shutdown on the exporter, # and set shutdown_is_occuring to prevent further export calls. It's possible that a single export # call is ongoing and the thread isn't finished. In this case we will return instead of waiting on # the thread to finish. # TODO: Fix force flush so the timeout is used https://github.com/open-telemetry/opentelemetry-python/issues/4568. def force_flush(self, timeout_millis: int | None = None) -> bool: if self._shutdown: return False # Blocking call to export. self._export(BatchExportStrategy.EXPORT_ALL) return True _processor_metrics.py000066400000000000000000000110751527070070700354460ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-sdk/src/opentelemetry/sdk/_shared_internal# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations from collections import Counter from collections.abc import Callable from typing import Literal, Protocol from opentelemetry.metrics import CallbackOptions, MeterProvider, Observation from opentelemetry.semconv._incubating.attributes.otel_attributes import ( OTEL_COMPONENT_NAME, OTEL_COMPONENT_TYPE, OtelComponentTypeValues, ) from opentelemetry.semconv._incubating.metrics.otel_metrics import ( OTEL_SDK_PROCESSOR_LOG_QUEUE_SIZE, OTEL_SDK_PROCESSOR_SPAN_QUEUE_SIZE, create_otel_sdk_processor_log_processed, create_otel_sdk_processor_log_queue_capacity, create_otel_sdk_processor_span_processed, create_otel_sdk_processor_span_queue_capacity, ) from opentelemetry.semconv.attributes.error_attributes import ERROR_TYPE _component_counter = Counter() class ProcessorMetricsT(Protocol): def register_queue_size(self, get_queue_size: Callable[[], int]) -> None: ... def drop_items(self, count: int, error_type: str = "queue_full") -> None: ... def finish_items(self, count: int) -> None: ... class NoOpProcessorMetrics: def register_queue_size(self, get_queue_size: Callable[[], int]) -> None: pass def drop_items(self, count: int, error_type: str = "queue_full") -> None: pass def finish_items(self, count: int) -> None: pass class ProcessorMetrics: def __init__( self, signal: Literal["traces", "logs"], component_type: OtelComponentTypeValues, meter_provider: MeterProvider, *, capacity: int | None = None, ) -> None: self._signal = signal meter = meter_provider.get_meter("opentelemetry-sdk") self._meter = meter count = _component_counter[component_type.value] _component_counter[component_type.value] = count + 1 self._standard_attrs = { OTEL_COMPONENT_TYPE: component_type.value, OTEL_COMPONENT_NAME: f"{component_type.value}/{count}", } self._dropped_attrs = { **self._standard_attrs, ERROR_TYPE: "queue_full", } self._already_shutdown_attrs = { **self._standard_attrs, ERROR_TYPE: "already_shutdown", } if signal == "traces": create_processed = create_otel_sdk_processor_span_processed create_queue_capacity = create_otel_sdk_processor_span_queue_capacity else: create_processed = create_otel_sdk_processor_log_processed create_queue_capacity = create_otel_sdk_processor_log_queue_capacity self._processed = create_processed(meter) if capacity is not None: self._queue_capacity = create_queue_capacity(meter) self._queue_capacity.add(capacity, self._standard_attrs) def register_queue_size(self, get_queue_size: Callable[[], int]) -> None: def record_queue_size( _options: CallbackOptions, ) -> tuple[Observation]: return (Observation(get_queue_size(), self._standard_attrs),) if self._signal == "traces": queue_size_name = OTEL_SDK_PROCESSOR_SPAN_QUEUE_SIZE queue_size_description = "The number of spans in the queue of a given instance of an SDK span processor." queue_size_unit = "{span}" else: queue_size_name = OTEL_SDK_PROCESSOR_LOG_QUEUE_SIZE queue_size_description = "The number of logs in the queue of a given instance of an SDK log processor." queue_size_unit = "{log}" self._meter.create_observable_up_down_counter( queue_size_name, callbacks=(record_queue_size,), description=queue_size_description, unit=queue_size_unit, ) def drop_items(self, count: int, error_type: str = "queue_full") -> None: if error_type == "already_shutdown": self._processed.add(count, self._already_shutdown_attrs) else: self._processed.add(count, self._dropped_attrs) def finish_items(self, count: int) -> None: self._processed.add(count, self._standard_attrs) def create_processor_metrics( signal: Literal["traces", "logs"], component_type: OtelComponentTypeValues, meter_provider: MeterProvider, *, capacity: int | None = None, enabled: bool, ) -> ProcessorMetricsT: if not enabled: return NoOpProcessorMetrics() return ProcessorMetrics( signal, component_type, meter_provider, capacity=capacity, ) python-opentelemetry-1.45.2/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables/000077500000000000000000000000001527070070700323365ustar00rootroot00000000000000__init__.py000066400000000000000000000765641527070070700344120ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 OTEL_SDK_DISABLED = "OTEL_SDK_DISABLED" """ .. envvar:: OTEL_SDK_DISABLED The :envvar:`OTEL_SDK_DISABLED` environment variable disables the SDK for all signals Default: "false" """ OTEL_CONFIG_FILE = "OTEL_CONFIG_FILE" """ .. envvar:: OTEL_CONFIG_FILE The :envvar:`OTEL_CONFIG_FILE` environment variable points the SDK at a declarative configuration file (YAML or JSON). When set, the file is the sole source for SDK construction. Spec-defined ``OTEL_*`` variables with schema equivalents are ignored. Env vars may still be read indirectly by components the file enables (e.g. resource detectors) and via ``${env:VAR}`` substitution inside the file. Python-implementation extensions outside the spec (``OTEL_PYTHON_*`` variables such as ``OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED`` or :envvar:`OTEL_PYTHON_TRACER_CONFIGURATOR`) are also bypassed when :envvar:`OTEL_CONFIG_FILE` is set, because the env-var initialisation path is skipped entirely in favour of the declarative file. Honouring these alongside a config file is tracked separately. See the OpenTelemetry declarative configuration specification for details. """ OTEL_RESOURCE_ATTRIBUTES = "OTEL_RESOURCE_ATTRIBUTES" """ .. envvar:: OTEL_RESOURCE_ATTRIBUTES The :envvar:`OTEL_RESOURCE_ATTRIBUTES` environment variable allows resource attributes to be passed to the SDK at process invocation. The attributes from :envvar:`OTEL_RESOURCE_ATTRIBUTES` are merged with those passed to `Resource.create`, meaning :envvar:`OTEL_RESOURCE_ATTRIBUTES` takes *lower* priority. Attributes should be in the format ``key1=value1,key2=value2``. Additional details are available `in the specification `__. .. code-block:: console $ OTEL_RESOURCE_ATTRIBUTES="service.name=shoppingcard,will_be_overridden=foo" python - <`__. """ OTEL_EXPORTER_OTLP_TIMEOUT = "OTEL_EXPORTER_OTLP_TIMEOUT" """ .. envvar:: OTEL_EXPORTER_OTLP_TIMEOUT The :envvar:`OTEL_EXPORTER_OTLP_TIMEOUT` is the maximum time (in seconds) the OTLP exporter will wait for each batch export. Default: 10 """ OTEL_EXPORTER_OTLP_ENDPOINT = "OTEL_EXPORTER_OTLP_ENDPOINT" """ .. envvar:: OTEL_EXPORTER_OTLP_ENDPOINT The :envvar:`OTEL_EXPORTER_OTLP_ENDPOINT` target to which the exporter is going to send spans or metrics. The endpoint MUST be a valid URL host, and MAY contain a scheme (http or https), port and path. A scheme of https indicates a secure connection and takes precedence over the insecure configuration setting. Default: "http://localhost:4317" """ OTEL_EXPORTER_OTLP_INSECURE = "OTEL_EXPORTER_OTLP_INSECURE" """ .. envvar:: OTEL_EXPORTER_OTLP_INSECURE The :envvar:`OTEL_EXPORTER_OTLP_INSECURE` represents whether to enable client transport security for gRPC requests. A scheme of https takes precedence over this configuration setting. Default: False """ OTEL_EXPORTER_OTLP_TRACES_INSECURE = "OTEL_EXPORTER_OTLP_TRACES_INSECURE" """ .. envvar:: OTEL_EXPORTER_OTLP_TRACES_INSECURE The :envvar:`OTEL_EXPORTER_OTLP_TRACES_INSECURE` represents whether to enable client transport security for gRPC requests for spans. A scheme of https takes precedence over the this configuration setting. Default: False """ OTEL_EXPORTER_OTLP_TRACES_ENDPOINT = "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT" """ .. envvar:: OTEL_EXPORTER_OTLP_TRACES_ENDPOINT The :envvar:`OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` target to which the span exporter is going to send spans. The endpoint MUST be a valid URL host, and MAY contain a scheme (http or https), port and path. A scheme of https indicates a secure connection and takes precedence over this configuration setting. """ OTEL_EXPORTER_OTLP_METRICS_ENDPOINT = "OTEL_EXPORTER_OTLP_METRICS_ENDPOINT" """ .. envvar:: OTEL_EXPORTER_OTLP_METRICS_ENDPOINT The :envvar:`OTEL_EXPORTER_OTLP_METRICS_ENDPOINT` target to which the metrics exporter is going to send metrics. The endpoint MUST be a valid URL host, and MAY contain a scheme (http or https), port and path. A scheme of https indicates a secure connection and takes precedence over this configuration setting. """ OTEL_EXPORTER_OTLP_LOGS_ENDPOINT = "OTEL_EXPORTER_OTLP_LOGS_ENDPOINT" """ .. envvar:: OTEL_EXPORTER_OTLP_LOGS_ENDPOINT The :envvar:`OTEL_EXPORTER_OTLP_LOGS_ENDPOINT` target to which the log exporter is going to send logs. The endpoint MUST be a valid URL host, and MAY contain a scheme (http or https), port and path. A scheme of https indicates a secure connection and takes precedence over this configuration setting. """ _OTEL_PYTHON_EXPORTER_OTLP_GRPC_LOGS_CREDENTIAL_PROVIDER = "OTEL_PYTHON_EXPORTER_OTLP_GRPC_LOGS_CREDENTIAL_PROVIDER" """ .. envvar:: OTEL_PYTHON_EXPORTER_OTLP_GRPC_LOGS_CREDENTIAL_PROVIDER The :envvar:`OTEL_PYTHON_EXPORTER_OTLP_GRPC_LOGS_CREDENTIAL_PROVIDER` provides `grpc.ChannelCredentials` to the grpc OTLP Log exporter, Entry point providers should implement the following: .. code-block:: python import grpc # Add a reference to this function under the `opentelemetry_otlp_credential_provider` entry point. def channel_credential_provider() -> grpc.ChannelCredentials: Note: This environment variable is experimental and subject to change. """ _OTEL_PYTHON_EXPORTER_OTLP_HTTP_LOGS_CREDENTIAL_PROVIDER = "OTEL_PYTHON_EXPORTER_OTLP_HTTP_LOGS_CREDENTIAL_PROVIDER" """ .. envvar:: OTEL_PYTHON_EXPORTER_OTLP_HTTP_LOGS_CREDENTIAL_PROVIDER The :envvar:`OTEL_PYTHON_EXPORTER_OTLP_HTTP_LOGS_CREDENTIAL_PROVIDER` provides `requests.Session` for the HTTP OTLP Log exporter. Entry point providers should implement the following: .. code-block:: python import requests # Add a reference to this function under the `opentelemetry_otlp_credential_provider` entry point. def request_session_provder() -> requests.Session: Note: This environment variable is experimental and subject to change. """ _OTEL_PYTHON_EXPORTER_OTLP_HTTP_CREDENTIAL_PROVIDER = "OTEL_PYTHON_EXPORTER_OTLP_HTTP_CREDENTIAL_PROVIDER" """ .. envvar:: OTEL_PYTHON_EXPORTER_OTLP_HTTP_CREDENTIAL_PROVIDER The :envvar:`OTEL_PYTHON_EXPORTER_OTLP_HTTP_CREDENTIAL_PROVIDER` provides `requests.Session` for all HTTP OTLP exporters. Entry point providers should implement the following: .. code-block:: python import requests # Add a reference to this function under the `opentelemetry_otlp_credential_provider` entry point. def request_session_provder() -> requests.Session: Note: This environment variable is experimental and subject to change. """ _OTEL_PYTHON_EXPORTER_OTLP_GRPC_CREDENTIAL_PROVIDER = "OTEL_PYTHON_EXPORTER_OTLP_GRPC_CREDENTIAL_PROVIDER" """ .. envvar:: OTEL_PYTHON_EXPORTER_OTLP_GRPC_CREDENTIAL_PROVIDER The :envvar:`OTEL_PYTHON_EXPORTER_OTLP_GRPC_CREDENTIAL_PROVIDER` provides `grpc.ChannelCredentials` for all GRPC OTLP exporters. Entry point providers should implement the following: .. code-block:: python import grpc # Add a reference to this function under the `opentelemetry_otlp_credential_provider` entry point. def channel_credential_provider() -> grpc.ChannelCredentials: Note: This environment variable is experimental and subject to change. """ _OTEL_PYTHON_EXPORTER_OTLP_HTTP_TRACES_CREDENTIAL_PROVIDER = "OTEL_PYTHON_EXPORTER_OTLP_HTTP_TRACES_CREDENTIAL_PROVIDER" """ .. envvar:: OTEL_PYTHON_EXPORTER_OTLP_HTTP_TRACES_CREDENTIAL_PROVIDER The :envvar:`OTEL_PYTHON_EXPORTER_OTLP_HTTP_TRACES_CREDENTIAL_PROVIDER` provides `requests.Session` to the HTTP OTLP Span exporter. Entry point providers should implement the following: .. code-block:: python import requests # Add a reference to this function under the `opentelemetry_otlp_credential_provider` entry point. def request_session_provder() -> requests.Session: Note: This environment variable is experimental and subject to change. """ _OTEL_PYTHON_EXPORTER_OTLP_GRPC_TRACES_CREDENTIAL_PROVIDER = "OTEL_PYTHON_EXPORTER_OTLP_GRPC_TRACES_CREDENTIAL_PROVIDER" """ .. envvar:: OTEL_PYTHON_EXPORTER_OTLP_GRPC_TRACES_CREDENTIAL_PROVIDER The :envvar:`OTEL_PYTHON_EXPORTER_OTLP_GRPC_TRACES_CREDENTIAL_PROVIDER` provides `grpc.ChannelCredentials` to the GRPC OTLP Span exporter. Entry point providers should implement the following: .. code-block:: python import grpc # Add a reference to this function under the `opentelemetry_otlp_credential_provider` entry point. def channel_credential_provider() -> grpc.ChannelCredentials: Note: This environment variable is experimental and subject to change. """ _OTEL_PYTHON_EXPORTER_OTLP_HTTP_METRICS_CREDENTIAL_PROVIDER = ( "OTEL_PYTHON_EXPORTER_OTLP_HTTP_METRICS_CREDENTIAL_PROVIDER" ) """ .. envvar:: OTEL_PYTHON_EXPORTER_OTLP_HTTP_METRICS_CREDENTIAL_PROVIDER The :envvar:`OTEL_PYTHON_EXPORTER_OTLP_HTTP_METRICS_CREDENTIAL_PROVIDER` provides `requests.Session` to the HTTP OTLP Metric exporter. Entry point providers should implement the following: .. code-block:: python import requests # Add a reference to this function under the `opentelemetry_otlp_credential_provider` entry point. def request_session_provder() -> requests.Session: Note: This environment variable is experimental and subject to change. """ _OTEL_PYTHON_EXPORTER_OTLP_GRPC_METRICS_CREDENTIAL_PROVIDER = ( "OTEL_PYTHON_EXPORTER_OTLP_GRPC_METRICS_CREDENTIAL_PROVIDER" ) """ .. envvar:: OTEL_PYTHON_EXPORTER_OTLP_GRPC_METRICS_CREDENTIAL_PROVIDER The :envvar:`OTEL_PYTHON_EXPORTER_OTLP_GRPC_METRICS_CREDENTIAL_PROVIDER` provides `grpc.ChannelCredentials` to the GRPC OTLP Metric exporter. Entry point providers should implement the following: .. code-block:: python import grpc # Add a reference to this function under the `opentelemetry_otlp_credential_provider` entry point. def channel_credential_provider() -> grpc.ChannelCredentials: Note: This environment variable is experimental and subject to change. """ _OTEL_PYTHON_EXPORTER_OTLP_GRPC_RETRYABLE_ERROR_CODES = "OTEL_PYTHON_EXPORTER_OTLP_GRPC_RETRYABLE_ERROR_CODES" """ .. envvar:: OTEL_PYTHON_EXPORTER_OTLP_GRPC_RETRYABLE_ERROR_CODES The :envvar:`OTEL_PYTHON_EXPORTER_OTLP_GRPC_RETRYABLE_ERROR_CODES` stores a comma-separated list of human-readable gRPC error codes that are considered retryable for the OTLP gRPC exporters (e.g. `UNAVAILABLE, DEADLINE_EXCEEDED`). Supported error codes are defined in `grpc.StatusCode` and are parsed in a case-insensitive manner. Note: This environment variable is experimental and subject to change. """ OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE = "OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE" """ .. envvar:: OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE The :envvar:`OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE` stores the path to the certificate file for TLS credentials of gRPC client for traces. Should only be used for a secure connection for tracing. """ OTEL_EXPORTER_OTLP_METRICS_CERTIFICATE = "OTEL_EXPORTER_OTLP_METRICS_CERTIFICATE" """ .. envvar:: OTEL_EXPORTER_OTLP_METRICS_CERTIFICATE The :envvar:`OTEL_EXPORTER_OTLP_METRICS_CERTIFICATE` stores the path to the certificate file for TLS credentials of gRPC client for metrics. Should only be used for a secure connection for exporting metrics. """ OTEL_EXPORTER_OTLP_CLIENT_KEY = "OTEL_EXPORTER_OTLP_CLIENT_KEY" """ .. envvar:: OTEL_EXPORTER_OTLP_CLIENT_KEY The :envvar:`OTEL_EXPORTER_OTLP_CLIENT_KEY` stores the path to the client private key to use in mTLS communication in PEM format. """ OTEL_EXPORTER_OTLP_TRACES_CLIENT_KEY = "OTEL_EXPORTER_OTLP_TRACES_CLIENT_KEY" """ .. envvar:: OTEL_EXPORTER_OTLP_TRACES_CLIENT_KEY The :envvar:`OTEL_EXPORTER_OTLP_TRACES_CLIENT_KEY` stores the path to the client private key to use in mTLS communication in PEM format for traces. """ OTEL_EXPORTER_OTLP_METRICS_CLIENT_KEY = "OTEL_EXPORTER_OTLP_METRICS_CLIENT_KEY" """ .. envvar:: OTEL_EXPORTER_OTLP_METRICS_CLIENT_KEY The :envvar:`OTEL_EXPORTER_OTLP_METRICS_CLIENT_KEY` stores the path to the client private key to use in mTLS communication in PEM format for metrics. """ OTEL_EXPORTER_OTLP_LOGS_CLIENT_KEY = "OTEL_EXPORTER_OTLP_LOGS_CLIENT_KEY" """ .. envvar:: OTEL_EXPORTER_OTLP_LOGS_CLIENT_KEY The :envvar:`OTEL_EXPORTER_OTLP_LOGS_CLIENT_KEY` stores the path to the client private key to use in mTLS communication in PEM format for logs. """ OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE = "OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE" """ .. envvar:: OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE The :envvar:`OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE` stores the path to the client certificate/chain trust for clients private key to use in mTLS communication in PEM format. """ OTEL_EXPORTER_OTLP_TRACES_CLIENT_CERTIFICATE = "OTEL_EXPORTER_OTLP_TRACES_CLIENT_CERTIFICATE" """ .. envvar:: OTEL_EXPORTER_OTLP_TRACES_CLIENT_CERTIFICATE The :envvar:`OTEL_EXPORTER_OTLP_TRACES_CLIENT_CERTIFICATE` stores the path to the client certificate/chain trust for clients private key to use in mTLS communication in PEM format for traces. """ OTEL_EXPORTER_OTLP_METRICS_CLIENT_CERTIFICATE = "OTEL_EXPORTER_OTLP_METRICS_CLIENT_CERTIFICATE" """ .. envvar:: OTEL_EXPORTER_OTLP_METRICS_CLIENT_CERTIFICATE The :envvar:`OTEL_EXPORTER_OTLP_METRICS_CLIENT_CERTIFICATE` stores the path to the client certificate/chain trust for clients private key to use in mTLS communication in PEM format for metrics. """ OTEL_EXPORTER_OTLP_LOGS_CLIENT_CERTIFICATE = "OTEL_EXPORTER_OTLP_LOGS_CLIENT_CERTIFICATE" """ .. envvar:: OTEL_EXPORTER_OTLP_LOGS_CLIENT_CERTIFICATE The :envvar:`OTEL_EXPORTER_OTLP_LOGS_CLIENT_CERTIFICATE` stores the path to the client certificate/chain trust for clients private key to use in mTLS communication in PEM format for logs. """ OTEL_EXPORTER_OTLP_TRACES_HEADERS = "OTEL_EXPORTER_OTLP_TRACES_HEADERS" """ .. envvar:: OTEL_EXPORTER_OTLP_TRACES_HEADERS The :envvar:`OTEL_EXPORTER_OTLP_TRACES_HEADERS` contains the key-value pairs to be used as headers for spans associated with gRPC or HTTP requests. """ OTEL_EXPORTER_OTLP_METRICS_HEADERS = "OTEL_EXPORTER_OTLP_METRICS_HEADERS" """ .. envvar:: OTEL_EXPORTER_OTLP_METRICS_HEADERS The :envvar:`OTEL_EXPORTER_OTLP_METRICS_HEADERS` contains the key-value pairs to be used as headers for metrics associated with gRPC or HTTP requests. """ OTEL_EXPORTER_OTLP_LOGS_HEADERS = "OTEL_EXPORTER_OTLP_LOGS_HEADERS" """ .. envvar:: OTEL_EXPORTER_OTLP_LOGS_HEADERS The :envvar:`OTEL_EXPORTER_OTLP_LOGS_HEADERS` contains the key-value pairs to be used as headers for logs associated with gRPC or HTTP requests. """ OTEL_EXPORTER_OTLP_TRACES_COMPRESSION = "OTEL_EXPORTER_OTLP_TRACES_COMPRESSION" """ .. envvar:: OTEL_EXPORTER_OTLP_TRACES_COMPRESSION Same as :envvar:`OTEL_EXPORTER_OTLP_COMPRESSION` but only for the span exporter. If both are present, this takes higher precedence. """ OTEL_EXPORTER_OTLP_METRICS_COMPRESSION = "OTEL_EXPORTER_OTLP_METRICS_COMPRESSION" """ .. envvar:: OTEL_EXPORTER_OTLP_METRICS_COMPRESSION Same as :envvar:`OTEL_EXPORTER_OTLP_COMPRESSION` but only for the metric exporter. If both are present, this takes higher precedence. """ OTEL_EXPORTER_OTLP_LOGS_COMPRESSION = "OTEL_EXPORTER_OTLP_LOGS_COMPRESSION" """ .. envvar:: OTEL_EXPORTER_OTLP_LOGS_COMPRESSION Same as :envvar:`OTEL_EXPORTER_OTLP_COMPRESSION` but only for the log exporter. If both are present, this takes higher precedence. """ OTEL_EXPORTER_OTLP_TRACES_TIMEOUT = "OTEL_EXPORTER_OTLP_TRACES_TIMEOUT" """ .. envvar:: OTEL_EXPORTER_OTLP_TRACES_TIMEOUT The :envvar:`OTEL_EXPORTER_OTLP_TRACES_TIMEOUT` is the maximum time (in seconds) the OTLP exporter will wait for each batch export for spans. Default: 10 """ OTEL_EXPORTER_OTLP_METRICS_TIMEOUT = "OTEL_EXPORTER_OTLP_METRICS_TIMEOUT" """ .. envvar:: OTEL_EXPORTER_OTLP_METRICS_TIMEOUT The :envvar:`OTEL_EXPORTER_OTLP_METRICS_TIMEOUT` is the maximum time (in seconds) the OTLP exporter will wait for each batch export for metrics. Default: 10 """ OTEL_EXPORTER_OTLP_METRICS_INSECURE = "OTEL_EXPORTER_OTLP_METRICS_INSECURE" """ .. envvar:: OTEL_EXPORTER_OTLP_METRICS_INSECURE The :envvar:`OTEL_EXPORTER_OTLP_METRICS_INSECURE` represents whether to enable client transport security for gRPC requests for metrics. A scheme of https takes precedence over the this configuration setting. Default: False """ OTEL_EXPORTER_OTLP_LOGS_INSECURE = "OTEL_EXPORTER_OTLP_LOGS_INSECURE" """ .. envvar:: OTEL_EXPORTER_OTLP_LOGS_INSECURE The :envvar:`OTEL_EXPORTER_OTLP_LOGS_INSECURE` represents whether to enable client transport security for gRPC requests for logs. A scheme of https takes precedence over the this configuration setting. Default: False """ OTEL_EXPORTER_OTLP_LOGS_CERTIFICATE = "OTEL_EXPORTER_OTLP_LOGS_CERTIFICATE" """ .. envvar:: OTEL_EXPORTER_OTLP_LOGS_CERTIFICATE The :envvar:`OTEL_EXPORTER_OTLP_LOGS_CERTIFICATE` stores the path to the certificate file for TLS credentials of gRPC client for logs. Should only be used for a secure connection for logs. """ OTEL_EXPORTER_OTLP_LOGS_TIMEOUT = "OTEL_EXPORTER_OTLP_LOGS_TIMEOUT" """ .. envvar:: OTEL_EXPORTER_OTLP_LOGS_TIMEOUT The :envvar:`OTEL_EXPORTER_OTLP_LOGS_TIMEOUT` is the maximum time (in seconds) the OTLP exporter will wait for each batch export for logs. Default: 10 """ OTEL_SERVICE_NAME = "OTEL_SERVICE_NAME" """ .. envvar:: OTEL_SERVICE_NAME Convenience environment variable for setting the service name resource attribute. The following two environment variables have the same effect .. code-block:: console OTEL_SERVICE_NAME=my-python-service OTEL_RESOURCE_ATTRIBUTES=service.name=my-python-service If both are set, :envvar:`OTEL_SERVICE_NAME` takes precedence. """ _OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED = "OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED" """ .. envvar:: OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED The :envvar:`OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED` environment variable allows users to enable/disable the auto instrumentation for the python logging module. Default: False Note: Logs SDK and its related settings are experimental. .. warning:: This option is deprecated, instead you should install `opentelemetry-instrumentation-logging`. """ OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE = "OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE" """ .. envvar:: OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE The :envvar:`OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE` environment variable allows users to set the default aggregation temporality policy to use on the basis of instrument kind. The valid (case-insensitive) values are: ``CUMULATIVE``: Use ``CUMULATIVE`` aggregation temporality for all instrument kinds. ``DELTA``: Use ``DELTA`` aggregation temporality for ``Counter``, ``Asynchronous Counter`` and ``Histogram``. Use ``CUMULATIVE`` aggregation temporality for ``UpDownCounter`` and ``Asynchronous UpDownCounter``. ``LOWMEMORY``: Use ``DELTA`` aggregation temporality for ``Counter`` and ``Histogram``. Use ``CUMULATIVE`` aggregation temporality for ``UpDownCounter``, ``AsynchronousCounter`` and ``Asynchronous UpDownCounter``. """ OTEL_METRIC_EXPORT_INTERVAL = "OTEL_METRIC_EXPORT_INTERVAL" """ .. envvar:: OTEL_METRIC_EXPORT_INTERVAL The :envvar:`OTEL_METRIC_EXPORT_INTERVAL` is the time interval (in milliseconds) between the start of two export attempts. """ OTEL_METRIC_EXPORT_TIMEOUT = "OTEL_METRIC_EXPORT_TIMEOUT" """ .. envvar:: OTEL_METRIC_EXPORT_TIMEOUT The :envvar:`OTEL_METRIC_EXPORT_TIMEOUT` is the maximum allowed time (in milliseconds) to export data. """ OTEL_METRICS_EXEMPLAR_FILTER = "OTEL_METRICS_EXEMPLAR_FILTER" """ .. envvar:: OTEL_METRICS_EXEMPLAR_FILTER The :envvar:`OTEL_METRICS_EXEMPLAR_FILTER` is the filter for which measurements can become Exemplars. """ OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION = "OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION" """ .. envvar:: OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION The :envvar:`OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION` is the default aggregation to use for histogram instruments. """ OTEL_EXPERIMENTAL_RESOURCE_DETECTORS = "OTEL_EXPERIMENTAL_RESOURCE_DETECTORS" """ .. envvar:: OTEL_EXPERIMENTAL_RESOURCE_DETECTORS The :envvar:`OTEL_EXPERIMENTAL_RESOURCE_DETECTORS` is a comma-separated string of names of resource detectors. These names must be the same as the names of entry points for the ```opentelemetry_resource_detector``` entry point. This is an experimental feature and the name of this variable and its behavior can change in a non-backwards compatible way. Detectors are run in the order they are listed and their attributes are merged in that order, with later detectors taking precedence over earlier ones on conflicting keys. The ``otel`` detector (which reads :envvar:`OTEL_RESOURCE_ATTRIBUTES` and :envvar:`OTEL_SERVICE_NAME`) is always appended last unless explicitly placed elsewhere in the list, ensuring environment variable attributes take highest priority among detectors. """ OTEL_EXPORTER_PROMETHEUS_HOST = "OTEL_EXPORTER_PROMETHEUS_HOST" """ .. envvar:: OTEL_EXPORTER_PROMETHEUS_HOST The :envvar:`OTEL_EXPORTER_PROMETHEUS_HOST` environment variable configures the host used by the Prometheus exporter. Default: "localhost" This is an experimental environment variable and the name of this variable and its behavior can change in a non-backwards compatible way. """ OTEL_EXPORTER_PROMETHEUS_PORT = "OTEL_EXPORTER_PROMETHEUS_PORT" """ .. envvar:: OTEL_EXPORTER_PROMETHEUS_PORT The :envvar:`OTEL_EXPORTER_PROMETHEUS_PORT` environment variable configures the port used by the Prometheus exporter. Default: 9464 This is an experimental environment variable and the name of this variable and its behavior can change in a non-backwards compatible way. """ OTEL_PYTHON_TRACER_CONFIGURATOR = "OTEL_PYTHON_TRACER_CONFIGURATOR" """ .. envvar:: OTEL_PYTHON_TRACER_CONFIGURATOR The :envvar:`OTEL_PYTHON_TRACER_CONFIGURATOR` environment variable allows users to set a custom Tracer Configurator function. Default: opentelemetry.sdk.trace._default_tracer_configurator This is an experimental environment variable and the name of this variable and its behavior can change in a non-backwards compatible way. """ OTEL_PYTHON_METER_CONFIGURATOR = "OTEL_PYTHON_METER_CONFIGURATOR" """ .. envvar:: OTEL_PYTHON_METER_CONFIGURATOR The :envvar:`OTEL_PYTHON_METER_CONFIGURATOR` environment variable allows users to set a custom Meter Configurator function. Default: opentelemetry.sdk.metrics._internal._default_meter_configurator This is an experimental environment variable and the name of this variable and its behavior can change in a non-backwards compatible way. """ OTEL_PYTHON_LOGGER_CONFIGURATOR = "OTEL_PYTHON_LOGGER_CONFIGURATOR" """ .. envvar:: OTEL_PYTHON_LOGGER_CONFIGURATOR The :envvar:`OTEL_PYTHON_LOGGER_CONFIGURATOR` environment variable allows users to set a custom Logger Configurator function. Default: opentelemetry.sdk._logs._internal._default_logger_configurator This is an experimental environment variable and the name of this variable and its behavior can change in a non-backwards compatible way. """ OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED = "OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED" """ .. envvar:: OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED The :envvar:`OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED` environment variable enables metrics emitted by the SDK about its own internal state. Default: "false" """ _internal.py000066400000000000000000000012721527070070700346060ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from logging import getLogger from os import environ _logger = getLogger(__name__) def parse_boolean_environment_variable(environment_variable: str, default: bool = False) -> bool: value = environ.get(environment_variable) if value is None: return default match value.strip().lower(): case "true": return True case "false": return False case _: _logger.warning( "Invalid value for %s: %r. Expected 'true' or 'false'.", environment_variable, value, ) return default python-opentelemetry-1.45.2/opentelemetry-sdk/src/opentelemetry/sdk/error_handler/000077500000000000000000000000001527070070700305705ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-sdk/src/opentelemetry/sdk/error_handler/__init__.py000066400000000000000000000077401527070070700327110ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 """ Global Error Handler This module provides a global error handler and an interface that allows error handlers to be registered with the global error handler via entry points. A default error handler is also provided. To use this feature, users can create an error handler that is registered using the ``opentelemetry_error_handler`` entry point. A class is to be registered in this entry point, this class must inherit from the ``opentelemetry.sdk.error_handler.ErrorHandler`` class and implement the corresponding ``handle`` method. This method will receive the exception object that is to be handled. The error handler class should also inherit from the exception classes it wants to handle. For example, this would be an error handler that handles ``ZeroDivisionError``: .. code:: python from opentelemetry.sdk.error_handler import ErrorHandler from logging import getLogger logger = getLogger(__name__) class ErrorHandler0(ErrorHandler, ZeroDivisionError): def _handle(self, error: Exception, *args, **kwargs): logger.exception("ErrorHandler0 handling a ZeroDivisionError") To use the global error handler, just instantiate it as a context manager where you want exceptions to be handled: .. code:: python from opentelemetry.sdk.error_handler import GlobalErrorHandler with GlobalErrorHandler(): 1 / 0 If the class of the exception raised in the scope of the ``GlobalErrorHandler`` object is not parent of any registered error handler, then the default error handler will handle the exception. This default error handler will only log the exception to standard logging, the exception won't be raised any further. """ from abc import ABC, abstractmethod from logging import getLogger from opentelemetry.util._importlib_metadata import entry_points logger = getLogger(__name__) class ErrorHandler(ABC): @abstractmethod def _handle(self, error: Exception, *args, **kwargs): """ Handle an exception """ class _DefaultErrorHandler(ErrorHandler): """ Default error handler This error handler just logs the exception using standard logging. """ # pylint: disable=useless-return def _handle(self, error: Exception, *args, **kwargs): logger.exception("Error handled by default error handler: ") class GlobalErrorHandler: """ Global error handler This is a singleton class that can be instantiated anywhere to get the global error handler. This object provides a handle method that receives an exception object that will be handled by the registered error handlers. """ _instance = None def __new__(cls) -> "GlobalErrorHandler": if cls._instance is None: cls._instance = super().__new__(cls) return cls._instance def __enter__(self): pass # pylint: disable=no-self-use def __exit__(self, exc_type, exc_value, traceback): if exc_value is None: return None plugin_handled = False error_handler_entry_points = entry_points(group="opentelemetry_error_handler") for error_handler_entry_point in error_handler_entry_points: error_handler_class = error_handler_entry_point.load() if issubclass(error_handler_class, exc_value.__class__): try: error_handler_class()._handle(exc_value) plugin_handled = True # pylint: disable=broad-exception-caught except Exception as error_handling_error: logger.exception( "%s error while handling error %s by error handler %s", error_handling_error.__class__.__name__, exc_value.__class__.__name__, error_handler_class.__name__, ) if not plugin_handled: _DefaultErrorHandler()._handle(exc_value) return True python-opentelemetry-1.45.2/opentelemetry-sdk/src/opentelemetry/sdk/metrics/000077500000000000000000000000001527070070700274105ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py000066400000000000000000000024421527070070700315230ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from opentelemetry.sdk.metrics import export, view from opentelemetry.sdk.metrics._internal import Meter, MeterProvider from opentelemetry.sdk.metrics._internal.exceptions import MetricsTimeoutError from opentelemetry.sdk.metrics._internal.exemplar import ( AlignedHistogramBucketExemplarReservoir, AlwaysOffExemplarFilter, AlwaysOnExemplarFilter, Exemplar, ExemplarFilter, ExemplarReservoir, SimpleFixedSizeExemplarReservoir, TraceBasedExemplarFilter, ) from opentelemetry.sdk.metrics._internal.instrument import ( Counter, Histogram, ObservableCounter, ObservableGauge, ObservableUpDownCounter, UpDownCounter, ) from opentelemetry.sdk.metrics._internal.instrument import Gauge as _Gauge __all__ = [ "AlignedHistogramBucketExemplarReservoir", "AlwaysOffExemplarFilter", "AlwaysOnExemplarFilter", "Counter", "Exemplar", "ExemplarFilter", "ExemplarReservoir", "Histogram", "Meter", "MeterProvider", "MetricsTimeoutError", "ObservableCounter", "ObservableGauge", "ObservableUpDownCounter", "SimpleFixedSizeExemplarReservoir", "TraceBasedExemplarFilter", "UpDownCounter", "_Gauge", "export", "view", ] python-opentelemetry-1.45.2/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/000077500000000000000000000000001527070070700313635ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/__init__.py000066400000000000000000000615111527070070700335000ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import os import weakref from atexit import register, unregister from collections.abc import Callable, Sequence from dataclasses import dataclass from logging import getLogger from os import environ from threading import Lock from time import time_ns # This kind of import is needed to avoid Sphinx errors. import opentelemetry.sdk.metrics from opentelemetry.metrics import Counter as APICounter from opentelemetry.metrics import Histogram as APIHistogram from opentelemetry.metrics import Meter as APIMeter from opentelemetry.metrics import MeterProvider as APIMeterProvider from opentelemetry.metrics import NoOpMeter from opentelemetry.metrics import ObservableCounter as APIObservableCounter from opentelemetry.metrics import ObservableGauge as APIObservableGauge from opentelemetry.metrics import ( ObservableUpDownCounter as APIObservableUpDownCounter, ) from opentelemetry.metrics import UpDownCounter as APIUpDownCounter from opentelemetry.metrics import _Gauge as APIGauge from opentelemetry.sdk.environment_variables import ( OTEL_METRICS_EXEMPLAR_FILTER, OTEL_SDK_DISABLED, ) from opentelemetry.sdk.metrics._internal.exceptions import MetricsTimeoutError from opentelemetry.sdk.metrics._internal.exemplar import ( AlwaysOffExemplarFilter, AlwaysOnExemplarFilter, ExemplarFilter, TraceBasedExemplarFilter, ) from opentelemetry.sdk.metrics._internal.instrument import ( _Counter, _Gauge, _Histogram, _ObservableCounter, _ObservableGauge, _ObservableUpDownCounter, _UpDownCounter, ) from opentelemetry.sdk.metrics._internal.measurement_consumer import ( MeasurementConsumer, SynchronousMeasurementConsumer, ) from opentelemetry.sdk.metrics._internal.sdk_configuration import ( SdkConfiguration, ) from opentelemetry.sdk.resources import ( Resource, _get_process_dependent_resource, ) from opentelemetry.sdk.util._configurator import RuleBasedConfigurator from opentelemetry.sdk.util.instrumentation import ( InstrumentationScope, ) from opentelemetry.util._once import Once from opentelemetry.util.types import ( Attributes, ) _logger = getLogger(__name__) @dataclass class _MeterConfig: is_enabled: bool = True @classmethod def default(cls) -> "_MeterConfig": return _MeterConfig() class _ProxyMeterConfig: def __init__(self, config: _MeterConfig): self._config = config @property def is_enabled(self) -> bool: return self._config.is_enabled def update(self, config: _MeterConfig) -> None: self._config = config class Meter(APIMeter): """See `opentelemetry.metrics.Meter`.""" def __init__( self, instrumentation_scope: InstrumentationScope, measurement_consumer: MeasurementConsumer, *, _meter_config: _MeterConfig | None = None, ): super().__init__( name=instrumentation_scope.name, version=instrumentation_scope.version, schema_url=instrumentation_scope.schema_url, ) self._instrumentation_scope = instrumentation_scope self._measurement_consumer = measurement_consumer self._instrument_id_instrument = {} self._instrument_registration_lock = Lock() self._meter_config = _ProxyMeterConfig(_meter_config or _MeterConfig.default()) def _is_enabled(self) -> bool: return self._meter_config.is_enabled def _set_meter_config(self, meter_config: _MeterConfig) -> None: self._meter_config.update(meter_config) def create_counter(self, name, unit="", description="") -> APICounter: with self._instrument_registration_lock: status = self._register_instrument(name, _Counter, unit, description) if not status.already_registered: self._instrument_id_instrument[status.instrument_id] = _Counter( name, self._instrumentation_scope, self._measurement_consumer, unit, description, _meter_config=self._meter_config, ) instrument = self._instrument_id_instrument[status.instrument_id] if status.conflict: # FIXME #2558 go through all views here and check if this # instrument registration conflict can be fixed. If it can be, do # not log the following warning. self._log_instrument_registration_conflict( name, APICounter.__name__, unit, description, status, ) return instrument def create_up_down_counter(self, name, unit="", description="") -> APIUpDownCounter: with self._instrument_registration_lock: status = self._register_instrument(name, _UpDownCounter, unit, description) if not status.already_registered: self._instrument_id_instrument[status.instrument_id] = _UpDownCounter( name, self._instrumentation_scope, self._measurement_consumer, unit, description, _meter_config=self._meter_config, ) instrument = self._instrument_id_instrument[status.instrument_id] if status.conflict: # FIXME #2558 go through all views here and check if this # instrument registration conflict can be fixed. If it can be, do # not log the following warning. self._log_instrument_registration_conflict( name, APIUpDownCounter.__name__, unit, description, status, ) return instrument def create_observable_counter( self, name, callbacks=None, unit="", description="", ) -> APIObservableCounter: with self._instrument_registration_lock: status = self._register_instrument(name, _ObservableCounter, unit, description) if not status.already_registered: self._instrument_id_instrument[status.instrument_id] = _ObservableCounter( name, self._instrumentation_scope, self._measurement_consumer, callbacks, unit, description, _meter_config=self._meter_config, ) instrument = self._instrument_id_instrument[status.instrument_id] if not status.already_registered: self._measurement_consumer.register_asynchronous_instrument(instrument) if status.conflict: # FIXME #2558 go through all views here and check if this # instrument registration conflict can be fixed. If it can be, do # not log the following warning. self._log_instrument_registration_conflict( name, APIObservableCounter.__name__, unit, description, status, ) return instrument def create_histogram( self, name: str, unit: str = "", description: str = "", *, explicit_bucket_boundaries_advisory: Sequence[float] | None = None, ) -> APIHistogram: if explicit_bucket_boundaries_advisory is not None: invalid_advisory = False if isinstance(explicit_bucket_boundaries_advisory, Sequence): try: invalid_advisory = not ( all(isinstance(e, (float, int)) for e in explicit_bucket_boundaries_advisory) ) except (KeyError, TypeError): invalid_advisory = True else: invalid_advisory = True if invalid_advisory: explicit_bucket_boundaries_advisory = None _logger.warning("explicit_bucket_boundaries_advisory must be a sequence of numbers") with self._instrument_registration_lock: status = self._register_instrument( name, _Histogram, unit, description, explicit_bucket_boundaries_advisory, ) if not status.already_registered: self._instrument_id_instrument[status.instrument_id] = _Histogram( name, self._instrumentation_scope, self._measurement_consumer, unit, description, explicit_bucket_boundaries_advisory, _meter_config=self._meter_config, ) instrument = self._instrument_id_instrument[status.instrument_id] if status.conflict: # FIXME #2558 go through all views here and check if this # instrument registration conflict can be fixed. If it can be, do # not log the following warning. self._log_instrument_registration_conflict( name, APIHistogram.__name__, unit, description, status, ) return instrument def create_gauge(self, name, unit="", description="") -> APIGauge: with self._instrument_registration_lock: status = self._register_instrument(name, _Gauge, unit, description) if not status.already_registered: self._instrument_id_instrument[status.instrument_id] = _Gauge( name, self._instrumentation_scope, self._measurement_consumer, unit, description, _meter_config=self._meter_config, ) instrument = self._instrument_id_instrument[status.instrument_id] if status.conflict: # FIXME #2558 go through all views here and check if this # instrument registration conflict can be fixed. If it can be, do # not log the following warning. self._log_instrument_registration_conflict( name, APIGauge.__name__, unit, description, status, ) return instrument def create_observable_gauge(self, name, callbacks=None, unit="", description="") -> APIObservableGauge: with self._instrument_registration_lock: status = self._register_instrument(name, _ObservableGauge, unit, description) if not status.already_registered: self._instrument_id_instrument[status.instrument_id] = _ObservableGauge( name, self._instrumentation_scope, self._measurement_consumer, callbacks, unit, description, _meter_config=self._meter_config, ) instrument = self._instrument_id_instrument[status.instrument_id] if not status.already_registered: self._measurement_consumer.register_asynchronous_instrument(instrument) if status.conflict: # FIXME #2558 go through all views here and check if this # instrument registration conflict can be fixed. If it can be, do # not log the following warning. self._log_instrument_registration_conflict( name, APIObservableGauge.__name__, unit, description, status, ) return instrument def create_observable_up_down_counter( self, name, callbacks=None, unit="", description="" ) -> APIObservableUpDownCounter: with self._instrument_registration_lock: status = self._register_instrument(name, _ObservableUpDownCounter, unit, description) if not status.already_registered: self._instrument_id_instrument[status.instrument_id] = _ObservableUpDownCounter( name, self._instrumentation_scope, self._measurement_consumer, callbacks, unit, description, _meter_config=self._meter_config, ) instrument = self._instrument_id_instrument[status.instrument_id] if not status.already_registered: self._measurement_consumer.register_asynchronous_instrument(instrument) if status.conflict: # FIXME #2558 go through all views here and check if this # instrument registration conflict can be fixed. If it can be, do # not log the following warning. self._log_instrument_registration_conflict( name, APIObservableUpDownCounter.__name__, unit, description, status, ) return instrument def _get_exemplar_filter(exemplar_filter: str) -> ExemplarFilter: if exemplar_filter == "trace_based": return TraceBasedExemplarFilter() if exemplar_filter == "always_on": return AlwaysOnExemplarFilter() if exemplar_filter == "always_off": return AlwaysOffExemplarFilter() msg = f"Unknown exemplar filter '{exemplar_filter}'." raise ValueError(msg) _MeterConfiguratorT = Callable[[InstrumentationScope], _MeterConfig] _RuleBasedMeterConfigurator = RuleBasedConfigurator[_MeterConfig] def _default_meter_configurator( _meter_scope: InstrumentationScope, ) -> _MeterConfig: return _MeterConfig.default() def _disable_meter_configurator( _meter_scope: InstrumentationScope, ) -> _MeterConfig: return _MeterConfig(is_enabled=False) class MeterProvider(APIMeterProvider): r"""See `opentelemetry.metrics.MeterProvider`. Args: metric_readers: Register metric readers to collect metrics from the SDK on demand. Each :class:`opentelemetry.sdk.metrics.export.MetricReader` is completely independent and will collect separate streams of metrics. For push-based export, use :class:`opentelemetry.sdk.metrics.export.PeriodicExportingMetricReader`. resource: The resource representing what the metrics emitted from the SDK pertain to. shutdown_on_exit: If true, registers an `atexit` handler to call `MeterProvider.shutdown` views: The views to configure the metric output the SDK .. code-block:: python :caption: Push-based export with PeriodicExportingMetricReader from opentelemetry.sdk.metrics import MeterProvider from opentelemetry.sdk.metrics.export import ( ConsoleMetricExporter, PeriodicExportingMetricReader, ) reader = PeriodicExportingMetricReader(ConsoleMetricExporter()) provider = MeterProvider(metric_readers=[reader]) By default, instruments which do not match any :class:`opentelemetry.sdk.metrics.view.View` (or if no :class:`opentelemetry.sdk.metrics.view.View`\ s are provided) will report metrics with the default aggregation for the instrument's kind. To disable instruments by default, configure a match-all :class:`opentelemetry.sdk.metrics.view.View` with `DropAggregation` and then create :class:`opentelemetry.sdk.metrics.view.View`\ s to re-enable individual instruments: .. code-block:: python :caption: Disable default views MeterProvider( views=[ View(instrument_name="*", aggregation=DropAggregation()), View(instrument_name="mycounter"), ], # ... ) """ _all_metric_readers_lock = Lock() _all_metric_readers = weakref.WeakSet() def __init__( self, metric_readers: Sequence["opentelemetry.sdk.metrics.export.MetricReader"] = (), resource: Resource | None = None, exemplar_filter: ExemplarFilter | None = None, shutdown_on_exit: bool = True, views: Sequence["opentelemetry.sdk.metrics.view.View"] = (), *, _meter_configurator: _MeterConfiguratorT | None = None, ): self._lock = Lock() self._meter_lock = Lock() self._atexit_handler = None if resource is None: resource = Resource.create({}) self._sdk_config = SdkConfiguration( exemplar_filter=( exemplar_filter or _get_exemplar_filter(environ.get(OTEL_METRICS_EXEMPLAR_FILTER, "trace_based")) ), resource=resource, views=views, ) self._metric_readers = metric_readers self._measurement_consumer = SynchronousMeasurementConsumer( sdk_config=self._sdk_config, metric_readers=metric_readers, ) disabled = environ.get(OTEL_SDK_DISABLED, "") self._disabled = disabled.lower().strip() == "true" if shutdown_on_exit: self._atexit_handler = register(self.shutdown) self._meters: dict[InstrumentationScope, Meter] = {} self._shutdown_once = Once() self._shutdown = False self._meter_configurator = _meter_configurator or _default_meter_configurator for metric_reader in self._metric_readers: with self._all_metric_readers_lock: if metric_reader in self._all_metric_readers: # pylint: disable=broad-exception-raised raise Exception( f"MetricReader {metric_reader} has been registered already in other MeterProvider instance" ) self._all_metric_readers.add(metric_reader) metric_reader._set_collect_callback(self._measurement_consumer.collect) metric_reader._set_meter_provider(self) if hasattr(os, "register_at_fork"): weak_at_fork = weakref.WeakMethod(self._handle_fork) def _after_in_child() -> None: if at_fork := weak_at_fork(): at_fork() os.register_at_fork(after_in_child=_after_in_child) def _handle_fork(self) -> None: self._lock = Lock() self._meter_lock = Lock() type(self)._all_metric_readers_lock = Lock() self._update_resource(_get_process_dependent_resource()) def _set_meter_configurator(self, *, meter_configurator: _MeterConfiguratorT): """Set a new MeterConfigurator for this MeterProvider. Setting a new MeterConfigurator will result in the configurator being called for each outstanding Meter and for any newly created meters thereafter. Therefore, it is important that the provided function returns quickly. """ with self._meter_lock: self._meter_configurator = meter_configurator for instrumentation_scope, meter in self._meters.items(): # pylint: disable-next=protected-access meter._set_meter_config(self._apply_meter_configurator(instrumentation_scope)) def _update_resource(self, resource: Resource) -> None: with self._meter_lock: self._sdk_config.resource = self._sdk_config.resource.merge(resource) def _apply_meter_configurator(self, instrumentation_scope: InstrumentationScope) -> _MeterConfig: try: return self._meter_configurator(instrumentation_scope) # pylint: disable-next=broad-exception-caught except Exception: _logger.exception( "meter configurator failed for scope '%s', using default config", instrumentation_scope.name, ) return _MeterConfig.default() def force_flush(self, timeout_millis: float = 10_000) -> bool: deadline_ns = time_ns() + timeout_millis * 10**6 metric_reader_error = {} for metric_reader in self._metric_readers: current_ts = time_ns() try: if current_ts >= deadline_ns: raise MetricsTimeoutError("Timed out while flushing metric readers") metric_reader.force_flush(timeout_millis=(deadline_ns - current_ts) / 10**6) # pylint: disable=broad-exception-caught except Exception as error: metric_reader_error[metric_reader] = error if metric_reader_error: metric_reader_error_string = "\n".join( [ f"{metric_reader.__class__.__name__}: {error!r}" for metric_reader, error in metric_reader_error.items() ] ) # pylint: disable=broad-exception-raised raise Exception( "MeterProvider.force_flush failed because the following " "metric readers failed during collect:\n" f"{metric_reader_error_string}" ) return True def shutdown(self, timeout_millis: float = 30_000): deadline_ns = time_ns() + timeout_millis * 10**6 def _shutdown(): self._shutdown = True did_shutdown = self._shutdown_once.do_once(_shutdown) if not did_shutdown: _logger.warning("shutdown can only be called once") return metric_reader_error = {} for metric_reader in self._metric_readers: current_ts = time_ns() try: if current_ts >= deadline_ns: # pylint: disable=broad-exception-raised raise Exception("Didn't get to execute, deadline already exceeded") metric_reader.shutdown(timeout_millis=(deadline_ns - current_ts) / 10**6) # pylint: disable=broad-exception-caught except Exception as error: metric_reader_error[metric_reader] = error if self._atexit_handler is not None: unregister(self._atexit_handler) self._atexit_handler = None if metric_reader_error: metric_reader_error_string = "\n".join( [ f"{metric_reader.__class__.__name__}: {error!r}" for metric_reader, error in metric_reader_error.items() ] ) # pylint: disable=broad-exception-raised raise Exception( "MeterProvider.shutdown failed because the following " "metric readers failed during shutdown:\n" f"{metric_reader_error_string}" ) def get_meter( self, name: str, version: str | None = None, schema_url: str | None = None, attributes: Attributes = None, ) -> APIMeter: if self._disabled: return NoOpMeter(name, version=version, schema_url=schema_url) if self._shutdown: _logger.warning("A shutdown `MeterProvider` can not provide a `Meter`") return NoOpMeter(name, version=version, schema_url=schema_url) if not name: _logger.warning("Meter name cannot be None or empty.") return NoOpMeter(name, version=version, schema_url=schema_url) instrumentation_scope = InstrumentationScope(name, version, schema_url, attributes) with self._meter_lock: if not self._meters.get(instrumentation_scope): # FIXME #2558 pass SDKConfig object to meter so that the meter # has access to views. self._meters[instrumentation_scope] = Meter( instrumentation_scope, self._measurement_consumer, _meter_config=self._apply_meter_configurator(instrumentation_scope), ) return self._meters[instrumentation_scope] def add_metric_reader(self, metric_reader: "opentelemetry.sdk.metrics.export.MetricReader") -> None: with self._all_metric_readers_lock: if metric_reader in self._all_metric_readers: _logger.warning( "MetricReader '%s' has been registered already!", metric_reader, ) return self._measurement_consumer.add_metric_reader(metric_reader) # pylint: disable-next=protected-access metric_reader._set_collect_callback(self._measurement_consumer.collect) self._all_metric_readers.add(metric_reader) def remove_metric_reader( self, metric_reader: "opentelemetry.sdk.metrics.export.MetricReader", ) -> None: with self._all_metric_readers_lock: if metric_reader not in self._all_metric_readers: _logger.warning("MetricReader '%s' has not been registered!", metric_reader) return self._measurement_consumer.remove_metric_reader(metric_reader) # pylint: disable-next=protected-access metric_reader._set_collect_callback(None) metric_reader.shutdown() self._all_metric_readers.remove(metric_reader) _view_instrument_match.py000066400000000000000000000213201527070070700364310ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from collections.abc import Mapping, Sequence from logging import getLogger from threading import Lock from time import time_ns from types import NoneType from typing import TYPE_CHECKING, cast from typing_extensions import assert_never from opentelemetry.sdk.metrics._internal.aggregation import ( Aggregation, AggregationTemporality, DefaultAggregation, _Aggregation, _SumAggregation, ) from opentelemetry.sdk.metrics._internal.instrument import _Instrument from opentelemetry.sdk.metrics._internal.measurement import Measurement from opentelemetry.sdk.metrics._internal.point import DataPointT from opentelemetry.sdk.metrics._internal.view import View from opentelemetry.util.types import AnyValue, Attributes _logger = getLogger(__name__) # Every branch is tagged so that values Python considers equal but the OTel # data model does not -- True/1/1.0, or a sequence of pairs and the mapping # it resembles -- produce different aggregation keys. _HashedAttributes = tuple[ str, str | bool | int | float | bytes | None | tuple["_HashedAttributes", ...] | tuple[tuple[str, "_HashedAttributes"], ...], ] # pylint: disable=inconsistent-return-statements def _hash_attributes(value: Attributes | AnyValue) -> _HashedAttributes: # Attributes have been cleaned and validated when Measurement was instantiated, # so value is guaranteed to match one of the branches below at runtime. if isinstance(value, (NoneType, str, int, float, bool, bytes)): # bool is a subclass of int and 1 == 1.0, so the value alone is not # enough to tell these apart. return (type(value).__name__, value) if isinstance(value, Sequence): return ("sequence", tuple(_hash_attributes(v) for v in value)) if isinstance(value, Mapping): return ( "mapping", tuple( (k, _hash_attributes(value[k])) for k in sorted( value, key=lambda item: item if isinstance(item, str) else str(item), ) ), ) if TYPE_CHECKING: assert_never(value) _DEFAULT_CARDINALITY_LIMIT = 2000 _OVERFLOW_ATTRIBUTES: dict[str, AnyValue] = {"otel.metric.overflow": True} _OVERFLOW_KEY = _hash_attributes(_OVERFLOW_ATTRIBUTES) class _ViewInstrumentMatch: def __init__( self, view: View, instrument: _Instrument, instrument_class_aggregation: dict[type, Aggregation], cardinality_limit: int = _DEFAULT_CARDINALITY_LIMIT, reset_on_collect: bool = False, ): self._view = view self._cardinality_limit = cardinality_limit self._reset_on_collect = reset_on_collect # Delta aggregations created after a collection continue from it. self._previous_collection_start_nanos: int | None = None self._instrument = instrument self._attributes_aggregation: dict[_HashedAttributes, _Aggregation] = {} self._lock = Lock() self._instrument_class_aggregation = instrument_class_aggregation self._name = self._view._name or self._instrument.name self._description = self._view._description or self._instrument.description if not isinstance(self._view._aggregation, DefaultAggregation): self._aggregation = self._view._aggregation._create_aggregation( self._instrument, None, self._view._exemplar_reservoir_factory, 0, ) else: self._aggregation = self._instrument_class_aggregation[self._instrument.__class__]._create_aggregation( self._instrument, None, self._view._exemplar_reservoir_factory, 0, ) def conflicts(self, other: "_ViewInstrumentMatch") -> bool: # pylint: disable=protected-access result = ( self._name == other._name and self._instrument.unit == other._instrument.unit # The aggregation class is being used here instead of data point # type since they are functionally equivalent. and self._aggregation.__class__ == other._aggregation.__class__ ) if not result: return result if isinstance(self._aggregation, _SumAggregation): # if result is True the two aggregation are of the same type self._aggregation = cast(_SumAggregation, self._aggregation) other._aggregation = cast(_SumAggregation, other._aggregation) result = ( self._aggregation._instrument_is_monotonic == other._aggregation._instrument_is_monotonic and self._aggregation._instrument_aggregation_temporality == other._aggregation._instrument_aggregation_temporality ) return result # pylint: disable=protected-access def consume_measurement(self, measurement: Measurement, should_sample_exemplar: bool = True) -> None: attributes = {} if measurement.attributes: # Make a shallow copy since the user can mutate the dict after the fact. # The user can still modify mutable attribute values (lists/dicts) # leading to unexpected behavior, but deep copying is expensive. attributes = dict(measurement.attributes) if self._view._attribute_keys is not None: attributes = {k: v for k, v in attributes.items() if k in self._view._attribute_keys} aggr_key = _hash_attributes(attributes) if self._reset_on_collect: # collect() discards the aggregations, so hold the lock until the # measurement is aggregated to not lose it. with self._lock: self._get_or_create_aggregation(aggr_key, attributes).aggregate(measurement, should_sample_exemplar) return aggregation = self._attributes_aggregation.get(aggr_key) if aggregation is None: with self._lock: aggregation = self._get_or_create_aggregation(aggr_key, attributes) aggregation.aggregate(measurement, should_sample_exemplar) def _get_or_create_aggregation(self, aggr_key: _HashedAttributes, attributes: Attributes) -> _Aggregation: """Must be called with ``self._lock`` held.""" aggregation = self._attributes_aggregation.get(aggr_key) if aggregation is not None: return aggregation # Overflow is only used once the limit of distinct attribute sets is # reached, so the overflow point is exported in addition to them. if len(self._attributes_aggregation) >= self._cardinality_limit: aggr_key = _OVERFLOW_KEY attributes = dict(_OVERFLOW_ATTRIBUTES) aggregation = self._attributes_aggregation.get(aggr_key) if aggregation is not None: return aggregation start_time_unix_nano = self._previous_collection_start_nanos or time_ns() # pylint: disable=protected-access if not isinstance(self._view._aggregation, DefaultAggregation): aggregation = self._view._aggregation._create_aggregation( self._instrument, attributes, self._view._exemplar_reservoir_factory, start_time_unix_nano, ) else: aggregation = self._instrument_class_aggregation[self._instrument.__class__]._create_aggregation( self._instrument, attributes, self._view._exemplar_reservoir_factory, start_time_unix_nano, ) self._attributes_aggregation[aggr_key] = aggregation return aggregation def collect( self, collection_aggregation_temporality: AggregationTemporality, collection_start_nanos: int, ) -> Sequence[DataPointT] | None: data_points: list[DataPointT] = [] with self._lock: aggregations = self._attributes_aggregation if self._reset_on_collect and collection_aggregation_temporality is AggregationTemporality.DELTA: self._attributes_aggregation = {} self._previous_collection_start_nanos = collection_start_nanos for aggregation in aggregations.values(): data_point = aggregation.collect(collection_aggregation_temporality, collection_start_nanos) if data_point is not None: data_points.append(data_point) # Returning here None instead of an empty list because the caller # does not consume a sequence and to be consistent with the rest of # collect methods that also return None. return data_points or None python-opentelemetry-1.45.2/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/aggregation.py000066400000000000000000001431161527070070700342320ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # pylint: disable=too-many-lines import math from abc import ABC, abstractmethod from bisect import bisect_left from collections.abc import Callable, Sequence from enum import IntEnum from functools import partial from logging import getLogger from threading import Lock from typing import ( Generic, TypeVar, cast, ) from opentelemetry.metrics import ( Asynchronous, Counter, Histogram, ObservableCounter, ObservableGauge, ObservableUpDownCounter, Synchronous, UpDownCounter, _Gauge, ) from opentelemetry.sdk.metrics._internal.exemplar import ( Exemplar, ExemplarReservoirBuilder, ) from opentelemetry.sdk.metrics._internal.exponential_histogram.buckets import ( Buckets, ) from opentelemetry.sdk.metrics._internal.exponential_histogram.mapping import ( Mapping, ) from opentelemetry.sdk.metrics._internal.exponential_histogram.mapping.exponent_mapping import ( ExponentMapping, ) from opentelemetry.sdk.metrics._internal.exponential_histogram.mapping.logarithm_mapping import ( LogarithmMapping, ) from opentelemetry.sdk.metrics._internal.instrument import _Instrument from opentelemetry.sdk.metrics._internal.measurement import Measurement from opentelemetry.sdk.metrics._internal.point import Buckets as BucketsPoint from opentelemetry.sdk.metrics._internal.point import ( ExponentialHistogramDataPoint, HistogramDataPoint, NumberDataPoint, Sum, ) from opentelemetry.sdk.metrics._internal.point import Gauge as GaugePoint from opentelemetry.sdk.metrics._internal.point import ( Histogram as HistogramPoint, ) from opentelemetry.util.types import Attributes _DataPointVarT = TypeVar("_DataPointVarT", NumberDataPoint, HistogramDataPoint) _logger = getLogger(__name__) class AggregationTemporality(IntEnum): """ The temporality to use when aggregating data. Can be one of the following values: """ UNSPECIFIED = 0 DELTA = 1 CUMULATIVE = 2 class _Aggregation(ABC, Generic[_DataPointVarT]): def __init__( self, attributes: Attributes, reservoir_builder: ExemplarReservoirBuilder, ): self._lock = Lock() self._attributes = attributes self._reservoir = reservoir_builder() self._previous_point = None @abstractmethod def aggregate(self, measurement: Measurement, should_sample_exemplar: bool = True) -> None: """Aggregate a measurement. Args: measurement: Measurement to aggregate should_sample_exemplar: Whether the measurement should be sampled by the exemplars reservoir or not. """ @abstractmethod def collect( self, collection_aggregation_temporality: AggregationTemporality, collection_start_nano: int, ) -> _DataPointVarT | None: pass def _collect_exemplars(self) -> Sequence[Exemplar]: """Returns the collected exemplars. Returns: The exemplars collected by the reservoir """ return self._reservoir.collect(self._attributes) def _sample_exemplar(self, measurement: Measurement, should_sample_exemplar: bool) -> None: """Offer the measurement to the exemplar reservoir for sampling. It should be called within the each :ref:`aggregate` call. Args: measurement: The new measurement should_sample_exemplar: Whether the measurement should be sampled by the exemplars reservoir or not. """ if should_sample_exemplar: self._reservoir.offer( measurement.value, measurement.time_unix_nano, measurement.attributes, measurement.context, ) class _DropAggregation(_Aggregation): def aggregate(self, measurement: Measurement, should_sample_exemplar: bool = True) -> None: pass def collect( self, collection_aggregation_temporality: AggregationTemporality, collection_start_nano: int, ) -> _DataPointVarT | None: pass class _SumAggregation(_Aggregation[Sum]): def __init__( self, attributes: Attributes, instrument_is_monotonic: bool, instrument_aggregation_temporality: AggregationTemporality, start_time_unix_nano: int, reservoir_builder: ExemplarReservoirBuilder, ): super().__init__(attributes, reservoir_builder) self._start_time_unix_nano = start_time_unix_nano self._instrument_aggregation_temporality = instrument_aggregation_temporality self._instrument_is_monotonic = instrument_is_monotonic self._value = None self._previous_collection_start_nano = self._start_time_unix_nano self._previous_value = 0 def aggregate(self, measurement: Measurement, should_sample_exemplar: bool = True) -> None: with self._lock: if self._value is None: self._value = 0 self._value = self._value + measurement.value self._sample_exemplar(measurement, should_sample_exemplar) def collect( self, collection_aggregation_temporality: AggregationTemporality, collection_start_nano: int, ) -> NumberDataPoint | None: """ Atomically return a point for the current value of the metric and reset the aggregation value. Synchronous instruments have a method which is called directly with increments for a given quantity: For example, an instrument that counts the amount of passengers in every vehicle that crosses a certain point in a highway: synchronous_instrument.add(2) collect(...) # 2 passengers are counted synchronous_instrument.add(3) collect(...) # 3 passengers are counted synchronous_instrument.add(1) collect(...) # 1 passenger is counted In this case the instrument aggregation temporality is DELTA because every value represents an increment to the count, Asynchronous instruments have a callback which returns the total value of a given quantity: For example, an instrument that measures the amount of bytes written to a certain hard drive: callback() -> 1352 collect(...) # 1352 bytes have been written so far callback() -> 2324 collect(...) # 2324 bytes have been written so far callback() -> 4542 collect(...) # 4542 bytes have been written so far In this case the instrument aggregation temporality is CUMULATIVE because every value represents the total of the measurement. There is also the collection aggregation temporality, which is passed to this method. The collection aggregation temporality defines the nature of the returned value by this aggregation. When the collection aggregation temporality matches the instrument aggregation temporality, then this method returns the current value directly: synchronous_instrument.add(2) collect(DELTA) -> 2 synchronous_instrument.add(3) collect(DELTA) -> 3 synchronous_instrument.add(1) collect(DELTA) -> 1 callback() -> 1352 collect(CUMULATIVE) -> 1352 callback() -> 2324 collect(CUMULATIVE) -> 2324 callback() -> 4542 collect(CUMULATIVE) -> 4542 When the collection aggregation temporality does not match the instrument aggregation temporality, then a conversion is made. For this purpose, this aggregation keeps a private attribute, self._previous_value. When the instrument is synchronous: self._previous_value is the sum of every previously collected (delta) value. In this case, the returned (cumulative) value will be: self._previous_value + value synchronous_instrument.add(2) collect(CUMULATIVE) -> 2 synchronous_instrument.add(3) collect(CUMULATIVE) -> 5 synchronous_instrument.add(1) collect(CUMULATIVE) -> 6 Also, as a diagram: time -> self._previous_value |-------------| value (delta) |----| returned value (cumulative) |------------------| When the instrument is asynchronous: self._previous_value is the value of the previously collected (cumulative) value. In this case, the returned (delta) value will be: value - self._previous_value callback() -> 1352 collect(DELTA) -> 1352 callback() -> 2324 collect(DELTA) -> 972 callback() -> 4542 collect(DELTA) -> 2218 Also, as a diagram: time -> self._previous_value |-------------| value (cumulative) |------------------| returned value (delta) |----| """ with self._lock: value = self._value self._value = None if self._instrument_aggregation_temporality is AggregationTemporality.DELTA: # This happens when the corresponding instrument for this # aggregation is synchronous. if collection_aggregation_temporality is AggregationTemporality.DELTA: previous_collection_start_nano = self._previous_collection_start_nano self._previous_collection_start_nano = collection_start_nano if value is None: return None return NumberDataPoint( attributes=self._attributes, exemplars=self._collect_exemplars(), start_time_unix_nano=previous_collection_start_nano, time_unix_nano=collection_start_nano, value=value, ) if value is None: value = 0 self._previous_value = value + self._previous_value return NumberDataPoint( attributes=self._attributes, exemplars=self._collect_exemplars(), start_time_unix_nano=self._start_time_unix_nano, time_unix_nano=collection_start_nano, value=self._previous_value, ) # This happens when the corresponding instrument for this # aggregation is asynchronous. if value is None: # This happens when the corresponding instrument callback # does not produce measurements. return None if collection_aggregation_temporality is AggregationTemporality.DELTA: result_value = value - self._previous_value self._previous_value = value previous_collection_start_nano = self._previous_collection_start_nano self._previous_collection_start_nano = collection_start_nano return NumberDataPoint( attributes=self._attributes, exemplars=self._collect_exemplars(), start_time_unix_nano=previous_collection_start_nano, time_unix_nano=collection_start_nano, value=result_value, ) return NumberDataPoint( attributes=self._attributes, exemplars=self._collect_exemplars(), start_time_unix_nano=self._start_time_unix_nano, time_unix_nano=collection_start_nano, value=value, ) class _LastValueAggregation(_Aggregation[GaugePoint]): def __init__( self, attributes: Attributes, reservoir_builder: ExemplarReservoirBuilder, instrument_is_synchronous: bool, ): super().__init__(attributes, reservoir_builder) self._instrument_is_synchronous = instrument_is_synchronous self._value = None def aggregate(self, measurement: Measurement, should_sample_exemplar: bool = True): with self._lock: self._value = measurement.value self._sample_exemplar(measurement, should_sample_exemplar) def collect( self, collection_aggregation_temporality: AggregationTemporality, collection_start_nano: int, ) -> _DataPointVarT | None: """ Atomically return a point for the current value of the metric. """ with self._lock: if self._value is None: return None value = self._value if not ( self._instrument_is_synchronous and collection_aggregation_temporality is AggregationTemporality.CUMULATIVE ): self._value = None exemplars = self._collect_exemplars() return NumberDataPoint( attributes=self._attributes, exemplars=exemplars, start_time_unix_nano=None, time_unix_nano=collection_start_nano, value=value, ) _DEFAULT_EXPLICIT_BUCKET_HISTOGRAM_AGGREGATION_BOUNDARIES: Sequence[float] = ( 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, ) class _ExplicitBucketHistogramAggregation(_Aggregation[HistogramPoint]): def __init__( self, attributes: Attributes, instrument_aggregation_temporality: AggregationTemporality, start_time_unix_nano: int, reservoir_builder: ExemplarReservoirBuilder, boundaries: Sequence[float] | None = None, record_min_max: bool = True, ): if boundaries is None: boundaries = _DEFAULT_EXPLICIT_BUCKET_HISTOGRAM_AGGREGATION_BOUNDARIES if boundaries: for idx, bound in enumerate(boundaries): if not math.isfinite(bound): raise ValueError(f"invalid boundary: {bound!r}") if idx and boundaries[idx - 1] >= bound: raise ValueError("boundaries must be strictly increasing") super().__init__( attributes, reservoir_builder=partial(reservoir_builder, boundaries=boundaries), ) self._instrument_aggregation_temporality = instrument_aggregation_temporality self._start_time_unix_nano = start_time_unix_nano self._boundaries = tuple(boundaries) self._record_min_max = record_min_max self._value = None self._min = math.inf self._max = -math.inf self._sum = 0 self._previous_value = None self._previous_min = math.inf self._previous_max = -math.inf self._previous_sum = 0 self._previous_collection_start_nano = self._start_time_unix_nano def _get_empty_bucket_counts(self) -> list[int]: return [0] * (len(self._boundaries) + 1) def aggregate(self, measurement: Measurement, should_sample_exemplar: bool = True) -> None: with self._lock: if self._value is None: self._value = self._get_empty_bucket_counts() measurement_value = measurement.value self._sum += measurement_value if self._record_min_max: self._min = min(self._min, measurement_value) self._max = max(self._max, measurement_value) self._value[bisect_left(self._boundaries, measurement_value)] += 1 self._sample_exemplar(measurement, should_sample_exemplar) def collect( self, collection_aggregation_temporality: AggregationTemporality, collection_start_nano: int, ) -> _DataPointVarT | None: """ Atomically return a point for the current value of the metric. """ with self._lock: value = self._value sum_ = self._sum min_ = self._min max_ = self._max self._value = None self._sum = 0 self._min = math.inf self._max = -math.inf if self._instrument_aggregation_temporality is AggregationTemporality.DELTA: # This happens when the corresponding instrument for this # aggregation is synchronous. if collection_aggregation_temporality is AggregationTemporality.DELTA: previous_collection_start_nano = self._previous_collection_start_nano self._previous_collection_start_nano = collection_start_nano if value is None: return None return HistogramDataPoint( attributes=self._attributes, exemplars=self._collect_exemplars(), start_time_unix_nano=previous_collection_start_nano, time_unix_nano=collection_start_nano, count=sum(value), sum=sum_, bucket_counts=tuple(value), explicit_bounds=self._boundaries, min=min_, max=max_, ) if value is None: value = self._get_empty_bucket_counts() if self._previous_value is None: self._previous_value = self._get_empty_bucket_counts() self._previous_value = [ value_element + previous_value_element for ( value_element, previous_value_element, ) in zip(value, self._previous_value) ] self._previous_min = min(min_, self._previous_min) self._previous_max = max(max_, self._previous_max) self._previous_sum = sum_ + self._previous_sum return HistogramDataPoint( attributes=self._attributes, exemplars=self._collect_exemplars(), start_time_unix_nano=self._start_time_unix_nano, time_unix_nano=collection_start_nano, count=sum(self._previous_value), sum=self._previous_sum, bucket_counts=tuple(self._previous_value), explicit_bounds=self._boundaries, min=self._previous_min, max=self._previous_max, ) return None # pylint: disable=protected-access class _ExponentialBucketHistogramAggregation(_Aggregation[HistogramPoint]): # _min_max_size and _max_max_size are the smallest and largest values # the max_size parameter may have, respectively. # _min_max_size is is the smallest reasonable value which is small enough # to contain the entire normal floating point range at the minimum scale. _min_max_size = 2 # _max_max_size is an arbitrary limit meant to limit accidental creation of # giant exponential bucket histograms. _max_max_size = 16384 def __init__( self, attributes: Attributes, reservoir_builder: ExemplarReservoirBuilder, instrument_aggregation_temporality: AggregationTemporality, start_time_unix_nano: int, # This is the default maximum number of buckets per positive or # negative number range. The value 160 is specified by OpenTelemetry. # See the derivation here: # https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk.md#exponential-bucket-histogram-aggregation) max_size: int = 160, max_scale: int = 20, record_min_max: bool = True, ): # max_size is the maximum capacity of the positive and negative # buckets. # _sum is the sum of all the values aggregated by this aggregator. # _count is the count of all calls to aggregate. # _zero_count is the count of all the calls to aggregate when the value # to be aggregated is exactly 0. # _min is the smallest value aggregated by this aggregator. # _max is the smallest value aggregated by this aggregator. # _positive holds the positive values. # _negative holds the negative values by their absolute value. if max_size < self._min_max_size: raise ValueError(f"Buckets max size {max_size} is smaller than minimum max size {self._min_max_size}") if max_size > self._max_max_size: raise ValueError(f"Buckets max size {max_size} is larger than maximum max size {self._max_max_size}") if max_scale > 20: _logger.warning( "max_scale is set to %s which is larger than the recommended value of 20", max_scale, ) # This aggregation is analogous to _ExplicitBucketHistogramAggregation, # the only difference is that with every call to aggregate, the size # and amount of buckets can change (in # _ExplicitBucketHistogramAggregation both size and amount of buckets # remain constant once it is instantiated). super().__init__( attributes, reservoir_builder=partial(reservoir_builder, size=min(20, max_size)), ) self._instrument_aggregation_temporality = instrument_aggregation_temporality self._start_time_unix_nano = start_time_unix_nano self._max_size = max_size self._max_scale = max_scale self._record_min_max = record_min_max self._value_positive = None self._value_negative = None self._min = math.inf self._max = -math.inf self._sum = 0 self._count = 0 self._zero_count = 0 self._scale = None self._previous_value_positive = None self._previous_value_negative = None self._previous_min = math.inf self._previous_max = -math.inf self._previous_sum = 0 self._previous_count = 0 self._previous_zero_count = 0 self._previous_scale = None self._previous_collection_start_nano = self._start_time_unix_nano self._mapping = self._new_mapping(self._max_scale) def aggregate(self, measurement: Measurement, should_sample_exemplar: bool = True) -> None: # pylint: disable=too-many-branches,too-many-statements, too-many-locals with self._lock: if self._value_positive is None: self._value_positive = Buckets() if self._value_negative is None: self._value_negative = Buckets() measurement_value = measurement.value self._sum += measurement_value if self._record_min_max: self._min = min(self._min, measurement_value) self._max = max(self._max, measurement_value) self._count += 1 if measurement_value == 0: self._zero_count += 1 if self._count == self._zero_count: self._scale = 0 return if measurement_value > 0: value = self._value_positive else: measurement_value = -measurement_value value = self._value_negative # The following code finds out if it is necessary to change the # buckets to hold the incoming measurement_value, changes them if # necessary. This process does not exist in # _ExplicitBucketHistogram aggregation because the buckets there # are constant in size and amount. index = self._mapping.map_to_index(measurement_value) is_rescaling_needed = False low, high = 0, 0 if len(value) == 0: value.index_start = index value.index_end = index value.index_base = index elif index < value.index_start and (value.index_end - index) >= self._max_size: is_rescaling_needed = True low = index high = value.index_end elif index > value.index_end and (index - value.index_start) >= self._max_size: is_rescaling_needed = True low = value.index_start high = index if is_rescaling_needed: scale_change = self._get_scale_change(low, high) self._downscale( scale_change, self._value_positive, self._value_negative, ) self._mapping = self._new_mapping(self._mapping.scale - scale_change) index = self._mapping.map_to_index(measurement_value) self._scale = self._mapping.scale if index < value.index_start: span = value.index_end - index if span >= len(value.counts): value.grow(span + 1, self._max_size) value.index_start = index elif index > value.index_end: span = index - value.index_start if span >= len(value.counts): value.grow(span + 1, self._max_size) value.index_end = index bucket_index = index - value.index_base if bucket_index < 0: bucket_index += len(value.counts) # Now the buckets have been changed if needed and bucket_index will # be used to increment the counter of the bucket that needs to be # incremented. # This is analogous to # self._value[bisect_left(self._boundaries, measurement_value)] += 1 # in _ExplicitBucketHistogramAggregation.aggregate value.increment_bucket(bucket_index) self._sample_exemplar(measurement, should_sample_exemplar) def collect( self, collection_aggregation_temporality: AggregationTemporality, collection_start_nano: int, ) -> _DataPointVarT | None: """ Atomically return a point for the current value of the metric. """ # pylint: disable=too-many-statements, too-many-locals with self._lock: value_positive = self._value_positive value_negative = self._value_negative sum_ = self._sum min_ = self._min max_ = self._max count = self._count zero_count = self._zero_count scale = self._scale self._value_positive = None self._value_negative = None self._sum = 0 self._min = math.inf self._max = -math.inf self._count = 0 self._zero_count = 0 self._scale = None if self._instrument_aggregation_temporality is AggregationTemporality.DELTA: # This happens when the corresponding instrument for this # aggregation is synchronous. if collection_aggregation_temporality is AggregationTemporality.DELTA: previous_collection_start_nano = self._previous_collection_start_nano self._previous_collection_start_nano = collection_start_nano if value_positive is None and value_negative is None: return None return ExponentialHistogramDataPoint( attributes=self._attributes, exemplars=self._collect_exemplars(), start_time_unix_nano=previous_collection_start_nano, time_unix_nano=collection_start_nano, count=count, sum=sum_, scale=scale, zero_count=zero_count, positive=BucketsPoint( offset=value_positive.offset, bucket_counts=(value_positive.get_offset_counts()), ), negative=BucketsPoint( offset=value_negative.offset, bucket_counts=(value_negative.get_offset_counts()), ), # FIXME: Find the right value for flags flags=0, min=min_, max=max_, ) # Here collection_temporality is CUMULATIVE. # instrument_temporality is always DELTA for the time being. # Here we need to handle the case where: # collect is called after at least one other call to collect # (there is data in previous buckets, a call to merge is needed # to handle possible differences in bucket sizes). # collect is called without another call previous call to # collect was made (there is no previous buckets, previous, # empty buckets that are the same scale of the current buckets # need to be made so that they can be cumulatively aggregated # to the current buckets). if value_positive is None and self._previous_value_positive is None: # This happens if collect is called for the first time # and aggregate has not yet been called. value_positive = Buckets() self._previous_value_positive = value_positive.copy_empty() if value_negative is None and self._previous_value_negative is None: value_negative = Buckets() self._previous_value_negative = value_negative.copy_empty() if scale is None and self._previous_scale is None: scale = self._mapping.scale self._previous_scale = scale if value_positive is not None and self._previous_value_positive is None: # This happens when collect is called the very first time # and aggregate has been called before. # We need previous buckets to add them to the current ones. # When collect is called for the first time, there are no # previous buckets, so we need to create empty buckets to # add them to the current ones. The addition of empty # buckets to the current ones will result in the current # ones unchanged. # The way the previous buckets are generated here is # different from the explicit bucket histogram where # the size and amount of the buckets does not change once # they are instantiated. Here, the size and amount of the # buckets can change with every call to aggregate. In order # to get empty buckets that can be added to the current # ones resulting in the current ones unchanged we need to # generate empty buckets that have the same size and amount # as the current ones, this is what copy_empty does. self._previous_value_positive = value_positive.copy_empty() if value_negative is not None and self._previous_value_negative is None: self._previous_value_negative = value_negative.copy_empty() if scale is not None and self._previous_scale is None: self._previous_scale = scale if value_positive is None and self._previous_value_positive is not None: value_positive = self._previous_value_positive.copy_empty() if value_negative is None and self._previous_value_negative is not None: value_negative = self._previous_value_negative.copy_empty() if scale is None and self._previous_scale is not None: scale = self._previous_scale # here self._previous_value_negative and self._previous_value_positive are not Optional anymore self._previous_value_negative = cast(Buckets, self._previous_value_negative) self._previous_value_positive = cast(Buckets, self._previous_value_positive) min_scale = min(self._previous_scale, scale) low_positive, high_positive = self._get_low_high_previous_current( self._previous_value_positive, value_positive, scale, min_scale, ) low_negative, high_negative = self._get_low_high_previous_current( self._previous_value_negative, value_negative, scale, min_scale, ) min_scale = min( min_scale - self._get_scale_change(low_positive, high_positive), min_scale - self._get_scale_change(low_negative, high_negative), ) self._downscale( self._previous_scale - min_scale, self._previous_value_positive, self._previous_value_negative, ) # self._merge adds the values from value to # self._previous_value, this is analogous to # self._previous_value = [ # value_element + previous_value_element # for ( # value_element, # previous_value_element, # ) in zip(value, self._previous_value) # ] # in _ExplicitBucketHistogramAggregation.collect. self._merge( self._previous_value_positive, value_positive, scale, min_scale, collection_aggregation_temporality, ) self._merge( self._previous_value_negative, value_negative, scale, min_scale, collection_aggregation_temporality, ) self._previous_min = min(min_, self._previous_min) self._previous_max = max(max_, self._previous_max) self._previous_sum = sum_ + self._previous_sum self._previous_count = count + self._previous_count self._previous_zero_count = zero_count + self._previous_zero_count self._previous_scale = min_scale return ExponentialHistogramDataPoint( attributes=self._attributes, exemplars=self._collect_exemplars(), start_time_unix_nano=self._start_time_unix_nano, time_unix_nano=collection_start_nano, count=self._previous_count, sum=self._previous_sum, scale=self._previous_scale, zero_count=self._previous_zero_count, positive=BucketsPoint( offset=self._previous_value_positive.offset, bucket_counts=(self._previous_value_positive.get_offset_counts()), ), negative=BucketsPoint( offset=self._previous_value_negative.offset, bucket_counts=(self._previous_value_negative.get_offset_counts()), ), # FIXME: Find the right value for flags flags=0, min=self._previous_min, max=self._previous_max, ) return None def _get_low_high_previous_current( self, previous_point_buckets, current_point_buckets, current_scale, min_scale, ): (previous_point_low, previous_point_high) = self._get_low_high( previous_point_buckets, self._previous_scale, min_scale ) (current_point_low, current_point_high) = self._get_low_high(current_point_buckets, current_scale, min_scale) if current_point_low > current_point_high: low = previous_point_low high = previous_point_high elif previous_point_low > previous_point_high: low = current_point_low high = current_point_high else: low = min(previous_point_low, current_point_low) high = max(previous_point_high, current_point_high) return low, high @staticmethod def _get_low_high(buckets, scale, min_scale): if buckets.counts == [0]: return 0, -1 shift = scale - min_scale return buckets.index_start >> shift, buckets.index_end >> shift @staticmethod def _new_mapping(scale: int) -> Mapping: if scale <= 0: return ExponentMapping(scale) return LogarithmMapping(scale) def _get_scale_change(self, low, high): change = 0 while high - low >= self._max_size: high = high >> 1 low = low >> 1 change += 1 return change @staticmethod def _downscale(change: int, positive, negative): if change == 0: return if change < 0: # pylint: disable=broad-exception-raised raise Exception("Invalid change of scale") positive.downscale(change) negative.downscale(change) def _merge( self, previous_buckets: Buckets, current_buckets: Buckets, current_scale, min_scale, aggregation_temporality, ): current_change = current_scale - min_scale for current_bucket_index, current_bucket in enumerate(current_buckets.counts): if current_bucket == 0: continue # Not considering the case where len(previous_buckets) == 0. This # would not happen because self._previous_point is only assigned to # an ExponentialHistogramDataPoint object if self._count != 0. current_index = current_buckets.index_base + current_bucket_index if current_index > current_buckets.index_end: current_index -= len(current_buckets.counts) index = current_index >> current_change if index < previous_buckets.index_start: span = previous_buckets.index_end - index if span >= self._max_size: # pylint: disable=broad-exception-raised raise Exception("Incorrect merge scale") if span >= len(previous_buckets.counts): previous_buckets.grow(span + 1, self._max_size) previous_buckets.index_start = index if index > previous_buckets.index_end: span = index - previous_buckets.index_start if span >= self._max_size: # pylint: disable=broad-exception-raised raise Exception("Incorrect merge scale") if span >= len(previous_buckets.counts): previous_buckets.grow(span + 1, self._max_size) previous_buckets.index_end = index bucket_index = index - previous_buckets.index_base if bucket_index < 0: bucket_index += len(previous_buckets.counts) if aggregation_temporality is AggregationTemporality.DELTA: current_bucket = -current_bucket previous_buckets.increment_bucket(bucket_index, increment=current_bucket) class Aggregation(ABC): """ Base class for all aggregation types. """ @abstractmethod def _create_aggregation( self, instrument: _Instrument, attributes: Attributes, reservoir_factory: Callable[[type[_Aggregation]], ExemplarReservoirBuilder], start_time_unix_nano: int, ) -> _Aggregation: """Creates an aggregation""" class DefaultAggregation(Aggregation): """ The default aggregation to be used in a `View`. This aggregation will create an actual aggregation depending on the instrument type, as specified next: ==================================================== ==================================== Instrument Aggregation ==================================================== ==================================== `opentelemetry.sdk.metrics.Counter` `SumAggregation` `opentelemetry.sdk.metrics.UpDownCounter` `SumAggregation` `opentelemetry.sdk.metrics.ObservableCounter` `SumAggregation` `opentelemetry.sdk.metrics.ObservableUpDownCounter` `SumAggregation` `opentelemetry.sdk.metrics.Histogram` `ExplicitBucketHistogramAggregation` `opentelemetry.sdk.metrics.ObservableGauge` `LastValueAggregation` ==================================================== ==================================== """ def _create_aggregation( self, instrument: _Instrument, attributes: Attributes, reservoir_factory: Callable[[type[_Aggregation]], ExemplarReservoirBuilder], start_time_unix_nano: int, ) -> _Aggregation: # pylint: disable=too-many-return-statements if isinstance(instrument, Counter): return _SumAggregation( attributes, reservoir_builder=reservoir_factory(_SumAggregation), instrument_is_monotonic=True, instrument_aggregation_temporality=(AggregationTemporality.DELTA), start_time_unix_nano=start_time_unix_nano, ) if isinstance(instrument, UpDownCounter): return _SumAggregation( attributes, reservoir_builder=reservoir_factory(_SumAggregation), instrument_is_monotonic=False, instrument_aggregation_temporality=(AggregationTemporality.DELTA), start_time_unix_nano=start_time_unix_nano, ) if isinstance(instrument, ObservableCounter): return _SumAggregation( attributes, reservoir_builder=reservoir_factory(_SumAggregation), instrument_is_monotonic=True, instrument_aggregation_temporality=(AggregationTemporality.CUMULATIVE), start_time_unix_nano=start_time_unix_nano, ) if isinstance(instrument, ObservableUpDownCounter): return _SumAggregation( attributes, reservoir_builder=reservoir_factory(_SumAggregation), instrument_is_monotonic=False, instrument_aggregation_temporality=(AggregationTemporality.CUMULATIVE), start_time_unix_nano=start_time_unix_nano, ) if isinstance(instrument, Histogram): boundaries = instrument._advisory.explicit_bucket_boundaries return _ExplicitBucketHistogramAggregation( attributes, reservoir_builder=reservoir_factory(_ExplicitBucketHistogramAggregation), instrument_aggregation_temporality=(AggregationTemporality.DELTA), boundaries=boundaries, start_time_unix_nano=start_time_unix_nano, ) if isinstance(instrument, ObservableGauge): return _LastValueAggregation( attributes, reservoir_builder=reservoir_factory(_LastValueAggregation), instrument_is_synchronous=False, ) if isinstance(instrument, _Gauge): return _LastValueAggregation( attributes, reservoir_builder=reservoir_factory(_LastValueAggregation), instrument_is_synchronous=True, ) # pylint: disable=broad-exception-raised raise Exception(f"Invalid instrument type {type(instrument)} found") class ExponentialBucketHistogramAggregation(Aggregation): """This aggregation informs the SDK to collect: - Count of Measurement values falling using a base-2 exponential formula. - Arithmetic sum of Measurement values in population. This SHOULD NOT be collected when used with instruments that record negative measurements, e.g. UpDownCounter or ObservableGauge. - Min (optional) Measurement value in population. - Max (optional) Measurement value in population. Args: max_size: Maximum number of buckets in each of the positive and negative ranges, not counting the special zero bucket. max_scale: Maximum scale factor. record_min_max: Whether to record min and max. """ def __init__( self, max_size: int = 160, max_scale: int = 20, record_min_max: bool = True, ): self._max_size = max_size self._max_scale = max_scale self._record_min_max = record_min_max def _create_aggregation( self, instrument: _Instrument, attributes: Attributes, reservoir_factory: Callable[[type[_Aggregation]], ExemplarReservoirBuilder], start_time_unix_nano: int, ) -> _Aggregation: instrument_aggregation_temporality = AggregationTemporality.UNSPECIFIED if isinstance(instrument, Synchronous): instrument_aggregation_temporality = AggregationTemporality.DELTA elif isinstance(instrument, Asynchronous): instrument_aggregation_temporality = AggregationTemporality.CUMULATIVE return _ExponentialBucketHistogramAggregation( attributes, reservoir_factory(_ExponentialBucketHistogramAggregation), instrument_aggregation_temporality, start_time_unix_nano, max_size=self._max_size, max_scale=self._max_scale, record_min_max=self._record_min_max, ) class ExplicitBucketHistogramAggregation(Aggregation): """This aggregation informs the SDK to collect: - Count of Measurement values falling within explicit bucket boundaries. - Arithmetic sum of Measurement values in population. This SHOULD NOT be collected when used with instruments that record negative measurements, e.g. UpDownCounter or ObservableGauge. - Min (optional) Measurement value in population. - Max (optional) Measurement value in population. Args: boundaries: Array of increasing values representing explicit bucket boundary values. record_min_max: Whether to record min and max. """ def __init__( self, boundaries: Sequence[float] | None = None, record_min_max: bool = True, ) -> None: self._boundaries = boundaries self._record_min_max = record_min_max def _create_aggregation( self, instrument: _Instrument, attributes: Attributes, reservoir_factory: Callable[[type[_Aggregation]], ExemplarReservoirBuilder], start_time_unix_nano: int, ) -> _Aggregation: instrument_aggregation_temporality = AggregationTemporality.UNSPECIFIED if isinstance(instrument, Synchronous): instrument_aggregation_temporality = AggregationTemporality.DELTA elif isinstance(instrument, Asynchronous): instrument_aggregation_temporality = AggregationTemporality.CUMULATIVE if self._boundaries is not None: boundaries = self._boundaries else: # guard for usage with instruments without advisory advisory = getattr(instrument, "_advisory", None) boundaries = advisory.explicit_bucket_boundaries if advisory is not None else None return _ExplicitBucketHistogramAggregation( attributes, instrument_aggregation_temporality, start_time_unix_nano, reservoir_factory(_ExplicitBucketHistogramAggregation), boundaries, self._record_min_max, ) class SumAggregation(Aggregation): """This aggregation informs the SDK to collect: - The arithmetic sum of Measurement values. """ def _create_aggregation( self, instrument: _Instrument, attributes: Attributes, reservoir_factory: Callable[[type[_Aggregation]], ExemplarReservoirBuilder], start_time_unix_nano: int, ) -> _Aggregation: instrument_aggregation_temporality = AggregationTemporality.UNSPECIFIED if isinstance(instrument, Synchronous): instrument_aggregation_temporality = AggregationTemporality.DELTA elif isinstance(instrument, Asynchronous): instrument_aggregation_temporality = AggregationTemporality.CUMULATIVE return _SumAggregation( attributes, isinstance(instrument, (Counter, ObservableCounter)), instrument_aggregation_temporality, start_time_unix_nano, reservoir_factory(_SumAggregation), ) class LastValueAggregation(Aggregation): """ This aggregation informs the SDK to collect: - The last Measurement. - The timestamp of the last Measurement. """ def _create_aggregation( self, instrument: _Instrument, attributes: Attributes, reservoir_factory: Callable[[type[_Aggregation]], ExemplarReservoirBuilder], start_time_unix_nano: int, ) -> _Aggregation: return _LastValueAggregation( attributes, reservoir_builder=reservoir_factory(_LastValueAggregation), instrument_is_synchronous=isinstance(instrument, Synchronous), ) class DropAggregation(Aggregation): """Using this aggregation will make all measurements be ignored.""" def _create_aggregation( self, instrument: _Instrument, attributes: Attributes, reservoir_factory: Callable[[type[_Aggregation]], ExemplarReservoirBuilder], start_time_unix_nano: int, ) -> _Aggregation: return _DropAggregation(attributes, reservoir_factory(_DropAggregation)) python-opentelemetry-1.45.2/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exceptions.py000066400000000000000000000002471527070070700341210ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 class MetricsTimeoutError(Exception): """Raised when a metrics function times out""" python-opentelemetry-1.45.2/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exemplar/000077500000000000000000000000001527070070700332005ustar00rootroot00000000000000__init__.py000066400000000000000000000013061527070070700352320ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exemplar# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from .exemplar import Exemplar from .exemplar_filter import ( AlwaysOffExemplarFilter, AlwaysOnExemplarFilter, ExemplarFilter, TraceBasedExemplarFilter, ) from .exemplar_reservoir import ( AlignedHistogramBucketExemplarReservoir, ExemplarReservoir, ExemplarReservoirBuilder, SimpleFixedSizeExemplarReservoir, ) __all__ = [ "AlignedHistogramBucketExemplarReservoir", "AlwaysOffExemplarFilter", "AlwaysOnExemplarFilter", "Exemplar", "ExemplarFilter", "ExemplarReservoir", "ExemplarReservoirBuilder", "SimpleFixedSizeExemplarReservoir", "TraceBasedExemplarFilter", ] exemplar.py000066400000000000000000000023451527070070700353140ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exemplar# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import dataclasses from opentelemetry.util.types import Attributes @dataclasses.dataclass(frozen=True) class Exemplar: """A representation of an exemplar, which is a sample input measurement. Exemplars also hold information about the environment when the measurement was recorded, for example the span and trace ID of the active span when the exemplar was recorded. Attributes: trace_id: (optional) The trace associated with a recording span_id: (optional) The span associated with a recording time_unix_nano: The time of the observation value: The recorded value filtered_attributes: A set of filtered attributes which provide additional insight into the Context when the observation was made. References: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/data-model.md#exemplars https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk.md#exemplar """ filtered_attributes: Attributes value: int | float time_unix_nano: int span_id: int | None = None trace_id: int | None = None exemplar_filter.py000066400000000000000000000077461527070070700366730ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exemplar# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from abc import ABC, abstractmethod from opentelemetry import trace from opentelemetry.context import Context from opentelemetry.trace.span import INVALID_SPAN from opentelemetry.util.types import Attributes class ExemplarFilter(ABC): """``ExemplarFilter`` determines which measurements are eligible for becoming an ``Exemplar``. Exemplar filters are used to filter measurements before attempting to store them in a reservoir. Reference: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk.md#exemplarfilter """ @abstractmethod def should_sample( self, value: float, time_unix_nano: int, attributes: Attributes, context: Context, ) -> bool: """Returns whether or not a reservoir should attempt to filter a measurement. Args: value: The value of the measurement timestamp: A timestamp that best represents when the measurement was taken attributes: The complete set of measurement attributes context: The Context of the measurement """ raise NotImplementedError("ExemplarFilter.should_sample is not implemented") class AlwaysOnExemplarFilter(ExemplarFilter): """An ExemplarFilter which makes all measurements eligible for being an Exemplar. Reference: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk.md#alwayson """ def should_sample( self, value: float, time_unix_nano: int, attributes: Attributes, context: Context, ) -> bool: """Returns whether or not a reservoir should attempt to filter a measurement. Args: value: The value of the measurement timestamp: A timestamp that best represents when the measurement was taken attributes: The complete set of measurement attributes context: The Context of the measurement """ return True class AlwaysOffExemplarFilter(ExemplarFilter): """An ExemplarFilter which makes no measurements eligible for being an Exemplar. Using this ExemplarFilter is as good as disabling Exemplar feature. Reference: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk.md#alwaysoff """ def should_sample( self, value: float, time_unix_nano: int, attributes: Attributes, context: Context, ) -> bool: """Returns whether or not a reservoir should attempt to filter a measurement. Args: value: The value of the measurement timestamp: A timestamp that best represents when the measurement was taken attributes: The complete set of measurement attributes context: The Context of the measurement """ return False class TraceBasedExemplarFilter(ExemplarFilter): """An ExemplarFilter which makes those measurements eligible for being an Exemplar, which are recorded in the context of a sampled parent span. Reference: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk.md#tracebased """ def should_sample( self, value: float, time_unix_nano: int, attributes: Attributes, context: Context, ) -> bool: """Returns whether or not a reservoir should attempt to filter a measurement. Args: value: The value of the measurement timestamp: A timestamp that best represents when the measurement was taken attributes: The complete set of measurement attributes context: The Context of the measurement """ span = trace.get_current_span(context) if span == INVALID_SPAN: return False return span.get_span_context().trace_flags.sampled exemplar_reservoir.py000066400000000000000000000227621527070070700374210ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exemplar# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from abc import ABC, abstractmethod from collections import defaultdict from collections.abc import Callable, Mapping, Sequence from random import randrange from threading import Lock from typing import ( Any, ) from opentelemetry import trace from opentelemetry.context import Context from opentelemetry.trace.span import INVALID_SPAN from opentelemetry.util.types import Attributes from .exemplar import Exemplar class ExemplarReservoir(ABC): """ExemplarReservoir provide a method to offer measurements to the reservoir and another to collect accumulated Exemplars. Note: The constructor MUST accept ``**kwargs`` that may be set from aggregation parameters. Note: All methods MUST be safe to call concurrently. Measurements are offered from application threads while a metric reader collects from its own thread. Reference: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk.md#exemplarreservoir """ @abstractmethod def offer( self, value: float, time_unix_nano: int, attributes: Attributes, context: Context, ) -> None: """Offers a measurement to be sampled. Args: value: Measured value time_unix_nano: Measurement instant attributes: Measurement attributes context: Measurement context """ raise NotImplementedError("ExemplarReservoir.offer is not implemented") @abstractmethod def collect(self, point_attributes: Attributes) -> list[Exemplar]: """Returns accumulated Exemplars and also resets the reservoir for the next sampling period Args: point_attributes: The attributes associated with metric point. Returns: a list of ``opentelemetry.sdk.metrics._internal.exemplar.exemplar.Exemplar`` s. Returned exemplars contain the attributes that were filtered out by the aggregator, but recorded alongside the original measurement. """ raise NotImplementedError("ExemplarReservoir.collect is not implemented") class ExemplarBucket: def __init__(self) -> None: self.__value: int | float = 0 self.__attributes: Attributes = None self.__time_unix_nano: int = 0 self.__span_id: int | None = None self.__trace_id: int | None = None self.__offered: bool = False def offer( self, value: float, time_unix_nano: int, attributes: Attributes, context: Context, ) -> None: """Offers a measurement to be sampled. Args: value: Measured value time_unix_nano: Measurement instant attributes: Measurement attributes context: Measurement context """ self.__value = value self.__time_unix_nano = time_unix_nano self.__attributes = attributes span = trace.get_current_span(context) if span != INVALID_SPAN: span_context = span.get_span_context() self.__span_id = span_context.span_id self.__trace_id = span_context.trace_id self.__offered = True def collect(self, point_attributes: Attributes) -> Exemplar | None: """May return an Exemplar and resets the bucket for the next sampling period.""" if not self.__offered: return None # filters out attributes from the measurement that are already included in the metric data point # See the specification for more details: # https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk.md#exemplar filtered_attributes = ( {k: v for k, v in self.__attributes.items() if k not in point_attributes} if self.__attributes else None ) exemplar = Exemplar( filtered_attributes, self.__value, self.__time_unix_nano, self.__span_id, self.__trace_id, ) self.__reset() return exemplar def __reset(self) -> None: """Reset the bucket state after a collection cycle.""" self.__value = 0 self.__attributes = {} self.__time_unix_nano = 0 self.__span_id = None self.__trace_id = None self.__offered = False class BucketIndexError(ValueError): """An exception raised when the bucket index cannot be found.""" class FixedSizeExemplarReservoirABC(ExemplarReservoir): """Abstract class for a reservoir with fixed size.""" def __init__(self, size: int, **kwargs) -> None: super().__init__(**kwargs) self._size: int = size self._reservoir_storage: Mapping[int, ExemplarBucket] = defaultdict(ExemplarBucket) self._lock = Lock() def collect(self, point_attributes: Attributes) -> list[Exemplar]: """Returns accumulated Exemplars and also resets the reservoir for the next sampling period Args: point_attributes: The attributes associated with metric point. Returns: a list of ``opentelemetry.sdk.metrics._internal.exemplar.exemplar.Exemplar`` s. Returned exemplars contain the attributes that were filtered out by the aggregator, but recorded alongside the original measurement. """ with self._lock: exemplars = [ e for e in (bucket.collect(point_attributes) for _, bucket in sorted(self._reservoir_storage.items())) if e is not None ] self._reset() return exemplars def offer( self, value: float, time_unix_nano: int, attributes: Attributes, context: Context, ) -> None: """Offers a measurement to be sampled. Args: value: Measured value time_unix_nano: Measurement instant attributes: Measurement attributes context: Measurement context """ with self._lock: try: index = self._find_bucket_index(value, time_unix_nano, attributes, context) self._reservoir_storage[index].offer(value, time_unix_nano, attributes, context) except BucketIndexError: # Ignore invalid bucket index pass @abstractmethod def _find_bucket_index( self, value: float, time_unix_nano: int, attributes: Attributes, context: Context, ) -> int: """Determines the bucket index for the given measurement. It should be implemented by subclasses based on specific strategies. Args: value: Measured value time_unix_nano: Measurement instant attributes: Measurement attributes context: Measurement context Returns: The bucket index Raises: BucketIndexError: If no bucket index can be found. """ def _reset(self) -> None: """Reset the reservoir by resetting any stateful logic after a collection cycle.""" class SimpleFixedSizeExemplarReservoir(FixedSizeExemplarReservoirABC): """This reservoir uses an uniformly-weighted sampling algorithm based on the number of samples the reservoir has seen so far to determine if the offered measurements should be sampled. Reference: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk.md#simplefixedsizeexemplarreservoir """ def __init__(self, size: int = 1, **kwargs) -> None: super().__init__(size, **kwargs) self._measurements_seen: int = 0 def _reset(self) -> None: super()._reset() self._measurements_seen = 0 def _find_bucket_index( self, value: float, time_unix_nano: int, attributes: Attributes, context: Context, ) -> int: if self._measurements_seen < self._size: self._measurements_seen += 1 return self._measurements_seen - 1 self._measurements_seen += 1 index = randrange(0, self._measurements_seen) if index < self._size: return index raise BucketIndexError("Unable to find the bucket index.") class AlignedHistogramBucketExemplarReservoir(FixedSizeExemplarReservoirABC): """This Exemplar reservoir takes a configuration parameter that is the configuration of a Histogram. This implementation keeps the last seen measurement that falls within a histogram bucket. Reference: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk.md#alignedhistogrambucketexemplarreservoir """ def __init__(self, boundaries: Sequence[float], **kwargs) -> None: super().__init__(len(boundaries) + 1, **kwargs) self._boundaries: Sequence[float] = boundaries def _find_bucket_index( self, value: float, time_unix_nano: int, attributes: Attributes, context: Context, ) -> int: for index, boundary in enumerate(self._boundaries): if value <= boundary: return index return len(self._boundaries) # ExemplarReservoirBuilder: A callable that takes Aggregation parameters # (e.g. boundaries for _ExplicitBucketHistogramAggregation) and returns # an ExemplarReservoir instance. ExemplarReservoirBuilder = Callable[[dict[str, Any]], ExemplarReservoir] exponential_histogram/000077500000000000000000000000001527070070700357075ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal__init__.py000066400000000000000000000000001527070070700400060ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogrambuckets.py000066400000000000000000000126071527070070700377270ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from math import ceil, log2 class Buckets: # No method of this class is protected by locks because instances of this # class are only used in methods that are protected by locks themselves. def __init__(self): self._counts = [0] # The term index refers to the number of the exponential histogram bucket # used to determine its boundaries. The lower boundary of a bucket is # determined by base ** index and the upper boundary of a bucket is # determined by base ** (index + 1). index values are signedto account # for values less than or equal to 1. # self._index_* will all have values equal to a certain index that is # determined by the corresponding mapping _map_to_index function and # the value of the index depends on the value passed to _map_to_index. # Index of the 0th position in self._counts: self._counts[0] is the # count in the bucket with index self.__index_base. self.__index_base = 0 # self.__index_start is the smallest index value represented in # self._counts. self.__index_start = 0 # self.__index_start is the largest index value represented in # self._counts. self.__index_end = 0 @property def index_start(self) -> int: return self.__index_start @index_start.setter def index_start(self, value: int) -> None: self.__index_start = value @property def index_end(self) -> int: return self.__index_end @index_end.setter def index_end(self, value: int) -> None: self.__index_end = value @property def index_base(self) -> int: return self.__index_base @index_base.setter def index_base(self, value: int) -> None: self.__index_base = value @property def counts(self): return self._counts def get_offset_counts(self): bias = self.__index_base - self.__index_start return self._counts[-bias:] + self._counts[:-bias] def grow(self, needed: int, max_size: int) -> None: size = len(self._counts) bias = self.__index_base - self.__index_start old_positive_limit = size - bias # 2 ** ceil(log2(needed)) finds the smallest power of two that is larger # or equal than needed: # 2 ** ceil(log2(1)) == 1 # 2 ** ceil(log2(2)) == 2 # 2 ** ceil(log2(3)) == 4 # 2 ** ceil(log2(4)) == 4 # 2 ** ceil(log2(5)) == 8 # 2 ** ceil(log2(6)) == 8 # 2 ** ceil(log2(7)) == 8 # 2 ** ceil(log2(8)) == 8 new_size = min(2 ** ceil(log2(needed)), max_size) new_positive_limit = new_size - bias tmp = [0] * new_size tmp[new_positive_limit:] = self._counts[old_positive_limit:] tmp[0:old_positive_limit] = self._counts[0:old_positive_limit] self._counts = tmp @property def offset(self) -> int: return self.__index_start def __len__(self) -> int: if len(self._counts) == 0: return 0 if self.__index_end == self.__index_start and self[0] == 0: return 0 return self.__index_end - self.__index_start + 1 def __getitem__(self, key: int) -> int: bias = self.__index_base - self.__index_start if key < bias: key += len(self._counts) key -= bias return self._counts[key] def downscale(self, amount: int) -> None: """ Rotates, then collapses 2 ** amount to 1 buckets. """ bias = self.__index_base - self.__index_start if bias != 0: self.__index_base = self.__index_start # [0, 1, 2, 3, 4] Original backing array self._counts = self._counts[::-1] # [4, 3, 2, 1, 0] self._counts = self._counts[:bias][::-1] + self._counts[bias:][::-1] # [3, 4, 0, 1, 2] This is a rotation of the backing array. size = 1 + self.__index_end - self.__index_start each = 1 << amount inpos = 0 outpos = 0 pos = self.__index_start while pos <= self.__index_end: mod = pos % each if mod < 0: mod += each index = mod while index < each and inpos < size: if outpos != inpos: self._counts[outpos] += self._counts[inpos] self._counts[inpos] = 0 inpos += 1 pos += 1 index += 1 outpos += 1 self.__index_start >>= amount self.__index_end >>= amount self.__index_base = self.__index_start def increment_bucket(self, bucket_index: int, increment: int = 1) -> None: self._counts[bucket_index] += increment def copy_empty(self) -> "Buckets": copy = Buckets() # pylint: disable=no-member # pylint: disable=protected-access # pylint: disable=attribute-defined-outside-init # pylint: disable=invalid-name copy._Buckets__index_base = self._Buckets__index_base # type: ignore[reportArgumentType] copy._Buckets__index_start = self._Buckets__index_start # type: ignore[reportArgumentType] copy._Buckets__index_end = self._Buckets__index_end # type: ignore[reportArgumentType] copy._counts = [0 for _ in self._counts] return copy mapping/000077500000000000000000000000001527070070700373425ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram__init__.py000066400000000000000000000066311527070070700414610ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from abc import ABC, abstractmethod from threading import Lock class Mapping(ABC): """ Parent class for `LogarithmMapping` and `ExponentialMapping`. """ _mappings: dict[int, "Mapping"] _mappings_lock: Lock _min_scale: int _max_scale: int # pylint: disable=no-member def __new__(cls, scale: int): with cls._mappings_lock: # cls._mappings and cls._mappings_lock are implemented in each of # the child classes as a dictionary and a lock, respectively. They # are not instantiated here because that would lead to both child # classes having the same instance of cls._mappings and # cls._mappings_lock. if scale not in cls._mappings: cls._mappings[scale] = super().__new__(cls) cls._mappings[scale]._init(scale) return cls._mappings[scale] @abstractmethod def _init(self, scale: int) -> None: # pylint: disable=attribute-defined-outside-init if scale > self._get_max_scale(): # pylint: disable=broad-exception-raised raise Exception(f"scale is larger than {self._max_scale}") if scale < self._get_min_scale(): # pylint: disable=broad-exception-raised raise Exception(f"scale is smaller than {self._min_scale}") # The size of the exponential histogram buckets is determined by a # parameter known as scale, larger values of scale will produce smaller # buckets. Bucket boundaries of the exponential histogram are located # at integer powers of the base, where: # # base = 2 ** (2 ** (-scale)) # https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/data-model.md#all-scales-use-the-logarithm-function self._scale = scale @abstractmethod def _get_min_scale(self) -> int: """ Return the smallest possible value for the mapping scale """ @abstractmethod def _get_max_scale(self) -> int: """ Return the largest possible value for the mapping scale """ @abstractmethod def map_to_index(self, value: float) -> int: """ Maps positive floating point values to indexes corresponding to `Mapping.scale`. Implementations are not expected to handle zeros, +inf, NaN, or negative values. """ @abstractmethod def get_lower_boundary(self, index: int) -> float: """ Returns the lower boundary of a given bucket index. The index is expected to map onto a range that is at least partially inside the range of normal floating point values. If the corresponding bucket's upper boundary is less than or equal to 2 ** -1022, :class:`~opentelemetry.sdk.metrics.MappingUnderflowError` will be raised. If the corresponding bucket's lower boundary is greater than ``sys.float_info.max``, :class:`~opentelemetry.sdk.metrics.MappingOverflowError` will be raised. """ @property def scale(self) -> int: """ Returns the parameter that controls the resolution of this mapping. See: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/datamodel.md#exponential-scale """ return self._scale errors.py000066400000000000000000000005721527070070700412340ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 class MappingUnderflowError(Exception): """ Raised when computing the lower boundary of an index that maps into a denormal floating point value. """ class MappingOverflowError(Exception): """ Raised when computing the lower boundary of an index that maps into +inf. """ exponent_mapping.py000066400000000000000000000144601527070070700432740ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from math import ldexp from threading import Lock from opentelemetry.sdk.metrics._internal.exponential_histogram.mapping import ( Mapping, ) from opentelemetry.sdk.metrics._internal.exponential_histogram.mapping.errors import ( MappingOverflowError, MappingUnderflowError, ) from opentelemetry.sdk.metrics._internal.exponential_histogram.mapping.ieee_754 import ( MANTISSA_WIDTH, MAX_NORMAL_EXPONENT, MIN_NORMAL_EXPONENT, MIN_NORMAL_VALUE, get_ieee_754_exponent, get_ieee_754_mantissa, ) class ExponentMapping(Mapping): # Reference implementation here: # https://github.com/open-telemetry/opentelemetry-go/blob/0e6f9c29c10d6078e8131418e1d1d166c7195d61/sdk/metric/aggregator/exponential/mapping/exponent/exponent.go _mappings = {} _mappings_lock = Lock() _min_scale = -10 _max_scale = 0 def _get_min_scale(self): # _min_scale defines the point at which the exponential mapping # function becomes useless for 64-bit floats. With scale -10, ignoring # subnormal values, bucket indices range from -1 to 1. return -10 def _get_max_scale(self): # _max_scale is the largest scale supported by exponential mapping. Use # a logarithm mapping for larger scales. return 0 def _init(self, scale: int): # pylint: disable=attribute-defined-outside-init super()._init(scale) # self._min_normal_lower_boundary_index is the largest index such that # base ** index < MIN_NORMAL_VALUE and # base ** (index + 1) >= MIN_NORMAL_VALUE. An exponential histogram # bucket with this index covers the range # (base ** index, base (index + 1)], including MIN_NORMAL_VALUE. This # is the smallest valid index that contains at least one normal value. index = MIN_NORMAL_EXPONENT >> -self._scale if -self._scale < 2: # For scales -1 and 0, the maximum value 2 ** -1022 is a # power-of-two multiple, meaning base ** index == MIN_NORMAL_VALUE. # Subtracting 1 so that base ** (index + 1) == MIN_NORMAL_VALUE. index -= 1 self._min_normal_lower_boundary_index = index # self._max_normal_lower_boundary_index is the index such that # base**index equals the greatest representable lower boundary. An # exponential histogram bucket with this index covers the range # ((2 ** 1024) / base, 2 ** 1024], which includes opentelemetry.sdk. # metrics._internal.exponential_histogram.ieee_754.MAX_NORMAL_VALUE. # This bucket is incomplete, since the upper boundary cannot be # represented. One greater than this index corresponds with the bucket # containing values > 2 ** 1024. self._max_normal_lower_boundary_index = MAX_NORMAL_EXPONENT >> -self._scale def map_to_index(self, value: float) -> int: if value < MIN_NORMAL_VALUE: return self._min_normal_lower_boundary_index exponent = get_ieee_754_exponent(value) # Positive integers are represented in binary as having an infinite # amount of leading zeroes, for example 2 is represented as ...00010. # A negative integer -x is represented in binary as the complement of # (x - 1). For example, -4 is represented as the complement of 4 - 1 # == 3. 3 is represented as ...00011. Its compliment is ...11100, the # binary representation of -4. # get_ieee_754_mantissa(value) gets the positive integer made up # from the rightmost MANTISSA_WIDTH bits (the mantissa) of the IEEE # 754 representation of value. If value is an exact power of 2, all # these MANTISSA_WIDTH bits would be all zeroes, and when 1 is # subtracted the resulting value is -1. The binary representation of # -1 is ...111, so when these bits are right shifted MANTISSA_WIDTH # places, the resulting value for correction is -1. If value is not an # exact power of 2, at least one of the rightmost MANTISSA_WIDTH # bits would be 1 (even for values whose decimal part is 0, like 5.0 # since the IEEE 754 of such number is too the product of a power of 2 # (defined in the exponent part of the IEEE 754 representation) and the # value defined in the mantissa). Having at least one of the rightmost # MANTISSA_WIDTH bit being 1 means that get_ieee_754(value) will # always be greater or equal to 1, and when 1 is subtracted, the # result will be greater or equal to 0, whose representation in binary # will be of at most MANTISSA_WIDTH ones that have an infinite # amount of leading zeroes. When those MANTISSA_WIDTH bits are # shifted to the right MANTISSA_WIDTH places, the resulting value # will be 0. # In summary, correction will be -1 if value is a power of 2, 0 if not. # map_to_index requires value to be a finite, positive real number. # 0, inf, and NaN violate this precondition. # Zero is represented in IEEE 754 with all exponent bits set to 0, # giving get_ieee_754_exponent a result of -1023. Since -1023 is less # than MIN_NORMAL_EXPONENT (-1022), zero is caught by the guard above # and returned early. # Inf is represented in IEEE 754 with all 11 exponent bits set to 1 # and a mantissa of 0, giving get_ieee_754_exponent a result of 1024 # and correction a value of -1. # NaN is represented in IEEE 754 with all 11 exponent bits set to 1 # and a non-zero mantissa of unspecified bit pattern, producing an # unspecified correction value. # Inf and NaN are not caught by the guard above. Callers must ensure # that only finite, positive values are passed to map_to_index. correction = (get_ieee_754_mantissa(value) - 1) >> MANTISSA_WIDTH return (exponent + correction) >> -self._scale def get_lower_boundary(self, index: int) -> float: if index < self._min_normal_lower_boundary_index: raise MappingUnderflowError() if index > self._max_normal_lower_boundary_index: raise MappingOverflowError() return ldexp(1, index << -self._scale) @property def scale(self) -> int: return self._scale ieee_754.md000066400000000000000000000115641527070070700412010ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping# IEEE 754 Explained IEEE 754 is a standard that defines a way to represent certain mathematical objects using binary numbers. ## Binary Number Fields The binary numbers used in IEEE 754 can have different lengths, the length that is interesting for the purposes of this project is 64 bits. These binary numbers are made up of 3 contiguous fields of bits, from left to right: 1. 1 sign bit 2. 11 exponent bits 3. 52 mantissa bits Depending on the values these fields have, the represented mathematical object can be one of: * Floating point number * Zero * NaN * Infinite ## Floating Point Numbers IEEE 754 represents a floating point number $f$ using an exponential notation with 4 components: $sign$, $mantissa$, $base$ and $exponent$: $$f = sign \times mantissa \times base ^ {exponent}$$ There are two possible representations of floating point numbers: _normal_ and _denormal_, which have different valid values for their $mantissa$ and $exponent$ fields. ### Binary Representation $sign$, $mantissa$, and $exponent$ are represented in binary, the representation of each component has certain details explained next. $base$ is always $2$ and it is not represented in binary. #### Sign $sign$ can have 2 values: 1. $1$ if the `sign` bit is `0` 2. $-1$ if the `sign` bit is `1`. #### Mantissa ##### Normal Floating Point Numbers $mantissa$ is a positive fractional number whose integer part is $1$, for example $1.2345 \dots$. The `mantissa` bits represent only the fractional part and the $mantissa$ value can be calculated as: $$mantissa = 1 + \sum_{i=1}^{52} b_{i} \times 2^{-i} = 1 + \frac{b_{1}}{2^{1}} + \frac{b_{2}}{2^{2}} + \dots + \frac{b_{51}}{2^{51}} + \frac{b_{52}}{2^{52}}$$ Where $b_{i}$ is: 1. $0$ if the bit at the position `i - 1` is `0`. 2. $1$ if the bit at the position `i - 1` is `1`. ##### Denormal Floating Point Numbers $mantissa$ is a positive fractional number whose integer part is $0$, for example $0.12345 \dots$. The `mantissa` bits represent only the fractional part and the $mantissa$ value can be calculated as: $$mantissa = \sum_{i=1}^{52} b_{i} \times 2^{-i} = \frac{b_{1}}{2^{1}} + \frac{b_{2}}{2^{2}} + \dots + \frac{b_{51}}{2^{51}} + \frac{b_{52}}{2^{52}}$$ Where $b_{i}$ is: 1. $0$ if the bit at the position `i - 1` is `0`. 2. $1$ if the bit at the position `i - 1` is `1`. #### Exponent ##### Normal Floating Point Numbers Only the following bit sequences are allowed: `00000000001` to `11111111110`. That is, there must be at least one `0` and one `1` in the exponent bits. The actual value of the $exponent$ can be calculated as: $$exponent = v - bias$$ where $v$ is the value of the binary number in the exponent bits and $bias$ is $1023$. Considering the restrictions above, the respective minimum and maximum values for the exponent are: 1. `00000000001` = $1$, $1 - 1023 = -1022$ 2. `11111111110` = $2046$, $2046 - 1023 = 1023$ So, $exponent$ is an integer in the range $\left[-1022, 1023\right]$. ##### Denormal Floating Point Numbers $exponent$ is always $-1022$. Nevertheless, it is always represented as `00000000000`. ### Normal and Denormal Floating Point Numbers The smallest absolute value a normal floating point number can have is calculated like this: $$1 \times 1.0\dots0 \times 2^{-1022} = 2.2250738585072014 \times 10^{-308}$$ Since normal floating point numbers always have a $1$ as the integer part of the $mantissa$, then smaller values can be achieved by using the smallest possible exponent ( $-1022$ ) and a $0$ in the integer part of the $mantissa$, but significant digits are lost. The smallest absolute value a denormal floating point number can have is calculated like this: $$1 \times 2^{-52} \times 2^{-1022} = 5 \times 10^{-324}$$ ## Zero Zero is represented like this: * Sign bit: `X` * Exponent bits: `00000000000` * Mantissa bits: `0000000000000000000000000000000000000000000000000000` where `X` means `0` or `1`. ## NaN There are 2 kinds of NaNs that are represented: 1. QNaNs (Quiet NaNs): represent the result of indeterminate operations. 2. SNaNs (Signalling NaNs): represent the result of invalid operations. ### QNaNs QNaNs are represented like this: * Sign bit: `X` * Exponent bits: `11111111111` * Mantissa bits: `1XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX` where `X` means `0` or `1`. ### SNaNs SNaNs are represented like this: * Sign bit: `X` * Exponent bits: `11111111111` * Mantissa bits: `0XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX1` where `X` means `0` or `1`. ## Infinite ### Positive Infinite Positive infinite is represented like this: * Sign bit: `0` * Exponent bits: `11111111111` * Mantissa bits: `0000000000000000000000000000000000000000000000000000` where `X` means `0` or `1`. ### Negative Infinite Negative infinite is represented like this: * Sign bit: `1` * Exponent bits: `11111111111` * Mantissa bits: `0000000000000000000000000000000000000000000000000000` where `X` means `0` or `1`. ieee_754.py000066400000000000000000000115561527070070700412320ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from ctypes import c_double, c_uint64 from sys import float_info # IEEE 754 64-bit floating point numbers use 11 bits for the exponent and 52 # bits for the mantissa. MANTISSA_WIDTH = 52 EXPONENT_WIDTH = 11 # This mask is equivalent to 52 "1" bits (there are 13 hexadecimal 4-bit "f"s # in the mantissa mask, 13 * 4 == 52) or 0xfffffffffffff in hexadecimal. MANTISSA_MASK = (1 << MANTISSA_WIDTH) - 1 # There are 11 bits for the exponent, but the exponent values 0 (11 "0" # bits) and 2047 (11 "1" bits) have special meanings so the exponent range is # from 1 to 2046. To calculate the exponent value, 1023 (the bias) is # subtracted from the exponent, so the exponent value range is from -1022 to # +1023. EXPONENT_BIAS = (2 ** (EXPONENT_WIDTH - 1)) - 1 # All the exponent mask bits are set to 1 for the 11 exponent bits. EXPONENT_MASK = ((1 << EXPONENT_WIDTH) - 1) << MANTISSA_WIDTH # The sign mask has the first bit set to 1 and the rest to 0. SIGN_MASK = 1 << (EXPONENT_WIDTH + MANTISSA_WIDTH) # For normal floating point numbers, the exponent can have a value in the # range [-1022, 1023]. MIN_NORMAL_EXPONENT = -EXPONENT_BIAS + 1 MAX_NORMAL_EXPONENT = EXPONENT_BIAS # The smallest possible normal value is 2.2250738585072014e-308. # This value is the result of using the smallest possible number in the # mantissa, 1.0000000000000000000000000000000000000000000000000000 (52 "0"s in # the fractional part) and a single "1" in the exponent. # Finally 1 * (2 ** -1022) = 2.2250738585072014e-308. MIN_NORMAL_VALUE = float_info.min # Greatest possible normal value (1.7976931348623157e+308) # The binary representation of a float in scientific notation uses (for the # mantissa) one bit for the integer part (which is implicit) and 52 bits for # the fractional part. Consider a float binary 1.111. It is equal to 1 + 1/2 + # 1/4 + 1/8. The greatest possible value in the 52-bit binary mantissa would be # then 1.1111111111111111111111111111111111111111111111111111 (52 "1"s in the # fractional part) whose decimal value is 1.9999999999999998. Finally, # 1.9999999999999998 * (2 ** 1023) = 1.7976931348623157e+308. MAX_NORMAL_VALUE = float_info.max def get_ieee_754_exponent(value: float) -> int: """ Gets the exponent of the IEEE 754 representation of a float. """ return ( ( # This step gives the integer that corresponds to the IEEE 754 # representation of a float. For example, consider # -MAX_NORMAL_VALUE for an example. We choose this value because # of its binary representation which makes easy to understand the # subsequent operations. # # c_uint64.from_buffer(c_double(-MAX_NORMAL_VALUE)).value == 18442240474082181119 # bin(18442240474082181119) == '0b1111111111101111111111111111111111111111111111111111111111111111' # # The first bit of the previous binary number is the sign bit: 1 (1 means negative, 0 means positive) # The next 11 bits are the exponent bits: 11111111110 # The next 52 bits are the mantissa bits: 1111111111111111111111111111111111111111111111111111 # # This step isolates the exponent bits, turning every bit outside # of the exponent field (sign and mantissa bits) to 0. c_uint64.from_buffer(c_double(value)).value & EXPONENT_MASK # For the example this means: # 18442240474082181119 & EXPONENT_MASK == 9214364837600034816 # bin(9214364837600034816) == '0b111111111100000000000000000000000000000000000000000000000000000' # Notice that the previous binary representation does not include # leading zeroes, so the sign bit is not included since it is a # zero. ) # This step moves the exponent bits to the right, removing the # mantissa bits that were set to 0 by the previous step. This # leaves the IEEE 754 exponent value, ready for the next step. >> MANTISSA_WIDTH # For the example this means: # 9214364837600034816 >> MANTISSA_WIDTH == 2046 # bin(2046) == '0b11111111110' # As shown above, these are the original 11 bits that correspond to the # exponent. # This step subtracts the exponent bias from the IEEE 754 value, # leaving the actual exponent value. ) - EXPONENT_BIAS # For the example this means: # 2046 - EXPONENT_BIAS == 1023 # As mentioned in a comment above, the largest value for the exponent is def get_ieee_754_mantissa(value: float) -> int: return ( c_uint64.from_buffer(c_double(value)).value # This step isolates the mantissa bits. There is no need to do any # bit shifting as the mantissa bits are already the rightmost field # in an IEEE 754 representation. & MANTISSA_MASK ) logarithm_mapping.py000066400000000000000000000125431527070070700434220ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from math import exp, floor, ldexp, log from threading import Lock from opentelemetry.sdk.metrics._internal.exponential_histogram.mapping import ( Mapping, ) from opentelemetry.sdk.metrics._internal.exponential_histogram.mapping.errors import ( MappingOverflowError, MappingUnderflowError, ) from opentelemetry.sdk.metrics._internal.exponential_histogram.mapping.ieee_754 import ( MAX_NORMAL_EXPONENT, MIN_NORMAL_EXPONENT, MIN_NORMAL_VALUE, get_ieee_754_exponent, get_ieee_754_mantissa, ) class LogarithmMapping(Mapping): # Reference implementation here: # https://github.com/open-telemetry/opentelemetry-go/blob/0e6f9c29c10d6078e8131418e1d1d166c7195d61/sdk/metric/aggregator/exponential/mapping/logarithm/logarithm.go _mappings = {} _mappings_lock = Lock() _min_scale = 1 _max_scale = 20 def _get_min_scale(self): # _min_scale ensures that ExponentMapping is used for zero and negative # scale values. return self._min_scale def _get_max_scale(self): # _max_scale is 20. The OpenTelemetry specification requires that # bucket indices fit within a signed 32-bit integer. At scale 20, # the maximum bucket index is ((MAX_NORMAL_EXPONENT + 1) << 20) - 1, # which fits within this range. At scale 21, the maximum bucket # index reaches the upper limit of a signed 32-bit integer, making # it difficult to test correctness. See: # https://github.com/lightstep/otel-launcher-go/blob/c9ca8483be067a39ab306b09060446e7fda65f35/lightstep/sdk/metric/aggregator/histogram/structure/README.md#mapping-function # https://github.com/open-telemetry/opentelemetry-go/blob/0e6f9c29c10d6078e8131418e1d1d166c7195d61/sdk/metric/aggregator/exponential/mapping/logarithm/logarithm.go#L32-L45 return self._max_scale def _init(self, scale: int): # pylint: disable=attribute-defined-outside-init super()._init(scale) # self._scale_factor is defined as a multiplier because multiplication # is faster than division. self._scale_factor is defined as: # index = log(value) * self._scale_factor # Where: # index = log(value) / log(base) # index = log(value) / log(2 ** (2 ** -scale)) # index = log(value) / ((2 ** -scale) * log(2)) # index = log(value) * ((1 / log(2)) * (2 ** scale)) # self._scale_factor = ((1 / log(2)) * (2 ** scale)) # self._scale_factor = (1 /log(2)) * (2 ** scale) # self._scale_factor = ldexp(1 / log(2), scale) # This implementation was copied from a Java prototype. See: # https://github.com/newrelic-experimental/newrelic-sketch-java/blob/1ce245713603d61ba3a4510f6df930a5479cd3f6/src/main/java/com/newrelic/nrsketch/indexer/LogIndexer.java # for the equations used here. self._scale_factor = ldexp(1 / log(2), scale) # self._min_normal_lower_boundary_index is the index such that # base ** index == MIN_NORMAL_VALUE. An exponential histogram bucket # with this index covers the range # (MIN_NORMAL_VALUE, MIN_NORMAL_VALUE * base]. One less than this index # corresponds with the bucket containing values <= MIN_NORMAL_VALUE. self._min_normal_lower_boundary_index = MIN_NORMAL_EXPONENT << self._scale # self._max_normal_lower_boundary_index is the index such that # base ** index equals the greatest representable lower boundary. An # exponential histogram bucket with this index covers the range # ((2 ** 1024) / base, 2 ** 1024], which includes opentelemetry.sdk. # metrics._internal.exponential_histogram.ieee_754.MAX_NORMAL_VALUE. # This bucket is incomplete, since the upper boundary cannot be # represented. One greater than this index corresponds with the bucket # containing values > 2 ** 1024. self._max_normal_lower_boundary_index = ((MAX_NORMAL_EXPONENT + 1) << self._scale) - 1 def map_to_index(self, value: float) -> int: """ Maps positive floating point values to indexes corresponding to scale. """ # value is subnormal if value <= MIN_NORMAL_VALUE: return self._min_normal_lower_boundary_index - 1 # value is an exact power of two. if get_ieee_754_mantissa(value) == 0: exponent = get_ieee_754_exponent(value) return (exponent << self._scale) - 1 return min( floor(log(value) * self._scale_factor), self._max_normal_lower_boundary_index, ) def get_lower_boundary(self, index: int) -> float: if index >= self._max_normal_lower_boundary_index: if index == self._max_normal_lower_boundary_index: return 2 * exp((index - (1 << self._scale)) / self._scale_factor) raise MappingOverflowError() if index <= self._min_normal_lower_boundary_index: if index == self._min_normal_lower_boundary_index: return MIN_NORMAL_VALUE if index == self._min_normal_lower_boundary_index - 1: return exp((index + (1 << self._scale)) / self._scale_factor) / 2 raise MappingUnderflowError() return exp(index / self._scale_factor) @property def scale(self) -> int: return self._scale python-opentelemetry-1.45.2/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/export/000077500000000000000000000000001527070070700327045ustar00rootroot00000000000000__init__.py000066400000000000000000000563211527070070700347450ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/export# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations import math import os import weakref from abc import ABC, abstractmethod from collections.abc import Callable, Iterable from enum import Enum from logging import getLogger from os import environ, linesep from sys import stdout from threading import Event, Lock, RLock, Thread from time import perf_counter, time_ns from typing import IO from typing_extensions import final # This kind of import is needed to avoid Sphinx errors. import opentelemetry.sdk.metrics._internal from opentelemetry.context import ( _SUPPRESS_INSTRUMENTATION_KEY, attach, detach, set_value, ) from opentelemetry.metrics import MeterProvider, NoOpMeterProvider from opentelemetry.sdk.environment_variables import ( OTEL_METRIC_EXPORT_INTERVAL, OTEL_METRIC_EXPORT_TIMEOUT, OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED, ) from opentelemetry.sdk.environment_variables._internal import ( parse_boolean_environment_variable, ) from opentelemetry.sdk.metrics._internal._view_instrument_match import ( _DEFAULT_CARDINALITY_LIMIT, ) from opentelemetry.sdk.metrics._internal.aggregation import ( AggregationTemporality, DefaultAggregation, ) from opentelemetry.sdk.metrics._internal.exceptions import MetricsTimeoutError from opentelemetry.sdk.metrics._internal.instrument import ( Counter, Gauge, Histogram, ObservableCounter, ObservableGauge, ObservableUpDownCounter, UpDownCounter, _Counter, _Gauge, _Histogram, _ObservableCounter, _ObservableGauge, _ObservableUpDownCounter, _UpDownCounter, ) from opentelemetry.sdk.metrics._internal.point import MetricsData from opentelemetry.semconv._incubating.attributes.otel_attributes import ( OtelComponentTypeValues, ) from opentelemetry.util._once import Once from ._metric_reader_metrics import create_metric_reader_metrics _logger = getLogger(__name__) class MetricExportResult(Enum): """Result of exporting a metric Can be any of the following values:""" SUCCESS = 0 FAILURE = 1 class MetricExporter(ABC): """Interface for exporting metrics. Interface to be implemented by services that want to export metrics received in their own format. Args: preferred_temporality: Used by `opentelemetry.sdk.metrics.export.PeriodicExportingMetricReader` to configure exporter level preferred temporality. See `opentelemetry.sdk.metrics.export.MetricReader` for more details on what preferred temporality is. preferred_aggregation: Used by `opentelemetry.sdk.metrics.export.PeriodicExportingMetricReader` to configure exporter level preferred aggregation. See `opentelemetry.sdk.metrics.export.MetricReader` for more details on what preferred aggregation is. """ def __init__( self, preferred_temporality: dict[type, AggregationTemporality] | None = None, preferred_aggregation: dict[type, opentelemetry.sdk.metrics.view.Aggregation] | None = None, ) -> None: self._preferred_temporality = preferred_temporality self._preferred_aggregation = preferred_aggregation @abstractmethod def export( self, metrics_data: MetricsData, timeout_millis: float = 10_000, **kwargs, ) -> MetricExportResult: """Exports a batch of telemetry data. Args: metrics: The list of `opentelemetry.sdk.metrics.export.Metric` objects to be exported Returns: The result of the export """ @abstractmethod def force_flush(self, timeout_millis: float = 10_000) -> bool: """ Ensure that export of any metrics currently received by the exporter are completed as soon as possible. """ @abstractmethod def shutdown(self, timeout_millis: float = 30_000, **kwargs) -> None: """Shuts down the exporter. Called when the SDK is shut down. """ class ConsoleMetricExporter(MetricExporter): """Implementation of :class:`MetricExporter` that prints metrics to the console. This class can be used for diagnostic purposes. It prints the exported metrics to the console STDOUT. """ def __init__( self, out: IO = stdout, formatter: Callable[[MetricsData], str] = lambda metrics_data: metrics_data.to_json() + linesep, preferred_temporality: dict[type, AggregationTemporality] | None = None, preferred_aggregation: dict[type, opentelemetry.sdk.metrics.view.Aggregation] | None = None, ): super().__init__( preferred_temporality=preferred_temporality, preferred_aggregation=preferred_aggregation, ) self.out = out self.formatter = formatter def export( self, metrics_data: MetricsData, timeout_millis: float = 10_000, **kwargs, ) -> MetricExportResult: self.out.write(self.formatter(metrics_data)) self.out.flush() return MetricExportResult.SUCCESS def shutdown(self, timeout_millis: float = 30_000, **kwargs) -> None: pass def force_flush(self, timeout_millis: float = 10_000) -> bool: return True InstrumentCardinalityKey = ( type[Counter] | type[UpDownCounter] | type[Histogram] | type[Gauge] | type[ObservableCounter] | type[ObservableUpDownCounter] | type[ObservableGauge] ) CardinalityLimits = dict[InstrumentCardinalityKey, int] _CARDINALITY_INSTRUMENT_CLASSES: dict[type, type] = { Counter: _Counter, UpDownCounter: _UpDownCounter, Histogram: _Histogram, Gauge: _Gauge, ObservableCounter: _ObservableCounter, ObservableUpDownCounter: _ObservableUpDownCounter, ObservableGauge: _ObservableGauge, } def _validate_cardinality_limit(typ: type, limit: int) -> type: """Return the SDK instrument class for ``typ`` after validating ``limit``.""" if typ not in _CARDINALITY_INSTRUMENT_CLASSES: raise ValueError(f"Invalid instrument class found {typ}") if isinstance(limit, bool) or not isinstance(limit, int) or limit < 1: raise ValueError(f"Cardinality limit for {typ} must be a positive integer, got {limit!r}") return _CARDINALITY_INSTRUMENT_CLASSES[typ] class MetricReader(ABC): # pylint: disable=too-many-branches,broad-exception-raised """ Base class for all metric readers Args: preferred_temporality: A mapping between instrument classes and aggregation temporality. By default uses CUMULATIVE for all instrument classes. This mapping will be used to define the default aggregation temporality of every instrument class. If the user wants to make a change in the default aggregation temporality of an instrument class, it is enough to pass here a dictionary whose keys are the instrument classes and the values are the corresponding desired aggregation temporalities of the classes that the user wants to change, not all of them. The classes not included in the passed dictionary will retain their association to their default aggregation temporalities. preferred_aggregation: A mapping between instrument classes and aggregation instances. By default maps all instrument classes to an instance of `DefaultAggregation`. This mapping will be used to define the default aggregation of every instrument class. If the user wants to make a change in the default aggregation of an instrument class, it is enough to pass here a dictionary whose keys are the instrument classes and the values are the corresponding desired aggregation for the instrument classes that the user wants to change, not necessarily all of them. The classes not included in the passed dictionary will retain their association to their default aggregations. The aggregation defined here will be overridden by an aggregation defined by a view that is not `DefaultAggregation`. cardinality_limits: A dictionary mapping instrument classes to the maximum number of distinct attribute sets a metric stream of that instrument class aggregates per collection cycle. Instrument classes not included use a limit of 2000. A limit defined by a view takes precedence. Measurements with further attribute sets are aggregated into one additional point identified by the attribute ``otel.metric.overflow=true``. .. document protected _receive_metrics which is a intended to be overridden by subclass .. automethod:: _receive_metrics """ def __init__( self, preferred_temporality: dict[type, AggregationTemporality] | None = None, preferred_aggregation: dict[type, opentelemetry.sdk.metrics.view.Aggregation] | None = None, cardinality_limits: CardinalityLimits | None = None, *, otel_component_type: OtelComponentTypeValues | None = None, ) -> None: self._collect: ( Callable[ [ opentelemetry.sdk.metrics.export.MetricReader, AggregationTemporality, ], Iterable[opentelemetry.sdk.metrics.export.Metric], ] | None ) = None self._instrument_class_temporality = { _Counter: AggregationTemporality.CUMULATIVE, _UpDownCounter: AggregationTemporality.CUMULATIVE, _Histogram: AggregationTemporality.CUMULATIVE, _Gauge: AggregationTemporality.CUMULATIVE, _ObservableCounter: AggregationTemporality.CUMULATIVE, _ObservableUpDownCounter: AggregationTemporality.CUMULATIVE, _ObservableGauge: AggregationTemporality.CUMULATIVE, } if preferred_temporality is not None: for temporality in preferred_temporality.values(): if temporality not in ( AggregationTemporality.CUMULATIVE, AggregationTemporality.DELTA, ): raise Exception(f"Invalid temporality value found {temporality}") if preferred_temporality is not None: for typ, temporality in preferred_temporality.items(): if typ is Counter: self._instrument_class_temporality[_Counter] = temporality elif typ is UpDownCounter: self._instrument_class_temporality[_UpDownCounter] = temporality elif typ is Histogram: self._instrument_class_temporality[_Histogram] = temporality elif typ is Gauge: self._instrument_class_temporality[_Gauge] = temporality elif typ is ObservableCounter: self._instrument_class_temporality[_ObservableCounter] = temporality elif typ is ObservableUpDownCounter: self._instrument_class_temporality[_ObservableUpDownCounter] = temporality elif typ is ObservableGauge: self._instrument_class_temporality[_ObservableGauge] = temporality else: raise Exception(f"Invalid instrument class found {typ}") self._preferred_temporality = preferred_temporality self._instrument_class_aggregation = { _Counter: DefaultAggregation(), _UpDownCounter: DefaultAggregation(), _Histogram: DefaultAggregation(), _Gauge: DefaultAggregation(), _ObservableCounter: DefaultAggregation(), _ObservableUpDownCounter: DefaultAggregation(), _ObservableGauge: DefaultAggregation(), } if preferred_aggregation is not None: for typ, aggregation in preferred_aggregation.items(): if typ is Counter: self._instrument_class_aggregation[_Counter] = aggregation elif typ is UpDownCounter: self._instrument_class_aggregation[_UpDownCounter] = aggregation elif typ is Histogram: self._instrument_class_aggregation[_Histogram] = aggregation elif typ is Gauge: self._instrument_class_aggregation[_Gauge] = aggregation elif typ is ObservableCounter: self._instrument_class_aggregation[_ObservableCounter] = aggregation elif typ is ObservableUpDownCounter: self._instrument_class_aggregation[_ObservableUpDownCounter] = aggregation elif typ is ObservableGauge: self._instrument_class_aggregation[_ObservableGauge] = aggregation else: raise Exception(f"Invalid instrument class found {typ}") self._instrument_class_cardinality_limit = { _Counter: _DEFAULT_CARDINALITY_LIMIT, _UpDownCounter: _DEFAULT_CARDINALITY_LIMIT, _Histogram: _DEFAULT_CARDINALITY_LIMIT, _Gauge: _DEFAULT_CARDINALITY_LIMIT, _ObservableCounter: _DEFAULT_CARDINALITY_LIMIT, _ObservableUpDownCounter: _DEFAULT_CARDINALITY_LIMIT, _ObservableGauge: _DEFAULT_CARDINALITY_LIMIT, } if cardinality_limits is not None: for typ, limit in cardinality_limits.items(): self._instrument_class_cardinality_limit[_validate_cardinality_limit(typ, limit)] = limit self._otel_component_type = otel_component_type.value if otel_component_type else type(self).__qualname__ self._metrics = create_metric_reader_metrics( self._otel_component_type, NoOpMeterProvider(), parse_boolean_environment_variable(OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED), ) @final def collect(self, timeout_millis: float = 10_000) -> None: """Collects the metrics from the internal SDK state and invokes the `_receive_metrics` with the collection. Args: timeout_millis: Amount of time in milliseconds before this function raises a timeout error. If any of the underlying ``collect`` methods called by this method fails by any reason (including timeout) an exception will be raised detailing the individual errors that caused this function to fail. """ if self._collect is None: _logger.warning("Cannot call collect on a MetricReader until it is registered on a MeterProvider") return start_time = perf_counter() try: metrics = self._collect(self, timeout_millis=timeout_millis) finally: self._metrics.record_collection(perf_counter() - start_time) if metrics is not None: self._receive_metrics( metrics, timeout_millis=timeout_millis, ) @final def _set_collect_callback( self, func: Callable[ [ opentelemetry.sdk.metrics.export.MetricReader, AggregationTemporality, ], MetricsData, ] | None, ) -> None: """This function is internal to the SDK. It should not be called or overridden by users""" self._collect = func @abstractmethod def _receive_metrics( self, metrics_data: MetricsData, timeout_millis: float = 10_000, **kwargs, ) -> None: """Called by `MetricReader.collect` when it receives a batch of metrics""" def _set_meter_provider(self, meter_provider: MeterProvider) -> None: self._metrics = create_metric_reader_metrics( self._otel_component_type, meter_provider, parse_boolean_environment_variable(OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED), ) def force_flush(self, timeout_millis: float = 10_000) -> bool: self.collect(timeout_millis=timeout_millis) return True @abstractmethod def shutdown(self, timeout_millis: float = 30_000, **kwargs) -> None: """Shuts down the MetricReader. This method provides a way for the MetricReader to do any cleanup required. A metric reader can only be shutdown once, any subsequent calls are ignored and return failure status. When a `MetricReader` is registered on a :class:`~opentelemetry.sdk.metrics.MeterProvider`, :meth:`~opentelemetry.sdk.metrics.MeterProvider.shutdown` will invoke this automatically. """ class InMemoryMetricReader(MetricReader): """Implementation of `MetricReader` that returns its metrics from :func:`get_metrics_data`. This is useful for e.g. unit tests. """ def __init__( self, preferred_temporality: dict[type, AggregationTemporality] | None = None, preferred_aggregation: dict[type, opentelemetry.sdk.metrics.view.Aggregation] | None = None, cardinality_limits: CardinalityLimits | None = None, ) -> None: super().__init__( preferred_temporality=preferred_temporality, preferred_aggregation=preferred_aggregation, cardinality_limits=cardinality_limits, ) self._lock = RLock() self._metrics_data: MetricsData | None = None def get_metrics_data( self, ) -> MetricsData | None: """Reads and returns current metrics from the SDK""" with self._lock: self.collect() metrics_data = self._metrics_data self._metrics_data = None return metrics_data def _receive_metrics( self, metrics_data: MetricsData, timeout_millis: float = 10_000, **kwargs, ) -> None: with self._lock: self._metrics_data = metrics_data def shutdown(self, timeout_millis: float = 30_000, **kwargs) -> None: pass class PeriodicExportingMetricReader(MetricReader): """`PeriodicExportingMetricReader` is an implementation of `MetricReader` that collects metrics based on a user-configurable time interval, and passes the metrics to the configured exporter. If the time interval is set to `math.inf`, the reader will not invoke periodic collection. The configured exporter's :py:meth:`~MetricExporter.export` method will not be called concurrently. """ def __init__( self, exporter: MetricExporter, export_interval_millis: float | None = None, export_timeout_millis: float | None = None, cardinality_limits: CardinalityLimits | None = None, ) -> None: # PeriodicExportingMetricReader defers to exporter for configuration super().__init__( preferred_temporality=exporter._preferred_temporality, preferred_aggregation=exporter._preferred_aggregation, cardinality_limits=cardinality_limits, otel_component_type=OtelComponentTypeValues.PERIODIC_METRIC_READER, ) # This lock is held whenever calling self._exporter.export() to prevent concurrent # execution of MetricExporter.export() # https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk.md#exportbatch self._export_lock = Lock() self._exporter = exporter if export_interval_millis is None: try: export_interval_millis = float(environ.get(OTEL_METRIC_EXPORT_INTERVAL, 60000)) except ValueError: _logger.warning("Found invalid value for export interval, using default") export_interval_millis = 60000 if export_timeout_millis is None: try: export_timeout_millis = float(environ.get(OTEL_METRIC_EXPORT_TIMEOUT, 30000)) except ValueError: _logger.warning("Found invalid value for export timeout, using default") export_timeout_millis = 30000 self._export_interval_millis = export_interval_millis self._export_timeout_millis = export_timeout_millis self._shutdown = False self._shutdown_event = Event() self._shutdown_once = Once() self._daemon_thread = None if self._export_interval_millis > 0 and self._export_interval_millis < math.inf: self._daemon_thread = Thread( name="OtelPeriodicExportingMetricReader", target=self._ticker, daemon=True, ) self._daemon_thread.start() if hasattr(os, "register_at_fork"): weak_at_fork = weakref.WeakMethod(self._at_fork_reinit) def _after_in_child() -> None: if at_fork := weak_at_fork(): at_fork() os.register_at_fork(after_in_child=_after_in_child) elif self._export_interval_millis <= 0: raise ValueError( f"interval value {self._export_interval_millis} is invalid \ and needs to be larger than zero." ) def _at_fork_reinit(self): self._daemon_thread = Thread( name="OtelPeriodicExportingMetricReader", target=self._ticker, daemon=True, ) self._daemon_thread.start() def _ticker(self) -> None: interval_secs = self._export_interval_millis / 1e3 while not self._shutdown_event.wait(interval_secs): try: self.collect(timeout_millis=self._export_timeout_millis) except MetricsTimeoutError: _logger.warning( "Metric collection timed out. Will try again after %s seconds", interval_secs, exc_info=True, ) # one last collection below before shutting down completely try: self.collect(timeout_millis=self._export_interval_millis) except MetricsTimeoutError: _logger.warning( "Metric collection timed out.", exc_info=True, ) def _receive_metrics( self, metrics_data: MetricsData, timeout_millis: float = 10_000, **kwargs, ) -> None: token = attach(set_value(_SUPPRESS_INSTRUMENTATION_KEY, True)) # pylint: disable=broad-exception-caught,invalid-name try: with self._export_lock: self._exporter.export(metrics_data, timeout_millis=timeout_millis) except Exception: _logger.exception("Exception while exporting metrics") detach(token) def shutdown(self, timeout_millis: float = 30_000, **kwargs) -> None: deadline_ns = time_ns() + timeout_millis * 10**6 def _shutdown(): self._shutdown = True did_set = self._shutdown_once.do_once(_shutdown) if not did_set: _logger.warning("Can't shutdown multiple times") return self._shutdown_event.set() if self._daemon_thread: self._daemon_thread.join(timeout=(deadline_ns - time_ns()) / 10**9) self._exporter.shutdown(timeout=(deadline_ns - time_ns()) / 10**6) def force_flush(self, timeout_millis: float = 10_000) -> bool: super().force_flush(timeout_millis=timeout_millis) self._exporter.force_flush(timeout_millis=timeout_millis) return True _metric_reader_metrics.py000066400000000000000000000031071527070070700376720ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/export# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from collections import Counter from typing import Protocol from opentelemetry.metrics import MeterProvider from opentelemetry.semconv._incubating.attributes.otel_attributes import ( OTEL_COMPONENT_NAME, OTEL_COMPONENT_TYPE, ) from opentelemetry.semconv._incubating.metrics.otel_metrics import ( create_otel_sdk_metric_reader_collection_duration, ) _component_counter = Counter() class MetricReaderMetricsT(Protocol): def record_collection(self, duration: float) -> None: ... class NoOpMetricReaderMetrics: def record_collection(self, duration: float) -> None: pass class MetricReaderMetrics: def __init__(self, component_type: str, meter_provider: MeterProvider) -> None: meter = meter_provider.get_meter("opentelemetry-sdk") count = _component_counter[component_type] _component_counter[component_type] = count + 1 self._standard_attrs = { OTEL_COMPONENT_TYPE: component_type, OTEL_COMPONENT_NAME: f"{component_type}/{count}", } self._collection_duration = create_otel_sdk_metric_reader_collection_duration(meter) def record_collection(self, duration: float) -> None: self._collection_duration.record(duration, self._standard_attrs) def create_metric_reader_metrics( component_type: str, meter_provider: MeterProvider, enabled: bool, ) -> MetricReaderMetricsT: if not enabled: return NoOpMetricReaderMetrics() return MetricReaderMetrics(component_type, meter_provider) python-opentelemetry-1.45.2/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/instrument.py000066400000000000000000000307151527070070700341530ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # pylint: disable=too-many-ancestors, unused-import from __future__ import annotations import math from collections.abc import Generator, Iterable, Sequence from logging import getLogger from time import time_ns from typing import ( TYPE_CHECKING, Protocol, cast, runtime_checkable, ) # This kind of import is needed to avoid Sphinx errors. from opentelemetry.context import Context, get_current from opentelemetry.metrics import Asynchronous, CallbackT, Synchronous from opentelemetry.metrics import Counter as APICounter from opentelemetry.metrics import Histogram as APIHistogram from opentelemetry.metrics import ObservableCounter as APIObservableCounter from opentelemetry.metrics import ObservableGauge as APIObservableGauge from opentelemetry.metrics import ( ObservableUpDownCounter as APIObservableUpDownCounter, ) from opentelemetry.metrics import UpDownCounter as APIUpDownCounter from opentelemetry.metrics import _Gauge as APIGauge from opentelemetry.metrics._internal.instrument import ( CallbackOptions, _MetricsHistogramAdvisory, ) from opentelemetry.sdk.metrics._internal.measurement import Measurement from opentelemetry.util.types import Attributes if TYPE_CHECKING: from opentelemetry.sdk.metrics._internal import ( _ProxyMeterConfig, ) from opentelemetry.sdk.metrics._internal.measurement_consumer import ( MeasurementConsumer, ) from opentelemetry.sdk.util.instrumentation import InstrumentationScope _logger = getLogger(__name__) _NAME_ERROR_MESSAGE = "Expected ASCII string of maximum length 255 characters but got {}" _UNIT_ERROR_MESSAGE = "Expected ASCII string of maximum length 63 characters but got {}" @runtime_checkable class _Instrument(Protocol): name: str unit: str description: str instrumentation_scope: InstrumentationScope class _Synchronous(_Instrument, Synchronous): def __init__( self, name: str, instrumentation_scope: InstrumentationScope, measurement_consumer: MeasurementConsumer, unit: str = "", description: str = "", *, _meter_config: _ProxyMeterConfig | None = None, ): # pylint: disable=no-member result = self._check_name_unit_description(name, unit, description) if result["name"] is None: # pylint: disable=broad-exception-raised raise Exception(_NAME_ERROR_MESSAGE.format(name)) if result["unit"] is None: # pylint: disable=broad-exception-raised raise Exception(_UNIT_ERROR_MESSAGE.format(unit)) name = result["name"] unit = result["unit"] description = cast(str, result["description"]) self.name = name.lower() self.unit = unit self.description = description self.instrumentation_scope = instrumentation_scope self._measurement_consumer = measurement_consumer self._meter_config = _meter_config super().__init__(name, unit=unit, description=description) def _is_enabled(self) -> bool: return self._meter_config is None or self._meter_config.is_enabled class _Asynchronous(_Instrument, Asynchronous): def __init__( self, name: str, instrumentation_scope: InstrumentationScope, measurement_consumer: MeasurementConsumer, callbacks: Sequence[CallbackT] | None = None, unit: str = "", description: str = "", *, _meter_config: _ProxyMeterConfig | None = None, ): # pylint: disable=no-member result = self._check_name_unit_description(name, unit, description) if result["name"] is None: # pylint: disable=broad-exception-raised raise Exception(_NAME_ERROR_MESSAGE.format(name)) if result["unit"] is None: # pylint: disable=broad-exception-raised raise Exception(_UNIT_ERROR_MESSAGE.format(unit)) name = result["name"] unit = result["unit"] description = cast(str, result["description"]) self.name = name.lower() self.unit = unit self.description = description self.instrumentation_scope = instrumentation_scope self._measurement_consumer = measurement_consumer self._meter_config = _meter_config super().__init__(name, callbacks, unit=unit, description=description) self._callbacks: list[CallbackT] = [] if callbacks is not None: for callback in callbacks: if isinstance(callback, Generator): # advance generator to it's first yield next(callback) def inner( options: CallbackOptions, callback=callback, ) -> Iterable[Measurement]: try: return callback.send(options) except StopIteration: return [] self._callbacks.append(inner) else: self._callbacks.append(callback) def _is_enabled(self) -> bool: return self._meter_config is None or self._meter_config.is_enabled def callback(self, callback_options: CallbackOptions) -> Iterable[Measurement]: if not self._is_enabled(): return for callback in self._callbacks: try: for api_measurement in callback(callback_options): if not math.isfinite(api_measurement.value): _logger.warning( "Callback returned a non-finite value %s for instrument %s, ignoring measurement.", api_measurement.value, self.name, ) continue yield Measurement( api_measurement.value, time_unix_nano=time_ns(), instrument=self, context=api_measurement.context or get_current(), attributes=api_measurement.attributes, ) except Exception: # pylint: disable=broad-exception-caught _logger.exception("Callback failed for instrument %s.", self.name) class Counter(_Synchronous, APICounter): def __new__(cls, *args, **kwargs): if cls is Counter: raise TypeError("Counter must be instantiated via a meter.") return super().__new__(cls) def add( self, amount: float, attributes: Attributes = None, context: Context | None = None, ): if not self._is_enabled(): super().add(amount, attributes=attributes, context=context) return if not math.isfinite(amount): _logger.warning( "Add amount %s is not finite on Counter %s, ignoring measurement.", amount, self.name, ) return if amount < 0: _logger.warning("Add amount must be non-negative on Counter %s.", self.name) return time_unix_nano = time_ns() self._measurement_consumer.consume_measurement( Measurement( amount, time_unix_nano, self, context or get_current(), attributes, ) ) class UpDownCounter(_Synchronous, APIUpDownCounter): def __new__(cls, *args, **kwargs): if cls is UpDownCounter: raise TypeError("UpDownCounter must be instantiated via a meter.") return super().__new__(cls) def add( self, amount: float, attributes: Attributes = None, context: Context | None = None, ): if not self._is_enabled(): super().add(amount, attributes=attributes, context=context) return if not math.isfinite(amount): _logger.warning( "Add amount %s is not finite on UpDownCounter %s, ignoring measurement.", amount, self.name, ) return time_unix_nano = time_ns() self._measurement_consumer.consume_measurement( Measurement( amount, time_unix_nano, self, context or get_current(), attributes, ) ) class ObservableCounter(_Asynchronous, APIObservableCounter): def __new__(cls, *args, **kwargs): if cls is ObservableCounter: raise TypeError("ObservableCounter must be instantiated via a meter.") return super().__new__(cls) class ObservableUpDownCounter(_Asynchronous, APIObservableUpDownCounter): def __new__(cls, *args, **kwargs): if cls is ObservableUpDownCounter: raise TypeError("ObservableUpDownCounter must be instantiated via a meter.") return super().__new__(cls) class Histogram(_Synchronous, APIHistogram): def __init__( self, name: str, instrumentation_scope: InstrumentationScope, measurement_consumer: MeasurementConsumer, unit: str = "", description: str = "", explicit_bucket_boundaries_advisory: Sequence[float] | None = None, *, _meter_config: _ProxyMeterConfig | None = None, ): super().__init__( name, unit=unit, description=description, instrumentation_scope=instrumentation_scope, measurement_consumer=measurement_consumer, _meter_config=_meter_config, ) self._advisory = _MetricsHistogramAdvisory(explicit_bucket_boundaries=explicit_bucket_boundaries_advisory) def __new__(cls, *args, **kwargs): if cls is Histogram: raise TypeError("Histogram must be instantiated via a meter.") return super().__new__(cls) def record( self, amount: float, attributes: Attributes = None, context: Context | None = None, ): if not self._is_enabled(): super().record(amount, attributes=attributes, context=context) return if not math.isfinite(amount): _logger.warning( "Record amount %s is not finite on Histogram %s, ignoring measurement.", amount, self.name, ) return if amount < 0: _logger.warning( "Record amount must be non-negative on Histogram %s.", self.name, ) return time_unix_nano = time_ns() self._measurement_consumer.consume_measurement( Measurement( amount, time_unix_nano, self, context or get_current(), attributes, ) ) class Gauge(_Synchronous, APIGauge): def __new__(cls, *args, **kwargs): if cls is Gauge: raise TypeError("Gauge must be instantiated via a meter.") return super().__new__(cls) def set( self, amount: float, attributes: Attributes = None, context: Context | None = None, ): if not self._is_enabled(): super().set(amount, attributes=attributes, context=context) return if not math.isfinite(amount): _logger.warning( "Set amount %s is not finite on Gauge %s, ignoring measurement.", amount, self.name, ) return time_unix_nano = time_ns() self._measurement_consumer.consume_measurement( Measurement( amount, time_unix_nano, self, context or get_current(), attributes, ) ) class ObservableGauge(_Asynchronous, APIObservableGauge): def __new__(cls, *args, **kwargs): if cls is ObservableGauge: raise TypeError("ObservableGauge must be instantiated via a meter.") return super().__new__(cls) # Below classes exist to prevent the direct instantiation class _Counter(Counter): pass class _UpDownCounter(UpDownCounter): pass class _ObservableCounter(ObservableCounter): pass class _ObservableUpDownCounter(ObservableUpDownCounter): pass class _Histogram(Histogram): pass class _Gauge(Gauge): pass class _ObservableGauge(ObservableGauge): pass python-opentelemetry-1.45.2/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/measurement.py000066400000000000000000000034101527070070700342600ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations from collections.abc import Mapping from dataclasses import dataclass from logging import getLogger from typing import TYPE_CHECKING from opentelemetry.attributes import _clean_attribute_value from opentelemetry.context import Context from opentelemetry.util.types import Attributes _logger = getLogger(__name__) if TYPE_CHECKING: from opentelemetry.sdk.metrics._internal.instrument import _Instrument @dataclass(frozen=True) class Measurement: """ Represents a data point reported via the metrics API to the SDK. Attributes: value: Measured value time_unix_nano: The time the API call was made to record the Measurement instrument: The instrument that produced this `Measurement`. context: The active Context of the Measurement at API call time. attributes: Measurement attributes. Mutating mutable attribute values (lists or dicts) outside of the Measurement object can lead to undefined and bad behavior. """ value: int | float time_unix_nano: int instrument: _Instrument context: Context attributes: Attributes = None def __post_init__(self) -> None: if self.attributes is not None: if isinstance(self.attributes, Mapping): object.__setattr__( self, "attributes", _clean_attribute_value(self.attributes, None), ) else: _logger.warning( "Invalid type '%s' for attributes. Expected a Mapping or None.", type(self.attributes), ) object.__setattr__(self, "attributes", None) measurement_consumer.py000066400000000000000000000133471527070070700361260ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # pylint: disable=unused-import from abc import ABC, abstractmethod from collections.abc import Iterable, Mapping from threading import Lock from time import time_ns # This kind of import is needed to avoid Sphinx errors. import opentelemetry.sdk.metrics import opentelemetry.sdk.metrics._internal.instrument from opentelemetry.metrics._internal.instrument import CallbackOptions from opentelemetry.sdk.metrics._internal.exceptions import MetricsTimeoutError from opentelemetry.sdk.metrics._internal.measurement import Measurement from opentelemetry.sdk.metrics._internal.metric_reader_storage import ( MetricReaderStorage, ) from opentelemetry.sdk.metrics._internal.point import MetricsData class MeasurementConsumer(ABC): @abstractmethod def consume_measurement(self, measurement: Measurement) -> None: pass @abstractmethod def register_asynchronous_instrument( self, instrument: ("opentelemetry.sdk.metrics._internal.instrument._Asynchronous"), ): pass @abstractmethod def collect( self, metric_reader: "opentelemetry.sdk.metrics.export.MetricReader", timeout_millis: float = 10_000, ) -> MetricsData | None: pass class SynchronousMeasurementConsumer(MeasurementConsumer): def __init__( self, sdk_config: "opentelemetry.sdk.metrics._internal.sdk_configuration.SdkConfiguration", metric_readers: Iterable["opentelemetry.sdk.metrics.MetricReader"], ) -> None: self._lock = Lock() self._sdk_config = sdk_config self._reader_storages: Mapping[opentelemetry.sdk.metrics.export.MetricReader, MetricReaderStorage] = { reader: MetricReaderStorage( sdk_config, reader._instrument_class_temporality, reader._instrument_class_aggregation, reader._instrument_class_cardinality_limit, ) for reader in metric_readers } self._async_instruments: list[opentelemetry.sdk.metrics._internal.instrument._Asynchronous] = [] def consume_measurement(self, measurement: Measurement) -> None: should_sample_exemplar = self._sdk_config.exemplar_filter.should_sample( measurement.value, measurement.time_unix_nano, measurement.attributes, measurement.context, ) # `_reader_storages` is replaced (never mutated in place) by # `add_metric_reader` and `remove_metric_reader`, so it is safe # to iterate over without a lock. for reader_storage in self._reader_storages.values(): reader_storage.consume_measurement(measurement, should_sample_exemplar) def register_asynchronous_instrument( self, instrument: ("opentelemetry.sdk.metrics._internal.instrument._Asynchronous"), ) -> None: with self._lock: self._async_instruments.append(instrument) def collect( self, metric_reader: "opentelemetry.sdk.metrics.export.MetricReader", timeout_millis: float = 10_000, ) -> MetricsData | None: with self._lock: metric_reader_storage = self._reader_storages[metric_reader] # for now, just use the defaults callback_options = CallbackOptions() deadline_ns = time_ns() + (timeout_millis * 1e6) default_timeout_ns = 10000 * 1e6 for async_instrument in self._async_instruments: remaining_time = deadline_ns - time_ns() if remaining_time < default_timeout_ns: callback_options = CallbackOptions(timeout_millis=remaining_time / 1e6) measurements = async_instrument.callback(callback_options) if time_ns() >= deadline_ns: raise MetricsTimeoutError("Timed out while executing callback") for measurement in measurements: should_sample_exemplar = self._sdk_config.exemplar_filter.should_sample( measurement.value, measurement.time_unix_nano, measurement.attributes, measurement.context, ) metric_reader_storage.consume_measurement(measurement, should_sample_exemplar) result = self._reader_storages[metric_reader].collect() return result def add_metric_reader(self, metric_reader: "opentelemetry.sdk.metrics.MetricReader") -> None: """Registers a new metric reader.""" # Build a new mapping and swap it in atomically so that # a concurrent consume_measurement never iterates a mapping # that is being mutated in place. with self._lock: new_reader_storages = dict(self._reader_storages) new_reader_storages[metric_reader] = MetricReaderStorage( self._sdk_config, # pylint: disable-next=protected-access metric_reader._instrument_class_temporality, # pylint: disable-next=protected-access metric_reader._instrument_class_aggregation, # pylint: disable-next=protected-access metric_reader._instrument_class_cardinality_limit, ) self._reader_storages = new_reader_storages def remove_metric_reader(self, metric_reader: "opentelemetry.sdk.metrics.MetricReader") -> None: """Unregisters the given metric reader.""" # Mutate using copy-on-write: see add_metric_reader. with self._lock: new_reader_storages = dict(self._reader_storages) new_reader_storages.pop(metric_reader) self._reader_storages = new_reader_storages metric_reader_storage.py000066400000000000000000000271601527070070700362150ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from logging import getLogger from threading import RLock from time import time_ns from opentelemetry.metrics import ( Asynchronous, Counter, ObservableCounter, ) from opentelemetry.sdk.metrics._internal._view_instrument_match import ( _DEFAULT_CARDINALITY_LIMIT, _ViewInstrumentMatch, ) from opentelemetry.sdk.metrics._internal.aggregation import ( Aggregation, AggregationTemporality, ExplicitBucketHistogramAggregation, ExponentialBucketHistogramAggregation, _DropAggregation, _ExplicitBucketHistogramAggregation, _ExponentialBucketHistogramAggregation, _LastValueAggregation, _SumAggregation, ) from opentelemetry.sdk.metrics._internal.instrument import ( _Asynchronous, _Instrument, ) from opentelemetry.sdk.metrics._internal.measurement import Measurement from opentelemetry.sdk.metrics._internal.point import ( ExponentialHistogram, Gauge, Histogram, Metric, MetricsData, ResourceMetrics, ScopeMetrics, Sum, ) from opentelemetry.sdk.metrics._internal.sdk_configuration import ( SdkConfiguration, ) from opentelemetry.sdk.metrics._internal.view import View from opentelemetry.sdk.util.instrumentation import InstrumentationScope _logger = getLogger(__name__) _DEFAULT_VIEW = View(instrument_name="") class MetricReaderStorage: """The SDK's storage for a given reader""" def __init__( self, sdk_config: SdkConfiguration, instrument_class_temporality: dict[type, AggregationTemporality], instrument_class_aggregation: dict[type, Aggregation], instrument_class_cardinality_limit: dict[type, int] | None = None, ) -> None: self._lock = RLock() self._sdk_config = sdk_config self._instrument_view_instrument_matches: dict[_Instrument, list[_ViewInstrumentMatch]] = {} self._instrument_class_temporality = instrument_class_temporality self._instrument_class_aggregation = instrument_class_aggregation self._instrument_class_cardinality_limit = instrument_class_cardinality_limit or {} def _new_view_instrument_match(self, view: View, instrument: _Instrument) -> _ViewInstrumentMatch: # pylint: disable=protected-access cardinality_limit = view._aggregation_cardinality_limit if cardinality_limit is None: cardinality_limit = self._instrument_class_cardinality_limit.get( instrument.__class__, _DEFAULT_CARDINALITY_LIMIT ) # Idle attribute sets of synchronous instruments are only released on # delta collection, so that the limit applies per collection cycle. # Asynchronous instruments keep their state, which delta conversion # relies on. reset_on_collect = self._instrument_class_temporality[ instrument.__class__ ] is AggregationTemporality.DELTA and not isinstance(instrument, _Asynchronous) return _ViewInstrumentMatch( view=view, instrument=instrument, instrument_class_aggregation=self._instrument_class_aggregation, cardinality_limit=cardinality_limit, reset_on_collect=reset_on_collect, ) def _get_or_init_view_instrument_match(self, instrument: _Instrument) -> list[_ViewInstrumentMatch]: # Optimistically get the relevant views for the given instrument. Once set for a given # instrument, the mapping will never change if instrument in self._instrument_view_instrument_matches: return self._instrument_view_instrument_matches[instrument] with self._lock: # double check if it was set before we held the lock if instrument in self._instrument_view_instrument_matches: return self._instrument_view_instrument_matches[instrument] # not present, hold the lock and add a new mapping view_instrument_matches = [] self._handle_view_instrument_match(instrument, view_instrument_matches) # if no view targeted the instrument, use the default if not view_instrument_matches: view_instrument_matches.append(self._new_view_instrument_match(_DEFAULT_VIEW, instrument)) self._instrument_view_instrument_matches[instrument] = view_instrument_matches return view_instrument_matches def consume_measurement(self, measurement: Measurement, should_sample_exemplar: bool = True) -> None: for view_instrument_match in self._get_or_init_view_instrument_match(measurement.instrument): view_instrument_match.consume_measurement(measurement, should_sample_exemplar) def collect(self) -> MetricsData | None: # Use a list instead of yielding to prevent a slow reader from holding # SDK locks # While holding the lock, new _ViewInstrumentMatch can't be added from # another thread (so we are sure we collect all existing view). # However, instruments can still send measurements that will make it # into the individual aggregations; collection will acquire those locks # iteratively to keep locking as fine-grained as possible. One side # effect is that end times can be slightly skewed among the metric # streams produced by the SDK, but we still align the output timestamps # for a single instrument. collection_start_nanos = time_ns() with self._lock: instrumentation_scope_scope_metrics: dict[InstrumentationScope, ScopeMetrics] = {} instrument_matches_snapshot = list(self._instrument_view_instrument_matches.items()) for ( instrument, view_instrument_matches, ) in instrument_matches_snapshot: aggregation_temporality = self._instrument_class_temporality[instrument.__class__] metrics: list[Metric] = [] for view_instrument_match in view_instrument_matches: data_points = view_instrument_match.collect(aggregation_temporality, collection_start_nanos) if data_points is None: continue if isinstance( # pylint: disable=protected-access view_instrument_match._aggregation, _SumAggregation, ): data = Sum( aggregation_temporality=aggregation_temporality, data_points=data_points, is_monotonic=isinstance(instrument, (Counter, ObservableCounter)), ) elif isinstance( # pylint: disable=protected-access view_instrument_match._aggregation, _LastValueAggregation, ): data = Gauge(data_points=data_points) elif isinstance( # pylint: disable=protected-access view_instrument_match._aggregation, _ExplicitBucketHistogramAggregation, ): data = Histogram( data_points=data_points, aggregation_temporality=aggregation_temporality, ) elif isinstance( # pylint: disable=protected-access view_instrument_match._aggregation, _DropAggregation, ): continue elif isinstance( # pylint: disable=protected-access view_instrument_match._aggregation, _ExponentialBucketHistogramAggregation, ): data = ExponentialHistogram( data_points=data_points, aggregation_temporality=aggregation_temporality, ) metrics.append( Metric( # pylint: disable=protected-access # pylint: disable=possibly-used-before-assignment name=view_instrument_match._name, description=view_instrument_match._description, unit=view_instrument_match._instrument.unit, data=data, ) ) if metrics: if instrument.instrumentation_scope not in (instrumentation_scope_scope_metrics): instrumentation_scope_scope_metrics[instrument.instrumentation_scope] = ScopeMetrics( scope=instrument.instrumentation_scope, metrics=metrics, schema_url=instrument.instrumentation_scope.schema_url, ) else: instrumentation_scope_scope_metrics[instrument.instrumentation_scope].metrics.extend(metrics) if instrumentation_scope_scope_metrics: return MetricsData( resource_metrics=[ ResourceMetrics( resource=self._sdk_config.resource, scope_metrics=list(instrumentation_scope_scope_metrics.values()), schema_url=self._sdk_config.resource.schema_url, ) ] ) return None def _handle_view_instrument_match( self, instrument: _Instrument, view_instrument_matches: list["_ViewInstrumentMatch"], ) -> None: for view in self._sdk_config.views: # pylint: disable=protected-access if not view._match(instrument): continue if not self._check_view_instrument_compatibility(view, instrument): continue new_view_instrument_match = self._new_view_instrument_match(view, instrument) for existing_view_instrument_matches in self._instrument_view_instrument_matches.values(): for existing_view_instrument_match in existing_view_instrument_matches: if existing_view_instrument_match.conflicts(new_view_instrument_match): _logger.warning( "Views %s and %s will cause conflicting metrics identities", existing_view_instrument_match._view, new_view_instrument_match._view, ) view_instrument_matches.append(new_view_instrument_match) @staticmethod def _check_view_instrument_compatibility(view: View, instrument: _Instrument) -> bool: """ Checks if a view and an instrument are compatible. Returns `true` if they are compatible and a `_ViewInstrumentMatch` object should be created, `false` otherwise. """ result = True # pylint: disable=protected-access if isinstance(instrument, Asynchronous) and isinstance( view._aggregation, ( ExplicitBucketHistogramAggregation, ExponentialBucketHistogramAggregation, ), ): _logger.warning( "View %s and instrument %s will produce semantic errors when matched, the view has not been applied.", view, instrument, ) result = False return result python-opentelemetry-1.45.2/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/point.py000066400000000000000000000156701527070070700330770ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # pylint: disable=unused-import from collections.abc import Sequence from dataclasses import asdict, dataclass, field from json import dumps, loads # This kind of import is needed to avoid Sphinx errors. import opentelemetry.sdk.metrics._internal from opentelemetry.sdk.metrics._internal.exemplar import Exemplar from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.util.instrumentation import InstrumentationScope from opentelemetry.util.types import Attributes @dataclass(frozen=True) class NumberDataPoint: """Single data point in a timeseries that describes the time-varying scalar value of a metric. """ attributes: Attributes start_time_unix_nano: int time_unix_nano: int value: int | float exemplars: Sequence[Exemplar] = field(default_factory=list) def to_json(self, indent: int | None = 4) -> str: return dumps(asdict(self), indent=indent) @dataclass(frozen=True) class HistogramDataPoint: """Single data point in a timeseries that describes the time-varying scalar value of a metric. """ attributes: Attributes start_time_unix_nano: int time_unix_nano: int count: int sum: int | float bucket_counts: Sequence[int] explicit_bounds: Sequence[float] min: float max: float exemplars: Sequence[Exemplar] = field(default_factory=list) def to_json(self, indent: int | None = 4) -> str: return dumps(asdict(self), indent=indent) @dataclass(frozen=True) class Buckets: offset: int bucket_counts: Sequence[int] @dataclass(frozen=True) class ExponentialHistogramDataPoint: """Single data point in a timeseries whose boundaries are defined by an exponential function. This timeseries describes the time-varying scalar value of a metric. """ attributes: Attributes start_time_unix_nano: int time_unix_nano: int count: int sum: int | float scale: int zero_count: int positive: Buckets negative: Buckets flags: int min: float max: float exemplars: Sequence[Exemplar] = field(default_factory=list) def to_json(self, indent: int | None = 4) -> str: return dumps(asdict(self), indent=indent) @dataclass(frozen=True) class ExponentialHistogram: """Represents the type of a metric that is calculated by aggregating as an ExponentialHistogram of all reported measurements over a time interval. """ data_points: Sequence[ExponentialHistogramDataPoint] aggregation_temporality: "opentelemetry.sdk.metrics.export.AggregationTemporality" def to_json(self, indent: int | None = 4) -> str: return dumps( { "data_points": [loads(data_point.to_json(indent=indent)) for data_point in self.data_points], "aggregation_temporality": self.aggregation_temporality, }, indent=indent, ) @dataclass(frozen=True) class Sum: """Represents the type of a scalar metric that is calculated as a sum of all reported measurements over a time interval.""" data_points: Sequence[NumberDataPoint] aggregation_temporality: "opentelemetry.sdk.metrics.export.AggregationTemporality" is_monotonic: bool def to_json(self, indent: int | None = 4) -> str: return dumps( { "data_points": [loads(data_point.to_json(indent=indent)) for data_point in self.data_points], "aggregation_temporality": self.aggregation_temporality, "is_monotonic": self.is_monotonic, }, indent=indent, ) @dataclass(frozen=True) class Gauge: """Represents the type of a scalar metric that always exports the current value for every data point. It should be used for an unknown aggregation.""" data_points: Sequence[NumberDataPoint] def to_json(self, indent: int | None = 4) -> str: return dumps( { "data_points": [loads(data_point.to_json(indent=indent)) for data_point in self.data_points], }, indent=indent, ) @dataclass(frozen=True) class Histogram: """Represents the type of a metric that is calculated by aggregating as a histogram of all reported measurements over a time interval.""" data_points: Sequence[HistogramDataPoint] aggregation_temporality: "opentelemetry.sdk.metrics.export.AggregationTemporality" def to_json(self, indent: int | None = 4) -> str: return dumps( { "data_points": [loads(data_point.to_json(indent=indent)) for data_point in self.data_points], "aggregation_temporality": self.aggregation_temporality, }, indent=indent, ) # pylint: disable=invalid-name DataT = Sum | Gauge | Histogram | ExponentialHistogram DataPointT = NumberDataPoint | HistogramDataPoint | ExponentialHistogramDataPoint @dataclass(frozen=True) class Metric: """Represents a metric point in the OpenTelemetry data model to be exported.""" name: str description: str | None unit: str | None data: DataT def to_json(self, indent: int | None = 4) -> str: return dumps( { "name": self.name, "description": self.description or "", "unit": self.unit or "", "data": loads(self.data.to_json(indent=indent)), }, indent=indent, ) @dataclass(frozen=True) class ScopeMetrics: """A collection of Metrics produced by a scope""" scope: InstrumentationScope metrics: Sequence[Metric] schema_url: str def to_json(self, indent: int | None = 4) -> str: return dumps( { "scope": loads(self.scope.to_json(indent=indent)), "metrics": [loads(metric.to_json(indent=indent)) for metric in self.metrics], "schema_url": self.schema_url, }, indent=indent, ) @dataclass(frozen=True) class ResourceMetrics: """A collection of ScopeMetrics from a Resource""" resource: Resource scope_metrics: Sequence[ScopeMetrics] schema_url: str def to_json(self, indent: int | None = 4) -> str: return dumps( { "resource": loads(self.resource.to_json(indent=indent)), "scope_metrics": [loads(scope_metrics.to_json(indent=indent)) for scope_metrics in self.scope_metrics], "schema_url": self.schema_url, }, indent=indent, ) @dataclass(frozen=True) class MetricsData: """An array of ResourceMetrics""" resource_metrics: Sequence[ResourceMetrics] def to_json(self, indent: int | None = 4) -> str: return dumps( { "resource_metrics": [ loads(resource_metrics.to_json(indent=indent)) for resource_metrics in self.resource_metrics ] }, indent=indent, ) sdk_configuration.py000066400000000000000000000010071527070070700353640ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # pylint: disable=unused-import from collections.abc import Sequence from dataclasses import dataclass # This kind of import is needed to avoid Sphinx errors. import opentelemetry.sdk.metrics import opentelemetry.sdk.resources @dataclass class SdkConfiguration: exemplar_filter: "opentelemetry.sdk.metrics.ExemplarFilter" resource: "opentelemetry.sdk.resources.Resource" views: Sequence["opentelemetry.sdk.metrics.view.View"] python-opentelemetry-1.45.2/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/view.py000066400000000000000000000202031527070070700327040ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from collections.abc import Callable from fnmatch import fnmatchcase from logging import getLogger from opentelemetry.metrics import Instrument from opentelemetry.sdk.metrics._internal.aggregation import ( Aggregation, DefaultAggregation, _Aggregation, _ExplicitBucketHistogramAggregation, _ExponentialBucketHistogramAggregation, ) from opentelemetry.sdk.metrics._internal.exemplar import ( AlignedHistogramBucketExemplarReservoir, ExemplarReservoirBuilder, SimpleFixedSizeExemplarReservoir, ) from opentelemetry.sdk.metrics._internal.instrument import _Instrument _logger = getLogger(__name__) def _default_reservoir_factory( aggregation_type: type[_Aggregation], ) -> ExemplarReservoirBuilder: """Default reservoir factory per aggregation.""" if issubclass(aggregation_type, _ExplicitBucketHistogramAggregation): return AlignedHistogramBucketExemplarReservoir if issubclass(aggregation_type, _ExponentialBucketHistogramAggregation): return SimpleFixedSizeExemplarReservoir return SimpleFixedSizeExemplarReservoir class View: """ A `View` configuration parameters can be used for the following purposes: 1. Match instruments: When an instrument matches a view, measurements received by that instrument will be processed. 2. Customize metric streams: A metric stream is identified by a match between a view and an instrument and a set of attributes. The metric stream can be customized by certain attributes of the corresponding view. The attributes documented next serve one of the previous two purposes. Args: instrument_type: This is an instrument matching attribute: the class the instrument must be to match the view. instrument_name: This is an instrument matching attribute: the name the instrument must have to match the view. Wild card characters are supported. Wild card characters should not be used with this attribute if the view has also a ``name`` defined. meter_name: This is an instrument matching attribute: the name the instrument meter must have to match the view. meter_version: This is an instrument matching attribute: the version the instrument meter must have to match the view. meter_schema_url: This is an instrument matching attribute: the schema URL the instrument meter must have to match the view. name: This is a metric stream customizing attribute: the name of the metric stream. If `None`, the name of the instrument will be used. description: This is a metric stream customizing attribute: the description of the metric stream. If `None`, the description of the instrument will be used. attribute_keys: This is a metric stream customizing attribute: this is a set of attribute keys. If not `None` then only the measurement attributes that are in ``attribute_keys`` will be used to identify the metric stream. aggregation: This is a metric stream customizing attribute: the aggregation instance to use when data is aggregated for the corresponding metrics stream. If `None` an instance of `DefaultAggregation` will be used. exemplar_reservoir_factory: This is a metric stream customizing attribute: the exemplar reservoir factory instrument_unit: This is an instrument matching attribute: the unit the instrument must have to match the view. aggregation_cardinality_limit: This is a metric stream customizing attribute: the maximum number of distinct attribute sets the stream aggregates per collection cycle. Measurements with further attribute sets are aggregated into one additional point identified by the attribute ``otel.metric.overflow=true``. If `None` the cardinality limit of the metric reader is used, which defaults to 2000. Must be a positive integer. This class is not intended to be subclassed by the user. """ _default_aggregation = DefaultAggregation() def __init__( self, instrument_type: type[Instrument] | None = None, instrument_name: str | None = None, meter_name: str | None = None, meter_version: str | None = None, meter_schema_url: str | None = None, name: str | None = None, description: str | None = None, attribute_keys: set[str] | None = None, aggregation: Aggregation | None = None, exemplar_reservoir_factory: Callable[[type[_Aggregation]], ExemplarReservoirBuilder] | None = None, instrument_unit: str | None = None, aggregation_cardinality_limit: int | None = None, ): if aggregation_cardinality_limit is not None and ( isinstance(aggregation_cardinality_limit, bool) or not isinstance(aggregation_cardinality_limit, int) or aggregation_cardinality_limit < 1 ): raise ValueError( f"aggregation_cardinality_limit must be a positive integer, got {aggregation_cardinality_limit!r}" ) if ( instrument_type is instrument_name is instrument_unit is meter_name is meter_version is meter_schema_url is None ): # pylint: disable=broad-exception-raised raise Exception(f"Some instrument selection criteria must be provided for View {name}") if name is not None and instrument_name is not None and ("*" in instrument_name or "?" in instrument_name): # pylint: disable=broad-exception-raised raise Exception(f"View {name} declared with wildcard characters in instrument_name") # _name, _description, _aggregation, _exemplar_reservoir_factory, # _attribute_keys and _aggregation_cardinality_limit will be accessed # when instantiating a _ViewInstrumentMatch. self._name = name self._instrument_type = instrument_type self._instrument_name = instrument_name self._instrument_unit = instrument_unit self._meter_name = meter_name self._meter_version = meter_version self._meter_schema_url = meter_schema_url self._description = description self._attribute_keys = attribute_keys self._aggregation = aggregation or self._default_aggregation self._exemplar_reservoir_factory = exemplar_reservoir_factory or _default_reservoir_factory self._aggregation_cardinality_limit = aggregation_cardinality_limit # pylint: disable=too-many-return-statements # pylint: disable=too-many-branches def _match(self, instrument: _Instrument) -> bool: if self._instrument_type is not None: if not isinstance(instrument, self._instrument_type): return False if self._instrument_name is not None: # Instrument names are treated case-insensitively and are stored # lower-cased by the SDK, so the pattern is lower-cased to match. # fnmatchcase is used instead of fnmatch so it does not rely # on the host platform's filename case sensitivity (normcase). if not fnmatchcase(instrument.name, self._instrument_name.lower()): return False if self._instrument_unit is not None: # Units are case-sensitive; fnmatchcase keeps matching consistent # across platforms. if not fnmatchcase(instrument.unit, self._instrument_unit): return False if self._meter_name is not None: if instrument.instrumentation_scope.name != self._meter_name: return False if self._meter_version is not None: if instrument.instrumentation_scope.version != self._meter_version: return False if self._meter_schema_url is not None: if instrument.instrumentation_scope.schema_url != self._meter_schema_url: return False return True python-opentelemetry-1.45.2/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/000077500000000000000000000000001527070070700307315ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/__init__.py000066400000000000000000000025171527070070700330470ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from opentelemetry.sdk.metrics._internal.aggregation import ( AggregationTemporality, ) from opentelemetry.sdk.metrics._internal.export import ( CardinalityLimits, ConsoleMetricExporter, InMemoryMetricReader, InstrumentCardinalityKey, MetricExporter, MetricExportResult, MetricReader, PeriodicExportingMetricReader, ) # The point module is not in the export directory to avoid a circular import. from opentelemetry.sdk.metrics._internal.point import ( Buckets, DataPointT, DataT, ExponentialHistogram, ExponentialHistogramDataPoint, Gauge, Histogram, HistogramDataPoint, Metric, MetricsData, NumberDataPoint, ResourceMetrics, ScopeMetrics, Sum, ) __all__ = [ "AggregationTemporality", "Buckets", "CardinalityLimits", "ConsoleMetricExporter", "DataPointT", "DataT", "ExponentialHistogram", "ExponentialHistogramDataPoint", "Gauge", "Histogram", "HistogramDataPoint", "InMemoryMetricReader", "InstrumentCardinalityKey", "Metric", "MetricExportResult", "MetricExporter", "MetricReader", "MetricsData", "NumberDataPoint", "PeriodicExportingMetricReader", "ResourceMetrics", "ScopeMetrics", "Sum", ] python-opentelemetry-1.45.2/opentelemetry-sdk/src/opentelemetry/sdk/metrics/view/000077500000000000000000000000001527070070700303625ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-sdk/src/opentelemetry/sdk/metrics/view/__init__.py000066400000000000000000000011561527070070700324760ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from opentelemetry.sdk.metrics._internal.aggregation import ( Aggregation, DefaultAggregation, DropAggregation, ExplicitBucketHistogramAggregation, ExponentialBucketHistogramAggregation, LastValueAggregation, SumAggregation, ) from opentelemetry.sdk.metrics._internal.view import View __all__ = [ "Aggregation", "DefaultAggregation", "DropAggregation", "ExplicitBucketHistogramAggregation", "ExponentialBucketHistogramAggregation", "LastValueAggregation", "SumAggregation", "View", ] python-opentelemetry-1.45.2/opentelemetry-sdk/src/opentelemetry/sdk/py.typed000066400000000000000000000000001527070070700274270ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-sdk/src/opentelemetry/sdk/resources/000077500000000000000000000000001527070070700277545ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py000066400000000000000000000712441527070070700320750ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 """ This package implements `OpenTelemetry Resources `_: *A Resource is an immutable representation of the entity producing telemetry. For example, a process producing telemetry that is running in a container on Kubernetes has a Pod name, it is in a namespace and possibly is part of a Deployment which also has a name. All three of these attributes can be included in the Resource.* Resource objects are created with `Resource.create`, which accepts attributes (key-values). Resources should NOT be created via constructor except by `ResourceDetector` instances which can't use `Resource.create` to avoid infinite loops. Working with `Resource` objects should only be done via the Resource API methods. Resource attributes can also be passed at process invocation in the :envvar:`OTEL_RESOURCE_ATTRIBUTES` environment variable. You should register your resource with the `opentelemetry.sdk.trace.TracerProvider` by passing them into their constructors. The `Resource` passed to a provider is available to the exporter, which can send on this information as it sees fit. .. code-block:: python trace.set_tracer_provider( TracerProvider( resource=Resource.create( { "service.name": "shoppingcart", "service.instance.id": "instance-12", } ), ), ) print(trace.get_tracer_provider().resource.attributes) { "telemetry.sdk.language": "python", "telemetry.sdk.name": "opentelemetry", "telemetry.sdk.version": "0.13.dev0", "service.name": "shoppingcart", "service.instance.id": "instance-12", } Note that the OpenTelemetry project documents certain `"standard attributes" `_ that have prescribed semantic meanings, for example ``service.name`` in the above example. """ # ResourceAttributes is deprecated # pyright: reportDeprecated=false import abc import concurrent.futures import logging import os import platform import socket import subprocess import sys import threading import uuid from collections.abc import Mapping, Sequence from json import dumps from os import environ from types import ModuleType from urllib import parse from opentelemetry.attributes import BoundedAttributes from opentelemetry.sdk.environment_variables import ( OTEL_EXPERIMENTAL_RESOURCE_DETECTORS, OTEL_RESOURCE_ATTRIBUTES, OTEL_SERVICE_NAME, ) from opentelemetry.sdk.version import ( __version__ as _OPENTELEMETRY_SDK_VERSION, ) from opentelemetry.semconv.resource import ResourceAttributes from opentelemetry.util.types import AnyValue psutil: ModuleType | None = None try: import psutil as psutil_module psutil = psutil_module except ImportError: pass # Only available on Windows, where it is used to read the MachineGuid for host.id. winreg: ModuleType | None = None try: import winreg as winreg_module winreg = winreg_module except ImportError: pass LabelValue = AnyValue Attributes = Mapping[str, LabelValue] logger = logging.getLogger(__name__) CLOUD_PROVIDER = ResourceAttributes.CLOUD_PROVIDER CLOUD_ACCOUNT_ID = ResourceAttributes.CLOUD_ACCOUNT_ID CLOUD_REGION = ResourceAttributes.CLOUD_REGION CLOUD_AVAILABILITY_ZONE = ResourceAttributes.CLOUD_AVAILABILITY_ZONE CONTAINER_NAME = ResourceAttributes.CONTAINER_NAME CONTAINER_ID = ResourceAttributes.CONTAINER_ID CONTAINER_IMAGE_NAME = ResourceAttributes.CONTAINER_IMAGE_NAME CONTAINER_IMAGE_TAG = ResourceAttributes.CONTAINER_IMAGE_TAG DEPLOYMENT_ENVIRONMENT = ResourceAttributes.DEPLOYMENT_ENVIRONMENT FAAS_NAME = ResourceAttributes.FAAS_NAME FAAS_ID = ResourceAttributes.FAAS_ID FAAS_VERSION = ResourceAttributes.FAAS_VERSION FAAS_INSTANCE = ResourceAttributes.FAAS_INSTANCE HOST_NAME = ResourceAttributes.HOST_NAME HOST_ARCH = ResourceAttributes.HOST_ARCH HOST_ID = ResourceAttributes.HOST_ID HOST_TYPE = ResourceAttributes.HOST_TYPE HOST_IMAGE_NAME = ResourceAttributes.HOST_IMAGE_NAME HOST_IMAGE_ID = ResourceAttributes.HOST_IMAGE_ID HOST_IMAGE_VERSION = ResourceAttributes.HOST_IMAGE_VERSION KUBERNETES_CLUSTER_NAME = ResourceAttributes.K8S_CLUSTER_NAME KUBERNETES_NAMESPACE_NAME = ResourceAttributes.K8S_NAMESPACE_NAME KUBERNETES_POD_UID = ResourceAttributes.K8S_POD_UID KUBERNETES_POD_NAME = ResourceAttributes.K8S_POD_NAME KUBERNETES_CONTAINER_NAME = ResourceAttributes.K8S_CONTAINER_NAME KUBERNETES_REPLICA_SET_UID = ResourceAttributes.K8S_REPLICASET_UID KUBERNETES_REPLICA_SET_NAME = ResourceAttributes.K8S_REPLICASET_NAME KUBERNETES_DEPLOYMENT_UID = ResourceAttributes.K8S_DEPLOYMENT_UID KUBERNETES_DEPLOYMENT_NAME = ResourceAttributes.K8S_DEPLOYMENT_NAME KUBERNETES_STATEFUL_SET_UID = ResourceAttributes.K8S_STATEFULSET_UID KUBERNETES_STATEFUL_SET_NAME = ResourceAttributes.K8S_STATEFULSET_NAME KUBERNETES_DAEMON_SET_UID = ResourceAttributes.K8S_DAEMONSET_UID KUBERNETES_DAEMON_SET_NAME = ResourceAttributes.K8S_DAEMONSET_NAME KUBERNETES_JOB_UID = ResourceAttributes.K8S_JOB_UID KUBERNETES_JOB_NAME = ResourceAttributes.K8S_JOB_NAME KUBERNETES_CRON_JOB_UID = ResourceAttributes.K8S_CRONJOB_UID KUBERNETES_CRON_JOB_NAME = ResourceAttributes.K8S_CRONJOB_NAME OS_DESCRIPTION = ResourceAttributes.OS_DESCRIPTION OS_TYPE = ResourceAttributes.OS_TYPE OS_VERSION = ResourceAttributes.OS_VERSION PROCESS_PID = ResourceAttributes.PROCESS_PID PROCESS_PARENT_PID = ResourceAttributes.PROCESS_PARENT_PID PROCESS_EXECUTABLE_NAME = ResourceAttributes.PROCESS_EXECUTABLE_NAME PROCESS_EXECUTABLE_PATH = ResourceAttributes.PROCESS_EXECUTABLE_PATH PROCESS_COMMAND = ResourceAttributes.PROCESS_COMMAND PROCESS_COMMAND_LINE = ResourceAttributes.PROCESS_COMMAND_LINE PROCESS_COMMAND_ARGS = ResourceAttributes.PROCESS_COMMAND_ARGS PROCESS_OWNER = ResourceAttributes.PROCESS_OWNER PROCESS_RUNTIME_NAME = ResourceAttributes.PROCESS_RUNTIME_NAME PROCESS_RUNTIME_VERSION = ResourceAttributes.PROCESS_RUNTIME_VERSION PROCESS_RUNTIME_DESCRIPTION = ResourceAttributes.PROCESS_RUNTIME_DESCRIPTION SERVICE_NAME = ResourceAttributes.SERVICE_NAME SERVICE_NAMESPACE = ResourceAttributes.SERVICE_NAMESPACE SERVICE_INSTANCE_ID = ResourceAttributes.SERVICE_INSTANCE_ID SERVICE_VERSION = ResourceAttributes.SERVICE_VERSION TELEMETRY_SDK_NAME = ResourceAttributes.TELEMETRY_SDK_NAME TELEMETRY_SDK_VERSION = ResourceAttributes.TELEMETRY_SDK_VERSION TELEMETRY_AUTO_VERSION = ResourceAttributes.TELEMETRY_AUTO_VERSION TELEMETRY_SDK_LANGUAGE = ResourceAttributes.TELEMETRY_SDK_LANGUAGE class Resource: """A Resource is an immutable representation of the entity producing telemetry as Attributes.""" _attributes: BoundedAttributes _schema_url: str def __init__(self, attributes: Attributes, schema_url: str | None = None): # Immutable set to true so attributes cannot be added or removed after creation. self._attributes = BoundedAttributes(attributes=attributes, immutable=True) if schema_url is None: schema_url = "" self._schema_url = schema_url @staticmethod def create( attributes: Attributes | None = None, schema_url: str | None = None, ) -> "Resource": """Creates a new `Resource` from attributes. `ResourceDetector` instances should not call this method. Args: attributes: Optional zero or more key-value pairs. schema_url: Optional URL pointing to the schema Returns: The newly-created Resource. """ if not attributes: attributes = {} resource = get_aggregated_resources(_build_resource_detectors(), _DEFAULT_RESOURCE).merge( Resource(attributes, schema_url) ) if not resource.attributes.get(SERVICE_NAME, None): default_service_name = "unknown_service" if sys.executable: default_service_name += f":{os.path.basename(sys.executable)}" resource = resource.merge(Resource({SERVICE_NAME: default_service_name}, schema_url)) return resource @staticmethod def get_empty() -> "Resource": return _EMPTY_RESOURCE @property def attributes(self) -> Attributes: return self._attributes @property def schema_url(self) -> str: return self._schema_url def merge(self, other: "Resource") -> "Resource": """Merges this resource and an updating resource into a new `Resource`. If a key exists on both the old and updating resource, the value of the updating resource will override the old resource value. The updating resource's `schema_url` will be used only if the old `schema_url` is empty. Attempting to merge two resources with different, non-empty values for `schema_url` will result in an error and return the old resource. Args: other: The other resource to be merged. Returns: The newly-created Resource. """ merged_attributes = dict(self.attributes) merged_attributes.update(other.attributes) if self.schema_url == "": schema_url = other.schema_url elif other.schema_url == "": schema_url = self.schema_url elif self.schema_url == other.schema_url: schema_url = other.schema_url else: logger.error( "Failed to merge resources: The two schemas %s and %s are incompatible", self.schema_url, other.schema_url, ) return self return Resource(merged_attributes, schema_url) def __eq__(self, other: object) -> bool: if not isinstance(other, Resource): return False return self._attributes == other._attributes and self._schema_url == other._schema_url def __hash__(self) -> int: return hash(f"{dumps(self._attributes.copy(), sort_keys=True, default=_json_default)}|{self._schema_url}") def to_json(self, indent: int | None = 4) -> str: return dumps( { "attributes": dict(self.attributes), "schema_url": self._schema_url, }, indent=indent, default=_json_default, ) def _json_default(value: object) -> str: """Represent attribute values that `json` cannot encode natively. `types.AnyValue` admits `bytes`, which `json.dumps` rejects. Rendering it as hex keeps `to_json` readable and keeps `__hash__` working; `__eq__` still distinguishes `b"\x01"` from the string `"01"`, so the resulting hash collision is harmless. """ if isinstance(value, (bytes, bytearray)): return value.hex() return str(value) _EMPTY_RESOURCE = Resource({}) _DEFAULT_RESOURCE = Resource( { TELEMETRY_SDK_LANGUAGE: "python", TELEMETRY_SDK_NAME: "opentelemetry", TELEMETRY_SDK_VERSION: _OPENTELEMETRY_SDK_VERSION, } ) _service_instance_id: str | None = None _service_instance_id_pid: int | None = None _service_instance_id_lock = threading.Lock() class ResourceDetector(abc.ABC): def __init__(self, raise_on_error: bool = False) -> None: self.raise_on_error = raise_on_error @abc.abstractmethod def detect(self) -> "Resource": """Don't call `Resource.create` here to avoid an infinite loop, instead instantiate `Resource` directly""" raise NotImplementedError() # pylint: disable-next=no-self-use def is_process_dependent(self) -> bool: """Return whether this detector depends on the current process identity. Process dependent detectors may return resource attributes that become stale after a process identity change, such as :func:`os.fork`. Detectors returning ``True`` should be re-run after such changes so the resulting :class:`Resource` describes the current process. Returns: ``True`` if this detector should be re-run after process identity changes otherwise ``False``. """ return False class OTELResourceDetector(ResourceDetector): # pylint: disable=no-self-use def detect(self) -> "Resource": env_resources_items = environ.get(OTEL_RESOURCE_ATTRIBUTES) env_resource_map: dict[str, AnyValue] = {} if env_resources_items: for item in env_resources_items.split(","): try: key, value = item.split("=", maxsplit=1) except ValueError as exc: logger.warning( "Invalid key value resource attribute pair %s: %s", item, exc, ) continue value_url_decoded = parse.unquote(value.strip()) env_resource_map[key.strip()] = value_url_decoded service_name = environ.get(OTEL_SERVICE_NAME) if service_name: env_resource_map[SERVICE_NAME] = service_name return Resource(env_resource_map) class ProcessResourceDetector(ResourceDetector): """Detect process resource attributes. Args: raise_on_error: Raise errors from detection instead of logging them. include_command_args: Include ``process.command_args`` and ``process.command_line``. These attributes can contain sensitive command-line values, so they are excluded by default. """ def __init__( self, raise_on_error: bool = False, *, include_command_args: bool = False, ) -> None: super().__init__(raise_on_error=raise_on_error) self._include_command_args = include_command_args # pylint: disable=no-self-use def is_process_dependent(self) -> bool: return True def detect(self) -> "Resource": _runtime_version = ".".join( map( str, ( sys.version_info[:3] if sys.version_info.releaselevel == "final" and not sys.version_info.serial else sys.version_info ), ) ) _process_pid = os.getpid() # Use sys.orig_argv, which preserves the original arguments received # by the interpreter. This correctly captures ``python -m `` # invocations where sys.argv is rewritten to the resolved module path # and the ``-m `` information is lost. Only read argv[0] by # default because full command arguments are opt-in. _process_command = sys.orig_argv[0] if sys.orig_argv else "" executable = sys.executable or "" resource_info: dict[str, AnyValue] = { PROCESS_RUNTIME_DESCRIPTION: sys.version, PROCESS_RUNTIME_NAME: sys.implementation.name, PROCESS_RUNTIME_VERSION: _runtime_version, PROCESS_PID: _process_pid, PROCESS_EXECUTABLE_NAME: os.path.basename(executable), PROCESS_EXECUTABLE_PATH: executable, PROCESS_COMMAND: _process_command, } if self._include_command_args: _process_argv = list(sys.orig_argv) resource_info[PROCESS_COMMAND_LINE] = " ".join(_process_argv) resource_info[PROCESS_COMMAND_ARGS] = _process_argv if hasattr(os, "getppid"): # pypy3 does not have getppid() resource_info[PROCESS_PARENT_PID] = os.getppid() if psutil is not None: process = psutil.Process() username = process.username() resource_info[PROCESS_OWNER] = username return Resource(resource_info) # type: ignore class OsResourceDetector(ResourceDetector): """Detect os resources based on `Operating System conventions `_.""" def detect(self) -> "Resource": """Returns a resource with with ``os.type`` and ``os.version``. Python's platform library ~~~~~~~~~~~~~~~~~~~~~~~~~ To grab this information, Python's ``platform`` does not return what a user might expect it to. Below is a breakdown of its return values in different operating systems. .. code-block:: python :caption: Linux >>> platform.system() 'Linux' >>> platform.release() '6.5.0-35-generic' >>> platform.version() '#35~22.04.1-Ubuntu SMP PREEMPT_DYNAMIC Tue May 7 09:00:52 UTC 2' .. code-block:: python :caption: MacOS >>> platform.system() 'Darwin' >>> platform.release() '23.0.0' >>> platform.version() 'Darwin Kernel Version 23.0.0: Fri Sep 15 14:42:57 PDT 2023; root:xnu-10002.1.13~1/RELEASE_ARM64_T8112' .. code-block:: python :caption: Windows >>> platform.system() 'Windows' >>> platform.release() '2022Server' >>> platform.version() '10.0.20348' .. code-block:: python :caption: FreeBSD >>> platform.system() 'FreeBSD' >>> platform.release() '14.1-RELEASE' >>> platform.version() 'FreeBSD 14.1-RELEASE releng/14.1-n267679-10e31f0946d8 GENERIC' .. code-block:: python :caption: Solaris >>> platform.system() 'SunOS' >>> platform.release() '5.11' >>> platform.version() '11.4.0.15.0' """ os_type = platform.system().lower() os_version = platform.release() # See docstring if os_type == "windows": os_version = platform.version() # Align SunOS with conventions elif os_type == "sunos": os_type = "solaris" os_version = platform.version() return Resource( { OS_TYPE: os_type, OS_VERSION: os_version, } ) # Non-privileged machine id sources per the semantic conventions: # https://opentelemetry.io/docs/specs/semconv/resource/host/#non-privileged-machine-id-lookup _LINUX_MACHINE_ID_PATHS = ("/etc/machine-id", "/var/lib/dbus/machine-id") _BSD_HOSTID_PATH = "/etc/hostid" _BSD_KENV_COMMAND = ("/bin/kenv", "-q", "smbios.system.uuid") _MACOS_IOREG_COMMAND = ("/usr/sbin/ioreg", "-rd1", "-c", "IOPlatformExpertDevice") _WINDOWS_CRYPTOGRAPHY_KEY = r"SOFTWARE\Microsoft\Cryptography" _WINDOWS_MACHINE_GUID_VALUE = "MachineGuid" # Deliberately below get_aggregated_resources' per detector timeout so that a # hung command still leaves time for host.name and host.arch to be returned. _COMMAND_TIMEOUT_SECONDS = 2 def _read_machine_id_file(path: str) -> str | None: try: with open(path, encoding="utf8") as machine_id_file: return machine_id_file.read().strip() or None except OSError as exception: logger.debug("Failed to read %s: %s", path, exception) return None def _run_command(command: tuple[str, ...]) -> str: """Returns the command's stdout, or "" when the source is unavailable here. A non-zero exit or a missing binary means this host has no machine id to offer. """ try: completed = subprocess.run( command, capture_output=True, text=True, timeout=_COMMAND_TIMEOUT_SECONDS, check=True, ) except (subprocess.CalledProcessError, FileNotFoundError) as exception: logger.debug("Failed to run %s: %s", command[0], exception) return "" return completed.stdout def _get_linux_machine_id() -> str | None: for path in _LINUX_MACHINE_ID_PATHS: machine_id = _read_machine_id_file(path) if machine_id: return machine_id return None def _get_bsd_machine_id() -> str | None: return _read_machine_id_file(_BSD_HOSTID_PATH) or _run_command(_BSD_KENV_COMMAND).strip() or None def _get_macos_machine_id() -> str | None: for line in _run_command(_MACOS_IOREG_COMMAND).splitlines(): # The line looks like: ` "IOPlatformUUID" = "AAAAAAAA-BBBB-..."` key, separator, value = line.partition("=") if not separator or key.strip().strip('"') != "IOPlatformUUID": continue machine_id = value.strip().strip('"') if machine_id: return machine_id return None def _get_windows_machine_id() -> str | None: if winreg is None: logger.debug("winreg is unavailable, cannot detect %s", HOST_ID) return None with winreg.OpenKey( winreg.HKEY_LOCAL_MACHINE, _WINDOWS_CRYPTOGRAPHY_KEY, access=winreg.KEY_READ | winreg.KEY_WOW64_64KEY, ) as key: machine_guid, _ = winreg.QueryValueEx(key, _WINDOWS_MACHINE_GUID_VALUE) return str(machine_guid) if machine_guid else None def _get_host_id() -> str | None: system = platform.system() if system == "Linux": return _get_linux_machine_id() if system == "Darwin": return _get_macos_machine_id() if system == "Windows": return _get_windows_machine_id() if system == "DragonFly" or system.endswith("BSD"): return _get_bsd_machine_id() logger.debug("Unsupported OS type for %s detection: %s", HOST_ID, system) return None class _HostResourceDetector(ResourceDetector): # type: ignore[reportUnusedClass] """ The HostResourceDetector detects the hostname, architecture and host id attributes. ``host.id`` is the non-privileged machine id described by the `Host resource conventions `_, and is omitted when it cannot be determined. A failed lookup does not prevent ``host.name`` and ``host.arch`` from being detected unless ``raise_on_error=True``. """ def detect(self) -> "Resource": resource_info: dict[str, AnyValue] = { HOST_NAME: socket.gethostname(), HOST_ARCH: platform.machine(), } # A failed host id lookup must not cost the caller the attributes above, # so it is guarded here rather than relying on the handling in # get_aggregated_resources: detect() is also called directly. try: if host_id := _get_host_id(): resource_info[HOST_ID] = host_id # pylint: disable=broad-exception-caught except Exception as exception: logger.warning("Failed to detect %s: %s", HOST_ID, exception) if self.raise_on_error: raise return Resource(resource_info) class ServiceInstanceIdResourceDetector(ResourceDetector): """Detects service.instance.id as a random UUID v4. Per the OpenTelemetry specification, SDKs SHOULD generate a random v1/v4 UUID for service.instance.id to uniquely identify each service instance. The ID is shared across all detector instances within the same process and regenerated automatically when the process PID changes (e.g. after a fork). Note: because this detector is process dependent, providers refresh it automatically after a fork and merge the newly detected value on top of the existing resource. This means that if a user explicitly sets `service.instance.id` (e.g. via OTEL_RESOURCE_ATTRIBUTES or Resource.create(attributes=...)), that value will be overwritten with a newly generated UUID the next time the process forks. This differs from Resource.create(), which does preserve user supplied overrides. """ def is_process_dependent(self) -> bool: return True def detect(self) -> "Resource": # pylint: disable-next=global-statement global _service_instance_id, _service_instance_id_pid with _service_instance_id_lock: current_pid = os.getpid() if _service_instance_id is None or _service_instance_id_pid != current_pid: _service_instance_id = str(uuid.uuid4()) _service_instance_id_pid = current_pid instance_id = _service_instance_id return Resource({SERVICE_INSTANCE_ID: instance_id}) def _build_resource_detectors() -> list["ResourceDetector"]: """Returns the ordered list of resource detectors to use for Resource.create. Fast path: if no extra detectors are configured, returns only the two built-in detectors without scanning entry_points. "service_instance" (ServiceInstanceIdResourceDetector) is prepended unless it is explicitly configured. "otel" (OTELResourceDetector) is last so that OTEL_RESOURCE_ATTRIBUTES and OTEL_SERVICE_NAME take highest merge priority, but an explicit position in OTEL_EXPERIMENTAL_RESOURCE_DETECTORS is respected for either name. """ configured_detector_names = [ name.strip() for name in environ.get(OTEL_EXPERIMENTAL_RESOURCE_DETECTORS, "").split(",") if name.strip() ] default_detector_names = [] if "service_instance" in configured_detector_names else ["service_instance"] detector_names: list[str] = list(dict.fromkeys(default_detector_names + configured_detector_names + ["otel"])) # Fast path: only the two built-in detectors — no entry_points scan needed. if detector_names == ["service_instance", "otel"]: return [ServiceInstanceIdResourceDetector(), OTELResourceDetector()] # pylint: disable=import-outside-toplevel from opentelemetry.util._importlib_metadata import ( # noqa: PLC0415 entry_points, # type: ignore[reportUnknownVariableType] ) if "*" in detector_names: registered = set( name for name in entry_points(group="opentelemetry_resource_detector").names # type: ignore[reportUnknownArgumentType] if name != "otel" ) expansion = sorted(registered - set(detector_names)) idx = detector_names.index("*") detector_names = detector_names[:idx] + expansion + detector_names[idx + 1 :] detectors: list[ResourceDetector] = [] for name in detector_names: try: detectors.append( next( iter( entry_points( group="opentelemetry_resource_detector", name=name, ) # type: ignore[reportUnknownArgumentType] ) ).load()() ) except Exception: # pylint: disable=broad-exception-caught logger.exception( "Failed to load resource detector '%s', skipping", name, ) return detectors def _get_process_dependent_resource() -> Resource: # pyright: ignore[reportUnusedFunction] return get_aggregated_resources( [detector for detector in _build_resource_detectors() if detector.is_process_dependent()], Resource.get_empty(), ) def get_aggregated_resources( detectors: Sequence["ResourceDetector"], initial_resource: Resource | None = None, timeout: int = 5, ) -> "Resource": """Retrieves resources from detectors in the order that they were passed :param detectors: List of resources in order of priority :param initial_resource: Static resource. This has highest priority :param timeout: Number of seconds to wait for each detector to return :return: """ detectors_merged_resource = initial_resource or Resource.create() # A detector that exceeds `timeout` is skipped, but shutdown still joins # its worker so no threads outlive this call. A detector that blocks # indefinitely therefore blocks this call. `cancel_futures=True` drops # detectors that have not started yet if a `raise_on_error` detector raises. executor = concurrent.futures.ThreadPoolExecutor(max_workers=4) try: futures = [executor.submit(detector.detect) for detector in detectors] for detector_ind, future in enumerate(futures): detector = detectors[detector_ind] detected_resource: Resource = _EMPTY_RESOURCE try: detected_resource = future.result(timeout=timeout) except concurrent.futures.TimeoutError as ex: if detector.raise_on_error: raise ex logger.warning( "Detector %s took longer than %s seconds, skipping", detector, timeout, ) # pylint: disable=broad-exception-caught except Exception as ex: if detector.raise_on_error: raise ex logger.warning("Exception %s in detector %s, ignoring", ex, detector) finally: detectors_merged_resource = detectors_merged_resource.merge(detected_resource) finally: executor.shutdown(wait=True, cancel_futures=True) return detectors_merged_resource python-opentelemetry-1.45.2/opentelemetry-sdk/src/opentelemetry/sdk/trace/000077500000000000000000000000001527070070700270405ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py000066400000000000000000001447041527070070700311630ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # pylint: disable=too-many-lines import abc import atexit import concurrent.futures import json import logging import os import threading import traceback import typing import weakref from collections.abc import ( Callable, Iterator, Mapping, MutableMapping, Sequence, ) from dataclasses import dataclass from os import environ from time import time_ns from types import MappingProxyType, TracebackType from typing import ( Any, ) from warnings import filterwarnings from typing_extensions import deprecated from opentelemetry import context as context_api from opentelemetry import metrics as metrics_api from opentelemetry import trace as trace_api from opentelemetry.attributes import BoundedAttributes from opentelemetry.sdk import util from opentelemetry.sdk.environment_variables import ( OTEL_ATTRIBUTE_COUNT_LIMIT, OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT, OTEL_EVENT_ATTRIBUTE_COUNT_LIMIT, OTEL_LINK_ATTRIBUTE_COUNT_LIMIT, OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED, OTEL_SDK_DISABLED, OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT, OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT, OTEL_SPAN_EVENT_COUNT_LIMIT, OTEL_SPAN_LINK_COUNT_LIMIT, ) from opentelemetry.sdk.environment_variables._internal import ( parse_boolean_environment_variable, ) from opentelemetry.sdk.resources import ( Resource, _get_process_dependent_resource, ) from opentelemetry.sdk.trace import sampling from opentelemetry.sdk.trace._tracer_metrics import create_tracer_metrics from opentelemetry.sdk.trace.id_generator import IdGenerator, RandomIdGenerator from opentelemetry.sdk.util import BoundedList from opentelemetry.sdk.util._configurator import RuleBasedConfigurator from opentelemetry.sdk.util.instrumentation import ( InstrumentationInfo, InstrumentationScope, ) from opentelemetry.semconv.attributes.exception_attributes import ( EXCEPTION_ESCAPED, EXCEPTION_MESSAGE, EXCEPTION_STACKTRACE, EXCEPTION_TYPE, ) from opentelemetry.trace import NoOpTracer, SpanContext from opentelemetry.trace.status import Status, StatusCode from opentelemetry.util import types from opentelemetry.util._decorator import _agnosticcontextmanager logger = logging.getLogger(__name__) _DEFAULT_OTEL_ATTRIBUTE_COUNT_LIMIT = 128 _DEFAULT_OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT = 128 _DEFAULT_OTEL_EVENT_ATTRIBUTE_COUNT_LIMIT = 128 _DEFAULT_OTEL_LINK_ATTRIBUTE_COUNT_LIMIT = 128 _DEFAULT_OTEL_SPAN_EVENT_COUNT_LIMIT = 128 _DEFAULT_OTEL_SPAN_LINK_COUNT_LIMIT = 128 _ENV_VALUE_UNSET = "" class SpanProcessor: """Interface which allows hooks for SDK's `Span` start and end method invocations. Span processors can be registered directly using :func:`TracerProvider.add_span_processor` and they are invoked in the same order as they were registered. """ def on_start( self, span: "Span", parent_context: context_api.Context | None = None, ) -> None: """Called when a :class:`opentelemetry.trace.Span` is started. This method is called synchronously on the thread that starts the span, therefore it should not block or throw an exception. Args: span: The :class:`opentelemetry.trace.Span` that just started. parent_context: The parent context of the span that just started. """ def _on_ending(self, span: "Span") -> None: """Called when a :class:`opentelemetry.trace.Span` is ending. This method is called synchronously on the thread that ends the span, therefore it should not block or throw an exception. Args: span: The :class:`opentelemetry.trace.Span` that is ending. """ def on_end(self, span: "ReadableSpan") -> None: """Called when a :class:`opentelemetry.trace.Span` is ended. This method is called synchronously on the thread that ends the span, therefore it should not block or throw an exception. Args: span: The :class:`opentelemetry.trace.Span` that just ended. """ def shutdown(self) -> None: """Called when a :class:`opentelemetry.sdk.trace.TracerProvider` is shutdown.""" def force_flush(self, timeout_millis: int = 30000) -> bool: # pylint: disable=no-self-use """Export all ended spans to the configured Exporter that have not yet been exported. Args: timeout_millis: The maximum amount of time to wait for spans to be exported. Returns: False if the timeout is exceeded, True otherwise. """ return True # Temporary fix until https://github.com/PyCQA/pylint/issues/4098 is resolved # pylint:disable=no-member class SynchronousMultiSpanProcessor(SpanProcessor): """Implementation of class:`SpanProcessor` that forwards all received events to a list of span processors sequentially. The underlying span processors are called in sequential order as they were added. """ _span_processors: tuple[SpanProcessor, ...] def __init__(self): # use a tuple to avoid race conditions when adding a new span and # iterating through it on "on_start" and "on_end". self._span_processors = () self._lock = threading.Lock() def add_span_processor(self, span_processor: SpanProcessor) -> None: """Adds a SpanProcessor to the list handled by this instance.""" with self._lock: self._span_processors += (span_processor,) def on_start( self, span: "Span", parent_context: context_api.Context | None = None, ) -> None: for sp in self._span_processors: sp.on_start(span, parent_context=parent_context) def _on_ending(self, span: "Span") -> None: for sp in self._span_processors: # pylint: disable=protected-access sp._on_ending(span) def on_end(self, span: "ReadableSpan") -> None: for sp in self._span_processors: sp.on_end(span) def shutdown(self) -> None: """Sequentially shuts down all underlying span processors.""" for sp in self._span_processors: sp.shutdown() def force_flush(self, timeout_millis: int = 30000) -> bool: """Sequentially calls force_flush on all underlying :class:`SpanProcessor` Args: timeout_millis: The maximum amount of time over all span processors to wait for spans to be exported. In case the first n span processors exceeded the timeout followup span processors will be skipped. Returns: True if all span processors flushed their spans within the given timeout, False otherwise. """ deadline_ns = time_ns() + timeout_millis * 1000000 all_flushed = True for sp in self._span_processors: current_time_ns = time_ns() if current_time_ns >= deadline_ns: return False if sp.force_flush((deadline_ns - current_time_ns) // 1000000) is False: all_flushed = False return all_flushed class ConcurrentMultiSpanProcessor(SpanProcessor): """Implementation of :class:`SpanProcessor` that forwards all received events to a list of span processors in parallel. Calls to the underlying span processors are forwarded in parallel by submitting them to a thread pool executor and waiting until each span processor finished its work. Args: num_threads: The number of threads managed by the thread pool executor and thus defining how many span processors can work in parallel. """ _span_processors: tuple[SpanProcessor, ...] def __init__(self, num_threads: int = 2): # use a tuple to avoid race conditions when adding a new span and # iterating through it on "on_start" and "on_end". self._span_processors = () self._lock = threading.Lock() self._init_executor(num_threads) if hasattr(os, "register_at_fork"): # Only the main thread is kept in forked processed, the executor # needs to be re-instantiated to get a fresh pool of threads: weak_reinit = weakref.WeakMethod(self._init_executor) def _after_in_child() -> None: reinit = weak_reinit() if reinit is not None: reinit(num_threads) os.register_at_fork(after_in_child=_after_in_child) def _init_executor(self, num_threads: int) -> None: self._executor = concurrent.futures.ThreadPoolExecutor(max_workers=num_threads) def add_span_processor(self, span_processor: SpanProcessor) -> None: """Adds a SpanProcessor to the list handled by this instance.""" with self._lock: self._span_processors += (span_processor,) def _submit_and_await( self, func: Callable[[SpanProcessor], Callable[..., None]], *args: Any, **kwargs: Any, ): futures = [] for sp in self._span_processors: future = self._executor.submit(func(sp), *args, **kwargs) futures.append(future) for future in futures: future.result() def on_start( self, span: "Span", parent_context: context_api.Context | None = None, ) -> None: self._submit_and_await(lambda sp: sp.on_start, span, parent_context=parent_context) def _on_ending(self, span: "Span") -> None: # pylint: disable=protected-access self._submit_and_await(lambda sp: sp._on_ending, span) def on_end(self, span: "ReadableSpan") -> None: self._submit_and_await(lambda sp: sp.on_end, span) def shutdown(self) -> None: """Shuts down all underlying span processors in parallel.""" self._submit_and_await(lambda sp: sp.shutdown) def force_flush(self, timeout_millis: int = 30000) -> bool: """Calls force_flush on all underlying span processors in parallel. Args: timeout_millis: The maximum amount of time to wait for spans to be exported. Returns: True if all span processors flushed their spans within the given timeout, False otherwise. """ futures = [] for sp in self._span_processors: future = self._executor.submit(sp.force_flush, timeout_millis) futures.append(future) timeout_sec = timeout_millis / 1e3 done_futures, not_done_futures = concurrent.futures.wait(futures, timeout_sec) if not_done_futures: return False for future in done_futures: if future.result() is False: return False return True class EventBase(abc.ABC): def __init__(self, name: str, timestamp: int | None = None) -> None: self._name = name if timestamp is None: self._timestamp = time_ns() else: self._timestamp = timestamp @property def name(self) -> str: return self._name @property def timestamp(self) -> int: return self._timestamp @property @abc.abstractmethod def attributes(self) -> types.Attributes: pass class Event(EventBase): """A text annotation with a set of attributes. The attributes of an event are immutable. Args: name: Name of the event. attributes: Attributes of the event. timestamp: Timestamp of the event. If `None` it will filled automatically. """ def __init__( self, name: str, attributes: types.Attributes = None, timestamp: int | None = None, limit: int | None = _DEFAULT_OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT, ) -> None: super().__init__(name, timestamp) self._attributes = attributes @property def attributes(self) -> types.Attributes: return self._attributes @property def dropped_attributes(self) -> int: if isinstance(self._attributes, BoundedAttributes): return self._attributes.dropped return 0 def _check_span_ended(func): def wrapper(self, *args, **kwargs): already_ended = False with self._lock: # pylint: disable=protected-access if self._end_time is None: # pylint: disable=protected-access func(self, *args, **kwargs) else: already_ended = True if already_ended: logger.warning("Tried calling %s on an ended span.", func.__name__) return wrapper def _is_valid_link(context: SpanContext, attributes: types.Attributes) -> bool: return bool(context and (context.is_valid or (attributes or context.trace_state))) class ReadableSpan: """Provides read-only access to span attributes. Users should NOT be creating these objects directly. `ReadableSpan`s are created as a direct result from using the tracing pipeline via the `Tracer`. """ def __init__( self, name: str, context: trace_api.SpanContext | None = None, parent: trace_api.SpanContext | None = None, resource: Resource | None = None, attributes: types.Attributes = None, events: Sequence[Event] = (), links: Sequence[trace_api.Link] = (), kind: trace_api.SpanKind = trace_api.SpanKind.INTERNAL, instrumentation_info: InstrumentationInfo | None = None, status: Status = Status(StatusCode.UNSET), start_time: int | None = None, end_time: int | None = None, instrumentation_scope: InstrumentationScope | None = None, ) -> None: self._name = name self._context = context self._kind = kind self._instrumentation_info = instrumentation_info self._instrumentation_scope = instrumentation_scope self._parent = parent self._start_time = start_time self._end_time = end_time self._attributes = attributes self._events = events self._links = links if resource is None: self._resource = Resource.create({}) else: self._resource = resource self._status = status @property def dropped_attributes(self) -> int: if isinstance(self._attributes, BoundedAttributes): return self._attributes.dropped return 0 @property def dropped_events(self) -> int: if isinstance(self._events, BoundedList): return self._events.dropped return 0 @property def dropped_links(self) -> int: if isinstance(self._links, BoundedList): return self._links.dropped return 0 @property def name(self) -> str: return self._name def get_span_context(self) -> trace_api.SpanContext | None: return self._context @property def context(self): return self._context @property def kind(self) -> trace_api.SpanKind: return self._kind @property def parent(self) -> trace_api.SpanContext | None: return self._parent @property def start_time(self) -> int | None: return self._start_time @property def end_time(self) -> int | None: return self._end_time @property def status(self) -> trace_api.Status: return self._status @property def attributes(self) -> types.Attributes: return MappingProxyType(self._attributes or {}) @property def events(self) -> Sequence[Event]: return tuple(self._events) @property def links(self) -> Sequence[trace_api.Link]: return tuple(self._links) @property def resource(self) -> Resource: return self._resource @property @deprecated("You should use instrumentation_scope. Deprecated since version 1.11.1.") def instrumentation_info(self) -> InstrumentationInfo | None: return self._instrumentation_info @property def instrumentation_scope(self) -> InstrumentationScope | None: return self._instrumentation_scope def to_json(self, indent: int | None = 4): parent_id = None if self.parent is not None: parent_id = f"0x{trace_api.format_span_id(self.parent.span_id)}" start_time = None if self._start_time: start_time = util.ns_to_iso_str(self._start_time) end_time = None if self._end_time: end_time = util.ns_to_iso_str(self._end_time) status = { "status_code": str(self._status.status_code.name), } if self._status.description: status["description"] = self._status.description f_span = { "name": self._name, "context": (self._format_context(self._context) if self._context else None), "kind": str(self.kind), "parent_id": parent_id, "start_time": start_time, "end_time": end_time, "status": status, "attributes": self._format_attributes(self._attributes), "events": self._format_events(self._events), "links": self._format_links(self._links), "resource": json.loads(self.resource.to_json()), } return json.dumps(f_span, indent=indent) @staticmethod def _format_context(context: SpanContext) -> dict[str, str]: return { "trace_id": f"0x{trace_api.format_trace_id(context.trace_id)}", "span_id": f"0x{trace_api.format_span_id(context.span_id)}", "trace_state": repr(context.trace_state), } @staticmethod def _format_attributes( attributes: types.Attributes, ) -> dict[str, Any] | None: if attributes is not None and not isinstance(attributes, dict): return dict(attributes) return attributes @staticmethod def _format_events(events: Sequence[Event]) -> list[dict[str, Any]]: return [ { "name": event.name, "timestamp": util.ns_to_iso_str(event.timestamp), "attributes": Span._format_attributes( # pylint: disable=protected-access event.attributes ), } for event in events ] @staticmethod def _format_links(links: Sequence[trace_api.Link]) -> list[dict[str, Any]]: return [ { "context": Span._format_context( # pylint: disable=protected-access link.context ), "attributes": Span._format_attributes( # pylint: disable=protected-access link.attributes ), } for link in links ] class SpanLimits: """The limits that should be enforced on recorded data such as events, links, attributes etc. This class does not enforce any limits itself. It only provides a way to read limits from env, default values and from user provided arguments. All limit arguments must be either a non-negative integer, ``None`` or ``SpanLimits.UNSET``. - All limit arguments are optional. - If a limit argument is not set, the class will try to read its value from the corresponding environment variable. - If the environment variable is not set, the default value, if any, will be used. Limit precedence: - If a model specific limit is set, it will be used. - Else if the corresponding global limit is set, it will be used. - Else if the model specific limit has a default value, the default value will be used. - Else if the global limit has a default value, the default value will be used. Args: max_attributes: Maximum number of attributes that can be added to a span, event, and link. Environment variable: OTEL_ATTRIBUTE_COUNT_LIMIT Default: {_DEFAULT_ATTRIBUTE_COUNT_LIMIT} max_events: Maximum number of events that can be added to a Span. Environment variable: OTEL_SPAN_EVENT_COUNT_LIMIT Default: {_DEFAULT_SPAN_EVENT_COUNT_LIMIT} max_links: Maximum number of links that can be added to a Span. Environment variable: OTEL_SPAN_LINK_COUNT_LIMIT Default: {_DEFAULT_SPAN_LINK_COUNT_LIMIT} max_span_attributes: Maximum number of attributes that can be added to a Span. Environment variable: OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT Default: {_DEFAULT_OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT} max_event_attributes: Maximum number of attributes that can be added to an Event. Default: {_DEFAULT_OTEL_EVENT_ATTRIBUTE_COUNT_LIMIT} max_link_attributes: Maximum number of attributes that can be added to a Link. Default: {_DEFAULT_OTEL_LINK_ATTRIBUTE_COUNT_LIMIT} max_attribute_length: Maximum length an attribute value can have. Values longer than the specified length will be truncated. max_span_attribute_length: Maximum length a span attribute value can have. Values longer than the specified length will be truncated. """ UNSET = -1 def __init__( self, max_attributes: int | None = None, max_events: int | None = None, max_links: int | None = None, max_span_attributes: int | None = None, max_event_attributes: int | None = None, max_link_attributes: int | None = None, max_attribute_length: int | None = None, max_span_attribute_length: int | None = None, ): # span events and links count self.max_events = self._from_env_if_absent( max_events, OTEL_SPAN_EVENT_COUNT_LIMIT, _DEFAULT_OTEL_SPAN_EVENT_COUNT_LIMIT, ) self.max_links = self._from_env_if_absent( max_links, OTEL_SPAN_LINK_COUNT_LIMIT, _DEFAULT_OTEL_SPAN_LINK_COUNT_LIMIT, ) # attribute count global_max_attributes = self._from_env_if_absent(max_attributes, OTEL_ATTRIBUTE_COUNT_LIMIT) self.max_attributes = ( global_max_attributes if global_max_attributes is not None else _DEFAULT_OTEL_ATTRIBUTE_COUNT_LIMIT ) self.max_span_attributes = self._from_env_if_absent( max_span_attributes, OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT, (global_max_attributes if global_max_attributes is not None else _DEFAULT_OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT), ) self.max_event_attributes = self._from_env_if_absent( max_event_attributes, OTEL_EVENT_ATTRIBUTE_COUNT_LIMIT, (global_max_attributes if global_max_attributes is not None else _DEFAULT_OTEL_EVENT_ATTRIBUTE_COUNT_LIMIT), ) self.max_link_attributes = self._from_env_if_absent( max_link_attributes, OTEL_LINK_ATTRIBUTE_COUNT_LIMIT, (global_max_attributes if global_max_attributes is not None else _DEFAULT_OTEL_LINK_ATTRIBUTE_COUNT_LIMIT), ) # attribute length self.max_attribute_length = self._from_env_if_absent( max_attribute_length, OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT, ) self.max_span_attribute_length = self._from_env_if_absent( max_span_attribute_length, OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT, # use global attribute length limit as default self.max_attribute_length, ) def __repr__(self): return f"{type(self).__name__}(max_span_attributes={self.max_span_attributes}, max_events_attributes={self.max_event_attributes}, max_link_attributes={self.max_link_attributes}, max_attributes={self.max_attributes}, max_events={self.max_events}, max_links={self.max_links}, max_attribute_length={self.max_attribute_length})" @classmethod def _from_env_if_absent(cls, value: int | None, env_var: str, default: int | None = None) -> int | None: if value == cls.UNSET: return None err_msg = "{} must be a non-negative integer but got {}" # if no value is provided for the limit, try to load it from env if value is None: # return default value if env var is not set if env_var not in environ: return default str_value = environ.get(env_var, "").strip().lower() if str_value == _ENV_VALUE_UNSET: return None try: value = int(str_value) except ValueError: raise ValueError(err_msg.format(env_var, str_value)) if value < 0: raise ValueError(err_msg.format(env_var, value)) return value _UnsetLimits = SpanLimits( max_attributes=SpanLimits.UNSET, max_events=SpanLimits.UNSET, max_links=SpanLimits.UNSET, max_span_attributes=SpanLimits.UNSET, max_event_attributes=SpanLimits.UNSET, max_link_attributes=SpanLimits.UNSET, max_attribute_length=SpanLimits.UNSET, max_span_attribute_length=SpanLimits.UNSET, ) # not removed for backward compat. please use SpanLimits instead. SPAN_ATTRIBUTE_COUNT_LIMIT = SpanLimits._from_env_if_absent( # pylint: disable=protected-access None, OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT, _DEFAULT_OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT, ) class Span(trace_api.Span, ReadableSpan): """See `opentelemetry.trace.Span`. Users should create `Span` objects via the `Tracer` instead of this constructor. Args: name: The name of the operation this span represents context: The immutable span context parent: This span's parent's `opentelemetry.trace.SpanContext`, or None if this is a root span sampler: The sampler used to create this span trace_config: Unused. Originally intended for trace-level configuration from the OpenTelemetry protocol, but the upstream ``TraceConfig`` proto was removed. Retained for backwards compatibility. resource: Entity producing telemetry attributes: The span's attributes to be exported events: Timestamped events to be exported links: Links to other spans to be exported span_processor: `SpanProcessor` to invoke when starting and ending this `Span`. limits: `SpanLimits` instance that was passed to the `TracerProvider` """ def __new__(cls, *args, **kwargs): if cls is Span: raise TypeError("Span must be instantiated via a tracer.") return super().__new__(cls) # pylint: disable=too-many-locals def __init__( self, name: str, context: trace_api.SpanContext, parent: trace_api.SpanContext | None = None, sampler: sampling.Sampler | None = None, trace_config: None = None, resource: Resource | None = None, attributes: types.Attributes = None, events: Sequence[Event] | None = None, links: Sequence[trace_api.Link] = (), kind: trace_api.SpanKind = trace_api.SpanKind.INTERNAL, span_processor: SpanProcessor = SpanProcessor(), instrumentation_info: InstrumentationInfo | None = None, record_exception: bool = True, set_status_on_exception: bool = True, limits=_UnsetLimits, instrumentation_scope: InstrumentationScope | None = None, *, record_end_metrics: Callable[[], None] | None = None, ) -> None: if resource is None: resource = Resource.create({}) super().__init__( name=name, context=context, parent=parent, kind=kind, resource=resource, instrumentation_info=instrumentation_info, instrumentation_scope=instrumentation_scope, ) self._sampler = sampler self._trace_config = trace_config self._record_exception = record_exception self._set_status_on_exception = set_status_on_exception self._span_processor = span_processor self._limits = limits self._lock = threading.Lock() self._attributes = BoundedAttributes( self._limits.max_span_attributes, attributes, immutable=False, max_value_len=self._limits.max_span_attribute_length, ) self._events = self._new_events() if events: for event in events: # Immutable set to true so attributes cannot be added or removed after creation. event._attributes = BoundedAttributes( self._limits.max_event_attributes, event.attributes, max_value_len=self._limits.max_attribute_length, immutable=True, ) self._events.append(event) self._links = self._new_links(links) self._record_end_metrics = record_end_metrics def __repr__(self): return f'{type(self).__name__}(name="{self._name}", context={self._context})' def _new_events(self): return BoundedList(self._limits.max_events) def _new_links(self, links: Sequence[trace_api.Link]): if not links: return BoundedList(self._limits.max_links) valid_links = [] for link in links: if link and _is_valid_link(link.context, link.attributes): # Immutable set to true so attributes cannot be added or removed after creation. # pylint: disable=protected-access link._attributes = BoundedAttributes( self._limits.max_link_attributes, link.attributes, max_value_len=self._limits.max_attribute_length, immutable=True, ) valid_links.append(link) return BoundedList.from_seq(self._limits.max_links, valid_links) def get_span_context(self) -> trace_api.SpanContext: return typing.cast(trace_api.SpanContext, self._context) def set_attributes(self, attributes: Mapping[str, types.AnyValue]) -> None: with self._lock: if self._end_time is not None: logger.warning("Setting attribute on ended span.") return self._attributes._set_items(attributes) # pylint: disable=protected-access def set_attribute(self, key: str, value: types.AnyValue) -> None: with self._lock: if self._end_time is not None: logger.warning("Setting attribute on ended span.") return self._attributes[key] = value @_check_span_ended def _add_event(self, event: EventBase) -> None: self._events.append(event) def add_event( self, name: str, attributes: types.Attributes = None, timestamp: int | None = None, ) -> None: # Immutable set to true so attributes cannot be added or removed after creation. attributes = BoundedAttributes( self._limits.max_event_attributes, attributes, max_value_len=self._limits.max_attribute_length, immutable=True, ) self._add_event( Event( name=name, attributes=attributes, timestamp=timestamp, ) ) @_check_span_ended def _add_link(self, link: trace_api.Link) -> None: self._links.append(link) def add_link( self, context: SpanContext, attributes: types.Attributes = None, ) -> None: if not _is_valid_link(context, attributes): return # Immutable set to true so attributes cannot be added or removed after creation. attributes = BoundedAttributes( self._limits.max_link_attributes, attributes, max_value_len=self._limits.max_attribute_length, immutable=True, ) self._add_link( trace_api.Link( context=context, attributes=attributes, ) ) def _readable_span(self) -> ReadableSpan: return ReadableSpan( name=self._name, context=self._context, parent=self._parent, resource=self._resource, attributes=self._attributes, events=self._events, links=self._links, kind=self.kind, status=self._status, start_time=self._start_time, end_time=self._end_time, instrumentation_info=self._instrumentation_info, instrumentation_scope=self._instrumentation_scope, ) def start( self, start_time: int | None = None, parent_context: context_api.Context | None = None, ) -> None: with self._lock: if self._start_time is not None: logger.warning("Calling start() on a started span.") return self._start_time = start_time if start_time is not None else time_ns() self._span_processor.on_start(self, parent_context=parent_context) def end(self, end_time: int | None = None) -> None: with self._lock: if self._start_time is None: raise RuntimeError("Calling end() on a not started span.") if self._end_time is not None: logger.warning("Calling end() on an ended span.") return self._end_time = end_time if end_time is not None else time_ns() self._attributes._immutable = True # pylint: disable=protected-access if self._record_end_metrics: self._record_end_metrics() # pylint: disable=protected-access self._span_processor._on_ending(self) self._span_processor.on_end(self._readable_span()) @_check_span_ended def update_name(self, name: str) -> None: self._name = name def is_recording(self) -> bool: return self._end_time is None @_check_span_ended def set_status( self, status: Status | StatusCode, description: str | None = None, ) -> None: # Ignore future calls if status is already set to OK # Ignore calls to set to StatusCode.UNSET if isinstance(status, Status): if self._status and self._status.status_code is StatusCode.OK or status.status_code is StatusCode.UNSET: return if description is not None: logger.warning( "Description %s ignored. Use either `Status` or `(StatusCode, Description)`", description, ) self._status = status elif isinstance(status, StatusCode): if self._status and self._status.status_code is StatusCode.OK or status is StatusCode.UNSET: return self._status = Status(status, description) def __exit__( self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: TracebackType | None, ) -> None: """Ends context manager and calls `end` on the `Span`.""" if exc_val is not None and self.is_recording(): # Record the exception as an event # pylint:disable=protected-access if self._record_exception: self.record_exception(exception=exc_val, escaped=True) # Records status if span is used as context manager # i.e. with tracer.start_span() as span: if self._set_status_on_exception: self.set_status( Status( status_code=StatusCode.ERROR, description=(f"{type(exc_val).__name__}: {exc_val}"), ) ) super().__exit__(exc_type, exc_val, exc_tb) def record_exception( self, exception: BaseException, attributes: types.Attributes = None, timestamp: int | None = None, escaped: bool = False, ) -> None: """Records an exception as a span event.""" stacktrace = "".join(traceback.format_exception(exception)) module = type(exception).__module__ qualname = type(exception).__qualname__ exception_type = f"{module}.{qualname}" if module and module != "builtins" else qualname _attributes: MutableMapping[str, types.AnyValue] = { EXCEPTION_TYPE: exception_type, EXCEPTION_MESSAGE: str(exception), EXCEPTION_STACKTRACE: stacktrace, EXCEPTION_ESCAPED: str(escaped), } if attributes: _attributes.update(attributes) self.add_event(name="exception", attributes=_attributes, timestamp=timestamp) class _Span(Span): """Protected implementation of `opentelemetry.trace.Span`. This constructor exists to prevent the instantiation of the `Span` class by other mechanisms than through the `Tracer`. """ @dataclass class _TracerConfig: is_enabled: bool @classmethod def default(cls): return cls(is_enabled=True) class Tracer(trace_api.Tracer): """See `opentelemetry.trace.Tracer`.""" def __init__( self, sampler: sampling.Sampler, resource: Resource, span_processor: SynchronousMultiSpanProcessor | ConcurrentMultiSpanProcessor, id_generator: IdGenerator, instrumentation_info: InstrumentationInfo, span_limits: SpanLimits, instrumentation_scope: InstrumentationScope, *, meter_provider: metrics_api.MeterProvider | None = None, _tracer_config: _TracerConfig | None = None, ) -> None: self.sampler = sampler self.resource = resource self.span_processor = span_processor self.id_generator = id_generator self.instrumentation_info = instrumentation_info self._span_limits = span_limits self._instrumentation_scope = instrumentation_scope self._tracer_config = _tracer_config or _TracerConfig.default() meter_provider = meter_provider or metrics_api.get_meter_provider() self._tracer_metrics = create_tracer_metrics( meter_provider, parse_boolean_environment_variable(OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED), ) def _set_tracer_config(self, tracer_config: _TracerConfig): self._tracer_config = tracer_config def _set_resource(self, resource: Resource) -> None: self.resource = resource def _is_enabled(self) -> bool: """If the tracer is not enabled, start_span will create a NonRecordingSpan""" return self._tracer_config.is_enabled @_agnosticcontextmanager # pylint: disable=protected-access def start_as_current_span( self, name: str, context: context_api.Context | None = None, kind: trace_api.SpanKind = trace_api.SpanKind.INTERNAL, attributes: types.Attributes = None, links: Sequence[trace_api.Link] | None = (), start_time: int | None = None, record_exception: bool = True, set_status_on_exception: bool = True, end_on_exit: bool = True, ) -> Iterator[trace_api.Span]: span = self.start_span( name=name, context=context, kind=kind, attributes=attributes, links=links, start_time=start_time, record_exception=record_exception, set_status_on_exception=set_status_on_exception, ) with trace_api.use_span( span, end_on_exit=end_on_exit, record_exception=record_exception, set_status_on_exception=set_status_on_exception, ) as span: yield span def start_span( # pylint: disable=too-many-locals self, name: str, context: context_api.Context | None = None, kind: trace_api.SpanKind = trace_api.SpanKind.INTERNAL, attributes: types.Attributes = None, links: Sequence[trace_api.Link] | None = (), start_time: int | None = None, record_exception: bool = True, set_status_on_exception: bool = True, ) -> trace_api.Span: links = links or () parent_span_context = trace_api.get_current_span(context).get_span_context() if parent_span_context is not None and not isinstance(parent_span_context, trace_api.SpanContext): raise TypeError("parent_span_context must be a SpanContext or None.") if not self._is_enabled(): return trace_api.NonRecordingSpan(context=parent_span_context) # is_valid determines root span if parent_span_context is None or not parent_span_context.is_valid: parent_span_context = None trace_id = self.id_generator.generate_trace_id() else: trace_id = parent_span_context.trace_id # The sampler decides whether to create a real or no-op span at the # time of span creation. No-op spans do not record events, and are not # exported. # The sampler may also add attributes to the newly-created span, e.g. # to include information about the sampling result. # The sampler may also modify the parent span context's tracestate sampling_result = self.sampler.should_sample(context, trace_id, name, kind, attributes, links) trace_flags = ( trace_api.TraceFlags(trace_api.TraceFlags.SAMPLED) if sampling_result.decision.is_sampled() else trace_api.TraceFlags(trace_api.TraceFlags.DEFAULT) ) if parent_span_context is None: random_trace_id = self.id_generator.is_trace_id_random() else: random_trace_id = parent_span_context.trace_flags.random_trace_id if random_trace_id: trace_flags = trace_api.TraceFlags(trace_flags | trace_api.TraceFlags.RANDOM_TRACE_ID) span_context = trace_api.SpanContext( trace_id, self.id_generator.generate_span_id(), is_remote=False, trace_flags=trace_flags, trace_state=sampling_result.trace_state, ) record_end_metrics = self._tracer_metrics.start_span(parent_span_context, sampling_result.decision) # Only record if is_recording() is true if sampling_result.decision.is_recording(): # pylint:disable=protected-access span = _Span( name=name, context=span_context, parent=parent_span_context, sampler=self.sampler, resource=self.resource, attributes=sampling_result.attributes, span_processor=self.span_processor, kind=kind, links=links, instrumentation_info=self.instrumentation_info, record_exception=record_exception, set_status_on_exception=set_status_on_exception, limits=self._span_limits, instrumentation_scope=self._instrumentation_scope, record_end_metrics=record_end_metrics, ) span.start(start_time=start_time, parent_context=context) else: span = trace_api.NonRecordingSpan(context=span_context) return span _TracerConfiguratorT = Callable[[InstrumentationScope], _TracerConfig] _RuleBasedTracerConfigurator = RuleBasedConfigurator[_TracerConfig] def _default_tracer_configurator( tracer_scope: InstrumentationScope, ) -> _TracerConfig: """Default Tracer Configurator implementation In order to update Tracers configs you need to call TracerProvider._set_tracer_configurator with a function implementing this interface returning a Tracer Config.""" return _RuleBasedTracerConfigurator( rules=[], default_config=_TracerConfig.default(), )(tracer_scope) def _disable_tracer_configurator( tracer_scope: InstrumentationScope, ) -> _TracerConfig: return _RuleBasedTracerConfigurator( rules=[], default_config=_TracerConfig(is_enabled=False), )(tracer_scope) class TracerProvider(trace_api.TracerProvider): """See `opentelemetry.trace.TracerProvider`.""" def __init__( self, sampler: sampling.Sampler | None = None, resource: Resource | None = None, shutdown_on_exit: bool = True, active_span_processor: SynchronousMultiSpanProcessor | ConcurrentMultiSpanProcessor | None = None, id_generator: IdGenerator | None = None, span_limits: SpanLimits | None = None, *, meter_provider: metrics_api.MeterProvider | None = None, _tracer_configurator: _TracerConfiguratorT | None = None, ) -> None: self._active_span_processor = active_span_processor or SynchronousMultiSpanProcessor() if id_generator is None: self.id_generator = RandomIdGenerator() else: self.id_generator = id_generator if resource is None: self._resource = Resource.create({}) else: self._resource = resource if not sampler: sampler = sampling._get_from_env_or_default() self.sampler = sampler self._span_limits = span_limits or SpanLimits() disabled = environ.get(OTEL_SDK_DISABLED, "") self._disabled = disabled.lower().strip() == "true" self._atexit_handler = None self._meter_provider = meter_provider if shutdown_on_exit: self._atexit_handler = atexit.register(self.shutdown) self._tracer_configurator = _tracer_configurator or _default_tracer_configurator self._tracers_lock = threading.Lock() self._tracers: dict[InstrumentationScope, Tracer] = {} if hasattr(os, "register_at_fork"): weak_at_fork = weakref.WeakMethod(self._handle_fork) def _after_in_child() -> None: if at_fork := weak_at_fork(): at_fork() os.register_at_fork(after_in_child=_after_in_child) def _handle_fork(self) -> None: self._tracers_lock = threading.Lock() self._update_resource(_get_process_dependent_resource()) def _set_tracer_configurator(self, *, tracer_configurator: _TracerConfiguratorT): """This is the function used to update the TracerProvider TracerConfigurator Setting a new TracerConfigurator for a TracerProvider will update the TracerConfig of all Tracers create by this TracerProvider. """ self._tracer_configurator = tracer_configurator with self._tracers_lock: for instrumentation_scope, tracer in self._tracers.items(): tracer_config = self._apply_tracer_configurator(instrumentation_scope) # pylint: disable-next=protected-access tracer._set_tracer_config(tracer_config) @property def resource(self) -> Resource: return self._resource def _update_resource(self, resource: Resource) -> None: with self._tracers_lock: self._resource = self._resource.merge(resource) for tracer in self._tracers.values(): tracer._set_resource(self._resource) # pylint: disable=protected-access def _apply_tracer_configurator(self, instrumentation_scope: InstrumentationScope): try: return self._tracer_configurator(instrumentation_scope) except Exception: # pylint: disable=broad-exception-caught logger.exception( "Failed to create a Tracer Config for %s, using default Tracer config", instrumentation_scope, ) return _TracerConfig.default() def get_tracer( self, instrumenting_module_name: str, instrumenting_library_version: str | None = None, schema_url: str | None = None, attributes: types.Attributes = None, ) -> "trace_api.Tracer": if self._disabled: return NoOpTracer() if not instrumenting_module_name: # Reject empty strings too. instrumenting_module_name = "" logger.error("get_tracer called with missing module name.") if instrumenting_library_version is None: instrumenting_library_version = "" filterwarnings( "ignore", message=(r"You should use InstrumentationScope. Deprecated since version 1.11.1."), category=DeprecationWarning, module="opentelemetry.sdk.trace", ) instrumentation_info = InstrumentationInfo( instrumenting_module_name, instrumenting_library_version, schema_url, ) instrumentation_scope = InstrumentationScope( instrumenting_module_name, instrumenting_library_version, schema_url, attributes, ) with self._tracers_lock: if instrumentation_scope in self._tracers: return self._tracers[instrumentation_scope] tracer_config = self._apply_tracer_configurator(instrumentation_scope) tracer = Tracer( self.sampler, self.resource, self._active_span_processor, self.id_generator, instrumentation_info, self._span_limits, instrumentation_scope, meter_provider=self._meter_provider, _tracer_config=tracer_config, ) self._tracers[instrumentation_scope] = tracer return tracer def add_span_processor(self, span_processor: SpanProcessor) -> None: """Registers a new :class:`SpanProcessor` for this `TracerProvider`. The span processors are invoked in the same order they are registered. """ # no lock here because add_span_processor is thread safe for both # SynchronousMultiSpanProcessor and ConcurrentMultiSpanProcessor. self._active_span_processor.add_span_processor(span_processor) def shutdown(self) -> None: """Shut down the span processors added to the tracer provider.""" self._active_span_processor.shutdown() if self._atexit_handler is not None: atexit.unregister(self._atexit_handler) self._atexit_handler = None def force_flush(self, timeout_millis: int = 30000) -> bool: """Requests the active span processor to process all spans that have not yet been processed. By default force flush is called sequentially on all added span processors. This means that span processors further back in the list have less time to flush their spans. To have span processors flush their spans in parallel it is possible to initialize the tracer provider with an instance of `ConcurrentMultiSpanProcessor` at the cost of using multiple threads. Args: timeout_millis: The maximum amount of time to wait for spans to be processed. Returns: False if the timeout is exceeded, True otherwise. """ return self._active_span_processor.force_flush(timeout_millis) python-opentelemetry-1.45.2/opentelemetry-sdk/src/opentelemetry/sdk/trace/_sampling_experimental/000077500000000000000000000000001527070070700335665ustar00rootroot00000000000000__init__.py000066400000000000000000000012501527070070700356160ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-sdk/src/opentelemetry/sdk/trace/_sampling_experimental# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 __all__ = [ "ComposableSampler", "SamplingIntent", "composable_always_off", "composable_always_on", "composable_parent_threshold", "composable_rule_based", "composable_traceid_ratio_based", "composite_sampler", ] from ._always_off import composable_always_off from ._always_on import composable_always_on from ._composable import ComposableSampler, SamplingIntent from ._parent_threshold import composable_parent_threshold from ._rule_based import composable_rule_based from ._sampler import composite_sampler from ._traceid_ratio import composable_traceid_ratio_based _always_off.py000066400000000000000000000023671527070070700363620ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-sdk/src/opentelemetry/sdk/trace/_sampling_experimental# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations from collections.abc import Sequence from opentelemetry.context import Context from opentelemetry.trace import Link, SpanKind, TraceState from opentelemetry.util.types import Attributes from ._composable import ComposableSampler, SamplingIntent from ._util import INVALID_THRESHOLD _intent = SamplingIntent(threshold=INVALID_THRESHOLD, threshold_reliable=False) class _ComposableAlwaysOffSampler(ComposableSampler): def sampling_intent( self, parent_ctx: Context | None, name: str, span_kind: SpanKind | None, attributes: Attributes, links: Sequence[Link] | None, trace_state: TraceState | None = None, ) -> SamplingIntent: return _intent def get_description(self) -> str: return "ComposableAlwaysOff" _always_off = _ComposableAlwaysOffSampler() def composable_always_off() -> ComposableSampler: """Returns a composable sampler that does not sample any span. - Always returns a SamplingIntent with no threshold, indicating all spans should be dropped - Sets threshold_reliable to false - Does not add any attributes """ return _always_off _always_on.py000066400000000000000000000023041527070070700362130ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-sdk/src/opentelemetry/sdk/trace/_sampling_experimental# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations from collections.abc import Sequence from opentelemetry.context import Context from opentelemetry.trace import Link, SpanKind, TraceState from opentelemetry.util.types import Attributes from ._composable import ComposableSampler, SamplingIntent from ._util import MIN_THRESHOLD _intent = SamplingIntent(threshold=MIN_THRESHOLD) class _ComposableAlwaysOnSampler(ComposableSampler): def sampling_intent( self, parent_ctx: Context | None, name: str, span_kind: SpanKind | None, attributes: Attributes, links: Sequence[Link] | None, trace_state: TraceState | None = None, ) -> SamplingIntent: return _intent def get_description(self) -> str: return "ComposableAlwaysOn" _always_on = _ComposableAlwaysOnSampler() def composable_always_on() -> ComposableSampler: """Returns a composable sampler that samples all spans. - Always returns a SamplingIntent with threshold set to sample all spans (threshold = 0) - Sets threshold_reliable to true - Does not add any attributes """ return _always_on _composable.py000066400000000000000000000031321527070070700363430ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-sdk/src/opentelemetry/sdk/trace/_sampling_experimental# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations from collections.abc import Callable, Sequence from dataclasses import dataclass, field from typing import Protocol from opentelemetry.context import Context from opentelemetry.trace import Link, SpanKind, TraceState from opentelemetry.util.types import Attributes @dataclass(frozen=True) class SamplingIntent: """Information to make a consistent sampling decision.""" threshold: int """The sampling threshold value. A lower threshold increases the likelihood of sampling.""" threshold_reliable: bool = field(default=True) """Indicates whether the threshold is reliable for Span-to-Metrics estimation.""" attributes: Attributes = field(default=None) """Any attributes to be added to a sampled span.""" update_trace_state: Callable[[TraceState], TraceState] = field(default=lambda ts: ts) """Any updates to be made to trace state.""" class ComposableSampler(Protocol): """A sampler that can be composed to make a final sampling decision.""" def sampling_intent( self, parent_ctx: Context | None, name: str, span_kind: SpanKind | None, attributes: Attributes, links: Sequence[Link] | None, trace_state: TraceState | None, ) -> SamplingIntent: """Returns information to make a sampling decision.""" ... # pylint: disable=unnecessary-ellipsis def get_description(self) -> str: """Returns a description of the sampler.""" ... # pylint: disable=unnecessary-ellipsis _parent_threshold.py000066400000000000000000000053111527070070700375650ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-sdk/src/opentelemetry/sdk/trace/_sampling_experimental# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations from collections.abc import Sequence from opentelemetry.context import Context from opentelemetry.trace import Link, SpanKind, TraceState, get_current_span from opentelemetry.util.types import Attributes from ._composable import ComposableSampler, SamplingIntent from ._trace_state import OtelTraceState from ._util import ( INVALID_THRESHOLD, MIN_THRESHOLD, is_valid_threshold, ) class _ComposableParentThreshold(ComposableSampler): def __init__(self, root_sampler: ComposableSampler): self._root_sampler = root_sampler self._description = f"ComposableParentThreshold{{root={root_sampler.get_description()}}}" def sampling_intent( self, parent_ctx: Context | None, name: str, span_kind: SpanKind | None, attributes: Attributes, links: Sequence[Link] | None, trace_state: TraceState | None = None, ) -> SamplingIntent: parent_span = get_current_span(parent_ctx) parent_span_ctx = parent_span.get_span_context() is_root = not parent_span_ctx.is_valid if is_root: return self._root_sampler.sampling_intent(parent_ctx, name, span_kind, attributes, links, trace_state) ot_trace_state = OtelTraceState.parse(trace_state) if is_valid_threshold(ot_trace_state.threshold): return SamplingIntent( threshold=ot_trace_state.threshold, threshold_reliable=True, ) threshold = MIN_THRESHOLD if parent_span_ctx.trace_flags.sampled else INVALID_THRESHOLD return SamplingIntent(threshold=threshold, threshold_reliable=False) def get_description(self) -> str: return self._description def composable_parent_threshold( root_sampler: ComposableSampler, ) -> ComposableSampler: """Returns a consistent sampler that respects the sampling decision of the parent span or falls-back to the given sampler if it is a root span. - For spans without a parent context, delegate to the root sampler - For spans with a parent context, returns a SamplingIntent that propagates the parent's sampling decision - Returns the parent's threshold if available; otherwise, if the parent's sampled flag is set, returns threshold=0; otherwise, if the parent's sampled flag is not set, no threshold is returned. - Sets threshold_reliable to match the parent’s reliability, which is true if the parent had a threshold. - Does not add any attributes Args: root_sampler: The root sampler to use for spans without a parent context. """ return _ComposableParentThreshold(root_sampler) _rule_based.py000066400000000000000000000172701527070070700363340ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-sdk/src/opentelemetry/sdk/trace/_sampling_experimental# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations import logging from collections.abc import Sequence from fnmatch import fnmatchcase from typing import Protocol from opentelemetry.context import Context from opentelemetry.trace import ( Link, SpanKind, TraceState, get_current_span, ) from opentelemetry.util.types import AnyValue, Attributes from ._composable import ComposableSampler, SamplingIntent from ._util import INVALID_THRESHOLD class PredicateT(Protocol): def __call__( self, parent_ctx: Context | None, name: str, span_kind: SpanKind | None, attributes: Attributes, links: Sequence[Link] | None, trace_state: TraceState | None, ) -> bool: ... def __str__(self) -> str: ... class AttributePredicate: """An exact match of an attribute value""" def __init__(self, key: str, value: AnyValue): logging.warning("This is deprecated, use AttributeValuesPredicate instead") self.key = key self.value = value def __call__( self, parent_ctx: Context | None, name: str, span_kind: SpanKind | None, attributes: Attributes, links: Sequence[Link] | None, trace_state: TraceState | None, ) -> bool: if not attributes: return False return attributes.get(self.key) == self.value def __str__(self): return f"{self.key}={self.value}" class AlwaysMatchPredicate: def __call__( self, parent_ctx: Context | None, name: str, span_kind: SpanKind | None, attributes: Attributes, links: Sequence[Link] | None, trace_state: TraceState | None, ) -> bool: return True def __str__(self) -> str: return "AlwaysMatch" class AllPredicate: def __init__(self, predicates: Sequence[PredicateT]): self._predicates = tuple(predicates) def __call__( self, parent_ctx: Context | None, name: str, span_kind: SpanKind | None, attributes: Attributes, links: Sequence[Link] | None, trace_state: TraceState | None, ) -> bool: return all( predicate( parent_ctx, name, span_kind, attributes, links, trace_state, ) for predicate in self._predicates ) def __str__(self) -> str: return " && ".join(str(predicate) for predicate in self._predicates) class AttributeValuesPredicate: def __init__(self, key: str, values: Sequence[str]): self._key = key self._values = frozenset(values) def __call__( self, parent_ctx: Context | None, name: str, span_kind: SpanKind | None, attributes: Attributes, links: Sequence[Link] | None, trace_state: TraceState | None, ) -> bool: if not attributes or self._key not in attributes: return False return any(str(value) in self._values for value in _attribute_values(attributes[self._key])) def __str__(self) -> str: values = ",".join(sorted(self._values)) return f"{self._key} in [{values}]" class AttributePatternsPredicate: def __init__( self, key: str, included: Sequence[str] | None = None, excluded: Sequence[str] | None = None, ): self._key = key self._included = tuple(included or ()) self._excluded = tuple(excluded or ()) def __call__( self, parent_ctx: Context | None, name: str, span_kind: SpanKind | None, attributes: Attributes, links: Sequence[Link] | None, trace_state: TraceState | None, ) -> bool: if not attributes or self._key not in attributes: return False return any(self._matches_value(str(value)) for value in _attribute_values(attributes[self._key])) def _matches_value(self, value: str) -> bool: included = not self._included or any(fnmatchcase(value, pattern) for pattern in self._included) excluded = any(fnmatchcase(value, pattern) for pattern in self._excluded) return included and not excluded def __str__(self) -> str: return f"{self._key} matches" class SpanKindPredicate: def __init__(self, span_kinds: Sequence[SpanKind]): self._span_kinds = frozenset(span_kinds) def __call__( self, parent_ctx: Context | None, name: str, span_kind: SpanKind | None, attributes: Attributes, links: Sequence[Link] | None, trace_state: TraceState | None, ) -> bool: return span_kind in self._span_kinds def __str__(self) -> str: kinds = ",".join(kind.name.lower() for kind in self._span_kinds) return f"span_kind in [{kinds}]" class ParentPredicate: def __init__(self, parents: Sequence[str]): self._parents = frozenset(parents) def __call__( self, parent_ctx: Context | None, name: str, span_kind: SpanKind | None, attributes: Attributes, links: Sequence[Link] | None, trace_state: TraceState | None, ) -> bool: parent_span_context = get_current_span(parent_ctx).get_span_context() if not parent_span_context.is_valid: parent = "none" elif parent_span_context.is_remote: parent = "remote" else: parent = "local" return parent in self._parents def __str__(self) -> str: parents = ",".join(self._parents) return f"parent in [{parents}]" def _attribute_values(value): if isinstance(value, Sequence) and not isinstance(value, (str, bytes, bytearray)): return value return (value,) RulesT = Sequence[tuple[PredicateT, ComposableSampler]] _non_sampling_intent = SamplingIntent(threshold=INVALID_THRESHOLD, threshold_reliable=False) class _ComposableRuleBased(ComposableSampler): def __init__(self, rules: RulesT): # work on an internal copy of the rules self._rules = list(rules) def sampling_intent( self, parent_ctx: Context | None, name: str, span_kind: SpanKind | None, attributes: Attributes, links: Sequence[Link] | None, trace_state: TraceState | None = None, ) -> SamplingIntent: for predicate, sampler in self._rules: if predicate( parent_ctx=parent_ctx, name=name, span_kind=span_kind, attributes=attributes, links=links, trace_state=trace_state, ): return sampler.sampling_intent( parent_ctx=parent_ctx, name=name, span_kind=span_kind, attributes=attributes, links=links, trace_state=trace_state, ) return _non_sampling_intent def get_description(self) -> str: rules_str = ",".join(f"({predicate}:{sampler.get_description()})" for predicate, sampler in self._rules) return f"ComposableRuleBased{{[{rules_str}]}}" def composable_rule_based( rules: RulesT, ) -> ComposableSampler: """Returns a consistent sampler that: - Evaluates a series of rules based on predicates and returns the SamplingIntent from the first matching sampler - If no rules match, returns a non-sampling intent Args: rules: A list of (Predicate, ComposableSampler) pairs, where Predicate is a function that evaluates whether a rule applies """ return _ComposableRuleBased(rules) _sampler.py000066400000000000000000000056731527070070700356760ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-sdk/src/opentelemetry/sdk/trace/_sampling_experimental# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations from collections.abc import Sequence from opentelemetry.context import Context from opentelemetry.sdk.trace.sampling import Decision, Sampler, SamplingResult from opentelemetry.trace import Link, SpanKind, TraceState from opentelemetry.util.types import Attributes from ._composable import ComposableSampler, SamplingIntent from ._trace_state import OTEL_TRACE_STATE_KEY, OtelTraceState from ._util import INVALID_THRESHOLD, is_valid_random_value, is_valid_threshold class _CompositeSampler(Sampler): def __init__(self, delegate: ComposableSampler): self._delegate = delegate def should_sample( self, parent_context: Context | None, trace_id: int, name: str, kind: SpanKind | None = None, attributes: Attributes = None, links: Sequence[Link] | None = None, trace_state: TraceState | None = None, ) -> SamplingResult: ot_trace_state = OtelTraceState.parse(trace_state) intent = self._delegate.sampling_intent(parent_context, name, kind, attributes, links, trace_state) threshold = intent.threshold if is_valid_threshold(threshold): adjusted_count_correct = intent.threshold_reliable if is_valid_random_value(ot_trace_state.random_value): randomness = ot_trace_state.random_value else: # Use last 56 bits of trace_id as randomness randomness = trace_id & 0x00FFFFFFFFFFFFFF sampled = threshold <= randomness else: sampled = False adjusted_count_correct = False decision = Decision.RECORD_AND_SAMPLE if sampled else Decision.DROP if sampled and adjusted_count_correct: ot_trace_state.threshold = threshold else: ot_trace_state.threshold = INVALID_THRESHOLD return SamplingResult( decision, intent.attributes, _update_trace_state(trace_state, ot_trace_state, intent), ) def get_description(self) -> str: return self._delegate.get_description() def _update_trace_state( trace_state: TraceState | None, ot_trace_state: OtelTraceState, intent: SamplingIntent, ) -> TraceState | None: otts = ot_trace_state.serialize() if not trace_state: if otts: return TraceState(((OTEL_TRACE_STATE_KEY, otts),)) return None new_trace_state = intent.update_trace_state(trace_state) if otts: return new_trace_state.update(OTEL_TRACE_STATE_KEY, otts) return new_trace_state def composite_sampler(delegate: ComposableSampler) -> Sampler: """A sampler that uses a a composable sampler to make its decision while handling tracestate. Args: delegate: The composable sampler to use for making sampling decisions. """ return _CompositeSampler(delegate) _trace_state.py000066400000000000000000000066221527070070700365240ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-sdk/src/opentelemetry/sdk/trace/_sampling_experimental# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations from collections.abc import Sequence from dataclasses import dataclass from opentelemetry.trace import TraceState from ._util import ( INVALID_RANDOM_VALUE, INVALID_THRESHOLD, MAX_THRESHOLD, is_valid_random_value, is_valid_threshold, ) OTEL_TRACE_STATE_KEY = "ot" _TRACE_STATE_SIZE_LIMIT = 256 _MAX_VALUE_LENGTH = 14 # 56 bits, 4 bits per hex digit @dataclass class OtelTraceState: """Marshals OpenTelemetry tracestate for sampling parameters. https://opentelemetry.io/docs/specs/otel/trace/tracestate-probability-sampling/ """ random_value: int threshold: int rest: Sequence[str] @staticmethod def invalid() -> OtelTraceState: return OtelTraceState(INVALID_RANDOM_VALUE, INVALID_THRESHOLD, ()) @staticmethod def parse(trace_state: TraceState | None) -> OtelTraceState: if not trace_state: return OtelTraceState.invalid() ot = trace_state.get(OTEL_TRACE_STATE_KEY, "") if not ot or len(ot) > _TRACE_STATE_SIZE_LIMIT: return OtelTraceState.invalid() threshold = INVALID_THRESHOLD random_value = INVALID_RANDOM_VALUE members = ot.split(";") rest: list[str] | None = None for member in members: if member.startswith("th:"): threshold = _parse_th(member[len("th:") :], INVALID_THRESHOLD) continue if member.startswith("rv:"): random_value = _parse_rv(member[len("rv:") :], INVALID_RANDOM_VALUE) continue if rest is None: rest = [member] else: rest.append(member) return OtelTraceState(random_value=random_value, threshold=threshold, rest=rest or ()) def serialize(self) -> str: if not is_valid_threshold(self.threshold) and not is_valid_random_value(self.random_value) and not self.rest: return "" parts: list[str] = [] if is_valid_threshold(self.threshold) and self.threshold != MAX_THRESHOLD: parts.append(f"th:{serialize_th(self.threshold)}") if is_valid_random_value(self.random_value): parts.append(f"rv:{_serialize_rv(self.random_value)}") if self.rest: parts.extend(self.rest) res = ";".join(parts) while len(res) > _TRACE_STATE_SIZE_LIMIT: delim_idx = res.rfind(";") if delim_idx == -1: break res = res[:delim_idx] return res def _parse_th(value: str, default: int) -> int: if not value or len(value) > _MAX_VALUE_LENGTH: return default try: parsed = int(value, 16) except ValueError: return default # th value is compressed by removing all trailing zeros, # so we restore them to get the real value. trailing_zeros = _MAX_VALUE_LENGTH - len(value) return parsed << (trailing_zeros * 4) def _parse_rv(value: str, default: int) -> int: if not value or len(value) != _MAX_VALUE_LENGTH: return default try: return int(value, 16) except ValueError: return default def serialize_th(threshold: int) -> str: if not threshold: return "0" return f"{threshold:014x}".rstrip("0") def _serialize_rv(random_value: int) -> str: return f"{random_value:014x}" _traceid_ratio.py000066400000000000000000000041421527070070700370320ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-sdk/src/opentelemetry/sdk/trace/_sampling_experimental# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations from collections.abc import Sequence from opentelemetry.context import Context from opentelemetry.trace import Link, SpanKind, TraceState from opentelemetry.util.types import Attributes from ._composable import ComposableSampler, SamplingIntent from ._trace_state import serialize_th from ._util import INVALID_THRESHOLD, MAX_THRESHOLD, calculate_threshold class ComposableTraceIDRatioBased(ComposableSampler): _threshold: int _description: str def __init__(self, ratio: float): threshold = calculate_threshold(ratio) if threshold == MAX_THRESHOLD: threshold_str = "max" else: threshold_str = serialize_th(threshold) if threshold != MAX_THRESHOLD: intent = SamplingIntent(threshold=threshold) else: intent = SamplingIntent(threshold=INVALID_THRESHOLD, threshold_reliable=False) self._intent = intent self._description = f"ComposableTraceIDRatioBased{{threshold={threshold_str}, ratio={ratio}}}" def sampling_intent( self, parent_ctx: Context | None, name: str, span_kind: SpanKind | None, attributes: Attributes, links: Sequence[Link] | None, trace_state: TraceState | None, ) -> SamplingIntent: return self._intent def get_description(self) -> str: return self._description def composable_traceid_ratio_based( ratio: float, ) -> ComposableSampler: """Returns a composable sampler that samples each span with a fixed ratio. - Returns a SamplingIntent with threshold determined by the configured sampling ratio - Sets threshold_reliable to true - Does not add any attributes Note: If the ratio is 0, it will behave as an ComposableAlwaysOff sampler instead. Args: ratio: The sampling ratio to use (between 0.0 and 1.0). """ if not 0.0 <= ratio <= 1.0: raise ValueError("Sampling ratio must be between 0.0 and 1.0") return ComposableTraceIDRatioBased(ratio) _util.py000066400000000000000000000012621527070070700351760ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-sdk/src/opentelemetry/sdk/trace/_sampling_experimental# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 RANDOM_VALUE_BITS = 56 MAX_THRESHOLD = 1 << RANDOM_VALUE_BITS # 0% sampling MIN_THRESHOLD = 0 # 100% sampling MAX_RANDOM_VALUE = MAX_THRESHOLD - 1 INVALID_THRESHOLD = -1 INVALID_RANDOM_VALUE = -1 _probability_threshold_scale = float.fromhex("0x1p56") def calculate_threshold(sampling_probability: float) -> int: return MAX_THRESHOLD - round(sampling_probability * _probability_threshold_scale) def is_valid_threshold(threshold: int) -> bool: return MIN_THRESHOLD <= threshold <= MAX_THRESHOLD def is_valid_random_value(random_value: int) -> bool: return 0 <= random_value <= MAX_RANDOM_VALUE python-opentelemetry-1.45.2/opentelemetry-sdk/src/opentelemetry/sdk/trace/_tracer_metrics.py000066400000000000000000000054531527070070700325660ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations from collections.abc import Callable from typing import Protocol from opentelemetry import metrics as metrics_api from opentelemetry.sdk.trace.sampling import Decision from opentelemetry.semconv._incubating.attributes.otel_attributes import ( OTEL_SPAN_PARENT_ORIGIN, OTEL_SPAN_SAMPLING_RESULT, OtelSpanSamplingResultValues, ) from opentelemetry.semconv._incubating.metrics.otel_metrics import ( create_otel_sdk_span_live, create_otel_sdk_span_started, ) from opentelemetry.trace.span import SpanContext class TracerMetricsT(Protocol): def start_span( self, parent_span_context: SpanContext | None, sampling_decision: Decision, ) -> Callable[[], None]: ... # pylint: disable=no-self-use class NoOpTracerMetrics: def start_span( self, parent_span_context: SpanContext | None, sampling_decision: Decision, ) -> Callable[[], None]: return noop class TracerMetrics: def __init__(self, meter_provider: metrics_api.MeterProvider) -> None: meter = meter_provider.get_meter("opentelemetry-sdk") self._started_spans = create_otel_sdk_span_started(meter) self._live_spans = create_otel_sdk_span_live(meter) def start_span( self, parent_span_context: SpanContext | None, sampling_decision: Decision, ) -> Callable[[], None]: sampling_result_value = sampling_result(sampling_decision) self._started_spans.add( 1, { OTEL_SPAN_PARENT_ORIGIN: parent_origin(parent_span_context), OTEL_SPAN_SAMPLING_RESULT: sampling_result_value, }, ) if not sampling_decision.is_recording(): return noop live_span_attrs = { OTEL_SPAN_SAMPLING_RESULT: sampling_result_value, } self._live_spans.add(1, live_span_attrs) def end_span() -> None: self._live_spans.add(-1, live_span_attrs) return end_span def create_tracer_metrics( meter_provider: metrics_api.MeterProvider, enabled: bool, ) -> TracerMetricsT: if not enabled: return NoOpTracerMetrics() return TracerMetrics(meter_provider) def noop() -> None: pass def parent_origin(span_ctx: SpanContext | None) -> str: if span_ctx is None: return "none" if span_ctx.is_remote: return "remote" return "local" def sampling_result(decision: Decision) -> str: if decision == Decision.RECORD_AND_SAMPLE: return OtelSpanSamplingResultValues.RECORD_AND_SAMPLE.value if decision == Decision.RECORD_ONLY: return OtelSpanSamplingResultValues.RECORD_ONLY.value return OtelSpanSamplingResultValues.DROP.value python-opentelemetry-1.45.2/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/000077500000000000000000000000001527070070700303615ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py000066400000000000000000000246621527070070700325040ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations import collections.abc import logging import sys import typing from enum import Enum from os import environ, linesep from opentelemetry.context import ( _SUPPRESS_INSTRUMENTATION_KEY, Context, attach, detach, set_value, ) from opentelemetry.metrics import MeterProvider, get_meter_provider from opentelemetry.sdk._shared_internal import ( BatchProcessor, ) from opentelemetry.sdk._shared_internal._processor_metrics import ( create_processor_metrics, ) from opentelemetry.sdk.environment_variables import ( OTEL_BSP_EXPORT_TIMEOUT, OTEL_BSP_MAX_EXPORT_BATCH_SIZE, OTEL_BSP_MAX_QUEUE_SIZE, OTEL_BSP_SCHEDULE_DELAY, OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED, ) from opentelemetry.sdk.environment_variables._internal import ( parse_boolean_environment_variable, ) from opentelemetry.sdk.trace import ReadableSpan, Span, SpanProcessor from opentelemetry.semconv._incubating.attributes.otel_attributes import ( OtelComponentTypeValues, ) _DEFAULT_SCHEDULE_DELAY_MILLIS = 5000 _DEFAULT_MAX_EXPORT_BATCH_SIZE = 512 _DEFAULT_EXPORT_TIMEOUT_MILLIS = 30000 _DEFAULT_MAX_QUEUE_SIZE = 2048 _ENV_VAR_INT_VALUE_ERROR_MESSAGE = "Unable to parse value for %s as integer. Defaulting to %s." logger = logging.getLogger(__name__) class SpanExportResult(Enum): SUCCESS = 0 FAILURE = 1 class SpanExporter: """Interface for exporting spans. Interface to be implemented by services that want to export spans recorded in their own format. To export data this MUST be registered to the :class`opentelemetry.sdk.trace.Tracer` using a `SimpleSpanProcessor` or a `BatchSpanProcessor`. """ def export(self, spans: collections.abc.Sequence[ReadableSpan]) -> SpanExportResult: # pyright: ignore[reportReturnType] """Exports a batch of telemetry data. Args: spans: The list of `opentelemetry.trace.Span` objects to be exported Returns: The result of the export """ def shutdown(self) -> None: """Shuts down the exporter. Called when the SDK is shut down. """ def force_flush(self, timeout_millis: int = 30000) -> bool: # pyright: ignore[reportReturnType] """Hint to ensure that the export of any spans the exporter has received prior to the call to ForceFlush SHOULD be completed as soon as possible, preferably before returning from this method. """ class SimpleSpanProcessor(SpanProcessor): """Simple SpanProcessor implementation. SimpleSpanProcessor is an implementation of `SpanProcessor` that passes ended spans directly to the configured `SpanExporter`. """ def __init__( self, span_exporter: SpanExporter, *, meter_provider: MeterProvider | None = None, ): self.span_exporter = span_exporter self._shutdown = False self._metrics = create_processor_metrics( "traces", OtelComponentTypeValues.SIMPLE_SPAN_PROCESSOR, meter_provider or get_meter_provider(), enabled=parse_boolean_environment_variable(OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED), ) def on_start(self, span: Span, parent_context: Context | None = None) -> None: pass def _on_ending(self, span: Span) -> None: pass def on_end(self, span: ReadableSpan) -> None: if not (span.context and span.context.trace_flags.sampled): return if self._shutdown: logger.warning("Processor is already shutdown, ignoring call") self._metrics.drop_items(1, "already_shutdown") return token = attach(set_value(_SUPPRESS_INSTRUMENTATION_KEY, True)) # Record on submission to the exporter. self._metrics.finish_items(1) try: self.span_exporter.export((span,)) # pylint: disable=broad-exception-caught except Exception: logger.exception("Exception while exporting Span.") finally: detach(token) def shutdown(self) -> None: self._shutdown = True self.span_exporter.shutdown() def force_flush(self, timeout_millis: int = 30000) -> bool: # pylint: disable=unused-argument return True class BatchSpanProcessor(SpanProcessor): """Batch span processor implementation. `BatchSpanProcessor` is an implementation of `SpanProcessor` that batches ended spans and pushes them to the configured `SpanExporter`. `BatchSpanProcessor` is configurable with the following environment variables which correspond to constructor parameters: - :envvar:`OTEL_BSP_SCHEDULE_DELAY` - :envvar:`OTEL_BSP_MAX_QUEUE_SIZE` - :envvar:`OTEL_BSP_MAX_EXPORT_BATCH_SIZE` - :envvar:`OTEL_BSP_EXPORT_TIMEOUT` All the logic for emitting spans, shutting down etc. resides in the `BatchProcessor` class. """ def __init__( self, span_exporter: SpanExporter, max_queue_size: int | None = None, schedule_delay_millis: float | None = None, max_export_batch_size: int | None = None, export_timeout_millis: float | None = None, *, meter_provider: MeterProvider | None = None, ): if max_queue_size is None: max_queue_size = BatchSpanProcessor._default_max_queue_size() if schedule_delay_millis is None: schedule_delay_millis = BatchSpanProcessor._default_schedule_delay_millis() if max_export_batch_size is None: max_export_batch_size = BatchSpanProcessor._default_max_export_batch_size() # Not used. No way currently to pass timeout to export. if export_timeout_millis is None: export_timeout_millis = BatchSpanProcessor._default_export_timeout_millis() BatchSpanProcessor._validate_arguments(max_queue_size, schedule_delay_millis, max_export_batch_size) self._batch_processor = BatchProcessor( span_exporter, schedule_delay_millis, max_export_batch_size, export_timeout_millis, max_queue_size, "Span", create_processor_metrics( "traces", OtelComponentTypeValues.BATCHING_SPAN_PROCESSOR, meter_provider or get_meter_provider(), capacity=max_queue_size, enabled=parse_boolean_environment_variable(OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED), ), ) # Added for backward compatibility. Not recommended to directly access/use underlying exporter. @property def span_exporter(self): return self._batch_processor._exporter # pylint: disable=protected-access def on_start(self, span: Span, parent_context: Context | None = None) -> None: pass def _on_ending(self, span: Span) -> None: pass def on_end(self, span: ReadableSpan) -> None: if not (span.context and span.context.trace_flags.sampled): return self._batch_processor.emit(span) def shutdown(self): return self._batch_processor.shutdown() def force_flush(self, timeout_millis: int | None = None) -> bool: return self._batch_processor.force_flush(timeout_millis) @staticmethod def _default_max_queue_size(): try: return int(environ.get(OTEL_BSP_MAX_QUEUE_SIZE, _DEFAULT_MAX_QUEUE_SIZE)) except ValueError: logger.exception( _ENV_VAR_INT_VALUE_ERROR_MESSAGE, OTEL_BSP_MAX_QUEUE_SIZE, _DEFAULT_MAX_QUEUE_SIZE, ) return _DEFAULT_MAX_QUEUE_SIZE @staticmethod def _default_schedule_delay_millis(): try: return int(environ.get(OTEL_BSP_SCHEDULE_DELAY, _DEFAULT_SCHEDULE_DELAY_MILLIS)) except ValueError: logger.exception( _ENV_VAR_INT_VALUE_ERROR_MESSAGE, OTEL_BSP_SCHEDULE_DELAY, _DEFAULT_SCHEDULE_DELAY_MILLIS, ) return _DEFAULT_SCHEDULE_DELAY_MILLIS @staticmethod def _default_max_export_batch_size(): try: return int( environ.get( OTEL_BSP_MAX_EXPORT_BATCH_SIZE, _DEFAULT_MAX_EXPORT_BATCH_SIZE, ) ) except ValueError: logger.exception( _ENV_VAR_INT_VALUE_ERROR_MESSAGE, OTEL_BSP_MAX_EXPORT_BATCH_SIZE, _DEFAULT_MAX_EXPORT_BATCH_SIZE, ) return _DEFAULT_MAX_EXPORT_BATCH_SIZE @staticmethod def _default_export_timeout_millis(): try: return int(environ.get(OTEL_BSP_EXPORT_TIMEOUT, _DEFAULT_EXPORT_TIMEOUT_MILLIS)) except ValueError: logger.exception( _ENV_VAR_INT_VALUE_ERROR_MESSAGE, OTEL_BSP_EXPORT_TIMEOUT, _DEFAULT_EXPORT_TIMEOUT_MILLIS, ) return _DEFAULT_EXPORT_TIMEOUT_MILLIS @staticmethod def _validate_arguments(max_queue_size, schedule_delay_millis, max_export_batch_size): if max_queue_size <= 0: raise ValueError("max_queue_size must be a positive integer.") if schedule_delay_millis <= 0: raise ValueError("schedule_delay_millis must be positive.") if max_export_batch_size <= 0: raise ValueError("max_export_batch_size must be a positive integer.") if max_export_batch_size > max_queue_size: raise ValueError("max_export_batch_size must be less than or equal to max_queue_size.") class ConsoleSpanExporter(SpanExporter): """Implementation of :class:`SpanExporter` that prints spans to the console. This class can be used for diagnostic purposes. It prints the exported spans to the console STDOUT. """ def __init__( self, service_name: str | None = None, out: typing.IO = sys.stdout, formatter: collections.abc.Callable[[ReadableSpan], str] = lambda span: span.to_json() + linesep, ): self.out = out self.formatter = formatter self.service_name = service_name def export(self, spans: collections.abc.Sequence[ReadableSpan]) -> SpanExportResult: for span in spans: self.out.write(self.formatter(span)) self.out.flush() return SpanExportResult.SUCCESS def force_flush(self, timeout_millis: int = 30000) -> bool: return True in_memory_span_exporter.py000066400000000000000000000035561527070070700356340ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-sdk/src/opentelemetry/sdk/trace/export# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import collections.abc import threading from collections import deque from opentelemetry.sdk.trace import ReadableSpan from opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult class InMemorySpanExporter(SpanExporter): """Implementation of :class:`.SpanExporter` that stores spans in memory. This class can be used for testing purposes. It stores the exported spans in a deque in memory that can be retrieved using the :func:`.get_finished_spans` method. Args: max_spans: Maximum number of spans to store in memory. When the limit is reached, the oldest spans are dropped automatically. If ``None`` (default), there is no limit. """ def __init__(self, max_spans: int | None = None) -> None: self._finished_spans: deque[ReadableSpan] = deque(maxlen=max_spans) self._stopped = False self._lock = threading.Lock() def clear(self) -> None: """Clear list of collected spans.""" with self._lock: self._finished_spans.clear() def get_finished_spans(self) -> tuple[ReadableSpan, ...]: """Get list of collected spans.""" with self._lock: return tuple(self._finished_spans) def export(self, spans: collections.abc.Sequence[ReadableSpan]) -> SpanExportResult: """Stores a list of spans in memory.""" if self._stopped: return SpanExportResult.FAILURE with self._lock: self._finished_spans.extend(spans) return SpanExportResult.SUCCESS def shutdown(self) -> None: """Shut downs the exporter. Calls to export after the exporter has been shut down will fail. """ self._stopped = True def force_flush(self, timeout_millis: int = 30000) -> bool: return True python-opentelemetry-1.45.2/opentelemetry-sdk/src/opentelemetry/sdk/trace/id_generator.py000066400000000000000000000046101527070070700320550ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import abc import random from opentelemetry import trace class IdGenerator(abc.ABC): @abc.abstractmethod def generate_span_id(self) -> int: """Get a new span ID. Returns: A 64-bit int for use as a span ID """ @abc.abstractmethod def generate_trace_id(self) -> int: """Get a new trace ID. Implementations should at least make the 56 least significant bits uniformly random. Samplers like the `TraceIdRatioBased` sampler rely on this randomness to make sampling decisions. If the implementation does randomly generate the 56 least significant bits, it should also implement `is_trace_id_random` to return True. See `the specification on TraceIdRatioBased `_. Returns: A 128-bit int for use as a trace ID """ # pylint: disable=no-self-use def is_trace_id_random(self) -> bool: """Indicates whether generated trace IDs are random. When True, the `trace-id` field will have the `random-trace-id` flag set in the W3C traceparent header. Per the W3C Trace Context specification, this indicates that at least the 7 rightmost bytes (56 bits) of the trace ID were generated randomly with uniform distribution. See `the W3C Trace Context specification `_. Returns: True if this generator produces random IDs, False otherwise. """ # By default, return False for backwards compatibility. return False class RandomIdGenerator(IdGenerator): """The default ID generator for TracerProvider which randomly generates all bits when generating IDs. """ def generate_span_id(self) -> int: span_id = random.getrandbits(64) while span_id == trace.INVALID_SPAN_ID: span_id = random.getrandbits(64) return span_id def generate_trace_id(self) -> int: trace_id = random.getrandbits(128) while trace_id == trace.INVALID_TRACE_ID: trace_id = random.getrandbits(128) return trace_id def is_trace_id_random(self) -> bool: return True python-opentelemetry-1.45.2/opentelemetry-sdk/src/opentelemetry/sdk/trace/sampling.py000066400000000000000000000431741527070070700312350ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 """ For general information about sampling, see `the specification `_. OpenTelemetry provides two types of samplers: - `StaticSampler` - `TraceIdRatioBased` A `StaticSampler` always returns the same sampling result regardless of the conditions. Both possible StaticSamplers are already created: - Always sample spans: ALWAYS_ON - Never sample spans: ALWAYS_OFF A `TraceIdRatioBased` sampler makes a random sampling result based on the sampling probability given. If the span being sampled has a parent, `ParentBased` will respect the parent delegate sampler. Otherwise, it returns the sampling result from the given root sampler. Currently, sampling results are always made during the creation of the span. However, this might not always be the case in the future (see `OTEP #115 `_). Custom samplers can be created by subclassing `Sampler` and implementing `Sampler.should_sample` as well as `Sampler.get_description`. Samplers are able to modify the `opentelemetry.trace.span.TraceState` of the parent of the span being created. For custom samplers, it is suggested to implement `Sampler.should_sample` to utilize the parent span context's `opentelemetry.trace.span.TraceState` and pass into the `SamplingResult` instead of the explicit trace_state field passed into the parameter of `Sampler.should_sample`. To use a sampler, pass it into the tracer provider constructor. For example: .. code:: python from opentelemetry import trace from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import ( ConsoleSpanExporter, SimpleSpanProcessor, ) from opentelemetry.sdk.trace.sampling import TraceIdRatioBased # sample 1 in every 1000 traces sampler = TraceIdRatioBased(1/1000) # set the sampler onto the global tracer provider trace.set_tracer_provider(TracerProvider(sampler=sampler)) # set up an exporter for sampled spans trace.get_tracer_provider().add_span_processor( SimpleSpanProcessor(ConsoleSpanExporter()) ) # created spans will now be sampled by the TraceIdRatioBased sampler with trace.get_tracer(__name__).start_as_current_span("Test Span"): ... The tracer sampler can also be configured via environment variables ``OTEL_TRACES_SAMPLER`` and ``OTEL_TRACES_SAMPLER_ARG`` (only if applicable). The list of built-in values for ``OTEL_TRACES_SAMPLER`` are: * always_on - Sampler that always samples spans, regardless of the parent span's sampling decision. * always_off - Sampler that never samples spans, regardless of the parent span's sampling decision. * traceidratio - Sampler that samples probabilistically based on rate. * parentbased_always_on - (default) Sampler that respects its parent span's sampling decision, but otherwise always samples. * parentbased_always_off - Sampler that respects its parent span's sampling decision, but otherwise never samples. * parentbased_traceidratio - Sampler that respects its parent span's sampling decision, but otherwise samples probabilistically based on rate. Sampling probability can be set with ``OTEL_TRACES_SAMPLER_ARG`` if the sampler is traceidratio or parentbased_traceidratio. Rate must be in the range [0.0,1.0]. When not provided rate will be set to 1.0 (maximum rate possible). Prev example but with environment variables. Please make sure to set the env ``OTEL_TRACES_SAMPLER=traceidratio`` and ``OTEL_TRACES_SAMPLER_ARG=0.001``. .. code:: python from opentelemetry import trace from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import ( ConsoleSpanExporter, SimpleSpanProcessor, ) trace.set_tracer_provider(TracerProvider()) # set up an exporter for sampled spans trace.get_tracer_provider().add_span_processor( SimpleSpanProcessor(ConsoleSpanExporter()) ) # created spans will now be sampled by the TraceIdRatioBased sampler with rate 1/1000. with trace.get_tracer(__name__).start_as_current_span("Test Span"): ... When utilizing a configurator, you can configure a custom sampler. In order to create a configurable custom sampler, create an entry point for the custom sampler factory method or function under the entry point group, ``opentelemetry_traces_sampler``. The custom sampler factory method must be of type ``Callable[[str], Sampler]``, taking a single string argument and returning a Sampler object. The single input will come from the string value of the ``OTEL_TRACES_SAMPLER_ARG`` environment variable. If ``OTEL_TRACES_SAMPLER_ARG`` is not configured, the input will be an empty string. For example: .. code:: python setup( ... entry_points={ ... "opentelemetry_traces_sampler": [ "custom_sampler_name = path.to.sampler.factory.method:CustomSamplerFactory.get_sampler" ] } ) # ... class CustomRatioSampler(Sampler): def __init__(rate): # ... # ... class CustomSamplerFactory: @staticmethod def get_sampler(sampler_argument): try: rate = float(sampler_argument) return CustomSampler(rate) except ValueError: # In case argument is empty string. return CustomSampler(0.5) In order to configure you application with a custom sampler's entry point, set the ``OTEL_TRACES_SAMPLER`` environment variable to the key name of the entry point. For example, to configured the above sampler, set ``OTEL_TRACES_SAMPLER=custom_sampler_name`` and ``OTEL_TRACES_SAMPLER_ARG=0.5``. """ from __future__ import annotations import abc import enum import os from collections.abc import Sequence from logging import getLogger from types import MappingProxyType # pylint: disable=unused-import from opentelemetry.context import Context from opentelemetry.sdk.environment_variables import ( OTEL_TRACES_SAMPLER, OTEL_TRACES_SAMPLER_ARG, ) from opentelemetry.trace import Link, SpanKind, get_current_span from opentelemetry.trace.span import TraceState from opentelemetry.util.types import Attributes _logger = getLogger(__name__) class Decision(enum.Enum): # IsRecording() == false, span will not be recorded and all events and attributes will be dropped. DROP = 0 # IsRecording() == true, but Sampled flag MUST NOT be set. RECORD_ONLY = 1 # IsRecording() == true AND Sampled flag` MUST be set. RECORD_AND_SAMPLE = 2 def is_recording(self): return self in (Decision.RECORD_ONLY, Decision.RECORD_AND_SAMPLE) def is_sampled(self): return self is Decision.RECORD_AND_SAMPLE class SamplingResult: """A sampling result as applied to a newly-created Span. Args: decision: A sampling decision based off of whether the span is recorded and the sampled flag in trace flags in the span context. attributes: Attributes to add to the `opentelemetry.trace.Span`. trace_state: The tracestate used for the `opentelemetry.trace.Span`. Could possibly have been modified by the sampler. """ def __repr__(self) -> str: return f"{type(self).__name__}({self.decision!s}, attributes={self.attributes!s})" def __init__( self, decision: Decision, attributes: Attributes = None, trace_state: TraceState | None = None, ) -> None: self.decision = decision if attributes is None: self.attributes = MappingProxyType({}) else: self.attributes = MappingProxyType(attributes) self.trace_state = trace_state class Sampler(abc.ABC): @abc.abstractmethod def should_sample( self, parent_context: Context | None, trace_id: int, name: str, kind: SpanKind | None = None, attributes: Attributes = None, links: Sequence[Link] | None = None, trace_state: TraceState | None = None, ) -> SamplingResult: pass @abc.abstractmethod def get_description(self) -> str: pass class StaticSampler(Sampler): """Sampler that always returns the same decision.""" def __init__(self, decision: Decision) -> None: self._decision = decision def should_sample( self, parent_context: Context | None, trace_id: int, name: str, kind: SpanKind | None = None, attributes: Attributes = None, links: Sequence[Link] | None = None, trace_state: TraceState | None = None, ) -> SamplingResult: if self._decision is Decision.DROP: attributes = None return SamplingResult( self._decision, attributes, _get_parent_trace_state(parent_context), ) def get_description(self) -> str: if self._decision is Decision.DROP: return "AlwaysOffSampler" return "AlwaysOnSampler" ALWAYS_OFF = StaticSampler(Decision.DROP) """Sampler that never samples spans, regardless of the parent span's sampling decision.""" ALWAYS_ON = StaticSampler(Decision.RECORD_AND_SAMPLE) """Sampler that always samples spans, regardless of the parent span's sampling decision.""" class TraceIdRatioBased(Sampler): """ Sampler that makes sampling decisions probabilistically based on `rate`. Args: rate: Probability (between 0 and 1) that a span will be sampled """ def __init__(self, rate: float): if rate < 0.0 or rate > 1.0: raise ValueError("Probability must be in range [0.0, 1.0].") self._rate = rate self._bound = self.get_bound_for_rate(self._rate) # For compatibility with 64 bit trace IDs, the sampler checks the 64 # low-order bits of the trace ID to decide whether to sample a given trace. TRACE_ID_LIMIT = (1 << 64) - 1 @classmethod def get_bound_for_rate(cls, rate: float) -> int: return round(rate * (cls.TRACE_ID_LIMIT + 1)) @property def rate(self) -> float: return self._rate @property def bound(self) -> int: return self._bound def should_sample( self, parent_context: Context | None, trace_id: int, name: str, kind: SpanKind | None = None, attributes: Attributes = None, links: Sequence[Link] | None = None, trace_state: TraceState | None = None, ) -> SamplingResult: decision = Decision.DROP if trace_id & self.TRACE_ID_LIMIT < self.bound: decision = Decision.RECORD_AND_SAMPLE if decision is Decision.DROP: attributes = None return SamplingResult( decision, attributes, _get_parent_trace_state(parent_context), ) def get_description(self) -> str: return f"TraceIdRatioBased{{{self._rate}}}" class ParentBased(Sampler): """ If a parent is set, applies the respective delegate sampler. Otherwise, uses the root provided at initialization to make a decision. Args: root: Sampler called for spans with no parent (root spans). remote_parent_sampled: Sampler called for a remote sampled parent. remote_parent_not_sampled: Sampler called for a remote parent that is not sampled. local_parent_sampled: Sampler called for a local sampled parent. local_parent_not_sampled: Sampler called for a local parent that is not sampled. """ def __init__( self, root: Sampler, remote_parent_sampled: Sampler = ALWAYS_ON, remote_parent_not_sampled: Sampler = ALWAYS_OFF, local_parent_sampled: Sampler = ALWAYS_ON, local_parent_not_sampled: Sampler = ALWAYS_OFF, ): self._root = root self._remote_parent_sampled = remote_parent_sampled self._remote_parent_not_sampled = remote_parent_not_sampled self._local_parent_sampled = local_parent_sampled self._local_parent_not_sampled = local_parent_not_sampled def should_sample( self, parent_context: Context | None, trace_id: int, name: str, kind: SpanKind | None = None, attributes: Attributes = None, links: Sequence[Link] | None = None, trace_state: TraceState | None = None, ) -> SamplingResult: parent_span_context = get_current_span(parent_context).get_span_context() # default to the root sampler sampler = self._root # respect the sampling and remote flag of the parent if present if parent_span_context is not None and parent_span_context.is_valid: if parent_span_context.is_remote: if parent_span_context.trace_flags.sampled: sampler = self._remote_parent_sampled else: sampler = self._remote_parent_not_sampled else: if parent_span_context.trace_flags.sampled: sampler = self._local_parent_sampled else: sampler = self._local_parent_not_sampled return sampler.should_sample( parent_context=parent_context, trace_id=trace_id, name=name, kind=kind, attributes=attributes, links=links, ) def get_description(self): return f"ParentBased{{root:{self._root.get_description()},remoteParentSampled:{self._remote_parent_sampled.get_description()},remoteParentNotSampled:{self._remote_parent_not_sampled.get_description()},localParentSampled:{self._local_parent_sampled.get_description()},localParentNotSampled:{self._local_parent_not_sampled.get_description()}}}" class AlwaysRecordSampler(Sampler): """ This sampler will return the sampling result of the provided `root`, unless the sampling result contains the sampling decision `Decision.DROP`, in which case, a new sampling result will be returned that is functionally equivalent to the original, except that it contains the sampling decision `Decision.RECORD_ONLY`. This ensures that all spans are recorded, with no change to sampling. The intended use case of this sampler is to provide a means of sending all spans to a processor without having an impact on the sampling rate. This may be desirable if a user wishes to count or otherwise measure all spans produced in a service, without incurring the cost of 100% sampling. """ def __init__(self, root: Sampler): if root is None: raise ValueError("root must not be None") self._root = root def should_sample( self, parent_context: Context | None, trace_id: int, name: str, kind: SpanKind | None = None, attributes: Attributes = None, links: Sequence[Link] | None = None, trace_state: TraceState | None = None, ) -> SamplingResult: result: SamplingResult = self._root.should_sample( parent_context, trace_id, name, kind, attributes, links, trace_state, ) if result.decision is Decision.DROP: result = SamplingResult(Decision.RECORD_ONLY, result.attributes, result.trace_state) return result def get_description(self): return f"AlwaysRecordSampler{{{self._root.get_description()}}}" DEFAULT_OFF = ParentBased(ALWAYS_OFF) """Sampler that respects its parent span's sampling decision, but otherwise never samples.""" DEFAULT_ON = ParentBased(ALWAYS_ON) """Sampler that respects its parent span's sampling decision, but otherwise always samples.""" class ParentBasedTraceIdRatio(ParentBased): """ Sampler that respects its parent span's sampling decision, but otherwise samples probabilistically based on `rate`. """ def __init__(self, rate: float): root = TraceIdRatioBased(rate=rate) super().__init__(root=root) class _AlwaysOff(StaticSampler): def __init__(self, _): super().__init__(Decision.DROP) class _AlwaysOn(StaticSampler): def __init__(self, _): super().__init__(Decision.RECORD_AND_SAMPLE) class _ParentBasedAlwaysOff(ParentBased): def __init__(self, _): super().__init__(ALWAYS_OFF) class _ParentBasedAlwaysOn(ParentBased): def __init__(self, _): super().__init__(ALWAYS_ON) _KNOWN_SAMPLERS = { "always_on": ALWAYS_ON, "always_off": ALWAYS_OFF, "parentbased_always_on": DEFAULT_ON, "parentbased_always_off": DEFAULT_OFF, "traceidratio": TraceIdRatioBased, "parentbased_traceidratio": ParentBasedTraceIdRatio, } def _get_from_env_or_default() -> Sampler: trace_sampler = os.getenv(OTEL_TRACES_SAMPLER, "parentbased_always_on").lower() if trace_sampler not in _KNOWN_SAMPLERS: _logger.warning("Couldn't recognize sampler %s.", trace_sampler) trace_sampler = "parentbased_always_on" if trace_sampler in ("traceidratio", "parentbased_traceidratio"): try: rate = float(os.getenv(OTEL_TRACES_SAMPLER_ARG, "")) return _KNOWN_SAMPLERS[trace_sampler](rate) except (ValueError, TypeError): _logger.warning("Could not convert TRACES_SAMPLER_ARG to float. Using default rate 1.0.") return _KNOWN_SAMPLERS[trace_sampler](1.0) return _KNOWN_SAMPLERS[trace_sampler] def _get_parent_trace_state( parent_context: Context | None, ) -> TraceState | None: parent_span_context = get_current_span(parent_context).get_span_context() if parent_span_context is None or not parent_span_context.is_valid: return None return parent_span_context.trace_state python-opentelemetry-1.45.2/opentelemetry-sdk/src/opentelemetry/sdk/util/000077500000000000000000000000001527070070700267175ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-sdk/src/opentelemetry/sdk/util/__init__.py000066400000000000000000000076171527070070700310430ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import copy import datetime import threading from collections import deque from collections.abc import MutableMapping, Sequence from typing_extensions import deprecated def ns_to_iso_str(nanoseconds): """Get an ISO 8601 string from time_ns value.""" ts = datetime.datetime.fromtimestamp(nanoseconds / 1e9, tz=datetime.timezone.utc) return ts.strftime("%Y-%m-%dT%H:%M:%S.%fZ") def get_dict_as_key(labels): """Converts a dict to be used as a unique key""" return tuple( sorted( map( lambda kv: (kv[0], tuple(kv[1])) if isinstance(kv[1], list) else kv, labels.items(), ) ) ) class BoundedList(Sequence): """An append only list with a fixed max size. Calls to `append` and `extend` will drop the oldest elements if there is not enough room. """ def __init__(self, maxlen: int | None): self.dropped = 0 self._dq = deque(maxlen=maxlen) # type: deque self._lock = threading.Lock() def __deepcopy__(self, memo): copy_ = BoundedList(0) memo[id(self)] = copy_ with self._lock: copy_.dropped = self.dropped copy_._dq = copy.deepcopy(self._dq, memo) return copy_ def __repr__(self): return f"{type(self).__name__}({list(self._dq)}, maxlen={self._dq.maxlen})" def __getitem__(self, index): return self._dq[index] def __len__(self): return len(self._dq) def __iter__(self): with self._lock: return iter(list(self._dq)) def append(self, item): with self._lock: if self._dq.maxlen is not None and len(self._dq) == self._dq.maxlen: self.dropped += 1 self._dq.append(item) def extend(self, seq): with self._lock: if self._dq.maxlen is not None: to_drop = len(seq) + len(self._dq) - self._dq.maxlen if to_drop > 0: self.dropped += to_drop self._dq.extend(seq) @classmethod def from_seq(cls, maxlen, seq): seq = tuple(seq) bounded_list = cls(maxlen) bounded_list.extend(seq) return bounded_list @deprecated("Deprecated since version 1.4.0.") class BoundedDict(MutableMapping): """An ordered dict with a fixed max capacity. Oldest elements are dropped when the dict is full and a new element is added. """ def __init__(self, maxlen: int | None): if maxlen is not None: if not isinstance(maxlen, int): raise ValueError if maxlen < 0: raise ValueError self.maxlen = maxlen self.dropped = 0 self._dict = {} # type: dict self._lock = threading.Lock() # type: threading.Lock def __repr__(self): return f"{type(self).__name__}({dict(self._dict)}, maxlen={self.maxlen})" def __getitem__(self, key): return self._dict[key] def __setitem__(self, key, value): with self._lock: if self.maxlen is not None and self.maxlen == 0: self.dropped += 1 return if key in self._dict: del self._dict[key] elif self.maxlen is not None and len(self._dict) == self.maxlen: del self._dict[next(iter(self._dict.keys()))] self.dropped += 1 self._dict[key] = value def __delitem__(self, key): del self._dict[key] def __iter__(self): with self._lock: return iter(self._dict.copy()) def __len__(self): return len(self._dict) @classmethod def from_map(cls, maxlen, mapping): mapping = dict(mapping) bounded_dict = cls(maxlen) for key, value in mapping.items(): bounded_dict[key] = value return bounded_dict python-opentelemetry-1.45.2/opentelemetry-sdk/src/opentelemetry/sdk/util/__init__.pyi000066400000000000000000000046241527070070700312070ustar00rootroot00000000000000# 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. from collections.abc import ( Iterable, Iterator, Mapping, MutableMapping, Sequence, ) from typing import ( Any, TypeVar, overload, ) from opentelemetry.util.types import AnyValue, AttributesAsKey _T = TypeVar("_T") _KT = TypeVar("_KT") _VT = TypeVar("_VT") def ns_to_iso_str(nanoseconds: int) -> str: ... def get_dict_as_key( labels: Mapping[str, AnyValue], ) -> AttributesAsKey: ... # pylint: disable=no-self-use class BoundedList(Sequence[_T]): """An append only list with a fixed max size. Calls to `append` and `extend` will drop the oldest elements if there is not enough room. """ dropped: int def __init__(self, maxlen: int | None): ... def __deepcopy__(self, memo: dict[int, Any]) -> BoundedList[_T]: ... def insert(self, index: int, value: _T) -> None: ... @overload def __getitem__(self, i: int) -> _T: ... @overload def __getitem__(self, s: slice) -> Sequence[_T]: ... def __len__(self) -> int: ... def append(self, item: _T) -> None: ... def extend(self, seq: Sequence[_T]) -> None: ... @classmethod def from_seq(cls, maxlen: int | None, seq: Iterable[_T]) -> BoundedList[_T]: ... # pylint: disable=undefined-variable class BoundedDict(MutableMapping[_KT, _VT]): """An ordered dict with a fixed max capacity. Oldest elements are dropped when the dict is full and a new element is added. """ dropped: int def __init__(self, maxlen: int): ... def __getitem__(self, k: _KT) -> _VT: ... def __setitem__(self, k: _KT, v: _VT) -> None: ... def __delitem__(self, v: _KT) -> None: ... def __iter__(self) -> Iterator[_KT]: ... def __len__(self) -> int: ... @classmethod def from_map(cls, maxlen: int, mapping: Mapping[_KT, _VT]) -> BoundedDict[_KT, _VT]: ... # pylint: disable=undefined-variable python-opentelemetry-1.45.2/opentelemetry-sdk/src/opentelemetry/sdk/util/_configurator.py000066400000000000000000000016341527070070700321360ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from collections.abc import Sequence from typing import Generic, TypeVar from opentelemetry.sdk.util.instrumentation import ( InstrumentationScope, _InstrumentationScopePredicateT, ) ConfigT = TypeVar("ConfigT") ConfiguratorRulesT = Sequence[tuple[_InstrumentationScopePredicateT, ConfigT]] class RuleBasedConfigurator(Generic[ConfigT]): def __init__(self, *, rules: ConfiguratorRulesT, default_config: ConfigT): self._rules = rules self._default_config = default_config def __call__(self, scope: InstrumentationScope) -> ConfigT: for predicate, config in self._rules: if predicate(scope): return config # by default return default config return self._default_config def update_rules(self, rules: ConfiguratorRulesT) -> None: self._rules = rules python-opentelemetry-1.45.2/opentelemetry-sdk/src/opentelemetry/sdk/util/instrumentation.py000066400000000000000000000117511527070070700325410ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from collections.abc import Callable from fnmatch import fnmatchcase from json import dumps from typing_extensions import deprecated from opentelemetry.attributes import BoundedAttributes from opentelemetry.util.types import ( # TODO: see if we can remove F401 when using new sphinx version # noqa: F401 # pylint: disable=unused-import AnyValue, Attributes, ) class InstrumentationInfo: """Immutable information about an instrumentation library module. See `opentelemetry.trace.TracerProvider.get_tracer` for the meaning of these properties. """ __slots__ = ("_name", "_version", "_schema_url") @deprecated("You should use InstrumentationScope. Deprecated since version 1.11.1.") def __init__( self, name: str, version: str | None = None, schema_url: str | None = None, ): self._name = name self._version = version if schema_url is None: schema_url = "" self._schema_url = schema_url def __repr__(self): return f"{type(self).__name__}({self._name}, {self._version}, {self._schema_url})" def __hash__(self): return hash((self._name, self._version, self._schema_url)) def __eq__(self, value): return type(value) is type(self) and ( self._name, self._version, self._schema_url, ) == (value._name, value._version, value._schema_url) def __lt__(self, value): if type(value) is not type(self): return NotImplemented return (self._name, self._version, self._schema_url) < ( value._name, value._version, value._schema_url, ) @property def schema_url(self) -> str | None: return self._schema_url @property def version(self) -> str | None: return self._version @property def name(self) -> str: return self._name class InstrumentationScope: """A logical unit of the application code with which the emitted telemetry can be associated. See `opentelemetry.trace.TracerProvider.get_tracer` for the meaning of these properties. """ __slots__ = ( "_name", "_version", "_schema_url", "_attributes", ) def __init__( self, name: str, version: str | None = None, schema_url: str | None = None, attributes: Attributes = None, ) -> None: self._name = name self._version = version if schema_url is None: schema_url = "" self._schema_url = schema_url # Attributes cannot be added/removed after creation. self._attributes = BoundedAttributes(attributes=attributes, immutable=True) def __repr__(self) -> str: return f"{type(self).__name__}({self._name}, {self._version}, {self._schema_url}, {self._attributes})" def __hash__(self) -> int: return hash((self._name, self._version, self._schema_url)) def __eq__(self, value: object) -> bool: if not isinstance(value, InstrumentationScope): return NotImplemented return ( self._name, self._version, self._schema_url, self._attributes, ) == ( value._name, value._version, value._schema_url, value._attributes, ) def __lt__(self, value: object) -> bool: if not isinstance(value, InstrumentationScope): return NotImplemented return ( self._name, self._version, self._schema_url, self._attributes, ) < ( value._name, value._version, value._schema_url, value._attributes, ) @property def schema_url(self) -> str | None: return self._schema_url @property def version(self) -> str | None: return self._version @property def name(self) -> str: return self._name @property def attributes(self) -> Attributes: return self._attributes def to_json(self, indent: int | None = 4) -> str: return dumps( { "name": self._name, "version": self._version, "schema_url": self._schema_url, "attributes": (dict(self._attributes) if bool(self._attributes) else None), }, indent=indent, ) _InstrumentationScopePredicateT = Callable[[InstrumentationScope], bool] def _scope_name_matches_glob( glob_pattern: str, ) -> _InstrumentationScopePredicateT: def inner(scope: InstrumentationScope) -> bool: # Scope name matching is case-sensitive. fnmatchcase is used instead of # fnmatch so it does not rely on the host platform's filename case # sensitivity (normcase), which lower-cases both operands on Windows. return fnmatchcase(scope.name, glob_pattern) return inner python-opentelemetry-1.45.2/opentelemetry-sdk/src/opentelemetry/sdk/version/000077500000000000000000000000001527070070700274275ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-sdk/src/opentelemetry/sdk/version/__init__.py000066400000000000000000000001441527070070700315370ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 __version__ = "1.45.2" python-opentelemetry-1.45.2/opentelemetry-sdk/test-requirements.txt000066400000000000000000000004551527070070700257430ustar00rootroot00000000000000asgiref==3.7.2 flaky==3.8.1 iniconfig==2.0.0 packaging==24.0 pluggy==1.6.0 psutil==7.2.2 py-cpuinfo==9.0.0 pytest==9.0.3 tomli==2.0.1 typing_extensions==4.12.0 wrapt==1.16.0 zipp==3.19.2 -e opentelemetry-api -e tests/opentelemetry-test-utils -e opentelemetry-semantic-conventions -e opentelemetry-sdk python-opentelemetry-1.45.2/opentelemetry-sdk/tests/000077500000000000000000000000001527070070700226405ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-sdk/tests/__init__.py000066400000000000000000000001141527070070700247450ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 python-opentelemetry-1.45.2/opentelemetry-sdk/tests/_configuration/000077500000000000000000000000001527070070700256465ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-sdk/tests/_configuration/test_configurator_file_routing.py000066400000000000000000000107321527070070700345320ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # Tests access private members of SDK classes to assert correct configuration. # pylint: disable=protected-access,no-self-use import types import unittest from unittest.mock import MagicMock, patch from opentelemetry.sdk._configuration import _OTelSDKConfigurator from opentelemetry.sdk.environment_variables import OTEL_CONFIG_FILE class _FakeConfigurationModule(types.ModuleType): """Stub `opentelemetry.configuration` module. The SDK lazy-imports this package at runtime when OTEL_CONFIG_FILE is set, but the SDK's test env does not depend on opentelemetry-configuration. Injecting an instance into sys.modules lets these tests exercise the routing without installing the downstream package. Declaring the attrs at class level lets pylint introspect them without a no-member disable. """ configure_sdk: MagicMock load_config_file: MagicMock def __init__(self) -> None: super().__init__("opentelemetry.configuration") self.configure_sdk = MagicMock() self.load_config_file = MagicMock() class TestConfiguratorFileRouting(unittest.TestCase): def tearDown(self): # _BaseConfigurator caches instances via a singleton; reset so sibling # tests (e.g. test_configurator.py's CustomConfigurator subclass) are # not affected by this class's singleton state. _OTelSDKConfigurator._instance = None @patch.dict("os.environ", {}, clear=True) @patch("opentelemetry.sdk._configuration._initialize_components") def test_env_var_unset_runs_env_var_path(self, mock_init_components): _OTelSDKConfigurator()._configure(auto_instrumentation_version="X") mock_init_components.assert_called_once_with(auto_instrumentation_version="X") @patch.dict("os.environ", {OTEL_CONFIG_FILE: "/tmp/otel.yaml"}) @patch("opentelemetry.sdk._configuration._initialize_components") def test_env_var_set_routes_to_declarative_path(self, mock_init_components): fake = _FakeConfigurationModule() sentinel_config = object() fake.load_config_file.return_value = sentinel_config with patch.dict("sys.modules", {"opentelemetry.configuration": fake}): _OTelSDKConfigurator()._configure() fake.load_config_file.assert_called_once_with("/tmp/otel.yaml") fake.configure_sdk.assert_called_once_with(sentinel_config) mock_init_components.assert_not_called() @patch.dict("os.environ", {OTEL_CONFIG_FILE: "/tmp/otel.yaml"}) @patch.dict("sys.modules", {"opentelemetry.configuration": None}, clear=False) @patch("opentelemetry.sdk._configuration._initialize_components") def test_env_var_set_but_package_missing_raises(self, mock_init_components): # When opentelemetry-configuration is not installed but the env # var is set, surface a clear RuntimeError instead of a bare # ImportError so users know which package to install. with self.assertRaises(RuntimeError) as ctx: _OTelSDKConfigurator()._configure() self.assertIn("opentelemetry-configuration", str(ctx.exception)) mock_init_components.assert_not_called() @patch.dict("os.environ", {OTEL_CONFIG_FILE: "/tmp/otel.yaml"}) def test_env_var_set_with_kwargs_warns_and_ignores(self): fake = _FakeConfigurationModule() fake.load_config_file.return_value = object() with patch.dict("sys.modules", {"opentelemetry.configuration": fake}): with self.assertLogs("opentelemetry.sdk._configuration", level="WARNING") as captured: _OTelSDKConfigurator()._configure(sampler="X", auto_instrumentation_version="Y") self.assertTrue( any("OTEL_CONFIG_FILE" in msg and "sampler" in msg for msg in captured.output), f"Expected warning about ignored kwargs, got: {captured.output}", ) fake.configure_sdk.assert_called_once() @patch.dict("os.environ", {}, clear=True) @patch("opentelemetry.sdk._configuration._initialize_components") def test_distro_override_pattern_still_works(self, mock_init_components): class CustomConfigurator(_OTelSDKConfigurator): def _configure(self, **kwargs): kwargs["sampler"] = "TEST_SAMPLER" super()._configure(**kwargs) CustomConfigurator()._configure(auto_instrumentation_version="V") mock_init_components.assert_called_once_with(auto_instrumentation_version="V", sampler="TEST_SAMPLER") python-opentelemetry-1.45.2/opentelemetry-sdk/tests/conftest.py000066400000000000000000000011301527070070700250320ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import random from os import environ import pytest from opentelemetry.environment_variables import OTEL_PYTHON_CONTEXT def pytest_sessionstart(session): # pylint: disable=unused-argument environ[OTEL_PYTHON_CONTEXT] = "contextvars_context" def pytest_sessionfinish(session): # pylint: disable=unused-argument environ.pop(OTEL_PYTHON_CONTEXT) @pytest.fixture(autouse=True) def random_seed(): # We use random numbers a lot in sampling tests, make sure they are always the same. random.seed(0) python-opentelemetry-1.45.2/opentelemetry-sdk/tests/context/000077500000000000000000000000001527070070700243245ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-sdk/tests/context/__init__.py000066400000000000000000000000001527070070700264230ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-sdk/tests/context/test_asyncio.py000066400000000000000000000055441527070070700274120ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import asyncio import unittest from unittest.mock import patch from opentelemetry import context from opentelemetry.context.contextvars_context import ContextVarsRuntimeContext from opentelemetry.sdk import trace from opentelemetry.sdk.trace import export from opentelemetry.sdk.trace.export.in_memory_span_exporter import ( InMemorySpanExporter, ) _SPAN_NAMES = [ "test_span1", "test_span2", "test_span3", "test_span4", "test_span5", ] def stop_loop_when(loop, cond_func, timeout=5.0): """Registers a periodic callback that stops the loop when cond_func() == True. Compatible with both Tornado and asyncio. """ if cond_func() or timeout <= 0.0: loop.stop() return timeout -= 0.1 loop.call_later(0.1, stop_loop_when, loop, cond_func, timeout) class TestAsyncio(unittest.TestCase): async def task(self, name): with self.tracer.start_as_current_span(name): context.set_value("say", "bar") def submit_another_task(self, name): self.loop.create_task(self.task(name)) def setUp(self): self.token = context.attach(context.Context()) self.tracer_provider = trace.TracerProvider() self.tracer = self.tracer_provider.get_tracer(__name__) self.memory_exporter = InMemorySpanExporter() span_processor = export.SimpleSpanProcessor(self.memory_exporter) self.tracer_provider.add_span_processor(span_processor) self.loop = asyncio.new_event_loop() asyncio.set_event_loop(self.loop) def tearDown(self): context.detach(self.token) self.loop.close() @patch("opentelemetry.context._RUNTIME_CONTEXT", ContextVarsRuntimeContext()) def test_with_asyncio(self): with self.tracer.start_as_current_span("asyncio_test"): for name in _SPAN_NAMES: self.submit_another_task(name) stop_loop_when( self.loop, lambda: len(self.memory_exporter.get_finished_spans()) >= 5, timeout=5.0, ) self.loop.run_forever() span_list = self.memory_exporter.get_finished_spans() span_names_list = [span.name for span in span_list] expected = [ "test_span1", "test_span2", "test_span3", "test_span4", "test_span5", "asyncio_test", ] self.assertCountEqual(span_names_list, expected) span_names_list.sort() expected.sort() self.assertListEqual(span_names_list, expected) expected_parent = next(span for span in span_list if span.name == "asyncio_test") for span in span_list: if span is expected_parent: continue self.assertEqual(span.parent, expected_parent.context) python-opentelemetry-1.45.2/opentelemetry-sdk/tests/error_handler/000077500000000000000000000000001527070070700254665ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-sdk/tests/error_handler/__init__.py000066400000000000000000000000001527070070700275650ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-sdk/tests/error_handler/test_error_handler.py000066400000000000000000000066751527070070700317430ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from logging import ERROR from unittest import TestCase from unittest.mock import Mock, patch from opentelemetry.sdk.error_handler import ( ErrorHandler, GlobalErrorHandler, logger, ) class TestErrorHandler(TestCase): @patch("opentelemetry.sdk.error_handler.entry_points") def test_default_error_handler(self, mock_entry_points): with self.assertLogs(logger, ERROR), GlobalErrorHandler(): # pylint: disable=broad-exception-raised raise Exception("some exception") # pylint: disable=no-self-use @patch("opentelemetry.sdk.error_handler.entry_points") def test_plugin_error_handler(self, mock_entry_points): class ZeroDivisionErrorHandler(ErrorHandler, ZeroDivisionError): # pylint: disable=arguments-differ _handle = Mock() class AssertionErrorHandler(ErrorHandler, AssertionError): # pylint: disable=arguments-differ _handle = Mock() mock_entry_point_zero_division_error_handler = Mock() mock_entry_point_zero_division_error_handler.configure_mock(**{"load.return_value": ZeroDivisionErrorHandler}) mock_entry_point_assertion_error_handler = Mock() mock_entry_point_assertion_error_handler.configure_mock(**{"load.return_value": AssertionErrorHandler}) mock_entry_points.configure_mock( return_value=[ mock_entry_point_zero_division_error_handler, mock_entry_point_assertion_error_handler, ] ) error = ZeroDivisionError() with GlobalErrorHandler(): raise error # pylint: disable=protected-access ZeroDivisionErrorHandler._handle.assert_called_with(error) error = AssertionError() with GlobalErrorHandler(): raise error AssertionErrorHandler._handle.assert_called_with(error) @patch("opentelemetry.sdk.error_handler.entry_points") def test_error_in_handler(self, mock_entry_points): class ErrorErrorHandler(ErrorHandler, ZeroDivisionError): # pylint: disable=arguments-differ def _handle(self, error: Exception): assert False mock_entry_point_error_error_handler = Mock() mock_entry_point_error_error_handler.configure_mock(**{"load.return_value": ErrorErrorHandler}) mock_entry_points.configure_mock(return_value=[mock_entry_point_error_error_handler]) error = ZeroDivisionError() with self.assertLogs(logger, ERROR), GlobalErrorHandler(): raise error # pylint: disable=no-self-use @patch("opentelemetry.sdk.error_handler.entry_points") def test_plugin_error_handler_context_manager(self, mock_entry_points): mock_error_handler_instance = Mock() class MockErrorHandlerClass(IndexError): def __new__(cls): return mock_error_handler_instance mock_entry_point_error_handler = Mock() mock_entry_point_error_handler.configure_mock(**{"load.return_value": MockErrorHandlerClass}) mock_entry_points.configure_mock(return_value=[mock_entry_point_error_handler]) error = IndexError() with GlobalErrorHandler(): raise error with GlobalErrorHandler(): pass # pylint: disable=protected-access mock_error_handler_instance._handle.assert_called_once_with(error) python-opentelemetry-1.45.2/opentelemetry-sdk/tests/logs/000077500000000000000000000000001527070070700236045ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-sdk/tests/logs/__init__.py000066400000000000000000000001141527070070700257110ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 python-opentelemetry-1.45.2/opentelemetry-sdk/tests/logs/scripts/000077500000000000000000000000001527070070700252735ustar00rootroot00000000000000logger_provider_resource_after_fork.py000066400000000000000000000044241527070070700350740ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-sdk/tests/logs/scripts# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import json import os from opentelemetry._logs import LogRecord from opentelemetry.sdk._logs import LoggerProvider from opentelemetry.sdk._logs.export import ( InMemoryLogRecordExporter, SimpleLogRecordProcessor, ) from opentelemetry.sdk.resources import PROCESS_PID # pylint: disable-next=too-many-locals def main() -> None: exporter = InMemoryLogRecordExporter() logger_provider = LoggerProvider(shutdown_on_exit=False) logger_provider.add_log_record_processor(SimpleLogRecordProcessor(exporter)) logger = logger_provider.get_logger("cached") parent_pid = os.getpid() parent_resource_pid = logger_provider.resource.attributes[PROCESS_PID] parent_logger_pid = logger.resource.attributes[PROCESS_PID] pid = os.fork() if not pid: child_pid = os.getpid() new_logger = logger_provider.get_logger("new") logger.emit(LogRecord(observed_timestamp=0, body="cached")) new_logger.emit(LogRecord(observed_timestamp=0, body="new")) finished_logs = exporter.get_finished_logs() print( json.dumps( { "child_pid": child_pid, "provider_pid": logger_provider.resource.attributes[PROCESS_PID], "cached_logger_pid": logger.resource.attributes[PROCESS_PID], "new_logger_pid": new_logger.resource.attributes[PROCESS_PID], "exported_resource_pids": [log.resource.attributes[PROCESS_PID] for log in finished_logs], "log_bodies": sorted(log.log_record.body for log in finished_logs), } ), flush=True, ) # pylint: disable-next=protected-access os._exit(0) os.waitpid(pid, 0) print( json.dumps( { "parent_pid": parent_pid, "parent_resource_pid": parent_resource_pid, "parent_logger_pid": parent_logger_pid, "parent_resource_pid_after_fork": logger_provider.resource.attributes[PROCESS_PID], "parent_logger_pid_after_fork": logger.resource.attributes[PROCESS_PID], } ), flush=True, ) if __name__ == "__main__": main() python-opentelemetry-1.45.2/opentelemetry-sdk/tests/logs/test_export.py000066400000000000000000001077331527070070700265510ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # pylint: disable=protected-access,too-many-lines import logging import os import sys import threading import time import unittest from collections.abc import Sequence from concurrent.futures import ( # pylint: disable=no-name-in-module ThreadPoolExecutor, ) from unittest import mock from unittest.mock import Mock, patch from pytest import mark from opentelemetry._logs import LogRecord, SeverityNumber from opentelemetry.metrics import NoOpMeterProvider from opentelemetry.sdk import trace from opentelemetry.sdk._logs import ( LoggerProvider, LoggingHandler, ReadableLogRecord, ReadWriteLogRecord, ) from opentelemetry.sdk._logs._internal.export import _logger from opentelemetry.sdk._logs.export import ( BatchLogRecordProcessor, ConsoleLogRecordExporter, InMemoryLogRecordExporter, LogRecordExporter, LogRecordExportResult, SimpleLogRecordProcessor, ) from opentelemetry.sdk.environment_variables import ( OTEL_BLRP_EXPORT_TIMEOUT, OTEL_BLRP_MAX_EXPORT_BATCH_SIZE, OTEL_BLRP_MAX_QUEUE_SIZE, OTEL_BLRP_SCHEDULE_DELAY, OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED, ) from opentelemetry.sdk.metrics import MeterProvider from opentelemetry.sdk.metrics.export import InMemoryMetricReader from opentelemetry.sdk.resources import Resource as SDKResource from opentelemetry.sdk.util.instrumentation import InstrumentationScope from opentelemetry.trace import ( NonRecordingSpan, SpanContext, TraceFlags, set_span_in_context, ) from opentelemetry.trace.span import INVALID_SPAN_CONTEXT EMPTY_LOG = ReadWriteLogRecord( log_record=LogRecord(), instrumentation_scope=InstrumentationScope("example", "example"), ) class TestSimpleLogRecordProcessor(unittest.TestCase): @mark.skipif( (3, 13, 0) <= sys.version_info <= (3, 13, 5), reason="This will fail on 3.13.5 due to https://github.com/python/cpython/pull/131812 which prevents recursive log messages but was rolled back in 3.13.6.", ) def test_simple_log_record_processor_doesnt_enter_recursive_loop(self): class Exporter(LogRecordExporter): def shutdown(self): pass def force_flush(self, timeout_millis: int = 10_000) -> bool: return True def export(self, batch: Sequence[ReadableLogRecord]): logger = logging.getLogger("any logger..") logger.warning("Something happened.") exporter = Exporter() logger_provider = LoggerProvider() logger_provider.add_log_record_processor(SimpleLogRecordProcessor(exporter)) root_logger = logging.getLogger() # Add the OTLP handler to the root logger like is done in auto instrumentation. # This causes logs generated from within SimpleLogRecordProcessor.on_emit (such as the above log in export) # to be sent back to SimpleLogRecordProcessor.on_emit handler = LoggingHandler(level=logging.DEBUG, logger_provider=logger_provider) root_logger.addHandler(handler) propagate_false_logger = logging.getLogger("opentelemetry.sdk._logs._internal.export.propagate.false") # This would cause a max recursion depth exceeded error.. try: with self.assertLogs(propagate_false_logger) as cm: root_logger.warning("hello!") assert "SimpleLogRecordProcessor.on_emit has entered a recursive loop" in cm.output[0] finally: root_logger.removeHandler(handler) def test_simple_log_record_processor_default_level(self): exporter = InMemoryLogRecordExporter() logger_provider = LoggerProvider() logger_provider.add_log_record_processor(SimpleLogRecordProcessor(exporter)) logger = logging.getLogger("default_level") logger.propagate = False logger.addHandler(LoggingHandler(logger_provider=logger_provider)) logger.warning("Something is wrong") finished_logs = exporter.get_finished_logs() self.assertEqual(len(finished_logs), 1) warning_log_record = finished_logs[0] self.assertEqual(warning_log_record.log_record.body, "Something is wrong") self.assertEqual(warning_log_record.log_record.severity_text, "WARN") self.assertEqual(warning_log_record.log_record.severity_number, SeverityNumber.WARN) self.assertEqual(finished_logs[0].instrumentation_scope.name, "default_level") def test_simple_log_record_processor_custom_level(self): exporter = InMemoryLogRecordExporter() logger_provider = LoggerProvider() logger_provider.add_log_record_processor(SimpleLogRecordProcessor(exporter)) logger = logging.getLogger("custom_level") logger.propagate = False logger.setLevel(logging.ERROR) logger.addHandler(LoggingHandler(logger_provider=logger_provider)) logger.warning("Warning message") logger.debug("Debug message") logger.error("Error message") logger.critical("Critical message") finished_logs = exporter.get_finished_logs() # Make sure only level >= logging.CRITICAL logs are recorded self.assertEqual(len(finished_logs), 2) critical_log_record = finished_logs[0] fatal_log_record = finished_logs[1] self.assertEqual(critical_log_record.log_record.body, "Error message") self.assertEqual(critical_log_record.log_record.severity_text, "ERROR") self.assertEqual( critical_log_record.log_record.severity_number, SeverityNumber.ERROR, ) self.assertEqual(fatal_log_record.log_record.body, "Critical message") self.assertEqual(fatal_log_record.log_record.severity_text, "FATAL") self.assertEqual(fatal_log_record.log_record.severity_number, SeverityNumber.FATAL) self.assertEqual(finished_logs[0].instrumentation_scope.name, "custom_level") self.assertEqual(finished_logs[1].instrumentation_scope.name, "custom_level") def test_simple_log_record_processor_trace_correlation(self): exporter = InMemoryLogRecordExporter() logger_provider = LoggerProvider() logger_provider.add_log_record_processor(SimpleLogRecordProcessor(exporter)) logger = logging.getLogger("trace_correlation") logger.propagate = False logger.addHandler(LoggingHandler(logger_provider=logger_provider)) logger.warning("Warning message") finished_logs = exporter.get_finished_logs() self.assertEqual(len(finished_logs), 1) sdk_record = finished_logs[0] self.assertEqual(sdk_record.log_record.body, "Warning message") self.assertEqual(sdk_record.log_record.severity_text, "WARN") self.assertEqual(sdk_record.log_record.severity_number, SeverityNumber.WARN) self.assertEqual(sdk_record.log_record.trace_id, INVALID_SPAN_CONTEXT.trace_id) self.assertEqual(sdk_record.log_record.span_id, INVALID_SPAN_CONTEXT.span_id) self.assertEqual(sdk_record.log_record.trace_flags, INVALID_SPAN_CONTEXT.trace_flags) self.assertEqual(finished_logs[0].instrumentation_scope.name, "trace_correlation") exporter.clear() tracer = trace.TracerProvider().get_tracer(__name__) with tracer.start_as_current_span("test") as span: logger.critical("Critical message within span") finished_logs = exporter.get_finished_logs() sdk_record = finished_logs[0] self.assertEqual(sdk_record.log_record.body, "Critical message within span") self.assertEqual(sdk_record.log_record.severity_text, "FATAL") self.assertEqual(sdk_record.log_record.severity_number, SeverityNumber.FATAL) self.assertEqual( finished_logs[0].instrumentation_scope.name, "trace_correlation", ) span_context = span.get_span_context() self.assertEqual(sdk_record.log_record.trace_id, span_context.trace_id) self.assertEqual(sdk_record.log_record.span_id, span_context.span_id) self.assertEqual(sdk_record.log_record.trace_flags, span_context.trace_flags) def test_simple_log_record_processor_shutdown(self): exporter = InMemoryLogRecordExporter() logger_provider = LoggerProvider() logger_provider.add_log_record_processor(SimpleLogRecordProcessor(exporter)) logger = logging.getLogger("shutdown") logger.propagate = False logger.addHandler(LoggingHandler(logger_provider=logger_provider)) logger.warning("Something is wrong") finished_logs = exporter.get_finished_logs() self.assertEqual(len(finished_logs), 1) warning_log_record = finished_logs[0] self.assertEqual(warning_log_record.log_record.body, "Something is wrong") self.assertEqual(warning_log_record.log_record.severity_text, "WARN") self.assertEqual(warning_log_record.log_record.severity_number, SeverityNumber.WARN) self.assertEqual(finished_logs[0].instrumentation_scope.name, "shutdown") exporter.clear() logger_provider.shutdown() logger.warning("Log after shutdown") finished_logs = exporter.get_finished_logs() self.assertEqual(len(finished_logs), 0) def test_simple_log_record_processor_different_msg_types(self): exporter = InMemoryLogRecordExporter() log_record_processor = BatchLogRecordProcessor(exporter) provider = LoggerProvider() provider.add_log_record_processor(log_record_processor) logger = logging.getLogger("different_msg_types") logger.addHandler(LoggingHandler(logger_provider=provider)) logger.warning("warning message: %s", "possible upcoming heatwave") logger.error("Very high rise in temperatures across the globe") logger.critical("Temperature hits high 420 C in Hyderabad") logger.warning(["list", "of", "strings"]) logger.error({"key": "value"}) log_record_processor.shutdown() finished_logs = exporter.get_finished_logs() expected = [ ("warning message: possible upcoming heatwave", "WARN"), ("Very high rise in temperatures across the globe", "ERROR"), ( "Temperature hits high 420 C in Hyderabad", "FATAL", ), (["list", "of", "strings"], "WARN"), ({"key": "value"}, "ERROR"), ] emitted = [(item.log_record.body, item.log_record.severity_text) for item in finished_logs] self.assertEqual(expected, emitted) for item in finished_logs: self.assertEqual(item.instrumentation_scope.name, "different_msg_types") def test_simple_log_record_processor_custom_single_obj(self): """ Tests that special-case handling for logging a single non-string object is correctly applied. """ exporter = InMemoryLogRecordExporter() log_record_processor = BatchLogRecordProcessor(exporter) provider = LoggerProvider() provider.add_log_record_processor(log_record_processor) logger = logging.getLogger("single_obj") logger.addHandler(LoggingHandler(logger_provider=provider)) # 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: # - normally getMessage would stringify the object and bypass formatting # - SPECIAL CASE: bypass stringification as well to keep the raw object logger.warning(["a non-string with a percent-s", "%s"]) log_record_processor.shutdown() finished_logs = exporter.get_finished_logs() 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 emitted, expected in zip(finished_logs, expected): self.assertEqual(emitted.log_record.body, expected) self.assertEqual(emitted.instrumentation_scope.name, "single_obj") def test_simple_log_record_processor_different_msg_types_with_formatter( self, ): exporter = InMemoryLogRecordExporter() log_record_processor = BatchLogRecordProcessor(exporter) provider = LoggerProvider() provider.add_log_record_processor(log_record_processor) logger = logging.getLogger("different_msg_types") handler = LoggingHandler(logger_provider=provider) handler.setFormatter(logging.Formatter("%(name)s - %(levelname)s - %(message)s")) logger.addHandler(handler) logger.warning("warning message: %s", "possible upcoming heatwave") logger.error("Very high rise in temperatures across the globe") logger.critical("Temperature hits high 420 C in Hyderabad") logger.warning(["list", "of", "strings"]) logger.error({"key": "value"}) log_record_processor.shutdown() finished_logs = exporter.get_finished_logs() expected = [ ( "different_msg_types - WARNING - warning message: possible upcoming heatwave", "WARN", ), ( "different_msg_types - ERROR - Very high rise in temperatures across the globe", "ERROR", ), ( "different_msg_types - CRITICAL - Temperature hits high 420 C in Hyderabad", "FATAL", ), ( "different_msg_types - WARNING - ['list', 'of', 'strings']", "WARN", ), ("different_msg_types - ERROR - {'key': 'value'}", "ERROR"), ] emitted = [(item.log_record.body, item.log_record.severity_text) for item in finished_logs] self.assertEqual(expected, emitted) @patch.dict("os.environ", {OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED: "true"}) def test_metrics(self): # pylint: disable=too-many-locals metric_reader = InMemoryMetricReader() meter_provider = MeterProvider(metric_readers=[metric_reader]) num_exports = 0 def export_logs(_logs): nonlocal num_exports num_exports += 1 if num_exports == 3: raise RuntimeError("Export failed") return LogRecordExportResult.SUCCESS exporter = mock.MagicMock() exporter.export.side_effect = export_logs processor = SimpleLogRecordProcessor(exporter, meter_provider=meter_provider) provider = LoggerProvider() provider.add_log_record_processor(processor) logger = provider.get_logger("test_simple_metrics") logger.emit(LogRecord(body="foo", severity_number=SeverityNumber.WARN)) logger.emit(LogRecord(body="bar", severity_number=SeverityNumber.WARN)) logger.emit(LogRecord(body="baz", severity_number=SeverityNumber.WARN)) metrics_data = metric_reader.get_metrics_data() scope_metrics = metrics_data.resource_metrics[0].scope_metrics[0] self.assertEqual(scope_metrics.scope.name, "opentelemetry-sdk") metrics = sorted(scope_metrics.metrics, key=lambda m: m.name) self.assertEqual(len(metrics), 1) self.assertEqual(metrics[0].name, "otel.sdk.processor.log.processed") processed_data_points = metrics[0].data.data_points self.assertEqual(len(processed_data_points), 1) processed_data_point0 = processed_data_points[0] # All 3 logs are counted as processed when submitted to the exporter, # independent of the export outcome (the 3rd export fails). self.assertEqual(processed_data_point0.value, 3) self.assertEqual( processed_data_point0.attributes["otel.component.type"], "simple_log_processor", ) self.assertTrue(processed_data_point0.attributes["otel.component.name"].startswith("simple_log_processor/")) self.assertIsNone(processed_data_point0.attributes.get("error.type")) @patch.dict("os.environ", {OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED: "true"}) def test_metrics_already_shutdown(self): metric_reader = InMemoryMetricReader() meter_provider = MeterProvider(metric_readers=[metric_reader]) exporter = mock.MagicMock() exporter.export.return_value = LogRecordExportResult.SUCCESS processor = SimpleLogRecordProcessor(exporter, meter_provider=meter_provider) processor.on_emit(EMPTY_LOG) # Shut only the processor down; the record emitted afterwards hits the # already-shutdown early return and is counted as processed with # error.type=already_shutdown (never handed to the exporter). processor.shutdown() processor.on_emit(EMPTY_LOG) metrics_data = metric_reader.get_metrics_data() scope_metrics = metrics_data.resource_metrics[0].scope_metrics[0] metrics = scope_metrics.metrics self.assertEqual(len(metrics), 1) self.assertEqual(metrics[0].name, "otel.sdk.processor.log.processed") data_points = sorted( metrics[0].data.data_points, key=lambda dp: dp.attributes.get("error.type", ""), ) self.assertEqual(len(data_points), 2) # Successful submission (before shutdown), no error.type. self.assertEqual(data_points[0].value, 1) self.assertIsNone(data_points[0].attributes.get("error.type")) # Dropped after shutdown. self.assertEqual(data_points[1].value, 1) self.assertEqual(data_points[1].attributes.get("error.type"), "already_shutdown") self.assertEqual(exporter.export.call_count, 1) # Many more test cases for the BatchLogRecordProcessor exist under # opentelemetry-sdk/tests/shared_internal/test_batch_processor.py. # Important: make sure to call .shutdown() on the BatchLogRecordProcessor # before the end of the test, otherwise the worker thread will continue # to run after the end of the test. class TestBatchLogRecordProcessor(unittest.TestCase): def test_emit_call_log_record(self): exporter = InMemoryLogRecordExporter() log_record_processor = Mock(wraps=BatchLogRecordProcessor(exporter)) provider = LoggerProvider() provider.add_log_record_processor(log_record_processor) logger = logging.getLogger("emit_call") logger.propagate = False logger.addHandler(LoggingHandler(logger_provider=provider)) logger.error("error") self.assertEqual(log_record_processor.on_emit.call_count, 1) log_record_processor.shutdown() def test_with_multiple_threads(self): # pylint: disable=no-self-use exporter = InMemoryLogRecordExporter() batch_processor = BatchLogRecordProcessor( exporter, max_queue_size=3000, max_export_batch_size=50, schedule_delay_millis=30000, export_timeout_millis=500, ) def bulk_emit(num_emit): for _ in range(num_emit): batch_processor.on_emit(EMPTY_LOG) total_expected_logs = 0 with ThreadPoolExecutor(max_workers=69) as executor: for num_logs_to_emit in range(1, 70): executor.submit(bulk_emit, num_logs_to_emit) total_expected_logs += num_logs_to_emit executor.shutdown() batch_processor.shutdown() # Wait a bit for logs to flush. time.sleep(2) assert len(exporter.get_finished_logs()) == total_expected_logs def test_args(self): exporter = InMemoryLogRecordExporter() log_record_processor = BatchLogRecordProcessor( exporter, max_queue_size=1024, schedule_delay_millis=2500, max_export_batch_size=256, export_timeout_millis=15000, ) self.assertEqual(log_record_processor._batch_processor._exporter, exporter) self.assertEqual(log_record_processor._batch_processor._max_queue_size, 1024) self.assertEqual(log_record_processor._batch_processor._schedule_delay, 2.5) self.assertEqual(log_record_processor._batch_processor._max_export_batch_size, 256) self.assertEqual(log_record_processor._batch_processor._export_timeout_millis, 15000) log_record_processor.shutdown() @patch.dict( "os.environ", { OTEL_BLRP_MAX_QUEUE_SIZE: "1024", OTEL_BLRP_SCHEDULE_DELAY: "2500", OTEL_BLRP_MAX_EXPORT_BATCH_SIZE: "256", OTEL_BLRP_EXPORT_TIMEOUT: "15000", }, ) def test_env_vars(self): exporter = InMemoryLogRecordExporter() log_record_processor = BatchLogRecordProcessor(exporter) self.assertEqual(log_record_processor._batch_processor._exporter, exporter) self.assertEqual(log_record_processor._batch_processor._max_queue_size, 1024) self.assertEqual(log_record_processor._batch_processor._schedule_delay, 2.5) self.assertEqual(log_record_processor._batch_processor._max_export_batch_size, 256) self.assertEqual(log_record_processor._batch_processor._export_timeout_millis, 15000) log_record_processor.shutdown() def test_args_defaults(self): exporter = InMemoryLogRecordExporter() log_record_processor = BatchLogRecordProcessor(exporter) self.assertEqual(log_record_processor._batch_processor._exporter, exporter) self.assertEqual(log_record_processor._batch_processor._max_queue_size, 2048) self.assertEqual(log_record_processor._batch_processor._schedule_delay, 1) self.assertEqual(log_record_processor._batch_processor._max_export_batch_size, 512) self.assertEqual(log_record_processor._batch_processor._export_timeout_millis, 30000) log_record_processor.shutdown() @patch.dict( "os.environ", { OTEL_BLRP_MAX_QUEUE_SIZE: "a", OTEL_BLRP_SCHEDULE_DELAY: " ", OTEL_BLRP_MAX_EXPORT_BATCH_SIZE: "One", OTEL_BLRP_EXPORT_TIMEOUT: "@", }, ) def test_args_env_var_value_error(self): exporter = InMemoryLogRecordExporter() _logger.disabled = True log_record_processor = BatchLogRecordProcessor(exporter) _logger.disabled = False self.assertEqual(log_record_processor._batch_processor._exporter, exporter) self.assertEqual(log_record_processor._batch_processor._max_queue_size, 2048) self.assertEqual(log_record_processor._batch_processor._schedule_delay, 1) self.assertEqual(log_record_processor._batch_processor._max_export_batch_size, 512) self.assertEqual(log_record_processor._batch_processor._export_timeout_millis, 30000) log_record_processor.shutdown() def test_args_none_defaults(self): exporter = InMemoryLogRecordExporter() log_record_processor = BatchLogRecordProcessor( exporter, max_queue_size=None, schedule_delay_millis=None, max_export_batch_size=None, export_timeout_millis=None, ) self.assertEqual(log_record_processor._batch_processor._exporter, exporter) self.assertEqual(log_record_processor._batch_processor._max_queue_size, 2048) self.assertEqual(log_record_processor._batch_processor._schedule_delay, 1) self.assertEqual(log_record_processor._batch_processor._max_export_batch_size, 512) self.assertEqual(log_record_processor._batch_processor._export_timeout_millis, 30000) log_record_processor.shutdown() def test_validation_negative_max_queue_size(self): exporter = InMemoryLogRecordExporter() self.assertRaises( ValueError, BatchLogRecordProcessor, exporter, max_queue_size=0, ) self.assertRaises( ValueError, BatchLogRecordProcessor, exporter, max_queue_size=-1, ) self.assertRaises( ValueError, BatchLogRecordProcessor, exporter, schedule_delay_millis=0, ) self.assertRaises( ValueError, BatchLogRecordProcessor, exporter, schedule_delay_millis=-1, ) self.assertRaises( ValueError, BatchLogRecordProcessor, exporter, max_export_batch_size=0, ) self.assertRaises( ValueError, BatchLogRecordProcessor, exporter, max_export_batch_size=-1, ) self.assertRaises( ValueError, BatchLogRecordProcessor, exporter, max_queue_size=100, max_export_batch_size=101, ) @patch.dict("os.environ", {OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED: "true"}) def test_metrics_already_shutdown(self): metric_reader = InMemoryMetricReader() meter_provider = MeterProvider(metric_readers=[metric_reader]) exporter = mock.MagicMock() exporter.export.return_value = LogRecordExportResult.SUCCESS processor = BatchLogRecordProcessor(exporter, meter_provider=meter_provider) # Emitted before shutdown: drained on shutdown and counted as a # successful submit to the exporter. processor.on_emit(EMPTY_LOG) processor.shutdown() # Emitted after shutdown: dropped and counted as already_shutdown. processor.on_emit(EMPTY_LOG) metrics_data = metric_reader.get_metrics_data() scope_metrics = metrics_data.resource_metrics[0].scope_metrics[0] processed = next(m for m in scope_metrics.metrics if m.name == "otel.sdk.processor.log.processed") data_points = sorted( processed.data.data_points, key=lambda dp: dp.attributes.get("error.type", ""), ) self.assertEqual(len(data_points), 2) self.assertEqual(data_points[0].value, 1) self.assertIsNone(data_points[0].attributes.get("error.type")) self.assertEqual(data_points[1].value, 1) self.assertEqual(data_points[1].attributes.get("error.type"), "already_shutdown") @patch.dict("os.environ", {OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED: "true"}) def test_metrics(self): # pylint: disable=too-many-locals,too-many-statements metric_reader = InMemoryMetricReader() meter_provider = MeterProvider(metric_readers=[metric_reader]) metric_reader._set_meter_provider(NoOpMeterProvider()) num_exports = 0 first_export_event = threading.Event() last_export_event = threading.Event() run_exports = threading.Event() def export_logs(_logs): first_export_event.set() run_exports.wait() nonlocal num_exports num_exports += 1 if num_exports == 3: last_export_event.set() raise BrokenPipeError("Export failed") return LogRecordExportResult.SUCCESS exporter = mock.MagicMock() exporter.export.side_effect = export_logs processor = BatchLogRecordProcessor( exporter, meter_provider=meter_provider, max_queue_size=1, max_export_batch_size=1, schedule_delay_millis=1_000_000_000, # Manually flush ) provider = LoggerProvider() provider.add_log_record_processor(processor) logger = provider.get_logger("test_batch_metrics") logger.emit(LogRecord(body="foo", severity_number=SeverityNumber.WARN)) # Wait for log to be sent to exporter first_export_event.wait() # Queue empty, export in progress, this log is queued logger.emit(LogRecord(body="bar", severity_number=SeverityNumber.WARN)) # Queue full, this log is dropped logger.emit(LogRecord(body="baz", severity_number=SeverityNumber.WARN)) metrics_data = metric_reader.get_metrics_data() scope_metrics = metrics_data.resource_metrics[0].scope_metrics[0] self.assertEqual(scope_metrics.scope.name, "opentelemetry-sdk") metrics = sorted(scope_metrics.metrics, key=lambda m: m.name) self.assertEqual(len(metrics), 3) self.assertEqual(metrics[0].name, "otel.sdk.processor.log.processed") processed_data_points = sorted( metrics[0].data.data_points, key=lambda dp: dp.attributes.get("error.type", ""), ) # "foo" is counted as processed when submitted to the exporter (before # its export call blocks); "baz" is dropped due to a full queue. self.assertEqual(len(processed_data_points), 2) processed_data_point0 = processed_data_points[0] self.assertEqual(processed_data_point0.value, 1) self.assertEqual( processed_data_point0.attributes["otel.component.type"], "batching_log_processor", ) self.assertTrue(processed_data_point0.attributes["otel.component.name"].startswith("batching_log_processor/")) self.assertIsNone(processed_data_point0.attributes.get("error.type")) processed_data_point_queue_full = processed_data_points[1] self.assertEqual(processed_data_point_queue_full.value, 1) self.assertEqual( processed_data_point_queue_full.attributes.get("error.type"), "queue_full", ) self.assertEqual(metrics[1].name, "otel.sdk.processor.log.queue.capacity") queue_capacity_data_point = metrics[1].data.data_points[0] self.assertEqual(queue_capacity_data_point.value, 1) self.assertEqual( queue_capacity_data_point.attributes["otel.component.type"], "batching_log_processor", ) self.assertTrue( queue_capacity_data_point.attributes["otel.component.name"].startswith("batching_log_processor/") ) self.assertEqual(metrics[2].name, "otel.sdk.processor.log.queue.size") queue_size_data_point = metrics[2].data.data_points[0] self.assertEqual(queue_size_data_point.value, 1) self.assertEqual( queue_size_data_point.attributes["otel.component.type"], "batching_log_processor", ) self.assertTrue(queue_size_data_point.attributes["otel.component.name"].startswith("batching_log_processor/")) run_exports.set() provider.force_flush() # This log is processed and exporter returns an error logger.emit(LogRecord(body="baz", severity_number=SeverityNumber.WARN)) provider.force_flush() last_export_event.wait() metrics_data = metric_reader.get_metrics_data() scope_metrics = metrics_data.resource_metrics[0].scope_metrics[0] self.assertEqual(scope_metrics.scope.name, "opentelemetry-sdk") metrics = sorted(scope_metrics.metrics, key=lambda m: m.name) self.assertEqual(len(metrics), 3) self.assertEqual(metrics[0].name, "otel.sdk.processor.log.processed") processed_data_points = sorted( metrics[0].data.data_points, key=lambda dp: dp.attributes.get("error.type", ""), ) # "foo", "bar" and "failed" are all counted as processed when submitted # to the exporter, independent of the export outcome ("failed" raises). # "baz" remains a queue_full drop. self.assertEqual(len(processed_data_points), 2) processed_data_point0 = processed_data_points[0] self.assertEqual(processed_data_point0.value, 3) self.assertEqual( processed_data_point0.attributes["otel.component.type"], "batching_log_processor", ) self.assertTrue(processed_data_point0.attributes["otel.component.name"].startswith("batching_log_processor/")) self.assertIsNone(processed_data_point0.attributes.get("error.type")) processed_data_point1 = processed_data_points[1] self.assertEqual(processed_data_point1.value, 1) self.assertEqual( processed_data_point1.attributes["otel.component.type"], "batching_log_processor", ) self.assertTrue(processed_data_point1.attributes["otel.component.name"].startswith("batching_log_processor/")) self.assertEqual(processed_data_point1.attributes.get("error.type"), "queue_full") self.assertEqual(metrics[1].name, "otel.sdk.processor.log.queue.capacity") queue_capacity_data_point = metrics[1].data.data_points[0] self.assertEqual(queue_capacity_data_point.value, 1) self.assertEqual( queue_capacity_data_point.attributes["otel.component.type"], "batching_log_processor", ) self.assertTrue( queue_capacity_data_point.attributes["otel.component.name"].startswith("batching_log_processor/") ) self.assertEqual(metrics[2].name, "otel.sdk.processor.log.queue.size") queue_size_data_point = metrics[2].data.data_points[0] self.assertEqual(queue_size_data_point.value, 0) self.assertEqual( queue_size_data_point.attributes["otel.component.type"], "batching_log_processor", ) self.assertTrue(queue_size_data_point.attributes["otel.component.name"].startswith("batching_log_processor/")) provider.shutdown() class TestConsoleLogExporter(unittest.TestCase): def test_export(self): # pylint: disable=no-self-use """Check that the console exporter prints log records.""" ctx = set_span_in_context( NonRecordingSpan( SpanContext( 2604504634922341076776623263868986797, 5213367945872657620, False, TraceFlags(0x01), ) ) ) log_record = ReadableLogRecord( LogRecord( timestamp=int(time.time() * 1e9), context=ctx, severity_text="WARN", severity_number=SeverityNumber.WARN, body="Zhengzhou, We have a heaviest rains in 1000 years", attributes={"a": 1, "b": "c"}, ), resource=SDKResource({"key": "value"}), instrumentation_scope=InstrumentationScope("first_name", "first_version"), ) exporter = ConsoleLogRecordExporter() # Mocking stdout interferes with debugging and test reporting, mock on # the exporter instance instead. with patch.object(exporter, "out") as mock_stdout: exporter.export([log_record]) mock_stdout.write.assert_called_once_with(log_record.to_json() + os.linesep) self.assertEqual(mock_stdout.write.call_count, 1) self.assertEqual(mock_stdout.flush.call_count, 1) def test_export_custom(self): # pylint: disable=no-self-use """Check that console exporter uses custom io, formatter.""" mock_record_str = Mock(str) def formatter(record): # pylint: disable=unused-argument return mock_record_str mock_stdout = Mock() exporter = ConsoleLogRecordExporter(out=mock_stdout, formatter=formatter) exporter.export([EMPTY_LOG]) mock_stdout.write.assert_called_once_with(mock_record_str) def test_force_flush(self): exporter = ConsoleLogRecordExporter() self.assertTrue(exporter.force_flush()) class TestInMemoryLogRecordExporter(unittest.TestCase): def test_force_flush(self): exporter = InMemoryLogRecordExporter() self.assertTrue(exporter.force_flush()) python-opentelemetry-1.45.2/opentelemetry-sdk/tests/logs/test_handler.py000066400000000000000000000501041527070070700266320ustar00rootroot00000000000000# 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.sdk import trace from opentelemetry.sdk._logs import ( LoggerProvider, LoggingHandler, 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, ) # pylint: disable=too-many-public-methods class TestLoggingHandler(unittest.TestCase): def test_warns_when_used(self): with self.assertWarnsRegex( DeprecationWarning, "`LoggingHandler` in `opentelemetry-sdk` is deprecated", ): LoggingHandler() 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_provder = NoOpLoggerProvider() logger_mock = APIGetLogger(__name__, logger_provider=noop_logger_provder) 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.sdk._logs._internal.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), 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_user_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_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) @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 10 dropped attributes (10 custom + 3 code - 3 kept = 10 dropped) self.assertEqual( record.dropped_attributes, 10, f"Should have 10 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 6 dropped attributes (8 user + 3 code - 5 kept = 6 dropped) self.assertEqual( record.dropped_attributes, 6, f"Should have 6 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, handler = 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 25 dropped attributes (150 user + 3 code - 128 kept = 25 dropped) self.assertEqual( record.dropped_attributes, 25, f"Should have 25 dropped attributes, got {record.dropped_attributes}", ) logger.removeHandler(handler) def set_up_test_logging(level, formatter=None, root_logger=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) 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] python-opentelemetry-1.45.2/opentelemetry-sdk/tests/logs/test_log_limits.py000066400000000000000000000111721527070070700273610ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import unittest from unittest.mock import patch from opentelemetry.sdk._logs import LogRecordLimits from opentelemetry.sdk._logs._internal import ( _DEFAULT_OTEL_ATTRIBUTE_COUNT_LIMIT, ) from opentelemetry.sdk.environment_variables import ( OTEL_ATTRIBUTE_COUNT_LIMIT, OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT, OTEL_LOGRECORD_ATTRIBUTE_COUNT_LIMIT, OTEL_LOGRECORD_ATTRIBUTE_VALUE_LENGTH_LIMIT, ) class TestLogLimits(unittest.TestCase): def test_log_limits_repr_unset(self): expected = ( f"LogRecordLimits(" f"max_attributes={_DEFAULT_OTEL_ATTRIBUTE_COUNT_LIMIT}, " f"max_attribute_length=None, " f"max_log_record_attributes={_DEFAULT_OTEL_ATTRIBUTE_COUNT_LIMIT}, " f"max_log_record_attribute_length=None)" ) limits = str(LogRecordLimits()) self.assertEqual(expected, limits) def test_log_limits_max_attributes(self): expected = 1 limits = LogRecordLimits(max_attributes=1) self.assertEqual(expected, limits.max_attributes) def test_log_limits_max_attribute_length(self): expected = 1 limits = LogRecordLimits(max_attribute_length=1) self.assertEqual(expected, limits.max_attribute_length) def test_log_limits_max_log_record_attributes(self): limits = LogRecordLimits(max_log_record_attributes=5) self.assertEqual(5, limits.max_log_record_attributes) def test_log_limits_max_log_record_attribute_length(self): limits = LogRecordLimits(max_log_record_attribute_length=10) self.assertEqual(10, limits.max_log_record_attribute_length) @patch.dict("os.environ", {OTEL_LOGRECORD_ATTRIBUTE_COUNT_LIMIT: "7"}) def test_logrecord_count_env_var(self): limits = LogRecordLimits() self.assertEqual(7, limits.max_log_record_attributes) self.assertEqual(_DEFAULT_OTEL_ATTRIBUTE_COUNT_LIMIT, limits.max_attributes) @patch.dict("os.environ", {OTEL_LOGRECORD_ATTRIBUTE_VALUE_LENGTH_LIMIT: "20"}) def test_logrecord_length_env_var(self): limits = LogRecordLimits() self.assertEqual(20, limits.max_log_record_attribute_length) self.assertIsNone(limits.max_attribute_length) @patch.dict( "os.environ", { OTEL_ATTRIBUTE_COUNT_LIMIT: "50", OTEL_LOGRECORD_ATTRIBUTE_COUNT_LIMIT: "3", }, ) def test_logrecord_count_env_takes_precedence(self): limits = LogRecordLimits() self.assertEqual(50, limits.max_attributes) self.assertEqual(3, limits.max_log_record_attributes) @patch.dict( "os.environ", { OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT: "100", OTEL_LOGRECORD_ATTRIBUTE_VALUE_LENGTH_LIMIT: "25", }, ) def test_logrecord_length_env_takes_precedence(self): limits = LogRecordLimits() self.assertEqual(100, limits.max_attribute_length) self.assertEqual(25, limits.max_log_record_attribute_length) @patch.dict("os.environ", {OTEL_ATTRIBUTE_COUNT_LIMIT: "42"}, clear=True) def test_global_count_env_applies_as_fallback(self): limits = LogRecordLimits() self.assertEqual(42, limits.max_attributes) self.assertEqual(42, limits.max_log_record_attributes) @patch.dict("os.environ", {OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT: "60"}, clear=True) def test_global_length_env_applies_as_fallback(self): limits = LogRecordLimits() self.assertEqual(60, limits.max_attribute_length) self.assertEqual(60, limits.max_log_record_attribute_length) def test_invalid_env_vars_raise(self): env_vars = [ OTEL_ATTRIBUTE_COUNT_LIMIT, OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT, OTEL_LOGRECORD_ATTRIBUTE_COUNT_LIMIT, OTEL_LOGRECORD_ATTRIBUTE_VALUE_LENGTH_LIMIT, ] bad_values = ["bad", "-1"] test_cases = {env_var: bad_value for env_var in env_vars for bad_value in bad_values} for env_var, bad_value in test_cases.items(): with self.subTest(f"Testing {env_var}={bad_value}"): with ( self.assertRaises(ValueError) as error, patch.dict("os.environ", {env_var: bad_value}, clear=True), ): LogRecordLimits() expected_msg = f"{env_var} must be a non-negative integer but got {bad_value}" self.assertEqual( expected_msg, str(error.exception), f"Unexpected error message for {env_var}={bad_value}", ) python-opentelemetry-1.45.2/opentelemetry-sdk/tests/logs/test_log_record.py000066400000000000000000000172511527070070700273420ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import json import unittest import warnings from opentelemetry._logs import LogRecord, SeverityNumber from opentelemetry.attributes import BoundedAttributes from opentelemetry.context import get_current from opentelemetry.sdk._logs import ( LogRecordDroppedAttributesWarning, LogRecordLimits, ReadableLogRecord, ReadWriteLogRecord, ) from opentelemetry.sdk.resources import Resource from opentelemetry.trace.span import TraceFlags class TestLogRecord(unittest.TestCase): def test_log_record_to_json(self): log_record = ReadableLogRecord( LogRecord( timestamp=0, observed_timestamp=0, body={"key": "logLine", "bytes": b"123"}, attributes={ "mapping": {"key": "value"}, "none": None, "sequence": [1, 2], "str": "string", }, event_name="a.event", ), resource=Resource({"service.name": "foo"}), ) self.assertEqual( log_record.to_json(indent=None), '{"body": {"key": "logLine", "bytes": "MTIz"}, "severity_number": null, "severity_text": null, "attributes": {"mapping": {"key": "value"}, "none": null, "sequence": [1, 2], "str": "string"}, "dropped_attributes": 0, "timestamp": "1970-01-01T00:00:00.000000Z", "observed_timestamp": "1970-01-01T00:00:00.000000Z", "trace_id": "0x00000000000000000000000000000000", "span_id": "0x0000000000000000", "trace_flags": 0, "resource": {"attributes": {"service.name": "foo"}, "schema_url": ""}, "event_name": "a.event"}', ) def test_log_record_to_json_serializes_severity_number_as_int(self): actual = ReadableLogRecord( LogRecord( timestamp=0, severity_number=SeverityNumber.WARN, observed_timestamp=0, body="a log line", ), resource=Resource({"service.name": "foo"}), ) decoded = json.loads(actual.to_json()) self.assertEqual(SeverityNumber.WARN.value, decoded["severity_number"]) def test_log_record_to_json_serializes_null_severity_number(self): actual = ReadableLogRecord( LogRecord( observed_timestamp=0, body="a log line", ), resource=Resource({"service.name": "foo"}), ) decoded = json.loads(actual.to_json()) self.assertEqual(None, decoded["timestamp"]) def test_log_record_bounded_attributes(self): attr = {"key": "value"} result = ReadWriteLogRecord(LogRecord(timestamp=0, body="a log line", attributes=attr)) self.assertTrue(isinstance(result.log_record.attributes, BoundedAttributes)) def test_log_record_dropped_attributes_empty_limits(self): attr = {"key": "value"} result = ReadWriteLogRecord(LogRecord(timestamp=0, body="a log line", attributes=attr)) self.assertTrue(result.dropped_attributes == 0) def test_log_record_dropped_attributes_set_limits_max_attribute(self): attr = {"key": "value", "key2": "value2"} limits = LogRecordLimits( max_attributes=1, ) result = ReadWriteLogRecord( LogRecord(timestamp=0, body="a log line", attributes=attr), limits=limits, ) self.assertTrue(result.dropped_attributes == 1) def test_log_record_dropped_attributes_set_limits_max_attribute_length( self, ): attr = {"key": "value", "key2": "value2"} expected = {"key": "v", "key2": "v"} limits = LogRecordLimits( max_attribute_length=1, ) result = ReadWriteLogRecord( LogRecord( timestamp=0, body="a log line", attributes=attr, ), limits=limits, ) self.assertTrue(result.dropped_attributes == 0) self.assertEqual(expected, result.log_record.attributes) def test_log_record_dropped_attributes_set_limits(self): attr = {"key": "value", "key2": "value2"} expected = {"key2": "v"} limits = LogRecordLimits( max_attributes=1, max_attribute_length=1, ) result = ReadWriteLogRecord( LogRecord( timestamp=0, body="a log line", attributes=attr, ), limits=limits, ) self.assertTrue(result.dropped_attributes == 1) self.assertEqual(expected, result.log_record.attributes) def test_log_record_dropped_attributes_set_limits_warning_once(self): attr = {"key1": "value1", "key2": "value2"} limits = LogRecordLimits( max_attributes=1, max_attribute_length=1, ) with warnings.catch_warnings(record=True) as cw: for _ in range(10): ReadWriteLogRecord( LogRecord( timestamp=0, body="a log line", attributes=attr, ), limits=limits, ) # Check that at least one LogRecordDroppedAttributesWarning was emitted dropped_attributes_warnings = [w for w in cw if isinstance(w.message, LogRecordDroppedAttributesWarning)] self.assertEqual( len(dropped_attributes_warnings), 1, "Expected exactly one LogRecordDroppedAttributesWarning due to simplefilter('once')", ) # Check the message content of the LogRecordDroppedAttributesWarning warning_message = str(dropped_attributes_warnings[0].message) self.assertIn( "Log record attributes were dropped due to limits", warning_message, ) def test_log_record_dropped_attributes_unset_limits(self): attr = {"key": "value", "key2": "value2"} limits = LogRecordLimits() result = ReadWriteLogRecord( LogRecord( timestamp=0, body="a log line", attributes=attr, ), limits=limits, ) self.assertTrue(result.dropped_attributes == 0) self.assertEqual(attr, result.log_record.attributes) # pylint:disable=protected-access def test_log_record_from_api_log_record(self): api_log_record = LogRecord( timestamp=1, observed_timestamp=2, context=get_current(), severity_text="WARN", severity_number=SeverityNumber.WARN, body="a log line", attributes={"a": "b"}, event_name="an.event", ) resource = Resource.create({}) record = ReadWriteLogRecord._from_api_log_record(record=api_log_record, resource=resource) self.assertEqual(record.log_record.timestamp, 1) self.assertEqual(record.log_record.observed_timestamp, 2) self.assertEqual(record.log_record.context, get_current()) # trace_id, span_id, and trace_flags come from the context's span self.assertEqual(record.log_record.trace_id, 0) self.assertEqual(record.log_record.span_id, 0) self.assertEqual(record.log_record.trace_flags, TraceFlags(0x00)) self.assertEqual(record.log_record.severity_text, "WARN") self.assertEqual(record.log_record.severity_number, SeverityNumber.WARN) self.assertEqual(record.log_record.body, "a log line") self.assertEqual(record.log_record.attributes, {"a": "b"}) self.assertEqual(record.log_record.event_name, "an.event") self.assertEqual(record.resource, resource) python-opentelemetry-1.45.2/opentelemetry-sdk/tests/logs/test_logger_provider_cache.py000066400000000000000000000071031527070070700315320ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import logging import unittest from opentelemetry.sdk._logs import LoggerProvider, LoggingHandler 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, logger_provider def create_logger(handler, name): logger = logging.getLogger(name) logger.addHandler(handler) return logger class TestLoggerProviderCache(unittest.TestCase): def test_get_logger_single_handler(self): handler, logger_provider = set_up_logging_handler(level=logging.DEBUG) # pylint: disable=protected-access logger_cache = logger_provider._logger_cache logger = create_logger(handler, "test_logger") # Ensure logger is lazily cached self.assertEqual(0, len(logger_cache)) with self.assertLogs(level=logging.WARNING): logger.warning("test message") self.assertEqual(1, len(logger_cache)) # Ensure only one logger is cached with self.assertLogs(level=logging.WARNING): rounds = 100 for _ in range(rounds): logger.warning("test message") self.assertEqual(1, len(logger_cache)) def test_get_logger_multiple_loggers(self): handler, logger_provider = set_up_logging_handler(level=logging.DEBUG) # pylint: disable=protected-access logger_cache = logger_provider._logger_cache num_loggers = 10 loggers = [create_logger(handler, str(i)) for i in range(num_loggers)] # Ensure loggers are lazily cached self.assertEqual(0, len(logger_cache)) with self.assertLogs(level=logging.WARNING): for logger in loggers: logger.warning("test message") self.assertEqual(num_loggers, len(logger_cache)) with self.assertLogs(level=logging.WARNING): rounds = 100 for _ in range(rounds): for logger in loggers: logger.warning("test message") self.assertEqual(num_loggers, len(logger_cache)) def test_provider_get_logger_no_cache(self): _, logger_provider = set_up_logging_handler(level=logging.DEBUG) # pylint: disable=protected-access logger_cache = logger_provider._logger_cache logger_provider.get_logger( name="test_logger", version="version", schema_url="schema_url", attributes={"key": "value"}, ) # Ensure logger is not cached if attributes is set self.assertEqual(0, len(logger_cache)) def test_provider_get_logger_cached(self): _, logger_provider = set_up_logging_handler(level=logging.DEBUG) # pylint: disable=protected-access logger_cache = logger_provider._logger_cache logger_provider.get_logger( name="test_logger", version="version", schema_url="schema_url", ) # Ensure only one logger is cached self.assertEqual(1, len(logger_cache)) logger_provider.get_logger( name="test_logger", version="version", schema_url="schema_url", ) # Ensure only one logger is cached self.assertEqual(1, len(logger_cache)) python-opentelemetry-1.45.2/opentelemetry-sdk/tests/logs/test_logs.py000066400000000000000000000523771527070070700261770ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # pylint: disable=protected-access import json import os import subprocess import sys import unittest from pathlib import Path from unittest.mock import Mock, patch from opentelemetry._logs import LogRecord, SeverityNumber from opentelemetry.attributes import BoundedAttributes from opentelemetry.context import get_current from opentelemetry.sdk._logs import ( Logger, LoggerProvider, ReadableLogRecord, ReadWriteLogRecord, ) from opentelemetry.sdk._logs._internal import ( NoOpLogger, SynchronousMultiLogRecordProcessor, _disable_logger_configurator, _LoggerConfig, _RuleBasedLoggerConfigurator, create_logger_metrics, ) from opentelemetry.sdk.environment_variables import ( OTEL_EXPERIMENTAL_RESOURCE_DETECTORS, OTEL_SDK_DISABLED, ) from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.util.instrumentation import ( InstrumentationScope, _scope_name_matches_glob, ) from opentelemetry.semconv.attributes import exception_attributes class TestLoggerProvider(unittest.TestCase): def test_resource(self): """ `LoggerProvider` provides a way to allow a `Resource` to be specified. """ logger_provider_0 = LoggerProvider() logger_provider_1 = LoggerProvider() self.assertEqual( logger_provider_0.resource, logger_provider_1.resource, ) self.assertIsInstance(logger_provider_0.resource, Resource) self.assertIsInstance(logger_provider_1.resource, Resource) resource = Resource({"key": "value"}) self.assertIs(LoggerProvider(resource=resource).resource, resource) def test_update_resource(self): initial_resource = Resource({"one": "one", "two": "old"}) updating_resource = Resource({"two": "new", "three": "three"}) logger_provider = LoggerProvider(resource=initial_resource) logger = logger_provider.get_logger("name") other_logger = logger_provider.get_logger("other", attributes={"key": "value"}) processor_mock = Mock() logger_provider.add_log_record_processor(processor_mock) logger_provider._update_resource(updating_resource) self.assertEqual( logger_provider.resource.attributes, {"one": "one", "two": "new", "three": "three"}, ) self.assertIs(logger.resource, logger_provider.resource) self.assertIs(other_logger.resource, logger_provider.resource) logger.emit(LogRecord(observed_timestamp=0, body="a log line")) log_data = processor_mock.on_emit.call_args.args[0] self.assertIs(log_data.resource, logger_provider.resource) new_logger = logger_provider.get_logger("new") self.assertIs(new_logger.resource, logger_provider.resource) @unittest.skipUnless( hasattr(os, "fork") and hasattr(os, "register_at_fork"), "requires os.fork and os.register_at_fork", ) def test_logger_provider_updates_process_dependent_resource_after_fork( self, ): script_path = Path(__file__).parent / "scripts" / "logger_provider_resource_after_fork.py" result = subprocess.run( [sys.executable, str(script_path)], capture_output=True, check=False, text=True, env={ **os.environ, OTEL_EXPERIMENTAL_RESOURCE_DETECTORS: "process", }, ) self.assertEqual( result.returncode, 0, f"stdout: {result.stdout}\nstderr: {result.stderr}", ) lines = result.stdout.strip().splitlines() child_payload = json.loads(lines[0]) parent_payload = json.loads(lines[1]) self.assertEqual(parent_payload["parent_resource_pid"], parent_payload["parent_pid"]) self.assertEqual(parent_payload["parent_logger_pid"], parent_payload["parent_pid"]) self.assertEqual( parent_payload["parent_resource_pid_after_fork"], parent_payload["parent_pid"], ) self.assertEqual( parent_payload["parent_logger_pid_after_fork"], parent_payload["parent_pid"], ) self.assertNotEqual(child_payload["child_pid"], parent_payload["parent_pid"]) self.assertEqual(child_payload["provider_pid"], child_payload["child_pid"]) self.assertEqual(child_payload["cached_logger_pid"], child_payload["child_pid"]) self.assertEqual(child_payload["new_logger_pid"], child_payload["child_pid"]) self.assertEqual( child_payload["exported_resource_pids"], [child_payload["child_pid"], child_payload["child_pid"]], ) self.assertEqual(child_payload["log_bodies"], ["cached", "new"]) def test_get_logger(self): """ `LoggerProvider.get_logger` arguments are used to create an `InstrumentationScope` object on the created `Logger`. """ logger = LoggerProvider().get_logger( "name", version="version", schema_url="schema_url", attributes={"key": "value"}, ) self.assertEqual(logger._instrumentation_scope.name, "name") self.assertEqual(logger._instrumentation_scope.version, "version") self.assertEqual(logger._instrumentation_scope.schema_url, "schema_url") self.assertEqual(logger._instrumentation_scope.attributes, {"key": "value"}) @patch.dict("os.environ", {OTEL_SDK_DISABLED: "true"}) def test_get_logger_with_sdk_disabled(self): logger = LoggerProvider().get_logger(Mock()) self.assertIsInstance(logger, NoOpLogger) @patch.object(Resource, "create") def test_logger_provider_init(self, resource_patch): logger_provider = LoggerProvider() resource_patch.assert_called_once() self.assertIsNotNone(logger_provider._resource) self.assertTrue( isinstance( logger_provider._multi_log_record_processor, SynchronousMultiLogRecordProcessor, ) ) self.assertIsNotNone(logger_provider._at_exit_handler) def test_default_logger_configurator(self): provider = LoggerProvider() logger = provider.get_logger("module_name", "1.0", "schema_url") other_logger = provider.get_logger("other_module_name", "1.0", "schema_url") self.assertTrue(logger._is_enabled()) self.assertTrue(other_logger._is_enabled()) def test_logger_provider_with_disabled_configurator(self): provider = LoggerProvider(_logger_configurator=_disable_logger_configurator) logger = provider.get_logger("test") self.assertFalse(logger._is_enabled()) def test_logger_provider_with_custom_configurator(self): def configurator(scope): if scope.name == "disabled_logger": return _LoggerConfig(is_enabled=False) return _LoggerConfig.default() provider = LoggerProvider(_logger_configurator=configurator) enabled = provider.get_logger("enabled_logger") disabled = provider.get_logger("disabled_logger") self.assertTrue(enabled._is_enabled()) self.assertFalse(disabled._is_enabled()) def test_set_logger_configurator_updates_existing_loggers(self): provider = LoggerProvider() logger = provider.get_logger("test") self.assertTrue(logger._is_enabled()) provider._set_logger_configurator(logger_configurator=_disable_logger_configurator) self.assertFalse(logger._is_enabled()) def test_set_logger_configurator_affects_new_loggers(self): provider = LoggerProvider() provider._set_logger_configurator(logger_configurator=_disable_logger_configurator) logger = provider.get_logger("new_logger") self.assertFalse(logger._is_enabled()) # pylint: disable-next=no-self-use def test_disabled_logger_skips_emit(self): provider = LoggerProvider(_logger_configurator=_disable_logger_configurator) logger = provider.get_logger("test") processor_mock = Mock() provider.add_log_record_processor(processor_mock) logger.emit(LogRecord(observed_timestamp=0, body="should not be emitted")) processor_mock.on_emit.assert_not_called() def test_rule_based_logger_configurator(self): rules = [ ( _scope_name_matches_glob(glob_pattern="module_name"), _LoggerConfig(is_enabled=True), ), ( _scope_name_matches_glob(glob_pattern="other_module_name"), _LoggerConfig(is_enabled=False), ), ] configurator = _RuleBasedLoggerConfigurator(rules=rules, default_config=_LoggerConfig(is_enabled=True)) provider = LoggerProvider() logger = provider.get_logger("module_name", "1.0", "schema_url") other_logger = provider.get_logger("other_module_name", "1.0", "schema_url") self.assertTrue(logger._is_enabled()) self.assertTrue(other_logger._is_enabled()) provider._set_logger_configurator(logger_configurator=configurator) self.assertTrue(logger._is_enabled()) self.assertFalse(other_logger._is_enabled()) def test_rule_based_logger_configurator_default_when_rules_dont_match( self, ): rules = [ ( _scope_name_matches_glob(glob_pattern="module_name"), _LoggerConfig(is_enabled=False), ), ] configurator = _RuleBasedLoggerConfigurator(rules=rules, default_config=_LoggerConfig(is_enabled=True)) provider = LoggerProvider() logger = provider.get_logger("module_name", "1.0", "schema_url") other_logger = provider.get_logger("other_module_name", "1.0", "schema_url") self.assertTrue(logger._is_enabled()) self.assertTrue(other_logger._is_enabled()) provider._set_logger_configurator(logger_configurator=configurator) self.assertFalse(logger._is_enabled()) self.assertTrue(other_logger._is_enabled()) def test_rule_based_configurator_first_match_wins(self): disabled_config = _LoggerConfig(is_enabled=False) enabled_config = _LoggerConfig(is_enabled=True) configurator = _RuleBasedLoggerConfigurator( rules=[ (lambda s: s.name == "foo", disabled_config), (lambda s: s.name == "foo", enabled_config), ], default_config=enabled_config, ) scope = InstrumentationScope("foo", "1.0") result = configurator(scope) self.assertFalse(result.is_enabled) class TestReadableLogRecord(unittest.TestCase): def setUp(self): self.log_record = LogRecord( timestamp=1234567890, observed_timestamp=1234567891, body="Test log message", attributes={"key": "value"}, severity_number=SeverityNumber.INFO, severity_text="INFO", ) self.resource = Resource({"service.name": "test-service"}) self.readable_log_record = ReadableLogRecord( log_record=self.log_record, resource=self.resource, instrumentation_scope=None, ) def test_readable_log_record_is_frozen(self): """Test that ReadableLogRecord is frozen and cannot be modified.""" with self.assertRaises((AttributeError, TypeError)): self.readable_log_record.log_record = LogRecord(timestamp=999, body="Modified") def test_readable_log_record_can_read_attributes(self): """Test that ReadableLogRecord provides read access to all fields.""" self.assertEqual(self.readable_log_record.log_record.timestamp, 1234567890) self.assertEqual(self.readable_log_record.log_record.body, "Test log message") self.assertEqual(self.readable_log_record.log_record.attributes["key"], "value") self.assertEqual( self.readable_log_record.resource.attributes["service.name"], "test-service", ) class TestLogger(unittest.TestCase): @staticmethod def _get_logger(): log_record_processor_mock = Mock() logger = Logger( resource=Resource.create({}), multi_log_record_processor=log_record_processor_mock, instrumentation_scope=InstrumentationScope( "name", "version", "schema_url", {"an": "attribute"}, ), logger_metrics=create_logger_metrics(Mock(), False), _logger_config=_LoggerConfig.default(), ) return logger, log_record_processor_mock def test_can_emit_logrecord(self): logger, log_record_processor_mock = self._get_logger() log_record = LogRecord( observed_timestamp=0, body="a log line", ) logger.emit(log_record) log_record_processor_mock.on_emit.assert_called_once() log_data = log_record_processor_mock.on_emit.call_args.args[0] self.assertTrue(isinstance(log_data.log_record, LogRecord)) self.assertTrue(log_data.log_record is log_record) def test_can_emit_api_logrecord(self): logger, log_record_processor_mock = self._get_logger() api_log_record = LogRecord( observed_timestamp=0, body="a log line", ) logger.emit(api_log_record) log_record_processor_mock.on_emit.assert_called_once() log_data = log_record_processor_mock.on_emit.call_args.args[0] log_record = log_data.log_record self.assertTrue(isinstance(log_record, LogRecord)) self.assertEqual(log_record.timestamp, None) self.assertEqual(log_record.observed_timestamp, 0) self.assertIsNotNone(log_record.context) self.assertEqual(log_record.severity_number, None) self.assertEqual(log_record.severity_text, None) self.assertEqual(log_record.body, "a log line") self.assertEqual(log_record.attributes, {}) self.assertEqual(log_record.event_name, None) self.assertEqual(log_data.resource, logger.resource) def test_can_emit_with_keywords_arguments(self): logger, log_record_processor_mock = self._get_logger() log_record = LogRecord( timestamp=100, observed_timestamp=101, context=get_current(), severity_number=SeverityNumber.WARN, severity_text="warn", body="a body", attributes={"some": "attributes"}, event_name="event_name", ) logger.emit(log_record) log_record_processor_mock.on_emit.assert_called_once() log_data = log_record_processor_mock.on_emit.call_args.args[0] result_log_record = log_data.log_record self.assertTrue(isinstance(result_log_record, LogRecord)) self.assertEqual(result_log_record.timestamp, 100) self.assertEqual(result_log_record.observed_timestamp, 101) self.assertIsNotNone(result_log_record.context) self.assertEqual(result_log_record.severity_number, SeverityNumber.WARN) self.assertEqual(result_log_record.severity_text, "warn") self.assertEqual(result_log_record.body, "a body") self.assertEqual(result_log_record.attributes, {"some": "attributes"}) self.assertEqual(result_log_record.event_name, "event_name") self.assertEqual(log_data.resource, logger.resource) def test_emit_with_exception_adds_attributes(self): logger, log_record_processor_mock = self._get_logger() exc = ValueError("boom") logger.emit(body="a log line", exception=exc) log_record_processor_mock.on_emit.assert_called_once() log_data = log_record_processor_mock.on_emit.call_args.args[0] attributes = dict(log_data.log_record.attributes) self.assertEqual(attributes[exception_attributes.EXCEPTION_TYPE], "ValueError") self.assertEqual(attributes[exception_attributes.EXCEPTION_MESSAGE], "boom") self.assertIn( "ValueError: boom", attributes[exception_attributes.EXCEPTION_STACKTRACE], ) def test_emit_with_raised_exception_has_stacktrace(self): logger, log_record_processor_mock = self._get_logger() try: raise ValueError("boom") except ValueError as exc: logger.emit(body="error", exception=exc) log_record_processor_mock.on_emit.assert_called_once() log_data = log_record_processor_mock.on_emit.call_args.args[0] stacktrace = dict(log_data.log_record.attributes)[exception_attributes.EXCEPTION_STACKTRACE] self.assertIn("Traceback (most recent call last)", stacktrace) self.assertIn("raise ValueError", stacktrace) def test_emit_logrecord_exception_preserves_user_attributes(self): logger, log_record_processor_mock = self._get_logger() exc = ValueError("boom") log_record = LogRecord( observed_timestamp=0, body="a log line", attributes={exception_attributes.EXCEPTION_TYPE: "custom"}, exception=exc, ) logger.emit(log_record) log_record_processor_mock.on_emit.assert_called_once() log_data = log_record_processor_mock.on_emit.call_args.args[0] attributes = dict(log_data.log_record.attributes) self.assertEqual(attributes[exception_attributes.EXCEPTION_TYPE], "custom") self.assertEqual(attributes[exception_attributes.EXCEPTION_MESSAGE], "boom") def test_emit_logrecord_exception_with_immutable_attributes(self): logger, log_record_processor_mock = self._get_logger() exc = ValueError("boom") original_attributes = BoundedAttributes( attributes={"custom": "value"}, immutable=True, ) log_record = LogRecord( observed_timestamp=0, body="a log line", attributes=original_attributes, exception=exc, ) logger.emit(log_record) self.assertNotIn(exception_attributes.EXCEPTION_TYPE, log_record.attributes) log_record_processor_mock.on_emit.assert_called_once() log_data = log_record_processor_mock.on_emit.call_args.args[0] attributes = dict(log_data.log_record.attributes) self.assertEqual(attributes["custom"], "value") self.assertEqual(attributes[exception_attributes.EXCEPTION_TYPE], "ValueError") def test_emit_readwrite_logrecord_uses_exception(self): logger, log_record_processor_mock = self._get_logger() exc = RuntimeError("kaput") log_record = LogRecord( observed_timestamp=0, body="a log line", exception=exc, ) readwrite = ReadWriteLogRecord( log_record=log_record, resource=Resource.create({}), instrumentation_scope=logger._instrumentation_scope, ) logger.emit(readwrite) log_record_processor_mock.on_emit.assert_called_once() log_data = log_record_processor_mock.on_emit.call_args.args[0] attributes = dict(log_data.log_record.attributes) self.assertEqual(attributes[exception_attributes.EXCEPTION_TYPE], "RuntimeError") def test_enabled_with_no_processors_returns_false(self): provider = LoggerProvider() logger = provider.get_logger("test") self.assertFalse(logger.enabled()) def test_enabled_with_processor_returns_true(self): provider = LoggerProvider() processor = Mock() processor.enabled.return_value = True provider.add_log_record_processor(processor) logger = provider.get_logger("test") self.assertTrue(logger.enabled()) processor.enabled.assert_called_once() def test_enabled_disabled_logger_returns_false(self): provider = LoggerProvider(_logger_configurator=_disable_logger_configurator) provider.add_log_record_processor(Mock()) logger = provider.get_logger("test") self.assertFalse(logger.enabled()) def test_enabled_passes_args_to_processor(self): # pylint: disable=no-self-use provider = LoggerProvider() processor_mock = Mock() processor_mock.enabled.return_value = True provider.add_log_record_processor(processor_mock) logger = provider.get_logger("test") logger.enabled(severity_number=SeverityNumber.INFO, event_name="my.event") processor_mock.enabled.assert_called_once_with( context=None, instrumentation_scope=logger._instrumentation_scope, severity_number=SeverityNumber.INFO, event_name="my.event", ) def test_enabled_all_processors_disabled_returns_false(self): provider = LoggerProvider() p1 = Mock() p1.enabled.return_value = False p2 = Mock() p2.enabled.return_value = False provider.add_log_record_processor(p1) provider.add_log_record_processor(p2) logger = provider.get_logger("test") self.assertFalse(logger.enabled()) def test_enabled_one_processor_enabled_returns_true(self): provider = LoggerProvider() p1 = Mock() p1.enabled.return_value = False p2 = Mock() p2.enabled.return_value = True provider.add_log_record_processor(p1) provider.add_log_record_processor(p2) logger = provider.get_logger("test") self.assertTrue(logger.enabled()) python-opentelemetry-1.45.2/opentelemetry-sdk/tests/logs/test_multi_log_processor.py000066400000000000000000000222321527070070700313100ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # pylint:disable=protected-access,no-self-use,no-member import logging import threading import time import unittest from abc import ABC, abstractmethod from unittest.mock import Mock from opentelemetry._logs import LogRecord, SeverityNumber from opentelemetry.sdk._logs._internal import ( ConcurrentMultiLogRecordProcessor, LoggerProvider, LoggingHandler, LogRecordProcessor, ReadWriteLogRecord, SynchronousMultiLogRecordProcessor, ) class AnotherLogRecordProcessor(LogRecordProcessor): def __init__(self, exporter, logs_list): self._exporter = exporter self._log_list = logs_list self._closed = False def on_emit(self, log_record: ReadWriteLogRecord): if self._closed: return self._log_list.append( ( log_record.log_record.body, log_record.log_record.severity_text, ) ) def shutdown(self): self._closed = True self._exporter.shutdown() def force_flush(self, timeout_millis=30000): self._log_list.clear() return True class TestLogRecordProcessor(unittest.TestCase): def test_log_record_processor(self): provider = LoggerProvider() handler = LoggingHandler(logger_provider=provider) logs_list_1 = [] processor1 = AnotherLogRecordProcessor(Mock(), logs_list_1) logs_list_2 = [] processor2 = AnotherLogRecordProcessor(Mock(), logs_list_2) logger = logging.getLogger("test.span.processor") logger.addHandler(handler) # Test no proessor added with self.assertLogs(level=logging.CRITICAL): logger.critical("Odisha, we have another major cyclone") self.assertEqual(len(logs_list_1), 0) self.assertEqual(len(logs_list_2), 0) # Add one processor provider.add_log_record_processor(processor1) with self.assertLogs(level=logging.WARNING): logger.warning("Brace yourself") with self.assertLogs(level=logging.ERROR): logger.error("Some error message") expected_list_1 = [ ("Brace yourself", "WARN"), ("Some error message", "ERROR"), ] self.assertEqual(logs_list_1, expected_list_1) # Add another processor provider.add_log_record_processor(processor2) with self.assertLogs(level=logging.CRITICAL): logger.critical("Something disastrous") expected_list_1.append(("Something disastrous", "FATAL")) expected_list_2 = [("Something disastrous", "FATAL")] self.assertEqual(logs_list_1, expected_list_1) self.assertEqual(logs_list_2, expected_list_2) class MultiLogRecordProcessorTestBase(ABC): @abstractmethod def _get_multi_log_record_processor(self): pass def make_record(self): return ReadWriteLogRecord( LogRecord( timestamp=1622300111608942000, severity_text="WARN", severity_number=SeverityNumber.WARN, body="Warning message", ) ) def test_on_emit(self): multi_log_record_processor = self._get_multi_log_record_processor() mocks = [Mock(spec=LogRecordProcessor) for _ in range(5)] for mock in mocks: multi_log_record_processor.add_log_record_processor(mock) record = self.make_record() multi_log_record_processor.on_emit(record) for mock in mocks: mock.on_emit.assert_called_with(record) multi_log_record_processor.shutdown() def test_on_shutdown(self): multi_log_record_processor = self._get_multi_log_record_processor() mocks = [Mock(spec=LogRecordProcessor) for _ in range(5)] for mock in mocks: multi_log_record_processor.add_log_record_processor(mock) multi_log_record_processor.shutdown() for mock in mocks: mock.shutdown.assert_called_once_with() def test_on_force_flush(self): multi_log_record_processor = self._get_multi_log_record_processor() mocks = [Mock(spec=LogRecordProcessor) for _ in range(5)] for mock in mocks: multi_log_record_processor.add_log_record_processor(mock) ret_value = multi_log_record_processor.force_flush(100) self.assertTrue(ret_value) for mock_processor in mocks: self.assertEqual(1, mock_processor.force_flush.call_count) class TestSynchronousMultiLogRecordProcessor(MultiLogRecordProcessorTestBase, unittest.TestCase): def _get_multi_log_record_processor(self): return SynchronousMultiLogRecordProcessor() def test_force_flush_delayed(self): multi_log_record_processor = SynchronousMultiLogRecordProcessor() def delay(_): time.sleep(0.09) mock_processor1 = Mock(spec=LogRecordProcessor) mock_processor1.force_flush = Mock(side_effect=delay) multi_log_record_processor.add_log_record_processor(mock_processor1) mock_processor2 = Mock(spec=LogRecordProcessor) multi_log_record_processor.add_log_record_processor(mock_processor2) ret_value = multi_log_record_processor.force_flush(50) self.assertFalse(ret_value) self.assertEqual(mock_processor1.force_flush.call_count, 1) self.assertEqual(mock_processor2.force_flush.call_count, 0) def test_force_flush_processor_returns_false(self): multi_log_record_processor = SynchronousMultiLogRecordProcessor() mock_processor1 = Mock(spec=LogRecordProcessor) mock_processor1.force_flush = Mock(return_value=False) multi_log_record_processor.add_log_record_processor(mock_processor1) mock_processor2 = Mock(spec=LogRecordProcessor) mock_processor2.force_flush = Mock(return_value=True) multi_log_record_processor.add_log_record_processor(mock_processor2) ret_value = multi_log_record_processor.force_flush(50) self.assertFalse(ret_value) self.assertEqual(mock_processor1.force_flush.call_count, 1) self.assertEqual(mock_processor2.force_flush.call_count, 1) def test_force_flush_processor_returns_none(self): multi_log_record_processor = SynchronousMultiLogRecordProcessor() mock_processor1 = Mock(spec=LogRecordProcessor) mock_processor1.force_flush = Mock(return_value=None) multi_log_record_processor.add_log_record_processor(mock_processor1) mock_processor2 = Mock(spec=LogRecordProcessor) mock_processor2.force_flush = Mock(return_value=True) multi_log_record_processor.add_log_record_processor(mock_processor2) ret_value = multi_log_record_processor.force_flush(50) self.assertTrue(ret_value) self.assertEqual(mock_processor1.force_flush.call_count, 1) self.assertEqual(mock_processor2.force_flush.call_count, 1) class TestConcurrentMultiLogRecordProcessor(MultiLogRecordProcessorTestBase, unittest.TestCase): def _get_multi_log_record_processor(self): return ConcurrentMultiLogRecordProcessor() def test_force_flush_delayed(self): multi_log_record_processor = ConcurrentMultiLogRecordProcessor() wait_event = threading.Event() def delay(_): wait_event.wait() mock1 = Mock(spec=LogRecordProcessor) mock1.force_flush = Mock(side_effect=delay) mocks = [Mock(LogRecordProcessor) for _ in range(5)] mocks = [mock1] + mocks for mock_processor in mocks: multi_log_record_processor.add_log_record_processor(mock_processor) ret_value = multi_log_record_processor.force_flush(50) wait_event.set() self.assertFalse(ret_value) for mock in mocks: self.assertEqual(1, mock.force_flush.call_count) multi_log_record_processor.shutdown() def test_force_flush_processor_returns_false(self): multi_log_record_processor = ConcurrentMultiLogRecordProcessor() false_mock = Mock(spec=LogRecordProcessor) false_mock.force_flush = Mock(return_value=False) mocks = [Mock(spec=LogRecordProcessor) for _ in range(4)] mocks.insert(0, false_mock) for mock_processor in mocks: multi_log_record_processor.add_log_record_processor(mock_processor) ret_value = multi_log_record_processor.force_flush() self.assertFalse(ret_value) for mock in mocks: self.assertEqual(1, mock.force_flush.call_count) multi_log_record_processor.shutdown() def test_force_flush_processor_returns_none(self): multi_log_record_processor = ConcurrentMultiLogRecordProcessor() none_mock = Mock(spec=LogRecordProcessor) none_mock.force_flush = Mock(return_value=None) mocks = [Mock(spec=LogRecordProcessor) for _ in range(4)] mocks.insert(0, none_mock) for mock_processor in mocks: multi_log_record_processor.add_log_record_processor(mock_processor) ret_value = multi_log_record_processor.force_flush() self.assertTrue(ret_value) for mock in mocks: self.assertEqual(1, mock.force_flush.call_count) multi_log_record_processor.shutdown() python-opentelemetry-1.45.2/opentelemetry-sdk/tests/logs/test_sdk_metrics.py000066400000000000000000000042521527070070700275270ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from unittest import TestCase from unittest.mock import patch from opentelemetry.sdk._logs import LoggerProvider from opentelemetry.sdk.environment_variables import ( OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED, ) from opentelemetry.sdk.metrics import MeterProvider from opentelemetry.sdk.metrics.export import InMemoryMetricReader @patch.dict("os.environ", {OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED: "true"}) class TestLoggerProviderMetrics(TestCase): def setUp(self): self.metric_reader = InMemoryMetricReader() self.meter_provider = MeterProvider(metric_readers=[self.metric_reader]) def tearDown(self): self.meter_provider.shutdown() def assert_created_logs(self, metric_data, value, attrs): metrics = metric_data.resource_metrics[0].scope_metrics[0].metrics created_logs_metric = next((m for m in metrics if m.name == "otel.sdk.log.created"), None) self.assertIsNotNone(created_logs_metric) self.assertEqual(created_logs_metric.data.data_points[0].value, value) self.assertDictEqual(created_logs_metric.data.data_points[0].attributes, attrs) def test_create_logs(self): logger_provider = LoggerProvider(meter_provider=self.meter_provider) logger = logger_provider.get_logger("test") logger.emit(body="log1") metric_data = self.metric_reader.get_metrics_data() self.assert_created_logs( metric_data, 1, {}, ) logger.emit(body="log2") metric_data = self.metric_reader.get_metrics_data() self.assert_created_logs( metric_data, 2, {}, ) class TestLoggerProviderMetricsDisabled(TestCase): def test_disabled_by_default(self): metric_reader = InMemoryMetricReader() meter_provider = MeterProvider(metric_readers=[metric_reader]) logger_provider = LoggerProvider(meter_provider=meter_provider) logger = logger_provider.get_logger("test") logger.emit(body="log1") self.assertIsNone(metric_reader.get_metrics_data()) meter_provider.shutdown() python-opentelemetry-1.45.2/opentelemetry-sdk/tests/metrics/000077500000000000000000000000001527070070700243065ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-sdk/tests/metrics/exponential_histogram/000077500000000000000000000000001527070070700307115ustar00rootroot00000000000000test_exponent_mapping.py000066400000000000000000000341771527070070700356320ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-sdk/tests/metrics/exponential_histogram# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # pylint: disable=protected-access from math import inf, nextafter from sys import float_info from unittest.mock import patch from opentelemetry.sdk.metrics._internal.exponential_histogram.mapping.errors import ( MappingUnderflowError, ) from opentelemetry.sdk.metrics._internal.exponential_histogram.mapping.exponent_mapping import ( ExponentMapping, ) from opentelemetry.sdk.metrics._internal.exponential_histogram.mapping.ieee_754 import ( MAX_NORMAL_EXPONENT, MAX_NORMAL_VALUE, MIN_NORMAL_EXPONENT, MIN_NORMAL_VALUE, ) from opentelemetry.test import TestCase def right_boundary(scale: int, index: int) -> float: result = 2**index for _ in range(scale, 0): result = result * result return result class TestExponentMapping(TestCase): def test_singleton(self): self.assertIs(ExponentMapping(-3), ExponentMapping(-3)) self.assertIsNot(ExponentMapping(-3), ExponentMapping(-5)) @patch( "opentelemetry.sdk.metrics._internal.exponential_histogram.mapping.exponent_mapping.ExponentMapping._mappings", new={}, ) @patch("opentelemetry.sdk.metrics._internal.exponential_histogram.mapping.exponent_mapping.ExponentMapping._init") def test_init_called_once(self, mock_init): # pylint: disable=no-self-use ExponentMapping(-3) ExponentMapping(-3) mock_init.assert_called_once() def test_exponent_mapping_0(self): with self.assertNotRaises(Exception): ExponentMapping(0) def test_exponent_mapping_zero(self): exponent_mapping = ExponentMapping(0) # This is the equivalent to 1.1 in hexadecimal hex_1_1 = 1 + (1 / 16) # Testing with values near +inf self.assertEqual( exponent_mapping.map_to_index(MAX_NORMAL_VALUE), MAX_NORMAL_EXPONENT, ) self.assertEqual(exponent_mapping.map_to_index(MAX_NORMAL_VALUE), 1023) self.assertEqual(exponent_mapping.map_to_index(2**1023), 1022) self.assertEqual(exponent_mapping.map_to_index(2**1022), 1021) self.assertEqual(exponent_mapping.map_to_index(hex_1_1 * (2**1023)), 1023) self.assertEqual(exponent_mapping.map_to_index(hex_1_1 * (2**1022)), 1022) # Testing with values near 1 self.assertEqual(exponent_mapping.map_to_index(4), 1) self.assertEqual(exponent_mapping.map_to_index(3), 1) self.assertEqual(exponent_mapping.map_to_index(2), 0) self.assertEqual(exponent_mapping.map_to_index(1), -1) self.assertEqual(exponent_mapping.map_to_index(0.75), -1) self.assertEqual(exponent_mapping.map_to_index(0.51), -1) self.assertEqual(exponent_mapping.map_to_index(0.5), -2) self.assertEqual(exponent_mapping.map_to_index(0.26), -2) self.assertEqual(exponent_mapping.map_to_index(0.25), -3) self.assertEqual(exponent_mapping.map_to_index(0.126), -3) self.assertEqual(exponent_mapping.map_to_index(0.125), -4) # Testing with values near 0 self.assertEqual(exponent_mapping.map_to_index(2**-1022), -1023) self.assertEqual(exponent_mapping.map_to_index(hex_1_1 * (2**-1022)), -1022) self.assertEqual(exponent_mapping.map_to_index(2**-1021), -1022) self.assertEqual(exponent_mapping.map_to_index(hex_1_1 * (2**-1021)), -1021) self.assertEqual(exponent_mapping.map_to_index(2**-1022), MIN_NORMAL_EXPONENT - 1) self.assertEqual(exponent_mapping.map_to_index(2**-1021), MIN_NORMAL_EXPONENT) # The smallest subnormal value is 2 ** -1074 = 5e-324. # This value is also the result of: # s = 1 # while s / 2: # s = s / 2 # s == 5e-324 self.assertEqual(exponent_mapping.map_to_index(2**-1074), MIN_NORMAL_EXPONENT - 1) def test_exponent_mapping_min_scale(self): exponent_mapping = ExponentMapping(ExponentMapping._min_scale) self.assertEqual(exponent_mapping.map_to_index(1.000001), 0) self.assertEqual(exponent_mapping.map_to_index(1), -1) self.assertEqual(exponent_mapping.map_to_index(float_info.max), 0) self.assertEqual(exponent_mapping.map_to_index(float_info.min), -1) def test_invalid_scale(self): with self.assertRaises(Exception): ExponentMapping(1) with self.assertRaises(Exception): ExponentMapping(ExponentMapping._min_scale - 1) def test_exponent_mapping_neg_one(self): exponent_mapping = ExponentMapping(-1) self.assertEqual(exponent_mapping.map_to_index(17), 2) self.assertEqual(exponent_mapping.map_to_index(16), 1) self.assertEqual(exponent_mapping.map_to_index(15), 1) self.assertEqual(exponent_mapping.map_to_index(9), 1) self.assertEqual(exponent_mapping.map_to_index(8), 1) self.assertEqual(exponent_mapping.map_to_index(5), 1) self.assertEqual(exponent_mapping.map_to_index(4), 0) self.assertEqual(exponent_mapping.map_to_index(3), 0) self.assertEqual(exponent_mapping.map_to_index(2), 0) self.assertEqual(exponent_mapping.map_to_index(1.5), 0) self.assertEqual(exponent_mapping.map_to_index(1), -1) self.assertEqual(exponent_mapping.map_to_index(0.75), -1) self.assertEqual(exponent_mapping.map_to_index(0.5), -1) self.assertEqual(exponent_mapping.map_to_index(0.25), -2) self.assertEqual(exponent_mapping.map_to_index(0.20), -2) self.assertEqual(exponent_mapping.map_to_index(0.13), -2) self.assertEqual(exponent_mapping.map_to_index(0.125), -2) self.assertEqual(exponent_mapping.map_to_index(0.10), -2) self.assertEqual(exponent_mapping.map_to_index(0.0625), -3) self.assertEqual(exponent_mapping.map_to_index(0.06), -3) def test_exponent_mapping_neg_four(self): # pylint: disable=too-many-statements exponent_mapping = ExponentMapping(-4) self.assertEqual(exponent_mapping.map_to_index(float(0x1)), -1) self.assertEqual(exponent_mapping.map_to_index(float(0x10)), 0) self.assertEqual(exponent_mapping.map_to_index(float(0x100)), 0) self.assertEqual(exponent_mapping.map_to_index(float(0x1000)), 0) self.assertEqual(exponent_mapping.map_to_index(float(0x10000)), 0) # base == 2 ** 16 self.assertEqual(exponent_mapping.map_to_index(float(0x100000)), 1) self.assertEqual(exponent_mapping.map_to_index(float(0x1000000)), 1) self.assertEqual(exponent_mapping.map_to_index(float(0x10000000)), 1) self.assertEqual(exponent_mapping.map_to_index(float(0x100000000)), 1) # base == 2 ** 32 self.assertEqual(exponent_mapping.map_to_index(float(0x1000000000)), 2) self.assertEqual(exponent_mapping.map_to_index(float(0x10000000000)), 2) self.assertEqual(exponent_mapping.map_to_index(float(0x100000000000)), 2) self.assertEqual(exponent_mapping.map_to_index(float(0x1000000000000)), 2) # base == 2 ** 48 self.assertEqual(exponent_mapping.map_to_index(float(0x10000000000000)), 3) self.assertEqual(exponent_mapping.map_to_index(float(0x100000000000000)), 3) self.assertEqual(exponent_mapping.map_to_index(float(0x1000000000000000)), 3) self.assertEqual(exponent_mapping.map_to_index(float(0x10000000000000000)), 3) # base == 2 ** 64 self.assertEqual(exponent_mapping.map_to_index(float(0x100000000000000000)), 4) self.assertEqual(exponent_mapping.map_to_index(float(0x1000000000000000000)), 4) self.assertEqual(exponent_mapping.map_to_index(float(0x10000000000000000000)), 4) self.assertEqual(exponent_mapping.map_to_index(float(0x100000000000000000000)), 4) # base == 2 ** 80 self.assertEqual(exponent_mapping.map_to_index(float(0x1000000000000000000000)), 5) self.assertEqual(exponent_mapping.map_to_index(1 / float(0x1)), -1) self.assertEqual(exponent_mapping.map_to_index(1 / float(0x10)), -1) self.assertEqual(exponent_mapping.map_to_index(1 / float(0x100)), -1) self.assertEqual(exponent_mapping.map_to_index(1 / float(0x1000)), -1) self.assertEqual(exponent_mapping.map_to_index(1 / float(0x10000)), -2) # base == 2 ** -16 self.assertEqual(exponent_mapping.map_to_index(1 / float(0x100000)), -2) self.assertEqual(exponent_mapping.map_to_index(1 / float(0x1000000)), -2) self.assertEqual(exponent_mapping.map_to_index(1 / float(0x10000000)), -2) self.assertEqual(exponent_mapping.map_to_index(1 / float(0x100000000)), -3) # base == 2 ** -32 self.assertEqual(exponent_mapping.map_to_index(1 / float(0x1000000000)), -3) self.assertEqual(exponent_mapping.map_to_index(1 / float(0x10000000000)), -3) self.assertEqual(exponent_mapping.map_to_index(1 / float(0x100000000000)), -3) self.assertEqual(exponent_mapping.map_to_index(1 / float(0x1000000000000)), -4) # base == 2 ** -32 self.assertEqual(exponent_mapping.map_to_index(1 / float(0x10000000000000)), -4) self.assertEqual(exponent_mapping.map_to_index(1 / float(0x100000000000000)), -4) self.assertEqual(exponent_mapping.map_to_index(1 / float(0x1000000000000000)), -4) self.assertEqual(exponent_mapping.map_to_index(1 / float(0x10000000000000000)), -5) # base == 2 ** -64 self.assertEqual(exponent_mapping.map_to_index(1 / float(0x100000000000000000)), -5) self.assertEqual(exponent_mapping.map_to_index(float_info.max), 63) self.assertEqual(exponent_mapping.map_to_index(2**1023), 63) self.assertEqual(exponent_mapping.map_to_index(2**1019), 63) self.assertEqual(exponent_mapping.map_to_index(2**1009), 63) self.assertEqual(exponent_mapping.map_to_index(2**1008), 62) self.assertEqual(exponent_mapping.map_to_index(2**1007), 62) self.assertEqual(exponent_mapping.map_to_index(2**1000), 62) self.assertEqual(exponent_mapping.map_to_index(2**993), 62) self.assertEqual(exponent_mapping.map_to_index(2**992), 61) self.assertEqual(exponent_mapping.map_to_index(2**991), 61) self.assertEqual(exponent_mapping.map_to_index(2**-1074), -64) self.assertEqual(exponent_mapping.map_to_index(2**-1073), -64) self.assertEqual(exponent_mapping.map_to_index(2**-1072), -64) self.assertEqual(exponent_mapping.map_to_index(2**-1057), -64) self.assertEqual(exponent_mapping.map_to_index(2**-1056), -64) self.assertEqual(exponent_mapping.map_to_index(2**-1041), -64) self.assertEqual(exponent_mapping.map_to_index(2**-1040), -64) self.assertEqual(exponent_mapping.map_to_index(2**-1025), -64) self.assertEqual(exponent_mapping.map_to_index(2**-1024), -64) self.assertEqual(exponent_mapping.map_to_index(2**-1023), -64) self.assertEqual(exponent_mapping.map_to_index(2**-1022), -64) self.assertEqual(exponent_mapping.map_to_index(2**-1009), -64) self.assertEqual(exponent_mapping.map_to_index(2**-1008), -64) self.assertEqual(exponent_mapping.map_to_index(2**-1007), -63) self.assertEqual(exponent_mapping.map_to_index(2**-993), -63) self.assertEqual(exponent_mapping.map_to_index(2**-992), -63) self.assertEqual(exponent_mapping.map_to_index(2**-991), -62) self.assertEqual(exponent_mapping.map_to_index(2**-977), -62) self.assertEqual(exponent_mapping.map_to_index(2**-976), -62) self.assertEqual(exponent_mapping.map_to_index(2**-975), -61) def test_exponent_index_max(self): for scale in range(ExponentMapping._min_scale, ExponentMapping._max_scale): exponent_mapping = ExponentMapping(scale) index = exponent_mapping.map_to_index(MAX_NORMAL_VALUE) max_index = ((MAX_NORMAL_EXPONENT + 1) >> -scale) - 1 self.assertEqual(index, max_index) boundary = exponent_mapping.get_lower_boundary(index) self.assertEqual(boundary, right_boundary(scale, max_index)) with self.assertRaises(Exception): exponent_mapping.get_lower_boundary(index + 1) def test_exponent_index_min(self): for scale in range(ExponentMapping._min_scale, ExponentMapping._max_scale + 1): exponent_mapping = ExponentMapping(scale) min_index = exponent_mapping.map_to_index(MIN_NORMAL_VALUE) boundary = exponent_mapping.get_lower_boundary(min_index) correct_min_index = MIN_NORMAL_EXPONENT >> -scale if MIN_NORMAL_EXPONENT % (1 << -scale) == 0: correct_min_index -= 1 # We do not check for correct_min_index to be greater than the # smallest integer because the smallest integer in Python is -inf. self.assertEqual(correct_min_index, min_index) correct_boundary = right_boundary(scale, correct_min_index) self.assertEqual(correct_boundary, boundary) self.assertGreater(right_boundary(scale, correct_min_index + 1), boundary) self.assertEqual( correct_min_index, exponent_mapping.map_to_index(MIN_NORMAL_VALUE / 2), ) self.assertEqual( correct_min_index, exponent_mapping.map_to_index(MIN_NORMAL_VALUE / 3), ) self.assertEqual( correct_min_index, exponent_mapping.map_to_index(MIN_NORMAL_VALUE / 100), ) self.assertEqual(correct_min_index, exponent_mapping.map_to_index(2**-1050)) self.assertEqual(correct_min_index, exponent_mapping.map_to_index(2**-1073)) self.assertEqual( correct_min_index, exponent_mapping.map_to_index(1.1 * (2**-1073)), ) self.assertEqual(correct_min_index, exponent_mapping.map_to_index(2**-1074)) with self.assertRaises(MappingUnderflowError): exponent_mapping.get_lower_boundary(min_index - 1) self.assertEqual( exponent_mapping.map_to_index( nextafter( # pylint: disable=possibly-used-before-assignment MIN_NORMAL_VALUE, inf ) ), MIN_NORMAL_EXPONENT >> -scale, ) test_exponential_bucket_histogram_aggregation.py000066400000000000000000001225651527070070700425650ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-sdk/tests/metrics/exponential_histogram# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # pylint: disable=protected-access,too-many-lines,invalid-name # pylint: disable=consider-using-enumerate,no-self-use,too-many-public-methods from inspect import currentframe from itertools import permutations from logging import WARNING from math import inf, ldexp from random import Random, randrange from sys import float_info, maxsize from time import time_ns from types import MethodType from unittest.mock import Mock, patch from opentelemetry.context import Context from opentelemetry.sdk.metrics._internal.aggregation import ( AggregationTemporality, _ExponentialBucketHistogramAggregation, ) from opentelemetry.sdk.metrics._internal.exponential_histogram.buckets import ( Buckets, ) from opentelemetry.sdk.metrics._internal.exponential_histogram.mapping.exponent_mapping import ( ExponentMapping, ) from opentelemetry.sdk.metrics._internal.exponential_histogram.mapping.ieee_754 import ( MAX_NORMAL_EXPONENT, MIN_NORMAL_EXPONENT, ) from opentelemetry.sdk.metrics._internal.exponential_histogram.mapping.logarithm_mapping import ( LogarithmMapping, ) from opentelemetry.sdk.metrics._internal.measurement import Measurement from opentelemetry.sdk.metrics._internal.point import ( ExponentialHistogramDataPoint, ) from opentelemetry.sdk.metrics._internal.view import _default_reservoir_factory from opentelemetry.sdk.metrics.view import ( ExponentialBucketHistogramAggregation, ) from opentelemetry.test import TestCase def get_counts(buckets: Buckets) -> int: counts = [] for index in range(len(buckets)): counts.append(buckets[index]) return counts def center_val(mapping: ExponentMapping, index: int) -> float: return (mapping.get_lower_boundary(index) + mapping.get_lower_boundary(index + 1)) / 2 def swap( first: _ExponentialBucketHistogramAggregation, second: _ExponentialBucketHistogramAggregation, ): for attribute in [ "_value_positive", "_value_negative", "_sum", "_count", "_zero_count", "_min", "_max", "_mapping", ]: temp = getattr(first, attribute) setattr(first, attribute, getattr(second, attribute)) setattr(second, attribute, temp) class TestExponentialBucketHistogramAggregation(TestCase): @patch("opentelemetry.sdk.metrics._internal.aggregation.LogarithmMapping") def test_create_aggregation(self, mock_logarithm_mapping): exponential_bucket_histogram_aggregation = (ExponentialBucketHistogramAggregation())._create_aggregation( Mock(), Mock(), Mock(), Mock() ) self.assertEqual(exponential_bucket_histogram_aggregation._max_scale, 20) mock_logarithm_mapping.assert_called_with(20) exponential_bucket_histogram_aggregation = ( ExponentialBucketHistogramAggregation(max_scale=10) )._create_aggregation(Mock(), Mock(), Mock(), Mock()) self.assertEqual(exponential_bucket_histogram_aggregation._max_scale, 10) mock_logarithm_mapping.assert_called_with(10) with self.assertLogs(level=WARNING): exponential_bucket_histogram_aggregation = ( ExponentialBucketHistogramAggregation(max_scale=100) )._create_aggregation(Mock(), Mock(), Mock(), Mock()) self.assertEqual(exponential_bucket_histogram_aggregation._max_scale, 100) mock_logarithm_mapping.assert_called_with(100) def test_create_aggregation_record_min_max(self): for record_min_max, expected in [ (None, True), (True, True), (False, False), ]: with self.subTest(record_min_max=record_min_max): kwargs = {} if record_min_max is None else {"record_min_max": record_min_max} exponential_bucket_histogram_aggregation = ( ExponentialBucketHistogramAggregation(**kwargs) )._create_aggregation(Mock(), Mock(), Mock(), Mock()) self.assertEqual( exponential_bucket_histogram_aggregation._record_min_max, expected, ) def test_min_max(self): """ `record_min_max` indicates the aggregator to record the minimum and maximum value in the population """ now = time_ns() for record_min_max, expected_min, expected_max in [ (True, 1, 9999), (False, inf, -inf), ]: with self.subTest(record_min_max=record_min_max): ctx = Context() exponential_histogram_aggregation = _ExponentialBucketHistogramAggregation( Mock(), _default_reservoir_factory(_ExponentialBucketHistogramAggregation), AggregationTemporality.CUMULATIVE, 0, record_min_max=record_min_max, ) for value in [2, 4, 1, 9999]: exponential_histogram_aggregation.aggregate(Measurement(value, now, Mock(), ctx)) self.assertEqual(exponential_histogram_aggregation._min, expected_min) self.assertEqual(exponential_histogram_aggregation._max, expected_max) def assertInEpsilon(self, first, second, epsilon): self.assertLessEqual(first, (second * (1 + epsilon))) self.assertGreaterEqual(first, (second * (1 - epsilon))) def require_equal(self, a, b): if a._sum == 0 or b._sum == 0: self.assertAlmostEqual(a._sum, b._sum, 1e-6) else: self.assertInEpsilon(a._sum, b._sum, 1e-6) self.assertEqual(a._count, b._count) self.assertEqual(a._zero_count, b._zero_count) self.assertEqual(a._mapping.scale, b._mapping.scale) self.assertEqual(len(a._value_positive), len(b._value_positive)) self.assertEqual(len(a._value_negative), len(b._value_negative)) for index in range(len(a._value_positive)): self.assertEqual(a._value_positive[index], b._value_positive[index]) for index in range(len(a._value_negative)): self.assertEqual(a._value_negative[index], b._value_negative[index]) def test_alternating_growth_0(self): """ Tests insertion of [2, 4, 1]. The index of 2 (i.e., 0) becomes `indexBase`, the 4 goes to its right and the 1 goes in the last position of the backing array. With 3 binary orders of magnitude and MaxSize=4, this must finish with scale=0; with minimum value 1 this must finish with offset=-1 (all scales). """ # The corresponding Go test is TestAlternatingGrowth1 where: # agg := NewFloat64(NewConfig(WithMaxSize(4))) # agg is an instance of github.com/lightstep/otel-launcher-go/lightstep/sdk/metric/aggregator/histogram/structure.Histogram[float64] exponential_histogram_aggregation = _ExponentialBucketHistogramAggregation( Mock(), _default_reservoir_factory(_ExponentialBucketHistogramAggregation), AggregationTemporality.DELTA, Mock(), max_size=4, ) now = time_ns() ctx = Context() exponential_histogram_aggregation.aggregate(Measurement(2, now, Mock(), ctx)) exponential_histogram_aggregation.aggregate(Measurement(4, now, Mock(), ctx)) exponential_histogram_aggregation.aggregate(Measurement(1, now, Mock(), ctx)) self.assertEqual(exponential_histogram_aggregation._value_positive.offset, -1) self.assertEqual(exponential_histogram_aggregation._mapping.scale, 0) self.assertEqual( get_counts(exponential_histogram_aggregation._value_positive), [1, 1, 1], ) def test_alternating_growth_1(self): """ Tests insertion of [2, 2, 4, 1, 8, 0.5]. The test proceeds as¶ above but then downscales once further to scale=-1, thus index -1¶ holds range [0.25, 1.0), index 0 holds range [1.0, 4), index 1¶ holds range [4, 16).¶ """ exponential_histogram_aggregation = _ExponentialBucketHistogramAggregation( Mock(), _default_reservoir_factory(_ExponentialBucketHistogramAggregation), AggregationTemporality.DELTA, Mock(), max_size=4, ) now = time_ns() ctx = Context() exponential_histogram_aggregation.aggregate(Measurement(2, now, Mock(), ctx)) exponential_histogram_aggregation.aggregate(Measurement(2, now, Mock(), ctx)) exponential_histogram_aggregation.aggregate(Measurement(2, now, Mock(), ctx)) exponential_histogram_aggregation.aggregate(Measurement(1, now, Mock(), ctx)) exponential_histogram_aggregation.aggregate(Measurement(8, now, Mock(), ctx)) exponential_histogram_aggregation.aggregate(Measurement(0.5, now, Mock(), ctx)) self.assertEqual(exponential_histogram_aggregation._value_positive.offset, -1) self.assertEqual(exponential_histogram_aggregation._mapping.scale, -1) self.assertEqual( get_counts(exponential_histogram_aggregation._value_positive), [2, 3, 1], ) def test_permutations(self): """ Tests that every permutation of certain sequences with maxSize=2 results in the same scale=-1 histogram. """ now = time_ns() ctx = Context() for test_values, expected in [ [ [0.5, 1.0, 2.0], { "scale": -1, "offset": -1, "len": 2, "at_0": 2, "at_1": 1, }, ], [ [1.0, 2.0, 4.0], { "scale": -1, "offset": -1, "len": 2, "at_0": 1, "at_1": 2, }, ], [ [0.25, 0.5, 1], { "scale": -1, "offset": -2, "len": 2, "at_0": 1, "at_1": 2, }, ], ]: for permutation in permutations(test_values): exponential_histogram_aggregation = _ExponentialBucketHistogramAggregation( Mock(), _default_reservoir_factory(_ExponentialBucketHistogramAggregation), AggregationTemporality.DELTA, Mock(), max_size=2, ) for value in permutation: exponential_histogram_aggregation.aggregate(Measurement(value, now, Mock(), ctx)) self.assertEqual( exponential_histogram_aggregation._mapping.scale, expected["scale"], ) self.assertEqual( exponential_histogram_aggregation._value_positive.offset, expected["offset"], ) self.assertEqual( len(exponential_histogram_aggregation._value_positive), expected["len"], ) self.assertEqual( exponential_histogram_aggregation._value_positive[0], expected["at_0"], ) self.assertEqual( exponential_histogram_aggregation._value_positive[1], expected["at_1"], ) def test_ascending_sequence(self): for max_size in [3, 4, 6, 9]: for offset in range(-5, 6): for init_scale in [0, 4]: self.ascending_sequence_test(max_size, offset, init_scale) # pylint: disable=too-many-locals def ascending_sequence_test(self, max_size: int, offset: int, init_scale: int): now = time_ns() ctx = Context() for step in range(max_size, max_size * 4): exponential_histogram_aggregation = _ExponentialBucketHistogramAggregation( Mock(), _default_reservoir_factory(_ExponentialBucketHistogramAggregation), AggregationTemporality.DELTA, Mock(), max_size=max_size, ) if init_scale <= 0: mapping = ExponentMapping(init_scale) else: mapping = LogarithmMapping(init_scale) min_val = center_val(mapping, offset) max_val = center_val(mapping, offset + step) sum_ = 0.0 for index in range(max_size): value = center_val(mapping, offset + index) exponential_histogram_aggregation.aggregate(Measurement(value, now, Mock(), ctx)) sum_ += value self.assertEqual(init_scale, exponential_histogram_aggregation._mapping._scale) self.assertEqual( offset, exponential_histogram_aggregation._value_positive.offset, ) exponential_histogram_aggregation.aggregate(Measurement(max_val, now, Mock(), ctx)) sum_ += max_val self.assertNotEqual(0, exponential_histogram_aggregation._value_positive[0]) # The maximum-index filled bucket is at or # above the mid-point, (otherwise we # downscaled too much). max_fill = 0 total_count = 0 for index in range(len(exponential_histogram_aggregation._value_positive)): total_count += exponential_histogram_aggregation._value_positive[index] if exponential_histogram_aggregation._value_positive[index] != 0: max_fill = index # FIXME the corresponding Go code is # require.GreaterOrEqual(t, maxFill, uint32(maxSize)/2), make sure # this is actually equivalent. self.assertGreaterEqual(max_fill, int(max_size / 2)) self.assertGreaterEqual(max_size + 1, total_count) self.assertGreaterEqual(max_size + 1, exponential_histogram_aggregation._count) self.assertGreaterEqual(sum_, exponential_histogram_aggregation._sum) if init_scale <= 0: mapping = ExponentMapping(exponential_histogram_aggregation._mapping.scale) else: mapping = LogarithmMapping(exponential_histogram_aggregation._mapping.scale) index = mapping.map_to_index(min_val) self.assertEqual(index, exponential_histogram_aggregation._value_positive.offset) index = mapping.map_to_index(max_val) self.assertEqual( index, exponential_histogram_aggregation._value_positive.offset + len(exponential_histogram_aggregation._value_positive) - 1, ) def test_reset(self): now = time_ns() ctx = Context() for increment in [0x1, 0x100, 0x10000, 0x100000000, 0x200000000]: def mock_increment(self, bucket_index: int) -> None: """ Increments a bucket """ # pylint: disable=cell-var-from-loop self._counts[bucket_index] += increment exponential_histogram_aggregation = _ExponentialBucketHistogramAggregation( Mock(), _default_reservoir_factory(_ExponentialBucketHistogramAggregation), AggregationTemporality.DELTA, Mock(), max_size=256, ) self.assertEqual( exponential_histogram_aggregation._count, exponential_histogram_aggregation._zero_count, ) self.assertEqual(0, exponential_histogram_aggregation._sum) expect = 0 exponential_histogram_aggregation._value_positive = Buckets() for value in range(2, 257): expect += value * increment with patch.object( exponential_histogram_aggregation._value_positive, "increment_bucket", MethodType( mock_increment, exponential_histogram_aggregation._value_positive, ), ): exponential_histogram_aggregation.aggregate(Measurement(value, now, Mock(), ctx)) exponential_histogram_aggregation._count *= increment exponential_histogram_aggregation._sum *= increment self.assertEqual(expect, exponential_histogram_aggregation._sum) self.assertEqual(255 * increment, exponential_histogram_aggregation._count) # See test_integer_aggregation about why scale is 5, len is # 256 - (1 << scale)- 1 and offset is (1 << scale) - 1. scale = exponential_histogram_aggregation._mapping.scale self.assertEqual(5, scale) self.assertEqual( 256 - ((1 << scale) - 1), len(exponential_histogram_aggregation._value_positive), ) self.assertEqual( (1 << scale) - 1, exponential_histogram_aggregation._value_positive.offset, ) for index in range(256): self.assertLessEqual( exponential_histogram_aggregation._value_positive[index], 6 * increment, ) def test_move_into(self): now = time_ns() ctx = Context() exponential_histogram_aggregation_0 = _ExponentialBucketHistogramAggregation( Mock(), _default_reservoir_factory(_ExponentialBucketHistogramAggregation), AggregationTemporality.DELTA, Mock(), max_size=256, ) exponential_histogram_aggregation_1 = _ExponentialBucketHistogramAggregation( Mock(), _default_reservoir_factory(_ExponentialBucketHistogramAggregation), AggregationTemporality.DELTA, Mock(), max_size=256, ) expect = 0 for index in range(2, 257): expect += index exponential_histogram_aggregation_0.aggregate(Measurement(index, now, Mock(), ctx)) exponential_histogram_aggregation_0.aggregate(Measurement(0, now, Mock(), ctx)) swap( exponential_histogram_aggregation_0, exponential_histogram_aggregation_1, ) self.assertEqual(0, exponential_histogram_aggregation_0._sum) self.assertEqual(0, exponential_histogram_aggregation_0._count) self.assertEqual(0, exponential_histogram_aggregation_0._zero_count) self.assertEqual(expect, exponential_histogram_aggregation_1._sum) self.assertEqual(255 * 2, exponential_histogram_aggregation_1._count) self.assertEqual(255, exponential_histogram_aggregation_1._zero_count) scale = exponential_histogram_aggregation_1._mapping.scale self.assertEqual(5, scale) self.assertEqual( 256 - ((1 << scale) - 1), len(exponential_histogram_aggregation_1._value_positive), ) self.assertEqual( (1 << scale) - 1, exponential_histogram_aggregation_1._value_positive.offset, ) for index in range(256): self.assertLessEqual(exponential_histogram_aggregation_1._value_positive[index], 6) def test_very_large_numbers(self): now = time_ns() ctx = Context() exponential_histogram_aggregation = _ExponentialBucketHistogramAggregation( Mock(), _default_reservoir_factory(_ExponentialBucketHistogramAggregation), AggregationTemporality.DELTA, Mock(), max_size=2, ) def expect_balanced(count: int): self.assertEqual(2, len(exponential_histogram_aggregation._value_positive)) self.assertEqual(-1, exponential_histogram_aggregation._value_positive.offset) self.assertEqual(count, exponential_histogram_aggregation._value_positive[0]) self.assertEqual(count, exponential_histogram_aggregation._value_positive[1]) exponential_histogram_aggregation.aggregate(Measurement(2**-100, now, Mock(), ctx)) exponential_histogram_aggregation.aggregate(Measurement(2**100, now, Mock(), ctx)) self.assertLessEqual(2**100, (exponential_histogram_aggregation._sum * (1 + 1e-5))) self.assertGreaterEqual(2**100, (exponential_histogram_aggregation._sum * (1 - 1e-5))) self.assertEqual(2, exponential_histogram_aggregation._count) self.assertEqual(-7, exponential_histogram_aggregation._mapping.scale) expect_balanced(1) exponential_histogram_aggregation.aggregate(Measurement(2**-127, now, Mock(), ctx)) exponential_histogram_aggregation.aggregate(Measurement(2**128, now, Mock(), ctx)) self.assertLessEqual(2**128, (exponential_histogram_aggregation._sum * (1 + 1e-5))) self.assertGreaterEqual(2**128, (exponential_histogram_aggregation._sum * (1 - 1e-5))) self.assertEqual(4, exponential_histogram_aggregation._count) self.assertEqual(-7, exponential_histogram_aggregation._mapping.scale) expect_balanced(2) exponential_histogram_aggregation.aggregate(Measurement(2**-129, now, Mock(), ctx)) exponential_histogram_aggregation.aggregate(Measurement(2**255, now, Mock(), ctx)) self.assertLessEqual(2**255, (exponential_histogram_aggregation._sum * (1 + 1e-5))) self.assertGreaterEqual(2**255, (exponential_histogram_aggregation._sum * (1 - 1e-5))) self.assertEqual(6, exponential_histogram_aggregation._count) self.assertEqual(-8, exponential_histogram_aggregation._mapping.scale) expect_balanced(3) def test_full_range(self): now = time_ns() ctx = Context() exponential_histogram_aggregation = _ExponentialBucketHistogramAggregation( Mock(), _default_reservoir_factory(_ExponentialBucketHistogramAggregation), AggregationTemporality.DELTA, Mock(), max_size=2, ) exponential_histogram_aggregation.aggregate(Measurement(float_info.max, now, Mock(), ctx)) exponential_histogram_aggregation.aggregate(Measurement(1, now, Mock(), ctx)) exponential_histogram_aggregation.aggregate(Measurement(2**-1074, now, Mock(), ctx)) self.assertEqual(float_info.max, exponential_histogram_aggregation._sum) self.assertEqual(3, exponential_histogram_aggregation._count) self.assertEqual( ExponentMapping._min_scale, exponential_histogram_aggregation._mapping.scale, ) self.assertEqual( _ExponentialBucketHistogramAggregation._min_max_size, len(exponential_histogram_aggregation._value_positive), ) self.assertEqual(-1, exponential_histogram_aggregation._value_positive.offset) self.assertLessEqual(exponential_histogram_aggregation._value_positive[0], 2) self.assertLessEqual(exponential_histogram_aggregation._value_positive[1], 1) def test_aggregator_min_max(self): now = time_ns() ctx = Context() exponential_histogram_aggregation = _ExponentialBucketHistogramAggregation( Mock(), _default_reservoir_factory(_ExponentialBucketHistogramAggregation), AggregationTemporality.DELTA, Mock(), ) for value in [1, 3, 5, 7, 9]: exponential_histogram_aggregation.aggregate(Measurement(value, now, Mock(), ctx)) self.assertEqual(1, exponential_histogram_aggregation._min) self.assertEqual(9, exponential_histogram_aggregation._max) exponential_histogram_aggregation = _ExponentialBucketHistogramAggregation( Mock(), _default_reservoir_factory(_ExponentialBucketHistogramAggregation), AggregationTemporality.DELTA, Mock(), ) for value in [-1, -3, -5, -7, -9]: exponential_histogram_aggregation.aggregate(Measurement(value, now, Mock(), ctx)) self.assertEqual(-9, exponential_histogram_aggregation._min) self.assertEqual(-1, exponential_histogram_aggregation._max) def test_aggregator_copy_swap(self): now = time_ns() ctx = Context() exponential_histogram_aggregation_0 = _ExponentialBucketHistogramAggregation( Mock(), _default_reservoir_factory(_ExponentialBucketHistogramAggregation), AggregationTemporality.DELTA, Mock(), ) for value in [1, 3, 5, 7, 9, -1, -3, -5]: exponential_histogram_aggregation_0.aggregate(Measurement(value, now, Mock(), ctx)) exponential_histogram_aggregation_1 = _ExponentialBucketHistogramAggregation( Mock(), _default_reservoir_factory(_ExponentialBucketHistogramAggregation), AggregationTemporality.DELTA, Mock(), ) for value in [5, 4, 3, 2]: exponential_histogram_aggregation_1.aggregate(Measurement(value, now, Mock(), ctx)) exponential_histogram_aggregation_2 = _ExponentialBucketHistogramAggregation( Mock(), _default_reservoir_factory(_ExponentialBucketHistogramAggregation), AggregationTemporality.DELTA, Mock(), ) swap( exponential_histogram_aggregation_0, exponential_histogram_aggregation_1, ) # pylint: disable=unnecessary-dunder-call exponential_histogram_aggregation_2._value_positive.__init__() exponential_histogram_aggregation_2._value_negative.__init__() exponential_histogram_aggregation_2._sum = 0 exponential_histogram_aggregation_2._count = 0 exponential_histogram_aggregation_2._zero_count = 0 exponential_histogram_aggregation_2._min = 0 exponential_histogram_aggregation_2._max = 0 exponential_histogram_aggregation_2._mapping = LogarithmMapping(LogarithmMapping._max_scale) for attribute in [ "_value_positive", "_value_negative", "_sum", "_count", "_zero_count", "_min", "_max", "_mapping", ]: setattr( exponential_histogram_aggregation_2, attribute, getattr(exponential_histogram_aggregation_1, attribute), ) self.require_equal( exponential_histogram_aggregation_1, exponential_histogram_aggregation_2, ) def test_zero_count_by_increment(self): now = time_ns() ctx = Context() exponential_histogram_aggregation_0 = _ExponentialBucketHistogramAggregation( Mock(), _default_reservoir_factory(_ExponentialBucketHistogramAggregation), AggregationTemporality.DELTA, Mock(), ) increment = 10 for _ in range(increment): exponential_histogram_aggregation_0.aggregate(Measurement(0, now, Mock(), ctx)) exponential_histogram_aggregation_1 = _ExponentialBucketHistogramAggregation( Mock(), _default_reservoir_factory(_ExponentialBucketHistogramAggregation), AggregationTemporality.DELTA, Mock(), ) def mock_increment(self, bucket_index: int) -> None: """ Increments a bucket """ self._counts[bucket_index] += increment exponential_histogram_aggregation_1._value_positive = Buckets() with patch.object( exponential_histogram_aggregation_1._value_positive, "increment_bucket", MethodType( mock_increment, exponential_histogram_aggregation_1._value_positive, ), ): exponential_histogram_aggregation_1.aggregate(Measurement(0, now, Mock(), ctx)) exponential_histogram_aggregation_1._count *= increment exponential_histogram_aggregation_1._zero_count *= increment self.require_equal( exponential_histogram_aggregation_0, exponential_histogram_aggregation_1, ) def test_one_count_by_increment(self): now = time_ns() ctx = Context() exponential_histogram_aggregation_0 = _ExponentialBucketHistogramAggregation( Mock(), _default_reservoir_factory(_ExponentialBucketHistogramAggregation), AggregationTemporality.DELTA, Mock(), ) increment = 10 for _ in range(increment): exponential_histogram_aggregation_0.aggregate(Measurement(1, now, Mock(), ctx)) exponential_histogram_aggregation_1 = _ExponentialBucketHistogramAggregation( Mock(), _default_reservoir_factory(_ExponentialBucketHistogramAggregation), AggregationTemporality.DELTA, Mock(), ) def mock_increment(self, bucket_index: int) -> None: """ Increments a bucket """ self._counts[bucket_index] += increment exponential_histogram_aggregation_1._value_positive = Buckets() with patch.object( exponential_histogram_aggregation_1._value_positive, "increment_bucket", MethodType( mock_increment, exponential_histogram_aggregation_1._value_positive, ), ): exponential_histogram_aggregation_1.aggregate(Measurement(1, now, Mock(), ctx)) exponential_histogram_aggregation_1._count *= increment exponential_histogram_aggregation_1._sum *= increment self.require_equal( exponential_histogram_aggregation_0, exponential_histogram_aggregation_1, ) def test_boundary_statistics(self): total = MAX_NORMAL_EXPONENT - MIN_NORMAL_EXPONENT + 1 for scale in range(LogarithmMapping._min_scale, LogarithmMapping._max_scale + 1): above = 0 below = 0 if scale <= 0: mapping = ExponentMapping(scale) else: mapping = LogarithmMapping(scale) for exp in range(MIN_NORMAL_EXPONENT, MAX_NORMAL_EXPONENT + 1): value = ldexp(1, exp) index = mapping.map_to_index(value) with self.assertNotRaises(Exception): boundary = mapping.get_lower_boundary(index + 1) if boundary < value: above += 1 elif boundary > value: below += 1 self.assertInEpsilon(0.5, above / total, 0.05) self.assertInEpsilon(0.5, below / total, 0.06) def test_min_max_size(self): """ Tests that the minimum max_size is the right value. """ exponential_histogram_aggregation = _ExponentialBucketHistogramAggregation( Mock(), _default_reservoir_factory(_ExponentialBucketHistogramAggregation), AggregationTemporality.DELTA, Mock(), max_size=_ExponentialBucketHistogramAggregation._min_max_size, ) # The minimum and maximum normal floating point values are used here to # make sure the mapping can contain the full range of values. exponential_histogram_aggregation.aggregate(Mock(value=float_info.min)) exponential_histogram_aggregation.aggregate(Mock(value=float_info.max)) # This means the smallest max_scale is enough for the full range of the # normal floating point values. self.assertEqual( len(exponential_histogram_aggregation._value_positive._counts), exponential_histogram_aggregation._min_max_size, ) def test_aggregate_collect(self): """ Tests a repeated cycle of aggregation and collection. """ now = time_ns() ctx = Context() exponential_histogram_aggregation = _ExponentialBucketHistogramAggregation( Mock(), _default_reservoir_factory(_ExponentialBucketHistogramAggregation), AggregationTemporality.DELTA, Mock(), ) exponential_histogram_aggregation.aggregate(Measurement(2, now, Mock(), ctx)) exponential_histogram_aggregation.collect(AggregationTemporality.CUMULATIVE, 0) exponential_histogram_aggregation.aggregate(Measurement(2, now, Mock(), ctx)) exponential_histogram_aggregation.collect(AggregationTemporality.CUMULATIVE, 0) exponential_histogram_aggregation.aggregate(Measurement(2, now, Mock(), ctx)) exponential_histogram_aggregation.collect(AggregationTemporality.CUMULATIVE, 0) def test_collect_results_cumulative(self) -> None: now = time_ns() ctx = Context() exponential_histogram_aggregation = _ExponentialBucketHistogramAggregation( Mock(), _default_reservoir_factory(_ExponentialBucketHistogramAggregation), AggregationTemporality.DELTA, Mock(), ) self.maxDiff = None self.assertEqual(exponential_histogram_aggregation._mapping._scale, 20) exponential_histogram_aggregation.aggregate(Measurement(2, now, Mock(), ctx)) self.assertEqual(exponential_histogram_aggregation._mapping._scale, 20) exponential_histogram_aggregation.aggregate(Measurement(4, now, Mock(), ctx)) self.assertEqual(exponential_histogram_aggregation._mapping._scale, 7) exponential_histogram_aggregation.aggregate(Measurement(1, now, Mock(), ctx)) self.assertEqual(exponential_histogram_aggregation._mapping._scale, 6) collection_0 = exponential_histogram_aggregation.collect(AggregationTemporality.CUMULATIVE, Mock()) self.assertEqual(len(collection_0.positive.bucket_counts), 160) self.assertEqual(collection_0.count, 3) self.assertEqual(collection_0.sum, 7) self.assertEqual(collection_0.scale, 6) self.assertEqual(collection_0.zero_count, 0) self.assertEqual( collection_0.positive.bucket_counts, [1, *[0] * 63, 1, *[0] * 63, 1, *[0] * 31], ) self.assertEqual(collection_0.flags, 0) self.assertEqual(collection_0.min, 1) self.assertEqual(collection_0.max, 4) exponential_histogram_aggregation.aggregate(Measurement(1, now, Mock(), ctx)) exponential_histogram_aggregation.aggregate(Measurement(8, now, Mock(), ctx)) exponential_histogram_aggregation.aggregate(Measurement(0.5, now, Mock(), ctx)) exponential_histogram_aggregation.aggregate(Measurement(0.1, now, Mock(), ctx)) exponential_histogram_aggregation.aggregate(Measurement(0.045, now, Mock(), ctx)) collection_1 = exponential_histogram_aggregation.collect(AggregationTemporality.CUMULATIVE, Mock()) previous_count = collection_1.positive.bucket_counts[0] count_counts = [[previous_count, 0]] for count in collection_1.positive.bucket_counts: if count == previous_count: count_counts[-1][1] += 1 else: previous_count = count count_counts.append([previous_count, 1]) self.assertEqual(collection_1.count, 8) self.assertEqual(collection_1.sum, 16.645) self.assertEqual(collection_1.scale, 4) self.assertEqual(collection_1.zero_count, 0) self.assertEqual( collection_1.positive.bucket_counts, [ 1, *[0] * 17, 1, *[0] * 36, 1, *[0] * 15, 2, *[0] * 15, 1, *[0] * 15, 1, *[0] * 15, 1, *[0] * 40, ], ) self.assertEqual(collection_1.flags, 0) self.assertEqual(collection_1.min, 0.045) self.assertEqual(collection_1.max, 8) def test_cumulative_aggregation_with_random_data(self) -> None: now = time_ns() ctx = Context() histogram = _ExponentialBucketHistogramAggregation( Mock(), _default_reservoir_factory(_ExponentialBucketHistogramAggregation), AggregationTemporality.DELTA, Mock(), ) def collect_and_validate(values, histogram) -> None: result: ExponentialHistogramDataPoint = histogram.collect(AggregationTemporality.CUMULATIVE, 0) buckets = result.positive.bucket_counts scale = result.scale index_start = result.positive.offset for i in range(len(buckets)): index = index_start + i count = buckets[i] lower_bound = 2 ** (index / (2**scale)) upper_bound = 2 ** ((index + 1) / (2**scale)) matches = 0 for value in values: # pylint: disable=chained-comparison if value > lower_bound and value <= upper_bound: matches += 1 assert matches == count, ( f"index: {index}, count: {count}, scale: {scale}, lower_bound: {lower_bound}, upper_bound: {upper_bound}, matches: {matches}" ) assert sum(buckets) + result.zero_count == len(values) assert result.sum == sum(values) assert result.count == len(values) assert result.min == min(values) assert result.max == max(values) assert result.zero_count == len([v for v in values if v == 0]) assert scale >= 3 seed = randrange(maxsize) # This test case is executed with random values every time. In order to # run this test case with the same values used in a previous execution, # check the value printed by that previous execution of this test case # and use the same value for the seed variable in the line below. # seed = 3373389994391084876 random_generator = Random(seed) print(f"seed for {currentframe().f_code.co_name} is {seed}") values = [] for i in range(2000): # avoid both values being 0 value = random_generator.randint(0 if i else 1, 1000) values.append(value) histogram.aggregate(Measurement(value, now, Mock(), ctx)) if i % 20 == 0: collect_and_validate(values, histogram) collect_and_validate(values, histogram) def test_merge_collect_cumulative(self): now = time_ns() ctx = Context() exponential_histogram_aggregation = _ExponentialBucketHistogramAggregation( Mock(), _default_reservoir_factory(_ExponentialBucketHistogramAggregation), AggregationTemporality.DELTA, Mock(), max_size=4, ) for value in [2, 4, 8, 16]: exponential_histogram_aggregation.aggregate(Measurement(value, now, Mock(), ctx)) self.assertEqual(exponential_histogram_aggregation._mapping.scale, 0) self.assertEqual(exponential_histogram_aggregation._value_positive.offset, 0) self.assertEqual( exponential_histogram_aggregation._value_positive.counts, [1, 1, 1, 1], ) result_0 = exponential_histogram_aggregation.collect( AggregationTemporality.CUMULATIVE, 0, ) self.assertEqual(result_0.scale, 0) for value in [1, 2, 4, 8]: exponential_histogram_aggregation.aggregate(Measurement(1 / value, now, Mock(), ctx)) self.assertEqual(exponential_histogram_aggregation._mapping.scale, 0) self.assertEqual(exponential_histogram_aggregation._value_positive.offset, -4) self.assertEqual( exponential_histogram_aggregation._value_positive.counts, [1, 1, 1, 1], ) result_1 = exponential_histogram_aggregation.collect( AggregationTemporality.CUMULATIVE, 0, ) self.assertEqual(result_1.scale, -1) def test_merge_collect_delta(self): now = time_ns() ctx = Context() exponential_histogram_aggregation = _ExponentialBucketHistogramAggregation( Mock(), _default_reservoir_factory(_ExponentialBucketHistogramAggregation), AggregationTemporality.DELTA, Mock(), max_size=4, ) for value in [2, 4, 8, 16]: exponential_histogram_aggregation.aggregate(Measurement(value, now, Mock(), ctx)) self.assertEqual(exponential_histogram_aggregation._mapping.scale, 0) self.assertEqual(exponential_histogram_aggregation._value_positive.offset, 0) self.assertEqual( exponential_histogram_aggregation._value_positive.counts, [1, 1, 1, 1], ) result = exponential_histogram_aggregation.collect( AggregationTemporality.DELTA, 0, ) for value in [1, 2, 4, 8]: exponential_histogram_aggregation.aggregate(Measurement(1 / value, now, Mock(), ctx)) self.assertEqual(exponential_histogram_aggregation._mapping.scale, 0) self.assertEqual(exponential_histogram_aggregation._value_positive.offset, -4) self.assertEqual( exponential_histogram_aggregation._value_positive.counts, [1, 1, 1, 1], ) result_1 = exponential_histogram_aggregation.collect( AggregationTemporality.DELTA, 0, ) self.assertEqual(result.scale, result_1.scale) test_logarithm_mapping.py000066400000000000000000000163011527070070700357450ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-sdk/tests/metrics/exponential_histogram# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # pylint: disable=protected-access from math import sqrt from unittest import TestCase from unittest.mock import patch from opentelemetry.sdk.metrics._internal.exponential_histogram.mapping.errors import ( MappingOverflowError, MappingUnderflowError, ) from opentelemetry.sdk.metrics._internal.exponential_histogram.mapping.ieee_754 import ( MAX_NORMAL_EXPONENT, MAX_NORMAL_VALUE, MIN_NORMAL_EXPONENT, MIN_NORMAL_VALUE, ) from opentelemetry.sdk.metrics._internal.exponential_histogram.mapping.logarithm_mapping import ( LogarithmMapping, ) def left_boundary(scale: int, index: int) -> float: # This is implemented in this way to avoid using a third-party bigfloat # package. The Go implementation uses a bigfloat package that is part of # their standard library. The assumption here is that the smallest float # available in Python is 2 ** -1022 (from sys.float_info.min). while scale > 0: if index < -1022: index /= 2 scale -= 1 else: break result = 2**index for _ in range(scale, 0, -1): result = sqrt(result) return result class TestLogarithmMapping(TestCase): # pylint: disable=invalid-name def assertInEpsilon(self, first, second, epsilon): self.assertLessEqual(first, (second * (1 + epsilon))) self.assertGreaterEqual(first, (second * (1 - epsilon))) @patch( "opentelemetry.sdk.metrics._internal.exponential_histogram.mapping." "logarithm_mapping.LogarithmMapping._mappings", new={}, ) @patch("opentelemetry.sdk.metrics._internal.exponential_histogram.mapping.logarithm_mapping.LogarithmMapping._init") def test_init_called_once(self, mock_init): # pylint: disable=no-self-use LogarithmMapping(3) LogarithmMapping(3) mock_init.assert_called_once() def test_invalid_scale(self): with self.assertRaises(Exception): LogarithmMapping(-1) def test_logarithm_mapping_scale_one(self): # The exponentiation factor for this logarithm exponent histogram # mapping is square_root(2). # Scale 1 means 1 division between every power of two, having # a factor sqare_root(2) times the lower boundary. logarithm_exponent_histogram_mapping = LogarithmMapping(1) self.assertEqual(logarithm_exponent_histogram_mapping.scale, 1) # Note: Do not test exact boundaries, with the exception of # 1, because we expect errors in that case (e.g., # MapToIndex(8) returns 5, an off-by-one. See the following # test. self.assertEqual(logarithm_exponent_histogram_mapping.map_to_index(15), 7) self.assertEqual(logarithm_exponent_histogram_mapping.map_to_index(9), 6) self.assertEqual(logarithm_exponent_histogram_mapping.map_to_index(7), 5) self.assertEqual(logarithm_exponent_histogram_mapping.map_to_index(5), 4) self.assertEqual(logarithm_exponent_histogram_mapping.map_to_index(3), 3) self.assertEqual(logarithm_exponent_histogram_mapping.map_to_index(2.5), 2) self.assertEqual(logarithm_exponent_histogram_mapping.map_to_index(1.5), 1) self.assertEqual(logarithm_exponent_histogram_mapping.map_to_index(1.2), 0) # This one is actually an exact test self.assertEqual(logarithm_exponent_histogram_mapping.map_to_index(1), -1) self.assertEqual(logarithm_exponent_histogram_mapping.map_to_index(0.75), -1) self.assertEqual(logarithm_exponent_histogram_mapping.map_to_index(0.55), -2) self.assertEqual(logarithm_exponent_histogram_mapping.map_to_index(0.45), -3) def test_logarithm_boundary(self): for scale in [1, 2, 3, 4, 10, 15]: logarithm_exponent_histogram_mapping = LogarithmMapping(scale) for index in [-100, -10, -1, 0, 1, 10, 100]: lower_boundary = logarithm_exponent_histogram_mapping.get_lower_boundary(index) mapped_index = logarithm_exponent_histogram_mapping.map_to_index(lower_boundary) self.assertLessEqual(index - 1, mapped_index) self.assertGreaterEqual(index, mapped_index) self.assertInEpsilon(lower_boundary, left_boundary(scale, index), 1e-9) def test_logarithm_index_max(self): for scale in range(LogarithmMapping._min_scale, LogarithmMapping._max_scale + 1): logarithm_mapping = LogarithmMapping(scale) index = logarithm_mapping.map_to_index(MAX_NORMAL_VALUE) max_index = ((MAX_NORMAL_EXPONENT + 1) << scale) - 1 # We do not check for max_index to be lesser than the # greatest integer because the greatest integer in Python is inf. self.assertEqual(index, max_index) boundary = logarithm_mapping.get_lower_boundary(index) base = logarithm_mapping.get_lower_boundary(1) self.assertLess(boundary, MAX_NORMAL_VALUE) self.assertInEpsilon((MAX_NORMAL_VALUE - boundary) / boundary, base - 1, 1e-6) with self.assertRaises(MappingOverflowError): logarithm_mapping.get_lower_boundary(index + 1) with self.assertRaises(MappingOverflowError): logarithm_mapping.get_lower_boundary(index + 2) def test_logarithm_index_min(self): for scale in range(LogarithmMapping._min_scale, LogarithmMapping._max_scale + 1): logarithm_mapping = LogarithmMapping(scale) min_index = logarithm_mapping.map_to_index(MIN_NORMAL_VALUE) correct_min_index = (MIN_NORMAL_EXPONENT << scale) - 1 self.assertEqual(min_index, correct_min_index) correct_mapped = left_boundary(scale, correct_min_index) self.assertLess(correct_mapped, MIN_NORMAL_VALUE) correct_mapped_upper = left_boundary(scale, correct_min_index + 1) self.assertEqual(correct_mapped_upper, MIN_NORMAL_VALUE) mapped = logarithm_mapping.get_lower_boundary(min_index + 1) self.assertInEpsilon(mapped, MIN_NORMAL_VALUE, 1e-6) self.assertEqual( logarithm_mapping.map_to_index(MIN_NORMAL_VALUE / 2), correct_min_index, ) self.assertEqual( logarithm_mapping.map_to_index(MIN_NORMAL_VALUE / 3), correct_min_index, ) self.assertEqual( logarithm_mapping.map_to_index(MIN_NORMAL_VALUE / 100), correct_min_index, ) self.assertEqual(logarithm_mapping.map_to_index(2**-1050), correct_min_index) self.assertEqual(logarithm_mapping.map_to_index(2**-1073), correct_min_index) self.assertEqual( logarithm_mapping.map_to_index(1.1 * 2**-1073), correct_min_index, ) self.assertEqual(logarithm_mapping.map_to_index(2**-1074), correct_min_index) mapped_lower = logarithm_mapping.get_lower_boundary(min_index) self.assertInEpsilon(correct_mapped, mapped_lower, 1e-6) with self.assertRaises(MappingUnderflowError): logarithm_mapping.get_lower_boundary(min_index - 1) python-opentelemetry-1.45.2/opentelemetry-sdk/tests/metrics/integration_test/000077500000000000000000000000001527070070700276705ustar00rootroot00000000000000test_cardinality_limit.py000066400000000000000000000234541527070070700347330ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-sdk/tests/metrics/integration_test# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # pylint: disable=protected-access from threading import Thread from unittest import TestCase from opentelemetry.metrics import NoOpMeterProvider, Observation from opentelemetry.sdk.metrics import ( Counter, Histogram, MeterProvider, ObservableCounter, ) from opentelemetry.sdk.metrics.export import ( AggregationTemporality, InMemoryMetricReader, ) from opentelemetry.sdk.metrics.view import View _OVERFLOW = "otel.metric.overflow" def _data_points(reader): metrics = reader.get_metrics_data().resource_metrics[0].scope_metrics[0].metrics return metrics[0].data.data_points def _overflow_points(data_points): return [point for point in data_points if _OVERFLOW in point.attributes] class TestCardinalityLimit(TestCase): @staticmethod def _create(reader, views=()): reader._set_meter_provider(NoOpMeterProvider()) return MeterProvider(metric_readers=[reader], views=list(views)) def test_default_limit_is_2000(self): reader = InMemoryMetricReader() counter = self._create(reader).get_meter("m").create_counter("c") for index in range(2500): counter.add(1, {"k": index}) data_points = _data_points(reader) self.assertEqual(len(data_points), 2001) self.assertEqual(sum(point.value for point in data_points), 2500) overflow = _overflow_points(data_points) self.assertEqual(len(overflow), 1) self.assertEqual(dict(overflow[0].attributes), {_OVERFLOW: True}) self.assertEqual(overflow[0].value, 2500 - 2000) def test_no_overflow_up_to_limit(self): for attribute_sets in (4, 5): with self.subTest(attribute_sets=attribute_sets): reader = InMemoryMetricReader(cardinality_limits={Counter: 5}) counter = self._create(reader).get_meter("m").create_counter("c") for index in range(attribute_sets): counter.add(1, {"k": index}) data_points = _data_points(reader) self.assertEqual(len(data_points), attribute_sets) self.assertEqual(_overflow_points(data_points), []) def test_reader_limit_per_instrument_class(self): reader = InMemoryMetricReader(cardinality_limits={Counter: 3}) meter = self._create(reader).get_meter("m") counter = meter.create_counter("counter") histogram = meter.create_histogram("histogram") for index in range(10): counter.add(1, {"k": index}) histogram.record(1, {"k": index}) metrics = { metric.name: metric for metric in reader.get_metrics_data().resource_metrics[0].scope_metrics[0].metrics } self.assertEqual(len(metrics["counter"].data.data_points), 4) self.assertEqual(len(metrics["histogram"].data.data_points), 10) def test_view_limit_overrides_reader_limit(self): reader = InMemoryMetricReader(cardinality_limits={Counter: 3}) view = View(instrument_name="c", aggregation_cardinality_limit=6) counter = self._create(reader, [view]).get_meter("m").create_counter("c") for index in range(10): counter.add(1, {"k": index}) data_points = _data_points(reader) self.assertEqual(len(data_points), 7) self.assertEqual(sum(point.value for point in data_points), 10) def test_limit_of_one_keeps_first_attribute_set(self): reader = InMemoryMetricReader(cardinality_limits={Counter: 1}) counter = self._create(reader).get_meter("m").create_counter("c") for index in range(5): counter.add(2, {"k": index}) data_points = _data_points(reader) by_attributes = {tuple(sorted(point.attributes.items())): point.value for point in data_points} self.assertEqual(by_attributes, {(("k", 0),): 2, ((_OVERFLOW, True),): 8}) def test_limit_applies_after_attribute_filtering(self): reader = InMemoryMetricReader(cardinality_limits={Counter: 3}) view = View(instrument_name="c", attribute_keys={"keep"}) counter = self._create(reader, [view]).get_meter("m").create_counter("c") for index in range(10): counter.add(1, {"keep": index % 2, "drop": index}) data_points = _data_points(reader) self.assertEqual(len(data_points), 2) self.assertEqual(_overflow_points(data_points), []) def test_cumulative_keeps_sets_observed_before_overflow(self): reader = InMemoryMetricReader(cardinality_limits={Counter: 3}) counter = self._create(reader).get_meter("m").create_counter("c") counter.add(1, {"k": "a"}) counter.add(1, {"k": "b"}) counter.add(1, {"k": "c"}) counter.add(1, {"k": "d"}) counter.add(1, {"k": "a"}) counter.add(1, {"k": "e"}) data_points = _data_points(reader) by_attributes = {tuple(sorted(point.attributes.items())): point.value for point in data_points} self.assertEqual( by_attributes, { (("k", "a"),): 2, (("k", "b"),): 1, (("k", "c"),): 1, ((_OVERFLOW, True),): 2, }, ) # The sets that were present before overflow are still exported. counter.add(1, {"k": "c"}) data_points = _data_points(reader) self.assertEqual(len(data_points), 4) self.assertEqual(sum(point.value for point in data_points), 7) def test_delta_limit_resets_every_collection(self): reader = InMemoryMetricReader( preferred_temporality={Counter: AggregationTemporality.DELTA}, cardinality_limits={Counter: 3}, ) counter = self._create(reader).get_meter("m").create_counter("c") for cycle in range(3): for index in range(6): counter.add(1, {"k": f"{cycle}-{index}"}) data_points = _data_points(reader) with self.subTest(cycle=cycle): self.assertEqual(len(data_points), 4) self.assertEqual(sum(point.value for point in data_points), 6) self.assertEqual(len(_overflow_points(data_points)), 1) def test_delta_idle_sets_are_not_exported_or_counted(self): reader = InMemoryMetricReader( preferred_temporality={Counter: AggregationTemporality.DELTA}, cardinality_limits={Counter: 3}, ) counter = self._create(reader).get_meter("m").create_counter("c") counter.add(1, {"k": "a"}) counter.add(1, {"k": "b"}) _data_points(reader) counter.add(1, {"k": "c"}) counter.add(1, {"k": "d"}) data_points = _data_points(reader) self.assertEqual(len(data_points), 2) self.assertEqual(_overflow_points(data_points), []) def test_delta_concurrent_measurements_are_not_lost(self): reader = InMemoryMetricReader( preferred_temporality={Counter: AggregationTemporality.DELTA}, cardinality_limits={Counter: 10}, ) counter = self._create(reader).get_meter("m").create_counter("c") measurements_per_thread = 2000 def record(): for index in range(measurements_per_thread): counter.add(1, {"k": index % 50}) threads = [Thread(target=record) for _ in range(4)] for thread in threads: thread.start() total = 0 while any(thread.is_alive() for thread in threads): total += sum(point.value for point in _data_points_or_empty(reader)) for thread in threads: thread.join() total += sum(point.value for point in _data_points_or_empty(reader)) self.assertEqual(total, 4 * measurements_per_thread) def test_asynchronous_instrument_prefers_first_observed(self): def callback(options): for index in range(6): yield Observation(index, {"k": index}) reader = InMemoryMetricReader(cardinality_limits={ObservableCounter: 3}) meter = self._create(reader).get_meter("m") meter.create_observable_counter("oc", callbacks=[callback]) data_points = _data_points(reader) by_attributes = {tuple(sorted(point.attributes.items())): point.value for point in data_points} self.assertEqual( by_attributes, { (("k", 0),): 0, (("k", 1),): 1, (("k", 2),): 2, ((_OVERFLOW, True),): 3 + 4 + 5, }, ) def test_asynchronous_delta_conversion_survives_collection(self): values = iter([10, 25]) def callback(options): yield Observation(next(values, 0), {"k": "a"}) reader = InMemoryMetricReader( preferred_temporality={ObservableCounter: AggregationTemporality.DELTA}, cardinality_limits={ObservableCounter: 3}, ) self._create(reader).get_meter("m").create_observable_counter("oc", callbacks=[callback]) self.assertEqual(_data_points(reader)[0].value, 10) self.assertEqual(_data_points(reader)[0].value, 15) def test_histogram_overflow(self): reader = InMemoryMetricReader(cardinality_limits={Histogram: 2}) histogram = self._create(reader).get_meter("m").create_histogram("h") histogram.record(1, {"k": "a"}) histogram.record(2, {"k": "b"}) histogram.record(3, {"k": "c"}) data_points = _data_points(reader) self.assertEqual(len(data_points), 3) overflow = _overflow_points(data_points) self.assertEqual(len(overflow), 1) self.assertEqual(overflow[0].count, 1) self.assertEqual(overflow[0].sum, 3) def _data_points_or_empty(reader): data = reader.get_metrics_data() if data is None: return [] return data.resource_metrics[0].scope_metrics[0].metrics[0].data.data_points test_console_exporter.py000066400000000000000000000076621527070070700346270ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-sdk/tests/metrics/integration_test# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from io import StringIO from json import loads from os import linesep from unittest import TestCase from unittest.mock import Mock, patch from opentelemetry.context import Context from opentelemetry.metrics import get_meter, set_meter_provider from opentelemetry.sdk.metrics import AlwaysOnExemplarFilter, MeterProvider from opentelemetry.sdk.metrics.export import ( ConsoleMetricExporter, PeriodicExportingMetricReader, ) from opentelemetry.test.globals_test import reset_metrics_globals TEST_TIMESTAMP = 1_234_567_890 class TestConsoleExporter(TestCase): def setUp(self): reset_metrics_globals() def tearDown(self): reset_metrics_globals() def test_console_exporter(self): output = StringIO() exporter = ConsoleMetricExporter(out=output) reader = PeriodicExportingMetricReader(exporter, export_interval_millis=100) provider = MeterProvider(metric_readers=[reader]) set_meter_provider(provider) meter = get_meter(__name__) counter = meter.create_counter("name", description="description", unit="unit") counter.add(1, attributes={"a": "b"}) provider.shutdown() output.seek(0) result_0 = loads("".join(output.readlines())) self.assertGreater(len(result_0), 0) metrics = result_0["resource_metrics"][0]["scope_metrics"][0] self.assertEqual(metrics["scope"]["name"], "test_console_exporter") metrics = metrics["metrics"][0] self.assertEqual(metrics["name"], "name") self.assertEqual(metrics["description"], "description") self.assertEqual(metrics["unit"], "unit") metrics = metrics["data"] self.assertEqual(metrics["aggregation_temporality"], 2) self.assertTrue(metrics["is_monotonic"]) metrics = metrics["data_points"][0] self.assertEqual(metrics["attributes"], {"a": "b"}) self.assertEqual(metrics["value"], 1) def test_console_exporter_no_export(self): output = StringIO() exporter = ConsoleMetricExporter(out=output) reader = PeriodicExportingMetricReader(exporter, export_interval_millis=100) provider = MeterProvider(metric_readers=[reader]) provider.shutdown() output.seek(0) actual = "".join(output.readlines()) expected = "" self.assertEqual(actual, expected) @patch( "opentelemetry.sdk.metrics._internal.instrument.time_ns", Mock(return_value=TEST_TIMESTAMP), ) def test_console_exporter_with_exemplars(self): ctx = Context() output = StringIO() exporter = ConsoleMetricExporter(out=output) reader = PeriodicExportingMetricReader(exporter, export_interval_millis=100) provider = MeterProvider(metric_readers=[reader], exemplar_filter=AlwaysOnExemplarFilter()) set_meter_provider(provider) meter = get_meter(__name__) counter = meter.create_counter("name", description="description", unit="unit") counter.add(1, attributes={"a": "b"}, context=ctx) provider.shutdown() output.seek(0) joined_output = "".join(output.readlines()) result_0 = loads(joined_output.strip(linesep)) self.assertGreater(len(result_0), 0) metrics = result_0["resource_metrics"][0]["scope_metrics"][0] self.assertEqual(metrics["scope"]["name"], "test_console_exporter") point = metrics["metrics"][0]["data"]["data_points"][0] self.assertEqual(point["attributes"], {"a": "b"}) self.assertEqual(point["value"], 1) self.assertEqual( point["exemplars"], [ { "filtered_attributes": {}, "value": 1, "time_unix_nano": TEST_TIMESTAMP, "span_id": None, "trace_id": None, } ], ) python-opentelemetry-1.45.2/opentelemetry-sdk/tests/metrics/integration_test/test_cpu_time.py000066400000000000000000000265401527070070700331150ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # type: ignore import io from collections.abc import Generator, Iterable from unittest import TestCase from unittest.mock import Mock, patch from opentelemetry.context import Context from opentelemetry.metrics import CallbackOptions, Instrument, Observation from opentelemetry.sdk.metrics import MeterProvider from opentelemetry.sdk.metrics._internal.measurement import Measurement # FIXME Test that the instrument methods can be called concurrently safely. TEST_TIMESTAMP = 1_234_567_890 TEST_CONTEXT = Context() @patch( "opentelemetry.sdk.metrics._internal.instrument.time_ns", Mock(return_value=TEST_TIMESTAMP), ) class TestCpuTimeIntegration(TestCase): """Integration test of scraping CPU time from proc stat with an observable counter""" procstat_str = """\ cpu 8549517 4919096 9165935 1430260740 1641349 0 1646147 623279 0 0 cpu0 615029 317746 594601 89126459 129629 0 834346 42137 0 0 cpu1 588232 349185 640492 89156411 124485 0 241004 41862 0 0 intr 4370168813 38 9 0 0 1639 0 0 0 0 0 2865202 0 152 0 0 0 0 0 0 0 0 0 0 0 0 7236812 5966240 4501046 6467792 7289114 6048205 5299600 5178254 4642580 6826812 6880917 6230308 6307699 4699637 6119330 4905094 5644039 4700633 10539029 5365438 6086908 2227906 5094323 9685701 10137610 7739951 7143508 8123281 4968458 5683103 9890878 4466603 0 0 0 8929628 0 5 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 ctxt 6877594077 btime 1631501040 processes 2557351 procs_running 2 procs_blocked 0 softirq 1644603067 0 166540056 208 309152755 8936439 0 1354908 935642970 13 222975718\n""" @staticmethod def create_measurements_expected( instrument: Instrument, ) -> list[Measurement]: return [ Measurement( 6150.29, TEST_TIMESTAMP, instrument=instrument, context=TEST_CONTEXT, attributes={"cpu": "cpu0", "state": "user"}, ), Measurement( 3177.46, TEST_TIMESTAMP, instrument=instrument, context=TEST_CONTEXT, attributes={"cpu": "cpu0", "state": "nice"}, ), Measurement( 5946.01, TEST_TIMESTAMP, instrument=instrument, context=TEST_CONTEXT, attributes={"cpu": "cpu0", "state": "system"}, ), Measurement( 891264.59, TEST_TIMESTAMP, instrument=instrument, context=TEST_CONTEXT, attributes={"cpu": "cpu0", "state": "idle"}, ), Measurement( 1296.29, TEST_TIMESTAMP, instrument=instrument, context=TEST_CONTEXT, attributes={"cpu": "cpu0", "state": "iowait"}, ), Measurement( 0.0, TEST_TIMESTAMP, instrument=instrument, context=TEST_CONTEXT, attributes={"cpu": "cpu0", "state": "irq"}, ), Measurement( 8343.46, TEST_TIMESTAMP, instrument=instrument, context=TEST_CONTEXT, attributes={"cpu": "cpu0", "state": "softirq"}, ), Measurement( 421.37, TEST_TIMESTAMP, instrument=instrument, context=TEST_CONTEXT, attributes={"cpu": "cpu0", "state": "guest"}, ), Measurement( 0, TEST_TIMESTAMP, instrument=instrument, context=TEST_CONTEXT, attributes={"cpu": "cpu0", "state": "guest_nice"}, ), Measurement( 5882.32, TEST_TIMESTAMP, instrument=instrument, context=TEST_CONTEXT, attributes={"cpu": "cpu1", "state": "user"}, ), Measurement( 3491.85, TEST_TIMESTAMP, instrument=instrument, context=TEST_CONTEXT, attributes={"cpu": "cpu1", "state": "nice"}, ), Measurement( 6404.92, TEST_TIMESTAMP, instrument=instrument, context=TEST_CONTEXT, attributes={"cpu": "cpu1", "state": "system"}, ), Measurement( 891564.11, TEST_TIMESTAMP, instrument=instrument, context=TEST_CONTEXT, attributes={"cpu": "cpu1", "state": "idle"}, ), Measurement( 1244.85, TEST_TIMESTAMP, instrument=instrument, context=TEST_CONTEXT, attributes={"cpu": "cpu1", "state": "iowait"}, ), Measurement( 0, TEST_TIMESTAMP, instrument=instrument, context=TEST_CONTEXT, attributes={"cpu": "cpu1", "state": "irq"}, ), Measurement( 2410.04, TEST_TIMESTAMP, instrument=instrument, context=TEST_CONTEXT, attributes={"cpu": "cpu1", "state": "softirq"}, ), Measurement( 418.62, TEST_TIMESTAMP, instrument=instrument, context=TEST_CONTEXT, attributes={"cpu": "cpu1", "state": "guest"}, ), Measurement( 0, TEST_TIMESTAMP, instrument=instrument, context=TEST_CONTEXT, attributes={"cpu": "cpu1", "state": "guest_nice"}, ), ] def test_cpu_time_callback(self): def cpu_time_callback( options: CallbackOptions, ) -> Iterable[Observation]: procstat = io.StringIO(self.procstat_str) procstat.readline() # skip the first line for line in procstat: if not line.startswith("cpu"): break cpu, *states = line.split() yield Observation(int(states[0]) / 100, {"cpu": cpu, "state": "user"}) yield Observation(int(states[1]) / 100, {"cpu": cpu, "state": "nice"}) yield Observation(int(states[2]) / 100, {"cpu": cpu, "state": "system"}) yield Observation(int(states[3]) / 100, {"cpu": cpu, "state": "idle"}) yield Observation(int(states[4]) / 100, {"cpu": cpu, "state": "iowait"}) yield Observation(int(states[5]) / 100, {"cpu": cpu, "state": "irq"}) yield Observation(int(states[6]) / 100, {"cpu": cpu, "state": "softirq"}) yield Observation(int(states[7]) / 100, {"cpu": cpu, "state": "guest"}) yield Observation(int(states[8]) / 100, {"cpu": cpu, "state": "guest_nice"}) meter = MeterProvider().get_meter("name") observable_counter = meter.create_observable_counter( "system.cpu.time", callbacks=[cpu_time_callback], unit="s", description="CPU time", ) measurements = list(observable_counter.callback(CallbackOptions())) self.assertEqual(measurements, self.create_measurements_expected(observable_counter)) def test_cpu_time_generator(self): def cpu_time_generator() -> Generator[Iterable[Observation], None, None]: options = yield while True: self.assertIsInstance(options, CallbackOptions) measurements = [] procstat = io.StringIO(self.procstat_str) procstat.readline() # skip the first line for line in procstat: if not line.startswith("cpu"): break cpu, *states = line.split() measurements.append( Observation( int(states[0]) / 100, {"cpu": cpu, "state": "user"}, ) ) measurements.append( Observation( int(states[1]) / 100, {"cpu": cpu, "state": "nice"}, ) ) measurements.append( Observation( int(states[2]) / 100, {"cpu": cpu, "state": "system"}, ) ) measurements.append( Observation( int(states[3]) / 100, {"cpu": cpu, "state": "idle"}, ) ) measurements.append( Observation( int(states[4]) / 100, {"cpu": cpu, "state": "iowait"}, ) ) measurements.append(Observation(int(states[5]) / 100, {"cpu": cpu, "state": "irq"})) measurements.append( Observation( int(states[6]) / 100, {"cpu": cpu, "state": "softirq"}, ) ) measurements.append( Observation( int(states[7]) / 100, {"cpu": cpu, "state": "guest"}, ) ) measurements.append( Observation( int(states[8]) / 100, {"cpu": cpu, "state": "guest_nice"}, ) ) options = yield measurements meter = MeterProvider().get_meter("name") observable_counter = meter.create_observable_counter( "system.cpu.time", callbacks=[cpu_time_generator()], unit="s", description="CPU time", ) measurements = list(observable_counter.callback(CallbackOptions())) self.assertEqual(measurements, self.create_measurements_expected(observable_counter)) maxDiff = None test_disable_default_views.py000066400000000000000000000036551527070070700355570ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-sdk/tests/metrics/integration_test# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from unittest import TestCase from opentelemetry.sdk.metrics import MeterProvider from opentelemetry.sdk.metrics.export import InMemoryMetricReader from opentelemetry.sdk.metrics.view import DropAggregation, View class TestDisableDefaultViews(TestCase): def test_disable_default_views(self): reader = InMemoryMetricReader() meter_provider = MeterProvider( metric_readers=[reader], views=[View(instrument_name="*", aggregation=DropAggregation())], ) meter = meter_provider.get_meter("testmeter") counter = meter.create_counter("testcounter") counter.add(10, {"label": "value1"}) counter.add(10, {"label": "value2"}) counter.add(10, {"label": "value3"}) self.assertIsNone(reader.get_metrics_data()) def test_disable_default_views_add_custom(self): reader = InMemoryMetricReader() meter_provider = MeterProvider( metric_readers=[reader], views=[ View(instrument_name="*", aggregation=DropAggregation()), View(instrument_name="testhist"), ], ) meter = meter_provider.get_meter("testmeter") counter = meter.create_counter("testcounter") histogram = meter.create_histogram("testhist") counter.add(10, {"label": "value1"}) counter.add(10, {"label": "value2"}) counter.add(10, {"label": "value3"}) histogram.record(12, {"label": "value"}) metrics = reader.get_metrics_data() self.assertEqual(len(metrics.resource_metrics), 1) self.assertEqual(len(metrics.resource_metrics[0].scope_metrics), 1) self.assertEqual(len(metrics.resource_metrics[0].scope_metrics[0].metrics), 1) self.assertEqual( metrics.resource_metrics[0].scope_metrics[0].metrics[0].name, "testhist", ) python-opentelemetry-1.45.2/opentelemetry-sdk/tests/metrics/integration_test/test_exemplars.py000066400000000000000000000263661527070070700333160ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import os from unittest import TestCase, mock from opentelemetry import trace as trace_api from opentelemetry.sdk.metrics import Exemplar, MeterProvider from opentelemetry.sdk.metrics.export import ( AggregationTemporality, InMemoryMetricReader, Metric, NumberDataPoint, Sum, ) from opentelemetry.trace import SpanContext, TraceFlags class TestExemplars(TestCase): TRACE_ID = int("d4cda95b652f4a1592b449d5929fda1b", 16) SPAN_ID = int("6e0c63257de34c92", 16) @mock.patch.dict(os.environ, {"OTEL_METRICS_EXEMPLAR_FILTER": "always_on"}) def test_always_on_exemplars(self): reader = InMemoryMetricReader() meter_provider = MeterProvider( metric_readers=[reader], ) meter = meter_provider.get_meter("testmeter") counter = meter.create_counter("testcounter") counter.add(10, {"label": "value1"}) data = reader.get_metrics_data() metrics = data.resource_metrics[0].scope_metrics[0].metrics self.assertEqual( metrics, [ Metric( name="testcounter", description="", unit="", data=Sum( data_points=[ NumberDataPoint( attributes={"label": "value1"}, start_time_unix_nano=mock.ANY, time_unix_nano=mock.ANY, value=10, exemplars=[ Exemplar( filtered_attributes={}, value=10, time_unix_nano=mock.ANY, span_id=None, trace_id=None, ), ], ) ], aggregation_temporality=AggregationTemporality.CUMULATIVE, is_monotonic=True, ), ) ], ) @mock.patch.dict(os.environ, {"OTEL_METRICS_EXEMPLAR_FILTER": "trace_based"}) def test_trace_based_exemplars(self): span_context = SpanContext( trace_id=self.TRACE_ID, span_id=self.SPAN_ID, is_remote=False, trace_flags=TraceFlags(TraceFlags.SAMPLED), trace_state={}, ) span = trace_api.NonRecordingSpan(span_context) trace_api.set_span_in_context(span) reader = InMemoryMetricReader() meter_provider = MeterProvider( metric_readers=[reader], ) meter = meter_provider.get_meter("testmeter") counter = meter.create_counter("testcounter") with trace_api.use_span(span): counter.add(10, {"label": "value1"}) data = reader.get_metrics_data() metrics = data.resource_metrics[0].scope_metrics[0].metrics self.assertEqual( metrics, [ Metric( name="testcounter", description="", unit="", data=Sum( data_points=[ NumberDataPoint( attributes={"label": "value1"}, start_time_unix_nano=mock.ANY, time_unix_nano=mock.ANY, value=10, exemplars=[ Exemplar( filtered_attributes={}, value=10, time_unix_nano=mock.ANY, span_id=self.SPAN_ID, trace_id=self.TRACE_ID, ), ], ) ], aggregation_temporality=AggregationTemporality.CUMULATIVE, is_monotonic=True, ), ) ], ) def test_default_exemplar_filter_no_span(self): reader = InMemoryMetricReader() meter_provider = MeterProvider( metric_readers=[reader], ) meter = meter_provider.get_meter("testmeter") counter = meter.create_counter("testcounter") counter.add(10, {"label": "value1"}) data = reader.get_metrics_data() metrics = data.resource_metrics[0].scope_metrics[0].metrics self.assertEqual( metrics, [ Metric( name="testcounter", description="", unit="", data=Sum( data_points=[ NumberDataPoint( attributes={"label": "value1"}, start_time_unix_nano=mock.ANY, time_unix_nano=mock.ANY, value=10, exemplars=[], ) ], aggregation_temporality=AggregationTemporality.CUMULATIVE, is_monotonic=True, ), ) ], ) def test_default_exemplar_filter(self): span_context = SpanContext( trace_id=self.TRACE_ID, span_id=self.SPAN_ID, is_remote=False, trace_flags=TraceFlags(TraceFlags.SAMPLED), trace_state={}, ) span = trace_api.NonRecordingSpan(span_context) trace_api.set_span_in_context(span) reader = InMemoryMetricReader() meter_provider = MeterProvider( metric_readers=[reader], ) meter = meter_provider.get_meter("testmeter") counter = meter.create_counter("testcounter") with trace_api.use_span(span): counter.add(10, {"label": "value1"}) data = reader.get_metrics_data() metrics = data.resource_metrics[0].scope_metrics[0].metrics self.assertEqual( metrics, [ Metric( name="testcounter", description="", unit="", data=Sum( data_points=[ NumberDataPoint( attributes={"label": "value1"}, start_time_unix_nano=mock.ANY, time_unix_nano=mock.ANY, value=10, exemplars=[ Exemplar( filtered_attributes={}, value=10, time_unix_nano=mock.ANY, span_id=self.SPAN_ID, trace_id=self.TRACE_ID, ), ], ) ], aggregation_temporality=AggregationTemporality.CUMULATIVE, is_monotonic=True, ), ) ], ) def test_exemplar_trace_based_manual_context(self): span_context = SpanContext( trace_id=self.TRACE_ID, span_id=self.SPAN_ID, is_remote=False, trace_flags=TraceFlags(TraceFlags.SAMPLED), trace_state={}, ) span = trace_api.NonRecordingSpan(span_context) ctx = trace_api.set_span_in_context(span) reader = InMemoryMetricReader() meter_provider = MeterProvider( metric_readers=[reader], ) meter = meter_provider.get_meter("testmeter") counter = meter.create_counter("testcounter") counter.add(10, {"label": "value1"}, context=ctx) data = reader.get_metrics_data() metrics = data.resource_metrics[0].scope_metrics[0].metrics self.assertEqual( metrics, [ Metric( name="testcounter", description="", unit="", data=Sum( data_points=[ NumberDataPoint( attributes={"label": "value1"}, start_time_unix_nano=mock.ANY, time_unix_nano=mock.ANY, value=10, exemplars=[ Exemplar( filtered_attributes={}, value=10, time_unix_nano=mock.ANY, span_id=self.SPAN_ID, trace_id=self.TRACE_ID, ), ], ) ], aggregation_temporality=AggregationTemporality.CUMULATIVE, is_monotonic=True, ), ) ], ) @mock.patch.dict(os.environ, {"OTEL_METRICS_EXEMPLAR_FILTER": "always_off"}) def test_always_off_exemplars(self): span_context = SpanContext( trace_id=self.TRACE_ID, span_id=self.SPAN_ID, is_remote=False, trace_flags=TraceFlags(TraceFlags.SAMPLED), trace_state={}, ) span = trace_api.NonRecordingSpan(span_context) trace_api.set_span_in_context(span) reader = InMemoryMetricReader() meter_provider = MeterProvider( metric_readers=[reader], ) meter = meter_provider.get_meter("testmeter") counter = meter.create_counter("testcounter") with trace_api.use_span(span): counter.add(10, {"label": "value1"}) data = reader.get_metrics_data() metrics = data.resource_metrics[0].scope_metrics[0].metrics self.assertEqual( metrics, [ Metric( name="testcounter", description="", unit="", data=Sum( data_points=[ NumberDataPoint( attributes={"label": "value1"}, start_time_unix_nano=mock.ANY, time_unix_nano=mock.ANY, value=10, exemplars=[], ) ], aggregation_temporality=AggregationTemporality.CUMULATIVE, is_monotonic=True, ), ) ], ) test_explicit_bucket_histogram_aggregation.py000066400000000000000000000156511527070070700410340ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-sdk/tests/metrics/integration_test# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from platform import system from time import sleep from unittest import TestCase from pytest import mark from opentelemetry.metrics import NoOpMeterProvider from opentelemetry.sdk.metrics import Histogram, MeterProvider from opentelemetry.sdk.metrics.export import ( AggregationTemporality, InMemoryMetricReader, ) from opentelemetry.sdk.metrics.view import ExplicitBucketHistogramAggregation class TestExplicitBucketHistogramAggregation(TestCase): test_values = [1, 6, 11, 26, 51, 76, 101, 251, 501, 751] @mark.skipif( system() == "Windows", reason=( "Tests fail because Windows time_ns resolution is too low so " "two different time measurements may end up having the exact same" "value." ), ) def test_synchronous_delta_temporality(self): aggregation = ExplicitBucketHistogramAggregation() reader = InMemoryMetricReader( preferred_aggregation={Histogram: aggregation}, preferred_temporality={Histogram: AggregationTemporality.DELTA}, ) provider = MeterProvider(metric_readers=[reader]) # Disable SDK metrics # pylint: disable=protected-access reader._set_meter_provider(NoOpMeterProvider()) meter = provider.get_meter("name", "version") histogram = meter.create_histogram("histogram") results = [] for _ in range(10): results.append(reader.get_metrics_data()) for metrics_data in results: self.assertIsNone(metrics_data) results = [] for test_value in self.test_values: histogram.record(test_value) results.append(reader.get_metrics_data()) metric_data = results[0].resource_metrics[0].scope_metrics[0].metrics[0].data.data_points[0] previous_time_unix_nano = metric_data.time_unix_nano self.assertEqual( metric_data.bucket_counts, (0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), ) self.assertLess( metric_data.start_time_unix_nano, previous_time_unix_nano, ) self.assertEqual(metric_data.min, self.test_values[0]) self.assertEqual(metric_data.max, self.test_values[0]) self.assertEqual(metric_data.sum, self.test_values[0]) for index, metrics_data in enumerate(results[1:]): metric_data = metrics_data.resource_metrics[0].scope_metrics[0].metrics[0].data.data_points[0] self.assertEqual(previous_time_unix_nano, metric_data.start_time_unix_nano) previous_time_unix_nano = metric_data.time_unix_nano self.assertEqual( metric_data.bucket_counts, # pylint: disable=consider-using-generator tuple([1 if internal_index == index + 2 else 0 for internal_index in range(16)]), ) self.assertLess(metric_data.start_time_unix_nano, metric_data.time_unix_nano) self.assertEqual(metric_data.min, self.test_values[index + 1]) self.assertEqual(metric_data.max, self.test_values[index + 1]) self.assertEqual(metric_data.sum, self.test_values[index + 1]) results = [] for _ in range(10): results.append(reader.get_metrics_data()) for metrics_data in results: self.assertIsNone(metrics_data) results = [] histogram.record(1) results.append(reader.get_metrics_data()) sleep(0.1) results.append(reader.get_metrics_data()) histogram.record(2) results.append(reader.get_metrics_data()) metric_data_0 = results[0].resource_metrics[0].scope_metrics[0].metrics[0].data.data_points[0] metric_data_2 = results[2].resource_metrics[0].scope_metrics[0].metrics[0].data.data_points[0] self.assertIsNone(results[1]) self.assertGreater(metric_data_2.start_time_unix_nano, metric_data_0.time_unix_nano) provider.shutdown() @mark.skipif( system() == "Windows", reason=( "Tests fail because Windows time_ns resolution is too low so " "two different time measurements may end up having the exact same" "value." ), ) def test_synchronous_cumulative_temporality(self): aggregation = ExplicitBucketHistogramAggregation() reader = InMemoryMetricReader( preferred_aggregation={Histogram: aggregation}, preferred_temporality={Histogram: AggregationTemporality.CUMULATIVE}, ) provider = MeterProvider(metric_readers=[reader]) # Disable SDK metrics # pylint: disable=protected-access reader._set_meter_provider(NoOpMeterProvider()) meter = provider.get_meter("name", "version") histogram = meter.create_histogram("histogram") results = [] for _ in range(10): results.append(reader.get_metrics_data()) for metrics_data in results: self.assertIsNone(metrics_data) results = [] for test_value in self.test_values: histogram.record(test_value) results.append(reader.get_metrics_data()) start_time_unix_nano = ( results[0].resource_metrics[0].scope_metrics[0].metrics[0].data.data_points[0].start_time_unix_nano ) for index, metrics_data in enumerate(results): metric_data = metrics_data.resource_metrics[0].scope_metrics[0].metrics[0].data.data_points[0] self.assertEqual(start_time_unix_nano, metric_data.start_time_unix_nano) self.assertEqual( metric_data.bucket_counts, # pylint: disable=consider-using-generator tuple([(0 if internal_index < 1 or internal_index > index + 1 else 1) for internal_index in range(16)]), ) self.assertEqual(metric_data.min, self.test_values[0]) self.assertEqual(metric_data.max, self.test_values[index]) self.assertEqual(metric_data.sum, sum(self.test_values[: index + 1])) results = [] for _ in range(10): results.append(reader.get_metrics_data()) provider.shutdown() start_time_unix_nano = ( results[0].resource_metrics[0].scope_metrics[0].metrics[0].data.data_points[0].start_time_unix_nano ) for metrics_data in results: metric_data = metrics_data.resource_metrics[0].scope_metrics[0].metrics[0].data.data_points[0] self.assertEqual(start_time_unix_nano, metric_data.start_time_unix_nano) self.assertEqual( metric_data.bucket_counts, (0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0), ) self.assertEqual(metric_data.min, self.test_values[0]) self.assertEqual(metric_data.max, self.test_values[-1]) self.assertEqual(metric_data.sum, sum(self.test_values)) test_exponential_bucket_histogram.py000066400000000000000000000235351527070070700371720ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-sdk/tests/metrics/integration_test# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from platform import system from time import sleep from unittest import TestCase from pytest import mark from opentelemetry.metrics import NoOpMeterProvider from opentelemetry.sdk.metrics import Histogram, MeterProvider from opentelemetry.sdk.metrics.export import ( AggregationTemporality, InMemoryMetricReader, ) from opentelemetry.sdk.metrics.view import ( ExponentialBucketHistogramAggregation, ) class TestExponentialBucketHistogramAggregation(TestCase): test_values = [2, 4, 1, 1, 8, 0.5, 0.1, 0.045] @mark.skipif( system() == "Windows", reason=( "Tests fail because Windows time_ns resolution is too low so " "two different time measurements may end up having the exact same" "value." ), ) def test_synchronous_delta_temporality(self): """ This test case instantiates an exponential histogram aggregation and then uses it to record measurements and get metrics. The order in which these actions are taken are relevant to the testing that happens here. For this reason, the aggregation is only instantiated once, since the reinstantiation of the aggregation would defeat the purpose of this test case. """ aggregation = ExponentialBucketHistogramAggregation() reader = InMemoryMetricReader( preferred_aggregation={Histogram: aggregation}, preferred_temporality={Histogram: AggregationTemporality.DELTA}, ) provider = MeterProvider(metric_readers=[reader]) # Disable SDK metrics # pylint: disable=protected-access reader._set_meter_provider(NoOpMeterProvider()) meter = provider.get_meter("name", "version") histogram = meter.create_histogram("histogram") # The test scenario here is calling collect without calling aggregate # ever before. results = [] for _ in range(10): results.append(reader.get_metrics_data()) for metrics_data in results: self.assertIsNone(metrics_data) # The test scenario here is calling aggregate then collect repeatedly. results = [] for test_value in self.test_values: histogram.record(test_value) results.append(reader.get_metrics_data()) metric_data = results[0].resource_metrics[0].scope_metrics[0].metrics[0].data.data_points[0] previous_time_unix_nano = metric_data.time_unix_nano self.assertEqual(metric_data.positive.bucket_counts, [1]) self.assertEqual(metric_data.negative.bucket_counts, [0]) self.assertLess( metric_data.start_time_unix_nano, previous_time_unix_nano, ) self.assertEqual(metric_data.min, self.test_values[0]) self.assertEqual(metric_data.max, self.test_values[0]) self.assertEqual(metric_data.sum, self.test_values[0]) for index, metrics_data in enumerate(results[1:]): metric_data = metrics_data.resource_metrics[0].scope_metrics[0].metrics[0].data.data_points[0] self.assertEqual(previous_time_unix_nano, metric_data.start_time_unix_nano) previous_time_unix_nano = metric_data.time_unix_nano self.assertEqual(metric_data.positive.bucket_counts, [1]) self.assertEqual(metric_data.negative.bucket_counts, [0]) self.assertLess(metric_data.start_time_unix_nano, metric_data.time_unix_nano) self.assertEqual(metric_data.min, self.test_values[index + 1]) self.assertEqual(metric_data.max, self.test_values[index + 1]) # Using assertAlmostEqual here because in 3.12 resolution can cause # these checks to fail. self.assertAlmostEqual(metric_data.sum, self.test_values[index + 1]) # The test scenario here is calling collect without calling aggregate # immediately before, but having aggregate being called before at some # moment. results = [] for _ in range(10): results.append(reader.get_metrics_data()) for metrics_data in results: self.assertIsNone(metrics_data) # The test scenario here is calling aggregate and collect, waiting for # a certain amount of time, calling collect, then calling aggregate and # collect again. results = [] histogram.record(1) results.append(reader.get_metrics_data()) sleep(0.1) results.append(reader.get_metrics_data()) histogram.record(2) results.append(reader.get_metrics_data()) metric_data_0 = results[0].resource_metrics[0].scope_metrics[0].metrics[0].data.data_points[0] metric_data_2 = results[2].resource_metrics[0].scope_metrics[0].metrics[0].data.data_points[0] self.assertIsNone(results[1]) self.assertGreater(metric_data_2.start_time_unix_nano, metric_data_0.time_unix_nano) provider.shutdown() @mark.skipif( system() == "Windows", reason=( "Tests fail because Windows time_ns resolution is too low so " "two different time measurements may end up having the exact same" "value." ), ) def test_synchronous_cumulative_temporality(self): aggregation = ExponentialBucketHistogramAggregation() reader = InMemoryMetricReader( preferred_aggregation={Histogram: aggregation}, preferred_temporality={Histogram: AggregationTemporality.CUMULATIVE}, ) provider = MeterProvider(metric_readers=[reader]) # Disable SDK metrics # pylint: disable=protected-access reader._set_meter_provider(NoOpMeterProvider()) meter = provider.get_meter("name", "version") histogram = meter.create_histogram("histogram") results = [] for _ in range(10): results.append(reader.get_metrics_data()) for metrics_data in results: self.assertIsNone(metrics_data) results = [] for test_value in self.test_values: histogram.record(test_value) results.append(reader.get_metrics_data()) metric_data = results[0].resource_metrics[0].scope_metrics[0].metrics[0].data.data_points[0] start_time_unix_nano = metric_data.start_time_unix_nano self.assertLess( metric_data.start_time_unix_nano, metric_data.time_unix_nano, ) self.assertEqual(metric_data.min, self.test_values[0]) self.assertEqual(metric_data.max, self.test_values[0]) self.assertEqual(metric_data.sum, self.test_values[0]) previous_time_unix_nano = metric_data.time_unix_nano for index, metrics_data in enumerate(results[1:]): metric_data = metrics_data.resource_metrics[0].scope_metrics[0].metrics[0].data.data_points[0] self.assertEqual(start_time_unix_nano, metric_data.start_time_unix_nano) self.assertLess( metric_data.start_time_unix_nano, metric_data.time_unix_nano, ) self.assertEqual(metric_data.min, min(self.test_values[: index + 2])) self.assertEqual(metric_data.max, max(self.test_values[: index + 2])) self.assertAlmostEqual(metric_data.sum, sum(self.test_values[: index + 2])) self.assertGreater(metric_data.time_unix_nano, previous_time_unix_nano) previous_time_unix_nano = metric_data.time_unix_nano self.assertEqual( metric_data.positive.bucket_counts, [ 1, *[0] * 17, 1, *[0] * 36, 1, *[0] * 15, 2, *[0] * 15, 1, *[0] * 15, 1, *[0] * 15, 1, *[0] * 40, ], ) self.assertEqual(metric_data.negative.bucket_counts, [0]) results = [] for _ in range(10): results.append(reader.get_metrics_data()) provider.shutdown() metric_data = results[0].resource_metrics[0].scope_metrics[0].metrics[0].data.data_points[0] start_time_unix_nano = metric_data.start_time_unix_nano self.assertLess( metric_data.start_time_unix_nano, metric_data.time_unix_nano, ) self.assertEqual(metric_data.min, min(self.test_values)) self.assertEqual(metric_data.max, max(self.test_values)) self.assertAlmostEqual(metric_data.sum, sum(self.test_values)) previous_metric_data = metric_data for index, metrics_data in enumerate(results[1:]): metric_data = metrics_data.resource_metrics[0].scope_metrics[0].metrics[0].data.data_points[0] self.assertEqual( previous_metric_data.start_time_unix_nano, metric_data.start_time_unix_nano, ) self.assertEqual(previous_metric_data.min, metric_data.min) self.assertEqual(previous_metric_data.max, metric_data.max) self.assertAlmostEqual(previous_metric_data.sum, metric_data.sum) self.assertEqual( metric_data.positive.bucket_counts, [ 1, *[0] * 17, 1, *[0] * 36, 1, *[0] * 15, 2, *[0] * 15, 1, *[0] * 15, 1, *[0] * 15, 1, *[0] * 40, ], ) self.assertEqual(metric_data.negative.bucket_counts, [0]) self.assertLess( previous_metric_data.time_unix_nano, metric_data.time_unix_nano, ) test_exporter_concurrency.py000066400000000000000000000065671527070070700355220ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-sdk/tests/metrics/integration_test# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import time from threading import Lock from opentelemetry.metrics import CallbackOptions, Observation from opentelemetry.sdk.metrics import MeterProvider from opentelemetry.sdk.metrics.export import ( MetricExporter, MetricExportResult, MetricsData, PeriodicExportingMetricReader, ) from opentelemetry.test.concurrency_test import ConcurrencyTestBase class MaxCountExporter(MetricExporter): def __init__(self) -> None: super().__init__(None, None) self._lock = Lock() # the number of threads inside of export() self.count_in_export = 0 # the total count of calls to export() self.export_count = 0 # the maximum number of threads in export() ever self.max_count_in_export = 0 def export( self, metrics_data: MetricsData, timeout_millis: float = 10_000, **kwargs, ) -> MetricExportResult: with self._lock: self.export_count += 1 self.count_in_export += 1 # yield to other threads time.sleep(0) with self._lock: self.max_count_in_export = max(self.max_count_in_export, self.count_in_export) self.count_in_export -= 1 def force_flush(self, timeout_millis: float = 10_000) -> bool: return True def shutdown(self, timeout_millis: float = 30_000, **kwargs) -> None: pass class TestExporterConcurrency(ConcurrencyTestBase): """ Tests the requirement that: > `Export` will never be called concurrently for the same exporter instance. `Export` can > be called again only after the current call returns. https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk.md#exportbatch This test also tests that a thread that calls the a ``MetricReader.collect`` method using an asynchronous instrument is able to perform two actions in the same thread lock space (without it being interrupted by another thread): 1. Consume the measurement produced by the callback associated to the asynchronous instrument. 2. Export the measurement mentioned in the step above. """ def test_exporter_not_called_concurrently(self): exporter = MaxCountExporter() reader = PeriodicExportingMetricReader( exporter=exporter, export_interval_millis=100_000, ) meter_provider = MeterProvider(metric_readers=[reader]) counter_cb_counter = 0 def counter_cb(options: CallbackOptions): nonlocal counter_cb_counter counter_cb_counter += 1 yield Observation(2) meter_provider.get_meter(__name__).create_observable_counter("testcounter", callbacks=[counter_cb]) # call collect from a bunch of threads to try and enter export() concurrently def test_many_threads(): reader.collect() self.run_with_many_threads(test_many_threads, num_threads=100) self.assertEqual(counter_cb_counter, 100) # no thread should be in export() now self.assertEqual(exporter.count_in_export, 0) # should be one call for each thread self.assertEqual(exporter.export_count, 100) # should never have been more than one concurrent call self.assertEqual(exporter.max_count_in_export, 1) test_histogram_advisory_explicit_buckets.py000066400000000000000000000205631527070070700405660ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-sdk/tests/metrics/integration_test# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from unittest import TestCase from opentelemetry.sdk.metrics import MeterProvider from opentelemetry.sdk.metrics._internal.aggregation import ( _DEFAULT_EXPLICIT_BUCKET_HISTOGRAM_AGGREGATION_BOUNDARIES, ) from opentelemetry.sdk.metrics._internal.instrument import Histogram from opentelemetry.sdk.metrics.export import InMemoryMetricReader from opentelemetry.sdk.metrics.view import ( ExplicitBucketHistogramAggregation, View, ) class TestHistogramAdvisory(TestCase): def test_default(self): reader = InMemoryMetricReader() meter_provider = MeterProvider( metric_readers=[reader], ) meter = meter_provider.get_meter("testmeter") histogram = meter.create_histogram( "testhistogram", explicit_bucket_boundaries_advisory=[1.0, 2.0, 3.0], ) histogram.record(1, {"label": "value"}) histogram.record(2, {"label": "value"}) histogram.record(3, {"label": "value"}) metrics = reader.get_metrics_data() self.assertEqual(len(metrics.resource_metrics), 1) self.assertEqual(len(metrics.resource_metrics[0].scope_metrics), 1) self.assertEqual(len(metrics.resource_metrics[0].scope_metrics[0].metrics), 1) metric = metrics.resource_metrics[0].scope_metrics[0].metrics[0] self.assertEqual(metric.name, "testhistogram") self.assertEqual(metric.data.data_points[0].explicit_bounds, (1.0, 2.0, 3.0)) def test_empty_buckets(self): reader = InMemoryMetricReader() meter_provider = MeterProvider( metric_readers=[reader], ) meter = meter_provider.get_meter("testmeter") histogram = meter.create_histogram( "testhistogram", explicit_bucket_boundaries_advisory=[], ) histogram.record(1, {"label": "value"}) histogram.record(2, {"label": "value"}) histogram.record(3, {"label": "value"}) metrics = reader.get_metrics_data() self.assertEqual(len(metrics.resource_metrics), 1) self.assertEqual(len(metrics.resource_metrics[0].scope_metrics), 1) self.assertEqual(len(metrics.resource_metrics[0].scope_metrics[0].metrics), 1) metric = metrics.resource_metrics[0].scope_metrics[0].metrics[0] self.assertEqual(metric.name, "testhistogram") self.assertEqual(metric.data.data_points[0].explicit_bounds, ()) def test_view_default_aggregation(self): reader = InMemoryMetricReader() view = View(instrument_name="testhistogram") meter_provider = MeterProvider( metric_readers=[reader], views=[view], ) meter = meter_provider.get_meter("testmeter") histogram = meter.create_histogram( "testhistogram", explicit_bucket_boundaries_advisory=[1.0, 2.0, 3.0], ) histogram.record(1, {"label": "value"}) histogram.record(2, {"label": "value"}) histogram.record(3, {"label": "value"}) metrics = reader.get_metrics_data() self.assertEqual(len(metrics.resource_metrics), 1) self.assertEqual(len(metrics.resource_metrics[0].scope_metrics), 1) self.assertEqual(len(metrics.resource_metrics[0].scope_metrics[0].metrics), 1) metric = metrics.resource_metrics[0].scope_metrics[0].metrics[0] self.assertEqual(metric.name, "testhistogram") self.assertEqual(metric.data.data_points[0].explicit_bounds, (1.0, 2.0, 3.0)) def test_view_overrides_buckets(self): reader = InMemoryMetricReader() view = View( instrument_name="testhistogram", aggregation=ExplicitBucketHistogramAggregation(boundaries=[10.0, 100.0, 1000.0]), ) meter_provider = MeterProvider( metric_readers=[reader], views=[view], ) meter = meter_provider.get_meter("testmeter") histogram = meter.create_histogram( "testhistogram", explicit_bucket_boundaries_advisory=[1.0, 2.0, 3.0], ) histogram.record(1, {"label": "value"}) histogram.record(2, {"label": "value"}) histogram.record(3, {"label": "value"}) metrics = reader.get_metrics_data() self.assertEqual(len(metrics.resource_metrics), 1) self.assertEqual(len(metrics.resource_metrics[0].scope_metrics), 1) self.assertEqual(len(metrics.resource_metrics[0].scope_metrics[0].metrics), 1) metric = metrics.resource_metrics[0].scope_metrics[0].metrics[0] self.assertEqual(metric.name, "testhistogram") self.assertEqual(metric.data.data_points[0].explicit_bounds, (10.0, 100.0, 1000.0)) def test_explicit_aggregation(self): reader = InMemoryMetricReader(preferred_aggregation={Histogram: ExplicitBucketHistogramAggregation()}) meter_provider = MeterProvider( metric_readers=[reader], ) meter = meter_provider.get_meter("testmeter") histogram = meter.create_histogram( "testhistogram", explicit_bucket_boundaries_advisory=[1.0, 2.0, 3.0], ) histogram.record(1, {"label": "value"}) histogram.record(2, {"label": "value"}) histogram.record(3, {"label": "value"}) metrics = reader.get_metrics_data() self.assertEqual(len(metrics.resource_metrics), 1) self.assertEqual(len(metrics.resource_metrics[0].scope_metrics), 1) self.assertEqual(len(metrics.resource_metrics[0].scope_metrics[0].metrics), 1) metric = metrics.resource_metrics[0].scope_metrics[0].metrics[0] self.assertEqual(metric.name, "testhistogram") self.assertEqual(metric.data.data_points[0].explicit_bounds, (1.0, 2.0, 3.0)) def test_explicit_aggregation_multiple_histograms(self): reader = InMemoryMetricReader(preferred_aggregation={Histogram: ExplicitBucketHistogramAggregation()}) meter_provider = MeterProvider( metric_readers=[reader], ) meter = meter_provider.get_meter("testmeter") histogram1 = meter.create_histogram( "testhistogram1", explicit_bucket_boundaries_advisory=[1.0, 2.0, 3.0], ) histogram1.record(1, {"label": "value"}) histogram1.record(2, {"label": "value"}) histogram1.record(3, {"label": "value"}) histogram2 = meter.create_histogram( "testhistogram2", explicit_bucket_boundaries_advisory=[4.0, 5.0, 6.0], ) histogram2.record(4, {"label": "value"}) histogram2.record(5, {"label": "value"}) histogram2.record(6, {"label": "value"}) metrics = reader.get_metrics_data() self.assertEqual(len(metrics.resource_metrics), 1) self.assertEqual(len(metrics.resource_metrics[0].scope_metrics), 1) self.assertEqual(len(metrics.resource_metrics[0].scope_metrics[0].metrics), 2) metric1 = metrics.resource_metrics[0].scope_metrics[0].metrics[0] self.assertEqual(metric1.name, "testhistogram1") self.assertEqual(metric1.data.data_points[0].explicit_bounds, (1.0, 2.0, 3.0)) metric2 = metrics.resource_metrics[0].scope_metrics[0].metrics[1] self.assertEqual(metric2.name, "testhistogram2") self.assertEqual(metric2.data.data_points[0].explicit_bounds, (4.0, 5.0, 6.0)) def test_explicit_aggregation_default_boundaries(self): reader = InMemoryMetricReader(preferred_aggregation={Histogram: ExplicitBucketHistogramAggregation()}) meter_provider = MeterProvider( metric_readers=[reader], ) meter = meter_provider.get_meter("testmeter") histogram = meter.create_histogram( "testhistogram", ) histogram.record(1, {"label": "value"}) histogram.record(2, {"label": "value"}) histogram.record(3, {"label": "value"}) metrics = reader.get_metrics_data() self.assertEqual(len(metrics.resource_metrics), 1) self.assertEqual(len(metrics.resource_metrics[0].scope_metrics), 1) self.assertEqual(len(metrics.resource_metrics[0].scope_metrics[0].metrics), 1) metric = metrics.resource_metrics[0].scope_metrics[0].metrics[0] self.assertEqual(metric.name, "testhistogram") self.assertEqual( metric.data.data_points[0].explicit_bounds, _DEFAULT_EXPLICIT_BUCKET_HISTOGRAM_AGGREGATION_BOUNDARIES, ) test_histogram_export.py000066400000000000000000000114631527070070700346250ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-sdk/tests/metrics/integration_test# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from unittest import TestCase from opentelemetry.sdk.metrics import MeterProvider from opentelemetry.sdk.metrics._internal.exemplar import ( AlwaysOffExemplarFilter, AlwaysOnExemplarFilter, ) from opentelemetry.sdk.metrics.export import InMemoryMetricReader from opentelemetry.sdk.resources import SERVICE_NAME, Resource class TestHistogramExport(TestCase): def test_histogram_counter_collection(self): in_memory_metric_reader = InMemoryMetricReader() provider = MeterProvider( resource=Resource.create({SERVICE_NAME: "otel-test"}), metric_readers=[in_memory_metric_reader], ) meter = provider.get_meter("my-meter") histogram = meter.create_histogram("my_histogram") counter = meter.create_counter("my_counter") histogram.record(5, {"attribute": "value"}) counter.add(1, {"attribute": "value_counter"}) metric_data = in_memory_metric_reader.get_metrics_data() self.assertEqual(len(metric_data.resource_metrics[0].scope_metrics[0].metrics), 2) self.assertEqual( (metric_data.resource_metrics[0].scope_metrics[0].metrics[0].data.data_points[0].bucket_counts), (0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), ) self.assertEqual( (metric_data.resource_metrics[0].scope_metrics[0].metrics[1].data.data_points[0].value), 1, ) metric_data = in_memory_metric_reader.get_metrics_data() self.assertEqual(len(metric_data.resource_metrics[0].scope_metrics[0].metrics), 2) self.assertEqual( (metric_data.resource_metrics[0].scope_metrics[0].metrics[0].data.data_points[0].bucket_counts), (0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), ) self.assertEqual( (metric_data.resource_metrics[0].scope_metrics[0].metrics[1].data.data_points[0].value), 1, ) def test_histogram_with_exemplars(self): in_memory_metric_reader = InMemoryMetricReader() provider = MeterProvider( resource=Resource.create({SERVICE_NAME: "otel-test"}), metric_readers=[in_memory_metric_reader], exemplar_filter=AlwaysOnExemplarFilter(), ) meter = provider.get_meter("my-meter") histogram = meter.create_histogram("my_histogram") histogram.record(2, {"attribute": "value1"}) # Should go in the first bucket histogram.record(7, {"attribute": "value2"}) # Should go in the second bucket histogram.record(9, {"attribute": "value2"}) # Should also go in the second bucket histogram.record(15, {"attribute": "value3"}) # Should go in the third bucket metric_data = in_memory_metric_reader.get_metrics_data() self.assertEqual(len(metric_data.resource_metrics[0].scope_metrics[0].metrics), 1) histogram_metric = metric_data.resource_metrics[0].scope_metrics[0].metrics[0] self.assertEqual(len(histogram_metric.data.data_points), 3) self.assertEqual(len(histogram_metric.data.data_points[0].exemplars), 1) self.assertEqual(len(histogram_metric.data.data_points[1].exemplars), 1) self.assertEqual(len(histogram_metric.data.data_points[2].exemplars), 1) self.assertEqual(histogram_metric.data.data_points[0].sum, 2) self.assertEqual(histogram_metric.data.data_points[1].sum, 16) self.assertEqual(histogram_metric.data.data_points[2].sum, 15) self.assertEqual(histogram_metric.data.data_points[0].exemplars[0].value, 2.0) self.assertEqual(histogram_metric.data.data_points[1].exemplars[0].value, 9.0) self.assertEqual(histogram_metric.data.data_points[2].exemplars[0].value, 15.0) def test_filter_with_exemplars(self): in_memory_metric_reader = InMemoryMetricReader() provider = MeterProvider( resource=Resource.create({SERVICE_NAME: "otel-test"}), metric_readers=[in_memory_metric_reader], exemplar_filter=AlwaysOffExemplarFilter(), ) meter = provider.get_meter("my-meter") histogram = meter.create_histogram("my_histogram") histogram.record(2, {"attribute": "value1"}) # Should go in the first bucket histogram.record(7, {"attribute": "value2"}) # Should go in the second bucket metric_data = in_memory_metric_reader.get_metrics_data() self.assertEqual(len(metric_data.resource_metrics[0].scope_metrics[0].metrics), 1) histogram_metric = metric_data.resource_metrics[0].scope_metrics[0].metrics[0] self.assertEqual(len(histogram_metric.data.data_points), 2) self.assertEqual(len(histogram_metric.data.data_points[0].exemplars), 0) self.assertEqual(len(histogram_metric.data.data_points[1].exemplars), 0) test_nonfinite_measurements.py000066400000000000000000000053031527070070700360040ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-sdk/tests/metrics/integration_test# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from unittest import TestCase from opentelemetry.sdk.metrics import MeterProvider from opentelemetry.sdk.metrics.export import InMemoryMetricReader from opentelemetry.sdk.resources import SERVICE_NAME, Resource class TestNonFiniteMeasurements(TestCase): """Verify non-finite values (NaN, Inf) are dropped before aggregation and do not corrupt exported metric data.""" def setUp(self): self.reader = InMemoryMetricReader() self.provider = MeterProvider( resource=Resource.create({SERVICE_NAME: "otel-test"}), metric_readers=[self.reader], ) self.meter = self.provider.get_meter("test-meter") def tearDown(self): self.provider.shutdown() def _get_data_point(self, metric_name): metrics = self.reader.get_metrics_data() for rm in metrics.resource_metrics: for sm in rm.scope_metrics: for metric in sm.metrics: if metric.name == metric_name: return metric.data.data_points[0] return None def test_counter_nan_does_not_corrupt_sum(self): counter = self.meter.create_counter("c_nan") counter.add(5) counter.add(float("nan")) counter.add(3) dp = self._get_data_point("c_nan") self.assertIsNotNone(dp) self.assertEqual(dp.value, 8) def test_counter_inf_does_not_corrupt_sum(self): counter = self.meter.create_counter("c_inf") counter.add(5) counter.add(float("inf")) counter.add(3) dp = self._get_data_point("c_inf") self.assertIsNotNone(dp) self.assertEqual(dp.value, 8) def test_histogram_nan_does_not_corrupt_count_or_sum(self): hist = self.meter.create_histogram("h_nan") hist.record(10) hist.record(float("nan")) hist.record(20) dp = self._get_data_point("h_nan") self.assertIsNotNone(dp) self.assertEqual(dp.count, 2) self.assertEqual(dp.sum, 30) def test_histogram_inf_does_not_corrupt_count_or_sum(self): hist = self.meter.create_histogram("h_inf") hist.record(10) hist.record(float("inf")) hist.record(20) dp = self._get_data_point("h_inf") self.assertIsNotNone(dp) self.assertEqual(dp.count, 2) self.assertEqual(dp.sum, 30) def test_updown_counter_nan_does_not_corrupt_sum(self): udc = self.meter.create_up_down_counter("udc_nan") udc.add(10) udc.add(float("nan")) udc.add(-3) dp = self._get_data_point("udc_nan") self.assertIsNotNone(dp) self.assertEqual(dp.value, 7) test_provider_shutdown.py000066400000000000000000000041321527070070700350070ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-sdk/tests/metrics/integration_test# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import gc import time import weakref from collections.abc import Sequence from unittest import TestCase from opentelemetry.sdk.metrics import MeterProvider from opentelemetry.sdk.metrics.export import ( Metric, MetricExporter, MetricExportResult, PeriodicExportingMetricReader, ) class FakeMetricsExporter(MetricExporter): def __init__(self, wait=0, preferred_temporality=None, preferred_aggregation=None): self.wait = wait self.metrics = [] self._shutdown = False super().__init__( preferred_temporality=preferred_temporality, preferred_aggregation=preferred_aggregation, ) def export( self, metrics_data: Sequence[Metric], timeout_millis: float = 10_000, **kwargs, ) -> MetricExportResult: time.sleep(self.wait) self.metrics.extend(metrics_data) return True def shutdown(self, timeout_millis: float = 30_000, **kwargs) -> None: self._shutdown = True def force_flush(self, timeout_millis: float = 10_000) -> bool: return True class TestMeterProviderShutdown(TestCase): def test_meter_provider_shutdown_cleans_up_successfully(self): def create_and_shutdown(): exporter = FakeMetricsExporter() exporter_wr = weakref.ref(exporter) reader = PeriodicExportingMetricReader(exporter) reader_wr = weakref.ref(reader) provider = MeterProvider(metric_readers=[reader]) provider_wr = weakref.ref(provider) provider.shutdown() return exporter_wr, reader_wr, provider_wr # When: the provider is shutdown ( exporter_weakref, reader_weakref, provider_weakref, ) = create_and_shutdown() gc.collect() # Then: the provider, exporter and reader should be garbage collected self.assertIsNone(exporter_weakref()) self.assertIsNone(reader_weakref()) self.assertIsNone(provider_weakref()) python-opentelemetry-1.45.2/opentelemetry-sdk/tests/metrics/integration_test/test_sum_aggregation.py000066400000000000000000000300371527070070700344570ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from itertools import count from logging import ERROR from platform import system from time import sleep from unittest import TestCase from pytest import mark from opentelemetry.context import Context from opentelemetry.metrics import NoOpMeterProvider, Observation from opentelemetry.sdk.metrics import Counter, MeterProvider, ObservableCounter from opentelemetry.sdk.metrics._internal.exemplar import AlwaysOnExemplarFilter from opentelemetry.sdk.metrics.export import ( AggregationTemporality, InMemoryMetricReader, ) from opentelemetry.sdk.metrics.view import SumAggregation class TestSumAggregation(TestCase): @mark.skipif( system() == "Windows", reason=( "Tests fail because Windows time_ns resolution is too low so " "two different time measurements may end up having the exact same" "value." ), ) def test_asynchronous_delta_temporality(self): eight_multiple_generator = count(start=8, step=8) counter = 0 def observable_counter_callback(callback_options): nonlocal counter counter += 1 if counter < 11: yield elif counter < 21: yield Observation(next(eight_multiple_generator)) else: yield aggregation = SumAggregation() reader = InMemoryMetricReader( preferred_aggregation={ObservableCounter: aggregation}, preferred_temporality={ObservableCounter: AggregationTemporality.DELTA}, ) provider = MeterProvider(metric_readers=[reader]) # Disable SDK metrics # pylint: disable=protected-access reader._set_meter_provider(NoOpMeterProvider()) meter = provider.get_meter("name", "version") meter.create_observable_counter("observable_counter", [observable_counter_callback]) results = [] for _ in range(10): with self.assertLogs(level=ERROR): results.append(reader.get_metrics_data()) self.assertEqual(counter, 10) for metrics_data in results: self.assertIsNone(metrics_data) results = [] for _ in range(10): results.append(reader.get_metrics_data()) self.assertEqual(counter, 20) previous_time_unix_nano = ( results[0].resource_metrics[0].scope_metrics[0].metrics[0].data.data_points[0].time_unix_nano ) self.assertEqual( (results[0].resource_metrics[0].scope_metrics[0].metrics[0].data.data_points[0].value), 8, ) self.assertLess( (results[0].resource_metrics[0].scope_metrics[0].metrics[0].data.data_points[0].start_time_unix_nano), previous_time_unix_nano, ) for metrics_data in results[1:]: metric_data = metrics_data.resource_metrics[0].scope_metrics[0].metrics[0].data.data_points[0] self.assertEqual(previous_time_unix_nano, metric_data.start_time_unix_nano) previous_time_unix_nano = metric_data.time_unix_nano self.assertEqual(metric_data.value, 8) self.assertLess(metric_data.start_time_unix_nano, metric_data.time_unix_nano) results = [] for _ in range(10): with self.assertLogs(level=ERROR): results.append(reader.get_metrics_data()) self.assertEqual(counter, 30) provider.shutdown() for metrics_data in results: self.assertIsNone(metrics_data) @mark.skipif( system() == "Windows", reason=( "Tests fail because Windows time_ns resolution is too low so " "two different time measurements may end up having the exact same" "value." ), ) def test_asynchronous_cumulative_temporality(self): eight_multiple_generator = count(start=8, step=8) counter = 0 def observable_counter_callback(callback_options): nonlocal counter counter += 1 if counter < 11: yield elif counter < 21: yield Observation(next(eight_multiple_generator)) else: yield aggregation = SumAggregation() reader = InMemoryMetricReader( preferred_aggregation={ObservableCounter: aggregation}, preferred_temporality={ObservableCounter: AggregationTemporality.CUMULATIVE}, ) provider = MeterProvider(metric_readers=[reader]) # Disable SDK metrics # pylint: disable=protected-access reader._set_meter_provider(NoOpMeterProvider()) meter = provider.get_meter("name", "version") meter.create_observable_counter("observable_counter", [observable_counter_callback]) results = [] for _ in range(10): with self.assertLogs(level=ERROR): results.append(reader.get_metrics_data()) self.assertEqual(counter, 10) for metrics_data in results: self.assertIsNone(metrics_data) results = [] for _ in range(10): results.append(reader.get_metrics_data()) self.assertEqual(counter, 20) start_time_unix_nano = ( results[0].resource_metrics[0].scope_metrics[0].metrics[0].data.data_points[0].start_time_unix_nano ) for index, metrics_data in enumerate(results): metric_data = metrics_data.resource_metrics[0].scope_metrics[0].metrics[0].data.data_points[0] self.assertEqual(start_time_unix_nano, metric_data.start_time_unix_nano) self.assertEqual(metric_data.value, 8 * (index + 1)) results = [] for _ in range(10): with self.assertLogs(level=ERROR): results.append(reader.get_metrics_data()) self.assertEqual(counter, 30) provider.shutdown() for metrics_data in results: self.assertIsNone(metrics_data) @mark.skipif( system() == "Windows", reason=( "Tests fail because Windows time_ns resolution is too low so " "two different time measurements may end up having the exact same" "value." ), ) def test_synchronous_delta_temporality(self): aggregation = SumAggregation() reader = InMemoryMetricReader( preferred_aggregation={Counter: aggregation}, preferred_temporality={Counter: AggregationTemporality.DELTA}, ) provider = MeterProvider(metric_readers=[reader]) # Disable SDK metrics # pylint: disable=protected-access reader._set_meter_provider(NoOpMeterProvider()) meter = provider.get_meter("name", "version") counter = meter.create_counter("counter") results = [] for _ in range(10): results.append(reader.get_metrics_data()) for metrics_data in results: self.assertIsNone(metrics_data) results = [] for _ in range(10): counter.add(8) results.append(reader.get_metrics_data()) previous_time_unix_nano = ( results[0].resource_metrics[0].scope_metrics[0].metrics[0].data.data_points[0].time_unix_nano ) self.assertEqual( (results[0].resource_metrics[0].scope_metrics[0].metrics[0].data.data_points[0].value), 8, ) self.assertLess( (results[0].resource_metrics[0].scope_metrics[0].metrics[0].data.data_points[0].start_time_unix_nano), previous_time_unix_nano, ) for metrics_data in results[1:]: metric_data = metrics_data.resource_metrics[0].scope_metrics[0].metrics[0].data.data_points[0] self.assertEqual(previous_time_unix_nano, metric_data.start_time_unix_nano) previous_time_unix_nano = metric_data.time_unix_nano self.assertEqual(metric_data.value, 8) self.assertLess(metric_data.start_time_unix_nano, metric_data.time_unix_nano) results = [] for _ in range(10): results.append(reader.get_metrics_data()) for metrics_data in results: self.assertIsNone(metrics_data) results = [] counter.add(1) results.append(reader.get_metrics_data()) sleep(0.1) results.append(reader.get_metrics_data()) counter.add(2) results.append(reader.get_metrics_data()) metric_data_0 = results[0].resource_metrics[0].scope_metrics[0].metrics[0].data.data_points[0] metric_data_2 = results[2].resource_metrics[0].scope_metrics[0].metrics[0].data.data_points[0] self.assertIsNone(results[1]) self.assertGreater(metric_data_2.start_time_unix_nano, metric_data_0.time_unix_nano) provider.shutdown() @mark.skipif( system() == "Windows", reason=( "Tests fail because Windows time_ns resolution is too low so " "two different time measurements may end up having the exact same" "value." ), ) def test_synchronous_cumulative_temporality(self): aggregation = SumAggregation() reader = InMemoryMetricReader( preferred_aggregation={Counter: aggregation}, preferred_temporality={Counter: AggregationTemporality.CUMULATIVE}, ) provider = MeterProvider(metric_readers=[reader]) # Disable SDK metrics # pylint: disable=protected-access reader._set_meter_provider(NoOpMeterProvider()) meter = provider.get_meter("name", "version") counter = meter.create_counter("counter") results = [] for _ in range(10): results.append(reader.get_metrics_data()) for metrics_data in results: self.assertIsNone(metrics_data) results = [] for _ in range(10): counter.add(8) results.append(reader.get_metrics_data()) start_time_unix_nano = ( results[0].resource_metrics[0].scope_metrics[0].metrics[0].data.data_points[0].start_time_unix_nano ) for index, metrics_data in enumerate(results): metric_data = metrics_data.resource_metrics[0].scope_metrics[0].metrics[0].data.data_points[0] self.assertEqual(start_time_unix_nano, metric_data.start_time_unix_nano) self.assertEqual(metric_data.value, 8 * (index + 1)) results = [] for _ in range(10): results.append(reader.get_metrics_data()) provider.shutdown() start_time_unix_nano = ( results[0].resource_metrics[0].scope_metrics[0].metrics[0].data.data_points[0].start_time_unix_nano ) for metrics_data in results: metric_data = metrics_data.resource_metrics[0].scope_metrics[0].metrics[0].data.data_points[0] self.assertEqual(start_time_unix_nano, metric_data.start_time_unix_nano) self.assertEqual(metric_data.value, 80) def test_sum_aggregation_with_exemplars(self): in_memory_metric_reader = InMemoryMetricReader() provider = MeterProvider( metric_readers=[in_memory_metric_reader], exemplar_filter=AlwaysOnExemplarFilter(), ) meter = provider.get_meter("my-meter") counter = meter.create_counter("my_counter") counter.add(2, {"attribute": "value1"}, context=Context()) counter.add(5, {"attribute": "value2"}, context=Context()) counter.add(3, {"attribute": "value3"}, context=Context()) metric_data = in_memory_metric_reader.get_metrics_data() self.assertEqual(len(metric_data.resource_metrics[0].scope_metrics[0].metrics), 1) sum_metric = metric_data.resource_metrics[0].scope_metrics[0].metrics[0] data_points = sum_metric.data.data_points self.assertEqual(len(data_points), 3) self.assertEqual(data_points[0].exemplars[0].value, 2.0) self.assertEqual(data_points[1].exemplars[0].value, 5.0) self.assertEqual(data_points[2].exemplars[0].value, 3.0) provider.shutdown() python-opentelemetry-1.45.2/opentelemetry-sdk/tests/metrics/integration_test/test_time_align.py000066400000000000000000000210121527070070700334050ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from platform import system from time import sleep from unittest import TestCase from pytest import mark from opentelemetry.sdk.metrics import Counter, MeterProvider from opentelemetry.sdk.metrics.export import ( AggregationTemporality, InMemoryMetricReader, ) class TestTimeAlign(TestCase): # This delay is needed for these tests to pass when they are run in # Windows. delay = 0.001 def test_time_align_cumulative(self): reader = InMemoryMetricReader() meter_provider = MeterProvider(metric_readers=[reader]) meter = meter_provider.get_meter("testmeter") counter_0 = meter.create_counter("counter_0") counter_1 = meter.create_counter("counter_1") counter_0.add(10, {"label": "value1"}) sleep(self.delay) counter_0.add(10, {"label": "value2"}) sleep(self.delay) counter_1.add(10, {"label": "value1"}) sleep(self.delay) counter_1.add(10, {"label": "value2"}) metrics = reader.get_metrics_data() data_points_0_0 = list(metrics.resource_metrics[0].scope_metrics[0].metrics[0].data.data_points) data_points_0_1 = list(metrics.resource_metrics[0].scope_metrics[0].metrics[1].data.data_points) self.assertEqual(len(data_points_0_0), 2) self.assertEqual(len(data_points_0_1), 2) self.assertLess( data_points_0_0[0].start_time_unix_nano, data_points_0_0[1].start_time_unix_nano, ) self.assertLess( data_points_0_1[0].start_time_unix_nano, data_points_0_1[1].start_time_unix_nano, ) self.assertNotEqual( data_points_0_0[1].start_time_unix_nano, data_points_0_1[0].start_time_unix_nano, ) self.assertEqual( data_points_0_0[0].time_unix_nano, data_points_0_0[1].time_unix_nano, ) self.assertEqual( data_points_0_1[0].time_unix_nano, data_points_0_1[1].time_unix_nano, ) self.assertEqual( data_points_0_0[1].time_unix_nano, data_points_0_1[0].time_unix_nano, ) counter_0.add(10, {"label": "value1"}) sleep(self.delay) counter_0.add(10, {"label": "value2"}) sleep(self.delay) counter_1.add(10, {"label": "value1"}) sleep(self.delay) counter_1.add(10, {"label": "value2"}) metrics = reader.get_metrics_data() data_points_1_0 = list(metrics.resource_metrics[0].scope_metrics[0].metrics[0].data.data_points) data_points_1_1 = list(metrics.resource_metrics[0].scope_metrics[0].metrics[1].data.data_points) self.assertEqual(len(data_points_1_0), 2) self.assertEqual(len(data_points_1_1), 2) self.assertLess( data_points_1_0[0].start_time_unix_nano, data_points_1_0[1].start_time_unix_nano, ) self.assertLess( data_points_1_1[0].start_time_unix_nano, data_points_1_1[1].start_time_unix_nano, ) self.assertNotEqual( data_points_1_0[1].start_time_unix_nano, data_points_1_1[0].start_time_unix_nano, ) self.assertEqual( data_points_1_0[0].time_unix_nano, data_points_1_0[1].time_unix_nano, ) self.assertEqual( data_points_1_1[0].time_unix_nano, data_points_1_1[1].time_unix_nano, ) self.assertEqual( data_points_1_0[1].time_unix_nano, data_points_1_1[0].time_unix_nano, ) self.assertEqual( data_points_0_0[0].start_time_unix_nano, data_points_1_0[0].start_time_unix_nano, ) self.assertEqual( data_points_0_0[1].start_time_unix_nano, data_points_1_0[1].start_time_unix_nano, ) self.assertEqual( data_points_0_1[0].start_time_unix_nano, data_points_1_1[0].start_time_unix_nano, ) self.assertEqual( data_points_0_1[1].start_time_unix_nano, data_points_1_1[1].start_time_unix_nano, ) @mark.skipif(system() != "Linux", reason="test failing in CI when run in Windows") def test_time_align_delta(self): reader = InMemoryMetricReader(preferred_temporality={Counter: AggregationTemporality.DELTA}) meter_provider = MeterProvider(metric_readers=[reader]) meter = meter_provider.get_meter("testmeter") counter_0 = meter.create_counter("counter_0") counter_1 = meter.create_counter("counter_1") counter_0.add(10, {"label": "value1"}) sleep(self.delay) counter_0.add(10, {"label": "value2"}) sleep(self.delay) counter_1.add(10, {"label": "value1"}) sleep(self.delay) counter_1.add(10, {"label": "value2"}) metrics = reader.get_metrics_data() data_points_0_0 = list(metrics.resource_metrics[0].scope_metrics[0].metrics[0].data.data_points) data_points_0_1 = list(metrics.resource_metrics[0].scope_metrics[0].metrics[1].data.data_points) self.assertEqual(len(data_points_0_0), 2) self.assertEqual(len(data_points_0_1), 2) self.assertLess( data_points_0_0[0].start_time_unix_nano, data_points_0_0[1].start_time_unix_nano, ) self.assertLess( data_points_0_1[0].start_time_unix_nano, data_points_0_1[1].start_time_unix_nano, ) self.assertNotEqual( data_points_0_0[1].start_time_unix_nano, data_points_0_1[0].start_time_unix_nano, ) self.assertEqual( data_points_0_0[0].time_unix_nano, data_points_0_0[1].time_unix_nano, ) self.assertEqual( data_points_0_1[0].time_unix_nano, data_points_0_1[1].time_unix_nano, ) self.assertEqual( data_points_0_0[1].time_unix_nano, data_points_0_1[0].time_unix_nano, ) counter_0.add(10, {"label": "value1"}) sleep(self.delay) counter_0.add(10, {"label": "value2"}) sleep(self.delay) counter_1.add(10, {"label": "value1"}) sleep(self.delay) counter_1.add(10, {"label": "value2"}) metrics = reader.get_metrics_data() data_points_1_0 = list(metrics.resource_metrics[0].scope_metrics[0].metrics[0].data.data_points) data_points_1_1 = list(metrics.resource_metrics[0].scope_metrics[0].metrics[1].data.data_points) self.assertEqual(len(data_points_1_0), 2) self.assertEqual(len(data_points_1_1), 2) self.assertEqual( data_points_1_0[0].start_time_unix_nano, data_points_1_0[1].start_time_unix_nano, ) self.assertEqual( data_points_1_1[0].start_time_unix_nano, data_points_1_1[1].start_time_unix_nano, ) self.assertEqual( data_points_1_0[1].start_time_unix_nano, data_points_1_1[0].start_time_unix_nano, ) self.assertEqual( data_points_1_0[0].time_unix_nano, data_points_1_0[1].time_unix_nano, ) self.assertEqual( data_points_1_1[0].time_unix_nano, data_points_1_1[1].time_unix_nano, ) self.assertEqual( data_points_1_0[1].time_unix_nano, data_points_1_1[0].time_unix_nano, ) self.assertNotEqual( data_points_0_0[0].start_time_unix_nano, data_points_1_0[0].start_time_unix_nano, ) self.assertNotEqual( data_points_0_0[1].start_time_unix_nano, data_points_1_0[1].start_time_unix_nano, ) self.assertNotEqual( data_points_0_1[0].start_time_unix_nano, data_points_1_1[0].start_time_unix_nano, ) self.assertNotEqual( data_points_0_1[1].start_time_unix_nano, data_points_1_1[1].start_time_unix_nano, ) self.assertEqual( data_points_0_0[0].time_unix_nano, data_points_1_0[0].start_time_unix_nano, ) self.assertEqual( data_points_0_0[1].time_unix_nano, data_points_1_0[1].start_time_unix_nano, ) self.assertEqual( data_points_0_1[0].time_unix_nano, data_points_1_1[0].start_time_unix_nano, ) self.assertEqual( data_points_0_1[1].time_unix_nano, data_points_1_1[1].start_time_unix_nano, ) python-opentelemetry-1.45.2/opentelemetry-sdk/tests/metrics/scripts/000077500000000000000000000000001527070070700257755ustar00rootroot00000000000000meter_provider_resource_after_fork.py000066400000000000000000000045061527070070700354340ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-sdk/tests/metrics/scripts# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import json import os from opentelemetry.sdk.metrics import MeterProvider from opentelemetry.sdk.metrics.export import InMemoryMetricReader from opentelemetry.sdk.resources import PROCESS_PID def _resource_pids(metrics_data) -> list[int]: return [resource_metric.resource.attributes[PROCESS_PID] for resource_metric in metrics_data.resource_metrics] def _metric_names(metrics_data) -> list[str]: return sorted( metric.name for resource_metric in metrics_data.resource_metrics for scope_metric in resource_metric.scope_metrics for metric in scope_metric.metrics ) # pylint: disable-next=too-many-locals def main() -> None: reader = InMemoryMetricReader() meter_provider = MeterProvider(metric_readers=[reader], shutdown_on_exit=False) meter = meter_provider.get_meter("cached") counter = meter.create_counter("cached_counter") parent_pid = os.getpid() # pylint: disable-next=protected-access parent_resource_pid = meter_provider._sdk_config.resource.attributes[PROCESS_PID] pid = os.fork() if not pid: child_pid = os.getpid() new_meter = meter_provider.get_meter("new") new_counter = new_meter.create_counter("new_counter") counter.add(1) new_counter.add(1) metrics_data = reader.get_metrics_data() print( json.dumps( { "child_pid": child_pid, # pylint: disable-next=protected-access "provider_pid": meter_provider._sdk_config.resource.attributes[PROCESS_PID], "exported_resource_pids": _resource_pids(metrics_data), "metric_names": _metric_names(metrics_data), } ), flush=True, ) # pylint: disable-next=protected-access os._exit(0) os.waitpid(pid, 0) print( json.dumps( { "parent_pid": parent_pid, "parent_resource_pid": parent_resource_pid, # pylint: disable-next=protected-access "parent_resource_pid_after_fork": meter_provider._sdk_config.resource.attributes[PROCESS_PID], } ), flush=True, ) if __name__ == "__main__": main() python-opentelemetry-1.45.2/opentelemetry-sdk/tests/metrics/test_aggregation.py000066400000000000000000000675451527070070700302270ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # pylint: disable=protected-access from math import inf from time import sleep, time_ns from unittest import TestCase from unittest.mock import Mock from opentelemetry.context import Context from opentelemetry.sdk.metrics._internal.aggregation import ( _ExplicitBucketHistogramAggregation, _LastValueAggregation, _SumAggregation, ) from opentelemetry.sdk.metrics._internal.exemplar import ( AlignedHistogramBucketExemplarReservoir, SimpleFixedSizeExemplarReservoir, ) from opentelemetry.sdk.metrics._internal.instrument import ( _Counter, _Gauge, _Histogram, _ObservableCounter, _ObservableGauge, _ObservableUpDownCounter, _UpDownCounter, ) from opentelemetry.sdk.metrics._internal.measurement import Measurement from opentelemetry.sdk.metrics._internal.view import _default_reservoir_factory from opentelemetry.sdk.metrics.export import ( AggregationTemporality, NumberDataPoint, ) from opentelemetry.sdk.metrics.view import ( DefaultAggregation, ExplicitBucketHistogramAggregation, LastValueAggregation, SumAggregation, ) from opentelemetry.util.types import Attributes def measurement(value: float, attributes: Attributes = None) -> Measurement: return Measurement( value, time_ns(), instrument=Mock(), context=Context(), attributes=attributes, ) class TestSynchronousSumAggregation(TestCase): def test_aggregate_delta(self): """ `SynchronousSumAggregation` aggregates data for sum metric points """ synchronous_sum_aggregation = _SumAggregation( Mock(), True, AggregationTemporality.DELTA, 0, _default_reservoir_factory(_SumAggregation), ) synchronous_sum_aggregation.aggregate(measurement(1)) synchronous_sum_aggregation.aggregate(measurement(2)) synchronous_sum_aggregation.aggregate(measurement(3)) self.assertEqual(synchronous_sum_aggregation._value, 6) synchronous_sum_aggregation = _SumAggregation( Mock(), True, AggregationTemporality.DELTA, 0, _default_reservoir_factory(_SumAggregation), ) synchronous_sum_aggregation.aggregate(measurement(1)) synchronous_sum_aggregation.aggregate(measurement(-2)) synchronous_sum_aggregation.aggregate(measurement(3)) self.assertEqual(synchronous_sum_aggregation._value, 2) def test_aggregate_cumulative(self): """ `SynchronousSumAggregation` aggregates data for sum metric points """ synchronous_sum_aggregation = _SumAggregation( Mock(), True, AggregationTemporality.CUMULATIVE, 0, _default_reservoir_factory(_SumAggregation), ) synchronous_sum_aggregation.aggregate(measurement(1)) synchronous_sum_aggregation.aggregate(measurement(2)) synchronous_sum_aggregation.aggregate(measurement(3)) self.assertEqual(synchronous_sum_aggregation._value, 6) synchronous_sum_aggregation = _SumAggregation( Mock(), True, AggregationTemporality.CUMULATIVE, 0, _default_reservoir_factory(_SumAggregation), ) synchronous_sum_aggregation.aggregate(measurement(1)) synchronous_sum_aggregation.aggregate(measurement(-2)) synchronous_sum_aggregation.aggregate(measurement(3)) self.assertEqual(synchronous_sum_aggregation._value, 2) def test_collect_delta(self): """ `SynchronousSumAggregation` collects sum metric points """ synchronous_sum_aggregation = _SumAggregation( Mock(), True, AggregationTemporality.DELTA, 0, _default_reservoir_factory(_SumAggregation), ) synchronous_sum_aggregation.aggregate(measurement(1)) # 1 is used here directly to simulate the instant the first # collection process starts. first_sum = synchronous_sum_aggregation.collect(AggregationTemporality.CUMULATIVE, 1) self.assertEqual(first_sum.value, 1) synchronous_sum_aggregation.aggregate(measurement(1)) # 2 is used here directly to simulate the instant the first # collection process starts. second_sum = synchronous_sum_aggregation.collect(AggregationTemporality.CUMULATIVE, 2) self.assertEqual(second_sum.value, 2) self.assertEqual(second_sum.start_time_unix_nano, first_sum.start_time_unix_nano) synchronous_sum_aggregation = _SumAggregation( Mock(), True, AggregationTemporality.DELTA, 0, _default_reservoir_factory(_SumAggregation), ) synchronous_sum_aggregation.aggregate(measurement(1)) # 1 is used here directly to simulate the instant the first # collection process starts. first_sum = synchronous_sum_aggregation.collect(AggregationTemporality.DELTA, 1) self.assertEqual(first_sum.value, 1) synchronous_sum_aggregation.aggregate(measurement(1)) # 2 is used here directly to simulate the instant the first # collection process starts. second_sum = synchronous_sum_aggregation.collect(AggregationTemporality.DELTA, 2) self.assertEqual(second_sum.value, 1) self.assertGreater(second_sum.start_time_unix_nano, first_sum.start_time_unix_nano) def test_collect_cumulative(self): """ `SynchronousSumAggregation` collects number data points """ sum_aggregation = _SumAggregation( Mock(), True, AggregationTemporality.CUMULATIVE, 0, _default_reservoir_factory(_SumAggregation), ) sum_aggregation.aggregate(measurement(1)) first_sum = sum_aggregation.collect(AggregationTemporality.CUMULATIVE, 1) self.assertEqual(first_sum.value, 1) # should have been reset after first collect sum_aggregation.aggregate(measurement(1)) second_sum = sum_aggregation.collect(AggregationTemporality.CUMULATIVE, 1) self.assertEqual(second_sum.value, 1) self.assertEqual(second_sum.start_time_unix_nano, first_sum.start_time_unix_nano) # if no point seen for a whole interval, should return None third_sum = sum_aggregation.collect(AggregationTemporality.CUMULATIVE, 1) self.assertIsNone(third_sum) class TestLastValueAggregation(TestCase): def test_aggregate(self): """ `LastValueAggregation` collects data for gauge metric points with delta temporality """ last_value_aggregation = _LastValueAggregation( Mock(), _default_reservoir_factory(_LastValueAggregation), instrument_is_synchronous=True, ) last_value_aggregation.aggregate(measurement(1)) self.assertEqual(last_value_aggregation._value, 1) last_value_aggregation.aggregate(measurement(2)) self.assertEqual(last_value_aggregation._value, 2) last_value_aggregation.aggregate(measurement(3)) self.assertEqual(last_value_aggregation._value, 3) def test_collect(self): """ `LastValueAggregation` collects number data points """ last_value_aggregation = _LastValueAggregation( Mock(), _default_reservoir_factory(_LastValueAggregation), instrument_is_synchronous=True, ) self.assertIsNone(last_value_aggregation.collect(AggregationTemporality.CUMULATIVE, 1)) last_value_aggregation.aggregate(measurement(1)) # 1 is used here directly to simulate the instant the first # collection process starts. first_number_data_point = last_value_aggregation.collect(AggregationTemporality.CUMULATIVE, 1) self.assertIsInstance(first_number_data_point, NumberDataPoint) self.assertEqual(first_number_data_point.value, 1) self.assertIsNone(first_number_data_point.start_time_unix_nano) last_value_aggregation.aggregate(measurement(2)) # CI fails the last assertion without this sleep(0.1) # 2 is used here directly to simulate the instant the second # collection process starts. second_number_data_point = last_value_aggregation.collect(AggregationTemporality.CUMULATIVE, 2) self.assertEqual(second_number_data_point.value, 2) self.assertIsNone(second_number_data_point.start_time_unix_nano) self.assertGreater( second_number_data_point.time_unix_nano, first_number_data_point.time_unix_nano, ) # 3 is used here directly to simulate the instant the third # collection process starts. third_number_data_point = last_value_aggregation.collect(AggregationTemporality.CUMULATIVE, 3) self.assertIsInstance(third_number_data_point, NumberDataPoint) self.assertEqual(third_number_data_point.value, 2) self.assertIsNone(third_number_data_point.start_time_unix_nano) self.assertGreater( third_number_data_point.time_unix_nano, second_number_data_point.time_unix_nano, ) def test_collect_delta_resets_value(self): last_value_aggregation = _LastValueAggregation( Mock(), _default_reservoir_factory(_LastValueAggregation), instrument_is_synchronous=True, ) last_value_aggregation.aggregate(measurement(1)) number_data_point = last_value_aggregation.collect(AggregationTemporality.DELTA, 1) self.assertEqual(number_data_point.value, 1) self.assertIsNone(last_value_aggregation.collect(AggregationTemporality.DELTA, 2)) def test_collect_asynchronous_resets_value(self): last_value_aggregation = _LastValueAggregation( Mock(), _default_reservoir_factory(_LastValueAggregation), instrument_is_synchronous=False, ) last_value_aggregation.aggregate(measurement(1)) number_data_point = last_value_aggregation.collect(AggregationTemporality.CUMULATIVE, 1) self.assertEqual(number_data_point.value, 1) self.assertIsNone(last_value_aggregation.collect(AggregationTemporality.CUMULATIVE, 2)) class TestExplicitBucketHistogramAggregation(TestCase): def test_aggregate(self): """ Test `ExplicitBucketHistogramAggregation with custom boundaries """ explicit_bucket_histogram_aggregation = _ExplicitBucketHistogramAggregation( Mock(), AggregationTemporality.DELTA, 0, _default_reservoir_factory(_ExplicitBucketHistogramAggregation), boundaries=[0, 2, 4], ) explicit_bucket_histogram_aggregation.aggregate(measurement(-1)) explicit_bucket_histogram_aggregation.aggregate(measurement(0)) explicit_bucket_histogram_aggregation.aggregate(measurement(1)) explicit_bucket_histogram_aggregation.aggregate(measurement(2)) explicit_bucket_histogram_aggregation.aggregate(measurement(3)) explicit_bucket_histogram_aggregation.aggregate(measurement(4)) explicit_bucket_histogram_aggregation.aggregate(measurement(5)) # The first bucket keeps count of values between (-inf, 0] (-1 and 0) self.assertEqual(explicit_bucket_histogram_aggregation._value[0], 2) # The second bucket keeps count of values between (0, 2] (1 and 2) self.assertEqual(explicit_bucket_histogram_aggregation._value[1], 2) # The third bucket keeps count of values between (2, 4] (3 and 4) self.assertEqual(explicit_bucket_histogram_aggregation._value[2], 2) # The fourth bucket keeps count of values between (4, inf) (3 and 4) self.assertEqual(explicit_bucket_histogram_aggregation._value[3], 1) histo = explicit_bucket_histogram_aggregation.collect(AggregationTemporality.CUMULATIVE, 1) self.assertEqual(histo.sum, 14) def test_min_max(self): """ `record_min_max` indicates the aggregator to record the minimum and maximum value in the population """ explicit_bucket_histogram_aggregation = _ExplicitBucketHistogramAggregation( Mock(), AggregationTemporality.CUMULATIVE, 0, _default_reservoir_factory(_ExplicitBucketHistogramAggregation), ) explicit_bucket_histogram_aggregation.aggregate(measurement(-1)) explicit_bucket_histogram_aggregation.aggregate(measurement(2)) explicit_bucket_histogram_aggregation.aggregate(measurement(7)) explicit_bucket_histogram_aggregation.aggregate(measurement(8)) explicit_bucket_histogram_aggregation.aggregate(measurement(9999)) self.assertEqual(explicit_bucket_histogram_aggregation._min, -1) self.assertEqual(explicit_bucket_histogram_aggregation._max, 9999) explicit_bucket_histogram_aggregation = _ExplicitBucketHistogramAggregation( Mock(), AggregationTemporality.CUMULATIVE, 0, _default_reservoir_factory(_ExplicitBucketHistogramAggregation), record_min_max=False, ) explicit_bucket_histogram_aggregation.aggregate(measurement(-1)) explicit_bucket_histogram_aggregation.aggregate(measurement(2)) explicit_bucket_histogram_aggregation.aggregate(measurement(7)) explicit_bucket_histogram_aggregation.aggregate(measurement(8)) explicit_bucket_histogram_aggregation.aggregate(measurement(9999)) self.assertEqual(explicit_bucket_histogram_aggregation._min, inf) self.assertEqual(explicit_bucket_histogram_aggregation._max, -inf) def test_collect(self): """ `_ExplicitBucketHistogramAggregation` collects sum metric points """ explicit_bucket_histogram_aggregation = _ExplicitBucketHistogramAggregation( Mock(), AggregationTemporality.DELTA, 0, _default_reservoir_factory(_ExplicitBucketHistogramAggregation), boundaries=[0, 1, 2], ) explicit_bucket_histogram_aggregation.aggregate(measurement(1)) # 1 is used here directly to simulate the instant the first # collection process starts. first_histogram = explicit_bucket_histogram_aggregation.collect(AggregationTemporality.CUMULATIVE, 1) self.assertEqual(first_histogram.bucket_counts, (0, 1, 0, 0)) self.assertEqual(first_histogram.sum, 1) # CI fails the last assertion without this sleep(0.1) explicit_bucket_histogram_aggregation.aggregate(measurement(1)) # 2 is used here directly to simulate the instant the second # collection process starts. second_histogram = explicit_bucket_histogram_aggregation.collect(AggregationTemporality.CUMULATIVE, 2) self.assertEqual(second_histogram.bucket_counts, (0, 2, 0, 0)) self.assertEqual(second_histogram.sum, 2) self.assertGreater(second_histogram.time_unix_nano, first_histogram.time_unix_nano) def test_boundaries(self): self.assertEqual( _ExplicitBucketHistogramAggregation( Mock(), AggregationTemporality.CUMULATIVE, 0, _default_reservoir_factory(_ExplicitBucketHistogramAggregation), )._boundaries, ( 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, ), ) def test_create_aggregation_on_instrument_without_boundaries(self): """ExplicitBucketHistogramAggregation should not crash when applied to a non-Histogram instrument without explicit boundaries. """ aggregation = ExplicitBucketHistogramAggregation() result = aggregation._create_aggregation( _Counter("test.counter", Mock(), Mock()), Mock(), _default_reservoir_factory, 0, ) self.assertIsInstance(result, _ExplicitBucketHistogramAggregation) def test_unsorted_boundaries_raise(self): with self.assertRaises(ValueError): _ExplicitBucketHistogramAggregation( Mock(), AggregationTemporality.DELTA, 0, _default_reservoir_factory(_ExplicitBucketHistogramAggregation), boundaries=[100, 10, 50], ) def test_duplicate_boundaries_raise(self): with self.assertRaises(ValueError): _ExplicitBucketHistogramAggregation( Mock(), AggregationTemporality.DELTA, 0, _default_reservoir_factory(_ExplicitBucketHistogramAggregation), boundaries=[10, 50, 50, 100], ) def test_nan_boundary_raises(self): with self.assertRaises(ValueError): _ExplicitBucketHistogramAggregation( Mock(), AggregationTemporality.DELTA, 0, _default_reservoir_factory(_ExplicitBucketHistogramAggregation), boundaries=[10, float("nan"), 100], ) def test_inf_boundary_raises(self): with self.assertRaises(ValueError): _ExplicitBucketHistogramAggregation( Mock(), AggregationTemporality.DELTA, 0, _default_reservoir_factory(_ExplicitBucketHistogramAggregation), boundaries=[10, 50, float("inf")], ) def test_negative_inf_boundary_raises(self): with self.assertRaises(ValueError): _ExplicitBucketHistogramAggregation( Mock(), AggregationTemporality.DELTA, 0, _default_reservoir_factory(_ExplicitBucketHistogramAggregation), boundaries=[float("-inf"), 50, 100], ) class TestAggregationFactory(TestCase): def test_sum_factory(self): counter = _Counter("name", Mock(), Mock()) factory = SumAggregation() aggregation = factory._create_aggregation(counter, Mock(), _default_reservoir_factory, 0) self.assertIsInstance(aggregation, _SumAggregation) self.assertTrue(aggregation._instrument_is_monotonic) self.assertEqual( aggregation._instrument_aggregation_temporality, AggregationTemporality.DELTA, ) aggregation2 = factory._create_aggregation(counter, Mock(), _default_reservoir_factory, 0) self.assertNotEqual(aggregation, aggregation2) counter = _UpDownCounter("name", Mock(), Mock()) factory = SumAggregation() aggregation = factory._create_aggregation(counter, Mock(), _default_reservoir_factory, 0) self.assertIsInstance(aggregation, _SumAggregation) self.assertFalse(aggregation._instrument_is_monotonic) self.assertEqual( aggregation._instrument_aggregation_temporality, AggregationTemporality.DELTA, ) counter = _ObservableCounter("name", Mock(), Mock(), None) factory = SumAggregation() aggregation = factory._create_aggregation(counter, Mock(), _default_reservoir_factory, 0) self.assertIsInstance(aggregation, _SumAggregation) self.assertTrue(aggregation._instrument_is_monotonic) self.assertEqual( aggregation._instrument_aggregation_temporality, AggregationTemporality.CUMULATIVE, ) def test_explicit_bucket_histogram_factory(self): histo = _Histogram("name", Mock(), Mock()) factory = ExplicitBucketHistogramAggregation( boundaries=( 0.0, 5.0, ), record_min_max=False, ) aggregation = factory._create_aggregation(histo, Mock(), _default_reservoir_factory, 0) self.assertIsInstance(aggregation, _ExplicitBucketHistogramAggregation) self.assertFalse(aggregation._record_min_max) self.assertEqual(aggregation._boundaries, (0.0, 5.0)) aggregation2 = factory._create_aggregation(histo, Mock(), _default_reservoir_factory, 0) self.assertNotEqual(aggregation, aggregation2) def test_last_value_factory(self): counter = _Counter("name", Mock(), Mock()) factory = LastValueAggregation() aggregation = factory._create_aggregation(counter, Mock(), _default_reservoir_factory, 0) self.assertIsInstance(aggregation, _LastValueAggregation) aggregation2 = factory._create_aggregation(counter, Mock(), _default_reservoir_factory, 0) self.assertNotEqual(aggregation, aggregation2) class TestDefaultAggregation(TestCase): @classmethod def setUpClass(cls): cls.default_aggregation = DefaultAggregation() def test_counter(self): aggregation = self.default_aggregation._create_aggregation( _Counter("name", Mock(), Mock()), Mock(), _default_reservoir_factory, 0, ) self.assertIsInstance(aggregation, _SumAggregation) self.assertTrue(aggregation._instrument_is_monotonic) self.assertEqual( aggregation._instrument_aggregation_temporality, AggregationTemporality.DELTA, ) def test_up_down_counter(self): aggregation = self.default_aggregation._create_aggregation( _UpDownCounter("name", Mock(), Mock()), Mock(), _default_reservoir_factory, 0, ) self.assertIsInstance(aggregation, _SumAggregation) self.assertFalse(aggregation._instrument_is_monotonic) self.assertEqual( aggregation._instrument_aggregation_temporality, AggregationTemporality.DELTA, ) def test_observable_counter(self): aggregation = self.default_aggregation._create_aggregation( _ObservableCounter("name", Mock(), Mock(), callbacks=[Mock()]), Mock(), _default_reservoir_factory, 0, ) self.assertIsInstance(aggregation, _SumAggregation) self.assertTrue(aggregation._instrument_is_monotonic) self.assertEqual( aggregation._instrument_aggregation_temporality, AggregationTemporality.CUMULATIVE, ) def test_observable_up_down_counter(self): aggregation = self.default_aggregation._create_aggregation( _ObservableUpDownCounter("name", Mock(), Mock(), callbacks=[Mock()]), Mock(), _default_reservoir_factory, 0, ) self.assertIsInstance(aggregation, _SumAggregation) self.assertFalse(aggregation._instrument_is_monotonic) self.assertEqual( aggregation._instrument_aggregation_temporality, AggregationTemporality.CUMULATIVE, ) def test_histogram(self): aggregation = self.default_aggregation._create_aggregation( _Histogram( "name", Mock(), Mock(), ), Mock(), _default_reservoir_factory, 0, ) self.assertIsInstance(aggregation, _ExplicitBucketHistogramAggregation) def test_histogram_with_advisory(self): boundaries = [1.0, 2.0, 3.0] aggregation = self.default_aggregation._create_aggregation( _Histogram( "name", Mock(), Mock(), explicit_bucket_boundaries_advisory=boundaries, ), Mock(), _default_reservoir_factory, 0, ) self.assertIsInstance(aggregation, _ExplicitBucketHistogramAggregation) self.assertEqual(aggregation._boundaries, tuple(boundaries)) def test_gauge(self): aggregation = self.default_aggregation._create_aggregation( _Gauge( "name", Mock(), Mock(), ), Mock(), _default_reservoir_factory, 0, ) self.assertIsInstance(aggregation, _LastValueAggregation) def test_observable_gauge(self): aggregation = self.default_aggregation._create_aggregation( _ObservableGauge( "name", Mock(), Mock(), callbacks=[Mock()], ), Mock(), _default_reservoir_factory, 0, ) self.assertIsInstance(aggregation, _LastValueAggregation) class TestExemplarsFromAggregations(TestCase): def test_collection_simple_fixed_size_reservoir(self): synchronous_sum_aggregation = _SumAggregation( Mock(), True, AggregationTemporality.DELTA, 0, lambda: SimpleFixedSizeExemplarReservoir(size=3), ) synchronous_sum_aggregation.aggregate(measurement(1)) synchronous_sum_aggregation.aggregate(measurement(2)) synchronous_sum_aggregation.aggregate(measurement(3)) self.assertEqual(synchronous_sum_aggregation._value, 6) datapoint = synchronous_sum_aggregation.collect(AggregationTemporality.CUMULATIVE, 0) # As the reservoir as multiple buckets, it may store up to # 3 exemplars self.assertGreater(len(datapoint.exemplars), 0) self.assertLessEqual(len(datapoint.exemplars), 3) def test_collection_simple_fixed_size_reservoir_with_default_reservoir( self, ): synchronous_sum_aggregation = _SumAggregation( Mock(), True, AggregationTemporality.DELTA, 0, _default_reservoir_factory(_SumAggregation), ) synchronous_sum_aggregation.aggregate(measurement(1)) synchronous_sum_aggregation.aggregate(measurement(2)) synchronous_sum_aggregation.aggregate(measurement(3)) self.assertEqual(synchronous_sum_aggregation._value, 6) datapoint = synchronous_sum_aggregation.collect(AggregationTemporality.CUMULATIVE, 0) self.assertEqual(len(datapoint.exemplars), 1) def test_collection_aligned_histogram_bucket_reservoir(self): boundaries = [5.0, 10.0, 20.0] synchronous_sum_aggregation = _SumAggregation( Mock(), True, AggregationTemporality.DELTA, 0, lambda: AlignedHistogramBucketExemplarReservoir(boundaries), ) synchronous_sum_aggregation.aggregate(measurement(2.0)) synchronous_sum_aggregation.aggregate(measurement(4.0)) synchronous_sum_aggregation.aggregate(measurement(6.0)) synchronous_sum_aggregation.aggregate(measurement(15.0)) synchronous_sum_aggregation.aggregate(measurement(25.0)) datapoint = synchronous_sum_aggregation.collect(AggregationTemporality.CUMULATIVE, 0) self.assertEqual(len(datapoint.exemplars), 4) # Verify that exemplars are associated with the correct boundaries expected_buckets = [ ( 4.0, boundaries[0], ), # First bucket, should hold the last value <= 5.0 ( 6.0, boundaries[1], ), # Second bucket, should hold the last value <= 10.0 ( 15.0, boundaries[2], ), # Third bucket, should hold the last value <= 20.0 (25.0, None), # Last bucket, should hold the value > 20.0 ] for exemplar, (value, boundary) in zip(datapoint.exemplars, expected_buckets): self.assertEqual(exemplar.value, value) if boundary is not None: self.assertLessEqual(exemplar.value, boundary) else: self.assertGreater(exemplar.value, boundaries[-1]) python-opentelemetry-1.45.2/opentelemetry-sdk/tests/metrics/test_attribute_hashing.py000066400000000000000000000076521527070070700314350ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 """Attribute sets that differ in the OTel data model must not share a stream. `_hash_attributes` builds the aggregation key. Python compares `True == 1 == 1.0` and hashes them identically, but OTLP encodes them as `bool_value`, `int_value` and `double_value` -- three different values. Folding them together merges unrelated time series and mislabels the survivor. """ import unittest from opentelemetry.sdk.metrics import MeterProvider from opentelemetry.sdk.metrics._internal._view_instrument_match import ( _hash_attributes, ) from opentelemetry.sdk.metrics.export import InMemoryMetricReader class TestHashAttributesDistinguishesTypes(unittest.TestCase): def test_bool_int_and_float_are_distinct(self): keys = [ _hash_attributes({"k": True}), _hash_attributes({"k": 1}), _hash_attributes({"k": 1.0}), ] self.assertEqual(len(set(keys)), 3) def test_false_and_zero_are_distinct(self): self.assertNotEqual(_hash_attributes({"k": False}), _hash_attributes({"k": 0})) def test_none_and_the_string_none_are_distinct(self): self.assertNotEqual(_hash_attributes({"k": None}), _hash_attributes({"k": "None"})) def test_sequence_of_pairs_and_mapping_are_distinct(self): """A list of pairs must not hash like the equivalent mapping.""" self.assertNotEqual( _hash_attributes({"k": (("x", 1),)}), _hash_attributes({"k": {"x": 1}}), ) def test_nested_types_are_distinguished(self): self.assertNotEqual(_hash_attributes({"k": (True,)}), _hash_attributes({"k": (1,)})) def test_key_order_still_does_not_matter(self): """Ordering must remain irrelevant; this is the property that matters most.""" self.assertEqual( _hash_attributes({"a": 1, "b": 2}), _hash_attributes({"b": 2, "a": 1}), ) def test_equal_attributes_still_share_a_key(self): self.assertEqual(_hash_attributes({"a": "x"}), _hash_attributes({"a": "x"})) def test_result_is_hashable(self): key = _hash_attributes({"a": 1, "b": (1, 2), "c": {"d": None}}) self.assertIsInstance(hash(key), int) class TestDistinctAttributeTypesProduceDistinctStreams(unittest.TestCase): def setUp(self): self.reader = InMemoryMetricReader() self.provider = MeterProvider(metric_readers=[self.reader]) self.meter = self.provider.get_meter(__name__) def tearDown(self): self.provider.shutdown() def _data_points(self): data = self.reader.get_metrics_data() return data.resource_metrics[0].scope_metrics[0].metrics[0].data.data_points def test_counter_keeps_bool_int_and_float_apart(self): counter = self.meter.create_counter("counter") counter.add(1, {"k": True}) counter.add(1, {"k": 1}) counter.add(1, {"k": 1.0}) points = self._data_points() self.assertEqual(len(points), 3) self.assertEqual([p.value for p in points], [1, 1, 1]) self.assertEqual( sorted(type(dict(p.attributes)["k"]).__name__ for p in points), ["bool", "float", "int"], ) def test_histogram_keeps_false_and_zero_apart(self): histogram = self.meter.create_histogram("histogram") histogram.record(1, {"flag": False}) histogram.record(2, {"flag": 0}) points = self._data_points() self.assertEqual(len(points), 2) self.assertEqual( sorted(type(dict(p.attributes)["flag"]).__name__ for p in points), ["bool", "int"], ) def test_same_attributes_still_aggregate_together(self): counter = self.meter.create_counter("counter") counter.add(1, {"k": "same"}) counter.add(1, {"k": "same"}) points = self._data_points() self.assertEqual(len(points), 1) self.assertEqual(points[0].value, 2) python-opentelemetry-1.45.2/opentelemetry-sdk/tests/metrics/test_backward_compat.py000066400000000000000000000063261527070070700310470ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 """ The purpose of this test is to test for backward compatibility with any user-implementable interfaces as they were originally defined. For example, changes to the MetricExporter ABC must be made in such a way that existing implementations (outside of this repo) continue to work when *called* by the SDK. This does not apply to classes which are not intended to be overridden by the user e.g. Meter and PeriodicExportingMetricReader concrete class. Those may freely be modified in a backward-compatible way for *callers*. Ideally, we could use pyright for this as well, but SDK is not type checked atm. """ from collections.abc import Iterable, Sequence from opentelemetry.metrics import CallbackOptions, Observation from opentelemetry.sdk.metrics import MeterProvider from opentelemetry.sdk.metrics._internal.export import InMemoryMetricReader from opentelemetry.sdk.metrics.export import ( Metric, MetricExporter, MetricExportResult, MetricReader, PeriodicExportingMetricReader, ) from opentelemetry.test import TestCase # Do not change these classes until after major version 1 class OrigMetricExporter(MetricExporter): def export( self, metrics_data: Sequence[Metric], timeout_millis: float = 10_000, **kwargs, ) -> MetricExportResult: pass def shutdown(self, timeout_millis: float = 30_000, **kwargs) -> None: pass def force_flush(self, timeout_millis: float = 10_000) -> bool: return True class OrigMetricReader(MetricReader): def _receive_metrics( self, metrics_data: Iterable[Metric], timeout_millis: float = 10_000, **kwargs, ) -> None: pass def shutdown(self, timeout_millis: float = 30_000, **kwargs) -> None: self.collect() def orig_callback(options: CallbackOptions) -> Iterable[Observation]: yield Observation(2) class TestBackwardCompat(TestCase): def test_metric_exporter(self): exporter = OrigMetricExporter() meter_provider = MeterProvider(metric_readers=[PeriodicExportingMetricReader(exporter)]) # produce some data meter_provider.get_meter("foo").create_counter("mycounter").add(12) with self.assertNotRaises(Exception): meter_provider.shutdown() def test_metric_reader(self): reader = OrigMetricReader() meter_provider = MeterProvider(metric_readers=[reader]) # produce some data meter_provider.get_meter("foo").create_counter("mycounter").add(12) with self.assertNotRaises(Exception): meter_provider.shutdown() def test_observable_callback(self): reader = InMemoryMetricReader() meter_provider = MeterProvider(metric_readers=[reader]) # produce some data meter_provider.get_meter("foo").create_counter("mycounter").add(12) with self.assertNotRaises(Exception): metrics_data = reader.get_metrics_data() self.assertEqual(len(metrics_data.resource_metrics), 1) self.assertEqual(len(metrics_data.resource_metrics[0].scope_metrics), 1) self.assertEqual(len(metrics_data.resource_metrics[0].scope_metrics[0].metrics), 1) python-opentelemetry-1.45.2/opentelemetry-sdk/tests/metrics/test_exemplarfilter.py000066400000000000000000000040651527070070700307470ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from unittest import TestCase from opentelemetry import trace from opentelemetry.context import Context from opentelemetry.sdk.metrics._internal.exemplar import ( AlwaysOffExemplarFilter, AlwaysOnExemplarFilter, TraceBasedExemplarFilter, ) from opentelemetry.trace import TraceFlags from opentelemetry.trace.span import SpanContext class TestAlwaysOnExemplarFilter(TestCase): def test_should_sample(self): filter = AlwaysOnExemplarFilter() self.assertTrue(filter.should_sample(10, 0, {}, Context())) class TestAlwaysOffExemplarFilter(TestCase): def test_should_sample(self): filter = AlwaysOffExemplarFilter() self.assertFalse(filter.should_sample(10, 0, {}, Context())) class TestTraceBasedExemplarFilter(TestCase): TRACE_ID = int("d4cda95b652f4a1592b449d5929fda1b", 16) SPAN_ID = int("6e0c63257de34c92", 16) def test_should_not_sample_without_trace(self): filter = TraceBasedExemplarFilter() span_context = SpanContext( trace_id=self.TRACE_ID, span_id=self.SPAN_ID, is_remote=False, trace_flags=TraceFlags(TraceFlags.DEFAULT), trace_state={}, ) span = trace.NonRecordingSpan(span_context) ctx = trace.set_span_in_context(span) self.assertFalse(filter.should_sample(10, 0, {}, ctx)) def test_should_not_sample_with_invalid_span(self): filter = TraceBasedExemplarFilter() self.assertFalse(filter.should_sample(10, 0, {}, Context())) def test_should_sample_when_trace_is_sampled(self): filter = TraceBasedExemplarFilter() span_context = SpanContext( trace_id=self.TRACE_ID, span_id=self.SPAN_ID, is_remote=False, trace_flags=TraceFlags(TraceFlags.SAMPLED), trace_state={}, ) span = trace.NonRecordingSpan(span_context) ctx = trace.set_span_in_context(span) self.assertTrue(filter.should_sample(10, 0, {}, ctx)) python-opentelemetry-1.45.2/opentelemetry-sdk/tests/metrics/test_exemplarreservoir.py000066400000000000000000000210251527070070700314750ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from itertools import chain, count, islice from time import time_ns from unittest import TestCase from opentelemetry import trace from opentelemetry.context import Context from opentelemetry.sdk.metrics._internal.aggregation import ( _ExplicitBucketHistogramAggregation, _LastValueAggregation, _SumAggregation, ) from opentelemetry.sdk.metrics._internal.exemplar import ( AlignedHistogramBucketExemplarReservoir, SimpleFixedSizeExemplarReservoir, ) from opentelemetry.sdk.metrics._internal.view import _default_reservoir_factory from opentelemetry.test.concurrency_test import ConcurrencyTestBase from opentelemetry.trace import SpanContext, TraceFlags class TestSimpleFixedSizeExemplarReservoir(TestCase): TRACE_ID = int("d4cda95b652f4a1592b449d5929fda1b", 16) SPAN_ID = int("6e0c63257de34c92", 16) def test_no_measurements(self): reservoir = SimpleFixedSizeExemplarReservoir(10) self.assertEqual(len(reservoir.collect({})), 0) def test_has_context(self): reservoir = SimpleFixedSizeExemplarReservoir(1) span_context = SpanContext( trace_id=self.TRACE_ID, span_id=self.SPAN_ID, is_remote=False, trace_flags=TraceFlags(TraceFlags.SAMPLED), trace_state={}, ) span = trace.NonRecordingSpan(span_context) ctx = trace.set_span_in_context(span) reservoir.offer(1, time_ns(), {}, ctx) exemplars = reservoir.collect({}) self.assertEqual(len(exemplars), 1) self.assertEqual(exemplars[0].trace_id, self.TRACE_ID) self.assertEqual(exemplars[0].span_id, self.SPAN_ID) def test_filter_attributes(self): reservoir = SimpleFixedSizeExemplarReservoir(1) span_context = SpanContext( trace_id=self.TRACE_ID, span_id=self.SPAN_ID, is_remote=False, trace_flags=TraceFlags(TraceFlags.SAMPLED), trace_state={}, ) span = trace.NonRecordingSpan(span_context) ctx = trace.set_span_in_context(span) reservoir.offer(1, time_ns(), {"key1": "value1", "key2": "value2"}, ctx) exemplars = reservoir.collect({"key2": "value2"}) self.assertEqual(len(exemplars), 1) self.assertIn("key1", exemplars[0].filtered_attributes) self.assertNotIn("key2", exemplars[0].filtered_attributes) def test_fills_every_bucket_before_sampling(self): # The first `size` measurements must each land in their own bucket, # otherwise the reservoir never reaches its configured capacity. for size in (1, 2, 4, 10): with self.subTest(size=size): reservoir = SimpleFixedSizeExemplarReservoir(size) for value in range(size): reservoir.offer( float(value), time_ns(), {"attribute": "value"}, Context(), ) exemplars = reservoir.collect({}) self.assertEqual(len(exemplars), size) self.assertEqual( sorted(exemplar.value for exemplar in exemplars), [float(value) for value in range(size)], ) def test_reset_after_collection(self): reservoir = SimpleFixedSizeExemplarReservoir(4) reservoir.offer(1.0, time_ns(), {"attribute": "value1"}, Context()) reservoir.offer(2.0, time_ns(), {"attribute": "value2"}, Context()) reservoir.offer(3.0, time_ns(), {"attribute": "value3"}, Context()) exemplars = reservoir.collect({}) self.assertEqual(len(exemplars), 3) # Offer new measurements after reset reservoir.offer(4.0, time_ns(), {"attribute": "value4"}, Context()) reservoir.offer(5.0, time_ns(), {"attribute": "value5"}, Context()) # Collect again and check the number of exemplars new_exemplars = reservoir.collect({}) self.assertEqual(len(new_exemplars), 2) self.assertEqual(new_exemplars[0].value, 4.0) self.assertEqual(new_exemplars[1].value, 5.0) class TestAlignedHistogramBucketExemplarReservoir(TestCase): TRACE_ID = int("d4cda95b652f4a1592b449d5929fda1b", 16) SPAN_ID = int("6e0c63257de34c92", 16) def test_measurement_in_buckets(self): reservoir = AlignedHistogramBucketExemplarReservoir([0, 5, 10, 25, 50, 75]) span_context = SpanContext( trace_id=self.TRACE_ID, span_id=self.SPAN_ID, is_remote=False, trace_flags=TraceFlags(TraceFlags.SAMPLED), trace_state={}, ) span = trace.NonRecordingSpan(span_context) ctx = trace.set_span_in_context(span) reservoir.offer(80, time_ns(), {"bucket": "5"}, ctx) # outliner reservoir.offer(52, time_ns(), {"bucket": "4"}, ctx) reservoir.offer(7, time_ns(), {"bucket": "1"}, ctx) reservoir.offer(6, time_ns(), {"bucket": "1"}, ctx) exemplars = reservoir.collect({"bucket": "1"}) self.assertEqual(len(exemplars), 3) self.assertEqual(exemplars[0].value, 6) self.assertEqual(exemplars[1].value, 52) self.assertEqual(exemplars[2].value, 80) # outliner self.assertEqual(len(exemplars[0].filtered_attributes), 0) def test_last_measurement_in_bucket(self): reservoir = AlignedHistogramBucketExemplarReservoir([0, 5, 10, 25]) span_context = SpanContext( trace_id=self.TRACE_ID, span_id=self.SPAN_ID, is_remote=False, trace_flags=TraceFlags(TraceFlags.SAMPLED), trace_state={}, ) span = trace.NonRecordingSpan(span_context) ctx = trace.set_span_in_context(span) # Offer values to the reservoir reservoir.offer(2, time_ns(), {"bucket": "1"}, ctx) # Bucket 1 reservoir.offer(7, time_ns(), {"bucket": "2"}, ctx) # Bucket 2 reservoir.offer(8, time_ns(), {"bucket": "2"}, ctx) # Bucket 2 - should replace the 7 reservoir.offer(15, time_ns(), {"bucket": "3"}, ctx) # Bucket 3 exemplars = reservoir.collect({}) # Check that each bucket has the correct value self.assertEqual(len(exemplars), 3) self.assertEqual(exemplars[0].value, 2) self.assertEqual(exemplars[1].value, 8) self.assertEqual(exemplars[2].value, 15) class TestExemplarReservoirFactory(TestCase): def test_sum_aggregation(self): exemplar_reservoir = _default_reservoir_factory(_SumAggregation) self.assertEqual(exemplar_reservoir, SimpleFixedSizeExemplarReservoir) def test_last_value_aggregation(self): exemplar_reservoir = _default_reservoir_factory(_LastValueAggregation) self.assertEqual(exemplar_reservoir, SimpleFixedSizeExemplarReservoir) def test_explicit_histogram_aggregation(self): exemplar_reservoir = _default_reservoir_factory(_ExplicitBucketHistogramAggregation) self.assertEqual(exemplar_reservoir, AlignedHistogramBucketExemplarReservoir) class TestExemplarReservoirConcurrency(ConcurrencyTestBase): """Every reservoir method must be safe to call concurrently: measurements are offered from application threads while a metric reader collects from its own thread. """ NUM_THREADS = 50 ITERATIONS = 200 def _run_concurrently(self, reservoir): threads = count() values = count(1) def worker(): if next(threads) % 2: return [exemplar for _ in range(self.ITERATIONS) for exemplar in reservoir.collect({})] for value in islice(values, self.ITERATIONS): reservoir.offer(value, value, {"v": value}, Context()) return [] collected = self.run_with_many_threads(worker, self.NUM_THREADS) return list(chain(*collected, reservoir.collect({}))) def test_offer_and_collect_are_mutually_exclusive(self): for name, build_reservoir in ( ( "simple_fixed_size", lambda: SimpleFixedSizeExemplarReservoir(size=4), ), ( "aligned_histogram_bucket", lambda: AlignedHistogramBucketExemplarReservoir([10.0, 20.0, 30.0]), ), ): with self.subTest(reservoir=name): for exemplar in self._run_concurrently(build_reservoir()): self.assertEqual(exemplar.value, exemplar.time_unix_nano) self.assertEqual(exemplar.filtered_attributes, {"v": exemplar.value}) python-opentelemetry-1.45.2/opentelemetry-sdk/tests/metrics/test_import.py000066400000000000000000000040471527070070700272360ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # pylint: disable=unused-import,import-outside-toplevel,too-many-locals from opentelemetry.test import TestCase class TestImport(TestCase): def test_import_init(self): """ Test that the metrics root module has the right symbols """ with self.assertNotRaises(Exception): from opentelemetry.sdk.metrics import ( # noqa: PLC0415, F401 Counter, Histogram, Meter, MeterProvider, ObservableCounter, ObservableGauge, ObservableUpDownCounter, UpDownCounter, _Gauge, ) def test_import_export(self): """ Test that the metrics export module has the right symbols """ with self.assertNotRaises(Exception): from opentelemetry.sdk.metrics.export import ( # noqa: PLC0415, F401 AggregationTemporality, ConsoleMetricExporter, DataPointT, DataT, Gauge, Histogram, HistogramDataPoint, InMemoryMetricReader, Metric, MetricExporter, MetricExportResult, MetricReader, MetricsData, NumberDataPoint, PeriodicExportingMetricReader, ResourceMetrics, ScopeMetrics, Sum, ) def test_import_view(self): """ Test that the metrics view module has the right symbols """ with self.assertNotRaises(Exception): from opentelemetry.sdk.metrics.view import ( # noqa: PLC0415, F401 Aggregation, DefaultAggregation, DropAggregation, ExplicitBucketHistogramAggregation, LastValueAggregation, SumAggregation, View, ) python-opentelemetry-1.45.2/opentelemetry-sdk/tests/metrics/test_in_memory_metric_reader.py000066400000000000000000000160421527070070700326050ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # pylint: disable=protected-access from time import sleep from unittest import TestCase from unittest.mock import Mock from opentelemetry.metrics import Observation from opentelemetry.sdk.metrics import Counter, MeterProvider from opentelemetry.sdk.metrics._internal.point import ( MetricsData, ResourceMetrics, ScopeMetrics, ) from opentelemetry.sdk.metrics.export import ( AggregationTemporality, InMemoryMetricReader, Metric, NumberDataPoint, Sum, ) from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.util.instrumentation import InstrumentationScope class TestInMemoryMetricReader(TestCase): def test_no_metrics(self): mock_collect_callback = Mock(return_value=MetricsData(resource_metrics=[])) reader = InMemoryMetricReader() reader._set_collect_callback(mock_collect_callback) self.assertEqual(reader.get_metrics_data(), MetricsData(resource_metrics=[])) mock_collect_callback.assert_called_once() def test_converts_metrics_to_list(self): metric = Metric( name="foo", description="", unit="", data=Sum( data_points=[ NumberDataPoint( attributes={"myattr": "baz"}, start_time_unix_nano=1647626444152947792, time_unix_nano=1647626444153163239, value=72.3309814450449, ) ], aggregation_temporality=AggregationTemporality.CUMULATIVE, is_monotonic=True, ), ) metric_data = MetricsData( resource_metrics=[ ResourceMetrics( scope_metrics=[ ScopeMetrics( metrics=[metric], scope=InstrumentationScope(name="test"), schema_url="", ) ], resource=Resource.create(), schema_url="", ) ] ) mock_collect_callback = Mock(return_value=metric_data) reader = InMemoryMetricReader() reader._set_collect_callback(mock_collect_callback) returned_metrics = reader.get_metrics_data() mock_collect_callback.assert_called_once() self.assertIsNotNone(returned_metrics) self.assertIs( returned_metrics.resource_metrics[0].scope_metrics[0].metrics[0], metric, ) def test_shutdown(self): # shutdown should always be successful self.assertIsNone(InMemoryMetricReader().shutdown()) def test_integration(self): reader = InMemoryMetricReader() meter = MeterProvider(metric_readers=[reader]).get_meter("test_meter") counter1 = meter.create_counter("counter1") meter.create_observable_gauge( "observable_gauge1", callbacks=[lambda options: [Observation(value=12)]], ) counter1.add(1, {"foo": "1"}) counter1.add(1, {"foo": "2"}) metrics = reader.get_metrics_data() # should be 3 number data points, one from the observable gauge and one # for each labelset from the counter self.assertEqual(len(metrics.resource_metrics[0].scope_metrics), 1) self.assertEqual(len(metrics.resource_metrics[0].scope_metrics[0].metrics), 2) self.assertEqual( len(list(metrics.resource_metrics[0].scope_metrics[0].metrics[0].data.data_points)), 2, ) self.assertEqual( len(list(metrics.resource_metrics[0].scope_metrics[0].metrics[1].data.data_points)), 1, ) def test_cumulative_multiple_collect(self): reader = InMemoryMetricReader(preferred_temporality={Counter: AggregationTemporality.CUMULATIVE}) meter = MeterProvider(metric_readers=[reader]).get_meter("test_meter") counter = meter.create_counter("counter1") counter.add(1, attributes={"key": "value"}) reader.collect() number_data_point_0 = list( reader._metrics_data.resource_metrics[0].scope_metrics[0].metrics[0].data.data_points )[0] # Windows tests fail without this sleep because both time_unix_nano # values are the same. sleep(0.1) reader.collect() number_data_point_1 = list( reader._metrics_data.resource_metrics[0].scope_metrics[0].metrics[0].data.data_points )[0] self.assertEqual(number_data_point_0.attributes, number_data_point_1.attributes) self.assertEqual( number_data_point_0.start_time_unix_nano, number_data_point_1.start_time_unix_nano, ) self.assertEqual( number_data_point_0.start_time_unix_nano, number_data_point_1.start_time_unix_nano, ) self.assertEqual(number_data_point_0.value, number_data_point_1.value) self.assertGreater( number_data_point_1.time_unix_nano, number_data_point_0.time_unix_nano, ) def test_synchronous_gauge_cumulative_multiple_collect(self): reader = InMemoryMetricReader() meter = MeterProvider(metric_readers=[reader]).get_meter("test_meter") gauge = meter.create_gauge("gauge1") gauge.set(5, attributes={"key": "value"}) first_metrics_data = reader.get_metrics_data() second_metrics_data = reader.get_metrics_data() self.assertIsNotNone(second_metrics_data) first_gauge_data = first_metrics_data.resource_metrics[0].scope_metrics[0].metrics[0].data second_gauge_data = second_metrics_data.resource_metrics[0].scope_metrics[0].metrics[0].data self.assertEqual( [(point.attributes, point.value) for point in first_gauge_data.data_points], [({"key": "value"}, 5)], ) self.assertEqual( [(point.attributes, point.value) for point in second_gauge_data.data_points], [({"key": "value"}, 5)], ) class TestInMemoryMetricReaderCardinalityLimits(TestCase): def test_default_limit(self): reader = InMemoryMetricReader() self.assertEqual(set(reader._instrument_class_cardinality_limit.values()), {2000}) self.assertEqual(len(reader._instrument_class_cardinality_limit), 7) def test_override_limit_per_instrument_class(self): reader = InMemoryMetricReader(cardinality_limits={Counter: 5}) limits = reader._instrument_class_cardinality_limit self.assertEqual(sorted(limits.values()), [5] + [2000] * 6) def test_invalid_limit(self): for limit in (0, -1, 1.5, "5", True): with self.subTest(limit=limit): with self.assertRaises(ValueError): InMemoryMetricReader(cardinality_limits={Counter: limit}) def test_invalid_instrument_class(self): with self.assertRaises(ValueError): InMemoryMetricReader(cardinality_limits={int: 5}) python-opentelemetry-1.45.2/opentelemetry-sdk/tests/metrics/test_instrument.py000066400000000000000000000462471527070070700301440ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # pylint: disable=no-self-use from logging import WARNING # from time import time_ns from unittest import TestCase from unittest.mock import Mock, patch from opentelemetry.context import Context from opentelemetry.metrics import Observation from opentelemetry.metrics._internal.instrument import CallbackOptions from opentelemetry.sdk.metrics import ( Counter, Histogram, ObservableCounter, ObservableGauge, ObservableUpDownCounter, UpDownCounter, ) from opentelemetry.sdk.metrics import _Gauge as _SDKGauge from opentelemetry.sdk.metrics._internal.instrument import ( _Counter, _Gauge, _Histogram, _ObservableCounter, _ObservableGauge, _ObservableUpDownCounter, _UpDownCounter, ) from opentelemetry.sdk.metrics._internal.measurement import Measurement class TestCounter(TestCase): def testname(self): self.assertEqual(_Counter("name", Mock(), Mock()).name, "name") self.assertEqual(_Counter("Name", Mock(), Mock()).name, "name") def test_add(self): mc = Mock() counter = _Counter("name", Mock(), mc) counter.add(1.0) mc.consume_measurement.assert_called_once() def test_add_non_monotonic(self): mc = Mock() counter = _Counter("name", Mock(), mc) with self.assertLogs(level=WARNING): counter.add(-1.0) mc.consume_measurement.assert_not_called() def test_add_nan(self): mc = Mock() counter = _Counter("name", Mock(), mc) with self.assertLogs(level=WARNING): counter.add(float("nan")) mc.consume_measurement.assert_not_called() def test_add_inf(self): mc = Mock() counter = _Counter("name", Mock(), mc) with self.assertLogs(level=WARNING): counter.add(float("inf")) mc.consume_measurement.assert_not_called() def test_disallow_direct_counter_creation(self): with self.assertRaises(TypeError): # pylint: disable=abstract-class-instantiated Counter("name", Mock(), Mock()) class TestUpDownCounter(TestCase): def test_add(self): mc = Mock() counter = _UpDownCounter("name", Mock(), mc) counter.add(1.0) mc.consume_measurement.assert_called_once() def test_add_non_monotonic(self): mc = Mock() counter = _UpDownCounter("name", Mock(), mc) counter.add(-1.0) mc.consume_measurement.assert_called_once() def test_add_nan(self): mc = Mock() counter = _UpDownCounter("name", Mock(), mc) with self.assertLogs(level=WARNING): counter.add(float("nan")) mc.consume_measurement.assert_not_called() def test_add_inf(self): mc = Mock() counter = _UpDownCounter("name", Mock(), mc) with self.assertLogs(level=WARNING): counter.add(float("inf")) mc.consume_measurement.assert_not_called() def test_disallow_direct_up_down_counter_creation(self): with self.assertRaises(TypeError): # pylint: disable=abstract-class-instantiated UpDownCounter("name", Mock(), Mock()) TEST_ATTRIBUTES = {"foo": "bar"} TEST_CONTEXT = Context() TEST_TIMESTAMP = 1_000_000_000 def callable_callback_0(options: CallbackOptions): return [ Observation(1, attributes=TEST_ATTRIBUTES, context=TEST_CONTEXT), Observation(2, attributes=TEST_ATTRIBUTES, context=TEST_CONTEXT), Observation(3, attributes=TEST_ATTRIBUTES, context=TEST_CONTEXT), ] def callable_callback_1(options: CallbackOptions): return [ Observation(4, attributes=TEST_ATTRIBUTES, context=TEST_CONTEXT), Observation(5, attributes=TEST_ATTRIBUTES, context=TEST_CONTEXT), Observation(6, attributes=TEST_ATTRIBUTES, context=TEST_CONTEXT), ] def generator_callback_0(): options = yield assert isinstance(options, CallbackOptions) options = yield [ Observation(1, attributes=TEST_ATTRIBUTES, context=TEST_CONTEXT), Observation(2, attributes=TEST_ATTRIBUTES, context=TEST_CONTEXT), Observation(3, attributes=TEST_ATTRIBUTES, context=TEST_CONTEXT), ] assert isinstance(options, CallbackOptions) def generator_callback_1(): options = yield assert isinstance(options, CallbackOptions) options = yield [ Observation(4, attributes=TEST_ATTRIBUTES, context=TEST_CONTEXT), Observation(5, attributes=TEST_ATTRIBUTES, context=TEST_CONTEXT), Observation(6, attributes=TEST_ATTRIBUTES, context=TEST_CONTEXT), ] assert isinstance(options, CallbackOptions) @patch( "opentelemetry.sdk.metrics._internal.instrument.time_ns", Mock(return_value=TEST_TIMESTAMP), ) class TestObservableGauge(TestCase): def testname(self): self.assertEqual(_ObservableGauge("name", Mock(), Mock()).name, "name") self.assertEqual(_ObservableGauge("Name", Mock(), Mock()).name, "name") def test_callable_callback_0(self): observable_gauge = _ObservableGauge("name", Mock(), Mock(), [callable_callback_0]) assert list(observable_gauge.callback(CallbackOptions())) == ( [ Measurement( 1, TEST_TIMESTAMP, instrument=observable_gauge, context=TEST_CONTEXT, attributes=TEST_ATTRIBUTES, ), Measurement( 2, TEST_TIMESTAMP, instrument=observable_gauge, context=TEST_CONTEXT, attributes=TEST_ATTRIBUTES, ), Measurement( 3, TEST_TIMESTAMP, instrument=observable_gauge, context=TEST_CONTEXT, attributes=TEST_ATTRIBUTES, ), ] ) def test_callable_multiple_callable_callback(self): observable_gauge = _ObservableGauge("name", Mock(), Mock(), [callable_callback_0, callable_callback_1]) self.assertEqual( list(observable_gauge.callback(CallbackOptions())), [ Measurement( 1, TEST_TIMESTAMP, instrument=observable_gauge, context=TEST_CONTEXT, attributes=TEST_ATTRIBUTES, ), Measurement( 2, TEST_TIMESTAMP, instrument=observable_gauge, context=TEST_CONTEXT, attributes=TEST_ATTRIBUTES, ), Measurement( 3, TEST_TIMESTAMP, instrument=observable_gauge, context=TEST_CONTEXT, attributes=TEST_ATTRIBUTES, ), Measurement( 4, TEST_TIMESTAMP, instrument=observable_gauge, context=TEST_CONTEXT, attributes=TEST_ATTRIBUTES, ), Measurement( 5, TEST_TIMESTAMP, instrument=observable_gauge, context=TEST_CONTEXT, attributes=TEST_ATTRIBUTES, ), Measurement( 6, TEST_TIMESTAMP, instrument=observable_gauge, context=TEST_CONTEXT, attributes=TEST_ATTRIBUTES, ), ], ) def test_generator_callback_0(self): observable_gauge = _ObservableGauge("name", Mock(), Mock(), [generator_callback_0()]) self.assertEqual( list(observable_gauge.callback(CallbackOptions())), [ Measurement( 1, TEST_TIMESTAMP, instrument=observable_gauge, context=TEST_CONTEXT, attributes=TEST_ATTRIBUTES, ), Measurement( 2, TEST_TIMESTAMP, instrument=observable_gauge, context=TEST_CONTEXT, attributes=TEST_ATTRIBUTES, ), Measurement( 3, TEST_TIMESTAMP, instrument=observable_gauge, context=TEST_CONTEXT, attributes=TEST_ATTRIBUTES, ), ], ) def test_generator_multiple_generator_callback(self): observable_gauge = _ObservableGauge( "name", Mock(), Mock(), callbacks=[generator_callback_0(), generator_callback_1()], ) self.assertEqual( list(observable_gauge.callback(CallbackOptions())), [ Measurement( 1, TEST_TIMESTAMP, instrument=observable_gauge, context=TEST_CONTEXT, attributes=TEST_ATTRIBUTES, ), Measurement( 2, TEST_TIMESTAMP, instrument=observable_gauge, context=TEST_CONTEXT, attributes=TEST_ATTRIBUTES, ), Measurement( 3, TEST_TIMESTAMP, instrument=observable_gauge, context=TEST_CONTEXT, attributes=TEST_ATTRIBUTES, ), Measurement( 4, TEST_TIMESTAMP, instrument=observable_gauge, context=TEST_CONTEXT, attributes=TEST_ATTRIBUTES, ), Measurement( 5, TEST_TIMESTAMP, instrument=observable_gauge, context=TEST_CONTEXT, attributes=TEST_ATTRIBUTES, ), Measurement( 6, TEST_TIMESTAMP, instrument=observable_gauge, context=TEST_CONTEXT, attributes=TEST_ATTRIBUTES, ), ], ) def test_nan_callback_value_is_dropped(self): def nan_callback(options: CallbackOptions): return [ Observation(float("nan"), attributes=TEST_ATTRIBUTES), Observation(1, attributes=TEST_ATTRIBUTES), ] observable_gauge = _ObservableGauge("name", Mock(), Mock(), [nan_callback]) with self.assertLogs(level=WARNING): measurements = list(observable_gauge.callback(CallbackOptions())) self.assertEqual(len(measurements), 1) self.assertEqual(measurements[0].value, 1) def test_inf_callback_value_is_dropped(self): def inf_callback(options: CallbackOptions): return [ Observation(float("inf"), attributes=TEST_ATTRIBUTES), Observation(1, attributes=TEST_ATTRIBUTES), ] observable_gauge = _ObservableGauge("name", Mock(), Mock(), [inf_callback]) with self.assertLogs(level=WARNING): measurements = list(observable_gauge.callback(CallbackOptions())) self.assertEqual(len(measurements), 1) self.assertEqual(measurements[0].value, 1) def test_disallow_direct_observable_gauge_creation(self): with self.assertRaises(TypeError): # pylint: disable=abstract-class-instantiated ObservableGauge("name", Mock(), Mock()) @patch( "opentelemetry.sdk.metrics._internal.instrument.time_ns", Mock(return_value=TEST_TIMESTAMP), ) class TestObservableCounter(TestCase): def test_callable_callback_0(self): observable_counter = _ObservableCounter("name", Mock(), Mock(), [callable_callback_0]) self.assertEqual( list(observable_counter.callback(CallbackOptions())), [ Measurement( 1, TEST_TIMESTAMP, instrument=observable_counter, context=TEST_CONTEXT, attributes=TEST_ATTRIBUTES, ), Measurement( 2, TEST_TIMESTAMP, instrument=observable_counter, context=TEST_CONTEXT, attributes=TEST_ATTRIBUTES, ), Measurement( 3, TEST_TIMESTAMP, instrument=observable_counter, context=TEST_CONTEXT, attributes=TEST_ATTRIBUTES, ), ], ) def test_generator_callback_0(self): observable_counter = _ObservableCounter("name", Mock(), Mock(), [generator_callback_0()]) self.assertEqual( list(observable_counter.callback(CallbackOptions())), [ Measurement( 1, TEST_TIMESTAMP, instrument=observable_counter, context=TEST_CONTEXT, attributes=TEST_ATTRIBUTES, ), Measurement( 2, TEST_TIMESTAMP, instrument=observable_counter, context=TEST_CONTEXT, attributes=TEST_ATTRIBUTES, ), Measurement( 3, TEST_TIMESTAMP, instrument=observable_counter, context=TEST_CONTEXT, attributes=TEST_ATTRIBUTES, ), ], ) def test_disallow_direct_observable_counter_creation(self): with self.assertRaises(TypeError): # pylint: disable=abstract-class-instantiated ObservableCounter("name", Mock(), Mock()) class TestGauge(TestCase): def testname(self): self.assertEqual(_Gauge("name", Mock(), Mock()).name, "name") self.assertEqual(_Gauge("Name", Mock(), Mock()).name, "name") def test_set(self): mc = Mock() gauge = _Gauge("name", Mock(), mc) gauge.set(1.0) mc.consume_measurement.assert_called_once() def test_set_nan(self): mc = Mock() gauge = _Gauge("name", Mock(), mc) with self.assertLogs(level=WARNING): gauge.set(float("nan")) mc.consume_measurement.assert_not_called() def test_set_inf(self): mc = Mock() gauge = _Gauge("name", Mock(), mc) with self.assertLogs(level=WARNING): gauge.set(float("inf")) mc.consume_measurement.assert_not_called() def test_disallow_direct_counter_creation(self): with self.assertRaises(TypeError): # pylint: disable=abstract-class-instantiated _SDKGauge("name", Mock(), Mock()) @patch( "opentelemetry.sdk.metrics._internal.instrument.time_ns", Mock(return_value=TEST_TIMESTAMP), ) class TestObservableUpDownCounter(TestCase): def test_callable_callback_0(self): observable_up_down_counter = _ObservableUpDownCounter("name", Mock(), Mock(), [callable_callback_0]) self.assertEqual( list(observable_up_down_counter.callback(CallbackOptions())), [ Measurement( 1, TEST_TIMESTAMP, instrument=observable_up_down_counter, context=TEST_CONTEXT, attributes=TEST_ATTRIBUTES, ), Measurement( 2, TEST_TIMESTAMP, instrument=observable_up_down_counter, context=TEST_CONTEXT, attributes=TEST_ATTRIBUTES, ), Measurement( 3, TEST_TIMESTAMP, instrument=observable_up_down_counter, context=TEST_CONTEXT, attributes=TEST_ATTRIBUTES, ), ], ) def test_generator_callback_0(self): observable_up_down_counter = _ObservableUpDownCounter("name", Mock(), Mock(), [generator_callback_0()]) self.assertEqual( list(observable_up_down_counter.callback(CallbackOptions())), [ Measurement( 1, TEST_TIMESTAMP, instrument=observable_up_down_counter, context=TEST_CONTEXT, attributes=TEST_ATTRIBUTES, ), Measurement( 2, TEST_TIMESTAMP, instrument=observable_up_down_counter, context=TEST_CONTEXT, attributes=TEST_ATTRIBUTES, ), Measurement( 3, TEST_TIMESTAMP, instrument=observable_up_down_counter, context=TEST_CONTEXT, attributes=TEST_ATTRIBUTES, ), ], ) def test_disallow_direct_observable_up_down_counter_creation(self): with self.assertRaises(TypeError): # pylint: disable=abstract-class-instantiated ObservableUpDownCounter("name", Mock(), Mock()) class TestHistogram(TestCase): def test_record(self): mc = Mock() hist = _Histogram("name", Mock(), mc) hist.record(1.0) mc.consume_measurement.assert_called_once() def test_record_non_monotonic(self): mc = Mock() hist = _Histogram("name", Mock(), mc) with self.assertLogs(level=WARNING): hist.record(-1.0) mc.consume_measurement.assert_not_called() def test_record_nan(self): mc = Mock() hist = _Histogram("name", Mock(), mc) with self.assertLogs(level=WARNING): hist.record(float("nan")) mc.consume_measurement.assert_not_called() def test_record_inf(self): mc = Mock() hist = _Histogram("name", Mock(), mc) with self.assertLogs(level=WARNING): hist.record(float("inf")) mc.consume_measurement.assert_not_called() def test_disallow_direct_histogram_creation(self): with self.assertRaises(TypeError): # pylint: disable=abstract-class-instantiated Histogram("name", Mock(), Mock()) class TestInstrumentValidationMessages(TestCase): def test_invalid_name_error_message(self): with self.assertRaises(Exception) as ctx: _Counter("1-invalid-name", Mock(), Mock()) self.assertIn("maximum length 255", str(ctx.exception)) self.assertNotIn("63", str(ctx.exception)) def test_invalid_unit_error_message(self): with self.assertRaises(Exception) as ctx: _Counter("name", Mock(), Mock(), unit="u" * 64) self.assertIn("maximum length 63", str(ctx.exception)) self.assertNotIn("255", str(ctx.exception)) python-opentelemetry-1.45.2/opentelemetry-sdk/tests/metrics/test_measurement_consumer.py000066400000000000000000000247151527070070700321700ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # pylint: disable=invalid-name,no-self-use from threading import Event, Thread from time import sleep from unittest import TestCase from unittest.mock import MagicMock, Mock, patch from opentelemetry.sdk.metrics._internal.measurement_consumer import ( MeasurementConsumer, SynchronousMeasurementConsumer, ) from opentelemetry.sdk.metrics._internal.sdk_configuration import ( SdkConfiguration, ) @patch("opentelemetry.sdk.metrics._internal.measurement_consumer.MetricReaderStorage") class TestSynchronousMeasurementConsumer(TestCase): def test_parent(self, _): self.assertIsInstance( SynchronousMeasurementConsumer(MagicMock(), metric_readers=()), MeasurementConsumer, ) def test_creates_metric_reader_storages(self, MockMetricReaderStorage): """It should create one MetricReaderStorage per metric reader passed in the SdkConfiguration""" reader_mocks = [Mock() for _ in range(5)] SynchronousMeasurementConsumer( SdkConfiguration( exemplar_filter=Mock(), resource=Mock(), views=Mock(), ), metric_readers=reader_mocks, ) self.assertEqual(len(MockMetricReaderStorage.mock_calls), 5) def test_measurements_passed_to_each_reader_storage(self, MockMetricReaderStorage): reader_mocks = [Mock() for _ in range(5)] reader_storage_mocks = [Mock() for _ in range(5)] MockMetricReaderStorage.side_effect = reader_storage_mocks consumer = SynchronousMeasurementConsumer( SdkConfiguration( exemplar_filter=Mock(should_sample=Mock(return_value=False)), resource=Mock(), views=Mock(), ), metric_readers=reader_mocks, ) measurement_mock = Mock() consumer.consume_measurement(measurement_mock) for rs_mock in reader_storage_mocks: rs_mock.consume_measurement.assert_called_once_with(measurement_mock, False) def test_collect_passed_to_reader_stage(self, MockMetricReaderStorage): """Its collect() method should defer to the underlying MetricReaderStorage""" reader_mocks = [Mock() for _ in range(5)] reader_storage_mocks = [Mock() for _ in range(5)] MockMetricReaderStorage.side_effect = reader_storage_mocks consumer = SynchronousMeasurementConsumer( SdkConfiguration( exemplar_filter=Mock(), resource=Mock(), views=Mock(), ), metric_readers=reader_mocks, ) for r_mock, rs_mock in zip(reader_mocks, reader_storage_mocks): rs_mock.collect.assert_not_called() consumer.collect(r_mock) rs_mock.collect.assert_called_once_with() def test_collect_calls_async_instruments(self, MockMetricReaderStorage): """Its collect() method should invoke async instruments and pass measurements to the corresponding metric reader storage""" reader_mock = Mock() reader_storage_mock = Mock() MockMetricReaderStorage.return_value = reader_storage_mock consumer = SynchronousMeasurementConsumer( SdkConfiguration( exemplar_filter=Mock(should_sample=Mock(return_value=False)), resource=Mock(), views=Mock(), ), metric_readers=[reader_mock], ) async_instrument_mocks = [MagicMock() for _ in range(5)] for i_mock in async_instrument_mocks: i_mock.callback.return_value = [Mock()] consumer.register_asynchronous_instrument(i_mock) consumer.collect(reader_mock) # it should call async instruments for i_mock in async_instrument_mocks: i_mock.callback.assert_called_once() # it should pass measurements to reader storage self.assertEqual(len(reader_storage_mock.consume_measurement.mock_calls), 5) # assert consume_measurement was called with at least 2 arguments the second # matching the mocked exemplar filter self.assertFalse(reader_storage_mock.consume_measurement.call_args[1]) def test_collect_timeout(self, MockMetricReaderStorage): reader_mock = Mock() reader_storage_mock = Mock() MockMetricReaderStorage.return_value = reader_storage_mock consumer = SynchronousMeasurementConsumer( SdkConfiguration( exemplar_filter=Mock(), resource=Mock(), views=Mock(), ), metric_readers=[reader_mock], ) def sleep_1(*args, **kwargs): sleep(1) consumer.register_asynchronous_instrument(Mock(**{"callback.side_effect": sleep_1})) with self.assertRaises(Exception) as error: consumer.collect(reader_mock, timeout_millis=10) self.assertIn("Timed out while executing callback", error.exception.args[0]) @patch("opentelemetry.sdk.metrics._internal.measurement_consumer.CallbackOptions") @patch("opentelemetry.sdk.metrics._internal.measurement_consumer.time_ns") def test_collect_deadline(self, mock_time_ns, mock_callback_options, MockMetricReaderStorage): reader_mock = Mock() reader_storage_mock = Mock() MockMetricReaderStorage.return_value = reader_storage_mock consumer = SynchronousMeasurementConsumer( SdkConfiguration( exemplar_filter=Mock(), resource=Mock(), views=Mock(), ), metric_readers=[reader_mock], ) consumer.register_asynchronous_instrument(Mock(**{"callback.return_value": []})) consumer.register_asynchronous_instrument(Mock(**{"callback.return_value": []})) # collect start, first remaining_time, post-first callback, # second remaining_time, post-second callback mock_time_ns.side_effect = [ 0, 0, int(1e9), int(1e9), int(2e9), ] consumer.collect(reader_mock) callback_options_time_call = mock_callback_options.mock_calls[-1].kwargs["timeout_millis"] self.assertLess( callback_options_time_call, 10000, ) class TestSynchronousMeasurementConsumerConcurrency(TestCase): def test_consume_measurement_does_not_acquire_lock(self): """consume_measurement must stay lock free on the hot path.""" with patch("opentelemetry.sdk.metrics._internal.measurement_consumer.MetricReaderStorage"): consumer = SynchronousMeasurementConsumer( SdkConfiguration( exemplar_filter=Mock(should_sample=Mock(return_value=False)), resource=Mock(), views=Mock(), ), metric_readers=[Mock()], ) mock_lock = MagicMock() # pylint: disable-next=protected-access consumer._lock = mock_lock consumer.consume_measurement(Mock()) mock_lock.__enter__.assert_not_called() def test_concurrent_changes_to_metric_readers(self): timeout = 1 failure = None iteration_started = Event() mutation_done = Event() iteration_timeout_error = "Timed out waiting for iteration to start" mutation_timeout_error = "Timed out waiting for mutation to be done" reader = MagicMock() reader._instrument_class_cardinality_limit = {} # pylint: disable=protected-access consumer = SynchronousMeasurementConsumer( SdkConfiguration( exemplar_filter=MagicMock(), resource=MagicMock(), views=MagicMock(), ), metric_readers=[reader], ) def _hooked_iter(iterable): nonlocal failure iterable = iter(iterable) iteration_started.set() if not mutation_done.wait(timeout): failure = mutation_timeout_error yield next(iterable, None) yield from iterable class HookedDict(dict): def values(self): return _hooked_iter(super().values()) with patch.object( consumer, "_reader_storages", # pylint: disable-next=protected-access HookedDict(consumer._reader_storages), ): def mutate(): """Directly mutate _reader_storages after iteration starts""" nonlocal failure if not iteration_started.wait(timeout): failure = iteration_timeout_error # pylint: disable-next=protected-access consumer._reader_storages.clear() mutation_done.set() # Verify that test setup works (direct mutation with no synchronization fails) with self.assertRaises(RuntimeError) as cm: t = Thread(target=mutate) t.start() try: consumer.consume_measurement(MagicMock()) finally: t.join() self.assertEqual("dictionary changed size during iteration", str(cm.exception)) self.assertIsNone(failure) # Reset the events for the second scenario iteration_started.clear() mutation_done.clear() failure = None with patch.object( consumer, "_reader_storages", # pylint: disable-next=protected-access HookedDict(consumer._reader_storages), ): def add_and_remove_readers(): """Mutate via the public API while consume_measurement runs""" nonlocal failure if not iteration_started.wait(timeout): failure = iteration_timeout_error reader = MagicMock() consumer.add_metric_reader(reader) consumer.remove_metric_reader(reader) mutation_done.set() # The copy-on-write API never mutates the mapping being # iterated, so `consume_measurement` completes without raising even # though add/remove run concurrently mid-iteration. t = Thread(target=add_and_remove_readers) t.start() try: consumer.consume_measurement(MagicMock()) finally: t.join() self.assertIsNone(failure) python-opentelemetry-1.45.2/opentelemetry-sdk/tests/metrics/test_metric_reader.py000066400000000000000000000101731527070070700305260ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # pylint: disable=protected-access from collections.abc import Iterable from unittest import TestCase from unittest.mock import patch from opentelemetry.sdk.metrics import Counter, Histogram, ObservableGauge from opentelemetry.sdk.metrics import _Gauge as _SDKGauge from opentelemetry.sdk.metrics._internal.instrument import ( _Counter, _Gauge, _Histogram, _ObservableCounter, _ObservableGauge, _ObservableUpDownCounter, _UpDownCounter, ) from opentelemetry.sdk.metrics.export import ( AggregationTemporality, Metric, MetricReader, ) from opentelemetry.sdk.metrics.view import ( Aggregation, DefaultAggregation, LastValueAggregation, ) _expected_keys = [ _Counter, _UpDownCounter, _Gauge, _Histogram, _ObservableCounter, _ObservableUpDownCounter, _ObservableGauge, ] class DummyMetricReader(MetricReader): def __init__( self, preferred_temporality: dict[type, AggregationTemporality] = None, preferred_aggregation: dict[type, Aggregation] = None, ) -> None: super().__init__( preferred_temporality=preferred_temporality, preferred_aggregation=preferred_aggregation, ) def _receive_metrics( self, metrics_data: Iterable[Metric], timeout_millis: float = 10_000, **kwargs, ) -> None: pass def shutdown(self, timeout_millis: float = 30_000, **kwargs) -> None: return True class TestMetricReader(TestCase): def test_configure_temporality(self): dummy_metric_reader = DummyMetricReader( preferred_temporality={ Histogram: AggregationTemporality.DELTA, ObservableGauge: AggregationTemporality.DELTA, _SDKGauge: AggregationTemporality.DELTA, } ) self.assertEqual( dummy_metric_reader._instrument_class_temporality.keys(), set(_expected_keys), ) self.assertEqual( dummy_metric_reader._instrument_class_temporality[_Counter], AggregationTemporality.CUMULATIVE, ) self.assertEqual( dummy_metric_reader._instrument_class_temporality[_UpDownCounter], AggregationTemporality.CUMULATIVE, ) self.assertEqual( dummy_metric_reader._instrument_class_temporality[_Histogram], AggregationTemporality.DELTA, ) self.assertEqual( dummy_metric_reader._instrument_class_temporality[_ObservableCounter], AggregationTemporality.CUMULATIVE, ) self.assertEqual( dummy_metric_reader._instrument_class_temporality[_ObservableUpDownCounter], AggregationTemporality.CUMULATIVE, ) self.assertEqual( dummy_metric_reader._instrument_class_temporality[_ObservableGauge], AggregationTemporality.DELTA, ) self.assertEqual( dummy_metric_reader._instrument_class_temporality[_Gauge], AggregationTemporality.DELTA, ) def test_configure_aggregation(self): dummy_metric_reader = DummyMetricReader() self.assertEqual( dummy_metric_reader._instrument_class_aggregation.keys(), set(_expected_keys), ) for value in dummy_metric_reader._instrument_class_aggregation.values(): self.assertIsInstance(value, DefaultAggregation) dummy_metric_reader = DummyMetricReader(preferred_aggregation={Counter: LastValueAggregation()}) self.assertEqual( dummy_metric_reader._instrument_class_aggregation.keys(), set(_expected_keys), ) self.assertIsInstance( dummy_metric_reader._instrument_class_aggregation[_Counter], LastValueAggregation, ) # pylint: disable=no-self-use def test_force_flush(self): with patch.object(DummyMetricReader, "collect") as mock_collect: DummyMetricReader().force_flush(timeout_millis=10) mock_collect.assert_called_with(timeout_millis=10) python-opentelemetry-1.45.2/opentelemetry-sdk/tests/metrics/test_metric_reader_storage.py000066400000000000000000000723531527070070700322620ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # pylint: disable=protected-access,invalid-name from logging import WARNING from time import time_ns from unittest.mock import MagicMock, Mock, patch from opentelemetry.context import Context from opentelemetry.sdk.metrics._internal.aggregation import ( _LastValueAggregation, ) from opentelemetry.sdk.metrics._internal.instrument import ( _Counter, _Gauge, _Histogram, _ObservableCounter, _UpDownCounter, ) from opentelemetry.sdk.metrics._internal.measurement import Measurement from opentelemetry.sdk.metrics._internal.metric_reader_storage import ( _DEFAULT_VIEW, MetricReaderStorage, ) from opentelemetry.sdk.metrics._internal.sdk_configuration import ( SdkConfiguration, ) from opentelemetry.sdk.metrics.export import AggregationTemporality from opentelemetry.sdk.metrics.view import ( DefaultAggregation, DropAggregation, ExplicitBucketHistogramAggregation, ExponentialBucketHistogramAggregation, SumAggregation, View, ) from opentelemetry.test.concurrency_test import ConcurrencyTestBase, MockFunc def mock_view_matching(name, *instruments) -> Mock: mock = Mock(name=name) mock._match.side_effect = lambda instrument: instrument in instruments return mock def mock_instrument() -> Mock: instr = Mock() instr.attributes = {} return instr class TestMetricReaderStorage(ConcurrencyTestBase): @patch("opentelemetry.sdk.metrics._internal.metric_reader_storage._ViewInstrumentMatch") def test_creates_view_instrument_matches(self, MockViewInstrumentMatch: Mock): """It should create a MockViewInstrumentMatch when an instrument matches a view""" instrument1 = Mock(name="instrument1") instrument2 = Mock(name="instrument2") view1 = mock_view_matching("view_1", instrument1) view2 = mock_view_matching("view_2", instrument1, instrument2) storage = MetricReaderStorage( SdkConfiguration( exemplar_filter=Mock(), resource=Mock(), views=(view1, view2), ), MagicMock(**{"__getitem__.return_value": AggregationTemporality.CUMULATIVE}), MagicMock(**{"__getitem__.return_value": DefaultAggregation()}), ) # instrument1 matches view1 and view2, so should create two # ViewInstrumentMatch objects storage.consume_measurement(Measurement(1, time_ns(), instrument1, Context())) self.assertEqual( len(MockViewInstrumentMatch.call_args_list), 2, MockViewInstrumentMatch.mock_calls, ) # they should only be created the first time the instrument is seen storage.consume_measurement(Measurement(1, time_ns(), instrument1, Context())) self.assertEqual(len(MockViewInstrumentMatch.call_args_list), 2) # instrument2 matches view2, so should create a single # ViewInstrumentMatch MockViewInstrumentMatch.call_args_list.clear() with self.assertLogs(level=WARNING): storage.consume_measurement(Measurement(1, time_ns(), instrument2, Context())) self.assertEqual(len(MockViewInstrumentMatch.call_args_list), 1) @patch("opentelemetry.sdk.metrics._internal.metric_reader_storage._ViewInstrumentMatch") def test_forwards_calls_to_view_instrument_match(self, MockViewInstrumentMatch: Mock): view_instrument_match1 = Mock(_aggregation=_LastValueAggregation({}, Mock(), instrument_is_synchronous=True)) view_instrument_match2 = Mock(_aggregation=_LastValueAggregation({}, Mock(), instrument_is_synchronous=True)) view_instrument_match3 = Mock(_aggregation=_LastValueAggregation({}, Mock(), instrument_is_synchronous=True)) MockViewInstrumentMatch.side_effect = [ view_instrument_match1, view_instrument_match2, view_instrument_match3, ] instrument1 = Mock(name="instrument1") instrument2 = Mock(name="instrument2") view1 = mock_view_matching("view1", instrument1) view2 = mock_view_matching("view2", instrument1, instrument2) storage = MetricReaderStorage( SdkConfiguration( exemplar_filter=Mock(), resource=Mock(), views=(view1, view2), ), MagicMock(**{"__getitem__.return_value": AggregationTemporality.CUMULATIVE}), MagicMock(**{"__getitem__.return_value": DefaultAggregation()}), ) # Measurements from an instrument should be passed on to each # ViewInstrumentMatch objects created for that instrument measurement = Measurement(1, time_ns(), instrument1, Context()) storage.consume_measurement(measurement) view_instrument_match1.consume_measurement.assert_called_once_with(measurement, True) view_instrument_match2.consume_measurement.assert_called_once_with(measurement, True) view_instrument_match3.consume_measurement.assert_not_called() measurement = Measurement(1, time_ns(), instrument2, Context()) with self.assertLogs(level=WARNING): storage.consume_measurement(measurement) view_instrument_match3.consume_measurement.assert_called_once_with(measurement, True) # collect() should call collect on all of its _ViewInstrumentMatch # objects and combine them together all_metrics = [Mock() for _ in range(6)] view_instrument_match1.collect.return_value = all_metrics[:2] view_instrument_match2.collect.return_value = all_metrics[2:4] view_instrument_match3.collect.return_value = all_metrics[4:] result = storage.collect() view_instrument_match1.collect.assert_called_once() view_instrument_match2.collect.assert_called_once() view_instrument_match3.collect.assert_called_once() self.assertEqual( (result.resource_metrics[0].scope_metrics[0].metrics[0].data.data_points[0]), all_metrics[0], ) self.assertEqual( (result.resource_metrics[0].scope_metrics[0].metrics[0].data.data_points[1]), all_metrics[1], ) self.assertEqual( (result.resource_metrics[0].scope_metrics[0].metrics[1].data.data_points[0]), all_metrics[2], ) self.assertEqual( (result.resource_metrics[0].scope_metrics[0].metrics[1].data.data_points[1]), all_metrics[3], ) self.assertEqual( (result.resource_metrics[0].scope_metrics[1].metrics[0].data.data_points[0]), all_metrics[4], ) self.assertEqual( (result.resource_metrics[0].scope_metrics[1].metrics[0].data.data_points[1]), all_metrics[5], ) @patch("opentelemetry.sdk.metrics._internal.metric_reader_storage._ViewInstrumentMatch") def test_race_concurrent_measurements(self, MockViewInstrumentMatch: Mock): mock_view_instrument_match_ctor = MockFunc() MockViewInstrumentMatch.side_effect = mock_view_instrument_match_ctor instrument1 = Mock(name="instrument1") view1 = mock_view_matching("view1", instrument1) storage = MetricReaderStorage( SdkConfiguration( exemplar_filter=Mock(), resource=Mock(), views=(view1,), ), MagicMock(**{"__getitem__.return_value": AggregationTemporality.CUMULATIVE}), MagicMock(**{"__getitem__.return_value": DefaultAggregation()}), ) def send_measurement(): storage.consume_measurement(Measurement(1, time_ns(), instrument1, Context())) # race sending many measurements concurrently self.run_with_many_threads(send_measurement) # Only one cached entry should exist for the instrument, with exactly # one view-instrument match — duplicate initialization is the bug we're guarding against self.assertIn(instrument1, storage._instrument_view_instrument_matches) self.assertEqual(len(storage._instrument_view_instrument_matches[instrument1]), 1) def test_race_collect_with_new_instruments(self): storage = MetricReaderStorage( SdkConfiguration( exemplar_filter=Mock(), resource=Mock(), views=(View(instrument_name="test"),), ), MagicMock(**{"__getitem__.return_value": AggregationTemporality.CUMULATIVE}), MagicMock(**{"__getitem__.return_value": DefaultAggregation()}), ) counter = _Counter("counter", Mock(), Mock()) storage.consume_measurement(Measurement(1, time_ns(), counter, Context())) view_instrument_match = storage._instrument_view_instrument_matches[counter][0] original_collect = view_instrument_match.collect new_counter = _Counter("new_counter", Mock(), Mock()) # Patch collect() to add a new counter during iteration def collect_with_modification(*args, **kwargs): storage._instrument_view_instrument_matches[new_counter] = [] return original_collect(*args, **kwargs) view_instrument_match.collect = collect_with_modification storage.collect() self.assertIn(new_counter, storage._instrument_view_instrument_matches) @patch("opentelemetry.sdk.metrics._internal.metric_reader_storage._ViewInstrumentMatch") def test_default_view_enabled(self, MockViewInstrumentMatch: Mock): """Instruments should be matched with default views when enabled""" instrument1 = Mock(name="instrument1") instrument2 = Mock(name="instrument2") storage = MetricReaderStorage( SdkConfiguration( exemplar_filter=Mock(), resource=Mock(), views=(), ), MagicMock(**{"__getitem__.return_value": AggregationTemporality.CUMULATIVE}), MagicMock(**{"__getitem__.return_value": DefaultAggregation()}), ) storage.consume_measurement(Measurement(1, time_ns(), instrument1, Context())) self.assertEqual( len(MockViewInstrumentMatch.call_args_list), 1, MockViewInstrumentMatch.mock_calls, ) storage.consume_measurement(Measurement(1, time_ns(), instrument1, Context())) self.assertEqual(len(MockViewInstrumentMatch.call_args_list), 1) MockViewInstrumentMatch.call_args_list.clear() storage.consume_measurement(Measurement(1, time_ns(), instrument2, Context())) self.assertEqual(len(MockViewInstrumentMatch.call_args_list), 1) def test_drop_aggregation(self): counter = _Counter("name", Mock(), Mock()) metric_reader_storage = MetricReaderStorage( SdkConfiguration( exemplar_filter=Mock(), resource=Mock(), views=(View(instrument_name="name", aggregation=DropAggregation()),), ), MagicMock(**{"__getitem__.return_value": AggregationTemporality.CUMULATIVE}), MagicMock(**{"__getitem__.return_value": DefaultAggregation()}), ) metric_reader_storage.consume_measurement(Measurement(1, time_ns(), counter, Context())) self.assertIsNone(metric_reader_storage.collect()) def test_same_collection_start(self): counter = _Counter("name", Mock(), Mock()) up_down_counter = _UpDownCounter("name", Mock(), Mock()) metric_reader_storage = MetricReaderStorage( SdkConfiguration( exemplar_filter=Mock(), resource=Mock(), views=(View(instrument_name="name"),), ), MagicMock(**{"__getitem__.return_value": AggregationTemporality.CUMULATIVE}), MagicMock(**{"__getitem__.return_value": DefaultAggregation()}), ) metric_reader_storage.consume_measurement(Measurement(1, time_ns(), counter, Context())) metric_reader_storage.consume_measurement(Measurement(1, time_ns(), up_down_counter, Context())) actual = metric_reader_storage.collect() self.assertEqual( list(actual.resource_metrics[0].scope_metrics[0].metrics[0].data.data_points)[0].time_unix_nano, list(actual.resource_metrics[0].scope_metrics[1].metrics[0].data.data_points)[0].time_unix_nano, ) def test_conflicting_view_configuration(self): observable_counter = _ObservableCounter( "observable_counter", Mock(), [Mock()], unit="unit", description="description", ) metric_reader_storage = MetricReaderStorage( SdkConfiguration( exemplar_filter=Mock(), resource=Mock(), views=( View( instrument_name="observable_counter", aggregation=ExplicitBucketHistogramAggregation(), ), ), ), MagicMock(**{"__getitem__.return_value": AggregationTemporality.CUMULATIVE}), MagicMock(**{"__getitem__.return_value": DefaultAggregation()}), ) with self.assertLogs(level=WARNING): metric_reader_storage.consume_measurement(Measurement(1, time_ns(), observable_counter, Context())) self.assertIs( metric_reader_storage._instrument_view_instrument_matches[observable_counter][0]._view, _DEFAULT_VIEW, ) def test_conflicting_view_configuration_exponential_histogram(self): observable_counter = _ObservableCounter( "observable_counter", Mock(), [Mock()], unit="unit", description="description", ) metric_reader_storage = MetricReaderStorage( SdkConfiguration( exemplar_filter=Mock(), resource=Mock(), views=( View( instrument_name="observable_counter", aggregation=ExponentialBucketHistogramAggregation(), ), ), ), MagicMock(**{"__getitem__.return_value": AggregationTemporality.CUMULATIVE}), MagicMock(**{"__getitem__.return_value": DefaultAggregation()}), ) with self.assertLogs(level=WARNING): metric_reader_storage.consume_measurement(Measurement(1, time_ns(), observable_counter, Context())) self.assertIs( metric_reader_storage._instrument_view_instrument_matches[observable_counter][0]._view, _DEFAULT_VIEW, ) def test_view_instrument_match_conflict_0(self): # There is a conflict between views and instruments. observable_counter_0 = _ObservableCounter( "observable_counter_0", Mock(), [Mock()], unit="unit", description="description", ) observable_counter_1 = _ObservableCounter( "observable_counter_1", Mock(), [Mock()], unit="unit", description="description", ) metric_reader_storage = MetricReaderStorage( SdkConfiguration( exemplar_filter=Mock(), resource=Mock(), views=( View(instrument_name="observable_counter_0", name="foo"), View(instrument_name="observable_counter_1", name="foo"), ), ), MagicMock(**{"__getitem__.return_value": AggregationTemporality.CUMULATIVE}), MagicMock(**{"__getitem__.return_value": DefaultAggregation()}), ) with self.assertRaises(AssertionError): with self.assertLogs(level=WARNING): metric_reader_storage.consume_measurement(Measurement(1, time_ns(), observable_counter_0, Context())) with self.assertLogs(level=WARNING) as log: metric_reader_storage.consume_measurement(Measurement(1, time_ns(), observable_counter_1, Context())) self.assertIn( "will cause conflicting metrics", log.records[0].message, ) def test_view_instrument_match_conflict_1(self): # There is a conflict between views and instruments. observable_counter_foo = _ObservableCounter( "foo", Mock(), [Mock()], unit="unit", description="description", ) observable_counter_bar = _ObservableCounter( "bar", Mock(), [Mock()], unit="unit", description="description", ) observable_counter_baz = _ObservableCounter( "baz", Mock(), [Mock()], unit="unit", description="description", ) metric_reader_storage = MetricReaderStorage( SdkConfiguration( exemplar_filter=Mock(), resource=Mock(), views=( View(instrument_name="bar", name="foo"), View(instrument_name="baz", name="foo"), ), ), MagicMock(**{"__getitem__.return_value": AggregationTemporality.CUMULATIVE}), MagicMock(**{"__getitem__.return_value": DefaultAggregation()}), ) with self.assertRaises(AssertionError): with self.assertLogs(level=WARNING): metric_reader_storage.consume_measurement(Measurement(1, time_ns(), observable_counter_foo, Context())) with self.assertLogs(level=WARNING) as log: metric_reader_storage.consume_measurement(Measurement(1, time_ns(), observable_counter_bar, Context())) self.assertIn( "will cause conflicting metrics", log.records[0].message, ) with self.assertLogs(level=WARNING) as log: metric_reader_storage.consume_measurement(Measurement(1, time_ns(), observable_counter_baz, Context())) self.assertIn( "will cause conflicting metrics", log.records[0].message, ) for view_instrument_matches in metric_reader_storage._instrument_view_instrument_matches.values(): for view_instrument_match in view_instrument_matches: self.assertEqual(view_instrument_match._name, "foo") def test_view_instrument_match_conflict_2(self): # There is no conflict because the metric streams names are different. observable_counter_foo = _ObservableCounter( "foo", Mock(), [Mock()], unit="unit", description="description", ) observable_counter_bar = _ObservableCounter( "bar", Mock(), [Mock()], unit="unit", description="description", ) metric_reader_storage = MetricReaderStorage( SdkConfiguration( exemplar_filter=Mock(), resource=Mock(), views=( View(instrument_name="foo"), View(instrument_name="bar"), ), ), MagicMock(**{"__getitem__.return_value": AggregationTemporality.CUMULATIVE}), MagicMock(**{"__getitem__.return_value": DefaultAggregation()}), ) with self.assertRaises(AssertionError): with self.assertLogs(level=WARNING): metric_reader_storage.consume_measurement(Measurement(1, time_ns(), observable_counter_foo, Context())) with self.assertRaises(AssertionError): with self.assertLogs(level=WARNING): metric_reader_storage.consume_measurement(Measurement(1, time_ns(), observable_counter_bar, Context())) def test_view_instrument_match_conflict_3(self): # There is no conflict because the aggregation temporality of the # instruments is different. counter_bar = _Counter( "bar", Mock(), [Mock()], unit="unit", description="description", ) observable_counter_baz = _ObservableCounter( "baz", Mock(), [Mock()], unit="unit", description="description", ) metric_reader_storage = MetricReaderStorage( SdkConfiguration( exemplar_filter=Mock(), resource=Mock(), views=( View(instrument_name="bar", name="foo"), View(instrument_name="baz", name="foo"), ), ), MagicMock(**{"__getitem__.return_value": AggregationTemporality.CUMULATIVE}), MagicMock(**{"__getitem__.return_value": DefaultAggregation()}), ) with self.assertRaises(AssertionError): with self.assertLogs(level=WARNING): metric_reader_storage.consume_measurement(Measurement(1, time_ns(), counter_bar, Context())) with self.assertRaises(AssertionError): with self.assertLogs(level=WARNING): metric_reader_storage.consume_measurement(Measurement(1, time_ns(), observable_counter_baz, Context())) def test_view_instrument_match_conflict_4(self): # There is no conflict because the monotonicity of the instruments is # different. counter_bar = _Counter( "bar", Mock(), [Mock()], unit="unit", description="description", ) up_down_counter_baz = _UpDownCounter( "baz", Mock(), [Mock()], unit="unit", description="description", ) metric_reader_storage = MetricReaderStorage( SdkConfiguration( exemplar_filter=Mock(), resource=Mock(), views=( View(instrument_name="bar", name="foo"), View(instrument_name="baz", name="foo"), ), ), MagicMock(**{"__getitem__.return_value": AggregationTemporality.CUMULATIVE}), MagicMock(**{"__getitem__.return_value": DefaultAggregation()}), ) with self.assertRaises(AssertionError): with self.assertLogs(level=WARNING): metric_reader_storage.consume_measurement(Measurement(1, time_ns(), counter_bar, Context())) with self.assertRaises(AssertionError): with self.assertLogs(level=WARNING): metric_reader_storage.consume_measurement(Measurement(1, time_ns(), up_down_counter_baz, Context())) def test_view_instrument_match_conflict_5(self): # There is no conflict because the instrument units are different. observable_counter_0 = _ObservableCounter( "observable_counter_0", Mock(), [Mock()], unit="unit_0", description="description", ) observable_counter_1 = _ObservableCounter( "observable_counter_1", Mock(), [Mock()], unit="unit_1", description="description", ) metric_reader_storage = MetricReaderStorage( SdkConfiguration( exemplar_filter=Mock(), resource=Mock(), views=( View(instrument_name="observable_counter_0", name="foo"), View(instrument_name="observable_counter_1", name="foo"), ), ), MagicMock(**{"__getitem__.return_value": AggregationTemporality.CUMULATIVE}), MagicMock(**{"__getitem__.return_value": DefaultAggregation()}), ) with self.assertRaises(AssertionError): with self.assertLogs(level=WARNING): metric_reader_storage.consume_measurement(Measurement(1, time_ns(), observable_counter_0, Context())) with self.assertRaises(AssertionError): with self.assertLogs(level=WARNING): metric_reader_storage.consume_measurement(Measurement(1, time_ns(), observable_counter_1, Context())) def test_view_instrument_match_conflict_6(self): # There is no conflict because the instrument data points are # different. observable_counter = _ObservableCounter( "observable_counter", Mock(), [Mock()], unit="unit", description="description", ) histogram = _Histogram( "histogram", Mock(), [Mock()], unit="unit", description="description", ) gauge = _Gauge( "gauge", Mock(), [Mock()], unit="unit", description="description", ) metric_reader_storage = MetricReaderStorage( SdkConfiguration( exemplar_filter=Mock(), resource=Mock(), views=( View(instrument_name="observable_counter", name="foo"), View(instrument_name="histogram", name="foo"), View(instrument_name="gauge", name="foo"), ), ), MagicMock(**{"__getitem__.return_value": AggregationTemporality.CUMULATIVE}), MagicMock(**{"__getitem__.return_value": DefaultAggregation()}), ) with self.assertRaises(AssertionError): with self.assertLogs(level=WARNING): metric_reader_storage.consume_measurement(Measurement(1, time_ns(), observable_counter, Context())) with self.assertRaises(AssertionError): with self.assertLogs(level=WARNING): metric_reader_storage.consume_measurement(Measurement(1, time_ns(), histogram, Context())) with self.assertRaises(AssertionError): with self.assertLogs(level=WARNING): metric_reader_storage.consume_measurement(Measurement(1, time_ns(), gauge, Context())) def test_view_instrument_match_conflict_7(self): # There is a conflict between views and instruments because the # description being different does not avoid a conflict. observable_counter_0 = _ObservableCounter( "observable_counter_0", Mock(), [Mock()], unit="unit", description="description_0", ) observable_counter_1 = _ObservableCounter( "observable_counter_1", Mock(), [Mock()], unit="unit", description="description_1", ) metric_reader_storage = MetricReaderStorage( SdkConfiguration( exemplar_filter=Mock(), resource=Mock(), views=( View(instrument_name="observable_counter_0", name="foo"), View(instrument_name="observable_counter_1", name="foo"), ), ), MagicMock(**{"__getitem__.return_value": AggregationTemporality.CUMULATIVE}), MagicMock(**{"__getitem__.return_value": DefaultAggregation()}), ) with self.assertRaises(AssertionError): with self.assertLogs(level=WARNING): metric_reader_storage.consume_measurement(Measurement(1, time_ns(), observable_counter_0, Context())) with self.assertLogs(level=WARNING) as log: metric_reader_storage.consume_measurement(Measurement(1, time_ns(), observable_counter_1, Context())) self.assertIn( "will cause conflicting metrics", log.records[0].message, ) def test_view_instrument_match_conflict_8(self): # There is a conflict because the histogram-matching view changes the # default aggregation of the histogram to Sum aggregation which is the # same aggregation as the default aggregation of the up down counter # and also the temporality and monotonicity of the up down counter and # the histogram are the same. up_down_counter = _UpDownCounter( "up_down_counter", Mock(), [Mock()], unit="unit", description="description", ) histogram = _Histogram( "histogram", Mock(), [Mock()], unit="unit", description="description", ) metric_reader_storage = MetricReaderStorage( SdkConfiguration( exemplar_filter=Mock(), resource=Mock(), views=( View(instrument_name="up_down_counter", name="foo"), View( instrument_name="histogram", name="foo", aggregation=SumAggregation(), ), ), ), MagicMock(**{"__getitem__.return_value": AggregationTemporality.CUMULATIVE}), MagicMock(**{"__getitem__.return_value": DefaultAggregation()}), ) with self.assertRaises(AssertionError): with self.assertLogs(level=WARNING): metric_reader_storage.consume_measurement(Measurement(1, time_ns(), up_down_counter, Context())) with self.assertLogs(level=WARNING) as log: metric_reader_storage.consume_measurement(Measurement(1, time_ns(), histogram, Context())) self.assertIn( "will cause conflicting metrics", log.records[0].message, ) python-opentelemetry-1.45.2/opentelemetry-sdk/tests/metrics/test_metrics.py000066400000000000000000001166461527070070700274030ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # pylint: disable=protected-access,no-self-use,too-many-lines import json import os import subprocess import sys import unittest import weakref from collections.abc import Callable, Iterable, Sequence from logging import DEBUG, WARNING from pathlib import Path from threading import Lock from time import sleep from typing import Any from unittest.mock import MagicMock, Mock, patch from opentelemetry.attributes import BoundedAttributes from opentelemetry.metrics import NoOpMeter from opentelemetry.sdk.environment_variables import ( OTEL_EXPERIMENTAL_RESOURCE_DETECTORS, OTEL_SDK_DISABLED, ) from opentelemetry.sdk.metrics import ( Counter, Histogram, Meter, MeterProvider, ObservableCounter, ObservableGauge, ObservableUpDownCounter, UpDownCounter, _Gauge, ) from opentelemetry.sdk.metrics._internal import ( SynchronousMeasurementConsumer, _default_meter_configurator, _disable_meter_configurator, _MeterConfig, _ProxyMeterConfig, _RuleBasedMeterConfigurator, ) from opentelemetry.sdk.metrics._internal.measurement import Measurement from opentelemetry.sdk.metrics.export import ( InMemoryMetricReader, Metric, MetricExporter, MetricExportResult, MetricReader, PeriodicExportingMetricReader, ) from opentelemetry.sdk.metrics.view import SumAggregation, View from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.util.instrumentation import ( InstrumentationScope, _scope_name_matches_glob, ) from opentelemetry.test import TestCase from opentelemetry.test.concurrency_test import ConcurrencyTestBase, MockFunc class DummyMetricReader(MetricReader): def __init__(self): super().__init__() def _receive_metrics( self, metrics_data: Iterable[Metric], timeout_millis: float = 10_000, **kwargs, ) -> None: pass def shutdown(self, timeout_millis: float = 30_000, **kwargs) -> None: return True # pylint: disable=too-many-public-methods class TestMeterProvider(ConcurrencyTestBase, TestCase): def tearDown(self): MeterProvider._all_metric_readers = weakref.WeakSet() @patch.object(Resource, "create") def test_init_default(self, resource_patch): meter_provider = MeterProvider() resource_mock = resource_patch.return_value resource_patch.assert_called_once() self.assertIsNotNone(meter_provider._sdk_config) self.assertEqual(meter_provider._sdk_config.resource, resource_mock) self.assertTrue( isinstance( meter_provider._measurement_consumer, SynchronousMeasurementConsumer, ) ) self.assertIsNotNone(meter_provider._atexit_handler) def test_register_metric_readers(self): mock_exporter = Mock() mock_exporter._preferred_temporality = None mock_exporter._preferred_aggregation = None metric_reader_0 = PeriodicExportingMetricReader(mock_exporter) metric_reader_1 = PeriodicExportingMetricReader(mock_exporter) with self.assertNotRaises(Exception): MeterProvider(metric_readers=(metric_reader_0,)) MeterProvider(metric_readers=(metric_reader_1,)) with self.assertRaises(Exception): MeterProvider(metric_readers=(metric_reader_0,)) MeterProvider(metric_readers=(metric_reader_0,)) def test_resource(self): """ `MeterProvider` provides a way to allow a `Resource` to be specified. """ meter_provider_0 = MeterProvider() meter_provider_1 = MeterProvider() self.assertEqual( meter_provider_0._sdk_config.resource, meter_provider_1._sdk_config.resource, ) self.assertIsInstance(meter_provider_0._sdk_config.resource, Resource) self.assertIsInstance(meter_provider_1._sdk_config.resource, Resource) resource = Resource({"key": "value"}) self.assertIs(MeterProvider(resource=resource)._sdk_config.resource, resource) def test_update_resource(self): initial_resource = Resource({"one": "one", "two": "old"}) updating_resource = Resource({"two": "new", "three": "three"}) reader = InMemoryMetricReader() meter_provider = MeterProvider(metric_readers=[reader], resource=initial_resource) meter = meter_provider.get_meter("name") counter = meter.create_counter("counter") meter_provider._update_resource(updating_resource) self.assertEqual( meter_provider._sdk_config.resource.attributes, {"one": "one", "two": "new", "three": "three"}, ) counter.add(1) metrics_data = reader.get_metrics_data() self.assertIs( metrics_data.resource_metrics[0].resource, meter_provider._sdk_config.resource, ) new_meter = meter_provider.get_meter("new") new_counter = new_meter.create_counter("new_counter") new_counter.add(1) metrics_data = reader.get_metrics_data() self.assertIs( metrics_data.resource_metrics[0].resource, meter_provider._sdk_config.resource, ) @unittest.skipUnless( hasattr(os, "fork") and hasattr(os, "register_at_fork"), "requires os.fork and os.register_at_fork", ) def test_meter_provider_updates_process_dependent_resource_after_fork( self, ): script_path = Path(__file__).parent / "scripts" / "meter_provider_resource_after_fork.py" result = subprocess.run( [sys.executable, str(script_path)], capture_output=True, check=False, text=True, env={ **os.environ, OTEL_EXPERIMENTAL_RESOURCE_DETECTORS: "process", }, ) self.assertEqual( result.returncode, 0, f"stdout: {result.stdout}\nstderr: {result.stderr}", ) lines = result.stdout.strip().splitlines() child_payload = json.loads(lines[0]) parent_payload = json.loads(lines[1]) self.assertEqual(parent_payload["parent_resource_pid"], parent_payload["parent_pid"]) self.assertEqual( parent_payload["parent_resource_pid_after_fork"], parent_payload["parent_pid"], ) self.assertNotEqual(child_payload["child_pid"], parent_payload["parent_pid"]) self.assertEqual(child_payload["provider_pid"], child_payload["child_pid"]) self.assertEqual( child_payload["exported_resource_pids"], [child_payload["child_pid"]], ) self.assertEqual( child_payload["metric_names"], ["cached_counter", "new_counter"], ) def test_get_meter(self): """ `MeterProvider.get_meter` arguments are used to create an `InstrumentationScope` object on the created `Meter`. """ meter = MeterProvider().get_meter( "name", version="version", schema_url="schema_url", attributes={"key": "value"}, ) self.assertEqual(meter._instrumentation_scope.name, "name") self.assertEqual(meter._instrumentation_scope.version, "version") self.assertEqual(meter._instrumentation_scope.schema_url, "schema_url") self.assertEqual(meter._instrumentation_scope.attributes, {"key": "value"}) def test_get_meter_attributes(self): """ `MeterProvider.get_meter` arguments are used to create an `InstrumentationScope` object on the created `Meter`. """ meter = MeterProvider().get_meter( "name", version="version", schema_url="schema_url", attributes={"key": "value", "key2": 5, "key3": "value3"}, ) self.assertEqual(meter._instrumentation_scope.name, "name") self.assertEqual(meter._instrumentation_scope.version, "version") self.assertEqual(meter._instrumentation_scope.schema_url, "schema_url") self.assertEqual( meter._instrumentation_scope.attributes, {"key": "value", "key2": 5, "key3": "value3"}, ) def test_get_meter_empty(self): """ `MeterProvider.get_meter` called with None or empty string as name should return a NoOpMeter. """ with self.assertLogs(level=WARNING): meter = MeterProvider().get_meter( None, version="version", schema_url="schema_url", ) self.assertIsInstance(meter, NoOpMeter) self.assertEqual(meter._name, None) with self.assertLogs(level=WARNING): meter = MeterProvider().get_meter( "", version="version", schema_url="schema_url", ) self.assertIsInstance(meter, NoOpMeter) self.assertEqual(meter._name, "") def test_get_meter_duplicate(self): """ Subsequent calls to `MeterProvider.get_meter` with the same arguments should return the same `Meter` instance. """ mp = MeterProvider() meter1 = mp.get_meter( "name", version="version", schema_url="schema_url", ) meter2 = mp.get_meter( "name", version="version", schema_url="schema_url", ) meter3 = mp.get_meter( "name2", version="version", schema_url="schema_url", ) self.assertIs(meter1, meter2) self.assertIsNot(meter1, meter3) def test_get_meter_comparison_with_attributes(self): """ Subsequent calls to `MeterProvider.get_meter` with the same arguments should return the same `Meter` instance. """ mp = MeterProvider() meter1 = mp.get_meter( "name", version="version", schema_url="schema_url", attributes={"key": "value", "key2": 5, "key3": "value3"}, ) meter2 = mp.get_meter( "name", version="version", schema_url="schema_url", attributes={"key": "value", "key2": 5, "key3": "value3"}, ) meter3 = mp.get_meter( "name2", version="version", schema_url="schema_url", ) meter4 = mp.get_meter( "name", version="version", schema_url="schema_url", attributes={"key": "value", "key2": 5, "key3": "value4"}, ) self.assertIs(meter1, meter2) self.assertIsNot(meter1, meter3) self.assertTrue(meter3._instrumentation_scope > meter4._instrumentation_scope) self.assertIsInstance(meter4._instrumentation_scope.attributes, BoundedAttributes) def test_shutdown(self): mock_metric_reader_0 = MagicMock( **{ "shutdown.side_effect": ZeroDivisionError(), } ) mock_metric_reader_1 = MagicMock( **{ "shutdown.side_effect": AssertionError(), } ) meter_provider = MeterProvider(metric_readers=[mock_metric_reader_0, mock_metric_reader_1]) with self.assertRaises(Exception) as error: meter_provider.shutdown() error = error.exception self.assertEqual( str(error), ( "MeterProvider.shutdown failed because the following " "metric readers failed during shutdown:\n" "MagicMock: ZeroDivisionError()\n" "MagicMock: AssertionError()" ), ) mock_metric_reader_0.shutdown.assert_called_once() mock_metric_reader_1.shutdown.assert_called_once() mock_metric_reader_0 = Mock() mock_metric_reader_1 = Mock() meter_provider = MeterProvider(metric_readers=[mock_metric_reader_0, mock_metric_reader_1]) self.assertIsNone(meter_provider.shutdown()) mock_metric_reader_0.shutdown.assert_called_once() mock_metric_reader_1.shutdown.assert_called_once() def test_shutdown_subsequent_calls(self): """ No subsequent attempts to get a `Meter` are allowed after calling `MeterProvider.shutdown` """ meter_provider = MeterProvider() with self.assertRaises(AssertionError): with self.assertLogs(level=WARNING): meter_provider.shutdown() with self.assertLogs(level=WARNING): meter_provider.shutdown() @patch("opentelemetry.sdk.metrics._internal._logger") def test_shutdown_race(self, mock_logger): mock_logger.warning = MockFunc() meter_provider = MeterProvider() num_threads = 70 self.run_with_many_threads(meter_provider.shutdown, num_threads=num_threads) self.assertEqual(mock_logger.warning.call_count, num_threads - 1) @patch("opentelemetry.sdk.metrics._internal.SynchronousMeasurementConsumer") def test_measurement_collect_callback(self, mock_sync_measurement_consumer): metric_readers = [ DummyMetricReader(), DummyMetricReader(), DummyMetricReader(), DummyMetricReader(), DummyMetricReader(), ] sync_consumer_instance = mock_sync_measurement_consumer() sync_consumer_instance.collect = MockFunc() MeterProvider(metric_readers=metric_readers) for reader in metric_readers: reader.collect() self.assertEqual(sync_consumer_instance.collect.call_count, len(metric_readers)) @patch("opentelemetry.sdk.metrics._internal.SynchronousMeasurementConsumer") def test_creates_sync_measurement_consumer(self, mock_sync_measurement_consumer): MeterProvider() mock_sync_measurement_consumer.assert_called() @patch("opentelemetry.sdk.metrics._internal.SynchronousMeasurementConsumer") def test_register_asynchronous_instrument(self, mock_sync_measurement_consumer): meter_provider = MeterProvider() # pylint: disable=no-member meter_provider._measurement_consumer.register_asynchronous_instrument.assert_called_with( meter_provider.get_meter("name").create_observable_counter("name0", callbacks=[Mock()]) ) meter_provider._measurement_consumer.register_asynchronous_instrument.assert_called_with( meter_provider.get_meter("name").create_observable_up_down_counter("name1", callbacks=[Mock()]) ) meter_provider._measurement_consumer.register_asynchronous_instrument.assert_called_with( meter_provider.get_meter("name").create_observable_gauge("name2", callbacks=[Mock()]) ) @patch("opentelemetry.sdk.metrics._internal.SynchronousMeasurementConsumer") def test_consume_measurement_counter(self, mock_sync_measurement_consumer): sync_consumer_instance = mock_sync_measurement_consumer() meter_provider = MeterProvider() counter = meter_provider.get_meter("name").create_counter("name") counter.add(1) sync_consumer_instance.consume_measurement.assert_called() @patch("opentelemetry.sdk.metrics._internal.SynchronousMeasurementConsumer") def test_consume_measurement_up_down_counter(self, mock_sync_measurement_consumer): sync_consumer_instance = mock_sync_measurement_consumer() meter_provider = MeterProvider() counter = meter_provider.get_meter("name").create_up_down_counter("name") counter.add(1) sync_consumer_instance.consume_measurement.assert_called() @patch("opentelemetry.sdk.metrics._internal.SynchronousMeasurementConsumer") def test_consume_measurement_histogram(self, mock_sync_measurement_consumer): sync_consumer_instance = mock_sync_measurement_consumer() meter_provider = MeterProvider() counter = meter_provider.get_meter("name").create_histogram("name") counter.record(1) sync_consumer_instance.consume_measurement.assert_called() def test_meter_provider_with_disabled_configurator(self): mp = MeterProvider(_meter_configurator=_disable_meter_configurator) meter = mp.get_meter("test") self.assertFalse(meter._is_enabled()) def test_meter_provider_with_custom_configurator(self): def configurator(scope): if scope.name == "disabled_meter": return _MeterConfig(is_enabled=False) return _MeterConfig.default() mp = MeterProvider(_meter_configurator=configurator) enabled = mp.get_meter("enabled_meter") disabled = mp.get_meter("disabled_meter") self.assertTrue(enabled._is_enabled()) self.assertFalse(disabled._is_enabled()) def test_set_meter_configurator_updates_existing_meters(self): mp = MeterProvider() meter = mp.get_meter("test") self.assertTrue(meter._is_enabled()) mp._set_meter_configurator(meter_configurator=_disable_meter_configurator) self.assertFalse(meter._is_enabled()) def test_set_meter_configurator_affects_new_meters(self): mp = MeterProvider() mp._set_meter_configurator(meter_configurator=_disable_meter_configurator) meter = mp.get_meter("new_meter") self.assertFalse(meter._is_enabled()) def test_buggy_configurator_falls_back_to_default_on_get_meter(self): def raising_configurator(_scope): raise RuntimeError("configurator error") mp = MeterProvider(_meter_configurator=raising_configurator) with self.assertLogs(level="ERROR"): meter = mp.get_meter("test") self.assertTrue(meter._is_enabled()) def test_buggy_configurator_falls_back_to_default_on_set_configurator( self, ): mp = MeterProvider() meter = mp.get_meter("test") self.assertTrue(meter._is_enabled()) def raising_configurator(_scope): raise ValueError("bad config") with self.assertLogs(level="ERROR"): mp._set_meter_configurator(meter_configurator=raising_configurator) # Should still be enabled (default config) despite the error self.assertTrue(meter._is_enabled()) @patch("opentelemetry.sdk.metrics._internal.SynchronousMeasurementConsumer") def test_consume_measurement_gauge(self, mock_sync_measurement_consumer): sync_consumer_instance = mock_sync_measurement_consumer() meter_provider = MeterProvider() gauge = meter_provider.get_meter("name").create_gauge("name") gauge.set(1) sync_consumer_instance.consume_measurement.assert_called() def test_addition_of_metric_reader(self): internal_logger = "opentelemetry.sdk.metrics._internal" export_logger = "opentelemetry.sdk.metrics._internal.export" reader = InMemoryMetricReader() meter_provider = MeterProvider() meter = meter_provider.get_meter(__name__) counter = meter.create_counter("counter") counter.add(1) # Suppress warnings for calling collect on an unregistered metric reader with self.assertLogs(export_logger, DEBUG): self.assertIsNone(reader.get_metrics_data()) meter_provider.add_metric_reader(reader) counter.add(1) self.assertIsNotNone(reader.get_metrics_data()) with self.assertLogs(internal_logger, DEBUG) as cm: meter_provider.add_metric_reader(reader) self.assertIn("has been registered already!", cm.output[0]) meter_provider.remove_metric_reader(reader) counter.add(1) with self.assertLogs(export_logger, DEBUG): self.assertIsNone(reader.get_metrics_data()) with self.assertLogs(internal_logger, DEBUG) as cm: meter_provider.remove_metric_reader(reader) self.assertIn("has not been registered!", cm.output[0]) class TestMeterConcurrency(ConcurrencyTestBase, TestCase): def test_create_instrument_concurrency(self): """ Tests that concurrent creation of the same instrument does not result in a KeyError or inconsistent state for all instrument types. """ meter = Meter(Mock(), Mock()) original_register = meter._register_instrument lock = Lock() registered_names = set() def mocked_register(name: str, *args, **kwargs): status = original_register(name, *args, **kwargs) with lock: first: bool = name not in registered_names registered_names.add(name) if first: # Test interleaving of threads by sleeping after the first thread registers # the instrument, but before the instrument is created. sleep(0.25) return status def make_create_instrument(meter: Meter, method_name: str, args: list[Any]) -> Callable[[], Any]: return lambda: getattr(meter, method_name)(f"concurrent_{method_name}", *args) with patch.object(meter, "_register_instrument", side_effect=mocked_register): create_methods = [ ("create_counter", []), ("create_up_down_counter", []), ("create_histogram", []), ("create_gauge", []), ("create_observable_counter", [[lambda options: []]]), ("create_observable_gauge", [[lambda options: []]]), ("create_observable_up_down_counter", [[lambda options: []]]), ] for method_name, args in create_methods: with self.subTest(method=method_name): instruments = self.run_with_many_threads( make_create_instrument(meter, method_name, args), num_threads=20, ) for instr in instruments: self.assertIs(instr, instruments[0]) class TestMeter(TestCase): def setUp(self): self.meter = Meter(Mock(), Mock()) def test_repeated_instrument_names(self): with self.assertNotRaises(Exception): self.meter.create_counter("counter") self.meter.create_up_down_counter("up_down_counter") self.meter.create_observable_counter("observable_counter", callbacks=[Mock()]) self.meter.create_histogram("histogram") self.meter.create_gauge("gauge") self.meter.create_observable_gauge("observable_gauge", callbacks=[Mock()]) self.meter.create_observable_up_down_counter("observable_up_down_counter", callbacks=[Mock()]) for instrument_name in [ "counter", "up_down_counter", "histogram", "gauge", ]: with self.assertNoLogs("opentelemetry.sdk.metrics._internal", level="WARNING"): getattr(self.meter, f"create_{instrument_name}")(instrument_name) for instrument_name in [ "observable_counter", "observable_gauge", "observable_up_down_counter", ]: with self.assertNoLogs("opentelemetry.sdk.metrics._internal", level="WARNING"): getattr(self.meter, f"create_{instrument_name}")(instrument_name, callbacks=[Mock()]) def test_repeated_instrument_names_with_different_advisory(self): with self.assertNotRaises(Exception): self.meter.create_histogram("histogram", explicit_bucket_boundaries_advisory=[1.0]) for instrument_name in [ "histogram", ]: with self.assertLogs(level=WARNING): getattr(self.meter, f"create_{instrument_name}")(instrument_name) def test_create_counter(self): counter = self.meter.create_counter("name", unit="unit", description="description") self.assertIsInstance(counter, Counter) self.assertEqual(counter.name, "name") def test_create_up_down_counter(self): up_down_counter = self.meter.create_up_down_counter("name", unit="unit", description="description") self.assertIsInstance(up_down_counter, UpDownCounter) self.assertEqual(up_down_counter.name, "name") def test_create_observable_counter(self): observable_counter = self.meter.create_observable_counter( "name", callbacks=[Mock()], unit="unit", description="description" ) self.assertIsInstance(observable_counter, ObservableCounter) self.assertEqual(observable_counter.name, "name") def test_create_histogram(self): histogram = self.meter.create_histogram("name", unit="unit", description="description") self.assertIsInstance(histogram, Histogram) self.assertEqual(histogram.name, "name") def test_create_histogram_with_advisory(self): histogram = self.meter.create_histogram( "name", unit="unit", description="description", explicit_bucket_boundaries_advisory=[0.0, 1.0, 2], ) self.assertIsInstance(histogram, Histogram) self.assertEqual(histogram.name, "name") self.assertEqual( histogram._advisory.explicit_bucket_boundaries, [0.0, 1.0, 2], ) def test_create_histogram_advisory_validation(self): advisories = [ {"explicit_bucket_boundaries_advisory": "hello"}, {"explicit_bucket_boundaries_advisory": ["1"]}, ] for advisory in advisories: with self.subTest(advisory=advisory): with self.assertLogs(level=WARNING): self.meter.create_histogram( "name", unit="unit", description="description", **advisory, ) def test_create_observable_gauge(self): observable_gauge = self.meter.create_observable_gauge( "name", callbacks=[Mock()], unit="unit", description="description" ) self.assertIsInstance(observable_gauge, ObservableGauge) self.assertEqual(observable_gauge.name, "name") def test_create_gauge(self): gauge = self.meter.create_gauge("name", unit="unit", description="description") self.assertIsInstance(gauge, _Gauge) self.assertEqual(gauge.name, "name") def test_create_observable_up_down_counter(self): observable_up_down_counter = self.meter.create_observable_up_down_counter( "name", callbacks=[Mock()], unit="unit", description="description", ) self.assertIsInstance(observable_up_down_counter, ObservableUpDownCounter) self.assertEqual(observable_up_down_counter.name, "name") @patch.dict("os.environ", {OTEL_SDK_DISABLED: "true"}) def test_get_meter_with_sdk_disabled(self): meter_provider = MeterProvider() self.assertIsInstance(meter_provider.get_meter(Mock()), NoOpMeter) def test_meter_config_default(self): config = _MeterConfig.default() self.assertTrue(config.is_enabled) def test_meter_config_disabled(self): config = _MeterConfig(is_enabled=False) self.assertFalse(config.is_enabled) def test_proxy_meter_config_delegates(self): proxy = _ProxyMeterConfig(_MeterConfig(is_enabled=True)) self.assertTrue(proxy.is_enabled) proxy_disabled = _ProxyMeterConfig(_MeterConfig(is_enabled=False)) self.assertFalse(proxy_disabled.is_enabled) def test_proxy_meter_config_update(self): proxy = _ProxyMeterConfig(_MeterConfig(is_enabled=True)) self.assertTrue(proxy.is_enabled) proxy.update(_MeterConfig(is_enabled=False)) self.assertFalse(proxy.is_enabled) proxy.update(_MeterConfig(is_enabled=True)) self.assertTrue(proxy.is_enabled) def test_default_meter_configurator(self): scope = InstrumentationScope("any_name", "1.0") config = _default_meter_configurator(scope) self.assertTrue(config.is_enabled) def test_disable_meter_configurator(self): scope = InstrumentationScope("any_name", "1.0") config = _disable_meter_configurator(scope) self.assertFalse(config.is_enabled) def test_rule_based_configurator_first_match_wins(self): disabled_config = _MeterConfig(is_enabled=False) enabled_config = _MeterConfig(is_enabled=True) configurator = _RuleBasedMeterConfigurator( rules=[ (lambda s: s.name == "foo", disabled_config), (lambda s: s.name == "foo", enabled_config), ], default_config=enabled_config, ) scope = InstrumentationScope("foo", "1.0") result = configurator(scope) self.assertFalse(result.is_enabled) def test_rule_based_configurator_default_when_no_match(self): disabled_config = _MeterConfig(is_enabled=False) configurator = _RuleBasedMeterConfigurator( rules=[ ( lambda s: s.name == "specific", _MeterConfig(is_enabled=True), ), ], default_config=disabled_config, ) scope = InstrumentationScope("other", "1.0") result = configurator(scope) self.assertFalse(result.is_enabled) def test_rule_based_configurator_with_glob_predicate(self): disabled_config = _MeterConfig(is_enabled=False) configurator = _RuleBasedMeterConfigurator( rules=[ (_scope_name_matches_glob("opentelemetry.*"), disabled_config), ], default_config=_MeterConfig.default(), ) self.assertFalse(configurator(InstrumentationScope("opentelemetry.sdk", "1.0")).is_enabled) self.assertTrue(configurator(InstrumentationScope("custom.name", "1.0")).is_enabled) def test_scope_name_matches_glob_exact(self): predicate = _scope_name_matches_glob("my.meter") self.assertTrue(predicate(InstrumentationScope("my.meter", "1.0"))) def test_scope_name_matches_glob_wildcard(self): predicate = _scope_name_matches_glob("my.*") self.assertTrue(predicate(InstrumentationScope("my.meter", "1.0"))) self.assertTrue(predicate(InstrumentationScope("my.other", "1.0"))) self.assertFalse(predicate(InstrumentationScope("other.meter", "1.0"))) def test_scope_name_matches_glob_no_match(self): predicate = _scope_name_matches_glob("no.match") self.assertFalse(predicate(InstrumentationScope("my.meter", "1.0"))) def test_scope_name_matches_glob_is_case_sensitive_on_every_platform(self): # fnmatch normcases both operands, which lower-cases them on Windows. # Scope name matching must stay case-sensitive everywhere. exact = _scope_name_matches_glob("my.meter") self.assertTrue(exact(InstrumentationScope("my.meter", "1.0"))) self.assertFalse(exact(InstrumentationScope("My.Meter", "1.0"))) wildcard = _scope_name_matches_glob("my.*") self.assertTrue(wildcard(InstrumentationScope("my.meter", "1.0"))) self.assertFalse(wildcard(InstrumentationScope("MY.meter", "1.0"))) def test_scope_name_matches_glob_pattern_case_is_not_normalized(self): predicate = _scope_name_matches_glob("MY.*") self.assertTrue(predicate(InstrumentationScope("MY.meter", "1.0"))) self.assertFalse(predicate(InstrumentationScope("my.meter", "1.0"))) @patch("opentelemetry.sdk.metrics._internal.SynchronousMeasurementConsumer") def test_disabled_meter_counter_skips_measurement(self, mock_sync_measurement_consumer): sync_consumer_instance = mock_sync_measurement_consumer() mp = MeterProvider(_meter_configurator=_disable_meter_configurator) counter = mp.get_meter("test").create_counter("c") counter.add(1) sync_consumer_instance.consume_measurement.assert_not_called() @patch("opentelemetry.sdk.metrics._internal.SynchronousMeasurementConsumer") def test_disabled_meter_up_down_counter_skips_measurement(self, mock_sync_measurement_consumer): sync_consumer_instance = mock_sync_measurement_consumer() mp = MeterProvider(_meter_configurator=_disable_meter_configurator) counter = mp.get_meter("test").create_up_down_counter("udc") counter.add(1) sync_consumer_instance.consume_measurement.assert_not_called() @patch("opentelemetry.sdk.metrics._internal.SynchronousMeasurementConsumer") def test_disabled_meter_histogram_skips_measurement(self, mock_sync_measurement_consumer): sync_consumer_instance = mock_sync_measurement_consumer() mp = MeterProvider(_meter_configurator=_disable_meter_configurator) histogram = mp.get_meter("test").create_histogram("h") histogram.record(1) sync_consumer_instance.consume_measurement.assert_not_called() @patch("opentelemetry.sdk.metrics._internal.SynchronousMeasurementConsumer") def test_disabled_meter_gauge_skips_measurement(self, mock_sync_measurement_consumer): sync_consumer_instance = mock_sync_measurement_consumer() mp = MeterProvider(_meter_configurator=_disable_meter_configurator) gauge = mp.get_meter("test").create_gauge("g") gauge.set(1) sync_consumer_instance.consume_measurement.assert_not_called() def test_disabled_meter_observable_counter_skips_callback(self): cb = Mock() mp = MeterProvider(_meter_configurator=_disable_meter_configurator) oc = mp.get_meter("test").create_observable_counter("oc", callbacks=[cb]) # Trigger callback collection list(oc.callback(Mock())) cb.assert_not_called() def test_disabled_meter_observable_gauge_skips_callback(self): cb = Mock() mp = MeterProvider(_meter_configurator=_disable_meter_configurator) og = mp.get_meter("test").create_observable_gauge("og", callbacks=[cb]) list(og.callback(Mock())) cb.assert_not_called() def test_disabled_meter_observable_up_down_counter_skips_callback(self): cb = Mock() mp = MeterProvider(_meter_configurator=_disable_meter_configurator) oudc = mp.get_meter("test").create_observable_up_down_counter("oudc", callbacks=[cb]) list(oudc.callback(Mock())) cb.assert_not_called() @patch("opentelemetry.sdk.metrics._internal.SynchronousMeasurementConsumer") def test_counter_noop_after_meter_disabled(self, mock_sync_measurement_consumer): sync_consumer_instance = mock_sync_measurement_consumer() mp = MeterProvider() meter = mp.get_meter("test") counter = meter.create_counter("c") counter.add(1) self.assertEqual(sync_consumer_instance.consume_measurement.call_count, 1) counter.add(2) self.assertEqual(sync_consumer_instance.consume_measurement.call_count, 2) mp._set_meter_configurator(meter_configurator=_disable_meter_configurator) self.assertFalse(meter._is_enabled()) counter.add(3) counter.add(4) self.assertEqual(sync_consumer_instance.consume_measurement.call_count, 2) @patch("opentelemetry.sdk.metrics._internal.SynchronousMeasurementConsumer") def test_reenable_meter_after_disable(self, mock_sync_measurement_consumer): sync_consumer_instance = mock_sync_measurement_consumer() mp = MeterProvider(_meter_configurator=_disable_meter_configurator) meter = mp.get_meter("test") counter = meter.create_counter("c") counter.add(1) sync_consumer_instance.consume_measurement.assert_not_called() mp._set_meter_configurator(meter_configurator=_default_meter_configurator) self.assertTrue(meter._is_enabled()) counter.add(1) sync_consumer_instance.consume_measurement.assert_called_once() class InMemoryMetricExporter(MetricExporter): def __init__(self): super().__init__() self.metrics = {} self._counter = 0 def export( self, metrics_data: Sequence[Metric], timeout_millis: float = 10_000, **kwargs, ) -> MetricExportResult: self.metrics[self._counter] = metrics_data self._counter += 1 return MetricExportResult.SUCCESS def shutdown(self, timeout_millis: float = 30_000, **kwargs) -> None: pass def force_flush(self, timeout_millis: float = 10_000) -> bool: return True class TestDuplicateInstrumentAggregateData(TestCase): def test_duplicate_instrument_aggregate_data(self): exporter = InMemoryMetricExporter() reader = PeriodicExportingMetricReader(exporter, export_interval_millis=500) view = View( instrument_type=Counter, attribute_keys=[], aggregation=SumAggregation(), ) provider = MeterProvider( metric_readers=[reader], resource=Resource.create(), views=[view], ) meter_0 = provider.get_meter( name="meter_0", version="version", schema_url="schema_url", ) meter_1 = provider.get_meter( name="meter_1", version="version", schema_url="schema_url", ) counter_0_0 = meter_0.create_counter("counter", unit="unit", description="description") counter_0_1 = meter_0.create_counter("counter", unit="unit", description="description") counter_1_0 = meter_1.create_counter("counter", unit="unit", description="description") self.assertIs(counter_0_0, counter_0_1) self.assertIsNot(counter_0_0, counter_1_0) counter_0_0.add(1, {}) counter_0_1.add(2, {}) with self.assertLogs(level=WARNING): counter_1_0.add(7, {}) sleep(1) reader.shutdown() sleep(1) metrics = exporter.metrics[0] scope_metrics = metrics.resource_metrics[0].scope_metrics self.assertEqual(len(scope_metrics), 2) metric_0 = scope_metrics[0].metrics[0] self.assertEqual(metric_0.name, "counter") self.assertEqual(metric_0.unit, "unit") self.assertEqual(metric_0.description, "description") self.assertEqual(next(iter(metric_0.data.data_points)).value, 3) metric_1 = scope_metrics[1].metrics[0] self.assertEqual(metric_1.name, "counter") self.assertEqual(metric_1.unit, "unit") self.assertEqual(metric_1.description, "description") self.assertEqual(next(iter(metric_1.data.data_points)).value, 7) class TestMeasurement(TestCase): def test_measurement_attributes_cleaned(self): measurement = Measurement( value=10, time_unix_nano=0, instrument=Mock(), context=Mock(), attributes={"a": "b", 1: 2, "seq": [1, 2]}, ) self.assertEqual( measurement.attributes, {"a": "b", "1": 2, "seq": (1, 2)}, ) def test_measurement_attributes_invalid_type(self): with self.assertLogs(level=WARNING) as logs: measurement = Measurement( value=10, time_unix_nano=0, instrument=Mock(), context=Mock(), attributes="invalid", ) self.assertIsNone(measurement.attributes) self.assertIn("Invalid type", logs.output[0]) def test_measurement_attributes_none_or_empty(self): m1 = Measurement( value=10, time_unix_nano=0, instrument=Mock(), context=Mock(), attributes=None, ) self.assertIsNone(m1.attributes) m2 = Measurement( value=10, time_unix_nano=0, instrument=Mock(), context=Mock(), attributes={}, ) self.assertEqual(m2.attributes, {}) python-opentelemetry-1.45.2/opentelemetry-sdk/tests/metrics/test_periodic_exporting_metric_reader.py000066400000000000000000000312021527070070700344770ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # pylint: disable=protected-access,invalid-name,no-self-use import gc import math import os import sys import weakref from logging import WARNING from time import sleep, time_ns from typing import cast from unittest.mock import Mock, patch import pytest from opentelemetry.sdk.environment_variables import ( OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED, ) from opentelemetry.sdk.metrics import ( Counter, MeterProvider, MetricsTimeoutError, ) from opentelemetry.sdk.metrics._internal import _Counter from opentelemetry.sdk.metrics._internal.point import ( HistogramDataPoint, MetricsData, ResourceMetrics, ScopeMetrics, ) from opentelemetry.sdk.metrics.export import ( AggregationTemporality, Gauge, Metric, MetricExporter, MetricExportResult, NumberDataPoint, PeriodicExportingMetricReader, Sum, ) from opentelemetry.sdk.metrics.view import ( DefaultAggregation, LastValueAggregation, ) from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.util.instrumentation import InstrumentationScope from opentelemetry.test.concurrency_test import ConcurrencyTestBase class FakeMetricsExporter(MetricExporter): def __init__(self, wait=0, preferred_temporality=None, preferred_aggregation=None): self.wait = wait self.metrics: list[MetricsData] = [] self._shutdown = False super().__init__( preferred_temporality=preferred_temporality, preferred_aggregation=preferred_aggregation, ) def export( self, metrics_data: MetricsData, timeout_millis: float = 10_000, **kwargs, ) -> MetricExportResult: sleep(self.wait) self.metrics.append(metrics_data) return MetricExportResult.SUCCESS def shutdown(self, timeout_millis: float = 30_000, **kwargs) -> None: self._shutdown = True def force_flush(self, timeout_millis: float = 10_000) -> bool: return True class ExceptionAtCollectionPeriodicExportingMetricReader(PeriodicExportingMetricReader): def __init__( self, exporter: MetricExporter, exception: Exception, export_interval_millis: float | None = None, export_timeout_millis: float | None = None, ) -> None: super().__init__(exporter, export_interval_millis, export_timeout_millis) self._collect_exception = exception # pylint: disable=overridden-final-method def collect(self, timeout_millis: float = 10_000) -> None: raise self._collect_exception metrics_list = [ Metric( name="sum_name", description="", unit="", data=Sum( data_points=[ NumberDataPoint( attributes={}, start_time_unix_nano=time_ns(), time_unix_nano=time_ns(), value=2, ) ], aggregation_temporality=1, is_monotonic=True, ), ), Metric( name="gauge_name", description="", unit="", data=Gauge( data_points=[ NumberDataPoint( attributes={}, start_time_unix_nano=time_ns(), time_unix_nano=time_ns(), value=2, ) ] ), ), ] metrics = MetricsData( resource_metrics=[ ResourceMetrics( scope_metrics=[ ScopeMetrics( metrics=metrics_list, scope=InstrumentationScope(name="test"), schema_url="", ) ], resource=Resource.create(), schema_url="", ) ] ) class TestPeriodicExportingMetricReader(ConcurrencyTestBase): def test_defaults(self): pmr = PeriodicExportingMetricReader(FakeMetricsExporter()) self.assertEqual(pmr._export_interval_millis, 60000) self.assertEqual(pmr._export_timeout_millis, 30000) with self.assertLogs(level=WARNING): pmr.shutdown() def _create_periodic_reader( self, metrics_data: MetricsData, exporter, collect_wait=0, interval=60000, timeout=30000, ): pmr = PeriodicExportingMetricReader( exporter, export_interval_millis=interval, export_timeout_millis=timeout, ) def _collect(reader, timeout_millis): sleep(collect_wait) return metrics_data pmr._set_collect_callback(_collect) return pmr def test_ticker_called(self): collect_mock = Mock() exporter = FakeMetricsExporter() exporter.export = Mock() pmr = PeriodicExportingMetricReader(exporter, export_interval_millis=1) pmr._set_collect_callback(collect_mock) sleep(0.1) self.assertTrue(collect_mock.assert_called_once) pmr.shutdown() def test_ticker_not_called_on_infinity(self): collect_mock = Mock() exporter = FakeMetricsExporter() exporter.export = Mock() pmr = PeriodicExportingMetricReader(exporter, export_interval_millis=math.inf) pmr._set_collect_callback(collect_mock) sleep(0.1) self.assertTrue(collect_mock.assert_not_called) pmr.shutdown() def test_ticker_value_exception_on_zero(self): exporter = FakeMetricsExporter() exporter.export = Mock() self.assertRaises( ValueError, PeriodicExportingMetricReader, exporter, export_interval_millis=0, ) def test_ticker_value_exception_on_negative(self): exporter = FakeMetricsExporter() exporter.export = Mock() self.assertRaises( ValueError, PeriodicExportingMetricReader, exporter, export_interval_millis=-100, ) @pytest.mark.flaky(max_runs=3, min_passes=1) def test_ticker_collects_metrics(self): exporter = FakeMetricsExporter() pmr = self._create_periodic_reader(metrics, exporter, interval=100) sleep(0.15) self.assertEqual(exporter.metrics[0], metrics) pmr.shutdown() def test_shutdown(self): exporter = FakeMetricsExporter() pmr = self._create_periodic_reader(MetricsData(resource_metrics=[]), exporter) pmr.shutdown() self.assertEqual(exporter.metrics[0], MetricsData(resource_metrics=[])) self.assertTrue(pmr._shutdown) self.assertTrue(exporter._shutdown) def test_shutdown_multiple_times(self): pmr = self._create_periodic_reader(MetricsData(resource_metrics=[]), FakeMetricsExporter()) with self.assertLogs(level="WARNING") as w: self.run_with_many_threads(pmr.shutdown) self.assertTrue("Can't shutdown multiple times" in w.output[0]) with self.assertLogs(level="WARNING") as w: pmr.shutdown() def test_exporter_temporality_preference(self): exporter = FakeMetricsExporter( preferred_temporality={ Counter: AggregationTemporality.DELTA, }, ) pmr = PeriodicExportingMetricReader(exporter) for key, value in pmr._instrument_class_temporality.items(): if key is not _Counter: self.assertEqual(value, AggregationTemporality.CUMULATIVE) else: self.assertEqual(value, AggregationTemporality.DELTA) def test_exporter_aggregation_preference(self): exporter = FakeMetricsExporter( preferred_aggregation={ Counter: LastValueAggregation(), }, ) pmr = PeriodicExportingMetricReader(exporter) for key, value in pmr._instrument_class_aggregation.items(): if key is not _Counter: self.assertTrue(isinstance(value, DefaultAggregation)) else: self.assertTrue(isinstance(value, LastValueAggregation)) def test_metric_timeout_does_not_kill_worker_thread(self): exporter = FakeMetricsExporter() pmr = ExceptionAtCollectionPeriodicExportingMetricReader( exporter, MetricsTimeoutError("test timeout"), export_timeout_millis=1, ) sleep(0.1) self.assertTrue(pmr._daemon_thread.is_alive()) pmr.shutdown() def test_metric_exporer_gc(self): # Given a PeriodicExportingMetricReader exporter = FakeMetricsExporter( preferred_aggregation={ Counter: LastValueAggregation(), }, ) processor = PeriodicExportingMetricReader(exporter) weak_ref = weakref.ref(processor) processor.shutdown() # When we garbage collect the reader del processor gc.collect() # Then the reference to the reader should no longer exist self.assertIsNone( weak_ref(), "The PeriodicExportingMetricReader object created by this test wasn't garbage collected", ) @pytest.mark.skipif( not hasattr(os, "fork") or not hasattr(os, "register_at_fork"), reason="needs fork and register_at_fork", ) def test_garbage_collected_processor_does_not_crash_on_fork(self): exporter = FakeMetricsExporter( preferred_aggregation={ Counter: LastValueAggregation(), }, ) processor = PeriodicExportingMetricReader(exporter) weak_ref = weakref.ref(processor) processor.shutdown() del processor gc.collect() self.assertIsNone(weak_ref()) # The bug causes an unraisable exception to be printed to stderr. # We redirect stderr to a pipe before fork to capture it. r_fd, w_fd = os.pipe() # Save original stderr and redirect it to the pipe # We also temporarily restore the default sys.unraisablehook. Pytest overrides # it to capture exceptions in memory, which would be lost on os._exit(0). old_fd = os.dup(sys.stderr.fileno()) old_hook = sys.unraisablehook try: os.dup2(w_fd, sys.stderr.fileno()) sys.unraisablehook = sys.__unraisablehook__ pid = os.fork() if pid == 0: os.close(r_fd) # os.fork() has already run the at_fork hooks. os._exit(0) _, status = os.waitpid(pid, 0) self.assertEqual(status, 0) finally: # Restore original stderr and hook in parent sys.unraisablehook = old_hook os.dup2(old_fd, sys.stderr.fileno()) os.close(old_fd) os.close(w_fd) os.set_blocking(r_fd, False) child_stderr = b"" while True: try: chunk = os.read(r_fd, 4096) if not chunk: break child_stderr += chunk except BlockingIOError: break child_stderr = child_stderr.decode("utf-8") os.close(r_fd) self.assertNotIn("TypeError", child_stderr) @patch.dict("os.environ", {OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED: "true"}) def test_metric_reader_metrics(self): exporter = FakeMetricsExporter() pmr = PeriodicExportingMetricReader(exporter, export_interval_millis=100000) mp = MeterProvider(metric_readers=[pmr]) counter = mp.get_meter("test").create_counter("test_counter") counter.add(1) mp.force_flush() self.assertEqual(len(exporter.metrics), 1) # Need a second collection to get the metric we recorded during first collection exporter.metrics.clear() mp.force_flush() self.assertEqual(len(exporter.metrics), 1) metric_data = exporter.metrics[0] scope_metrics = [ sm for sm in metric_data.resource_metrics[0].scope_metrics if sm.scope.name == "opentelemetry-sdk" ] self.assertEqual(len(scope_metrics), 1) reader_metrics = [m for m in scope_metrics[0].metrics if m.name == "otel.sdk.metric_reader.collection.duration"] self.assertEqual(len(reader_metrics), 1) metric = reader_metrics[0] point = metric.data.data_points[0] histogram = cast(HistogramDataPoint, point) self.assertEqual(histogram.count, 1) attrs = histogram.attributes assert attrs is not None self.assertEqual(attrs["otel.component.type"], "periodic_metric_reader") name = attrs["otel.component.name"] assert isinstance(name, str) self.assertTrue(name.startswith("periodic_metric_reader/")) mp.shutdown() python-opentelemetry-1.45.2/opentelemetry-sdk/tests/metrics/test_point.py000066400000000000000000000252301527070070700270520ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from unittest import TestCase from opentelemetry.sdk.metrics.export import ( AggregationTemporality, Buckets, ExponentialHistogram, ExponentialHistogramDataPoint, Gauge, Histogram, HistogramDataPoint, Metric, MetricsData, NumberDataPoint, ResourceMetrics, ScopeMetrics, Sum, ) from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.util.instrumentation import InstrumentationScope class TestToJson(TestCase): @classmethod def setUpClass(cls): cls.attributes_0 = { "a": "b", "b": True, "c": 1, "d": 1.1, "e": ["a", "b"], "f": [True, False], "g": [1, 2], "h": [1.1, 2.2], } cls.attributes_0_str = ( '{"a": "b", "b": true, "c": 1, "d": 1.1, "e": ["a", "b"], "f": [true, false], "g": [1, 2], "h": [1.1, 2.2]}' ) cls.attributes_1 = { "i": "a", "j": False, "k": 2, "l": 2.2, "m": ["b", "a"], "n": [False, True], "o": [2, 1], "p": [2.2, 1.1], } cls.attributes_1_str = '{"i": "a", "j": false, "k": 2, "l": 2.2, "m": ["b", "a"], "n": [false, true], "o": [2, 1], "p": [2.2, 1.1]}' cls.number_data_point_0 = NumberDataPoint( attributes=cls.attributes_0, start_time_unix_nano=1, time_unix_nano=2, value=3.3, ) cls.number_data_point_0_str = f'{{"attributes": {cls.attributes_0_str}, "start_time_unix_nano": 1, "time_unix_nano": 2, "value": 3.3, "exemplars": []}}' cls.number_data_point_1 = NumberDataPoint( attributes=cls.attributes_1, start_time_unix_nano=2, time_unix_nano=3, value=4.4, ) cls.number_data_point_1_str = f'{{"attributes": {cls.attributes_1_str}, "start_time_unix_nano": 2, "time_unix_nano": 3, "value": 4.4, "exemplars": []}}' cls.histogram_data_point_0 = HistogramDataPoint( attributes=cls.attributes_0, start_time_unix_nano=1, time_unix_nano=2, count=3, sum=3.3, bucket_counts=[1, 1, 1], explicit_bounds=[0.1, 1.2, 2.3, 3.4], min=0.2, max=3.3, ) cls.histogram_data_point_0_str = f'{{"attributes": {cls.attributes_0_str}, "start_time_unix_nano": 1, "time_unix_nano": 2, "count": 3, "sum": 3.3, "bucket_counts": [1, 1, 1], "explicit_bounds": [0.1, 1.2, 2.3, 3.4], "min": 0.2, "max": 3.3, "exemplars": []}}' cls.histogram_data_point_1 = HistogramDataPoint( attributes=cls.attributes_1, start_time_unix_nano=2, time_unix_nano=3, count=4, sum=4.4, bucket_counts=[2, 1, 1], explicit_bounds=[1.2, 2.3, 3.4, 4.5], min=0.3, max=4.4, ) cls.histogram_data_point_1_str = f'{{"attributes": {cls.attributes_1_str}, "start_time_unix_nano": 2, "time_unix_nano": 3, "count": 4, "sum": 4.4, "bucket_counts": [2, 1, 1], "explicit_bounds": [1.2, 2.3, 3.4, 4.5], "min": 0.3, "max": 4.4, "exemplars": []}}' cls.exp_histogram_data_point_0 = ExponentialHistogramDataPoint( attributes=cls.attributes_0, start_time_unix_nano=1, time_unix_nano=2, count=1, sum=10, scale=1, zero_count=0, positive=Buckets(offset=0, bucket_counts=[1]), negative=Buckets(offset=0, bucket_counts=[0]), flags=0, min=10, max=10, ) cls.exp_histogram_data_point_0_str = f'{{"attributes": {cls.attributes_0_str}, "start_time_unix_nano": 1, "time_unix_nano": 2, "count": 1, "sum": 10, "scale": 1, "zero_count": 0, "positive": {{"offset": 0, "bucket_counts": [1]}}, "negative": {{"offset": 0, "bucket_counts": [0]}}, "flags": 0, "min": 10, "max": 10, "exemplars": []}}' cls.sum_0 = Sum( data_points=[cls.number_data_point_0, cls.number_data_point_1], aggregation_temporality=AggregationTemporality.DELTA, is_monotonic=False, ) cls.sum_0_str = f'{{"data_points": [{cls.number_data_point_0_str}, {cls.number_data_point_1_str}], "aggregation_temporality": 1, "is_monotonic": false}}' cls.gauge_0 = Gauge( data_points=[cls.number_data_point_0, cls.number_data_point_1], ) cls.gauge_0_str = f'{{"data_points": [{cls.number_data_point_0_str}, {cls.number_data_point_1_str}]}}' cls.histogram_0 = Histogram( data_points=[ cls.histogram_data_point_0, cls.histogram_data_point_1, ], aggregation_temporality=AggregationTemporality.DELTA, ) cls.histogram_0_str = f'{{"data_points": [{cls.histogram_data_point_0_str}, {cls.histogram_data_point_1_str}], "aggregation_temporality": 1}}' cls.exp_histogram_0 = ExponentialHistogram( data_points=[ cls.exp_histogram_data_point_0, ], aggregation_temporality=AggregationTemporality.CUMULATIVE, ) cls.exp_histogram_0_str = ( f'{{"data_points": [{cls.exp_histogram_data_point_0_str}], "aggregation_temporality": 2}}' ) cls.metric_0 = Metric( name="metric_0", description="description_0", unit="unit_0", data=cls.sum_0, ) cls.metric_0_str = ( f'{{"name": "metric_0", "description": "description_0", "unit": "unit_0", "data": {cls.sum_0_str}}}' ) cls.metric_1 = Metric(name="metric_1", description=None, unit="unit_1", data=cls.gauge_0) cls.metric_1_str = f'{{"name": "metric_1", "description": "", "unit": "unit_1", "data": {cls.gauge_0_str}}}' cls.metric_2 = Metric( name="metric_2", description="description_2", unit=None, data=cls.histogram_0, ) cls.metric_2_str = ( f'{{"name": "metric_2", "description": "description_2", "unit": "", "data": {cls.histogram_0_str}}}' ) cls.scope_metrics_0 = ScopeMetrics( scope=InstrumentationScope( name="name_0", version="version_0", schema_url="schema_url_0", ), metrics=[cls.metric_0, cls.metric_1, cls.metric_2], schema_url="schema_url_0", ) cls.scope_metrics_0_str = f'{{"scope": {{"name": "name_0", "version": "version_0", "schema_url": "schema_url_0", "attributes": null}}, "metrics": [{cls.metric_0_str}, {cls.metric_1_str}, {cls.metric_2_str}], "schema_url": "schema_url_0"}}' cls.scope_metrics_1 = ScopeMetrics( scope=InstrumentationScope( name="name_1", version="version_1", schema_url="schema_url_1", ), metrics=[cls.metric_0, cls.metric_1, cls.metric_2], schema_url="schema_url_1", ) cls.scope_metrics_1_str = f'{{"scope": {{"name": "name_1", "version": "version_1", "schema_url": "schema_url_1", "attributes": null}}, "metrics": [{cls.metric_0_str}, {cls.metric_1_str}, {cls.metric_2_str}], "schema_url": "schema_url_1"}}' cls.resource_metrics_0 = ResourceMetrics( resource=Resource(attributes=cls.attributes_0, schema_url="schema_url_0"), scope_metrics=[cls.scope_metrics_0, cls.scope_metrics_1], schema_url="schema_url_0", ) cls.resource_metrics_0_str = f'{{"resource": {{"attributes": {cls.attributes_0_str}, "schema_url": "schema_url_0"}}, "scope_metrics": [{cls.scope_metrics_0_str}, {cls.scope_metrics_1_str}], "schema_url": "schema_url_0"}}' cls.resource_metrics_1 = ResourceMetrics( resource=Resource(attributes=cls.attributes_1, schema_url="schema_url_1"), scope_metrics=[cls.scope_metrics_0, cls.scope_metrics_1], schema_url="schema_url_1", ) cls.resource_metrics_1_str = f'{{"resource": {{"attributes": {cls.attributes_1_str}, "schema_url": "schema_url_1"}}, "scope_metrics": [{cls.scope_metrics_0_str}, {cls.scope_metrics_1_str}], "schema_url": "schema_url_1"}}' cls.metrics_data_0 = MetricsData(resource_metrics=[cls.resource_metrics_0, cls.resource_metrics_1]) cls.metrics_data_0_str = f'{{"resource_metrics": [{cls.resource_metrics_0_str}, {cls.resource_metrics_1_str}]}}' def test_number_data_point(self): self.assertEqual( self.number_data_point_0.to_json(indent=None), self.number_data_point_0_str, ) self.assertEqual( self.number_data_point_1.to_json(indent=None), self.number_data_point_1_str, ) def test_histogram_data_point(self): self.assertEqual( self.histogram_data_point_0.to_json(indent=None), self.histogram_data_point_0_str, ) self.assertEqual( self.histogram_data_point_1.to_json(indent=None), self.histogram_data_point_1_str, ) def test_exp_histogram_data_point(self): self.assertEqual( self.exp_histogram_data_point_0.to_json(indent=None), self.exp_histogram_data_point_0_str, ) def test_sum(self): self.assertEqual(self.sum_0.to_json(indent=None), self.sum_0_str) def test_gauge(self): self.assertEqual(self.gauge_0.to_json(indent=None), self.gauge_0_str) def test_histogram(self): self.assertEqual(self.histogram_0.to_json(indent=None), self.histogram_0_str) def test_exp_histogram(self): self.assertEqual(self.exp_histogram_0.to_json(indent=None), self.exp_histogram_0_str) def test_metric(self): self.assertEqual(self.metric_0.to_json(indent=None), self.metric_0_str) self.assertEqual(self.metric_1.to_json(indent=None), self.metric_1_str) self.assertEqual(self.metric_2.to_json(indent=None), self.metric_2_str) def test_scope_metrics(self): self.assertEqual(self.scope_metrics_0.to_json(indent=None), self.scope_metrics_0_str) self.assertEqual(self.scope_metrics_1.to_json(indent=None), self.scope_metrics_1_str) def test_resource_metrics(self): self.assertEqual( self.resource_metrics_0.to_json(indent=None), self.resource_metrics_0_str, ) self.assertEqual( self.resource_metrics_1.to_json(indent=None), self.resource_metrics_1_str, ) def test_metrics_data(self): self.assertEqual(self.metrics_data_0.to_json(indent=None), self.metrics_data_0_str) python-opentelemetry-1.45.2/opentelemetry-sdk/tests/metrics/test_view.py000066400000000000000000000106751527070070700267020ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # pylint: disable=protected-access from unittest import TestCase from unittest.mock import Mock from opentelemetry.sdk.metrics.view import View class TestView(TestCase): def test_required_instrument_criteria(self): with self.assertRaises(Exception): View() def test_aggregation_cardinality_limit(self): self.assertIsNone(View(instrument_name="a")._aggregation_cardinality_limit) self.assertEqual(View(instrument_name="a", aggregation_cardinality_limit=5)._aggregation_cardinality_limit, 5) def test_invalid_aggregation_cardinality_limit(self): for limit in (0, -1, 1.5, "5", True): with self.subTest(limit=limit): with self.assertRaises(ValueError): View(instrument_name="a", aggregation_cardinality_limit=limit) def test_instrument_type(self): self.assertTrue(View(instrument_type=Mock)._match(Mock())) def test_instrument_name(self): mock_instrument = Mock() mock_instrument.configure_mock(name="instrument_name") self.assertTrue(View(instrument_name="instrument_name")._match(mock_instrument)) def test_instrument_name_case_insensitive(self): # The SDK stores instrument names lower-cased, so a view pattern that # reuses the instrument's original name must still match regardless of # case (and regardless of the host platform). mock_instrument = Mock() mock_instrument.configure_mock(name="instrument_name") self.assertTrue(View(instrument_name="Instrument_Name")._match(mock_instrument)) self.assertTrue(View(instrument_name="INSTRUMENT_*")._match(mock_instrument)) self.assertFalse(View(instrument_name="other_name")._match(mock_instrument)) def test_instrument_unit(self): mock_instrument = Mock() mock_instrument.configure_mock(unit="instrument_unit") self.assertTrue(View(instrument_unit="instrument_unit")._match(mock_instrument)) def test_instrument_unit_case_sensitive(self): # Units are case-sensitive and matching must not depend on the host # platform's filename case sensitivity. mock_instrument = Mock() mock_instrument.configure_mock(unit="By") self.assertTrue(View(instrument_unit="By")._match(mock_instrument)) self.assertFalse(View(instrument_unit="by")._match(mock_instrument)) def test_meter_name(self): self.assertTrue(View(meter_name="meter_name")._match(Mock(**{"instrumentation_scope.name": "meter_name"}))) def test_meter_version(self): self.assertTrue( View(meter_version="meter_version")._match(Mock(**{"instrumentation_scope.version": "meter_version"})) ) def test_meter_schema_url(self): self.assertTrue( View(meter_schema_url="meter_schema_url")._match( Mock(**{"instrumentation_scope.schema_url": "meter_schema_url"}) ) ) self.assertFalse( View(meter_schema_url="meter_schema_url")._match( Mock(**{"instrumentation_scope.schema_url": "meter_schema_urlabc"}) ) ) self.assertTrue( View(meter_schema_url="meter_schema_url")._match( Mock(**{"instrumentation_scope.schema_url": "meter_schema_url"}) ) ) def test_additive_criteria(self): view = View( meter_name="meter_name", meter_version="meter_version", meter_schema_url="meter_schema_url", ) self.assertTrue( view._match( Mock( **{ "instrumentation_scope.name": "meter_name", "instrumentation_scope.version": "meter_version", "instrumentation_scope.schema_url": "meter_schema_url", } ) ) ) self.assertFalse( view._match( Mock( **{ "instrumentation_scope.name": "meter_name", "instrumentation_scope.version": "meter_version", "instrumentation_scope.schema_url": "meter_schema_vrl", } ) ) ) def test_view_name(self): with self.assertRaises(Exception): View(name="name", instrument_name="instrument_name*") python-opentelemetry-1.45.2/opentelemetry-sdk/tests/metrics/test_view_instrument_match.py000066400000000000000000000776071527070070700323560ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # pylint: disable=protected-access from __future__ import annotations from collections.abc import Callable, Sequence from time import time_ns from unittest import TestCase from unittest.mock import MagicMock, Mock, patch from opentelemetry.context import Context from opentelemetry.sdk.metrics._internal._view_instrument_match import ( _hash_attributes, _ViewInstrumentMatch, ) from opentelemetry.sdk.metrics._internal.aggregation import ( _Aggregation, _DropAggregation, _ExplicitBucketHistogramAggregation, _LastValueAggregation, ) from opentelemetry.sdk.metrics._internal.exemplar import ( AlignedHistogramBucketExemplarReservoir, ExemplarReservoirBuilder, SimpleFixedSizeExemplarReservoir, ) from opentelemetry.sdk.metrics._internal.instrument import _Counter, _Histogram from opentelemetry.sdk.metrics._internal.measurement import Measurement from opentelemetry.sdk.metrics._internal.sdk_configuration import ( SdkConfiguration, ) from opentelemetry.sdk.metrics._internal.view import _default_reservoir_factory from opentelemetry.sdk.metrics.export import AggregationTemporality from opentelemetry.sdk.metrics.view import ( DefaultAggregation, DropAggregation, LastValueAggregation, View, ) def generalized_reservoir_factory( size: int = 1, boundaries: Sequence[float] | None = None ) -> Callable[[type[_Aggregation]], ExemplarReservoirBuilder]: def factory( aggregation_type: type[_Aggregation], ) -> ExemplarReservoirBuilder: if issubclass(aggregation_type, _ExplicitBucketHistogramAggregation): return lambda **kwargs: AlignedHistogramBucketExemplarReservoir( boundaries=boundaries or [], **{k: v for k, v in kwargs.items() if k != "boundaries"}, ) return lambda **kwargs: SimpleFixedSizeExemplarReservoir( size=size, **{k: v for k, v in kwargs.items() if k != "size"}, ) return factory class Test_ViewInstrumentMatch(TestCase): # pylint: disable=invalid-name @classmethod def setUpClass(cls): cls.mock_aggregation_factory = Mock() cls.mock_created_aggregation = cls.mock_aggregation_factory._create_aggregation() cls.mock_resource = Mock() cls.mock_instrumentation_scope = Mock() cls.sdk_configuration = SdkConfiguration( exemplar_filter=Mock(), resource=cls.mock_resource, views=[], ) def test_consume_measurement(self): instrument1 = Mock(name="instrument1") instrument1.instrumentation_scope = self.mock_instrumentation_scope view_instrument_match = _ViewInstrumentMatch( view=View( instrument_name="instrument1", name="name", aggregation=self.mock_aggregation_factory, attribute_keys={"a", "c"}, ), instrument=instrument1, instrument_class_aggregation=MagicMock(**{"__getitem__.return_value": DefaultAggregation()}), ) view_instrument_match.consume_measurement( Measurement( value=0, time_unix_nano=time_ns(), instrument=instrument1, context=Context(), attributes={"c": "d", "f": "g"}, ) ) self.assertEqual( view_instrument_match._attributes_aggregation, {_hash_attributes({"c": "d"}): self.mock_created_aggregation}, ) view_instrument_match.consume_measurement( Measurement( value=0, time_unix_nano=time_ns(), instrument=instrument1, context=Context(), attributes={"w": "x", "y": "z"}, ) ) self.assertEqual( view_instrument_match._attributes_aggregation, { _hash_attributes({}): self.mock_created_aggregation, _hash_attributes({"c": "d"}): self.mock_created_aggregation, }, ) # setup new instrument match without `attribute_keys` set, defaults to keeping # all attributes. view_instrument_match = _ViewInstrumentMatch( view=View( instrument_name="instrument1", name="name", aggregation=self.mock_aggregation_factory, ), instrument=instrument1, instrument_class_aggregation=MagicMock(**{"__getitem__.return_value": DefaultAggregation()}), ) view_instrument_match.consume_measurement( Measurement( value=0, time_unix_nano=time_ns(), instrument=instrument1, context=Context(), attributes={"c": "d", "f": "g"}, ) ) self.assertEqual( view_instrument_match._attributes_aggregation, {_hash_attributes({"c": "d", "f": "g"}): self.mock_created_aggregation}, ) # empty set attribute_keys will drop all attributes and aggregate # everything together view_instrument_match = _ViewInstrumentMatch( view=View( instrument_name="instrument1", name="name", aggregation=self.mock_aggregation_factory, attribute_keys={}, ), instrument=instrument1, instrument_class_aggregation=MagicMock(**{"__getitem__.return_value": DefaultAggregation()}), ) view_instrument_match.consume_measurement( Measurement( value=0, time_unix_nano=time_ns(), instrument=instrument1, context=Context(), attributes={"a": 1, "b": 2}, ) ) self.assertEqual( view_instrument_match._attributes_aggregation, {_hash_attributes({}): self.mock_created_aggregation}, ) # Test that a drop aggregation is handled in the same way as any # other aggregation. drop_aggregation = DropAggregation() view_instrument_match = _ViewInstrumentMatch( view=View( instrument_name="instrument1", name="name", aggregation=drop_aggregation, attribute_keys={}, ), instrument=instrument1, instrument_class_aggregation=MagicMock(**{"__getitem__.return_value": DefaultAggregation()}), ) view_instrument_match.consume_measurement( Measurement( value=0, time_unix_nano=time_ns(), instrument=instrument1, context=Context(), attributes=None, ) ) self.assertIsInstance( view_instrument_match._attributes_aggregation[_hash_attributes({})], _DropAggregation, ) def test_collect(self): instrument1 = _Counter( "instrument1", Mock(), Mock(), description="description", unit="unit", ) instrument1.instrumentation_scope = self.mock_instrumentation_scope view_instrument_match = _ViewInstrumentMatch( view=View( instrument_name="instrument1", name="name", aggregation=DefaultAggregation(), attribute_keys={"a", "c"}, ), instrument=instrument1, instrument_class_aggregation=MagicMock(**{"__getitem__.return_value": DefaultAggregation()}), ) view_instrument_match.consume_measurement( Measurement( value=0, time_unix_nano=time_ns(), instrument=Mock(name="instrument1"), context=Context(), attributes={"c": "d", "f": "g"}, ) ) number_data_points = view_instrument_match.collect(AggregationTemporality.CUMULATIVE, 0) number_data_points = list(number_data_points) self.assertEqual(len(number_data_points), 1) number_data_point = number_data_points[0] self.assertEqual(number_data_point.attributes, {"c": "d"}) self.assertEqual(number_data_point.value, 0) def test_consume_measurement_attributes_are_copied(self): """Mutating the attributes dict after recording must not affect stored data points.""" # Note mutating mutable objects in the attributes dict will still cause issues, # as they are not deep copied. instrument1 = _Counter( "instrument1", Mock(), Mock(), description="description", unit="unit", ) instrument1.instrumentation_scope = self.mock_instrumentation_scope view_instrument_match = _ViewInstrumentMatch( view=View( instrument_name="instrument1", name="name", aggregation=DefaultAggregation(), ), instrument=instrument1, instrument_class_aggregation=MagicMock(**{"__getitem__.return_value": DefaultAggregation()}), ) attributes = {"key": "original"} view_instrument_match.consume_measurement( Measurement( value=1, time_unix_nano=time_ns(), instrument=instrument1, context=Context(), attributes=attributes, ) ) # Mutate the original dict after recording attributes["key"] = "mutated" number_data_points = view_instrument_match.collect(AggregationTemporality.CUMULATIVE, 0) number_data_points = list(number_data_points) self.assertEqual(len(number_data_points), 1) self.assertEqual( number_data_points[0].attributes, {"key": "original"}, ) @patch( "opentelemetry.sdk.metrics._internal._view_instrument_match.time_ns", side_effect=[0, 1, 2], ) def test_collect_resets_start_time_unix_nano(self, mock_time_ns): instrument = Mock(name="instrument") instrument.instrumentation_scope = self.mock_instrumentation_scope view_instrument_match = _ViewInstrumentMatch( view=View( instrument_name="instrument", name="name", aggregation=self.mock_aggregation_factory, ), instrument=instrument, instrument_class_aggregation=MagicMock(**{"__getitem__.return_value": DefaultAggregation()}), ) start_time_unix_nano = 0 self.assertEqual(mock_time_ns.call_count, 0) # +1 call to _create_aggregation view_instrument_match.consume_measurement( Measurement( value=0, time_unix_nano=time_ns(), instrument=instrument, attributes={"foo": "bar0"}, context=Context(), ) ) view_instrument_match._view._aggregation._create_aggregation.assert_called_with( instrument, {"foo": "bar0"}, _default_reservoir_factory, start_time_unix_nano, ) collection_start_time_unix_nano = time_ns() collected_data_points = view_instrument_match.collect( AggregationTemporality.CUMULATIVE, collection_start_time_unix_nano ) self.assertIsNotNone(collected_data_points) self.assertEqual(len(collected_data_points), 1) # +1 call to _create_aggregation view_instrument_match.consume_measurement( Measurement( value=0, time_unix_nano=time_ns(), instrument=instrument, attributes={"foo": "bar1"}, context=Context(), ) ) view_instrument_match._view._aggregation._create_aggregation.assert_called_with( instrument, {"foo": "bar1"}, _default_reservoir_factory, 1 ) collection_start_time_unix_nano = time_ns() collected_data_points = view_instrument_match.collect( AggregationTemporality.CUMULATIVE, collection_start_time_unix_nano ) self.assertIsNotNone(collected_data_points) self.assertEqual(len(collected_data_points), 2) collected_data_points = view_instrument_match.collect( AggregationTemporality.CUMULATIVE, collection_start_time_unix_nano ) # +1 call to create_aggregation view_instrument_match.consume_measurement( Measurement( value=0, time_unix_nano=time_ns(), instrument=instrument, attributes={"foo": "bar"}, context=Context(), ) ) view_instrument_match._view._aggregation._create_aggregation.assert_called_with( instrument, {"foo": "bar"}, _default_reservoir_factory, 2 ) # No new calls to _create_aggregation because attributes remain the same view_instrument_match.consume_measurement( Measurement( value=0, time_unix_nano=time_ns(), instrument=instrument, attributes={"foo": "bar"}, context=Context(), ) ) view_instrument_match.consume_measurement( Measurement( value=0, time_unix_nano=time_ns(), instrument=instrument, attributes={"foo": "bar"}, context=Context(), ) ) # In total we have 5 calls for _create_aggregation # 1 from the _ViewInstrumentMatch initialization and 4 # from the consume_measurement calls with different attributes self.assertEqual( view_instrument_match._view._aggregation._create_aggregation.call_count, 5, ) def test_data_point_check(self): instrument1 = _Counter( "instrument1", Mock(), Mock(), description="description", unit="unit", ) instrument1.instrumentation_scope = self.mock_instrumentation_scope view_instrument_match = _ViewInstrumentMatch( view=View( instrument_name="instrument1", name="name", aggregation=DefaultAggregation(), ), instrument=instrument1, instrument_class_aggregation=MagicMock( **{ "__getitem__.return_value": Mock( **{ "_create_aggregation.return_value": Mock( **{ "collect.side_effect": [ Mock(), Mock(), None, Mock(), ] } ) } ) } ), ) view_instrument_match.consume_measurement( Measurement( value=0, time_unix_nano=time_ns(), instrument=Mock(name="instrument1"), context=Context(), attributes={"c": "d", "f": "g"}, ) ) view_instrument_match.consume_measurement( Measurement( value=0, time_unix_nano=time_ns(), instrument=Mock(name="instrument1"), context=Context(), attributes={"h": "i", "j": "k"}, ) ) view_instrument_match.consume_measurement( Measurement( value=0, time_unix_nano=time_ns(), instrument=Mock(name="instrument1"), context=Context(), attributes={"l": "m", "n": "o"}, ) ) view_instrument_match.consume_measurement( Measurement( value=0, time_unix_nano=time_ns(), instrument=Mock(name="instrument1"), context=Context(), attributes={"p": "q", "r": "s"}, ) ) result = view_instrument_match.collect(AggregationTemporality.CUMULATIVE, 0) self.assertEqual(len(list(result)), 3) def test_setting_aggregation(self): instrument1 = _Counter( name="instrument1", instrumentation_scope=Mock(), measurement_consumer=Mock(), description="description", unit="unit", ) instrument1.instrumentation_scope = self.mock_instrumentation_scope view_instrument_match = _ViewInstrumentMatch( view=View( instrument_name="instrument1", name="name", aggregation=DefaultAggregation(), attribute_keys={"a", "c"}, ), instrument=instrument1, instrument_class_aggregation={_Counter: LastValueAggregation()}, ) view_instrument_match.consume_measurement( Measurement( value=0, time_unix_nano=time_ns(), instrument=Mock(name="instrument1"), context=Context(), attributes={"c": "d", "f": "g"}, ) ) self.assertIsInstance( view_instrument_match._attributes_aggregation[_hash_attributes({"c": "d"})], _LastValueAggregation, ) def test_hash_attributes_works_as_stable_hash_key(self): attributes = { "a": [1, 2], "b": [2, 1], "c": b"1234asf", "d": 1.2324124, "e": -2.32323124, "f": {1: 2, 2: (1, 2, 3), 3: "a", 4: "bc"}, } self.assertEqual( _hash_attributes(attributes), _hash_attributes(attributes), ) self.assertNotEqual( _hash_attributes({"1": (1, "2", 3, "4")}), _hash_attributes({"1": ("1", 2, "3", 4)}), ) def test_hash_attributes_stable_hash_key_order_independence(self): attrs1 = {"b": 2, "a": 1, "c": 3} attrs2 = {"a": 1, "c": 3, "b": 2} self.assertEqual( _hash_attributes(attrs1), _hash_attributes(attrs2), ) nested1 = {"root": {"z": (3, 2, 1), "x": "val", "y": b"bytes_data"}} nested2 = {"root": {"y": b"bytes_data", "z": (3, 2, 1), "x": "val"}} self.assertEqual( _hash_attributes(nested1), _hash_attributes(nested2), ) def test_hash_attributes_stable_hash_with_tuples_and_nested_dicts(self): complex_attrs_a = { "service": {"name": "test-svc", "version": "1.0"}, "tags": ("prod", "us-east-1"), "endpoints": ( {"host": "a.com", "port": 80}, {"host": "b.com", "port": 443}, ), "payload": b"binary-content", } complex_attrs_b = { "payload": b"binary-content", "endpoints": ( {"port": 80, "host": "a.com"}, {"port": 443, "host": "b.com"}, ), "service": {"version": "1.0", "name": "test-svc"}, "tags": ("prod", "us-east-1"), } self.assertEqual( _hash_attributes(complex_attrs_a), _hash_attributes(complex_attrs_b), ) def test_hash_attributes_tuple_and_list_equivalence(self): attrs_tuple = {"values": (1, 2, 3), "nested": {"items": ("a", "b")}} attrs_list = {"values": [1, 2, 3], "nested": {"items": ["a", "b"]}} self.assertEqual( _hash_attributes(attrs_tuple), _hash_attributes(attrs_list), ) def test_consume_measurement_with_non_string_keys(self): instrument1 = _Counter( name="instrument1", instrumentation_scope=Mock(), measurement_consumer=Mock(), description="description", unit="unit", ) instrument1.instrumentation_scope = self.mock_instrumentation_scope view_instrument_match = _ViewInstrumentMatch( view=View( instrument_name="instrument1", name="name", aggregation=DefaultAggregation(), ), instrument=instrument1, instrument_class_aggregation={_Counter: LastValueAggregation()}, ) attributes = {"c": 1, 22: 3, (1,): 2} measurement = Measurement( value=0, time_unix_nano=time_ns(), instrument=Mock(name="instrument1"), context=Context(), attributes=attributes, ) view_instrument_match.consume_measurement(measurement) self.assertEqual( measurement.attributes, {"c": 1, "22": 3, "(1,)": 2}, ) self.assertIn( _hash_attributes(measurement.attributes), view_instrument_match._attributes_aggregation, ) def test_consume_measurement_stable_hash_key_order(self): instrument1 = _Counter( "instrument1", Mock(), Mock(), description="description", unit="unit", ) instrument1.instrumentation_scope = self.mock_instrumentation_scope view_instrument_match = _ViewInstrumentMatch( view=View( instrument_name="instrument1", name="name", aggregation=DefaultAggregation(), ), instrument=instrument1, instrument_class_aggregation=MagicMock(**{"__getitem__.return_value": DefaultAggregation()}), ) attrs1 = { "service": {"version": "1.0", "name": "app"}, "tags": ("prod", "v1"), "region": "us-east-1", } attrs2 = { "region": "us-east-1", "tags": ("prod", "v1"), "service": {"name": "app", "version": "1.0"}, } view_instrument_match.consume_measurement( Measurement( value=1, time_unix_nano=time_ns(), instrument=instrument1, context=Context(), attributes=attrs1, ) ) view_instrument_match.consume_measurement( Measurement( value=2, time_unix_nano=time_ns(), instrument=instrument1, context=Context(), attributes=attrs2, ) ) self.assertEqual(len(view_instrument_match._attributes_aggregation), 1) expected_key = _hash_attributes(attrs1) self.assertIn(expected_key, view_instrument_match._attributes_aggregation) class TestSimpleFixedSizeExemplarReservoir(TestCase): def test_consume_measurement_with_custom_reservoir_factory(self): simple_fixed_size_factory = generalized_reservoir_factory(size=10) # Create an instance of _Counter instrument1 = _Counter( name="instrument1", instrumentation_scope=None, measurement_consumer=None, description="description", unit="unit", ) view_instrument_match = _ViewInstrumentMatch( view=View( instrument_name="instrument1", name="name", aggregation=DefaultAggregation(), exemplar_reservoir_factory=simple_fixed_size_factory, ), instrument=instrument1, instrument_class_aggregation={_Counter: DefaultAggregation()}, ) # Consume measurements with the same attributes to ensure aggregation view_instrument_match.consume_measurement( Measurement( value=2.0, time_unix_nano=time_ns(), instrument=instrument1, context=Context(), attributes={"attribute1": "value1"}, ) ) view_instrument_match.consume_measurement( Measurement( value=4.0, time_unix_nano=time_ns(), instrument=instrument1, context=Context(), attributes={"attribute2": "value2"}, ) ) view_instrument_match.consume_measurement( Measurement( value=5.0, time_unix_nano=time_ns(), instrument=instrument1, context=Context(), attributes={"attribute2": "value2"}, ) ) data_points = list(view_instrument_match.collect(AggregationTemporality.CUMULATIVE, 0)) # Ensure only one data point is collected self.assertEqual(len(data_points), 2) # Verify that exemplars have been correctly stored and collected self.assertEqual(len(data_points[0].exemplars), 1) self.assertEqual(len(data_points[1].exemplars), 2) self.assertEqual(data_points[0].exemplars[0].value, 2.0) self.assertEqual(data_points[1].exemplars[0].value, 4.0) self.assertEqual(data_points[1].exemplars[1].value, 5.0) def test_consume_measurement_with_exemplars(self): # Create an instance of _Counter instrument1 = _Counter( name="instrument1", instrumentation_scope=None, # No mock, set to None or actual scope if available measurement_consumer=None, # No mock, set to None or actual consumer if available description="description", unit="unit", ) view_instrument_match = _ViewInstrumentMatch( view=View( instrument_name="instrument1", name="name", aggregation=DefaultAggregation(), ), instrument=instrument1, instrument_class_aggregation={_Counter: DefaultAggregation()}, ) # Consume measurements with the same attributes to ensure aggregation view_instrument_match.consume_measurement( Measurement( value=4.0, time_unix_nano=time_ns(), instrument=instrument1, context=Context(), attributes={"attribute2": "value2"}, ) ) view_instrument_match.consume_measurement( Measurement( value=5.0, time_unix_nano=time_ns(), instrument=instrument1, context=Context(), attributes={"attribute2": "value2"}, ) ) # Collect the data points data_points = list(view_instrument_match.collect(AggregationTemporality.CUMULATIVE, 0)) # Ensure only one data point is collected self.assertEqual(len(data_points), 1) # Verify that exemplars have been correctly stored and collected # As the default reservoir as only one bucket, it will retain # either one of the measurements based on random selection self.assertEqual(len(data_points[0].exemplars), 1) self.assertIn(data_points[0].exemplars[0].value, [4.0, 5.0]) def test_consume_measurement_with_exemplars_and_view_attributes_filter( self, ): value = 22 # Create an instance of _Counter instrument1 = _Counter( name="instrument1", instrumentation_scope=None, # No mock, set to None or actual scope if available measurement_consumer=None, # No mock, set to None or actual consumer if available ) view_instrument_match = _ViewInstrumentMatch( view=View( instrument_name="instrument1", name="name", attribute_keys={"X", "Y"}, ), instrument=instrument1, instrument_class_aggregation={_Counter: DefaultAggregation()}, ) view_instrument_match.consume_measurement( Measurement( value=value, time_unix_nano=time_ns(), instrument=instrument1, context=Context(), attributes={"X": "x-value", "Y": "y-value", "Z": "z-value"}, ) ) # Collect the data points data_points = list(view_instrument_match.collect(AggregationTemporality.CUMULATIVE, 0)) # Ensure only one data point is collected self.assertEqual(len(data_points), 1) # Verify that exemplars have been correctly stored and collected self.assertEqual(len(data_points[0].exemplars), 1) # Check the exemplar has the dropped attribute exemplar = list(data_points[0].exemplars)[0] self.assertEqual(exemplar.value, value) self.assertDictEqual(exemplar.filtered_attributes, {"Z": "z-value"}) class TestAlignedHistogramBucketExemplarReservoir(TestCase): def test_consume_measurement_with_custom_reservoir_factory(self): # Custom factory for AlignedHistogramBucketExemplarReservoir with specific boundaries histogram_reservoir_factory = generalized_reservoir_factory(boundaries=[0, 5, 10, 25]) # Create an instance of _Histogram instrument1 = _Histogram( name="instrument1", instrumentation_scope=None, measurement_consumer=None, description="description", unit="unit", ) view_instrument_match = _ViewInstrumentMatch( view=View( instrument_name="instrument1", name="name", aggregation=DefaultAggregation(), exemplar_reservoir_factory=histogram_reservoir_factory, ), instrument=instrument1, instrument_class_aggregation={_Histogram: DefaultAggregation()}, ) # Consume measurements with different values to ensure they are placed in the correct buckets view_instrument_match.consume_measurement( Measurement( value=2.0, # Should go into the first bucket (0 to 5) time_unix_nano=time_ns(), instrument=instrument1, context=Context(), attributes={"attribute1": "value1"}, ) ) view_instrument_match.consume_measurement( Measurement( value=7.0, # Should go into the second bucket (5 to 10) time_unix_nano=time_ns(), instrument=instrument1, context=Context(), attributes={"attribute2": "value2"}, ) ) view_instrument_match.consume_measurement( Measurement( value=8.0, # Should go into the second bucket (5 to 10) time_unix_nano=time_ns(), instrument=instrument1, context=Context(), attributes={"attribute2": "value2"}, ) ) view_instrument_match.consume_measurement( Measurement( value=15.0, # Should go into the third bucket (10 to 25) time_unix_nano=time_ns(), instrument=instrument1, context=Context(), attributes={"attribute3": "value3"}, ) ) # Collect the data points data_points = list(view_instrument_match.collect(AggregationTemporality.CUMULATIVE, 0)) # Ensure three data points are collected, one for each bucket self.assertEqual(len(data_points), 3) # Verify that exemplars have been correctly stored and collected in their respective buckets self.assertEqual(len(data_points[0].exemplars), 1) self.assertEqual(len(data_points[1].exemplars), 1) self.assertEqual(len(data_points[2].exemplars), 1) self.assertEqual(data_points[0].exemplars[0].value, 2.0) # First bucket self.assertEqual(data_points[1].exemplars[0].value, 8.0) # Second bucket self.assertEqual(data_points[2].exemplars[0].value, 15.0) # Third bucket python-opentelemetry-1.45.2/opentelemetry-sdk/tests/resources/000077500000000000000000000000001527070070700246525ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-sdk/tests/resources/__init__.py000066400000000000000000000000001527070070700267510ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-sdk/tests/resources/test_resource_bytes_attributes.py000066400000000000000000000032241527070070700335670ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 """`bytes` is a valid attribute value type, so a Resource carrying one must stay hashable and serialisable. Every OTLP encoder groups telemetry by using the Resource as a dict key, so a Resource that cannot be hashed takes the whole export path down with it. """ import json import unittest from opentelemetry.sdk.resources import Resource _BYTES_RESOURCE = {"service.name": "svc", "build.id": b"\x01\x02\x03"} class TestResourceWithBytesAttribute(unittest.TestCase): def setUp(self): self.resource = Resource.create(_BYTES_RESOURCE) def test_resource_is_hashable(self): hash(self.resource) def test_resource_usable_as_dict_key(self): """OTLP encoders group telemetry by Resource identity.""" self.assertEqual({self.resource: "value"}[self.resource], "value") def test_resource_to_json_is_valid_json(self): payload = json.loads(self.resource.to_json()) self.assertEqual(payload["attributes"]["build.id"], "010203") def test_equal_resources_hash_equally(self): self.assertEqual(hash(self.resource), hash(Resource.create(dict(_BYTES_RESOURCE)))) def test_differing_bytes_hash_differently(self): other = Resource.create({"service.name": "svc", "build.id": b"\xff"}) self.assertNotEqual(hash(self.resource), hash(other)) def test_bytes_and_equivalent_string_are_distinct(self): """The hash fallback must not make b"\\x01\\x02\\x03" collide with "010203".""" as_text = Resource.create({"service.name": "svc", "build.id": "010203"}) self.assertNotEqual(self.resource, as_text) python-opentelemetry-1.45.2/opentelemetry-sdk/tests/resources/test_resources.py000066400000000000000000001521501527070070700303010ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # pylint: disable=too-many-lines import os import platform import subprocess import sys import threading import time import unittest import uuid from concurrent.futures import TimeoutError from functools import partial from logging import ERROR, WARNING from os import environ from unittest.mock import MagicMock, Mock, call, mock_open, patch from urllib import parse import opentelemetry.sdk.resources as _resources_module from opentelemetry.sdk.environment_variables import ( OTEL_EXPERIMENTAL_RESOURCE_DETECTORS, ) from opentelemetry.sdk.resources import ( _BSD_KENV_COMMAND, _DEFAULT_RESOURCE, _EMPTY_RESOURCE, _LINUX_MACHINE_ID_PATHS, _OPENTELEMETRY_SDK_VERSION, HOST_ARCH, HOST_ID, HOST_NAME, OS_TYPE, OS_VERSION, OTEL_RESOURCE_ATTRIBUTES, OTEL_SERVICE_NAME, PROCESS_COMMAND, PROCESS_COMMAND_ARGS, PROCESS_COMMAND_LINE, PROCESS_EXECUTABLE_NAME, PROCESS_EXECUTABLE_PATH, PROCESS_OWNER, PROCESS_PARENT_PID, PROCESS_PID, PROCESS_RUNTIME_DESCRIPTION, PROCESS_RUNTIME_NAME, PROCESS_RUNTIME_VERSION, SERVICE_INSTANCE_ID, SERVICE_NAME, TELEMETRY_SDK_LANGUAGE, TELEMETRY_SDK_NAME, TELEMETRY_SDK_VERSION, OsResourceDetector, OTELResourceDetector, ProcessResourceDetector, Resource, ResourceDetector, ServiceInstanceIdResourceDetector, _get_process_dependent_resource, _HostResourceDetector, get_aggregated_resources, ) from opentelemetry.util._importlib_metadata import ( entry_points as real_entry_points, ) try: import psutil except ImportError: psutil = None try: import winreg except ImportError: winreg = None class DefaultResourceDetector(ResourceDetector): def detect(self) -> Resource: return Resource.get_empty() class ProcessDependentResourceDetector(ResourceDetector): def __init__(self, resource: Resource, process_dependent: bool = False) -> None: super().__init__() self.resource = resource self.process_dependent = process_dependent def detect(self) -> Resource: return self.resource def is_process_dependent(self) -> bool: return self.process_dependent # pylint: disable-next=too-many-public-methods class TestResources(unittest.TestCase): def setUp(self) -> None: environ[OTEL_RESOURCE_ATTRIBUTES] = "" self._service_instance_id = ServiceInstanceIdResourceDetector().detect().attributes[SERVICE_INSTANCE_ID] def tearDown(self) -> None: environ.pop(OTEL_RESOURCE_ATTRIBUTES) @patch("sys.executable", "/usr/bin/python3") def test_create(self): attributes = { "service": "ui", "version": 1, "has_bugs": True, "cost": 112.12, } expected_attributes = { "service": "ui", "version": 1, "has_bugs": True, "cost": 112.12, TELEMETRY_SDK_NAME: "opentelemetry", TELEMETRY_SDK_LANGUAGE: "python", TELEMETRY_SDK_VERSION: _OPENTELEMETRY_SDK_VERSION, SERVICE_INSTANCE_ID: self._service_instance_id, SERVICE_NAME: "unknown_service:python3", } resource = Resource.create(attributes) self.assertIsInstance(resource, Resource) self.assertEqual(resource.attributes, expected_attributes) self.assertEqual(resource.schema_url, "") schema_url = "https://opentelemetry.io/schemas/1.3.0" resource = Resource.create(attributes, schema_url) self.assertIsInstance(resource, Resource) self.assertEqual(resource.attributes, expected_attributes) self.assertEqual(resource.schema_url, schema_url) environ[OTEL_RESOURCE_ATTRIBUTES] = "key=value" resource = Resource.create(attributes) self.assertIsInstance(resource, Resource) expected_with_envar = expected_attributes.copy() expected_with_envar["key"] = "value" self.assertEqual(resource.attributes, expected_with_envar) environ[OTEL_RESOURCE_ATTRIBUTES] = "" resource = Resource.get_empty() self.assertEqual(resource, _EMPTY_RESOURCE) expected_default = _DEFAULT_RESOURCE.merge( Resource( { SERVICE_INSTANCE_ID: self._service_instance_id, SERVICE_NAME: "unknown_service:python3", }, "", ) ) resource = Resource.create(None) self.assertEqual(resource, expected_default) self.assertEqual(resource.schema_url, "") resource = Resource.create(None, None) self.assertEqual(resource, expected_default) self.assertEqual(resource.schema_url, "") resource = Resource.create({}) self.assertEqual(resource, expected_default) self.assertEqual(resource.schema_url, "") resource = Resource.create({}, None) self.assertEqual(resource, expected_default) self.assertEqual(resource.schema_url, "") def test_resource_merge(self): left = Resource({"service": "ui"}) right = Resource({"host": "service-host"}) self.assertEqual( left.merge(right), Resource({"service": "ui", "host": "service-host"}), ) schema_urls = ( "https://opentelemetry.io/schemas/1.2.0", "https://opentelemetry.io/schemas/1.3.0", ) left = Resource.create({}, None) right = Resource.create({}, None) self.assertEqual(left.merge(right).schema_url, "") left = Resource.create({}, None) right = Resource.create({}, schema_urls[0]) self.assertEqual(left.merge(right).schema_url, schema_urls[0]) left = Resource.create({}, schema_urls[0]) right = Resource.create({}, None) self.assertEqual(left.merge(right).schema_url, schema_urls[0]) left = Resource.create({}, schema_urls[0]) right = Resource.create({}, schema_urls[0]) self.assertEqual(left.merge(right).schema_url, schema_urls[0]) left = Resource.create({}, schema_urls[0]) right = Resource.create({}, schema_urls[1]) with self.assertLogs(level=ERROR) as log_entry: self.assertEqual(left.merge(right), left) self.assertIn(schema_urls[0], log_entry.output[0]) self.assertIn(schema_urls[1], log_entry.output[0]) def test_resource_merge_empty_string(self): """Verify Resource.merge behavior with the empty string. Attributes from the source Resource take precedence, with the exception of the empty string. """ left = Resource({"service": "ui", "host": ""}) right = Resource({"host": "service-host", "service": "not-ui"}) self.assertEqual( left.merge(right), Resource({"service": "not-ui", "host": "service-host"}), ) @patch("sys.executable", "/usr/bin/python3") def test_immutability(self): attributes = { "service": "ui", "version": 1, "has_bugs": True, "cost": 112.12, } default_attributes = { TELEMETRY_SDK_NAME: "opentelemetry", TELEMETRY_SDK_LANGUAGE: "python", TELEMETRY_SDK_VERSION: _OPENTELEMETRY_SDK_VERSION, SERVICE_INSTANCE_ID: self._service_instance_id, SERVICE_NAME: "unknown_service:python3", } attributes_copy = attributes.copy() attributes_copy.update(default_attributes) resource = Resource.create(attributes) self.assertEqual(resource.attributes, attributes_copy) with self.assertRaises(TypeError): resource.attributes["has_bugs"] = False self.assertEqual(resource.attributes, attributes_copy) attributes["cost"] = 999.91 self.assertEqual(resource.attributes, attributes_copy) with self.assertRaises(AttributeError): resource.schema_url = "bug" self.assertEqual(resource.schema_url, "") def test_invalid_resource_attribute_values(self): # This class has no __str__ or __repr__ method, so BoundedAttributes does # not attempt to convert it to a string and defaults to returning None. class NoStrNoRepr: def __init__(self): pass # Empty key will get dropped. with self.assertLogs(level=WARNING): resource = Resource( { SERVICE_NAME: "test", "bad-type": NoStrNoRepr(), "": "empty-key-value", } ) self.assertEqual( resource.attributes, { SERVICE_NAME: "test", "bad-type": None, }, ) self.assertEqual(len(resource.attributes), 2) @patch("sys.executable", "/usr/bin/python3") def test_aggregated_resources_no_detectors(self): aggregated_resources = get_aggregated_resources([]) self.assertEqual( aggregated_resources, _DEFAULT_RESOURCE.merge( Resource( { SERVICE_INSTANCE_ID: self._service_instance_id, SERVICE_NAME: "unknown_service:python3", }, "", ) ), ) def test_aggregated_resources_with_default_destroying_static_resource( self, ): static_resource = Resource({"static_key": "static_value"}) self.assertEqual( get_aggregated_resources([], initial_resource=static_resource), static_resource, ) resource_detector = Mock(spec=ResourceDetector) resource_detector.detect.return_value = Resource( {"static_key": "try_to_overwrite_existing_value", "key": "value"} ) self.assertEqual( get_aggregated_resources([resource_detector], initial_resource=static_resource), Resource( { "static_key": "try_to_overwrite_existing_value", "key": "value", } ), ) @patch("sys.executable", "/usr/bin/python3") def test_aggregated_resources_multiple_detectors(self): resource_detector1 = Mock(spec=ResourceDetector) resource_detector1.detect.return_value = Resource({"key1": "value1"}) resource_detector2 = Mock(spec=ResourceDetector) resource_detector2.detect.return_value = Resource({"key2": "value2", "key3": "value3"}) resource_detector3 = Mock(spec=ResourceDetector) resource_detector3.detect.return_value = Resource( { "key2": "try_to_overwrite_existing_value", "key3": "try_to_overwrite_existing_value", "key4": "value4", } ) self.assertEqual( get_aggregated_resources([resource_detector1, resource_detector2, resource_detector3]), _DEFAULT_RESOURCE.merge( Resource( { SERVICE_INSTANCE_ID: self._service_instance_id, SERVICE_NAME: "unknown_service:python3", }, "", ) ).merge( Resource( { "key1": "value1", "key2": "try_to_overwrite_existing_value", "key3": "try_to_overwrite_existing_value", "key4": "value4", } ) ), ) @patch("sys.executable", "/usr/bin/python3") def test_aggregated_resources_different_schema_urls(self): resource_detector1 = Mock(spec=ResourceDetector) resource_detector1.detect.return_value = Resource({"key1": "value1"}, "") resource_detector2 = Mock(spec=ResourceDetector) resource_detector2.detect.return_value = Resource({"key2": "value2", "key3": "value3"}, "url1") resource_detector3 = Mock(spec=ResourceDetector) resource_detector3.detect.return_value = Resource( { "key2": "try_to_overwrite_existing_value", "key3": "try_to_overwrite_existing_value", "key4": "value4", }, "url2", ) resource_detector4 = Mock(spec=ResourceDetector) resource_detector4.detect.return_value = Resource( { "key2": "try_to_overwrite_existing_value", "key3": "try_to_overwrite_existing_value", "key4": "value4", }, "url1", ) self.assertEqual( get_aggregated_resources([resource_detector1, resource_detector2]), _DEFAULT_RESOURCE.merge( Resource( { SERVICE_INSTANCE_ID: self._service_instance_id, SERVICE_NAME: "unknown_service:python3", }, "", ) ).merge( Resource( {"key1": "value1", "key2": "value2", "key3": "value3"}, "url1", ) ), ) with self.assertLogs(level=ERROR) as log_entry: self.assertEqual( get_aggregated_resources([resource_detector2, resource_detector3]), _DEFAULT_RESOURCE.merge( Resource( { SERVICE_INSTANCE_ID: self._service_instance_id, SERVICE_NAME: "unknown_service:python3", }, "", ) ).merge(Resource({"key2": "value2", "key3": "value3"}, "url1")), ) self.assertIn("url1", log_entry.output[0]) self.assertIn("url2", log_entry.output[0]) with self.assertLogs(level=ERROR): self.assertEqual( get_aggregated_resources( [ resource_detector2, resource_detector3, resource_detector4, resource_detector1, ] ), _DEFAULT_RESOURCE.merge( Resource( { SERVICE_INSTANCE_ID: self._service_instance_id, SERVICE_NAME: "unknown_service:python3", }, "", ) ).merge( Resource( { "key1": "value1", "key2": "try_to_overwrite_existing_value", "key3": "try_to_overwrite_existing_value", "key4": "value4", }, "url1", ) ), ) self.assertIn("url1", log_entry.output[0]) self.assertIn("url2", log_entry.output[0]) @patch("sys.executable", "/usr/bin/python3") def test_resource_detector_ignore_error(self): resource_detector = Mock(spec=ResourceDetector) resource_detector.detect.side_effect = Exception() resource_detector.raise_on_error = False with self.assertLogs(level=WARNING): self.assertEqual( get_aggregated_resources([resource_detector]), _DEFAULT_RESOURCE.merge( Resource( { SERVICE_INSTANCE_ID: self._service_instance_id, SERVICE_NAME: "unknown_service:python3", }, "", ) ), ) def test_resource_detector_raise_error(self): resource_detector = Mock(spec=ResourceDetector) resource_detector.detect.side_effect = Exception() resource_detector.raise_on_error = True self.assertRaises(Exception, get_aggregated_resources, [resource_detector]) def test_aggregated_resources_joins_detector_threads(self): # A detector that takes longer than the timeout is skipped, but its # worker thread must be joined before the call returns so no threads # outlive resource creation. worker_threads = [] def slow_detect(): worker_threads.append(threading.current_thread()) time.sleep(0.5) resource_detector = Mock(spec=ResourceDetector) resource_detector.detect.side_effect = slow_detect resource_detector.raise_on_error = False with self.assertLogs(level=WARNING) as log_entry: self.assertEqual( get_aggregated_resources( [resource_detector], initial_resource=_DEFAULT_RESOURCE, timeout=0.1, ), _DEFAULT_RESOURCE, ) self.assertIn("took longer than", log_entry.output[0]) self.assertEqual(len(worker_threads), 1) self.assertFalse(worker_threads[0].is_alive()) def test_resource_detector_is_not_process_dependent_by_default(self): self.assertFalse(DefaultResourceDetector().is_process_dependent()) def test_process_resource_detector_is_process_dependent(self): self.assertTrue(ProcessResourceDetector().is_process_dependent()) @patch("opentelemetry.sdk.resources._build_resource_detectors") def test_get_process_dependent_resource(self, build_resource_detectors_mock): build_resource_detectors_mock.return_value = [ ProcessDependentResourceDetector(Resource({"ignored": "ignored"})), ProcessDependentResourceDetector(Resource({"one": "one", "two": "old"}), process_dependent=True), ProcessDependentResourceDetector( Resource({"two": "new", "three": "three"}), process_dependent=True, ), ] self.assertEqual( _get_process_dependent_resource(), Resource({"one": "one", "two": "new", "three": "three"}), ) @patch("opentelemetry.sdk.resources._build_resource_detectors") def test_get_process_dependent_resource_empty(self, build_resource_detectors_mock): build_resource_detectors_mock.return_value = [ ProcessDependentResourceDetector(Resource({"ignored": "ignored"})), ] self.assertEqual(_get_process_dependent_resource(), Resource.get_empty()) @patch("sys.executable", "/usr/bin/python3") @patch("opentelemetry.sdk.resources.logger") def test_resource_detector_timeout(self, mock_logger): resource_detector = Mock(spec=ResourceDetector) resource_detector.detect.side_effect = TimeoutError() resource_detector.raise_on_error = False self.assertEqual( get_aggregated_resources([resource_detector]), _DEFAULT_RESOURCE.merge( Resource( { SERVICE_INSTANCE_ID: self._service_instance_id, SERVICE_NAME: "unknown_service:python3", }, "", ) ), ) mock_logger.warning.assert_called_with( "Detector %s took longer than %s seconds, skipping", resource_detector, 5, ) @patch.dict( environ, {"OTEL_RESOURCE_ATTRIBUTES": "key1=env_value1,key2=env_value2"}, ) def test_env_priority(self): resource_env = Resource.create() self.assertEqual(resource_env.attributes["key1"], "env_value1") self.assertEqual(resource_env.attributes["key2"], "env_value2") resource_env_override = Resource.create({"key1": "value1", "key2": "value2"}) self.assertEqual(resource_env_override.attributes["key1"], "value1") self.assertEqual(resource_env_override.attributes["key2"], "value2") @patch.dict( environ, { OTEL_SERVICE_NAME: "test-srv-name", OTEL_RESOURCE_ATTRIBUTES: "service.name=svc-name-from-resource", }, ) def test_service_name_env(self): resource = Resource.create() self.assertEqual(resource.attributes["service.name"], "test-srv-name") resource = Resource.create({"service.name": "from-code"}) self.assertEqual(resource.attributes["service.name"], "from-code") @patch("sys.executable", "") def test_service_name_without_sys_executable(self): resource = Resource.create() self.assertEqual(resource.attributes["service.name"], "unknown_service") # pylint: disable=too-many-public-methods def _make_detector_ep(resource): return Mock(**{"load.return_value": Mock(return_value=Mock(**{"detect.return_value": resource}))}) class TestOTELResourceDetector(unittest.TestCase): def setUp(self) -> None: environ[OTEL_RESOURCE_ATTRIBUTES] = "" def tearDown(self) -> None: environ.pop(OTEL_RESOURCE_ATTRIBUTES) def test_empty(self): detector = OTELResourceDetector() environ[OTEL_RESOURCE_ATTRIBUTES] = "" self.assertEqual(detector.detect(), Resource.get_empty()) def test_one(self): detector = OTELResourceDetector() environ[OTEL_RESOURCE_ATTRIBUTES] = "k=v" self.assertEqual(detector.detect(), Resource({"k": "v"})) def test_one_with_whitespace(self): detector = OTELResourceDetector() environ[OTEL_RESOURCE_ATTRIBUTES] = " k = v " self.assertEqual(detector.detect(), Resource({"k": "v"})) def test_multiple(self): detector = OTELResourceDetector() environ[OTEL_RESOURCE_ATTRIBUTES] = "k=v,k2=v2" self.assertEqual(detector.detect(), Resource({"k": "v", "k2": "v2"})) def test_multiple_with_whitespace(self): detector = OTELResourceDetector() environ[OTEL_RESOURCE_ATTRIBUTES] = " k = v , k2 = v2 " self.assertEqual(detector.detect(), Resource({"k": "v", "k2": "v2"})) def test_invalid_key_value_pairs(self): detector = OTELResourceDetector() environ[OTEL_RESOURCE_ATTRIBUTES] = "k=v,k2=v2,invalid,,foo=bar=baz," with self.assertLogs(level=WARNING): self.assertEqual( detector.detect(), Resource({"k": "v", "k2": "v2", "foo": "bar=baz"}), ) def test_multiple_with_url_decode(self): detector = OTELResourceDetector() environ[OTEL_RESOURCE_ATTRIBUTES] = "key=value%20test%0A, key2=value+%202" self.assertEqual( detector.detect(), Resource({"key": "value test\n", "key2": "value+ 2"}), ) self.assertEqual( detector.detect(), Resource( { "key": parse.unquote("value%20test%0A"), "key2": parse.unquote("value+%202"), } ), ) @patch.dict( environ, {OTEL_SERVICE_NAME: "test-srv-name"}, ) def test_service_name_env(self): detector = OTELResourceDetector() self.assertEqual( detector.detect(), Resource({"service.name": "test-srv-name"}), ) @patch.dict( environ, { OTEL_SERVICE_NAME: "from-service-name", OTEL_RESOURCE_ATTRIBUTES: "service.name=from-resource-attrs", }, ) def test_service_name_env_precedence(self): detector = OTELResourceDetector() self.assertEqual( detector.detect(), Resource({"service.name": "from-service-name"}), ) @patch( "sys.orig_argv", ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"], ) @patch("sys.executable", "/usr/bin/uvicorn") def test_process_detector(self): initial_resource = Resource({"foo": "bar"}) aggregated_resource = get_aggregated_resources([ProcessResourceDetector()], initial_resource) self.assertIn( PROCESS_RUNTIME_NAME, aggregated_resource.attributes.keys(), ) self.assertIn( PROCESS_RUNTIME_DESCRIPTION, aggregated_resource.attributes.keys(), ) self.assertIn( PROCESS_RUNTIME_VERSION, aggregated_resource.attributes.keys(), ) self.assertEqual(aggregated_resource.attributes[PROCESS_PID], os.getpid()) if hasattr(os, "getppid"): self.assertEqual( aggregated_resource.attributes[PROCESS_PARENT_PID], os.getppid(), ) if psutil is not None: self.assertEqual( aggregated_resource.attributes[PROCESS_OWNER], psutil.Process().username(), ) self.assertEqual( aggregated_resource.attributes[PROCESS_EXECUTABLE_NAME], "uvicorn", ) self.assertEqual( aggregated_resource.attributes[PROCESS_EXECUTABLE_PATH], "/usr/bin/uvicorn", ) self.assertEqual(aggregated_resource.attributes[PROCESS_COMMAND], "uvicorn") self.assertNotIn( PROCESS_COMMAND_LINE, aggregated_resource.attributes, ) self.assertNotIn( PROCESS_COMMAND_ARGS, aggregated_resource.attributes, ) @patch( "sys.orig_argv", ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"], ) def test_process_detector_includes_command_args_on_opt_in(self): initial_resource = Resource({"foo": "bar"}) aggregated_resource = get_aggregated_resources( [ProcessResourceDetector(include_command_args=True)], initial_resource, ) self.assertEqual( aggregated_resource.attributes[PROCESS_COMMAND_LINE], " ".join(sys.orig_argv), ) self.assertEqual( aggregated_resource.attributes[PROCESS_COMMAND_ARGS], tuple(sys.orig_argv), ) def test_process_detector_does_not_iterate_orig_argv_by_default(self): class SensitiveOrigArgv: def __getitem__(self, index): if index == 0: return "uvicorn" raise AssertionError("unexpected argv access") def __iter__(self): raise AssertionError("unexpected argv iteration") with patch("sys.orig_argv", SensitiveOrigArgv()): aggregated_resource = get_aggregated_resources( [ProcessResourceDetector()], Resource({"foo": "bar"}), ) self.assertEqual( aggregated_resource.attributes[PROCESS_COMMAND], "uvicorn", ) self.assertNotIn( PROCESS_COMMAND_LINE, aggregated_resource.attributes, ) self.assertNotIn( PROCESS_COMMAND_ARGS, aggregated_resource.attributes, ) @patch("sys.orig_argv", ["/usr/bin/python", "-m", "myapp"]) def test_process_detector_uses_orig_argv_for_python_m(self): """For ``python -m `` invocations sys.argv[0] is rewritten to the resolved module path, losing the ``-m `` information. sys.orig_argv preserves the original invocation and must be preferred. See https://github.com/open-telemetry/opentelemetry-python/issues/4518. """ aggregated_resource = get_aggregated_resources([ProcessResourceDetector()], Resource({"foo": "bar"})) self.assertEqual( aggregated_resource.attributes[PROCESS_COMMAND], "/usr/bin/python", ) self.assertNotIn( PROCESS_COMMAND_LINE, aggregated_resource.attributes, ) self.assertNotIn( PROCESS_COMMAND_ARGS, aggregated_resource.attributes, ) @patch("sys.orig_argv", ["/usr/bin/python", "-m", "myapp"]) def test_process_detector_uses_orig_argv_for_python_m_on_opt_in(self): aggregated_resource = get_aggregated_resources( [ProcessResourceDetector(include_command_args=True)], Resource({"foo": "bar"}), ) self.assertEqual( aggregated_resource.attributes[PROCESS_COMMAND], "/usr/bin/python", ) self.assertEqual( aggregated_resource.attributes[PROCESS_COMMAND_LINE], "/usr/bin/python -m myapp", ) self.assertEqual( aggregated_resource.attributes[PROCESS_COMMAND_ARGS], ("/usr/bin/python", "-m", "myapp"), ) @patch("sys.executable", None) def test_process_detector_handles_missing_executable(self): initial_resource = Resource({"foo": "bar"}) aggregated_resource = get_aggregated_resources([ProcessResourceDetector()], initial_resource) self.assertEqual( aggregated_resource.attributes[PROCESS_EXECUTABLE_NAME], "", ) self.assertEqual( aggregated_resource.attributes[PROCESS_EXECUTABLE_PATH], "", ) @patch("sys.executable", "/usr/bin/python3") def test_resource_detector_entry_points_default(self): resource = Resource({}).create() self.assertEqual(resource.attributes["telemetry.sdk.language"], "python") self.assertEqual(resource.attributes["telemetry.sdk.name"], "opentelemetry") self.assertEqual(resource.attributes["service.name"], "unknown_service:python3") self.assertEqual(resource.schema_url, "") resource = Resource({}).create({"a": "b", "c": "d"}) self.assertEqual(resource.attributes["telemetry.sdk.language"], "python") self.assertEqual(resource.attributes["telemetry.sdk.name"], "opentelemetry") self.assertEqual(resource.attributes["service.name"], "unknown_service:python3") self.assertEqual(resource.attributes["a"], "b") self.assertEqual(resource.attributes["c"], "d") self.assertEqual(resource.schema_url, "") @patch.dict(environ, {OTEL_EXPERIMENTAL_RESOURCE_DETECTORS: "mock"}, clear=True) @patch( "opentelemetry.util._importlib_metadata.entry_points", Mock( return_value=[ Mock(**{"load.return_value": Mock(return_value=Mock(**{"detect.return_value": Resource({"a": "b"})}))}) ] ), ) @patch("sys.executable", "/usr/bin/python3") def test_resource_detector_entry_points_non_default(self): resource = Resource({}).create() self.assertEqual(resource.attributes["telemetry.sdk.language"], "python") self.assertEqual(resource.attributes["telemetry.sdk.name"], "opentelemetry") self.assertEqual(resource.attributes["service.name"], "unknown_service:python3") self.assertEqual(resource.attributes["a"], "b") self.assertEqual(resource.schema_url, "") @patch.dict(environ, {OTEL_EXPERIMENTAL_RESOURCE_DETECTORS: ""}, clear=True) def test_resource_detector_entry_points_empty(self): resource = Resource({}).create() self.assertEqual(resource.attributes["telemetry.sdk.language"], "python") @patch.dict(environ, {OTEL_EXPERIMENTAL_RESOURCE_DETECTORS: "os"}, clear=True) def test_resource_detector_entry_points_os(self): resource = Resource({}).create() self.assertIn(OS_TYPE, resource.attributes) self.assertIn(OS_VERSION, resource.attributes) @patch.dict(environ, {OTEL_EXPERIMENTAL_RESOURCE_DETECTORS: "*"}, clear=True) def test_resource_detector_entry_points_all(self): resource = Resource({}).create() self.assertIn( TELEMETRY_SDK_NAME, resource.attributes, "'otel' resource detector not enabled", ) self.assertIn(OS_TYPE, resource.attributes, "'os' resource detector not enabled") self.assertIn( HOST_ARCH, resource.attributes, "'host' resource detector not enabled", ) self.assertIn( PROCESS_RUNTIME_NAME, resource.attributes, "'process' resource detector not enabled", ) @patch("sys.executable", "/usr/bin/python4") def test_resource_detector_entry_points_otel(self): """ Test that OTELResourceDetector-resource-generated attributes are always being added. """ with patch.dict(environ, {OTEL_RESOURCE_ATTRIBUTES: "a=b,c=d"}, clear=True): resource = Resource({}).create() self.assertEqual(resource.attributes["telemetry.sdk.language"], "python") self.assertEqual(resource.attributes["telemetry.sdk.name"], "opentelemetry") self.assertEqual(resource.attributes["service.name"], "unknown_service:python4") self.assertEqual(resource.attributes["a"], "b") self.assertEqual(resource.attributes["c"], "d") self.assertEqual(resource.schema_url, "") with patch.dict( environ, { OTEL_RESOURCE_ATTRIBUTES: "a=b,c=d", OTEL_EXPERIMENTAL_RESOURCE_DETECTORS: "process", }, clear=True, ): resource = Resource({}).create() self.assertEqual(resource.attributes["telemetry.sdk.language"], "python") self.assertEqual(resource.attributes["telemetry.sdk.name"], "opentelemetry") self.assertEqual( resource.attributes["service.name"], "unknown_service:python4", ) self.assertEqual(resource.attributes["a"], "b") self.assertEqual(resource.attributes["c"], "d") self.assertIn(PROCESS_RUNTIME_NAME, resource.attributes.keys()) self.assertIn(PROCESS_RUNTIME_DESCRIPTION, resource.attributes.keys()) self.assertIn(PROCESS_RUNTIME_VERSION, resource.attributes.keys()) self.assertEqual(resource.schema_url, "") @patch.dict( environ, {OTEL_EXPERIMENTAL_RESOURCE_DETECTORS: "mock_a,mock_b"}, clear=True, ) def test_resource_detector_ordering_last_wins(self): """Last detector in OTEL_EXPERIMENTAL_RESOURCE_DETECTORS wins on conflict.""" ep_a = _make_detector_ep(Resource({"conflict_key": "from_a"})) ep_b = _make_detector_ep(Resource({"conflict_key": "from_b"})) def side_effect(*args, **kwargs): return {"mock_a": [ep_a], "mock_b": [ep_b]}.get(kwargs.get("name", ""), []) with patch( "opentelemetry.util._importlib_metadata.entry_points", side_effect=side_effect, ): resource = Resource({}).create() self.assertEqual(resource.attributes["conflict_key"], "from_b") @patch.dict( environ, { OTEL_EXPERIMENTAL_RESOURCE_DETECTORS: "mock", OTEL_RESOURCE_ATTRIBUTES: "conflict_key=otel_value", }, clear=True, ) def test_otel_detector_appended_last(self): """'otel' detector is always appended last, so its attributes win over earlier detectors.""" ep_mock = _make_detector_ep(Resource({"conflict_key": "mock_value"})) def side_effect(*args, **kwargs): if kwargs.get("name") == "mock": return [ep_mock] return real_entry_points(*args, **kwargs) with patch( "opentelemetry.util._importlib_metadata.entry_points", side_effect=side_effect, ): resource = Resource({}).create() self.assertEqual(resource.attributes["conflict_key"], "otel_value") @patch("platform.system", lambda: "Linux") @patch("platform.release", lambda: "666.5.0-35-generic") def test_os_detector_linux(self): resource = get_aggregated_resources( [OsResourceDetector()], Resource({}), ) self.assertEqual(resource.attributes[OS_TYPE], "linux") self.assertEqual(resource.attributes[OS_VERSION], "666.5.0-35-generic") @patch("platform.system", lambda: "Windows") @patch("platform.version", lambda: "10.0.666") def test_os_detector_windows(self): resource = get_aggregated_resources( [OsResourceDetector()], Resource({}), ) self.assertEqual(resource.attributes[OS_TYPE], "windows") self.assertEqual(resource.attributes[OS_VERSION], "10.0.666") @patch("platform.system", lambda: "SunOS") @patch("platform.version", lambda: "666.4.0.15.0") def test_os_detector_solaris(self): resource = get_aggregated_resources( [OsResourceDetector()], Resource({}), ) self.assertEqual(resource.attributes[OS_TYPE], "solaris") self.assertEqual(resource.attributes[OS_VERSION], "666.4.0.15.0") _IOREG_OUTPUT = """+-o IOPlatformExpertDevice { "IOPlatformUUID" = "AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE" "IOPlatformSerialNumber" = "C02XXXXXXXXX" } """ def _stdout(text: str): """A subprocess.run replacement that writes `text` to stdout.""" return lambda *args, **kwargs: Mock(stdout=text) def _detect() -> Resource: return _HostResourceDetector().detect() class TestHostResourceDetector(unittest.TestCase): def _assert_host_name_and_arch_survive(self, resource: Resource) -> None: """A failed host.id lookup must not cost the other two attributes.""" self.assertNotIn(HOST_ID, resource.attributes) self.assertIn(HOST_NAME, resource.attributes) self.assertIn(HOST_ARCH, resource.attributes) @patch("socket.gethostname", lambda: "foo") @patch("platform.machine", lambda: "AMD64") def test_host_resource_detector(self): resource = get_aggregated_resources( [_HostResourceDetector()], Resource({}), ) self.assertEqual(resource.attributes[HOST_NAME], "foo") self.assertEqual(resource.attributes[HOST_ARCH], "AMD64") @patch.dict(environ, {OTEL_EXPERIMENTAL_RESOURCE_DETECTORS: "host"}, clear=True) @patch("opentelemetry.sdk.resources._get_host_id", lambda: "host-id") def test_resource_detector_entry_points_host(self): resource = Resource({}).create() self.assertIn(HOST_NAME, resource.attributes) self.assertIn(HOST_ARCH, resource.attributes) self.assertEqual(resource.attributes[HOST_ID], "host-id") @patch.dict( environ, {OTEL_EXPERIMENTAL_RESOURCE_DETECTORS: "doesnotexist,host"}, clear=True, ) def test_resource_detector_entry_points_tolerate_missing_detector(self): resource = Resource({}).create() self.assertEqual(resource.attributes["telemetry.sdk.language"], "python") self.assertIn(HOST_NAME, resource.attributes) @patch("platform.system", lambda: "Linux") @patch("builtins.open", new_callable=mock_open, read_data=" primary-machine-id\n") def test_host_id_linux_prefers_primary_machine_id(self, open_mock): self.assertEqual(_detect().attributes[HOST_ID], "primary-machine-id") open_mock.assert_called_once_with("/etc/machine-id", encoding="utf8") @patch("platform.system", lambda: "Linux") @patch("builtins.open") def test_host_id_linux_falls_back_to_dbus_machine_id(self, open_mock): open_mock.side_effect = [mock_open(read_data=" \n")(), mock_open(read_data="dbus-machine-id\n")()] self.assertEqual(_detect().attributes[HOST_ID], "dbus-machine-id") self.assertEqual( open_mock.call_args_list, [call("/etc/machine-id", encoding="utf8"), call("/var/lib/dbus/machine-id", encoding="utf8")], ) @patch("platform.system", lambda: "Linux") @patch("builtins.open") def test_host_id_linux_no_machine_id(self, open_mock): open_mock.side_effect = [PermissionError("access denied"), mock_open(read_data="")()] with self.assertNoLogs(level=WARNING): self._assert_host_name_and_arch_survive(_detect()) @patch("platform.system", lambda: "Darwin") @patch("opentelemetry.sdk.resources.subprocess.run", return_value=Mock(stdout=_IOREG_OUTPUT)) def test_host_id_macos_parses_ioreg_platform_uuid(self, run_mock): self.assertEqual( _detect().attributes[HOST_ID], "AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE", ) run_mock.assert_called_once_with( ("/usr/sbin/ioreg", "-rd1", "-c", "IOPlatformExpertDevice"), capture_output=True, text=True, timeout=2, check=True, ) @patch("platform.system", lambda: "Darwin") @patch("opentelemetry.sdk.resources.subprocess.run", _stdout("no uuid here")) def test_host_id_macos_no_platform_uuid(self): self._assert_host_name_and_arch_survive(_detect()) @patch("platform.system", lambda: "Windows") def test_host_id_windows_reads_machine_guid_from_registry(self): mock_winreg = MagicMock() mock_winreg.KEY_READ = 0x20019 mock_winreg.KEY_WOW64_64KEY = 0x0100 mock_winreg.QueryValueEx.return_value = ("registry-machine-guid", 1) with patch("opentelemetry.sdk.resources.winreg", mock_winreg): self.assertEqual(_detect().attributes[HOST_ID], "registry-machine-guid") self.assertEqual(mock_winreg.OpenKey.call_args.args[1], r"SOFTWARE\Microsoft\Cryptography") # The 64 bit view must be requested explicitly, or a 32 bit interpreter # reads the WOW6432Node copy of the key. self.assertEqual(mock_winreg.OpenKey.call_args.kwargs["access"], 0x20119) self.assertEqual(mock_winreg.QueryValueEx.call_args.args[1], "MachineGuid") @patch("platform.system", lambda: "Windows") def test_host_id_windows_registry_read_fails(self): mock_winreg = MagicMock() mock_winreg.OpenKey.side_effect = OSError("no such key") with patch("opentelemetry.sdk.resources.winreg", mock_winreg), self.assertLogs(level=WARNING): self._assert_host_name_and_arch_survive(_detect()) @patch("platform.system", lambda: "Windows") @patch("opentelemetry.sdk.resources.winreg", None) def test_host_id_windows_without_winreg(self): with self.assertNoLogs(level=WARNING): self._assert_host_name_and_arch_survive(_detect()) @patch("platform.system", lambda: "FreeBSD") @patch("opentelemetry.sdk.resources._read_machine_id_file", return_value="bsd-host-id") def test_host_id_bsd_reads_etc_hostid(self, mock_read): self.assertEqual(_detect().attributes[HOST_ID], "bsd-host-id") self.assertEqual(mock_read.call_args.args[0], "/etc/hostid") @patch("platform.system", lambda: "NetBSD") @patch("opentelemetry.sdk.resources._read_machine_id_file", lambda _: None) @patch("opentelemetry.sdk.resources.subprocess.run", return_value=Mock(stdout="bsd-kenv-uuid\n")) def test_host_id_bsd_falls_back_to_kenv(self, run_mock): self.assertEqual(_detect().attributes[HOST_ID], "bsd-kenv-uuid") run_mock.assert_called_once_with( ("/bin/kenv", "-q", "smbios.system.uuid"), capture_output=True, text=True, timeout=2, check=True, ) @patch("platform.system", lambda: "FreeBSD") @patch("opentelemetry.sdk.resources._read_machine_id_file", lambda _: None) @patch( "opentelemetry.sdk.resources.subprocess.run", Mock(side_effect=subprocess.CalledProcessError(1, _BSD_KENV_COMMAND)), ) def test_host_id_bsd_no_host_id(self): # `kenv -q` exits non-zero when the host has no SMBIOS UUID. That is an # ordinary outcome, so it must not warn on every detection. with self.assertNoLogs(level=WARNING): self._assert_host_name_and_arch_survive(_detect()) @patch("platform.system", lambda: "FreeBSD") @patch("opentelemetry.sdk.resources._read_machine_id_file", lambda _: None) @patch("opentelemetry.sdk.resources.subprocess.run", Mock(side_effect=FileNotFoundError)) def test_host_id_bsd_without_kenv(self): with self.assertNoLogs(level=WARNING): self._assert_host_name_and_arch_survive(_detect()) @patch("platform.system", lambda: "Java") def test_host_id_unsupported_os(self): with self.assertNoLogs(level=WARNING): self._assert_host_name_and_arch_survive(_detect()) @patch("platform.system", lambda: "Darwin") @patch( "opentelemetry.sdk.resources.subprocess.run", Mock(side_effect=subprocess.TimeoutExpired(cmd="ioreg", timeout=2)), ) def test_host_id_command_timeout(self): with self.assertLogs(level=WARNING): self._assert_host_name_and_arch_survive(_detect()) @patch("opentelemetry.sdk.resources._get_host_id", Mock(side_effect=ValueError("boom"))) def test_host_id_error_swallowed_by_default(self): # detect() is called directly here, without the handling in # get_aggregated_resources, to prove the detector guards itself. with self.assertLogs(level=WARNING): self._assert_host_name_and_arch_survive(_detect()) @patch("opentelemetry.sdk.resources._get_host_id", Mock(side_effect=ValueError("boom"))) def test_host_id_raise_on_error(self): with self.assertRaises(ValueError), self.assertLogs(level=WARNING): _HostResourceDetector(raise_on_error=True).detect() class TestHostResourceDetectorIntegration(unittest.TestCase): @unittest.skipUnless(platform.system() == "Linux", "requires Linux machine-id files") def test_host_id_matches_machine_id_file(self): # Check the source independently so a detector regression cannot turn # this test into a skip. Empty or unreadable files are valid on containers. for path in _LINUX_MACHINE_ID_PATHS: try: with open(path, encoding="utf8") as machine_id_file: expected_id = machine_id_file.read().strip() except OSError: continue if expected_id: self.assertEqual(_detect().attributes[HOST_ID], expected_id) return self.skipTest("no readable, nonempty machine-id file on this host") @unittest.skipUnless(winreg is not None, "requires the Windows registry") def test_host_id_matches_windows_machine_guid(self): if winreg is None: self.skipTest("requires the Windows registry") # Read the native source independently of the detector's helpers. try: with winreg.OpenKey( winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\Microsoft\Cryptography", access=winreg.KEY_READ | winreg.KEY_WOW64_64KEY, ) as key: expected_id, _ = winreg.QueryValueEx(key, "MachineGuid") except OSError as exception: self.skipTest(f"MachineGuid is unavailable: {exception}") if not expected_id: self.skipTest("MachineGuid is empty") self.assertEqual(_detect().attributes[HOST_ID], expected_id) # pylint: disable=protected-access class TestServiceInstanceIdResourceDetector(unittest.TestCase): def setUp(self) -> None: self._orig_instance_id = _resources_module._service_instance_id self._orig_instance_pid = _resources_module._service_instance_id_pid def tearDown(self) -> None: _resources_module._service_instance_id = self._orig_instance_id _resources_module._service_instance_id_pid = self._orig_instance_pid def test_is_process_dependent(self): self.assertTrue(ServiceInstanceIdResourceDetector().is_process_dependent()) @patch.dict(environ, {}, clear=True) def test_resource_attributes_override_service_instance_id(self): resource = Resource.create({SERVICE_INSTANCE_ID: "resource-instance-id"}) self.assertEqual(resource.attributes[SERVICE_INSTANCE_ID], "resource-instance-id") @patch.dict( environ, {OTEL_RESOURCE_ATTRIBUTES: "service.instance.id=environment-instance-id"}, clear=True, ) def test_environment_resource_attributes_override_service_instance_id(self): resource = Resource.create() self.assertEqual(resource.attributes[SERVICE_INSTANCE_ID], "environment-instance-id") def test_service_instance_detector_ordering(self): test_cases = ( ("", False, True), ("mock", True, False), ("mock,service_instance", True, True), ) def entry_points_side_effect(entry_point, *args, **kwargs): if kwargs.get("name") == "mock": return [entry_point] return real_entry_points(*args, **kwargs) for detector_names, includes_custom_detector, expects_generated_id in test_cases: with self.subTest(detector_names=detector_names): custom_detector = Mock(spec=ResourceDetector) custom_detector.detect.return_value = Resource( { SERVICE_INSTANCE_ID: "configured-instance-id", "custom.detector": "value", } ) entry_point = Mock(**{"load.return_value": Mock(return_value=custom_detector)}) with patch.dict( environ, {OTEL_EXPERIMENTAL_RESOURCE_DETECTORS: detector_names}, clear=True, ): if includes_custom_detector: with patch( "opentelemetry.util._importlib_metadata.entry_points", side_effect=partial(entry_points_side_effect, entry_point), ): resource = Resource.create() custom_detector.detect.assert_called_once() self.assertEqual(resource.attributes["custom.detector"], "value") else: resource = Resource.create() if expects_generated_id: self.assertEqual( uuid.UUID(resource.attributes[SERVICE_INSTANCE_ID]).version, 4, ) else: self.assertEqual( resource.attributes[SERVICE_INSTANCE_ID], "configured-instance-id", ) def test_detect_value_is_valid_uuid4(self): _resources_module._service_instance_id = None _resources_module._service_instance_id_pid = None detector = ServiceInstanceIdResourceDetector() value = detector.detect().attributes[SERVICE_INSTANCE_ID] parsed = uuid.UUID(value) self.assertEqual(parsed.version, 4) def test_detect_stable_within_instance(self): _resources_module._service_instance_id = None _resources_module._service_instance_id_pid = None detector = ServiceInstanceIdResourceDetector() id1 = detector.detect().attributes[SERVICE_INSTANCE_ID] id2 = detector.detect().attributes[SERVICE_INSTANCE_ID] self.assertEqual(id1, id2) def test_detect_shared_across_instances(self): _resources_module._service_instance_id = None _resources_module._service_instance_id_pid = None id1 = ServiceInstanceIdResourceDetector().detect().attributes[SERVICE_INSTANCE_ID] id2 = ServiceInstanceIdResourceDetector().detect().attributes[SERVICE_INSTANCE_ID] self.assertEqual(id1, id2) def test_detect_pid_change_generates_new_id(self): _resources_module._service_instance_id = "old-id" _resources_module._service_instance_id_pid = os.getpid() - 1 new_id = ServiceInstanceIdResourceDetector().detect().attributes[SERVICE_INSTANCE_ID] self.assertNotEqual(new_id, "old-id") self.assertEqual(_resources_module._service_instance_id_pid, os.getpid()) uuid.UUID(new_id) def test_detect_pid_unchanged_returns_same_id(self): known_id = "known-stable-id" _resources_module._service_instance_id = known_id _resources_module._service_instance_id_pid = os.getpid() result = ServiceInstanceIdResourceDetector().detect().attributes[SERVICE_INSTANCE_ID] self.assertEqual(result, known_id) @unittest.skipUnless(hasattr(os, "fork"), "requires os.fork") def test_detect_fork_generates_new_id(self): script = """\ import os import sys from opentelemetry.sdk.resources import ServiceInstanceIdResourceDetector, SERVICE_INSTANCE_ID parent_id = ServiceInstanceIdResourceDetector().detect().attributes[SERVICE_INSTANCE_ID] pid = os.fork() if not pid: child_id = ServiceInstanceIdResourceDetector().detect().attributes[SERVICE_INSTANCE_ID] print(f"child:{child_id}", flush=True) os._exit(0) else: os.waitpid(pid, 0) print(f"parent:{parent_id}", flush=True) """ result = subprocess.run( [sys.executable, "-c", script], capture_output=True, text=True, check=True, ) ids = dict(line.split(":", 1) for line in result.stdout.strip().splitlines()) parent_id, child_id = ids["parent"], ids["child"] self.assertNotEqual(parent_id, child_id) self.assertEqual(uuid.UUID(parent_id).version, 4) self.assertEqual(uuid.UUID(child_id).version, 4) @patch.dict( environ, {OTEL_EXPERIMENTAL_RESOURCE_DETECTORS: "service_instance"}, clear=True, ) def test_resource_detector_entry_points_service_instance(self): resource = Resource.create() self.assertIn(SERVICE_INSTANCE_ID, resource.attributes) uuid.UUID(resource.attributes[SERVICE_INSTANCE_ID]) python-opentelemetry-1.45.2/opentelemetry-sdk/tests/shared_internal/000077500000000000000000000000001527070070700260025ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-sdk/tests/shared_internal/__init__.py000066400000000000000000000001141527070070700301070ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 python-opentelemetry-1.45.2/opentelemetry-sdk/tests/shared_internal/test_batch_processor.py000066400000000000000000000253461527070070700326050ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # pylint: disable=protected-access import gc import logging import multiprocessing import os import sys import threading import time import unittest import weakref from platform import system from typing import Any from unittest.mock import Mock import pytest from opentelemetry._logs import ( LogRecord, ) from opentelemetry.sdk._logs import ( ReadWriteLogRecord, ) from opentelemetry.sdk._logs.export import ( BatchLogRecordProcessor, ) from opentelemetry.sdk._shared_internal import ( DuplicateFilter, ) from opentelemetry.sdk.trace import ReadableSpan from opentelemetry.sdk.trace.export import BatchSpanProcessor from opentelemetry.sdk.util.instrumentation import InstrumentationScope EMPTY_LOG = ReadWriteLogRecord( log_record=LogRecord(), instrumentation_scope=InstrumentationScope("example", "example"), ) BASIC_SPAN = ReadableSpan( "MySpan", instrumentation_scope=InstrumentationScope("example", "example"), ) if system() != "Windows": multiprocessing.set_start_method("fork") class MockExporterForTesting: def __init__(self, export_sleep: int): self.num_export_calls = 0 self.export_sleep = export_sleep self._shutdown = False self.sleep_interrupted = False self.export_sleep_event = threading.Event() def export(self, _: list[Any]): self.num_export_calls += 1 if self._shutdown: raise ValueError("Cannot export, already shutdown") sleep_interrupted = self.export_sleep_event.wait(self.export_sleep) if sleep_interrupted: self.sleep_interrupted = True raise ValueError("Did not get to finish !") def shutdown(self): # Force export to finish sleeping. self._shutdown = True self.export_sleep_event.set() # BatchLogRecodProcessor/BatchSpanProcessor initialize and use BatchProcessor. # Important: make sure to call .shutdown() before the end of the test, # otherwise the worker thread will continue to run after the end of the test. @pytest.mark.parametrize( "batch_processor_class,telemetry", [(BatchLogRecordProcessor, EMPTY_LOG), (BatchSpanProcessor, BASIC_SPAN)], ) class TestBatchProcessor: # pylint: disable=no-self-use def test_telemetry_exported_once_batch_size_reached(self, batch_processor_class, telemetry): exporter = Mock() batch_processor = batch_processor_class( exporter, max_queue_size=15, max_export_batch_size=15, # Will not reach this during the test, this sleep should be interrupted when batch size is reached. schedule_delay_millis=30000, export_timeout_millis=500, ) before_export = time.time_ns() for _ in range(15): batch_processor._batch_processor.emit(telemetry) # Wait a bit for the worker thread to wake up and call export. time.sleep(0.1) exporter.export.assert_called_once() after_export = time.time_ns() # Shows the worker's 30 second sleep was interrupted within a second. assert after_export - before_export < 1e9 batch_processor.shutdown() # pylint: disable=no-self-use def test_telemetry_exported_once_schedule_delay_reached(self, batch_processor_class, telemetry): exporter = Mock() batch_processor = batch_processor_class( exporter, max_queue_size=15, max_export_batch_size=15, schedule_delay_millis=100, export_timeout_millis=500, ) batch_processor._batch_processor.emit(telemetry) time.sleep(0.2) exporter.export.assert_called_once_with([telemetry]) batch_processor.shutdown() def test_telemetry_flushed_before_shutdown_and_dropped_after_shutdown(self, batch_processor_class, telemetry): exporter = Mock() batch_processor = batch_processor_class( exporter, # Neither of these thresholds should be hit before test ends. max_queue_size=15, max_export_batch_size=15, schedule_delay_millis=30000, export_timeout_millis=500, ) # This log should be flushed because it was written before shutdown. batch_processor._batch_processor.emit(telemetry) batch_processor.shutdown() exporter.export.assert_called_once_with([telemetry]) assert batch_processor._batch_processor._shutdown is True # This should not be flushed. batch_processor._batch_processor.emit(telemetry) exporter.export.assert_called_once() # pylint: disable=no-self-use def test_force_flush_flushes_telemetry(self, batch_processor_class, telemetry): exporter = Mock() batch_processor = batch_processor_class( exporter, # Neither of these thresholds should be hit before test ends. max_queue_size=15, max_export_batch_size=15, schedule_delay_millis=30000, export_timeout_millis=500, ) for _ in range(10): batch_processor._batch_processor.emit(telemetry) batch_processor.force_flush() exporter.export.assert_called_once_with([telemetry for _ in range(10)]) batch_processor.shutdown() @unittest.skipUnless( hasattr(os, "fork"), "needs *nix", ) def test_batch_telemetry_record_processor_fork(self, batch_processor_class, telemetry): exporter = Mock() batch_processor = batch_processor_class( exporter, max_queue_size=200, max_export_batch_size=10, schedule_delay_millis=30000, export_timeout_millis=500, ) # This telemetry should be flushed only from the parent process. # _at_fork_reinit should be called in the child process, to # clear the logs/spans in the child process. for _ in range(9): batch_processor._batch_processor.emit(telemetry) def child(conn): for _ in range(100): batch_processor._batch_processor.emit(telemetry) batch_processor.force_flush() # Expect force flush to export 10 batches of max export batch size (10) conn.send(exporter.export.call_count == 10) conn.close() parent_conn, child_conn = multiprocessing.Pipe() process = multiprocessing.Process(target=child, args=(child_conn,)) process.start() assert parent_conn.recv() is True process.join() batch_processor.force_flush() # Single export for the telemetry we emitted at the start of the test. assert exporter.export.call_count == 1 batch_processor.shutdown() def test_record_processor_is_garbage_collected(self, batch_processor_class, telemetry): exporter = Mock() processor = batch_processor_class(exporter) weak_ref = weakref.ref(processor) processor.shutdown() # When the processor is garbage collected del processor gc.collect() # Then the reference to the processor should no longer exist assert weak_ref() is None @unittest.skipUnless( hasattr(os, "fork") and hasattr(os, "register_at_fork"), "needs fork and register_at_fork", ) def test_garbage_collected_processor_does_not_crash_on_fork(self, batch_processor_class, telemetry): exporter = Mock() processor = batch_processor_class(exporter) w_ref = weakref.ref(processor) processor.shutdown() del processor gc.collect() assert w_ref() is None # The bug causes an unraisable exception to be printed to stderr. # We redirect stderr to a pipe before fork to capture it. r_fd, w_fd = os.pipe() # Save original stderr and redirect it to the pipe # We also temporarily restore the default sys.unraisablehook. Pytest overrides # it to capture exceptions in memory, which would be lost on os._exit(0). old_fd = os.dup(sys.stderr.fileno()) old_hook = sys.unraisablehook try: os.dup2(w_fd, sys.stderr.fileno()) sys.unraisablehook = sys.__unraisablehook__ pid = os.fork() if pid == 0: os.close(r_fd) # os.fork() has already run the at_fork hooks. os._exit(0) os.waitpid(pid, 0) finally: # Restore original stderr and hook in parent sys.unraisablehook = old_hook os.dup2(old_fd, sys.stderr.fileno()) os.close(old_fd) os.close(w_fd) os.set_blocking(r_fd, False) child_stderr = b"" while True: try: chunk = os.read(r_fd, 4096) if not chunk: break child_stderr += chunk except BlockingIOError: break child_stderr = child_stderr.decode("utf-8") os.close(r_fd) assert "TypeError" not in child_stderr def test_shutdown_allows_1_export_to_finish(self, batch_processor_class, telemetry): # This exporter throws an exception if it's export sleep cannot finish. exporter = MockExporterForTesting(export_sleep=2) processor = batch_processor_class( exporter, max_queue_size=200, max_export_batch_size=1, schedule_delay_millis=30000, ) # Max export batch size is 1, so 3 emit calls requires 3 separate calls (each block for 2 seconds) to Export to clear the queue. processor._batch_processor.emit(telemetry) processor._batch_processor.emit(telemetry) processor._batch_processor.emit(telemetry) before = time.monotonic() processor._batch_processor.shutdown(timeout_millis=3000) after = time.monotonic() assert after - before < 3.3 # The exporter shutdown interrupts the in-progress export. Depending # on thread scheduling, the worker may stop before or shortly after # shutdown() returns. processor._batch_processor._worker_thread.join(timeout=1) assert processor._batch_processor._worker_thread.is_alive() is False # Expect the second call to be interrupted by shutdown, and the third call to never be made. assert exporter.sleep_interrupted is True assert 2 == exporter.num_export_calls class TestCommonFuncs(unittest.TestCase): def test_duplicate_logs_filter_works(self): test_logger = logging.getLogger("testLogger") test_logger.addFilter(DuplicateFilter()) with self.assertLogs("testLogger") as cm: test_logger.info("message") test_logger.info("message") self.assertEqual(len(cm.output), 1) python-opentelemetry-1.45.2/opentelemetry-sdk/tests/test_configurator.py000066400000000000000000001564251527070070700267700ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # type: ignore # pylint: skip-file from __future__ import annotations import logging import logging.config from collections.abc import Iterable, Sequence from logging import WARNING, getLogger from os import environ from unittest import TestCase, mock from unittest.mock import Mock, patch from pytest import raises from opentelemetry import trace from opentelemetry.context import Context from opentelemetry.environment_variables import OTEL_PYTHON_ID_GENERATOR from opentelemetry.sdk._configuration import ( _EXPORTER_OTLP, _EXPORTER_OTLP_PROTO_GRPC, _EXPORTER_OTLP_PROTO_HTTP, _get_exporter_names, _get_id_generator, _get_logger_configurator, _get_meter_configurator, _get_sampler, _get_tracer_configurator, _import_config_components, _import_exporters, _import_id_generator, _import_logger_configurator, _import_meter_configurator, _import_sampler, _import_tracer_configurator, _init_logging, _init_metrics, _init_tracing, _initialize_components, _OTelSDKConfigurator, ) from opentelemetry.sdk._logs import LoggingHandler, LogRecordProcessor from opentelemetry.sdk._logs._internal import _RuleBasedLoggerConfigurator from opentelemetry.sdk._logs._internal.export import LogRecordExporter from opentelemetry.sdk._logs.export import ( ConsoleLogRecordExporter, SimpleLogRecordProcessor, ) from opentelemetry.sdk.environment_variables import ( OTEL_PYTHON_LOGGER_CONFIGURATOR, OTEL_PYTHON_METER_CONFIGURATOR, OTEL_TRACES_SAMPLER, OTEL_TRACES_SAMPLER_ARG, ) from opentelemetry.sdk.metrics import MeterProvider from opentelemetry.sdk.metrics._internal import ( _default_meter_configurator, _RuleBasedMeterConfigurator, ) from opentelemetry.sdk.metrics.export import ( AggregationTemporality, ConsoleMetricExporter, Metric, MetricExporter, MetricReader, ) from opentelemetry.sdk.metrics.view import Aggregation from opentelemetry.sdk.resources import SERVICE_NAME, Resource from opentelemetry.sdk.trace import SpanProcessor, _RuleBasedTracerConfigurator from opentelemetry.sdk.trace.export import ( ConsoleSpanExporter, SimpleSpanProcessor, ) from opentelemetry.sdk.trace.id_generator import IdGenerator, RandomIdGenerator from opentelemetry.sdk.trace.sampling import ( ALWAYS_ON, Decision, ParentBased, Sampler, SamplingResult, TraceIdRatioBased, ) from opentelemetry.test.mock_test_classes import IterEntryPoint from opentelemetry.trace import Link, SpanKind from opentelemetry.trace.span import TraceState from opentelemetry.util.types import Attributes class Provider: def __init__( self, resource=None, sampler=None, id_generator=None, *, _tracer_configurator=None, ): self.sampler = sampler self.id_generator = id_generator self.processors = [] self._tracer_configurator = _tracer_configurator self.resource = resource or Resource.create({}) def add_span_processor(self, processor): self.processors.append(processor) class DummyLoggerProvider: def __init__(self, resource=None, *, _logger_configurator=None): self.resource = resource self.processors = [] self._logger_configurator = _logger_configurator def add_log_record_processor(self, processor): self.processors.append(processor) def get_logger(self, name, *args, **kwargs): return DummyLogger(name, self.resource, self.processors) def force_flush(self, *args, **kwargs): pass class DummyMeterProvider(MeterProvider): pass class DummyLogger: def __init__(self, name, resource, processors): self.name = name self.resource = resource self.processors = processors def emit( self, record=None, *, timestamp=None, observed_timestamp=None, context=None, severity_number=None, severity_text=None, body=None, attributes=None, event_name=None, ): for processor in self.processors: processor.emit(record) class DummyLogRecordProcessor: def __init__(self, exporter): self.exporter = exporter def emit(self, record): self.exporter.export([record]) def force_flush(self, time): pass def shutdown(self): pass class Processor: def __init__(self, exporter): self.exporter = exporter class DummyMetricReader(MetricReader): def __init__( self, exporter: MetricExporter, preferred_temporality: dict[type, AggregationTemporality] | None = None, preferred_aggregation: dict[type, Aggregation] | None = None, export_interval_millis: float | None = None, export_timeout_millis: float | None = None, ) -> None: super().__init__( preferred_temporality=preferred_temporality, preferred_aggregation=preferred_aggregation, ) self.exporter = exporter def _receive_metrics( self, metrics: Iterable[Metric], timeout_millis: float = 10_000, **kwargs, ) -> None: self.exporter.export(None) def shutdown(self, timeout_millis: float = 30_000, **kwargs) -> None: return True # MetricReader that can be configured as a pull exporter class DummyMetricReaderPullExporter(MetricReader): def _receive_metrics( self, metrics: Iterable[Metric], timeout_millis: float = 10_000, **kwargs, ) -> None: pass def shutdown(self, timeout_millis: float = 30_000, **kwargs) -> None: return True class DummyOTLPMetricExporter: def __init__(self, compression: str | None = None, *args, **kwargs): self.export_called = False self.compression = compression def export(self, batch): self.export_called = True def shutdown(self): pass class Exporter: def __init__(self): tracer_provider = trace.get_tracer_provider() self.service_name = ( tracer_provider.resource.attributes[SERVICE_NAME] if getattr(tracer_provider, "resource", None) else Resource.create().attributes.get(SERVICE_NAME) ) def shutdown(self): pass class OTLPSpanExporter: def __init__(self, compression: str | None = None, *args, **kwargs): self.compression = compression class DummyOTLPLogExporter(LogRecordExporter): def __init__(self, compression: str | None = None, *args, **kwargs): self.export_called = False self.compression = compression def export(self, batch): self.export_called = True def shutdown(self): pass def force_flush(self, timeout_millis: int = 10_000) -> bool: return True class CustomSampler(Sampler): def __init__(self) -> None: pass def get_description(self) -> str: return "CustomSampler" def should_sample( self, parent_context: Context | None, trace_id: int, name: str, kind: SpanKind = None, attributes: Attributes = None, links: Sequence[Link] = None, trace_state: TraceState = None, ) -> SamplingResult: return SamplingResult( Decision.RECORD_AND_SAMPLE, None, None, ) class CustomRatioSampler(TraceIdRatioBased): def __init__(self, ratio): if not isinstance(ratio, float): raise ValueError("CustomRatioSampler ratio argument is not a float.") self.ratio = ratio super().__init__(ratio) def get_description(self) -> str: return "CustomSampler" def should_sample( self, parent_context: Context | None, trace_id: int, name: str, kind: SpanKind | None = None, attributes: Attributes = None, links: Sequence[Link] | None = None, trace_state: TraceState | None = None, ) -> SamplingResult: return SamplingResult( Decision.RECORD_AND_SAMPLE, None, None, ) class CustomSamplerFactory: @staticmethod def get_custom_sampler(unused_sampler_arg): return CustomSampler() @staticmethod def get_custom_ratio_sampler(sampler_arg): return CustomRatioSampler(float(sampler_arg)) @staticmethod def empty_get_custom_sampler(sampler_arg): return class CustomIdGenerator(IdGenerator): def generate_span_id(self): pass def generate_trace_id(self): pass def is_trace_id_random(self): return False class TestTraceInit(TestCase): def setUp(self): super() self.get_provider_patcher = patch("opentelemetry.sdk._configuration.TracerProvider", Provider) self.get_processor_patcher = patch("opentelemetry.sdk._configuration.BatchSpanProcessor", Processor) self.set_provider_patcher = patch("opentelemetry.sdk._configuration.set_tracer_provider") self.get_provider_mock = self.get_provider_patcher.start() self.get_processor_mock = self.get_processor_patcher.start() self.set_provider_mock = self.set_provider_patcher.start() def tearDown(self): super() self.get_provider_patcher.stop() self.get_processor_patcher.stop() self.set_provider_patcher.stop() # pylint: disable=protected-access @patch.dict(environ, {"OTEL_RESOURCE_ATTRIBUTES": "service.name=my-test-service"}) def test_trace_init_default(self): auto_resource = Resource.create( { "telemetry.auto.version": "test-version", } ) _init_tracing( {"zipkin": Exporter}, id_generator=RandomIdGenerator(), resource=auto_resource, ) self.assertEqual(self.set_provider_mock.call_count, 1) provider = self.set_provider_mock.call_args[0][0] self.assertIsInstance(provider, Provider) self.assertIsInstance(provider.id_generator, RandomIdGenerator) self.assertEqual(len(provider.processors), 1) self.assertIsInstance(provider.processors[0], Processor) self.assertIsInstance(provider.processors[0].exporter, Exporter) self.assertEqual(provider.processors[0].exporter.service_name, "my-test-service") self.assertEqual( provider.resource.attributes.get("telemetry.auto.version"), "test-version", ) @patch.dict( environ, {"OTEL_RESOURCE_ATTRIBUTES": "service.name=my-otlp-test-service"}, ) def test_trace_init_otlp(self): _init_tracing({"otlp": OTLPSpanExporter}, id_generator=RandomIdGenerator()) self.assertEqual(self.set_provider_mock.call_count, 1) provider = self.set_provider_mock.call_args[0][0] self.assertIsInstance(provider, Provider) self.assertIsInstance(provider.id_generator, RandomIdGenerator) self.assertEqual(len(provider.processors), 1) self.assertIsInstance(provider.processors[0], Processor) self.assertIsInstance(provider.processors[0].exporter, OTLPSpanExporter) self.assertIsInstance(provider.resource, Resource) self.assertEqual( provider.resource.attributes.get("service.name"), "my-otlp-test-service", ) def test_trace_init_exporter_uses_exporter_args_map(self): _init_tracing( {"otlp": OTLPSpanExporter}, id_generator=RandomIdGenerator(), exporter_args_map={ OTLPSpanExporter: {"compression": "gzip"}, DummyMetricReaderPullExporter: {"compression": "no"}, }, ) provider = self.set_provider_mock.call_args[0][0] self.assertEqual(len(provider.processors), 1) exporter = provider.processors[0].exporter self.assertEqual(exporter.compression, "gzip") def test_trace_init_custom_span_processors(self): span_processor = mock.Mock(spec=SpanProcessor) _init_tracing( {"otlp": OTLPSpanExporter}, id_generator=RandomIdGenerator(), span_processors=[span_processor], export_span_processor=SimpleSpanProcessor, ) provider = self.set_provider_mock.call_args[0][0] self.assertEqual(len(provider.processors), 2) self.assertEqual(provider.processors[0], span_processor) self.assertTrue(isinstance(provider.processors[1], SimpleSpanProcessor)) @patch.dict(environ, {OTEL_PYTHON_ID_GENERATOR: "custom_id_generator"}) @patch("opentelemetry.sdk._configuration.IdGenerator", new=IdGenerator) @patch("opentelemetry.sdk._configuration.entry_points") def test_trace_init_custom_id_generator(self, mock_entry_points): mock_entry_points.configure_mock(return_value=[IterEntryPoint("custom_id_generator", CustomIdGenerator)]) id_generator_name = _get_id_generator() id_generator = _import_id_generator(id_generator_name) _init_tracing({}, id_generator=id_generator) provider = self.set_provider_mock.call_args[0][0] self.assertIsInstance(provider.id_generator, CustomIdGenerator) @patch.dict("os.environ", {OTEL_TRACES_SAMPLER: "non_existent_entry_point"}) def test_trace_init_custom_sampler_with_env_non_existent_entry_point(self): sampler_name = _get_sampler() with self.assertLogs(level=WARNING): sampler = _import_sampler(sampler_name) _init_tracing({}, sampler=sampler) provider = self.set_provider_mock.call_args[0][0] self.assertIsNone(provider.sampler) @patch("opentelemetry.sdk._configuration.entry_points") @patch.dict("os.environ", {OTEL_TRACES_SAMPLER: "custom_sampler_factory"}) def test_trace_init_custom_sampler_with_env(self, mock_entry_points): mock_entry_points.configure_mock( return_value=[ IterEntryPoint( "custom_sampler_factory", CustomSamplerFactory.get_custom_sampler, ) ] ) sampler_name = _get_sampler() sampler = _import_sampler(sampler_name) _init_tracing({}, sampler=sampler) provider = self.set_provider_mock.call_args[0][0] self.assertIsInstance(provider.sampler, CustomSampler) @patch("opentelemetry.sdk._configuration.entry_points") @patch.dict("os.environ", {OTEL_TRACES_SAMPLER: "custom_sampler_factory"}) def test_trace_init_custom_sampler_with_env_bad_factory(self, mock_entry_points): mock_entry_points.configure_mock( return_value=[ IterEntryPoint( "custom_sampler_factory", CustomSamplerFactory.empty_get_custom_sampler, ) ] ) sampler_name = _get_sampler() with self.assertLogs(level=WARNING): sampler = _import_sampler(sampler_name) _init_tracing({}, sampler=sampler) provider = self.set_provider_mock.call_args[0][0] self.assertIsNone(provider.sampler) @patch("opentelemetry.sdk._configuration.entry_points") @patch.dict( "os.environ", { OTEL_TRACES_SAMPLER: "custom_sampler_factory", OTEL_TRACES_SAMPLER_ARG: "0.5", }, ) def test_trace_init_custom_sampler_with_env_unused_arg(self, mock_entry_points): mock_entry_points.configure_mock( return_value=[ IterEntryPoint( "custom_sampler_factory", CustomSamplerFactory.get_custom_sampler, ) ] ) sampler_name = _get_sampler() sampler = _import_sampler(sampler_name) _init_tracing({}, sampler=sampler) provider = self.set_provider_mock.call_args[0][0] self.assertIsInstance(provider.sampler, CustomSampler) @patch("opentelemetry.sdk._configuration.entry_points") @patch.dict( "os.environ", { OTEL_TRACES_SAMPLER: "custom_ratio_sampler_factory", OTEL_TRACES_SAMPLER_ARG: "0.5", }, ) def test_trace_init_custom_ratio_sampler_with_env(self, mock_entry_points): mock_entry_points.configure_mock( return_value=[ IterEntryPoint( "custom_ratio_sampler_factory", CustomSamplerFactory.get_custom_ratio_sampler, ) ] ) sampler_name = _get_sampler() sampler = _import_sampler(sampler_name) _init_tracing({}, sampler=sampler) provider = self.set_provider_mock.call_args[0][0] self.assertIsInstance(provider.sampler, CustomRatioSampler) self.assertEqual(provider.sampler.ratio, 0.5) @patch("opentelemetry.sdk._configuration.entry_points") @patch.dict( "os.environ", { OTEL_TRACES_SAMPLER: "custom_ratio_sampler_factory", OTEL_TRACES_SAMPLER_ARG: "foobar", }, ) def test_trace_init_custom_ratio_sampler_with_env_bad_arg(self, mock_entry_points): mock_entry_points.configure_mock( return_value=[ IterEntryPoint( "custom_ratio_sampler_factory", CustomSamplerFactory.get_custom_ratio_sampler, ) ] ) sampler_name = _get_sampler() with self.assertLogs(level=WARNING): sampler = _import_sampler(sampler_name) _init_tracing({}, sampler=sampler) provider = self.set_provider_mock.call_args[0][0] self.assertIsNone(provider.sampler) @patch("opentelemetry.sdk._configuration.entry_points") @patch.dict( "os.environ", { OTEL_TRACES_SAMPLER: "custom_ratio_sampler_factory", }, ) def test_trace_init_custom_ratio_sampler_with_env_missing_arg(self, mock_entry_points): mock_entry_points.configure_mock( return_value=[ IterEntryPoint( "custom_ratio_sampler_factory", CustomSamplerFactory.get_custom_ratio_sampler, ) ] ) sampler_name = _get_sampler() with self.assertLogs(level=WARNING): sampler = _import_sampler(sampler_name) _init_tracing({}, sampler=sampler) provider = self.set_provider_mock.call_args[0][0] self.assertIsNone(provider.sampler) @patch("opentelemetry.sdk._configuration.entry_points") @patch.dict( "os.environ", { OTEL_TRACES_SAMPLER: "custom_sampler_factory", OTEL_TRACES_SAMPLER_ARG: "0.5", }, ) def test_trace_init_custom_ratio_sampler_with_env_multiple_entry_points(self, mock_entry_points): mock_entry_points.configure_mock( return_value=[ IterEntryPoint( "custom_sampler_factory", CustomSamplerFactory.get_custom_sampler, ), ] ) sampler_name = _get_sampler() sampler = _import_sampler(sampler_name) _init_tracing({}, sampler=sampler) provider = self.set_provider_mock.call_args[0][0] self.assertIsInstance(provider.sampler, CustomSampler) def verify_default_sampler(self, tracer_provider): self.assertIsInstance(tracer_provider.sampler, ParentBased) # pylint: disable=protected-access self.assertEqual(tracer_provider.sampler._root, ALWAYS_ON) @patch.dict( "os.environ", {"OTEL_PYTHON_TRACER_CONFIGURATOR": "non_existent_entry_point"}, ) def test_trace_init_custom_tracer_configurator_with_env_non_existent_entry_point( self, ): tracer_configurator_name = _get_tracer_configurator() with self.assertLogs(level=WARNING): tracer_configurator = _import_tracer_configurator(tracer_configurator_name) _init_tracing({}, tracer_configurator=tracer_configurator) @patch("opentelemetry.sdk._configuration.entry_points") @patch.dict( "os.environ", {"OTEL_PYTHON_TRACER_CONFIGURATOR": "custom_tracer_configurator"}, ) def test_trace_init_custom_tracer_configurator_with_env(self, mock_entry_points): def custom_tracer_configurator(tracer_scope): return mock.Mock(spec=_RuleBasedTracerConfigurator)(tracer_scope) mock_entry_points.configure_mock( return_value=[ IterEntryPoint( "custom_tracer_configurator", custom_tracer_configurator, ) ] ) tracer_configurator_name = _get_tracer_configurator() tracer_configurator = _import_tracer_configurator(tracer_configurator_name) _init_tracing({}, tracer_configurator=tracer_configurator) provider = self.set_provider_mock.call_args[0][0] self.assertEqual(provider._tracer_configurator, custom_tracer_configurator) class TestLoggingInit(TestCase): def setUp(self): self.processor_patch = patch( "opentelemetry.sdk._configuration.BatchLogRecordProcessor", DummyLogRecordProcessor, ) self.provider_patch = patch( "opentelemetry.sdk._configuration.LoggerProvider", DummyLoggerProvider, ) self.set_provider_patch = patch("opentelemetry.sdk._configuration.set_logger_provider") self.processor_mock = self.processor_patch.start() self.provider_mock = self.provider_patch.start() self.set_provider_mock = self.set_provider_patch.start() def tearDown(self): self.processor_patch.stop() self.set_provider_patch.stop() self.provider_patch.stop() root_logger = getLogger("root") root_logger.handlers = [handler for handler in root_logger.handlers if not isinstance(handler, LoggingHandler)] def test_logging_init_empty(self): with ResetGlobalLoggingState(): auto_resource = Resource.create( { "telemetry.auto.version": "auto-version", } ) _init_logging({}, resource=auto_resource) self.assertEqual(self.set_provider_mock.call_count, 1) provider = self.set_provider_mock.call_args[0][0] self.assertIsInstance(provider, DummyLoggerProvider) self.assertIsInstance(provider.resource, Resource) self.assertEqual( provider.resource.attributes.get("telemetry.auto.version"), "auto-version", ) @patch.dict( environ, {"OTEL_RESOURCE_ATTRIBUTES": "service.name=otlp-service"}, ) def test_logging_init_exporter(self): with ResetGlobalLoggingState(): resource = Resource.create({}) _init_logging({"otlp": DummyOTLPLogExporter}, resource=resource) self.assertEqual(self.set_provider_mock.call_count, 1) provider = self.set_provider_mock.call_args[0][0] self.assertIsInstance(provider, DummyLoggerProvider) self.assertIsInstance(provider.resource, Resource) self.assertEqual( provider.resource.attributes.get("service.name"), "otlp-service", ) self.assertEqual(len(provider.processors), 1) self.assertIsInstance(provider.processors[0], DummyLogRecordProcessor) self.assertIsInstance(provider.processors[0].exporter, DummyOTLPLogExporter) getLogger(__name__).error("hello") self.assertEqual(len(provider.processors), 1) self.assertTrue(provider.processors[0].exporter.export_called) def test_logging_init_exporter_uses_exporter_args_map(self): with ResetGlobalLoggingState(): resource = Resource.create({}) _init_logging( {"otlp": DummyOTLPLogExporter}, resource=resource, exporter_args_map={ DummyOTLPLogExporter: {"compression": "gzip"}, DummyOTLPMetricExporter: {"compression": "no"}, }, ) self.assertEqual(self.set_provider_mock.call_count, 1) provider = self.set_provider_mock.call_args[0][0] self.assertEqual(len(provider.processors), 1) self.assertEqual(provider.processors[0].exporter.compression, "gzip") def test_logging_init_custom_log_record_processors(self): log_record_processor = mock.Mock(spec=LogRecordProcessor) with ResetGlobalLoggingState(): resource = Resource.create({}) _init_logging( {"otlp": DummyOTLPLogExporter}, resource=resource, log_record_processors=[log_record_processor], export_log_record_processor=SimpleLogRecordProcessor, ) provider = self.set_provider_mock.call_args[0][0] self.assertEqual(len(provider.processors), 2) self.assertEqual(provider.processors[0], log_record_processor) self.assertIsInstance(provider.processors[1], SimpleLogRecordProcessor) @patch.dict( environ, {"OTEL_RESOURCE_ATTRIBUTES": "service.name=otlp-service"}, ) def test_logging_init_exporter_without_handler_setup(self): resource = Resource.create({}) _init_logging( {"otlp": DummyOTLPLogExporter}, resource=resource, setup_logging_handler=False, ) self.assertEqual(self.set_provider_mock.call_count, 1) provider = self.set_provider_mock.call_args[0][0] self.assertIsInstance(provider, DummyLoggerProvider) self.assertIsInstance(provider.resource, Resource) self.assertEqual( provider.resource.attributes.get("service.name"), "otlp-service", ) self.assertEqual(len(provider.processors), 1) self.assertIsInstance(provider.processors[0], DummyLogRecordProcessor) self.assertIsInstance(provider.processors[0].exporter, DummyOTLPLogExporter) getLogger(__name__).error("hello") self.assertFalse(provider.processors[0].exporter.export_called) def test_logging_init_with_setup_logging_handler_to_true_warns(self): resource = Resource.create({}) with self.assertWarnsRegex( DeprecationWarning, "and the `LoggingHandler` in `opentelemetry-sdk` that it controls are deprecated", ): with ResetGlobalLoggingState(): _init_logging( {"otlp": DummyOTLPLogExporter}, resource=resource, setup_logging_handler=True, ) @patch.dict( environ, {"OTEL_RESOURCE_ATTRIBUTES": "service.name=otlp-service"}, ) @patch("opentelemetry.sdk._configuration._init_tracing") @patch("opentelemetry.sdk._configuration._init_logging") def test_logging_init_disable_default(self, logging_mock, tracing_mock): _initialize_components(auto_instrumentation_version="auto-version") self.assertEqual(tracing_mock.call_count, 1) logging_mock.assert_called_once_with( mock.ANY, mock.ANY, False, exporter_args_map=None, log_record_processors=None, export_log_record_processor=None, logger_configurator=None, ) @patch.dict( environ, { "OTEL_RESOURCE_ATTRIBUTES": "service.name=otlp-service", "OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED": "True", }, ) @patch("opentelemetry.sdk._configuration._init_tracing") @patch("opentelemetry.sdk._configuration._init_logging") def test_logging_init_enable_env(self, logging_mock, tracing_mock): with self.assertLogs(level=WARNING): _initialize_components(auto_instrumentation_version="auto-version") logging_mock.assert_called_once_with( mock.ANY, mock.ANY, True, exporter_args_map=None, log_record_processors=None, export_log_record_processor=None, logger_configurator=None, ) self.assertEqual(tracing_mock.call_count, 1) @patch.dict( environ, { "OTEL_RESOURCE_ATTRIBUTES": "service.name=otlp-service", "OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED": "True", }, ) @patch("opentelemetry.sdk._configuration._init_tracing") @patch("opentelemetry.sdk._configuration._init_logging") @patch("opentelemetry.sdk._configuration._init_metrics") def test_initialize_components_resource(self, metrics_mock, logging_mock, tracing_mock): _initialize_components(auto_instrumentation_version="auto-version") self.assertEqual(logging_mock.call_count, 1) self.assertEqual(tracing_mock.call_count, 1) self.assertEqual(metrics_mock.call_count, 1) _, args, _ = logging_mock.mock_calls[0] logging_resource = args[1] _, _, kwargs = tracing_mock.mock_calls[0] tracing_resource = kwargs["resource"] _, args, _ = metrics_mock.mock_calls[0] metrics_resource = kwargs["resource"] self.assertEqual(logging_resource, tracing_resource) self.assertEqual(logging_resource, metrics_resource) self.assertEqual(tracing_resource, metrics_resource) @patch.dict( environ, { "OTEL_TRACES_EXPORTER": _EXPORTER_OTLP, "OTEL_METRICS_EXPORTER": _EXPORTER_OTLP_PROTO_GRPC, "OTEL_LOGS_EXPORTER": _EXPORTER_OTLP_PROTO_HTTP, }, ) @patch.dict( environ, { "OTEL_RESOURCE_ATTRIBUTES": "service.name=otlp-service, custom.key.1=env-value", "OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED": "False", }, ) @patch("opentelemetry.sdk._configuration.Resource") @patch("opentelemetry.sdk._configuration._import_exporters") @patch("opentelemetry.sdk._configuration._get_exporter_names") @patch("opentelemetry.sdk._configuration._init_tracing") @patch("opentelemetry.sdk._configuration._init_logging") @patch("opentelemetry.sdk._configuration._init_metrics") def test_initialize_components_kwargs( self, metrics_mock, logging_mock, tracing_mock, exporter_names_mock, import_exporters_mock, resource_mock, ): exporter_names_mock.return_value = [ "env_var_exporter_1", "env_var_exporter_2", ] import_exporters_mock.return_value = ( "TEST_SPAN_EXPORTERS_DICT", "TEST_METRICS_EXPORTERS_DICT", "TEST_LOG_EXPORTERS_DICT", ) resource_mock.create.return_value = "TEST_RESOURCE" kwargs = { "auto_instrumentation_version": "auto-version", "trace_exporter_names": ["custom_span_exporter"], "metric_exporter_names": ["custom_metric_exporter"], "log_exporter_names": ["custom_log_exporter"], "sampler": "TEST_SAMPLER", "resource_attributes": { "custom.key.1": "pass-in-value-1", "custom.key.2": "pass-in-value-2", }, "id_generator": "TEST_GENERATOR", "setup_logging_handler": True, "exporter_args_map": {1: {"compression": "gzip"}}, "export_log_record_processor": SimpleLogRecordProcessor, "export_span_processor": SimpleSpanProcessor, "log_record_processors": [], "span_processors": [], "tracer_configurator": "tracer_configurator_test", "meter_configurator": "meter_configurator_test", } _initialize_components(**kwargs) import_exporters_mock.assert_called_once_with( [ "custom_span_exporter", "env_var_exporter_1", "env_var_exporter_2", ], [ "custom_metric_exporter", "env_var_exporter_1", "env_var_exporter_2", ], [ "custom_log_exporter", "env_var_exporter_1", "env_var_exporter_2", ], ) resource_mock.create.assert_called_once_with( { "telemetry.auto.version": "auto-version", "custom.key.1": "pass-in-value-1", "custom.key.2": "pass-in-value-2", } ) # Resource is checked separates tracing_mock.assert_called_once_with( exporters="TEST_SPAN_EXPORTERS_DICT", id_generator="TEST_GENERATOR", sampler="TEST_SAMPLER", resource="TEST_RESOURCE", exporter_args_map={1: {"compression": "gzip"}}, span_processors=[], export_span_processor=SimpleSpanProcessor, tracer_configurator="tracer_configurator_test", ) metrics_mock.assert_called_once_with( exporters_or_readers="TEST_METRICS_EXPORTERS_DICT", resource="TEST_RESOURCE", exporter_args_map={1: {"compression": "gzip"}}, meter_configurator="meter_configurator_test", ) logging_mock.assert_called_once_with( "TEST_LOG_EXPORTERS_DICT", "TEST_RESOURCE", True, exporter_args_map={1: {"compression": "gzip"}}, log_record_processors=[], export_log_record_processor=SimpleLogRecordProcessor, logger_configurator=None, ) def test_basicConfig_works_with_otel_handler(self): with ResetGlobalLoggingState(): _init_logging( {"otlp": DummyOTLPLogExporter}, Resource.create({}), setup_logging_handler=True, ) 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): with ResetGlobalLoggingState(): _init_logging( {"otlp": DummyOTLPLogExporter}, Resource.create({}), setup_logging_handler=True, ) 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", ) def test_dictConfig_preserves_otel_handler(self): with ResetGlobalLoggingState(): _init_logging( {"otlp": DummyOTLPLogExporter}, Resource.create({}), setup_logging_handler=True, ) root = logging.getLogger() self.assertEqual( len(root.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.handlers), 2) logging_handlers = [h for h in root.handlers if isinstance(h, LoggingHandler)] self.assertEqual( len(logging_handlers), 1, "Should still have exactly one OpenTelemetry LoggingHandler", ) def test_logging_init_logger_configurator_none_by_default(self): with ResetGlobalLoggingState(): _init_logging({}) provider = self.set_provider_mock.call_args[0][0] self.assertIsInstance(provider, DummyLoggerProvider) self.assertIsNone(provider._logger_configurator) def test_logging_init_logger_configurator_passed_directly(self): mock_configurator = Mock() with ResetGlobalLoggingState(): _init_logging({}, logger_configurator=mock_configurator) provider = self.set_provider_mock.call_args[0][0] self.assertIsInstance(provider, DummyLoggerProvider) self.assertEqual(provider._logger_configurator, mock_configurator) @patch.dict( "os.environ", {OTEL_PYTHON_LOGGER_CONFIGURATOR: "non_existent_entry_point"}, ) def test_logging_init_custom_logger_configurator_with_env_non_existent_entry_point( self, ): logger_configurator_name = _get_logger_configurator() with self.assertLogs(level=WARNING): logger_configurator = _import_logger_configurator(logger_configurator_name) with ResetGlobalLoggingState(): _init_logging({}, logger_configurator=logger_configurator) @patch("opentelemetry.sdk._configuration.entry_points") @patch.dict( "os.environ", {OTEL_PYTHON_LOGGER_CONFIGURATOR: "custom_logger_configurator"}, ) def test_logging_init_custom_logger_configurator_with_env(self, mock_entry_points): def custom_logger_configurator(logger_scope): return mock.Mock(spec=_RuleBasedLoggerConfigurator)(logger_scope) mock_entry_points.configure_mock( return_value=[ IterEntryPoint( "custom_logger_configurator", custom_logger_configurator, ) ] ) logger_configurator_name = _get_logger_configurator() logger_configurator = _import_logger_configurator(logger_configurator_name) with ResetGlobalLoggingState(): _init_logging({}, logger_configurator=logger_configurator) provider = self.set_provider_mock.call_args[0][0] self.assertEqual(provider._logger_configurator, custom_logger_configurator) class TestMetricsInit(TestCase): def setUp(self): self.metric_reader_patch = patch( "opentelemetry.sdk._configuration.PeriodicExportingMetricReader", DummyMetricReader, ) self.provider_patch = patch( "opentelemetry.sdk._configuration.MeterProvider", DummyMeterProvider, ) self.set_provider_patch = patch("opentelemetry.sdk._configuration.set_meter_provider") self.metric_reader_mock = self.metric_reader_patch.start() self.provider_mock = self.provider_patch.start() self.set_provider_mock = self.set_provider_patch.start() def tearDown(self): self.metric_reader_patch.stop() self.set_provider_patch.stop() self.provider_patch.stop() def test_metrics_init_empty(self): auto_resource = Resource.create( { "telemetry.auto.version": "auto-version", } ) _init_metrics({}, resource=auto_resource) self.assertEqual(self.set_provider_mock.call_count, 1) provider = self.set_provider_mock.call_args[0][0] self.assertIsInstance(provider, DummyMeterProvider) self.assertIsInstance(provider._sdk_config.resource, Resource) self.assertEqual( provider._sdk_config.resource.attributes.get("telemetry.auto.version"), "auto-version", ) @patch.dict( environ, {"OTEL_RESOURCE_ATTRIBUTES": "service.name=otlp-service"}, ) def test_metrics_init_exporter(self): resource = Resource.create({}) _init_metrics({"otlp": DummyOTLPMetricExporter}, resource=resource) self.assertEqual(self.set_provider_mock.call_count, 1) provider = self.set_provider_mock.call_args[0][0] self.assertIsInstance(provider, DummyMeterProvider) self.assertIsInstance(provider._sdk_config.resource, Resource) self.assertEqual( provider._sdk_config.resource.attributes.get("service.name"), "otlp-service", ) reader = provider._metric_readers[0] self.assertIsInstance(reader, DummyMetricReader) self.assertIsInstance(reader.exporter, DummyOTLPMetricExporter) def test_metrics_init_pull_exporter(self): resource = Resource.create({}) _init_metrics( {"dummy_metric_reader": DummyMetricReaderPullExporter}, resource=resource, ) self.assertEqual(self.set_provider_mock.call_count, 1) provider = self.set_provider_mock.call_args[0][0] self.assertIsInstance(provider, DummyMeterProvider) reader = provider._metric_readers[0] self.assertIsInstance(reader, DummyMetricReaderPullExporter) def test_metrics_init_exporter_uses_exporter_args_map(self): resource = Resource.create({}) _init_metrics( {"otlp": DummyOTLPMetricExporter}, resource=resource, exporter_args_map={ DummyOTLPMetricExporter: {"compression": "gzip"}, DummyMetricReaderPullExporter: {"compression": "no"}, }, ) provider = self.set_provider_mock.call_args[0][0] reader = provider._metric_readers[0] self.assertEqual(reader.exporter.compression, "gzip") def test_metrics_init_meter_configurator_none_by_default(self): _init_metrics({}) provider = self.set_provider_mock.call_args[0][0] self.assertIsInstance(provider, DummyMeterProvider) self.assertEqual(provider._meter_configurator, _default_meter_configurator) def test_metrics_init_meter_configurator_passed_directly(self): mock_configurator = Mock() _init_metrics({}, meter_configurator=mock_configurator) provider = self.set_provider_mock.call_args[0][0] self.assertIsInstance(provider, DummyMeterProvider) self.assertEqual(provider._meter_configurator, mock_configurator) @patch.dict( "os.environ", {OTEL_PYTHON_METER_CONFIGURATOR: "non_existent_entry_point"}, ) def test_metrics_init_custom_meter_configurator_with_env_non_existent_entry_point( self, ): meter_configurator_name = _get_meter_configurator() with self.assertLogs(level=WARNING): meter_configurator = _import_meter_configurator(meter_configurator_name) _init_metrics({}, meter_configurator=meter_configurator) @patch("opentelemetry.sdk._configuration.entry_points") @patch.dict( "os.environ", {OTEL_PYTHON_METER_CONFIGURATOR: "custom_meter_configurator"}, ) def test_metrics_init_custom_meter_configurator_with_env(self, mock_entry_points): def custom_meter_configurator(meter_scope): return mock.Mock(spec=_RuleBasedMeterConfigurator)(meter_scope) mock_entry_points.configure_mock( return_value=[ IterEntryPoint( "custom_meter_configurator", custom_meter_configurator, ) ] ) meter_configurator_name = _get_meter_configurator() meter_configurator = _import_meter_configurator(meter_configurator_name) _init_metrics({}, meter_configurator=meter_configurator) provider = self.set_provider_mock.call_args[0][0] self.assertEqual(provider._meter_configurator, custom_meter_configurator) class TestExporterNames(TestCase): @patch.dict( environ, { "OTEL_TRACES_EXPORTER": _EXPORTER_OTLP, "OTEL_METRICS_EXPORTER": _EXPORTER_OTLP_PROTO_GRPC, "OTEL_LOGS_EXPORTER": _EXPORTER_OTLP_PROTO_HTTP, }, ) def test_otlp_exporter(self): self.assertEqual(_get_exporter_names("traces"), [_EXPORTER_OTLP_PROTO_GRPC]) self.assertEqual(_get_exporter_names("metrics"), [_EXPORTER_OTLP_PROTO_GRPC]) self.assertEqual(_get_exporter_names("logs"), [_EXPORTER_OTLP_PROTO_HTTP]) @patch.dict( environ, { "OTEL_TRACES_EXPORTER": _EXPORTER_OTLP, "OTEL_METRICS_EXPORTER": _EXPORTER_OTLP, "OTEL_EXPORTER_OTLP_PROTOCOL": "http/protobuf", "OTEL_EXPORTER_OTLP_METRICS_PROTOCOL": "grpc", }, ) def test_otlp_custom_exporter(self): self.assertEqual(_get_exporter_names("traces"), [_EXPORTER_OTLP_PROTO_HTTP]) self.assertEqual(_get_exporter_names("metrics"), [_EXPORTER_OTLP_PROTO_GRPC]) @patch.dict( environ, { "OTEL_TRACES_EXPORTER": _EXPORTER_OTLP_PROTO_HTTP, "OTEL_METRICS_EXPORTER": _EXPORTER_OTLP_PROTO_GRPC, "OTEL_EXPORTER_OTLP_PROTOCOL": "grpc", "OTEL_EXPORTER_OTLP_METRICS_PROTOCOL": "http/protobuf", }, ) def test_otlp_exporter_conflict(self): # Verify that OTEL_*_EXPORTER is used, and a warning is logged with self.assertLogs(level="WARNING") as logs_context: self.assertEqual(_get_exporter_names("traces"), [_EXPORTER_OTLP_PROTO_HTTP]) assert len(logs_context.output) == 1 with self.assertLogs(level="WARNING") as logs_context: self.assertEqual(_get_exporter_names("metrics"), [_EXPORTER_OTLP_PROTO_GRPC]) assert len(logs_context.output) == 1 @patch.dict(environ, {"OTEL_TRACES_EXPORTER": "zipkin"}) def test_multiple_exporters(self): self.assertEqual(sorted(_get_exporter_names("traces")), ["zipkin"]) @patch.dict(environ, {"OTEL_TRACES_EXPORTER": "none"}) def test_none_exporters(self): self.assertEqual(sorted(_get_exporter_names("traces")), []) def test_no_exporters(self): self.assertEqual(sorted(_get_exporter_names("traces")), []) @patch.dict(environ, {"OTEL_TRACES_EXPORTER": ""}) def test_empty_exporters(self): self.assertEqual(sorted(_get_exporter_names("traces")), []) class TestImportExporters(TestCase): def test_console_exporters(self): trace_exporters, metric_exporterts, logs_exporters = _import_exporters(["console"], ["console"], ["console"]) self.assertEqual(trace_exporters["console"].__class__, ConsoleSpanExporter.__class__) self.assertEqual( logs_exporters["console"].__class__, ConsoleLogRecordExporter.__class__, ) self.assertEqual( metric_exporterts["console"].__class__, ConsoleMetricExporter.__class__, ) @patch( "opentelemetry.sdk._configuration.entry_points", ) def test_metric_pull_exporter(self, mock_entry_points: Mock): def mock_entry_points_impl(group, name): if name == "dummy_pull_exporter": return [IterEntryPoint(name=name, class_type=DummyMetricReaderPullExporter)] return [] mock_entry_points.side_effect = mock_entry_points_impl _, metric_exporters, _ = _import_exporters([], ["dummy_pull_exporter"], []) self.assertIs( metric_exporters["dummy_pull_exporter"], DummyMetricReaderPullExporter, ) class TestImportConfigComponents(TestCase): @patch( "opentelemetry.sdk._configuration.entry_points", side_effect=KeyError, ) def test__import_config_components_missing_entry_point(self, mock_entry_points): with raises(RuntimeError) as error: _import_config_components(["a", "b", "c"], "name") self.assertEqual(str(error.value), "Requested entry point 'name' not found") @patch( "opentelemetry.sdk._configuration.entry_points", side_effect=StopIteration, ) def test__import_config_components_missing_component(self, mock_entry_points): with raises(RuntimeError) as error: _import_config_components(["a", "b", "c"], "name") self.assertEqual( str(error.value), "Requested component 'a' not found in entry point 'name'", ) class TestConfigurator(TestCase): class CustomConfigurator(_OTelSDKConfigurator): def _configure(self, **kwargs): kwargs["sampler"] = "TEST_SAMPLER" super()._configure(**kwargs) @patch("opentelemetry.sdk._configuration._initialize_components") def test_custom_configurator(self, mock_init_comp): custom_configurator = TestConfigurator.CustomConfigurator() custom_configurator._configure(auto_instrumentation_version="TEST_VERSION2") kwargs = { "auto_instrumentation_version": "TEST_VERSION2", "sampler": "TEST_SAMPLER", } mock_init_comp.assert_called_once_with(**kwargs) def test_custom_configurator_with_init_args(self): class ConfiguratorWithArgs(_OTelSDKConfigurator): def __init__(self, name, strict=False): super().__init__() self.name = name self.strict = strict def _configure(self, **kwargs): pass configurator = ConfiguratorWithArgs("TEST_NAME", strict=True) self.assertEqual(configurator.name, "TEST_NAME") self.assertTrue(configurator.strict) self.assertIs(configurator, ConfiguratorWithArgs("TEST_NAME", strict=True)) # 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 = 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 class TestClearLoggingHandlers(TestCase): def test_preserves_handlers(self): root_logger = getLogger() initial_handlers = root_logger.handlers[:] test_handler = logging.StreamHandler() root_logger.addHandler(test_handler) expected_handlers = initial_handlers + [test_handler] with ResetGlobalLoggingState(): self.assertEqual(len(root_logger.handlers), 0) temp_handler = logging.StreamHandler() root_logger.addHandler(temp_handler) self.assertEqual(len(root_logger.handlers), len(expected_handlers)) for h1, h2 in zip(root_logger.handlers, expected_handlers): self.assertIs(h1, h2) root_logger.removeHandler(test_handler) def test_preserves_original_logging_fns(self): def f(x): print("f") with ResetGlobalLoggingState(): logging.basicConfig = f logging.config.dictConfig = f logging.config.fileConfig = f self.assertEqual(logging.config.dictConfig.__name__, "dictConfig") self.assertEqual(logging.basicConfig.__name__, "basicConfig") self.assertEqual(logging.config.fileConfig.__name__, "fileConfig") class TestOpAMPInit(TestCase): @patch("opentelemetry.sdk._configuration._init_metrics") @patch("opentelemetry.sdk._configuration._init_tracing") @patch("opentelemetry.sdk._configuration._init_logging") @patch("opentelemetry.sdk._configuration.entry_points") @patch("opentelemetry.sdk._configuration.Resource") def test_pre_sdk_init_function_found( self, mock_resource, mock_entry_points, mock_logging, mock_tracing, mock_metrics, ): init_function = mock.Mock() mock_entry_points.side_effect = [ [IterEntryPoint("pre_sdk_init_function", init_function)], [], ] _initialize_components(id_generator=1) mock_entry_points.assert_has_calls([mock.call(group="_opentelemetry_opamp", name="pre_sdk_init_function")]) init_function.assert_called_once_with(mock_resource.create.return_value) @patch("opentelemetry.sdk._configuration._init_metrics") @patch("opentelemetry.sdk._configuration._init_tracing") @patch("opentelemetry.sdk._configuration._init_logging") @patch("opentelemetry.sdk._configuration.entry_points") @patch("opentelemetry.sdk._configuration.Resource") def test_post_sdk_init_function_found( self, mock_resource, mock_entry_points, mock_logging, mock_tracing, mock_metrics, ): init_function = mock.Mock() mock_entry_points.side_effect = [ [], [IterEntryPoint("post_sdk_init_function", init_function)], ] _initialize_components(id_generator=1) mock_entry_points.assert_has_calls([mock.call(group="_opentelemetry_opamp", name="post_sdk_init_function")]) init_function.assert_called_once_with(mock_resource.create.return_value) @patch("opentelemetry.sdk._configuration._init_metrics") @patch("opentelemetry.sdk._configuration._init_tracing") @patch("opentelemetry.sdk._configuration._init_logging") @patch("opentelemetry.sdk._configuration.entry_points") def test_init_function_load_failure(self, mock_entry_points, mock_logging, mock_tracing, mock_metrics): entry_point_mock = mock.Mock() entry_point_mock.load.side_effect = AttributeError("module 'foo' has no attribute 'OpampInit'") mock_entry_points.configure_mock( return_value=[entry_point_mock], ) entry_point_mock.__str__ = lambda x: "" with self.assertLogs(level="WARNING") as cm: _initialize_components(id_generator=1) mock_entry_points.assert_has_calls([mock.call(group="_opentelemetry_opamp", name="pre_sdk_init_function")]) self.assertIn( "WARNING:opentelemetry.sdk._configuration:Failed to load OpAMP init function from entry point," " : module 'foo' has no attribute 'OpampInit'", cm.output, ) @patch("opentelemetry.sdk._configuration._init_metrics") @patch("opentelemetry.sdk._configuration._init_tracing") @patch("opentelemetry.sdk._configuration._init_logging") @patch("opentelemetry.sdk._configuration.entry_points") def test_init_function_not_found(self, mock_entry_points, mock_logging, mock_tracing, mock_metrics): mock_entry_points.configure_mock(return_value=[]) with self.assertLogs(level="DEBUG") as cm: _initialize_components(id_generator=1) self.assertIn( "DEBUG:opentelemetry.sdk._configuration:No OpAMP init function found", cm.output, ) python-opentelemetry-1.45.2/opentelemetry-sdk/tests/test_environment_variables_internal.py000066400000000000000000000037771527070070700325570ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from unittest import TestCase from unittest.mock import patch from opentelemetry.sdk.environment_variables._internal import ( parse_boolean_environment_variable, ) class TestParseBooleanEnvironmentVariable(TestCase): def test_unset_returns_default(self): for default, expected in ( (False, False), (True, True), ): with self.subTest(default=default): with patch.dict("os.environ", {}, clear=True): self.assertEqual( parse_boolean_environment_variable("TEST_BOOL", default=default), expected, ) def test_valid_values(self): for value, expected in ( ("true", True), (" TrUe ", True), ("false", False), (" FaLsE ", False), ): with self.subTest(value=value): with patch.dict("os.environ", {"TEST_BOOL": value}): self.assertEqual( parse_boolean_environment_variable("TEST_BOOL"), expected, ) def test_invalid_value_warns_and_returns_default(self): for default, expected in ( (False, False), (True, True), ): with self.subTest(default=default): with patch.dict("os.environ", {"TEST_BOOL": "yes"}): with self.assertLogs( "opentelemetry.sdk.environment_variables._internal", level="WARNING", ) as logs: self.assertEqual( parse_boolean_environment_variable("TEST_BOOL", default=default), expected, ) self.assertIn( "Invalid value for TEST_BOOL", logs.records[0].message, ) python-opentelemetry-1.45.2/opentelemetry-sdk/tests/test_util.py000066400000000000000000000111511527070070700252250ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import copy import unittest from opentelemetry.sdk.util import BoundedList # pylint: disable=unsubscriptable-object class TestBoundedList(unittest.TestCase): base = [52, 36, 53, 29, 54, 99, 56, 48, 22, 35, 21, 65, 10, 95, 42, 60] def test_raises(self): """Test corner cases - negative list size - access out of range indexes """ with self.assertRaises(ValueError): BoundedList(-1) blist = BoundedList(4) blist.append(37) blist.append(13) with self.assertRaises(IndexError): _ = blist[2] with self.assertRaises(IndexError): _ = blist[4] with self.assertRaises(IndexError): _ = blist[-3] def test_from_seq(self): list_len = len(self.base) base_copy = list(self.base) blist = BoundedList.from_seq(list_len, base_copy) self.assertEqual(len(blist), list_len) # modify base_copy and test that blist is not changed for idx in range(list_len): base_copy[idx] = idx * base_copy[idx] for idx in range(list_len): self.assertEqual(blist[idx], self.base[idx]) # test that iter yields the correct number of elements self.assertEqual(len(tuple(blist)), list_len) # sequence too big blist = BoundedList.from_seq(list_len // 2, base_copy) self.assertEqual(len(blist), list_len // 2) self.assertEqual(blist.dropped, list_len - (list_len // 2)) def test_append_no_drop(self): """Append max capacity elements to the list without dropping elements.""" # create empty list list_len = len(self.base) blist = BoundedList(list_len) self.assertEqual(len(blist), 0) # fill list for item in self.base: blist.append(item) self.assertEqual(len(blist), list_len) self.assertEqual(blist.dropped, 0) for idx in range(list_len): self.assertEqual(blist[idx], self.base[idx]) # test __iter__ in BoundedList for idx, val in enumerate(blist): self.assertEqual(val, self.base[idx]) def test_append_drop(self): """Append more than max capacity elements and test that oldest ones are dropped.""" list_len = len(self.base) # create full BoundedList blist = BoundedList.from_seq(list_len, self.base) # try to append more items for val in self.base: # should drop the element without raising exceptions blist.append(2 * val) self.assertEqual(len(blist), list_len) self.assertEqual(blist.dropped, list_len) # test that new elements are in the list for idx in range(list_len): self.assertEqual(blist[idx], 2 * self.base[idx]) def test_extend_no_drop(self): # create empty list list_len = len(self.base) blist = BoundedList(list_len) self.assertEqual(len(blist), 0) # fill list blist.extend(self.base) self.assertEqual(len(blist), list_len) self.assertEqual(blist.dropped, 0) for idx in range(list_len): self.assertEqual(blist[idx], self.base[idx]) # test __iter__ in BoundedList for idx, val in enumerate(blist): self.assertEqual(val, self.base[idx]) def test_extend_drop(self): list_len = len(self.base) # create full BoundedList blist = BoundedList.from_seq(list_len, self.base) other_list = [13, 37, 51, 91] # try to extend with more elements blist.extend(other_list) self.assertEqual(len(blist), list_len) self.assertEqual(blist.dropped, len(other_list)) def test_no_limit(self): blist = BoundedList(maxlen=None) for num in range(100): blist.append(num) for num in range(100): self.assertEqual(blist[num], num) # pylint: disable=protected-access def test_deepcopy(self): blist = BoundedList(maxlen=10) blist.append(1) blist.append([2, 3]) blist.dropped = 5 blist_copy = copy.deepcopy(blist) self.assertIsNot(blist, blist_copy) self.assertIsNot(blist._dq, blist_copy._dq) self.assertIsNot(blist._lock, blist_copy._lock) self.assertEqual(list(blist), list(blist_copy)) self.assertEqual(blist.dropped, blist_copy.dropped) self.assertEqual(blist._dq.maxlen, blist_copy._dq.maxlen) self.assertIsNot(blist[1], blist_copy[1]) self.assertEqual(blist[1], blist_copy[1]) python-opentelemetry-1.45.2/opentelemetry-sdk/tests/trace/000077500000000000000000000000001527070070700237365ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-sdk/tests/trace/__init__.py000066400000000000000000000001141527070070700260430ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 python-opentelemetry-1.45.2/opentelemetry-sdk/tests/trace/composite_sampler/000077500000000000000000000000001527070070700274635ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-sdk/tests/trace/composite_sampler/__init__.py000066400000000000000000000000001527070070700315620ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-sdk/tests/trace/composite_sampler/test_always_off.py000066400000000000000000000020041527070070700332220ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from opentelemetry.sdk.trace._sampling_experimental import ( composable_always_off, composite_sampler, ) from opentelemetry.sdk.trace.id_generator import RandomIdGenerator from opentelemetry.sdk.trace.sampling import Decision def test_description(): assert composable_always_off().get_description() == "ComposableAlwaysOff" def test_threshold(): assert composable_always_off().sampling_intent(None, "test", None, {}, None, None).threshold == -1 def test_sampling(): sampler = composite_sampler(composable_always_off()) num_sampled = 0 for _ in range(10000): res = sampler.should_sample( None, RandomIdGenerator().generate_trace_id(), "span", None, None, None, None, ) if res.decision == Decision.RECORD_AND_SAMPLE: num_sampled += 1 assert not res.trace_state assert num_sampled == 0 python-opentelemetry-1.45.2/opentelemetry-sdk/tests/trace/composite_sampler/test_always_on.py000066400000000000000000000021111527070070700330630ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from opentelemetry.sdk.trace._sampling_experimental import ( composable_always_on, composite_sampler, ) from opentelemetry.sdk.trace.id_generator import RandomIdGenerator from opentelemetry.sdk.trace.sampling import Decision def test_description(): assert composable_always_on().get_description() == "ComposableAlwaysOn" def test_threshold(): assert composable_always_on().sampling_intent(None, "test", None, {}, None, None).threshold == 0 def test_sampling(): sampler = composite_sampler(composable_always_on()) num_sampled = 0 for _ in range(10000): res = sampler.should_sample( None, RandomIdGenerator().generate_trace_id(), "span", None, None, None, None, ) if res.decision == Decision.RECORD_AND_SAMPLE: num_sampled += 1 assert res.trace_state is not None assert res.trace_state.get("ot", "") == "th:0" assert num_sampled == 10000 python-opentelemetry-1.45.2/opentelemetry-sdk/tests/trace/composite_sampler/test_rule_based.py000066400000000000000000000216721527070070700332110ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from opentelemetry.sdk.trace._sampling_experimental import ( composable_always_off, composable_always_on, composable_rule_based, composite_sampler, ) from opentelemetry.sdk.trace._sampling_experimental._rule_based import ( AllPredicate, AlwaysMatchPredicate, AttributePatternsPredicate, AttributePredicate, AttributeValuesPredicate, ParentPredicate, SpanKindPredicate, ) from opentelemetry.sdk.trace.id_generator import RandomIdGenerator from opentelemetry.sdk.trace.sampling import Decision from opentelemetry.trace import ( NonRecordingSpan, SpanContext, SpanKind, TraceFlags, set_span_in_context, ) TRACE_ID = int("00112233445566778800000000000000", 16) SPAN_ID = int("0123456789abcdef", 16) class NameIsFooPredicate: def __call__( self, parent_ctx, name, span_kind, attributes, links, trace_state, ): return name == "foo" def __str__(self): return "NameIsFooPredicate" def _predicate_result( predicate, *, parent_ctx=None, span_kind=None, attributes=None, ): return predicate( parent_ctx=parent_ctx, name="span", span_kind=span_kind, attributes=attributes, links=None, trace_state=None, ) def _parent_context(is_remote: bool): return set_span_in_context( NonRecordingSpan( SpanContext( TRACE_ID, SPAN_ID, is_remote=is_remote, trace_flags=TraceFlags.get_default(), ) ) ) def test_description_with_no_rules(): assert composable_rule_based(rules=[]).get_description() == "ComposableRuleBased{[]}" def test_always_match_predicate(): assert _predicate_result(AlwaysMatchPredicate()) is True assert str(AlwaysMatchPredicate()) == "AlwaysMatch" def test_all_predicate_matches_when_all_predicates_match(): predicate = AllPredicate( [ AttributeValuesPredicate("http.route", ["/users"]), SpanKindPredicate([SpanKind.SERVER]), ] ) assert ( _predicate_result( predicate, span_kind=SpanKind.SERVER, attributes={"http.route": "/users"}, ) is True ) def test_all_predicate_does_not_match_when_any_predicate_does_not_match(): predicate = AllPredicate( [ AttributeValuesPredicate("http.route", ["/users"]), SpanKindPredicate([SpanKind.SERVER]), ] ) assert ( _predicate_result( predicate, span_kind=SpanKind.CLIENT, attributes={"http.route": "/users"}, ) is False ) def test_attribute_values_predicate_no_attributes(): assert _predicate_result(AttributeValuesPredicate("http.route", ["/users"])) is False def test_attribute_values_predicate_stringifies_values(): assert ( _predicate_result( AttributeValuesPredicate("http.response.status_code", ["404"]), attributes={"http.response.status_code": 404}, ) is True ) def test_attribute_values_predicate_matches_array_item(): assert ( _predicate_result( AttributeValuesPredicate("http.request.method", ["POST"]), attributes={"http.request.method": ("GET", "POST")}, ) is True ) def test_attribute_values_predicate_no_match(): assert ( _predicate_result( AttributeValuesPredicate("http.route", ["/users"]), attributes={"http.route": "/health"}, ) is False ) def test_attribute_patterns_predicate_includes_all_by_default(): assert ( _predicate_result( AttributePatternsPredicate("http.route"), attributes={"http.route": "/users"}, ) is True ) def test_attribute_patterns_predicate_include_exclude_precedence(): predicate = AttributePatternsPredicate( "http.route", included=["/api/*"], excluded=["/api/private/*"], ) assert _predicate_result(predicate, attributes={"http.route": "/api/users"}) is True assert _predicate_result(predicate, attributes={"http.route": "/api/private/user"}) is False def test_attribute_patterns_predicate_is_case_sensitive(): assert ( _predicate_result( AttributePatternsPredicate("http.route", included=["/api/*"]), attributes={"http.route": "/API/users"}, ) is False ) def test_attribute_patterns_predicate_matches_array_item(): assert ( _predicate_result( AttributePatternsPredicate("http.request.method", ["POST"]), attributes={"http.request.method": ("GET", "POST")}, ) is True ) def test_span_kind_predicate(): predicate = SpanKindPredicate([SpanKind.SERVER]) assert _predicate_result(predicate, span_kind=SpanKind.SERVER) is True assert _predicate_result(predicate, span_kind=SpanKind.CLIENT) is False assert _predicate_result(predicate) is False def test_parent_predicate_matches_no_parent(): assert _predicate_result(ParentPredicate(["none"])) is True assert _predicate_result(ParentPredicate(["local"])) is False def test_parent_predicate_matches_local_parent(): local_parent_ctx = _parent_context(is_remote=False) assert _predicate_result(ParentPredicate(["local"]), parent_ctx=local_parent_ctx) is True assert _predicate_result(ParentPredicate(["remote"]), parent_ctx=local_parent_ctx) is False def test_parent_predicate_matches_remote_parent(): remote_parent_ctx = _parent_context(is_remote=True) assert _predicate_result(ParentPredicate(["remote"]), parent_ctx=remote_parent_ctx) is True assert _predicate_result(ParentPredicate(["local"]), parent_ctx=remote_parent_ctx) is False def test_description_with_rules(): rules = [ (AttributeValuesPredicate("foo", ["bar"]), composable_always_on()), (NameIsFooPredicate(), composable_always_off()), ] assert ( composable_rule_based(rules=rules).get_description() == "ComposableRuleBased{[(foo in [bar]:ComposableAlwaysOn),(NameIsFooPredicate:ComposableAlwaysOff)]}" ) def test_sampling_intent_match(): rules = [ (NameIsFooPredicate(), composable_always_on()), ] assert composable_rule_based(rules=rules).sampling_intent(None, "foo", None, {}, None, None).threshold == 0 def test_sampling_intent_no_match(): rules = [ (NameIsFooPredicate(), composable_always_on()), ] assert composable_rule_based(rules=rules).sampling_intent(None, "test", None, {}, None, None).threshold == -1 def test_should_sample_match(): rules = [ (NameIsFooPredicate(), composable_always_on()), ] sampler = composite_sampler(composable_rule_based(rules=rules)) res = sampler.should_sample( None, RandomIdGenerator().generate_trace_id(), "foo", None, None, None, None, ) assert res.decision == Decision.RECORD_AND_SAMPLE assert res.trace_state is not None assert res.trace_state.get("ot", "") == "th:0" def test_should_sample_match_multiple_rules(): rules = [ (AttributeValuesPredicate("foo", ["bar"]), composable_always_off()), (NameIsFooPredicate(), composable_always_on()), ] sampler = composite_sampler(composable_rule_based(rules=rules)) res = sampler.should_sample( None, RandomIdGenerator().generate_trace_id(), "foo", None, None, None, None, ) assert res.decision == Decision.RECORD_AND_SAMPLE assert res.trace_state is not None assert res.trace_state.get("ot", "") == "th:0" def test_should_sample_no_match(): rules = [ (NameIsFooPredicate(), composable_always_on()), ] sampler = composite_sampler(composable_rule_based(rules=rules)) res = sampler.should_sample( None, RandomIdGenerator().generate_trace_id(), "test", None, None, None, None, ) assert res.decision == Decision.DROP assert res.trace_state is None def test_attribute_predicate_no_attributes(): rules = [ (AttributePredicate("foo", "bar"), composable_always_on()), ] assert composable_rule_based(rules=rules).sampling_intent(None, "span", None, None, None, None).threshold == -1 def test_attribute_predicate_no_match(): rules = [ (AttributePredicate("foo", "bar"), composable_always_on()), ] assert ( composable_rule_based(rules=rules).sampling_intent(None, "span", None, {"foo": "foo"}, None, None).threshold == -1 ) def test_attribute_predicate_match(): rules = [ (AttributePredicate("foo", "bar"), composable_always_on()), ] assert ( composable_rule_based(rules=rules).sampling_intent(None, "span", None, {"foo": "bar"}, None, None).threshold == 0 ) python-opentelemetry-1.45.2/opentelemetry-sdk/tests/trace/composite_sampler/test_sampler.py000066400000000000000000000126061527070070700325440ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations from dataclasses import dataclass import pytest from pytest import param as p from opentelemetry.sdk.trace._sampling_experimental import ( ComposableSampler, composable_always_off, composable_always_on, composable_parent_threshold, composable_traceid_ratio_based, composite_sampler, ) from opentelemetry.sdk.trace._sampling_experimental._trace_state import ( OtelTraceState, ) from opentelemetry.sdk.trace._sampling_experimental._util import ( INVALID_RANDOM_VALUE, INVALID_THRESHOLD, ) from opentelemetry.sdk.trace.sampling import Decision from opentelemetry.trace import ( NonRecordingSpan, SpanContext, TraceFlags, TraceState, set_span_in_context, ) TRACE_ID = int("00112233445566778800000000000000", 16) SPAN_ID = int("0123456789abcdef", 16) @dataclass class Input: sampler: ComposableSampler sampled: bool threshold: int | None random_value: int | None @dataclass class Output: sampled: bool threshold: int random_value: int @pytest.mark.parametrize( "input,output", ( p( Input( sampler=composable_always_on(), sampled=True, threshold=None, random_value=None, ), Output(sampled=True, threshold=0, random_value=INVALID_RANDOM_VALUE), id="min threshold no parent random value", ), p( Input( sampler=composable_always_on(), sampled=True, threshold=None, random_value=0x7F99AA40C02744, ), Output(sampled=True, threshold=0, random_value=0x7F99AA40C02744), id="min threshold with parent random value", ), p( Input( sampler=composable_always_off(), sampled=True, threshold=None, random_value=None, ), Output( sampled=False, threshold=INVALID_THRESHOLD, random_value=INVALID_RANDOM_VALUE, ), id="max threshold", ), p( Input( sampler=composable_parent_threshold(composable_always_on()), sampled=False, threshold=0x7F99AA40C02744, random_value=0x7F99AA40C02744, ), Output( sampled=True, threshold=0x7F99AA40C02744, random_value=0x7F99AA40C02744, ), id="parent based in consistent mode", ), p( Input( sampler=composable_parent_threshold(composable_always_on()), sampled=True, threshold=None, random_value=None, ), Output( sampled=True, threshold=INVALID_THRESHOLD, random_value=INVALID_RANDOM_VALUE, ), id="parent based in legacy mode", ), p( Input( sampler=composable_traceid_ratio_based(0.5), sampled=True, threshold=None, random_value=0x7FFFFFFFFFFFFF, ), Output( sampled=False, threshold=INVALID_THRESHOLD, random_value=0x7FFFFFFFFFFFFF, ), id="half threshold not sampled", ), p( Input( sampler=composable_traceid_ratio_based(0.5), sampled=False, threshold=None, random_value=0x80000000000000, ), Output( sampled=True, threshold=0x80000000000000, random_value=0x80000000000000, ), id="half threshold sampled", ), p( Input( sampler=composable_traceid_ratio_based(1.0), sampled=False, threshold=0x80000000000000, random_value=0x80000000000000, ), Output(sampled=True, threshold=0, random_value=0x80000000000000), id="parent violating invariant", ), ), ) def test_sample(input: Input, output: Output): parent_state = OtelTraceState.invalid() if input.threshold is not None: parent_state.threshold = input.threshold if input.random_value is not None: parent_state.random_value = input.random_value parent_state_str = parent_state.serialize() parent_trace_state = TraceState((("ot", parent_state_str),)) if parent_state_str else None flags = TraceFlags(TraceFlags.SAMPLED) if input.sampled else TraceFlags.get_default() parent_span_context = SpanContext(TRACE_ID, SPAN_ID, False, flags, parent_trace_state) parent_span = NonRecordingSpan(parent_span_context) parent_context = set_span_in_context(parent_span) result = composite_sampler(input.sampler).should_sample( parent_context, TRACE_ID, "name", trace_state=parent_trace_state ) decision = Decision.RECORD_AND_SAMPLE if output.sampled else Decision.DROP state = OtelTraceState.parse(result.trace_state) assert result.decision == decision assert state.threshold == output.threshold assert state.random_value == output.random_value python-opentelemetry-1.45.2/opentelemetry-sdk/tests/trace/composite_sampler/test_traceid_ratio.py000066400000000000000000000036371527070070700337160ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import pytest from opentelemetry.sdk.trace._sampling_experimental import ( composable_traceid_ratio_based, composite_sampler, ) from opentelemetry.sdk.trace._sampling_experimental._trace_state import ( OtelTraceState, ) from opentelemetry.sdk.trace.id_generator import RandomIdGenerator from opentelemetry.sdk.trace.sampling import Decision @pytest.mark.parametrize( ("ratio", "threshold"), ( (1.0, "0"), (0.5, "8"), (0.25, "c"), (1e-300, "max"), (0, "max"), ), ) def test_description(ratio: float, threshold: str): assert ( composable_traceid_ratio_based(ratio).get_description() == f"ComposableTraceIDRatioBased{{threshold={threshold}, ratio={ratio}}}" ) @pytest.mark.parametrize( ("ratio", "threshold"), ( (1.0, 0), (0.5, 36028797018963968), (0.25, 54043195528445952), (0.125, 63050394783186944), (0.0, 72057594037927936), (0.45, 39631676720860364), (0.2, 57646075230342348), (0.13, 62690106812997304), (0.05, 68454714336031539), ), ) def test_sampling(ratio: float, threshold: int): sampler = composite_sampler(composable_traceid_ratio_based(ratio)) num_sampled = 0 for _ in range(10000): res = sampler.should_sample( None, RandomIdGenerator().generate_trace_id(), "span", None, None, None, None, ) if res.decision == Decision.RECORD_AND_SAMPLE: num_sampled += 1 assert res.trace_state is not None otts = OtelTraceState.parse(res.trace_state) assert otts.threshold == threshold assert otts.random_value == -1 expected_num_sampled = int(10000 * ratio) assert abs(num_sampled - expected_num_sampled) < 50 python-opentelemetry-1.45.2/opentelemetry-sdk/tests/trace/composite_sampler/test_tracestate.py000066400000000000000000000035321527070070700332360ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import pytest from opentelemetry.sdk.trace._sampling_experimental._trace_state import ( OtelTraceState, ) from opentelemetry.trace import TraceState @pytest.mark.parametrize( "input_str,output_str", ( ("a", "a"), ("#", "#"), ("rv:1234567890abcd", "rv:1234567890abcd"), ("rv:01020304050607", "rv:01020304050607"), ("rv:1234567890abcde", ""), ("th:1234567890abcd", "th:1234567890abcd"), ("th:1234567890abcd", "th:1234567890abcd"), ("th:10000000000000", "th:1"), ("th:1234500000000", "th:12345"), ("th:0", "th:0"), ("th:100000000000000", ""), ("th:1234567890abcde", ""), pytest.param( f"a:{'X' * 214};rv:1234567890abcd;th:1234567890abcd;x:3", f"th:1234567890abcd;rv:1234567890abcd;a:{'X' * 214};x:3", id="long", ), ("th:x", ""), ("th:100000000000000", ""), ("th:10000000000000", "th:1"), ("th:1000000000000", "th:1"), ("th:100000000000", "th:1"), ("th:10000000000", "th:1"), ("th:1000000000", "th:1"), ("th:100000000", "th:1"), ("th:10000000", "th:1"), ("th:1000000", "th:1"), ("th:100000", "th:1"), ("th:10000", "th:1"), ("th:1000", "th:1"), ("th:100", "th:1"), ("th:10", "th:1"), ("th:1", "th:1"), ("th:10000000000001", "th:10000000000001"), ("th:10000000000010", "th:1000000000001"), ("rv:x", ""), ("rv:100000000000000", ""), ("rv:10000000000000", "rv:10000000000000"), ("rv:1000000000000", ""), ), ) def test_marshal(input_str: str, output_str: str): state = OtelTraceState.parse(TraceState((("ot", input_str),))).serialize() assert state == output_str python-opentelemetry-1.45.2/opentelemetry-sdk/tests/trace/export/000077500000000000000000000000001527070070700252575ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-sdk/tests/trace/export/__init__.py000066400000000000000000000001141527070070700273640ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 python-opentelemetry-1.45.2/opentelemetry-sdk/tests/trace/export/test_export.py000066400000000000000000000567271527070070700302320ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import os import threading import time import unittest from unittest import mock from opentelemetry import trace as trace_api from opentelemetry.context import Context from opentelemetry.metrics import NoOpMeterProvider from opentelemetry.sdk import trace from opentelemetry.sdk.environment_variables import ( OTEL_BSP_EXPORT_TIMEOUT, OTEL_BSP_MAX_EXPORT_BATCH_SIZE, OTEL_BSP_MAX_QUEUE_SIZE, OTEL_BSP_SCHEDULE_DELAY, OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED, ) from opentelemetry.sdk.metrics import MeterProvider from opentelemetry.sdk.metrics.export import InMemoryMetricReader from opentelemetry.sdk.trace import export from opentelemetry.sdk.trace.export import logger # pylint: disable=protected-access class MySpanExporter(export.SpanExporter): """Very simple span exporter used for testing.""" def __init__( self, destination, max_export_batch_size=None, export_timeout_millis=0.0, export_event: threading.Event = None, ): self.destination = destination self.max_export_batch_size = max_export_batch_size self.is_shutdown = False self.export_timeout = export_timeout_millis / 1e3 self.export_event = export_event def export(self, spans: trace.Span) -> export.SpanExportResult: if self.max_export_batch_size is not None and len(spans) > self.max_export_batch_size: raise ValueError("Batch is too big") time.sleep(self.export_timeout) self.destination.extend(span.name for span in spans) if self.export_event: self.export_event.set() return export.SpanExportResult.SUCCESS def shutdown(self): self.is_shutdown = True class TestSimpleSpanProcessor(unittest.TestCase): def test_simple_span_processor(self): tracer_provider = trace.TracerProvider() tracer = tracer_provider.get_tracer(__name__) spans_names_list = [] my_exporter = MySpanExporter(destination=spans_names_list) span_processor = export.SimpleSpanProcessor(my_exporter) tracer_provider.add_span_processor(span_processor) with tracer.start_as_current_span("foo"): with tracer.start_as_current_span("bar"): with tracer.start_as_current_span("xxx"): pass self.assertListEqual(["xxx", "bar", "foo"], spans_names_list) span_processor.shutdown() self.assertTrue(my_exporter.is_shutdown) def test_simple_span_processor_no_context(self): """Check that we process spans that are never made active. SpanProcessors should act on a span's start and end events whether or not it is ever the active span. """ tracer_provider = trace.TracerProvider() tracer = tracer_provider.get_tracer(__name__) spans_names_list = [] my_exporter = MySpanExporter(destination=spans_names_list) span_processor = export.SimpleSpanProcessor(my_exporter) tracer_provider.add_span_processor(span_processor) with tracer.start_span("foo"), tracer.start_span("bar"): with tracer.start_span("xxx"): pass self.assertListEqual(["xxx", "bar", "foo"], spans_names_list) def test_on_start_accepts_context(self): # pylint: disable=no-self-use tracer_provider = trace.TracerProvider() tracer = tracer_provider.get_tracer(__name__) exporter = MySpanExporter([]) span_processor = mock.Mock(wraps=export.SimpleSpanProcessor(exporter)) tracer_provider.add_span_processor(span_processor) context = Context() span = tracer.start_span("foo", context=context) span_processor.on_start.assert_called_once_with(span, parent_context=context) def test_simple_span_processor_not_sampled(self): tracer_provider = trace.TracerProvider(sampler=trace.sampling.ALWAYS_OFF) tracer = tracer_provider.get_tracer(__name__) spans_names_list = [] my_exporter = MySpanExporter(destination=spans_names_list) span_processor = export.SimpleSpanProcessor(my_exporter) tracer_provider.add_span_processor(span_processor) with tracer.start_as_current_span("foo"): with tracer.start_as_current_span("bar"): with tracer.start_as_current_span("xxx"): pass self.assertListEqual([], spans_names_list) @mock.patch.dict("os.environ", {OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED: "true"}) def test_metrics(self): metric_reader = InMemoryMetricReader() meter_provider = MeterProvider(metric_readers=[metric_reader]) num_exports = 0 def export_spans(_spans): nonlocal num_exports num_exports += 1 if num_exports == 3: raise RuntimeError("Export failed") return export.SpanExportResult.SUCCESS exporter = mock.MagicMock() exporter.export.side_effect = export_spans span_processor = export.SimpleSpanProcessor(exporter, meter_provider=meter_provider) tracer_provider = trace.TracerProvider() tracer = tracer_provider.get_tracer(__name__) tracer_provider.add_span_processor(span_processor) with tracer.start_as_current_span("foo"): with tracer.start_span("bar"): pass with tracer.start_span("baz"): pass metrics_data = metric_reader.get_metrics_data() scope_metrics = metrics_data.resource_metrics[0].scope_metrics[0] self.assertEqual(scope_metrics.scope.name, "opentelemetry-sdk") metrics = sorted(scope_metrics.metrics, key=lambda m: m.name) self.assertEqual(len(metrics), 1) self.assertEqual(metrics[0].name, "otel.sdk.processor.span.processed") processed_data_points = metrics[0].data.data_points self.assertEqual(len(processed_data_points), 1) processed_data_point0 = processed_data_points[0] # All 3 spans are counted as processed when submitted to the exporter, # independent of the export outcome (the 3rd export fails). self.assertEqual(processed_data_point0.value, 3) self.assertEqual( processed_data_point0.attributes["otel.component.type"], "simple_span_processor", ) self.assertTrue(processed_data_point0.attributes["otel.component.name"].startswith("simple_span_processor/")) self.assertIsNone(processed_data_point0.attributes.get("error.type")) @mock.patch.dict("os.environ", {OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED: "true"}) def test_metrics_already_shutdown(self): metric_reader = InMemoryMetricReader() meter_provider = MeterProvider(metric_readers=[metric_reader]) exporter = mock.MagicMock() exporter.export.return_value = export.SpanExportResult.SUCCESS span_processor = export.SimpleSpanProcessor(exporter, meter_provider=meter_provider) provider = trace.TracerProvider() tracer = provider.get_tracer(__name__) provider.add_span_processor(span_processor) # Ended before shutdown: submitted to the exporter and counted as # successfully processed. with tracer.start_as_current_span("foo"): pass span_processor.shutdown() # Ended after shutdown: dropped and counted as already_shutdown. with tracer.start_as_current_span("bar"): pass metrics_data = metric_reader.get_metrics_data() scope_metrics = metrics_data.resource_metrics[0].scope_metrics[0] processed = next(m for m in scope_metrics.metrics if m.name == "otel.sdk.processor.span.processed") data_points = sorted( processed.data.data_points, key=lambda dp: dp.attributes.get("error.type", ""), ) self.assertEqual(len(data_points), 2) self.assertEqual(data_points[0].value, 1) self.assertIsNone(data_points[0].attributes.get("error.type")) self.assertEqual(data_points[1].value, 1) self.assertEqual(data_points[1].attributes.get("error.type"), "already_shutdown") # Only the pre-shutdown span is submitted to the exporter; the # post-shutdown span is dropped, not exported. self.assertEqual(exporter.export.call_count, 1) # Many more test cases for the BatchSpanProcessor exist under # opentelemetry-sdk/tests/shared_internal/test_batch_processor.py. # Important: make sure to call .shutdown() on the BatchSpanProcessor # before the end of the test, otherwise the worker thread will continue # to run after the end of the test. class TestBatchSpanProcessor(unittest.TestCase): def test_get_span_exporter(self): exporter = MySpanExporter(destination=[]) batch_span_processor = export.BatchSpanProcessor(exporter) self.assertEqual(exporter, batch_span_processor.span_exporter) @mock.patch.dict( "os.environ", { OTEL_BSP_MAX_QUEUE_SIZE: "10", OTEL_BSP_SCHEDULE_DELAY: "2", OTEL_BSP_MAX_EXPORT_BATCH_SIZE: "3", OTEL_BSP_EXPORT_TIMEOUT: "4", }, ) def test_args_env_var(self): batch_span_processor = export.BatchSpanProcessor(MySpanExporter(destination=[])) self.assertEqual(batch_span_processor._batch_processor._max_queue_size, 10) self.assertEqual(batch_span_processor._batch_processor._schedule_delay_millis, 2) self.assertEqual(batch_span_processor._batch_processor._max_export_batch_size, 3) self.assertEqual(batch_span_processor._batch_processor._export_timeout_millis, 4) batch_span_processor.shutdown() def test_args_env_var_defaults(self): batch_span_processor = export.BatchSpanProcessor(MySpanExporter(destination=[])) self.assertEqual(batch_span_processor._batch_processor._max_queue_size, 2048) self.assertEqual(batch_span_processor._batch_processor._schedule_delay_millis, 5000) self.assertEqual(batch_span_processor._batch_processor._max_export_batch_size, 512) self.assertEqual(batch_span_processor._batch_processor._export_timeout_millis, 30000) batch_span_processor.shutdown() @mock.patch.dict( "os.environ", { OTEL_BSP_MAX_QUEUE_SIZE: "a", OTEL_BSP_SCHEDULE_DELAY: " ", OTEL_BSP_MAX_EXPORT_BATCH_SIZE: "One", OTEL_BSP_EXPORT_TIMEOUT: "@", }, ) def test_args_env_var_value_error(self): logger.disabled = True batch_span_processor = export.BatchSpanProcessor(MySpanExporter(destination=[])) logger.disabled = False self.assertEqual(batch_span_processor._batch_processor._max_queue_size, 2048) self.assertEqual(batch_span_processor._batch_processor._schedule_delay_millis, 5000) self.assertEqual(batch_span_processor._batch_processor._max_export_batch_size, 512) self.assertEqual(batch_span_processor._batch_processor._export_timeout_millis, 30000) batch_span_processor.shutdown() def test_on_start_accepts_parent_context(self): # pylint: disable=no-self-use my_exporter = MySpanExporter(destination=[]) span_processor = mock.Mock(wraps=export.BatchSpanProcessor(my_exporter)) tracer_provider = trace.TracerProvider() tracer_provider.add_span_processor(span_processor) tracer = tracer_provider.get_tracer(__name__) context = Context() span = tracer.start_span("foo", context=context) span_processor.on_start.assert_called_once_with(span, parent_context=context) def test_batch_span_processor_not_sampled(self): tracer_provider = trace.TracerProvider(sampler=trace.sampling.ALWAYS_OFF) tracer = tracer_provider.get_tracer(__name__) spans_names_list = [] my_exporter = MySpanExporter(destination=spans_names_list, max_export_batch_size=128) span_processor = export.BatchSpanProcessor( my_exporter, max_queue_size=256, max_export_batch_size=64, schedule_delay_millis=100, ) tracer_provider.add_span_processor(span_processor) with tracer.start_as_current_span("foo"): pass time.sleep(0.05) # give some time for the exporter to upload spans span_processor.force_flush() self.assertEqual(len(spans_names_list), 0) span_processor.shutdown() def test_batch_span_processor_parameters(self): # zero max_queue_size self.assertRaises(ValueError, export.BatchSpanProcessor, None, max_queue_size=0) # negative max_queue_size self.assertRaises( ValueError, export.BatchSpanProcessor, None, max_queue_size=-500, ) # zero schedule_delay_millis self.assertRaises( ValueError, export.BatchSpanProcessor, None, schedule_delay_millis=0, ) # negative schedule_delay_millis self.assertRaises( ValueError, export.BatchSpanProcessor, None, schedule_delay_millis=-500, ) # zero max_export_batch_size self.assertRaises( ValueError, export.BatchSpanProcessor, None, max_export_batch_size=0, ) # negative max_export_batch_size self.assertRaises( ValueError, export.BatchSpanProcessor, None, max_export_batch_size=-500, ) # max_export_batch_size > max_queue_size: self.assertRaises( ValueError, export.BatchSpanProcessor, None, max_queue_size=256, max_export_batch_size=512, ) @mock.patch.dict("os.environ", {OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED: "true"}) def test_metrics_already_shutdown(self): metric_reader = InMemoryMetricReader() meter_provider = MeterProvider(metric_readers=[metric_reader]) exporter = mock.MagicMock() exporter.export.return_value = export.SpanExportResult.SUCCESS span_processor = export.BatchSpanProcessor(exporter, meter_provider=meter_provider) provider = trace.TracerProvider() tracer = provider.get_tracer(__name__) provider.add_span_processor(span_processor) # Ended before shutdown: drained on shutdown and counted as a # successful submit to the exporter. with tracer.start_as_current_span("foo"): pass span_processor.shutdown() # Ended after shutdown: dropped and counted as already_shutdown. with tracer.start_as_current_span("bar"): pass metrics_data = metric_reader.get_metrics_data() scope_metrics = metrics_data.resource_metrics[0].scope_metrics[0] processed = next(m for m in scope_metrics.metrics if m.name == "otel.sdk.processor.span.processed") data_points = sorted( processed.data.data_points, key=lambda dp: dp.attributes.get("error.type", ""), ) self.assertEqual(len(data_points), 2) self.assertEqual(data_points[0].value, 1) self.assertIsNone(data_points[0].attributes.get("error.type")) self.assertEqual(data_points[1].value, 1) self.assertEqual(data_points[1].attributes.get("error.type"), "already_shutdown") @mock.patch.dict("os.environ", {OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED: "true"}) def test_metrics(self): # pylint: disable=too-many-locals,too-many-statements metric_reader = InMemoryMetricReader() meter_provider = MeterProvider(metric_readers=[metric_reader]) metric_reader._set_meter_provider(NoOpMeterProvider()) num_exports = 0 first_export_event = threading.Event() last_export_event = threading.Event() run_exports = threading.Event() def export_spans(_spans): first_export_event.set() run_exports.wait() nonlocal num_exports num_exports += 1 if num_exports == 3: last_export_event.set() raise ValueError("Export failed") return export.SpanExportResult.SUCCESS exporter = mock.MagicMock() exporter.export.side_effect = export_spans span_processor = export.BatchSpanProcessor( exporter, meter_provider=meter_provider, max_queue_size=1, max_export_batch_size=1, schedule_delay_millis=1_000_000_000, # Manually flush ) provider = trace.TracerProvider() tracer = provider.get_tracer(__name__) provider.add_span_processor(span_processor) with tracer.start_as_current_span("foo"): pass # Wait for span to be sent to exporter first_export_event.wait() # Queue empty, export in progress, this span is queued with tracer.start_as_current_span("bar"): pass # Queue full, this span is dropped with tracer.start_as_current_span("baz"): pass metrics_data = metric_reader.get_metrics_data() scope_metrics = metrics_data.resource_metrics[0].scope_metrics[0] self.assertEqual(scope_metrics.scope.name, "opentelemetry-sdk") metrics = sorted(scope_metrics.metrics, key=lambda m: m.name) self.assertEqual(len(metrics), 3) self.assertEqual(metrics[0].name, "otel.sdk.processor.span.processed") processed_data_points = sorted( metrics[0].data.data_points, key=lambda dp: dp.attributes.get("error.type", ""), ) # "foo" is counted as processed when submitted to the exporter (before # its export call blocks); "baz" is dropped due to a full queue. self.assertEqual(len(processed_data_points), 2) processed_data_point0 = processed_data_points[0] self.assertEqual(processed_data_point0.value, 1) self.assertEqual( processed_data_point0.attributes["otel.component.type"], "batching_span_processor", ) self.assertTrue(processed_data_point0.attributes["otel.component.name"].startswith("batching_span_processor/")) self.assertIsNone(processed_data_point0.attributes.get("error.type")) processed_data_point_queue_full = processed_data_points[1] self.assertEqual(processed_data_point_queue_full.value, 1) self.assertEqual( processed_data_point_queue_full.attributes.get("error.type"), "queue_full", ) self.assertEqual(metrics[1].name, "otel.sdk.processor.span.queue.capacity") queue_capacity_data_point = metrics[1].data.data_points[0] self.assertEqual(queue_capacity_data_point.value, 1) self.assertEqual( queue_capacity_data_point.attributes["otel.component.type"], "batching_span_processor", ) self.assertTrue( queue_capacity_data_point.attributes["otel.component.name"].startswith("batching_span_processor/") ) self.assertEqual(metrics[2].name, "otel.sdk.processor.span.queue.size") queue_size_data_point = metrics[2].data.data_points[0] self.assertEqual(queue_size_data_point.value, 1) self.assertEqual( queue_size_data_point.attributes["otel.component.type"], "batching_span_processor", ) self.assertTrue(queue_size_data_point.attributes["otel.component.name"].startswith("batching_span_processor/")) run_exports.set() provider.force_flush() # This span is processed and exporter returns an error with tracer.start_as_current_span("failed"): pass provider.force_flush() last_export_event.wait() metrics_data = metric_reader.get_metrics_data() scope_metrics = metrics_data.resource_metrics[0].scope_metrics[0] self.assertEqual(scope_metrics.scope.name, "opentelemetry-sdk") metrics = sorted(scope_metrics.metrics, key=lambda m: m.name) self.assertEqual(len(metrics), 3) self.assertEqual(metrics[0].name, "otel.sdk.processor.span.processed") processed_data_points = sorted( metrics[0].data.data_points, key=lambda dp: dp.attributes.get("error.type", ""), ) # "foo", "bar" and "failed" are all counted as processed when submitted # to the exporter, independent of the export outcome ("failed" raises). # "baz" remains a queue_full drop. self.assertEqual(len(processed_data_points), 2) processed_data_point0 = processed_data_points[0] self.assertEqual(processed_data_point0.value, 3) self.assertEqual( processed_data_point0.attributes["otel.component.type"], "batching_span_processor", ) self.assertTrue(processed_data_point0.attributes["otel.component.name"].startswith("batching_span_processor/")) self.assertIsNone(processed_data_point0.attributes.get("error.type")) processed_data_point1 = processed_data_points[1] self.assertEqual(processed_data_point1.value, 1) self.assertEqual( processed_data_point1.attributes["otel.component.type"], "batching_span_processor", ) self.assertTrue(processed_data_point1.attributes["otel.component.name"].startswith("batching_span_processor/")) self.assertEqual(processed_data_point1.attributes.get("error.type"), "queue_full") self.assertEqual(metrics[1].name, "otel.sdk.processor.span.queue.capacity") queue_capacity_data_point = metrics[1].data.data_points[0] self.assertEqual(queue_capacity_data_point.value, 1) self.assertEqual( queue_capacity_data_point.attributes["otel.component.type"], "batching_span_processor", ) self.assertTrue( queue_capacity_data_point.attributes["otel.component.name"].startswith("batching_span_processor/") ) self.assertEqual(metrics[2].name, "otel.sdk.processor.span.queue.size") queue_size_data_point = metrics[2].data.data_points[0] self.assertEqual(queue_size_data_point.value, 0) self.assertEqual( queue_size_data_point.attributes["otel.component.type"], "batching_span_processor", ) self.assertTrue(queue_size_data_point.attributes["otel.component.name"].startswith("batching_span_processor/")) provider.shutdown() class TestConsoleSpanExporter(unittest.TestCase): def test_export(self): # pylint: disable=no-self-use """Check that the console exporter prints spans.""" exporter = export.ConsoleSpanExporter() # Mocking stdout interferes with debugging and test reporting, mock on # the exporter instance instead. span = trace._Span("span name", trace_api.INVALID_SPAN_CONTEXT) with mock.patch.object(exporter, "out") as mock_stdout: exporter.export([span]) mock_stdout.write.assert_called_once_with(span.to_json() + os.linesep) self.assertEqual(mock_stdout.write.call_count, 1) self.assertEqual(mock_stdout.flush.call_count, 1) def test_export_custom(self): # pylint: disable=no-self-use """Check that console exporter uses custom io, formatter.""" mock_span_str = mock.Mock(str) def formatter(span): # pylint: disable=unused-argument return mock_span_str mock_stdout = mock.Mock() exporter = export.ConsoleSpanExporter(out=mock_stdout, formatter=formatter) exporter.export([trace._Span("span name", mock.Mock())]) mock_stdout.write.assert_called_once_with(mock_span_str) python-opentelemetry-1.45.2/opentelemetry-sdk/tests/trace/export/test_in_memory_span_exporter.py000066400000000000000000000062251527070070700336440ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import unittest from unittest import mock from opentelemetry import trace as trace_api from opentelemetry.sdk import trace from opentelemetry.sdk.trace import export from opentelemetry.sdk.trace.export.in_memory_span_exporter import ( InMemorySpanExporter, ) class TestInMemorySpanExporter(unittest.TestCase): def setUp(self): self.tracer_provider = trace.TracerProvider() self.tracer = self.tracer_provider.get_tracer(__name__) self.memory_exporter = InMemorySpanExporter() span_processor = export.SimpleSpanProcessor(self.memory_exporter) self.tracer_provider.add_span_processor(span_processor) self.exec_scenario() def exec_scenario(self): with self.tracer.start_as_current_span("foo"): with self.tracer.start_as_current_span("bar"): with self.tracer.start_as_current_span("xxx"): pass def test_get_finished_spans(self): span_list = self.memory_exporter.get_finished_spans() spans_names_list = [span.name for span in span_list] self.assertListEqual(["xxx", "bar", "foo"], spans_names_list) def test_clear(self): self.memory_exporter.clear() span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 0) def test_shutdown(self): span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 3) self.memory_exporter.shutdown() # after shutdown no new spans are accepted self.exec_scenario() span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 3) def test_return_code(self): span = trace._Span("name", mock.Mock(spec=trace_api.SpanContext)) span_list = (span,) memory_exporter = InMemorySpanExporter() ret = memory_exporter.export(span_list) self.assertEqual(ret, export.SpanExportResult.SUCCESS) memory_exporter.shutdown() # after shutdown export should fail ret = memory_exporter.export(span_list) self.assertEqual(ret, export.SpanExportResult.FAILURE) def test_max_spans_limit(self): """Test that max_spans limits memory usage by dropping oldest spans.""" exporter = InMemorySpanExporter(max_spans=2) span1 = trace._Span("span1", mock.Mock(spec=trace_api.SpanContext)) span2 = trace._Span("span2", mock.Mock(spec=trace_api.SpanContext)) span3 = trace._Span("span3", mock.Mock(spec=trace_api.SpanContext)) exporter.export([span1, span2, span3]) span_list = exporter.get_finished_spans() self.assertEqual(len(span_list), 2) self.assertSequenceEqual((span2, span3), span_list) def test_max_spans_none_is_unlimited(self): """Test that max_spans=None (default) imposes no limit.""" exporter = InMemorySpanExporter(max_spans=None) span = trace._Span("name", mock.Mock(spec=trace_api.SpanContext)) exporter.export([span] * 100) span_list = exporter.get_finished_spans() self.assertEqual(len(span_list), 100) python-opentelemetry-1.45.2/opentelemetry-sdk/tests/trace/propagation/000077500000000000000000000000001527070070700262615ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-sdk/tests/trace/propagation/__init__.py000066400000000000000000000000001527070070700303600ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-sdk/tests/trace/scripts/000077500000000000000000000000001527070070700254255ustar00rootroot00000000000000tracer_provider_resource_after_fork.py000066400000000000000000000032761527070070700352330ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-sdk/tests/trace/scripts# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import json import os from opentelemetry.sdk import trace from opentelemetry.sdk.resources import PROCESS_PID def main() -> None: tracer_provider = trace.TracerProvider(shutdown_on_exit=False) tracer = tracer_provider.get_tracer("cached") parent_pid = os.getpid() parent_resource_pid = tracer_provider.resource.attributes[PROCESS_PID] parent_tracer_pid = tracer.resource.attributes[PROCESS_PID] pid = os.fork() if not pid: child_pid = os.getpid() new_tracer = tracer_provider.get_tracer("new") span = tracer.start_span("child") print( json.dumps( { "child_pid": child_pid, "provider_pid": tracer_provider.resource.attributes[PROCESS_PID], "cached_tracer_pid": tracer.resource.attributes[PROCESS_PID], "new_tracer_pid": new_tracer.resource.attributes[PROCESS_PID], "span_pid": span.resource.attributes[PROCESS_PID], } ), flush=True, ) # pylint: disable-next=protected-access os._exit(0) os.waitpid(pid, 0) print( json.dumps( { "parent_pid": parent_pid, "parent_resource_pid": parent_resource_pid, "parent_tracer_pid": parent_tracer_pid, "parent_resource_pid_after_fork": tracer_provider.resource.attributes[PROCESS_PID], "parent_tracer_pid_after_fork": tracer.resource.attributes[PROCESS_PID], } ), flush=True, ) if __name__ == "__main__": main() python-opentelemetry-1.45.2/opentelemetry-sdk/tests/trace/test_globals.py000066400000000000000000000015411527070070700267730ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # type:ignore import unittest from logging import WARNING from opentelemetry import trace from opentelemetry.sdk.trace import TracerProvider # type:ignore class TestGlobals(unittest.TestCase): def test_tracer_provider_override_warning(self): """trace.set_tracer_provider should throw a warning when overridden""" trace.set_tracer_provider(TracerProvider()) tracer_provider = trace.get_tracer_provider() with self.assertLogs(level=WARNING) as test: trace.set_tracer_provider(TracerProvider()) self.assertEqual( test.output, [("WARNING:opentelemetry.trace:Overriding of current TracerProvider is not allowed")], ) self.assertIs(tracer_provider, trace.get_tracer_provider()) python-opentelemetry-1.45.2/opentelemetry-sdk/tests/trace/test_implementation.py000066400000000000000000000026651527070070700304050ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import unittest from opentelemetry.sdk import trace from opentelemetry.trace import INVALID_SPAN, INVALID_SPAN_CONTEXT class TestTracerImplementation(unittest.TestCase): """ This test is in place to ensure the SDK implementation of the API is returning values that are valid. The same tests have been added to the API with different expected results. See issue for more details: https://github.com/open-telemetry/opentelemetry-python/issues/142 """ def test_tracer(self): tracer = trace.TracerProvider().get_tracer(__name__) with tracer.start_span("test") as span: self.assertNotEqual(span.get_span_context(), INVALID_SPAN_CONTEXT) self.assertNotEqual(span, INVALID_SPAN) self.assertIs(span.is_recording(), True) with tracer.start_span("test2") as span2: self.assertNotEqual(span2.get_span_context(), INVALID_SPAN_CONTEXT) self.assertNotEqual(span2, INVALID_SPAN) self.assertIs(span2.is_recording(), True) def test_span(self): with self.assertRaises(Exception): # pylint: disable=no-value-for-parameter span = trace._Span() span = trace._Span("name", INVALID_SPAN_CONTEXT) self.assertEqual(span.get_span_context(), INVALID_SPAN_CONTEXT) self.assertIs(span.is_recording(), True) python-opentelemetry-1.45.2/opentelemetry-sdk/tests/trace/test_sampling.py000066400000000000000000000534651527070070700271760ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import contextlib import sys import unittest import unittest.mock from opentelemetry import context as context_api from opentelemetry import trace from opentelemetry.sdk.trace import sampling TO_DEFAULT = trace.TraceFlags(trace.TraceFlags.DEFAULT) TO_SAMPLED = trace.TraceFlags(trace.TraceFlags.SAMPLED) class TestDecision(unittest.TestCase): def test_is_recording(self): self.assertTrue(sampling.Decision.is_recording(sampling.Decision.RECORD_ONLY)) self.assertTrue(sampling.Decision.is_recording(sampling.Decision.RECORD_AND_SAMPLE)) self.assertFalse(sampling.Decision.is_recording(sampling.Decision.DROP)) def test_is_sampled(self): self.assertFalse(sampling.Decision.is_sampled(sampling.Decision.RECORD_ONLY)) self.assertTrue(sampling.Decision.is_sampled(sampling.Decision.RECORD_AND_SAMPLE)) self.assertFalse(sampling.Decision.is_sampled(sampling.Decision.DROP)) class TestSamplingResult(unittest.TestCase): def test_ctr(self): attributes = {"asd": "test"} trace_state = {} # pylint: disable=E1137 trace_state["test"] = "123" result = sampling.SamplingResult(sampling.Decision.RECORD_ONLY, attributes, trace_state) self.assertIs(result.decision, sampling.Decision.RECORD_ONLY) with self.assertRaises(TypeError): result.attributes["test"] = "mess-this-up" self.assertTrue(len(result.attributes), 1) self.assertEqual(result.attributes["asd"], "test") self.assertEqual(result.trace_state["test"], "123") class TestSampler(unittest.TestCase): def _create_parent( self, trace_flags: trace.TraceFlags, is_remote=False, trace_state=None ) -> context_api.Context | None: if trace_flags is None: return None return trace.set_span_in_context(self._create_parent_span(trace_flags, is_remote, trace_state)) @staticmethod def _create_parent_span(trace_flags: trace.TraceFlags, is_remote=False, trace_state=None) -> trace.NonRecordingSpan: return trace.NonRecordingSpan( trace.SpanContext( 0xDEADBEEF, 0xDEADBEF0, is_remote=is_remote, trace_flags=trace_flags, trace_state=trace_state, ) ) def test_always_on(self): trace_state = trace.TraceState([("key", "value")]) test_data = (TO_DEFAULT, TO_SAMPLED, None) for trace_flags in test_data: with self.subTest(trace_flags=trace_flags): context = self._create_parent(trace_flags, False, trace_state) sample_result = sampling.ALWAYS_ON.should_sample( context, 0xDEADBEF1, "sampling on", trace.SpanKind.INTERNAL, attributes={"sampled.expect": "true"}, ) self.assertTrue(sample_result.decision.is_sampled()) self.assertEqual(sample_result.attributes, {"sampled.expect": "true"}) if context is not None: self.assertEqual(sample_result.trace_state, trace_state) else: self.assertIsNone(sample_result.trace_state) def test_always_off(self): trace_state = trace.TraceState([("key", "value")]) test_data = (TO_DEFAULT, TO_SAMPLED, None) for trace_flags in test_data: with self.subTest(trace_flags=trace_flags): context = self._create_parent(trace_flags, False, trace_state) sample_result = sampling.ALWAYS_OFF.should_sample( context, 0xDEADBEF1, "sampling off", trace.SpanKind.INTERNAL, attributes={"sampled.expect": "false"}, ) self.assertFalse(sample_result.decision.is_sampled()) self.assertEqual(sample_result.attributes, {}) if context is not None: self.assertEqual(sample_result.trace_state, trace_state) else: self.assertIsNone(sample_result.trace_state) def test_default_on(self): trace_state = trace.TraceState([("key", "value")]) context = self._create_parent(TO_DEFAULT, False, trace_state) sample_result = sampling.DEFAULT_ON.should_sample( context, 0xDEADBEF1, "unsampled parent, sampling on", trace.SpanKind.INTERNAL, attributes={"sampled.expect": "false"}, ) self.assertFalse(sample_result.decision.is_sampled()) self.assertEqual(sample_result.attributes, {}) self.assertEqual(sample_result.trace_state, trace_state) context = self._create_parent(TO_SAMPLED, False, trace_state) sample_result = sampling.DEFAULT_ON.should_sample( context, 0xDEADBEF1, "sampled parent, sampling on", trace.SpanKind.INTERNAL, attributes={"sampled.expect": "true"}, ) self.assertTrue(sample_result.decision.is_sampled()) self.assertEqual(sample_result.attributes, {"sampled.expect": "true"}) self.assertEqual(sample_result.trace_state, trace_state) sample_result = sampling.DEFAULT_ON.should_sample( None, 0xDEADBEF1, "no parent, sampling on", trace.SpanKind.INTERNAL, attributes={"sampled.expect": "true"}, ) self.assertTrue(sample_result.decision.is_sampled()) self.assertEqual(sample_result.attributes, {"sampled.expect": "true"}) self.assertIsNone(sample_result.trace_state) def test_default_off(self): trace_state = trace.TraceState([("key", "value")]) context = self._create_parent(TO_DEFAULT, False, trace_state) sample_result = sampling.DEFAULT_OFF.should_sample( context, 0xDEADBEF1, "unsampled parent, sampling off", trace.SpanKind.INTERNAL, attributes={"sampled.expect", "false"}, ) self.assertFalse(sample_result.decision.is_sampled()) self.assertEqual(sample_result.attributes, {}) self.assertEqual(sample_result.trace_state, trace_state) context = self._create_parent(TO_SAMPLED, False, trace_state) sample_result = sampling.DEFAULT_OFF.should_sample( context, 0xDEADBEF1, "sampled parent, sampling on", trace.SpanKind.INTERNAL, attributes={"sampled.expect": "true"}, ) self.assertTrue(sample_result.decision.is_sampled()) self.assertEqual(sample_result.attributes, {"sampled.expect": "true"}) self.assertEqual(sample_result.trace_state, trace_state) default_off = sampling.DEFAULT_OFF.should_sample( None, 0xDEADBEF1, "unsampled parent, sampling off", trace.SpanKind.INTERNAL, attributes={"sampled.expect": "false"}, ) self.assertFalse(default_off.decision.is_sampled()) self.assertEqual(default_off.attributes, {}) self.assertIsNone(default_off.trace_state) def test_probability_sampler(self): sampler = sampling.TraceIdRatioBased(0.5) # Check that we sample based on the trace ID if the parent context is # null # trace_state should also be empty since it is based off of parent sampled_result = sampler.should_sample( None, 0x7FFFFFFFFFFFFFFF, "sampled true", trace.SpanKind.INTERNAL, attributes={"sampled.expect": "true"}, ) self.assertTrue(sampled_result.decision.is_sampled()) self.assertEqual(sampled_result.attributes, {"sampled.expect": "true"}) self.assertIsNone(sampled_result.trace_state) not_sampled_result = sampler.should_sample( None, 0x8000000000000000, "sampled false", trace.SpanKind.INTERNAL, attributes={"sampled.expect": "false"}, ) self.assertFalse(not_sampled_result.decision.is_sampled()) self.assertEqual(not_sampled_result.attributes, {}) self.assertIsNone(sampled_result.trace_state) def test_probability_sampler_zero(self): default_off = sampling.TraceIdRatioBased(0.0) self.assertFalse(default_off.should_sample(None, 0x0, "span name").decision.is_sampled()) def test_probability_sampler_one(self): default_off = sampling.TraceIdRatioBased(1.0) self.assertTrue(default_off.should_sample(None, 0xFFFFFFFFFFFFFFFF, "span name").decision.is_sampled()) def test_probability_sampler_limits(self): # Sample one of every 2^64 (= 5e-20) traces. This is the lowest # possible meaningful sampling rate, only traces with trace ID 0x0 # should get sampled. almost_always_off = sampling.TraceIdRatioBased(2**-64) self.assertTrue(almost_always_off.should_sample(None, 0x0, "span name").decision.is_sampled()) self.assertFalse(almost_always_off.should_sample(None, 0x1, "span name").decision.is_sampled()) self.assertEqual(sampling.TraceIdRatioBased.get_bound_for_rate(2**-64), 0x1) # Sample every trace with trace ID less than 0xffffffffffffffff. In # principle this is the highest possible sampling rate less than 1, but # we can't actually express this rate as a float! # # In practice, the highest possible sampling rate is: # # 1 - sys.float_info.epsilon almost_always_on = sampling.TraceIdRatioBased(1 - 2**-64) self.assertTrue(almost_always_on.should_sample(None, 0xFFFFFFFFFFFFFFFE, "span name").decision.is_sampled()) # These tests are logically consistent, but fail because of the float # precision issue above. Changing the sampler to check fewer bytes of # the trace ID will cause these to pass. # self.assertFalse( # almost_always_on.should_sample( # None, # 0xFFFFFFFFFFFFFFFF, # "span name", # ).decision.is_sampled() # ) # self.assertEqual( # sampling.TraceIdRatioBased.get_bound_for_rate(1 - 2 ** -64)), # 0xFFFFFFFFFFFFFFFF, # ) # Check that a sampler with the highest effective sampling rate < 1 # refuses to sample traces with trace ID 0xffffffffffffffff. almost_almost_always_on = sampling.TraceIdRatioBased(1 - sys.float_info.epsilon) self.assertFalse( almost_almost_always_on.should_sample(None, 0xFFFFFFFFFFFFFFFF, "span name").decision.is_sampled() ) # Check that the highest effective sampling rate is actually lower than # the highest theoretical sampling rate. If this test fails the test # above is wrong. self.assertLess( almost_almost_always_on.bound, 0xFFFFFFFFFFFFFFFF, ) # pylint:disable=too-many-statements def exec_parent_based(self, parent_sampling_context): trace_state = trace.TraceState([("key", "value")]) sampler = sampling.ParentBased(sampling.ALWAYS_ON) # Check that the sampling decision matches the parent context if given with parent_sampling_context( self._create_parent_span( trace_flags=TO_DEFAULT, trace_state=trace_state, ) ) as context: # local, not sampled not_sampled_result = sampler.should_sample( context, 0x7FFFFFFFFFFFFFFF, "unsampled parent, sampling on", trace.SpanKind.INTERNAL, attributes={"sampled": "false"}, ) self.assertFalse(not_sampled_result.decision.is_sampled()) self.assertEqual(not_sampled_result.attributes, {}) self.assertEqual(not_sampled_result.trace_state, trace_state) with parent_sampling_context( self._create_parent_span( trace_flags=TO_DEFAULT, trace_state=trace_state, ) ) as context: sampler = sampling.ParentBased( root=sampling.ALWAYS_OFF, local_parent_not_sampled=sampling.ALWAYS_ON, ) # local, not sampled -> opposite sampler sampled_result = sampler.should_sample( context, 0x7FFFFFFFFFFFFFFF, "unsampled parent, sampling on", trace.SpanKind.INTERNAL, attributes={"sampled": "false"}, ) self.assertTrue(sampled_result.decision.is_sampled()) self.assertEqual(sampled_result.attributes, {"sampled": "false"}) self.assertEqual(sampled_result.trace_state, trace_state) with parent_sampling_context( self._create_parent_span( trace_flags=TO_SAMPLED, trace_state=trace_state, ) ) as context: sampler = sampling.ParentBased(sampling.ALWAYS_OFF) # local, sampled sampled_result = sampler.should_sample( context, 0x8000000000000000, "sampled parent, sampling off", trace.SpanKind.INTERNAL, attributes={"sampled": "true"}, trace_state=trace_state, ) self.assertTrue(sampled_result.decision.is_sampled()) self.assertEqual(sampled_result.attributes, {"sampled": "true"}) self.assertEqual(sampled_result.trace_state, trace_state) with parent_sampling_context( self._create_parent_span( trace_flags=TO_SAMPLED, trace_state=trace_state, ) ) as context: sampler = sampling.ParentBased( root=sampling.ALWAYS_ON, local_parent_sampled=sampling.ALWAYS_OFF, ) # local, sampled -> opposite sampler not_sampled_result = sampler.should_sample( context, 0x7FFFFFFFFFFFFFFF, "unsampled parent, sampling on", trace.SpanKind.INTERNAL, attributes={"sampled": "false"}, trace_state=trace_state, ) self.assertFalse(not_sampled_result.decision.is_sampled()) self.assertEqual(not_sampled_result.attributes, {}) self.assertEqual(not_sampled_result.trace_state, trace_state) with parent_sampling_context( self._create_parent_span( trace_flags=TO_DEFAULT, is_remote=True, trace_state=trace_state, ) ) as context: sampler = sampling.ParentBased(sampling.ALWAYS_ON) # remote, not sampled not_sampled_result = sampler.should_sample( context, 0x7FFFFFFFFFFFFFFF, "unsampled parent, sampling on", trace.SpanKind.INTERNAL, attributes={"sampled": "false"}, trace_state=trace_state, ) self.assertFalse(not_sampled_result.decision.is_sampled()) self.assertEqual(not_sampled_result.attributes, {}) self.assertEqual(not_sampled_result.trace_state, trace_state) with parent_sampling_context( self._create_parent_span( trace_flags=TO_DEFAULT, is_remote=True, trace_state=trace_state, ) ) as context: sampler = sampling.ParentBased( root=sampling.ALWAYS_OFF, remote_parent_not_sampled=sampling.ALWAYS_ON, ) # remote, not sampled -> opposite sampler sampled_result = sampler.should_sample( context, 0x7FFFFFFFFFFFFFFF, "unsampled parent, sampling on", trace.SpanKind.INTERNAL, attributes={"sampled": "false"}, ) self.assertTrue(sampled_result.decision.is_sampled()) self.assertEqual(sampled_result.attributes, {"sampled": "false"}) self.assertEqual(sampled_result.trace_state, trace_state) with parent_sampling_context( self._create_parent_span( trace_flags=TO_SAMPLED, is_remote=True, trace_state=trace_state, ) ) as context: sampler = sampling.ParentBased(sampling.ALWAYS_OFF) # remote, sampled sampled_result = sampler.should_sample( context, 0x8000000000000000, "sampled parent, sampling off", trace.SpanKind.INTERNAL, attributes={"sampled": "true"}, ) self.assertTrue(sampled_result.decision.is_sampled()) self.assertEqual(sampled_result.attributes, {"sampled": "true"}) self.assertEqual(sampled_result.trace_state, trace_state) with parent_sampling_context( self._create_parent_span( trace_flags=TO_SAMPLED, is_remote=True, trace_state=trace_state, ) ) as context: sampler = sampling.ParentBased( root=sampling.ALWAYS_ON, remote_parent_sampled=sampling.ALWAYS_OFF, ) # remote, sampled -> opposite sampler not_sampled_result = sampler.should_sample( context, 0x7FFFFFFFFFFFFFFF, "unsampled parent, sampling on", trace.SpanKind.INTERNAL, attributes={"sampled": "false"}, ) self.assertFalse(not_sampled_result.decision.is_sampled()) self.assertEqual(not_sampled_result.attributes, {}) self.assertEqual(not_sampled_result.trace_state, trace_state) # for root span follow decision of root sampler with parent_sampling_context(trace.INVALID_SPAN) as context: sampler = sampling.ParentBased(sampling.ALWAYS_OFF) not_sampled_result = sampler.should_sample( context, 0x8000000000000000, "parent, sampling off", trace.SpanKind.INTERNAL, attributes={"sampled": "false"}, ) self.assertFalse(not_sampled_result.decision.is_sampled()) self.assertEqual(not_sampled_result.attributes, {}) self.assertIsNone(not_sampled_result.trace_state) with parent_sampling_context(trace.INVALID_SPAN) as context: sampler = sampling.ParentBased(sampling.ALWAYS_ON) sampled_result = sampler.should_sample( context, 0x8000000000000000, "no parent, sampling on", trace.SpanKind.INTERNAL, attributes={"sampled": "true"}, trace_state=trace_state, ) self.assertTrue(sampled_result.decision.is_sampled()) self.assertEqual(sampled_result.attributes, {"sampled": "true"}) self.assertIsNone(sampled_result.trace_state) def test_parent_based_explicit_parent_context(self): @contextlib.contextmanager def explicit_parent_context(span: trace.Span): yield trace.set_span_in_context(span) self.exec_parent_based(explicit_parent_context) def test_parent_based_implicit_parent_context(self): @contextlib.contextmanager def implicit_parent_context(span: trace.Span): token = context_api.attach(trace.set_span_in_context(span)) yield None context_api.detach(token) self.exec_parent_based(implicit_parent_context) class TestAlwaysRecordSampler(unittest.TestCase): def setUp(self): self.mock_sampler: sampling.Sampler = unittest.mock.MagicMock() self.sampler: sampling.Sampler = sampling.AlwaysRecordSampler(self.mock_sampler) def test_get_description(self): static_sampler: sampling.Sampler = sampling.StaticSampler(sampling.Decision.DROP) test_sampler: sampling.Sampler = sampling.AlwaysRecordSampler(static_sampler) self.assertEqual( "AlwaysRecordSampler{AlwaysOffSampler}", test_sampler.get_description(), ) def test_record_and_sample_sampling_decision(self): self.validate_should_sample( sampling.Decision.RECORD_AND_SAMPLE, sampling.Decision.RECORD_AND_SAMPLE, ) def test_record_only_sampling_decision(self): self.validate_should_sample(sampling.Decision.RECORD_ONLY, sampling.Decision.RECORD_ONLY) def test_drop_sampling_decision(self): self.validate_should_sample(sampling.Decision.DROP, sampling.Decision.RECORD_ONLY) def validate_should_sample( self, root_decision: sampling.Decision, expected_decision: sampling.Decision, ): trace_state: trace.TraceState = trace.TraceState() trace_state = trace_state.add("key", root_decision.name) root_result: sampling.SamplingResult = sampling.SamplingResult( attributes={"key": root_decision.name}, decision=root_decision, trace_state=trace_state, ) self.mock_sampler.should_sample.return_value = root_result actual_result: sampling.SamplingResult = self.sampler.should_sample( parent_context=context_api.Context(), trace_id=0, name="name", kind=trace.SpanKind.CLIENT, attributes={"key": root_decision.name}, trace_state=trace.TraceState(), ) self.assertEqual(actual_result.decision, expected_decision) self.assertEqual(actual_result.attributes, root_result.attributes) self.assertEqual(actual_result.trace_state, root_result.trace_state) python-opentelemetry-1.45.2/opentelemetry-sdk/tests/trace/test_sdk_metrics.py000066400000000000000000000176611527070070700276710ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from unittest import TestCase from unittest.mock import patch from opentelemetry import trace as trace_api from opentelemetry.sdk.environment_variables import ( OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED, ) from opentelemetry.sdk.metrics import MeterProvider from opentelemetry.sdk.metrics.export import InMemoryMetricReader from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.sampling import ( ALWAYS_OFF, ALWAYS_ON, Decision, StaticSampler, ) from opentelemetry.trace.span import SpanContext @patch.dict("os.environ", {OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED: "true"}) class TestTracerProviderMetrics(TestCase): def setUp(self): self.metric_reader = InMemoryMetricReader() self.meter_provider = MeterProvider(metric_readers=[self.metric_reader]) def tearDown(self): self.meter_provider.shutdown() def assert_started_spans(self, metric_data, value, attrs): metrics = metric_data.resource_metrics[0].scope_metrics[0].metrics started_spans_metric = next((m for m in metrics if m.name == "otel.sdk.span.started"), None) self.assertIsNotNone(started_spans_metric) self.assertEqual(started_spans_metric.data.data_points[0].value, value) self.assertDictEqual(started_spans_metric.data.data_points[0].attributes, attrs) def assert_live_spans(self, metric_data, value, attrs): metrics = metric_data.resource_metrics[0].scope_metrics[0].metrics live_spans_metric = next((m for m in metrics if m.name == "otel.sdk.span.live"), None) if value is None: self.assertIsNone(live_spans_metric) return self.assertIsNotNone(live_spans_metric) self.assertEqual(live_spans_metric.data.data_points[0].value, value) self.assertDictEqual(live_spans_metric.data.data_points[0].attributes, attrs) def test_sampled(self): tracer_provider = TracerProvider(sampler=ALWAYS_ON, meter_provider=self.meter_provider) tracer = tracer_provider.get_tracer("test") span = tracer.start_span("span") metric_data = self.metric_reader.get_metrics_data() self.assert_started_spans( metric_data, 1, { "otel.span.parent.origin": "none", "otel.span.sampling_result": "RECORD_AND_SAMPLE", }, ) self.assert_live_spans( metric_data, 1, { "otel.span.sampling_result": "RECORD_AND_SAMPLE", }, ) span.end() metric_data = self.metric_reader.get_metrics_data() self.assert_started_spans( metric_data, 1, { "otel.span.parent.origin": "none", "otel.span.sampling_result": "RECORD_AND_SAMPLE", }, ) self.assert_live_spans( metric_data, 0, { "otel.span.sampling_result": "RECORD_AND_SAMPLE", }, ) def test_record_only(self): tracer_provider = TracerProvider( sampler=StaticSampler(Decision.RECORD_ONLY), meter_provider=self.meter_provider, ) tracer = tracer_provider.get_tracer("test") span = tracer.start_span("span") metric_data = self.metric_reader.get_metrics_data() self.assert_started_spans( metric_data, 1, { "otel.span.parent.origin": "none", "otel.span.sampling_result": "RECORD_ONLY", }, ) self.assert_live_spans( metric_data, 1, { "otel.span.sampling_result": "RECORD_ONLY", }, ) span.end() metric_data = self.metric_reader.get_metrics_data() self.assert_started_spans( metric_data, 1, { "otel.span.parent.origin": "none", "otel.span.sampling_result": "RECORD_ONLY", }, ) self.assert_live_spans( metric_data, 0, { "otel.span.sampling_result": "RECORD_ONLY", }, ) def test_dropped(self): tracer_provider = TracerProvider(sampler=ALWAYS_OFF, meter_provider=self.meter_provider) tracer = tracer_provider.get_tracer("test") span = tracer.start_span("span") metric_data = self.metric_reader.get_metrics_data() self.assert_started_spans( metric_data, 1, { "otel.span.parent.origin": "none", "otel.span.sampling_result": "DROP", }, ) self.assert_live_spans(metric_data, None, {}) span.end() metric_data = self.metric_reader.get_metrics_data() self.assert_started_spans( metric_data, 1, { "otel.span.parent.origin": "none", "otel.span.sampling_result": "DROP", }, ) self.assert_live_spans(metric_data, None, {}) def test_dropped_remote_parent(self): tracer_provider = TracerProvider(sampler=ALWAYS_OFF, meter_provider=self.meter_provider) tracer = tracer_provider.get_tracer("test") parent_span_context = SpanContext( trace_id=1, span_id=2, is_remote=True, ) parent_context = trace_api.set_span_in_context(trace_api.NonRecordingSpan(parent_span_context)) span = tracer.start_span("span", context=parent_context) metric_data = self.metric_reader.get_metrics_data() self.assert_started_spans( metric_data, 1, { "otel.span.parent.origin": "remote", "otel.span.sampling_result": "DROP", }, ) self.assert_live_spans(metric_data, None, {}) span.end() metric_data = self.metric_reader.get_metrics_data() self.assert_started_spans( metric_data, 1, { "otel.span.parent.origin": "remote", "otel.span.sampling_result": "DROP", }, ) self.assert_live_spans(metric_data, None, {}) def test_dropped_local_parent(self): tracer_provider = TracerProvider(sampler=ALWAYS_OFF, meter_provider=self.meter_provider) tracer = tracer_provider.get_tracer("test") parent_span_context = SpanContext( trace_id=1, span_id=2, is_remote=False, ) parent_context = trace_api.set_span_in_context(trace_api.NonRecordingSpan(parent_span_context)) span = tracer.start_span("span", context=parent_context) metric_data = self.metric_reader.get_metrics_data() self.assert_started_spans( metric_data, 1, { "otel.span.parent.origin": "local", "otel.span.sampling_result": "DROP", }, ) self.assert_live_spans(metric_data, None, {}) span.end() metric_data = self.metric_reader.get_metrics_data() self.assert_started_spans( metric_data, 1, { "otel.span.parent.origin": "local", "otel.span.sampling_result": "DROP", }, ) self.assert_live_spans(metric_data, None, {}) class TestTracerProviderMetricsDisabled(TestCase): def test_disabled_by_default(self): metric_reader = InMemoryMetricReader() meter_provider = MeterProvider(metric_readers=[metric_reader]) tracer_provider = TracerProvider(sampler=ALWAYS_ON, meter_provider=meter_provider) tracer = tracer_provider.get_tracer("test") with tracer.start_as_current_span("span"): pass self.assertIsNone(metric_reader.get_metrics_data()) meter_provider.shutdown() python-opentelemetry-1.45.2/opentelemetry-sdk/tests/trace/test_span_processor.py000066400000000000000000000530751527070070700304210ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import abc import gc import multiprocessing import os import time import unittest import weakref from threading import Event from unittest import mock from opentelemetry import trace as trace_api from opentelemetry.context import Context from opentelemetry.sdk import trace from opentelemetry.sdk.trace.export import SimpleSpanProcessor from opentelemetry.sdk.trace.export.in_memory_span_exporter import ( InMemorySpanExporter, ) def span_event_start_fmt(span_processor_name, span_name): return span_processor_name + ":" + span_name + ":start" def span_event_ending_fmt(span_processor_name, span_name): return span_processor_name + ":" + span_name + ":ending" def span_event_end_fmt(span_processor_name, span_name): return span_processor_name + ":" + span_name + ":end" class MySpanProcessor(trace.SpanProcessor): def __init__(self, name, span_list): self.name = name self.span_list = span_list def on_start(self, span: "trace.Span", parent_context: Context | None = None) -> None: self.span_list.append(span_event_start_fmt(self.name, span.name)) def on_end(self, span: "trace.Span") -> None: self.span_list.append(span_event_end_fmt(self.name, span.name)) class MyExtendedSpanProcessor(MySpanProcessor): def _on_ending(self, span: "trace.Span") -> None: self.span_list.append(span_event_ending_fmt(self.name, span.name)) class TestSpanProcessor(unittest.TestCase): def test_span_processor(self): tracer_provider = trace.TracerProvider() tracer = tracer_provider.get_tracer(__name__) spans_calls_list = [] # filled by MySpanProcessor expected_list = [] # filled by hand # Span processors are created but not added to the tracer yet sp1 = MySpanProcessor("SP1", spans_calls_list) sp2 = MySpanProcessor("SP2", spans_calls_list) with tracer.start_as_current_span("foo"): with tracer.start_as_current_span("bar"): with tracer.start_as_current_span("baz"): pass # at this point lists must be empty self.assertEqual(len(spans_calls_list), 0) # add single span processor tracer_provider.add_span_processor(sp1) with tracer.start_as_current_span("foo"): expected_list.append(span_event_start_fmt("SP1", "foo")) with tracer.start_as_current_span("bar"): expected_list.append(span_event_start_fmt("SP1", "bar")) with tracer.start_as_current_span("baz"): expected_list.append(span_event_start_fmt("SP1", "baz")) expected_list.append(span_event_end_fmt("SP1", "baz")) expected_list.append(span_event_end_fmt("SP1", "bar")) expected_list.append(span_event_end_fmt("SP1", "foo")) self.assertListEqual(spans_calls_list, expected_list) spans_calls_list.clear() expected_list.clear() # go for multiple span processors tracer_provider.add_span_processor(sp2) with tracer.start_as_current_span("foo"): expected_list.append(span_event_start_fmt("SP1", "foo")) expected_list.append(span_event_start_fmt("SP2", "foo")) with tracer.start_as_current_span("bar"): expected_list.append(span_event_start_fmt("SP1", "bar")) expected_list.append(span_event_start_fmt("SP2", "bar")) with tracer.start_as_current_span("baz"): expected_list.append(span_event_start_fmt("SP1", "baz")) expected_list.append(span_event_start_fmt("SP2", "baz")) expected_list.append(span_event_end_fmt("SP1", "baz")) expected_list.append(span_event_end_fmt("SP2", "baz")) expected_list.append(span_event_end_fmt("SP1", "bar")) expected_list.append(span_event_end_fmt("SP2", "bar")) expected_list.append(span_event_end_fmt("SP1", "foo")) expected_list.append(span_event_end_fmt("SP2", "foo")) # compare if two lists are the same self.assertListEqual(spans_calls_list, expected_list) # pylint: disable=too-many-statements def test_span_processor_with_on_ending(self): tracer_provider = trace.TracerProvider() tracer = tracer_provider.get_tracer(__name__) spans_calls_list = [] # filled by MySpanProcessor expected_list = [] # filled by hand # Span processors are created but not added to the tracer yet sp1 = MyExtendedSpanProcessor("SP1", spans_calls_list) sp2 = MyExtendedSpanProcessor("SP2", spans_calls_list) with tracer.start_as_current_span("foo"): with tracer.start_as_current_span("bar"): with tracer.start_as_current_span("baz"): pass # at this point lists must be empty self.assertEqual(len(spans_calls_list), 0) # add single span processor tracer_provider.add_span_processor(sp1) with tracer.start_as_current_span("foo"): expected_list.append(span_event_start_fmt("SP1", "foo")) with tracer.start_as_current_span("bar"): expected_list.append(span_event_start_fmt("SP1", "bar")) with tracer.start_as_current_span("baz"): expected_list.append(span_event_start_fmt("SP1", "baz")) expected_list.append(span_event_ending_fmt("SP1", "baz")) expected_list.append(span_event_end_fmt("SP1", "baz")) expected_list.append(span_event_ending_fmt("SP1", "bar")) expected_list.append(span_event_end_fmt("SP1", "bar")) expected_list.append(span_event_ending_fmt("SP1", "foo")) expected_list.append(span_event_end_fmt("SP1", "foo")) self.assertListEqual(spans_calls_list, expected_list) spans_calls_list.clear() expected_list.clear() # go for multiple span processors tracer_provider.add_span_processor(sp2) with tracer.start_as_current_span("foo"): expected_list.append(span_event_start_fmt("SP1", "foo")) expected_list.append(span_event_start_fmt("SP2", "foo")) with tracer.start_as_current_span("bar"): expected_list.append(span_event_start_fmt("SP1", "bar")) expected_list.append(span_event_start_fmt("SP2", "bar")) with tracer.start_as_current_span("baz"): expected_list.append(span_event_start_fmt("SP1", "baz")) expected_list.append(span_event_start_fmt("SP2", "baz")) expected_list.append(span_event_ending_fmt("SP1", "baz")) expected_list.append(span_event_ending_fmt("SP2", "baz")) expected_list.append(span_event_end_fmt("SP1", "baz")) expected_list.append(span_event_end_fmt("SP2", "baz")) expected_list.append(span_event_ending_fmt("SP1", "bar")) expected_list.append(span_event_ending_fmt("SP2", "bar")) expected_list.append(span_event_end_fmt("SP1", "bar")) expected_list.append(span_event_end_fmt("SP2", "bar")) expected_list.append(span_event_ending_fmt("SP1", "foo")) expected_list.append(span_event_ending_fmt("SP2", "foo")) expected_list.append(span_event_end_fmt("SP1", "foo")) expected_list.append(span_event_end_fmt("SP2", "foo")) # compare if two lists are the same self.assertListEqual(spans_calls_list, expected_list) def test_add_span_processor_after_span_creation(self): tracer_provider = trace.TracerProvider() tracer = tracer_provider.get_tracer(__name__) spans_calls_list = [] # filled by MySpanProcessor expected_list = [] # filled by hand # Span processors are created but not added to the tracer yet sp = MySpanProcessor("SP1", spans_calls_list) with tracer.start_as_current_span("foo"): with tracer.start_as_current_span("bar"): with tracer.start_as_current_span("baz"): # add span processor after spans have been created tracer_provider.add_span_processor(sp) expected_list.append(span_event_end_fmt("SP1", "baz")) expected_list.append(span_event_end_fmt("SP1", "bar")) expected_list.append(span_event_end_fmt("SP1", "foo")) self.assertListEqual(spans_calls_list, expected_list) def test_on_ending_not_implemented_does_not_raise(self): tracer_provider = trace.TracerProvider() tracer = tracer_provider.get_tracer(__name__) spans_calls_list = [] # filled by MySpanProcessor expected_list = [] # filled by hand # Does not implement _on_ending sp = MySpanProcessor("SP1", spans_calls_list) tracer_provider.add_span_processor(sp) try: with tracer.start_as_current_span("foo"): expected_list.append(span_event_start_fmt("SP1", "foo")) with tracer.start_as_current_span("bar"): expected_list.append(span_event_start_fmt("SP1", "bar")) with tracer.start_as_current_span("baz"): expected_list.append(span_event_start_fmt("SP1", "baz")) expected_list.append(span_event_end_fmt("SP1", "baz")) expected_list.append(span_event_end_fmt("SP1", "bar")) expected_list.append(span_event_end_fmt("SP1", "foo")) except NotImplementedError: self.fail("_on_ending() should not raise an exception") self.assertListEqual(spans_calls_list, expected_list) class MultiSpanProcessorTestBase(abc.ABC): @abc.abstractmethod def create_multi_span_processor( self, ) -> trace.SynchronousMultiSpanProcessor | trace.ConcurrentMultiSpanProcessor: pass @staticmethod def create_default_span() -> trace_api.Span: span_context = trace_api.SpanContext(37, 73, is_remote=False) return trace_api.NonRecordingSpan(span_context) def test_on_start(self): multi_processor = self.create_multi_span_processor() mocks = [mock.Mock(spec=trace.SpanProcessor) for _ in range(5)] for mock_processor in mocks: multi_processor.add_span_processor(mock_processor) span = self.create_default_span() context = Context() multi_processor.on_start(span, parent_context=context) for mock_processor in mocks: mock_processor.on_start.assert_called_once_with(span, parent_context=context) multi_processor.shutdown() def test_on_ending(self): multi_processor = self.create_multi_span_processor() mocks = [mock.Mock(spec=trace.SpanProcessor) for _ in range(5)] for mock_processor in mocks: multi_processor.add_span_processor(mock_processor) span = self.create_default_span() # pylint: disable=protected-access multi_processor._on_ending(span) for mock_processor in mocks: # pylint: disable=protected-access mock_processor._on_ending.assert_called_once_with(span) multi_processor.shutdown() def test_on_end(self): multi_processor = self.create_multi_span_processor() mocks = [mock.Mock(spec=trace.SpanProcessor) for _ in range(5)] for mock_processor in mocks: multi_processor.add_span_processor(mock_processor) span = self.create_default_span() multi_processor.on_end(span) for mock_processor in mocks: mock_processor.on_end.assert_called_once_with(span) multi_processor.shutdown() def test_on_shutdown(self): multi_processor = self.create_multi_span_processor() mocks = [mock.Mock(spec=trace.SpanProcessor) for _ in range(5)] for mock_processor in mocks: multi_processor.add_span_processor(mock_processor) multi_processor.shutdown() for mock_processor in mocks: mock_processor.shutdown.assert_called_once_with() def test_force_flush(self): multi_processor = self.create_multi_span_processor() mocks = [mock.Mock(spec=trace.SpanProcessor) for _ in range(5)] for mock_processor in mocks: multi_processor.add_span_processor(mock_processor) timeout_millis = 100 flushed = multi_processor.force_flush(timeout_millis) # pylint: disable=no-member self.assertTrue(flushed) for mock_processor in mocks: # pylint: disable=no-member self.assertEqual(1, mock_processor.force_flush.call_count) multi_processor.shutdown() def test_on_ending_not_implemented_does_not_raise(self): tracer_provider = trace.TracerProvider() tracer = tracer_provider.get_tracer(__name__) spans_calls_list = [] # filled by MySpanProcessor expected_list = [] # filled by hand multi_processor = self.create_multi_span_processor() # Does not implement _on_ending multi_processor.add_span_processor(MySpanProcessor("SP1", spans_calls_list)) tracer_provider.add_span_processor(multi_processor) try: with tracer.start_as_current_span("foo"): expected_list.append(span_event_start_fmt("SP1", "foo")) with tracer.start_as_current_span("bar"): expected_list.append(span_event_start_fmt("SP1", "bar")) with tracer.start_as_current_span("baz"): expected_list.append(span_event_start_fmt("SP1", "baz")) expected_list.append(span_event_end_fmt("SP1", "baz")) expected_list.append(span_event_end_fmt("SP1", "bar")) expected_list.append(span_event_end_fmt("SP1", "foo")) except NotImplementedError: # pylint: disable=no-member self.fail("_on_ending() should not raise an exception") # pylint: disable=no-member self.assertListEqual(spans_calls_list, expected_list) class TestSynchronousMultiSpanProcessor(MultiSpanProcessorTestBase, unittest.TestCase): def create_multi_span_processor( self, ) -> trace.SynchronousMultiSpanProcessor: return trace.SynchronousMultiSpanProcessor() def test_force_flush_late_by_timeout(self): multi_processor = trace.SynchronousMultiSpanProcessor() def delayed_flush(_): time.sleep(0.055) mock_processor1 = mock.Mock(spec=trace.SpanProcessor) mock_processor1.force_flush = mock.Mock(side_effect=delayed_flush) multi_processor.add_span_processor(mock_processor1) mock_processor2 = mock.Mock(spec=trace.SpanProcessor) multi_processor.add_span_processor(mock_processor2) flushed = multi_processor.force_flush(50) self.assertFalse(flushed) self.assertEqual(1, mock_processor1.force_flush.call_count) self.assertEqual(0, mock_processor2.force_flush.call_count) def test_force_flush_late_by_span_processor(self): multi_processor = trace.SynchronousMultiSpanProcessor() mock_processor1 = mock.Mock(spec=trace.SpanProcessor) mock_processor1.force_flush = mock.Mock(return_value=False) multi_processor.add_span_processor(mock_processor1) mock_processor2 = mock.Mock(spec=trace.SpanProcessor) multi_processor.add_span_processor(mock_processor2) flushed = multi_processor.force_flush(50) self.assertFalse(flushed) self.assertEqual(1, mock_processor1.force_flush.call_count) self.assertEqual(1, mock_processor2.force_flush.call_count) def test_force_flush_processor_returns_none(self): multi_processor = trace.SynchronousMultiSpanProcessor() mock_processor1 = mock.Mock(spec=trace.SpanProcessor) mock_processor1.force_flush = mock.Mock(return_value=None) multi_processor.add_span_processor(mock_processor1) mock_processor2 = mock.Mock(spec=trace.SpanProcessor) mock_processor2.force_flush = mock.Mock(return_value=True) multi_processor.add_span_processor(mock_processor2) flushed = multi_processor.force_flush(50) self.assertTrue(flushed) self.assertEqual(1, mock_processor1.force_flush.call_count) self.assertEqual(1, mock_processor2.force_flush.call_count) def test_force_flush_default_processor(self): multi_processor = trace.SynchronousMultiSpanProcessor() default_processor = trace.SpanProcessor() multi_processor.add_span_processor(default_processor) mock_processor = mock.Mock(spec=trace.SpanProcessor) mock_processor.force_flush = mock.Mock(return_value=True) multi_processor.add_span_processor(mock_processor) flushed = multi_processor.force_flush(50) self.assertTrue(flushed) self.assertEqual(1, mock_processor.force_flush.call_count) class TestConcurrentMultiSpanProcessor(MultiSpanProcessorTestBase, unittest.TestCase): def create_multi_span_processor( self, ) -> trace.ConcurrentMultiSpanProcessor: return trace.ConcurrentMultiSpanProcessor(3) def test_force_flush_late_by_timeout(self): multi_processor = trace.ConcurrentMultiSpanProcessor(5) wait_event = Event() def delayed_flush(_): wait_event.wait() late_mock = mock.Mock(spec=trace.SpanProcessor) late_mock.force_flush = mock.Mock(side_effect=delayed_flush) mocks = [mock.Mock(spec=trace.SpanProcessor) for _ in range(4)] mocks.insert(0, late_mock) for mock_processor in mocks: multi_processor.add_span_processor(mock_processor) flushed = multi_processor.force_flush(timeout_millis=25) # let the thread executing the late_mock continue wait_event.set() multi_processor.shutdown() self.assertFalse(flushed) for mock_processor in mocks: self.assertEqual(1, mock_processor.force_flush.call_count) def test_force_flush_late_by_span_processor(self): multi_processor = trace.ConcurrentMultiSpanProcessor(5) late_mock = mock.Mock(spec=trace.SpanProcessor) late_mock.force_flush = mock.Mock(return_value=False) mocks = [mock.Mock(spec=trace.SpanProcessor) for _ in range(4)] mocks.insert(0, late_mock) for mock_processor in mocks: multi_processor.add_span_processor(mock_processor) flushed = multi_processor.force_flush() self.assertFalse(flushed) for mock_processor in mocks: self.assertEqual(1, mock_processor.force_flush.call_count) multi_processor.shutdown() def test_force_flush_processor_returns_none(self): multi_processor = trace.ConcurrentMultiSpanProcessor(5) none_mock = mock.Mock(spec=trace.SpanProcessor) none_mock.force_flush = mock.Mock(return_value=None) mocks = [mock.Mock(spec=trace.SpanProcessor) for _ in range(4)] mocks.insert(0, none_mock) for mock_processor in mocks: multi_processor.add_span_processor(mock_processor) flushed = multi_processor.force_flush() self.assertTrue(flushed) for mock_processor in mocks: self.assertEqual(1, mock_processor.force_flush.call_count) multi_processor.shutdown() def test_processor_gc(self): multi_processor = trace.ConcurrentMultiSpanProcessor(5) weak_ref = weakref.ref(multi_processor) multi_processor.shutdown() # When the processor is garbage collected del multi_processor gc.collect() # Then the reference to the processor should no longer exist self.assertIsNone( weak_ref(), "The ConcurrentMultiSpanProcessor object created by this test wasn't garbage collected", ) @unittest.skipUnless(hasattr(os, "fork"), "needs *nix") def test_batch_span_processor_fork(self): multiprocessing_context = multiprocessing.get_context("fork") tracer_provider = trace.TracerProvider() tracer = tracer_provider.get_tracer(__name__) exporter = InMemorySpanExporter() multi_processor = trace.ConcurrentMultiSpanProcessor(2) multi_processor.add_span_processor(SimpleSpanProcessor(exporter)) tracer_provider.add_span_processor(multi_processor) # Use the ConcurrentMultiSpanProcessor in the main process. # This is necessary in this test to start using the underlying ThreadPoolExecutor and avoid false positive: with tracer.start_as_current_span("main process before fork span"): pass assert exporter.get_finished_spans()[-1].name == "main process before fork span" # The forked ConcurrentMultiSpanProcessor is usable in the child process: def child(conn): with tracer.start_as_current_span("child process span"): pass conn.send(exporter.get_finished_spans()[-1].name) conn.close() parent_conn, child_conn = multiprocessing_context.Pipe() process = multiprocessing_context.Process(target=child, args=(child_conn,)) process.start() has_response = parent_conn.poll(timeout=5) if not has_response: process.kill() self.fail("The child process did not send any message after 5 seconds, it's very probably locked") process.join(timeout=5) assert parent_conn.recv() == "child process span" # The ConcurrentMultiSpanProcessor is still usable in the main process after the child process termination: with tracer.start_as_current_span("main process after fork span"): pass assert exporter.get_finished_spans()[-1].name == "main process after fork span" python-opentelemetry-1.45.2/opentelemetry-sdk/tests/trace/test_trace.py000066400000000000000000002701751527070070700264610ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # pylint: disable=too-many-lines # pylint: disable=no-member import copy import dataclasses import json import os import shutil import subprocess import sys import unittest from importlib import reload from logging import ERROR, WARNING from pathlib import Path from random import randint from time import time_ns from unittest import mock from unittest.mock import Mock, patch from opentelemetry import trace as trace_api from opentelemetry.attributes import BoundedAttributes from opentelemetry.context import Context from opentelemetry.sdk import resources, trace from opentelemetry.sdk.environment_variables import ( OTEL_ATTRIBUTE_COUNT_LIMIT, OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT, OTEL_EVENT_ATTRIBUTE_COUNT_LIMIT, OTEL_EXPERIMENTAL_RESOURCE_DETECTORS, OTEL_LINK_ATTRIBUTE_COUNT_LIMIT, OTEL_SDK_DISABLED, OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT, OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT, OTEL_SPAN_EVENT_COUNT_LIMIT, OTEL_SPAN_LINK_COUNT_LIMIT, OTEL_TRACES_SAMPLER, OTEL_TRACES_SAMPLER_ARG, ) from opentelemetry.sdk.trace import ( Resource, TracerProvider, _RuleBasedTracerConfigurator, _TracerConfig, ) from opentelemetry.sdk.trace.id_generator import RandomIdGenerator from opentelemetry.sdk.trace.sampling import ( ALWAYS_OFF, ALWAYS_ON, Decision, ParentBased, StaticSampler, TraceIdRatioBased, ) from opentelemetry.sdk.util import BoundedDict, BoundedList, ns_to_iso_str from opentelemetry.sdk.util.instrumentation import ( InstrumentationInfo, _scope_name_matches_glob, ) from opentelemetry.test.spantestutil import ( get_span_with_dropped_attributes_events_links, new_tracer, ) from opentelemetry.trace import ( Status, StatusCode, get_tracer, set_tracer_provider, ) class TestTracer(unittest.TestCase): def test_no_deprecated_warning(self): with self.assertRaises(AssertionError): with self.assertWarns(DeprecationWarning): TracerProvider(Mock(), Mock()).get_tracer(Mock(), Mock()) # This is being added here to make sure the filter on # InstrumentationInfo does not affect other DeprecationWarnings that # may be raised. with self.assertWarns(DeprecationWarning): BoundedDict(0) def test_extends_api(self): tracer = new_tracer() self.assertIsInstance(tracer, trace.Tracer) self.assertIsInstance(tracer, trace_api.Tracer) def test_shutdown(self): tracer_provider = trace.TracerProvider() mock_processor1 = mock.Mock(spec=trace.SpanProcessor) tracer_provider.add_span_processor(mock_processor1) mock_processor2 = mock.Mock(spec=trace.SpanProcessor) tracer_provider.add_span_processor(mock_processor2) tracer_provider.shutdown() self.assertEqual(mock_processor1.shutdown.call_count, 1) self.assertEqual(mock_processor2.shutdown.call_count, 1) shutdown_python_code = """ import atexit from unittest import mock from opentelemetry.sdk import trace mock_processor = mock.Mock(spec=trace.SpanProcessor) def print_shutdown_count(): print(mock_processor.shutdown.call_count) # atexit hooks are called in inverse order they are added, so do this before # creating the tracer atexit.register(print_shutdown_count) tracer_provider = trace.TracerProvider({tracer_parameters}) tracer_provider.add_span_processor(mock_processor) {tracer_shutdown} """ def run_general_code(shutdown_on_exit, explicit_shutdown): tracer_parameters = "" tracer_shutdown = "" if not shutdown_on_exit: tracer_parameters = "shutdown_on_exit=False" if explicit_shutdown: tracer_shutdown = "tracer_provider.shutdown()" return subprocess.check_output( [ # use shutil to avoid calling python outside the # virtualenv on windows. shutil.which("python"), "-c", shutdown_python_code.format( tracer_parameters=tracer_parameters, tracer_shutdown=tracer_shutdown, ), ] ) # test default shutdown_on_exit (True) out = run_general_code(True, False) self.assertTrue(out.startswith(b"1")) # test that shutdown is called only once even if Tracer.shutdown is # called explicitly out = run_general_code(True, True) self.assertTrue(out.startswith(b"1")) # test shutdown_on_exit=False out = run_general_code(False, False) self.assertTrue(out.startswith(b"0")) def test_tracer_provider_accepts_concurrent_multi_span_processor(self): span_processor = trace.ConcurrentMultiSpanProcessor(2) tracer_provider = trace.TracerProvider(active_span_processor=span_processor) # pylint: disable=protected-access self.assertEqual(span_processor, tracer_provider._active_span_processor) @unittest.skipUnless( hasattr(os, "fork") and hasattr(os, "register_at_fork"), "requires os.fork and os.register_at_fork", ) def test_tracer_provider_updates_process_dependent_resource_after_fork( self, ): script_path = Path(__file__).parent / "scripts" / "tracer_provider_resource_after_fork.py" result = subprocess.run( [sys.executable, str(script_path)], capture_output=True, check=False, text=True, env={ **os.environ, OTEL_EXPERIMENTAL_RESOURCE_DETECTORS: "process", }, ) self.assertEqual( result.returncode, 0, f"stdout: {result.stdout}\nstderr: {result.stderr}", ) lines = result.stdout.strip().splitlines() child_payload = json.loads(lines[0]) parent_payload = json.loads(lines[1]) self.assertEqual(parent_payload["parent_resource_pid"], parent_payload["parent_pid"]) self.assertEqual(parent_payload["parent_tracer_pid"], parent_payload["parent_pid"]) self.assertEqual( parent_payload["parent_resource_pid_after_fork"], parent_payload["parent_pid"], ) self.assertEqual( parent_payload["parent_tracer_pid_after_fork"], parent_payload["parent_pid"], ) self.assertNotEqual(child_payload["child_pid"], parent_payload["parent_pid"]) self.assertEqual(child_payload["provider_pid"], child_payload["child_pid"]) self.assertEqual(child_payload["cached_tracer_pid"], child_payload["child_pid"]) self.assertEqual(child_payload["new_tracer_pid"], child_payload["child_pid"]) self.assertEqual(child_payload["span_pid"], child_payload["child_pid"]) def test_get_tracer_sdk(self): tracer_provider = trace.TracerProvider() tracer = tracer_provider.get_tracer( "module_name", "library_version", "schema_url", {"key1": "value1", "key2": 6}, ) # pylint: disable=protected-access self.assertEqual(tracer._instrumentation_scope._name, "module_name") # pylint: disable=protected-access self.assertEqual(tracer._instrumentation_scope._version, "library_version") # pylint: disable=protected-access self.assertEqual(tracer._instrumentation_scope._schema_url, "schema_url") # pylint: disable=protected-access self.assertEqual( tracer._instrumentation_scope._attributes, {"key1": "value1", "key2": 6}, ) def test_get_tracer_sdk_returns_same_tracer_when_called_with_same_instrumentation_scope( self, ): tracer_provider = trace.TracerProvider() tracer1 = tracer_provider.get_tracer( "module_name", "library_version", "schema_url", {"key1": "value1", "key2": 6}, ) tracer2 = tracer_provider.get_tracer( "module_name", "library_version", "schema_url", {"key1": "value1", "key2": 6}, ) self.assertEqual(tracer1, tracer2) self.assertTrue(tracer1 is tracer2) def test_get_tracer_sdk_sets_default_tracer_config_if_configurator_raises( self, ): def raising_tracer_configurator(tracer_scope): raise ValueError() tracer_provider = trace.TracerProvider(_tracer_configurator=raising_tracer_configurator) tracer = tracer_provider.get_tracer( "module_name", "library_version", ) # pylint: disable=protected-access self.assertEqual(tracer._tracer_config, _TracerConfig.default()) @mock.patch.dict("os.environ", {OTEL_SDK_DISABLED: "true"}) def test_get_tracer_with_sdk_disabled(self): tracer_provider = trace.TracerProvider() self.assertIsInstance(tracer_provider.get_tracer(Mock()), trace_api.NoOpTracer) def test_start_span_returns_invalid_span_if_not_enabled(self): # pylint: disable=protected-access tracer_provider = trace.TracerProvider() tracer = tracer_provider.get_tracer( "module_name", "library_version", "schema_url", {}, ) self.assertEqual(tracer._is_enabled(), True) tracer_provider._set_tracer_configurator(tracer_configurator=trace._disable_tracer_configurator) self.assertEqual(tracer._is_enabled(), False) span = tracer.start_span(name="invalid span") self.assertFalse(span.is_recording()) class TestTracerSampling(unittest.TestCase): def tearDown(self): reload(trace) def test_default_sampler(self): tracer = new_tracer() # Check that the default tracer creates real spans via the default # sampler root_span = tracer.start_span(name="root span", context=None) ctx = trace_api.set_span_in_context(root_span) self.assertIsInstance(root_span, trace.Span) child_span = tracer.start_span(name="child span", context=ctx) self.assertIsInstance(child_span, trace.Span) self.assertTrue(root_span.get_span_context().trace_flags.sampled) self.assertTrue(child_span.get_span_context().trace_flags.sampled) def test_default_sampler_type(self): tracer_provider = trace.TracerProvider() self.verify_default_sampler(tracer_provider) @mock.patch("opentelemetry.sdk.trace.sampling._get_from_env_or_default") def test_sampler_no_sampling(self, _get_from_env_or_default): tracer_provider = trace.TracerProvider(ALWAYS_OFF) tracer = tracer_provider.get_tracer(__name__) # Check that the default tracer creates no-op spans if the sampler # decides not to sampler root_span = tracer.start_span(name="root span", context=None) ctx = trace_api.set_span_in_context(root_span) self.assertIsInstance(root_span, trace_api.NonRecordingSpan) child_span = tracer.start_span(name="child span", context=ctx) self.assertIsInstance(child_span, trace_api.NonRecordingSpan) self.assertFalse(root_span.get_span_context().trace_flags.sampled) self.assertFalse(child_span.get_span_context().trace_flags.sampled) self.assertFalse(_get_from_env_or_default.called) @mock.patch.dict("os.environ", {OTEL_TRACES_SAMPLER: "always_off"}) def test_sampler_with_env(self): # pylint: disable=protected-access reload(trace) tracer_provider = trace.TracerProvider() self.assertIsInstance(tracer_provider.sampler, StaticSampler) self.assertEqual(tracer_provider.sampler._decision, Decision.DROP) tracer = tracer_provider.get_tracer(__name__) root_span = tracer.start_span(name="root span", context=None) # Should be no-op self.assertIsInstance(root_span, trace_api.NonRecordingSpan) @mock.patch.dict( "os.environ", { OTEL_TRACES_SAMPLER: "parentbased_traceidratio", OTEL_TRACES_SAMPLER_ARG: "0.25", }, ) def test_ratio_sampler_with_env(self): # pylint: disable=protected-access reload(trace) tracer_provider = trace.TracerProvider() self.assertIsInstance(tracer_provider.sampler, ParentBased) self.assertEqual(tracer_provider.sampler._root.rate, 0.25) @mock.patch.dict( "os.environ", { OTEL_TRACES_SAMPLER: "traceidratio", OTEL_TRACES_SAMPLER_ARG: "5.0", }, ) def test_ratio_sampler_with_out_of_range_env_arg_does_not_raise(self): # A syntactically valid but out-of-range OTEL_TRACES_SAMPLER_ARG # (outside [0.0, 1.0]) must not crash TracerProvider() construction. # It should degrade gracefully, the same way a non-numeric value # already does. # pylint: disable=protected-access reload(trace) tracer_provider = trace.TracerProvider() self.assertIsInstance(tracer_provider.sampler, TraceIdRatioBased) self.assertEqual(tracer_provider.sampler.rate, 1.0) @mock.patch.dict( "os.environ", { OTEL_TRACES_SAMPLER: "traceidratio", OTEL_TRACES_SAMPLER_ARG: "-0.5", }, ) def test_ratio_sampler_with_negative_env_arg_does_not_raise(self): # pylint: disable=protected-access reload(trace) tracer_provider = trace.TracerProvider() self.assertIsInstance(tracer_provider.sampler, TraceIdRatioBased) self.assertEqual(tracer_provider.sampler.rate, 1.0) @mock.patch.dict( "os.environ", { OTEL_TRACES_SAMPLER: "traceidratio", OTEL_TRACES_SAMPLER_ARG: "0.0", }, ) def test_ratio_sampler_with_env_arg_lower_boundary(self): # 0.0 is a valid boundary value and must be used as-is, not # treated as falsy/invalid and overridden by the fallback. # pylint: disable=protected-access reload(trace) tracer_provider = trace.TracerProvider() self.assertIsInstance(tracer_provider.sampler, TraceIdRatioBased) self.assertEqual(tracer_provider.sampler.rate, 0.0) @mock.patch.dict( "os.environ", { OTEL_TRACES_SAMPLER: "traceidratio", OTEL_TRACES_SAMPLER_ARG: "1.0", }, ) def test_ratio_sampler_with_env_arg_upper_boundary(self): # pylint: disable=protected-access reload(trace) tracer_provider = trace.TracerProvider() self.assertIsInstance(tracer_provider.sampler, TraceIdRatioBased) self.assertEqual(tracer_provider.sampler.rate, 1.0) def verify_default_sampler(self, tracer_provider): self.assertIsInstance(tracer_provider.sampler, ParentBased) # pylint: disable=protected-access self.assertEqual(tracer_provider.sampler._root, ALWAYS_ON) class TestSpanCreation(unittest.TestCase): def test_start_span_invalid_spancontext(self): """If an invalid span context is passed as the parent, the created span should use a new span id. Invalid span contexts should also not be added as a parent. This eliminates redundant error handling logic in exporters. """ tracer = new_tracer() parent_context = trace_api.set_span_in_context(trace_api.INVALID_SPAN_CONTEXT) new_span = tracer.start_span("root", context=parent_context) self.assertTrue(new_span.context.is_valid) self.assertIsNone(new_span.parent) def test_instrumentation_info(self): tracer_provider = trace.TracerProvider() schema_url = "https://opentelemetry.io/schemas/1.3.0" tracer1 = tracer_provider.get_tracer("instr1") tracer2 = tracer_provider.get_tracer("instr2", "1.3b3", schema_url) span1 = tracer1.start_span("s1") span2 = tracer2.start_span("s2") with self.assertWarns(DeprecationWarning): self.assertEqual(span1.instrumentation_info, InstrumentationInfo("instr1", "")) with self.assertWarns(DeprecationWarning): self.assertEqual( span2.instrumentation_info, InstrumentationInfo("instr2", "1.3b3", schema_url), ) with self.assertWarns(DeprecationWarning): self.assertEqual(span2.instrumentation_info.schema_url, schema_url) with self.assertWarns(DeprecationWarning): self.assertEqual(span2.instrumentation_info.version, "1.3b3") with self.assertWarns(DeprecationWarning): self.assertEqual(span2.instrumentation_info.name, "instr2") with self.assertWarns(DeprecationWarning): self.assertLess(span1.instrumentation_info, span2.instrumentation_info) # Check sortability. def test_invalid_instrumentation_info(self): tracer_provider = trace.TracerProvider() with self.assertLogs(level=ERROR): tracer1 = tracer_provider.get_tracer("") with self.assertLogs(level=ERROR): tracer2 = tracer_provider.get_tracer(None) self.assertIsInstance(tracer1.instrumentation_info, InstrumentationInfo) span1 = tracer1.start_span("foo") self.assertTrue(span1.is_recording()) self.assertEqual(tracer1.instrumentation_info.schema_url, "") self.assertEqual(tracer1.instrumentation_info.version, "") self.assertEqual(tracer1.instrumentation_info.name, "") self.assertIsInstance(tracer2.instrumentation_info, InstrumentationInfo) span2 = tracer2.start_span("bar") self.assertTrue(span2.is_recording()) self.assertEqual(tracer2.instrumentation_info.schema_url, "") self.assertEqual(tracer2.instrumentation_info.version, "") self.assertEqual(tracer2.instrumentation_info.name, "") self.assertEqual(tracer1.instrumentation_info, tracer2.instrumentation_info) def test_span_processor_for_source(self): tracer_provider = trace.TracerProvider() tracer1 = tracer_provider.get_tracer("instr1") tracer2 = tracer_provider.get_tracer("instr2", "1.3b3") span1 = tracer1.start_span("s1") span2 = tracer2.start_span("s2") # pylint:disable=protected-access self.assertIs(span1._span_processor, tracer_provider._active_span_processor) self.assertIs(span2._span_processor, tracer_provider._active_span_processor) def test_start_span_implicit(self): tracer = new_tracer() self.assertEqual(trace_api.get_current_span(), trace_api.INVALID_SPAN) root = tracer.start_span("root") self.assertIsNotNone(root.start_time) self.assertIsNone(root.end_time) self.assertEqual(root.kind, trace_api.SpanKind.INTERNAL) with trace_api.use_span(root, True): self.assertIs(trace_api.get_current_span(), root) with tracer.start_span("child", kind=trace_api.SpanKind.CLIENT) as child: self.assertIs(child.parent, root.get_span_context()) self.assertEqual(child.kind, trace_api.SpanKind.CLIENT) self.assertIsNotNone(child.start_time) self.assertIsNone(child.end_time) # The new child span should inherit the parent's context but # get a new span ID. root_context = root.get_span_context() child_context = child.get_span_context() self.assertEqual(root_context.trace_id, child_context.trace_id) self.assertNotEqual(root_context.span_id, child_context.span_id) self.assertEqual(root_context.trace_state, child_context.trace_state) self.assertEqual(root_context.trace_flags, child_context.trace_flags) # Verify start_span() did not set the current span. self.assertIs(trace_api.get_current_span(), root) self.assertIsNotNone(child.end_time) self.assertEqual(trace_api.get_current_span(), trace_api.INVALID_SPAN) self.assertIsNotNone(root.end_time) def test_start_span_explicit(self): tracer = new_tracer() other_parent = trace._Span( "name", trace_api.SpanContext( trace_id=0x000000000000000000000000DEADBEEF, span_id=0x00000000DEADBEF0, is_remote=False, trace_flags=trace_api.TraceFlags(trace_api.TraceFlags.SAMPLED), ), ) other_parent_context = trace_api.set_span_in_context(other_parent) self.assertEqual(trace_api.get_current_span(), trace_api.INVALID_SPAN) root = tracer.start_span("root") self.assertIsNotNone(root.start_time) self.assertIsNone(root.end_time) # Test with the implicit root span with trace_api.use_span(root, True): self.assertIs(trace_api.get_current_span(), root) with tracer.start_span("stepchild", other_parent_context) as child: # The child's parent should be the one passed in, # not the current span. self.assertNotEqual(child.parent, root) self.assertIs(child.parent, other_parent.get_span_context()) self.assertIsNotNone(child.start_time) self.assertIsNone(child.end_time) # The child should inherit its context from the explicit # parent, not the current span. child_context = child.get_span_context() self.assertEqual( other_parent.get_span_context().trace_id, child_context.trace_id, ) self.assertNotEqual( other_parent.get_span_context().span_id, child_context.span_id, ) self.assertEqual( other_parent.get_span_context().trace_state, child_context.trace_state, ) self.assertTrue(other_parent.get_span_context().trace_flags.sampled) # Verify start_span() did not set the current span. self.assertIs(trace_api.get_current_span(), root) # Verify ending the child did not set the current span. self.assertIs(trace_api.get_current_span(), root) self.assertIsNotNone(child.end_time) def test_start_span_preserves_parent_random_trace_id_flag(self): tracer = new_tracer() for parent_trace_flags in ( trace_api.TraceFlags(trace_api.TraceFlags.SAMPLED), trace_api.TraceFlags(trace_api.TraceFlags.SAMPLED | trace_api.TraceFlags.RANDOM_TRACE_ID), ): with self.subTest(parent_trace_flags=parent_trace_flags): parent_context = trace_api.SpanContext( trace_id=0x000000000000000000000000DEADBEEF, span_id=0x00000000DEADBEF0, is_remote=True, trace_flags=parent_trace_flags, ) context = trace_api.set_span_in_context(trace_api.NonRecordingSpan(parent_context)) child = tracer.start_span("child", context) child_trace_flags = child.get_span_context().trace_flags self.assertEqual( parent_trace_flags.random_trace_id, child_trace_flags.random_trace_id, ) def test_start_as_current_span_implicit(self): tracer = new_tracer() self.assertEqual(trace_api.get_current_span(), trace_api.INVALID_SPAN) with tracer.start_as_current_span("root") as root: self.assertIs(trace_api.get_current_span(), root) with tracer.start_as_current_span("child") as child: self.assertIs(trace_api.get_current_span(), child) self.assertIs(child.parent, root.get_span_context()) # After exiting the child's scope the parent should become the # current span again. self.assertIs(trace_api.get_current_span(), root) self.assertIsNotNone(child.end_time) self.assertEqual(trace_api.get_current_span(), trace_api.INVALID_SPAN) self.assertIsNotNone(root.end_time) def test_start_as_current_span_explicit(self): tracer = new_tracer() other_parent = trace._Span( "name", trace_api.SpanContext( trace_id=0x000000000000000000000000DEADBEEF, span_id=0x00000000DEADBEF0, is_remote=False, trace_flags=trace_api.TraceFlags(trace_api.TraceFlags.SAMPLED), ), ) other_parent_ctx = trace_api.set_span_in_context(other_parent) self.assertEqual(trace_api.get_current_span(), trace_api.INVALID_SPAN) # Test with the implicit root span with tracer.start_as_current_span("root") as root: self.assertIs(trace_api.get_current_span(), root) self.assertIsNotNone(root.start_time) self.assertIsNone(root.end_time) with tracer.start_as_current_span("stepchild", other_parent_ctx) as child: # The child should become the current span as usual, but its # parent should be the one passed in, not the # previously-current span. self.assertIs(trace_api.get_current_span(), child) self.assertNotEqual(child.parent, root) self.assertIs(child.parent, other_parent.get_span_context()) # After exiting the child's scope the last span on the stack should # become current, not the child's parent. self.assertNotEqual(trace_api.get_current_span(), other_parent) self.assertIs(trace_api.get_current_span(), root) self.assertIsNotNone(child.end_time) def test_start_as_current_span_decorator(self): tracer = new_tracer() self.assertEqual(trace_api.get_current_span(), trace_api.INVALID_SPAN) @tracer.start_as_current_span("root") def func(): root = trace_api.get_current_span() with tracer.start_as_current_span("child") as child: self.assertIs(trace_api.get_current_span(), child) self.assertIs(child.parent, root.get_span_context()) # After exiting the child's scope the parent should become the # current span again. self.assertIs(trace_api.get_current_span(), root) self.assertIsNotNone(child.end_time) return root root1 = func() self.assertEqual(trace_api.get_current_span(), trace_api.INVALID_SPAN) self.assertIsNotNone(root1.end_time) # Second call must create a new span root2 = func() self.assertEqual(trace_api.get_current_span(), trace_api.INVALID_SPAN) self.assertIsNotNone(root2.end_time) self.assertIsNot(root1, root2) def test_start_as_current_span_no_end_on_exit(self): tracer = new_tracer() with tracer.start_as_current_span("root", end_on_exit=False) as root: self.assertIsNone(root.end_time) self.assertIsNone(root.end_time) def test_explicit_span_resource(self): resource = resources.Resource.create({}) tracer_provider = trace.TracerProvider(resource=resource) tracer = tracer_provider.get_tracer(__name__) span = tracer.start_span("root") self.assertIs(span.resource, resource) def test_update_resource(self): initial_resource = resources.Resource({"one": "one", "two": "old"}) updating_resource = resources.Resource({"two": "new", "three": "three"}) tracer_provider = trace.TracerProvider(resource=initial_resource) tracer = tracer_provider.get_tracer(__name__) other_tracer = tracer_provider.get_tracer("other") original_span = tracer.start_span("original") # pylint: disable-next=protected-access tracer_provider._update_resource(updating_resource) self.assertEqual( tracer_provider.resource.attributes, {"one": "one", "two": "new", "three": "three"}, ) self.assertIs(tracer.resource, tracer_provider.resource) self.assertIs(other_tracer.resource, tracer_provider.resource) self.assertIs(original_span.resource, initial_resource) updated_span = tracer.start_span("updated") self.assertIs(updated_span.resource, tracer_provider.resource) updated_tracer = tracer_provider.get_tracer("new") self.assertIs(updated_tracer.resource, tracer_provider.resource) new_span = updated_tracer.start_span("new") self.assertIs(new_span.resource, tracer_provider.resource) @patch("sys.executable", "/usr/bin/python3") def test_default_span_resource(self): tracer_provider = trace.TracerProvider() tracer = tracer_provider.get_tracer(__name__) span = tracer.start_span("root") # pylint: disable=protected-access self.assertIsInstance(span.resource, resources.Resource) self.assertEqual( span.resource.attributes.get(resources.SERVICE_NAME), "unknown_service:python3", ) self.assertEqual( span.resource.attributes.get(resources.TELEMETRY_SDK_LANGUAGE), "python", ) self.assertEqual( span.resource.attributes.get(resources.TELEMETRY_SDK_NAME), "opentelemetry", ) self.assertEqual( span.resource.attributes.get(resources.TELEMETRY_SDK_VERSION), resources._OPENTELEMETRY_SDK_VERSION, ) def test_span_context_remote_flag(self): tracer = new_tracer() span = tracer.start_span("foo") self.assertFalse(span.context.is_remote) def test_disallow_direct_span_creation(self): with self.assertRaises(TypeError): # pylint: disable=abstract-class-instantiated trace.Span("name", mock.Mock(spec=trace_api.SpanContext)) def test_surplus_span_links(self): # pylint: disable=protected-access max_links = trace.SpanLimits().max_links links = [trace_api.Link(trace_api.SpanContext(0x1, idx, is_remote=False)) for idx in range(16 + max_links)] tracer = new_tracer() with tracer.start_as_current_span("span", links=links) as root: self.assertEqual(len(root.links), max_links) def test_surplus_span_attributes(self): # pylint: disable=protected-access max_attrs = trace.SpanLimits().max_span_attributes attributes = {str(idx): idx for idx in range(16 + max_attrs)} tracer = new_tracer() with tracer.start_as_current_span("span", attributes=attributes) as root: self.assertEqual(len(root.attributes), max_attrs) class TestReadableSpan(unittest.TestCase): def test_links(self): span = trace.ReadableSpan("test") self.assertEqual(span.links, ()) span = trace.ReadableSpan( "test", links=[trace_api.Link(context=trace_api.INVALID_SPAN_CONTEXT)] * 2, ) self.assertEqual(len(span.links), 2) for link in span.links: self.assertFalse(link.context.is_valid) def test_events(self): span = trace.ReadableSpan("test") self.assertEqual(span.events, ()) events = [ trace.Event("foo1", {"bar1": "baz1"}), trace.Event("foo2", {"bar2": "baz2"}), ] span = trace.ReadableSpan("test", events=events) self.assertEqual(span.events, tuple(events)) def test_event_dropped_attributes(self): event1 = trace.Event("foo1", BoundedAttributes(0, attributes={"bar1": "baz1"})) self.assertEqual(event1.dropped_attributes, 1) event2 = trace.Event("foo2", {"bar2": "baz2"}) self.assertEqual(event2.dropped_attributes, 0) def test_link_dropped_attributes(self): link1 = trace_api.Link( mock.Mock(spec=trace_api.SpanContext), BoundedAttributes(0, attributes={"bar1": "baz1"}), ) self.assertEqual(link1.dropped_attributes, 1) link2 = trace_api.Link( mock.Mock(spec=trace_api.SpanContext), {"bar2": "baz2"}, ) self.assertEqual(link2.dropped_attributes, 0) def test_deepcopy(self): context = trace_api.SpanContext( trace_id=0x000000000000000000000000DEADBEEF, span_id=0x00000000DEADBEF0, is_remote=False, ) attributes = BoundedAttributes(10, {"key1": "value1", "key2": 42}, immutable=False) events = BoundedList(10) events.extend( ( trace.Event("event1", {"ekey": "evalue"}), trace.Event("event2", {"ekey2": "evalue2"}), ) ) links = [ trace_api.Link( context=trace_api.INVALID_SPAN_CONTEXT, attributes={"lkey": "lvalue"}, ) ] span = trace.ReadableSpan( name="test-span", context=context, attributes=attributes, events=events, links=links, status=Status(StatusCode.OK), ) span_copy = copy.deepcopy(span) self.assertEqual(span_copy.name, span.name) self.assertEqual(span_copy.status.status_code, span.status.status_code) self.assertEqual(span_copy.context.trace_id, span.context.trace_id) self.assertEqual(span_copy.context.span_id, span.context.span_id) self.assertEqual(dict(span_copy.attributes), dict(span.attributes)) attributes["key1"] = "mutated" self.assertNotEqual(span_copy.attributes["key1"], span.attributes["key1"]) self.assertEqual(len(span_copy.events), len(span.events)) self.assertIsNot(span_copy.events, span.events) self.assertEqual(span_copy.events[0].name, span.events[0].name) self.assertEqual(span_copy.events[0].attributes, span.events[0].attributes) self.assertEqual(len(span_copy.links), len(span.links)) self.assertEqual(span_copy.links[0].attributes, span.links[0].attributes) links[0] = trace_api.Link( context=trace_api.INVALID_SPAN_CONTEXT, attributes={"mutated": "link"}, ) self.assertNotIn("mutated", span_copy.links[0].attributes) class DummyError(Exception): pass class TestSpan(unittest.TestCase): # pylint: disable=too-many-public-methods def setUp(self): self.tracer = new_tracer() def test_basic_span(self): span = trace._Span("name", mock.Mock(spec=trace_api.SpanContext)) self.assertEqual(span.name, "name") def test_attributes(self): with self.tracer.start_as_current_span("root") as root: root.set_attributes( { "http.request.method": "GET", "url.full": "https://example.com:779/path/12/?q=d#123", } ) root.set_attribute("http.response.status_code", 200) root.set_attribute("http.status_text", "OK") root.set_attribute("misc.pi", 3.14) root.set_attribute("mapping-key", {"key": "value"}) root.set_attribute("array-key", [1, 2, 3]) root.set_attribute( "complex-key", [{"key1": "value1"}, {"key2": 42}, "key3", [1, 2, 3]], ) # Setting an attribute with the same key as an existing attribute # SHOULD overwrite the existing attribute's value. root.set_attribute("attr-key", "attr-value1") root.set_attribute("attr-key", "attr-value2") root.set_attribute("empty-list", []) list_of_bools = [True, True, False] root.set_attribute("list-of-bools", list_of_bools) list_of_numerics = [123, 314, 0] root.set_attribute("list-of-numerics", list_of_numerics) self.assertEqual(len(root.attributes), 12) self.assertEqual(root.attributes["http.request.method"], "GET") self.assertEqual( root.attributes["url.full"], "https://example.com:779/path/12/?q=d#123", ) self.assertEqual(root.attributes["http.response.status_code"], 200) self.assertEqual(root.attributes["http.status_text"], "OK") self.assertEqual(root.attributes["misc.pi"], 3.14) self.assertEqual(root.attributes["attr-key"], "attr-value2") self.assertEqual(root.attributes["empty-list"], ()) self.assertEqual(root.attributes["list-of-bools"], (True, True, False)) list_of_bools.append(False) self.assertEqual(root.attributes["list-of-bools"], (True, True, False)) self.assertEqual(root.attributes["list-of-numerics"], (123, 314, 0)) list_of_numerics.append(227) self.assertEqual(root.attributes["list-of-numerics"], (123, 314, 0)) attributes = { "attr-key": "val", "attr-key2": "val2", "attr-in-both": "span-attr", } with self.tracer.start_as_current_span("root2", attributes=attributes) as root: self.assertEqual(len(root.attributes), 3) self.assertEqual(root.attributes["attr-key"], "val") self.assertEqual(root.attributes["attr-key2"], "val2") self.assertEqual(root.attributes["attr-in-both"], "span-attr") def test_invalid_attribute_values(self): # This class has no __str__ or __repr__ method, so BoundedAttributes does # not attempt to convert it to a string and defaults to returning None. class NoStrNoRepr: def __init__(self): pass with self.tracer.start_as_current_span("root") as root: with self.assertLogs(level=WARNING): root.set_attributes( { "correct-value": "foo", "bad-type": NoStrNoRepr(), } ) self.assertEqual(len(root.attributes), 2) self.assertEqual(root.attributes["correct-value"], "foo") self.assertIsNone(root.attributes["bad-type"]) def test_byte_type_attribute_value(self): with self.tracer.start_as_current_span("root") as root: root.set_attribute("valid-byte-type-attribute", b"valid byte") self.assertTrue(isinstance(root.attributes["valid-byte-type-attribute"], bytes)) def test_sampling_attributes(self): sampling_attributes = { "sampler-attr": "sample-val", "attr-in-both": "decision-attr", } tracer_provider = trace.TracerProvider(StaticSampler(Decision.RECORD_AND_SAMPLE)) self.tracer = tracer_provider.get_tracer(__name__) with self.tracer.start_as_current_span(name="root2", attributes=sampling_attributes) as root: self.assertEqual(len(root.attributes), 2) self.assertEqual(root.attributes["sampler-attr"], "sample-val") self.assertEqual(root.attributes["attr-in-both"], "decision-attr") self.assertTrue(root.get_span_context().trace_flags.sampled) def test_events(self): self.assertEqual(trace_api.get_current_span(), trace_api.INVALID_SPAN) with self.tracer.start_as_current_span("root") as root: # only event name root.add_event("event0") # event name and attributes root.add_event("event1", {"name": "pluto", "some_bools": [True, False]}) # event name, attributes and timestamp now = time_ns() root.add_event("event2", {"name": ["birthday"]}, now) mutable_list = ["original_contents"] root.add_event("event3", {"name": mutable_list}) self.assertEqual(len(root.events), 4) self.assertEqual(root.events[0].name, "event0") self.assertEqual(root.events[0].attributes, {}) self.assertEqual(root.events[1].name, "event1") self.assertEqual( root.events[1].attributes, {"name": "pluto", "some_bools": (True, False)}, ) self.assertEqual(root.events[2].name, "event2") self.assertEqual(root.events[2].attributes, {"name": ("birthday",)}) self.assertEqual(root.events[2].timestamp, now) self.assertEqual(root.events[3].name, "event3") self.assertEqual(root.events[3].attributes, {"name": ("original_contents",)}) mutable_list = ["new_contents"] self.assertEqual(root.events[3].attributes, {"name": ("original_contents",)}) def test_events_are_immutable(self): event_properties = [prop for prop in dir(trace.EventBase) if not prop.startswith("_")] with self.tracer.start_as_current_span("root") as root: root.add_event("event0", {"name": ["birthday"]}) event = root.events[0] for prop in event_properties: with self.assertRaises(AttributeError): setattr(event, prop, "something") def test_event_attributes_are_immutable(self): with self.tracer.start_as_current_span("root") as root: root.add_event("event0", {"name": ["birthday"]}) event = root.events[0] with self.assertRaises(TypeError): event.attributes["name"][0] = "happy" with self.assertRaises(TypeError): event.attributes["name"] = "hello" def test_setting_event_attributes(self): self.assertEqual(trace_api.get_current_span(), trace_api.INVALID_SPAN) with self.tracer.start_as_current_span("root") as root: root.add_event("event0", {"attr1": True, "attr2": ["hi", False]}) root.add_event("event0", {"attr1": {}}) root.add_event("event0", {"attr1": [[True]]}) root.add_event("event0", {"attr1": [{}], "attr2": [1, 2]}) self.assertEqual(len(root.events), 4) self.assertEqual( root.events[0].attributes, {"attr1": True, "attr2": ("hi", False)}, ) self.assertEqual(root.events[1].attributes, {"attr1": {}}) self.assertEqual(root.events[2].attributes, {"attr1": ((True,),)}) self.assertEqual(root.events[3].attributes, {"attr2": (1, 2), "attr1": ({},)}) def test_links(self): id_generator = RandomIdGenerator() other_context1 = trace_api.SpanContext( trace_id=id_generator.generate_trace_id(), span_id=id_generator.generate_span_id(), is_remote=False, ) other_context2 = trace_api.SpanContext( trace_id=id_generator.generate_trace_id(), span_id=id_generator.generate_span_id(), is_remote=False, ) links = ( trace_api.Link(other_context1), trace_api.Link(other_context2, {"name": "neighbor"}), ) with self.tracer.start_as_current_span("root", links=links) as root: self.assertEqual(len(root.links), 2) self.assertEqual(root.links[0].context.trace_id, other_context1.trace_id) self.assertEqual(root.links[0].context.span_id, other_context1.span_id) self.assertEqual(0, len(root.links[0].attributes)) self.assertEqual(root.links[1].context.trace_id, other_context2.trace_id) self.assertEqual(root.links[1].context.span_id, other_context2.span_id) self.assertEqual(root.links[1].attributes, {"name": "neighbor"}) with self.assertRaises(TypeError): root.links[1].attributes["name"] = "new_neighbour" def test_add_link(self): id_generator = RandomIdGenerator() other_context = trace_api.SpanContext( trace_id=id_generator.generate_trace_id(), span_id=id_generator.generate_span_id(), is_remote=False, ) with self.tracer.start_as_current_span("root") as root: root.add_link(other_context, {"name": "neighbor"}) self.assertEqual(len(root.links), 1) self.assertEqual(root.links[0].context.trace_id, other_context.trace_id) self.assertEqual(root.links[0].context.span_id, other_context.span_id) self.assertEqual(root.links[0].attributes, {"name": "neighbor"}) with self.assertRaises(TypeError): root.links[0].attributes["name"] = "new_neighbour" def test_add_link_with_invalid_span_context(self): other_context = trace_api.INVALID_SPAN_CONTEXT with self.tracer.start_as_current_span("root") as root: root.add_link(other_context) root.add_link(None) self.assertEqual(len(root.links), 0) with self.tracer.start_as_current_span("root", links=[trace_api.Link(other_context), None]) as root: self.assertEqual(len(root.links), 0) def test_add_link_with_invalid_span_context_with_attributes(self): invalid_context = trace_api.INVALID_SPAN_CONTEXT with self.tracer.start_as_current_span("root") as root: root.add_link(invalid_context) root.add_link(invalid_context, {"name": "neighbor"}) self.assertEqual(len(root.links), 1) self.assertEqual(root.links[0].attributes, {"name": "neighbor"}) with self.tracer.start_as_current_span( "root", links=[ trace_api.Link(invalid_context, {"name": "neighbor"}), trace_api.Link(invalid_context), ], ) as root: self.assertEqual(len(root.links), 1) def test_add_link_with_invalid_span_context_with_tracestate(self): invalid_context = trace.SpanContext( trace_api.INVALID_TRACE_ID, trace_api.INVALID_SPAN_ID, is_remote=False, trace_state="foo=bar", ) with self.tracer.start_as_current_span("root") as root: root.add_link(invalid_context) root.add_link(trace_api.INVALID_SPAN_CONTEXT) self.assertEqual(len(root.links), 1) self.assertEqual(root.links[0].context.trace_state, "foo=bar") with self.tracer.start_as_current_span( "root", links=[ trace_api.Link(invalid_context), trace_api.Link(trace_api.INVALID_SPAN_CONTEXT), ], ) as root: self.assertEqual(len(root.links), 1) def test_update_name(self): with self.tracer.start_as_current_span("root") as root: # name root.update_name("toor") self.assertEqual(root.name, "toor") def test_start_span(self): """Start twice, end a not started""" span = trace._Span("name", mock.Mock(spec=trace_api.SpanContext)) # end not started span self.assertRaises(RuntimeError, span.end) span.start() start_time = span.start_time with self.assertLogs(level=WARNING): span.start() self.assertEqual(start_time, span.start_time) self.assertIsNotNone(span.status) self.assertIs(span.status.status_code, trace_api.StatusCode.UNSET) # status new_status = trace_api.status.Status(trace_api.StatusCode.ERROR, "Test description") span.set_status(new_status) self.assertIs(span.status.status_code, trace_api.StatusCode.ERROR) self.assertIs(span.status.description, "Test description") def test_start_accepts_context(self): # pylint: disable=no-self-use span_processor = mock.Mock(spec=trace.SpanProcessor) span = trace._Span( "name", mock.Mock(spec=trace_api.SpanContext), span_processor=span_processor, ) context = Context() span.start(parent_context=context) span_processor.on_start.assert_called_once_with(span, parent_context=context) def test_span_override_start_and_end_time(self): """Span sending custom start_time and end_time values""" span = trace._Span("name", mock.Mock(spec=trace_api.SpanContext)) start_time = 123 span.start(start_time) self.assertEqual(start_time, span.start_time) end_time = 456 span.end(end_time) self.assertEqual(end_time, span.end_time) def test_span_set_status(self): span1 = self.tracer.start_span("span1") span1.set_status(Status(status_code=StatusCode.ERROR)) self.assertEqual(span1.status.status_code, StatusCode.ERROR) self.assertEqual(span1.status.description, None) span2 = self.tracer.start_span("span2") span2.set_status(Status(status_code=StatusCode.ERROR, description="desc")) self.assertEqual(span2.status.status_code, StatusCode.ERROR) self.assertEqual(span2.status.description, "desc") span3 = self.tracer.start_span("span3") span3.set_status(StatusCode.ERROR) self.assertEqual(span3.status.status_code, StatusCode.ERROR) self.assertEqual(span3.status.description, None) span4 = self.tracer.start_span("span4") span4.set_status(StatusCode.ERROR, "span4 desc") self.assertEqual(span4.status.status_code, StatusCode.ERROR) self.assertEqual(span4.status.description, "span4 desc") span5 = self.tracer.start_span("span5") with self.assertLogs(level=WARNING): span5.set_status( Status(status_code=StatusCode.ERROR, description="desc"), description="ignored", ) self.assertEqual(span5.status.status_code, StatusCode.ERROR) self.assertEqual(span5.status.description, "desc") def test_ended_span(self): """Events, attributes are not allowed after span is ended""" root = self.tracer.start_span("root") # everything should be empty at the beginning self.assertEqual(len(root.attributes), 0) self.assertEqual(len(root.events), 0) self.assertEqual(len(root.links), 0) # call end first time root.end() end_time0 = root.end_time # call it a second time with self.assertLogs(level=WARNING): root.end() # end time shouldn't be changed self.assertEqual(end_time0, root.end_time) with self.assertLogs(level=WARNING): root.set_attribute("http.request.method", "GET") self.assertEqual(len(root.attributes), 0) with self.assertLogs(level=WARNING): root.add_event("event1") self.assertEqual(len(root.events), 0) with self.assertLogs(level=WARNING): root.update_name("xxx") self.assertEqual(root.name, "root") new_status = trace_api.status.Status(trace_api.StatusCode.ERROR, "Test description") with self.assertLogs(level=WARNING): root.set_status(new_status) self.assertEqual(root.status.status_code, trace_api.StatusCode.UNSET) def test_error_status(self): def error_status_test(context): with self.assertRaises(AssertionError): with context as root: raise AssertionError("unknown") self.assertIs(root.status.status_code, StatusCode.ERROR) self.assertEqual(root.status.description, "AssertionError: unknown") error_status_test(trace.TracerProvider().get_tracer(__name__).start_span("root")) error_status_test(trace.TracerProvider().get_tracer(__name__).start_as_current_span("root")) def test_status_cannot_override_ok(self): def error_status_test(context): with self.assertRaises(AssertionError): with context as root: root.set_status(trace_api.status.Status(StatusCode.OK)) raise AssertionError("unknown") self.assertIs(root.status.status_code, StatusCode.OK) self.assertIsNone(root.status.description) error_status_test(trace.TracerProvider().get_tracer(__name__).start_span("root")) error_status_test(trace.TracerProvider().get_tracer(__name__).start_as_current_span("root")) def test_status_cannot_set_unset(self): def unset_status_test(context): with self.assertRaises(AssertionError): with context as root: raise AssertionError("unknown") root.set_status(trace_api.status.Status(StatusCode.UNSET)) self.assertIs(root.status.status_code, StatusCode.ERROR) self.assertEqual(root.status.description, "AssertionError: unknown") with self.assertLogs(level=WARNING): unset_status_test(trace.TracerProvider().get_tracer(__name__).start_span("root")) with self.assertLogs(level=WARNING): unset_status_test(trace.TracerProvider().get_tracer(__name__).start_as_current_span("root")) def test_last_status_wins(self): def error_status_test(context): with self.assertRaises(AssertionError): with context as root: raise AssertionError("unknown") root.set_status(trace_api.status.Status(StatusCode.OK)) self.assertIs(root.status.status_code, StatusCode.OK) self.assertIsNone(root.status.description) error_status_test(trace.TracerProvider().get_tracer(__name__).start_span("root")) error_status_test(trace.TracerProvider().get_tracer(__name__).start_as_current_span("root")) def test_record_exception_fqn(self): span = trace._Span("name", mock.Mock(spec=trace_api.SpanContext)) exception = DummyError("error") exception_type = "tests.trace.test_trace.DummyError" span.record_exception(exception) exception_event = span.events[0] self.assertEqual("exception", exception_event.name) self.assertEqual("error", exception_event.attributes["exception.message"]) self.assertEqual( exception_type, exception_event.attributes["exception.type"], ) self.assertIn( "DummyError: error", exception_event.attributes["exception.stacktrace"], ) def test_record_exception(self): span = trace._Span("name", mock.Mock(spec=trace_api.SpanContext)) try: raise ValueError("invalid") except ValueError as err: span.record_exception(err) exception_event = span.events[0] self.assertEqual("exception", exception_event.name) self.assertEqual("invalid", exception_event.attributes["exception.message"]) self.assertEqual("ValueError", exception_event.attributes["exception.type"]) self.assertIn( "ValueError: invalid", exception_event.attributes["exception.stacktrace"], ) def test_record_exception_with_attributes(self): span = trace._Span("name", mock.Mock(spec=trace_api.SpanContext)) try: raise RuntimeError("error") except RuntimeError as err: attributes = {"has_additional_attributes": True} span.record_exception(err, attributes) exception_event = span.events[0] self.assertEqual("exception", exception_event.name) self.assertEqual("error", exception_event.attributes["exception.message"]) self.assertEqual("RuntimeError", exception_event.attributes["exception.type"]) self.assertEqual("False", exception_event.attributes["exception.escaped"]) self.assertIn( "RuntimeError: error", exception_event.attributes["exception.stacktrace"], ) self.assertIn("has_additional_attributes", exception_event.attributes) self.assertEqual(True, exception_event.attributes["has_additional_attributes"]) def test_record_exception_escaped(self): span = trace._Span("name", mock.Mock(spec=trace_api.SpanContext)) try: raise RuntimeError("error") except RuntimeError as err: span.record_exception(exception=err, escaped=True) exception_event = span.events[0] self.assertEqual("exception", exception_event.name) self.assertEqual("error", exception_event.attributes["exception.message"]) self.assertEqual("RuntimeError", exception_event.attributes["exception.type"]) self.assertIn( "RuntimeError: error", exception_event.attributes["exception.stacktrace"], ) self.assertEqual("True", exception_event.attributes["exception.escaped"]) def test_record_exception_with_timestamp(self): span = trace._Span("name", mock.Mock(spec=trace_api.SpanContext)) try: raise RuntimeError("error") except RuntimeError as err: timestamp = 1604238587112021089 span.record_exception(err, timestamp=timestamp) exception_event = span.events[0] self.assertEqual("exception", exception_event.name) self.assertEqual("error", exception_event.attributes["exception.message"]) self.assertEqual("RuntimeError", exception_event.attributes["exception.type"]) self.assertIn( "RuntimeError: error", exception_event.attributes["exception.stacktrace"], ) self.assertEqual(1604238587112021089, exception_event.timestamp) def test_record_exception_with_attributes_and_timestamp(self): span = trace._Span("name", mock.Mock(spec=trace_api.SpanContext)) try: raise RuntimeError("error") except RuntimeError as err: attributes = {"has_additional_attributes": True} timestamp = 1604238587112021089 span.record_exception(err, attributes, timestamp) exception_event = span.events[0] self.assertEqual("exception", exception_event.name) self.assertEqual("error", exception_event.attributes["exception.message"]) self.assertEqual("RuntimeError", exception_event.attributes["exception.type"]) self.assertIn( "RuntimeError: error", exception_event.attributes["exception.stacktrace"], ) self.assertIn("has_additional_attributes", exception_event.attributes) self.assertEqual(True, exception_event.attributes["has_additional_attributes"]) self.assertEqual(1604238587112021089, exception_event.timestamp) def test_record_exception_context_manager(self): span = None try: with self.tracer.start_as_current_span("span") as span: raise RuntimeError("example error") except RuntimeError: pass finally: self.assertEqual(len(span.events), 1) event = span.events[0] self.assertEqual("exception", event.name) self.assertEqual("RuntimeError", event.attributes["exception.type"]) self.assertEqual("example error", event.attributes["exception.message"]) stacktrace = """in test_record_exception_context_manager raise RuntimeError("example error") RuntimeError: example error""" self.assertIn(stacktrace, event.attributes["exception.stacktrace"]) try: with self.tracer.start_as_current_span("span", record_exception=False) as span: raise RuntimeError("example error") except RuntimeError: pass finally: self.assertEqual(len(span.events), 0) def test_record_exception_out_of_scope(self): span = trace._Span("name", mock.Mock(spec=trace_api.SpanContext)) out_of_scope_exception = ValueError("invalid") span.record_exception(out_of_scope_exception) exception_event = span.events[0] self.assertEqual("exception", exception_event.name) self.assertEqual("invalid", exception_event.attributes["exception.message"]) self.assertEqual("ValueError", exception_event.attributes["exception.type"]) self.assertIn( "ValueError: invalid", exception_event.attributes["exception.stacktrace"], ) def span_event_start_fmt(span_processor_name, span_name): return span_processor_name + ":" + span_name + ":start" def span_event_ending_fmt(span_processor_name, span_name): return span_processor_name + ":" + span_name + ":ending" def span_event_end_fmt(span_processor_name, span_name): return span_processor_name + ":" + span_name + ":end" class MySpanProcessor(trace.SpanProcessor): def __init__(self, name, span_list): self.name = name self.span_list = span_list def on_start(self, span: "trace.Span", parent_context: Context | None = None) -> None: self.span_list.append(span_event_start_fmt(self.name, span.name)) def _on_ending(self, span: "trace.ReadableSpan") -> None: self.span_list.append(span_event_ending_fmt(self.name, span.name)) def on_end(self, span: "trace.ReadableSpan") -> None: self.span_list.append(span_event_end_fmt(self.name, span.name)) class TestSpanProcessor(unittest.TestCase): # pylint: disable=too-many-statements def test_span_processor(self): tracer_provider = trace.TracerProvider() tracer = tracer_provider.get_tracer(__name__) spans_calls_list = [] # filled by MySpanProcessor expected_list = [] # filled by hand # Span processors are created but not added to the tracer yet sp1 = MySpanProcessor("SP1", spans_calls_list) sp2 = MySpanProcessor("SP2", spans_calls_list) with tracer.start_as_current_span("foo"): with tracer.start_as_current_span("bar"): with tracer.start_as_current_span("baz"): pass # at this point lists must be empty self.assertEqual(len(spans_calls_list), 0) # add single span processor tracer_provider.add_span_processor(sp1) with tracer.start_as_current_span("foo"): expected_list.append(span_event_start_fmt("SP1", "foo")) with tracer.start_as_current_span("bar"): expected_list.append(span_event_start_fmt("SP1", "bar")) with tracer.start_as_current_span("baz"): expected_list.append(span_event_start_fmt("SP1", "baz")) expected_list.append(span_event_ending_fmt("SP1", "baz")) expected_list.append(span_event_end_fmt("SP1", "baz")) expected_list.append(span_event_ending_fmt("SP1", "bar")) expected_list.append(span_event_end_fmt("SP1", "bar")) expected_list.append(span_event_ending_fmt("SP1", "foo")) expected_list.append(span_event_end_fmt("SP1", "foo")) self.assertListEqual(spans_calls_list, expected_list) spans_calls_list.clear() expected_list.clear() # go for multiple span processors tracer_provider.add_span_processor(sp2) with tracer.start_as_current_span("foo"): expected_list.append(span_event_start_fmt("SP1", "foo")) expected_list.append(span_event_start_fmt("SP2", "foo")) with tracer.start_as_current_span("bar"): expected_list.append(span_event_start_fmt("SP1", "bar")) expected_list.append(span_event_start_fmt("SP2", "bar")) with tracer.start_as_current_span("baz"): expected_list.append(span_event_start_fmt("SP1", "baz")) expected_list.append(span_event_start_fmt("SP2", "baz")) expected_list.append(span_event_ending_fmt("SP1", "baz")) expected_list.append(span_event_ending_fmt("SP2", "baz")) expected_list.append(span_event_end_fmt("SP1", "baz")) expected_list.append(span_event_end_fmt("SP2", "baz")) expected_list.append(span_event_ending_fmt("SP1", "bar")) expected_list.append(span_event_ending_fmt("SP2", "bar")) expected_list.append(span_event_end_fmt("SP1", "bar")) expected_list.append(span_event_end_fmt("SP2", "bar")) expected_list.append(span_event_ending_fmt("SP1", "foo")) expected_list.append(span_event_ending_fmt("SP2", "foo")) expected_list.append(span_event_end_fmt("SP1", "foo")) expected_list.append(span_event_end_fmt("SP2", "foo")) # compare if two lists are the same self.assertListEqual(spans_calls_list, expected_list) def test_add_span_processor_after_span_creation(self): tracer_provider = trace.TracerProvider() tracer = tracer_provider.get_tracer(__name__) spans_calls_list = [] # filled by MySpanProcessor expected_list = [] # filled by hand # Span processors are created but not added to the tracer yet sp = MySpanProcessor("SP1", spans_calls_list) with tracer.start_as_current_span("foo"): with tracer.start_as_current_span("bar"): with tracer.start_as_current_span("baz"): # add span processor after spans have been created tracer_provider.add_span_processor(sp) expected_list.append(span_event_ending_fmt("SP1", "baz")) expected_list.append(span_event_end_fmt("SP1", "baz")) expected_list.append(span_event_ending_fmt("SP1", "bar")) expected_list.append(span_event_end_fmt("SP1", "bar")) expected_list.append(span_event_ending_fmt("SP1", "foo")) expected_list.append(span_event_end_fmt("SP1", "foo")) self.assertListEqual(spans_calls_list, expected_list) def test_to_json(self): context = trace_api.SpanContext( trace_id=0x000000000000000000000000DEADBEEF, span_id=0x00000000DEADBEF0, is_remote=False, trace_flags=trace_api.TraceFlags(trace_api.TraceFlags.SAMPLED), ) parent = trace._Span("parent-name", context, resource=Resource({})) span = trace._Span("span-name", context, resource=Resource({}), parent=parent.context) self.assertEqual( span.to_json(), """{ "name": "span-name", "context": { "trace_id": "0x000000000000000000000000deadbeef", "span_id": "0x00000000deadbef0", "trace_state": "[]" }, "kind": "SpanKind.INTERNAL", "parent_id": "0x00000000deadbef0", "start_time": null, "end_time": null, "status": { "status_code": "UNSET" }, "attributes": {}, "events": [], "links": [], "resource": { "attributes": {}, "schema_url": "" } }""", ) self.assertEqual( span.to_json(indent=None), '{"name": "span-name", "context": {"trace_id": "0x000000000000000000000000deadbeef", "span_id": "0x00000000deadbef0", "trace_state": "[]"}, "kind": "SpanKind.INTERNAL", "parent_id": "0x00000000deadbef0", "start_time": null, "end_time": null, "status": {"status_code": "UNSET"}, "attributes": {}, "events": [], "links": [], "resource": {"attributes": {}, "schema_url": ""}}', ) def test_attributes_to_json(self): context = trace_api.SpanContext( trace_id=0x000000000000000000000000DEADBEEF, span_id=0x00000000DEADBEF0, is_remote=False, trace_flags=trace_api.TraceFlags(trace_api.TraceFlags.SAMPLED), ) span = trace._Span("span-name", context, resource=Resource({})) span.set_attribute("key", "value") span.add_event("event", {"key2": "value2"}, 123) date_str = ns_to_iso_str(123) self.assertEqual( span.to_json(indent=None), '{"name": "span-name", "context": {"trace_id": "0x000000000000000000000000deadbeef", "span_id": "0x00000000deadbef0", "trace_state": "[]"}, "kind": "SpanKind.INTERNAL", "parent_id": null, "start_time": null, "end_time": null, "status": {"status_code": "UNSET"}, "attributes": {"key": "value"}, "events": [{"name": "event", "timestamp": "' + date_str + '", "attributes": {"key2": "value2"}}], "links": [], "resource": {"attributes": {}, "schema_url": ""}}', ) class TestSpanLimits(unittest.TestCase): # pylint: disable=protected-access long_val = "v" * 1000 def _assert_attr_length(self, attr_val, max_len): if isinstance(attr_val, str): expected = self.long_val if max_len is not None: expected = expected[:max_len] self.assertEqual(attr_val, expected) def test_limits_defaults(self): limits = trace.SpanLimits() self.assertEqual( limits.max_attributes, trace._DEFAULT_OTEL_ATTRIBUTE_COUNT_LIMIT, ) self.assertEqual( limits.max_span_attributes, trace._DEFAULT_OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT, ) self.assertEqual( limits.max_event_attributes, trace._DEFAULT_OTEL_EVENT_ATTRIBUTE_COUNT_LIMIT, ) self.assertEqual( limits.max_link_attributes, trace._DEFAULT_OTEL_LINK_ATTRIBUTE_COUNT_LIMIT, ) self.assertEqual(limits.max_events, trace._DEFAULT_OTEL_SPAN_EVENT_COUNT_LIMIT) self.assertEqual(limits.max_links, trace._DEFAULT_OTEL_SPAN_LINK_COUNT_LIMIT) self.assertIsNone(limits.max_attribute_length) self.assertIsNone(limits.max_span_attribute_length) def test_limits_attribute_length_limits_code(self): # global limit unset while span limit is set limits = trace.SpanLimits(max_span_attribute_length=22) self.assertIsNone(limits.max_attribute_length) self.assertEqual(limits.max_span_attribute_length, 22) # span limit falls back to global limit when no value is provided limits = trace.SpanLimits(max_attribute_length=22) self.assertEqual(limits.max_attribute_length, 22) self.assertEqual(limits.max_span_attribute_length, 22) # global and span limits set to different values limits = trace.SpanLimits(max_attribute_length=22, max_span_attribute_length=33) self.assertEqual(limits.max_attribute_length, 22) self.assertEqual(limits.max_span_attribute_length, 33) def test_limits_values_code(self): ( max_attributes, max_span_attributes, max_link_attributes, max_event_attributes, max_events, max_links, max_attr_length, max_span_attr_length, ) = ( randint(0, 10000), randint(0, 10000), randint(0, 10000), randint(0, 10000), randint(0, 10000), randint(0, 10000), randint(0, 10000), randint(0, 10000), ) limits = trace.SpanLimits( max_events=max_events, max_links=max_links, max_attributes=max_attributes, max_span_attributes=max_span_attributes, max_event_attributes=max_event_attributes, max_link_attributes=max_link_attributes, max_attribute_length=max_attr_length, max_span_attribute_length=max_span_attr_length, ) self.assertEqual(limits.max_events, max_events) self.assertEqual(limits.max_links, max_links) self.assertEqual(limits.max_attributes, max_attributes) self.assertEqual(limits.max_span_attributes, max_span_attributes) self.assertEqual(limits.max_event_attributes, max_event_attributes) self.assertEqual(limits.max_link_attributes, max_link_attributes) self.assertEqual(limits.max_attribute_length, max_attr_length) self.assertEqual(limits.max_span_attribute_length, max_span_attr_length) def test_limits_values_env(self): ( max_attributes, max_span_attributes, max_link_attributes, max_event_attributes, max_events, max_links, max_attr_length, max_span_attr_length, ) = ( randint(0, 10000), randint(0, 10000), randint(0, 10000), randint(0, 10000), randint(0, 10000), randint(0, 10000), randint(0, 10000), randint(0, 10000), ) with mock.patch.dict( "os.environ", { OTEL_ATTRIBUTE_COUNT_LIMIT: str(max_attributes), OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT: str(max_span_attributes), OTEL_EVENT_ATTRIBUTE_COUNT_LIMIT: str(max_event_attributes), OTEL_LINK_ATTRIBUTE_COUNT_LIMIT: str(max_link_attributes), OTEL_SPAN_EVENT_COUNT_LIMIT: str(max_events), OTEL_SPAN_LINK_COUNT_LIMIT: str(max_links), OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT: str(max_attr_length), OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT: str(max_span_attr_length), }, ): limits = trace.SpanLimits() self.assertEqual(limits.max_events, max_events) self.assertEqual(limits.max_links, max_links) self.assertEqual(limits.max_attributes, max_attributes) self.assertEqual(limits.max_span_attributes, max_span_attributes) self.assertEqual(limits.max_event_attributes, max_event_attributes) self.assertEqual(limits.max_link_attributes, max_link_attributes) self.assertEqual(limits.max_attribute_length, max_attr_length) self.assertEqual(limits.max_span_attribute_length, max_span_attr_length) @mock.patch.dict( "os.environ", { OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT: "13", OTEL_SPAN_EVENT_COUNT_LIMIT: "7", OTEL_SPAN_LINK_COUNT_LIMIT: "4", OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT: "11", OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT: "15", }, ) def test_span_limits_env(self): self._test_span_limits( new_tracer(), max_attrs=13, max_events=7, max_links=4, max_attr_len=11, max_span_attr_len=15, ) @mock.patch.dict( "os.environ", { OTEL_ATTRIBUTE_COUNT_LIMIT: "13", OTEL_SPAN_EVENT_COUNT_LIMIT: "7", OTEL_SPAN_LINK_COUNT_LIMIT: "4", OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT: "11", }, ) def test_span_limits_global_env(self): self._test_span_limits( new_tracer(), max_attrs=13, max_events=7, max_links=4, max_attr_len=11, max_span_attr_len=11, ) @mock.patch.dict( "os.environ", { OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT: "10", OTEL_SPAN_EVENT_COUNT_LIMIT: "20", OTEL_SPAN_LINK_COUNT_LIMIT: "30", OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT: "40", OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT: "50", }, ) def test_span_limits_default_to_env(self): self._test_span_limits( new_tracer( span_limits=trace.SpanLimits( max_attributes=None, max_events=None, max_links=None, max_attribute_length=None, max_span_attribute_length=None, ) ), max_attrs=10, max_events=20, max_links=30, max_attr_len=40, max_span_attr_len=50, ) def test_span_limits_code(self): self._test_span_limits( new_tracer( span_limits=trace.SpanLimits( max_attributes=11, max_events=15, max_links=13, max_attribute_length=9, max_span_attribute_length=25, ) ), max_attrs=11, max_events=15, max_links=13, max_attr_len=9, max_span_attr_len=25, ) @mock.patch.dict( "os.environ", { OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT: "", OTEL_SPAN_EVENT_COUNT_LIMIT: "", OTEL_SPAN_LINK_COUNT_LIMIT: "", OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT: "", }, ) def test_span_no_limits_env(self): self._test_span_no_limits(new_tracer()) def test_span_no_limits_code(self): self._test_span_no_limits( new_tracer( span_limits=trace.SpanLimits( max_span_attributes=trace.SpanLimits.UNSET, max_links=trace.SpanLimits.UNSET, max_events=trace.SpanLimits.UNSET, max_attribute_length=trace.SpanLimits.UNSET, ) ) ) def test_span_zero_global_limit(self): self._test_span_limits( new_tracer( span_limits=trace.SpanLimits( max_attributes=0, max_events=0, max_links=0, ) ), 0, 0, 0, 0, 0, ) def test_span_zero_global_nonzero_model(self): self._test_span_limits( new_tracer( span_limits=trace.SpanLimits( max_attributes=0, max_events=0, max_links=0, max_span_attributes=15, max_span_attribute_length=25, ) ), 15, 0, 0, 0, 25, ) def test_span_zero_global_unset_model(self): self._test_span_no_limits( new_tracer( span_limits=trace.SpanLimits( max_attributes=0, max_span_attributes=trace.SpanLimits.UNSET, max_links=trace.SpanLimits.UNSET, max_events=trace.SpanLimits.UNSET, max_attribute_length=trace.SpanLimits.UNSET, ) ) ) def test_dropped_attributes(self): span = get_span_with_dropped_attributes_events_links() self.assertEqual(1, span.dropped_links) self.assertEqual(2, span.dropped_attributes) self.assertEqual(3, span.dropped_events) self.assertEqual(2, span.events[0].dropped_attributes) self.assertEqual(2, span.links[0].dropped_attributes) def _test_span_limits( self, tracer, max_attrs, max_events, max_links, max_attr_len, max_span_attr_len, ): id_generator = RandomIdGenerator() some_links = [ trace_api.Link( trace_api.SpanContext( trace_id=id_generator.generate_trace_id(), span_id=id_generator.generate_span_id(), is_remote=False, ), attributes={"k": self.long_val}, ) for _ in range(100) ] some_attrs = {f"init_attribute_{idx}": self.long_val for idx in range(100)} with tracer.start_as_current_span("root", links=some_links, attributes=some_attrs) as root: self.assertEqual(len(root.links), max_links) self.assertEqual(len(root.attributes), max_attrs) for idx in range(100): root.set_attribute(f"my_str_attribute_{idx}", self.long_val) root.set_attribute(f"my_byte_attribute_{idx}", self.long_val.encode()) root.set_attribute(f"my_int_attribute_{idx}", self.long_val.encode()) root.add_event(f"my_event_{idx}", attributes={"k": self.long_val}) self.assertEqual(len(root.attributes), max_attrs) self.assertEqual(len(root.events), max_events) for link in root.links: for attr_val in link.attributes.values(): self._assert_attr_length(attr_val, max_attr_len) for event in root.events: for attr_val in event.attributes.values(): self._assert_attr_length(attr_val, max_attr_len) for attr_val in root.attributes.values(): self._assert_attr_length(attr_val, max_span_attr_len) def _test_span_no_limits(self, tracer): num_links = int(trace._DEFAULT_OTEL_SPAN_LINK_COUNT_LIMIT) + randint(1, 100) id_generator = RandomIdGenerator() some_links = [ trace_api.Link( trace_api.SpanContext( trace_id=id_generator.generate_trace_id(), span_id=id_generator.generate_span_id(), is_remote=False, ) ) for _ in range(num_links) ] with tracer.start_as_current_span("root", links=some_links) as root: self.assertEqual(len(root.links), num_links) num_events = int(trace._DEFAULT_OTEL_SPAN_EVENT_COUNT_LIMIT) + randint(1, 100) with tracer.start_as_current_span("root") as root: for idx in range(num_events): root.add_event(f"my_event_{idx}", attributes={"k": self.long_val}) self.assertEqual(len(root.events), num_events) num_attributes = int(trace._DEFAULT_OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT) + randint(1, 100) with tracer.start_as_current_span("root") as root: for idx in range(num_attributes): root.set_attribute(f"my_attribute_{idx}", self.long_val) self.assertEqual(len(root.attributes), num_attributes) for attr_val in root.attributes.values(): self.assertEqual(attr_val, self.long_val) def test_invalid_env_vars_raise(self): env_vars = [ OTEL_SPAN_EVENT_COUNT_LIMIT, OTEL_SPAN_LINK_COUNT_LIMIT, OTEL_ATTRIBUTE_COUNT_LIMIT, OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT, OTEL_EVENT_ATTRIBUTE_COUNT_LIMIT, OTEL_LINK_ATTRIBUTE_COUNT_LIMIT, OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT, ] bad_values = ["bad", "-1"] test_cases = {env_var: bad_value for env_var in env_vars for bad_value in bad_values} for env_var, bad_value in test_cases.items(): with self.subTest(f"Testing {env_var}={bad_value}"): with ( self.assertRaises(ValueError) as error, patch.dict("os.environ", {env_var: bad_value}, clear=True), ): trace.SpanLimits() expected_msg = f"{env_var} must be a non-negative integer but got {bad_value}" self.assertEqual( expected_msg, str(error.exception), f"Unexpected error message for {env_var}={bad_value}", ) class TestTraceFlags(unittest.TestCase): def test_constant_default(self): self.assertEqual(trace_api.TraceFlags.DEFAULT, 0) def test_constant_sampled(self): self.assertEqual(trace_api.TraceFlags.SAMPLED, 1) def test_constant_random_trace_id(self): self.assertEqual(trace_api.TraceFlags.RANDOM_TRACE_ID, 2) def test_get_default(self): self.assertEqual(trace_api.TraceFlags.get_default(), trace_api.TraceFlags.DEFAULT) def test_sampled_true(self): self.assertTrue(trace_api.TraceFlags(0xF1).sampled) def test_sampled_false(self): self.assertFalse(trace_api.TraceFlags(0xF0).sampled) def test_random_trace_id_true(self): self.assertTrue(trace_api.TraceFlags(0xF2).random_trace_id) self.assertTrue(trace_api.TraceFlags(0xF3).random_trace_id) def test_random_trace_id_false(self): self.assertFalse(trace_api.TraceFlags(0xF0).random_trace_id) self.assertFalse(trace_api.TraceFlags(0xF1).random_trace_id) def test_constant_default_trace_options(self): self.assertEqual(trace_api.DEFAULT_TRACE_OPTIONS, trace_api.TraceFlags.DEFAULT) class TestParentChildSpanException(unittest.TestCase): def test_parent_child_span_exception(self): """ Tests that a parent span has its status set to ERROR when a child span raises an exception even when the child span has its ``record_exception`` and ``set_status_on_exception`` attributes set to ``False``. """ set_tracer_provider(TracerProvider()) tracer = get_tracer(__name__) exception = Exception("exception") exception_type = exception.__class__.__name__ exception_message = exception.args[0] child_span = None try: with ( tracer.start_as_current_span( "parent", ) as parent_span, tracer.start_as_current_span( "child", record_exception=False, set_status_on_exception=False, ) as child_span, ): raise exception except Exception: # pylint: disable=broad-exception-caught pass self.assertIsNotNone(child_span) self.assertTrue(child_span.status.is_ok) self.assertIsNone(child_span.status.description) self.assertTupleEqual(child_span.events, ()) self.assertFalse(parent_span.status.is_ok) self.assertEqual( parent_span.status.description, f"{exception_type}: {exception_message}", ) self.assertEqual(parent_span.events[0].attributes["exception.type"], exception_type) self.assertEqual( parent_span.events[0].attributes["exception.message"], exception_message, ) def test_child_parent_span_exception(self): """ Tests that a child span does not have its status set to ERROR when a parent span raises an exception and the parent span has its ``record_exception`` and ``set_status_on_exception`` attributes set to ``False``. """ set_tracer_provider(TracerProvider()) tracer = get_tracer(__name__) exception = Exception("exception") child_span = None try: with tracer.start_as_current_span( "parent", record_exception=False, set_status_on_exception=False, ) as parent_span: with tracer.start_as_current_span( "child", ) as child_span: pass raise exception except Exception: # pylint: disable=broad-exception-caught pass self.assertIsNotNone(child_span) self.assertTrue(child_span.status.is_ok) self.assertIsNone(child_span.status.description) self.assertTupleEqual(child_span.events, ()) self.assertTrue(parent_span.status.is_ok) self.assertIsNone(parent_span.status.description) self.assertTupleEqual(parent_span.events, ()) class TestTracerConfig(unittest.TestCase): def test_default(self): self.assertEqual( _TracerConfig.default(), _TracerConfig(is_enabled=True), ) def test_equality(self): config = _TracerConfig(is_enabled=True) same_config = _TracerConfig(is_enabled=True) other_config = _TracerConfig(is_enabled=False) self.assertEqual(config, same_config) self.assertNotEqual(config, other_config) self.assertNotEqual(config, "string") # pylint: disable=protected-access class TestTracerProvider(unittest.TestCase): @patch("opentelemetry.sdk.trace.sampling._get_from_env_or_default") @patch.object(Resource, "create") def test_tracer_provider_init_default(self, resource_patch, sample_patch): tracer_provider = trace.TracerProvider() self.assertTrue(isinstance(tracer_provider.id_generator, RandomIdGenerator)) resource_patch.assert_called_once() self.assertIsNotNone(tracer_provider._resource) sample_patch.assert_called_once() self.assertIsNotNone(tracer_provider._span_limits) self.assertIsNotNone(tracer_provider._atexit_handler) def test_default_tracer_configurator(self): # pylint: disable=protected-access tracer_provider = trace.TracerProvider() tracer = tracer_provider.get_tracer( "module_name", "library_version", "schema_url", {}, ) other_tracer = tracer_provider.get_tracer( "other_module_name", "library_version", "schema_url", {}, ) self.assertEqual(tracer._instrumentation_scope.name, "module_name") self.assertEqual(other_tracer._instrumentation_scope.name, "other_module_name") self.assertEqual(tracer._is_enabled(), True) self.assertEqual(other_tracer._is_enabled(), True) def test_set_tracer_configurator_sets_default_tracer_config_if_configurator_raises( self, ): def raising_tracer_configurator(tracer_scope): raise ValueError() tracer_provider = trace.TracerProvider() tracer = tracer_provider.get_tracer( "module_name", "library_version", ) tracer_provider._set_tracer_configurator(tracer_configurator=raising_tracer_configurator) # pylint: disable=protected-access self.assertEqual( dataclasses.asdict(tracer._tracer_config), dataclasses.asdict(_TracerConfig.default()), ) def test_rule_based_tracer_configurator(self): # pylint: disable=protected-access rules = [ ( _scope_name_matches_glob(glob_pattern="module_name"), _TracerConfig(is_enabled=True), ), ( _scope_name_matches_glob(glob_pattern="other_module_name"), _TracerConfig(is_enabled=False), ), ] configurator = _RuleBasedTracerConfigurator(rules=rules, default_config=_TracerConfig(is_enabled=True)) tracer_provider = trace.TracerProvider() tracer = tracer_provider.get_tracer( "module_name", "library_version", "schema_url", {}, ) other_tracer = tracer_provider.get_tracer( "other_module_name", "library_version", "schema_url", {}, ) self.assertEqual(tracer._instrumentation_scope.name, "module_name") self.assertEqual(other_tracer._instrumentation_scope.name, "other_module_name") self.assertEqual(tracer._is_enabled(), True) self.assertEqual(other_tracer._is_enabled(), True) tracer_provider._set_tracer_configurator(tracer_configurator=configurator) self.assertEqual(tracer._is_enabled(), True) self.assertEqual(other_tracer._is_enabled(), False) def test_rule_based_tracer_configurator_default_when_rules_dont_match( self, ): # pylint: disable=protected-access rules = [ ( _scope_name_matches_glob(glob_pattern="module_name"), _TracerConfig(is_enabled=False), ), ] configurator = _RuleBasedTracerConfigurator(rules=rules, default_config=_TracerConfig(is_enabled=True)) tracer_provider = trace.TracerProvider() tracer = tracer_provider.get_tracer( "module_name", "library_version", "schema_url", {}, ) other_tracer = tracer_provider.get_tracer( "other_module_name", "library_version", "schema_url", {}, ) self.assertEqual(tracer._instrumentation_scope.name, "module_name") self.assertEqual(other_tracer._instrumentation_scope.name, "other_module_name") self.assertEqual(tracer._is_enabled(), True) self.assertEqual(other_tracer._is_enabled(), True) tracer_provider._set_tracer_configurator(tracer_configurator=configurator) self.assertEqual(tracer._is_enabled(), False) self.assertEqual(other_tracer._is_enabled(), True) class TestRandomIdGenerator(unittest.TestCase): _TRACE_ID_MAX_VALUE = 2**128 - 1 _SPAN_ID_MAX_VALUE = 2**64 - 1 @patch( "random.getrandbits", side_effect=[trace_api.INVALID_SPAN_ID, 0x00000000DEADBEF0], ) def test_generate_span_id_avoids_invalid(self, mock_getrandbits): generator = RandomIdGenerator() span_id = generator.generate_span_id() self.assertNotEqual(span_id, trace_api.INVALID_SPAN_ID) mock_getrandbits.assert_any_call(64) self.assertEqual(mock_getrandbits.call_count, 2) @patch( "random.getrandbits", side_effect=[ trace_api.INVALID_TRACE_ID, 0x000000000000000000000000DEADBEEF, ], ) def test_generate_trace_id_avoids_invalid(self, mock_getrandbits): generator = RandomIdGenerator() trace_id = generator.generate_trace_id() self.assertNotEqual(trace_id, trace_api.INVALID_TRACE_ID) mock_getrandbits.assert_any_call(128) self.assertEqual(mock_getrandbits.call_count, 2) def test_is_trace_id_random_returns_true(self): generator = RandomIdGenerator() self.assertTrue(generator.is_trace_id_random()) python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/000077500000000000000000000000001527070070700250635ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/.pylintrc000066400000000000000000000364271527070070700267440ustar00rootroot00000000000000[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= # Add list of files or directories to be excluded. They should be base names, not # paths. ignore=CVS,gen,proto # Add files or directories matching the regex patterns to be excluded. The # regex matches against base names, not paths. ignore-patterns= # 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= # When enabled, pylint would attempt to guess common misconfiguration and emit # user-friendly hints instead of false-positive error messages. suggestion-mode=yes # 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, too-many-positional-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 redefined-builtin, cyclic-import, too-many-lines, # 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, _agnosticcontextmanager # 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=zipkin_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=120 # 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 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. overgeneral-exceptions=builtins.Exception python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/LICENSE000066400000000000000000000261351527070070700260770ustar00rootroot00000000000000 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. python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/README.rst000066400000000000000000000024451527070070700265570ustar00rootroot00000000000000OpenTelemetry Semantic Conventions ================================== |pypi| .. |pypi| image:: https://badge.fury.io/py/opentelemetry-semantic-conventions.svg :target: https://pypi.org/project/opentelemetry-semantic-conventions/ This library contains generated code for the semantic conventions defined by the OpenTelemetry specification. Installation ------------ :: pip install opentelemetry-semantic-conventions Code Generation --------------- These files were generated automatically from code in semconv_. To regenerate the code, run ``../scripts/semconv/generate.sh``. To build against a new release or specific commit of opentelemetry-specification_, update the ``SPEC_VERSION`` variable in ``../scripts/semconv/generate.sh``. Then run the script and commit the changes. .. _opentelemetry-specification: https://github.com/open-telemetry/opentelemetry-specification .. _semconv: https://github.com/open-telemetry/opentelemetry-python/tree/main/scripts/semconv References ---------- * `OpenTelemetry Project `_ * `OpenTelemetry Semantic Conventions Definitions `_ * `generate.sh script `_ python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/pyproject.toml000066400000000000000000000024421527070070700300010ustar00rootroot00000000000000[build-system] requires = ["hatchling"] build-backend = "hatchling.build" [project] name = "opentelemetry-semantic-conventions" dynamic = ["version"] description = "OpenTelemetry Semantic Conventions" 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", "Framework :: OpenTelemetry", "Intended Audience :: Developers", "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.45.2", "typing-extensions >= 4.5.0", ] [project.urls] Homepage = "https://github.com/open-telemetry/opentelemetry-python/tree/main/opentelemetry-semantic-conventions" Repository = "https://github.com/open-telemetry/opentelemetry-python" [tool.hatch.version] path = "src/opentelemetry/semconv/version/__init__.py" [tool.hatch.build.targets.sdist] include = [ "/src", "/tests", ] [tool.hatch.build.targets.wheel] packages = ["src/opentelemetry"] python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/src/000077500000000000000000000000001527070070700256525ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/src/opentelemetry/000077500000000000000000000000001527070070700305465ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/src/opentelemetry/semconv/000077500000000000000000000000001527070070700322205ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/src/opentelemetry/semconv/__init__.py000066400000000000000000000000001527070070700343170ustar00rootroot00000000000000_incubating/000077500000000000000000000000001527070070700344235ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/src/opentelemetry/semconvattributes/000077500000000000000000000000001527070070700366115ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/src/opentelemetry/semconv/_incubatingapp_attributes.py000066400000000000000000000102261527070070700422120ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/src/opentelemetry/semconv/_incubating/attributes# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from typing import Final APP_BUILD_ID: Final = "app.build_id" """ Unique identifier for a particular build or compilation of the application. """ APP_CRASH_ID: Final = "app.crash.id" """ A unique identifier representing an instance of an end user facing app crash. Note: Its value MAY be meaningful and be used as a reference for telemetry and metadata recorded by the same instrumentation (e.g. it is an ID generated by an external source that captured the crash). It MAY come from a source external to the instrumentation such that it can be used to look up additional data from other sources as well as facilitate deduplication. """ APP_INSTALLATION_ID: Final = "app.installation.id" """ A unique identifier representing the installation of an application on a specific device. Note: Its value SHOULD persist across launches of the same application installation, including through application upgrades. It SHOULD change if the application is uninstalled or if all applications of the vendor are uninstalled. Additionally, users might be able to reset this value (e.g. by clearing application data). If an app is installed multiple times on the same device (e.g. in different accounts on Android), each `app.installation.id` SHOULD have a different value. If multiple OpenTelemetry SDKs are used within the same application, they SHOULD use the same value for `app.installation.id`. Hardware IDs (e.g. serial number, IMEI, MAC address) MUST NOT be used as the `app.installation.id`. For iOS, this value SHOULD be equal to the [vendor identifier](https://developer.apple.com/documentation/uikit/uidevice/identifierforvendor). For Android, examples of `app.installation.id` implementations include: - [Firebase Installation ID](https://firebase.google.com/docs/projects/manage-installations). - A globally unique UUID which is persisted across sessions in your application. - [App set ID](https://developer.android.com/identity/app-set-id). - [`Settings.getString(Settings.Secure.ANDROID_ID)`](https://developer.android.com/reference/android/provider/Settings.Secure#ANDROID_ID). More information about Android identifier best practices can be found in the [Android user data IDs guide](https://developer.android.com/training/articles/user-data-ids). """ APP_JANK_FRAME_COUNT: Final = "app.jank.frame_count" """ A number of frame renders that experienced jank. Note: Depending on platform limitations, the value provided MAY be approximation. """ APP_JANK_PERIOD: Final = "app.jank.period" """ The time period, in seconds, for which this jank is being reported. """ APP_JANK_THRESHOLD: Final = "app.jank.threshold" """ The minimum rendering threshold for this jank, in seconds. """ APP_SCREEN_COORDINATE_X: Final = "app.screen.coordinate.x" """ The x (horizontal) coordinate of a screen coordinate, in screen pixels. """ APP_SCREEN_COORDINATE_Y: Final = "app.screen.coordinate.y" """ The y (vertical) component of a screen coordinate, in screen pixels. """ APP_SCREEN_ID: Final = "app.screen.id" """ An identifier that uniquely differentiates this screen from other screens in the same application. Note: A screen represents only the part of the device display drawn by the app. It typically contains multiple widgets or UI components and is larger in scope than individual widgets. Multiple screens can coexist on the same display simultaneously (e.g., split view on tablets). """ APP_SCREEN_NAME: Final = "app.screen.name" """ The name of an application screen. Note: A screen represents only the part of the device display drawn by the app. It typically contains multiple widgets or UI components and is larger in scope than individual widgets. Multiple screens can coexist on the same display simultaneously (e.g., split view on tablets). """ APP_WIDGET_ID: Final = "app.widget.id" """ An identifier that uniquely differentiates this widget from other widgets in the same application. Note: A widget is an application component, typically an on-screen visual GUI element. """ APP_WIDGET_NAME: Final = "app.widget.name" """ The name of an application widget. Note: A widget is an application component, typically an on-screen visual GUI element. """ artifact_attributes.py000066400000000000000000000050451527070070700432320ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/src/opentelemetry/semconv/_incubating/attributes# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from typing import Final ARTIFACT_ATTESTATION_FILENAME: Final = "artifact.attestation.filename" """ The provenance filename of the built attestation which directly relates to the build artifact filename. This filename SHOULD accompany the artifact at publish time. See the [SLSA Relationship](https://slsa.dev/spec/v1.0/distributing-provenance#relationship-between-artifacts-and-attestations) specification for more information. """ ARTIFACT_ATTESTATION_HASH: Final = "artifact.attestation.hash" """ The full [hash value (see glossary)](https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.186-5.pdf), of the built attestation. Some envelopes in the [software attestation space](https://github.com/in-toto/attestation/tree/main/spec) also refer to this as the **digest**. """ ARTIFACT_ATTESTATION_ID: Final = "artifact.attestation.id" """ The ID of the build [software attestation](https://slsa.dev/attestation-model). """ ARTIFACT_FILENAME: Final = "artifact.filename" """ The human readable filename of the artifact, typically generated during build and release processes. Often includes the package name and version in the filename. Note: This filename can also act as the [Package Name](https://slsa.dev/spec/v1.0/terminology#package-model) in cases where the package ecosystem maps accordingly. Additionally, the artifact [can be published](https://slsa.dev/spec/v1.0/terminology#software-supply-chain) for others, but that is not a guarantee. """ ARTIFACT_HASH: Final = "artifact.hash" """ The full [hash value (see glossary)](https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.186-5.pdf), often found in checksum.txt on a release of the artifact and used to verify package integrity. Note: The specific algorithm used to create the cryptographic hash value is not defined. In situations where an artifact has multiple cryptographic hashes, it is up to the implementer to choose which hash value to set here; this should be the most secure hash algorithm that is suitable for the situation and consistent with the corresponding attestation. The implementer can then provide the other hash values through an additional set of attribute extensions as they deem necessary. """ ARTIFACT_PURL: Final = "artifact.purl" """ The [Package URL](https://github.com/package-url/purl-spec) of the [package artifact](https://slsa.dev/spec/v1.0/terminology#package-model) provides a standard way to identify and locate the packaged artifact. """ ARTIFACT_VERSION: Final = "artifact.version" """ The version of the artifact. """ aws_attributes.py000066400000000000000000000323731527070070700422330ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/src/opentelemetry/semconv/_incubating/attributes# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from enum import Enum from typing import Final AWS_BEDROCK_GUARDRAIL_ID: Final = "aws.bedrock.guardrail.id" """ The unique identifier of the AWS Bedrock Guardrail. A [guardrail](https://docs.aws.amazon.com/bedrock/latest/userguide/guardrails.html) helps safeguard and prevent unwanted behavior from model responses or user messages. """ AWS_BEDROCK_KNOWLEDGE_BASE_ID: Final = "aws.bedrock.knowledge_base.id" """ The unique identifier of the AWS Bedrock Knowledge base. A [knowledge base](https://docs.aws.amazon.com/bedrock/latest/userguide/knowledge-base.html) is a bank of information that can be queried by models to generate more relevant responses and augment prompts. """ AWS_DYNAMODB_ATTRIBUTE_DEFINITIONS: Final = "aws.dynamodb.attribute_definitions" """ The JSON-serialized value of each item in the `AttributeDefinitions` request field. """ AWS_DYNAMODB_ATTRIBUTES_TO_GET: Final = "aws.dynamodb.attributes_to_get" """ The value of the `AttributesToGet` request parameter. """ AWS_DYNAMODB_CONSISTENT_READ: Final = "aws.dynamodb.consistent_read" """ The value of the `ConsistentRead` request parameter. """ AWS_DYNAMODB_CONSUMED_CAPACITY: Final = "aws.dynamodb.consumed_capacity" """ The JSON-serialized value of each item in the `ConsumedCapacity` response field. """ AWS_DYNAMODB_COUNT: Final = "aws.dynamodb.count" """ The value of the `Count` response parameter. """ AWS_DYNAMODB_EXCLUSIVE_START_TABLE: Final = "aws.dynamodb.exclusive_start_table" """ The value of the `ExclusiveStartTableName` request parameter. """ AWS_DYNAMODB_GLOBAL_SECONDARY_INDEX_UPDATES: Final = "aws.dynamodb.global_secondary_index_updates" """ The JSON-serialized value of each item in the `GlobalSecondaryIndexUpdates` request field. """ AWS_DYNAMODB_GLOBAL_SECONDARY_INDEXES: Final = "aws.dynamodb.global_secondary_indexes" """ The JSON-serialized value of each item of the `GlobalSecondaryIndexes` request field. """ AWS_DYNAMODB_INDEX_NAME: Final = "aws.dynamodb.index_name" """ The value of the `IndexName` request parameter. """ AWS_DYNAMODB_ITEM_COLLECTION_METRICS: Final = "aws.dynamodb.item_collection_metrics" """ The JSON-serialized value of the `ItemCollectionMetrics` response field. """ AWS_DYNAMODB_LIMIT: Final = "aws.dynamodb.limit" """ The value of the `Limit` request parameter. """ AWS_DYNAMODB_LOCAL_SECONDARY_INDEXES: Final = "aws.dynamodb.local_secondary_indexes" """ The JSON-serialized value of each item of the `LocalSecondaryIndexes` request field. """ AWS_DYNAMODB_PROJECTION: Final = "aws.dynamodb.projection" """ The value of the `ProjectionExpression` request parameter. """ AWS_DYNAMODB_PROVISIONED_READ_CAPACITY: Final = "aws.dynamodb.provisioned_read_capacity" """ The value of the `ProvisionedThroughput.ReadCapacityUnits` request parameter. """ AWS_DYNAMODB_PROVISIONED_WRITE_CAPACITY: Final = "aws.dynamodb.provisioned_write_capacity" """ The value of the `ProvisionedThroughput.WriteCapacityUnits` request parameter. """ AWS_DYNAMODB_SCAN_FORWARD: Final = "aws.dynamodb.scan_forward" """ The value of the `ScanIndexForward` request parameter. """ AWS_DYNAMODB_SCANNED_COUNT: Final = "aws.dynamodb.scanned_count" """ The value of the `ScannedCount` response parameter. """ AWS_DYNAMODB_SEGMENT: Final = "aws.dynamodb.segment" """ The value of the `Segment` request parameter. """ AWS_DYNAMODB_SELECT: Final = "aws.dynamodb.select" """ The value of the `Select` request parameter. """ AWS_DYNAMODB_TABLE_COUNT: Final = "aws.dynamodb.table_count" """ The number of items in the `TableNames` response parameter. """ AWS_DYNAMODB_TABLE_NAMES: Final = "aws.dynamodb.table_names" """ The keys in the `RequestItems` object field. """ AWS_DYNAMODB_TOTAL_SEGMENTS: Final = "aws.dynamodb.total_segments" """ The value of the `TotalSegments` request parameter. """ AWS_ECS_CLUSTER_ARN: Final = "aws.ecs.cluster.arn" """ The ARN of an [ECS cluster](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/clusters.html). """ AWS_ECS_CONTAINER_ARN: Final = "aws.ecs.container.arn" """ The Amazon Resource Name (ARN) of an [ECS container instance](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/ECS_instances.html). """ AWS_ECS_LAUNCHTYPE: Final = "aws.ecs.launchtype" """ The [launch type](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/launch_types.html) for an ECS task. """ AWS_ECS_TASK_ARN: Final = "aws.ecs.task.arn" """ The ARN of a running [ECS task](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/ecs-account-settings.html#ecs-resource-ids). """ AWS_ECS_TASK_FAMILY: Final = "aws.ecs.task.family" """ The family name of the [ECS task definition](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task_definitions.html) used to create the ECS task. """ AWS_ECS_TASK_ID: Final = "aws.ecs.task.id" """ The ID of a running ECS task. The ID MUST be extracted from `task.arn`. """ AWS_ECS_TASK_REVISION: Final = "aws.ecs.task.revision" """ The revision for the task definition used to create the ECS task. """ AWS_EKS_CLUSTER_ARN: Final = "aws.eks.cluster.arn" """ The ARN of an EKS cluster. """ AWS_EXTENDED_REQUEST_ID: Final = "aws.extended_request_id" """ The AWS extended request ID as returned in the response header `x-amz-id-2`. """ AWS_KINESIS_STREAM_NAME: Final = "aws.kinesis.stream_name" """ The name of the AWS Kinesis [stream](https://docs.aws.amazon.com/streams/latest/dev/introduction.html) the request refers to. Corresponds to the `--stream-name` parameter of the Kinesis [describe-stream](https://docs.aws.amazon.com/cli/latest/reference/kinesis/describe-stream.html) operation. """ AWS_LAMBDA_INVOKED_ARN: Final = "aws.lambda.invoked_arn" """ The full invoked ARN as provided on the `Context` passed to the function (`Lambda-Runtime-Invoked-Function-Arn` header on the `/runtime/invocation/next` applicable). Note: This may be different from `cloud.resource_id` if an alias is involved. """ AWS_LAMBDA_RESOURCE_MAPPING_ID: Final = "aws.lambda.resource_mapping.id" """ The UUID of the [AWS Lambda EvenSource Mapping](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lambda-eventsourcemapping.html). An event source is mapped to a lambda function. It's contents are read by Lambda and used to trigger a function. This isn't available in the lambda execution context or the lambda runtime environment. This is going to be populated by the AWS SDK for each language when that UUID is present. Some of these operations are Create/Delete/Get/List/Update EventSourceMapping. """ AWS_LOG_GROUP_ARNS: Final = "aws.log.group.arns" """ The Amazon Resource Name(s) (ARN) of the AWS log group(s). Note: See the [log group ARN format documentation](https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/iam-access-control-overview-cwl.html#CWL_ARN_Format). """ AWS_LOG_GROUP_NAMES: Final = "aws.log.group.names" """ The name(s) of the AWS log group(s) an application is writing to. Note: Multiple log groups must be supported for cases like multi-container applications, where a single application has sidecar containers, and each write to their own log group. """ AWS_LOG_STREAM_ARNS: Final = "aws.log.stream.arns" """ The ARN(s) of the AWS log stream(s). Note: See the [log stream ARN format documentation](https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/iam-access-control-overview-cwl.html#CWL_ARN_Format). One log group can contain several log streams, so these ARNs necessarily identify both a log group and a log stream. """ AWS_LOG_STREAM_NAMES: Final = "aws.log.stream.names" """ The name(s) of the AWS log stream(s) an application is writing to. """ AWS_REQUEST_ID: Final = "aws.request_id" """ The AWS request ID as returned in the response headers `x-amzn-requestid`, `x-amzn-request-id` or `x-amz-request-id`. """ AWS_S3_BUCKET: Final = "aws.s3.bucket" """ The S3 bucket name the request refers to. Corresponds to the `--bucket` parameter of the [S3 API](https://docs.aws.amazon.com/cli/latest/reference/s3api/index.html) operations. Note: The `bucket` attribute is applicable to all S3 operations that reference a bucket, i.e. that require the bucket name as a mandatory parameter. This applies to almost all S3 operations except `list-buckets`. """ AWS_S3_COPY_SOURCE: Final = "aws.s3.copy_source" """ The source object (in the form `bucket`/`key`) for the copy operation. Note: The `copy_source` attribute applies to S3 copy operations and corresponds to the `--copy-source` parameter of the [copy-object operation within the S3 API](https://docs.aws.amazon.com/cli/latest/reference/s3api/copy-object.html). This applies in particular to the following operations: - [copy-object](https://docs.aws.amazon.com/cli/latest/reference/s3api/copy-object.html) - [upload-part-copy](https://docs.aws.amazon.com/cli/latest/reference/s3api/upload-part-copy.html). """ AWS_S3_DELETE: Final = "aws.s3.delete" """ The delete request container that specifies the objects to be deleted. Note: The `delete` attribute is only applicable to the [delete-object](https://docs.aws.amazon.com/cli/latest/reference/s3api/delete-object.html) operation. The `delete` attribute corresponds to the `--delete` parameter of the [delete-objects operation within the S3 API](https://docs.aws.amazon.com/cli/latest/reference/s3api/delete-objects.html). """ AWS_S3_KEY: Final = "aws.s3.key" """ The S3 object key the request refers to. Corresponds to the `--key` parameter of the [S3 API](https://docs.aws.amazon.com/cli/latest/reference/s3api/index.html) operations. Note: The `key` attribute is applicable to all object-related S3 operations, i.e. that require the object key as a mandatory parameter. This applies in particular to the following operations: - [copy-object](https://docs.aws.amazon.com/cli/latest/reference/s3api/copy-object.html) - [delete-object](https://docs.aws.amazon.com/cli/latest/reference/s3api/delete-object.html) - [get-object](https://docs.aws.amazon.com/cli/latest/reference/s3api/get-object.html) - [head-object](https://docs.aws.amazon.com/cli/latest/reference/s3api/head-object.html) - [put-object](https://docs.aws.amazon.com/cli/latest/reference/s3api/put-object.html) - [restore-object](https://docs.aws.amazon.com/cli/latest/reference/s3api/restore-object.html) - [select-object-content](https://docs.aws.amazon.com/cli/latest/reference/s3api/select-object-content.html) - [abort-multipart-upload](https://docs.aws.amazon.com/cli/latest/reference/s3api/abort-multipart-upload.html) - [complete-multipart-upload](https://docs.aws.amazon.com/cli/latest/reference/s3api/complete-multipart-upload.html) - [create-multipart-upload](https://docs.aws.amazon.com/cli/latest/reference/s3api/create-multipart-upload.html) - [list-parts](https://docs.aws.amazon.com/cli/latest/reference/s3api/list-parts.html) - [upload-part](https://docs.aws.amazon.com/cli/latest/reference/s3api/upload-part.html) - [upload-part-copy](https://docs.aws.amazon.com/cli/latest/reference/s3api/upload-part-copy.html). """ AWS_S3_PART_NUMBER: Final = "aws.s3.part_number" """ The part number of the part being uploaded in a multipart-upload operation. This is a positive integer between 1 and 10,000. Note: The `part_number` attribute is only applicable to the [upload-part](https://docs.aws.amazon.com/cli/latest/reference/s3api/upload-part.html) and [upload-part-copy](https://docs.aws.amazon.com/cli/latest/reference/s3api/upload-part-copy.html) operations. The `part_number` attribute corresponds to the `--part-number` parameter of the [upload-part operation within the S3 API](https://docs.aws.amazon.com/cli/latest/reference/s3api/upload-part.html). """ AWS_S3_UPLOAD_ID: Final = "aws.s3.upload_id" """ Upload ID that identifies the multipart upload. Note: The `upload_id` attribute applies to S3 multipart-upload operations and corresponds to the `--upload-id` parameter of the [S3 API](https://docs.aws.amazon.com/cli/latest/reference/s3api/index.html) multipart operations. This applies in particular to the following operations: - [abort-multipart-upload](https://docs.aws.amazon.com/cli/latest/reference/s3api/abort-multipart-upload.html) - [complete-multipart-upload](https://docs.aws.amazon.com/cli/latest/reference/s3api/complete-multipart-upload.html) - [list-parts](https://docs.aws.amazon.com/cli/latest/reference/s3api/list-parts.html) - [upload-part](https://docs.aws.amazon.com/cli/latest/reference/s3api/upload-part.html) - [upload-part-copy](https://docs.aws.amazon.com/cli/latest/reference/s3api/upload-part-copy.html). """ AWS_SECRETSMANAGER_SECRET_ARN: Final = "aws.secretsmanager.secret.arn" """ The ARN of the Secret stored in the Secrets Manager. """ AWS_SNS_TOPIC_ARN: Final = "aws.sns.topic.arn" """ The ARN of the AWS SNS Topic. An Amazon SNS [topic](https://docs.aws.amazon.com/sns/latest/dg/sns-create-topic.html) is a logical access point that acts as a communication channel. """ AWS_SQS_QUEUE_URL: Final = "aws.sqs.queue.url" """ The URL of the AWS SQS Queue. It's a unique identifier for a queue in Amazon Simple Queue Service (SQS) and is used to access the queue and perform actions on it. """ AWS_STEP_FUNCTIONS_ACTIVITY_ARN: Final = "aws.step_functions.activity.arn" """ The ARN of the AWS Step Functions Activity. """ AWS_STEP_FUNCTIONS_STATE_MACHINE_ARN: Final = "aws.step_functions.state_machine.arn" """ The ARN of the AWS Step Functions State Machine. """ class AwsEcsLaunchtypeValues(Enum): EC2 = "ec2" """Amazon EC2.""" FARGATE = "fargate" """Amazon Fargate.""" az_attributes.py000066400000000000000000000005051527070070700420430ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/src/opentelemetry/semconv/_incubating/attributes# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from typing import Final AZ_NAMESPACE: Final = "az.namespace" """ Deprecated: Replaced by `azure.resource_provider.namespace`. """ AZ_SERVICE_REQUEST_ID: Final = "az.service_request_id" """ Deprecated: Replaced by `azure.service.request.id`. """ azure_attributes.py000066400000000000000000000050011527070070700425530ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/src/opentelemetry/semconv/_incubating/attributes# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from enum import Enum from typing import Final AZURE_CLIENT_ID: Final = "azure.client.id" """ The unique identifier of the client instance. """ AZURE_COSMOSDB_CONNECTION_MODE: Final = "azure.cosmosdb.connection.mode" """ Cosmos client connection mode. """ AZURE_COSMOSDB_CONSISTENCY_LEVEL: Final = "azure.cosmosdb.consistency.level" """ Account or request [consistency level](https://learn.microsoft.com/azure/cosmos-db/consistency-levels). """ AZURE_COSMOSDB_OPERATION_CONTACTED_REGIONS: Final = "azure.cosmosdb.operation.contacted_regions" """ List of regions contacted during operation in the order that they were contacted. If there is more than one region listed, it indicates that the operation was performed on multiple regions i.e. cross-regional call. Note: Region name matches the format of `displayName` in [Azure Location API](https://learn.microsoft.com/rest/api/resources/subscriptions/list-locations). """ AZURE_COSMOSDB_OPERATION_REQUEST_CHARGE: Final = "azure.cosmosdb.operation.request_charge" """ The number of request units consumed by the operation. """ AZURE_COSMOSDB_REQUEST_BODY_SIZE: Final = "azure.cosmosdb.request.body.size" """ Request payload size in bytes. """ AZURE_COSMOSDB_RESPONSE_SUB_STATUS_CODE: Final = "azure.cosmosdb.response.sub_status_code" """ Cosmos DB sub status code. """ AZURE_RESOURCE_GROUP_NAME: Final = "azure.resource_group.name" """ The name of the Azure [resource group](https://learn.microsoft.com/azure/azure-resource-manager/management/manage-resource-groups-portal) the resource belongs to. """ AZURE_RESOURCE_PROVIDER_NAMESPACE: Final = "azure.resource_provider.namespace" """ [Azure Resource Provider Namespace](https://learn.microsoft.com/azure/azure-resource-manager/management/azure-services-resource-providers) as recognized by the client. """ AZURE_SERVICE_REQUEST_ID: Final = "azure.service.request.id" """ The unique identifier of the service request. It's generated by the Azure service and returned with the response. """ class AzureCosmosdbConnectionModeValues(Enum): GATEWAY = "gateway" """Gateway (HTTP) connection.""" DIRECT = "direct" """Direct connection.""" class AzureCosmosdbConsistencyLevelValues(Enum): STRONG = "Strong" """Strong.""" BOUNDED_STALENESS = "BoundedStaleness" """Bounded Staleness.""" SESSION = "Session" """Session.""" EVENTUAL = "Eventual" """Eventual.""" CONSISTENT_PREFIX = "ConsistentPrefix" """Consistent Prefix.""" browser_attributes.py000066400000000000000000000110521527070070700431130ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/src/opentelemetry/semconv/_incubating/attributes# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from enum import Enum from typing import Final BROWSER_BRANDS: Final = "browser.brands" """ Array of brand name and version separated by a space. Note: This value is intended to be taken from the [UA client hints API](https://wicg.github.io/ua-client-hints/#interface) (`navigator.userAgentData.brands`). """ BROWSER_DOCUMENT_URL_FULL: Final = "browser.document.url.full" """ Absolute URL of the current browser document according to [RFC3986](https://www.rfc-editor.org/rfc/rfc3986). """ BROWSER_LANGUAGE: Final = "browser.language" """ Preferred language of the user using the browser. Note: This value is intended to be taken from the Navigator API `navigator.language`. """ BROWSER_MOBILE: Final = "browser.mobile" """ A boolean that is true if the browser is running on a mobile device. Note: This value is intended to be taken from the [UA client hints API](https://wicg.github.io/ua-client-hints/#interface) (`navigator.userAgentData.mobile`). If unavailable, this attribute SHOULD be left unset. """ BROWSER_PLATFORM: Final = "browser.platform" """ The platform on which the browser is running. Note: This value is intended to be taken from the [UA client hints API](https://wicg.github.io/ua-client-hints/#interface) (`navigator.userAgentData.platform`). If unavailable, the legacy `navigator.platform` API SHOULD NOT be used instead and this attribute SHOULD be left unset in order for the values to be consistent. The list of possible values is defined in the [W3C User-Agent Client Hints specification](https://wicg.github.io/ua-client-hints/#sec-ch-ua-platform). Note that some (but not all) of these values can overlap with values in the [`os.type` and `os.name` attributes](./os.md). However, for consistency, the values in the `browser.platform` attribute should capture the exact value that the user agent provides. """ BROWSER_WEB_VITAL_DELTA: Final = "browser.web_vital.delta" """ The delta between the current value and the last-reported value. See [delta](https://github.com/GoogleChrome/web-vitals?tab=readme-ov-file#report-only-the-delta-of-changes). """ BROWSER_WEB_VITAL_ID: Final = "browser.web_vital.id" """ A unique ID representing this particular metric instance. """ BROWSER_WEB_VITAL_NAME: Final = "browser.web_vital.name" """ Name of the web vital. """ BROWSER_WEB_VITAL_NAVIGATION_TYPE: Final = "browser.web_vital.navigation_type" """ The type of navigation, as reported by the [Navigation Timing API](https://developer.mozilla.org/docs/Web/API/PerformanceNavigationTiming/type), with additional values reported by the web-vitals library. """ BROWSER_WEB_VITAL_RATING: Final = "browser.web_vital.rating" """ The rating of the web vital value against the "good", "needs improvement", and "poor" thresholds defined for the metric. """ BROWSER_WEB_VITAL_VALUE: Final = "browser.web_vital.value" """ Value of the web vital. """ class BrowserWebVitalNameValues(Enum): CLS = "cls" """Cumulative Layout Shift. See [cls](https://web.dev/articles/cls).""" LCP = "lcp" """Largest Contentful Paint. See [lcp](https://web.dev/articles/lcp).""" FCP = "fcp" """First Contentful Paint. See [fcp](https://web.dev/articles/fcp).""" INP = "inp" """Interaction to Next Paint. See [inp](https://web.dev/articles/inp).""" TTFB = "ttfb" """Time to First Byte. See [ttfb](https://web.dev/articles/ttfb).""" FID = "fid" """Deprecated: Replaced by Interaction to Next Paint (`inp`), which became a Core Web Vital in March 2024. See [inp](https://web.dev/articles/inp).""" class BrowserWebVitalNavigationTypeValues(Enum): NAVIGATE = "navigate" """Navigation started by clicking a link, entering a URL, form submission, or a script operation.""" RELOAD = "reload" """Navigation through a reload operation or a `Location.reload()` call.""" BACK_FORWARD = "back-forward" """Navigation through the browser's history traversal (e.g. back/forward buttons).""" BACK_FORWARD_CACHE = "back-forward-cache" """Navigation restoring a page from the back/forward cache (bfcache).""" PRERENDER = "prerender" """Navigation to a page that was prerendered.""" RESTORE = "restore" """Navigation restoring a page that was previously discarded by the browser.""" class BrowserWebVitalRatingValues(Enum): GOOD = "good" """The metric value is within the "good" threshold.""" NEEDS_IMPROVEMENT = "needs-improvement" """The metric value is within the "needs improvement" threshold.""" POOR = "poor" """The metric value is within the "poor" threshold.""" cassandra_attributes.py000066400000000000000000000030461527070070700433730ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/src/opentelemetry/semconv/_incubating/attributes# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from enum import Enum from typing import Final CASSANDRA_CONSISTENCY_LEVEL: Final = "cassandra.consistency.level" """ The consistency level of the query. Based on consistency values from [CQL](https://docs.datastax.com/en/cassandra-oss/3.0/cassandra/dml/dmlConfigConsistency.html). """ CASSANDRA_COORDINATOR_DC: Final = "cassandra.coordinator.dc" """ The data center of the coordinating node for a query. """ CASSANDRA_COORDINATOR_ID: Final = "cassandra.coordinator.id" """ The ID of the coordinating node for a query. """ CASSANDRA_PAGE_SIZE: Final = "cassandra.page.size" """ The fetch size used for paging, i.e. how many rows will be returned at once. """ CASSANDRA_QUERY_IDEMPOTENT: Final = "cassandra.query.idempotent" """ Whether or not the query is idempotent. """ CASSANDRA_SPECULATIVE_EXECUTION_COUNT: Final = "cassandra.speculative_execution.count" """ The number of times a query was speculatively executed. Not set or `0` if the query was not executed speculatively. """ class CassandraConsistencyLevelValues(Enum): ALL = "all" """All.""" EACH_QUORUM = "each_quorum" """Each Quorum.""" QUORUM = "quorum" """Quorum.""" LOCAL_QUORUM = "local_quorum" """Local Quorum.""" ONE = "one" """One.""" TWO = "two" """Two.""" THREE = "three" """Three.""" LOCAL_ONE = "local_one" """Local One.""" ANY = "any" """Any.""" SERIAL = "serial" """Serial.""" LOCAL_SERIAL = "local_serial" """Local Serial.""" cicd_attributes.py000066400000000000000000000126721527070070700423430ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/src/opentelemetry/semconv/_incubating/attributes# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from enum import Enum from typing import Final CICD_PIPELINE_ACTION_NAME: Final = "cicd.pipeline.action.name" """ The kind of action a pipeline run is performing. """ CICD_PIPELINE_NAME: Final = "cicd.pipeline.name" """ The human readable name of the pipeline within a CI/CD system. """ CICD_PIPELINE_RESULT: Final = "cicd.pipeline.result" """ The result of a pipeline run. """ CICD_PIPELINE_RUN_ID: Final = "cicd.pipeline.run.id" """ The unique identifier of a pipeline run within a CI/CD system. """ CICD_PIPELINE_RUN_STATE: Final = "cicd.pipeline.run.state" """ The pipeline run goes through these states during its lifecycle. """ CICD_PIPELINE_RUN_URL_FULL: Final = "cicd.pipeline.run.url.full" """ The [URL](https://wikipedia.org/wiki/URL) of the pipeline run, providing the complete address in order to locate and identify the pipeline run. """ CICD_PIPELINE_TASK_NAME: Final = "cicd.pipeline.task.name" """ The human readable name of a task within a pipeline. Task here most closely aligns with a [computing process](https://wikipedia.org/wiki/Pipeline_(computing)) in a pipeline. Other terms for tasks include commands, steps, and procedures. """ CICD_PIPELINE_TASK_RUN_ID: Final = "cicd.pipeline.task.run.id" """ The unique identifier of a task run within a pipeline. Note: For a given pipeline run and task, the `cicd.pipeline.task.run.id` MUST be unique within that run. For the same task across different runs of the same pipeline, the `cicd.pipeline.task.run.id` MAY remain the same, enabling correlation of `cicd.pipeline.task.run.result` values across multiple pipeline runs. """ CICD_PIPELINE_TASK_RUN_RESULT: Final = "cicd.pipeline.task.run.result" """ The result of a task run. """ CICD_PIPELINE_TASK_RUN_URL_FULL: Final = "cicd.pipeline.task.run.url.full" """ The [URL](https://wikipedia.org/wiki/URL) of the pipeline task run, providing the complete address in order to locate and identify the pipeline task run. """ CICD_PIPELINE_TASK_TYPE: Final = "cicd.pipeline.task.type" """ The type of the task within a pipeline. """ CICD_SYSTEM_COMPONENT: Final = "cicd.system.component" """ The name of a component of the CI/CD system. """ CICD_WORKER_ID: Final = "cicd.worker.id" """ The unique identifier of a worker within a CI/CD system. """ CICD_WORKER_NAME: Final = "cicd.worker.name" """ The name of a worker within a CI/CD system. """ CICD_WORKER_STATE: Final = "cicd.worker.state" """ The state of a CI/CD worker / agent. """ CICD_WORKER_URL_FULL: Final = "cicd.worker.url.full" """ The [URL](https://wikipedia.org/wiki/URL) of the worker, providing the complete address in order to locate and identify the worker. """ class CicdPipelineActionNameValues(Enum): BUILD = "BUILD" """The pipeline run is executing a build.""" RUN = "RUN" """The pipeline run is executing.""" SYNC = "SYNC" """The pipeline run is executing a sync.""" class CicdPipelineResultValues(Enum): SUCCESS = "success" """The pipeline run finished successfully.""" FAILURE = "failure" """The pipeline run did not finish successfully, eg. due to a compile error or a failing test. Such failures are usually detected by non-zero exit codes of the tools executed in the pipeline run.""" ERROR = "error" """The pipeline run failed due to an error in the CI/CD system, eg. due to the worker being killed.""" TIMEOUT = "timeout" """A timeout caused the pipeline run to be interrupted.""" CANCELLATION = "cancellation" """The pipeline run was cancelled, eg. by a user manually cancelling the pipeline run.""" SKIP = "skip" """The pipeline run was skipped, eg. due to a precondition not being met.""" class CicdPipelineRunStateValues(Enum): PENDING = "pending" """The run pending state spans from the event triggering the pipeline run until the execution of the run starts (eg. time spent in a queue, provisioning agents, creating run resources).""" EXECUTING = "executing" """The executing state spans the execution of any run tasks (eg. build, test).""" FINALIZING = "finalizing" """The finalizing state spans from when the run has finished executing (eg. cleanup of run resources).""" class CicdPipelineTaskRunResultValues(Enum): SUCCESS = "success" """The task run finished successfully.""" FAILURE = "failure" """The task run did not finish successfully, eg. due to a compile error or a failing test. Such failures are usually detected by non-zero exit codes of the tools executed in the task run.""" ERROR = "error" """The task run failed due to an error in the CI/CD system, eg. due to the worker being killed.""" TIMEOUT = "timeout" """A timeout caused the task run to be interrupted.""" CANCELLATION = "cancellation" """The task run was cancelled, eg. by a user manually cancelling the task run.""" SKIP = "skip" """The task run was skipped, eg. due to a precondition not being met.""" class CicdPipelineTaskTypeValues(Enum): BUILD = "build" """build.""" TEST = "test" """test.""" DEPLOY = "deploy" """deploy.""" class CicdWorkerStateValues(Enum): AVAILABLE = "available" """The worker is not performing work for the CI/CD system. It is available to the CI/CD system to perform work on (online / idle).""" BUSY = "busy" """The worker is performing work for the CI/CD system.""" OFFLINE = "offline" """The worker is not available to the CI/CD system (disconnected / down).""" client_attributes.py000066400000000000000000000006331527070070700427110ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/src/opentelemetry/semconv/_incubating/attributes# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from typing import Final CLIENT_ADDRESS: Final = "client.address" """ Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.client_attributes.CLIENT_ADDRESS`. """ CLIENT_PORT: Final = "client.port" """ Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.client_attributes.CLIENT_PORT`. """ cloud_attributes.py000066400000000000000000000154311527070070700425430ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/src/opentelemetry/semconv/_incubating/attributes# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from enum import Enum from typing import Final CLOUD_ACCOUNT_ID: Final = "cloud.account.id" """ The cloud account ID the resource is assigned to. Note: For Azure, this is the subscription ID. """ CLOUD_AVAILABILITY_ZONE: Final = "cloud.availability_zone" """ Cloud regions often have multiple, isolated locations known as zones to increase availability. Availability zone represents the zone where the resource is running. Note: Availability zones are called "zones" on Alibaba Cloud and Google Cloud. """ CLOUD_PLATFORM: Final = "cloud.platform" """ The cloud platform in use. Note: The prefix of the service SHOULD match the one specified in `cloud.provider`. """ CLOUD_PROVIDER: Final = "cloud.provider" """ Name of the cloud provider. """ CLOUD_REGION: Final = "cloud.region" """ The geographical region within a cloud provider. When associated with a resource, this attribute specifies the region where the resource operates. When calling services or APIs deployed on a cloud, this attribute identifies the region where the called destination is deployed. Note: Refer to your provider's docs to see the available regions, for example [Alibaba Cloud regions](https://www.alibabacloud.com/help/doc-detail/40654.htm), [AWS regions](https://aws.amazon.com/about-aws/global-infrastructure/regions_az/), [Azure regions](https://azure.microsoft.com/global-infrastructure/geographies/), [Google Cloud regions](https://cloud.google.com/about/locations), or [Tencent Cloud regions](https://www.tencentcloud.com/document/product/213/6091). """ CLOUD_RESOURCE_ID: Final = "cloud.resource_id" """ Cloud provider-specific native identifier of the monitored cloud resource (e.g. an [ARN](https://docs.aws.amazon.com/general/latest/gr/aws-arns-and-namespaces.html) on AWS, a [fully qualified resource ID](https://learn.microsoft.com/rest/api/resources/resources/get-by-id) on Azure, a [full resource name](https://google.aip.dev/122#full-resource-names) on GCP). Note: 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. The exact value to use for `cloud.resource_id` depends on the cloud provider. The following well-known definitions MUST be used if you set this attribute and they apply: - **AWS Lambda:** The function [ARN](https://docs.aws.amazon.com/general/latest/gr/aws-arns-and-namespaces.html). Take care not to use the "invoked ARN" directly but replace any [alias suffix](https://docs.aws.amazon.com/lambda/latest/dg/configuration-aliases.html) with the resolved function version, as the same runtime instance may be invocable with multiple different aliases. - **GCP:** The [URI of the resource](https://cloud.google.com/iam/docs/full-resource-names) - **Azure:** The [Fully Qualified Resource ID](https://learn.microsoft.com/rest/api/resources/resources/get-by-id) of the invoked function, *not* the function app, having the form `/subscriptions//resourceGroups//providers/Microsoft.Web/sites//functions/`. This means that a span attribute MUST be used, as an Azure function app can host multiple functions that would usually share a TracerProvider. """ class CloudPlatformValues(Enum): AKAMAI_CLOUD_COMPUTE = "akamai_cloud.compute" """Akamai Cloud Compute.""" ALIBABA_CLOUD_ECS = "alibaba_cloud_ecs" """Alibaba Cloud Elastic Compute Service.""" ALIBABA_CLOUD_FC = "alibaba_cloud_fc" """Alibaba Cloud Function Compute.""" ALIBABA_CLOUD_OPENSHIFT = "alibaba_cloud_openshift" """Red Hat OpenShift on Alibaba Cloud.""" AWS_EC2 = "aws_ec2" """AWS Elastic Compute Cloud.""" AWS_ECS = "aws_ecs" """AWS Elastic Container Service.""" AWS_EKS = "aws_eks" """AWS Elastic Kubernetes Service.""" AWS_LAMBDA = "aws_lambda" """AWS Lambda.""" AWS_ELASTIC_BEANSTALK = "aws_elastic_beanstalk" """AWS Elastic Beanstalk.""" AWS_APP_RUNNER = "aws_app_runner" """AWS App Runner.""" AWS_OPENSHIFT = "aws_openshift" """Red Hat OpenShift on AWS (ROSA).""" AZURE_VM = "azure.vm" """Azure Virtual Machines.""" AZURE_CONTAINER_APPS = "azure.container_apps" """Azure Container Apps.""" AZURE_CONTAINER_INSTANCES = "azure.container_instances" """Azure Container Instances.""" AZURE_AKS = "azure.aks" """Azure Kubernetes Service.""" AZURE_FUNCTIONS = "azure.functions" """Azure Functions.""" AZURE_APP_SERVICE = "azure.app_service" """Azure App Service.""" AZURE_OPENSHIFT = "azure.openshift" """Azure Red Hat OpenShift.""" GCP_AGENT_ENGINE = "gcp.agent_engine" """Google Vertex AI Agent Engine.""" GCP_BARE_METAL_SOLUTION = "gcp_bare_metal_solution" """Google Bare Metal Solution (BMS).""" GCP_COMPUTE_ENGINE = "gcp_compute_engine" """Google Cloud Compute Engine (GCE).""" GCP_CLOUD_RUN = "gcp_cloud_run" """Google Cloud Run.""" GCP_KUBERNETES_ENGINE = "gcp_kubernetes_engine" """Google Cloud Kubernetes Engine (GKE).""" GCP_CLOUD_FUNCTIONS = "gcp_cloud_functions" """Google Cloud Functions (GCF).""" GCP_APP_ENGINE = "gcp_app_engine" """Google Cloud App Engine (GAE).""" GCP_OPENSHIFT = "gcp_openshift" """Red Hat OpenShift on Google Cloud.""" HETZNER_CLOUD_SERVER = "hetzner.cloud_server" """Server on Hetzner Cloud.""" IBM_CLOUD_OPENSHIFT = "ibm_cloud_openshift" """Red Hat OpenShift on IBM Cloud.""" ORACLE_CLOUD_COMPUTE = "oracle_cloud_compute" """Compute on Oracle Cloud Infrastructure (OCI).""" ORACLE_CLOUD_OKE = "oracle_cloud_oke" """Kubernetes Engine (OKE) on Oracle Cloud Infrastructure (OCI).""" SCALEWAY_CLOUD_COMPUTE = "scaleway_cloud_compute" """Compute on Scaleway Cloud.""" TENCENT_CLOUD_CVM = "tencent_cloud_cvm" """Tencent Cloud Cloud Virtual Machine (CVM).""" TENCENT_CLOUD_EKS = "tencent_cloud_eks" """Tencent Cloud Elastic Kubernetes Service (EKS).""" TENCENT_CLOUD_SCF = "tencent_cloud_scf" """Tencent Cloud Serverless Cloud Function (SCF).""" VULTR_CLOUD_COMPUTE = "vultr.cloud_compute" """Vultr Cloud Compute.""" class CloudProviderValues(Enum): AKAMAI_CLOUD = "akamai_cloud" """Akamai Cloud.""" ALIBABA_CLOUD = "alibaba_cloud" """Alibaba Cloud.""" AWS = "aws" """Amazon Web Services.""" AZURE = "azure" """Microsoft Azure.""" GCP = "gcp" """Google Cloud Platform.""" HEROKU = "heroku" """Heroku Platform as a Service.""" HETZNER = "hetzner" """Hetzner.""" IBM_CLOUD = "ibm_cloud" """IBM Cloud.""" ORACLE_CLOUD = "oracle_cloud" """Oracle Cloud Infrastructure (OCI).""" SCALEWAY_CLOUD = "scaleway_cloud" """Scaleway Cloud.""" TENCENT_CLOUD = "tencent_cloud" """Tencent Cloud.""" VULTR = "vultr" """Vultr.""" cloudevents_attributes.py000066400000000000000000000022651527070070700437710ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/src/opentelemetry/semconv/_incubating/attributes# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from typing import Final CLOUDEVENTS_EVENT_ID: Final = "cloudevents.event_id" """ The [event_id](https://github.com/cloudevents/spec/blob/v1.0.2/cloudevents/spec.md#id) uniquely identifies the event. """ CLOUDEVENTS_EVENT_SOURCE: Final = "cloudevents.event_source" """ The [source](https://github.com/cloudevents/spec/blob/v1.0.2/cloudevents/spec.md#source-1) identifies the context in which an event happened. """ CLOUDEVENTS_EVENT_SPEC_VERSION: Final = "cloudevents.event_spec_version" """ The [version of the CloudEvents specification](https://github.com/cloudevents/spec/blob/v1.0.2/cloudevents/spec.md#specversion) which the event uses. """ CLOUDEVENTS_EVENT_SUBJECT: Final = "cloudevents.event_subject" """ The [subject](https://github.com/cloudevents/spec/blob/v1.0.2/cloudevents/spec.md#subject) of the event in the context of the event producer (identified by source). """ CLOUDEVENTS_EVENT_TYPE: Final = "cloudevents.event_type" """ The [event_type](https://github.com/cloudevents/spec/blob/v1.0.2/cloudevents/spec.md#type) contains a value describing the type of event related to the originating occurrence. """ cloudfoundry_attributes.py000066400000000000000000000101621527070070700441460ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/src/opentelemetry/semconv/_incubating/attributes# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from typing import Final CLOUDFOUNDRY_APP_ID: Final = "cloudfoundry.app.id" """ The guid of the application. Note: Application instrumentation should use the value from environment variable `VCAP_APPLICATION.application_id`. This is the same value as reported by `cf app --guid`. """ CLOUDFOUNDRY_APP_INSTANCE_ID: Final = "cloudfoundry.app.instance.id" """ The index of the application instance. 0 when just one instance is active. Note: CloudFoundry defines the `instance_id` in the [Loggregator v2 envelope](https://github.com/cloudfoundry/loggregator-api#v2-envelope). It is used for logs and metrics emitted by CloudFoundry. It is supposed to contain the application instance index for applications deployed on the runtime. Application instrumentation should use the value from environment variable `CF_INSTANCE_INDEX`. """ CLOUDFOUNDRY_APP_NAME: Final = "cloudfoundry.app.name" """ The name of the application. Note: Application instrumentation should use the value from environment variable `VCAP_APPLICATION.application_name`. This is the same value as reported by `cf apps`. """ CLOUDFOUNDRY_ORG_ID: Final = "cloudfoundry.org.id" """ The guid of the CloudFoundry org the application is running in. Note: Application instrumentation should use the value from environment variable `VCAP_APPLICATION.org_id`. This is the same value as reported by `cf org --guid`. """ CLOUDFOUNDRY_ORG_NAME: Final = "cloudfoundry.org.name" """ The name of the CloudFoundry organization the app is running in. Note: Application instrumentation should use the value from environment variable `VCAP_APPLICATION.org_name`. This is the same value as reported by `cf orgs`. """ CLOUDFOUNDRY_PROCESS_ID: Final = "cloudfoundry.process.id" """ The UID identifying the process. Note: Application instrumentation should use the value from environment variable `VCAP_APPLICATION.process_id`. It is supposed to be equal to `VCAP_APPLICATION.app_id` for applications deployed to the runtime. For system components, this could be the actual PID. """ CLOUDFOUNDRY_PROCESS_TYPE: Final = "cloudfoundry.process.type" """ The type of process. Note: CloudFoundry applications can consist of multiple jobs. Usually the main process will be of type `web`. There can be additional background tasks or side-cars with different process types. """ CLOUDFOUNDRY_SPACE_ID: Final = "cloudfoundry.space.id" """ The guid of the CloudFoundry space the application is running in. Note: Application instrumentation should use the value from environment variable `VCAP_APPLICATION.space_id`. This is the same value as reported by `cf space --guid`. """ CLOUDFOUNDRY_SPACE_NAME: Final = "cloudfoundry.space.name" """ The name of the CloudFoundry space the application is running in. Note: Application instrumentation should use the value from environment variable `VCAP_APPLICATION.space_name`. This is the same value as reported by `cf spaces`. """ CLOUDFOUNDRY_SYSTEM_ID: Final = "cloudfoundry.system.id" """ A guid or another name describing the event source. Note: CloudFoundry defines the `source_id` in the [Loggregator v2 envelope](https://github.com/cloudfoundry/loggregator-api#v2-envelope). It is used for logs and metrics emitted by CloudFoundry. It is supposed to contain the component name, e.g. "gorouter", for CloudFoundry components. When system components are instrumented, values from the [Bosh spec](https://bosh.io/docs/jobs/#properties-spec) should be used. The `system.id` should be set to `spec.deployment/spec.name`. """ CLOUDFOUNDRY_SYSTEM_INSTANCE_ID: Final = "cloudfoundry.system.instance.id" """ A guid describing the concrete instance of the event source. Note: CloudFoundry defines the `instance_id` in the [Loggregator v2 envelope](https://github.com/cloudfoundry/loggregator-api#v2-envelope). It is used for logs and metrics emitted by CloudFoundry. It is supposed to contain the vm ID for CloudFoundry components. When system components are instrumented, values from the [Bosh spec](https://bosh.io/docs/jobs/#properties-spec) should be used. The `system.instance.id` should be set to `spec.id`. """ code_attributes.py000066400000000000000000000027401527070070700423460ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/src/opentelemetry/semconv/_incubating/attributes# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from typing import Final CODE_COLUMN: Final = "code.column" """ Deprecated: Replaced by `code.column.number`. """ CODE_COLUMN_NUMBER: Final = "code.column.number" """ Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.code_attributes.CODE_COLUMN_NUMBER`. """ CODE_FILE_PATH: Final = "code.file.path" """ Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.code_attributes.CODE_FILE_PATH`. """ CODE_FILEPATH: Final = "code.filepath" """ Deprecated: Replaced by `code.file.path`. """ CODE_FUNCTION: Final = "code.function" """ Deprecated: Value should be included in `code.function.name` which is expected to be a fully-qualified name. """ CODE_FUNCTION_NAME: Final = "code.function.name" """ Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.code_attributes.CODE_FUNCTION_NAME`. """ CODE_LINE_NUMBER: Final = "code.line.number" """ Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.code_attributes.CODE_LINE_NUMBER`. """ CODE_LINENO: Final = "code.lineno" """ Deprecated: Replaced by `code.line.number`. """ CODE_NAMESPACE: Final = "code.namespace" """ Deprecated: Value should be included in `code.function.name` which is expected to be a fully-qualified name. """ CODE_STACKTRACE: Final = "code.stacktrace" """ Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.code_attributes.CODE_STACKTRACE`. """ container_attributes.py000066400000000000000000000107641527070070700434230ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/src/opentelemetry/semconv/_incubating/attributes# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from enum import Enum from typing import Final from typing_extensions import deprecated CONTAINER_COMMAND: Final = "container.command" """ The command used to run the container (i.e. the command name). Note: If using embedded credentials or sensitive data, it is recommended to remove them to prevent potential leakage. """ CONTAINER_COMMAND_ARGS: Final = "container.command_args" """ All the command arguments (including the command/executable itself) run by the container. """ CONTAINER_COMMAND_LINE: Final = "container.command_line" """ The full command run by the container as a single string representing the full command. """ CONTAINER_CPU_STATE: Final = "container.cpu.state" """ Deprecated: Replaced by `cpu.mode`. """ CONTAINER_CSI_PLUGIN_NAME: Final = "container.csi.plugin.name" """ The name of the CSI ([Container Storage Interface](https://github.com/container-storage-interface/spec)) plugin used by the volume. Note: This can sometimes be referred to as a "driver" in CSI implementations. This should represent the `name` field of the GetPluginInfo RPC. """ CONTAINER_CSI_VOLUME_ID: Final = "container.csi.volume.id" """ The unique volume ID returned by the CSI ([Container Storage Interface](https://github.com/container-storage-interface/spec)) plugin. Note: This can sometimes be referred to as a "volume handle" in CSI implementations. This should represent the `Volume.volume_id` field in CSI spec. """ CONTAINER_ID: Final = "container.id" """ Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.container_attributes.CONTAINER_ID`. """ CONTAINER_IMAGE_ID: Final = "container.image.id" """ Runtime specific image identifier. Usually a hash algorithm followed by a UUID. Note: Docker defines a sha256 of the image ID; `container.image.id` corresponds to the `Image` field from the Docker container inspect [API](https://docs.docker.com/reference/api/engine/version/v1.52/#tag/Container/operation/ContainerInspect) endpoint. K8s defines a link to the container registry repository with digest `"imageID": "registry.azurecr.io /namespace/service/dockerfile@sha256:bdeabd40c3a8a492eaf9e8e44d0ebbb84bac7ee25ac0cf8a7159d25f62555625"`. The ID is assigned by the container runtime and can vary in different environments. Consider using `oci.manifest.digest` if it is important to identify the same image in different environments/runtimes. """ CONTAINER_IMAGE_NAME: Final = "container.image.name" """ Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.container_attributes.CONTAINER_IMAGE_NAME`. """ CONTAINER_IMAGE_REPO_DIGESTS: Final = "container.image.repo_digests" """ Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.container_attributes.CONTAINER_IMAGE_REPO_DIGESTS`. """ CONTAINER_IMAGE_TAGS: Final = "container.image.tags" """ Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.container_attributes.CONTAINER_IMAGE_TAGS`. """ CONTAINER_LABEL_TEMPLATE: Final = "container.label" """ Container labels, `` being the label name, the value being the label value. Note: For example, a Docker container label `app` with value `nginx` SHOULD be recorded as the `container.label.app` attribute with value `"nginx"`. """ CONTAINER_LABELS_TEMPLATE: Final = "container.labels" """ Deprecated: Replaced by `container.label`. """ CONTAINER_NAME: Final = "container.name" """ Container name used by container runtime. """ CONTAINER_RUNTIME: Final = "container.runtime" """ Deprecated: Replaced by `container.runtime.name`. """ CONTAINER_RUNTIME_DESCRIPTION: Final = "container.runtime.description" """ A description about the runtime which could include, for example details about the CRI/API version being used or other customizations. """ CONTAINER_RUNTIME_NAME: Final = "container.runtime.name" """ The container runtime managing this container. """ CONTAINER_RUNTIME_VERSION: Final = "container.runtime.version" """ The version of the runtime of this process, as returned by the runtime without modification. """ @deprecated("The attribute container.cpu.state is deprecated - Replaced by `cpu.mode`") class ContainerCpuStateValues(Enum): USER = "user" """When tasks of the cgroup are in user mode (Linux). When all container processes are in user mode (Windows).""" SYSTEM = "system" """When CPU is used by the system (host OS).""" KERNEL = "kernel" """When tasks of the cgroup are in kernel mode (Linux). When all container processes are in kernel mode (Windows).""" cpu_attributes.py000066400000000000000000000011761527070070700422250ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/src/opentelemetry/semconv/_incubating/attributes# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from enum import Enum from typing import Final CPU_LOGICAL_NUMBER: Final = "cpu.logical_number" """ The logical CPU number [0..n-1]. """ CPU_MODE: Final = "cpu.mode" """ The mode of the CPU. """ class CpuModeValues(Enum): USER = "user" """User.""" SYSTEM = "system" """System.""" NICE = "nice" """Nice.""" IDLE = "idle" """Idle.""" IOWAIT = "iowait" """IO Wait.""" INTERRUPT = "interrupt" """Interrupt.""" STEAL = "steal" """Steal.""" KERNEL = "kernel" """Deprecated: Replaced by `system`.""" cpython_attributes.py000066400000000000000000000006421527070070700431170ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/src/opentelemetry/semconv/_incubating/attributes# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from enum import Enum from typing import Final CPYTHON_GC_GENERATION: Final = "cpython.gc.generation" """ Value of the garbage collector collection generation. """ class CPythonGCGenerationValues(Enum): GENERATION_0 = 0 """Generation 0.""" GENERATION_1 = 1 """Generation 1.""" GENERATION_2 = 2 """Generation 2.""" db_attributes.py000066400000000000000000000424311527070070700420220ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/src/opentelemetry/semconv/_incubating/attributes# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from enum import Enum from typing import Final from typing_extensions import deprecated DB_CASSANDRA_CONSISTENCY_LEVEL: Final = "db.cassandra.consistency_level" """ Deprecated: Replaced by `cassandra.consistency.level`. """ DB_CASSANDRA_COORDINATOR_DC: Final = "db.cassandra.coordinator.dc" """ Deprecated: Replaced by `cassandra.coordinator.dc`. """ DB_CASSANDRA_COORDINATOR_ID: Final = "db.cassandra.coordinator.id" """ Deprecated: Replaced by `cassandra.coordinator.id`. """ DB_CASSANDRA_IDEMPOTENCE: Final = "db.cassandra.idempotence" """ Deprecated: Replaced by `cassandra.query.idempotent`. """ DB_CASSANDRA_PAGE_SIZE: Final = "db.cassandra.page_size" """ Deprecated: Replaced by `cassandra.page.size`. """ DB_CASSANDRA_SPECULATIVE_EXECUTION_COUNT: Final = "db.cassandra.speculative_execution_count" """ Deprecated: Replaced by `cassandra.speculative_execution.count`. """ DB_CASSANDRA_TABLE: Final = "db.cassandra.table" """ Deprecated: Replaced by `db.collection.name`. """ DB_CLIENT_CONNECTION_POOL_NAME: Final = "db.client.connection.pool.name" """ The name of the connection pool; unique within the instrumented application. In case the connection pool implementation doesn't provide a name, instrumentation SHOULD use a combination of parameters that would make the name unique, for example, combining attributes `server.address`, `server.port`, and `db.namespace`, formatted as `server.address:server.port/db.namespace`. Instrumentations that generate connection pool name following different patterns SHOULD document it. """ DB_CLIENT_CONNECTION_STATE: Final = "db.client.connection.state" """ The state of a connection in the pool. """ DB_CLIENT_CONNECTIONS_POOL_NAME: Final = "db.client.connections.pool.name" """ Deprecated: Replaced by `db.client.connection.pool.name`. """ DB_CLIENT_CONNECTIONS_STATE: Final = "db.client.connections.state" """ Deprecated: Replaced by `db.client.connection.state`. """ DB_COLLECTION_NAME: Final = "db.collection.name" """ Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.db_attributes.DB_COLLECTION_NAME`. """ DB_CONNECTION_STRING: Final = "db.connection_string" """ Deprecated: Replaced by `server.address` and `server.port`. """ DB_COSMOSDB_CLIENT_ID: Final = "db.cosmosdb.client_id" """ Deprecated: Replaced by `azure.client.id`. """ DB_COSMOSDB_CONNECTION_MODE: Final = "db.cosmosdb.connection_mode" """ Deprecated: Replaced by `azure.cosmosdb.connection.mode`. """ DB_COSMOSDB_CONSISTENCY_LEVEL: Final = "db.cosmosdb.consistency_level" """ Deprecated: Replaced by `azure.cosmosdb.consistency.level`. """ DB_COSMOSDB_CONTAINER: Final = "db.cosmosdb.container" """ Deprecated: Replaced by `db.collection.name`. """ DB_COSMOSDB_OPERATION_TYPE: Final = "db.cosmosdb.operation_type" """ Deprecated: Removed, no replacement at this time. """ DB_COSMOSDB_REGIONS_CONTACTED: Final = "db.cosmosdb.regions_contacted" """ Deprecated: Replaced by `azure.cosmosdb.operation.contacted_regions`. """ DB_COSMOSDB_REQUEST_CHARGE: Final = "db.cosmosdb.request_charge" """ Deprecated: Replaced by `azure.cosmosdb.operation.request_charge`. """ DB_COSMOSDB_REQUEST_CONTENT_LENGTH: Final = "db.cosmosdb.request_content_length" """ Deprecated: Replaced by `azure.cosmosdb.request.body.size`. """ DB_COSMOSDB_STATUS_CODE: Final = "db.cosmosdb.status_code" """ Deprecated: Use `db.response.status_code` instead. """ DB_COSMOSDB_SUB_STATUS_CODE: Final = "db.cosmosdb.sub_status_code" """ Deprecated: Replaced by `azure.cosmosdb.response.sub_status_code`. """ DB_ELASTICSEARCH_CLUSTER_NAME: Final = "db.elasticsearch.cluster.name" """ Deprecated: Replaced by `db.namespace`. """ DB_ELASTICSEARCH_NODE_NAME: Final = "db.elasticsearch.node.name" """ Deprecated: Replaced by `elasticsearch.node.name`. """ DB_ELASTICSEARCH_PATH_PARTS_TEMPLATE: Final = "db.elasticsearch.path_parts" """ Deprecated: Replaced by `db.operation.parameter`. """ DB_INSTANCE_ID: Final = "db.instance.id" """ Deprecated: Removed, no general replacement at this time. For Elasticsearch, use `db.elasticsearch.node.name` instead. """ DB_JDBC_DRIVER_CLASSNAME: Final = "db.jdbc.driver_classname" """ Deprecated: Removed, no replacement at this time. """ DB_MONGODB_COLLECTION: Final = "db.mongodb.collection" """ Deprecated: Replaced by `db.collection.name`. """ DB_MSSQL_INSTANCE_NAME: Final = "db.mssql.instance_name" """ Deprecated: Removed, no replacement at this time. """ DB_NAME: Final = "db.name" """ Deprecated: Replaced by `db.namespace`. """ DB_NAMESPACE: Final = "db.namespace" """ Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.db_attributes.DB_NAMESPACE`. """ DB_OPERATION: Final = "db.operation" """ Deprecated: Replaced by `db.operation.name`. """ DB_OPERATION_BATCH_SIZE: Final = "db.operation.batch.size" """ Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.db_attributes.DB_OPERATION_BATCH_SIZE`. """ DB_OPERATION_NAME: Final = "db.operation.name" """ Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.db_attributes.DB_OPERATION_NAME`. """ DB_OPERATION_PARAMETER_TEMPLATE: Final = "db.operation.parameter" """ A database operation parameter, with `` being the parameter name, and the attribute value being a string representation of the parameter value. Note: For example, a client-side maximum number of rows to read from the database MAY be recorded as the `db.operation.parameter.max_rows` attribute. `db.query.text` parameters SHOULD be captured using `db.query.parameter.` instead of `db.operation.parameter.`. """ DB_QUERY_PARAMETER_TEMPLATE: Final = "db.query.parameter" """ A database query parameter, with `` being the parameter name, and the attribute value being a string representation of the parameter value. Note: If a query parameter has no name and instead is referenced only by index, then `` SHOULD be the 0-based index. `db.query.parameter.` SHOULD match up with the parameterized placeholders present in `db.query.text`. It is RECOMMENDED to capture the value as provided by the application without attempting to do any case normalization or sanitization. Instrumentations SHOULD NOT capture `db.query.parameter.` by default since values may contain PII or sensitive details. Application operators are expected to enable specific keys depending on their privacy and security considerations. `db.query.parameter.` SHOULD NOT be captured on batch operations. Examples: - For a query `SELECT * FROM users where username = %s` with the parameter `"jdoe"`, the attribute `db.query.parameter.0` SHOULD be set to `"jdoe"`. - For a query `"SELECT * FROM users WHERE username = %(userName)s;` with parameter `userName = "jdoe"`, the attribute `db.query.parameter.userName` SHOULD be set to `"jdoe"`. """ DB_QUERY_SUMMARY: Final = "db.query.summary" """ Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.db_attributes.DB_QUERY_SUMMARY`. """ DB_QUERY_TEXT: Final = "db.query.text" """ Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.db_attributes.DB_QUERY_TEXT`. """ DB_REDIS_DATABASE_INDEX: Final = "db.redis.database_index" """ Deprecated: Replaced by `db.namespace` (string). """ DB_RESPONSE_RETURNED_ROWS: Final = "db.response.returned_rows" """ Number of rows returned by the operation. """ DB_RESPONSE_STATUS_CODE: Final = "db.response.status_code" """ Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.db_attributes.DB_RESPONSE_STATUS_CODE`. """ DB_SQL_TABLE: Final = "db.sql.table" """ Deprecated: Replaced by `db.collection.name`, but only if not extracting the value from `db.query.text`. """ DB_STATEMENT: Final = "db.statement" """ Deprecated: Replaced by `db.query.text`. """ DB_STORED_PROCEDURE_NAME: Final = "db.stored_procedure.name" """ Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.db_attributes.DB_STORED_PROCEDURE_NAME`. """ DB_SYSTEM: Final = "db.system" """ Deprecated: Replaced by `db.system.name`. """ DB_SYSTEM_NAME: Final = "db.system.name" """ Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.db_attributes.DB_SYSTEM_NAME`. """ DB_USER: Final = "db.user" """ Deprecated: Removed, no replacement at this time. """ @deprecated("The attribute db.cassandra.consistency_level is deprecated - Replaced by `cassandra.consistency.level`") class DbCassandraConsistencyLevelValues(Enum): ALL = "all" """all.""" EACH_QUORUM = "each_quorum" """each_quorum.""" QUORUM = "quorum" """quorum.""" LOCAL_QUORUM = "local_quorum" """local_quorum.""" ONE = "one" """one.""" TWO = "two" """two.""" THREE = "three" """three.""" LOCAL_ONE = "local_one" """local_one.""" ANY = "any" """any.""" SERIAL = "serial" """serial.""" LOCAL_SERIAL = "local_serial" """local_serial.""" class DbClientConnectionStateValues(Enum): IDLE = "idle" """idle.""" USED = "used" """used.""" @deprecated("The attribute db.client.connections.state is deprecated - Replaced by `db.client.connection.state`") class DbClientConnectionsStateValues(Enum): IDLE = "idle" """idle.""" USED = "used" """used.""" @deprecated("The attribute db.cosmosdb.connection_mode is deprecated - Replaced by `azure.cosmosdb.connection.mode`") class DbCosmosdbConnectionModeValues(Enum): GATEWAY = "gateway" """Gateway (HTTP) connection.""" DIRECT = "direct" """Direct connection.""" @deprecated( "The attribute db.cosmosdb.consistency_level is deprecated - Replaced by `azure.cosmosdb.consistency.level`" ) class DbCosmosdbConsistencyLevelValues(Enum): STRONG = "Strong" """strong.""" BOUNDED_STALENESS = "BoundedStaleness" """bounded_staleness.""" SESSION = "Session" """session.""" EVENTUAL = "Eventual" """eventual.""" CONSISTENT_PREFIX = "ConsistentPrefix" """consistent_prefix.""" @deprecated("The attribute db.cosmosdb.operation_type is deprecated - Removed, no replacement at this time") class DbCosmosdbOperationTypeValues(Enum): BATCH = "batch" """batch.""" CREATE = "create" """create.""" DELETE = "delete" """delete.""" EXECUTE = "execute" """execute.""" EXECUTE_JAVASCRIPT = "execute_javascript" """execute_javascript.""" INVALID = "invalid" """invalid.""" HEAD = "head" """head.""" HEAD_FEED = "head_feed" """head_feed.""" PATCH = "patch" """patch.""" QUERY = "query" """query.""" QUERY_PLAN = "query_plan" """query_plan.""" READ = "read" """read.""" READ_FEED = "read_feed" """read_feed.""" REPLACE = "replace" """replace.""" UPSERT = "upsert" """upsert.""" @deprecated("The attribute db.system is deprecated - Replaced by `db.system.name`") class DbSystemValues(Enum): OTHER_SQL = "other_sql" """Some other SQL database. Fallback only. See notes.""" ADABAS = "adabas" """Adabas (Adaptable Database System).""" CACHE = "cache" """Deprecated: Replaced by `intersystems_cache`.""" INTERSYSTEMS_CACHE = "intersystems_cache" """InterSystems Caché.""" CASSANDRA = "cassandra" """Apache Cassandra.""" CLICKHOUSE = "clickhouse" """ClickHouse.""" CLOUDSCAPE = "cloudscape" """Deprecated: Replaced by `other_sql`.""" COCKROACHDB = "cockroachdb" """CockroachDB.""" COLDFUSION = "coldfusion" """Deprecated: Obsoleted.""" COSMOSDB = "cosmosdb" """Microsoft Azure Cosmos DB.""" COUCHBASE = "couchbase" """Couchbase.""" COUCHDB = "couchdb" """CouchDB.""" DB2 = "db2" """IBM Db2.""" DERBY = "derby" """Apache Derby.""" DYNAMODB = "dynamodb" """Amazon DynamoDB.""" EDB = "edb" """EnterpriseDB.""" ELASTICSEARCH = "elasticsearch" """Elasticsearch.""" FILEMAKER = "filemaker" """FileMaker.""" FIREBIRD = "firebird" """Firebird.""" FIRSTSQL = "firstsql" """Deprecated: Replaced by `other_sql`.""" GEODE = "geode" """Apache Geode.""" H2 = "h2" """H2.""" HANADB = "hanadb" """SAP HANA.""" HBASE = "hbase" """Apache HBase.""" HIVE = "hive" """Apache Hive.""" HSQLDB = "hsqldb" """HyperSQL DataBase.""" INFLUXDB = "influxdb" """InfluxDB.""" INFORMIX = "informix" """Informix.""" INGRES = "ingres" """Ingres.""" INSTANTDB = "instantdb" """InstantDB.""" INTERBASE = "interbase" """InterBase.""" MARIADB = "mariadb" """MariaDB.""" MAXDB = "maxdb" """SAP MaxDB.""" MEMCACHED = "memcached" """Memcached.""" MONGODB = "mongodb" """MongoDB.""" MSSQL = "mssql" """Microsoft SQL Server.""" MSSQLCOMPACT = "mssqlcompact" """Deprecated: Replaced by `other_sql`.""" MYSQL = "mysql" """MySQL.""" NEO4J = "neo4j" """Neo4j.""" NETEZZA = "netezza" """Netezza.""" OPENSEARCH = "opensearch" """OpenSearch.""" ORACLE = "oracle" """Oracle Database.""" PERVASIVE = "pervasive" """Pervasive PSQL.""" POINTBASE = "pointbase" """PointBase.""" POSTGRESQL = "postgresql" """PostgreSQL.""" PROGRESS = "progress" """Progress Database.""" REDIS = "redis" """Redis.""" REDSHIFT = "redshift" """Amazon Redshift.""" SPANNER = "spanner" """Cloud Spanner.""" SQLITE = "sqlite" """SQLite.""" SYBASE = "sybase" """Sybase.""" TERADATA = "teradata" """Teradata.""" TRINO = "trino" """Trino.""" VERTICA = "vertica" """Vertica.""" @deprecated( "Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.db_attributes.DbSystemNameValues`." ) class DbSystemNameValues(Enum): OTHER_SQL = "other_sql" """Some other SQL database. Fallback only.""" SOFTWAREAG_ADABAS = "softwareag.adabas" """[Adabas (Adaptable Database System)](https://documentation.softwareag.com/?pf=adabas).""" ACTIAN_INGRES = "actian.ingres" """[Actian Ingres](https://www.actian.com/databases/ingres/).""" AWS_DYNAMODB = "aws.dynamodb" """[Amazon DynamoDB](https://aws.amazon.com/pm/dynamodb/).""" AWS_REDSHIFT = "aws.redshift" """[Amazon Redshift](https://aws.amazon.com/redshift/).""" AZURE_COSMOSDB = "azure.cosmosdb" """[Azure Cosmos DB](https://learn.microsoft.com/azure/cosmos-db).""" INTERSYSTEMS_CACHE = "intersystems.cache" """[InterSystems Caché](https://www.intersystems.com/products/cache/).""" CASSANDRA = "cassandra" """[Apache Cassandra](https://cassandra.apache.org/).""" CLICKHOUSE = "clickhouse" """[ClickHouse](https://clickhouse.com/).""" COCKROACHDB = "cockroachdb" """[CockroachDB](https://www.cockroachlabs.com/).""" COUCHBASE = "couchbase" """[Couchbase](https://www.couchbase.com/).""" COUCHDB = "couchdb" """[Apache CouchDB](https://couchdb.apache.org/).""" DERBY = "derby" """[Apache Derby](https://db.apache.org/derby/).""" ELASTICSEARCH = "elasticsearch" """[Elasticsearch](https://www.elastic.co/elasticsearch).""" FIREBIRDSQL = "firebirdsql" """[Firebird](https://www.firebirdsql.org/).""" GCP_SPANNER = "gcp.spanner" """[Google Cloud Spanner](https://cloud.google.com/spanner).""" GEODE = "geode" """[Apache Geode](https://geode.apache.org/).""" H2DATABASE = "h2database" """[H2 Database](https://h2database.com/).""" HBASE = "hbase" """[Apache HBase](https://hbase.apache.org/).""" HIVE = "hive" """[Apache Hive](https://hive.apache.org/).""" HSQLDB = "hsqldb" """[HyperSQL Database](https://hsqldb.org/).""" IBM_DB2 = "ibm.db2" """[IBM Db2](https://www.ibm.com/db2).""" IBM_INFORMIX = "ibm.informix" """[IBM Informix](https://www.ibm.com/products/informix).""" IBM_NETEZZA = "ibm.netezza" """[IBM Netezza](https://www.ibm.com/products/netezza).""" INFLUXDB = "influxdb" """[InfluxDB](https://www.influxdata.com/).""" INSTANTDB = "instantdb" """[Instant](https://www.instantdb.com/).""" MARIADB = "mariadb" """Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.db_attributes.DbSystemNameValues.MARIADB`.""" MEMCACHED = "memcached" """[Memcached](https://memcached.org/).""" MONGODB = "mongodb" """[MongoDB](https://www.mongodb.com/).""" MICROSOFT_SQL_SERVER = "microsoft.sql_server" """Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.db_attributes.DbSystemNameValues.MICROSOFT_SQL_SERVER`.""" MYSQL = "mysql" """Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.db_attributes.DbSystemNameValues.MYSQL`.""" NEO4J = "neo4j" """[Neo4j](https://neo4j.com/).""" OPENSEARCH = "opensearch" """[OpenSearch](https://opensearch.org/).""" ORACLE_DB = "oracle.db" """[Oracle Database](https://www.oracle.com/database/).""" POSTGRESQL = "postgresql" """Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.db_attributes.DbSystemNameValues.POSTGRESQL`.""" REDIS = "redis" """[Redis](https://redis.io/).""" SAP_HANA = "sap.hana" """[SAP HANA](https://www.sap.com/products/technology-platform/hana/what-is-sap-hana.html).""" SAP_MAXDB = "sap.maxdb" """[SAP MaxDB](https://maxdb.sap.com/).""" SQLITE = "sqlite" """[SQLite](https://www.sqlite.org/).""" TERADATA = "teradata" """[Teradata](https://www.teradata.com/).""" TRINO = "trino" """[Trino](https://trino.io/).""" deployment_attributes.py000066400000000000000000000033321527070070700436120ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/src/opentelemetry/semconv/_incubating/attributes# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from enum import Enum from typing import Final from typing_extensions import deprecated DEPLOYMENT_ENVIRONMENT: Final = "deployment.environment" """ Deprecated: Replaced by `deployment.environment.name`. """ DEPLOYMENT_ENVIRONMENT_NAME: Final = "deployment.environment.name" """ Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.deployment_attributes.DEPLOYMENT_ENVIRONMENT_NAME`. """ DEPLOYMENT_ID: Final = "deployment.id" """ The ID of the deployment. """ DEPLOYMENT_NAME: Final = "deployment.name" """ The name of the deployment. """ DEPLOYMENT_STATUS: Final = "deployment.status" """ The status of the deployment. """ @deprecated( "Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.deployment_attributes.DeploymentEnvironmentNameValues`." ) class DeploymentEnvironmentNameValues(Enum): PRODUCTION = "production" """Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.deployment_attributes.DeploymentEnvironmentNameValues.PRODUCTION`.""" STAGING = "staging" """Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.deployment_attributes.DeploymentEnvironmentNameValues.STAGING`.""" TEST = "test" """Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.deployment_attributes.DeploymentEnvironmentNameValues.TEST`.""" DEVELOPMENT = "development" """Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.deployment_attributes.DeploymentEnvironmentNameValues.DEVELOPMENT`.""" class DeploymentStatusValues(Enum): FAILED = "failed" """failed.""" SUCCEEDED = "succeeded" """succeeded.""" destination_attributes.py000066400000000000000000000011121527070070700437450ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/src/opentelemetry/semconv/_incubating/attributes# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from typing import Final DESTINATION_ADDRESS: Final = "destination.address" """ Destination address - domain name if available without reverse DNS lookup; otherwise, IP address or UNIX domain socket name. Note: When observed from the source side, and when communicating through an intermediary, `destination.address` SHOULD represent the destination address behind any intermediaries, for example proxies, if it's available. """ DESTINATION_PORT: Final = "destination.port" """ Destination port number. """ device_attributes.py000066400000000000000000000042241527070070700426720ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/src/opentelemetry/semconv/_incubating/attributes# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from typing import Final DEVICE_ID: Final = "device.id" """ A unique identifier representing the device. Note: Its value SHOULD be identical for all apps on a device and it SHOULD NOT change if an app is uninstalled and re-installed. However, it might be resettable by the user for all apps on a device. Hardware IDs (e.g. vendor-specific serial number, IMEI or MAC address) MAY be used as values. More information about Android identifier best practices can be found in the [Android user data IDs guide](https://developer.android.com/training/articles/user-data-ids). > [!WARNING] > > This attribute may contain sensitive (PII) information. Caution should be taken when storing personal data or anything which can identify a user. GDPR and data protection laws may apply, > ensure you do your own due diligence. > > Due to these reasons, this identifier is not recommended for consumer applications and will likely result in rejection from both Google Play and App Store. > However, it may be appropriate for specific enterprise scenarios, such as kiosk devices or enterprise-managed devices, with appropriate compliance clearance. > Any instrumentation providing this identifier MUST implement it as an opt-in feature. > > See [`app.installation.id`](/docs/registry/attributes/app.md#app-installation-id) for a more privacy-preserving alternative. """ DEVICE_MANUFACTURER: Final = "device.manufacturer" """ The name of the device manufacturer. Note: The Android OS provides this field via [Build](https://developer.android.com/reference/android/os/Build#MANUFACTURER). iOS apps SHOULD hardcode the value `Apple`. """ DEVICE_MODEL_IDENTIFIER: Final = "device.model.identifier" """ The model identifier for the device. Note: It's recommended this value represents a machine-readable version of the model identifier rather than the market or consumer-friendly name of the device. """ DEVICE_MODEL_NAME: Final = "device.model.name" """ The marketing name for the device model. Note: It's recommended this value represents a human-readable version of the device model rather than a machine-readable alternative. """ disk_attributes.py000066400000000000000000000005011527070070700423570ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/src/opentelemetry/semconv/_incubating/attributes# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from enum import Enum from typing import Final DISK_IO_DIRECTION: Final = "disk.io.direction" """ The disk IO operation direction. """ class DiskIoDirectionValues(Enum): READ = "read" """read.""" WRITE = "write" """write.""" dns_attributes.py000066400000000000000000000006301527070070700422140ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/src/opentelemetry/semconv/_incubating/attributes# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from typing import Final DNS_ANSWERS: Final = "dns.answers" """ The list of IPv4 or IPv6 addresses resolved during DNS lookup. """ DNS_QUESTION_NAME: Final = "dns.question.name" """ The name being queried. Note: The name represents the queried domain name as it appears in the DNS query without any additional normalization. """ elasticsearch_attributes.py000066400000000000000000000004071527070070700442440ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/src/opentelemetry/semconv/_incubating/attributes# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from typing import Final ELASTICSEARCH_NODE_NAME: Final = "elasticsearch.node.name" """ Represents the human-readable identifier of the node/instance to which a request was routed. """ enduser_attributes.py000066400000000000000000000015661527070070700431060ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/src/opentelemetry/semconv/_incubating/attributes# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from typing import Final ENDUSER_ID: Final = "enduser.id" """ Unique identifier of an end user in the system. It maybe a username, email address, or other identifier. Note: Unique identifier of an end user in the system. > [!Warning] > This field contains sensitive (PII) information. """ ENDUSER_PSEUDO_ID: Final = "enduser.pseudo.id" """ Pseudonymous identifier of an end user. This identifier should be a random value that is not directly linked or associated with the end user's actual identity. Note: Pseudonymous identifier of an end user. > [!Warning] > This field contains sensitive (linkable PII) information. """ ENDUSER_ROLE: Final = "enduser.role" """ Deprecated: Use `user.roles` instead. """ ENDUSER_SCOPE: Final = "enduser.scope" """ Deprecated: Removed, no replacement at this time. """ error_attributes.py000066400000000000000000000014331527070070700425630ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/src/opentelemetry/semconv/_incubating/attributes# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from enum import Enum from typing import Final from typing_extensions import deprecated ERROR_MESSAGE: Final = "error.message" """ Deprecated: Use domain-specific error message attribute. For example, use `feature_flag.error.message` for feature flag errors. """ ERROR_TYPE: Final = "error.type" """ Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.error_attributes.ERROR_TYPE`. """ @deprecated( "Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.error_attributes.ErrorTypeValues`." ) class ErrorTypeValues(Enum): OTHER = "_OTHER" """Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.error_attributes.ErrorTypeValues.OTHER`.""" event_attributes.py000066400000000000000000000004611527070070700425530ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/src/opentelemetry/semconv/_incubating/attributes# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from typing import Final EVENT_NAME: Final = "event.name" """ Deprecated: The value of this attribute MUST now be set as the value of the EventName field on the LogRecord to indicate that the LogRecord represents an Event. """ exception_attributes.py000066400000000000000000000014231527070070700434270ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/src/opentelemetry/semconv/_incubating/attributes# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from typing import Final EXCEPTION_ESCAPED: Final = "exception.escaped" """ Deprecated: It's no longer recommended to record exceptions that are handled and do not escape the scope of a span. """ EXCEPTION_MESSAGE: Final = "exception.message" """ Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.exception_attributes.EXCEPTION_MESSAGE`. """ EXCEPTION_STACKTRACE: Final = "exception.stacktrace" """ Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.exception_attributes.EXCEPTION_STACKTRACE`. """ EXCEPTION_TYPE: Final = "exception.type" """ Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.exception_attributes.EXCEPTION_TYPE`. """ faas_attributes.py000066400000000000000000000130451527070070700423460ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/src/opentelemetry/semconv/_incubating/attributes# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from enum import Enum from typing import Final FAAS_COLDSTART: Final = "faas.coldstart" """ A boolean that is true if the serverless function is executed for the first time (aka cold-start). """ FAAS_CRON: Final = "faas.cron" """ A string containing the schedule period as [Cron Expression](https://docs.oracle.com/cd/E12058_01/doc/doc.1014/e12030/cron_expressions.htm). """ FAAS_DOCUMENT_COLLECTION: Final = "faas.document.collection" """ The name of the source on which the triggering operation was performed. For example, in Cloud Storage or S3 corresponds to the bucket name, and in Cosmos DB to the database name. """ FAAS_DOCUMENT_NAME: Final = "faas.document.name" """ The document name/table subjected to the operation. For example, in Cloud Storage or S3 is the name of the file, and in Cosmos DB the table name. """ FAAS_DOCUMENT_OPERATION: Final = "faas.document.operation" """ Describes the type of the operation that was performed on the data. """ FAAS_DOCUMENT_TIME: Final = "faas.document.time" """ A string containing the time when the data was accessed in the [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format expressed in [UTC](https://www.w3.org/TR/NOTE-datetime). """ FAAS_INSTANCE: Final = "faas.instance" """ The execution environment ID as a string, that will be potentially reused for other invocations to the same function/function version. Note: - **AWS Lambda:** Use the (full) log stream name. """ FAAS_INVOCATION_ID: Final = "faas.invocation_id" """ The invocation ID of the current function invocation. """ FAAS_INVOKED_NAME: Final = "faas.invoked_name" """ The name of the invoked function. Note: SHOULD be equal to the `faas.name` resource attribute of the invoked function. """ FAAS_INVOKED_PROVIDER: Final = "faas.invoked_provider" """ The cloud provider of the invoked function. Note: SHOULD be equal to the `cloud.provider` resource attribute of the invoked function. """ FAAS_INVOKED_REGION: Final = "faas.invoked_region" """ The cloud region of the invoked function. Note: SHOULD be equal to the `cloud.region` resource attribute of the invoked function. """ FAAS_MAX_MEMORY: Final = "faas.max_memory" """ The amount of memory available to the serverless function converted to Bytes. Note: It's recommended to set this attribute since e.g. too little memory can easily stop a Java AWS Lambda function from working correctly. On AWS Lambda, the environment variable `AWS_LAMBDA_FUNCTION_MEMORY_SIZE` provides this information (which must be multiplied by 1,048,576). """ FAAS_NAME: Final = "faas.name" """ The name of the single function that this runtime instance executes. Note: This is the name of the function as configured/deployed on the FaaS platform and is usually different from the name of the callback function (which may be stored in the [`code.namespace`/`code.function.name`](/docs/general/attributes.md#source-code-attributes) span attributes). For some cloud providers, the above definition is ambiguous. The following definition of function name MUST be used for this attribute (and consequently the span name) for the listed cloud providers/products: - **Azure:** The full name `/`, i.e., function app name followed by a forward slash followed by the function name (this form can also be seen in the resource JSON for the function). This means that a span attribute MUST be used, as an Azure function app can host multiple functions that would usually share a TracerProvider (see also the `cloud.resource_id` attribute). """ FAAS_TIME: Final = "faas.time" """ A string containing the function invocation time in the [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format expressed in [UTC](https://www.w3.org/TR/NOTE-datetime). """ FAAS_TRIGGER: Final = "faas.trigger" """ Type of the trigger which caused this function invocation. """ FAAS_VERSION: Final = "faas.version" """ The immutable version of the function being executed. Note: Depending on the cloud provider and platform, use: - **AWS Lambda:** The [function version](https://docs.aws.amazon.com/lambda/latest/dg/configuration-versions.html) (an integer represented as a decimal string). - **Google Cloud Run (Services):** The [revision](https://cloud.google.com/run/docs/managing/revisions) (i.e., the function name plus the revision suffix). - **Google Cloud Functions:** The value of the [`K_REVISION` environment variable](https://cloud.google.com/run/docs/container-contract#services-env-vars). - **Azure Functions:** Not applicable. Do not set this attribute. """ class FaasDocumentOperationValues(Enum): INSERT = "insert" """When a new object is created.""" EDIT = "edit" """When an object is modified.""" DELETE = "delete" """When an object is deleted.""" class FaasInvokedProviderValues(Enum): ALIBABA_CLOUD = "alibaba_cloud" """Alibaba Cloud.""" AWS = "aws" """Amazon Web Services.""" AZURE = "azure" """Microsoft Azure.""" GCP = "gcp" """Google Cloud Platform.""" TENCENT_CLOUD = "tencent_cloud" """Tencent Cloud.""" class FaasTriggerValues(Enum): DATASOURCE = "datasource" """A response to some data source operation such as a database or filesystem read/write.""" HTTP = "http" """To provide an answer to an inbound HTTP request.""" PUBSUB = "pubsub" """A function is set to be executed when messages are sent to a messaging system.""" TIMER = "timer" """A function is scheduled to be executed regularly.""" OTHER = "other" """If none of the others apply.""" feature_flag_attributes.py000066400000000000000000000112471527070070700440620ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/src/opentelemetry/semconv/_incubating/attributes# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from enum import Enum from typing import Final from typing_extensions import deprecated FEATURE_FLAG_CONTEXT_ID: Final = "feature_flag.context.id" """ The unique identifier for the flag evaluation context. For example, the targeting key. """ FEATURE_FLAG_ERROR_MESSAGE: Final = "feature_flag.error.message" """ A message providing more detail about an error that occurred during feature flag evaluation in human-readable form. """ FEATURE_FLAG_EVALUATION_ERROR_MESSAGE: Final = "feature_flag.evaluation.error.message" """ Deprecated: Replaced by `feature_flag.error.message`. """ FEATURE_FLAG_EVALUATION_REASON: Final = "feature_flag.evaluation.reason" """ Deprecated: Replaced by `feature_flag.result.reason`. """ FEATURE_FLAG_KEY: Final = "feature_flag.key" """ The lookup key of the feature flag. """ FEATURE_FLAG_PROVIDER_NAME: Final = "feature_flag.provider.name" """ Identifies the feature flag provider. """ FEATURE_FLAG_RESULT_REASON: Final = "feature_flag.result.reason" """ The reason code which shows how a feature flag value was determined. """ FEATURE_FLAG_RESULT_VALUE: Final = "feature_flag.result.value" """ The evaluated value of the feature flag. Note: With some feature flag providers, feature flag results can be quite large or contain private or sensitive details. Because of this, `feature_flag.result.variant` is often the preferred attribute if it is available. It may be desirable to redact or otherwise limit the size and scope of `feature_flag.result.value` if possible. Because the evaluated flag value is unstructured and may be any type, it is left to the instrumentation author to determine how best to achieve this. """ FEATURE_FLAG_RESULT_VARIANT: Final = "feature_flag.result.variant" """ A semantic identifier for an evaluated flag value. Note: A semantic identifier, commonly referred to as a variant, provides a means for referring to a value without including the value itself. This can provide additional context for understanding the meaning behind a value. For example, the variant `red` maybe be used for the value `#c05543`. """ FEATURE_FLAG_SET_ID: Final = "feature_flag.set.id" """ The identifier of the [flag set](https://openfeature.dev/specification/glossary/#flag-set) to which the feature flag belongs. """ FEATURE_FLAG_VARIANT: Final = "feature_flag.variant" """ Deprecated: Replaced by `feature_flag.result.variant`. """ FEATURE_FLAG_VERSION: Final = "feature_flag.version" """ The version of the ruleset used during the evaluation. This may be any stable value which uniquely identifies the ruleset. """ @deprecated("The attribute feature_flag.evaluation.reason is deprecated - Replaced by `feature_flag.result.reason`") class FeatureFlagEvaluationReasonValues(Enum): STATIC = "static" """The resolved value is static (no dynamic evaluation).""" DEFAULT = "default" """The resolved value fell back to a pre-configured value (no dynamic evaluation occurred or dynamic evaluation yielded no result).""" TARGETING_MATCH = "targeting_match" """The resolved value was the result of a dynamic evaluation, such as a rule or specific user-targeting.""" SPLIT = "split" """The resolved value was the result of pseudorandom assignment.""" CACHED = "cached" """The resolved value was retrieved from cache.""" DISABLED = "disabled" """The resolved value was the result of the flag being disabled in the management system.""" UNKNOWN = "unknown" """The reason for the resolved value could not be determined.""" STALE = "stale" """The resolved value is non-authoritative or possibly out of date.""" ERROR = "error" """The resolved value was the result of an error.""" class FeatureFlagResultReasonValues(Enum): STATIC = "static" """The resolved value is static (no dynamic evaluation).""" DEFAULT = "default" """The resolved value fell back to a pre-configured value (no dynamic evaluation occurred or dynamic evaluation yielded no result).""" TARGETING_MATCH = "targeting_match" """The resolved value was the result of a dynamic evaluation, such as a rule or specific user-targeting.""" SPLIT = "split" """The resolved value was the result of pseudorandom assignment.""" CACHED = "cached" """The resolved value was retrieved from cache.""" DISABLED = "disabled" """The resolved value was the result of the flag being disabled in the management system.""" UNKNOWN = "unknown" """The reason for the resolved value could not be determined.""" STALE = "stale" """The resolved value is non-authoritative or possibly out of date.""" ERROR = "error" """The resolved value was the result of an error.""" file_attributes.py000066400000000000000000000101371527070070700423520ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/src/opentelemetry/semconv/_incubating/attributes# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from enum import Enum from typing import Final FILE_ACCESSED: Final = "file.accessed" """ Time when the file was last accessed, in ISO 8601 format. Note: This attribute might not be supported by some file systems — NFS, FAT32, in embedded OS, etc. """ FILE_ATTRIBUTES: Final = "file.attributes" """ Array of file attributes. Note: Attributes names depend on the OS or file system. Here’s a non-exhaustive list of values expected for this attribute: `archive`, `compressed`, `directory`, `encrypted`, `execute`, `hidden`, `immutable`, `journaled`, `read`, `readonly`, `symbolic link`, `system`, `temporary`, `write`. """ FILE_CHANGED: Final = "file.changed" """ Time when the file attributes or metadata was last changed, in ISO 8601 format. Note: `file.changed` captures the time when any of the file's properties or attributes (including the content) are changed, while `file.modified` captures the timestamp when the file content is modified. """ FILE_CREATED: Final = "file.created" """ Time when the file was created, in ISO 8601 format. Note: This attribute might not be supported by some file systems — NFS, FAT32, in embedded OS, etc. """ FILE_DIRECTORY: Final = "file.directory" """ Directory where the file is located. It should include the drive letter, when appropriate. """ FILE_EXTENSION: Final = "file.extension" """ File extension, excluding the leading dot. Note: When the filename has multiple extensions (example.tar.gz), only the last one should be captured ("gz", not "tar.gz"). """ FILE_FORK_NAME: Final = "file.fork_name" """ Name of the fork. A fork is additional data associated with a filesystem object. Note: On Linux, a resource fork is used to store additional data with a filesystem object. A file always has at least one fork for the data portion, and additional forks may exist. On NTFS, this is analogous to an Alternate Data Stream (ADS), and the default data stream for a file is just called $DATA. Zone.Identifier is commonly used by Windows to track contents downloaded from the internet. An ADS is typically of the form: C:\\path\\to\\filename.extension:some_fork_name, and some_fork_name is the value that should populate `fork_name`. `filename.extension` should populate `file.name`, and `extension` should populate `file.extension`. The full path, `file.path`, will include the fork name. """ FILE_GROUP_ID: Final = "file.group.id" """ Primary Group ID (GID) of the file. """ FILE_GROUP_NAME: Final = "file.group.name" """ Primary group name of the file. """ FILE_INODE: Final = "file.inode" """ Inode representing the file in the filesystem. """ FILE_LOCK_MECHANISM: Final = "file.lock.mechanism" """ The lock mechanism such as noted by [POSIX](https://pubs.opengroup.org/onlinepubs/9699919799/functions/fcntl.html). """ FILE_LOCK_MODE: Final = "file.lock.mode" """ Mode of lock or operation such as documented by [POSIX](https://pubs.opengroup.org/onlinepubs/9699919799/functions/fcntl.html). """ FILE_LOCK_TYPE: Final = "file.lock.type" """ The lock type as represented by i.e. [POSIX](https://pubs.opengroup.org/onlinepubs/9699919799/functions/fcntl.html)'s l_type. """ FILE_MODE: Final = "file.mode" """ Mode of the file in octal representation. """ FILE_MODIFIED: Final = "file.modified" """ Time when the file content was last modified, in ISO 8601 format. """ FILE_NAME: Final = "file.name" """ Name of the file including the extension, without the directory. """ FILE_OWNER_ID: Final = "file.owner.id" """ The user ID (UID) or security identifier (SID) of the file owner. """ FILE_OWNER_NAME: Final = "file.owner.name" """ Username of the file owner. """ FILE_PATH: Final = "file.path" """ Full path to the file, including the filename. It should include the drive letter, when appropriate. """ FILE_SIZE: Final = "file.size" """ File size in bytes. """ FILE_SYMBOLIC_LINK_TARGET_PATH: Final = "file.symbolic_link.target_path" """ Path to the target of a symbolic link. Note: This attribute is only applicable to symbolic links. """ class FileLockTypeValues(Enum): READ = "read" """read.""" WRITE = "write" """write.""" gcp_attributes.py000066400000000000000000000207461527070070700422130ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/src/opentelemetry/semconv/_incubating/attributes# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from enum import Enum from typing import Final GCP_APPHUB_APPLICATION_CONTAINER: Final = "gcp.apphub.application.container" """ The container within GCP where the AppHub application is defined. """ GCP_APPHUB_APPLICATION_ID: Final = "gcp.apphub.application.id" """ The name of the application as configured in AppHub. """ GCP_APPHUB_APPLICATION_LOCATION: Final = "gcp.apphub.application.location" """ The GCP zone or region where the application is defined. """ GCP_APPHUB_SERVICE_CRITICALITY_TYPE: Final = "gcp.apphub.service.criticality_type" """ Criticality of a service indicates its importance to the business. Note: [See AppHub type enum](https://cloud.google.com/app-hub/docs/reference/rest/v1/Attributes#type). """ GCP_APPHUB_SERVICE_ENVIRONMENT_TYPE: Final = "gcp.apphub.service.environment_type" """ Environment of a service is the stage of a software lifecycle. Note: [See AppHub environment type](https://cloud.google.com/app-hub/docs/reference/rest/v1/Attributes#type_1). """ GCP_APPHUB_SERVICE_ID: Final = "gcp.apphub.service.id" """ The name of the service as configured in AppHub. """ GCP_APPHUB_WORKLOAD_CRITICALITY_TYPE: Final = "gcp.apphub.workload.criticality_type" """ Criticality of a workload indicates its importance to the business. Note: [See AppHub type enum](https://cloud.google.com/app-hub/docs/reference/rest/v1/Attributes#type). """ GCP_APPHUB_WORKLOAD_ENVIRONMENT_TYPE: Final = "gcp.apphub.workload.environment_type" """ Environment of a workload is the stage of a software lifecycle. Note: [See AppHub environment type](https://cloud.google.com/app-hub/docs/reference/rest/v1/Attributes#type_1). """ GCP_APPHUB_WORKLOAD_ID: Final = "gcp.apphub.workload.id" """ The name of the workload as configured in AppHub. """ GCP_APPHUB_DESTINATION_APPLICATION_CONTAINER: Final = "gcp.apphub_destination.application.container" """ The container within GCP where the AppHub destination application is defined. """ GCP_APPHUB_DESTINATION_APPLICATION_ID: Final = "gcp.apphub_destination.application.id" """ The name of the destination application as configured in AppHub. """ GCP_APPHUB_DESTINATION_APPLICATION_LOCATION: Final = "gcp.apphub_destination.application.location" """ The GCP zone or region where the destination application is defined. """ GCP_APPHUB_DESTINATION_SERVICE_CRITICALITY_TYPE: Final = "gcp.apphub_destination.service.criticality_type" """ Criticality of a destination workload indicates its importance to the business as specified in [AppHub type enum](https://cloud.google.com/app-hub/docs/reference/rest/v1/Attributes#type). """ GCP_APPHUB_DESTINATION_SERVICE_ENVIRONMENT_TYPE: Final = "gcp.apphub_destination.service.environment_type" """ Software lifecycle stage of a destination service as defined [AppHub environment type](https://cloud.google.com/app-hub/docs/reference/rest/v1/Attributes#type_1). """ GCP_APPHUB_DESTINATION_SERVICE_ID: Final = "gcp.apphub_destination.service.id" """ The name of the destination service as configured in AppHub. """ GCP_APPHUB_DESTINATION_WORKLOAD_CRITICALITY_TYPE: Final = "gcp.apphub_destination.workload.criticality_type" """ Criticality of a destination workload indicates its importance to the business as specified in [AppHub type enum](https://cloud.google.com/app-hub/docs/reference/rest/v1/Attributes#type). """ GCP_APPHUB_DESTINATION_WORKLOAD_ENVIRONMENT_TYPE: Final = "gcp.apphub_destination.workload.environment_type" """ Environment of a destination workload is the stage of a software lifecycle as provided in the [AppHub environment type](https://cloud.google.com/app-hub/docs/reference/rest/v1/Attributes#type_1). """ GCP_APPHUB_DESTINATION_WORKLOAD_ID: Final = "gcp.apphub_destination.workload.id" """ The name of the destination workload as configured in AppHub. """ GCP_CLIENT_SERVICE: Final = "gcp.client.service" """ Identifies the Google Cloud service for which the official client library is intended. Note: Intended to be a stable identifier for Google Cloud client libraries that is uniform across implementation languages. The value should be derived from the canonical service domain for the service; for example, 'foo.googleapis.com' should result in a value of 'foo'. """ GCP_CLOUD_RUN_JOB_EXECUTION: Final = "gcp.cloud_run.job.execution" """ The name of the Cloud Run [execution](https://cloud.google.com/run/docs/managing/job-executions) being run for the Job, as set by the [`CLOUD_RUN_EXECUTION`](https://cloud.google.com/run/docs/container-contract#jobs-env-vars) environment variable. """ GCP_CLOUD_RUN_JOB_TASK_INDEX: Final = "gcp.cloud_run.job.task_index" """ The index for a task within an execution as provided by the [`CLOUD_RUN_TASK_INDEX`](https://cloud.google.com/run/docs/container-contract#jobs-env-vars) environment variable. """ GCP_GCE_INSTANCE_HOSTNAME: Final = "gcp.gce.instance.hostname" """ The hostname of a GCE instance. This is the full value of the default or [custom hostname](https://cloud.google.com/compute/docs/instances/custom-hostname-vm). """ GCP_GCE_INSTANCE_LABELS_TEMPLATE: Final = "gcp.gce.instance.labels" """ GCE instance labels, `` being the label name and the value being the label value. Note: For example, a GCE instance label `team` with value `observability` SHOULD be recorded as the `gcp.gce.instance.labels.team` attribute with value `"observability"`. The `` MUST be the exact GCE instance label key. """ GCP_GCE_INSTANCE_NAME: Final = "gcp.gce.instance.name" """ The instance name of a GCE instance. This is the value provided by `host.name`, the visible name of the instance in the Cloud Console UI, and the prefix for the default hostname of the instance as defined by the [default internal DNS name](https://cloud.google.com/compute/docs/internal-dns#instance-fully-qualified-domain-names). """ GCP_GCE_INSTANCE_GROUP_MANAGER_NAME: Final = "gcp.gce.instance_group_manager.name" """ The name of the Instance Group Manager (IGM) that manages this VM, if any. """ GCP_GCE_INSTANCE_GROUP_MANAGER_REGION: Final = "gcp.gce.instance_group_manager.region" """ The region of a **regional** Instance Group Manager (e.g., `us-central1`). Set this **only** when the IGM is regional. """ GCP_GCE_INSTANCE_GROUP_MANAGER_ZONE: Final = "gcp.gce.instance_group_manager.zone" """ The zone of a **zonal** Instance Group Manager (e.g., `us-central1-a`). Set this **only** when the IGM is zonal. """ class GcpApphubServiceCriticalityTypeValues(Enum): MISSION_CRITICAL = "MISSION_CRITICAL" """Mission critical service.""" HIGH = "HIGH" """High impact.""" MEDIUM = "MEDIUM" """Medium impact.""" LOW = "LOW" """Low impact.""" class GcpApphubServiceEnvironmentTypeValues(Enum): PRODUCTION = "PRODUCTION" """Production environment.""" STAGING = "STAGING" """Staging environment.""" TEST = "TEST" """Test environment.""" DEVELOPMENT = "DEVELOPMENT" """Development environment.""" class GcpApphubWorkloadCriticalityTypeValues(Enum): MISSION_CRITICAL = "MISSION_CRITICAL" """Mission critical service.""" HIGH = "HIGH" """High impact.""" MEDIUM = "MEDIUM" """Medium impact.""" LOW = "LOW" """Low impact.""" class GcpApphubWorkloadEnvironmentTypeValues(Enum): PRODUCTION = "PRODUCTION" """Production environment.""" STAGING = "STAGING" """Staging environment.""" TEST = "TEST" """Test environment.""" DEVELOPMENT = "DEVELOPMENT" """Development environment.""" class GcpApphubDestinationServiceCriticalityTypeValues(Enum): MISSION_CRITICAL = "MISSION_CRITICAL" """Mission critical service.""" HIGH = "HIGH" """High impact.""" MEDIUM = "MEDIUM" """Medium impact.""" LOW = "LOW" """Low impact.""" class GcpApphubDestinationServiceEnvironmentTypeValues(Enum): PRODUCTION = "PRODUCTION" """Production environment.""" STAGING = "STAGING" """Staging environment.""" TEST = "TEST" """Test environment.""" DEVELOPMENT = "DEVELOPMENT" """Development environment.""" class GcpApphubDestinationWorkloadCriticalityTypeValues(Enum): MISSION_CRITICAL = "MISSION_CRITICAL" """Mission critical service.""" HIGH = "HIGH" """High impact.""" MEDIUM = "MEDIUM" """Medium impact.""" LOW = "LOW" """Low impact.""" class GcpApphubDestinationWorkloadEnvironmentTypeValues(Enum): PRODUCTION = "PRODUCTION" """Production environment.""" STAGING = "STAGING" """Staging environment.""" TEST = "TEST" """Test environment.""" DEVELOPMENT = "DEVELOPMENT" """Development environment.""" gen_ai_attributes.py000066400000000000000000000454371527070070700426700ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/src/opentelemetry/semconv/_incubating/attributes# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from enum import Enum from typing import Final from typing_extensions import deprecated GEN_AI_AGENT_DESCRIPTION: Final = "gen_ai.agent.description" """ Deprecated: Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai). """ GEN_AI_AGENT_ID: Final = "gen_ai.agent.id" """ Deprecated: Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai). """ GEN_AI_AGENT_NAME: Final = "gen_ai.agent.name" """ Deprecated: Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai). """ GEN_AI_AGENT_VERSION: Final = "gen_ai.agent.version" """ Deprecated: Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai). """ GEN_AI_COMPLETION: Final = "gen_ai.completion" """ Deprecated: Removed, no replacement at this time. """ GEN_AI_CONVERSATION_ID: Final = "gen_ai.conversation.id" """ Deprecated: Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai). """ GEN_AI_DATA_SOURCE_ID: Final = "gen_ai.data_source.id" """ Deprecated: Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai). """ GEN_AI_EMBEDDINGS_DIMENSION_COUNT: Final = "gen_ai.embeddings.dimension.count" """ Deprecated: Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai). """ GEN_AI_EVALUATION_EXPLANATION: Final = "gen_ai.evaluation.explanation" """ Deprecated: Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai). """ GEN_AI_EVALUATION_NAME: Final = "gen_ai.evaluation.name" """ Deprecated: Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai). """ GEN_AI_EVALUATION_SCORE_LABEL: Final = "gen_ai.evaluation.score.label" """ Deprecated: Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai). """ GEN_AI_EVALUATION_SCORE_VALUE: Final = "gen_ai.evaluation.score.value" """ Deprecated: Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai). """ GEN_AI_INPUT_MESSAGES: Final = "gen_ai.input.messages" """ Deprecated: Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai). """ GEN_AI_OPENAI_REQUEST_RESPONSE_FORMAT: Final = "gen_ai.openai.request.response_format" """ Deprecated: Replaced by `gen_ai.output.type`, which has moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai). """ GEN_AI_OPENAI_REQUEST_SEED: Final = "gen_ai.openai.request.seed" """ Deprecated: Replaced by `gen_ai.request.seed`, which has moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai). """ GEN_AI_OPENAI_REQUEST_SERVICE_TIER: Final = "gen_ai.openai.request.service_tier" """ Deprecated: Replaced by `openai.request.service_tier`, which has moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai). """ GEN_AI_OPENAI_RESPONSE_SERVICE_TIER: Final = "gen_ai.openai.response.service_tier" """ Deprecated: Replaced by `openai.response.service_tier`, which has moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai). """ GEN_AI_OPENAI_RESPONSE_SYSTEM_FINGERPRINT: Final = "gen_ai.openai.response.system_fingerprint" """ Deprecated: Replaced by `openai.response.system_fingerprint`, which has moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai). """ GEN_AI_OPERATION_NAME: Final = "gen_ai.operation.name" """ Deprecated: Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai). """ GEN_AI_OUTPUT_MESSAGES: Final = "gen_ai.output.messages" """ Deprecated: Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai). """ GEN_AI_OUTPUT_TYPE: Final = "gen_ai.output.type" """ Deprecated: Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai). """ GEN_AI_PROMPT: Final = "gen_ai.prompt" """ Deprecated: Removed, no replacement at this time. """ GEN_AI_PROMPT_NAME: Final = "gen_ai.prompt.name" """ Deprecated: Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai). """ GEN_AI_PROVIDER_NAME: Final = "gen_ai.provider.name" """ Deprecated: Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai). """ GEN_AI_REQUEST_CHOICE_COUNT: Final = "gen_ai.request.choice.count" """ Deprecated: Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai). """ GEN_AI_REQUEST_ENCODING_FORMATS: Final = "gen_ai.request.encoding_formats" """ Deprecated: Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai). """ GEN_AI_REQUEST_FREQUENCY_PENALTY: Final = "gen_ai.request.frequency_penalty" """ Deprecated: Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai). """ GEN_AI_REQUEST_MAX_TOKENS: Final = "gen_ai.request.max_tokens" """ Deprecated: Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai). """ GEN_AI_REQUEST_MODEL: Final = "gen_ai.request.model" """ Deprecated: Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai). """ GEN_AI_REQUEST_PRESENCE_PENALTY: Final = "gen_ai.request.presence_penalty" """ Deprecated: Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai). """ GEN_AI_REQUEST_SEED: Final = "gen_ai.request.seed" """ Deprecated: Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai). """ GEN_AI_REQUEST_STOP_SEQUENCES: Final = "gen_ai.request.stop_sequences" """ Deprecated: Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai). """ GEN_AI_REQUEST_STREAM: Final = "gen_ai.request.stream" """ Deprecated: Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai). """ GEN_AI_REQUEST_TEMPERATURE: Final = "gen_ai.request.temperature" """ Deprecated: Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai). """ GEN_AI_REQUEST_TOP_K: Final = "gen_ai.request.top_k" """ Deprecated: Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai). """ GEN_AI_REQUEST_TOP_P: Final = "gen_ai.request.top_p" """ Deprecated: Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai). """ GEN_AI_RESPONSE_FINISH_REASONS: Final = "gen_ai.response.finish_reasons" """ Deprecated: Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai). """ GEN_AI_RESPONSE_ID: Final = "gen_ai.response.id" """ Deprecated: Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai). """ GEN_AI_RESPONSE_MODEL: Final = "gen_ai.response.model" """ Deprecated: Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai). """ GEN_AI_RESPONSE_TIME_TO_FIRST_CHUNK: Final = "gen_ai.response.time_to_first_chunk" """ Deprecated: Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai). """ GEN_AI_RETRIEVAL_DOCUMENTS: Final = "gen_ai.retrieval.documents" """ Deprecated: Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai). """ GEN_AI_RETRIEVAL_QUERY_TEXT: Final = "gen_ai.retrieval.query.text" """ Deprecated: Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai). """ GEN_AI_SYSTEM: Final = "gen_ai.system" """ Deprecated: Replaced by `gen_ai.provider.name`, which has moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai). """ GEN_AI_SYSTEM_INSTRUCTIONS: Final = "gen_ai.system_instructions" """ Deprecated: Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai). """ GEN_AI_TOKEN_TYPE: Final = "gen_ai.token.type" """ Deprecated: Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai). """ GEN_AI_TOOL_CALL_ARGUMENTS: Final = "gen_ai.tool.call.arguments" """ Deprecated: Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai). """ GEN_AI_TOOL_CALL_ID: Final = "gen_ai.tool.call.id" """ Deprecated: Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai). """ GEN_AI_TOOL_CALL_RESULT: Final = "gen_ai.tool.call.result" """ Deprecated: Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai). """ GEN_AI_TOOL_DEFINITIONS: Final = "gen_ai.tool.definitions" """ Deprecated: Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai). """ GEN_AI_TOOL_DESCRIPTION: Final = "gen_ai.tool.description" """ Deprecated: Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai). """ GEN_AI_TOOL_NAME: Final = "gen_ai.tool.name" """ Deprecated: Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai). """ GEN_AI_TOOL_TYPE: Final = "gen_ai.tool.type" """ Deprecated: Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai). """ GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS: Final = "gen_ai.usage.cache_creation.input_tokens" """ Deprecated: Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai). """ GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS: Final = "gen_ai.usage.cache_read.input_tokens" """ Deprecated: Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai). """ GEN_AI_USAGE_COMPLETION_TOKENS: Final = "gen_ai.usage.completion_tokens" """ Deprecated: Replaced by `gen_ai.usage.output_tokens`, which has moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai). """ GEN_AI_USAGE_INPUT_TOKENS: Final = "gen_ai.usage.input_tokens" """ Deprecated: Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai). """ GEN_AI_USAGE_OUTPUT_TOKENS: Final = "gen_ai.usage.output_tokens" """ Deprecated: Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai). """ GEN_AI_USAGE_PROMPT_TOKENS: Final = "gen_ai.usage.prompt_tokens" """ Deprecated: Replaced by `gen_ai.usage.input_tokens`, which has moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai). """ GEN_AI_USAGE_REASONING_OUTPUT_TOKENS: Final = "gen_ai.usage.reasoning.output_tokens" """ Deprecated: Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai). """ GEN_AI_WORKFLOW_NAME: Final = "gen_ai.workflow.name" """ Deprecated: Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai). """ @deprecated( "The attribute gen_ai.openai.request.response_format is deprecated - Replaced by `gen_ai.output.type`, which has moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai)" ) class GenAiOpenaiRequestResponseFormatValues(Enum): TEXT = "text" """Text response format.""" JSON_OBJECT = "json_object" """JSON object response format.""" JSON_SCHEMA = "json_schema" """JSON schema response format.""" @deprecated( "The attribute gen_ai.openai.request.service_tier is deprecated - Replaced by `openai.request.service_tier`, which has moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai)" ) class GenAiOpenaiRequestServiceTierValues(Enum): AUTO = "auto" """The system will utilize scale tier credits until they are exhausted.""" DEFAULT = "default" """The system will utilize the default scale tier.""" @deprecated( "The attribute gen_ai.operation.name is deprecated - Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai)" ) class GenAiOperationNameValues(Enum): CHAT = "chat" """Chat completion operation such as [OpenAI Chat API](https://platform.openai.com/docs/api-reference/chat).""" GENERATE_CONTENT = "generate_content" """Multimodal content generation operation such as [Gemini Generate Content](https://ai.google.dev/api/generate-content).""" TEXT_COMPLETION = "text_completion" """Text completions operation such as [OpenAI Completions API (Legacy)](https://platform.openai.com/docs/api-reference/completions).""" EMBEDDINGS = "embeddings" """Embeddings operation such as [OpenAI Create embeddings API](https://platform.openai.com/docs/api-reference/embeddings/create).""" RETRIEVAL = "retrieval" """Retrieval operation such as [OpenAI Search Vector Store API](https://platform.openai.com/docs/api-reference/vector-stores/search).""" CREATE_AGENT = "create_agent" """Create GenAI agent.""" INVOKE_AGENT = "invoke_agent" """Invoke GenAI agent.""" EXECUTE_TOOL = "execute_tool" """Execute a tool.""" INVOKE_WORKFLOW = "invoke_workflow" """Invoke GenAI workflow.""" @deprecated( "The attribute gen_ai.output.type is deprecated - Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai)" ) class GenAiOutputTypeValues(Enum): TEXT = "text" """Plain text.""" JSON = "json" """JSON object with known or unknown schema.""" IMAGE = "image" """Image.""" SPEECH = "speech" """Speech.""" @deprecated( "The attribute gen_ai.provider.name is deprecated - Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai)" ) class GenAiProviderNameValues(Enum): OPENAI = "openai" """[OpenAI](https://openai.com/).""" GCP_GEN_AI = "gcp.gen_ai" """Any Google generative AI endpoint.""" GCP_VERTEX_AI = "gcp.vertex_ai" """[Vertex AI](https://cloud.google.com/vertex-ai).""" GCP_GEMINI = "gcp.gemini" """[Gemini](https://cloud.google.com/products/gemini).""" ANTHROPIC = "anthropic" """[Anthropic](https://www.anthropic.com/).""" COHERE = "cohere" """[Cohere](https://cohere.com/).""" AZURE_AI_INFERENCE = "azure.ai.inference" """Azure AI Inference.""" AZURE_AI_OPENAI = "azure.ai.openai" """[Azure OpenAI](https://learn.microsoft.com/en-us/azure/ai-services/openai/overview).""" IBM_WATSONX_AI = "ibm.watsonx.ai" """[IBM Watsonx AI](https://www.ibm.com/products/watsonx-ai).""" AWS_BEDROCK = "aws.bedrock" """[AWS Bedrock](https://aws.amazon.com/bedrock).""" PERPLEXITY = "perplexity" """[Perplexity](https://www.perplexity.ai/).""" X_AI = "x_ai" """[xAI](https://x.ai/).""" DEEPSEEK = "deepseek" """[DeepSeek](https://www.deepseek.com/).""" GROQ = "groq" """[Groq](https://groq.com/).""" MISTRAL_AI = "mistral_ai" """[Mistral AI](https://mistral.ai/).""" @deprecated( "The attribute gen_ai.system is deprecated - Replaced by `gen_ai.provider.name`, which has moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai)" ) class GenAiSystemValues(Enum): OPENAI = "openai" """OpenAI.""" GCP_GEN_AI = "gcp.gen_ai" """Any Google generative AI endpoint.""" GCP_VERTEX_AI = "gcp.vertex_ai" """Vertex AI.""" GCP_GEMINI = "gcp.gemini" """Gemini.""" VERTEX_AI = "vertex_ai" """Deprecated: Replaced by `gcp.vertex_ai`.""" GEMINI = "gemini" """Deprecated: Replaced by `gcp.gemini`.""" ANTHROPIC = "anthropic" """Anthropic.""" COHERE = "cohere" """Cohere.""" AZ_AI_INFERENCE = "az.ai.inference" """Deprecated: Replaced by `azure.ai.inference`.""" AZ_AI_OPENAI = "az.ai.openai" """Deprecated: Replaced by `azure.ai.openai`.""" AZURE_AI_INFERENCE = "azure.ai.inference" """Azure AI Inference.""" AZURE_AI_OPENAI = "azure.ai.openai" """Azure OpenAI.""" IBM_WATSONX_AI = "ibm.watsonx.ai" """IBM Watsonx AI.""" AWS_BEDROCK = "aws.bedrock" """AWS Bedrock.""" PERPLEXITY = "perplexity" """Perplexity.""" XAI = "xai" """xAI.""" DEEPSEEK = "deepseek" """DeepSeek.""" GROQ = "groq" """Groq.""" MISTRAL_AI = "mistral_ai" """Mistral AI.""" @deprecated( "The attribute gen_ai.token.type is deprecated - Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai)" ) class GenAiTokenTypeValues(Enum): INPUT = "input" """Input tokens (prompt, input, etc.).""" COMPLETION = "output" """Deprecated: Replaced by `output`.""" OUTPUT = "output" """Output tokens (completion, response, etc.).""" geo_attributes.py000066400000000000000000000026711527070070700422110ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/src/opentelemetry/semconv/_incubating/attributes# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from enum import Enum from typing import Final GEO_CONTINENT_CODE: Final = "geo.continent.code" """ Two-letter code representing continent’s name. """ GEO_COUNTRY_ISO_CODE: Final = "geo.country.iso_code" """ Two-letter ISO Country Code ([ISO 3166-1 alpha2](https://wikipedia.org/wiki/ISO_3166-1#Codes)). """ GEO_LOCALITY_NAME: Final = "geo.locality.name" """ Locality name. Represents the name of a city, town, village, or similar populated place. """ GEO_LOCATION_LAT: Final = "geo.location.lat" """ Latitude of the geo location in [WGS84](https://wikipedia.org/wiki/World_Geodetic_System#WGS84). """ GEO_LOCATION_LON: Final = "geo.location.lon" """ Longitude of the geo location in [WGS84](https://wikipedia.org/wiki/World_Geodetic_System#WGS84). """ GEO_POSTAL_CODE: Final = "geo.postal_code" """ Postal code associated with the location. Values appropriate for this field may also be known as a postcode or ZIP code and will vary widely from country to country. """ GEO_REGION_ISO_CODE: Final = "geo.region.iso_code" """ Region ISO code ([ISO 3166-2](https://wikipedia.org/wiki/ISO_3166-2)). """ class GeoContinentCodeValues(Enum): AF = "AF" """Africa.""" AN = "AN" """Antarctica.""" AS = "AS" """Asia.""" EU = "EU" """Europe.""" NA = "NA" """North America.""" OC = "OC" """Oceania.""" SA = "SA" """South America.""" graphql_attributes.py000066400000000000000000000013371527070070700430730ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/src/opentelemetry/semconv/_incubating/attributes# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from enum import Enum from typing import Final GRAPHQL_DOCUMENT: Final = "graphql.document" """ The GraphQL document being executed. Note: If instrumentation can reliably identify and redact sensitive information it SHOULD do it. """ GRAPHQL_OPERATION_NAME: Final = "graphql.operation.name" """ The name of the operation being executed. """ GRAPHQL_OPERATION_TYPE: Final = "graphql.operation.type" """ The type of the operation being executed. """ class GraphqlOperationTypeValues(Enum): QUERY = "query" """GraphQL query.""" MUTATION = "mutation" """GraphQL mutation.""" SUBSCRIPTION = "subscription" """GraphQL subscription.""" heroku_attributes.py000066400000000000000000000006411527070070700427270ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/src/opentelemetry/semconv/_incubating/attributes# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from typing import Final HEROKU_APP_ID: Final = "heroku.app.id" """ Unique identifier for the application. """ HEROKU_RELEASE_COMMIT: Final = "heroku.release.commit" """ Commit hash for the current release. """ HEROKU_RELEASE_CREATION_TIMESTAMP: Final = "heroku.release.creation_timestamp" """ Time and date the release was created. """ host_attributes.py000066400000000000000000000060351527070070700424120ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/src/opentelemetry/semconv/_incubating/attributes# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from enum import Enum from typing import Final HOST_ARCH: Final = "host.arch" """ The CPU architecture the host system is running on. """ HOST_CPU_CACHE_L2_SIZE: Final = "host.cpu.cache.l2.size" """ The amount of level 2 memory cache available to the processor (in Bytes). """ HOST_CPU_FAMILY: Final = "host.cpu.family" """ Family or generation of the CPU. """ HOST_CPU_MODEL_ID: Final = "host.cpu.model.id" """ Model identifier. It provides more granular information about the CPU, distinguishing it from other CPUs within the same family. """ HOST_CPU_MODEL_NAME: Final = "host.cpu.model.name" """ Model designation of the processor. """ HOST_CPU_STEPPING: Final = "host.cpu.stepping" """ Stepping or core revisions. """ HOST_CPU_VENDOR_ID: Final = "host.cpu.vendor.id" """ Processor manufacturer identifier. A maximum 12-character string. Note: [CPUID](https://wiki.osdev.org/CPUID) command returns the vendor ID string in EBX, EDX and ECX registers. Writing these to memory in this order results in a 12-character string. """ HOST_ID: Final = "host.id" """ Unique host ID. For Cloud, this must be the instance_id assigned by the cloud provider. For non-containerized systems, this should be the `machine-id`. See the table below for the sources to use to determine the `machine-id` based on operating system. """ HOST_IMAGE_ID: Final = "host.image.id" """ VM image ID or host OS image ID. For Cloud, this value is from the provider. """ HOST_IMAGE_NAME: Final = "host.image.name" """ Name of the VM image or OS install the host was instantiated from. """ HOST_IMAGE_VERSION: Final = "host.image.version" """ The version string of the VM image or host OS as defined in [Version Attributes](/docs/resource/README.md#version-attributes). """ HOST_IP: Final = "host.ip" """ Available IP addresses of the host, excluding loopback interfaces. Note: IPv4 Addresses MUST be specified in dotted-quad notation. IPv6 addresses MUST be specified in the [RFC 5952](https://www.rfc-editor.org/rfc/rfc5952.html) format. """ HOST_MAC: Final = "host.mac" """ Available MAC addresses of the host, excluding loopback interfaces. Note: MAC Addresses MUST be represented in [IEEE RA hexadecimal form](https://standards.ieee.org/wp-content/uploads/import/documents/tutorials/eui.pdf): as hyphen-separated octets in uppercase hexadecimal form from most to least significant. """ HOST_NAME: Final = "host.name" """ Name of the host. On UNIX systems, it may contain what the hostname command returns, or the fully qualified hostname, or another name specified by the user. """ HOST_TYPE: Final = "host.type" """ Type of host. For Cloud, this must be the machine type. """ class HostArchValues(Enum): AMD64 = "amd64" """AMD64.""" ARM32 = "arm32" """ARM32.""" ARM64 = "arm64" """ARM64.""" IA64 = "ia64" """Itanium.""" PPC32 = "ppc32" """32-bit PowerPC.""" PPC64 = "ppc64" """64-bit PowerPC.""" S390X = "s390x" """IBM z/Architecture.""" X86 = "x86" """32-bit x86.""" http_attributes.py000066400000000000000000000151301527070070700424100ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/src/opentelemetry/semconv/_incubating/attributes# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from enum import Enum from typing import Final from typing_extensions import deprecated HTTP_CLIENT_IP: Final = "http.client_ip" """ Deprecated: Replaced by `client.address`. """ HTTP_CONNECTION_STATE: Final = "http.connection.state" """ State of the HTTP connection in the HTTP connection pool. """ HTTP_FLAVOR: Final = "http.flavor" """ Deprecated: Split into `network.protocol.name` and `network.protocol.version`. """ HTTP_HOST: Final = "http.host" """ Deprecated: Replaced by one of `server.address`, `client.address` or `http.request.header.host`, depending on the usage. """ HTTP_METHOD: Final = "http.method" """ Deprecated: Replaced by `http.request.method`. """ HTTP_REQUEST_BODY_SIZE: Final = "http.request.body.size" """ The size of the request payload body in bytes. This is the number of bytes transferred excluding headers and is often, but not always, present as the [Content-Length](https://www.rfc-editor.org/rfc/rfc9110.html#field.content-length) header. For requests using transport encoding, this should be the compressed size. """ HTTP_REQUEST_HEADER_TEMPLATE: Final = "http.request.header" """ Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.http_attributes.HTTP_REQUEST_HEADER_TEMPLATE`. """ HTTP_REQUEST_METHOD: Final = "http.request.method" """ Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.http_attributes.HTTP_REQUEST_METHOD`. """ HTTP_REQUEST_METHOD_ORIGINAL: Final = "http.request.method_original" """ Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.http_attributes.HTTP_REQUEST_METHOD_ORIGINAL`. """ HTTP_REQUEST_RESEND_COUNT: Final = "http.request.resend_count" """ Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.http_attributes.HTTP_REQUEST_RESEND_COUNT`. """ HTTP_REQUEST_SIZE: Final = "http.request.size" """ The total size of the request in bytes. This should be the total number of bytes sent over the wire, including the request line (HTTP/1.1), framing (HTTP/2 and HTTP/3), headers, and request body if any. """ HTTP_REQUEST_CONTENT_LENGTH: Final = "http.request_content_length" """ Deprecated: Replaced by `http.request.header.content-length`. """ HTTP_REQUEST_CONTENT_LENGTH_UNCOMPRESSED: Final = "http.request_content_length_uncompressed" """ Deprecated: Replaced by `http.request.body.size`. """ HTTP_RESPONSE_BODY_SIZE: Final = "http.response.body.size" """ The size of the response payload body in bytes. This is the number of bytes transferred excluding headers and is often, but not always, present as the [Content-Length](https://www.rfc-editor.org/rfc/rfc9110.html#field.content-length) header. For requests using transport encoding, this should be the compressed size. """ HTTP_RESPONSE_HEADER_TEMPLATE: Final = "http.response.header" """ Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.http_attributes.HTTP_RESPONSE_HEADER_TEMPLATE`. """ HTTP_RESPONSE_SIZE: Final = "http.response.size" """ The total size of the response in bytes. This should be the total number of bytes sent over the wire, including the status line (HTTP/1.1), framing (HTTP/2 and HTTP/3), headers, and response body and trailers if any. """ HTTP_RESPONSE_STATUS_CODE: Final = "http.response.status_code" """ Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.http_attributes.HTTP_RESPONSE_STATUS_CODE`. """ HTTP_RESPONSE_CONTENT_LENGTH: Final = "http.response_content_length" """ Deprecated: Replaced by `http.response.header.content-length`. """ HTTP_RESPONSE_CONTENT_LENGTH_UNCOMPRESSED: Final = "http.response_content_length_uncompressed" """ Deprecated: Replaced by `http.response.body.size`. """ HTTP_ROUTE: Final = "http.route" """ Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.http_attributes.HTTP_ROUTE`. """ HTTP_SCHEME: Final = "http.scheme" """ Deprecated: Replaced by `url.scheme`. """ HTTP_SERVER_NAME: Final = "http.server_name" """ Deprecated: Replaced by `server.address`. """ HTTP_STATUS_CODE: Final = "http.status_code" """ Deprecated: Replaced by `http.response.status_code`. """ HTTP_TARGET: Final = "http.target" """ Deprecated: Split to `url.path` and `url.query`. """ HTTP_URL: Final = "http.url" """ Deprecated: Replaced by `url.full`. """ HTTP_USER_AGENT: Final = "http.user_agent" """ Deprecated: Replaced by `user_agent.original`. """ class HttpConnectionStateValues(Enum): ACTIVE = "active" """active state.""" IDLE = "idle" """idle state.""" @deprecated( "The attribute http.flavor is deprecated - Split into `network.protocol.name` and `network.protocol.version`" ) class HttpFlavorValues(Enum): HTTP_1_0 = "1.0" """HTTP/1.0.""" HTTP_1_1 = "1.1" """HTTP/1.1.""" HTTP_2_0 = "2.0" """HTTP/2.""" HTTP_3_0 = "3.0" """HTTP/3.""" SPDY = "SPDY" """SPDY protocol.""" QUIC = "QUIC" """QUIC protocol.""" @deprecated( "Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.http_attributes.HttpRequestMethodValues`." ) class HttpRequestMethodValues(Enum): CONNECT = "CONNECT" """Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.http_attributes.HttpRequestMethodValues.CONNECT`.""" DELETE = "DELETE" """Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.http_attributes.HttpRequestMethodValues.DELETE`.""" GET = "GET" """Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.http_attributes.HttpRequestMethodValues.GET`.""" HEAD = "HEAD" """Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.http_attributes.HttpRequestMethodValues.HEAD`.""" OPTIONS = "OPTIONS" """Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.http_attributes.HttpRequestMethodValues.OPTIONS`.""" PATCH = "PATCH" """Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.http_attributes.HttpRequestMethodValues.PATCH`.""" POST = "POST" """Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.http_attributes.HttpRequestMethodValues.POST`.""" PUT = "PUT" """Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.http_attributes.HttpRequestMethodValues.PUT`.""" TRACE = "TRACE" """Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.http_attributes.HttpRequestMethodValues.TRACE`.""" QUERY = "QUERY" """QUERY method.""" OTHER = "_OTHER" """Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.http_attributes.HttpRequestMethodValues.OTHER`.""" hw_attributes.py000066400000000000000000000124001527070070700420440ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/src/opentelemetry/semconv/_incubating/attributes# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from enum import Enum from typing import Final HW_BATTERY_CAPACITY: Final = "hw.battery.capacity" """ Design capacity in Watts-hours or Ampere-hours. """ HW_BATTERY_CHEMISTRY: Final = "hw.battery.chemistry" """ Battery [chemistry](https://schemas.dmtf.org/wbem/cim-html/2.31.0/CIM_Battery.html), e.g. Lithium-Ion, Nickel-Cadmium, etc. """ HW_BATTERY_STATE: Final = "hw.battery.state" """ The current state of the battery. """ HW_BIOS_VERSION: Final = "hw.bios_version" """ BIOS version of the hardware component. """ HW_DRIVER_VERSION: Final = "hw.driver_version" """ Driver version for the hardware component. """ HW_ENCLOSURE_TYPE: Final = "hw.enclosure.type" """ Type of the enclosure (useful for modular systems). """ HW_FIRMWARE_VERSION: Final = "hw.firmware_version" """ Firmware version of the hardware component. """ HW_GPU_TASK: Final = "hw.gpu.task" """ Type of task the GPU is performing. """ HW_ID: Final = "hw.id" """ An identifier for the hardware component, unique within the monitored host. """ HW_LIMIT_TYPE: Final = "hw.limit_type" """ Type of limit for hardware components. """ HW_LOGICAL_DISK_RAID_LEVEL: Final = "hw.logical_disk.raid_level" """ RAID Level of the logical disk. """ HW_LOGICAL_DISK_STATE: Final = "hw.logical_disk.state" """ State of the logical disk space usage. """ HW_MEMORY_TYPE: Final = "hw.memory.type" """ Type of the memory module. """ HW_MODEL: Final = "hw.model" """ Descriptive model name of the hardware component. """ HW_NAME: Final = "hw.name" """ An easily-recognizable name for the hardware component. """ HW_NETWORK_LOGICAL_ADDRESSES: Final = "hw.network.logical_addresses" """ Logical addresses of the adapter (e.g. IP address, or WWPN). """ HW_NETWORK_PHYSICAL_ADDRESS: Final = "hw.network.physical_address" """ Physical address of the adapter (e.g. MAC address, or WWNN). """ HW_PARENT: Final = "hw.parent" """ Unique identifier of the parent component (typically the `hw.id` attribute of the enclosure, or disk controller). """ HW_PHYSICAL_DISK_SMART_ATTRIBUTE: Final = "hw.physical_disk.smart_attribute" """ [S.M.A.R.T.](https://wikipedia.org/wiki/S.M.A.R.T.) (Self-Monitoring, Analysis, and Reporting Technology) attribute of the physical disk. """ HW_PHYSICAL_DISK_STATE: Final = "hw.physical_disk.state" """ State of the physical disk endurance utilization. """ HW_PHYSICAL_DISK_TYPE: Final = "hw.physical_disk.type" """ Type of the physical disk. """ HW_SENSOR_LOCATION: Final = "hw.sensor_location" """ Location of the sensor. """ HW_SERIAL_NUMBER: Final = "hw.serial_number" """ Serial number of the hardware component. """ HW_STATE: Final = "hw.state" """ The current state of the component. """ HW_TAPE_DRIVE_OPERATION_TYPE: Final = "hw.tape_drive.operation_type" """ Type of tape drive operation. """ HW_TYPE: Final = "hw.type" """ Type of the component. Note: Describes the category of the hardware component for which `hw.state` is being reported. For example, `hw.type=temperature` along with `hw.state=degraded` would indicate that the temperature of the hardware component has been reported as `degraded`. """ HW_VENDOR: Final = "hw.vendor" """ Vendor name of the hardware component. """ class HwBatteryStateValues(Enum): CHARGING = "charging" """Charging.""" DISCHARGING = "discharging" """Discharging.""" class HwGpuTaskValues(Enum): DECODER = "decoder" """Decoder.""" ENCODER = "encoder" """Encoder.""" GENERAL = "general" """General.""" class HwLimitTypeValues(Enum): CRITICAL = "critical" """Critical.""" DEGRADED = "degraded" """Degraded.""" HIGH_CRITICAL = "high.critical" """High Critical.""" HIGH_DEGRADED = "high.degraded" """High Degraded.""" LOW_CRITICAL = "low.critical" """Low Critical.""" LOW_DEGRADED = "low.degraded" """Low Degraded.""" MAX = "max" """Maximum.""" THROTTLED = "throttled" """Throttled.""" TURBO = "turbo" """Turbo.""" class HwLogicalDiskStateValues(Enum): USED = "used" """Used.""" FREE = "free" """Free.""" class HwPhysicalDiskStateValues(Enum): REMAINING = "remaining" """Remaining.""" class HwStateValues(Enum): DEGRADED = "degraded" """Degraded.""" FAILED = "failed" """Failed.""" NEEDS_CLEANING = "needs_cleaning" """Needs Cleaning.""" OK = "ok" """OK.""" PREDICTED_FAILURE = "predicted_failure" """Predicted Failure.""" class HwTapeDriveOperationTypeValues(Enum): MOUNT = "mount" """Mount.""" UNMOUNT = "unmount" """Unmount.""" CLEAN = "clean" """Clean.""" class HwTypeValues(Enum): BATTERY = "battery" """Battery.""" CPU = "cpu" """CPU.""" DISK_CONTROLLER = "disk_controller" """Disk controller.""" ENCLOSURE = "enclosure" """Enclosure.""" FAN = "fan" """Fan.""" GPU = "gpu" """GPU.""" LOGICAL_DISK = "logical_disk" """Logical disk.""" MEMORY = "memory" """Memory.""" NETWORK = "network" """Network.""" PHYSICAL_DISK = "physical_disk" """Physical disk.""" POWER_SUPPLY = "power_supply" """Power supply.""" TAPE_DRIVE = "tape_drive" """Tape drive.""" TEMPERATURE = "temperature" """Temperature.""" VOLTAGE = "voltage" """Voltage.""" jsonrpc_attributes.py000066400000000000000000000014341527070070700431110ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/src/opentelemetry/semconv/_incubating/attributes# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from typing import Final JSONRPC_PROTOCOL_VERSION: Final = "jsonrpc.protocol.version" """ Protocol version, as specified in the `jsonrpc` property of the request and its corresponding response. """ JSONRPC_REQUEST_ID: Final = "jsonrpc.request.id" """ A string representation of the `id` property of the request and its corresponding response. Note: Under the [JSON-RPC specification](https://www.jsonrpc.org/specification), the `id` property may be a string, number, null, or omitted entirely. When omitted, the request is treated as a notification. Using `null` is not equivalent to omitting the `id`, but it is discouraged. Instrumentations SHOULD NOT capture this attribute when the `id` is `null` or omitted. """ k8s_attributes.py000066400000000000000000000676331527070070700421550ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/src/opentelemetry/semconv/_incubating/attributes# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from enum import Enum from typing import Final K8S_CLUSTER_NAME: Final = "k8s.cluster.name" """ Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.k8s_attributes.K8S_CLUSTER_NAME`. """ K8S_CLUSTER_UID: Final = "k8s.cluster.uid" """ Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.k8s_attributes.K8S_CLUSTER_UID`. """ K8S_CONTAINER_EPHEMERAL_STORAGE_FS_TYPE: Final = "k8s.container.ephemeral_storage.fs_type" """ The type of file system component for ephemeral storage. Note: Eviction decisions based on ephemeral-storage resource limits are made based on the total container usage. """ K8S_CONTAINER_NAME: Final = "k8s.container.name" """ Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.k8s_attributes.K8S_CONTAINER_NAME`. """ K8S_CONTAINER_RESTART_COUNT: Final = "k8s.container.restart_count" """ Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.k8s_attributes.K8S_CONTAINER_RESTART_COUNT`. """ K8S_CONTAINER_STATUS_LAST_TERMINATED_REASON: Final = "k8s.container.status.last_terminated_reason" """ Last terminated reason of the Container. """ K8S_CONTAINER_STATUS_REASON: Final = "k8s.container.status.reason" """ The reason for the container state. Corresponds to the `reason` field of the: [K8s ContainerStateWaiting](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#containerstatewaiting-v1-core) or [K8s ContainerStateTerminated](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#containerstateterminated-v1-core). """ K8S_CONTAINER_STATUS_STATE: Final = "k8s.container.status.state" """ The state of the container. [K8s ContainerState](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#containerstate-v1-core). """ K8S_CRONJOB_ANNOTATION_TEMPLATE: Final = "k8s.cronjob.annotation" """ Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.k8s_attributes.K8S_CRONJOB_ANNOTATION_TEMPLATE`. """ K8S_CRONJOB_LABEL_TEMPLATE: Final = "k8s.cronjob.label" """ Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.k8s_attributes.K8S_CRONJOB_LABEL_TEMPLATE`. """ K8S_CRONJOB_NAME: Final = "k8s.cronjob.name" """ Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.k8s_attributes.K8S_CRONJOB_NAME`. """ K8S_CRONJOB_UID: Final = "k8s.cronjob.uid" """ Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.k8s_attributes.K8S_CRONJOB_UID`. """ K8S_DAEMONSET_ANNOTATION_TEMPLATE: Final = "k8s.daemonset.annotation" """ Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.k8s_attributes.K8S_DAEMONSET_ANNOTATION_TEMPLATE`. """ K8S_DAEMONSET_LABEL_TEMPLATE: Final = "k8s.daemonset.label" """ Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.k8s_attributes.K8S_DAEMONSET_LABEL_TEMPLATE`. """ K8S_DAEMONSET_NAME: Final = "k8s.daemonset.name" """ Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.k8s_attributes.K8S_DAEMONSET_NAME`. """ K8S_DAEMONSET_UID: Final = "k8s.daemonset.uid" """ Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.k8s_attributes.K8S_DAEMONSET_UID`. """ K8S_DEPLOYMENT_ANNOTATION_TEMPLATE: Final = "k8s.deployment.annotation" """ Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.k8s_attributes.K8S_DEPLOYMENT_ANNOTATION_TEMPLATE`. """ K8S_DEPLOYMENT_LABEL_TEMPLATE: Final = "k8s.deployment.label" """ Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.k8s_attributes.K8S_DEPLOYMENT_LABEL_TEMPLATE`. """ K8S_DEPLOYMENT_NAME: Final = "k8s.deployment.name" """ Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.k8s_attributes.K8S_DEPLOYMENT_NAME`. """ K8S_DEPLOYMENT_UID: Final = "k8s.deployment.uid" """ Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.k8s_attributes.K8S_DEPLOYMENT_UID`. """ K8S_HPA_METRIC_TYPE: Final = "k8s.hpa.metric.type" """ The type of metric source for the horizontal pod autoscaler. Note: This attribute reflects the `type` field of spec.metrics[] in the HPA. """ K8S_HPA_NAME: Final = "k8s.hpa.name" """ The name of the horizontal pod autoscaler. """ K8S_HPA_SCALETARGETREF_API_VERSION: Final = "k8s.hpa.scaletargetref.api_version" """ The API version of the target resource to scale for the HorizontalPodAutoscaler. Note: This maps to the `apiVersion` field in the `scaleTargetRef` of the HPA spec. """ K8S_HPA_SCALETARGETREF_KIND: Final = "k8s.hpa.scaletargetref.kind" """ The kind of the target resource to scale for the HorizontalPodAutoscaler. Note: This maps to the `kind` field in the `scaleTargetRef` of the HPA spec. """ K8S_HPA_SCALETARGETREF_NAME: Final = "k8s.hpa.scaletargetref.name" """ The name of the target resource to scale for the HorizontalPodAutoscaler. Note: This maps to the `name` field in the `scaleTargetRef` of the HPA spec. """ K8S_HPA_UID: Final = "k8s.hpa.uid" """ The UID of the horizontal pod autoscaler. """ K8S_HUGEPAGE_SIZE: Final = "k8s.hugepage.size" """ The size (identifier) of the K8s huge page. """ K8S_JOB_ANNOTATION_TEMPLATE: Final = "k8s.job.annotation" """ Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.k8s_attributes.K8S_JOB_ANNOTATION_TEMPLATE`. """ K8S_JOB_LABEL_TEMPLATE: Final = "k8s.job.label" """ Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.k8s_attributes.K8S_JOB_LABEL_TEMPLATE`. """ K8S_JOB_NAME: Final = "k8s.job.name" """ Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.k8s_attributes.K8S_JOB_NAME`. """ K8S_JOB_UID: Final = "k8s.job.uid" """ Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.k8s_attributes.K8S_JOB_UID`. """ K8S_NAMESPACE_ANNOTATION_TEMPLATE: Final = "k8s.namespace.annotation" """ Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.k8s_attributes.K8S_NAMESPACE_ANNOTATION_TEMPLATE`. """ K8S_NAMESPACE_LABEL_TEMPLATE: Final = "k8s.namespace.label" """ Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.k8s_attributes.K8S_NAMESPACE_LABEL_TEMPLATE`. """ K8S_NAMESPACE_NAME: Final = "k8s.namespace.name" """ Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.k8s_attributes.K8S_NAMESPACE_NAME`. """ K8S_NAMESPACE_PHASE: Final = "k8s.namespace.phase" """ The phase of the K8s namespace. Note: This attribute aligns with the `phase` field of the [K8s NamespaceStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#namespacestatus-v1-core). """ K8S_NODE_ANNOTATION_TEMPLATE: Final = "k8s.node.annotation" """ Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.k8s_attributes.K8S_NODE_ANNOTATION_TEMPLATE`. """ K8S_NODE_CONDITION_STATUS: Final = "k8s.node.condition.status" """ The status of the condition, one of True, False, Unknown. Note: This attribute aligns with the `status` field of the [NodeCondition](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#nodecondition-v1-core). """ K8S_NODE_CONDITION_TYPE: Final = "k8s.node.condition.type" """ The condition type of a K8s Node. Note: K8s Node conditions as described by [K8s documentation](https://kubernetes.io/docs/reference/node/node-status/#condition). This attribute aligns with the `type` field of the [NodeCondition](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#nodecondition-v1-core) The set of possible values is not limited to those listed here. Managed Kubernetes environments, or custom controllers MAY introduce additional node condition types. When this occurs, the exact value as reported by the Kubernetes API SHOULD be used. """ K8S_NODE_LABEL_TEMPLATE: Final = "k8s.node.label" """ Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.k8s_attributes.K8S_NODE_LABEL_TEMPLATE`. """ K8S_NODE_NAME: Final = "k8s.node.name" """ Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.k8s_attributes.K8S_NODE_NAME`. """ K8S_NODE_SYSTEM_CONTAINER_NAME: Final = "k8s.node.system_container.name" """ The name of the system container running on the K8s Node. """ K8S_NODE_UID: Final = "k8s.node.uid" """ Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.k8s_attributes.K8S_NODE_UID`. """ K8S_PERSISTENTVOLUME_ANNOTATION_TEMPLATE: Final = "k8s.persistentvolume.annotation" """ The annotation placed on the PersistentVolume, the `` being the annotation name, the value being the annotation value, even if the value is empty. Note: Examples: - An annotation `pv.kubernetes.io/provisioned-by` with value `kubernetes.io/aws-ebs` SHOULD be recorded as the `k8s.persistentvolume.annotation.pv.kubernetes.io/provisioned-by` attribute with value `"kubernetes.io/aws-ebs"`. - An annotation `data` with empty string value SHOULD be recorded as the `k8s.persistentvolume.annotation.data` attribute with value `""`. """ K8S_PERSISTENTVOLUME_LABEL_TEMPLATE: Final = "k8s.persistentvolume.label" """ The label placed on the PersistentVolume, the `` being the label name, the value being the label value, even if the value is empty. Note: Examples: - A label `type` with value `ssd` SHOULD be recorded as the `k8s.persistentvolume.label.type` attribute with value `"ssd"`. - A label `data` with empty string value SHOULD be recorded as the `k8s.persistentvolume.label.data` attribute with value `""`. """ K8S_PERSISTENTVOLUME_NAME: Final = "k8s.persistentvolume.name" """ The name of the PersistentVolume. """ K8S_PERSISTENTVOLUME_RECLAIM_POLICY: Final = "k8s.persistentvolume.reclaim_policy" """ The reclaim policy of the PersistentVolume. Note: This attribute aligns with the `persistentVolumeReclaimPolicy` field of the [K8s PersistentVolumeSpec](https://kubernetes.io/docs/reference/kubernetes-api/config-and-storage-resources/persistent-volume-v1/#PersistentVolumeSpec). """ K8S_PERSISTENTVOLUME_STATUS_PHASE: Final = "k8s.persistentvolume.status.phase" """ The phase of the PersistentVolume. Note: This attribute aligns with the `phase` field of the [K8s PersistentVolumeStatus](https://kubernetes.io/docs/reference/kubernetes-api/config-and-storage-resources/persistent-volume-v1/#PersistentVolumeStatus). """ K8S_PERSISTENTVOLUME_UID: Final = "k8s.persistentvolume.uid" """ The UID of the PersistentVolume. """ K8S_PERSISTENTVOLUMECLAIM_ANNOTATION_TEMPLATE: Final = "k8s.persistentvolumeclaim.annotation" """ The annotation placed on the PersistentVolumeClaim, the `` being the annotation name, the value being the annotation value, even if the value is empty. Note: Examples: - An annotation `volume.beta.kubernetes.io/storage-provisioner` with value `kubernetes.io/aws-ebs` SHOULD be recorded as the `k8s.persistentvolumeclaim.annotation.volume.beta.kubernetes.io/storage-provisioner` attribute with value `"kubernetes.io/aws-ebs"`. - An annotation `data` with empty string value SHOULD be recorded as the `k8s.persistentvolumeclaim.annotation.data` attribute with value `""`. """ K8S_PERSISTENTVOLUMECLAIM_LABEL_TEMPLATE: Final = "k8s.persistentvolumeclaim.label" """ The label placed on the PersistentVolumeClaim, the `` being the label name, the value being the label value, even if the value is empty. Note: Examples: - A label `app` with value `my-app` SHOULD be recorded as the `k8s.persistentvolumeclaim.label.app` attribute with value `"my-app"`. - A label `data` with empty string value SHOULD be recorded as the `k8s.persistentvolumeclaim.label.data` attribute with value `""`. """ K8S_PERSISTENTVOLUMECLAIM_NAME: Final = "k8s.persistentvolumeclaim.name" """ The name of the PersistentVolumeClaim. """ K8S_PERSISTENTVOLUMECLAIM_STATUS_PHASE: Final = "k8s.persistentvolumeclaim.status.phase" """ The phase of the PersistentVolumeClaim. Note: This attribute aligns with the `phase` field of the [K8s PersistentVolumeClaimStatus](https://kubernetes.io/docs/reference/kubernetes-api/config-and-storage-resources/persistent-volume-claim-v1/#PersistentVolumeClaimStatus). """ K8S_PERSISTENTVOLUMECLAIM_UID: Final = "k8s.persistentvolumeclaim.uid" """ The UID of the PersistentVolumeClaim. """ K8S_POD_ANNOTATION_TEMPLATE: Final = "k8s.pod.annotation" """ Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.k8s_attributes.K8S_POD_ANNOTATION_TEMPLATE`. """ K8S_POD_HOSTNAME: Final = "k8s.pod.hostname" """ Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.k8s_attributes.K8S_POD_HOSTNAME`. """ K8S_POD_IP: Final = "k8s.pod.ip" """ Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.k8s_attributes.K8S_POD_IP`. """ K8S_POD_LABEL_TEMPLATE: Final = "k8s.pod.label" """ Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.k8s_attributes.K8S_POD_LABEL_TEMPLATE`. """ K8S_POD_LABELS_TEMPLATE: Final = "k8s.pod.labels" """ Deprecated: Replaced by `k8s.pod.label`. """ K8S_POD_NAME: Final = "k8s.pod.name" """ Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.k8s_attributes.K8S_POD_NAME`. """ K8S_POD_START_TIME: Final = "k8s.pod.start_time" """ Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.k8s_attributes.K8S_POD_START_TIME`. """ K8S_POD_STATUS_PHASE: Final = "k8s.pod.status.phase" """ The phase for the pod. Corresponds to the `phase` field of the: [K8s PodStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.33/#podstatus-v1-core). """ K8S_POD_STATUS_REASON: Final = "k8s.pod.status.reason" """ The reason for the pod state. Corresponds to the `reason` field of the: [K8s PodStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.33/#podstatus-v1-core). """ K8S_POD_UID: Final = "k8s.pod.uid" """ Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.k8s_attributes.K8S_POD_UID`. """ K8S_REPLICASET_ANNOTATION_TEMPLATE: Final = "k8s.replicaset.annotation" """ Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.k8s_attributes.K8S_REPLICASET_ANNOTATION_TEMPLATE`. """ K8S_REPLICASET_LABEL_TEMPLATE: Final = "k8s.replicaset.label" """ Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.k8s_attributes.K8S_REPLICASET_LABEL_TEMPLATE`. """ K8S_REPLICASET_NAME: Final = "k8s.replicaset.name" """ Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.k8s_attributes.K8S_REPLICASET_NAME`. """ K8S_REPLICASET_UID: Final = "k8s.replicaset.uid" """ Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.k8s_attributes.K8S_REPLICASET_UID`. """ K8S_REPLICATIONCONTROLLER_NAME: Final = "k8s.replicationcontroller.name" """ The name of the replication controller. """ K8S_REPLICATIONCONTROLLER_UID: Final = "k8s.replicationcontroller.uid" """ The UID of the replication controller. """ K8S_RESOURCEQUOTA_NAME: Final = "k8s.resourcequota.name" """ The name of the resource quota. """ K8S_RESOURCEQUOTA_RESOURCE_NAME: Final = "k8s.resourcequota.resource_name" """ The name of the K8s resource a resource quota defines. Note: The value for this attribute can be either the full `count/[.]` string (e.g., count/deployments.apps, count/pods), or, for certain core Kubernetes resources, just the resource name (e.g., pods, services, configmaps). Both forms are supported by Kubernetes for object count quotas. See [Kubernetes Resource Quotas documentation](https://kubernetes.io/docs/concepts/policy/resource-quotas/#quota-on-object-count) for more details. """ K8S_RESOURCEQUOTA_UID: Final = "k8s.resourcequota.uid" """ The UID of the resource quota. """ K8S_SERVICE_ANNOTATION_TEMPLATE: Final = "k8s.service.annotation" """ The annotation placed on the Service, the `` being the annotation name, the value being the annotation value, even if the value is empty. Note: Examples: - An annotation `prometheus.io/scrape` with value `true` SHOULD be recorded as the `k8s.service.annotation.prometheus.io/scrape` attribute with value `"true"`. - An annotation `data` with empty string value SHOULD be recorded as the `k8s.service.annotation.data` attribute with value `""`. """ K8S_SERVICE_ENDPOINT_ADDRESS_TYPE: Final = "k8s.service.endpoint.address_type" """ The address type of the service endpoint. Note: The network address family or type of the endpoint. This attribute aligns with the `addressType` field of the [K8s EndpointSlice](https://kubernetes.io/docs/reference/kubernetes-api/service-resources/endpoint-slice-v1/). It is used to differentiate metrics when a Service is backed by multiple address types (e.g., in dual-stack clusters). """ K8S_SERVICE_ENDPOINT_CONDITION: Final = "k8s.service.endpoint.condition" """ The condition of the service endpoint. Note: The current operational condition of the service endpoint. An endpoint can have multiple conditions set at once (e.g., both `serving` and `terminating` during rollout). This attribute aligns with the condition fields in the [K8s EndpointSlice](https://kubernetes.io/docs/reference/kubernetes-api/service-resources/endpoint-slice-v1/). """ K8S_SERVICE_ENDPOINT_ZONE: Final = "k8s.service.endpoint.zone" """ The zone of the service endpoint. Note: The zone where the endpoint is located, typically corresponding to a failure domain. This attribute aligns with the `zone` field of endpoints in the [K8s EndpointSlice](https://kubernetes.io/docs/reference/kubernetes-api/service-resources/endpoint-slice-v1/). It enables zone-aware monitoring of service endpoint distribution and supports features like [Topology Aware Routing](https://kubernetes.io/docs/concepts/services-networking/topology-aware-routing/). If the zone is not populated (e.g., nodes without the `topology.kubernetes.io/zone` label), the attribute value will be an empty string. """ K8S_SERVICE_LABEL_TEMPLATE: Final = "k8s.service.label" """ The label placed on the Service, the `` being the label name, the value being the label value, even if the value is empty. Note: Examples: - A label `app` with value `my-service` SHOULD be recorded as the `k8s.service.label.app` attribute with value `"my-service"`. - A label `data` with empty string value SHOULD be recorded as the `k8s.service.label.data` attribute with value `""`. """ K8S_SERVICE_NAME: Final = "k8s.service.name" """ The name of the Service. """ K8S_SERVICE_PUBLISH_NOT_READY_ADDRESSES: Final = "k8s.service.publish_not_ready_addresses" """ Whether the Service publishes not-ready endpoints. Note: Whether the Service is configured to publish endpoints before the pods are ready. This attribute is typically used to indicate that a Service (such as a headless Service for a StatefulSet) allows peer discovery before pods pass their readiness probes. It aligns with the `publishNotReadyAddresses` field of the [K8s ServiceSpec](https://kubernetes.io/docs/reference/kubernetes-api/service-resources/service-v1/#ServiceSpec). """ K8S_SERVICE_SELECTOR_TEMPLATE: Final = "k8s.service.selector" """ The selector key-value pair placed on the Service, the `` being the selector key, the value being the selector value. Note: These selectors are used to correlate with pod labels. Each selector key-value pair becomes a separate attribute. Examples: - A selector `app=my-app` SHOULD be recorded as the `k8s.service.selector.app` attribute with value `"my-app"`. - A selector `version=v1` SHOULD be recorded as the `k8s.service.selector.version` attribute with value `"v1"`. """ K8S_SERVICE_TRAFFIC_DISTRIBUTION: Final = "k8s.service.traffic_distribution" """ The traffic distribution policy for the Service. Note: Specifies how traffic is distributed to endpoints for this Service. This attribute aligns with the `trafficDistribution` field of the [K8s ServiceSpec](https://kubernetes.io/docs/reference/networking/virtual-ips/#traffic-distribution). Known values include `PreferSameZone` (prefer endpoints in the same zone as the client) and `PreferSameNode` (prefer endpoints on the same node, fallback to same zone, then cluster-wide). If this field is not set on the Service, the attribute SHOULD NOT be emitted. When not set, Kubernetes distributes traffic evenly across all endpoints cluster-wide. """ K8S_SERVICE_TYPE: Final = "k8s.service.type" """ The type of the Kubernetes Service. Note: This attribute aligns with the `type` field of the [K8s ServiceSpec](https://kubernetes.io/docs/reference/kubernetes-api/service-resources/service-v1/#ServiceSpec). """ K8S_SERVICE_UID: Final = "k8s.service.uid" """ The UID of the Service. """ K8S_STATEFULSET_ANNOTATION_TEMPLATE: Final = "k8s.statefulset.annotation" """ Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.k8s_attributes.K8S_STATEFULSET_ANNOTATION_TEMPLATE`. """ K8S_STATEFULSET_LABEL_TEMPLATE: Final = "k8s.statefulset.label" """ Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.k8s_attributes.K8S_STATEFULSET_LABEL_TEMPLATE`. """ K8S_STATEFULSET_NAME: Final = "k8s.statefulset.name" """ Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.k8s_attributes.K8S_STATEFULSET_NAME`. """ K8S_STATEFULSET_UID: Final = "k8s.statefulset.uid" """ Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.k8s_attributes.K8S_STATEFULSET_UID`. """ K8S_STORAGECLASS_NAME: Final = "k8s.storageclass.name" """ The name of K8s [StorageClass](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#storageclass-v1-storage-k8s-io) object. """ K8S_VOLUME_NAME: Final = "k8s.volume.name" """ The name of the K8s volume. """ K8S_VOLUME_TYPE: Final = "k8s.volume.type" """ The type of the K8s volume. """ class K8sContainerEphemeralStorageFsTypeValues(Enum): ROOTFS = "rootfs" """For the container's writable layer usage.""" LOGS = "logs" """For the container's log files usage (stdout/stderr).""" class K8sContainerStatusReasonValues(Enum): CONTAINER_CREATING = "ContainerCreating" """The container is being created.""" CRASH_LOOP_BACK_OFF = "CrashLoopBackOff" """The container is in a crash loop back off state.""" CREATE_CONTAINER_CONFIG_ERROR = "CreateContainerConfigError" """There was an error creating the container configuration.""" ERR_IMAGE_PULL = "ErrImagePull" """There was an error pulling the container image.""" IMAGE_PULL_BACK_OFF = "ImagePullBackOff" """The container image pull is in back off state.""" OOM_KILLED = "OOMKilled" """The container was killed due to out of memory.""" COMPLETED = "Completed" """The container has completed execution.""" ERROR = "Error" """There was an error with the container.""" CONTAINER_CANNOT_RUN = "ContainerCannotRun" """The container cannot run.""" class K8sContainerStatusStateValues(Enum): TERMINATED = "terminated" """The container has terminated.""" RUNNING = "running" """The container is running.""" WAITING = "waiting" """The container is waiting.""" class K8sNamespacePhaseValues(Enum): ACTIVE = "active" """Active namespace phase as described by [K8s API](https://pkg.go.dev/k8s.io/api@v0.31.3/core/v1#NamespacePhase).""" TERMINATING = "terminating" """Terminating namespace phase as described by [K8s API](https://pkg.go.dev/k8s.io/api@v0.31.3/core/v1#NamespacePhase).""" class K8sNodeConditionStatusValues(Enum): CONDITION_TRUE = "true" """condition_true.""" CONDITION_FALSE = "false" """condition_false.""" CONDITION_UNKNOWN = "unknown" """condition_unknown.""" class K8sNodeConditionTypeValues(Enum): READY = "Ready" """The node is healthy and ready to accept pods.""" DISK_PRESSURE = "DiskPressure" """Pressure exists on the disk size—that is, if the disk capacity is low.""" MEMORY_PRESSURE = "MemoryPressure" """Pressure exists on the node memory—that is, if the node memory is low.""" PID_PRESSURE = "PIDPressure" """Pressure exists on the processes—that is, if there are too many processes on the node.""" NETWORK_UNAVAILABLE = "NetworkUnavailable" """The network for the node is not correctly configured.""" class K8sPersistentvolumeReclaimPolicyValues(Enum): DELETE = "Delete" """The volume will be deleted when released from its claim.""" RECYCLE = "Recycle" """The volume will be recycled (basic scrub) when released from its claim.""" RETAIN = "Retain" """The volume will be retained when released from its claim.""" class K8sPersistentvolumeStatusPhaseValues(Enum): AVAILABLE = "Available" """The volume is available and not yet bound to a claim.""" BOUND = "Bound" """The volume is bound to a claim.""" FAILED = "Failed" """The volume has failed its automatic reclamation.""" PENDING = "Pending" """The volume is being provisioned.""" RELEASED = "Released" """The claim has been deleted but the volume is not yet available.""" class K8sPersistentvolumeclaimStatusPhaseValues(Enum): BOUND = "Bound" """The claim is bound to a volume.""" LOST = "Lost" """The claim has lost its underlying volume (the volume does not exist anymore).""" PENDING = "Pending" """The claim has not yet been bound to a volume.""" class K8sPodStatusPhaseValues(Enum): PENDING = "Pending" """The pod has been accepted by the system, but one or more of the containers has not been started. This includes time before being bound to a node, as well as time spent pulling images onto the host.""" RUNNING = "Running" """The pod has been bound to a node and all of the containers have been started. At least one container is still running or is in the process of being restarted.""" SUCCEEDED = "Succeeded" """All containers in the pod have voluntarily terminated with a container exit code of 0, and the system is not going to restart any of these containers.""" FAILED = "Failed" """All containers in the pod have terminated, and at least one container has terminated in a failure (exited with a non-zero exit code or was stopped by the system).""" UNKNOWN = "Unknown" """For some reason the state of the pod could not be obtained, typically due to an error in communicating with the host of the pod.""" class K8sPodStatusReasonValues(Enum): EVICTED = "Evicted" """The pod is evicted.""" NODE_AFFINITY = "NodeAffinity" """The pod is in a status because of its node affinity.""" NODE_LOST = "NodeLost" """The reason on a pod when its state cannot be confirmed as kubelet is unresponsive on the node it is (was) running.""" SHUTDOWN = "Shutdown" """The node is shutdown.""" UNEXPECTED_ADMISSION_ERROR = "UnexpectedAdmissionError" """The pod was rejected admission to the node because of an error during admission that could not be categorized.""" class K8sServiceEndpointAddressTypeValues(Enum): IPV4 = "IPv4" """IPv4 address type.""" IPV6 = "IPv6" """IPv6 address type.""" FQDN = "FQDN" """FQDN address type.""" class K8sServiceEndpointConditionValues(Enum): READY = "ready" """The endpoint is ready to receive new connections.""" SERVING = "serving" """The endpoint is currently handling traffic.""" TERMINATING = "terminating" """The endpoint is in the process of shutting down.""" class K8sServiceTypeValues(Enum): CLUSTER_IP = "ClusterIP" """ClusterIP service type.""" NODE_PORT = "NodePort" """NodePort service type.""" LOAD_BALANCER = "LoadBalancer" """LoadBalancer service type.""" EXTERNAL_NAME = "ExternalName" """ExternalName service type.""" class K8sVolumeTypeValues(Enum): PERSISTENT_VOLUME_CLAIM = "persistentVolumeClaim" """A [persistentVolumeClaim](https://kubernetes.io/docs/concepts/storage/volumes/#persistentvolumeclaim) volume.""" CONFIG_MAP = "configMap" """A [configMap](https://kubernetes.io/docs/concepts/storage/volumes/#configmap) volume.""" DOWNWARD_API = "downwardAPI" """A [downwardAPI](https://kubernetes.io/docs/concepts/storage/volumes/#downwardapi) volume.""" EMPTY_DIR = "emptyDir" """An [emptyDir](https://kubernetes.io/docs/concepts/storage/volumes/#emptydir) volume.""" SECRET = "secret" """A [secret](https://kubernetes.io/docs/concepts/storage/volumes/#secret) volume.""" LOCAL = "local" """A [local](https://kubernetes.io/docs/concepts/storage/volumes/#local) volume.""" linux_attributes.py000066400000000000000000000010641527070070700425710ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/src/opentelemetry/semconv/_incubating/attributes# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from enum import Enum from typing import Final from typing_extensions import deprecated LINUX_MEMORY_SLAB_STATE: Final = "linux.memory.slab.state" """ Deprecated: Replaced by `system.memory.linux.slab.state`. """ @deprecated("The attribute linux.memory.slab.state is deprecated - Replaced by `system.memory.linux.slab.state`") class LinuxMemorySlabStateValues(Enum): RECLAIMABLE = "reclaimable" """reclaimable.""" UNRECLAIMABLE = "unreclaimable" """unreclaimable.""" log_attributes.py000066400000000000000000000030551527070070700422150ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/src/opentelemetry/semconv/_incubating/attributes# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from enum import Enum from typing import Final LOG_FILE_NAME: Final = "log.file.name" """ The basename of the file. """ LOG_FILE_NAME_RESOLVED: Final = "log.file.name_resolved" """ The basename of the file, with symlinks resolved. """ LOG_FILE_PATH: Final = "log.file.path" """ The full path to the file. """ LOG_FILE_PATH_RESOLVED: Final = "log.file.path_resolved" """ The full path to the file, with symlinks resolved. """ LOG_IOSTREAM: Final = "log.iostream" """ The stream associated with the log. See below for a list of well-known values. """ LOG_RECORD_ORIGINAL: Final = "log.record.original" """ The complete original Log Record. Note: This value MAY be added when processing a Log Record which was originally transmitted as a string or equivalent data type AND the Body field of the Log Record does not contain the same value. (e.g. a syslog or a log record read from a file.). """ LOG_RECORD_UID: Final = "log.record.uid" """ A unique identifier for the Log Record. Note: If an ID is provided, other log records with the same ID will be considered duplicates and can be removed safely. This means, that two distinguishable log records MUST have different values. The ID MAY be an [Universally Unique Lexicographically Sortable Identifier (ULID)](https://github.com/ulid/spec), but other identifiers (e.g. UUID) may be used as needed. """ class LogIostreamValues(Enum): STDOUT = "stdout" """Logs from stdout stream.""" STDERR = "stderr" """Events from stderr stream.""" mainframe_attributes.py000066400000000000000000000003701527070070700433700ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/src/opentelemetry/semconv/_incubating/attributes# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from typing import Final MAINFRAME_LPAR_NAME: Final = "mainframe.lpar.name" """ Name of the logical partition that hosts a systems with a mainframe operating system. """ mcp_attributes.py000066400000000000000000000074571527070070700422250ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/src/opentelemetry/semconv/_incubating/attributes# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from enum import Enum from typing import Final from typing_extensions import deprecated MCP_METHOD_NAME: Final = "mcp.method.name" """ Deprecated: Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai). """ MCP_PROTOCOL_VERSION: Final = "mcp.protocol.version" """ Deprecated: Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai). """ MCP_RESOURCE_URI: Final = "mcp.resource.uri" """ Deprecated: Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai). """ MCP_SESSION_ID: Final = "mcp.session.id" """ Deprecated: Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai). """ @deprecated( "The attribute mcp.method.name is deprecated - Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai)" ) class McpMethodNameValues(Enum): NOTIFICATIONS_CANCELLED = "notifications/cancelled" """Notification cancelling a previously-issued request.""" INITIALIZE = "initialize" """Request to initialize the MCP client.""" NOTIFICATIONS_INITIALIZED = "notifications/initialized" """Notification indicating that the MCP client has been initialized.""" NOTIFICATIONS_PROGRESS = "notifications/progress" """Notification indicating the progress for a long-running operation.""" PING = "ping" """Request to check that the other party is still alive.""" RESOURCES_LIST = "resources/list" """Request to list resources available on server.""" RESOURCES_TEMPLATES_LIST = "resources/templates/list" """Request to list resource templates available on server.""" RESOURCES_READ = "resources/read" """Request to read a resource.""" NOTIFICATIONS_RESOURCES_LIST_CHANGED = "notifications/resources/list_changed" """Notification indicating that the list of resources has changed.""" RESOURCES_SUBSCRIBE = "resources/subscribe" """Request to subscribe to a resource.""" RESOURCES_UNSUBSCRIBE = "resources/unsubscribe" """Request to unsubscribe from resource updates.""" NOTIFICATIONS_RESOURCES_UPDATED = "notifications/resources/updated" """Notification indicating that a resource has been updated.""" PROMPTS_LIST = "prompts/list" """Request to list prompts available on server.""" PROMPTS_GET = "prompts/get" """Request to get a prompt.""" NOTIFICATIONS_PROMPTS_LIST_CHANGED = "notifications/prompts/list_changed" """Notification indicating that the list of prompts has changed.""" TOOLS_LIST = "tools/list" """Request to list tools available on server.""" TOOLS_CALL = "tools/call" """Request to call a tool.""" NOTIFICATIONS_TOOLS_LIST_CHANGED = "notifications/tools/list_changed" """Notification indicating that the list of tools has changed.""" LOGGING_SET_LEVEL = "logging/setLevel" """Request to set the logging level.""" NOTIFICATIONS_MESSAGE = "notifications/message" """Notification indicating that a message has been received.""" SAMPLING_CREATE_MESSAGE = "sampling/createMessage" """Request to create a sampling message.""" COMPLETION_COMPLETE = "completion/complete" """Request to complete a prompt.""" ROOTS_LIST = "roots/list" """Request to list roots available on server.""" NOTIFICATIONS_ROOTS_LIST_CHANGED = "notifications/roots/list_changed" """Notification indicating that the list of roots has changed.""" ELICITATION_CREATE = "elicitation/create" """Request from the server to elicit additional information from the user via the client.""" message_attributes.py000066400000000000000000000014611527070070700430570ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/src/opentelemetry/semconv/_incubating/attributes# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from enum import Enum from typing import Final from typing_extensions import deprecated MESSAGE_COMPRESSED_SIZE: Final = "message.compressed_size" """ Deprecated: Deprecated, no replacement at this time. """ MESSAGE_ID: Final = "message.id" """ Deprecated: Deprecated, no replacement at this time. """ MESSAGE_TYPE: Final = "message.type" """ Deprecated: Deprecated, no replacement at this time. """ MESSAGE_UNCOMPRESSED_SIZE: Final = "message.uncompressed_size" """ Deprecated: Deprecated, no replacement at this time. """ @deprecated("The attribute message.type is deprecated - Deprecated, no replacement at this time") class MessageTypeValues(Enum): SENT = "SENT" """sent.""" RECEIVED = "RECEIVED" """received.""" messaging_attributes.py000066400000000000000000000303011527070070700434030ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/src/opentelemetry/semconv/_incubating/attributes# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from enum import Enum from typing import Final MESSAGING_BATCH_MESSAGE_COUNT: Final = "messaging.batch.message_count" """ The number of messages sent, received, or processed in the scope of the batching operation. Note: Instrumentations SHOULD NOT set `messaging.batch.message_count` on spans that operate with a single message. When a messaging client library supports both batch and single-message API for the same operation, instrumentations SHOULD use `messaging.batch.message_count` for batching APIs and SHOULD NOT use it for single-message APIs. """ MESSAGING_CLIENT_ID: Final = "messaging.client.id" """ A unique identifier for the client that consumes or produces a message. """ MESSAGING_CONSUMER_GROUP_NAME: Final = "messaging.consumer.group.name" """ The name of the consumer group with which a consumer is associated. Note: Semantic conventions for individual messaging systems SHOULD document whether `messaging.consumer.group.name` is applicable and what it means in the context of that system. """ MESSAGING_DESTINATION_ANONYMOUS: Final = "messaging.destination.anonymous" """ A boolean that is true if the message destination is anonymous (could be unnamed or have auto-generated name). """ MESSAGING_DESTINATION_NAME: Final = "messaging.destination.name" """ The message destination name. Note: SHOULD uniquely identify a specific queue, topic or other entity within the broker. If the broker doesn't have such notion, it SHOULD uniquely identify the broker. """ MESSAGING_DESTINATION_PARTITION_ID: Final = "messaging.destination.partition.id" """ The identifier of the partition messages are sent to or received from, unique within the `messaging.destination.name`. """ MESSAGING_DESTINATION_SUBSCRIPTION_NAME: Final = "messaging.destination.subscription.name" """ The name of the destination subscription from which a message is consumed. Note: Semantic conventions for individual messaging systems SHOULD document whether `messaging.destination.subscription.name` is applicable and what it means in the context of that system. """ MESSAGING_DESTINATION_TEMPLATE: Final = "messaging.destination.template" """ Low cardinality representation of the messaging destination name. Note: Destination names could be constructed from templates. An example would be a destination name involving a username or product ID. Although the destination name in this case is of high cardinality, the underlying template is of low cardinality and can be effectively used for grouping and aggregation. """ MESSAGING_DESTINATION_TEMPORARY: Final = "messaging.destination.temporary" """ A boolean that is true if the message destination is temporary and might not exist anymore after messages are processed. """ MESSAGING_DESTINATION_PUBLISH_ANONYMOUS: Final = "messaging.destination_publish.anonymous" """ Deprecated: Removed. No replacement at this time. """ MESSAGING_DESTINATION_PUBLISH_NAME: Final = "messaging.destination_publish.name" """ Deprecated: Removed. No replacement at this time. """ MESSAGING_EVENTHUBS_CONSUMER_GROUP: Final = "messaging.eventhubs.consumer.group" """ Deprecated: Replaced by `messaging.consumer.group.name`. """ MESSAGING_EVENTHUBS_MESSAGE_ENQUEUED_TIME: Final = "messaging.eventhubs.message.enqueued_time" """ The UTC epoch seconds at which the message has been accepted and stored in the entity. """ MESSAGING_GCP_PUBSUB_MESSAGE_ACK_DEADLINE: Final = "messaging.gcp_pubsub.message.ack_deadline" """ The ack deadline in seconds set for the modify ack deadline request. """ MESSAGING_GCP_PUBSUB_MESSAGE_ACK_ID: Final = "messaging.gcp_pubsub.message.ack_id" """ The ack ID for a given message. """ MESSAGING_GCP_PUBSUB_MESSAGE_DELIVERY_ATTEMPT: Final = "messaging.gcp_pubsub.message.delivery_attempt" """ The delivery attempt for a given message. """ MESSAGING_GCP_PUBSUB_MESSAGE_ORDERING_KEY: Final = "messaging.gcp_pubsub.message.ordering_key" """ The ordering key for a given message. If the attribute is not present, the message does not have an ordering key. """ MESSAGING_KAFKA_CLUSTER_ID: Final = "messaging.kafka.cluster.id" """ The Kafka cluster ID, obtained from the broker metadata exposed through the Kafka client (or AdminClient) API. Note: The cluster ID is a unique identifier reported by the Kafka broker. It identifies the cluster independently of the individual brokers the client is configured to connect to, and remains stable even if broker hostnames, IP addresses, or ports change. """ MESSAGING_KAFKA_CONSUMER_GROUP: Final = "messaging.kafka.consumer.group" """ Deprecated: Replaced by `messaging.consumer.group.name`. """ MESSAGING_KAFKA_DESTINATION_PARTITION: Final = "messaging.kafka.destination.partition" """ Deprecated: Record string representation of the partition ID in `messaging.destination.partition.id` attribute. """ MESSAGING_KAFKA_MESSAGE_KEY: Final = "messaging.kafka.message.key" """ Message keys in Kafka are used for grouping alike messages to ensure they're processed on the same partition. They differ from `messaging.message.id` in that they're not unique. If the key is `null`, the attribute MUST NOT be set. Note: If the key type is not string, it's string representation has to be supplied for the attribute. If the key has no unambiguous, canonical string form, don't include its value. """ MESSAGING_KAFKA_MESSAGE_OFFSET: Final = "messaging.kafka.message.offset" """ Deprecated: Replaced by `messaging.kafka.offset`. """ MESSAGING_KAFKA_MESSAGE_TOMBSTONE: Final = "messaging.kafka.message.tombstone" """ A boolean that is true if the message is a tombstone. """ MESSAGING_KAFKA_OFFSET: Final = "messaging.kafka.offset" """ The offset of a record in the corresponding Kafka partition. """ MESSAGING_MESSAGE_BODY_SIZE: Final = "messaging.message.body.size" """ The size of the message body in bytes. Note: This can refer to both the compressed or uncompressed body size. If both sizes are known, the uncompressed body size should be used. """ MESSAGING_MESSAGE_CONVERSATION_ID: Final = "messaging.message.conversation_id" """ The conversation ID identifying the conversation to which the message belongs, represented as a string. Sometimes called "Correlation ID". """ MESSAGING_MESSAGE_ENVELOPE_SIZE: Final = "messaging.message.envelope.size" """ The size of the message body and metadata in bytes. Note: This can refer to both the compressed or uncompressed size. If both sizes are known, the uncompressed size should be used. """ MESSAGING_MESSAGE_ID: Final = "messaging.message.id" """ A value used by the messaging system as an identifier for the message, represented as a string. """ MESSAGING_OPERATION: Final = "messaging.operation" """ Deprecated: Replaced by `messaging.operation.type`. """ MESSAGING_OPERATION_NAME: Final = "messaging.operation.name" """ The system-specific name of the messaging operation. """ MESSAGING_OPERATION_TYPE: Final = "messaging.operation.type" """ A string identifying the type of the messaging operation. Note: If a custom value is used, it MUST be of low cardinality. """ MESSAGING_RABBITMQ_DESTINATION_ROUTING_KEY: Final = "messaging.rabbitmq.destination.routing_key" """ RabbitMQ message routing key. """ MESSAGING_RABBITMQ_MESSAGE_DELIVERY_TAG: Final = "messaging.rabbitmq.message.delivery_tag" """ RabbitMQ message delivery tag. """ MESSAGING_ROCKETMQ_CLIENT_GROUP: Final = "messaging.rocketmq.client_group" """ Deprecated: Replaced by `messaging.consumer.group.name` on the consumer spans. No replacement for producer spans. """ MESSAGING_ROCKETMQ_CONSUMPTION_MODEL: Final = "messaging.rocketmq.consumption_model" """ Model of message consumption. This only applies to consumer spans. """ MESSAGING_ROCKETMQ_MESSAGE_DELAY_TIME_LEVEL: Final = "messaging.rocketmq.message.delay_time_level" """ The delay time level for delay message, which determines the message delay time. """ MESSAGING_ROCKETMQ_MESSAGE_DELIVERY_TIMESTAMP: Final = "messaging.rocketmq.message.delivery_timestamp" """ The timestamp in milliseconds that the delay message is expected to be delivered to consumer. """ MESSAGING_ROCKETMQ_MESSAGE_GROUP: Final = "messaging.rocketmq.message.group" """ It is essential for FIFO message. Messages that belong to the same message group are always processed one by one within the same consumer group. """ MESSAGING_ROCKETMQ_MESSAGE_KEYS: Final = "messaging.rocketmq.message.keys" """ Key(s) of message, another way to mark message besides message ID. """ MESSAGING_ROCKETMQ_MESSAGE_TAG: Final = "messaging.rocketmq.message.tag" """ The secondary classifier of message besides topic. """ MESSAGING_ROCKETMQ_MESSAGE_TYPE: Final = "messaging.rocketmq.message.type" """ Type of message. """ MESSAGING_ROCKETMQ_NAMESPACE: Final = "messaging.rocketmq.namespace" """ Namespace of RocketMQ resources, resources in different namespaces are individual. """ MESSAGING_SERVICEBUS_DESTINATION_SUBSCRIPTION_NAME: Final = "messaging.servicebus.destination.subscription_name" """ Deprecated: Replaced by `messaging.destination.subscription.name`. """ MESSAGING_SERVICEBUS_DISPOSITION_STATUS: Final = "messaging.servicebus.disposition_status" """ Describes the [settlement type](https://learn.microsoft.com/azure/service-bus-messaging/message-transfers-locks-settlement#peeklock). """ MESSAGING_SERVICEBUS_MESSAGE_DELIVERY_COUNT: Final = "messaging.servicebus.message.delivery_count" """ Number of deliveries that have been attempted for this message. """ MESSAGING_SERVICEBUS_MESSAGE_ENQUEUED_TIME: Final = "messaging.servicebus.message.enqueued_time" """ The UTC epoch seconds at which the message has been accepted and stored in the entity. """ MESSAGING_SYSTEM: Final = "messaging.system" """ The messaging system as identified by the client instrumentation. Note: The actual messaging system may differ from the one known by the client. For example, when using Kafka client libraries to communicate with Azure Event Hubs, the `messaging.system` is set to `kafka` based on the instrumentation's best knowledge. """ class MessagingOperationTypeValues(Enum): CREATE = "create" """A message is created. "Create" spans always refer to a single message and are used to provide a unique creation context for messages in batch sending scenarios.""" SEND = "send" """One or more messages are provided for sending to an intermediary. If a single message is sent, the context of the "Send" span can be used as the creation context and no "Create" span needs to be created.""" RECEIVE = "receive" """One or more messages are requested by a consumer. This operation refers to pull-based scenarios, where consumers explicitly call methods of messaging SDKs to receive messages.""" PROCESS = "process" """One or more messages are processed by a consumer.""" SETTLE = "settle" """One or more messages are settled.""" DELIVER = "deliver" """Deprecated: Replaced by `process`.""" PUBLISH = "publish" """Deprecated: Replaced by `send`.""" class MessagingRocketmqConsumptionModelValues(Enum): CLUSTERING = "clustering" """Clustering consumption model.""" BROADCASTING = "broadcasting" """Broadcasting consumption model.""" class MessagingRocketmqMessageTypeValues(Enum): NORMAL = "normal" """Normal message.""" FIFO = "fifo" """FIFO message.""" DELAY = "delay" """Delay message.""" TRANSACTION = "transaction" """Transaction message.""" class MessagingServicebusDispositionStatusValues(Enum): COMPLETE = "complete" """Message is completed.""" ABANDON = "abandon" """Message is abandoned.""" DEAD_LETTER = "dead_letter" """Message is sent to dead letter queue.""" DEFER = "defer" """Message is deferred.""" class MessagingSystemValues(Enum): ACTIVEMQ = "activemq" """Apache ActiveMQ.""" AWS_SNS = "aws.sns" """Amazon Simple Notification Service (SNS).""" AWS_SQS = "aws_sqs" """Amazon Simple Queue Service (SQS).""" EVENTGRID = "eventgrid" """Azure Event Grid.""" EVENTHUBS = "eventhubs" """Azure Event Hubs.""" SERVICEBUS = "servicebus" """Azure Service Bus.""" GCP_PUBSUB = "gcp_pubsub" """Google Cloud Pub/Sub.""" JMS = "jms" """Java Message Service.""" KAFKA = "kafka" """Apache Kafka.""" RABBITMQ = "rabbitmq" """RabbitMQ.""" ROCKETMQ = "rocketmq" """Apache RocketMQ.""" PULSAR = "pulsar" """Apache Pulsar.""" net_attributes.py000066400000000000000000000045651527070070700422310ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/src/opentelemetry/semconv/_incubating/attributes# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from enum import Enum from typing import Final from typing_extensions import deprecated NET_HOST_IP: Final = "net.host.ip" """ Deprecated: Replaced by `network.local.address`. """ NET_HOST_NAME: Final = "net.host.name" """ Deprecated: Replaced by `server.address`. """ NET_HOST_PORT: Final = "net.host.port" """ Deprecated: Replaced by `server.port`. """ NET_PEER_IP: Final = "net.peer.ip" """ Deprecated: Replaced by `network.peer.address`. """ NET_PEER_NAME: Final = "net.peer.name" """ Deprecated: Replaced by `server.address` on client spans and `client.address` on server spans. """ NET_PEER_PORT: Final = "net.peer.port" """ Deprecated: Replaced by `server.port` on client spans and `client.port` on server spans. """ NET_PROTOCOL_NAME: Final = "net.protocol.name" """ Deprecated: Replaced by `network.protocol.name`. """ NET_PROTOCOL_VERSION: Final = "net.protocol.version" """ Deprecated: Replaced by `network.protocol.version`. """ NET_SOCK_FAMILY: Final = "net.sock.family" """ Deprecated: Split to `network.transport` and `network.type`. """ NET_SOCK_HOST_ADDR: Final = "net.sock.host.addr" """ Deprecated: Replaced by `network.local.address`. """ NET_SOCK_HOST_PORT: Final = "net.sock.host.port" """ Deprecated: Replaced by `network.local.port`. """ NET_SOCK_PEER_ADDR: Final = "net.sock.peer.addr" """ Deprecated: Replaced by `network.peer.address`. """ NET_SOCK_PEER_NAME: Final = "net.sock.peer.name" """ Deprecated: Removed. No replacement at this time. """ NET_SOCK_PEER_PORT: Final = "net.sock.peer.port" """ Deprecated: Replaced by `network.peer.port`. """ NET_TRANSPORT: Final = "net.transport" """ Deprecated: Replaced by `network.transport`. """ @deprecated("The attribute net.sock.family is deprecated - Split to `network.transport` and `network.type`") class NetSockFamilyValues(Enum): INET = "inet" """IPv4 address.""" INET6 = "inet6" """IPv6 address.""" UNIX = "unix" """UNIX domain socket path.""" @deprecated("The attribute net.transport is deprecated - Replaced by `network.transport`") class NetTransportValues(Enum): IP_TCP = "ip_tcp" """ip_tcp.""" IP_UDP = "ip_udp" """ip_udp.""" PIPE = "pipe" """Named or anonymous pipe.""" INPROC = "inproc" """In-process communication.""" OTHER = "other" """Something else (non IP-based).""" network_attributes.py000066400000000000000000000140231527070070700431220ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/src/opentelemetry/semconv/_incubating/attributes# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from enum import Enum from typing import Final from typing_extensions import deprecated NETWORK_CARRIER_ICC: Final = "network.carrier.icc" """ The ISO 3166-1 alpha-2 2-character country code associated with the mobile carrier network. """ NETWORK_CARRIER_MCC: Final = "network.carrier.mcc" """ The mobile carrier country code. """ NETWORK_CARRIER_MNC: Final = "network.carrier.mnc" """ The mobile carrier network code. """ NETWORK_CARRIER_NAME: Final = "network.carrier.name" """ The name of the mobile carrier. """ NETWORK_CONNECTION_STATE: Final = "network.connection.state" """ The state of network connection. Note: Connection states are defined as part of the [rfc9293](https://datatracker.ietf.org/doc/html/rfc9293#section-3.3.2). """ NETWORK_CONNECTION_SUBTYPE: Final = "network.connection.subtype" """ This describes more details regarding the connection.type. It may be the type of cell technology connection, but it could be used for describing details about a Wi-Fi connection. """ NETWORK_CONNECTION_TYPE: Final = "network.connection.type" """ The internet connection type. """ NETWORK_INTERFACE_NAME: Final = "network.interface.name" """ The network interface name. """ NETWORK_IO_DIRECTION: Final = "network.io.direction" """ The direction of traffic from the perspective of the observing host's physical or virtual network interface. It should not be used to represent the logical direction of a stateful connection or network flow. """ NETWORK_LOCAL_ADDRESS: Final = "network.local.address" """ Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.network_attributes.NETWORK_LOCAL_ADDRESS`. """ NETWORK_LOCAL_PORT: Final = "network.local.port" """ Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.network_attributes.NETWORK_LOCAL_PORT`. """ NETWORK_PEER_ADDRESS: Final = "network.peer.address" """ Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.network_attributes.NETWORK_PEER_ADDRESS`. """ NETWORK_PEER_PORT: Final = "network.peer.port" """ Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.network_attributes.NETWORK_PEER_PORT`. """ NETWORK_PROTOCOL_NAME: Final = "network.protocol.name" """ Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.network_attributes.NETWORK_PROTOCOL_NAME`. """ NETWORK_PROTOCOL_VERSION: Final = "network.protocol.version" """ Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.network_attributes.NETWORK_PROTOCOL_VERSION`. """ NETWORK_TRANSPORT: Final = "network.transport" """ Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.network_attributes.NETWORK_TRANSPORT`. """ NETWORK_TYPE: Final = "network.type" """ Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.network_attributes.NETWORK_TYPE`. """ class NetworkConnectionStateValues(Enum): CLOSED = "closed" """closed.""" CLOSE_WAIT = "close_wait" """close_wait.""" CLOSING = "closing" """closing.""" ESTABLISHED = "established" """established.""" FIN_WAIT_1 = "fin_wait_1" """fin_wait_1.""" FIN_WAIT_2 = "fin_wait_2" """fin_wait_2.""" LAST_ACK = "last_ack" """last_ack.""" LISTEN = "listen" """listen.""" SYN_RECEIVED = "syn_received" """syn_received.""" SYN_SENT = "syn_sent" """syn_sent.""" TIME_WAIT = "time_wait" """time_wait.""" class NetworkConnectionSubtypeValues(Enum): GPRS = "gprs" """GPRS.""" EDGE = "edge" """EDGE.""" UMTS = "umts" """UMTS.""" CDMA = "cdma" """CDMA.""" EVDO_0 = "evdo_0" """EVDO Rel. 0.""" EVDO_A = "evdo_a" """EVDO Rev. A.""" CDMA2000_1XRTT = "cdma2000_1xrtt" """CDMA2000 1XRTT.""" HSDPA = "hsdpa" """HSDPA.""" HSUPA = "hsupa" """HSUPA.""" HSPA = "hspa" """HSPA.""" IDEN = "iden" """IDEN.""" EVDO_B = "evdo_b" """EVDO Rev. B.""" LTE = "lte" """LTE.""" EHRPD = "ehrpd" """EHRPD.""" HSPAP = "hspap" """HSPAP.""" GSM = "gsm" """GSM.""" TD_SCDMA = "td_scdma" """TD-SCDMA.""" IWLAN = "iwlan" """IWLAN.""" NR = "nr" """5G NR (New Radio).""" NRNSA = "nrnsa" """5G NRNSA (New Radio Non-Standalone).""" LTE_CA = "lte_ca" """LTE CA.""" class NetworkConnectionTypeValues(Enum): WIFI = "wifi" """Wi-Fi.""" WIRED = "wired" """wired.""" CELL = "cell" """cell.""" UNAVAILABLE = "unavailable" """unavailable.""" UNKNOWN = "unknown" """unknown.""" class NetworkIoDirectionValues(Enum): TRANSMIT = "transmit" """transmit.""" RECEIVE = "receive" """receive.""" @deprecated( "Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.network_attributes.NetworkTransportValues`." ) class NetworkTransportValues(Enum): TCP = "tcp" """Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.network_attributes.NetworkTransportValues.TCP`.""" UDP = "udp" """Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.network_attributes.NetworkTransportValues.UDP`.""" PIPE = "pipe" """Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.network_attributes.NetworkTransportValues.PIPE`.""" UNIX = "unix" """Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.network_attributes.NetworkTransportValues.UNIX`.""" QUIC = "quic" """Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.network_attributes.NetworkTransportValues.QUIC`.""" @deprecated( "Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.network_attributes.NetworkTypeValues`." ) class NetworkTypeValues(Enum): IPV4 = "ipv4" """Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.network_attributes.NetworkTypeValues.IPV4`.""" IPV6 = "ipv6" """Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.network_attributes.NetworkTypeValues.IPV6`.""" nfs_attributes.py000066400000000000000000000005771527070070700422300ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/src/opentelemetry/semconv/_incubating/attributes# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from typing import Final NFS_OPERATION_NAME: Final = "nfs.operation.name" """ NFSv4+ operation name. """ NFS_SERVER_REPCACHE_STATUS: Final = "nfs.server.repcache.status" """ Linux: one of "hit" (NFSD_STATS_RC_HITS), "miss" (NFSD_STATS_RC_MISSES), or "nocache" (NFSD_STATS_RC_NOCACHE -- uncacheable). """ oci_attributes.py000066400000000000000000000012331527070070700422020ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/src/opentelemetry/semconv/_incubating/attributes# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from typing import Final OCI_MANIFEST_DIGEST: Final = "oci.manifest.digest" """ The digest of the OCI image manifest. For container images specifically is the digest by which the container image is known. Note: Follows [OCI Image Manifest Specification](https://github.com/opencontainers/image-spec/blob/main/manifest.md), and specifically the [Digest property](https://github.com/opencontainers/image-spec/blob/main/descriptor.md#digests). An example can be found in [Example Image Manifest](https://github.com/opencontainers/image-spec/blob/main/manifest.md#example-image-manifest). """ onc_rpc_attributes.py000066400000000000000000000007211527070070700430540ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/src/opentelemetry/semconv/_incubating/attributes# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from typing import Final ONC_RPC_PROCEDURE_NAME: Final = "onc_rpc.procedure.name" """ ONC/Sun RPC procedure name. """ ONC_RPC_PROCEDURE_NUMBER: Final = "onc_rpc.procedure.number" """ ONC/Sun RPC procedure number. """ ONC_RPC_PROGRAM_NAME: Final = "onc_rpc.program.name" """ ONC/Sun RPC program name. """ ONC_RPC_VERSION: Final = "onc_rpc.version" """ ONC/Sun RPC program version. """ openai_attributes.py000066400000000000000000000036761527070070700427200ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/src/opentelemetry/semconv/_incubating/attributes# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from enum import Enum from typing import Final from typing_extensions import deprecated OPENAI_API_TYPE: Final = "openai.api.type" """ Deprecated: Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai). """ OPENAI_REQUEST_SERVICE_TIER: Final = "openai.request.service_tier" """ Deprecated: Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai). """ OPENAI_RESPONSE_SERVICE_TIER: Final = "openai.response.service_tier" """ Deprecated: Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai). """ OPENAI_RESPONSE_SYSTEM_FINGERPRINT: Final = "openai.response.system_fingerprint" """ Deprecated: Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai). """ @deprecated( "The attribute openai.api.type is deprecated - Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai)" ) class OpenaiApiTypeValues(Enum): CHAT_COMPLETIONS = "chat_completions" """The OpenAI [Chat Completions API](https://developers.openai.com/api/reference/chat-completions/overview).""" RESPONSES = "responses" """The OpenAI [Responses API](https://developers.openai.com/api/reference/responses/overview).""" @deprecated( "The attribute openai.request.service_tier is deprecated - Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai)" ) class OpenaiRequestServiceTierValues(Enum): AUTO = "auto" """The system will utilize scale tier credits until they are exhausted.""" DEFAULT = "default" """The system will utilize the default scale tier.""" openshift_attributes.py000066400000000000000000000004711527070070700434320ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/src/opentelemetry/semconv/_incubating/attributes# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from typing import Final OPENSHIFT_CLUSTERQUOTA_NAME: Final = "openshift.clusterquota.name" """ The name of the cluster quota. """ OPENSHIFT_CLUSTERQUOTA_UID: Final = "openshift.clusterquota.uid" """ The UID of the cluster quota. """ opentracing_attributes.py000066400000000000000000000010341527070070700437400ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/src/opentelemetry/semconv/_incubating/attributes# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from enum import Enum from typing import Final OPENTRACING_REF_TYPE: Final = "opentracing.ref_type" """ Parent-child Reference type. Note: The causal relationship between a child Span and a parent Span. """ class OpentracingRefTypeValues(Enum): CHILD_OF = "child_of" """The parent Span depends on the child Span in some capacity.""" FOLLOWS_FROM = "follows_from" """The parent Span doesn't depend in any way on the result of the child Span.""" oracle_attributes.py000066400000000000000000000041451527070070700427020ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/src/opentelemetry/semconv/_incubating/attributes# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from typing import Final ORACLE_DB_DOMAIN: Final = "oracle.db.domain" """ The database domain associated with the connection. Note: This attribute SHOULD be set to the value of the `DB_DOMAIN` initialization parameter, as exposed in `v$parameter`. `DB_DOMAIN` defines the domain portion of the global database name and SHOULD be configured when a database is, or may become, part of a distributed environment. Its value consists of one or more valid identifiers (alphanumeric ASCII characters) separated by periods. """ ORACLE_DB_INSTANCE_NAME: Final = "oracle.db.instance.name" """ The instance name associated with the connection in an Oracle Real Application Clusters environment. Note: There can be multiple instances associated with a single database service. It indicates the unique instance name to which the connection is currently bound. For non-RAC databases, this value defaults to the `oracle.db.name`. """ ORACLE_DB_NAME: Final = "oracle.db.name" """ The database name associated with the connection. Note: This attribute SHOULD be set to the value of the parameter `DB_NAME` exposed in `v$parameter`. """ ORACLE_DB_PDB: Final = "oracle.db.pdb" """ The pluggable database (PDB) name associated with the connection. Note: This attribute SHOULD reflect the PDB that the session is currently connected to. If instrumentation cannot reliably obtain the active PDB name for each operation without issuing an additional query (such as `SELECT SYS_CONTEXT`), it is RECOMMENDED to fall back to the PDB name specified at connection establishment. """ ORACLE_DB_SERVICE: Final = "oracle.db.service" """ The service name currently associated with the database connection. Note: The effective service name for a connection can change during its lifetime, for example after executing SQL, `ALTER SESSION`. If an instrumentation cannot reliably obtain the current service name for each operation without issuing an additional query (such as `SELECT SYS_CONTEXT`), it is RECOMMENDED to fall back to the service name originally provided at connection establishment. """ oracle_cloud_attributes.py000066400000000000000000000005721527070070700440700ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/src/opentelemetry/semconv/_incubating/attributes# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from typing import Final ORACLE_CLOUD_REALM: Final = "oracle_cloud.realm" """ The OCI realm identifier that indicates the isolated partition in which the tenancy and its resources reside. Note: See [OCI documentation on realms](https://docs.oracle.com/iaas/Content/General/Concepts/regions.htm). """ os_attributes.py000066400000000000000000000025061527070070700420550ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/src/opentelemetry/semconv/_incubating/attributes# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from enum import Enum from typing import Final OS_BUILD_ID: Final = "os.build_id" """ Unique identifier for a particular build or compilation of the operating system. """ OS_DESCRIPTION: Final = "os.description" """ Human readable (not intended to be parsed) OS version information, like e.g. reported by `ver` or `lsb_release -a` commands. """ OS_NAME: Final = "os.name" """ Human readable operating system name. """ OS_TYPE: Final = "os.type" """ The operating system type. """ OS_VERSION: Final = "os.version" """ The version string of the operating system as defined in [Version Attributes](/docs/resource/README.md#version-attributes). """ class OsTypeValues(Enum): WINDOWS = "windows" """Microsoft Windows.""" LINUX = "linux" """Linux.""" DARWIN = "darwin" """Apple Darwin.""" FREEBSD = "freebsd" """FreeBSD.""" NETBSD = "netbsd" """NetBSD.""" OPENBSD = "openbsd" """OpenBSD.""" DRAGONFLYBSD = "dragonflybsd" """DragonFly BSD.""" HPUX = "hpux" """HP-UX (Hewlett Packard UNIX).""" AIX = "aix" """AIX (Advanced Interactive eXecutive).""" SOLARIS = "solaris" """SunOS, Oracle Solaris.""" Z_OS = "z_os" """Deprecated: Replaced by `zos`.""" ZOS = "zos" """IBM z/OS.""" otel_attributes.py000066400000000000000000000143661527070070700424060ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/src/opentelemetry/semconv/_incubating/attributes# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from enum import Enum from typing import Final from typing_extensions import deprecated OTEL_COMPONENT_NAME: Final = "otel.component.name" """ A name uniquely identifying the instance of the OpenTelemetry component within its containing SDK instance. Note: Implementations SHOULD ensure a low cardinality for this attribute, even across application or SDK restarts. E.g. implementations MUST NOT use UUIDs as values for this attribute. Implementations MAY achieve these goals by following a `/` pattern, e.g. `batching_span_processor/0`. Hereby `otel.component.type` refers to the corresponding attribute value of the component. The value of `instance-counter` MAY be automatically assigned by the component and uniqueness within the enclosing SDK instance MUST be guaranteed. For example, `` MAY be implemented by using a monotonically increasing counter (starting with `0`), which is incremented every time an instance of the given component type is started. With this implementation, for example the first Batching Span Processor would have `batching_span_processor/0` as `otel.component.name`, the second one `batching_span_processor/1` and so on. These values will therefore be reused in the case of an application restart. """ OTEL_COMPONENT_TYPE: Final = "otel.component.type" """ A name identifying the type of the OpenTelemetry component. Note: If none of the standardized values apply, implementations SHOULD use the language-defined name of the type. E.g. for Java the fully qualified classname SHOULD be used in this case. """ OTEL_EVENT_NAME: Final = "otel.event.name" """ Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.otel_attributes.OTEL_EVENT_NAME`. """ OTEL_LIBRARY_NAME: Final = "otel.library.name" """ Deprecated: Replaced by `otel.scope.name`. """ OTEL_LIBRARY_VERSION: Final = "otel.library.version" """ Deprecated: Replaced by `otel.scope.version`. """ OTEL_SCOPE_NAME: Final = "otel.scope.name" """ Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.otel_attributes.OTEL_SCOPE_NAME`. """ OTEL_SCOPE_SCHEMA_URL: Final = "otel.scope.schema_url" """ The schema URL of the instrumentation scope. """ OTEL_SCOPE_VERSION: Final = "otel.scope.version" """ Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.otel_attributes.OTEL_SCOPE_VERSION`. """ OTEL_SPAN_PARENT_ORIGIN: Final = "otel.span.parent.origin" """ Determines whether the span has a parent span, and if so, [whether it is a remote parent](https://opentelemetry.io/docs/specs/otel/trace/api/#isremote). """ OTEL_SPAN_SAMPLING_RESULT: Final = "otel.span.sampling_result" """ The result value of the sampler for this span. """ OTEL_STATUS_CODE: Final = "otel.status_code" """ Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.otel_attributes.OTEL_STATUS_CODE`. """ OTEL_STATUS_DESCRIPTION: Final = "otel.status_description" """ Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.otel_attributes.OTEL_STATUS_DESCRIPTION`. """ class OtelComponentTypeValues(Enum): BATCHING_SPAN_PROCESSOR = "batching_span_processor" """The built-in SDK batching span processor.""" SIMPLE_SPAN_PROCESSOR = "simple_span_processor" """The built-in SDK simple span processor.""" BATCHING_LOG_PROCESSOR = "batching_log_processor" """The built-in SDK batching log record processor.""" SIMPLE_LOG_PROCESSOR = "simple_log_processor" """The built-in SDK simple log record processor.""" OTLP_GRPC_SPAN_EXPORTER = "otlp_grpc_span_exporter" """OTLP span exporter over gRPC with protobuf serialization.""" OTLP_HTTP_SPAN_EXPORTER = "otlp_http_span_exporter" """OTLP span exporter over HTTP with protobuf serialization.""" OTLP_HTTP_JSON_SPAN_EXPORTER = "otlp_http_json_span_exporter" """OTLP span exporter over HTTP with JSON serialization.""" ZIPKIN_HTTP_SPAN_EXPORTER = "zipkin_http_span_exporter" """Zipkin span exporter over HTTP.""" OTLP_GRPC_LOG_EXPORTER = "otlp_grpc_log_exporter" """OTLP log record exporter over gRPC with protobuf serialization.""" OTLP_HTTP_LOG_EXPORTER = "otlp_http_log_exporter" """OTLP log record exporter over HTTP with protobuf serialization.""" OTLP_HTTP_JSON_LOG_EXPORTER = "otlp_http_json_log_exporter" """OTLP log record exporter over HTTP with JSON serialization.""" PERIODIC_METRIC_READER = "periodic_metric_reader" """The built-in SDK periodically exporting metric reader.""" OTLP_GRPC_METRIC_EXPORTER = "otlp_grpc_metric_exporter" """OTLP metric exporter over gRPC with protobuf serialization.""" OTLP_HTTP_METRIC_EXPORTER = "otlp_http_metric_exporter" """OTLP metric exporter over HTTP with protobuf serialization.""" OTLP_HTTP_JSON_METRIC_EXPORTER = "otlp_http_json_metric_exporter" """OTLP metric exporter over HTTP with JSON serialization.""" PROMETHEUS_HTTP_TEXT_METRIC_EXPORTER = "prometheus_http_text_metric_exporter" """Prometheus metric exporter over HTTP with the default text-based format.""" class OtelSpanParentOriginValues(Enum): NONE = "none" """The span does not have a parent, it is a root span.""" LOCAL = "local" """The span has a parent and the parent's span context [isRemote()](https://opentelemetry.io/docs/specs/otel/trace/api/#isremote) is false.""" REMOTE = "remote" """The span has a parent and the parent's span context [isRemote()](https://opentelemetry.io/docs/specs/otel/trace/api/#isremote) is true.""" class OtelSpanSamplingResultValues(Enum): DROP = "DROP" """The span is not sampled and not recording.""" RECORD_ONLY = "RECORD_ONLY" """The span is not sampled, but recording.""" RECORD_AND_SAMPLE = "RECORD_AND_SAMPLE" """The span is sampled and recording.""" @deprecated( "Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.otel_attributes.OtelStatusCodeValues`." ) class OtelStatusCodeValues(Enum): OK = "OK" """Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.otel_attributes.OtelStatusCodeValues.OK`.""" ERROR = "ERROR" """Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.otel_attributes.OtelStatusCodeValues.ERROR`.""" other_attributes.py000066400000000000000000000006671527070070700425630ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/src/opentelemetry/semconv/_incubating/attributes# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from enum import Enum from typing import Final from typing_extensions import deprecated STATE: Final = "state" """ Deprecated: Replaced by `db.client.connection.state`. """ @deprecated("The attribute state is deprecated - Replaced by `db.client.connection.state`") class StateValues(Enum): IDLE = "idle" """idle.""" USED = "used" """used.""" peer_attributes.py000066400000000000000000000003011527070070700423560ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/src/opentelemetry/semconv/_incubating/attributes# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from typing import Final PEER_SERVICE: Final = "peer.service" """ Deprecated: Replaced by `service.peer.name`. """ pool_attributes.py000066400000000000000000000003101527070070700423740ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/src/opentelemetry/semconv/_incubating/attributes# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from typing import Final POOL_NAME: Final = "pool.name" """ Deprecated: Replaced by `db.client.connection.pool.name`. """ pprof_attributes.py000066400000000000000000000046321527070070700425640ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/src/opentelemetry/semconv/_incubating/attributes# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from typing import Final PPROF_LOCATION_IS_FOLDED: Final = "pprof.location.is_folded" """ Provides an indication that multiple symbols map to this location's address, for example due to identical code folding by the linker. In that case the line information represents one of the multiple symbols. This field must be recomputed when the symbolization state of the profile changes. """ PPROF_MAPPING_HAS_FILENAMES: Final = "pprof.mapping.has_filenames" """ Indicates that there are filenames related to this mapping. """ PPROF_MAPPING_HAS_FUNCTIONS: Final = "pprof.mapping.has_functions" """ Indicates that there are functions related to this mapping. """ PPROF_MAPPING_HAS_INLINE_FRAMES: Final = "pprof.mapping.has_inline_frames" """ Indicates that there are inline frames related to this mapping. """ PPROF_MAPPING_HAS_LINE_NUMBERS: Final = "pprof.mapping.has_line_numbers" """ Indicates that there are line numbers related to this mapping. """ PPROF_PROFILE_COMMENT: Final = "pprof.profile.comment" """ Free-form text associated with the profile. This field should not be used to store any machine-readable information, it is only for human-friendly content. """ PPROF_PROFILE_DOC_URL: Final = "pprof.profile.doc_url" """ Documentation link for this profile type. Note: The URL must be absolute and may be missing if the profile was generated by code that did not supply a link. """ PPROF_PROFILE_DROP_FRAMES: Final = "pprof.profile.drop_frames" """ Frames with Function.function_name fully matching the regular expression will be dropped from the samples, along with their successors. """ PPROF_PROFILE_KEEP_FRAMES: Final = "pprof.profile.keep_frames" """ Frames with Function.function_name fully matching the regular expression will be kept, even if it matches drop_frames. """ PPROF_SCOPE_DEFAULT_SAMPLE_TYPE: Final = "pprof.scope.default_sample_type" """ Records the pprof's default_sample_type in the original profile. Not set if the default sample type was missing. Note: This attribute, if present, MUST be set at the scope level (resource_profiles[].scope_profiles[].scope.attributes[]). """ PPROF_SCOPE_SAMPLE_TYPE_ORDER: Final = "pprof.scope.sample_type_order" """ Records the indexes of the sample types in the original profile. Note: This attribute, if present, MUST be set at the scope level (resource_profiles[].scope_profiles[].scope.attributes[]). """ process_attributes.py000066400000000000000000000202701527070070700431100ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/src/opentelemetry/semconv/_incubating/attributes# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from enum import Enum from typing import Final from typing_extensions import deprecated PROCESS_ARGS_COUNT: Final = "process.args_count" """ Length of the process.command_args array. Note: This field can be useful for querying or performing bucket analysis on how many arguments were provided to start a process. More arguments may be an indication of suspicious activity. """ PROCESS_COMMAND: Final = "process.command" """ The command used to launch the process (i.e. the command name). On Linux based systems, can be set to the zeroth string in `proc/[pid]/cmdline`. On Windows, can be set to the first parameter extracted from `GetCommandLineW`. """ PROCESS_COMMAND_ARGS: Final = "process.command_args" """ All the command arguments (including the command/executable itself) as received by the process. On Linux-based systems (and some other Unixoid systems supporting procfs), can be set according to the list of null-delimited strings extracted from `proc/[pid]/cmdline`. For libc-based executables, this would be the full argv vector passed to `main`. SHOULD NOT be collected by default unless there is sanitization that excludes sensitive data. """ PROCESS_COMMAND_LINE: Final = "process.command_line" """ The full command used to launch the process as a single string representing the full command. On Windows, can be set to the result of `GetCommandLineW`. Do not set this if you have to assemble it just for monitoring; use `process.command_args` instead. SHOULD NOT be collected by default unless there is sanitization that excludes sensitive data. """ PROCESS_CONTEXT_SWITCH_TYPE: Final = "process.context_switch.type" """ Specifies whether the context switches for this data point were voluntary or involuntary. """ PROCESS_CPU_STATE: Final = "process.cpu.state" """ Deprecated: Replaced by `cpu.mode`. """ PROCESS_CREATION_TIME: Final = "process.creation.time" """ The date and time the process was created, in ISO 8601 format. """ PROCESS_ENVIRONMENT_VARIABLE_TEMPLATE: Final = "process.environment_variable" """ Process environment variables, `` being the environment variable name, the value being the environment variable value. Note: Examples: - an environment variable `USER` with value `"ubuntu"` SHOULD be recorded as the `process.environment_variable.USER` attribute with value `"ubuntu"`. - an environment variable `PATH` with value `"/usr/local/bin:/usr/bin"` SHOULD be recorded as the `process.environment_variable.PATH` attribute with value `"/usr/local/bin:/usr/bin"`. """ PROCESS_EXECUTABLE_BUILD_ID_GNU: Final = "process.executable.build_id.gnu" """ The GNU build ID as found in the `.note.gnu.build-id` ELF section (hex string). """ PROCESS_EXECUTABLE_BUILD_ID_GO: Final = "process.executable.build_id.go" """ The Go build ID as retrieved by `go tool buildid `. """ PROCESS_EXECUTABLE_BUILD_ID_HTLHASH: Final = "process.executable.build_id.htlhash" """ Deterministic build ID for executables. Note: GNU and Go build IDs may be stripped or unavailable in some environments (e.g., Alpine Linux, Docker images). This attribute provides a deterministic build ID computed by hashing the first and last 4096 bytes of the file along with its length: ``` Input � Concat(File[:4096], File[-4096:], BigEndianUInt64(Len(File))) Digest � SHA256(Input) BuildID � Digest[:16] ``` The result is the first 16 bytes (128 bits) of the SHA256 digest, represented as a hex string. """ PROCESS_EXECUTABLE_BUILD_ID_PROFILING: Final = "process.executable.build_id.profiling" """ Deprecated: Replaced by `process.executable.build_id.htlhash`. """ PROCESS_EXECUTABLE_NAME: Final = "process.executable.name" """ The name of the process executable. On Linux based systems, this SHOULD be set to the base name of the target of `/proc/[pid]/exe`. On Windows, this SHOULD be set to the base name of `GetProcessImageFileNameW`. """ PROCESS_EXECUTABLE_PATH: Final = "process.executable.path" """ The full path to the process executable. On Linux based systems, can be set to the target of `proc/[pid]/exe`. On Windows, can be set to the result of `GetProcessImageFileNameW`. """ PROCESS_EXIT_CODE: Final = "process.exit.code" """ The exit code of the process. """ PROCESS_EXIT_TIME: Final = "process.exit.time" """ The date and time the process exited, in ISO 8601 format. """ PROCESS_GROUP_LEADER_PID: Final = "process.group_leader.pid" """ The PID of the process's group leader. This is also the process group ID (PGID) of the process. """ PROCESS_INTERACTIVE: Final = "process.interactive" """ Whether the process is connected to an interactive shell. """ PROCESS_LINUX_CGROUP: Final = "process.linux.cgroup" """ The control group associated with the process. Note: Control groups (cgroups) are a kernel feature used to organize and manage process resources. This attribute provides the path(s) to the cgroup(s) associated with the process, which should match the contents of the [/proc/\\[PID\\]/cgroup](https://man7.org/linux/man-pages/man7/cgroups.7.html) file. """ PROCESS_OWNER: Final = "process.owner" """ The username of the user that owns the process. """ PROCESS_PAGING_FAULT_TYPE: Final = "process.paging.fault_type" """ Deprecated: Replaced by `system.paging.fault.type`. """ PROCESS_PARENT_PID: Final = "process.parent_pid" """ Parent Process identifier (PPID). """ PROCESS_PID: Final = "process.pid" """ Process identifier (PID). """ PROCESS_REAL_USER_ID: Final = "process.real_user.id" """ The real user ID (RUID) of the process. """ PROCESS_REAL_USER_NAME: Final = "process.real_user.name" """ The username of the real user of the process. """ PROCESS_RUNTIME_DESCRIPTION: Final = "process.runtime.description" """ An additional description about the runtime of the process, for example a specific vendor customization of the runtime environment. """ PROCESS_RUNTIME_NAME: Final = "process.runtime.name" """ The name of the runtime of this process. """ PROCESS_RUNTIME_VERSION: Final = "process.runtime.version" """ The version of the runtime of this process, as returned by the runtime without modification. """ PROCESS_SAVED_USER_ID: Final = "process.saved_user.id" """ The saved user ID (SUID) of the process. """ PROCESS_SAVED_USER_NAME: Final = "process.saved_user.name" """ The username of the saved user. """ PROCESS_SESSION_LEADER_PID: Final = "process.session_leader.pid" """ The PID of the process's session leader. This is also the session ID (SID) of the process. """ PROCESS_STATE: Final = "process.state" """ The process state, e.g., [Linux Process State Codes](https://man7.org/linux/man-pages/man1/ps.1.html#PROCESS_STATE_CODES). """ PROCESS_TITLE: Final = "process.title" """ Process title (proctitle). Note: In many Unix-like systems, process title (proctitle), is the string that represents the name or command line of a running process, displayed by system monitoring tools like ps, top, and htop. """ PROCESS_USER_ID: Final = "process.user.id" """ The effective user ID (EUID) of the process. """ PROCESS_USER_NAME: Final = "process.user.name" """ The username of the effective user of the process. """ PROCESS_VPID: Final = "process.vpid" """ Virtual process identifier. Note: The process ID within a PID namespace. This is not necessarily unique across all processes on the host but it is unique within the process namespace that the process exists within. """ PROCESS_WORKING_DIRECTORY: Final = "process.working_directory" """ The working directory of the process. """ class ProcessContextSwitchTypeValues(Enum): VOLUNTARY = "voluntary" """voluntary.""" INVOLUNTARY = "involuntary" """involuntary.""" @deprecated("The attribute process.cpu.state is deprecated - Replaced by `cpu.mode`") class ProcessCpuStateValues(Enum): SYSTEM = "system" """system.""" USER = "user" """user.""" WAIT = "wait" """wait.""" @deprecated("The attribute process.paging.fault_type is deprecated - Replaced by `system.paging.fault.type`") class ProcessPagingFaultTypeValues(Enum): MAJOR = "major" """major.""" MINOR = "minor" """minor.""" class ProcessStateValues(Enum): RUNNING = "running" """running.""" SLEEPING = "sleeping" """sleeping.""" STOPPED = "stopped" """stopped.""" DEFUNCT = "defunct" """defunct.""" profile_attributes.py000066400000000000000000000031471527070070700430760ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/src/opentelemetry/semconv/_incubating/attributes# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from enum import Enum from typing import Final PROFILE_FRAME_TYPE: Final = "profile.frame.type" """ Describes the interpreter or compiler of a single frame. """ class ProfileFrameTypeValues(Enum): DOTNET = "dotnet" """[.NET](https://wikipedia.org/wiki/.NET).""" JVM = "jvm" """[JVM](https://wikipedia.org/wiki/Java_virtual_machine).""" KERNEL = "kernel" """[Kernel](https://wikipedia.org/wiki/Kernel_(operating_system)).""" NATIVE = "native" """Can be one of but not limited to [C](https://wikipedia.org/wiki/C_(programming_language)), [C++](https://wikipedia.org/wiki/C%2B%2B), [Go](https://wikipedia.org/wiki/Go_(programming_language)) or [Rust](https://wikipedia.org/wiki/Rust_(programming_language)). If possible, a more precise value MUST be used.""" PERL = "perl" """[Perl](https://wikipedia.org/wiki/Perl).""" PHP = "php" """[PHP](https://wikipedia.org/wiki/PHP).""" CPYTHON = "cpython" """[Python](https://wikipedia.org/wiki/Python_(programming_language)).""" RUBY = "ruby" """[Ruby](https://wikipedia.org/wiki/Ruby_(programming_language)).""" V8JS = "v8js" """[V8JS](https://wikipedia.org/wiki/V8_(JavaScript_engine)).""" BEAM = "beam" """[Erlang](https://en.wikipedia.org/wiki/BEAM_(Erlang_virtual_machine)).""" GO = "go" """[Go](https://wikipedia.org/wiki/Go_(programming_language)),.""" RUST = "rust" """[Rust](https://wikipedia.org/wiki/Rust_(programming_language)).""" LUAJIT = "luajit" """[LuaJIT](https://en.wikipedia.org/wiki/LuaJIT).""" rpc_attributes.py000066400000000000000000000207641527070070700422260ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/src/opentelemetry/semconv/_incubating/attributes# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from enum import Enum from typing import Final from typing_extensions import deprecated RPC_CONNECT_RPC_ERROR_CODE: Final = "rpc.connect_rpc.error_code" """ Deprecated: Replaced by `rpc.response.status_code`. """ RPC_CONNECT_RPC_REQUEST_METADATA_TEMPLATE: Final = "rpc.connect_rpc.request.metadata" """ Deprecated: Replaced by `rpc.request.metadata`. """ RPC_CONNECT_RPC_RESPONSE_METADATA_TEMPLATE: Final = "rpc.connect_rpc.response.metadata" """ Deprecated: Replaced by `rpc.response.metadata`. """ RPC_GRPC_REQUEST_METADATA_TEMPLATE: Final = "rpc.grpc.request.metadata" """ Deprecated: Replaced by `rpc.request.metadata`. """ RPC_GRPC_RESPONSE_METADATA_TEMPLATE: Final = "rpc.grpc.response.metadata" """ Deprecated: Replaced by `rpc.response.metadata`. """ RPC_GRPC_STATUS_CODE: Final = "rpc.grpc.status_code" """ Deprecated: Use string representation of the gRPC status code on the `rpc.response.status_code` attribute. """ RPC_JSONRPC_ERROR_CODE: Final = "rpc.jsonrpc.error_code" """ Deprecated: Use string representation of the error code on the `rpc.response.status_code` attribute. """ RPC_JSONRPC_ERROR_MESSAGE: Final = "rpc.jsonrpc.error_message" """ Deprecated: Use the span status description when reporting JSON-RPC spans. """ RPC_JSONRPC_REQUEST_ID: Final = "rpc.jsonrpc.request_id" """ Deprecated: Replaced by `jsonrpc.request.id`. """ RPC_JSONRPC_VERSION: Final = "rpc.jsonrpc.version" """ Deprecated: Replaced by `jsonrpc.protocol.version`. """ RPC_MESSAGE_COMPRESSED_SIZE: Final = "rpc.message.compressed_size" """ Deprecated: Deprecated, no replacement at this time. """ RPC_MESSAGE_ID: Final = "rpc.message.id" """ Deprecated: Deprecated, no replacement at this time. """ RPC_MESSAGE_TYPE: Final = "rpc.message.type" """ Deprecated: Deprecated, no replacement at this time. """ RPC_MESSAGE_UNCOMPRESSED_SIZE: Final = "rpc.message.uncompressed_size" """ Deprecated: Deprecated, no replacement at this time. """ RPC_METHOD: Final = "rpc.method" """ The fully-qualified logical name of the method from the RPC interface perspective. Note: The method name MAY have unbounded cardinality in edge or error cases. Some RPC frameworks or libraries provide a fixed set of recognized methods for client stubs and server implementations. Instrumentations for such frameworks MUST set this attribute to the original method name only when the method is recognized by the framework or library. When the method is not recognized, for example, when the server receives a request for a method that is not predefined on the server, or when instrumentation is not able to reliably detect if the method is predefined, the attribute MUST be set to `_OTHER`. If the RPC instrumentation could end up converting valid RPC methods to `_OTHER`, then it SHOULD provide a way to configure the list of recognized RPC methods. The `rpc.method` can be different from the name of any implementing method/function. The `code.function.name` attribute may be used to record the fully-qualified method actually executing the call on the server side, or the RPC client stub method on the client side. """ RPC_METHOD_ORIGINAL: Final = "rpc.method_original" """ The original name of the method used by the client. """ RPC_REQUEST_METADATA_TEMPLATE: Final = "rpc.request.metadata" """ RPC request metadata, `` being the normalized RPC metadata key (lowercase), the value being the metadata values. Note: Instrumentations SHOULD require an explicit configuration of which metadata values are to be captured. Including all request metadata values can be a security risk - explicit configuration helps avoid leaking sensitive information. For example, a property `my-custom-key` with value `["1.2.3.4", "1.2.3.5"]` SHOULD be recorded as `rpc.request.metadata.my-custom-key` attribute with value `["1.2.3.4", "1.2.3.5"]`. """ RPC_RESPONSE_METADATA_TEMPLATE: Final = "rpc.response.metadata" """ RPC response metadata, `` being the normalized RPC metadata key (lowercase), the value being the metadata values. Note: Instrumentations SHOULD require an explicit configuration of which metadata values are to be captured. Including all response metadata values can be a security risk - explicit configuration helps avoid leaking sensitive information. For example, a property `my-custom-key` with value `["attribute_value"]` SHOULD be recorded as the `rpc.response.metadata.my-custom-key` attribute with value `["attribute_value"]`. """ RPC_RESPONSE_STATUS_CODE: Final = "rpc.response.status_code" """ Status code of the RPC returned by the RPC server or generated by the client. Note: Usually it represents an error code, but may also represent partial success, warning, or differentiate between various types of successful outcomes. Semantic conventions for individual RPC frameworks SHOULD document what `rpc.response.status_code` means in the context of that system and which values are considered to represent errors. """ RPC_SERVICE: Final = "rpc.service" """ Deprecated: Value should be included in `rpc.method` which is expected to be a fully-qualified name. """ RPC_SYSTEM: Final = "rpc.system" """ Deprecated: Replaced by `rpc.system.name`. """ RPC_SYSTEM_NAME: Final = "rpc.system.name" """ The Remote Procedure Call (RPC) system. Note: The client and server RPC systems may differ for the same RPC interaction. For example, a client may use Apache Dubbo or Connect RPC to communicate with a server that uses gRPC since both protocols provide compatibility with gRPC. """ @deprecated("The attribute rpc.connect_rpc.error_code is deprecated - Replaced by `rpc.response.status_code`") class RpcConnectRpcErrorCodeValues(Enum): CANCELLED = "cancelled" """cancelled.""" UNKNOWN = "unknown" """unknown.""" INVALID_ARGUMENT = "invalid_argument" """invalid_argument.""" DEADLINE_EXCEEDED = "deadline_exceeded" """deadline_exceeded.""" NOT_FOUND = "not_found" """not_found.""" ALREADY_EXISTS = "already_exists" """already_exists.""" PERMISSION_DENIED = "permission_denied" """permission_denied.""" RESOURCE_EXHAUSTED = "resource_exhausted" """resource_exhausted.""" FAILED_PRECONDITION = "failed_precondition" """failed_precondition.""" ABORTED = "aborted" """aborted.""" OUT_OF_RANGE = "out_of_range" """out_of_range.""" UNIMPLEMENTED = "unimplemented" """unimplemented.""" INTERNAL = "internal" """internal.""" UNAVAILABLE = "unavailable" """unavailable.""" DATA_LOSS = "data_loss" """data_loss.""" UNAUTHENTICATED = "unauthenticated" """unauthenticated.""" @deprecated( "The attribute rpc.grpc.status_code is deprecated - Use string representation of the gRPC status code on the `rpc.response.status_code` attribute" ) class RpcGrpcStatusCodeValues(Enum): OK = 0 """OK.""" CANCELLED = 1 """CANCELLED.""" UNKNOWN = 2 """UNKNOWN.""" INVALID_ARGUMENT = 3 """INVALID_ARGUMENT.""" DEADLINE_EXCEEDED = 4 """DEADLINE_EXCEEDED.""" NOT_FOUND = 5 """NOT_FOUND.""" ALREADY_EXISTS = 6 """ALREADY_EXISTS.""" PERMISSION_DENIED = 7 """PERMISSION_DENIED.""" RESOURCE_EXHAUSTED = 8 """RESOURCE_EXHAUSTED.""" FAILED_PRECONDITION = 9 """FAILED_PRECONDITION.""" ABORTED = 10 """ABORTED.""" OUT_OF_RANGE = 11 """OUT_OF_RANGE.""" UNIMPLEMENTED = 12 """UNIMPLEMENTED.""" INTERNAL = 13 """INTERNAL.""" UNAVAILABLE = 14 """UNAVAILABLE.""" DATA_LOSS = 15 """DATA_LOSS.""" UNAUTHENTICATED = 16 """UNAUTHENTICATED.""" @deprecated("The attribute rpc.message.type is deprecated - Deprecated, no replacement at this time") class RpcMessageTypeValues(Enum): SENT = "SENT" """sent.""" RECEIVED = "RECEIVED" """received.""" @deprecated("The attribute rpc.system is deprecated - Replaced by `rpc.system.name`") class RpcSystemValues(Enum): GRPC = "grpc" """gRPC.""" JAVA_RMI = "java_rmi" """Java RMI.""" DOTNET_WCF = "dotnet_wcf" """.NET WCF.""" APACHE_DUBBO = "apache_dubbo" """Apache Dubbo.""" CONNECT_RPC = "connect_rpc" """Connect RPC.""" ONC_RPC = "onc_rpc" """[ONC RPC (Sun RPC)](https://datatracker.ietf.org/doc/html/rfc5531).""" JSONRPC = "jsonrpc" """JSON-RPC.""" class RpcSystemNameValues(Enum): GRPC = "grpc" """[gRPC](https://grpc.io/).""" DUBBO = "dubbo" """[Apache Dubbo](https://dubbo.apache.org/).""" CONNECTRPC = "connectrpc" """[Connect RPC](https://connectrpc.com/).""" JSONRPC = "jsonrpc" """[JSON-RPC](https://www.jsonrpc.org/).""" security_rule_attributes.py000066400000000000000000000027231527070070700443330ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/src/opentelemetry/semconv/_incubating/attributes# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from typing import Final SECURITY_RULE_CATEGORY: Final = "security_rule.category" """ A categorization value keyword used by the entity using the rule for detection of this event. """ SECURITY_RULE_DESCRIPTION: Final = "security_rule.description" """ The description of the rule generating the event. """ SECURITY_RULE_LICENSE: Final = "security_rule.license" """ Name of the license under which the rule used to generate this event is made available. """ SECURITY_RULE_NAME: Final = "security_rule.name" """ The name of the rule or signature generating the event. """ SECURITY_RULE_REFERENCE: Final = "security_rule.reference" """ Reference URL to additional information about the rule used to generate this event. Note: The URL can point to the vendor’s documentation about the rule. If that’s not available, it can also be a link to a more general page describing this type of alert. """ SECURITY_RULE_RULESET_NAME: Final = "security_rule.ruleset.name" """ Name of the ruleset, policy, group, or parent category in which the rule used to generate this event is a member. """ SECURITY_RULE_UUID: Final = "security_rule.uuid" """ A rule ID that is unique within the scope of a set or group of agents, observers, or other entities using the rule for detection of this event. """ SECURITY_RULE_VERSION: Final = "security_rule.version" """ The version / revision of the rule being used for analysis. """ server_attributes.py000066400000000000000000000006331527070070700427410ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/src/opentelemetry/semconv/_incubating/attributes# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from typing import Final SERVER_ADDRESS: Final = "server.address" """ Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.server_attributes.SERVER_ADDRESS`. """ SERVER_PORT: Final = "server.port" """ Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.server_attributes.SERVER_PORT`. """ service_attributes.py000066400000000000000000000041601527070070700430720ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/src/opentelemetry/semconv/_incubating/attributes# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from enum import Enum from typing import Final SERVICE_CRITICALITY: Final = "service.criticality" """ The operational criticality of the service. Note: Application developers are encouraged to set `service.criticality` to express the operational importance of their services. Telemetry consumers MAY use this attribute to optimize telemetry collection or improve user experience. """ SERVICE_INSTANCE_ID: Final = "service.instance.id" """ Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.service_attributes.SERVICE_INSTANCE_ID`. """ SERVICE_NAME: Final = "service.name" """ Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.service_attributes.SERVICE_NAME`. """ SERVICE_NAMESPACE: Final = "service.namespace" """ Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.service_attributes.SERVICE_NAMESPACE`. """ SERVICE_PEER_NAME: Final = "service.peer.name" """ Logical name of the service on the other side of the connection. SHOULD be equal to the actual [`service.name`](/docs/resource/README.md#service) resource attribute of the remote service if any. """ SERVICE_PEER_NAMESPACE: Final = "service.peer.namespace" """ Logical namespace of the service on the other side of the connection. SHOULD be equal to the actual [`service.namespace`](/docs/resource/README.md#service) resource attribute of the remote service if any. """ SERVICE_VERSION: Final = "service.version" """ Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.service_attributes.SERVICE_VERSION`. """ class ServiceCriticalityValues(Enum): CRITICAL = "critical" """Service is business-critical; downtime directly impacts revenue, user experience, or core functionality.""" HIGH = "high" """Service is important but has degradation tolerance or fallback mechanisms.""" MEDIUM = "medium" """Service provides supplementary functionality; degradation has limited user impact.""" LOW = "low" """Service is non-essential to core operations; used for background tasks or internal tools.""" session_attributes.py000066400000000000000000000004441527070070700431160ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/src/opentelemetry/semconv/_incubating/attributes# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from typing import Final SESSION_ID: Final = "session.id" """ A unique ID to identify a session. """ SESSION_PREVIOUS_ID: Final = "session.previous_id" """ The previous `session.id` for this user, when known. """ source_attributes.py000066400000000000000000000010471527070070700427330ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/src/opentelemetry/semconv/_incubating/attributes# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from typing import Final SOURCE_ADDRESS: Final = "source.address" """ Source address - domain name if available without reverse DNS lookup; otherwise, IP address or UNIX domain socket name. Note: When observed from the destination side, and when communicating through an intermediary, `source.address` SHOULD represent the source address behind any intermediaries, for example proxies, if it's available. """ SOURCE_PORT: Final = "source.port" """ Source port number. """ system_attributes.py000066400000000000000000000120421527070070700427540ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/src/opentelemetry/semconv/_incubating/attributes# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from enum import Enum from typing import Final from typing_extensions import deprecated SYSTEM_CPU_LOGICAL_NUMBER: Final = "system.cpu.logical_number" """ Deprecated: Replaced by `cpu.logical_number`. """ SYSTEM_CPU_STATE: Final = "system.cpu.state" """ Deprecated: Replaced by `cpu.mode`. """ SYSTEM_DEVICE: Final = "system.device" """ The device identifier. """ SYSTEM_FILESYSTEM_MODE: Final = "system.filesystem.mode" """ The filesystem mode. """ SYSTEM_FILESYSTEM_MOUNTPOINT: Final = "system.filesystem.mountpoint" """ The filesystem mount path. """ SYSTEM_FILESYSTEM_STATE: Final = "system.filesystem.state" """ The filesystem state. """ SYSTEM_FILESYSTEM_TYPE: Final = "system.filesystem.type" """ The filesystem type. """ SYSTEM_MEMORY_LINUX_HUGEPAGES_STATE: Final = "system.memory.linux.hugepages.state" """ The Linux HugePages memory state. """ SYSTEM_MEMORY_LINUX_SLAB_STATE: Final = "system.memory.linux.slab.state" """ The Linux Slab memory state. """ SYSTEM_MEMORY_STATE: Final = "system.memory.state" """ The memory state. """ SYSTEM_NETWORK_STATE: Final = "system.network.state" """ Deprecated: Replaced by `network.connection.state`. """ SYSTEM_PAGING_DIRECTION: Final = "system.paging.direction" """ The paging access direction. """ SYSTEM_PAGING_FAULT_TYPE: Final = "system.paging.fault.type" """ The paging fault type. """ SYSTEM_PAGING_STATE: Final = "system.paging.state" """ The memory paging state. """ SYSTEM_PAGING_TYPE: Final = "system.paging.type" """ Deprecated: Replaced by `system.paging.fault.type`. """ SYSTEM_PROCESS_STATUS: Final = "system.process.status" """ Deprecated: Replaced by `process.state`. """ SYSTEM_PROCESSES_STATUS: Final = "system.processes.status" """ Deprecated: Replaced by `process.state`. """ @deprecated("The attribute system.cpu.state is deprecated - Replaced by `cpu.mode`") class SystemCpuStateValues(Enum): USER = "user" """user.""" SYSTEM = "system" """system.""" NICE = "nice" """nice.""" IDLE = "idle" """idle.""" IOWAIT = "iowait" """iowait.""" INTERRUPT = "interrupt" """interrupt.""" STEAL = "steal" """steal.""" class SystemFilesystemStateValues(Enum): USED = "used" """used.""" FREE = "free" """free.""" RESERVED = "reserved" """reserved.""" class SystemFilesystemTypeValues(Enum): FAT32 = "fat32" """fat32.""" EXFAT = "exfat" """exfat.""" NTFS = "ntfs" """ntfs.""" REFS = "refs" """refs.""" HFSPLUS = "hfsplus" """hfsplus.""" EXT4 = "ext4" """ext4.""" class SystemMemoryLinuxHugepagesStateValues(Enum): FREE = "free" """free.""" USED = "used" """used.""" class SystemMemoryLinuxSlabStateValues(Enum): RECLAIMABLE = "reclaimable" """reclaimable.""" UNRECLAIMABLE = "unreclaimable" """unreclaimable.""" class SystemMemoryStateValues(Enum): USED = "used" """Actual used virtual memory in bytes.""" FREE = "free" """free.""" SHARED = "shared" """Deprecated: Removed, report shared memory usage with `metric.system.memory.linux.shared` metric.""" BUFFERS = "buffers" """buffers.""" CACHED = "cached" """cached.""" @deprecated("The attribute system.network.state is deprecated - Replaced by `network.connection.state`") class SystemNetworkStateValues(Enum): CLOSE = "close" """close.""" CLOSE_WAIT = "close_wait" """close_wait.""" CLOSING = "closing" """closing.""" DELETE = "delete" """delete.""" ESTABLISHED = "established" """established.""" FIN_WAIT_1 = "fin_wait_1" """fin_wait_1.""" FIN_WAIT_2 = "fin_wait_2" """fin_wait_2.""" LAST_ACK = "last_ack" """last_ack.""" LISTEN = "listen" """listen.""" SYN_RECV = "syn_recv" """syn_recv.""" SYN_SENT = "syn_sent" """syn_sent.""" TIME_WAIT = "time_wait" """time_wait.""" class SystemPagingDirectionValues(Enum): IN = "in" """in.""" OUT = "out" """out.""" class SystemPagingFaultTypeValues(Enum): MAJOR = "major" """major.""" MINOR = "minor" """minor.""" class SystemPagingStateValues(Enum): USED = "used" """used.""" FREE = "free" """free.""" @deprecated("The attribute system.paging.type is deprecated - Replaced by `system.paging.fault.type`") class SystemPagingTypeValues(Enum): MAJOR = "major" """major.""" MINOR = "minor" """minor.""" @deprecated("The attribute system.process.status is deprecated - Replaced by `process.state`") class SystemProcessStatusValues(Enum): RUNNING = "running" """running.""" SLEEPING = "sleeping" """sleeping.""" STOPPED = "stopped" """stopped.""" DEFUNCT = "defunct" """defunct.""" @deprecated("The attribute system.processes.status is deprecated - Replaced by `process.state`") class SystemProcessesStatusValues(Enum): RUNNING = "running" """running.""" SLEEPING = "sleeping" """sleeping.""" STOPPED = "stopped" """stopped.""" DEFUNCT = "defunct" """defunct.""" telemetry_attributes.py000066400000000000000000000064101527070070700434440ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/src/opentelemetry/semconv/_incubating/attributes# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from enum import Enum from typing import Final from typing_extensions import deprecated TELEMETRY_DISTRO_NAME: Final = "telemetry.distro.name" """ Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.telemetry_attributes.TELEMETRY_DISTRO_NAME`. """ TELEMETRY_DISTRO_VERSION: Final = "telemetry.distro.version" """ Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.telemetry_attributes.TELEMETRY_DISTRO_VERSION`. """ TELEMETRY_SDK_LANGUAGE: Final = "telemetry.sdk.language" """ Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.telemetry_attributes.TELEMETRY_SDK_LANGUAGE`. """ TELEMETRY_SDK_NAME: Final = "telemetry.sdk.name" """ Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.telemetry_attributes.TELEMETRY_SDK_NAME`. """ TELEMETRY_SDK_VERSION: Final = "telemetry.sdk.version" """ Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.telemetry_attributes.TELEMETRY_SDK_VERSION`. """ @deprecated( "Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.telemetry_attributes.TelemetrySdkLanguageValues`." ) class TelemetrySdkLanguageValues(Enum): CPP = "cpp" """Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.telemetry_attributes.TelemetrySdkLanguageValues.CPP`.""" DOTNET = "dotnet" """Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.telemetry_attributes.TelemetrySdkLanguageValues.DOTNET`.""" ERLANG = "erlang" """Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.telemetry_attributes.TelemetrySdkLanguageValues.ERLANG`.""" GO = "go" """Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.telemetry_attributes.TelemetrySdkLanguageValues.GO`.""" JAVA = "java" """Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.telemetry_attributes.TelemetrySdkLanguageValues.JAVA`.""" KOTLIN = "kotlin" """Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.telemetry_attributes.TelemetrySdkLanguageValues.KOTLIN`.""" NODEJS = "nodejs" """Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.telemetry_attributes.TelemetrySdkLanguageValues.NODEJS`.""" PHP = "php" """Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.telemetry_attributes.TelemetrySdkLanguageValues.PHP`.""" PYTHON = "python" """Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.telemetry_attributes.TelemetrySdkLanguageValues.PYTHON`.""" RUBY = "ruby" """Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.telemetry_attributes.TelemetrySdkLanguageValues.RUBY`.""" RUST = "rust" """Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.telemetry_attributes.TelemetrySdkLanguageValues.RUST`.""" SWIFT = "swift" """Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.telemetry_attributes.TelemetrySdkLanguageValues.SWIFT`.""" WEBJS = "webjs" """Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.telemetry_attributes.TelemetrySdkLanguageValues.WEBJS`.""" test_attributes.py000066400000000000000000000020451527070070700424110ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/src/opentelemetry/semconv/_incubating/attributes# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from enum import Enum from typing import Final TEST_CASE_NAME: Final = "test.case.name" """ The fully qualified human readable name of the [test case](https://wikipedia.org/wiki/Test_case). """ TEST_CASE_RESULT_STATUS: Final = "test.case.result.status" """ The status of the actual test case result from test execution. """ TEST_SUITE_NAME: Final = "test.suite.name" """ The human readable name of a [test suite](https://wikipedia.org/wiki/Test_suite). """ TEST_SUITE_RUN_STATUS: Final = "test.suite.run.status" """ The status of the test suite run. """ class TestCaseResultStatusValues(Enum): PASS = "pass" """pass.""" FAIL = "fail" """fail.""" class TestSuiteRunStatusValues(Enum): SUCCESS = "success" """success.""" FAILURE = "failure" """failure.""" SKIPPED = "skipped" """skipped.""" ABORTED = "aborted" """aborted.""" TIMED_OUT = "timed_out" """timed_out.""" IN_PROGRESS = "in_progress" """in_progress.""" thread_attributes.py000066400000000000000000000016711527070070700427050ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/src/opentelemetry/semconv/_incubating/attributes# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from typing import Final THREAD_ID: Final = "thread.id" """ Current "managed" thread ID (as opposed to OS thread ID). Note: Examples of where the value can be extracted from: | Language or platform | Source | | --- | --- | | JVM | `Thread.currentThread().threadId()` | | .NET | `Thread.CurrentThread.ManagedThreadId` | | Python | `threading.current_thread().ident` | | Ruby | `Thread.current.object_id` | | C++ | `std::this_thread::get_id()` | | Erlang | `erlang:self()` |. """ THREAD_NAME: Final = "thread.name" """ Current thread name. Note: Examples of where the value can be extracted from: | Language or platform | Source | | --- | --- | | JVM | `Thread.currentThread().getName()` | | .NET | `Thread.CurrentThread.Name` | | Python | `threading.current_thread().name` | | Ruby | `Thread.current.name` | | Erlang | `erlang:process_info(self(), registered_name)` |. """ tls_attributes.py000066400000000000000000000140041527070070700422320ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/src/opentelemetry/semconv/_incubating/attributes# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from enum import Enum from typing import Final TLS_CIPHER: Final = "tls.cipher" """ String indicating the [cipher](https://datatracker.ietf.org/doc/html/rfc5246#appendix-A.5) used during the current connection. Note: The values allowed for `tls.cipher` MUST be one of the `Descriptions` of the [registered TLS Cipher Suits](https://www.iana.org/assignments/tls-parameters/tls-parameters.xhtml#table-tls-parameters-4). """ TLS_CLIENT_CERTIFICATE: Final = "tls.client.certificate" """ PEM-encoded stand-alone certificate offered by the client. This is usually mutually-exclusive of `client.certificate_chain` since this value also exists in that list. """ TLS_CLIENT_CERTIFICATE_CHAIN: Final = "tls.client.certificate_chain" """ Array of PEM-encoded certificates that make up the certificate chain offered by the client. This is usually mutually-exclusive of `client.certificate` since that value should be the first certificate in the chain. """ TLS_CLIENT_HASH_MD5: Final = "tls.client.hash.md5" """ Certificate fingerprint using the MD5 digest of DER-encoded version of certificate offered by the client. For consistency with other hash values, this value should be formatted as an uppercase hash. """ TLS_CLIENT_HASH_SHA1: Final = "tls.client.hash.sha1" """ Certificate fingerprint using the SHA1 digest of DER-encoded version of certificate offered by the client. For consistency with other hash values, this value should be formatted as an uppercase hash. """ TLS_CLIENT_HASH_SHA256: Final = "tls.client.hash.sha256" """ Certificate fingerprint using the SHA256 digest of DER-encoded version of certificate offered by the client. For consistency with other hash values, this value should be formatted as an uppercase hash. """ TLS_CLIENT_ISSUER: Final = "tls.client.issuer" """ Distinguished name of [subject](https://datatracker.ietf.org/doc/html/rfc5280#section-4.1.2.6) of the issuer of the x.509 certificate presented by the client. """ TLS_CLIENT_JA3: Final = "tls.client.ja3" """ A hash that identifies clients based on how they perform an SSL/TLS handshake. """ TLS_CLIENT_NOT_AFTER: Final = "tls.client.not_after" """ Date/Time indicating when client certificate is no longer considered valid. """ TLS_CLIENT_NOT_BEFORE: Final = "tls.client.not_before" """ Date/Time indicating when client certificate is first considered valid. """ TLS_CLIENT_SERVER_NAME: Final = "tls.client.server_name" """ Deprecated: Replaced by `server.address`. """ TLS_CLIENT_SUBJECT: Final = "tls.client.subject" """ Distinguished name of subject of the x.509 certificate presented by the client. """ TLS_CLIENT_SUPPORTED_CIPHERS: Final = "tls.client.supported_ciphers" """ Array of ciphers offered by the client during the client hello. """ TLS_CURVE: Final = "tls.curve" """ String indicating the curve used for the given cipher, when applicable. """ TLS_ESTABLISHED: Final = "tls.established" """ Boolean flag indicating if the TLS negotiation was successful and transitioned to an encrypted tunnel. """ TLS_NEXT_PROTOCOL: Final = "tls.next_protocol" """ String indicating the protocol being tunneled. Per the values in the [IANA registry](https://www.iana.org/assignments/tls-extensiontype-values/tls-extensiontype-values.xhtml#alpn-protocol-ids), this string should be lower case. """ TLS_PROTOCOL_NAME: Final = "tls.protocol.name" """ Normalized lowercase protocol name parsed from original string of the negotiated [SSL/TLS protocol version](https://docs.openssl.org/1.1.1/man3/SSL_get_version/#return-values). """ TLS_PROTOCOL_VERSION: Final = "tls.protocol.version" """ Numeric part of the version parsed from the original string of the negotiated [SSL/TLS protocol version](https://docs.openssl.org/1.1.1/man3/SSL_get_version/#return-values). """ TLS_RESUMED: Final = "tls.resumed" """ Boolean flag indicating if this TLS connection was resumed from an existing TLS negotiation. """ TLS_SERVER_CERTIFICATE: Final = "tls.server.certificate" """ PEM-encoded stand-alone certificate offered by the server. This is usually mutually-exclusive of `server.certificate_chain` since this value also exists in that list. """ TLS_SERVER_CERTIFICATE_CHAIN: Final = "tls.server.certificate_chain" """ Array of PEM-encoded certificates that make up the certificate chain offered by the server. This is usually mutually-exclusive of `server.certificate` since that value should be the first certificate in the chain. """ TLS_SERVER_HASH_MD5: Final = "tls.server.hash.md5" """ Certificate fingerprint using the MD5 digest of DER-encoded version of certificate offered by the server. For consistency with other hash values, this value should be formatted as an uppercase hash. """ TLS_SERVER_HASH_SHA1: Final = "tls.server.hash.sha1" """ Certificate fingerprint using the SHA1 digest of DER-encoded version of certificate offered by the server. For consistency with other hash values, this value should be formatted as an uppercase hash. """ TLS_SERVER_HASH_SHA256: Final = "tls.server.hash.sha256" """ Certificate fingerprint using the SHA256 digest of DER-encoded version of certificate offered by the server. For consistency with other hash values, this value should be formatted as an uppercase hash. """ TLS_SERVER_ISSUER: Final = "tls.server.issuer" """ Distinguished name of [subject](https://datatracker.ietf.org/doc/html/rfc5280#section-4.1.2.6) of the issuer of the x.509 certificate presented by the client. """ TLS_SERVER_JA3S: Final = "tls.server.ja3s" """ A hash that identifies servers based on how they perform an SSL/TLS handshake. """ TLS_SERVER_NOT_AFTER: Final = "tls.server.not_after" """ Date/Time indicating when server certificate is no longer considered valid. """ TLS_SERVER_NOT_BEFORE: Final = "tls.server.not_before" """ Date/Time indicating when server certificate is first considered valid. """ TLS_SERVER_SUBJECT: Final = "tls.server.subject" """ Distinguished name of subject of the x.509 certificate presented by the server. """ class TlsProtocolNameValues(Enum): SSL = "ssl" """SSL.""" TLS = "tls" """TLS.""" url_attributes.py000066400000000000000000000071261527070070700422410ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/src/opentelemetry/semconv/_incubating/attributes# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from typing import Final URL_DOMAIN: Final = "url.domain" """ Domain extracted from the `url.full`, such as "opentelemetry.io". Note: In some cases a URL may refer to an IP and/or port directly, without a domain name. In this case, the IP address would go to the domain field. If the URL contains a [literal IPv6 address](https://www.rfc-editor.org/rfc/rfc2732#section-2) enclosed by `[` and `]`, the `[` and `]` characters should also be captured in the domain field. """ URL_EXTENSION: Final = "url.extension" """ The file extension extracted from the `url.full`, excluding the leading dot. Note: The file extension is only set if it exists, as not every URL has a file extension. When the filename has multiple extensions `example.tar.gz`, only the last one should be captured `gz`, not `tar.gz`. """ URL_FRAGMENT: Final = "url.fragment" """ Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.url_attributes.URL_FRAGMENT`. """ URL_FULL: Final = "url.full" """ Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.url_attributes.URL_FULL`. """ URL_ORIGINAL: Final = "url.original" """ Unmodified original URL as seen in the event source. Note: In network monitoring, the observed URL may be a full URL, whereas in access logs, the URL is often just represented as a path. This field is meant to represent the URL as it was observed, complete or not. `url.original` might contain credentials passed via URL in form of `https://username:password@www.example.com/`. In such case password and username SHOULD NOT be redacted and attribute's value SHOULD remain the same. """ URL_PATH: Final = "url.path" """ Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.url_attributes.URL_PATH`. """ URL_PORT: Final = "url.port" """ Port extracted from the `url.full`. """ URL_QUERY: Final = "url.query" """ Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.url_attributes.URL_QUERY`. """ URL_REGISTERED_DOMAIN: Final = "url.registered_domain" """ The highest registered URL domain, stripped of the subdomain. Note: This value can be determined precisely with the [public suffix list](https://publicsuffix.org/). For example, the registered domain for `foo.example.com` is `example.com`. Trying to approximate this by simply taking the last two labels will not work well for TLDs such as `co.uk`. """ URL_SCHEME: Final = "url.scheme" """ Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.url_attributes.URL_SCHEME`. """ URL_SUBDOMAIN: Final = "url.subdomain" """ The subdomain portion of a fully qualified domain name includes all of the names except the hostname under the registered_domain. In a partially qualified domain, or if the qualification level of the full name cannot be determined, subdomain contains all of the names below the registered domain. Note: The subdomain portion of `www.east.mydomain.co.uk` is `east`. If the domain has multiple levels of subdomain, such as `sub2.sub1.example.com`, the subdomain field should contain `sub2.sub1`, with no trailing period. """ URL_TEMPLATE: Final = "url.template" """ The low-cardinality template of an [absolute path reference](https://www.rfc-editor.org/rfc/rfc3986#section-4.2). """ URL_TOP_LEVEL_DOMAIN: Final = "url.top_level_domain" """ The effective top level domain (eTLD), also known as the domain suffix, is the last part of the domain name. For example, the top level domain for example.com is `com`. Note: This value can be determined precisely with the [public suffix list](https://publicsuffix.org/). """ user_agent_attributes.py000066400000000000000000000050411527070070700435650ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/src/opentelemetry/semconv/_incubating/attributes# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from enum import Enum from typing import Final USER_AGENT_NAME: Final = "user_agent.name" """ Name of the user-agent extracted from original. Usually refers to the browser's name. Note: [Example](https://uaparser.dev/#demo) of extracting browser's name from original string. In the case of using a user-agent for non-browser products, such as microservices with multiple names/versions inside the `user_agent.original`, the most significant name SHOULD be selected. In such a scenario it should align with `user_agent.version`. """ USER_AGENT_ORIGINAL: Final = "user_agent.original" """ Deprecated in favor of stable :py:const:`opentelemetry.semconv.attributes.user_agent_attributes.USER_AGENT_ORIGINAL`. """ USER_AGENT_OS_NAME: Final = "user_agent.os.name" """ Human readable operating system name. Note: For mapping user agent strings to OS names, libraries such as [ua-parser](https://github.com/ua-parser) can be utilized. """ USER_AGENT_OS_VERSION: Final = "user_agent.os.version" """ The version string of the operating system as defined in [Version Attributes](/docs/resource/README.md#version-attributes). Note: For mapping user agent strings to OS versions, libraries such as [ua-parser](https://github.com/ua-parser) can be utilized. """ USER_AGENT_SYNTHETIC_TYPE: Final = "user_agent.synthetic.type" """ Specifies the category of synthetic traffic, such as tests or bots. Note: This attribute MAY be derived from the contents of the `user_agent.original` attribute. Components that populate the attribute are responsible for determining what they consider to be synthetic bot or test traffic. This attribute can either be set for self-identification purposes, or on telemetry detected to be generated as a result of a synthetic request. This attribute is useful for distinguishing between genuine client traffic and synthetic traffic generated by bots or tests. """ USER_AGENT_VERSION: Final = "user_agent.version" """ Version of the user-agent extracted from original. Usually refers to the browser's version. Note: [Example](https://uaparser.dev/#demo) of extracting browser's version from original string. In the case of using a user-agent for non-browser products, such as microservices with multiple names/versions inside the `user_agent.original`, the most significant version SHOULD be selected. In such a scenario it should align with `user_agent.name`. """ class UserAgentSyntheticTypeValues(Enum): BOT = "bot" """Bot source.""" TEST = "test" """Synthetic test source.""" user_attributes.py000066400000000000000000000012441527070070700424100ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/src/opentelemetry/semconv/_incubating/attributes# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from typing import Final USER_EMAIL: Final = "user.email" """ User email address. """ USER_FULL_NAME: Final = "user.full_name" """ User's full name. """ USER_HASH: Final = "user.hash" """ Unique user hash to correlate information for a user in anonymized form. Note: Useful if `user.id` or `user.name` contain confidential information and cannot be used. """ USER_ID: Final = "user.id" """ Unique identifier of the user. """ USER_NAME: Final = "user.name" """ Short name or login/username of the user. """ USER_ROLES: Final = "user.roles" """ Array of user roles at the time of the event. """ vcs_attributes.py000066400000000000000000000210041527070070700422210ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/src/opentelemetry/semconv/_incubating/attributes# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from enum import Enum from typing import Final from typing_extensions import deprecated VCS_CHANGE_ID: Final = "vcs.change.id" """ The ID of the change (pull request/merge request/changelist) if applicable. This is usually a unique (within repository) identifier generated by the VCS system. """ VCS_CHANGE_STATE: Final = "vcs.change.state" """ The state of the change (pull request/merge request/changelist). """ VCS_CHANGE_TITLE: Final = "vcs.change.title" """ The human readable title of the change (pull request/merge request/changelist). This title is often a brief summary of the change and may get merged in to a ref as the commit summary. """ VCS_LINE_CHANGE_TYPE: Final = "vcs.line_change.type" """ The type of line change being measured on a branch or change. """ VCS_OWNER_NAME: Final = "vcs.owner.name" """ The group owner within the version control system. """ VCS_PROVIDER_NAME: Final = "vcs.provider.name" """ The name of the version control system provider. """ VCS_REF_BASE_NAME: Final = "vcs.ref.base.name" """ The name of the [reference](https://git-scm.com/docs/gitglossary#def_ref) such as **branch** or **tag** in the repository. Note: `base` refers to the starting point of a change. For example, `main` would be the base reference of type branch if you've created a new reference of type branch from it and created new commits. """ VCS_REF_BASE_REVISION: Final = "vcs.ref.base.revision" """ The revision, literally [revised version](https://www.merriam-webster.com/dictionary/revision), The revision most often refers to a commit object in Git, or a revision number in SVN. Note: `base` refers to the starting point of a change. For example, `main` would be the base reference of type branch if you've created a new reference of type branch from it and created new commits. The revision can be a full [hash value (see glossary)](https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.186-5.pdf), of the recorded change to a ref within a repository pointing to a commit [commit](https://git-scm.com/docs/git-commit) object. It does not necessarily have to be a hash; it can simply define a [revision number](https://svnbook.red-bean.com/en/1.7/svn.tour.revs.specifiers.html) which is an integer that is monotonically increasing. In cases where it is identical to the `ref.base.name`, it SHOULD still be included. It is up to the implementer to decide which value to set as the revision based on the VCS system and situational context. """ VCS_REF_BASE_TYPE: Final = "vcs.ref.base.type" """ The type of the [reference](https://git-scm.com/docs/gitglossary#def_ref) in the repository. Note: `base` refers to the starting point of a change. For example, `main` would be the base reference of type branch if you've created a new reference of type branch from it and created new commits. """ VCS_REF_HEAD_NAME: Final = "vcs.ref.head.name" """ The name of the [reference](https://git-scm.com/docs/gitglossary#def_ref) such as **branch** or **tag** in the repository. Note: `head` refers to where you are right now; the current reference at a given time. """ VCS_REF_HEAD_REVISION: Final = "vcs.ref.head.revision" """ The revision, literally [revised version](https://www.merriam-webster.com/dictionary/revision), The revision most often refers to a commit object in Git, or a revision number in SVN. Note: `head` refers to where you are right now; the current reference at a given time.The revision can be a full [hash value (see glossary)](https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.186-5.pdf), of the recorded change to a ref within a repository pointing to a commit [commit](https://git-scm.com/docs/git-commit) object. It does not necessarily have to be a hash; it can simply define a [revision number](https://svnbook.red-bean.com/en/1.7/svn.tour.revs.specifiers.html) which is an integer that is monotonically increasing. In cases where it is identical to the `ref.head.name`, it SHOULD still be included. It is up to the implementer to decide which value to set as the revision based on the VCS system and situational context. """ VCS_REF_HEAD_TYPE: Final = "vcs.ref.head.type" """ The type of the [reference](https://git-scm.com/docs/gitglossary#def_ref) in the repository. Note: `head` refers to where you are right now; the current reference at a given time. """ VCS_REF_TYPE: Final = "vcs.ref.type" """ The type of the [reference](https://git-scm.com/docs/gitglossary#def_ref) in the repository. """ VCS_REPOSITORY_CHANGE_ID: Final = "vcs.repository.change.id" """ Deprecated: Replaced by `vcs.change.id`. """ VCS_REPOSITORY_CHANGE_TITLE: Final = "vcs.repository.change.title" """ Deprecated: Replaced by `vcs.change.title`. """ VCS_REPOSITORY_NAME: Final = "vcs.repository.name" """ The human readable name of the repository. It SHOULD NOT include any additional identifier like Group/SubGroup in GitLab or organization in GitHub. Note: Due to it only being the name, it can clash with forks of the same repository if collecting telemetry across multiple orgs or groups in the same backends. """ VCS_REPOSITORY_REF_NAME: Final = "vcs.repository.ref.name" """ Deprecated: Replaced by `vcs.ref.head.name`. """ VCS_REPOSITORY_REF_REVISION: Final = "vcs.repository.ref.revision" """ Deprecated: Replaced by `vcs.ref.head.revision`. """ VCS_REPOSITORY_REF_TYPE: Final = "vcs.repository.ref.type" """ Deprecated: Replaced by `vcs.ref.head.type`. """ VCS_REPOSITORY_URL_FULL: Final = "vcs.repository.url.full" """ The [canonical URL](https://support.google.com/webmasters/answer/10347851) of the repository providing the complete HTTP(S) address in order to locate and identify the repository through a browser. Note: In Git Version Control Systems, the canonical URL SHOULD NOT include the `.git` extension. """ VCS_REVISION_DELTA_DIRECTION: Final = "vcs.revision_delta.direction" """ The type of revision comparison. """ class VcsChangeStateValues(Enum): OPEN = "open" """Open means the change is currently active and under review. It hasn't been merged into the target branch yet, and it's still possible to make changes or add comments.""" WIP = "wip" """WIP (work-in-progress, draft) means the change is still in progress and not yet ready for a full review. It might still undergo significant changes.""" CLOSED = "closed" """Closed means the merge request has been closed without merging. This can happen for various reasons, such as the changes being deemed unnecessary, the issue being resolved in another way, or the author deciding to withdraw the request.""" MERGED = "merged" """Merged indicates that the change has been successfully integrated into the target codebase.""" class VcsLineChangeTypeValues(Enum): ADDED = "added" """How many lines were added.""" REMOVED = "removed" """How many lines were removed.""" class VcsProviderNameValues(Enum): GITHUB = "github" """[GitHub](https://github.com).""" GITLAB = "gitlab" """[GitLab](https://gitlab.com).""" GITTEA = "gittea" """Deprecated: Replaced by `gitea`.""" GITEA = "gitea" """[Gitea](https://gitea.io).""" BITBUCKET = "bitbucket" """[Bitbucket](https://bitbucket.org).""" class VcsRefBaseTypeValues(Enum): BRANCH = "branch" """[branch](https://git-scm.com/docs/gitglossary#Documentation/gitglossary.txt-aiddefbranchabranch).""" TAG = "tag" """[tag](https://git-scm.com/docs/gitglossary#Documentation/gitglossary.txt-aiddeftagatag).""" class VcsRefHeadTypeValues(Enum): BRANCH = "branch" """[branch](https://git-scm.com/docs/gitglossary#Documentation/gitglossary.txt-aiddefbranchabranch).""" TAG = "tag" """[tag](https://git-scm.com/docs/gitglossary#Documentation/gitglossary.txt-aiddeftagatag).""" class VcsRefTypeValues(Enum): BRANCH = "branch" """[branch](https://git-scm.com/docs/gitglossary#Documentation/gitglossary.txt-aiddefbranchabranch).""" TAG = "tag" """[tag](https://git-scm.com/docs/gitglossary#Documentation/gitglossary.txt-aiddeftagatag).""" @deprecated("The attribute vcs.repository.ref.type is deprecated - Replaced by `vcs.ref.head.type`") class VcsRepositoryRefTypeValues(Enum): BRANCH = "branch" """[branch](https://git-scm.com/docs/gitglossary#Documentation/gitglossary.txt-aiddefbranchabranch).""" TAG = "tag" """[tag](https://git-scm.com/docs/gitglossary#Documentation/gitglossary.txt-aiddeftagatag).""" class VcsRevisionDeltaDirectionValues(Enum): BEHIND = "behind" """How many revisions the change is behind the target ref.""" AHEAD = "ahead" """How many revisions the change is ahead of the target ref.""" webengine_attributes.py000066400000000000000000000006451527070070700434010ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/src/opentelemetry/semconv/_incubating/attributes# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from typing import Final WEBENGINE_DESCRIPTION: Final = "webengine.description" """ Additional description of the web engine (e.g. detailed version and edition information). """ WEBENGINE_NAME: Final = "webengine.name" """ The name of the web engine. """ WEBENGINE_VERSION: Final = "webengine.version" """ The version of the web engine. """ zos_attributes.py000066400000000000000000000006611527070070700422470ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/src/opentelemetry/semconv/_incubating/attributes# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from typing import Final ZOS_SMF_ID: Final = "zos.smf.id" """ The System Management Facility (SMF) Identifier uniquely identified a z/OS system within a SYSPLEX or mainframe environment and is used for system and performance analysis. """ ZOS_SYSPLEX_NAME: Final = "zos.sysplex.name" """ The name of the SYSPLEX to which the z/OS system belongs too. """ metrics/000077500000000000000000000000001527070070700360715ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/src/opentelemetry/semconv/_incubatingazure_metrics.py000066400000000000000000000025621527070070700413240ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/src/opentelemetry/semconv/_incubating/metrics# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from typing import Final from opentelemetry.metrics import Histogram, Meter, UpDownCounter AZURE_COSMOSDB_CLIENT_ACTIVE_INSTANCE_COUNT: Final = "azure.cosmosdb.client.active_instance.count" """ Number of active client instances Instrument: updowncounter Unit: {instance} """ def create_azure_cosmosdb_client_active_instance_count(meter: Meter) -> UpDownCounter: """Number of active client instances""" return meter.create_up_down_counter( name=AZURE_COSMOSDB_CLIENT_ACTIVE_INSTANCE_COUNT, description="Number of active client instances.", unit="{instance}", ) AZURE_COSMOSDB_CLIENT_OPERATION_REQUEST_CHARGE: Final = "azure.cosmosdb.client.operation.request_charge" """ [Request units](https://learn.microsoft.com/azure/cosmos-db/request-units) consumed by the operation Instrument: histogram Unit: {request_unit} """ def create_azure_cosmosdb_client_operation_request_charge(meter: Meter) -> Histogram: """[Request units](https://learn.microsoft.com/azure/cosmos-db/request-units) consumed by the operation""" return meter.create_histogram( name=AZURE_COSMOSDB_CLIENT_OPERATION_REQUEST_CHARGE, description="[Request units](https://learn.microsoft.com/azure/cosmos-db/request-units) consumed by the operation.", unit="{request_unit}", ) cicd_metrics.py000066400000000000000000000061241527070070700410760ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/src/opentelemetry/semconv/_incubating/metrics# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from typing import Final from opentelemetry.metrics import Counter, Histogram, Meter, UpDownCounter CICD_PIPELINE_RUN_ACTIVE: Final = "cicd.pipeline.run.active" """ The number of pipeline runs currently active in the system by state Instrument: updowncounter Unit: {run} """ def create_cicd_pipeline_run_active(meter: Meter) -> UpDownCounter: """The number of pipeline runs currently active in the system by state""" return meter.create_up_down_counter( name=CICD_PIPELINE_RUN_ACTIVE, description="The number of pipeline runs currently active in the system by state.", unit="{run}", ) CICD_PIPELINE_RUN_DURATION: Final = "cicd.pipeline.run.duration" """ Duration of a pipeline run grouped by pipeline, state and result Instrument: histogram Unit: s """ def create_cicd_pipeline_run_duration(meter: Meter) -> Histogram: """Duration of a pipeline run grouped by pipeline, state and result""" return meter.create_histogram( name=CICD_PIPELINE_RUN_DURATION, description="Duration of a pipeline run grouped by pipeline, state and result.", unit="s", ) CICD_PIPELINE_RUN_ERRORS: Final = "cicd.pipeline.run.errors" """ The number of errors encountered in pipeline runs (eg. compile, test failures) Instrument: counter Unit: {error} Note: There might be errors in a pipeline run that are non fatal (eg. they are suppressed) or in a parallel stage multiple stages could have a fatal error. This means that this error count might not be the same as the count of metric `cicd.pipeline.run.duration` with run result `failure`. """ def create_cicd_pipeline_run_errors(meter: Meter) -> Counter: """The number of errors encountered in pipeline runs (eg. compile, test failures)""" return meter.create_counter( name=CICD_PIPELINE_RUN_ERRORS, description="The number of errors encountered in pipeline runs (eg. compile, test failures).", unit="{error}", ) CICD_SYSTEM_ERRORS: Final = "cicd.system.errors" """ The number of errors in a component of the CI/CD system (eg. controller, scheduler, agent) Instrument: counter Unit: {error} Note: Errors in pipeline run execution are explicitly excluded. Ie a test failure is not counted in this metric. """ def create_cicd_system_errors(meter: Meter) -> Counter: """The number of errors in a component of the CI/CD system (eg. controller, scheduler, agent)""" return meter.create_counter( name=CICD_SYSTEM_ERRORS, description="The number of errors in a component of the CI/CD system (eg. controller, scheduler, agent).", unit="{error}", ) CICD_WORKER_COUNT: Final = "cicd.worker.count" """ The number of workers on the CI/CD system by state Instrument: updowncounter Unit: {worker} """ def create_cicd_worker_count(meter: Meter) -> UpDownCounter: """The number of workers on the CI/CD system by state""" return meter.create_up_down_counter( name=CICD_WORKER_COUNT, description="The number of workers on the CI/CD system by state.", unit="{worker}", ) container_metrics.py000066400000000000000000000254711527070070700421640ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/src/opentelemetry/semconv/_incubating/metrics# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from collections.abc import Callable, Generator, Iterable, Sequence from typing import Final from opentelemetry.metrics import CallbackOptions, Counter, Meter, ObservableGauge, Observation, UpDownCounter # pylint: disable=invalid-name CallbackT = Callable[[CallbackOptions], Iterable[Observation]] | Generator[Iterable[Observation], CallbackOptions, None] CONTAINER_CPU_TIME: Final = "container.cpu.time" """ Total CPU time consumed Instrument: counter Unit: s Note: CPU time consumed by the specific container on all available CPU cores. """ def create_container_cpu_time(meter: Meter) -> Counter: """Total CPU time consumed""" return meter.create_counter( name=CONTAINER_CPU_TIME, description="Total CPU time consumed.", unit="s", ) CONTAINER_CPU_USAGE: Final = "container.cpu.usage" """ Container's CPU usage, measured in CPUs. Range from 0 to the number of allocatable CPUs Instrument: gauge Unit: {cpu} Note: CPU usage of the specific container on all available CPU cores. It is calculated as the change in cumulative CPU time (container.cpu.time) over a measurement interval, divided by the elapsed time: usageCores = (cpuTimeEnd - cpuTimeStart) / elapsedSeconds. """ def create_container_cpu_usage(meter: Meter, callbacks: Sequence[CallbackT] | None) -> ObservableGauge: """Container's CPU usage, measured in CPUs. Range from 0 to the number of allocatable CPUs""" return meter.create_observable_gauge( name=CONTAINER_CPU_USAGE, callbacks=callbacks, description="Container's CPU usage, measured in CPUs. Range from 0 to the number of allocatable CPUs.", unit="{cpu}", ) CONTAINER_DISK_IO: Final = "container.disk.io" """ Disk bytes for the container Instrument: counter Unit: By Note: The total number of bytes read/written successfully (aggregated from all disks). """ def create_container_disk_io(meter: Meter) -> Counter: """Disk bytes for the container""" return meter.create_counter( name=CONTAINER_DISK_IO, description="Disk bytes for the container.", unit="By", ) CONTAINER_FILESYSTEM_AVAILABLE: Final = "container.filesystem.available" """ Container filesystem available bytes Instrument: updowncounter Unit: By Note: In K8s, this metric is derived from the [FsStats.AvailableBytes](https://pkg.go.dev/k8s.io/kubelet@v0.33.0/pkg/apis/stats/v1alpha1#FsStats) field of the [ContainerStats.Rootfs](https://pkg.go.dev/k8s.io/kubelet@v0.33.0/pkg/apis/stats/v1alpha1#ContainerStats) of the Kubelet's stats API. """ def create_container_filesystem_available(meter: Meter) -> UpDownCounter: """Container filesystem available bytes""" return meter.create_up_down_counter( name=CONTAINER_FILESYSTEM_AVAILABLE, description="Container filesystem available bytes.", unit="By", ) CONTAINER_FILESYSTEM_CAPACITY: Final = "container.filesystem.capacity" """ Container filesystem capacity Instrument: updowncounter Unit: By Note: In K8s, this metric is derived from the [FsStats.CapacityBytes](https://pkg.go.dev/k8s.io/kubelet@v0.33.0/pkg/apis/stats/v1alpha1#FsStats) field of the [ContainerStats.Rootfs](https://pkg.go.dev/k8s.io/kubelet@v0.33.0/pkg/apis/stats/v1alpha1#ContainerStats) of the Kubelet's stats API. """ def create_container_filesystem_capacity(meter: Meter) -> UpDownCounter: """Container filesystem capacity""" return meter.create_up_down_counter( name=CONTAINER_FILESYSTEM_CAPACITY, description="Container filesystem capacity.", unit="By", ) CONTAINER_FILESYSTEM_USAGE: Final = "container.filesystem.usage" """ Container filesystem usage Instrument: updowncounter Unit: By Note: This may not equal capacity - available. In K8s, this metric is derived from the [FsStats.UsedBytes](https://pkg.go.dev/k8s.io/kubelet@v0.33.0/pkg/apis/stats/v1alpha1#FsStats) field of the [ContainerStats.Rootfs](https://pkg.go.dev/k8s.io/kubelet@v0.33.0/pkg/apis/stats/v1alpha1#ContainerStats) of the Kubelet's stats API. """ def create_container_filesystem_usage(meter: Meter) -> UpDownCounter: """Container filesystem usage""" return meter.create_up_down_counter( name=CONTAINER_FILESYSTEM_USAGE, description="Container filesystem usage.", unit="By", ) CONTAINER_MEMORY_AVAILABLE: Final = "container.memory.available" """ Container memory available Instrument: updowncounter Unit: By Note: Available memory for use. This is defined as the memory limit - workingSetBytes. If memory limit is undefined, the available bytes is omitted. In general, this metric can be derived from [cadvisor](https://github.com/google/cadvisor/blob/v0.53.0/docs/storage/prometheus.md#prometheus-container-metrics) and by subtracting the `container_memory_working_set_bytes` metric from the `container_spec_memory_limit_bytes` metric. In K8s, this metric is derived from the [MemoryStats.AvailableBytes](https://pkg.go.dev/k8s.io/kubelet@v0.34.0/pkg/apis/stats/v1alpha1#MemoryStats) field of the [ContainerStats.Memory](https://pkg.go.dev/k8s.io/kubelet@v0.34.0/pkg/apis/stats/v1alpha1#ContainerStats) of the Kubelet's stats API. """ def create_container_memory_available(meter: Meter) -> UpDownCounter: """Container memory available""" return meter.create_up_down_counter( name=CONTAINER_MEMORY_AVAILABLE, description="Container memory available.", unit="By", ) CONTAINER_MEMORY_PAGING_FAULTS: Final = "container.memory.paging.faults" """ Deprecated: Replaced by `container.paging.faults`. """ def create_container_memory_paging_faults(meter: Meter) -> Counter: """Deprecated, use `container.paging.faults` instead""" return meter.create_counter( name=CONTAINER_MEMORY_PAGING_FAULTS, description="Deprecated, use `container.paging.faults` instead.", unit="{fault}", ) CONTAINER_MEMORY_RSS: Final = "container.memory.rss" """ Container memory RSS Instrument: updowncounter Unit: By Note: In general, this metric can be derived from [cadvisor](https://github.com/google/cadvisor/blob/v0.53.0/docs/storage/prometheus.md#prometheus-container-metrics) and specifically the `container_memory_rss` metric. In K8s, this metric is derived from the [MemoryStats.RSSBytes](https://pkg.go.dev/k8s.io/kubelet@v0.34.0/pkg/apis/stats/v1alpha1#MemoryStats) field of the [ContainerStats.Memory](https://pkg.go.dev/k8s.io/kubelet@v0.34.0/pkg/apis/stats/v1alpha1#ContainerStats) of the Kubelet's stats API. """ def create_container_memory_rss(meter: Meter) -> UpDownCounter: """Container memory RSS""" return meter.create_up_down_counter( name=CONTAINER_MEMORY_RSS, description="Container memory RSS.", unit="By", ) CONTAINER_MEMORY_USAGE: Final = "container.memory.usage" """ Memory usage of the container Instrument: updowncounter Unit: By Note: Current memory usage, including all memory regardless of when it was accessed. In general, this metric can be derived from [cadvisor](https://github.com/google/cadvisor/blob/v0.53.0/docs/storage/prometheus.md#prometheus-container-metrics) and specifically the `container_memory_usage_bytes` metric. In K8s, this metric is derived from the [MemoryStats.UsageBytes](https://pkg.go.dev/k8s.io/kubelet@v0.34.0/pkg/apis/stats/v1alpha1#MemoryStats) field of the [ContainerStats.Memory](https://pkg.go.dev/k8s.io/kubelet@v0.34.0/pkg/apis/stats/v1alpha1#ContainerStats) of the Kubelet's stats API. """ def create_container_memory_usage(meter: Meter) -> UpDownCounter: """Memory usage of the container""" return meter.create_up_down_counter( name=CONTAINER_MEMORY_USAGE, description="Memory usage of the container.", unit="By", ) CONTAINER_MEMORY_WORKING_SET: Final = "container.memory.working_set" """ Container memory working set Instrument: updowncounter Unit: By Note: In general, this metric can be derived from [cadvisor](https://github.com/google/cadvisor/blob/v0.53.0/docs/storage/prometheus.md#prometheus-container-metrics) and specifically the `container_memory_working_set_bytes` metric. In K8s, this metric is derived from the [MemoryStats.WorkingSetBytes](https://pkg.go.dev/k8s.io/kubelet@v0.34.0/pkg/apis/stats/v1alpha1#MemoryStats) field of the [ContainerStats.Memory](https://pkg.go.dev/k8s.io/kubelet@v0.34.0/pkg/apis/stats/v1alpha1#ContainerStats) of the Kubelet's stats API. """ def create_container_memory_working_set(meter: Meter) -> UpDownCounter: """Container memory working set""" return meter.create_up_down_counter( name=CONTAINER_MEMORY_WORKING_SET, description="Container memory working set.", unit="By", ) CONTAINER_NETWORK_IO: Final = "container.network.io" """ Network bytes for the container Instrument: counter Unit: By Note: The number of bytes sent/received on all network interfaces by the container. """ def create_container_network_io(meter: Meter) -> Counter: """Network bytes for the container""" return meter.create_counter( name=CONTAINER_NETWORK_IO, description="Network bytes for the container.", unit="By", ) CONTAINER_PAGING_FAULTS: Final = "container.paging.faults" """ Container memory paging faults Instrument: counter Unit: {fault} Note: In general, this metric can be derived from [cadvisor](https://github.com/google/cadvisor/blob/v0.53.0/docs/storage/prometheus.md#prometheus-container-metrics) and specifically the `container_memory_failures_total{failure_type=pgfault, scope=container}` and `container_memory_failures_total{failure_type=pgmajfault, scope=container}`metric. In K8s, this metric is derived from the [MemoryStats.PageFaults](https://pkg.go.dev/k8s.io/kubelet@v0.34.0/pkg/apis/stats/v1alpha1#MemoryStats) and [MemoryStats.MajorPageFaults](https://pkg.go.dev/k8s.io/kubelet@v0.34.0/pkg/apis/stats/v1alpha1#MemoryStats) field of the [ContainerStats.Memory](https://pkg.go.dev/k8s.io/kubelet@v0.34.0/pkg/apis/stats/v1alpha1#ContainerStats) of the Kubelet's stats API. """ def create_container_paging_faults(meter: Meter) -> Counter: """Container memory paging faults""" return meter.create_counter( name=CONTAINER_PAGING_FAULTS, description="Container memory paging faults.", unit="{fault}", ) CONTAINER_UPTIME: Final = "container.uptime" """ The time the container has been running Instrument: gauge Unit: s Note: Instrumentations SHOULD use a gauge with type `double` and measure uptime in seconds as a floating point number with the highest precision available. The actual accuracy would depend on the instrumentation and operating system. """ def create_container_uptime(meter: Meter, callbacks: Sequence[CallbackT] | None) -> ObservableGauge: """The time the container has been running""" return meter.create_observable_gauge( name=CONTAINER_UPTIME, callbacks=callbacks, description="The time the container has been running.", unit="s", ) cpu_metrics.py000066400000000000000000000031761527070070700407670ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/src/opentelemetry/semconv/_incubating/metrics# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from collections.abc import Callable, Generator, Iterable, Sequence from typing import Final from opentelemetry.metrics import CallbackOptions, Counter, Meter, ObservableGauge, Observation # pylint: disable=invalid-name CallbackT = Callable[[CallbackOptions], Iterable[Observation]] | Generator[Iterable[Observation], CallbackOptions, None] CPU_FREQUENCY: Final = "cpu.frequency" """ Deprecated: Replaced by `system.cpu.frequency`. """ def create_cpu_frequency(meter: Meter, callbacks: Sequence[CallbackT] | None) -> ObservableGauge: """Deprecated. Use `system.cpu.frequency` instead""" return meter.create_observable_gauge( name=CPU_FREQUENCY, callbacks=callbacks, description="Deprecated. Use `system.cpu.frequency` instead.", unit="{Hz}", ) CPU_TIME: Final = "cpu.time" """ Deprecated: Replaced by `system.cpu.time`. """ def create_cpu_time(meter: Meter) -> Counter: """Deprecated. Use `system.cpu.time` instead""" return meter.create_counter( name=CPU_TIME, description="Deprecated. Use `system.cpu.time` instead.", unit="s", ) CPU_UTILIZATION: Final = "cpu.utilization" """ Deprecated: Replaced by `system.cpu.utilization`. """ def create_cpu_utilization(meter: Meter, callbacks: Sequence[CallbackT] | None) -> ObservableGauge: """Deprecated. Use `system.cpu.utilization` instead""" return meter.create_observable_gauge( name=CPU_UTILIZATION, callbacks=callbacks, description="Deprecated. Use `system.cpu.utilization` instead.", unit="1", ) cpython_metrics.py000066400000000000000000000042671527070070700416660ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/src/opentelemetry/semconv/_incubating/metrics# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from typing import Final from opentelemetry.metrics import Counter, Meter CPYTHON_GC_COLLECTED_OBJECTS: Final = "cpython.gc.collected_objects" """ The total number of objects collected inside a generation since interpreter start Instrument: counter Unit: {object} Note: This metric reports data from [`gc.stats()`](https://docs.python.org/3/library/gc.html#gc.get_stats). """ def create_cpython_gc_collected_objects(meter: Meter) -> Counter: """The total number of objects collected inside a generation since interpreter start""" return meter.create_counter( name=CPYTHON_GC_COLLECTED_OBJECTS, description="The total number of objects collected inside a generation since interpreter start.", unit="{object}", ) CPYTHON_GC_COLLECTIONS: Final = "cpython.gc.collections" """ The number of times a generation was collected since interpreter start Instrument: counter Unit: {collection} Note: This metric reports data from [`gc.stats()`](https://docs.python.org/3/library/gc.html#gc.get_stats). """ def create_cpython_gc_collections(meter: Meter) -> Counter: """The number of times a generation was collected since interpreter start""" return meter.create_counter( name=CPYTHON_GC_COLLECTIONS, description="The number of times a generation was collected since interpreter start.", unit="{collection}", ) CPYTHON_GC_UNCOLLECTABLE_OBJECTS: Final = "cpython.gc.uncollectable_objects" """ The total number of objects which were found to be uncollectable inside a generation since interpreter start Instrument: counter Unit: {object} Note: This metric reports data from [`gc.stats()`](https://docs.python.org/3/library/gc.html#gc.get_stats). """ def create_cpython_gc_uncollectable_objects(meter: Meter) -> Counter: """The total number of objects which were found to be uncollectable inside a generation since interpreter start""" return meter.create_counter( name=CPYTHON_GC_UNCOLLECTABLE_OBJECTS, description="The total number of objects which were found to be uncollectable inside a generation since interpreter start.", unit="{object}", ) db_metrics.py000066400000000000000000000267161527070070700405720ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/src/opentelemetry/semconv/_incubating/metrics# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from typing import Final from opentelemetry.metrics import Counter, Histogram, Meter, UpDownCounter DB_CLIENT_CONNECTION_COUNT: Final = "db.client.connection.count" """ The number of connections that are currently in state described by the `state` attribute Instrument: updowncounter Unit: {connection} """ def create_db_client_connection_count(meter: Meter) -> UpDownCounter: """The number of connections that are currently in state described by the `state` attribute""" return meter.create_up_down_counter( name=DB_CLIENT_CONNECTION_COUNT, description="The number of connections that are currently in state described by the `state` attribute.", unit="{connection}", ) DB_CLIENT_CONNECTION_CREATE_TIME: Final = "db.client.connection.create_time" """ The time it took to create a new connection Instrument: histogram Unit: s """ def create_db_client_connection_create_time(meter: Meter) -> Histogram: """The time it took to create a new connection""" return meter.create_histogram( name=DB_CLIENT_CONNECTION_CREATE_TIME, description="The time it took to create a new connection.", unit="s", ) DB_CLIENT_CONNECTION_IDLE_MAX: Final = "db.client.connection.idle.max" """ The maximum number of idle open connections allowed Instrument: updowncounter Unit: {connection} """ def create_db_client_connection_idle_max(meter: Meter) -> UpDownCounter: """The maximum number of idle open connections allowed""" return meter.create_up_down_counter( name=DB_CLIENT_CONNECTION_IDLE_MAX, description="The maximum number of idle open connections allowed.", unit="{connection}", ) DB_CLIENT_CONNECTION_IDLE_MIN: Final = "db.client.connection.idle.min" """ The minimum number of idle open connections allowed Instrument: updowncounter Unit: {connection} """ def create_db_client_connection_idle_min(meter: Meter) -> UpDownCounter: """The minimum number of idle open connections allowed""" return meter.create_up_down_counter( name=DB_CLIENT_CONNECTION_IDLE_MIN, description="The minimum number of idle open connections allowed.", unit="{connection}", ) DB_CLIENT_CONNECTION_MAX: Final = "db.client.connection.max" """ The maximum number of open connections allowed Instrument: updowncounter Unit: {connection} """ def create_db_client_connection_max(meter: Meter) -> UpDownCounter: """The maximum number of open connections allowed""" return meter.create_up_down_counter( name=DB_CLIENT_CONNECTION_MAX, description="The maximum number of open connections allowed.", unit="{connection}", ) DB_CLIENT_CONNECTION_PENDING_REQUESTS: Final = "db.client.connection.pending_requests" """ The number of current pending requests for an open connection Instrument: updowncounter Unit: {request} """ def create_db_client_connection_pending_requests(meter: Meter) -> UpDownCounter: """The number of current pending requests for an open connection""" return meter.create_up_down_counter( name=DB_CLIENT_CONNECTION_PENDING_REQUESTS, description="The number of current pending requests for an open connection.", unit="{request}", ) DB_CLIENT_CONNECTION_TIMEOUTS: Final = "db.client.connection.timeouts" """ The number of connection timeouts that have occurred trying to obtain a connection from the pool Instrument: counter Unit: {timeout} """ def create_db_client_connection_timeouts(meter: Meter) -> Counter: """The number of connection timeouts that have occurred trying to obtain a connection from the pool""" return meter.create_counter( name=DB_CLIENT_CONNECTION_TIMEOUTS, description="The number of connection timeouts that have occurred trying to obtain a connection from the pool.", unit="{timeout}", ) DB_CLIENT_CONNECTION_USE_TIME: Final = "db.client.connection.use_time" """ The time between borrowing a connection and returning it to the pool Instrument: histogram Unit: s """ def create_db_client_connection_use_time(meter: Meter) -> Histogram: """The time between borrowing a connection and returning it to the pool""" return meter.create_histogram( name=DB_CLIENT_CONNECTION_USE_TIME, description="The time between borrowing a connection and returning it to the pool.", unit="s", ) DB_CLIENT_CONNECTION_WAIT_TIME: Final = "db.client.connection.wait_time" """ The time it took to obtain an open connection from the pool Instrument: histogram Unit: s """ def create_db_client_connection_wait_time(meter: Meter) -> Histogram: """The time it took to obtain an open connection from the pool""" return meter.create_histogram( name=DB_CLIENT_CONNECTION_WAIT_TIME, description="The time it took to obtain an open connection from the pool.", unit="s", ) DB_CLIENT_CONNECTIONS_CREATE_TIME: Final = "db.client.connections.create_time" """ Deprecated: Replaced by `db.client.connection.create_time` with unit `s`. """ def create_db_client_connections_create_time(meter: Meter) -> Histogram: """Deprecated, use `db.client.connection.create_time` instead. Note: the unit also changed from `ms` to `s`""" return meter.create_histogram( name=DB_CLIENT_CONNECTIONS_CREATE_TIME, description="Deprecated, use `db.client.connection.create_time` instead. Note: the unit also changed from `ms` to `s`.", unit="ms", ) DB_CLIENT_CONNECTIONS_IDLE_MAX: Final = "db.client.connections.idle.max" """ Deprecated: Replaced by `db.client.connection.idle.max`. """ def create_db_client_connections_idle_max(meter: Meter) -> UpDownCounter: """Deprecated, use `db.client.connection.idle.max` instead""" return meter.create_up_down_counter( name=DB_CLIENT_CONNECTIONS_IDLE_MAX, description="Deprecated, use `db.client.connection.idle.max` instead.", unit="{connection}", ) DB_CLIENT_CONNECTIONS_IDLE_MIN: Final = "db.client.connections.idle.min" """ Deprecated: Replaced by `db.client.connection.idle.min`. """ def create_db_client_connections_idle_min(meter: Meter) -> UpDownCounter: """Deprecated, use `db.client.connection.idle.min` instead""" return meter.create_up_down_counter( name=DB_CLIENT_CONNECTIONS_IDLE_MIN, description="Deprecated, use `db.client.connection.idle.min` instead.", unit="{connection}", ) DB_CLIENT_CONNECTIONS_MAX: Final = "db.client.connections.max" """ Deprecated: Replaced by `db.client.connection.max`. """ def create_db_client_connections_max(meter: Meter) -> UpDownCounter: """Deprecated, use `db.client.connection.max` instead""" return meter.create_up_down_counter( name=DB_CLIENT_CONNECTIONS_MAX, description="Deprecated, use `db.client.connection.max` instead.", unit="{connection}", ) DB_CLIENT_CONNECTIONS_PENDING_REQUESTS: Final = "db.client.connections.pending_requests" """ Deprecated: Replaced by `db.client.connection.pending_requests`. """ def create_db_client_connections_pending_requests(meter: Meter) -> UpDownCounter: """Deprecated, use `db.client.connection.pending_requests` instead""" return meter.create_up_down_counter( name=DB_CLIENT_CONNECTIONS_PENDING_REQUESTS, description="Deprecated, use `db.client.connection.pending_requests` instead.", unit="{request}", ) DB_CLIENT_CONNECTIONS_TIMEOUTS: Final = "db.client.connections.timeouts" """ Deprecated: Replaced by `db.client.connection.timeouts`. """ def create_db_client_connections_timeouts(meter: Meter) -> Counter: """Deprecated, use `db.client.connection.timeouts` instead""" return meter.create_counter( name=DB_CLIENT_CONNECTIONS_TIMEOUTS, description="Deprecated, use `db.client.connection.timeouts` instead.", unit="{timeout}", ) DB_CLIENT_CONNECTIONS_USAGE: Final = "db.client.connections.usage" """ Deprecated: Replaced by `db.client.connection.count`. """ def create_db_client_connections_usage(meter: Meter) -> UpDownCounter: """Deprecated, use `db.client.connection.count` instead""" return meter.create_up_down_counter( name=DB_CLIENT_CONNECTIONS_USAGE, description="Deprecated, use `db.client.connection.count` instead.", unit="{connection}", ) DB_CLIENT_CONNECTIONS_USE_TIME: Final = "db.client.connections.use_time" """ Deprecated: Replaced by `db.client.connection.use_time` with unit `s`. """ def create_db_client_connections_use_time(meter: Meter) -> Histogram: """Deprecated, use `db.client.connection.use_time` instead. Note: the unit also changed from `ms` to `s`""" return meter.create_histogram( name=DB_CLIENT_CONNECTIONS_USE_TIME, description="Deprecated, use `db.client.connection.use_time` instead. Note: the unit also changed from `ms` to `s`.", unit="ms", ) DB_CLIENT_CONNECTIONS_WAIT_TIME: Final = "db.client.connections.wait_time" """ Deprecated: Replaced by `db.client.connection.wait_time` with unit `s`. """ def create_db_client_connections_wait_time(meter: Meter) -> Histogram: """Deprecated, use `db.client.connection.wait_time` instead. Note: the unit also changed from `ms` to `s`""" return meter.create_histogram( name=DB_CLIENT_CONNECTIONS_WAIT_TIME, description="Deprecated, use `db.client.connection.wait_time` instead. Note: the unit also changed from `ms` to `s`.", unit="ms", ) DB_CLIENT_COSMOSDB_ACTIVE_INSTANCE_COUNT: Final = "db.client.cosmosdb.active_instance.count" """ Deprecated: Replaced by `azure.cosmosdb.client.active_instance.count`. """ def create_db_client_cosmosdb_active_instance_count(meter: Meter) -> UpDownCounter: """Deprecated, use `azure.cosmosdb.client.active_instance.count` instead""" return meter.create_up_down_counter( name=DB_CLIENT_COSMOSDB_ACTIVE_INSTANCE_COUNT, description="Deprecated, use `azure.cosmosdb.client.active_instance.count` instead.", unit="{instance}", ) DB_CLIENT_COSMOSDB_OPERATION_REQUEST_CHARGE: Final = "db.client.cosmosdb.operation.request_charge" """ Deprecated: Replaced by `azure.cosmosdb.client.operation.request_charge`. """ def create_db_client_cosmosdb_operation_request_charge(meter: Meter) -> Histogram: """Deprecated, use `azure.cosmosdb.client.operation.request_charge` instead""" return meter.create_histogram( name=DB_CLIENT_COSMOSDB_OPERATION_REQUEST_CHARGE, description="Deprecated, use `azure.cosmosdb.client.operation.request_charge` instead.", unit="{request_unit}", ) DB_CLIENT_OPERATION_DURATION: Final = "db.client.operation.duration" """ Deprecated in favor of stable :py:const:`opentelemetry.semconv.metrics.db_metrics.DB_CLIENT_OPERATION_DURATION`. """ def create_db_client_operation_duration(meter: Meter) -> Histogram: """Duration of database client operations""" return meter.create_histogram( name=DB_CLIENT_OPERATION_DURATION, description="Duration of database client operations.", unit="s", ) DB_CLIENT_RESPONSE_RETURNED_ROWS: Final = "db.client.response.returned_rows" """ The actual number of records returned by the database operation Instrument: histogram Unit: {row} """ def create_db_client_response_returned_rows(meter: Meter) -> Histogram: """The actual number of records returned by the database operation""" return meter.create_histogram( name=DB_CLIENT_RESPONSE_RETURNED_ROWS, description="The actual number of records returned by the database operation.", unit="{row}", ) dns_metrics.py000066400000000000000000000011011527070070700407460ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/src/opentelemetry/semconv/_incubating/metrics# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from typing import Final from opentelemetry.metrics import Histogram, Meter DNS_LOOKUP_DURATION: Final = "dns.lookup.duration" """ Measures the time taken to perform a DNS lookup Instrument: histogram Unit: s """ def create_dns_lookup_duration(meter: Meter) -> Histogram: """Measures the time taken to perform a DNS lookup""" return meter.create_histogram( name=DNS_LOOKUP_DURATION, description="Measures the time taken to perform a DNS lookup.", unit="s", ) faas_metrics.py000066400000000000000000000072351527070070700411120ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/src/opentelemetry/semconv/_incubating/metrics# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from typing import Final from opentelemetry.metrics import Counter, Histogram, Meter FAAS_COLDSTARTS: Final = "faas.coldstarts" """ Number of invocation cold starts Instrument: counter Unit: {coldstart} """ def create_faas_coldstarts(meter: Meter) -> Counter: """Number of invocation cold starts""" return meter.create_counter( name=FAAS_COLDSTARTS, description="Number of invocation cold starts.", unit="{coldstart}", ) FAAS_CPU_USAGE: Final = "faas.cpu_usage" """ Distribution of CPU usage per invocation Instrument: histogram Unit: s """ def create_faas_cpu_usage(meter: Meter) -> Histogram: """Distribution of CPU usage per invocation""" return meter.create_histogram( name=FAAS_CPU_USAGE, description="Distribution of CPU usage per invocation.", unit="s", ) FAAS_ERRORS: Final = "faas.errors" """ Number of invocation errors Instrument: counter Unit: {error} """ def create_faas_errors(meter: Meter) -> Counter: """Number of invocation errors""" return meter.create_counter( name=FAAS_ERRORS, description="Number of invocation errors.", unit="{error}", ) FAAS_INIT_DURATION: Final = "faas.init_duration" """ Measures the duration of the function's initialization, such as a cold start Instrument: histogram Unit: s """ def create_faas_init_duration(meter: Meter) -> Histogram: """Measures the duration of the function's initialization, such as a cold start""" return meter.create_histogram( name=FAAS_INIT_DURATION, description="Measures the duration of the function's initialization, such as a cold start.", unit="s", ) FAAS_INVOCATIONS: Final = "faas.invocations" """ Number of successful invocations Instrument: counter Unit: {invocation} """ def create_faas_invocations(meter: Meter) -> Counter: """Number of successful invocations""" return meter.create_counter( name=FAAS_INVOCATIONS, description="Number of successful invocations.", unit="{invocation}", ) FAAS_INVOKE_DURATION: Final = "faas.invoke_duration" """ Measures the duration of the function's logic execution Instrument: histogram Unit: s """ def create_faas_invoke_duration(meter: Meter) -> Histogram: """Measures the duration of the function's logic execution""" return meter.create_histogram( name=FAAS_INVOKE_DURATION, description="Measures the duration of the function's logic execution.", unit="s", ) FAAS_MEM_USAGE: Final = "faas.mem_usage" """ Distribution of max memory usage per invocation Instrument: histogram Unit: By """ def create_faas_mem_usage(meter: Meter) -> Histogram: """Distribution of max memory usage per invocation""" return meter.create_histogram( name=FAAS_MEM_USAGE, description="Distribution of max memory usage per invocation.", unit="By", ) FAAS_NET_IO: Final = "faas.net_io" """ Distribution of net I/O usage per invocation Instrument: histogram Unit: By """ def create_faas_net_io(meter: Meter) -> Histogram: """Distribution of net I/O usage per invocation""" return meter.create_histogram( name=FAAS_NET_IO, description="Distribution of net I/O usage per invocation.", unit="By", ) FAAS_TIMEOUTS: Final = "faas.timeouts" """ Number of invocation timeouts Instrument: counter Unit: {timeout} """ def create_faas_timeouts(meter: Meter) -> Counter: """Number of invocation timeouts""" return meter.create_counter( name=FAAS_TIMEOUTS, description="Number of invocation timeouts.", unit="{timeout}", ) gen_ai_metrics.py000066400000000000000000000107071527070070700414200ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/src/opentelemetry/semconv/_incubating/metrics# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from typing import Final from opentelemetry.metrics import Histogram, Meter GEN_AI_CLIENT_OPERATION_DURATION: Final = "gen_ai.client.operation.duration" """ Deprecated: Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai). """ def create_gen_ai_client_operation_duration(meter: Meter) -> Histogram: """GenAI operation duration""" return meter.create_histogram( name=GEN_AI_CLIENT_OPERATION_DURATION, description="GenAI operation duration.", unit="s", ) GEN_AI_CLIENT_OPERATION_TIME_PER_OUTPUT_CHUNK: Final = "gen_ai.client.operation.time_per_output_chunk" """ Deprecated: Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai). """ def create_gen_ai_client_operation_time_per_output_chunk(meter: Meter) -> Histogram: """Time per output chunk, recorded for each chunk received after the first one, measured as the time elapsed from the end of the previous chunk to the end of the current chunk""" return meter.create_histogram( name=GEN_AI_CLIENT_OPERATION_TIME_PER_OUTPUT_CHUNK, description="Time per output chunk, recorded for each chunk received after the first one, measured as the time elapsed from the end of the previous chunk to the end of the current chunk.", unit="s", ) GEN_AI_CLIENT_OPERATION_TIME_TO_FIRST_CHUNK: Final = "gen_ai.client.operation.time_to_first_chunk" """ Deprecated: Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai). """ def create_gen_ai_client_operation_time_to_first_chunk(meter: Meter) -> Histogram: """Time to receive the first chunk, measured from when the client issues the generation request to when the first chunk is received in the response stream""" return meter.create_histogram( name=GEN_AI_CLIENT_OPERATION_TIME_TO_FIRST_CHUNK, description="Time to receive the first chunk, measured from when the client issues the generation request to when the first chunk is received in the response stream.", unit="s", ) GEN_AI_CLIENT_TOKEN_USAGE: Final = "gen_ai.client.token.usage" """ Deprecated: Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai). """ def create_gen_ai_client_token_usage(meter: Meter) -> Histogram: """Number of input and output tokens used""" return meter.create_histogram( name=GEN_AI_CLIENT_TOKEN_USAGE, description="Number of input and output tokens used.", unit="{token}", ) GEN_AI_SERVER_REQUEST_DURATION: Final = "gen_ai.server.request.duration" """ Deprecated: Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai). """ def create_gen_ai_server_request_duration(meter: Meter) -> Histogram: """Generative AI server request duration such as time-to-last byte or last output token""" return meter.create_histogram( name=GEN_AI_SERVER_REQUEST_DURATION, description="Generative AI server request duration such as time-to-last byte or last output token.", unit="s", ) GEN_AI_SERVER_TIME_PER_OUTPUT_TOKEN: Final = "gen_ai.server.time_per_output_token" """ Deprecated: Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai). """ def create_gen_ai_server_time_per_output_token(meter: Meter) -> Histogram: """Time per output token generated after the first token for successful responses""" return meter.create_histogram( name=GEN_AI_SERVER_TIME_PER_OUTPUT_TOKEN, description="Time per output token generated after the first token for successful responses.", unit="s", ) GEN_AI_SERVER_TIME_TO_FIRST_TOKEN: Final = "gen_ai.server.time_to_first_token" """ Deprecated: Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai). """ def create_gen_ai_server_time_to_first_token(meter: Meter) -> Histogram: """Time to generate first token for successful responses""" return meter.create_histogram( name=GEN_AI_SERVER_TIME_TO_FIRST_TOKEN, description="Time to generate first token for successful responses.", unit="s", ) http_metrics.py000066400000000000000000000137751527070070700411650ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/src/opentelemetry/semconv/_incubating/metrics# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from typing import Final from opentelemetry.metrics import Histogram, Meter, UpDownCounter HTTP_CLIENT_ACTIVE_REQUESTS: Final = "http.client.active_requests" """ Number of active HTTP requests Instrument: updowncounter Unit: {request} """ def create_http_client_active_requests(meter: Meter) -> UpDownCounter: """Number of active HTTP requests""" return meter.create_up_down_counter( name=HTTP_CLIENT_ACTIVE_REQUESTS, description="Number of active HTTP requests.", unit="{request}", ) HTTP_CLIENT_CONNECTION_DURATION: Final = "http.client.connection.duration" """ The duration of the successfully established outbound HTTP connections Instrument: histogram Unit: s """ def create_http_client_connection_duration(meter: Meter) -> Histogram: """The duration of the successfully established outbound HTTP connections""" return meter.create_histogram( name=HTTP_CLIENT_CONNECTION_DURATION, description="The duration of the successfully established outbound HTTP connections.", unit="s", ) HTTP_CLIENT_OPEN_CONNECTIONS: Final = "http.client.open_connections" """ Number of outbound HTTP connections that are currently active or idle on the client Instrument: updowncounter Unit: {connection} """ def create_http_client_open_connections(meter: Meter) -> UpDownCounter: """Number of outbound HTTP connections that are currently active or idle on the client""" return meter.create_up_down_counter( name=HTTP_CLIENT_OPEN_CONNECTIONS, description="Number of outbound HTTP connections that are currently active or idle on the client.", unit="{connection}", ) HTTP_CLIENT_REQUEST_BODY_SIZE: Final = "http.client.request.body.size" """ Size of HTTP client request bodies Instrument: histogram Unit: By Note: The size of the request payload body in bytes. This is the number of bytes transferred excluding headers and is often, but not always, present as the [Content-Length](https://www.rfc-editor.org/rfc/rfc9110.html#field.content-length) header. For requests using transport encoding, this should be the compressed size. """ def create_http_client_request_body_size(meter: Meter) -> Histogram: """Size of HTTP client request bodies""" return meter.create_histogram( name=HTTP_CLIENT_REQUEST_BODY_SIZE, description="Size of HTTP client request bodies.", unit="By", ) HTTP_CLIENT_REQUEST_DURATION: Final = "http.client.request.duration" """ Deprecated in favor of stable :py:const:`opentelemetry.semconv.metrics.http_metrics.HTTP_CLIENT_REQUEST_DURATION`. """ def create_http_client_request_duration(meter: Meter) -> Histogram: """Duration of HTTP client requests""" return meter.create_histogram( name=HTTP_CLIENT_REQUEST_DURATION, description="Duration of HTTP client requests.", unit="s", ) HTTP_CLIENT_RESPONSE_BODY_SIZE: Final = "http.client.response.body.size" """ Size of HTTP client response bodies Instrument: histogram Unit: By Note: The size of the response payload body in bytes. This is the number of bytes transferred excluding headers and is often, but not always, present as the [Content-Length](https://www.rfc-editor.org/rfc/rfc9110.html#field.content-length) header. For requests using transport encoding, this should be the compressed size. """ def create_http_client_response_body_size(meter: Meter) -> Histogram: """Size of HTTP client response bodies""" return meter.create_histogram( name=HTTP_CLIENT_RESPONSE_BODY_SIZE, description="Size of HTTP client response bodies.", unit="By", ) HTTP_SERVER_ACTIVE_REQUESTS: Final = "http.server.active_requests" """ Number of active HTTP server requests Instrument: updowncounter Unit: {request} """ def create_http_server_active_requests(meter: Meter) -> UpDownCounter: """Number of active HTTP server requests""" return meter.create_up_down_counter( name=HTTP_SERVER_ACTIVE_REQUESTS, description="Number of active HTTP server requests.", unit="{request}", ) HTTP_SERVER_REQUEST_BODY_SIZE: Final = "http.server.request.body.size" """ Size of HTTP server request bodies Instrument: histogram Unit: By Note: The size of the request payload body in bytes. This is the number of bytes transferred excluding headers and is often, but not always, present as the [Content-Length](https://www.rfc-editor.org/rfc/rfc9110.html#field.content-length) header. For requests using transport encoding, this should be the compressed size. """ def create_http_server_request_body_size(meter: Meter) -> Histogram: """Size of HTTP server request bodies""" return meter.create_histogram( name=HTTP_SERVER_REQUEST_BODY_SIZE, description="Size of HTTP server request bodies.", unit="By", ) HTTP_SERVER_REQUEST_DURATION: Final = "http.server.request.duration" """ Deprecated in favor of stable :py:const:`opentelemetry.semconv.metrics.http_metrics.HTTP_SERVER_REQUEST_DURATION`. """ def create_http_server_request_duration(meter: Meter) -> Histogram: """Duration of HTTP server requests""" return meter.create_histogram( name=HTTP_SERVER_REQUEST_DURATION, description="Duration of HTTP server requests.", unit="s", ) HTTP_SERVER_RESPONSE_BODY_SIZE: Final = "http.server.response.body.size" """ Size of HTTP server response bodies Instrument: histogram Unit: By Note: The size of the response payload body in bytes. This is the number of bytes transferred excluding headers and is often, but not always, present as the [Content-Length](https://www.rfc-editor.org/rfc/rfc9110.html#field.content-length) header. For requests using transport encoding, this should be the compressed size. """ def create_http_server_response_body_size(meter: Meter) -> Histogram: """Size of HTTP server response bodies""" return meter.create_histogram( name=HTTP_SERVER_RESPONSE_BODY_SIZE, description="Size of HTTP server response bodies.", unit="By", ) hw_metrics.py000066400000000000000000000506651527070070700406230ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/src/opentelemetry/semconv/_incubating/metrics# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from collections.abc import Callable, Generator, Iterable, Sequence from typing import Final from opentelemetry.metrics import CallbackOptions, Counter, Meter, ObservableGauge, Observation, UpDownCounter # pylint: disable=invalid-name CallbackT = Callable[[CallbackOptions], Iterable[Observation]] | Generator[Iterable[Observation], CallbackOptions, None] HW_BATTERY_CHARGE: Final = "hw.battery.charge" """ Remaining fraction of battery charge Instrument: gauge Unit: 1 """ def create_hw_battery_charge(meter: Meter, callbacks: Sequence[CallbackT] | None) -> ObservableGauge: """Remaining fraction of battery charge""" return meter.create_observable_gauge( name=HW_BATTERY_CHARGE, callbacks=callbacks, description="Remaining fraction of battery charge.", unit="1", ) HW_BATTERY_CHARGE_LIMIT: Final = "hw.battery.charge.limit" """ Lower limit of battery charge fraction to ensure proper operation Instrument: gauge Unit: 1 """ def create_hw_battery_charge_limit(meter: Meter, callbacks: Sequence[CallbackT] | None) -> ObservableGauge: """Lower limit of battery charge fraction to ensure proper operation""" return meter.create_observable_gauge( name=HW_BATTERY_CHARGE_LIMIT, callbacks=callbacks, description="Lower limit of battery charge fraction to ensure proper operation.", unit="1", ) HW_BATTERY_TIME_LEFT: Final = "hw.battery.time_left" """ Time left before battery is completely charged or discharged Instrument: gauge Unit: s """ def create_hw_battery_time_left(meter: Meter, callbacks: Sequence[CallbackT] | None) -> ObservableGauge: """Time left before battery is completely charged or discharged""" return meter.create_observable_gauge( name=HW_BATTERY_TIME_LEFT, callbacks=callbacks, description="Time left before battery is completely charged or discharged.", unit="s", ) HW_CPU_SPEED: Final = "hw.cpu.speed" """ CPU current frequency Instrument: gauge Unit: Hz """ def create_hw_cpu_speed(meter: Meter, callbacks: Sequence[CallbackT] | None) -> ObservableGauge: """CPU current frequency""" return meter.create_observable_gauge( name=HW_CPU_SPEED, callbacks=callbacks, description="CPU current frequency.", unit="Hz", ) HW_CPU_SPEED_LIMIT: Final = "hw.cpu.speed.limit" """ CPU maximum frequency Instrument: gauge Unit: Hz """ def create_hw_cpu_speed_limit(meter: Meter, callbacks: Sequence[CallbackT] | None) -> ObservableGauge: """CPU maximum frequency""" return meter.create_observable_gauge( name=HW_CPU_SPEED_LIMIT, callbacks=callbacks, description="CPU maximum frequency.", unit="Hz", ) HW_ENERGY: Final = "hw.energy" """ Energy consumed by the component Instrument: counter Unit: J """ def create_hw_energy(meter: Meter) -> Counter: """Energy consumed by the component""" return meter.create_counter( name=HW_ENERGY, description="Energy consumed by the component.", unit="J", ) HW_ERRORS: Final = "hw.errors" """ Number of errors encountered by the component Instrument: counter Unit: {error} """ def create_hw_errors(meter: Meter) -> Counter: """Number of errors encountered by the component""" return meter.create_counter( name=HW_ERRORS, description="Number of errors encountered by the component.", unit="{error}", ) HW_FAN_SPEED: Final = "hw.fan.speed" """ Fan speed in revolutions per minute Instrument: gauge Unit: rpm """ def create_hw_fan_speed(meter: Meter, callbacks: Sequence[CallbackT] | None) -> ObservableGauge: """Fan speed in revolutions per minute""" return meter.create_observable_gauge( name=HW_FAN_SPEED, callbacks=callbacks, description="Fan speed in revolutions per minute.", unit="rpm", ) HW_FAN_SPEED_LIMIT: Final = "hw.fan.speed.limit" """ Speed limit in rpm Instrument: gauge Unit: rpm """ def create_hw_fan_speed_limit(meter: Meter, callbacks: Sequence[CallbackT] | None) -> ObservableGauge: """Speed limit in rpm""" return meter.create_observable_gauge( name=HW_FAN_SPEED_LIMIT, callbacks=callbacks, description="Speed limit in rpm.", unit="rpm", ) HW_FAN_SPEED_RATIO: Final = "hw.fan.speed_ratio" """ Fan speed expressed as a fraction of its maximum speed Instrument: gauge Unit: 1 """ def create_hw_fan_speed_ratio(meter: Meter, callbacks: Sequence[CallbackT] | None) -> ObservableGauge: """Fan speed expressed as a fraction of its maximum speed""" return meter.create_observable_gauge( name=HW_FAN_SPEED_RATIO, callbacks=callbacks, description="Fan speed expressed as a fraction of its maximum speed.", unit="1", ) HW_GPU_IO: Final = "hw.gpu.io" """ Received and transmitted bytes by the GPU Instrument: counter Unit: By """ def create_hw_gpu_io(meter: Meter) -> Counter: """Received and transmitted bytes by the GPU""" return meter.create_counter( name=HW_GPU_IO, description="Received and transmitted bytes by the GPU.", unit="By", ) HW_GPU_MEMORY_LIMIT: Final = "hw.gpu.memory.limit" """ Size of the GPU memory Instrument: updowncounter Unit: By """ def create_hw_gpu_memory_limit(meter: Meter) -> UpDownCounter: """Size of the GPU memory""" return meter.create_up_down_counter( name=HW_GPU_MEMORY_LIMIT, description="Size of the GPU memory.", unit="By", ) HW_GPU_MEMORY_USAGE: Final = "hw.gpu.memory.usage" """ GPU memory used Instrument: updowncounter Unit: By """ def create_hw_gpu_memory_usage(meter: Meter) -> UpDownCounter: """GPU memory used""" return meter.create_up_down_counter( name=HW_GPU_MEMORY_USAGE, description="GPU memory used.", unit="By", ) HW_GPU_MEMORY_UTILIZATION: Final = "hw.gpu.memory.utilization" """ Fraction of GPU memory used Instrument: gauge Unit: 1 """ def create_hw_gpu_memory_utilization(meter: Meter, callbacks: Sequence[CallbackT] | None) -> ObservableGauge: """Fraction of GPU memory used""" return meter.create_observable_gauge( name=HW_GPU_MEMORY_UTILIZATION, callbacks=callbacks, description="Fraction of GPU memory used.", unit="1", ) HW_GPU_UTILIZATION: Final = "hw.gpu.utilization" """ Fraction of time spent in a specific task Instrument: gauge Unit: 1 """ def create_hw_gpu_utilization(meter: Meter, callbacks: Sequence[CallbackT] | None) -> ObservableGauge: """Fraction of time spent in a specific task""" return meter.create_observable_gauge( name=HW_GPU_UTILIZATION, callbacks=callbacks, description="Fraction of time spent in a specific task.", unit="1", ) HW_HOST_AMBIENT_TEMPERATURE: Final = "hw.host.ambient_temperature" """ Ambient (external) temperature of the physical host Instrument: gauge Unit: Cel """ def create_hw_host_ambient_temperature(meter: Meter, callbacks: Sequence[CallbackT] | None) -> ObservableGauge: """Ambient (external) temperature of the physical host""" return meter.create_observable_gauge( name=HW_HOST_AMBIENT_TEMPERATURE, callbacks=callbacks, description="Ambient (external) temperature of the physical host.", unit="Cel", ) HW_HOST_ENERGY: Final = "hw.host.energy" """ Total energy consumed by the entire physical host, in joules Instrument: counter Unit: J Note: The overall energy usage of a host MUST be reported using the specific `hw.host.energy` and `hw.host.power` metrics **only**, instead of the generic `hw.energy` and `hw.power` described in the previous section, to prevent summing up overlapping values. """ def create_hw_host_energy(meter: Meter) -> Counter: """Total energy consumed by the entire physical host, in joules""" return meter.create_counter( name=HW_HOST_ENERGY, description="Total energy consumed by the entire physical host, in joules.", unit="J", ) HW_HOST_HEATING_MARGIN: Final = "hw.host.heating_margin" """ By how many degrees Celsius the temperature of the physical host can be increased, before reaching a warning threshold on one of the internal sensors Instrument: gauge Unit: Cel """ def create_hw_host_heating_margin(meter: Meter, callbacks: Sequence[CallbackT] | None) -> ObservableGauge: """By how many degrees Celsius the temperature of the physical host can be increased, before reaching a warning threshold on one of the internal sensors""" return meter.create_observable_gauge( name=HW_HOST_HEATING_MARGIN, callbacks=callbacks, description="By how many degrees Celsius the temperature of the physical host can be increased, before reaching a warning threshold on one of the internal sensors.", unit="Cel", ) HW_HOST_POWER: Final = "hw.host.power" """ Instantaneous power consumed by the entire physical host in Watts (`hw.host.energy` is preferred) Instrument: gauge Unit: W Note: The overall energy usage of a host MUST be reported using the specific `hw.host.energy` and `hw.host.power` metrics **only**, instead of the generic `hw.energy` and `hw.power` described in the previous section, to prevent summing up overlapping values. """ def create_hw_host_power(meter: Meter, callbacks: Sequence[CallbackT] | None) -> ObservableGauge: """Instantaneous power consumed by the entire physical host in Watts (`hw.host.energy` is preferred)""" return meter.create_observable_gauge( name=HW_HOST_POWER, callbacks=callbacks, description="Instantaneous power consumed by the entire physical host in Watts (`hw.host.energy` is preferred).", unit="W", ) HW_LOGICAL_DISK_LIMIT: Final = "hw.logical_disk.limit" """ Size of the logical disk Instrument: updowncounter Unit: By """ def create_hw_logical_disk_limit(meter: Meter) -> UpDownCounter: """Size of the logical disk""" return meter.create_up_down_counter( name=HW_LOGICAL_DISK_LIMIT, description="Size of the logical disk.", unit="By", ) HW_LOGICAL_DISK_USAGE: Final = "hw.logical_disk.usage" """ Logical disk space usage Instrument: updowncounter Unit: By """ def create_hw_logical_disk_usage(meter: Meter) -> UpDownCounter: """Logical disk space usage""" return meter.create_up_down_counter( name=HW_LOGICAL_DISK_USAGE, description="Logical disk space usage.", unit="By", ) HW_LOGICAL_DISK_UTILIZATION: Final = "hw.logical_disk.utilization" """ Logical disk space utilization as a fraction Instrument: gauge Unit: 1 """ def create_hw_logical_disk_utilization(meter: Meter, callbacks: Sequence[CallbackT] | None) -> ObservableGauge: """Logical disk space utilization as a fraction""" return meter.create_observable_gauge( name=HW_LOGICAL_DISK_UTILIZATION, callbacks=callbacks, description="Logical disk space utilization as a fraction.", unit="1", ) HW_MEMORY_SIZE: Final = "hw.memory.size" """ Size of the memory module Instrument: updowncounter Unit: By """ def create_hw_memory_size(meter: Meter) -> UpDownCounter: """Size of the memory module""" return meter.create_up_down_counter( name=HW_MEMORY_SIZE, description="Size of the memory module.", unit="By", ) HW_NETWORK_BANDWIDTH_LIMIT: Final = "hw.network.bandwidth.limit" """ Link speed Instrument: updowncounter Unit: By/s """ def create_hw_network_bandwidth_limit(meter: Meter) -> UpDownCounter: """Link speed""" return meter.create_up_down_counter( name=HW_NETWORK_BANDWIDTH_LIMIT, description="Link speed.", unit="By/s", ) HW_NETWORK_BANDWIDTH_UTILIZATION: Final = "hw.network.bandwidth.utilization" """ Utilization of the network bandwidth as a fraction Instrument: gauge Unit: 1 """ def create_hw_network_bandwidth_utilization(meter: Meter, callbacks: Sequence[CallbackT] | None) -> ObservableGauge: """Utilization of the network bandwidth as a fraction""" return meter.create_observable_gauge( name=HW_NETWORK_BANDWIDTH_UTILIZATION, callbacks=callbacks, description="Utilization of the network bandwidth as a fraction.", unit="1", ) HW_NETWORK_IO: Final = "hw.network.io" """ Received and transmitted network traffic in bytes Instrument: counter Unit: By """ def create_hw_network_io(meter: Meter) -> Counter: """Received and transmitted network traffic in bytes""" return meter.create_counter( name=HW_NETWORK_IO, description="Received and transmitted network traffic in bytes.", unit="By", ) HW_NETWORK_PACKETS: Final = "hw.network.packets" """ Received and transmitted network traffic in packets (or frames) Instrument: counter Unit: {packet} """ def create_hw_network_packets(meter: Meter) -> Counter: """Received and transmitted network traffic in packets (or frames)""" return meter.create_counter( name=HW_NETWORK_PACKETS, description="Received and transmitted network traffic in packets (or frames).", unit="{packet}", ) HW_NETWORK_UP: Final = "hw.network.up" """ Link status: `1` (up) or `0` (down) Instrument: updowncounter Unit: 1 """ def create_hw_network_up(meter: Meter) -> UpDownCounter: """Link status: `1` (up) or `0` (down)""" return meter.create_up_down_counter( name=HW_NETWORK_UP, description="Link status: `1` (up) or `0` (down).", unit="1", ) HW_PHYSICAL_DISK_ENDURANCE_UTILIZATION: Final = "hw.physical_disk.endurance_utilization" """ Endurance remaining for this SSD disk Instrument: gauge Unit: 1 """ def create_hw_physical_disk_endurance_utilization( meter: Meter, callbacks: Sequence[CallbackT] | None ) -> ObservableGauge: """Endurance remaining for this SSD disk""" return meter.create_observable_gauge( name=HW_PHYSICAL_DISK_ENDURANCE_UTILIZATION, callbacks=callbacks, description="Endurance remaining for this SSD disk.", unit="1", ) HW_PHYSICAL_DISK_SIZE: Final = "hw.physical_disk.size" """ Size of the disk Instrument: updowncounter Unit: By """ def create_hw_physical_disk_size(meter: Meter) -> UpDownCounter: """Size of the disk""" return meter.create_up_down_counter( name=HW_PHYSICAL_DISK_SIZE, description="Size of the disk.", unit="By", ) HW_PHYSICAL_DISK_SMART: Final = "hw.physical_disk.smart" """ Value of the corresponding [S.M.A.R.T.](https://wikipedia.org/wiki/S.M.A.R.T.) (Self-Monitoring, Analysis, and Reporting Technology) attribute Instrument: gauge Unit: 1 """ def create_hw_physical_disk_smart(meter: Meter, callbacks: Sequence[CallbackT] | None) -> ObservableGauge: """Value of the corresponding [S.M.A.R.T.](https://wikipedia.org/wiki/S.M.A.R.T.) (Self-Monitoring, Analysis, and Reporting Technology) attribute""" return meter.create_observable_gauge( name=HW_PHYSICAL_DISK_SMART, callbacks=callbacks, description="Value of the corresponding [S.M.A.R.T.](https://wikipedia.org/wiki/S.M.A.R.T.) (Self-Monitoring, Analysis, and Reporting Technology) attribute.", unit="1", ) HW_POWER: Final = "hw.power" """ Instantaneous power consumed by the component Instrument: gauge Unit: W Note: It is recommended to report `hw.energy` instead of `hw.power` when possible. """ def create_hw_power(meter: Meter, callbacks: Sequence[CallbackT] | None) -> ObservableGauge: """Instantaneous power consumed by the component""" return meter.create_observable_gauge( name=HW_POWER, callbacks=callbacks, description="Instantaneous power consumed by the component.", unit="W", ) HW_POWER_SUPPLY_LIMIT: Final = "hw.power_supply.limit" """ Maximum power output of the power supply Instrument: updowncounter Unit: W """ def create_hw_power_supply_limit(meter: Meter) -> UpDownCounter: """Maximum power output of the power supply""" return meter.create_up_down_counter( name=HW_POWER_SUPPLY_LIMIT, description="Maximum power output of the power supply.", unit="W", ) HW_POWER_SUPPLY_USAGE: Final = "hw.power_supply.usage" """ Current power output of the power supply Instrument: updowncounter Unit: W """ def create_hw_power_supply_usage(meter: Meter) -> UpDownCounter: """Current power output of the power supply""" return meter.create_up_down_counter( name=HW_POWER_SUPPLY_USAGE, description="Current power output of the power supply.", unit="W", ) HW_POWER_SUPPLY_UTILIZATION: Final = "hw.power_supply.utilization" """ Utilization of the power supply as a fraction of its maximum output Instrument: gauge Unit: 1 """ def create_hw_power_supply_utilization(meter: Meter, callbacks: Sequence[CallbackT] | None) -> ObservableGauge: """Utilization of the power supply as a fraction of its maximum output""" return meter.create_observable_gauge( name=HW_POWER_SUPPLY_UTILIZATION, callbacks=callbacks, description="Utilization of the power supply as a fraction of its maximum output.", unit="1", ) HW_STATUS: Final = "hw.status" """ Operational status: `1` (true) or `0` (false) for each of the possible states Instrument: updowncounter Unit: 1 Note: `hw.status` is currently specified as an *UpDownCounter* but would ideally be represented using a [*StateSet* as defined in OpenMetrics](https://github.com/prometheus/OpenMetrics/blob/v1.0.0/specification/OpenMetrics.md#stateset). This semantic convention will be updated once *StateSet* is specified in OpenTelemetry. This planned change is not expected to have any consequence on the way users query their timeseries backend to retrieve the values of `hw.status` over time. """ def create_hw_status(meter: Meter) -> UpDownCounter: """Operational status: `1` (true) or `0` (false) for each of the possible states""" return meter.create_up_down_counter( name=HW_STATUS, description="Operational status: `1` (true) or `0` (false) for each of the possible states.", unit="1", ) HW_TAPE_DRIVE_OPERATIONS: Final = "hw.tape_drive.operations" """ Operations performed by the tape drive Instrument: counter Unit: {operation} """ def create_hw_tape_drive_operations(meter: Meter) -> Counter: """Operations performed by the tape drive""" return meter.create_counter( name=HW_TAPE_DRIVE_OPERATIONS, description="Operations performed by the tape drive.", unit="{operation}", ) HW_TEMPERATURE: Final = "hw.temperature" """ Temperature in degrees Celsius Instrument: gauge Unit: Cel """ def create_hw_temperature(meter: Meter, callbacks: Sequence[CallbackT] | None) -> ObservableGauge: """Temperature in degrees Celsius""" return meter.create_observable_gauge( name=HW_TEMPERATURE, callbacks=callbacks, description="Temperature in degrees Celsius.", unit="Cel", ) HW_TEMPERATURE_LIMIT: Final = "hw.temperature.limit" """ Temperature limit in degrees Celsius Instrument: gauge Unit: Cel """ def create_hw_temperature_limit(meter: Meter, callbacks: Sequence[CallbackT] | None) -> ObservableGauge: """Temperature limit in degrees Celsius""" return meter.create_observable_gauge( name=HW_TEMPERATURE_LIMIT, callbacks=callbacks, description="Temperature limit in degrees Celsius.", unit="Cel", ) HW_VOLTAGE: Final = "hw.voltage" """ Voltage measured by the sensor Instrument: gauge Unit: V """ def create_hw_voltage(meter: Meter, callbacks: Sequence[CallbackT] | None) -> ObservableGauge: """Voltage measured by the sensor""" return meter.create_observable_gauge( name=HW_VOLTAGE, callbacks=callbacks, description="Voltage measured by the sensor.", unit="V", ) HW_VOLTAGE_LIMIT: Final = "hw.voltage.limit" """ Voltage limit in Volts Instrument: gauge Unit: V """ def create_hw_voltage_limit(meter: Meter, callbacks: Sequence[CallbackT] | None) -> ObservableGauge: """Voltage limit in Volts""" return meter.create_observable_gauge( name=HW_VOLTAGE_LIMIT, callbacks=callbacks, description="Voltage limit in Volts.", unit="V", ) HW_VOLTAGE_NOMINAL: Final = "hw.voltage.nominal" """ Nominal (expected) voltage Instrument: gauge Unit: V """ def create_hw_voltage_nominal(meter: Meter, callbacks: Sequence[CallbackT] | None) -> ObservableGauge: """Nominal (expected) voltage""" return meter.create_observable_gauge( name=HW_VOLTAGE_NOMINAL, callbacks=callbacks, description="Nominal (expected) voltage.", unit="V", ) k8s_metrics.py000066400000000000000000003374571527070070700407210ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/src/opentelemetry/semconv/_incubating/metrics# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from collections.abc import Callable, Generator, Iterable, Sequence from typing import Final from opentelemetry.metrics import CallbackOptions, Counter, Meter, ObservableGauge, Observation, UpDownCounter # pylint: disable=invalid-name CallbackT = Callable[[CallbackOptions], Iterable[Observation]] | Generator[Iterable[Observation], CallbackOptions, None] K8S_CONTAINER_CPU_LIMIT: Final = "k8s.container.cpu.limit" """ Deprecated: Replaced by `k8s.container.cpu.limit.desired`. """ def create_k8s_container_cpu_limit(meter: Meter) -> UpDownCounter: """Deprecated, use `k8s.container.cpu.limit.desired` and `k8s.container.cpu.limit.current` instead""" return meter.create_up_down_counter( name=K8S_CONTAINER_CPU_LIMIT, description="Deprecated, use `k8s.container.cpu.limit.desired` and `k8s.container.cpu.limit.current` instead.", unit="{cpu}", ) K8S_CONTAINER_CPU_LIMIT_CURRENT: Final = "k8s.container.cpu.limit.current" """ Maximum CPU resource limit currently configured for a running container Instrument: updowncounter Unit: {cpu} Note: This metric aligns with the limit in the [`resources`](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#resourcerequirements-v1-core) field of [K8s ContainerStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#containerstatus-v1-core) (status.containerStatuses[*].resources). Also see `Actual Resources` in for more details. """ def create_k8s_container_cpu_limit_current(meter: Meter) -> UpDownCounter: """Maximum CPU resource limit currently configured for a running container""" return meter.create_up_down_counter( name=K8S_CONTAINER_CPU_LIMIT_CURRENT, description="Maximum CPU resource limit currently configured for a running container.", unit="{cpu}", ) K8S_CONTAINER_CPU_LIMIT_DESIRED: Final = "k8s.container.cpu.limit.desired" """ Maximum CPU resource limit as defined by the container spec Instrument: updowncounter Unit: {cpu} Note: This metric aligns with the limit in the [`resources`](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#resourcerequirements-v1-core) field of [K8s Container](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#container-v1-core) (spec.containers[*].resources). Also see `Desired Resources` in for more details. """ def create_k8s_container_cpu_limit_desired(meter: Meter) -> UpDownCounter: """Maximum CPU resource limit as defined by the container spec""" return meter.create_up_down_counter( name=K8S_CONTAINER_CPU_LIMIT_DESIRED, description="Maximum CPU resource limit as defined by the container spec.", unit="{cpu}", ) K8S_CONTAINER_CPU_LIMIT_UTILIZATION: Final = "k8s.container.cpu.limit.utilization" """ The ratio of container CPU usage to its current CPU limit Instrument: gauge Unit: 1 Note: The current CPU limit reflects the actual resources applied to the container, as reported by [ContainerStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#containerstatus-v1-core). The value range is [0.0,1.0]. A value of 1.0 means the container is using 100% of its actual CPU limit. If the CPU limit is not set, this metric SHOULD NOT be emitted for that container. """ def create_k8s_container_cpu_limit_utilization(meter: Meter, callbacks: Sequence[CallbackT] | None) -> ObservableGauge: """The ratio of container CPU usage to its current CPU limit""" return meter.create_observable_gauge( name=K8S_CONTAINER_CPU_LIMIT_UTILIZATION, callbacks=callbacks, description="The ratio of container CPU usage to its current CPU limit.", unit="1", ) K8S_CONTAINER_CPU_REQUEST: Final = "k8s.container.cpu.request" """ Deprecated: Replaced by `k8s.container.cpu.request.desired`. """ def create_k8s_container_cpu_request(meter: Meter) -> UpDownCounter: """Deprecated, use `k8s.container.cpu.request.desired` and `k8s.container.cpu.request.current` instead""" return meter.create_up_down_counter( name=K8S_CONTAINER_CPU_REQUEST, description="Deprecated, use `k8s.container.cpu.request.desired` and `k8s.container.cpu.request.current` instead.", unit="{cpu}", ) K8S_CONTAINER_CPU_REQUEST_CURRENT: Final = "k8s.container.cpu.request.current" """ CPU resource requested currently configured for a running container Instrument: updowncounter Unit: {cpu} Note: This metric aligns with the request in the [`resources`](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#resourcerequirements-v1-core) field of [K8s ContainerStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#containerstatus-v1-core) (status.containerStatuses[*].resources). Also see `Actual Resources` in for more details. """ def create_k8s_container_cpu_request_current(meter: Meter) -> UpDownCounter: """CPU resource requested currently configured for a running container""" return meter.create_up_down_counter( name=K8S_CONTAINER_CPU_REQUEST_CURRENT, description="CPU resource requested currently configured for a running container.", unit="{cpu}", ) K8S_CONTAINER_CPU_REQUEST_DESIRED: Final = "k8s.container.cpu.request.desired" """ CPU resource requested as defined by the container spec Instrument: updowncounter Unit: {cpu} Note: This metric aligns with the request in the [`resources`](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#resourcerequirements-v1-core) field of [K8s Container](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#container-v1-core) (spec.containers[*].resources). Also see `Desired Resources` in for more details. """ def create_k8s_container_cpu_request_desired(meter: Meter) -> UpDownCounter: """CPU resource requested as defined by the container spec""" return meter.create_up_down_counter( name=K8S_CONTAINER_CPU_REQUEST_DESIRED, description="CPU resource requested as defined by the container spec.", unit="{cpu}", ) K8S_CONTAINER_CPU_REQUEST_UTILIZATION: Final = "k8s.container.cpu.request.utilization" """ The ratio of container CPU usage to its current CPU request Instrument: gauge Unit: 1 Note: The current CPU request reflects the request applied to the running container, as reported by [ContainerStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#containerstatus-v1-core). The value range is [0.0,1.0]. A value of 1.0 means the container is using 100% of its actual CPU request. If the CPU request is not set, this metric SHOULD NOT be emitted for that container. """ def create_k8s_container_cpu_request_utilization( meter: Meter, callbacks: Sequence[CallbackT] | None ) -> ObservableGauge: """The ratio of container CPU usage to its current CPU request""" return meter.create_observable_gauge( name=K8S_CONTAINER_CPU_REQUEST_UTILIZATION, callbacks=callbacks, description="The ratio of container CPU usage to its current CPU request.", unit="1", ) K8S_CONTAINER_EPHEMERAL_STORAGE_LIMIT: Final = "k8s.container.ephemeral_storage.limit" """ Maximum ephemeral storage resource limit set for the container Instrument: updowncounter Unit: By Note: See https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#resourcerequirements-v1-core for details. """ def create_k8s_container_ephemeral_storage_limit(meter: Meter) -> UpDownCounter: """Maximum ephemeral storage resource limit set for the container""" return meter.create_up_down_counter( name=K8S_CONTAINER_EPHEMERAL_STORAGE_LIMIT, description="Maximum ephemeral storage resource limit set for the container.", unit="By", ) K8S_CONTAINER_EPHEMERAL_STORAGE_REQUEST: Final = "k8s.container.ephemeral_storage.request" """ Ephemeral storage resource requested for the container Instrument: updowncounter Unit: By Note: See https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#resourcerequirements-v1-core for details. """ def create_k8s_container_ephemeral_storage_request(meter: Meter) -> UpDownCounter: """Ephemeral storage resource requested for the container""" return meter.create_up_down_counter( name=K8S_CONTAINER_EPHEMERAL_STORAGE_REQUEST, description="Ephemeral storage resource requested for the container.", unit="By", ) K8S_CONTAINER_EPHEMERAL_STORAGE_USAGE: Final = "k8s.container.ephemeral_storage.usage" """ The ephemeral storage used by a container Instrument: updowncounter Unit: By Note: The value for this metric can be compared against `metric.k8s.container.ephemeral_storage.request` and `metric.k8s.container.ephemeral_storage.limit`. """ def create_k8s_container_ephemeral_storage_usage(meter: Meter) -> UpDownCounter: """The ephemeral storage used by a container""" return meter.create_up_down_counter( name=K8S_CONTAINER_EPHEMERAL_STORAGE_USAGE, description="The ephemeral storage used by a container.", unit="By", ) K8S_CONTAINER_MEMORY_LIMIT: Final = "k8s.container.memory.limit" """ Deprecated: Replaced by `k8s.container.memory.limit.desired`. """ def create_k8s_container_memory_limit(meter: Meter) -> UpDownCounter: """Deprecated, use `k8s.container.memory.limit.desired` and `k8s.container.memory.limit.current` instead""" return meter.create_up_down_counter( name=K8S_CONTAINER_MEMORY_LIMIT, description="Deprecated, use `k8s.container.memory.limit.desired` and `k8s.container.memory.limit.current` instead.", unit="By", ) K8S_CONTAINER_MEMORY_LIMIT_CURRENT: Final = "k8s.container.memory.limit.current" """ Maximum memory resource limit currently configured for a running container Instrument: updowncounter Unit: By Note: This metric aligns with the limit in the [`resources`](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#resourcerequirements-v1-core) field of [K8s ContainerStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#containerstatus-v1-core) (status.containerStatuses[*].resources). Also see `Actual Resources` in for more details. """ def create_k8s_container_memory_limit_current(meter: Meter) -> UpDownCounter: """Maximum memory resource limit currently configured for a running container""" return meter.create_up_down_counter( name=K8S_CONTAINER_MEMORY_LIMIT_CURRENT, description="Maximum memory resource limit currently configured for a running container.", unit="By", ) K8S_CONTAINER_MEMORY_LIMIT_DESIRED: Final = "k8s.container.memory.limit.desired" """ Maximum memory resource limit as defined by the container spec Instrument: updowncounter Unit: By Note: This metric aligns with the limit in the [`resources`](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#resourcerequirements-v1-core) field of [K8s Container](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#container-v1-core) (spec.containers[*].resources). Also see `Desired Resources` in for more details. """ def create_k8s_container_memory_limit_desired(meter: Meter) -> UpDownCounter: """Maximum memory resource limit as defined by the container spec""" return meter.create_up_down_counter( name=K8S_CONTAINER_MEMORY_LIMIT_DESIRED, description="Maximum memory resource limit as defined by the container spec.", unit="By", ) K8S_CONTAINER_MEMORY_REQUEST: Final = "k8s.container.memory.request" """ Deprecated: Replaced by `k8s.container.memory.request.desired`. """ def create_k8s_container_memory_request(meter: Meter) -> UpDownCounter: """Deprecated, use `k8s.container.memory.request.desired` and `k8s.container.memory.request.current` instead""" return meter.create_up_down_counter( name=K8S_CONTAINER_MEMORY_REQUEST, description="Deprecated, use `k8s.container.memory.request.desired` and `k8s.container.memory.request.current` instead.", unit="By", ) K8S_CONTAINER_MEMORY_REQUEST_CURRENT: Final = "k8s.container.memory.request.current" """ Memory resource request currently configured for a running container Instrument: updowncounter Unit: By Note: This metric aligns with the request in the [`resources`](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#resourcerequirements-v1-core) field of [K8s ContainerStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#containerstatus-v1-core) (status.containerStatuses[*].resources). Also see `Actual Resources` in for more details. """ def create_k8s_container_memory_request_current(meter: Meter) -> UpDownCounter: """Memory resource request currently configured for a running container""" return meter.create_up_down_counter( name=K8S_CONTAINER_MEMORY_REQUEST_CURRENT, description="Memory resource request currently configured for a running container.", unit="By", ) K8S_CONTAINER_MEMORY_REQUEST_DESIRED: Final = "k8s.container.memory.request.desired" """ Memory resource requested as defined by the container spec Instrument: updowncounter Unit: By Note: This metric aligns with the request in the [`resources`](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#resourcerequirements-v1-core) field of [K8s Container](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#container-v1-core) (spec.containers[*].resources). Also see `Desired Resources` in for more details. """ def create_k8s_container_memory_request_desired(meter: Meter) -> UpDownCounter: """Memory resource requested as defined by the container spec""" return meter.create_up_down_counter( name=K8S_CONTAINER_MEMORY_REQUEST_DESIRED, description="Memory resource requested as defined by the container spec.", unit="By", ) K8S_CONTAINER_READY: Final = "k8s.container.ready" """ Indicates whether the container is currently marked as ready to accept traffic, based on its readiness probe (1 = ready, 0 = not ready) Instrument: updowncounter Unit: {container} Note: This metric SHOULD reflect the value of the `ready` field in the [K8s ContainerStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#containerstatus-v1-core). """ def create_k8s_container_ready(meter: Meter) -> UpDownCounter: """Indicates whether the container is currently marked as ready to accept traffic, based on its readiness probe (1 = ready, 0 = not ready)""" return meter.create_up_down_counter( name=K8S_CONTAINER_READY, description="Indicates whether the container is currently marked as ready to accept traffic, based on its readiness probe (1 = ready, 0 = not ready).", unit="{container}", ) K8S_CONTAINER_RESTART_COUNT: Final = "k8s.container.restart.count" """ Describes how many times the container has restarted (since the last counter reset) Instrument: updowncounter Unit: {restart} Note: This value is pulled directly from the K8s API and the value can go indefinitely high and be reset to 0 at any time depending on how your kubelet is configured to prune dead containers. It is best to not depend too much on the exact value but rather look at it as either == 0, in which case you can conclude there were no restarts in the recent past, or > 0, in which case you can conclude there were restarts in the recent past, and not try and analyze the value beyond that. """ def create_k8s_container_restart_count(meter: Meter) -> UpDownCounter: """Describes how many times the container has restarted (since the last counter reset)""" return meter.create_up_down_counter( name=K8S_CONTAINER_RESTART_COUNT, description="Describes how many times the container has restarted (since the last counter reset).", unit="{restart}", ) K8S_CONTAINER_STATUS_REASON: Final = "k8s.container.status.reason" """ Describes the number of K8s containers that are currently in a state for a given reason Instrument: updowncounter Unit: {container} Note: All possible container state reasons will be reported at each time interval to avoid missing metrics. Only the value corresponding to the current state reason will be non-zero. """ def create_k8s_container_status_reason(meter: Meter) -> UpDownCounter: """Describes the number of K8s containers that are currently in a state for a given reason""" return meter.create_up_down_counter( name=K8S_CONTAINER_STATUS_REASON, description="Describes the number of K8s containers that are currently in a state for a given reason.", unit="{container}", ) K8S_CONTAINER_STATUS_STATE: Final = "k8s.container.status.state" """ Describes the number of K8s containers that are currently in a given state Instrument: updowncounter Unit: {container} Note: All possible container states will be reported at each time interval to avoid missing metrics. Only the value corresponding to the current state will be non-zero. """ def create_k8s_container_status_state(meter: Meter) -> UpDownCounter: """Describes the number of K8s containers that are currently in a given state""" return meter.create_up_down_counter( name=K8S_CONTAINER_STATUS_STATE, description="Describes the number of K8s containers that are currently in a given state.", unit="{container}", ) K8S_CONTAINER_STORAGE_LIMIT: Final = "k8s.container.storage.limit" """ Maximum storage resource limit set for the container Instrument: updowncounter Unit: By Note: See https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#resourcerequirements-v1-core for details. """ def create_k8s_container_storage_limit(meter: Meter) -> UpDownCounter: """Maximum storage resource limit set for the container""" return meter.create_up_down_counter( name=K8S_CONTAINER_STORAGE_LIMIT, description="Maximum storage resource limit set for the container.", unit="By", ) K8S_CONTAINER_STORAGE_REQUEST: Final = "k8s.container.storage.request" """ Storage resource requested for the container Instrument: updowncounter Unit: By Note: See https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#resourcerequirements-v1-core for details. """ def create_k8s_container_storage_request(meter: Meter) -> UpDownCounter: """Storage resource requested for the container""" return meter.create_up_down_counter( name=K8S_CONTAINER_STORAGE_REQUEST, description="Storage resource requested for the container.", unit="By", ) K8S_CRONJOB_ACTIVE_JOBS: Final = "k8s.cronjob.active_jobs" """ Deprecated: Replaced by `k8s.cronjob.job.active`. """ def create_k8s_cronjob_active_jobs(meter: Meter) -> UpDownCounter: """Deprecated, use `k8s.cronjob.job.active` instead""" return meter.create_up_down_counter( name=K8S_CRONJOB_ACTIVE_JOBS, description="Deprecated, use `k8s.cronjob.job.active` instead.", unit="{job}", ) K8S_CRONJOB_JOB_ACTIVE: Final = "k8s.cronjob.job.active" """ The number of actively running jobs for a cronjob Instrument: updowncounter Unit: {job} Note: This metric aligns with the `active` field of the [K8s CronJobStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#cronjobstatus-v1-batch). """ def create_k8s_cronjob_job_active(meter: Meter) -> UpDownCounter: """The number of actively running jobs for a cronjob""" return meter.create_up_down_counter( name=K8S_CRONJOB_JOB_ACTIVE, description="The number of actively running jobs for a cronjob.", unit="{job}", ) K8S_DAEMONSET_CURRENT_SCHEDULED_NODES: Final = "k8s.daemonset.current_scheduled_nodes" """ Deprecated: Replaced by `k8s.daemonset.node.current_scheduled`. """ def create_k8s_daemonset_current_scheduled_nodes(meter: Meter) -> UpDownCounter: """Deprecated, use `k8s.daemonset.node.current_scheduled` instead""" return meter.create_up_down_counter( name=K8S_DAEMONSET_CURRENT_SCHEDULED_NODES, description="Deprecated, use `k8s.daemonset.node.current_scheduled` instead.", unit="{node}", ) K8S_DAEMONSET_DESIRED_SCHEDULED_NODES: Final = "k8s.daemonset.desired_scheduled_nodes" """ Deprecated: Replaced by `k8s.daemonset.node.desired_scheduled`. """ def create_k8s_daemonset_desired_scheduled_nodes(meter: Meter) -> UpDownCounter: """Deprecated, use `k8s.daemonset.node.desired_scheduled` instead""" return meter.create_up_down_counter( name=K8S_DAEMONSET_DESIRED_SCHEDULED_NODES, description="Deprecated, use `k8s.daemonset.node.desired_scheduled` instead.", unit="{node}", ) K8S_DAEMONSET_MISSCHEDULED_NODES: Final = "k8s.daemonset.misscheduled_nodes" """ Deprecated: Replaced by `k8s.daemonset.node.misscheduled`. """ def create_k8s_daemonset_misscheduled_nodes(meter: Meter) -> UpDownCounter: """Deprecated, use `k8s.daemonset.node.misscheduled` instead""" return meter.create_up_down_counter( name=K8S_DAEMONSET_MISSCHEDULED_NODES, description="Deprecated, use `k8s.daemonset.node.misscheduled` instead.", unit="{node}", ) K8S_DAEMONSET_NODE_CURRENT_SCHEDULED: Final = "k8s.daemonset.node.current_scheduled" """ Number of nodes that are running at least 1 daemon pod and are supposed to run the daemon pod Instrument: updowncounter Unit: {node} Note: This metric aligns with the `currentNumberScheduled` field of the [K8s DaemonSetStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#daemonsetstatus-v1-apps). """ def create_k8s_daemonset_node_current_scheduled(meter: Meter) -> UpDownCounter: """Number of nodes that are running at least 1 daemon pod and are supposed to run the daemon pod""" return meter.create_up_down_counter( name=K8S_DAEMONSET_NODE_CURRENT_SCHEDULED, description="Number of nodes that are running at least 1 daemon pod and are supposed to run the daemon pod.", unit="{node}", ) K8S_DAEMONSET_NODE_DESIRED_SCHEDULED: Final = "k8s.daemonset.node.desired_scheduled" """ Number of nodes that should be running the daemon pod (including nodes currently running the daemon pod) Instrument: updowncounter Unit: {node} Note: This metric aligns with the `desiredNumberScheduled` field of the [K8s DaemonSetStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#daemonsetstatus-v1-apps). """ def create_k8s_daemonset_node_desired_scheduled(meter: Meter) -> UpDownCounter: """Number of nodes that should be running the daemon pod (including nodes currently running the daemon pod)""" return meter.create_up_down_counter( name=K8S_DAEMONSET_NODE_DESIRED_SCHEDULED, description="Number of nodes that should be running the daemon pod (including nodes currently running the daemon pod).", unit="{node}", ) K8S_DAEMONSET_NODE_MISSCHEDULED: Final = "k8s.daemonset.node.misscheduled" """ Number of nodes that are running the daemon pod, but are not supposed to run the daemon pod Instrument: updowncounter Unit: {node} Note: This metric aligns with the `numberMisscheduled` field of the [K8s DaemonSetStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#daemonsetstatus-v1-apps). """ def create_k8s_daemonset_node_misscheduled(meter: Meter) -> UpDownCounter: """Number of nodes that are running the daemon pod, but are not supposed to run the daemon pod""" return meter.create_up_down_counter( name=K8S_DAEMONSET_NODE_MISSCHEDULED, description="Number of nodes that are running the daemon pod, but are not supposed to run the daemon pod.", unit="{node}", ) K8S_DAEMONSET_NODE_READY: Final = "k8s.daemonset.node.ready" """ Number of nodes that should be running the daemon pod and have one or more of the daemon pod running and ready Instrument: updowncounter Unit: {node} Note: This metric aligns with the `numberReady` field of the [K8s DaemonSetStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#daemonsetstatus-v1-apps). """ def create_k8s_daemonset_node_ready(meter: Meter) -> UpDownCounter: """Number of nodes that should be running the daemon pod and have one or more of the daemon pod running and ready""" return meter.create_up_down_counter( name=K8S_DAEMONSET_NODE_READY, description="Number of nodes that should be running the daemon pod and have one or more of the daemon pod running and ready.", unit="{node}", ) K8S_DAEMONSET_READY_NODES: Final = "k8s.daemonset.ready_nodes" """ Deprecated: Replaced by `k8s.daemonset.node.ready`. """ def create_k8s_daemonset_ready_nodes(meter: Meter) -> UpDownCounter: """Deprecated, use `k8s.daemonset.node.ready` instead""" return meter.create_up_down_counter( name=K8S_DAEMONSET_READY_NODES, description="Deprecated, use `k8s.daemonset.node.ready` instead.", unit="{node}", ) K8S_DEPLOYMENT_AVAILABLE_PODS: Final = "k8s.deployment.available_pods" """ Deprecated: Replaced by `k8s.deployment.pod.available`. """ def create_k8s_deployment_available_pods(meter: Meter) -> UpDownCounter: """Deprecated, use `k8s.deployment.pod.available` instead""" return meter.create_up_down_counter( name=K8S_DEPLOYMENT_AVAILABLE_PODS, description="Deprecated, use `k8s.deployment.pod.available` instead.", unit="{pod}", ) K8S_DEPLOYMENT_DESIRED_PODS: Final = "k8s.deployment.desired_pods" """ Deprecated: Replaced by `k8s.deployment.pod.desired`. """ def create_k8s_deployment_desired_pods(meter: Meter) -> UpDownCounter: """Deprecated, use `k8s.deployment.pod.desired` instead""" return meter.create_up_down_counter( name=K8S_DEPLOYMENT_DESIRED_PODS, description="Deprecated, use `k8s.deployment.pod.desired` instead.", unit="{pod}", ) K8S_DEPLOYMENT_POD_AVAILABLE: Final = "k8s.deployment.pod.available" """ Total number of available replica pods (ready for at least minReadySeconds) targeted by this deployment Instrument: updowncounter Unit: {pod} Note: This metric aligns with the `availableReplicas` field of the [K8s DeploymentStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#deploymentstatus-v1-apps). """ def create_k8s_deployment_pod_available(meter: Meter) -> UpDownCounter: """Total number of available replica pods (ready for at least minReadySeconds) targeted by this deployment""" return meter.create_up_down_counter( name=K8S_DEPLOYMENT_POD_AVAILABLE, description="Total number of available replica pods (ready for at least minReadySeconds) targeted by this deployment.", unit="{pod}", ) K8S_DEPLOYMENT_POD_DESIRED: Final = "k8s.deployment.pod.desired" """ Number of desired replica pods in this deployment Instrument: updowncounter Unit: {pod} Note: This metric aligns with the `replicas` field of the [K8s DeploymentSpec](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#deploymentspec-v1-apps). """ def create_k8s_deployment_pod_desired(meter: Meter) -> UpDownCounter: """Number of desired replica pods in this deployment""" return meter.create_up_down_counter( name=K8S_DEPLOYMENT_POD_DESIRED, description="Number of desired replica pods in this deployment.", unit="{pod}", ) K8S_HPA_CURRENT_PODS: Final = "k8s.hpa.current_pods" """ Deprecated: Replaced by `k8s.hpa.pod.current`. """ def create_k8s_hpa_current_pods(meter: Meter) -> UpDownCounter: """Deprecated, use `k8s.hpa.pod.current` instead""" return meter.create_up_down_counter( name=K8S_HPA_CURRENT_PODS, description="Deprecated, use `k8s.hpa.pod.current` instead.", unit="{pod}", ) K8S_HPA_DESIRED_PODS: Final = "k8s.hpa.desired_pods" """ Deprecated: Replaced by `k8s.hpa.pod.desired`. """ def create_k8s_hpa_desired_pods(meter: Meter) -> UpDownCounter: """Deprecated, use `k8s.hpa.pod.desired` instead""" return meter.create_up_down_counter( name=K8S_HPA_DESIRED_PODS, description="Deprecated, use `k8s.hpa.pod.desired` instead.", unit="{pod}", ) K8S_HPA_MAX_PODS: Final = "k8s.hpa.max_pods" """ Deprecated: Replaced by `k8s.hpa.pod.max`. """ def create_k8s_hpa_max_pods(meter: Meter) -> UpDownCounter: """Deprecated, use `k8s.hpa.pod.max` instead""" return meter.create_up_down_counter( name=K8S_HPA_MAX_PODS, description="Deprecated, use `k8s.hpa.pod.max` instead.", unit="{pod}", ) K8S_HPA_METRIC_TARGET_CPU_AVERAGE_UTILIZATION: Final = "k8s.hpa.metric.target.cpu.average_utilization" """ Target average utilization, in percentage, for CPU resource in HPA config Instrument: gauge Unit: 1 Note: This metric aligns with the `averageUtilization` field of the [K8s HPA MetricTarget](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#metrictarget-v2-autoscaling). If the type of the metric is [`ContainerResource`](https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale/#support-for-metrics-apis), the `k8s.container.name` attribute MUST be set to identify the specific container within the pod to which the metric applies. """ def create_k8s_hpa_metric_target_cpu_average_utilization( meter: Meter, callbacks: Sequence[CallbackT] | None ) -> ObservableGauge: """Target average utilization, in percentage, for CPU resource in HPA config""" return meter.create_observable_gauge( name=K8S_HPA_METRIC_TARGET_CPU_AVERAGE_UTILIZATION, callbacks=callbacks, description="Target average utilization, in percentage, for CPU resource in HPA config.", unit="1", ) K8S_HPA_METRIC_TARGET_CPU_AVERAGE_VALUE: Final = "k8s.hpa.metric.target.cpu.average_value" """ Target average value for CPU resource in HPA config Instrument: gauge Unit: {cpu} Note: This metric aligns with the `averageValue` field of the [K8s HPA MetricTarget](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#metrictarget-v2-autoscaling). If the type of the metric is [`ContainerResource`](https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale/#support-for-metrics-apis), the `k8s.container.name` attribute MUST be set to identify the specific container within the pod to which the metric applies. """ def create_k8s_hpa_metric_target_cpu_average_value( meter: Meter, callbacks: Sequence[CallbackT] | None ) -> ObservableGauge: """Target average value for CPU resource in HPA config""" return meter.create_observable_gauge( name=K8S_HPA_METRIC_TARGET_CPU_AVERAGE_VALUE, callbacks=callbacks, description="Target average value for CPU resource in HPA config.", unit="{cpu}", ) K8S_HPA_METRIC_TARGET_CPU_VALUE: Final = "k8s.hpa.metric.target.cpu.value" """ Target value for CPU resource in HPA config Instrument: gauge Unit: {cpu} Note: This metric aligns with the `value` field of the [K8s HPA MetricTarget](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#metrictarget-v2-autoscaling). If the type of the metric is [`ContainerResource`](https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale/#support-for-metrics-apis), the `k8s.container.name` attribute MUST be set to identify the specific container within the pod to which the metric applies. """ def create_k8s_hpa_metric_target_cpu_value(meter: Meter, callbacks: Sequence[CallbackT] | None) -> ObservableGauge: """Target value for CPU resource in HPA config""" return meter.create_observable_gauge( name=K8S_HPA_METRIC_TARGET_CPU_VALUE, callbacks=callbacks, description="Target value for CPU resource in HPA config.", unit="{cpu}", ) K8S_HPA_MIN_PODS: Final = "k8s.hpa.min_pods" """ Deprecated: Replaced by `k8s.hpa.pod.min`. """ def create_k8s_hpa_min_pods(meter: Meter) -> UpDownCounter: """Deprecated, use `k8s.hpa.pod.min` instead""" return meter.create_up_down_counter( name=K8S_HPA_MIN_PODS, description="Deprecated, use `k8s.hpa.pod.min` instead.", unit="{pod}", ) K8S_HPA_POD_CURRENT: Final = "k8s.hpa.pod.current" """ Current number of replica pods managed by this horizontal pod autoscaler, as last seen by the autoscaler Instrument: updowncounter Unit: {pod} Note: This metric aligns with the `currentReplicas` field of the [K8s HorizontalPodAutoscalerStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#horizontalpodautoscalerstatus-v2-autoscaling). """ def create_k8s_hpa_pod_current(meter: Meter) -> UpDownCounter: """Current number of replica pods managed by this horizontal pod autoscaler, as last seen by the autoscaler""" return meter.create_up_down_counter( name=K8S_HPA_POD_CURRENT, description="Current number of replica pods managed by this horizontal pod autoscaler, as last seen by the autoscaler.", unit="{pod}", ) K8S_HPA_POD_DESIRED: Final = "k8s.hpa.pod.desired" """ Desired number of replica pods managed by this horizontal pod autoscaler, as last calculated by the autoscaler Instrument: updowncounter Unit: {pod} Note: This metric aligns with the `desiredReplicas` field of the [K8s HorizontalPodAutoscalerStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#horizontalpodautoscalerstatus-v2-autoscaling). """ def create_k8s_hpa_pod_desired(meter: Meter) -> UpDownCounter: """Desired number of replica pods managed by this horizontal pod autoscaler, as last calculated by the autoscaler""" return meter.create_up_down_counter( name=K8S_HPA_POD_DESIRED, description="Desired number of replica pods managed by this horizontal pod autoscaler, as last calculated by the autoscaler.", unit="{pod}", ) K8S_HPA_POD_MAX: Final = "k8s.hpa.pod.max" """ The upper limit for the number of replica pods to which the autoscaler can scale up Instrument: updowncounter Unit: {pod} Note: This metric aligns with the `maxReplicas` field of the [K8s HorizontalPodAutoscalerSpec](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#horizontalpodautoscalerspec-v2-autoscaling). """ def create_k8s_hpa_pod_max(meter: Meter) -> UpDownCounter: """The upper limit for the number of replica pods to which the autoscaler can scale up""" return meter.create_up_down_counter( name=K8S_HPA_POD_MAX, description="The upper limit for the number of replica pods to which the autoscaler can scale up.", unit="{pod}", ) K8S_HPA_POD_MIN: Final = "k8s.hpa.pod.min" """ The lower limit for the number of replica pods to which the autoscaler can scale down Instrument: updowncounter Unit: {pod} Note: This metric aligns with the `minReplicas` field of the [K8s HorizontalPodAutoscalerSpec](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#horizontalpodautoscalerspec-v2-autoscaling). """ def create_k8s_hpa_pod_min(meter: Meter) -> UpDownCounter: """The lower limit for the number of replica pods to which the autoscaler can scale down""" return meter.create_up_down_counter( name=K8S_HPA_POD_MIN, description="The lower limit for the number of replica pods to which the autoscaler can scale down.", unit="{pod}", ) K8S_JOB_ACTIVE_PODS: Final = "k8s.job.active_pods" """ Deprecated: Replaced by `k8s.job.pod.active`. """ def create_k8s_job_active_pods(meter: Meter) -> UpDownCounter: """Deprecated, use `k8s.job.pod.active` instead""" return meter.create_up_down_counter( name=K8S_JOB_ACTIVE_PODS, description="Deprecated, use `k8s.job.pod.active` instead.", unit="{pod}", ) K8S_JOB_DESIRED_SUCCESSFUL_PODS: Final = "k8s.job.desired_successful_pods" """ Deprecated: Replaced by `k8s.job.pod.desired_successful`. """ def create_k8s_job_desired_successful_pods(meter: Meter) -> UpDownCounter: """Deprecated, use `k8s.job.pod.desired_successful` instead""" return meter.create_up_down_counter( name=K8S_JOB_DESIRED_SUCCESSFUL_PODS, description="Deprecated, use `k8s.job.pod.desired_successful` instead.", unit="{pod}", ) K8S_JOB_FAILED_PODS: Final = "k8s.job.failed_pods" """ Deprecated: Replaced by `k8s.job.pod.failed`. """ def create_k8s_job_failed_pods(meter: Meter) -> UpDownCounter: """Deprecated, use `k8s.job.pod.failed` instead""" return meter.create_up_down_counter( name=K8S_JOB_FAILED_PODS, description="Deprecated, use `k8s.job.pod.failed` instead.", unit="{pod}", ) K8S_JOB_MAX_PARALLEL_PODS: Final = "k8s.job.max_parallel_pods" """ Deprecated: Replaced by `k8s.job.pod.max_parallel`. """ def create_k8s_job_max_parallel_pods(meter: Meter) -> UpDownCounter: """Deprecated, use `k8s.job.pod.max_parallel` instead""" return meter.create_up_down_counter( name=K8S_JOB_MAX_PARALLEL_PODS, description="Deprecated, use `k8s.job.pod.max_parallel` instead.", unit="{pod}", ) K8S_JOB_POD_ACTIVE: Final = "k8s.job.pod.active" """ The number of pending and actively running pods for a job Instrument: updowncounter Unit: {pod} Note: This metric aligns with the `active` field of the [K8s JobStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#jobstatus-v1-batch). """ def create_k8s_job_pod_active(meter: Meter) -> UpDownCounter: """The number of pending and actively running pods for a job""" return meter.create_up_down_counter( name=K8S_JOB_POD_ACTIVE, description="The number of pending and actively running pods for a job.", unit="{pod}", ) K8S_JOB_POD_DESIRED_SUCCESSFUL: Final = "k8s.job.pod.desired_successful" """ The desired number of successfully finished pods the job should be run with Instrument: updowncounter Unit: {pod} Note: This metric aligns with the `completions` field of the [K8s JobSpec](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#jobspec-v1-batch). """ def create_k8s_job_pod_desired_successful(meter: Meter) -> UpDownCounter: """The desired number of successfully finished pods the job should be run with""" return meter.create_up_down_counter( name=K8S_JOB_POD_DESIRED_SUCCESSFUL, description="The desired number of successfully finished pods the job should be run with.", unit="{pod}", ) K8S_JOB_POD_FAILED: Final = "k8s.job.pod.failed" """ The number of pods which reached phase Failed for a job Instrument: updowncounter Unit: {pod} Note: This metric aligns with the `failed` field of the [K8s JobStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#jobstatus-v1-batch). """ def create_k8s_job_pod_failed(meter: Meter) -> UpDownCounter: """The number of pods which reached phase Failed for a job""" return meter.create_up_down_counter( name=K8S_JOB_POD_FAILED, description="The number of pods which reached phase Failed for a job.", unit="{pod}", ) K8S_JOB_POD_MAX_PARALLEL: Final = "k8s.job.pod.max_parallel" """ The max desired number of pods the job should run at any given time Instrument: updowncounter Unit: {pod} Note: This metric aligns with the `parallelism` field of the [K8s JobSpec](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#jobspec-v1-batch). """ def create_k8s_job_pod_max_parallel(meter: Meter) -> UpDownCounter: """The max desired number of pods the job should run at any given time""" return meter.create_up_down_counter( name=K8S_JOB_POD_MAX_PARALLEL, description="The max desired number of pods the job should run at any given time.", unit="{pod}", ) K8S_JOB_POD_SUCCESSFUL: Final = "k8s.job.pod.successful" """ The number of pods which reached phase Succeeded for a job Instrument: updowncounter Unit: {pod} Note: This metric aligns with the `succeeded` field of the [K8s JobStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#jobstatus-v1-batch). """ def create_k8s_job_pod_successful(meter: Meter) -> UpDownCounter: """The number of pods which reached phase Succeeded for a job""" return meter.create_up_down_counter( name=K8S_JOB_POD_SUCCESSFUL, description="The number of pods which reached phase Succeeded for a job.", unit="{pod}", ) K8S_JOB_SUCCESSFUL_PODS: Final = "k8s.job.successful_pods" """ Deprecated: Replaced by `k8s.job.pod.successful`. """ def create_k8s_job_successful_pods(meter: Meter) -> UpDownCounter: """Deprecated, use `k8s.job.pod.successful` instead""" return meter.create_up_down_counter( name=K8S_JOB_SUCCESSFUL_PODS, description="Deprecated, use `k8s.job.pod.successful` instead.", unit="{pod}", ) K8S_NAMESPACE_PHASE: Final = "k8s.namespace.phase" """ Describes number of K8s namespaces that are currently in a given phase Instrument: updowncounter Unit: {namespace} """ def create_k8s_namespace_phase(meter: Meter) -> UpDownCounter: """Describes number of K8s namespaces that are currently in a given phase""" return meter.create_up_down_counter( name=K8S_NAMESPACE_PHASE, description="Describes number of K8s namespaces that are currently in a given phase.", unit="{namespace}", ) K8S_NODE_ALLOCATABLE_CPU: Final = "k8s.node.allocatable.cpu" """ Deprecated: Replaced by `k8s.node.cpu.allocatable`. """ def create_k8s_node_allocatable_cpu(meter: Meter) -> UpDownCounter: """Deprecated, use `k8s.node.cpu.allocatable` instead""" return meter.create_up_down_counter( name=K8S_NODE_ALLOCATABLE_CPU, description="Deprecated, use `k8s.node.cpu.allocatable` instead.", unit="{cpu}", ) K8S_NODE_ALLOCATABLE_EPHEMERAL_STORAGE: Final = "k8s.node.allocatable.ephemeral_storage" """ Deprecated: Replaced by `k8s.node.ephemeral_storage.allocatable`. """ def create_k8s_node_allocatable_ephemeral_storage(meter: Meter) -> UpDownCounter: """Deprecated, use `k8s.node.ephemeral_storage.allocatable` instead""" return meter.create_up_down_counter( name=K8S_NODE_ALLOCATABLE_EPHEMERAL_STORAGE, description="Deprecated, use `k8s.node.ephemeral_storage.allocatable` instead.", unit="By", ) K8S_NODE_ALLOCATABLE_MEMORY: Final = "k8s.node.allocatable.memory" """ Deprecated: Replaced by `k8s.node.memory.allocatable`. """ def create_k8s_node_allocatable_memory(meter: Meter) -> UpDownCounter: """Deprecated, use `k8s.node.memory.allocatable` instead""" return meter.create_up_down_counter( name=K8S_NODE_ALLOCATABLE_MEMORY, description="Deprecated, use `k8s.node.memory.allocatable` instead.", unit="By", ) K8S_NODE_ALLOCATABLE_PODS: Final = "k8s.node.allocatable.pods" """ Deprecated: Replaced by `k8s.node.pod.allocatable`. """ def create_k8s_node_allocatable_pods(meter: Meter) -> UpDownCounter: """Deprecated, use `k8s.node.pod.allocatable` instead""" return meter.create_up_down_counter( name=K8S_NODE_ALLOCATABLE_PODS, description="Deprecated, use `k8s.node.pod.allocatable` instead.", unit="{pod}", ) K8S_NODE_CONDITION_STATUS: Final = "k8s.node.condition.status" """ Describes the condition of a particular Node Instrument: updowncounter Unit: {node} Note: All possible node condition pairs (type and status) will be reported at each time interval to avoid missing metrics. Condition pairs corresponding to the current conditions' statuses will be non-zero. """ def create_k8s_node_condition_status(meter: Meter) -> UpDownCounter: """Describes the condition of a particular Node""" return meter.create_up_down_counter( name=K8S_NODE_CONDITION_STATUS, description="Describes the condition of a particular Node.", unit="{node}", ) K8S_NODE_CPU_ALLOCATABLE: Final = "k8s.node.cpu.allocatable" """ Amount of CPU allocatable on the node Instrument: updowncounter Unit: {cpu} """ def create_k8s_node_cpu_allocatable(meter: Meter) -> UpDownCounter: """Amount of CPU allocatable on the node""" return meter.create_up_down_counter( name=K8S_NODE_CPU_ALLOCATABLE, description="Amount of CPU allocatable on the node.", unit="{cpu}", ) K8S_NODE_CPU_TIME: Final = "k8s.node.cpu.time" """ Total CPU time consumed Instrument: counter Unit: s Note: Total CPU time consumed by the specific Node on all available CPU cores. """ def create_k8s_node_cpu_time(meter: Meter) -> Counter: """Total CPU time consumed""" return meter.create_counter( name=K8S_NODE_CPU_TIME, description="Total CPU time consumed.", unit="s", ) K8S_NODE_CPU_USAGE: Final = "k8s.node.cpu.usage" """ Node's CPU usage, measured in CPUs. Range from 0 to the number of allocatable CPUs Instrument: gauge Unit: {cpu} Note: CPU usage of the specific Node on all available CPU cores. It is calculated as the change in cumulative CPU time (k8s.node.cpu.time) over a measurement interval, divided by the elapsed time: usageCores = (cpuTimeEnd - cpuTimeStart) / elapsedSeconds. """ def create_k8s_node_cpu_usage(meter: Meter, callbacks: Sequence[CallbackT] | None) -> ObservableGauge: """Node's CPU usage, measured in CPUs. Range from 0 to the number of allocatable CPUs""" return meter.create_observable_gauge( name=K8S_NODE_CPU_USAGE, callbacks=callbacks, description="Node's CPU usage, measured in CPUs. Range from 0 to the number of allocatable CPUs.", unit="{cpu}", ) K8S_NODE_EPHEMERAL_STORAGE_ALLOCATABLE: Final = "k8s.node.ephemeral_storage.allocatable" """ Amount of ephemeral-storage allocatable on the node Instrument: updowncounter Unit: By """ def create_k8s_node_ephemeral_storage_allocatable(meter: Meter) -> UpDownCounter: """Amount of ephemeral-storage allocatable on the node""" return meter.create_up_down_counter( name=K8S_NODE_EPHEMERAL_STORAGE_ALLOCATABLE, description="Amount of ephemeral-storage allocatable on the node.", unit="By", ) K8S_NODE_FILESYSTEM_AVAILABLE: Final = "k8s.node.filesystem.available" """ Node filesystem available bytes Instrument: updowncounter Unit: By Note: This metric is derived from the [FsStats.AvailableBytes](https://pkg.go.dev/k8s.io/kubelet@v0.33.0/pkg/apis/stats/v1alpha1#FsStats) field of the [NodeStats.Fs](https://pkg.go.dev/k8s.io/kubelet@v0.33.0/pkg/apis/stats/v1alpha1#NodeStats) of the Kubelet's stats API. """ def create_k8s_node_filesystem_available(meter: Meter) -> UpDownCounter: """Node filesystem available bytes""" return meter.create_up_down_counter( name=K8S_NODE_FILESYSTEM_AVAILABLE, description="Node filesystem available bytes.", unit="By", ) K8S_NODE_FILESYSTEM_CAPACITY: Final = "k8s.node.filesystem.capacity" """ Node filesystem capacity Instrument: updowncounter Unit: By Note: This metric is derived from the [FsStats.CapacityBytes](https://pkg.go.dev/k8s.io/kubelet@v0.33.0/pkg/apis/stats/v1alpha1#FsStats) field of the [NodeStats.Fs](https://pkg.go.dev/k8s.io/kubelet@v0.33.0/pkg/apis/stats/v1alpha1#NodeStats) of the Kubelet's stats API. """ def create_k8s_node_filesystem_capacity(meter: Meter) -> UpDownCounter: """Node filesystem capacity""" return meter.create_up_down_counter( name=K8S_NODE_FILESYSTEM_CAPACITY, description="Node filesystem capacity.", unit="By", ) K8S_NODE_FILESYSTEM_INODE_COUNT: Final = "k8s.node.filesystem.inode.count" """ The total inodes in the node's root filesystem Instrument: updowncounter Unit: {inode} Note: This metric is derived from the [FsStats.Inodes](https://pkg.go.dev/k8s.io/kubelet@v0.33.0/pkg/apis/stats/v1alpha1#FsStats) field of the [NodeStats.Fs](https://pkg.go.dev/k8s.io/kubelet@v0.33.0/pkg/apis/stats/v1alpha1#NodeStats) of the Kubelet's stats API. """ def create_k8s_node_filesystem_inode_count(meter: Meter) -> UpDownCounter: """The total inodes in the node's root filesystem""" return meter.create_up_down_counter( name=K8S_NODE_FILESYSTEM_INODE_COUNT, description="The total inodes in the node's root filesystem.", unit="{inode}", ) K8S_NODE_FILESYSTEM_INODE_FREE: Final = "k8s.node.filesystem.inode.free" """ The free inodes in the node's root filesystem Instrument: updowncounter Unit: {inode} Note: This metric is derived from the [FsStats.InodesFree](https://pkg.go.dev/k8s.io/kubelet@v0.33.0/pkg/apis/stats/v1alpha1#FsStats) field of the [NodeStats.Fs](https://pkg.go.dev/k8s.io/kubelet@v0.33.0/pkg/apis/stats/v1alpha1#NodeStats) of the Kubelet's stats API. """ def create_k8s_node_filesystem_inode_free(meter: Meter) -> UpDownCounter: """The free inodes in the node's root filesystem""" return meter.create_up_down_counter( name=K8S_NODE_FILESYSTEM_INODE_FREE, description="The free inodes in the node's root filesystem.", unit="{inode}", ) K8S_NODE_FILESYSTEM_USAGE: Final = "k8s.node.filesystem.usage" """ Node filesystem usage Instrument: updowncounter Unit: By Note: This may not equal capacity - available. This metric is derived from the [FsStats.UsedBytes](https://pkg.go.dev/k8s.io/kubelet@v0.33.0/pkg/apis/stats/v1alpha1#FsStats) field of the [NodeStats.Fs](https://pkg.go.dev/k8s.io/kubelet@v0.33.0/pkg/apis/stats/v1alpha1#NodeStats) of the Kubelet's stats API. """ def create_k8s_node_filesystem_usage(meter: Meter) -> UpDownCounter: """Node filesystem usage""" return meter.create_up_down_counter( name=K8S_NODE_FILESYSTEM_USAGE, description="Node filesystem usage.", unit="By", ) K8S_NODE_MEMORY_ALLOCATABLE: Final = "k8s.node.memory.allocatable" """ Amount of memory allocatable on the node Instrument: updowncounter Unit: By """ def create_k8s_node_memory_allocatable(meter: Meter) -> UpDownCounter: """Amount of memory allocatable on the node""" return meter.create_up_down_counter( name=K8S_NODE_MEMORY_ALLOCATABLE, description="Amount of memory allocatable on the node.", unit="By", ) K8S_NODE_MEMORY_AVAILABLE: Final = "k8s.node.memory.available" """ Node memory available Instrument: updowncounter Unit: By Note: Available memory for use. This is defined as the memory limit - workingSetBytes. If memory limit is undefined, the available bytes is omitted. This metric is derived from the [MemoryStats.AvailableBytes](https://pkg.go.dev/k8s.io/kubelet@v0.34.0/pkg/apis/stats/v1alpha1#MemoryStats) field of the [NodeStats.Memory](https://pkg.go.dev/k8s.io/kubelet@v0.34.0/pkg/apis/stats/v1alpha1#NodeStats) of the Kubelet's stats API. """ def create_k8s_node_memory_available(meter: Meter) -> UpDownCounter: """Node memory available""" return meter.create_up_down_counter( name=K8S_NODE_MEMORY_AVAILABLE, description="Node memory available.", unit="By", ) K8S_NODE_MEMORY_PAGING_FAULTS: Final = "k8s.node.memory.paging.faults" """ Deprecated: Replaced by `k8s.node.paging.faults`. """ def create_k8s_node_memory_paging_faults(meter: Meter) -> Counter: """Deprecated, use `k8s.node.paging.faults` instead""" return meter.create_counter( name=K8S_NODE_MEMORY_PAGING_FAULTS, description="Deprecated, use `k8s.node.paging.faults` instead.", unit="{fault}", ) K8S_NODE_MEMORY_RSS: Final = "k8s.node.memory.rss" """ Node memory RSS Instrument: updowncounter Unit: By Note: The amount of anonymous and swap cache memory (includes transparent hugepages). This metric is derived from the [MemoryStats.RSSBytes](https://pkg.go.dev/k8s.io/kubelet@v0.34.0/pkg/apis/stats/v1alpha1#MemoryStats) field of the [NodeStats.Memory](https://pkg.go.dev/k8s.io/kubelet@v0.34.0/pkg/apis/stats/v1alpha1#NodeStats) of the Kubelet's stats API. """ def create_k8s_node_memory_rss(meter: Meter) -> UpDownCounter: """Node memory RSS""" return meter.create_up_down_counter( name=K8S_NODE_MEMORY_RSS, description="Node memory RSS.", unit="By", ) K8S_NODE_MEMORY_USAGE: Final = "k8s.node.memory.usage" """ Memory usage of the Node Instrument: updowncounter Unit: By Note: Total memory in use. This includes all memory regardless of when it was accessed. This metric is derived from the [MemoryStats.UsageBytes](https://pkg.go.dev/k8s.io/kubelet@v0.34.0/pkg/apis/stats/v1alpha1#MemoryStats) field of the [NodeStats.Memory](https://pkg.go.dev/k8s.io/kubelet@v0.34.0/pkg/apis/stats/v1alpha1#NodeStats) of the Kubelet's stats API. """ def create_k8s_node_memory_usage(meter: Meter) -> UpDownCounter: """Memory usage of the Node""" return meter.create_up_down_counter( name=K8S_NODE_MEMORY_USAGE, description="Memory usage of the Node.", unit="By", ) K8S_NODE_MEMORY_WORKING_SET: Final = "k8s.node.memory.working_set" """ Node memory working set Instrument: updowncounter Unit: By Note: The amount of working set memory. This includes recently accessed memory, dirty memory, and kernel memory. WorkingSetBytes is <= UsageBytes. This metric is derived from the [MemoryStats.WorkingSetBytes](https://pkg.go.dev/k8s.io/kubelet@v0.34.0/pkg/apis/stats/v1alpha1#MemoryStats) field of the [NodeStats.Memory](https://pkg.go.dev/k8s.io/kubelet@v0.34.0/pkg/apis/stats/v1alpha1#NodeStats) of the Kubelet's stats API. """ def create_k8s_node_memory_working_set(meter: Meter) -> UpDownCounter: """Node memory working set""" return meter.create_up_down_counter( name=K8S_NODE_MEMORY_WORKING_SET, description="Node memory working set.", unit="By", ) K8S_NODE_NETWORK_ERRORS: Final = "k8s.node.network.errors" """ Node network errors Instrument: counter Unit: {error} """ def create_k8s_node_network_errors(meter: Meter) -> Counter: """Node network errors""" return meter.create_counter( name=K8S_NODE_NETWORK_ERRORS, description="Node network errors.", unit="{error}", ) K8S_NODE_NETWORK_IO: Final = "k8s.node.network.io" """ Network bytes for the Node Instrument: counter Unit: By """ def create_k8s_node_network_io(meter: Meter) -> Counter: """Network bytes for the Node""" return meter.create_counter( name=K8S_NODE_NETWORK_IO, description="Network bytes for the Node.", unit="By", ) K8S_NODE_PAGING_FAULTS: Final = "k8s.node.paging.faults" """ Node memory paging faults Instrument: counter Unit: {fault} Note: Cumulative number of major/minor page faults. This metric is derived from the [MemoryStats.PageFaults](https://pkg.go.dev/k8s.io/kubelet@v0.34.0/pkg/apis/stats/v1alpha1#MemoryStats) and [MemoryStats.MajorPageFaults](https://pkg.go.dev/k8s.io/kubelet@v0.34.0/pkg/apis/stats/v1alpha1#MemoryStats) fields of the [NodeStats.Memory](https://pkg.go.dev/k8s.io/kubelet@v0.34.0/pkg/apis/stats/v1alpha1#NodeStats) of the Kubelet's stats API. """ def create_k8s_node_paging_faults(meter: Meter) -> Counter: """Node memory paging faults""" return meter.create_counter( name=K8S_NODE_PAGING_FAULTS, description="Node memory paging faults.", unit="{fault}", ) K8S_NODE_POD_ALLOCATABLE: Final = "k8s.node.pod.allocatable" """ Amount of pods allocatable on the node Instrument: updowncounter Unit: {pod} """ def create_k8s_node_pod_allocatable(meter: Meter) -> UpDownCounter: """Amount of pods allocatable on the node""" return meter.create_up_down_counter( name=K8S_NODE_POD_ALLOCATABLE, description="Amount of pods allocatable on the node.", unit="{pod}", ) K8S_NODE_SYSTEM_CONTAINER_CPU_TIME: Final = "k8s.node.system_container.cpu.time" """ Node's system container CPU time Instrument: counter Unit: s Note: This metric is derived from the [CPUStats.UsageCoreNanoSeconds](https://github.com/kubernetes/kubelet/blob/v0.35.2/pkg/apis/stats/v1alpha1/types.go#L236) field of the [ContainerStats](https://github.com/kubernetes/kubelet/blob/v0.35.2/pkg/apis/stats/v1alpha1/types.go#L157C6-L157C20) of [Node.SystemContainers](https://github.com/kubernetes/kubelet/blob/v0.35.2/pkg/apis/stats/v1alpha1/types.go#L40) of the Kubelet's stats API. """ def create_k8s_node_system_container_cpu_time(meter: Meter) -> Counter: """Node's system container CPU time""" return meter.create_counter( name=K8S_NODE_SYSTEM_CONTAINER_CPU_TIME, description="Node's system container CPU time.", unit="s", ) K8S_NODE_SYSTEM_CONTAINER_CPU_USAGE: Final = "k8s.node.system_container.cpu.usage" """ Node's system container CPU usage, measured in CPUs Instrument: gauge Unit: {cpu} Note: CPU usage of the specific System Container on all available CPU cores. It is calculated as the change in cumulative CPU time (k8s.node.system_container.cpu.time) over a measurement interval, divided by the elapsed time: usageCores = (cpuTimeEnd - cpuTimeStart) / elapsedSeconds. """ def create_k8s_node_system_container_cpu_usage(meter: Meter, callbacks: Sequence[CallbackT] | None) -> ObservableGauge: """Node's system container CPU usage, measured in CPUs""" return meter.create_observable_gauge( name=K8S_NODE_SYSTEM_CONTAINER_CPU_USAGE, callbacks=callbacks, description="Node's system container CPU usage, measured in CPUs.", unit="{cpu}", ) K8S_NODE_SYSTEM_CONTAINER_MEMORY_USAGE: Final = "k8s.node.system_container.memory.usage" """ Node's system container memory usage Instrument: updowncounter Unit: By Note: This metric is derived from the [MemoryStats.UsageBytes](https://github.com/kubernetes/kubelet/blob/v0.35.2/pkg/apis/stats/v1alpha1/types.go#L252) field of the [ContainerStats](https://github.com/kubernetes/kubelet/blob/v0.35.2/pkg/apis/stats/v1alpha1/types.go#L157C6-L157C20) of [Node.SystemContainers](https://github.com/kubernetes/kubelet/blob/v0.35.2/pkg/apis/stats/v1alpha1/types.go#L40) of the Kubelet's stats API. """ def create_k8s_node_system_container_memory_usage(meter: Meter) -> UpDownCounter: """Node's system container memory usage""" return meter.create_up_down_counter( name=K8S_NODE_SYSTEM_CONTAINER_MEMORY_USAGE, description="Node's system container memory usage.", unit="By", ) K8S_NODE_SYSTEM_CONTAINER_MEMORY_WORKING_SET: Final = "k8s.node.system_container.memory.working_set" """ The amount of working set memory Instrument: updowncounter Unit: By Note: This metric is derived from the [MemoryStats.WorkingSetBytes](https://github.com/kubernetes/kubelet/blob/v0.35.2/pkg/apis/stats/v1alpha1/types.go#L256) field of the [ContainerStats](https://github.com/kubernetes/kubelet/blob/v0.35.2/pkg/apis/stats/v1alpha1/types.go#L157C6-L157C20) of [Node.SystemContainers](https://github.com/kubernetes/kubelet/blob/v0.35.2/pkg/apis/stats/v1alpha1/types.go#L40) of the Kubelet's stats API. """ def create_k8s_node_system_container_memory_working_set(meter: Meter) -> UpDownCounter: """The amount of working set memory""" return meter.create_up_down_counter( name=K8S_NODE_SYSTEM_CONTAINER_MEMORY_WORKING_SET, description="The amount of working set memory.", unit="By", ) K8S_NODE_UPTIME: Final = "k8s.node.uptime" """ The time the Node has been running Instrument: gauge Unit: s Note: Instrumentations SHOULD use a gauge with type `double` and measure uptime in seconds as a floating point number with the highest precision available. The actual accuracy would depend on the instrumentation and operating system. """ def create_k8s_node_uptime(meter: Meter, callbacks: Sequence[CallbackT] | None) -> ObservableGauge: """The time the Node has been running""" return meter.create_observable_gauge( name=K8S_NODE_UPTIME, callbacks=callbacks, description="The time the Node has been running.", unit="s", ) K8S_PERSISTENTVOLUME_STATUS_PHASE: Final = "k8s.persistentvolume.status.phase" """ Number of PersistentVolumes in a given phase Instrument: updowncounter Unit: {persistentvolume} Note: All possible phases should be reported at each interval to avoid gaps in the time series. This metric is derived from the `.status.phase` field of the [K8s PersistentVolumeStatus](https://kubernetes.io/docs/reference/kubernetes-api/config-and-storage-resources/persistent-volume-v1/#PersistentVolumeStatus). """ def create_k8s_persistentvolume_status_phase(meter: Meter) -> UpDownCounter: """Number of PersistentVolumes in a given phase""" return meter.create_up_down_counter( name=K8S_PERSISTENTVOLUME_STATUS_PHASE, description="Number of PersistentVolumes in a given phase.", unit="{persistentvolume}", ) K8S_PERSISTENTVOLUME_STORAGE_CAPACITY: Final = "k8s.persistentvolume.storage.capacity" """ The storage capacity of the PersistentVolume Instrument: updowncounter Unit: By Note: This metric is derived from the `.spec.capacity.storage` field of the [K8s PersistentVolumeSpec](https://kubernetes.io/docs/reference/kubernetes-api/config-and-storage-resources/persistent-volume-v1/#PersistentVolumeSpec). """ def create_k8s_persistentvolume_storage_capacity(meter: Meter) -> UpDownCounter: """The storage capacity of the PersistentVolume""" return meter.create_up_down_counter( name=K8S_PERSISTENTVOLUME_STORAGE_CAPACITY, description="The storage capacity of the PersistentVolume.", unit="By", ) K8S_PERSISTENTVOLUMECLAIM_STATUS_PHASE: Final = "k8s.persistentvolumeclaim.status.phase" """ Number of PersistentVolumeClaims in a given phase Instrument: updowncounter Unit: {persistentvolumeclaim} Note: All possible phases should be reported at each interval to avoid gaps in the time series. This metric is derived from the `.status.phase` field of the [K8s PersistentVolumeClaimStatus](https://kubernetes.io/docs/reference/kubernetes-api/config-and-storage-resources/persistent-volume-claim-v1/#PersistentVolumeClaimStatus). """ def create_k8s_persistentvolumeclaim_status_phase(meter: Meter) -> UpDownCounter: """Number of PersistentVolumeClaims in a given phase""" return meter.create_up_down_counter( name=K8S_PERSISTENTVOLUMECLAIM_STATUS_PHASE, description="Number of PersistentVolumeClaims in a given phase.", unit="{persistentvolumeclaim}", ) K8S_PERSISTENTVOLUMECLAIM_STORAGE_CAPACITY: Final = "k8s.persistentvolumeclaim.storage.capacity" """ The actual storage capacity provisioned for the PersistentVolumeClaim Instrument: updowncounter Unit: By Note: Only available when the PVC is bound. May differ from the requested capacity due to provisioner rounding. This metric is derived from the `.status.capacity.storage` field of the [K8s PersistentVolumeClaimStatus](https://kubernetes.io/docs/reference/kubernetes-api/config-and-storage-resources/persistent-volume-claim-v1/#PersistentVolumeClaimStatus). """ def create_k8s_persistentvolumeclaim_storage_capacity(meter: Meter) -> UpDownCounter: """The actual storage capacity provisioned for the PersistentVolumeClaim""" return meter.create_up_down_counter( name=K8S_PERSISTENTVOLUMECLAIM_STORAGE_CAPACITY, description="The actual storage capacity provisioned for the PersistentVolumeClaim.", unit="By", ) K8S_PERSISTENTVOLUMECLAIM_STORAGE_REQUEST: Final = "k8s.persistentvolumeclaim.storage.request" """ The storage requested by the PersistentVolumeClaim Instrument: updowncounter Unit: By Note: This metric is derived from the `.spec.resources.requests.storage` field of the [K8s PersistentVolumeClaimSpec](https://kubernetes.io/docs/reference/kubernetes-api/config-and-storage-resources/persistent-volume-claim-v1/#PersistentVolumeClaimSpec). """ def create_k8s_persistentvolumeclaim_storage_request(meter: Meter) -> UpDownCounter: """The storage requested by the PersistentVolumeClaim""" return meter.create_up_down_counter( name=K8S_PERSISTENTVOLUMECLAIM_STORAGE_REQUEST, description="The storage requested by the PersistentVolumeClaim.", unit="By", ) K8S_POD_CPU_TIME: Final = "k8s.pod.cpu.time" """ Total CPU time consumed Instrument: counter Unit: s Note: Total CPU time consumed by the specific Pod on all available CPU cores. """ def create_k8s_pod_cpu_time(meter: Meter) -> Counter: """Total CPU time consumed""" return meter.create_counter( name=K8S_POD_CPU_TIME, description="Total CPU time consumed.", unit="s", ) K8S_POD_CPU_USAGE: Final = "k8s.pod.cpu.usage" """ Pod's CPU usage, measured in CPUs. Range from 0 to the number of allocatable CPUs Instrument: gauge Unit: {cpu} Note: CPU usage of the specific Pod on all available CPU cores. It is calculated as the change in cumulative CPU time (k8s.pod.cpu.time) over a measurement interval, divided by the elapsed time: usageCores = (cpuTimeEnd - cpuTimeStart) / elapsedSeconds. """ def create_k8s_pod_cpu_usage(meter: Meter, callbacks: Sequence[CallbackT] | None) -> ObservableGauge: """Pod's CPU usage, measured in CPUs. Range from 0 to the number of allocatable CPUs""" return meter.create_observable_gauge( name=K8S_POD_CPU_USAGE, callbacks=callbacks, description="Pod's CPU usage, measured in CPUs. Range from 0 to the number of allocatable CPUs.", unit="{cpu}", ) K8S_POD_FILESYSTEM_AVAILABLE: Final = "k8s.pod.filesystem.available" """ Pod filesystem available bytes Instrument: updowncounter Unit: By Note: This metric is derived from the [FsStats.AvailableBytes](https://pkg.go.dev/k8s.io/kubelet@v0.33.0/pkg/apis/stats/v1alpha1#FsStats) field of the [PodStats.EphemeralStorage](https://pkg.go.dev/k8s.io/kubelet@v0.33.0/pkg/apis/stats/v1alpha1#PodStats) of the Kubelet's stats API. """ def create_k8s_pod_filesystem_available(meter: Meter) -> UpDownCounter: """Pod filesystem available bytes""" return meter.create_up_down_counter( name=K8S_POD_FILESYSTEM_AVAILABLE, description="Pod filesystem available bytes.", unit="By", ) K8S_POD_FILESYSTEM_CAPACITY: Final = "k8s.pod.filesystem.capacity" """ Pod filesystem capacity Instrument: updowncounter Unit: By Note: This metric is derived from the [FsStats.CapacityBytes](https://pkg.go.dev/k8s.io/kubelet@v0.33.0/pkg/apis/stats/v1alpha1#FsStats) field of the [PodStats.EphemeralStorage](https://pkg.go.dev/k8s.io/kubelet@v0.33.0/pkg/apis/stats/v1alpha1#PodStats) of the Kubelet's stats API. """ def create_k8s_pod_filesystem_capacity(meter: Meter) -> UpDownCounter: """Pod filesystem capacity""" return meter.create_up_down_counter( name=K8S_POD_FILESYSTEM_CAPACITY, description="Pod filesystem capacity.", unit="By", ) K8S_POD_FILESYSTEM_USAGE: Final = "k8s.pod.filesystem.usage" """ Pod filesystem usage Instrument: updowncounter Unit: By Note: This may not equal capacity - available. This metric is derived from the [FsStats.UsedBytes](https://pkg.go.dev/k8s.io/kubelet@v0.33.0/pkg/apis/stats/v1alpha1#FsStats) field of the [PodStats.EphemeralStorage](https://pkg.go.dev/k8s.io/kubelet@v0.33.0/pkg/apis/stats/v1alpha1#PodStats) of the Kubelet's stats API. """ def create_k8s_pod_filesystem_usage(meter: Meter) -> UpDownCounter: """Pod filesystem usage""" return meter.create_up_down_counter( name=K8S_POD_FILESYSTEM_USAGE, description="Pod filesystem usage.", unit="By", ) K8S_POD_MEMORY_AVAILABLE: Final = "k8s.pod.memory.available" """ Pod memory available Instrument: updowncounter Unit: By Note: Available memory for use. This is defined as the memory limit - workingSetBytes. If memory limit is undefined, the available bytes is omitted. This metric is derived from the [MemoryStats.AvailableBytes](https://pkg.go.dev/k8s.io/kubelet@v0.34.0/pkg/apis/stats/v1alpha1#MemoryStats) field of the [PodStats.Memory](https://pkg.go.dev/k8s.io/kubelet@v0.34.0/pkg/apis/stats/v1alpha1#PodStats) of the Kubelet's stats API. """ def create_k8s_pod_memory_available(meter: Meter) -> UpDownCounter: """Pod memory available""" return meter.create_up_down_counter( name=K8S_POD_MEMORY_AVAILABLE, description="Pod memory available.", unit="By", ) K8S_POD_MEMORY_PAGING_FAULTS: Final = "k8s.pod.memory.paging.faults" """ Deprecated: Replaced by `k8s.pod.paging.faults`. """ def create_k8s_pod_memory_paging_faults(meter: Meter) -> Counter: """Deprecated, use `k8s.pod.paging.faults` instead""" return meter.create_counter( name=K8S_POD_MEMORY_PAGING_FAULTS, description="Deprecated, use `k8s.pod.paging.faults` instead.", unit="{fault}", ) K8S_POD_MEMORY_RSS: Final = "k8s.pod.memory.rss" """ Pod memory RSS Instrument: updowncounter Unit: By Note: The amount of anonymous and swap cache memory (includes transparent hugepages). This metric is derived from the [MemoryStats.RSSBytes](https://pkg.go.dev/k8s.io/kubelet@v0.34.0/pkg/apis/stats/v1alpha1#MemoryStats) field of the [PodStats.Memory](https://pkg.go.dev/k8s.io/kubelet@v0.34.0/pkg/apis/stats/v1alpha1#PodStats) of the Kubelet's stats API. """ def create_k8s_pod_memory_rss(meter: Meter) -> UpDownCounter: """Pod memory RSS""" return meter.create_up_down_counter( name=K8S_POD_MEMORY_RSS, description="Pod memory RSS.", unit="By", ) K8S_POD_MEMORY_USAGE: Final = "k8s.pod.memory.usage" """ Memory usage of the Pod Instrument: updowncounter Unit: By Note: Total memory in use. This includes all memory regardless of when it was accessed. This metric is derived from the [MemoryStats.UsageBytes](https://pkg.go.dev/k8s.io/kubelet@v0.34.0/pkg/apis/stats/v1alpha1#MemoryStats) field of the [PodStats.Memory](https://pkg.go.dev/k8s.io/kubelet@v0.34.0/pkg/apis/stats/v1alpha1#PodStats) of the Kubelet's stats API. """ def create_k8s_pod_memory_usage(meter: Meter) -> UpDownCounter: """Memory usage of the Pod""" return meter.create_up_down_counter( name=K8S_POD_MEMORY_USAGE, description="Memory usage of the Pod.", unit="By", ) K8S_POD_MEMORY_WORKING_SET: Final = "k8s.pod.memory.working_set" """ Pod memory working set Instrument: updowncounter Unit: By Note: The amount of working set memory. This includes recently accessed memory, dirty memory, and kernel memory. WorkingSetBytes is <= UsageBytes. This metric is derived from the [MemoryStats.WorkingSetBytes](https://pkg.go.dev/k8s.io/kubelet@v0.34.0/pkg/apis/stats/v1alpha1#MemoryStats) field of the [PodStats.Memory](https://pkg.go.dev/k8s.io/kubelet@v0.34.0/pkg/apis/stats/v1alpha1#PodStats) of the Kubelet's stats API. """ def create_k8s_pod_memory_working_set(meter: Meter) -> UpDownCounter: """Pod memory working set""" return meter.create_up_down_counter( name=K8S_POD_MEMORY_WORKING_SET, description="Pod memory working set.", unit="By", ) K8S_POD_NETWORK_ERRORS: Final = "k8s.pod.network.errors" """ Pod network errors Instrument: counter Unit: {error} """ def create_k8s_pod_network_errors(meter: Meter) -> Counter: """Pod network errors""" return meter.create_counter( name=K8S_POD_NETWORK_ERRORS, description="Pod network errors.", unit="{error}", ) K8S_POD_NETWORK_IO: Final = "k8s.pod.network.io" """ Network bytes for the Pod Instrument: counter Unit: By """ def create_k8s_pod_network_io(meter: Meter) -> Counter: """Network bytes for the Pod""" return meter.create_counter( name=K8S_POD_NETWORK_IO, description="Network bytes for the Pod.", unit="By", ) K8S_POD_PAGING_FAULTS: Final = "k8s.pod.paging.faults" """ Pod memory paging faults Instrument: counter Unit: {fault} Note: Cumulative number of major/minor page faults. This metric is derived from the [MemoryStats.PageFaults](https://pkg.go.dev/k8s.io/kubelet@v0.34.0/pkg/apis/stats/v1alpha1#MemoryStats) and [MemoryStats.MajorPageFaults](https://pkg.go.dev/k8s.io/kubelet@v0.34.0/pkg/apis/stats/v1alpha1#MemoryStats) field of the [PodStats.Memory](https://pkg.go.dev/k8s.io/kubelet@v0.34.0/pkg/apis/stats/v1alpha1#PodStats) of the Kubelet's stats API. """ def create_k8s_pod_paging_faults(meter: Meter) -> Counter: """Pod memory paging faults""" return meter.create_counter( name=K8S_POD_PAGING_FAULTS, description="Pod memory paging faults.", unit="{fault}", ) K8S_POD_STATUS_PHASE: Final = "k8s.pod.status.phase" """ Describes number of K8s Pods that are currently in a given phase Instrument: updowncounter Unit: {pod} Note: All possible pod phases will be reported at each time interval to avoid missing metrics. Only the value corresponding to the current phase will be non-zero. """ def create_k8s_pod_status_phase(meter: Meter) -> UpDownCounter: """Describes number of K8s Pods that are currently in a given phase""" return meter.create_up_down_counter( name=K8S_POD_STATUS_PHASE, description="Describes number of K8s Pods that are currently in a given phase.", unit="{pod}", ) K8S_POD_STATUS_REASON: Final = "k8s.pod.status.reason" """ Describes the number of K8s Pods that are currently in a state for a given reason Instrument: updowncounter Unit: {pod} Note: All possible pod status reasons will be reported at each time interval to avoid missing metrics. Only the value corresponding to the current reason will be non-zero. """ def create_k8s_pod_status_reason(meter: Meter) -> UpDownCounter: """Describes the number of K8s Pods that are currently in a state for a given reason""" return meter.create_up_down_counter( name=K8S_POD_STATUS_REASON, description="Describes the number of K8s Pods that are currently in a state for a given reason.", unit="{pod}", ) K8S_POD_UPTIME: Final = "k8s.pod.uptime" """ The time the Pod has been running Instrument: gauge Unit: s Note: Instrumentations SHOULD use a gauge with type `double` and measure uptime in seconds as a floating point number with the highest precision available. The actual accuracy would depend on the instrumentation and operating system. """ def create_k8s_pod_uptime(meter: Meter, callbacks: Sequence[CallbackT] | None) -> ObservableGauge: """The time the Pod has been running""" return meter.create_observable_gauge( name=K8S_POD_UPTIME, callbacks=callbacks, description="The time the Pod has been running.", unit="s", ) K8S_POD_VOLUME_AVAILABLE: Final = "k8s.pod.volume.available" """ Pod volume storage space available Instrument: updowncounter Unit: By Note: This metric is derived from the [VolumeStats.AvailableBytes](https://pkg.go.dev/k8s.io/kubelet@v0.33.0/pkg/apis/stats/v1alpha1#VolumeStats) field of the [PodStats](https://pkg.go.dev/k8s.io/kubelet@v0.33.0/pkg/apis/stats/v1alpha1#PodStats) of the Kubelet's stats API. """ def create_k8s_pod_volume_available(meter: Meter) -> UpDownCounter: """Pod volume storage space available""" return meter.create_up_down_counter( name=K8S_POD_VOLUME_AVAILABLE, description="Pod volume storage space available.", unit="By", ) K8S_POD_VOLUME_CAPACITY: Final = "k8s.pod.volume.capacity" """ Pod volume total capacity Instrument: updowncounter Unit: By Note: This metric is derived from the [VolumeStats.CapacityBytes](https://pkg.go.dev/k8s.io/kubelet@v0.33.0/pkg/apis/stats/v1alpha1#VolumeStats) field of the [PodStats](https://pkg.go.dev/k8s.io/kubelet@v0.33.0/pkg/apis/stats/v1alpha1#PodStats) of the Kubelet's stats API. """ def create_k8s_pod_volume_capacity(meter: Meter) -> UpDownCounter: """Pod volume total capacity""" return meter.create_up_down_counter( name=K8S_POD_VOLUME_CAPACITY, description="Pod volume total capacity.", unit="By", ) K8S_POD_VOLUME_INODE_COUNT: Final = "k8s.pod.volume.inode.count" """ The total inodes in the filesystem of the Pod's volume Instrument: updowncounter Unit: {inode} Note: This metric is derived from the [VolumeStats.Inodes](https://pkg.go.dev/k8s.io/kubelet@v0.33.0/pkg/apis/stats/v1alpha1#VolumeStats) field of the [PodStats](https://pkg.go.dev/k8s.io/kubelet@v0.33.0/pkg/apis/stats/v1alpha1#PodStats) of the Kubelet's stats API. """ def create_k8s_pod_volume_inode_count(meter: Meter) -> UpDownCounter: """The total inodes in the filesystem of the Pod's volume""" return meter.create_up_down_counter( name=K8S_POD_VOLUME_INODE_COUNT, description="The total inodes in the filesystem of the Pod's volume.", unit="{inode}", ) K8S_POD_VOLUME_INODE_FREE: Final = "k8s.pod.volume.inode.free" """ The free inodes in the filesystem of the Pod's volume Instrument: updowncounter Unit: {inode} Note: This metric is derived from the [VolumeStats.InodesFree](https://pkg.go.dev/k8s.io/kubelet@v0.33.0/pkg/apis/stats/v1alpha1#VolumeStats) field of the [PodStats](https://pkg.go.dev/k8s.io/kubelet@v0.33.0/pkg/apis/stats/v1alpha1#PodStats) of the Kubelet's stats API. """ def create_k8s_pod_volume_inode_free(meter: Meter) -> UpDownCounter: """The free inodes in the filesystem of the Pod's volume""" return meter.create_up_down_counter( name=K8S_POD_VOLUME_INODE_FREE, description="The free inodes in the filesystem of the Pod's volume.", unit="{inode}", ) K8S_POD_VOLUME_INODE_USED: Final = "k8s.pod.volume.inode.used" """ The inodes used by the filesystem of the Pod's volume Instrument: updowncounter Unit: {inode} Note: This metric is derived from the [VolumeStats.InodesUsed](https://pkg.go.dev/k8s.io/kubelet@v0.33.0/pkg/apis/stats/v1alpha1#VolumeStats) field of the [PodStats](https://pkg.go.dev/k8s.io/kubelet@v0.33.0/pkg/apis/stats/v1alpha1#PodStats) of the Kubelet's stats API. This may not be equal to `inodes - free` because filesystem may share inodes with other filesystems. """ def create_k8s_pod_volume_inode_used(meter: Meter) -> UpDownCounter: """The inodes used by the filesystem of the Pod's volume""" return meter.create_up_down_counter( name=K8S_POD_VOLUME_INODE_USED, description="The inodes used by the filesystem of the Pod's volume.", unit="{inode}", ) K8S_POD_VOLUME_USAGE: Final = "k8s.pod.volume.usage" """ Pod volume usage Instrument: updowncounter Unit: By Note: This may not equal capacity - available. This metric is derived from the [VolumeStats.UsedBytes](https://pkg.go.dev/k8s.io/kubelet@v0.33.0/pkg/apis/stats/v1alpha1#VolumeStats) field of the [PodStats](https://pkg.go.dev/k8s.io/kubelet@v0.33.0/pkg/apis/stats/v1alpha1#PodStats) of the Kubelet's stats API. """ def create_k8s_pod_volume_usage(meter: Meter) -> UpDownCounter: """Pod volume usage""" return meter.create_up_down_counter( name=K8S_POD_VOLUME_USAGE, description="Pod volume usage.", unit="By", ) K8S_REPLICASET_AVAILABLE_PODS: Final = "k8s.replicaset.available_pods" """ Deprecated: Replaced by `k8s.replicaset.pod.available`. """ def create_k8s_replicaset_available_pods(meter: Meter) -> UpDownCounter: """Deprecated, use `k8s.replicaset.pod.available` instead""" return meter.create_up_down_counter( name=K8S_REPLICASET_AVAILABLE_PODS, description="Deprecated, use `k8s.replicaset.pod.available` instead.", unit="{pod}", ) K8S_REPLICASET_DESIRED_PODS: Final = "k8s.replicaset.desired_pods" """ Deprecated: Replaced by `k8s.replicaset.pod.desired`. """ def create_k8s_replicaset_desired_pods(meter: Meter) -> UpDownCounter: """Deprecated, use `k8s.replicaset.pod.desired` instead""" return meter.create_up_down_counter( name=K8S_REPLICASET_DESIRED_PODS, description="Deprecated, use `k8s.replicaset.pod.desired` instead.", unit="{pod}", ) K8S_REPLICASET_POD_AVAILABLE: Final = "k8s.replicaset.pod.available" """ Total number of available replica pods (ready for at least minReadySeconds) targeted by this replicaset Instrument: updowncounter Unit: {pod} Note: This metric aligns with the `availableReplicas` field of the [K8s ReplicaSetStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#replicasetstatus-v1-apps). """ def create_k8s_replicaset_pod_available(meter: Meter) -> UpDownCounter: """Total number of available replica pods (ready for at least minReadySeconds) targeted by this replicaset""" return meter.create_up_down_counter( name=K8S_REPLICASET_POD_AVAILABLE, description="Total number of available replica pods (ready for at least minReadySeconds) targeted by this replicaset.", unit="{pod}", ) K8S_REPLICASET_POD_DESIRED: Final = "k8s.replicaset.pod.desired" """ Number of desired replica pods in this replicaset Instrument: updowncounter Unit: {pod} Note: This metric aligns with the `replicas` field of the [K8s ReplicaSetSpec](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#replicasetspec-v1-apps). """ def create_k8s_replicaset_pod_desired(meter: Meter) -> UpDownCounter: """Number of desired replica pods in this replicaset""" return meter.create_up_down_counter( name=K8S_REPLICASET_POD_DESIRED, description="Number of desired replica pods in this replicaset.", unit="{pod}", ) K8S_REPLICATION_CONTROLLER_AVAILABLE_PODS: Final = "k8s.replication_controller.available_pods" """ Deprecated: Replaced by `k8s.replicationcontroller.pod.available`. """ def create_k8s_replication_controller_available_pods(meter: Meter) -> UpDownCounter: """Deprecated, use `k8s.replicationcontroller.pod.available` instead""" return meter.create_up_down_counter( name=K8S_REPLICATION_CONTROLLER_AVAILABLE_PODS, description="Deprecated, use `k8s.replicationcontroller.pod.available` instead.", unit="{pod}", ) K8S_REPLICATION_CONTROLLER_DESIRED_PODS: Final = "k8s.replication_controller.desired_pods" """ Deprecated: Replaced by `k8s.replicationcontroller.pod.desired`. """ def create_k8s_replication_controller_desired_pods(meter: Meter) -> UpDownCounter: """Deprecated, use `k8s.replicationcontroller.pod.desired` instead""" return meter.create_up_down_counter( name=K8S_REPLICATION_CONTROLLER_DESIRED_PODS, description="Deprecated, use `k8s.replicationcontroller.pod.desired` instead.", unit="{pod}", ) K8S_REPLICATIONCONTROLLER_AVAILABLE_PODS: Final = "k8s.replicationcontroller.available_pods" """ Deprecated: Replaced by `k8s.replicationcontroller.pod.available`. """ def create_k8s_replicationcontroller_available_pods(meter: Meter) -> UpDownCounter: """Deprecated, use `k8s.replicationcontroller.pod.available` instead""" return meter.create_up_down_counter( name=K8S_REPLICATIONCONTROLLER_AVAILABLE_PODS, description="Deprecated, use `k8s.replicationcontroller.pod.available` instead.", unit="{pod}", ) K8S_REPLICATIONCONTROLLER_DESIRED_PODS: Final = "k8s.replicationcontroller.desired_pods" """ Deprecated: Replaced by `k8s.replicationcontroller.pod.desired`. """ def create_k8s_replicationcontroller_desired_pods(meter: Meter) -> UpDownCounter: """Deprecated, use `k8s.replicationcontroller.pod.desired` instead""" return meter.create_up_down_counter( name=K8S_REPLICATIONCONTROLLER_DESIRED_PODS, description="Deprecated, use `k8s.replicationcontroller.pod.desired` instead.", unit="{pod}", ) K8S_REPLICATIONCONTROLLER_POD_AVAILABLE: Final = "k8s.replicationcontroller.pod.available" """ Total number of available replica pods (ready for at least minReadySeconds) targeted by this replication controller Instrument: updowncounter Unit: {pod} Note: This metric aligns with the `availableReplicas` field of the [K8s ReplicationControllerStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#replicationcontrollerstatus-v1-core). """ def create_k8s_replicationcontroller_pod_available(meter: Meter) -> UpDownCounter: """Total number of available replica pods (ready for at least minReadySeconds) targeted by this replication controller""" return meter.create_up_down_counter( name=K8S_REPLICATIONCONTROLLER_POD_AVAILABLE, description="Total number of available replica pods (ready for at least minReadySeconds) targeted by this replication controller.", unit="{pod}", ) K8S_REPLICATIONCONTROLLER_POD_DESIRED: Final = "k8s.replicationcontroller.pod.desired" """ Number of desired replica pods in this replication controller Instrument: updowncounter Unit: {pod} Note: This metric aligns with the `replicas` field of the [K8s ReplicationControllerSpec](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#replicationcontrollerspec-v1-core). """ def create_k8s_replicationcontroller_pod_desired(meter: Meter) -> UpDownCounter: """Number of desired replica pods in this replication controller""" return meter.create_up_down_counter( name=K8S_REPLICATIONCONTROLLER_POD_DESIRED, description="Number of desired replica pods in this replication controller.", unit="{pod}", ) K8S_RESOURCEQUOTA_CPU_LIMIT_HARD: Final = "k8s.resourcequota.cpu.limit.hard" """ The CPU limits in a specific namespace. The value represents the configured quota limit of the resource in the namespace Instrument: updowncounter Unit: {cpu} Note: This metric is retrieved from the `hard` field of the [K8s ResourceQuotaStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#resourcequotastatus-v1-core). """ def create_k8s_resourcequota_cpu_limit_hard(meter: Meter) -> UpDownCounter: """The CPU limits in a specific namespace. The value represents the configured quota limit of the resource in the namespace""" return meter.create_up_down_counter( name=K8S_RESOURCEQUOTA_CPU_LIMIT_HARD, description="The CPU limits in a specific namespace. The value represents the configured quota limit of the resource in the namespace.", unit="{cpu}", ) K8S_RESOURCEQUOTA_CPU_LIMIT_USED: Final = "k8s.resourcequota.cpu.limit.used" """ The CPU limits in a specific namespace. The value represents the current observed total usage of the resource in the namespace Instrument: updowncounter Unit: {cpu} Note: This metric is retrieved from the `used` field of the [K8s ResourceQuotaStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#resourcequotastatus-v1-core). """ def create_k8s_resourcequota_cpu_limit_used(meter: Meter) -> UpDownCounter: """The CPU limits in a specific namespace. The value represents the current observed total usage of the resource in the namespace""" return meter.create_up_down_counter( name=K8S_RESOURCEQUOTA_CPU_LIMIT_USED, description="The CPU limits in a specific namespace. The value represents the current observed total usage of the resource in the namespace.", unit="{cpu}", ) K8S_RESOURCEQUOTA_CPU_REQUEST_HARD: Final = "k8s.resourcequota.cpu.request.hard" """ The CPU requests in a specific namespace. The value represents the configured quota limit of the resource in the namespace Instrument: updowncounter Unit: {cpu} Note: This metric is retrieved from the `hard` field of the [K8s ResourceQuotaStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#resourcequotastatus-v1-core). """ def create_k8s_resourcequota_cpu_request_hard(meter: Meter) -> UpDownCounter: """The CPU requests in a specific namespace. The value represents the configured quota limit of the resource in the namespace""" return meter.create_up_down_counter( name=K8S_RESOURCEQUOTA_CPU_REQUEST_HARD, description="The CPU requests in a specific namespace. The value represents the configured quota limit of the resource in the namespace.", unit="{cpu}", ) K8S_RESOURCEQUOTA_CPU_REQUEST_USED: Final = "k8s.resourcequota.cpu.request.used" """ The CPU requests in a specific namespace. The value represents the current observed total usage of the resource in the namespace Instrument: updowncounter Unit: {cpu} Note: This metric is retrieved from the `used` field of the [K8s ResourceQuotaStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#resourcequotastatus-v1-core). """ def create_k8s_resourcequota_cpu_request_used(meter: Meter) -> UpDownCounter: """The CPU requests in a specific namespace. The value represents the current observed total usage of the resource in the namespace""" return meter.create_up_down_counter( name=K8S_RESOURCEQUOTA_CPU_REQUEST_USED, description="The CPU requests in a specific namespace. The value represents the current observed total usage of the resource in the namespace.", unit="{cpu}", ) K8S_RESOURCEQUOTA_EPHEMERAL_STORAGE_LIMIT_HARD: Final = "k8s.resourcequota.ephemeral_storage.limit.hard" """ The sum of local ephemeral storage limits in the namespace. The value represents the configured quota limit of the resource in the namespace Instrument: updowncounter Unit: By Note: This metric is retrieved from the `hard` field of the [K8s ResourceQuotaStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#resourcequotastatus-v1-core). """ def create_k8s_resourcequota_ephemeral_storage_limit_hard(meter: Meter) -> UpDownCounter: """The sum of local ephemeral storage limits in the namespace. The value represents the configured quota limit of the resource in the namespace""" return meter.create_up_down_counter( name=K8S_RESOURCEQUOTA_EPHEMERAL_STORAGE_LIMIT_HARD, description="The sum of local ephemeral storage limits in the namespace. The value represents the configured quota limit of the resource in the namespace.", unit="By", ) K8S_RESOURCEQUOTA_EPHEMERAL_STORAGE_LIMIT_USED: Final = "k8s.resourcequota.ephemeral_storage.limit.used" """ The sum of local ephemeral storage limits in the namespace. The value represents the current observed total usage of the resource in the namespace Instrument: updowncounter Unit: By Note: This metric is retrieved from the `used` field of the [K8s ResourceQuotaStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#resourcequotastatus-v1-core). """ def create_k8s_resourcequota_ephemeral_storage_limit_used(meter: Meter) -> UpDownCounter: """The sum of local ephemeral storage limits in the namespace. The value represents the current observed total usage of the resource in the namespace""" return meter.create_up_down_counter( name=K8S_RESOURCEQUOTA_EPHEMERAL_STORAGE_LIMIT_USED, description="The sum of local ephemeral storage limits in the namespace. The value represents the current observed total usage of the resource in the namespace.", unit="By", ) K8S_RESOURCEQUOTA_EPHEMERAL_STORAGE_REQUEST_HARD: Final = "k8s.resourcequota.ephemeral_storage.request.hard" """ The sum of local ephemeral storage requests in the namespace. The value represents the configured quota limit of the resource in the namespace Instrument: updowncounter Unit: By Note: This metric is retrieved from the `hard` field of the [K8s ResourceQuotaStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#resourcequotastatus-v1-core). """ def create_k8s_resourcequota_ephemeral_storage_request_hard(meter: Meter) -> UpDownCounter: """The sum of local ephemeral storage requests in the namespace. The value represents the configured quota limit of the resource in the namespace""" return meter.create_up_down_counter( name=K8S_RESOURCEQUOTA_EPHEMERAL_STORAGE_REQUEST_HARD, description="The sum of local ephemeral storage requests in the namespace. The value represents the configured quota limit of the resource in the namespace.", unit="By", ) K8S_RESOURCEQUOTA_EPHEMERAL_STORAGE_REQUEST_USED: Final = "k8s.resourcequota.ephemeral_storage.request.used" """ The sum of local ephemeral storage requests in the namespace. The value represents the current observed total usage of the resource in the namespace Instrument: updowncounter Unit: By Note: This metric is retrieved from the `used` field of the [K8s ResourceQuotaStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#resourcequotastatus-v1-core). """ def create_k8s_resourcequota_ephemeral_storage_request_used(meter: Meter) -> UpDownCounter: """The sum of local ephemeral storage requests in the namespace. The value represents the current observed total usage of the resource in the namespace""" return meter.create_up_down_counter( name=K8S_RESOURCEQUOTA_EPHEMERAL_STORAGE_REQUEST_USED, description="The sum of local ephemeral storage requests in the namespace. The value represents the current observed total usage of the resource in the namespace.", unit="By", ) K8S_RESOURCEQUOTA_HUGEPAGE_COUNT_REQUEST_HARD: Final = "k8s.resourcequota.hugepage_count.request.hard" """ The huge page requests in a specific namespace. The value represents the configured quota limit of the resource in the namespace Instrument: updowncounter Unit: {hugepage} Note: This metric is retrieved from the `hard` field of the [K8s ResourceQuotaStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#resourcequotastatus-v1-core). """ def create_k8s_resourcequota_hugepage_count_request_hard(meter: Meter) -> UpDownCounter: """The huge page requests in a specific namespace. The value represents the configured quota limit of the resource in the namespace""" return meter.create_up_down_counter( name=K8S_RESOURCEQUOTA_HUGEPAGE_COUNT_REQUEST_HARD, description="The huge page requests in a specific namespace. The value represents the configured quota limit of the resource in the namespace.", unit="{hugepage}", ) K8S_RESOURCEQUOTA_HUGEPAGE_COUNT_REQUEST_USED: Final = "k8s.resourcequota.hugepage_count.request.used" """ The huge page requests in a specific namespace. The value represents the current observed total usage of the resource in the namespace Instrument: updowncounter Unit: {hugepage} Note: This metric is retrieved from the `used` field of the [K8s ResourceQuotaStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#resourcequotastatus-v1-core). """ def create_k8s_resourcequota_hugepage_count_request_used(meter: Meter) -> UpDownCounter: """The huge page requests in a specific namespace. The value represents the current observed total usage of the resource in the namespace""" return meter.create_up_down_counter( name=K8S_RESOURCEQUOTA_HUGEPAGE_COUNT_REQUEST_USED, description="The huge page requests in a specific namespace. The value represents the current observed total usage of the resource in the namespace.", unit="{hugepage}", ) K8S_RESOURCEQUOTA_MEMORY_LIMIT_HARD: Final = "k8s.resourcequota.memory.limit.hard" """ The memory limits in a specific namespace. The value represents the configured quota limit of the resource in the namespace Instrument: updowncounter Unit: By Note: This metric is retrieved from the `hard` field of the [K8s ResourceQuotaStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#resourcequotastatus-v1-core). """ def create_k8s_resourcequota_memory_limit_hard(meter: Meter) -> UpDownCounter: """The memory limits in a specific namespace. The value represents the configured quota limit of the resource in the namespace""" return meter.create_up_down_counter( name=K8S_RESOURCEQUOTA_MEMORY_LIMIT_HARD, description="The memory limits in a specific namespace. The value represents the configured quota limit of the resource in the namespace.", unit="By", ) K8S_RESOURCEQUOTA_MEMORY_LIMIT_USED: Final = "k8s.resourcequota.memory.limit.used" """ The memory limits in a specific namespace. The value represents the current observed total usage of the resource in the namespace Instrument: updowncounter Unit: By Note: This metric is retrieved from the `used` field of the [K8s ResourceQuotaStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#resourcequotastatus-v1-core). """ def create_k8s_resourcequota_memory_limit_used(meter: Meter) -> UpDownCounter: """The memory limits in a specific namespace. The value represents the current observed total usage of the resource in the namespace""" return meter.create_up_down_counter( name=K8S_RESOURCEQUOTA_MEMORY_LIMIT_USED, description="The memory limits in a specific namespace. The value represents the current observed total usage of the resource in the namespace.", unit="By", ) K8S_RESOURCEQUOTA_MEMORY_REQUEST_HARD: Final = "k8s.resourcequota.memory.request.hard" """ The memory requests in a specific namespace. The value represents the configured quota limit of the resource in the namespace Instrument: updowncounter Unit: By Note: This metric is retrieved from the `hard` field of the [K8s ResourceQuotaStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#resourcequotastatus-v1-core). """ def create_k8s_resourcequota_memory_request_hard(meter: Meter) -> UpDownCounter: """The memory requests in a specific namespace. The value represents the configured quota limit of the resource in the namespace""" return meter.create_up_down_counter( name=K8S_RESOURCEQUOTA_MEMORY_REQUEST_HARD, description="The memory requests in a specific namespace. The value represents the configured quota limit of the resource in the namespace.", unit="By", ) K8S_RESOURCEQUOTA_MEMORY_REQUEST_USED: Final = "k8s.resourcequota.memory.request.used" """ The memory requests in a specific namespace. The value represents the current observed total usage of the resource in the namespace Instrument: updowncounter Unit: By Note: This metric is retrieved from the `used` field of the [K8s ResourceQuotaStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#resourcequotastatus-v1-core). """ def create_k8s_resourcequota_memory_request_used(meter: Meter) -> UpDownCounter: """The memory requests in a specific namespace. The value represents the current observed total usage of the resource in the namespace""" return meter.create_up_down_counter( name=K8S_RESOURCEQUOTA_MEMORY_REQUEST_USED, description="The memory requests in a specific namespace. The value represents the current observed total usage of the resource in the namespace.", unit="By", ) K8S_RESOURCEQUOTA_OBJECT_COUNT_HARD: Final = "k8s.resourcequota.object_count.hard" """ The object count limits in a specific namespace. The value represents the configured quota limit of the resource in the namespace Instrument: updowncounter Unit: {object} Note: This metric is retrieved from the `hard` field of the [K8s ResourceQuotaStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#resourcequotastatus-v1-core). """ def create_k8s_resourcequota_object_count_hard(meter: Meter) -> UpDownCounter: """The object count limits in a specific namespace. The value represents the configured quota limit of the resource in the namespace""" return meter.create_up_down_counter( name=K8S_RESOURCEQUOTA_OBJECT_COUNT_HARD, description="The object count limits in a specific namespace. The value represents the configured quota limit of the resource in the namespace.", unit="{object}", ) K8S_RESOURCEQUOTA_OBJECT_COUNT_USED: Final = "k8s.resourcequota.object_count.used" """ The object count limits in a specific namespace. The value represents the current observed total usage of the resource in the namespace Instrument: updowncounter Unit: {object} Note: This metric is retrieved from the `used` field of the [K8s ResourceQuotaStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#resourcequotastatus-v1-core). """ def create_k8s_resourcequota_object_count_used(meter: Meter) -> UpDownCounter: """The object count limits in a specific namespace. The value represents the current observed total usage of the resource in the namespace""" return meter.create_up_down_counter( name=K8S_RESOURCEQUOTA_OBJECT_COUNT_USED, description="The object count limits in a specific namespace. The value represents the current observed total usage of the resource in the namespace.", unit="{object}", ) K8S_RESOURCEQUOTA_PERSISTENTVOLUMECLAIM_COUNT_HARD: Final = "k8s.resourcequota.persistentvolumeclaim_count.hard" """ The total number of PersistentVolumeClaims that can exist in the namespace. The value represents the configured quota limit of the resource in the namespace Instrument: updowncounter Unit: {persistentvolumeclaim} Note: This metric is retrieved from the `hard` field of the [K8s ResourceQuotaStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#resourcequotastatus-v1-core). The `k8s.storageclass.name` should be required when a resource quota is defined for a specific storage class. """ def create_k8s_resourcequota_persistentvolumeclaim_count_hard(meter: Meter) -> UpDownCounter: """The total number of PersistentVolumeClaims that can exist in the namespace. The value represents the configured quota limit of the resource in the namespace""" return meter.create_up_down_counter( name=K8S_RESOURCEQUOTA_PERSISTENTVOLUMECLAIM_COUNT_HARD, description="The total number of PersistentVolumeClaims that can exist in the namespace. The value represents the configured quota limit of the resource in the namespace.", unit="{persistentvolumeclaim}", ) K8S_RESOURCEQUOTA_PERSISTENTVOLUMECLAIM_COUNT_USED: Final = "k8s.resourcequota.persistentvolumeclaim_count.used" """ The total number of PersistentVolumeClaims that can exist in the namespace. The value represents the current observed total usage of the resource in the namespace Instrument: updowncounter Unit: {persistentvolumeclaim} Note: This metric is retrieved from the `used` field of the [K8s ResourceQuotaStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#resourcequotastatus-v1-core). The `k8s.storageclass.name` should be required when a resource quota is defined for a specific storage class. """ def create_k8s_resourcequota_persistentvolumeclaim_count_used(meter: Meter) -> UpDownCounter: """The total number of PersistentVolumeClaims that can exist in the namespace. The value represents the current observed total usage of the resource in the namespace""" return meter.create_up_down_counter( name=K8S_RESOURCEQUOTA_PERSISTENTVOLUMECLAIM_COUNT_USED, description="The total number of PersistentVolumeClaims that can exist in the namespace. The value represents the current observed total usage of the resource in the namespace.", unit="{persistentvolumeclaim}", ) K8S_RESOURCEQUOTA_STORAGE_REQUEST_HARD: Final = "k8s.resourcequota.storage.request.hard" """ The storage requests in a specific namespace. The value represents the configured quota limit of the resource in the namespace Instrument: updowncounter Unit: By Note: This metric is retrieved from the `hard` field of the [K8s ResourceQuotaStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#resourcequotastatus-v1-core). The `k8s.storageclass.name` should be required when a resource quota is defined for a specific storage class. """ def create_k8s_resourcequota_storage_request_hard(meter: Meter) -> UpDownCounter: """The storage requests in a specific namespace. The value represents the configured quota limit of the resource in the namespace""" return meter.create_up_down_counter( name=K8S_RESOURCEQUOTA_STORAGE_REQUEST_HARD, description="The storage requests in a specific namespace. The value represents the configured quota limit of the resource in the namespace.", unit="By", ) K8S_RESOURCEQUOTA_STORAGE_REQUEST_USED: Final = "k8s.resourcequota.storage.request.used" """ The storage requests in a specific namespace. The value represents the current observed total usage of the resource in the namespace Instrument: updowncounter Unit: By Note: This metric is retrieved from the `used` field of the [K8s ResourceQuotaStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#resourcequotastatus-v1-core). The `k8s.storageclass.name` should be required when a resource quota is defined for a specific storage class. """ def create_k8s_resourcequota_storage_request_used(meter: Meter) -> UpDownCounter: """The storage requests in a specific namespace. The value represents the current observed total usage of the resource in the namespace""" return meter.create_up_down_counter( name=K8S_RESOURCEQUOTA_STORAGE_REQUEST_USED, description="The storage requests in a specific namespace. The value represents the current observed total usage of the resource in the namespace.", unit="By", ) K8S_SERVICE_ENDPOINT_COUNT: Final = "k8s.service.endpoint.count" """ Number of endpoints for a service by condition and address type Instrument: gauge Unit: {endpoint} Note: This metric is derived from the Kubernetes [EndpointSlice API](https://kubernetes.io/docs/reference/kubernetes-api/service-resources/endpoint-slice-v1/). It reports the number of network endpoints backing a Service, broken down by their condition and address type. In dual-stack or multi-protocol clusters, separate counts are reported for each address family (`IPv4`, `IPv6`, `FQDN`). When the optional `zone` attribute is enabled, counts are further broken down by availability zone for zone-aware monitoring. An endpoint may be reported under multiple conditions simultaneously (e.g., both `serving` and `terminating` during a graceful shutdown). See [K8s EndpointConditions](https://kubernetes.io/docs/reference/kubernetes-api/service-resources/endpoint-slice-v1/) for more details. The conditions represent: - `ready`: Endpoints capable of receiving new connections. - `serving`: Endpoints currently handling traffic. - `terminating`: Endpoints that are being phased out but may still be handling existing connections. For Services with `publishNotReadyAddresses` enabled (common for headless StatefulSets), this metric will include endpoints that are published despite not being ready. The `k8s.service.publish_not_ready_addresses` resource attribute indicates this setting. """ def create_k8s_service_endpoint_count(meter: Meter, callbacks: Sequence[CallbackT] | None) -> ObservableGauge: """Number of endpoints for a service by condition and address type""" return meter.create_observable_gauge( name=K8S_SERVICE_ENDPOINT_COUNT, callbacks=callbacks, description="Number of endpoints for a service by condition and address type.", unit="{endpoint}", ) K8S_SERVICE_LOAD_BALANCER_INGRESS_COUNT: Final = "k8s.service.load_balancer.ingress.count" """ Number of load balancer ingress points (external IPs/hostnames) assigned to the service Instrument: gauge Unit: {ingress} Note: This metric reports the number of external ingress points (IP addresses or hostnames) assigned to a LoadBalancer Service. It is only emitted for Services of type `LoadBalancer` and reflects the assignments made by the underlying infrastructure's load balancer controller in the [.status.loadBalancer.ingress](https://kubernetes.io/docs/reference/kubernetes-api/service-resources/service-v1/#ServiceStatus) field. A value of `0` indicates that no ingress points have been assigned yet (e.g., during provisioning). A value greater than `1` may occur when multiple IPs or hostnames are assigned (e.g., dual-stack configurations). This metric signals that external endpoints have been assigned by the load balancer controller, but it does not guarantee that the load balancer is healthy. """ def create_k8s_service_load_balancer_ingress_count( meter: Meter, callbacks: Sequence[CallbackT] | None ) -> ObservableGauge: """Number of load balancer ingress points (external IPs/hostnames) assigned to the service""" return meter.create_observable_gauge( name=K8S_SERVICE_LOAD_BALANCER_INGRESS_COUNT, callbacks=callbacks, description="Number of load balancer ingress points (external IPs/hostnames) assigned to the service.", unit="{ingress}", ) K8S_STATEFULSET_CURRENT_PODS: Final = "k8s.statefulset.current_pods" """ Deprecated: Replaced by `k8s.statefulset.pod.current`. """ def create_k8s_statefulset_current_pods(meter: Meter) -> UpDownCounter: """Deprecated, use `k8s.statefulset.pod.current` instead""" return meter.create_up_down_counter( name=K8S_STATEFULSET_CURRENT_PODS, description="Deprecated, use `k8s.statefulset.pod.current` instead.", unit="{pod}", ) K8S_STATEFULSET_DESIRED_PODS: Final = "k8s.statefulset.desired_pods" """ Deprecated: Replaced by `k8s.statefulset.pod.desired`. """ def create_k8s_statefulset_desired_pods(meter: Meter) -> UpDownCounter: """Deprecated, use `k8s.statefulset.pod.desired` instead""" return meter.create_up_down_counter( name=K8S_STATEFULSET_DESIRED_PODS, description="Deprecated, use `k8s.statefulset.pod.desired` instead.", unit="{pod}", ) K8S_STATEFULSET_POD_CURRENT: Final = "k8s.statefulset.pod.current" """ The number of replica pods created by the statefulset controller from the statefulset version indicated by currentRevision Instrument: updowncounter Unit: {pod} Note: This metric aligns with the `currentReplicas` field of the [K8s StatefulSetStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#statefulsetstatus-v1-apps). """ def create_k8s_statefulset_pod_current(meter: Meter) -> UpDownCounter: """The number of replica pods created by the statefulset controller from the statefulset version indicated by currentRevision""" return meter.create_up_down_counter( name=K8S_STATEFULSET_POD_CURRENT, description="The number of replica pods created by the statefulset controller from the statefulset version indicated by currentRevision.", unit="{pod}", ) K8S_STATEFULSET_POD_DESIRED: Final = "k8s.statefulset.pod.desired" """ Number of desired replica pods in this statefulset Instrument: updowncounter Unit: {pod} Note: This metric aligns with the `replicas` field of the [K8s StatefulSetSpec](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#statefulsetspec-v1-apps). """ def create_k8s_statefulset_pod_desired(meter: Meter) -> UpDownCounter: """Number of desired replica pods in this statefulset""" return meter.create_up_down_counter( name=K8S_STATEFULSET_POD_DESIRED, description="Number of desired replica pods in this statefulset.", unit="{pod}", ) K8S_STATEFULSET_POD_READY: Final = "k8s.statefulset.pod.ready" """ The number of replica pods created for this statefulset with a Ready Condition Instrument: updowncounter Unit: {pod} Note: This metric aligns with the `readyReplicas` field of the [K8s StatefulSetStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#statefulsetstatus-v1-apps). """ def create_k8s_statefulset_pod_ready(meter: Meter) -> UpDownCounter: """The number of replica pods created for this statefulset with a Ready Condition""" return meter.create_up_down_counter( name=K8S_STATEFULSET_POD_READY, description="The number of replica pods created for this statefulset with a Ready Condition.", unit="{pod}", ) K8S_STATEFULSET_POD_UPDATED: Final = "k8s.statefulset.pod.updated" """ Number of replica pods created by the statefulset controller from the statefulset version indicated by updateRevision Instrument: updowncounter Unit: {pod} Note: This metric aligns with the `updatedReplicas` field of the [K8s StatefulSetStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#statefulsetstatus-v1-apps). """ def create_k8s_statefulset_pod_updated(meter: Meter) -> UpDownCounter: """Number of replica pods created by the statefulset controller from the statefulset version indicated by updateRevision""" return meter.create_up_down_counter( name=K8S_STATEFULSET_POD_UPDATED, description="Number of replica pods created by the statefulset controller from the statefulset version indicated by updateRevision.", unit="{pod}", ) K8S_STATEFULSET_READY_PODS: Final = "k8s.statefulset.ready_pods" """ Deprecated: Replaced by `k8s.statefulset.pod.ready`. """ def create_k8s_statefulset_ready_pods(meter: Meter) -> UpDownCounter: """Deprecated, use `k8s.statefulset.pod.ready` instead""" return meter.create_up_down_counter( name=K8S_STATEFULSET_READY_PODS, description="Deprecated, use `k8s.statefulset.pod.ready` instead.", unit="{pod}", ) K8S_STATEFULSET_UPDATED_PODS: Final = "k8s.statefulset.updated_pods" """ Deprecated: Replaced by `k8s.statefulset.pod.updated`. """ def create_k8s_statefulset_updated_pods(meter: Meter) -> UpDownCounter: """Deprecated, use `k8s.statefulset.pod.updated` instead""" return meter.create_up_down_counter( name=K8S_STATEFULSET_UPDATED_PODS, description="Deprecated, use `k8s.statefulset.pod.updated` instead.", unit="{pod}", ) mcp_metrics.py000066400000000000000000000051201527070070700407460ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/src/opentelemetry/semconv/_incubating/metrics# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from typing import Final from opentelemetry.metrics import Histogram, Meter MCP_CLIENT_OPERATION_DURATION: Final = "mcp.client.operation.duration" """ Deprecated: Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai). """ def create_mcp_client_operation_duration(meter: Meter) -> Histogram: """The duration of the MCP request or notification as observed on the sender from the time it was sent until the response or ack is received""" return meter.create_histogram( name=MCP_CLIENT_OPERATION_DURATION, description="The duration of the MCP request or notification as observed on the sender from the time it was sent until the response or ack is received.", unit="s", ) MCP_CLIENT_SESSION_DURATION: Final = "mcp.client.session.duration" """ Deprecated: Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai). """ def create_mcp_client_session_duration(meter: Meter) -> Histogram: """The duration of the MCP session as observed on the MCP client""" return meter.create_histogram( name=MCP_CLIENT_SESSION_DURATION, description="The duration of the MCP session as observed on the MCP client.", unit="s", ) MCP_SERVER_OPERATION_DURATION: Final = "mcp.server.operation.duration" """ Deprecated: Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai). """ def create_mcp_server_operation_duration(meter: Meter) -> Histogram: """MCP request or notification duration as observed on the receiver from the time it was received until the result or ack is sent""" return meter.create_histogram( name=MCP_SERVER_OPERATION_DURATION, description="MCP request or notification duration as observed on the receiver from the time it was received until the result or ack is sent.", unit="s", ) MCP_SERVER_SESSION_DURATION: Final = "mcp.server.session.duration" """ Deprecated: Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai). """ def create_mcp_server_session_duration(meter: Meter) -> Histogram: """The duration of the MCP session as observed on the MCP server""" return meter.create_histogram( name=MCP_SERVER_SESSION_DURATION, description="The duration of the MCP session as observed on the MCP server.", unit="s", ) messaging_metrics.py000066400000000000000000000131061527070070700421470ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/src/opentelemetry/semconv/_incubating/metrics# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from typing import Final from opentelemetry.metrics import Counter, Histogram, Meter MESSAGING_CLIENT_CONSUMED_MESSAGES: Final = "messaging.client.consumed.messages" """ Number of messages that were delivered to the application Instrument: counter Unit: {message} Note: Records the number of messages pulled from the broker or number of messages dispatched to the application in push-based scenarios. The metric SHOULD be reported once per message delivery. For example, if receiving and processing operations are both instrumented for a single message delivery, this counter is incremented when the message is received and not reported when it is processed. """ def create_messaging_client_consumed_messages(meter: Meter) -> Counter: """Number of messages that were delivered to the application""" return meter.create_counter( name=MESSAGING_CLIENT_CONSUMED_MESSAGES, description="Number of messages that were delivered to the application.", unit="{message}", ) MESSAGING_CLIENT_OPERATION_DURATION: Final = "messaging.client.operation.duration" """ Duration of messaging operation initiated by a producer or consumer client Instrument: histogram Unit: s Note: This metric SHOULD NOT be used to report processing duration - processing duration is reported in `messaging.process.duration` metric. """ def create_messaging_client_operation_duration(meter: Meter) -> Histogram: """Duration of messaging operation initiated by a producer or consumer client""" return meter.create_histogram( name=MESSAGING_CLIENT_OPERATION_DURATION, description="Duration of messaging operation initiated by a producer or consumer client.", unit="s", ) MESSAGING_CLIENT_PUBLISHED_MESSAGES: Final = "messaging.client.published.messages" """ Deprecated: Replaced by `messaging.client.sent.messages`. """ def create_messaging_client_published_messages(meter: Meter) -> Counter: """Deprecated. Use `messaging.client.sent.messages` instead""" return meter.create_counter( name=MESSAGING_CLIENT_PUBLISHED_MESSAGES, description="Deprecated. Use `messaging.client.sent.messages` instead.", unit="{message}", ) MESSAGING_CLIENT_SENT_MESSAGES: Final = "messaging.client.sent.messages" """ Number of messages producer attempted to send to the broker Instrument: counter Unit: {message} Note: This metric MUST NOT count messages that were created but haven't yet been sent. """ def create_messaging_client_sent_messages(meter: Meter) -> Counter: """Number of messages producer attempted to send to the broker""" return meter.create_counter( name=MESSAGING_CLIENT_SENT_MESSAGES, description="Number of messages producer attempted to send to the broker.", unit="{message}", ) MESSAGING_PROCESS_DURATION: Final = "messaging.process.duration" """ Duration of processing operation Instrument: histogram Unit: s Note: This metric MUST be reported for operations with `messaging.operation.type` that matches `process`. """ def create_messaging_process_duration(meter: Meter) -> Histogram: """Duration of processing operation""" return meter.create_histogram( name=MESSAGING_PROCESS_DURATION, description="Duration of processing operation.", unit="s", ) MESSAGING_PROCESS_MESSAGES: Final = "messaging.process.messages" """ Deprecated: Replaced by `messaging.client.consumed.messages`. """ def create_messaging_process_messages(meter: Meter) -> Counter: """Deprecated. Use `messaging.client.consumed.messages` instead""" return meter.create_counter( name=MESSAGING_PROCESS_MESSAGES, description="Deprecated. Use `messaging.client.consumed.messages` instead.", unit="{message}", ) MESSAGING_PUBLISH_DURATION: Final = "messaging.publish.duration" """ Deprecated: Replaced by `messaging.client.operation.duration`. """ def create_messaging_publish_duration(meter: Meter) -> Histogram: """Deprecated. Use `messaging.client.operation.duration` instead""" return meter.create_histogram( name=MESSAGING_PUBLISH_DURATION, description="Deprecated. Use `messaging.client.operation.duration` instead.", unit="s", ) MESSAGING_PUBLISH_MESSAGES: Final = "messaging.publish.messages" """ Deprecated: Replaced by `messaging.client.sent.messages`. """ def create_messaging_publish_messages(meter: Meter) -> Counter: """Deprecated. Use `messaging.client.sent.messages` instead""" return meter.create_counter( name=MESSAGING_PUBLISH_MESSAGES, description="Deprecated. Use `messaging.client.sent.messages` instead.", unit="{message}", ) MESSAGING_RECEIVE_DURATION: Final = "messaging.receive.duration" """ Deprecated: Replaced by `messaging.client.operation.duration`. """ def create_messaging_receive_duration(meter: Meter) -> Histogram: """Deprecated. Use `messaging.client.operation.duration` instead""" return meter.create_histogram( name=MESSAGING_RECEIVE_DURATION, description="Deprecated. Use `messaging.client.operation.duration` instead.", unit="s", ) MESSAGING_RECEIVE_MESSAGES: Final = "messaging.receive.messages" """ Deprecated: Replaced by `messaging.client.consumed.messages`. """ def create_messaging_receive_messages(meter: Meter) -> Counter: """Deprecated. Use `messaging.client.consumed.messages` instead""" return meter.create_counter( name=MESSAGING_RECEIVE_MESSAGES, description="Deprecated. Use `messaging.client.consumed.messages` instead.", unit="{message}", ) nfs_metrics.py000066400000000000000000000226161527070070700407660ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/src/opentelemetry/semconv/_incubating/metrics# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from typing import Final from opentelemetry.metrics import Counter, Meter, UpDownCounter NFS_CLIENT_NET_COUNT: Final = "nfs.client.net.count" """ Reports the count of kernel NFS client TCP segments and UDP datagrams handled Instrument: counter Unit: {record} Note: Linux: this metric is taken from the Linux kernel's svc_stat.netudpcnt and svc_stat.nettcpcnt. """ def create_nfs_client_net_count(meter: Meter) -> Counter: """Reports the count of kernel NFS client TCP segments and UDP datagrams handled""" return meter.create_counter( name=NFS_CLIENT_NET_COUNT, description="Reports the count of kernel NFS client TCP segments and UDP datagrams handled.", unit="{record}", ) NFS_CLIENT_NET_TCP_CONNECTION_ACCEPTED: Final = "nfs.client.net.tcp.connection.accepted" """ Reports the count of kernel NFS client TCP connections accepted Instrument: counter Unit: {connection} Note: Linux: this metric is taken from the Linux kernel's svc_stat.nettcpconn. """ def create_nfs_client_net_tcp_connection_accepted(meter: Meter) -> Counter: """Reports the count of kernel NFS client TCP connections accepted""" return meter.create_counter( name=NFS_CLIENT_NET_TCP_CONNECTION_ACCEPTED, description="Reports the count of kernel NFS client TCP connections accepted.", unit="{connection}", ) NFS_CLIENT_OPERATION_COUNT: Final = "nfs.client.operation.count" """ Reports the count of kernel NFSv4+ client operations Instrument: counter Unit: {operation} """ def create_nfs_client_operation_count(meter: Meter) -> Counter: """Reports the count of kernel NFSv4+ client operations""" return meter.create_counter( name=NFS_CLIENT_OPERATION_COUNT, description="Reports the count of kernel NFSv4+ client operations.", unit="{operation}", ) NFS_CLIENT_PROCEDURE_COUNT: Final = "nfs.client.procedure.count" """ Reports the count of kernel NFS client procedures Instrument: counter Unit: {procedure} """ def create_nfs_client_procedure_count(meter: Meter) -> Counter: """Reports the count of kernel NFS client procedures""" return meter.create_counter( name=NFS_CLIENT_PROCEDURE_COUNT, description="Reports the count of kernel NFS client procedures.", unit="{procedure}", ) NFS_CLIENT_RPC_AUTHREFRESH_COUNT: Final = "nfs.client.rpc.authrefresh.count" """ Reports the count of kernel NFS client RPC authentication refreshes Instrument: counter Unit: {authrefresh} Note: Linux: this metric is taken from the Linux kernel's svc_stat.rpcauthrefresh. """ def create_nfs_client_rpc_authrefresh_count(meter: Meter) -> Counter: """Reports the count of kernel NFS client RPC authentication refreshes""" return meter.create_counter( name=NFS_CLIENT_RPC_AUTHREFRESH_COUNT, description="Reports the count of kernel NFS client RPC authentication refreshes.", unit="{authrefresh}", ) NFS_CLIENT_RPC_COUNT: Final = "nfs.client.rpc.count" """ Reports the count of kernel NFS client RPCs sent, regardless of whether they're accepted/rejected by the server Instrument: counter Unit: {request} Note: Linux: this metric is taken from the Linux kernel's svc_stat.rpccnt. """ def create_nfs_client_rpc_count(meter: Meter) -> Counter: """Reports the count of kernel NFS client RPCs sent, regardless of whether they're accepted/rejected by the server""" return meter.create_counter( name=NFS_CLIENT_RPC_COUNT, description="Reports the count of kernel NFS client RPCs sent, regardless of whether they're accepted/rejected by the server.", unit="{request}", ) NFS_CLIENT_RPC_RETRANSMIT_COUNT: Final = "nfs.client.rpc.retransmit.count" """ Reports the count of kernel NFS client RPC retransmits Instrument: counter Unit: {retransmit} Note: Linux: this metric is taken from the Linux kernel's svc_stat.rpcretrans. """ def create_nfs_client_rpc_retransmit_count(meter: Meter) -> Counter: """Reports the count of kernel NFS client RPC retransmits""" return meter.create_counter( name=NFS_CLIENT_RPC_RETRANSMIT_COUNT, description="Reports the count of kernel NFS client RPC retransmits.", unit="{retransmit}", ) NFS_SERVER_FH_STALE_COUNT: Final = "nfs.server.fh.stale.count" """ Reports the count of kernel NFS server stale file handles Instrument: counter Unit: {fh} Note: Linux: this metric is taken from the Linux kernel NFSD_STATS_FH_STALE counter in the nfsd_net struct. """ def create_nfs_server_fh_stale_count(meter: Meter) -> Counter: """Reports the count of kernel NFS server stale file handles""" return meter.create_counter( name=NFS_SERVER_FH_STALE_COUNT, description="Reports the count of kernel NFS server stale file handles.", unit="{fh}", ) NFS_SERVER_IO: Final = "nfs.server.io" """ Reports the count of kernel NFS server bytes returned to receive and transmit (read and write) requests Instrument: counter Unit: By Note: Linux: this metric is taken from the Linux kernel NFSD_STATS_IO_READ and NFSD_STATS_IO_WRITE counters in the nfsd_net struct. """ def create_nfs_server_io(meter: Meter) -> Counter: """Reports the count of kernel NFS server bytes returned to receive and transmit (read and write) requests""" return meter.create_counter( name=NFS_SERVER_IO, description="Reports the count of kernel NFS server bytes returned to receive and transmit (read and write) requests.", unit="By", ) NFS_SERVER_NET_COUNT: Final = "nfs.server.net.count" """ Reports the count of kernel NFS server TCP segments and UDP datagrams handled Instrument: counter Unit: {record} Note: Linux: this metric is taken from the Linux kernel's svc_stat.nettcpcnt and svc_stat.netudpcnt. """ def create_nfs_server_net_count(meter: Meter) -> Counter: """Reports the count of kernel NFS server TCP segments and UDP datagrams handled""" return meter.create_counter( name=NFS_SERVER_NET_COUNT, description="Reports the count of kernel NFS server TCP segments and UDP datagrams handled.", unit="{record}", ) NFS_SERVER_NET_TCP_CONNECTION_ACCEPTED: Final = "nfs.server.net.tcp.connection.accepted" """ Reports the count of kernel NFS server TCP connections accepted Instrument: counter Unit: {connection} Note: Linux: this metric is taken from the Linux kernel's svc_stat.nettcpconn. """ def create_nfs_server_net_tcp_connection_accepted(meter: Meter) -> Counter: """Reports the count of kernel NFS server TCP connections accepted""" return meter.create_counter( name=NFS_SERVER_NET_TCP_CONNECTION_ACCEPTED, description="Reports the count of kernel NFS server TCP connections accepted.", unit="{connection}", ) NFS_SERVER_OPERATION_COUNT: Final = "nfs.server.operation.count" """ Reports the count of kernel NFSv4+ server operations Instrument: counter Unit: {operation} """ def create_nfs_server_operation_count(meter: Meter) -> Counter: """Reports the count of kernel NFSv4+ server operations""" return meter.create_counter( name=NFS_SERVER_OPERATION_COUNT, description="Reports the count of kernel NFSv4+ server operations.", unit="{operation}", ) NFS_SERVER_PROCEDURE_COUNT: Final = "nfs.server.procedure.count" """ Reports the count of kernel NFS server procedures Instrument: counter Unit: {procedure} """ def create_nfs_server_procedure_count(meter: Meter) -> Counter: """Reports the count of kernel NFS server procedures""" return meter.create_counter( name=NFS_SERVER_PROCEDURE_COUNT, description="Reports the count of kernel NFS server procedures.", unit="{procedure}", ) NFS_SERVER_REPCACHE_REQUESTS: Final = "nfs.server.repcache.requests" """ Reports the kernel NFS server reply cache request count by cache hit status Instrument: counter Unit: {request} """ def create_nfs_server_repcache_requests(meter: Meter) -> Counter: """Reports the kernel NFS server reply cache request count by cache hit status""" return meter.create_counter( name=NFS_SERVER_REPCACHE_REQUESTS, description="Reports the kernel NFS server reply cache request count by cache hit status.", unit="{request}", ) NFS_SERVER_RPC_COUNT: Final = "nfs.server.rpc.count" """ Reports the count of kernel NFS server RPCs handled Instrument: counter Unit: {request} Note: Linux: this metric is taken from the Linux kernel's svc_stat.rpccnt, the count of good RPCs. This metric can have an error.type of "format", "auth", or "client" for svc_stat.badfmt, svc_stat.badauth, and svc_stat.badclnt. """ def create_nfs_server_rpc_count(meter: Meter) -> Counter: """Reports the count of kernel NFS server RPCs handled""" return meter.create_counter( name=NFS_SERVER_RPC_COUNT, description="Reports the count of kernel NFS server RPCs handled.", unit="{request}", ) NFS_SERVER_THREAD_COUNT: Final = "nfs.server.thread.count" """ Reports the count of kernel NFS server available threads Instrument: updowncounter Unit: {thread} Note: Linux: this metric is taken from the Linux kernel nfsd_th_cnt variable. """ def create_nfs_server_thread_count(meter: Meter) -> UpDownCounter: """Reports the count of kernel NFS server available threads""" return meter.create_up_down_counter( name=NFS_SERVER_THREAD_COUNT, description="Reports the count of kernel NFS server available threads.", unit="{thread}", ) openshift_metrics.py000066400000000000000000000476741527070070700422120ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/src/opentelemetry/semconv/_incubating/metrics# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from typing import Final from opentelemetry.metrics import Meter, UpDownCounter OPENSHIFT_CLUSTERQUOTA_CPU_LIMIT_HARD: Final = "openshift.clusterquota.cpu.limit.hard" """ The enforced hard limit of the resource across all projects Instrument: updowncounter Unit: {cpu} Note: This metric is retrieved from the `Status.Total.Hard` field of the [K8s ResourceQuotaStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#resourcequotastatus-v1-core) of the [ClusterResourceQuota](https://docs.redhat.com/en/documentation/openshift_container_platform/4.19/html/schedule_and_quota_apis/clusterresourcequota-quota-openshift-io-v1#status-total). """ def create_openshift_clusterquota_cpu_limit_hard(meter: Meter) -> UpDownCounter: """The enforced hard limit of the resource across all projects""" return meter.create_up_down_counter( name=OPENSHIFT_CLUSTERQUOTA_CPU_LIMIT_HARD, description="The enforced hard limit of the resource across all projects.", unit="{cpu}", ) OPENSHIFT_CLUSTERQUOTA_CPU_LIMIT_USED: Final = "openshift.clusterquota.cpu.limit.used" """ The current observed total usage of the resource across all projects Instrument: updowncounter Unit: {cpu} Note: This metric is retrieved from the `Status.Total.Used` field of the [K8s ResourceQuotaStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#resourcequotastatus-v1-core) of the [ClusterResourceQuota](https://docs.redhat.com/en/documentation/openshift_container_platform/4.19/html/schedule_and_quota_apis/clusterresourcequota-quota-openshift-io-v1#status-total). """ def create_openshift_clusterquota_cpu_limit_used(meter: Meter) -> UpDownCounter: """The current observed total usage of the resource across all projects""" return meter.create_up_down_counter( name=OPENSHIFT_CLUSTERQUOTA_CPU_LIMIT_USED, description="The current observed total usage of the resource across all projects.", unit="{cpu}", ) OPENSHIFT_CLUSTERQUOTA_CPU_REQUEST_HARD: Final = "openshift.clusterquota.cpu.request.hard" """ The enforced hard limit of the resource across all projects Instrument: updowncounter Unit: {cpu} Note: This metric is retrieved from the `Status.Total.Hard` field of the [K8s ResourceQuotaStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#resourcequotastatus-v1-core) of the [ClusterResourceQuota](https://docs.redhat.com/en/documentation/openshift_container_platform/4.19/html/schedule_and_quota_apis/clusterresourcequota-quota-openshift-io-v1#status-total). """ def create_openshift_clusterquota_cpu_request_hard(meter: Meter) -> UpDownCounter: """The enforced hard limit of the resource across all projects""" return meter.create_up_down_counter( name=OPENSHIFT_CLUSTERQUOTA_CPU_REQUEST_HARD, description="The enforced hard limit of the resource across all projects.", unit="{cpu}", ) OPENSHIFT_CLUSTERQUOTA_CPU_REQUEST_USED: Final = "openshift.clusterquota.cpu.request.used" """ The current observed total usage of the resource across all projects Instrument: updowncounter Unit: {cpu} Note: This metric is retrieved from the `Status.Total.Used` field of the [K8s ResourceQuotaStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#resourcequotastatus-v1-core) of the [ClusterResourceQuota](https://docs.redhat.com/en/documentation/openshift_container_platform/4.19/html/schedule_and_quota_apis/clusterresourcequota-quota-openshift-io-v1#status-total). """ def create_openshift_clusterquota_cpu_request_used(meter: Meter) -> UpDownCounter: """The current observed total usage of the resource across all projects""" return meter.create_up_down_counter( name=OPENSHIFT_CLUSTERQUOTA_CPU_REQUEST_USED, description="The current observed total usage of the resource across all projects.", unit="{cpu}", ) OPENSHIFT_CLUSTERQUOTA_EPHEMERAL_STORAGE_LIMIT_HARD: Final = "openshift.clusterquota.ephemeral_storage.limit.hard" """ The enforced hard limit of the resource across all projects Instrument: updowncounter Unit: By Note: This metric is retrieved from the `Status.Total.Hard` field of the [K8s ResourceQuotaStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#resourcequotastatus-v1-core) of the [ClusterResourceQuota](https://docs.redhat.com/en/documentation/openshift_container_platform/4.19/html/schedule_and_quota_apis/clusterresourcequota-quota-openshift-io-v1#status-total). """ def create_openshift_clusterquota_ephemeral_storage_limit_hard(meter: Meter) -> UpDownCounter: """The enforced hard limit of the resource across all projects""" return meter.create_up_down_counter( name=OPENSHIFT_CLUSTERQUOTA_EPHEMERAL_STORAGE_LIMIT_HARD, description="The enforced hard limit of the resource across all projects.", unit="By", ) OPENSHIFT_CLUSTERQUOTA_EPHEMERAL_STORAGE_LIMIT_USED: Final = "openshift.clusterquota.ephemeral_storage.limit.used" """ The current observed total usage of the resource across all projects Instrument: updowncounter Unit: By Note: This metric is retrieved from the `Status.Total.Used` field of the [K8s ResourceQuotaStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#resourcequotastatus-v1-core) of the [ClusterResourceQuota](https://docs.redhat.com/en/documentation/openshift_container_platform/4.19/html/schedule_and_quota_apis/clusterresourcequota-quota-openshift-io-v1#status-total). """ def create_openshift_clusterquota_ephemeral_storage_limit_used(meter: Meter) -> UpDownCounter: """The current observed total usage of the resource across all projects""" return meter.create_up_down_counter( name=OPENSHIFT_CLUSTERQUOTA_EPHEMERAL_STORAGE_LIMIT_USED, description="The current observed total usage of the resource across all projects.", unit="By", ) OPENSHIFT_CLUSTERQUOTA_EPHEMERAL_STORAGE_REQUEST_HARD: Final = "openshift.clusterquota.ephemeral_storage.request.hard" """ The enforced hard limit of the resource across all projects Instrument: updowncounter Unit: By Note: This metric is retrieved from the `Status.Total.Hard` field of the [K8s ResourceQuotaStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#resourcequotastatus-v1-core) of the [ClusterResourceQuota](https://docs.redhat.com/en/documentation/openshift_container_platform/4.19/html/schedule_and_quota_apis/clusterresourcequota-quota-openshift-io-v1#status-total). """ def create_openshift_clusterquota_ephemeral_storage_request_hard(meter: Meter) -> UpDownCounter: """The enforced hard limit of the resource across all projects""" return meter.create_up_down_counter( name=OPENSHIFT_CLUSTERQUOTA_EPHEMERAL_STORAGE_REQUEST_HARD, description="The enforced hard limit of the resource across all projects.", unit="By", ) OPENSHIFT_CLUSTERQUOTA_EPHEMERAL_STORAGE_REQUEST_USED: Final = "openshift.clusterquota.ephemeral_storage.request.used" """ The current observed total usage of the resource across all projects Instrument: updowncounter Unit: By Note: This metric is retrieved from the `Status.Total.Used` field of the [K8s ResourceQuotaStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#resourcequotastatus-v1-core) of the [ClusterResourceQuota](https://docs.redhat.com/en/documentation/openshift_container_platform/4.19/html/schedule_and_quota_apis/clusterresourcequota-quota-openshift-io-v1#status-total). """ def create_openshift_clusterquota_ephemeral_storage_request_used(meter: Meter) -> UpDownCounter: """The current observed total usage of the resource across all projects""" return meter.create_up_down_counter( name=OPENSHIFT_CLUSTERQUOTA_EPHEMERAL_STORAGE_REQUEST_USED, description="The current observed total usage of the resource across all projects.", unit="By", ) OPENSHIFT_CLUSTERQUOTA_HUGEPAGE_COUNT_REQUEST_HARD: Final = "openshift.clusterquota.hugepage_count.request.hard" """ The enforced hard limit of the resource across all projects Instrument: updowncounter Unit: {hugepage} Note: This metric is retrieved from the `Status.Total.Hard` field of the [K8s ResourceQuotaStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#resourcequotastatus-v1-core) of the [ClusterResourceQuota](https://docs.redhat.com/en/documentation/openshift_container_platform/4.19/html/schedule_and_quota_apis/clusterresourcequota-quota-openshift-io-v1#status-total). """ def create_openshift_clusterquota_hugepage_count_request_hard(meter: Meter) -> UpDownCounter: """The enforced hard limit of the resource across all projects""" return meter.create_up_down_counter( name=OPENSHIFT_CLUSTERQUOTA_HUGEPAGE_COUNT_REQUEST_HARD, description="The enforced hard limit of the resource across all projects.", unit="{hugepage}", ) OPENSHIFT_CLUSTERQUOTA_HUGEPAGE_COUNT_REQUEST_USED: Final = "openshift.clusterquota.hugepage_count.request.used" """ The current observed total usage of the resource across all projects Instrument: updowncounter Unit: {hugepage} Note: This metric is retrieved from the `Status.Total.Used` field of the [K8s ResourceQuotaStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#resourcequotastatus-v1-core) of the [ClusterResourceQuota](https://docs.redhat.com/en/documentation/openshift_container_platform/4.19/html/schedule_and_quota_apis/clusterresourcequota-quota-openshift-io-v1#status-total). """ def create_openshift_clusterquota_hugepage_count_request_used(meter: Meter) -> UpDownCounter: """The current observed total usage of the resource across all projects""" return meter.create_up_down_counter( name=OPENSHIFT_CLUSTERQUOTA_HUGEPAGE_COUNT_REQUEST_USED, description="The current observed total usage of the resource across all projects.", unit="{hugepage}", ) OPENSHIFT_CLUSTERQUOTA_MEMORY_LIMIT_HARD: Final = "openshift.clusterquota.memory.limit.hard" """ The enforced hard limit of the resource across all projects Instrument: updowncounter Unit: By Note: This metric is retrieved from the `Status.Total.Hard` field of the [K8s ResourceQuotaStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#resourcequotastatus-v1-core) of the [ClusterResourceQuota](https://docs.redhat.com/en/documentation/openshift_container_platform/4.19/html/schedule_and_quota_apis/clusterresourcequota-quota-openshift-io-v1#status-total). """ def create_openshift_clusterquota_memory_limit_hard(meter: Meter) -> UpDownCounter: """The enforced hard limit of the resource across all projects""" return meter.create_up_down_counter( name=OPENSHIFT_CLUSTERQUOTA_MEMORY_LIMIT_HARD, description="The enforced hard limit of the resource across all projects.", unit="By", ) OPENSHIFT_CLUSTERQUOTA_MEMORY_LIMIT_USED: Final = "openshift.clusterquota.memory.limit.used" """ The current observed total usage of the resource across all projects Instrument: updowncounter Unit: By Note: This metric is retrieved from the `Status.Total.Used` field of the [K8s ResourceQuotaStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#resourcequotastatus-v1-core) of the [ClusterResourceQuota](https://docs.redhat.com/en/documentation/openshift_container_platform/4.19/html/schedule_and_quota_apis/clusterresourcequota-quota-openshift-io-v1#status-total). """ def create_openshift_clusterquota_memory_limit_used(meter: Meter) -> UpDownCounter: """The current observed total usage of the resource across all projects""" return meter.create_up_down_counter( name=OPENSHIFT_CLUSTERQUOTA_MEMORY_LIMIT_USED, description="The current observed total usage of the resource across all projects.", unit="By", ) OPENSHIFT_CLUSTERQUOTA_MEMORY_REQUEST_HARD: Final = "openshift.clusterquota.memory.request.hard" """ The enforced hard limit of the resource across all projects Instrument: updowncounter Unit: By Note: This metric is retrieved from the `Status.Total.Hard` field of the [K8s ResourceQuotaStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#resourcequotastatus-v1-core) of the [ClusterResourceQuota](https://docs.redhat.com/en/documentation/openshift_container_platform/4.19/html/schedule_and_quota_apis/clusterresourcequota-quota-openshift-io-v1#status-total). """ def create_openshift_clusterquota_memory_request_hard(meter: Meter) -> UpDownCounter: """The enforced hard limit of the resource across all projects""" return meter.create_up_down_counter( name=OPENSHIFT_CLUSTERQUOTA_MEMORY_REQUEST_HARD, description="The enforced hard limit of the resource across all projects.", unit="By", ) OPENSHIFT_CLUSTERQUOTA_MEMORY_REQUEST_USED: Final = "openshift.clusterquota.memory.request.used" """ The current observed total usage of the resource across all projects Instrument: updowncounter Unit: By Note: This metric is retrieved from the `Status.Total.Used` field of the [K8s ResourceQuotaStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#resourcequotastatus-v1-core) of the [ClusterResourceQuota](https://docs.redhat.com/en/documentation/openshift_container_platform/4.19/html/schedule_and_quota_apis/clusterresourcequota-quota-openshift-io-v1#status-total). """ def create_openshift_clusterquota_memory_request_used(meter: Meter) -> UpDownCounter: """The current observed total usage of the resource across all projects""" return meter.create_up_down_counter( name=OPENSHIFT_CLUSTERQUOTA_MEMORY_REQUEST_USED, description="The current observed total usage of the resource across all projects.", unit="By", ) OPENSHIFT_CLUSTERQUOTA_OBJECT_COUNT_HARD: Final = "openshift.clusterquota.object_count.hard" """ The enforced hard limit of the resource across all projects Instrument: updowncounter Unit: {object} Note: This metric is retrieved from the `Status.Total.Hard` field of the [K8s ResourceQuotaStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#resourcequotastatus-v1-core) of the [ClusterResourceQuota](https://docs.redhat.com/en/documentation/openshift_container_platform/4.19/html/schedule_and_quota_apis/clusterresourcequota-quota-openshift-io-v1#status-total). """ def create_openshift_clusterquota_object_count_hard(meter: Meter) -> UpDownCounter: """The enforced hard limit of the resource across all projects""" return meter.create_up_down_counter( name=OPENSHIFT_CLUSTERQUOTA_OBJECT_COUNT_HARD, description="The enforced hard limit of the resource across all projects.", unit="{object}", ) OPENSHIFT_CLUSTERQUOTA_OBJECT_COUNT_USED: Final = "openshift.clusterquota.object_count.used" """ The current observed total usage of the resource across all projects Instrument: updowncounter Unit: {object} Note: This metric is retrieved from the `Status.Total.Used` field of the [K8s ResourceQuotaStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#resourcequotastatus-v1-core) of the [ClusterResourceQuota](https://docs.redhat.com/en/documentation/openshift_container_platform/4.19/html/schedule_and_quota_apis/clusterresourcequota-quota-openshift-io-v1#status-total). """ def create_openshift_clusterquota_object_count_used(meter: Meter) -> UpDownCounter: """The current observed total usage of the resource across all projects""" return meter.create_up_down_counter( name=OPENSHIFT_CLUSTERQUOTA_OBJECT_COUNT_USED, description="The current observed total usage of the resource across all projects.", unit="{object}", ) OPENSHIFT_CLUSTERQUOTA_PERSISTENTVOLUMECLAIM_COUNT_HARD: Final = ( "openshift.clusterquota.persistentvolumeclaim_count.hard" ) """ The enforced hard limit of the resource across all projects Instrument: updowncounter Unit: {persistentvolumeclaim} Note: This metric is retrieved from the `Status.Total.Hard` field of the [K8s ResourceQuotaStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#resourcequotastatus-v1-core) of the [ClusterResourceQuota](https://docs.redhat.com/en/documentation/openshift_container_platform/4.19/html/schedule_and_quota_apis/clusterresourcequota-quota-openshift-io-v1#status-total). The `k8s.storageclass.name` should be required when a resource quota is defined for a specific storage class. """ def create_openshift_clusterquota_persistentvolumeclaim_count_hard(meter: Meter) -> UpDownCounter: """The enforced hard limit of the resource across all projects""" return meter.create_up_down_counter( name=OPENSHIFT_CLUSTERQUOTA_PERSISTENTVOLUMECLAIM_COUNT_HARD, description="The enforced hard limit of the resource across all projects.", unit="{persistentvolumeclaim}", ) OPENSHIFT_CLUSTERQUOTA_PERSISTENTVOLUMECLAIM_COUNT_USED: Final = ( "openshift.clusterquota.persistentvolumeclaim_count.used" ) """ The current observed total usage of the resource across all projects Instrument: updowncounter Unit: {persistentvolumeclaim} Note: This metric is retrieved from the `Status.Total.Used` field of the [K8s ResourceQuotaStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#resourcequotastatus-v1-core) of the [ClusterResourceQuota](https://docs.redhat.com/en/documentation/openshift_container_platform/4.19/html/schedule_and_quota_apis/clusterresourcequota-quota-openshift-io-v1#status-total). The `k8s.storageclass.name` should be required when a resource quota is defined for a specific storage class. """ def create_openshift_clusterquota_persistentvolumeclaim_count_used(meter: Meter) -> UpDownCounter: """The current observed total usage of the resource across all projects""" return meter.create_up_down_counter( name=OPENSHIFT_CLUSTERQUOTA_PERSISTENTVOLUMECLAIM_COUNT_USED, description="The current observed total usage of the resource across all projects.", unit="{persistentvolumeclaim}", ) OPENSHIFT_CLUSTERQUOTA_STORAGE_REQUEST_HARD: Final = "openshift.clusterquota.storage.request.hard" """ The enforced hard limit of the resource across all projects Instrument: updowncounter Unit: By Note: This metric is retrieved from the `Status.Total.Hard` field of the [K8s ResourceQuotaStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#resourcequotastatus-v1-core) of the [ClusterResourceQuota](https://docs.redhat.com/en/documentation/openshift_container_platform/4.19/html/schedule_and_quota_apis/clusterresourcequota-quota-openshift-io-v1#status-total). The `k8s.storageclass.name` should be required when a resource quota is defined for a specific storage class. """ def create_openshift_clusterquota_storage_request_hard(meter: Meter) -> UpDownCounter: """The enforced hard limit of the resource across all projects""" return meter.create_up_down_counter( name=OPENSHIFT_CLUSTERQUOTA_STORAGE_REQUEST_HARD, description="The enforced hard limit of the resource across all projects.", unit="By", ) OPENSHIFT_CLUSTERQUOTA_STORAGE_REQUEST_USED: Final = "openshift.clusterquota.storage.request.used" """ The current observed total usage of the resource across all projects Instrument: updowncounter Unit: By Note: This metric is retrieved from the `Status.Total.Used` field of the [K8s ResourceQuotaStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#resourcequotastatus-v1-core) of the [ClusterResourceQuota](https://docs.redhat.com/en/documentation/openshift_container_platform/4.19/html/schedule_and_quota_apis/clusterresourcequota-quota-openshift-io-v1#status-total). The `k8s.storageclass.name` should be required when a resource quota is defined for a specific storage class. """ def create_openshift_clusterquota_storage_request_used(meter: Meter) -> UpDownCounter: """The current observed total usage of the resource across all projects""" return meter.create_up_down_counter( name=OPENSHIFT_CLUSTERQUOTA_STORAGE_REQUEST_USED, description="The current observed total usage of the resource across all projects.", unit="By", ) otel_metrics.py000066400000000000000000000500251527070070700411360ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/src/opentelemetry/semconv/_incubating/metrics# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from typing import Final from opentelemetry.metrics import Counter, Histogram, Meter, UpDownCounter OTEL_SDK_EXPORTER_LOG_EXPORTED: Final = "otel.sdk.exporter.log.exported" """ The number of log records for which the export has finished, either successful or failed Instrument: counter Unit: {log_record} Note: For successful exports, `error.type` MUST NOT be set. For failed exports, `error.type` MUST contain the failure cause. For exporters with partial success semantics (e.g. OTLP with `rejected_log_records`), rejected log records MUST count as failed and only non-rejected log records count as success. If no rejection reason is available, `rejected` SHOULD be used as value for `error.type`. If the exporter retries failed export attempts, the export operation is considered finished only after the final attempt has concluded. Each log record MUST be counted exactly once per export operation: intermediate failed attempts that are followed by a retry MUST NOT increment the counter, and `error.type` reflects the cause of the final attempt. """ def create_otel_sdk_exporter_log_exported(meter: Meter) -> Counter: """The number of log records for which the export has finished, either successful or failed""" return meter.create_counter( name=OTEL_SDK_EXPORTER_LOG_EXPORTED, description="The number of log records for which the export has finished, either successful or failed.", unit="{log_record}", ) OTEL_SDK_EXPORTER_LOG_INFLIGHT: Final = "otel.sdk.exporter.log.inflight" """ The number of log records which were passed to the exporter, but that have not been exported yet (neither successful, nor failed) Instrument: updowncounter Unit: {log_record} Note: Log records are counted as inflight from when they are passed to the exporter until the export operation has concluded. If the exporter retries failed export attempts, log records remain inflight across all retry attempts and any backoff between them. """ def create_otel_sdk_exporter_log_inflight(meter: Meter) -> UpDownCounter: """The number of log records which were passed to the exporter, but that have not been exported yet (neither successful, nor failed)""" return meter.create_up_down_counter( name=OTEL_SDK_EXPORTER_LOG_INFLIGHT, description="The number of log records which were passed to the exporter, but that have not been exported yet (neither successful, nor failed).", unit="{log_record}", ) OTEL_SDK_EXPORTER_METRIC_DATA_POINT_EXPORTED: Final = "otel.sdk.exporter.metric_data_point.exported" """ The number of metric data points for which the export has finished, either successful or failed Instrument: counter Unit: {data_point} Note: For successful exports, `error.type` MUST NOT be set. For failed exports, `error.type` MUST contain the failure cause. For exporters with partial success semantics (e.g. OTLP with `rejected_data_points`), rejected data points MUST count as failed and only non-rejected data points count as success. If no rejection reason is available, `rejected` SHOULD be used as value for `error.type`. If the exporter retries failed export attempts, the export operation is considered finished only after the final attempt has concluded. Each metric data point MUST be counted exactly once per export operation: intermediate failed attempts that are followed by a retry MUST NOT increment the counter, and `error.type` reflects the cause of the final attempt. """ def create_otel_sdk_exporter_metric_data_point_exported(meter: Meter) -> Counter: """The number of metric data points for which the export has finished, either successful or failed""" return meter.create_counter( name=OTEL_SDK_EXPORTER_METRIC_DATA_POINT_EXPORTED, description="The number of metric data points for which the export has finished, either successful or failed.", unit="{data_point}", ) OTEL_SDK_EXPORTER_METRIC_DATA_POINT_INFLIGHT: Final = "otel.sdk.exporter.metric_data_point.inflight" """ The number of metric data points which were passed to the exporter, but that have not been exported yet (neither successful, nor failed) Instrument: updowncounter Unit: {data_point} Note: Metric data points are counted as inflight from when they are passed to the exporter until the export operation has concluded. If the exporter retries failed export attempts, metric data points remain inflight across all retry attempts and any backoff between them. """ def create_otel_sdk_exporter_metric_data_point_inflight(meter: Meter) -> UpDownCounter: """The number of metric data points which were passed to the exporter, but that have not been exported yet (neither successful, nor failed)""" return meter.create_up_down_counter( name=OTEL_SDK_EXPORTER_METRIC_DATA_POINT_INFLIGHT, description="The number of metric data points which were passed to the exporter, but that have not been exported yet (neither successful, nor failed).", unit="{data_point}", ) OTEL_SDK_EXPORTER_OPERATION_DURATION: Final = "otel.sdk.exporter.operation.duration" """ The duration of exporting a batch of telemetry records Instrument: histogram Unit: s Note: This metric defines successful operations using the full success definitions for [HTTP](https://github.com/open-telemetry/opentelemetry-proto/blob/v1.5.0/docs/specification.md#full-success-1) and [gRPC](https://github.com/open-telemetry/opentelemetry-proto/blob/v1.5.0/docs/specification.md#full-success). Anything else is defined as an unsuccessful operation. For successful operations, `error.type` MUST NOT be set. For unsuccessful export operations, `error.type` MUST contain a relevant failure cause. If the exporter retries failed export attempts, exactly one observation MUST be recorded per export operation, covering the wall-clock duration from the start of the first attempt through the conclusion of the final attempt (including any backoff between attempts). `error.type` reflects the cause of the final attempt. """ def create_otel_sdk_exporter_operation_duration(meter: Meter) -> Histogram: """The duration of exporting a batch of telemetry records""" return meter.create_histogram( name=OTEL_SDK_EXPORTER_OPERATION_DURATION, description="The duration of exporting a batch of telemetry records.", unit="s", ) OTEL_SDK_EXPORTER_SPAN_EXPORTED: Final = "otel.sdk.exporter.span.exported" """ The number of spans for which the export has finished, either successful or failed Instrument: counter Unit: {span} Note: For successful exports, `error.type` MUST NOT be set. For failed exports, `error.type` MUST contain the failure cause. For exporters with partial success semantics (e.g. OTLP with `rejected_spans`), rejected spans MUST count as failed and only non-rejected spans count as success. If no rejection reason is available, `rejected` SHOULD be used as value for `error.type`. If the exporter retries failed export attempts, the export operation is considered finished only after the final attempt has concluded. Each span MUST be counted exactly once per export operation: intermediate failed attempts that are followed by a retry MUST NOT increment the counter, and `error.type` reflects the cause of the final attempt. """ def create_otel_sdk_exporter_span_exported(meter: Meter) -> Counter: """The number of spans for which the export has finished, either successful or failed""" return meter.create_counter( name=OTEL_SDK_EXPORTER_SPAN_EXPORTED, description="The number of spans for which the export has finished, either successful or failed.", unit="{span}", ) OTEL_SDK_EXPORTER_SPAN_EXPORTED_COUNT: Final = "otel.sdk.exporter.span.exported.count" """ Deprecated: Replaced by `otel.sdk.exporter.span.exported`. """ def create_otel_sdk_exporter_span_exported_count(meter: Meter) -> UpDownCounter: """Deprecated, use `otel.sdk.exporter.span.exported` instead""" return meter.create_up_down_counter( name=OTEL_SDK_EXPORTER_SPAN_EXPORTED_COUNT, description="Deprecated, use `otel.sdk.exporter.span.exported` instead.", unit="{span}", ) OTEL_SDK_EXPORTER_SPAN_INFLIGHT: Final = "otel.sdk.exporter.span.inflight" """ The number of spans which were passed to the exporter, but that have not been exported yet (neither successful, nor failed) Instrument: updowncounter Unit: {span} Note: Spans are counted as inflight from when they are passed to the exporter until the export operation has concluded. If the exporter retries failed export attempts, spans remain inflight across all retry attempts and any backoff between them. """ def create_otel_sdk_exporter_span_inflight(meter: Meter) -> UpDownCounter: """The number of spans which were passed to the exporter, but that have not been exported yet (neither successful, nor failed)""" return meter.create_up_down_counter( name=OTEL_SDK_EXPORTER_SPAN_INFLIGHT, description="The number of spans which were passed to the exporter, but that have not been exported yet (neither successful, nor failed).", unit="{span}", ) OTEL_SDK_EXPORTER_SPAN_INFLIGHT_COUNT: Final = "otel.sdk.exporter.span.inflight.count" """ Deprecated: Replaced by `otel.sdk.exporter.span.inflight`. """ def create_otel_sdk_exporter_span_inflight_count(meter: Meter) -> UpDownCounter: """Deprecated, use `otel.sdk.exporter.span.inflight` instead""" return meter.create_up_down_counter( name=OTEL_SDK_EXPORTER_SPAN_INFLIGHT_COUNT, description="Deprecated, use `otel.sdk.exporter.span.inflight` instead.", unit="{span}", ) OTEL_SDK_LOG_CREATED: Final = "otel.sdk.log.created" """ The number of logs submitted to enabled SDK Loggers Instrument: counter Unit: {log_record} """ def create_otel_sdk_log_created(meter: Meter) -> Counter: """The number of logs submitted to enabled SDK Loggers""" return meter.create_counter( name=OTEL_SDK_LOG_CREATED, description="The number of logs submitted to enabled SDK Loggers.", unit="{log_record}", ) OTEL_SDK_METRIC_READER_COLLECTION_DURATION: Final = "otel.sdk.metric_reader.collection.duration" """ The duration of the collect operation of the metric reader Instrument: histogram Unit: s Note: For successful collections, `error.type` MUST NOT be set. For failed collections, `error.type` SHOULD contain the failure cause. It can happen that metrics collection is successful for some MetricProducers, while others fail. In that case `error.type` SHOULD be set to any of the failure causes. """ def create_otel_sdk_metric_reader_collection_duration(meter: Meter) -> Histogram: """The duration of the collect operation of the metric reader""" return meter.create_histogram( name=OTEL_SDK_METRIC_READER_COLLECTION_DURATION, description="The duration of the collect operation of the metric reader.", unit="s", ) OTEL_SDK_PROCESSOR_LOG_PROCESSED: Final = "otel.sdk.processor.log.processed" """ The number of log records for which the processing has finished, either successful or failed Instrument: counter Unit: {log_record} Note: For successful processing, `error.type` MUST NOT be set. For failed processing, `error.type` MUST contain the failure cause. SDK Batching Log Record Processors MUST use `queue_full` as the value of `error.type` for log records dropped due to a full queue. If a processor reports a log record dropped because it has already been shut down, `error.type` MUST be `already_shutdown`. Whether and when a processor drops such log records is governed by the SDK specification, not by this metric. For the SDK Simple and Batching Log Record Processors, a log record MUST be counted as successfully processed at the point the processor invokes the export operation. For batching processors, all log records in the batch passed to the exporter are counted at that point; log records accepted into the processor's queue but not yet passed to the exporter have not been processed. Implementations MUST NOT delay this count until the export operation concludes, and the outcome of the export operation, including an immediate failure of the invocation itself, MUST NOT affect this metric. Export outcomes are reported by `otel.sdk.exporter.log.exported`. """ def create_otel_sdk_processor_log_processed(meter: Meter) -> Counter: """The number of log records for which the processing has finished, either successful or failed""" return meter.create_counter( name=OTEL_SDK_PROCESSOR_LOG_PROCESSED, description="The number of log records for which the processing has finished, either successful or failed.", unit="{log_record}", ) OTEL_SDK_PROCESSOR_LOG_QUEUE_CAPACITY: Final = "otel.sdk.processor.log.queue.capacity" """ The maximum number of log records the queue of a given instance of an SDK Log Record processor can hold Instrument: updowncounter Unit: {log_record} Note: Only applies to Log Record processors which use a queue, e.g. the SDK Batching Log Record Processor. """ def create_otel_sdk_processor_log_queue_capacity(meter: Meter) -> UpDownCounter: """The maximum number of log records the queue of a given instance of an SDK Log Record processor can hold""" return meter.create_up_down_counter( name=OTEL_SDK_PROCESSOR_LOG_QUEUE_CAPACITY, description="The maximum number of log records the queue of a given instance of an SDK Log Record processor can hold.", unit="{log_record}", ) OTEL_SDK_PROCESSOR_LOG_QUEUE_SIZE: Final = "otel.sdk.processor.log.queue.size" """ The number of log records in the queue of a given instance of an SDK log processor Instrument: updowncounter Unit: {log_record} Note: Only applies to log record processors which use a queue, e.g. the SDK Batching Log Record Processor. """ def create_otel_sdk_processor_log_queue_size(meter: Meter) -> UpDownCounter: """The number of log records in the queue of a given instance of an SDK log processor""" return meter.create_up_down_counter( name=OTEL_SDK_PROCESSOR_LOG_QUEUE_SIZE, description="The number of log records in the queue of a given instance of an SDK log processor.", unit="{log_record}", ) OTEL_SDK_PROCESSOR_SPAN_PROCESSED: Final = "otel.sdk.processor.span.processed" """ The number of spans for which the processing has finished, either successful or failed Instrument: counter Unit: {span} Note: For successful processing, `error.type` MUST NOT be set. For failed processing, `error.type` MUST contain the failure cause. SDK Batching Span Processors MUST use `queue_full` as the value of `error.type` for spans dropped due to a full queue. If a processor reports a span dropped because it has already been shut down, `error.type` MUST be `already_shutdown`. Whether and when a processor drops such spans is governed by the SDK specification, not by this metric. For the SDK Simple and Batching Span Processors, a span MUST be counted as successfully processed at the point the processor invokes the export operation. For batching processors, all spans in the batch passed to the exporter are counted at that point; spans accepted into the processor's queue but not yet passed to the exporter have not been processed. Implementations MUST NOT delay this count until the export operation concludes, and the outcome of the export operation, including an immediate failure of the invocation itself, MUST NOT affect this metric. Export outcomes are reported by `otel.sdk.exporter.span.exported`. """ def create_otel_sdk_processor_span_processed(meter: Meter) -> Counter: """The number of spans for which the processing has finished, either successful or failed""" return meter.create_counter( name=OTEL_SDK_PROCESSOR_SPAN_PROCESSED, description="The number of spans for which the processing has finished, either successful or failed.", unit="{span}", ) OTEL_SDK_PROCESSOR_SPAN_PROCESSED_COUNT: Final = "otel.sdk.processor.span.processed.count" """ Deprecated: Replaced by `otel.sdk.processor.span.processed`. """ def create_otel_sdk_processor_span_processed_count(meter: Meter) -> UpDownCounter: """Deprecated, use `otel.sdk.processor.span.processed` instead""" return meter.create_up_down_counter( name=OTEL_SDK_PROCESSOR_SPAN_PROCESSED_COUNT, description="Deprecated, use `otel.sdk.processor.span.processed` instead.", unit="{span}", ) OTEL_SDK_PROCESSOR_SPAN_QUEUE_CAPACITY: Final = "otel.sdk.processor.span.queue.capacity" """ The maximum number of spans the queue of a given instance of an SDK span processor can hold Instrument: updowncounter Unit: {span} Note: Only applies to span processors which use a queue, e.g. the SDK Batching Span Processor. """ def create_otel_sdk_processor_span_queue_capacity(meter: Meter) -> UpDownCounter: """The maximum number of spans the queue of a given instance of an SDK span processor can hold""" return meter.create_up_down_counter( name=OTEL_SDK_PROCESSOR_SPAN_QUEUE_CAPACITY, description="The maximum number of spans the queue of a given instance of an SDK span processor can hold.", unit="{span}", ) OTEL_SDK_PROCESSOR_SPAN_QUEUE_SIZE: Final = "otel.sdk.processor.span.queue.size" """ The number of spans in the queue of a given instance of an SDK span processor Instrument: updowncounter Unit: {span} Note: Only applies to span processors which use a queue, e.g. the SDK Batching Span Processor. """ def create_otel_sdk_processor_span_queue_size(meter: Meter) -> UpDownCounter: """The number of spans in the queue of a given instance of an SDK span processor""" return meter.create_up_down_counter( name=OTEL_SDK_PROCESSOR_SPAN_QUEUE_SIZE, description="The number of spans in the queue of a given instance of an SDK span processor.", unit="{span}", ) OTEL_SDK_SPAN_ENDED: Final = "otel.sdk.span.ended" """ Deprecated: Obsoleted. """ def create_otel_sdk_span_ended(meter: Meter) -> Counter: """Use `otel.sdk.span.started` minus `otel.sdk.span.live` to derive this value""" return meter.create_counter( name=OTEL_SDK_SPAN_ENDED, description="Use `otel.sdk.span.started` minus `otel.sdk.span.live` to derive this value.", unit="{span}", ) OTEL_SDK_SPAN_ENDED_COUNT: Final = "otel.sdk.span.ended.count" """ Deprecated: Obsoleted. """ def create_otel_sdk_span_ended_count(meter: Meter) -> Counter: """Use `otel.sdk.span.started` minus `otel.sdk.span.live` to derive this value""" return meter.create_counter( name=OTEL_SDK_SPAN_ENDED_COUNT, description="Use `otel.sdk.span.started` minus `otel.sdk.span.live` to derive this value.", unit="{span}", ) OTEL_SDK_SPAN_LIVE: Final = "otel.sdk.span.live" """ The number of created spans with `recording=true` for which the end operation has not been called yet Instrument: updowncounter Unit: {span} Note: Non-recording spans are not counted, hence `otel.span.sampling_result` can only take values `RECORD_ONLY` and `RECORD_AND_SAMPLE`, not `DROP`. """ def create_otel_sdk_span_live(meter: Meter) -> UpDownCounter: """The number of created spans with `recording=true` for which the end operation has not been called yet""" return meter.create_up_down_counter( name=OTEL_SDK_SPAN_LIVE, description="The number of created spans with `recording=true` for which the end operation has not been called yet.", unit="{span}", ) OTEL_SDK_SPAN_LIVE_COUNT: Final = "otel.sdk.span.live.count" """ Deprecated: Replaced by `otel.sdk.span.live`. """ def create_otel_sdk_span_live_count(meter: Meter) -> UpDownCounter: """Deprecated, use `otel.sdk.span.live` instead""" return meter.create_up_down_counter( name=OTEL_SDK_SPAN_LIVE_COUNT, description="Deprecated, use `otel.sdk.span.live` instead.", unit="{span}", ) OTEL_SDK_SPAN_STARTED: Final = "otel.sdk.span.started" """ The number of created spans Instrument: counter Unit: {span} Note: Implementations MUST record this metric for all spans, even for non-recording ones. """ def create_otel_sdk_span_started(meter: Meter) -> Counter: """The number of created spans""" return meter.create_counter( name=OTEL_SDK_SPAN_STARTED, description="The number of created spans.", unit="{span}", ) process_metrics.py000066400000000000000000000176561527070070700416660ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/src/opentelemetry/semconv/_incubating/metrics# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from collections.abc import Callable, Generator, Iterable, Sequence from typing import Final from opentelemetry.metrics import CallbackOptions, Counter, Meter, ObservableGauge, Observation, UpDownCounter # pylint: disable=invalid-name CallbackT = Callable[[CallbackOptions], Iterable[Observation]] | Generator[Iterable[Observation], CallbackOptions, None] PROCESS_CONTEXT_SWITCHES: Final = "process.context_switches" """ Number of times the process has been context switched Instrument: counter Unit: {context_switch} """ def create_process_context_switches(meter: Meter) -> Counter: """Number of times the process has been context switched""" return meter.create_counter( name=PROCESS_CONTEXT_SWITCHES, description="Number of times the process has been context switched.", unit="{context_switch}", ) PROCESS_CPU_TIME: Final = "process.cpu.time" """ Total CPU seconds broken down by different CPU modes Instrument: counter Unit: s """ def create_process_cpu_time(meter: Meter) -> Counter: """Total CPU seconds broken down by different CPU modes""" return meter.create_counter( name=PROCESS_CPU_TIME, description="Total CPU seconds broken down by different CPU modes.", unit="s", ) PROCESS_CPU_UTILIZATION: Final = "process.cpu.utilization" """ Difference in process.cpu.time since the last measurement, divided by the elapsed time and number of CPUs available to the process Instrument: gauge Unit: 1 """ def create_process_cpu_utilization(meter: Meter, callbacks: Sequence[CallbackT] | None) -> ObservableGauge: """Difference in process.cpu.time since the last measurement, divided by the elapsed time and number of CPUs available to the process""" return meter.create_observable_gauge( name=PROCESS_CPU_UTILIZATION, callbacks=callbacks, description="Difference in process.cpu.time since the last measurement, divided by the elapsed time and number of CPUs available to the process.", unit="1", ) PROCESS_DISK_IO: Final = "process.disk.io" """ Disk bytes transferred Instrument: counter Unit: By """ def create_process_disk_io(meter: Meter) -> Counter: """Disk bytes transferred""" return meter.create_counter( name=PROCESS_DISK_IO, description="Disk bytes transferred.", unit="By", ) PROCESS_DISK_OPERATIONS: Final = "process.disk.operations" """ Number of disk operations performed by the process Instrument: counter Unit: {operation} """ def create_process_disk_operations(meter: Meter) -> Counter: """Number of disk operations performed by the process""" return meter.create_counter( name=PROCESS_DISK_OPERATIONS, description="Number of disk operations performed by the process.", unit="{operation}", ) PROCESS_MEMORY_USAGE: Final = "process.memory.usage" """ The amount of physical memory in use Instrument: updowncounter Unit: By """ def create_process_memory_usage(meter: Meter) -> UpDownCounter: """The amount of physical memory in use""" return meter.create_up_down_counter( name=PROCESS_MEMORY_USAGE, description="The amount of physical memory in use.", unit="By", ) PROCESS_MEMORY_UTILIZATION: Final = "process.memory.utilization" """ Percentage of total physical memory that is used by the process Instrument: gauge Unit: 1 """ def create_process_memory_utilization(meter: Meter, callbacks: Sequence[CallbackT] | None) -> ObservableGauge: """Percentage of total physical memory that is used by the process""" return meter.create_observable_gauge( name=PROCESS_MEMORY_UTILIZATION, callbacks=callbacks, description="Percentage of total physical memory that is used by the process.", unit="1", ) PROCESS_MEMORY_VIRTUAL: Final = "process.memory.virtual" """ The amount of committed virtual memory Instrument: updowncounter Unit: By """ def create_process_memory_virtual(meter: Meter) -> UpDownCounter: """The amount of committed virtual memory""" return meter.create_up_down_counter( name=PROCESS_MEMORY_VIRTUAL, description="The amount of committed virtual memory.", unit="By", ) PROCESS_NETWORK_IO: Final = "process.network.io" """ Network bytes transferred Instrument: counter Unit: By """ def create_process_network_io(meter: Meter) -> Counter: """Network bytes transferred""" return meter.create_counter( name=PROCESS_NETWORK_IO, description="Network bytes transferred.", unit="By", ) PROCESS_OPEN_FILE_DESCRIPTOR_COUNT: Final = "process.open_file_descriptor.count" """ Deprecated: Replaced by `process.unix.file_descriptor.count`. """ def create_process_open_file_descriptor_count(meter: Meter) -> UpDownCounter: """Deprecated, use `process.unix.file_descriptor.count` instead""" return meter.create_up_down_counter( name=PROCESS_OPEN_FILE_DESCRIPTOR_COUNT, description="Deprecated, use `process.unix.file_descriptor.count` instead.", unit="{file_descriptor}", ) PROCESS_PAGING_FAULTS: Final = "process.paging.faults" """ Number of page faults the process has made Instrument: counter Unit: {fault} """ def create_process_paging_faults(meter: Meter) -> Counter: """Number of page faults the process has made""" return meter.create_counter( name=PROCESS_PAGING_FAULTS, description="Number of page faults the process has made.", unit="{fault}", ) PROCESS_SIGNALS_PENDING: Final = "process.signals_pending" """ Number of pending signals for the process Instrument: updowncounter Unit: {signal} """ def create_process_signals_pending(meter: Meter) -> UpDownCounter: """Number of pending signals for the process""" return meter.create_up_down_counter( name=PROCESS_SIGNALS_PENDING, description="Number of pending signals for the process.", unit="{signal}", ) PROCESS_THREAD_COUNT: Final = "process.thread.count" """ Process threads count Instrument: updowncounter Unit: {thread} """ def create_process_thread_count(meter: Meter) -> UpDownCounter: """Process threads count""" return meter.create_up_down_counter( name=PROCESS_THREAD_COUNT, description="Process threads count.", unit="{thread}", ) PROCESS_UNIX_FILE_DESCRIPTOR_COUNT: Final = "process.unix.file_descriptor.count" """ Number of UNIX file descriptors in use by the process Instrument: updowncounter Unit: {file_descriptor} """ def create_process_unix_file_descriptor_count(meter: Meter) -> UpDownCounter: """Number of UNIX file descriptors in use by the process""" return meter.create_up_down_counter( name=PROCESS_UNIX_FILE_DESCRIPTOR_COUNT, description="Number of UNIX file descriptors in use by the process.", unit="{file_descriptor}", ) PROCESS_UPTIME: Final = "process.uptime" """ The time the process has been running Instrument: gauge Unit: s Note: Instrumentations SHOULD use a gauge with type `double` and measure uptime in seconds as a floating point number with the highest precision available. The actual accuracy would depend on the instrumentation and operating system. """ def create_process_uptime(meter: Meter, callbacks: Sequence[CallbackT] | None) -> ObservableGauge: """The time the process has been running""" return meter.create_observable_gauge( name=PROCESS_UPTIME, callbacks=callbacks, description="The time the process has been running.", unit="s", ) PROCESS_WINDOWS_HANDLE_COUNT: Final = "process.windows.handle.count" """ Number of handles held by the process Instrument: updowncounter Unit: {handle} """ def create_process_windows_handle_count(meter: Meter) -> UpDownCounter: """Number of handles held by the process""" return meter.create_up_down_counter( name=PROCESS_WINDOWS_HANDLE_COUNT, description="Number of handles held by the process.", unit="{handle}", ) rpc_metrics.py000066400000000000000000000134061527070070700407610ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/src/opentelemetry/semconv/_incubating/metrics# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from typing import Final from opentelemetry.metrics import Histogram, Meter RPC_CLIENT_CALL_DURATION: Final = "rpc.client.call.duration" """ Measures the duration of an outgoing Remote Procedure Call (RPC) Instrument: histogram Unit: s Note: When this metric is reported alongside an RPC client span, the metric value SHOULD be the same as the RPC client span duration. """ def create_rpc_client_call_duration(meter: Meter) -> Histogram: """Measures the duration of an outgoing Remote Procedure Call (RPC)""" return meter.create_histogram( name=RPC_CLIENT_CALL_DURATION, description="Measures the duration of an outgoing Remote Procedure Call (RPC).", unit="s", ) RPC_CLIENT_DURATION: Final = "rpc.client.duration" """ Deprecated: Replaced by `rpc.client.call.duration` with unit `s`. """ def create_rpc_client_duration(meter: Meter) -> Histogram: """Deprecated, use `rpc.client.call.duration` instead. Note: the unit also changed from `ms` to `s`""" return meter.create_histogram( name=RPC_CLIENT_DURATION, description="Deprecated, use `rpc.client.call.duration` instead. Note: the unit also changed from `ms` to `s`.", unit="ms", ) RPC_CLIENT_REQUEST_SIZE: Final = "rpc.client.request.size" """ Deprecated: Removed, no replacement at this time. """ def create_rpc_client_request_size(meter: Meter) -> Histogram: """Measures the size of RPC request messages (uncompressed)""" return meter.create_histogram( name=RPC_CLIENT_REQUEST_SIZE, description="Measures the size of RPC request messages (uncompressed).", unit="By", ) RPC_CLIENT_REQUESTS_PER_RPC: Final = "rpc.client.requests_per_rpc" """ Deprecated: Removed, no replacement at this time. """ def create_rpc_client_requests_per_rpc(meter: Meter) -> Histogram: """Measures the number of messages received per RPC""" return meter.create_histogram( name=RPC_CLIENT_REQUESTS_PER_RPC, description="Measures the number of messages received per RPC.", unit="{count}", ) RPC_CLIENT_RESPONSE_SIZE: Final = "rpc.client.response.size" """ Deprecated: Removed, no replacement at this time. """ def create_rpc_client_response_size(meter: Meter) -> Histogram: """Measures the size of RPC response messages (uncompressed)""" return meter.create_histogram( name=RPC_CLIENT_RESPONSE_SIZE, description="Measures the size of RPC response messages (uncompressed).", unit="By", ) RPC_CLIENT_RESPONSES_PER_RPC: Final = "rpc.client.responses_per_rpc" """ Deprecated: Removed, no replacement at this time. """ def create_rpc_client_responses_per_rpc(meter: Meter) -> Histogram: """Measures the number of messages sent per RPC""" return meter.create_histogram( name=RPC_CLIENT_RESPONSES_PER_RPC, description="Measures the number of messages sent per RPC.", unit="{count}", ) RPC_SERVER_CALL_DURATION: Final = "rpc.server.call.duration" """ Measures the duration of an incoming Remote Procedure Call (RPC) Instrument: histogram Unit: s Note: When this metric is reported alongside an RPC server span, the metric value SHOULD be the same as the RPC server span duration. """ def create_rpc_server_call_duration(meter: Meter) -> Histogram: """Measures the duration of an incoming Remote Procedure Call (RPC)""" return meter.create_histogram( name=RPC_SERVER_CALL_DURATION, description="Measures the duration of an incoming Remote Procedure Call (RPC).", unit="s", ) RPC_SERVER_DURATION: Final = "rpc.server.duration" """ Deprecated: Replaced by `rpc.server.call.duration` with unit `s`. """ def create_rpc_server_duration(meter: Meter) -> Histogram: """Deprecated, use `rpc.server.call.duration` instead. Note: the unit also changed from `ms` to `s`""" return meter.create_histogram( name=RPC_SERVER_DURATION, description="Deprecated, use `rpc.server.call.duration` instead. Note: the unit also changed from `ms` to `s`.", unit="ms", ) RPC_SERVER_REQUEST_SIZE: Final = "rpc.server.request.size" """ Deprecated: Removed, no replacement at this time. """ def create_rpc_server_request_size(meter: Meter) -> Histogram: """Measures the size of RPC request messages (uncompressed)""" return meter.create_histogram( name=RPC_SERVER_REQUEST_SIZE, description="Measures the size of RPC request messages (uncompressed).", unit="By", ) RPC_SERVER_REQUESTS_PER_RPC: Final = "rpc.server.requests_per_rpc" """ Deprecated: Removed, no replacement at this time. """ def create_rpc_server_requests_per_rpc(meter: Meter) -> Histogram: """Measures the number of messages received per RPC""" return meter.create_histogram( name=RPC_SERVER_REQUESTS_PER_RPC, description="Measures the number of messages received per RPC.", unit="{count}", ) RPC_SERVER_RESPONSE_SIZE: Final = "rpc.server.response.size" """ Deprecated: Removed, no replacement at this time. """ def create_rpc_server_response_size(meter: Meter) -> Histogram: """Measures the size of RPC response messages (uncompressed)""" return meter.create_histogram( name=RPC_SERVER_RESPONSE_SIZE, description="Measures the size of RPC response messages (uncompressed).", unit="By", ) RPC_SERVER_RESPONSES_PER_RPC: Final = "rpc.server.responses_per_rpc" """ Deprecated: Removed, no replacement at this time. """ def create_rpc_server_responses_per_rpc(meter: Meter) -> Histogram: """Measures the number of messages sent per RPC""" return meter.create_histogram( name=RPC_SERVER_RESPONSES_PER_RPC, description="Measures the number of messages sent per RPC.", unit="{count}", ) system_metrics.py000066400000000000000000000632121527070070700415210ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/src/opentelemetry/semconv/_incubating/metrics# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from collections.abc import Callable, Generator, Iterable, Sequence from typing import Final from opentelemetry.metrics import CallbackOptions, Counter, Meter, ObservableGauge, Observation, UpDownCounter # pylint: disable=invalid-name CallbackT = Callable[[CallbackOptions], Iterable[Observation]] | Generator[Iterable[Observation], CallbackOptions, None] SYSTEM_CPU_FREQUENCY: Final = "system.cpu.frequency" """ Operating frequency of the logical CPU in Hertz Instrument: gauge Unit: Hz """ def create_system_cpu_frequency(meter: Meter, callbacks: Sequence[CallbackT] | None) -> ObservableGauge: """Operating frequency of the logical CPU in Hertz""" return meter.create_observable_gauge( name=SYSTEM_CPU_FREQUENCY, callbacks=callbacks, description="Operating frequency of the logical CPU in Hertz.", unit="Hz", ) SYSTEM_CPU_LOGICAL_COUNT: Final = "system.cpu.logical.count" """ Reports the number of logical (virtual) processor cores created by the operating system to manage multitasking Instrument: updowncounter Unit: {cpu} Note: Calculated by multiplying the number of sockets by the number of cores per socket, and then by the number of threads per core. """ def create_system_cpu_logical_count(meter: Meter) -> UpDownCounter: """Reports the number of logical (virtual) processor cores created by the operating system to manage multitasking""" return meter.create_up_down_counter( name=SYSTEM_CPU_LOGICAL_COUNT, description="Reports the number of logical (virtual) processor cores created by the operating system to manage multitasking.", unit="{cpu}", ) SYSTEM_CPU_PHYSICAL_COUNT: Final = "system.cpu.physical.count" """ Reports the number of actual physical processor cores on the hardware Instrument: updowncounter Unit: {cpu} Note: Calculated by multiplying the number of sockets by the number of cores per socket. """ def create_system_cpu_physical_count(meter: Meter) -> UpDownCounter: """Reports the number of actual physical processor cores on the hardware""" return meter.create_up_down_counter( name=SYSTEM_CPU_PHYSICAL_COUNT, description="Reports the number of actual physical processor cores on the hardware.", unit="{cpu}", ) SYSTEM_CPU_TIME: Final = "system.cpu.time" """ Seconds each logical CPU spent on each mode Instrument: counter Unit: s """ def create_system_cpu_time(meter: Meter) -> Counter: """Seconds each logical CPU spent on each mode""" return meter.create_counter( name=SYSTEM_CPU_TIME, description="Seconds each logical CPU spent on each mode.", unit="s", ) SYSTEM_CPU_UTILIZATION: Final = "system.cpu.utilization" """ For each logical CPU, the utilization is calculated as the change in cumulative CPU time (cpu.time) over a measurement interval, divided by the elapsed time Instrument: gauge Unit: 1 """ def create_system_cpu_utilization(meter: Meter, callbacks: Sequence[CallbackT] | None) -> ObservableGauge: """For each logical CPU, the utilization is calculated as the change in cumulative CPU time (cpu.time) over a measurement interval, divided by the elapsed time""" return meter.create_observable_gauge( name=SYSTEM_CPU_UTILIZATION, callbacks=callbacks, description="For each logical CPU, the utilization is calculated as the change in cumulative CPU time (cpu.time) over a measurement interval, divided by the elapsed time.", unit="1", ) SYSTEM_DISK_IO: Final = "system.disk.io" """ Disk bytes transferred Instrument: counter Unit: By """ def create_system_disk_io(meter: Meter) -> Counter: """Disk bytes transferred""" return meter.create_counter( name=SYSTEM_DISK_IO, description="Disk bytes transferred.", unit="By", ) SYSTEM_DISK_IO_TIME: Final = "system.disk.io_time" """ Time disk spent activated Instrument: counter Unit: s Note: The real elapsed time ("wall clock") used in the I/O path (time from operations running in parallel are not counted). Measured as: - Linux: Field 13 from [procfs-diskstats](https://www.kernel.org/doc/Documentation/ABI/testing/procfs-diskstats) - Windows: The complement of ["Disk\\% Idle Time"](https://learn.microsoft.com/archive/blogs/askcore/windows-performance-monitor-disk-counters-explained#windows-performance-monitor-disk-counters-explained) performance counter: `uptime * (100 - "Disk\\% Idle Time") / 100`. """ def create_system_disk_io_time(meter: Meter) -> Counter: """Time disk spent activated""" return meter.create_counter( name=SYSTEM_DISK_IO_TIME, description="Time disk spent activated.", unit="s", ) SYSTEM_DISK_LIMIT: Final = "system.disk.limit" """ The total storage capacity of the disk Instrument: updowncounter Unit: By """ def create_system_disk_limit(meter: Meter) -> UpDownCounter: """The total storage capacity of the disk""" return meter.create_up_down_counter( name=SYSTEM_DISK_LIMIT, description="The total storage capacity of the disk.", unit="By", ) SYSTEM_DISK_MERGED: Final = "system.disk.merged" """ The number of disk reads/writes merged into single physical disk access operations Instrument: counter Unit: {operation} """ def create_system_disk_merged(meter: Meter) -> Counter: """The number of disk reads/writes merged into single physical disk access operations""" return meter.create_counter( name=SYSTEM_DISK_MERGED, description="The number of disk reads/writes merged into single physical disk access operations.", unit="{operation}", ) SYSTEM_DISK_OPERATION_TIME: Final = "system.disk.operation_time" """ Sum of the time each operation took to complete Instrument: counter Unit: s Note: Because it is the sum of time each request took, parallel-issued requests each contribute to make the count grow. Measured as: - Linux: Fields 7 & 11 from [procfs-diskstats](https://www.kernel.org/doc/Documentation/ABI/testing/procfs-diskstats) - Windows: "Avg. Disk sec/Read" perf counter multiplied by "Disk Reads/sec" perf counter (similar for Writes). """ def create_system_disk_operation_time(meter: Meter) -> Counter: """Sum of the time each operation took to complete""" return meter.create_counter( name=SYSTEM_DISK_OPERATION_TIME, description="Sum of the time each operation took to complete.", unit="s", ) SYSTEM_DISK_OPERATIONS: Final = "system.disk.operations" """ Disk operations count Instrument: counter Unit: {operation} """ def create_system_disk_operations(meter: Meter) -> Counter: """Disk operations count""" return meter.create_counter( name=SYSTEM_DISK_OPERATIONS, description="Disk operations count.", unit="{operation}", ) SYSTEM_FILESYSTEM_LIMIT: Final = "system.filesystem.limit" """ The total storage capacity of the filesystem Instrument: updowncounter Unit: By """ def create_system_filesystem_limit(meter: Meter) -> UpDownCounter: """The total storage capacity of the filesystem""" return meter.create_up_down_counter( name=SYSTEM_FILESYSTEM_LIMIT, description="The total storage capacity of the filesystem.", unit="By", ) SYSTEM_FILESYSTEM_LOCK_COUNT: Final = "system.filesystem.lock.count" """ Filesystem lock counts Instrument: updowncounter Unit: {lock} """ def create_system_filesystem_lock_count(meter: Meter) -> UpDownCounter: """Filesystem lock counts""" return meter.create_up_down_counter( name=SYSTEM_FILESYSTEM_LOCK_COUNT, description="Filesystem lock counts.", unit="{lock}", ) SYSTEM_FILESYSTEM_USAGE: Final = "system.filesystem.usage" """ Reports a filesystem's space usage across different states Instrument: updowncounter Unit: By Note: The sum of all `system.filesystem.usage` values over the different `system.filesystem.state` attributes SHOULD equal the total storage capacity of the filesystem, that is `system.filesystem.limit`. """ def create_system_filesystem_usage(meter: Meter) -> UpDownCounter: """Reports a filesystem's space usage across different states""" return meter.create_up_down_counter( name=SYSTEM_FILESYSTEM_USAGE, description="Reports a filesystem's space usage across different states.", unit="By", ) SYSTEM_FILESYSTEM_UTILIZATION: Final = "system.filesystem.utilization" """ Fraction of filesystem bytes used Instrument: gauge Unit: 1 """ def create_system_filesystem_utilization(meter: Meter, callbacks: Sequence[CallbackT] | None) -> ObservableGauge: """Fraction of filesystem bytes used""" return meter.create_observable_gauge( name=SYSTEM_FILESYSTEM_UTILIZATION, callbacks=callbacks, description="Fraction of filesystem bytes used.", unit="1", ) SYSTEM_LINUX_MEMORY_AVAILABLE: Final = "system.linux.memory.available" """ Deprecated: Replaced by `system.memory.linux.available`. """ def create_system_linux_memory_available(meter: Meter) -> Counter: """The number of packets transferred""" return meter.create_counter( name=SYSTEM_LINUX_MEMORY_AVAILABLE, description="The number of packets transferred.", unit="{packet}", ) SYSTEM_LINUX_MEMORY_SLAB_USAGE: Final = "system.linux.memory.slab.usage" """ Deprecated: Replaced by `system.memory.linux.slab.usage`. """ def create_system_linux_memory_slab_usage(meter: Meter) -> Counter: """The number of packets transferred""" return meter.create_counter( name=SYSTEM_LINUX_MEMORY_SLAB_USAGE, description="The number of packets transferred.", unit="{packet}", ) SYSTEM_MEMORY_LIMIT: Final = "system.memory.limit" """ Total virtual memory available in the system Instrument: updowncounter Unit: By """ def create_system_memory_limit(meter: Meter) -> UpDownCounter: """Total virtual memory available in the system""" return meter.create_up_down_counter( name=SYSTEM_MEMORY_LIMIT, description="Total virtual memory available in the system.", unit="By", ) SYSTEM_MEMORY_LINUX_AVAILABLE: Final = "system.memory.linux.available" """ An estimate of how much memory is available for starting new applications, without causing swapping Instrument: updowncounter Unit: By Note: This is an alternative to `system.memory.usage` metric with `state=free`. Linux starting from 3.14 exports "available" memory. It takes "free" memory as a baseline, and then factors in kernel-specific values. This is supposed to be more accurate than just "free" memory. For reference, see the calculations [here](https://superuser.com/a/980821). See also `MemAvailable` in [/proc/meminfo](https://man7.org/linux/man-pages/man5/proc.5.html). """ def create_system_memory_linux_available(meter: Meter) -> UpDownCounter: """An estimate of how much memory is available for starting new applications, without causing swapping""" return meter.create_up_down_counter( name=SYSTEM_MEMORY_LINUX_AVAILABLE, description="An estimate of how much memory is available for starting new applications, without causing swapping.", unit="By", ) SYSTEM_MEMORY_LINUX_HUGEPAGES_LIMIT: Final = "system.memory.linux.hugepages.limit" """ Total number of hugepages available Instrument: updowncounter Unit: {page} """ def create_system_memory_linux_hugepages_limit(meter: Meter) -> UpDownCounter: """Total number of hugepages available""" return meter.create_up_down_counter( name=SYSTEM_MEMORY_LINUX_HUGEPAGES_LIMIT, description="Total number of hugepages available.", unit="{page}", ) SYSTEM_MEMORY_LINUX_HUGEPAGES_PAGE_SIZE: Final = "system.memory.linux.hugepages.page_size" """ System hugepage size in bytes Instrument: updowncounter Unit: By """ def create_system_memory_linux_hugepages_page_size(meter: Meter) -> UpDownCounter: """System hugepage size in bytes""" return meter.create_up_down_counter( name=SYSTEM_MEMORY_LINUX_HUGEPAGES_PAGE_SIZE, description="System hugepage size in bytes.", unit="By", ) SYSTEM_MEMORY_LINUX_HUGEPAGES_RESERVED: Final = "system.memory.linux.hugepages.reserved" """ Number of reserved hugepages Instrument: updowncounter Unit: {page} Note: Hugepages for which a commitment to allocate has been made, but no allocation has yet been made. This is reported as a separate metric rather than a `usage` state because reserved pages are already counted in `free` pages. They represent a subset of free pages that cannot be used for non-reserved allocations. """ def create_system_memory_linux_hugepages_reserved(meter: Meter) -> UpDownCounter: """Number of reserved hugepages""" return meter.create_up_down_counter( name=SYSTEM_MEMORY_LINUX_HUGEPAGES_RESERVED, description="Number of reserved hugepages.", unit="{page}", ) SYSTEM_MEMORY_LINUX_HUGEPAGES_SURPLUS: Final = "system.memory.linux.hugepages.surplus" """ Number of surplus hugepages Instrument: updowncounter Unit: {page} Note: Overcommitted hugepages beyond the persistent pool. This is reported as a separate metric rather than a `usage` state because surplus pages can be in either `used` or `free` state. Including them in `usage` would break the convention that `usage` states sum to the `limit`. """ def create_system_memory_linux_hugepages_surplus(meter: Meter) -> UpDownCounter: """Number of surplus hugepages""" return meter.create_up_down_counter( name=SYSTEM_MEMORY_LINUX_HUGEPAGES_SURPLUS, description="Number of surplus hugepages.", unit="{page}", ) SYSTEM_MEMORY_LINUX_HUGEPAGES_USAGE: Final = "system.memory.linux.hugepages.usage" """ Number of hugepages in use by state Instrument: updowncounter Unit: {page} """ def create_system_memory_linux_hugepages_usage(meter: Meter) -> UpDownCounter: """Number of hugepages in use by state""" return meter.create_up_down_counter( name=SYSTEM_MEMORY_LINUX_HUGEPAGES_USAGE, description="Number of hugepages in use by state.", unit="{page}", ) SYSTEM_MEMORY_LINUX_HUGEPAGES_UTILIZATION: Final = "system.memory.linux.hugepages.utilization" """ Percentage of hugepages in use by state Instrument: gauge Unit: 1 """ def create_system_memory_linux_hugepages_utilization( meter: Meter, callbacks: Sequence[CallbackT] | None ) -> ObservableGauge: """Percentage of hugepages in use by state""" return meter.create_observable_gauge( name=SYSTEM_MEMORY_LINUX_HUGEPAGES_UTILIZATION, callbacks=callbacks, description="Percentage of hugepages in use by state.", unit="1", ) SYSTEM_MEMORY_LINUX_SHARED: Final = "system.memory.linux.shared" """ Shared memory used (mostly by tmpfs) Instrument: updowncounter Unit: By Note: Equivalent of `shared` from [`free` command](https://man7.org/linux/man-pages/man1/free.1.html) or `Shmem` from [`/proc/meminfo`](https://man7.org/linux/man-pages/man5/proc.5.html)". """ def create_system_memory_linux_shared(meter: Meter) -> UpDownCounter: """Shared memory used (mostly by tmpfs)""" return meter.create_up_down_counter( name=SYSTEM_MEMORY_LINUX_SHARED, description="Shared memory used (mostly by tmpfs).", unit="By", ) SYSTEM_MEMORY_LINUX_SLAB_USAGE: Final = "system.memory.linux.slab.usage" """ Reports the memory used by the Linux kernel for managing caches of frequently used objects Instrument: updowncounter Unit: By Note: The sum over the `reclaimable` and `unreclaimable` state values in `memory.linux.slab.usage` SHOULD be equal to the total slab memory available on the system. Note that the total slab memory is not constant and may vary over time. See also the [Slab allocator](https://blogs.oracle.com/linux/post/understanding-linux-kernel-memory-statistics) and `Slab` in [/proc/meminfo](https://man7.org/linux/man-pages/man5/proc.5.html). """ def create_system_memory_linux_slab_usage(meter: Meter) -> UpDownCounter: """Reports the memory used by the Linux kernel for managing caches of frequently used objects""" return meter.create_up_down_counter( name=SYSTEM_MEMORY_LINUX_SLAB_USAGE, description="Reports the memory used by the Linux kernel for managing caches of frequently used objects.", unit="By", ) SYSTEM_MEMORY_SHARED: Final = "system.memory.shared" """ Deprecated: Replaced by `system.memory.linux.shared`. """ def create_system_memory_shared(meter: Meter) -> UpDownCounter: """Deprecated, use `system.memory.linux.shared` instead""" return meter.create_up_down_counter( name=SYSTEM_MEMORY_SHARED, description="Deprecated, use `system.memory.linux.shared` instead.", unit="By", ) SYSTEM_MEMORY_USAGE: Final = "system.memory.usage" """ Reports memory in use by state Instrument: updowncounter Unit: By """ def create_system_memory_usage(meter: Meter) -> UpDownCounter: """Reports memory in use by state""" return meter.create_up_down_counter( name=SYSTEM_MEMORY_USAGE, description="Reports memory in use by state.", unit="By", ) SYSTEM_MEMORY_UTILIZATION: Final = "system.memory.utilization" """ Percentage of memory bytes in use Instrument: gauge Unit: 1 """ def create_system_memory_utilization(meter: Meter, callbacks: Sequence[CallbackT] | None) -> ObservableGauge: """Percentage of memory bytes in use""" return meter.create_observable_gauge( name=SYSTEM_MEMORY_UTILIZATION, callbacks=callbacks, description="Percentage of memory bytes in use.", unit="1", ) SYSTEM_NETWORK_CONNECTION_COUNT: Final = "system.network.connection.count" """ The number of connections Instrument: updowncounter Unit: {connection} """ def create_system_network_connection_count(meter: Meter) -> UpDownCounter: """The number of connections""" return meter.create_up_down_counter( name=SYSTEM_NETWORK_CONNECTION_COUNT, description="The number of connections.", unit="{connection}", ) SYSTEM_NETWORK_CONNECTIONS: Final = "system.network.connections" """ Deprecated: Replaced by `system.network.connection.count`. """ def create_system_network_connections(meter: Meter) -> UpDownCounter: """Deprecated, use `system.network.connection.count` instead""" return meter.create_up_down_counter( name=SYSTEM_NETWORK_CONNECTIONS, description="Deprecated, use `system.network.connection.count` instead.", unit="{connection}", ) SYSTEM_NETWORK_DROPPED: Final = "system.network.dropped" """ Deprecated: Replaced by `system.network.packet.dropped`. """ def create_system_network_dropped(meter: Meter) -> Counter: """Count of packets that are dropped or discarded even though there was no error""" return meter.create_counter( name=SYSTEM_NETWORK_DROPPED, description="Count of packets that are dropped or discarded even though there was no error.", unit="{packet}", ) SYSTEM_NETWORK_ERRORS: Final = "system.network.errors" """ Count of network errors detected Instrument: counter Unit: {error} Note: Measured as: - Linux: the `errs` column in `/proc/net/dev` ([source](https://web.archive.org/web/20180321091318/http://www.onlamp.com/pub/a/linux/2000/11/16/LinuxAdmin.html)). - Windows: [`InErrors`/`OutErrors`](https://docs.microsoft.com/windows/win32/api/netioapi/ns-netioapi-mib_if_row2) from [`GetIfEntry2`](https://docs.microsoft.com/windows/win32/api/netioapi/nf-netioapi-getifentry2). """ def create_system_network_errors(meter: Meter) -> Counter: """Count of network errors detected""" return meter.create_counter( name=SYSTEM_NETWORK_ERRORS, description="Count of network errors detected.", unit="{error}", ) SYSTEM_NETWORK_IO: Final = "system.network.io" """ The number of bytes transmitted and received Instrument: counter Unit: By """ def create_system_network_io(meter: Meter) -> Counter: """The number of bytes transmitted and received""" return meter.create_counter( name=SYSTEM_NETWORK_IO, description="The number of bytes transmitted and received.", unit="By", ) SYSTEM_NETWORK_PACKET_COUNT: Final = "system.network.packet.count" """ The number of packets transferred Instrument: counter Unit: {packet} """ def create_system_network_packet_count(meter: Meter) -> Counter: """The number of packets transferred""" return meter.create_counter( name=SYSTEM_NETWORK_PACKET_COUNT, description="The number of packets transferred.", unit="{packet}", ) SYSTEM_NETWORK_PACKET_DROPPED: Final = "system.network.packet.dropped" """ Count of packets that are dropped or discarded even though there was no error Instrument: counter Unit: {packet} Note: Measured as: - Linux: the `drop` column in `/proc/net/dev` ([source](https://web.archive.org/web/20180321091318/http://www.onlamp.com/pub/a/linux/2000/11/16/LinuxAdmin.html)) - Windows: [`InDiscards`/`OutDiscards`](https://docs.microsoft.com/windows/win32/api/netioapi/ns-netioapi-mib_if_row2) from [`GetIfEntry2`](https://docs.microsoft.com/windows/win32/api/netioapi/nf-netioapi-getifentry2). """ def create_system_network_packet_dropped(meter: Meter) -> Counter: """Count of packets that are dropped or discarded even though there was no error""" return meter.create_counter( name=SYSTEM_NETWORK_PACKET_DROPPED, description="Count of packets that are dropped or discarded even though there was no error.", unit="{packet}", ) SYSTEM_NETWORK_PACKETS: Final = "system.network.packets" """ Deprecated: Replaced by `system.network.packet.count`. """ def create_system_network_packets(meter: Meter) -> Counter: """The number of packets transferred""" return meter.create_counter( name=SYSTEM_NETWORK_PACKETS, description="The number of packets transferred.", unit="{packet}", ) SYSTEM_PAGING_FAULTS: Final = "system.paging.faults" """ The number of page faults Instrument: counter Unit: {fault} """ def create_system_paging_faults(meter: Meter) -> Counter: """The number of page faults""" return meter.create_counter( name=SYSTEM_PAGING_FAULTS, description="The number of page faults.", unit="{fault}", ) SYSTEM_PAGING_OPERATIONS: Final = "system.paging.operations" """ The number of paging operations Instrument: counter Unit: {operation} """ def create_system_paging_operations(meter: Meter) -> Counter: """The number of paging operations""" return meter.create_counter( name=SYSTEM_PAGING_OPERATIONS, description="The number of paging operations.", unit="{operation}", ) SYSTEM_PAGING_USAGE: Final = "system.paging.usage" """ UNIX swap or windows pagefile usage Instrument: updowncounter Unit: By """ def create_system_paging_usage(meter: Meter) -> UpDownCounter: """UNIX swap or windows pagefile usage""" return meter.create_up_down_counter( name=SYSTEM_PAGING_USAGE, description="UNIX swap or windows pagefile usage.", unit="By", ) SYSTEM_PAGING_UTILIZATION: Final = "system.paging.utilization" """ Swap (UNIX) or pagefile (windows) utilization Instrument: gauge Unit: 1 """ def create_system_paging_utilization(meter: Meter, callbacks: Sequence[CallbackT] | None) -> ObservableGauge: """Swap (UNIX) or pagefile (windows) utilization""" return meter.create_observable_gauge( name=SYSTEM_PAGING_UTILIZATION, callbacks=callbacks, description="Swap (UNIX) or pagefile (windows) utilization.", unit="1", ) SYSTEM_PROCESS_COUNT: Final = "system.process.count" """ Total number of processes in each state Instrument: updowncounter Unit: {process} """ def create_system_process_count(meter: Meter) -> UpDownCounter: """Total number of processes in each state""" return meter.create_up_down_counter( name=SYSTEM_PROCESS_COUNT, description="Total number of processes in each state.", unit="{process}", ) SYSTEM_PROCESS_CREATED: Final = "system.process.created" """ Total number of processes created over uptime of the host Instrument: counter Unit: {process} """ def create_system_process_created(meter: Meter) -> Counter: """Total number of processes created over uptime of the host""" return meter.create_counter( name=SYSTEM_PROCESS_CREATED, description="Total number of processes created over uptime of the host.", unit="{process}", ) SYSTEM_PROCESS_LIMIT: Final = "system.process.limit" """ The maximum number of concurrent processes/tasks allowed by the operating system Instrument: updowncounter Unit: {thread} Note: On Linux, this corresponds to `/proc/sys/kernel/pid_max` or `/proc/sys/kernel/threads-max`. A per-user process limit may also be retrieved via `getrlimit(RLIMIT_NPROC)`. On BSD-like systems, this corresponds to `sysctl kern.maxproc`. This metric is unsupported on Windows systems. """ def create_system_process_limit(meter: Meter) -> UpDownCounter: """The maximum number of concurrent processes/tasks allowed by the operating system""" return meter.create_up_down_counter( name=SYSTEM_PROCESS_LIMIT, description="The maximum number of concurrent processes/tasks allowed by the operating system.", unit="{thread}", ) SYSTEM_UPTIME: Final = "system.uptime" """ The time the system has been running Instrument: gauge Unit: s Note: Instrumentations SHOULD use a gauge with type `double` and measure uptime in seconds as a floating point number with the highest precision available. The actual accuracy would depend on the instrumentation and operating system. """ def create_system_uptime(meter: Meter, callbacks: Sequence[CallbackT] | None) -> ObservableGauge: """The time the system has been running""" return meter.create_observable_gauge( name=SYSTEM_UPTIME, callbacks=callbacks, description="The time the system has been running.", unit="s", ) vcs_metrics.py000066400000000000000000000165001527070070700407660ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/src/opentelemetry/semconv/_incubating/metrics# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from collections.abc import Callable, Generator, Iterable, Sequence from typing import Final from opentelemetry.metrics import CallbackOptions, Meter, ObservableGauge, Observation, UpDownCounter # pylint: disable=invalid-name CallbackT = Callable[[CallbackOptions], Iterable[Observation]] | Generator[Iterable[Observation], CallbackOptions, None] VCS_CHANGE_COUNT: Final = "vcs.change.count" """ The number of changes (pull requests/merge requests/changelists) in a repository, categorized by their state (e.g. open or merged) Instrument: updowncounter Unit: {change} """ def create_vcs_change_count(meter: Meter) -> UpDownCounter: """The number of changes (pull requests/merge requests/changelists) in a repository, categorized by their state (e.g. open or merged)""" return meter.create_up_down_counter( name=VCS_CHANGE_COUNT, description="The number of changes (pull requests/merge requests/changelists) in a repository, categorized by their state (e.g. open or merged).", unit="{change}", ) VCS_CHANGE_DURATION: Final = "vcs.change.duration" """ The time duration a change (pull request/merge request/changelist) has been in a given state Instrument: gauge Unit: s """ def create_vcs_change_duration(meter: Meter, callbacks: Sequence[CallbackT] | None) -> ObservableGauge: """The time duration a change (pull request/merge request/changelist) has been in a given state""" return meter.create_observable_gauge( name=VCS_CHANGE_DURATION, callbacks=callbacks, description="The time duration a change (pull request/merge request/changelist) has been in a given state.", unit="s", ) VCS_CHANGE_TIME_TO_APPROVAL: Final = "vcs.change.time_to_approval" """ The amount of time since its creation it took a change (pull request/merge request/changelist) to get the first approval Instrument: gauge Unit: s """ def create_vcs_change_time_to_approval(meter: Meter, callbacks: Sequence[CallbackT] | None) -> ObservableGauge: """The amount of time since its creation it took a change (pull request/merge request/changelist) to get the first approval""" return meter.create_observable_gauge( name=VCS_CHANGE_TIME_TO_APPROVAL, callbacks=callbacks, description="The amount of time since its creation it took a change (pull request/merge request/changelist) to get the first approval.", unit="s", ) VCS_CHANGE_TIME_TO_MERGE: Final = "vcs.change.time_to_merge" """ The amount of time since its creation it took a change (pull request/merge request/changelist) to get merged into the target(base) ref Instrument: gauge Unit: s """ def create_vcs_change_time_to_merge(meter: Meter, callbacks: Sequence[CallbackT] | None) -> ObservableGauge: """The amount of time since its creation it took a change (pull request/merge request/changelist) to get merged into the target(base) ref""" return meter.create_observable_gauge( name=VCS_CHANGE_TIME_TO_MERGE, callbacks=callbacks, description="The amount of time since its creation it took a change (pull request/merge request/changelist) to get merged into the target(base) ref.", unit="s", ) VCS_CONTRIBUTOR_COUNT: Final = "vcs.contributor.count" """ The number of unique contributors to a repository Instrument: gauge Unit: {contributor} """ def create_vcs_contributor_count(meter: Meter, callbacks: Sequence[CallbackT] | None) -> ObservableGauge: """The number of unique contributors to a repository""" return meter.create_observable_gauge( name=VCS_CONTRIBUTOR_COUNT, callbacks=callbacks, description="The number of unique contributors to a repository.", unit="{contributor}", ) VCS_REF_COUNT: Final = "vcs.ref.count" """ The number of refs of type branch or tag in a repository Instrument: updowncounter Unit: {ref} """ def create_vcs_ref_count(meter: Meter) -> UpDownCounter: """The number of refs of type branch or tag in a repository""" return meter.create_up_down_counter( name=VCS_REF_COUNT, description="The number of refs of type branch or tag in a repository.", unit="{ref}", ) VCS_REF_LINES_DELTA: Final = "vcs.ref.lines_delta" """ The number of lines added/removed in a ref (branch) relative to the ref from the `vcs.ref.base.name` attribute Instrument: gauge Unit: {line} Note: This metric should be reported for each `vcs.line_change.type` value. For example if a ref added 3 lines and removed 2 lines, instrumentation SHOULD report two measurements: 3 and 2 (both positive numbers). If number of lines added/removed should be calculated from the start of time, then `vcs.ref.base.name` SHOULD be set to an empty string. """ def create_vcs_ref_lines_delta(meter: Meter, callbacks: Sequence[CallbackT] | None) -> ObservableGauge: """The number of lines added/removed in a ref (branch) relative to the ref from the `vcs.ref.base.name` attribute""" return meter.create_observable_gauge( name=VCS_REF_LINES_DELTA, callbacks=callbacks, description="The number of lines added/removed in a ref (branch) relative to the ref from the `vcs.ref.base.name` attribute.", unit="{line}", ) VCS_REF_REVISIONS_DELTA: Final = "vcs.ref.revisions_delta" """ The number of revisions (commits) a ref (branch) is ahead/behind the branch from the `vcs.ref.base.name` attribute Instrument: gauge Unit: {revision} Note: This metric should be reported for each `vcs.revision_delta.direction` value. For example if branch `a` is 3 commits behind and 2 commits ahead of `trunk`, instrumentation SHOULD report two measurements: 3 and 2 (both positive numbers) and `vcs.ref.base.name` is set to `trunk`. """ def create_vcs_ref_revisions_delta(meter: Meter, callbacks: Sequence[CallbackT] | None) -> ObservableGauge: """The number of revisions (commits) a ref (branch) is ahead/behind the branch from the `vcs.ref.base.name` attribute""" return meter.create_observable_gauge( name=VCS_REF_REVISIONS_DELTA, callbacks=callbacks, description="The number of revisions (commits) a ref (branch) is ahead/behind the branch from the `vcs.ref.base.name` attribute.", unit="{revision}", ) VCS_REF_TIME: Final = "vcs.ref.time" """ Time a ref (branch) created from the default branch (trunk) has existed. The `ref.type` attribute will always be `branch` Instrument: gauge Unit: s """ def create_vcs_ref_time(meter: Meter, callbacks: Sequence[CallbackT] | None) -> ObservableGauge: """Time a ref (branch) created from the default branch (trunk) has existed. The `ref.type` attribute will always be `branch`""" return meter.create_observable_gauge( name=VCS_REF_TIME, callbacks=callbacks, description="Time a ref (branch) created from the default branch (trunk) has existed. The `ref.type` attribute will always be `branch`.", unit="s", ) VCS_REPOSITORY_COUNT: Final = "vcs.repository.count" """ The number of repositories in an organization Instrument: updowncounter Unit: {repository} """ def create_vcs_repository_count(meter: Meter) -> UpDownCounter: """The number of repositories in an organization""" return meter.create_up_down_counter( name=VCS_REPOSITORY_COUNT, description="The number of repositories in an organization.", unit="{repository}", ) python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/src/opentelemetry/semconv/attributes/000077500000000000000000000000001527070070700344065ustar00rootroot00000000000000__init__.py000066400000000000000000000000001527070070700364260ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/src/opentelemetry/semconv/attributesclient_attributes.py000066400000000000000000000013601527070070700404250ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/src/opentelemetry/semconv/attributes# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from typing import Final CLIENT_ADDRESS: Final = "client.address" """ Client address - domain name if available without reverse DNS lookup; otherwise, IP address or UNIX domain socket name. Note: When observed from the server side, and when communicating through an intermediary, `client.address` SHOULD represent the client address behind any intermediaries, for example proxies, if it's available. """ CLIENT_PORT: Final = "client.port" """ Client port number. Note: When observed from the server side, and when communicating through an intermediary, `client.port` SHOULD represent the client port behind any intermediaries, for example proxies, if it's available. """ code_attributes.py000066400000000000000000000054501527070070700400650ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/src/opentelemetry/semconv/attributes# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from typing import Final CODE_COLUMN_NUMBER: Final = "code.column.number" """ The column number in `code.file.path` best representing the operation. It SHOULD point within the code unit named in `code.function.name`. This attribute MUST NOT be used on the Profile signal since the data is already captured in 'message Line'. This constraint is imposed to prevent redundancy and maintain data integrity. """ CODE_FILE_PATH: Final = "code.file.path" """ The source code filename that identifies the code unit as uniquely as possible (preferably an absolute file path). This attribute MUST NOT be used on the Profile signal since the data is already captured in 'message Function'. This constraint is imposed to prevent redundancy and maintain data integrity. """ CODE_FUNCTION_NAME: Final = "code.function.name" """ The method or function fully-qualified name without arguments. The value should fit the natural representation of the language runtime, which is also likely the same used within `code.stacktrace` attribute value. This attribute MUST NOT be used on the Profile signal since the data is already captured in 'message Function'. This constraint is imposed to prevent redundancy and maintain data integrity. Note: Values and format depends on each language runtime, thus it is impossible to provide an exhaustive list of examples. The values are usually the same (or prefixes of) the ones found in native stack trace representation stored in `code.stacktrace` without information on arguments. Examples: * Java method: `com.example.MyHttpService.serveRequest` * Java anonymous class method: `com.mycompany.Main$1.myMethod` * Java lambda method: `com.mycompany.Main$$Lambda/0x0000748ae4149c00.myMethod` * PHP function: `GuzzleHttp\\Client::transfer` * Go function: `github.com/my/repo/pkg.foo.func5` * Elixir: `OpenTelemetry.Ctx.new` * Erlang: `opentelemetry_ctx:new` * Rust: `playground::my_module::my_cool_func` * C function: `fopen`. """ CODE_LINE_NUMBER: Final = "code.line.number" """ The line number in `code.file.path` best representing the operation. It SHOULD point within the code unit named in `code.function.name`. This attribute MUST NOT be used on the Profile signal since the data is already captured in 'message Line'. This constraint is imposed to prevent redundancy and maintain data integrity. """ CODE_STACKTRACE: Final = "code.stacktrace" """ A stacktrace as a string in the natural representation for the language runtime. The representation is identical to [`exception.stacktrace`](/docs/exceptions/exceptions-spans.md#stacktrace-representation). This attribute MUST NOT be used on the Profile signal since the data is already captured in 'message Location'. This constraint is imposed to prevent redundancy and maintain data integrity. """ container_attributes.py000066400000000000000000000023231527070070700411310ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/src/opentelemetry/semconv/attributes# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from typing import Final CONTAINER_ID: Final = "container.id" """ Container ID. Usually a UUID, as for example used to [identify Docker containers](https://docs.docker.com/engine/containers/run/#container-identification). The UUID might be abbreviated. """ CONTAINER_IMAGE_NAME: Final = "container.image.name" """ Name of the image the container was built on. """ CONTAINER_IMAGE_REPO_DIGESTS: Final = "container.image.repo_digests" """ Repository digests of the container image as provided by the container runtime. Note: [Docker](https://docs.docker.com/reference/api/engine/version/v1.52/#tag/Image/operation/ImageInspect) and [CRI](https://github.com/kubernetes/cri-api/blob/c75ef5b473bbe2d0a4fc92f82235efd665ea8e9f/pkg/apis/runtime/v1/api.proto#L1237-L1238) report those under the `RepoDigests` field. """ CONTAINER_IMAGE_TAGS: Final = "container.image.tags" """ Container image tags. An example can be found in [Docker Image Inspect](https://docs.docker.com/reference/api/engine/version/v1.52/#tag/Image/operation/ImageInspect). Should be only the `` section of the full name for example from `registry.example.com/my-org/my-image:`. """ db_attributes.py000066400000000000000000000150161527070070700375370ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/src/opentelemetry/semconv/attributes# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from enum import Enum from typing import Final DB_COLLECTION_NAME: Final = "db.collection.name" """ The name of a collection (table, container) within the database. Note: It is RECOMMENDED to capture the value as provided by the application without attempting to do any case normalization. The collection name SHOULD NOT be extracted from `db.query.text`, when the database system supports query text with multiple collections in non-batch operations. For batch operations, if the individual operations are known to have the same collection name then that collection name SHOULD be used. """ DB_NAMESPACE: Final = "db.namespace" """ The name of the database, fully qualified within the server address and port. Note: If a database system has multiple namespace components, they SHOULD be concatenated from the most general to the most specific namespace component, using `|` as a separator between the components. Any missing components (and their associated separators) SHOULD be omitted. Semantic conventions for individual database systems SHOULD document what `db.namespace` means in the context of that system. It is RECOMMENDED to capture the value as provided by the application without attempting to do any case normalization. """ DB_OPERATION_BATCH_SIZE: Final = "db.operation.batch.size" """ The number of database operations included in a batch operation. Note: Except for empty batch requests described below, a batch operation contains two or more database operations explicitly submitted as separate operations in a single client call, protocol message, or database command. Requests to batch APIs that contain only one operation SHOULD be modeled as single operations, not as batch operations. A database call is not a batch operation solely because one operation accepts multiple operands, such as keys, rows, documents, points, or other data elements, including Redis [`MGET`](https://redis.io/docs/latest/commands/mget/) with multiple keys. In batch APIs that execute the same parameterized operation with parameter sets, each parameter set represents one database operation for determining whether the request is a batch operation. Requests with only one parameter set SHOULD be modeled as single operations, not as batch operations. `db.operation.batch.size` SHOULD be set to the number of operations in the batch. It SHOULD NOT be set for non-batch operations. A request to execute a batch operation with no operations SHOULD also be treated as a batch operation, and `db.operation.batch.size` SHOULD be set to `0`. """ DB_OPERATION_NAME: Final = "db.operation.name" """ The name of the operation or command being executed. Note: It is RECOMMENDED to capture the value as provided by the application without attempting to do any case normalization. The operation name SHOULD NOT be extracted from `db.query.text`, when the database system supports query text with multiple operations in non-batch operations. If spaces can occur in the operation name, multiple consecutive spaces SHOULD be normalized to a single space. For batch operations, if the individual operations are known to have the same operation name then that operation name SHOULD be used prepended by `BATCH `, otherwise `db.operation.name` SHOULD be `BATCH` or some other database system specific term if more applicable. """ DB_QUERY_SUMMARY: Final = "db.query.summary" """ Low cardinality summary of a database query. Note: The query summary describes a class of database queries and is useful as a grouping key, especially when analyzing telemetry for database calls involving complex queries. Summary may be available to the instrumentation through instrumentation hooks or other means. If it is not available, instrumentations that support query parsing SHOULD generate a summary following [Generating query summary](/docs/db/database-spans.md#generating-a-summary-of-the-query) section. For batch operations, if the individual operations are known to have the same query summary then that query summary SHOULD be used prepended by `BATCH `, otherwise `db.query.summary` SHOULD be `BATCH` or some other database system specific term if more applicable. """ DB_QUERY_TEXT: Final = "db.query.text" """ The database query being executed. Note: For sanitization see [Sanitization of `db.query.text`](/docs/db/database-spans.md#sanitization-of-dbquerytext). For batch operations, if the individual operations are known to have the same query text then that query text SHOULD be used, otherwise all of the individual query texts SHOULD be concatenated with separator `; ` or some other database system specific separator if more applicable. Parameterized query text SHOULD NOT be sanitized. Even though parameterized query text can potentially have sensitive data, by using a parameterized query the user is giving a strong signal that any sensitive data will be passed as parameter values, and the benefit to observability of capturing the static part of the query text by default outweighs the risk. """ DB_RESPONSE_STATUS_CODE: Final = "db.response.status_code" """ Database response status code. Note: The status code returned by the database. Usually it represents an error code, but may also represent partial success, warning, or differentiate between various types of successful outcomes. Semantic conventions for individual database systems SHOULD document what `db.response.status_code` means in the context of that system. """ DB_STORED_PROCEDURE_NAME: Final = "db.stored_procedure.name" """ The name of a stored procedure within the database. Note: It is RECOMMENDED to capture the value as provided by the application without attempting to do any case normalization. For batch operations, if the individual operations are known to have the same stored procedure name then that stored procedure name SHOULD be used. """ DB_SYSTEM_NAME: Final = "db.system.name" """ The database management system (DBMS) product as identified by the client instrumentation. Note: The actual DBMS may differ from the one identified by the client. For example, when using PostgreSQL client libraries to connect to a CockroachDB, the `db.system.name` is set to `postgresql` based on the instrumentation's best knowledge. """ class DbSystemNameValues(Enum): MARIADB = "mariadb" """[MariaDB](https://mariadb.org/).""" MICROSOFT_SQL_SERVER = "microsoft.sql_server" """[Microsoft SQL Server](https://www.microsoft.com/sql-server).""" MYSQL = "mysql" """[MySQL](https://www.mysql.com/).""" POSTGRESQL = "postgresql" """[PostgreSQL](https://www.postgresql.org/).""" deployment_attributes.py000066400000000000000000000020171527070070700413270ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/src/opentelemetry/semconv/attributes# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from enum import Enum from typing import Final DEPLOYMENT_ENVIRONMENT_NAME: Final = "deployment.environment.name" """ Name of the [deployment environment](https://wikipedia.org/wiki/Deployment_environment) (aka deployment tier). Note: `deployment.environment.name` does not affect the uniqueness constraints defined through the `service.namespace`, `service.name` and `service.instance.id` resource attributes. This implies that resources carrying the following attribute combinations MUST be considered to be identifying the same service: - `service.name=frontend`, `deployment.environment.name=production` - `service.name=frontend`, `deployment.environment.name=staging`. """ class DeploymentEnvironmentNameValues(Enum): PRODUCTION = "production" """Production environment.""" STAGING = "staging" """Staging environment.""" TEST = "test" """Testing environment.""" DEVELOPMENT = "development" """Development environment.""" error_attributes.py000066400000000000000000000031171527070070700403020ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/src/opentelemetry/semconv/attributes# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from enum import Enum from typing import Final ERROR_TYPE: Final = "error.type" """ Describes a class of error the operation ended with. Note: The `error.type` SHOULD be predictable, and SHOULD have low cardinality. When `error.type` is set to a type (e.g., an exception type), its canonical class name identifying the type within the artifact SHOULD be used. If the recorded error type is a wrapper that is not meaningful for failure classification, instrumentation MAY use the type of the inner error instead. For example, in Go, errors created with `fmt.Errorf` using `%w` MAY be unwrapped when the wrapper type does not help classify the failure. Instrumentations SHOULD document the list of errors they report. The cardinality of `error.type` within one instrumentation library SHOULD be low. Telemetry consumers that aggregate data from multiple instrumentation libraries and applications should be prepared for `error.type` to have high cardinality at query time when no additional filters are applied. If the operation has completed successfully, instrumentations SHOULD NOT set `error.type`. If a specific domain defines its own set of error identifiers (such as HTTP or RPC status codes), it's RECOMMENDED to: - Use a domain-specific attribute - Set `error.type` to capture all errors, regardless of whether they are defined within the domain-specific set or not. """ class ErrorTypeValues(Enum): OTHER = "_OTHER" """A fallback error value to be used when the instrumentation doesn't define a custom value.""" exception_attributes.py000066400000000000000000000022341527070070700411460ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/src/opentelemetry/semconv/attributes# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from typing import Final EXCEPTION_ESCAPED: Final = "exception.escaped" """ Deprecated: It's no longer recommended to record exceptions that are handled and do not escape the scope of a span. """ EXCEPTION_MESSAGE: Final = "exception.message" """ The exception message. Note: > [!WARNING] > > This attribute may contain sensitive information. """ EXCEPTION_STACKTRACE: Final = "exception.stacktrace" """ A stacktrace as a string in the natural representation for the language runtime. The representation is to be determined and documented by each language SIG. """ EXCEPTION_TYPE: Final = "exception.type" """ The type of the exception (its fully-qualified class name, if applicable). The dynamic type of the exception should be preferred over the static type in languages that support it. Note: If the recorded exception type is a wrapper that is not meaningful for failure classification, instrumentation MAY use the type of the inner exception instead. For example, in Go, errors created with `fmt.Errorf` using `%w` MAY be unwrapped when the wrapper type does not help classify the failure. """ http_attributes.py000066400000000000000000000151171527070070700401330ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/src/opentelemetry/semconv/attributes# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from enum import Enum from typing import Final HTTP_REQUEST_HEADER_TEMPLATE: Final = "http.request.header" """ HTTP request headers, `` being the normalized HTTP Header name (lowercase), the value being the header values. Note: Instrumentations SHOULD require an explicit configuration of which headers are to be captured. Including all request headers can be a security risk - explicit configuration helps avoid leaking sensitive information. The `User-Agent` header is already captured in the `user_agent.original` attribute. Users MAY explicitly configure instrumentations to capture them even though it is not recommended. The attribute value MUST consist of either multiple header values as an array of strings or a single-item array containing a possibly comma-concatenated string, depending on the way the HTTP library provides access to headers. Examples: - A header `Content-Type: application/json` SHOULD be recorded as the `http.request.header.content-type` attribute with value `["application/json"]`. - A header `X-Forwarded-For: 1.2.3.4, 1.2.3.5` SHOULD be recorded as the `http.request.header.x-forwarded-for` attribute with value `["1.2.3.4", "1.2.3.5"]` or `["1.2.3.4, 1.2.3.5"]` depending on the HTTP library. """ HTTP_REQUEST_METHOD: Final = "http.request.method" """ HTTP request method. Note: HTTP request method value SHOULD be "known" to the instrumentation. By default, this convention defines "known" methods as the ones listed in [RFC9110](https://www.rfc-editor.org/rfc/rfc9110.html#name-methods), the PATCH method defined in [RFC5789](https://www.rfc-editor.org/rfc/rfc5789.html) and the QUERY method defined in [httpbis-safe-method-w-body](https://datatracker.ietf.org/doc/draft-ietf-httpbis-safe-method-w-body/?include_text=1). If the HTTP request method is not known to instrumentation, it MUST set the `http.request.method` attribute to `_OTHER`. If the HTTP instrumentation could end up converting valid HTTP request methods to `_OTHER`, then it MUST provide a way to override the list of known HTTP methods. If this override is done via environment variable, then the environment variable MUST be named OTEL_INSTRUMENTATION_HTTP_KNOWN_METHODS and support a comma-separated list of case-sensitive known HTTP methods. ![Development](https://img.shields.io/badge/-development-blue) If this override is done via declarative configuration, then the list MUST be configurable via the `known_methods` property (an array of case-sensitive strings with minimum items 0) under `.instrumentation/development.general.http.client` and/or `.instrumentation/development.general.http.server`. In either case, this list MUST be a full override of the default known methods, it is not a list of known methods in addition to the defaults. HTTP method names are case-sensitive and `http.request.method` attribute value MUST match a known HTTP method name exactly. Instrumentations for specific web frameworks that consider HTTP methods to be case insensitive, SHOULD populate a canonical equivalent. Tracing instrumentations that do so, MUST also set `http.request.method_original` to the original value. """ HTTP_REQUEST_METHOD_ORIGINAL: Final = "http.request.method_original" """ Original HTTP method sent by the client in the request line. """ HTTP_REQUEST_RESEND_COUNT: Final = "http.request.resend_count" """ The ordinal number of request resending attempt (for any reason, including redirects). Note: The resend count SHOULD be updated each time an HTTP request gets resent by the client, regardless of what was the cause of the resending (e.g. redirection, authorization failure, 503 Server Unavailable, network issues, or any other). """ HTTP_RESPONSE_HEADER_TEMPLATE: Final = "http.response.header" """ HTTP response headers, `` being the normalized HTTP Header name (lowercase), the value being the header values. Note: Instrumentations SHOULD require an explicit configuration of which headers are to be captured. Including all response headers can be a security risk - explicit configuration helps avoid leaking sensitive information. Users MAY explicitly configure instrumentations to capture them even though it is not recommended. The attribute value MUST consist of either multiple header values as an array of strings or a single-item array containing a possibly comma-concatenated string, depending on the way the HTTP library provides access to headers. Examples: - A header `Content-Type: application/json` header SHOULD be recorded as the `http.request.response.content-type` attribute with value `["application/json"]`. - A header `My-custom-header: abc, def` header SHOULD be recorded as the `http.response.header.my-custom-header` attribute with value `["abc", "def"]` or `["abc, def"]` depending on the HTTP library. """ HTTP_RESPONSE_STATUS_CODE: Final = "http.response.status_code" """ [HTTP response status code](https://tools.ietf.org/html/rfc7231#section-6). """ HTTP_ROUTE: Final = "http.route" """ The matched route template for the request. This MUST be low-cardinality and include all static path segments, with dynamic path segments represented with placeholders. Note: MUST NOT be populated when this is not supported by the HTTP server framework as the route attribute should have low-cardinality and the URI path can NOT substitute it. SHOULD include the [application root](/docs/http/http-spans.md#http-server-definitions) if there is one. A static path segment is a part of the route template with a fixed, low-cardinality value. This includes literal strings like `/users/` and placeholders that are constrained to a finite, predefined set of values, e.g. `{controller}` or `{action}`. A dynamic path segment is a placeholder for a value that can have high cardinality and is not constrained to a predefined list like static path segments. Instrumentations SHOULD use routing information provided by the corresponding web framework. They SHOULD pick the most precise source of routing information and MAY support custom route formatting. Instrumentations SHOULD document the format and the API used to obtain the route string. """ class HttpRequestMethodValues(Enum): CONNECT = "CONNECT" """CONNECT method.""" DELETE = "DELETE" """DELETE method.""" GET = "GET" """GET method.""" HEAD = "HEAD" """HEAD method.""" OPTIONS = "OPTIONS" """OPTIONS method.""" PATCH = "PATCH" """PATCH method.""" POST = "POST" """POST method.""" PUT = "PUT" """PUT method.""" TRACE = "TRACE" """TRACE method.""" OTHER = "_OTHER" """Any HTTP method that the instrumentation has no prior knowledge of.""" k8s_attributes.py000066400000000000000000000321701527070070700376570ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/src/opentelemetry/semconv/attributes# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from typing import Final K8S_CLUSTER_NAME: Final = "k8s.cluster.name" """ The name of the cluster. """ K8S_CLUSTER_UID: Final = "k8s.cluster.uid" """ A pseudo-ID for the cluster, set to the UID of the `kube-system` namespace. Note: K8s doesn't have support for obtaining a cluster ID. If this is ever added, we will recommend collecting the `k8s.cluster.uid` through the official APIs. In the meantime, we are able to use the `uid` of the `kube-system` namespace as a proxy for cluster ID. Read on for the rationale. Every object created in a K8s cluster is assigned a distinct UID. The `kube-system` namespace is used by Kubernetes itself and will exist for the lifetime of the cluster. Using the `uid` of the `kube-system` namespace is a reasonable proxy for the K8s ClusterID as it will only change if the cluster is rebuilt. Furthermore, Kubernetes UIDs are UUIDs as standardized by [ISO/IEC 9834-8 and ITU-T X.667](https://www.itu.int/ITU-T/studygroups/com17/oid.html). Which states: > If generated according to one of the mechanisms defined in Rec. > ITU-T X.667 | ISO/IEC 9834-8, a UUID is either guaranteed to be > different from all other UUIDs generated before 3603 A.D., or is > extremely likely to be different (depending on the mechanism chosen). Therefore, UIDs between clusters should be extremely unlikely to conflict. """ K8S_CONTAINER_NAME: Final = "k8s.container.name" """ The name of the Container from Pod specification, must be unique within a Pod. Container runtime usually uses different globally unique name (`container.name`). """ K8S_CONTAINER_RESTART_COUNT: Final = "k8s.container.restart_count" """ Number of times the container was restarted. This attribute can be used to identify a particular container (running or stopped) within a container spec. """ K8S_CRONJOB_ANNOTATION_TEMPLATE: Final = "k8s.cronjob.annotation" """ The cronjob annotation placed on the CronJob, the `` being the annotation name, the value being the annotation value. Note: Examples: - An annotation `retries` with value `4` SHOULD be recorded as the `k8s.cronjob.annotation.retries` attribute with value `"4"`. - An annotation `data` with empty string value SHOULD be recorded as the `k8s.cronjob.annotation.data` attribute with value `""`. """ K8S_CRONJOB_LABEL_TEMPLATE: Final = "k8s.cronjob.label" """ The label placed on the CronJob, the `` being the label name, the value being the label value. Note: Examples: - A label `type` with value `weekly` SHOULD be recorded as the `k8s.cronjob.label.type` attribute with value `"weekly"`. - A label `automated` with empty string value SHOULD be recorded as the `k8s.cronjob.label.automated` attribute with value `""`. """ K8S_CRONJOB_NAME: Final = "k8s.cronjob.name" """ The name of the CronJob. """ K8S_CRONJOB_UID: Final = "k8s.cronjob.uid" """ The UID of the CronJob. """ K8S_DAEMONSET_ANNOTATION_TEMPLATE: Final = "k8s.daemonset.annotation" """ The annotation placed on the DaemonSet, the `` being the annotation name, the value being the annotation value, even if the value is empty. Note: Examples: - An annotation `replicas` with value `1` SHOULD be recorded as the `k8s.daemonset.annotation.replicas` attribute with value `"1"`. - An annotation `data` with empty string value SHOULD be recorded as the `k8s.daemonset.annotation.data` attribute with value `""`. """ K8S_DAEMONSET_LABEL_TEMPLATE: Final = "k8s.daemonset.label" """ The label placed on the DaemonSet, the `` being the label name, the value being the label value, even if the value is empty. Note: Examples: - A label `app` with value `guestbook` SHOULD be recorded as the `k8s.daemonset.label.app` attribute with value `"guestbook"`. - A label `injected` with empty string value SHOULD be recorded as the `k8s.daemonset.label.injected` attribute with value `""`. """ K8S_DAEMONSET_NAME: Final = "k8s.daemonset.name" """ The name of the DaemonSet. """ K8S_DAEMONSET_UID: Final = "k8s.daemonset.uid" """ The UID of the DaemonSet. """ K8S_DEPLOYMENT_ANNOTATION_TEMPLATE: Final = "k8s.deployment.annotation" """ The annotation placed on the Deployment, the `` being the annotation name, the value being the annotation value, even if the value is empty. Note: Examples: - An annotation `replicas` with value `1` SHOULD be recorded as the `k8s.deployment.annotation.replicas` attribute with value `"1"`. - An annotation `data` with empty string value SHOULD be recorded as the `k8s.deployment.annotation.data` attribute with value `""`. """ K8S_DEPLOYMENT_LABEL_TEMPLATE: Final = "k8s.deployment.label" """ The label placed on the Deployment, the `` being the label name, the value being the label value, even if the value is empty. Note: Examples: - A label `app` with value `guestbook` SHOULD be recorded as the `k8s.deployment.label.app` attribute with value `"guestbook"`. - A label `injected` with empty string value SHOULD be recorded as the `k8s.deployment.label.injected` attribute with value `""`. """ K8S_DEPLOYMENT_NAME: Final = "k8s.deployment.name" """ The name of the Deployment. """ K8S_DEPLOYMENT_UID: Final = "k8s.deployment.uid" """ The UID of the Deployment. """ K8S_JOB_ANNOTATION_TEMPLATE: Final = "k8s.job.annotation" """ The annotation placed on the Job, the `` being the annotation name, the value being the annotation value, even if the value is empty. Note: Examples: - An annotation `number` with value `1` SHOULD be recorded as the `k8s.job.annotation.number` attribute with value `"1"`. - An annotation `data` with empty string value SHOULD be recorded as the `k8s.job.annotation.data` attribute with value `""`. """ K8S_JOB_LABEL_TEMPLATE: Final = "k8s.job.label" """ The label placed on the Job, the `` being the label name, the value being the label value, even if the value is empty. Note: Examples: - A label `jobtype` with value `ci` SHOULD be recorded as the `k8s.job.label.jobtype` attribute with value `"ci"`. - A label `automated` with empty string value SHOULD be recorded as the `k8s.job.label.automated` attribute with value `""`. """ K8S_JOB_NAME: Final = "k8s.job.name" """ The name of the Job. """ K8S_JOB_UID: Final = "k8s.job.uid" """ The UID of the Job. """ K8S_NAMESPACE_ANNOTATION_TEMPLATE: Final = "k8s.namespace.annotation" """ The annotation placed on the Namespace, the `` being the annotation name, the value being the annotation value, even if the value is empty. Note: Examples: - An annotation `ttl` with value `0` SHOULD be recorded as the `k8s.namespace.annotation.ttl` attribute with value `"0"`. - An annotation `data` with empty string value SHOULD be recorded as the `k8s.namespace.annotation.data` attribute with value `""`. """ K8S_NAMESPACE_LABEL_TEMPLATE: Final = "k8s.namespace.label" """ The label placed on the Namespace, the `` being the label name, the value being the label value, even if the value is empty. Note: Examples: - A label `kubernetes.io/metadata.name` with value `default` SHOULD be recorded as the `k8s.namespace.label.kubernetes.io/metadata.name` attribute with value `"default"`. - A label `data` with empty string value SHOULD be recorded as the `k8s.namespace.label.data` attribute with value `""`. """ K8S_NAMESPACE_NAME: Final = "k8s.namespace.name" """ The name of the namespace that the pod is running in. """ K8S_NODE_ANNOTATION_TEMPLATE: Final = "k8s.node.annotation" """ The annotation placed on the Node, the `` being the annotation name, the value being the annotation value, even if the value is empty. Note: Examples: - An annotation `node.alpha.kubernetes.io/ttl` with value `0` SHOULD be recorded as the `k8s.node.annotation.node.alpha.kubernetes.io/ttl` attribute with value `"0"`. - An annotation `data` with empty string value SHOULD be recorded as the `k8s.node.annotation.data` attribute with value `""`. """ K8S_NODE_LABEL_TEMPLATE: Final = "k8s.node.label" """ The label placed on the Node, the `` being the label name, the value being the label value, even if the value is empty. Note: Examples: - A label `kubernetes.io/arch` with value `arm64` SHOULD be recorded as the `k8s.node.label.kubernetes.io/arch` attribute with value `"arm64"`. - A label `data` with empty string value SHOULD be recorded as the `k8s.node.label.data` attribute with value `""`. """ K8S_NODE_NAME: Final = "k8s.node.name" """ The name of the Node. """ K8S_NODE_UID: Final = "k8s.node.uid" """ The UID of the Node. """ K8S_POD_ANNOTATION_TEMPLATE: Final = "k8s.pod.annotation" """ The annotation placed on the Pod, the `` being the annotation name, the value being the annotation value. Note: Examples: - An annotation `kubernetes.io/enforce-mountable-secrets` with value `true` SHOULD be recorded as the `k8s.pod.annotation.kubernetes.io/enforce-mountable-secrets` attribute with value `"true"`. - An annotation `mycompany.io/arch` with value `x64` SHOULD be recorded as the `k8s.pod.annotation.mycompany.io/arch` attribute with value `"x64"`. - An annotation `data` with empty string value SHOULD be recorded as the `k8s.pod.annotation.data` attribute with value `""`. """ K8S_POD_HOSTNAME: Final = "k8s.pod.hostname" """ Specifies the hostname of the Pod. Note: The K8s Pod spec has an optional hostname field, which can be used to specify a hostname. Refer to [K8s docs](https://kubernetes.io/docs/concepts/services-networking/dns-pod-service/#pod-hostname-and-subdomain-field) for more information about this field. This attribute aligns with the `hostname` field of the [K8s PodSpec](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#podspec-v1-core). """ K8S_POD_IP: Final = "k8s.pod.ip" """ IP address allocated to the Pod. Note: This attribute aligns with the `podIP` field of the [K8s PodStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#podstatus-v1-core). """ K8S_POD_LABEL_TEMPLATE: Final = "k8s.pod.label" """ The label placed on the Pod, the `` being the label name, the value being the label value. Note: Examples: - A label `app` with value `my-app` SHOULD be recorded as the `k8s.pod.label.app` attribute with value `"my-app"`. - A label `mycompany.io/arch` with value `x64` SHOULD be recorded as the `k8s.pod.label.mycompany.io/arch` attribute with value `"x64"`. - A label `data` with empty string value SHOULD be recorded as the `k8s.pod.label.data` attribute with value `""`. """ K8S_POD_NAME: Final = "k8s.pod.name" """ The name of the Pod. """ K8S_POD_START_TIME: Final = "k8s.pod.start_time" """ The start timestamp of the Pod. Note: Date and time at which the object was acknowledged by the Kubelet. This is before the Kubelet pulled the container image(s) for the pod. This attribute aligns with the `startTime` field of the [K8s PodStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#podstatus-v1-core), in ISO 8601 (RFC 3339 compatible) format. """ K8S_POD_UID: Final = "k8s.pod.uid" """ The UID of the Pod. """ K8S_REPLICASET_ANNOTATION_TEMPLATE: Final = "k8s.replicaset.annotation" """ The annotation placed on the ReplicaSet, the `` being the annotation name, the value being the annotation value, even if the value is empty. Note: Examples: - An annotation `replicas` with value `0` SHOULD be recorded as the `k8s.replicaset.annotation.replicas` attribute with value `"0"`. - An annotation `data` with empty string value SHOULD be recorded as the `k8s.replicaset.annotation.data` attribute with value `""`. """ K8S_REPLICASET_LABEL_TEMPLATE: Final = "k8s.replicaset.label" """ The label placed on the ReplicaSet, the `` being the label name, the value being the label value, even if the value is empty. Note: Examples: - A label `app` with value `guestbook` SHOULD be recorded as the `k8s.replicaset.label.app` attribute with value `"guestbook"`. - A label `injected` with empty string value SHOULD be recorded as the `k8s.replicaset.label.injected` attribute with value `""`. """ K8S_REPLICASET_NAME: Final = "k8s.replicaset.name" """ The name of the ReplicaSet. """ K8S_REPLICASET_UID: Final = "k8s.replicaset.uid" """ The UID of the ReplicaSet. """ K8S_STATEFULSET_ANNOTATION_TEMPLATE: Final = "k8s.statefulset.annotation" """ The annotation placed on the StatefulSet, the `` being the annotation name, the value being the annotation value, even if the value is empty. Note: Examples: - An annotation `replicas` with value `1` SHOULD be recorded as the `k8s.statefulset.annotation.replicas` attribute with value `"1"`. - An annotation `data` with empty string value SHOULD be recorded as the `k8s.statefulset.annotation.data` attribute with value `""`. """ K8S_STATEFULSET_LABEL_TEMPLATE: Final = "k8s.statefulset.label" """ The label placed on the StatefulSet, the `` being the label name, the value being the label value, even if the value is empty. Note: Examples: - A label `app` with value `guestbook` SHOULD be recorded as the `k8s.statefulset.label.app` attribute with value `"guestbook"`. - A label `injected` with empty string value SHOULD be recorded as the `k8s.statefulset.label.injected` attribute with value `""`. """ K8S_STATEFULSET_NAME: Final = "k8s.statefulset.name" """ The name of the StatefulSet. """ K8S_STATEFULSET_UID: Final = "k8s.statefulset.uid" """ The UID of the StatefulSet. """ network_attributes.py000066400000000000000000000042631527070070700406450ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/src/opentelemetry/semconv/attributes# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from enum import Enum from typing import Final NETWORK_LOCAL_ADDRESS: Final = "network.local.address" """ Local address of the network connection - IP address or UNIX domain socket name. """ NETWORK_LOCAL_PORT: Final = "network.local.port" """ Local port number of the network connection. """ NETWORK_PEER_ADDRESS: Final = "network.peer.address" """ Peer address of the network connection - IP address or UNIX domain socket name. """ NETWORK_PEER_PORT: Final = "network.peer.port" """ Peer port number of the network connection. """ NETWORK_PROTOCOL_NAME: Final = "network.protocol.name" """ [OSI application layer](https://wikipedia.org/wiki/Application_layer) or non-OSI equivalent. Note: The value SHOULD be normalized to lowercase. """ NETWORK_PROTOCOL_VERSION: Final = "network.protocol.version" """ The actual version of the protocol used for network communication. Note: If protocol version is subject to negotiation (for example using [ALPN](https://www.rfc-editor.org/rfc/rfc7301.html)), this attribute SHOULD be set to the negotiated version. If the actual protocol version is not known, this attribute SHOULD NOT be set. """ NETWORK_TRANSPORT: Final = "network.transport" """ [OSI transport layer](https://wikipedia.org/wiki/Transport_layer) or [inter-process communication method](https://wikipedia.org/wiki/Inter-process_communication). Note: The value SHOULD be normalized to lowercase. Consider always setting the transport when setting a port number, since a port number is ambiguous without knowing the transport. For example different processes could be listening on TCP port 12345 and UDP port 12345. """ NETWORK_TYPE: Final = "network.type" """ [OSI network layer](https://wikipedia.org/wiki/Network_layer) or non-OSI equivalent. Note: The value SHOULD be normalized to lowercase. """ class NetworkTransportValues(Enum): TCP = "tcp" """TCP.""" UDP = "udp" """UDP.""" PIPE = "pipe" """Named or anonymous pipe.""" UNIX = "unix" """UNIX domain socket.""" QUIC = "quic" """QUIC.""" class NetworkTypeValues(Enum): IPV4 = "ipv4" """IPv4.""" IPV6 = "ipv6" """IPv6.""" otel_attributes.py000066400000000000000000000023721527070070700401160ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/src/opentelemetry/semconv/attributes# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from enum import Enum from typing import Final OTEL_EVENT_NAME: Final = "otel.event.name" """ Identifies the class / type of event. Note: This attribute SHOULD be used by non-OTLP exporters when destination does not support `EventName` or equivalent field. This attribute MAY be used by applications using existing logging libraries so that it can be used to set the `EventName` field by Collector or SDK components. """ OTEL_SCOPE_NAME: Final = "otel.scope.name" """ The name of the instrumentation scope - (`InstrumentationScope.Name` in OTLP). """ OTEL_SCOPE_VERSION: Final = "otel.scope.version" """ The version of the instrumentation scope - (`InstrumentationScope.Version` in OTLP). """ OTEL_STATUS_CODE: Final = "otel.status_code" """ Name of the code, either "OK" or "ERROR". MUST NOT be set if the status code is UNSET. """ OTEL_STATUS_DESCRIPTION: Final = "otel.status_description" """ Description of the Status if it has a value, otherwise not set. """ class OtelStatusCodeValues(Enum): OK = "OK" """The operation has been validated by an Application developer or Operator to have completed successfully.""" ERROR = "ERROR" """The operation contains an error.""" server_attributes.py000066400000000000000000000013441527070070700404570ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/src/opentelemetry/semconv/attributes# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from typing import Final SERVER_ADDRESS: Final = "server.address" """ Server domain name if available without reverse DNS lookup; otherwise, IP address or UNIX domain socket name. Note: When observed from the client side, and when communicating through an intermediary, `server.address` SHOULD represent the server address behind any intermediaries, for example proxies, if it's available. """ SERVER_PORT: Final = "server.port" """ Server port number. Note: When observed from the client side, and when communicating through an intermediary, `server.port` SHOULD represent the server port behind any intermediaries, for example proxies, if it's available. """ service_attributes.py000066400000000000000000000066321527070070700406160ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/src/opentelemetry/semconv/attributes# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from typing import Final SERVICE_INSTANCE_ID: Final = "service.instance.id" """ The string ID of the service instance. Note: MUST be unique for each instance of the same `service.namespace,service.name` pair (in other words `service.namespace,service.name,service.instance.id` triplet MUST be globally unique). The ID helps to distinguish instances of the same service that exist at the same time (e.g. instances of a horizontally scaled service). Implementations, such as SDKs, are recommended to generate a random Version 1 or Version 4 [RFC 4122](https://www.ietf.org/rfc/rfc4122.txt) UUID, but are free to use an inherent unique ID as the source of this value if stability is desirable. In that case, the ID SHOULD be used as source of a UUID Version 5 and SHOULD use the following UUID as the namespace: `4d63009a-8d0f-11ee-aad7-4c796ed8e320`. UUIDs are typically recommended, as only an opaque value for the purposes of identifying a service instance is needed. Similar to what can be seen in the man page for the [`/etc/machine-id`](https://www.freedesktop.org/software/systemd/man/latest/machine-id.html) file, the underlying data, such as pod name and namespace should be treated as confidential, being the user's choice to expose it or not via another resource attribute. For applications running behind an application server (like unicorn), we do not recommend using one identifier for all processes participating in the application. Instead, it's recommended each division (e.g. a worker thread in unicorn) to have its own instance.id. It's not recommended for a Collector to set `service.instance.id` if it can't unambiguously determine the service instance that is generating that telemetry. For instance, creating an UUID based on `pod.name` will likely be wrong, as the Collector might not know from which container within that pod the telemetry originated. However, Collectors can set the `service.instance.id` if they can unambiguously determine the service instance for that telemetry. This is typically the case for scraping receivers, as they know the target address and port. """ SERVICE_NAME: Final = "service.name" """ Logical name of the service. Note: MUST be the same for all instances of horizontally scaled services. If the value was not specified, SDKs MUST fallback to `unknown_service:` concatenated with the process executable name, e.g. `unknown_service:bash`. If the process executable name is not available, the value MUST be set to `unknown_service`. The process executable name is the name of the process executable, the same value as described by the [`process.executable.name`](process.md) resource attribute. """ SERVICE_NAMESPACE: Final = "service.namespace" """ A namespace for `service.name`. Note: A string value having a meaning that helps to distinguish a group of services, for example the team name that owns a group of services. `service.name` is expected to be unique within the same namespace. If `service.namespace` is not specified in the Resource then `service.name` is expected to be unique for all services that have no explicit namespace defined (so the empty/unspecified namespace is simply one more valid namespace). Zero-length namespace string is assumed equal to unspecified namespace. """ SERVICE_VERSION: Final = "service.version" """ The version string of the service component. The format is not defined by these conventions. """ telemetry_attributes.py000066400000000000000000000050331527070070700411620ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/src/opentelemetry/semconv/attributes# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from enum import Enum from typing import Final TELEMETRY_DISTRO_NAME: Final = "telemetry.distro.name" """ The name of the auto instrumentation agent or distribution, if used. Note: Official auto instrumentation agents and distributions SHOULD set the `telemetry.distro.name` attribute to a string starting with `opentelemetry-`, e.g. `opentelemetry-java-instrumentation`. """ TELEMETRY_DISTRO_VERSION: Final = "telemetry.distro.version" """ The version string of the auto instrumentation agent or distribution, if used. """ TELEMETRY_SDK_LANGUAGE: Final = "telemetry.sdk.language" """ The language of the telemetry SDK. """ TELEMETRY_SDK_NAME: Final = "telemetry.sdk.name" """ The name of the telemetry SDK as defined above. Note: The OpenTelemetry SDK MUST set the `telemetry.sdk.name` attribute to `opentelemetry`. If another SDK, like a fork or a vendor-provided implementation, is used, this SDK MUST set the `telemetry.sdk.name` attribute to the fully-qualified class or module name of this SDK's main entry point or another suitable identifier depending on the language. The identifier `opentelemetry` is reserved and MUST NOT be used in this case. All custom identifiers SHOULD be stable across different versions of an implementation. """ TELEMETRY_SDK_VERSION: Final = "telemetry.sdk.version" """ The version string of the telemetry SDK. """ class TelemetrySdkLanguageValues(Enum): CPP = "cpp" """[C++](https://opentelemetry.io/docs/languages/cpp/).""" DOTNET = "dotnet" """[.NET](https://opentelemetry.io/docs/languages/dotnet/).""" ERLANG = "erlang" """[Erlang/Elixir](https://opentelemetry.io/docs/languages/erlang/).""" GO = "go" """[Go](https://opentelemetry.io/docs/languages/go/).""" JAVA = "java" """[Java](https://opentelemetry.io/docs/languages/java/).""" KOTLIN = "kotlin" """[Kotlin](https://opentelemetry.io/docs/languages/kotlin/).""" NODEJS = "nodejs" """[Node.js](https://opentelemetry.io/docs/languages/js/).""" PHP = "php" """[PHP](https://opentelemetry.io/docs/languages/php/).""" PYTHON = "python" """[Python](https://opentelemetry.io/docs/languages/python/).""" RUBY = "ruby" """[Ruby](https://opentelemetry.io/docs/languages/ruby/).""" RUST = "rust" """[Rust](https://opentelemetry.io/docs/languages/rust/).""" SWIFT = "swift" """[Swift](https://opentelemetry.io/docs/languages/swift/).""" WEBJS = "webjs" """[Browser](https://opentelemetry.io/docs/languages/js/).""" url_attributes.py000066400000000000000000000105131527070070700377510ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/src/opentelemetry/semconv/attributes# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from typing import Final URL_FRAGMENT: Final = "url.fragment" """ The [URI fragment](https://www.rfc-editor.org/rfc/rfc3986#section-3.5) component. """ URL_FULL: Final = "url.full" """ Absolute URL describing a network resource according to [RFC3986](https://www.rfc-editor.org/rfc/rfc3986). Note: For network calls, URL usually has `scheme://host[:port][path][?query][#fragment]` format, where the fragment is not transmitted over HTTP, but if it is known, it SHOULD be included nevertheless. `url.full` MUST NOT contain credentials passed via URL in form of `https://username:password@www.example.com/`. In such case username and password SHOULD be redacted and attribute's value SHOULD be `https://REDACTED:REDACTED@www.example.com/`. `url.full` SHOULD capture the absolute URL when it is available (or can be reconstructed). Sensitive content provided in `url.full` SHOULD be scrubbed when instrumentations can identify it. ![Development](https://img.shields.io/badge/-development-blue) Query string values for the following keys SHOULD be redacted by default and replaced by the value `REDACTED`: * [`X-Amz-Signature`](https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_sigv-authentication-methods.html) * [`X-Amz-Credential`](https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_sigv-authentication-methods.html) * [`X-Amz-Security-Token`](https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_sigv-authentication-methods.html) * [`sig`](https://learn.microsoft.com/azure/storage/common/storage-sas-overview#sas-token) * [`X-Goog-Signature`](https://cloud.google.com/storage/docs/access-control/signed-urls) This list is subject to change over time. Matching of query parameter keys against the sensitive list SHOULD be case-sensitive. ![Development](https://img.shields.io/badge/-development-blue) Instrumentation MAY provide a way to override this list via declarative configuration. If so, it SHOULD use the `sensitive_query_parameters` property (an array of case-sensitive strings with minimum items 0) under `.instrumentation/development.general.sanitization.url`. This list is a full override of the default sensitive query parameter keys, it is not a list of keys in addition to the defaults. When a query string value is redacted, the query string key SHOULD still be preserved, e.g. `https://www.example.com/path?color=blue&sig=REDACTED`. """ URL_PATH: Final = "url.path" """ The [URI path](https://www.rfc-editor.org/rfc/rfc3986#section-3.3) component. Note: Sensitive content provided in `url.path` SHOULD be scrubbed when instrumentations can identify it. """ URL_QUERY: Final = "url.query" """ The [URI query](https://www.rfc-editor.org/rfc/rfc3986#section-3.4) component. Note: Sensitive content provided in `url.query` SHOULD be scrubbed when instrumentations can identify it. ![Development](https://img.shields.io/badge/-development-blue) Query string values for the following keys SHOULD be redacted by default and replaced by the value `REDACTED`: * [`X-Amz-Signature`](https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_sigv-authentication-methods.html) * [`X-Amz-Credential`](https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_sigv-authentication-methods.html) * [`X-Amz-Security-Token`](https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_sigv-authentication-methods.html) * [`sig`](https://learn.microsoft.com/azure/storage/common/storage-sas-overview#sas-token) * [`X-Goog-Signature`](https://cloud.google.com/storage/docs/access-control/signed-urls) This list is subject to change over time. Matching of query parameter keys against the sensitive list SHOULD be case-sensitive. Instrumentation MAY provide a way to override this list via declarative configuration. If so, it SHOULD use the `sensitive_query_parameters` property (an array of case-sensitive strings with minimum items 0) under `.instrumentation/development.general.sanitization.url`. This list is a full override of the default sensitive query parameter keys, it is not a list of keys in addition to the defaults. When a query string value is redacted, the query string key SHOULD still be preserved, e.g. `q=OpenTelemetry&sig=REDACTED`. """ URL_SCHEME: Final = "url.scheme" """ The [URI scheme](https://www.rfc-editor.org/rfc/rfc3986#section-3.1) component identifying the used protocol. """ user_agent_attributes.py000066400000000000000000000004321527070070700413020ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/src/opentelemetry/semconv/attributes# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from typing import Final USER_AGENT_ORIGINAL: Final = "user_agent.original" """ Value of the [HTTP User-Agent](https://www.rfc-editor.org/rfc/rfc9110.html#field.user-agent) header sent by the client. """ python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/src/opentelemetry/semconv/metrics/000077500000000000000000000000001527070070700336665ustar00rootroot00000000000000__init__.py000066400000000000000000000121041527070070700357160ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/src/opentelemetry/semconv/metrics# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from typing_extensions import deprecated @deprecated( "Use metrics defined in the :py:const:`opentelemetry.semconv.metrics` and :py:const:`opentelemetry.semconv._incubating.metrics` modules instead. Deprecated since version 1.25.0.", ) class MetricInstruments: SCHEMA_URL = "https://opentelemetry.io/schemas/1.21.0" """ The URL of the OpenTelemetry schema for these keys and values. """ HTTP_SERVER_DURATION = "http.server.duration" """ Measures the duration of inbound HTTP requests Instrument: histogram Unit: s """ HTTP_SERVER_ACTIVE_REQUESTS = "http.server.active_requests" """ Measures the number of concurrent HTTP requests that are currently in-flight Instrument: updowncounter Unit: {request} """ HTTP_SERVER_REQUEST_SIZE = "http.server.request.size" """ Measures the size of HTTP request messages (compressed) Instrument: histogram Unit: By """ HTTP_SERVER_RESPONSE_SIZE = "http.server.response.size" """ Measures the size of HTTP response messages (compressed) Instrument: histogram Unit: By """ HTTP_CLIENT_DURATION = "http.client.duration" """ Measures the duration of outbound HTTP requests Instrument: histogram Unit: s """ HTTP_CLIENT_REQUEST_SIZE = "http.client.request.size" """ Measures the size of HTTP request messages (compressed) Instrument: histogram Unit: By """ HTTP_CLIENT_RESPONSE_SIZE = "http.client.response.size" """ Measures the size of HTTP response messages (compressed) Instrument: histogram Unit: By """ PROCESS_RUNTIME_JVM_MEMORY_INIT = "process.runtime.jvm.memory.init" """ Measure of initial memory requested Instrument: updowncounter Unit: By """ PROCESS_RUNTIME_JVM_SYSTEM_CPU_UTILIZATION = "process.runtime.jvm.system.cpu.utilization" """ Recent CPU utilization for the whole system as reported by the JVM Instrument: gauge Unit: 1 """ PROCESS_RUNTIME_JVM_SYSTEM_CPU_LOAD_1M = "process.runtime.jvm.system.cpu.load_1m" """ Average CPU load of the whole system for the last minute as reported by the JVM Instrument: gauge Unit: 1 """ PROCESS_RUNTIME_JVM_BUFFER_USAGE = "process.runtime.jvm.buffer.usage" """ Measure of memory used by buffers Instrument: updowncounter Unit: By """ PROCESS_RUNTIME_JVM_BUFFER_LIMIT = "process.runtime.jvm.buffer.limit" """ Measure of total memory capacity of buffers Instrument: updowncounter Unit: By """ PROCESS_RUNTIME_JVM_BUFFER_COUNT = "process.runtime.jvm.buffer.count" """ Number of buffers in the pool Instrument: updowncounter Unit: {buffer} """ PROCESS_RUNTIME_JVM_MEMORY_USAGE = "process.runtime.jvm.memory.usage" """ Measure of memory used Instrument: updowncounter Unit: By """ PROCESS_RUNTIME_JVM_MEMORY_COMMITTED = "process.runtime.jvm.memory.committed" """ Measure of memory committed Instrument: updowncounter Unit: By """ PROCESS_RUNTIME_JVM_MEMORY_LIMIT = "process.runtime.jvm.memory.limit" """ Measure of max obtainable memory Instrument: updowncounter Unit: By """ PROCESS_RUNTIME_JVM_MEMORY_USAGE_AFTER_LAST_GC = "process.runtime.jvm.memory.usage_after_last_gc" """ Measure of memory used, as measured after the most recent garbage collection event on this pool Instrument: updowncounter Unit: By """ PROCESS_RUNTIME_JVM_GC_DURATION = "process.runtime.jvm.gc.duration" """ Duration of JVM garbage collection actions Instrument: histogram Unit: s """ PROCESS_RUNTIME_JVM_THREADS_COUNT = "process.runtime.jvm.threads.count" """ Number of executing platform threads Instrument: updowncounter Unit: {thread} """ PROCESS_RUNTIME_JVM_CLASSES_LOADED = "process.runtime.jvm.classes.loaded" """ Number of classes loaded since JVM start Instrument: counter Unit: {class} """ PROCESS_RUNTIME_JVM_CLASSES_UNLOADED = "process.runtime.jvm.classes.unloaded" """ Number of classes unloaded since JVM start Instrument: counter Unit: {class} """ PROCESS_RUNTIME_JVM_CLASSES_CURRENT_LOADED = "process.runtime.jvm.classes.current_loaded" """ Number of classes currently loaded Instrument: updowncounter Unit: {class} """ PROCESS_RUNTIME_JVM_CPU_TIME = "process.runtime.jvm.cpu.time" """ CPU time used by the process as reported by the JVM Instrument: counter Unit: s """ PROCESS_RUNTIME_JVM_CPU_RECENT_UTILIZATION = "process.runtime.jvm.cpu.recent_utilization" """ Recent CPU utilization for the process as reported by the JVM Instrument: gauge Unit: 1 """ # Manually defined metrics DB_CLIENT_CONNECTIONS_USAGE = "db.client.connections.usage" """ The number of connections that are currently in state described by the `state` attribute Instrument: UpDownCounter Unit: {connection} """ db_metrics.py000066400000000000000000000004731527070070700363000ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/src/opentelemetry/semconv/metrics# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from typing import Final DB_CLIENT_OPERATION_DURATION: Final = "db.client.operation.duration" """ Duration of database client operations Instrument: histogram Unit: s Note: Batch operations SHOULD be recorded as a single operation. """ http_metrics.py000066400000000000000000000006021527070070700366640ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/src/opentelemetry/semconv/metrics# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from typing import Final HTTP_CLIENT_REQUEST_DURATION: Final = "http.client.request.duration" """ Duration of HTTP client requests Instrument: histogram Unit: s """ HTTP_SERVER_REQUEST_DURATION: Final = "http.server.request.duration" """ Duration of HTTP server requests Instrument: histogram Unit: s """ python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/src/opentelemetry/semconv/py.typed000066400000000000000000000000001527070070700337050ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/src/opentelemetry/semconv/resource/000077500000000000000000000000001527070070700340475ustar00rootroot00000000000000__init__.py000066400000000000000000000773261527070070700361200ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/src/opentelemetry/semconv/resource# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # pylint: disable=too-many-lines from enum import Enum from typing_extensions import deprecated @deprecated( "Use attributes defined in the :py:const:`opentelemetry.semconv.attributes` and :py:const:`opentelemetry.semconv._incubating.attributes` modules instead. Deprecated since version 1.25.0.", ) class ResourceAttributes: SCHEMA_URL = "https://opentelemetry.io/schemas/1.21.0" """ The URL of the OpenTelemetry schema for these keys and values. """ BROWSER_BRANDS = "browser.brands" """ Array of brand name and version separated by a space. Note: This value is intended to be taken from the [UA client hints API](https://wicg.github.io/ua-client-hints/#interface) (`navigator.userAgentData.brands`). """ BROWSER_PLATFORM = "browser.platform" """ The platform on which the browser is running. Note: This value is intended to be taken from the [UA client hints API](https://wicg.github.io/ua-client-hints/#interface) (`navigator.userAgentData.platform`). If unavailable, the legacy `navigator.platform` API SHOULD NOT be used instead and this attribute SHOULD be left unset in order for the values to be consistent. The list of possible values is defined in the [W3C User-Agent Client Hints specification](https://wicg.github.io/ua-client-hints/#sec-ch-ua-platform). Note that some (but not all) of these values can overlap with values in the [`os.type` and `os.name` attributes](./os.md). However, for consistency, the values in the `browser.platform` attribute should capture the exact value that the user agent provides. """ BROWSER_MOBILE = "browser.mobile" """ A boolean that is true if the browser is running on a mobile device. Note: This value is intended to be taken from the [UA client hints API](https://wicg.github.io/ua-client-hints/#interface) (`navigator.userAgentData.mobile`). If unavailable, this attribute SHOULD be left unset. """ BROWSER_LANGUAGE = "browser.language" """ Preferred language of the user using the browser. Note: This value is intended to be taken from the Navigator API `navigator.language`. """ USER_AGENT_ORIGINAL = "user_agent.original" """ Full user-agent string provided by the browser. Note: The user-agent value SHOULD be provided only from browsers that do not have a mechanism to retrieve brands and platform individually from the User-Agent Client Hints API. To retrieve the value, the legacy `navigator.userAgent` API can be used. """ CLOUD_PROVIDER = "cloud.provider" """ Name of the cloud provider. """ CLOUD_ACCOUNT_ID = "cloud.account.id" """ The cloud account ID the resource is assigned to. """ CLOUD_REGION = "cloud.region" """ The geographical region the resource is running. Note: Refer to your provider's docs to see the available regions, for example [Alibaba Cloud regions](https://www.alibabacloud.com/help/doc-detail/40654.htm), [AWS regions](https://aws.amazon.com/about-aws/global-infrastructure/regions_az/), [Azure regions](https://azure.microsoft.com/en-us/global-infrastructure/geographies/), [Google Cloud regions](https://cloud.google.com/about/locations), or [Tencent Cloud regions](https://www.tencentcloud.com/document/product/213/6091). """ CLOUD_RESOURCE_ID = "cloud.resource_id" """ Cloud provider-specific native identifier of the monitored cloud resource (e.g. an [ARN](https://docs.aws.amazon.com/general/latest/gr/aws-arns-and-namespaces.html) on AWS, a [fully qualified resource ID](https://learn.microsoft.com/en-us/rest/api/resources/resources/get-by-id) on Azure, a [full resource name](https://cloud.google.com/apis/design/resource_names#full_resource_name) on GCP). Note: 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. The exact value to use for `cloud.resource_id` depends on the cloud provider. The following well-known definitions MUST be used if you set this attribute and they apply: * **AWS Lambda:** The function [ARN](https://docs.aws.amazon.com/general/latest/gr/aws-arns-and-namespaces.html). Take care not to use the "invoked ARN" directly but replace any [alias suffix](https://docs.aws.amazon.com/lambda/latest/dg/configuration-aliases.html) with the resolved function version, as the same runtime instance may be invokable with multiple different aliases. * **GCP:** The [URI of the resource](https://cloud.google.com/iam/docs/full-resource-names) * **Azure:** The [Fully Qualified Resource ID](https://docs.microsoft.com/en-us/rest/api/resources/resources/get-by-id) of the invoked function, *not* the function app, having the form `/subscriptions//resourceGroups//providers/Microsoft.Web/sites//functions/`. This means that a span attribute MUST be used, as an Azure function app can host multiple functions that would usually share a TracerProvider. """ CLOUD_AVAILABILITY_ZONE = "cloud.availability_zone" """ Cloud regions often have multiple, isolated locations known as zones to increase availability. Availability zone represents the zone where the resource is running. Note: Availability zones are called "zones" on Alibaba Cloud and Google Cloud. """ CLOUD_PLATFORM = "cloud.platform" """ The cloud platform in use. Note: The prefix of the service SHOULD match the one specified in `cloud.provider`. """ AWS_ECS_CONTAINER_ARN = "aws.ecs.container.arn" """ The Amazon Resource Name (ARN) of an [ECS container instance](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/ECS_instances.html). """ AWS_ECS_CLUSTER_ARN = "aws.ecs.cluster.arn" """ The ARN of an [ECS cluster](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/clusters.html). """ AWS_ECS_LAUNCHTYPE = "aws.ecs.launchtype" """ The [launch type](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/launch_types.html) for an ECS task. """ AWS_ECS_TASK_ARN = "aws.ecs.task.arn" """ The ARN of an [ECS task definition](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task_definitions.html). """ AWS_ECS_TASK_FAMILY = "aws.ecs.task.family" """ The task definition family this task definition is a member of. """ AWS_ECS_TASK_REVISION = "aws.ecs.task.revision" """ The revision for this task definition. """ AWS_EKS_CLUSTER_ARN = "aws.eks.cluster.arn" """ The ARN of an EKS cluster. """ AWS_LOG_GROUP_NAMES = "aws.log.group.names" """ The name(s) of the AWS log group(s) an application is writing to. Note: Multiple log groups must be supported for cases like multi-container applications, where a single application has sidecar containers, and each write to their own log group. """ AWS_LOG_GROUP_ARNS = "aws.log.group.arns" """ The Amazon Resource Name(s) (ARN) of the AWS log group(s). Note: See the [log group ARN format documentation](https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/iam-access-control-overview-cwl.html#CWL_ARN_Format). """ AWS_LOG_STREAM_NAMES = "aws.log.stream.names" """ The name(s) of the AWS log stream(s) an application is writing to. """ AWS_LOG_STREAM_ARNS = "aws.log.stream.arns" """ The ARN(s) of the AWS log stream(s). Note: See the [log stream ARN format documentation](https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/iam-access-control-overview-cwl.html#CWL_ARN_Format). One log group can contain several log streams, so these ARNs necessarily identify both a log group and a log stream. """ GCP_CLOUD_RUN_JOB_EXECUTION = "gcp.cloud_run.job.execution" """ The name of the Cloud Run [execution](https://cloud.google.com/run/docs/managing/job-executions) being run for the Job, as set by the [`CLOUD_RUN_EXECUTION`](https://cloud.google.com/run/docs/container-contract#jobs-env-vars) environment variable. """ GCP_CLOUD_RUN_JOB_TASK_INDEX = "gcp.cloud_run.job.task_index" """ The index for a task within an execution as provided by the [`CLOUD_RUN_TASK_INDEX`](https://cloud.google.com/run/docs/container-contract#jobs-env-vars) environment variable. """ GCP_GCE_INSTANCE_NAME = "gcp.gce.instance.name" """ The instance name of a GCE instance. This is the value provided by `host.name`, the visible name of the instance in the Cloud Console UI, and the prefix for the default hostname of the instance as defined by the [default internal DNS name](https://cloud.google.com/compute/docs/internal-dns#instance-fully-qualified-domain-names). """ GCP_GCE_INSTANCE_HOSTNAME = "gcp.gce.instance.hostname" """ The hostname of a GCE instance. This is the full value of the default or [custom hostname](https://cloud.google.com/compute/docs/instances/custom-hostname-vm). """ HEROKU_RELEASE_CREATION_TIMESTAMP = "heroku.release.creation_timestamp" """ Time and date the release was created. """ HEROKU_RELEASE_COMMIT = "heroku.release.commit" """ Commit hash for the current release. """ HEROKU_APP_ID = "heroku.app.id" """ Unique identifier for the application. """ CONTAINER_NAME = "container.name" """ Container name used by container runtime. """ CONTAINER_ID = "container.id" """ Container ID. Usually a UUID, as for example used to [identify Docker containers](https://docs.docker.com/engine/reference/run/#container-identification). The UUID might be abbreviated. """ CONTAINER_RUNTIME = "container.runtime" """ The container runtime managing this container. """ CONTAINER_IMAGE_NAME = "container.image.name" """ Name of the image the container was built on. """ CONTAINER_IMAGE_TAG = "container.image.tag" """ Container image tag. """ CONTAINER_IMAGE_ID = "container.image.id" """ Runtime specific image identifier. Usually a hash algorithm followed by a UUID. Note: Docker defines a sha256 of the image id; `container.image.id` corresponds to the `Image` field from the Docker container inspect [API](https://docs.docker.com/engine/api/v1.43/#tag/Container/operation/ContainerInspect) endpoint. K8s defines a link to the container registry repository with digest `"imageID": "registry.azurecr.io /namespace/service/dockerfile@sha256:bdeabd40c3a8a492eaf9e8e44d0ebbb84bac7ee25ac0cf8a7159d25f62555625"`. OCI defines a digest of manifest. """ CONTAINER_COMMAND = "container.command" """ The command used to run the container (i.e. the command name). Note: If using embedded credentials or sensitive data, it is recommended to remove them to prevent potential leakage. """ CONTAINER_COMMAND_LINE = "container.command_line" """ The full command run by the container as a single string representing the full command. [2]. """ CONTAINER_COMMAND_ARGS = "container.command_args" """ All the command arguments (including the command/executable itself) run by the container. [2]. """ DEPLOYMENT_ENVIRONMENT = "deployment.environment" """ Name of the [deployment environment](https://en.wikipedia.org/wiki/Deployment_environment) (aka deployment tier). """ DEVICE_ID = "device.id" """ A unique identifier representing the device. Note: The device identifier MUST only be defined using the values outlined below. This value is not an advertising identifier and MUST NOT be used as such. On iOS (Swift or Objective-C), this value MUST be equal to the [vendor identifier](https://developer.apple.com/documentation/uikit/uidevice/1620059-identifierforvendor). On Android (Java or Kotlin), this value MUST be equal to the Firebase Installation ID or a globally unique UUID which is persisted across sessions in your application. More information can be found [here](https://developer.android.com/training/articles/user-data-ids) on best practices and exact implementation details. Caution should be taken when storing personal data or anything which can identify a user. GDPR and data protection laws may apply, ensure you do your own due diligence. """ DEVICE_MODEL_IDENTIFIER = "device.model.identifier" """ The model identifier for the device. Note: It's recommended this value represents a machine readable version of the model identifier rather than the market or consumer-friendly name of the device. """ DEVICE_MODEL_NAME = "device.model.name" """ The marketing name for the device model. Note: It's recommended this value represents a human readable version of the device model rather than a machine readable alternative. """ DEVICE_MANUFACTURER = "device.manufacturer" """ The name of the device manufacturer. Note: The Android OS provides this field via [Build](https://developer.android.com/reference/android/os/Build#MANUFACTURER). iOS apps SHOULD hardcode the value `Apple`. """ FAAS_NAME = "faas.name" """ The name of the single function that this runtime instance executes. Note: This is the name of the function as configured/deployed on the FaaS platform and is usually different from the name of the callback function (which may be stored in the [`code.namespace`/`code.function`](/docs/general/general-attributes.md#source-code-attributes) span attributes). For some cloud providers, the above definition is ambiguous. The following definition of function name MUST be used for this attribute (and consequently the span name) for the listed cloud providers/products: * **Azure:** The full name `/`, i.e., function app name followed by a forward slash followed by the function name (this form can also be seen in the resource JSON for the function). This means that a span attribute MUST be used, as an Azure function app can host multiple functions that would usually share a TracerProvider (see also the `cloud.resource_id` attribute). """ FAAS_VERSION = "faas.version" """ The immutable version of the function being executed. Note: Depending on the cloud provider and platform, use: * **AWS Lambda:** The [function version](https://docs.aws.amazon.com/lambda/latest/dg/configuration-versions.html) (an integer represented as a decimal string). * **Google Cloud Run (Services):** The [revision](https://cloud.google.com/run/docs/managing/revisions) (i.e., the function name plus the revision suffix). * **Google Cloud Functions:** The value of the [`K_REVISION` environment variable](https://cloud.google.com/functions/docs/env-var#runtime_environment_variables_set_automatically). * **Azure Functions:** Not applicable. Do not set this attribute. """ FAAS_INSTANCE = "faas.instance" """ The execution environment ID as a string, that will be potentially reused for other invocations to the same function/function version. Note: * **AWS Lambda:** Use the (full) log stream name. """ FAAS_MAX_MEMORY = "faas.max_memory" """ The amount of memory available to the serverless function converted to Bytes. Note: It's recommended to set this attribute since e.g. too little memory can easily stop a Java AWS Lambda function from working correctly. On AWS Lambda, the environment variable `AWS_LAMBDA_FUNCTION_MEMORY_SIZE` provides this information (which must be multiplied by 1,048,576). """ HOST_ID = "host.id" """ Unique host ID. For Cloud, this must be the instance_id assigned by the cloud provider. For non-containerized systems, this should be the `machine-id`. See the table below for the sources to use to determine the `machine-id` based on operating system. """ HOST_NAME = "host.name" """ Name of the host. On Unix systems, it may contain what the hostname command returns, or the fully qualified hostname, or another name specified by the user. """ HOST_TYPE = "host.type" """ Type of host. For Cloud, this must be the machine type. """ HOST_ARCH = "host.arch" """ The CPU architecture the host system is running on. """ HOST_IMAGE_NAME = "host.image.name" """ Name of the VM image or OS install the host was instantiated from. """ HOST_IMAGE_ID = "host.image.id" """ VM image ID or host OS image ID. For Cloud, this value is from the provider. """ HOST_IMAGE_VERSION = "host.image.version" """ The version string of the VM image or host OS as defined in [Version Attributes](README.md#version-attributes). """ K8S_CLUSTER_NAME = "k8s.cluster.name" """ The name of the cluster. """ K8S_CLUSTER_UID = "k8s.cluster.uid" """ A pseudo-ID for the cluster, set to the UID of the `kube-system` namespace. Note: K8s does not have support for obtaining a cluster ID. If this is ever added, we will recommend collecting the `k8s.cluster.uid` through the official APIs. In the meantime, we are able to use the `uid` of the `kube-system` namespace as a proxy for cluster ID. Read on for the rationale. Every object created in a K8s cluster is assigned a distinct UID. The `kube-system` namespace is used by Kubernetes itself and will exist for the lifetime of the cluster. Using the `uid` of the `kube-system` namespace is a reasonable proxy for the K8s ClusterID as it will only change if the cluster is rebuilt. Furthermore, Kubernetes UIDs are UUIDs as standardized by [ISO/IEC 9834-8 and ITU-T X.667](https://www.itu.int/ITU-T/studygroups/com17/oid.html). Which states: > If generated according to one of the mechanisms defined in Rec. ITU-T X.667 | ISO/IEC 9834-8, a UUID is either guaranteed to be different from all other UUIDs generated before 3603 A.D., or is extremely likely to be different (depending on the mechanism chosen). Therefore, UIDs between clusters should be extremely unlikely to conflict. """ K8S_NODE_NAME = "k8s.node.name" """ The name of the Node. """ K8S_NODE_UID = "k8s.node.uid" """ The UID of the Node. """ K8S_NAMESPACE_NAME = "k8s.namespace.name" """ The name of the namespace that the pod is running in. """ K8S_POD_UID = "k8s.pod.uid" """ The UID of the Pod. """ K8S_POD_NAME = "k8s.pod.name" """ The name of the Pod. """ K8S_CONTAINER_NAME = "k8s.container.name" """ The name of the Container from Pod specification, must be unique within a Pod. Container runtime usually uses different globally unique name (`container.name`). """ K8S_CONTAINER_RESTART_COUNT = "k8s.container.restart_count" """ Number of times the container was restarted. This attribute can be used to identify a particular container (running or stopped) within a container spec. """ K8S_REPLICASET_UID = "k8s.replicaset.uid" """ The UID of the ReplicaSet. """ K8S_REPLICASET_NAME = "k8s.replicaset.name" """ The name of the ReplicaSet. """ K8S_DEPLOYMENT_UID = "k8s.deployment.uid" """ The UID of the Deployment. """ K8S_DEPLOYMENT_NAME = "k8s.deployment.name" """ The name of the Deployment. """ K8S_STATEFULSET_UID = "k8s.statefulset.uid" """ The UID of the StatefulSet. """ K8S_STATEFULSET_NAME = "k8s.statefulset.name" """ The name of the StatefulSet. """ K8S_DAEMONSET_UID = "k8s.daemonset.uid" """ The UID of the DaemonSet. """ K8S_DAEMONSET_NAME = "k8s.daemonset.name" """ The name of the DaemonSet. """ K8S_JOB_UID = "k8s.job.uid" """ The UID of the Job. """ K8S_JOB_NAME = "k8s.job.name" """ The name of the Job. """ K8S_CRONJOB_UID = "k8s.cronjob.uid" """ The UID of the CronJob. """ K8S_CRONJOB_NAME = "k8s.cronjob.name" """ The name of the CronJob. """ OS_TYPE = "os.type" """ The operating system type. """ OS_DESCRIPTION = "os.description" """ Human readable (not intended to be parsed) OS version information, like e.g. reported by `ver` or `lsb_release -a` commands. """ OS_NAME = "os.name" """ Human readable operating system name. """ OS_VERSION = "os.version" """ The version string of the operating system as defined in [Version Attributes](/docs/resource/README.md#version-attributes). """ PROCESS_PID = "process.pid" """ Process identifier (PID). """ PROCESS_PARENT_PID = "process.parent_pid" """ Parent Process identifier (PID). """ PROCESS_EXECUTABLE_NAME = "process.executable.name" """ The name of the process executable. On Linux based systems, can be set to the `Name` in `proc/[pid]/status`. On Windows, can be set to the base name of `GetProcessImageFileNameW`. """ PROCESS_EXECUTABLE_PATH = "process.executable.path" """ The full path to the process executable. On Linux based systems, can be set to the target of `proc/[pid]/exe`. On Windows, can be set to the result of `GetProcessImageFileNameW`. """ PROCESS_COMMAND = "process.command" """ The command used to launch the process (i.e. the command name). On Linux based systems, can be set to the zeroth string in `proc/[pid]/cmdline`. On Windows, can be set to the first parameter extracted from `GetCommandLineW`. """ PROCESS_COMMAND_LINE = "process.command_line" """ The full command used to launch the process as a single string representing the full command. On Windows, can be set to the result of `GetCommandLineW`. Do not set this if you have to assemble it just for monitoring; use `process.command_args` instead. """ PROCESS_COMMAND_ARGS = "process.command_args" """ All the command arguments (including the command/executable itself) as received by the process. On Linux-based systems (and some other Unixoid systems supporting procfs), can be set according to the list of null-delimited strings extracted from `proc/[pid]/cmdline`. For libc-based executables, this would be the full argv vector passed to `main`. """ PROCESS_OWNER = "process.owner" """ The username of the user that owns the process. """ PROCESS_RUNTIME_NAME = "process.runtime.name" """ The name of the runtime of this process. For compiled native binaries, this SHOULD be the name of the compiler. """ PROCESS_RUNTIME_VERSION = "process.runtime.version" """ The version of the runtime of this process, as returned by the runtime without modification. """ PROCESS_RUNTIME_DESCRIPTION = "process.runtime.description" """ An additional description about the runtime of the process, for example a specific vendor customization of the runtime environment. """ SERVICE_NAME = "service.name" """ Logical name of the service. Note: MUST be the same for all instances of horizontally scaled services. If the value was not specified, SDKs MUST fallback to `unknown_service:` concatenated with [`process.executable.name`](process.md#process), e.g. `unknown_service:bash`. If `process.executable.name` is not available, the value MUST be set to `unknown_service`. """ SERVICE_VERSION = "service.version" """ The version string of the service API or implementation. The format is not defined by these conventions. """ SERVICE_NAMESPACE = "service.namespace" """ A namespace for `service.name`. Note: A string value having a meaning that helps to distinguish a group of services, for example the team name that owns a group of services. `service.name` is expected to be unique within the same namespace. If `service.namespace` is not specified in the Resource then `service.name` is expected to be unique for all services that have no explicit namespace defined (so the empty/unspecified namespace is simply one more valid namespace). Zero-length namespace string is assumed equal to unspecified namespace. """ SERVICE_INSTANCE_ID = "service.instance.id" """ The string ID of the service instance. Note: MUST be unique for each instance of the same `service.namespace,service.name` pair (in other words `service.namespace,service.name,service.instance.id` triplet MUST be globally unique). The ID helps to distinguish instances of the same service that exist at the same time (e.g. instances of a horizontally scaled service). It is preferable for the ID to be persistent and stay the same for the lifetime of the service instance, however it is acceptable that the ID is ephemeral and changes during important lifetime events for the service (e.g. service restarts). If the service has no inherent unique ID that can be used as the value of this attribute it is recommended to generate a random Version 1 or Version 4 RFC 4122 UUID (services aiming for reproducible UUIDs may also use Version 5, see RFC 4122 for more recommendations). """ TELEMETRY_SDK_NAME = "telemetry.sdk.name" """ The name of the telemetry SDK as defined above. Note: The OpenTelemetry SDK MUST set the `telemetry.sdk.name` attribute to `opentelemetry`. If another SDK, like a fork or a vendor-provided implementation, is used, this SDK MUST set the `telemetry.sdk.name` attribute to the fully-qualified class or module name of this SDK's main entry point or another suitable identifier depending on the language. The identifier `opentelemetry` is reserved and MUST NOT be used in this case. All custom identifiers SHOULD be stable across different versions of an implementation. """ TELEMETRY_SDK_LANGUAGE = "telemetry.sdk.language" """ The language of the telemetry SDK. """ TELEMETRY_SDK_VERSION = "telemetry.sdk.version" """ The version string of the telemetry SDK. """ TELEMETRY_AUTO_VERSION = "telemetry.auto.version" """ The version string of the auto instrumentation agent, if used. """ WEBENGINE_NAME = "webengine.name" """ The name of the web engine. """ WEBENGINE_VERSION = "webengine.version" """ The version of the web engine. """ WEBENGINE_DESCRIPTION = "webengine.description" """ Additional description of the web engine (e.g. detailed version and edition information). """ OTEL_SCOPE_NAME = "otel.scope.name" """ The name of the instrumentation scope - (`InstrumentationScope.Name` in OTLP). """ OTEL_SCOPE_VERSION = "otel.scope.version" """ The version of the instrumentation scope - (`InstrumentationScope.Version` in OTLP). """ OTEL_LIBRARY_NAME = "otel.library.name" """ Deprecated, use the `otel.scope.name` attribute. """ OTEL_LIBRARY_VERSION = "otel.library.version" """ Deprecated, use the `otel.scope.version` attribute. """ # Manually defined deprecated attributes FAAS_ID = "faas.id" """ Deprecated, use the `cloud.resource.id` attribute. """ @deprecated( "Use :py:const:`opentelemetry.semconv._incubating.attributes.CloudProviderValues` instead. Deprecated since version 1.25.0.", ) class CloudProviderValues(Enum): ALIBABA_CLOUD = "alibaba_cloud" """Alibaba Cloud.""" AWS = "aws" """Amazon Web Services.""" AZURE = "azure" """Microsoft Azure.""" GCP = "gcp" """Google Cloud Platform.""" HEROKU = "heroku" """Heroku Platform as a Service.""" IBM_CLOUD = "ibm_cloud" """IBM Cloud.""" TENCENT_CLOUD = "tencent_cloud" """Tencent Cloud.""" @deprecated( "Use :py:const:`opentelemetry.semconv._incubating.attributes.CloudPlatformValues` instead. Deprecated since version 1.25.0.", ) class CloudPlatformValues(Enum): ALIBABA_CLOUD_ECS = "alibaba_cloud_ecs" """Alibaba Cloud Elastic Compute Service.""" ALIBABA_CLOUD_FC = "alibaba_cloud_fc" """Alibaba Cloud Function Compute.""" ALIBABA_CLOUD_OPENSHIFT = "alibaba_cloud_openshift" """Red Hat OpenShift on Alibaba Cloud.""" AWS_EC2 = "aws_ec2" """AWS Elastic Compute Cloud.""" AWS_ECS = "aws_ecs" """AWS Elastic Container Service.""" AWS_EKS = "aws_eks" """AWS Elastic Kubernetes Service.""" AWS_LAMBDA = "aws_lambda" """AWS Lambda.""" AWS_ELASTIC_BEANSTALK = "aws_elastic_beanstalk" """AWS Elastic Beanstalk.""" AWS_APP_RUNNER = "aws_app_runner" """AWS App Runner.""" AWS_OPENSHIFT = "aws_openshift" """Red Hat OpenShift on AWS (ROSA).""" AZURE_VM = "azure_vm" """Azure Virtual Machines.""" AZURE_CONTAINER_INSTANCES = "azure_container_instances" """Azure Container Instances.""" AZURE_AKS = "azure_aks" """Azure Kubernetes Service.""" AZURE_FUNCTIONS = "azure_functions" """Azure Functions.""" AZURE_APP_SERVICE = "azure_app_service" """Azure App Service.""" AZURE_OPENSHIFT = "azure_openshift" """Azure Red Hat OpenShift.""" GCP_BARE_METAL_SOLUTION = "gcp_bare_metal_solution" """Google Bare Metal Solution (BMS).""" GCP_COMPUTE_ENGINE = "gcp_compute_engine" """Google Cloud Compute Engine (GCE).""" GCP_CLOUD_RUN = "gcp_cloud_run" """Google Cloud Run.""" GCP_KUBERNETES_ENGINE = "gcp_kubernetes_engine" """Google Cloud Kubernetes Engine (GKE).""" GCP_CLOUD_FUNCTIONS = "gcp_cloud_functions" """Google Cloud Functions (GCF).""" GCP_APP_ENGINE = "gcp_app_engine" """Google Cloud App Engine (GAE).""" GCP_OPENSHIFT = "gcp_openshift" """Red Hat OpenShift on Google Cloud.""" IBM_CLOUD_OPENSHIFT = "ibm_cloud_openshift" """Red Hat OpenShift on IBM Cloud.""" TENCENT_CLOUD_CVM = "tencent_cloud_cvm" """Tencent Cloud Cloud Virtual Machine (CVM).""" TENCENT_CLOUD_EKS = "tencent_cloud_eks" """Tencent Cloud Elastic Kubernetes Service (EKS).""" TENCENT_CLOUD_SCF = "tencent_cloud_scf" """Tencent Cloud Serverless Cloud Function (SCF).""" @deprecated( "Use :py:const:`opentelemetry.semconv._incubating.attributes.AwsEcsLaunchtypeValues` instead. Deprecated since version 1.25.0.", ) class AwsEcsLaunchtypeValues(Enum): EC2 = "ec2" """ec2.""" FARGATE = "fargate" """fargate.""" @deprecated( "Use :py:const:`opentelemetry.semconv._incubating.attributes.HostArchValues` instead. Deprecated since version 1.25.0.", ) class HostArchValues(Enum): AMD64 = "amd64" """AMD64.""" ARM32 = "arm32" """ARM32.""" ARM64 = "arm64" """ARM64.""" IA64 = "ia64" """Itanium.""" PPC32 = "ppc32" """32-bit PowerPC.""" PPC64 = "ppc64" """64-bit PowerPC.""" S390X = "s390x" """IBM z/Architecture.""" X86 = "x86" """32-bit x86.""" @deprecated( "Use :py:const:`opentelemetry.semconv._incubating.attributes.OsTypeValues` instead. Deprecated since version 1.25.0.", ) class OsTypeValues(Enum): WINDOWS = "windows" """Microsoft Windows.""" LINUX = "linux" """Linux.""" DARWIN = "darwin" """Apple Darwin.""" FREEBSD = "freebsd" """FreeBSD.""" NETBSD = "netbsd" """NetBSD.""" OPENBSD = "openbsd" """OpenBSD.""" DRAGONFLYBSD = "dragonflybsd" """DragonFly BSD.""" HPUX = "hpux" """HP-UX (Hewlett Packard Unix).""" AIX = "aix" """AIX (Advanced Interactive eXecutive).""" SOLARIS = "solaris" """SunOS, Oracle Solaris.""" Z_OS = "z_os" """IBM z/OS.""" @deprecated( "Use :py:const:`opentelemetry.semconv.attributes.TelemetrySdkLanguageValues` instead. Deprecated since version 1.25.0.", ) class TelemetrySdkLanguageValues(Enum): CPP = "cpp" """cpp.""" DOTNET = "dotnet" """dotnet.""" ERLANG = "erlang" """erlang.""" GO = "go" """go.""" JAVA = "java" """java.""" NODEJS = "nodejs" """nodejs.""" PHP = "php" """php.""" PYTHON = "python" """python.""" RUBY = "ruby" """ruby.""" RUST = "rust" """rust.""" SWIFT = "swift" """swift.""" WEBJS = "webjs" """webjs.""" python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/src/opentelemetry/semconv/schemas.py000066400000000000000000000057611527070070700342260ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from enum import Enum class Schemas(Enum): V1_21_0 = "https://opentelemetry.io/schemas/1.21.0" """ The URL of the OpenTelemetry schema version 1.21.0. """ V1_23_1 = "https://opentelemetry.io/schemas/1.23.1" """ The URL of the OpenTelemetry schema version 1.23.1. """ V1_25_0 = "https://opentelemetry.io/schemas/1.25.0" """ The URL of the OpenTelemetry schema version 1.25.0. """ V1_26_0 = "https://opentelemetry.io/schemas/1.26.0" """ The URL of the OpenTelemetry schema version 1.26.0. """ V1_27_0 = "https://opentelemetry.io/schemas/1.27.0" """ The URL of the OpenTelemetry schema version 1.27.0. """ V1_28_0 = "https://opentelemetry.io/schemas/1.28.0" """ The URL of the OpenTelemetry schema version 1.28.0. """ V1_29_0 = "https://opentelemetry.io/schemas/1.29.0" """ The URL of the OpenTelemetry schema version 1.29.0. """ V1_30_0 = "https://opentelemetry.io/schemas/1.30.0" """ The URL of the OpenTelemetry schema version 1.30.0. """ V1_31_0 = "https://opentelemetry.io/schemas/1.31.0" """ The URL of the OpenTelemetry schema version 1.31.0. """ V1_32_0 = "https://opentelemetry.io/schemas/1.32.0" """ The URL of the OpenTelemetry schema version 1.32.0. """ V1_33_0 = "https://opentelemetry.io/schemas/1.33.0" """ The URL of the OpenTelemetry schema version 1.33.0. """ V1_34_0 = "https://opentelemetry.io/schemas/1.34.0" """ The URL of the OpenTelemetry schema version 1.34.0. """ V1_36_0 = "https://opentelemetry.io/schemas/1.36.0" """ The URL of the OpenTelemetry schema version 1.36.0. """ V1_37_0 = "https://opentelemetry.io/schemas/1.37.0" """ The URL of the OpenTelemetry schema version 1.37.0. """ V1_38_0 = "https://opentelemetry.io/schemas/1.38.0" """ The URL of the OpenTelemetry schema version 1.38.0. """ V1_39_0 = "https://opentelemetry.io/schemas/1.39.0" """ The URL of the OpenTelemetry schema version 1.39.0. """ V1_40_0 = "https://opentelemetry.io/schemas/1.40.0" """ The URL of the OpenTelemetry schema version 1.40.0. """ V1_41_0 = "https://opentelemetry.io/schemas/1.41.0" """ The URL of the OpenTelemetry schema version 1.41.0. """ V1_41_1 = "https://opentelemetry.io/schemas/1.41.1" """ The URL of the OpenTelemetry schema version 1.41.1. """ V1_42_0 = "https://opentelemetry.io/schemas/1.42.0" """ The URL of the OpenTelemetry schema version 1.42.0. """ V1_43_0 = "https://opentelemetry.io/schemas/1.43.0" """ The URL of the OpenTelemetry schema version 1.43.0. """ V1_44_0 = "https://opentelemetry.io/schemas/1.44.0" """ The URL of the OpenTelemetry schema version 1.44.0. """ # when generating new semantic conventions, # make sure to add new versions version here. python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/src/opentelemetry/semconv/trace/000077500000000000000000000000001527070070700333165ustar00rootroot00000000000000__init__.py000066400000000000000000002060221527070070700353520ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/src/opentelemetry/semconv/trace# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # pylint: disable=too-many-lines from enum import Enum from typing_extensions import deprecated @deprecated( "Use attributes defined in the :py:const:`opentelemetry.semconv.attributes` and :py:const:`opentelemetry.semconv._incubating.attributes` modules instead. Deprecated since version 1.25.0.", ) class SpanAttributes: SCHEMA_URL = "https://opentelemetry.io/schemas/1.21.0" """ The URL of the OpenTelemetry schema for these keys and values. """ CLIENT_ADDRESS = "client.address" """ Client address - unix domain socket name, IPv4 or IPv6 address. Note: When observed from the server side, and when communicating through an intermediary, `client.address` SHOULD represent client address behind any intermediaries (e.g. proxies) if it's available. """ CLIENT_PORT = "client.port" """ Client port number. Note: When observed from the server side, and when communicating through an intermediary, `client.port` SHOULD represent client port behind any intermediaries (e.g. proxies) if it's available. """ CLIENT_SOCKET_ADDRESS = "client.socket.address" """ Immediate client peer address - unix domain socket name, IPv4 or IPv6 address. """ CLIENT_SOCKET_PORT = "client.socket.port" """ Immediate client peer port number. """ HTTP_METHOD = "http.method" """ Deprecated, use `http.request.method` instead. """ HTTP_STATUS_CODE = "http.status_code" """ Deprecated, use `http.response.status_code` instead. """ HTTP_SCHEME = "http.scheme" """ Deprecated, use `url.scheme` instead. """ HTTP_URL = "http.url" """ Deprecated, use `url.full` instead. """ HTTP_TARGET = "http.target" """ Deprecated, use `url.path` and `url.query` instead. """ HTTP_REQUEST_CONTENT_LENGTH = "http.request_content_length" """ Deprecated, use `http.request.body.size` instead. """ HTTP_RESPONSE_CONTENT_LENGTH = "http.response_content_length" """ Deprecated, use `http.response.body.size` instead. """ NET_SOCK_PEER_NAME = "net.sock.peer.name" """ Deprecated, use `server.socket.domain` on client spans. """ NET_SOCK_PEER_ADDR = "net.sock.peer.addr" """ Deprecated, use `server.socket.address` on client spans and `client.socket.address` on server spans. """ NET_SOCK_PEER_PORT = "net.sock.peer.port" """ Deprecated, use `server.socket.port` on client spans and `client.socket.port` on server spans. """ NET_PEER_NAME = "net.peer.name" """ Deprecated, use `server.address` on client spans and `client.address` on server spans. """ NET_PEER_PORT = "net.peer.port" """ Deprecated, use `server.port` on client spans and `client.port` on server spans. """ NET_HOST_NAME = "net.host.name" """ Deprecated, use `server.address`. """ NET_HOST_PORT = "net.host.port" """ Deprecated, use `server.port`. """ NET_SOCK_HOST_ADDR = "net.sock.host.addr" """ Deprecated, use `server.socket.address`. """ NET_SOCK_HOST_PORT = "net.sock.host.port" """ Deprecated, use `server.socket.port`. """ NET_TRANSPORT = "net.transport" """ Deprecated, use `network.transport`. """ NET_PROTOCOL_NAME = "net.protocol.name" """ Deprecated, use `network.protocol.name`. """ NET_PROTOCOL_VERSION = "net.protocol.version" """ Deprecated, use `network.protocol.version`. """ NET_SOCK_FAMILY = "net.sock.family" """ Deprecated, use `network.transport` and `network.type`. """ DESTINATION_DOMAIN = "destination.domain" """ The domain name of the destination system. Note: This value may be a host name, a fully qualified domain name, or another host naming format. """ DESTINATION_ADDRESS = "destination.address" """ Peer address, for example IP address or UNIX socket name. """ DESTINATION_PORT = "destination.port" """ Peer port number. """ EXCEPTION_TYPE = "exception.type" """ The type of the exception (its fully-qualified class name, if applicable). The dynamic type of the exception should be preferred over the static type in languages that support it. """ EXCEPTION_MESSAGE = "exception.message" """ The exception message. """ EXCEPTION_STACKTRACE = "exception.stacktrace" """ A stacktrace as a string in the natural representation for the language runtime. The representation is to be determined and documented by each language SIG. """ HTTP_REQUEST_METHOD = "http.request.method" """ HTTP request method. Note: HTTP request method value SHOULD be "known" to the instrumentation. By default, this convention defines "known" methods as the ones listed in [RFC9110](https://www.rfc-editor.org/rfc/rfc9110.html#name-methods) and the PATCH method defined in [RFC5789](https://www.rfc-editor.org/rfc/rfc5789.html). If the HTTP request method is not known to instrumentation, it MUST set the `http.request.method` attribute to `_OTHER` and, except if reporting a metric, MUST set the exact method received in the request line as value of the `http.request.method_original` attribute. If the HTTP instrumentation could end up converting valid HTTP request methods to `_OTHER`, then it MUST provide a way to override the list of known HTTP methods. If this override is done via environment variable, then the environment variable MUST be named OTEL_INSTRUMENTATION_HTTP_KNOWN_METHODS and support a comma-separated list of case-sensitive known HTTP methods (this list MUST be a full override of the default known method, it is not a list of known methods in addition to the defaults). HTTP method names are case-sensitive and `http.request.method` attribute value MUST match a known HTTP method name exactly. Instrumentations for specific web frameworks that consider HTTP methods to be case insensitive, SHOULD populate a canonical equivalent. Tracing instrumentations that do so, MUST also set `http.request.method_original` to the original value. """ HTTP_RESPONSE_STATUS_CODE = "http.response.status_code" """ [HTTP response status code](https://tools.ietf.org/html/rfc7231#section-6). """ NETWORK_PROTOCOL_NAME = "network.protocol.name" """ [OSI Application Layer](https://osi-model.com/application-layer/) or non-OSI equivalent. The value SHOULD be normalized to lowercase. """ NETWORK_PROTOCOL_VERSION = "network.protocol.version" """ Version of the application layer protocol used. See note below. Note: `network.protocol.version` refers to the version of the protocol used and might be different from the protocol client's version. If the HTTP client used has a version of `0.27.2`, but sends HTTP version `1.1`, this attribute should be set to `1.1`. """ SERVER_ADDRESS = "server.address" """ Host identifier of the ["URI origin"](https://www.rfc-editor.org/rfc/rfc9110.html#name-uri-origin) HTTP request is sent to. Note: Determined by using the first of the following that applies - Host identifier of the [request target](https://www.rfc-editor.org/rfc/rfc9110.html#target.resource) if it's sent in absolute-form - Host identifier of the `Host` header SHOULD NOT be set if capturing it would require an extra DNS lookup. """ SERVER_PORT = "server.port" """ Port identifier of the ["URI origin"](https://www.rfc-editor.org/rfc/rfc9110.html#name-uri-origin) HTTP request is sent to. Note: When [request target](https://www.rfc-editor.org/rfc/rfc9110.html#target.resource) is absolute URI, `server.port` MUST match URI port identifier, otherwise it MUST match `Host` header port identifier. """ HTTP_ROUTE = "http.route" """ The matched route (path template in the format used by the respective server framework). See note below. Note: MUST NOT be populated when this is not supported by the HTTP server framework as the route attribute should have low-cardinality and the URI path can NOT substitute it. SHOULD include the [application root](/docs/http/http-spans.md#http-server-definitions) if there is one. """ URL_SCHEME = "url.scheme" """ The [URI scheme](https://www.rfc-editor.org/rfc/rfc3986#section-3.1) component identifying the used protocol. """ EVENT_NAME = "event.name" """ The name identifies the event. """ EVENT_DOMAIN = "event.domain" """ The domain identifies the business context for the events. Note: Events across different domains may have same `event.name`, yet be unrelated events. """ LOG_RECORD_UID = "log.record.uid" """ A unique identifier for the Log Record. Note: If an id is provided, other log records with the same id will be considered duplicates and can be removed safely. This means, that two distinguishable log records MUST have different values. The id MAY be an [Universally Unique Lexicographically Sortable Identifier (ULID)](https://github.com/ulid/spec), but other identifiers (e.g. UUID) may be used as needed. """ FEATURE_FLAG_KEY = "feature_flag.key" """ The unique identifier of the feature flag. """ FEATURE_FLAG_PROVIDER_NAME = "feature_flag.provider_name" """ The name of the service provider that performs the flag evaluation. """ FEATURE_FLAG_VARIANT = "feature_flag.variant" """ SHOULD be a semantic identifier for a value. If one is unavailable, a stringified version of the value can be used. Note: A semantic identifier, commonly referred to as a variant, provides a means for referring to a value without including the value itself. This can provide additional context for understanding the meaning behind a value. For example, the variant `red` maybe be used for the value `#c05543`. A stringified version of the value can be used in situations where a semantic identifier is unavailable. String representation of the value should be determined by the implementer. """ LOG_IOSTREAM = "log.iostream" """ The stream associated with the log. See below for a list of well-known values. """ LOG_FILE_NAME = "log.file.name" """ The basename of the file. """ LOG_FILE_PATH = "log.file.path" """ The full path to the file. """ LOG_FILE_NAME_RESOLVED = "log.file.name_resolved" """ The basename of the file, with symlinks resolved. """ LOG_FILE_PATH_RESOLVED = "log.file.path_resolved" """ The full path to the file, with symlinks resolved. """ SERVER_SOCKET_ADDRESS = "server.socket.address" """ Physical server IP address or Unix socket address. If set from the client, should simply use the socket's peer address, and not attempt to find any actual server IP (i.e., if set from client, this may represent some proxy server instead of the logical server). """ POOL = "pool" """ Name of the buffer pool. Note: Pool names are generally obtained via [BufferPoolMXBean#getName()](https://docs.oracle.com/en/java/javase/11/docs/api/java.management/java/lang/management/BufferPoolMXBean.html#getName()). """ TYPE = "type" """ The type of memory. """ SERVER_SOCKET_DOMAIN = "server.socket.domain" """ The domain name of an immediate peer. Note: Typically observed from the client side, and represents a proxy or other intermediary domain name. """ SERVER_SOCKET_PORT = "server.socket.port" """ Physical server port. """ SOURCE_DOMAIN = "source.domain" """ The domain name of the source system. Note: This value may be a host name, a fully qualified domain name, or another host naming format. """ SOURCE_ADDRESS = "source.address" """ Source address, for example IP address or Unix socket name. """ SOURCE_PORT = "source.port" """ Source port number. """ AWS_LAMBDA_INVOKED_ARN = "aws.lambda.invoked_arn" """ The full invoked ARN as provided on the `Context` passed to the function (`Lambda-Runtime-Invoked-Function-Arn` header on the `/runtime/invocation/next` applicable). Note: This may be different from `cloud.resource_id` if an alias is involved. """ CLOUDEVENTS_EVENT_ID = "cloudevents.event_id" """ The [event_id](https://github.com/cloudevents/spec/blob/v1.0.2/cloudevents/spec.md#id) uniquely identifies the event. """ CLOUDEVENTS_EVENT_SOURCE = "cloudevents.event_source" """ The [source](https://github.com/cloudevents/spec/blob/v1.0.2/cloudevents/spec.md#source-1) identifies the context in which an event happened. """ CLOUDEVENTS_EVENT_SPEC_VERSION = "cloudevents.event_spec_version" """ The [version of the CloudEvents specification](https://github.com/cloudevents/spec/blob/v1.0.2/cloudevents/spec.md#specversion) which the event uses. """ CLOUDEVENTS_EVENT_TYPE = "cloudevents.event_type" """ The [event_type](https://github.com/cloudevents/spec/blob/v1.0.2/cloudevents/spec.md#type) contains a value describing the type of event related to the originating occurrence. """ CLOUDEVENTS_EVENT_SUBJECT = "cloudevents.event_subject" """ The [subject](https://github.com/cloudevents/spec/blob/v1.0.2/cloudevents/spec.md#subject) of the event in the context of the event producer (identified by source). """ OPENTRACING_REF_TYPE = "opentracing.ref_type" """ Parent-child Reference type. Note: The causal relationship between a child Span and a parent Span. """ DB_SYSTEM = "db.system" """ An identifier for the database management system (DBMS) product being used. See below for a list of well-known identifiers. """ DB_CONNECTION_STRING = "db.connection_string" """ The connection string used to connect to the database. It is recommended to remove embedded credentials. """ DB_USER = "db.user" """ Username for accessing the database. """ DB_JDBC_DRIVER_CLASSNAME = "db.jdbc.driver_classname" """ The fully-qualified class name of the [Java Database Connectivity (JDBC)](https://docs.oracle.com/javase/8/docs/technotes/guides/jdbc/) driver used to connect. """ DB_NAME = "db.name" """ This attribute is used to report the name of the database being accessed. For commands that switch the database, this should be set to the target database (even if the command fails). Note: In some SQL databases, the database name to be used is called "schema name". In case there are multiple layers that could be considered for database name (e.g. Oracle instance name and schema name), the database name to be used is the more specific layer (e.g. Oracle schema name). """ DB_STATEMENT = "db.statement" """ The database statement being executed. """ DB_OPERATION = "db.operation" """ The name of the operation being executed, e.g. the [MongoDB command name](https://docs.mongodb.com/manual/reference/command/#database-operations) such as `findAndModify`, or the SQL keyword. Note: When setting this to an SQL keyword, it is not recommended to attempt any client-side parsing of `db.statement` just to get this property, but it should be set if the operation name is provided by the library being instrumented. If the SQL statement has an ambiguous operation, or performs more than one operation, this value may be omitted. """ NETWORK_TRANSPORT = "network.transport" """ [OSI Transport Layer](https://osi-model.com/transport-layer/) or [Inter-process Communication method](https://en.wikipedia.org/wiki/Inter-process_communication). The value SHOULD be normalized to lowercase. """ NETWORK_TYPE = "network.type" """ [OSI Network Layer](https://osi-model.com/network-layer/) or non-OSI equivalent. The value SHOULD be normalized to lowercase. """ DB_MSSQL_INSTANCE_NAME = "db.mssql.instance_name" """ The Microsoft SQL Server [instance name](https://docs.microsoft.com/en-us/sql/connect/jdbc/building-the-connection-url?view=sql-server-ver15) connecting to. This name is used to determine the port of a named instance. Note: If setting a `db.mssql.instance_name`, `server.port` is no longer required (but still recommended if non-standard). """ DB_CASSANDRA_PAGE_SIZE = "db.cassandra.page_size" """ The fetch size used for paging, i.e. how many rows will be returned at once. """ DB_CASSANDRA_CONSISTENCY_LEVEL = "db.cassandra.consistency_level" """ The consistency level of the query. Based on consistency values from [CQL](https://docs.datastax.com/en/cassandra-oss/3.0/cassandra/dml/dmlConfigConsistency.html). """ DB_CASSANDRA_TABLE = "db.cassandra.table" """ The name of the primary table that the operation is acting upon, including the keyspace name (if applicable). Note: This mirrors the db.sql.table attribute but references cassandra rather than sql. It is not recommended to attempt any client-side parsing of `db.statement` just to get this property, but it should be set if it is provided by the library being instrumented. If the operation is acting upon an anonymous table, or more than one table, this value MUST NOT be set. """ DB_CASSANDRA_IDEMPOTENCE = "db.cassandra.idempotence" """ Whether or not the query is idempotent. """ DB_CASSANDRA_SPECULATIVE_EXECUTION_COUNT = "db.cassandra.speculative_execution_count" """ The number of times a query was speculatively executed. Not set or `0` if the query was not executed speculatively. """ DB_CASSANDRA_COORDINATOR_ID = "db.cassandra.coordinator.id" """ The ID of the coordinating node for a query. """ DB_CASSANDRA_COORDINATOR_DC = "db.cassandra.coordinator.dc" """ The data center of the coordinating node for a query. """ DB_REDIS_DATABASE_INDEX = "db.redis.database_index" """ The index of the database being accessed as used in the [`SELECT` command](https://redis.io/commands/select), provided as an integer. To be used instead of the generic `db.name` attribute. """ DB_MONGODB_COLLECTION = "db.mongodb.collection" """ The collection being accessed within the database stated in `db.name`. """ URL_FULL = "url.full" """ Absolute URL describing a network resource according to [RFC3986](https://www.rfc-editor.org/rfc/rfc3986). Note: For network calls, URL usually has `scheme://host[:port][path][?query][#fragment]` format, where the fragment is not transmitted over HTTP, but if it is known, it should be included nevertheless. `url.full` MUST NOT contain credentials passed via URL in form of `https://username:password@www.example.com/`. In such case username and password should be redacted and attribute's value should be `https://REDACTED:REDACTED@www.example.com/`. `url.full` SHOULD capture the absolute URL when it is available (or can be reconstructed) and SHOULD NOT be validated or modified except for sanitizing purposes. """ DB_SQL_TABLE = "db.sql.table" """ The name of the primary table that the operation is acting upon, including the database name (if applicable). Note: It is not recommended to attempt any client-side parsing of `db.statement` just to get this property, but it should be set if it is provided by the library being instrumented. If the operation is acting upon an anonymous table, or more than one table, this value MUST NOT be set. """ DB_COSMOSDB_CLIENT_ID = "db.cosmosdb.client_id" """ Unique Cosmos client instance id. """ DB_COSMOSDB_OPERATION_TYPE = "db.cosmosdb.operation_type" """ CosmosDB Operation Type. """ USER_AGENT_ORIGINAL = "user_agent.original" """ Full user-agent string is generated by Cosmos DB SDK. Note: The user-agent value is generated by SDK which is a combination of
    `sdk_version` : Current version of SDK. e.g. 'cosmos-netstandard-sdk/3.23.0'
    `direct_pkg_version` : Direct package version used by Cosmos DB SDK. e.g. '3.23.1'
    `number_of_client_instances` : Number of cosmos client instances created by the application. e.g. '1'
    `type_of_machine_architecture` : Machine architecture. e.g. 'X64'
    `operating_system` : Operating System. e.g. 'Linux 5.4.0-1098-azure 104 18'
    `runtime_framework` : Runtime Framework. e.g. '.NET Core 3.1.32'
    `failover_information` : Generated key to determine if region failover enabled. Format Reg-{D (Disabled discovery)}-S(application region)|L(List of preferred regions)|N(None, user did not configure it). Default value is "NS". """ DB_COSMOSDB_CONNECTION_MODE = "db.cosmosdb.connection_mode" """ Cosmos client connection mode. """ DB_COSMOSDB_CONTAINER = "db.cosmosdb.container" """ Cosmos DB container name. """ DB_COSMOSDB_REQUEST_CONTENT_LENGTH = "db.cosmosdb.request_content_length" """ Request payload size in bytes. """ DB_COSMOSDB_STATUS_CODE = "db.cosmosdb.status_code" """ Cosmos DB status code. """ DB_COSMOSDB_SUB_STATUS_CODE = "db.cosmosdb.sub_status_code" """ Cosmos DB sub status code. """ DB_COSMOSDB_REQUEST_CHARGE = "db.cosmosdb.request_charge" """ RU consumed for that operation. """ OTEL_STATUS_CODE = "otel.status_code" """ Name of the code, either "OK" or "ERROR". MUST NOT be set if the status code is UNSET. """ OTEL_STATUS_DESCRIPTION = "otel.status_description" """ Description of the Status if it has a value, otherwise not set. """ FAAS_TRIGGER = "faas.trigger" """ Type of the trigger which caused this function invocation. Note: For the server/consumer span on the incoming side, `faas.trigger` MUST be set. Clients invoking FaaS instances usually cannot set `faas.trigger`, since they would typically need to look in the payload to determine the event type. If clients set it, it should be the same as the trigger that corresponding incoming would have (i.e., this has nothing to do with the underlying transport used to make the API call to invoke the lambda, which is often HTTP). """ FAAS_INVOCATION_ID = "faas.invocation_id" """ The invocation ID of the current function invocation. """ CLOUD_RESOURCE_ID = "cloud.resource_id" """ Cloud provider-specific native identifier of the monitored cloud resource (e.g. an [ARN](https://docs.aws.amazon.com/general/latest/gr/aws-arns-and-namespaces.html) on AWS, a [fully qualified resource ID](https://learn.microsoft.com/en-us/rest/api/resources/resources/get-by-id) on Azure, a [full resource name](https://cloud.google.com/apis/design/resource_names#full_resource_name) on GCP). Note: 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. The exact value to use for `cloud.resource_id` depends on the cloud provider. The following well-known definitions MUST be used if you set this attribute and they apply: * **AWS Lambda:** The function [ARN](https://docs.aws.amazon.com/general/latest/gr/aws-arns-and-namespaces.html). Take care not to use the "invoked ARN" directly but replace any [alias suffix](https://docs.aws.amazon.com/lambda/latest/dg/configuration-aliases.html) with the resolved function version, as the same runtime instance may be invokable with multiple different aliases. * **GCP:** The [URI of the resource](https://cloud.google.com/iam/docs/full-resource-names) * **Azure:** The [Fully Qualified Resource ID](https://docs.microsoft.com/en-us/rest/api/resources/resources/get-by-id) of the invoked function, *not* the function app, having the form `/subscriptions//resourceGroups//providers/Microsoft.Web/sites//functions/`. This means that a span attribute MUST be used, as an Azure function app can host multiple functions that would usually share a TracerProvider. """ FAAS_DOCUMENT_COLLECTION = "faas.document.collection" """ The name of the source on which the triggering operation was performed. For example, in Cloud Storage or S3 corresponds to the bucket name, and in Cosmos DB to the database name. """ FAAS_DOCUMENT_OPERATION = "faas.document.operation" """ Describes the type of the operation that was performed on the data. """ FAAS_DOCUMENT_TIME = "faas.document.time" """ A string containing the time when the data was accessed in the [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format expressed in [UTC](https://www.w3.org/TR/NOTE-datetime). """ FAAS_DOCUMENT_NAME = "faas.document.name" """ The document name/table subjected to the operation. For example, in Cloud Storage or S3 is the name of the file, and in Cosmos DB the table name. """ URL_PATH = "url.path" """ The [URI path](https://www.rfc-editor.org/rfc/rfc3986#section-3.3) component. Note: When missing, the value is assumed to be `/`. """ URL_QUERY = "url.query" """ The [URI query](https://www.rfc-editor.org/rfc/rfc3986#section-3.4) component. Note: Sensitive content provided in query string SHOULD be scrubbed when instrumentations can identify it. """ MESSAGING_SYSTEM = "messaging.system" """ A string identifying the messaging system. """ MESSAGING_OPERATION = "messaging.operation" """ A string identifying the kind of messaging operation as defined in the [Operation names](#operation-names) section above. Note: If a custom value is used, it MUST be of low cardinality. """ MESSAGING_BATCH_MESSAGE_COUNT = "messaging.batch.message_count" """ The number of messages sent, received, or processed in the scope of the batching operation. Note: Instrumentations SHOULD NOT set `messaging.batch.message_count` on spans that operate with a single message. When a messaging client library supports both batch and single-message API for the same operation, instrumentations SHOULD use `messaging.batch.message_count` for batching APIs and SHOULD NOT use it for single-message APIs. """ MESSAGING_CLIENT_ID = "messaging.client_id" """ A unique identifier for the client that consumes or produces a message. """ MESSAGING_DESTINATION_NAME = "messaging.destination.name" """ The message destination name. Note: Destination name SHOULD uniquely identify a specific queue, topic or other entity within the broker. If the broker does not have such notion, the destination name SHOULD uniquely identify the broker. """ MESSAGING_DESTINATION_TEMPLATE = "messaging.destination.template" """ Low cardinality representation of the messaging destination name. Note: Destination names could be constructed from templates. An example would be a destination name involving a user name or product id. Although the destination name in this case is of high cardinality, the underlying template is of low cardinality and can be effectively used for grouping and aggregation. """ MESSAGING_DESTINATION_TEMPORARY = "messaging.destination.temporary" """ A boolean that is true if the message destination is temporary and might not exist anymore after messages are processed. """ MESSAGING_DESTINATION_ANONYMOUS = "messaging.destination.anonymous" """ A boolean that is true if the message destination is anonymous (could be unnamed or have auto-generated name). """ MESSAGING_MESSAGE_ID = "messaging.message.id" """ A value used by the messaging system as an identifier for the message, represented as a string. """ MESSAGING_MESSAGE_CONVERSATION_ID = "messaging.message.conversation_id" """ The [conversation ID](#conversations) identifying the conversation to which the message belongs, represented as a string. Sometimes called "Correlation ID". """ MESSAGING_MESSAGE_PAYLOAD_SIZE_BYTES = "messaging.message.payload_size_bytes" """ The (uncompressed) size of the message payload in bytes. Also use this attribute if it is unknown whether the compressed or uncompressed payload size is reported. """ MESSAGING_MESSAGE_PAYLOAD_COMPRESSED_SIZE_BYTES = "messaging.message.payload_compressed_size_bytes" """ The compressed size of the message payload in bytes. """ FAAS_TIME = "faas.time" """ A string containing the function invocation time in the [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format expressed in [UTC](https://www.w3.org/TR/NOTE-datetime). """ FAAS_CRON = "faas.cron" """ A string containing the schedule period as [Cron Expression](https://docs.oracle.com/cd/E12058_01/doc/doc.1014/e12030/cron_expressions.htm). """ FAAS_COLDSTART = "faas.coldstart" """ A boolean that is true if the serverless function is executed for the first time (aka cold-start). """ FAAS_INVOKED_NAME = "faas.invoked_name" """ The name of the invoked function. Note: SHOULD be equal to the `faas.name` resource attribute of the invoked function. """ FAAS_INVOKED_PROVIDER = "faas.invoked_provider" """ The cloud provider of the invoked function. Note: SHOULD be equal to the `cloud.provider` resource attribute of the invoked function. """ FAAS_INVOKED_REGION = "faas.invoked_region" """ The cloud region of the invoked function. Note: SHOULD be equal to the `cloud.region` resource attribute of the invoked function. """ NETWORK_CONNECTION_TYPE = "network.connection.type" """ The internet connection type. """ NETWORK_CONNECTION_SUBTYPE = "network.connection.subtype" """ This describes more details regarding the connection.type. It may be the type of cell technology connection, but it could be used for describing details about a wifi connection. """ NETWORK_CARRIER_NAME = "network.carrier.name" """ The name of the mobile carrier. """ NETWORK_CARRIER_MCC = "network.carrier.mcc" """ The mobile carrier country code. """ NETWORK_CARRIER_MNC = "network.carrier.mnc" """ The mobile carrier network code. """ NETWORK_CARRIER_ICC = "network.carrier.icc" """ The ISO 3166-1 alpha-2 2-character country code associated with the mobile carrier network. """ PEER_SERVICE = "peer.service" """ The [`service.name`](/docs/resource/README.md#service) of the remote service. SHOULD be equal to the actual `service.name` resource attribute of the remote service if any. """ ENDUSER_ID = "enduser.id" """ Username or client_id extracted from the access token or [Authorization](https://tools.ietf.org/html/rfc7235#section-4.2) header in the inbound request from outside the system. """ ENDUSER_ROLE = "enduser.role" """ Actual/assumed role the client is making the request under extracted from token or application security context. """ ENDUSER_SCOPE = "enduser.scope" """ Scopes or granted authorities the client currently possesses extracted from token or application security context. The value would come from the scope associated with an [OAuth 2.0 Access Token](https://tools.ietf.org/html/rfc6749#section-3.3) or an attribute value in a [SAML 2.0 Assertion](http://docs.oasis-open.org/security/saml/Post2.0/sstc-saml-tech-overview-2.0.html). """ THREAD_ID = "thread.id" """ Current "managed" thread ID (as opposed to OS thread ID). """ THREAD_NAME = "thread.name" """ Current thread name. """ CODE_FUNCTION = "code.function" """ The method or function name, or equivalent (usually rightmost part of the code unit's name). """ CODE_NAMESPACE = "code.namespace" """ The "namespace" within which `code.function` is defined. Usually the qualified class or module name, such that `code.namespace` + some separator + `code.function` form a unique identifier for the code unit. """ CODE_FILEPATH = "code.filepath" """ The source code file name that identifies the code unit as uniquely as possible (preferably an absolute file path). """ CODE_LINENO = "code.lineno" """ The line number in `code.filepath` best representing the operation. It SHOULD point within the code unit named in `code.function`. """ CODE_COLUMN = "code.column" """ The column number in `code.filepath` best representing the operation. It SHOULD point within the code unit named in `code.function`. """ HTTP_REQUEST_METHOD_ORIGINAL = "http.request.method_original" """ Original HTTP method sent by the client in the request line. """ HTTP_REQUEST_BODY_SIZE = "http.request.body.size" """ The size of the request payload body in bytes. This is the number of bytes transferred excluding headers and is often, but not always, present as the [Content-Length](https://www.rfc-editor.org/rfc/rfc9110.html#field.content-length) header. For requests using transport encoding, this should be the compressed size. """ HTTP_RESPONSE_BODY_SIZE = "http.response.body.size" """ The size of the response payload body in bytes. This is the number of bytes transferred excluding headers and is often, but not always, present as the [Content-Length](https://www.rfc-editor.org/rfc/rfc9110.html#field.content-length) header. For requests using transport encoding, this should be the compressed size. """ HTTP_RESEND_COUNT = "http.resend_count" """ The ordinal number of request resending attempt (for any reason, including redirects). Note: The resend count SHOULD be updated each time an HTTP request gets resent by the client, regardless of what was the cause of the resending (e.g. redirection, authorization failure, 503 Server Unavailable, network issues, or any other). """ RPC_SYSTEM = "rpc.system" """ The value `aws-api`. """ RPC_SERVICE = "rpc.service" """ The name of the service to which a request is made, as returned by the AWS SDK. Note: This is the logical name of the service from the RPC interface perspective, which can be different from the name of any implementing class. The `code.namespace` attribute may be used to store the latter (despite the attribute name, it may include a class name; e.g., class with method actually executing the call on the server side, RPC client stub class on the client side). """ RPC_METHOD = "rpc.method" """ The name of the operation corresponding to the request, as returned by the AWS SDK. Note: This is the logical name of the method from the RPC interface perspective, which can be different from the name of any implementing method/function. The `code.function` attribute may be used to store the latter (e.g., method actually executing the call on the server side, RPC client stub method on the client side). """ AWS_REQUEST_ID = "aws.request_id" """ The AWS request ID as returned in the response headers `x-amz-request-id` or `x-amz-requestid`. """ AWS_DYNAMODB_TABLE_NAMES = "aws.dynamodb.table_names" """ The keys in the `RequestItems` object field. """ AWS_DYNAMODB_CONSUMED_CAPACITY = "aws.dynamodb.consumed_capacity" """ The JSON-serialized value of each item in the `ConsumedCapacity` response field. """ AWS_DYNAMODB_ITEM_COLLECTION_METRICS = "aws.dynamodb.item_collection_metrics" """ The JSON-serialized value of the `ItemCollectionMetrics` response field. """ AWS_DYNAMODB_PROVISIONED_READ_CAPACITY = "aws.dynamodb.provisioned_read_capacity" """ The value of the `ProvisionedThroughput.ReadCapacityUnits` request parameter. """ AWS_DYNAMODB_PROVISIONED_WRITE_CAPACITY = "aws.dynamodb.provisioned_write_capacity" """ The value of the `ProvisionedThroughput.WriteCapacityUnits` request parameter. """ AWS_DYNAMODB_CONSISTENT_READ = "aws.dynamodb.consistent_read" """ The value of the `ConsistentRead` request parameter. """ AWS_DYNAMODB_PROJECTION = "aws.dynamodb.projection" """ The value of the `ProjectionExpression` request parameter. """ AWS_DYNAMODB_LIMIT = "aws.dynamodb.limit" """ The value of the `Limit` request parameter. """ AWS_DYNAMODB_ATTRIBUTES_TO_GET = "aws.dynamodb.attributes_to_get" """ The value of the `AttributesToGet` request parameter. """ AWS_DYNAMODB_INDEX_NAME = "aws.dynamodb.index_name" """ The value of the `IndexName` request parameter. """ AWS_DYNAMODB_SELECT = "aws.dynamodb.select" """ The value of the `Select` request parameter. """ AWS_DYNAMODB_GLOBAL_SECONDARY_INDEXES = "aws.dynamodb.global_secondary_indexes" """ The JSON-serialized value of each item of the `GlobalSecondaryIndexes` request field. """ AWS_DYNAMODB_LOCAL_SECONDARY_INDEXES = "aws.dynamodb.local_secondary_indexes" """ The JSON-serialized value of each item of the `LocalSecondaryIndexes` request field. """ AWS_DYNAMODB_EXCLUSIVE_START_TABLE = "aws.dynamodb.exclusive_start_table" """ The value of the `ExclusiveStartTableName` request parameter. """ AWS_DYNAMODB_TABLE_COUNT = "aws.dynamodb.table_count" """ The the number of items in the `TableNames` response parameter. """ AWS_DYNAMODB_SCAN_FORWARD = "aws.dynamodb.scan_forward" """ The value of the `ScanIndexForward` request parameter. """ AWS_DYNAMODB_SEGMENT = "aws.dynamodb.segment" """ The value of the `Segment` request parameter. """ AWS_DYNAMODB_TOTAL_SEGMENTS = "aws.dynamodb.total_segments" """ The value of the `TotalSegments` request parameter. """ AWS_DYNAMODB_COUNT = "aws.dynamodb.count" """ The value of the `Count` response parameter. """ AWS_DYNAMODB_SCANNED_COUNT = "aws.dynamodb.scanned_count" """ The value of the `ScannedCount` response parameter. """ AWS_DYNAMODB_ATTRIBUTE_DEFINITIONS = "aws.dynamodb.attribute_definitions" """ The JSON-serialized value of each item in the `AttributeDefinitions` request field. """ AWS_DYNAMODB_GLOBAL_SECONDARY_INDEX_UPDATES = "aws.dynamodb.global_secondary_index_updates" """ The JSON-serialized value of each item in the the `GlobalSecondaryIndexUpdates` request field. """ AWS_S3_BUCKET = "aws.s3.bucket" """ The S3 bucket name the request refers to. Corresponds to the `--bucket` parameter of the [S3 API](https://docs.aws.amazon.com/cli/latest/reference/s3api/index.html) operations. Note: The `bucket` attribute is applicable to all S3 operations that reference a bucket, i.e. that require the bucket name as a mandatory parameter. This applies to almost all S3 operations except `list-buckets`. """ AWS_S3_KEY = "aws.s3.key" """ The S3 object key the request refers to. Corresponds to the `--key` parameter of the [S3 API](https://docs.aws.amazon.com/cli/latest/reference/s3api/index.html) operations. Note: The `key` attribute is applicable to all object-related S3 operations, i.e. that require the object key as a mandatory parameter. This applies in particular to the following operations: - [copy-object](https://docs.aws.amazon.com/cli/latest/reference/s3api/copy-object.html) - [delete-object](https://docs.aws.amazon.com/cli/latest/reference/s3api/delete-object.html) - [get-object](https://docs.aws.amazon.com/cli/latest/reference/s3api/get-object.html) - [head-object](https://docs.aws.amazon.com/cli/latest/reference/s3api/head-object.html) - [put-object](https://docs.aws.amazon.com/cli/latest/reference/s3api/put-object.html) - [restore-object](https://docs.aws.amazon.com/cli/latest/reference/s3api/restore-object.html) - [select-object-content](https://docs.aws.amazon.com/cli/latest/reference/s3api/select-object-content.html) - [abort-multipart-upload](https://docs.aws.amazon.com/cli/latest/reference/s3api/abort-multipart-upload.html) - [complete-multipart-upload](https://docs.aws.amazon.com/cli/latest/reference/s3api/complete-multipart-upload.html) - [create-multipart-upload](https://docs.aws.amazon.com/cli/latest/reference/s3api/create-multipart-upload.html) - [list-parts](https://docs.aws.amazon.com/cli/latest/reference/s3api/list-parts.html) - [upload-part](https://docs.aws.amazon.com/cli/latest/reference/s3api/upload-part.html) - [upload-part-copy](https://docs.aws.amazon.com/cli/latest/reference/s3api/upload-part-copy.html). """ AWS_S3_COPY_SOURCE = "aws.s3.copy_source" """ The source object (in the form `bucket`/`key`) for the copy operation. Note: The `copy_source` attribute applies to S3 copy operations and corresponds to the `--copy-source` parameter of the [copy-object operation within the S3 API](https://docs.aws.amazon.com/cli/latest/reference/s3api/copy-object.html). This applies in particular to the following operations: - [copy-object](https://docs.aws.amazon.com/cli/latest/reference/s3api/copy-object.html) - [upload-part-copy](https://docs.aws.amazon.com/cli/latest/reference/s3api/upload-part-copy.html). """ AWS_S3_UPLOAD_ID = "aws.s3.upload_id" """ Upload ID that identifies the multipart upload. Note: The `upload_id` attribute applies to S3 multipart-upload operations and corresponds to the `--upload-id` parameter of the [S3 API](https://docs.aws.amazon.com/cli/latest/reference/s3api/index.html) multipart operations. This applies in particular to the following operations: - [abort-multipart-upload](https://docs.aws.amazon.com/cli/latest/reference/s3api/abort-multipart-upload.html) - [complete-multipart-upload](https://docs.aws.amazon.com/cli/latest/reference/s3api/complete-multipart-upload.html) - [list-parts](https://docs.aws.amazon.com/cli/latest/reference/s3api/list-parts.html) - [upload-part](https://docs.aws.amazon.com/cli/latest/reference/s3api/upload-part.html) - [upload-part-copy](https://docs.aws.amazon.com/cli/latest/reference/s3api/upload-part-copy.html). """ AWS_S3_DELETE = "aws.s3.delete" """ The delete request container that specifies the objects to be deleted. Note: The `delete` attribute is only applicable to the [delete-object](https://docs.aws.amazon.com/cli/latest/reference/s3api/delete-object.html) operation. The `delete` attribute corresponds to the `--delete` parameter of the [delete-objects operation within the S3 API](https://docs.aws.amazon.com/cli/latest/reference/s3api/delete-objects.html). """ AWS_S3_PART_NUMBER = "aws.s3.part_number" """ The part number of the part being uploaded in a multipart-upload operation. This is a positive integer between 1 and 10,000. Note: The `part_number` attribute is only applicable to the [upload-part](https://docs.aws.amazon.com/cli/latest/reference/s3api/upload-part.html) and [upload-part-copy](https://docs.aws.amazon.com/cli/latest/reference/s3api/upload-part-copy.html) operations. The `part_number` attribute corresponds to the `--part-number` parameter of the [upload-part operation within the S3 API](https://docs.aws.amazon.com/cli/latest/reference/s3api/upload-part.html). """ GRAPHQL_OPERATION_NAME = "graphql.operation.name" """ The name of the operation being executed. """ GRAPHQL_OPERATION_TYPE = "graphql.operation.type" """ The type of the operation being executed. """ GRAPHQL_DOCUMENT = "graphql.document" """ The GraphQL document being executed. Note: The value may be sanitized to exclude sensitive information. """ MESSAGING_RABBITMQ_DESTINATION_ROUTING_KEY = "messaging.rabbitmq.destination.routing_key" """ RabbitMQ message routing key. """ MESSAGING_KAFKA_MESSAGE_KEY = "messaging.kafka.message.key" """ Message keys in Kafka are used for grouping alike messages to ensure they're processed on the same partition. They differ from `messaging.message.id` in that they're not unique. If the key is `null`, the attribute MUST NOT be set. Note: If the key type is not string, it's string representation has to be supplied for the attribute. If the key has no unambiguous, canonical string form, don't include its value. """ MESSAGING_KAFKA_CONSUMER_GROUP = "messaging.kafka.consumer.group" """ Name of the Kafka Consumer Group that is handling the message. Only applies to consumers, not producers. """ MESSAGING_KAFKA_DESTINATION_PARTITION = "messaging.kafka.destination.partition" """ Partition the message is sent to. """ MESSAGING_KAFKA_MESSAGE_OFFSET = "messaging.kafka.message.offset" """ The offset of a record in the corresponding Kafka partition. """ MESSAGING_KAFKA_MESSAGE_TOMBSTONE = "messaging.kafka.message.tombstone" """ A boolean that is true if the message is a tombstone. """ MESSAGING_ROCKETMQ_NAMESPACE = "messaging.rocketmq.namespace" """ Namespace of RocketMQ resources, resources in different namespaces are individual. """ MESSAGING_ROCKETMQ_CLIENT_GROUP = "messaging.rocketmq.client_group" """ Name of the RocketMQ producer/consumer group that is handling the message. The client type is identified by the SpanKind. """ MESSAGING_ROCKETMQ_MESSAGE_DELIVERY_TIMESTAMP = "messaging.rocketmq.message.delivery_timestamp" """ The timestamp in milliseconds that the delay message is expected to be delivered to consumer. """ MESSAGING_ROCKETMQ_MESSAGE_DELAY_TIME_LEVEL = "messaging.rocketmq.message.delay_time_level" """ The delay time level for delay message, which determines the message delay time. """ MESSAGING_ROCKETMQ_MESSAGE_GROUP = "messaging.rocketmq.message.group" """ It is essential for FIFO message. Messages that belong to the same message group are always processed one by one within the same consumer group. """ MESSAGING_ROCKETMQ_MESSAGE_TYPE = "messaging.rocketmq.message.type" """ Type of message. """ MESSAGING_ROCKETMQ_MESSAGE_TAG = "messaging.rocketmq.message.tag" """ The secondary classifier of message besides topic. """ MESSAGING_ROCKETMQ_MESSAGE_KEYS = "messaging.rocketmq.message.keys" """ Key(s) of message, another way to mark message besides message id. """ MESSAGING_ROCKETMQ_CONSUMPTION_MODEL = "messaging.rocketmq.consumption_model" """ Model of message consumption. This only applies to consumer spans. """ RPC_GRPC_STATUS_CODE = "rpc.grpc.status_code" """ The [numeric status code](https://github.com/grpc/grpc/blob/v1.33.2/doc/statuscodes.md) of the gRPC request. """ RPC_JSONRPC_VERSION = "rpc.jsonrpc.version" """ Protocol version as in `jsonrpc` property of request/response. Since JSON-RPC 1.0 does not specify this, the value can be omitted. """ RPC_JSONRPC_REQUEST_ID = "rpc.jsonrpc.request_id" """ `id` property of request or response. Since protocol allows id to be int, string, `null` or missing (for notifications), value is expected to be cast to string for simplicity. Use empty string in case of `null` value. Omit entirely if this is a notification. """ RPC_JSONRPC_ERROR_CODE = "rpc.jsonrpc.error_code" """ `error.code` property of response if it is an error response. """ RPC_JSONRPC_ERROR_MESSAGE = "rpc.jsonrpc.error_message" """ `error.message` property of response if it is an error response. """ MESSAGE_TYPE = "message.type" """ Whether this is a received or sent message. """ MESSAGE_ID = "message.id" """ MUST be calculated as two different counters starting from `1` one for sent messages and one for received message. Note: This way we guarantee that the values will be consistent between different implementations. """ MESSAGE_COMPRESSED_SIZE = "message.compressed_size" """ Compressed size of the message in bytes. """ MESSAGE_UNCOMPRESSED_SIZE = "message.uncompressed_size" """ Uncompressed size of the message in bytes. """ RPC_CONNECT_RPC_ERROR_CODE = "rpc.connect_rpc.error_code" """ The [error codes](https://connect.build/docs/protocol/#error-codes) of the Connect request. Error codes are always string values. """ EXCEPTION_ESCAPED = "exception.escaped" """ SHOULD be set to true if the exception event is recorded at a point where it is known that the exception is escaping the scope of the span. Note: An exception is considered to have escaped (or left) the scope of a span, if that span is ended while the exception is still logically "in flight". This may be actually "in flight" in some languages (e.g. if the exception is passed to a Context manager's `__exit__` method in Python) but will usually be caught at the point of recording the exception in most languages. It is usually not possible to determine at the point where an exception is thrown whether it will escape the scope of a span. However, it is trivial to know that an exception will escape, if one checks for an active exception just before ending the span, as done in the [example above](#recording-an-exception). It follows that an exception may still escape the scope of the span even if the `exception.escaped` attribute was not set or set to false, since the event might have been recorded at a time where it was not clear whether the exception will escape. """ URL_FRAGMENT = "url.fragment" """ The [URI fragment](https://www.rfc-editor.org/rfc/rfc3986#section-3.5) component. """ # Manually defined deprecated attributes NET_PEER_IP = "net.peer.ip" """ Deprecated, use the `client.socket.address` attribute. """ NET_HOST_IP = "net.host.ip" """ Deprecated, use the `server.socket.address` attribute. """ HTTP_SERVER_NAME = "http.server_name" """ Deprecated, use the `server.address` attribute. """ HTTP_HOST = "http.host" """ Deprecated, use the `server.address` and `server.port` attributes. """ HTTP_RETRY_COUNT = "http.retry_count" """ Deprecated, use the `http.resend_count` attribute. """ HTTP_REQUEST_CONTENT_LENGTH_UNCOMPRESSED = "http.request_content_length_uncompressed" """ Deprecated, use the `http.request.body.size` attribute. """ HTTP_RESPONSE_CONTENT_LENGTH_UNCOMPRESSED = "http.response_content_length_uncompressed" """ Deprecated, use the `http.response.body.size` attribute. """ MESSAGING_DESTINATION = "messaging.destination" """ Deprecated, use the `messaging.destination.name` attribute. """ MESSAGING_DESTINATION_KIND = "messaging.destination_kind" """ Deprecated. """ MESSAGING_TEMP_DESTINATION = "messaging.temp_destination" """ Deprecated. Use `messaging.destination.temporary` attribute. """ MESSAGING_PROTOCOL = "messaging.protocol" """ Deprecated. Use `network.protocol.name` attribute. """ MESSAGING_PROTOCOL_VERSION = "messaging.protocol_version" """ Deprecated. Use `network.protocol.version` attribute. """ MESSAGING_URL = "messaging.url" """ Deprecated. Use `server.address` and `server.port` attributes. """ MESSAGING_CONVERSATION_ID = "messaging.conversation_id" """ Deprecated. Use `messaging.message.conversation.id` attribute. """ MESSAGING_KAFKA_PARTITION = "messaging.kafka.partition" """ Deprecated. Use `messaging.kafka.destination.partition` attribute. """ FAAS_EXECUTION = "faas.execution" """ Deprecated. Use `faas.invocation_id` attribute. """ HTTP_USER_AGENT = "http.user_agent" """ Deprecated. Use `user_agent.original` attribute. """ MESSAGING_RABBITMQ_ROUTING_KEY = "messaging.rabbitmq.routing_key" """ Deprecated. Use `messaging.rabbitmq.destination.routing_key` attribute. """ MESSAGING_KAFKA_TOMBSTONE = "messaging.kafka.tombstone" """ Deprecated. Use `messaging.kafka.destination.tombstone` attribute. """ NET_APP_PROTOCOL_NAME = "net.app.protocol.name" """ Deprecated. Use `network.protocol.name` attribute. """ NET_APP_PROTOCOL_VERSION = "net.app.protocol.version" """ Deprecated. Use `network.protocol.version` attribute. """ HTTP_CLIENT_IP = "http.client_ip" """ Deprecated. Use `client.address` attribute. """ HTTP_FLAVOR = "http.flavor" """ Deprecated. Use `network.protocol.name` and `network.protocol.version` attributes. """ NET_HOST_CONNECTION_TYPE = "net.host.connection.type" """ Deprecated. Use `network.connection.type` attribute. """ NET_HOST_CONNECTION_SUBTYPE = "net.host.connection.subtype" """ Deprecated. Use `network.connection.subtype` attribute. """ NET_HOST_CARRIER_NAME = "net.host.carrier.name" """ Deprecated. Use `network.carrier.name` attribute. """ NET_HOST_CARRIER_MCC = "net.host.carrier.mcc" """ Deprecated. Use `network.carrier.mcc` attribute. """ NET_HOST_CARRIER_MNC = "net.host.carrier.mnc" """ Deprecated. Use `network.carrier.mnc` attribute. """ MESSAGING_CONSUMER_ID = "messaging.consumer_id" """ Deprecated. Use `messaging.client_id` attribute. """ MESSAGING_KAFKA_CLIENT_ID = "messaging.kafka.client_id" """ Deprecated. Use `messaging.client_id` attribute. """ MESSAGING_ROCKETMQ_CLIENT_ID = "messaging.rocketmq.client_id" """ Deprecated. Use `messaging.client_id` attribute. """ @deprecated( "Removed from the specification in favor of `network.protocol.name` and `network.protocol.version` attributes. Deprecated since version 1.18.0.", ) class HttpFlavorValues(Enum): HTTP_1_0 = "1.0" HTTP_1_1 = "1.1" HTTP_2_0 = "2.0" HTTP_3_0 = "3.0" SPDY = "SPDY" QUIC = "QUIC" @deprecated( "Removed from the specification. Deprecated since version 1.18.0.", ) class MessagingDestinationKindValues(Enum): QUEUE = "queue" """A message sent to a queue.""" TOPIC = "topic" """A message sent to a topic.""" @deprecated( "Renamed to NetworkConnectionTypeValues. Deprecated since version 1.21.0.", ) class NetHostConnectionTypeValues(Enum): WIFI = "wifi" """wifi.""" WIRED = "wired" """wired.""" CELL = "cell" """cell.""" UNAVAILABLE = "unavailable" """unavailable.""" UNKNOWN = "unknown" """unknown.""" @deprecated( "Renamed to NetworkConnectionSubtypeValues. Deprecated since version 1.21.0.", ) class NetHostConnectionSubtypeValues(Enum): GPRS = "gprs" """GPRS.""" EDGE = "edge" """EDGE.""" UMTS = "umts" """UMTS.""" CDMA = "cdma" """CDMA.""" EVDO_0 = "evdo_0" """EVDO Rel. 0.""" EVDO_A = "evdo_a" """EVDO Rev. A.""" CDMA2000_1XRTT = "cdma2000_1xrtt" """CDMA2000 1XRTT.""" HSDPA = "hsdpa" """HSDPA.""" HSUPA = "hsupa" """HSUPA.""" HSPA = "hspa" """HSPA.""" IDEN = "iden" """IDEN.""" EVDO_B = "evdo_b" """EVDO Rev. B.""" LTE = "lte" """LTE.""" EHRPD = "ehrpd" """EHRPD.""" HSPAP = "hspap" """HSPAP.""" GSM = "gsm" """GSM.""" TD_SCDMA = "td_scdma" """TD-SCDMA.""" IWLAN = "iwlan" """IWLAN.""" NR = "nr" """5G NR (New Radio).""" NRNSA = "nrnsa" """5G NRNSA (New Radio Non-Standalone).""" LTE_CA = "lte_ca" """LTE CA.""" @deprecated( "Use :py:const:`opentelemetry.semconv.attributes.NetworkTransportValues` instead. Deprecated since version 1.25.0.", ) class NetTransportValues(Enum): IP_TCP = "ip_tcp" """ip_tcp.""" IP_UDP = "ip_udp" """ip_udp.""" PIPE = "pipe" """Named or anonymous pipe.""" INPROC = "inproc" """In-process communication.""" OTHER = "other" """Something else (non IP-based).""" @deprecated( "Use :py:const:`opentelemetry.semconv.attributes.NetworkType` instead. Deprecated since version 1.25.0.", ) class NetSockFamilyValues(Enum): INET = "inet" """IPv4 address.""" INET6 = "inet6" """IPv6 address.""" UNIX = "unix" """Unix domain socket path.""" @deprecated( "Use :py:const:`opentelemetry.semconv.attributes.HttpRequestMethodValues` instead. Deprecated since version 1.25.0.", ) class HttpRequestMethodValues(Enum): CONNECT = "CONNECT" """CONNECT method.""" DELETE = "DELETE" """DELETE method.""" GET = "GET" """GET method.""" HEAD = "HEAD" """HEAD method.""" OPTIONS = "OPTIONS" """OPTIONS method.""" PATCH = "PATCH" """PATCH method.""" POST = "POST" """POST method.""" PUT = "PUT" """PUT method.""" TRACE = "TRACE" """TRACE method.""" OTHER = "_OTHER" """Any HTTP method that the instrumentation has no prior knowledge of.""" @deprecated("Removed from the specification. Deprecated since version 1.25.0.") class EventDomainValues(Enum): BROWSER = "browser" """Events from browser apps.""" DEVICE = "device" """Events from mobile apps.""" K8S = "k8s" """Events from Kubernetes.""" @deprecated( "Use :py:const:`opentelemetry.semconv._incubating.attributes.LogIostreamValues` instead. Deprecated since version 1.25.0.", ) class LogIostreamValues(Enum): STDOUT = "stdout" """Logs from stdout stream.""" STDERR = "stderr" """Events from stderr stream.""" @deprecated("Removed from the specification. Deprecated since version 1.25.0.") class TypeValues(Enum): HEAP = "heap" """Heap memory.""" NON_HEAP = "non_heap" """Non-heap memory.""" @deprecated( "Use :py:const:`opentelemetry.semconv._incubating.attributes.OpentracingRefTypeValues` instead. Deprecated since version 1.25.0.", ) class OpentracingRefTypeValues(Enum): CHILD_OF = "child_of" """The parent Span depends on the child Span in some capacity.""" FOLLOWS_FROM = "follows_from" """The parent Span does not depend in any way on the result of the child Span.""" class DbSystemValues(Enum): OTHER_SQL = "other_sql" """Some other SQL database. Fallback only. See notes.""" MSSQL = "mssql" """Microsoft SQL Server.""" MSSQLCOMPACT = "mssqlcompact" """Microsoft SQL Server Compact.""" MYSQL = "mysql" """MySQL.""" ORACLE = "oracle" """Oracle Database.""" DB2 = "db2" """IBM Db2.""" POSTGRESQL = "postgresql" """PostgreSQL.""" REDSHIFT = "redshift" """Amazon Redshift.""" HIVE = "hive" """Apache Hive.""" CLOUDSCAPE = "cloudscape" """Cloudscape.""" HSQLDB = "hsqldb" """HyperSQL DataBase.""" PROGRESS = "progress" """Progress Database.""" MAXDB = "maxdb" """SAP MaxDB.""" HANADB = "hanadb" """SAP HANA.""" INGRES = "ingres" """Ingres.""" FIRSTSQL = "firstsql" """FirstSQL.""" EDB = "edb" """EnterpriseDB.""" CACHE = "cache" """InterSystems Caché.""" ADABAS = "adabas" """Adabas (Adaptable Database System).""" FIREBIRD = "firebird" """Firebird.""" DERBY = "derby" """Apache Derby.""" FILEMAKER = "filemaker" """FileMaker.""" INFORMIX = "informix" """Informix.""" INSTANTDB = "instantdb" """InstantDB.""" INTERBASE = "interbase" """InterBase.""" MARIADB = "mariadb" """MariaDB.""" NETEZZA = "netezza" """Netezza.""" PERVASIVE = "pervasive" """Pervasive PSQL.""" POINTBASE = "pointbase" """PointBase.""" SQLITE = "sqlite" """SQLite.""" SYBASE = "sybase" """Sybase.""" TERADATA = "teradata" """Teradata.""" VERTICA = "vertica" """Vertica.""" H2 = "h2" """H2.""" COLDFUSION = "coldfusion" """ColdFusion IMQ.""" CASSANDRA = "cassandra" """Apache Cassandra.""" HBASE = "hbase" """Apache HBase.""" MONGODB = "mongodb" """MongoDB.""" REDIS = "redis" """Redis.""" COUCHBASE = "couchbase" """Couchbase.""" COUCHDB = "couchdb" """CouchDB.""" COSMOSDB = "cosmosdb" """Microsoft Azure Cosmos DB.""" DYNAMODB = "dynamodb" """Amazon DynamoDB.""" NEO4J = "neo4j" """Neo4j.""" GEODE = "geode" """Apache Geode.""" ELASTICSEARCH = "elasticsearch" """Elasticsearch.""" MEMCACHED = "memcached" """Memcached.""" COCKROACHDB = "cockroachdb" """CockroachDB.""" OPENSEARCH = "opensearch" """OpenSearch.""" CLICKHOUSE = "clickhouse" """ClickHouse.""" SPANNER = "spanner" """Cloud Spanner.""" TRINO = "trino" """Trino.""" class NetworkTransportValues(Enum): TCP = "tcp" """TCP.""" UDP = "udp" """UDP.""" PIPE = "pipe" """Named or anonymous pipe. See note below.""" UNIX = "unix" """Unix domain socket.""" class NetworkTypeValues(Enum): IPV4 = "ipv4" """IPv4.""" IPV6 = "ipv6" """IPv6.""" class DbCassandraConsistencyLevelValues(Enum): ALL = "all" """all.""" EACH_QUORUM = "each_quorum" """each_quorum.""" QUORUM = "quorum" """quorum.""" LOCAL_QUORUM = "local_quorum" """local_quorum.""" ONE = "one" """one.""" TWO = "two" """two.""" THREE = "three" """three.""" LOCAL_ONE = "local_one" """local_one.""" ANY = "any" """any.""" SERIAL = "serial" """serial.""" LOCAL_SERIAL = "local_serial" """local_serial.""" class DbCosmosdbOperationTypeValues(Enum): INVALID = "Invalid" """invalid.""" CREATE = "Create" """create.""" PATCH = "Patch" """patch.""" READ = "Read" """read.""" READ_FEED = "ReadFeed" """read_feed.""" DELETE = "Delete" """delete.""" REPLACE = "Replace" """replace.""" EXECUTE = "Execute" """execute.""" QUERY = "Query" """query.""" HEAD = "Head" """head.""" HEAD_FEED = "HeadFeed" """head_feed.""" UPSERT = "Upsert" """upsert.""" BATCH = "Batch" """batch.""" QUERY_PLAN = "QueryPlan" """query_plan.""" EXECUTE_JAVASCRIPT = "ExecuteJavaScript" """execute_javascript.""" class DbCosmosdbConnectionModeValues(Enum): GATEWAY = "gateway" """Gateway (HTTP) connections mode.""" DIRECT = "direct" """Direct connection.""" class OtelStatusCodeValues(Enum): OK = "OK" """The operation has been validated by an Application developer or Operator to have completed successfully.""" ERROR = "ERROR" """The operation contains an error.""" class FaasTriggerValues(Enum): DATASOURCE = "datasource" """A response to some data source operation such as a database or filesystem read/write.""" HTTP = "http" """To provide an answer to an inbound HTTP request.""" PUBSUB = "pubsub" """A function is set to be executed when messages are sent to a messaging system.""" TIMER = "timer" """A function is scheduled to be executed regularly.""" OTHER = "other" """If none of the others apply.""" class FaasDocumentOperationValues(Enum): INSERT = "insert" """When a new object is created.""" EDIT = "edit" """When an object is modified.""" DELETE = "delete" """When an object is deleted.""" class MessagingOperationValues(Enum): PUBLISH = "publish" """publish.""" RECEIVE = "receive" """receive.""" PROCESS = "process" """process.""" class FaasInvokedProviderValues(Enum): ALIBABA_CLOUD = "alibaba_cloud" """Alibaba Cloud.""" AWS = "aws" """Amazon Web Services.""" AZURE = "azure" """Microsoft Azure.""" GCP = "gcp" """Google Cloud Platform.""" TENCENT_CLOUD = "tencent_cloud" """Tencent Cloud.""" class NetworkConnectionTypeValues(Enum): WIFI = "wifi" """wifi.""" WIRED = "wired" """wired.""" CELL = "cell" """cell.""" UNAVAILABLE = "unavailable" """unavailable.""" UNKNOWN = "unknown" """unknown.""" class NetworkConnectionSubtypeValues(Enum): GPRS = "gprs" """GPRS.""" EDGE = "edge" """EDGE.""" UMTS = "umts" """UMTS.""" CDMA = "cdma" """CDMA.""" EVDO_0 = "evdo_0" """EVDO Rel. 0.""" EVDO_A = "evdo_a" """EVDO Rev. A.""" CDMA2000_1XRTT = "cdma2000_1xrtt" """CDMA2000 1XRTT.""" HSDPA = "hsdpa" """HSDPA.""" HSUPA = "hsupa" """HSUPA.""" HSPA = "hspa" """HSPA.""" IDEN = "iden" """IDEN.""" EVDO_B = "evdo_b" """EVDO Rev. B.""" LTE = "lte" """LTE.""" EHRPD = "ehrpd" """EHRPD.""" HSPAP = "hspap" """HSPAP.""" GSM = "gsm" """GSM.""" TD_SCDMA = "td_scdma" """TD-SCDMA.""" IWLAN = "iwlan" """IWLAN.""" NR = "nr" """5G NR (New Radio).""" NRNSA = "nrnsa" """5G NRNSA (New Radio Non-Standalone).""" LTE_CA = "lte_ca" """LTE CA.""" class RpcSystemValues(Enum): GRPC = "grpc" """gRPC.""" JAVA_RMI = "java_rmi" """Java RMI.""" DOTNET_WCF = "dotnet_wcf" """.NET WCF.""" APACHE_DUBBO = "apache_dubbo" """Apache Dubbo.""" CONNECT_RPC = "connect_rpc" """Connect RPC.""" class GraphqlOperationTypeValues(Enum): QUERY = "query" """GraphQL query.""" MUTATION = "mutation" """GraphQL mutation.""" SUBSCRIPTION = "subscription" """GraphQL subscription.""" class MessagingRocketmqMessageTypeValues(Enum): NORMAL = "normal" """Normal message.""" FIFO = "fifo" """FIFO message.""" DELAY = "delay" """Delay message.""" TRANSACTION = "transaction" """Transaction message.""" class MessagingRocketmqConsumptionModelValues(Enum): CLUSTERING = "clustering" """Clustering consumption model.""" BROADCASTING = "broadcasting" """Broadcasting consumption model.""" class RpcGrpcStatusCodeValues(Enum): OK = 0 """OK.""" CANCELLED = 1 """CANCELLED.""" UNKNOWN = 2 """UNKNOWN.""" INVALID_ARGUMENT = 3 """INVALID_ARGUMENT.""" DEADLINE_EXCEEDED = 4 """DEADLINE_EXCEEDED.""" NOT_FOUND = 5 """NOT_FOUND.""" ALREADY_EXISTS = 6 """ALREADY_EXISTS.""" PERMISSION_DENIED = 7 """PERMISSION_DENIED.""" RESOURCE_EXHAUSTED = 8 """RESOURCE_EXHAUSTED.""" FAILED_PRECONDITION = 9 """FAILED_PRECONDITION.""" ABORTED = 10 """ABORTED.""" OUT_OF_RANGE = 11 """OUT_OF_RANGE.""" UNIMPLEMENTED = 12 """UNIMPLEMENTED.""" INTERNAL = 13 """INTERNAL.""" UNAVAILABLE = 14 """UNAVAILABLE.""" DATA_LOSS = 15 """DATA_LOSS.""" UNAUTHENTICATED = 16 """UNAUTHENTICATED.""" class MessageTypeValues(Enum): SENT = "SENT" """sent.""" RECEIVED = "RECEIVED" """received.""" class RpcConnectRpcErrorCodeValues(Enum): CANCELLED = "cancelled" """cancelled.""" UNKNOWN = "unknown" """unknown.""" INVALID_ARGUMENT = "invalid_argument" """invalid_argument.""" DEADLINE_EXCEEDED = "deadline_exceeded" """deadline_exceeded.""" NOT_FOUND = "not_found" """not_found.""" ALREADY_EXISTS = "already_exists" """already_exists.""" PERMISSION_DENIED = "permission_denied" """permission_denied.""" RESOURCE_EXHAUSTED = "resource_exhausted" """resource_exhausted.""" FAILED_PRECONDITION = "failed_precondition" """failed_precondition.""" ABORTED = "aborted" """aborted.""" OUT_OF_RANGE = "out_of_range" """out_of_range.""" UNIMPLEMENTED = "unimplemented" """unimplemented.""" INTERNAL = "internal" """internal.""" UNAVAILABLE = "unavailable" """unavailable.""" DATA_LOSS = "data_loss" """data_loss.""" UNAUTHENTICATED = "unauthenticated" """unauthenticated.""" python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version/000077500000000000000000000000001527070070700337055ustar00rootroot00000000000000__init__.py000066400000000000000000000001441527070070700357360ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 __version__ = "0.66b2" python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/test-requirements.txt000066400000000000000000000003331527070070700313230ustar00rootroot00000000000000asgiref==3.7.2 iniconfig==2.0.0 packaging==24.0 pluggy==1.6.0 py-cpuinfo==9.0.0 pytest==9.0.3 tomli==2.0.1 typing_extensions==4.10.0 wrapt==1.16.0 zipp==3.19.2 -e opentelemetry-api -e opentelemetry-semantic-conventions python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/tests/000077500000000000000000000000001527070070700262255ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/tests/__init__.py000066400000000000000000000000001527070070700303240ustar00rootroot00000000000000python-opentelemetry-1.45.2/opentelemetry-semantic-conventions/tests/test_semconv.py000066400000000000000000000005641527070070700313150ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # type: ignore from importlib.util import find_spec from unittest import TestCase class TestSemanticConventions(TestCase): def test_semantic_conventions(self): if find_spec("opentelemetry.semconv") is None: self.fail("opentelemetry-semantic-conventions not installed") python-opentelemetry-1.45.2/propagator/000077500000000000000000000000001527070070700202015ustar00rootroot00000000000000python-opentelemetry-1.45.2/propagator/opentelemetry-propagator-b3/000077500000000000000000000000001527070070700255535ustar00rootroot00000000000000python-opentelemetry-1.45.2/propagator/opentelemetry-propagator-b3/LICENSE000066400000000000000000000261351527070070700265670ustar00rootroot00000000000000 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. python-opentelemetry-1.45.2/propagator/opentelemetry-propagator-b3/README.rst000066400000000000000000000007271527070070700272500ustar00rootroot00000000000000OpenTelemetry B3 Propagator =========================== |pypi| .. |pypi| image:: https://badge.fury.io/py/opentelemetry-propagator-b3.svg :target: https://pypi.org/project/opentelemetry-propagator-b3/ This library provides a propagator for the B3 format Installation ------------ :: pip install opentelemetry-propagator-b3 References ---------- * `OpenTelemetry `_ * `B3 format `_ python-opentelemetry-1.45.2/propagator/opentelemetry-propagator-b3/benchmark-requirements.txt000066400000000000000000000000301527070070700327600ustar00rootroot00000000000000pytest-benchmark==4.0.0 python-opentelemetry-1.45.2/propagator/opentelemetry-propagator-b3/benchmarks/000077500000000000000000000000001527070070700276705ustar00rootroot00000000000000python-opentelemetry-1.45.2/propagator/opentelemetry-propagator-b3/benchmarks/trace/000077500000000000000000000000001527070070700307665ustar00rootroot00000000000000python-opentelemetry-1.45.2/propagator/opentelemetry-propagator-b3/benchmarks/trace/propagation/000077500000000000000000000000001527070070700333115ustar00rootroot00000000000000test_benchmark_b3_format.py000066400000000000000000000016151527070070700405340ustar00rootroot00000000000000python-opentelemetry-1.45.2/propagator/opentelemetry-propagator-b3/benchmarks/trace/propagation# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import opentelemetry.propagators.b3 as b3_format from opentelemetry.sdk.trace import TracerProvider FORMAT = b3_format.B3Format() def test_extract_single_header(benchmark): benchmark( FORMAT.extract, {FORMAT.SINGLE_HEADER_KEY: "bdb5b63237ed38aea578af665aa5aa60-c32d953d73ad2251-1"}, ) def test_inject_empty_context(benchmark): tracer = TracerProvider().get_tracer("sdk_tracer_provider") with tracer.start_as_current_span("Root Span"): with tracer.start_as_current_span("Child Span"): benchmark( FORMAT.inject, { FORMAT.TRACE_ID_KEY: "bdb5b63237ed38aea578af665aa5aa60", FORMAT.SPAN_ID_KEY: "00000000000000000c32d953d73ad225", FORMAT.SAMPLED_KEY: "1", }, ) python-opentelemetry-1.45.2/propagator/opentelemetry-propagator-b3/py.typed000066400000000000000000000000001527070070700272400ustar00rootroot00000000000000python-opentelemetry-1.45.2/propagator/opentelemetry-propagator-b3/pyproject.toml000066400000000000000000000027131527070070700304720ustar00rootroot00000000000000[build-system] requires = ["hatchling"] build-backend = "hatchling.build" [project] name = "opentelemetry-propagator-b3" dynamic = ["version"] description = "OpenTelemetry B3 Propagator" 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", "Framework :: OpenTelemetry", "Intended Audience :: Developers", "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 = [ "typing-extensions >= 4.5.0", "opentelemetry-api ~= 1.3", ] [project.entry-points.opentelemetry_propagator] b3 = "opentelemetry.propagators.b3:B3SingleFormat" b3multi = "opentelemetry.propagators.b3:B3MultiFormat" [project.urls] Homepage = "https://github.com/open-telemetry/opentelemetry-python/tree/main/propagator/opentelemetry-propagator-b3" Repository = "https://github.com/open-telemetry/opentelemetry-python" [tool.hatch.version] path = "src/opentelemetry/propagators/b3/version/__init__.py" [tool.hatch.build.targets.sdist] include = [ "/src", "/tests", ] [tool.hatch.build.targets.wheel] packages = ["src/opentelemetry"] python-opentelemetry-1.45.2/propagator/opentelemetry-propagator-b3/src/000077500000000000000000000000001527070070700263425ustar00rootroot00000000000000python-opentelemetry-1.45.2/propagator/opentelemetry-propagator-b3/src/opentelemetry/000077500000000000000000000000001527070070700312365ustar00rootroot00000000000000python-opentelemetry-1.45.2/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/000077500000000000000000000000001527070070700335775ustar00rootroot00000000000000python-opentelemetry-1.45.2/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/000077500000000000000000000000001527070070700341035ustar00rootroot00000000000000__init__.py000066400000000000000000000134501527070070700361400ustar00rootroot00000000000000python-opentelemetry-1.45.2/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import collections.abc from re import compile as re_compile from typing_extensions import deprecated from opentelemetry import trace from opentelemetry.context import Context from opentelemetry.propagators.textmap import ( CarrierT, Getter, Setter, TextMapPropagator, default_getter, default_setter, ) from opentelemetry.trace import format_span_id, format_trace_id class B3MultiFormat(TextMapPropagator): """Propagator for the B3 HTTP multi-header format. See: https://github.com/openzipkin/b3-propagation https://github.com/openzipkin/b3-propagation#multiple-headers """ SINGLE_HEADER_KEY = "b3" TRACE_ID_KEY = "x-b3-traceid" SPAN_ID_KEY = "x-b3-spanid" SAMPLED_KEY = "x-b3-sampled" FLAGS_KEY = "x-b3-flags" _SAMPLE_PROPAGATE_VALUES = {"1", "True", "true", "d"} _trace_id_regex = re_compile(r"[\da-fA-F]{16}|[\da-fA-F]{32}") _span_id_regex = re_compile(r"[\da-fA-F]{16}") def extract( self, carrier: CarrierT, context: Context | None = None, getter: Getter = default_getter, ) -> Context: if context is None: context = Context() trace_id = trace.INVALID_TRACE_ID span_id = trace.INVALID_SPAN_ID sampled = "0" flags = None single_header = _extract_first_element(getter.get(carrier, self.SINGLE_HEADER_KEY)) if single_header: # The b3 spec calls for the sampling state to be # "deferred", which is unspecified. This concept does not # translate to SpanContext, so we set it as recorded. sampled = "1" fields = single_header.split("-", 4) if len(fields) == 1: sampled = fields[0] elif len(fields) == 2: trace_id, span_id = fields elif len(fields) == 3: trace_id, span_id, sampled = fields elif len(fields) == 4: trace_id, span_id, sampled, _ = fields else: trace_id = _extract_first_element(getter.get(carrier, self.TRACE_ID_KEY)) or trace_id span_id = _extract_first_element(getter.get(carrier, self.SPAN_ID_KEY)) or span_id sampled = _extract_first_element(getter.get(carrier, self.SAMPLED_KEY)) or sampled flags = _extract_first_element(getter.get(carrier, self.FLAGS_KEY)) or flags if ( trace_id == trace.INVALID_TRACE_ID or span_id == trace.INVALID_SPAN_ID or self._trace_id_regex.fullmatch(trace_id) is None or self._span_id_regex.fullmatch(span_id) is None ): return context trace_id = int(trace_id, 16) span_id = int(span_id, 16) options = 0 # The b3 spec provides no defined behavior for both sample and # flag values set. Since the setting of at least one implies # the desire for some form of sampling, propagate if either # header is set to allow. if sampled in self._SAMPLE_PROPAGATE_VALUES or flags == "1": options |= trace.TraceFlags.SAMPLED return trace.set_span_in_context( trace.NonRecordingSpan( trace.SpanContext( # trace an span ids are encoded in hex, so must be converted trace_id=trace_id, span_id=span_id, is_remote=True, trace_flags=trace.TraceFlags(options), trace_state=trace.TraceState(), ) ), context, ) def inject( self, carrier: CarrierT, context: Context | None = None, setter: Setter = default_setter, ) -> None: span = trace.get_current_span(context=context) span_context = span.get_span_context() if span_context == trace.INVALID_SPAN_CONTEXT: return sampled = (trace.TraceFlags.SAMPLED & span_context.trace_flags) != 0 setter.set( carrier, self.TRACE_ID_KEY, format_trace_id(span_context.trace_id), ) setter.set(carrier, self.SPAN_ID_KEY, format_span_id(span_context.span_id)) setter.set(carrier, self.SAMPLED_KEY, "1" if sampled else "0") @property def fields(self) -> set[str]: return { self.TRACE_ID_KEY, self.SPAN_ID_KEY, self.SAMPLED_KEY, } class B3SingleFormat(B3MultiFormat): """Propagator for the B3 HTTP single-header format. See: https://github.com/openzipkin/b3-propagation https://github.com/openzipkin/b3-propagation#single-header """ def inject( self, carrier: CarrierT, context: Context | None = None, setter: Setter = default_setter, ) -> None: span = trace.get_current_span(context=context) span_context = span.get_span_context() if span_context == trace.INVALID_SPAN_CONTEXT: return sampled = (trace.TraceFlags.SAMPLED & span_context.trace_flags) != 0 fields = [ format_trace_id(span_context.trace_id), format_span_id(span_context.span_id), "1" if sampled else "0", ] setter.set(carrier, self.SINGLE_HEADER_KEY, "-".join(fields)) @property def fields(self) -> set[str]: return {self.SINGLE_HEADER_KEY} class B3Format(B3MultiFormat): @deprecated( "B3Format is deprecated in favor of B3MultiFormat. Deprecated since version 1.2.0.", ) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) def _extract_first_element( items: collections.abc.Iterable[CarrierT], ) -> CarrierT | None: if items is None: return None return next(iter(items), None) py.typed000066400000000000000000000000001527070070700355110ustar00rootroot00000000000000python-opentelemetry-1.45.2/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3version/000077500000000000000000000000001527070070700355115ustar00rootroot00000000000000python-opentelemetry-1.45.2/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3__init__.py000066400000000000000000000001441527070070700376210ustar00rootroot00000000000000python-opentelemetry-1.45.2/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 __version__ = "1.45.2" python-opentelemetry-1.45.2/propagator/opentelemetry-propagator-b3/test-requirements.txt000066400000000000000000000004151527070070700320140ustar00rootroot00000000000000asgiref==3.7.2 iniconfig==2.0.0 packaging==24.0 pluggy==1.6.0 py-cpuinfo==9.0.0 pytest==9.0.3 tomli==2.0.1 typing_extensions==4.10.0 wrapt==1.16.0 -e opentelemetry-api -e opentelemetry-sdk -e opentelemetry-semantic-conventions -e propagator/opentelemetry-propagator-b3 python-opentelemetry-1.45.2/propagator/opentelemetry-propagator-b3/tests/000077500000000000000000000000001527070070700267155ustar00rootroot00000000000000python-opentelemetry-1.45.2/propagator/opentelemetry-propagator-b3/tests/__init__.py000066400000000000000000000001141527070070700310220ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 python-opentelemetry-1.45.2/propagator/opentelemetry-propagator-b3/tests/test_b3_format.py000066400000000000000000000370361527070070700322130ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import unittest from abc import abstractmethod from unittest.mock import Mock import opentelemetry.trace as trace_api from opentelemetry.context import Context, get_current from opentelemetry.propagators.b3 import ( # pylint: disable=no-name-in-module,import-error B3MultiFormat, B3SingleFormat, ) from opentelemetry.propagators.textmap import DefaultGetter from opentelemetry.sdk import trace from opentelemetry.sdk.trace import id_generator from opentelemetry.trace.propagation import _SPAN_KEY def get_child_parent_new_carrier(old_carrier, propagator): ctx = propagator.extract(old_carrier) parent_span_context = trace_api.get_current_span(ctx).get_span_context() parent = trace._Span("parent", parent_span_context) child = trace._Span( "child", trace_api.SpanContext( parent_span_context.trace_id, id_generator.RandomIdGenerator().generate_span_id(), is_remote=False, trace_flags=parent_span_context.trace_flags, trace_state=parent_span_context.trace_state, ), parent=parent.get_span_context(), ) new_carrier = {} ctx = trace_api.set_span_in_context(child) propagator.inject(new_carrier, context=ctx) return child, parent, new_carrier class AbstractB3FormatTestCase: # pylint: disable=too-many-public-methods,no-member,invalid-name @classmethod def setUpClass(cls): generator = id_generator.RandomIdGenerator() cls.serialized_trace_id = trace_api.format_trace_id(generator.generate_trace_id()) cls.serialized_span_id = trace_api.format_span_id(generator.generate_span_id()) def setUp(self) -> None: tracer_provider = trace.TracerProvider() patcher = unittest.mock.patch.object(trace_api, "get_tracer_provider", return_value=tracer_provider) patcher.start() self.addCleanup(patcher.stop) @classmethod def get_child_parent_new_carrier(cls, old_carrier): return get_child_parent_new_carrier(old_carrier, cls.get_propagator()) @classmethod @abstractmethod def get_propagator(cls): pass @classmethod @abstractmethod def get_trace_id(cls, carrier): pass def assertSampled(self, carrier): pass def assertNotSampled(self, carrier): pass def test_extract_multi_header(self): """Test the extraction of B3 headers.""" propagator = self.get_propagator() context = { propagator.TRACE_ID_KEY: self.serialized_trace_id, propagator.SPAN_ID_KEY: self.serialized_span_id, propagator.SAMPLED_KEY: "1", } child, parent, _ = self.get_child_parent_new_carrier(context) self.assertEqual( context[propagator.TRACE_ID_KEY], trace_api.format_trace_id(child.context.trace_id), ) self.assertEqual( context[propagator.SPAN_ID_KEY], trace_api.format_span_id(child.parent.span_id), ) self.assertTrue(parent.context.is_remote) self.assertTrue(parent.context.trace_flags.sampled) def test_extract_single_header(self): """Test the extraction from a single b3 header.""" propagator = self.get_propagator() child, parent, _ = self.get_child_parent_new_carrier( {propagator.SINGLE_HEADER_KEY: f"{self.serialized_trace_id}-{self.serialized_span_id}"} ) self.assertEqual( self.serialized_trace_id, trace_api.format_trace_id(child.context.trace_id), ) self.assertEqual( self.serialized_span_id, trace_api.format_span_id(child.parent.span_id), ) self.assertTrue(parent.context.is_remote) self.assertTrue(parent.context.trace_flags.sampled) child, parent, _ = self.get_child_parent_new_carrier( {propagator.SINGLE_HEADER_KEY: f"{self.serialized_trace_id}-{self.serialized_span_id}-1"} ) self.assertEqual( self.serialized_trace_id, trace_api.format_trace_id(child.context.trace_id), ) self.assertEqual( self.serialized_span_id, trace_api.format_span_id(child.parent.span_id), ) self.assertTrue(parent.context.is_remote) self.assertTrue(parent.context.trace_flags.sampled) def test_extract_header_precedence(self): """A single b3 header should take precedence over multiple headers. """ propagator = self.get_propagator() single_header_trace_id = self.serialized_trace_id[:-3] + "123" _, _, new_carrier = self.get_child_parent_new_carrier( { propagator.SINGLE_HEADER_KEY: f"{single_header_trace_id}-{self.serialized_span_id}", propagator.TRACE_ID_KEY: self.serialized_trace_id, propagator.SPAN_ID_KEY: self.serialized_span_id, propagator.SAMPLED_KEY: "1", } ) self.assertEqual(self.get_trace_id(new_carrier), single_header_trace_id) def test_enabled_sampling(self): """Test b3 sample key variants that turn on sampling.""" propagator = self.get_propagator() for variant in ["1", "True", "true", "d"]: _, _, new_carrier = self.get_child_parent_new_carrier( { propagator.TRACE_ID_KEY: self.serialized_trace_id, propagator.SPAN_ID_KEY: self.serialized_span_id, propagator.SAMPLED_KEY: variant, } ) self.assertSampled(new_carrier) def test_disabled_sampling(self): """Test b3 sample key variants that turn off sampling.""" propagator = self.get_propagator() for variant in ["0", "False", "false", None]: _, _, new_carrier = self.get_child_parent_new_carrier( { propagator.TRACE_ID_KEY: self.serialized_trace_id, propagator.SPAN_ID_KEY: self.serialized_span_id, propagator.SAMPLED_KEY: variant, } ) self.assertNotSampled(new_carrier) def test_flags(self): """x-b3-flags set to "1" should result in propagation.""" propagator = self.get_propagator() _, _, new_carrier = self.get_child_parent_new_carrier( { propagator.TRACE_ID_KEY: self.serialized_trace_id, propagator.SPAN_ID_KEY: self.serialized_span_id, propagator.FLAGS_KEY: "1", } ) self.assertSampled(new_carrier) def test_flags_and_sampling(self): """Propagate if b3 flags and sampling are set.""" propagator = self.get_propagator() _, _, new_carrier = self.get_child_parent_new_carrier( { propagator.TRACE_ID_KEY: self.serialized_trace_id, propagator.SPAN_ID_KEY: self.serialized_span_id, propagator.FLAGS_KEY: "1", } ) self.assertSampled(new_carrier) def test_derived_ctx_is_returned_for_success(self): """Ensure returned context is derived from the given context.""" old_ctx = Context({"k1": "v1"}) propagator = self.get_propagator() new_ctx = propagator.extract( { propagator.TRACE_ID_KEY: self.serialized_trace_id, propagator.SPAN_ID_KEY: self.serialized_span_id, propagator.FLAGS_KEY: "1", }, old_ctx, ) self.assertIn(_SPAN_KEY, new_ctx) for key, value in old_ctx.items(): # pylint:disable=no-member self.assertIn(key, new_ctx) # pylint:disable=unsubscriptable-object self.assertEqual(new_ctx[key], value) def test_derived_ctx_is_returned_for_failure(self): """Ensure returned context is derived from the given context.""" old_ctx = Context({"k2": "v2"}) new_ctx = self.get_propagator().extract({}, old_ctx) self.assertNotIn(_SPAN_KEY, new_ctx) for key, value in old_ctx.items(): # pylint:disable=no-member self.assertIn(key, new_ctx) # pylint:disable=unsubscriptable-object self.assertEqual(new_ctx[key], value) def test_64bit_trace_id(self): """64 bit trace ids should be padded to 128 bit trace ids.""" propagator = self.get_propagator() trace_id_64_bit = self.serialized_trace_id[:16] _, _, new_carrier = self.get_child_parent_new_carrier( { propagator.TRACE_ID_KEY: trace_id_64_bit, propagator.SPAN_ID_KEY: self.serialized_span_id, propagator.FLAGS_KEY: "1", }, ) self.assertEqual(self.get_trace_id(new_carrier), "0" * 16 + trace_id_64_bit) def test_extract_invalid_single_header_to_explicit_ctx(self): """Given unparsable header, do not modify context""" old_ctx = Context({"k1": "v1"}) propagator = self.get_propagator() carrier = {propagator.SINGLE_HEADER_KEY: "0-1-2-3-4-5-6-7"} new_ctx = propagator.extract(carrier, old_ctx) self.assertDictEqual(new_ctx, old_ctx) def test_extract_invalid_single_header_to_implicit_ctx(self): propagator = self.get_propagator() carrier = {propagator.SINGLE_HEADER_KEY: "0-1-2-3-4-5-6-7"} new_ctx = propagator.extract(carrier) self.assertDictEqual(Context(), new_ctx) def test_extract_missing_trace_id_to_explicit_ctx(self): """Given no trace ID, do not modify context""" old_ctx = Context({"k1": "v1"}) propagator = self.get_propagator() carrier = { propagator.SPAN_ID_KEY: self.serialized_span_id, propagator.FLAGS_KEY: "1", } new_ctx = propagator.extract(carrier, old_ctx) self.assertDictEqual(new_ctx, old_ctx) def test_extract_missing_trace_id_to_implicit_ctx(self): propagator = self.get_propagator() carrier = { propagator.SPAN_ID_KEY: self.serialized_span_id, propagator.FLAGS_KEY: "1", } new_ctx = propagator.extract(carrier) self.assertDictEqual(Context(), new_ctx) def test_extract_invalid_trace_id_to_explicit_ctx(self): """Given invalid trace ID, do not modify context""" old_ctx = Context({"k1": "v1"}) propagator = self.get_propagator() carrier = { propagator.TRACE_ID_KEY: "abc123", propagator.SPAN_ID_KEY: self.serialized_span_id, propagator.FLAGS_KEY: "1", } new_ctx = propagator.extract(carrier, old_ctx) self.assertDictEqual(new_ctx, old_ctx) def test_extract_invalid_trace_id_to_implicit_ctx(self): propagator = self.get_propagator() carrier = { propagator.TRACE_ID_KEY: "abc123", propagator.SPAN_ID_KEY: self.serialized_span_id, propagator.FLAGS_KEY: "1", } new_ctx = propagator.extract(carrier) self.assertDictEqual(Context(), new_ctx) def test_extract_invalid_span_id_to_explicit_ctx(self): """Given invalid span ID, do not modify context""" old_ctx = Context({"k1": "v1"}) propagator = self.get_propagator() carrier = { propagator.TRACE_ID_KEY: self.serialized_trace_id, propagator.SPAN_ID_KEY: "abc123", propagator.FLAGS_KEY: "1", } new_ctx = propagator.extract(carrier, old_ctx) self.assertDictEqual(new_ctx, old_ctx) def test_extract_invalid_span_id_to_implicit_ctx(self): propagator = self.get_propagator() carrier = { propagator.TRACE_ID_KEY: self.serialized_trace_id, propagator.SPAN_ID_KEY: "abc123", propagator.FLAGS_KEY: "1", } new_ctx = propagator.extract(carrier) self.assertDictEqual(Context(), new_ctx) def test_extract_missing_span_id_to_explicit_ctx(self): """Given no span ID, do not modify context""" old_ctx = Context({"k1": "v1"}) propagator = self.get_propagator() carrier = { propagator.TRACE_ID_KEY: self.serialized_trace_id, propagator.FLAGS_KEY: "1", } new_ctx = propagator.extract(carrier, old_ctx) self.assertDictEqual(new_ctx, old_ctx) def test_extract_missing_span_id_to_implicit_ctx(self): propagator = self.get_propagator() carrier = { propagator.TRACE_ID_KEY: self.serialized_trace_id, propagator.FLAGS_KEY: "1", } new_ctx = propagator.extract(carrier) self.assertDictEqual(Context(), new_ctx) def test_extract_empty_carrier_to_explicit_ctx(self): """Given no headers at all, do not modify context""" old_ctx = Context({"k1": "v1"}) carrier = {} new_ctx = self.get_propagator().extract(carrier, old_ctx) self.assertDictEqual(new_ctx, old_ctx) def test_extract_empty_carrier_to_implicit_ctx(self): new_ctx = self.get_propagator().extract({}) self.assertDictEqual(Context(), new_ctx) def test_inject_empty_context(self): """If the current context has no span, don't add headers""" new_carrier = {} self.get_propagator().inject(new_carrier, get_current()) assert len(new_carrier) == 0 def test_default_span(self): """Make sure propagator does not crash when working with NonRecordingSpan""" class CarrierGetter(DefaultGetter): def get(self, carrier, key): return carrier.get(key, None) propagator = self.get_propagator() ctx = propagator.extract({}, getter=CarrierGetter()) propagator.inject({}, context=ctx) def test_fields(self): """Make sure the fields attribute returns the fields used in inject""" propagator = self.get_propagator() tracer = trace.TracerProvider().get_tracer("sdk_tracer_provider") mock_setter = Mock() with tracer.start_as_current_span("parent"): with tracer.start_as_current_span("child"): propagator.inject({}, setter=mock_setter) inject_fields = set() for call in mock_setter.mock_calls: inject_fields.add(call[1][1]) self.assertEqual(propagator.fields, inject_fields) def test_extract_none_context(self): """Given no trace ID, do not modify context""" old_ctx = None carrier = {} new_ctx = self.get_propagator().extract(carrier, old_ctx) self.assertDictEqual(Context(), new_ctx) class TestB3MultiFormat(AbstractB3FormatTestCase, unittest.TestCase): @classmethod def get_propagator(cls): return B3MultiFormat() @classmethod def get_trace_id(cls, carrier): return carrier[cls.get_propagator().TRACE_ID_KEY] def assertSampled(self, carrier): self.assertEqual(carrier[self.get_propagator().SAMPLED_KEY], "1") def assertNotSampled(self, carrier): self.assertEqual(carrier[self.get_propagator().SAMPLED_KEY], "0") class TestB3SingleFormat(AbstractB3FormatTestCase, unittest.TestCase): @classmethod def get_propagator(cls): return B3SingleFormat() @classmethod def get_trace_id(cls, carrier): return carrier[cls.get_propagator().SINGLE_HEADER_KEY].split("-")[0] def assertSampled(self, carrier): self.assertEqual(carrier[self.get_propagator().SINGLE_HEADER_KEY].split("-")[2], "1") def assertNotSampled(self, carrier): self.assertEqual(carrier[self.get_propagator().SINGLE_HEADER_KEY].split("-")[2], "0") python-opentelemetry-1.45.2/propagator/opentelemetry-propagator-jaeger/000077500000000000000000000000001527070070700265045ustar00rootroot00000000000000python-opentelemetry-1.45.2/propagator/opentelemetry-propagator-jaeger/LICENSE000066400000000000000000000261351527070070700275200ustar00rootroot00000000000000 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. python-opentelemetry-1.45.2/propagator/opentelemetry-propagator-jaeger/README.rst000066400000000000000000000010221527070070700301660ustar00rootroot00000000000000OpenTelemetry Jaeger Propagator =============================== |pypi| .. |pypi| image:: https://badge.fury.io/py/opentelemetry-propagator-jaeger.svg :target: https://pypi.org/project/opentelemetry-propagator-jaeger/ This library provides a propagator for the Jaeger format Installation ------------ :: pip install opentelemetry-propagator-jaeger References ---------- * `OpenTelemetry `_ * `Jaeger format `_ python-opentelemetry-1.45.2/propagator/opentelemetry-propagator-jaeger/py.typed000066400000000000000000000000001527070070700301710ustar00rootroot00000000000000python-opentelemetry-1.45.2/propagator/opentelemetry-propagator-jaeger/pyproject.toml000066400000000000000000000026161527070070700314250ustar00rootroot00000000000000[build-system] requires = ["hatchling"] build-backend = "hatchling.build" [project] name = "opentelemetry-propagator-jaeger" dynamic = ["version"] description = "OpenTelemetry Jaeger Propagator" 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", "Framework :: OpenTelemetry", "Intended Audience :: Developers", "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.3", ] [project.entry-points.opentelemetry_propagator] jaeger = "opentelemetry.propagators.jaeger:JaegerPropagator" [project.urls] Homepage = "https://github.com/open-telemetry/opentelemetry-python/tree/main/propagator/opentelemetry-propagator-jaeger" Repository = "https://github.com/open-telemetry/opentelemetry-python" [tool.hatch.version] path = "src/opentelemetry/propagators/jaeger/version/__init__.py" [tool.hatch.build.targets.sdist] include = [ "/src", "/tests", ] [tool.hatch.build.targets.wheel] packages = ["src/opentelemetry"] python-opentelemetry-1.45.2/propagator/opentelemetry-propagator-jaeger/src/000077500000000000000000000000001527070070700272735ustar00rootroot00000000000000python-opentelemetry-1.45.2/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/000077500000000000000000000000001527070070700321675ustar00rootroot00000000000000propagators/000077500000000000000000000000001527070070700344515ustar00rootroot00000000000000python-opentelemetry-1.45.2/propagator/opentelemetry-propagator-jaeger/src/opentelemetryjaeger/000077500000000000000000000000001527070070700357065ustar00rootroot00000000000000python-opentelemetry-1.45.2/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators__init__.py000066400000000000000000000152031527070070700400200ustar00rootroot00000000000000python-opentelemetry-1.45.2/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import collections.abc import itertools import urllib.parse from opentelemetry import baggage, trace from opentelemetry.context import Context from opentelemetry.propagators.textmap import ( CarrierT, Getter, Setter, TextMapPropagator, default_getter, default_setter, ) from opentelemetry.trace import SpanContext, format_span_id, format_trace_id class JaegerPropagator(TextMapPropagator): """Propagator for the Jaeger format. See: https://www.jaegertracing.io/docs/1.19/client-libraries/#propagation-format """ TRACE_ID_KEY = "uber-trace-id" BAGGAGE_PREFIX = "uberctx-" DEBUG_FLAG = 0x02 # The Jaeger format defines no baggage limits, so the W3C Baggage spec # limits are borrowed to bound an unbounded inbound carrier on extract. MAX_BAGGAGE_ENTRIES = 180 MAX_BAGGAGE_ENTRY_BYTES = 4096 MAX_BAGGAGE_TOTAL_BYTES = 8192 def extract( self, carrier: CarrierT, context: Context | None = None, getter: Getter[CarrierT] = default_getter, ) -> Context: if context is None: context = Context() header = getter.get(carrier, self.TRACE_ID_KEY) if not header: return context context = self._extract_baggage(getter, carrier, context) trace_id, span_id, flags = _parse_trace_id_header(header) if trace_id == trace.INVALID_TRACE_ID or span_id == trace.INVALID_SPAN_ID: return context span = trace.NonRecordingSpan( trace.SpanContext( trace_id=trace_id, span_id=span_id, is_remote=True, trace_flags=trace.TraceFlags(flags & trace.TraceFlags.SAMPLED), ) ) return trace.set_span_in_context(span, context) 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 span_context == trace.INVALID_SPAN_CONTEXT: return # Non-recording spans do not have a parent; the API Span type does not # declare a parent attribute, so it has to be accessed via getattr parent: SpanContext | None = getattr(span, "parent", None) if span.is_recording() else None span_parent_id = parent.span_id if parent else 0 trace_flags = span_context.trace_flags if trace_flags.sampled: trace_flags |= self.DEBUG_FLAG # set span identity setter.set( carrier, self.TRACE_ID_KEY, _format_uber_trace_id( span_context.trace_id, span_context.span_id, span_parent_id, trace_flags, ), ) # set span baggage, if any baggage_entries = baggage.get_all(context=context) if not baggage_entries: return self._inject_baggage(setter, carrier, baggage_entries) @property def fields(self) -> set[str]: return {self.TRACE_ID_KEY} def _extract_baggage( self, getter: Getter[CarrierT], carrier: CarrierT, context: Context, ) -> Context: # The limit bounds the candidates inspected, not the entries kept, so a # carrier full of oversized ones cannot force unbounded work. candidates = itertools.islice( (key for key in getter.keys(carrier) if key.startswith(self.BAGGAGE_PREFIX)), self.MAX_BAGGAGE_ENTRIES, ) pairs = [] for key in candidates: value = _extract_first_element(getter.get(carrier, key)) if value is not None: pairs.append((key.replace(self.BAGGAGE_PREFIX, ""), value)) for baggage_key, value in _limit_baggage_bytes( pairs, self.MAX_BAGGAGE_ENTRY_BYTES, self.MAX_BAGGAGE_TOTAL_BYTES ): context = baggage.set_baggage( baggage_key, urllib.parse.unquote(value).strip(), context=context, ) return context def _inject_baggage( self, setter: Setter[CarrierT], carrier: CarrierT, baggage_entries: collections.abc.Mapping[str, object], ) -> None: candidates = itertools.islice(baggage_entries.items(), self.MAX_BAGGAGE_ENTRIES) pairs = [(key, urllib.parse.quote(str(value))) for key, value in candidates] for key, encoded_value in _limit_baggage_bytes( pairs, self.MAX_BAGGAGE_ENTRY_BYTES, self.MAX_BAGGAGE_TOTAL_BYTES ): setter.set(carrier, self.BAGGAGE_PREFIX + key, encoded_value) def _limit_baggage_bytes( pairs: collections.abc.Iterable[tuple[str, str]], max_entry_bytes: int, max_total_bytes: int, ) -> collections.abc.Iterator[tuple[str, str]]: total_bytes = 0 accepted = 0 for key, value in pairs: entry_bytes = len(key.encode()) + len(value.encode()) + 1 if entry_bytes > max_entry_bytes: continue separator_bytes = 1 if accepted > 0 else 0 if total_bytes + separator_bytes + entry_bytes > max_total_bytes: continue yield key, value total_bytes += separator_bytes + entry_bytes accepted += 1 def _format_uber_trace_id(trace_id, span_id, parent_span_id, flags): return f"{format_trace_id(trace_id)}:{format_span_id(span_id)}:{format_span_id(parent_span_id)}:{flags:02x}" def _extract_first_element( items: collections.abc.Iterable[str] | None, ) -> str | None: if items is None: return None return next(iter(items), None) def _parse_trace_id_header( items: collections.abc.Iterable[str], ) -> tuple[int, int, int]: invalid_header_result = (trace.INVALID_TRACE_ID, trace.INVALID_SPAN_ID, 0) header = _extract_first_element(items) if header is None: return invalid_header_result fields = header.split(":") if len(fields) != 4: return invalid_header_result trace_id_str, span_id_str, _parent_id_str, flags_str = fields flags = _int_from_hex_str(flags_str) if flags is None: return invalid_header_result trace_id = _int_from_hex_str(trace_id_str) if trace_id is None: trace_id = trace.INVALID_TRACE_ID span_id = _int_from_hex_str(span_id_str) if span_id is None: span_id = trace.INVALID_SPAN_ID return trace_id, span_id, flags def _int_from_hex_str(identifier: str) -> int | None: try: return int(identifier, 16) except ValueError: return None py.typed000066400000000000000000000000001527070070700373730ustar00rootroot00000000000000python-opentelemetry-1.45.2/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaegerversion/000077500000000000000000000000001527070070700373735ustar00rootroot00000000000000python-opentelemetry-1.45.2/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger__init__.py000066400000000000000000000001441527070070700415030ustar00rootroot00000000000000python-opentelemetry-1.45.2/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 __version__ = "1.45.2" python-opentelemetry-1.45.2/propagator/opentelemetry-propagator-jaeger/test-requirements.txt000066400000000000000000000004631527070070700327500ustar00rootroot00000000000000asgiref==3.7.2 iniconfig==2.0.0 packaging==24.0 pluggy==1.6.0 py-cpuinfo==9.0.0 pytest==9.0.3 tomli==2.0.1 typing_extensions==4.10.0 wrapt==1.16.0 -e opentelemetry-api -e opentelemetry-sdk -e opentelemetry-semantic-conventions -e tests/opentelemetry-test-utils -e propagator/opentelemetry-propagator-jaeger python-opentelemetry-1.45.2/propagator/opentelemetry-propagator-jaeger/tests/000077500000000000000000000000001527070070700276465ustar00rootroot00000000000000python-opentelemetry-1.45.2/propagator/opentelemetry-propagator-jaeger/tests/__init__.py000066400000000000000000000001141527070070700317530ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 test_jaeger_propagator.py000066400000000000000000000350521527070070700347000ustar00rootroot00000000000000python-opentelemetry-1.45.2/propagator/opentelemetry-propagator-jaeger/tests# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from unittest.mock import Mock import opentelemetry.trace as trace_api from opentelemetry import baggage from opentelemetry.baggage import _BAGGAGE_KEY from opentelemetry.context import Context from opentelemetry.propagators import ( # pylint: disable=no-name-in-module jaeger, ) from opentelemetry.propagators.textmap import DefaultGetter from opentelemetry.sdk import trace from opentelemetry.sdk.trace import id_generator from opentelemetry.test import TestCase FORMAT = jaeger.JaegerPropagator() def get_context_new_carrier(old_carrier, carrier_baggage=None): ctx = FORMAT.extract(old_carrier) if carrier_baggage: for key, value in carrier_baggage.items(): ctx = baggage.set_baggage(key, value, ctx) parent_span_context = trace_api.get_current_span(ctx).get_span_context() parent = trace._Span("parent", parent_span_context) child = trace._Span( "child", trace_api.SpanContext( parent_span_context.trace_id, id_generator.RandomIdGenerator().generate_span_id(), is_remote=False, trace_flags=parent_span_context.trace_flags, trace_state=parent_span_context.trace_state, ), parent=parent.get_span_context(), ) new_carrier = {} ctx = trace_api.set_span_in_context(child, ctx) FORMAT.inject(new_carrier, context=ctx) return ctx, new_carrier class TestJaegerPropagator(TestCase): # pylint: disable=too-many-public-methods @classmethod def setUpClass(cls): generator = id_generator.RandomIdGenerator() cls.trace_id = generator.generate_trace_id() cls.span_id = generator.generate_span_id() cls.parent_span_id = generator.generate_span_id() cls.serialized_uber_trace_id = jaeger._format_uber_trace_id( # pylint: disable=protected-access cls.trace_id, cls.span_id, cls.parent_span_id, 11 ) def test_extract_valid_span(self): old_carrier = {FORMAT.TRACE_ID_KEY: self.serialized_uber_trace_id} ctx = FORMAT.extract(old_carrier) span_context = trace_api.get_current_span(ctx).get_span_context() self.assertEqual(span_context.trace_id, self.trace_id) self.assertEqual(span_context.span_id, self.span_id) def test_missing_carrier(self): old_carrier = {} ctx = FORMAT.extract(old_carrier) span_context = trace_api.get_current_span(ctx).get_span_context() self.assertEqual(span_context.trace_id, trace_api.INVALID_TRACE_ID) self.assertEqual(span_context.span_id, trace_api.INVALID_SPAN_ID) def test_trace_id(self): old_carrier = {FORMAT.TRACE_ID_KEY: self.serialized_uber_trace_id} _, new_carrier = get_context_new_carrier(old_carrier) self.assertEqual( self.serialized_uber_trace_id.split(":", maxsplit=1)[0], new_carrier[FORMAT.TRACE_ID_KEY].split(":")[0], ) def test_parent_span_id(self): old_carrier = {FORMAT.TRACE_ID_KEY: self.serialized_uber_trace_id} _, new_carrier = get_context_new_carrier(old_carrier) span_id = self.serialized_uber_trace_id.split(":")[1] parent_span_id = new_carrier[FORMAT.TRACE_ID_KEY].split(":")[2] self.assertEqual(span_id, parent_span_id) def test_sampled_flag_set(self): old_carrier = {FORMAT.TRACE_ID_KEY: self.serialized_uber_trace_id} _, new_carrier = get_context_new_carrier(old_carrier) sample_flag_value = int(new_carrier[FORMAT.TRACE_ID_KEY].split(":")[3]) & 0x01 self.assertEqual(1, sample_flag_value) def test_debug_flag_set(self): old_carrier = {FORMAT.TRACE_ID_KEY: self.serialized_uber_trace_id} _, new_carrier = get_context_new_carrier(old_carrier) debug_flag_value = int(new_carrier[FORMAT.TRACE_ID_KEY].split(":")[3]) & FORMAT.DEBUG_FLAG self.assertEqual(FORMAT.DEBUG_FLAG, debug_flag_value) def test_sample_debug_flags_unset(self): uber_trace_id = jaeger._format_uber_trace_id( # pylint: disable=protected-access self.trace_id, self.span_id, self.parent_span_id, 0 ) old_carrier = {FORMAT.TRACE_ID_KEY: uber_trace_id} _, new_carrier = get_context_new_carrier(old_carrier) flags = int(new_carrier[FORMAT.TRACE_ID_KEY].split(":")[3]) sample_flag_value = flags & 0x01 debug_flag_value = flags & FORMAT.DEBUG_FLAG self.assertEqual(0, sample_flag_value) self.assertEqual(0, debug_flag_value) def test_baggage(self): old_carrier = {FORMAT.TRACE_ID_KEY: self.serialized_uber_trace_id} input_baggage = {"key1": "value1"} _, new_carrier = get_context_new_carrier(old_carrier, input_baggage) ctx = FORMAT.extract(new_carrier) self.assertDictEqual(input_baggage, ctx[_BAGGAGE_KEY]) def test_non_string_baggage(self): old_carrier = {FORMAT.TRACE_ID_KEY: self.serialized_uber_trace_id} input_baggage = {"key1": 1, "key2": True} formatted_baggage = {"key1": "1", "key2": "True"} _, new_carrier = get_context_new_carrier(old_carrier, input_baggage) ctx = FORMAT.extract(new_carrier) self.assertDictEqual(formatted_baggage, ctx[_BAGGAGE_KEY]) def test_extract_empty_baggage_value(self): old_carrier = { FORMAT.TRACE_ID_KEY: self.serialized_uber_trace_id, "uberctx-key1": [], "uberctx-key2": None, "uberctx-key3": "value3", } context = FORMAT.extract(old_carrier) self.assertDictEqual({"key3": "value3"}, context[_BAGGAGE_KEY]) def test_extract_enforces_max_baggage_entries(self): old_carrier = {FORMAT.TRACE_ID_KEY: self.serialized_uber_trace_id} for index in range(200): old_carrier[f"uberctx-k{index}"] = f"v{index}" extracted = FORMAT.extract(old_carrier)[_BAGGAGE_KEY] self.assertEqual(FORMAT.MAX_BAGGAGE_ENTRIES, len(extracted)) self.assertIn("k0", extracted) self.assertNotIn("k180", extracted) def test_extract_drops_oversized_baggage_entry(self): old_carrier = { FORMAT.TRACE_ID_KEY: self.serialized_uber_trace_id, "uberctx-ok": "value", "uberctx-big": "x" * 5000, } context = FORMAT.extract(old_carrier) self.assertDictEqual({"ok": "value"}, context[_BAGGAGE_KEY]) def test_extract_measures_entry_limit_in_bytes(self): # 2100 multibyte characters is 4200 bytes: under the character limit, # over the byte limit, so the entry must be dropped. old_carrier = { FORMAT.TRACE_ID_KEY: self.serialized_uber_trace_id, "uberctx-ok": "value", "uberctx-u": "é" * 2100, } context = FORMAT.extract(old_carrier) self.assertDictEqual({"ok": "value"}, context[_BAGGAGE_KEY]) def test_extract_enforces_max_baggage_total_bytes(self): old_carrier = {FORMAT.TRACE_ID_KEY: self.serialized_uber_trace_id} for index in range(100): old_carrier[f"uberctx-k{index}"] = "y" * 200 extracted = FORMAT.extract(old_carrier)[_BAGGAGE_KEY] self.assertLess(len(extracted), 100) self.assertIn("k0", extracted) self.assertNotIn("k99", extracted) def test_extract_counts_the_key_value_separator_byte(self): old_carrier = { FORMAT.TRACE_ID_KEY: self.serialized_uber_trace_id, "uberctx-fits": "x" * (FORMAT.MAX_BAGGAGE_ENTRY_BYTES - len("fits") - 1), "uberctx-over": "x" * (FORMAT.MAX_BAGGAGE_ENTRY_BYTES - len("over")), } extracted = FORMAT.extract(old_carrier)[_BAGGAGE_KEY] self.assertIn("fits", extracted) self.assertNotIn("over", extracted) def test_extract_charges_a_separator_byte_between_accepted_entries(self): small_entry_bytes = len("small") + 1 # empty value filler_a_entry_bytes = FORMAT.MAX_BAGGAGE_ENTRY_BYTES filler_a_value_len = filler_a_entry_bytes - len("filler_a") - 1 target_total_after_fillers = FORMAT.MAX_BAGGAGE_TOTAL_BYTES - small_entry_bytes filler_b_entry_bytes = target_total_after_fillers - filler_a_entry_bytes - 1 filler_b_value_len = filler_b_entry_bytes - len("filler_b") - 1 old_carrier = { FORMAT.TRACE_ID_KEY: self.serialized_uber_trace_id, "uberctx-filler_a": "x" * filler_a_value_len, "uberctx-filler_b": "x" * filler_b_value_len, "uberctx-small": "", } extracted = FORMAT.extract(old_carrier)[_BAGGAGE_KEY] self.assertIn("filler_a", extracted) self.assertIn("filler_b", extracted) self.assertNotIn("small", extracted) def test_extract_stops_inspecting_after_the_candidate_limit(self): old_carrier = {FORMAT.TRACE_ID_KEY: self.serialized_uber_trace_id} for index in range(1000): old_carrier[f"uberctx-k{index}"] = "x" * 5000 class CountingGetter(DefaultGetter): def __init__(self): self.reads = 0 def get(self, carrier, key): self.reads += 1 return super().get(carrier, key) getter = CountingGetter() FORMAT.extract(old_carrier, getter=getter) self.assertLessEqual(getter.reads, FORMAT.MAX_BAGGAGE_ENTRIES + 1) def test_inject_enforces_max_baggage_entries(self): span = trace_api.NonRecordingSpan(trace_api.SpanContext(1, 1, True)) ctx = trace_api.set_span_in_context(span) for index in range(200): ctx = baggage.set_baggage(f"k{index}", f"v{index}", ctx) carrier = {} FORMAT.inject(carrier, context=ctx) self.assertEqual(FORMAT.MAX_BAGGAGE_ENTRIES, sum(1 for key in carrier if key.startswith(FORMAT.BAGGAGE_PREFIX))) self.assertIn(FORMAT.BAGGAGE_PREFIX + "k0", carrier) self.assertNotIn(FORMAT.BAGGAGE_PREFIX + "k180", carrier) def test_inject_enforces_max_baggage_total_bytes(self): span = trace_api.NonRecordingSpan(trace_api.SpanContext(1, 1, True)) ctx = trace_api.set_span_in_context(span) for index in range(100): ctx = baggage.set_baggage(f"k{index}", "y" * 200, ctx) carrier = {} FORMAT.inject(carrier, context=ctx) injected = [key for key in carrier if key.startswith(FORMAT.BAGGAGE_PREFIX)] self.assertLess(len(injected), 100) self.assertIn(FORMAT.BAGGAGE_PREFIX + "k0", carrier) self.assertNotIn(FORMAT.BAGGAGE_PREFIX + "k99", carrier) def test_extract_invalid_uber_trace_id(self): old_carrier = { "uber-trace-id": "000000000000000000000000deadbeef:00000000deadbef0:00", "uberctx-key1": "value1", } formatted_baggage = {"key1": "value1"} context = FORMAT.extract(old_carrier) span_context = trace_api.get_current_span(context).get_span_context() self.assertEqual(span_context.span_id, trace_api.INVALID_SPAN_ID) self.assertDictEqual(formatted_baggage, context[_BAGGAGE_KEY]) def test_extract_invalid_trace_id(self): old_carrier = { "uber-trace-id": "00000000000000000000000000000000:00000000deadbef0:00:00", "uberctx-key1": "value1", } formatted_baggage = {"key1": "value1"} context = FORMAT.extract(old_carrier) span_context = trace_api.get_current_span(context).get_span_context() self.assertEqual(span_context.trace_id, trace_api.INVALID_TRACE_ID) self.assertDictEqual(formatted_baggage, context[_BAGGAGE_KEY]) def test_extract_invalid_span_id(self): old_carrier = { "uber-trace-id": "000000000000000000000000deadbeef:0000000000000000:00:00", "uberctx-key1": "value1", } formatted_baggage = {"key1": "value1"} context = FORMAT.extract(old_carrier) span_context = trace_api.get_current_span(context).get_span_context() self.assertEqual(span_context.span_id, trace_api.INVALID_SPAN_ID) self.assertDictEqual(formatted_baggage, context[_BAGGAGE_KEY]) def test_fields(self): tracer = trace.TracerProvider().get_tracer("sdk_tracer_provider") mock_setter = Mock() with tracer.start_as_current_span("parent"): with tracer.start_as_current_span("child"): FORMAT.inject({}, setter=mock_setter) inject_fields = set() for call in mock_setter.mock_calls: inject_fields.add(call[1][1]) self.assertEqual(FORMAT.fields, inject_fields) def test_extract_no_trace_id_to_explicit_ctx(self): carrier = {} orig_ctx = Context({"k1": "v1"}) ctx = FORMAT.extract(carrier, orig_ctx) self.assertDictEqual(orig_ctx, ctx) def test_extract_no_trace_id_to_implicit_ctx(self): carrier = {} ctx = FORMAT.extract(carrier) self.assertDictEqual(Context(), ctx) def test_extract_invalid_uber_trace_id_header_to_explicit_ctx(self): trace_id_headers = [ "000000000000000000000000deadbeef:00000000deadbef0:00", "00000000000000000000000000000000:00000000deadbef0:00:00", "000000000000000000000000deadbeef:0000000000000000:00:00", "000000000000000000000000deadbeef:0000000000000000:00:xyz", ] for trace_id_header in trace_id_headers: with self.subTest(trace_id_header=trace_id_header): carrier = {"uber-trace-id": trace_id_header} orig_ctx = Context({"k1": "v1"}) ctx = FORMAT.extract(carrier, orig_ctx) self.assertDictEqual(orig_ctx, ctx) def test_extract_invalid_uber_trace_id_header_to_implicit_ctx(self): trace_id_headers = [ "000000000000000000000000deadbeef:00000000deadbef0:00", "00000000000000000000000000000000:00000000deadbef0:00:00", "000000000000000000000000deadbeef:0000000000000000:00:00", "000000000000000000000000deadbeef:0000000000000000:00:xyz", ] for trace_id_header in trace_id_headers: with self.subTest(trace_id_header=trace_id_header): carrier = {"uber-trace-id": trace_id_header} ctx = FORMAT.extract(carrier) self.assertDictEqual(Context(), ctx) def test_non_recording_span_does_not_crash(self): """Make sure propagator does not crash when working with NonRecordingSpan""" mock_setter = Mock() span = trace_api.NonRecordingSpan(trace_api.SpanContext(1, 1, True)) with trace_api.use_span(span, end_on_exit=True): with self.assertNotRaises(Exception): FORMAT.inject({}, setter=mock_setter) python-opentelemetry-1.45.2/pyproject.toml000066400000000000000000000216311527070070700207420ustar00rootroot00000000000000[project] name = "opentelemetry-python" version = "0.0.0" # This is not used. requires-python = ">=3.10" dependencies = [ "opentelemetry-api", "opentelemetry-sdk", "opentelemetry-configuration", "opentelemetry-semantic-conventions", "opentelemetry-proto", "opentelemetry-proto-json", "opentelemetry-test-utils", "opentelemetry-exporter-otlp-json-file", "opentelemetry-exporter-otlp-json-http", "opentelemetry-exporter-http-transport", "opentelemetry-exporter-otlp-proto-grpc", "opentelemetry-exporter-otlp-proto-http", "opentelemetry-exporter-otlp-proto-common", "opentelemetry-exporter-otlp-common", "opentelemetry-exporter-otlp-json-common", "opentelemetry-exporter-zipkin-json", "opentelemetry-exporter-prometheus", "opentelemetry-propagator-jaeger", "opentelemetry-propagator-b3", "opentelemetry-codegen-json", ] # https://docs.astral.sh/uv/reference/settings/ [tool.uv] package = false # https://docs.astral.sh/uv/reference/settings/#package required-version = ">=0.6.0" [tool.uv.sources] opentelemetry-api = { workspace = true} opentelemetry-sdk = { workspace = true } opentelemetry-configuration = { workspace = true } opentelemetry-proto = { workspace = true } opentelemetry-proto-json = { workspace = true } opentelemetry-semantic-conventions = { workspace = true } opentelemetry-test-utils = { workspace = true } opentelemetry-exporter-http-transport = { workspace = true } opentelemetry-exporter-otlp-json-file = { workspace = true } opentelemetry-exporter-otlp-json-http = { workspace = true } opentelemetry-exporter-otlp-proto-grpc = { workspace = true } opentelemetry-exporter-otlp-proto-http = { workspace = true } opentelemetry-exporter-otlp-proto-common = { workspace = true } opentelemetry-exporter-otlp-json-common = { workspace = true } opentelemetry-exporter-otlp-common = { workspace = true } opentelemetry-exporter-zipkin-json = { workspace = true } opentelemetry-exporter-prometheus = {workspace = true } opentelemetry-propagator-jaeger = { workspace = true } opentelemetry-propagator-b3 = { workspace = true } opentelemetry-codegen-json = { workspace = true } [tool.uv.workspace] members = [ "opentelemetry-api", "opentelemetry-sdk", "opentelemetry-configuration", "opentelemetry-semantic-conventions", "opentelemetry-proto", "opentelemetry-proto-json", "exporter/*", "propagator/*", "codegen/*", "tests/opentelemetry-test-utils", ] exclude = [ "exporter/opentelemetry-exporter-opencensus", "exporter/opentelemetry-exporter-zipkin", "exporter/opentelemetry-exporter-zipkin-proto-http", ] [tool.pytest.ini_options] addopts = "-rs -v" log_cli = true [tool.ruff] # https://docs.astral.sh/ruff/configuration/ target-version = "py310" line-length = 120 force-exclude = true extend-exclude = [ "*_pb2*.py*", "opentelemetry-proto-json/src/*", ] 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 = [ "E", # https://docs.astral.sh/ruff/rules/#error-e "F", # https://docs.astral.sh/ruff/rules/#pyflakes-f "G", # https://docs.astral.sh/ruff/rules/#flake8-logging-format-g "I", # https://docs.astral.sh/ruff/rules/#isort-i "PLC", # https://docs.astral.sh/ruff/rules/#convention-plc "PLE", # https://docs.astral.sh/ruff/rules/#error-ple "Q", # https://docs.astral.sh/ruff/rules/#flake8-quotes-q "TID", # https://docs.astral.sh/ruff/rules/#flake8-tidy-imports-tid "PIE", # https://docs.astral.sh/ruff/rules/#flake8-pie-pie "UP", # https://docs.astral.sh/ruff/rules/#pyupgrade-up "W", # https://docs.astral.sh/ruff/rules/#warning-w ] # Ignore rules from the default ruleset that don't have auto-fixes available and need to be # manually resolved, plus RUF023 which is intentionally ignored. ignore = [ "B008", "B017", "B018", "B020", "B023", "B039", "BLE001", "C401", "C408", "C417", "DTZ007", "E501", "FLY002", "ISC004", "LOG015", "PERF102", "PLW0602", "PLW1508", "PT014", "PYI034", "PYI036", "RUF009", "RUF012", "RUF013", "RUF015", "RUF023", "S110", "SIM102", "SIM103", "SIM115", "SIM117", "SIM118", "TRY002", "TRY004", "TRY201", "TRY401", ] [tool.ruff.lint.per-file-ignores] "docs/**/*.*" = ["PLE"] "opentelemetry-configuration/tests/test_models.py" = ["E402", "PLC0415"] "shim/opentelemetry-opentracing-shim/tests/*" = ["TID252"] # Deprecated semconv enum members are intentionally aliased to the same value # as their replacement (e.g. GenAiTokenTypeValues.COMPLETION == .OUTPUT). The # semantic_attributes.j2 template does not emit a `noqa`, so an inline # suppression in the generated file would be lost on the next `generate.sh`. "opentelemetry-semantic-conventions/src/opentelemetry/semconv/**/attributes/*.py" = ["PIE796"] [tool.ruff.lint.isort] known-third-party = [ "psutil", "pytest", "redis", "redis_opentracing", "opencensus", ] known-first-party = ["opentelemetry", "opentelemetry_example_app"] [tool.ruff.format] docstring-code-format = true [tool.pyright] typeCheckingMode = "standard" pythonVersion = "3.10" include = [ "opentelemetry-semantic-conventions", "opentelemetry-api", "opentelemetry-sdk", "opentelemetry-configuration", "opentelemetry-proto-json", "exporter/opentelemetry-exporter-otlp-json-file", "exporter/opentelemetry-exporter-otlp-json-http", "exporter/opentelemetry-exporter-http-transport", "exporter/opentelemetry-exporter-otlp-proto-grpc", "exporter/opentelemetry-exporter-otlp-proto-http", "exporter/opentelemetry-exporter-otlp-json-common", "exporter/opentelemetry-exporter-otlp-common", "codegen/opentelemetry-codegen-json", "propagator/opentelemetry-propagator-jaeger", ] exclude = [ "opentelemetry-sdk/tests", "opentelemetry-configuration/tests", "opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/__init__.py", "opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/aggregation.py", "opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exemplar/exemplar_reservoir.py", "opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/export/", "opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/instrument.py", "opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/measurement_consumer.py", "opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/metric_reader_storage.py", "opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/view.py", "opentelemetry-sdk/benchmarks", "exporter/opentelemetry-exporter-http-transport/tests", "exporter/opentelemetry-exporter-otlp-proto-grpc/tests", "exporter/opentelemetry-exporter-otlp-proto-http/tests", "exporter/opentelemetry-exporter-otlp-json-common/tests", "exporter/opentelemetry-exporter-otlp-json-http/tests", "exporter/opentelemetry-exporter-otlp-common/tests", "exporter/opentelemetry-exporter-otlp-json-common/benchmarks", "propagator/opentelemetry-propagator-jaeger/tests", ] # When packages are correct typed add them to the strict list strict = [ "opentelemetry-semantic-conventions", "opentelemetry-sdk/src/opentelemetry/sdk/environment_variables", "opentelemetry-sdk/src/opentelemetry/sdk/resources", "opentelemetry-api/src/opentelemetry/attributes/__init__.py", ] [tool.datamodel-codegen] input = "opentelemetry-configuration/src/opentelemetry/configuration/schema.json" input-file-type = "jsonschema" output = "opentelemetry-configuration/src/opentelemetry/configuration/models.py" output-model-type = "dataclasses.dataclass" field-constraints = true formatters = ["ruff-format", "ruff-check"] use-standard-collections = true use-schema-description = true use-title-as-name = true use-union-operator = true target-python-version = "3.10" custom-template-dir = "opentelemetry-configuration/codegen" additional-imports = "typing.ClassVar,opentelemetry.configuration._common._additional_properties" [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/pull/{issue})" wrap = true # wrap fragments to 79 char line length issue_pattern = "^(\\d+)" # only PR numbers as fragment prefix (e.g. 1234.fixed) # 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 [tool.inline-snapshot] format-command="ruff format --stdin-filename {filename}" [dependency-groups] dev = [ "tox", "tox-uv>=1", "pre-commit", "datamodel-code-generator[http]", "datamodel-code-generator[ruff]", "towncrier", ] python-opentelemetry-1.45.2/rationale.md000066400000000000000000000074521527070070700203330ustar00rootroot00000000000000# OpenTelemetry Rationale When creating a library, often times designs and decisions are made that get lost over time. This document tries to collect information on design decisions to answer common questions that may come up when you explore the SDK. ## Versioning and Releasing This document describes the versioning and stability policy of components shipped from this repository, as per the [OpenTelemetry versioning and stability specification](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/versioning-and-stability.md). The OpenTelemetry implementations, the OpenTelemetry Spec itself and this repo follows [SemVer V2](https://semver.org/spec/v2.0.0.html) guidelines. This means that, for any stable packages released from this repo, all public APIs will remain [backward compatible](https://www.python.org/dev/peps/pep-0387/), unless a major version bump occurs. This applies to the API, SDK, as well as Exporters, Instrumentation etc. shipped from this repo. For example, users can take a dependency on 1.0.0 version of any package, with the assurance that all future releases until 2.0.0 will be backward compatible. ## Goals ### API Stability Once the API for a given signal (spans, logs, metrics, baggage) has been officially released, that API module will function with any SDK that has the same major version, and equal or greater minor or patch version. For example, libraries that are instrumented with `opentelemetry-api 1.0.1` will function with SDK library `opentelemetry-sdk 1.11.33` or `opentelemetry-sdk 1.3.4`. ### SDK Stability: Public portions of the SDK (constructors, configuration, end-user interfaces) must remain backwards compatible. Internal interfaces are allowed to break. ## Core components Core components refer to the set of components which are required as per the spec. This includes API, SDK, propagators (B3 and Jaeger) and exporters which are required by the specification. These exporters are OTLP, and Zipkin. ## Mature or stable Signals Modules for mature (i.e. released) signals will be found in the latest versions of the corresponding packages of the core components. The version numbers of these will have no suffix appended, indicating they are stable. For example, the package `opentelemetry-api` v1.x.y will be considered stable. ## Pre-releases Pre-release packages are denoted by appending identifiers such as -Alpha, -Beta, -RC etc. There are no API guarantees in pre-releases. Each release can contain breaking changes and functionality could be removed as well. In general, an RC pre-release is more stable than a Beta release, which is more stable than an Alpha release. ### Immature or experimental signals Modules for experimental signals will be released in the same packages as the core components, but prefixed with `_` to indicate that they are unstable and subject to change. NO STABILITY GUARANTEES ARE MADE. ## Examples Purely for illustration purposes, not intended to represent actual releases: #### V1.0.0 Release (tracing, baggage, propagators, context) - `opentelemetry-api` 1.0.0 - Contains APIs for tracing, baggage, propagators, context - `opentelemetry-sdk` 1.0.0 - Contains SDK components for tracing, baggage, propagators, and context #### V1.15.0 Release (with metrics) - `opentelemetry-api` 1.15.0 - Contains APIs for tracing, baggage, propagators, context, and metrics - `opentelemetry-sdk` 1.15.0 - Contains SDK components for tracing, baggage, propagators, context and metrics ##### Contains the following pre-release packages - `opentelemetry-api` 1.x.yrc1 - Contains the experimental public API for logging plus other unstable features. There are no stability guarantees. - `opentelemetry-sdk` 1.x.yrc1 - Contains the experimental public SDK for logging plus other unstable features. There are no stability guarantees. python-opentelemetry-1.45.2/scripts/000077500000000000000000000000001527070070700175125ustar00rootroot00000000000000python-opentelemetry-1.45.2/scripts/add_required_checks.py000066400000000000000000000031171527070070700240360ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # This script is to be used by maintainers by running it locally. from json import dumps from os import environ from requests import put from yaml import safe_load job_names = ["EasyCLA"] # Check that the files below are all the workflow YAML files that should be # considered. for yml_file_name in [ "test_0", "test_1", "misc_0", "lint_0", "contrib_0", "check-links", ]: with open(f"../.github/workflows/{yml_file_name}.yml") as yml_file: job_names.extend([job["name"] for job in safe_load(yml_file)["jobs"].values()]) owner = "open-telemetry" repo = "opentelemetry-python" branch = "main" response = put( (f"https://api.github.com/repos/{owner}/{repo}/branches/{branch}/protection/required_status_checks/contexts"), headers={ "Accept": "application/vnd.github.v3+json", # The token has to be created in Github, and exported to the # environment variable below. When creating the token, the resource # owner must be open-telemetry, the access must be for the repo above, # and read and write permissions must be granted for administration # permissions and read permissions must be granted for metadata # permissions. "Authorization": f"token {environ.get('REQUIRED_CHECKS_TOKEN')}", }, data=dumps({"contexts": job_names}), ) if response.status_code == 200: print(response.content) else: print(f"Failed to update branch protection settings. Status code: {response.status_code}") print(response.json()) python-opentelemetry-1.45.2/scripts/build.sh000077500000000000000000000015671527070070700211610ustar00rootroot00000000000000#!/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:?}/* for d in opentelemetry-api/ opentelemetry-configuration/ opentelemetry-sdk/ opentelemetry-proto/ opentelemetry-proto-json/ opentelemetry-semantic-conventions/ exporter/*/ shim/*/ propagator/*/ tests/opentelemetry-test-utils/; 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 ) python-opentelemetry-1.45.2/scripts/changelog_template.j2000066400000000000000000000012321527070070700235670ustar00rootroot00000000000000## 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 %} python-opentelemetry-1.45.2/scripts/check_for_valid_readme.py000066400000000000000000000025411527070070700245050ustar00rootroot00000000000000# 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: error = True 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() python-opentelemetry-1.45.2/scripts/check_license_header.py000066400000000000000000000047301527070070700241570ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import os import sys from pathlib import Path EXPECTED_LINES = ( "# Copyright The OpenTelemetry Authors\n", "# 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 return lines[start] == EXPECTED_LINES[0] and lines[start + 1] == EXPECTED_LINES[1] 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 The OpenTelemetry Authors") 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() python-opentelemetry-1.45.2/scripts/coverage.sh000077500000000000000000000020031527070070700216370ustar00rootroot00000000000000#!/bin/bash set -e function cov { if [ ${TOX_ENV_NAME:0:4} == "py34" ] then pytest \ --ignore-glob=instrumentation/opentelemetry-instrumentation-opentracing-shim/tests/testbed/* \ --cov ${1} \ --cov-append \ --cov-branch \ --cov-report='' \ ${1} else pytest \ --cov ${1} \ --cov-append \ --cov-branch \ --cov-report='' \ ${1} fi } coverage erase cov opentelemetry-api cov opentelemetry-sdk cov exporter/opentelemetry-exporter-datadog cov instrumentation/opentelemetry-instrumentation-flask cov instrumentation/opentelemetry-instrumentation-requests cov instrumentation/opentelemetry-instrumentation-opentracing-shim cov util/opentelemetry-util-http cov exporter/opentelemetry-exporter-zipkin cov instrumentation/opentelemetry-instrumentation-aiohttp-client cov instrumentation/opentelemetry-instrumentation-asgi coverage report --show-missing coverage xml python-opentelemetry-1.45.2/scripts/eachdist.py000077500000000000000000000544531527070070700216660ustar00rootroot00000000000000#!/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 inspect import cleandoc from itertools import chain from os.path import basename from pathlib import Path, PurePath from toml import load DEFAULT_ALLSEP = " " DEFAULT_ALLFMT = "{rel}" 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.""" ), ) 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 ("setup.py", "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): rootdir = str(find_projectroot()) runsubprocess( args.dry_run, ("black", "--config", "pyproject.toml", ".") + (("--diff", "--check") if args.check_only else ()), cwd=rootdir, check=True, ) runsubprocess( args.dry_run, ("isort", "--settings-path", ".isort.cfg", ".") + (("--diff", "--check-only") if args.check_only else ()), cwd=rootdir, check=True, ) runsubprocess(args.dry_run, ("flake8", "--config", ".flake8", rootdir), 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 find(name, path): for root, _, files in os.walk(path): if name in files: return os.path.join(root, name) return None def filter_packages(targets, packages): package_names = set(packages) filtered_packages = [] for target in targets: pyproject = target.joinpath("pyproject.toml") if not pyproject.exists(): continue target_name = load(pyproject).get("project", {}).get("name") if target_name in package_names: filtered_packages.append(target) return filtered_packages def update_version_files(targets, version, packages): print("updating version/__init__.py files") search = "__version__ .*" replace = f'__version__ = "{version}"' for target in filter_packages(targets, packages): version_file_path = target.joinpath(load(target.joinpath("pyproject.toml"))["tool"]["hatch"]["version"]["path"]) with open(version_file_path) as file: text = file.read() if replace in text: print(f"{version_file_path} already contains {replace}") continue with open(version_file_path, "w", encoding="utf-8") as file: file.write(re.sub(search, replace, text)) def update_dependencies(targets, version, packages): print("updating dependencies") # 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})(.*\.dev)" replace = r"\1\2 " + version update_files( targets, "pyproject.toml", search, replace, ) def update_patch_dependencies(targets, version, prev_version, packages): print("updating patch dependencies") # 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*)" rf"(\s?({operators_pattern})\s?)(.*{re.escape(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 = [] for group in versions.split(","): mcfg = cfg[group] version = mcfg["version"] updated_versions.append(version) packages = mcfg["packages"].split() print(f"update {group} packages to {version}") update_dependencies(targets, version, packages) update_version_files(targets, version, packages) 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")) # 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): root_dir = format_dir = str(find_projectroot()) if args.path: format_dir = os.path.join(format_dir, args.path) runsubprocess( args.dry_run, ("black", "--config", f"{root_dir}/pyproject.toml", "."), cwd=format_dir, check=True, ) runsubprocess( args.dry_run, ( "isort", "--settings-path", f"{root_dir}/.isort.cfg", "--profile", "black", ".", ), cwd=format_dir, check=True, ) def version_args(args): cfg = ConfigParser() cfg.read(str(find_projectroot() / "eachdist.ini")) print(cfg[args.mode]["version"]) def main(): args = parse_args() args.func(args) if __name__ == "__main__": main() python-opentelemetry-1.45.2/scripts/generate_website_docs.sh000077500000000000000000000003371527070070700244000ustar00rootroot00000000000000#!/bin/bash set -e # this script generates the documentation required for # opentelemetry.io pip install -r docs-requirements.txt TMP_DIR=/tmp/python_otel_docs rm -Rf ${TMP_DIR} sphinx-build -M jekyll ./docs ${TMP_DIR} python-opentelemetry-1.45.2/scripts/griffe_check.py000066400000000000000000000037551527070070700224750ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import argparse import sys import griffe from eachdist import find_projectroot, find_targets def get_modules() -> list[str]: rootpath = find_projectroot() targets = find_targets("DEFAULT", rootpath) dirs_to_exclude = [ "docs", "scripts", "opentelemetry-docker-tests", "examples", "_template", ] packages = [] for target in targets: rel_path = target.relative_to(rootpath) if not any(excluded in str(rel_path) for excluded in dirs_to_exclude): packages.append(str(rel_path / "src")) return packages def main(): parser = argparse.ArgumentParser( description="Check for breaking changes using griffe", formatter_class=argparse.ArgumentDefaultsHelpFormatter, ) parser.add_argument( "--module", default="opentelemetry", help="Name of the module to check for breaking changes (e.g., opentelemetry, opentelemetry.sdk, opentelemetry.sdk.resources)", ) parser.add_argument( "--against", default="main", help="Git ref to compare against (e.g., branch, tag, or commit)", ) args = parser.parse_args() modules = get_modules() base = griffe.load(args.module, search_paths=modules) against = griffe.load_git(args.module, ref=args.against, search_paths=modules) breakages = list(griffe.find_breaking_changes(against, base)) # exclude version bumps from breakages as they are expected breakages = [b for b in breakages if b._format_title() != "__version__"] if breakages: for b in breakages: # We can use `b.explain()` to get a detailed explanation of the breaking change # and we can iterate over breakages to perform more complex logic # like skipping per object.path or breakage type print(b.explain()) return 1 return 0 if __name__ == "__main__": sys.exit(main()) python-opentelemetry-1.45.2/scripts/proto_codegen.sh000077500000000000000000000040361527070070700227030ustar00rootroot00000000000000#!/bin/bash # # Regenerate python code from OTLP protos in # https://github.com/open-telemetry/opentelemetry-proto # # To use, update PROTO_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: # PROTO_REPO_DIR - the path to an existing checkout of the opentelemetry-proto repo # Pinned commit/branch/tag for the current version used in opentelemetry-proto python package. PROTO_REPO_BRANCH_OR_COMMIT="v1.10.0" set -e PROTO_REPO_DIR=${PROTO_REPO_DIR:-"/tmp/opentelemetry-proto"} # root of opentelemetry-python repo repo_root="$(git rev-parse --show-toplevel)" protoc() { uvx -c $repo_root/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 "$PROTO_REPO_DIR" ]; then git clone https://github.com/open-telemetry/opentelemetry-proto.git $PROTO_REPO_DIR fi # Pull in changes and switch to requested branch ( cd $PROTO_REPO_DIR git fetch --all git checkout $PROTO_REPO_BRANCH_OR_COMMIT # pull if PROTO_REPO_BRANCH_OR_COMMIT is not a detached head git symbolic-ref -q HEAD && git pull --ff-only || true ) cd $repo_root/opentelemetry-proto/src # clean up old generated code find opentelemetry/ -regex ".*_pb2.*\.pyi?" -exec rm {} + # generate proto code for all protos all_protos=$(find $PROTO_REPO_DIR/ -iname "*.proto") protoc \ -I $PROTO_REPO_DIR \ --python_out=. \ --mypy_out=. \ $all_protos # generate grpc output only for protos with service definitions service_protos=$(grep -REl "service \w+ {" $PROTO_REPO_DIR/opentelemetry/) protoc \ -I $PROTO_REPO_DIR \ --python_out=. \ --mypy_out=. \ --grpc_python_out=. \ $service_protos echo "Please update ./opentelemetry-proto/README.rst to include the updated version." python-opentelemetry-1.45.2/scripts/proto_codegen_json.sh000077500000000000000000000034571527070070700237420ustar00rootroot00000000000000#!/bin/bash # # Regenerate python code from OTLP protos in # https://github.com/open-telemetry/opentelemetry-proto # # To use, update PROTO_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: # PROTO_REPO_DIR - the path to an existing checkout of the opentelemetry-proto repo # Pinned commit/branch/tag for the current version used in opentelemetry-proto python package. PROTO_REPO_BRANCH_OR_COMMIT="v1.10.0" set -e PROTO_REPO_DIR=${PROTO_REPO_DIR:-"/tmp/opentelemetry-proto"} # root of opentelemetry-python repo repo_root="$(git rev-parse --show-toplevel)" protoc() { uvx -c $repo_root/gen-requirements.txt \ --python 3.12 \ --from grpcio-tools \ --with "$repo_root/codegen/opentelemetry-codegen-json" \ python -m grpc_tools.protoc "$@" } protoc --version # Clone the proto repo if it doesn't exist if [ ! -d "$PROTO_REPO_DIR" ]; then git clone https://github.com/open-telemetry/opentelemetry-proto.git $PROTO_REPO_DIR fi # Pull in changes and switch to requested branch ( cd $PROTO_REPO_DIR git fetch --all git checkout $PROTO_REPO_BRANCH_OR_COMMIT # pull if PROTO_REPO_BRANCH_OR_COMMIT is not a detached head git symbolic-ref -q HEAD && git pull --ff-only || true ) cd $repo_root/opentelemetry-proto-json/src # clean up old generated code find opentelemetry/proto_json/ -name "*.py" -delete # generate proto code for all protos all_protos=$(find $PROTO_REPO_DIR/ -iname "*.proto") protoc \ -I $PROTO_REPO_DIR \ --otlp_json_out=. \ $all_protos echo "Please update ./opentelemetry-proto-json/README.rst to include the updated version." python-opentelemetry-1.45.2/scripts/public_symbols_checker.py000066400000000000000000000111411527070070700245740ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from collections import defaultdict from difflib import unified_diff from pathlib import Path from re import match from sys import exit from git import Repo from git.db import GitDB repo = Repo(__file__, odbt=GitDB, search_parent_directories=True) added_symbols = defaultdict(list) removed_symbols = defaultdict(list) def get_symbols(change_type, diff_lines_getter, prefix): if change_type == "D" or prefix == r"\-": file_path_symbols = removed_symbols else: file_path_symbols = added_symbols for diff_lines in repo.commit("main").diff(repo.head.commit).iter_change_type(change_type): if diff_lines.b_blob is None: # This happens if a file has been removed completely. b_file_path = diff_lines.a_blob.path else: b_file_path = diff_lines.b_blob.path b_file_path_obj = Path(b_file_path) if ( b_file_path_obj.suffix != ".py" or "opentelemetry" not in b_file_path or any( # single leading underscore part[0] == "_" and part[1] != "_" # tests directories or part == "tests" # benchmarks directories or part == "benchmarks" for part in b_file_path_obj.parts ) ): continue for diff_line in diff_lines_getter(diff_lines): matching_line = match( r"{prefix}({symbol_re})\s=\s.+|" r"{prefix}def\s({symbol_re})|" r"{prefix}class\s({symbol_re})".format(symbol_re=r"[a-zA-Z][_\w]+", prefix=prefix), diff_line, ) if matching_line is not None: file_path_symbols[b_file_path].append(next(filter(bool, matching_line.groups()))) def a_diff_lines_getter(diff_lines): return diff_lines.b_blob.data_stream.read().decode("utf-8").split("\n") def d_diff_lines_getter(diff_lines): return diff_lines.a_blob.data_stream.read().decode("utf-8").split("\n") def m_diff_lines_getter(diff_lines): return unified_diff( diff_lines.a_blob.data_stream.read().decode("utf-8").split("\n"), diff_lines.b_blob.data_stream.read().decode("utf-8").split("\n"), ) get_symbols("A", a_diff_lines_getter, r"") get_symbols("D", d_diff_lines_getter, r"") get_symbols("M", m_diff_lines_getter, r"\+") get_symbols("M", m_diff_lines_getter, r"\-") def remove_common_symbols(): # For each file, we remove the symbols that are added and removed in the # same commit. common_symbols = defaultdict(list) for file_path, symbols in added_symbols.items(): for symbol in symbols: if symbol in removed_symbols[file_path]: common_symbols[file_path].append(symbol) for file_path, symbols in common_symbols.items(): for symbol in symbols: added_symbols[file_path].remove(symbol) removed_symbols[file_path].remove(symbol) # If a file has no added or removed symbols, we remove it from the # dictionaries. for file_path in list(added_symbols.keys()): if not added_symbols[file_path]: del added_symbols[file_path] for file_path in list(removed_symbols.keys()): if not removed_symbols[file_path]: del removed_symbols[file_path] # If a symbol is added and removed in the same commit, we consider it as not # added or removed. remove_common_symbols() if added_symbols or removed_symbols: print("The code in this branch adds the following public symbols:") print() for file_path_, symbols_ in added_symbols.items(): print(f"- {file_path_}") for symbol_ in symbols_: print(f"\t{symbol_}") print() print( "Please make sure that all of them are strictly necessary, if not, " "please consider prefixing them with an underscore to make them " 'private. After that, please label this PR with "Approve Public API ' 'check".' ) print() print("The code in this branch removes the following public symbols:") print() for file_path_, symbols_ in removed_symbols.items(): print(f"- {file_path_}") for symbol_ in symbols_: print(f"\t{symbol_}") print() print( "Please make sure no public symbols are removed, if so, please " "consider deprecating them instead. After that, please label this " 'PR with "Approve Public API check".' ) exit(1) else: print("The code in this branch will not add any public symbols") python-opentelemetry-1.45.2/scripts/semconv/000077500000000000000000000000001527070070700211645ustar00rootroot00000000000000python-opentelemetry-1.45.2/scripts/semconv/.gitignore000066400000000000000000000000331527070070700231500ustar00rootroot00000000000000opentelemetry-specificationpython-opentelemetry-1.45.2/scripts/semconv/generate.sh000077500000000000000000000036511527070070700233220ustar00rootroot00000000000000#!/bin/bash set -ex SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" ROOT_DIR="${SCRIPT_DIR}/../.." # freeze the spec version to make SemanticAttributes generation reproducible SEMCONV_VERSION=1.44.0 SEMCONV_VERSION_TAG=v$SEMCONV_VERSION OTEL_WEAVER_IMG_VERSION=v0.24.2 INCUBATING_DIR=_incubating cd ${SCRIPT_DIR} rm -rf semantic-conventions || true mkdir semantic-conventions cd semantic-conventions git init git remote add origin https://github.com/open-telemetry/semantic-conventions.git git fetch origin "$SEMCONV_VERSION_TAG" git reset --hard FETCH_HEAD cd ${SCRIPT_DIR} # Check new schema version was added to schemas.py manually SCHEMAS_PY_PATH=${ROOT_DIR}/opentelemetry-semantic-conventions/src/opentelemetry/semconv/schemas.py if ! grep -q $SEMCONV_VERSION "$SCHEMAS_PY_PATH"; then echo "Error: schema version $SEMCONV_VERSION is not found in $SCHEMAS_PY_PATH. Please add it manually." exit 1 fi generate() { TARGET=$1 OUTPUT=$2 FILTER=$3 docker run --rm \ -v ${SCRIPT_DIR}/semantic-conventions/model:/source \ -v ${SCRIPT_DIR}/templates:/templates \ -v ${ROOT_DIR}/opentelemetry-semantic-conventions/src/opentelemetry/semconv/:/output \ otel/weaver:$OTEL_WEAVER_IMG_VERSION \ registry \ generate \ --registry=/source \ --templates=/templates \ ${TARGET} \ /output/${TARGET} \ --param output=${OUTPUT} \ --param filter=${FILTER} } # stable attributes and metrics mkdir -p ${ROOT_DIR}/opentelemetry-semantic-conventions/src/opentelemetry/semconv/attributes mkdir -p ${ROOT_DIR}/opentelemetry-semantic-conventions/src/opentelemetry/semconv/metrics generate "./" "./" "stable" mkdir -p ${ROOT_DIR}/opentelemetry-semantic-conventions/src/opentelemetry/semconv/${INCUBATING_DIR}/attributes mkdir -p ${ROOT_DIR}/opentelemetry-semantic-conventions/src/opentelemetry/semconv/${INCUBATING_DIR}/metrics generate "./" "./${INCUBATING_DIR}/" "any" cd "$ROOT_DIR" tox -e precommit python-opentelemetry-1.45.2/scripts/semconv/templates/000077500000000000000000000000001527070070700231625ustar00rootroot00000000000000python-opentelemetry-1.45.2/scripts/semconv/templates/registry/000077500000000000000000000000001527070070700250325ustar00rootroot00000000000000python-opentelemetry-1.45.2/scripts/semconv/templates/registry/common.j2000066400000000000000000000016771527070070700265720ustar00rootroot00000000000000{%- macro file_header() -%} # Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 {% endmacro -%} {%- macro str_or_empty(str) -%} {% if str is none %}{{""}}{% else %}{{str}}{% endif %} {%- endmacro %} {%- macro remove_trailing_dots(str) -%} {%- if str[-1:] == '.' -%}{{ remove_trailing_dots(str[:-1]) }}{%- else -%}{{ str }}{%- endif -%} {%- endmacro -%} {%- macro comment_with_prefix(str, prefix) -%} {{remove_trailing_dots(str | trim(' \n')) | comment_with_prefix(prefix) | replace("\\", "\\\\")}} {%- endmacro %} {%- macro import_deprecated(semconv) -%} {%- if (semconv | select("deprecated") | list | count > 0) or (ctx.filter == "any" and semconv | select("stable") | list | count > 0) -%} from typing_extensions import deprecated {%- endif -%} {%- endmacro-%} {%- macro deprecated_note_or_empty(attribute) -%} {% if attribute is deprecated %}{{ attribute.deprecated.note }}{% else %}{{""}}{% endif %} {%- endmacro %} python-opentelemetry-1.45.2/scripts/semconv/templates/registry/semantic_attributes.j2000066400000000000000000000060331527070070700313420ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from typing import Final {% set file_name = ctx.output + (ctx.root_namespace | snake_case) ~ "_attributes.py" -%} {{- template.set_file_name(file_name) -}} {%- import 'common.j2' as c %} {% set attributes = ctx.attributes | list %} {% set enum_attributes = attributes | select("enum") | rejectattr("name", "in", ctx.excluded_attributes) | list %} {% if enum_attributes | count > 0 %}from enum import Enum{% endif %} {{c.import_deprecated(enum_attributes)}} {%- macro attribute_name(attribute) -%} {{ attribute.name | screaming_snake_case }}{%- if attribute.type is template_type -%}_TEMPLATE{%- endif -%} {%- endmacro -%} {%- macro stable_class_ref(const_name, separator) -%} {{ctx.stable_package_name}}.{{ctx.root_namespace}}_attributes{{separator}}{{const_name}} {%- endmacro %} {%- macro write_docstring(name, brief, note, deprecated_note, stability, multiline) -%} {%- if multiline %}""" {% endif %} {%- if deprecated_note is mapping -%} {{prefix}}Deprecated: {{c.comment_with_prefix(deprecated_note.note, "")}}. {%- elif c.str_or_empty(deprecated_note)|length -%} {{prefix}}Deprecated: {{c.comment_with_prefix(deprecated_note, "")}}. {%- elif ctx.filter == "any" and stability == "stable" -%} {{prefix}}Deprecated in favor of stable :py:const:`{{stable_class_ref(name, '.')}}`. {%- elif c.str_or_empty(brief)|length -%} {{prefix}}{{c.comment_with_prefix(brief, "")}}. {%- if c.str_or_empty(note)|length %} {{prefix}}Note: {{c.comment_with_prefix(note, "")}}. {%- endif -%} {%- endif -%} {%- if multiline %} """{%- endif %} {%- endmacro -%} {% for attribute in attributes %} {% set attr_name = attribute_name(attribute) %} {%- set multiline = attribute.name not in ctx.excluded_attributes -%} {%- set deprecated_note = c.deprecated_note_or_empty(attribute) %} {%- set doc_string = write_docstring(attr_name, attribute.brief, attribute.note, deprecated_note, attribute.stability, multiline)-%} {%- set prefix = "" if multiline else "# " -%} {{prefix}}{{attr_name}}: Final = "{{attribute.name}}" {{prefix}}{{doc_string}} {% endfor %} {% for attribute in enum_attributes %}{%- set class_name = attribute.name | map_text("py_enum_attribute_to_class_name", attribute.name | pascal_case ~ "Values") -%} {%- if attribute is deprecated %} @deprecated("The attribute {{attribute.name}} is deprecated - {{ c.comment_with_prefix(attribute.deprecated.note, "") }}") {%- elif attribute.stability == "stable" and ctx.filter == "any" %} @deprecated("Deprecated in favor of stable :py:const:`{{stable_class_ref(class_name, '.')}}`.") {%- endif %} class {{class_name}}(Enum): {%- for member in attribute.type.members %} {% set member_name = member.id | screaming_snake_case -%} {%- set doc_string=write_docstring(class_name + '.' + member_name, member.brief or member.id, "", member.deprecated, member.stability, false)-%} {{member_name}} = {{ member.value | print_member_value }} {% if doc_string %}"""{{doc_string}}"""{% endif %} {%- endfor %} {% endfor %} python-opentelemetry-1.45.2/scripts/semconv/templates/registry/semantic_metrics.j2000066400000000000000000000063751527070070700306330ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 {% set file_name = ctx.output + ctx.root_namespace | snake_case ~ "_metrics.py" -%} {{- template.set_file_name(file_name) -}} {% import 'common.j2' as c -%} {%- macro stable_class_ref(const_name, separator) -%} {{ctx.stable_package_name}}.{{ctx.root_namespace}}_metrics{{separator}}{{const_name}} {%- endmacro %} {%- macro write_docstring(metric, const_name, prefix) -%} {%- if metric is deprecated %} {{prefix}}Deprecated: {{c.comment_with_prefix(metric.deprecated.note, prefix)}}. {%- elif ctx.filter == "any" and metric.stability == "stable" %} {{prefix}}Deprecated in favor of stable :py:const:`{{stable_class_ref(const_name, '.')}}`. {%- else -%} {%- if c.str_or_empty(metric.brief)|length %} {{prefix}}{{c.comment_with_prefix(metric.brief, prefix)}} {%- endif %} {{prefix}}Instrument: {{ metric.instrument }} {{prefix}}Unit: {{ metric.unit }} {%- if c.str_or_empty(metric.note)|length %} {{prefix}}Note: {{c.comment_with_prefix(metric.note, prefix)}}. {%- endif -%} {%- endif -%} {%- endmacro -%} {%- macro import_instrument_classes(metrics) -%} {% if ctx.filter == "any" %} from opentelemetry.metrics import Meter {%- set instruments = ["counter", "histogram", "updowncounter"]-%} {%- for i in instruments -%} {%- if ctx.metrics | selectattr("instrument", "equalto", i) | list | count > 0 %} from opentelemetry.metrics import {{i | map_text("py_instrument_to_type")}} {%- endif -%} {%- endfor-%} {%- if ctx.metrics | selectattr("instrument", "equalto", "gauge") | list | count > 0 %} from collections.abc import Callable, Generator, Iterable, Sequence from opentelemetry.metrics import CallbackOptions, ObservableGauge, Observation # pylint: disable=invalid-name CallbackT = ( Callable[[CallbackOptions], Iterable[Observation]] | Generator[Iterable[Observation], CallbackOptions, None] ) {%- endif %} {%- endif -%} {%- endmacro %} from typing import Final {{ import_instrument_classes(filtered_metrics) }} {%- for metric in ctx.metrics %} {% if metric.metric_name not in ctx.excluded_metrics %} {% set const_name = metric.metric_name | screaming_snake_case %} {{const_name}}: Final = "{{metric.metric_name}}" {%- set doc_string=write_docstring(metric, const_name, "")-%}{%- if doc_string %} """{{doc_string}} """{% endif %} {% if ctx.filter == "any" %} {% set metric_name = metric.metric_name | replace(".", "_") %} {%- if metric.instrument == "gauge" %} def create_{{ metric_name }}(meter: Meter, callbacks: Sequence[CallbackT] | None) -> {{metric.instrument | map_text("py_instrument_to_type")}}: {%- else %} def create_{{ metric_name }}(meter: Meter) -> {{metric.instrument | map_text("py_instrument_to_type")}}: {%- endif %} {%- if c.str_or_empty(metric.brief) |length %} """{{ c.comment_with_prefix(metric.brief, "") }}""" {% endif -%} return meter.create_{{ metric.instrument | map_text("py_instrument_to_factory")}}( name={{ const_name }}, {%- if metric.instrument == "gauge" %} callbacks=callbacks, {%- endif %} description="{{ c.str_or_empty(metric.brief|trim)|replace('\n', ' ')}}", unit="{{ metric.unit }}", ) {%- endif -%} {% endif %} {% endfor %} python-opentelemetry-1.45.2/scripts/semconv/templates/registry/weaver.yaml000066400000000000000000000037661527070070700272230ustar00rootroot00000000000000params: # excluded namespaces will not be generated excluded_namespaces: [ios, aspnetcore, signalr, android, dotnet, jvm, kestrel, v8js, veightjs, go, nodejs] # excluded attributes will be commented out in the generated code # this behavior is fully controlled by jinja templates excluded_attributes: ["messaging.client_id"] # excluded metrics will be excluded from the generated code in jinja templates excluded_metrics: [] stable_package_name: opentelemetry.semconv templates: - pattern: semantic_attributes.j2 filter: > semconv_grouped_attributes({ "exclude_root_namespace": $excluded_namespaces, "exclude_stability": if $filter == "any" then [] else ["experimental", "", null] end, }) | map({ root_namespace: .root_namespace, attributes: .attributes, output: $output + "attributes/", stable_package_name: $stable_package_name + ".attributes", filter: $filter, excluded_attributes: $excluded_attributes[] }) application_mode: each - pattern: semantic_metrics.j2 filter: > semconv_grouped_metrics({ "exclude_root_namespace": $excluded_namespaces, "exclude_stability": if $filter == "any" then [] else ["experimental", "", null] end, }) | map({ root_namespace: .root_namespace, metrics: [.metrics[] | select(.id == ("metric." + .metric_name))], output: $output + "metrics/", stable_package_name: $stable_package_name + ".metrics", filter: $filter, excluded_metrics: $excluded_metrics }) application_mode: each text_maps: py_instrument_to_factory: counter: counter histogram: histogram updowncounter: up_down_counter gauge: observable_gauge py_instrument_to_type: counter: Counter histogram: Histogram updowncounter: UpDownCounter gauge: ObservableGauge # remember the Values suffix! py_enum_attribute_to_class_name: cpython.gc.generation: CPythonGCGenerationValues python-opentelemetry-1.45.2/scripts/tests/000077500000000000000000000000001527070070700206545ustar00rootroot00000000000000python-opentelemetry-1.45.2/scripts/tests/test-requirements.txt000066400000000000000000000000331527070070700251110ustar00rootroot00000000000000pytest==9.0.3 toml==0.10.2 python-opentelemetry-1.45.2/scripts/tests/test_eachdist.py000066400000000000000000000111041527070070700240460ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import importlib.util from configparser import ConfigParser from pathlib import Path from textwrap import dedent from toml import load PACKAGE_ROOT_GLOBS = ( "opentelemetry-*/pyproject.toml", "exporter/opentelemetry-*/pyproject.toml", "propagator/opentelemetry-*/pyproject.toml", "shim/opentelemetry-*/pyproject.toml", "tests/opentelemetry-*/pyproject.toml", "codegen/opentelemetry-*/pyproject.toml", ) PRIVATE_PACKAGE_CLASSIFIER = "Private :: Do Not Upload" def load_eachdist(): script = Path(__file__).parents[1] / "eachdist.py" spec = importlib.util.spec_from_file_location("eachdist", script) module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) return module def write_pyproject(target, dependencies): target.mkdir() target.joinpath("pyproject.toml").write_text( dedent( f""" [project] dependencies = [ {dependencies} ] """ ), encoding="utf-8", ) def write_versioned_project(target, name, version): version_file = target / "src" / "opentelemetry" / "version" / "__init__.py" version_file.parent.mkdir(parents=True) version_file.write_text( f'__version__ = "{version}"\n', encoding="utf-8", ) target.mkdir(exist_ok=True) target.joinpath("pyproject.toml").write_text( dedent( f""" [project] name = "{name}" [tool.hatch.version] path = "src/opentelemetry/version/__init__.py" """ ), encoding="utf-8", ) def project_root(): return Path(__file__).parents[2] def package_name(pyproject): project = load(pyproject)["project"] if PRIVATE_PACKAGE_CLASSIFIER in project.get("classifiers", ()): return None return project["name"] def test_all_release_packages_are_listed_in_eachdist(): root = project_root() releasable_package_names = { name for package_glob in PACKAGE_ROOT_GLOBS for pyproject in root.glob(package_glob) if (name := package_name(pyproject)) is not None } config = ConfigParser() config.read(root / "eachdist.ini") eachdist_package_names = set(config["stable"]["packages"].split()) | set(config["prerelease"]["packages"].split()) missing_package_names = sorted(releasable_package_names - eachdist_package_names) assert not missing_package_names, f"packages missing from eachdist.ini: {', '.join(missing_package_names)}" def test_update_dependencies_matches_exact_package_name(tmp_path): eachdist = load_eachdist() target = tmp_path / "target" write_pyproject( target, '"opentelemetry-proto == 1.44.0.dev",\n "opentelemetry-proto-json == 0.65b0.dev",', ) eachdist.update_dependencies( [target], "1.44.0", ["opentelemetry-proto"], ) pyproject = target.joinpath("pyproject.toml").read_text(encoding="utf-8") assert '"opentelemetry-proto == 1.44.0",' in pyproject assert '"opentelemetry-proto-json == 0.65b0.dev",' in pyproject def test_update_patch_dependencies_matches_exact_package_name(tmp_path): eachdist = load_eachdist() target = tmp_path / "target" write_pyproject( target, '"opentelemetry-proto == 1.43.0",\n "opentelemetry-proto-json == 1.43.0",', ) eachdist.update_patch_dependencies( [target], "1.43.1", "1.43.0", ["opentelemetry-proto"], ) pyproject = target.joinpath("pyproject.toml").read_text(encoding="utf-8") assert '"opentelemetry-proto == 1.43.1",' in pyproject assert '"opentelemetry-proto-json == 1.43.0",' in pyproject def test_update_version_files_matches_exact_project_name(tmp_path): eachdist = load_eachdist() proto = tmp_path / "opentelemetry-proto" proto_json = tmp_path / "opentelemetry-proto-json" write_versioned_project( proto, "opentelemetry-proto", "1.44.0.dev", ) write_versioned_project( proto_json, "opentelemetry-proto-json", "0.65b0.dev", ) eachdist.update_version_files( [proto, proto_json], "1.45.0.dev", ["opentelemetry-proto"], ) assert ( proto.joinpath("src/opentelemetry/version/__init__.py").read_text(encoding="utf-8") == '__version__ = "1.45.0.dev"\n' ) assert ( proto_json.joinpath("src/opentelemetry/version/__init__.py").read_text(encoding="utf-8") == '__version__ = "0.65b0.dev"\n' ) python-opentelemetry-1.45.2/scripts/tracecontext-integration-test.sh000077500000000000000000000042111527070070700260500ustar00rootroot00000000000000#!/bin/sh set -e # hard-coding the git tag to ensure stable builds. TRACECONTEXT_GIT_TAG="d782773b2cf2fa4afd6a80a93b289d8a74ca894d" # clone w3c tracecontext tests mkdir -p target rm -rf ./target/trace-context git clone https://github.com/w3c/trace-context ./target/trace-context cd ./target/trace-context && git checkout $TRACECONTEXT_GIT_TAG && cd - # start example opentelemetry service, which propagates trace-context by # default. python ./tests/w3c_tracecontext_validation_server.py 1>&2 & EXAMPLE_SERVER_PID=$! onshutdown() { # send a sigint, to ensure # it is caught as a KeyboardInterrupt in the # example service. kill $EXAMPLE_SERVER_PID } trap onshutdown EXIT # Wait for the example server to accept connections on 127.0.0.1:5000 # before running the W3C tracecontext tests. A fixed `sleep` raced the # Flask startup on slow CI runners and produced intermittent connection # errors (see issue #5104). wait_for_server() { host=127.0.0.1 port=5000 deadline=$(( $(date +%s) + 30 )) while [ "$(date +%s)" -lt "$deadline" ]; do # Bail out early if the server process died. if ! kill -0 "$EXAMPLE_SERVER_PID" 2>/dev/null; then echo "tracecontext example server exited before becoming ready" >&2 return 1 fi # Use python so we don't depend on extra tools (nc, curl, etc.). if python -c "import socket,sys; s=socket.socket(); s.settimeout(1); sys.exit(0 if s.connect_ex(('$host', $port)) == 0 else 1)" 2>/dev/null; then return 0 fi sleep 0.5 done echo "tracecontext example server did not become ready within 30s" >&2 return 1 } wait_for_server cd ./target/trace-context/test # The disabled test is not compatible with an optional part of the W3C # spec that we have implemented (dropping duplicated keys from tracestate). # W3C are planning to include flags for optional features in the test suite. # https://github.com/w3c/trace-context/issues/529 # FIXME: update test to use flags for optional features when available. export SERVICE_ENDPOINT=http://127.0.0.1:5000/verify-tracecontext pytest test.py -k "not test_tracestate_duplicated_keys"python-opentelemetry-1.45.2/shim/000077500000000000000000000000001527070070700167635ustar00rootroot00000000000000python-opentelemetry-1.45.2/shim/opentelemetry-opencensus-shim/000077500000000000000000000000001527070070700247755ustar00rootroot00000000000000python-opentelemetry-1.45.2/shim/opentelemetry-opencensus-shim/LICENSE000066400000000000000000000261351527070070700260110ustar00rootroot00000000000000 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. python-opentelemetry-1.45.2/shim/opentelemetry-opencensus-shim/README.rst000066400000000000000000000010051527070070700264600ustar00rootroot00000000000000OpenCensus Shim for OpenTelemetry ================================== |pypi| .. |pypi| image:: https://badge.fury.io/py/opentelemetry-opencensus-shim.svg :target: https://pypi.org/project/opentelemetry-opencensus-shim/ Installation ------------ :: pip install opentelemetry-opencensus-shim References ---------- * `OpenCensus Shim for OpenTelemetry `_ * `OpenTelemetry Project `_ python-opentelemetry-1.45.2/shim/opentelemetry-opencensus-shim/pyproject.toml000066400000000000000000000026301527070070700277120ustar00rootroot00000000000000[build-system] requires = ["hatchling"] build-backend = "hatchling.build" [project] name = "opentelemetry-opencensus-shim" dynamic = ["version"] description = "OpenCensus Shim 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", "Framework :: OpenTelemetry", "Intended Audience :: Developers", "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.3", "wrapt ~= 1.0", # may work with older versions but this is the oldest confirmed version "opencensus >= 0.11.0", ] [project.optional-dependencies] [project.urls] Homepage = "https://github.com/open-telemetry/opentelemetry-python/tree/main/shim/opentelemetry-opencensus-shim" Repository = "https://github.com/open-telemetry/opentelemetry-python" [tool.hatch.version] path = "src/opentelemetry/shim/opencensus/version/__init__.py" [tool.hatch.build.targets.sdist] include = ["/src", "/tests"] [tool.hatch.build.targets.wheel] packages = ["src/opentelemetry"] python-opentelemetry-1.45.2/shim/opentelemetry-opencensus-shim/src/000077500000000000000000000000001527070070700255645ustar00rootroot00000000000000python-opentelemetry-1.45.2/shim/opentelemetry-opencensus-shim/src/opentelemetry/000077500000000000000000000000001527070070700304605ustar00rootroot00000000000000python-opentelemetry-1.45.2/shim/opentelemetry-opencensus-shim/src/opentelemetry/shim/000077500000000000000000000000001527070070700314205ustar00rootroot00000000000000python-opentelemetry-1.45.2/shim/opentelemetry-opencensus-shim/src/opentelemetry/shim/opencensus/000077500000000000000000000000001527070070700336025ustar00rootroot00000000000000__init__.py000066400000000000000000000020331527070070700356320ustar00rootroot00000000000000python-opentelemetry-1.45.2/shim/opentelemetry-opencensus-shim/src/opentelemetry/shim/opencensus# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 """ The OpenTelemetry OpenCensus shim is a library which allows an easy migration from OpenCensus to OpenTelemetry. Additional details can be found `in the specification `_. The shim consists of a set of classes which implement the OpenCensus Python API while using OpenTelemetry constructs behind the scenes. Its purpose is to allow applications which are already instrumented using OpenCensus to start using OpenTelemetry with minimal effort, without having to rewrite large portions of the codebase. """ from opentelemetry.shim.opencensus._patch import install_shim, uninstall_shim __all__ = [ "install_shim", "uninstall_shim", ] # TODO: Decide when this should be called. # 1. defensive import in opentelemetry-api # 2. defensive import directly in OpenCensus, although that would require a release # 3. ask the user to do it # install_shim() _patch.py000066400000000000000000000040241527070070700353330ustar00rootroot00000000000000python-opentelemetry-1.45.2/shim/opentelemetry-opencensus-shim/src/opentelemetry/shim/opencensus# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from functools import lru_cache from logging import getLogger from opencensus.trace.span_context import SpanContext from opencensus.trace.tracer import Tracer from opencensus.trace.tracers.noop_tracer import NoopTracer from opentelemetry import trace from opentelemetry.shim.opencensus._shim_tracer import ShimTracer from opentelemetry.shim.opencensus.version import __version__ _logger = getLogger(__name__) def install_shim( tracer_provider: trace.TracerProvider | None = None, ) -> None: otel_tracer = trace.get_tracer( "opentelemetry-opencensus-shim", __version__, tracer_provider=tracer_provider, ) @lru_cache def cached_shim_tracer(span_context: SpanContext) -> ShimTracer: return ShimTracer( NoopTracer(), oc_span_context=span_context, otel_tracer=otel_tracer, ) def fget_tracer(self: Tracer) -> ShimTracer: # self.span_context is how instrumentations pass propagated context into OpenCensus e.g. # https://github.com/census-instrumentation/opencensus-python/blob/fd064f438c5e490d25b004ee2545be55d2e28679/contrib/opencensus-ext-flask/opencensus/ext/flask/flask_middleware.py#L147-L153 return cached_shim_tracer(self.span_context) def fset_tracer(self, value) -> None: # ignore attempts to set the value pass # Tracer's constructor sets self.tracer to either a NoopTracer or ContextTracer depending # on sampler: # https://github.com/census-instrumentation/opencensus-python/blob/2e08df591b507612b3968be8c2538dedbf8fab37/opencensus/trace/tracer.py#L63. # We monkeypatch Tracer.tracer with a property to return a shim instance instead. This # makes all instances of Tracer (even those already created) use a ShimTracer. Tracer.tracer = property(fget_tracer, fset_tracer) _logger.info("Installed OpenCensus shim") def uninstall_shim() -> None: if hasattr(Tracer, "tracer"): del Tracer.tracer _shim_span.py000066400000000000000000000131711527070070700362200ustar00rootroot00000000000000python-opentelemetry-1.45.2/shim/opentelemetry-opencensus-shim/src/opentelemetry/shim/opencensus# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import logging from datetime import datetime from typing import TYPE_CHECKING import wrapt from opencensus.trace import execution_context from opencensus.trace.blank_span import BlankSpan from opencensus.trace.span import SpanKind from opencensus.trace.status import Status from opencensus.trace.time_event import MessageEvent from opentelemetry import context, trace if TYPE_CHECKING: from opentelemetry.shim.opencensus._shim_tracer import ShimTracer _logger = logging.getLogger(__name__) # Copied from Java # https://github.com/open-telemetry/opentelemetry-java/blob/0d3a04669e51b33ea47b29399a7af00012d25ccb/opencensus-shim/src/main/java/io/opentelemetry/opencensusshim/SpanConverter.java#L24-L27 _MESSAGE_EVENT_ATTRIBUTE_KEY_TYPE = "message.event.type" _MESSAGE_EVENT_ATTRIBUTE_KEY_SIZE_UNCOMPRESSED = "message.event.size.uncompressed" _MESSAGE_EVENT_ATTRIBUTE_KEY_SIZE_COMPRESSED = "message.event.size.compressed" _MESSAGE_EVENT_TYPE_STR_MAPPING = { 0: "TYPE_UNSPECIFIED", 1: "SENT", 2: "RECEIVED", } def _opencensus_time_to_nanos(timestamp: str) -> int: """Converts an OpenCensus formatted time string (ISO 8601 with Z) to time.time_ns style unix timestamp """ # format taken from # https://github.com/census-instrumentation/opencensus-python/blob/c38c71b9285e71de94d0185ff3c5bf65ee163345/opencensus/common/utils/__init__.py#L76 # # datetime.fromisoformat() does not work with the added "Z" until python 3.11 seconds_float = datetime.strptime(timestamp, "%Y-%m-%dT%H:%M:%S.%fZ").timestamp() return round(seconds_float * 1e9) # pylint: disable=abstract-method class ShimSpan(wrapt.ObjectProxy): def __init__( self, wrapped: BlankSpan, *, otel_span: trace.Span, shim_tracer: "ShimTracer", ) -> None: super().__init__(wrapped) self._self_otel_span = otel_span self._self_shim_tracer = shim_tracer self._self_token: object = None # Set a few values for BlankSpan members (they appear to be part of the "public" API # even though they are not documented in BaseSpan). Some instrumentations may use these # and not expect an AttributeError to be raised. Set values from OTel where possible # and let ObjectProxy defer to the wrapped BlankSpan otherwise. sc = self._self_otel_span.get_span_context() self.same_process_as_parent_span = not sc.is_remote self.span_id = sc.span_id def span(self, name="child_span"): return self._self_shim_tracer.start_span(name=name) def add_attribute(self, attribute_key, attribute_value): self._self_otel_span.set_attribute(attribute_key, attribute_value) def add_annotation(self, description, **attrs): self._self_otel_span.add_event(description, attrs) def add_message_event(self, message_event: MessageEvent): attrs = { _MESSAGE_EVENT_ATTRIBUTE_KEY_TYPE: _MESSAGE_EVENT_TYPE_STR_MAPPING[message_event.type], } if message_event.uncompressed_size_bytes is not None: attrs[_MESSAGE_EVENT_ATTRIBUTE_KEY_SIZE_UNCOMPRESSED] = message_event.uncompressed_size_bytes if message_event.compressed_size_bytes is not None: attrs[_MESSAGE_EVENT_ATTRIBUTE_KEY_SIZE_COMPRESSED] = message_event.compressed_size_bytes timestamp = _opencensus_time_to_nanos(message_event.timestamp) self._self_otel_span.add_event( str(message_event.id), attrs, timestamp=timestamp, ) # pylint: disable=no-self-use def add_link(self, link): """span links do not work with the shim because the OpenCensus Tracer does not accept links in start_span(). Same issue applies to SpanKind. Also see: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/compatibility/opencensus.md#known-incompatibilities """ _logger.warning("OpenTelemetry does not support links added after a span is created.") @property def span_kind(self): """Setting span_kind does not work with the shim because the OpenCensus Tracer does not accept the param in start_span() and there's no way to set OTel span kind after start_span(). """ return SpanKind.UNSPECIFIED @span_kind.setter def span_kind(self, value): _logger.warning("OpenTelemetry does not support setting span kind after a span is created.") def set_status(self, status: Status): self._self_otel_span.set_status( trace.StatusCode.OK if status.is_ok else trace.StatusCode.ERROR, status.description, ) def finish(self): """Note this method does not pop the span from current context. Use Tracer.end_span() or a `with span: ...` statement (contextmanager) to do that. """ self._self_otel_span.end() def __enter__(self): self._self_otel_span.__enter__() return self # pylint: disable=arguments-differ def __exit__(self, exception_type, exception_value, traceback): self._self_otel_span.__exit__(exception_type, exception_value, traceback) # OpenCensus Span.__exit__() calls Tracer.end_span() # https://github.com/census-instrumentation/opencensus-python/blob/2e08df591b507612b3968be8c2538dedbf8fab37/opencensus/trace/span.py#L390 # but that would cause the OTel span to be ended twice. Instead, this code just copies # the context teardown from that method. context.detach(self._self_token) execution_context.set_current_span(self._self_shim_tracer.current_span()) _shim_tracer.py000066400000000000000000000121031527070070700365310ustar00rootroot00000000000000python-opentelemetry-1.45.2/shim/opentelemetry-opencensus-shim/src/opentelemetry/shim/opencensus# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import logging import wrapt from opencensus.trace import execution_context from opencensus.trace.blank_span import BlankSpan from opencensus.trace.span_context import SpanContext from opencensus.trace.tracers.base import Tracer as BaseTracer from opencensus.trace.tracestate import Tracestate from opentelemetry import context, trace from opentelemetry.shim.opencensus._shim_span import ShimSpan _logger = logging.getLogger(__name__) _SHIM_SPAN_KEY = context.create_key("opencensus-shim-span-key") _SAMPLED = trace.TraceFlags(trace.TraceFlags.SAMPLED) def set_shim_span_in_context(span: ShimSpan, ctx: context.Context) -> context.Context: return context.set_value(_SHIM_SPAN_KEY, span, ctx) def get_shim_span_in_context() -> ShimSpan: return context.get_value(_SHIM_SPAN_KEY) def set_oc_span_in_context(oc_span_context: SpanContext, ctx: context.Context) -> context.Context: """Returns a new OTel context based on ctx with oc_span_context set as the current span""" # If no SpanContext is passed to the opencensus.trace.tracer.Tracer, it creates a new one # with a random trace ID and a None span ID to be the parent: # https://github.com/census-instrumentation/opencensus-python/blob/2e08df591b507612b3968be8c2538dedbf8fab37/opencensus/trace/tracer.py#L47. # # OpenTelemetry considers this an invalid SpanContext and will ignore it, so we can just # return early if oc_span_context.span_id is None: return ctx trace_id = int(oc_span_context.trace_id, 16) span_id = int(oc_span_context.span_id, 16) is_remote = oc_span_context.from_header trace_flags = _SAMPLED if oc_span_context.trace_options.get_enabled() else None trace_state = ( trace.TraceState(tuple(oc_span_context.tracestate.items())) # OC SpanContext does not validate this type if isinstance(oc_span_context.tracestate, Tracestate) else None ) return trace.set_span_in_context( trace.NonRecordingSpan( trace.SpanContext( trace_id=trace_id, span_id=span_id, is_remote=is_remote, trace_flags=trace_flags, trace_state=trace_state, ) ) ) # pylint: disable=abstract-method class ShimTracer(wrapt.ObjectProxy): def __init__( self, wrapped: BaseTracer, *, oc_span_context: SpanContext, otel_tracer: trace.Tracer, ) -> None: super().__init__(wrapped) self._self_oc_span_context = oc_span_context self._self_otel_tracer = otel_tracer # For now, finish() is not implemented by the shim. It would require keeping a list of all # spans created so they can all be finished. # def finish(self): # """End spans and send to reporter.""" def span(self, name="span"): return self.start_span(name=name) def start_span(self, name="span"): parent_ctx = context.get_current() # If there is no current span in context, use the one provided to the OC Tracer at # creation time if trace.get_current_span(parent_ctx) is trace.INVALID_SPAN: parent_ctx = set_oc_span_in_context(self._self_oc_span_context, parent_ctx) span = self._self_otel_tracer.start_span(name, context=parent_ctx) shim_span = ShimSpan( BlankSpan(name=name, context_tracer=self), otel_span=span, shim_tracer=self, ) ctx = trace.set_span_in_context(span) ctx = set_shim_span_in_context(shim_span, ctx) # OpenCensus's ContextTracer calls execution_context.set_current_span(span) which is # equivalent to the below. This can cause context to leak but is equivalent. # pylint: disable=protected-access shim_span._self_token = context.attach(ctx) # Also set it in OC's context, equivalent to # https://github.com/census-instrumentation/opencensus-python/blob/2e08df591b507612b3968be8c2538dedbf8fab37/opencensus/trace/tracers/context_tracer.py#L94 execution_context.set_current_span(shim_span) return shim_span def end_span(self): """Finishes the current span in the context and restores the context from before the span was started. """ span = self.current_span() if not span: _logger.warning("No active span, cannot do end_span.") return span.finish() # pylint: disable=protected-access context.detach(span._self_token) # Also reset the OC execution_context, equivalent to # https://github.com/census-instrumentation/opencensus-python/blob/2e08df591b507612b3968be8c2538dedbf8fab37/opencensus/trace/tracers/context_tracer.py#L114-L117 execution_context.set_current_span(self.current_span()) # pylint: disable=no-self-use def current_span(self): return get_shim_span_in_context() def add_attribute_to_current_span(self, attribute_key, attribute_value): self.current_span().add_attribute(attribute_key, attribute_value) py.typed000066400000000000000000000000001527070070700352100ustar00rootroot00000000000000python-opentelemetry-1.45.2/shim/opentelemetry-opencensus-shim/src/opentelemetry/shim/opencensusversion/000077500000000000000000000000001527070070700352105ustar00rootroot00000000000000python-opentelemetry-1.45.2/shim/opentelemetry-opencensus-shim/src/opentelemetry/shim/opencensus__init__.py000066400000000000000000000001441527070070700373200ustar00rootroot00000000000000python-opentelemetry-1.45.2/shim/opentelemetry-opencensus-shim/src/opentelemetry/shim/opencensus/version# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 __version__ = "0.66b2" python-opentelemetry-1.45.2/shim/opentelemetry-opencensus-shim/test-requirements.txt000066400000000000000000000012011527070070700312300ustar00rootroot00000000000000asgiref==3.7.2 cachetools==5.5.2 certifi==2024.12.14 charset-normalizer==3.3.2 google-api-core==2.17.1 google-auth==2.28.1 googleapis-common-protos==1.63.2 grpcio==1.75.1 idna==3.15 iniconfig==2.0.0 opencensus==0.11.1 opencensus-context==0.1.3 opencensus-proto==0.1.0 packaging==24.0 pluggy==1.6.0 protobuf==3.20.3 py-cpuinfo==9.0.0 pyasn1==0.6.4 pyasn1-modules==0.4.2 pytest==9.0.3 requests==2.33.0 rsa==4.9 six==1.16.0 tomli==2.0.1 typing_extensions==4.12.0 urllib3==2.7.0 wrapt==1.16.0 -e opentelemetry-api -e opentelemetry-sdk -e tests/opentelemetry-test-utils -e opentelemetry-semantic-conventions -e shim/opentelemetry-opencensus-shim python-opentelemetry-1.45.2/shim/opentelemetry-opencensus-shim/tests/000077500000000000000000000000001527070070700261375ustar00rootroot00000000000000python-opentelemetry-1.45.2/shim/opentelemetry-opencensus-shim/tests/__init__.py000066400000000000000000000000001527070070700302360ustar00rootroot00000000000000python-opentelemetry-1.45.2/shim/opentelemetry-opencensus-shim/tests/test_patch.py000066400000000000000000000046751527070070700306630ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import unittest from opencensus.trace.tracer import Tracer from opencensus.trace.tracers.noop_tracer import NoopTracer from opentelemetry.shim.opencensus import install_shim, uninstall_shim from opentelemetry.shim.opencensus._shim_tracer import ShimTracer class TestPatch(unittest.TestCase): def setUp(self): uninstall_shim() def tearDown(self): uninstall_shim() def test_install_shim(self): # Initially the shim is not installed. The Tracer class has no tracer property, it is # instance level only. self.assertFalse(hasattr(Tracer, "tracer")) install_shim() # The actual Tracer class should now be patched with a tracer property self.assertTrue(hasattr(Tracer, "tracer")) self.assertIsInstance(Tracer.tracer, property) def test_install_shim_affects_existing_tracers(self): # Initially the shim is not installed. A OC Tracer instance should have a NoopTracer oc_tracer = Tracer() self.assertIsInstance(oc_tracer.tracer, NoopTracer) self.assertNotIsInstance(oc_tracer.tracer, ShimTracer) install_shim() # The property should cause existing instances to get the singleton ShimTracer self.assertIsInstance(oc_tracer.tracer, ShimTracer) def test_install_shim_affects_new_tracers(self): install_shim() # The property should cause existing instances to get the singleton ShimTracer oc_tracer = Tracer() self.assertIsInstance(oc_tracer.tracer, ShimTracer) def test_uninstall_shim_resets_tracer(self): install_shim() uninstall_shim() # The actual Tracer class should not be patched self.assertFalse(hasattr(Tracer, "tracer")) def test_uninstall_shim_resets_existing_tracers(self): oc_tracer = Tracer() orig = oc_tracer.tracer install_shim() uninstall_shim() # Accessing the tracer member should no longer use the property, and instead should get # its original NoopTracer self.assertIs(oc_tracer.tracer, orig) def test_uninstall_shim_resets_new_tracers(self): install_shim() uninstall_shim() # Accessing the tracer member should get the NoopTracer oc_tracer = Tracer() self.assertIsInstance(oc_tracer.tracer, NoopTracer) self.assertNotIsInstance(oc_tracer.tracer, ShimTracer) python-opentelemetry-1.45.2/shim/opentelemetry-opencensus-shim/tests/test_shim.py000066400000000000000000000171161527070070700305160ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import logging import unittest from unittest.mock import patch from opencensus.trace import trace_options, tracestate from opencensus.trace.blank_span import BlankSpan as OcBlankSpan from opencensus.trace.link import Link as OcLink from opencensus.trace.span import SpanKind from opencensus.trace.span_context import SpanContext from opencensus.trace.tracer import Tracer as OcTracer from opencensus.trace.tracers.noop_tracer import NoopTracer as OcNoopTracer from opentelemetry import context, trace from opentelemetry.shim.opencensus import install_shim, uninstall_shim from opentelemetry.shim.opencensus._shim_span import ShimSpan from opentelemetry.shim.opencensus._shim_tracer import ( ShimTracer, set_oc_span_in_context, ) class TestShim(unittest.TestCase): def setUp(self): uninstall_shim() install_shim() def tearDown(self): uninstall_shim() def assert_hasattr(self, obj, key): self.assertTrue(hasattr(obj, key)) def test_shim_tracer_wraps_noop_tracer(self): oc_tracer = OcTracer() self.assertIsInstance(oc_tracer.tracer, ShimTracer) # wrapt.ObjectProxy does the magic here. The ShimTracer should look like the real OC # NoopTracer. self.assertIsInstance(oc_tracer.tracer, OcNoopTracer) self.assert_hasattr(oc_tracer.tracer, "finish") self.assert_hasattr(oc_tracer.tracer, "span") self.assert_hasattr(oc_tracer.tracer, "start_span") self.assert_hasattr(oc_tracer.tracer, "end_span") self.assert_hasattr(oc_tracer.tracer, "current_span") self.assert_hasattr(oc_tracer.tracer, "add_attribute_to_current_span") self.assert_hasattr(oc_tracer.tracer, "list_collected_spans") def test_shim_tracer_starts_shim_spans(self): oc_tracer = OcTracer() with oc_tracer.start_span("foo") as span: self.assertIsInstance(span, ShimSpan) def test_shim_span_wraps_blank_span(self): oc_tracer = OcTracer() with oc_tracer.start_span("foo") as span: # wrapt.ObjectProxy does the magic here. The ShimSpan should look like the real OC # BlankSpan. self.assertIsInstance(span, OcBlankSpan) # members self.assert_hasattr(span, "name") self.assert_hasattr(span, "parent_span") self.assert_hasattr(span, "start_time") self.assert_hasattr(span, "end_time") self.assert_hasattr(span, "span_id") self.assert_hasattr(span, "attributes") self.assert_hasattr(span, "stack_trace") self.assert_hasattr(span, "annotations") self.assert_hasattr(span, "message_events") self.assert_hasattr(span, "links") self.assert_hasattr(span, "status") self.assert_hasattr(span, "same_process_as_parent_span") self.assert_hasattr(span, "_child_spans") self.assert_hasattr(span, "context_tracer") self.assert_hasattr(span, "span_kind") # methods self.assert_hasattr(span, "on_create") self.assert_hasattr(span, "children") self.assert_hasattr(span, "span") self.assert_hasattr(span, "add_attribute") self.assert_hasattr(span, "add_annotation") self.assert_hasattr(span, "add_message_event") self.assert_hasattr(span, "add_link") self.assert_hasattr(span, "set_status") self.assert_hasattr(span, "start") self.assert_hasattr(span, "finish") self.assert_hasattr(span, "__iter__") self.assert_hasattr(span, "__enter__") self.assert_hasattr(span, "__exit__") def test_add_link_logs_a_warning(self): oc_tracer = OcTracer() with oc_tracer.start_span("foo") as span: with self.assertLogs(level=logging.WARNING): span.add_link(OcLink("1", "1")) def test_set_span_kind_logs_a_warning(self): oc_tracer = OcTracer() with oc_tracer.start_span("foo") as span: with self.assertLogs(level=logging.WARNING): span.span_kind = SpanKind.CLIENT # pylint: disable=no-self-use,no-member,protected-access,confusing-with-statement def test_shim_span_contextmanager_calls_does_not_call_end(self): # This was a bug in first implementation where the underlying OTel span.end() was # called after span.__exit__ which caused double-ending the span. oc_tracer = OcTracer() oc_span = oc_tracer.start_span("foo") with ( patch.object( oc_span, "_self_otel_span", wraps=oc_span._self_otel_span, ) as spy_otel_span, oc_span, ): pass spy_otel_span.end.assert_not_called() def test_set_oc_span_in_context_no_span_id(self): # This won't create a span ID and is the default behavior if you don't pass a context # when creating the Tracer ctx = set_oc_span_in_context(SpanContext(), context.get_current()) self.assertIs(trace.get_current_span(ctx), trace.INVALID_SPAN) def test_set_oc_span_in_context_ids(self): ctx = set_oc_span_in_context( SpanContext( trace_id="ace0216bab2b7ba249761dbb19c871b7", span_id="1fead89ecf242225", ), context.get_current(), ) span_ctx = trace.get_current_span(ctx).get_span_context() self.assertEqual( trace.format_trace_id(span_ctx.trace_id), "ace0216bab2b7ba249761dbb19c871b7", ) self.assertEqual(trace.format_span_id(span_ctx.span_id), "1fead89ecf242225") def test_set_oc_span_in_context_remote(self): for is_from_remote in True, False: ctx = set_oc_span_in_context( SpanContext( trace_id="ace0216bab2b7ba249761dbb19c871b7", span_id="1fead89ecf242225", from_header=is_from_remote, ), context.get_current(), ) span_ctx = trace.get_current_span(ctx).get_span_context() self.assertEqual(span_ctx.is_remote, is_from_remote) def test_set_oc_span_in_context_traceoptions(self): for oc_trace_options, expect in [ # Not sampled ( trace_options.TraceOptions("0"), trace.TraceFlags(trace.TraceFlags.DEFAULT), ), # Sampled ( trace_options.TraceOptions("1"), trace.TraceFlags(trace.TraceFlags.SAMPLED), ), ]: ctx = set_oc_span_in_context( SpanContext( trace_id="ace0216bab2b7ba249761dbb19c871b7", span_id="1fead89ecf242225", trace_options=oc_trace_options, ), context.get_current(), ) span_ctx = trace.get_current_span(ctx).get_span_context() self.assertEqual(span_ctx.trace_flags, expect) def test_set_oc_span_in_context_tracestate(self): ctx = set_oc_span_in_context( SpanContext( trace_id="ace0216bab2b7ba249761dbb19c871b7", span_id="1fead89ecf242225", tracestate=tracestate.Tracestate({"hello": "tracestate"}), ), context.get_current(), ) span_ctx = trace.get_current_span(ctx).get_span_context() self.assertEqual(span_ctx.trace_state, trace.TraceState([("hello", "tracestate")])) python-opentelemetry-1.45.2/shim/opentelemetry-opencensus-shim/tests/test_shim_with_sdk.py000066400000000000000000000263761527070070700324220ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import logging import unittest from datetime import datetime from opencensus.trace import execution_context, time_event from opencensus.trace.span_context import SpanContext from opencensus.trace.status import Status as OcStatus from opencensus.trace.tracer import Tracer as OcTracer from opentelemetry import trace 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 ALWAYS_ON from opentelemetry.shim.opencensus import install_shim, uninstall_shim _TIMESTAMP = datetime.fromisoformat("2023-01-01T00:00:00.000000") class TestShimWithSdk(unittest.TestCase): def setUp(self): uninstall_shim() self.tracer_provider = TracerProvider(sampler=ALWAYS_ON, shutdown_on_exit=False) self.mem_exporter = InMemorySpanExporter() self.tracer_provider.add_span_processor(SimpleSpanProcessor(self.mem_exporter)) install_shim(self.tracer_provider) def tearDown(self): uninstall_shim() def test_start_span_interacts_with_context(self): oc_tracer = OcTracer() span = oc_tracer.start_span("foo") # Should have created a real OTel span in implicit context under the hood. OpenCensus # does not require another step to set the span in context. otel_span = trace.get_current_span() self.assertNotEqual(span.span_id, 0) self.assertEqual(span.span_id, otel_span.get_span_context().span_id) # This should end the span and remove it from context oc_tracer.end_span() self.assertIs(trace.get_current_span(), trace.INVALID_SPAN) def test_start_span_interacts_with_oc_context(self): oc_tracer = OcTracer() span = oc_tracer.start_span("foo") # Should have put the shim span in OC's implicit context under the hood. OpenCensus # does not require another step to set the span in context. self.assertIs(execution_context.get_current_span(), span) # This should end the span and remove it from context oc_tracer.end_span() self.assertIs(execution_context.get_current_span(), None) def test_context_manager_interacts_with_context(self): oc_tracer = OcTracer() with oc_tracer.start_span("foo") as span: # Should have created a real OTel span in implicit context under the hood otel_span = trace.get_current_span() self.assertNotEqual(span.span_id, 0) self.assertEqual(span.span_id, otel_span.get_span_context().span_id) # The span should now be popped from context self.assertIs(trace.get_current_span(), trace.INVALID_SPAN) def test_context_manager_interacts_with_oc_context(self): oc_tracer = OcTracer() with oc_tracer.start_span("foo") as span: # Should have placed the shim span in implicit context under the hood self.assertIs(execution_context.get_current_span(), span) # The span should now be popped from context self.assertIs(execution_context.get_current_span(), None) def test_exports_a_span(self): oc_tracer = OcTracer() with oc_tracer.start_span("span1"): pass self.assertEqual(len(self.mem_exporter.get_finished_spans()), 1) def test_uses_tracers_span_context_when_no_parent_in_context(self): # the SpanContext passed to the Tracer will become the parent when there is no span # already set in the OTel context oc_tracer = OcTracer( span_context=SpanContext( trace_id="ace0216bab2b7ba249761dbb19c871b7", span_id="1fead89ecf242225", ) ) with oc_tracer.start_span("span1"): pass exported_span: ReadableSpan = self.mem_exporter.get_finished_spans()[0] parent = exported_span.parent self.assertIsNotNone(parent) self.assertEqual( trace.format_trace_id(parent.trace_id), "ace0216bab2b7ba249761dbb19c871b7", ) self.assertEqual(trace.format_span_id(parent.span_id), "1fead89ecf242225") def test_ignores_tracers_span_context_when_parent_already_in_context(self): # the SpanContext passed to the Tracer will be ignored since there is already a span # set in the OTel context oc_tracer = OcTracer( span_context=SpanContext( trace_id="ace0216bab2b7ba249761dbb19c871b7", span_id="1fead89ecf242225", ) ) otel_tracer = self.tracer_provider.get_tracer(__name__) with otel_tracer.start_as_current_span("some_parent"): with oc_tracer.start_span("span1"): pass oc_span: ReadableSpan = self.mem_exporter.get_finished_spans()[0] otel_parent: ReadableSpan = self.mem_exporter.get_finished_spans()[1] self.assertEqual( oc_span.parent, otel_parent.context, ) def test_span_attributes(self): oc_tracer = OcTracer() with oc_tracer.start_span("span1") as span: span.add_attribute("key1", "value1") span.add_attribute("key2", "value2") exported_span: ReadableSpan = self.mem_exporter.get_finished_spans()[0] self.assertDictEqual( dict(exported_span.attributes), {"key1": "value1", "key2": "value2"}, ) def test_span_annotations(self): oc_tracer = OcTracer() with oc_tracer.start_span("span1") as span: span.add_annotation("description", key1="value1", key2="value2") exported_span: ReadableSpan = self.mem_exporter.get_finished_spans()[0] self.assertEqual(len(exported_span.events), 1) event = exported_span.events[0] self.assertEqual(event.name, "description") self.assertDictEqual(dict(event.attributes), {"key1": "value1", "key2": "value2"}) def test_span_message_event(self): oc_tracer = OcTracer() with oc_tracer.start_span("span1") as span: span.add_message_event(time_event.MessageEvent(_TIMESTAMP, "id_sent", time_event.Type.SENT, "20", "10")) span.add_message_event( time_event.MessageEvent( _TIMESTAMP, "id_received", time_event.Type.RECEIVED, "20", "10", ) ) span.add_message_event( time_event.MessageEvent( _TIMESTAMP, "id_unspecified", None, "20", "10", ) ) exported_span: ReadableSpan = self.mem_exporter.get_finished_spans()[0] self.assertEqual(len(exported_span.events), 3) event1, event2, event3 = exported_span.events self.assertEqual(event1.name, "id_sent") self.assertDictEqual( dict(event1.attributes), { "message.event.size.compressed": "10", "message.event.size.uncompressed": "20", "message.event.type": "SENT", }, ) self.assertEqual(event2.name, "id_received") self.assertDictEqual( dict(event2.attributes), { "message.event.size.compressed": "10", "message.event.size.uncompressed": "20", "message.event.type": "RECEIVED", }, ) self.assertEqual(event3.name, "id_unspecified") self.assertDictEqual( dict(event3.attributes), { "message.event.size.compressed": "10", "message.event.size.uncompressed": "20", "message.event.type": "TYPE_UNSPECIFIED", }, ) def test_span_status(self): oc_tracer = OcTracer() with oc_tracer.start_span("span_ok") as span: # OTel will log about the message being set on a not OK span with self.assertLogs(level=logging.WARNING) as rec: span.set_status(OcStatus(0, "message")) self.assertIn( "description should only be set when status_code is set to StatusCode.ERROR", rec.output[0], ) with oc_tracer.start_span("span_exception") as span: span.set_status(OcStatus.from_exception(Exception("exception message"))) self.assertEqual(len(self.mem_exporter.get_finished_spans()), 2) ok_span: ReadableSpan = self.mem_exporter.get_finished_spans()[0] exc_span: ReadableSpan = self.mem_exporter.get_finished_spans()[1] self.assertTrue(ok_span.status.is_ok) # should be none even though we provided it because OTel drops the description when # status is not ERROR self.assertIsNone(ok_span.status.description) self.assertFalse(exc_span.status.is_ok) self.assertEqual(exc_span.status.description, "exception message") def assert_related(self, *, child: ReadableSpan, parent: ReadableSpan): self.assertEqual(child.parent.span_id, parent.get_span_context().span_id) def test_otel_sandwich(self): oc_tracer = OcTracer() otel_tracer = self.tracer_provider.get_tracer(__name__) with oc_tracer.start_span("opencensus_outer"): with otel_tracer.start_as_current_span("otel_middle"): with oc_tracer.start_span("opencensus_inner"): pass self.assertEqual(len(self.mem_exporter.get_finished_spans()), 3) opencensus_inner: ReadableSpan = self.mem_exporter.get_finished_spans()[0] otel_middle: ReadableSpan = self.mem_exporter.get_finished_spans()[1] opencensus_outer: ReadableSpan = self.mem_exporter.get_finished_spans()[2] self.assertEqual(opencensus_outer.name, "opencensus_outer") self.assertEqual(otel_middle.name, "otel_middle") self.assertEqual(opencensus_inner.name, "opencensus_inner") self.assertIsNone(opencensus_outer.parent) self.assert_related(parent=opencensus_outer, child=otel_middle) self.assert_related(parent=otel_middle, child=opencensus_inner) def test_opencensus_sandwich(self): oc_tracer = OcTracer() otel_tracer = self.tracer_provider.get_tracer(__name__) with otel_tracer.start_as_current_span("otel_outer"): with oc_tracer.start_span("opencensus_middle"): with otel_tracer.start_as_current_span("otel_inner"): pass self.assertEqual(len(self.mem_exporter.get_finished_spans()), 3) otel_inner: ReadableSpan = self.mem_exporter.get_finished_spans()[0] opencensus_middle: ReadableSpan = self.mem_exporter.get_finished_spans()[1] otel_outer: ReadableSpan = self.mem_exporter.get_finished_spans()[2] self.assertEqual(otel_outer.name, "otel_outer") self.assertEqual(opencensus_middle.name, "opencensus_middle") self.assertEqual(otel_inner.name, "otel_inner") self.assertIsNone(otel_outer.parent) self.assert_related(parent=otel_outer, child=opencensus_middle) self.assert_related(parent=opencensus_middle, child=otel_inner) python-opentelemetry-1.45.2/shim/opentelemetry-opentracing-shim/000077500000000000000000000000001527070070700251245ustar00rootroot00000000000000python-opentelemetry-1.45.2/shim/opentelemetry-opentracing-shim/LICENSE000066400000000000000000000261351527070070700261400ustar00rootroot00000000000000 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. python-opentelemetry-1.45.2/shim/opentelemetry-opentracing-shim/README.rst000066400000000000000000000010141527070070700266070ustar00rootroot00000000000000OpenTracing Shim for OpenTelemetry ================================== |pypi| .. |pypi| image:: https://badge.fury.io/py/opentelemetry-opentracing-shim.svg :target: https://pypi.org/project/opentelemetry-opentracing-shim/ Installation ------------ :: pip install opentelemetry-opentracing-shim References ---------- * `OpenTracing Shim for OpenTelemetry `_ * `OpenTelemetry Project `_ python-opentelemetry-1.45.2/shim/opentelemetry-opentracing-shim/pyproject.toml000066400000000000000000000025111527070070700300370ustar00rootroot00000000000000[build-system] requires = ["hatchling"] build-backend = "hatchling.build" [project] name = "opentelemetry-opentracing-shim" dynamic = ["version"] description = "OpenTracing Shim 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", "Framework :: OpenTelemetry", "Intended Audience :: Developers", "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 = [ "typing-extensions >= 4.5.0", "opentelemetry-api ~= 1.3", "opentracing ~= 2.0", ] [project.urls] Homepage = "https://github.com/open-telemetry/opentelemetry-python/tree/main/shim/opentelemetry-opentracing-shim" Repository = "https://github.com/open-telemetry/opentelemetry-python" [tool.hatch.version] path = "src/opentelemetry/shim/opentracing_shim/version/__init__.py" [tool.hatch.build.targets.sdist] include = [ "/src", "/tests", ] [tool.hatch.build.targets.wheel] packages = ["src/opentelemetry"] python-opentelemetry-1.45.2/shim/opentelemetry-opentracing-shim/src/000077500000000000000000000000001527070070700257135ustar00rootroot00000000000000python-opentelemetry-1.45.2/shim/opentelemetry-opentracing-shim/src/opentelemetry/000077500000000000000000000000001527070070700306075ustar00rootroot00000000000000python-opentelemetry-1.45.2/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/000077500000000000000000000000001527070070700315475ustar00rootroot00000000000000opentracing_shim/000077500000000000000000000000001527070070700350215ustar00rootroot00000000000000python-opentelemetry-1.45.2/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim__init__.py000066400000000000000000000675701527070070700371510ustar00rootroot00000000000000python-opentelemetry-1.45.2/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 """ The OpenTelemetry OpenTracing shim is a library which allows an easy migration from OpenTracing to OpenTelemetry. The shim consists of a set of classes which implement the OpenTracing Python API while using OpenTelemetry constructs behind the scenes. Its purpose is to allow applications which are already instrumented using OpenTracing to start using OpenTelemetry with a minimal effort, without having to rewrite large portions of the codebase. To use the shim, a :class:`TracerShim` instance is created and then used as if it were an "ordinary" OpenTracing :class:`opentracing.Tracer`, as in the following example:: import time from opentelemetry import trace from opentelemetry.sdk.trace import TracerProvider from opentelemetry.shim.opentracing_shim import create_tracer # Define which OpenTelemetry Tracer provider implementation to use. trace.set_tracer_provider(TracerProvider()) # Get the OpenTelemetry TracerProvider. tracer_provider = trace.get_tracer_provider() # Create an OpenTracing shim. shim = create_tracer(tracer_provider) with shim.start_active_span("ProcessHTTPRequest"): print("Processing HTTP request") # Sleeping to mock real work. time.sleep(0.1) with shim.start_active_span("GetDataFromDB"): print("Getting data from DB") # Sleeping to mock real work. time.sleep(0.2) Note: While the OpenTracing Python API represents time values as the number of **seconds** since the epoch expressed as :obj:`float` values, the OpenTelemetry Python API represents time values as the number of **nanoseconds** since the epoch expressed as :obj:`int` values. This fact requires the OpenTracing shim to convert time values back and forth between the two representations, which involves floating point arithmetic. Due to the way computers represent floating point values in hardware, representation of decimal floating point values in binary-based hardware is imprecise by definition. The above results in **slight imprecisions** in time values passed to the shim via the OpenTracing API when comparing the value passed to the shim and the value stored in the OpenTelemetry :class:`opentelemetry.trace.Span` object behind the scenes. **This is not a bug in this library or in Python**. Rather, this is a generic problem which stems from the fact that not every decimal floating point number can be correctly represented in binary, and therefore affects other libraries and programming languages as well. More information about this problem can be found in the `Floating Point Arithmetic\\: Issues and Limitations`_ section of the Python documentation. While testing this library, the aforementioned imprecisions were observed to be of *less than a microsecond*. API --- .. _Floating Point Arithmetic\\: Issues and Limitations: https://docs.python.org/3/tutorial/floatingpoint.html """ # TODO: make pylint use 3p opentracing module for type inference # pylint:disable=no-member from __future__ import annotations import logging from types import TracebackType from typing import TypeVar from opentracing import ( Format, Scope, ScopeManager, Span, SpanContext, Tracer, UnsupportedFormatException, ) from typing_extensions import deprecated from opentelemetry.baggage import get_baggage, set_baggage from opentelemetry.context import ( Context, attach, create_key, detach, get_value, set_value, ) from opentelemetry.propagate import get_global_textmap from opentelemetry.shim.opentracing_shim import util from opentelemetry.shim.opentracing_shim.version import __version__ from opentelemetry.trace import ( INVALID_SPAN_CONTEXT, Link, NonRecordingSpan, TracerProvider, get_current_span, set_span_in_context, use_span, ) from opentelemetry.trace import SpanContext as OtelSpanContext from opentelemetry.trace import Tracer as OtelTracer from opentelemetry.util.types import Attributes ValueT = TypeVar("ValueT", int, float, bool, str) logger = logging.getLogger(__name__) _SHIM_KEY = create_key("scope_shim") def create_tracer(otel_tracer_provider: TracerProvider) -> TracerShim: """Creates a :class:`TracerShim` object from the provided OpenTelemetry :class:`opentelemetry.trace.TracerProvider`. The returned :class:`TracerShim` is an implementation of :class:`opentracing.Tracer` using OpenTelemetry under the hood. Args: otel_tracer_provider: A tracer from this provider will be used to perform the actual tracing when user code is instrumented using the OpenTracing API. Returns: The created :class:`TracerShim`. """ return TracerShim(otel_tracer_provider.get_tracer(__name__, __version__)) class SpanContextShim(SpanContext): """Implements :class:`opentracing.SpanContext` by wrapping a :class:`opentelemetry.trace.SpanContext` object. Args: otel_context: A :class:`opentelemetry.trace.SpanContext` to be used for constructing the :class:`SpanContextShim`. """ def __init__(self, otel_context: OtelSpanContext): self._otel_context = otel_context # Context is being used here since it must be immutable. self._baggage = Context() def unwrap(self) -> OtelSpanContext: """Returns the wrapped :class:`opentelemetry.trace.SpanContext` object. Returns: The :class:`opentelemetry.trace.SpanContext` object wrapped by this :class:`SpanContextShim`. """ return self._otel_context @property def baggage(self) -> Context: """Returns the ``baggage`` associated with this object""" return self._baggage class SpanShim(Span): """Wraps a :class:`opentelemetry.trace.Span` object. Args: tracer: The :class:`opentracing.Tracer` that created this `SpanShim`. context: A :class:`SpanContextShim` which contains the context for this :class:`SpanShim`. span: A :class:`opentelemetry.trace.Span` to wrap. """ def __init__(self, tracer, context: SpanContextShim, span): super().__init__(tracer, context) self._otel_span = span def unwrap(self): """Returns the wrapped :class:`opentelemetry.trace.Span` object. Returns: The :class:`opentelemetry.trace.Span` object wrapped by this :class:`SpanShim`. """ return self._otel_span def set_operation_name(self, operation_name: str) -> SpanShim: """Updates the name of the wrapped OpenTelemetry span. Args: operation_name: The new name to be used for the underlying :class:`opentelemetry.trace.Span` object. Returns: Returns this :class:`SpanShim` instance to allow call chaining. """ self._otel_span.update_name(operation_name) return self def finish(self, finish_time: float | None = None): """Ends the OpenTelemetry span wrapped by this :class:`SpanShim`. If *finish_time* is provided, the time value is converted to the OpenTelemetry time format (number of nanoseconds since the epoch, expressed as an integer) and passed on to the OpenTelemetry tracer when ending the OpenTelemetry span. If *finish_time* isn't provided, it is up to the OpenTelemetry tracer implementation to generate a timestamp when ending the span. Args: finish_time: A value that represents the finish time expressed as the number of seconds since the epoch as returned by :func:`time.time()`. """ end_time = finish_time if end_time is not None: end_time = util.time_seconds_to_ns(finish_time) self._otel_span.end(end_time=end_time) def set_tag(self, key: str, value: ValueT) -> SpanShim: """Sets an OpenTelemetry attribute on the wrapped OpenTelemetry span. Args: key: A tag key. value: A tag value. Returns: Returns this :class:`SpanShim` instance to allow call chaining. """ self._otel_span.set_attribute(key, value) return self def log_kv(self, key_values: Attributes, timestamp: float | None = None) -> SpanShim: """Logs an event for the wrapped OpenTelemetry span. Note: The OpenTracing API defines the values of *key_values* to be of any type. However, the OpenTelemetry API requires that the values be any one of the types defined in ``opentelemetry.trace.util.Attributes`` therefore, only these types are supported as values. Args: key_values: A dictionary as specified in ``opentelemetry.trace.util.Attributes``. timestamp: Timestamp of the OpenTelemetry event, will be generated automatically if omitted. Returns: Returns this :class:`SpanShim` instance to allow call chaining. """ if timestamp is not None: event_timestamp = util.time_seconds_to_ns(timestamp) else: event_timestamp = None event_name = util.event_name_from_kv(key_values) self._otel_span.add_event(event_name, key_values, event_timestamp) return self @deprecated("This method is deprecated in favor of log_kv") def log(self, **kwargs): super().log(**kwargs) @deprecated("This method is deprecated in favor of log_kv") def log_event(self, event, payload=None): super().log_event(event, payload=payload) def set_baggage_item(self, key: str, value: str): """Stores a Baggage item in the span as a key/value pair. Args: key: A tag key. value: A tag value. """ # pylint: disable=protected-access self._context._baggage = set_baggage(key, value, context=self._context._baggage) def get_baggage_item(self, key: str) -> object | None: """Retrieves value of the baggage item with the given key. Args: key: A tag key. Returns: Returns this :class:`SpanShim` instance to allow call chaining. """ # pylint: disable=protected-access return get_baggage(key, context=self._context._baggage) class ScopeShim(Scope): """A `ScopeShim` wraps the OpenTelemetry functionality related to span activation/deactivation while using OpenTracing :class:`opentracing.Scope` objects for presentation. Unlike other classes in this package, the `ScopeShim` class doesn't wrap an OpenTelemetry class because OpenTelemetry doesn't have the notion of "scope" (though it *does* have similar functionality). There are two ways to construct a `ScopeShim` object: using the default initializer and using the :meth:`from_context_manager()` class method. It is necessary to have both ways for constructing `ScopeShim` objects because in some cases we need to create the object from an OpenTelemetry `opentelemetry.trace.Span` context manager (as returned by :meth:`opentelemetry.trace.use_span`), in which case our only way of retrieving a `opentelemetry.trace.Span` object is by calling the ``__enter__()`` method on the context manager, which makes the span active in the OpenTelemetry tracer; whereas in other cases we need to accept a `SpanShim` object and wrap it in a `ScopeShim`. The former is used mainly when the instrumentation code retrieves the currently-active span using `ScopeManagerShim.active`. The latter is mainly used when the instrumentation code activates a span using :meth:`ScopeManagerShim.activate`. Args: manager: The :class:`ScopeManagerShim` that created this :class:`ScopeShim`. span: The :class:`SpanShim` this :class:`ScopeShim` controls. span_cm: A Python context manager which yields an OpenTelemetry `opentelemetry.trace.Span` from its ``__enter__()`` method. Used by :meth:`from_context_manager` to store the context manager as an attribute so that it can later be closed by calling its ``__exit__()`` method. Defaults to `None`. """ def __init__(self, manager: ScopeManagerShim, span: SpanShim, span_cm=None): super().__init__(manager, span) self._span_cm = span_cm self._token = attach(set_value(_SHIM_KEY, self)) # TODO: Change type of `manager` argument to `opentracing.ScopeManager`? We # need to get rid of `manager.tracer` for this. @classmethod def from_context_manager(cls, manager: ScopeManagerShim, span_cm): """Constructs a :class:`ScopeShim` from an OpenTelemetry `opentelemetry.trace.Span` context manager. The method extracts a `opentelemetry.trace.Span` object from the context manager by calling the context manager's ``__enter__()`` method. This causes the span to start in the OpenTelemetry tracer. Example usage:: span = otel_tracer.start_span("TestSpan") span_cm = opentelemetry.trace.use_span(span) scope_shim = ScopeShim.from_context_manager( scope_manager_shim, span_cm=span_cm, ) Args: manager: The :class:`ScopeManagerShim` that created this :class:`ScopeShim`. span_cm: A context manager as returned by :meth:`opentelemetry.trace.use_span`. """ # pylint: disable=unnecessary-dunder-call otel_span = span_cm.__enter__() span_context = SpanContextShim(otel_span.get_span_context()) span = SpanShim(manager.tracer, span_context, otel_span) return cls(manager, span, span_cm) def close(self): """Closes the `ScopeShim`. If the `ScopeShim` was created from a context manager, calling this method sets the active span in the OpenTelemetry tracer back to the span which was active before this `ScopeShim` was created. In addition, if the span represented by this `ScopeShim` was activated with the *finish_on_close* argument set to `True`, calling this method will end the span. Warning: In the current state of the implementation it is possible to create a `ScopeShim` directly from a `SpanShim`, that is - without using :meth:`from_context_manager()`. For that reason we need to be able to end the span represented by the `ScopeShim` in this case, too. Please note that closing a `ScopeShim` created this way (for example as returned by :meth:`ScopeManagerShim.active`) **always ends the associated span**, regardless of the value passed in *finish_on_close* when activating the span. """ self._end_span_scope(None, None, None) def __exit__(self, exc_type, exc_val, exc_tb): """ Override the __exit__ method of `opentracing.scope.Scope` so we can report exceptions correctly in opentelemetry specification format. """ self._end_span_scope(exc_type, exc_val, exc_tb) def _end_span_scope( self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: TracebackType | None, ) -> None: detach(self._token) if self._span_cm is not None: self._span_cm.__exit__(exc_type, exc_val, exc_tb) else: self._span.unwrap().end() class ScopeManagerShim(ScopeManager): """Implements :class:`opentracing.ScopeManager` by setting and getting the active `opentelemetry.trace.Span` in the OpenTelemetry tracer. This class keeps a reference to a :class:`TracerShim` as an attribute. This reference is used to communicate with the OpenTelemetry tracer. It is necessary to have a reference to the :class:`TracerShim` rather than the :class:`opentelemetry.trace.Tracer` wrapped by it because when constructing a :class:`SpanShim` we need to pass a reference to a :class:`opentracing.Tracer`. Args: tracer: A :class:`TracerShim` to use for setting and getting active span state. """ def __init__(self, tracer: TracerShim): # The only thing the ``__init__()``` method on the base class does is # initialize `self._noop_span` and `self._noop_scope` with no-op # objects. Therefore, it doesn't seem useful to call it. # pylint: disable=super-init-not-called self._tracer = tracer def activate(self, span: SpanShim, finish_on_close: bool) -> ScopeShim: """Activates a :class:`SpanShim` and returns a :class:`ScopeShim` which represents the active span. Args: span: A :class:`SpanShim` to be activated. finish_on_close(:obj:`bool`): Determines whether the OpenTelemetry span should be ended when the returned :class:`ScopeShim` is closed. Returns: A :class:`ScopeShim` representing the activated span. """ span_cm = use_span(span.unwrap(), end_on_exit=finish_on_close) return ScopeShim.from_context_manager(self, span_cm=span_cm) @property def active(self) -> ScopeShim: """Returns a :class:`ScopeShim` object representing the currently-active span in the OpenTelemetry tracer. Returns: A :class:`ScopeShim` representing the active span in the OpenTelemetry tracer, or `None` if no span is currently active. Warning: Calling :meth:`ScopeShim.close` on the :class:`ScopeShim` returned by this property **always ends the corresponding span**, regardless of the *finish_on_close* value used when activating the span. This is a limitation of the current implementation of the OpenTracing shim and is likely to be handled in future versions. """ span = get_current_span() if span.get_span_context() == INVALID_SPAN_CONTEXT: return None try: return get_value(_SHIM_KEY) except KeyError: span_context = SpanContextShim(span.get_span_context()) wrapped_span = SpanShim(self._tracer, span_context, span) return ScopeShim(self, span=wrapped_span) @property def tracer(self) -> TracerShim: """Returns the :class:`TracerShim` reference used by this :class:`ScopeManagerShim` for setting and getting the active span from the OpenTelemetry tracer. Returns: The :class:`TracerShim` used for setting and getting the active span. Warning: This property is *not* a part of the OpenTracing API. It is used internally by the current implementation of the OpenTracing shim and will likely be removed in future versions. """ return self._tracer class TracerShim(Tracer): """Wraps a :class:`opentelemetry.trace.Tracer` object. This wrapper class allows using an OpenTelemetry tracer as if it were an OpenTracing tracer. It exposes the same methods as an "ordinary" OpenTracing tracer, and uses OpenTelemetry transparently for performing the actual tracing. This class depends on the *OpenTelemetry API*. Therefore, any implementation of a :class:`opentelemetry.trace.Tracer` should work with this class. Args: tracer: A :class:`opentelemetry.trace.Tracer` to use for tracing. This tracer will be invoked by the shim to create actual spans. """ def __init__(self, tracer: OtelTracer): super().__init__(scope_manager=ScopeManagerShim(self)) self._otel_tracer = tracer self._supported_formats = ( Format.TEXT_MAP, Format.HTTP_HEADERS, ) def unwrap(self): """Returns the :class:`opentelemetry.trace.Tracer` object that is wrapped by this :class:`TracerShim` and used for actual tracing. Returns: The :class:`opentelemetry.trace.Tracer` used for actual tracing. """ return self._otel_tracer def start_active_span( self, operation_name: str, child_of: SpanShim | SpanContextShim | None = None, references: list | None = None, tags: Attributes = None, start_time: float | None = None, ignore_active_span: bool = False, finish_on_close: bool = True, ) -> ScopeShim: """Starts and activates a span. In terms of functionality, this method behaves exactly like the same method on a "regular" OpenTracing tracer. See :meth:`opentracing.Tracer.start_active_span` for more details. Args: operation_name: Name of the operation represented by the new span from the perspective of the current service. child_of: A :class:`SpanShim` or :class:`SpanContextShim` representing the parent in a "child of" reference. If specified, the *references* parameter must be omitted. references: A list of :class:`opentracing.Reference` objects that identify one or more parents of type :class:`SpanContextShim`. tags: A dictionary of tags. start_time: An explicit start time expressed as the number of seconds since the epoch as returned by :func:`time.time()`. ignore_active_span: Ignore the currently-active span in the OpenTelemetry tracer and make the created span the root span of a new trace. finish_on_close: Determines whether the created span should end automatically when closing the returned :class:`ScopeShim`. Returns: A :class:`ScopeShim` that is already activated by the :class:`ScopeManagerShim`. """ current_span = get_current_span() if child_of is None and current_span.get_span_context() is not INVALID_SPAN_CONTEXT: child_of = SpanShim(None, None, current_span) span = self.start_span( operation_name=operation_name, child_of=child_of, references=references, tags=tags, start_time=start_time, ignore_active_span=ignore_active_span, ) return self._scope_manager.activate(span, finish_on_close) def start_span( self, operation_name: str | None = None, child_of: SpanShim | SpanContextShim | None = None, references: list | None = None, tags: Attributes = None, start_time: float | None = None, ignore_active_span: bool = False, ) -> SpanShim: """Implements the ``start_span()`` method from the base class. Starts a span. In terms of functionality, this method behaves exactly like the same method on a "regular" OpenTracing tracer. See :meth:`opentracing.Tracer.start_span` for more details. Args: operation_name: Name of the operation represented by the new span from the perspective of the current service. child_of: A :class:`SpanShim` or :class:`SpanContextShim` representing the parent in a "child of" reference. If specified, the *references* parameter must be omitted. references: A list of :class:`opentracing.Reference` objects that identify one or more parents of type :class:`SpanContextShim`. tags: A dictionary of tags. start_time: An explicit start time expressed as the number of seconds since the epoch as returned by :func:`time.time()`. ignore_active_span: Ignore the currently-active span in the OpenTelemetry tracer and make the created span the root span of a new trace. Returns: An already-started :class:`SpanShim` instance. """ # Use active span as parent when no explicit parent is specified. if not ignore_active_span and not child_of: child_of = self.active_span # Use the specified parent or the active span if possible. Otherwise, # use a `None` parent, which triggers the creation of a new trace. parent = child_of.unwrap() if child_of else None if isinstance(parent, OtelSpanContext): parent = NonRecordingSpan(parent) valid_links = [] if references: for ref in references: if ref.referenced_context.unwrap() is not INVALID_SPAN_CONTEXT: valid_links.append(Link(ref.referenced_context.unwrap())) if valid_links and parent is None: parent = NonRecordingSpan(valid_links[0].context) parent_span_context = set_span_in_context(parent) # The OpenTracing API expects time values to be `float` values which # represent the number of seconds since the epoch. OpenTelemetry # represents time values as nanoseconds since the epoch. start_time_ns = start_time if start_time_ns is not None: start_time_ns = util.time_seconds_to_ns(start_time) span = self._otel_tracer.start_span( operation_name, context=parent_span_context, links=valid_links, attributes=tags, start_time=start_time_ns, ) context = SpanContextShim(span.get_span_context()) return SpanShim(self, context, span) def inject(self, span_context, format: object, carrier: object): """Injects ``span_context`` into ``carrier``. See base class for more details. Args: span_context: The ``opentracing.SpanContext`` to inject. format: a Python object instance that represents a given carrier format. `format` may be of any type, and `format` equality is defined by Python ``==`` operator. carrier: the format-specific carrier object to inject into """ # pylint: disable=redefined-builtin # This implementation does not perform the injecting by itself but # uses the configured propagators in opentelemetry.propagators. # TODO: Support Format.BINARY once it is supported in # opentelemetry-python. if format not in self._supported_formats: raise UnsupportedFormatException propagator = get_global_textmap() span = span_context.unwrap() if span_context else None if isinstance(span, OtelSpanContext): span = NonRecordingSpan(span) ctx = set_span_in_context(span) propagator.inject(carrier, context=ctx) def extract(self, format: object, carrier: object): """Returns an ``opentracing.SpanContext`` instance extracted from a ``carrier``. See base class for more details. Args: format: a Python object instance that represents a given carrier format. ``format`` may be of any type, and ``format`` equality is defined by python ``==`` operator. carrier: the format-specific carrier object to extract from Returns: An ``opentracing.SpanContext`` extracted from ``carrier`` or ``None`` if no such ``SpanContext`` could be found. """ # pylint: disable=redefined-builtin # This implementation does not perform the extracting by itself but # uses the configured propagators in opentelemetry.propagators. # TODO: Support Format.BINARY once it is supported in # opentelemetry-python. if format not in self._supported_formats: raise UnsupportedFormatException propagator = get_global_textmap() ctx = propagator.extract(carrier) span = get_current_span(ctx) if span is not None: otel_context = span.get_span_context() else: otel_context = INVALID_SPAN_CONTEXT return SpanContextShim(otel_context) py.typed000066400000000000000000000000001527070070700365060ustar00rootroot00000000000000python-opentelemetry-1.45.2/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shimutil.py000066400000000000000000000023171527070070700363530ustar00rootroot00000000000000python-opentelemetry-1.45.2/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # A default event name to be used for logging events when a better event name # can't be derived from the event's key-value pairs. DEFAULT_EVENT_NAME = "log" def time_seconds_to_ns(time_seconds): """Converts a time value in seconds to a time value in nanoseconds. `time_seconds` is a `float` as returned by `time.time()` which represents the number of seconds since the epoch. The returned value is an `int` representing the number of nanoseconds since the epoch. """ return int(time_seconds * 1e9) def time_seconds_from_ns(time_nanoseconds): """Converts a time value in nanoseconds to a time value in seconds. `time_nanoseconds` is an `int` representing the number of nanoseconds since the epoch. The returned value is a `float` representing the number of seconds since the epoch. """ return time_nanoseconds / 1e9 def event_name_from_kv(key_values): """A helper function which returns an event name from the given dict, or a default event name. """ if key_values is None or "event" not in key_values: return DEFAULT_EVENT_NAME return key_values["event"] version/000077500000000000000000000000001527070070700365065ustar00rootroot00000000000000python-opentelemetry-1.45.2/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim__init__.py000066400000000000000000000001441527070070700406160ustar00rootroot00000000000000python-opentelemetry-1.45.2/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 __version__ = "0.66b2" python-opentelemetry-1.45.2/shim/opentelemetry-opentracing-shim/test-requirements.txt000066400000000000000000000004771527070070700313750ustar00rootroot00000000000000asgiref==3.7.2 iniconfig==2.0.0 opentracing==2.4.0 packaging==24.0 pluggy==1.6.0 py-cpuinfo==9.0.0 pytest==9.0.3 tomli==2.0.1 typing_extensions==4.10.0 wrapt==1.16.0 -e opentelemetry-api -e opentelemetry-sdk -e tests/opentelemetry-test-utils -e opentelemetry-semantic-conventions -e shim/opentelemetry-opentracing-shim python-opentelemetry-1.45.2/shim/opentelemetry-opentracing-shim/tests/000077500000000000000000000000001527070070700262665ustar00rootroot00000000000000python-opentelemetry-1.45.2/shim/opentelemetry-opentracing-shim/tests/__init__.py000066400000000000000000000000001527070070700303650ustar00rootroot00000000000000python-opentelemetry-1.45.2/shim/opentelemetry-opentracing-shim/tests/test_shim.py000066400000000000000000000550701527070070700306460ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # TODO: make pylint use 3p opentracing module for type inference # pylint:disable=no-member import time import traceback from unittest import TestCase from unittest.mock import Mock import opentracing from opentelemetry import trace from opentelemetry.propagate import get_global_textmap, set_global_textmap from opentelemetry.sdk.trace import TracerProvider from opentelemetry.shim.opentracing_shim import ( SpanContextShim, SpanShim, create_tracer, util, ) from opentelemetry.test.mock_textmap import ( MockTextMapPropagator, NOOPTextMapPropagator, ) class TestShim(TestCase): # pylint: disable=too-many-public-methods def setUp(self): """Create an OpenTelemetry tracer and a shim before every test case.""" trace.set_tracer_provider(TracerProvider()) self.shim = create_tracer(trace.get_tracer_provider()) @classmethod def setUpClass(cls): # Save current propagator to be restored on teardown. cls._previous_propagator = get_global_textmap() # Set mock propagator for testing. set_global_textmap(MockTextMapPropagator()) @classmethod def tearDownClass(cls): # Restore previous propagator. set_global_textmap(cls._previous_propagator) def test_shim_type(self): # Verify shim is an OpenTracing tracer. self.assertIsInstance(self.shim, opentracing.Tracer) def test_start_active_span(self): """Test span creation and activation using `start_active_span()`.""" with self.shim.start_active_span("TestSpan0") as scope: # Verify correct type of Scope and Span objects. self.assertIsInstance(scope, opentracing.Scope) self.assertIsInstance(scope.span, opentracing.Span) # Verify span is started. self.assertIsNotNone(scope.span.unwrap().start_time) # Verify span is active. self.assertEqual( self.shim.active_span.context.unwrap(), scope.span.context.unwrap(), ) # TODO: We can't check for equality of self.shim.active_span and # scope.span because the same OpenTelemetry span is returned inside # different SpanShim objects. A possible solution is described # here: # https://github.com/open-telemetry/opentelemetry-python/issues/161#issuecomment-534136274 # Verify span has ended. self.assertIsNotNone(scope.span.unwrap().end_time) # Verify no span is active. self.assertIsNone(self.shim.active_span) def test_start_span(self): """Test span creation using `start_span()`.""" with self.shim.start_span("TestSpan1") as span: # Verify correct type of Span object. self.assertIsInstance(span, opentracing.Span) # Verify span is started. self.assertIsNotNone(span.unwrap().start_time) # Verify `start_span()` does NOT make the span active. self.assertIsNone(self.shim.active_span) # Verify span has ended. self.assertIsNotNone(span.unwrap().end_time) def test_start_span_no_contextmanager(self): """Test `start_span()` without a `with` statement.""" span = self.shim.start_span("TestSpan2") # Verify span is started. self.assertIsNotNone(span.unwrap().start_time) # Verify `start_span()` does NOT make the span active. self.assertIsNone(self.shim.active_span) span.finish() def test_explicit_span_finish(self): """Test `finish()` method on `Span` objects.""" span = self.shim.start_span("TestSpan3") # Verify span hasn't ended. self.assertIsNone(span.unwrap().end_time) span.finish() # Verify span has ended. self.assertIsNotNone(span.unwrap().end_time) def test_explicit_start_time(self): """Test `start_time` argument.""" now = time.time() with self.shim.start_active_span("TestSpan4", start_time=now) as scope: result = util.time_seconds_from_ns(scope.span.unwrap().start_time) # Tolerate inaccuracies of less than a microsecond. See Note: # https://open-telemetry.github.io/opentelemetry-python/opentelemetry.shim.opentracing_shim.html # TODO: This seems to work consistently, but we should find out the # biggest possible loss of precision. self.assertAlmostEqual(result, now, places=6) def test_explicit_end_time(self): """Test `end_time` argument of `finish()` method.""" span = self.shim.start_span("TestSpan5") now = time.time() span.finish(now) end_time = util.time_seconds_from_ns(span.unwrap().end_time) # Tolerate inaccuracies of less than a microsecond. See Note: # https://open-telemetry.github.io/opentelemetry-python/opentelemetry.shim.opentracing_shim.html # TODO: This seems to work consistently, but we should find out the # biggest possible loss of precision. self.assertAlmostEqual(end_time, now, places=6) def test_explicit_span_activation(self): """Test manual activation and deactivation of a span.""" span = self.shim.start_span("TestSpan6") # Verify no span is currently active. self.assertIsNone(self.shim.active_span) with self.shim.scope_manager.activate(span, finish_on_close=True) as scope: # Verify span is active. self.assertEqual( self.shim.active_span.context.unwrap(), scope.span.context.unwrap(), ) # Verify no span is active. self.assertIsNone(self.shim.active_span) def test_start_active_span_finish_on_close(self): """Test `finish_on_close` argument of `start_active_span()`.""" with self.shim.start_active_span("TestSpan7", finish_on_close=True) as scope: # Verify span hasn't ended. self.assertIsNone(scope.span.unwrap().end_time) # Verify span has ended. self.assertIsNotNone(scope.span.unwrap().end_time) with self.shim.start_active_span("TestSpan8", finish_on_close=False) as scope: # Verify span hasn't ended. self.assertIsNone(scope.span.unwrap().end_time) # Verify span hasn't ended after scope had been closed. self.assertIsNone(scope.span.unwrap().end_time) scope.span.finish() def test_activate_finish_on_close(self): """Test `finish_on_close` argument of `activate()`.""" span = self.shim.start_span("TestSpan9") with self.shim.scope_manager.activate(span, finish_on_close=True) as scope: # Verify span is active. self.assertEqual( self.shim.active_span.context.unwrap(), scope.span.context.unwrap(), ) # Verify span has ended. self.assertIsNotNone(span.unwrap().end_time) span = self.shim.start_span("TestSpan10") with self.shim.scope_manager.activate(span, finish_on_close=False) as scope: # Verify span is active. self.assertEqual( self.shim.active_span.context.unwrap(), scope.span.context.unwrap(), ) # Verify span hasn't ended. self.assertIsNone(span.unwrap().end_time) span.finish() def test_explicit_scope_close(self): """Test `close()` method on `ScopeShim`.""" with self.shim.start_active_span("ParentSpan") as parent: # Verify parent span is active. self.assertEqual( self.shim.active_span.context.unwrap(), parent.span.context.unwrap(), ) child = self.shim.start_active_span("ChildSpan") # Verify child span is active. self.assertEqual( self.shim.active_span.context.unwrap(), child.span.context.unwrap(), ) # Verify child span hasn't ended. self.assertIsNone(child.span.unwrap().end_time) child.close() # Verify child span has ended. self.assertIsNotNone(child.span.unwrap().end_time) # Verify parent span becomes active again. self.assertEqual( self.shim.active_span.context.unwrap(), parent.span.context.unwrap(), ) def test_parent_child_implicit(self): """Test parent-child relationship and activation/deactivation of spans without specifying the parent span upon creation. """ with self.shim.start_active_span("ParentSpan") as parent: # Verify parent span is the active span. self.assertEqual( self.shim.active_span.context.unwrap(), parent.span.context.unwrap(), ) with self.shim.start_active_span("ChildSpan") as child: # Verify child span is the active span. self.assertEqual( self.shim.active_span.context.unwrap(), child.span.context.unwrap(), ) # Verify parent-child relationship. parent_trace_id = parent.span.unwrap().get_span_context().trace_id child_trace_id = child.span.unwrap().get_span_context().trace_id self.assertEqual(parent_trace_id, child_trace_id) self.assertEqual( child.span.unwrap().parent, parent.span.unwrap().get_span_context(), ) # Verify parent span becomes the active span again. self.assertEqual( self.shim.active_span.context.unwrap(), parent.span.context.unwrap(), # TODO: Check equality of the spans themselves rather than # their context once the SpanShim reconstruction problem has # been addressed (see previous TODO). ) # Verify there is no active span. self.assertIsNone(self.shim.active_span) def test_parent_child_explicit_span(self): """Test parent-child relationship of spans when specifying a `Span` object as a parent upon creation. """ with self.shim.start_span("ParentSpan") as parent: with self.shim.start_active_span("ChildSpan", child_of=parent) as child: parent_trace_id = parent.unwrap().get_span_context().trace_id child_trace_id = child.span.unwrap().get_span_context().trace_id self.assertEqual(child_trace_id, parent_trace_id) self.assertEqual( child.span.unwrap().parent, parent.unwrap().get_span_context(), ) with self.shim.start_span("ParentSpan") as parent: child = self.shim.start_span("ChildSpan", child_of=parent) parent_trace_id = parent.unwrap().get_span_context().trace_id child_trace_id = child.unwrap().get_span_context().trace_id self.assertEqual(child_trace_id, parent_trace_id) self.assertEqual(child.unwrap().parent, parent.unwrap().get_span_context()) child.finish() def test_parent_child_explicit_span_context(self): """Test parent-child relationship of spans when specifying a `SpanContext` object as a parent upon creation. """ with self.shim.start_span("ParentSpan") as parent: with self.shim.start_active_span("ChildSpan", child_of=parent.context) as child: parent_trace_id = parent.unwrap().get_span_context().trace_id child_trace_id = child.span.unwrap().get_span_context().trace_id self.assertEqual(child_trace_id, parent_trace_id) self.assertEqual(child.span.unwrap().parent, parent.context.unwrap()) with self.shim.start_span("ParentSpan") as parent: with self.shim.start_span("SpanWithContextParent", child_of=parent.context) as child: parent_trace_id = parent.unwrap().get_span_context().trace_id child_trace_id = child.unwrap().get_span_context().trace_id self.assertEqual(child_trace_id, parent_trace_id) self.assertEqual(child.unwrap().parent, parent.context.unwrap()) def test_references(self): """Test span creation using the `references` argument.""" with self.shim.start_span("ParentSpan") as parent: ref = opentracing.child_of(parent.context) with self.shim.start_active_span("ChildSpan", references=[ref]) as child: self.assertEqual( child.span.unwrap().links[0].context, parent.context.unwrap(), ) def test_follows_from_references(self): """Test span creation using the `references` argument with a follows from relationship.""" with self.shim.start_span("ParentSpan") as parent: ref = opentracing.follows_from(parent.context) with self.shim.start_active_span("FollowingSpan", references=[ref]) as child: self.assertEqual( child.span.unwrap().links[0].context, parent.context.unwrap(), ) self.assertEqual( child.span.unwrap().parent, parent.context.unwrap(), ) def test_set_operation_name(self): """Test `set_operation_name()` method.""" with self.shim.start_active_span("TestName") as scope: self.assertEqual(scope.span.unwrap().name, "TestName") scope.span.set_operation_name("NewName") self.assertEqual(scope.span.unwrap().name, "NewName") def test_tags(self): """Test tags behavior using the `tags` argument and the `set_tags()` method. """ tags = {"foo": "bar"} with self.shim.start_active_span("TestSetTag", tags=tags) as scope: scope.span.set_tag("baz", "qux") self.assertEqual(scope.span.unwrap().attributes["foo"], "bar") self.assertEqual(scope.span.unwrap().attributes["baz"], "qux") def test_span_tracer(self): """Test the `tracer` property on `Span` objects.""" with self.shim.start_active_span("TestSpan11") as scope: self.assertEqual(scope.span.tracer, self.shim) def test_log_kv(self): """Test the `log_kv()` method on `Span` objects.""" with self.shim.start_span("TestSpan12") as span: span.log_kv({"foo": "bar"}) self.assertEqual(span.unwrap().events[0].attributes["foo"], "bar") # Verify timestamp was generated automatically. self.assertIsNotNone(span.unwrap().events[0].timestamp) # Test explicit timestamp. now = time.time() span.log_kv({"foo": "bar"}, now) result = util.time_seconds_from_ns(span.unwrap().events[1].timestamp) self.assertEqual(span.unwrap().events[1].attributes["foo"], "bar") # Tolerate inaccuracies of less than a microsecond. See Note: # https://open-telemetry.github.io/opentelemetry-python/shim/opentracing_shim/opentracing_shim.html # TODO: This seems to work consistently, but we should find out the # biggest possible loss of precision. self.assertAlmostEqual(result, now, places=6) def test_log(self): """Test the deprecated `log` method on `Span` objects.""" with self.shim.start_span("TestSpan13") as span: with self.assertWarns(DeprecationWarning): span.log(event="foo", payload="bar") self.assertEqual(span.unwrap().events[0].attributes["event"], "foo") self.assertEqual(span.unwrap().events[0].attributes["payload"], "bar") self.assertIsNotNone(span.unwrap().events[0].timestamp) def test_log_event(self): """Test the deprecated `log_event` method on `Span` objects.""" with self.shim.start_span("TestSpan14") as span: with self.assertWarns(DeprecationWarning): span.log_event("foo", "bar") self.assertEqual(span.unwrap().events[0].attributes["event"], "foo") self.assertEqual(span.unwrap().events[0].attributes["payload"], "bar") self.assertIsNotNone(span.unwrap().events[0].timestamp) def test_span_context(self): """Test construction of `SpanContextShim` objects.""" otel_context = trace.SpanContext(1234, 5678, is_remote=False) context = SpanContextShim(otel_context) self.assertIsInstance(context, opentracing.SpanContext) self.assertEqual(context.unwrap().trace_id, 1234) self.assertEqual(context.unwrap().span_id, 5678) def test_span_on_error(self): """Verify error tag and logs are created on span when an exception is raised. """ # Raise an exception while a span is active. with self.assertRaises(Exception) as exc_ctx: with self.shim.start_active_span("TestName") as scope: # pylint: disable=broad-exception-raised raise Exception("bad thing") ex = exc_ctx.exception expected_stack = "".join(traceback.format_exception(type(ex), value=ex, tb=ex.__traceback__)) # Verify exception details have been added to span. exc_event = scope.span.unwrap().events[0] self.assertEqual(exc_event.name, "exception") self.assertEqual(exc_event.attributes["exception.message"], "bad thing") self.assertEqual(exc_event.attributes["exception.type"], Exception.__name__) # cannot get the whole stacktrace so just assert exception part is contained self.assertIn(expected_stack, exc_event.attributes["exception.stacktrace"]) def test_inject_http_headers(self): """Test `inject()` method for Format.HTTP_HEADERS.""" otel_context = trace.SpanContext(trace_id=1220, span_id=7478, is_remote=False) context = SpanContextShim(otel_context) headers = {} self.shim.inject(context, opentracing.Format.HTTP_HEADERS, headers) self.assertEqual(headers[MockTextMapPropagator.TRACE_ID_KEY], str(1220)) self.assertEqual(headers[MockTextMapPropagator.SPAN_ID_KEY], str(7478)) def test_inject_text_map(self): """Test `inject()` method for Format.TEXT_MAP.""" otel_context = trace.SpanContext(trace_id=1220, span_id=7478, is_remote=False) context = SpanContextShim(otel_context) # Verify Format.TEXT_MAP text_map = {} self.shim.inject(context, opentracing.Format.TEXT_MAP, text_map) self.assertEqual(text_map[MockTextMapPropagator.TRACE_ID_KEY], str(1220)) self.assertEqual(text_map[MockTextMapPropagator.SPAN_ID_KEY], str(7478)) def test_inject_binary(self): """Test `inject()` method for Format.BINARY.""" otel_context = trace.SpanContext(trace_id=1220, span_id=7478, is_remote=False) context = SpanContextShim(otel_context) # Verify exception for non supported binary format. with self.assertRaises(opentracing.UnsupportedFormatException): self.shim.inject(context, opentracing.Format.BINARY, bytearray()) def test_extract_http_headers(self): """Test `extract()` method for Format.HTTP_HEADERS.""" carrier = { MockTextMapPropagator.TRACE_ID_KEY: 1220, MockTextMapPropagator.SPAN_ID_KEY: 7478, } ctx = self.shim.extract(opentracing.Format.HTTP_HEADERS, carrier) self.assertEqual(ctx.unwrap().trace_id, 1220) self.assertEqual(ctx.unwrap().span_id, 7478) def test_extract_empty_context_returns_invalid_context(self): """In the case where the propagator cannot extract a SpanContext, extract should return and invalid span context. """ _old_propagator = get_global_textmap() set_global_textmap(NOOPTextMapPropagator()) try: carrier = {} ctx = self.shim.extract(opentracing.Format.HTTP_HEADERS, carrier) self.assertEqual(ctx.unwrap(), trace.INVALID_SPAN_CONTEXT) finally: set_global_textmap(_old_propagator) def test_extract_text_map(self): """Test `extract()` method for Format.TEXT_MAP.""" carrier = { MockTextMapPropagator.TRACE_ID_KEY: 1220, MockTextMapPropagator.SPAN_ID_KEY: 7478, } ctx = self.shim.extract(opentracing.Format.TEXT_MAP, carrier) self.assertEqual(ctx.unwrap().trace_id, 1220) self.assertEqual(ctx.unwrap().span_id, 7478) def test_extract_binary(self): """Test `extract()` method for Format.BINARY.""" # Verify exception for non supported binary format. with self.assertRaises(opentracing.UnsupportedFormatException): self.shim.extract(opentracing.Format.BINARY, bytearray()) def test_baggage(self): span_context_shim = SpanContextShim(trace.SpanContext(1234, 5678, is_remote=False)) baggage = span_context_shim.baggage with self.assertRaises(ValueError): baggage[1] = 3 span_shim = SpanShim(Mock(), span_context_shim, Mock()) span_shim.set_baggage_item(1, 2) self.assertTrue(span_shim.get_baggage_item(1), 2) def test_active(self): """Test that the active property and start_active_span return the same object""" # Verify no span is currently active. self.assertIsNone(self.shim.active_span) with self.shim.start_active_span("TestSpan15") as scope: # Verify span is active. self.assertEqual( self.shim.active_span.context.unwrap(), scope.span.context.unwrap(), ) self.assertIs(self.shim.scope_manager.active, scope) # Verify no span is active. self.assertIsNone(self.shim.active_span) def test_mixed_mode(self): """Test that span parent-child relationship is kept between OpenTelemetry and the OpenTracing shim""" span_shim = self.shim.start_span("TestSpan16") with self.shim.scope_manager.activate(span_shim, finish_on_close=True): with TracerProvider().get_tracer(__name__).start_as_current_span("abc") as opentelemetry_span: self.assertIs( span_shim.unwrap().context, opentelemetry_span.parent, ) with TracerProvider().get_tracer(__name__).start_as_current_span("abc") as opentelemetry_span: with self.shim.start_active_span("TestSpan17") as scope: self.assertIs( scope.span.unwrap().parent, opentelemetry_span.context, ) python-opentelemetry-1.45.2/shim/opentelemetry-opentracing-shim/tests/test_util.py000066400000000000000000000035641527070070700306640ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from time import time, time_ns from unittest import TestCase from opentelemetry.shim.opentracing_shim.util import ( DEFAULT_EVENT_NAME, event_name_from_kv, time_seconds_from_ns, time_seconds_to_ns, ) class TestUtil(TestCase): def test_event_name_from_kv(self): # Test basic behavior. event_name = "send HTTP request" res = event_name_from_kv({"event": event_name, "foo": "bar"}) self.assertEqual(res, event_name) # Test None. res = event_name_from_kv(None) self.assertEqual(res, DEFAULT_EVENT_NAME) # Test empty dict. res = event_name_from_kv({}) self.assertEqual(res, DEFAULT_EVENT_NAME) # Test missing `event` field. res = event_name_from_kv({"foo": "bar"}) self.assertEqual(res, DEFAULT_EVENT_NAME) def test_time_seconds_to_ns(self): time_seconds = time() result = time_seconds_to_ns(time_seconds) self.assertEqual(result, int(time_seconds * 1e9)) def test_time_seconds_from_ns(self): time_nanoseconds = time_ns() result = time_seconds_from_ns(time_nanoseconds) self.assertEqual(result, time_nanoseconds / 1e9) def test_time_conversion_precision(self): """Verify time conversion from seconds to nanoseconds and vice versa is accurate enough. """ time_seconds = 1570484241.9501917 time_nanoseconds = time_seconds_to_ns(time_seconds) result = time_seconds_from_ns(time_nanoseconds) # Tolerate inaccuracies of less than a microsecond. # TODO: Put a link to an explanation in the docs. # TODO: This seems to work consistently, but we should find out the # biggest possible loss of precision. self.assertAlmostEqual(result, time_seconds, places=6) python-opentelemetry-1.45.2/shim/opentelemetry-opentracing-shim/tests/testbed/000077500000000000000000000000001527070070700277205ustar00rootroot00000000000000python-opentelemetry-1.45.2/shim/opentelemetry-opentracing-shim/tests/testbed/README.rst000066400000000000000000000030531527070070700314100ustar00rootroot00000000000000 Testbed suite for the OpenTelemetry-OpenTracing Bridge ====================================================== Testbed suite designed to test the API changes. Build and test. --------------- .. code-block:: sh tox -e py311-test-opentracing-shim Alternatively, due to the organization of the suite, it's possible to run directly the tests using ``py.test``\ : .. code-block:: sh py.test -s testbed/test_multiple_callbacks/test_threads.py Tested frameworks ----------------- Currently the examples cover ``threading`` and ``asyncio``. List of patterns ---------------- * `Active Span replacement `_ - Start an isolated task and query for its results in another task/thread. * `Client-Server `_ - Typical client-server example. * `Common Request Handler `_ - One request handler for all requests. * `Late Span finish `_ - Late parent ``Span`` finish. * `Multiple callbacks `_ - Multiple callbacks spawned at the same time. * `Nested callbacks `_ - One callback at a time, defined in a pipeline fashion. * `Subtask Span propagation `_ - ``Span`` propagation for subtasks/coroutines. Adding new patterns ------------------- A new pattern is composed of a directory under *testbed* with the *test_* prefix, and containing the files for each platform, also with the *test_* prefix: .. code-block:: testbed/ test_new_pattern/ test_threads.py test_asyncio.py python-opentelemetry-1.45.2/shim/opentelemetry-opentracing-shim/tests/testbed/__init__.py000066400000000000000000000000001527070070700320170ustar00rootroot00000000000000python-opentelemetry-1.45.2/shim/opentelemetry-opentracing-shim/tests/testbed/otel_ot_shim_tracer.py000066400000000000000000000017731527070070700343270ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import opentelemetry.shim.opentracing_shim as opentracingshim from opentelemetry.sdk import trace from opentelemetry.sdk.trace.export import SimpleSpanProcessor from opentelemetry.sdk.trace.export.in_memory_span_exporter import ( InMemorySpanExporter, ) class MockTracer(opentracingshim.TracerShim): """Wrapper of `opentracingshim.TracerShim`. MockTracer extends `opentracingshim.TracerShim` by adding a in memory span exporter that can be used to get the list of finished spans.""" def __init__(self): tracer_provider = trace.TracerProvider() oteltracer = tracer_provider.get_tracer(__name__) super().__init__(oteltracer) exporter = InMemorySpanExporter() span_processor = SimpleSpanProcessor(exporter) tracer_provider.add_span_processor(span_processor) self.exporter = exporter def finished_spans(self): return self.exporter.get_finished_spans() test_active_span_replacement/000077500000000000000000000000001527070070700355535ustar00rootroot00000000000000python-opentelemetry-1.45.2/shim/opentelemetry-opentracing-shim/tests/testbedREADME.rst000066400000000000000000000012471527070070700372460ustar00rootroot00000000000000python-opentelemetry-1.45.2/shim/opentelemetry-opentracing-shim/tests/testbed/test_active_span_replacement Active Span replacement example. ================================ This example shows a ``Span`` being created and then passed to an asynchronous task, which will temporary activate it to finish its processing, and further restore the previously active ``Span``. ``threading`` implementation: .. code-block:: python # Create a new Span for this task with self.tracer.start_active_span("task"): with self.tracer.scope_manager.activate(span, True): # Simulate work strictly related to the initial Span pass # Use the task span as parent of a new subtask with self.tracer.start_active_span("subtask"): pass __init__.py000066400000000000000000000000001527070070700376520ustar00rootroot00000000000000python-opentelemetry-1.45.2/shim/opentelemetry-opentracing-shim/tests/testbed/test_active_span_replacementtest_asyncio.py000066400000000000000000000036201527070070700406320ustar00rootroot00000000000000python-opentelemetry-1.45.2/shim/opentelemetry-opentracing-shim/tests/testbed/test_active_span_replacement# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import asyncio # pylint: disable=import-error from ..otel_ot_shim_tracer import MockTracer from ..testcase import OpenTelemetryTestCase from ..utils import stop_loop_when class TestAsyncio(OpenTelemetryTestCase): def setUp(self): self.tracer = MockTracer() self.loop = asyncio.new_event_loop() asyncio.set_event_loop(self.loop) def tearDown(self): self.loop.close() super().tearDown() def test_main(self): # Start an isolated task and query for its result -and finish it- # in another task/thread span = self.tracer.start_span("initial") self.submit_another_task(span) stop_loop_when( self.loop, lambda: len(self.tracer.finished_spans()) >= 3, timeout=5.0, ) self.loop.run_forever() spans = self.tracer.finished_spans() self.assertEqual(len(spans), 3) self.assertNamesEqual(spans, ["initial", "subtask", "task"]) # task/subtask are part of the same trace, # and subtask is a child of task self.assertSameTrace(spans[1], spans[2]) self.assertIsChildOf(spans[1], spans[2]) # initial task is not related in any way to those two tasks self.assertNotSameTrace(spans[0], spans[1]) self.assertEqual(spans[0].parent, None) async def task(self, span): # Create a new Span for this task with self.tracer.start_active_span("task"): with self.tracer.scope_manager.activate(span, True): # Simulate work strictly related to the initial Span pass # Use the task span as parent of a new subtask with self.tracer.start_active_span("subtask"): pass def submit_another_task(self, span): self.loop.create_task(self.task(span)) test_threads.py000066400000000000000000000035431527070070700406230ustar00rootroot00000000000000python-opentelemetry-1.45.2/shim/opentelemetry-opentracing-shim/tests/testbed/test_active_span_replacement# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from concurrent.futures import ( # pylint: disable=no-name-in-module ThreadPoolExecutor, ) # pylint: disable=import-error from ..otel_ot_shim_tracer import MockTracer from ..testcase import OpenTelemetryTestCase class TestThreads(OpenTelemetryTestCase): def setUp(self): # pylint: disable=invalid-name self.tracer = MockTracer() # use max_workers=3 as a general example even if only one would suffice self.executor = ThreadPoolExecutor(max_workers=3) def test_main(self): # Start an isolated task and query for its result -and finish it- # in another task/thread span = self.tracer.start_span("initial") self.submit_another_task(span) self.executor.shutdown(True) spans = self.tracer.finished_spans() self.assertEqual(len(spans), 3) self.assertNamesEqual(spans, ["initial", "subtask", "task"]) # task/subtask are part of the same trace, # and subtask is a child of task self.assertSameTrace(spans[1], spans[2]) self.assertIsChildOf(spans[1], spans[2]) # initial task is not related in any way to those two tasks self.assertNotSameTrace(spans[0], spans[1]) self.assertEqual(spans[0].parent, None) self.assertEqual(spans[2].parent, None) def task(self, span): # Create a new Span for this task with self.tracer.start_active_span("task"): with self.tracer.scope_manager.activate(span, True): # Simulate work strictly related to the initial Span pass # Use the task span as parent of a new subtask with self.tracer.start_active_span("subtask"): pass def submit_another_task(self, span): self.executor.submit(self.task, span) python-opentelemetry-1.45.2/shim/opentelemetry-opentracing-shim/tests/testbed/test_client_server/000077500000000000000000000000001527070070700336235ustar00rootroot00000000000000README.rst000066400000000000000000000015061527070070700352350ustar00rootroot00000000000000python-opentelemetry-1.45.2/shim/opentelemetry-opentracing-shim/tests/testbed/test_client_server Client-Server example. ====================== This example shows a ``Span`` created by a ``Client``, which will send a ``Message`` / ``SpanContext`` to a ``Server``, which will in turn extract such context and use it as parent of a new (server-side) ``Span``. ``Client.send()`` is used to send messages and inject the ``SpanContext`` using the ``TEXT_MAP`` format, and ``Server.process()`` will process received messages and will extract the context used as parent. .. code-block:: python def send(self): with self.tracer.start_active_span("send") as scope: scope.span.set_tag(tags.SPAN_KIND, tags.SPAN_KIND_RPC_CLIENT) message = {} self.tracer.inject(scope.span.context, opentracing.Format.TEXT_MAP, message) self.queue.put(message) __init__.py000066400000000000000000000000001527070070700356430ustar00rootroot00000000000000python-opentelemetry-1.45.2/shim/opentelemetry-opentracing-shim/tests/testbed/test_client_servertest_asyncio.py000066400000000000000000000045611527070070700366300ustar00rootroot00000000000000python-opentelemetry-1.45.2/shim/opentelemetry-opentracing-shim/tests/testbed/test_client_server# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import asyncio import opentracing from opentracing.ext import tags # pylint: disable=import-error from ..otel_ot_shim_tracer import MockTracer from ..testcase import OpenTelemetryTestCase from ..utils import get_logger, get_one_by_tag, stop_loop_when logger = get_logger(__name__) class Server: def __init__(self, *args, **kwargs): tracer = kwargs.pop("tracer") queue = kwargs.pop("queue") super().__init__(*args, **kwargs) self.tracer = tracer self.queue = queue async def run(self): value = await self.queue.get() self.process(value) def process(self, message): logger.info("Processing message in server") ctx = self.tracer.extract(opentracing.Format.TEXT_MAP, message) with self.tracer.start_active_span("receive", child_of=ctx) as scope: scope.span.set_tag(tags.SPAN_KIND, tags.SPAN_KIND_RPC_SERVER) class Client: def __init__(self, tracer, queue): self.tracer = tracer self.queue = queue async def send(self): with self.tracer.start_active_span("send") as scope: scope.span.set_tag(tags.SPAN_KIND, tags.SPAN_KIND_RPC_CLIENT) message = {} self.tracer.inject(scope.span.context, opentracing.Format.TEXT_MAP, message) await self.queue.put(message) logger.info("Sent message from client") class TestAsyncio(OpenTelemetryTestCase): def setUp(self): self.tracer = MockTracer() self.loop = asyncio.new_event_loop() asyncio.set_event_loop(self.loop) self.queue = asyncio.Queue() self.server = Server(tracer=self.tracer, queue=self.queue) def tearDown(self): self.loop.close() super().tearDown() def test(self): client = Client(self.tracer, self.queue) self.loop.create_task(self.server.run()) self.loop.create_task(client.send()) stop_loop_when( self.loop, lambda: len(self.tracer.finished_spans()) >= 2, timeout=5.0, ) self.loop.run_forever() spans = self.tracer.finished_spans() self.assertIsNotNone(get_one_by_tag(spans, tags.SPAN_KIND, tags.SPAN_KIND_RPC_SERVER)) self.assertIsNotNone(get_one_by_tag(spans, tags.SPAN_KIND, tags.SPAN_KIND_RPC_CLIENT)) test_threads.py000066400000000000000000000041741527070070700366150ustar00rootroot00000000000000python-opentelemetry-1.45.2/shim/opentelemetry-opentracing-shim/tests/testbed/test_client_server# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from queue import Queue from threading import Thread import opentracing from opentracing.ext import tags # pylint: disable=import-error from ..otel_ot_shim_tracer import MockTracer from ..testcase import OpenTelemetryTestCase from ..utils import await_until, get_logger, get_one_by_tag logger = get_logger(__name__) class Server(Thread): def __init__(self, *args, **kwargs): tracer = kwargs.pop("tracer") queue = kwargs.pop("queue") super().__init__(*args, **kwargs) self.daemon = True self.tracer = tracer self.queue = queue def run(self): value = self.queue.get() self.process(value) def process(self, message): logger.info("Processing message in server") ctx = self.tracer.extract(opentracing.Format.TEXT_MAP, message) with self.tracer.start_active_span("receive", child_of=ctx) as scope: scope.span.set_tag(tags.SPAN_KIND, tags.SPAN_KIND_RPC_SERVER) class Client: def __init__(self, tracer, queue): self.tracer = tracer self.queue = queue def send(self): with self.tracer.start_active_span("send") as scope: scope.span.set_tag(tags.SPAN_KIND, tags.SPAN_KIND_RPC_CLIENT) message = {} self.tracer.inject(scope.span.context, opentracing.Format.TEXT_MAP, message) self.queue.put(message) logger.info("Sent message from client") class TestThreads(OpenTelemetryTestCase): def setUp(self): # pylint: disable=invalid-name self.tracer = MockTracer() self.queue = Queue() self.server = Server(tracer=self.tracer, queue=self.queue) self.server.start() def test(self): client = Client(self.tracer, self.queue) client.send() await_until(lambda: len(self.tracer.finished_spans()) >= 2) spans = self.tracer.finished_spans() self.assertIsNotNone(get_one_by_tag(spans, tags.SPAN_KIND, tags.SPAN_KIND_RPC_SERVER)) self.assertIsNotNone(get_one_by_tag(spans, tags.SPAN_KIND, tags.SPAN_KIND_RPC_CLIENT)) test_common_request_handler/000077500000000000000000000000001527070070700354355ustar00rootroot00000000000000python-opentelemetry-1.45.2/shim/opentelemetry-opentracing-shim/tests/testbedREADME.rst000066400000000000000000000016271527070070700371320ustar00rootroot00000000000000python-opentelemetry-1.45.2/shim/opentelemetry-opentracing-shim/tests/testbed/test_common_request_handler Common Request Handler example. =============================== This example shows a ``Span`` used with ``RequestHandler``, which is used as a middleware (as in web frameworks) to manage a new ``Span`` per operation through its ``before_request()`` / ``after_response()`` methods. Implementation details: * For ``threading``, no active ``Span`` is consumed as the tasks may be run concurrently on different threads, and an explicit ``SpanContext`` has to be saved to be used as parent. RequestHandler implementation: .. code-block:: python def before_request(self, request, request_context): # If we should ignore the active Span, use any passed SpanContext # as the parent. Else, use the active one. span = self.tracer.start_span("send", child_of=self.context, ignore_active_span=True) __init__.py000066400000000000000000000000001527070070700375340ustar00rootroot00000000000000python-opentelemetry-1.45.2/shim/opentelemetry-opentracing-shim/tests/testbed/test_common_request_handlerrequest_handler.py000066400000000000000000000022461527070070700412000ustar00rootroot00000000000000python-opentelemetry-1.45.2/shim/opentelemetry-opentracing-shim/tests/testbed/test_common_request_handler# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from opentracing.ext import tags # pylint: disable=import-error from ..utils import get_logger logger = get_logger(__name__) class RequestHandler: def __init__(self, tracer, context=None, ignore_active_span=True): self.tracer = tracer self.context = context self.ignore_active_span = ignore_active_span def before_request(self, request, request_context): logger.info("Before request %s", request) # If we should ignore the active Span, use any passed SpanContext # as the parent. Else, use the active one. if self.ignore_active_span: span = self.tracer.start_span("send", child_of=self.context, ignore_active_span=True) else: span = self.tracer.start_span("send") span.set_tag(tags.SPAN_KIND, tags.SPAN_KIND_RPC_CLIENT) request_context["span"] = span def after_request(self, request, request_context): # pylint: disable=no-self-use logger.info("After request %s", request) span = request_context.get("span") if span is not None: span.finish() test_asyncio.py000066400000000000000000000113241527070070700405140ustar00rootroot00000000000000python-opentelemetry-1.45.2/shim/opentelemetry-opentracing-shim/tests/testbed/test_common_request_handler# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import asyncio from opentracing.ext import tags # pylint: disable=import-error from ..otel_ot_shim_tracer import MockTracer from ..testcase import OpenTelemetryTestCase from ..utils import get_logger, get_one_by_operation_name, stop_loop_when from .request_handler import RequestHandler logger = get_logger(__name__) class Client: def __init__(self, request_handler, loop): self.request_handler = request_handler self.loop = loop async def send_task(self, message): request_context = {} async def before_handler(): self.request_handler.before_request(message, request_context) async def after_handler(): self.request_handler.after_request(message, request_context) await before_handler() await after_handler() return f"{message}::response" def send(self, message): return self.send_task(message) def send_sync(self, message): return self.loop.run_until_complete(self.send_task(message)) class TestAsyncio(OpenTelemetryTestCase): """ There is only one instance of 'RequestHandler' per 'Client'. Methods of 'RequestHandler' are executed in different Tasks, and no Span propagation among them is done automatically. Therefore we cannot use current active span and activate span. So one issue here is setting correct parent span. """ def setUp(self): self.tracer = MockTracer() self.loop = asyncio.new_event_loop() asyncio.set_event_loop(self.loop) self.client = Client(RequestHandler(self.tracer), self.loop) def tearDown(self): self.loop.close() super().tearDown() def test_two_callbacks(self): res_future1 = self.loop.create_task(self.client.send("message1")) res_future2 = self.loop.create_task(self.client.send("message2")) stop_loop_when( self.loop, lambda: len(self.tracer.finished_spans()) >= 2, timeout=5.0, ) self.loop.run_forever() self.assertEqual("message1::response", res_future1.result()) self.assertEqual("message2::response", res_future2.result()) spans = self.tracer.finished_spans() self.assertEqual(len(spans), 2) for span in spans: self.assertEqual( span.attributes.get(tags.SPAN_KIND, None), tags.SPAN_KIND_RPC_CLIENT, ) self.assertNotSameTrace(spans[0], spans[1]) self.assertIsNone(spans[0].parent) self.assertIsNone(spans[1].parent) def test_parent_not_picked(self): """Active parent should not be picked up by child.""" async def do_task(): with self.tracer.start_active_span("parent"): response = await self.client.send_task("no_parent") self.assertEqual("no_parent::response", response) self.loop.run_until_complete(do_task()) spans = self.tracer.finished_spans() self.assertEqual(len(spans), 2) child_span = get_one_by_operation_name(spans, "send") self.assertIsNotNone(child_span) parent_span = get_one_by_operation_name(spans, "parent") self.assertIsNotNone(parent_span) # Here check that there is no parent-child relation. self.assertIsNotChildOf(child_span, parent_span) def test_good_solution_to_set_parent(self): """Asyncio and contextvars are integrated, in this case it is not needed to activate current span by hand. """ async def do_task(): with self.tracer.start_active_span("parent"): # Set ignore_active_span to False indicating that the # framework will do it for us. req_handler = RequestHandler( self.tracer, ignore_active_span=False, ) client = Client(req_handler, self.loop) response = await client.send_task("correct_parent") self.assertEqual("correct_parent::response", response) # Send second request, now there is no active parent, # but it will be set, ups response = await client.send_task("wrong_parent") self.assertEqual("wrong_parent::response", response) self.loop.run_until_complete(do_task()) spans = self.tracer.finished_spans() self.assertEqual(len(spans), 3) parent_span = get_one_by_operation_name(spans, "parent") self.assertIsNotNone(parent_span) spans = [span for span in spans if span != parent_span] self.assertIsChildOf(spans[0], parent_span) self.assertIsNotChildOf(spans[1], parent_span) test_threads.py000066400000000000000000000104041527070070700404770ustar00rootroot00000000000000python-opentelemetry-1.45.2/shim/opentelemetry-opentracing-shim/tests/testbed/test_common_request_handler# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from concurrent.futures import ( # pylint: disable=no-name-in-module ThreadPoolExecutor, ) from opentracing.ext import tags # pylint: disable=import-error from ..otel_ot_shim_tracer import MockTracer from ..testcase import OpenTelemetryTestCase from ..utils import get_logger, get_one_by_operation_name from .request_handler import RequestHandler logger = get_logger(__name__) class Client: def __init__(self, request_handler, executor): self.request_handler = request_handler self.executor = executor def send_task(self, message): request_context = {} def before_handler(): self.request_handler.before_request(message, request_context) def after_handler(): self.request_handler.after_request(message, request_context) self.executor.submit(before_handler).result() self.executor.submit(after_handler).result() return f"{message}::response" def send(self, message): return self.executor.submit(self.send_task, message) def send_sync(self, message, timeout=5.0): fut = self.executor.submit(self.send_task, message) return fut.result(timeout=timeout) class TestThreads(OpenTelemetryTestCase): """ There is only one instance of 'RequestHandler' per 'Client'. Methods of 'RequestHandler' are executed concurrently in different threads which are reused (executor). Therefore we cannot use current active span and activate span. So one issue here is setting correct parent span. """ def setUp(self): # pylint: disable=invalid-name self.tracer = MockTracer() self.executor = ThreadPoolExecutor(max_workers=3) self.client = Client(RequestHandler(self.tracer), self.executor) def test_two_callbacks(self): response_future1 = self.client.send("message1") response_future2 = self.client.send("message2") self.assertEqual("message1::response", response_future1.result(5.0)) self.assertEqual("message2::response", response_future2.result(5.0)) spans = self.tracer.finished_spans() self.assertEqual(len(spans), 2) for span in spans: self.assertEqual( span.attributes.get(tags.SPAN_KIND, None), tags.SPAN_KIND_RPC_CLIENT, ) self.assertNotSameTrace(spans[0], spans[1]) self.assertIsNone(spans[0].parent) self.assertIsNone(spans[1].parent) def test_parent_not_picked(self): """Active parent should not be picked up by child.""" with self.tracer.start_active_span("parent"): response = self.client.send_sync("no_parent") self.assertEqual("no_parent::response", response) spans = self.tracer.finished_spans() self.assertEqual(len(spans), 2) child_span = get_one_by_operation_name(spans, "send") self.assertIsNotNone(child_span) parent_span = get_one_by_operation_name(spans, "parent") self.assertIsNotNone(parent_span) # Here check that there is no parent-child relation. self.assertIsNotChildOf(child_span, parent_span) def test_bad_solution_to_set_parent(self): """Solution is bad because parent is per client and is not automatically activated depending on the context. """ with self.tracer.start_active_span("parent") as scope: client = Client( # Pass a span context to be used ad the parent. RequestHandler(self.tracer, scope.span.context), self.executor, ) response = client.send_sync("correct_parent") self.assertEqual("correct_parent::response", response) response = client.send_sync("wrong_parent") self.assertEqual("wrong_parent::response", response) spans = self.tracer.finished_spans() self.assertEqual(len(spans), 3) spans = sorted(spans, key=lambda x: x.start_time) parent_span = get_one_by_operation_name(spans, "parent") self.assertIsNotNone(parent_span) spans = [s for s in spans if s != parent_span] self.assertEqual(len(spans), 2) for span in spans: self.assertIsChildOf(span, parent_span) python-opentelemetry-1.45.2/shim/opentelemetry-opentracing-shim/tests/testbed/test_late_span_finish/000077500000000000000000000000001527070070700342655ustar00rootroot00000000000000README.rst000066400000000000000000000013621527070070700356770ustar00rootroot00000000000000python-opentelemetry-1.45.2/shim/opentelemetry-opentracing-shim/tests/testbed/test_late_span_finish Late Span finish example. ========================= This example shows a ``Span`` for a top-level operation, with independent, unknown lifetime, acting as parent of a few asynchronous subtasks (which must re-activate it but not finish it). .. code-block:: python # Fire away a few subtasks, passing a parent Span whose lifetime # is not tied at all to the children. def submit_subtasks(self, parent_span): def task(name, interval): with self.tracer.scope_manager.activate(parent_span, False): with self.tracer.start_active_span(name): time.sleep(interval) self.executor.submit(task, "task1", 0.1) self.executor.submit(task, "task2", 0.3) __init__.py000066400000000000000000000000001527070070700363050ustar00rootroot00000000000000python-opentelemetry-1.45.2/shim/opentelemetry-opentracing-shim/tests/testbed/test_late_span_finishtest_asyncio.py000066400000000000000000000035161527070070700372710ustar00rootroot00000000000000python-opentelemetry-1.45.2/shim/opentelemetry-opentracing-shim/tests/testbed/test_late_span_finish# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import asyncio # pylint: disable=import-error from ..otel_ot_shim_tracer import MockTracer from ..testcase import OpenTelemetryTestCase from ..utils import get_logger, stop_loop_when logger = get_logger(__name__) class TestAsyncio(OpenTelemetryTestCase): def setUp(self): self.tracer = MockTracer() self.loop = asyncio.new_event_loop() asyncio.set_event_loop(self.loop) def tearDown(self): self.loop.close() super().tearDown() def test_main(self): # Create a Span and use it as (explicit) parent of a pair of subtasks. parent_span = self.tracer.start_span("parent") self.submit_subtasks(parent_span) stop_loop_when( self.loop, lambda: len(self.tracer.finished_spans()) >= 2, timeout=5.0, ) self.loop.run_forever() # Late-finish the parent Span now. parent_span.finish() spans = self.tracer.finished_spans() self.assertEqual(len(spans), 3) self.assertNamesEqual(spans, ["task1", "task2", "parent"]) for idx in range(2): self.assertSameTrace(spans[idx], spans[-1]) self.assertIsChildOf(spans[idx], spans[-1]) self.assertTrue(spans[idx].end_time <= spans[-1].end_time) # Fire away a few subtasks, passing a parent Span whose lifetime # is not tied at all to the children. def submit_subtasks(self, parent_span): async def task(name): logger.info("Running %s", name) with self.tracer.scope_manager.activate(parent_span, False): with self.tracer.start_active_span(name): await asyncio.sleep(0.1) self.loop.create_task(task("task1")) self.loop.create_task(task("task2")) test_threads.py000066400000000000000000000032231527070070700372510ustar00rootroot00000000000000python-opentelemetry-1.45.2/shim/opentelemetry-opentracing-shim/tests/testbed/test_late_span_finish# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import time from concurrent.futures import ( # pylint: disable=no-name-in-module ThreadPoolExecutor, ) # pylint: disable=import-error from ..otel_ot_shim_tracer import MockTracer from ..testcase import OpenTelemetryTestCase class TestThreads(OpenTelemetryTestCase): def setUp(self): # pylint: disable=invalid-name self.tracer = MockTracer() self.executor = ThreadPoolExecutor(max_workers=3) def test_main(self): # Create a Span and use it as (explicit) parent of a pair of subtasks. parent_span = self.tracer.start_span("parent") self.submit_subtasks(parent_span) # Wait for the threadpool to be done. self.executor.shutdown(True) # Late-finish the parent Span now. parent_span.finish() spans = self.tracer.finished_spans() self.assertEqual(len(spans), 3) self.assertNamesEqual(spans, ["task1", "task2", "parent"]) for idx in range(2): self.assertSameTrace(spans[idx], spans[-1]) self.assertIsChildOf(spans[idx], spans[-1]) self.assertTrue(spans[idx].end_time <= spans[-1].end_time) # Fire away a few subtasks, passing a parent Span whose lifetime # is not tied at all to the children. def submit_subtasks(self, parent_span): def task(name, interval): with self.tracer.scope_manager.activate(parent_span, False): with self.tracer.start_active_span(name): time.sleep(interval) self.executor.submit(task, "task1", 0.1) self.executor.submit(task, "task2", 0.3) test_listener_per_request/000077500000000000000000000000001527070070700351435ustar00rootroot00000000000000python-opentelemetry-1.45.2/shim/opentelemetry-opentracing-shim/tests/testbedREADME.rst000066400000000000000000000013221527070070700366300ustar00rootroot00000000000000python-opentelemetry-1.45.2/shim/opentelemetry-opentracing-shim/tests/testbed/test_listener_per_request Listener Response example. ========================== This example shows a ``Span`` created upon a message being sent to a ``Client``, and its handling along a related, **not shared** ``ResponseListener`` object with a ``on_response(self, response)`` method to finish it. .. code-block:: python def _task(self, message, listener): res = "%s::response" % message listener.on_response(res) return res def send_sync(self, message): span = self.tracer.start_span("send") span.set_tag(tags.SPAN_KIND, tags.SPAN_KIND_RPC_CLIENT) listener = ResponseListener(span) return self.executor.submit(self._task, message, listener).result() __init__.py000066400000000000000000000000001527070070700372420ustar00rootroot00000000000000python-opentelemetry-1.45.2/shim/opentelemetry-opentracing-shim/tests/testbed/test_listener_per_requestresponse_listener.py000066400000000000000000000003511527070070700412570ustar00rootroot00000000000000python-opentelemetry-1.45.2/shim/opentelemetry-opentracing-shim/tests/testbed/test_listener_per_request# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 class ResponseListener: def __init__(self, span): self.span = span def on_response(self, res): del res self.span.finish() test_asyncio.py000066400000000000000000000026451527070070700402300ustar00rootroot00000000000000python-opentelemetry-1.45.2/shim/opentelemetry-opentracing-shim/tests/testbed/test_listener_per_request# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import asyncio from opentracing.ext import tags # pylint: disable=import-error from ..otel_ot_shim_tracer import MockTracer from ..testcase import OpenTelemetryTestCase from ..utils import get_one_by_tag from .response_listener import ResponseListener async def task(message, listener): res = f"{message}::response" listener.on_response(res) return res class Client: def __init__(self, tracer, loop): self.tracer = tracer self.loop = loop def send_sync(self, message): span = self.tracer.start_span("send") span.set_tag(tags.SPAN_KIND, tags.SPAN_KIND_RPC_CLIENT) listener = ResponseListener(span) return self.loop.run_until_complete(task(message, listener)) class TestAsyncio(OpenTelemetryTestCase): def setUp(self): self.tracer = MockTracer() self.loop = asyncio.new_event_loop() asyncio.set_event_loop(self.loop) def tearDown(self): self.loop.close() super().tearDown() def test_main(self): client = Client(self.tracer, self.loop) res = client.send_sync("message") self.assertEqual(res, "message::response") spans = self.tracer.finished_spans() self.assertEqual(len(spans), 1) span = get_one_by_tag(spans, tags.SPAN_KIND, tags.SPAN_KIND_RPC_CLIENT) self.assertIsNotNone(span) test_threads.py000066400000000000000000000026761527070070700402210ustar00rootroot00000000000000python-opentelemetry-1.45.2/shim/opentelemetry-opentracing-shim/tests/testbed/test_listener_per_request# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from concurrent.futures import ( # pylint: disable=no-name-in-module ThreadPoolExecutor, ) from opentracing.ext import tags # pylint: disable=import-error from ..otel_ot_shim_tracer import MockTracer from ..testcase import OpenTelemetryTestCase from ..utils import get_one_by_tag from .response_listener import ResponseListener class Client: def __init__(self, tracer): self.tracer = tracer self.executor = ThreadPoolExecutor(max_workers=3) def _task(self, message, listener): # pylint: disable=no-self-use res = f"{message}::response" listener.on_response(res) return res def send_sync(self, message): span = self.tracer.start_span("send") span.set_tag(tags.SPAN_KIND, tags.SPAN_KIND_RPC_CLIENT) listener = ResponseListener(span) return self.executor.submit(self._task, message, listener).result() class TestThreads(OpenTelemetryTestCase): def setUp(self): # pylint: disable=invalid-name self.tracer = MockTracer() def test_main(self): client = Client(self.tracer) res = client.send_sync("message") self.assertEqual(res, "message::response") spans = self.tracer.finished_spans() self.assertEqual(len(spans), 1) span = get_one_by_tag(spans, tags.SPAN_KIND, tags.SPAN_KIND_RPC_CLIENT) self.assertIsNotNone(span) test_multiple_callbacks/000077500000000000000000000000001527070070700345325ustar00rootroot00000000000000python-opentelemetry-1.45.2/shim/opentelemetry-opentracing-shim/tests/testbedREADME.rst000066400000000000000000000032721527070070700362250ustar00rootroot00000000000000python-opentelemetry-1.45.2/shim/opentelemetry-opentracing-shim/tests/testbed/test_multiple_callbacks Multiple callbacks example. =========================== This example shows a ``Span`` created for a top-level operation, covering a set of asynchronous operations (representing callbacks), and have this ``Span`` finished when **all** of them have been executed. ``Client.send()`` is used to create a new asynchronous operation (callback), and in turn every operation both restores the active ``Span``, and creates a child ``Span`` (useful for measuring the performance of each callback). Implementation details: * For ``threading``, a thread-safe counter is put in each ``Span`` to keep track of the pending callbacks, and call ``Span.finish()`` when the count becomes 0. * For ``asyncio`` the children corotuines representing the subtasks are simply yielded over, so no counter is needed. ``threading`` implementation: .. code-block:: python def task(self, interval, parent_span): logger.info("Starting task") try: scope = self.tracer.scope_manager.activate(parent_span, False) with self.tracer.start_active_span("task"): time.sleep(interval) finally: scope.close() if parent_span._ref_count.decr() == 0: parent_span.finish() ``asyncio`` implementation: .. code-block:: python async def task(self, interval, parent_span): logger.info("Starting task") with self.tracer.start_active_span("task"): await asyncio.sleep(interval) # Invoke and yield over the coroutines. with self.tracer.start_active_span("parent"): tasks = self.submit_callbacks() await asyncio.gather(*tasks) __init__.py000066400000000000000000000000001527070070700366310ustar00rootroot00000000000000python-opentelemetry-1.45.2/shim/opentelemetry-opentracing-shim/tests/testbed/test_multiple_callbackstest_asyncio.py000066400000000000000000000037201527070070700376120ustar00rootroot00000000000000python-opentelemetry-1.45.2/shim/opentelemetry-opentracing-shim/tests/testbed/test_multiple_callbacks# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import asyncio import random # pylint: disable=import-error from ..otel_ot_shim_tracer import MockTracer from ..testcase import OpenTelemetryTestCase from ..utils import get_logger, stop_loop_when random.seed() logger = get_logger(__name__) class TestAsyncio(OpenTelemetryTestCase): def setUp(self): self.tracer = MockTracer() self.loop = asyncio.new_event_loop() asyncio.set_event_loop(self.loop) def tearDown(self): self.loop.close() super().tearDown() def test_main(self): # Need to run within a Task, as the scope manager depends # on Task.current_task() async def main_task(): with self.tracer.start_active_span("parent"): tasks = self.submit_callbacks() await asyncio.gather(*tasks) self.loop.create_task(main_task()) stop_loop_when( self.loop, lambda: len(self.tracer.finished_spans()) >= 4, timeout=5.0, ) self.loop.run_forever() spans = self.tracer.finished_spans() self.assertEqual(len(spans), 4) self.assertNamesEqual(spans, ["task", "task", "task", "parent"]) for idx in range(3): self.assertSameTrace(spans[idx], spans[-1]) self.assertIsChildOf(spans[idx], spans[-1]) async def task(self, interval, parent_span): logger.info("Starting task") with self.tracer.scope_manager.activate(parent_span, False): with self.tracer.start_active_span("task"): await asyncio.sleep(interval) def submit_callbacks(self): parent_span = self.tracer.scope_manager.active.span tasks = [] for _ in range(3): interval = 0.1 + random.randint(200, 500) * 0.001 task = self.loop.create_task(self.task(interval, parent_span)) tasks.append(task) return tasks test_threads.py000066400000000000000000000035601527070070700376010ustar00rootroot00000000000000python-opentelemetry-1.45.2/shim/opentelemetry-opentracing-shim/tests/testbed/test_multiple_callbacks# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import random import time from concurrent.futures import ( # pylint: disable=no-name-in-module ThreadPoolExecutor, ) # pylint: disable=import-error from ..otel_ot_shim_tracer import MockTracer from ..testcase import OpenTelemetryTestCase from ..utils import RefCount, get_logger random.seed() logger = get_logger(__name__) class TestThreads(OpenTelemetryTestCase): def setUp(self): # pylint: disable=invalid-name self.tracer = MockTracer() self.executor = ThreadPoolExecutor(max_workers=3) def test_main(self): try: scope = self.tracer.start_active_span("parent", finish_on_close=False) scope.span.ref_count = RefCount(1) self.submit_callbacks(scope.span) finally: scope.close() if scope.span.ref_count.decr() == 0: scope.span.finish() self.executor.shutdown(True) spans = self.tracer.finished_spans() self.assertEqual(len(spans), 4) self.assertNamesEqual(spans, ["task", "task", "task", "parent"]) for idx in range(3): self.assertSameTrace(spans[idx], spans[-1]) self.assertIsChildOf(spans[idx], spans[-1]) def task(self, interval, parent_span): logger.info("Starting task") scope = None try: scope = self.tracer.scope_manager.activate(parent_span, False) with self.tracer.start_active_span("task"): time.sleep(interval) finally: scope.close() if parent_span.ref_count.decr() == 0: parent_span.finish() def submit_callbacks(self, parent_span): for _ in range(3): parent_span.ref_count.incr() self.executor.submit(self.task, 0.1 + random.randint(200, 500) * 0.001, parent_span) python-opentelemetry-1.45.2/shim/opentelemetry-opentracing-shim/tests/testbed/test_nested_callbacks/000077500000000000000000000000001527070070700342405ustar00rootroot00000000000000README.rst000066400000000000000000000027311527070070700356530ustar00rootroot00000000000000python-opentelemetry-1.45.2/shim/opentelemetry-opentracing-shim/tests/testbed/test_nested_callbacks Nested callbacks example. ========================= This example shows a ``Span`` for a top-level operation, and how it can be passed down on a list of nested callbacks (always one at a time), have it as the active one for each of them, and finished **only** when the last one executes. For Python, we have decided to do it in a **fire-and-forget** fashion. Implementation details: * For ``threading``, the ``Span`` is manually activated in each coroutine/task. * For ``asyncio``, the active ``Span`` is not activated down the chain as the ``Context`` automatically propagates it. ``threading`` implementation: .. code-block:: python def submit(self): span = self.tracer.scope_manager.active.span def task1(): with self.tracer.scope_manager.activate(span, False): span.set_tag("key1", "1") def task2(): with self.tracer.scope_manager.activate(span, False): span.set_tag("key2", "2") ... ``asyncio`` implementation: .. code-block:: python async def task1(): span.set_tag("key1", "1") async def task2(): span.set_tag("key2", "2") async def task3(): span.set_tag("key3", "3") span.finish() self.loop.create_task(task3()) self.loop.create_task(task2()) self.loop.create_task(task1()) __init__.py000066400000000000000000000000001527070070700362600ustar00rootroot00000000000000python-opentelemetry-1.45.2/shim/opentelemetry-opentracing-shim/tests/testbed/test_nested_callbackstest_asyncio.py000066400000000000000000000032351527070070700372420ustar00rootroot00000000000000python-opentelemetry-1.45.2/shim/opentelemetry-opentracing-shim/tests/testbed/test_nested_callbacks# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import asyncio # pylint: disable=import-error from ..otel_ot_shim_tracer import MockTracer from ..testcase import OpenTelemetryTestCase from ..utils import stop_loop_when class TestAsyncio(OpenTelemetryTestCase): def setUp(self): self.tracer = MockTracer() self.loop = asyncio.new_event_loop() asyncio.set_event_loop(self.loop) def tearDown(self): self.loop.close() super().tearDown() def test_main(self): # Start a Span and let the callback-chain # finish it when the task is done async def task(): with self.tracer.start_active_span("one", finish_on_close=False): self.submit() self.loop.create_task(task()) stop_loop_when( self.loop, lambda: len(self.tracer.finished_spans()) == 1, timeout=5.0, ) self.loop.run_forever() spans = self.tracer.finished_spans() self.assertEqual(len(spans), 1) self.assertEqual(spans[0].name, "one") for idx in range(1, 4): self.assertEqual(spans[0].attributes.get(f"key{idx}", None), str(idx)) def submit(self): span = self.tracer.scope_manager.active.span async def task1(): span.set_tag("key1", "1") async def task2(): span.set_tag("key2", "2") async def task3(): span.set_tag("key3", "3") span.finish() self.loop.create_task(task3()) self.loop.create_task(task2()) self.loop.create_task(task1()) test_threads.py000066400000000000000000000037211527070070700372270ustar00rootroot00000000000000python-opentelemetry-1.45.2/shim/opentelemetry-opentracing-shim/tests/testbed/test_nested_callbacks# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from concurrent.futures import ( # pylint: disable=no-name-in-module ThreadPoolExecutor, ) # pylint: disable=import-error from ..otel_ot_shim_tracer import MockTracer from ..testcase import OpenTelemetryTestCase from ..utils import await_until class TestThreads(OpenTelemetryTestCase): def setUp(self): # pylint: disable=invalid-name self.tracer = MockTracer() self.executor = ThreadPoolExecutor(max_workers=3) def tearDown(self): # pylint: disable=invalid-name self.executor.shutdown(False) def test_main(self): # Start a Span and let the callback-chain # finish it when the task is done with self.tracer.start_active_span("one", finish_on_close=False): self.submit() # Cannot shutdown the executor and wait for the callbacks # to be run, as in such case only the first will be executed, # and the rest will get canceled. await_until(lambda: len(self.tracer.finished_spans()) == 1, 5) spans = self.tracer.finished_spans() self.assertEqual(len(spans), 1) self.assertEqual(spans[0].name, "one") for idx in range(1, 4): self.assertEqual(spans[0].attributes.get(f"key{idx}", None), str(idx)) def submit(self): span = self.tracer.scope_manager.active.span def task1(): with self.tracer.scope_manager.activate(span, False): span.set_tag("key1", "1") def task2(): with self.tracer.scope_manager.activate(span, False): span.set_tag("key2", "2") def task3(): with self.tracer.scope_manager.activate(span, True): span.set_tag("key3", "3") self.executor.submit(task3) self.executor.submit(task2) self.executor.submit(task1) test_subtask_span_propagation/000077500000000000000000000000001527070070700360005ustar00rootroot00000000000000python-opentelemetry-1.45.2/shim/opentelemetry-opentracing-shim/tests/testbedREADME.rst000066400000000000000000000032021527070070700374640ustar00rootroot00000000000000python-opentelemetry-1.45.2/shim/opentelemetry-opentracing-shim/tests/testbed/test_subtask_span_propagation Subtask Span propagation example. ================================= This example shows an active ``Span`` being simply propagated to the subtasks -either threads or coroutines-, and finished **by** the parent task. In real-life scenarios instrumentation libraries may help with ``Span`` propagation **if** not offered by default (see implementation details below), but we show here the case without such help. Implementation details: * For ``threading``, the ``Span`` is manually passed down the call chain, activating it in each corotuine/task. * For ``asyncio``, the active ``Span`` is not passed nor activated down the chain as the ``Context`` automatically propagates it. ``threading`` implementation: .. code-block:: python def parent_task(self, message): with self.tracer.start_active_span("parent") as scope: f = self.executor.submit(self.child_task, message, scope.span) res = f.result() return res def child_task(self, message, span): with self.tracer.scope_manager.activate(span, False): with self.tracer.start_active_span("child"): return "%s::response" % message ``asyncio`` implementation: .. code-block:: python async def parent_task(self, message): # noqa with self.tracer.start_active_span("parent"): res = await self.child_task(message) return res async def child_task(self, message): # No need to pass/activate the parent Span, as it stays in the context. with self.tracer.start_active_span("child"): return "%s::response" % message __init__.py000066400000000000000000000000001527070070700400770ustar00rootroot00000000000000python-opentelemetry-1.45.2/shim/opentelemetry-opentracing-shim/tests/testbed/test_subtask_span_propagationtest_asyncio.py000066400000000000000000000022641527070070700410620ustar00rootroot00000000000000python-opentelemetry-1.45.2/shim/opentelemetry-opentracing-shim/tests/testbed/test_subtask_span_propagation# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import asyncio # pylint: disable=import-error from ..otel_ot_shim_tracer import MockTracer from ..testcase import OpenTelemetryTestCase class TestAsyncio(OpenTelemetryTestCase): def setUp(self): self.tracer = MockTracer() self.loop = asyncio.new_event_loop() asyncio.set_event_loop(self.loop) def tearDown(self): self.loop.close() super().tearDown() def test_main(self): res = self.loop.run_until_complete(self.parent_task("message")) self.assertEqual(res, "message::response") spans = self.tracer.finished_spans() self.assertEqual(len(spans), 2) self.assertNamesEqual(spans, ["child", "parent"]) self.assertIsChildOf(spans[0], spans[1]) async def parent_task(self, message): with self.tracer.start_active_span("parent"): res = await self.child_task(message) return res async def child_task(self, message): # No need to pass/activate the parent Span, as it stays in the context. with self.tracer.start_active_span("child"): return f"{message}::response" test_threads.py000066400000000000000000000023571527070070700410520ustar00rootroot00000000000000python-opentelemetry-1.45.2/shim/opentelemetry-opentracing-shim/tests/testbed/test_subtask_span_propagation# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from concurrent.futures import ( # pylint: disable=no-name-in-module ThreadPoolExecutor, ) # pylint: disable=import-error from ..otel_ot_shim_tracer import MockTracer from ..testcase import OpenTelemetryTestCase class TestThreads(OpenTelemetryTestCase): def setUp(self): # pylint: disable=invalid-name self.tracer = MockTracer() self.executor = ThreadPoolExecutor(max_workers=3) def test_main(self): res = self.executor.submit(self.parent_task, "message").result() self.assertEqual(res, "message::response") spans = self.tracer.finished_spans() self.assertEqual(len(spans), 2) self.assertNamesEqual(spans, ["child", "parent"]) self.assertIsChildOf(spans[0], spans[1]) def parent_task(self, message): with self.tracer.start_active_span("parent") as scope: fut = self.executor.submit(self.child_task, message, scope.span) res = fut.result() return res def child_task(self, message, span): with self.tracer.scope_manager.activate(span, False): with self.tracer.start_active_span("child"): return f"{message}::response" python-opentelemetry-1.45.2/shim/opentelemetry-opentracing-shim/tests/testbed/testcase.py000066400000000000000000000026301527070070700321060ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import unittest import opentelemetry.trace as trace_api # pylint: disable=C0103 class OpenTelemetryTestCase(unittest.TestCase): def assertSameTrace(self, spanA, spanB): return self.assertEqual(spanA.context.trace_id, spanB.context.trace_id) def assertNotSameTrace(self, spanA, spanB): return self.assertNotEqual(spanA.context.trace_id, spanB.context.trace_id) def assertIsChildOf(self, spanA, spanB): # spanA is child of spanB self.assertIsNotNone(spanA.parent) ctxA = spanA.parent if not isinstance(ctxA, trace_api.SpanContext): ctxA = spanA.parent.context ctxB = spanB if not isinstance(ctxB, trace_api.SpanContext): ctxB = spanB.context return self.assertEqual(ctxA.span_id, ctxB.span_id) def assertIsNotChildOf(self, spanA, spanB): # spanA is NOT child of spanB if spanA.parent is None: return ctxA = spanA.parent if not isinstance(ctxA, trace_api.SpanContext): ctxA = spanA.parent.context ctxB = spanB if not isinstance(ctxB, trace_api.SpanContext): ctxB = spanB.context self.assertNotEqual(ctxA.span_id, ctxB.span_id) def assertNamesEqual(self, spans, names): self.assertEqual(list(map(lambda x: x.name, spans)), names) python-opentelemetry-1.45.2/shim/opentelemetry-opentracing-shim/tests/testbed/utils.py000066400000000000000000000036001527070070700314310ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import logging import threading import time class RefCount: """Thread-safe counter""" def __init__(self, count=1): self._lock = threading.Lock() self._count = count def incr(self): with self._lock: self._count += 1 return self._count def decr(self): with self._lock: self._count -= 1 return self._count def await_until(func, timeout=5.0): """Polls for func() to return True""" end_time = time.time() + timeout while time.time() < end_time and not func(): time.sleep(0.01) def stop_loop_when(loop, cond_func, timeout=5.0): """ Registers a periodic callback that stops the loop when cond_func() == True. Compatible with both Tornado and asyncio. """ if cond_func() or timeout <= 0.0: loop.stop() return timeout -= 0.1 loop.call_later(0.1, stop_loop_when, loop, cond_func, timeout) def get_logger(name): """Returns a logger with log level set to INFO""" logging.basicConfig(level=logging.INFO) return logging.getLogger(name) def get_one_by_tag(spans, key, value): """Return a single Span with a tag value/key from a list, errors if more than one is found.""" found = [] for span in spans: if span.attributes.get(key) == value: found.append(span) if len(found) > 1: raise RuntimeError("Too many values") return found[0] if len(found) > 0 else None def get_one_by_operation_name(spans, name): """Return a single Span with a name from a list, errors if more than one is found.""" found = [] for span in spans: if span.name == name: found.append(span) if len(found) > 1: raise RuntimeError("Too many values") return found[0] if len(found) > 0 else None python-opentelemetry-1.45.2/tests/000077500000000000000000000000001527070070700171655ustar00rootroot00000000000000python-opentelemetry-1.45.2/tests/opentelemetry-docker-tests/000077500000000000000000000000001527070070700244665ustar00rootroot00000000000000python-opentelemetry-1.45.2/tests/opentelemetry-docker-tests/tests/000077500000000000000000000000001527070070700256305ustar00rootroot00000000000000python-opentelemetry-1.45.2/tests/opentelemetry-docker-tests/tests/.gitignore000066400000000000000000000003131527070070700276150ustar00rootroot00000000000000# Directory the OTLP JSON File exporter writes to during docker tests, created at # runtime and tailed by the collector's otlp_json_file receiver (see # docker-compose.yml). otlpexporter/otlp-file-data/ python-opentelemetry-1.45.2/tests/opentelemetry-docker-tests/tests/collector-config.yaml000066400000000000000000000021361527070070700317470ustar00rootroot00000000000000receivers: otlp: protocols: grpc: endpoint: 0.0.0.0:4317 http: endpoint: 0.0.0.0:4318 otlp_json_file/traces: include: [/otlp-file-data/traces/*.jsonl] start_at: beginning include_file_name: false otlp_json_file/metrics: include: [/otlp-file-data/metrics/*.jsonl] start_at: beginning include_file_name: false otlp_json_file/logs: include: [/otlp-file-data/logs/*.jsonl] start_at: beginning include_file_name: false prometheus: config: scrape_configs: - job_name: otel-python-e2e scrape_interval: 1s static_configs: - targets: ['host.docker.internal:9464'] exporters: otlphttp: endpoint: http://host.docker.internal:4319 service: pipelines: traces: receivers: [otlp, otlp_json_file/traces] exporters: [otlphttp] metrics: receivers: [otlp, otlp_json_file/metrics] exporters: [otlphttp] metrics/prometheus: receivers: [prometheus] exporters: [otlphttp] logs: receivers: [otlp, otlp_json_file/logs] exporters: [otlphttp] python-opentelemetry-1.45.2/tests/opentelemetry-docker-tests/tests/conftest.py000066400000000000000000000014341527070070700300310ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations from collections.abc import Iterator import pytest from prometheus_client import CollectorRegistry, start_http_server _PROMETHEUS_PORT = 9464 @pytest.fixture(scope="session") def prometheus_registry() -> CollectorRegistry: return CollectorRegistry() @pytest.fixture(scope="session", autouse=True) def _prometheus_scrape_target(prometheus_registry: CollectorRegistry) -> Iterator[None]: """Keep a Prometheus HTTP endpoint up on :9464 for the whole docker-test session.""" httpd, _thread = start_http_server(port=_PROMETHEUS_PORT, addr="0.0.0.0", registry=prometheus_registry) try: yield finally: httpd.shutdown() httpd.server_close() python-opentelemetry-1.45.2/tests/opentelemetry-docker-tests/tests/declconfig/000077500000000000000000000000001527070070700277255ustar00rootroot00000000000000python-opentelemetry-1.45.2/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/000077500000000000000000000000001527070070700317135ustar00rootroot00000000000000metric_cardinality_limit_default/000077500000000000000000000000001527070070700404045ustar00rootroot00000000000000python-opentelemetry-1.45.2/tests/opentelemetry-docker-tests/tests/declconfig/scenariosotel-config.yaml000066400000000000000000000004571527070070700435040ustar00rootroot00000000000000python-opentelemetry-1.45.2/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/metric_cardinality_limit_defaultfile_format: "1.0" resource: attributes: - name: service.name value: declconfig-metric-cardinality-limit-default meter_provider: readers: - periodic: interval: 60000 exporter: otlp_http: endpoint: http://${OTEL_COLLECTOR_ENDPOINT}:4318/v1/metrics run.py000066400000000000000000000004221527070070700415600ustar00rootroot00000000000000python-opentelemetry-1.45.2/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/metric_cardinality_limit_default# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from opentelemetry import metrics requests = metrics.get_meter("declconfig.cardinality").create_counter("requests") for request_id in range(2100): requests.add(1, {"request.id": request_id}) scenario.yaml000066400000000000000000000006001527070070700430670ustar00rootroot00000000000000python-opentelemetry-1.45.2/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/metric_cardinality_limit_defaultdescription: > Without any configured limit, a metric stream is limited to 2000 attribute sets plus the overflow point, which aggregates the other 100 measurements. until: - exited: {} expect: - metric: {name: requests, count: 2001} - metric: name: requests data_point: attributes: {values: {otel.metric.overflow: true}, strict: true} value: 100 metric_cardinality_limit_delta/000077500000000000000000000000001527070070700400515ustar00rootroot00000000000000python-opentelemetry-1.45.2/tests/opentelemetry-docker-tests/tests/declconfig/scenariosotel-config.yaml000066400000000000000000000006071527070070700431460ustar00rootroot00000000000000python-opentelemetry-1.45.2/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/metric_cardinality_limit_deltafile_format: "1.0" resource: attributes: - name: service.name value: declconfig-metric-cardinality-limit-delta meter_provider: readers: - periodic: interval: 2000 exporter: otlp_http: endpoint: http://${OTEL_COLLECTOR_ENDPOINT}:4318/v1/metrics temporality_preference: delta cardinality_limits: default: 3 run.py000066400000000000000000000007051527070070700412310ustar00rootroot00000000000000python-opentelemetry-1.45.2/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/metric_cardinality_limit_delta# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import time from opentelemetry import metrics requests = metrics.get_meter("declconfig.cardinality").create_counter("requests") def record(batch): for route in range(10): requests.add(1, {"batch": batch, "route": f"/{route}"}) record("first") # Wait for more than one export interval so the batches are collected separately. time.sleep(2.5) record("second") scenario.yaml000066400000000000000000000011451527070070700425410ustar00rootroot00000000000000python-opentelemetry-1.45.2/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/metric_cardinality_limit_deltadescription: > With delta temporality the cardinality limit applies to each collection cycle. Each batch of 10 attribute sets is limited to 3 attribute sets plus the overflow point, even though the first batch's attribute sets were already exported. until: - exited: {} expect: - metric: {name: requests, temporality: DELTA, count: 8} - metric: name: requests data_point: attributes: {values: {otel.metric.overflow: true}, strict: true} value: 7 count: 2 - metric: name: requests data_point: attributes: {values: {batch: second}} count: 3 metric_cardinality_limits_reader/000077500000000000000000000000001527070070700404055ustar00rootroot00000000000000python-opentelemetry-1.45.2/tests/opentelemetry-docker-tests/tests/declconfig/scenariosotel-config.yaml000066400000000000000000000005651527070070700435050ustar00rootroot00000000000000python-opentelemetry-1.45.2/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/metric_cardinality_limits_readerfile_format: "1.0" resource: attributes: - name: service.name value: declconfig-metric-cardinality-limits-reader meter_provider: readers: - periodic: interval: 60000 exporter: otlp_http: endpoint: http://${OTEL_COLLECTOR_ENDPOINT}:4318/v1/metrics cardinality_limits: default: 5 counter: 3 run.py000066400000000000000000000005571527070070700415720ustar00rootroot00000000000000python-opentelemetry-1.45.2/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/metric_cardinality_limits_reader# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from opentelemetry import metrics meter = metrics.get_meter("declconfig.cardinality") requests = meter.create_counter("requests") latency = meter.create_histogram("latency") for route in range(10): requests.add(1, {"route": f"/{route}"}) latency.record(1, {"route": f"/{route}"}) scenario.yaml000066400000000000000000000013421527070070700430740ustar00rootroot00000000000000python-opentelemetry-1.45.2/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/metric_cardinality_limits_readerdescription: > A reader's `cardinality_limits` applies per instrument type and falls back to `default`. Counters are limited to 3 attribute sets plus the overflow point, which aggregates the other 7 measurements. Histograms use `default` and are limited to 5 attribute sets plus the overflow point, which aggregates the other 5 measurements. until: - exited: {} expect: - metric: {name: requests, count: 4} - metric: name: requests data_point: attributes: {values: {otel.metric.overflow: true}, strict: true} value: 7 - metric: {name: latency, count: 6} - metric: name: latency data_point: attributes: {values: {otel.metric.overflow: true}, strict: true} count: 5 metric_cardinality_limits_view/000077500000000000000000000000001527070070700401155ustar00rootroot00000000000000python-opentelemetry-1.45.2/tests/opentelemetry-docker-tests/tests/declconfig/scenariosotel-config.yaml000066400000000000000000000011551527070070700432110ustar00rootroot00000000000000python-opentelemetry-1.45.2/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/metric_cardinality_limits_viewfile_format: "1.0" resource: attributes: - name: service.name value: declconfig-metric-cardinality-limits-view meter_provider: readers: - periodic: interval: 60000 exporter: otlp_http: endpoint: http://${OTEL_COLLECTOR_ENDPOINT}:4318/v1/metrics cardinality_limits: default: 2 views: - selector: instrument_name: requests stream: aggregation_cardinality_limit: 4 - selector: instrument_name: filtered stream: attribute_keys: included: [route] aggregation_cardinality_limit: 3 run.py000066400000000000000000000010131527070070700412660ustar00rootroot00000000000000python-opentelemetry-1.45.2/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/metric_cardinality_limits_view# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from opentelemetry import metrics meter = metrics.get_meter("declconfig.cardinality") requests = meter.create_counter("requests") other = meter.create_counter("other") filtered = meter.create_counter("filtered") for route in range(10): requests.add(1, {"route": f"/{route}"}) other.add(1, {"route": f"/{route}"}) for route in range(2): for user in range(10): filtered.add(1, {"route": f"/{route}", "user": f"user-{user}"}) scenario.yaml000066400000000000000000000020261527070070700426040ustar00rootroot00000000000000python-opentelemetry-1.45.2/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/metric_cardinality_limits_viewdescription: > A view's `aggregation_cardinality_limit` takes precedence over the reader's `cardinality_limits`. `requests` uses its view's limit of 4 (4 attribute sets plus the overflow point, which aggregates the other 6 measurements), while `other` uses the reader's limit of 2 (2 attribute sets plus the overflow point). `filtered` has a limit of 3, which is applied after its view keeps only the `route` attribute, so its 20 measurements make 2 attribute sets and nothing overflows. until: - exited: {} expect: - metric: {name: requests, count: 5} - metric: name: requests data_point: attributes: {values: {otel.metric.overflow: true}, strict: true} value: 6 - metric: {name: other, count: 3} - metric: name: other data_point: attributes: {values: {otel.metric.overflow: true}, strict: true} value: 8 - metric: {name: filtered, count: 2} - metric: name: filtered data_point: attributes: {values: {otel.metric.overflow: true}} count: 0 python-opentelemetry-1.45.2/tests/opentelemetry-docker-tests/tests/docker-compose.yml000066400000000000000000000012661527070070700312720ustar00rootroot00000000000000services: otopencensus: image: rafaeljesus/opencensus-collector:latest@sha256:1bfcd226bb2eaf0702e09888507abfe345537f9f859cfba4143c0093f20bf68a command: --logging-exporter DEBUG ports: - "8888:8888" - "55678:55678" otcollector: image: otel/opentelemetry-collector-contrib:0.161.0@sha256:fd328de2552466ad78385e1b1289c3f2402b1c45f265b252aab1955b42845ac1 command: ["--config=/etc/otelcol/collector-config.yaml"] volumes: - ./collector-config.yaml:/etc/otelcol/collector-config.yaml - ./otlpexporter/otlp-file-data:/otlp-file-data ports: - "4317:4317" - "4318:4318" extra_hosts: - "host.docker.internal:host-gateway" python-opentelemetry-1.45.2/tests/opentelemetry-docker-tests/tests/opencensus/000077500000000000000000000000001527070070700300125ustar00rootroot00000000000000test_opencensusexporter_functional.py000066400000000000000000000031501527070070700375400ustar00rootroot00000000000000python-opentelemetry-1.45.2/tests/opentelemetry-docker-tests/tests/opencensus# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from opentelemetry import trace from opentelemetry.context import attach, detach, set_value from opentelemetry.exporter.opencensus.trace_exporter import ( OpenCensusSpanExporter, ) from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import SimpleSpanProcessor from opentelemetry.test.test_base import TestBase class ExportStatusSpanProcessor(SimpleSpanProcessor): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.export_status = [] def on_end(self, span): token = attach(set_value("suppress_instrumentation", True)) self.export_status.append(self.span_exporter.export((span,))) detach(token) class TestOpenCensusSpanExporter(TestBase): def setUp(self): super().setUp() trace.set_tracer_provider(TracerProvider()) self.tracer = trace.get_tracer(__name__) self.span_processor = ExportStatusSpanProcessor(OpenCensusSpanExporter(endpoint="localhost:55678")) trace.get_tracer_provider().add_span_processor(self.span_processor) def test_export(self): with self.tracer.start_as_current_span("foo"): with self.tracer.start_as_current_span("bar"): with self.tracer.start_as_current_span("baz"): pass self.assertTrue(len(self.span_processor.export_status), 3) for export_status in self.span_processor.export_status: self.assertEqual(export_status.name, "SUCCESS") self.assertEqual(export_status.value, 0) python-opentelemetry-1.45.2/tests/opentelemetry-docker-tests/tests/otlpexporter/000077500000000000000000000000001527070070700303775ustar00rootroot00000000000000python-opentelemetry-1.45.2/tests/opentelemetry-docker-tests/tests/otlpexporter/__init__.py000066400000000000000000000034341527070070700325140ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass, field from pathlib import Path from typing import Any, Generic, Literal, TypeVar from uuid import uuid4 ExporterT = TypeVar("ExporterT") CUSTOM_HEADERS: dict[str, str] = { "x-custom-header": "custom-value", "x-another-header": "another-value", } # Directory bind mounted into the collector container (see docker-compose.yml). OTLP_FILE_DATA_DIR = Path(__file__).parent / "otlp-file-data" @dataclass class ExporterConfig(Generic[ExporterT]): id: str exporter_class: type[ExporterT] kwargs: dict[str, Any] = field(default_factory=dict) lazy_kwargs: dict[str, Callable[[], Any]] = field(default_factory=dict) def build(self) -> ExporterT: kwargs = { **self.kwargs, **{key: factory() for key, factory in self.lazy_kwargs.items()}, } return self.exporter_class(**kwargs) def make_otlp_file(signal: Literal["traces", "metrics", "logs"]) -> str: """Return a unique .jsonl path under the collector mounted directory.""" directory = OTLP_FILE_DATA_DIR / signal directory.mkdir(parents=True, exist_ok=True) return str(directory / f"{uuid4().hex}.jsonl") def _attrs_to_dict(attributes) -> dict: result = {} for kv in attributes: which = kv.value.WhichOneof("value") if which == "string_value": result[kv.key] = kv.value.string_value elif which == "int_value": result[kv.key] = kv.value.int_value elif which == "double_value": result[kv.key] = kv.value.double_value elif which == "bool_value": result[kv.key] = kv.value.bool_value return result python-opentelemetry-1.45.2/tests/opentelemetry-docker-tests/tests/otlpexporter/conftest.py000066400000000000000000000007171527070070700326030ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations from collections.abc import Iterator import pytest from opentelemetry.test._otlp_test_server import OtlpProtoTestServer @pytest.fixture(scope="class") def server() -> Iterator[OtlpProtoTestServer]: test_server = OtlpProtoTestServer(host="0.0.0.0", port=4319).start() try: yield test_server finally: test_server.stop() test_otlp_logs_functional.py000066400000000000000000000223171527070070700361620ustar00rootroot00000000000000python-opentelemetry-1.45.2/tests/opentelemetry-docker-tests/tests/otlpexporter# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations import math from collections.abc import Iterator import pytest from grpc import Compression as GRPCCompression from inline_snapshot import snapshot from opentelemetry._logs import Logger, SeverityNumber from opentelemetry.exporter.http.transport._requests import ( RequestsHTTPTransport, ) from opentelemetry.exporter.http.transport._urllib3 import ( Urllib3HTTPTransport, ) from opentelemetry.exporter.otlp.common.http import ( Compression as JSONCompression, ) from opentelemetry.exporter.otlp.json.file._log_exporter import ( FileLogExporter, ) from opentelemetry.exporter.otlp.json.http._log_exporter import ( OTLPLogExporter as JSONLogExporter, ) from opentelemetry.exporter.otlp.proto.grpc._log_exporter import ( OTLPLogExporter as GRPCLogExporter, ) from opentelemetry.exporter.otlp.proto.http import ( Compression as HTTPCompression, ) from opentelemetry.exporter.otlp.proto.http._log_exporter import ( OTLPLogExporter as HTTPLogExporter, ) from opentelemetry.sdk._logs import LoggerProvider from opentelemetry.sdk._logs.export import ( LogRecordExporter, SimpleLogRecordProcessor, ) from opentelemetry.sdk.resources import Resource from opentelemetry.test._otlp_test_server import OtlpProtoTestServer from . import CUSTOM_HEADERS, ExporterConfig, _attrs_to_dict, make_otlp_file LOG_EXPORTER_CONFIGS: list[ExporterConfig[LogRecordExporter]] = [ ExporterConfig( id="http-urllib3", exporter_class=HTTPLogExporter, kwargs={"endpoint": "http://localhost:4318/v1/logs"}, lazy_kwargs={"_transport": Urllib3HTTPTransport}, ), ExporterConfig( id="http-requests", exporter_class=HTTPLogExporter, kwargs={"endpoint": "http://localhost:4318/v1/logs"}, lazy_kwargs={"_transport": RequestsHTTPTransport}, ), ExporterConfig( id="http-urllib3-deflate", exporter_class=HTTPLogExporter, kwargs={ "endpoint": "http://localhost:4318/v1/logs", "compression": HTTPCompression.Deflate, }, lazy_kwargs={"_transport": Urllib3HTTPTransport}, ), ExporterConfig( id="http-requests-deflate", exporter_class=HTTPLogExporter, kwargs={ "endpoint": "http://localhost:4318/v1/logs", "compression": HTTPCompression.Deflate, }, lazy_kwargs={"_transport": RequestsHTTPTransport}, ), ExporterConfig( id="http-urllib3-gzip", exporter_class=HTTPLogExporter, kwargs={ "endpoint": "http://localhost:4318/v1/logs", "compression": HTTPCompression.Gzip, }, lazy_kwargs={"_transport": Urllib3HTTPTransport}, ), ExporterConfig( id="http-requests-gzip", exporter_class=HTTPLogExporter, kwargs={ "endpoint": "http://localhost:4318/v1/logs", "compression": HTTPCompression.Gzip, }, lazy_kwargs={"_transport": RequestsHTTPTransport}, ), ExporterConfig( id="http-urllib3-headers", exporter_class=HTTPLogExporter, kwargs={ "endpoint": "http://localhost:4318/v1/logs", "headers": CUSTOM_HEADERS, }, lazy_kwargs={"_transport": Urllib3HTTPTransport}, ), ExporterConfig( id="http-requests-headers", exporter_class=HTTPLogExporter, kwargs={ "endpoint": "http://localhost:4318/v1/logs", "headers": CUSTOM_HEADERS, }, lazy_kwargs={"_transport": RequestsHTTPTransport}, ), ExporterConfig( id="grpc", exporter_class=GRPCLogExporter, kwargs={"insecure": True}, ), ExporterConfig( id="grpc-gzip", exporter_class=GRPCLogExporter, kwargs={"insecure": True, "compression": GRPCCompression.Gzip}, ), ExporterConfig( id="grpc-headers", exporter_class=GRPCLogExporter, kwargs={"insecure": True, "headers": CUSTOM_HEADERS}, ), ExporterConfig( id="file", exporter_class=FileLogExporter, lazy_kwargs={"path": lambda: make_otlp_file("logs")}, ), ExporterConfig( id="json-urllib3", exporter_class=JSONLogExporter, kwargs={"endpoint": "http://localhost:4318/v1/logs"}, lazy_kwargs={"_transport": Urllib3HTTPTransport}, ), ExporterConfig( id="json-urllib3-deflate", exporter_class=JSONLogExporter, kwargs={ "endpoint": "http://localhost:4318/v1/logs", "compression": JSONCompression.DEFLATE, }, lazy_kwargs={"_transport": Urllib3HTTPTransport}, ), ExporterConfig( id="json-urllib3-gzip", exporter_class=JSONLogExporter, kwargs={ "endpoint": "http://localhost:4318/v1/logs", "compression": JSONCompression.GZIP, }, lazy_kwargs={"_transport": Urllib3HTTPTransport}, ), ExporterConfig( id="json-requests", exporter_class=JSONLogExporter, kwargs={"endpoint": "http://localhost:4318/v1/logs"}, lazy_kwargs={"_transport": RequestsHTTPTransport}, ), ExporterConfig( id="json-requests-deflate", exporter_class=JSONLogExporter, kwargs={ "endpoint": "http://localhost:4318/v1/logs", "compression": JSONCompression.DEFLATE, }, lazy_kwargs={"_transport": RequestsHTTPTransport}, ), ExporterConfig( id="json-requests-gzip", exporter_class=JSONLogExporter, kwargs={ "endpoint": "http://localhost:4318/v1/logs", "compression": JSONCompression.GZIP, }, lazy_kwargs={"_transport": RequestsHTTPTransport}, ), ] class TestLogsExporter: @pytest.fixture(scope="class", params=LOG_EXPORTER_CONFIGS, ids=lambda c: c.id) def config(self, request) -> ExporterConfig[LogRecordExporter]: return request.param @pytest.fixture(scope="class") def logger_provider( self, config: ExporterConfig[LogRecordExporter], server: OtlpProtoTestServer, ) -> Iterator[LoggerProvider]: provider = LoggerProvider( resource=Resource.create({"service.name": "test-service"}), ) provider.add_log_record_processor(SimpleLogRecordProcessor(config.build())) try: yield provider finally: provider.shutdown() @pytest.fixture(scope="class") def logger(self, logger_provider: LoggerProvider) -> Logger: return logger_provider.get_logger(__name__) @pytest.fixture(autouse=True) def clear_server(self, server: OtlpProtoTestServer) -> None: server.clear() def test_log_body(self, logger: Logger, server: OtlpProtoTestServer): logger.emit(body="hello world", severity_number=SeverityNumber.INFO) recorded = server.get_log_record(timeout=5000.0) assert recorded.log_record.body.string_value == snapshot("hello world") def test_log_severity_number(self, logger: Logger, server: OtlpProtoTestServer): logger.emit(severity_number=SeverityNumber.ERROR, body="error occurred") recorded = server.get_log_record(timeout=5.0) assert recorded.log_record.severity_number == SeverityNumber.ERROR.value def test_log_severity_text(self, logger: Logger, server: OtlpProtoTestServer): logger.emit( severity_number=SeverityNumber.WARN, severity_text="WARN", body="warning", ) recorded = server.get_log_record(timeout=5.0) assert recorded.log_record.severity_text == snapshot("WARN") def test_log_attributes(self, logger: Logger, server: OtlpProtoTestServer): logger.emit( body="attrs test", severity_number=SeverityNumber.INFO, attributes={ "str_key": "hello", "int_key": 42, "float_key": 3.14, "bool_key": True, }, ) recorded = server.get_log_record(timeout=5.0) attrs = _attrs_to_dict(recorded.log_record.attributes) assert math.isclose(attrs.pop("float_key"), 3.14, abs_tol=1e-5) assert attrs == snapshot({"str_key": "hello", "int_key": 42, "bool_key": True}) def test_scope_attributes(self, logger_provider: LoggerProvider, server: OtlpProtoTestServer): logger = logger_provider.get_logger( "test.scope", version="1.0.0", attributes={"scope.key": "scope.val"}, ) logger.emit(body="scope test", severity_number=SeverityNumber.INFO) recorded = server.get_log_record(timeout=5.0) assert recorded.scope.name == snapshot("test.scope") assert recorded.scope.version == snapshot("1.0.0") assert _attrs_to_dict(recorded.scope.attributes) == snapshot({"scope.key": "scope.val"}) def test_resource_attributes(self, logger: Logger, server: OtlpProtoTestServer): logger.emit(body="resource test", severity_number=SeverityNumber.INFO) recorded = server.get_log_record(timeout=5.0) resource_attrs = _attrs_to_dict(recorded.resource.attributes) assert resource_attrs["service.name"] == snapshot("test-service") test_otlp_metrics_functional.py000066400000000000000000000313111527070070700366560ustar00rootroot00000000000000python-opentelemetry-1.45.2/tests/opentelemetry-docker-tests/tests/otlpexporter# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations import math from collections.abc import Iterator import pytest from grpc import Compression as GRPCCompression from inline_snapshot import snapshot from opentelemetry.exporter.http.transport._requests import ( RequestsHTTPTransport, ) from opentelemetry.exporter.http.transport._urllib3 import ( Urllib3HTTPTransport, ) from opentelemetry.exporter.otlp.common.http import ( Compression as JSONCompression, ) from opentelemetry.exporter.otlp.json.file.metric_exporter import ( FileMetricExporter, ) from opentelemetry.exporter.otlp.json.http.metric_exporter import ( OTLPMetricExporter as JSONMetricExporter, ) from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import ( OTLPMetricExporter as GRPCMetricExporter, ) from opentelemetry.exporter.otlp.proto.http import ( Compression as HTTPCompression, ) from opentelemetry.exporter.otlp.proto.http.metric_exporter import ( OTLPMetricExporter as HTTPMetricExporter, ) from opentelemetry.metrics import Meter from opentelemetry.sdk.metrics import MeterProvider from opentelemetry.sdk.metrics._internal.export import ( PeriodicExportingMetricReader, ) from opentelemetry.sdk.metrics.export import MetricExporter from opentelemetry.sdk.metrics.view import ( ExponentialBucketHistogramAggregation, View, ) from opentelemetry.sdk.resources import Resource from opentelemetry.test._otlp_test_server import OtlpProtoTestServer from . import CUSTOM_HEADERS, ExporterConfig, _attrs_to_dict, make_otlp_file METRIC_EXPORTER_CONFIGS: list[ExporterConfig[MetricExporter]] = [ ExporterConfig( id="http-urllib3", exporter_class=HTTPMetricExporter, kwargs={"endpoint": "http://localhost:4318/v1/metrics"}, lazy_kwargs={"_transport": Urllib3HTTPTransport}, ), ExporterConfig( id="http-requests", exporter_class=HTTPMetricExporter, kwargs={"endpoint": "http://localhost:4318/v1/metrics"}, lazy_kwargs={"_transport": RequestsHTTPTransport}, ), ExporterConfig( id="http-urllib3-deflate", exporter_class=HTTPMetricExporter, kwargs={ "endpoint": "http://localhost:4318/v1/metrics", "compression": HTTPCompression.Deflate, }, lazy_kwargs={"_transport": Urllib3HTTPTransport}, ), ExporterConfig( id="http-requests-deflate", exporter_class=HTTPMetricExporter, kwargs={ "endpoint": "http://localhost:4318/v1/metrics", "compression": HTTPCompression.Deflate, }, lazy_kwargs={"_transport": RequestsHTTPTransport}, ), ExporterConfig( id="http-urllib3-gzip", exporter_class=HTTPMetricExporter, kwargs={ "endpoint": "http://localhost:4318/v1/metrics", "compression": HTTPCompression.Gzip, }, lazy_kwargs={"_transport": Urllib3HTTPTransport}, ), ExporterConfig( id="http-requests-gzip", exporter_class=HTTPMetricExporter, kwargs={ "endpoint": "http://localhost:4318/v1/metrics", "compression": HTTPCompression.Gzip, }, lazy_kwargs={"_transport": RequestsHTTPTransport}, ), ExporterConfig( id="http-urllib3-headers", exporter_class=HTTPMetricExporter, kwargs={ "endpoint": "http://localhost:4318/v1/metrics", "headers": CUSTOM_HEADERS, }, lazy_kwargs={"_transport": Urllib3HTTPTransport}, ), ExporterConfig( id="http-requests-headers", exporter_class=HTTPMetricExporter, kwargs={ "endpoint": "http://localhost:4318/v1/metrics", "headers": CUSTOM_HEADERS, }, lazy_kwargs={"_transport": RequestsHTTPTransport}, ), ExporterConfig( id="grpc", exporter_class=GRPCMetricExporter, kwargs={"insecure": True}, ), ExporterConfig( id="grpc-gzip", exporter_class=GRPCMetricExporter, kwargs={"insecure": True, "compression": GRPCCompression.Gzip}, ), ExporterConfig( id="grpc-headers", exporter_class=GRPCMetricExporter, kwargs={"insecure": True, "headers": CUSTOM_HEADERS}, ), ExporterConfig( id="file", exporter_class=FileMetricExporter, lazy_kwargs={"path": lambda: make_otlp_file("metrics")}, ), ExporterConfig( id="json-urllib3", exporter_class=JSONMetricExporter, kwargs={"endpoint": "http://localhost:4318/v1/metrics"}, lazy_kwargs={"_transport": Urllib3HTTPTransport}, ), ExporterConfig( id="json-urllib3-deflate", exporter_class=JSONMetricExporter, kwargs={ "endpoint": "http://localhost:4318/v1/metrics", "compression": JSONCompression.DEFLATE, }, lazy_kwargs={"_transport": Urllib3HTTPTransport}, ), ExporterConfig( id="json-urllib3-gzip", exporter_class=JSONMetricExporter, kwargs={ "endpoint": "http://localhost:4318/v1/metrics", "compression": JSONCompression.GZIP, }, lazy_kwargs={"_transport": Urllib3HTTPTransport}, ), ExporterConfig( id="json-requests", exporter_class=JSONMetricExporter, kwargs={"endpoint": "http://localhost:4318/v1/metrics"}, lazy_kwargs={"_transport": RequestsHTTPTransport}, ), ExporterConfig( id="json-requests-deflate", exporter_class=JSONMetricExporter, kwargs={ "endpoint": "http://localhost:4318/v1/metrics", "compression": JSONCompression.DEFLATE, }, lazy_kwargs={"_transport": RequestsHTTPTransport}, ), ExporterConfig( id="json-requests-gzip", exporter_class=JSONMetricExporter, kwargs={ "endpoint": "http://localhost:4318/v1/metrics", "compression": JSONCompression.GZIP, }, lazy_kwargs={"_transport": RequestsHTTPTransport}, ), ] class TestMetricsExporter: @pytest.fixture(scope="class", params=METRIC_EXPORTER_CONFIGS, ids=lambda c: c.id) def config(self, request) -> ExporterConfig[MetricExporter]: return request.param @pytest.fixture(scope="class") def reader( self, config: ExporterConfig[MetricExporter], server: OtlpProtoTestServer, ) -> PeriodicExportingMetricReader: return PeriodicExportingMetricReader(config.build(), export_interval_millis=math.inf) @pytest.fixture(scope="class") def meter_provider(self, reader: PeriodicExportingMetricReader) -> Iterator[MeterProvider]: provider = MeterProvider( metric_readers=[reader], resource=Resource.create({"service.name": "test-service"}), ) try: yield provider finally: provider.shutdown() @pytest.fixture(scope="class") def meter(self, meter_provider: MeterProvider) -> Meter: return meter_provider.get_meter(__name__) @pytest.fixture(autouse=True) def clear_server(self, server: OtlpProtoTestServer) -> None: server.clear() def test_sum_counter( self, meter: Meter, reader: PeriodicExportingMetricReader, server: OtlpProtoTestServer, ): counter = meter.create_counter("test.counter", unit="requests") counter.add(3, {"status": "ok"}) counter.add(7, {"status": "ok"}) counter.add(5, {"status": "error"}) reader.force_flush(timeout_millis=5000) recorded = server.wait_for_metric(name="test.counter", timeout=5.0) assert recorded.metric.name == snapshot("test.counter") assert recorded.metric.unit == snapshot("requests") assert recorded.metric.HasField("sum") assert recorded.metric.sum.is_monotonic dps = {_attrs_to_dict(dp.attributes)["status"]: dp.as_int for dp in recorded.metric.sum.data_points} assert dps == snapshot({"ok": 10, "error": 5}) def test_sum_up_down_counter( self, meter: Meter, reader: PeriodicExportingMetricReader, server: OtlpProtoTestServer, ): counter = meter.create_up_down_counter("test.up_down_counter") counter.add(10) counter.add(-3) reader.force_flush(timeout_millis=5000) recorded = server.wait_for_metric(name="test.up_down_counter", timeout=5.0) assert recorded.metric.HasField("sum") assert not recorded.metric.sum.is_monotonic assert recorded.metric.sum.data_points[0].as_int == 7 def test_gauge( self, meter: Meter, reader: PeriodicExportingMetricReader, server: OtlpProtoTestServer, ): gauge = meter.create_gauge("test.gauge") gauge.set(42, {"status": "active"}) reader.force_flush(timeout_millis=5000) recorded = server.wait_for_metric(name="test.gauge", timeout=5.0) assert recorded.metric.HasField("gauge") assert recorded.metric.gauge.data_points[0].as_int == snapshot(42) def test_explicit_bucket_histogram( self, meter: Meter, reader: PeriodicExportingMetricReader, server: OtlpProtoTestServer, ): histogram = meter.create_histogram("test.histogram") histogram.record(5) histogram.record(15) histogram.record(150) reader.force_flush(timeout_millis=5000) recorded = server.wait_for_metric(name="test.histogram", timeout=5.0) assert recorded.metric.HasField("histogram") dp = recorded.metric.histogram.data_points[0] assert dp.count == 3 assert math.isclose(dp.sum, 170.0, abs_tol=1e-5) assert len(dp.bucket_counts) > 0 assert len(dp.explicit_bounds) > 0 def test_exponential_histogram( self, config: ExporterConfig[MetricExporter], server: OtlpProtoTestServer, ): reader = PeriodicExportingMetricReader(config.build(), export_interval_millis=math.inf) meter_provider = MeterProvider( metric_readers=[reader], resource=Resource.create({"service.name": "test-service"}), views=[ View( instrument_name="test.exp.histogram", aggregation=ExponentialBucketHistogramAggregation(), ) ], ) meter = meter_provider.get_meter(__name__) histogram = meter.create_histogram("test.exp.histogram") for v in [1.0, 2.0, 4.0]: histogram.record(v) reader.force_flush(timeout_millis=5000) recorded = server.wait_for_metric(name="test.exp.histogram", timeout=5.0) assert recorded.metric.HasField("exponential_histogram") dp = recorded.metric.exponential_histogram.data_points[0] assert dp.count == 3 assert math.isclose(dp.sum, 7.0, abs_tol=1e-5) assert len(dp.positive.bucket_counts) > 0 meter_provider.shutdown() def test_metric_data_point_attributes( self, meter: Meter, reader: PeriodicExportingMetricReader, server: OtlpProtoTestServer, ): counter = meter.create_counter("test.attrs.counter") counter.add(1, {"str_key": "hello", "int_key": 42}) reader.force_flush(timeout_millis=5000) recorded = server.wait_for_metric(name="test.attrs.counter", timeout=5.0) attrs = _attrs_to_dict(recorded.metric.sum.data_points[0].attributes) assert attrs == snapshot({"str_key": "hello", "int_key": 42}) def test_scope_attributes( self, meter_provider: MeterProvider, reader: PeriodicExportingMetricReader, server: OtlpProtoTestServer, ): meter = meter_provider.get_meter( "test.scope", version="1.0.0", attributes={"scope.key": "scope.val"}, ) counter = meter.create_counter("scope.counter") counter.add(1) reader.force_flush(timeout_millis=5000) recorded = server.wait_for_metric(name="scope.counter", timeout=5.0) assert recorded.scope.name == snapshot("test.scope") assert recorded.scope.version == snapshot("1.0.0") assert _attrs_to_dict(recorded.scope.attributes) == snapshot({"scope.key": "scope.val"}) def test_resource_attributes( self, meter: Meter, reader: PeriodicExportingMetricReader, server: OtlpProtoTestServer, ): counter = meter.create_counter("resource.counter") counter.add(1) reader.force_flush(timeout_millis=5000) recorded = server.wait_for_metric(name="resource.counter", timeout=5.0) resource_attrs = _attrs_to_dict(recorded.resource.attributes) assert resource_attrs["service.name"] == snapshot("test-service") test_otlp_traces_functional.py000066400000000000000000000234771527070070700365070ustar00rootroot00000000000000python-opentelemetry-1.45.2/tests/opentelemetry-docker-tests/tests/otlpexporter# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations import math from collections.abc import Iterator import pytest from grpc import Compression as GRPCCompression from inline_snapshot import snapshot from opentelemetry.exporter.http.transport._requests import ( RequestsHTTPTransport, ) from opentelemetry.exporter.http.transport._urllib3 import ( Urllib3HTTPTransport, ) from opentelemetry.exporter.otlp.common.http import ( Compression as JSONCompression, ) from opentelemetry.exporter.otlp.json.file.trace_exporter import ( FileSpanExporter, ) from opentelemetry.exporter.otlp.json.http.trace_exporter import ( OTLPSpanExporter as JSONSpanExporter, ) from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import ( OTLPSpanExporter as GRPCSpanExporter, ) from opentelemetry.exporter.otlp.proto.http import ( Compression as HTTPCompression, ) from opentelemetry.exporter.otlp.proto.http.trace_exporter import ( OTLPSpanExporter as HTTPSpanExporter, ) from opentelemetry.sdk.trace import Tracer, TracerProvider from opentelemetry.sdk.trace.export import SimpleSpanProcessor, SpanExporter from opentelemetry.test._otlp_test_server import OtlpProtoTestServer from opentelemetry.trace import Link, SpanContext, StatusCode, TraceFlags from . import CUSTOM_HEADERS, ExporterConfig, _attrs_to_dict, make_otlp_file TRACE_EXPORTER_CONFIGS: list[ExporterConfig[SpanExporter]] = [ ExporterConfig( id="http-urllib3", exporter_class=HTTPSpanExporter, kwargs={"endpoint": "http://localhost:4318/v1/traces"}, lazy_kwargs={"_transport": Urllib3HTTPTransport}, ), ExporterConfig( id="http-requests", exporter_class=HTTPSpanExporter, kwargs={"endpoint": "http://localhost:4318/v1/traces"}, lazy_kwargs={"_transport": RequestsHTTPTransport}, ), ExporterConfig( id="http-urllib3-deflate", exporter_class=HTTPSpanExporter, kwargs={ "endpoint": "http://localhost:4318/v1/traces", "compression": HTTPCompression.Deflate, }, lazy_kwargs={"_transport": Urllib3HTTPTransport}, ), ExporterConfig( id="http-requests-deflate", exporter_class=HTTPSpanExporter, kwargs={ "endpoint": "http://localhost:4318/v1/traces", "compression": HTTPCompression.Deflate, }, lazy_kwargs={"_transport": RequestsHTTPTransport}, ), ExporterConfig( id="http-urllib3-gzip", exporter_class=HTTPSpanExporter, kwargs={ "endpoint": "http://localhost:4318/v1/traces", "compression": HTTPCompression.Gzip, }, lazy_kwargs={"_transport": Urllib3HTTPTransport}, ), ExporterConfig( id="http-requests-gzip", exporter_class=HTTPSpanExporter, kwargs={ "endpoint": "http://localhost:4318/v1/traces", "compression": HTTPCompression.Gzip, }, lazy_kwargs={"_transport": RequestsHTTPTransport}, ), ExporterConfig( id="http-urllib3-headers", exporter_class=HTTPSpanExporter, kwargs={ "endpoint": "http://localhost:4318/v1/traces", "headers": CUSTOM_HEADERS, }, lazy_kwargs={"_transport": Urllib3HTTPTransport}, ), ExporterConfig( id="http-requests-headers", exporter_class=HTTPSpanExporter, kwargs={ "endpoint": "http://localhost:4318/v1/traces", "headers": CUSTOM_HEADERS, }, lazy_kwargs={"_transport": RequestsHTTPTransport}, ), ExporterConfig( id="grpc", exporter_class=GRPCSpanExporter, kwargs={"insecure": True}, ), ExporterConfig( id="grpc-gzip", exporter_class=GRPCSpanExporter, kwargs={"insecure": True, "compression": GRPCCompression.Gzip}, ), ExporterConfig( id="grpc-headers", exporter_class=GRPCSpanExporter, kwargs={"insecure": True, "headers": CUSTOM_HEADERS}, ), ExporterConfig( id="file", exporter_class=FileSpanExporter, lazy_kwargs={"path": lambda: make_otlp_file("traces")}, ), ExporterConfig( id="json-urllib3", exporter_class=JSONSpanExporter, kwargs={"endpoint": "http://localhost:4318/v1/traces"}, lazy_kwargs={"_transport": Urllib3HTTPTransport}, ), ExporterConfig( id="json-urllib3-deflate", exporter_class=JSONSpanExporter, kwargs={ "endpoint": "http://localhost:4318/v1/traces", "compression": JSONCompression.DEFLATE, }, lazy_kwargs={"_transport": Urllib3HTTPTransport}, ), ExporterConfig( id="json-urllib3-gzip", exporter_class=JSONSpanExporter, kwargs={ "endpoint": "http://localhost:4318/v1/traces", "compression": JSONCompression.GZIP, }, lazy_kwargs={"_transport": Urllib3HTTPTransport}, ), ExporterConfig( id="json-requests", exporter_class=JSONSpanExporter, kwargs={"endpoint": "http://localhost:4318/v1/traces"}, lazy_kwargs={"_transport": RequestsHTTPTransport}, ), ExporterConfig( id="json-requests-deflate", exporter_class=JSONSpanExporter, kwargs={ "endpoint": "http://localhost:4318/v1/traces", "compression": JSONCompression.DEFLATE, }, lazy_kwargs={"_transport": RequestsHTTPTransport}, ), ExporterConfig( id="json-requests-gzip", exporter_class=JSONSpanExporter, kwargs={ "endpoint": "http://localhost:4318/v1/traces", "compression": JSONCompression.GZIP, }, lazy_kwargs={"_transport": RequestsHTTPTransport}, ), ] class TestTracesExporter: @pytest.fixture(scope="class", params=TRACE_EXPORTER_CONFIGS, ids=lambda c: c.id) def config(self, request) -> ExporterConfig[SpanExporter]: return request.param @pytest.fixture(scope="class") def tracer( self, config: ExporterConfig[SpanExporter], server: OtlpProtoTestServer, ) -> Iterator[Tracer]: tracer_provider = TracerProvider() tracer_provider.add_span_processor(SimpleSpanProcessor(config.build())) try: yield tracer_provider.get_tracer(__name__) finally: tracer_provider.shutdown() @pytest.fixture(autouse=True) def clear_server(self, server: OtlpProtoTestServer) -> None: server.clear() def test_simple_span_name(self, tracer: Tracer, server: OtlpProtoTestServer): with tracer.start_as_current_span("my-span"): pass recorded = server.get_span(timeout=5.0) assert recorded.span.name == "my-span" def test_span_attributes(self, tracer: Tracer, server: OtlpProtoTestServer): with tracer.start_as_current_span( "attrs-span", attributes={ "str_key": "hello", "int_key": 42, "float_key": 3.14, "bool_key": True, }, ): pass recorded = server.get_span(timeout=5.0) attrs = _attrs_to_dict(recorded.span.attributes) assert math.isclose(attrs.pop("float_key"), 3.14, abs_tol=1e-5) assert attrs == snapshot({"str_key": "hello", "int_key": 42, "bool_key": True}) def test_nested_spans_parent_child(self, tracer: Tracer, server: OtlpProtoTestServer): with tracer.start_as_current_span("foo"): with tracer.start_as_current_span("bar"): with tracer.start_as_current_span("baz"): pass spans = {r.span.name: r.span for r in server.get_spans(count=3, timeout=10.0)} assert set(spans.keys()) == snapshot({"bar", "baz", "foo"}) assert spans["baz"].parent_span_id == spans["bar"].span_id assert spans["bar"].parent_span_id == spans["foo"].span_id assert spans["foo"].parent_span_id == b"" def test_span_with_event(self, tracer: Tracer, server: OtlpProtoTestServer): with tracer.start_as_current_span("event-span") as span: span.add_event("my-event", {"event_key": "event_val"}) recorded = server.get_span(timeout=5.0) assert len(recorded.span.events) == 1 event = recorded.span.events[0] assert event.name == "my-event" assert _attrs_to_dict(event.attributes) == snapshot({"event_key": "event_val"}) def test_span_with_link(self, tracer: Tracer, server: OtlpProtoTestServer): link_trace_id = 0x000000000000000000000000DEADBEEF link_span_id = 0x00000000DEADBEF0 link_context = SpanContext( trace_id=link_trace_id, span_id=link_span_id, is_remote=True, trace_flags=TraceFlags(0x01), ) with tracer.start_as_current_span("linked-span", links=[Link(link_context)]): pass recorded = server.get_span(timeout=5.0) assert len(recorded.span.links) == 1 link = recorded.span.links[0] assert link.trace_id == link_trace_id.to_bytes(16, "big") assert link.span_id == link_span_id.to_bytes(8, "big") def test_span_status_ok(self, tracer: Tracer, server: OtlpProtoTestServer): with tracer.start_as_current_span("ok-span") as span: span.set_status(StatusCode.OK) recorded = server.get_span(timeout=5.0) assert recorded.span.status.code == snapshot(1) def test_span_status_error(self, tracer: Tracer, server: OtlpProtoTestServer): with tracer.start_as_current_span("error-span") as span: span.set_status(StatusCode.ERROR, "something went wrong") recorded = server.get_span(timeout=5.0) assert recorded.span.status.code == snapshot(2) assert recorded.span.status.message == snapshot("something went wrong") test_prometheus_metrics_functional.py000066400000000000000000000266271527070070700401110ustar00rootroot00000000000000python-opentelemetry-1.45.2/tests/opentelemetry-docker-tests/tests/otlpexporter# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations import math import time from collections.abc import Callable, Iterator from dataclasses import dataclass from typing import TYPE_CHECKING import pytest from prometheus_client import CollectorRegistry from opentelemetry.exporter.prometheus import PrometheusMetricReader from opentelemetry.metrics import Meter from opentelemetry.proto.metrics.v1.metrics_pb2 import AggregationTemporality from opentelemetry.sdk.metrics import MeterProvider from opentelemetry.sdk.resources import Resource from opentelemetry.test._otlp_test_server import ( OtlpProtoTestServer, RecordedMetric, ) from . import _attrs_to_dict if TYPE_CHECKING: from collections.abc import Sequence from opentelemetry.proto.metrics.v1.metrics_pb2 import ( HistogramDataPoint, Metric, NumberDataPoint, ) _CUMULATIVE = AggregationTemporality.AGGREGATION_TEMPORALITY_CUMULATIVE _DEFAULT_HISTOGRAM_BOUNDS = [ 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, ] @dataclass class PrometheusExporterConfig: id: str prefix: str = "" disable_target_info: bool = False scope_info_enabled: bool = True def build(self, registry: CollectorRegistry) -> PrometheusMetricReader: return PrometheusMetricReader( prefix=self.prefix, disable_target_info=self.disable_target_info, scope_info_enabled=self.scope_info_enabled, registry=registry, ) def expected_name(self, base: str, *, is_counter: bool = False) -> str: name = f"{self.prefix}_{base}" if self.prefix else base if is_counter: name = f"{name}_total" return name PROMETHEUS_EXPORTER_CONFIGS: list[PrometheusExporterConfig] = [ PrometheusExporterConfig(id="default"), PrometheusExporterConfig(id="prefix", prefix="e2e"), PrometheusExporterConfig(id="no-target-info", disable_target_info=True), PrometheusExporterConfig(id="no-scope-info", scope_info_enabled=False), ] # DataPointFlags bit set by the prometheus receiver for staleness markers, i.e. # a series that was present on a previous scrape but is now absent. Such points # carry no value and must be skipped. _NO_RECORDED_VALUE_FLAG = 1 def _number(data_point: NumberDataPoint) -> float: """Return whichever of the NumberDataPoint value oneof is set.""" which = data_point.WhichOneof("value") return getattr(data_point, which) def _data_points( metric: Metric, ) -> Sequence[NumberDataPoint | HistogramDataPoint]: for field in ("sum", "gauge", "histogram", "exponential_histogram"): if metric.HasField(field): return getattr(metric, field).data_points return [] def _has_recorded_value(recorded: RecordedMetric) -> bool: return any(not (dp.flags & _NO_RECORDED_VALUE_FLAG) for dp in _data_points(recorded.metric)) def _wait_for_metric_matching( server: OtlpProtoTestServer, predicate: Callable[[RecordedMetric], bool], timeout: float = 20.0, ) -> RecordedMetric: """Drain the OTLP sink until a metric matches ``predicate``.""" deadline = time.monotonic() + timeout seen: list[str] = [] while True: remaining = deadline - time.monotonic() if remaining <= 0: raise TimeoutError(f"No matching metric within {timeout}s, saw names={seen!r}") try: recorded = server.get_metric(timeout=remaining) except TimeoutError: raise TimeoutError(f"No matching metric within {timeout}s, saw names={seen!r}") from None if predicate(recorded) and _has_recorded_value(recorded): return recorded seen.append(recorded.metric.name) def _wait_for_named_metric(server: OtlpProtoTestServer, name: str) -> RecordedMetric: recorded = _wait_for_metric_matching(server, lambda r: r.metric.name == name) assert recorded.metric.name == name return recorded class TestPrometheusMetricsExporter: @pytest.fixture( scope="class", params=PROMETHEUS_EXPORTER_CONFIGS, ids=lambda c: c.id, ) def config(self, request) -> PrometheusExporterConfig: return request.param @pytest.fixture(scope="class") def registry(self, prometheus_registry: CollectorRegistry) -> CollectorRegistry: return prometheus_registry @pytest.fixture(scope="class") def reader( self, config: PrometheusExporterConfig, registry: CollectorRegistry, ) -> PrometheusMetricReader: return config.build(registry) @pytest.fixture(scope="class") def meter_provider( self, reader: PrometheusMetricReader, ) -> Iterator[MeterProvider]: provider = MeterProvider( metric_readers=[reader], resource=Resource.create({"service.name": "test-service"}), ) try: yield provider finally: provider.shutdown() @pytest.fixture(scope="class") def meter(self, meter_provider: MeterProvider) -> Meter: return meter_provider.get_meter(__name__) @pytest.fixture(autouse=True) def clear_server(self, server: OtlpProtoTestServer) -> None: server.clear() def test_counter( self, config: PrometheusExporterConfig, meter: Meter, server: OtlpProtoTestServer, ): counter = meter.create_counter("prom_counter") counter.add(3, {"status": "ok"}) counter.add(7, {"status": "ok"}) recorded = _wait_for_named_metric(server, config.expected_name("prom_counter", is_counter=True)) assert recorded.metric.HasField("sum") sum_ = recorded.metric.sum assert sum_.is_monotonic assert sum_.aggregation_temporality == _CUMULATIVE assert len(sum_.data_points) == 1 (data_point,) = sum_.data_points assert _number(data_point) == 10 assert _attrs_to_dict(data_point.attributes) == {"status": "ok"} def test_up_down_counter( self, config: PrometheusExporterConfig, meter: Meter, server: OtlpProtoTestServer, ): counter = meter.create_up_down_counter("prom_updown") counter.add(10) counter.add(-3) recorded = _wait_for_named_metric(server, config.expected_name("prom_updown")) # A cumulative non-monotonic sum is exported as a Prometheus gauge. assert recorded.metric.HasField("gauge") assert len(recorded.metric.gauge.data_points) == 1 (data_point,) = recorded.metric.gauge.data_points assert _number(data_point) == 7 assert _attrs_to_dict(data_point.attributes) == {} def test_gauge( self, config: PrometheusExporterConfig, meter: Meter, server: OtlpProtoTestServer, ): gauge = meter.create_gauge("prom_gauge") gauge.set(42, {"status": "active"}) recorded = _wait_for_named_metric(server, config.expected_name("prom_gauge")) assert recorded.metric.HasField("gauge") assert len(recorded.metric.gauge.data_points) == 1 (data_point,) = recorded.metric.gauge.data_points assert _number(data_point) == 42 assert _attrs_to_dict(data_point.attributes) == {"status": "active"} def test_histogram( self, config: PrometheusExporterConfig, meter: Meter, server: OtlpProtoTestServer, ): histogram = meter.create_histogram("prom_histogram") histogram.record(5) histogram.record(15) histogram.record(150) recorded = _wait_for_named_metric(server, config.expected_name("prom_histogram")) assert recorded.metric.HasField("histogram") assert recorded.metric.histogram.aggregation_temporality == _CUMULATIVE assert len(recorded.metric.histogram.data_points) == 1 (data_point,) = recorded.metric.histogram.data_points assert data_point.count == 3 assert math.isclose(data_point.sum, 170.0, abs_tol=1e-5) assert _attrs_to_dict(data_point.attributes) == {} assert list(data_point.explicit_bounds) == _DEFAULT_HISTOGRAM_BOUNDS # 5 -> (0, 5], 15 -> (10, 25], 150 -> (100, 250]. assert list(data_point.bucket_counts) == [ 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, ] def test_data_point_attributes( self, config: PrometheusExporterConfig, meter: Meter, server: OtlpProtoTestServer, ): counter = meter.create_counter("prom_attrs") counter.add(1, {"str_key": "hello", "int_key": 42}) recorded = _wait_for_named_metric(server, config.expected_name("prom_attrs", is_counter=True)) (data_point,) = recorded.metric.sum.data_points # Prometheus labels are strings, so integer attributes roundtrip as text. assert _attrs_to_dict(data_point.attributes) == { "str_key": "hello", "int_key": "42", } def test_target_info_resource( self, config: PrometheusExporterConfig, meter: Meter, server: OtlpProtoTestServer, ): counter = meter.create_counter("prom_target") counter.add(1) recorded = _wait_for_named_metric(server, config.expected_name("prom_target", is_counter=True)) resource_attrs = _attrs_to_dict(recorded.resource.attributes) # The resource's SDK/service attributes reach the receiver only via the # target_info metric if config.disable_target_info: assert "service_name" not in resource_attrs assert "telemetry_sdk_language" not in resource_attrs else: assert resource_attrs["service_name"] == "test-service" assert resource_attrs["telemetry_sdk_language"] == "python" def test_scope_info( self, config: PrometheusExporterConfig, meter_provider: MeterProvider, server: OtlpProtoTestServer, ): scoped_meter = meter_provider.get_meter("test-scope", version="1.2.3") counter = scoped_meter.create_counter("prom_scope") counter.add(1) recorded = _wait_for_named_metric(server, config.expected_name("prom_scope", is_counter=True)) if config.scope_info_enabled: assert recorded.scope.name == "test-scope" assert recorded.scope.version == "1.2.3" else: assert recorded.scope.name != "test-scope" assert "prometheusreceiver" in recorded.scope.name def test_scope_attributes( self, config: PrometheusExporterConfig, meter_provider: MeterProvider, server: OtlpProtoTestServer, ): scoped_meter = meter_provider.get_meter( "attr-scope", version="2.0", attributes={"scope.key": "scope.val"}, ) counter = scoped_meter.create_counter("prom_scope_attrs") counter.add(1) recorded = _wait_for_named_metric(server, config.expected_name("prom_scope_attrs", is_counter=True)) scope_attrs = _attrs_to_dict(recorded.scope.attributes) if config.scope_info_enabled: assert recorded.scope.name == "attr-scope" assert scope_attrs == {"scope_key": "scope.val"} else: assert scope_attrs == {} python-opentelemetry-1.45.2/tests/opentelemetry-test-utils/000077500000000000000000000000001527070070700241745ustar00rootroot00000000000000python-opentelemetry-1.45.2/tests/opentelemetry-test-utils/README.rst000066400000000000000000000006561527070070700256720ustar00rootroot00000000000000OpenTelemetry Test Utilities ============================ This package provides internal testing utilities for the OpenTelemetry Python project and provides no stability or quality guarantees. Please do not use it for anything other than writing or running tests for the OpenTelemetry Python project (github.com/open-telemetry/opentelemetry-python). References ---------- * `OpenTelemetry Project `_ python-opentelemetry-1.45.2/tests/opentelemetry-test-utils/pyproject.toml000066400000000000000000000024411527070070700271110ustar00rootroot00000000000000[build-system] requires = ["hatchling"] build-backend = "hatchling.build" [project] name = "opentelemetry-test-utils" dynamic = ["version"] description = "Test utilities for OpenTelemetry unit tests" 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", "Framework :: OpenTelemetry", "Intended Audience :: Developers", "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.45.2", "opentelemetry-sdk == 1.45.2", "requests ~= 2.28", ] [project.urls] Homepage = "https://github.com/open-telemetry/opentelemetry-python/tests/opentelemetry-test-utils" Repository = "https://github.com/open-telemetry/opentelemetry-python" [tool.hatch.version] path = "src/opentelemetry/test/version/__init__.py" [tool.hatch.build.targets.sdist] include = [ "/src", ] [tool.hatch.build.targets.wheel] packages = ["src/opentelemetry"] python-opentelemetry-1.45.2/tests/opentelemetry-test-utils/src/000077500000000000000000000000001527070070700247635ustar00rootroot00000000000000python-opentelemetry-1.45.2/tests/opentelemetry-test-utils/src/opentelemetry/000077500000000000000000000000001527070070700276575ustar00rootroot00000000000000python-opentelemetry-1.45.2/tests/opentelemetry-test-utils/src/opentelemetry/test/000077500000000000000000000000001527070070700306365ustar00rootroot00000000000000python-opentelemetry-1.45.2/tests/opentelemetry-test-utils/src/opentelemetry/test/__init__.py000066400000000000000000000021251527070070700327470ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # type: ignore from traceback import format_tb from unittest import TestCase class _AssertNotRaisesMixin: class _AssertNotRaises: def __init__(self, test_case): self._test_case = test_case def __enter__(self): return self def __exit__(self, type_, value, tb): # pylint: disable=invalid-name if value is not None and type_ in self._exception_types: self._test_case.fail("Unexpected exception was raised:\n{}".format("\n".join(format_tb(tb)))) return True def __call__(self, exception, *exceptions): # pylint: disable=attribute-defined-outside-init self._exception_types = (exception, *exceptions) return self def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # pylint: disable=invalid-name self.assertNotRaises = self._AssertNotRaises(self) class TestCase(_AssertNotRaisesMixin, TestCase): # pylint: disable=function-redefined pass _otlp_test_server.py000066400000000000000000000273261527070070700347050ustar00rootroot00000000000000python-opentelemetry-1.45.2/tests/opentelemetry-test-utils/src/opentelemetry/test# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations import gzip import time import zlib from dataclasses import dataclass from http.server import BaseHTTPRequestHandler, HTTPServer from queue import Empty, Queue from threading import Thread from typing import TYPE_CHECKING from urllib.parse import urlparse if TYPE_CHECKING: from opentelemetry.proto.common.v1.common_pb2 import InstrumentationScope from opentelemetry.proto.logs.v1.logs_pb2 import LogRecord from opentelemetry.proto.metrics.v1.metrics_pb2 import Metric from opentelemetry.proto.resource.v1.resource_pb2 import Resource from opentelemetry.proto.trace.v1.trace_pb2 import Span @dataclass class RecordedSpan: span: Span resource: Resource scope: InstrumentationScope @dataclass class RecordedMetric: metric: Metric resource: Resource scope: InstrumentationScope @dataclass class RecordedLogRecord: log_record: LogRecord resource: Resource scope: InstrumentationScope def _make_handler( spans_queue: Queue[RecordedSpan], metrics_queue: Queue[RecordedMetric], logs_queue: Queue[RecordedLogRecord], traces_path: str, metrics_path: str, logs_path: str, ) -> type[BaseHTTPRequestHandler]: # pylint: disable=import-outside-toplevel,no-name-in-module from opentelemetry.proto.collector.logs.v1.logs_service_pb2 import ( # noqa: PLC0415 ExportLogsServiceRequest, ExportLogsServiceResponse, ) from opentelemetry.proto.collector.metrics.v1.metrics_service_pb2 import ( # noqa: PLC0415 ExportMetricsServiceRequest, ExportMetricsServiceResponse, ) from opentelemetry.proto.collector.trace.v1.trace_service_pb2 import ( # noqa: PLC0415 ExportTraceServiceRequest, ExportTraceServiceResponse, ) class _Handler(BaseHTTPRequestHandler): def do_POST(self): # pylint: disable=invalid-name content_length = int(self.headers.get("Content-Length", 0)) body = self._decompress(self.rfile.read(content_length)) path = urlparse(self.path).path if path == traces_path: response_body = self._handle_traces(body) elif path == metrics_path: response_body = self._handle_metrics(body) elif path == logs_path: response_body = self._handle_logs(body) else: self.send_error(404) return self.send_response(200) self.send_header("Content-Type", "application/x-protobuf") self.send_header("Content-Length", str(len(response_body))) self.end_headers() self.wfile.write(response_body) def _decompress(self, body: bytes) -> bytes: encoding = self.headers.get("Content-Encoding", "") if encoding == "gzip": return gzip.decompress(body) if encoding == "deflate": return zlib.decompress(body) return body @staticmethod def _handle_traces(body: bytes) -> bytes: request = ExportTraceServiceRequest() request.ParseFromString(body) for rs in request.resource_spans: for ss in rs.scope_spans: for span in ss.spans: spans_queue.put(RecordedSpan(span=span, resource=rs.resource, scope=ss.scope)) return ExportTraceServiceResponse().SerializeToString() @staticmethod def _handle_metrics(body: bytes) -> bytes: request = ExportMetricsServiceRequest() request.ParseFromString(body) for rm in request.resource_metrics: for sm in rm.scope_metrics: for metric in sm.metrics: metrics_queue.put( RecordedMetric( metric=metric, resource=rm.resource, scope=sm.scope, ) ) return ExportMetricsServiceResponse().SerializeToString() @staticmethod def _handle_logs(body: bytes) -> bytes: request = ExportLogsServiceRequest() request.ParseFromString(body) for rl in request.resource_logs: for sl in rl.scope_logs: for log_record in sl.log_records: logs_queue.put( RecordedLogRecord( log_record=log_record, resource=rl.resource, scope=sl.scope, ) ) return ExportLogsServiceResponse().SerializeToString() def log_message(self, format, *args): # pylint: disable=redefined-builtin pass return _Handler class OtlpProtoTestServer: def __init__(self, host: str = "127.0.0.1", port: int = 0, base_path: str = "") -> None: try: # pylint: disable-next=import-outside-toplevel,unused-import import opentelemetry.proto # noqa: PLC0415, F401 except ImportError: raise ImportError( "opentelemetry-proto is required to use OtlpProtoTestServer. " "Install it with: pip install opentelemetry-proto" ) from None self._host = host self._port = port self._base_path = base_path.rstrip("/") self._spans_queue: Queue[RecordedSpan] = Queue() self._metrics_queue: Queue[RecordedMetric] = Queue() self._logs_queue: Queue[RecordedLogRecord] = Queue() self._server: HTTPServer | None = None self._thread: Thread | None = None def start(self) -> OtlpProtoTestServer: handler = _make_handler( self._spans_queue, self._metrics_queue, self._logs_queue, f"{self._base_path}/v1/traces", f"{self._base_path}/v1/metrics", f"{self._base_path}/v1/logs", ) self._server = HTTPServer((self._host, self._port), handler) self._port = self._server.server_address[1] self._thread = Thread( target=self._server.serve_forever, daemon=True, name="OtlpProtoTestServer", ) self._thread.start() return self def stop(self) -> None: if self._server is not None: self._server.shutdown() if self._thread is not None: self._thread.join() self._server.server_close() def __enter__(self) -> OtlpProtoTestServer: return self.start() def __exit__(self, *exc: object) -> None: self.stop() @property def port(self) -> int: return self._port @property def traces_endpoint(self) -> str: return f"http://{self._host}:{self._port}{self._base_path}/v1/traces" @property def metrics_endpoint(self) -> str: return f"http://{self._host}:{self._port}{self._base_path}/v1/metrics" @property def logs_endpoint(self) -> str: return f"http://{self._host}:{self._port}{self._base_path}/v1/logs" def get_span(self, timeout: float = 5.0) -> RecordedSpan: try: return self._spans_queue.get(timeout=timeout) except Empty: raise TimeoutError(f"No span received within {timeout}s") from None def get_spans(self, count: int = 1, timeout: float = 5.0) -> list[RecordedSpan]: deadline = time.monotonic() + timeout spans = [] for _ in range(count): remaining = deadline - time.monotonic() if remaining <= 0: raise TimeoutError(f"Timed out after receiving {len(spans)}/{count} spans") spans.append(self.get_span(timeout=remaining)) return spans def wait_for_span(self, *, name: str | None = None, timeout: float = 5.0) -> RecordedSpan: deadline = time.monotonic() + timeout while True: remaining = deadline - time.monotonic() if remaining <= 0: raise TimeoutError(f"No span with name={name!r} received within {timeout}s") try: recorded = self._spans_queue.get(timeout=remaining) except Empty: raise TimeoutError(f"No span with name={name!r} received within {timeout}s") from None if name is None or recorded.span.name == name: return recorded def drain_spans(self) -> list[RecordedSpan]: result = [] while True: try: result.append(self._spans_queue.get_nowait()) except Empty: return result def get_metric(self, timeout: float = 5.0) -> RecordedMetric: try: return self._metrics_queue.get(timeout=timeout) except Empty: raise TimeoutError(f"No metric received within {timeout}s") from None def get_metrics(self, count: int = 1, timeout: float = 5.0) -> list[RecordedMetric]: deadline = time.monotonic() + timeout metrics = [] for _ in range(count): remaining = deadline - time.monotonic() if remaining <= 0: raise TimeoutError(f"Timed out after receiving {len(metrics)}/{count} metrics") metrics.append(self.get_metric(timeout=remaining)) return metrics def wait_for_metric(self, *, name: str | None = None, timeout: float = 5.0) -> RecordedMetric: deadline = time.monotonic() + timeout while True: remaining = deadline - time.monotonic() if remaining <= 0: raise TimeoutError(f"No metric with name={name!r} received within {timeout}s") try: recorded = self._metrics_queue.get(timeout=remaining) except Empty: raise TimeoutError(f"No metric with name={name!r} received within {timeout}s") from None if name is None or recorded.metric.name == name: return recorded def drain_metrics(self) -> list[RecordedMetric]: result = [] while True: try: result.append(self._metrics_queue.get_nowait()) except Empty: return result def get_log_record(self, timeout: float = 5.0) -> RecordedLogRecord: try: return self._logs_queue.get(timeout=timeout) except Empty: raise TimeoutError(f"No log record received within {timeout}s") from None def get_log_records(self, count: int = 1, timeout: float = 5.0) -> list[RecordedLogRecord]: deadline = time.monotonic() + timeout log_records = [] for _ in range(count): remaining = deadline - time.monotonic() if remaining <= 0: raise TimeoutError(f"Timed out after receiving {len(log_records)}/{count} log records") log_records.append(self.get_log_record(timeout=remaining)) return log_records def wait_for_log_record(self, *, timeout: float = 5.0) -> RecordedLogRecord: try: return self._logs_queue.get(timeout=timeout) except Empty: raise TimeoutError(f"No log record received within {timeout}s") from None def drain_log_records(self) -> list[RecordedLogRecord]: result = [] while True: try: result.append(self._logs_queue.get_nowait()) except Empty: return result def clear(self) -> None: for queue in ( self._spans_queue, self._metrics_queue, self._logs_queue, ): while True: try: queue.get_nowait() except Empty: break python-opentelemetry-1.45.2/tests/opentelemetry-test-utils/src/opentelemetry/test/asgitestutil.py000066400000000000000000000051461527070070700337370ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import asyncio from unittest import IsolatedAsyncioTestCase from asgiref.testing import ApplicationCommunicator from opentelemetry.test.test_base import TestBase def setup_testing_defaults(scope): scope.update( { "client": ("127.0.0.1", 32767), "headers": [], "http_version": "1.0", "method": "GET", "path": "/", "query_string": b"", "scheme": "http", "server": ("127.0.0.1", 80), "type": "http", } ) class AsgiTestBase(TestBase): def setUp(self): super().setUp() self.scope = {} setup_testing_defaults(self.scope) self.communicator = None def tearDown(self): if self.communicator: asyncio.get_event_loop().run_until_complete(self.communicator.wait()) def seed_app(self, app): self.communicator = ApplicationCommunicator(app, self.scope) def send_input(self, message): asyncio.get_event_loop().run_until_complete(self.communicator.send_input(message)) def send_default_request(self): self.send_input({"type": "http.request", "body": b""}) def get_output(self): output = asyncio.get_event_loop().run_until_complete(self.communicator.receive_output(0)) return output def get_all_output(self): outputs = [] while True: try: outputs.append(self.get_output()) except asyncio.TimeoutError: break return outputs class AsyncAsgiTestBase(TestBase, IsolatedAsyncioTestCase): def setUp(self): super().setUp() self.scope = {} setup_testing_defaults(self.scope) self.communicator = None def tearDown(self): if self.communicator: asyncio.get_event_loop().run_until_complete(self.communicator.wait()) def seed_app(self, app): self.communicator = ApplicationCommunicator(app, self.scope) async def send_input(self, message): await self.communicator.send_input(message) async def send_default_request(self): await self.send_input({"type": "http.request", "body": b""}) async def get_output(self, timeout=0.01): return await self.communicator.receive_output(timeout) async def get_all_output(self, timeout=0.01): outputs = [] while True: try: outputs.append(await self.communicator.receive_output(timeout)) except asyncio.TimeoutError: break return outputs concurrency_test.py000066400000000000000000000042621527070070700345260ustar00rootroot00000000000000python-opentelemetry-1.45.2/tests/opentelemetry-test-utils/src/opentelemetry/test# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import sys import threading import unittest from collections.abc import Callable from functools import partial from typing import TypeVar from unittest.mock import Mock ReturnT = TypeVar("ReturnT") class MockFunc: """A thread safe mock function Use this as part of your mock if you want to count calls across multiple threads. """ def __init__(self) -> None: self.lock = threading.Lock() self.call_count = 0 self.mock = Mock() def __call__(self, *args, **kwargs): with self.lock: self.call_count += 1 return self.mock class ConcurrencyTestBase(unittest.TestCase): """Test base class/mixin for tests of concurrent code This test class calls ``sys.setswitchinterval(1e-12)`` to try to create more contention while running tests that use many threads. It also provides ``run_with_many_threads`` to run some test code in many threads concurrently. """ orig_switch_interval = sys.getswitchinterval() @classmethod def setUpClass(cls) -> None: super().setUpClass() # switch threads more often to increase chance of contention sys.setswitchinterval(1e-12) @classmethod def tearDownClass(cls) -> None: super().tearDownClass() sys.setswitchinterval(cls.orig_switch_interval) @staticmethod def run_with_many_threads( func_to_test: Callable[[], ReturnT], num_threads: int = 100, ) -> list[ReturnT]: """Util to run ``func_to_test`` in ``num_threads`` concurrently""" barrier = threading.Barrier(num_threads) results: list[ReturnT | None] = [None] * num_threads def thread_start(idx: int) -> None: nonlocal results # Get all threads here before releasing them to create contention barrier.wait() results[idx] = func_to_test() threads = [threading.Thread(target=partial(thread_start, i)) for i in range(num_threads)] for thread in threads: thread.start() for thread in threads: thread.join() return results # type: ignore python-opentelemetry-1.45.2/tests/opentelemetry-test-utils/src/opentelemetry/test/globals_test.py000066400000000000000000000057431527070070700337030ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import unittest from opentelemetry import trace as trace_api from opentelemetry._logs import _internal as logging_api from opentelemetry.metrics import _internal as metrics_api from opentelemetry.metrics._internal import _ProxyMeterProvider from opentelemetry.util._once import Once # pylint: disable=protected-access def reset_trace_globals() -> None: """WARNING: only use this for tests.""" trace_api._TRACER_PROVIDER_SET_ONCE = Once() trace_api._TRACER_PROVIDER = None trace_api._PROXY_TRACER_PROVIDER = trace_api.ProxyTracerProvider() # pylint: disable=protected-access def reset_metrics_globals() -> None: """WARNING: only use this for tests.""" metrics_api._METER_PROVIDER_SET_ONCE = Once() # type: ignore[attr-defined] metrics_api._METER_PROVIDER = None # type: ignore[attr-defined] metrics_api._PROXY_METER_PROVIDER = _ProxyMeterProvider() # type: ignore[attr-defined] # pylint: disable=protected-access def reset_logging_globals() -> None: """WARNING: only use this for tests.""" logging_api._LOGGER_PROVIDER_SET_ONCE = Once() # type: ignore[attr-defined] logging_api._LOGGER_PROVIDER = None # type: ignore[attr-defined] logging_api._PROXY_LOGGER_PROVIDER = logging_api.ProxyLoggerProvider() # type: ignore[attr-defined] # pylint: disable=protected-access def reset_event_globals() -> None: """Deprecated no-op kept for backward compatibility. The Events API has been removed; this function does nothing. """ class TraceGlobalsTest(unittest.TestCase): """Resets trace API globals in setUp/tearDown Use as a base class or mixin for your test that modifies trace API globals. """ def setUp(self) -> None: super().setUp() reset_trace_globals() def tearDown(self) -> None: super().tearDown() reset_trace_globals() class MetricsGlobalsTest(unittest.TestCase): """Resets metrics API globals in setUp/tearDown Use as a base class or mixin for your test that modifies metrics API globals. """ def setUp(self) -> None: super().setUp() reset_metrics_globals() def tearDown(self) -> None: super().tearDown() reset_metrics_globals() class LoggingGlobalsTest(unittest.TestCase): """Resets logging API globals in setUp/tearDown Use as a base class or mixin for your test that modifies logging API globals. """ def setUp(self) -> None: super().setUp() reset_logging_globals() def tearDown(self) -> None: super().tearDown() reset_logging_globals() class EventsGlobalsTest(unittest.TestCase): """Deprecated no-op kept for backward compatibility. The Events API has been removed; this class does nothing. """ def setUp(self) -> None: # pylint: disable=useless-parent-delegation super().setUp() def tearDown(self) -> None: # pylint: disable=useless-parent-delegation super().tearDown() python-opentelemetry-1.45.2/tests/opentelemetry-test-utils/src/opentelemetry/test/httptest.py000066400000000000000000000032451527070070700330730ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import re import unittest from http import HTTPStatus from http.server import BaseHTTPRequestHandler, HTTPServer from threading import Thread class HttpTestBase(unittest.TestCase): DEFAULT_RESPONSE = b"Hello!" class Handler(BaseHTTPRequestHandler): protocol_version = "HTTP/1.1" # Support keep-alive. timeout = 3 # Seconds STATUS_RE = re.compile(r"/status/(\d+)") def do_GET(self): # pylint:disable=invalid-name status_match = self.STATUS_RE.fullmatch(self.path) status = 200 if status_match: status = int(status_match.group(1)) if status == 200: body = HttpTestBase.DEFAULT_RESPONSE self.send_response(HTTPStatus.OK) self.send_header("Content-Length", str(len(body))) self.end_headers() self.wfile.write(body) else: self.send_error(status) @classmethod def create_server(cls): server_address = ("127.0.0.1", 0) # Only bind to localhost. return HTTPServer(server_address, cls.Handler) @classmethod def run_server(cls): httpd = cls.create_server() worker = Thread(target=httpd.serve_forever, daemon=True, name="Test server worker") worker.start() return worker, httpd @classmethod def setUpClass(cls): super().setUpClass() cls.server_thread, cls.server = cls.run_server() @classmethod def tearDownClass(cls): cls.server.shutdown() cls.server_thread.join() super().tearDownClass() python-opentelemetry-1.45.2/tests/opentelemetry-test-utils/src/opentelemetry/test/metrictestutil.py000066400000000000000000000060751527070070700343010ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from opentelemetry.attributes import BoundedAttributes from opentelemetry.sdk.metrics.export import ( AggregationTemporality, Gauge, Histogram, HistogramDataPoint, Metric, NumberDataPoint, Sum, ) from opentelemetry.util.types import Attributes def _generate_metric(name, data, attributes=None, description=None, unit=None) -> Metric: if description is None: description = "foo" if unit is None: unit = "s" return Metric( name=name, description=description, unit=unit, data=data, ) def _generate_sum( name, value, attributes=None, description=None, unit=None, is_monotonic=True, ) -> Metric: if attributes is None: attributes = BoundedAttributes(attributes={"a": 1, "b": True}) return _generate_metric( name, Sum( data_points=[ NumberDataPoint( attributes=attributes, start_time_unix_nano=1641946015139533244, time_unix_nano=1641946016139533244, value=value, ) ], aggregation_temporality=AggregationTemporality.CUMULATIVE, is_monotonic=is_monotonic, ), description=description, unit=unit, ) def _generate_gauge(name, value, attributes=None, description=None, unit=None) -> Metric: if attributes is None: attributes = BoundedAttributes(attributes={"a": 1, "b": True}) return _generate_metric( name, Gauge( data_points=[ NumberDataPoint( attributes=attributes, start_time_unix_nano=None, time_unix_nano=1641946016139533244, value=value, ) ], ), description=description, unit=unit, ) def _generate_unsupported_metric(name, attributes=None, description=None, unit=None) -> Metric: return _generate_metric( name, None, description=description, unit=unit, ) def _generate_histogram( name: str, attributes: Attributes = None, description: str | None = None, unit: str | None = None, ) -> Metric: if attributes is None: attributes = BoundedAttributes(attributes={"a": 1, "b": True}) return _generate_metric( name, Histogram( data_points=[ HistogramDataPoint( attributes=attributes, start_time_unix_nano=1641946016139533244, time_unix_nano=1641946016139533244, count=6, sum=579.0, bucket_counts=[1, 3, 2], explicit_bounds=[123.0, 456.0], min=1, max=457, ) ], aggregation_temporality=AggregationTemporality.CUMULATIVE, ), description=description, unit=unit, ) mock_test_classes.py000066400000000000000000000004001527070070700346300ustar00rootroot00000000000000python-opentelemetry-1.45.2/tests/opentelemetry-test-utils/src/opentelemetry/test# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 class IterEntryPoint: def __init__(self, name, class_type): self.name = name self.class_type = class_type def load(self): return self.class_type python-opentelemetry-1.45.2/tests/opentelemetry-test-utils/src/opentelemetry/test/mock_textmap.py000066400000000000000000000043651527070070700337130ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from opentelemetry import trace from opentelemetry.context import Context from opentelemetry.propagators.textmap import ( CarrierT, Getter, Setter, TextMapPropagator, default_getter, default_setter, ) class NOOPTextMapPropagator(TextMapPropagator): """A propagator that does not extract nor inject. This class is useful for catching edge cases assuming a SpanContext will always be present. """ def extract( self, carrier: CarrierT, context: Context | None = None, getter: Getter = default_getter, ) -> Context: return Context() def inject( self, carrier: CarrierT, context: Context | None = None, setter: Setter = default_setter, ) -> None: return None @property def fields(self): return set() class MockTextMapPropagator(TextMapPropagator): """Mock propagator for testing purposes.""" TRACE_ID_KEY = "mock-traceid" SPAN_ID_KEY = "mock-spanid" def extract( self, carrier: CarrierT, context: Context | None = None, getter: Getter = default_getter, ) -> Context: if context is None: context = Context() trace_id_list = getter.get(carrier, self.TRACE_ID_KEY) span_id_list = getter.get(carrier, self.SPAN_ID_KEY) if not trace_id_list or not span_id_list: return context return trace.set_span_in_context( trace.NonRecordingSpan( trace.SpanContext( trace_id=int(trace_id_list[0]), span_id=int(span_id_list[0]), is_remote=True, ) ), context, ) def inject( self, carrier: CarrierT, context: Context | None = None, setter: Setter = default_setter, ) -> None: span = trace.get_current_span(context) setter.set(carrier, self.TRACE_ID_KEY, str(span.get_span_context().trace_id)) setter.set(carrier, self.SPAN_ID_KEY, str(span.get_span_context().span_id)) @property def fields(self): return {self.TRACE_ID_KEY, self.SPAN_ID_KEY} python-opentelemetry-1.45.2/tests/opentelemetry-test-utils/src/opentelemetry/test/samplertestutil.py000066400000000000000000000044271527070070700344600ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations from collections.abc import Sequence from opentelemetry.context import Context from opentelemetry.sdk.trace.sampling import Decision, Sampler, SamplingResult from opentelemetry.trace import Link, SpanKind, TraceState, get_current_span from opentelemetry.util.types import Attributes, AttributeValue class CapturingSampler(Sampler): """Sampler that records what the SDK passes to ``should_sample``. Instrumentation tests use it to assert which span kind and attributes a sampler can see when a span starts, for example request headers that have to be available for sampling decisions. Every span is recorded and sampled unless ``required_attributes`` is given, in which case a span is dropped when any of these attributes is missing or has a different value. """ name: str | None kind: SpanKind | None attributes: dict[str, AttributeValue] def __init__(self, required_attributes: Attributes = None) -> None: self.required_attributes = dict(required_attributes or {}) self.name = None self.kind = None self.attributes = {} def should_sample( self, parent_context: Context | None, trace_id: int, name: str, kind: SpanKind | None = None, attributes: Attributes = None, links: Sequence[Link] | None = None, trace_state: TraceState | None = None, ) -> SamplingResult: self.name = name self.kind = kind # Snapshot now so attributes set after span creation cannot hide a regression. self.attributes = dict(attributes or {}) decision = Decision.RECORD_AND_SAMPLE if any( key not in self.attributes or self.attributes[key] != value for key, value in self.required_attributes.items() ): decision = Decision.DROP attributes = None parent_span_context = get_current_span(parent_context).get_span_context() return SamplingResult( decision, attributes, parent_span_context.trace_state if parent_span_context.is_valid else None, ) def get_description(self) -> str: return "CapturingSampler" python-opentelemetry-1.45.2/tests/opentelemetry-test-utils/src/opentelemetry/test/spantestutil.py000066400000000000000000000026001527070070700337450ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from functools import partial from opentelemetry import trace as trace_api from opentelemetry.sdk import trace as trace_sdk from opentelemetry.sdk.trace import Resource def new_tracer(span_limits=None, resource=None) -> trace_api.Tracer: provider_factory = trace_sdk.TracerProvider if resource is not None: provider_factory = partial(provider_factory, resource=resource) return provider_factory(span_limits=span_limits).get_tracer(__name__) def get_span_with_dropped_attributes_events_links(): attributes = {} for index in range(130): attributes[f"key{index}"] = [f"value{index}"] links = [] for index in range(129): links.append( trace_api.Link( trace_sdk._Span( name=f"span{index}", context=trace_api.INVALID_SPAN_CONTEXT, attributes=attributes, ).get_span_context(), attributes=attributes, ) ) tracer = new_tracer( span_limits=trace_sdk.SpanLimits(), resource=Resource(attributes=attributes), ) with tracer.start_as_current_span("span", links=links, attributes=attributes) as span: for index in range(131): span.add_event(f"event{index}", attributes=attributes) return span python-opentelemetry-1.45.2/tests/opentelemetry-test-utils/src/opentelemetry/test/test_base.py000066400000000000000000000214441527070070700331660ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import logging import unittest from collections.abc import Sequence from contextlib import contextmanager from opentelemetry import metrics as metrics_api from opentelemetry import trace as trace_api from opentelemetry.sdk.metrics import MeterProvider from opentelemetry.sdk.metrics._internal.aggregation import ( _DEFAULT_EXPLICIT_BUCKET_HISTOGRAM_AGGREGATION_BOUNDARIES, ) from opentelemetry.sdk.metrics._internal.point import Metric from opentelemetry.sdk.metrics.export import ( DataPointT, HistogramDataPoint, InMemoryMetricReader, MetricReader, NumberDataPoint, ) from opentelemetry.sdk.trace import TracerProvider, export from opentelemetry.sdk.trace.export.in_memory_span_exporter import ( InMemorySpanExporter, ) from opentelemetry.test.globals_test import ( reset_metrics_globals, reset_trace_globals, ) class TestBase(unittest.TestCase): # pylint: disable=C0103 def setUp(self): super().setUp() result = self.create_tracer_provider() self.tracer_provider, self.memory_exporter = result # This is done because set_tracer_provider cannot override the # current tracer provider. reset_trace_globals() trace_api.set_tracer_provider(self.tracer_provider) 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() metrics_api.set_meter_provider(self.meter_provider) def tearDown(self): super().tearDown() reset_trace_globals() reset_metrics_globals() def get_finished_spans(self): return FinishedTestSpans(self, self.memory_exporter.get_finished_spans()) def assertEqualSpanInstrumentationScope(self, span, module): self.assertEqual(span.instrumentation_scope.name, module.__name__) self.assertEqual(span.instrumentation_scope.version, module.__version__) def assertSpanHasAttributes(self, span, attributes): for key, val in attributes.items(): self.assertIn(key, span.attributes) self.assertEqual(val, span.attributes[key]) def sorted_spans(self, spans): # pylint: disable=R0201 """ Sorts spans by span creation time. Note: This method should not be used to sort spans in a deterministic way as the order depends on timing precision provided by the platform. """ return sorted( spans, key=lambda s: s._start_time, # pylint: disable=W0212 reverse=True, ) @staticmethod def create_tracer_provider(**kwargs): """Helper to create a configured tracer provider. Creates and configures a `TracerProvider` with a `SimpleSpanProcessor` and a `InMemorySpanExporter`. All the parameters passed are forwarded to the TracerProvider constructor. Returns: A list with the tracer provider in the first element and the in-memory span exporter in the second. """ tracer_provider = TracerProvider(**kwargs) memory_exporter = InMemorySpanExporter() span_processor = export.SimpleSpanProcessor(memory_exporter) tracer_provider.add_span_processor(span_processor) return tracer_provider, memory_exporter @staticmethod def create_meter_provider(**kwargs) -> tuple[MeterProvider, MetricReader]: """Helper to create a configured meter provider Creates a `MeterProvider` and an `InMemoryMetricReader`. Returns: A tuple with the meter provider in the first element and the in-memory metrics exporter in the second """ memory_reader = InMemoryMetricReader() metric_readers = kwargs.get("metric_readers", []) metric_readers.append(memory_reader) kwargs["metric_readers"] = metric_readers meter_provider = MeterProvider(**kwargs) return meter_provider, memory_reader @staticmethod @contextmanager def disable_logging(highest_level=logging.CRITICAL): logging.disable(highest_level) try: yield finally: logging.disable(logging.NOTSET) def get_sorted_metrics(self, scope: str | None = None): """Returns recorded metrics sorted by name. Args: scope: Optional scope name to filter metrics by. If unset, all metrics are returned. """ metrics_data = self.memory_metrics_reader.get_metrics_data() resource_metrics = metrics_data.resource_metrics if metrics_data else [] all_metrics = [] for metrics in resource_metrics: for scope_metrics in metrics.scope_metrics: if scope is not None and scope_metrics.scope.name != scope: continue all_metrics.extend(scope_metrics.metrics) return self.sorted_metrics(all_metrics) @staticmethod def sorted_metrics(metrics): """ Sorts metrics by metric name. """ return sorted( metrics, key=lambda m: m.name, ) def assert_metric_expected( self, metric: Metric, expected_data_points: Sequence[DataPointT], est_value_delta: float | None = 0, ): self.assertEqual(len(expected_data_points), len(metric.data.data_points)) for expected_data_point in expected_data_points: self.assert_data_point_expected(expected_data_point, metric.data.data_points, est_value_delta) # pylint: disable=unidiomatic-typecheck @staticmethod def is_data_points_equal( expected_data_point: DataPointT, data_point: DataPointT, est_value_delta: float | None = 0, ): if type(expected_data_point) != type(data_point) or not isinstance( # noqa: E721 expected_data_point, (HistogramDataPoint, NumberDataPoint) ): return False values_diff = None if isinstance(data_point, NumberDataPoint): values_diff = abs(expected_data_point.value - data_point.value) elif isinstance(data_point, HistogramDataPoint): values_diff = abs(expected_data_point.sum - data_point.sum) if expected_data_point.count != data_point.count or ( est_value_delta == 0 and (expected_data_point.min != data_point.min or expected_data_point.max != data_point.max) ): return False if expected_data_point.explicit_bounds != data_point.explicit_bounds: return False return values_diff <= est_value_delta and expected_data_point.attributes == dict(data_point.attributes) def assert_data_point_expected( self, expected_data_point: DataPointT, data_points: Sequence[DataPointT], est_value_delta: float | None = 0, ): is_data_point_exist = False for data_point in data_points: if self.is_data_points_equal(expected_data_point, data_point, est_value_delta): is_data_point_exist = True break self.assertTrue( is_data_point_exist, msg=f"Data point {expected_data_point} does not exist", ) @staticmethod def create_number_data_point(value, attributes): return NumberDataPoint( value=value, attributes=attributes, start_time_unix_nano=0, time_unix_nano=0, ) @staticmethod def create_histogram_data_point( sum_data_point, count, max_data_point, min_data_point, attributes, explicit_bounds=None, ): return HistogramDataPoint( count=count, sum=sum_data_point, min=min_data_point, max=max_data_point, attributes=attributes, start_time_unix_nano=0, time_unix_nano=0, bucket_counts=[], explicit_bounds=explicit_bounds if explicit_bounds is not None else _DEFAULT_EXPLICIT_BUCKET_HISTOGRAM_AGGREGATION_BOUNDARIES, ) class FinishedTestSpans(list): def __init__(self, test, spans): super().__init__(spans) self.test = test def by_name(self, name): for span in self: if span.name == name: return span self.test.fail(f"Did not find span with name {name}") return None def by_attr(self, key, value): for span in self: if span.attributes.get(key) == value: return span self.test.fail(f"Did not find span with attrs {key}={value}") return None python-opentelemetry-1.45.2/tests/opentelemetry-test-utils/src/opentelemetry/test/version/000077500000000000000000000000001527070070700323235ustar00rootroot00000000000000__init__.py000066400000000000000000000001441527070070700343540ustar00rootroot00000000000000python-opentelemetry-1.45.2/tests/opentelemetry-test-utils/src/opentelemetry/test/version# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 __version__ = "0.66b2" weaver_live_check.py000066400000000000000000000424351527070070700346060ustar00rootroot00000000000000python-opentelemetry-1.45.2/tests/opentelemetry-test-utils/src/opentelemetry/test# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import functools import json import logging import os import shutil import socket import subprocess import tempfile from collections import defaultdict from collections.abc import Sequence from itertools import chain from typing import Any from requests import Session, post from requests.adapters import HTTPAdapter from urllib3.util.retry import Retry from opentelemetry.semconv.schemas import Schemas logger = logging.getLogger(__name__) def _find_free_port() -> int: with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: sock.bind(("", 0)) return sock.getsockname()[1] def _extract_violations(report: dict) -> list: """Extract and deduplicate violations from the full report. Get all violations using python version of this jq filter: [ .. | objects | select(has("live_check_result")) | .live_check_result.all_advice[]? | select(.level == "violation") ] | group_by(.id, .message, .context, .signal_name, .signal_type) | map({ ..., count: length }) | sort_by(-.count, .message) """ raw: list[dict] = [] def _collect(obj: Any) -> list[dict]: if isinstance(obj, dict): result: list[dict] = [] lcr = obj.get("live_check_result") if isinstance(lcr, dict): advice_list = lcr.get("all_advice") if isinstance(advice_list, list): result.extend(a for a in advice_list if a.get("level") == "violation") for value in obj.values(): result.extend(_collect(value)) return result if isinstance(obj, list): return list(chain.from_iterable(_collect(item) for item in obj)) return [] raw = _collect(report) groups: dict[tuple, list] = defaultdict(list) for violation in raw: ctx = violation.get("context") key = ( violation.get("id"), violation.get("message"), json.dumps(ctx, sort_keys=True) if isinstance(ctx, (dict, list)) else ctx, violation.get("signal_name"), violation.get("signal_type"), ) groups[key].append(violation) violations = [ { "id": k[0], "message": k[1], "context": vs[0].get("context"), # preserve original dict, not JSON string "signal_name": k[3], "signal_type": k[4], "count": len(vs), } for k, vs in groups.items() ] violations.sort(key=lambda v: (-v["count"], v.get("message") or "")) return violations def _format_violations(violations: list) -> str: """Format violations list as human-readable text.""" lines = [] for violation in violations: signal = "" signal_type = violation.get("signal_type") signal_name = violation.get("signal_name") if signal_type and signal_name: signal = f" on {signal_type} '{signal_name}'" elif signal_type: signal = f" on {signal_type}" elif signal_name: signal = f" on '{signal_name}'" lines.append( f"- [{violation.get('id')}] {violation.get('message')} ({violation['count']} occurrence(s){signal})" ) return "\n".join(lines) class LiveCheckError(AssertionError): """Raised by :meth:`WeaverLiveCheck.end_and_check` when semconv violations are found. The full :class:`LiveCheckReport` is attached as :attr:`report` for structured inspection beyond the human-readable message:: with pytest.raises(LiveCheckError) as exc_info: weaver.end_and_check() err = exc_info.value assert any( v["id"] == "my_policy_check" and v["context"]["attribute_name"] == "my.attribute" for v in err.report.violations ) """ def __init__(self, message: str, report: "LiveCheckReport") -> None: super().__init__(message) self.report = report class LiveCheckReport: """The result of a weaver live-check run. Provides structured access to violations and the full raw JSON report. See https://github.com/open-telemetry/weaver/tree/main/crates/weaver_live_check#output for the full report structure. Example — asserting on metrics statistics:: report = weaver.end() seen = report["statistics"]["seen_registry_metrics"] assert seen.get("http.server.request.duration") == 1 Example — asserting on violations:: report = weaver.end() assert any( v["id"] == "my_policy_check" and v["context"]["attribute_name"] == "my.attribute" for v in report.violations ) """ def __init__(self, report: dict[str, Any]) -> None: self._report = report @functools.cached_property def violations(self) -> list[dict[str, Any]]: """Deduplicated list of semconv violations found in the report. Each item is a dict with keys: ``id``, ``message``, ``context`` (the raw context dict from weaver, e.g. ``{"attribute_name": "foo"}``), ``signal_name``, ``signal_type``, ``count``. """ return _extract_violations(self._report) def __getitem__(self, key: str) -> Any: return self._report[key] def get(self, key: str, default: Any = None) -> Any: return self._report.get(key, default) def __contains__(self, key: object) -> bool: return key in self._report def __repr__(self) -> str: num_violations = len(self.violations) return f"LiveCheckReport({num_violations} violation{'s' if num_violations != 1 else ''})" # NOTE: WeaverLiveCheck is experimental and its API is subject to change. class WeaverLiveCheck: """Runs ``weaver registry live-check`` as a subprocess and validates OTLP telemetry against OpenTelemetry semantic conventions. .. note:: This class is experimental and its API is subject to change without notice. Requires the ``weaver`` binary on PATH: https://github.com/open-telemetry/weaver/releases Typical use as a context manager:: def test_my_telemetry(self): with WeaverLiveCheck() as weaver: exporter = OTLPSpanExporter(endpoint=weaver.otlp_endpoint, insecure=True) # ... configure provider, emit telemetry ... provider.force_flush() # Signals weaver to stop, raises LiveCheckError listing violations # if any, or returns a LiveCheckReport on success. report = weaver.end_and_check() # __exit__ calls close(), which is idempotent if end_and_check() was already called Use :meth:`end` when you need the full :class:`LiveCheckReport` regardless of whether violations were found — for example, to assert that specific metrics were observed or to inspect violation fields directly:: with WeaverLiveCheck() as weaver: # ... configure provider, emit telemetry ... provider.force_flush() report = weaver.end() seen_metrics = report["statistics"]["seen_registry_metrics"] assert seen_metrics.get("http.server.request.duration") == 1 Lifecycle: - :meth:`start` — launches weaver and waits for it to become ready. - :attr:`otlp_endpoint` — gRPC OTLP endpoint to point exporters at. - :meth:`end` — signals weaver to stop and always returns a :class:`LiveCheckReport`. Never raises for semconv violations; use this when you want to write your own assertions. - :meth:`end_and_check` — signals weaver to stop and raises :class:`LiveCheckError` with a human-readable violation list and the full report attached if weaver exits non-zero. Returns a :class:`LiveCheckReport` on success. - :meth:`close` — stops weaver if not already stopped and terminates the process. Never raises for semconv violations. Idempotent; safe to call even if :meth:`end_and_check` or :meth:`end` was already called. """ def __init__( self, registry: str | None = None, schema_version: str | None = None, policies_dir: str | None = None, inactivity_timeout: int = 30, otlp_port: int = 0, admin_port: int = 0, extra_args: Sequence[str] | None = None, ): """Build the ``weaver registry live-check`` command. ``extra_args`` is appended verbatim to the weaver command after the managed flags (``--registry``, ``--otlp-grpc-port``, etc.) and lets callers pass additional weaver options — for example ``["--quiet"]`` or ``["--skip-policies"]`` — without subclassing. """ weaver_bin = shutil.which("weaver") if not weaver_bin: raise RuntimeError( "weaver binary not found on PATH. Install it from https://github.com/open-telemetry/weaver/releases" ) self._otlp_port = otlp_port or _find_free_port() self._admin_port = admin_port or _find_free_port() self._ready = False self._stopped = False self._process: subprocess.Popen[bytes] | None = None self._stdout_path: str | None = None self._stderr_path: str | None = None command = [ weaver_bin, "registry", "live-check", f"--inactivity-timeout={inactivity_timeout}", f"--otlp-grpc-port={self._otlp_port}", f"--admin-port={self._admin_port}", "--output=http", "--format=json", ] if policies_dir: command += ["--advice-policies", os.path.abspath(policies_dir)] if registry is None: if schema_version is None: schema_version = list(Schemas)[-1].value.rsplit("/", 1)[-1] registry = f"https://github.com/open-telemetry/semantic-conventions/archive/refs/tags/v{schema_version}.tar.gz[model]" elif os.path.isdir(registry): registry = os.path.abspath(registry) command += ["--registry", registry] if extra_args: command.extend(extra_args) self._command = command logger.debug("Weaver command: %s", command) def __enter__(self) -> "WeaverLiveCheck": return self.start() def __exit__(self, exc_type: Any, *_: object) -> None: if exc_type is not None: self._stopped = True self.close() def start(self) -> "WeaverLiveCheck": logger.debug("Starting WeaverLiveCheck process...") # Redirect weaver's stdout/stderr to tempfiles stdout_fd, self._stdout_path = tempfile.mkstemp(prefix="weaver-stdout-", suffix=".log") stderr_fd, self._stderr_path = tempfile.mkstemp(prefix="weaver-stderr-", suffix=".log") try: self._process = subprocess.Popen( # pylint: disable=consider-using-with self._command, stdout=stdout_fd, stderr=stderr_fd, ) finally: os.close(stdout_fd) os.close(stderr_fd) try: self._wait_for_ready() self._ready = True except Exception as exc: # pylint: disable=broad-except logs = self._read_weaver_logs() logger.error("WeaverLiveCheck did not start: %s, logs: %s", exc, logs) raise return self def _wait_for_ready(self) -> None: retry = Retry( total=10, backoff_factor=1, backoff_max=1, # Any non-2xx response from /health means weaver isn't ready yet. status_forcelist=list(range(300, 600)), raise_on_status=True, allowed_methods=["GET"], ) session = Session() session.mount("http://", HTTPAdapter(max_retries=retry)) try: session.get(f"http://localhost:{self._admin_port}/health", timeout=5) except Exception as exc: # pylint: disable=broad-except if self._process is not None and self._process.poll() is not None: raise RuntimeError( f"WeaverLiveCheck process exited unexpectedly (code {self._process.returncode})" ) from exc raise TimeoutError("WeaverLiveCheck did not become ready in time") from exc @property def otlp_endpoint(self) -> str: return f"http://localhost:{self._otlp_port}" def _do_stop(self, timeout: int) -> tuple["LiveCheckReport", int]: """POST /stop, wait for the process to exit, return (report, exit_code). Raises for infrastructure errors (HTTP failure, process communication). Never raises for semconv violations. """ if not self._ready: raise RuntimeError("WeaverLiveCheck process did not start successfully") try: response = post(f"http://localhost:{self._admin_port}/stop", timeout=5) response.raise_for_status() report = LiveCheckReport(response.json()) assert self._process is not None exit_code = self._process.wait(timeout=timeout) except Exception as exc: # pylint: disable=broad-except logs = self._read_weaver_logs() logger.error("Error communicating with weaver: %s, logs: %s", exc, logs) raise return report, exit_code def end(self, timeout: int = 30) -> "LiveCheckReport": """Signal weaver to stop and return the full :class:`LiveCheckReport`. Never raises for semconv violations — use this when you want to write your own assertions against :attr:`LiveCheckReport.violations` or the raw report data. Raises :exc:`RuntimeError` for infrastructure problems (weaver failed to start, HTTP communication error, etc.). See https://github.com/open-telemetry/weaver/tree/main/crates/weaver_live_check#output for the report structure. """ if self._stopped: logger.warning("end() called after weaver already stopped; returning empty report") return LiveCheckReport({}) self._stopped = True report, _ = self._do_stop(timeout) return report def end_and_check(self, timeout: int = 30) -> "LiveCheckReport": """Signal weaver to stop and assert no semconv violations were found. Returns the :class:`LiveCheckReport` when weaver exits successfully (exit code 0). Does **not** return if weaver exits with a non-zero status — raises :exc:`LiveCheckError` (a subclass of :exc:`AssertionError`) with a human-readable list of violations and the full :class:`LiveCheckReport` attached as :attr:`LiveCheckError.report`. Use :meth:`end` if you need the report regardless of violations. Raises :exc:`RuntimeError` for infrastructure problems (weaver failed to start, HTTP communication error, etc.). """ if self._stopped: logger.warning("end_and_check() called after weaver already stopped; returning empty report") return LiveCheckReport({}) self._stopped = True report, exit_code = self._do_stop(timeout) if exit_code == 0: # Success — no violations found, no errors communicating with weaver return report raise LiveCheckError( f"Semconv violations found:\n{_format_violations(report.violations)}", report, ) def _read_weaver_logs(self) -> str | None: if self._process is None: return None try: if self._process.poll() is None: self._process.kill() try: self._process.wait(timeout=5) except subprocess.TimeoutExpired: pass def _read(path: str | None) -> str: if path is None or not os.path.exists(path): return "" with open(path, "rb") as fp: return fp.read().decode(errors="replace") return f"{_read(self._stdout_path)}\n{_read(self._stderr_path)}" except Exception as exc: # pylint: disable=broad-except logger.error("Could not get weaver logs: %s", exc) return None def close(self) -> None: """Stop weaver and clean up the process. If weaver has not been stopped yet, sends the ``/stop`` signal and waits for the process to exit. Never raises for semconv violations. Idempotent — safe to call multiple times or after :meth:`end` / :meth:`end_and_check` has already been called. """ if not self._stopped: self._stopped = True if self._ready: try: self._do_stop(timeout=30) except Exception as exc: # pylint: disable=broad-except logger.debug("Error stopping weaver during close: %s", exc) if self._process and self._process.poll() is None: self._process.terminate() try: self._process.wait(timeout=5) except subprocess.TimeoutExpired: self._process.kill() for path in (self._stdout_path, self._stderr_path): if path is not None: try: os.unlink(path) except OSError: pass self._stdout_path = None self._stderr_path = None python-opentelemetry-1.45.2/tests/opentelemetry-test-utils/src/opentelemetry/test/wsgitestutil.py000066400000000000000000000025231527070070700337610ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 import io import wsgiref.util as wsgiref_util from opentelemetry import trace from opentelemetry.test.test_base import TestBase class WsgiTestBase(TestBase): def setUp(self): super().setUp() self.write_buffer = io.BytesIO() self.write = self.write_buffer.write self.environ = {} wsgiref_util.setup_testing_defaults(self.environ) self.status = None self.response_headers = None self.exc_info = None def start_response(self, status, response_headers, exc_info=None): self.status = status self.response_headers = response_headers self.exc_info = exc_info return self.write def assertTraceResponseHeaderMatchesSpan(self, headers, span): # pylint: disable=invalid-name self.assertIn("traceresponse", headers) self.assertEqual( headers["access-control-expose-headers"], "traceresponse", ) trace_id = trace.format_trace_id(span.get_span_context().trace_id) span_id = trace.format_span_id(span.get_span_context().span_id) trace_flags = span.get_span_context().trace_flags self.assertEqual( f"00-{trace_id}-{span_id}-{trace_flags:02x}", headers["traceresponse"], ) python-opentelemetry-1.45.2/tests/opentelemetry-test-utils/test-requirements.txt000066400000000000000000000016721527070070700304430ustar00rootroot00000000000000asgiref==3.7.2 iniconfig==2.0.0 packaging==24.0 pluggy==1.6.0 py-cpuinfo==9.0.0 pytest==9.0.3 tomli==2.0.1 typing_extensions==4.12.0 wrapt==1.16.0 -e opentelemetry-api -e opentelemetry-sdk -e opentelemetry-semantic-conventions -e tests/opentelemetry-test-utils # these are required for weaver integration tests, we're running that only on linux / CPython # because of lack of support for gRPC wheels on some platforms ./opentelemetry-proto ; platform_python_implementation != 'PyPy' ./exporter/opentelemetry-exporter-http-transport[urllib3] ; platform_python_implementation != 'PyPy' ./exporter/opentelemetry-exporter-otlp-common ; platform_python_implementation != 'PyPy' ./exporter/opentelemetry-exporter-otlp-proto-common ; platform_python_implementation != 'PyPy' ./exporter/opentelemetry-exporter-otlp-proto-grpc ; platform_python_implementation != 'PyPy' ./exporter/opentelemetry-exporter-otlp-proto-http ; platform_python_implementation != 'PyPy' python-opentelemetry-1.45.2/tests/opentelemetry-test-utils/tests/000077500000000000000000000000001527070070700253365ustar00rootroot00000000000000python-opentelemetry-1.45.2/tests/opentelemetry-test-utils/tests/__init__.py000066400000000000000000000001141527070070700274430ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 python-opentelemetry-1.45.2/tests/opentelemetry-test-utils/tests/test_base.py000066400000000000000000000004621527070070700276630ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from opentelemetry.test.test_base import TestBase class TestBaseTestCase(TestBase): def test_get_sorted_metrics_works_without_metrics(self): metrics = self.get_sorted_metrics() self.assertEqual(metrics, []) python-opentelemetry-1.45.2/tests/opentelemetry-test-utils/tests/test_otlp_test_server.py000066400000000000000000000302721527070070700323560ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations import unittest import unittest.mock import requests from opentelemetry._logs import SeverityNumber from opentelemetry.sdk._logs import LoggerProvider from opentelemetry.sdk._logs.export import SimpleLogRecordProcessor from opentelemetry.sdk.metrics import MeterProvider from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import SimpleSpanProcessor from opentelemetry.test._otlp_test_server import ( OtlpProtoTestServer, RecordedLogRecord, RecordedMetric, RecordedSpan, ) try: # pylint: disable-next=unused-import import opentelemetry.proto # noqa: F401 from opentelemetry.exporter.otlp.proto.http import Compression from opentelemetry.exporter.otlp.proto.http._log_exporter import ( OTLPLogExporter, ) from opentelemetry.exporter.otlp.proto.http.metric_exporter import ( OTLPMetricExporter, ) from opentelemetry.exporter.otlp.proto.http.trace_exporter import ( OTLPSpanExporter, ) _HAS_PROTO_DEPS = True except ImportError: _HAS_PROTO_DEPS = False @unittest.skipUnless( _HAS_PROTO_DEPS, "opentelemetry-exporter-otlp-proto-http or opentelemetry-proto are not installed", ) class TestOtlpProtoTestServer(unittest.TestCase): def setUp(self): self.server = OtlpProtoTestServer() self.server.start() def tearDown(self): self.server.stop() def _make_trace_provider(self, service: str = "trace-svc", **exporter_kwargs) -> TracerProvider: provider = TracerProvider(resource=Resource.create({"service.name": service})) provider.add_span_processor( SimpleSpanProcessor( OTLPSpanExporter( endpoint=self.server.traces_endpoint, timeout=1, **exporter_kwargs, ) ) ) return provider def _make_metrics_provider( self, service: str = "metrics-svc", **exporter_kwargs ) -> tuple[PeriodicExportingMetricReader, MeterProvider]: reader = PeriodicExportingMetricReader( OTLPMetricExporter( endpoint=self.server.metrics_endpoint, timeout=1, **exporter_kwargs, ), export_interval_millis=100, ) provider = MeterProvider( resource=Resource.create({"service.name": service}), metric_readers=[reader], ) return reader, provider def _make_log_provider(self, service: str = "log-svc", **exporter_kwargs) -> LoggerProvider: provider = LoggerProvider(resource=Resource.create({"service.name": service})) provider.add_log_record_processor( SimpleLogRecordProcessor( OTLPLogExporter( endpoint=self.server.logs_endpoint, timeout=1, **exporter_kwargs, ) ) ) return provider def test_span_export(self): provider = self._make_trace_provider() tracer = provider.get_tracer("trace-scope") with tracer.start_as_current_span("foo"): with tracer.start_as_current_span("bar"): pass tracer.start_span("baz").end() spans = self.server.get_spans(count=3, timeout=5.0) self.assertEqual({recorded.span.name for recorded in spans}, {"foo", "bar", "baz"}) for recorded in spans: self.assertIsInstance(recorded, RecordedSpan) self.assertEqual(recorded.scope.name, "trace-scope") svc = next(a.value.string_value for a in recorded.resource.attributes if a.key == "service.name") self.assertEqual(svc, "trace-svc") provider.shutdown() def test_wait_for_span(self): provider = self._make_trace_provider() tracer = provider.get_tracer("trace-scope") tracer.start_span("alpha").end() tracer.start_span("beta").end() found = self.server.wait_for_span(name="beta", timeout=5.0) self.assertEqual(found.span.name, "beta") with self.assertRaises(TimeoutError): self.server.get_span(timeout=0.1) provider.shutdown() def test_span_drain_and_clear(self): provider = self._make_trace_provider() tracer = provider.get_tracer("trace-scope") tracer.start_span("d1").end() tracer.start_span("d2").end() drained = self.server.drain_spans() self.assertEqual({r.span.name for r in drained}, {"d1", "d2"}) tracer.start_span("c1").end() tracer.start_span("c2").end() self.server.clear() with self.assertRaises(TimeoutError): self.server.get_span(timeout=0.1) provider.shutdown() def test_metric_export(self): reader, provider = self._make_metrics_provider() meter = provider.get_meter("metrics-scope") meter.create_counter("test.requests").add(5, {"env": "test"}) meter.create_histogram("test.latency").record(1.5, {"route": "/api"}) reader.force_flush(timeout_millis=3000) metrics = self.server.get_metrics(count=2, timeout=5.0) self.assertEqual({r.metric.name for r in metrics}, {"test.requests", "test.latency"}) for recorded in metrics: self.assertIsInstance(recorded, RecordedMetric) self.assertEqual(recorded.scope.name, "metrics-scope") svc = next(a.value.string_value for a in recorded.resource.attributes if a.key == "service.name") self.assertEqual(svc, "metrics-svc") provider.shutdown() def test_wait_for_metric(self): reader, provider = self._make_metrics_provider() meter = provider.get_meter("metrics-scope") meter.create_counter("other.counter").add(1) meter.create_counter("target.counter").add(1) reader.force_flush(timeout_millis=3000) found = self.server.wait_for_metric(name="target.counter", timeout=5.0) self.assertEqual(found.metric.name, "target.counter") provider.shutdown() def test_metric_drain_and_timeout(self): reader, provider = self._make_metrics_provider() meter = provider.get_meter("metrics-scope") meter.create_counter("drain.counter").add(1) reader.force_flush(timeout_millis=3000) self.server.get_metric(timeout=5.0) self.assertEqual(self.server.drain_metrics(), []) provider.shutdown() self.server.drain_metrics() with self.assertRaises(TimeoutError): self.server.get_metric(timeout=0.1) def test_log_export(self): provider = self._make_log_provider() logger = provider.get_logger("log-scope") logger.emit(body="first message", severity_number=SeverityNumber.WARN) logger.emit(body="second message", severity_number=SeverityNumber.ERROR) log_records = self.server.get_log_records(count=2, timeout=5.0) self.assertEqual( {r.log_record.body.string_value for r in log_records}, {"first message", "second message"}, ) for recorded in log_records: self.assertIsInstance(recorded, RecordedLogRecord) self.assertEqual(recorded.scope.name, "log-scope") svc = next(a.value.string_value for a in recorded.resource.attributes if a.key == "service.name") self.assertEqual(svc, "log-svc") provider.shutdown() def test_wait_for_log_record(self): provider = self._make_log_provider() logger = provider.get_logger("log-scope") logger.emit(body="wait-target", severity_number=SeverityNumber.INFO) found = self.server.wait_for_log_record(timeout=5.0) self.assertIsInstance(found, RecordedLogRecord) self.assertEqual(found.log_record.body.string_value, "wait-target") provider.shutdown() def test_log_drain_and_timeout(self): provider = self._make_log_provider() logger = provider.get_logger("log-scope") logger.emit(body="drain-me", severity_number=SeverityNumber.DEBUG) self.server.get_log_record(timeout=5.0) self.assertEqual(self.server.drain_log_records(), []) with self.assertRaises(TimeoutError): self.server.get_log_record(timeout=0.1) provider.shutdown() def test_compression(self): trace_provider = self._make_trace_provider(compression=Compression.Gzip) metrics_reader, metrics_provider = self._make_metrics_provider(compression=Compression.Gzip) log_provider = self._make_log_provider(compression=Compression.Gzip) trace_provider.get_tracer("s").start_span("gzip-span").end() self.assertEqual(self.server.get_span(timeout=5.0).span.name, "gzip-span") metrics_provider.get_meter("s").create_counter("gzip.counter").add(1) metrics_reader.force_flush(timeout_millis=3000) self.assertEqual(self.server.get_metric(timeout=5.0).metric.name, "gzip.counter") log_provider.get_logger("s").emit(body="gzip log", severity_number=SeverityNumber.INFO) self.assertEqual( self.server.get_log_record(timeout=5.0).log_record.body.string_value, "gzip log", ) trace_provider.shutdown() metrics_provider.shutdown() log_provider.shutdown() def test_endpoint_urls(self): port = self.server.port self.assertGreater(port, 0) self.assertEqual(self.server.traces_endpoint, f"http://127.0.0.1:{port}/v1/traces") self.assertEqual(self.server.metrics_endpoint, f"http://127.0.0.1:{port}/v1/metrics") self.assertEqual(self.server.logs_endpoint, f"http://127.0.0.1:{port}/v1/logs") def test_context_manager(self): with OtlpProtoTestServer() as srv: provider = TracerProvider() provider.add_span_processor(SimpleSpanProcessor(OTLPSpanExporter(endpoint=srv.traces_endpoint, timeout=1))) provider.get_tracer("s").start_span("ctx-span").end() self.assertEqual(srv.get_span(timeout=5.0).span.name, "ctx-span") provider.shutdown() def test_base_path(self): with OtlpProtoTestServer(base_path="/custom") as srv: self.assertTrue(srv.traces_endpoint.endswith("/custom/v1/traces")) provider = TracerProvider() provider.add_span_processor(SimpleSpanProcessor(OTLPSpanExporter(endpoint=srv.traces_endpoint, timeout=1))) provider.get_tracer("s").start_span("prefixed-span").end() self.assertEqual(srv.get_span(timeout=5.0).span.name, "prefixed-span") provider.shutdown() def test_signal_routing(self): trace_provider = self._make_trace_provider() metrics_reader, metrics_provider = self._make_metrics_provider() log_provider = self._make_log_provider() trace_provider.get_tracer("s").start_span("routed-span").end() metrics_provider.get_meter("s").create_counter("routed.counter").add(1) metrics_reader.force_flush(timeout_millis=3000) log_provider.get_logger("s").emit(body="routed log", severity_number=SeverityNumber.INFO) self.assertEqual(self.server.get_span(timeout=5.0).span.name, "routed-span") self.assertEqual(self.server.get_metric(timeout=5.0).metric.name, "routed.counter") self.assertEqual( self.server.get_log_record(timeout=5.0).log_record.body.string_value, "routed log", ) trace_provider.shutdown() metrics_provider.shutdown() log_provider.shutdown() def test_unknown_path_returns_404(self): resp = requests.post( f"http://127.0.0.1:{self.server.port}/unknown", data=b"", headers={"Content-Type": "application/x-protobuf"}, timeout=2, ) self.assertEqual(resp.status_code, 404) def test_missing_proto_raises_import_error(self): with ( unittest.mock.patch.dict("sys.modules", {"opentelemetry.proto": None}), self.assertRaises(ImportError) as cm, ): OtlpProtoTestServer() self.assertIn("opentelemetry-proto", str(cm.exception)) python-opentelemetry-1.45.2/tests/opentelemetry-test-utils/tests/test_samplertestutil.py000066400000000000000000000122231527070070700322100ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from unittest import TestCase from opentelemetry.sdk.trace.sampling import Decision from opentelemetry.test.samplertestutil import CapturingSampler from opentelemetry.test.test_base import TestBase from opentelemetry.trace import ( NonRecordingSpan, SpanContext, SpanKind, TraceFlags, TraceState, set_span_in_context, ) class TestCapturingSampler(TestCase): def test_nothing_captured_before_sampling(self): sampler = CapturingSampler() self.assertIsNone(sampler.name) self.assertIsNone(sampler.kind) self.assertEqual(sampler.attributes, {}) self.assertEqual(sampler.get_description(), "CapturingSampler") def test_captures_sampling_arguments(self): sampler = CapturingSampler() attributes = {"http.request.header.custom_test_header_1": ["value"]} result = sampler.should_sample(None, 0x1, "request", SpanKind.SERVER, attributes) self.assertEqual(result.decision, Decision.RECORD_AND_SAMPLE) self.assertEqual(dict(result.attributes), attributes) self.assertIsNone(result.trace_state) self.assertEqual(sampler.name, "request") self.assertEqual(sampler.kind, SpanKind.SERVER) self.assertEqual(sampler.attributes, attributes) def test_captured_attributes_are_a_snapshot(self): sampler = CapturingSampler() attributes = {"first": "value"} sampler.should_sample(None, 0x1, "request", SpanKind.SERVER, attributes) attributes["second"] = "value" self.assertEqual(sampler.attributes, {"first": "value"}) def test_captures_last_call(self): sampler = CapturingSampler() sampler.should_sample(None, 0x1, "first", SpanKind.SERVER, {"first": 1}) sampler.should_sample(None, 0x1, "second", SpanKind.INTERNAL) self.assertEqual(sampler.name, "second") self.assertEqual(sampler.kind, SpanKind.INTERNAL) self.assertEqual(sampler.attributes, {}) def test_required_attributes(self): sampler = CapturingSampler(required_attributes={"tenant": ["sample"]}) for attributes in ({"tenant": ["drop"]}, {"other": ["sample"]}, None): with self.subTest(attributes=attributes): result = sampler.should_sample(None, 0x1, "request", SpanKind.SERVER, attributes) self.assertEqual(result.decision, Decision.DROP) self.assertEqual(dict(result.attributes), {}) self.assertEqual(sampler.attributes, attributes or {}) attributes = {"tenant": ["sample"], "other": ["value"]} result = sampler.should_sample(None, 0x1, "request", SpanKind.SERVER, attributes) self.assertEqual(result.decision, Decision.RECORD_AND_SAMPLE) self.assertEqual(dict(result.attributes), attributes) self.assertEqual(sampler.attributes, attributes) def test_required_attribute_expected_to_be_none_must_be_present(self): sampler = CapturingSampler(required_attributes={"tenant": None}) result = sampler.should_sample(None, 0x1, "request", SpanKind.SERVER, {"other": "value"}) self.assertEqual(result.decision, Decision.DROP) self.assertEqual(dict(result.attributes), {}) result = sampler.should_sample(None, 0x1, "request", SpanKind.SERVER, {"tenant": None}) self.assertEqual(result.decision, Decision.RECORD_AND_SAMPLE) def test_keeps_parent_trace_state(self): sampler = CapturingSampler() trace_state = TraceState([("vendor", "value")]) parent = NonRecordingSpan( SpanContext( trace_id=0x1, span_id=0x2, is_remote=True, trace_flags=TraceFlags(TraceFlags.SAMPLED), trace_state=trace_state, ) ) result = sampler.should_sample(set_span_in_context(parent), 0x1, "request") self.assertEqual(result.trace_state, trace_state) def test_tracer_provider(self): sampler = CapturingSampler(required_attributes={"tenant": "sampled"}) tracer_provider, exporter = TestBase.create_tracer_provider(sampler=sampler) self.addCleanup(tracer_provider.shutdown) tracer = tracer_provider.get_tracer(__name__) with tracer.start_as_current_span("kept", kind=SpanKind.SERVER, attributes={"tenant": "sampled"}) as span: self.assertTrue(span.is_recording()) self.assertEqual(sampler.name, "kept") self.assertEqual(sampler.kind, SpanKind.SERVER) self.assertEqual(sampler.attributes, {"tenant": "sampled"}) with tracer.start_as_current_span("dropped", attributes={"tenant": "other"}) as span: self.assertFalse(span.is_recording()) self.assertEqual(sampler.name, "dropped") self.assertEqual(sampler.kind, SpanKind.INTERNAL) self.assertEqual(sampler.attributes, {"tenant": "other"}) spans = exporter.get_finished_spans() self.assertEqual([span.name for span in spans], ["kept"]) self.assertEqual(spans[0].kind, SpanKind.SERVER) self.assertEqual(dict(spans[0].attributes), {"tenant": "sampled"}) python-opentelemetry-1.45.2/tests/opentelemetry-test-utils/tests/test_utils.py000066400000000000000000000040571527070070700301150ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from opentelemetry.test import TestCase class TestAssertNotRaises(TestCase): def test_no_exception(self): try: with self.assertNotRaises(Exception): pass except Exception as error: # pylint: disable=broad-exception-caught self.fail( # pylint: disable=no-member f"Unexpected exception {error} was raised" ) def test_no_specified_exception_single(self): try: with self.assertNotRaises(KeyError): 1 / 0 # pylint: disable=pointless-statement except Exception as error: # pylint: disable=broad-exception-caught self.fail( # pylint: disable=no-member f"Unexpected exception {error} was raised" ) def test_no_specified_exception_multiple(self): try: with self.assertNotRaises(KeyError, IndexError): 1 / 0 # pylint: disable=pointless-statement except Exception as error: # pylint: disable=broad-exception-caught self.fail( # pylint: disable=no-member f"Unexpected exception {error} was raised" ) def test_exception(self): with self.assertRaises(AssertionError): with self.assertNotRaises(ZeroDivisionError): 1 / 0 # pylint: disable=pointless-statement def test_missing_exception(self): with self.assertRaises(AssertionError) as error: with self.assertNotRaises(ZeroDivisionError): def raise_zero_division_error(): raise ZeroDivisionError() raise_zero_division_error() error_lines = error.exception.args[0].split("\n") stripped_error_lines = [line.strip() for line in error_lines] self.assertIn("Unexpected exception was raised:", stripped_error_lines) self.assertIn("raise_zero_division_error()", stripped_error_lines) self.assertIn("raise ZeroDivisionError()", stripped_error_lines) python-opentelemetry-1.45.2/tests/opentelemetry-test-utils/tests/test_weaver_live_check.py000066400000000000000000000152771527070070700324300ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 """Live-check tests using Weaver to validate SDK telemetry against semconv. Requires the `weaver` binary on PATH: https://github.com/open-telemetry/weaver/releases """ import os import shutil import unittest from opentelemetry.sdk.resources import SERVICE_NAME, Resource from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchSpanProcessor from opentelemetry.test.weaver_live_check import ( LiveCheckError, LiveCheckReport, WeaverLiveCheck, ) try: from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import ( # pylint: disable=no-name-in-module OTLPSpanExporter, ) _HAS_GRPC = True except ImportError: _HAS_GRPC = False _TESTDATA_DIR = os.path.join(os.path.dirname(__file__), "testdata") _REGISTRY_DIR = os.path.join(_TESTDATA_DIR, "registry") def _make_provider(otlp_endpoint: str) -> TracerProvider: resource = Resource.create({SERVICE_NAME: "test-service"}) exporter = OTLPSpanExporter(endpoint=otlp_endpoint, insecure=True) provider = TracerProvider(resource=resource) provider.add_span_processor(BatchSpanProcessor(exporter)) return provider @unittest.skipUnless( _HAS_GRPC, "grpc exporter not installed", ) @unittest.skipUnless( shutil.which("weaver") is not None, "weaver binary not found on PATH — install from https://github.com/open-telemetry/weaver/releases", ) class TestSDKInitLiveCheck(unittest.TestCase): def test_end_and_check_no_violations(self): """end_and_check() returns a LiveCheckReport with no violations on conformant telemetry.""" with WeaverLiveCheck(registry=_REGISTRY_DIR) as weaver: provider = _make_provider(weaver.otlp_endpoint) with provider.get_tracer("test-tracer").start_as_current_span("test-span"): pass provider.force_flush() report = weaver.end_and_check() self.assertIsInstance(report, LiveCheckReport) self.assertEqual(report.violations, []) def test_end_and_check_raises_on_violations(self): """end_and_check() raises LiveCheckError with the report attached.""" with WeaverLiveCheck(registry=_REGISTRY_DIR, policies_dir=_TESTDATA_DIR) as weaver: provider = _make_provider(weaver.otlp_endpoint) with provider.get_tracer("test-tracer").start_as_current_span("test-span") as span: span.set_attribute("never.use.this.attribute", "bad value") provider.force_flush() with self.assertRaises(LiveCheckError) as cm: weaver.end_and_check() # Human-readable message lists the violation self.assertIn( "never.use.this.attribute is forbidden by this bogus policy", str(cm.exception), ) # Structured report is attached for programmatic inspection self.assertTrue( any( v["id"] == "test_check" and v["context"].get("attribute_name") == "never.use.this.attribute" for v in cm.exception.report.violations ) ) def test_end_no_violations(self): """end() returns a LiveCheckReport with no violations on conformant telemetry.""" with WeaverLiveCheck(registry=_REGISTRY_DIR) as weaver: provider = _make_provider(weaver.otlp_endpoint) with provider.get_tracer("test-tracer").start_as_current_span("test-span"): pass provider.force_flush() report = weaver.end() self.assertIsInstance(report, LiveCheckReport) self.assertEqual(report.violations, []) # LiveCheckReport supports dict-style access to the raw report data self.assertIn("statistics", report) self.assertIsNotNone(report.get("statistics")) self.assertIsNone(report.get("nonexistent")) def test_end_with_violations(self): """end() returns a LiveCheckReport with violations without raising.""" with WeaverLiveCheck(registry=_REGISTRY_DIR, policies_dir=_TESTDATA_DIR) as weaver: provider = _make_provider(weaver.otlp_endpoint) with provider.get_tracer("test-tracer").start_as_current_span("test-span") as span: span.set_attribute("never.use.this.attribute", "bad value") provider.force_flush() report = weaver.end() self.assertIsInstance(report, LiveCheckReport) # Check the violation id (maps to advice_type in the rego policy) self.assertTrue(any(v["id"] == "test_check" for v in report.violations)) # Check the structured context identifies the offending attribute by name self.assertTrue( any( isinstance(v["context"], dict) and v["context"].get("attribute_name") == "never.use.this.attribute" for v in report.violations ) ) def test_report_span_statistics(self): """The full report exposes span counts and individual span samples.""" with WeaverLiveCheck(registry=_REGISTRY_DIR) as weaver: provider = _make_provider(weaver.otlp_endpoint) with provider.get_tracer("test-tracer").start_as_current_span("test-span"): pass provider.force_flush() report = weaver.end() # Individual spans are accessible in report["samples"], each entry # with a "span" key containing the span data. span_samples = [s["span"] for s in report.get("samples", []) if "span" in s] self.assertTrue( any(s["name"] == "test-span" for s in span_samples), f"Expected 'test-span' in samples, got: {[s['name'] for s in span_samples]}", ) @unittest.skipUnless( _HAS_GRPC, "grpc exporter not installed", ) @unittest.skipUnless( shutil.which("weaver") is not None, "weaver binary not found on PATH — install from https://github.com/open-telemetry/weaver/releases", ) class TestOutputCapture(unittest.TestCase): """weaver's stdout/stderr is captured to tempfiles, not PIPE, so large diagnostic output can't deadlock the subprocess.""" def test_does_not_deadlock_on_large_diagnostic_output(self): """`--debug --debug` makes weaver dump trace logs that exceed the 64KB PIPE buffer; with tempfile capture the subprocess still exits cleanly.""" with WeaverLiveCheck(registry=_REGISTRY_DIR, extra_args=["--debug", "--debug"]) as weaver: provider = _make_provider(weaver.otlp_endpoint) with provider.get_tracer("test-tracer").start_as_current_span("test-span"): pass provider.force_flush() report = weaver.end() self.assertIsInstance(report, LiveCheckReport) python-opentelemetry-1.45.2/tests/opentelemetry-test-utils/tests/testdata/000077500000000000000000000000001527070070700271475ustar00rootroot00000000000000python-opentelemetry-1.45.2/tests/opentelemetry-test-utils/tests/testdata/registry/000077500000000000000000000000001527070070700310175ustar00rootroot00000000000000python-opentelemetry-1.45.2/tests/opentelemetry-test-utils/tests/testdata/registry/attributes.yaml000066400000000000000000000021431527070070700340710ustar00rootroot00000000000000# this is a test registry, used to save up time on weaver loading semconv from GH repo # and preventing flaky tests resulting from unpredictable network/performance in CI groups: - id: registry.test type: attribute_group brief: Minimal registry for WeaverLiveCheck self-tests. attributes: - id: service.instance.id type: string brief: The unique identifier of the service instance. stability: stable examples: ["my-k8s-pod-deployment-1"] - id: service.name type: string brief: The name of the service. stability: stable examples: ["test-service"] - id: telemetry.sdk.language type: string brief: The language of the telemetry SDK. stability: stable examples: ["python"] - id: telemetry.sdk.name type: string brief: The name of the telemetry SDK. stability: stable examples: ["opentelemetry"] - id: telemetry.sdk.version type: string brief: The version string of the telemetry SDK. stability: stable examples: ["1.0.0"] python-opentelemetry-1.45.2/tests/opentelemetry-test-utils/tests/testdata/test-policy.rego000066400000000000000000000006711527070070700323050ustar00rootroot00000000000000package live_check_advice import rego.v1 deny contains result if { input.sample.attribute.name == "never.use.this.attribute" result := { "type": "advice", "advice_type": "test_check", "advice_level": "violation", "context": { "attribute_name": input.sample.attribute.name, }, "message": "never.use.this.attribute is forbidden by this bogus policy", } } python-opentelemetry-1.45.2/tests/w3c_tracecontext_validation_server.py000066400000000000000000000037031527070070700266210ustar00rootroot00000000000000# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 """ This server is intended to be used with the W3C tracecontext validation Service. It implements the APIs needed to be exercised by the test bed. """ import json import flask import requests from opentelemetry import trace from opentelemetry.instrumentation.requests import RequestsInstrumentor from opentelemetry.instrumentation.wsgi import OpenTelemetryMiddleware from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import ( ConsoleSpanExporter, SimpleSpanProcessor, ) # FIXME This could likely be avoided by integrating this script into the # standard test running mechanisms. # Integrations are the glue that binds the OpenTelemetry API and the # frameworks and libraries that are used together, automatically creating # Spans and propagating context as appropriate. trace.set_tracer_provider(TracerProvider()) RequestsInstrumentor().instrument() # SpanExporter receives the spans and send them to the target location. span_processor = SimpleSpanProcessor(ConsoleSpanExporter()) trace.get_tracer_provider().add_span_processor(span_processor) app = flask.Flask(__name__) app.wsgi_app = OpenTelemetryMiddleware(app.wsgi_app) @app.route("/verify-tracecontext", methods=["POST"]) def verify_tracecontext(): """Upon reception of some payload, sends a request back to the designated url. This route is designed to be testable with the w3c tracecontext server / client test. """ for action in flask.request.json: requests.post( url=action["url"], data=json.dumps(action["arguments"]), headers={ "Accept": "application/json", "Content-Type": "application/json; charset=utf-8", }, timeout=5.0, ) return "hello" if __name__ == "__main__": try: app.run(debug=False) finally: span_processor.shutdown() python-opentelemetry-1.45.2/tox-uv.toml000066400000000000000000000001271527070070700201620ustar00rootroot00000000000000# https://docs.astral.sh/uv/reference/settings/#pip_no-sources [pip] no-sources = true python-opentelemetry-1.45.2/tox.ini000066400000000000000000000573171527070070700173530ustar00rootroot00000000000000[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. py3{10,11,12,13,14,14t}-test-opentelemetry-api pypy3-test-opentelemetry-api lint-opentelemetry-api py3{10,11,12,13,14,14t}-test-opentelemetry-proto-gen-{oldest,latest} pypy3-test-opentelemetry-proto-gen-{oldest,latest} lint-opentelemetry-proto-gen-latest py3{10,11,12,13,14,14t}-test-opentelemetry-protojson-gen-{oldest,latest} pypy3-test-opentelemetry-protojson-gen-{oldest,latest} lint-opentelemetry-protojson-gen-latest py3{10,11,12,13,14}-test-opentelemetry-codegen-json ; intentionally excluded from pypy3 lint-opentelemetry-codegen-json py3{10,11,12,13,14,14t}-test-opentelemetry-sdk pypy3-test-opentelemetry-sdk lint-opentelemetry-sdk benchmark-opentelemetry-sdk py3{10,11,12,13,14,14t}-test-opentelemetry-configuration pypy3-test-opentelemetry-configuration lint-opentelemetry-configuration py3{10,11,12,13,14,14t}-test-opentelemetry-semantic-conventions pypy3-test-opentelemetry-semantic-conventions lint-opentelemetry-semantic-conventions py3{10,11,12,13,14}-test-opentelemetry-getting-started lint-opentelemetry-getting-started py3{10,11,12,13,14,14t}-test-opentelemetry-opentracing-shim pypy3-test-opentelemetry-opentracing-shim lint-opentelemetry-opentracing-shim py3{10,11,12,13,14}-test-opentelemetry-opencensus-shim ; opencensus-shim intentionally excluded from pypy3 (grpcio install fails) lint-opentelemetry-opencensus-shim py3{10,11,12,13,14,14t}-test-opentelemetry-exporter-http-transport-{oldest,latest} ; exporter-http-transport intentionally excluded from pypy3 lint-opentelemetry-exporter-http-transport py3{10,11,12,13,14,14t}-test-opentelemetry-exporter-otlpcommon-{oldest,latest} ; exporter-otlpcommon intentionally excluded from pypy3 lint-opentelemetry-exporter-otlpcommon-latest py3{10,11,12,13,14}-test-opentelemetry-exporter-opencensus ; exporter-opencensus intentionally excluded from pypy3 lint-opentelemetry-exporter-opencensus py3{10,11,12,13,14,14t}-test-opentelemetry-exporter-otlp-proto-common pypy3-test-opentelemetry-exporter-otlp-proto-common lint-opentelemetry-exporter-otlp-proto-common py3{10,11,12,13,14,14t}-test-opentelemetry-exporter-otlp-json-common pypy3-test-opentelemetry-exporter-otlp-json-common lint-opentelemetry-exporter-otlp-json-common benchmark-opentelemetry-exporter-otlp-json-common ; opentelemetry-exporter-otlp py3{10,11,12,13,14}-test-opentelemetry-exporter-otlp-combined ; intentionally excluded from pypy3 lint-opentelemetry-exporter-otlp-combined py3{10,11,12,13,14,14t}-test-opentelemetry-exporter-otlp-json-file pypy3-test-opentelemetry-exporter-otlp-json-file lint-opentelemetry-exporter-otlp-json-file py3{10,11,12,13,14,14t}-test-opentelemetry-exporter-otlp-json-http pypy3-test-opentelemetry-exporter-otlp-json-http lint-opentelemetry-exporter-otlp-json-http py3{10,11,12,13,14}-test-opentelemetry-exporter-otlp-proto-grpc-{oldest,latest} ; intentionally excluded from pypy3 lint-opentelemetry-exporter-otlp-proto-grpc-latest benchmark-opentelemetry-exporter-otlp-proto-grpc-latest py3{10,11,12,13,14,14t}-test-opentelemetry-exporter-otlp-proto-http pypy3-test-opentelemetry-exporter-otlp-proto-http lint-opentelemetry-exporter-otlp-proto-http py3{10,11,12,13,14,14t}-test-opentelemetry-exporter-prometheus pypy3-test-opentelemetry-exporter-prometheus lint-opentelemetry-exporter-prometheus ; opentelemetry-exporter-zipkin py3{10,11,12,13,14,14t}-test-opentelemetry-exporter-zipkin-combined pypy3-test-opentelemetry-exporter-zipkin-combined lint-opentelemetry-exporter-zipkin-combined py3{10,11,12,13,14,14t}-test-opentelemetry-exporter-zipkin-proto-http pypy3-test-opentelemetry-exporter-zipkin-proto-http lint-opentelemetry-exporter-zipkin-proto-http py3{10,11,12,13,14,14t}-test-opentelemetry-exporter-zipkin-json pypy3-test-opentelemetry-exporter-zipkin-json lint-opentelemetry-exporter-zipkin-json py3{10,11,12,13,14,14t}-test-opentelemetry-propagator-b3 pypy3-test-opentelemetry-propagator-b3 lint-opentelemetry-propagator-b3 benchmark-opentelemetry-propagator-b3 py3{10,11,12,13,14,14t}-test-opentelemetry-propagator-jaeger pypy3-test-opentelemetry-propagator-jaeger lint-opentelemetry-propagator-jaeger ; skip py314t until grpc gets wheels for it or we stop using grpc exporter in weaver tests py3{10,11,12,13,14}-test-opentelemetry-test-utils pypy3-test-opentelemetry-test-utils lint-opentelemetry-test-utils test-scripts lint-license-header-check spellcheck tracecontext typecheck docs docker-tests-{otlpexporter,opencensus} public-symbols-check shellcheck generate-workflows precommit [testenv] deps = lint: -r dev-requirements.txt coverage: pytest coverage: pytest-cov api: -r {toxinidir}/opentelemetry-api/test-requirements.txt codegen-json: -r {toxinidir}/codegen/opentelemetry-codegen-json/test-requirements.txt sdk: -r {toxinidir}/opentelemetry-sdk/test-requirements.txt benchmark-opentelemetry-sdk: -r {toxinidir}/opentelemetry-sdk/benchmark-requirements.txt configuration: -r {toxinidir}/opentelemetry-configuration/test-requirements.txt semantic-conventions: -r {toxinidir}/opentelemetry-semantic-conventions/test-requirements.txt test-utils: -r {toxinidir}/tests/opentelemetry-test-utils/test-requirements.txt opentelemetry-proto-gen-oldest: -r {toxinidir}/opentelemetry-proto/test-requirements.oldest.txt opentelemetry-proto-gen-latest: -r {toxinidir}/opentelemetry-proto/test-requirements.latest.txt opentelemetry-protojson-gen-oldest: -r {toxinidir}/opentelemetry-proto-json/test-requirements.oldest.txt opentelemetry-protojson-gen-latest: -r {toxinidir}/opentelemetry-proto-json/test-requirements.latest.txt opentelemetry-exporter-http-transport-oldest: -r {toxinidir}/exporter/opentelemetry-exporter-http-transport/test-requirements.oldest.txt opentelemetry-exporter-http-transport-latest: -r {toxinidir}/exporter/opentelemetry-exporter-http-transport/test-requirements.latest.txt opentelemetry-exporter-otlpcommon-oldest: -r {toxinidir}/exporter/opentelemetry-exporter-otlp-common/test-requirements.oldest.txt opentelemetry-exporter-otlpcommon-latest: -r {toxinidir}/exporter/opentelemetry-exporter-otlp-common/test-requirements.latest.txt exporter-opencensus: -r {toxinidir}/exporter/opentelemetry-exporter-opencensus/test-requirements.txt exporter-otlp-proto-common: -r {toxinidir}/exporter/opentelemetry-exporter-otlp-proto-common/test-requirements.txt exporter-otlp-json-common: -r {toxinidir}/exporter/opentelemetry-exporter-otlp-json-common/test-requirements.txt benchmark-exporter-otlp-json-common: -r {toxinidir}/exporter/opentelemetry-exporter-otlp-json-common/benchmark-requirements.txt exporter-otlp-combined: -r {toxinidir}/exporter/opentelemetry-exporter-otlp/test-requirements.txt exporter-otlp-json-file: -r {toxinidir}/exporter/opentelemetry-exporter-otlp-json-file/test-requirements.txt exporter-otlp-json-http: -r {toxinidir}/exporter/opentelemetry-exporter-otlp-json-http/test-requirements.txt opentelemetry-exporter-otlp-proto-grpc-oldest: -r {toxinidir}/exporter/opentelemetry-exporter-otlp-proto-grpc/test-requirements.oldest.txt opentelemetry-exporter-otlp-proto-grpc-latest: -r {toxinidir}/exporter/opentelemetry-exporter-otlp-proto-grpc/test-requirements.latest.txt benchmark-exporter-otlp-proto-grpc: -r {toxinidir}/exporter/opentelemetry-exporter-otlp-proto-grpc/benchmark-requirements.txt opentelemetry-exporter-otlp-proto-http: -r {toxinidir}/exporter/opentelemetry-exporter-otlp-proto-http/test-requirements.txt opentracing-shim: -r {toxinidir}/shim/opentelemetry-opentracing-shim/test-requirements.txt opencensus-shim: -r {toxinidir}/shim/opentelemetry-opencensus-shim/test-requirements.txt exporter-prometheus: -r {toxinidir}/exporter/opentelemetry-exporter-prometheus/test-requirements.txt exporter-zipkin-combined: -r {toxinidir}/exporter/opentelemetry-exporter-zipkin/test-requirements.txt exporter-zipkin-proto-http: -r {toxinidir}/exporter/opentelemetry-exporter-zipkin-proto-http/test-requirements.txt exporter-zipkin-json: -r {toxinidir}/exporter/opentelemetry-exporter-zipkin-json/test-requirements.txt propagator-b3: -r {toxinidir}/propagator/opentelemetry-propagator-b3/test-requirements.txt benchmark-opentelemetry-propagator-b3: -r {toxinidir}/propagator/opentelemetry-propagator-b3/benchmark-requirements.txt propagator-jaeger: -r {toxinidir}/propagator/opentelemetry-propagator-jaeger/test-requirements.txt test-scripts: -r {toxinidir}/scripts/tests/test-requirements.txt getting-started: -r {toxinidir}/docs/getting_started/tests/requirements.txt getting-started: {env:CONTRIB_REPO_UTIL_HTTP} getting-started: {env:CONTRIB_REPO_INSTRUMENTATION} getting-started: {env:CONTRIB_REPO_INSTRUMENTATION_REQUESTS} getting-started: {env:CONTRIB_REPO_INSTRUMENTATION_WSGI} getting-started: {env:CONTRIB_REPO_INSTRUMENTATION_FLASK} allowlist_externals = sh setenv = ; override CONTRIB_REPO_SHA via env variable when testing other branches/commits than main ; i.e: CONTRIB_REPO_SHA=dde62cebffe519c35875af6d06fae053b3be65ec tox -e CONTRIB_REPO_SHA={env:CONTRIB_REPO_SHA:main} CONTRIB_REPO=git+https://github.com/open-telemetry/opentelemetry-python-contrib.git@{env:CONTRIB_REPO_SHA} CONTRIB_REPO_UTIL_HTTP={env:CONTRIB_REPO_UTIL_HTTP:{env:CONTRIB_REPO}\#egg=opentelemetry-util-http&subdirectory=util/opentelemetry-util-http} CONTRIB_REPO_INSTRUMENTATION={env:CONTRIB_REPO_INSTRUMENTATION:{env:CONTRIB_REPO}\#egg=opentelemetry-instrumentation&subdirectory=opentelemetry-instrumentation} CONTRIB_REPO_INSTRUMENTATION_REQUESTS={env:CONTRIB_REPO_INSTRUMENTATION_REQUESTS:{env:CONTRIB_REPO}\#egg=opentelemetry-instrumentation-requests&subdirectory=instrumentation/opentelemetry-instrumentation-requests} CONTRIB_REPO_INSTRUMENTATION_WSGI={env:CONTRIB_REPO_INSTRUMENTATION_WSGI:{env:CONTRIB_REPO}\#egg=opentelemetry-instrumentation-wsgi&subdirectory=instrumentation/opentelemetry-instrumentation-wsgi} CONTRIB_REPO_INSTRUMENTATION_FLASK={env:CONTRIB_REPO_INSTRUMENTATION_FLASK:{env:CONTRIB_REPO}\#egg=opentelemetry-instrumentation-flask&subdirectory=instrumentation/opentelemetry-instrumentation-flask} UV_CONFIG_FILE={toxinidir}/tox-uv.toml commands_pre = ; In order to get a healthy coverage report, ; we have to install packages in editable mode. coverage: python {toxinidir}/scripts/eachdist.py install --editable commands = test-opentelemetry-api: pytest {toxinidir}/opentelemetry-api/tests {posargs} lint-opentelemetry-api: pylint {toxinidir}/opentelemetry-api test-opentelemetry-codegen-json: pytest {toxinidir}/codegen/opentelemetry-codegen-json/tests {posargs} lint-opentelemetry-codegen-json: pylint {toxinidir}/codegen/opentelemetry-codegen-json test-opentelemetry-sdk: pytest {toxinidir}/opentelemetry-sdk/tests {posargs} lint-opentelemetry-sdk: pylint {toxinidir}/opentelemetry-sdk benchmark-opentelemetry-sdk: pytest {toxinidir}/opentelemetry-sdk/benchmarks --benchmark-json={toxinidir}/opentelemetry-sdk/sdk-benchmark.json {posargs} test-opentelemetry-configuration: pytest {toxinidir}/opentelemetry-configuration/tests {posargs} lint-opentelemetry-configuration: pylint {toxinidir}/opentelemetry-configuration test-opentelemetry-proto-gen: pytest {toxinidir}/opentelemetry-proto/tests {posargs} lint-opentelemetry-proto-gen: pylint {toxinidir}/opentelemetry-proto test-opentelemetry-protojson-gen: pytest {toxinidir}/opentelemetry-proto-json/tests {posargs} lint-opentelemetry-protojson-gen: pylint {toxinidir}/opentelemetry-proto-json test-opentelemetry-semantic-conventions: pytest {toxinidir}/opentelemetry-semantic-conventions/tests {posargs} lint-opentelemetry-semantic-conventions: pylint --rcfile {toxinidir}/opentelemetry-semantic-conventions/.pylintrc {toxinidir}/opentelemetry-semantic-conventions test-opentelemetry-getting-started: pytest {toxinidir}/docs/getting_started/tests {posargs} lint-opentelemetry-getting-started: pylint {toxinidir}/docs/getting_started test-opentelemetry-opentracing-shim: pytest {toxinidir}/shim/opentelemetry-opentracing-shim/tests {posargs} lint-opentelemetry-opentracing-shim: sh -c "cd shim && pylint --rcfile ../.pylintrc {toxinidir}/shim/opentelemetry-opentracing-shim" test-opentelemetry-opencensus-shim: pytest {toxinidir}/shim/opentelemetry-opencensus-shim/tests {posargs} lint-opentelemetry-opencensus-shim: sh -c "cd shim && pylint --rcfile ../.pylintrc {toxinidir}/shim/opentelemetry-opencensus-shim" test-opentelemetry-exporter-http-transport: pytest {toxinidir}/exporter/opentelemetry-exporter-http-transport/tests {posargs} lint-opentelemetry-exporter-http-transport: sh -c "cd exporter && pylint --rcfile ../.pylintrc {toxinidir}/exporter/opentelemetry-exporter-http-transport" test-opentelemetry-exporter-otlpcommon: pytest {toxinidir}/exporter/opentelemetry-exporter-otlp-common/tests {posargs} lint-opentelemetry-exporter-otlpcommon: sh -c "cd exporter && pylint --rcfile ../.pylintrc {toxinidir}/exporter/opentelemetry-exporter-otlp-common" test-opentelemetry-exporter-opencensus: pytest {toxinidir}/exporter/opentelemetry-exporter-opencensus/tests {posargs} lint-opentelemetry-exporter-opencensus: sh -c "cd exporter && pylint --rcfile ../.pylintrc {toxinidir}/exporter/opentelemetry-exporter-opencensus" test-opentelemetry-exporter-otlp-proto-common: pytest {toxinidir}/exporter/opentelemetry-exporter-otlp-proto-common/tests {posargs} lint-opentelemetry-exporter-otlp-proto-common: sh -c "cd exporter && pylint --prefer-stubs yes --rcfile ../.pylintrc {toxinidir}/exporter/opentelemetry-exporter-otlp-proto-common" test-opentelemetry-exporter-otlp-json-common: pytest {toxinidir}/exporter/opentelemetry-exporter-otlp-json-common/tests {posargs} lint-opentelemetry-exporter-otlp-json-common: sh -c "cd exporter && pylint --prefer-stubs yes --rcfile ../.pylintrc {toxinidir}/exporter/opentelemetry-exporter-otlp-json-common" benchmark-opentelemetry-exporter-otlp-json-common: pytest {toxinidir}/exporter/opentelemetry-exporter-otlp-json-common/benchmarks --benchmark-json=exporter-otlp-json-common-benchmark.json {posargs} test-opentelemetry-exporter-otlp-combined: pytest {toxinidir}/exporter/opentelemetry-exporter-otlp/tests {posargs} lint-opentelemetry-exporter-otlp-combined: sh -c "cd exporter && pylint --rcfile ../.pylintrc {toxinidir}/exporter/opentelemetry-exporter-otlp" test-opentelemetry-exporter-otlp-json-file: pytest {toxinidir}/exporter/opentelemetry-exporter-otlp-json-file/tests {posargs} lint-opentelemetry-exporter-otlp-json-file: sh -c "cd exporter && pylint --rcfile ../.pylintrc {toxinidir}/exporter/opentelemetry-exporter-otlp-json-file" test-opentelemetry-exporter-otlp-json-http: pytest {toxinidir}/exporter/opentelemetry-exporter-otlp-json-http/tests {posargs} lint-opentelemetry-exporter-otlp-json-http: sh -c "cd exporter && pylint --rcfile ../.pylintrc {toxinidir}/exporter/opentelemetry-exporter-otlp-json-http" test-opentelemetry-exporter-otlp-proto-grpc: pytest {toxinidir}/exporter/opentelemetry-exporter-otlp-proto-grpc/tests {posargs} lint-opentelemetry-exporter-otlp-proto-grpc: sh -c "cd exporter && pylint --prefer-stubs yes --rcfile ../.pylintrc {toxinidir}/exporter/opentelemetry-exporter-otlp-proto-grpc" benchmark-opentelemetry-exporter-otlp-proto-grpc: pytest {toxinidir}/exporter/opentelemetry-exporter-otlp-proto-grpc/benchmarks --benchmark-json=exporter-otlp-proto-grpc-benchmark.json {posargs} test-opentelemetry-exporter-otlp-proto-http: pytest {toxinidir}/exporter/opentelemetry-exporter-otlp-proto-http/tests {posargs} lint-opentelemetry-exporter-otlp-proto-http: sh -c "cd exporter && pylint --prefer-stubs yes --rcfile ../.pylintrc {toxinidir}/exporter/opentelemetry-exporter-otlp-proto-http" test-opentelemetry-exporter-prometheus: pytest {toxinidir}/exporter/opentelemetry-exporter-prometheus/tests {posargs} lint-opentelemetry-exporter-prometheus: sh -c "cd exporter && pylint --rcfile ../.pylintrc {toxinidir}/exporter/opentelemetry-exporter-prometheus" test-opentelemetry-exporter-zipkin-combined: pytest {toxinidir}/exporter/opentelemetry-exporter-zipkin/tests {posargs} lint-opentelemetry-exporter-zipkin-combined: sh -c "cd exporter && pylint --rcfile ../.pylintrc {toxinidir}/exporter/opentelemetry-exporter-zipkin" test-opentelemetry-exporter-zipkin-proto-http: pytest {toxinidir}/exporter/opentelemetry-exporter-zipkin-proto-http/tests {posargs} lint-opentelemetry-exporter-zipkin-proto-http: sh -c "cd exporter && pylint --rcfile ../.pylintrc {toxinidir}/exporter/opentelemetry-exporter-zipkin-proto-http" test-opentelemetry-exporter-zipkin-json: pytest {toxinidir}/exporter/opentelemetry-exporter-zipkin-json/tests {posargs} lint-opentelemetry-exporter-zipkin-json: sh -c "cd exporter && pylint --rcfile ../.pylintrc {toxinidir}/exporter/opentelemetry-exporter-zipkin-json" test-opentelemetry-propagator-b3: pytest {toxinidir}/propagator/opentelemetry-propagator-b3/tests {posargs} lint-opentelemetry-propagator-b3: sh -c "cd propagator && pylint --rcfile ../.pylintrc {toxinidir}/propagator/opentelemetry-propagator-b3" benchmark-opentelemetry-propagator-b3: pytest {toxinidir}/propagator/opentelemetry-propagator-b3/benchmarks --benchmark-json=propagator-b3-benchmark.json {posargs} test-opentelemetry-propagator-jaeger: pytest {toxinidir}/propagator/opentelemetry-propagator-jaeger/tests {posargs} lint-opentelemetry-propagator-jaeger: sh -c "cd propagator && pylint --rcfile ../.pylintrc {toxinidir}/propagator/opentelemetry-propagator-jaeger" test-opentelemetry-test-utils: pytest {toxinidir}/tests/opentelemetry-test-utils/tests {posargs} test-scripts: pytest {toxinidir}/scripts/tests {posargs} coverage: {toxinidir}/scripts/coverage.sh [testenv:spellcheck] recreate = True deps = -c {toxinidir}/dev-requirements.txt codespell commands = codespell [testenv:docs] recreate = True deps = -c {toxinidir}/dev-requirements.txt -r {toxinidir}/docs-requirements.txt setenv = ; We need this workaround to allow generating docs for exporters that have different protobuf versions as requirement. ; See https://github.com/open-telemetry/opentelemetry-python/pull/4206 ; We can remove the workaround when opentelemetry-exporter-zipkin-proto-http support protobuf > 5.26 PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION=python changedir = docs commands = sphinx-build -E -a -W -b html -T . _build/html [testenv:tracecontext] deps = -c dev-requirements.txt # needed for tracecontext aiohttp~=3.6 pytest # needed for example trace integration flask~=2.3 requests~=2.7 markupsafe~=2.1 -e {toxinidir}/opentelemetry-api -e {toxinidir}/opentelemetry-semantic-conventions -e {toxinidir}/opentelemetry-sdk {env:CONTRIB_REPO_UTIL_HTTP} {env:CONTRIB_REPO_INSTRUMENTATION} {env:CONTRIB_REPO_INSTRUMENTATION_REQUESTS} {env:CONTRIB_REPO_INSTRUMENTATION_WSGI} allowlist_externals = {toxinidir}/scripts/tracecontext-integration-test.sh commands = {toxinidir}/scripts/tracecontext-integration-test.sh [testenv:docker-tests-{otlpexporter,opencensus}] deps = -c dev-requirements.txt pytest prometheus_client ; core packages -e {toxinidir}/opentelemetry-api -e {toxinidir}/opentelemetry-semantic-conventions -e {toxinidir}/opentelemetry-sdk -e {toxinidir}/tests/opentelemetry-test-utils ; OTLP packages otlpexporter: inline-snapshot otlpexporter: -e {toxinidir}/opentelemetry-proto otlpexporter: -e {toxinidir}/exporter/opentelemetry-exporter-otlp-proto-common otlpexporter: -e {toxinidir}/exporter/opentelemetry-exporter-otlp-common otlpexporter: -e {toxinidir}/exporter/opentelemetry-exporter-http-transport otlpexporter: -e {toxinidir}/exporter/opentelemetry-exporter-otlp-proto-grpc otlpexporter: -e {toxinidir}/exporter/opentelemetry-exporter-otlp-proto-http otlpexporter: -e {toxinidir}/exporter/opentelemetry-exporter-otlp otlpexporter: -e {toxinidir}/opentelemetry-proto-json otlpexporter: -e {toxinidir}/exporter/opentelemetry-exporter-otlp-common otlpexporter: -e {toxinidir}/exporter/opentelemetry-exporter-otlp-json-common otlpexporter: -e {toxinidir}/exporter/opentelemetry-exporter-otlp-json-file otlpexporter: -e {toxinidir}/exporter/opentelemetry-exporter-prometheus otlpexporter: -e {toxinidir}/exporter/opentelemetry-exporter-otlp-json-http otlpexporter: -e {toxinidir}/exporter/opentelemetry-exporter-http-transport[requests] opencensus: -e {toxinidir}/exporter/opentelemetry-exporter-opencensus allowlist_externals = docker mkdir rm changedir = tests/opentelemetry-docker-tests/tests commands_pre = ; Remove stale .jsonl files so the collector doesn't replay old runs, then ; recreate the dirs (user-owned) before docker creates the bind-mount source. rm -rf otlpexporter/otlp-file-data mkdir -p otlpexporter/otlp-file-data/traces otlpexporter/otlp-file-data/metrics otlpexporter/otlp-file-data/logs docker compose version docker compose up -d commands = otlpexporter: pytest otlpexporter {posargs} opencensus: pytest opencensus {posargs} commands_post = docker compose down -v [testenv:lint-opentelemetry-test-utils] ; This environment must be defined explicitly because of how ; tox parses and matches environment names. deps = -r dev-requirements.txt -r {toxinidir}/tests/opentelemetry-test-utils/test-requirements.txt allowlist_externals = sh commands = sh -c "cd tests && pylint --rcfile ../.pylintrc {toxinidir}/tests/opentelemetry-test-utils" [testenv:lint-license-header-check] commands = python {toxinidir}/scripts/check_license_header.py [testenv:public-symbols-check] recreate = True deps = -c dev-requirements.txt GitPython griffe toml commands = ; griffe check before to fail fast if there are any issues python {toxinidir}/scripts/griffe_check.py python {toxinidir}/scripts/public_symbols_checker.py [testenv:generate-workflows] deps = tox Jinja2 commands = python {toxinidir}/.github/workflows/generate_workflows.py [testenv:generate-config-from-jsonschema] deps = datamodel-code-generator[ruff] allowlist_externals = datamodel-codegen python commands = datamodel-codegen [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:typecheck] deps = -c {toxinidir}/dev-requirements.txt pyright psutil types-PyYAML -e {toxinidir}/opentelemetry-api -e {toxinidir}/opentelemetry-semantic-conventions -e {toxinidir}/opentelemetry-sdk -e {toxinidir}/opentelemetry-configuration -e {toxinidir}/tests/opentelemetry-test-utils -e {toxinidir}/exporter/opentelemetry-exporter-http-transport -e {toxinidir}/exporter/opentelemetry-exporter-otlp-common -e {toxinidir}/exporter/opentelemetry-exporter-otlp-proto-common -e {toxinidir}/exporter/opentelemetry-exporter-otlp-json-common -e {toxinidir}/exporter/opentelemetry-exporter-otlp -e {toxinidir}/exporter/opentelemetry-exporter-otlp-common -e {toxinidir}/exporter/opentelemetry-exporter-otlp-json-file -e {toxinidir}/exporter/opentelemetry-exporter-otlp-json-http -e {toxinidir}/exporter/opentelemetry-exporter-otlp-proto-grpc -e {toxinidir}/exporter/opentelemetry-exporter-otlp-proto-http -e {toxinidir}/opentelemetry-proto -e {toxinidir}/opentelemetry-proto-json -e {toxinidir}/codegen/opentelemetry-codegen-json commands = pyright --version pyright [testenv:{precommit,ruff}] deps = -c {toxinidir}/dev-requirements.txt pre-commit commands = pre-commit run --color=always --all-files {posargs} python-opentelemetry-1.45.2/uv.lock000066400000000000000000012632671527070070700173500ustar00rootroot00000000000000version = 1 revision = 3 requires-python = ">=3.10" resolution-markers = [ "python_full_version >= '3.14'", "python_full_version == '3.13.*'", "python_full_version >= '3.11' and python_full_version < '3.13'", "python_full_version < '3.11'", ] [manifest] members = [ "opentelemetry-api", "opentelemetry-codegen-json", "opentelemetry-configuration", "opentelemetry-exporter-http-transport", "opentelemetry-exporter-otlp", "opentelemetry-exporter-otlp-common", "opentelemetry-exporter-otlp-json-common", "opentelemetry-exporter-otlp-json-file", "opentelemetry-exporter-otlp-json-http", "opentelemetry-exporter-otlp-proto-common", "opentelemetry-exporter-otlp-proto-grpc", "opentelemetry-exporter-otlp-proto-http", "opentelemetry-exporter-prometheus", "opentelemetry-exporter-zipkin-json", "opentelemetry-propagator-b3", "opentelemetry-propagator-jaeger", "opentelemetry-proto", "opentelemetry-proto-json", "opentelemetry-python", "opentelemetry-sdk", "opentelemetry-semantic-conventions", "opentelemetry-test-utils", ] [[package]] name = "annotated-types" version = "0.8.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/5f/56/a8120250d128bed162cd73c76d45f6ef9991f3e068f62a8ee060afa3104a/annotated_types-0.8.0.tar.gz", hash = "sha256:13b2beaad985e05e2d6407ee4c4f35590b11f8d693a258a561055cac8f64cab7", size = 15893, upload-time = "2026-07-23T20:16:13.995Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/99/91/8acff4f5e50511b911bbccb72b8628a49c68ce14148cd9f6431094859a90/annotated_types-0.8.0-py3-none-any.whl", hash = "sha256:f072f4d804ea359e4eaf198b1af7a8b0943881a87f31bb764f8bf219bb9419e0", size = 13427, upload-time = "2026-07-23T20:16:12.938Z" }, ] [[package]] name = "anyio" version = "4.14.2" 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/61/cc/a381afa6efea9f496eff839d4a6a1aed3bfafc7b3ab4b0d1b243a12573dd/anyio-4.14.2.tar.gz", hash = "sha256:cfa139f3ed1a23ee8f88a145ddb5ac7605b8bbfd8592baacd7ce3d8bb4313c7f", size = 260176, upload-time = "2026-07-12T20:29:07.082Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl", hash = "sha256:9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494", size = 125813, upload-time = "2026-07-12T20:29:05.763Z" }, ] [[package]] name = "argcomplete" version = "3.7.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/87/6f/5a73f04007ca950701765949209f068da628bd11f9c2da287278ce91e0ee/argcomplete-3.7.2.tar.gz", hash = "sha256:aad8b69a0b9969edb62db0d1752354c0d50717b10e0cbb00e2a958381b9fc6b9", size = 74473, upload-time = "2026-08-06T04:53:21.662Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/46/bd/551ee6af426af84ca33e02622be722925c196608e9127d731ef17c47f06e/argcomplete-3.7.2-py3-none-any.whl", hash = "sha256:6029205678bdd9c1c728a155f5f9ecf5812393f969eef58807641a2bc2aa5b19", size = 43294, upload-time = "2026-08-06T04:53:20.246Z" }, ] [[package]] name = "asgiref" version = "3.12.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/e6/26/3b59f2bdae5f640389becb1f673cded775287f5fc4f816309d9ca9a3f93d/asgiref-3.12.1.tar.gz", hash = "sha256:59dcb51c272ad209d59bed5708a64a333083e86017d7fcdd67498eeab7784340", size = 42378, upload-time = "2026-07-14T09:56:18.087Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/c0/1b/54f4ad77cd8a584fa70746c47df988e002cf1ee1eba43364d46f87803647/asgiref-3.12.1-py3-none-any.whl", hash = "sha256:fe386d1c2bff7259ea95929266d12a8cf9a8b5a1c2598402967d8792e7a7c094", size = 25478, upload-time = "2026-07-14T09:56:16.926Z" }, ] [[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 = "black" version = "26.5.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, { name = "mypy-extensions" }, { name = "packaging" }, { name = "pathspec" }, { name = "platformdirs" }, { name = "pytokens" }, { name = "tomli", marker = "python_full_version < '3.11'" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/c0/37/5628dd55bf2b34257fc7603f0fe97c40e3aaf24265f416a9c85c95ca1436/black-26.5.1.tar.gz", hash = "sha256:dd321f668053961824bcc1be1cc1df748b2d7e4fa28086b08331e577b0100a73", size = 679439, upload-time = "2026-05-18T16:53:36.107Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/be/84/b3f55026206a9e8820a91503308075ca48eadc515e436731ca01dbe043b3/black-26.5.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9942db8888e06943c5dde66ca0037dcff82a2a4ec1ad0ada9e0d2ee9d9823893", size = 1987719, upload-time = "2026-05-18T17:05:02.757Z" }, { url = "https://files.pythonhosted.org/packages/c6/34/7db312c5e5783d6e76cffd9d5ac8972a32badae4c6e3288dac0eed8d3bed/black-26.5.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:89c93167a74d3a75dfaa38a5c7cca015537d5820dd7f17d63267d674a61cae90", size = 1810083, upload-time = "2026-05-18T17:05:04.302Z" }, { url = "https://files.pythonhosted.org/packages/33/e2/e0101e73c2c8727634e2efcb35e2b34bd23ad70dfa673789f5773a591b21/black-26.5.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22f2cd76d069cc54c71f10360744ba8983fbb616903b4304a85b734915c8e1b4", size = 1860633, upload-time = "2026-05-18T17:05:06.391Z" }, { url = "https://files.pythonhosted.org/packages/b0/4c/e15c0c5b23cf3651035fe5addcce90e283af3548a3f91bb03d81b83106ab/black-26.5.1-cp310-cp310-win_amd64.whl", hash = "sha256:87ed5c6f450580a2f6790bc7cbfb016dfc73bc750249762268a3695361315eef", size = 1477886, upload-time = "2026-05-18T17:05:07.96Z" }, { url = "https://files.pythonhosted.org/packages/9f/3f/59d43ade98d2ce5c8dc34a4e46cbecd177e6d55d7d4092969c6003ccc655/black-26.5.1-cp310-cp310-win_arm64.whl", hash = "sha256:58b4bd92cf88aacf83d88479c8f9caee044b1ec55f2451a337354a7ea2590a22", size = 1277111, upload-time = "2026-05-18T17:05:09.473Z" }, { url = "https://files.pythonhosted.org/packages/4b/96/3c3e09f09f44a37aac36b178a279cd19aa7001bd796187a7b162a294c81f/black-26.5.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:96ae2c733b2aabdd9986e2c5df628ff3473676cd1c5faded1ff496cf6d74083c", size = 1970639, upload-time = "2026-05-18T17:05:11.461Z" }, { url = "https://files.pythonhosted.org/packages/83/ea/5ad117b9ee3ecd933c712bcbae610006e5b7cc9f41c526cd7ed3b6c4124c/black-26.5.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0e48b87e03bf109288e55cfceadcfa15ff5470aca2851a851950ed2926f450d7", size = 1792130, upload-time = "2026-05-18T17:05:12.983Z" }, { url = "https://files.pythonhosted.org/packages/06/3a/7c448bc623fcdfa96672531beb5a616ea5e64f6975955254d7731ffb0ad9/black-26.5.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5119fa92ae61f786e8c3662fd60aece1d0a2dd5cca5d0c79417a95e7a4272a59", size = 1846134, upload-time = "2026-05-18T17:05:14.506Z" }, { url = "https://files.pythonhosted.org/packages/a1/5b/0b39b3a5917f0657ac014ad2edb58c139553a478adfe7f817abf1622ff6e/black-26.5.1-cp311-cp311-win_amd64.whl", hash = "sha256:30d3c14661f2792e9142cce3eeeb1cbc175b3eb5f733be0c8eeb99651e52b0c3", size = 1478883, upload-time = "2026-05-18T17:05:16.542Z" }, { url = "https://files.pythonhosted.org/packages/4c/48/dc222692e0f95030db1bbfb6c857e76858bad09058221ea7aae815255327/black-26.5.1-cp311-cp311-win_arm64.whl", hash = "sha256:1ef92b76f7733f282fd096ea406200b5a286c42947412b0eaff3a74e3616cefe", size = 1277776, upload-time = "2026-05-18T17:05:18.029Z" }, { url = "https://files.pythonhosted.org/packages/24/99/7744b906703228264ef73bdd534df88ec1ef3de45c4e78f6d31b9e32d0c9/black-26.5.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4ad6fa01f941920f54f2bbb35f3df7673428a0ef98a0b0840c2eaef3b110efa8", size = 2012518, upload-time = "2026-05-18T17:05:20.108Z" }, { url = "https://files.pythonhosted.org/packages/b7/c0/c5a3b1636dfd09c42534f2b3cf33506814f6d3e066fb0879ffa16c1ae860/black-26.5.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3915f256e75a2d7cf88d8953d37f780455dc586cc72dee059c528fe77f581217", size = 1816016, upload-time = "2026-05-18T17:05:21.84Z" }, { url = "https://files.pythonhosted.org/packages/1f/0e/36044316b65ca471d3bb6d3703fd06fb50c6b727c3562f6a5a3153634f88/black-26.5.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d98d4137277c75dfb898ec8d846c4fd68ba1e9cf77f95e2865c203dc18f4c3d", size = 1884150, upload-time = "2026-05-18T17:05:23.546Z" }, { url = "https://files.pythonhosted.org/packages/b3/33/dafc5808c2af43672912111d7c3354af1615f7e2be3bed7a878461abbe4d/black-26.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:a1dca32d9f1784af512a13410ec204c6f7f0aa9797a111c42e1c03449821c264", size = 1486825, upload-time = "2026-05-18T17:05:25.004Z" }, { url = "https://files.pythonhosted.org/packages/82/14/b965ee6ad2a311f28bdbf692def3ee9848d2ae289dab28b27657fcee3e78/black-26.5.1-cp312-cp312-win_arm64.whl", hash = "sha256:1037d5ac7b7b310b2632ad867ec8d0e4c4819dcdb0b820f63135da746a24e418", size = 1288646, upload-time = "2026-05-18T17:05:26.477Z" }, { url = "https://files.pythonhosted.org/packages/3f/5c/c384363980e11e25ca6b93205949bb331fbf35f4e0dbec376dfa6326cec8/black-26.5.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2b36cf2ddf5566e205f6535f782a62194a184d33e175b64ae8c40b1737522be3", size = 2009020, upload-time = "2026-05-18T17:05:28.132Z" }, { url = "https://files.pythonhosted.org/packages/0b/df/9f31c5e0babbfed77d505fc5d120beb98b21b33feaeded3924ea941fe360/black-26.5.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1f7ea64ebfa01b50f693508fc39f875e264446d3b097088f84f203b9d09618a0", size = 1813335, upload-time = "2026-05-18T17:05:31.266Z" }, { url = "https://files.pythonhosted.org/packages/fb/24/8e7b9a2fa61b0afd82209efe937557d180a1fa055bd7f6161eb9defc3719/black-26.5.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecb3e624844c798144e9bd986954e0adc81d8911a1f30f375e1252fe26e8c294", size = 1881614, upload-time = "2026-05-18T17:05:32.718Z" }, { url = "https://files.pythonhosted.org/packages/49/ad/b4e0d9365ba8ac34f6bbab62a4b1b2dd5d618fac3fa1b8db968c844201b5/black-26.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:e1a26503279b6b310669fb0b219c39e4820b77e8189fe80f522bb511f247db0a", size = 1488925, upload-time = "2026-05-18T17:05:34.259Z" }, { url = "https://files.pythonhosted.org/packages/a1/4b/652b859bf5df88a751c30451b09338f7fd26a77d1271c666992f836b7711/black-26.5.1-cp313-cp313-win_arm64.whl", hash = "sha256:5c34b25da232ead53a6f335b76dbea124f4d152ad568b9080d6f944bc2b34b52", size = 1289883, upload-time = "2026-05-18T17:05:36.019Z" }, { url = "https://files.pythonhosted.org/packages/a6/16/a8da8eb208c51c7f4ce74609a45d0dcc6d8a2141e45e81ee5289d1bb0d59/black-26.5.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:e88976690a64b0af98312ca958415849cb42423423c5f2ee74af4b49a97a2168", size = 2004800, upload-time = "2026-05-18T17:05:38.182Z" }, { url = "https://files.pythonhosted.org/packages/11/8a/a479296a19e383b70a725882a6cf3d786540601ff03cabbaaf1cce864c5a/black-26.5.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:32d5ea7f6c8bdfa6e648326ebca1f02b0764e2a029edc6f8dce2627e19d468c3", size = 1815576, upload-time = "2026-05-18T17:05:40.309Z" }, { url = "https://files.pythonhosted.org/packages/81/6b/cfaf3d39f25132c156a068f6b805576c9103a84086019507c70e1911ee7d/black-26.5.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ea8d16dc41655aa113cd64665e7219446cd7e4ff2248d7178eaa905190c86b18", size = 1877927, upload-time = "2026-05-18T17:05:42.463Z" }, { url = "https://files.pythonhosted.org/packages/66/76/302e313964bcff7e28df329d39f84f5270095730d85ff0acc260610a0d82/black-26.5.1-cp314-cp314-win_amd64.whl", hash = "sha256:577f21094ea469ef92ec1adaf2c9441a226d2144d01a5be2fa823cecf6543e50", size = 1511860, upload-time = "2026-05-18T17:05:43.943Z" }, { url = "https://files.pythonhosted.org/packages/27/4e/a3827e35e0e567f9f9ee59e2a0ab979267dca98718f25547ca8c6733afd4/black-26.5.1-cp314-cp314-win_arm64.whl", hash = "sha256:ed1a20af114c301a0269bf01163d51dbef72737fd65f850001e7cbe7f3c7abae", size = 1316632, upload-time = "2026-05-18T17:05:45.521Z" }, { url = "https://files.pythonhosted.org/packages/94/51/f975cae76d44274cc2868dc9040ac5d58d464784610234455b4e7b19c6ef/black-26.5.1-py3-none-any.whl", hash = "sha256:4ed7f7da04046d2e488437170797d3b4a4ad83906683bcb7dfc68b673bbce5e2", size = 213693, upload-time = "2026-05-18T16:53:33.964Z" }, ] [[package]] name = "cachetools" version = "7.1.7" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/70/d2/47e8bc06fe2a06d3f5bdf20f1126ab66c4e99dc48d940e7ba873f7ac7131/cachetools-7.1.7.tar.gz", hash = "sha256:a3e2a00b14d8f8a6b70c1dae7b4685e7ad3bc965c5b42124a2d6ce895da6cf50", size = 40680, upload-time = "2026-08-01T21:20:40.434Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/e4/d8/767faeda872075724b95dd675466a645f1b92aadcdcf2d1429dcfd76c176/cachetools-7.1.7-py3-none-any.whl", hash = "sha256:ef98ef375ad188819ef2f9b3645e3987f4b8c5b7550e436ad998c2de78296df0", size = 16830, upload-time = "2026-08-01T21:20:38.977Z" }, ] [[package]] name = "certifi" version = "2026.7.22" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/a3/c2/24167ea9858356b47a87a50d39908bfdb72ceeefe0041586e704e5376b3a/certifi-2026.7.22.tar.gz", hash = "sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55", size = 138112, upload-time = "2026-07-22T03:35:12.644Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/0b/a7/71ac2cff56fec219ed242bb11b8efb69fcc4bec75db06fb7bfe35de520e6/certifi-2026.7.22-py3-none-any.whl", hash = "sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775", size = 136983, upload-time = "2026-07-22T03:35:11.276Z" }, ] [[package]] name = "cffi" version = "2.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pycparser", marker = "implementation_name != 'PyPy'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/9e/ef/008a1939e372c06329a3fce4279c02f328488f3526744906eeec3da7ad5f/cffi-2.1.1.tar.gz", hash = "sha256:dd31f52ea1086513bb9df30f8fcee9b8918323ae067a3d5b78bc826a000712be", size = 530807, upload-time = "2026-08-03T21:21:18.939Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/b6/d2/2cde336b375f55c76ca670f0be3978cc048e31e24f3b4d7ce8473150a388/cffi-2.1.1-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:baed1e86cc735622097354b9d1281406caf42ff42a886d29faa8e8d1630333be", size = 183779, upload-time = "2026-08-03T21:19:15.602Z" }, { url = "https://files.pythonhosted.org/packages/94/1a/4b2f7c92293ba05cbd4a9a1b28faaf0326272d9488e6354657571c48a7aa/cffi-2.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ca82be1a1d406ecfe1d25dc16cb33488e5a16bf4438c9fb590484ea29d92478b", size = 184178, upload-time = "2026-08-03T21:19:16.67Z" }, { url = "https://files.pythonhosted.org/packages/17/0b/ba385d8ccedf926c3cd06e8e2f327027da5afe5f0eb30f1f7bc43ac55125/cffi-2.1.1-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:42e2f76b9455f5a9a844f770bf3e200ed3da0e15f5df3db9c31fe80b04b3d004", size = 211037, upload-time = "2026-08-03T21:19:17.705Z" }, { url = "https://files.pythonhosted.org/packages/a3/b9/0f2e58b2cefa33255bff36935d42b13180fe559bba82596540eb404bde7d/cffi-2.1.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5a59cc1c4442bc3d5c703bf720b51138d0bfc173618807c9ee2490a7541dd3d9", size = 218652, upload-time = "2026-08-03T21:19:18.735Z" }, { url = "https://files.pythonhosted.org/packages/37/15/180e0dab27b9312c7479003d14c9e547634b7dcb934e2cc4650e1b131a7a/cffi-2.1.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:9f8d177621de5cb38ee3e731eda45d421db093ec0739f46a5594babda7987a98", size = 205422, upload-time = "2026-08-03T21:19:19.96Z" }, { url = "https://files.pythonhosted.org/packages/18/d4/03026f0c850cbbaa9030750490225b4a7f4d524ea4df72c3cc740a90f4ef/cffi-2.1.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:75f80557d1389eddbd0de2681f6a390a0c5338c31ddaa821381c203fc3fd50d9", size = 205444, upload-time = "2026-08-03T21:19:21.246Z" }, { url = "https://files.pythonhosted.org/packages/75/77/60bebf6f818bec84210ac5b6979ce4eeadce6fbbaabc9c7ab23e506d1ce5/cffi-2.1.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:194cffa889098ced9976c3fc6340305e43f6303657d298da55366907c05c22d6", size = 218742, upload-time = "2026-08-03T21:19:22.523Z" }, { url = "https://files.pythonhosted.org/packages/b0/ae/679bf47e73fd77b352171727f07de559a003f14de5d02b904a6ec1fa73ca/cffi-2.1.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5bb4e7ea95dcd6a014a6fef62e62467d67d8e582326443f3d68e71d6320a9fcf", size = 221054, upload-time = "2026-08-03T21:19:23.694Z" }, { url = "https://files.pythonhosted.org/packages/09/b8/eefc0e06913b70aa153bf74c946094a18f58fd4aff11b7f372bfdfdca050/cffi-2.1.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3d22a20b1fb1632cc72c22f95f7b0d2961c3e1c235f245ba4c606c4771035659", size = 213489, upload-time = "2026-08-03T21:19:24.922Z" }, { url = "https://files.pythonhosted.org/packages/6f/13/4e56852824a03cdf68523a35686f1c28eacd4bd30a7b0a78e682e6e6e1d3/cffi-2.1.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:1dea0e4d7d4f11f619fe8c1d76caf49e24405b4b5743c0e3be16a500ecd930c9", size = 220241, upload-time = "2026-08-03T21:19:26.214Z" }, { url = "https://files.pythonhosted.org/packages/99/7f/040f9e163e4acac3ee3d85b02d00b2576e7ca980d8785f0a3a5f1a9bf7f5/cffi-2.1.1-cp310-cp310-win32.whl", hash = "sha256:7ce713ace7c0e4520535b42b77eaa742c16dab813978064913e5a3cf82973b41", size = 174578, upload-time = "2026-08-03T21:19:27.338Z" }, { url = "https://files.pythonhosted.org/packages/ba/0b/644a2ec1a4eaba49c2939410bb1eb1d25b09d6d0582f5d2f95c537043725/cffi-2.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:a48d62ab9d6f4f98c983223a547af44be6ca3691074c31cecced6facd3ba2dc1", size = 185082, upload-time = "2026-08-03T21:19:28.409Z" }, { url = "https://files.pythonhosted.org/packages/70/d2/16d99a0c4948febc0ebd133a13b2f688ff7f8cb04da971e1128872ce0c03/cffi-2.1.1-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:c8d2c9fd1f2d16f780d15127abb050d13d1a76c03a4bd87d7e4980e45e511e12", size = 183838, upload-time = "2026-08-03T21:19:29.637Z" }, { url = "https://files.pythonhosted.org/packages/cd/95/31b535a9f0220ae9f357de4a08d57ce89cb417653c2fd9f075f50822a388/cffi-2.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:398aff33cee2767e3e781d2554c54bd0dff386bb437581e0d8011fde1a942ec1", size = 184168, upload-time = "2026-08-03T21:19:30.764Z" }, { url = "https://files.pythonhosted.org/packages/ad/5a/4707a0dc1f203f5dde5a907b0d4e3c25d71120241048bd5bc6f1bb9d4e71/cffi-2.1.1-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:154852545011f779917b11c78db2358d095da62a9a172b78ad0a583ee5adc0d0", size = 211805, upload-time = "2026-08-03T21:19:31.867Z" }, { url = "https://files.pythonhosted.org/packages/ad/66/c19feabb28485b6e0bbaaafa90837a1ef5d302e90f2178bd33f17a49879b/cffi-2.1.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3311ed60d36f83378794e1009ac6258bafbf81f7888b4caa7b35a521e3f95813", size = 218716, upload-time = "2026-08-03T21:19:32.896Z" }, { url = "https://files.pythonhosted.org/packages/a7/92/500760486c8baab49a7a8a58ba7fc3355ec3974b454b8a09e528efde9e1d/cffi-2.1.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6e192623c49c94421616a5778fba35cf0d5a8d000650c1967ef4448ee5cdd990", size = 205569, upload-time = "2026-08-03T21:19:34.142Z" }, { url = "https://files.pythonhosted.org/packages/a5/a7/a67c733254d6e7373f7822f8082d8d6beade791e0cf12a7611f376fa61c7/cffi-2.1.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a6e721d4b0e45d5b65e87534470e67b18dcd092c83f68fba09f152b9cbc061af", size = 204907, upload-time = "2026-08-03T21:19:35.174Z" }, { url = "https://files.pythonhosted.org/packages/f7/a4/4399daaf8f7dfee9d7c3327fdb0426ee041cc63edc358b93911ceb2bfc7a/cffi-2.1.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:34e261f78cb6ceaaa36f42f2613f4380d94d9c759a9c73c769ee6e0247364632", size = 217807, upload-time = "2026-08-03T21:19:36.286Z" }, { url = "https://files.pythonhosted.org/packages/28/f7/dabe6da2466ecbd82dc62e7342dc6b1065dad990c06f00f0ede9ebf2a0ed/cffi-2.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7225e4514edb64eb6740324353e0da0711954fd8d7da4576755b1c6e09b697cd", size = 221252, upload-time = "2026-08-03T21:19:37.416Z" }, { url = "https://files.pythonhosted.org/packages/ce/87/616202d8e51342c07d2534c510111c4cc37201775ce8f60802c9335d1edd/cffi-2.1.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:df913725b79db7bcf03448f36b7bf8815363417d5b58deecf9305e3e30f0f21a", size = 214214, upload-time = "2026-08-03T21:19:38.507Z" }, { url = "https://files.pythonhosted.org/packages/b4/c6/ab025d75d2c26c19b087c0124e75ee31cb65032f4fe345d356d8c507ab97/cffi-2.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f5cfbc5fe74540d335175b656c725d74d90e3730c626d92575eea35029d9afaa", size = 219408, upload-time = "2026-08-03T21:19:39.809Z" }, { url = "https://files.pythonhosted.org/packages/db/e2/7e8109f65445bdc673a7b54f02c677de462db75674220fd1335efc8eb598/cffi-2.1.1-cp311-cp311-win32.whl", hash = "sha256:f8ec5e643a9a937f64e1999eb9f75d072263751912dc5cd06d3c85f8f44be7c3", size = 174470, upload-time = "2026-08-03T21:19:41.246Z" }, { url = "https://files.pythonhosted.org/packages/73/c0/77ba02423c2f7d7091143c45cd49e0e6575c4c1967394bb542bd923a9b74/cffi-2.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:42f6930c31dc7f50732c9ae793c2786c7b6b044195967bbdde40bb9be81c4cc0", size = 185096, upload-time = "2026-08-03T21:19:42.615Z" }, { url = "https://files.pythonhosted.org/packages/7c/47/9f1f85f9672ceda4984dc6c4f8824e8558992a2972c3d3c81fb8eb28d4ba/cffi-2.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:c7659f22557c5a0bc4855cd635f55edec690cc008a40768527762cb9fb263455", size = 179941, upload-time = "2026-08-03T21:19:43.747Z" }, { url = "https://files.pythonhosted.org/packages/10/69/43965eccfdead3b9220015fd1320e117be8c6ed01a62ffab76eeb752f5d5/cffi-2.1.1-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:c8c69575568085ba0b1b10c0249d779a214aea6f6522e949a0fc9fb0fcb449d0", size = 184821, upload-time = "2026-08-03T21:19:44.887Z" }, { url = "https://files.pythonhosted.org/packages/54/7d/16e5a096677b5e313ca80cd5e5170efa3ea44624a82bb111925522da64b1/cffi-2.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f81b3b8f3d4e343550fa4baa0e479bba9f2d29ce9c2e9b51d1ce1718d7442fcf", size = 184719, upload-time = "2026-08-03T21:19:46.129Z" }, { url = "https://files.pythonhosted.org/packages/56/e6/8941622732edec876dd17d0453dce07317ae96db34f2ec1436c9d3785986/cffi-2.1.1-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:811bd1e21d32de12efca32393a0ab3f5133b54fce9bd44b8bd77ab07da14bf6a", size = 214799, upload-time = "2026-08-03T21:19:47.218Z" }, { url = "https://files.pythonhosted.org/packages/44/de/f98430906df1545ffde0d543dd124a7a439bc2cd32b36b9c53f805df7333/cffi-2.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:68e62fe11f30d5ca8289242866f0a5291402d8529ca2178ab8afc5c9694ae890", size = 222389, upload-time = "2026-08-03T21:19:48.331Z" }, { url = "https://files.pythonhosted.org/packages/6a/5b/717f1526b9957b34456313c31645c5b82b8fb5c3fe9e4752999be7128bfc/cffi-2.1.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:4a7c934f7360e8cd64fe9efadcbd10c7c6364f531e432b9a4bf5ccbc9e0e8b50", size = 210249, upload-time = "2026-08-03T21:19:49.543Z" }, { url = "https://files.pythonhosted.org/packages/64/b3/f8aa4f3e34986c7e4ec45072d1b1b9dd295b6b18007b45518d79726dd725/cffi-2.1.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:3143d81e29e1e20a9ce10901ec369012947876596f75a222235965f2b7ae832e", size = 208775, upload-time = "2026-08-03T21:19:50.918Z" }, { url = "https://files.pythonhosted.org/packages/b1/db/dceb9dd5b231e1da801793f8acc9f3c52a7e1afe40bb1aae37e02b0faad5/cffi-2.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c1453022f490d2459a11819d83ad1d586e9ff65a12ac3e705ffebd46d3685dcf", size = 221822, upload-time = "2026-08-03T21:19:52.054Z" }, { url = "https://files.pythonhosted.org/packages/a0/d2/6cd24ae3be000a634109c247d1475d62e5616d0dc78c82770942ec384248/cffi-2.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:208f941bb9d18e768138677f0a6d2ce01f590df56043dda1df1535ac57c88517", size = 225232, upload-time = "2026-08-03T21:19:53.109Z" }, { url = "https://files.pythonhosted.org/packages/cb/52/3fa190537004dd7f0ab860a6dc7c0175b8667f68d1e618a46f5498d30250/cffi-2.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:210019b6c7cf07f081b4c54635c8cf744377001350e29cc0f81c4377b4797735", size = 223597, upload-time = "2026-08-03T21:19:54.515Z" }, { url = "https://files.pythonhosted.org/packages/80/fb/0bb75b7039588c074b37ae99f40d9bfddf990ecb2fbc346ebccd2e56b9be/cffi-2.1.1-cp312-cp312-win32.whl", hash = "sha256:046bfc24911b37851ee1b51aab8bffe713d89c68c6a057b09484ce9fd5f69b4e", size = 175292, upload-time = "2026-08-03T21:19:55.566Z" }, { url = "https://files.pythonhosted.org/packages/d9/79/615cc094e2fb508cade7de88d3b4f6c4ec2bab695c97bce9153dc65aadf5/cffi-2.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:f53e442b08449d42821fa4a4fba000095af9f62742a500f978a9f557ec44339a", size = 185919, upload-time = "2026-08-03T21:19:56.89Z" }, { url = "https://files.pythonhosted.org/packages/70/c6/d0ea84713fe46b243a436a18fcd47d639732747e21635c8a27191b06dc30/cffi-2.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:7bde5e4cc5c10140859842b9d383af292b22639a4dffb725314baf45968cef80", size = 180093, upload-time = "2026-08-03T21:19:58.155Z" }, { url = "https://files.pythonhosted.org/packages/9d/f4/035513d4117049066b4779dc3b7c0c0fdad175fa13731c9f4003f1cd1478/cffi-2.1.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:b5bdfd1c873d4e093aabc0ca84c4ca6dbc4f752afb5c86f146d9742580c9da2e", size = 194248, upload-time = "2026-08-03T21:19:59.399Z" }, { url = "https://files.pythonhosted.org/packages/76/af/2aeb4dbb5fc41a04161ae9ff1518de7cec08e164f44a8ce6a4cf7fd2cd1d/cffi-2.1.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:31348097ff5bbe827ccc41795d4dd099d9f0625e7def00ee653c137a490c2a6c", size = 196908, upload-time = "2026-08-03T21:20:00.746Z" }, { url = "https://files.pythonhosted.org/packages/a7/46/2e5fdde8555706dd98139a910ca11be02809f3f605ce956f655d0214e100/cffi-2.1.1-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:9d2055050ea716bd38b7f7f1579c275386646b4894c155a3e2f3cd62ed41b7c6", size = 184805, upload-time = "2026-08-03T21:20:02.02Z" }, { url = "https://files.pythonhosted.org/packages/55/41/4c7042f317b9217502988f0873af87e16ad606dc20f84e546e3e6ce9764c/cffi-2.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:19ee6127ee34de7d83ce3d371ebc5ed91addbdcc39f9ab15ce4eb35a4e534971", size = 184764, upload-time = "2026-08-03T21:20:03.141Z" }, { url = "https://files.pythonhosted.org/packages/43/1f/1c3d90d91811c8f86ced9ed637956c54bfe5b79ca98fe976d7f8c8979f6b/cffi-2.1.1-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:6a8dddef476fab96d066d578fc88526767b836ab5ab21754e1d5bf3879c31c7c", size = 214722, upload-time = "2026-08-03T21:20:04.377Z" }, { url = "https://files.pythonhosted.org/packages/37/6f/3b5ce4c3b2192d250f04908f2bfd91ef34552ec8f7716a5d4abdb8d67bb2/cffi-2.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f16c709686a78c727bbbf059f92b0bf41c6fc60deec706d2dc19f529175a6125", size = 222369, upload-time = "2026-08-03T21:20:05.544Z" }, { url = "https://files.pythonhosted.org/packages/02/10/4b3c75dde3d9663c9e02ba05c2668b954f671d4bbe346413ca8c696b295a/cffi-2.1.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:fcd22650c908d7b7da162bbfaab594a1227a15d1643a98c68b122ac642fa2264", size = 210175, upload-time = "2026-08-03T21:20:06.75Z" }, { url = "https://files.pythonhosted.org/packages/df/62/14f74b9543e605d17701dc797b815958b8bb70b7624ce1b832ddad48ed6c/cffi-2.1.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:aa9511c62d14da7aacc9b4bf51f3f697a621e83b2d6919008243c3aad168eea3", size = 208670, upload-time = "2026-08-03T21:20:08.04Z" }, { url = "https://files.pythonhosted.org/packages/95/95/86342356ff5953b3fb06f7ef7c5bee212d45e770abc7218d451b9148313c/cffi-2.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a931079504ecc49efed7744c476a5c343a92fabf66dec2db95edb1b2fdc770e2", size = 221824, upload-time = "2026-08-03T21:20:09.274Z" }, { url = "https://files.pythonhosted.org/packages/eb/ff/7b3429ff53aafe931ed8a5fc69f481bbef7ba6de87ddcbb63d08f483f613/cffi-2.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a2d7755bef5a12ed488f4ef1f1b69ee9191d7396083b755a5d2295f6edb4768b", size = 225148, upload-time = "2026-08-03T21:20:10.7Z" }, { url = "https://files.pythonhosted.org/packages/34/34/a95870b9221e09cf4f2ce3178b1a210abdfe63a1bd357da940418d7b8d15/cffi-2.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e0bcb7e0f677f543555d2adff3bf19c05f66cdb4796e5ff602442ab2fe3c4ef7", size = 223564, upload-time = "2026-08-03T21:20:12.165Z" }, { url = "https://files.pythonhosted.org/packages/70/ea/839b50531021a647fb5e929f72cf97bc1ff702b5472166164b5b6e76b851/cffi-2.1.1-cp313-cp313-win32.whl", hash = "sha256:334644fbac4eff73d985a17a91226df55d0f394160c4cfb880e084c8f7161cac", size = 175263, upload-time = "2026-08-03T21:20:13.559Z" }, { url = "https://files.pythonhosted.org/packages/60/a6/8b149b2c3f2e11aaa1618ef64500b45f50f22c57a977a4dff1aff1f91042/cffi-2.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:1aa5645c30469b09530c4ebca77ebf8f17618293c58f8549cb1a543a50236e7d", size = 185688, upload-time = "2026-08-03T21:20:14.69Z" }, { url = "https://files.pythonhosted.org/packages/01/9a/11f687cb39d6a3504060d5242f04f48c735afb4d3d533958a20594890cb2/cffi-2.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:63bbfd5ded17c4840ac07cd8f1c21ba9d9708141f840b324f422f41b207e3973", size = 180078, upload-time = "2026-08-03T21:20:15.917Z" }, { url = "https://files.pythonhosted.org/packages/d3/7b/d6bbf82b8b96e7391438898c42f5bd96dd02030fd5b64937d248220003e2/cffi-2.1.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:7dbb61fe3a7699468030f71bbe5f8a0e326a151daa91beb11a6fc1f980c55e1c", size = 194064, upload-time = "2026-08-03T21:20:17.148Z" }, { url = "https://files.pythonhosted.org/packages/94/e6/bcc91b283be94735e268487a054004f0aa19947b6348fa367db53230abc8/cffi-2.1.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:f24fb43132a4c6b4cb4eb029492919b2db645be6808d738f244fd146c03c32cb", size = 196720, upload-time = "2026-08-03T21:20:18.268Z" }, { url = "https://files.pythonhosted.org/packages/d9/99/c4b0c17cacdc9c3b8f280026286a9826d6a208c0f047591a3c3ce99b91fd/cffi-2.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d28630f5854ab07ab1fd4aba756de52326c82e6be15d414b12793f1975048b54", size = 184964, upload-time = "2026-08-03T21:20:19.708Z" }, { url = "https://files.pythonhosted.org/packages/b3/a9/9db617d05d7367c1ad0ab00b3aa6e6f9281edd689b4ee9ea0e5a84e89c97/cffi-2.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:661c298b4821edebead0c91edd2b00374d67ad7c5a1f7a91d4442633b79d6a72", size = 184962, upload-time = "2026-08-03T21:20:20.833Z" }, { url = "https://files.pythonhosted.org/packages/67/b8/b42132ca113dc567d37684437b46ca1dafc885902b02a110a02d5b511857/cffi-2.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:58acb8ab8e295e6c5ea12f888cbb13cf21511ef2a3303a23f4325c29d17fe5c1", size = 222328, upload-time = "2026-08-03T21:20:22.118Z" }, { url = "https://files.pythonhosted.org/packages/80/10/c5c0cbf0a657aecf59ef511409734230bf556f05a0d6c9eed7aa5c0a0166/cffi-2.1.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:456a61fa52d579ebf9df2e9552ead5129855dbaff6c1e5a9b1bc408809bdc062", size = 209985, upload-time = "2026-08-03T21:20:23.401Z" }, { url = "https://files.pythonhosted.org/packages/d5/6c/bfa0b87b03b9238148beca990292843c9396ba069b54496596594173de7b/cffi-2.1.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a4f00aa42f75d6e4595e8866e748cc1705adc0cddfeb2ca86d0d03993d63ba03", size = 208530, upload-time = "2026-08-03T21:20:24.628Z" }, { url = "https://files.pythonhosted.org/packages/e9/02/4e7d553a7ac4b4238b38b3c1b80d486e9d4436f8d2acbf87a0997fe3f402/cffi-2.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b0431303acaea1089ad4b3e9ce4e6518193def1118d4073ca848635ee4ea2e96", size = 221525, upload-time = "2026-08-03T21:20:25.758Z" }, { url = "https://files.pythonhosted.org/packages/82/1d/a4aaf9babd75acb4d5f223bff71533bee748dd770a382619a798960ee9ba/cffi-2.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:64faea20f4e2613363a1a9b9c7dd73058f3ecd00133a511e72ad7c511658f527", size = 225053, upload-time = "2026-08-03T21:20:26.985Z" }, { url = "https://files.pythonhosted.org/packages/81/10/5dc0e7bdd18e22107054288283380fc97a06ae3f1656a106908d666a3c88/cffi-2.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5c58fe613dc5e5336357eff555824a314d8e43282600435c8d1cb6a7a2fedd13", size = 223213, upload-time = "2026-08-03T21:20:28.277Z" }, { url = "https://files.pythonhosted.org/packages/0b/e9/d0061c364cde06ee43168a0d076ac1da512cbc380d44767b844ba34fe2b6/cffi-2.1.1-cp314-cp314-win32.whl", hash = "sha256:1a18a57b58cfb21fc28d72e876acf10eaed67a1ed96226f92af4df681d571c4c", size = 177682, upload-time = "2026-08-03T21:20:44.288Z" }, { url = "https://files.pythonhosted.org/packages/a7/06/1c3e01e3ba14c39f6d10bfbac52753b7e22259e38088e5cfe1d704918690/cffi-2.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:3222ba5d678f80a030e6afbcc33dc1ae5cb45facabb61cee2c7016b8432fde48", size = 187949, upload-time = "2026-08-03T21:20:45.623Z" }, { url = "https://files.pythonhosted.org/packages/87/5b/da4e39efe18eeb89cf580ea9cfc66b6a7c3eadb808fc0cc1d3a295cb5a5d/cffi-2.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:ab36d55f9ed2d067327667c2fea18dda018eb628dd6347aa01dda6cf1f5d3836", size = 182947, upload-time = "2026-08-03T21:20:46.955Z" }, { url = "https://files.pythonhosted.org/packages/23/59/40338bf421c5accea1d45158170c87006ef1cd371b05c077e76476949728/cffi-2.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7750c6449dff7864bb9bb27ddfb0267756189201a3afc911d82b3caacd70dfc3", size = 188504, upload-time = "2026-08-03T21:20:29.495Z" }, { url = "https://files.pythonhosted.org/packages/7d/47/5ecf1023850036e674c77ec4de86182d309ae344e39e7cba984b7df5d647/cffi-2.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0beceaabe56af686895136a2de78db54ecd8e4046b236b8fd6d6cb61389e9bf2", size = 188259, upload-time = "2026-08-03T21:20:31.291Z" }, { url = "https://files.pythonhosted.org/packages/2a/9c/92934c3bea9f785b23eba304538c0b4d37a2a96d2431eb3a1bc87a11aa19/cffi-2.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:49cbc70e6542d4ccccb936558d1064a8012541e78f821f955cff24e357776c94", size = 223864, upload-time = "2026-08-03T21:20:32.571Z" }, { url = "https://files.pythonhosted.org/packages/4d/45/ba4c93527bc38616a8bd36488acb69a2212d60486794f0c1f318949bbb76/cffi-2.1.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e2d65b31f36619cda3999b78b2aa9632e76b78448e7a56fc4240824200e7c4fc", size = 211538, upload-time = "2026-08-03T21:20:33.808Z" }, { url = "https://files.pythonhosted.org/packages/80/e9/b6ef565e452acb932fb0cb5443f44a78efbd1233e566f02b5a83855e9115/cffi-2.1.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:28907ab9bfb6aa13184cfc17c6b8e1023c5ab6fd7076d8c20a35e59fe04f8f29", size = 210688, upload-time = "2026-08-03T21:20:34.974Z" }, { url = "https://files.pythonhosted.org/packages/9a/95/eff5f0cee78d2eabc7eebffec40d3fc1876b5f3c95582e018bb4b99601f2/cffi-2.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:51b31d1c98274844cfd7838ce00bfc27c7423a4dc00fc0772fc3331c2cc90676", size = 223803, upload-time = "2026-08-03T21:20:36.564Z" }, { url = "https://files.pythonhosted.org/packages/fa/01/579d39fb8bef00a335a23d83757b44feb24cd6345a2c451b64cb67b9c362/cffi-2.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e7cecbaadb83884793e05828cee59b210b24583b9c7425d0ba6a754fe22eb4e", size = 226763, upload-time = "2026-08-03T21:20:37.816Z" }, { url = "https://files.pythonhosted.org/packages/8d/b0/0b44f47c60b01b57b6e2bbd92343f13a85a1d93bc46ccf6e47e244acd99c/cffi-2.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:25792eac27877609e7bb06d42ff88278a6624fff2ba9bbb523c09616b117e80f", size = 225688, upload-time = "2026-08-03T21:20:38.959Z" }, { url = "https://files.pythonhosted.org/packages/eb/d2/3b7176cb570a1d3e27faf67b72f591af508036e0d8b2be2ef9af9e8c84bb/cffi-2.1.1-cp314-cp314t-win32.whl", hash = "sha256:8ef53b2de9bcb9197d31854256575d59dbac0cba72ac627bb291ef5eceb74be4", size = 182868, upload-time = "2026-08-03T21:20:40.388Z" }, { url = "https://files.pythonhosted.org/packages/56/78/31f00c1bcd97c9bbf55f1bfdf5bc809a5de8887473e90bb9960dca825e80/cffi-2.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:616f097f2fe415bc92a247f02e11f634e1f9e9a83d327e3c915c15089c87869e", size = 194104, upload-time = "2026-08-03T21:20:41.725Z" }, { url = "https://files.pythonhosted.org/packages/7b/1b/58496f2ed0a35de575250c02a43ab3cc2c04d494a88fed31c1cabc0fd176/cffi-2.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ad2c86c495b899d862ea0f4b42891b8713a3bd45dd4105c7fd51c2a72f39f3a5", size = 186402, upload-time = "2026-08-03T21:20:43.042Z" }, { url = "https://files.pythonhosted.org/packages/c1/8f/9ebe220eab48a093d1a5a5e339ab0dc7316eef3bb04d63c42f0251b61f50/cffi-2.1.1-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:dddad92b554513a31f272570678ba307fb9f618f05e3d4a5eacafff9eae03e1d", size = 194043, upload-time = "2026-08-03T21:20:48.179Z" }, { url = "https://files.pythonhosted.org/packages/ff/69/844bad3ece306c4782c2ecb93597035b6690d48704b803914c199da1e8b3/cffi-2.1.1-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:da0e573f9f97159390c89d9f1a9e41908b66d408cc5b58d08cf3847d844c531b", size = 196737, upload-time = "2026-08-03T21:20:49.457Z" }, { url = "https://files.pythonhosted.org/packages/1b/8a/af668013284634733f02d683458a0728739c7d6ddb5e14cb0c20832266fe/cffi-2.1.1-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:fb92203a88b3d3053034db775110081c49d28be6551923805e039924093761e4", size = 184933, upload-time = "2026-08-03T21:20:50.639Z" }, { url = "https://files.pythonhosted.org/packages/0c/75/2f5207ff6d1a613133b23a5203cc0c2a628313b5eb3974d7956ae3c57950/cffi-2.1.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2ae64be792b8966f2c69538199728b290e34726562896df1e5dc8ffd8d8188e8", size = 185002, upload-time = "2026-08-03T21:20:52.173Z" }, { url = "https://files.pythonhosted.org/packages/e2/31/9e1313b0a6e30e91b3b3d3fff51ae99c857c07738e3afcce1f7334e1b7ab/cffi-2.1.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:507a24c282e0f42f8ed737cf048572cbf580468da5555764a8331735e9c736b6", size = 222271, upload-time = "2026-08-03T21:20:53.462Z" }, { url = "https://files.pythonhosted.org/packages/50/e3/f6234a833e6e08c7007003074723c406559eecf9b48dfc97471e5a8eb7a0/cffi-2.1.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:246fa40ce8645a614ff682e0b70f37134e460eaf93a775e0cbe3cca585a67a80", size = 209919, upload-time = "2026-08-03T21:20:54.783Z" }, { url = "https://files.pythonhosted.org/packages/0d/fc/5f74e293fced6edb51af3a46c4ccf6c23c9943774ecb375ddbd522c76add/cffi-2.1.1-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:471cee653ae88de62096552e6d24ccb4a5adb8c8c9f10b5054d0122c15bf2779", size = 208529, upload-time = "2026-08-03T21:20:56.066Z" }, { url = "https://files.pythonhosted.org/packages/44/16/29e6d01b388bef055ecd6ca8244b3f4d336bd09e92d5d892187b9601084e/cffi-2.1.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aeae0e330c9f6acd681f647d46cefd30c29f93e3392882e792e82080c9691399", size = 221630, upload-time = "2026-08-03T21:20:57.336Z" }, { url = "https://files.pythonhosted.org/packages/a4/18/fa7f1f6857d5eb88a4ca99ffcbfb7c387a287ccc154c64a73e86314745d7/cffi-2.1.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:42a494cee34437f05546455144f2b5d9ac09b1face62bcfce597d2e521066688", size = 225134, upload-time = "2026-08-03T21:20:58.675Z" }, { url = "https://files.pythonhosted.org/packages/e0/9f/e8e3dfa04a1b4c241f8c91faacad872b4d4efd051d49764ad4e2fd4b9fea/cffi-2.1.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:cc572dace3f60ef98d7b12ff411d20f5362feb31a0439eab0085bbfd349982d7", size = 223197, upload-time = "2026-08-03T21:20:59.968Z" }, { url = "https://files.pythonhosted.org/packages/f8/7e/8debeb04f1ab9fe2a6963964cd6f1aaf7192627b83926586a6a4e089c9fa/cffi-2.1.1-cp315-cp315-win32.whl", hash = "sha256:4f42141fc14250de6dde5ee7ea4432be017252d91f19c5ad043c084cea629cac", size = 177683, upload-time = "2026-08-03T21:21:14.901Z" }, { url = "https://files.pythonhosted.org/packages/e0/31/5158704cc474ab65c1647932e88be78dc0873f47130e253be38bcaf13d01/cffi-2.1.1-cp315-cp315-win_amd64.whl", hash = "sha256:e6e8cff14d6fb0be70a09c0bdc58096f501952d04624ebf867e0e56da2df8960", size = 187897, upload-time = "2026-08-03T21:21:16.108Z" }, { url = "https://files.pythonhosted.org/packages/cc/4b/b3a2da8570c704ffc0f9762cdc3ec0f02c8573798e0b5cf7f11c82bbb70f/cffi-2.1.1-cp315-cp315-win_arm64.whl", hash = "sha256:27350daa11d4f10c540e6e89dada4c54feb7256ad03e9a4dc075ebad7ba360d1", size = 182935, upload-time = "2026-08-03T21:21:17.271Z" }, { url = "https://files.pythonhosted.org/packages/d0/ef/5443574510a1207e6f6bc38ba6e1f1de36cb48fef07b2728bb896a21f430/cffi-2.1.1-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:c26608d2222fb1e94487e4a387d85f13eb55d5ed725cb25a0c589ac4ee60e7bc", size = 188464, upload-time = "2026-08-03T21:21:01.163Z" }, { url = "https://files.pythonhosted.org/packages/7e/ae/a56fa8c4686ad50e148fcbc8d3ae0d03915ff5c30d795058988c24118cef/cffi-2.1.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:4be96343e422f2dfcd12ab5c9f5aebe03f82f737c6bffeca6830b3875cb44aab", size = 188262, upload-time = "2026-08-03T21:21:02.382Z" }, { url = "https://files.pythonhosted.org/packages/53/b2/6187f46f2912276a3ae284076109cc5c8680482f11f766ccf26db4a86427/cffi-2.1.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:937c0052c05a31ca1daf18de3158eed4dbfcb9cc107adbea227728d647be701e", size = 223779, upload-time = "2026-08-03T21:21:03.553Z" }, { url = "https://files.pythonhosted.org/packages/8a/f6/c3ad28bd19f77047a03084424fbd4cbe997303267c14423737324be0385d/cffi-2.1.1-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:df423d40ee8654634421812bc3b196da3f9bd7d32929da813f8394c4348a5358", size = 211520, upload-time = "2026-08-03T21:21:04.863Z" }, { url = "https://files.pythonhosted.org/packages/a0/cd/ccac9013a5bd9fd764de118674ab9c805b5ca10c19270d90ee273f8b2240/cffi-2.1.1-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a730a083190634c65cca36ba5f489531576ebd79bcd5c8e172130f6453127231", size = 210673, upload-time = "2026-08-03T21:21:06.223Z" }, { url = "https://files.pythonhosted.org/packages/52/86/2976131c639aead931c5bee5aba67e4b09fbeb8018b6f282f70803f923a7/cffi-2.1.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:363e05fa78e15116c3c32c210ee36884fd6b9afa6d440e47112c3bd511d64cb6", size = 223835, upload-time = "2026-08-03T21:21:07.539Z" }, { url = "https://files.pythonhosted.org/packages/ac/0c/33a7aeab2f9c76918c52e084beb39c570db3588133412929e8ec06fab90b/cffi-2.1.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:770de9db11e84213beec501cfcaa013b019820ca881e03344dea5844f7876d94", size = 226705, upload-time = "2026-08-03T21:21:08.774Z" }, { url = "https://files.pythonhosted.org/packages/e3/26/2cde30fdde421130bfc18f70395731a6e6b2053c6a1978a5258ff04e72fa/cffi-2.1.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:7da0c5eff80f0197f3b3d1232ec5a682a9325f4ae9016a78f5f5ca35f9ced1f5", size = 225539, upload-time = "2026-08-03T21:21:09.911Z" }, { url = "https://files.pythonhosted.org/packages/6d/cd/a361394c94b2129d604bb846f624a8e88255a3ee33129c434a00d715e64f/cffi-2.1.1-cp315-cp315t-win32.whl", hash = "sha256:06c72bb76605a4b0cd0aad6930b69d4baf7dd5d806cfc409b824191099700e66", size = 182707, upload-time = "2026-08-03T21:21:11.226Z" }, { url = "https://files.pythonhosted.org/packages/9b/b5/ba2b299993c26577d529b6ae29841f9e15b9fcf004d65f423f4fcf94ade9/cffi-2.1.1-cp315-cp315t-win_amd64.whl", hash = "sha256:d9c275eaacd24aa73f94ffd6de08fc3f932424d8b6c376f4bed7cde376fe7bc3", size = 193772, upload-time = "2026-08-03T21:21:12.39Z" }, { url = "https://files.pythonhosted.org/packages/aa/29/35e016098c814cd93de9cd320c66b5bfba14dc6ecedd3cb518fa7c408c69/cffi-2.1.1-cp315-cp315t-win_arm64.whl", hash = "sha256:d18e5ac0f2f03f4f518d3e23db0f0cad7faa1da8620e9c09461d443bbf6e6692", size = 186360, upload-time = "2026-08-03T21:21:13.636Z" }, ] [[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.9" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/bd/2a/23f34ec9d04624958e137efdc394888716353190e75f25dd22c7a2c7a8aa/charset_normalizer-3.4.9.tar.gz", hash = "sha256:673611bbd43f0810bec0b0f028ddeaaa501190339cac411f347ac76917c3ae7b", size = 152439, upload-time = "2026-07-07T14:34:58.454Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/ad/81/8e983840c6e5b93b33c2ba81aa3d52c2e42f0e9a690ce7607a2e61da4a5c/charset_normalizer-3.4.9-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cd6280cf040f233bd7d3407b743b4b4c74f70e8e1c4199cb112a62c941c0772a", size = 322240, upload-time = "2026-07-07T14:32:36.236Z" }, { url = "https://files.pythonhosted.org/packages/de/d1/b4319dc3229d8272fba305e206fc0a148e2de8d4087917ce62ae6382f359/charset_normalizer-3.4.9-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa99adc8f081b475a12843953db36831eaf83ec33eb46a90629ca6a5de45a616", size = 216475, upload-time = "2026-07-07T14:32:38.142Z" }, { url = "https://files.pythonhosted.org/packages/80/33/6c99c1b3e6b8bf730e1bc809b9a2608f224145069114c479a2e9e1494346/charset_normalizer-3.4.9-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c1225416b463483160e4af85d5fc3a9690ccb53fd4b1865a6437825f5ede3209", size = 238670, upload-time = "2026-07-07T14:32:39.658Z" }, { url = "https://files.pythonhosted.org/packages/7f/f4/ffbb83546e1f198ecc70ecd372b65cf2b50f9068b380abd67640f17a8e18/charset_normalizer-3.4.9-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:16d10d789dd9bcca1173c95af82c58433122564b7bc39385124be735a35cbe99", size = 233476, upload-time = "2026-07-07T14:32:41.155Z" }, { url = "https://files.pythonhosted.org/packages/e8/5f/b98b8da398637b551e427e7be922bdec19177dc54d6811dcdaa503f23aac/charset_normalizer-3.4.9-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9bb41182d93ea91f60b4bc8fbf4c820c69ef8a12ab2d917f3f1834f1acad07e8", size = 223817, upload-time = "2026-07-07T14:32:42.592Z" }, { url = "https://files.pythonhosted.org/packages/36/31/a276bb2e66243072a3fd06fdcab9cbb61a305b02143d70d2bda21d888fa8/charset_normalizer-3.4.9-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:bcf74c1df76758a395bf0af608c04c82257523f55c9868b334f06270d0f2112b", size = 207974, upload-time = "2026-07-07T14:32:44.258Z" }, { url = "https://files.pythonhosted.org/packages/5e/be/7ee4453d7e88dfbc4104ccd34900b9f2c7c17dac22881865fe0e82424a25/charset_normalizer-3.4.9-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b5314963fce9b0b12743891de876e724997864ee22aa496f903f426c7e2fa5b2", size = 221655, upload-time = "2026-07-07T14:32:45.64Z" }, { url = "https://files.pythonhosted.org/packages/1d/85/181c652953eb5276d198f375b1dd641047392050098100a3a02d6534f657/charset_normalizer-3.4.9-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e9701d0049d92c16703a42771b98d560b95248949f23f8cf7b4eddd201814fb9", size = 219229, upload-time = "2026-07-07T14:32:47.376Z" }, { url = "https://files.pythonhosted.org/packages/0c/e7/aaf6da33fc9f4691cda8f7efbc9f69179d3d39ec8a4799baf273ee1d8db0/charset_normalizer-3.4.9-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:65a7ff3f705e57d392f7261b6d0550fe137c3019477431f1c355e0db0a7d3e15", size = 209704, upload-time = "2026-07-07T14:32:48.855Z" }, { url = "https://files.pythonhosted.org/packages/63/01/f2fb3bd3a73be48b173ee0c6aa8d2497af97d5663a8c4c4b491de4c62f7a/charset_normalizer-3.4.9-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:79580094b00d1789d1f93ea55bc43cb2f611910c72235b7657f3482ddcc1b22d", size = 226243, upload-time = "2026-07-07T14:32:50.239Z" }, { url = "https://files.pythonhosted.org/packages/c4/02/c57a22739fe05246b0b5783b3bfb6afaac4eebb46f3ececdfb2f048f780e/charset_normalizer-3.4.9-cp310-cp310-win32.whl", hash = "sha256:432786d3561e69aeeae6c7e8648964ce0ad05736120135601f87ac26b9c83381", size = 150935, upload-time = "2026-07-07T14:32:51.676Z" }, { url = "https://files.pythonhosted.org/packages/37/8d/ca39a7559a4797505530d084fd3a49a2c959efbbbff146302fb7be4e3b35/charset_normalizer-3.4.9-cp310-cp310-win_amd64.whl", hash = "sha256:8c041122946b7ba21bb32c45b1aa57b1be35527690aeb3c5c234521085632eee", size = 162314, upload-time = "2026-07-07T14:32:53.193Z" }, { url = "https://files.pythonhosted.org/packages/01/da/a44bd7a13d426e69e4894557106cd58669097bfad4a8681123b618fbfc5d/charset_normalizer-3.4.9-cp310-cp310-win_arm64.whl", hash = "sha256:375b83ed0aecfce76c16d198fbc21f3b11b337d68662bea0a995046682a11419", size = 153075, upload-time = "2026-07-07T14:32:54.554Z" }, { url = "https://files.pythonhosted.org/packages/0b/e3/85ec501f206fb049259288c1f3506e53876937fb00edb47009348e66756b/charset_normalizer-3.4.9-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0e94703ec9684807f20cfb5eed95c70f67f2a8f21ad620146d7b5a13677b93e5", size = 317075, upload-time = "2026-07-07T14:32:56.021Z" }, { url = "https://files.pythonhosted.org/packages/c3/69/2a5385192e67175f7d8bd5ce4f57c24bc956439adeae5c13a99aa28a53d1/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a441ea71902098ffe78c5abe6c494f44160b4af614ed16c3d9a3b1d17fd8ee2", size = 213837, upload-time = "2026-07-07T14:32:57.78Z" }, { url = "https://files.pythonhosted.org/packages/b3/46/03ddc7da576d814fe0a36dd1f0fd3258e95404b4b2e3c026b7923d7e133f/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:304b13570067b2547562e308af560b3963857b1fa90bd6afd978130130fe2d6a", size = 235503, upload-time = "2026-07-07T14:32:59.205Z" }, { url = "https://files.pythonhosted.org/packages/4e/6e/de0229a7ef40f6f9d28a837eebf4ec47bdca5dab4e900c84f22919af636a/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4773092f8019072343a7447203308b176e10199920eb02d6195e81bbb3274c29", size = 229944, upload-time = "2026-07-07T14:33:00.803Z" }, { url = "https://files.pythonhosted.org/packages/a5/34/49b9060e8418b14fb5cba9cf6bfb383111e2538a03a1fb18e66a95aeb3d5/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:04ce310cb89c15df659582aee80a0603788732a5e017d5bd5c81158106ce249c", size = 221276, upload-time = "2026-07-07T14:33:02.199Z" }, { url = "https://files.pythonhosted.org/packages/44/95/80282cce0fae9c3061203d723ee87da996aed79679e65d8935050ee7ca1f/charset_normalizer-3.4.9-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:c0323c9daef75ef2e5083624b4585018a0c9d5e3b40f607eed81a311270b934b", size = 205260, upload-time = "2026-07-07T14:33:03.698Z" }, { url = "https://files.pythonhosted.org/packages/0c/74/2f62c8821b969ea3bd67cc2e6976834f48ca5d12664d2559ebcd9bcfbed7/charset_normalizer-3.4.9-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:871ff67ea1aad4dfd91736464934d56b32dac49f9fbe16cddba36198a7b3a0db", size = 217786, upload-time = "2026-07-07T14:33:05.12Z" }, { url = "https://files.pythonhosted.org/packages/d9/8d/feabb82cb49fcad14515b1d7d1ca4787b0da7fc723a212bf89bc9e0fac52/charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:67830fc78e67501f47bb950471b2dcb9b35b140084429318e862895a8e89c993", size = 216798, upload-time = "2026-07-07T14:33:06.629Z" }, { url = "https://files.pythonhosted.org/packages/a5/ff/c946d63bc3786d5b84d960b0f7ab7e25b828486a946b5aa997625bcaf6a6/charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:3d92613ec25e43b05f042302531ec0f00b8445190e43325880cbd6ab7c2581da", size = 206429, upload-time = "2026-07-07T14:33:08.006Z" }, { url = "https://files.pythonhosted.org/packages/af/ba/5e5007c370702f85d2ef75791fac7943ed41e080364a673b20142e430e3e/charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:280081916dc341820640489a66e4696049401ef1cf6dd672f672e70ad915aca3", size = 223066, upload-time = "2026-07-07T14:33:09.783Z" }, { url = "https://files.pythonhosted.org/packages/83/d5/9096aa3cf532dfad237861544eb47a0f20d5adbf1039760fed8eaae935d9/charset_normalizer-3.4.9-cp311-cp311-win32.whl", hash = "sha256:ac351b3b8014eead140e77e9717e2992c6bbe30b63bc3422422eb84865412e3d", size = 150456, upload-time = "2026-07-07T14:33:11.217Z" }, { url = "https://files.pythonhosted.org/packages/ed/a1/e29995109e455dc8eff8d0fac6ae509be39561318a7cfeac5d33ad029213/charset_normalizer-3.4.9-cp311-cp311-win_amd64.whl", hash = "sha256:6366a16e1a25018694d6a5d784d09b046edc9eac40ea2b54065c3052672516a1", size = 161410, upload-time = "2026-07-07T14:33:12.743Z" }, { url = "https://files.pythonhosted.org/packages/4f/8d/1569f4d0032d6ba2a4fe4591c35bf87868c600c41a71eb5c2e1ffa8464c2/charset_normalizer-3.4.9-cp311-cp311-win_arm64.whl", hash = "sha256:1d22856ffbe153a602df38e4a5464f0b748a54002e0d69ac6d2ad0a197cc99ec", size = 152649, upload-time = "2026-07-07T14:33:14.173Z" }, { url = "https://files.pythonhosted.org/packages/70/4a/ecbd131485c07fcdfad54e28946d513e3da22ef3b4bd854dcafae54ec739/charset_normalizer-3.4.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:45b0cc4e3556cd875e09102988d1ab8356c998b596c9fced84547c8138b487a0", size = 319300, upload-time = "2026-07-07T14:33:15.666Z" }, { url = "https://files.pythonhosted.org/packages/ec/96/5d9364e3342d69f3a045e1777bc47c85c383e6e9466d561b33fdb419d1f9/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9b2aff1c7b3884512b9512c3eaadd9bab39fb45042ffaaa1dd08ff2b9f8109d9", size = 215802, upload-time = "2026-07-07T14:33:17.031Z" }, { url = "https://files.pythonhosted.org/packages/4b/4c/5361f9aa7f2cb58d94f2ab831b3d493f69efb1d239654b4744e3c09527cb/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9104ed0bd76a429d46f9ec0dbc9b08ad1d2dcdf2b00a5a0daa1c145329b35b44", size = 237171, upload-time = "2026-07-07T14:33:18.576Z" }, { url = "https://files.pythonhosted.org/packages/50/78/ce342ca4ff30b2eb49fe6d9578df85974f90c67d294113e94efdd9664cbd/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7b86a2b16095d250c6f58b3d9b2eee6f4147754344f3dab0922f7c9bf7d226c9", size = 233075, upload-time = "2026-07-07T14:33:20.084Z" }, { url = "https://files.pythonhosted.org/packages/01/c4/4fa4c8b3097a11f3c5f09a35b72ed6855fb1d332469504962ab7bafcc702/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e226f6218febc71f6c1fc2fafb91c226f75bdc1d8fb12d66823716e891608fd", size = 224256, upload-time = "2026-07-07T14:33:21.747Z" }, { url = "https://files.pythonhosted.org/packages/87/3a/ad914516df7e358a81aae018caa5e0470ba827fa6d763b1d2e87d920a5f6/charset_normalizer-3.4.9-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:90c44bc373b7687f6948b693cceaea1348ae0975d7474746559494468e3c1d84", size = 208784, upload-time = "2026-07-07T14:33:23.313Z" }, { url = "https://files.pythonhosted.org/packages/d7/74/3c12f9755717dfe5c5c87da63f35d765fa0c00382ec26bf23f7fae34f2ba/charset_normalizer-3.4.9-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cdef90ae47919cae358d8ab15797a800ed41da7aba5d72419fb510729e2ed4b", size = 219928, upload-time = "2026-07-07T14:33:24.814Z" }, { url = "https://files.pythonhosted.org/packages/33/9a/895095b83e7907abd6d3d99aad3a38ad0d9686cc186cb0c94c24320fe63e/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:60f44ade2cf573dad7a277e6f8ca9a51a21dda572b13bd7d8539bb3cd5dbedde", size = 218489, upload-time = "2026-07-07T14:33:26.42Z" }, { url = "https://files.pythonhosted.org/packages/a1/34/ef5c05f412f42520d7709b7d3784d19640839eb7366ded1755511585429f/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:a1786910334ed46ab1dd73222f2cd1e05c2c3bb39f6dddb4f8b36fc382058a39", size = 210267, upload-time = "2026-07-07T14:33:27.952Z" }, { url = "https://files.pythonhosted.org/packages/83/dc/9b29fa4412b318bf3bfea985c35d67eb55e04b59a7c3f2237168b0e0be6f/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:03d07803992c6c7bbc976327f34b18b6160327fc81cb82c9d504720ac0be3b62", size = 226030, upload-time = "2026-07-07T14:33:29.397Z" }, { url = "https://files.pythonhosted.org/packages/0e/42/6dbc00b8cd16011691203e33570fa42ed5746599a2e878112d16eab403a3/charset_normalizer-3.4.9-cp312-cp312-win32.whl", hash = "sha256:78841cccf1af7b40f6f716338d50c0902dbe88d9f800b3c973b7a9a0a693a642", size = 151185, upload-time = "2026-07-07T14:33:30.781Z" }, { url = "https://files.pythonhosted.org/packages/80/cc/f920afd1a23c58ccd53c1d36085a71893a4737ff5e66e0371efab6809850/charset_normalizer-3.4.9-cp312-cp312-win_amd64.whl", hash = "sha256:4b3dac63058cc36820b0dd072f89898604e2d39686fe05321729d00d8ac185a0", size = 162557, upload-time = "2026-07-07T14:33:32.176Z" }, { url = "https://files.pythonhosted.org/packages/f0/e6/0386d43a261ff4e4b30c5857af7df877254b46bec7b9d1b74b6bf969a90b/charset_normalizer-3.4.9-cp312-cp312-win_arm64.whl", hash = "sha256:78fa18e436a1a0e58dbd7e02fc4473f3f32cceb12df9dfca542d075961c307d2", size = 152665, upload-time = "2026-07-07T14:33:33.711Z" }, { url = "https://files.pythonhosted.org/packages/b2/06/97ec2aeae780b31d742b6352218b43841a6871e2564578ca522dce4a45c3/charset_normalizer-3.4.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:440eede837960000d74978f0eba527be106b5b9aee0daf779d395276ed0b0614", size = 317688, upload-time = "2026-07-07T14:33:35.408Z" }, { url = "https://files.pythonhosted.org/packages/d0/39/8ff066c672434225f8d25f8b739f992af250944392173dcc88362681c9bf/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21e764fd1e70b6a3e205a0e46f3051701f98a8cb3fad66eeb80e48bb502f8698", size = 214982, upload-time = "2026-07-07T14:33:36.996Z" }, { url = "https://files.pythonhosted.org/packages/92/8f/3a47a3667c83c2df9483d91644c6c107de3bf8874aa1793da9d3012eb986/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e4fd89cc178bced6ad29cb3e6dd4aa63fa5017c3524dbd0b25998fb64a87cc8b", size = 236460, upload-time = "2026-07-07T14:33:38.536Z" }, { url = "https://files.pythonhosted.org/packages/f1/60/b22cdbee7e4013dab8b0d7647fc6181120fbbbc8f7025c226d15bd5a47fc/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bd47ba7fc3ca94896759ea0109775132d3e7ab921fbf54038e1bab2e46c313c9", size = 232003, upload-time = "2026-07-07T14:33:40.059Z" }, { url = "https://files.pythonhosted.org/packages/ea/f8/72eb13dcabe7257035cea8aefd922caad2f110d252bf9f67c4c2ca763aee/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:84fd18bcc17526fc2b3c1af7d2b9217d32c9c04448c16ec693b9b4f1985c3d33", size = 223149, upload-time = "2026-07-07T14:33:41.631Z" }, { url = "https://files.pythonhosted.org/packages/b0/3e/faee8f9de92b14ee1198e9163252bb15efee7301b31256a3b6d9ebfdd0dd/charset_normalizer-3.4.9-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:5b10cd92fc5c498b35a8635df6d5a100207f88b63a4dc1de7ef9a548e1e2cd63", size = 207901, upload-time = "2026-07-07T14:33:43.209Z" }, { url = "https://files.pythonhosted.org/packages/3a/25/45f30093ae27dd7b92a793b61882a38685f993700113ca36e0c9c14965e1/charset_normalizer-3.4.9-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a4fbdde9dd4a9ce5fd52c2b3a347bb50cc89483ef783f1cb00d408c13f7a96c0", size = 219176, upload-time = "2026-07-07T14:33:44.725Z" }, { url = "https://files.pythonhosted.org/packages/48/18/c8f397329c35e32f6a837e488986f4ae03bd2abebc453b48714991630c2f/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:416c229f77e5ea25b3dfd4b582f8d73d7e43c22320302b9ab128a2d3a0b38efe", size = 217356, upload-time = "2026-07-07T14:33:46.192Z" }, { url = "https://files.pythonhosted.org/packages/86/7e/5ce0bba863470fd1902d5e5843968951bddf38abe4742fc97116ef4598b3/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:75286256590a6320cf106a0d28970d3560aad9ee09aa7b34fb40524792436d35", size = 209614, upload-time = "2026-07-07T14:33:47.705Z" }, { url = "https://files.pythonhosted.org/packages/6c/ef/2473d3c4d869155be4af1191111d59c4d5c4e0173026f7e85b176e23bf65/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:69b157c5d3292bcd443faca052f3096f637f1e074b98212a933c074ae23dc3b8", size = 224991, upload-time = "2026-07-07T14:33:49.238Z" }, { url = "https://files.pythonhosted.org/packages/d0/a3/53ddae3db108a088156aa8ddfafd411ebbc1340f48c5573f697b27f69a39/charset_normalizer-3.4.9-cp313-cp313-win32.whl", hash = "sha256:51307f5c71007673a2bf8232ad973483d281e74cb99c8c5a990af1eefa6277d9", size = 150622, upload-time = "2026-07-07T14:33:50.711Z" }, { url = "https://files.pythonhosted.org/packages/e8/ef/6953a77c7cf2c2ff9998e6f575ab3e380119f100223381565a4f94c1f836/charset_normalizer-3.4.9-cp313-cp313-win_amd64.whl", hash = "sha256:fe2c7201c642b7c308f1675355ad7ff7b66acfe3541625efe5a3ad38f29d6115", size = 161947, upload-time = "2026-07-07T14:33:52.197Z" }, { url = "https://files.pythonhosted.org/packages/6e/fb/d560d1d1555debbfe7849d9cac6145c1b537709d79576bf22557ed803b82/charset_normalizer-3.4.9-cp313-cp313-win_arm64.whl", hash = "sha256:611057cc5d5c0afc743ba8be6bd828c17e0aaa8643f9d0a9b9bb7dea80eb8012", size = 152594, upload-time = "2026-07-07T14:33:53.486Z" }, { url = "https://files.pythonhosted.org/packages/7e/8d/496817fa0944239ecae662dd57ea765cfeaec6a735f9f025d4b7b72e7143/charset_normalizer-3.4.9-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:0327fcd59a935777d83410750c50600ee9571af2846f71ce40f25b13da1ef380", size = 317253, upload-time = "2026-07-07T14:33:54.994Z" }, { url = "https://files.pythonhosted.org/packages/2b/f9/ef4a69ea338ad3c0deceea0f5f7d2380ae8b52132b06d652cb0d2cd86706/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a79d9f4d8001473a30c163556b3c3bfebec837495a412dde78b51672f6134f9", size = 215898, upload-time = "2026-07-07T14:33:56.334Z" }, { url = "https://files.pythonhosted.org/packages/8c/e7/5ddfd76fc061eb52de219658a4aa431cbacadf0a0219c8854f00da50d289/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:33bdcc2a32c0a0e861f60841a512c8acc658c87c2ac59d89e3a46dacf7d866e4", size = 236718, upload-time = "2026-07-07T14:33:57.9Z" }, { url = "https://files.pythonhosted.org/packages/49/ba/768fa3f36048d81c477a0ce61f813bc1454d80917ccfe550abd9f44f5e24/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f840ed6d8ecba8255df8c42b87fadeda98ddfc6eeec05e2dc66e26d46dd6f58a", size = 232519, upload-time = "2026-07-07T14:33:59.811Z" }, { url = "https://files.pythonhosted.org/packages/f4/c4/b3e049d2aa3766180c78507110543d9d50894cc97f57de543f1be521dcdc/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c25fe15c70c59eb7c5ce8c06a1f3fa1da0ecc5ea1e7a5922c40fd2fa9b0d5046", size = 223143, upload-time = "2026-07-07T14:34:01.517Z" }, { url = "https://files.pythonhosted.org/packages/19/79/55c32d06d76ae4feafe053f061f3e3ab70bcf19f4007797ce8c3efda7830/charset_normalizer-3.4.9-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:f7fb7d750cfa0a070d2c24e831fd3481019a60dd317ea2b39acbcebc08b6ed81", size = 206742, upload-time = "2026-07-07T14:34:03.04Z" }, { url = "https://files.pythonhosted.org/packages/10/e0/47c079dd82d217c807479cd59ffd30af56307ea31c108b75758970459ad3/charset_normalizer-3.4.9-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4d1c96a7a18b9690a4d46df09e3e3382406ae3213727cd1019ebade1c4a81917", size = 219191, upload-time = "2026-07-07T14:34:04.657Z" }, { url = "https://files.pythonhosted.org/packages/42/ab/b9bc2e77d6b44a7e46ef62ec5cac1c9a6ba7b9135a5d560f002696ec9995/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a4cfde78a9f2880208d16a93b795726a3017d5977e08d1e162a7a31322479c41", size = 218328, upload-time = "2026-07-07T14:34:06.115Z" }, { url = "https://files.pythonhosted.org/packages/f1/78/c9c71d599f5aa2d42bcdd35cbbd46d7f535351a57e40ff7d8e5a7e219401/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:d4d6fcde76f94f5cb9e43e9e9a61f16dacefd228cbbf6f1a09bd9b219a92f1a1", size = 207406, upload-time = "2026-07-07T14:34:07.554Z" }, { url = "https://files.pythonhosted.org/packages/f6/39/c914445c321a845097ce4f6ac7de9a18228a77b766272125a1ce00d851eb/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:898f0e9068ca27d37f8e83a5b962821df851532e6c4a7d615c1c033f9da6eedf", size = 225157, upload-time = "2026-07-07T14:34:09.061Z" }, { url = "https://files.pythonhosted.org/packages/9b/f2/c0d4b8508565a36bc5c624e88ed297f5b0b1095011034d7f5b83a69908b5/charset_normalizer-3.4.9-cp314-cp314-win32.whl", hash = "sha256:c1c948747b03be832dceed96ca815cef7360de9aa19d37c730f8e3f6101aca48", size = 151095, upload-time = "2026-07-07T14:34:10.901Z" }, { url = "https://files.pythonhosted.org/packages/49/fd/a1d26144398c67486422a72bf5812cda22cb4ccfcd95a290fb41ceb4b8e2/charset_normalizer-3.4.9-cp314-cp314-win_amd64.whl", hash = "sha256:16b65ea0f2465b6fb52aa22de5eca612aa964ddfec00a912e26f4656cbef890b", size = 162796, upload-time = "2026-07-07T14:34:12.47Z" }, { url = "https://files.pythonhosted.org/packages/20/95/d75e82f8ce9fd323ebf059c16c9aadefb22a1ecde13b7840b35835e4886c/charset_normalizer-3.4.9-cp314-cp314-win_arm64.whl", hash = "sha256:40a126142a56b2dfc0aacbad1de8310cbf60da7656db0e6b16eebd48e3e93519", size = 153334, upload-time = "2026-07-07T14:34:14.044Z" }, { url = "https://files.pythonhosted.org/packages/00/5e/17398df3a139985ba9d11ed072531986f408c8fca952835ef1ab1820c02b/charset_normalizer-3.4.9-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:609b3ba8fcc0fb5ab7af00719d0fb6ad0cb518e48e7712d12fd68f1327951198", size = 338848, upload-time = "2026-07-07T14:34:15.688Z" }, { url = "https://files.pythonhosted.org/packages/cd/91/7253a32e86b7e1d1239b1b36ba6dd0f021a21107ab33054b53119cc083b9/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51447e9aa2684679af07ca5021c3db526e0284347ebf4ffcec1154c3350cfe32", size = 223022, upload-time = "2026-07-07T14:34:17.248Z" }, { url = "https://files.pythonhosted.org/packages/cb/32/2e64bd2be10e89c61e57ebe6a93fd98ae88eb7ebe414b5121f22c96c69eb/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cc1b0fff8ead343dae06305f954eb8468ba0ec1a97881f42489d198e4ce3c632", size = 241590, upload-time = "2026-07-07T14:34:18.813Z" }, { url = "https://files.pythonhosted.org/packages/3d/ef/d96ec496cfea0c21db43b0ad03891308b02388d054cc902cf0e5a1ad6a88/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fa36ec09ef71d158186bc79e359ff5fdd6e7996fe8ab638f00d6b93139ba4fcf", size = 239584, upload-time = "2026-07-07T14:34:20.52Z" }, { url = "https://files.pythonhosted.org/packages/d4/ce/9af95f7876194bd7a14e3dfe4a4de2e0bff02666a3910d72beafd06cc297/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:df115d4d83168fdf2cae48ef1ff6d1cb4c466364e30861b37121de0f3bf1b990", size = 230224, upload-time = "2026-07-07T14:34:22.189Z" }, { url = "https://files.pythonhosted.org/packages/52/94/af74dde74a3996bd959c350709bfe50e297823d70a8c1cbd54b838880863/charset_normalizer-3.4.9-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:f86c6358749bd4fda175388691e3ba8c46e24c5347d0afd20f9b7edfc9faf07d", size = 212667, upload-time = "2026-07-07T14:34:23.857Z" }, { url = "https://files.pythonhosted.org/packages/ee/f0/f1c4fe746c395922961b5916ed1d7d6e7d4c84851d19ed43cc89980ec953/charset_normalizer-3.4.9-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:32286a2c8d167e897177b673176c1e3e00d4057caf5d2b64eef9a3666b03018e", size = 227179, upload-time = "2026-07-07T14:34:25.586Z" }, { url = "https://files.pythonhosted.org/packages/e4/56/6c745619ac397e8871e2bcd3cea1eec86b877488f33888b3aef5c3ed506e/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:83aed2c10721ddd90f68140685391b50811a880af20654c59af6b6c66c40513c", size = 225372, upload-time = "2026-07-07T14:34:27.212Z" }, { url = "https://files.pythonhosted.org/packages/78/ad/98aae8630ac71f16711968e38a5acfecce41b778bf2f0312851020f565a8/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cd6c3d4b783c556fa00bf540854e42f135e2f256abd29669fcd0da0f2dec79c2", size = 215222, upload-time = "2026-07-07T14:34:28.774Z" }, { url = "https://files.pythonhosted.org/packages/f7/40/9593d54209765207a7f11073c06494c1721e4ca4a0a426c597679bf7f91e/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ee2f2a527e3c1a6e6411eb4209642e138b544a2d72fe5d0d76daf77b24063534", size = 231958, upload-time = "2026-07-07T14:34:30.345Z" }, { url = "https://files.pythonhosted.org/packages/b1/27/693ee5e8a18191eb38647360c51cd505013e2bd3b366aa43fd5344c21e3c/charset_normalizer-3.4.9-cp314-cp314t-win32.whl", hash = "sha256:0d861473f743244d349b50f850d10eb87aeb22bbdcc8e64f79273c94af5a8226", size = 155580, upload-time = "2026-07-07T14:34:31.884Z" }, { url = "https://files.pythonhosted.org/packages/80/3f/bd97d3d9c613013d07cb7733d299385b41df37f0471310f5a73dc359f0b8/charset_normalizer-3.4.9-cp314-cp314t-win_amd64.whl", hash = "sha256:9b8e0f3107e2200b76f6054de99016eac3ee6762713587b36baaa7e4bd2ae177", size = 167620, upload-time = "2026-07-07T14:34:33.438Z" }, { url = "https://files.pythonhosted.org/packages/3d/c6/eee9dca4439b1061f76373f06ea855678cc4a64c1c3c90b50e479edbb8eb/charset_normalizer-3.4.9-cp314-cp314t-win_arm64.whl", hash = "sha256:19ac87f93086ce37b86e098888555c4b4bc48102279bae3350098c0ed664b501", size = 158037, upload-time = "2026-07-07T14:34:35.018Z" }, { url = "https://files.pythonhosted.org/packages/98/2b/f97f1c193fb855c345d678f5077d6926034db0722df74c8f057020e05a25/charset_normalizer-3.4.9-py3-none-any.whl", hash = "sha256:68e5f26a1ad57ded6d1cfb85331d1c1a195314756471d97758c48498bb4dcdf5", size = 64538, upload-time = "2026-07-07T14:34:56.993Z" }, ] [[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 = "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 = "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 = "datamodel-code-generator" version = "0.72.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "argcomplete" }, { name = "black", marker = "sys_platform != 'emscripten'" }, { name = "genson" }, { name = "inflect" }, { name = "isort", marker = "sys_platform != 'emscripten'" }, { name = "jinja2" }, { name = "pydantic" }, { name = "pyyaml" }, { name = "tomli", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b6/2d/12becb81ac1492ed5f6e3afcb2426e0d665c5ae51fa24bbe951b408dd867/datamodel_code_generator-0.72.2.tar.gz", hash = "sha256:0e11564564ccf12e8ae89ca28b7848c795b40609e9f011e7b3434428eebd6f12", size = 1856918, upload-time = "2026-08-06T14:36:05.515Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/da/e5/a64c15b05307974cf9676a73ab9e11bed49656a1df16188685c73cb290ed/datamodel_code_generator-0.72.2-py3-none-any.whl", hash = "sha256:df75b3a3225085e098337538cb6895f12e4daca80c43dc15284102bd42948dfc", size = 514012, upload-time = "2026-08-06T14:36:03.785Z" }, ] [package.optional-dependencies] http = [ { name = "httpx" }, ] ruff = [ { name = "ruff" }, ] [[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 = "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 = "filelock" version = "3.32.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/f6/57/3ba6e6cb097f85b855b00163d169f35365f44277df044dcf96d55b8f62a3/filelock-3.32.2.tar.gz", hash = "sha256:c33351e1f49cae33414acbc6d56784e6ecee82514ec90795da1161fc4836b5b8", size = 217172, upload-time = "2026-07-29T22:46:04.895Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/c1/e8/72f8cef9fdfeffe06213fe8508039396ee48daa0e3259457ed766173bfd6/filelock-3.32.2-py3-none-any.whl", hash = "sha256:87dd94cf281e586d135fa51132b8e3d9a598b316e90377a288663c9321036c82", size = 98830, upload-time = "2026-07-29T22:46:03.52Z" }, ] [[package]] name = "genson" version = "1.4.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/f8/53/de162dc8e03fccd9ebe59d17c7812378fe8bd2b604f6b1b94d00165140ac/genson-1.4.0.tar.gz", hash = "sha256:bc7f1c1bae87a21ca44d81149aec95a3f4468d676de9b8b08caa064f3c50b3da", size = 47908, upload-time = "2026-07-06T08:21:50.331Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/16/02/767f744ab6d4cb7761e5008acc3d534b7a0481af62563d52e391fbcb2140/genson-1.4.0-py3-none-any.whl", hash = "sha256:03bc71bbe52defde70660cc4dcd1ea1097997da5a1cbb90a9dbd3acc7c9e1b65", size = 24484, upload-time = "2026-07-06T08:21:49.046Z" }, ] [[package]] name = "google-auth" version = "2.56.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cryptography" }, { name = "pyasn1-modules" }, ] sdist = { url = "https://files.pythonhosted.org/packages/db/4c/fa42116a48bab3f7a143cf5042ecff7df9c8b73f8a376203cd534d1dc966/google_auth-2.56.3.tar.gz", hash = "sha256:40e229fc901f0a305b553050e5fce562d509bee0435be053abfa91582b51b90c", size = 367110, upload-time = "2026-08-06T06:24:01.36Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/bc/b3/6117b2f24065cd7e2c4f140e9a193e215f089ca8ba314cf91eb9d0b7fe0a/google_auth-2.56.3-py3-none-any.whl", hash = "sha256:8ec438808f813ad034535000261eed1067475d229d05bbf4216e78c3f2362e53", size = 259116, upload-time = "2026-08-06T06:22:51.788Z" }, ] [[package]] name = "googleapis-common-protos" version = "1.75.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "protobuf" }, ] sdist = { url = "https://files.pythonhosted.org/packages/72/73/74bcab964c9a7a61f2bb71e8179b0f13e6fa98f7ce00fd168aab291e4a2e/googleapis_common_protos-1.75.1.tar.gz", hash = "sha256:d3042c6c5a2d4e67113104d6b6818b59b6bd92a197f2a91508e801fe815cf071", size = 150967, upload-time = "2026-08-06T06:24:51.972Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/9a/51/186c02b8549b69ccda44429cf6ff5081e4b61a602ddfe6a8020d1be31d1b/googleapis_common_protos-1.75.1-py3-none-any.whl", hash = "sha256:28a1934bcd33b9c9da66ac301a0a4227e3367f095a17d0375cb98f0a09d93b79", size = 300626, upload-time = "2026-08-06T06:23:46.696Z" }, ] [[package]] name = "grpcio" version = "1.83.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/0c/98/304898ac4e04e2d5e4e4c2eadc178b1f2a16d5f4bc2f91306c87d64680b9/grpcio-1.83.0.tar.gz", hash = "sha256:7674587248fbbb2ac6e4eecf83a8a0f3d91a928f941de571acfd3a2f007fbc24", size = 13428824, upload-time = "2026-07-23T15:20:37.759Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/6b/fd/655c8a773d728bc3c93fb4713ae4bf79ffc75996f86fb78b2974c8e1dfbd/grpcio-1.83.0-cp310-cp310-linux_armv7l.whl", hash = "sha256:fba099b716e73512d61b97f71ea3c31a72abb36904036e316bf4dd148ca8dcc8", size = 6334247, upload-time = "2026-07-23T15:18:53.099Z" }, { url = "https://files.pythonhosted.org/packages/d4/9a/1ce5760d35a04a992006dd2f79afff2db548f93ee7426fa95c9f1fc90c61/grpcio-1.83.0-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:6755ed67cc3e454d51ae9f6e1915b80d3942fa4de956ef48dacd45ab7f40b727", size = 12168650, upload-time = "2026-07-23T15:18:56.348Z" }, { url = "https://files.pythonhosted.org/packages/8d/ab/bbcb5be0a1a6cb21f036e2afdd4f7a70147cfb7a7b42648a310d7c43acfc/grpcio-1.83.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5882c1a721b50ce0123ee5e839e1ab059ad72a7ade76cdf2d5bd833b56791acf", size = 6916899, upload-time = "2026-07-23T15:18:58.339Z" }, { url = "https://files.pythonhosted.org/packages/d5/d2/4c27977ecb3b3f9f363b93f570e001cb24ef264a9a907d7fd0f949ed59f0/grpcio-1.83.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:4e3eedfc92b6b9f2960115e7e620cf0cbf80bb7849a51ce3820dc54dfd88b6b9", size = 7648761, upload-time = "2026-07-23T15:19:00.071Z" }, { url = "https://files.pythonhosted.org/packages/23/49/0c823a7627ff2e69a61e4a53c4edf215272892fc2c47c6431f033d46f4cc/grpcio-1.83.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4fcaa7c45c45b4a89e2867d1f1785d9481a788399d915e341ed2eb49aeef9dd4", size = 7074920, upload-time = "2026-07-23T15:19:02.293Z" }, { url = "https://files.pythonhosted.org/packages/3b/ce/963f01ff7c789a76909c9691b704112e02ca1e11c10405cd99c2bd7c40f1/grpcio-1.83.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6b6c666a1d5613ff360c9e90f44665e3a88b25a815209ddbc0917eec281931cb", size = 7598046, upload-time = "2026-07-23T15:19:03.921Z" }, { url = "https://files.pythonhosted.org/packages/eb/de/1ce6bdefc847a7973040d10cebc8996c653a2a687c0a4da8d05dcab4e397/grpcio-1.83.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:6be5c807b717be3dd649446f021301fd7907e376318675d2147823071034112a", size = 8634792, upload-time = "2026-07-23T15:19:05.633Z" }, { url = "https://files.pythonhosted.org/packages/a5/8b/7fe6a73895e3bdd788101d1276e48e0d262ebb165afacec1ec4efebcd785/grpcio-1.83.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c834e86d8fd2f03d7e4db49a027f7c5b89c5b88eed305543a5295bd6fee61e40", size = 8000286, upload-time = "2026-07-23T15:19:07.739Z" }, { url = "https://files.pythonhosted.org/packages/c4/b0/9a779de2bcda8722501a056fad1bec3d1117977af0c080ab1fc0655fdf35/grpcio-1.83.0-cp310-cp310-win32.whl", hash = "sha256:35a5b1c192496b6c25956eebfa963468935612206fd2543ac3ce981e6a5e0f03", size = 4404616, upload-time = "2026-07-23T15:19:09.988Z" }, { url = "https://files.pythonhosted.org/packages/f0/8e/ce9a23590cac33a6c24e6386cc0ffc55821cc13212acc822e98f00a67161/grpcio-1.83.0-cp310-cp310-win_amd64.whl", hash = "sha256:8f6c395e493d20c39b29392ca200e9aaeb78d0bc2f04db0c0a7da7ddc939aa57", size = 5162304, upload-time = "2026-07-23T15:19:11.467Z" }, { url = "https://files.pythonhosted.org/packages/d3/f6/3b781cd07a715ea5f5125ae264226e7fc4d87603d6d3955022cabfdc5da2/grpcio-1.83.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:8ff0b8767ddd62704e0d9571c1890af08d84a3a689ebba1807e62519d0b3277f", size = 6338720, upload-time = "2026-07-23T15:19:13.177Z" }, { url = "https://files.pythonhosted.org/packages/21/cc/d14833d15d5984e366f1b027fa78bd038c9b028c66880bffb0f5a4d25ee2/grpcio-1.83.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:4772402f43517b4824980be4b3b2274a81eec0004a70009473c31b340d43e223", size = 12178773, upload-time = "2026-07-23T15:19:15.401Z" }, { url = "https://files.pythonhosted.org/packages/6b/98/8acbb416544e7871132d8e42a07ed70c802d70e6a16c6009e505a34d32a4/grpcio-1.83.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f4cee5fc86e84a0cf7ad1574b454c3320e087c07f55b7df5dc0ac6a873fb90c0", size = 6921203, upload-time = "2026-07-23T15:19:17.824Z" }, { url = "https://files.pythonhosted.org/packages/45/9c/0fdbfaf4fc54e5c88f6bce4008a065092fe7fbc4460eb5617ae8b20fd505/grpcio-1.83.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:f5e822a7e7d03282f6ad225e710493c48b9057a353358344a5f7c42b2b37618d", size = 7648508, upload-time = "2026-07-23T15:19:19.685Z" }, { url = "https://files.pythonhosted.org/packages/f3/ea/107b9dbb2ed3ad14dd774fd3dde7d29ff9938a6c198654becb2c3a0e9a6a/grpcio-1.83.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f5f410d7c2903eabb34789dfd6342eef04af1ad459943936b7e09a9f5bd417b9", size = 7079466, upload-time = "2026-07-23T15:19:21.478Z" }, { url = "https://files.pythonhosted.org/packages/3b/06/9fa9941089e6fae83b060b6ce61c1e81053e52decae43197245f45e07d36/grpcio-1.83.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ee94a4016fdf8699fb1fd8a38652475ff677f1c72074cee44deeeb9a7e95e745", size = 7605583, upload-time = "2026-07-23T15:19:23.74Z" }, { url = "https://files.pythonhosted.org/packages/a8/2f/f10fb56062dc2771c630827a82d9ad0ecd05cad572ea3b08d49f6631680a/grpcio-1.83.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:c6444666317338e903093c7c756e6cc88eee59f798cb8dd41e87725bf54e1617", size = 8637810, upload-time = "2026-07-23T15:19:25.536Z" }, { url = "https://files.pythonhosted.org/packages/99/55/f84927258f6a1b6ea6dea661fdc6de859b35e560c96f3012d15ccd39f85e/grpcio-1.83.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:aa074041231f03959cb097dd5517b0677b8ea49215bae01d5710a7b69dd59969", size = 8008021, upload-time = "2026-07-23T15:19:27.863Z" }, { url = "https://files.pythonhosted.org/packages/c9/6b/cdf72161397ccd29d4ca2192f641524536c9cf54ad948c9dd0e0e01138fa/grpcio-1.83.0-cp311-cp311-win32.whl", hash = "sha256:cb056f6e171c42639a50460b2929c82241fda51f71cf3dcdd68090fe45095a45", size = 4404376, upload-time = "2026-07-23T15:19:30.137Z" }, { url = "https://files.pythonhosted.org/packages/df/ed/e0ffeb4c848699c194dc9fb6a29ab29bcb2b6aac8c416bf18c51bfe8242c/grpcio-1.83.0-cp311-cp311-win_amd64.whl", hash = "sha256:7416952ca770477990257206276999056f8316d79196f2f25942393e58a20b49", size = 5164469, upload-time = "2026-07-23T15:19:31.941Z" }, { url = "https://files.pythonhosted.org/packages/15/2b/51e32514a4e9b715375c99721aadff0f24164cc2049b8269eda4de82a814/grpcio-1.83.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:28f6c35ac8fcf10e4594f138e468f194360089dde40d126a7033e863fc479930", size = 6303167, upload-time = "2026-07-23T15:19:33.78Z" }, { url = "https://files.pythonhosted.org/packages/39/33/b5b50fc2c6fbe350e04814047bb2d409feec7b36ef8b170254c050e06bc0/grpcio-1.83.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:33898e6a28e4ae598f1577cb1c4fec2a15c033d0ec52b9b45a09610dd045b9da", size = 12160538, upload-time = "2026-07-23T15:19:35.958Z" }, { url = "https://files.pythonhosted.org/packages/7b/5f/734e72e7b9f79bcf0b2c270b8d3bca0e4ebb97a27a50d06240b145f6d41e/grpcio-1.83.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6fb8a1dd0c6f0f931e69e9d0dc6d1c406ed2a44fa963414eafba07b7fb685d16", size = 6869310, upload-time = "2026-07-23T15:19:38.607Z" }, { url = "https://files.pythonhosted.org/packages/a4/17/a1735f215b2a5cd43c38b79eac072ad197e61be9829905b6b29550abd0db/grpcio-1.83.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:2b5e75c34842cd9c1b95285ca395c6a569664b81e3ffa6b714125922942abaaf", size = 7613472, upload-time = "2026-07-23T15:19:40.645Z" }, { url = "https://files.pythonhosted.org/packages/b2/78/c9e81f806ac704b6b145cb01628db398985b1f8dfdc10e23b55fb0902b3d/grpcio-1.83.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aeb339838db07600481ef869507279b75326c75eac6d10f7afa62a0da1d2bcdd", size = 7040616, upload-time = "2026-07-23T15:19:42.349Z" }, { url = "https://files.pythonhosted.org/packages/9a/ba/94cd5af859876049d340480acbb61a959096c84b567f215534faa78d0424/grpcio-1.83.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f47d62808b4c0a97b78bff88a6d4ca283a2a492b9a04a87d814af95ca3b9c19c", size = 7570491, upload-time = "2026-07-23T15:19:44.357Z" }, { url = "https://files.pythonhosted.org/packages/3e/15/108d30d5a5c964312ae8b9cb0e8cc5b3c1cc68d8f757cca52b3565534d26/grpcio-1.83.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:62003babc444a606dcd1f009cd16391ce23669ae4ad6ec267a873da7937a69f5", size = 8605036, upload-time = "2026-07-23T15:19:46.454Z" }, { url = "https://files.pythonhosted.org/packages/ea/23/3828ae13c3db8233d123ad612747665817b952d8a954f32390230b582336/grpcio-1.83.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1aa567f8c3f19850ffd5d2858c9a8ea7c80f0db6c01186b71eb31e923ec984f5", size = 7981587, upload-time = "2026-07-23T15:19:48.913Z" }, { url = "https://files.pythonhosted.org/packages/17/5b/77af31228f55f55a2a5112bb0077ad0a1c4d23dbb0c2853a62475bbdcc14/grpcio-1.83.0-cp312-cp312-win32.whl", hash = "sha256:cb2906c61db4f9c64cc360054b5df70eeb81846228e9e56a4944bd415a63dadc", size = 4394004, upload-time = "2026-07-23T15:19:50.618Z" }, { url = "https://files.pythonhosted.org/packages/c0/da/f706e39550e7a3732ce2b9c5926107a93d74a802775b19b642a6df27dc96/grpcio-1.83.0-cp312-cp312-win_amd64.whl", hash = "sha256:1c699bbb20f143c8f2bff219de578aa2dc1f919399d67dc702b038b986ee62df", size = 5158525, upload-time = "2026-07-23T15:19:52.246Z" }, { url = "https://files.pythonhosted.org/packages/56/eb/135daaa713f32d33b8f99b4153b3f8dc3b2a124996ac15581bf9ebdad3c3/grpcio-1.83.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:6662f3b1e07cc7493d437351860dc867bddc6a93c83ecf33bbfdaf0c217ab2d0", size = 6304480, upload-time = "2026-07-23T15:19:53.962Z" }, { url = "https://files.pythonhosted.org/packages/8f/a1/121806ce69f23138dabe06aa595b0e5f1ae051a37e4c1954eed7d692c800/grpcio-1.83.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:74fe6f9e8a35c7dbf32255ee154d15e3e5338a81ed39173d079d594d2e544cd1", size = 12154419, upload-time = "2026-07-23T15:19:56.3Z" }, { url = "https://files.pythonhosted.org/packages/b0/e8/d0389e09cd6b4c4d3089b92967ae4e3ffd64795bd349bf2f85cd6656d3da/grpcio-1.83.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:10b3fa0475eb572c9a81a6fe37fa16a9c500c0c91cfc148cac15692b7e3c2867", size = 6873200, upload-time = "2026-07-23T15:19:58.701Z" }, { url = "https://files.pythonhosted.org/packages/f8/51/f464c1d211fa50d5adbabe1b2e519948d99c13757052bfc9ea7afa28e284/grpcio-1.83.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:5f20a988480b0f28207f057f7f7ae1313393c3cef0adcfeae8248f9947eaf881", size = 7618811, upload-time = "2026-07-23T15:20:00.733Z" }, { url = "https://files.pythonhosted.org/packages/e8/c0/539fe0832f2dd6500a28f5263071623fb34e8d4867aec632ccf81bd21156/grpcio-1.83.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7bd82671b39065ba18cd536e9cd45b27ff649053f81ddd2c6a966d595067080f", size = 7042310, upload-time = "2026-07-23T15:20:02.675Z" }, { url = "https://files.pythonhosted.org/packages/8c/ca/ccf617d37ffa72567fa8e005ec7090c99da922799be2fb9847c8b21ca18c/grpcio-1.83.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bc60215b5cb9fc8ca72942c498b551ac2305bd08f6ef8d4e3f0d21b64fbecd61", size = 7575412, upload-time = "2026-07-23T15:20:04.712Z" }, { url = "https://files.pythonhosted.org/packages/eb/b9/fd8d5245f823a8e0fd35d90e20ea3aa4acd47f8d5318fa8df307df52dec6/grpcio-1.83.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:f1c3e5689d4b90987b1d72022bcfe866a9a3dc66197484cf856d96b6150e7f45", size = 8604248, upload-time = "2026-07-23T15:20:06.77Z" }, { url = "https://files.pythonhosted.org/packages/14/1e/f37632fc11db72dfa4bba86c3a43e54358e53030df111ecae5e91a733ad6/grpcio-1.83.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a21cb4eeeba124443f399be2e8b624943cde864dcbe588cb42e5c483a52a906c", size = 7977458, upload-time = "2026-07-23T15:20:09.109Z" }, { url = "https://files.pythonhosted.org/packages/93/b6/d70b69ae5c0cfc341b9ba474980e4ed99cbf05c0e4a14e9eee8cb73db0a5/grpcio-1.83.0-cp313-cp313-win32.whl", hash = "sha256:8fe04f1050a59f875601eb55d42b4f66946fe89817f967e34db1462ccd07dadf", size = 4393993, upload-time = "2026-07-23T15:20:11.017Z" }, { url = "https://files.pythonhosted.org/packages/0f/13/45d4cccb555cf4c476226979bf3d2fd0b0254216f7564c3a053e35117efc/grpcio-1.83.0-cp313-cp313-win_amd64.whl", hash = "sha256:6e01ecd9d8ef280abe1365138a4dc318f9a5287f4cb1b41d07816f796653f735", size = 5159650, upload-time = "2026-07-23T15:20:12.979Z" }, { url = "https://files.pythonhosted.org/packages/9c/60/f2cca8147ea213d3e43ae9158d03ad04e020fdf32ff027253e1fe93f921d/grpcio-1.83.0-cp314-cp314-linux_armv7l.whl", hash = "sha256:3f351629f6ae16ecc0ec3553e586a6763ffd9f6114044286d0cbec3e09241bfa", size = 6305607, upload-time = "2026-07-23T15:20:15.353Z" }, { url = "https://files.pythonhosted.org/packages/d0/ab/d3874931d123a95e83a3ebf8aa04537988fb62425cedb8bf3cefc5ad41b2/grpcio-1.83.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:d05ff664100d429335b93c91b8b34ddf9e94a112205e7fa06dede309e44a4e4c", size = 12166617, upload-time = "2026-07-23T15:20:17.435Z" }, { url = "https://files.pythonhosted.org/packages/92/ff/6f18f9426b69306f4e00a9add3b0ee2748da8aad53836ef80cab0d62d04f/grpcio-1.83.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7936f2a56cf04f6514705c0fedf400971de01b6aa1719327e4718f410a765e2b", size = 6880213, upload-time = "2026-07-23T15:20:19.98Z" }, { url = "https://files.pythonhosted.org/packages/70/21/706d1147c6b93b98f179240c13991fbcc56880eba0c868abb1ad40d8a0a6/grpcio-1.83.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:b0a0be840e51b6b7ee9df9269770faf77bdf4b771053c257c21d12bad607714c", size = 7618335, upload-time = "2026-07-23T15:20:22.161Z" }, { url = "https://files.pythonhosted.org/packages/74/04/1a8443c889115ec9e213a213e86bc93a71ee9088027e5befa09aaa0edd9d/grpcio-1.83.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:009667eaf3dcd5224c713589cdc98e7ca4ed0ff0b61132c6b276e930eb83a2df", size = 7043416, upload-time = "2026-07-23T15:20:24.209Z" }, { url = "https://files.pythonhosted.org/packages/86/c6/94e0fee5b12bc1da1370185b680988db6f739d19b42d9959db01a7ea50bf/grpcio-1.83.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:bb669918fd88936b15599caff4160a77ab74bdeb25f2231f6e45b61282d6107b", size = 7583253, upload-time = "2026-07-23T15:20:26.313Z" }, { url = "https://files.pythonhosted.org/packages/a0/97/de1ccb671fb85575bc5192faedf9ecdbdf5b390d2e6584dcf552bcbd370e/grpcio-1.83.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:c19b454d3d3f28db81f2c7c4dbaee96e7f6fd149721733ffe79d6bc530f17404", size = 8605102, upload-time = "2026-07-23T15:20:28.437Z" }, { url = "https://files.pythonhosted.org/packages/17/0f/0e0ec749a7034ffcbaa050e39779872950ead90c22e7e0116be3f28b2b46/grpcio-1.83.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:61007cd08640abc5c54547ee32505474c482cd733a53cb87551ea81faa6350af", size = 7979826, upload-time = "2026-07-23T15:20:31.182Z" }, { url = "https://files.pythonhosted.org/packages/83/fa/c3fda157287f64bc65acee6c5aa90c41acf9e0d3a8e69a265eecff6d00a1/grpcio-1.83.0-cp314-cp314-win32.whl", hash = "sha256:32e11c37f5285b0c6fa3042c05fe06903696689749833fc64e67dec71b9bbe33", size = 4471765, upload-time = "2026-07-23T15:20:33.195Z" }, { url = "https://files.pythonhosted.org/packages/a1/00/b1b26431c9d54eee11724fd6e5585473a2ed47fbc1fb95e5204906a642ce/grpcio-1.83.0-cp314-cp314-win_amd64.whl", hash = "sha256:2bb48cb5e6dd005ca12b89ce4b6ac0b48ff3112c747542ee7986ef611a8ca6d9", size = 5298932, upload-time = "2026-07-23T15:20:35.48Z" }, ] [[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 = "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 = "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 = "inflect" version = "7.5.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "more-itertools" }, { name = "typeguard" }, ] sdist = { url = "https://files.pythonhosted.org/packages/78/c6/943357d44a21fd995723d07ccaddd78023eace03c1846049a2645d4324a3/inflect-7.5.0.tar.gz", hash = "sha256:faf19801c3742ed5a05a8ce388e0d8fe1a07f8d095c82201eb904f5d27ad571f", size = 73751, upload-time = "2024-12-28T17:11:18.897Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/8a/eb/427ed2b20a38a4ee29f24dbe4ae2dafab198674fe9a85e3d6adf9e5f5f41/inflect-7.5.0-py3-none-any.whl", hash = "sha256:2aea70e5e70c35d8350b8097396ec155ffd68def678c7ff97f51aa69c1d92344", size = 35197, upload-time = "2024-12-28T17:11:15.931Z" }, ] [[package]] name = "isort" version = "8.0.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/ef/7c/ec4ab396d31b3b395e2e999c8f46dec78c5e29209fac49d1f4dace04041d/isort-8.0.1.tar.gz", hash = "sha256:171ac4ff559cdc060bcfff550bc8404a486fee0caab245679c2abe7cb253c78d", size = 769592, upload-time = "2026-02-28T10:08:20.685Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/3e/95/c7c34aa53c16353c56d0b802fba48d5f5caa2cdee7958acbcb795c830416/isort-8.0.1-py3-none-any.whl", hash = "sha256:28b89bc70f751b559aeca209e6120393d43fbe2490de0559662be7a9787e3d75", size = 89733, upload-time = "2026-02-28T10:08:19.466Z" }, ] [[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 = "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 = "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 = "more-itertools" version = "11.1.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/de/1d/f4da6f02cdffe04d6362210b807146a26044c88d839208aec273bb0d9184/more_itertools-11.1.0.tar.gz", hash = "sha256:48e8f4d9e7e5878571ecf6f2b4e57634f93cd474cc8cfbd2376f2d11b396e30d", size = 145772, upload-time = "2026-05-22T14:14:29.909Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/e8/3d/1087453384dbde46a8c7f9356eead2c58be8a7bf156bca40243377c85715/more_itertools-11.1.0-py3-none-any.whl", hash = "sha256:4b65538ae22f6fed0ce4874efd317463a7489796a0939fa66824dd542125a192", size = 72226, upload-time = "2026-05-22T14:14:28.824Z" }, ] [[package]] name = "mypy-extensions" version = "1.1.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, ] [[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 = "opentelemetry-api" source = { editable = "opentelemetry-api" } dependencies = [ { name = "typing-extensions" }, ] [package.metadata] requires-dist = [{ name = "typing-extensions", specifier = ">=4.5.0" }] [[package]] name = "opentelemetry-codegen-json" version = "0.0.0" source = { editable = "codegen/opentelemetry-codegen-json" } dependencies = [ { name = "protobuf" }, { name = "types-protobuf" }, ] [package.metadata] requires-dist = [ { name = "protobuf", specifier = ">=4.25.3" }, { name = "types-protobuf", specifier = ">=4.24" }, ] [[package]] name = "opentelemetry-configuration" source = { editable = "opentelemetry-configuration" } dependencies = [ { name = "jsonschema" }, { name = "opentelemetry-api" }, { name = "opentelemetry-sdk" }, { name = "pyyaml" }, ] [package.metadata] requires-dist = [ { name = "jsonschema", specifier = ">=4.0" }, { name = "opentelemetry-api", editable = "opentelemetry-api" }, { name = "opentelemetry-sdk", editable = "opentelemetry-sdk" }, { name = "pyyaml", specifier = ">=5.4" }, ] [[package]] name = "opentelemetry-exporter-credential-provider-gcp" version = "0.65b0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "google-auth" }, { name = "grpcio" }, { name = "requests" }, ] sdist = { url = "https://files.pythonhosted.org/packages/07/b3/47849add32e1bb8265c830b64d24b402e350c75fb67e4aa28d7e493aff43/opentelemetry_exporter_credential_provider_gcp-0.65b0.tar.gz", hash = "sha256:4715920a33e816ddcff1d086cf8f679e43fa1ef0611c0eb951168077dc49db38", size = 7130, upload-time = "2026-07-16T15:25:48.498Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/75/0a/f2ec7a626f9d693e6cc8ddf002fbbcd8256515621f9ce5f061cd2ca6d852/opentelemetry_exporter_credential_provider_gcp-0.65b0-py3-none-any.whl", hash = "sha256:44c6d62b4cc4ca700fd371cd94a2af0359607b612bd759fd5c809b1a644afcd3", size = 7769, upload-time = "2026-07-16T15:24:48.706Z" }, ] [[package]] name = "opentelemetry-exporter-http-transport" source = { editable = "exporter/opentelemetry-exporter-http-transport" } dependencies = [ { name = "opentelemetry-api" }, ] [package.optional-dependencies] requests = [ { name = "requests" }, ] urllib3 = [ { name = "urllib3" }, ] [package.metadata] requires-dist = [ { name = "opentelemetry-api", editable = "opentelemetry-api" }, { name = "requests", marker = "extra == 'requests'", specifier = "~=2.25" }, { name = "urllib3", marker = "extra == 'urllib3'", specifier = ">=1.26" }, ] provides-extras = ["requests", "urllib3"] [[package]] name = "opentelemetry-exporter-otlp" source = { editable = "exporter/opentelemetry-exporter-otlp" } dependencies = [ { name = "opentelemetry-exporter-otlp-proto-grpc" }, { name = "opentelemetry-exporter-otlp-proto-http" }, ] [package.metadata] requires-dist = [ { name = "opentelemetry-exporter-otlp-proto-grpc", editable = "exporter/opentelemetry-exporter-otlp-proto-grpc" }, { name = "opentelemetry-exporter-otlp-proto-http", editable = "exporter/opentelemetry-exporter-otlp-proto-http" }, ] [[package]] name = "opentelemetry-exporter-otlp-common" source = { editable = "exporter/opentelemetry-exporter-otlp-common" } dependencies = [ { name = "opentelemetry-sdk" }, ] [package.optional-dependencies] http = [ { name = "opentelemetry-exporter-http-transport" }, ] [package.metadata] requires-dist = [ { name = "opentelemetry-exporter-http-transport", marker = "extra == 'http'", editable = "exporter/opentelemetry-exporter-http-transport" }, { name = "opentelemetry-sdk", editable = "opentelemetry-sdk" }, ] provides-extras = ["http"] [[package]] name = "opentelemetry-exporter-otlp-json-common" source = { editable = "exporter/opentelemetry-exporter-otlp-json-common" } dependencies = [ { name = "opentelemetry-proto-json" }, { name = "opentelemetry-sdk" }, ] [package.metadata] requires-dist = [ { name = "opentelemetry-proto-json", editable = "opentelemetry-proto-json" }, { name = "opentelemetry-sdk", editable = "opentelemetry-sdk" }, ] [[package]] name = "opentelemetry-exporter-otlp-json-file" source = { editable = "exporter/opentelemetry-exporter-otlp-json-file" } dependencies = [ { name = "opentelemetry-exporter-otlp-common" }, { name = "opentelemetry-exporter-otlp-json-common" }, { name = "opentelemetry-sdk" }, ] [package.metadata] requires-dist = [ { name = "opentelemetry-exporter-otlp-common", editable = "exporter/opentelemetry-exporter-otlp-common" }, { name = "opentelemetry-exporter-otlp-json-common", editable = "exporter/opentelemetry-exporter-otlp-json-common" }, { name = "opentelemetry-sdk", editable = "opentelemetry-sdk" }, ] [[package]] name = "opentelemetry-exporter-otlp-json-http" source = { editable = "exporter/opentelemetry-exporter-otlp-json-http" } dependencies = [ { name = "opentelemetry-exporter-http-transport", extra = ["urllib3"] }, { name = "opentelemetry-exporter-otlp-common" }, { name = "opentelemetry-exporter-otlp-json-common" }, { name = "opentelemetry-proto-json" }, { name = "opentelemetry-sdk" }, ] [package.metadata] requires-dist = [ { name = "opentelemetry-exporter-http-transport", extras = ["urllib3"], editable = "exporter/opentelemetry-exporter-http-transport" }, { name = "opentelemetry-exporter-otlp-common", editable = "exporter/opentelemetry-exporter-otlp-common" }, { name = "opentelemetry-exporter-otlp-json-common", editable = "exporter/opentelemetry-exporter-otlp-json-common" }, { name = "opentelemetry-proto-json", editable = "opentelemetry-proto-json" }, { name = "opentelemetry-sdk", editable = "opentelemetry-sdk" }, ] [[package]] name = "opentelemetry-exporter-otlp-proto-common" source = { editable = "exporter/opentelemetry-exporter-otlp-proto-common" } dependencies = [ { name = "opentelemetry-proto" }, ] [package.metadata] requires-dist = [{ name = "opentelemetry-proto", editable = "opentelemetry-proto" }] [[package]] name = "opentelemetry-exporter-otlp-proto-grpc" source = { editable = "exporter/opentelemetry-exporter-otlp-proto-grpc" } dependencies = [ { name = "googleapis-common-protos" }, { name = "grpcio" }, { name = "opentelemetry-api" }, { name = "opentelemetry-exporter-otlp-common" }, { name = "opentelemetry-exporter-otlp-proto-common" }, { name = "opentelemetry-proto" }, { name = "opentelemetry-sdk" }, { name = "typing-extensions" }, ] [package.optional-dependencies] gcp-auth = [ { name = "opentelemetry-exporter-credential-provider-gcp" }, ] [package.metadata] requires-dist = [ { name = "googleapis-common-protos", specifier = "~=1.57" }, { name = "grpcio", marker = "python_full_version < '3.13'", specifier = ">=1.63.2,<2.0.0" }, { name = "grpcio", marker = "python_full_version == '3.13.*'", specifier = ">=1.66.2,<2.0.0" }, { name = "grpcio", marker = "python_full_version >= '3.14'", specifier = ">=1.75.1,<2.0.0" }, { name = "opentelemetry-api", editable = "opentelemetry-api" }, { name = "opentelemetry-exporter-credential-provider-gcp", marker = "extra == 'gcp-auth'", specifier = ">=0.59b0" }, { name = "opentelemetry-exporter-otlp-common", editable = "exporter/opentelemetry-exporter-otlp-common" }, { name = "opentelemetry-exporter-otlp-proto-common", editable = "exporter/opentelemetry-exporter-otlp-proto-common" }, { name = "opentelemetry-proto", editable = "opentelemetry-proto" }, { name = "opentelemetry-sdk", editable = "opentelemetry-sdk" }, { name = "typing-extensions", specifier = ">=4.6.0" }, ] provides-extras = ["gcp-auth"] [[package]] name = "opentelemetry-exporter-otlp-proto-http" source = { editable = "exporter/opentelemetry-exporter-otlp-proto-http" } dependencies = [ { name = "googleapis-common-protos" }, { name = "opentelemetry-api" }, { name = "opentelemetry-exporter-http-transport", extra = ["requests"] }, { name = "opentelemetry-exporter-otlp-common" }, { name = "opentelemetry-exporter-otlp-proto-common" }, { name = "opentelemetry-proto" }, { name = "opentelemetry-sdk" }, { name = "requests" }, { name = "typing-extensions" }, ] [package.optional-dependencies] gcp-auth = [ { name = "opentelemetry-exporter-credential-provider-gcp" }, ] requests = [ { name = "opentelemetry-exporter-http-transport", extra = ["requests"] }, { name = "requests" }, ] [package.metadata] requires-dist = [ { name = "googleapis-common-protos", specifier = "~=1.52" }, { name = "opentelemetry-api", editable = "opentelemetry-api" }, { name = "opentelemetry-exporter-credential-provider-gcp", marker = "extra == 'gcp-auth'", specifier = ">=0.59b0" }, { name = "opentelemetry-exporter-http-transport", extras = ["requests"], editable = "exporter/opentelemetry-exporter-http-transport" }, { name = "opentelemetry-exporter-http-transport", extras = ["requests"], marker = "extra == 'requests'", editable = "exporter/opentelemetry-exporter-http-transport" }, { name = "opentelemetry-exporter-otlp-common", editable = "exporter/opentelemetry-exporter-otlp-common" }, { name = "opentelemetry-exporter-otlp-proto-common", editable = "exporter/opentelemetry-exporter-otlp-proto-common" }, { name = "opentelemetry-proto", editable = "opentelemetry-proto" }, { name = "opentelemetry-sdk", editable = "opentelemetry-sdk" }, { name = "requests", specifier = "~=2.7" }, { name = "requests", marker = "extra == 'requests'", specifier = "~=2.7" }, { name = "typing-extensions", specifier = ">=4.5.0" }, ] provides-extras = ["gcp-auth", "requests"] [[package]] name = "opentelemetry-exporter-prometheus" source = { editable = "exporter/opentelemetry-exporter-prometheus" } dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-sdk" }, { name = "prometheus-client" }, ] [package.metadata] requires-dist = [ { name = "opentelemetry-api", editable = "opentelemetry-api" }, { name = "opentelemetry-sdk", editable = "opentelemetry-sdk" }, { name = "prometheus-client", specifier = ">=0.5.0,<1.0.0" }, ] [[package]] name = "opentelemetry-exporter-zipkin-json" source = { editable = "exporter/opentelemetry-exporter-zipkin-json" } dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-sdk" }, { name = "requests" }, ] [package.metadata] requires-dist = [ { name = "opentelemetry-api", editable = "opentelemetry-api" }, { name = "opentelemetry-sdk", editable = "opentelemetry-sdk" }, { name = "requests", specifier = "~=2.7" }, ] [[package]] name = "opentelemetry-propagator-b3" source = { editable = "propagator/opentelemetry-propagator-b3" } dependencies = [ { name = "opentelemetry-api" }, { name = "typing-extensions" }, ] [package.metadata] requires-dist = [ { name = "opentelemetry-api", editable = "opentelemetry-api" }, { name = "typing-extensions", specifier = ">=4.5.0" }, ] [[package]] name = "opentelemetry-propagator-jaeger" source = { editable = "propagator/opentelemetry-propagator-jaeger" } dependencies = [ { name = "opentelemetry-api" }, ] [package.metadata] requires-dist = [{ name = "opentelemetry-api", editable = "opentelemetry-api" }] [[package]] name = "opentelemetry-proto" source = { editable = "opentelemetry-proto" } dependencies = [ { name = "protobuf" }, ] [package.metadata] requires-dist = [{ name = "protobuf", specifier = ">=5.0,<8.0" }] [[package]] name = "opentelemetry-proto-json" source = { editable = "opentelemetry-proto-json" } [[package]] name = "opentelemetry-python" version = "0.0.0" source = { virtual = "." } dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-codegen-json" }, { name = "opentelemetry-configuration" }, { name = "opentelemetry-exporter-http-transport" }, { name = "opentelemetry-exporter-otlp-common" }, { name = "opentelemetry-exporter-otlp-json-common" }, { name = "opentelemetry-exporter-otlp-json-file" }, { name = "opentelemetry-exporter-otlp-json-http" }, { name = "opentelemetry-exporter-otlp-proto-common" }, { name = "opentelemetry-exporter-otlp-proto-grpc" }, { name = "opentelemetry-exporter-otlp-proto-http" }, { name = "opentelemetry-exporter-prometheus" }, { name = "opentelemetry-exporter-zipkin-json" }, { name = "opentelemetry-propagator-b3" }, { name = "opentelemetry-propagator-jaeger" }, { name = "opentelemetry-proto" }, { name = "opentelemetry-proto-json" }, { name = "opentelemetry-sdk" }, { name = "opentelemetry-semantic-conventions" }, { name = "opentelemetry-test-utils" }, ] [package.dev-dependencies] dev = [ { name = "datamodel-code-generator", extra = ["http", "ruff"] }, { name = "pre-commit" }, { name = "towncrier" }, { name = "tox" }, { name = "tox-uv" }, ] [package.metadata] requires-dist = [ { name = "opentelemetry-api", editable = "opentelemetry-api" }, { name = "opentelemetry-codegen-json", editable = "codegen/opentelemetry-codegen-json" }, { name = "opentelemetry-configuration", editable = "opentelemetry-configuration" }, { name = "opentelemetry-exporter-http-transport", editable = "exporter/opentelemetry-exporter-http-transport" }, { name = "opentelemetry-exporter-otlp-common", editable = "exporter/opentelemetry-exporter-otlp-common" }, { name = "opentelemetry-exporter-otlp-json-common", editable = "exporter/opentelemetry-exporter-otlp-json-common" }, { name = "opentelemetry-exporter-otlp-json-file", editable = "exporter/opentelemetry-exporter-otlp-json-file" }, { name = "opentelemetry-exporter-otlp-json-http", editable = "exporter/opentelemetry-exporter-otlp-json-http" }, { name = "opentelemetry-exporter-otlp-proto-common", editable = "exporter/opentelemetry-exporter-otlp-proto-common" }, { name = "opentelemetry-exporter-otlp-proto-grpc", editable = "exporter/opentelemetry-exporter-otlp-proto-grpc" }, { name = "opentelemetry-exporter-otlp-proto-http", editable = "exporter/opentelemetry-exporter-otlp-proto-http" }, { name = "opentelemetry-exporter-prometheus", editable = "exporter/opentelemetry-exporter-prometheus" }, { name = "opentelemetry-exporter-zipkin-json", editable = "exporter/opentelemetry-exporter-zipkin-json" }, { name = "opentelemetry-propagator-b3", editable = "propagator/opentelemetry-propagator-b3" }, { name = "opentelemetry-propagator-jaeger", editable = "propagator/opentelemetry-propagator-jaeger" }, { name = "opentelemetry-proto", editable = "opentelemetry-proto" }, { name = "opentelemetry-proto-json", editable = "opentelemetry-proto-json" }, { name = "opentelemetry-sdk", editable = "opentelemetry-sdk" }, { name = "opentelemetry-semantic-conventions", editable = "opentelemetry-semantic-conventions" }, { name = "opentelemetry-test-utils", editable = "tests/opentelemetry-test-utils" }, ] [package.metadata.requires-dev] dev = [ { name = "datamodel-code-generator", extras = ["http"] }, { name = "datamodel-code-generator", extras = ["ruff"] }, { name = "pre-commit" }, { name = "towncrier" }, { name = "tox" }, { name = "tox-uv", specifier = ">=1" }, ] [[package]] name = "opentelemetry-sdk" source = { editable = "opentelemetry-sdk" } dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-semantic-conventions" }, { name = "typing-extensions" }, ] [package.optional-dependencies] file-configuration = [ { name = "opentelemetry-configuration" }, ] [package.metadata] requires-dist = [ { name = "opentelemetry-api", editable = "opentelemetry-api" }, { name = "opentelemetry-configuration", marker = "extra == 'file-configuration'", editable = "opentelemetry-configuration" }, { name = "opentelemetry-semantic-conventions", editable = "opentelemetry-semantic-conventions" }, { name = "typing-extensions", specifier = ">=4.5.0" }, ] provides-extras = ["file-configuration"] [[package]] name = "opentelemetry-semantic-conventions" source = { editable = "opentelemetry-semantic-conventions" } dependencies = [ { name = "opentelemetry-api" }, { name = "typing-extensions" }, ] [package.metadata] requires-dist = [ { name = "opentelemetry-api", editable = "opentelemetry-api" }, { name = "typing-extensions", specifier = ">=4.5.0" }, ] [[package]] name = "opentelemetry-test-utils" source = { editable = "tests/opentelemetry-test-utils" } dependencies = [ { name = "asgiref" }, { name = "opentelemetry-api" }, { name = "opentelemetry-sdk" }, { name = "requests" }, ] [package.metadata] requires-dist = [ { name = "asgiref", specifier = "~=3.0" }, { name = "opentelemetry-api", editable = "opentelemetry-api" }, { name = "opentelemetry-sdk", editable = "opentelemetry-sdk" }, { name = "requests", specifier = "~=2.28" }, ] [[package]] name = "packaging" version = "26.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/7d/fa/3944b40b07da9ce895c0e6303a5ab7d53da063554f534556b134a54d6093/packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79", size = 313412, upload-time = "2026-08-04T18:15:28.737Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956, upload-time = "2026-08-04T18:15:27.159Z" }, ] [[package]] name = "pathspec" version = "1.1.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/5a/82/42f767fc1c1143d6fd36efb827202a2d997a375e160a71eb2888a925aac1/pathspec-1.1.1.tar.gz", hash = "sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a", size = 135180, upload-time = "2026-04-27T01:46:08.907Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189", size = 57328, upload-time = "2026-04-27T01:46:07.06Z" }, ] [[package]] name = "platformdirs" version = "4.11.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/78/9b/560e4be8e26f6fd133a03630a8df0c663b9e8d61b4ade152b72005aec83b/platformdirs-4.11.0.tar.gz", hash = "sha256:0555d18370482847566ffabcaa53ad7c6c1c29f195989ae1ed634a05f76ea1e0", size = 31953, upload-time = "2026-07-21T13:09:36.565Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/7d/68/d8d58938dfb1370b266a1a729e6d77a985be23689a0496498ee17b2cbf90/platformdirs-4.11.0-py3-none-any.whl", hash = "sha256:360ccded2b7fce0af0ff80cc8f5942a1c5d99b0e856033acb030bfc634709e74", size = 23247, upload-time = "2026-07-21T13:09:35.422Z" }, ] [[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.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cfgv" }, { name = "identify" }, { name = "nodeenv" }, { name = "pyyaml" }, { name = "virtualenv" }, ] sdist = { url = "https://files.pythonhosted.org/packages/25/3a/ddb78f32a0814e66b18a099377a106a2dcdce92d86a034d69d65df9b256e/pre_commit-4.6.1.tar.gz", hash = "sha256:03e809865c7d178b9979d06c761fcbfe6808fdaded8581a745bb110e52050421", size = 198646, upload-time = "2026-07-21T20:56:58.225Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/fb/49/bc925106abcdac498074f2cbe6137e94e09f418dd2b7775df5b577dc0313/pre_commit-4.6.1-py2.py3-none-any.whl", hash = "sha256:0e3b2942510d1fb34eec167a3ec57331bf8442122f1153a9fb8b58f5c49b2717", size = 226186, upload-time = "2026-07-21T20:56:57.064Z" }, ] [[package]] name = "prometheus-client" version = "0.26.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/52/73/f1334c29c2af4cd9dba6c7817e61b611bd0215e2eb5565c6064a4de18802/prometheus_client-0.26.0.tar.gz", hash = "sha256:04a91bcf94e2cf74a44a1a874d651a2e853ed354b6e822f3b7487751465d5c2b", size = 92910, upload-time = "2026-07-24T19:36:41.893Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/eb/a3/b69efbf4143b5b9859b977770bbbabcc2796b702fa69dc40271e45cd5a56/prometheus_client-0.26.0-py3-none-any.whl", hash = "sha256:fa93d06737aa02bacd05794768508bb97d2fbee28cb3bca04eaae92f0ca953d6", size = 64494, upload-time = "2026-07-24T19:36:40.854Z" }, ] [[package]] name = "protobuf" version = "7.35.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/da/01/9ef0afd7999eb9badb3a768b4aedd78c86d4c65cfaf1958ab276199e76b4/protobuf-7.35.1.tar.gz", hash = "sha256:ce115a26fe0c39a2c29973d914d327e516a6455464489fe3cd1e51a1b354f81a", size = 458717, upload-time = "2026-06-11T21:55:40.257Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/10/03/8aeeb7458d22546bf64b5250ca1daeb5ff757d900e8e4a7476c6f0db843e/protobuf-7.35.1-cp310-abi3-macosx_10_9_universal2.whl", hash = "sha256:24f857477359a85c0c235261b8ba905fd51b2562f4a64ca1df5473f29850cbf6", size = 433226, upload-time = "2026-06-11T21:55:31.719Z" }, { url = "https://files.pythonhosted.org/packages/37/4b/dfb89eb0e652a1ff073c39a59fb5e3a83cfe9b57a2c83fa6d78270101767/protobuf-7.35.1-cp310-abi3-manylinux2014_aarch64.whl", hash = "sha256:11d6b0ec246892d85215b0a13ca6e0233cf5284b68f0ac02646427f4ff88a799", size = 328847, upload-time = "2026-06-11T21:55:34.035Z" }, { url = "https://files.pythonhosted.org/packages/0f/58/dc12f2cd484951524af6e3382c785869b9b3fb5e52ee95ae23add53ee8f9/protobuf-7.35.1-cp310-abi3-manylinux2014_s390x.whl", hash = "sha256:b73f9489a4b8b1c9cb1f8ed951c736392592edb24b9d6819f36d2e10b171d5b4", size = 344030, upload-time = "2026-06-11T21:55:34.941Z" }, { url = "https://files.pythonhosted.org/packages/e4/be/5b3cfe508bfab6761414ff944e3366eb13be4fd71efcd69450f89ba39f43/protobuf-7.35.1-cp310-abi3-manylinux2014_x86_64.whl", hash = "sha256:74758715c53d7158fb76caf4f0cfdacc5329a4b1bb994f865d6cf302d413a1c4", size = 327130, upload-time = "2026-06-11T21:55:35.921Z" }, { url = "https://files.pythonhosted.org/packages/d8/bc/6d6c7ba8709c85f8f2c390b2b118d6fb08a783676a572271851bf45a7d22/protobuf-7.35.1-cp310-abi3-win32.whl", hash = "sha256:353652e4efd0bca5b5fc2656abf8307ef351f0cf938c9eba09f0e09c20a25c30", size = 428945, upload-time = "2026-06-11T21:55:37.034Z" }, { url = "https://files.pythonhosted.org/packages/0a/19/8d0cb6f20a1ef7b18f1c8986ad5783f22f84cce39c6ce9a6e645ea55192e/protobuf-7.35.1-cp310-abi3-win_amd64.whl", hash = "sha256:230a75ddfc2de4806e56696ce9640c1cdfdb6543b7cfce98d42a4c0a0e7bdb87", size = 439996, upload-time = "2026-06-11T21:55:38.123Z" }, { url = "https://files.pythonhosted.org/packages/19/c7/5f7c636ec43e0c545e28d1f1db71990108306f7bdcb89f069ba97e428e7f/protobuf-7.35.1-py3-none-any.whl", hash = "sha256:4bc97768d8fe4ad6743c8a19403e314511ed9f6d13205b687e52421c023ac1b9", size = 171659, upload-time = "2026-06-11T21:55:39.155Z" }, ] [[package]] name = "pyasn1" version = "0.6.4" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/a4/9a/23310166d960def5897e91fe20e5b724601b02a22e84ba1f94232c0b7f67/pyasn1-0.6.4.tar.gz", hash = "sha256:9c447d8431c947fe4c8febc4ed9e760bc29011a5b01e5c74b67025bd9fb8ce81", size = 151262, upload-time = "2026-07-09T01:12:33.988Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/9a/3b/6163796d69c3977d1e4287bea4a6979161cbbdd170ebb430511e8e1999ce/pyasn1-0.6.4-py3-none-any.whl", hash = "sha256:deda9277cfd454080ec40b207fb6df82206a3a2688735233cdcd8d3d565f088b", size = 84410, upload-time = "2026-07-09T01:12:32.92Z" }, ] [[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 = "pyproject-api" version = "1.11.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "packaging" }, { name = "tomli", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ab/bd/2f985c12bf33fdd8637e3a8f9418d6806f177601dee7c4924d0b2bb28650/pyproject_api-1.11.0.tar.gz", hash = "sha256:b8807d85a293e6c9f133e6575946fed45f1d42b22d58c780b33aa2421a799549", size = 23787, upload-time = "2026-07-21T13:09:33.744Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/a3/6a/2822ee12bafe87af8f6cc620f7d1f126e77f82ce56ba888b94a9af5a979c/pyproject_api-1.11.0-py3-none-any.whl", hash = "sha256:860060c8832dce983b5eec6f41c4c43eb3ec06ff7332387a63acdf5ca27b68d8", size = 13275, upload-time = "2026-07-21T13:09:32.559Z" }, ] [[package]] name = "python-discovery" version = "1.5.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "filelock" }, ] sdist = { url = "https://files.pythonhosted.org/packages/04/b7/1581a8103855c43567776aa34135e5ec3c597346c23bfd10c7eb5e0b10a4/python_discovery-1.5.1.tar.gz", hash = "sha256:e2ea8b884cd1701f386eda8cf327b87743f1dc21b7f784470799537d95635384", size = 77200, upload-time = "2026-07-31T22:06:02.48Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/6a/07/a89b539750a159d5101c4eb9fc84e2961f65cefbd5e0b7440b284471c0b0/python_discovery-1.5.1-py3-none-any.whl", hash = "sha256:ac07f44cade589d954e9d6a1e1468539fdddd2cf676beb51da73e0f156b7c932", size = 35752, upload-time = "2026-07-31T22:06:01.116Z" }, ] [[package]] name = "pytokens" version = "0.4.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/b6/34/b4e015b99031667a7b960f888889c5bd34ef585c85e1cb56a594b92836ac/pytokens-0.4.1.tar.gz", hash = "sha256:292052fe80923aae2260c073f822ceba21f3872ced9a68bb7953b348e561179a", size = 23015, upload-time = "2026-01-30T01:03:45.924Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/42/24/f206113e05cb8ef51b3850e7ef88f20da6f4bf932190ceb48bd3da103e10/pytokens-0.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2a44ed93ea23415c54f3face3b65ef2b844d96aeb3455b8a69b3df6beab6acc5", size = 161522, upload-time = "2026-01-30T01:02:50.393Z" }, { url = "https://files.pythonhosted.org/packages/d4/e9/06a6bf1b90c2ed81a9c7d2544232fe5d2891d1cd480e8a1809ca354a8eb2/pytokens-0.4.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:add8bf86b71a5d9fb5b89f023a80b791e04fba57960aa790cc6125f7f1d39dfe", size = 246945, upload-time = "2026-01-30T01:02:52.399Z" }, { url = "https://files.pythonhosted.org/packages/69/66/f6fb1007a4c3d8b682d5d65b7c1fb33257587a5f782647091e3408abe0b8/pytokens-0.4.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:670d286910b531c7b7e3c0b453fd8156f250adb140146d234a82219459b9640c", size = 259525, upload-time = "2026-01-30T01:02:53.737Z" }, { url = "https://files.pythonhosted.org/packages/04/92/086f89b4d622a18418bac74ab5db7f68cf0c21cf7cc92de6c7b919d76c88/pytokens-0.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:4e691d7f5186bd2842c14813f79f8884bb03f5995f0575272009982c5ac6c0f7", size = 262693, upload-time = "2026-01-30T01:02:54.871Z" }, { url = "https://files.pythonhosted.org/packages/b4/7b/8b31c347cf94a3f900bdde750b2e9131575a61fdb620d3d3c75832262137/pytokens-0.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:27b83ad28825978742beef057bfe406ad6ed524b2d28c252c5de7b4a6dd48fa2", size = 103567, upload-time = "2026-01-30T01:02:56.414Z" }, { url = "https://files.pythonhosted.org/packages/3d/92/790ebe03f07b57e53b10884c329b9a1a308648fc083a6d4a39a10a28c8fc/pytokens-0.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d70e77c55ae8380c91c0c18dea05951482e263982911fc7410b1ffd1dadd3440", size = 160864, upload-time = "2026-01-30T01:02:57.882Z" }, { url = "https://files.pythonhosted.org/packages/13/25/a4f555281d975bfdd1eba731450e2fe3a95870274da73fb12c40aeae7625/pytokens-0.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a58d057208cb9075c144950d789511220b07636dd2e4708d5645d24de666bdc", size = 248565, upload-time = "2026-01-30T01:02:59.912Z" }, { url = "https://files.pythonhosted.org/packages/17/50/bc0394b4ad5b1601be22fa43652173d47e4c9efbf0044c62e9a59b747c56/pytokens-0.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b49750419d300e2b5a3813cf229d4e5a4c728dae470bcc89867a9ad6f25a722d", size = 260824, upload-time = "2026-01-30T01:03:01.471Z" }, { url = "https://files.pythonhosted.org/packages/4e/54/3e04f9d92a4be4fc6c80016bc396b923d2a6933ae94b5f557c939c460ee0/pytokens-0.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d9907d61f15bf7261d7e775bd5d7ee4d2930e04424bab1972591918497623a16", size = 264075, upload-time = "2026-01-30T01:03:04.143Z" }, { url = "https://files.pythonhosted.org/packages/d1/1b/44b0326cb5470a4375f37988aea5d61b5cc52407143303015ebee94abfd6/pytokens-0.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:ee44d0f85b803321710f9239f335aafe16553b39106384cef8e6de40cb4ef2f6", size = 103323, upload-time = "2026-01-30T01:03:05.412Z" }, { url = "https://files.pythonhosted.org/packages/41/5d/e44573011401fb82e9d51e97f1290ceb377800fb4eed650b96f4753b499c/pytokens-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:140709331e846b728475786df8aeb27d24f48cbcf7bcd449f8de75cae7a45083", size = 160663, upload-time = "2026-01-30T01:03:06.473Z" }, { url = "https://files.pythonhosted.org/packages/f0/e6/5bbc3019f8e6f21d09c41f8b8654536117e5e211a85d89212d59cbdab381/pytokens-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d6c4268598f762bc8e91f5dbf2ab2f61f7b95bdc07953b602db879b3c8c18e1", size = 255626, upload-time = "2026-01-30T01:03:08.177Z" }, { url = "https://files.pythonhosted.org/packages/bf/3c/2d5297d82286f6f3d92770289fd439956b201c0a4fc7e72efb9b2293758e/pytokens-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:24afde1f53d95348b5a0eb19488661147285ca4dd7ed752bbc3e1c6242a304d1", size = 269779, upload-time = "2026-01-30T01:03:09.756Z" }, { url = "https://files.pythonhosted.org/packages/20/01/7436e9ad693cebda0551203e0bf28f7669976c60ad07d6402098208476de/pytokens-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5ad948d085ed6c16413eb5fec6b3e02fa00dc29a2534f088d3302c47eb59adf9", size = 268076, upload-time = "2026-01-30T01:03:10.957Z" }, { url = "https://files.pythonhosted.org/packages/2e/df/533c82a3c752ba13ae7ef238b7f8cdd272cf1475f03c63ac6cf3fcfb00b6/pytokens-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:3f901fe783e06e48e8cbdc82d631fca8f118333798193e026a50ce1b3757ea68", size = 103552, upload-time = "2026-01-30T01:03:12.066Z" }, { url = "https://files.pythonhosted.org/packages/cb/dc/08b1a080372afda3cceb4f3c0a7ba2bde9d6a5241f1edb02a22a019ee147/pytokens-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8bdb9d0ce90cbf99c525e75a2fa415144fd570a1ba987380190e8b786bc6ef9b", size = 160720, upload-time = "2026-01-30T01:03:13.843Z" }, { url = "https://files.pythonhosted.org/packages/64/0c/41ea22205da480837a700e395507e6a24425151dfb7ead73343d6e2d7ffe/pytokens-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5502408cab1cb18e128570f8d598981c68a50d0cbd7c61312a90507cd3a1276f", size = 254204, upload-time = "2026-01-30T01:03:14.886Z" }, { url = "https://files.pythonhosted.org/packages/e0/d2/afe5c7f8607018beb99971489dbb846508f1b8f351fcefc225fcf4b2adc0/pytokens-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:29d1d8fb1030af4d231789959f21821ab6325e463f0503a61d204343c9b355d1", size = 268423, upload-time = "2026-01-30T01:03:15.936Z" }, { url = "https://files.pythonhosted.org/packages/68/d4/00ffdbd370410c04e9591da9220a68dc1693ef7499173eb3e30d06e05ed1/pytokens-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:970b08dd6b86058b6dc07efe9e98414f5102974716232d10f32ff39701e841c4", size = 266859, upload-time = "2026-01-30T01:03:17.458Z" }, { url = "https://files.pythonhosted.org/packages/a7/c9/c3161313b4ca0c601eeefabd3d3b576edaa9afdefd32da97210700e47652/pytokens-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:9bd7d7f544d362576be74f9d5901a22f317efc20046efe2034dced238cbbfe78", size = 103520, upload-time = "2026-01-30T01:03:18.652Z" }, { url = "https://files.pythonhosted.org/packages/8f/a7/b470f672e6fc5fee0a01d9e75005a0e617e162381974213a945fcd274843/pytokens-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4a14d5f5fc78ce85e426aa159489e2d5961acf0e47575e08f35584009178e321", size = 160821, upload-time = "2026-01-30T01:03:19.684Z" }, { url = "https://files.pythonhosted.org/packages/80/98/e83a36fe8d170c911f864bfded690d2542bfcfacb9c649d11a9e6eb9dc41/pytokens-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97f50fd18543be72da51dd505e2ed20d2228c74e0464e4262e4899797803d7fa", size = 254263, upload-time = "2026-01-30T01:03:20.834Z" }, { url = "https://files.pythonhosted.org/packages/0f/95/70d7041273890f9f97a24234c00b746e8da86df462620194cef1d411ddeb/pytokens-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dc74c035f9bfca0255c1af77ddd2d6ae8419012805453e4b0e7513e17904545d", size = 268071, upload-time = "2026-01-30T01:03:21.888Z" }, { url = "https://files.pythonhosted.org/packages/da/79/76e6d09ae19c99404656d7db9c35dfd20f2086f3eb6ecb496b5b31163bad/pytokens-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f66a6bbe741bd431f6d741e617e0f39ec7257ca1f89089593479347cc4d13324", size = 271716, upload-time = "2026-01-30T01:03:23.633Z" }, { url = "https://files.pythonhosted.org/packages/79/37/482e55fa1602e0a7ff012661d8c946bafdc05e480ea5a32f4f7e336d4aa9/pytokens-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:b35d7e5ad269804f6697727702da3c517bb8a5228afa450ab0fa787732055fc9", size = 104539, upload-time = "2026-01-30T01:03:24.788Z" }, { url = "https://files.pythonhosted.org/packages/30/e8/20e7db907c23f3d63b0be3b8a4fd1927f6da2395f5bcc7f72242bb963dfe/pytokens-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8fcb9ba3709ff77e77f1c7022ff11d13553f3c30299a9fe246a166903e9091eb", size = 168474, upload-time = "2026-01-30T01:03:26.428Z" }, { url = "https://files.pythonhosted.org/packages/d6/81/88a95ee9fafdd8f5f3452107748fd04c24930d500b9aba9738f3ade642cc/pytokens-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:79fc6b8699564e1f9b521582c35435f1bd32dd06822322ec44afdeba666d8cb3", size = 290473, upload-time = "2026-01-30T01:03:27.415Z" }, { url = "https://files.pythonhosted.org/packages/cf/35/3aa899645e29b6375b4aed9f8d21df219e7c958c4c186b465e42ee0a06bf/pytokens-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d31b97b3de0f61571a124a00ffe9a81fb9939146c122c11060725bd5aea79975", size = 303485, upload-time = "2026-01-30T01:03:28.558Z" }, { url = "https://files.pythonhosted.org/packages/52/a0/07907b6ff512674d9b201859f7d212298c44933633c946703a20c25e9d81/pytokens-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:967cf6e3fd4adf7de8fc73cd3043754ae79c36475c1c11d514fc72cf5490094a", size = 306698, upload-time = "2026-01-30T01:03:29.653Z" }, { url = "https://files.pythonhosted.org/packages/39/2a/cbbf9250020a4a8dd53ba83a46c097b69e5eb49dd14e708f496f548c6612/pytokens-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:584c80c24b078eec1e227079d56dc22ff755e0ba8654d8383b2c549107528918", size = 116287, upload-time = "2026-01-30T01:03:30.912Z" }, { url = "https://files.pythonhosted.org/packages/c6/78/397db326746f0a342855b81216ae1f0a32965deccfd7c830a2dbc66d2483/pytokens-0.4.1-py3-none-any.whl", hash = "sha256:26cef14744a8385f35d0e095dc8b3a7583f6c953c2e3d269c7f82484bf5ad2de", size = 13729, upload-time = "2026-01-30T01:03:45.029Z" }, ] [[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 = "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 = "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 = "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.11' and python_full_version < '3.13'", ] 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 = "ruff" version = "0.16.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/70/25/7113f6d5498888c5fb7db34081cba7d5971c4cb1bfb26819966eee68f003/ruff-0.16.1.tar.gz", hash = "sha256:fedad7c801dabd3fb9741d76aca39246e6ddd9ca446a015875207bf19f1e6bc7", size = 4877500, upload-time = "2026-07-30T19:37:01.379Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/1b/bd/694da69368e0973de65df2ddc73ab18d43c469d5963d9b150911de6bc513/ruff-0.16.1-py3-none-linux_armv6l.whl", hash = "sha256:58edb313b88f0c5460a26adf5f39a37a3be789494a15e3e411e35fa78b89f9a0", size = 10839126, upload-time = "2026-07-30T19:36:13.697Z" }, { url = "https://files.pythonhosted.org/packages/3f/f0/b626e5d5bd0dd9576263658ef12885e2288afd1029a48e26ffed65ec1ac1/ruff-0.16.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:fde5a99e2f97479af66edd6622c6d5a2a7592c77cf4153d9e4428f5eeb55b60c", size = 11070253, upload-time = "2026-07-30T19:36:17.14Z" }, { url = "https://files.pythonhosted.org/packages/83/63/f40acfb6b35b88623e71684942b552c3edd96035f5d98f313815f7b277de/ruff-0.16.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e0d4c20532fca4f7fa609369161d968dd28f65d83dabbd61d8e9c7edbf7001f6", size = 10561425, upload-time = "2026-07-30T19:36:20.04Z" }, { url = "https://files.pythonhosted.org/packages/aa/dd/14ec0e9c2b4d315547dd38765004b4863e354e1b52cb308272215d9f6f6d/ruff-0.16.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:30affbcedf59ad5703d9c91f82266e02b47739f797e1a7b6e158e5526a6dae38", size = 10948879, upload-time = "2026-07-30T19:36:22.476Z" }, { url = "https://files.pythonhosted.org/packages/33/e9/9d870cbae575030fdef595f04b4b97573c525b5497cce4f4498cf2f85446/ruff-0.16.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:24e9c631573cbca9d20f1283f8f479b2afa4a8503504822bd71a293889f16743", size = 10643691, upload-time = "2026-07-30T19:36:24.914Z" }, { url = "https://files.pythonhosted.org/packages/c4/09/12743d544e2173f53ecd27217c65f90d2bc0f8424a66a60339e56bbc0457/ruff-0.16.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b41bdd48fb420987a9b5212e4957c26ad4abce401fa9ea9d4d85843727945f4f", size = 11435354, upload-time = "2026-07-30T19:36:28.447Z" }, { url = "https://files.pythonhosted.org/packages/7f/89/a1652b2daee52083c9554a6333b678a8b01d0400f976827bb87857f9449a/ruff-0.16.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b0d1e1393b7648079e13669de1c1f4fde06d4583e84d8fd5c1551e0a77a2aa75", size = 12259033, upload-time = "2026-07-30T19:36:31.326Z" }, { url = "https://files.pythonhosted.org/packages/16/96/ecdcb8c54ee7b123b487f807eb014e6e019155a0b81dfb669acd52f28ce3/ruff-0.16.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:07bf434b1c95f4e093be4532068ef4fcf00924eb2ade8796075980902d6fd54a", size = 11667981, upload-time = "2026-07-30T19:36:34.394Z" }, { url = "https://files.pythonhosted.org/packages/cd/90/c52e12e0d862e9572f2a33aa227409143520abe53111e9a6babbac7b4af8/ruff-0.16.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39897739f112253ee4fdd2e8aa9a4f9ded99fb2be367d5f31dfa4ded6025584c", size = 11468183, upload-time = "2026-07-30T19:36:37.339Z" }, { url = "https://files.pythonhosted.org/packages/2c/6b/4ffb7ad1d83eb16cf8cbb3c8815d3f11c88460fd162d4b372a2059be1c2a/ruff-0.16.1-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:82ae3c0c0d74daf17b968a10b7b3bb3ef297ab7de0c1f749646b25e690ccb150", size = 11470071, upload-time = "2026-07-30T19:36:39.91Z" }, { url = "https://files.pythonhosted.org/packages/9c/72/32ae7db4c0b5e32ab611787caa19d1546800676d79f7483b7100a3561bf4/ruff-0.16.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4d5f2ed10f8242d83fc08d521301089364e3375375705356f20c0e31606ef3ef", size = 10919503, upload-time = "2026-07-30T19:36:42.65Z" }, { url = "https://files.pythonhosted.org/packages/f7/ca/3d901ba6ad6fc38da39c3448fc6c59ac945679293a17c3ceb6d6c1cba13e/ruff-0.16.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a4665b309891f83f3e3c25447935f1213e9abbd4b5640af7a1f2def9f8d413c1", size = 10649861, upload-time = "2026-07-30T19:36:45.18Z" }, { url = "https://files.pythonhosted.org/packages/92/79/894ef1ced26552d5f8c9cf6d85b0687840e1128c55aeab7b9c2d54a0d880/ruff-0.16.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:26e9ca5c9bc3971f20d3cf18a957f52ffd6a5f6564ff15c4912a144dcac22494", size = 11148137, upload-time = "2026-07-30T19:36:47.936Z" }, { url = "https://files.pythonhosted.org/packages/2d/69/3609a09fa1cb46cc28b762363e440a354204e5dff01bd0c8d7437874d6b9/ruff-0.16.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:67e1e1e3fa4f0c82f0e36d4cd61e661f6e7a6196cb1aa92fe0828fa7b8f257cd", size = 11559211, upload-time = "2026-07-30T19:36:50.448Z" }, { url = "https://files.pythonhosted.org/packages/fc/8a/fb22af2fd78a736e241fabf67e30ce1799a64244026377a49e133af90762/ruff-0.16.1-py3-none-win32.whl", hash = "sha256:d31765e131295b8445caf301e3e8a85b34d1b9b211b4109b7ba457888b051806", size = 10838258, upload-time = "2026-07-30T19:36:53.298Z" }, { url = "https://files.pythonhosted.org/packages/d4/35/e57fd9fb5d423961df087a00b12d42c0a830288dc2f3b45ecca299158b4f/ruff-0.16.1-py3-none-win_amd64.whl", hash = "sha256:09b05e8b90c2cb06ad63464350e7a45e8e44a2dfe52072ebfba6666ca8d3f596", size = 11961111, upload-time = "2026-07-30T19:36:56.107Z" }, { url = "https://files.pythonhosted.org/packages/cb/46/240ea004bf6dc4feb40e9832f2205a476a47dd5b8a3f8211a5fc5f95e20e/ruff-0.16.1-py3-none-win_arm64.whl", hash = "sha256:dbaadaac38c70239f056d306b7476f246b0bf000fa6b3876402acbf5b227eaf8", size = 11309414, upload-time = "2026-07-30T19:36:58.79Z" }, ] [[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 = "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.58.0" 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/6e/8e/4d2b1b2a81f4de1cd4e54fa40df1ab5f9bb88fe2e37461fc44aba8f9d302/tox-4.58.0.tar.gz", hash = "sha256:ab0b126a04dd56bc18e6d216386db09335247f2289b54cf534deb5c4ae3a8d2e", size = 296926, upload-time = "2026-07-21T13:10:36.622Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/6b/3d/7ba55871e9d794d40b6c8424f2e5d1b267ea5d9a4bd2175e08b57960ba13/tox-4.58.0-py3-none-any.whl", hash = "sha256:dcae21f5f015f3a67658e35644cce0d1aa0dedcd06f3927f95d84e1717f6cea5", size = 223298, upload-time = "2026-07-21T13:10:34.731Z" }, ] [[package]] name = "tox-uv" version = "1.36.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "tox-uv-bare" }, { name = "uv" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/f9/d7/3fce976b9e295218a2d7e541e0f1e9b1259cb893de7a8d277952a66a798d/tox_uv-1.36.0-py3-none-any.whl", hash = "sha256:5f81b39be3fe4e14c6b9bb7ba637ed97d34efa6214efd5f525d95ee9559a99ac", size = 6564, upload-time = "2026-07-21T13:09:54.316Z" }, ] [[package]] name = "tox-uv-bare" version = "1.36.0" 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/56/df/9f90a59de8c87cece6e691eee53dc1cb0a824d73aea8f380ae2f6ecb71de/tox_uv_bare-1.36.0.tar.gz", hash = "sha256:d9b0a2fd0f74fa65d9597108f8a0ef7abb08651c9196a998a6073b781b45cfd0", size = 32548, upload-time = "2026-07-21T13:09:56.435Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/f7/0a/6dc462e4fb543305283a6157c80f43e3d12ca4702da6ae6521d541c6b55c/tox_uv_bare-1.36.0-py3-none-any.whl", hash = "sha256:ba397dd0396df95a75744d4e42a50ee27207c0ffcf277b62ffba9c3de455a939", size = 22489, upload-time = "2026-07-21T13:09:55.389Z" }, ] [[package]] name = "typeguard" version = "4.6.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b4/de/4420db493fa8fc0856d5e5c1b159c63a323d2de2317babe36b01568928e8/typeguard-4.6.0.tar.gz", hash = "sha256:e7414f09111317de3e335de92cd397c5c0ca00b1cc1676de12e1d444a79b3f21", size = 82330, upload-time = "2026-07-26T08:40:23.207Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/8f/eb/461d5f167b6f5c7d97696f397c82f82e3480e003fce3f0a1cd1dd26e2eb2/typeguard-4.6.0-py3-none-any.whl", hash = "sha256:79878165bb86f2cf5d41d159a0ff1792a796cf496882d2fe1b1c6c7049b9cdd7", size = 36884, upload-time = "2026-07-26T08:40:21.868Z" }, ] [[package]] name = "types-protobuf" version = "7.34.1.20260518" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/29/59/e2b13b499d15e6720150c4b1a8d91e31fcacf716b432397475b3151ff7e4/types_protobuf-7.34.1.20260518.tar.gz", hash = "sha256:28cfaded25889cb83ebfb63cfb0a43628f0b6f3785767bec17287dc6468795f2", size = 68936, upload-time = "2026-05-18T06:01:47.332Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/2a/1f/ec5caf72c2e3b688ca3927e0979a04ddad19e1afc4bf1c199bd743e0f419/types_protobuf-7.34.1.20260518-py3-none-any.whl", hash = "sha256:a0a5337413347166439c0e07cbc26c6164d091401c6f01b1dfd8cdb966c4dd8f", size = 85992, upload-time = "2026-05-18T06:01:45.696Z" }, ] [[package]] name = "typing-extensions" version = "4.16.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" }, ] [[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 = "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.12.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/4d/e7/653d48766f5a7a330fdb83e7de67705e2a428bc65f783ed11a0d835e9865/uv-0.12.2.tar.gz", hash = "sha256:1fa777b1b334b4b4c95af09ae0b128c2404084f412a851bdb0b6f5e2eb357df1", size = 5873701, upload-time = "2026-08-05T19:21:55.919Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/47/22/629a8fbca3d2d4a00030e76e4aeb5bf99458b74ceb8e681e7d64c1a9094b/uv-0.12.2-py3-none-linux_armv6l.whl", hash = "sha256:618214e75871dba436c469456bdd8019ead2cf66689bd786bd48261e367f2b1f", size = 21779937, upload-time = "2026-08-05T19:20:52.663Z" }, { url = "https://files.pythonhosted.org/packages/db/46/012f5592c94cbbaca7a2aedb3d853891519a63675b846b1f72dc22bf984d/uv-0.12.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:594dab10d5ff79ca686807fd25c9f4ebd5c40157ddc4a25c0dd934d18ae56bc2", size = 20046124, upload-time = "2026-08-05T19:20:56.871Z" }, { url = "https://files.pythonhosted.org/packages/bc/bd/55222e2da09f12be3e662e4a36a9c68cde85c27c943c0867f3b12f1aea76/uv-0.12.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c5d10c5ba988afe7fe0cd0b943219eaa45d4191a37e3165126b0bb3e308373cb", size = 18415024, upload-time = "2026-08-05T19:21:00.228Z" }, { url = "https://files.pythonhosted.org/packages/3b/88/3b08dd402cea1121b45baf2a2e1b105ecea9fa6b007564fca14f04eedf58/uv-0.12.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:974a79184f901cd6f6fb4155d8fb2f709c951b4f5843352dabccee08afcfd8ee", size = 21167788, upload-time = "2026-08-05T19:21:03.587Z" }, { url = "https://files.pythonhosted.org/packages/8e/ab/df7048e32f7dc8c111bafd1c0000771182d44ba598cb2ca6aa01d6e1a701/uv-0.12.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:e57b047a5fcc5433a01397b6750e0abd472b320677b35bec24b5810db0414a55", size = 21295352, upload-time = "2026-08-05T19:21:07.198Z" }, { url = "https://files.pythonhosted.org/packages/ec/64/116bda88bf0aef59010b8bb0e7210876aee2d02859f720b1fbeda02538b0/uv-0.12.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:86ec228fe419e75b1d943d4c12ae1b7dd1e23caef565d52474467b5ca1e4e6a5", size = 21328080, upload-time = "2026-08-05T19:21:10.803Z" }, { url = "https://files.pythonhosted.org/packages/28/c7/9ea07fb177cafa41c5a7a419b8304a8721608a36ce54c08233c63bc65e02/uv-0.12.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8740cbb2d2d3f9da79049621506f45f1ec2afbb63badda1458c5eadac2ddc233", size = 21965650, upload-time = "2026-08-05T19:21:14.879Z" }, { url = "https://files.pythonhosted.org/packages/04/36/8d2857c23b946766930e2a7cb29b2625c2ae1d91f7334c3f8ce3e4fd32f3/uv-0.12.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:06ce541290777ddf31dcbfa35827270244b8015f7dd0b2e5db8394753e6023b5", size = 23249866, upload-time = "2026-08-05T19:21:18.376Z" }, { url = "https://files.pythonhosted.org/packages/20/a1/c9b594b8639e35ebac78e3fc14cd1c7d45db159a86c8beede317b66f975c/uv-0.12.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2665c331bff339e3bbe406d3c48ef241b4d96e4dc77b4ff7b8051b9f4a839fd2", size = 22927435, upload-time = "2026-08-05T19:21:21.979Z" }, { url = "https://files.pythonhosted.org/packages/a7/fe/a87be2a492440945e745b0dc81aef7fea9730d8194af88d854b7641f5ddf/uv-0.12.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a8d93d1fe019561b70494a8e1332492afb5f14dadc33d5239569d0964164d10", size = 22321279, upload-time = "2026-08-05T19:21:25.578Z" }, { url = "https://files.pythonhosted.org/packages/ae/8c/a980effdb4cb95462184bf2f1973e2d320e62743c904e1f7351177b25297/uv-0.12.2-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:8e2f9faae9ea4fd02d46f0a3a22f0a7d5076aa0046750a3250ec0f3ede8e36aa", size = 21312018, upload-time = "2026-08-05T19:21:29.183Z" }, { url = "https://files.pythonhosted.org/packages/87/b1/a83935957bf84414106e6486eb99d265085a04acd8584ff5220c18a5239a/uv-0.12.2-py3-none-manylinux_2_31_riscv64.musllinux_1_1_riscv64.whl", hash = "sha256:d19cb83db136b0def185cbb23e8894b1fb43587cd8c14e30681e9381144199a1", size = 21950961, upload-time = "2026-08-05T19:21:32.929Z" }, { url = "https://files.pythonhosted.org/packages/ca/68/04137a4dfc95fe0014a509ae643de065043811a988b8bbde3fa02397c080/uv-0.12.2-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:78ca00dcaf7fa5fb720d50232fc1e0931839d29a97ebb0e44f24e564c4d90763", size = 22088891, upload-time = "2026-08-05T19:21:36.308Z" }, { url = "https://files.pythonhosted.org/packages/e4/f2/b2885d06aed6c494387a25c64eaefe129ac73fe366fe887db27e1263c126/uv-0.12.2-py3-none-musllinux_1_1_i686.whl", hash = "sha256:cea51e735c0b68b8492c8b3d0496db1fd335a548011ae2093a6a9bd3a4cb4056", size = 21202799, upload-time = "2026-08-05T19:21:39.745Z" }, { url = "https://files.pythonhosted.org/packages/74/90/f347d51195286a7a3a7ad1075e557a5f86e20df4feaa9b009f9a2bd13a92/uv-0.12.2-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:da7780a76d0691eee1af4d43a3e637ece62b1d4ae5fe78a1a9feb299cd731d23", size = 22517594, upload-time = "2026-08-05T19:21:43.364Z" }, { url = "https://files.pythonhosted.org/packages/f8/90/4b040b8c6b40aa7d599c884a3abf34d566eabf360c33baaf3c00c2763a51/uv-0.12.2-py3-none-win32.whl", hash = "sha256:528c4bc3d41548670ec5619c339e158ea8818d88ab64337f158fd814d91397fb", size = 19400742, upload-time = "2026-08-05T19:21:46.788Z" }, { url = "https://files.pythonhosted.org/packages/48/ae/ab4a58082da1e890b785402c17bc94d8b0aeb29fb6ab366aebfec63ad301/uv-0.12.2-py3-none-win_amd64.whl", hash = "sha256:0c837592d9f5bc88e3c0c8da9ab868e79cf26f2938e0a02b59221af084d83de0", size = 20180444, upload-time = "2026-08-05T19:21:50.152Z" }, { url = "https://files.pythonhosted.org/packages/44/34/900d4bf7cbde72e0386cbebe392a4bd12057acb404253656b1092298be94/uv-0.12.2-py3-none-win_arm64.whl", hash = "sha256:ac36c7c26ee892855184e9346e56fa7b58a2e4f82ae7519fdd9c6d8670c49d96", size = 19122020, upload-time = "2026-08-05T19:21:53.399Z" }, ] [[package]] name = "virtualenv" version = "21.7.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/ea/fa/18004e5cb15541ad2a68ff219c755233b012b12d4ec8663d06a258082bec/virtualenv-21.7.1.tar.gz", hash = "sha256:d0dbfaa5483487baea28d7210ef8d24c9d1bd0f10f449eeb215568825a9b334e", size = 5525237, upload-time = "2026-07-30T15:40:36.36Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/4b/a7/ded126c19495158a05c7202b3389139839d4cf78d622d453867778e0f7a8/virtualenv-21.7.1-py3-none-any.whl", hash = "sha256:6394973f990536e34c05157179146c020284c42fe01da1dfeb0ba16c345280d9", size = 5504576, upload-time = "2026-07-30T15:40:34.512Z" }, ]