From fe6122f43eaa714341371a0080446905ac46add7 Mon Sep 17 00:00:00 2001 From: Lukas Hering Date: Thu, 18 Jun 2026 22:10:23 -0400 Subject: [PATCH 01/35] add package scaffolding --- .../opentelemetry-profiles-context/LICENSE | 201 +++++++++ .../opentelemetry-profiles-context/README.rst | 15 + .../pyproject.toml | 43 ++ .../rust/Cargo.lock | 410 ++++++++++++++++++ .../rust/Cargo.toml | 18 + .../rust/src/lib.rs | 16 + .../profiles/context/__init__.py | 2 + .../profiles/context/_rs/__init__.pyi | 4 + .../opentelemetry/profiles/context/py.typed | 0 .../opentelemetry/profiles/context/version.py | 4 + .../test-requirements.in | 5 + .../test-requirements.txt | 26 ++ .../tests/__init__.py | 0 .../tests/test_profiles_context.py | 8 + pyproject.toml | 6 +- tox.ini | 10 + uv.lock | 8 + 17 files changed, 775 insertions(+), 1 deletion(-) create mode 100644 profiles/opentelemetry-profiles-context/LICENSE create mode 100644 profiles/opentelemetry-profiles-context/README.rst create mode 100644 profiles/opentelemetry-profiles-context/pyproject.toml create mode 100644 profiles/opentelemetry-profiles-context/rust/Cargo.lock create mode 100644 profiles/opentelemetry-profiles-context/rust/Cargo.toml create mode 100644 profiles/opentelemetry-profiles-context/rust/src/lib.rs create mode 100644 profiles/opentelemetry-profiles-context/src/opentelemetry/profiles/context/__init__.py create mode 100644 profiles/opentelemetry-profiles-context/src/opentelemetry/profiles/context/_rs/__init__.pyi create mode 100644 profiles/opentelemetry-profiles-context/src/opentelemetry/profiles/context/py.typed create mode 100644 profiles/opentelemetry-profiles-context/src/opentelemetry/profiles/context/version.py create mode 100644 profiles/opentelemetry-profiles-context/test-requirements.in create mode 100644 profiles/opentelemetry-profiles-context/test-requirements.txt create mode 100644 profiles/opentelemetry-profiles-context/tests/__init__.py create mode 100644 profiles/opentelemetry-profiles-context/tests/test_profiles_context.py diff --git a/profiles/opentelemetry-profiles-context/LICENSE b/profiles/opentelemetry-profiles-context/LICENSE new file mode 100644 index 00000000000..261eeb9e9f8 --- /dev/null +++ b/profiles/opentelemetry-profiles-context/LICENSE @@ -0,0 +1,201 @@ + 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. diff --git a/profiles/opentelemetry-profiles-context/README.rst b/profiles/opentelemetry-profiles-context/README.rst new file mode 100644 index 00000000000..4fe8837deb4 --- /dev/null +++ b/profiles/opentelemetry-profiles-context/README.rst @@ -0,0 +1,15 @@ +OpenTelemetry Profiles Context +====================================== + +|pypi| + +.. |pypi| image:: https://badge.fury.io/py/opentelemetry-profiles-context.svg + :target: https://pypi.org/project/opentelemetry-profiles-context/ + +TODO: Update README + +References +---------- + +* `OpenTelemetry `_ +* `OpenTelemetry Protocol Specification `_ diff --git a/profiles/opentelemetry-profiles-context/pyproject.toml b/profiles/opentelemetry-profiles-context/pyproject.toml new file mode 100644 index 00000000000..f964a117d46 --- /dev/null +++ b/profiles/opentelemetry-profiles-context/pyproject.toml @@ -0,0 +1,43 @@ +[build-system] +requires = ["maturin>=1.0,<2.0"] +build-backend = "maturin" + +[project] +name = "opentelemetry-profiles-context" +version = "0.64b0.dev" +description = "OpenTelemetry Profiles Context" +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 :: Profiles", + "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/profiles/opentelemetry-profiles-context" +Repository = "https://github.com/open-telemetry/opentelemetry-python" + +[tool.maturin] +python-source = "src" +module-name = "opentelemetry.profiles.context._rs" + +[tool.uv] +cache-keys = [ + { file = "rust/Cargo.toml" }, + { file = "rust/Cargo.lock" }, + { file = "rust/src/**" }, +] diff --git a/profiles/opentelemetry-profiles-context/rust/Cargo.lock b/profiles/opentelemetry-profiles-context/rust/Cargo.lock new file mode 100644 index 00000000000..27ad7d26c0d --- /dev/null +++ b/profiles/opentelemetry-profiles-context/rust/Cargo.lock @@ -0,0 +1,410 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "bitflags" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" + +[[package]] +name = "bytes" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb593" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "either" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + +[[package]] +name = "fixedbitset" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "log" +version = "0.4.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a" + +[[package]] +name = "memchr" +version = "2.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" + +[[package]] +name = "multimap" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "opentelemetry-profiles-context" +version = "0.1.0" +dependencies = [ + "prost", + "prost-build", + "pyo3", +] + +[[package]] +name = "petgraph" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772" +dependencies = [ + "fixedbitset", + "indexmap", +] + +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "prost" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-build" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be769465445e8c1474e9c5dac2018218498557af32d9ed057325ec9a41ae81bf" +dependencies = [ + "heck", + "itertools", + "log", + "multimap", + "once_cell", + "petgraph", + "prettyplease", + "prost", + "prost-types", + "regex", + "syn", + "tempfile", +] + +[[package]] +name = "prost-derive" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d" +dependencies = [ + "anyhow", + "itertools", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "prost-types" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52c2c1bf36ddb1a1c396b3601a3cec27c2462e45f07c386894ec3ccf5332bd16" +dependencies = [ + "prost", +] + +[[package]] +name = "pyo3" +version = "0.28.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91fd8e38a3b50ed1167fb981cd6fd60147e091784c427b8f7183a7ee32c31c12" +dependencies = [ + "libc", + "once_cell", + "portable-atomic", + "pyo3-build-config", + "pyo3-ffi", + "pyo3-macros", +] + +[[package]] +name = "pyo3-build-config" +version = "0.28.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e368e7ddfdeb98c9bca7f8383be1648fd84ab466bf2bc015e94008db6d35611e" +dependencies = [ + "target-lexicon", +] + +[[package]] +name = "pyo3-ffi" +version = "0.28.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f29e10af80b1f7ccaf7f69eace800a03ecd13e883acfacc1e5d0988605f651e" +dependencies = [ + "libc", + "pyo3-build-config", +] + +[[package]] +name = "pyo3-macros" +version = "0.28.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df6e520eff47c45997d2fc7dd8214b25dd1310918bbb2642156ef66a67f29813" +dependencies = [ + "proc-macro2", + "pyo3-macros-backend", + "quote", + "syn", +] + +[[package]] +name = "pyo3-macros-backend" +version = "0.28.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4cdc218d835738f81c2338f822078af45b4afdf8b2e33cbb5916f108b813acb" +dependencies = [ + "heck", + "proc-macro2", + "pyo3-build-config", + "quote", + "syn", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "regex" +version = "1.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "syn" +version = "2.0.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "target-lexicon" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca" + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom", + "once_cell", + "rustix", + "windows-sys", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] diff --git a/profiles/opentelemetry-profiles-context/rust/Cargo.toml b/profiles/opentelemetry-profiles-context/rust/Cargo.toml new file mode 100644 index 00000000000..91c8d17c969 --- /dev/null +++ b/profiles/opentelemetry-profiles-context/rust/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "opentelemetry-profiles-context" +version = "0.1.0" +edition = "2024" + +[lib] +name = "opentelemetry_profiles_context" +crate-type = ["cdylib"] + +[dependencies] +prost = "0.13" + +[dependencies.pyo3] +version = "0.28.2" +features = ["abi3-py39"] + +[build-dependencies] +prost-build = "0.13" diff --git a/profiles/opentelemetry-profiles-context/rust/src/lib.rs b/profiles/opentelemetry-profiles-context/rust/src/lib.rs new file mode 100644 index 00000000000..439607b31a0 --- /dev/null +++ b/profiles/opentelemetry-profiles-context/rust/src/lib.rs @@ -0,0 +1,16 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +use pyo3::prelude::*; + +#[pyfunction] +fn sum_as_string(a: usize, b: usize) -> PyResult { + Ok((a + b).to_string()) +} + +#[pymodule] +#[pyo3(name = "_rs")] +fn init(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + m.add_wrapped(wrap_pyfunction!(sum_as_string))?; + Ok(()) +} diff --git a/profiles/opentelemetry-profiles-context/src/opentelemetry/profiles/context/__init__.py b/profiles/opentelemetry-profiles-context/src/opentelemetry/profiles/context/__init__.py new file mode 100644 index 00000000000..e57cf4aba95 --- /dev/null +++ b/profiles/opentelemetry-profiles-context/src/opentelemetry/profiles/context/__init__.py @@ -0,0 +1,2 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 diff --git a/profiles/opentelemetry-profiles-context/src/opentelemetry/profiles/context/_rs/__init__.pyi b/profiles/opentelemetry-profiles-context/src/opentelemetry/profiles/context/_rs/__init__.pyi new file mode 100644 index 00000000000..8d680d717d9 --- /dev/null +++ b/profiles/opentelemetry-profiles-context/src/opentelemetry/profiles/context/_rs/__init__.pyi @@ -0,0 +1,4 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +def sum_as_string(a: int, b: int) -> str: ... diff --git a/profiles/opentelemetry-profiles-context/src/opentelemetry/profiles/context/py.typed b/profiles/opentelemetry-profiles-context/src/opentelemetry/profiles/context/py.typed new file mode 100644 index 00000000000..e69de29bb2d diff --git a/profiles/opentelemetry-profiles-context/src/opentelemetry/profiles/context/version.py b/profiles/opentelemetry-profiles-context/src/opentelemetry/profiles/context/version.py new file mode 100644 index 00000000000..13e069be44f --- /dev/null +++ b/profiles/opentelemetry-profiles-context/src/opentelemetry/profiles/context/version.py @@ -0,0 +1,4 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +__version__ = "0.64b0.dev" diff --git a/profiles/opentelemetry-profiles-context/test-requirements.in b/profiles/opentelemetry-profiles-context/test-requirements.in new file mode 100644 index 00000000000..fc282afaa4d --- /dev/null +++ b/profiles/opentelemetry-profiles-context/test-requirements.in @@ -0,0 +1,5 @@ +iniconfig==2.3.0 +packaging==26.2 +pluggy==1.6.0 +pytest==7.4.4 +-e profiles/opentelemetry-profiles-context diff --git a/profiles/opentelemetry-profiles-context/test-requirements.txt b/profiles/opentelemetry-profiles-context/test-requirements.txt new file mode 100644 index 00000000000..f0f697e9594 --- /dev/null +++ b/profiles/opentelemetry-profiles-context/test-requirements.txt @@ -0,0 +1,26 @@ +# This file was autogenerated by uv via the following command: +# uv pip compile --python 3.10 --universal --resolution highest profiles/opentelemetry-profiles-context/test-requirements.in -o profiles/opentelemetry-profiles-context/test-requirements.txt +-e profiles/opentelemetry-profiles-context + # via -r profiles/opentelemetry-profiles-context/test-requirements.in +colorama==0.4.6 ; sys_platform == 'win32' + # via pytest +exceptiongroup==1.3.1 ; python_full_version < '3.11' + # via pytest +iniconfig==2.3.0 + # via + # -r profiles/opentelemetry-profiles-context/test-requirements.in + # pytest +packaging==26.2 + # via + # -r profiles/opentelemetry-profiles-context/test-requirements.in + # pytest +pluggy==1.6.0 + # via + # -r profiles/opentelemetry-profiles-context/test-requirements.in + # pytest +pytest==7.4.4 + # via -r profiles/opentelemetry-profiles-context/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 diff --git a/profiles/opentelemetry-profiles-context/tests/__init__.py b/profiles/opentelemetry-profiles-context/tests/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/profiles/opentelemetry-profiles-context/tests/test_profiles_context.py b/profiles/opentelemetry-profiles-context/tests/test_profiles_context.py new file mode 100644 index 00000000000..89865073ef4 --- /dev/null +++ b/profiles/opentelemetry-profiles-context/tests/test_profiles_context.py @@ -0,0 +1,8 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +from opentelemetry.profiles.context._rs import sum_as_string + + +def test_sum_as_string(): + assert sum_as_string(1, 2) == "3" diff --git a/pyproject.toml b/pyproject.toml index 95c826e01b6..dfe7b153d31 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,6 +20,7 @@ dependencies = [ "opentelemetry-propagator-jaeger", "opentelemetry-propagator-b3", "opentelemetry-codegen-json", + "opentelemetry-profiles-context", ] # https://docs.astral.sh/uv/reference/settings/ @@ -45,6 +46,7 @@ opentelemetry-exporter-prometheus = {workspace = true } opentelemetry-propagator-jaeger = { workspace = true } opentelemetry-propagator-b3 = { workspace = true } opentelemetry-codegen-json = { workspace = true } +opentelemetry-profiles-context = { workspace = true } [tool.uv.workspace] members = [ @@ -56,6 +58,7 @@ members = [ "exporter/*", "propagator/*", "codegen/*", + "profiles/*", "tests/opentelemetry-test-utils", ] @@ -128,7 +131,8 @@ include = [ "exporter/opentelemetry-exporter-otlp-proto-grpc", "exporter/opentelemetry-exporter-otlp-proto-http", "exporter/opentelemetry-exporter-otlp-json-common", - "codegen/opentelemetry-codegen-json" + "codegen/opentelemetry-codegen-json", + "profiles/opentelemetry-profiles-context" ] exclude = [ diff --git a/tox.ini b/tox.ini index 5476de0cde1..eee91a6868d 100644 --- a/tox.ini +++ b/tox.ini @@ -96,6 +96,10 @@ envlist = pypy3-test-opentelemetry-exporter-zipkin-json lint-opentelemetry-exporter-zipkin-json + py3{10,11,12,13,14,14t}-test-opentelemetry-profiles-context + pypy3-test-opentelemetry-profiles-context + lint-opentelemetry-profiles-context + py3{10,11,12,13,14,14t}-test-opentelemetry-propagator-b3 pypy3-test-opentelemetry-propagator-b3 lint-opentelemetry-propagator-b3 @@ -176,6 +180,8 @@ deps = exporter-zipkin-json: -r {toxinidir}/exporter/opentelemetry-exporter-zipkin-json/test-requirements.txt + profiles-context: -r {toxinidir}/profiles/opentelemetry-profiles-context/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 @@ -281,6 +287,9 @@ commands = 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-profiles-context: pytest {toxinidir}/profiles/opentelemetry-profiles-context/tests {posargs} + lint-opentelemetry-profiles-context: sh -c "cd profiles && pylint --rcfile ../.pylintrc {toxinidir}/profiles/opentelemetry-profiles-context" + 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} @@ -430,6 +439,7 @@ deps = -e {toxinidir}/exporter/opentelemetry-exporter-otlp-proto-http -e {toxinidir}/opentelemetry-proto -e {toxinidir}/opentelemetry-proto-json + -e {toxinidir}/opentelemetry-profiles-context -e {toxinidir}/codegen/opentelemetry-codegen-json commands = diff --git a/uv.lock b/uv.lock index e307a066394..27e13cf3c0e 100644 --- a/uv.lock +++ b/uv.lock @@ -20,6 +20,7 @@ members = [ "opentelemetry-exporter-otlp-proto-http", "opentelemetry-exporter-prometheus", "opentelemetry-exporter-zipkin-json", + "opentelemetry-profiles-context", "opentelemetry-propagator-b3", "opentelemetry-propagator-jaeger", "opentelemetry-proto", @@ -1009,6 +1010,11 @@ requires-dist = [ { name = "requests", specifier = "~=2.7" }, ] +[[package]] +name = "opentelemetry-profiles-context" +version = "0.64b0.dev0" +source = { editable = "profiles/opentelemetry-profiles-context" } + [[package]] name = "opentelemetry-propagator-b3" source = { editable = "propagator/opentelemetry-propagator-b3" } @@ -1062,6 +1068,7 @@ dependencies = [ { name = "opentelemetry-exporter-otlp-proto-http" }, { name = "opentelemetry-exporter-prometheus" }, { name = "opentelemetry-exporter-zipkin-json" }, + { name = "opentelemetry-profiles-context" }, { name = "opentelemetry-propagator-b3" }, { name = "opentelemetry-propagator-jaeger" }, { name = "opentelemetry-proto" }, @@ -1092,6 +1099,7 @@ requires-dist = [ { 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-profiles-context", editable = "profiles/opentelemetry-profiles-context" }, { name = "opentelemetry-propagator-b3", editable = "propagator/opentelemetry-propagator-b3" }, { name = "opentelemetry-propagator-jaeger", editable = "propagator/opentelemetry-propagator-jaeger" }, { name = "opentelemetry-proto", editable = "opentelemetry-proto" }, From 05a0a20447f1deb52fccf909c17375c4cc989e03 Mon Sep 17 00:00:00 2001 From: Lukas Hering Date: Thu, 18 Jun 2026 23:33:57 -0400 Subject: [PATCH 02/35] add conversion logic --- .../pyproject.toml | 5 +- .../rust/Cargo.lock | 722 +++++++++++++++++- .../rust/Cargo.toml | 2 + .../rust/build.rs | 43 ++ .../rust/src/convert.rs | 92 +++ .../rust/src/lib.rs | 10 +- .../rust/src/proto.rs | 20 + .../profiles/context/_rs/__init__.pyi | 4 +- .../test-requirements.in | 2 + .../test-requirements.txt | 20 +- .../tests/test_profiles_context.py | 13 +- uv.lock | 10 + 12 files changed, 929 insertions(+), 14 deletions(-) create mode 100644 profiles/opentelemetry-profiles-context/rust/build.rs create mode 100644 profiles/opentelemetry-profiles-context/rust/src/convert.rs create mode 100644 profiles/opentelemetry-profiles-context/rust/src/proto.rs diff --git a/profiles/opentelemetry-profiles-context/pyproject.toml b/profiles/opentelemetry-profiles-context/pyproject.toml index f964a117d46..314ac1ceca1 100644 --- a/profiles/opentelemetry-profiles-context/pyproject.toml +++ b/profiles/opentelemetry-profiles-context/pyproject.toml @@ -25,7 +25,10 @@ classifiers = [ "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", ] -dependencies = [] +dependencies = [ + "opentelemetry-api ~= 1.15", + "opentelemetry-sdk ~= 1.15", +] [project.urls] Homepage = "https://github.com/open-telemetry/opentelemetry-python/tree/main/profiles/opentelemetry-profiles-context" diff --git a/profiles/opentelemetry-profiles-context/rust/Cargo.lock b/profiles/opentelemetry-profiles-context/rust/Cargo.lock index 27ad7d26c0d..1dd532e4405 100644 --- a/profiles/opentelemetry-profiles-context/rust/Cargo.lock +++ b/profiles/opentelemetry-profiles-context/rust/Cargo.lock @@ -2,6 +2,12 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + [[package]] name = "aho-corasick" version = "1.1.4" @@ -17,24 +23,77 @@ version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +[[package]] +name = "backon" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cffb0e931875b666fc4fcb20fee52e9bbd1ef836fd9e9e04ec21555f9f85f7ef" +dependencies = [ + "fastrand", + "gloo-timers", + "tokio", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + [[package]] name = "bitflags" version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + [[package]] name = "bytes" version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb593" +[[package]] +name = "cc" +version = "1.2.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dad887fd958be91b5098c0248def011f4523ab786cd411be668777e55063501f" +dependencies = [ + "find-msvc-tools", + "shlex", +] + [[package]] name = "cfg-if" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "displaydoc" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "either" version = "1.16.0" @@ -54,7 +113,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -63,12 +122,81 @@ version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + [[package]] name = "fixedbitset" version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + [[package]] name = "getrandom" version = "0.4.3" @@ -80,6 +208,18 @@ dependencies = [ "r-efi", ] +[[package]] +name = "gloo-timers" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", +] + [[package]] name = "hashbrown" version = "0.17.1" @@ -92,6 +232,109 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + [[package]] name = "indexmap" version = "2.14.0" @@ -111,6 +354,17 @@ dependencies = [ "either", ] +[[package]] +name = "js-sys" +version = "0.3.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03d04c30968dffe80775bd4d7fb676131cd04a1fb46d2686dbffbaec2d9dfd31" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + [[package]] name = "libc" version = "0.2.186" @@ -123,6 +377,12 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + [[package]] name = "log" version = "0.4.32" @@ -135,6 +395,16 @@ version = "2.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + [[package]] name = "multimap" version = "0.10.1" @@ -151,11 +421,19 @@ checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" name = "opentelemetry-profiles-context" version = "0.1.0" dependencies = [ + "backon", "prost", "prost-build", "pyo3", + "ureq", ] +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + [[package]] name = "petgraph" version = "0.7.1" @@ -166,12 +444,27 @@ dependencies = [ "indexmap", ] +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + [[package]] name = "portable-atomic" version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + [[package]] name = "prettyplease" version = "0.2.37" @@ -345,6 +638,20 @@ version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + [[package]] name = "rustix" version = "1.1.4" @@ -355,9 +662,115 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" +dependencies = [ + "log", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" +dependencies = [ + "zeroize", ] +[[package]] +name = "rustls-webpki" +version = "0.103.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "simd-adler32" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "syn" version = "2.0.118" @@ -369,6 +782,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "target-lexicon" version = "0.13.5" @@ -382,10 +806,29 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom", + "getrandom 0.4.3", "once_cell", "rustix", - "windows-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tokio" +version = "1.52.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" +dependencies = [ + "pin-project-lite", ] [[package]] @@ -394,12 +837,130 @@ version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "ureq" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02d1a66277ed75f640d608235660df48c8e3c19f3b4edb6a263315626cc3c01d" +dependencies = [ + "base64", + "flate2", + "log", + "once_cell", + "rustls", + "rustls-pki-types", + "url", + "webpki-roots 0.26.11", +] + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.125" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ddb3f79143bced6de84270411622a2699cee572fc0875aeaf1e7867cf9fca1a" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.125" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e21a184b13fb19e157296e2c46056aec9092264fab83e4ba59e68c61b323c3d" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.125" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fecefd9c35bd935a20fc3fc344b5f29138961e4f47fb03297d88f2587afb5ebd" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.125" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23939e44bb9a5d7576fa2b563dc2e136628f1224e88a8deed09e04858b77871f" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "webpki-roots" +version = "0.26.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" +dependencies = [ + "webpki-roots 1.0.8", +] + +[[package]] +name = "webpki-roots" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf85cb06032201fa7c6f829d7db5a7e5aa45bcc0655327713065f6f0576731bf" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + [[package]] name = "windows-sys" version = "0.61.2" @@ -408,3 +969,156 @@ checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ "windows-link", ] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/profiles/opentelemetry-profiles-context/rust/Cargo.toml b/profiles/opentelemetry-profiles-context/rust/Cargo.toml index 91c8d17c969..b5c5d4c9882 100644 --- a/profiles/opentelemetry-profiles-context/rust/Cargo.toml +++ b/profiles/opentelemetry-profiles-context/rust/Cargo.toml @@ -16,3 +16,5 @@ features = ["abi3-py39"] [build-dependencies] prost-build = "0.13" +ureq = { version = "2", features = ["tls"] } +backon = "1" diff --git a/profiles/opentelemetry-profiles-context/rust/build.rs b/profiles/opentelemetry-profiles-context/rust/build.rs new file mode 100644 index 00000000000..931d179824f --- /dev/null +++ b/profiles/opentelemetry-profiles-context/rust/build.rs @@ -0,0 +1,43 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +use std::fs; +use std::path::PathBuf; + +use backon::{BlockingRetryable, ExponentialBuilder}; + +const COMMIT_HASH: &str = "023f8cd36cc946617caa9a9e2e9868186f6d22dd"; + +const UPSTREAM_PROTOS: &[&str] = &[ + "opentelemetry/proto/common/v1/common.proto", + "opentelemetry/proto/resource/v1/resource.proto", + "opentelemetry/proto/processcontext/v1development/process_context.proto", +]; + +const RAW_BASE: &str = + "https://raw.githubusercontent.com/open-telemetry/opentelemetry-proto"; + +fn main() { + let proto_root = PathBuf::from(std::env::var("OUT_DIR").unwrap()).join("proto"); + + for proto_path in UPSTREAM_PROTOS { + let dest = proto_root.join(proto_path); + if dest.exists() { + continue; + } + fs::create_dir_all(dest.parent().unwrap()).unwrap(); + let url = format!("{RAW_BASE}/{COMMIT_HASH}/{proto_path}"); + let content = (|| -> Result> { + Ok(ureq::get(&url).call()?.into_string()?) + }) + .retry(ExponentialBuilder::default()) + .call() + .unwrap(); + fs::write(&dest, content).unwrap(); + } + + let process_context_proto = proto_root + .join("opentelemetry/proto/processcontext/v1development/process_context.proto"); + + prost_build::compile_protos(&[process_context_proto], &[proto_root]).unwrap(); +} diff --git a/profiles/opentelemetry-profiles-context/rust/src/convert.rs b/profiles/opentelemetry-profiles-context/rust/src/convert.rs new file mode 100644 index 00000000000..0a9d702118f --- /dev/null +++ b/profiles/opentelemetry-profiles-context/rust/src/convert.rs @@ -0,0 +1,92 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +use std::collections::HashMap; + +use pyo3::exceptions::PyTypeError; +use pyo3::prelude::*; +use pyo3::types::{PyBool, PyDict, PyFloat, PyInt, PyString}; + +#[derive(Debug)] +pub enum AttributeValue { + String(String), + Bool(bool), + Int(i64), + Float(f64), + StringArray(Vec), + BoolArray(Vec), + IntArray(Vec), + FloatArray(Vec), +} + +#[derive(Debug)] +pub struct Resource { + pub attributes: HashMap, + pub schema_url: String, +} + +fn attribute_value_from_py(val: &Bound<'_, PyAny>) -> PyResult { + // PyBool must be checked before PyInt: bool is a subclass of int in Python. + if val.is_instance_of::() { + return Ok(AttributeValue::Bool(val.extract()?)); + } + if val.is_instance_of::() { + return Ok(AttributeValue::Int(val.extract()?)); + } + if val.is_instance_of::() { + return Ok(AttributeValue::Float(val.extract()?)); + } + if val.is_instance_of::() { + return Ok(AttributeValue::String(val.extract()?)); + } + // Sequence fallback: collect all elements, dispatch by type of the first. + let elements: Vec> = val.try_iter()?.collect::>()?; + if elements.is_empty() { + return Ok(AttributeValue::StringArray(Vec::new())); + } + let first = &elements[0]; + if first.is_instance_of::() { + return Ok(AttributeValue::BoolArray( + elements.iter().map(|e| e.extract()).collect::>()?, + )); + } + if first.is_instance_of::() { + return Ok(AttributeValue::IntArray( + elements.iter().map(|e| e.extract()).collect::>()?, + )); + } + if first.is_instance_of::() { + return Ok(AttributeValue::FloatArray( + elements.iter().map(|e| e.extract()).collect::>()?, + )); + } + if first.is_instance_of::() { + return Ok(AttributeValue::StringArray( + elements.iter().map(|e| e.extract()).collect::>()?, + )); + } + Err(PyTypeError::new_err(format!( + "unsupported attribute value type: {}", + val.get_type().name()? + ))) +} + +pub fn resource_from_py(resource: &Bound<'_, PyAny>) -> PyResult { + let py = resource.py(); + let attrs_obj = resource.getattr("attributes")?; + + // Convert BoundedAttributes Mapping → plain dict so PyO3 can unpack it cleanly. + let py_dict = py + .import("builtins")? + .getattr("dict")? + .call1((&attrs_obj,))? + .cast_into::()?; + + let mut attributes = HashMap::new(); + for (key, val) in py_dict.iter() { + attributes.insert(key.extract::()?, attribute_value_from_py(&val)?); + } + + let schema_url: String = resource.getattr("schema_url")?.extract()?; + Ok(Resource { attributes, schema_url }) +} diff --git a/profiles/opentelemetry-profiles-context/rust/src/lib.rs b/profiles/opentelemetry-profiles-context/rust/src/lib.rs index 439607b31a0..ea97dd09266 100644 --- a/profiles/opentelemetry-profiles-context/rust/src/lib.rs +++ b/profiles/opentelemetry-profiles-context/rust/src/lib.rs @@ -1,16 +1,20 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 +mod convert; +pub(crate) mod proto; + use pyo3::prelude::*; #[pyfunction] -fn sum_as_string(a: usize, b: usize) -> PyResult { - Ok((a + b).to_string()) +fn publish_context(resource: &Bound<'_, PyAny>) -> PyResult<()> { + let _r = convert::resource_from_py(resource)?; + Ok(()) } #[pymodule] #[pyo3(name = "_rs")] fn init(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { - m.add_wrapped(wrap_pyfunction!(sum_as_string))?; + m.add_wrapped(wrap_pyfunction!(publish_context))?; Ok(()) } diff --git a/profiles/opentelemetry-profiles-context/rust/src/proto.rs b/profiles/opentelemetry-profiles-context/rust/src/proto.rs new file mode 100644 index 00000000000..c9e3c0587c4 --- /dev/null +++ b/profiles/opentelemetry-profiles-context/rust/src/proto.rs @@ -0,0 +1,20 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +pub(crate) mod common { + pub(crate) mod v1 { + include!(concat!(env!("OUT_DIR"), "/opentelemetry.proto.common.v1.rs")); + } +} + +pub(crate) mod resource { + pub(crate) mod v1 { + include!(concat!(env!("OUT_DIR"), "/opentelemetry.proto.resource.v1.rs")); + } +} + +pub(crate) mod processcontext { + pub(crate) mod v1development { + include!(concat!(env!("OUT_DIR"), "/opentelemetry.proto.processcontext.v1development.rs")); + } +} diff --git a/profiles/opentelemetry-profiles-context/src/opentelemetry/profiles/context/_rs/__init__.pyi b/profiles/opentelemetry-profiles-context/src/opentelemetry/profiles/context/_rs/__init__.pyi index 8d680d717d9..ed9946d4362 100644 --- a/profiles/opentelemetry-profiles-context/src/opentelemetry/profiles/context/_rs/__init__.pyi +++ b/profiles/opentelemetry-profiles-context/src/opentelemetry/profiles/context/_rs/__init__.pyi @@ -1,4 +1,6 @@ # Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 -def sum_as_string(a: int, b: int) -> str: ... +from opentelemetry.sdk.resources import Resource + +def publish_context(resource: Resource) -> bool: ... diff --git a/profiles/opentelemetry-profiles-context/test-requirements.in b/profiles/opentelemetry-profiles-context/test-requirements.in index fc282afaa4d..120b7cc4998 100644 --- a/profiles/opentelemetry-profiles-context/test-requirements.in +++ b/profiles/opentelemetry-profiles-context/test-requirements.in @@ -2,4 +2,6 @@ iniconfig==2.3.0 packaging==26.2 pluggy==1.6.0 pytest==7.4.4 +-e opentelemetry-api +-e opentelemetry-sdk -e profiles/opentelemetry-profiles-context diff --git a/profiles/opentelemetry-profiles-context/test-requirements.txt b/profiles/opentelemetry-profiles-context/test-requirements.txt index f0f697e9594..b62334e3b3a 100644 --- a/profiles/opentelemetry-profiles-context/test-requirements.txt +++ b/profiles/opentelemetry-profiles-context/test-requirements.txt @@ -1,5 +1,17 @@ # This file was autogenerated by uv via the following command: # uv pip compile --python 3.10 --universal --resolution highest profiles/opentelemetry-profiles-context/test-requirements.in -o profiles/opentelemetry-profiles-context/test-requirements.txt +-e file:///Users/lukas/Documents/github-projects/opentelemetry-python/opentelemetry-semantic-conventions + # via opentelemetry-sdk +-e opentelemetry-api + # via + # -r profiles/opentelemetry-profiles-context/test-requirements.in + # opentelemetry-profiles-context + # opentelemetry-sdk + # opentelemetry-semantic-conventions +-e opentelemetry-sdk + # via + # -r profiles/opentelemetry-profiles-context/test-requirements.in + # opentelemetry-profiles-context -e profiles/opentelemetry-profiles-context # via -r profiles/opentelemetry-profiles-context/test-requirements.in colorama==0.4.6 ; sys_platform == 'win32' @@ -22,5 +34,9 @@ pytest==7.4.4 # via -r profiles/opentelemetry-profiles-context/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 +typing-extensions==4.15.0 + # via + # exceptiongroup + # opentelemetry-api + # opentelemetry-sdk + # opentelemetry-semantic-conventions diff --git a/profiles/opentelemetry-profiles-context/tests/test_profiles_context.py b/profiles/opentelemetry-profiles-context/tests/test_profiles_context.py index 89865073ef4..b07159632e8 100644 --- a/profiles/opentelemetry-profiles-context/tests/test_profiles_context.py +++ b/profiles/opentelemetry-profiles-context/tests/test_profiles_context.py @@ -1,8 +1,15 @@ # Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 -from opentelemetry.profiles.context._rs import sum_as_string +import unittest +from opentelemetry.profiles.context._rs import publish_context +from opentelemetry.sdk.resources import Resource -def test_sum_as_string(): - assert sum_as_string(1, 2) == "3" + +class TestPublishContext(unittest.TestCase): + def test_publish_context_does_not_raise(self): + resource = Resource( + {"service.name": "test", "version": 1, "pi": 3.14, "active": True} + ) + publish_context(resource) diff --git a/uv.lock b/uv.lock index 27e13cf3c0e..5bc62e3ce6f 100644 --- a/uv.lock +++ b/uv.lock @@ -1014,6 +1014,16 @@ requires-dist = [ name = "opentelemetry-profiles-context" version = "0.64b0.dev0" source = { editable = "profiles/opentelemetry-profiles-context" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-sdk" }, +] + +[package.metadata] +requires-dist = [ + { name = "opentelemetry-api", editable = "opentelemetry-api" }, + { name = "opentelemetry-sdk", editable = "opentelemetry-sdk" }, +] [[package]] name = "opentelemetry-propagator-b3" From 37f40accf46709cea23b1646fce3e92a57449dd1 Mon Sep 17 00:00:00 2001 From: Lukas Hering Date: Fri, 19 Jun 2026 14:32:00 -0400 Subject: [PATCH 03/35] rework package structure --- .../LICENSE | 0 .../README.rst | 6 +- .../pyproject.toml | 8 +- .../rust/Cargo.lock | 2 +- .../rust/Cargo.toml | 4 +- .../rust/build.rs | 0 .../rust/src/convert.rs | 86 +++++++++++++++++ .../rust/src/lib.rs | 0 .../rust/src/proto.rs | 0 .../process_context}/__init__.py | 0 .../process_context}/_rs/__init__.pyi | 0 .../opentelemetry/process_context}/py.typed | 0 .../opentelemetry/process_context}/version.py | 0 .../test-requirements.in | 2 +- .../test-requirements.txt | 22 ++--- .../tests/__init__.py | 0 .../tests/test_profiles_context.py | 3 +- .../rust/src/convert.rs | 92 ------------------- pyproject.toml | 8 +- tox.ini | 14 +-- uv.lock | 10 +- 21 files changed, 126 insertions(+), 131 deletions(-) rename {profiles/opentelemetry-profiles-context => opentelemetry-process-context}/LICENSE (100%) rename {profiles/opentelemetry-profiles-context => opentelemetry-process-context}/README.rst (60%) rename {profiles/opentelemetry-profiles-context => opentelemetry-process-context}/pyproject.toml (85%) rename {profiles/opentelemetry-profiles-context => opentelemetry-process-context}/rust/Cargo.lock (99%) rename {profiles/opentelemetry-profiles-context => opentelemetry-process-context}/rust/Cargo.toml (77%) rename {profiles/opentelemetry-profiles-context => opentelemetry-process-context}/rust/build.rs (100%) create mode 100644 opentelemetry-process-context/rust/src/convert.rs rename {profiles/opentelemetry-profiles-context => opentelemetry-process-context}/rust/src/lib.rs (100%) rename {profiles/opentelemetry-profiles-context => opentelemetry-process-context}/rust/src/proto.rs (100%) rename {profiles/opentelemetry-profiles-context/src/opentelemetry/profiles/context => opentelemetry-process-context/src/opentelemetry/process_context}/__init__.py (100%) rename {profiles/opentelemetry-profiles-context/src/opentelemetry/profiles/context => opentelemetry-process-context/src/opentelemetry/process_context}/_rs/__init__.pyi (100%) rename {profiles/opentelemetry-profiles-context/src/opentelemetry/profiles/context => opentelemetry-process-context/src/opentelemetry/process_context}/py.typed (100%) rename {profiles/opentelemetry-profiles-context/src/opentelemetry/profiles/context => opentelemetry-process-context/src/opentelemetry/process_context}/version.py (100%) rename {profiles/opentelemetry-profiles-context => opentelemetry-process-context}/test-requirements.in (70%) rename {profiles/opentelemetry-profiles-context => opentelemetry-process-context}/test-requirements.txt (53%) rename {profiles/opentelemetry-profiles-context => opentelemetry-process-context}/tests/__init__.py (100%) rename {profiles/opentelemetry-profiles-context => opentelemetry-process-context}/tests/test_profiles_context.py (79%) delete mode 100644 profiles/opentelemetry-profiles-context/rust/src/convert.rs diff --git a/profiles/opentelemetry-profiles-context/LICENSE b/opentelemetry-process-context/LICENSE similarity index 100% rename from profiles/opentelemetry-profiles-context/LICENSE rename to opentelemetry-process-context/LICENSE diff --git a/profiles/opentelemetry-profiles-context/README.rst b/opentelemetry-process-context/README.rst similarity index 60% rename from profiles/opentelemetry-profiles-context/README.rst rename to opentelemetry-process-context/README.rst index 4fe8837deb4..a4d5311c29b 100644 --- a/profiles/opentelemetry-profiles-context/README.rst +++ b/opentelemetry-process-context/README.rst @@ -1,10 +1,10 @@ -OpenTelemetry Profiles Context +OpenTelemetry Process Context ====================================== |pypi| -.. |pypi| image:: https://badge.fury.io/py/opentelemetry-profiles-context.svg - :target: https://pypi.org/project/opentelemetry-profiles-context/ +.. |pypi| image:: https://badge.fury.io/py/opentelemetry-process-context.svg + :target: https://pypi.org/project/opentelemetry-process-context/ TODO: Update README diff --git a/profiles/opentelemetry-profiles-context/pyproject.toml b/opentelemetry-process-context/pyproject.toml similarity index 85% rename from profiles/opentelemetry-profiles-context/pyproject.toml rename to opentelemetry-process-context/pyproject.toml index 314ac1ceca1..17f4c1d9ccc 100644 --- a/profiles/opentelemetry-profiles-context/pyproject.toml +++ b/opentelemetry-process-context/pyproject.toml @@ -3,9 +3,9 @@ requires = ["maturin>=1.0,<2.0"] build-backend = "maturin" [project] -name = "opentelemetry-profiles-context" +name = "opentelemetry-process-context" version = "0.64b0.dev" -description = "OpenTelemetry Profiles Context" +description = "OpenTelemetry Process Context" readme = "README.rst" license = "Apache-2.0" requires-python = ">=3.10" @@ -31,12 +31,12 @@ dependencies = [ ] [project.urls] -Homepage = "https://github.com/open-telemetry/opentelemetry-python/tree/main/profiles/opentelemetry-profiles-context" +Homepage = "https://github.com/open-telemetry/opentelemetry-python/tree/main/popentelemetry-process-context" Repository = "https://github.com/open-telemetry/opentelemetry-python" [tool.maturin] python-source = "src" -module-name = "opentelemetry.profiles.context._rs" +module-name = "opentelemetry.process_context._rs" [tool.uv] cache-keys = [ diff --git a/profiles/opentelemetry-profiles-context/rust/Cargo.lock b/opentelemetry-process-context/rust/Cargo.lock similarity index 99% rename from profiles/opentelemetry-profiles-context/rust/Cargo.lock rename to opentelemetry-process-context/rust/Cargo.lock index 1dd532e4405..8fa2e1d392a 100644 --- a/profiles/opentelemetry-profiles-context/rust/Cargo.lock +++ b/opentelemetry-process-context/rust/Cargo.lock @@ -418,7 +418,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] -name = "opentelemetry-profiles-context" +name = "opentelemetry-process-context" version = "0.1.0" dependencies = [ "backon", diff --git a/profiles/opentelemetry-profiles-context/rust/Cargo.toml b/opentelemetry-process-context/rust/Cargo.toml similarity index 77% rename from profiles/opentelemetry-profiles-context/rust/Cargo.toml rename to opentelemetry-process-context/rust/Cargo.toml index b5c5d4c9882..48b7b9dbbe8 100644 --- a/profiles/opentelemetry-profiles-context/rust/Cargo.toml +++ b/opentelemetry-process-context/rust/Cargo.toml @@ -1,10 +1,10 @@ [package] -name = "opentelemetry-profiles-context" +name = "opentelemetry-process-context" version = "0.1.0" edition = "2024" [lib] -name = "opentelemetry_profiles_context" +name = "opentelemetry_process_context" crate-type = ["cdylib"] [dependencies] diff --git a/profiles/opentelemetry-profiles-context/rust/build.rs b/opentelemetry-process-context/rust/build.rs similarity index 100% rename from profiles/opentelemetry-profiles-context/rust/build.rs rename to opentelemetry-process-context/rust/build.rs diff --git a/opentelemetry-process-context/rust/src/convert.rs b/opentelemetry-process-context/rust/src/convert.rs new file mode 100644 index 00000000000..fe80eff8e8f --- /dev/null +++ b/opentelemetry-process-context/rust/src/convert.rs @@ -0,0 +1,86 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +use crate::proto::common::v1::{any_value, AnyValue, ArrayValue, KeyValue, KeyValueList}; +use pyo3::prelude::*; +use pyo3::types::{PyBool, PyDict, PyFloat, PyInt, PyString}; + +fn any_value_from_py(val: &Bound<'_, PyAny>) -> PyResult { + // None -> empty AnyValue (value field absent). + if val.is_none() { + return Ok(AnyValue::default()); + } + // PyBool must be checked before PyInt: bool is a subclass of int in Python. + if val.is_instance_of::() { + return Ok(AnyValue { value: Some(any_value::Value::BoolValue(val.extract()?)) }); + } + if val.is_instance_of::() { + return Ok(AnyValue { value: Some(any_value::Value::IntValue(val.extract()?)) }); + } + if val.is_instance_of::() { + return Ok(AnyValue { value: Some(any_value::Value::DoubleValue(val.extract()?)) }); + } + if val.is_instance_of::() { + return Ok(AnyValue { value: Some(any_value::Value::StringValue(val.extract()?)) }); + } + // Mapping -> KvlistValue. Must come before the sequence fallback because Mappings are iterable. + let py = val.py(); + let collections_abc = py.import("collections.abc")?; + if val.is_instance(collections_abc.getattr("Mapping")?.as_ref())? { + // Normalise arbitrary Mapping to a plain dict first. + let py_dict = py + .import("builtins")? + .getattr("dict")? + .call1((val,))? + .cast_into::()?; + let values = py_dict + .iter() + .map(|(k, v)| { + Ok(KeyValue { + key: k.str()?.extract()?, + value: Some(any_value_from_py(&v)?), + ..Default::default() + }) + }) + .collect::>()?; + return Ok(AnyValue { + value: Some(any_value::Value::KvlistValue(KeyValueList { values })), + }); + } + // Sequence fallback: recurse per element into ArrayValue. + let values = val + .try_iter()? + .map(|item| any_value_from_py(&item?)) + .collect::>()?; + Ok(AnyValue { value: Some(any_value::Value::ArrayValue(ArrayValue { values })) }) +} + +pub fn resource_from_py( + resource: &Bound<'_, PyAny>, +) -> PyResult { + let py = resource.py(); + let attrs_obj = resource.getattr("attributes")?; + + // Convert BoundedAttributes Mapping → plain dict so PyO3 can unpack it cleanly. + let py_dict = py + .import("builtins")? + .getattr("dict")? + .call1((&attrs_obj,))? + .cast_into::()?; + + let attributes = py_dict + .iter() + .map(|(key, val)| { + Ok(KeyValue { + key: key.extract()?, + value: Some(any_value_from_py(&val)?), + ..Default::default() + }) + }) + .collect::>()?; + + Ok(crate::proto::resource::v1::Resource { + attributes, + ..Default::default() + }) +} diff --git a/profiles/opentelemetry-profiles-context/rust/src/lib.rs b/opentelemetry-process-context/rust/src/lib.rs similarity index 100% rename from profiles/opentelemetry-profiles-context/rust/src/lib.rs rename to opentelemetry-process-context/rust/src/lib.rs diff --git a/profiles/opentelemetry-profiles-context/rust/src/proto.rs b/opentelemetry-process-context/rust/src/proto.rs similarity index 100% rename from profiles/opentelemetry-profiles-context/rust/src/proto.rs rename to opentelemetry-process-context/rust/src/proto.rs diff --git a/profiles/opentelemetry-profiles-context/src/opentelemetry/profiles/context/__init__.py b/opentelemetry-process-context/src/opentelemetry/process_context/__init__.py similarity index 100% rename from profiles/opentelemetry-profiles-context/src/opentelemetry/profiles/context/__init__.py rename to opentelemetry-process-context/src/opentelemetry/process_context/__init__.py diff --git a/profiles/opentelemetry-profiles-context/src/opentelemetry/profiles/context/_rs/__init__.pyi b/opentelemetry-process-context/src/opentelemetry/process_context/_rs/__init__.pyi similarity index 100% rename from profiles/opentelemetry-profiles-context/src/opentelemetry/profiles/context/_rs/__init__.pyi rename to opentelemetry-process-context/src/opentelemetry/process_context/_rs/__init__.pyi diff --git a/profiles/opentelemetry-profiles-context/src/opentelemetry/profiles/context/py.typed b/opentelemetry-process-context/src/opentelemetry/process_context/py.typed similarity index 100% rename from profiles/opentelemetry-profiles-context/src/opentelemetry/profiles/context/py.typed rename to opentelemetry-process-context/src/opentelemetry/process_context/py.typed diff --git a/profiles/opentelemetry-profiles-context/src/opentelemetry/profiles/context/version.py b/opentelemetry-process-context/src/opentelemetry/process_context/version.py similarity index 100% rename from profiles/opentelemetry-profiles-context/src/opentelemetry/profiles/context/version.py rename to opentelemetry-process-context/src/opentelemetry/process_context/version.py diff --git a/profiles/opentelemetry-profiles-context/test-requirements.in b/opentelemetry-process-context/test-requirements.in similarity index 70% rename from profiles/opentelemetry-profiles-context/test-requirements.in rename to opentelemetry-process-context/test-requirements.in index 120b7cc4998..0a42b62d9ec 100644 --- a/profiles/opentelemetry-profiles-context/test-requirements.in +++ b/opentelemetry-process-context/test-requirements.in @@ -4,4 +4,4 @@ pluggy==1.6.0 pytest==7.4.4 -e opentelemetry-api -e opentelemetry-sdk --e profiles/opentelemetry-profiles-context +-e opentelemetry-process-context diff --git a/profiles/opentelemetry-profiles-context/test-requirements.txt b/opentelemetry-process-context/test-requirements.txt similarity index 53% rename from profiles/opentelemetry-profiles-context/test-requirements.txt rename to opentelemetry-process-context/test-requirements.txt index b62334e3b3a..1d551511d46 100644 --- a/profiles/opentelemetry-profiles-context/test-requirements.txt +++ b/opentelemetry-process-context/test-requirements.txt @@ -1,37 +1,37 @@ # This file was autogenerated by uv via the following command: -# uv pip compile --python 3.10 --universal --resolution highest profiles/opentelemetry-profiles-context/test-requirements.in -o profiles/opentelemetry-profiles-context/test-requirements.txt +# uv pip compile --python 3.10 --universal --resolution highest opentelemetry-process-context/test-requirements.in -o opentelemetry-process-context/test-requirements.txt -e file:///Users/lukas/Documents/github-projects/opentelemetry-python/opentelemetry-semantic-conventions # via opentelemetry-sdk -e opentelemetry-api # via - # -r profiles/opentelemetry-profiles-context/test-requirements.in - # opentelemetry-profiles-context + # -r opentelemetry-process-context/test-requirements.in + # opentelemetry-process-context # opentelemetry-sdk # opentelemetry-semantic-conventions +-e opentelemetry-process-context + # via -r opentelemetry-process-context/test-requirements.in -e opentelemetry-sdk # via - # -r profiles/opentelemetry-profiles-context/test-requirements.in - # opentelemetry-profiles-context --e profiles/opentelemetry-profiles-context - # via -r profiles/opentelemetry-profiles-context/test-requirements.in + # -r opentelemetry-process-context/test-requirements.in + # opentelemetry-process-context colorama==0.4.6 ; sys_platform == 'win32' # via pytest exceptiongroup==1.3.1 ; python_full_version < '3.11' # via pytest iniconfig==2.3.0 # via - # -r profiles/opentelemetry-profiles-context/test-requirements.in + # -r opentelemetry-process-context/test-requirements.in # pytest packaging==26.2 # via - # -r profiles/opentelemetry-profiles-context/test-requirements.in + # -r opentelemetry-process-context/test-requirements.in # pytest pluggy==1.6.0 # via - # -r profiles/opentelemetry-profiles-context/test-requirements.in + # -r opentelemetry-process-context/test-requirements.in # pytest pytest==7.4.4 - # via -r profiles/opentelemetry-profiles-context/test-requirements.in + # via -r opentelemetry-process-context/test-requirements.in tomli==2.4.1 ; python_full_version < '3.11' # via pytest typing-extensions==4.15.0 diff --git a/profiles/opentelemetry-profiles-context/tests/__init__.py b/opentelemetry-process-context/tests/__init__.py similarity index 100% rename from profiles/opentelemetry-profiles-context/tests/__init__.py rename to opentelemetry-process-context/tests/__init__.py diff --git a/profiles/opentelemetry-profiles-context/tests/test_profiles_context.py b/opentelemetry-process-context/tests/test_profiles_context.py similarity index 79% rename from profiles/opentelemetry-profiles-context/tests/test_profiles_context.py rename to opentelemetry-process-context/tests/test_profiles_context.py index b07159632e8..416b3dd75a0 100644 --- a/profiles/opentelemetry-profiles-context/tests/test_profiles_context.py +++ b/opentelemetry-process-context/tests/test_profiles_context.py @@ -3,11 +3,12 @@ import unittest -from opentelemetry.profiles.context._rs import publish_context +from opentelemetry.process_context._rs import publish_context from opentelemetry.sdk.resources import Resource class TestPublishContext(unittest.TestCase): + # pylint: disable-next=no-self-use def test_publish_context_does_not_raise(self): resource = Resource( {"service.name": "test", "version": 1, "pi": 3.14, "active": True} diff --git a/profiles/opentelemetry-profiles-context/rust/src/convert.rs b/profiles/opentelemetry-profiles-context/rust/src/convert.rs deleted file mode 100644 index 0a9d702118f..00000000000 --- a/profiles/opentelemetry-profiles-context/rust/src/convert.rs +++ /dev/null @@ -1,92 +0,0 @@ -// Copyright The OpenTelemetry Authors -// SPDX-License-Identifier: Apache-2.0 - -use std::collections::HashMap; - -use pyo3::exceptions::PyTypeError; -use pyo3::prelude::*; -use pyo3::types::{PyBool, PyDict, PyFloat, PyInt, PyString}; - -#[derive(Debug)] -pub enum AttributeValue { - String(String), - Bool(bool), - Int(i64), - Float(f64), - StringArray(Vec), - BoolArray(Vec), - IntArray(Vec), - FloatArray(Vec), -} - -#[derive(Debug)] -pub struct Resource { - pub attributes: HashMap, - pub schema_url: String, -} - -fn attribute_value_from_py(val: &Bound<'_, PyAny>) -> PyResult { - // PyBool must be checked before PyInt: bool is a subclass of int in Python. - if val.is_instance_of::() { - return Ok(AttributeValue::Bool(val.extract()?)); - } - if val.is_instance_of::() { - return Ok(AttributeValue::Int(val.extract()?)); - } - if val.is_instance_of::() { - return Ok(AttributeValue::Float(val.extract()?)); - } - if val.is_instance_of::() { - return Ok(AttributeValue::String(val.extract()?)); - } - // Sequence fallback: collect all elements, dispatch by type of the first. - let elements: Vec> = val.try_iter()?.collect::>()?; - if elements.is_empty() { - return Ok(AttributeValue::StringArray(Vec::new())); - } - let first = &elements[0]; - if first.is_instance_of::() { - return Ok(AttributeValue::BoolArray( - elements.iter().map(|e| e.extract()).collect::>()?, - )); - } - if first.is_instance_of::() { - return Ok(AttributeValue::IntArray( - elements.iter().map(|e| e.extract()).collect::>()?, - )); - } - if first.is_instance_of::() { - return Ok(AttributeValue::FloatArray( - elements.iter().map(|e| e.extract()).collect::>()?, - )); - } - if first.is_instance_of::() { - return Ok(AttributeValue::StringArray( - elements.iter().map(|e| e.extract()).collect::>()?, - )); - } - Err(PyTypeError::new_err(format!( - "unsupported attribute value type: {}", - val.get_type().name()? - ))) -} - -pub fn resource_from_py(resource: &Bound<'_, PyAny>) -> PyResult { - let py = resource.py(); - let attrs_obj = resource.getattr("attributes")?; - - // Convert BoundedAttributes Mapping → plain dict so PyO3 can unpack it cleanly. - let py_dict = py - .import("builtins")? - .getattr("dict")? - .call1((&attrs_obj,))? - .cast_into::()?; - - let mut attributes = HashMap::new(); - for (key, val) in py_dict.iter() { - attributes.insert(key.extract::()?, attribute_value_from_py(&val)?); - } - - let schema_url: String = resource.getattr("schema_url")?.extract()?; - Ok(Resource { attributes, schema_url }) -} diff --git a/pyproject.toml b/pyproject.toml index dfe7b153d31..2c2b7e9bd9c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,7 +20,7 @@ dependencies = [ "opentelemetry-propagator-jaeger", "opentelemetry-propagator-b3", "opentelemetry-codegen-json", - "opentelemetry-profiles-context", + "opentelemetry-process-context", ] # https://docs.astral.sh/uv/reference/settings/ @@ -46,7 +46,7 @@ opentelemetry-exporter-prometheus = {workspace = true } opentelemetry-propagator-jaeger = { workspace = true } opentelemetry-propagator-b3 = { workspace = true } opentelemetry-codegen-json = { workspace = true } -opentelemetry-profiles-context = { workspace = true } +opentelemetry-process-context = { workspace = true } [tool.uv.workspace] members = [ @@ -58,8 +58,8 @@ members = [ "exporter/*", "propagator/*", "codegen/*", - "profiles/*", "tests/opentelemetry-test-utils", + "opentelemetry-process-context", ] exclude = [ @@ -132,7 +132,7 @@ include = [ "exporter/opentelemetry-exporter-otlp-proto-http", "exporter/opentelemetry-exporter-otlp-json-common", "codegen/opentelemetry-codegen-json", - "profiles/opentelemetry-profiles-context" + "opentelemetry-process-context" ] exclude = [ diff --git a/tox.ini b/tox.ini index eee91a6868d..c6c405547a3 100644 --- a/tox.ini +++ b/tox.ini @@ -96,9 +96,9 @@ envlist = pypy3-test-opentelemetry-exporter-zipkin-json lint-opentelemetry-exporter-zipkin-json - py3{10,11,12,13,14,14t}-test-opentelemetry-profiles-context - pypy3-test-opentelemetry-profiles-context - lint-opentelemetry-profiles-context + py3{10,11,12,13,14,14t}-test-opentelemetry-process-context + pypy3-test-opentelemetry-process-context + lint-opentelemetry-process-context py3{10,11,12,13,14,14t}-test-opentelemetry-propagator-b3 pypy3-test-opentelemetry-propagator-b3 @@ -180,7 +180,7 @@ deps = exporter-zipkin-json: -r {toxinidir}/exporter/opentelemetry-exporter-zipkin-json/test-requirements.txt - profiles-context: -r {toxinidir}/profiles/opentelemetry-profiles-context/test-requirements.txt + process-context: -r {toxinidir}/opentelemetry-process-context/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 @@ -287,8 +287,8 @@ commands = 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-profiles-context: pytest {toxinidir}/profiles/opentelemetry-profiles-context/tests {posargs} - lint-opentelemetry-profiles-context: sh -c "cd profiles && pylint --rcfile ../.pylintrc {toxinidir}/profiles/opentelemetry-profiles-context" + test-opentelemetry-process-context: pytest {toxinidir}/opentelemetry-process-context/tests {posargs} + lint-opentelemetry-process-context: pylint {toxinidir}/opentelemetry-process-context 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" @@ -439,7 +439,7 @@ deps = -e {toxinidir}/exporter/opentelemetry-exporter-otlp-proto-http -e {toxinidir}/opentelemetry-proto -e {toxinidir}/opentelemetry-proto-json - -e {toxinidir}/opentelemetry-profiles-context + -e {toxinidir}/opentelemetry-process-context -e {toxinidir}/codegen/opentelemetry-codegen-json commands = diff --git a/uv.lock b/uv.lock index 5bc62e3ce6f..490e284afb7 100644 --- a/uv.lock +++ b/uv.lock @@ -20,7 +20,7 @@ members = [ "opentelemetry-exporter-otlp-proto-http", "opentelemetry-exporter-prometheus", "opentelemetry-exporter-zipkin-json", - "opentelemetry-profiles-context", + "opentelemetry-process-context", "opentelemetry-propagator-b3", "opentelemetry-propagator-jaeger", "opentelemetry-proto", @@ -1011,9 +1011,9 @@ requires-dist = [ ] [[package]] -name = "opentelemetry-profiles-context" +name = "opentelemetry-process-context" version = "0.64b0.dev0" -source = { editable = "profiles/opentelemetry-profiles-context" } +source = { editable = "opentelemetry-process-context" } dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-sdk" }, @@ -1078,7 +1078,7 @@ dependencies = [ { name = "opentelemetry-exporter-otlp-proto-http" }, { name = "opentelemetry-exporter-prometheus" }, { name = "opentelemetry-exporter-zipkin-json" }, - { name = "opentelemetry-profiles-context" }, + { name = "opentelemetry-process-context" }, { name = "opentelemetry-propagator-b3" }, { name = "opentelemetry-propagator-jaeger" }, { name = "opentelemetry-proto" }, @@ -1109,7 +1109,7 @@ requires-dist = [ { 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-profiles-context", editable = "profiles/opentelemetry-profiles-context" }, + { name = "opentelemetry-process-context", editable = "opentelemetry-process-context" }, { name = "opentelemetry-propagator-b3", editable = "propagator/opentelemetry-propagator-b3" }, { name = "opentelemetry-propagator-jaeger", editable = "propagator/opentelemetry-propagator-jaeger" }, { name = "opentelemetry-proto", editable = "opentelemetry-proto" }, From b40994709b44972804c763c00af2d18c99639ab6 Mon Sep 17 00:00:00 2001 From: Lukas Hering Date: Fri, 19 Jun 2026 17:07:49 -0400 Subject: [PATCH 04/35] publish/unpublish/update logic --- opentelemetry-process-context/rust/Cargo.lock | 19 ++ opentelemetry-process-context/rust/Cargo.toml | 1 + .../rust/src/convert.rs | 32 +- opentelemetry-process-context/rust/src/lib.rs | 22 +- .../rust/src/publish.rs | 319 ++++++++++++++++++ .../opentelemetry/process_context/__init__.py | 4 + .../process_context/_rs/__init__.pyi | 4 +- .../tests/test_profiles_context.py | 34 +- 8 files changed, 416 insertions(+), 19 deletions(-) create mode 100644 opentelemetry-process-context/rust/src/publish.rs diff --git a/opentelemetry-process-context/rust/Cargo.lock b/opentelemetry-process-context/rust/Cargo.lock index 8fa2e1d392a..110a6c8ea78 100644 --- a/opentelemetry-process-context/rust/Cargo.lock +++ b/opentelemetry-process-context/rust/Cargo.lock @@ -74,6 +74,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + [[package]] name = "crc32fast" version = "1.5.0" @@ -411,6 +417,18 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" +[[package]] +name = "nix" +version = "0.31.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d" +dependencies = [ + "bitflags", + "cfg-if", + "cfg_aliases", + "libc", +] + [[package]] name = "once_cell" version = "1.21.4" @@ -422,6 +440,7 @@ name = "opentelemetry-process-context" version = "0.1.0" dependencies = [ "backon", + "nix", "prost", "prost-build", "pyo3", diff --git a/opentelemetry-process-context/rust/Cargo.toml b/opentelemetry-process-context/rust/Cargo.toml index 48b7b9dbbe8..71fddaf202f 100644 --- a/opentelemetry-process-context/rust/Cargo.toml +++ b/opentelemetry-process-context/rust/Cargo.toml @@ -9,6 +9,7 @@ crate-type = ["cdylib"] [dependencies] prost = "0.13" +nix = { version = "0.31", features = ["mman", "fs", "time"] } [dependencies.pyo3] version = "0.28.2" diff --git a/opentelemetry-process-context/rust/src/convert.rs b/opentelemetry-process-context/rust/src/convert.rs index fe80eff8e8f..74b2c7cde4b 100644 --- a/opentelemetry-process-context/rust/src/convert.rs +++ b/opentelemetry-process-context/rust/src/convert.rs @@ -2,15 +2,15 @@ // SPDX-License-Identifier: Apache-2.0 use crate::proto::common::v1::{any_value, AnyValue, ArrayValue, KeyValue, KeyValueList}; +use pyo3::exceptions::PyTypeError; use pyo3::prelude::*; use pyo3::types::{PyBool, PyDict, PyFloat, PyInt, PyString}; +use crate::proto::resource::v1::Resource; fn any_value_from_py(val: &Bound<'_, PyAny>) -> PyResult { - // None -> empty AnyValue (value field absent). if val.is_none() { return Ok(AnyValue::default()); } - // PyBool must be checked before PyInt: bool is a subclass of int in Python. if val.is_instance_of::() { return Ok(AnyValue { value: Some(any_value::Value::BoolValue(val.extract()?)) }); } @@ -23,11 +23,11 @@ fn any_value_from_py(val: &Bound<'_, PyAny>) -> PyResult { if val.is_instance_of::() { return Ok(AnyValue { value: Some(any_value::Value::StringValue(val.extract()?)) }); } - // Mapping -> KvlistValue. Must come before the sequence fallback because Mappings are iterable. + let py = val.py(); let collections_abc = py.import("collections.abc")?; + if val.is_instance(collections_abc.getattr("Mapping")?.as_ref())? { - // Normalise arbitrary Mapping to a plain dict first. let py_dict = py .import("builtins")? .getattr("dict")? @@ -47,21 +47,27 @@ fn any_value_from_py(val: &Bound<'_, PyAny>) -> PyResult { value: Some(any_value::Value::KvlistValue(KeyValueList { values })), }); } - // Sequence fallback: recurse per element into ArrayValue. - let values = val - .try_iter()? - .map(|item| any_value_from_py(&item?)) - .collect::>()?; - Ok(AnyValue { value: Some(any_value::Value::ArrayValue(ArrayValue { values })) }) + + if val.is_instance(collections_abc.getattr("Sequence")?.as_ref())? { + let values = val + .try_iter()? + .map(|item| any_value_from_py(&item?)) + .collect::>()?; + return Ok(AnyValue { value: Some(any_value::Value::ArrayValue(ArrayValue { values })) }); + } + + let type_name: String = val.get_type().qualname()?.extract()?; + Err(PyTypeError::new_err(format!( + "unsupported attribute value type: {type_name}" + ))) } pub fn resource_from_py( resource: &Bound<'_, PyAny>, -) -> PyResult { +) -> PyResult { let py = resource.py(); let attrs_obj = resource.getattr("attributes")?; - // Convert BoundedAttributes Mapping → plain dict so PyO3 can unpack it cleanly. let py_dict = py .import("builtins")? .getattr("dict")? @@ -79,7 +85,7 @@ pub fn resource_from_py( }) .collect::>()?; - Ok(crate::proto::resource::v1::Resource { + Ok(Resource { attributes, ..Default::default() }) diff --git a/opentelemetry-process-context/rust/src/lib.rs b/opentelemetry-process-context/rust/src/lib.rs index ea97dd09266..cfe2d097847 100644 --- a/opentelemetry-process-context/rust/src/lib.rs +++ b/opentelemetry-process-context/rust/src/lib.rs @@ -3,12 +3,30 @@ mod convert; pub(crate) mod proto; +mod publish; use pyo3::prelude::*; #[pyfunction] fn publish_context(resource: &Bound<'_, PyAny>) -> PyResult<()> { - let _r = convert::resource_from_py(resource)?; + let _resource = convert::resource_from_py(resource)?; + // TODO(phase 2): encode the ProcessContext { resource, attributes } payload + // and publish the real bytes instead of an empty payload. + publish::publish(&[])?; + Ok(()) +} + +#[pyfunction] +fn update_context(resource: &Bound<'_, PyAny>) -> PyResult<()> { + let _resource = convert::resource_from_py(resource)?; + // TODO(phase 2): encode the ProcessContext { resource, attributes } payload. + publish::update(&[])?; + Ok(()) +} + +#[pyfunction] +fn unpublish_context() -> PyResult<()> { + publish::unpublish()?; Ok(()) } @@ -16,5 +34,7 @@ fn publish_context(resource: &Bound<'_, PyAny>) -> PyResult<()> { #[pyo3(name = "_rs")] fn init(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_wrapped(wrap_pyfunction!(publish_context))?; + m.add_wrapped(wrap_pyfunction!(update_context))?; + m.add_wrapped(wrap_pyfunction!(unpublish_context))?; Ok(()) } diff --git a/opentelemetry-process-context/rust/src/publish.rs b/opentelemetry-process-context/rust/src/publish.rs new file mode 100644 index 00000000000..96e873c746b --- /dev/null +++ b/opentelemetry-process-context/rust/src/publish.rs @@ -0,0 +1,319 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +//! Publication of the process context to a memory region that out-of-process +//! readers (e.g. the OpenTelemetry eBPF Profiler) can discover and read. +//! +//! The layout follows the OpenTelemetry "Process Context" specification: a fixed +//! 32-byte header mapping named `OTEL_CTX` and backed by a `memfd` on Linux +//! (visible in `/proc//maps`), with the payload living out-of-band in a +//! heap-allocated buffer. This means the header mapping never needs resizing: +//! updates only swap the payload buffer and rewrite the header pointer fields. + +use std::ffi::c_void; +use std::num::NonZeroUsize; +use std::ptr::NonNull; +use std::sync::atomic::{AtomicU64, Ordering}; +use std::sync::Mutex; + +use nix::sys::mman::{mmap_anonymous, MapFlags, ProtFlags}; +use nix::time::{clock_gettime, ClockId}; +use pyo3::exceptions::{PyOSError, PyRuntimeError}; +use pyo3::PyErr; + +/// 8 byte signature stamped at the start of the header. +const SIGNATURE: [u8; 8] = *b"OTEL_CTX"; +/// Format version. `2` is the first stable version (`1` is for development). +const VERSION: u32 = 2; +/// Size of the header mapping in bytes. The payload lives on the heap. +const HEADER_SIZE: usize = 32; + +/// The process context header. +#[repr(C)] +struct Header { + signature: [u8; 8], + version: u32, + payload_size: u32, + monotonic_published_at_ns: AtomicU64, + payload: u64, +} + +const _: () = assert!(std::mem::size_of::
() == HEADER_SIZE); + +/// A published mapping. +#[allow(dead_code)] +struct Region { + ptr: NonNull, + payload: Vec, +} + +// SAFETY: all access goes through `MAPPING` (a `Mutex`), which serializes +// reads and writes. The pointer lives for the process lifetime. +unsafe impl Send for Region {} + +/// The single active process context for this process, if any. +static MAPPING: Mutex> = Mutex::new(None); + +#[derive(Debug)] +pub enum PublishError { + /// A process context has already been published for this process. + AlreadyPublished, + /// `update()` was called before any context was published. + NotPublished, + /// The backing memory region could not be allocated. + Alloc, + /// `madvise(MADV_DONTFORK)` failed. + Madvise, + /// The monotonic clock could not be read for the publish timestamp. + Clock, + /// `munmap` of the header mapping failed during unpublish. + Munmap, +} + +impl From for PyErr { + fn from(err: PublishError) -> Self { + match err { + PublishError::AlreadyPublished => { + PyRuntimeError::new_err("a process context has already been published") + } + PublishError::NotPublished => { + PyRuntimeError::new_err("no process context has been published yet") + } + PublishError::Alloc => { + PyOSError::new_err("failed to allocate the process context mapping") + } + PublishError::Madvise => { + PyOSError::new_err("madvise(MADV_DONTFORK) failed for the process context mapping") + } + PublishError::Clock => { + PyOSError::new_err("failed to read the monotonic clock for the process context") + } + PublishError::Munmap => { + PyOSError::new_err("munmap of the process context mapping failed") + } + } + } +} + +/// Publish `payload` as the process context. +/// +/// The context is a per-process singleton: calling this a second time without a +/// prior teardown returns [`PublishError::AlreadyPublished`]. +pub fn publish(payload: &[u8]) -> Result<(), PublishError> { + let mut guard = MAPPING.lock().expect("process context mutex poisoned"); + if guard.is_some() { + return Err(PublishError::AlreadyPublished); + } + + let ptr = alloc_region()?; + advise_dontfork(ptr, HEADER_SIZE)?; + + let timestamp = get_boottime_ns()?; + + let payload_buf = payload.to_vec(); + + // SAFETY: `ptr` points to a freshly mapped, zero-initialized, page-aligned + // region of exactly `HEADER_SIZE` bytes. The payload lives in `payload_buf` + // on the heap; the header's `payload` field stores a pointer into it. + unsafe { + let header = ptr.as_ptr().cast::
(); + + std::ptr::addr_of_mut!((*header).signature).write(SIGNATURE); + std::ptr::addr_of_mut!((*header).version).write(VERSION); + std::ptr::addr_of_mut!((*header).payload_size).write(payload_buf.len() as u32); + std::ptr::addr_of_mut!((*header).payload).write(payload_buf.as_ptr() as u64); + + // Write the timestamp last with release ordering. This publishes every + // store above to any reader that observes the (non-zero) timestamp. + let published_at = &*std::ptr::addr_of!((*header).monotonic_published_at_ns); + published_at.store(timestamp, Ordering::Release); + } + + // Best effort naming so readers can find the mapping even without a memfd + // path, failures are ignored per the spec. + name_mapping(ptr, HEADER_SIZE); + + *guard = Some(Region { ptr, payload: payload_buf }); + Ok(()) +} + +/// Update the published process context with a new payload. +/// +/// Follows the spec's Updating Protocol: zeros the timestamp (Release) to signal +/// readers that an update is in progress, rewrites the payload fields, then +/// publishes the new timestamp (Release) to signal completion. The old payload +/// buffer is dropped after the new timestamp is live. +pub fn update(payload: &[u8]) -> Result<(), PublishError> { + let mut guard = MAPPING.lock().expect("process context mutex poisoned"); + let region = guard.as_mut().ok_or(PublishError::NotPublished)?; + + let new_buf = payload.to_vec(); + let timestamp = get_boottime_ns()?; + + // SAFETY: `region.ptr` points to the live header mapping with exactly + // `HEADER_SIZE` bytes, writable for the process lifetime. + unsafe { + let header = region.ptr.as_ptr().cast::
(); + let published_at = &*std::ptr::addr_of!((*header).monotonic_published_at_ns); + + // Zero timestamp with Release ensuring the previous payload state is + // visible to readers that observe the "update in progress" signal. + published_at.store(0, Ordering::Release); + + // Rewrite payload fields between the two Release stores. + std::ptr::addr_of_mut!((*header).payload_size).write(new_buf.len() as u32); + std::ptr::addr_of_mut!((*header).payload).write(new_buf.as_ptr() as u64); + + // Publish new timestamp with Release ensuring the new payload fields + // are visible to readers that observe the "update complete" signal. + published_at.store(timestamp, Ordering::Release); + } + + // Rename the mapping unconditionally (failures ignored per spec). + name_mapping(region.ptr, HEADER_SIZE); + + // Drop the old payload only after the new timestamp is live + region.payload = new_buf; + Ok(()) +} + +/// Remove the published process context. +/// +/// Zeros the timestamp (Release) so readers still observing the mapping see an +/// invalid state, then `munmap`s the header and drops the payload buffer. +/// After a successful call, `publish()` may be called again. +pub fn unpublish() -> Result<(), PublishError> { + let mut guard = MAPPING.lock().expect("process context mutex poisoned"); + let region = guard.take().ok_or(PublishError::NotPublished)?; + + // Zero the timestamp with Release before removing the mapping so any + // reader still observing it sees an invalid (zero) state. + unsafe { + let header = region.ptr.as_ptr().cast::
(); + let published_at = &*std::ptr::addr_of!((*header).monotonic_published_at_ns); + published_at.store(0, Ordering::Release); + } + + // Remove the mapping. The memfd fd was already closed in try_memfd_mapping() + // so nothing extra to close here; munmap releases the kernel's last reference + // to the backing store. The payload Vec drops with `region` at end of scope. + unsafe { nix::sys::mman::munmap(region.ptr, HEADER_SIZE) } + .map_err(|_| PublishError::Munmap) +} + +/// Allocate the 32-byte header mapping: a `memfd`-backed mapping on Linux (so +/// it shows up in `/proc//maps`), falling back to an anonymous mapping. +fn alloc_region() -> Result, PublishError> { + let len = NonZeroUsize::new(HEADER_SIZE).unwrap(); + + #[cfg(target_os = "linux")] + if let Some(ptr) = linux::try_memfd_mapping(len) { + return Ok(ptr); + } + + // SAFETY: a fresh anonymous mapping with a valid, non-zero length. + unsafe { + mmap_anonymous( + None, + len, + ProtFlags::PROT_READ | ProtFlags::PROT_WRITE, + MapFlags::MAP_PRIVATE | MapFlags::MAP_ANONYMOUS, + ) + } + .map_err(|_| PublishError::Alloc) +} + +/// Read the publish timestamp. Uses `CLOCK_BOOTTIME` on Linux (as the spec +/// requires) and `CLOCK_MONOTONIC` elsewhere. The value is forced non-zero, as a +/// zero timestamp is reserved to mean "being mutated, not ready". +fn get_boottime_ns() -> Result { + #[cfg(target_os = "linux")] + let clock = ClockId::CLOCK_BOOTTIME; + #[cfg(not(target_os = "linux"))] + let clock = ClockId::CLOCK_MONOTONIC; + + let ts = clock_gettime(clock).map_err(|_| PublishError::Clock)?; + let ns = (ts.tv_sec() as u64) + .saturating_mul(1_000_000_000) + .saturating_add(ts.tv_nsec() as u64); + Ok(ns.max(1)) +} + +/// Prevent child processes from inheriting (stale) context memory. No-op off +/// Linux, where `MADV_DONTFORK` does not exist. +#[cfg(target_os = "linux")] +fn advise_dontfork(ptr: NonNull, len: usize) -> Result<(), PublishError> { + // SAFETY: `ptr`/`len` describe the mapping we just created. + unsafe { nix::sys::mman::madvise(ptr, len, nix::sys::mman::MmapAdvise::MADV_DONTFORK) } + .map_err(|_| PublishError::Madvise) +} + +#[cfg(not(target_os = "linux"))] +fn advise_dontfork(_ptr: NonNull, _len: usize) -> Result<(), PublishError> { + Ok(()) +} + +/// Name the mapping `OTEL_CTX` via `prctl(PR_SET_VMA_ANON_NAME)`. No-op off +/// Linux. Failures (e.g. kernels without `CONFIG_ANON_VMA_NAME`) are ignored. +#[cfg(target_os = "linux")] +fn name_mapping(ptr: NonNull, len: usize) { + linux::name_mapping(ptr, len); +} + +#[cfg(not(target_os = "linux"))] +fn name_mapping(_ptr: NonNull, _len: usize) {} + +#[cfg(target_os = "linux")] +mod linux { + use std::ffi::c_void; + use std::num::NonZeroUsize; + use std::ptr::NonNull; + + use nix::sys::memfd::{memfd_create, MFdFlags}; + use nix::sys::mman::{mmap, MapFlags, ProtFlags}; + + // `prctl` options for naming an anonymous VMA. + const PR_SET_VMA: nix::libc::c_int = 0x53564d41; + const PR_SET_VMA_ANON_NAME: nix::libc::c_ulong = 0; + + /// Create a `memfd`, size it, and map it `MAP_PRIVATE`. Returns `None` if any + /// step fails so the caller can fall back to an anonymous mapping. + pub(super) fn try_memfd_mapping(len: NonZeroUsize) -> Option> { + let base = MFdFlags::MFD_CLOEXEC | MFdFlags::MFD_ALLOW_SEALING; + // `MFD_NOEXEC_SEAL` is a newer flag not exposed by `nix`, request it but + // fall back without it on kernels/libc that lack it. + let noexec_seal = MFdFlags::from_bits_retain(nix::libc::MFD_NOEXEC_SEAL as _); + let fd = memfd_create(c"OTEL_CTX", base | noexec_seal) + .or_else(|_| memfd_create(c"OTEL_CTX", base)) + .ok()?; + + nix::unistd::ftruncate(&fd, len.get() as nix::libc::off_t).ok()?; + + // SAFETY: `fd` is a valid, sized memfd and `len` is non-zero. + let ptr = unsafe { + mmap( + None, + len, + ProtFlags::PROT_READ | ProtFlags::PROT_WRITE, + MapFlags::MAP_PRIVATE, + &fd, + 0, + ) + } + .ok()?; + Some(ptr) + } + + pub(super) fn name_mapping(ptr: NonNull, len: usize) { + const NAME: &core::ffi::CStr = c"OTEL_CTX"; + unsafe { + let _ = nix::libc::prctl( + PR_SET_VMA, + PR_SET_VMA_ANON_NAME, + ptr.as_ptr() as nix::libc::c_ulong, + len as nix::libc::c_ulong, + NAME.as_ptr() as nix::libc::c_ulong, + ); + } + } +} diff --git a/opentelemetry-process-context/src/opentelemetry/process_context/__init__.py b/opentelemetry-process-context/src/opentelemetry/process_context/__init__.py index e57cf4aba95..6932ef75864 100644 --- a/opentelemetry-process-context/src/opentelemetry/process_context/__init__.py +++ b/opentelemetry-process-context/src/opentelemetry/process_context/__init__.py @@ -1,2 +1,6 @@ # Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 + +from opentelemetry.process_context._rs import publish_context + +__all__ = ["publish_context"] diff --git a/opentelemetry-process-context/src/opentelemetry/process_context/_rs/__init__.pyi b/opentelemetry-process-context/src/opentelemetry/process_context/_rs/__init__.pyi index ed9946d4362..e94289db917 100644 --- a/opentelemetry-process-context/src/opentelemetry/process_context/_rs/__init__.pyi +++ b/opentelemetry-process-context/src/opentelemetry/process_context/_rs/__init__.pyi @@ -3,4 +3,6 @@ from opentelemetry.sdk.resources import Resource -def publish_context(resource: Resource) -> bool: ... +def publish_context(resource: Resource) -> None: ... +def update_context(resource: Resource) -> None: ... +def unpublish_context() -> None: ... diff --git a/opentelemetry-process-context/tests/test_profiles_context.py b/opentelemetry-process-context/tests/test_profiles_context.py index 416b3dd75a0..0a1346781de 100644 --- a/opentelemetry-process-context/tests/test_profiles_context.py +++ b/opentelemetry-process-context/tests/test_profiles_context.py @@ -3,14 +3,40 @@ import unittest -from opentelemetry.process_context._rs import publish_context +from opentelemetry.process_context._rs import ( + publish_context, + unpublish_context, + update_context, +) from opentelemetry.sdk.resources import Resource class TestPublishContext(unittest.TestCase): - # pylint: disable-next=no-self-use - def test_publish_context_does_not_raise(self): + def tearDown(self): + try: + unpublish_context() + except RuntimeError: + pass + + def test_publish_context_lifecycle(self): resource = Resource( {"service.name": "test", "version": 1, "pi": 3.14, "active": True} ) - publish_context(resource) + self.assertIsNone(publish_context(resource)) + + with self.assertRaises(RuntimeError): + publish_context(resource) + + self.assertIsNone(update_context(resource)) + self.assertIsNone(update_context(resource)) + + self.assertIsNone(unpublish_context()) + self.assertIsNone(publish_context(resource)) + + def test_update_before_publish_raises(self): + with self.assertRaises(RuntimeError): + update_context(Resource({})) + + def test_unpublish_before_publish_raises(self): + with self.assertRaises(RuntimeError): + unpublish_context() From ed84540dd8862fb2b96f2c7a4e567c80178cfa3e Mon Sep 17 00:00:00 2001 From: Lukas Hering Date: Fri, 19 Jun 2026 17:23:58 -0400 Subject: [PATCH 05/35] add encoding logic --- .../rust/src/convert.rs | 12 ++++++++- opentelemetry-process-context/rust/src/lib.rs | 12 ++++----- .../rust/src/publish.rs | 26 +++++++------------ 3 files changed, 26 insertions(+), 24 deletions(-) diff --git a/opentelemetry-process-context/rust/src/convert.rs b/opentelemetry-process-context/rust/src/convert.rs index 74b2c7cde4b..0279d47a899 100644 --- a/opentelemetry-process-context/rust/src/convert.rs +++ b/opentelemetry-process-context/rust/src/convert.rs @@ -1,10 +1,12 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -use crate::proto::common::v1::{any_value, AnyValue, ArrayValue, KeyValue, KeyValueList}; +use prost::Message; use pyo3::exceptions::PyTypeError; use pyo3::prelude::*; use pyo3::types::{PyBool, PyDict, PyFloat, PyInt, PyString}; +use crate::proto::common::v1::{any_value, AnyValue, ArrayValue, KeyValue, KeyValueList}; +use crate::proto::processcontext::v1development::ProcessContext; use crate::proto::resource::v1::Resource; fn any_value_from_py(val: &Bound<'_, PyAny>) -> PyResult { @@ -90,3 +92,11 @@ pub fn resource_from_py( ..Default::default() }) } + +pub fn encode_process_context(resource: Resource) -> Vec { + ProcessContext { + resource: Some(resource), + attributes: vec![], + } + .encode_to_vec() +} diff --git a/opentelemetry-process-context/rust/src/lib.rs b/opentelemetry-process-context/rust/src/lib.rs index cfe2d097847..c4eaba1a248 100644 --- a/opentelemetry-process-context/rust/src/lib.rs +++ b/opentelemetry-process-context/rust/src/lib.rs @@ -6,21 +6,19 @@ pub(crate) mod proto; mod publish; use pyo3::prelude::*; +use crate::convert::{encode_process_context, resource_from_py}; #[pyfunction] fn publish_context(resource: &Bound<'_, PyAny>) -> PyResult<()> { - let _resource = convert::resource_from_py(resource)?; - // TODO(phase 2): encode the ProcessContext { resource, attributes } payload - // and publish the real bytes instead of an empty payload. - publish::publish(&[])?; + let resource = resource_from_py(resource)?; + publish::publish(encode_process_context(resource))?; Ok(()) } #[pyfunction] fn update_context(resource: &Bound<'_, PyAny>) -> PyResult<()> { - let _resource = convert::resource_from_py(resource)?; - // TODO(phase 2): encode the ProcessContext { resource, attributes } payload. - publish::update(&[])?; + let resource = resource_from_py(resource)?; + publish::update(encode_process_context(resource))?; Ok(()) } diff --git a/opentelemetry-process-context/rust/src/publish.rs b/opentelemetry-process-context/rust/src/publish.rs index 96e873c746b..550c3b86292 100644 --- a/opentelemetry-process-context/rust/src/publish.rs +++ b/opentelemetry-process-context/rust/src/publish.rs @@ -99,7 +99,7 @@ impl From for PyErr { /// /// The context is a per-process singleton: calling this a second time without a /// prior teardown returns [`PublishError::AlreadyPublished`]. -pub fn publish(payload: &[u8]) -> Result<(), PublishError> { +pub fn publish(payload: Vec) -> Result<(), PublishError> { let mut guard = MAPPING.lock().expect("process context mutex poisoned"); if guard.is_some() { return Err(PublishError::AlreadyPublished); @@ -110,11 +110,11 @@ pub fn publish(payload: &[u8]) -> Result<(), PublishError> { let timestamp = get_boottime_ns()?; - let payload_buf = payload.to_vec(); + let payload_buf = payload; - // SAFETY: `ptr` points to a freshly mapped, zero-initialized, page-aligned + // SAFETY: `ptr` points to a freshly mapped, zero initialized, page aligned // region of exactly `HEADER_SIZE` bytes. The payload lives in `payload_buf` - // on the heap; the header's `payload` field stores a pointer into it. + // on the heap and the header's `payload` field stores a pointer into it. unsafe { let header = ptr.as_ptr().cast::
(); @@ -123,8 +123,7 @@ pub fn publish(payload: &[u8]) -> Result<(), PublishError> { std::ptr::addr_of_mut!((*header).payload_size).write(payload_buf.len() as u32); std::ptr::addr_of_mut!((*header).payload).write(payload_buf.as_ptr() as u64); - // Write the timestamp last with release ordering. This publishes every - // store above to any reader that observes the (non-zero) timestamp. + // Write the timestamp last with release ordering. let published_at = &*std::ptr::addr_of!((*header).monotonic_published_at_ns); published_at.store(timestamp, Ordering::Release); } @@ -140,14 +139,14 @@ pub fn publish(payload: &[u8]) -> Result<(), PublishError> { /// Update the published process context with a new payload. /// /// Follows the spec's Updating Protocol: zeros the timestamp (Release) to signal -/// readers that an update is in progress, rewrites the payload fields, then +/// readers that an update is in progress, rewrites the payload fields and then /// publishes the new timestamp (Release) to signal completion. The old payload /// buffer is dropped after the new timestamp is live. -pub fn update(payload: &[u8]) -> Result<(), PublishError> { +pub fn update(payload: Vec) -> Result<(), PublishError> { let mut guard = MAPPING.lock().expect("process context mutex poisoned"); let region = guard.as_mut().ok_or(PublishError::NotPublished)?; - let new_buf = payload.to_vec(); + let new_buf = payload; let timestamp = get_boottime_ns()?; // SAFETY: `region.ptr` points to the live header mapping with exactly @@ -194,9 +193,6 @@ pub fn unpublish() -> Result<(), PublishError> { published_at.store(0, Ordering::Release); } - // Remove the mapping. The memfd fd was already closed in try_memfd_mapping() - // so nothing extra to close here; munmap releases the kernel's last reference - // to the backing store. The payload Vec drops with `region` at end of scope. unsafe { nix::sys::mman::munmap(region.ptr, HEADER_SIZE) } .map_err(|_| PublishError::Munmap) } @@ -239,8 +235,7 @@ fn get_boottime_ns() -> Result { Ok(ns.max(1)) } -/// Prevent child processes from inheriting (stale) context memory. No-op off -/// Linux, where `MADV_DONTFORK` does not exist. +/// Prevent child processes from inheriting (stale) context memory. #[cfg(target_os = "linux")] fn advise_dontfork(ptr: NonNull, len: usize) -> Result<(), PublishError> { // SAFETY: `ptr`/`len` describe the mapping we just created. @@ -253,8 +248,7 @@ fn advise_dontfork(_ptr: NonNull, _len: usize) -> Result<(), PublishErro Ok(()) } -/// Name the mapping `OTEL_CTX` via `prctl(PR_SET_VMA_ANON_NAME)`. No-op off -/// Linux. Failures (e.g. kernels without `CONFIG_ANON_VMA_NAME`) are ignored. +/// Name the mapping `OTEL_CTX` via `prctl(PR_SET_VMA_ANON_NAME)`. #[cfg(target_os = "linux")] fn name_mapping(ptr: NonNull, len: usize) { linux::name_mapping(ptr, len); From c384f9f04a30ff7e1f50b86bd07da0e8f8babba8 Mon Sep 17 00:00:00 2001 From: Lukas Hering Date: Sun, 21 Jun 2026 22:32:26 -0400 Subject: [PATCH 06/35] update generate-workflows.py --- .changelog/5337.added | 1 + .github/workflows/generate_workflows.py | 47 ++++++- .github/workflows/lint.yml | 19 +++ .github/workflows/test.yml | 133 ++++++++++++++++++ opentelemetry-process-context/pyproject.toml | 1 + .../opentelemetry/process_context/__init__.py | 8 +- .../process_context/_rs/__init__.pyi | 37 ++++- .../test-requirements.in | 2 + .../test-requirements.txt | 10 +- .../tests/test_process_context.py | 119 ++++++++++++++++ .../tests/test_profiles_context.py | 42 ------ tox.ini | 3 + 12 files changed, 369 insertions(+), 53 deletions(-) create mode 100644 .changelog/5337.added create mode 100644 opentelemetry-process-context/tests/test_process_context.py delete mode 100644 opentelemetry-process-context/tests/test_profiles_context.py diff --git a/.changelog/5337.added b/.changelog/5337.added new file mode 100644 index 00000000000..64b5a11e728 --- /dev/null +++ b/.changelog/5337.added @@ -0,0 +1 @@ +`opentelemetry-process-context`: implement process context publishing (OTEP-4719) diff --git a/.github/workflows/generate_workflows.py b/.github/workflows/generate_workflows.py index 5b53c211ebf..1f314daacf1 100644 --- a/.github/workflows/generate_workflows.py +++ b/.github/workflows/generate_workflows.py @@ -1,9 +1,11 @@ from collections import defaultdict from pathlib import Path from re import compile as re_compile +from re import fullmatch from jinja2 import Environment, FileSystemLoader from tox.config.cli.parse import get_options +from tox.config.main import Config from tox.config.sets import CoreConfigSet from tox.config.source.tox_ini import ToxIni from tox.session.state import State @@ -18,7 +20,7 @@ ) -def get_tox_envs(tox_ini_path: Path) -> list: +def get_core_config_set(tox_ini_path: Path) -> tuple[Config, CoreConfigSet]: tox_ini = ToxIni(tox_ini_path) conf = State(get_options(), []).conf @@ -40,11 +42,39 @@ def get_tox_envs(tox_ini_path: Path) -> list: ) ) - return core_config_set.load("env_list") + return conf, core_config_set + + +def get_tox_envs(tox_ini_path: Path) -> list: + return get_core_config_set(tox_ini_path)[1].load("env_list") + + +def get_env_platforms(tox_ini_path: Path) -> dict[str, str]: + conf, core_config_set = get_core_config_set(tox_ini_path) + + platforms = {} + for env_name in core_config_set.load("env_list"): + env_config_set = conf.get_env(env_name) + env_config_set.add_config( + keys=["platform"], + of_type=str, + default="", + desc="platform constraint regex", + ) + platforms[env_name] = env_config_set.load("platform") + + return platforms -def get_test_job_datas(tox_envs: list, operating_systems: list) -> list: + +def get_test_job_datas( + tox_envs: list, operating_systems: list, env_platforms: dict[str, str] +) -> list: os_alias = {"ubuntu-latest": "Ubuntu", "windows-latest": "Windows"} + os_sys_platform = { + "ubuntu-latest": "linux", + "windows-latest": "win32", + } python_version_alias = { "pypy3": "pypy-3.10", @@ -59,12 +89,17 @@ def get_test_job_datas(tox_envs: list, operating_systems: list) -> list: test_job_datas = [] for operating_system in operating_systems: + sys_platform = os_sys_platform[operating_system] + 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 + if (platform := env_platforms.get(tox_env, "")) and not fullmatch(platform, sys_platform): + continue + groups = tox_test_env_match.groupdict() aliased_python_version = python_version_alias[ @@ -156,7 +191,11 @@ 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), + get_test_job_datas( + get_tox_envs(tox_ini_path), + operating_systems, + get_env_platforms(tox_ini_path), + ), "test", workflow_directory_path, ) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 27033640dc1..b24172de37b 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -423,6 +423,25 @@ jobs: - name: Run tests run: tox -e lint-opentelemetry-exporter-zipkin-json + lint-opentelemetry-process-context: + name: opentelemetry-process-context + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - name: Checkout repo @ SHA - ${{ github.sha }} + uses: actions/checkout@v4 + + - name: Set up Python 3.14 + uses: actions/setup-python@v5 + with: + python-version: "3.14" + + - name: Install tox + run: pip install tox-uv + + - name: Run tests + run: tox -e lint-opentelemetry-process-context + lint-opentelemetry-propagator-b3: name: opentelemetry-propagator-b3 runs-on: ubuntu-latest diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d562b9d8745..6cd98d119c5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -3086,6 +3086,139 @@ jobs: - name: Run tests run: tox -e pypy3-test-opentelemetry-exporter-zipkin-json -- -ra + py310-test-opentelemetry-process-context_ubuntu-latest: + name: opentelemetry-process-context 3.10 Ubuntu + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - name: Checkout repo @ SHA - ${{ github.sha }} + uses: actions/checkout@v4 + + - name: Set up Python 3.10 + uses: actions/setup-python@v5 + with: + python-version: "3.10" + + - name: Install tox + run: pip install tox-uv + + - name: Run tests + run: tox -e py310-test-opentelemetry-process-context -- -ra + + py311-test-opentelemetry-process-context_ubuntu-latest: + name: opentelemetry-process-context 3.11 Ubuntu + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - name: Checkout repo @ SHA - ${{ github.sha }} + uses: actions/checkout@v4 + + - name: Set up Python 3.11 + uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Install tox + run: pip install tox-uv + + - name: Run tests + run: tox -e py311-test-opentelemetry-process-context -- -ra + + py312-test-opentelemetry-process-context_ubuntu-latest: + name: opentelemetry-process-context 3.12 Ubuntu + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - name: Checkout repo @ SHA - ${{ github.sha }} + uses: actions/checkout@v4 + + - name: Set up Python 3.12 + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install tox + run: pip install tox-uv + + - name: Run tests + run: tox -e py312-test-opentelemetry-process-context -- -ra + + py313-test-opentelemetry-process-context_ubuntu-latest: + name: opentelemetry-process-context 3.13 Ubuntu + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - name: Checkout repo @ SHA - ${{ github.sha }} + uses: actions/checkout@v4 + + - name: Set up Python 3.13 + uses: actions/setup-python@v5 + with: + python-version: "3.13" + + - name: Install tox + run: pip install tox-uv + + - name: Run tests + run: tox -e py313-test-opentelemetry-process-context -- -ra + + py314-test-opentelemetry-process-context_ubuntu-latest: + name: opentelemetry-process-context 3.14 Ubuntu + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - name: Checkout repo @ SHA - ${{ github.sha }} + uses: actions/checkout@v4 + + - name: Set up Python 3.14 + uses: actions/setup-python@v5 + with: + python-version: "3.14" + + - name: Install tox + run: pip install tox-uv + + - name: Run tests + run: tox -e py314-test-opentelemetry-process-context -- -ra + + py314t-test-opentelemetry-process-context_ubuntu-latest: + name: opentelemetry-process-context 3.14t Ubuntu + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - name: Checkout repo @ SHA - ${{ github.sha }} + uses: actions/checkout@v4 + + - name: Set up Python 3.14t + uses: actions/setup-python@v5 + with: + python-version: "3.14t" + + - name: Install tox + run: pip install tox-uv + + - name: Run tests + run: tox -e py314t-test-opentelemetry-process-context -- -ra + + pypy3-test-opentelemetry-process-context_ubuntu-latest: + name: opentelemetry-process-context pypy-3.10 Ubuntu + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - name: Checkout repo @ SHA - ${{ github.sha }} + uses: actions/checkout@v4 + + - name: Set up Python pypy-3.10 + uses: actions/setup-python@v5 + with: + python-version: "pypy-3.10" + + - name: Install tox + run: pip install tox-uv + + - name: Run tests + run: tox -e pypy3-test-opentelemetry-process-context -- -ra + py310-test-opentelemetry-propagator-b3_ubuntu-latest: name: opentelemetry-propagator-b3 3.10 Ubuntu runs-on: ubuntu-latest diff --git a/opentelemetry-process-context/pyproject.toml b/opentelemetry-process-context/pyproject.toml index 17f4c1d9ccc..5935689004a 100644 --- a/opentelemetry-process-context/pyproject.toml +++ b/opentelemetry-process-context/pyproject.toml @@ -17,6 +17,7 @@ classifiers = [ "Framework :: OpenTelemetry", "Framework :: OpenTelemetry :: Profiles", "Intended Audience :: Developers", + "Operating System :: POSIX :: Linux", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", diff --git a/opentelemetry-process-context/src/opentelemetry/process_context/__init__.py b/opentelemetry-process-context/src/opentelemetry/process_context/__init__.py index 6932ef75864..41c288f8c69 100644 --- a/opentelemetry-process-context/src/opentelemetry/process_context/__init__.py +++ b/opentelemetry-process-context/src/opentelemetry/process_context/__init__.py @@ -1,6 +1,10 @@ # Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 -from opentelemetry.process_context._rs import publish_context +from opentelemetry.process_context._rs import ( + publish_context, + unpublish_context, + update_context, +) -__all__ = ["publish_context"] +__all__ = ["publish_context", "update_context", "unpublish_context"] diff --git a/opentelemetry-process-context/src/opentelemetry/process_context/_rs/__init__.pyi b/opentelemetry-process-context/src/opentelemetry/process_context/_rs/__init__.pyi index e94289db917..026ba1c1dcd 100644 --- a/opentelemetry-process-context/src/opentelemetry/process_context/_rs/__init__.pyi +++ b/opentelemetry-process-context/src/opentelemetry/process_context/_rs/__init__.pyi @@ -3,6 +3,37 @@ from opentelemetry.sdk.resources import Resource -def publish_context(resource: Resource) -> None: ... -def update_context(resource: Resource) -> None: ... -def unpublish_context() -> None: ... +def publish_context(resource: Resource) -> None: + """Publish the process context for the given resource. + + Encodes ``resource`` as a protobuf ``ProcessContext`` message and writes it + to a named memory mapping (``OTEL_CTX``) that out of process readers such + as the OpenTelemetry eBPF Profiler can discover via ``/proc//maps``. + + The context is a per-process singleton. Calling this function a second time + without an intervening :func:`unpublish_context` raises :exc:`RuntimeError`. + + :param resource: The SDK resource whose attributes are to be published. + :raises RuntimeError: If a context has already been published. + :raises OSError: If the memory mapping or clock could not be initialized. + """ + +def update_context(resource: Resource) -> None: + """Update the published process context with a new resource. + + :func:`publish_context` must be called before this function. + + :param resource: The updated SDK resource to publish. + :raises RuntimeError: If no context has been published yet. + :raises OSError: If the clock could not be read. + """ + +def unpublish_context() -> None: + """Remove the published process context. + + Zeros the publish timestamp and unmaps the ``OTEL_CTX`` memory region. + After this call returns, :func:`publish_context` may be called again. + + :raises RuntimeError: If no context has been published yet. + :raises OSError: If unmapping the memory region failed. + """ diff --git a/opentelemetry-process-context/test-requirements.in b/opentelemetry-process-context/test-requirements.in index 0a42b62d9ec..00297963bf6 100644 --- a/opentelemetry-process-context/test-requirements.in +++ b/opentelemetry-process-context/test-requirements.in @@ -4,4 +4,6 @@ pluggy==1.6.0 pytest==7.4.4 -e opentelemetry-api -e opentelemetry-sdk +-e opentelemetry-semantic-conventions +-e opentelemetry-proto -e opentelemetry-process-context diff --git a/opentelemetry-process-context/test-requirements.txt b/opentelemetry-process-context/test-requirements.txt index 1d551511d46..e67e85ce843 100644 --- a/opentelemetry-process-context/test-requirements.txt +++ b/opentelemetry-process-context/test-requirements.txt @@ -1,7 +1,5 @@ # This file was autogenerated by uv via the following command: # uv pip compile --python 3.10 --universal --resolution highest opentelemetry-process-context/test-requirements.in -o opentelemetry-process-context/test-requirements.txt --e file:///Users/lukas/Documents/github-projects/opentelemetry-python/opentelemetry-semantic-conventions - # via opentelemetry-sdk -e opentelemetry-api # via # -r opentelemetry-process-context/test-requirements.in @@ -10,10 +8,16 @@ # opentelemetry-semantic-conventions -e opentelemetry-process-context # via -r opentelemetry-process-context/test-requirements.in +-e opentelemetry-proto + # via -r opentelemetry-process-context/test-requirements.in -e opentelemetry-sdk # via # -r opentelemetry-process-context/test-requirements.in # opentelemetry-process-context +-e opentelemetry-semantic-conventions + # via + # -r opentelemetry-process-context/test-requirements.in + # opentelemetry-sdk colorama==0.4.6 ; sys_platform == 'win32' # via pytest exceptiongroup==1.3.1 ; python_full_version < '3.11' @@ -30,6 +34,8 @@ pluggy==1.6.0 # via # -r opentelemetry-process-context/test-requirements.in # pytest +protobuf==7.35.1 + # via opentelemetry-proto pytest==7.4.4 # via -r opentelemetry-process-context/test-requirements.in tomli==2.4.1 ; python_full_version < '3.11' diff --git a/opentelemetry-process-context/tests/test_process_context.py b/opentelemetry-process-context/tests/test_process_context.py new file mode 100644 index 00000000000..a96318ca52c --- /dev/null +++ b/opentelemetry-process-context/tests/test_process_context.py @@ -0,0 +1,119 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +import struct +import subprocess +import sys +import textwrap +import unittest + +from opentelemetry.process_context import ( + publish_context, + unpublish_context, + update_context, +) +from opentelemetry.sdk.resources import Resource + + +class TestPublishContext(unittest.TestCase): + def tearDown(self): + try: + unpublish_context() + except RuntimeError: + pass + + def test_publish_context_lifecycle(self): + resource = Resource( + {"service.name": "test", "version": 1, "pi": 3.14, "active": True} + ) + self.assertIsNone(publish_context(resource)) + + with self.assertRaises(RuntimeError): + publish_context(resource) + + self.assertIsNone(update_context(resource)) + self.assertIsNone(update_context(resource)) + + self.assertIsNone(unpublish_context()) + self.assertIsNone(publish_context(resource)) + + def test_update_before_publish_raises(self): + with self.assertRaises(RuntimeError): + update_context(Resource({})) + + def test_unpublish_before_publish_raises(self): + with self.assertRaises(RuntimeError): + unpublish_context() + + def test_cross_process_memory_region(self): + """Spawn a child that publishes a fixed context; read and validate its memory region.""" + child_script = textwrap.dedent("""\ + import sys + from opentelemetry.sdk.resources import Resource + from opentelemetry.process_context._rs import publish_context + + resource = Resource({"service.name": "otel-test-service", "version": 42}) + publish_context(resource) + + sys.stdout.write("ready\\n") + sys.stdout.flush() + sys.stdin.readline() + """) + + # pylint: disable-next=consider-using-with + proc = subprocess.Popen( + [sys.executable, "-c", child_script], + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + ) + try: + self.assertEqual(proc.stdout.readline(), b"ready\n") + + pid = proc.pid + + # Locate the OTEL_CTX mapping and verify the memfd name. + start_addr = None + with open(f"/proc/{pid}/maps", encoding="utf-8") as maps_file: + for maps_line in maps_file: + if "OTEL_CTX" in maps_line: + start_addr = int(maps_line.split("-")[0], 16) + self.assertIn("/memfd:OTEL_CTX", maps_line) + break + + self.assertIsNotNone( + start_addr, + f"OTEL_CTX mapping not found in /proc/{pid}/maps", + ) + + # Read the 32-byte header and the variable-length payload in one + # open so there is no TOCTOU window between the two reads. + with open(f"/proc/{pid}/mem", "rb") as mem: + mem.seek(start_addr) + header_bytes = mem.read(32) + + signature = header_bytes[0:8] + version = struct.unpack_from(" Date: Sun, 21 Jun 2026 22:33:20 -0400 Subject: [PATCH 07/35] fix formatting --- .github/workflows/generate_workflows.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/generate_workflows.py b/.github/workflows/generate_workflows.py index 1f314daacf1..549f3b1d2e2 100644 --- a/.github/workflows/generate_workflows.py +++ b/.github/workflows/generate_workflows.py @@ -97,7 +97,9 @@ def get_test_job_datas( if tox_test_env_match is None: continue - if (platform := env_platforms.get(tox_env, "")) and not fullmatch(platform, sys_platform): + if (platform := env_platforms.get(tox_env, "")) and not fullmatch( + platform, sys_platform + ): continue groups = tox_test_env_match.groupdict() From 35564391f114104f405d552156765dfe5a102674 Mon Sep 17 00:00:00 2001 From: Lukas Hering Date: Sun, 21 Jun 2026 22:52:53 -0400 Subject: [PATCH 08/35] fix codespell issues --- .codespellrc | 4 +- opentelemetry-process-context/rust/Cargo.lock | 65 +++++++++++++++++++ opentelemetry-process-context/rust/Cargo.toml | 1 + opentelemetry-process-context/rust/build.rs | 5 +- 4 files changed, 72 insertions(+), 3 deletions(-) diff --git a/.codespellrc b/.codespellrc index 788c648bc5b..981b4e224f2 100644 --- a/.codespellrc +++ b/.codespellrc @@ -1,4 +1,4 @@ [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 +skip = ./.tox,./.mypy_cache,./docs/_build,./target,*/LICENSE,./venv,.git,./opentelemetry-semantic-conventions,*-requirements*.txt,./opentelemetry-process-context/rust/target +ignore-words-list = ans,ue,ot,hist,ro,astroid,crate diff --git a/opentelemetry-process-context/rust/Cargo.lock b/opentelemetry-process-context/rust/Cargo.lock index 110a6c8ea78..2891db83fd2 100644 --- a/opentelemetry-process-context/rust/Cargo.lock +++ b/opentelemetry-process-context/rust/Cargo.lock @@ -443,6 +443,7 @@ dependencies = [ "nix", "prost", "prost-build", + "protoc-bin-vendored", "pyo3", "ureq", ] @@ -555,6 +556,70 @@ dependencies = [ "prost", ] +[[package]] +name = "protoc-bin-vendored" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1c381df33c98266b5f08186583660090a4ffa0889e76c7e9a5e175f645a67fa" +dependencies = [ + "protoc-bin-vendored-linux-aarch_64", + "protoc-bin-vendored-linux-ppcle_64", + "protoc-bin-vendored-linux-s390_64", + "protoc-bin-vendored-linux-x86_32", + "protoc-bin-vendored-linux-x86_64", + "protoc-bin-vendored-macos-aarch_64", + "protoc-bin-vendored-macos-x86_64", + "protoc-bin-vendored-win32", +] + +[[package]] +name = "protoc-bin-vendored-linux-aarch_64" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c350df4d49b5b9e3ca79f7e646fde2377b199e13cfa87320308397e1f37e1a4c" + +[[package]] +name = "protoc-bin-vendored-linux-ppcle_64" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a55a63e6c7244f19b5c6393f025017eb5d793fd5467823a099740a7a4222440c" + +[[package]] +name = "protoc-bin-vendored-linux-s390_64" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dba5565db4288e935d5330a07c264a4ee8e4a5b4a4e6f4e83fad824cc32f3b0" + +[[package]] +name = "protoc-bin-vendored-linux-x86_32" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8854774b24ee28b7868cd71dccaae8e02a2365e67a4a87a6cd11ee6cdbdf9cf5" + +[[package]] +name = "protoc-bin-vendored-linux-x86_64" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b38b07546580df720fa464ce124c4b03630a6fb83e05c336fea2a241df7e5d78" + +[[package]] +name = "protoc-bin-vendored-macos-aarch_64" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89278a9926ce312e51f1d999fee8825d324d603213344a9a706daa009f1d8092" + +[[package]] +name = "protoc-bin-vendored-macos-x86_64" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81745feda7ccfb9471d7a4de888f0652e806d5795b61480605d4943176299756" + +[[package]] +name = "protoc-bin-vendored-win32" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95067976aca6421a523e491fce939a3e65249bac4b977adee0ee9771568e8aa3" + [[package]] name = "pyo3" version = "0.28.3" diff --git a/opentelemetry-process-context/rust/Cargo.toml b/opentelemetry-process-context/rust/Cargo.toml index 71fddaf202f..765621b34b0 100644 --- a/opentelemetry-process-context/rust/Cargo.toml +++ b/opentelemetry-process-context/rust/Cargo.toml @@ -17,5 +17,6 @@ features = ["abi3-py39"] [build-dependencies] prost-build = "0.13" +protoc-bin-vendored = "3" ureq = { version = "2", features = ["tls"] } backon = "1" diff --git a/opentelemetry-process-context/rust/build.rs b/opentelemetry-process-context/rust/build.rs index 931d179824f..7747b717758 100644 --- a/opentelemetry-process-context/rust/build.rs +++ b/opentelemetry-process-context/rust/build.rs @@ -39,5 +39,8 @@ fn main() { let process_context_proto = proto_root .join("opentelemetry/proto/processcontext/v1development/process_context.proto"); - prost_build::compile_protos(&[process_context_proto], &[proto_root]).unwrap(); + prost_build::Config::new() + .protoc_executable(protoc_bin_vendored::protoc_bin_path().unwrap()) + .compile_protos(&[process_context_proto], &[proto_root]) + .unwrap(); } From 9e50fed5a63fef5f233e4ae4ed2f879525f021a4 Mon Sep 17 00:00:00 2001 From: Lukas Hering Date: Sun, 21 Jun 2026 23:10:23 -0400 Subject: [PATCH 09/35] fix test assertions and remove pypy3 --- opentelemetry-process-context/tests/test_process_context.py | 2 +- tox.ini | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/opentelemetry-process-context/tests/test_process_context.py b/opentelemetry-process-context/tests/test_process_context.py index a96318ca52c..d3dd1b49829 100644 --- a/opentelemetry-process-context/tests/test_process_context.py +++ b/opentelemetry-process-context/tests/test_process_context.py @@ -79,7 +79,7 @@ def test_cross_process_memory_region(self): for maps_line in maps_file: if "OTEL_CTX" in maps_line: start_addr = int(maps_line.split("-")[0], 16) - self.assertIn("/memfd:OTEL_CTX", maps_line) + self.assertIn(":OTEL_CTX", maps_line) break self.assertIsNotNone( diff --git a/tox.ini b/tox.ini index 5a2022f62ea..911bb9d0a89 100644 --- a/tox.ini +++ b/tox.ini @@ -97,7 +97,7 @@ envlist = lint-opentelemetry-exporter-zipkin-json py3{10,11,12,13,14,14t}-test-opentelemetry-process-context - pypy3-test-opentelemetry-process-context + ; intentionally excluded from pypy3 (can't build) lint-opentelemetry-process-context py3{10,11,12,13,14,14t}-test-opentelemetry-propagator-b3 From f2031c30794405ee49794c6a33a289cc77a115a2 Mon Sep 17 00:00:00 2001 From: Lukas Hering Date: Sun, 21 Jun 2026 23:24:22 -0400 Subject: [PATCH 10/35] rerun generate-workflows --- .github/workflows/test.yml | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6cd98d119c5..4219292b92c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -3200,25 +3200,6 @@ jobs: - name: Run tests run: tox -e py314t-test-opentelemetry-process-context -- -ra - pypy3-test-opentelemetry-process-context_ubuntu-latest: - name: opentelemetry-process-context pypy-3.10 Ubuntu - runs-on: ubuntu-latest - timeout-minutes: 30 - steps: - - name: Checkout repo @ SHA - ${{ github.sha }} - uses: actions/checkout@v4 - - - name: Set up Python pypy-3.10 - uses: actions/setup-python@v5 - with: - python-version: "pypy-3.10" - - - name: Install tox - run: pip install tox-uv - - - name: Run tests - run: tox -e pypy3-test-opentelemetry-process-context -- -ra - py310-test-opentelemetry-propagator-b3_ubuntu-latest: name: opentelemetry-propagator-b3 3.10 Ubuntu runs-on: ubuntu-latest From 5d169fbd20f7fc1b13ff553d1e3d6d2827c6aebb Mon Sep 17 00:00:00 2001 From: Lukas Hering Date: Sun, 21 Jun 2026 23:25:44 -0400 Subject: [PATCH 11/35] remove process context tests from typecheck --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 2c2b7e9bd9c..070a8b825ff 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -152,6 +152,7 @@ exclude = [ "exporter/opentelemetry-exporter-otlp-proto-http/tests", "exporter/opentelemetry-exporter-otlp-json-common/tests", "exporter/opentelemetry-exporter-otlp-json-common/benchmarks", + "opentelemetry-process-context/tests" ] # When packages are correct typed add them to the strict list From 74a1c4079fd1f3e414a1dbf92ae03aa0caa614b0 Mon Sep 17 00:00:00 2001 From: Lukas Hering Date: Sun, 21 Jun 2026 23:35:20 -0400 Subject: [PATCH 12/35] update release workflow --- .github/workflows/release.yml | 75 +++++++++++++++++++++++++++++++++-- 1 file changed, 71 insertions(+), 4 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3179fc3486e..1b739f19462 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -6,7 +6,64 @@ permissions: contents: read jobs: + build-process-context-wheels: + name: process-context wheels (${{ matrix.platform.target }} ${{ matrix.platform.manylinux }}) + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + platform: + # manylinux + - { target: x86_64, manylinux: auto } + - { target: x86, manylinux: auto } + - { target: aarch64, manylinux: auto } + - { target: armv7, manylinux: auto } + - { target: ppc64le, manylinux: auto } + - { target: s390x, manylinux: auto } + # musllinux + - { target: x86_64, manylinux: musllinux_1_2 } + - { target: x86, manylinux: musllinux_1_2 } + - { target: aarch64, manylinux: musllinux_1_2 } + - { target: armv7, manylinux: musllinux_1_2 } + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + # opentelemetry-process-context is a maturin/PyO3 abi3 extension, its Cargo + # manifest lives in the rust/ subdirectory, so --manifest-path is required. + - name: Build wheels + uses: PyO3/maturin-action@fdd104694b233054d6e985e29b3af9f126af7ac6 # v1.9.0 + with: + working-directory: opentelemetry-process-context + target: ${{ matrix.platform.target }} + manylinux: ${{ matrix.platform.manylinux }} + args: --release --out dist --manifest-path rust/Cargo.toml + sccache: 'true' + + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: process-context-wheels-${{ matrix.platform.target }}-${{ matrix.platform.manylinux }} + path: opentelemetry-process-context/dist + + build-process-context-sdist: + name: process-context sdist + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Build sdist + uses: PyO3/maturin-action@fdd104694b233054d6e985e29b3af9f126af7ac6 # v1.9.0 + with: + working-directory: opentelemetry-process-context + command: sdist + args: --out dist --manifest-path rust/Cargo.toml + + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: process-context-sdist + path: opentelemetry-process-context/dist + release: + needs: [build-process-context-wheels, build-process-context-sdist] permissions: contents: write # required for creating GitHub releases runs-on: ubuntu-latest @@ -17,7 +74,7 @@ jobs: exit 1 fi - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Install toml run: pip install toml @@ -64,21 +121,31 @@ jobs: # 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@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: ref: main # back to the release branch - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 # next few steps publish to pypi - - uses: actions/setup-python@v5 + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: '3.10' - name: Build wheels run: ./scripts/build.sh + # Native wheels + sdist for opentelemetry-process-context, built by the + # build-process-context-* jobs above, are merged into dist/ so the twine + # uploads below publish them alongside the pure-Python packages. + - name: Download process-context wheels and sdist + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + with: + pattern: process-context-* + merge-multiple: true + path: dist + - name: Install twine run: | pip install twine From 23f4267533ecd00349b269516f0c5ea18e5c9ed4 Mon Sep 17 00:00:00 2001 From: Lukas Hering Date: Sun, 21 Jun 2026 23:38:12 -0400 Subject: [PATCH 13/35] fix workflow bugs --- .github/workflows/release.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1b739f19462..5aa23c37e65 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -29,20 +29,21 @@ jobs: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 # opentelemetry-process-context is a maturin/PyO3 abi3 extension, its Cargo - # manifest lives in the rust/ subdirectory, so --manifest-path is required. + # manifest lives in the rust/ subdirectory. maturin-action runs from the + # repo root and has no working-directory input, so point --manifest-path at + # the full path; maturin locates pyproject.toml by walking up from there. - name: Build wheels uses: PyO3/maturin-action@fdd104694b233054d6e985e29b3af9f126af7ac6 # v1.9.0 with: - working-directory: opentelemetry-process-context target: ${{ matrix.platform.target }} manylinux: ${{ matrix.platform.manylinux }} - args: --release --out dist --manifest-path rust/Cargo.toml + args: --release --out dist --manifest-path opentelemetry-process-context/rust/Cargo.toml sccache: 'true' - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: process-context-wheels-${{ matrix.platform.target }}-${{ matrix.platform.manylinux }} - path: opentelemetry-process-context/dist + path: dist build-process-context-sdist: name: process-context sdist @@ -53,14 +54,13 @@ jobs: - name: Build sdist uses: PyO3/maturin-action@fdd104694b233054d6e985e29b3af9f126af7ac6 # v1.9.0 with: - working-directory: opentelemetry-process-context command: sdist - args: --out dist --manifest-path rust/Cargo.toml + args: --out dist --manifest-path opentelemetry-process-context/rust/Cargo.toml - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: process-context-sdist - path: opentelemetry-process-context/dist + path: dist release: needs: [build-process-context-wheels, build-process-context-sdist] From bd309c268f36163470fe034bcfd89082ac2ded72 Mon Sep 17 00:00:00 2001 From: Lukas Hering Date: Sun, 21 Jun 2026 23:41:16 -0400 Subject: [PATCH 14/35] update maturin action commit pin --- .github/workflows/release.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5aa23c37e65..4bb1193eb24 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -33,7 +33,7 @@ jobs: # repo root and has no working-directory input, so point --manifest-path at # the full path; maturin locates pyproject.toml by walking up from there. - name: Build wheels - uses: PyO3/maturin-action@fdd104694b233054d6e985e29b3af9f126af7ac6 # v1.9.0 + uses: PyO3/maturin-action@3e2bdf6ba6453a61e649744019b8a2d906c7eb38 # v1.51.0 with: target: ${{ matrix.platform.target }} manylinux: ${{ matrix.platform.manylinux }} @@ -52,7 +52,7 @@ jobs: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Build sdist - uses: PyO3/maturin-action@fdd104694b233054d6e985e29b3af9f126af7ac6 # v1.9.0 + uses: PyO3/maturin-action@3e2bdf6ba6453a61e649744019b8a2d906c7eb38 # v1.51.0 with: command: sdist args: --out dist --manifest-path opentelemetry-process-context/rust/Cargo.toml From 8b4e7b9a23cdf1592fa3e0dca821c7194eaa7f78 Mon Sep 17 00:00:00 2001 From: Lukas Hering Date: Sun, 21 Jun 2026 23:53:33 -0400 Subject: [PATCH 15/35] small tweak --- .github/workflows/release.yml | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4bb1193eb24..82001a9aac0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -28,10 +28,6 @@ jobs: steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - # opentelemetry-process-context is a maturin/PyO3 abi3 extension, its Cargo - # manifest lives in the rust/ subdirectory. maturin-action runs from the - # repo root and has no working-directory input, so point --manifest-path at - # the full path; maturin locates pyproject.toml by walking up from there. - name: Build wheels uses: PyO3/maturin-action@3e2bdf6ba6453a61e649744019b8a2d906c7eb38 # v1.51.0 with: @@ -137,8 +133,8 @@ jobs: run: ./scripts/build.sh # Native wheels + sdist for opentelemetry-process-context, built by the - # build-process-context-* jobs above, are merged into dist/ so the twine - # uploads below publish them alongside the pure-Python packages. + # build-process-context-* jobs above are merged into dist/ so the twine + # uploads below publish them alongside the pure Python packages. - name: Download process-context wheels and sdist uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: From 1f4e4522e721ca788342f407252098627fe8b529 Mon Sep 17 00:00:00 2001 From: Lukas Hering Date: Sun, 21 Jun 2026 23:57:31 -0400 Subject: [PATCH 16/35] small tweak to tests --- .../tests/test_process_context.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/opentelemetry-process-context/tests/test_process_context.py b/opentelemetry-process-context/tests/test_process_context.py index d3dd1b49829..a6c137fc579 100644 --- a/opentelemetry-process-context/tests/test_process_context.py +++ b/opentelemetry-process-context/tests/test_process_context.py @@ -48,7 +48,7 @@ def test_unpublish_before_publish_raises(self): unpublish_context() def test_cross_process_memory_region(self): - """Spawn a child that publishes a fixed context; read and validate its memory region.""" + """Spawn a child that publishes a fixed context and read/validate its memory region.""" child_script = textwrap.dedent("""\ import sys from opentelemetry.sdk.resources import Resource @@ -73,7 +73,7 @@ def test_cross_process_memory_region(self): pid = proc.pid - # Locate the OTEL_CTX mapping and verify the memfd name. + # Locate the OTEL_CTX mapping and verify the name. start_addr = None with open(f"/proc/{pid}/maps", encoding="utf-8") as maps_file: for maps_line in maps_file: @@ -87,8 +87,6 @@ def test_cross_process_memory_region(self): f"OTEL_CTX mapping not found in /proc/{pid}/maps", ) - # Read the 32-byte header and the variable-length payload in one - # open so there is no TOCTOU window between the two reads. with open(f"/proc/{pid}/mem", "rb") as mem: mem.seek(start_addr) header_bytes = mem.read(32) @@ -107,9 +105,6 @@ def test_cross_process_memory_region(self): self.assertGreater(payload_size, 0) self.assertGreater(timestamp_ns, 0) self.assertNotEqual(payload_ptr, 0) - - # Protobuf string fields are length-prefixed raw UTF-8, so the - # attribute key and value appear verbatim in the serialised payload. self.assertIn(b"service.name", payload_bytes) self.assertIn(b"otel-test-service", payload_bytes) From c774c6b981a35945a5a6ffb11a2b8cc06a325dc0 Mon Sep 17 00:00:00 2001 From: Lukas Hering Date: Tue, 23 Jun 2026 20:29:34 -0400 Subject: [PATCH 17/35] change update logic --- opentelemetry-process-context/rust/src/publish.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/opentelemetry-process-context/rust/src/publish.rs b/opentelemetry-process-context/rust/src/publish.rs index 550c3b86292..a2de72ebc8f 100644 --- a/opentelemetry-process-context/rust/src/publish.rs +++ b/opentelemetry-process-context/rust/src/publish.rs @@ -157,7 +157,8 @@ pub fn update(payload: Vec) -> Result<(), PublishError> { // Zero timestamp with Release ensuring the previous payload state is // visible to readers that observe the "update in progress" signal. - published_at.store(0, Ordering::Release); + published_at.store(0, Ordering::Relaxed); + std::sync::atomic::fence(Ordering::Release); // Rewrite payload fields between the two Release stores. std::ptr::addr_of_mut!((*header).payload_size).write(new_buf.len() as u32); From a39ddede4595e218389fbaff1fd6f096545a26ad Mon Sep 17 00:00:00 2001 From: Lukas Hering Date: Tue, 23 Jun 2026 21:59:50 -0500 Subject: [PATCH 18/35] update comments --- .../rust/src/publish.rs | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/opentelemetry-process-context/rust/src/publish.rs b/opentelemetry-process-context/rust/src/publish.rs index a2de72ebc8f..a116f4b3d93 100644 --- a/opentelemetry-process-context/rust/src/publish.rs +++ b/opentelemetry-process-context/rust/src/publish.rs @@ -123,7 +123,8 @@ pub fn publish(payload: Vec) -> Result<(), PublishError> { std::ptr::addr_of_mut!((*header).payload_size).write(payload_buf.len() as u32); std::ptr::addr_of_mut!((*header).payload).write(payload_buf.as_ptr() as u64); - // Write the timestamp last with release ordering. + // Write the timestamp last with release ordering, ensuring that + // all writes above are not reordered after the timestamp store let published_at = &*std::ptr::addr_of!((*header).monotonic_published_at_ns); published_at.store(timestamp, Ordering::Release); } @@ -155,9 +156,11 @@ pub fn update(payload: Vec) -> Result<(), PublishError> { let header = region.ptr.as_ptr().cast::
(); let published_at = &*std::ptr::addr_of!((*header).monotonic_published_at_ns); - // Zero timestamp with Release ensuring the previous payload state is - // visible to readers that observe the "update in progress" signal. + // Zero timestamp signal the "update in progress" state. published_at.store(0, Ordering::Relaxed); + // An `Ordering::Release` fence is needed here to ensure that the + // preceding "update in progress" write above is not reordered with + // any of the proceeding writes that update the region. std::sync::atomic::fence(Ordering::Release); // Rewrite payload fields between the two Release stores. @@ -186,12 +189,15 @@ pub fn unpublish() -> Result<(), PublishError> { let mut guard = MAPPING.lock().expect("process context mutex poisoned"); let region = guard.take().ok_or(PublishError::NotPublished)?; - // Zero the timestamp with Release before removing the mapping so any - // reader still observing it sees an invalid (zero) state. + // Zero the timestamp and remove the mapping. unsafe { let header = region.ptr.as_ptr().cast::
(); let published_at = &*std::ptr::addr_of!((*header).monotonic_published_at_ns); - published_at.store(0, Ordering::Release); + // No ordering constraint is required because regardless of ordering, + // a reader can observe a valid timestamp before the store and subsequently + // attempt to read from the header or payload pointer after it has already + // deallocated/unmapped. + published_at.store(0, Ordering::Relaxed); } unsafe { nix::sys::mman::munmap(region.ptr, HEADER_SIZE) } From 5e6c845a22e920a5da76398f7b55b785c74ecf72 Mon Sep 17 00:00:00 2001 From: Lukas Hering <40302054+herin049@users.noreply.github.com> Date: Tue, 23 Jun 2026 22:01:48 -0500 Subject: [PATCH 19/35] Update opentelemetry-process-context/pyproject.toml Co-authored-by: Diego Hurtado --- opentelemetry-process-context/pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opentelemetry-process-context/pyproject.toml b/opentelemetry-process-context/pyproject.toml index 5935689004a..14ff909b075 100644 --- a/opentelemetry-process-context/pyproject.toml +++ b/opentelemetry-process-context/pyproject.toml @@ -32,7 +32,7 @@ dependencies = [ ] [project.urls] -Homepage = "https://github.com/open-telemetry/opentelemetry-python/tree/main/popentelemetry-process-context" +Homepage = "https://github.com/open-telemetry/opentelemetry-python/tree/main/opentelemetry-process-context" Repository = "https://github.com/open-telemetry/opentelemetry-python" [tool.maturin] From 66d6e50c827c9e89dd83fe2a42e364ddd07c67d5 Mon Sep 17 00:00:00 2001 From: Lukas Hering Date: Tue, 23 Jun 2026 22:33:30 -0500 Subject: [PATCH 20/35] combine publish/update into single function --- .../rust/src/{publish.rs => context.rs} | 84 +++++++++---------- opentelemetry-process-context/rust/src/lib.rs | 14 +--- .../opentelemetry/process_context/__init__.py | 3 +- .../process_context/_rs/__init__.pyi | 21 ++--- .../tests/test_process_context.py | 13 +-- 5 files changed, 53 insertions(+), 82 deletions(-) rename opentelemetry-process-context/rust/src/{publish.rs => context.rs} (85%) diff --git a/opentelemetry-process-context/rust/src/publish.rs b/opentelemetry-process-context/rust/src/context.rs similarity index 85% rename from opentelemetry-process-context/rust/src/publish.rs rename to opentelemetry-process-context/rust/src/context.rs index a116f4b3d93..15c19397f74 100644 --- a/opentelemetry-process-context/rust/src/publish.rs +++ b/opentelemetry-process-context/rust/src/context.rs @@ -56,10 +56,6 @@ static MAPPING: Mutex> = Mutex::new(None); #[derive(Debug)] pub enum PublishError { - /// A process context has already been published for this process. - AlreadyPublished, - /// `update()` was called before any context was published. - NotPublished, /// The backing memory region could not be allocated. Alloc, /// `madvise(MADV_DONTFORK)` failed. @@ -68,17 +64,13 @@ pub enum PublishError { Clock, /// `munmap` of the header mapping failed during unpublish. Munmap, + /// `unpublish()` was called before any context was published. + NotPublished, } impl From for PyErr { fn from(err: PublishError) -> Self { match err { - PublishError::AlreadyPublished => { - PyRuntimeError::new_err("a process context has already been published") - } - PublishError::NotPublished => { - PyRuntimeError::new_err("no process context has been published yet") - } PublishError::Alloc => { PyOSError::new_err("failed to allocate the process context mapping") } @@ -91,40 +83,34 @@ impl From for PyErr { PublishError::Munmap => { PyOSError::new_err("munmap of the process context mapping failed") } + PublishError::NotPublished => { + PyRuntimeError::new_err("no process context has been published yet") + } } } } -/// Publish `payload` as the process context. -/// -/// The context is a per-process singleton: calling this a second time without a -/// prior teardown returns [`PublishError::AlreadyPublished`]. -pub fn publish(payload: Vec) -> Result<(), PublishError> { - let mut guard = MAPPING.lock().expect("process context mutex poisoned"); - if guard.is_some() { - return Err(PublishError::AlreadyPublished); - } - +/// Allocate the mapping and write the initial header. Called with the mutex held +/// and `guard` confirmed to be `None`. +fn publish_new(guard: &mut Option, payload: Vec) -> Result<(), PublishError> { let ptr = alloc_region()?; advise_dontfork(ptr, HEADER_SIZE)?; let timestamp = get_boottime_ns()?; - let payload_buf = payload; - // SAFETY: `ptr` points to a freshly mapped, zero initialized, page aligned - // region of exactly `HEADER_SIZE` bytes. The payload lives in `payload_buf` - // on the heap and the header's `payload` field stores a pointer into it. + // region of exactly `HEADER_SIZE` bytes. The payload lives in `payload` on + // the heap and the header's `payload` field stores a pointer into it. unsafe { let header = ptr.as_ptr().cast::
(); std::ptr::addr_of_mut!((*header).signature).write(SIGNATURE); std::ptr::addr_of_mut!((*header).version).write(VERSION); - std::ptr::addr_of_mut!((*header).payload_size).write(payload_buf.len() as u32); - std::ptr::addr_of_mut!((*header).payload).write(payload_buf.as_ptr() as u64); + std::ptr::addr_of_mut!((*header).payload_size).write(payload.len() as u32); + std::ptr::addr_of_mut!((*header).payload).write(payload.as_ptr() as u64); // Write the timestamp last with release ordering, ensuring that - // all writes above are not reordered after the timestamp store + // all writes above are not reordered after the timestamp store. let published_at = &*std::ptr::addr_of!((*header).monotonic_published_at_ns); published_at.store(timestamp, Ordering::Release); } @@ -133,21 +119,18 @@ pub fn publish(payload: Vec) -> Result<(), PublishError> { // path, failures are ignored per the spec. name_mapping(ptr, HEADER_SIZE); - *guard = Some(Region { ptr, payload: payload_buf }); + *guard = Some(Region { ptr, payload }); Ok(()) } -/// Update the published process context with a new payload. +/// Rewrite the payload of an existing mapping in place. Called with the mutex +/// held and `region` confirmed to be live. /// -/// Follows the spec's Updating Protocol: zeros the timestamp (Release) to signal -/// readers that an update is in progress, rewrites the payload fields and then -/// publishes the new timestamp (Release) to signal completion. The old payload -/// buffer is dropped after the new timestamp is live. -pub fn update(payload: Vec) -> Result<(), PublishError> { - let mut guard = MAPPING.lock().expect("process context mutex poisoned"); - let region = guard.as_mut().ok_or(PublishError::NotPublished)?; - - let new_buf = payload; +/// Follows the spec's Updating Protocol: zeros the timestamp to signal readers +/// that an update is in progress, rewrites the payload fields, then publishes +/// the new timestamp. The old payload buffer is dropped after the new timestamp +/// is live. +fn publish_existing(region: &mut Region, payload: Vec) -> Result<(), PublishError> { let timestamp = get_boottime_ns()?; // SAFETY: `region.ptr` points to the live header mapping with exactly @@ -156,7 +139,7 @@ pub fn update(payload: Vec) -> Result<(), PublishError> { let header = region.ptr.as_ptr().cast::
(); let published_at = &*std::ptr::addr_of!((*header).monotonic_published_at_ns); - // Zero timestamp signal the "update in progress" state. + // Zero timestamp signals the "update in progress" state. published_at.store(0, Ordering::Relaxed); // An `Ordering::Release` fence is needed here to ensure that the // preceding "update in progress" write above is not reordered with @@ -164,8 +147,8 @@ pub fn update(payload: Vec) -> Result<(), PublishError> { std::sync::atomic::fence(Ordering::Release); // Rewrite payload fields between the two Release stores. - std::ptr::addr_of_mut!((*header).payload_size).write(new_buf.len() as u32); - std::ptr::addr_of_mut!((*header).payload).write(new_buf.as_ptr() as u64); + std::ptr::addr_of_mut!((*header).payload_size).write(payload.len() as u32); + std::ptr::addr_of_mut!((*header).payload).write(payload.as_ptr() as u64); // Publish new timestamp with Release ensuring the new payload fields // are visible to readers that observe the "update complete" signal. @@ -175,11 +158,26 @@ pub fn update(payload: Vec) -> Result<(), PublishError> { // Rename the mapping unconditionally (failures ignored per spec). name_mapping(region.ptr, HEADER_SIZE); - // Drop the old payload only after the new timestamp is live - region.payload = new_buf; + // Drop the old payload only after the new timestamp is live. + region.payload = payload; Ok(()) } +/// Publish or update the process context. +/// +/// Creates the named memory mapping on the first call. On subsequent calls the +/// existing mapping is updated in-place using the spec's Updating Protocol +/// (zero timestamp → rewrite payload fields → restore timestamp), so no new +/// mapping is allocated and the header pointer remains stable across updates. +pub fn publish(payload: Vec) -> Result<(), PublishError> { + let mut guard = MAPPING.lock().expect("process context mutex poisoned"); + if guard.is_none() { + publish_new(&mut guard, payload) + } else { + publish_existing(guard.as_mut().unwrap(), payload) + } +} + /// Remove the published process context. /// /// Zeros the timestamp (Release) so readers still observing the mapping see an diff --git a/opentelemetry-process-context/rust/src/lib.rs b/opentelemetry-process-context/rust/src/lib.rs index c4eaba1a248..efae5a3ca3c 100644 --- a/opentelemetry-process-context/rust/src/lib.rs +++ b/opentelemetry-process-context/rust/src/lib.rs @@ -3,7 +3,7 @@ mod convert; pub(crate) mod proto; -mod publish; +mod context; use pyo3::prelude::*; use crate::convert::{encode_process_context, resource_from_py}; @@ -11,20 +11,13 @@ use crate::convert::{encode_process_context, resource_from_py}; #[pyfunction] fn publish_context(resource: &Bound<'_, PyAny>) -> PyResult<()> { let resource = resource_from_py(resource)?; - publish::publish(encode_process_context(resource))?; - Ok(()) -} - -#[pyfunction] -fn update_context(resource: &Bound<'_, PyAny>) -> PyResult<()> { - let resource = resource_from_py(resource)?; - publish::update(encode_process_context(resource))?; + context::publish(encode_process_context(resource))?; Ok(()) } #[pyfunction] fn unpublish_context() -> PyResult<()> { - publish::unpublish()?; + context::unpublish()?; Ok(()) } @@ -32,7 +25,6 @@ fn unpublish_context() -> PyResult<()> { #[pyo3(name = "_rs")] fn init(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_wrapped(wrap_pyfunction!(publish_context))?; - m.add_wrapped(wrap_pyfunction!(update_context))?; m.add_wrapped(wrap_pyfunction!(unpublish_context))?; Ok(()) } diff --git a/opentelemetry-process-context/src/opentelemetry/process_context/__init__.py b/opentelemetry-process-context/src/opentelemetry/process_context/__init__.py index 41c288f8c69..448738b728e 100644 --- a/opentelemetry-process-context/src/opentelemetry/process_context/__init__.py +++ b/opentelemetry-process-context/src/opentelemetry/process_context/__init__.py @@ -4,7 +4,6 @@ from opentelemetry.process_context._rs import ( publish_context, unpublish_context, - update_context, ) -__all__ = ["publish_context", "update_context", "unpublish_context"] +__all__ = ["publish_context", "unpublish_context"] diff --git a/opentelemetry-process-context/src/opentelemetry/process_context/_rs/__init__.pyi b/opentelemetry-process-context/src/opentelemetry/process_context/_rs/__init__.pyi index 026ba1c1dcd..047d679f137 100644 --- a/opentelemetry-process-context/src/opentelemetry/process_context/_rs/__init__.pyi +++ b/opentelemetry-process-context/src/opentelemetry/process_context/_rs/__init__.pyi @@ -4,30 +4,21 @@ from opentelemetry.sdk.resources import Resource def publish_context(resource: Resource) -> None: - """Publish the process context for the given resource. + """Publish or update the process context for the given resource. Encodes ``resource`` as a protobuf ``ProcessContext`` message and writes it - to a named memory mapping (``OTEL_CTX``) that out of process readers such + to a named memory mapping (``OTEL_CTX``) that out-of-process readers such as the OpenTelemetry eBPF Profiler can discover via ``/proc//maps``. - The context is a per-process singleton. Calling this function a second time - without an intervening :func:`unpublish_context` raises :exc:`RuntimeError`. + On the first call the mapping is created and the full header is written. On + subsequent calls the existing mapping is updated in place using the spec's + update protocol, so no new mapping is allocated and the header pointer + remains stable across updates. :param resource: The SDK resource whose attributes are to be published. - :raises RuntimeError: If a context has already been published. :raises OSError: If the memory mapping or clock could not be initialized. """ -def update_context(resource: Resource) -> None: - """Update the published process context with a new resource. - - :func:`publish_context` must be called before this function. - - :param resource: The updated SDK resource to publish. - :raises RuntimeError: If no context has been published yet. - :raises OSError: If the clock could not be read. - """ - def unpublish_context() -> None: """Remove the published process context. diff --git a/opentelemetry-process-context/tests/test_process_context.py b/opentelemetry-process-context/tests/test_process_context.py index a6c137fc579..e1e2ee75afb 100644 --- a/opentelemetry-process-context/tests/test_process_context.py +++ b/opentelemetry-process-context/tests/test_process_context.py @@ -12,7 +12,6 @@ from opentelemetry.process_context import ( publish_context, unpublish_context, - update_context, ) from opentelemetry.sdk.resources import Resource @@ -29,20 +28,12 @@ def test_publish_context_lifecycle(self): {"service.name": "test", "version": 1, "pi": 3.14, "active": True} ) self.assertIsNone(publish_context(resource)) - - with self.assertRaises(RuntimeError): - publish_context(resource) - - self.assertIsNone(update_context(resource)) - self.assertIsNone(update_context(resource)) + self.assertIsNone(publish_context(resource)) + self.assertIsNone(publish_context(resource)) self.assertIsNone(unpublish_context()) self.assertIsNone(publish_context(resource)) - def test_update_before_publish_raises(self): - with self.assertRaises(RuntimeError): - update_context(Resource({})) - def test_unpublish_before_publish_raises(self): with self.assertRaises(RuntimeError): unpublish_context() From 13deb9efcee63c5ab2a5a31bbf6fcbfe2ab408eb Mon Sep 17 00:00:00 2001 From: Lukas Hering Date: Tue, 23 Jun 2026 23:37:52 -0500 Subject: [PATCH 21/35] switch to using protoc bash script for code generation --- opentelemetry-process-context/rust/Cargo.lock | 997 +----------------- opentelemetry-process-context/rust/Cargo.toml | 6 - opentelemetry-process-context/rust/build.rs | 46 - .../opentelemetry.proto.common.v1.rs | 160 +++ ...etry.proto.processcontext.v1development.rs | 38 + .../opentelemetry.proto.resource.v1.rs | 29 + .../rust/src/proto.rs | 14 +- scripts/proto_codegen_process_context.sh | 78 ++ 8 files changed, 318 insertions(+), 1050 deletions(-) delete mode 100644 opentelemetry-process-context/rust/build.rs create mode 100644 opentelemetry-process-context/rust/src/generated/opentelemetry.proto.common.v1.rs create mode 100644 opentelemetry-process-context/rust/src/generated/opentelemetry.proto.processcontext.v1development.rs create mode 100644 opentelemetry-process-context/rust/src/generated/opentelemetry.proto.resource.v1.rs create mode 100755 scripts/proto_codegen_process_context.sh diff --git a/opentelemetry-process-context/rust/Cargo.lock b/opentelemetry-process-context/rust/Cargo.lock index 2891db83fd2..35a899b161c 100644 --- a/opentelemetry-process-context/rust/Cargo.lock +++ b/opentelemetry-process-context/rust/Cargo.lock @@ -2,72 +2,24 @@ # It is not intended for manual editing. version = 4 -[[package]] -name = "adler2" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" - -[[package]] -name = "aho-corasick" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" -dependencies = [ - "memchr", -] - [[package]] name = "anyhow" version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" -[[package]] -name = "backon" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cffb0e931875b666fc4fcb20fee52e9bbd1ef836fd9e9e04ec21555f9f85f7ef" -dependencies = [ - "fastrand", - "gloo-timers", - "tokio", -] - -[[package]] -name = "base64" -version = "0.22.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" - [[package]] name = "bitflags" version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" -[[package]] -name = "bumpalo" -version = "3.20.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" - [[package]] name = "bytes" version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb593" -[[package]] -name = "cc" -version = "1.2.64" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dad887fd958be91b5098c0248def011f4523ab786cd411be668777e55063501f" -dependencies = [ - "find-msvc-tools", - "shlex", -] - [[package]] name = "cfg-if" version = "1.0.4" @@ -80,277 +32,18 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" -[[package]] -name = "crc32fast" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "displaydoc" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "either" version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" -[[package]] -name = "equivalent" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" - -[[package]] -name = "errno" -version = "0.3.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" -dependencies = [ - "libc", - "windows-sys 0.61.2", -] - -[[package]] -name = "fastrand" -version = "2.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" - -[[package]] -name = "find-msvc-tools" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" - -[[package]] -name = "fixedbitset" -version = "0.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" - -[[package]] -name = "flate2" -version = "1.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" -dependencies = [ - "crc32fast", - "miniz_oxide", -] - -[[package]] -name = "form_urlencoded" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" -dependencies = [ - "percent-encoding", -] - -[[package]] -name = "futures-channel" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" -dependencies = [ - "futures-core", -] - -[[package]] -name = "futures-core" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" - -[[package]] -name = "futures-task" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" - -[[package]] -name = "futures-util" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" -dependencies = [ - "futures-core", - "futures-task", - "pin-project-lite", - "slab", -] - -[[package]] -name = "getrandom" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" -dependencies = [ - "cfg-if", - "libc", - "wasi", -] - -[[package]] -name = "getrandom" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" -dependencies = [ - "cfg-if", - "libc", - "r-efi", -] - -[[package]] -name = "gloo-timers" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" -dependencies = [ - "futures-channel", - "futures-core", - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "hashbrown" -version = "0.17.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" - [[package]] name = "heck" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" -[[package]] -name = "icu_collections" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" -dependencies = [ - "displaydoc", - "potential_utf", - "utf8_iter", - "yoke", - "zerofrom", - "zerovec", -] - -[[package]] -name = "icu_locale_core" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" -dependencies = [ - "displaydoc", - "litemap", - "tinystr", - "writeable", - "zerovec", -] - -[[package]] -name = "icu_normalizer" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" -dependencies = [ - "icu_collections", - "icu_normalizer_data", - "icu_properties", - "icu_provider", - "smallvec", - "zerovec", -] - -[[package]] -name = "icu_normalizer_data" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" - -[[package]] -name = "icu_properties" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" -dependencies = [ - "icu_collections", - "icu_locale_core", - "icu_properties_data", - "icu_provider", - "zerotrie", - "zerovec", -] - -[[package]] -name = "icu_properties_data" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" - -[[package]] -name = "icu_provider" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" -dependencies = [ - "displaydoc", - "icu_locale_core", - "writeable", - "yoke", - "zerofrom", - "zerotrie", - "zerovec", -] - -[[package]] -name = "idna" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" -dependencies = [ - "idna_adapter", - "smallvec", - "utf8_iter", -] - -[[package]] -name = "idna_adapter" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" -dependencies = [ - "icu_normalizer", - "icu_properties", -] - -[[package]] -name = "indexmap" -version = "2.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" -dependencies = [ - "equivalent", - "hashbrown", -] - [[package]] name = "itertools" version = "0.14.0" @@ -360,63 +53,12 @@ dependencies = [ "either", ] -[[package]] -name = "js-sys" -version = "0.3.102" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03d04c30968dffe80775bd4d7fb676131cd04a1fb46d2686dbffbaec2d9dfd31" -dependencies = [ - "cfg-if", - "futures-util", - "wasm-bindgen", -] - [[package]] name = "libc" version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" -[[package]] -name = "linux-raw-sys" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" - -[[package]] -name = "litemap" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" - -[[package]] -name = "log" -version = "0.4.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a" - -[[package]] -name = "memchr" -version = "2.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" - -[[package]] -name = "miniz_oxide" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" -dependencies = [ - "adler2", - "simd-adler32", -] - -[[package]] -name = "multimap" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" - [[package]] name = "nix" version = "0.31.3" @@ -439,62 +81,17 @@ checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" name = "opentelemetry-process-context" version = "0.1.0" dependencies = [ - "backon", "nix", "prost", - "prost-build", - "protoc-bin-vendored", "pyo3", - "ureq", ] -[[package]] -name = "percent-encoding" -version = "2.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" - -[[package]] -name = "petgraph" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772" -dependencies = [ - "fixedbitset", - "indexmap", -] - -[[package]] -name = "pin-project-lite" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" - [[package]] name = "portable-atomic" version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" -[[package]] -name = "potential_utf" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" -dependencies = [ - "zerovec", -] - -[[package]] -name = "prettyplease" -version = "0.2.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" -dependencies = [ - "proc-macro2", - "syn", -] - [[package]] name = "proc-macro2" version = "1.0.106" @@ -514,26 +111,6 @@ dependencies = [ "prost-derive", ] -[[package]] -name = "prost-build" -version = "0.13.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be769465445e8c1474e9c5dac2018218498557af32d9ed057325ec9a41ae81bf" -dependencies = [ - "heck", - "itertools", - "log", - "multimap", - "once_cell", - "petgraph", - "prettyplease", - "prost", - "prost-types", - "regex", - "syn", - "tempfile", -] - [[package]] name = "prost-derive" version = "0.13.5" @@ -547,79 +124,6 @@ dependencies = [ "syn", ] -[[package]] -name = "prost-types" -version = "0.13.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52c2c1bf36ddb1a1c396b3601a3cec27c2462e45f07c386894ec3ccf5332bd16" -dependencies = [ - "prost", -] - -[[package]] -name = "protoc-bin-vendored" -version = "3.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1c381df33c98266b5f08186583660090a4ffa0889e76c7e9a5e175f645a67fa" -dependencies = [ - "protoc-bin-vendored-linux-aarch_64", - "protoc-bin-vendored-linux-ppcle_64", - "protoc-bin-vendored-linux-s390_64", - "protoc-bin-vendored-linux-x86_32", - "protoc-bin-vendored-linux-x86_64", - "protoc-bin-vendored-macos-aarch_64", - "protoc-bin-vendored-macos-x86_64", - "protoc-bin-vendored-win32", -] - -[[package]] -name = "protoc-bin-vendored-linux-aarch_64" -version = "3.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c350df4d49b5b9e3ca79f7e646fde2377b199e13cfa87320308397e1f37e1a4c" - -[[package]] -name = "protoc-bin-vendored-linux-ppcle_64" -version = "3.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a55a63e6c7244f19b5c6393f025017eb5d793fd5467823a099740a7a4222440c" - -[[package]] -name = "protoc-bin-vendored-linux-s390_64" -version = "3.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dba5565db4288e935d5330a07c264a4ee8e4a5b4a4e6f4e83fad824cc32f3b0" - -[[package]] -name = "protoc-bin-vendored-linux-x86_32" -version = "3.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8854774b24ee28b7868cd71dccaae8e02a2365e67a4a87a6cd11ee6cdbdf9cf5" - -[[package]] -name = "protoc-bin-vendored-linux-x86_64" -version = "3.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b38b07546580df720fa464ce124c4b03630a6fb83e05c336fea2a241df7e5d78" - -[[package]] -name = "protoc-bin-vendored-macos-aarch_64" -version = "3.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89278a9926ce312e51f1d999fee8825d324d603213344a9a706daa009f1d8092" - -[[package]] -name = "protoc-bin-vendored-macos-x86_64" -version = "3.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81745feda7ccfb9471d7a4de888f0652e806d5795b61480605d4943176299756" - -[[package]] -name = "protoc-bin-vendored-win32" -version = "3.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95067976aca6421a523e491fce939a3e65249bac4b977adee0ee9771568e8aa3" - [[package]] name = "pyo3" version = "0.28.3" @@ -680,181 +184,13 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "r-efi" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" - -[[package]] -name = "regex" -version = "1.12.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "regex-automata" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.8.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" - -[[package]] -name = "ring" -version = "0.17.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" -dependencies = [ - "cc", - "cfg-if", - "getrandom 0.2.17", - "libc", - "untrusted", - "windows-sys 0.52.0", -] - -[[package]] -name = "rustix" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" -dependencies = [ - "bitflags", - "errno", - "libc", - "linux-raw-sys", - "windows-sys 0.61.2", -] - -[[package]] -name = "rustls" -version = "0.23.40" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" -dependencies = [ - "log", - "once_cell", - "ring", - "rustls-pki-types", - "rustls-webpki", - "subtle", - "zeroize", -] - -[[package]] -name = "rustls-pki-types" -version = "1.14.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" -dependencies = [ - "zeroize", -] - -[[package]] -name = "rustls-webpki" -version = "0.103.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" -dependencies = [ - "ring", - "rustls-pki-types", - "untrusted", -] - -[[package]] -name = "rustversion" -version = "1.0.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" - -[[package]] -name = "serde" -version = "1.0.228" +version = "1.0.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" -dependencies = [ - "serde_core", -] - -[[package]] -name = "serde_core" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" dependencies = [ "proc-macro2", - "quote", - "syn", ] -[[package]] -name = "shlex" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" - -[[package]] -name = "simd-adler32" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" - -[[package]] -name = "slab" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" - -[[package]] -name = "smallvec" -version = "1.15.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" - -[[package]] -name = "stable_deref_trait" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" - -[[package]] -name = "subtle" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" - [[package]] name = "syn" version = "2.0.118" @@ -866,343 +202,14 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "synstructure" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "target-lexicon" version = "0.13.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca" -[[package]] -name = "tempfile" -version = "3.27.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" -dependencies = [ - "fastrand", - "getrandom 0.4.3", - "once_cell", - "rustix", - "windows-sys 0.61.2", -] - -[[package]] -name = "tinystr" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" -dependencies = [ - "displaydoc", - "zerovec", -] - -[[package]] -name = "tokio" -version = "1.52.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" -dependencies = [ - "pin-project-lite", -] - [[package]] name = "unicode-ident" version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" - -[[package]] -name = "untrusted" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" - -[[package]] -name = "ureq" -version = "2.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02d1a66277ed75f640d608235660df48c8e3c19f3b4edb6a263315626cc3c01d" -dependencies = [ - "base64", - "flate2", - "log", - "once_cell", - "rustls", - "rustls-pki-types", - "url", - "webpki-roots 0.26.11", -] - -[[package]] -name = "url" -version = "2.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" -dependencies = [ - "form_urlencoded", - "idna", - "percent-encoding", - "serde", -] - -[[package]] -name = "utf8_iter" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" - -[[package]] -name = "wasi" -version = "0.11.1+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" - -[[package]] -name = "wasm-bindgen" -version = "0.2.125" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ddb3f79143bced6de84270411622a2699cee572fc0875aeaf1e7867cf9fca1a" -dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.125" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e21a184b13fb19e157296e2c46056aec9092264fab83e4ba59e68c61b323c3d" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.125" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fecefd9c35bd935a20fc3fc344b5f29138961e4f47fb03297d88f2587afb5ebd" -dependencies = [ - "bumpalo", - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.125" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23939e44bb9a5d7576fa2b563dc2e136628f1224e88a8deed09e04858b77871f" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "webpki-roots" -version = "0.26.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" -dependencies = [ - "webpki-roots 1.0.8", -] - -[[package]] -name = "webpki-roots" -version = "1.0.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf85cb06032201fa7c6f829d7db5a7e5aa45bcc0655327713065f6f0576731bf" -dependencies = [ - "rustls-pki-types", -] - -[[package]] -name = "windows-link" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" - -[[package]] -name = "windows-sys" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" -dependencies = [ - "windows-targets", -] - -[[package]] -name = "windows-sys" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-targets" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - -[[package]] -name = "windows_i686_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" - -[[package]] -name = "writeable" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" - -[[package]] -name = "yoke" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" -dependencies = [ - "stable_deref_trait", - "yoke-derive", - "zerofrom", -] - -[[package]] -name = "yoke-derive" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "synstructure", -] - -[[package]] -name = "zerofrom" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" -dependencies = [ - "zerofrom-derive", -] - -[[package]] -name = "zerofrom-derive" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "synstructure", -] - -[[package]] -name = "zeroize" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" - -[[package]] -name = "zerotrie" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" -dependencies = [ - "displaydoc", - "yoke", - "zerofrom", -] - -[[package]] -name = "zerovec" -version = "0.11.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" -dependencies = [ - "yoke", - "zerofrom", - "zerovec-derive", -] - -[[package]] -name = "zerovec-derive" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] diff --git a/opentelemetry-process-context/rust/Cargo.toml b/opentelemetry-process-context/rust/Cargo.toml index 765621b34b0..a7ae90bfc7a 100644 --- a/opentelemetry-process-context/rust/Cargo.toml +++ b/opentelemetry-process-context/rust/Cargo.toml @@ -14,9 +14,3 @@ nix = { version = "0.31", features = ["mman", "fs", "time"] } [dependencies.pyo3] version = "0.28.2" features = ["abi3-py39"] - -[build-dependencies] -prost-build = "0.13" -protoc-bin-vendored = "3" -ureq = { version = "2", features = ["tls"] } -backon = "1" diff --git a/opentelemetry-process-context/rust/build.rs b/opentelemetry-process-context/rust/build.rs deleted file mode 100644 index 7747b717758..00000000000 --- a/opentelemetry-process-context/rust/build.rs +++ /dev/null @@ -1,46 +0,0 @@ -// Copyright The OpenTelemetry Authors -// SPDX-License-Identifier: Apache-2.0 - -use std::fs; -use std::path::PathBuf; - -use backon::{BlockingRetryable, ExponentialBuilder}; - -const COMMIT_HASH: &str = "023f8cd36cc946617caa9a9e2e9868186f6d22dd"; - -const UPSTREAM_PROTOS: &[&str] = &[ - "opentelemetry/proto/common/v1/common.proto", - "opentelemetry/proto/resource/v1/resource.proto", - "opentelemetry/proto/processcontext/v1development/process_context.proto", -]; - -const RAW_BASE: &str = - "https://raw.githubusercontent.com/open-telemetry/opentelemetry-proto"; - -fn main() { - let proto_root = PathBuf::from(std::env::var("OUT_DIR").unwrap()).join("proto"); - - for proto_path in UPSTREAM_PROTOS { - let dest = proto_root.join(proto_path); - if dest.exists() { - continue; - } - fs::create_dir_all(dest.parent().unwrap()).unwrap(); - let url = format!("{RAW_BASE}/{COMMIT_HASH}/{proto_path}"); - let content = (|| -> Result> { - Ok(ureq::get(&url).call()?.into_string()?) - }) - .retry(ExponentialBuilder::default()) - .call() - .unwrap(); - fs::write(&dest, content).unwrap(); - } - - let process_context_proto = proto_root - .join("opentelemetry/proto/processcontext/v1development/process_context.proto"); - - prost_build::Config::new() - .protoc_executable(protoc_bin_vendored::protoc_bin_path().unwrap()) - .compile_protos(&[process_context_proto], &[proto_root]) - .unwrap(); -} diff --git a/opentelemetry-process-context/rust/src/generated/opentelemetry.proto.common.v1.rs b/opentelemetry-process-context/rust/src/generated/opentelemetry.proto.common.v1.rs new file mode 100644 index 00000000000..204efeb3a8b --- /dev/null +++ b/opentelemetry-process-context/rust/src/generated/opentelemetry.proto.common.v1.rs @@ -0,0 +1,160 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +// @generated by scripts/proto_codegen_process_context.sh from open-telemetry/opentelemetry-proto. DO NOT EDIT. + +// @generated +// This file is @generated by prost-build. +/// 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. +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct AnyValue { + /// The value is one of the listed fields. It is valid for all values to be unspecified + /// in which case this AnyValue is considered to be "empty". + #[prost(oneof="any_value::Value", tags="1, 2, 3, 4, 5, 6, 7, 8")] + pub value: ::core::option::Option, +} +/// Nested message and enum types in `AnyValue`. +pub mod any_value { + /// The value is one of the listed fields. It is valid for all values to be unspecified + /// in which case this AnyValue is considered to be "empty". + #[derive(Clone, PartialEq, ::prost::Oneof)] + pub enum Value { + #[prost(string, tag="1")] + StringValue(::prost::alloc::string::String), + #[prost(bool, tag="2")] + BoolValue(bool), + #[prost(int64, tag="3")] + IntValue(i64), + #[prost(double, tag="4")] + DoubleValue(f64), + #[prost(message, tag="5")] + ArrayValue(super::ArrayValue), + #[prost(message, tag="6")] + KvlistValue(super::KeyValueList), + #[prost(bytes, tag="7")] + BytesValue(::prost::alloc::vec::Vec), + /// 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: \[Alpha\] + #[prost(int32, tag="8")] + StringValueStrindex(i32), + } +} +/// ArrayValue is a list of AnyValue messages. We need ArrayValue as a message +/// since oneof in AnyValue does not allow repeated fields. +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ArrayValue { + /// Array of values. The array may be empty (contain 0 elements). + #[prost(message, repeated, tag="1")] + pub values: ::prost::alloc::vec::Vec, +} +/// 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. +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct KeyValueList { + /// 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. + #[prost(message, repeated, tag="1")] + pub values: ::prost::alloc::vec::Vec, +} +/// Represents a key-value pair that is used to store Span attributes, Link +/// attributes, etc. +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct KeyValue { + /// The key name of the pair. + /// key_strindex MUST NOT be set if key is used. + #[prost(string, tag="1")] + pub key: ::prost::alloc::string::String, + /// The value of the pair. + #[prost(message, optional, tag="2")] + pub value: ::core::option::Option, + /// 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: \[Alpha\] + #[prost(int32, tag="3")] + pub key_strindex: i32, +} +/// InstrumentationScope is a message representing the instrumentation scope information +/// such as the fully qualified name and version. +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct InstrumentationScope { + /// A name denoting the Instrumentation scope. + /// An empty instrumentation scope name means the name is unknown. + #[prost(string, tag="1")] + pub name: ::prost::alloc::string::String, + /// Defines the version of the instrumentation scope. + /// An empty instrumentation scope version means the version is unknown. + #[prost(string, tag="2")] + pub version: ::prost::alloc::string::String, + /// 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. + #[prost(message, repeated, tag="3")] + pub attributes: ::prost::alloc::vec::Vec, + /// 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. + #[prost(uint32, tag="4")] + pub dropped_attributes_count: u32, +} +/// A reference to an Entity. +/// Entity represents an object of interest associated with produced telemetry: e.g spans, metrics, profiles, or logs. +/// +/// Status: \[Development\] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct EntityRef { + /// 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 + /// + /// + /// 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. + #[prost(string, tag="1")] + pub schema_url: ::prost::alloc::string::String, + /// 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. + #[prost(string, tag="2")] + pub r#type: ::prost::alloc::string::String, + /// 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. + #[prost(string, repeated, tag="3")] + pub id_keys: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, + /// 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. + #[prost(string, repeated, tag="4")] + pub description_keys: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, +} +// @@protoc_insertion_point(module) \ No newline at end of file diff --git a/opentelemetry-process-context/rust/src/generated/opentelemetry.proto.processcontext.v1development.rs b/opentelemetry-process-context/rust/src/generated/opentelemetry.proto.processcontext.v1development.rs new file mode 100644 index 00000000000..34185ee6625 --- /dev/null +++ b/opentelemetry-process-context/rust/src/generated/opentelemetry.proto.processcontext.v1development.rs @@ -0,0 +1,38 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +// @generated by scripts/proto_codegen_process_context.sh from open-telemetry/opentelemetry-proto. DO NOT EDIT. + +// @generated +// This file is @generated by prost-build. +/// ProcessContext represents the payload for the process context sharing mechanism. +/// +/// This message is designed to be published by OpenTelemetry SDKs via a memory-mapped +/// region, allowing external readers (such as the OpenTelemetry eBPF Profiler) to +/// discover and read resource attributes from instrumented processes without requiring +/// direct integration or process activity. It is not part of OTLP and is not +/// exchanged via the OpenTelemetry Collector. +/// +/// See OTEP 4719 () +/// for details of this mechanism. +/// +/// Status: \[Development\] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ProcessContext { + /// The resource for this process. + /// If this field is not set then no resource info is known. + #[prost(message, optional, tag="1")] + pub resource: ::core::option::Option, + /// Additional attributes to share with external readers that are not part of + /// the standard Resource. \[Optional\] + /// + /// This field allows publishers to include supplementary key-value pairs that + /// may be useful for external readers but are not part of the SDK's configured + /// Resource. + /// + /// Consider adding any keys here to the profiles semantic conventions in + /// + #[prost(message, repeated, tag="2")] + pub attributes: ::prost::alloc::vec::Vec, +} +// @@protoc_insertion_point(module) \ No newline at end of file diff --git a/opentelemetry-process-context/rust/src/generated/opentelemetry.proto.resource.v1.rs b/opentelemetry-process-context/rust/src/generated/opentelemetry.proto.resource.v1.rs new file mode 100644 index 00000000000..2f3078160e7 --- /dev/null +++ b/opentelemetry-process-context/rust/src/generated/opentelemetry.proto.resource.v1.rs @@ -0,0 +1,29 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +// @generated by scripts/proto_codegen_process_context.sh from open-telemetry/opentelemetry-proto. DO NOT EDIT. + +// @generated +// This file is @generated by prost-build. +/// Resource information. +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct Resource { + /// 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. + #[prost(message, repeated, tag="1")] + pub attributes: ::prost::alloc::vec::Vec, + /// The number of dropped attributes. If the value is 0, then + /// no attributes were dropped. + #[prost(uint32, tag="2")] + pub dropped_attributes_count: u32, + /// Set of entities that participate in this Resource. + /// + /// Note: keys in the references MUST exist in attributes of this message. + /// + /// Status: \[Development\] + #[prost(message, repeated, tag="3")] + pub entity_refs: ::prost::alloc::vec::Vec, +} +// @@protoc_insertion_point(module) \ No newline at end of file diff --git a/opentelemetry-process-context/rust/src/proto.rs b/opentelemetry-process-context/rust/src/proto.rs index c9e3c0587c4..cf736be28e1 100644 --- a/opentelemetry-process-context/rust/src/proto.rs +++ b/opentelemetry-process-context/rust/src/proto.rs @@ -1,20 +1,28 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 +// The included files are committed, machine-generated prost output produced by the +// maintainer-only `scripts/proto_codegen_process_context.sh` script. They are checked in so that +// building the crate — including building a wheel from the sdist — needs no network +// access and no `protoc`. Relative `include!` paths resolve against this file's +// directory (`src/`). The generated code carries unused message types, so lints are +// silenced on these modules. +#![allow(dead_code)] + pub(crate) mod common { pub(crate) mod v1 { - include!(concat!(env!("OUT_DIR"), "/opentelemetry.proto.common.v1.rs")); + include!("generated/opentelemetry.proto.common.v1.rs"); } } pub(crate) mod resource { pub(crate) mod v1 { - include!(concat!(env!("OUT_DIR"), "/opentelemetry.proto.resource.v1.rs")); + include!("generated/opentelemetry.proto.resource.v1.rs"); } } pub(crate) mod processcontext { pub(crate) mod v1development { - include!(concat!(env!("OUT_DIR"), "/opentelemetry.proto.processcontext.v1development.rs")); + include!("generated/opentelemetry.proto.processcontext.v1development.rs"); } } diff --git a/scripts/proto_codegen_process_context.sh b/scripts/proto_codegen_process_context.sh new file mode 100755 index 00000000000..179a0b57bee --- /dev/null +++ b/scripts/proto_codegen_process_context.sh @@ -0,0 +1,78 @@ +#!/bin/bash +# +# Regenerate the Rust (prost) code for opentelemetry-process-context from the +# process context proto in https://github.com/open-telemetry/opentelemetry-proto +# +# To use, update PROTO_REPO_BRANCH_OR_COMMIT below to the commit hash/tag you want to +# build off of, then run this script and commit the regenerated files. +# +# Requirements: cargo (to install the protoc-gen-prost plugin) and uv/uvx (provides +# protoc via grpcio-tools, similar to the other proto scripts). + +# Pinned commit/branch/tag for the process context proto. +PROTO_REPO_BRANCH_OR_COMMIT="023f8cd36cc946617caa9a9e2e9868186f6d22dd" + +# protoc-gen-prost release that emits code compatible with the crate's prost 0.13. +PROTOC_GEN_PROST_VERSION="0.4.0" + +set -e + +PROTO_REPO_DIR=${PROTO_REPO_DIR:-"/tmp/opentelemetry-proto"} +# root of opentelemetry-python repo +repo_root="$(git rev-parse --show-toplevel)" +out_dir="$repo_root/opentelemetry-process-context/rust/src/generated" + +# The protos to generate. The process context message imports common and resource. +PROTOS=( + "opentelemetry/proto/common/v1/common.proto" + "opentelemetry/proto/resource/v1/resource.proto" + "opentelemetry/proto/processcontext/v1development/process_context.proto" +) + +# Install the prost code generator plugin for protoc. +cargo install "protoc-gen-prost@${PROTOC_GEN_PROST_VERSION}" + +# protoc, provided by grpcio-tools via uvx (same approach as scripts/proto_codegen.sh), +# wired up to the cargo-installed prost plugin. +protoc() { + uvx --from grpcio-tools \ + --python 3.12 \ + python -m grpc_tools.protoc \ + --plugin=protoc-gen-prost="$(command -v protoc-gen-prost)" \ + "$@" +} + +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 +) + +# Regenerate from scratch. +rm -f "$out_dir"/*.rs +mkdir -p "$out_dir" +protoc \ + -I "$PROTO_REPO_DIR" \ + --prost_out="$out_dir" \ + "${PROTOS[@]}" + +# Prepend the standard copyright header +header="// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +// @generated by scripts/proto_codegen_process_context.sh from open-telemetry/opentelemetry-proto. DO NOT EDIT." +for f in "$out_dir"/*.rs; do + printf '%s\n\n%s' "$header" "$(cat "$f")" > "$f" +done + +echo "Regenerated prost output in $out_dir" From 052906d7c77956e004df431a099d1aef2f91152a Mon Sep 17 00:00:00 2001 From: Lukas Hering <40302054+herin049@users.noreply.github.com> Date: Wed, 24 Jun 2026 13:24:08 -0500 Subject: [PATCH 22/35] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- scripts/proto_codegen_process_context.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/proto_codegen_process_context.sh b/scripts/proto_codegen_process_context.sh index 179a0b57bee..576e2efb5e2 100755 --- a/scripts/proto_codegen_process_context.sh +++ b/scripts/proto_codegen_process_context.sh @@ -59,8 +59,8 @@ fi ) # Regenerate from scratch. -rm -f "$out_dir"/*.rs mkdir -p "$out_dir" +rm -f "$out_dir"/*.rs protoc \ -I "$PROTO_REPO_DIR" \ --prost_out="$out_dir" \ From e7902d9f0378b25315c3448d515a33794b0fe7db Mon Sep 17 00:00:00 2001 From: Lukas Hering <40302054+herin049@users.noreply.github.com> Date: Wed, 24 Jun 2026 13:26:30 -0500 Subject: [PATCH 23/35] Update import statement for publish_context --- opentelemetry-process-context/tests/test_process_context.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opentelemetry-process-context/tests/test_process_context.py b/opentelemetry-process-context/tests/test_process_context.py index e1e2ee75afb..80332615c43 100644 --- a/opentelemetry-process-context/tests/test_process_context.py +++ b/opentelemetry-process-context/tests/test_process_context.py @@ -43,7 +43,7 @@ def test_cross_process_memory_region(self): child_script = textwrap.dedent("""\ import sys from opentelemetry.sdk.resources import Resource - from opentelemetry.process_context._rs import publish_context + from opentelemetry.process_context import publish_context resource = Resource({"service.name": "otel-test-service", "version": 42}) publish_context(resource) From ea143c9f4a884dcdaa77fb9eb69ddafef0e2536f Mon Sep 17 00:00:00 2001 From: Lukas Hering Date: Wed, 24 Jun 2026 19:53:35 -0500 Subject: [PATCH 24/35] address Copilot comments --- opentelemetry-process-context/rust/src/context.rs | 6 +++--- opentelemetry-process-context/rust/src/proto.rs | 6 ------ opentelemetry-process-context/tests/test_process_context.py | 3 +++ 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/opentelemetry-process-context/rust/src/context.rs b/opentelemetry-process-context/rust/src/context.rs index 15c19397f74..b4a9a81507d 100644 --- a/opentelemetry-process-context/rust/src/context.rs +++ b/opentelemetry-process-context/rust/src/context.rs @@ -171,10 +171,10 @@ fn publish_existing(region: &mut Region, payload: Vec) -> Result<(), Publish /// mapping is allocated and the header pointer remains stable across updates. pub fn publish(payload: Vec) -> Result<(), PublishError> { let mut guard = MAPPING.lock().expect("process context mutex poisoned"); - if guard.is_none() { - publish_new(&mut guard, payload) + if let Some(region) = guard.as_mut() { + publish_existing(region, payload) } else { - publish_existing(guard.as_mut().unwrap(), payload) + publish_new(&mut guard, payload) } } diff --git a/opentelemetry-process-context/rust/src/proto.rs b/opentelemetry-process-context/rust/src/proto.rs index cf736be28e1..5d9168c95bb 100644 --- a/opentelemetry-process-context/rust/src/proto.rs +++ b/opentelemetry-process-context/rust/src/proto.rs @@ -1,12 +1,6 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -// The included files are committed, machine-generated prost output produced by the -// maintainer-only `scripts/proto_codegen_process_context.sh` script. They are checked in so that -// building the crate — including building a wheel from the sdist — needs no network -// access and no `protoc`. Relative `include!` paths resolve against this file's -// directory (`src/`). The generated code carries unused message types, so lints are -// silenced on these modules. #![allow(dead_code)] pub(crate) mod common { diff --git a/opentelemetry-process-context/tests/test_process_context.py b/opentelemetry-process-context/tests/test_process_context.py index 80332615c43..2ff12365a54 100644 --- a/opentelemetry-process-context/tests/test_process_context.py +++ b/opentelemetry-process-context/tests/test_process_context.py @@ -38,6 +38,9 @@ def test_unpublish_before_publish_raises(self): with self.assertRaises(RuntimeError): unpublish_context() + @unittest.skipUnless( + sys.platform.startswith("linux"), "requires /proc//{maps,mem}" + ) def test_cross_process_memory_region(self): """Spawn a child that publishes a fixed context and read/validate its memory region.""" child_script = textwrap.dedent("""\ From 1103446cd9fd6c737bdc09bb2d0d5da6d09dc2b5 Mon Sep 17 00:00:00 2001 From: Lukas Hering Date: Wed, 24 Jun 2026 21:55:39 -0500 Subject: [PATCH 25/35] refactor tests and make implementation fork safe --- opentelemetry-process-context/pyproject.toml | 2 +- opentelemetry-process-context/rust/Cargo.lock | 60 ++++ opentelemetry-process-context/rust/Cargo.toml | 1 + .../rust/src/context/linux.rs | 54 ++++ .../rust/src/{context.rs => context/mod.rs} | 141 +++++----- .../rust/src/convert.rs | 30 +- opentelemetry-process-context/rust/src/lib.rs | 8 +- .../rust/src/proto.rs | 2 +- .../opentelemetry/process_context/version.py | 2 +- .../tests/__init__.py | 2 + .../tests/scripts/fork_keepalive.py | 27 ++ .../tests/scripts/fork_republish.py | 34 +++ .../tests/scripts/publish_and_wait.py | 14 + .../tests/scripts/publisher.py | 26 ++ .../tests/test_process_context.py | 256 ++++++++++++++---- uv.lock | 2 +- 16 files changed, 519 insertions(+), 142 deletions(-) create mode 100644 opentelemetry-process-context/rust/src/context/linux.rs rename opentelemetry-process-context/rust/src/{context.rs => context/mod.rs} (76%) create mode 100644 opentelemetry-process-context/tests/scripts/fork_keepalive.py create mode 100644 opentelemetry-process-context/tests/scripts/fork_republish.py create mode 100644 opentelemetry-process-context/tests/scripts/publish_and_wait.py create mode 100644 opentelemetry-process-context/tests/scripts/publisher.py diff --git a/opentelemetry-process-context/pyproject.toml b/opentelemetry-process-context/pyproject.toml index 14ff909b075..642dde94089 100644 --- a/opentelemetry-process-context/pyproject.toml +++ b/opentelemetry-process-context/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "maturin" [project] name = "opentelemetry-process-context" -version = "0.64b0.dev" +version = "0.65b0.dev" description = "OpenTelemetry Process Context" readme = "README.rst" license = "Apache-2.0" diff --git a/opentelemetry-process-context/rust/Cargo.lock b/opentelemetry-process-context/rust/Cargo.lock index 35a899b161c..4ae3b8fe7c8 100644 --- a/opentelemetry-process-context/rust/Cargo.lock +++ b/opentelemetry-process-context/rust/Cargo.lock @@ -59,6 +59,15 @@ version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + [[package]] name = "nix" version = "0.31.3" @@ -82,10 +91,34 @@ name = "opentelemetry-process-context" version = "0.1.0" dependencies = [ "nix", + "parking_lot", "prost", "pyo3", ] +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + [[package]] name = "portable-atomic" version = "1.13.1" @@ -191,6 +224,27 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + [[package]] name = "syn" version = "2.0.118" @@ -213,3 +267,9 @@ name = "unicode-ident" version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" diff --git a/opentelemetry-process-context/rust/Cargo.toml b/opentelemetry-process-context/rust/Cargo.toml index a7ae90bfc7a..7cd9da37986 100644 --- a/opentelemetry-process-context/rust/Cargo.toml +++ b/opentelemetry-process-context/rust/Cargo.toml @@ -10,6 +10,7 @@ crate-type = ["cdylib"] [dependencies] prost = "0.13" nix = { version = "0.31", features = ["mman", "fs", "time"] } +parking_lot = "0.12" [dependencies.pyo3] version = "0.28.2" diff --git a/opentelemetry-process-context/rust/src/context/linux.rs b/opentelemetry-process-context/rust/src/context/linux.rs new file mode 100644 index 00000000000..e9b7b30d861 --- /dev/null +++ b/opentelemetry-process-context/rust/src/context/linux.rs @@ -0,0 +1,54 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +use std::ffi::c_void; +use std::num::NonZeroUsize; +use std::ptr::NonNull; + +use nix::sys::memfd::{MFdFlags, memfd_create}; +use nix::sys::mman::{MapFlags, ProtFlags, mmap}; + +// `prctl` options for naming an anonymous VMA. +const PR_SET_VMA: nix::libc::c_int = 0x53564d41; +const PR_SET_VMA_ANON_NAME: nix::libc::c_ulong = 0; + +/// Create a `memfd`, size it, and map it `MAP_PRIVATE`. Returns `None` if any +/// step fails so the caller can fall back to an anonymous mapping. +pub(super) fn try_memfd_mapping(len: NonZeroUsize) -> Option> { + let base = MFdFlags::MFD_CLOEXEC | MFdFlags::MFD_ALLOW_SEALING; + // `MFD_NOEXEC_SEAL` is a newer flag not exposed by `nix`, request it but + // fall back without it on kernels/libc that lack it. + let noexec_seal = MFdFlags::from_bits_retain(nix::libc::MFD_NOEXEC_SEAL as _); + let fd = memfd_create(c"OTEL_CTX", base | noexec_seal) + .or_else(|_| memfd_create(c"OTEL_CTX", base)) + .ok()?; + + nix::unistd::ftruncate(&fd, len.get() as nix::libc::off_t).ok()?; + + // SAFETY: `fd` is a valid, sized memfd and `len` is non-zero. + let ptr = unsafe { + mmap( + None, + len, + ProtFlags::PROT_READ | ProtFlags::PROT_WRITE, + MapFlags::MAP_PRIVATE, + &fd, + 0, + ) + } + .ok()?; + Some(ptr) +} + +pub(super) fn name_mapping(ptr: NonNull, len: usize) { + const NAME: &core::ffi::CStr = c"OTEL_CTX"; + unsafe { + let _ = nix::libc::prctl( + PR_SET_VMA, + PR_SET_VMA_ANON_NAME, + ptr.as_ptr() as nix::libc::c_ulong, + len as nix::libc::c_ulong, + NAME.as_ptr() as nix::libc::c_ulong, + ); + } +} diff --git a/opentelemetry-process-context/rust/src/context.rs b/opentelemetry-process-context/rust/src/context/mod.rs similarity index 76% rename from opentelemetry-process-context/rust/src/context.rs rename to opentelemetry-process-context/rust/src/context/mod.rs index b4a9a81507d..8e9d93fe281 100644 --- a/opentelemetry-process-context/rust/src/context.rs +++ b/opentelemetry-process-context/rust/src/context/mod.rs @@ -1,32 +1,32 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -//! Publication of the process context to a memory region that out-of-process +//! Publication of the process context to a memory region that out of process //! readers (e.g. the OpenTelemetry eBPF Profiler) can discover and read. //! //! The layout follows the OpenTelemetry "Process Context" specification: a fixed //! 32-byte header mapping named `OTEL_CTX` and backed by a `memfd` on Linux -//! (visible in `/proc//maps`), with the payload living out-of-band in a -//! heap-allocated buffer. This means the header mapping never needs resizing: -//! updates only swap the payload buffer and rewrite the header pointer fields. +//! (visible in `/proc//maps`), with the payload living out of band in a +//! heap allocated buffer. use std::ffi::c_void; use std::num::NonZeroUsize; use std::ptr::NonNull; use std::sync::atomic::{AtomicU64, Ordering}; -use std::sync::Mutex; -use nix::sys::mman::{mmap_anonymous, MapFlags, ProtFlags}; -use nix::time::{clock_gettime, ClockId}; -use pyo3::exceptions::{PyOSError, PyRuntimeError}; +use parking_lot::Mutex; + +use nix::sys::mman::{MapFlags, ProtFlags, mmap_anonymous}; +use nix::time::{ClockId, clock_gettime}; use pyo3::PyErr; +use pyo3::exceptions::{PyOSError, PyRuntimeError}; /// 8 byte signature stamped at the start of the header. const SIGNATURE: [u8; 8] = *b"OTEL_CTX"; /// Format version. `2` is the first stable version (`1` is for development). const VERSION: u32 = 2; /// Size of the header mapping in bytes. The payload lives on the heap. -const HEADER_SIZE: usize = 32; +const HEADER_SIZE: usize = std::mem::size_of::
(); /// The process context header. #[repr(C)] @@ -38,12 +38,10 @@ struct Header { payload: u64, } -const _: () = assert!(std::mem::size_of::
() == HEADER_SIZE); - /// A published mapping. -#[allow(dead_code)] struct Region { ptr: NonNull, + #[allow(unused)] payload: Vec, } @@ -54,6 +52,64 @@ unsafe impl Send for Region {} /// The single active process context for this process, if any. static MAPPING: Mutex> = Mutex::new(None); +/// Register the `pthread_atfork` handlers that keep [`MAPPING`] fork-safe. +/// +/// Idempotent: only the first call registers the handlers. Without these, a +/// forked child inherits the parent's `MAPPING` state while the underlying +/// header mapping is gone (stripped by `MADV_DONTFORK` on Linux, or merely +/// stale elsewhere), so a `publish()` in the child would dereference a pointer +/// into an unmapped region and crash. +pub fn register_fork_handlers() { + static REGISTERED: std::sync::Once = std::sync::Once::new(); + REGISTERED.call_once(|| { + // SAFETY: the handlers are valid `'static` `extern "C"` functions. + unsafe { + nix::libc::pthread_atfork( + Some(before_fork), + Some(after_fork_parent), + Some(after_fork_child), + ); + } + }); +} + +/// `pthread_atfork` prepare handler: acquire the mapping mutex so no other +/// thread holds it across the fork, then forget the guard so the lock stays +/// held into the parent/child handlers, which release it via `force_unlock`. +extern "C" fn before_fork() { + std::mem::forget(MAPPING.lock()); +} + +/// `pthread_atfork` parent handler: release the lock taken in [`before_fork`]. +extern "C" fn after_fork_parent() { + // SAFETY: `before_fork` acquired the lock and forgot its guard, so this + // thread logically owns the lock — the precondition for `force_unlock`. + unsafe { MAPPING.force_unlock() }; +} + +/// `pthread_atfork` child handler: discard the inherited mapping (the child +/// never received the live header mapping) so the child starts from the +/// pristine "never published" state, then release the lock. +extern "C" fn after_fork_child() { + // SAFETY: the child is single threaded here and the lock is held from + // `before_fork`, so this is exclusive access to the protected data. + let mapping = unsafe { &mut *MAPPING.data_ptr() }; + if let Some(region) = mapping.take() { + // On non-Linux the mapping was inherited (no `MADV_DONTFORK`), so unmap + // it. On Linux the child never received the mapping, so the pointer is + // simply abandoned. + #[cfg(not(target_os = "linux"))] + // SAFETY: `region.ptr`/`HEADER_SIZE` describe the inherited mapping. + unsafe { + let _ = nix::sys::mman::munmap(region.ptr, HEADER_SIZE); + } + // `region.payload` (the inherited `Vec` copy) is dropped here. + drop(region); + } + // SAFETY: release the lock acquired (and forgotten) in `before_fork`. + unsafe { MAPPING.force_unlock() }; +} + #[derive(Debug)] pub enum PublishError { /// The backing memory region could not be allocated. @@ -170,7 +226,7 @@ fn publish_existing(region: &mut Region, payload: Vec) -> Result<(), Publish /// (zero timestamp → rewrite payload fields → restore timestamp), so no new /// mapping is allocated and the header pointer remains stable across updates. pub fn publish(payload: Vec) -> Result<(), PublishError> { - let mut guard = MAPPING.lock().expect("process context mutex poisoned"); + let mut guard = MAPPING.lock(); if let Some(region) = guard.as_mut() { publish_existing(region, payload) } else { @@ -184,7 +240,7 @@ pub fn publish(payload: Vec) -> Result<(), PublishError> { /// invalid state, then `munmap`s the header and drops the payload buffer. /// After a successful call, `publish()` may be called again. pub fn unpublish() -> Result<(), PublishError> { - let mut guard = MAPPING.lock().expect("process context mutex poisoned"); + let mut guard = MAPPING.lock(); let region = guard.take().ok_or(PublishError::NotPublished)?; // Zero the timestamp and remove the mapping. @@ -198,8 +254,7 @@ pub fn unpublish() -> Result<(), PublishError> { published_at.store(0, Ordering::Relaxed); } - unsafe { nix::sys::mman::munmap(region.ptr, HEADER_SIZE) } - .map_err(|_| PublishError::Munmap) + unsafe { nix::sys::mman::munmap(region.ptr, HEADER_SIZE) }.map_err(|_| PublishError::Munmap) } /// Allocate the 32-byte header mapping: a `memfd`-backed mapping on Linux (so @@ -263,56 +318,4 @@ fn name_mapping(ptr: NonNull, len: usize) { fn name_mapping(_ptr: NonNull, _len: usize) {} #[cfg(target_os = "linux")] -mod linux { - use std::ffi::c_void; - use std::num::NonZeroUsize; - use std::ptr::NonNull; - - use nix::sys::memfd::{memfd_create, MFdFlags}; - use nix::sys::mman::{mmap, MapFlags, ProtFlags}; - - // `prctl` options for naming an anonymous VMA. - const PR_SET_VMA: nix::libc::c_int = 0x53564d41; - const PR_SET_VMA_ANON_NAME: nix::libc::c_ulong = 0; - - /// Create a `memfd`, size it, and map it `MAP_PRIVATE`. Returns `None` if any - /// step fails so the caller can fall back to an anonymous mapping. - pub(super) fn try_memfd_mapping(len: NonZeroUsize) -> Option> { - let base = MFdFlags::MFD_CLOEXEC | MFdFlags::MFD_ALLOW_SEALING; - // `MFD_NOEXEC_SEAL` is a newer flag not exposed by `nix`, request it but - // fall back without it on kernels/libc that lack it. - let noexec_seal = MFdFlags::from_bits_retain(nix::libc::MFD_NOEXEC_SEAL as _); - let fd = memfd_create(c"OTEL_CTX", base | noexec_seal) - .or_else(|_| memfd_create(c"OTEL_CTX", base)) - .ok()?; - - nix::unistd::ftruncate(&fd, len.get() as nix::libc::off_t).ok()?; - - // SAFETY: `fd` is a valid, sized memfd and `len` is non-zero. - let ptr = unsafe { - mmap( - None, - len, - ProtFlags::PROT_READ | ProtFlags::PROT_WRITE, - MapFlags::MAP_PRIVATE, - &fd, - 0, - ) - } - .ok()?; - Some(ptr) - } - - pub(super) fn name_mapping(ptr: NonNull, len: usize) { - const NAME: &core::ffi::CStr = c"OTEL_CTX"; - unsafe { - let _ = nix::libc::prctl( - PR_SET_VMA, - PR_SET_VMA_ANON_NAME, - ptr.as_ptr() as nix::libc::c_ulong, - len as nix::libc::c_ulong, - NAME.as_ptr() as nix::libc::c_ulong, - ); - } - } -} +mod linux; diff --git a/opentelemetry-process-context/rust/src/convert.rs b/opentelemetry-process-context/rust/src/convert.rs index 0279d47a899..a24c8e7481b 100644 --- a/opentelemetry-process-context/rust/src/convert.rs +++ b/opentelemetry-process-context/rust/src/convert.rs @@ -1,29 +1,37 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 +use crate::proto::common::v1::{AnyValue, ArrayValue, KeyValue, KeyValueList, any_value}; +use crate::proto::processcontext::v1development::ProcessContext; +use crate::proto::resource::v1::Resource; use prost::Message; use pyo3::exceptions::PyTypeError; use pyo3::prelude::*; use pyo3::types::{PyBool, PyDict, PyFloat, PyInt, PyString}; -use crate::proto::common::v1::{any_value, AnyValue, ArrayValue, KeyValue, KeyValueList}; -use crate::proto::processcontext::v1development::ProcessContext; -use crate::proto::resource::v1::Resource; fn any_value_from_py(val: &Bound<'_, PyAny>) -> PyResult { if val.is_none() { return Ok(AnyValue::default()); } if val.is_instance_of::() { - return Ok(AnyValue { value: Some(any_value::Value::BoolValue(val.extract()?)) }); + return Ok(AnyValue { + value: Some(any_value::Value::BoolValue(val.extract()?)), + }); } if val.is_instance_of::() { - return Ok(AnyValue { value: Some(any_value::Value::IntValue(val.extract()?)) }); + return Ok(AnyValue { + value: Some(any_value::Value::IntValue(val.extract()?)), + }); } if val.is_instance_of::() { - return Ok(AnyValue { value: Some(any_value::Value::DoubleValue(val.extract()?)) }); + return Ok(AnyValue { + value: Some(any_value::Value::DoubleValue(val.extract()?)), + }); } if val.is_instance_of::() { - return Ok(AnyValue { value: Some(any_value::Value::StringValue(val.extract()?)) }); + return Ok(AnyValue { + value: Some(any_value::Value::StringValue(val.extract()?)), + }); } let py = val.py(); @@ -55,7 +63,9 @@ fn any_value_from_py(val: &Bound<'_, PyAny>) -> PyResult { .try_iter()? .map(|item| any_value_from_py(&item?)) .collect::>()?; - return Ok(AnyValue { value: Some(any_value::Value::ArrayValue(ArrayValue { values })) }); + return Ok(AnyValue { + value: Some(any_value::Value::ArrayValue(ArrayValue { values })), + }); } let type_name: String = val.get_type().qualname()?.extract()?; @@ -64,9 +74,7 @@ fn any_value_from_py(val: &Bound<'_, PyAny>) -> PyResult { ))) } -pub fn resource_from_py( - resource: &Bound<'_, PyAny>, -) -> PyResult { +pub fn resource_from_py(resource: &Bound<'_, PyAny>) -> PyResult { let py = resource.py(); let attrs_obj = resource.getattr("attributes")?; diff --git a/opentelemetry-process-context/rust/src/lib.rs b/opentelemetry-process-context/rust/src/lib.rs index efae5a3ca3c..7d8ae6044a2 100644 --- a/opentelemetry-process-context/rust/src/lib.rs +++ b/opentelemetry-process-context/rust/src/lib.rs @@ -1,12 +1,12 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 +mod context; mod convert; pub(crate) mod proto; -mod context; -use pyo3::prelude::*; use crate::convert::{encode_process_context, resource_from_py}; +use pyo3::prelude::*; #[pyfunction] fn publish_context(resource: &Bound<'_, PyAny>) -> PyResult<()> { @@ -24,7 +24,7 @@ fn unpublish_context() -> PyResult<()> { #[pymodule] #[pyo3(name = "_rs")] fn init(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + context::register_fork_handlers(); m.add_wrapped(wrap_pyfunction!(publish_context))?; - m.add_wrapped(wrap_pyfunction!(unpublish_context))?; - Ok(()) + m.add_wrapped(wrap_pyfunction!(unpublish_context)) } diff --git a/opentelemetry-process-context/rust/src/proto.rs b/opentelemetry-process-context/rust/src/proto.rs index 5d9168c95bb..e3b4c97fe5b 100644 --- a/opentelemetry-process-context/rust/src/proto.rs +++ b/opentelemetry-process-context/rust/src/proto.rs @@ -1,7 +1,7 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -#![allow(dead_code)] +#![allow(dead_code, clippy::all)] pub(crate) mod common { pub(crate) mod v1 { diff --git a/opentelemetry-process-context/src/opentelemetry/process_context/version.py b/opentelemetry-process-context/src/opentelemetry/process_context/version.py index 13e069be44f..357a2a12e6b 100644 --- a/opentelemetry-process-context/src/opentelemetry/process_context/version.py +++ b/opentelemetry-process-context/src/opentelemetry/process_context/version.py @@ -1,4 +1,4 @@ # Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 -__version__ = "0.64b0.dev" +__version__ = "0.65b0.dev" diff --git a/opentelemetry-process-context/tests/__init__.py b/opentelemetry-process-context/tests/__init__.py index e69de29bb2d..e57cf4aba95 100644 --- a/opentelemetry-process-context/tests/__init__.py +++ b/opentelemetry-process-context/tests/__init__.py @@ -0,0 +1,2 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 diff --git a/opentelemetry-process-context/tests/scripts/fork_keepalive.py b/opentelemetry-process-context/tests/scripts/fork_keepalive.py new file mode 100644 index 00000000000..8479b941ff6 --- /dev/null +++ b/opentelemetry-process-context/tests/scripts/fork_keepalive.py @@ -0,0 +1,27 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +# pylint: disable=protected-access + +import os +import signal +import sys + +from opentelemetry.process_context import publish_context +from opentelemetry.sdk.resources import Resource + +resource = Resource({"service.name": "otel-test-service", "version": 42}) +publish_context(resource) + +pid = os.fork() +if not pid: + # Child: do not publish, block until the parent kills us. + signal.pause() + os._exit(0) + +sys.stdout.write(f"{os.getpid()} {pid}\n") +sys.stdout.flush() +sys.stdin.readline() +os.kill(pid, signal.SIGKILL) +os.waitpid(pid, 0) +os._exit(0) diff --git a/opentelemetry-process-context/tests/scripts/fork_republish.py b/opentelemetry-process-context/tests/scripts/fork_republish.py new file mode 100644 index 00000000000..00f0a3eb2a7 --- /dev/null +++ b/opentelemetry-process-context/tests/scripts/fork_republish.py @@ -0,0 +1,34 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +# pylint: disable=protected-access,bare-except + +import os +import sys + +from opentelemetry.process_context import publish_context, unpublish_context +from opentelemetry.sdk.resources import Resource + +resource = Resource({"service.name": "test", "version": 1}) +publish_context(resource) + +pid = os.fork() +if not pid: + try: + publish_context(resource) + unpublish_context() + except: + os._exit(1) + os._exit(0) + +_, status = os.waitpid(pid, 0) +if not (os.WIFEXITED(status) and os.WEXITSTATUS(status) == 0): + sys.exit(1) + +# The parent's own mapping is untouched and still usable. +try: + publish_context(resource) + unpublish_context() +except: + sys.exit(1) +sys.exit(0) diff --git a/opentelemetry-process-context/tests/scripts/publish_and_wait.py b/opentelemetry-process-context/tests/scripts/publish_and_wait.py new file mode 100644 index 00000000000..b158e10aa01 --- /dev/null +++ b/opentelemetry-process-context/tests/scripts/publish_and_wait.py @@ -0,0 +1,14 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +import sys + +from opentelemetry.process_context import publish_context +from opentelemetry.sdk.resources import Resource + +resource = Resource({"service.name": "otel-test-service", "version": 42}) +publish_context(resource) + +sys.stdout.write("ready\n") +sys.stdout.flush() +sys.stdin.readline() diff --git a/opentelemetry-process-context/tests/scripts/publisher.py b/opentelemetry-process-context/tests/scripts/publisher.py new file mode 100644 index 00000000000..3b7e5ae8584 --- /dev/null +++ b/opentelemetry-process-context/tests/scripts/publisher.py @@ -0,0 +1,26 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +import os +import sys + +from opentelemetry.process_context import publish_context, unpublish_context +from opentelemetry.sdk.resources import Resource + +first = Resource({"service.name": "otel-first", "version": 1}) +second = Resource({"service.name": "otel-second", "version": 2}) +publish_context(first) + +sys.stdout.write(f"{os.getpid()}\n") +sys.stdout.flush() + +for line in sys.stdin: + match line.strip(): + case "update": + publish_context(second) + case "unpublish": + unpublish_context() + case "exit": + break + sys.stdout.write("done\n") + sys.stdout.flush() diff --git a/opentelemetry-process-context/tests/test_process_context.py b/opentelemetry-process-context/tests/test_process_context.py index 2ff12365a54..9c0173ec846 100644 --- a/opentelemetry-process-context/tests/test_process_context.py +++ b/opentelemetry-process-context/tests/test_process_context.py @@ -3,11 +3,13 @@ from __future__ import annotations +import os import struct import subprocess import sys -import textwrap +import threading import unittest +from pathlib import Path from opentelemetry.process_context import ( publish_context, @@ -15,6 +17,45 @@ ) from opentelemetry.sdk.resources import Resource +HEADER_SIZE = 32 + +# Helper scripts (see tests/scripts/). +_SCRIPTS_DIR = Path(__file__).parent / "scripts" + + +def _script_cmd(name: str) -> list[str]: + """Build the command to run a helper script under the current interpreter.""" + return [sys.executable, str(_SCRIPTS_DIR / name)] + + +def _read_header(pid: int, addr: int) -> dict: + with open(f"/proc/{pid}/mem", "rb") as mem: + mem.seek(addr) + header_bytes = mem.read(HEADER_SIZE) + + payload_size = struct.unpack_from(" int | None: + with open(f"/proc/{pid}/maps", encoding="utf-8") as maps_file: + for maps_line in maps_file: + if ":OTEL_CTX" in maps_line: + return int(maps_line.split("-")[0], 16) + return None + class TestPublishContext(unittest.TestCase): def tearDown(self): @@ -38,71 +79,178 @@ def test_unpublish_before_publish_raises(self): with self.assertRaises(RuntimeError): unpublish_context() + def test_concurrent_publish(self): + """Many threads publishing concurrently must not crash or error.""" + thread_count = 8 + iterations = 200 + barrier = threading.Barrier(thread_count) + errors: list[BaseException] = [] + + def worker(index: int) -> None: + resource = Resource( + {"service.name": f"svc-{index}", "version": index} + ) + barrier.wait() + try: + for _ in range(iterations): + publish_context(resource) + # pylint: disable-next=broad-except + except BaseException as exc: + errors.append(exc) + + threads = [ + threading.Thread(target=worker, args=(i,)) + for i in range(thread_count) + ] + for thread in threads: + thread.start() + for thread in threads: + thread.join() + + self.assertEqual(errors, []) + # The mapping is still in a consistent, usable state. + self.assertIsNone(publish_context(Resource({"service.name": "final"}))) + self.assertIsNone(unpublish_context()) + + @unittest.skipUnless(hasattr(os, "fork"), "requires os.fork") + def test_publish_in_forked_child(self): + """A child that re-publishes after fork must not crash and the parent + must remain unaffected.""" + result = subprocess.run(_script_cmd("fork_republish.py"), check=False) + self.assertEqual( + result.returncode, + 0, + "fork/re-publish script did not exit cleanly", + ) + @unittest.skipUnless( sys.platform.startswith("linux"), "requires /proc//{maps,mem}" ) def test_cross_process_memory_region(self): """Spawn a child that publishes a fixed context and read/validate its memory region.""" - child_script = textwrap.dedent("""\ - import sys - from opentelemetry.sdk.resources import Resource - from opentelemetry.process_context import publish_context - - resource = Resource({"service.name": "otel-test-service", "version": 42}) - publish_context(resource) - - sys.stdout.write("ready\\n") - sys.stdout.flush() - sys.stdin.readline() - """) - - # pylint: disable-next=consider-using-with - proc = subprocess.Popen( - [sys.executable, "-c", child_script], + with subprocess.Popen( + _script_cmd("publish_and_wait.py"), stdin=subprocess.PIPE, stdout=subprocess.PIPE, - ) - try: + ) as proc: self.assertEqual(proc.stdout.readline(), b"ready\n") - pid = proc.pid + start_addr = _find_otel_ctx_addr(proc.pid) + self.assertIsNotNone( + start_addr, + f"OTEL_CTX mapping not found in /proc/{proc.pid}/maps", + ) + + header = _read_header(proc.pid, start_addr) - # Locate the OTEL_CTX mapping and verify the name. - start_addr = None - with open(f"/proc/{pid}/maps", encoding="utf-8") as maps_file: - for maps_line in maps_file: - if "OTEL_CTX" in maps_line: - start_addr = int(maps_line.split("-")[0], 16) - self.assertIn(":OTEL_CTX", maps_line) - break + self.assertEqual(header["signature"], b"OTEL_CTX") + self.assertEqual(header["version"], 2) + self.assertGreater(header["payload_size"], 0) + self.assertGreater(header["timestamp_ns"], 0) + self.assertNotEqual(header["payload_ptr"], 0) + self.assertIn(b"service.name", header["payload"]) + self.assertIn(b"otel-test-service", header["payload"]) + @unittest.skipUnless( + sys.platform.startswith("linux"), "requires /proc//{maps,mem}" + ) + def test_mapping_present_in_parent_absent_in_child(self): + """After a fork the mapping must be visible in the parent but stripped + from the child (MADV_DONTFORK).""" + with subprocess.Popen( + _script_cmd("fork_keepalive.py"), + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + ) as proc: + parent_pid, child_pid = ( + int(token) for token in proc.stdout.readline().split() + ) + + # Parent: mapping present in /proc/maps and readable via /proc/mem. + parent_addr = _find_otel_ctx_addr(parent_pid) self.assertIsNotNone( - start_addr, - f"OTEL_CTX mapping not found in /proc/{pid}/maps", + parent_addr, + "OTEL_CTX mapping missing from the parent", + ) + header = _read_header(parent_pid, parent_addr) + self.assertEqual(header["signature"], b"OTEL_CTX") + self.assertEqual(header["version"], 2) + + # Child: mapping absent from /proc/maps... + self.assertIsNone( + _find_otel_ctx_addr(child_pid), + "OTEL_CTX mapping leaked into the forked child", ) + # The parent's address is unmapped in the child, so reading + # it from /proc//mem fails. + with self.assertRaises(OSError): + with open(f"/proc/{child_pid}/mem", "rb") as mem: + mem.seek(parent_addr) + mem.read(HEADER_SIZE) - with open(f"/proc/{pid}/mem", "rb") as mem: - mem.seek(start_addr) - header_bytes = mem.read(32) - - signature = header_bytes[0:8] - version = struct.unpack_from("/{maps,mem}" + ) + def test_update_in_place_keeps_stable_mapping(self): + """An update reuses the same header mapping, advances the + timestamp and swaps in the new payload.""" + with subprocess.Popen( + _script_cmd("publisher.py"), + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + text=True, + ) as proc: + pid = int(proc.stdout.readline()) + + addr1 = _find_otel_ctx_addr(pid) + self.assertIsNotNone(addr1) + before = _read_header(pid, addr1) + self.assertIn(b"otel-first", before["payload"]) + + proc.stdin.write("update\n") proc.stdin.flush() - proc.wait() + self.assertEqual(proc.stdout.readline(), "done\n") + + addr2 = _find_otel_ctx_addr(pid) + self.assertEqual( + addr2, addr1, "header mapping moved across update" + ) + after = _read_header(pid, addr2) + + self.assertEqual(after["version"], 2) + self.assertNotEqual(after["payload_ptr"], 0) + self.assertGreater(before["timestamp_ns"], 0) + self.assertGreaterEqual( + after["timestamp_ns"], before["timestamp_ns"] + ) + self.assertIn(b"otel-second", after["payload"]) + + @unittest.skipUnless( + sys.platform.startswith("linux"), "requires /proc//{maps,mem}" + ) + def test_unpublish_removes_mapping(self): + """Unpublishing removes the mapping, a later publish recreates it.""" + with subprocess.Popen( + _script_cmd("publisher.py"), + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + text=True, + ) as proc: + pid = int(proc.stdout.readline()) + self.assertIsNotNone(_find_otel_ctx_addr(pid)) + + proc.stdin.write("unpublish\n") + proc.stdin.flush() + self.assertEqual(proc.stdout.readline(), "done\n") + self.assertIsNone( + _find_otel_ctx_addr(pid), + "OTEL_CTX mapping survived unpublish", + ) + + proc.stdin.write("update\n") + proc.stdin.flush() + self.assertEqual(proc.stdout.readline(), "done\n") + self.assertIsNotNone( + _find_otel_ctx_addr(pid), + "OTEL_CTX mapping not recreated after re-publish", + ) diff --git a/uv.lock b/uv.lock index 372f3804fab..b8f5c273d25 100644 --- a/uv.lock +++ b/uv.lock @@ -1016,7 +1016,7 @@ requires-dist = [ [[package]] name = "opentelemetry-process-context" -version = "0.64b0.dev0" +version = "0.65b0.dev0" source = { editable = "opentelemetry-process-context" } dependencies = [ { name = "opentelemetry-api" }, From c03eb5e49479a2426319eff789a1eaa4743afede Mon Sep 17 00:00:00 2001 From: Lukas Hering Date: Wed, 24 Jun 2026 21:59:03 -0500 Subject: [PATCH 26/35] fix pre-commit error --- opentelemetry-process-context/tests/scripts/fork_republish.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/opentelemetry-process-context/tests/scripts/fork_republish.py b/opentelemetry-process-context/tests/scripts/fork_republish.py index 00f0a3eb2a7..944b8f22009 100644 --- a/opentelemetry-process-context/tests/scripts/fork_republish.py +++ b/opentelemetry-process-context/tests/scripts/fork_republish.py @@ -17,7 +17,7 @@ try: publish_context(resource) unpublish_context() - except: + except: # noqa: E722 os._exit(1) os._exit(0) @@ -29,6 +29,6 @@ try: publish_context(resource) unpublish_context() -except: +except: # noqa: E722 sys.exit(1) sys.exit(0) From b21fc4cb3ec61d7f50647bd363a74bd68f0170e3 Mon Sep 17 00:00:00 2001 From: Lukas Hering Date: Wed, 24 Jun 2026 22:30:30 -0500 Subject: [PATCH 27/35] update eachdist.ini --- eachdist.ini | 1 + scripts/eachdist.py | 55 ++++++++++++++++++++++++++++++++------------- 2 files changed, 40 insertions(+), 16 deletions(-) diff --git a/eachdist.ini b/eachdist.ini index 60f1661def0..f564b1b5dd0 100644 --- a/eachdist.ini +++ b/eachdist.ini @@ -39,6 +39,7 @@ packages= opentelemetry-exporter-otlp-json-file opentelemetry-distro opentelemetry-proto-json + opentelemetry-process-context opentelemetry-semantic-conventions opentelemetry-test-utils tests diff --git a/scripts/eachdist.py b/scripts/eachdist.py index c5f5e9880c7..f7169211967 100755 --- a/scripts/eachdist.py +++ b/scripts/eachdist.py @@ -570,28 +570,49 @@ def filter_packages(targets, packages): return filtered_packages -def update_version_files(targets, version, packages): - print("updating version/__init__.py files") - +def update_dunder_version_file(version_file_path, version): search = "__version__ .*" replace = f'__version__ = "{version}"' + with open(version_file_path) as file: + text = file.read() + + if replace in text: + print(f"{version_file_path} already contains {replace}") + return + + with open(version_file_path, "w", encoding="utf-8") as file: + file.write(re.sub(search, replace, text)) + + +def update_version_files(targets, version, packages): + print("updating version/__init__.py files") + for target in filter_packages(targets, packages): - version_file_path = target.joinpath( - load(target.joinpath("pyproject.toml"))["tool"]["hatch"][ - "version" - ]["path"] + pyproject = load(target.joinpath("pyproject.toml")) + hatch_version = ( + pyproject.get("tool", {}).get("hatch", {}).get("version") ) - with open(version_file_path) as file: - text = file.read() - - if replace in text: - print(f"{version_file_path} already contains {replace}") + if hatch_version: + # Hatch packages declare a dynamic version sourced from a file + # containing a `__version__` assignment. + update_dunder_version_file( + target.joinpath(hatch_version["path"]), version + ) continue - with open(version_file_path, "w", encoding="utf-8") as file: - file.write(re.sub(search, replace, text)) + # Maturin packages keep a static `version` under + # `[project]` in pyproject.toml as the source of truth and is also + # mirrored in a version.py for introspection. Update both. + update_files( + [target], + "pyproject.toml", + r'(?m)^version = ".*"$', + f'version = "{version}"', + ) + for version_file_path in target.glob("src/**/version.py"): + update_dunder_version_file(version_file_path, version) def update_dependencies(targets, version, packages): @@ -601,7 +622,9 @@ def update_dependencies(targets, version, packages): operators_pattern = "|".join(re.escape(op) for op in operators) for pkg in packages: - search = rf"({basename(pkg)}[^,]*)({operators_pattern})(.*\.dev)" + # `[^,\n]*` keeps the match on a single line so a package's own static + # `version = "...dev"` line (for Maturin packages) is not swallowed. + search = rf"({basename(pkg)}[^,\n]*)({operators_pattern})(.*\.dev)" replace = r"\1\2 " + version update_files( targets, @@ -618,7 +641,7 @@ def update_patch_dependencies(targets, version, prev_version, packages): operators_pattern = "|".join(re.escape(op) for op in operators) for pkg in packages: - search = rf"({basename(pkg)}[^,]*?)(\s?({operators_pattern})\s?)(.*{prev_version})" + search = rf"({basename(pkg)}[^,\n]*?)(\s?({operators_pattern})\s?)(.*{prev_version})" replace = r"\g<1>\g<2>" + version print(f"{search=}\t{replace=}\t{pkg=}") update_files( From 9397c1c133e491e20731c9330a9901af69a475c3 Mon Sep 17 00:00:00 2001 From: Lukas Hering Date: Thu, 25 Jun 2026 21:56:34 -0500 Subject: [PATCH 28/35] small refactor --- opentelemetry-process-context/rust/Cargo.lock | 79 +++++++++++++ opentelemetry-process-context/rust/Cargo.toml | 9 +- .../rust/src/{context/mod.rs => context.rs} | 108 ++++++++++++++++-- .../rust/src/context/linux.rs | 54 --------- .../rust/src/convert.rs | 41 +++++++ opentelemetry-process-context/rust/src/lib.rs | 30 ++++- 6 files changed, 251 insertions(+), 70 deletions(-) rename opentelemetry-process-context/rust/src/{context/mod.rs => context.rs} (80%) delete mode 100644 opentelemetry-process-context/rust/src/context/linux.rs diff --git a/opentelemetry-process-context/rust/Cargo.lock b/opentelemetry-process-context/rust/Cargo.lock index 4ae3b8fe7c8..ebafa0944e5 100644 --- a/opentelemetry-process-context/rust/Cargo.lock +++ b/opentelemetry-process-context/rust/Cargo.lock @@ -38,6 +38,41 @@ version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + [[package]] name = "heck" version = "0.5.0" @@ -68,6 +103,12 @@ dependencies = [ "scopeguard", ] +[[package]] +name = "log" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" + [[package]] name = "nix" version = "0.31.3" @@ -94,6 +135,7 @@ dependencies = [ "parking_lot", "prost", "pyo3", + "serial_test", ] [[package]] @@ -119,6 +161,12 @@ dependencies = [ "windows-link", ] +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + [[package]] name = "portable-atomic" version = "1.13.1" @@ -239,6 +287,37 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "serial_test" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "699f4197115b8a7e7ff19c9a315a4bd6fffec26cc4626ef45ecaea389e081c6d" +dependencies = [ + "futures-executor", + "futures-util", + "log", + "once_cell", + "parking_lot", + "serial_test_derive", +] + +[[package]] +name = "serial_test_derive" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94e153fc76e1c6a068703d6d29c508a0b15c061c4b7e43da59cc097bc342673c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + [[package]] name = "smallvec" version = "1.15.2" diff --git a/opentelemetry-process-context/rust/Cargo.toml b/opentelemetry-process-context/rust/Cargo.toml index 7cd9da37986..58fc4a93ca1 100644 --- a/opentelemetry-process-context/rust/Cargo.toml +++ b/opentelemetry-process-context/rust/Cargo.toml @@ -9,9 +9,14 @@ crate-type = ["cdylib"] [dependencies] prost = "0.13" -nix = { version = "0.31", features = ["mman", "fs", "time"] } -parking_lot = "0.12" [dependencies.pyo3] version = "0.28.2" features = ["abi3-py39"] + +[target.'cfg(unix)'.dependencies] +nix = { version = "0.31", features = ["mman", "fs", "time"] } +parking_lot = "0.12" + +[dev-dependencies] +serial_test = "3" diff --git a/opentelemetry-process-context/rust/src/context/mod.rs b/opentelemetry-process-context/rust/src/context.rs similarity index 80% rename from opentelemetry-process-context/rust/src/context/mod.rs rename to opentelemetry-process-context/rust/src/context.rs index 8e9d93fe281..7964f3a298a 100644 --- a/opentelemetry-process-context/rust/src/context/mod.rs +++ b/opentelemetry-process-context/rust/src/context.rs @@ -21,6 +21,16 @@ use nix::time::{ClockId, clock_gettime}; use pyo3::PyErr; use pyo3::exceptions::{PyOSError, PyRuntimeError}; +#[cfg(target_os = "linux")] +use nix::sys::memfd::{MFdFlags, memfd_create}; +#[cfg(target_os = "linux")] +use nix::sys::mman::mmap; + +#[cfg(target_os = "linux")] +const PR_SET_VMA: nix::libc::c_int = 0x53564d41; +#[cfg(target_os = "linux")] +const PR_SET_VMA_ANON_NAME: nix::libc::c_ulong = 0; + /// 8 byte signature stamped at the start of the header. const SIGNATURE: [u8; 8] = *b"OTEL_CTX"; /// Format version. `2` is the first stable version (`1` is for development). @@ -83,7 +93,7 @@ extern "C" fn before_fork() { /// `pthread_atfork` parent handler: release the lock taken in [`before_fork`]. extern "C" fn after_fork_parent() { // SAFETY: `before_fork` acquired the lock and forgot its guard, so this - // thread logically owns the lock — the precondition for `force_unlock`. + // thread logically owns the lock. unsafe { MAPPING.force_unlock() }; } @@ -223,8 +233,6 @@ fn publish_existing(region: &mut Region, payload: Vec) -> Result<(), Publish /// /// Creates the named memory mapping on the first call. On subsequent calls the /// existing mapping is updated in-place using the spec's Updating Protocol -/// (zero timestamp → rewrite payload fields → restore timestamp), so no new -/// mapping is allocated and the header pointer remains stable across updates. pub fn publish(payload: Vec) -> Result<(), PublishError> { let mut guard = MAPPING.lock(); if let Some(region) = guard.as_mut() { @@ -262,8 +270,7 @@ pub fn unpublish() -> Result<(), PublishError> { fn alloc_region() -> Result, PublishError> { let len = NonZeroUsize::new(HEADER_SIZE).unwrap(); - #[cfg(target_os = "linux")] - if let Some(ptr) = linux::try_memfd_mapping(len) { + if let Some(ptr) = try_memfd_mapping(len) { return Ok(ptr); } @@ -308,14 +315,99 @@ fn advise_dontfork(_ptr: NonNull, _len: usize) -> Result<(), PublishErro Ok(()) } +/// Create a `memfd`, size it, and map it `MAP_PRIVATE`. Returns `None` if any +/// step fails so the caller can fall back to an anonymous mapping. +#[cfg(target_os = "linux")] +fn try_memfd_mapping(len: NonZeroUsize) -> Option> { + let base = MFdFlags::MFD_CLOEXEC | MFdFlags::MFD_ALLOW_SEALING; + // `MFD_NOEXEC_SEAL` is a newer flag not exposed by `nix`, request it but + // fall back without it on kernels/libc that lack it. + let noexec_seal = MFdFlags::from_bits_retain(nix::libc::MFD_NOEXEC_SEAL as _); + let fd = memfd_create(c"OTEL_CTX", base | noexec_seal) + .or_else(|_| memfd_create(c"OTEL_CTX", base)) + .ok()?; + + nix::unistd::ftruncate(&fd, len.get() as nix::libc::off_t).ok()?; + + // SAFETY: `fd` is a valid, sized memfd and `len` is non-zero. + let ptr = unsafe { + mmap( + None, + len, + ProtFlags::PROT_READ | ProtFlags::PROT_WRITE, + MapFlags::MAP_PRIVATE, + &fd, + 0, + ) + } + .ok()?; + Some(ptr) +} + +#[cfg(not(target_os = "linux"))] +fn try_memfd_mapping(_len: NonZeroUsize) -> Option> { + None +} + /// Name the mapping `OTEL_CTX` via `prctl(PR_SET_VMA_ANON_NAME)`. #[cfg(target_os = "linux")] fn name_mapping(ptr: NonNull, len: usize) { - linux::name_mapping(ptr, len); + const NAME: &core::ffi::CStr = c"OTEL_CTX"; + unsafe { + let _ = nix::libc::prctl( + PR_SET_VMA, + PR_SET_VMA_ANON_NAME, + ptr.as_ptr() as nix::libc::c_ulong, + len as nix::libc::c_ulong, + NAME.as_ptr() as nix::libc::c_ulong, + ); + } } #[cfg(not(target_os = "linux"))] fn name_mapping(_ptr: NonNull, _len: usize) {} -#[cfg(target_os = "linux")] -mod linux; +#[cfg(test)] +mod tests { + use super::{PublishError, publish, unpublish}; + use serial_test::serial; + + #[test] + fn get_boottime_ns_is_nonzero() { + let ns = super::get_boottime_ns().unwrap(); + assert!(ns >= 1); + } + + #[test] + #[serial] + fn unpublish_without_publish_returns_not_published() { + let _ = unpublish(); + assert!(matches!(unpublish(), Err(PublishError::NotPublished))); + } + + #[test] + #[serial] + fn publish_then_unpublish_succeeds() { + let _ = unpublish(); + publish(b"test".to_vec()).unwrap(); + unpublish().unwrap(); + } + + #[test] + #[serial] + fn double_unpublish_returns_not_published() { + let _ = unpublish(); + publish(b"test".to_vec()).unwrap(); + unpublish().unwrap(); + assert!(matches!(unpublish(), Err(PublishError::NotPublished))); + } + + #[test] + #[cfg(target_os = "linux")] + fn try_memfd_mapping_returns_some() { + use std::num::NonZeroUsize; + let len = NonZeroUsize::new(32).unwrap(); + let ptr = super::try_memfd_mapping(len).expect("memfd_create + mmap should succeed"); + unsafe { nix::sys::mman::munmap(ptr, 32).unwrap() }; + } +} diff --git a/opentelemetry-process-context/rust/src/context/linux.rs b/opentelemetry-process-context/rust/src/context/linux.rs deleted file mode 100644 index e9b7b30d861..00000000000 --- a/opentelemetry-process-context/rust/src/context/linux.rs +++ /dev/null @@ -1,54 +0,0 @@ -// Copyright The OpenTelemetry Authors -// SPDX-License-Identifier: Apache-2.0 - -use std::ffi::c_void; -use std::num::NonZeroUsize; -use std::ptr::NonNull; - -use nix::sys::memfd::{MFdFlags, memfd_create}; -use nix::sys::mman::{MapFlags, ProtFlags, mmap}; - -// `prctl` options for naming an anonymous VMA. -const PR_SET_VMA: nix::libc::c_int = 0x53564d41; -const PR_SET_VMA_ANON_NAME: nix::libc::c_ulong = 0; - -/// Create a `memfd`, size it, and map it `MAP_PRIVATE`. Returns `None` if any -/// step fails so the caller can fall back to an anonymous mapping. -pub(super) fn try_memfd_mapping(len: NonZeroUsize) -> Option> { - let base = MFdFlags::MFD_CLOEXEC | MFdFlags::MFD_ALLOW_SEALING; - // `MFD_NOEXEC_SEAL` is a newer flag not exposed by `nix`, request it but - // fall back without it on kernels/libc that lack it. - let noexec_seal = MFdFlags::from_bits_retain(nix::libc::MFD_NOEXEC_SEAL as _); - let fd = memfd_create(c"OTEL_CTX", base | noexec_seal) - .or_else(|_| memfd_create(c"OTEL_CTX", base)) - .ok()?; - - nix::unistd::ftruncate(&fd, len.get() as nix::libc::off_t).ok()?; - - // SAFETY: `fd` is a valid, sized memfd and `len` is non-zero. - let ptr = unsafe { - mmap( - None, - len, - ProtFlags::PROT_READ | ProtFlags::PROT_WRITE, - MapFlags::MAP_PRIVATE, - &fd, - 0, - ) - } - .ok()?; - Some(ptr) -} - -pub(super) fn name_mapping(ptr: NonNull, len: usize) { - const NAME: &core::ffi::CStr = c"OTEL_CTX"; - unsafe { - let _ = nix::libc::prctl( - PR_SET_VMA, - PR_SET_VMA_ANON_NAME, - ptr.as_ptr() as nix::libc::c_ulong, - len as nix::libc::c_ulong, - NAME.as_ptr() as nix::libc::c_ulong, - ); - } -} diff --git a/opentelemetry-process-context/rust/src/convert.rs b/opentelemetry-process-context/rust/src/convert.rs index a24c8e7481b..08042e2486c 100644 --- a/opentelemetry-process-context/rust/src/convert.rs +++ b/opentelemetry-process-context/rust/src/convert.rs @@ -108,3 +108,44 @@ pub fn encode_process_context(resource: Resource) -> Vec { } .encode_to_vec() } + +#[cfg(test)] +mod tests { + use super::encode_process_context; + use crate::proto::common::v1::{AnyValue, KeyValue, any_value}; + use crate::proto::processcontext::v1development::ProcessContext; + use crate::proto::resource::v1::Resource; + use prost::Message; + + #[test] + fn encode_empty_resource_roundtrip() { + let bytes = encode_process_context(Resource::default()); + let ctx = ProcessContext::decode(bytes.as_slice()).unwrap(); + assert!(ctx.resource.is_some()); + assert!(ctx.resource.unwrap().attributes.is_empty()); + assert!(ctx.attributes.is_empty()); + } + + #[test] + fn encode_resource_with_string_attribute_roundtrip() { + let resource = Resource { + attributes: vec![KeyValue { + key: "service.name".into(), + value: Some(AnyValue { + value: Some(any_value::Value::StringValue("my-service".into())), + }), + ..Default::default() + }], + ..Default::default() + }; + let bytes = encode_process_context(resource); + let ctx = ProcessContext::decode(bytes.as_slice()).unwrap(); + let attrs = ctx.resource.unwrap().attributes; + assert_eq!(attrs.len(), 1); + assert_eq!(attrs[0].key, "service.name"); + assert!(matches!( + attrs[0].value.as_ref().unwrap().value, + Some(any_value::Value::StringValue(ref s)) if s == "my-service" + )); + } +} diff --git a/opentelemetry-process-context/rust/src/lib.rs b/opentelemetry-process-context/rust/src/lib.rs index 7d8ae6044a2..081147ad4ab 100644 --- a/opentelemetry-process-context/rust/src/lib.rs +++ b/opentelemetry-process-context/rust/src/lib.rs @@ -1,29 +1,47 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 +#[cfg(all(unix, target_has_atomic = "64"))] mod context; mod convert; pub(crate) mod proto; -use crate::convert::{encode_process_context, resource_from_py}; use pyo3::prelude::*; #[pyfunction] fn publish_context(resource: &Bound<'_, PyAny>) -> PyResult<()> { - let resource = resource_from_py(resource)?; - context::publish(encode_process_context(resource))?; - Ok(()) + #[cfg(all(unix, target_has_atomic = "64"))] + { + let resource = crate::convert::resource_from_py(resource)?; + context::publish(crate::convert::encode_process_context(resource))?; + Ok(()) + } + #[cfg(not(all(unix, target_has_atomic = "64")))] + { + let _ = resource; + Err(pyo3::exceptions::PyRuntimeError::new_err( + "process context publication requires a Unix-like OS with 64 bit atomic support", + )) + } } #[pyfunction] fn unpublish_context() -> PyResult<()> { - context::unpublish()?; - Ok(()) + #[cfg(all(unix, target_has_atomic = "64"))] + { + context::unpublish()?; + Ok(()) + } + #[cfg(not(all(unix, target_has_atomic = "64")))] + Err(pyo3::exceptions::PyRuntimeError::new_err( + "process context publication requires a Unix-like OS with 64 bit atomic support", + )) } #[pymodule] #[pyo3(name = "_rs")] fn init(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + #[cfg(all(unix, target_has_atomic = "64"))] context::register_fork_handlers(); m.add_wrapped(wrap_pyfunction!(publish_context))?; m.add_wrapped(wrap_pyfunction!(unpublish_context)) From 9882a8948faf8da2d05c779d68b9b1ef7fe85ffe Mon Sep 17 00:00:00 2001 From: Lukas Hering Date: Thu, 25 Jun 2026 22:12:45 -0500 Subject: [PATCH 29/35] update workflows --- .github/workflows/ci.yml | 3 ++ .github/workflows/rust.yml | 43 +++++++++++++++++++++++++++ .github/workflows/templates/ci.yml.j2 | 3 ++ 3 files changed, 49 insertions(+) create mode 100644 .github/workflows/rust.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 68cedf55840..2ba8bc7b16b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,6 +22,8 @@ jobs: uses: ./.github/workflows/misc.yml lint: uses: ./.github/workflows/lint.yml + rust: + uses: ./.github/workflows/rust.yml tests: uses: ./.github/workflows/test.yml contrib: @@ -39,6 +41,7 @@ jobs: needs: - misc - lint + - rust - tests - contrib runs-on: ubuntu-latest diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml new file mode 100644 index 00000000000..43a6451835e --- /dev/null +++ b/.github/workflows/rust.yml @@ -0,0 +1,43 @@ +# This file is not auto-generated. See .github/workflows/templates/ for generated CI files. + +name: Rust + +on: + workflow_call: + +permissions: + contents: read + +jobs: + fmt-process-context: + name: cargo fmt (opentelemetry-process-context) + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - name: Update Rust toolchain + run: rustup update stable && rustup default stable + - name: cargo fmt + run: cargo fmt --check --manifest-path opentelemetry-process-context/rust/Cargo.toml + + clippy-process-context: + name: cargo clippy (opentelemetry-process-context) + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - name: Update Rust toolchain + run: rustup update stable && rustup default stable + - name: cargo clippy + run: cargo clippy --manifest-path opentelemetry-process-context/rust/Cargo.toml -- -D warnings + + test-process-context: + name: cargo test (opentelemetry-process-context) + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - name: Update Rust toolchain + run: rustup update stable && rustup default stable + - name: cargo test + run: cargo test --manifest-path opentelemetry-process-context/rust/Cargo.toml diff --git a/.github/workflows/templates/ci.yml.j2 b/.github/workflows/templates/ci.yml.j2 index ac67f5b2f94..a9d0039c689 100644 --- a/.github/workflows/templates/ci.yml.j2 +++ b/.github/workflows/templates/ci.yml.j2 @@ -22,6 +22,8 @@ jobs: uses: ./.github/workflows/misc.yml lint: uses: ./.github/workflows/lint.yml + rust: + uses: ./.github/workflows/rust.yml tests: uses: ./.github/workflows/test.yml contrib: @@ -39,6 +41,7 @@ jobs: needs: - misc - lint + - rust - tests - contrib runs-on: ubuntu-latest From e06d969c894fd08482b7d499cee26c14843da32d Mon Sep 17 00:00:00 2001 From: Lukas Hering Date: Thu, 25 Jun 2026 22:54:01 -0500 Subject: [PATCH 30/35] fix mmap leak scenario --- .github/workflows/release.yml | 2 ++ .../rust/src/context.rs | 13 ++++++-- .../rust/src/convert.rs | 31 ++++++++++++++++++- 3 files changed, 43 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 82001a9aac0..aaea6fcbf9d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -8,6 +8,7 @@ permissions: jobs: build-process-context-wheels: name: process-context wheels (${{ matrix.platform.target }} ${{ matrix.platform.manylinux }}) + if: startsWith(github.ref_name, 'release/') runs-on: ubuntu-latest strategy: fail-fast: false @@ -43,6 +44,7 @@ jobs: build-process-context-sdist: name: process-context sdist + if: startsWith(github.ref_name, 'release/') runs-on: ubuntu-latest steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 diff --git a/opentelemetry-process-context/rust/src/context.rs b/opentelemetry-process-context/rust/src/context.rs index 7964f3a298a..436cf4b17db 100644 --- a/opentelemetry-process-context/rust/src/context.rs +++ b/opentelemetry-process-context/rust/src/context.rs @@ -159,10 +159,19 @@ impl From for PyErr { /// Allocate the mapping and write the initial header. Called with the mutex held /// and `guard` confirmed to be `None`. fn publish_new(guard: &mut Option, payload: Vec) -> Result<(), PublishError> { + let timestamp = get_boottime_ns()?; + let ptr = alloc_region()?; - advise_dontfork(ptr, HEADER_SIZE)?; - let timestamp = get_boottime_ns()?; + // `ptr` is now a live mapping we own but have not yet stored in `guard`. + // If advising fails, unmap it before propagating so it does not leak. + if let Err(err) = advise_dontfork(ptr, HEADER_SIZE) { + // SAFETY: `ptr`/`HEADER_SIZE` describe the mapping we just created. + unsafe { + let _ = nix::sys::mman::munmap(ptr, HEADER_SIZE); + } + return Err(err); + } // SAFETY: `ptr` points to a freshly mapped, zero initialized, page aligned // region of exactly `HEADER_SIZE` bytes. The payload lives in `payload` on diff --git a/opentelemetry-process-context/rust/src/convert.rs b/opentelemetry-process-context/rust/src/convert.rs index 08042e2486c..d246e15230d 100644 --- a/opentelemetry-process-context/rust/src/convert.rs +++ b/opentelemetry-process-context/rust/src/convert.rs @@ -7,7 +7,7 @@ use crate::proto::resource::v1::Resource; use prost::Message; use pyo3::exceptions::PyTypeError; use pyo3::prelude::*; -use pyo3::types::{PyBool, PyDict, PyFloat, PyInt, PyString}; +use pyo3::types::{PyBool, PyByteArray, PyBytes, PyDict, PyFloat, PyInt, PyString}; fn any_value_from_py(val: &Bound<'_, PyAny>) -> PyResult { if val.is_none() { @@ -33,6 +33,13 @@ fn any_value_from_py(val: &Bound<'_, PyAny>) -> PyResult { value: Some(any_value::Value::StringValue(val.extract()?)), }); } + // `bytes`/`bytearray` are `Sequence`s, so this must precede the Sequence + // branch below or they would be expanded into an array of integers. + if val.is_instance_of::() || val.is_instance_of::() { + return Ok(AnyValue { + value: Some(any_value::Value::BytesValue(val.extract()?)), + }); + } let py = val.py(); let collections_abc = py.import("collections.abc")?; @@ -148,4 +155,26 @@ mod tests { Some(any_value::Value::StringValue(ref s)) if s == "my-service" )); } + + #[test] + fn encode_resource_with_bytes_attribute_roundtrip() { + let resource = Resource { + attributes: vec![KeyValue { + key: "raw".into(), + value: Some(AnyValue { + value: Some(any_value::Value::BytesValue(vec![1, 2, 3])), + }), + ..Default::default() + }], + ..Default::default() + }; + let bytes = encode_process_context(resource); + let ctx = ProcessContext::decode(bytes.as_slice()).unwrap(); + let attrs = ctx.resource.unwrap().attributes; + assert_eq!(attrs.len(), 1); + assert!(matches!( + attrs[0].value.as_ref().unwrap().value, + Some(any_value::Value::BytesValue(ref b)) if b == &[1, 2, 3] + )); + } } From 48dd8ca057190ca57633243882d8a738f2c0d7e0 Mon Sep 17 00:00:00 2001 From: Lukas Hering Date: Thu, 25 Jun 2026 23:26:06 -0500 Subject: [PATCH 31/35] add explicit manifest-path to pyproject.toml --- .github/workflows/release.yml | 14 ++++++++++++-- opentelemetry-process-context/pyproject.toml | 1 + 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index aaea6fcbf9d..6a2b5f16f51 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -8,7 +8,6 @@ permissions: jobs: build-process-context-wheels: name: process-context wheels (${{ matrix.platform.target }} ${{ matrix.platform.manylinux }}) - if: startsWith(github.ref_name, 'release/') runs-on: ubuntu-latest strategy: fail-fast: false @@ -27,6 +26,12 @@ jobs: - { target: aarch64, manylinux: musllinux_1_2 } - { target: armv7, manylinux: musllinux_1_2 } steps: + - run: | + if [[ $GITHUB_REF_NAME != release/* ]]; then + echo this workflow should only be run against release branches + exit 1 + fi + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Build wheels @@ -44,9 +49,14 @@ jobs: build-process-context-sdist: name: process-context sdist - if: startsWith(github.ref_name, 'release/') 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@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Build sdist diff --git a/opentelemetry-process-context/pyproject.toml b/opentelemetry-process-context/pyproject.toml index 642dde94089..1ee2a47ca57 100644 --- a/opentelemetry-process-context/pyproject.toml +++ b/opentelemetry-process-context/pyproject.toml @@ -38,6 +38,7 @@ Repository = "https://github.com/open-telemetry/opentelemetry-python" [tool.maturin] python-source = "src" module-name = "opentelemetry.process_context._rs" +manifest-path = "rust/Cargo.toml" [tool.uv] cache-keys = [ From ae9de41648f5dd0fd0bbe5606ebb3bf052420c6a Mon Sep 17 00:00:00 2001 From: Lukas Hering Date: Fri, 26 Jun 2026 22:28:04 -0500 Subject: [PATCH 32/35] remove pre/post fork handlers --- opentelemetry-process-context/rust/Cargo.lock | 1 - opentelemetry-process-context/rust/Cargo.toml | 1 - .../rust/src/context.rs | 114 +++++++++--------- opentelemetry-process-context/rust/src/lib.rs | 2 - .../scripts/fork_unpublish_without_publish.py | 37 ++++++ .../tests/test_process_context.py | 14 +++ 6 files changed, 105 insertions(+), 64 deletions(-) create mode 100644 opentelemetry-process-context/tests/scripts/fork_unpublish_without_publish.py diff --git a/opentelemetry-process-context/rust/Cargo.lock b/opentelemetry-process-context/rust/Cargo.lock index ebafa0944e5..f919f6cda79 100644 --- a/opentelemetry-process-context/rust/Cargo.lock +++ b/opentelemetry-process-context/rust/Cargo.lock @@ -132,7 +132,6 @@ name = "opentelemetry-process-context" version = "0.1.0" dependencies = [ "nix", - "parking_lot", "prost", "pyo3", "serial_test", diff --git a/opentelemetry-process-context/rust/Cargo.toml b/opentelemetry-process-context/rust/Cargo.toml index 58fc4a93ca1..98890b68ffe 100644 --- a/opentelemetry-process-context/rust/Cargo.toml +++ b/opentelemetry-process-context/rust/Cargo.toml @@ -16,7 +16,6 @@ features = ["abi3-py39"] [target.'cfg(unix)'.dependencies] nix = { version = "0.31", features = ["mman", "fs", "time"] } -parking_lot = "0.12" [dev-dependencies] serial_test = "3" diff --git a/opentelemetry-process-context/rust/src/context.rs b/opentelemetry-process-context/rust/src/context.rs index 436cf4b17db..84d87f6ec55 100644 --- a/opentelemetry-process-context/rust/src/context.rs +++ b/opentelemetry-process-context/rust/src/context.rs @@ -12,10 +12,9 @@ use std::ffi::c_void; use std::num::NonZeroUsize; use std::ptr::NonNull; +use std::sync::Mutex; use std::sync::atomic::{AtomicU64, Ordering}; -use parking_lot::Mutex; - use nix::sys::mman::{MapFlags, ProtFlags, mmap_anonymous}; use nix::time::{ClockId, clock_gettime}; use pyo3::PyErr; @@ -51,6 +50,9 @@ struct Header { /// A published mapping. struct Region { ptr: NonNull, + /// PID of the process that created this region. Used to detect a region + /// inherited across a `fork`, whose `ptr` must not be dereferenced. + owner_pid: u32, #[allow(unused)] payload: Vec, } @@ -62,62 +64,29 @@ unsafe impl Send for Region {} /// The single active process context for this process, if any. static MAPPING: Mutex> = Mutex::new(None); -/// Register the `pthread_atfork` handlers that keep [`MAPPING`] fork-safe. -/// -/// Idempotent: only the first call registers the handlers. Without these, a -/// forked child inherits the parent's `MAPPING` state while the underlying -/// header mapping is gone (stripped by `MADV_DONTFORK` on Linux, or merely -/// stale elsewhere), so a `publish()` in the child would dereference a pointer -/// into an unmapped region and crash. -pub fn register_fork_handlers() { - static REGISTERED: std::sync::Once = std::sync::Once::new(); - REGISTERED.call_once(|| { - // SAFETY: the handlers are valid `'static` `extern "C"` functions. - unsafe { - nix::libc::pthread_atfork( - Some(before_fork), - Some(after_fork_parent), - Some(after_fork_child), - ); - } - }); -} - -/// `pthread_atfork` prepare handler: acquire the mapping mutex so no other -/// thread holds it across the fork, then forget the guard so the lock stays -/// held into the parent/child handlers, which release it via `force_unlock`. -extern "C" fn before_fork() { - std::mem::forget(MAPPING.lock()); +/// Lock [`MAPPING`], recovering the guard if a previous holder panicked. The +/// protected data is a plain `Option` with no broken invariants on +/// panic, so ignoring poisoning is safe and keeps the API usable. +fn lock_mapping() -> std::sync::MutexGuard<'static, Option> { + MAPPING + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()) } -/// `pthread_atfork` parent handler: release the lock taken in [`before_fork`]. -extern "C" fn after_fork_parent() { - // SAFETY: `before_fork` acquired the lock and forgot its guard, so this - // thread logically owns the lock. - unsafe { MAPPING.force_unlock() }; -} - -/// `pthread_atfork` child handler: discard the inherited mapping (the child -/// never received the live header mapping) so the child starts from the -/// pristine "never published" state, then release the lock. -extern "C" fn after_fork_child() { - // SAFETY: the child is single threaded here and the lock is held from - // `before_fork`, so this is exclusive access to the protected data. - let mapping = unsafe { &mut *MAPPING.data_ptr() }; - if let Some(region) = mapping.take() { - // On non-Linux the mapping was inherited (no `MADV_DONTFORK`), so unmap - // it. On Linux the child never received the mapping, so the pointer is - // simply abandoned. - #[cfg(not(target_os = "linux"))] - // SAFETY: `region.ptr`/`HEADER_SIZE` describe the inherited mapping. - unsafe { - let _ = nix::sys::mman::munmap(region.ptr, HEADER_SIZE); - } - // `region.payload` (the inherited `Vec` copy) is dropped here. - drop(region); +/// Discard a `Region` inherited from a parent process across a `fork`. On Linux +/// the header mapping was never inherited (`MADV_DONTFORK`), so `region.ptr` +/// points to unmapped memory and is simply abandoned. Elsewhere the child +/// inherited a private copy, so unmap it to avoid leaking. The inherited +/// `payload` Vec is dropped in both cases. +fn discard_inherited(region: Region) { + #[cfg(not(target_os = "linux"))] + // SAFETY: on non-Linux the child holds a private copy of the mapping at + // `region.ptr` spanning `HEADER_SIZE` bytes. + unsafe { + let _ = nix::sys::mman::munmap(region.ptr, HEADER_SIZE); } - // SAFETY: release the lock acquired (and forgotten) in `before_fork`. - unsafe { MAPPING.force_unlock() }; + // Explicit drop for readability + drop(region); } #[derive(Debug)] @@ -158,7 +127,11 @@ impl From for PyErr { /// Allocate the mapping and write the initial header. Called with the mutex held /// and `guard` confirmed to be `None`. -fn publish_new(guard: &mut Option, payload: Vec) -> Result<(), PublishError> { +fn publish_new( + guard: &mut Option, + payload: Vec, + owner_pid: u32, +) -> Result<(), PublishError> { let timestamp = get_boottime_ns()?; let ptr = alloc_region()?; @@ -194,7 +167,11 @@ fn publish_new(guard: &mut Option, payload: Vec) -> Result<(), Publi // path, failures are ignored per the spec. name_mapping(ptr, HEADER_SIZE); - *guard = Some(Region { ptr, payload }); + *guard = Some(Region { + ptr, + owner_pid, + payload, + }); Ok(()) } @@ -243,11 +220,19 @@ fn publish_existing(region: &mut Region, payload: Vec) -> Result<(), Publish /// Creates the named memory mapping on the first call. On subsequent calls the /// existing mapping is updated in-place using the spec's Updating Protocol pub fn publish(payload: Vec) -> Result<(), PublishError> { - let mut guard = MAPPING.lock(); + let current_pid = std::process::id(); + let mut guard = lock_mapping(); + + // A region whose owner PID differs from ours was inherited across a fork. + // Drop it so the stale parent pointer is never dereferenced. + if guard.as_ref().is_some_and(|r| r.owner_pid != current_pid) { + discard_inherited(guard.take().unwrap()); + } + if let Some(region) = guard.as_mut() { publish_existing(region, payload) } else { - publish_new(&mut guard, payload) + publish_new(&mut guard, payload, current_pid) } } @@ -257,7 +242,16 @@ pub fn publish(payload: Vec) -> Result<(), PublishError> { /// invalid state, then `munmap`s the header and drops the payload buffer. /// After a successful call, `publish()` may be called again. pub fn unpublish() -> Result<(), PublishError> { - let mut guard = MAPPING.lock(); + let current_pid = std::process::id(); + let mut guard = lock_mapping(); + + // A region inherited across a fork was never published by this process. + // Discard it and report that nothing was published here. + if guard.as_ref().is_some_and(|r| r.owner_pid != current_pid) { + discard_inherited(guard.take().unwrap()); + return Err(PublishError::NotPublished); + } + let region = guard.take().ok_or(PublishError::NotPublished)?; // Zero the timestamp and remove the mapping. diff --git a/opentelemetry-process-context/rust/src/lib.rs b/opentelemetry-process-context/rust/src/lib.rs index 081147ad4ab..66fedb3ca60 100644 --- a/opentelemetry-process-context/rust/src/lib.rs +++ b/opentelemetry-process-context/rust/src/lib.rs @@ -41,8 +41,6 @@ fn unpublish_context() -> PyResult<()> { #[pymodule] #[pyo3(name = "_rs")] fn init(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { - #[cfg(all(unix, target_has_atomic = "64"))] - context::register_fork_handlers(); m.add_wrapped(wrap_pyfunction!(publish_context))?; m.add_wrapped(wrap_pyfunction!(unpublish_context)) } diff --git a/opentelemetry-process-context/tests/scripts/fork_unpublish_without_publish.py b/opentelemetry-process-context/tests/scripts/fork_unpublish_without_publish.py new file mode 100644 index 00000000000..d44e820c7aa --- /dev/null +++ b/opentelemetry-process-context/tests/scripts/fork_unpublish_without_publish.py @@ -0,0 +1,37 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +# pylint: disable=protected-access,bare-except + +import os +import sys + +from opentelemetry.process_context import publish_context, unpublish_context +from opentelemetry.sdk.resources import Resource + +resource = Resource({"service.name": "test", "version": 1}) +publish_context(resource) + +pid = os.fork() +if not pid: + # Child inherits the parent's region (stale owner PID). It never published, + # so unpublish must raise RuntimeError (NotPublished) rather than crash. + try: + unpublish_context() + except RuntimeError: + os._exit(0) + except: # noqa: E722 + os._exit(2) + # No exception at all is also wrong: the child published nothing. + os._exit(1) + +_, status = os.waitpid(pid, 0) +if not (os.WIFEXITED(status) and os.WEXITSTATUS(status) == 0): + sys.exit(1) + +# The parent's own mapping is untouched and still usable. +try: + unpublish_context() +except: # noqa: E722 + sys.exit(1) +sys.exit(0) diff --git a/opentelemetry-process-context/tests/test_process_context.py b/opentelemetry-process-context/tests/test_process_context.py index 9c0173ec846..e79f76f1d63 100644 --- a/opentelemetry-process-context/tests/test_process_context.py +++ b/opentelemetry-process-context/tests/test_process_context.py @@ -123,6 +123,20 @@ def test_publish_in_forked_child(self): "fork/re-publish script did not exit cleanly", ) + @unittest.skipUnless(hasattr(os, "fork"), "requires os.fork") + def test_unpublish_in_forked_child_without_publish(self): + """A child that inherits the parent's region but never publishes must + get NotPublished from unpublish (not a crash), and the parent stays + usable.""" + result = subprocess.run( + _script_cmd("fork_unpublish_without_publish.py"), check=False + ) + self.assertEqual( + result.returncode, + 0, + "fork/unpublish-without-publish script did not exit cleanly", + ) + @unittest.skipUnless( sys.platform.startswith("linux"), "requires /proc//{maps,mem}" ) From 891c8018b1f86833bac0a746ec6d491debb5a521 Mon Sep 17 00:00:00 2001 From: Lukas Hering Date: Sat, 27 Jun 2026 22:04:54 -0500 Subject: [PATCH 33/35] update package README.rst --- opentelemetry-process-context/README.rst | 57 ++++++++++++++++++++++-- 1 file changed, 54 insertions(+), 3 deletions(-) diff --git a/opentelemetry-process-context/README.rst b/opentelemetry-process-context/README.rst index a4d5311c29b..998489a375f 100644 --- a/opentelemetry-process-context/README.rst +++ b/opentelemetry-process-context/README.rst @@ -1,15 +1,66 @@ OpenTelemetry Process Context -====================================== +============================== |pypi| .. |pypi| image:: https://badge.fury.io/py/opentelemetry-process-context.svg :target: https://pypi.org/project/opentelemetry-process-context/ -TODO: Update README +This library implements the `OpenTelemetry Process Context`_ specification (OTEP 4719). +It serializes the process's OTel ``Resource`` attributes into a protobuf payload and +publishes them into a named memory mapped region called ``OTEL_CTX``, making the +process discoverable by out-of-process readers such as the +`OpenTelemetry eBPF Profiler`_ via ``/proc//maps`` and ``/proc//mem`` on +Linux without any in-process integration or network communication. + +.. _OpenTelemetry Process Context: https://github.com/open-telemetry/opentelemetry-specification/blob/main/oteps/profiles/4719-process-ctx.md +.. _OpenTelemetry eBPF Profiler: https://github.com/open-telemetry/opentelemetry-ebpf-profiler + +Installation +------------ + +:: + + pip install opentelemetry-process-context + +Platform Requirements +--------------------- + +This package targets Linux. On Linux the mapping is created via ``memfd`` so it +appears as a named entry (``/memfd:OTEL_CTX``) in ``/proc//maps``, and +``MADV_DONTFORK`` prevents child processes from inheriting it. A fallback to an +anonymous ``mmap`` is provided for other Unix-like systems, non-Unix and 32-bit +platforms raise ``RuntimeError``. + +Usage +----- + +Call ``publish_context`` with the same ``Resource`` used to configure your +OpenTelemetry SDK provider. The mapping stays live for the lifetime of the process. +Call ``unpublish_context`` on shutdown to remove it explicitly. + +.. code-block:: python + + from opentelemetry.sdk.resources import Resource + from opentelemetry.sdk.trace import TracerProvider + from opentelemetry.process_context import publish_context, unpublish_context + + resource = Resource(attributes={ + "service.name": "my-service", + "service.version": "1.0.0", + }) + + provider = TracerProvider(resource=resource) + # register provider with trace / metrics / logs APIs ... + + publish_context(resource) + + # On shutdown (optional): + unpublish_context() References ---------- +* `OpenTelemetry Process Context specification (OTEP 4719) `_ +* `OpenTelemetry eBPF Profiler `_ * `OpenTelemetry `_ -* `OpenTelemetry Protocol Specification `_ From 837364020cec0af79e7ac67cd5d93615d89c9590 Mon Sep 17 00:00:00 2001 From: Lukas Hering Date: Sat, 27 Jun 2026 22:38:15 -0500 Subject: [PATCH 34/35] add support for publishing extra attributes --- opentelemetry-process-context/README.rst | 4 + .../rust/src/convert.rs | 82 ++++++++++++------- opentelemetry-process-context/rust/src/lib.rs | 16 +++- .../process_context/_rs/__init__.pyi | 9 +- .../tests/scripts/publish_and_wait.py | 2 +- .../tests/test_process_context.py | 12 +++ 6 files changed, 89 insertions(+), 36 deletions(-) diff --git a/opentelemetry-process-context/README.rst b/opentelemetry-process-context/README.rst index 998489a375f..956aa0bd88f 100644 --- a/opentelemetry-process-context/README.rst +++ b/opentelemetry-process-context/README.rst @@ -55,6 +55,10 @@ Call ``unpublish_context`` on shutdown to remove it explicitly. publish_context(resource) + # Optionally publish supplementary attributes that are not part of the + # standard Resource. + publish_context(resource, {"deployment.environment": "prod"}) + # On shutdown (optional): unpublish_context() diff --git a/opentelemetry-process-context/rust/src/convert.rs b/opentelemetry-process-context/rust/src/convert.rs index d246e15230d..159948abac1 100644 --- a/opentelemetry-process-context/rust/src/convert.rs +++ b/opentelemetry-process-context/rust/src/convert.rs @@ -45,23 +45,10 @@ fn any_value_from_py(val: &Bound<'_, PyAny>) -> PyResult { let collections_abc = py.import("collections.abc")?; if val.is_instance(collections_abc.getattr("Mapping")?.as_ref())? { - let py_dict = py - .import("builtins")? - .getattr("dict")? - .call1((val,))? - .cast_into::()?; - let values = py_dict - .iter() - .map(|(k, v)| { - Ok(KeyValue { - key: k.str()?.extract()?, - value: Some(any_value_from_py(&v)?), - ..Default::default() - }) - }) - .collect::>()?; return Ok(AnyValue { - value: Some(any_value::Value::KvlistValue(KeyValueList { values })), + value: Some(any_value::Value::KvlistValue(KeyValueList { + values: key_values_from_py(val)?, + })), }); } @@ -81,37 +68,39 @@ fn any_value_from_py(val: &Bound<'_, PyAny>) -> PyResult { ))) } -pub fn resource_from_py(resource: &Bound<'_, PyAny>) -> PyResult { - let py = resource.py(); - let attrs_obj = resource.getattr("attributes")?; - +/// Convert a Python `Mapping[str, Any]` into a list of protobuf `KeyValue`s, +/// recursing into nested values via [`any_value_from_py`]. +pub fn key_values_from_py(mapping: &Bound<'_, PyAny>) -> PyResult> { + let py = mapping.py(); let py_dict = py .import("builtins")? .getattr("dict")? - .call1((&attrs_obj,))? + .call1((mapping,))? .cast_into::()?; - let attributes = py_dict + py_dict .iter() .map(|(key, val)| { Ok(KeyValue { - key: key.extract()?, + key: key.str()?.extract()?, value: Some(any_value_from_py(&val)?), ..Default::default() }) }) - .collect::>()?; + .collect() +} +pub fn resource_from_py(resource: &Bound<'_, PyAny>) -> PyResult { Ok(Resource { - attributes, + attributes: key_values_from_py(&resource.getattr("attributes")?)?, ..Default::default() }) } -pub fn encode_process_context(resource: Resource) -> Vec { +pub fn encode_process_context(resource: Resource, attributes: Vec) -> Vec { ProcessContext { resource: Some(resource), - attributes: vec![], + attributes, } .encode_to_vec() } @@ -124,15 +113,48 @@ mod tests { use crate::proto::resource::v1::Resource; use prost::Message; + fn string_kv(key: &str, value: &str) -> KeyValue { + KeyValue { + key: key.into(), + value: Some(AnyValue { + value: Some(any_value::Value::StringValue(value.into())), + }), + ..Default::default() + } + } + #[test] fn encode_empty_resource_roundtrip() { - let bytes = encode_process_context(Resource::default()); + let bytes = encode_process_context(Resource::default(), vec![]); let ctx = ProcessContext::decode(bytes.as_slice()).unwrap(); assert!(ctx.resource.is_some()); assert!(ctx.resource.unwrap().attributes.is_empty()); assert!(ctx.attributes.is_empty()); } + #[test] + fn encode_with_additional_attributes_roundtrip() { + let resource = Resource { + attributes: vec![string_kv("service.name", "my-service")], + ..Default::default() + }; + let bytes = + encode_process_context(resource, vec![string_kv("deployment.environment", "prod")]); + let ctx = ProcessContext::decode(bytes.as_slice()).unwrap(); + + // Resource attributes and the additional attributes are kept separate. + let resource_attrs = ctx.resource.unwrap().attributes; + assert_eq!(resource_attrs.len(), 1); + assert_eq!(resource_attrs[0].key, "service.name"); + + assert_eq!(ctx.attributes.len(), 1); + assert_eq!(ctx.attributes[0].key, "deployment.environment"); + assert!(matches!( + ctx.attributes[0].value.as_ref().unwrap().value, + Some(any_value::Value::StringValue(ref s)) if s == "prod" + )); + } + #[test] fn encode_resource_with_string_attribute_roundtrip() { let resource = Resource { @@ -145,7 +167,7 @@ mod tests { }], ..Default::default() }; - let bytes = encode_process_context(resource); + let bytes = encode_process_context(resource, vec![]); let ctx = ProcessContext::decode(bytes.as_slice()).unwrap(); let attrs = ctx.resource.unwrap().attributes; assert_eq!(attrs.len(), 1); @@ -168,7 +190,7 @@ mod tests { }], ..Default::default() }; - let bytes = encode_process_context(resource); + let bytes = encode_process_context(resource, vec![]); let ctx = ProcessContext::decode(bytes.as_slice()).unwrap(); let attrs = ctx.resource.unwrap().attributes; assert_eq!(attrs.len(), 1); diff --git a/opentelemetry-process-context/rust/src/lib.rs b/opentelemetry-process-context/rust/src/lib.rs index 66fedb3ca60..0d7c2eb3c2e 100644 --- a/opentelemetry-process-context/rust/src/lib.rs +++ b/opentelemetry-process-context/rust/src/lib.rs @@ -9,16 +9,24 @@ pub(crate) mod proto; use pyo3::prelude::*; #[pyfunction] -fn publish_context(resource: &Bound<'_, PyAny>) -> PyResult<()> { +#[pyo3(signature = (resource, attributes = None))] +fn publish_context( + resource: &Bound<'_, PyAny>, + attributes: Option<&Bound<'_, PyAny>>, +) -> PyResult<()> { #[cfg(all(unix, target_has_atomic = "64"))] { - let resource = crate::convert::resource_from_py(resource)?; - context::publish(crate::convert::encode_process_context(resource))?; + let resource = convert::resource_from_py(resource)?; + let attributes = match attributes { + Some(attributes) => convert::key_values_from_py(attributes)?, + None => Vec::new(), + }; + context::publish(convert::encode_process_context(resource, attributes))?; Ok(()) } #[cfg(not(all(unix, target_has_atomic = "64")))] { - let _ = resource; + let _ = (resource, attributes); Err(pyo3::exceptions::PyRuntimeError::new_err( "process context publication requires a Unix-like OS with 64 bit atomic support", )) diff --git a/opentelemetry-process-context/src/opentelemetry/process_context/_rs/__init__.pyi b/opentelemetry-process-context/src/opentelemetry/process_context/_rs/__init__.pyi index 047d679f137..f79458ef270 100644 --- a/opentelemetry-process-context/src/opentelemetry/process_context/_rs/__init__.pyi +++ b/opentelemetry-process-context/src/opentelemetry/process_context/_rs/__init__.pyi @@ -1,9 +1,14 @@ # Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 +from collections.abc import Mapping +from typing import Any + from opentelemetry.sdk.resources import Resource -def publish_context(resource: Resource) -> None: +def publish_context( + resource: Resource, attributes: Mapping[str, Any] | None = None +) -> None: """Publish or update the process context for the given resource. Encodes ``resource`` as a protobuf ``ProcessContext`` message and writes it @@ -16,6 +21,8 @@ def publish_context(resource: Resource) -> None: remains stable across updates. :param resource: The SDK resource whose attributes are to be published. + :param attributes: Optional supplementary attributes to share with external + readers. :raises OSError: If the memory mapping or clock could not be initialized. """ diff --git a/opentelemetry-process-context/tests/scripts/publish_and_wait.py b/opentelemetry-process-context/tests/scripts/publish_and_wait.py index b158e10aa01..e6cf49da18d 100644 --- a/opentelemetry-process-context/tests/scripts/publish_and_wait.py +++ b/opentelemetry-process-context/tests/scripts/publish_and_wait.py @@ -7,7 +7,7 @@ from opentelemetry.sdk.resources import Resource resource = Resource({"service.name": "otel-test-service", "version": 42}) -publish_context(resource) +publish_context(resource, {"deployment.environment": "otel-test-env"}) sys.stdout.write("ready\n") sys.stdout.flush() diff --git a/opentelemetry-process-context/tests/test_process_context.py b/opentelemetry-process-context/tests/test_process_context.py index e79f76f1d63..f0e962d5af1 100644 --- a/opentelemetry-process-context/tests/test_process_context.py +++ b/opentelemetry-process-context/tests/test_process_context.py @@ -75,6 +75,16 @@ def test_publish_context_lifecycle(self): self.assertIsNone(unpublish_context()) self.assertIsNone(publish_context(resource)) + def test_publish_context_with_attributes(self): + resource = Resource({"service.name": "test"}) + self.assertIsNone( + publish_context(resource, {"deployment.environment": "prod"}) + ) + self.assertIsNone( + publish_context(resource, {"k": 1, "nested": {"a": 2}}) + ) + self.assertIsNone(publish_context(resource)) + def test_unpublish_before_publish_raises(self): with self.assertRaises(RuntimeError): unpublish_context() @@ -164,6 +174,8 @@ def test_cross_process_memory_region(self): self.assertNotEqual(header["payload_ptr"], 0) self.assertIn(b"service.name", header["payload"]) self.assertIn(b"otel-test-service", header["payload"]) + self.assertIn(b"deployment.environment", header["payload"]) + self.assertIn(b"otel-test-env", header["payload"]) @unittest.skipUnless( sys.platform.startswith("linux"), "requires /proc//{maps,mem}" From 62096662a51eb38fc42c211cff8482e9247749e0 Mon Sep 17 00:00:00 2001 From: Lukas Hering Date: Sun, 28 Jun 2026 19:53:41 -0500 Subject: [PATCH 35/35] update type annotations --- .../src/opentelemetry/process_context/_rs/__init__.pyi | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/opentelemetry-process-context/src/opentelemetry/process_context/_rs/__init__.pyi b/opentelemetry-process-context/src/opentelemetry/process_context/_rs/__init__.pyi index f79458ef270..cc9563685e8 100644 --- a/opentelemetry-process-context/src/opentelemetry/process_context/_rs/__init__.pyi +++ b/opentelemetry-process-context/src/opentelemetry/process_context/_rs/__init__.pyi @@ -1,14 +1,10 @@ # Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 -from collections.abc import Mapping -from typing import Any - from opentelemetry.sdk.resources import Resource +from opentelemetry.util.types import Attributes -def publish_context( - resource: Resource, attributes: Mapping[str, Any] | None = None -) -> None: +def publish_context(resource: Resource, attributes: Attributes = None) -> None: """Publish or update the process context for the given resource. Encodes ``resource`` as a protobuf ``ProcessContext`` message and writes it