From 5866989c2ffb9621297482c606fb6c852c276264 Mon Sep 17 00:00:00 2001 From: Eric Hulburd Date: Wed, 29 Nov 2023 13:31:54 -0800 Subject: [PATCH 1/7] feat: support tracing subscriber --- .github/workflows/checks.yml | 9 +- .github/workflows/prerelease.yml | 6 +- .github/workflows/publish.yml | 40 +- .github/workflows/release.yml | 9 +- .gitignore | 2 + Cargo.toml | 12 + Makefile.toml | 15 +- README.md | 67 +-- .../CHANGELOG.md | 0 .../Cargo.toml | 4 +- .../src/lib.rs | 0 crates/{macros => opentelemetry}/CHANGELOG.md | 0 crates/{lib => opentelemetry}/Cargo.toml | 27 +- crates/opentelemetry/README.md | 42 ++ crates/{lib => opentelemetry}/src/lib.rs | 10 +- crates/tracing-subscriber/.gitignore | 72 +++ crates/tracing-subscriber/CHANGELOG.md | 0 crates/tracing-subscriber/Cargo.toml | 45 ++ crates/tracing-subscriber/README.md | 95 ++++ .../assets/python_stubs/__init__.py | 17 + .../assets/python_stubs/__init__.pyi | 125 +++++ .../assets/python_stubs/layers/__init__.py | 18 + .../assets/python_stubs/layers/__init__.pyi | 25 + .../python_stubs/layers/file/__init__.py | 19 + .../python_stubs/layers/file/__init__.pyi | 41 ++ .../python_stubs/layers/otel_otlp/__init__.py | 18 + .../layers/otel_otlp/__init__.pyi | 113 +++++ .../layers/otel_otlp_file/__init__.py | 18 + .../layers/otel_otlp_file/__init__.pyi | 35 ++ .../python_stubs/subscriber/__init__.py | 18 + .../python_stubs/subscriber/__init__.pyi | 22 + .../tracing-subscriber/src/contextmanager.rs | 428 ++++++++++++++++ .../src/export_process/background.rs | 80 +++ .../src/export_process/mod.rs | 196 ++++++++ .../tracing-subscriber/src/layers/fmt_file.rs | 136 ++++++ crates/tracing-subscriber/src/layers/mod.rs | 212 ++++++++ .../src/layers/otel_otlp.rs | 456 ++++++++++++++++++ .../src/layers/otel_otlp_file.rs | 86 ++++ crates/tracing-subscriber/src/lib.rs | 214 ++++++++ crates/tracing-subscriber/src/stubs.rs | 203 ++++++++ crates/tracing-subscriber/src/subscriber.rs | 226 +++++++++ examples/pyo3-opentelemetry-lib/.gitignore | 1 + examples/pyo3-opentelemetry-lib/Cargo.toml | 18 +- examples/pyo3-opentelemetry-lib/Makefile.toml | 21 +- examples/pyo3-opentelemetry-lib/README.md | 8 + examples/pyo3-opentelemetry-lib/build.rs | 7 + examples/pyo3-opentelemetry-lib/poetry.lock | 141 +++++- .../pyo3_opentelemetry_lib/__init__.py | 6 +- .../_tracing_subscriber/__init__.py | 17 + .../_tracing_subscriber/__init__.pyi | 125 +++++ .../_tracing_subscriber/layers/__init__.py | 18 + .../_tracing_subscriber/layers/__init__.pyi | 25 + .../layers/file/__init__.py | 19 + .../layers/file/__init__.pyi | 41 ++ .../layers/otel_otlp/__init__.py | 18 + .../layers/otel_otlp/__init__.pyi | 113 +++++ .../layers/otel_otlp_file/__init__.py | 18 + .../layers/otel_otlp_file/__init__.pyi | 35 ++ .../subscriber/__init__.py | 18 + .../subscriber/__init__.pyi | 22 + .../test/__artifacts__/.keep | 0 .../pyo3_opentelemetry_lib/test/conftest.py | 228 ++++++++- .../test/tracing_test.py | 375 ++++++++++++++ .../pyo3-opentelemetry-lib/pyproject.toml | 12 + examples/pyo3-opentelemetry-lib/pytest.ini | 2 + examples/pyo3-opentelemetry-lib/src/lib.rs | 14 +- knope.toml | 21 +- 67 files changed, 4335 insertions(+), 149 deletions(-) rename crates/{lib => opentelemetry-macros}/CHANGELOG.md (100%) rename crates/{macros => opentelemetry-macros}/Cargo.toml (90%) rename crates/{macros => opentelemetry-macros}/src/lib.rs (100%) rename crates/{macros => opentelemetry}/CHANGELOG.md (100%) rename crates/{lib => opentelemetry}/Cargo.toml (52%) create mode 100644 crates/opentelemetry/README.md rename crates/{lib => opentelemetry}/src/lib.rs (91%) create mode 100644 crates/tracing-subscriber/.gitignore create mode 100644 crates/tracing-subscriber/CHANGELOG.md create mode 100644 crates/tracing-subscriber/Cargo.toml create mode 100644 crates/tracing-subscriber/README.md create mode 100644 crates/tracing-subscriber/assets/python_stubs/__init__.py create mode 100644 crates/tracing-subscriber/assets/python_stubs/__init__.pyi create mode 100644 crates/tracing-subscriber/assets/python_stubs/layers/__init__.py create mode 100644 crates/tracing-subscriber/assets/python_stubs/layers/__init__.pyi create mode 100644 crates/tracing-subscriber/assets/python_stubs/layers/file/__init__.py create mode 100644 crates/tracing-subscriber/assets/python_stubs/layers/file/__init__.pyi create mode 100644 crates/tracing-subscriber/assets/python_stubs/layers/otel_otlp/__init__.py create mode 100644 crates/tracing-subscriber/assets/python_stubs/layers/otel_otlp/__init__.pyi create mode 100644 crates/tracing-subscriber/assets/python_stubs/layers/otel_otlp_file/__init__.py create mode 100644 crates/tracing-subscriber/assets/python_stubs/layers/otel_otlp_file/__init__.pyi create mode 100644 crates/tracing-subscriber/assets/python_stubs/subscriber/__init__.py create mode 100644 crates/tracing-subscriber/assets/python_stubs/subscriber/__init__.pyi create mode 100644 crates/tracing-subscriber/src/contextmanager.rs create mode 100644 crates/tracing-subscriber/src/export_process/background.rs create mode 100644 crates/tracing-subscriber/src/export_process/mod.rs create mode 100644 crates/tracing-subscriber/src/layers/fmt_file.rs create mode 100644 crates/tracing-subscriber/src/layers/mod.rs create mode 100644 crates/tracing-subscriber/src/layers/otel_otlp.rs create mode 100644 crates/tracing-subscriber/src/layers/otel_otlp_file.rs create mode 100644 crates/tracing-subscriber/src/lib.rs create mode 100644 crates/tracing-subscriber/src/stubs.rs create mode 100644 crates/tracing-subscriber/src/subscriber.rs create mode 100644 examples/pyo3-opentelemetry-lib/build.rs create mode 100644 examples/pyo3-opentelemetry-lib/pyo3_opentelemetry_lib/_tracing_subscriber/__init__.py create mode 100644 examples/pyo3-opentelemetry-lib/pyo3_opentelemetry_lib/_tracing_subscriber/__init__.pyi create mode 100644 examples/pyo3-opentelemetry-lib/pyo3_opentelemetry_lib/_tracing_subscriber/layers/__init__.py create mode 100644 examples/pyo3-opentelemetry-lib/pyo3_opentelemetry_lib/_tracing_subscriber/layers/__init__.pyi create mode 100644 examples/pyo3-opentelemetry-lib/pyo3_opentelemetry_lib/_tracing_subscriber/layers/file/__init__.py create mode 100644 examples/pyo3-opentelemetry-lib/pyo3_opentelemetry_lib/_tracing_subscriber/layers/file/__init__.pyi create mode 100644 examples/pyo3-opentelemetry-lib/pyo3_opentelemetry_lib/_tracing_subscriber/layers/otel_otlp/__init__.py create mode 100644 examples/pyo3-opentelemetry-lib/pyo3_opentelemetry_lib/_tracing_subscriber/layers/otel_otlp/__init__.pyi create mode 100644 examples/pyo3-opentelemetry-lib/pyo3_opentelemetry_lib/_tracing_subscriber/layers/otel_otlp_file/__init__.py create mode 100644 examples/pyo3-opentelemetry-lib/pyo3_opentelemetry_lib/_tracing_subscriber/layers/otel_otlp_file/__init__.pyi create mode 100644 examples/pyo3-opentelemetry-lib/pyo3_opentelemetry_lib/_tracing_subscriber/subscriber/__init__.py create mode 100644 examples/pyo3-opentelemetry-lib/pyo3_opentelemetry_lib/_tracing_subscriber/subscriber/__init__.pyi create mode 100644 examples/pyo3-opentelemetry-lib/pyo3_opentelemetry_lib/test/__artifacts__/.keep create mode 100644 examples/pyo3-opentelemetry-lib/pyo3_opentelemetry_lib/test/tracing_test.py create mode 100644 examples/pyo3-opentelemetry-lib/pytest.ini diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 61abcf7..0eb6b7b 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -40,7 +40,7 @@ jobs: uses: actions-rs/cargo@v1 with: command: make - args: --makefile Makefile.toml ci-flow + args: --makefile Makefile.toml check-all # Setup Python and run Python example - name: Setup python @@ -50,8 +50,7 @@ jobs: - name: Install poetry uses: snok/install-poetry@v1 - name: Run Python Example - uses: actions-rs/cargo@v1 - with: - command: make - args: --makefile Makefile.toml example-lib-python + run: | + cd ./examples/pyo3-opentelemetry-lib + cargo make --makefile Makefile.toml python-check-all diff --git a/.github/workflows/prerelease.yml b/.github/workflows/prerelease.yml index 64c6f90..b056a75 100644 --- a/.github/workflows/prerelease.yml +++ b/.github/workflows/prerelease.yml @@ -13,10 +13,10 @@ jobs: with: fetch-depth: 0 token: ${{ secrets.PAT }} - - uses: actions-rs/toolchain@v1 + - name: Install Knope + uses: knope-dev/action@v2.0.0 with: - toolchain: stable - - run: cargo install knope --version 0.13.2 + version: 0.13.2 - run: | git config --global user.name "${{ github.triggering_actor }}" git config --global user.email "${{ github.triggering_actor}}@users.noreply.github.com" diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 8a4ae70..b6071dc 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -6,9 +6,30 @@ on: - '**' jobs: - release-macros: + opentelemetry: runs-on: ubuntu-latest - if: startsWith(github.ref, 'refs/tags/macros') + if: startsWith(github.ref, 'refs/tags/opentelemetry') + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + token: ${{ secrets.PAT }} + - uses: actions-rs/toolchain@v1 + with: + toolchain: stable + override: true + - run: | + sudo apt update -y && sudo apt install curl -y + sudo curl -fsSL https://github.com/mikefarah/yq/releases/download/v4.35.1/yq_linux_amd64 -o /usr/bin/yq + sudo chmod +x /usr/bin/yq + name: Install yq + # Below we give some time to make sure that the macros crate is published before the lib crate + # in the case that the new lib crate depends on the new, yet to be published macros crate. + - run: timeout 15m bash -c 'until ./scripts/ci/assert-macros-crate-published.sh; do sleep 10; done' + - run: cargo publish -p pyo3-opentelemetry --token ${{ secrets.CRATES_IO_TOKEN }} + opentelemetry-macros: + runs-on: ubuntu-latest + if: startsWith(github.ref, 'refs/tags/opentelemetry-macros') steps: - uses: actions/checkout@v3 with: @@ -19,9 +40,9 @@ jobs: toolchain: stable override: true - run: cargo publish -p pyo3-opentelemetry-macros --token ${{ secrets.CRATES_IO_TOKEN }} - release-lib: + tracing-subscriber: runs-on: ubuntu-latest - if: startsWith(github.ref, 'refs/tags/lib') + if: startsWith(github.ref, 'refs/tags/tracing-subscriber') steps: - uses: actions/checkout@v3 with: @@ -31,12 +52,5 @@ jobs: with: toolchain: stable override: true - - run: | - sudo apt update -y && sudo apt install curl -y - sudo curl -fsSL https://github.com/mikefarah/yq/releases/download/v4.35.1/yq_linux_amd64 -o /usr/bin/yq - sudo chmod +x /usr/bin/yq - name: Install yq - # Below we give some time to make sure that the macros crate is published before the lib crate - # in the case that the new lib crate depends on the new, yet to be published macros crate. - - run: timeout 15m bash -c 'until ./scripts/ci/assert-macros-crate-published.sh; do sleep 10; done' - - run: cargo publish -p pyo3-opentelemetry --token ${{ secrets.CRATES_IO_TOKEN }} + - run: cargo publish -p pyo3-tracing-subscriber --token ${{ secrets.CRATES_IO_TOKEN }} + diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8d550f0..96db5dd 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,7 +1,6 @@ name: Release -on: - workflow_dispatch: {} +on: workflow_dispatch jobs: release: @@ -15,10 +14,10 @@ jobs: with: fetch-depth: 0 token: ${{ secrets.PAT }} - - uses: actions-rs/toolchain@v1 + - name: Install Knope + uses: knope-dev/action@v2.0.0 with: - toolchain: stable - - run: cargo install knope --version 0.13.2 + version: 0.13.2 - run: | git config --global user.name "${{ github.triggering_actor }}" git config --global user.email "${{ github.triggering_actor}}@users.noreply.github.com" diff --git a/.gitignore b/.gitignore index 25d3005..88cfd57 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,5 @@ scratch/ # ignore to support developer-specific configuration bacon.toml + +.DS_Store diff --git a/Cargo.toml b/Cargo.toml index e856a2e..da1c072 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,3 +4,15 @@ members = ["crates/*", "examples/*"] resolver = "2" [workspace.dependencies] +thiserror = "1.0.49" +serde = { version = "1.0.188", features = ["derive"] } +opentelemetry = { version = "0.20.0" } +opentelemetry_api = { version = "0.20.0" } +opentelemetry_sdk = { version = "0.20.0" } +pyo3 = { version = "0.19.0", features = ["macros"], default-features = false } +rstest = "0.17.0" +tokio = { version = "1.27.0", features = [] } +tracing = "0.1.37" +tracing-opentelemetry = "0.21.0" +tracing-subscriber = "0.3.17" + diff --git a/Makefile.toml b/Makefile.toml index 95293ec..4b2c2b7 100644 --- a/Makefile.toml +++ b/Makefile.toml @@ -2,6 +2,7 @@ # We exclude --no-default-features because pyo3 will not be able to link to python. CARGO_HACK_COMMON_FLAGS = "--feature-powerset --optional-deps --exclude-no-default-features" CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true + CARGO_MAKE_WORKSPACE_SKIP_MEMBERS = "examples/*" [tasks.clean] clear = true @@ -193,25 +194,15 @@ command = "cargo" args = ["outdated"] -[tasks.test] - clear = true - dependencies = ["install-cargo-hack"] - env = { CARGO_MAKE_WORKSPACE_SKIP_MEMBERS = "examples/*"} - script = "cargo hack test $CARGO_HACK_COMMON_FLAGS" - -[tasks.test-nextest] +[tasks.nextest] dependencies = ["install-cargo-hack", "install-cargo-nextest"] script = "cargo hack nextest run $CARGO_HACK_COMMON_FLAGS" -[tasks.example-lib-python] - cwd = "${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/examples/pyo3-opentelemetry-lib" - script = "cargo make python-check-all" - [tasks.examples] dependencies = ["example-lib-python"] [tasks.check-all] - dependencies = ["check", "clippy", "deny", "deadlinks", "msrv-verify", "examples", "test"] + dependencies = ["check", "clippy", "deny", "deadlinks", "msrv-verify", "nextest"] [tasks.pre-ci-flow] dependencies = ["check", "clippy", "deny", "deadlinks", "msrv-verify"] diff --git a/README.md b/README.md index e5cda0a..6133c8b 100644 --- a/README.md +++ b/README.md @@ -1,61 +1,15 @@ -# PyO3 OpenTelemetry +# PyO3 Tracing Crates -## Background +This repository contains four crates to support the ability of upstream `pyo3` extension modules to support the ability of their dependents to gather tracing telemetry from the instrumented Rust source code: -### What this is +* [crates/opentelemetry](./crates/opentelemetry): propagates OpenTelemetry context from Python into Rust. +* [crates/opentelemetry-macros](./crates/opentelemetry-macros): defines proc macros for `pyo3-opentelemetry`. +* [crates/tracing-subscriber](./crates/tracing-subscriber): supports configuration and initialization of Rust tracing subscribers from Python. -pyo3_opentelemetry provides a macro to simply and easily instrument your PyO3 bindings so that OpenTelemetry contexts can be easily passed from a Python caller into a Rust library. The `#[pypropagate]` macro instruments your Rust functions for you so that the global Python OpenTelemetry context is shared across the FFI boundary. - -### What this is not - -* This (currently) does not support propagating an OpenTelemetry context from Rust into Python. -* This does not "magically" instrument Rust code. Without the `#[pypropagate]` attribute, Rust code is unaffected and will not attach the Python OpenTelemetry context. -* This does not facilitate the processing or collection of OpenTelemetry spans; you still need to initialize and flush tracing providers and subscribers separately in Python and Rust. For more information, please see the respective OpenTelemetry documentation for [Python](https://opentelemetry.io/docs/instrumentation/python/) and [Rust](https://opentelemetry.io/docs/instrumentation/rust/). - - -### What this is - -This repo contains utilities for automatically passing OpenTelemetry contexts from Python into Rust. - -### What this is not - -* This does not facilitate the processing of spans. You still need to separately instrument OpenTelemetry processors on both the Rust and Python side. -* This does not allow you to propagate context into _any_ Rust code from Python. It requires instrumentation of the underlying Rust source code. -* While this repository could extend to pass OpenTelemetry contexts _from Rust into Python_, it currently does not. - -## Usage - -From Rust: - -```rs -use pyo3_opentelemetry::prelude::*; -use pyo3::prelude::*; -use tracing::instrument; - -#[pypropagate] -#[pyfunction] -#[instrument] -fn my_function() { - println!("span \"my_function\" is active and will share the Python OpenTelemetry context"); -} - -#[pymodule] -fn my_module(_py: Python, m: &PyModule) -> PyResult<()> { - m.add_function(wrap_pyfunction!(my_function, m)?)?; - Ok(()) -} -``` - -These features require no Python code changes, however, [opentelemetry-api](https://pypi.org/project/opentelemetry-api/) must be installed. +For a functional example of usage of all of these crates, see [examples/pyo3-opentelemetry-lib](./examples/pyo3-opentelemetry-lib). ## Development -| Directory | Purpose | -|-----------|---------| -| crates/macros | Rust macro definitions | -| crates/lib | Supporting Rust functions that get OTel context from Python. | -| examples/pyo3-opentelemetry-lib | maturin PyO3 project with Python test assertions on Rust OTel spans | - ### Rust It should be sufficient to [install the Rust toolchain](https://rustup.rs/) and [cargo-make](https://github.com/sagiegurari/cargo-make). Then: @@ -71,12 +25,3 @@ Install: * Python - installation through [pyenv](https://github.com/pyenv/pyenv) is recommended. * [Poetry](https://python-poetry.org/docs/#installation) - for installing plain Python dependencies. -#### Python Example - -[examples/pyo3-opentelemetry-lib](./examples/pyo3-opentelemetry-lib/) contains a full end-to-end pyo3 project with pytests. To build and run them: - -```sh -cd examples/pyo3-opentelemetry-lib -cargo make python-check-all -``` - diff --git a/crates/lib/CHANGELOG.md b/crates/opentelemetry-macros/CHANGELOG.md similarity index 100% rename from crates/lib/CHANGELOG.md rename to crates/opentelemetry-macros/CHANGELOG.md diff --git a/crates/macros/Cargo.toml b/crates/opentelemetry-macros/Cargo.toml similarity index 90% rename from crates/macros/Cargo.toml rename to crates/opentelemetry-macros/Cargo.toml index 4cd34c5..859875f 100644 --- a/crates/macros/Cargo.toml +++ b/crates/opentelemetry-macros/Cargo.toml @@ -5,7 +5,7 @@ edition = "2021" categories = ["Python bindings", "OpenTelemetry", "Tracing", "Macros"] keywords = ["python", "pyo3", "opentelemetry", "tracing"] license = "MIT OR Apache-2.0" -readme = "../../README.md" +readme = "../opentelemetry/README.md" description = "Macro for passing OpenTelemetry context from Python to Rust" repository = "https://github.com/rigetti/pyo3-opentelemetry" rust-version = "1.65.0" @@ -21,4 +21,4 @@ quote = "1.0.26" syn = { version = "2.0.14", features = ["full", "derive"] } [dev-dependencies] -rstest = "0.17.0" +rstest = { workspace = true } diff --git a/crates/macros/src/lib.rs b/crates/opentelemetry-macros/src/lib.rs similarity index 100% rename from crates/macros/src/lib.rs rename to crates/opentelemetry-macros/src/lib.rs diff --git a/crates/macros/CHANGELOG.md b/crates/opentelemetry/CHANGELOG.md similarity index 100% rename from crates/macros/CHANGELOG.md rename to crates/opentelemetry/CHANGELOG.md diff --git a/crates/lib/Cargo.toml b/crates/opentelemetry/Cargo.toml similarity index 52% rename from crates/lib/Cargo.toml rename to crates/opentelemetry/Cargo.toml index ea87800..1d4d7f7 100644 --- a/crates/lib/Cargo.toml +++ b/crates/opentelemetry/Cargo.toml @@ -5,7 +5,7 @@ edition = "2021" categories = ["Python bindings", "OpenTelemetry", "Tracing", "Macros"] keywords = ["python", "pyo3", "opentelemetry", "tracing"] license = "Apache-2.0" -readme = "../../README.md" +readme = "./README.md" description = "Macro and utilities for passing OpenTelemetry context from Python to Rust" repository = "https://github.com/rigetti/pyo3-opentelemetry" rust-version = "1.65.0" @@ -13,24 +13,23 @@ rust-version = "1.65.0" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [lib] name = "pyo3_opentelemetry" -crate-type = ["lib", "cdylib"] +crate-type = ["lib"] [dependencies] -opentelemetry = "0.18.0" -opentelemetry_api = "0.18.0" -opentelemetry_sdk = "0.18.0" -pyo3 = { version = "0.18.0", features = [] } -pyo3-opentelemetry-macros = { path = "../macros", version = "0.2.0" } +opentelemetry = { workspace = true } +opentelemetry_api = { workspace = true } +opentelemetry_sdk = { workspace = true } +pyo3 = { workspace = true } +pyo3-opentelemetry-macros = { path = "../opentelemetry-macros", version = "0.2.0" } [dev-dependencies] futures-util = "0.3.28" once_cell = "1.17.1" -opentelemetry = { version = "0.18.0", features = ["trace", "rt-tokio"] } -tokio = { version = "1.27.0", features = ["sync", "parking_lot", "macros"] } -tracing = "0.1.37" -tracing-opentelemetry = "0.18.0" -tracing-subscriber = "0.3.16" +opentelemetry = { workspace = true, features = ["trace", "rt-tokio"] } +tokio = { workspace = true, features = ["sync", "parking_lot", "macros"] } +tracing = { workspace = true } +tracing-opentelemetry = { workspace = true } +tracing-subscriber = { workspace = true } [features] -extension-module = ["pyo3/extension-module"] -default = ["extension-module"] + diff --git a/crates/opentelemetry/README.md b/crates/opentelemetry/README.md new file mode 100644 index 0000000..ba8d9e5 --- /dev/null +++ b/crates/opentelemetry/README.md @@ -0,0 +1,42 @@ +# PyO3 OpenTelemetry + +## Background + +### What this is + +pyo3_opentelemetry provides a macro to simply and easily instrument your PyO3 bindings so that OpenTelemetry contexts can be easily passed from a Python caller into a Rust library. The `#[pypropagate]` macro instruments your Rust functions for you so that the global Python OpenTelemetry context is shared across the FFI boundary. + +### What this is not + +* This (currently) does not support propagating an OpenTelemetry context from Rust into Python. +* This does not "magically" instrument Rust code. Without the `#[pypropagate]` attribute, Rust code is unaffected and will not attach the Python OpenTelemetry context. +* This does not facilitate the processing or collection of OpenTelemetry spans; you still need to initialize and flush tracing providers and subscribers separately in Python and Rust. For more information, please see the respective OpenTelemetry documentation for [Python](https://opentelemetry.io/docs/instrumentation/python/) and [Rust](https://opentelemetry.io/docs/instrumentation/rust/). + +## Usage + +> For a complete functioning example, see the `examples/pyo3-opentelemetry-lib/src/lib.rs` example within this crate's repository. + +From Rust: + +```rs +use pyo3_opentelemetry::prelude::*; +use pyo3::prelude::*; +use tracing::instrument; + +#[pypropagate] +#[pyfunction] +#[instrument] +fn my_function() { + println!("span \"my_function\" is active and will share the Python OpenTelemetry context"); +} + +#[pymodule] +fn my_module(_py: Python, m: &PyModule) -> PyResult<()> { + m.add_function(wrap_pyfunction!(my_function, m)?)?; + Ok(()) +} +``` + +These features require no Python code changes, however, [opentelemetry-api](https://pypi.org/project/opentelemetry-api/) must be installed. + + diff --git a/crates/lib/src/lib.rs b/crates/opentelemetry/src/lib.rs similarity index 91% rename from crates/lib/src/lib.rs rename to crates/opentelemetry/src/lib.rs index 50da4d1..cf362ac 100644 --- a/crates/lib/src/lib.rs +++ b/crates/opentelemetry/src/lib.rs @@ -52,7 +52,6 @@ unused_import_braces, unused_lifetimes, unused_parens, - unused_qualifications, variant_size_differences, while_true )] @@ -65,6 +64,12 @@ //! * All functionality here requires the calling Python code to have [opentelemetry-api](https://pypi.org/project/opentelemetry-api/) to be installed. //! * See `pypropagate` for additional requirements and limitations. //! +//! # Related Crates +//! +//! * `pyo3-opentelemetry-macros` - a crate defining the `pypropagate` macro. +//! * `pyo3-tracing-subscriber` - a crate supporting configuration and initialization of Rust +//! tracing subscribers from Python. +//! //! # Examples //! //! ``` @@ -85,6 +90,9 @@ //! Ok(()) //! } //! ``` +//! +//! For a more comprehensive example, see the `pyo3-opentelemetry-lib` example in this repository. +//! Specifically, see the `pyo3-opentelemetry-lib/src/lib.rs` for the Rust code and `pyo3-opentelemetry-lib/pyo3_opentelemetry_lib/tests/test_tracing.py` for the Python code and behavioural assertions. use std::collections::HashMap; use pyo3::{prelude::*, types::IntoPyDict}; diff --git a/crates/tracing-subscriber/.gitignore b/crates/tracing-subscriber/.gitignore new file mode 100644 index 0000000..af3ca5e --- /dev/null +++ b/crates/tracing-subscriber/.gitignore @@ -0,0 +1,72 @@ +/target + +# Byte-compiled / optimized / DLL files +__pycache__/ +.pytest_cache/ +*.py[cod] + +# C extensions +*.so + +# Distribution / packaging +.Python +.venv/ +env/ +bin/ +build/ +develop-eggs/ +dist/ +eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +include/ +man/ +venv/ +*.egg-info/ +.installed.cfg +*.egg + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt +pip-selfcheck.json + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.cache +nosetests.xml +coverage.xml + +# Translations +*.mo + +# Mr Developer +.mr.developer.cfg +.project +.pydevproject + +# Rope +.ropeproject + +# Django stuff: +*.log +*.pot + +.DS_Store + +# Sphinx documentation +docs/_build/ + +# PyCharm +.idea/ + +# VSCode +.vscode/ + +# Pyenv +.python-version \ No newline at end of file diff --git a/crates/tracing-subscriber/CHANGELOG.md b/crates/tracing-subscriber/CHANGELOG.md new file mode 100644 index 0000000..e69de29 diff --git a/crates/tracing-subscriber/Cargo.toml b/crates/tracing-subscriber/Cargo.toml new file mode 100644 index 0000000..08ae7bf --- /dev/null +++ b/crates/tracing-subscriber/Cargo.toml @@ -0,0 +1,45 @@ +[package] +name = "pyo3-tracing-subscriber" +version = "0.0.1" +edition = "2021" +categories = ["Python bindings", "OpenTelemetry", "Tracing"] +keywords = ["python", "pyo3", "opentelemetry", "tracing"] +license = "Apache-2.0" +readme = "./README.md" +description = "A Python module for configuring and initializing tracing subscribers from Python." +repository = "https://github.com/rigetti/pyo3-opentelemetry" +rust-version = "1.67.0" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[lib] +name = "pyo3_tracing_subscriber" +crate-type = ["lib"] + +[dependencies] +handlebars = { version = "4.4.0", optional = true } +pyo3 = { workspace = true } +opentelemetry = { workspace = true, features = ["rt-tokio"] } +opentelemetry-otlp = { version = "0.13.0", optional = true } +opentelemetry-proto = { version = "0.3.0", optional = true, features = ["tonic"] } +opentelemetry-stdout = { version = "0.1.0", optional = true, features = ["trace"] } +opentelemetry_api = { workspace = true } +opentelemetry_sdk = { workspace = true, features = ["rt-tokio-current-thread"] } +pyo3-asyncio = { version = "0.19.0", features = ["tokio-runtime"] } +rigetti-pyo3 = { version = "0.2.0" } +serde = { workspace = true } +thiserror = { workspace = true } +tokio = { workspace = true, features = ["rt-multi-thread"] } +tonic = { version = "0.9.2", optional = true } +tracing = { workspace = true } +tracing-opentelemetry = { workspace = true } +tracing-subscriber = { workspace = true, features = ["env-filter", "fmt", "json"] } + +[features] +layer-otel-otlp = ["dep:opentelemetry-otlp", "dep:opentelemetry-proto", "dep:tonic"] +layer-otel-otlp-file = ["dep:opentelemetry-stdout"] +stubs = ["dep:handlebars"] + +[dev-dependencies] +rstest = { workspace = true } +serde_json = "1.0.107" +tempfile = "3.8.0" diff --git a/crates/tracing-subscriber/README.md b/crates/tracing-subscriber/README.md new file mode 100644 index 0000000..a6d09ff --- /dev/null +++ b/crates/tracing-subscriber/README.md @@ -0,0 +1,95 @@ +# PyO3 Tracing Subscriber + +## Background + +### What this is + +`pyo3_tracing_subscriber` provides a `PyModule` that can be added to upstream `pyo3` extension modules in order to support the configuration and initialization of Rust tracing subscribers from Python. + +### What this is not + +* Any initialized tracing subscriber imported from your upstream package will _not_ collect traces from any other `pyo3` extension module. In other words, any `pyo3` extension module will need to separately export tracing configuration and context managers, which in turn must be separately initialized in order to capture Rust traces from respective `pyo3` extension modules. +* Currently, only three tracing subcriber layers are supported: + * `tracing_subscriber::fmt` which writes traces to file (or stdout) in a human readable format. + * `opentelemetry-stdout` which writes traces to file (or stdout) in OTLP format. Available only with the `layer-otel-otlp-file` feature. + * `opentelemetry-otlp` which sends traces to an OpenTelemetry OTLP endpoint. Available only with the `layer-otel-otlp` feature. +* This does not propagate OpenTelemetry contexts from Python into Rust (or vice versa). Use the `pyo3-opentelemetry` crate for that feature. + +## Usage + +> For a complete functioning example, see the `examples/pyo3-opentelemetry-lib/src/lib.rs` example within this crate's repository. + +Given a `pyo3` extension module named "my_module" that would like to expose the tracing subscriber configuration and context manager classes from "my_module._tracing_subscriber", from Rust: + +```rs +use pyo3::prelude::*; + +#[pymodule] +fn my_module(py: Python, m: &PyModule) -> PyResult<()> { + // Add your own Python classes, functions and modules. + + let tracing_subscriber = PyModule::new(py, "_tracing_subscriber")?; + pyo3_tracing_subscriber::add_submodule( + "my_module._tracing_subscriber", + py, + tracing_subscriber, + )?; + m.add_submodule(tracing_subscriber)?; + Ok(()) +} +``` + +Then a user could initialize a tracing subscriber that logged to stdout from Python: + +```py +import my_module +from my_module._tracing_subscriber import ( + GlobalTracingConfig, + SimpleConfig, + Tracing, + subscriber, +) +from pyo3_opentelemetry_lib._tracing_subscriber.layers import file + + +def main(): + tracing_configuration = GlobalTracingConfig( + export_process=SimpleConfig( + subscriber=subscriber.Config( + layer=file.Config() + ) + ) + ) + with Tracing(config=config): + result = my_module.example_function() + my_module.other_example_function(result) + +if __name__ == '__main__': + main() +``` + +### Building Python Stub Files + +This crate provides a convenient method for adding stub files to your Python source code with the `stubs` feature. + +Given a `pyo3` extension module named "my_module" that uses the `pyo3-tracing-subscriber` crate to expose tracing subscriber configuration and context manager classes from "my_module._tracing_subscriber", in the upstream `build.rs` file: + +```rs +use pyo3_tracing_subscriber_stubs::write_stub_files; + +fn main() { + let target_dir = std::path::Path::new("./my_module/_tracing_subscriber"); + std::fs::remove_dir_all(target_dir).unwrap(); + write_stub_files( + "my_module", + "_tracing_subscriber", + target_dir, + true, // layer_otel_otlp_file feature enabled + true, // layer_otel_otlp feature enabled + ) + .unwrap(); +} +``` + + + diff --git a/crates/tracing-subscriber/assets/python_stubs/__init__.py b/crates/tracing-subscriber/assets/python_stubs/__init__.py new file mode 100644 index 0000000..eb6b95f --- /dev/null +++ b/crates/tracing-subscriber/assets/python_stubs/__init__.py @@ -0,0 +1,17 @@ +# ***************************************************************************** +# * AUTO-GENERATED CODE * +# * * +# * This code was generated by the `pyo3-tracing-subscriber` crate. Any * +# * modifications to this file should be made to the script or the generation * +# * process that produced this code. Specifically, see: * +# * `pyo3_tracing_subscriber::stubs::write_stub_files` * +# * * +# * Do not manually edit this file, as your changes may be overwritten the * +# * next time the code is generated. * +# ***************************************************************************** + +from {{ host_package }} import {{ tracing_subscriber_module_name }} + + +__doc__ = {{ tracing_subscriber_module_name }}.__doc__ +__all__ = getattr({{ tracing_subscriber_module_name }}, "__all__", []) diff --git a/crates/tracing-subscriber/assets/python_stubs/__init__.pyi b/crates/tracing-subscriber/assets/python_stubs/__init__.pyi new file mode 100644 index 0000000..4bfe7ba --- /dev/null +++ b/crates/tracing-subscriber/assets/python_stubs/__init__.pyi @@ -0,0 +1,125 @@ +# ***************************************************************************** +# * AUTO-GENERATED CODE * +# * * +# * This code was generated by the `pyo3-tracing-subscriber` crate. Any * +# * modifications to this file should be made to the script or the generation * +# * process that produced this code. Specifically, see: * +# * `pyo3_tracing_subscriber::stubs::write_stub_files` * +# * * +# * Do not manually edit this file, as your changes may be overwritten the * +# * next time the code is generated. * +# ***************************************************************************** + +from types import TracebackType +from typing import Optional, Type, Union +from . import subscriber as subscriber +from . import layers as layers + + +class TracingContextManagerError(RuntimeError): + """ + Raised if the initialization, enter, and exit of the tracing context manager was + invoked in an invalid order. + """ + ... + + +class TracingStartError(RuntimeError): + """ + Raised if the tracing subscriber configuration is invalid or if a background export task + fails to start. + """ + ... + + +class TracingShutdownError(RuntimeError): + """ + Raised if the tracing subscriber fails to shutdown cleanly. + """ + ... + + +class BatchConfig: + """ + Configuration for exporting spans in batch. This will require a background task to be spawned + and run for the duration of the tracing context manager. + + This configuration is typically favorable unless the tracing context manager is short lived. + """ + def __init__(self, *, subscriber: subscriber.Config): + ... + + +class SimpleConfig: + """ + Configuration for exporting spans in a simple manner. This does not spawn a background task + unless it is required by the configured export layer. Generally favor `BatchConfig` instead, + unless the tracing context manager is short lived. + + Note, some export layers still spawn a background task even when `SimpleConfig` is used. + This is the case for the OTLP export layer, which makes gRPC export requests within the + background Tokio runtime. + """ + def __init__(self, *, subscriber: subscriber.Config): + ... + + +ExportConfig = Union[BatchConfig, SimpleConfig] +""" +One of `BatchConfig` or `SimpleConfig`. +""" + + +class CurrentThreadTracingConfig: + """ + This tracing configuration will export spans emitted only on the current thread. A `Tracing` context + manager may be initialized multiple times for the same process with this configuration (although + they should not be nested). + + Note, this configuration is currently incompatible with async methods defined with `pyo3_asyncio`. + """ + def __init__(self, *, export_process: ExportConfig): + ... + + +class GlobalTracingConfig: + """ + This tracing configuration will export spans emitted on any thread in the current process. Because + it sets a tracing subscriber at the global level, it can only be initialized once per process. + + This is typically favorable, as it only requires a single initialization across your entire Python + application. + """ + def __init__(self, *, export_process: ExportConfig): + ... + + +TracingConfig = Union[CurrentThreadTracingConfig, GlobalTracingConfig] +""" +One of `CurrentThreadTracingConfig` or `GlobalTracingConfig`. +""" + + +class Tracing: + """ + A context manager that initializes a tracing subscriber and exports spans + emitted from within the parent Rust-Python package. It may be used synchonously + or asynchronously. + + Each instance of this context manager can only be used once and only once. + """ + def __init__(self, *, config: TracingConfig): + ... + + def __enter__(self): + ... + + def __exit__(self, exc_type: Optional[Type[BaseException]], exc_value: Optional[BaseException], traceback: Optional[TracebackType]): + ... + + async def __aenter__(self): + ... + + async def __aexit__(self, exc_type: Optional[Type[BaseException]], exc_value: Optional[BaseException], traceback: Optional[TracebackType]): + ... + diff --git a/crates/tracing-subscriber/assets/python_stubs/layers/__init__.py b/crates/tracing-subscriber/assets/python_stubs/layers/__init__.py new file mode 100644 index 0000000..e1957c9 --- /dev/null +++ b/crates/tracing-subscriber/assets/python_stubs/layers/__init__.py @@ -0,0 +1,18 @@ +# ***************************************************************************** +# * AUTO-GENERATED CODE * +# * * +# * This code was generated by the `pyo3-tracing-subscriber` crate. Any * +# * modifications to this file should be made to the script or the generation * +# * process that produced this code. Specifically, see: * +# * `pyo3_tracing_subscriber::stubs::write_stub_files` * +# * * +# * Do not manually edit this file, as your changes may be overwritten the * +# * next time the code is generated. * +# ***************************************************************************** + +from {{ host_package }}.{{ tracing_subscriber_module_name }} import layers + + +__doc__ = layers.__doc__ +__all__ = getattr(layers, "__all__", []) + diff --git a/crates/tracing-subscriber/assets/python_stubs/layers/__init__.pyi b/crates/tracing-subscriber/assets/python_stubs/layers/__init__.pyi new file mode 100644 index 0000000..a3d6d89 --- /dev/null +++ b/crates/tracing-subscriber/assets/python_stubs/layers/__init__.pyi @@ -0,0 +1,25 @@ +# ***************************************************************************** +# * AUTO-GENERATED CODE * +# * * +# * This code was generated by the `pyo3-tracing-subscriber` crate. Any * +# * modifications to this file should be made to the script or the generation * +# * process that produced this code. Specifically, see: * +# * `pyo3_tracing_subscriber::stubs::write_stub_files` * +# * * +# * Do not manually edit this file, as your changes may be overwritten the * +# * next time the code is generated. * +# ***************************************************************************** + +from typing import Union +from .file import Config as FileConfig +{{#if layer_otel_otlp_file }}from .otel_otlp_file import Config as OtlpFileConfig{{/if}} +{{#if layer_otel_otlp}}from .otel_otlp import Config as OtlpConfig{{/if}} + +{{#if any_additional_layer}}Config = Union[ + FileConfig, + {{#if layer_otel_otlp_file }}OtlpFileConfig,{{/if}} + {{#if layer_otel_otlp }}OtlpConfig,{{/if}} +]{{else}}Config = FileConfig{{/if}} +""" +One of the supported layer configurations that may be set on the subscriber configuration. +""" diff --git a/crates/tracing-subscriber/assets/python_stubs/layers/file/__init__.py b/crates/tracing-subscriber/assets/python_stubs/layers/file/__init__.py new file mode 100644 index 0000000..76b68c1 --- /dev/null +++ b/crates/tracing-subscriber/assets/python_stubs/layers/file/__init__.py @@ -0,0 +1,19 @@ +# ***************************************************************************** +# * AUTO-GENERATED CODE * +# * * +# * This code was generated by the `pyo3-tracing-subscriber` crate. Any * +# * modifications to this file should be made to the script or the generation * +# * process that produced this code. Specifically, see: * +# * `pyo3_tracing_subscriber::stubs::write_stub_files` * +# * * +# * Do not manually edit this file, as your changes may be overwritten the * +# * next time the code is generated. * +# ***************************************************************************** + +from {{ host_package }}.{{ tracing_subscriber_module_name }}.layers import file + + +__doc__ = file.__doc__ +__all__ = getattr(file, "__all__", []) + + diff --git a/crates/tracing-subscriber/assets/python_stubs/layers/file/__init__.pyi b/crates/tracing-subscriber/assets/python_stubs/layers/file/__init__.pyi new file mode 100644 index 0000000..d1a4ff8 --- /dev/null +++ b/crates/tracing-subscriber/assets/python_stubs/layers/file/__init__.pyi @@ -0,0 +1,41 @@ +# ***************************************************************************** +# * AUTO-GENERATED CODE * +# * * +# * This code was generated by the `pyo3-tracing-subscriber` crate. Any * +# * modifications to this file should be made to the script or the generation * +# * process that produced this code. Specifically, see: * +# * `pyo3_tracing_subscriber::stubs::write_stub_files` * +# * * +# * Do not manually edit this file, as your changes may be overwritten the * +# * next time the code is generated. * +# ***************************************************************************** + +from typing import Optional + + +class Config: + """ + Configuration for a + `tracing_subscriber::fmt::Layer `_. + """ + + def __init__(self, *, file_path: Optional[str] = None, pretty: bool = False, filter: Optional[str] = None, json: bool = True) -> None: + """ + Create a new `Config`. + + :param file_path: The path to the file to write to. If `None`, defaults to `stdout`. + :param pretty: Whether or not to pretty-print the output. Defaults to `False`. + :param filter: A filter string to use for this layer. This uses the same format as the + `tracing_subscriber::filter::EnvFilter + `_. + In summary, each directive takes the form `target[span{field=value}]=level`, where `target` + is roughly the Rust namespace and _only_ `level` is required. + + If not specified, this will first check the `PYO3_TRACING_SUBSCRIBER_ENV_FILTER` environment + variable and then `RUST_LOG` environment variable. If all of these values are empty, no spans + will be exported. + :param json: Whether or not to format the output as JSON. Defaults to `True`. + """ + ... + + diff --git a/crates/tracing-subscriber/assets/python_stubs/layers/otel_otlp/__init__.py b/crates/tracing-subscriber/assets/python_stubs/layers/otel_otlp/__init__.py new file mode 100644 index 0000000..ff3f769 --- /dev/null +++ b/crates/tracing-subscriber/assets/python_stubs/layers/otel_otlp/__init__.py @@ -0,0 +1,18 @@ +# ***************************************************************************** +# * AUTO-GENERATED CODE * +# * * +# * This code was generated by the `pyo3-tracing-subscriber` crate. Any * +# * modifications to this file should be made to the script or the generation * +# * process that produced this code. Specifically, see: * +# * `pyo3_tracing_subscriber::stubs::write_stub_files` * +# * * +# * Do not manually edit this file, as your changes may be overwritten the * +# * next time the code is generated. * +# ***************************************************************************** + +from {{ host_package }}.{{ tracing_subscriber_module_name }}.layers import otel_otlp + + +__doc__ = otel_otlp.__doc__ +__all__ = getattr(otel_otlp, "__all__", []) + diff --git a/crates/tracing-subscriber/assets/python_stubs/layers/otel_otlp/__init__.pyi b/crates/tracing-subscriber/assets/python_stubs/layers/otel_otlp/__init__.pyi new file mode 100644 index 0000000..61c3603 --- /dev/null +++ b/crates/tracing-subscriber/assets/python_stubs/layers/otel_otlp/__init__.pyi @@ -0,0 +1,113 @@ +# ***************************************************************************** +# * AUTO-GENERATED CODE * +# * * +# * This code was generated by the `pyo3-tracing-subscriber` crate. Any * +# * modifications to this file should be made to the script or the generation * +# * process that produced this code. Specifically, see: * +# * `pyo3_tracing_subscriber::stubs::write_stub_files` * +# * * +# * Do not manually edit this file, as your changes may be overwritten the * +# * next time the code is generated. * +# ***************************************************************************** + +from typing import Dict, List, Optional, Union + + +class SpanLimits: + def __init__( + self, + *, + max_events_per_span: Optional[int] = None, + max_attributes_per_span: Optional[int] = None, + max_links_per_span: Optional[int] = None, + max_attributes_per_event: Optional[int] = None, + max_attributes_per_link: Optional[int] = None, + ) -> None: ... + """ + + :param max_events_per_span: The max events that can be added to a `Span`. + :param max_attributes_per_span: The max attributes that can be added to a `Span`. + :param max_links_per_span: The max links that can be added to a `Span`. + :param max_attributes_per_event: The max attributes that can be added to an `Event`. + :param max_attributes_per_link: The max attributes that can be added to a `Link`. + """ + + +ResourceValueArray = Union[List[bool], List[int], List[float], List[str]] +""" +An array of `ResourceValue`s. This array is homogenous, so all values must be of the same type. +""" + +ResourceValue = Union[bool, int, float, str, ResourceValueArray] +""" +A value that can be added to a `Resource`. +""" + + +class Resource: + """ + A `Resource` is a representation of the entity producing telemetry. This should represent the Python + process starting the tracing subscription process. + """ + def __init__( + self, + *, + attrs: Optional[Dict[str, ResourceValue]] = None, + schema_url: Optional[str] = None, + ) -> None: ... + + +Sampler = Union[bool, float] +""" +A `Sampler` is a representation of the sampling strategy to use. If this is a `bool`, it will +either sample all traces (`True`) or none of them (`False`). If this is a `float`, it will sample +traces at the given rate. +""" + + +class Config: + """ + A configuration for `opentelemetry-otlp `_ + layer. In addition to the values specified at initialization, this configuration will also respect the + canonical `OpenTelemetry OTLP environment variables + `_ that are `supported by opentelemetry-otlp + `_. + """ + + def __init__( + self, + *, + span_limits: Optional[SpanLimits] = None, + resource: Optional[Resource] = None, + metadata_map: Optional[Dict[str, str]] = None, + sampler: Optional[Sampler] = None, + endpoint: Optional[str] = None, + timeout_millis: Optional[int] = None, + pre_shutdown_timeout_millis: Optional[int] = 2000, + filter: Optional[str] = None, + ) -> None: + """ + Initializes a new `Config`. + + :param span_limits: The limits to apply to span exports. + :param resource: The OpenTelemetry resource to attach to all exported spans. + :param metadata_map: A map of metadata to attach to all exported spans. This is a map of key value pairs + that may be set as gRPC metadata by the tonic library. + :param sampler: The sampling strategy to use. See documentation for `Sampler` for more information. + :param endpoint: The endpoint to export to. This should be a valid URL. If not specified, this should be + specified by environment variables (see `Config` documentation). + :param timeout_millis: The timeout for each request, in milliseconds. If not specified, this should be + specified by environment variables (see `Config` documentation). + :param pre_shutdown_timeout_millis: The timeout to wait before shutting down the OTLP exporter in milliseconds. + This timeout is necessary to ensure all traces from `tracing_subscriber` to make it to the OpenTelemetry + layer, which may be effectively force flushed. It is enforced on the `Tracing` context manager exit. + :param filter: A filter string to use for this layer. This uses the same format as the + `tracing_subscriber::filter::EnvFilter + `_. + In summary, each directive takes the form `target[span{field=value}]=level`, where `target` is roughly the + Rust namespace and _only_ `level` is required. + + If not specified, this will first check the `PYO3_TRACING_SUBSCRIBER_ENV_FILTER` environment variable + and then `RUST_LOG` environment variable. If all of these values are empty, no spans will be exported. + """ + ... diff --git a/crates/tracing-subscriber/assets/python_stubs/layers/otel_otlp_file/__init__.py b/crates/tracing-subscriber/assets/python_stubs/layers/otel_otlp_file/__init__.py new file mode 100644 index 0000000..b0c54eb --- /dev/null +++ b/crates/tracing-subscriber/assets/python_stubs/layers/otel_otlp_file/__init__.py @@ -0,0 +1,18 @@ +# ***************************************************************************** +# * AUTO-GENERATED CODE * +# * * +# * This code was generated by the `pyo3-tracing-subscriber` crate. Any * +# * modifications to this file should be made to the script or the generation * +# * process that produced this code. Specifically, see: * +# * `pyo3_tracing_subscriber::stubs::write_stub_files` * +# * * +# * Do not manually edit this file, as your changes may be overwritten the * +# * next time the code is generated. * +# ***************************************************************************** + +from {{ host_package }}.{{ tracing_subscriber_module_name }}.layers import otel_otlp_file + + +__doc__ = otel_otlp_file.__doc__ +__all__ = getattr(otel_otlp_file, "__all__", []) + diff --git a/crates/tracing-subscriber/assets/python_stubs/layers/otel_otlp_file/__init__.pyi b/crates/tracing-subscriber/assets/python_stubs/layers/otel_otlp_file/__init__.pyi new file mode 100644 index 0000000..45330c4 --- /dev/null +++ b/crates/tracing-subscriber/assets/python_stubs/layers/otel_otlp_file/__init__.pyi @@ -0,0 +1,35 @@ +# ***************************************************************************** +# * AUTO-GENERATED CODE * +# * * +# * This code was generated by the `pyo3-tracing-subscriber` crate. Any * +# * modifications to this file should be made to the script or the generation * +# * process that produced this code. Specifically, see: * +# * `pyo3_tracing_subscriber::stubs::write_stub_files` * +# * * +# * Do not manually edit this file, as your changes may be overwritten the * +# * next time the code is generated. * +# ***************************************************************************** + +from typing import Optional + + +class Config: + """ + A configuration for `opentelemetry-stdout `_ + layer. + """ + + def __init__(self, *, file_path: Optional[str] = None, filter: Optional[str] = None) -> None: + """ + :param file_path: The path to the file to write to. If not specified, defaults to stdout. + :param filter: A filter string to use for this layer. This uses the same format as the + `tracing_subscriber::filter::EnvFilter + `_. + In summary, each directive takes the form `target[span{field=value}]=level`, where `target` is + roughly the Rust namespace and _only_ `level` is required. + + If not specified, this will first check the `PYO3_TRACING_SUBSCRIBER_ENV_FILTER` environment variable + and then `RUST_LOG` environment variable. If all of these values are empty, no spans will be exported. + """ + ... + diff --git a/crates/tracing-subscriber/assets/python_stubs/subscriber/__init__.py b/crates/tracing-subscriber/assets/python_stubs/subscriber/__init__.py new file mode 100644 index 0000000..65f4e6c --- /dev/null +++ b/crates/tracing-subscriber/assets/python_stubs/subscriber/__init__.py @@ -0,0 +1,18 @@ +# ***************************************************************************** +# * AUTO-GENERATED CODE * +# * * +# * This code was generated by the `pyo3-tracing-subscriber` crate. Any * +# * modifications to this file should be made to the script or the generation * +# * process that produced this code. Specifically, see: * +# * `pyo3_tracing_subscriber::stubs::write_stub_files` * +# * * +# * Do not manually edit this file, as your changes may be overwritten the * +# * next time the code is generated. * +# ***************************************************************************** + +from {{ host_package }}.{{ tracing_subscriber_module_name }} import subscriber + + +__doc__ = subscriber.__doc__ +__all__ = getattr(subscriber, "__all__", []) + diff --git a/crates/tracing-subscriber/assets/python_stubs/subscriber/__init__.pyi b/crates/tracing-subscriber/assets/python_stubs/subscriber/__init__.pyi new file mode 100644 index 0000000..323cb15 --- /dev/null +++ b/crates/tracing-subscriber/assets/python_stubs/subscriber/__init__.pyi @@ -0,0 +1,22 @@ +# ***************************************************************************** +# * AUTO-GENERATED CODE * +# * * +# * This code was generated by the `pyo3-tracing-subscriber` crate. Any * +# * modifications to this file should be made to the script or the generation * +# * process that produced this code. Specifically, see: * +# * `pyo3_tracing_subscriber::stubs::write_stub_files` * +# * * +# * Do not manually edit this file, as your changes may be overwritten the * +# * next time the code is generated. * +# ***************************************************************************** + +from .. import layers + + +class Config: + """ + Configuration for the tracing subscriber. Currently, this only requires a single layer to be + set on the `tracing_subscriber::Registry`. + """ + def __init__(self, *, layer: layers.Config): + ... diff --git a/crates/tracing-subscriber/src/contextmanager.rs b/crates/tracing-subscriber/src/contextmanager.rs new file mode 100644 index 0000000..0cab1f0 --- /dev/null +++ b/crates/tracing-subscriber/src/contextmanager.rs @@ -0,0 +1,428 @@ +// Copyright 2023 Rigetti Computing +// +// 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. +use pyo3::prelude::*; + +use rigetti_pyo3::{py_wrap_error, wrap_error, ToPythonError}; + +use super::export_process::{ + ExportProcess, ExportProcessConfig, RustTracingShutdownError, RustTracingStartError, +}; + +#[pyclass] +#[derive(Clone, Debug, Default)] +pub(crate) struct GlobalTracingConfig { + pub(crate) export_process: ExportProcessConfig, +} + +#[pymethods] +impl GlobalTracingConfig { + #[new] + #[pyo3(signature = (/, export_process = None))] + #[allow(clippy::pedantic)] + fn new(export_process: Option) -> PyResult { + let export_process = export_process.unwrap_or_default(); + Ok(Self { export_process }) + } +} + +#[pyclass] +#[derive(Clone, Debug)] +pub(crate) struct CurrentThreadTracingConfig { + pub(crate) export_process: ExportProcessConfig, +} + +#[pymethods] +impl CurrentThreadTracingConfig { + #[new] + #[pyo3(signature = (/, export_process = None))] + #[allow(clippy::pedantic)] + fn new(export_process: Option) -> PyResult { + let export_process = export_process.unwrap_or_default(); + Ok(Self { export_process }) + } +} + +#[derive(FromPyObject, Debug)] +pub(crate) enum TracingConfig { + Global(GlobalTracingConfig), + CurrentThread(CurrentThreadTracingConfig), +} + +impl Default for TracingConfig { + fn default() -> Self { + Self::Global(GlobalTracingConfig::default()) + } +} + +/// Represents the current state of the context manager. This state is used to ensure the +/// context manager methods are invoked in the correct order and multiplicity. +#[derive(Debug)] +enum ContextManagerState { + Initialized(TracingConfig), + Entered(ExportProcess), + Starting, + Exited, +} + +/// A Python class that implements the context manager interface. It is initialized with a +/// configuration. Upon entry it builds and installs the configured tracing subscriber. Upon exit +/// it shuts down the tracing subscriber. +#[pyclass] +#[derive(Debug)] +pub struct Tracing { + state: ContextManagerState, +} + +#[derive(thiserror::Error, Debug)] +enum ContextManagerError { + #[error("entered tracing context manager with no configuration defined; ensure contextmanager only enters once")] + EnterWithoutConfiguration, + #[error("exited tracing context manager with no export process defined; ensure contextmanager only exits once after being entered")] + ExitWithoutExportProcess, +} + +wrap_error!(RustContextManagerError(ContextManagerError)); +py_wrap_error!( + contextmanager, + RustContextManagerError, + TracingContextManagerError, + pyo3::exceptions::PyRuntimeError +); + +#[pymethods] +impl Tracing { + #[new] + #[pyo3(signature = (/, config = None))] + #[allow(clippy::pedantic)] + fn new(config: Option) -> PyResult { + let config = config.unwrap_or_default(); + Ok(Self { + state: ContextManagerState::Initialized(config), + }) + } + + fn __enter__(&mut self) -> PyResult<()> { + let state = std::mem::replace(&mut self.state, ContextManagerState::Starting); + if let ContextManagerState::Initialized(config) = state { + self.state = ContextManagerState::Entered( + ExportProcess::start(config) + .map_err(RustTracingStartError::from) + .map_err(ToPythonError::to_py_err)?, + ); + } else { + return Err(ContextManagerError::EnterWithoutConfiguration) + .map_err(RustContextManagerError::from) + .map_err(ToPythonError::to_py_err)?; + } + Ok(()) + } + + fn __aenter__<'a>(&'a mut self, py: Python<'a>) -> PyResult<&'a PyAny> { + self.__enter__()?; + pyo3_asyncio::tokio::future_into_py(py, async { Ok(()) }) + } + + fn __exit__( + &mut self, + _exc_type: Option<&PyAny>, + _exc_value: Option<&PyAny>, + _traceback: Option<&PyAny>, + ) -> PyResult<()> { + let state = std::mem::replace(&mut self.state, ContextManagerState::Exited); + if let ContextManagerState::Entered(export_process) = state { + let py_rt = pyo3_asyncio::tokio::get_runtime(); + // Why block and not run this in a future within aexit? The `shutdown` + // method returns a Tokio runtime, which cannot be dropped within another + // runtime. Additionally, `pyo3_asyncio::tokio::future_into_py` futures + // must resolve to something that implements `IntoPy`. + let export_runtime = py_rt.block_on(async move { + export_process + .shutdown() + .await + .map_err(RustTracingShutdownError::from) + .map_err(ToPythonError::to_py_err) + })?; + if let Some(export_runtime) = export_runtime { + // This immediately shuts the runtime down. The expectation here is that the + // process shutdown is responsible for cleaning up all background tasks and + // shutting down gracefully. + export_runtime.shutdown_background(); + } + } else { + return Err(ContextManagerError::ExitWithoutExportProcess) + .map_err(RustContextManagerError::from) + .map_err(ToPythonError::to_py_err)?; + } + + Ok(()) + } + + fn __aexit__<'a>( + &'a mut self, + py: Python<'a>, + exc_type: Option<&PyAny>, + exc_value: Option<&PyAny>, + traceback: Option<&PyAny>, + ) -> PyResult<&'a PyAny> { + self.__exit__(exc_type, exc_value, traceback)?; + pyo3_asyncio::tokio::future_into_py(py, async { Ok(()) }) + } +} + +#[cfg(feature = "layer-otel-otlp-file")] +#[cfg(test)] +mod test { + use std::{io::BufRead, thread::sleep, time::Duration}; + + use tokio::runtime::Builder; + + use crate::{ + contextmanager::{CurrentThreadTracingConfig, GlobalTracingConfig, TracingConfig}, + export_process::{BatchConfig, ExportProcess, ExportProcessConfig, SimpleConfig}, + subscriber::TracingSubscriberRegistryConfig, + }; + + #[tracing::instrument] + fn example() { + sleep(SPAN_DURATION); + } + + const N_SPANS: usize = 5; + const SPAN_DURATION: Duration = Duration::from_millis(100); + + /// A truncated implementation of `opentelemetry_stdout` that derives + /// `serde::Deserialize`. + #[derive(serde::Deserialize)] + #[serde(rename_all = "camelCase")] + struct SpanData { + resource_spans: Vec, + } + + #[derive(serde::Deserialize)] + #[serde(rename_all = "camelCase")] + struct ResourceSpan { + scope_spans: Vec, + } + + #[derive(serde::Deserialize)] + struct ScopeSpan { + spans: Vec, + } + + #[derive(serde::Deserialize, Clone)] + #[serde(rename_all = "camelCase")] + struct Span { + name: String, + start_time_unix_nano: u128, + end_time_unix_nano: u128, + } + + #[test] + /// Test that a global batch process can be started and stopped and that it exports + /// accurate spans as configured. + fn test_global_batch() { + let temporary_file = tempfile::NamedTempFile::new().unwrap(); + let temporary_file_path = temporary_file.path().to_owned(); + let layer_config = Box::new(crate::layers::otel_otlp_file::Config { + file_path: Some(temporary_file_path.as_os_str().to_str().unwrap().to_owned()), + filter: Some("error,pyo3_tracing_subscriber=info".to_string()), + }); + let subscriber = Box::new(TracingSubscriberRegistryConfig { layer_config }); + let config = TracingConfig::Global(GlobalTracingConfig { + export_process: ExportProcessConfig::Batch(BatchConfig { + subscriber: crate::subscriber::PyConfig { + subscriber_config: subscriber, + }, + }), + }); + let export_process = ExportProcess::start(config).unwrap(); + let rt2 = Builder::new_current_thread().enable_time().build().unwrap(); + let _guard = rt2.enter(); + let export_runtime = rt2 + .block_on(tokio::time::timeout(Duration::from_secs(1), async move { + for _ in 0..N_SPANS { + example(); + } + export_process.shutdown().await + })) + .unwrap() + .unwrap() + .unwrap(); + drop(export_runtime); + + let reader = std::io::BufReader::new(std::fs::File::open(temporary_file_path).unwrap()); + let lines = reader.lines(); + let spans = lines + .flat_map(|line| { + let line = line.unwrap(); + let span_data: SpanData = serde_json::from_str(line.as_str()).unwrap(); + span_data + .resource_spans + .iter() + .flat_map(|resource_span| { + resource_span + .scope_spans + .iter() + .flat_map(|scope_span| scope_span.spans.clone()) + }) + .collect::>() + }) + .collect::>(); + assert_eq!(spans.len(), N_SPANS); + + let span_grace = Duration::from_millis(200); + for span in spans { + assert_eq!(span.name, "example"); + assert!( + span.end_time_unix_nano - span.start_time_unix_nano >= SPAN_DURATION.as_nanos() + ); + assert!( + (span.end_time_unix_nano - span.start_time_unix_nano) + <= (SPAN_DURATION.as_nanos() + span_grace.as_nanos()) + ); + } + } + + #[test] + /// Test that a global simple export process can be started and stopped and that it + /// exports accurate spans as configured. + fn test_global_simple() { + let temporary_file = tempfile::NamedTempFile::new().unwrap(); + let temporary_file_path = temporary_file.path().to_owned(); + let layer_config = Box::new(crate::layers::otel_otlp_file::Config { + file_path: Some(temporary_file_path.as_os_str().to_str().unwrap().to_owned()), + filter: Some("error,pyo3_tracing_subscriber=info".to_string()), + }); + let subscriber = Box::new(TracingSubscriberRegistryConfig { layer_config }); + let config = TracingConfig::Global(GlobalTracingConfig { + export_process: ExportProcessConfig::Simple(SimpleConfig { + subscriber: crate::subscriber::PyConfig { + subscriber_config: subscriber, + }, + }), + }); + let export_process = ExportProcess::start(config).unwrap(); + let rt2 = Builder::new_current_thread().enable_time().build().unwrap(); + let _guard = rt2.enter(); + let runtime = rt2 + .block_on(tokio::time::timeout(Duration::from_secs(1), async move { + for _ in 0..N_SPANS { + example(); + } + export_process.shutdown().await + })) + .unwrap() + .unwrap(); + assert!(runtime.is_none()); + + let reader = std::io::BufReader::new(std::fs::File::open(temporary_file_path).unwrap()); + let lines = reader.lines(); + let spans = lines + .flat_map(|line| { + let line = line.unwrap(); + let span_data: SpanData = serde_json::from_str(line.as_str()).unwrap(); + span_data + .resource_spans + .iter() + .flat_map(|resource_span| { + resource_span + .scope_spans + .iter() + .flat_map(|scope_span| scope_span.spans.clone()) + }) + .collect::>() + }) + .collect::>(); + assert_eq!(spans.len(), N_SPANS); + + let span_grace = Duration::from_millis(10); + for span in spans { + assert_eq!(span.name, "example"); + assert!( + span.end_time_unix_nano - span.start_time_unix_nano >= SPAN_DURATION.as_nanos() + ); + assert!( + (span.end_time_unix_nano - span.start_time_unix_nano) + <= (SPAN_DURATION.as_nanos() + span_grace.as_nanos()) + ); + } + } + + #[test] + /// Test that a current thread simple export process can be started and stopped and that it + /// exports accurate spans as configured. + fn test_current_thread_simple() { + let temporary_file = tempfile::NamedTempFile::new().unwrap(); + let temporary_file_path = temporary_file.path().to_owned(); + let layer_config = Box::new(crate::layers::otel_otlp_file::Config { + file_path: Some(temporary_file_path.as_os_str().to_str().unwrap().to_owned()), + filter: Some("error,pyo3_tracing_subscriber=info".to_string()), + }); + let subscriber = Box::new(TracingSubscriberRegistryConfig { layer_config }); + let config = TracingConfig::CurrentThread(CurrentThreadTracingConfig { + export_process: crate::export_process::ExportProcessConfig::Simple(SimpleConfig { + subscriber: crate::subscriber::PyConfig { + subscriber_config: subscriber, + }, + }), + }); + let export_process = ExportProcess::start(config).unwrap(); + + for _ in 0..N_SPANS { + example(); + } + + let rt2 = Builder::new_current_thread().enable_time().build().unwrap(); + let _guard = rt2.enter(); + let runtime = rt2 + .block_on(tokio::time::timeout(Duration::from_secs(1), async move { + export_process.shutdown().await + })) + .unwrap() + .unwrap(); + assert!(runtime.is_none()); + + let reader = std::io::BufReader::new(std::fs::File::open(temporary_file_path).unwrap()); + let lines = reader.lines(); + let spans = lines + .flat_map(|line| { + let line = line.unwrap(); + let span_data: SpanData = serde_json::from_str(line.as_str()).unwrap(); + span_data + .resource_spans + .iter() + .flat_map(|resource_span| { + resource_span + .scope_spans + .iter() + .flat_map(|scope_span| scope_span.spans.clone()) + }) + .collect::>() + }) + .collect::>(); + assert_eq!(spans.len(), N_SPANS); + + let span_grace = Duration::from_millis(50); + for span in spans { + assert_eq!(span.name, "example"); + assert!( + span.end_time_unix_nano - span.start_time_unix_nano >= SPAN_DURATION.as_nanos() + ); + assert!( + (span.end_time_unix_nano - span.start_time_unix_nano) + <= (SPAN_DURATION.as_nanos() + span_grace.as_nanos()) + ); + } + } +} diff --git a/crates/tracing-subscriber/src/export_process/background.rs b/crates/tracing-subscriber/src/export_process/background.rs new file mode 100644 index 0000000..3ca8273 --- /dev/null +++ b/crates/tracing-subscriber/src/export_process/background.rs @@ -0,0 +1,80 @@ +// Copyright 2023 Rigetti Computing +// +// 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. + +use tokio::runtime::{Builder, Runtime}; + +use crate::subscriber::{set_subscriber, Config as SubscriberConfig, SubscriberManagerGuard}; + +use tracing::subscriber::SetGlobalDefaultError; + +use super::{ShutdownResult, StartResult}; + +#[derive(thiserror::Error, Debug)] +#[allow(variant_size_differences)] +pub(crate) enum StartError { + #[error("failed to build subscriber: {0}")] + SubscriberBuild(#[from] crate::subscriber::BuildError), + #[error("failed to set global default tracing subscriber: {0}")] + SetSubscriber(#[from] SetGlobalDefaultError), + #[error("exporter initialization timed out: {0}")] + ExportInitializationTimeout(#[from] tokio::time::error::Elapsed), + #[error("failed to receive export initialization signal: {0}")] + ExportInitializationRecv(#[from] tokio::sync::oneshot::error::RecvError), + #[error("failed to initialize export background tokio runtime: {0}")] + RuntimeInitialization(#[from] std::io::Error), +} + +/// Carries the background tokio runtime and the subscriber manager guard. +pub(crate) struct ExportProcess { + runtime: Runtime, + guard: SubscriberManagerGuard, +} + +impl ExportProcess { + fn new(guard: SubscriberManagerGuard, runtime: Runtime) -> Self { + Self { runtime, guard } + } + + /// Starts a background export process. Importantly, this: + /// + /// * Initializes a new tokio runtime, which will be persisted within the returned `Self`. + /// * Builds the tracing subscriber within the context of the new tokio runtime. + /// * Sets the subscriber as configured (globally or thread-local). + /// * Returns `Self` with the subscriber guard and runtime. + pub(super) fn start( + subscriber_config: Box, + global: bool, + ) -> StartResult { + let runtime = init_runtime()?; + let subscriber = runtime + .block_on(async move { subscriber_config.build(true).map_err(StartError::from) })?; + let guard = set_subscriber(subscriber, global)?; + Ok(Self::new(guard, runtime)) + } + + /// Shuts down the background export process. Importantly, this shuts down the guard. + /// Additionally, it _returns_ the tokio runtime. This is important because the runtime + /// may _not_ be dropped from the context of another tokio runtime. + pub(super) async fn shutdown(self) -> ShutdownResult { + self.guard.shutdown().await?; + Ok(self.runtime) + } +} + +fn init_runtime() -> Result { + Builder::new_multi_thread() + .enable_all() + .build() + .map_err(StartError::RuntimeInitialization) +} diff --git a/crates/tracing-subscriber/src/export_process/mod.rs b/crates/tracing-subscriber/src/export_process/mod.rs new file mode 100644 index 0000000..c5f232d --- /dev/null +++ b/crates/tracing-subscriber/src/export_process/mod.rs @@ -0,0 +1,196 @@ +// Copyright 2023 Rigetti Computing +// +// 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. + +use std::fmt::Debug; + +use crate::subscriber::PyConfig; +use pyo3::{exceptions::PyRuntimeError, prelude::*}; +use rigetti_pyo3::{create_init_submodule, py_wrap_error, wrap_error}; +use tokio::runtime::Runtime; + +use super::{ + contextmanager::TracingConfig, + subscriber::{self, set_subscriber, SetSubscriberError, SubscriberManagerGuard}, +}; + +mod background; + +/// Configuration for batch export processes. Batch export processes typically accumulate +/// trace data in memory and export that data in batch. This is favorable in +/// most situations to reduce the amount of I/O required to export trace data. See +/// `opentelemetry_sdk::trace::BatchSpanProcessor` for more details. +#[pyclass] +#[derive(Clone, Debug, Default)] +pub(crate) struct BatchConfig { + pub(super) subscriber: PyConfig, +} + +#[pymethods] +impl BatchConfig { + #[new] + #[pyo3(signature = (subscriber = None))] + #[allow(clippy::pedantic)] + fn new(subscriber: Option) -> PyResult { + let subscriber = subscriber.unwrap_or_default(); + Ok(Self { subscriber }) + } +} + +/// Configuration for simple export processes. A simple export process does not accumulate +/// trace data in memory, but instead exports each trace event as it is received. This may +/// be favorable in situations where the amount of trace data is expected to be small and +/// the overhead of background processing is not worth it. See +/// `opentelemetry_sdk::trace::SimpleSpanProcessor` for more details. +#[pyclass] +#[derive(Clone, Debug, Default)] +pub(crate) struct SimpleConfig { + pub(super) subscriber: PyConfig, +} + +#[pymethods] +impl SimpleConfig { + #[new] + #[pyo3(signature = (subscriber = None))] + #[allow(clippy::pedantic)] + fn new(subscriber: Option) -> PyResult { + let subscriber = subscriber.unwrap_or_default(); + Ok(Self { subscriber }) + } +} + +#[derive(FromPyObject, Clone, Debug)] +pub(crate) enum ExportProcessConfig { + Batch(BatchConfig), + Simple(SimpleConfig), +} + +impl Default for ExportProcessConfig { + fn default() -> Self { + Self::Batch(BatchConfig::default()) + } +} + +#[derive(thiserror::Error, Debug)] +pub(crate) enum StartError { + #[error("failed to start global batch: {0}")] + GlobalBatch(#[from] background::StartError), + #[error("failed to build subscriber {0}")] + BuildSubscriber(#[from] subscriber::BuildError), + #[error("failed to set subscriber: {0}")] + SetSubscriber(#[from] SetSubscriberError), +} + +wrap_error!(RustTracingStartError(StartError)); +py_wrap_error!( + export_process, + RustTracingStartError, + TracingStartError, + PyRuntimeError +); + +type StartResult = Result; + +#[derive(thiserror::Error, Debug)] +pub(crate) enum ShutdownError { + #[error("the subscriber failed to shutdown: {0}")] + Subscriber(#[from] crate::subscriber::ShutdownError), +} + +wrap_error!(RustTracingShutdownError(ShutdownError)); +py_wrap_error!( + export_process, + RustTracingShutdownError, + TracingShutdownError, + PyRuntimeError +); + +type ShutdownResult = Result; + +/// A representation of a running export process, either a background task or a process just +/// running in the current thread. A background task carries both its tokio runtime and the +/// tracing subscriber guard; a foreground task only carries the subscriber guard. +pub(crate) enum ExportProcess { + Background(background::ExportProcess), + Foreground(SubscriberManagerGuard), +} + +impl Debug for ExportProcess { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Background(_) => f + .debug_struct("ExportProcess::Background") + .field("process", &"process") + .finish(), + Self::Foreground(_) => f + .debug_struct("ExportProcess::Foreground") + .field("guard", &"guard") + .finish(), + } + } +} + +impl ExportProcess { + pub(crate) fn start(config: TracingConfig) -> StartResult { + match config { + TracingConfig::Global(config) => match config.export_process { + ExportProcessConfig::Batch(config) => Ok(Self::Background( + background::ExportProcess::start(config.subscriber.subscriber_config, true)?, + )), + ExportProcessConfig::Simple(config) => { + let requires_runtime = config.subscriber.subscriber_config.requires_runtime(); + if requires_runtime { + Ok(Self::Background(background::ExportProcess::start( + config.subscriber.subscriber_config, + true, + )?)) + } else { + let subscriber = config.subscriber.subscriber_config.build(false)?; + Ok(Self::Foreground(set_subscriber(subscriber, true)?)) + } + } + }, + TracingConfig::CurrentThread(config) => match config.export_process { + ExportProcessConfig::Batch(config) => Ok(Self::Background( + background::ExportProcess::start(config.subscriber.subscriber_config, false)?, + )), + ExportProcessConfig::Simple(config) => { + let requires_runtime = config.subscriber.subscriber_config.requires_runtime(); + if requires_runtime { + Ok(Self::Background(background::ExportProcess::start( + config.subscriber.subscriber_config, + false, + )?)) + } else { + let subscriber = config.subscriber.subscriber_config.build(false)?; + Ok(Self::Foreground(set_subscriber(subscriber, false)?)) + } + } + }, + } + } + + pub(crate) async fn shutdown(self) -> ShutdownResult> { + match self { + Self::Background(process) => Ok(Some(process.shutdown().await?)), + Self::Foreground(guard) => { + guard.shutdown().await?; + Ok(None) + } + } + } +} + +create_init_submodule! { + errors: [TracingStartError, TracingShutdownError], +} diff --git a/crates/tracing-subscriber/src/layers/fmt_file.rs b/crates/tracing-subscriber/src/layers/fmt_file.rs new file mode 100644 index 0000000..ad232d1 --- /dev/null +++ b/crates/tracing-subscriber/src/layers/fmt_file.rs @@ -0,0 +1,136 @@ +// Copyright 2023 Rigetti Computing +// +// 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. +use pyo3::prelude::*; +use rigetti_pyo3::create_init_submodule; +use tracing_subscriber::Layer; + +use super::{build_env_filter, LayerBuildResult, ShutdownResult, WithShutdown}; + +/// Configures the [`tracing_subscriber::fmt`] layer. If [`file_path`] is None, the layer +/// will write to stdout. This outputs data in a non-OpenTelemetry format. +#[pyclass] +#[derive(Clone, Debug)] +pub(crate) struct Config { + pub(crate) file_path: Option, + pub(crate) pretty: bool, + pub(crate) filter: Option, + pub(crate) json: bool, +} + +impl Default for Config { + fn default() -> Self { + Self { + file_path: None, + pretty: false, + filter: None, + json: true, + } + } +} + +#[pymethods] +impl Config { + #[new] + #[pyo3(signature = (/, file_path = None, pretty = false, filter = None, json = true))] + const fn new( + file_path: Option, + pretty: bool, + filter: Option, + json: bool, + ) -> Self { + Self { + file_path, + pretty, + filter, + json, + } + } +} + +impl crate::layers::Config for Config { + fn requires_runtime(&self) -> bool { + false + } + + fn build(&self, _batch: bool) -> LayerBuildResult { + let filter = build_env_filter(self.filter.clone())?; + let layer = if let Some(file_path) = self.file_path.as_ref() { + let file = std::fs::File::create(file_path).map_err(BuildError::from)?; + if self.json && self.pretty { + tracing_subscriber::fmt::layer() + .json() + .pretty() + .with_writer(file) + .with_filter(filter) + .boxed() + } else if self.json { + tracing_subscriber::fmt::layer() + .json() + .with_writer(file) + .with_filter(filter) + .boxed() + } else if self.pretty { + tracing_subscriber::fmt::layer() + .pretty() + .with_writer(file) + .with_filter(filter) + .boxed() + } else { + tracing_subscriber::fmt::layer() + .with_writer(file) + .with_filter(filter) + .boxed() + } + } else if self.json && self.pretty { + tracing_subscriber::fmt::layer() + .json() + .pretty() + .with_filter(filter) + .boxed() + } else if self.json { + tracing_subscriber::fmt::layer() + .json() + .with_filter(filter) + .boxed() + } else if self.pretty { + tracing_subscriber::fmt::layer() + .pretty() + .with_filter(filter) + .boxed() + } else { + tracing_subscriber::fmt::layer().with_filter(filter).boxed() + }; + + Ok(WithShutdown { + layer: Box::new(layer), + shutdown: Box::new( + move || -> std::pin::Pin> + Send + Sync>> { + Box::pin(async move { + Ok(()) + }) + }, + ) + }) + } +} + +#[derive(thiserror::Error, Debug)] +pub(crate) enum BuildError { + #[error("failed to initialize fmt layer for specified file path: {0}")] + InvalidFile(#[from] std::io::Error), +} + +create_init_submodule! { + classes: [ Config ], +} diff --git a/crates/tracing-subscriber/src/layers/mod.rs b/crates/tracing-subscriber/src/layers/mod.rs new file mode 100644 index 0000000..502de60 --- /dev/null +++ b/crates/tracing-subscriber/src/layers/mod.rs @@ -0,0 +1,212 @@ +// Copyright 2023 Rigetti Computing +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! This module contains a limited set of tracing layers which can be used to configure the +//! [`tracing_subscriber::Registry`] for use with the [`crate::Tracing`] context manager. +//! +//! Currently, the following layers are supported: +//! +//! * [`crate::layers::fmt_file::Config`] - a layer which writes spans to a file (or stdout) in +//! * [`crate::layers::otel_otlp_file::Config`] - a layer which writes spans to a file (or stdout) in +//! the `OpenTelemetry` OTLP JSON-serialized format. +//! * [`crate::layers::otel_otlp::Config`] - a layer which exports spans to an `OpenTelemetry` collector. +pub(crate) mod fmt_file; +#[cfg(feature = "layer-otel-otlp")] +pub(crate) mod otel_otlp; +#[cfg(feature = "layer-otel-otlp-file")] +pub(crate) mod otel_otlp_file; + +use std::fmt::Debug; + +use pyo3::prelude::*; +use tracing_subscriber::{ + filter::{FromEnvError, ParseError}, + EnvFilter, Layer, Registry, +}; + +pub(super) type Shutdown = Box< + dyn (FnOnce() -> std::pin::Pin< + Box> + Send + Sync>, + >) + Send + + Sync, +>; + +/// Carries the built tracing subscriber layer and a shutdown function that can later be used to +/// shutdown the subscriber upon context manager exit. +pub(crate) struct WithShutdown { + pub(crate) layer: Box + Send + Sync>, + pub(crate) shutdown: Shutdown, +} + +impl Debug for WithShutdown { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "LayerWithShutdown {{ layer: Box + Send + Sync>, shutdown: Shutdown }}") + } +} + +#[derive(thiserror::Error, Debug)] +pub(crate) enum BuildError { + #[cfg(feature = "layer-otel-otlp-file")] + #[error("file layer: {0}")] + File(#[from] otel_otlp_file::BuildError), + #[cfg(feature = "layer-otel-otlp")] + #[error("otlp layer: {0}")] + Otlp(#[from] otel_otlp::BuildError), + #[error("fmt layer: {0}")] + FmtFile(#[from] fmt_file::BuildError), + #[error("failed to parse specified trace filter: {0}")] + TraceFilterParseError(#[from] ParseError), + #[error("failed to parse trace filter from RUST_LOG: {0}")] + TraceFilterEnvError(#[from] FromEnvError), +} + +#[derive(thiserror::Error, Debug)] +#[non_exhaustive] +pub(crate) enum ShutdownError { + // This will eventually accept a `CustomError` that can be set by upstream libraries. + // See https://github.com/rigetti/pyo3-opentelemetry/issues/4 +} + +pub(crate) type ShutdownResult = Result; + +pub(super) type LayerBuildResult = Result; + +pub(crate) trait Config: Send + Sync + BoxDynConfigClone + Debug { + fn build(&self, batch: bool) -> LayerBuildResult; + fn requires_runtime(&self) -> bool; +} + +pub(crate) trait BoxDynConfigClone { + fn clone_box(&self) -> Box; +} + +/// This trait is necessary so that `Box` can be cloned and, therefore, +/// used as an attribute on a `pyo3` class. +impl BoxDynConfigClone for T +where + T: 'static + Config + Clone, +{ + fn clone_box(&self) -> Box { + Box::new(self.clone()) + } +} + +impl Clone for Box { + fn clone(&self) -> Self { + self.clone_box() + } +} + +#[cfg(any(feature = "layer-otel-otlp", feature = "layer-otel-otlp-file"))] +pub(super) fn force_flush_provider_as_shutdown( + provider: opentelemetry_sdk::trace::TracerProvider, + timeout: Option, +) -> Shutdown { + Box::new( + move || -> std::pin::Pin> + Send + Sync>> { + Box::pin(async move { + if let Some(timeout) = timeout { + tokio::time::sleep(timeout).await; + } + provider.force_flush(); + Ok(()) + }) + }, + ) +} + +/// An environment variable that can be used to set an [`EnvFilter`] for the OTLP layer. +/// This supersedes the `RUST_LOG` environment variable, but is superseded by an explicit +/// `env_filter` argument specified on any layer configuration. +const PYO3_OPENTELEMETRY_ENV_FILTER: &str = "PYO3_OPENTELEMETRY_ENV_FILTER"; + +pub(super) fn build_env_filter(env_filter: Option) -> Result { + env_filter + .or_else(|| std::env::var(PYO3_OPENTELEMETRY_ENV_FILTER).ok()) + .or_else(|| std::env::var(EnvFilter::DEFAULT_ENV).ok()) + .map_or_else( + || Ok(EnvFilter::from_default_env()), + |filter| EnvFilter::try_new(filter).map_err(BuildError::from), + ) +} + +/// A Python union of one of the supported layers. +#[derive(FromPyObject, Clone, Debug)] +#[allow(variant_size_differences, clippy::large_enum_variant)] +pub(crate) enum PyConfig { + #[cfg(feature = "layer-otel-otlp-file")] + OtlpFile(otel_otlp_file::Config), + #[cfg(feature = "layer-otel-otlp")] + Otlp(otel_otlp::PyConfig), + File(fmt_file::Config), +} + +impl Default for PyConfig { + fn default() -> Self { + Self::File(fmt_file::Config::default()) + } +} + +impl Config for PyConfig { + fn build(&self, batch: bool) -> LayerBuildResult { + match self { + #[cfg(feature = "layer-otel-otlp-file")] + Self::OtlpFile(config) => config.build(batch), + #[cfg(feature = "layer-otel-otlp")] + Self::Otlp(config) => config.build(batch), + Self::File(config) => config.build(batch), + } + } + + fn requires_runtime(&self) -> bool { + match self { + #[cfg(feature = "layer-otel-otlp-file")] + Self::OtlpFile(config) => config.requires_runtime(), + #[cfg(feature = "layer-otel-otlp")] + Self::Otlp(config) => config.requires_runtime(), + Self::File(config) => config.requires_runtime(), + } + } +} + +/// Adds `layers` submodule to the root level submodule. +#[allow(dead_code)] +pub(crate) fn init_submodule(name: &str, py: Python, m: &PyModule) -> PyResult<()> { + let modules = py.import("sys")?.getattr("modules")?; + + #[cfg(feature = "layer-otel-otlp-file")] + { + let submod = pyo3::types::PyModule::new(py, "otel_otlp_file")?; + let qualified_name = format!("{name}.otel_otlp_file"); + otel_otlp_file::init_submodule(qualified_name.as_str(), py, submod)?; + modules.set_item(qualified_name, submod)?; + m.add_submodule(submod)?; + } + #[cfg(feature = "layer-otel-otlp")] + { + let submod = pyo3::types::PyModule::new(py, "otel_otlp")?; + let qualified_name = format!("{name}.otel_otlp"); + otel_otlp::init_submodule(qualified_name.as_str(), py, submod)?; + modules.set_item(qualified_name, submod)?; + m.add_submodule(submod)?; + } + + let submod = pyo3::types::PyModule::new(py, "file")?; + let qualified_name = format!("{name}.file"); + fmt_file::init_submodule(qualified_name.as_str(), py, submod)?; + modules.set_item(qualified_name, submod)?; + m.add_submodule(submod)?; + + Ok(()) +} diff --git a/crates/tracing-subscriber/src/layers/otel_otlp.rs b/crates/tracing-subscriber/src/layers/otel_otlp.rs new file mode 100644 index 0000000..f82fdb9 --- /dev/null +++ b/crates/tracing-subscriber/src/layers/otel_otlp.rs @@ -0,0 +1,456 @@ +// Copyright 2023 Rigetti Computing +// +// 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. + +use std::{collections::HashMap, time::Duration}; + +use opentelemetry_api::{trace::TraceError, KeyValue}; +use opentelemetry_otlp::{TonicExporterBuilder, WithExportConfig}; +use opentelemetry_sdk::{ + trace::{Sampler, SpanLimits}, + Resource, +}; +use pyo3::prelude::*; + +use opentelemetry_sdk::trace; +use rigetti_pyo3::create_init_submodule; +use tonic::metadata::{ + errors::{InvalidMetadataKey, InvalidMetadataValue}, + MetadataKey, +}; +use tracing_subscriber::{ + filter::{FromEnvError, ParseError}, + Layer, +}; + +use super::{build_env_filter, force_flush_provider_as_shutdown, LayerBuildResult, WithShutdown}; + +/// Configures the [`opentelemetry-otlp`] crate layer. +#[derive(Clone, Debug)] +pub(crate) struct Config { + /// Configuration to limit the amount of trace data collected. + span_limits: SpanLimits, + /// OpenTelemetry resource attributes describing the entity that produced the telemetry. + resource: Resource, + /// The metadata map to use for requests to the remote collector. + metadata_map: Option, + /// The sampler to use for the [`opentelemetry::sdk::trace::TracerProvider`]. + sampler: Sampler, + /// The endpoint to which the exporter will send trace data. If not set, this must be set by + /// OTLP environment variables. + endpoint: Option, + /// Timeout applied the [`tonic::transport::Channel`] used to send trace data to the remote collector. + timeout: Option, + /// A timeout applied to the shutdown of the [`crate::contextmanager::Tracing`] context + /// manager upon exiting, before the underlying [`opentelemetry::sdk::trace::TracerProvider`] + /// is shutdown. Ensures that spans are flushed before the program exits. + pre_shutdown_timeout: Duration, + /// The filter to use for the [`tracing_subscriber::filter::EnvFilter`] layer. + filter: Option, +} + +impl Config { + fn initialize_otlp_exporter(&self) -> TonicExporterBuilder { + let mut otlp_exporter = opentelemetry_otlp::new_exporter().tonic().with_env(); + if let Some(endpoint) = self.endpoint.clone() { + otlp_exporter = otlp_exporter.with_endpoint(endpoint); + } + if let Some(timeout) = self.timeout { + otlp_exporter = otlp_exporter.with_timeout(timeout); + } + if let Some(metadata_map) = self.metadata_map.clone() { + otlp_exporter = otlp_exporter.with_metadata(metadata_map); + } + otlp_exporter + } +} + +impl crate::layers::Config for PyConfig { + fn requires_runtime(&self) -> bool { + Config::requires_runtime() + } + fn build(&self, batch: bool) -> LayerBuildResult { + Config::try_from(self.clone()) + .map_err(BuildError::from)? + .build(batch) + } +} + +impl Config { + const fn requires_runtime() -> bool { + true + } + + fn build(&self, batch: bool) -> LayerBuildResult { + let pipeline = opentelemetry_otlp::new_pipeline() + .tracing() + .with_exporter(self.initialize_otlp_exporter()) + .with_trace_config( + trace::config() + .with_sampler(self.sampler.clone()) + .with_span_limits(self.span_limits) + .with_resource(self.resource.clone()), + ); + + let tracer = if batch { + pipeline.install_batch(opentelemetry::runtime::TokioCurrentThread) + } else { + pipeline.install_simple() + } + .map_err(BuildError::from)?; + let provider = tracer + .provider() + .ok_or(BuildError::ProviderNotSetOnTracer)?; + let env_filter = build_env_filter(self.filter.clone())?; + let layer = tracing_opentelemetry::layer() + .with_tracer(tracer) + .with_filter(env_filter); + Ok(WithShutdown { + layer: Box::new(layer), + shutdown: force_flush_provider_as_shutdown(provider, Some(self.pre_shutdown_timeout)), + }) + } +} + +#[derive(thiserror::Error, Debug)] +pub(super) enum Error { + #[error("error in the configuration: {0}")] + Config(#[from] ConfigError), +} + +#[derive(thiserror::Error, Debug)] +pub(crate) enum BuildError { + #[error("failed to build opentelemetry-otlp pipeline: {0}")] + BatchInstall(#[from] TraceError), + #[error("provider not set on returned opentelemetry-otlp tracer")] + ProviderNotSetOnTracer, + #[error("error in the configuration: {0}")] + Config(#[from] ConfigError), + #[error("failed to parse specified trace filter: {0}")] + TraceFilterParseError(#[from] ParseError), + #[error("failed to parse trace filter from RUST_LOG: {0}")] + TraceFilterEnvError(#[from] FromEnvError), +} + +#[derive(thiserror::Error, Debug)] +pub(crate) enum ConfigError { + #[error("invalid metadata map value: {0}")] + InvalidMetadataValue(#[from] InvalidMetadataValue), + #[error("invalid metadata map key: {0}")] + InvalidMetadataKey(#[from] InvalidMetadataKey), +} + +#[pyclass(name = "SpanLimits")] +#[derive(Clone, Debug)] +struct PySpanLimits { + /// The max events that can be added to a `Span`. + max_events_per_span: u32, + /// The max attributes that can be added to a `Span`. + max_attributes_per_span: u32, + /// The max links that can be added to a `Span`. + max_links_per_span: u32, + /// The max attributes that can be added into an `Event` + max_attributes_per_event: u32, + /// The max attributes that can be added into a `Link` + max_attributes_per_link: u32, +} + +impl Default for PySpanLimits { + fn default() -> Self { + Self::from(SpanLimits::default()) + } +} + +impl From for PySpanLimits { + fn from(span_limits: SpanLimits) -> Self { + Self { + max_events_per_span: span_limits.max_events_per_span, + max_attributes_per_span: span_limits.max_attributes_per_span, + max_links_per_span: span_limits.max_links_per_span, + max_attributes_per_event: span_limits.max_attributes_per_event, + max_attributes_per_link: span_limits.max_attributes_per_link, + } + } +} + +impl From for SpanLimits { + fn from(span_limits: PySpanLimits) -> Self { + Self { + max_events_per_span: span_limits.max_events_per_span, + max_attributes_per_span: span_limits.max_attributes_per_span, + max_links_per_span: span_limits.max_links_per_span, + max_attributes_per_event: span_limits.max_attributes_per_event, + max_attributes_per_link: span_limits.max_attributes_per_link, + } + } +} + +#[pymethods] +impl PySpanLimits { + #[new] + #[pyo3(signature = ( + /, + max_events_per_span = None, + max_attributes_per_span = None, + max_links_per_span = None, + max_attributes_per_event = None, + max_attributes_per_link = None + ))] + fn new( + max_events_per_span: Option, + max_attributes_per_span: Option, + max_links_per_span: Option, + max_attributes_per_event: Option, + max_attributes_per_link: Option, + ) -> Self { + let span_limits = Self::default(); + Self { + max_events_per_span: max_events_per_span.unwrap_or(span_limits.max_events_per_span), + max_attributes_per_span: max_attributes_per_span + .unwrap_or(span_limits.max_attributes_per_span), + max_links_per_span: max_links_per_span.unwrap_or(span_limits.max_links_per_span), + max_attributes_per_event: max_attributes_per_event + .unwrap_or(span_limits.max_attributes_per_event), + max_attributes_per_link: max_attributes_per_link + .unwrap_or(span_limits.max_attributes_per_link), + } + } +} + +/// A Python representation of [`Config`]. +#[pyclass(name = "Config")] +#[derive(Clone, Default, Debug)] +pub(crate) struct PyConfig { + span_limits: PySpanLimits, + resource: PyResource, + metadata_map: Option>, + sampler: PySampler, + endpoint: Option, + timeout_millis: Option, + pre_shutdown_timeout_millis: u64, + filter: Option, +} + +#[pymethods] +impl PyConfig { + #[new] + #[pyo3(signature = ( + /, + span_limits = None, + resource = None, + metadata_map = None, + sampler = None, + endpoint = None, + timeout_millis = None, + pre_shutdown_timeout_millis = 2000, + filter = None + ))] + #[allow(clippy::too_many_arguments)] + fn new( + span_limits: Option, + resource: Option, + metadata_map: Option<&PyAny>, + sampler: Option<&PyAny>, + endpoint: Option<&str>, + timeout_millis: Option, + pre_shutdown_timeout_millis: u64, + filter: Option<&str>, + ) -> PyResult { + Ok(Self { + span_limits: span_limits.unwrap_or_default(), + resource: resource.unwrap_or_default(), + metadata_map: metadata_map.map(PyAny::extract).transpose()?, + sampler: sampler.map(PyAny::extract).transpose()?.unwrap_or_default(), + endpoint: endpoint.map(String::from), + timeout_millis, + pre_shutdown_timeout_millis, + filter: filter.map(String::from), + }) + } +} + +#[pyclass(name = "Resource")] +#[derive(Clone, Default, Debug)] +struct PyResource { + attrs: HashMap, + schema_url: Option, +} + +#[pymethods] +impl PyResource { + #[new] + #[pyo3(signature = (/, attrs = None, schema_url = None))] + fn new(attrs: Option>, schema_url: Option<&str>) -> Self { + Self { + attrs: attrs.unwrap_or_default(), + schema_url: schema_url.map(String::from), + } + } +} + +impl From for Resource { + fn from(resource: PyResource) -> Self { + let kvs = resource + .attrs + .into_iter() + .map(|(k, v)| KeyValue::new(k, v)) + .collect::>(); + match resource.schema_url { + Some(schema_url) => Self::from_schema_url(kvs, schema_url), + None => Self::new(kvs), + } + } +} + +#[derive(FromPyObject, Clone, Debug, PartialEq)] +pub(crate) enum PyResourceValue { + /// bool values + Bool(bool), + /// i64 values + I64(i64), + /// f64 values + F64(f64), + /// String values + String(String), + /// Array of homogeneous values + Array(PyResourceValueArray), +} + +#[derive(FromPyObject, Debug, Clone, PartialEq)] +pub(crate) enum PyResourceValueArray { + /// Array of bools + Bool(Vec), + /// Array of integers + I64(Vec), + /// Array of floats + F64(Vec), + /// Array of strings + String(Vec), +} + +impl From for opentelemetry_api::Array { + fn from(py_resource_value_array: PyResourceValueArray) -> Self { + match py_resource_value_array { + PyResourceValueArray::Bool(b) => Self::Bool(b), + PyResourceValueArray::I64(i) => Self::I64(i), + PyResourceValueArray::F64(f) => Self::F64(f), + PyResourceValueArray::String(s) => { + Self::String(s.iter().map(|v| v.clone().into()).collect()) + } + } + } +} + +impl From for opentelemetry_api::Value { + fn from(py_resource_value: PyResourceValue) -> Self { + match py_resource_value { + PyResourceValue::Bool(b) => Self::Bool(b), + PyResourceValue::I64(i) => Self::I64(i), + PyResourceValue::F64(f) => Self::F64(f), + PyResourceValue::String(s) => Self::String(s.into()), + PyResourceValue::Array(a) => Self::Array(a.into()), + } + } +} + +#[allow(variant_size_differences)] +#[derive(FromPyObject, Debug, Clone, PartialEq)] +enum PySampler { + AlwaysOn(bool), + TraceIdParentRatioBased(f64), +} + +impl Default for PySampler { + fn default() -> Self { + Self::AlwaysOn(true) + } +} + +impl From for Sampler { + fn from(sampler: PySampler) -> Self { + match sampler { + PySampler::AlwaysOn(b) if b => Self::AlwaysOn, + PySampler::AlwaysOn(_) => Self::AlwaysOff, + PySampler::TraceIdParentRatioBased(f) => Self::TraceIdRatioBased(f), + } + } +} + +/// The Rust `OpenTelemetry` SDK does not support the official OTLP headers [environment variables](https://opentelemetry.io/docs/specs/otel/protocol/exporter/). +/// Here we include a custom implementation. +const OTEL_EXPORTER_OTLP_HEADERS: &str = "OTEL_EXPORTER_OTLP_HEADERS"; +const OTEL_EXPORTER_OTLP_TRACES_HEADERS: &str = "OTEL_EXPORTER_OTLP_TRACES_HEADERS"; + +fn get_metadata_from_environment() -> Result { + [ + OTEL_EXPORTER_OTLP_HEADERS, + OTEL_EXPORTER_OTLP_TRACES_HEADERS, + ] + .iter() + .filter_map(|k| std::env::var(k).ok()) + .flat_map(|headers| { + headers + .split(',') + .map(String::from) + .filter_map(|kv| { + let mut x = kv.split('=').map(String::from); + Some((x.next()?, x.next()?)) + }) + .collect::>() + }) + .try_fold( + tonic::metadata::MetadataMap::new(), + |mut metadata, (k, v)| { + let key = k.parse::>().map_err(ConfigError::from)?; + metadata.insert(key, v.parse().map_err(ConfigError::from)?); + Ok(metadata) + }, + ) +} + +impl TryFrom for Config { + type Error = BuildError; + + fn try_from(config: PyConfig) -> Result { + let env_metadata_map = get_metadata_from_environment()?; + let metadata_map = match config.metadata_map { + Some(m) => Some(m.into_iter().try_fold( + env_metadata_map, + |mut metadata_map: tonic::metadata::MetadataMap, + (k, v)| + -> Result<_, Self::Error> { + let key = k.parse::>().map_err(ConfigError::from)?; + metadata_map.insert(key, v.parse().map_err(ConfigError::from)?); + Ok(metadata_map) + }, + )?), + None if !env_metadata_map.is_empty() => Some(env_metadata_map), + None => None, + }; + + Ok(Self { + span_limits: config.span_limits.into(), + resource: config.resource.into(), + metadata_map, + sampler: config.sampler.into(), + endpoint: config.endpoint, + timeout: config.timeout_millis.map(Duration::from_millis), + pre_shutdown_timeout: Duration::from_millis(config.pre_shutdown_timeout_millis), + filter: config.filter, + }) + } +} + +create_init_submodule! { + classes: [ PyConfig, PySpanLimits, PyResource ], +} diff --git a/crates/tracing-subscriber/src/layers/otel_otlp_file.rs b/crates/tracing-subscriber/src/layers/otel_otlp_file.rs new file mode 100644 index 0000000..86c7c5b --- /dev/null +++ b/crates/tracing-subscriber/src/layers/otel_otlp_file.rs @@ -0,0 +1,86 @@ +// Copyright 2023 Rigetti Computing +// +// 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. + +use opentelemetry_api::trace::TracerProvider; +use pyo3::prelude::*; +use rigetti_pyo3::create_init_submodule; +use tracing_subscriber::Layer; + +use super::{build_env_filter, force_flush_provider_as_shutdown, LayerBuildResult, WithShutdown}; + +/// Configures the [`opentelemetry-stdout`] crate layer. If [`file_path`] is None, the layer +/// will write to stdout. +#[pyclass] +#[derive(Clone, Debug, Default)] +pub(crate) struct Config { + pub(crate) file_path: Option, + pub(crate) filter: Option, +} + +#[pymethods] +impl Config { + #[new] + #[pyo3(signature = (/, file_path = None, filter = None))] + const fn new(file_path: Option, filter: Option) -> Self { + Self { file_path, filter } + } +} + +impl crate::layers::Config for Config { + fn requires_runtime(&self) -> bool { + false + } + + fn build(&self, batch: bool) -> LayerBuildResult { + let exporter_builder = opentelemetry_stdout::SpanExporter::builder(); + let exporter_builder = match self.file_path.as_ref() { + Some(file_path) => { + let file = std::fs::File::create(file_path).map_err(BuildError::from)?; + exporter_builder.with_writer(file) + } + None => exporter_builder, + }; + let provider = if batch { + opentelemetry_sdk::trace::TracerProvider::builder() + .with_batch_exporter( + exporter_builder.build(), + opentelemetry::runtime::TokioCurrentThread, + ) + .build() + } else { + opentelemetry_sdk::trace::TracerProvider::builder() + .with_simple_exporter(exporter_builder.build()) + .build() + }; + let tracer = provider.tracer("stdout"); + let env_filter = build_env_filter(self.filter.clone())?; + let layer = tracing_opentelemetry::layer() + .with_tracer(tracer) + .with_filter(env_filter); + Ok(WithShutdown { + layer: Box::new(layer), + shutdown: force_flush_provider_as_shutdown(provider, None), + }) + } +} + +#[derive(thiserror::Error, Debug)] +pub(crate) enum BuildError { + #[error("failed to initialize file span exporter for specified file path: {0}")] + InvalidFile(#[from] std::io::Error), +} + +create_init_submodule! { + classes: [ Config ], +} diff --git a/crates/tracing-subscriber/src/lib.rs b/crates/tracing-subscriber/src/lib.rs new file mode 100644 index 0000000..cc6ed48 --- /dev/null +++ b/crates/tracing-subscriber/src/lib.rs @@ -0,0 +1,214 @@ +// Copyright 2023 Rigetti Computing +// +// 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. + +// Covers correctness, suspicious, style, complexity, and perf +#![deny(clippy::all)] +#![deny(clippy::pedantic)] +#![deny(clippy::cargo)] +#![warn(clippy::nursery)] +// Has false positives that conflict with unreachable_pub +#![allow(clippy::redundant_pub_crate)] +#![deny( + absolute_paths_not_starting_with_crate, + anonymous_parameters, + bad_style, + dead_code, + keyword_idents, + improper_ctypes, + macro_use_extern_crate, + meta_variable_misuse, // May have false positives + missing_abi, + missing_debug_implementations, // can affect compile time/code size + missing_docs, + no_mangle_generic_items, + non_shorthand_field_patterns, + noop_method_call, + overflowing_literals, + path_statements, + patterns_in_fns_without_body, + pointer_structural_match, + private_in_public, + semicolon_in_expressions_from_macros, + trivial_casts, + trivial_numeric_casts, + unconditional_recursion, + unreachable_pub, + unsafe_code, + unused, + unused_allocation, + unused_comparisons, + unused_extern_crates, + unused_import_braces, + unused_lifetimes, + unused_parens, + variant_size_differences, + while_true +)] +//! This crate provides utilities for configuring and initializing a tracing subscriber from +//! Python. Because Rust pyo3-based Python packages are binaries, these utilities are exposed +//! as a `pyo3::types::PyModule` which can then be added to upstream pyo3 libraries. +//! +//! # Features +//! +//! * `layer-otel-otlp-file` - exports trace data with `opentelemetry-stdout`. See `crate::layers::otel_otlp_file`. +//! * `layer-otel-otlp` - exports trace data with `opentelemetry-otlp`. See `crate::layers::otel_otlp`. +//! * `stubs` - supports writing stub files in your Python source code from your Rust build scripts. See `crates::stubs` +//! +//! # Requirements and Limitations +//! +//! * The tracing subscribers initialized and configured _only_ capture tracing data for the pyo3 +//! library which adds the `pyo3-tracing-subscriber` module. Separate Python libraries require separate +//! bootstrapping. +//! * Python users can initialize tracing subscribers using context managers either globally, in +//! which case they can only initialize once, or per-thread, which is incompatible with Python +//! `async/await`. +//! * The `OTel` OTLP layer requires a heuristic based timeout upon context manager exit to ensure +//! trace data on the Rust side is flushed to the OTLP collector. This issue currently persists despite calls +//! to `force_flush` on the `opentelemetry_sdk::trace::TracerProvider` and `opentelemetry::global::shutdown_tracer_provider`. +//! +//! # Examples +//! +//! ``` +//! use pyo3_tracing_subscriber::pypropagate; +//! use pyo3::prelude::*; +//! use tracing::instrument; +//! +//! const MY_PACKAGE_NAME: &str = "example"; +//! const TRACING_SUBSCRIBER_SUBMODULE_NAME: &str = "tracing_subscriber"; +//! +//! #[pymodule] +//! fn example(_py: Python, m: &PyModule) -> PyResult<()> { +//! // add your functions, modules, and classes +//! let tracing_subscriber = PyModule::new(py, TRACING_SUBSCRIBER_SUBMODULE_NAME)?; +//! pyo3_tracing_subscriber::add_submodule( +//! format!("{MY_PACKAGE_NAME}.{TRACING_SUBSCRIBER_SUBMODULE_NAME}"), +//! py, +//! tracing_subscriber, +//! )?; +//! m.add_submodule(tracing_subscriber)?; +//! Ok(()) +//! } +//! ``` +//! +//! Then in Python: +//! +//! ```python +//! import asyncio +//! from example.tracing_subscriber import Tracing +//! +//! +//! async main(): +//! async with Tracing(): +//! # do stuff +//! pass +//! +//! +//! if __name__ == "__main__": +//! asyncio.run(main()) +//! ``` +//! +//! # Related Crates +//! +//! * `pyo3-opentelemetry` - propagates `OpenTelemetry` contexts from Python into Rust. +use pyo3::{types::PyModule, PyResult, Python}; +use rigetti_pyo3::create_init_submodule; + +use self::{ + contextmanager::{CurrentThreadTracingConfig, GlobalTracingConfig, TracingContextManagerError}, + export_process::{BatchConfig, SimpleConfig, TracingShutdownError, TracingStartError}, +}; +pub use contextmanager::Tracing; + +mod contextmanager; +mod export_process; +pub(crate) mod layers; +#[cfg(feature = "stubs")] +pub mod stubs; +pub(crate) mod subscriber; + +create_init_submodule! { + classes: [ + Tracing, + GlobalTracingConfig, + CurrentThreadTracingConfig, + BatchConfig, + SimpleConfig + ], + errors: [TracingContextManagerError, TracingStartError, TracingShutdownError], + submodules: [ + "layers": layers::init_submodule, + "subscriber": subscriber::init_submodule + ], +} + +/// Add the tracing submodule to the given module. This will add the submodule to the `sys.modules` +/// dictionary so that it can be imported from Python. +/// +/// # Arguments +/// +/// * `name` - the fully qualified name of the tracing subscriber submodule within your Python +/// package. For instance, if your package is named `my_package` and you want to add the tracing +/// subscriber submodule `tracing_subscriber`, then `name` should be +/// `my_package.tracing_subscriber`. +/// * `py` - the Python GIL token. +/// * `m` - the parent module to which the tracing subscriber submodule should be added. +/// +/// # Errors +/// +/// * `PyErr` if the submodule cannot be added. +/// +/// # Additional Details +/// +/// This function will add the following: +/// +/// * `Tracing` - a Python context manager which initializes the configured tracing subscriber. +/// * `GlobalTracingConfig` - a Python context manager which sets the configured tracing subscriber +/// as the global default (ie `tracing::subscriber::set_global_default`). The `Tracing` context +/// manager can be used _only once_ per process with this configuration. +/// * `CurrentThreadTracingConfig` - a Python context manager which sets the configured tracing +/// subscriber as the current thread default (ie `tracing::subscriber::set_default`). As the +/// context manager exits, the guard is dropped and the tracing subscriber can be re-initialized +/// with another default. Note, the default tracing subscriber will _not_ capture traces across +/// `async/await` boundaries that call `pyo3_asyncio::tokio::future_into_py`. +/// * `BatchConfig` - a Python context manager which configures the tracing subscriber to export +/// trace data in batch. As the `Tracing` context manager enters, a Tokio runtime is initialized +/// and will run in the background until the context manager exits. +/// * `SimpleConfig` - a Python context manager which configures the tracing subscriber to export +/// trace data in a non-batch manner. This only initializes a Tokio runtime if the underlying layer +/// requires an asynchronous runtime to export trace data (ie the `opentelemetry-otlp` layer). +/// * `layers` - a submodule which contains different layers to add to the tracing subscriber. +/// Currently supported: +/// * `tracing::fmt` - a layer which exports trace data to stdout in a non-OpenTelemetry data format. +/// * `opentelemetry-stdout` - a layer which exports trace data to stdout (requires the `layer-otel-otlp-file` feature). +/// * `opentelemetry-otlp` - a layer which exports trace data to an `OpenTelemetry` collector (requires the `layer-otel-otlp` feature). +/// * `subscriber` - a submodule which contains utilities for initialing the tracing subscriber +/// with the configured layer. Currently, the tracing subscriber is initialized as +/// `tracing::subscriber::Registry::default().with(layer)`. +/// +/// The following exceptions are added to the submodule: +/// +/// * `TracingContextManagerError` - raised when the `Tracing` context manager's methods are not +/// invoked in the correct order or multiplicity. +/// * `TracingStartError` - raised if the user-specified tracing layer or subscriber fails to build +/// and initialize properly upon context manager entry. +/// * `TracingShutdownError` - raised if the tracing layer or subscriber fails to shutdown properly on context manager exit. +/// +/// For detailed Python usage documentation, see the stub files written by +/// [`pyo3_tracing_subscriber::stubs::write_stub_files`]. +pub fn add_submodule(name: &str, py: Python, m: &PyModule) -> PyResult<()> { + init_submodule(name, py, m)?; + let modules = py.import("sys")?.getattr("modules")?; + modules.set_item(name, m)?; + Ok(()) +} diff --git a/crates/tracing-subscriber/src/stubs.rs b/crates/tracing-subscriber/src/stubs.rs new file mode 100644 index 0000000..fe14e9f --- /dev/null +++ b/crates/tracing-subscriber/src/stubs.rs @@ -0,0 +1,203 @@ +// Copyright 2023 Rigetti Computing +// +// 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. + +// Covers correctness, suspicious, style, complexity, and perf +#![deny(clippy::all)] +#![deny(clippy::pedantic)] +#![deny(clippy::cargo)] +#![warn(clippy::nursery)] +// Has false positives that conflict with unreachable_pub +#![allow(clippy::redundant_pub_crate)] +#![deny( + absolute_paths_not_starting_with_crate, + anonymous_parameters, + bad_style, + dead_code, + keyword_idents, + improper_ctypes, + macro_use_extern_crate, + meta_variable_misuse, // May have false positives + missing_abi, + missing_debug_implementations, // can affect compile time/code size + missing_docs, + no_mangle_generic_items, + non_shorthand_field_patterns, + noop_method_call, + overflowing_literals, + path_statements, + patterns_in_fns_without_body, + pointer_structural_match, + private_in_public, + semicolon_in_expressions_from_macros, + trivial_casts, + trivial_numeric_casts, + unconditional_recursion, + unreachable_pub, + unsafe_code, + unused, + unused_allocation, + unused_comparisons, + unused_extern_crates, + unused_import_braces, + unused_lifetimes, + unused_parens, + variant_size_differences, + while_true +)] +//! This module provides a function to evaluate Python stub file templates for the Python module +//! added by `pyo3_tracing_subscriber::add_submodule`. Upstream build scripts may use this to write +//! the Python stub files in their build scripts. +//! +//!
This function will render stubs based on the enabled features. In most +//! cases, this means you need to include any `pyo3_tracing_subscriber` features from your main +//! dependencies in your build dependencies as well.
+//! +//! # Example +//! +//! In `build.rs` with the `example/` directory containing Python source code. +//! +//! ```rust +//! use tracing_subscriber::stubs::write_stub_files; +//! +//! write_stub_files( +//! "example", +//! "tracing_subscriber", +//! std::path::Path::new("example/tracing_subscriber"), +//! ).unwrap(); +//! +use std::path::Path; + +use handlebars::{RenderError, TemplateError}; + +#[derive(serde::Serialize, Default)] +struct Data { + host_package: String, + tracing_subscriber_module_name: String, + version: String, + layer_otel_otlp_file: bool, + layer_otel_otlp: bool, + any_additional_layer: bool, +} + +impl Data { + fn new(host_package: String, tracing_subscriber_module_name: String) -> Self { + Self { + host_package, + tracing_subscriber_module_name, + version: env!("CARGO_PKG_VERSION").to_string(), + layer_otel_otlp_file: cfg!(feature = "layer-otel-otlp-file"), + layer_otel_otlp: cfg!(feature = "layer-otel-otlp"), + any_additional_layer: cfg!(feature = "layer-otel-otlp-file") + || cfg!(feature = "layer-otel-otlp"), + } + } +} + +/// Errors that may occur when writing stub files. +#[derive(thiserror::Error, Debug)] +pub enum Error { + /// Failed to open file for writing. + #[error("failed open file for writing: {0}")] + Io(#[from] std::io::Error), + /// Failed to render template. + #[error("failed to render template: {0}")] + Render(#[from] RenderError), + /// Failed to initialize template. + #[error("failed to initialize template: {0}")] + Template(#[from] Box), +} + +macro_rules! include_stub_and_init { + ($directory: ident, $template_name: tt, $hb: ident) => { + std::fs::create_dir_all($directory.join($template_name)).map_err(Error::from)?; + $hb.register_template_string( + concat!($template_name, "__init__.py"), + include_str!(concat!( + "../assets/python_stubs/", + $template_name, + "__init__.py" + )), + ) + .map_err(Box::new) + .map_err(Error::from)?; + $hb.register_template_string( + concat!($template_name, "__init__.pyi"), + include_str!(concat!( + "../assets/python_stubs/", + $template_name, + "__init__.pyi" + )), + ) + .map_err(Box::new) + .map_err(Error::from)?; + }; +} + +/// Write stub files for the given host package and tracing subscriber module name to the given +/// directory. +/// +/// # Arguments +/// +/// * `host_package` - The name of the host Python package. +/// * `tracing_subscriber_module_name` - The name of the tracing subscriber module (ie the Python +/// module that will contain the stub files). +/// * `directory` - The directory to write the stub files to. +/// * `layer_otel_otlp_file` - Whether to include stub files for the `otel_otlp_file` layer. +/// * `layer_otel_otlp` - Whether to include stub files for the `otel_otlp` layer. +/// +/// See module level documentation for the `pyo3-tracing-subscriber` crate for more information +/// about the `layer_` arguments. +/// +/// # Errors +/// +/// Will return an error if the stub files cannot be written to the given directory. +pub fn write_stub_files( + host_package: &str, + tracing_subscriber_module_name: &str, + directory: &Path, +) -> Result<(), Error> { + let mut hb = handlebars::Handlebars::new(); + include_stub_and_init!(directory, "", hb); + include_stub_and_init!(directory, "subscriber/", hb); + include_stub_and_init!(directory, "layers/", hb); + include_stub_and_init!(directory, "layers/file/", hb); + #[cfg(feature = "layer-otel-otlp-file")] + include_stub_and_init!(directory, "layers/otel_otlp_file/", hb); + #[cfg(feature = "layer-otel-otlp")] + include_stub_and_init!(directory, "layers/otel_otlp/", hb); + let data = Data::new( + host_package.to_string(), + tracing_subscriber_module_name.to_string(), + ); + for name in hb.get_templates().keys() { + let writer = std::fs::File::create(directory.join(name)).map_err(Error::from)?; + hb.render_to_write(name, &data, writer)?; + } + Ok(()) +} + +#[cfg(test)] +mod test { + use rstest::rstest; + + #[rstest] + fn test_build_stub_files() { + super::write_stub_files( + "example", + "_tracing_subscriber", + std::path::Path::new("target/stubs"), + ) + .unwrap(); + } +} diff --git a/crates/tracing-subscriber/src/subscriber.rs b/crates/tracing-subscriber/src/subscriber.rs new file mode 100644 index 0000000..f645567 --- /dev/null +++ b/crates/tracing-subscriber/src/subscriber.rs @@ -0,0 +1,226 @@ +// Copyright 2023 Rigetti Computing +// +// 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. +use pyo3::prelude::*; +use rigetti_pyo3::create_init_submodule; +use tracing::subscriber::DefaultGuard; +use tracing_subscriber::{layer::Layered, prelude::__tracing_subscriber_SubscriberExt, Registry}; + +#[derive(thiserror::Error, Debug)] +pub(crate) enum ShutdownError { + #[error("failed to shutdown configured layer: {0}")] + LayerShutdown(#[from] crate::layers::ShutdownError), +} + +type ShutdownResult = Result; + +#[derive(thiserror::Error, Debug)] +pub(crate) enum BuildError { + #[error("failed to build layer: {0}")] + LayerBuild(#[from] crate::layers::BuildError), +} + +#[derive(thiserror::Error, Debug)] +#[error("{message}")] +pub(crate) struct CustomError { + message: String, + #[source] + source: Option>, +} + +/// A shutdown function that can be used to shutdown the configured tracing subscriber. +pub(crate) type Shutdown = Box< + dyn (FnOnce() -> std::pin::Pin< + Box> + Send + Sync>, + >) + Send + + Sync, +>; + +type SubscriberBuildResult = Result; + +pub(crate) trait Config: BoxDynConfigClone + Send + Sync { + /// Indicates whether the underlying layer requires a Tokio runtime when the + /// layer is built _without_ batch export. + fn requires_runtime(&self) -> bool; + /// Builds the configured tracing subscriber. The `batch` argument may be + /// passed to underlying layers to indicate whether the subscriber will be + /// used in a batch context. + fn build(&self, batch: bool) -> SubscriberBuildResult; +} + +/// This trait is necessary so that `Box` can be cloned and, therefore, +/// used as an attribute on a `pyo3` class. +pub(crate) trait BoxDynConfigClone { + fn clone_box(&self) -> Box; +} + +impl BoxDynConfigClone for T +where + T: 'static + Config + Clone, +{ + fn clone_box(&self) -> Box { + Box::new(self.clone()) + } +} + +impl Clone for Box { + fn clone(&self) -> Self { + self.clone_box() + } +} + +/// A built tracing subscriber that is both `Send` and `Sync`. This is necessary to run any +/// background tasks necessary for exporting trace data. +pub(crate) trait SendSyncSubscriber: tracing::subscriber::Subscriber + Send + Sync {} + +impl SendSyncSubscriber for Layered +where + L: tracing_subscriber::Layer + Send + Sync, + I: tracing::Subscriber + Send + Sync, +{ +} + +/// Carries the built tracing subscriber and a shutdown function that can later be used to +/// shutdown the subscriber upon context manager exit. +pub(crate) struct WithShutdown { + pub(crate) subscriber: Box, + pub(crate) shutdown: Shutdown, +} + +impl core::fmt::Debug for WithShutdown { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "WithShutdown subscriber: Box, shutdown: Shutdown", + ) + } +} + +/// A Python wrapper for a tracing subscriber configuration. +#[pyclass(name = "Config")] +#[derive(Clone)] +pub(crate) struct PyConfig { + pub(crate) subscriber_config: Box, +} + +impl Default for PyConfig { + fn default() -> Self { + let layer = super::layers::PyConfig::default(); + Self { + subscriber_config: Box::new(TracingSubscriberRegistryConfig { + layer_config: Box::new(layer), + }), + } + } +} + +impl core::fmt::Debug for PyConfig { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "PyConfig {{ subscriber_config: Box }}") + } +} + +#[pymethods] +impl PyConfig { + #[new] + #[pyo3(signature = (/, layer = None))] + #[allow(clippy::pedantic)] + fn new(layer: Option) -> PyResult { + let layer = layer.unwrap_or_default(); + Ok(Self { + subscriber_config: Box::new(TracingSubscriberRegistryConfig { + layer_config: Box::new(layer), + }), + }) + } +} + +/// A concrete implementation of [`Config`] that wraps a [`tracing_subscriber::Registry`]. This is +/// used internally to build a [`tracing_subscriber::Registry`] from a [`crate::layers::PyConfig`]. +#[derive(Clone)] +pub(super) struct TracingSubscriberRegistryConfig { + pub(super) layer_config: Box, +} + +impl Config for TracingSubscriberRegistryConfig { + fn requires_runtime(&self) -> bool { + self.layer_config.requires_runtime() + } + + fn build(&self, batch: bool) -> SubscriberBuildResult { + let layer = self.layer_config.clone().build(batch)?; + let subscriber = Registry::default().with(layer.layer); + let shutdown = layer.shutdown; + Ok(WithShutdown { + subscriber: Box::new(subscriber), + shutdown: Box::new(move || { + Box::pin(async move { + shutdown().await?; + Ok(()) + }) + }), + }) + } +} + +#[derive(thiserror::Error, Debug)] +pub(crate) enum SetSubscriberError { + #[error("global default: {0}")] + SetGlobalDefault(#[from] tracing::subscriber::SetGlobalDefaultError), +} + +type SetSubscriberResult = Result; + +/// Sets the tracing subscriber for the current thread or globally. It returns a guard +/// that can be shutdown asynchronously. +pub(crate) fn set_subscriber( + subscriber: WithShutdown, + global: bool, +) -> SetSubscriberResult { + if global { + let shutdown = subscriber.shutdown; + tracing::subscriber::set_global_default(subscriber.subscriber)?; + Ok(SubscriberManagerGuard::Global(shutdown)) + } else { + let shutdown = subscriber.shutdown; + let guard = tracing::subscriber::set_default(subscriber.subscriber); + Ok(SubscriberManagerGuard::CurrentThread((shutdown, guard))) + } +} + +pub(crate) enum SubscriberManagerGuard { + Global(Shutdown), + CurrentThread((Shutdown, DefaultGuard)), +} + +impl SubscriberManagerGuard { + pub(crate) async fn shutdown(self) -> ShutdownResult<()> { + match self { + Self::Global(shutdown) => { + shutdown().await?; + opentelemetry::global::shutdown_tracer_provider(); + } + Self::CurrentThread((shutdown, guard)) => { + shutdown().await?; + drop(guard); + } + } + Ok(()) + } +} + +create_init_submodule! { + classes: [ + PyConfig + ], +} diff --git a/examples/pyo3-opentelemetry-lib/.gitignore b/examples/pyo3-opentelemetry-lib/.gitignore index 28238e1..925ce7a 100644 --- a/examples/pyo3-opentelemetry-lib/.gitignore +++ b/examples/pyo3-opentelemetry-lib/.gitignore @@ -73,3 +73,4 @@ docs/_build/ pyrightconfig.json +pyo3_opentelemetry_lib/test/__artifacts__ diff --git a/examples/pyo3-opentelemetry-lib/Cargo.toml b/examples/pyo3-opentelemetry-lib/Cargo.toml index f88b4df..690bbd6 100644 --- a/examples/pyo3-opentelemetry-lib/Cargo.toml +++ b/examples/pyo3-opentelemetry-lib/Cargo.toml @@ -18,15 +18,21 @@ name = "pyo3_opentelemetry_lib" crate-type = ["cdylib", "lib"] [dependencies] -opentelemetry = "0.18.0" -opentelemetry_api = "0.18.0" -opentelemetry_sdk = "0.18.0" -pyo3 = { version = "0.18.0" } -pyo3-asyncio = { version = "0.18.0", features = ["tokio", "tokio-runtime"] } -pyo3-opentelemetry = { path = "../../crates/lib" } +opentelemetry = "0.20.0" +opentelemetry_api = "0.20.0" +opentelemetry_sdk = "0.20.0" +pyo3 = { workspace = true } +pyo3-asyncio = { version = "0.19.0", features = ["tokio", "tokio-runtime"] } +pyo3-opentelemetry = { path = "../../crates/opentelemetry" } +pyo3-tracing-subscriber = { path = "../../crates/tracing-subscriber", features = ["layer-otel-otlp-file", "layer-otel-otlp"] } tokio = { version = "1.27.0", features = ["sync", "parking_lot", "macros"] } tracing = "0.1.37" +tracing-subscriber = { version = "0.3.17", features = ["env-filter"], optional = true } [features] extension-module = ["pyo3/extension-module"] default = ["extension-module"] + +[build-dependencies] +pyo3-tracing-subscriber = { path = "../../crates/tracing-subscriber", features = ["layer-otel-otlp-file", "layer-otel-otlp", "stubs"] } + diff --git a/examples/pyo3-opentelemetry-lib/Makefile.toml b/examples/pyo3-opentelemetry-lib/Makefile.toml index 94eb658..a9ffaaa 100644 --- a/examples/pyo3-opentelemetry-lib/Makefile.toml +++ b/examples/pyo3-opentelemetry-lib/Makefile.toml @@ -7,18 +7,18 @@ dependencies = ["python-install-dependencies"] script = ''' poetry run maturin develop + poetry run black pyo3_opentelemetry_lib/_tracing_subscriber + poetry run ruff pyo3_opentelemetry_lib/_tracing_subscriber --fix ''' [tasks.python-format] - private = true - dependencies = ["python-build"] + dependencies = [] script = ''' poetry run black . poetry run ruff . --fix ''' [tasks.python-check] - private = true dependencies = ["python-build"] script = ''' poetry run black . --check @@ -27,9 +27,20 @@ ''' [tasks.python-test] - private = true dependencies = ["python-build"] - script = "poetry run pytest ." + script = ''' + poetry run pytest . + + poetry run pytest 'pyo3_opentelemetry_lib/test/tracing_test.py::test_file_export[2]' --with-global-tracing-configuration + poetry run pytest 'pyo3_opentelemetry_lib/test/tracing_test.py::test_file_export[3]' --with-global-tracing-configuration + poetry run pytest 'pyo3_opentelemetry_lib/test/tracing_test.py::test_file_export_async[0]' --with-global-tracing-configuration + poetry run pytest 'pyo3_opentelemetry_lib/test/tracing_test.py::test_file_export_async[1]' --with-global-tracing-configuration + + poetry run pytest 'pyo3_opentelemetry_lib/test/tracing_test.py::test_otlp_export[config2]' --with-global-tracing-configuration + poetry run pytest 'pyo3_opentelemetry_lib/test/tracing_test.py::test_otlp_export[config3]' --with-global-tracing-configuration + poetry run pytest 'pyo3_opentelemetry_lib/test/tracing_test.py::test_otlp_export_async[config0]' --with-global-tracing-configuration + poetry run pytest 'pyo3_opentelemetry_lib/test/tracing_test.py::test_otlp_export_async[config1]' --with-global-tracing-configuration + ''' [tasks.python-check-all] dependencies = ["python-check", "python-test"] diff --git a/examples/pyo3-opentelemetry-lib/README.md b/examples/pyo3-opentelemetry-lib/README.md index ec9638c..14e2770 100644 --- a/examples/pyo3-opentelemetry-lib/README.md +++ b/examples/pyo3-opentelemetry-lib/README.md @@ -2,3 +2,11 @@ This crate demonstrates example usage of the `pypropagate` macro. It defines exa methods, which may wrap Rust async functions, and be called from Python. The generated Python bindings are used in the Poetry package within this crate to assert that contexts are properly set and propagated across the Python to Rust boundary. + +## Testing + +The following will build and test the example. + +```shell +cargo make python-check-all +``` diff --git a/examples/pyo3-opentelemetry-lib/build.rs b/examples/pyo3-opentelemetry-lib/build.rs new file mode 100644 index 0000000..24eb8d4 --- /dev/null +++ b/examples/pyo3-opentelemetry-lib/build.rs @@ -0,0 +1,7 @@ +use pyo3_tracing_subscriber::stubs::write_stub_files; + +fn main() { + let target_dir = std::path::Path::new("./pyo3_opentelemetry_lib/_tracing_subscriber"); + std::fs::remove_dir_all(target_dir).unwrap(); + write_stub_files("pyo3_opentelemetry_lib", "_tracing_subscriber", target_dir).unwrap(); +} diff --git a/examples/pyo3-opentelemetry-lib/poetry.lock b/examples/pyo3-opentelemetry-lib/poetry.lock index 2178fde..25e3f9c 100644 --- a/examples/pyo3-opentelemetry-lib/poetry.lock +++ b/examples/pyo3-opentelemetry-lib/poetry.lock @@ -110,6 +110,73 @@ files = [ [package.extras] test = ["pytest (>=6)"] +[[package]] +name = "grpcio" +version = "1.59.0" +description = "HTTP/2-based RPC framework" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "grpcio-1.59.0-cp310-cp310-linux_armv7l.whl", hash = "sha256:225e5fa61c35eeaebb4e7491cd2d768cd8eb6ed00f2664fa83a58f29418b39fd"}, + {file = "grpcio-1.59.0-cp310-cp310-macosx_12_0_universal2.whl", hash = "sha256:b95ec8ecc4f703f5caaa8d96e93e40c7f589bad299a2617bdb8becbcce525539"}, + {file = "grpcio-1.59.0-cp310-cp310-manylinux_2_17_aarch64.whl", hash = "sha256:1a839ba86764cc48226f50b924216000c79779c563a301586a107bda9cbe9dcf"}, + {file = "grpcio-1.59.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f6cfe44a5d7c7d5f1017a7da1c8160304091ca5dc64a0f85bca0d63008c3137a"}, + {file = "grpcio-1.59.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d0fcf53df684fcc0154b1e61f6b4a8c4cf5f49d98a63511e3f30966feff39cd0"}, + {file = "grpcio-1.59.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:fa66cac32861500f280bb60fe7d5b3e22d68c51e18e65367e38f8669b78cea3b"}, + {file = "grpcio-1.59.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8cd2d38c2d52f607d75a74143113174c36d8a416d9472415eab834f837580cf7"}, + {file = "grpcio-1.59.0-cp310-cp310-win32.whl", hash = "sha256:228b91ce454876d7eed74041aff24a8f04c0306b7250a2da99d35dd25e2a1211"}, + {file = "grpcio-1.59.0-cp310-cp310-win_amd64.whl", hash = "sha256:ca87ee6183421b7cea3544190061f6c1c3dfc959e0b57a5286b108511fd34ff4"}, + {file = "grpcio-1.59.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:c173a87d622ea074ce79be33b952f0b424fa92182063c3bda8625c11d3585d09"}, + {file = "grpcio-1.59.0-cp311-cp311-macosx_10_10_universal2.whl", hash = "sha256:ec78aebb9b6771d6a1de7b6ca2f779a2f6113b9108d486e904bde323d51f5589"}, + {file = "grpcio-1.59.0-cp311-cp311-manylinux_2_17_aarch64.whl", hash = "sha256:0b84445fa94d59e6806c10266b977f92fa997db3585f125d6b751af02ff8b9fe"}, + {file = "grpcio-1.59.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c251d22de8f9f5cca9ee47e4bade7c5c853e6e40743f47f5cc02288ee7a87252"}, + {file = "grpcio-1.59.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:956f0b7cb465a65de1bd90d5a7475b4dc55089b25042fe0f6c870707e9aabb1d"}, + {file = "grpcio-1.59.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:38da5310ef84e16d638ad89550b5b9424df508fd5c7b968b90eb9629ca9be4b9"}, + {file = "grpcio-1.59.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:63982150a7d598281fa1d7ffead6096e543ff8be189d3235dd2b5604f2c553e5"}, + {file = "grpcio-1.59.0-cp311-cp311-win32.whl", hash = "sha256:50eff97397e29eeee5df106ea1afce3ee134d567aa2c8e04fabab05c79d791a7"}, + {file = "grpcio-1.59.0-cp311-cp311-win_amd64.whl", hash = "sha256:15f03bd714f987d48ae57fe092cf81960ae36da4e520e729392a59a75cda4f29"}, + {file = "grpcio-1.59.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:f1feb034321ae2f718172d86b8276c03599846dc7bb1792ae370af02718f91c5"}, + {file = "grpcio-1.59.0-cp312-cp312-macosx_10_10_universal2.whl", hash = "sha256:d09bd2a4e9f5a44d36bb8684f284835c14d30c22d8ec92ce796655af12163588"}, + {file = "grpcio-1.59.0-cp312-cp312-manylinux_2_17_aarch64.whl", hash = "sha256:2f120d27051e4c59db2f267b71b833796770d3ea36ca712befa8c5fff5da6ebd"}, + {file = "grpcio-1.59.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ba0ca727a173ee093f49ead932c051af463258b4b493b956a2c099696f38aa66"}, + {file = "grpcio-1.59.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5711c51e204dc52065f4a3327dca46e69636a0b76d3e98c2c28c4ccef9b04c52"}, + {file = "grpcio-1.59.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:d74f7d2d7c242a6af9d4d069552ec3669965b74fed6b92946e0e13b4168374f9"}, + {file = "grpcio-1.59.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:3859917de234a0a2a52132489c4425a73669de9c458b01c9a83687f1f31b5b10"}, + {file = "grpcio-1.59.0-cp312-cp312-win32.whl", hash = "sha256:de2599985b7c1b4ce7526e15c969d66b93687571aa008ca749d6235d056b7205"}, + {file = "grpcio-1.59.0-cp312-cp312-win_amd64.whl", hash = "sha256:598f3530231cf10ae03f4ab92d48c3be1fee0c52213a1d5958df1a90957e6a88"}, + {file = "grpcio-1.59.0-cp37-cp37m-linux_armv7l.whl", hash = "sha256:b34c7a4c31841a2ea27246a05eed8a80c319bfc0d3e644412ec9ce437105ff6c"}, + {file = "grpcio-1.59.0-cp37-cp37m-macosx_10_10_universal2.whl", hash = "sha256:c4dfdb49f4997dc664f30116af2d34751b91aa031f8c8ee251ce4dcfc11277b0"}, + {file = "grpcio-1.59.0-cp37-cp37m-manylinux_2_17_aarch64.whl", hash = "sha256:61bc72a00ecc2b79d9695220b4d02e8ba53b702b42411397e831c9b0589f08a3"}, + {file = "grpcio-1.59.0-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f367e4b524cb319e50acbdea57bb63c3b717c5d561974ace0b065a648bb3bad3"}, + {file = "grpcio-1.59.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:849c47ef42424c86af069a9c5e691a765e304079755d5c29eff511263fad9c2a"}, + {file = "grpcio-1.59.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c0488c2b0528e6072010182075615620071371701733c63ab5be49140ed8f7f0"}, + {file = "grpcio-1.59.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:611d9aa0017fa386809bddcb76653a5ab18c264faf4d9ff35cb904d44745f575"}, + {file = "grpcio-1.59.0-cp37-cp37m-win_amd64.whl", hash = "sha256:e5378785dce2b91eb2e5b857ec7602305a3b5cf78311767146464bfa365fc897"}, + {file = "grpcio-1.59.0-cp38-cp38-linux_armv7l.whl", hash = "sha256:fe976910de34d21057bcb53b2c5e667843588b48bf11339da2a75f5c4c5b4055"}, + {file = "grpcio-1.59.0-cp38-cp38-macosx_10_10_universal2.whl", hash = "sha256:c041a91712bf23b2a910f61e16565a05869e505dc5a5c025d429ca6de5de842c"}, + {file = "grpcio-1.59.0-cp38-cp38-manylinux_2_17_aarch64.whl", hash = "sha256:0ae444221b2c16d8211b55326f8ba173ba8f8c76349bfc1768198ba592b58f74"}, + {file = "grpcio-1.59.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ceb1e68135788c3fce2211de86a7597591f0b9a0d2bb80e8401fd1d915991bac"}, + {file = "grpcio-1.59.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c4b1cc3a9dc1924d2eb26eec8792fedd4b3fcd10111e26c1d551f2e4eda79ce"}, + {file = "grpcio-1.59.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:871371ce0c0055d3db2a86fdebd1e1d647cf21a8912acc30052660297a5a6901"}, + {file = "grpcio-1.59.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:93e9cb546e610829e462147ce724a9cb108e61647a3454500438a6deef610be1"}, + {file = "grpcio-1.59.0-cp38-cp38-win32.whl", hash = "sha256:f21917aa50b40842b51aff2de6ebf9e2f6af3fe0971c31960ad6a3a2b24988f4"}, + {file = "grpcio-1.59.0-cp38-cp38-win_amd64.whl", hash = "sha256:14890da86a0c0e9dc1ea8e90101d7a3e0e7b1e71f4487fab36e2bfd2ecadd13c"}, + {file = "grpcio-1.59.0-cp39-cp39-linux_armv7l.whl", hash = "sha256:34341d9e81a4b669a5f5dca3b2a760b6798e95cdda2b173e65d29d0b16692857"}, + {file = "grpcio-1.59.0-cp39-cp39-macosx_10_10_universal2.whl", hash = "sha256:986de4aa75646e963466b386a8c5055c8b23a26a36a6c99052385d6fe8aaf180"}, + {file = "grpcio-1.59.0-cp39-cp39-manylinux_2_17_aarch64.whl", hash = "sha256:aca8a24fef80bef73f83eb8153f5f5a0134d9539b4c436a716256b311dda90a6"}, + {file = "grpcio-1.59.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:936b2e04663660c600d5173bc2cc84e15adbad9c8f71946eb833b0afc205b996"}, + {file = "grpcio-1.59.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc8bf2e7bc725e76c0c11e474634a08c8f24bcf7426c0c6d60c8f9c6e70e4d4a"}, + {file = "grpcio-1.59.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:81d86a096ccd24a57fa5772a544c9e566218bc4de49e8c909882dae9d73392df"}, + {file = "grpcio-1.59.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:2ea95cd6abbe20138b8df965b4a8674ec312aaef3147c0f46a0bac661f09e8d0"}, + {file = "grpcio-1.59.0-cp39-cp39-win32.whl", hash = "sha256:3b8ff795d35a93d1df6531f31c1502673d1cebeeba93d0f9bd74617381507e3f"}, + {file = "grpcio-1.59.0-cp39-cp39-win_amd64.whl", hash = "sha256:38823bd088c69f59966f594d087d3a929d1ef310506bee9e3648317660d65b81"}, + {file = "grpcio-1.59.0.tar.gz", hash = "sha256:acf70a63cf09dd494000007b798aff88a436e1c03b394995ce450be437b8e54f"}, +] + +[package.extras] +protobuf = ["grpcio-tools (>=1.59.0)"] + [[package]] name = "importlib-metadata" version = "6.0.1" @@ -216,6 +283,21 @@ deprecated = ">=1.2.6" importlib-metadata = ">=6.0.0,<6.1.0" setuptools = ">=16.0" +[[package]] +name = "opentelemetry-proto" +version = "1.20.0" +description = "OpenTelemetry Python Proto" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "opentelemetry_proto-1.20.0-py3-none-any.whl", hash = "sha256:512c3d2c6864fb7547a69577c3907348e6c985b7a204533563cb4c4c5046203b"}, + {file = "opentelemetry_proto-1.20.0.tar.gz", hash = "sha256:cf01f49b3072ee57468bccb1a4f93bdb55411f4512d0ac3f97c5c04c0040b5a2"}, +] + +[package.dependencies] +protobuf = ">=3.19,<5.0" + [[package]] name = "opentelemetry-sdk" version = "1.18.0" @@ -302,6 +384,41 @@ files = [ dev = ["pre-commit", "tox"] testing = ["pytest", "pytest-benchmark"] +[[package]] +name = "protobuf" +version = "4.24.3" +description = "" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "protobuf-4.24.3-cp310-abi3-win32.whl", hash = "sha256:20651f11b6adc70c0f29efbe8f4a94a74caf61b6200472a9aea6e19898f9fcf4"}, + {file = "protobuf-4.24.3-cp310-abi3-win_amd64.whl", hash = "sha256:3d42e9e4796a811478c783ef63dc85b5a104b44aaaca85d4864d5b886e4b05e3"}, + {file = "protobuf-4.24.3-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:6e514e8af0045be2b56e56ae1bb14f43ce7ffa0f68b1c793670ccbe2c4fc7d2b"}, + {file = "protobuf-4.24.3-cp37-abi3-manylinux2014_aarch64.whl", hash = "sha256:ba53c2f04798a326774f0e53b9c759eaef4f6a568ea7072ec6629851c8435959"}, + {file = "protobuf-4.24.3-cp37-abi3-manylinux2014_x86_64.whl", hash = "sha256:f6ccbcf027761a2978c1406070c3788f6de4a4b2cc20800cc03d52df716ad675"}, + {file = "protobuf-4.24.3-cp37-cp37m-win32.whl", hash = "sha256:1b182c7181a2891e8f7f3a1b5242e4ec54d1f42582485a896e4de81aa17540c2"}, + {file = "protobuf-4.24.3-cp37-cp37m-win_amd64.whl", hash = "sha256:b0271a701e6782880d65a308ba42bc43874dabd1a0a0f41f72d2dac3b57f8e76"}, + {file = "protobuf-4.24.3-cp38-cp38-win32.whl", hash = "sha256:e29d79c913f17a60cf17c626f1041e5288e9885c8579832580209de8b75f2a52"}, + {file = "protobuf-4.24.3-cp38-cp38-win_amd64.whl", hash = "sha256:067f750169bc644da2e1ef18c785e85071b7c296f14ac53e0900e605da588719"}, + {file = "protobuf-4.24.3-cp39-cp39-win32.whl", hash = "sha256:2da777d34b4f4f7613cdf85c70eb9a90b1fbef9d36ae4a0ccfe014b0b07906f1"}, + {file = "protobuf-4.24.3-cp39-cp39-win_amd64.whl", hash = "sha256:f631bb982c5478e0c1c70eab383af74a84be66945ebf5dd6b06fc90079668d0b"}, + {file = "protobuf-4.24.3-py3-none-any.whl", hash = "sha256:f6f8dc65625dadaad0c8545319c2e2f0424fede988368893ca3844261342c11a"}, + {file = "protobuf-4.24.3.tar.gz", hash = "sha256:12e9ad2ec079b833176d2921be2cb24281fa591f0b119b208b788adc48c2561d"}, +] + +[[package]] +name = "py" +version = "1.11.0" +description = "library with cross-python path, ini-parsing, io, code, log facilities" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +files = [ + {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, + {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, +] + [[package]] name = "pyright" version = "1.1.309" @@ -346,14 +463,14 @@ testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "no [[package]] name = "pytest-asyncio" -version = "0.21.0" +version = "0.21.1" description = "Pytest support for asyncio" category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "pytest-asyncio-0.21.0.tar.gz", hash = "sha256:2b38a496aef56f56b0e87557ec313e11e1ab9276fc3863f6a7be0f1d0e415e1b"}, - {file = "pytest_asyncio-0.21.0-py3-none-any.whl", hash = "sha256:f2b3366b7cd501a4056858bd39349d5af19742aed2d81660b7998b6341c7eb9c"}, + {file = "pytest-asyncio-0.21.1.tar.gz", hash = "sha256:40a7eae6dded22c7b604986855ea48400ab15b069ae38116e8c01238e9eeb64d"}, + {file = "pytest_asyncio-0.21.1-py3-none-any.whl", hash = "sha256:8666c1c8ac02631d7c51ba282e0c69a8a452b211ffedf2599099845da5c5c37b"}, ] [package.dependencies] @@ -363,6 +480,22 @@ pytest = ">=7.0.0" docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"] testing = ["coverage (>=6.2)", "flaky (>=3.5.0)", "hypothesis (>=5.7.1)", "mypy (>=0.931)", "pytest-trio (>=0.7.0)"] +[[package]] +name = "pytest-forked" +version = "1.6.0" +description = "run tests in isolated forked subprocesses" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pytest-forked-1.6.0.tar.gz", hash = "sha256:4dafd46a9a600f65d822b8f605133ecf5b3e1941ebb3588e943b4e3eb71a5a3f"}, + {file = "pytest_forked-1.6.0-py3-none-any.whl", hash = "sha256:810958f66a91afb1a1e2ae83089d8dc1cd2437ac96b12963042fbb9fb4d16af0"}, +] + +[package.dependencies] +py = "*" +pytest = ">=3.10" + [[package]] name = "ruff" version = "0.0.261" @@ -535,4 +668,4 @@ testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more [metadata] lock-version = "2.0" python-versions = ">=3.8" -content-hash = "91e942fedc00b8b672060b7eb8a82d235698519eb33e424672590caf51fa5ad7" +content-hash = "63d1f6f7e566e03838bfad367186ff1a6d0ec961c1cd05aa6ae6336815c2bfee" diff --git a/examples/pyo3-opentelemetry-lib/pyo3_opentelemetry_lib/__init__.py b/examples/pyo3-opentelemetry-lib/pyo3_opentelemetry_lib/__init__.py index bb9c33c..0bfaf6e 100644 --- a/examples/pyo3-opentelemetry-lib/pyo3_opentelemetry_lib/__init__.py +++ b/examples/pyo3-opentelemetry-lib/pyo3_opentelemetry_lib/__init__.py @@ -14,4 +14,8 @@ # limitations under the License. ############################################################################## -from .pyo3_opentelemetry_lib import * # noqa: F403 +# fmt: off +from .pyo3_opentelemetry_lib import * # noqa: F403, W291 + +__doc__ = pyo3_opentelemetry_lib.__doc__ # noqa: F405 +__all__ = getattr(pyo3_opentelemetry_lib, "__all__", []) # noqa: F405 diff --git a/examples/pyo3-opentelemetry-lib/pyo3_opentelemetry_lib/_tracing_subscriber/__init__.py b/examples/pyo3-opentelemetry-lib/pyo3_opentelemetry_lib/_tracing_subscriber/__init__.py new file mode 100644 index 0000000..0993fc1 --- /dev/null +++ b/examples/pyo3-opentelemetry-lib/pyo3_opentelemetry_lib/_tracing_subscriber/__init__.py @@ -0,0 +1,17 @@ +# ***************************************************************************** +# * AUTO-GENERATED CODE * +# * * +# * This code was generated by the `pyo3-tracing-subscriber` crate. Any * +# * modifications to this file should be made to the script or the generation * +# * process that produced this code. Specifically, see: * +# * `pyo3_tracing_subscriber::stubs::write_stub_files` * +# * * +# * Do not manually edit this file, as your changes may be overwritten the * +# * next time the code is generated. * +# ***************************************************************************** + +from pyo3_opentelemetry_lib import _tracing_subscriber + + +__doc__ = _tracing_subscriber.__doc__ +__all__ = getattr(_tracing_subscriber, "__all__", []) diff --git a/examples/pyo3-opentelemetry-lib/pyo3_opentelemetry_lib/_tracing_subscriber/__init__.pyi b/examples/pyo3-opentelemetry-lib/pyo3_opentelemetry_lib/_tracing_subscriber/__init__.pyi new file mode 100644 index 0000000..4bfe7ba --- /dev/null +++ b/examples/pyo3-opentelemetry-lib/pyo3_opentelemetry_lib/_tracing_subscriber/__init__.pyi @@ -0,0 +1,125 @@ +# ***************************************************************************** +# * AUTO-GENERATED CODE * +# * * +# * This code was generated by the `pyo3-tracing-subscriber` crate. Any * +# * modifications to this file should be made to the script or the generation * +# * process that produced this code. Specifically, see: * +# * `pyo3_tracing_subscriber::stubs::write_stub_files` * +# * * +# * Do not manually edit this file, as your changes may be overwritten the * +# * next time the code is generated. * +# ***************************************************************************** + +from types import TracebackType +from typing import Optional, Type, Union +from . import subscriber as subscriber +from . import layers as layers + + +class TracingContextManagerError(RuntimeError): + """ + Raised if the initialization, enter, and exit of the tracing context manager was + invoked in an invalid order. + """ + ... + + +class TracingStartError(RuntimeError): + """ + Raised if the tracing subscriber configuration is invalid or if a background export task + fails to start. + """ + ... + + +class TracingShutdownError(RuntimeError): + """ + Raised if the tracing subscriber fails to shutdown cleanly. + """ + ... + + +class BatchConfig: + """ + Configuration for exporting spans in batch. This will require a background task to be spawned + and run for the duration of the tracing context manager. + + This configuration is typically favorable unless the tracing context manager is short lived. + """ + def __init__(self, *, subscriber: subscriber.Config): + ... + + +class SimpleConfig: + """ + Configuration for exporting spans in a simple manner. This does not spawn a background task + unless it is required by the configured export layer. Generally favor `BatchConfig` instead, + unless the tracing context manager is short lived. + + Note, some export layers still spawn a background task even when `SimpleConfig` is used. + This is the case for the OTLP export layer, which makes gRPC export requests within the + background Tokio runtime. + """ + def __init__(self, *, subscriber: subscriber.Config): + ... + + +ExportConfig = Union[BatchConfig, SimpleConfig] +""" +One of `BatchConfig` or `SimpleConfig`. +""" + + +class CurrentThreadTracingConfig: + """ + This tracing configuration will export spans emitted only on the current thread. A `Tracing` context + manager may be initialized multiple times for the same process with this configuration (although + they should not be nested). + + Note, this configuration is currently incompatible with async methods defined with `pyo3_asyncio`. + """ + def __init__(self, *, export_process: ExportConfig): + ... + + +class GlobalTracingConfig: + """ + This tracing configuration will export spans emitted on any thread in the current process. Because + it sets a tracing subscriber at the global level, it can only be initialized once per process. + + This is typically favorable, as it only requires a single initialization across your entire Python + application. + """ + def __init__(self, *, export_process: ExportConfig): + ... + + +TracingConfig = Union[CurrentThreadTracingConfig, GlobalTracingConfig] +""" +One of `CurrentThreadTracingConfig` or `GlobalTracingConfig`. +""" + + +class Tracing: + """ + A context manager that initializes a tracing subscriber and exports spans + emitted from within the parent Rust-Python package. It may be used synchonously + or asynchronously. + + Each instance of this context manager can only be used once and only once. + """ + def __init__(self, *, config: TracingConfig): + ... + + def __enter__(self): + ... + + def __exit__(self, exc_type: Optional[Type[BaseException]], exc_value: Optional[BaseException], traceback: Optional[TracebackType]): + ... + + async def __aenter__(self): + ... + + async def __aexit__(self, exc_type: Optional[Type[BaseException]], exc_value: Optional[BaseException], traceback: Optional[TracebackType]): + ... + diff --git a/examples/pyo3-opentelemetry-lib/pyo3_opentelemetry_lib/_tracing_subscriber/layers/__init__.py b/examples/pyo3-opentelemetry-lib/pyo3_opentelemetry_lib/_tracing_subscriber/layers/__init__.py new file mode 100644 index 0000000..a2c6030 --- /dev/null +++ b/examples/pyo3-opentelemetry-lib/pyo3_opentelemetry_lib/_tracing_subscriber/layers/__init__.py @@ -0,0 +1,18 @@ +# ***************************************************************************** +# * AUTO-GENERATED CODE * +# * * +# * This code was generated by the `pyo3-tracing-subscriber` crate. Any * +# * modifications to this file should be made to the script or the generation * +# * process that produced this code. Specifically, see: * +# * `pyo3_tracing_subscriber::stubs::write_stub_files` * +# * * +# * Do not manually edit this file, as your changes may be overwritten the * +# * next time the code is generated. * +# ***************************************************************************** + +from pyo3_opentelemetry_lib._tracing_subscriber import layers + + +__doc__ = layers.__doc__ +__all__ = getattr(layers, "__all__", []) + diff --git a/examples/pyo3-opentelemetry-lib/pyo3_opentelemetry_lib/_tracing_subscriber/layers/__init__.pyi b/examples/pyo3-opentelemetry-lib/pyo3_opentelemetry_lib/_tracing_subscriber/layers/__init__.pyi new file mode 100644 index 0000000..f9963b8 --- /dev/null +++ b/examples/pyo3-opentelemetry-lib/pyo3_opentelemetry_lib/_tracing_subscriber/layers/__init__.pyi @@ -0,0 +1,25 @@ +# ***************************************************************************** +# * AUTO-GENERATED CODE * +# * * +# * This code was generated by the `pyo3-tracing-subscriber` crate. Any * +# * modifications to this file should be made to the script or the generation * +# * process that produced this code. Specifically, see: * +# * `pyo3_tracing_subscriber::stubs::write_stub_files` * +# * * +# * Do not manually edit this file, as your changes may be overwritten the * +# * next time the code is generated. * +# ***************************************************************************** + +from typing import Union +from .file import Config as FileConfig +from .otel_otlp_file import Config as OtlpFileConfig +from .otel_otlp import Config as OtlpConfig + +Config = Union[ + FileConfig, + OtlpFileConfig, + OtlpConfig, +] +""" +One of the supported layer configurations that may be set on the subscriber configuration. +""" diff --git a/examples/pyo3-opentelemetry-lib/pyo3_opentelemetry_lib/_tracing_subscriber/layers/file/__init__.py b/examples/pyo3-opentelemetry-lib/pyo3_opentelemetry_lib/_tracing_subscriber/layers/file/__init__.py new file mode 100644 index 0000000..365f089 --- /dev/null +++ b/examples/pyo3-opentelemetry-lib/pyo3_opentelemetry_lib/_tracing_subscriber/layers/file/__init__.py @@ -0,0 +1,19 @@ +# ***************************************************************************** +# * AUTO-GENERATED CODE * +# * * +# * This code was generated by the `pyo3-tracing-subscriber` crate. Any * +# * modifications to this file should be made to the script or the generation * +# * process that produced this code. Specifically, see: * +# * `pyo3_tracing_subscriber::stubs::write_stub_files` * +# * * +# * Do not manually edit this file, as your changes may be overwritten the * +# * next time the code is generated. * +# ***************************************************************************** + +from pyo3_opentelemetry_lib._tracing_subscriber.layers import file + + +__doc__ = file.__doc__ +__all__ = getattr(file, "__all__", []) + + diff --git a/examples/pyo3-opentelemetry-lib/pyo3_opentelemetry_lib/_tracing_subscriber/layers/file/__init__.pyi b/examples/pyo3-opentelemetry-lib/pyo3_opentelemetry_lib/_tracing_subscriber/layers/file/__init__.pyi new file mode 100644 index 0000000..d1a4ff8 --- /dev/null +++ b/examples/pyo3-opentelemetry-lib/pyo3_opentelemetry_lib/_tracing_subscriber/layers/file/__init__.pyi @@ -0,0 +1,41 @@ +# ***************************************************************************** +# * AUTO-GENERATED CODE * +# * * +# * This code was generated by the `pyo3-tracing-subscriber` crate. Any * +# * modifications to this file should be made to the script or the generation * +# * process that produced this code. Specifically, see: * +# * `pyo3_tracing_subscriber::stubs::write_stub_files` * +# * * +# * Do not manually edit this file, as your changes may be overwritten the * +# * next time the code is generated. * +# ***************************************************************************** + +from typing import Optional + + +class Config: + """ + Configuration for a + `tracing_subscriber::fmt::Layer `_. + """ + + def __init__(self, *, file_path: Optional[str] = None, pretty: bool = False, filter: Optional[str] = None, json: bool = True) -> None: + """ + Create a new `Config`. + + :param file_path: The path to the file to write to. If `None`, defaults to `stdout`. + :param pretty: Whether or not to pretty-print the output. Defaults to `False`. + :param filter: A filter string to use for this layer. This uses the same format as the + `tracing_subscriber::filter::EnvFilter + `_. + In summary, each directive takes the form `target[span{field=value}]=level`, where `target` + is roughly the Rust namespace and _only_ `level` is required. + + If not specified, this will first check the `PYO3_TRACING_SUBSCRIBER_ENV_FILTER` environment + variable and then `RUST_LOG` environment variable. If all of these values are empty, no spans + will be exported. + :param json: Whether or not to format the output as JSON. Defaults to `True`. + """ + ... + + diff --git a/examples/pyo3-opentelemetry-lib/pyo3_opentelemetry_lib/_tracing_subscriber/layers/otel_otlp/__init__.py b/examples/pyo3-opentelemetry-lib/pyo3_opentelemetry_lib/_tracing_subscriber/layers/otel_otlp/__init__.py new file mode 100644 index 0000000..cd94b23 --- /dev/null +++ b/examples/pyo3-opentelemetry-lib/pyo3_opentelemetry_lib/_tracing_subscriber/layers/otel_otlp/__init__.py @@ -0,0 +1,18 @@ +# ***************************************************************************** +# * AUTO-GENERATED CODE * +# * * +# * This code was generated by the `pyo3-tracing-subscriber` crate. Any * +# * modifications to this file should be made to the script or the generation * +# * process that produced this code. Specifically, see: * +# * `pyo3_tracing_subscriber::stubs::write_stub_files` * +# * * +# * Do not manually edit this file, as your changes may be overwritten the * +# * next time the code is generated. * +# ***************************************************************************** + +from pyo3_opentelemetry_lib._tracing_subscriber.layers import otel_otlp + + +__doc__ = otel_otlp.__doc__ +__all__ = getattr(otel_otlp, "__all__", []) + diff --git a/examples/pyo3-opentelemetry-lib/pyo3_opentelemetry_lib/_tracing_subscriber/layers/otel_otlp/__init__.pyi b/examples/pyo3-opentelemetry-lib/pyo3_opentelemetry_lib/_tracing_subscriber/layers/otel_otlp/__init__.pyi new file mode 100644 index 0000000..61c3603 --- /dev/null +++ b/examples/pyo3-opentelemetry-lib/pyo3_opentelemetry_lib/_tracing_subscriber/layers/otel_otlp/__init__.pyi @@ -0,0 +1,113 @@ +# ***************************************************************************** +# * AUTO-GENERATED CODE * +# * * +# * This code was generated by the `pyo3-tracing-subscriber` crate. Any * +# * modifications to this file should be made to the script or the generation * +# * process that produced this code. Specifically, see: * +# * `pyo3_tracing_subscriber::stubs::write_stub_files` * +# * * +# * Do not manually edit this file, as your changes may be overwritten the * +# * next time the code is generated. * +# ***************************************************************************** + +from typing import Dict, List, Optional, Union + + +class SpanLimits: + def __init__( + self, + *, + max_events_per_span: Optional[int] = None, + max_attributes_per_span: Optional[int] = None, + max_links_per_span: Optional[int] = None, + max_attributes_per_event: Optional[int] = None, + max_attributes_per_link: Optional[int] = None, + ) -> None: ... + """ + + :param max_events_per_span: The max events that can be added to a `Span`. + :param max_attributes_per_span: The max attributes that can be added to a `Span`. + :param max_links_per_span: The max links that can be added to a `Span`. + :param max_attributes_per_event: The max attributes that can be added to an `Event`. + :param max_attributes_per_link: The max attributes that can be added to a `Link`. + """ + + +ResourceValueArray = Union[List[bool], List[int], List[float], List[str]] +""" +An array of `ResourceValue`s. This array is homogenous, so all values must be of the same type. +""" + +ResourceValue = Union[bool, int, float, str, ResourceValueArray] +""" +A value that can be added to a `Resource`. +""" + + +class Resource: + """ + A `Resource` is a representation of the entity producing telemetry. This should represent the Python + process starting the tracing subscription process. + """ + def __init__( + self, + *, + attrs: Optional[Dict[str, ResourceValue]] = None, + schema_url: Optional[str] = None, + ) -> None: ... + + +Sampler = Union[bool, float] +""" +A `Sampler` is a representation of the sampling strategy to use. If this is a `bool`, it will +either sample all traces (`True`) or none of them (`False`). If this is a `float`, it will sample +traces at the given rate. +""" + + +class Config: + """ + A configuration for `opentelemetry-otlp `_ + layer. In addition to the values specified at initialization, this configuration will also respect the + canonical `OpenTelemetry OTLP environment variables + `_ that are `supported by opentelemetry-otlp + `_. + """ + + def __init__( + self, + *, + span_limits: Optional[SpanLimits] = None, + resource: Optional[Resource] = None, + metadata_map: Optional[Dict[str, str]] = None, + sampler: Optional[Sampler] = None, + endpoint: Optional[str] = None, + timeout_millis: Optional[int] = None, + pre_shutdown_timeout_millis: Optional[int] = 2000, + filter: Optional[str] = None, + ) -> None: + """ + Initializes a new `Config`. + + :param span_limits: The limits to apply to span exports. + :param resource: The OpenTelemetry resource to attach to all exported spans. + :param metadata_map: A map of metadata to attach to all exported spans. This is a map of key value pairs + that may be set as gRPC metadata by the tonic library. + :param sampler: The sampling strategy to use. See documentation for `Sampler` for more information. + :param endpoint: The endpoint to export to. This should be a valid URL. If not specified, this should be + specified by environment variables (see `Config` documentation). + :param timeout_millis: The timeout for each request, in milliseconds. If not specified, this should be + specified by environment variables (see `Config` documentation). + :param pre_shutdown_timeout_millis: The timeout to wait before shutting down the OTLP exporter in milliseconds. + This timeout is necessary to ensure all traces from `tracing_subscriber` to make it to the OpenTelemetry + layer, which may be effectively force flushed. It is enforced on the `Tracing` context manager exit. + :param filter: A filter string to use for this layer. This uses the same format as the + `tracing_subscriber::filter::EnvFilter + `_. + In summary, each directive takes the form `target[span{field=value}]=level`, where `target` is roughly the + Rust namespace and _only_ `level` is required. + + If not specified, this will first check the `PYO3_TRACING_SUBSCRIBER_ENV_FILTER` environment variable + and then `RUST_LOG` environment variable. If all of these values are empty, no spans will be exported. + """ + ... diff --git a/examples/pyo3-opentelemetry-lib/pyo3_opentelemetry_lib/_tracing_subscriber/layers/otel_otlp_file/__init__.py b/examples/pyo3-opentelemetry-lib/pyo3_opentelemetry_lib/_tracing_subscriber/layers/otel_otlp_file/__init__.py new file mode 100644 index 0000000..db105d0 --- /dev/null +++ b/examples/pyo3-opentelemetry-lib/pyo3_opentelemetry_lib/_tracing_subscriber/layers/otel_otlp_file/__init__.py @@ -0,0 +1,18 @@ +# ***************************************************************************** +# * AUTO-GENERATED CODE * +# * * +# * This code was generated by the `pyo3-tracing-subscriber` crate. Any * +# * modifications to this file should be made to the script or the generation * +# * process that produced this code. Specifically, see: * +# * `pyo3_tracing_subscriber::stubs::write_stub_files` * +# * * +# * Do not manually edit this file, as your changes may be overwritten the * +# * next time the code is generated. * +# ***************************************************************************** + +from pyo3_opentelemetry_lib._tracing_subscriber.layers import otel_otlp_file + + +__doc__ = otel_otlp_file.__doc__ +__all__ = getattr(otel_otlp_file, "__all__", []) + diff --git a/examples/pyo3-opentelemetry-lib/pyo3_opentelemetry_lib/_tracing_subscriber/layers/otel_otlp_file/__init__.pyi b/examples/pyo3-opentelemetry-lib/pyo3_opentelemetry_lib/_tracing_subscriber/layers/otel_otlp_file/__init__.pyi new file mode 100644 index 0000000..45330c4 --- /dev/null +++ b/examples/pyo3-opentelemetry-lib/pyo3_opentelemetry_lib/_tracing_subscriber/layers/otel_otlp_file/__init__.pyi @@ -0,0 +1,35 @@ +# ***************************************************************************** +# * AUTO-GENERATED CODE * +# * * +# * This code was generated by the `pyo3-tracing-subscriber` crate. Any * +# * modifications to this file should be made to the script or the generation * +# * process that produced this code. Specifically, see: * +# * `pyo3_tracing_subscriber::stubs::write_stub_files` * +# * * +# * Do not manually edit this file, as your changes may be overwritten the * +# * next time the code is generated. * +# ***************************************************************************** + +from typing import Optional + + +class Config: + """ + A configuration for `opentelemetry-stdout `_ + layer. + """ + + def __init__(self, *, file_path: Optional[str] = None, filter: Optional[str] = None) -> None: + """ + :param file_path: The path to the file to write to. If not specified, defaults to stdout. + :param filter: A filter string to use for this layer. This uses the same format as the + `tracing_subscriber::filter::EnvFilter + `_. + In summary, each directive takes the form `target[span{field=value}]=level`, where `target` is + roughly the Rust namespace and _only_ `level` is required. + + If not specified, this will first check the `PYO3_TRACING_SUBSCRIBER_ENV_FILTER` environment variable + and then `RUST_LOG` environment variable. If all of these values are empty, no spans will be exported. + """ + ... + diff --git a/examples/pyo3-opentelemetry-lib/pyo3_opentelemetry_lib/_tracing_subscriber/subscriber/__init__.py b/examples/pyo3-opentelemetry-lib/pyo3_opentelemetry_lib/_tracing_subscriber/subscriber/__init__.py new file mode 100644 index 0000000..5737e29 --- /dev/null +++ b/examples/pyo3-opentelemetry-lib/pyo3_opentelemetry_lib/_tracing_subscriber/subscriber/__init__.py @@ -0,0 +1,18 @@ +# ***************************************************************************** +# * AUTO-GENERATED CODE * +# * * +# * This code was generated by the `pyo3-tracing-subscriber` crate. Any * +# * modifications to this file should be made to the script or the generation * +# * process that produced this code. Specifically, see: * +# * `pyo3_tracing_subscriber::stubs::write_stub_files` * +# * * +# * Do not manually edit this file, as your changes may be overwritten the * +# * next time the code is generated. * +# ***************************************************************************** + +from pyo3_opentelemetry_lib._tracing_subscriber import subscriber + + +__doc__ = subscriber.__doc__ +__all__ = getattr(subscriber, "__all__", []) + diff --git a/examples/pyo3-opentelemetry-lib/pyo3_opentelemetry_lib/_tracing_subscriber/subscriber/__init__.pyi b/examples/pyo3-opentelemetry-lib/pyo3_opentelemetry_lib/_tracing_subscriber/subscriber/__init__.pyi new file mode 100644 index 0000000..323cb15 --- /dev/null +++ b/examples/pyo3-opentelemetry-lib/pyo3_opentelemetry_lib/_tracing_subscriber/subscriber/__init__.pyi @@ -0,0 +1,22 @@ +# ***************************************************************************** +# * AUTO-GENERATED CODE * +# * * +# * This code was generated by the `pyo3-tracing-subscriber` crate. Any * +# * modifications to this file should be made to the script or the generation * +# * process that produced this code. Specifically, see: * +# * `pyo3_tracing_subscriber::stubs::write_stub_files` * +# * * +# * Do not manually edit this file, as your changes may be overwritten the * +# * next time the code is generated. * +# ***************************************************************************** + +from .. import layers + + +class Config: + """ + Configuration for the tracing subscriber. Currently, this only requires a single layer to be + set on the `tracing_subscriber::Registry`. + """ + def __init__(self, *, layer: layers.Config): + ... diff --git a/examples/pyo3-opentelemetry-lib/pyo3_opentelemetry_lib/test/__artifacts__/.keep b/examples/pyo3-opentelemetry-lib/pyo3_opentelemetry_lib/test/__artifacts__/.keep new file mode 100644 index 0000000..e69de29 diff --git a/examples/pyo3-opentelemetry-lib/pyo3_opentelemetry_lib/test/conftest.py b/examples/pyo3-opentelemetry-lib/pyo3_opentelemetry_lib/test/conftest.py index 5c4b99a..a5b605f 100644 --- a/examples/pyo3-opentelemetry-lib/pyo3_opentelemetry_lib/test/conftest.py +++ b/examples/pyo3-opentelemetry-lib/pyo3_opentelemetry_lib/test/conftest.py @@ -13,19 +13,239 @@ # See the License for the specific language governing permissions and # limitations under the License. ############################################################################## +import asyncio +import multiprocessing as mp +import os +import socket +from concurrent import futures +from typing import AsyncGenerator, Generator, List, MutableMapping +from unittest import mock +from uuid import uuid4 +import grpc import pytest +from _pytest.config import Config +from _pytest.config.argparsing import Parser +from _pytest.nodes import Item +from grpc.aio import Metadata, ServicerContext, insecure_channel +from grpc.aio import server as create_grpc_server from opentelemetry import trace +from opentelemetry.proto.collector.trace.v1 import trace_service_pb2, trace_service_pb2_grpc +from opentelemetry.proto.trace.v1.trace_pb2 import ResourceSpans from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import ( BatchSpanProcessor, ConsoleSpanExporter, ) +mp.set_start_method("fork") + + +def pytest_addoption(parser: Parser): + parser.addoption( + "--with-global-tracing-configuration", + action="store_true", + default=False, + help="Run tests that use global tracing configuration.", + ) + + +def pytest_configure(config: Config): + config.addinivalue_line( + "markers", + ( + "global_tracing_configuration: mark test as using global tracing configuration (which can only be" + " initialized once per process)." + ), + ) + + +def pytest_collection_modifyitems(config: Config, items: List[Item]): + with_global_tracing_configuration = config.getoption("--with-global-tracing-configuration") + skip_global_tracing_configuration = pytest.mark.skip( + reason="requires --with-global-tracing-configuration pytest option to be true." + ) + + for item in items: + if not with_global_tracing_configuration: + if "global_tracing_configuration" in item.keywords: + item.add_marker(skip_global_tracing_configuration) + @pytest.fixture(scope="session") -def tracer() -> trace.Tracer: +def tracer() -> Generator[trace.Tracer, None, None]: + """ + Initializes a Python tracer. Because OpenTelemetry spans collected from Python are not of + concern to this library, we simply export them to `/dev/null`. + """ provider = TracerProvider() - processor = BatchSpanProcessor(ConsoleSpanExporter()) - provider.add_span_processor(processor) - return provider.get_tracer("integration-test") + with open(os.devnull, "w") as f: + processor = BatchSpanProcessor(ConsoleSpanExporter(out=f)) + provider.add_span_processor(processor) + try: + yield provider.get_tracer("integration-test") + finally: + # Even though we don't care about the exported spans from Python, we still flush the + # provider to ensure that all spans are exported before the process exits to avoid + # extraneous warnings. + provider.force_flush() + + +class TraceServiceServicer(trace_service_pb2_grpc.TraceServiceServicer): + """ + A mock implementation of the OpenTelemetry OTLP collector service. This + will keep track of all the spans that are sent to it in memory. It should + be run in a separate process to avoid blocking the main process. + + + """ + + def __init__(self, data: MutableMapping[str, List[ResourceSpans]]): + self.lock = asyncio.Lock() + self.resource_spans = data + + def _are_headers_set(self, metadata: Metadata) -> bool: + """ + Asserts that all `_SERVICE_TEST_HEADERS` are set in the metadata. + """ + for k, v in _SERVICE_TEST_HEADERS.items(): + value = next((value for key, value in metadata if key == k), None) + if value != v: + return False + return True + + async def Export( + self, request: trace_service_pb2.ExportTraceServiceRequest, context: ServicerContext + ) -> trace_service_pb2.ExportTraceServiceResponse: + """ + Verify the client metadata. Add the exported spans to `resource_spans` under the + namespace set by the `x-test-namespace` header. + """ + metadata = context.invocation_metadata() + if metadata is None or not self._are_headers_set(metadata): + context.set_code(grpc.StatusCode.PERMISSION_DENIED) + return trace_service_pb2.ExportTraceServiceResponse() + + namespace = next((value for key, value in metadata if key == "x-test-namespace"), None) + if namespace is None: + context.set_code(grpc.StatusCode.INVALID_ARGUMENT) + return trace_service_pb2.ExportTraceServiceResponse() + namespace = namespace.decode("utf-8") if isinstance(namespace, bytes) else str(namespace) + async with self.lock: + if namespace not in self.resource_spans: + self.resource_spans[namespace] = [] + self.resource_spans[namespace] += list(request.resource_spans) + context.set_code(grpc.StatusCode.OK) + return trace_service_pb2.ExportTraceServiceResponse( + partial_success=trace_service_pb2.ExportTracePartialSuccess() + ) + + +_SERVICE_TEST_HEADERS = { + "header1": "one", + "header2": "two", +} + + +async def _start_otlp_service_async(data, port): + server = create_grpc_server( + futures.ThreadPoolExecutor(max_workers=1), + ) + servicer = TraceServiceServicer(data) + trace_service_pb2_grpc.add_TraceServiceServicer_to_server(servicer, server) + + server.add_insecure_port(f"[::]:{port}") + try: + await server.start() + await server.wait_for_termination() + except Exception as e: + print(e) + + +def _start_otlp_service(data, port): + asyncio.run(_start_otlp_service_async(data, port)) + + +@pytest.fixture(scope="session") +def event_loop(): + """ + Required for async fixtures that use the "session" scope. + """ + loop = asyncio.get_event_loop() + try: + yield loop + finally: + loop.close() + + +@pytest.fixture(scope="session") +def file_export_filter() -> Generator[None, None, None]: + """ + Sets environment variables to set the desired `EnvFilter` for the OTLP file export layer. + """ + with mock.patch.dict( + os.environ, + { + "RUST_LOG": "error,pyo3_opentelemetry_lib=info", + }, + ): + yield + + +@pytest.fixture(scope="session") +async def otlp_service_data() -> AsyncGenerator[MutableMapping[str, List[ResourceSpans]], None]: + """ + Runs the `TraceServiceServicer` in a separate process, waits for a valid connection, and + yields the `resource_spans` dict. + """ + manager = mp.Manager() + data = manager.dict() + # find an available port for the `TraceServiceServicer` to use. + sock = socket.socket() + sock.bind(("", 0)) + port = sock.getsockname()[1] + # close the port so the `TraceServiceServicer` can use it. + sock.close() + + address = f"localhost:{port}" + process = mp.Process( + target=_start_otlp_service, + args=( + data, + port, + ), + ) + process.start() + + try: + # wait for the port to open + async with insecure_channel(address) as channel: + await asyncio.wait_for(channel.channel_ready(), timeout=30) + + with mock.patch.dict( + os.environ, + { + "OTEL_EXPORTER_OTLP_ENDPOINT": f"http://{address}", + "OTEL_EXPORTER_OTLP_INSECURE": "true", + "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT": f"http://{address}", + "OTEL_EXPORTER_OTLP_HEADERS": ",".join([f"{k}={v}" for k, v in _SERVICE_TEST_HEADERS.items()]), + "OTEL_EXPORTER_OTLP_TIMEOUT": "1s", + "RUST_LOG": "error,pyo3_opentelemetry_lib=info", + }, + ): + yield data + finally: + process.kill() + + +@pytest.fixture(scope="function") +def otlp_test_namespace() -> Generator[str, None, None]: + """ + Generates a new namespace per test function. `TraceServiceServicer` will store spans + under key of this generated namespace. + """ + namespace = str(uuid4()) + env = os.environ.copy() + env["OTEL_EXPORTER_OTLP_HEADERS"] += f",x-test-namespace={namespace}" + with mock.patch.dict(os.environ, env): + yield namespace diff --git a/examples/pyo3-opentelemetry-lib/pyo3_opentelemetry_lib/test/tracing_test.py b/examples/pyo3-opentelemetry-lib/pyo3_opentelemetry_lib/test/tracing_test.py new file mode 100644 index 0000000..ef9738b --- /dev/null +++ b/examples/pyo3-opentelemetry-lib/pyo3_opentelemetry_lib/test/tracing_test.py @@ -0,0 +1,375 @@ +from __future__ import annotations + +import json +import os +from collections import Counter +from time import time +from typing import TYPE_CHECKING, Any, Callable, Dict, List, MutableMapping + +import pytest +from opentelemetry import propagate +from opentelemetry.context import attach, detach +from opentelemetry.proto.trace.v1.trace_pb2 import ResourceSpans +from opentelemetry.trace import Tracer +from opentelemetry.trace.propagation import get_current_span + +import pyo3_opentelemetry_lib +from pyo3_opentelemetry_lib._tracing_subscriber import ( + BatchConfig, + CurrentThreadTracingConfig, + GlobalTracingConfig, + SimpleConfig, + Tracing, + subscriber, +) +from pyo3_opentelemetry_lib._tracing_subscriber.layers import otel_otlp as otlp +from pyo3_opentelemetry_lib._tracing_subscriber.layers import otel_otlp_file as file + +if TYPE_CHECKING: + from pyo3_opentelemetry_lib._tracing_subscriber import TracingConfig + + +_TEST_ARTIFACTS_DIR = os.path.join(os.path.dirname(__file__), "__artifacts__") + + +def global_tracing(param: Any): + """ + Do not run these tests unless the `global_tracing_configuration` option is set (see conftest.py). + It is necessary to run each test with global tracing configuration separately because + `GlobalTracingConfig` can only be initialized once per process. + + Alternative solutions such as `pytest-forked `_ + did not work with the `otel_service_data` fixture. + """ + return pytest.param(param, marks=pytest.mark.global_tracing_configuration) + + +@pytest.mark.parametrize( + "config_builder", + [ + lambda filename: CurrentThreadTracingConfig( + export_process=SimpleConfig( + subscriber=subscriber.Config(layer=file.Config(file_path=os.path.join(_TEST_ARTIFACTS_DIR, filename))) + ) + ), + lambda filename: CurrentThreadTracingConfig( + export_process=BatchConfig( + subscriber=subscriber.Config(layer=file.Config(file_path=os.path.join(_TEST_ARTIFACTS_DIR, filename))) + ) + ), + global_tracing( + lambda filename: GlobalTracingConfig( + export_process=SimpleConfig( + subscriber=subscriber.Config( + layer=file.Config(file_path=os.path.join(_TEST_ARTIFACTS_DIR, filename)) + ) + ) + ) + ), + global_tracing( + lambda filename: GlobalTracingConfig( + export_process=BatchConfig( + subscriber=subscriber.Config( + layer=file.Config(file_path=os.path.join(_TEST_ARTIFACTS_DIR, filename)) + ) + ) + ) + ), + ], +) +async def test_file_export(config_builder: Callable[[str], TracingConfig], tracer: Tracer, file_export_filter: None): + """ + Test that OTLP spans are accurately exported to a file. + """ + await _test_file_export(config_builder, tracer) + + +@pytest.mark.parametrize( + "config_builder", + [ + lambda filename: CurrentThreadTracingConfig( + export_process=SimpleConfig( + subscriber=subscriber.Config(layer=file.Config(file_path=os.path.join(_TEST_ARTIFACTS_DIR, filename))) + ) + ), + lambda filename: CurrentThreadTracingConfig( + export_process=BatchConfig( + subscriber=subscriber.Config(layer=file.Config(file_path=os.path.join(_TEST_ARTIFACTS_DIR, filename))) + ) + ), + ], +) +async def test_file_export_multi_threads( + config_builder: Callable[[str], TracingConfig], tracer: Tracer, file_export_filter: None +): + """ + Test that `CurrentThreadTracingConfig` can be initialized and used multiple times within the + same process. + """ + for _ in range(3): + await _test_file_export(config_builder, tracer) + + +async def _test_file_export(config_builder: Callable[[str], TracingConfig], tracer: Tracer): + """ + Implements a single test for file export. + """ + filename = f"test_file_export-{time()}.txt" + config = config_builder(filename) + with Tracing(config=config): + with tracer.start_as_current_span("test_file_export_tracing"): + current_span = get_current_span() + span_context = current_span.get_span_context() + assert span_context.is_valid + trace_id = span_context.trace_id + assert trace_id != 0 + # This function is implemented and instrumented in `examples/pyo3-opentelemetry-lib/src/lib.rs`. + result = pyo3_opentelemetry_lib.example_function() + + _assert_propagated_trace_id_eq(result, trace_id) + + # Read the OTLP spans written to file. + file_path = os.path.join(_TEST_ARTIFACTS_DIR, filename) + with open(file_path, "r") as f: + resource_spans: List[Dict[str, Any]] = [] + for line in f.readlines(): + datum = json.loads(line) + resource_spans += datum["resourceSpans"] + + counter = Counter() + for resource_span in resource_spans: + for scoped_span in resource_span["scopeSpans"]: + for span in scoped_span["spans"]: + span_trace_id = int(span["traceId"], 16) + assert span_trace_id is None or span_trace_id == trace_id, filename + counter[span["name"]] += 1 + # Assert that only the spans we expect are present. This makes use of the Rust `EnvFilter`, + # which we configure in the `file_export_filter` fixture (ie the `RUST_LOG` environment variable). + assert len(counter) == 1 + assert counter["example_function_impl"] == 1 + + +@pytest.mark.parametrize( + "config_builder", + [ + global_tracing( + lambda filename: GlobalTracingConfig( + export_process=SimpleConfig( + subscriber=subscriber.Config( + layer=file.Config(file_path=os.path.join(_TEST_ARTIFACTS_DIR, filename)) + ) + ) + ) + ), + global_tracing( + lambda filename: GlobalTracingConfig( + export_process=BatchConfig( + subscriber=subscriber.Config( + layer=file.Config(file_path=os.path.join(_TEST_ARTIFACTS_DIR, filename)) + ) + ) + ) + ), + ], +) +async def test_file_export_async( + config_builder: Callable[[str], TracingConfig], tracer: Tracer, file_export_filter: None +): + """ + Test that the `GlobalTracingConfig` supports async spans. + """ + filename = f"test_file_export_async-{time()}.txt" + config = config_builder(filename) + with Tracing(config=config): + with tracer.start_as_current_span("test_file_export_tracing"): + current_span = get_current_span() + span_context = current_span.get_span_context() + assert span_context.is_valid + trace_id = span_context.trace_id + assert trace_id != 0 + # This function is implemented and instrumented in `examples/pyo3-opentelemetry-lib/src/lib.rs`. + result = await pyo3_opentelemetry_lib.example_function_async() + + _assert_propagated_trace_id_eq(result, trace_id) + + file_path = os.path.join(_TEST_ARTIFACTS_DIR, filename) + with open(file_path, "r") as f: + resource_spans: List[Dict[str, Any]] = [] + for line in f.readlines(): + datum = json.loads(line) + resource_spans += datum["resourceSpans"] + + counter = Counter() + for resource_span in resource_spans: + for scoped_span in resource_span["scopeSpans"]: + for span in scoped_span["spans"]: + counter[span["name"]] += 1 + span_trace_id = int(span["traceId"], 16) + assert span_trace_id is None or span_trace_id == trace_id, filename + if span["name"] == "example_function_impl_async": + duration_ns = span["endTimeUnixNano"] - span["startTimeUnixNano"] + expected_duration_ms = 100 + assert duration_ns > (expected_duration_ms * 10**6) + assert duration_ns < (1.5 * expected_duration_ms * 10**6) + # Assert that only the spans we expect are present. This makes use of the Rust `EnvFilter`, + # which we configure in the `file_export_filter` fixture (ie the `RUST_LOG` environment variable). + assert len(counter) == 2 + assert counter["example_function_impl"] == 1 + assert counter["example_function_impl_async"] == 1 + + +@pytest.mark.parametrize( + "config", + [ + CurrentThreadTracingConfig(export_process=SimpleConfig(subscriber=subscriber.Config(layer=otlp.Config()))), + CurrentThreadTracingConfig(export_process=BatchConfig(subscriber=subscriber.Config(layer=otlp.Config()))), + global_tracing( + GlobalTracingConfig(export_process=SimpleConfig(subscriber=subscriber.Config(layer=otlp.Config()))) + ), + global_tracing( + GlobalTracingConfig(export_process=BatchConfig(subscriber=subscriber.Config(layer=otlp.Config()))) + ), + ], +) +async def test_otlp_export( + config: TracingConfig, + tracer: Tracer, + otlp_test_namespace: str, + otlp_service_data: MutableMapping[str, List[ResourceSpans]], +): + """ + Test that the `otlp.Config` can be used to export spans to an OTLP collector. Here, we use a mock + gRPC service (see `otlp_service_data` fixture) to collect spans and make assertions on them. + """ + with Tracing(config=config): + with tracer.start_as_current_span("test_file_export_tracing"): + current_span = get_current_span() + span_context = current_span.get_span_context() + assert span_context.is_valid + trace_id = span_context.trace_id + assert trace_id != 0 + # This function is implemented and instrumented in `examples/pyo3-opentelemetry-lib/src/lib.rs`. + result = pyo3_opentelemetry_lib.example_function() + + _assert_propagated_trace_id_eq(result, trace_id) + + counter = Counter() + data = otlp_service_data.get(otlp_test_namespace, None) + assert data is not None + for resource_span in data: + for scope_span in resource_span.scope_spans: + for span in scope_span.spans: + assert int.from_bytes(span.trace_id, "big") == trace_id, trace_id + counter[span.name] += 1 + # Assert that only the spans we expect are present. This makes use of the Rust `EnvFilter`, + # which we configure in the `otel_service_data` fixture (ie the `RUST_LOG` environment variable). + assert len(counter) == 1 + assert counter["example_function_impl"] == 1 + + +@pytest.mark.parametrize( + "config", + [ + CurrentThreadTracingConfig(export_process=SimpleConfig(subscriber=subscriber.Config(layer=otlp.Config()))), + CurrentThreadTracingConfig(export_process=BatchConfig(subscriber=subscriber.Config(layer=otlp.Config()))), + ], +) +async def test_otlp_export_multi_threads( + config: TracingConfig, + tracer: Tracer, + otlp_test_namespace: str, + otlp_service_data: MutableMapping[str, List[ResourceSpans]], +): + """ + Test that `CurrentThreadTracingConfig` can be used to export spans to an OTLP collector multiple times + within the same process. + """ + for _ in range(3): + with Tracing(config=config): + with tracer.start_as_current_span("test_file_export_tracing"): + current_span = get_current_span() + span_context = current_span.get_span_context() + assert span_context.is_valid + trace_id = span_context.trace_id + assert trace_id != 0 + # This function is implemented and instrumented in `examples/pyo3-opentelemetry-lib/src/lib.rs`. + result = pyo3_opentelemetry_lib.example_function() + + _assert_propagated_trace_id_eq(result, trace_id) + + data = otlp_service_data.get(otlp_test_namespace, None) + assert data is not None + counter = Counter() + for resource_span in data: + for scope_span in resource_span.scope_spans: + for span in scope_span.spans: + if int.from_bytes(span.trace_id, "big") == trace_id: + counter[span.name] += 1 + # Assert that only the spans we expect are present. This makes use of the Rust `EnvFilter`, + # which we configure in the `otel_service_data` fixture (ie the `RUST_LOG` environment variable). + assert len(counter) == 1 + assert counter["example_function_impl"] == 1 + + +@pytest.mark.parametrize( + "config", + [ + global_tracing( + GlobalTracingConfig(export_process=SimpleConfig(subscriber=subscriber.Config(layer=otlp.Config()))) + ), + global_tracing( + GlobalTracingConfig(export_process=BatchConfig(subscriber=subscriber.Config(layer=otlp.Config()))) + ), + ], +) +async def test_otlp_export_async( + config: TracingConfig, + tracer: Tracer, + otlp_test_namespace: str, + otlp_service_data: MutableMapping[str, List[ResourceSpans]], +): + """ + Test that the `GlobalTracingConfig` supports async spans when using the OTLP layer. + """ + with Tracing(config=config): + with tracer.start_as_current_span("test_file_export_tracing"): + current_span = get_current_span() + span_context = current_span.get_span_context() + assert span_context.is_valid + trace_id = span_context.trace_id + assert trace_id != 0 + # This function is implemented and instrumented in `examples/pyo3-opentelemetry-lib/src/lib.rs`. + result = await pyo3_opentelemetry_lib.example_function_async() + + _assert_propagated_trace_id_eq(result, trace_id) + + data = otlp_service_data.get(otlp_test_namespace, None) + assert data is not None + counter = Counter() + for resource_span in data: + for scope_span in resource_span.scope_spans: + for span in scope_span.spans: + counter[span.name] += 1 + assert int.from_bytes(span.trace_id, "big") == trace_id, trace_id + if span.name == "example_function_impl_async": + duration_ns = span.end_time_unix_nano - span.start_time_unix_nano + expected_duration_ms = 100 + assert duration_ns > (expected_duration_ms * 10**6) + assert duration_ns < (1.5 * expected_duration_ms * 10**6) + # Assert that only the spans we expect are present. This makes use of the Rust `EnvFilter`, + # which we configure in the `otel_service_data` fixture (ie the `RUST_LOG` environment variable). + assert len(counter) == 2 + assert counter["example_function_impl"] == 1 + assert counter["example_function_impl_async"] == 1 + + +def _assert_propagated_trace_id_eq(carrier: Dict[str, str], trace_id: int): + """ + The rust code is configured to return a hash map of the current span context. Here we + parse that map and assert that the trace id is the same as the one we initialized on the + Python side. + """ + new_context = propagate.extract(carrier=carrier) + token = attach(new_context) + assert get_current_span().get_span_context().trace_id == trace_id + detach(token) diff --git a/examples/pyo3-opentelemetry-lib/pyproject.toml b/examples/pyo3-opentelemetry-lib/pyproject.toml index a010f87..18ea611 100644 --- a/examples/pyo3-opentelemetry-lib/pyproject.toml +++ b/examples/pyo3-opentelemetry-lib/pyproject.toml @@ -27,12 +27,16 @@ pytest = "^7.3.0" opentelemetry-api = "^1.17.0" opentelemetry-sdk = "^1.17.0" pytest-asyncio = "^0.21.0" +opentelemetry-proto = "^1.20.0" [tool.poetry.group.dev.dependencies] maturin = "^0.14.17" black = "^23.3.0" pyright = "^1.1.303" ruff = "^0.0.261" +pytest-asyncio = "^0.21.1" +grpcio = "^1.59.0" +pytest-forked = "^1.6.0" [tool.black] line-length = 120 @@ -41,9 +45,17 @@ include = '\.pyi?$' preview= true extend-exclude = ''' ( + "pyo3_opentelemetry_example\/__init__.py", ) ''' +[tool.pyright] +exclude = [ + "pyo3_opentelemetry_lib/__init__.py", + ".venv" +] +reportUnsupportedDunderAll = false + [tool.ruff] select = [ # Pyflakes diff --git a/examples/pyo3-opentelemetry-lib/pytest.ini b/examples/pyo3-opentelemetry-lib/pytest.ini new file mode 100644 index 0000000..2f4c80e --- /dev/null +++ b/examples/pyo3-opentelemetry-lib/pytest.ini @@ -0,0 +1,2 @@ +[pytest] +asyncio_mode = auto diff --git a/examples/pyo3-opentelemetry-lib/src/lib.rs b/examples/pyo3-opentelemetry-lib/src/lib.rs index ce81e84..266c350 100644 --- a/examples/pyo3-opentelemetry-lib/src/lib.rs +++ b/examples/pyo3-opentelemetry-lib/src/lib.rs @@ -52,7 +52,6 @@ unused_import_braces, unused_lifetimes, unused_parens, - unused_qualifications, variant_size_differences, while_true )] @@ -70,9 +69,6 @@ use tracing::instrument; #[instrument] fn example_function_impl() -> HashMap { - let span = tracing::info_span!("example_function"); - let _guard = span.enter(); - let propagator = opentelemetry_sdk::propagation::TraceContextPropagator::new(); let mut injector = HashMap::new(); propagator.inject(&mut injector); @@ -132,9 +128,17 @@ impl ExampleStruct { } #[pymodule] -fn pyo3_opentelemetry_lib(_py: Python, m: &PyModule) -> PyResult<()> { +fn pyo3_opentelemetry_lib(py: Python, m: &PyModule) -> PyResult<()> { m.add_class::()?; m.add_function(wrap_pyfunction!(example_function, m)?)?; m.add_function(wrap_pyfunction!(example_function_async, m)?)?; + + let tracing_subscriber = PyModule::new(py, "_tracing_subscriber")?; + pyo3_tracing_subscriber::add_submodule( + "pyo3_opentelemetry_lib._tracing_subscriber", + py, + tracing_subscriber, + )?; + m.add_submodule(tracing_subscriber)?; Ok(()) } diff --git a/knope.toml b/knope.toml index ece7956..f705bbf 100644 --- a/knope.toml +++ b/knope.toml @@ -1,12 +1,17 @@ -[packages.lib] -versioned_files = ["crates/lib/Cargo.toml"] -changelog = "crates/lib/CHANGELOG.md" -scopes = ["lib", "pyo3-opentelemetry"] +[packages.opentelemetry] +versioned_files = ["crates/opentelemetry/Cargo.toml"] +changelog = "crates/opentelemetry/CHANGELOG.md" +scopes = ["lib", "opentelemetry", "pyo3-opentelemetry"] -[packages.macros] -versioned_files = ["crates/macros/Cargo.toml"] -changelog = "crates/macros/CHANGELOG.md" -scopes = ["macros", "pyo3-opentelemetry-macros"] +[packages.opentelemetry-macros] +versioned_files = ["crates/opentelemetry-macros/Cargo.toml"] +changelog = "crates/opentelemetry-macros/CHANGELOG.md" +scopes = ["macros", "opentelemetry-macros", "pyo3-opentelemetry-macros"] + +[packages.tracing-subscriber] +versioned_files = ["crates/tracing-subscriber/Cargo.toml"] +changelog = "crates/tracing-subscriber/CHANGELOG.md" +scopes = ["tracing-subscriber", "pyo3-tracing-subscriber"] [[workflows]] name = "release" From 2cf658eef4883c8242af51178d880bf3c18a88c7 Mon Sep 17 00:00:00 2001 From: Eric Hulburd Date: Wed, 29 Nov 2023 16:50:33 -0800 Subject: [PATCH 2/7] chore: get stubtest to pass --- .../assets/python_stubs/.stubtest-allowlist | 7 ++ .../assets/python_stubs/__init__.pyi | 81 +++++++++--------- .../assets/python_stubs/layers/__init__.pyi | 29 ++++--- .../python_stubs/layers/file/__init__.pyi | 13 +-- .../layers/otel_otlp/__init__.pyi | 79 +++++++++--------- .../layers/otel_otlp_file/__init__.pyi | 12 +-- .../python_stubs/subscriber/__init__.pyi | 17 ++-- crates/tracing-subscriber/src/stubs.rs | 6 ++ .../.stubtest-allowlist | 19 +++++ examples/pyo3-opentelemetry-lib/Makefile.toml | 2 +- examples/pyo3-opentelemetry-lib/mypy.ini | 7 ++ examples/pyo3-opentelemetry-lib/poetry.lock | 82 +++++++++++-------- .../pyo3_opentelemetry_lib/__init__.py | 2 +- .../pyo3_opentelemetry_lib/__init__.pyi | 18 ++-- .../_tracing_subscriber/.stubtest-allowlist | 7 ++ .../_tracing_subscriber/__init__.py | 1 - .../_tracing_subscriber/__init__.pyi | 81 +++++++++--------- .../_tracing_subscriber/layers/__init__.py | 2 - .../_tracing_subscriber/layers/__init__.pyi | 30 ++++--- .../layers/file/__init__.py | 3 - .../layers/file/__init__.pyi | 14 ++-- .../layers/otel_otlp/__init__.py | 2 - .../layers/otel_otlp/__init__.pyi | 80 +++++++++--------- .../layers/otel_otlp_file/__init__.py | 2 - .../layers/otel_otlp_file/__init__.pyi | 13 ++- .../subscriber/__init__.py | 2 - .../subscriber/__init__.pyi | 16 ++-- .../test/tracing_test.py | 10 +-- .../pyo3-opentelemetry-lib/pyproject.toml | 14 +++- 29 files changed, 372 insertions(+), 279 deletions(-) create mode 100644 crates/tracing-subscriber/assets/python_stubs/.stubtest-allowlist create mode 100644 examples/pyo3-opentelemetry-lib/.stubtest-allowlist create mode 100644 examples/pyo3-opentelemetry-lib/mypy.ini create mode 100644 examples/pyo3-opentelemetry-lib/pyo3_opentelemetry_lib/_tracing_subscriber/.stubtest-allowlist diff --git a/crates/tracing-subscriber/assets/python_stubs/.stubtest-allowlist b/crates/tracing-subscriber/assets/python_stubs/.stubtest-allowlist new file mode 100644 index 0000000..1b65f86 --- /dev/null +++ b/crates/tracing-subscriber/assets/python_stubs/.stubtest-allowlist @@ -0,0 +1,7 @@ +# The following are all type aliases. +{{ host_package }}.{{ tracing_subscriber_module_name }}.ExportConfig +{{ host_package }}.{{ tracing_subscriber_module_name }}.TracingConfig +{{ host_package }}.{{ tracing_subscriber_module_name }}.layers.Config +{{ host_package }}.{{ tracing_subscriber_module_name }}.layers.otel_otlp.ResourceValue +{{ host_package }}.{{ tracing_subscriber_module_name }}.layers.otel_otlp.ResourceValueArray +{{ host_package }}.{{ tracing_subscriber_module_name }}.layers.otel_otlp.Sampler diff --git a/crates/tracing-subscriber/assets/python_stubs/__init__.pyi b/crates/tracing-subscriber/assets/python_stubs/__init__.pyi index 4bfe7ba..240825d 100644 --- a/crates/tracing-subscriber/assets/python_stubs/__init__.pyi +++ b/crates/tracing-subscriber/assets/python_stubs/__init__.pyi @@ -10,35 +10,38 @@ # * next time the code is generated. * # ***************************************************************************** +from __future__ import annotations + from types import TracebackType -from typing import Optional, Type, Union -from . import subscriber as subscriber -from . import layers as layers +from typing import TYPE_CHECKING, Optional, Type, final +from . import layers as layers +from . import subscriber as subscriber class TracingContextManagerError(RuntimeError): """ Raised if the initialization, enter, and exit of the tracing context manager was invoked in an invalid order. """ - ... + ... class TracingStartError(RuntimeError): """ Raised if the tracing subscriber configuration is invalid or if a background export task fails to start. """ - ... + ... class TracingShutdownError(RuntimeError): """ Raised if the tracing subscriber fails to shutdown cleanly. """ - ... + ... +@final class BatchConfig: """ Configuration for exporting spans in batch. This will require a background task to be spawned @@ -46,30 +49,24 @@ class BatchConfig: This configuration is typically favorable unless the tracing context manager is short lived. """ - def __init__(self, *, subscriber: subscriber.Config): - ... + def __new__(cls, *, subscriber: subscriber.Config) -> "BatchConfig": ... +@final class SimpleConfig: """ Configuration for exporting spans in a simple manner. This does not spawn a background task unless it is required by the configured export layer. Generally favor `BatchConfig` instead, unless the tracing context manager is short lived. - Note, some export layers still spawn a background task even when `SimpleConfig` is used. + Note, some export layers still spawn a background task even when `SimpleConfig` is used. This is the case for the OTLP export layer, which makes gRPC export requests within the background Tokio runtime. """ - def __init__(self, *, subscriber: subscriber.Config): - ... - - -ExportConfig = Union[BatchConfig, SimpleConfig] -""" -One of `BatchConfig` or `SimpleConfig`. -""" + def __new__(cls, *, subscriber: subscriber.Config) -> "SimpleConfig": ... +@final class CurrentThreadTracingConfig: """ This tracing configuration will export spans emitted only on the current thread. A `Tracing` context @@ -78,10 +75,10 @@ class CurrentThreadTracingConfig: Note, this configuration is currently incompatible with async methods defined with `pyo3_asyncio`. """ - def __init__(self, *, export_process: ExportConfig): - ... + def __new__(cls, *, export_process: "ExportConfig") -> "CurrentThreadTracingConfig": ... +@final class GlobalTracingConfig: """ This tracing configuration will export spans emitted on any thread in the current process. Because @@ -90,16 +87,10 @@ class GlobalTracingConfig: This is typically favorable, as it only requires a single initialization across your entire Python application. """ - def __init__(self, *, export_process: ExportConfig): - ... - - -TracingConfig = Union[CurrentThreadTracingConfig, GlobalTracingConfig] -""" -One of `CurrentThreadTracingConfig` or `GlobalTracingConfig`. -""" + def __new__(cls, *, export_process: "ExportConfig") -> "GlobalTracingConfig": ... +@final class Tracing: """ A context manager that initializes a tracing subscriber and exports spans @@ -108,18 +99,32 @@ class Tracing: Each instance of this context manager can only be used once and only once. """ - def __init__(self, *, config: TracingConfig): - ... - def __enter__(self): - ... + def __new__(cls, *, config: "TracingConfig") -> "Tracing": ... + def __enter__(self): ... + def __exit__( + self, + exc_type: Optional[Type[BaseException]] = None, + exc_value: Optional[BaseException] = None, + traceback: Optional[TracebackType] = None, + ): ... + async def __aenter__(self): ... + async def __aexit__( + self, + exc_type: Optional[Type[BaseException]] = None, + exc_value: Optional[BaseException] = None, + traceback: Optional[TracebackType] = None, + ): ... - def __exit__(self, exc_type: Optional[Type[BaseException]], exc_value: Optional[BaseException], traceback: Optional[TracebackType]): - ... +if TYPE_CHECKING: + from typing import TypeAlias, Union - async def __aenter__(self): - ... - - async def __aexit__(self, exc_type: Optional[Type[BaseException]], exc_value: Optional[BaseException], traceback: Optional[TracebackType]): - ... + ExportConfig: TypeAlias = Union[BatchConfig, SimpleConfig] + """ + One of `BatchConfig` or `SimpleConfig`. + """ + TracingConfig: TypeAlias = Union[CurrentThreadTracingConfig, GlobalTracingConfig] + """ + One of `CurrentThreadTracingConfig` or `GlobalTracingConfig`. + """ diff --git a/crates/tracing-subscriber/assets/python_stubs/layers/__init__.pyi b/crates/tracing-subscriber/assets/python_stubs/layers/__init__.pyi index a3d6d89..95e4e91 100644 --- a/crates/tracing-subscriber/assets/python_stubs/layers/__init__.pyi +++ b/crates/tracing-subscriber/assets/python_stubs/layers/__init__.pyi @@ -10,16 +10,21 @@ # * next time the code is generated. * # ***************************************************************************** -from typing import Union -from .file import Config as FileConfig -{{#if layer_otel_otlp_file }}from .otel_otlp_file import Config as OtlpFileConfig{{/if}} -{{#if layer_otel_otlp}}from .otel_otlp import Config as OtlpConfig{{/if}} +from __future__ import annotations +from typing import TYPE_CHECKING -{{#if any_additional_layer}}Config = Union[ - FileConfig, - {{#if layer_otel_otlp_file }}OtlpFileConfig,{{/if}} - {{#if layer_otel_otlp }}OtlpConfig,{{/if}} -]{{else}}Config = FileConfig{{/if}} -""" -One of the supported layer configurations that may be set on the subscriber configuration. -""" +from . import file as file +{{#if layer_otel_otlp_file }}from . import otel_otlp_file as otel_otlp_file{{/if}} +{{#if layer_otel_otlp}}from . import otel_otlp as otel_otlp{{/if}} + +if TYPE_CHECKING: + from typing import TypeAlias{{#if any_additional_layer}}, Union{{/if}} + + {{#if any_additional_layer}}Config: TypeAlias = Union[ + file.Config, + {{#if layer_otel_otlp_file }}otel_otlp_file.Config,{{/if}} + {{#if layer_otel_otlp }}otel_otlp.Config,{{/if}} + ]{{else}}Config: TypeAlias = file.Config{{/if}} + """ + One of the supported layer configurations that may be set on the subscriber configuration. + """ diff --git a/crates/tracing-subscriber/assets/python_stubs/layers/file/__init__.pyi b/crates/tracing-subscriber/assets/python_stubs/layers/file/__init__.pyi index d1a4ff8..7caa5c2 100644 --- a/crates/tracing-subscriber/assets/python_stubs/layers/file/__init__.pyi +++ b/crates/tracing-subscriber/assets/python_stubs/layers/file/__init__.pyi @@ -10,16 +10,18 @@ # * next time the code is generated. * # ***************************************************************************** -from typing import Optional - +from typing import Optional, final +@final class Config: """ Configuration for a `tracing_subscriber::fmt::Layer `_. """ - - def __init__(self, *, file_path: Optional[str] = None, pretty: bool = False, filter: Optional[str] = None, json: bool = True) -> None: + + def __new__( + cls, *, file_path: Optional[str] = None, pretty: bool = False, filter: Optional[str] = None, json: bool = True + ) -> "Config": """ Create a new `Config`. @@ -33,9 +35,8 @@ class Config: If not specified, this will first check the `PYO3_TRACING_SUBSCRIBER_ENV_FILTER` environment variable and then `RUST_LOG` environment variable. If all of these values are empty, no spans - will be exported. + will be exported. :param json: Whether or not to format the output as JSON. Defaults to `True`. """ ... - diff --git a/crates/tracing-subscriber/assets/python_stubs/layers/otel_otlp/__init__.pyi b/crates/tracing-subscriber/assets/python_stubs/layers/otel_otlp/__init__.pyi index 61c3603..26a95e2 100644 --- a/crates/tracing-subscriber/assets/python_stubs/layers/otel_otlp/__init__.pyi +++ b/crates/tracing-subscriber/assets/python_stubs/layers/otel_otlp/__init__.pyi @@ -10,21 +10,22 @@ # * next time the code is generated. * # ***************************************************************************** -from typing import Dict, List, Optional, Union - +from __future__ import annotations +from typing import Dict, Optional, TYPE_CHECKING, final +@final class SpanLimits: - def __init__( - self, + def __new__( + cls, *, max_events_per_span: Optional[int] = None, max_attributes_per_span: Optional[int] = None, max_links_per_span: Optional[int] = None, max_attributes_per_event: Optional[int] = None, max_attributes_per_link: Optional[int] = None, - ) -> None: ... + ) -> "SpanLimits": ... """ - + :param max_events_per_span: The max events that can be added to a `Span`. :param max_attributes_per_span: The max attributes that can be added to a `Span`. :param max_links_per_span: The max links that can be added to a `Span`. @@ -32,67 +33,51 @@ class SpanLimits: :param max_attributes_per_link: The max attributes that can be added to a `Link`. """ - -ResourceValueArray = Union[List[bool], List[int], List[float], List[str]] -""" -An array of `ResourceValue`s. This array is homogenous, so all values must be of the same type. -""" - -ResourceValue = Union[bool, int, float, str, ResourceValueArray] -""" -A value that can be added to a `Resource`. -""" - - +@final class Resource: """ A `Resource` is a representation of the entity producing telemetry. This should represent the Python process starting the tracing subscription process. """ - def __init__( - self, + + def __new__( + cls, *, - attrs: Optional[Dict[str, ResourceValue]] = None, + attrs: Optional[Dict[str, "ResourceValue"]] = None, schema_url: Optional[str] = None, - ) -> None: ... + ) -> "Resource": ... -Sampler = Union[bool, float] -""" -A `Sampler` is a representation of the sampling strategy to use. If this is a `bool`, it will -either sample all traces (`True`) or none of them (`False`). If this is a `float`, it will sample -traces at the given rate. -""" - +@final class Config: """ A configuration for `opentelemetry-otlp `_ layer. In addition to the values specified at initialization, this configuration will also respect the - canonical `OpenTelemetry OTLP environment variables + canonical `OpenTelemetry OTLP environment variables `_ that are `supported by opentelemetry-otlp `_. """ - def __init__( - self, + def __new__( + cls, *, span_limits: Optional[SpanLimits] = None, resource: Optional[Resource] = None, metadata_map: Optional[Dict[str, str]] = None, - sampler: Optional[Sampler] = None, + sampler: Optional["Sampler"] = None, endpoint: Optional[str] = None, timeout_millis: Optional[int] = None, pre_shutdown_timeout_millis: Optional[int] = 2000, filter: Optional[str] = None, - ) -> None: + ) -> "Config": """ Initializes a new `Config`. :param span_limits: The limits to apply to span exports. :param resource: The OpenTelemetry resource to attach to all exported spans. :param metadata_map: A map of metadata to attach to all exported spans. This is a map of key value pairs - that may be set as gRPC metadata by the tonic library. + that may be set as gRPC metadata by the tonic library. :param sampler: The sampling strategy to use. See documentation for `Sampler` for more information. :param endpoint: The endpoint to export to. This should be a valid URL. If not specified, this should be specified by environment variables (see `Config` documentation). @@ -104,10 +89,30 @@ class Config: :param filter: A filter string to use for this layer. This uses the same format as the `tracing_subscriber::filter::EnvFilter `_. - In summary, each directive takes the form `target[span{field=value}]=level`, where `target` is roughly the + In summary, each directive takes the form `target[span{field=value}]=level`, where `target` is roughly the Rust namespace and _only_ `level` is required. If not specified, this will first check the `PYO3_TRACING_SUBSCRIBER_ENV_FILTER` environment variable - and then `RUST_LOG` environment variable. If all of these values are empty, no spans will be exported. + and then `RUST_LOG` environment variable. If all of these values are empty, no spans will be exported. """ ... + +if TYPE_CHECKING: + from typing import List, TypeAlias, Union + + ResourceValueArray: TypeAlias = Union[List[bool], List[int], List[float], List[str]] + """ + An array of `ResourceValue`s. This array is homogenous, so all values must be of the same type. + """ + + ResourceValue: TypeAlias = Union[bool, int, float, str, ResourceValueArray] + """ + A value that can be added to a `Resource`. + """ + + Sampler: TypeAlias = Union[bool, float] + """ + A `Sampler` is a representation of the sampling strategy to use. If this is a `bool`, it will + either sample all traces (`True`) or none of them (`False`). If this is a `float`, it will sample + traces at the given rate. + """ diff --git a/crates/tracing-subscriber/assets/python_stubs/layers/otel_otlp_file/__init__.pyi b/crates/tracing-subscriber/assets/python_stubs/layers/otel_otlp_file/__init__.pyi index 45330c4..613126b 100644 --- a/crates/tracing-subscriber/assets/python_stubs/layers/otel_otlp_file/__init__.pyi +++ b/crates/tracing-subscriber/assets/python_stubs/layers/otel_otlp_file/__init__.pyi @@ -10,26 +10,26 @@ # * next time the code is generated. * # ***************************************************************************** -from typing import Optional - +from typing import Optional, final +@final class Config: """ - A configuration for `opentelemetry-stdout `_ + A configuration for `opentelemetry-stdout `_ layer. """ - def __init__(self, *, file_path: Optional[str] = None, filter: Optional[str] = None) -> None: + def __new__(cls, *, file_path: Optional[str] = None, filter: Optional[str] = None) -> "Config": """ :param file_path: The path to the file to write to. If not specified, defaults to stdout. :param filter: A filter string to use for this layer. This uses the same format as the `tracing_subscriber::filter::EnvFilter - `_. + `_. In summary, each directive takes the form `target[span{field=value}]=level`, where `target` is roughly the Rust namespace and _only_ `level` is required. If not specified, this will first check the `PYO3_TRACING_SUBSCRIBER_ENV_FILTER` environment variable - and then `RUST_LOG` environment variable. If all of these values are empty, no spans will be exported. + and then `RUST_LOG` environment variable. If all of these values are empty, no spans will be exported. """ ... diff --git a/crates/tracing-subscriber/assets/python_stubs/subscriber/__init__.pyi b/crates/tracing-subscriber/assets/python_stubs/subscriber/__init__.pyi index 323cb15..548de0c 100644 --- a/crates/tracing-subscriber/assets/python_stubs/subscriber/__init__.pyi +++ b/crates/tracing-subscriber/assets/python_stubs/subscriber/__init__.pyi @@ -10,13 +10,16 @@ # * next time the code is generated. * # ***************************************************************************** -from .. import layers +from typing import final +from .. import layers +@final class Config: - """ - Configuration for the tracing subscriber. Currently, this only requires a single layer to be - set on the `tracing_subscriber::Registry`. - """ - def __init__(self, *, layer: layers.Config): - ... + """ + Configuration for the tracing subscriber. Currently, this only requires a single layer to be + set on the `tracing_subscriber::Registry`. + """ + + def __new__(cls, *, layer: layers.Config) -> "Config": ... + diff --git a/crates/tracing-subscriber/src/stubs.rs b/crates/tracing-subscriber/src/stubs.rs index fe14e9f..520f96e 100644 --- a/crates/tracing-subscriber/src/stubs.rs +++ b/crates/tracing-subscriber/src/stubs.rs @@ -169,6 +169,12 @@ pub fn write_stub_files( ) -> Result<(), Error> { let mut hb = handlebars::Handlebars::new(); include_stub_and_init!(directory, "", hb); + hb.register_template_string( + ".stubtest-allowlist", + include_str!("../assets/python_stubs/.stubtest-allowlist"), + ) + .map_err(Box::new) + .map_err(Error::from)?; include_stub_and_init!(directory, "subscriber/", hb); include_stub_and_init!(directory, "layers/", hb); include_stub_and_init!(directory, "layers/file/", hb); diff --git a/examples/pyo3-opentelemetry-lib/.stubtest-allowlist b/examples/pyo3-opentelemetry-lib/.stubtest-allowlist new file mode 100644 index 0000000..98213ae --- /dev/null +++ b/examples/pyo3-opentelemetry-lib/.stubtest-allowlist @@ -0,0 +1,19 @@ +pyo3_opentelemetry_lib.pyo3_opentelemetry_lib +pyo3_opentelemetry_lib.test.conftest.AsyncGenerator +pyo3_opentelemetry_lib.test.conftest.Generator +pyo3_opentelemetry_lib.test.conftest.BatchSpanProcessor.__init__ +pyo3_opentelemetry_lib.test.conftest.BatchSpanProcessor.force_flush +pyo3_opentelemetry_lib.test.conftest.Item.location +pyo3_opentelemetry_lib.test.conftest.MutableMapping +pyo3_opentelemetry_lib.test.conftest.ResourceSpans +pyo3_opentelemetry_lib.test.conftest.ResourceSpans.__init__ +pyo3_opentelemetry_lib.test.conftest.ResourceSpans.resource +pyo3_opentelemetry_lib.test.conftest.ResourceSpans.scope_spans +pyo3_opentelemetry_lib.test.conftest.TracerProvider.__init__ +pyo3_opentelemetry_lib.test.tracing_test.MutableMapping +pyo3_opentelemetry_lib.test.tracing_test.ResourceSpans +pyo3_opentelemetry_lib.test.tracing_test.ResourceSpans.__init__ +pyo3_opentelemetry_lib.test.tracing_test.ResourceSpans.resource +pyo3_opentelemetry_lib.test.tracing_test.ResourceSpans.scope_spans +pyo3_opentelemetry_lib.test.tracing_test.TracingConfig + diff --git a/examples/pyo3-opentelemetry-lib/Makefile.toml b/examples/pyo3-opentelemetry-lib/Makefile.toml index a9ffaaa..2a7aa07 100644 --- a/examples/pyo3-opentelemetry-lib/Makefile.toml +++ b/examples/pyo3-opentelemetry-lib/Makefile.toml @@ -3,7 +3,6 @@ script = "poetry install" [tasks.python-build] - private = true dependencies = ["python-install-dependencies"] script = ''' poetry run maturin develop @@ -24,6 +23,7 @@ poetry run black . --check poetry run ruff . poetry run pyright . + poetry run stubtest --allowlist .stubtest-allowlist --allowlist pyo3_opentelemetry_lib/_tracing_subscriber/.stubtest-allowlist --mypy-config-file ./mypy.ini pyo3_opentelemetry_lib ''' [tasks.python-test] diff --git a/examples/pyo3-opentelemetry-lib/mypy.ini b/examples/pyo3-opentelemetry-lib/mypy.ini new file mode 100644 index 0000000..28c7caa --- /dev/null +++ b/examples/pyo3-opentelemetry-lib/mypy.ini @@ -0,0 +1,7 @@ +# We only use mypy to run stubtest. We use pyright to check types. +[mypy] +[mypy-grpc.*] +ignore_missing_imports = True + +[mypy-grpc.aio.*] +ignore_missing_imports = True diff --git a/examples/pyo3-opentelemetry-lib/poetry.lock b/examples/pyo3-opentelemetry-lib/poetry.lock index 25e3f9c..1a45833 100644 --- a/examples/pyo3-opentelemetry-lib/poetry.lock +++ b/examples/pyo3-opentelemetry-lib/poetry.lock @@ -1,10 +1,9 @@ -# This file is automatically @generated by Poetry 1.4.2 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand. [[package]] name = "black" version = "23.3.0" description = "The uncompromising code formatter." -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -54,7 +53,6 @@ uvloop = ["uvloop (>=0.15.2)"] name = "click" version = "8.1.3" description = "Composable command line interface toolkit" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -69,7 +67,6 @@ colorama = {version = "*", markers = "platform_system == \"Windows\""} name = "colorama" version = "0.4.6" description = "Cross-platform colored terminal text." -category = "main" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" files = [ @@ -81,7 +78,6 @@ files = [ name = "deprecated" version = "1.2.13" description = "Python @deprecated decorator to deprecate old python classes, functions or methods." -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ @@ -99,7 +95,6 @@ dev = ["PyTest", "PyTest (<5)", "PyTest-Cov", "PyTest-Cov (<2.6)", "bump2version name = "exceptiongroup" version = "1.1.1" description = "Backport of PEP 654 (exception groups)" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -114,7 +109,6 @@ test = ["pytest (>=6)"] name = "grpcio" version = "1.59.0" description = "HTTP/2-based RPC framework" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -181,7 +175,6 @@ protobuf = ["grpcio-tools (>=1.59.0)"] name = "importlib-metadata" version = "6.0.1" description = "Read metadata from Python packages" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -201,7 +194,6 @@ testing = ["flake8 (<5)", "flufl.flake8", "importlib-resources (>=1.3)", "packag name = "iniconfig" version = "2.0.0" description = "brain-dead simple config-ini parsing" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -213,7 +205,6 @@ files = [ name = "maturin" version = "0.14.17" description = "Build and publish crates with pyo3, rust-cpython and cffi bindings as well as rust binaries as python packages" -category = "dev" optional = false python-versions = ">= 3.7" files = [ @@ -239,11 +230,57 @@ tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} patchelf = ["patchelf"] zig = ["ziglang (>=0.10.0,<0.11.0)"] +[[package]] +name = "mypy" +version = "1.7.1" +description = "Optional static typing for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "mypy-1.7.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:12cce78e329838d70a204293e7b29af9faa3ab14899aec397798a4b41be7f340"}, + {file = "mypy-1.7.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1484b8fa2c10adf4474f016e09d7a159602f3239075c7bf9f1627f5acf40ad49"}, + {file = "mypy-1.7.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31902408f4bf54108bbfb2e35369877c01c95adc6192958684473658c322c8a5"}, + {file = "mypy-1.7.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f2c2521a8e4d6d769e3234350ba7b65ff5d527137cdcde13ff4d99114b0c8e7d"}, + {file = "mypy-1.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:fcd2572dd4519e8a6642b733cd3a8cfc1ef94bafd0c1ceed9c94fe736cb65b6a"}, + {file = "mypy-1.7.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4b901927f16224d0d143b925ce9a4e6b3a758010673eeded9b748f250cf4e8f7"}, + {file = "mypy-1.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2f7f6985d05a4e3ce8255396df363046c28bea790e40617654e91ed580ca7c51"}, + {file = "mypy-1.7.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:944bdc21ebd620eafefc090cdf83158393ec2b1391578359776c00de00e8907a"}, + {file = "mypy-1.7.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9c7ac372232c928fff0645d85f273a726970c014749b924ce5710d7d89763a28"}, + {file = "mypy-1.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:f6efc9bd72258f89a3816e3a98c09d36f079c223aa345c659622f056b760ab42"}, + {file = "mypy-1.7.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6dbdec441c60699288adf051f51a5d512b0d818526d1dcfff5a41f8cd8b4aaf1"}, + {file = "mypy-1.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4fc3d14ee80cd22367caaaf6e014494415bf440980a3045bf5045b525680ac33"}, + {file = "mypy-1.7.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c6e4464ed5f01dc44dc9821caf67b60a4e5c3b04278286a85c067010653a0eb"}, + {file = "mypy-1.7.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:d9b338c19fa2412f76e17525c1b4f2c687a55b156320acb588df79f2e6fa9fea"}, + {file = "mypy-1.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:204e0d6de5fd2317394a4eff62065614c4892d5a4d1a7ee55b765d7a3d9e3f82"}, + {file = "mypy-1.7.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:84860e06ba363d9c0eeabd45ac0fde4b903ad7aa4f93cd8b648385a888e23200"}, + {file = "mypy-1.7.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8c5091ebd294f7628eb25ea554852a52058ac81472c921150e3a61cdd68f75a7"}, + {file = "mypy-1.7.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40716d1f821b89838589e5b3106ebbc23636ffdef5abc31f7cd0266db936067e"}, + {file = "mypy-1.7.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5cf3f0c5ac72139797953bd50bc6c95ac13075e62dbfcc923571180bebb662e9"}, + {file = "mypy-1.7.1-cp38-cp38-win_amd64.whl", hash = "sha256:78e25b2fd6cbb55ddfb8058417df193f0129cad5f4ee75d1502248e588d9e0d7"}, + {file = "mypy-1.7.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:75c4d2a6effd015786c87774e04331b6da863fc3fc4e8adfc3b40aa55ab516fe"}, + {file = "mypy-1.7.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2643d145af5292ee956aa0a83c2ce1038a3bdb26e033dadeb2f7066fb0c9abce"}, + {file = "mypy-1.7.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75aa828610b67462ffe3057d4d8a4112105ed211596b750b53cbfe182f44777a"}, + {file = "mypy-1.7.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ee5d62d28b854eb61889cde4e1dbc10fbaa5560cb39780c3995f6737f7e82120"}, + {file = "mypy-1.7.1-cp39-cp39-win_amd64.whl", hash = "sha256:72cf32ce7dd3562373f78bd751f73c96cfb441de147cc2448a92c1a308bd0ca6"}, + {file = "mypy-1.7.1-py3-none-any.whl", hash = "sha256:f7c5d642db47376a0cc130f0de6d055056e010debdaf0707cd2b0fc7e7ef30ea"}, + {file = "mypy-1.7.1.tar.gz", hash = "sha256:fcb6d9afb1b6208b4c712af0dafdc650f518836065df0d4fb1d800f5d6773db2"}, +] + +[package.dependencies] +mypy-extensions = ">=1.0.0" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typing-extensions = ">=4.1.0" + +[package.extras] +dmypy = ["psutil (>=4.0)"] +install-types = ["pip"] +mypyc = ["setuptools (>=50)"] +reports = ["lxml"] + [[package]] name = "mypy-extensions" version = "1.0.0" description = "Type system extensions for programs checked with the mypy type checker." -category = "dev" optional = false python-versions = ">=3.5" files = [ @@ -255,7 +292,6 @@ files = [ name = "nodeenv" version = "1.8.0" description = "Node.js virtual environment builder" -category = "dev" optional = false python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" files = [ @@ -270,7 +306,6 @@ setuptools = "*" name = "opentelemetry-api" version = "1.18.0" description = "OpenTelemetry Python API" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -287,7 +322,6 @@ setuptools = ">=16.0" name = "opentelemetry-proto" version = "1.20.0" description = "OpenTelemetry Python Proto" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -302,7 +336,6 @@ protobuf = ">=3.19,<5.0" name = "opentelemetry-sdk" version = "1.18.0" description = "OpenTelemetry Python SDK" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -320,7 +353,6 @@ typing-extensions = ">=3.7.4" name = "opentelemetry-semantic-conventions" version = "0.39b0" description = "OpenTelemetry Semantic Conventions" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -332,7 +364,6 @@ files = [ name = "packaging" version = "23.1" description = "Core utilities for Python packages" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -344,7 +375,6 @@ files = [ name = "pathspec" version = "0.11.1" description = "Utility library for gitignore style pattern matching of file paths." -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -356,7 +386,6 @@ files = [ name = "platformdirs" version = "3.5.1" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -372,7 +401,6 @@ test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.3.1)", "pytest- name = "pluggy" version = "1.0.0" description = "plugin and hook calling mechanisms for python" -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -388,7 +416,6 @@ testing = ["pytest", "pytest-benchmark"] name = "protobuf" version = "4.24.3" description = "" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -411,7 +438,6 @@ files = [ name = "py" version = "1.11.0" description = "library with cross-python path, ini-parsing, io, code, log facilities" -category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" files = [ @@ -423,7 +449,6 @@ files = [ name = "pyright" version = "1.1.309" description = "Command line wrapper for pyright" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -442,7 +467,6 @@ dev = ["twine (>=3.4.1)"] name = "pytest" version = "7.3.1" description = "pytest: simple powerful testing with Python" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -465,7 +489,6 @@ testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "no name = "pytest-asyncio" version = "0.21.1" description = "Pytest support for asyncio" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -484,7 +507,6 @@ testing = ["coverage (>=6.2)", "flaky (>=3.5.0)", "hypothesis (>=5.7.1)", "mypy name = "pytest-forked" version = "1.6.0" description = "run tests in isolated forked subprocesses" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -500,7 +522,6 @@ pytest = ">=3.10" name = "ruff" version = "0.0.261" description = "An extremely fast Python linter, written in Rust." -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -527,7 +548,6 @@ files = [ name = "setuptools" version = "67.8.0" description = "Easily download, build, install, upgrade, and uninstall Python packages" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -544,7 +564,6 @@ testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs ( name = "tomli" version = "2.0.1" description = "A lil' TOML parser" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -556,7 +575,6 @@ files = [ name = "typing-extensions" version = "4.5.0" description = "Backported and Experimental Type Hints for Python 3.7+" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -568,7 +586,6 @@ files = [ name = "wrapt" version = "1.15.0" description = "Module for decorators, wrappers and monkey patching." -category = "main" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" files = [ @@ -653,7 +670,6 @@ files = [ name = "zipp" version = "3.15.0" description = "Backport of pathlib-compatible object wrapper for zip files" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -668,4 +684,4 @@ testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more [metadata] lock-version = "2.0" python-versions = ">=3.8" -content-hash = "63d1f6f7e566e03838bfad367186ff1a6d0ec961c1cd05aa6ae6336815c2bfee" +content-hash = "b7af9b3579574b4eab556366e09743cb6ce42642258fbb3da4370a2d31ff8e8b" diff --git a/examples/pyo3-opentelemetry-lib/pyo3_opentelemetry_lib/__init__.py b/examples/pyo3-opentelemetry-lib/pyo3_opentelemetry_lib/__init__.py index 0bfaf6e..c163a80 100644 --- a/examples/pyo3-opentelemetry-lib/pyo3_opentelemetry_lib/__init__.py +++ b/examples/pyo3-opentelemetry-lib/pyo3_opentelemetry_lib/__init__.py @@ -18,4 +18,4 @@ from .pyo3_opentelemetry_lib import * # noqa: F403, W291 __doc__ = pyo3_opentelemetry_lib.__doc__ # noqa: F405 -__all__ = getattr(pyo3_opentelemetry_lib, "__all__", []) # noqa: F405 +__all__ = getattr(pyo3_opentelemetry_lib, "__all__", []) # noqa: F405 diff --git a/examples/pyo3-opentelemetry-lib/pyo3_opentelemetry_lib/__init__.pyi b/examples/pyo3-opentelemetry-lib/pyo3_opentelemetry_lib/__init__.pyi index 6b853bd..980c5a0 100644 --- a/examples/pyo3-opentelemetry-lib/pyo3_opentelemetry_lib/__init__.pyi +++ b/examples/pyo3-opentelemetry-lib/pyo3_opentelemetry_lib/__init__.pyi @@ -1,13 +1,11 @@ -from typing import Dict, Optional +from typing import Dict, final -class PyO3OpenTelemetryCarrier: - def __init__(self) -> None: ... - def __setitem__(self, key: str, value: str) -> None: ... - -def example_function(*, extractor: Optional[PyO3OpenTelemetryCarrier] = None) -> Dict[str, str]: ... -async def example_function_async(*, extractor: Optional[PyO3OpenTelemetryCarrier] = None) -> Dict[str, str]: ... +from . import _tracing_subscriber as _tracing_subscriber +def example_function() -> Dict[str, str]: ... +async def example_function_async() -> Dict[str, str]: ... +@final class ExampleStruct: - def __init__(self) -> None: ... - def example_method(self, extractor: Optional[PyO3OpenTelemetryCarrier] = None) -> Dict[str, str]: ... - async def example_method_async(self, extractor: Optional[PyO3OpenTelemetryCarrier] = None) -> Dict[str, str]: ... + def __new__(cls, /, *args, **kwargs) -> "ExampleStruct": ... + def example_method(self) -> Dict[str, str]: ... + async def example_method_async(self) -> Dict[str, str]: ... diff --git a/examples/pyo3-opentelemetry-lib/pyo3_opentelemetry_lib/_tracing_subscriber/.stubtest-allowlist b/examples/pyo3-opentelemetry-lib/pyo3_opentelemetry_lib/_tracing_subscriber/.stubtest-allowlist new file mode 100644 index 0000000..d916345 --- /dev/null +++ b/examples/pyo3-opentelemetry-lib/pyo3_opentelemetry_lib/_tracing_subscriber/.stubtest-allowlist @@ -0,0 +1,7 @@ +# The following are all type aliases. +pyo3_opentelemetry_lib._tracing_subscriber.ExportConfig +pyo3_opentelemetry_lib._tracing_subscriber.TracingConfig +pyo3_opentelemetry_lib._tracing_subscriber.layers.Config +pyo3_opentelemetry_lib._tracing_subscriber.layers.otel_otlp.ResourceValue +pyo3_opentelemetry_lib._tracing_subscriber.layers.otel_otlp.ResourceValueArray +pyo3_opentelemetry_lib._tracing_subscriber.layers.otel_otlp.Sampler diff --git a/examples/pyo3-opentelemetry-lib/pyo3_opentelemetry_lib/_tracing_subscriber/__init__.py b/examples/pyo3-opentelemetry-lib/pyo3_opentelemetry_lib/_tracing_subscriber/__init__.py index 0993fc1..5b14109 100644 --- a/examples/pyo3-opentelemetry-lib/pyo3_opentelemetry_lib/_tracing_subscriber/__init__.py +++ b/examples/pyo3-opentelemetry-lib/pyo3_opentelemetry_lib/_tracing_subscriber/__init__.py @@ -12,6 +12,5 @@ from pyo3_opentelemetry_lib import _tracing_subscriber - __doc__ = _tracing_subscriber.__doc__ __all__ = getattr(_tracing_subscriber, "__all__", []) diff --git a/examples/pyo3-opentelemetry-lib/pyo3_opentelemetry_lib/_tracing_subscriber/__init__.pyi b/examples/pyo3-opentelemetry-lib/pyo3_opentelemetry_lib/_tracing_subscriber/__init__.pyi index 4bfe7ba..240825d 100644 --- a/examples/pyo3-opentelemetry-lib/pyo3_opentelemetry_lib/_tracing_subscriber/__init__.pyi +++ b/examples/pyo3-opentelemetry-lib/pyo3_opentelemetry_lib/_tracing_subscriber/__init__.pyi @@ -10,35 +10,38 @@ # * next time the code is generated. * # ***************************************************************************** +from __future__ import annotations + from types import TracebackType -from typing import Optional, Type, Union -from . import subscriber as subscriber -from . import layers as layers +from typing import TYPE_CHECKING, Optional, Type, final +from . import layers as layers +from . import subscriber as subscriber class TracingContextManagerError(RuntimeError): """ Raised if the initialization, enter, and exit of the tracing context manager was invoked in an invalid order. """ - ... + ... class TracingStartError(RuntimeError): """ Raised if the tracing subscriber configuration is invalid or if a background export task fails to start. """ - ... + ... class TracingShutdownError(RuntimeError): """ Raised if the tracing subscriber fails to shutdown cleanly. """ - ... + ... +@final class BatchConfig: """ Configuration for exporting spans in batch. This will require a background task to be spawned @@ -46,30 +49,24 @@ class BatchConfig: This configuration is typically favorable unless the tracing context manager is short lived. """ - def __init__(self, *, subscriber: subscriber.Config): - ... + def __new__(cls, *, subscriber: subscriber.Config) -> "BatchConfig": ... +@final class SimpleConfig: """ Configuration for exporting spans in a simple manner. This does not spawn a background task unless it is required by the configured export layer. Generally favor `BatchConfig` instead, unless the tracing context manager is short lived. - Note, some export layers still spawn a background task even when `SimpleConfig` is used. + Note, some export layers still spawn a background task even when `SimpleConfig` is used. This is the case for the OTLP export layer, which makes gRPC export requests within the background Tokio runtime. """ - def __init__(self, *, subscriber: subscriber.Config): - ... - - -ExportConfig = Union[BatchConfig, SimpleConfig] -""" -One of `BatchConfig` or `SimpleConfig`. -""" + def __new__(cls, *, subscriber: subscriber.Config) -> "SimpleConfig": ... +@final class CurrentThreadTracingConfig: """ This tracing configuration will export spans emitted only on the current thread. A `Tracing` context @@ -78,10 +75,10 @@ class CurrentThreadTracingConfig: Note, this configuration is currently incompatible with async methods defined with `pyo3_asyncio`. """ - def __init__(self, *, export_process: ExportConfig): - ... + def __new__(cls, *, export_process: "ExportConfig") -> "CurrentThreadTracingConfig": ... +@final class GlobalTracingConfig: """ This tracing configuration will export spans emitted on any thread in the current process. Because @@ -90,16 +87,10 @@ class GlobalTracingConfig: This is typically favorable, as it only requires a single initialization across your entire Python application. """ - def __init__(self, *, export_process: ExportConfig): - ... - - -TracingConfig = Union[CurrentThreadTracingConfig, GlobalTracingConfig] -""" -One of `CurrentThreadTracingConfig` or `GlobalTracingConfig`. -""" + def __new__(cls, *, export_process: "ExportConfig") -> "GlobalTracingConfig": ... +@final class Tracing: """ A context manager that initializes a tracing subscriber and exports spans @@ -108,18 +99,32 @@ class Tracing: Each instance of this context manager can only be used once and only once. """ - def __init__(self, *, config: TracingConfig): - ... - def __enter__(self): - ... + def __new__(cls, *, config: "TracingConfig") -> "Tracing": ... + def __enter__(self): ... + def __exit__( + self, + exc_type: Optional[Type[BaseException]] = None, + exc_value: Optional[BaseException] = None, + traceback: Optional[TracebackType] = None, + ): ... + async def __aenter__(self): ... + async def __aexit__( + self, + exc_type: Optional[Type[BaseException]] = None, + exc_value: Optional[BaseException] = None, + traceback: Optional[TracebackType] = None, + ): ... - def __exit__(self, exc_type: Optional[Type[BaseException]], exc_value: Optional[BaseException], traceback: Optional[TracebackType]): - ... +if TYPE_CHECKING: + from typing import TypeAlias, Union - async def __aenter__(self): - ... - - async def __aexit__(self, exc_type: Optional[Type[BaseException]], exc_value: Optional[BaseException], traceback: Optional[TracebackType]): - ... + ExportConfig: TypeAlias = Union[BatchConfig, SimpleConfig] + """ + One of `BatchConfig` or `SimpleConfig`. + """ + TracingConfig: TypeAlias = Union[CurrentThreadTracingConfig, GlobalTracingConfig] + """ + One of `CurrentThreadTracingConfig` or `GlobalTracingConfig`. + """ diff --git a/examples/pyo3-opentelemetry-lib/pyo3_opentelemetry_lib/_tracing_subscriber/layers/__init__.py b/examples/pyo3-opentelemetry-lib/pyo3_opentelemetry_lib/_tracing_subscriber/layers/__init__.py index a2c6030..a4dbb1c 100644 --- a/examples/pyo3-opentelemetry-lib/pyo3_opentelemetry_lib/_tracing_subscriber/layers/__init__.py +++ b/examples/pyo3-opentelemetry-lib/pyo3_opentelemetry_lib/_tracing_subscriber/layers/__init__.py @@ -12,7 +12,5 @@ from pyo3_opentelemetry_lib._tracing_subscriber import layers - __doc__ = layers.__doc__ __all__ = getattr(layers, "__all__", []) - diff --git a/examples/pyo3-opentelemetry-lib/pyo3_opentelemetry_lib/_tracing_subscriber/layers/__init__.pyi b/examples/pyo3-opentelemetry-lib/pyo3_opentelemetry_lib/_tracing_subscriber/layers/__init__.pyi index f9963b8..09fe32f 100644 --- a/examples/pyo3-opentelemetry-lib/pyo3_opentelemetry_lib/_tracing_subscriber/layers/__init__.pyi +++ b/examples/pyo3-opentelemetry-lib/pyo3_opentelemetry_lib/_tracing_subscriber/layers/__init__.pyi @@ -10,16 +10,22 @@ # * next time the code is generated. * # ***************************************************************************** -from typing import Union -from .file import Config as FileConfig -from .otel_otlp_file import Config as OtlpFileConfig -from .otel_otlp import Config as OtlpConfig +from __future__ import annotations -Config = Union[ - FileConfig, - OtlpFileConfig, - OtlpConfig, -] -""" -One of the supported layer configurations that may be set on the subscriber configuration. -""" +from typing import TYPE_CHECKING + +from . import file as file +from . import otel_otlp as otel_otlp +from . import otel_otlp_file as otel_otlp_file + +if TYPE_CHECKING: + from typing import TypeAlias, Union + + Config: TypeAlias = Union[ + file.Config, + otel_otlp_file.Config, + otel_otlp.Config, + ] + """ + One of the supported layer configurations that may be set on the subscriber configuration. + """ diff --git a/examples/pyo3-opentelemetry-lib/pyo3_opentelemetry_lib/_tracing_subscriber/layers/file/__init__.py b/examples/pyo3-opentelemetry-lib/pyo3_opentelemetry_lib/_tracing_subscriber/layers/file/__init__.py index 365f089..ea3053c 100644 --- a/examples/pyo3-opentelemetry-lib/pyo3_opentelemetry_lib/_tracing_subscriber/layers/file/__init__.py +++ b/examples/pyo3-opentelemetry-lib/pyo3_opentelemetry_lib/_tracing_subscriber/layers/file/__init__.py @@ -12,8 +12,5 @@ from pyo3_opentelemetry_lib._tracing_subscriber.layers import file - __doc__ = file.__doc__ __all__ = getattr(file, "__all__", []) - - diff --git a/examples/pyo3-opentelemetry-lib/pyo3_opentelemetry_lib/_tracing_subscriber/layers/file/__init__.pyi b/examples/pyo3-opentelemetry-lib/pyo3_opentelemetry_lib/_tracing_subscriber/layers/file/__init__.pyi index d1a4ff8..8054827 100644 --- a/examples/pyo3-opentelemetry-lib/pyo3_opentelemetry_lib/_tracing_subscriber/layers/file/__init__.pyi +++ b/examples/pyo3-opentelemetry-lib/pyo3_opentelemetry_lib/_tracing_subscriber/layers/file/__init__.pyi @@ -10,16 +10,18 @@ # * next time the code is generated. * # ***************************************************************************** -from typing import Optional - +from typing import Optional, final +@final class Config: """ Configuration for a `tracing_subscriber::fmt::Layer `_. """ - - def __init__(self, *, file_path: Optional[str] = None, pretty: bool = False, filter: Optional[str] = None, json: bool = True) -> None: + + def __new__( + cls, *, file_path: Optional[str] = None, pretty: bool = False, filter: Optional[str] = None, json: bool = True + ) -> "Config": """ Create a new `Config`. @@ -33,9 +35,7 @@ class Config: If not specified, this will first check the `PYO3_TRACING_SUBSCRIBER_ENV_FILTER` environment variable and then `RUST_LOG` environment variable. If all of these values are empty, no spans - will be exported. + will be exported. :param json: Whether or not to format the output as JSON. Defaults to `True`. """ ... - - diff --git a/examples/pyo3-opentelemetry-lib/pyo3_opentelemetry_lib/_tracing_subscriber/layers/otel_otlp/__init__.py b/examples/pyo3-opentelemetry-lib/pyo3_opentelemetry_lib/_tracing_subscriber/layers/otel_otlp/__init__.py index cd94b23..efccc22 100644 --- a/examples/pyo3-opentelemetry-lib/pyo3_opentelemetry_lib/_tracing_subscriber/layers/otel_otlp/__init__.py +++ b/examples/pyo3-opentelemetry-lib/pyo3_opentelemetry_lib/_tracing_subscriber/layers/otel_otlp/__init__.py @@ -12,7 +12,5 @@ from pyo3_opentelemetry_lib._tracing_subscriber.layers import otel_otlp - __doc__ = otel_otlp.__doc__ __all__ = getattr(otel_otlp, "__all__", []) - diff --git a/examples/pyo3-opentelemetry-lib/pyo3_opentelemetry_lib/_tracing_subscriber/layers/otel_otlp/__init__.pyi b/examples/pyo3-opentelemetry-lib/pyo3_opentelemetry_lib/_tracing_subscriber/layers/otel_otlp/__init__.pyi index 61c3603..363df0f 100644 --- a/examples/pyo3-opentelemetry-lib/pyo3_opentelemetry_lib/_tracing_subscriber/layers/otel_otlp/__init__.pyi +++ b/examples/pyo3-opentelemetry-lib/pyo3_opentelemetry_lib/_tracing_subscriber/layers/otel_otlp/__init__.pyi @@ -10,21 +10,23 @@ # * next time the code is generated. * # ***************************************************************************** -from typing import Dict, List, Optional, Union +from __future__ import annotations +from typing import TYPE_CHECKING, Dict, Optional, final +@final class SpanLimits: - def __init__( - self, + def __new__( + cls, *, max_events_per_span: Optional[int] = None, max_attributes_per_span: Optional[int] = None, max_links_per_span: Optional[int] = None, max_attributes_per_event: Optional[int] = None, max_attributes_per_link: Optional[int] = None, - ) -> None: ... + ) -> "SpanLimits": ... """ - + :param max_events_per_span: The max events that can be added to a `Span`. :param max_attributes_per_span: The max attributes that can be added to a `Span`. :param max_links_per_span: The max links that can be added to a `Span`. @@ -32,67 +34,49 @@ class SpanLimits: :param max_attributes_per_link: The max attributes that can be added to a `Link`. """ - -ResourceValueArray = Union[List[bool], List[int], List[float], List[str]] -""" -An array of `ResourceValue`s. This array is homogenous, so all values must be of the same type. -""" - -ResourceValue = Union[bool, int, float, str, ResourceValueArray] -""" -A value that can be added to a `Resource`. -""" - - +@final class Resource: """ A `Resource` is a representation of the entity producing telemetry. This should represent the Python process starting the tracing subscription process. """ - def __init__( - self, + + def __new__( + cls, *, - attrs: Optional[Dict[str, ResourceValue]] = None, + attrs: Optional[Dict[str, "ResourceValue"]] = None, schema_url: Optional[str] = None, - ) -> None: ... - - -Sampler = Union[bool, float] -""" -A `Sampler` is a representation of the sampling strategy to use. If this is a `bool`, it will -either sample all traces (`True`) or none of them (`False`). If this is a `float`, it will sample -traces at the given rate. -""" - + ) -> "Resource": ... +@final class Config: """ A configuration for `opentelemetry-otlp `_ layer. In addition to the values specified at initialization, this configuration will also respect the - canonical `OpenTelemetry OTLP environment variables + canonical `OpenTelemetry OTLP environment variables `_ that are `supported by opentelemetry-otlp `_. """ - def __init__( - self, + def __new__( + cls, *, span_limits: Optional[SpanLimits] = None, resource: Optional[Resource] = None, metadata_map: Optional[Dict[str, str]] = None, - sampler: Optional[Sampler] = None, + sampler: Optional["Sampler"] = None, endpoint: Optional[str] = None, timeout_millis: Optional[int] = None, pre_shutdown_timeout_millis: Optional[int] = 2000, filter: Optional[str] = None, - ) -> None: + ) -> "Config": """ Initializes a new `Config`. :param span_limits: The limits to apply to span exports. :param resource: The OpenTelemetry resource to attach to all exported spans. :param metadata_map: A map of metadata to attach to all exported spans. This is a map of key value pairs - that may be set as gRPC metadata by the tonic library. + that may be set as gRPC metadata by the tonic library. :param sampler: The sampling strategy to use. See documentation for `Sampler` for more information. :param endpoint: The endpoint to export to. This should be a valid URL. If not specified, this should be specified by environment variables (see `Config` documentation). @@ -104,10 +88,30 @@ class Config: :param filter: A filter string to use for this layer. This uses the same format as the `tracing_subscriber::filter::EnvFilter `_. - In summary, each directive takes the form `target[span{field=value}]=level`, where `target` is roughly the + In summary, each directive takes the form `target[span{field=value}]=level`, where `target` is roughly the Rust namespace and _only_ `level` is required. If not specified, this will first check the `PYO3_TRACING_SUBSCRIBER_ENV_FILTER` environment variable - and then `RUST_LOG` environment variable. If all of these values are empty, no spans will be exported. + and then `RUST_LOG` environment variable. If all of these values are empty, no spans will be exported. """ ... + +if TYPE_CHECKING: + from typing import List, TypeAlias, Union + + ResourceValueArray: TypeAlias = Union[List[bool], List[int], List[float], List[str]] + """ + An array of `ResourceValue`s. This array is homogenous, so all values must be of the same type. + """ + + ResourceValue: TypeAlias = Union[bool, int, float, str, ResourceValueArray] + """ + A value that can be added to a `Resource`. + """ + + Sampler: TypeAlias = Union[bool, float] + """ + A `Sampler` is a representation of the sampling strategy to use. If this is a `bool`, it will + either sample all traces (`True`) or none of them (`False`). If this is a `float`, it will sample + traces at the given rate. + """ diff --git a/examples/pyo3-opentelemetry-lib/pyo3_opentelemetry_lib/_tracing_subscriber/layers/otel_otlp_file/__init__.py b/examples/pyo3-opentelemetry-lib/pyo3_opentelemetry_lib/_tracing_subscriber/layers/otel_otlp_file/__init__.py index db105d0..e28b7c3 100644 --- a/examples/pyo3-opentelemetry-lib/pyo3_opentelemetry_lib/_tracing_subscriber/layers/otel_otlp_file/__init__.py +++ b/examples/pyo3-opentelemetry-lib/pyo3_opentelemetry_lib/_tracing_subscriber/layers/otel_otlp_file/__init__.py @@ -12,7 +12,5 @@ from pyo3_opentelemetry_lib._tracing_subscriber.layers import otel_otlp_file - __doc__ = otel_otlp_file.__doc__ __all__ = getattr(otel_otlp_file, "__all__", []) - diff --git a/examples/pyo3-opentelemetry-lib/pyo3_opentelemetry_lib/_tracing_subscriber/layers/otel_otlp_file/__init__.pyi b/examples/pyo3-opentelemetry-lib/pyo3_opentelemetry_lib/_tracing_subscriber/layers/otel_otlp_file/__init__.pyi index 45330c4..31e2f49 100644 --- a/examples/pyo3-opentelemetry-lib/pyo3_opentelemetry_lib/_tracing_subscriber/layers/otel_otlp_file/__init__.pyi +++ b/examples/pyo3-opentelemetry-lib/pyo3_opentelemetry_lib/_tracing_subscriber/layers/otel_otlp_file/__init__.pyi @@ -10,26 +10,25 @@ # * next time the code is generated. * # ***************************************************************************** -from typing import Optional - +from typing import Optional, final +@final class Config: """ - A configuration for `opentelemetry-stdout `_ + A configuration for `opentelemetry-stdout `_ layer. """ - def __init__(self, *, file_path: Optional[str] = None, filter: Optional[str] = None) -> None: + def __new__(cls, *, file_path: Optional[str] = None, filter: Optional[str] = None) -> "Config": """ :param file_path: The path to the file to write to. If not specified, defaults to stdout. :param filter: A filter string to use for this layer. This uses the same format as the `tracing_subscriber::filter::EnvFilter - `_. + `_. In summary, each directive takes the form `target[span{field=value}]=level`, where `target` is roughly the Rust namespace and _only_ `level` is required. If not specified, this will first check the `PYO3_TRACING_SUBSCRIBER_ENV_FILTER` environment variable - and then `RUST_LOG` environment variable. If all of these values are empty, no spans will be exported. + and then `RUST_LOG` environment variable. If all of these values are empty, no spans will be exported. """ ... - diff --git a/examples/pyo3-opentelemetry-lib/pyo3_opentelemetry_lib/_tracing_subscriber/subscriber/__init__.py b/examples/pyo3-opentelemetry-lib/pyo3_opentelemetry_lib/_tracing_subscriber/subscriber/__init__.py index 5737e29..64d80de 100644 --- a/examples/pyo3-opentelemetry-lib/pyo3_opentelemetry_lib/_tracing_subscriber/subscriber/__init__.py +++ b/examples/pyo3-opentelemetry-lib/pyo3_opentelemetry_lib/_tracing_subscriber/subscriber/__init__.py @@ -12,7 +12,5 @@ from pyo3_opentelemetry_lib._tracing_subscriber import subscriber - __doc__ = subscriber.__doc__ __all__ = getattr(subscriber, "__all__", []) - diff --git a/examples/pyo3-opentelemetry-lib/pyo3_opentelemetry_lib/_tracing_subscriber/subscriber/__init__.pyi b/examples/pyo3-opentelemetry-lib/pyo3_opentelemetry_lib/_tracing_subscriber/subscriber/__init__.pyi index 323cb15..e0894d0 100644 --- a/examples/pyo3-opentelemetry-lib/pyo3_opentelemetry_lib/_tracing_subscriber/subscriber/__init__.pyi +++ b/examples/pyo3-opentelemetry-lib/pyo3_opentelemetry_lib/_tracing_subscriber/subscriber/__init__.pyi @@ -10,13 +10,15 @@ # * next time the code is generated. * # ***************************************************************************** -from .. import layers +from typing import final +from .. import layers +@final class Config: - """ - Configuration for the tracing subscriber. Currently, this only requires a single layer to be - set on the `tracing_subscriber::Registry`. - """ - def __init__(self, *, layer: layers.Config): - ... + """ + Configuration for the tracing subscriber. Currently, this only requires a single layer to be + set on the `tracing_subscriber::Registry`. + """ + + def __new__(cls, *, layer: layers.Config) -> "Config": ... diff --git a/examples/pyo3-opentelemetry-lib/pyo3_opentelemetry_lib/test/tracing_test.py b/examples/pyo3-opentelemetry-lib/pyo3_opentelemetry_lib/test/tracing_test.py index ef9738b..c717545 100644 --- a/examples/pyo3-opentelemetry-lib/pyo3_opentelemetry_lib/test/tracing_test.py +++ b/examples/pyo3-opentelemetry-lib/pyo3_opentelemetry_lib/test/tracing_test.py @@ -136,7 +136,7 @@ async def _test_file_export(config_builder: Callable[[str], TracingConfig], trac datum = json.loads(line) resource_spans += datum["resourceSpans"] - counter = Counter() + counter: Counter[str] = Counter() for resource_span in resource_spans: for scoped_span in resource_span["scopeSpans"]: for span in scoped_span["spans"]: @@ -199,7 +199,7 @@ async def test_file_export_async( datum = json.loads(line) resource_spans += datum["resourceSpans"] - counter = Counter() + counter: Counter[str] = Counter() for resource_span in resource_spans: for scoped_span in resource_span["scopeSpans"]: for span in scoped_span["spans"]: @@ -253,7 +253,7 @@ async def test_otlp_export( _assert_propagated_trace_id_eq(result, trace_id) - counter = Counter() + counter: Counter[str] = Counter() data = otlp_service_data.get(otlp_test_namespace, None) assert data is not None for resource_span in data: @@ -299,7 +299,7 @@ async def test_otlp_export_multi_threads( data = otlp_service_data.get(otlp_test_namespace, None) assert data is not None - counter = Counter() + counter: Counter[str] = Counter() for resource_span in data: for scope_span in resource_span.scope_spans: for span in scope_span.spans: @@ -345,7 +345,7 @@ async def test_otlp_export_async( data = otlp_service_data.get(otlp_test_namespace, None) assert data is not None - counter = Counter() + counter: Counter[str] = Counter() for resource_span in data: for scope_span in resource_span.scope_spans: for span in scope_span.spans: diff --git a/examples/pyo3-opentelemetry-lib/pyproject.toml b/examples/pyo3-opentelemetry-lib/pyproject.toml index 18ea611..2a449db 100644 --- a/examples/pyo3-opentelemetry-lib/pyproject.toml +++ b/examples/pyo3-opentelemetry-lib/pyproject.toml @@ -37,6 +37,7 @@ ruff = "^0.0.261" pytest-asyncio = "^0.21.1" grpcio = "^1.59.0" pytest-forked = "^1.6.0" +mypy = "^1.7.1" [tool.black] line-length = 120 @@ -45,10 +46,19 @@ include = '\.pyi?$' preview= true extend-exclude = ''' ( - "pyo3_opentelemetry_example\/__init__.py", + "pyo3_opentelemetry_lib\/__init__.py", ) ''' +[tool.mypy] + +[[tool.mypy.overrides]] +module = [ + "grpc", + "grpc.aio", +] +ignore_missing_imports = true + [tool.pyright] exclude = [ "pyo3_opentelemetry_lib/__init__.py", @@ -66,7 +76,7 @@ select = [ # isort "I001" ] -src = ["pyo3_opentelemetry_example", "tests"] +src = ["pyo3_opentelemetry_lib", "tests"] exclude = [ ".bzr", ".direnv", From 3f526d46116b663eba3fc67f292edb57ea01abbe Mon Sep 17 00:00:00 2001 From: Eric Hulburd Date: Wed, 29 Nov 2023 17:08:17 -0800 Subject: [PATCH 3/7] chore: new clippy lint --- .github/workflows/checks.yml | 2 +- crates/tracing-subscriber/src/contextmanager.rs | 14 ++++++++------ 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 0eb6b7b..6948509 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -17,7 +17,7 @@ jobs: rust: - stable # the scripts seem to inevitably involve building something that requires a newer version - # or rust, so we just run against stable for now. + # of rust, so we just run against stable for now. # - 1.60.0 steps: # Checkout, setup Rust tools, etc. diff --git a/crates/tracing-subscriber/src/contextmanager.rs b/crates/tracing-subscriber/src/contextmanager.rs index 0cab1f0..2238aef 100644 --- a/crates/tracing-subscriber/src/contextmanager.rs +++ b/crates/tracing-subscriber/src/contextmanager.rs @@ -121,9 +121,10 @@ impl Tracing { .map_err(ToPythonError::to_py_err)?, ); } else { - return Err(ContextManagerError::EnterWithoutConfiguration) - .map_err(RustContextManagerError::from) - .map_err(ToPythonError::to_py_err)?; + return Err(RustContextManagerError::from( + ContextManagerError::EnterWithoutConfiguration, + ) + .to_py_err())?; } Ok(()) } @@ -160,9 +161,10 @@ impl Tracing { export_runtime.shutdown_background(); } } else { - return Err(ContextManagerError::ExitWithoutExportProcess) - .map_err(RustContextManagerError::from) - .map_err(ToPythonError::to_py_err)?; + return Err(RustContextManagerError::from( + ContextManagerError::ExitWithoutExportProcess, + ) + .to_py_err())?; } Ok(()) From c02ca15800732c450276354fe66d5c44a812bfa2 Mon Sep 17 00:00:00 2001 From: Eric Hulburd Date: Wed, 29 Nov 2023 17:30:28 -0800 Subject: [PATCH 4/7] chore: drop type alias annotation --- .../tracing-subscriber/assets/python_stubs/__init__.pyi | 6 +++--- .../assets/python_stubs/layers/__init__.pyi | 6 +++--- .../assets/python_stubs/layers/otel_otlp/__init__.pyi | 8 ++++---- .../_tracing_subscriber/__init__.pyi | 6 +++--- .../_tracing_subscriber/layers/__init__.pyi | 4 ++-- .../_tracing_subscriber/layers/otel_otlp/__init__.pyi | 8 ++++---- 6 files changed, 19 insertions(+), 19 deletions(-) diff --git a/crates/tracing-subscriber/assets/python_stubs/__init__.pyi b/crates/tracing-subscriber/assets/python_stubs/__init__.pyi index 240825d..bfacfbe 100644 --- a/crates/tracing-subscriber/assets/python_stubs/__init__.pyi +++ b/crates/tracing-subscriber/assets/python_stubs/__init__.pyi @@ -117,14 +117,14 @@ class Tracing: ): ... if TYPE_CHECKING: - from typing import TypeAlias, Union + from typing import Union - ExportConfig: TypeAlias = Union[BatchConfig, SimpleConfig] + ExportConfig = Union[BatchConfig, SimpleConfig] """ One of `BatchConfig` or `SimpleConfig`. """ - TracingConfig: TypeAlias = Union[CurrentThreadTracingConfig, GlobalTracingConfig] + TracingConfig = Union[CurrentThreadTracingConfig, GlobalTracingConfig] """ One of `CurrentThreadTracingConfig` or `GlobalTracingConfig`. """ diff --git a/crates/tracing-subscriber/assets/python_stubs/layers/__init__.pyi b/crates/tracing-subscriber/assets/python_stubs/layers/__init__.pyi index 95e4e91..f842645 100644 --- a/crates/tracing-subscriber/assets/python_stubs/layers/__init__.pyi +++ b/crates/tracing-subscriber/assets/python_stubs/layers/__init__.pyi @@ -18,13 +18,13 @@ from . import file as file {{#if layer_otel_otlp}}from . import otel_otlp as otel_otlp{{/if}} if TYPE_CHECKING: - from typing import TypeAlias{{#if any_additional_layer}}, Union{{/if}} + {{#if any_additional_layer}}from typing import Union{{/if}} - {{#if any_additional_layer}}Config: TypeAlias = Union[ + {{#if any_additional_layer}}Config = Union[ file.Config, {{#if layer_otel_otlp_file }}otel_otlp_file.Config,{{/if}} {{#if layer_otel_otlp }}otel_otlp.Config,{{/if}} - ]{{else}}Config: TypeAlias = file.Config{{/if}} + ]{{else}}Config = file.Config{{/if}} """ One of the supported layer configurations that may be set on the subscriber configuration. """ diff --git a/crates/tracing-subscriber/assets/python_stubs/layers/otel_otlp/__init__.pyi b/crates/tracing-subscriber/assets/python_stubs/layers/otel_otlp/__init__.pyi index 26a95e2..5dde690 100644 --- a/crates/tracing-subscriber/assets/python_stubs/layers/otel_otlp/__init__.pyi +++ b/crates/tracing-subscriber/assets/python_stubs/layers/otel_otlp/__init__.pyi @@ -98,19 +98,19 @@ class Config: ... if TYPE_CHECKING: - from typing import List, TypeAlias, Union + from typing import List, Union - ResourceValueArray: TypeAlias = Union[List[bool], List[int], List[float], List[str]] + ResourceValueArray = Union[List[bool], List[int], List[float], List[str]] """ An array of `ResourceValue`s. This array is homogenous, so all values must be of the same type. """ - ResourceValue: TypeAlias = Union[bool, int, float, str, ResourceValueArray] + ResourceValue= Union[bool, int, float, str, ResourceValueArray] """ A value that can be added to a `Resource`. """ - Sampler: TypeAlias = Union[bool, float] + Sampler = Union[bool, float] """ A `Sampler` is a representation of the sampling strategy to use. If this is a `bool`, it will either sample all traces (`True`) or none of them (`False`). If this is a `float`, it will sample diff --git a/examples/pyo3-opentelemetry-lib/pyo3_opentelemetry_lib/_tracing_subscriber/__init__.pyi b/examples/pyo3-opentelemetry-lib/pyo3_opentelemetry_lib/_tracing_subscriber/__init__.pyi index 240825d..bfacfbe 100644 --- a/examples/pyo3-opentelemetry-lib/pyo3_opentelemetry_lib/_tracing_subscriber/__init__.pyi +++ b/examples/pyo3-opentelemetry-lib/pyo3_opentelemetry_lib/_tracing_subscriber/__init__.pyi @@ -117,14 +117,14 @@ class Tracing: ): ... if TYPE_CHECKING: - from typing import TypeAlias, Union + from typing import Union - ExportConfig: TypeAlias = Union[BatchConfig, SimpleConfig] + ExportConfig = Union[BatchConfig, SimpleConfig] """ One of `BatchConfig` or `SimpleConfig`. """ - TracingConfig: TypeAlias = Union[CurrentThreadTracingConfig, GlobalTracingConfig] + TracingConfig = Union[CurrentThreadTracingConfig, GlobalTracingConfig] """ One of `CurrentThreadTracingConfig` or `GlobalTracingConfig`. """ diff --git a/examples/pyo3-opentelemetry-lib/pyo3_opentelemetry_lib/_tracing_subscriber/layers/__init__.pyi b/examples/pyo3-opentelemetry-lib/pyo3_opentelemetry_lib/_tracing_subscriber/layers/__init__.pyi index 09fe32f..50d94c1 100644 --- a/examples/pyo3-opentelemetry-lib/pyo3_opentelemetry_lib/_tracing_subscriber/layers/__init__.pyi +++ b/examples/pyo3-opentelemetry-lib/pyo3_opentelemetry_lib/_tracing_subscriber/layers/__init__.pyi @@ -19,9 +19,9 @@ from . import otel_otlp as otel_otlp from . import otel_otlp_file as otel_otlp_file if TYPE_CHECKING: - from typing import TypeAlias, Union + from typing import Union - Config: TypeAlias = Union[ + Config = Union[ file.Config, otel_otlp_file.Config, otel_otlp.Config, diff --git a/examples/pyo3-opentelemetry-lib/pyo3_opentelemetry_lib/_tracing_subscriber/layers/otel_otlp/__init__.pyi b/examples/pyo3-opentelemetry-lib/pyo3_opentelemetry_lib/_tracing_subscriber/layers/otel_otlp/__init__.pyi index 363df0f..6accc02 100644 --- a/examples/pyo3-opentelemetry-lib/pyo3_opentelemetry_lib/_tracing_subscriber/layers/otel_otlp/__init__.pyi +++ b/examples/pyo3-opentelemetry-lib/pyo3_opentelemetry_lib/_tracing_subscriber/layers/otel_otlp/__init__.pyi @@ -97,19 +97,19 @@ class Config: ... if TYPE_CHECKING: - from typing import List, TypeAlias, Union + from typing import List, Union - ResourceValueArray: TypeAlias = Union[List[bool], List[int], List[float], List[str]] + ResourceValueArray = Union[List[bool], List[int], List[float], List[str]] """ An array of `ResourceValue`s. This array is homogenous, so all values must be of the same type. """ - ResourceValue: TypeAlias = Union[bool, int, float, str, ResourceValueArray] + ResourceValue = Union[bool, int, float, str, ResourceValueArray] """ A value that can be added to a `Resource`. """ - Sampler: TypeAlias = Union[bool, float] + Sampler = Union[bool, float] """ A `Sampler` is a representation of the sampling strategy to use. If this is a `bool`, it will either sample all traces (`True`) or none of them (`False`). If this is a `float`, it will sample From 3ad361837e6a8c8370b84661c7946503be9b35b8 Mon Sep 17 00:00:00 2001 From: Eric Hulburd Date: Wed, 29 Nov 2023 18:09:01 -0800 Subject: [PATCH 5/7] docs: fix readme code block highlighting --- README.md | 2 +- crates/opentelemetry/README.md | 2 +- crates/tracing-subscriber/README.md | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 6133c8b..783b468 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ For a functional example of usage of all of these crates, see [examples/pyo3-ope It should be sufficient to [install the Rust toolchain](https://rustup.rs/) and [cargo-make](https://github.com/sagiegurari/cargo-make). Then: -```sh +```shell cargo make check-all ``` diff --git a/crates/opentelemetry/README.md b/crates/opentelemetry/README.md index ba8d9e5..d7e7725 100644 --- a/crates/opentelemetry/README.md +++ b/crates/opentelemetry/README.md @@ -18,7 +18,7 @@ pyo3_opentelemetry provides a macro to simply and easily instrument your PyO3 bi From Rust: -```rs +```rust use pyo3_opentelemetry::prelude::*; use pyo3::prelude::*; use tracing::instrument; diff --git a/crates/tracing-subscriber/README.md b/crates/tracing-subscriber/README.md index a6d09ff..d1e94c6 100644 --- a/crates/tracing-subscriber/README.md +++ b/crates/tracing-subscriber/README.md @@ -21,7 +21,7 @@ Given a `pyo3` extension module named "my_module" that would like to expose the tracing subscriber configuration and context manager classes from "my_module._tracing_subscriber", from Rust: -```rs +```rust use pyo3::prelude::*; #[pymodule] @@ -41,7 +41,7 @@ fn my_module(py: Python, m: &PyModule) -> PyResult<()> { Then a user could initialize a tracing subscriber that logged to stdout from Python: -```py +```python import my_module from my_module._tracing_subscriber import ( GlobalTracingConfig, @@ -74,7 +74,7 @@ This crate provides a convenient method for adding stub files to your Python sou Given a `pyo3` extension module named "my_module" that uses the `pyo3-tracing-subscriber` crate to expose tracing subscriber configuration and context manager classes from "my_module._tracing_subscriber", in the upstream `build.rs` file: -```rs +```rust use pyo3_tracing_subscriber_stubs::write_stub_files; fn main() { From fae6797880d72c4fe4cddd14d813e6f0cae57e14 Mon Sep 17 00:00:00 2001 From: Eric Hulburd Date: Thu, 30 Nov 2023 11:23:43 -0800 Subject: [PATCH 6/7] fix: fix wait for macros crate --- .github/workflows/publish.yml | 4 ++-- scripts/ci/assert-macros-crate-published.sh | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index b6071dc..2139758 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -8,7 +8,7 @@ on: jobs: opentelemetry: runs-on: ubuntu-latest - if: startsWith(github.ref, 'refs/tags/opentelemetry') + if: startsWith(github.ref, 'refs/tags/opentelemetry/v') steps: - uses: actions/checkout@v3 with: @@ -29,7 +29,7 @@ jobs: - run: cargo publish -p pyo3-opentelemetry --token ${{ secrets.CRATES_IO_TOKEN }} opentelemetry-macros: runs-on: ubuntu-latest - if: startsWith(github.ref, 'refs/tags/opentelemetry-macros') + if: startsWith(github.ref, 'refs/tags/opentelemetry-macros/v') steps: - uses: actions/checkout@v3 with: diff --git a/scripts/ci/assert-macros-crate-published.sh b/scripts/ci/assert-macros-crate-published.sh index e35d826..3642c87 100755 --- a/scripts/ci/assert-macros-crate-published.sh +++ b/scripts/ci/assert-macros-crate-published.sh @@ -7,7 +7,7 @@ set -e DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" -cd ${DIR}/../../crates/macros +cd ${DIR}/../../crates/opentelemetry-macros CRATE_ID=pyo3-opentelemetry-macros VERSION=$(yq -r -oj .package.version Cargo.toml) From bff35f7f7091339855ac8d00f3d734ac7f86982c Mon Sep 17 00:00:00 2001 From: Eric Hulburd Date: Thu, 30 Nov 2023 12:19:39 -0800 Subject: [PATCH 7/7] refactor(tracing-subscriber): add submodule should add to parent module --- crates/tracing-subscriber/README.md | 7 ++--- crates/tracing-subscriber/src/lib.rs | 32 ++++++++++++++-------- examples/pyo3-opentelemetry-lib/src/lib.rs | 8 +----- 3 files changed, 24 insertions(+), 23 deletions(-) diff --git a/crates/tracing-subscriber/README.md b/crates/tracing-subscriber/README.md index d1e94c6..6faf4aa 100644 --- a/crates/tracing-subscriber/README.md +++ b/crates/tracing-subscriber/README.md @@ -28,13 +28,12 @@ use pyo3::prelude::*; fn my_module(py: Python, m: &PyModule) -> PyResult<()> { // Add your own Python classes, functions and modules. - let tracing_subscriber = PyModule::new(py, "_tracing_subscriber")?; pyo3_tracing_subscriber::add_submodule( - "my_module._tracing_subscriber", + "my_module", + "_tracing_subscriber", py, - tracing_subscriber, + m, )?; - m.add_submodule(tracing_subscriber)?; Ok(()) } ``` diff --git a/crates/tracing-subscriber/src/lib.rs b/crates/tracing-subscriber/src/lib.rs index cc6ed48..9e3ab99 100644 --- a/crates/tracing-subscriber/src/lib.rs +++ b/crates/tracing-subscriber/src/lib.rs @@ -90,13 +90,12 @@ //! #[pymodule] //! fn example(_py: Python, m: &PyModule) -> PyResult<()> { //! // add your functions, modules, and classes -//! let tracing_subscriber = PyModule::new(py, TRACING_SUBSCRIBER_SUBMODULE_NAME)?; //! pyo3_tracing_subscriber::add_submodule( -//! format!("{MY_PACKAGE_NAME}.{TRACING_SUBSCRIBER_SUBMODULE_NAME}"), +//! MY_PACKAGE_NAME, +//! TRACING_SUBSCRIBER_SUBMODULE_NAME, //! py, -//! tracing_subscriber, +//! m, //! )?; -//! m.add_submodule(tracing_subscriber)?; //! Ok(()) //! } //! ``` @@ -157,12 +156,13 @@ create_init_submodule! { /// /// # Arguments /// -/// * `name` - the fully qualified name of the tracing subscriber submodule within your Python -/// package. For instance, if your package is named `my_package` and you want to add the tracing -/// subscriber submodule `tracing_subscriber`, then `name` should be -/// `my_package.tracing_subscriber`. +/// * `fully_qualified_namespace` - the fully qualified namespace of the parent Python module to +/// which the tracing submodule should be added. This may be a nested namespace, such as +/// `my_package.my_module`. +/// * `name` - the name of the tracing subscriber submodule within the specified parent module. +/// This should not be a nested namespace. /// * `py` - the Python GIL token. -/// * `m` - the parent module to which the tracing subscriber submodule should be added. +/// * `parent_module` - the parent module to which the tracing subscriber submodule should be added. /// /// # Errors /// @@ -206,9 +206,17 @@ create_init_submodule! { /// /// For detailed Python usage documentation, see the stub files written by /// [`pyo3_tracing_subscriber::stubs::write_stub_files`]. -pub fn add_submodule(name: &str, py: Python, m: &PyModule) -> PyResult<()> { - init_submodule(name, py, m)?; +pub fn add_submodule( + fully_qualified_namespace: &str, + name: &str, + py: Python, + parent_module: &PyModule, +) -> PyResult<()> { + let tracing_subscriber = PyModule::new(py, name)?; + let fully_qualified_name = format!("{fully_qualified_namespace}.{name}"); + init_submodule(&fully_qualified_name, py, tracing_subscriber)?; let modules = py.import("sys")?.getattr("modules")?; - modules.set_item(name, m)?; + modules.set_item(fully_qualified_name, tracing_subscriber)?; + parent_module.add_submodule(tracing_subscriber)?; Ok(()) } diff --git a/examples/pyo3-opentelemetry-lib/src/lib.rs b/examples/pyo3-opentelemetry-lib/src/lib.rs index 266c350..36eac29 100644 --- a/examples/pyo3-opentelemetry-lib/src/lib.rs +++ b/examples/pyo3-opentelemetry-lib/src/lib.rs @@ -133,12 +133,6 @@ fn pyo3_opentelemetry_lib(py: Python, m: &PyModule) -> PyResult<()> { m.add_function(wrap_pyfunction!(example_function, m)?)?; m.add_function(wrap_pyfunction!(example_function_async, m)?)?; - let tracing_subscriber = PyModule::new(py, "_tracing_subscriber")?; - pyo3_tracing_subscriber::add_submodule( - "pyo3_opentelemetry_lib._tracing_subscriber", - py, - tracing_subscriber, - )?; - m.add_submodule(tracing_subscriber)?; + pyo3_tracing_subscriber::add_submodule("pyo3_opentelemetry_lib", "_tracing_subscriber", py, m)?; Ok(()) }