From 267a20f67e5658cd520db26714d40df5518e6139 Mon Sep 17 00:00:00 2001 From: Yagiz Nizipli Date: Thu, 16 May 2024 15:26:51 -0400 Subject: [PATCH] feat: migrate to pyo3 --- .codecov.yml | 1 - .github/workflows/build_library.yml | 64 +- Cargo.lock | 219 ++++-- Cargo.toml | 5 +- py/.gitignore | 2 +- py/CHANGELOG.md | 1 + py/pyproject.toml | 21 + py/requirements-build.txt | 3 + py/sentry_relay/__init__.py | 11 +- py/sentry_relay/__init__.pyi | 4 + py/sentry_relay/_relay_pyo3.pyi | 0 py/sentry_relay/auth.py | 149 ---- py/sentry_relay/auth.pyi | 32 + py/sentry_relay/consts.py | 124 ---- py/sentry_relay/consts.pyi | 61 ++ py/sentry_relay/exceptions.py | 65 -- py/sentry_relay/exceptions.pyi | 16 + py/sentry_relay/processing.py | 373 ---------- py/sentry_relay/processing.pyi | 150 ++++ py/sentry_relay/utils.py | 113 --- py/setup.py | 121 --- py/tests/test_consts.py | 26 +- py/tests/test_processing.py | 15 +- py/tests/test_utils.py | 10 - relay-base-schema/Cargo.toml | 1 + relay-base-schema/src/data_category.rs | 56 ++ relay-cabi/.gitignore | 1 - relay-cabi/Makefile | 20 - relay-cabi/cbindgen.toml | 22 - relay-cabi/include/relay.h | 672 ----------------- relay-cabi/src/auth.rs | 207 ------ relay-cabi/src/constants.rs | 32 - relay-cabi/src/core.rs | 208 ------ relay-cabi/src/ffi.rs | 137 ---- relay-cabi/src/lib.rs | 112 --- relay-cabi/src/processing.rs | 562 -------------- relay-dynamic-config/Cargo.toml | 1 + relay-dynamic-config/src/global.rs | 2 + relay-event-normalization/Cargo.toml | 1 + relay-event-normalization/src/geo.rs | 59 +- .../src/normalize/breakdowns.rs | 2 + relay-event-schema/Cargo.toml | 1 + relay-event-schema/src/processor/chunks.rs | 25 +- relay-event-schema/src/processor/traits.rs | 8 + relay-event-schema/src/protocol/event.rs | 2 + relay-event-schema/src/protocol/types.rs | 2 + relay-event-schema/src/protocol/user.rs | 2 + relay-ffi-macros/Cargo.toml | 20 - relay-ffi-macros/src/lib.rs | 66 -- relay-ffi/Cargo.toml | 17 - relay-ffi/src/lib.rs | 289 -------- relay-ffi/tests/test_macro.rs | 51 -- relay-pii/Cargo.toml | 1 + relay-pii/src/config.rs | 2 + relay-pii/src/generate_selectors.rs | 2 + relay-pii/src/legacy.rs | 2 + relay-protocol/Cargo.toml | 2 + relay-protocol/src/meta.rs | 4 + {relay-cabi => relay-pyo3}/Cargo.toml | 15 +- relay-pyo3/src/auth.rs | 196 +++++ {relay-cabi => relay-pyo3}/src/codeowners.rs | 29 +- relay-pyo3/src/consts.rs | 132 ++++ relay-pyo3/src/exceptions.rs | 172 +++++ relay-pyo3/src/lib.rs | 33 + relay-pyo3/src/processing.rs | 691 ++++++++++++++++++ relay-pyo3/src/utils.rs | 16 + 66 files changed, 1950 insertions(+), 3511 deletions(-) create mode 100644 py/pyproject.toml create mode 100644 py/requirements-build.txt create mode 100644 py/sentry_relay/__init__.pyi create mode 100644 py/sentry_relay/_relay_pyo3.pyi delete mode 100644 py/sentry_relay/auth.py create mode 100644 py/sentry_relay/auth.pyi delete mode 100644 py/sentry_relay/consts.py create mode 100644 py/sentry_relay/consts.pyi delete mode 100644 py/sentry_relay/exceptions.py create mode 100644 py/sentry_relay/exceptions.pyi delete mode 100644 py/sentry_relay/processing.py create mode 100644 py/sentry_relay/processing.pyi delete mode 100644 py/sentry_relay/utils.py delete mode 100644 py/setup.py delete mode 100644 py/tests/test_utils.py delete mode 100644 relay-cabi/.gitignore delete mode 100644 relay-cabi/Makefile delete mode 100644 relay-cabi/cbindgen.toml delete mode 100644 relay-cabi/include/relay.h delete mode 100644 relay-cabi/src/auth.rs delete mode 100644 relay-cabi/src/constants.rs delete mode 100644 relay-cabi/src/core.rs delete mode 100644 relay-cabi/src/ffi.rs delete mode 100644 relay-cabi/src/lib.rs delete mode 100644 relay-cabi/src/processing.rs delete mode 100644 relay-ffi-macros/Cargo.toml delete mode 100644 relay-ffi-macros/src/lib.rs delete mode 100644 relay-ffi/Cargo.toml delete mode 100644 relay-ffi/src/lib.rs delete mode 100644 relay-ffi/tests/test_macro.rs rename {relay-cabi => relay-pyo3}/Cargo.toml (82%) create mode 100644 relay-pyo3/src/auth.rs rename {relay-cabi => relay-pyo3}/src/codeowners.rs (89%) create mode 100644 relay-pyo3/src/consts.rs create mode 100644 relay-pyo3/src/exceptions.rs create mode 100644 relay-pyo3/src/lib.rs create mode 100644 relay-pyo3/src/processing.rs create mode 100644 relay-pyo3/src/utils.rs diff --git a/.codecov.yml b/.codecov.yml index fe14ca4af9b..74a2e486726 100644 --- a/.codecov.yml +++ b/.codecov.yml @@ -1,3 +1,2 @@ ignore: - - "relay-cabi/**/*.rs" - "relay-general/derive/**/*.rs" diff --git a/.github/workflows/build_library.yml b/.github/workflows/build_library.yml index 9dbb1262dc9..aec4795940c 100644 --- a/.github/workflows/build_library.yml +++ b/.github/workflows/build_library.yml @@ -14,11 +14,11 @@ jobs: strategy: fail-fast: false matrix: - build-arch: + target: - x86_64 - aarch64 - name: Python Linux ${{ matrix.build-arch }} + name: Python Linux ${{ matrix.target }} runs-on: ubuntu-latest steps: @@ -26,15 +26,18 @@ jobs: with: submodules: recursive - - if: matrix.build-arch == 'aarch64' - uses: docker/setup-qemu-action@v3 - with: - platforms: arm64 + - run: rustup toolchain install stable --profile minimal --no-self-update + + - uses: actions/setup-python@v5 - - name: Build in Docker - run: scripts/docker-manylinux.sh - env: - TARGET: ${{ matrix.build-arch }} + - name: Build wheels + uses: PyO3/maturin-action@v1 + working-directory: py + with: + target: ${{ matrix.target }} + args: --release --out dist --find-interpreter + sccache: "true" + manylinux: auto - uses: actions/upload-artifact@v3 with: @@ -45,39 +48,29 @@ jobs: strategy: fail-fast: false matrix: - include: - - target: x86_64-apple-darwin - py-platform: macosx-10_15_x86_64 - - target: aarch64-apple-darwin - py-platform: macosx-11_0_arm64 + target: + - x86_64 + - aarch64 - name: Python macOS ${{ matrix.py-platform }} - runs-on: macos-11 + name: Python macOS ${{ matrix.target }} + runs-on: macos-latest steps: - uses: actions/checkout@v4 with: submodules: recursive - - name: Install Rust Toolchain - run: | - rustup set profile minimal - rustup toolchain install stable - rustup override set stable - rustup target add --toolchain stable ${{ matrix.target }} + - run: rustup toolchain install stable --profile minimal --no-self-update - uses: actions/setup-python@v5 - with: - python-version: '3.10' - - name: Build Wheel - run: | - pip install wheel - python setup.py bdist_wheel -p ${{ matrix.py-platform }} + - name: Build wheels + uses: PyO3/maturin-action@v1 working-directory: py - env: - # consumed by cargo and setup.py to obtain the target dir - CARGO_BUILD_TARGET: ${{ matrix.target }} + with: + target: ${{ matrix.target }} + args: --release --out dist --find-interpreter + sccache: "true" - uses: actions/upload-artifact@v3 with: @@ -94,12 +87,13 @@ jobs: submodules: recursive - uses: actions/setup-python@v5 - with: - python-version: '3.10' - name: Build sdist - run: python setup.py sdist --format=zip + uses: PyO3/maturin-action@v1 working-directory: py + with: + command: sdist + args: --out dist - uses: actions/upload-artifact@v3 with: diff --git a/Cargo.lock b/Cargo.lock index 7b17791021e..04993453adc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -894,7 +894,7 @@ dependencies = [ "autocfg", "cfg-if", "crossbeam-utils", - "memoffset", + "memoffset 0.7.1", "scopeguard", ] @@ -2209,6 +2209,12 @@ dependencies = [ "hashbrown 0.14.3", ] +[[package]] +name = "indoc" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5" + [[package]] name = "insta" version = "1.31.0" @@ -2577,6 +2583,15 @@ dependencies = [ "autocfg", ] +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + [[package]] name = "mime" version = "0.3.16" @@ -3224,6 +3239,12 @@ dependencies = [ "plotters-backend", ] +[[package]] +name = "portable-atomic" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7170ef9988bc169ba16dd36a7fa041e5c4cbeb6a35b76d4c03daded371eae7c0" + [[package]] name = "ppv-lite86" version = "0.2.17" @@ -3363,6 +3384,72 @@ version = "2.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33cb294fe86a74cbcf50d4445b37da762029549ebeea341421c7c70370f86cac" +[[package]] +name = "pyo3" +version = "0.21.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e00b96a521718e08e03b1a622f01c8a8deb50719335de3f60b3b3950f069d8" +dependencies = [ + "anyhow", + "cfg-if", + "chrono", + "indoc", + "libc", + "memoffset 0.9.1", + "parking_lot", + "portable-atomic", + "pyo3-build-config", + "pyo3-ffi", + "pyo3-macros", + "serde", + "unindent", +] + +[[package]] +name = "pyo3-build-config" +version = "0.21.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7883df5835fafdad87c0d888b266c8ec0f4c9ca48a5bed6bbb592e8dedee1b50" +dependencies = [ + "once_cell", + "target-lexicon", +] + +[[package]] +name = "pyo3-ffi" +version = "0.21.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01be5843dc60b916ab4dad1dca6d20b9b4e6ddc8e15f50c47fe6d85f1fb97403" +dependencies = [ + "libc", + "pyo3-build-config", +] + +[[package]] +name = "pyo3-macros" +version = "0.21.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77b34069fc0682e11b31dbd10321cbf94808394c56fd996796ce45217dfac53c" +dependencies = [ + "proc-macro2", + "pyo3-macros-backend", + "quote", + "syn 2.0.38", +] + +[[package]] +name = "pyo3-macros-backend" +version = "0.21.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08260721f32db5e1a5beae69a55553f56b99bd0e1c3e6e0a5e8851a9d0f5a85c" +dependencies = [ + "heck", + "proc-macro2", + "pyo3-build-config", + "quote", + "syn 2.0.38", +] + [[package]] name = "quick-error" version = "1.2.3" @@ -3622,6 +3709,7 @@ dependencies = [ name = "relay-base-schema" version = "24.4.2" dependencies = [ + "pyo3", "regex", "relay-common", "relay-protocol", @@ -3630,33 +3718,6 @@ dependencies = [ "serde_json", ] -[[package]] -name = "relay-cabi" -version = "0.8.64" -dependencies = [ - "anyhow", - "chrono", - "json-forensics", - "lru", - "once_cell", - "regex", - "relay-auth", - "relay-base-schema", - "relay-cardinality", - "relay-common", - "relay-dynamic-config", - "relay-event-normalization", - "relay-event-schema", - "relay-ffi", - "relay-pii", - "relay-protocol", - "relay-sampling", - "sentry-release-parser", - "serde", - "serde_json", - "uuid", -] - [[package]] name = "relay-cardinality" version = "24.4.2" @@ -3749,6 +3810,7 @@ dependencies = [ "anyhow", "indexmap 2.2.5", "insta", + "pyo3", "relay-auth", "relay-base-schema", "relay-cardinality", @@ -3789,6 +3851,7 @@ dependencies = [ "md5", "once_cell", "psl", + "pyo3", "regex", "relay-base-schema", "relay-common", @@ -3819,6 +3882,7 @@ dependencies = [ "debugid", "enumset", "insta", + "pyo3", "relay-base-schema", "relay-common", "relay-event-derive", @@ -3834,22 +3898,6 @@ dependencies = [ "uuid", ] -[[package]] -name = "relay-ffi" -version = "24.4.2" -dependencies = [ - "anyhow", - "relay-ffi-macros", -] - -[[package]] -name = "relay-ffi-macros" -version = "24.4.2" -dependencies = [ - "quote", - "syn 1.0.109", -] - [[package]] name = "relay-filter" version = "24.4.2" @@ -3968,6 +4016,7 @@ dependencies = [ "pest", "pest_derive", "pretty-hex", + "pyo3", "regex", "relay-common", "relay-event-schema", @@ -4009,6 +4058,7 @@ version = "24.4.2" dependencies = [ "insta", "num-traits", + "pyo3", "relay-common", "relay-protocol-derive", "schemars", @@ -4030,6 +4080,35 @@ dependencies = [ "synstructure", ] +[[package]] +name = "relay-pyo3" +version = "0.8.64" +dependencies = [ + "anyhow", + "chrono", + "json-forensics", + "lru", + "once_cell", + "pyo3", + "regex", + "relay-auth", + "relay-base-schema", + "relay-cardinality", + "relay-common", + "relay-dynamic-config", + "relay-event-normalization", + "relay-event-schema", + "relay-pii", + "relay-protocol", + "relay-sampling", + "sentry-release-parser", + "serde", + "serde-pyobject", + "serde_json", + "strum", + "uuid", +] + [[package]] name = "relay-quotas" version = "24.4.2" @@ -4722,13 +4801,23 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.189" +version = "1.0.193" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e422a44e74ad4001bdc8eede9a4570ab52f71190e9c076d14369f38b9200537" +checksum = "25dd9975e68d0cb5aa1120c288333fc98731bd1dd12f561e468ea4728c042b89" dependencies = [ "serde_derive", ] +[[package]] +name = "serde-pyobject" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ffe7ea77d8eba5774068d55b00a4a3a7c6f9fa9b97dd8e3bc8750cc8503fee" +dependencies = [ + "pyo3", + "serde", +] + [[package]] name = "serde-transcode" version = "1.1.1" @@ -4760,9 +4849,9 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.189" +version = "1.0.193" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e48d1f918009ce3145511378cf68d613e3b3d9137d67272562080d68a2b32d5" +checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3" dependencies = [ "proc-macro2", "quote", @@ -5300,6 +5389,28 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +[[package]] +name = "strum" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d8cec3501a5194c432b2b7976db6b7d10ec95c253208b45f83f7136aa985e29" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6cf59daf282c0a494ba14fd21610a0325f9f90ec9d1231dea26bcb1d696c946" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.38", +] + [[package]] name = "subtle" version = "2.4.1" @@ -5391,6 +5502,12 @@ dependencies = [ "windows", ] +[[package]] +name = "target-lexicon" +version = "0.12.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1fc403891a21bcfb7c37834ba66a547a8f402146eba7265b5a6d88059c9ff2f" + [[package]] name = "tempfile" version = "3.5.0" @@ -5955,6 +6072,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" +[[package]] +name = "unindent" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7de7d73e1754487cb58364ee906a499937a0dfabd86bcb980fa99ec8c8fa2ce" + [[package]] name = "unsafe-libyaml" version = "0.2.10" diff --git a/Cargo.toml b/Cargo.toml index ce16f1bbb4d..ffb04e2f6fd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,8 +27,6 @@ relay-crash = { path = "relay-crash" } relay-dynamic-config = { path = "relay-dynamic-config" } relay-event-normalization = { path = "relay-event-normalization" } relay-event-schema = { path = "relay-event-schema" } -relay-ffi = { path = "relay-ffi" } -relay-ffi-macros = { path = "relay-ffi-macros" } relay-filter = { path = "relay-filter" } relay-kafka = { path = "relay-kafka" } relay-log = { path = "relay-log" } @@ -118,6 +116,7 @@ pin-project-lite = "0.2.12" pretty-hex = "0.3.0" proc-macro2 = "1.0.8" psl = "2.1.33" +pyo3 = { version = "0.21.2" } quote = "1.0.2" r2d2 = "0.8.10" rand = "0.8.5" @@ -143,6 +142,7 @@ serde-transcode = "1.1.1" serde_bytes = "0.11" serde_json = "1.0.93" serde_path_to_error = "0.1.14" +serde-pyobject = "0.3.0" serde_repr = "0.1.16" serde_test = "1.0.125" serde_urlencoded = "0.7.1" @@ -154,6 +154,7 @@ smallvec = { version = "1.11.2", features = ["serde"] } sqlparser = "0.44.0" sqlx = { version = "0.7.4", default-features = false } statsdproxy = { version = "0.1.2", default-features = false } +strum = { version = "0.26", features = ["derive"] } symbolic-common = { version = "12.1.2", default-features = false } symbolic-unreal = { version = "12.1.2", default-features = false } syn = "1.0.14" diff --git a/py/.gitignore b/py/.gitignore index ae3d63b4421..9485c89ad79 100644 --- a/py/.gitignore +++ b/py/.gitignore @@ -1,5 +1,5 @@ # FFI -sentry_relay/_lowlevel* +sentry_relay/_relay_pyo3.cpython* # Wheels dist diff --git a/py/CHANGELOG.md b/py/CHANGELOG.md index 17e27ce50d5..944f4904d9c 100644 --- a/py/CHANGELOG.md +++ b/py/CHANGELOG.md @@ -3,6 +3,7 @@ ## Unreleased - Remove publishing of x86 wheels. [#3596](https://github.com/getsentry/relay/pull/3596) +- Use more efficient FFI bindings for sentry_relay Python lib. ([#3520](https://github.com/getsentry/relay/pull/3520)) ## 0.8.64 diff --git a/py/pyproject.toml b/py/pyproject.toml new file mode 100644 index 00000000000..310e4d744e4 --- /dev/null +++ b/py/pyproject.toml @@ -0,0 +1,21 @@ +[build-system] +requires = ["maturin>=1.4,<2.0"] +build-backend = "maturin" + +[project] +name = "sentry_relay" +description = "A python library to access sentry relay functionality." +requires-python = ">=3.10" +authors = [ + { email = "hello@sentry.io" } +] +license = { text = "FSL-1.0-Apache-2.0" } +classifiers = [ + "Programming Language :: Rust", + "Programming Language :: Python :: Implementation :: CPython", +] +dynamic = ["version"] + +[tool.maturin] +module-name = "sentry_relay._relay_pyo3" +manifest-path = "../relay-pyo3/Cargo.toml" diff --git a/py/requirements-build.txt b/py/requirements-build.txt new file mode 100644 index 00000000000..fe69a6cbd4f --- /dev/null +++ b/py/requirements-build.txt @@ -0,0 +1,3 @@ +--index-url https://pypi.devinfra.sentry.io/simple + +maturin==1.5.1 diff --git a/py/sentry_relay/__init__.py b/py/sentry_relay/__init__.py index 09fef4d9272..b22197a2e80 100644 --- a/py/sentry_relay/__init__.py +++ b/py/sentry_relay/__init__.py @@ -1,11 +1,16 @@ +from sentry_relay import _relay_pyo3 + __all__ = [] +__doc__ = _relay_pyo3.__doc__ +if hasattr(_relay_pyo3, "__all__"): + __all__ = _relay_pyo3.__all__ -def _import_all(): - import pkgutil +def _import_all(): + submodules = ["auth", "consts", "exceptions", "processing"] glob = globals() - for _, modname, _ in pkgutil.iter_modules(__path__): + for modname in submodules: if modname[:1] == "_": continue mod = __import__("sentry_relay.%s" % modname, glob, glob, ["__name__"]) diff --git a/py/sentry_relay/__init__.pyi b/py/sentry_relay/__init__.pyi new file mode 100644 index 00000000000..930b6da905a --- /dev/null +++ b/py/sentry_relay/__init__.pyi @@ -0,0 +1,4 @@ +from .auth import * +from .exceptions import * +from .consts import * +from .processing import * diff --git a/py/sentry_relay/_relay_pyo3.pyi b/py/sentry_relay/_relay_pyo3.pyi new file mode 100644 index 00000000000..e69de29bb2d diff --git a/py/sentry_relay/auth.py b/py/sentry_relay/auth.py deleted file mode 100644 index 824f656ad53..00000000000 --- a/py/sentry_relay/auth.py +++ /dev/null @@ -1,149 +0,0 @@ -import json -import uuid -from typing import Callable, Any - -from sentry_relay._lowlevel import lib -from sentry_relay.utils import ( - RustObject, - encode_str, - decode_str, - decode_uuid, - rustcall, - make_buf, -) -from sentry_relay.exceptions import UnpackErrorBadSignature - - -__all__ = [ - "PublicKey", - "SecretKey", - "generate_key_pair", - "create_register_challenge", - "validate_register_response", - "is_version_supported", -] - - -class PublicKey(RustObject): - __dealloc_func__ = lib.relay_publickey_free - - @classmethod - def parse(cls, string): - s = encode_str(string) - ptr = rustcall(lib.relay_publickey_parse, s) - return cls._from_objptr(ptr) - - def verify(self, buf, sig, max_age=None): - buf = make_buf(buf) - sig = encode_str(sig) - if max_age is None: - return self._methodcall(lib.relay_publickey_verify, buf, sig) - return self._methodcall(lib.relay_publickey_verify_timestamp, buf, sig, max_age) - - def unpack( - self, - buf, - sig, - max_age=None, - json_loads: Callable[[str | bytes], Any] = json.loads, - ): - if not self.verify(buf, sig, max_age): - raise UnpackErrorBadSignature("invalid signature") - return json_loads(buf) - - def __str__(self): - return decode_str(self._methodcall(lib.relay_publickey_to_string), free=True) - - def __repr__(self): - return f"<{self.__class__.__name__} {str(self)!r}>" - - -class SecretKey(RustObject): - __dealloc_func__ = lib.relay_secretkey_free - - @classmethod - def parse(cls, string): - s = encode_str(string) - ptr = rustcall(lib.relay_secretkey_parse, s) - return cls._from_objptr(ptr) - - def sign(self, value): - buf = make_buf(value) - return decode_str(self._methodcall(lib.relay_secretkey_sign, buf), free=True) - - def pack(self, data): - # TODO(@anonrig): Look into separators requirement - packed = json.dumps(data, separators=(",", ":")).encode() - return packed, self.sign(packed) - - def __str__(self): - return decode_str(self._methodcall(lib.relay_secretkey_to_string), free=True) - - def __repr__(self): - return f"<{self.__class__.__name__} {str(self)!r}>" - - -def generate_key_pair(): - rv = rustcall(lib.relay_generate_key_pair) - return ( - SecretKey._from_objptr(rv.secret_key), - PublicKey._from_objptr(rv.public_key), - ) - - -def generate_relay_id(): - return decode_uuid(rustcall(lib.relay_generate_relay_id)) - - -def create_register_challenge( - data, - signature, - secret, - max_age=60, - json_loads: Callable[[str | bytes], Any] = json.loads, -): - challenge_json = rustcall( - lib.relay_create_register_challenge, - make_buf(data), - encode_str(signature), - encode_str(secret), - max_age, - ) - - challenge = json_loads(decode_str(challenge_json, free=True)) - return { - "relay_id": uuid.UUID(challenge["relay_id"]), - "token": challenge["token"], - } - - -def validate_register_response( - data, - signature, - secret, - max_age=60, - json_loads: Callable[[str | bytes], Any] = json.loads, -): - response_json = rustcall( - lib.relay_validate_register_response, - make_buf(data), - encode_str(signature), - encode_str(secret), - max_age, - ) - - response = json_loads(decode_str(response_json, free=True)) - return { - "relay_id": uuid.UUID(response["relay_id"]), - "token": response["token"], - "public_key": response["public_key"], - "version": response["version"], - } - - -def is_version_supported(version): - """ - Checks if the provided Relay version is still compatible with this library. The version can be - ``None``, in which case a legacy Relay is assumed. - """ - return rustcall(lib.relay_version_supported, encode_str(version or "")) diff --git a/py/sentry_relay/auth.pyi b/py/sentry_relay/auth.pyi new file mode 100644 index 00000000000..f278106435c --- /dev/null +++ b/py/sentry_relay/auth.pyi @@ -0,0 +1,32 @@ +import uuid +from typing import Tuple + +class PublicKey: + @staticmethod + def parse(input: str | bytes) -> PublicKey: ... + def verify( + self, buf: bytes, sign: str | bytes, max_age: int | None = None + ) -> bool: ... + def unpack( + self, buf: bytes, sign: str | bytes, max_age: int | None = None + ) -> bool: ... + +class SecretKey: + @staticmethod + def parse(input: str | bytes) -> SecretKey: ... + def sign(self, value: bytes) -> str: ... + +def generate_key_pair() -> Tuple[SecretKey, PublicKey]: ... +def generate_relay_id() -> bytes: ... +def create_register_challenge( + data: bytes, signature: str | bytes, secret: str | bytes, max_age: int = 60 +) -> dict[str, str | uuid.UUID]: ... +def validate_register_response( + data: bytes, signature: str | bytes, secret: str | bytes, max_age: int = 60 +) -> dict[str, str | uuid.UUID]: ... +def is_version_supported(version: str | bytes | None = None) -> bool: + """ + Checks if the provided Relay version is still compatible with this library. The version can be + ``None``, in which case a legacy Relay is assumed. + """ + ... diff --git a/py/sentry_relay/consts.py b/py/sentry_relay/consts.py deleted file mode 100644 index 6ff62d5f459..00000000000 --- a/py/sentry_relay/consts.py +++ /dev/null @@ -1,124 +0,0 @@ -import sys -from enum import IntEnum - -from sentry_relay._lowlevel import lib -from sentry_relay.utils import decode_str, encode_str - -__all__ = ["DataCategory", "SPAN_STATUS_CODE_TO_NAME", "SPAN_STATUS_NAME_TO_CODE"] - - -class DataCategory(IntEnum): - # begin generated - DEFAULT = 0 - ERROR = 1 - TRANSACTION = 2 - SECURITY = 3 - ATTACHMENT = 4 - SESSION = 5 - PROFILE = 6 - REPLAY = 7 - TRANSACTION_PROCESSED = 8 - TRANSACTION_INDEXED = 9 - MONITOR = 10 - PROFILE_INDEXED = 11 - SPAN = 12 - MONITOR_SEAT = 13 - USER_REPORT_V2 = 14 - METRIC_BUCKET = 15 - SPAN_INDEXED = 16 - PROFILE_DURATION = 17 - PROFILE_CHUNK = 18 - METRIC_SECOND = 19 - UNKNOWN = -1 - # end generated - - @classmethod - def parse(cls, name): - """ - Parses a `DataCategory` from its API name. - """ - category = DataCategory(lib.relay_data_category_parse(encode_str(name or ""))) - if category == DataCategory.UNKNOWN: - return None # Unknown is a Rust-only value, replace with None - return category - - @classmethod - def from_event_type(cls, event_type): - """ - Parses a `DataCategory` from an event type. - """ - s = encode_str(event_type or "") - return DataCategory(lib.relay_data_category_from_event_type(s)) - - @classmethod - def event_categories(cls): - """ - Returns categories that count as events, including transactions. - """ - return [ - DataCategory.DEFAULT, - DataCategory.ERROR, - DataCategory.TRANSACTION, - DataCategory.SECURITY, - DataCategory.USER_REPORT_V2, - ] - - @classmethod - def error_categories(cls): - """ - Returns categories that count as traditional error tracking events. - """ - return [DataCategory.DEFAULT, DataCategory.ERROR, DataCategory.SECURITY] - - def api_name(self): - """ - Returns the API name of the given `DataCategory`. - """ - return decode_str(lib.relay_data_category_name(self.value), free=True) - - -def _check_generated(): - prefix = "RELAY_DATA_CATEGORY_" - - attrs = {} - for attr in dir(lib): - if attr.startswith(prefix): - category_name = attr[len(prefix) :] - attrs[category_name] = getattr(lib, attr) - - if attrs != DataCategory.__members__: - values = sorted( - attrs.items(), key=lambda kv: sys.maxsize if kv[1] == -1 else kv[1] - ) - generated = "".join(f" {k} = {v}\n" for k, v in values) - raise AssertionError( - f"DataCategory enum does not match source!\n\n" - f"Paste this into `class DataCategory` in py/sentry_relay/consts.py:\n\n" - f"{generated}" - ) - - -_check_generated() - -SPAN_STATUS_CODE_TO_NAME = {} -SPAN_STATUS_NAME_TO_CODE = {} - - -def _make_span_statuses(): - prefix = "RELAY_SPAN_STATUS_" - - for attr in dir(lib): - if not attr.startswith(prefix): - continue - - status_name = attr[len(prefix) :].lower() - status_code = getattr(lib, attr) - - SPAN_STATUS_CODE_TO_NAME[status_code] = status_name - SPAN_STATUS_NAME_TO_CODE[status_name] = status_code - - # Legacy alias - SPAN_STATUS_NAME_TO_CODE["unknown_error"] = SPAN_STATUS_NAME_TO_CODE["unknown"] - - -_make_span_statuses() diff --git a/py/sentry_relay/consts.pyi b/py/sentry_relay/consts.pyi new file mode 100644 index 00000000000..2e602506b9c --- /dev/null +++ b/py/sentry_relay/consts.pyi @@ -0,0 +1,61 @@ +from enum import IntEnum + +SPAN_STATUS_CODE_TO_NAME: dict[int, str] +SPAN_STATUS_NAME_TO_CODE: dict[str, int] + +class DataCategory(IntEnum): + DEFAULT = 0 + ERROR = 1 + TRANSACTION = 2 + SECURITY = 3 + ATTACHMENT = 4 + SESSION = 5 + PROFILE = 6 + REPLAY = 7 + TRANSACTION_PROCESSED = 8 + TRANSACTION_INDEXED = 9 + MONITOR = 10 + PROFILE_INDEXED = 11 + SPAN = 12 + MONITOR_SEAT = 13 + USER_REPORT_V2 = 14 + METRIC_BUCKET = 15 + SPAN_INDEXED = 16 + PROFILE_DURATION = 17 + PROFILE_CHUNK = 18 + METRIC_HOUR = 19 + UNKNOWN = -1 + + @staticmethod + def parse(name: str | None = None) -> DataCategory | None: + """ + Parses a `DataCategory` from its API name. + """ + ... + + @staticmethod + def from_event_type(event_type: str | None = None) -> DataCategory: + """ + Parses a `DataCategory` from an event type. + """ + ... + + @staticmethod + def event_categories() -> list[DataCategory]: + """ + Returns categories that count as events, including transactions. + """ + ... + + @staticmethod + def error_categories() -> list[DataCategory]: + """ + Returns categories that count as traditional error tracking events. + """ + ... + + def api_name(self) -> str: + """ + Returns the API name of the given `DataCategory`. + """ + ... diff --git a/py/sentry_relay/exceptions.py b/py/sentry_relay/exceptions.py deleted file mode 100644 index 2ef81a7773a..00000000000 --- a/py/sentry_relay/exceptions.py +++ /dev/null @@ -1,65 +0,0 @@ -from __future__ import annotations -from typing import TYPE_CHECKING -from sentry_relay._lowlevel import lib - - -__all__ = ["RelayError"] -exceptions_by_code = {} - - -class RelayError(Exception): - code = None - - def __init__(self, msg): - Exception.__init__(self) - self.message = msg - self.rust_info = None - - def __str__(self): - rv = self.message - if self.rust_info is not None: - return f"{rv}\n\n{self.rust_info}" - return rv - - -def _make_error(error_name, base=RelayError, code=None): - class Exc(base): - pass - - Exc.__name__ = error_name - Exc.__qualname__ = error_name - if code is not None: - Exc.code = code - globals()[Exc.__name__] = Exc - __all__.append(Exc.__name__) - return Exc - - -def _get_error_base(error_name): - pieces = error_name.split("Error", 1) - if len(pieces) == 2 and pieces[0] and pieces[1]: - base_error_name = pieces[0] + "Error" - base_class = globals().get(base_error_name) - if base_class is None: - base_class = _make_error(base_error_name) - return base_class - return RelayError - - -def _make_exceptions(): - prefix = "RELAY_ERROR_CODE_" - for attr in dir(lib): - if not attr.startswith(prefix): - continue - - error_name = attr[len(prefix) :].title().replace("_", "") - base = _get_error_base(error_name) - exc = _make_error(error_name, base=base, code=getattr(lib, attr)) - exceptions_by_code[exc.code] = exc - - -_make_exceptions() - -if TYPE_CHECKING: - # treat unknown attribute names as exception types - def __getattr__(name: str) -> type[RelayError]: ... diff --git a/py/sentry_relay/exceptions.pyi b/py/sentry_relay/exceptions.pyi new file mode 100644 index 00000000000..daa689e21cf --- /dev/null +++ b/py/sentry_relay/exceptions.pyi @@ -0,0 +1,16 @@ +class RelayError: + code: int | None = None + +class Unknown(RelayError): ... +class InvalidJsonError(RelayError): ... +class KeyParseErrorBadEncoding(RelayError): ... +class KeyParseErrorBadKey(RelayError): ... +class UnpackErrorBadSignature(RelayError): ... +class UnpackErrorBadPayload(RelayError): ... +class UnpackErrorSignatureExpired(RelayError): ... +class UnpackErrorBadEncoding(RelayError): ... +class ProcessingErrorInvalidTransaction(RelayError): ... +class ProcessingErrorInvalidGeoIp(RelayError): ... +class InvalidReleaseErrorTooLong(RelayError): ... +class InvalidReleaseErrorRestrictedName(RelayError): ... +class InvalidReleaseErrorBadCharacters(RelayError): ... diff --git a/py/sentry_relay/processing.py b/py/sentry_relay/processing.py deleted file mode 100644 index 97fd4383bd0..00000000000 --- a/py/sentry_relay/processing.py +++ /dev/null @@ -1,373 +0,0 @@ -from __future__ import annotations - -import json -from typing import Callable, Any - -from sentry_relay._lowlevel import lib, ffi -from sentry_relay.utils import ( - encode_str, - decode_str, - rustcall, - RustObject, - attached_refs, - make_buf, -) - -__all__ = [ - "split_chunks", - "meta_with_chunks", - "StoreNormalizer", - "GeoIpLookup", - "is_glob_match", - "is_codeowners_path_match", - "parse_release", - "validate_pii_selector", - "validate_pii_config", - "convert_datascrubbing_config", - "pii_strip_event", - "pii_selector_suggestions_from_event", - "VALID_PLATFORMS", - "validate_rule_condition", - "validate_sampling_condition", - "validate_sampling_configuration", - "normalize_project_config", - "normalize_cardinality_limit_config", - "normalize_global_config", -] - - -def _init_valid_platforms() -> frozenset[str]: - size_out = ffi.new("uintptr_t *") - strings = rustcall(lib.relay_valid_platforms, size_out) - - valid_platforms = [] - for i in range(int(size_out[0])): - valid_platforms.append(decode_str(strings[i], free=True)) - - return frozenset(valid_platforms) - - -VALID_PLATFORMS = _init_valid_platforms() - - -def split_chunks( - string, - remarks, - json_dumps: Callable[[Any], Any] = json.dumps, - json_loads: Callable[[str | bytes], Any] = json.loads, -): - json_chunks = rustcall( - lib.relay_split_chunks, - encode_str(string), - encode_str(json_dumps(remarks)), - ) - return json_loads(decode_str(json_chunks, free=True)) - - -def meta_with_chunks(data, meta): - if not isinstance(meta, dict): - return meta - - result = {} - for key, item in meta.items(): - if key == "" and isinstance(item, dict): - result[""] = item.copy() - if item.get("rem") and isinstance(data, str): - result[""]["chunks"] = split_chunks(data, item["rem"]) - elif isinstance(data, dict): - result[key] = meta_with_chunks(data.get(key), item) - elif isinstance(data, list): - int_key = int(key) - val = data[int_key] if int_key < len(data) else None - result[key] = meta_with_chunks(val, item) - else: - result[key] = item - - return result - - -class GeoIpLookup(RustObject): - __dealloc_func__ = lib.relay_geoip_lookup_free - __slots__ = ("_path",) - - @classmethod - def from_path(cls, path): - if isinstance(path, str): - path = path.encode("utf-8") - rv = cls._from_objptr(rustcall(lib.relay_geoip_lookup_new, path)) - rv._path = path - return rv - - def __repr__(self): - return f"" - - -class StoreNormalizer(RustObject): - __dealloc_func__ = lib.relay_store_normalizer_free - __init__ = object.__init__ - __slots__ = ("__weakref__",) - - def __new__( - cls, geoip_lookup=None, json_dumps: Callable[[Any], Any] = json.dumps, **config - ): - config = json_dumps(config) - geoptr = geoip_lookup._get_objptr() if geoip_lookup is not None else ffi.NULL - rv = cls._from_objptr( - rustcall(lib.relay_store_normalizer_new, encode_str(config), geoptr) - ) - if geoip_lookup is not None: - attached_refs[rv] = geoip_lookup - return rv - - def normalize_event( - self, - event=None, - raw_event=None, - json_loads: Callable[[str | bytes], Any] = json.loads, - ): - if raw_event is None: - raw_event = _serialize_event(event) - - event = _encode_raw_event(raw_event) - rv = self._methodcall(lib.relay_store_normalizer_normalize_event, event) - return json_loads(decode_str(rv, free=True)) - - -def _serialize_event(event): - # TODO(@anonrig): Look into ensure_ascii requirement - raw_event = json.dumps(event, ensure_ascii=False) - if isinstance(raw_event, str): - raw_event = raw_event.encode("utf-8", errors="replace") - return raw_event - - -def _encode_raw_event(raw_event): - event = encode_str(raw_event, mutable=True) - rustcall(lib.relay_translate_legacy_python_json, event) - return event - - -def is_glob_match( - value, - pat, - double_star=False, - case_insensitive=False, - path_normalize=False, - allow_newline=False, -): - flags = 0 - if double_star: - flags |= lib.GLOB_FLAGS_DOUBLE_STAR - if case_insensitive: - flags |= lib.GLOB_FLAGS_CASE_INSENSITIVE - # Since on the C side we're only working with bytes we need to lowercase the pattern - # and value here. This works with both bytes and unicode strings. - value = value.lower() - pat = pat.lower() - if path_normalize: - flags |= lib.GLOB_FLAGS_PATH_NORMALIZE - if allow_newline: - flags |= lib.GLOB_FLAGS_ALLOW_NEWLINE - - if isinstance(value, str): - value = value.encode("utf-8") - return rustcall(lib.relay_is_glob_match, make_buf(value), encode_str(pat), flags) - - -def is_codeowners_path_match(value, pattern): - if isinstance(value, str): - value = value.encode("utf-8") - return rustcall( - lib.relay_is_codeowners_path_match, make_buf(value), encode_str(pattern) - ) - - -def validate_pii_selector(selector): - """ - Validate a PII selector spec. Used to validate datascrubbing safe fields. - """ - assert isinstance(selector, str) - raw_error = rustcall(lib.relay_validate_pii_selector, encode_str(selector)) - error = decode_str(raw_error, free=True) - if error: - raise ValueError(error) - - -def validate_pii_config(config): - """ - Validate a PII config against the schema. Used in project options UI. - - The parameter is a JSON-encoded string. We should pass the config through - as a string such that line numbers from the error message match with what - the user typed in. - """ - assert isinstance(config, str) - raw_error = rustcall(lib.relay_validate_pii_config, encode_str(config)) - error = decode_str(raw_error, free=True) - if error: - raise ValueError(error) - - -def convert_datascrubbing_config( - config, - json_dumps: Callable[[Any], Any] = json.dumps, - json_loads: Callable[[str | bytes], Any] = json.loads, -): - """ - Convert an old datascrubbing config to the new PII config format. - """ - raw_config = encode_str(json_dumps(config)) - raw_rv = rustcall(lib.relay_convert_datascrubbing_config, raw_config) - return json_loads(decode_str(raw_rv, free=True)) - - -def pii_strip_event( - config, - event, - json_dumps: Callable[[Any], Any] = json.dumps, - json_loads: Callable[[str | bytes], Any] = json.loads, -): - """ - Scrub an event using new PII stripping config. - """ - raw_config = encode_str(json_dumps(config)) - raw_event = encode_str(json_dumps(event)) - raw_rv = rustcall(lib.relay_pii_strip_event, raw_config, raw_event) - return json_loads(decode_str(raw_rv, free=True)) - - -def pii_selector_suggestions_from_event( - event, - json_dumps: Callable[[Any], Any] = json.dumps, - json_loads: Callable[[str | bytes], Any] = json.loads, -): - """ - Walk through the event and collect selectors that can be applied to it in a - PII config. This function is used in the UI to provide auto-completion of - selectors. - """ - raw_event = encode_str(json_dumps(event)) - raw_rv = rustcall(lib.relay_pii_selector_suggestions_from_event, raw_event) - return json_loads(decode_str(raw_rv, free=True)) - - -def parse_release(release, json_loads: Callable[[str | bytes], Any] = json.loads): - """Parses a release string into a dictionary of its components.""" - return json_loads( - decode_str(rustcall(lib.relay_parse_release, encode_str(release)), free=True) - ) - - -def compare_version(a, b): - """Compares two versions with each other and returns 1/0/-1.""" - return rustcall(lib.relay_compare_versions, encode_str(a), encode_str(b)) - - -def validate_sampling_condition(condition): - """ - Deprecated legacy alias. Please use ``validate_rule_condition`` instead. - """ - return validate_rule_condition(condition) - - -def validate_rule_condition(condition): - """ - Validate a dynamic rule condition. Used by dynamic sampling, metric extraction, and metric - tagging. - - :param condition: A string containing the condition encoded as JSON. - """ - assert isinstance(condition, str) - raw_error = rustcall(lib.relay_validate_rule_condition, encode_str(condition)) - error = decode_str(raw_error, free=True) - if error: - raise ValueError(error) - - -def validate_sampling_configuration(condition): - """ - Validate the whole sampling configuration. Used in dynamic sampling serializer. - The parameter is a string containing the rules configuration as JSON. - """ - assert isinstance(condition, str) - raw_error = rustcall( - lib.relay_validate_sampling_configuration, encode_str(condition) - ) - error = decode_str(raw_error, free=True) - if error: - raise ValueError(error) - - -def normalize_project_config( - config, - json_dumps: Callable[[Any], Any] = json.dumps, - json_loads: Callable[[str | bytes], Any] = json.loads, -): - """Normalize a project config. - - :param config: the project config to validate. - :param json_dumps: a function that stringifies python objects - :param json_loads: a function that parses and converts JSON strings - """ - serialized = json_dumps(config) - normalized = rustcall(lib.relay_normalize_project_config, encode_str(serialized)) - rv = decode_str(normalized, free=True) - try: - return json_loads(rv) - except Exception: - # Catch all errors since json.loads implementation can change. - raise ValueError(rv) - - -def normalize_cardinality_limit_config( - config, - json_dumps: Callable[[Any], Any] = json.dumps, - json_loads: Callable[[str | bytes], Any] = json.loads, -): - """Normalize the cardinality limit config. - - Normalization consists of deserializing and serializing back the given - cardinality limit config. If deserializing fails, throw an exception. Note that even if - the roundtrip doesn't produce errors, the given config may differ from - normalized one. - - :param config: the cardinality limit config to validate. - :param json_dumps: a function that stringifies python objects - :param json_loads: a function that parses and converts JSON strings - """ - serialized = json_dumps(config) - normalized = rustcall( - lib.normalize_cardinality_limit_config, encode_str(serialized) - ) - rv = decode_str(normalized, free=True) - try: - return json_loads(rv) - except Exception: - # Catch all errors since json.loads implementation can change. - raise ValueError(rv) - - -def normalize_global_config( - config, - json_dumps: Callable[[Any], Any] = json.dumps, - json_loads: Callable[[str | bytes], Any] = json.loads, -): - """Normalize the global config. - - Normalization consists of deserializing and serializing back the given - global config. If deserializing fails, throw an exception. Note that even if - the roundtrip doesn't produce errors, the given config may differ from - normalized one. - - :param config: the global config to validate. - :param json_dumps: a function that stringifies python objects - :param json_loads: a function that parses and converts JSON strings - """ - serialized = json_dumps(config) - normalized = rustcall(lib.relay_normalize_global_config, encode_str(serialized)) - rv = decode_str(normalized, free=True) - try: - return json_loads(rv) - except Exception: - # Catch all errors since json.loads implementation can change. - raise ValueError(rv) diff --git a/py/sentry_relay/processing.pyi b/py/sentry_relay/processing.pyi new file mode 100644 index 00000000000..a90839ed58d --- /dev/null +++ b/py/sentry_relay/processing.pyi @@ -0,0 +1,150 @@ +from typing import Any + +VALID_PLATFORMS: set[str] + +class GeoIpLookup: + @staticmethod + def open(path: str) -> GeoIpLookup: + """Opens a maxminddb file by path.""" + ... + + @staticmethod + def from_path(path: str) -> GeoIpLookup: + """Opens a maxminddb file by path.""" + ... + + def lookup(self, ip_address: str) -> Any | None: + """Looks up an IP address""" + ... + +class StoreNormalizer: + def __new__(cls, *args: object, **kwargs: object) -> StoreNormalizer: ... + def normalize_event( + self, event: dict | None = None, raw_event: str | bytes | None = None + ) -> Any: ... + +def validate_sampling_configuration(condition: bytes | str) -> None: + """ + Validate the whole sampling configuration. Used in dynamic sampling serializer. + The parameter is a string containing the rules configuration as JSON. + """ + ... + +def compare_versions(lhs: bytes | str, rhs: bytes | str) -> int: + """Compares two versions with each other and returns 1/0/-1.""" + ... + +def validate_pii_selector(selector: str) -> None: + """ + Validate a PII selector spec. Used to validate data-scrubbing safe fields. + """ + ... + +def validate_pii_config(config: str) -> None: + """ + Validate a PII config against the schema. Used in project options UI. + + The parameter is a JSON-encoded string. We should pass the config through + as a string such that line numbers from the error message match with what + the user typed in. + """ + ... + +def validate_rule_condition(condition: str) -> None: + """ + Validate a dynamic rule condition. Used by dynamic sampling, metric extraction, and metric + tagging. + + :param condition: A string containing the condition encoded as JSON. + """ + ... + +def validate_sampling_condition(condition: str) -> None: + """ + Deprecated legacy alias. Please use ``validate_rule_condition`` instead. + """ + ... + +def is_codeowners_path_match(value: bytes, pattern: str) -> bool: ... +def split_chunks(input: str, remarks: list[Any]) -> Any: ... +def pii_strip_event(config: Any, event: str) -> Any: + """ + Scrub an event using new PII stripping config. + """ + ... + +def parse_release(release: str) -> dict[str, Any]: + """Parses a release string into a dictionary of its components.""" + ... + +def normalize_global_config(config: object) -> Any: + """Normalize the global config. + + Normalization consists of deserializing and serializing back the given + global config. If deserializing fails, throw an exception. Note that even if + the roundtrip doesn't produce errors, the given config may differ from + normalized one. + + :param config: the global config to validate. + """ + ... + +def pii_selector_suggestions_from_event(event: object) -> Any: + """ + Walk through the event and collect selectors that can be applied to it in a + PII config. This function is used in the UI to provide auto-completion of + selectors. + """ + ... + +def convert_datascrubbing_config(config: object) -> Any: + """ + Convert an old datascrubbing config to the new PII config format. + """ + ... + +def init_valid_platforms() -> set[str]: + """ + Initialize the set of valid platforms. + """ + ... + +def is_glob_match( + value: str, + pat: str, + double_star: bool | None, + case_insensitive: bool | None, + path_normalize: bool | None, + allow_newline: bool | None, +) -> bool: + """ + Check if a value matches a pattern. + """ + ... + +def meta_with_chunks(data: str | object, meta: Any) -> Any: + """ + Add the chunks to the meta data. + """ + ... + +def normalize_cardinality_limit_config(config: dict) -> dict: + """Normalize the cardinality limit config. + + Normalization consists of deserializing and serializing back the given + cardinality limit config. If deserializing fails, throw an exception. Note that even if + the roundtrip doesn't produce errors, the given config may differ from + normalized one. + + :param config: the cardinality limit config to validate. + """ + ... + +def normalize_project_config(config: dict) -> dict: + """Normalize a project config. + + :param config: the project config to validate. + :param json_dumps: a function that stringifies python objects + :param json_loads: a function that parses and converts JSON strings + """ + ... diff --git a/py/sentry_relay/utils.py b/py/sentry_relay/utils.py deleted file mode 100644 index e886b6fd90b..00000000000 --- a/py/sentry_relay/utils.py +++ /dev/null @@ -1,113 +0,0 @@ -from __future__ import annotations - -import os -import uuid -import weakref -from sentry_relay._lowlevel import ffi, lib -from sentry_relay.exceptions import exceptions_by_code, RelayError - - -attached_refs: weakref.WeakKeyDictionary[object, bytes] -attached_refs = weakref.WeakKeyDictionary() - - -lib.relay_init() -os.environ["RUST_BACKTRACE"] = "1" - - -class _NoDict(type): - def __new__(cls, name, bases, d): - d.setdefault("__slots__", ()) - return type.__new__(cls, name, bases, d) - - -def rustcall(func, *args): - """Calls rust method and does some error handling.""" - lib.relay_err_clear() - rv = func(*args) - err = lib.relay_err_get_last_code() - if not err: - return rv - msg = lib.relay_err_get_last_message() - cls = exceptions_by_code.get(err, RelayError) - exc = cls(decode_str(msg, free=True)) - backtrace = decode_str(lib.relay_err_get_backtrace(), free=True) - if backtrace: - exc.rust_info = backtrace - raise exc - - -class RustObject(metaclass=_NoDict): - __slots__ = ["_objptr", "_shared"] - __dealloc_func__ = None - - def __init__(self): - raise TypeError("Cannot instanciate %r objects" % self.__class__.__name__) - - @classmethod - def _from_objptr(cls, ptr, shared=False): - rv = object.__new__(cls) - rv._objptr = ptr - rv._shared = shared - return rv - - def _methodcall(self, func, *args): - return rustcall(func, self._get_objptr(), *args) - - def _get_objptr(self): - if not self._objptr: - raise RuntimeError("Object is closed") - return self._objptr - - def __del__(self): - if rustcall is None: - # Interpreter is shutting down and our memory management utils are - # gone. Just give up, the process is going away anyway. - return - - if self._objptr is None or self._shared: - return - f = self.__class__.__dealloc_func__ - if f is not None: - rustcall(f, self._objptr) - self._objptr = None - - -def decode_str(s, free=False): - """Decodes a RelayStr""" - try: - if s.len == 0: - return "" - return ffi.unpack(s.data, s.len).decode("utf-8", "replace") - finally: - if free and s.owned: - lib.relay_str_free(ffi.addressof(s)) - - -def encode_str(s, mutable=False): - """Encodes a RelayStr""" - rv = ffi.new("RelayStr *") - if isinstance(s, str): - s = s.encode("utf-8") - if mutable: - s = bytearray(s) - rv.data = ffi.from_buffer(s) - rv.len = len(s) - # we have to hold a weak reference here to ensure our string does not - # get collected before the string is used. - attached_refs[rv] = s - return rv - - -def make_buf(value): - buf = memoryview(bytes(value)) - rv = ffi.new("RelayBuf *") - rv.data = ffi.from_buffer(buf) - rv.len = len(buf) - attached_refs[rv] = buf - return rv - - -def decode_uuid(value): - """Decodes the given uuid value.""" - return uuid.UUID(bytes=bytes(bytearray(ffi.unpack(value.data, 16)))) diff --git a/py/setup.py b/py/setup.py deleted file mode 100644 index ecda8bec19f..00000000000 --- a/py/setup.py +++ /dev/null @@ -1,121 +0,0 @@ -import os -import re -import sys -import atexit -import shutil -import zipfile -import tempfile -import subprocess -from setuptools import setup, find_packages -from distutils.command.sdist import sdist - - -_version_re = re.compile(r'(?m)^version\s*=\s*"(.*?)"\s*$') - - -DEBUG_BUILD = os.environ.get("RELAY_DEBUG") == "1" - -with open("README", encoding="UTF-8") as f: - readme = f.read() - - -if os.path.isfile("../relay-cabi/Cargo.toml"): - with open("../relay-cabi/Cargo.toml") as f: - match = _version_re.search(f.read()) - assert match is not None - version = match[1] -else: - with open("version.txt") as f: - version = f.readline().strip() - - -def vendor_rust_deps(): - subprocess.Popen(["scripts/git-archive-all", "py/rustsrc.zip"], cwd="..").wait() - - -def write_version(): - with open("version.txt", "wb") as f: - f.write(("%s\n" % version).encode()) - - -class CustomSDist(sdist): - def run(self): - vendor_rust_deps() - write_version() - sdist.run(self) - - -def build_native(spec): - cmd = ["cargo", "build", "-p", "relay-cabi"] - if not DEBUG_BUILD: - cmd.append("--release") - target = "release" - else: - target = "debug" - - # Step 0: find rust sources - if not os.path.isfile("../relay-cabi/Cargo.toml"): - scratchpad = tempfile.mkdtemp() - - @atexit.register - def delete_scratchpad(): - try: - shutil.rmtree(scratchpad) - except OSError: - pass - - zf = zipfile.ZipFile("rustsrc.zip") - zf.extractall(scratchpad) - rust_path = scratchpad + "/rustsrc" - else: - rust_path = ".." - scratchpad = None - - # if the lib already built we replace the command - if os.environ.get("SKIP_RELAY_LIB_BUILD") is not None: - cmd = ["echo", "'Use pre-built library.'"] - - # Step 1: build the rust library - build = spec.add_external_build(cmd=cmd, path=rust_path) - - def find_dylib(): - cargo_target = os.environ.get("CARGO_BUILD_TARGET") - if cargo_target: - in_path = f"target/{cargo_target}/{target}" - else: - in_path = "target/%s" % target - return build.find_dylib("relay_cabi", in_path=in_path) - - rtld_flags = ["NOW"] - if sys.platform == "darwin": - rtld_flags.append("NODELETE") - spec.add_cffi_module( - module_path="sentry_relay._lowlevel", - dylib=find_dylib, - header_filename=lambda: build.find_header( - "relay.h", in_path="relay-cabi/include" - ), - rtld_flags=rtld_flags, - ) - - -setup( - name="sentry-relay", - version=version, - packages=find_packages(), - author="Sentry", - license="FSL-1.0-Apache-2.0", - author_email="hello@sentry.io", - description="A python library to access sentry relay functionality.", - long_description=readme, - long_description_content_type="text/markdown", - include_package_data=True, - package_data={"sentry_relay": ["py.typed", "_lowlevel.pyi"]}, - zip_safe=False, - platforms="any", - python_requires=">=3.10", - install_requires=["milksnake>=0.1.6"], - setup_requires=["milksnake>=0.1.6"], - milksnake_tasks=[build_native], - cmdclass={"sdist": CustomSDist}, # type: ignore -) diff --git a/py/tests/test_consts.py b/py/tests/test_consts.py index e1b1c3eb187..eaf52a40c0e 100644 --- a/py/tests/test_consts.py +++ b/py/tests/test_consts.py @@ -1,4 +1,8 @@ -from sentry_relay.consts import DataCategory, SPAN_STATUS_CODE_TO_NAME +from sentry_relay.consts import ( + DataCategory, + SPAN_STATUS_CODE_TO_NAME, + SPAN_STATUS_NAME_TO_CODE, +) def test_parse_data_category(): @@ -49,3 +53,23 @@ def test_span_mapping(): 15: "data_loss", 16: "unauthenticated", } + + assert SPAN_STATUS_NAME_TO_CODE == { + "aborted": 10, + "already_exists": 6, + "cancelled": 1, + "data_loss": 15, + "deadline_exceeded": 4, + "failed_precondition": 9, + "internal_error": 13, + "invalid_argument": 3, + "not_found": 5, + "ok": 0, + "out_of_range": 11, + "permission_denied": 7, + "resource_exhausted": 8, + "unauthenticated": 16, + "unavailable": 14, + "unimplemented": 12, + "unknown": 2, + } diff --git a/py/tests/test_processing.py b/py/tests/test_processing.py index 0fcaae39484..c5342a2bc48 100644 --- a/py/tests/test_processing.py +++ b/py/tests/test_processing.py @@ -209,7 +209,7 @@ def test_parse_release(): "minor": 0, "patch": 0, "pre": "rc1", - "raw_quad": ["1", "0", None, None], + "raw_quad": ("1", "0", None, None), "raw_short": "1.0rc1", "revision": 0, }, @@ -222,10 +222,10 @@ def test_parse_release_error(): sentry_relay.parse_release("/var/foo/foo") -def compare_versions(): +def test_compare_versions(): assert sentry_relay.compare_versions("1.0.0", "0.1.1") == 1 assert sentry_relay.compare_versions("0.0.0", "0.1.1") == -1 - assert sentry_relay.compare_versions("1.0.0", "1.0") == -1 + assert sentry_relay.compare_versions("1.0.0", "1.0.0") == 0 def test_validate_rule_condition(): @@ -366,9 +366,7 @@ def test_cardinality_limit_config_unparsable(): } with pytest.raises(ValueError) as e: sentry_relay.normalize_cardinality_limit_config(config) - assert ( - str(e.value) == "invalid value: integer `-1`, expected u32 at line 1 column 107" - ) + assert str(e.value) == "RuntimeError: invalid value: integer `-1`, expected u32" def test_global_config_equal_normalization(): @@ -387,7 +385,4 @@ def test_global_config_unparsable(): config = {"measurements": {"maxCustomMeasurements": -5}} with pytest.raises(ValueError) as e: sentry_relay.normalize_global_config(config) - assert ( - str(e.value) - == "invalid value: integer `-5`, expected usize at line 1 column 45" - ) + assert str(e.value) == "RuntimeError: invalid value: integer `-5`, expected usize" diff --git a/py/tests/test_utils.py b/py/tests/test_utils.py deleted file mode 100644 index 36abe8101d4..00000000000 --- a/py/tests/test_utils.py +++ /dev/null @@ -1,10 +0,0 @@ -import pytest - -from sentry_relay._lowlevel import lib -from sentry_relay.utils import rustcall -from sentry_relay.exceptions import Panic - - -def test_panic(): - with pytest.raises(Panic): - rustcall(lib.relay_test_panic) diff --git a/relay-base-schema/Cargo.toml b/relay-base-schema/Cargo.toml index bdd24842e42..981fbf39c6c 100644 --- a/relay-base-schema/Cargo.toml +++ b/relay-base-schema/Cargo.toml @@ -18,6 +18,7 @@ relay-common = { workspace = true } relay-protocol = { workspace = true } schemars = { workspace = true, optional = true } serde = { workspace = true } +pyo3 = { workspace = true, features = ["serde"] } [dev-dependencies] serde_json = { workspace = true } diff --git a/relay-base-schema/src/data_category.rs b/relay-base-schema/src/data_category.rs index b38ae5ccd76..e66cc656572 100644 --- a/relay-base-schema/src/data_category.rs +++ b/relay-base-schema/src/data_category.rs @@ -1,16 +1,20 @@ //! Defines the [`DataCategory`] type that classifies data Relay can handle. +use pyo3::{pyclass, pymethods}; use std::fmt; use std::str::FromStr; use serde::{Deserialize, Serialize}; use crate::events::EventType; +use pyo3::prelude::*; +use pyo3::types::PyString; /// Classifies the type of data that is being ingested. #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize)] #[serde(rename_all = "snake_case")] #[repr(i8)] +#[pyclass(rename_all = "SCREAMING_SNAKE_CASE")] pub enum DataCategory { /// Reserved and unused. Default = 0, @@ -94,6 +98,58 @@ pub enum DataCategory { Unknown = -1, } +#[pymethods] +impl DataCategory { + #[staticmethod] + fn parse(name: Option<&Bound>) -> PyResult> { + let Some(name) = name else { + return Ok(None); + }; + + match name.to_str()?.parse::() { + Ok(DataCategory::Unknown) => Ok(None), + Ok(value) => Ok(Some(value)), + Err(_) => Ok(None), + } + } + + #[staticmethod] + fn from_event_type(event_type: Option<&Bound>) -> PyResult { + let Some(event_type) = event_type else { + return Ok(Self::Error); + }; + + match event_type.to_str()?.parse::() { + Ok(value) => Ok(value.into()), + Err(_) => Ok(Self::Error), + } + } + + #[staticmethod] + fn event_categories() -> [DataCategory; 5] { + [ + DataCategory::Default, + DataCategory::Error, + DataCategory::Transaction, + DataCategory::Security, + DataCategory::UserReportV2, + ] + } + + #[staticmethod] + fn error_categories() -> [DataCategory; 3] { + [ + DataCategory::Default, + DataCategory::Error, + DataCategory::Security, + ] + } + + fn api_name(&self) -> String { + self.name().to_owned() + } +} + impl DataCategory { /// Returns the data category corresponding to the given name. pub fn from_name(string: &str) -> Self { diff --git a/relay-cabi/.gitignore b/relay-cabi/.gitignore deleted file mode 100644 index eb5a316cbd1..00000000000 --- a/relay-cabi/.gitignore +++ /dev/null @@ -1 +0,0 @@ -target diff --git a/relay-cabi/Makefile b/relay-cabi/Makefile deleted file mode 100644 index e918d6d1d5d..00000000000 --- a/relay-cabi/Makefile +++ /dev/null @@ -1,20 +0,0 @@ -all: header release -.PHONY: all - -header: include/relay.h -.PHONY: header - -release: - cargo build --release -.PHONY: release - -clean: - cargo clean -.PHONY: clean - -include/relay.h: *.toml src/*.rs - @touch src/lib.rs # Ensure that rustc outputs something - cbindgen -c cbindgen.toml . -o $@ - @touch include/relay.h # Ensure that we don't build again - -.PHONY: include/relay.h diff --git a/relay-cabi/cbindgen.toml b/relay-cabi/cbindgen.toml deleted file mode 100644 index 25849081091..00000000000 --- a/relay-cabi/cbindgen.toml +++ /dev/null @@ -1,22 +0,0 @@ -header = "/* C bindings to the sentry relay library */" -include_guard = "RELAY_H_INCLUDED" -autogen_warning = "/* Warning, this file is autogenerated. Do not modify this manually. */" -include_version = true -line_length = 80 -tab_width = 2 -language = "C" - -[parse] -include = ["relay-base-schema"] -parse_deps = true - -[enum] -rename_variants = "ScreamingSnakeCase" -prefix_with_name = true - -[export] -include = ["SpanStatus"] - -[export.rename] -"DataCategory" = "RelayDataCategory" -"SpanStatus" = "RelaySpanStatus" diff --git a/relay-cabi/include/relay.h b/relay-cabi/include/relay.h deleted file mode 100644 index 6e8359d9c1d..00000000000 --- a/relay-cabi/include/relay.h +++ /dev/null @@ -1,672 +0,0 @@ -/* C bindings to the sentry relay library */ - -#ifndef RELAY_H_INCLUDED -#define RELAY_H_INCLUDED - -/* Generated with cbindgen:0.26.0 */ - -/* Warning, this file is autogenerated. Do not modify this manually. */ - -#include -#include -#include -#include - -/** - * Classifies the type of data that is being ingested. - */ -enum RelayDataCategory { - /** - * Reserved and unused. - */ - RELAY_DATA_CATEGORY_DEFAULT = 0, - /** - * Error events and Events with an `event_type` not explicitly listed below. - */ - RELAY_DATA_CATEGORY_ERROR = 1, - /** - * Transaction events. - */ - RELAY_DATA_CATEGORY_TRANSACTION = 2, - /** - * Events with an event type of `csp`, `hpkp`, `expectct` and `expectstaple`. - */ - RELAY_DATA_CATEGORY_SECURITY = 3, - /** - * An attachment. Quantity is the size of the attachment in bytes. - */ - RELAY_DATA_CATEGORY_ATTACHMENT = 4, - /** - * Session updates. Quantity is the number of updates in the batch. - */ - RELAY_DATA_CATEGORY_SESSION = 5, - /** - * Profile - * - * This is the category for processed profiles (all profiles, whether or not we store them). - */ - RELAY_DATA_CATEGORY_PROFILE = 6, - /** - * Session Replays - */ - RELAY_DATA_CATEGORY_REPLAY = 7, - /** - * DEPRECATED: A transaction for which metrics were extracted. - * - * This category is now obsolete because the `Transaction` variant will represent - * processed transactions from now on. - */ - RELAY_DATA_CATEGORY_TRANSACTION_PROCESSED = 8, - /** - * Indexed transaction events. - * - * This is the category for transaction payloads that were accepted and stored in full. In - * contrast, `transaction` only guarantees that metrics have been accepted for the transaction. - */ - RELAY_DATA_CATEGORY_TRANSACTION_INDEXED = 9, - /** - * Monitor check-ins. - */ - RELAY_DATA_CATEGORY_MONITOR = 10, - /** - * Indexed Profile - * - * This is the category for indexed profiles that will be stored later. - */ - RELAY_DATA_CATEGORY_PROFILE_INDEXED = 11, - /** - * Span - * - * This is the category for spans from which we extracted metrics from. - */ - RELAY_DATA_CATEGORY_SPAN = 12, - /** - * Monitor Seat - * - * Represents a monitor job that has scheduled monitor checkins. The seats are not ingested - * but we define it here to prevent clashing values since this data category enumeration - * is also used outside of Relay via the Python package. - */ - RELAY_DATA_CATEGORY_MONITOR_SEAT = 13, - /** - * User Feedback - * - * Represents a User Feedback processed. - * Currently standardized on name UserReportV2 to avoid clashing with the old UserReport. - * TODO(jferg): Rename this to UserFeedback once old UserReport is deprecated. - */ - RELAY_DATA_CATEGORY_USER_REPORT_V2 = 14, - /** - * Metric buckets. - */ - RELAY_DATA_CATEGORY_METRIC_BUCKET = 15, - /** - * SpanIndexed - * - * This is the category for spans we store in full. - */ - RELAY_DATA_CATEGORY_SPAN_INDEXED = 16, - /** - * ProfileDuration - * - * This data category is used to count the number of milliseconds we have per indexed profile chunk. - * We will then bill per second. - */ - RELAY_DATA_CATEGORY_PROFILE_DURATION = 17, - /** - * ProfileChunk - * - * This is a count of profile chunks received. It will not be used for billing but will be - * useful for customers to track what's being dropped. - */ - RELAY_DATA_CATEGORY_PROFILE_CHUNK = 18, - /** - * MetricSecond - * - * Reserved by billing to summarize the bucketed product of metric volume - * and metric cardinality. Defined here so as not to clash with future - * categories. - */ - RELAY_DATA_CATEGORY_METRIC_SECOND = 19, - /** - * Any other data category not known by this Relay. - */ - RELAY_DATA_CATEGORY_UNKNOWN = -1, -}; -typedef int8_t RelayDataCategory; - -/** - * Controls the globbing behaviors. - */ -enum GlobFlags { - /** - * When enabled `**` matches over path separators and `*` does not. - */ - GLOB_FLAGS_DOUBLE_STAR = 1, - /** - * Enables case insensitive path matching. - */ - GLOB_FLAGS_CASE_INSENSITIVE = 2, - /** - * Enables path normalization. - */ - GLOB_FLAGS_PATH_NORMALIZE = 4, - /** - * Allows newlines. - */ - GLOB_FLAGS_ALLOW_NEWLINE = 8, -}; -typedef uint32_t GlobFlags; - -/** - * Represents all possible error codes. - */ -enum RelayErrorCode { - RELAY_ERROR_CODE_NO_ERROR = 0, - RELAY_ERROR_CODE_PANIC = 1, - RELAY_ERROR_CODE_UNKNOWN = 2, - RELAY_ERROR_CODE_INVALID_JSON_ERROR = 101, - RELAY_ERROR_CODE_KEY_PARSE_ERROR_BAD_ENCODING = 1000, - RELAY_ERROR_CODE_KEY_PARSE_ERROR_BAD_KEY = 1001, - RELAY_ERROR_CODE_UNPACK_ERROR_BAD_SIGNATURE = 1003, - RELAY_ERROR_CODE_UNPACK_ERROR_BAD_PAYLOAD = 1004, - RELAY_ERROR_CODE_UNPACK_ERROR_SIGNATURE_EXPIRED = 1005, - RELAY_ERROR_CODE_UNPACK_ERROR_BAD_ENCODING = 1006, - RELAY_ERROR_CODE_PROCESSING_ERROR_INVALID_TRANSACTION = 2001, - RELAY_ERROR_CODE_PROCESSING_ERROR_INVALID_GEO_IP = 2002, - RELAY_ERROR_CODE_INVALID_RELEASE_ERROR_TOO_LONG = 3001, - RELAY_ERROR_CODE_INVALID_RELEASE_ERROR_RESTRICTED_NAME = 3002, - RELAY_ERROR_CODE_INVALID_RELEASE_ERROR_BAD_CHARACTERS = 3003, -}; -typedef uint32_t RelayErrorCode; - -/** - * Trace status. - * - * Values from - * Mapping to HTTP from - */ -enum RelaySpanStatus { - /** - * The operation completed successfully. - * - * HTTP status 100..299 + successful redirects from the 3xx range. - */ - RELAY_SPAN_STATUS_OK = 0, - /** - * The operation was cancelled (typically by the user). - */ - RELAY_SPAN_STATUS_CANCELLED = 1, - /** - * Unknown. Any non-standard HTTP status code. - * - * "We do not know whether the transaction failed or succeeded" - */ - RELAY_SPAN_STATUS_UNKNOWN = 2, - /** - * Client specified an invalid argument. 4xx. - * - * Note that this differs from FailedPrecondition. InvalidArgument indicates arguments that - * are problematic regardless of the state of the system. - */ - RELAY_SPAN_STATUS_INVALID_ARGUMENT = 3, - /** - * Deadline expired before operation could complete. - * - * For operations that change the state of the system, this error may be returned even if the - * operation has been completed successfully. - * - * HTTP redirect loops and 504 Gateway Timeout - */ - RELAY_SPAN_STATUS_DEADLINE_EXCEEDED = 4, - /** - * 404 Not Found. Some requested entity (file or directory) was not found. - */ - RELAY_SPAN_STATUS_NOT_FOUND = 5, - /** - * Already exists (409) - * - * Some entity that we attempted to create already exists. - */ - RELAY_SPAN_STATUS_ALREADY_EXISTS = 6, - /** - * 403 Forbidden - * - * The caller does not have permission to execute the specified operation. - */ - RELAY_SPAN_STATUS_PERMISSION_DENIED = 7, - /** - * 429 Too Many Requests - * - * Some resource has been exhausted, perhaps a per-user quota or perhaps the entire file - * system is out of space. - */ - RELAY_SPAN_STATUS_RESOURCE_EXHAUSTED = 8, - /** - * Operation was rejected because the system is not in a state required for the operation's - * execution - */ - RELAY_SPAN_STATUS_FAILED_PRECONDITION = 9, - /** - * The operation was aborted, typically due to a concurrency issue. - */ - RELAY_SPAN_STATUS_ABORTED = 10, - /** - * Operation was attempted past the valid range. - */ - RELAY_SPAN_STATUS_OUT_OF_RANGE = 11, - /** - * 501 Not Implemented - * - * Operation is not implemented or not enabled. - */ - RELAY_SPAN_STATUS_UNIMPLEMENTED = 12, - /** - * Other/generic 5xx. - */ - RELAY_SPAN_STATUS_INTERNAL_ERROR = 13, - /** - * 503 Service Unavailable - */ - RELAY_SPAN_STATUS_UNAVAILABLE = 14, - /** - * Unrecoverable data loss or corruption - */ - RELAY_SPAN_STATUS_DATA_LOSS = 15, - /** - * 401 Unauthorized (actually does mean unauthenticated according to RFC 7235) - * - * Prefer PermissionDenied if a user is logged in. - */ - RELAY_SPAN_STATUS_UNAUTHENTICATED = 16, -}; -typedef uint8_t RelaySpanStatus; - -/** - * A geo ip lookup helper based on maxmind db files. - */ -typedef struct RelayGeoIpLookup RelayGeoIpLookup; - -/** - * Represents a public key in Relay. - */ -typedef struct RelayPublicKey RelayPublicKey; - -/** - * Represents a secret key in Relay. - */ -typedef struct RelaySecretKey RelaySecretKey; - -/** - * The processor that normalizes events for store. - */ -typedef struct RelayStoreNormalizer RelayStoreNormalizer; - -/** - * A length-prefixed UTF-8 string. - * - * As opposed to C strings, this string is not null-terminated. If the string is owned, indicated - * by the `owned` flag, the owner must call the `free` function on this string. The convention is: - * - * - When obtained as instance through return values, always free the string. - * - When obtained as pointer through field access, never free the string. - */ -typedef struct RelayStr { - /** - * Pointer to the UTF-8 encoded string data. - */ - char *data; - /** - * The length of the string pointed to by `data`. - */ - uintptr_t len; - /** - * Indicates that the string is owned and must be freed. - */ - bool owned; -} RelayStr; - -/** - * A binary buffer of known length. - * - * If the buffer is owned, indicated by the `owned` flag, the owner must call the `free` function - * on this buffer. The convention is: - * - * - When obtained as instance through return values, always free the buffer. - * - When obtained as pointer through field access, never free the buffer. - */ -typedef struct RelayBuf { - /** - * Pointer to the raw data. - */ - uint8_t *data; - /** - * The length of the buffer pointed to by `data`. - */ - uintptr_t len; - /** - * Indicates that the buffer is owned and must be freed. - */ - bool owned; -} RelayBuf; - -/** - * Represents a key pair from key generation. - */ -typedef struct RelayKeyPair { - /** - * The public key used for verifying Relay signatures. - */ - struct RelayPublicKey *public_key; - /** - * The secret key used for signing Relay requests. - */ - struct RelaySecretKey *secret_key; -} RelayKeyPair; - -/** - * A 16-byte UUID. - */ -typedef struct RelayUuid { - /** - * UUID bytes in network byte order (big endian). - */ - uint8_t data[16]; -} RelayUuid; - -/** - * Parses a public key from a string. - */ -struct RelayPublicKey *relay_publickey_parse(const struct RelayStr *s); - -/** - * Frees a public key. - */ -void relay_publickey_free(struct RelayPublicKey *spk); - -/** - * Converts a public key into a string. - */ -struct RelayStr relay_publickey_to_string(const struct RelayPublicKey *spk); - -/** - * Verifies a signature - */ -bool relay_publickey_verify(const struct RelayPublicKey *spk, - const struct RelayBuf *data, - const struct RelayStr *sig); - -/** - * Verifies a signature - */ -bool relay_publickey_verify_timestamp(const struct RelayPublicKey *spk, - const struct RelayBuf *data, - const struct RelayStr *sig, - uint32_t max_age); - -/** - * Parses a secret key from a string. - */ -struct RelaySecretKey *relay_secretkey_parse(const struct RelayStr *s); - -/** - * Frees a secret key. - */ -void relay_secretkey_free(struct RelaySecretKey *spk); - -/** - * Converts a secret key into a string. - */ -struct RelayStr relay_secretkey_to_string(const struct RelaySecretKey *spk); - -/** - * Verifies a signature - */ -struct RelayStr relay_secretkey_sign(const struct RelaySecretKey *spk, - const struct RelayBuf *data); - -/** - * Generates a secret, public key pair. - */ -struct RelayKeyPair relay_generate_key_pair(void); - -/** - * Randomly generates an relay id - */ -struct RelayUuid relay_generate_relay_id(void); - -/** - * Creates a challenge from a register request and returns JSON. - */ -struct RelayStr relay_create_register_challenge(const struct RelayBuf *data, - const struct RelayStr *signature, - const struct RelayStr *secret, - uint32_t max_age); - -/** - * Validates a register response. - */ -struct RelayStr relay_validate_register_response(const struct RelayBuf *data, - const struct RelayStr *signature, - const struct RelayStr *secret, - uint32_t max_age); - -/** - * Returns true if the given version is supported by this library. - */ -bool relay_version_supported(const struct RelayStr *version); - -/** - * Returns `true` if the codeowners path matches the value, `false` otherwise. - */ -bool relay_is_codeowners_path_match(const struct RelayBuf *value, - const struct RelayStr *pattern); - -/** - * Returns the API name of the given `DataCategory`. - */ -struct RelayStr relay_data_category_name(RelayDataCategory category); - -/** - * Parses a `DataCategory` from its API name. - */ -RelayDataCategory relay_data_category_parse(const struct RelayStr *name); - -/** - * Parses a `DataCategory` from an event type. - */ -RelayDataCategory relay_data_category_from_event_type(const struct RelayStr *event_type); - -/** - * Creates a Relay string from a c string. - */ -struct RelayStr relay_str_from_cstr(const char *s); - -/** - * Frees a Relay str. - * - * If the string is marked as not owned then this function does not - * do anything. - */ -void relay_str_free(struct RelayStr *s); - -/** - * Returns true if the uuid is nil. - */ -bool relay_uuid_is_nil(const struct RelayUuid *uuid); - -/** - * Formats the UUID into a string. - * - * The string is newly allocated and needs to be released with - * `relay_cstr_free`. - */ -struct RelayStr relay_uuid_to_str(const struct RelayUuid *uuid); - -/** - * Frees a Relay buf. - * - * If the buffer is marked as not owned then this function does not - * do anything. - */ -void relay_buf_free(struct RelayBuf *b); - -/** - * Initializes the library - */ -void relay_init(void); - -/** - * Returns the last error code. - * - * If there is no error, 0 is returned. - */ -RelayErrorCode relay_err_get_last_code(void); - -/** - * Returns the last error message. - * - * If there is no error an empty string is returned. This allocates new memory - * that needs to be freed with `relay_str_free`. - */ -struct RelayStr relay_err_get_last_message(void); - -/** - * Returns the panic information as string. - */ -struct RelayStr relay_err_get_backtrace(void); - -/** - * Clears the last error. - */ -void relay_err_clear(void); - -/** - * Chunks the given text based on remarks. - */ -struct RelayStr relay_split_chunks(const struct RelayStr *string, - const struct RelayStr *remarks); - -/** - * Opens a maxminddb file by path. - */ -struct RelayGeoIpLookup *relay_geoip_lookup_new(const char *path); - -/** - * Frees a `RelayGeoIpLookup`. - */ -void relay_geoip_lookup_free(struct RelayGeoIpLookup *lookup); - -/** - * Returns a list of all valid platform identifiers. - */ -const struct RelayStr *relay_valid_platforms(uintptr_t *size_out); - -/** - * Creates a new normalization config. - */ -struct RelayStoreNormalizer *relay_store_normalizer_new(const struct RelayStr *config, - const struct RelayGeoIpLookup *_geoip_lookup); - -/** - * Frees a `RelayStoreNormalizer`. - */ -void relay_store_normalizer_free(struct RelayStoreNormalizer *normalizer); - -/** - * Normalizes the event given as JSON. - */ -struct RelayStr relay_store_normalizer_normalize_event(struct RelayStoreNormalizer *normalizer, - const struct RelayStr *event); - -/** - * Replaces invalid JSON generated by Python. - */ -bool relay_translate_legacy_python_json(struct RelayStr *event); - -/** - * Validates a PII selector spec. Used to validate datascrubbing safe fields. - */ -struct RelayStr relay_validate_pii_selector(const struct RelayStr *value); - -/** - * Validate a PII config against the schema. Used in project options UI. - */ -struct RelayStr relay_validate_pii_config(const struct RelayStr *value); - -/** - * Convert an old datascrubbing config to the new PII config format. - */ -struct RelayStr relay_convert_datascrubbing_config(const struct RelayStr *config); - -/** - * Scrub an event using new PII stripping config. - */ -struct RelayStr relay_pii_strip_event(const struct RelayStr *config, - const struct RelayStr *event); - -/** - * Walk through the event and collect selectors that can be applied to it in a PII config. This - * function is used in the UI to provide auto-completion of selectors. - */ -struct RelayStr relay_pii_selector_suggestions_from_event(const struct RelayStr *event); - -/** - * A test function that always panics. - */ -void relay_test_panic(void); - -/** - * Performs a glob operation on bytes. - * - * Returns `true` if the glob matches, `false` otherwise. - */ -bool relay_is_glob_match(const struct RelayBuf *value, - const struct RelayStr *pat, - GlobFlags flags); - -/** - * Parse a sentry release structure from a string. - */ -struct RelayStr relay_parse_release(const struct RelayStr *value); - -/** - * Compares two versions. - */ -int32_t relay_compare_versions(const struct RelayStr *a, - const struct RelayStr *b); - -/** - * Validate a dynamic rule condition. - * - * Used by dynamic sampling, metric extraction, and metric tagging. - */ -struct RelayStr relay_validate_rule_condition(const struct RelayStr *value); - -/** - * Validate whole rule ( this will be also implemented in Sentry for better error messages) - * The implementation in relay is just to make sure that the Sentry implementation doesn't - * go out of sync. - */ -struct RelayStr relay_validate_sampling_configuration(const struct RelayStr *value); - -/** - * Normalize a project config. - * - * If `strict` is true, checks for unknown fields in the input. - */ -struct RelayStr relay_normalize_project_config(const struct RelayStr *value); - -/** - * Validate cardinality limit config. - * - * If `strict` is true, checks for unknown fields in the input. - */ -struct RelayStr normalize_cardinality_limit_config(const struct RelayStr *value); - -/** - * Normalize a global config. - */ -struct RelayStr relay_normalize_global_config(const struct RelayStr *value); - -#endif /* RELAY_H_INCLUDED */ diff --git a/relay-cabi/src/auth.rs b/relay-cabi/src/auth.rs deleted file mode 100644 index 449512ae924..00000000000 --- a/relay-cabi/src/auth.rs +++ /dev/null @@ -1,207 +0,0 @@ -use chrono::Duration; -use relay_auth::{ - generate_key_pair, generate_relay_id, PublicKey, RegisterRequest, RegisterResponse, RelayId, - RelayVersion, SecretKey, -}; -use serde::Serialize; - -use crate::core::{RelayBuf, RelayStr, RelayUuid}; - -/// Represents a public key in Relay. -pub struct RelayPublicKey; - -/// Represents a secret key in Relay. -pub struct RelaySecretKey; - -/// Represents a key pair from key generation. -#[repr(C)] -pub struct RelayKeyPair { - /// The public key used for verifying Relay signatures. - pub public_key: *mut RelayPublicKey, - /// The secret key used for signing Relay requests. - pub secret_key: *mut RelaySecretKey, -} - -/// Represents a register request. -pub struct RelayRegisterRequest; - -/// Parses a public key from a string. -#[no_mangle] -#[relay_ffi::catch_unwind] -pub unsafe extern "C" fn relay_publickey_parse(s: *const RelayStr) -> *mut RelayPublicKey { - let public_key: PublicKey = (*s).as_str().parse()?; - Box::into_raw(Box::new(public_key)) as *mut RelayPublicKey -} - -/// Frees a public key. -#[no_mangle] -#[relay_ffi::catch_unwind] -pub unsafe extern "C" fn relay_publickey_free(spk: *mut RelayPublicKey) { - if !spk.is_null() { - let pk = spk as *mut PublicKey; - let _dropped = Box::from_raw(pk); - } -} - -/// Converts a public key into a string. -#[no_mangle] -#[relay_ffi::catch_unwind] -pub unsafe extern "C" fn relay_publickey_to_string(spk: *const RelayPublicKey) -> RelayStr { - let pk = spk as *const PublicKey; - RelayStr::from_string((*pk).to_string()) -} - -/// Verifies a signature -#[no_mangle] -#[relay_ffi::catch_unwind] -pub unsafe extern "C" fn relay_publickey_verify( - spk: *const RelayPublicKey, - data: *const RelayBuf, - sig: *const RelayStr, -) -> bool { - let pk = spk as *const PublicKey; - (*pk).verify((*data).as_bytes(), (*sig).as_str()) -} - -/// Verifies a signature -#[no_mangle] -#[relay_ffi::catch_unwind] -pub unsafe extern "C" fn relay_publickey_verify_timestamp( - spk: *const RelayPublicKey, - data: *const RelayBuf, - sig: *const RelayStr, - max_age: u32, -) -> bool { - let pk = spk as *const PublicKey; - let max_age = Some(Duration::seconds(i64::from(max_age))); - (*pk).verify_timestamp((*data).as_bytes(), (*sig).as_str(), max_age) -} - -/// Parses a secret key from a string. -#[no_mangle] -#[relay_ffi::catch_unwind] -pub unsafe extern "C" fn relay_secretkey_parse(s: &RelayStr) -> *mut RelaySecretKey { - let secret_key: SecretKey = s.as_str().parse()?; - Box::into_raw(Box::new(secret_key)) as *mut RelaySecretKey -} - -/// Frees a secret key. -#[no_mangle] -#[relay_ffi::catch_unwind] -pub unsafe extern "C" fn relay_secretkey_free(spk: *mut RelaySecretKey) { - if !spk.is_null() { - let pk = spk as *mut SecretKey; - let _dropped = Box::from_raw(pk); - } -} - -/// Converts a secret key into a string. -#[no_mangle] -#[relay_ffi::catch_unwind] -pub unsafe extern "C" fn relay_secretkey_to_string(spk: *const RelaySecretKey) -> RelayStr { - let pk = spk as *const SecretKey; - RelayStr::from_string((*pk).to_string()) -} - -/// Verifies a signature -#[no_mangle] -#[relay_ffi::catch_unwind] -pub unsafe extern "C" fn relay_secretkey_sign( - spk: *const RelaySecretKey, - data: *const RelayBuf, -) -> RelayStr { - let pk = spk as *const SecretKey; - RelayStr::from_string((*pk).sign((*data).as_bytes())) -} - -/// Generates a secret, public key pair. -#[no_mangle] -#[relay_ffi::catch_unwind] -pub unsafe extern "C" fn relay_generate_key_pair() -> RelayKeyPair { - let (sk, pk) = generate_key_pair(); - RelayKeyPair { - secret_key: Box::into_raw(Box::new(sk)) as *mut RelaySecretKey, - public_key: Box::into_raw(Box::new(pk)) as *mut RelayPublicKey, - } -} - -/// Randomly generates an relay id -#[no_mangle] -#[relay_ffi::catch_unwind] -pub unsafe extern "C" fn relay_generate_relay_id() -> RelayUuid { - let relay_id = generate_relay_id(); - RelayUuid::new(relay_id) -} - -/// Creates a challenge from a register request and returns JSON. -#[no_mangle] -#[relay_ffi::catch_unwind] -pub unsafe extern "C" fn relay_create_register_challenge( - data: *const RelayBuf, - signature: *const RelayStr, - secret: *const RelayStr, - max_age: u32, -) -> RelayStr { - let max_age = match max_age { - 0 => None, - m => Some(Duration::seconds(i64::from(m))), - }; - - let req = - RegisterRequest::bootstrap_unpack((*data).as_bytes(), (*signature).as_str(), max_age)?; - - let challenge = req.into_challenge((*secret).as_str().as_bytes()); - RelayStr::from_string(serde_json::to_string(&challenge)?) -} - -#[derive(Serialize)] -struct RelayRegisterResponse<'a> { - pub relay_id: RelayId, - pub token: &'a str, - pub public_key: &'a PublicKey, - pub version: RelayVersion, -} - -/// Validates a register response. -#[no_mangle] -#[relay_ffi::catch_unwind] -pub unsafe extern "C" fn relay_validate_register_response( - data: *const RelayBuf, - signature: *const RelayStr, - secret: *const RelayStr, - max_age: u32, -) -> RelayStr { - let max_age = match max_age { - 0 => None, - m => Some(Duration::seconds(i64::from(m))), - }; - - let (response, state) = RegisterResponse::unpack( - (*data).as_bytes(), - (*signature).as_str(), - (*secret).as_str().as_bytes(), - max_age, - )?; - - let relay_response = RelayRegisterResponse { - relay_id: response.relay_id(), - token: response.token(), - public_key: state.public_key(), - version: response.version(), - }; - - let json = serde_json::to_string(&relay_response)?; - RelayStr::from_string(json) -} - -/// Returns true if the given version is supported by this library. -#[no_mangle] -#[relay_ffi::catch_unwind] -pub unsafe extern "C" fn relay_version_supported(version: &RelayStr) -> bool { - let relay_version = match version.as_str() { - "" => RelayVersion::default(), - s => s.parse::()?, - }; - - relay_version.supported() -} diff --git a/relay-cabi/src/constants.rs b/relay-cabi/src/constants.rs deleted file mode 100644 index 301ffd27ebe..00000000000 --- a/relay-cabi/src/constants.rs +++ /dev/null @@ -1,32 +0,0 @@ -pub use relay_base_schema::data_category::DataCategory; -pub use relay_base_schema::events::EventType; -pub use relay_base_schema::spans::SpanStatus; - -use crate::core::RelayStr; - -/// Returns the API name of the given `DataCategory`. -#[no_mangle] -#[relay_ffi::catch_unwind] -pub unsafe extern "C" fn relay_data_category_name(category: DataCategory) -> RelayStr { - RelayStr::new(category.name()) -} - -/// Parses a `DataCategory` from its API name. -#[no_mangle] -#[relay_ffi::catch_unwind] -pub unsafe extern "C" fn relay_data_category_parse(name: *const RelayStr) -> DataCategory { - (*name).as_str().parse().unwrap_or(DataCategory::Unknown) -} - -/// Parses a `DataCategory` from an event type. -#[no_mangle] -#[relay_ffi::catch_unwind] -pub unsafe extern "C" fn relay_data_category_from_event_type( - event_type: *const RelayStr, -) -> DataCategory { - (*event_type) - .as_str() - .parse::() - .unwrap_or_default() - .into() -} diff --git a/relay-cabi/src/core.rs b/relay-cabi/src/core.rs deleted file mode 100644 index e7d6df8e9ce..00000000000 --- a/relay-cabi/src/core.rs +++ /dev/null @@ -1,208 +0,0 @@ -use std::ffi::CStr; -use std::os::raw::c_char; -use std::{mem, ptr, slice, str}; - -use uuid::Uuid; - -/// A length-prefixed UTF-8 string. -/// -/// As opposed to C strings, this string is not null-terminated. If the string is owned, indicated -/// by the `owned` flag, the owner must call the `free` function on this string. The convention is: -/// -/// - When obtained as instance through return values, always free the string. -/// - When obtained as pointer through field access, never free the string. -#[repr(C)] -pub struct RelayStr { - /// Pointer to the UTF-8 encoded string data. - pub data: *mut c_char, - /// The length of the string pointed to by `data`. - pub len: usize, - /// Indicates that the string is owned and must be freed. - pub owned: bool, -} - -impl RelayStr { - /// Creates a new `RelayStr` by borrowing the given `&str`. - pub(crate) fn new(s: &str) -> RelayStr { - RelayStr { - data: s.as_ptr() as *mut c_char, - len: s.len(), - owned: false, - } - } - - /// Creates a new `RelayStr` by assuming ownership over the given `String`. - /// - /// When dropping this `RelayStr` instance, the buffer is freed. - pub(crate) fn from_string(mut s: String) -> RelayStr { - s.shrink_to_fit(); - let rv = RelayStr { - data: s.as_ptr() as *mut c_char, - len: s.len(), - owned: true, - }; - mem::forget(s); - rv - } - - /// Frees the string buffer if it is owned. - pub(crate) unsafe fn free(&mut self) { - if self.owned { - String::from_raw_parts(self.data as *mut _, self.len, self.len); - self.data = ptr::null_mut(); - self.len = 0; - self.owned = false; - } - } - - /// Returns a borrowed string. - pub(crate) unsafe fn as_str(&self) -> &str { - str::from_utf8_unchecked(slice::from_raw_parts(self.data as *const _, self.len)) - } -} - -// RelayStr is immutable, thus it can be Send + Sync -unsafe impl Sync for RelayStr {} -unsafe impl Send for RelayStr {} - -impl Default for RelayStr { - fn default() -> RelayStr { - RelayStr { - data: ptr::null_mut(), - len: 0, - owned: false, - } - } -} - -impl From for RelayStr { - fn from(string: String) -> RelayStr { - RelayStr::from_string(string) - } -} - -impl<'a> From<&'a str> for RelayStr { - fn from(string: &str) -> RelayStr { - RelayStr::new(string) - } -} - -/// Creates a Relay string from a c string. -#[no_mangle] -#[relay_ffi::catch_unwind] -pub unsafe extern "C" fn relay_str_from_cstr(s: *const c_char) -> RelayStr { - let s = CStr::from_ptr(s).to_str()?; - RelayStr { - data: s.as_ptr() as *mut _, - len: s.len(), - owned: false, - } -} - -/// Frees a Relay str. -/// -/// If the string is marked as not owned then this function does not -/// do anything. -#[no_mangle] -#[relay_ffi::catch_unwind] -pub unsafe extern "C" fn relay_str_free(s: *mut RelayStr) { - if !s.is_null() { - (*s).free() - } -} - -/// A 16-byte UUID. -#[repr(C)] -pub struct RelayUuid { - /// UUID bytes in network byte order (big endian). - pub data: [u8; 16], -} - -impl RelayUuid { - pub(crate) fn new(uuid: Uuid) -> RelayUuid { - let data = *uuid.as_bytes(); - Self { data } - } -} - -impl From for RelayUuid { - fn from(uuid: Uuid) -> RelayUuid { - RelayUuid::new(uuid) - } -} - -/// Returns true if the uuid is nil. -#[no_mangle] -#[relay_ffi::catch_unwind] -pub unsafe extern "C" fn relay_uuid_is_nil(uuid: *const RelayUuid) -> bool { - if let Ok(uuid) = Uuid::from_slice(&(*uuid).data[..]) { - uuid == Uuid::nil() - } else { - false - } -} - -/// Formats the UUID into a string. -/// -/// The string is newly allocated and needs to be released with -/// `relay_cstr_free`. -#[no_mangle] -#[relay_ffi::catch_unwind] -pub unsafe extern "C" fn relay_uuid_to_str(uuid: *const RelayUuid) -> RelayStr { - let uuid = Uuid::from_slice(&(*uuid).data[..]).unwrap_or_else(|_| Uuid::nil()); - RelayStr::from_string(uuid.as_hyphenated().to_string()) -} - -/// A binary buffer of known length. -/// -/// If the buffer is owned, indicated by the `owned` flag, the owner must call the `free` function -/// on this buffer. The convention is: -/// -/// - When obtained as instance through return values, always free the buffer. -/// - When obtained as pointer through field access, never free the buffer. -#[repr(C)] -pub struct RelayBuf { - /// Pointer to the raw data. - pub data: *mut u8, - /// The length of the buffer pointed to by `data`. - pub len: usize, - /// Indicates that the buffer is owned and must be freed. - pub owned: bool, -} - -impl RelayBuf { - pub(crate) unsafe fn free(&mut self) { - if self.owned { - Vec::from_raw_parts(self.data, self.len, self.len); - self.data = ptr::null_mut(); - self.len = 0; - self.owned = false; - } - } - - pub(crate) unsafe fn as_bytes(&self) -> &[u8] { - slice::from_raw_parts(self.data as *const u8, self.len) - } -} - -impl Default for RelayBuf { - fn default() -> RelayBuf { - RelayBuf { - data: ptr::null_mut(), - len: 0, - owned: false, - } - } -} - -/// Frees a Relay buf. -/// -/// If the buffer is marked as not owned then this function does not -/// do anything. -#[no_mangle] -#[relay_ffi::catch_unwind] -pub unsafe extern "C" fn relay_buf_free(b: *mut RelayBuf) { - if !b.is_null() { - (*b).free() - } -} diff --git a/relay-cabi/src/ffi.rs b/relay-cabi/src/ffi.rs deleted file mode 100644 index 7c202cf7a47..00000000000 --- a/relay-cabi/src/ffi.rs +++ /dev/null @@ -1,137 +0,0 @@ -use relay_auth::{KeyParseError, UnpackError}; -use relay_event_normalization::GeoIpError; -use relay_event_schema::processor::ProcessingAction; -use relay_ffi::Panic; -use sentry_release_parser::InvalidRelease; - -use crate::core::RelayStr; - -/// Represents all possible error codes. -#[repr(u32)] -#[allow(missing_docs)] -pub enum RelayErrorCode { - NoError = 0, - Panic = 1, - Unknown = 2, - - InvalidJsonError = 101, // serde_json::Error - - // relay_auth::KeyParseError - KeyParseErrorBadEncoding = 1000, - KeyParseErrorBadKey = 1001, - - // relay_auth::UnpackError - UnpackErrorBadSignature = 1003, - UnpackErrorBadPayload = 1004, - UnpackErrorSignatureExpired = 1005, - UnpackErrorBadEncoding = 1006, - - // relay_protocol::annotated::ProcessingAction - ProcessingErrorInvalidTransaction = 2001, - ProcessingErrorInvalidGeoIp = 2002, - - // sentry_release_parser::InvalidRelease - InvalidReleaseErrorTooLong = 3001, - InvalidReleaseErrorRestrictedName = 3002, - InvalidReleaseErrorBadCharacters = 3003, -} - -impl RelayErrorCode { - /// This maps all errors that can possibly happen. - pub fn from_error(error: &anyhow::Error) -> RelayErrorCode { - for cause in error.chain() { - if cause.downcast_ref::().is_some() { - return RelayErrorCode::Panic; - } - if cause.downcast_ref::().is_some() { - return RelayErrorCode::InvalidJsonError; - } - if cause.downcast_ref::().is_some() { - return RelayErrorCode::ProcessingErrorInvalidGeoIp; - } - if let Some(err) = cause.downcast_ref::() { - return match err { - KeyParseError::BadEncoding => RelayErrorCode::KeyParseErrorBadEncoding, - KeyParseError::BadKey => RelayErrorCode::KeyParseErrorBadKey, - }; - } - if let Some(err) = cause.downcast_ref::() { - return match err { - UnpackError::BadSignature => RelayErrorCode::UnpackErrorBadSignature, - UnpackError::BadPayload(..) => RelayErrorCode::UnpackErrorBadPayload, - UnpackError::SignatureExpired => RelayErrorCode::UnpackErrorSignatureExpired, - UnpackError::BadEncoding => RelayErrorCode::UnpackErrorBadEncoding, - }; - } - if let Some(err) = cause.downcast_ref::() { - return match err { - ProcessingAction::InvalidTransaction(_) => { - RelayErrorCode::ProcessingErrorInvalidTransaction - } - _ => RelayErrorCode::Unknown, - }; - } - if let Some(err) = cause.downcast_ref::() { - return match err { - InvalidRelease::TooLong => RelayErrorCode::InvalidReleaseErrorTooLong, - InvalidRelease::RestrictedName => { - RelayErrorCode::InvalidReleaseErrorRestrictedName - } - InvalidRelease::BadCharacters => { - RelayErrorCode::InvalidReleaseErrorBadCharacters - } - }; - } - } - RelayErrorCode::Unknown - } -} - -/// Initializes the library -#[no_mangle] -pub extern "C" fn relay_init() { - relay_ffi::set_panic_hook(); -} - -/// Returns the last error code. -/// -/// If there is no error, 0 is returned. -#[no_mangle] -pub extern "C" fn relay_err_get_last_code() -> RelayErrorCode { - relay_ffi::with_last_error(RelayErrorCode::from_error).unwrap_or(RelayErrorCode::NoError) -} - -/// Returns the last error message. -/// -/// If there is no error an empty string is returned. This allocates new memory -/// that needs to be freed with `relay_str_free`. -#[no_mangle] -pub extern "C" fn relay_err_get_last_message() -> RelayStr { - use std::fmt::Write; - relay_ffi::with_last_error(|err| { - let mut msg = err.to_string(); - for cause in err.chain().skip(1) { - write!(&mut msg, "\n caused by: {cause}").ok(); - } - RelayStr::from_string(msg) - }) - .unwrap_or_default() -} - -/// Returns the panic information as string. -#[no_mangle] -pub extern "C" fn relay_err_get_backtrace() -> RelayStr { - let backtrace = relay_ffi::with_last_error(|error| error.backtrace().to_string()) - .filter(|bt| !bt.is_empty()); - - match backtrace { - Some(backtrace) => RelayStr::from_string(format!("stacktrace: {backtrace}")), - None => RelayStr::default(), - } -} - -/// Clears the last error. -#[no_mangle] -pub extern "C" fn relay_err_clear() { - relay_ffi::take_last_error(); -} diff --git a/relay-cabi/src/lib.rs b/relay-cabi/src/lib.rs deleted file mode 100644 index 57784d83a20..00000000000 --- a/relay-cabi/src/lib.rs +++ /dev/null @@ -1,112 +0,0 @@ -//! This package contains the C-ABI of Relay. It builds a C header file and -//! associated library that can be linked to any C or C++ program. Note that the -//! header is C only. Primarily, this package is meant to be consumed by -//! higher-level wrappers in other languages, such as the -//! [`sentry-relay`](https://pypi.org/project/sentry-relay/) Python package. -//! -//! # Building -//! -//! Building the dynamic library has the same requirements as building the -//! `relay` crate: -//! -//! - Latest stable Rust and Cargo -//! - A checkout of this repository and all its GIT submodules -//! -//! To build, run `make release` in this directory. This creates a release build of -//! the dynamic library in `target/release/librelay_cabi.*`. -//! -//! # Usage -//! -//! The header comes packaged in the `include/` directory. It does not have any -//! dependencies other than standard C headers. Then, include it in sources and use -//! like this: -//! -//! ```c -//! #include "relay.h" -//! -//! int main() { -//! RelayUuid uuid = relay_generate_relay_id(); -//! // use uuid -//! -//! return 0; -//! } -//! ``` -//! -//! In your application, point to the Relay include directory and specify the -//! `relay` library: -//! -//! ```bash -//! $(CC) -Irelay-cabi/include -Ltarget/release -lrelay -o myprogram main.c -//! ``` -//! -//! # Development -//! -//! ## Requirements -//! -//! In addition to the build requirements, development requires a recent version -//! of the the `cbindgen` tool. To set this up, run: -//! -//! ```bash -//! cargo install cbindgen -//! ``` -//! -//! If your machine already has `cbindgen` installed, check the header of -//! [`include/relay.h`] for the minimum version required. This can be verified by -//! running `cbindgen --version`. It is generally advisable to keep `cbindgen` -//! updated to its latest version, and always check in cbindgen updates separate -//! from changes to the public interface. -//! -//! ## Makefile -//! -//! This package contains the Rust crate `relay-cabi` that exposes a public FFI -//! interface. There are additional build steps involved in generating the header -//! file located at `include/relay.h`. To aid development, there is a _Makefile_ -//! that exposes all relevant commands for building: -//! -//! - `make build`: Builds the library using `cargo`. -//! - `make header`: Updates the header file based on the public interface. -//! - `make clean`: Removes all build artifacts but leaves the header. -//! - `make`: Builds the library and the header. -//! -//! For ease of development, the header is always checked into the repository. After -//! making changes, do not forget to run at least `make header` to ensure the header -//! is in sync with the library. -//! -//! ## Development Flow -//! -//! 1. Make changes to the `relay` crates and add tests. -//! 2. Update `relay-cabi` and add, remove or update functions as needed. -//! 3. Regenerate the header by running `make header` in the `relay-cabi/` directory. -//! 4. Go to the Python package in the `py/` folder and update the high-level wrappers. -//! 5. Consider whether this changeset requires a major version bump. -//! -//! The general rule for major versions is: -//! -//! - If everything is backward compatible, do **not major** bump. -//! - If the C interface breaks compatibility but high-level wrappers are still -//! backwards compatible, do **not major** bump. The C interface is currently -//! viewed as an implementation detail. -//! - If high-level wrappers are no longer backwards compatible or there are -//! breaking changes in the `relay` crate itself, **do major** bump. - -#![allow(clippy::missing_safety_doc)] -#![warn(missing_docs)] -#![doc( - html_logo_url = "https://raw.githubusercontent.com/getsentry/relay/master/artwork/relay-icon.png", - html_favicon_url = "https://raw.githubusercontent.com/getsentry/relay/master/artwork/relay-icon.png" -)] -#![allow(clippy::derive_partial_eq_without_eq)] - -mod auth; -mod codeowners; -mod constants; -mod core; -mod ffi; -mod processing; - -pub use crate::auth::*; -pub use crate::codeowners::*; -pub use crate::constants::*; -pub use crate::core::*; -pub use crate::ffi::*; -pub use crate::processing::*; diff --git a/relay-cabi/src/processing.rs b/relay-cabi/src/processing.rs deleted file mode 100644 index c5d3f8b81e6..00000000000 --- a/relay-cabi/src/processing.rs +++ /dev/null @@ -1,562 +0,0 @@ -// TODO: Fix casts between RelayGeoIpLookup and GeoIpLookup -#![allow(clippy::cast_ptr_alignment)] -#![deny(unused_must_use)] -#![allow(clippy::derive_partial_eq_without_eq)] - -use std::cmp::Ordering; -use std::ffi::CStr; -use std::os::raw::c_char; -use std::slice; -use std::sync::OnceLock; - -use chrono::{DateTime, Utc}; -use relay_cardinality::CardinalityLimit; -use relay_common::glob::{glob_match_bytes, GlobOptions}; -use relay_dynamic_config::{normalize_json, GlobalConfig, ProjectConfig}; -use relay_event_normalization::{ - normalize_event, validate_event_timestamps, validate_transaction, BreakdownsConfig, - ClientHints, EventValidationConfig, GeoIpLookup, NormalizationConfig, RawUserAgentInfo, - TransactionValidationConfig, -}; -use relay_event_schema::processor::{process_value, split_chunks, ProcessingState}; -use relay_event_schema::protocol::{Event, IpAddr, VALID_PLATFORMS}; -use relay_pii::{ - selector_suggestions_from_value, DataScrubbingConfig, InvalidSelectorError, PiiConfig, - PiiConfigError, PiiProcessor, SelectorSpec, -}; -use relay_protocol::{Annotated, Remark, RuleCondition}; -use relay_sampling::SamplingConfig; -use serde::{Deserialize, Serialize}; -use uuid::Uuid; - -use crate::core::{RelayBuf, RelayStr}; - -/// Configuration for the store step -- validation and normalization. -#[derive(Serialize, Deserialize, Debug, Default)] -#[serde(default)] -pub struct StoreNormalizer { - /// The identifier of the target project, which gets added to the payload. - pub project_id: Option, - - /// The IP address of the SDK that sent the event. - /// - /// When `{{auto}}` is specified and there is no other IP address in the payload, such as in the - /// `request` context, this IP address gets added to the `user` context. - pub client_ip: Option, - - /// The name and version of the SDK that sent the event. - pub client: Option, - - /// The internal identifier of the DSN, which gets added to the payload. - /// - /// Note that this is different from the DSN's public key. The ID is usually numeric. - pub key_id: Option, - - /// The version of the protocol. - /// - /// This is a deprecated field, as there is no more versioning of Relay event payloads. - pub protocol_version: Option, - - /// Configuration for issue grouping. - /// - /// This configuration is persisted into the event payload to achieve idempotency in the - /// processing pipeline and for reprocessing. - pub grouping_config: Option, - - /// The raw user-agent string obtained from the submission request headers. - /// - /// The user agent is used to infer device, operating system, and browser information should the - /// event payload contain no such data. - /// - /// Newer browsers have frozen their user agents and send [`client_hints`](Self::client_hints) - /// instead. If both a user agent and client hints are present, normalization uses client hints. - pub user_agent: Option, - - /// A collection of headers sent by newer browsers about the device and environment. - /// - /// Client hints are the preferred way to infer device, operating system, and browser - /// information should the event payload contain no such data. If no client hints are present, - /// normalization falls back to the user agent. - pub client_hints: ClientHints, - - /// The time at which the event was received in this Relay. - /// - /// This timestamp is persisted into the event payload. - pub received_at: Option>, - - /// The time at which the event was sent by the client. - /// - /// The difference between this and the `received_at` timestamps is used for clock drift - /// correction, should a significant difference be detected. - pub sent_at: Option>, - - /// The maximum amount of seconds an event can be predated into the future. - /// - /// If the event's timestamp lies further into the future, the received timestamp is assumed. - pub max_secs_in_future: Option, - - /// The maximum amount of seconds an event can be dated in the past. - /// - /// If the event's timestamp is older, the received timestamp is assumed. - pub max_secs_in_past: Option, - - /// When `Some(true)`, individual parts of the event payload is trimmed to a maximum size. - /// - /// See the event schema for size declarations. - pub enable_trimming: Option, - - /// When `Some(true)`, it is assumed that the event has been normalized before. - /// - /// This disables certain normalizations, especially all that are not idempotent. The - /// renormalize mode is intended for the use in the processing pipeline, so an event modified - /// during ingestion can be validated against the schema and large data can be trimmed. However, - /// advanced normalizations such as inferring contexts or clock drift correction are disabled. - /// - /// `None` equals to `false`. - pub is_renormalize: Option, - - /// Overrides the default flag for other removal. - pub remove_other: Option, - - /// When `Some(true)`, context information is extracted from the user agent. - pub normalize_user_agent: Option, - - /// Emit breakdowns based on given configuration. - pub breakdowns: Option, - - /// The SDK's sample rate as communicated via envelope headers. - /// - /// It is persisted into the event payload. - pub client_sample_rate: Option, - - /// The identifier of the Replay running while this event was created. - /// - /// It is persisted into the event payload for correlation. - pub replay_id: Option, - - /// Controls whether spans should be normalized (e.g. normalizing the exclusive time). - /// - /// To normalize spans in [`normalize_event`], `is_renormalize` must - /// be disabled _and_ `normalize_spans` enabled. - pub normalize_spans: bool, -} - -impl StoreNormalizer { - /// Helper method to parse *mut StoreConfig -> &StoreConfig - fn this(&self) -> &Self { - self - } -} - -/// A geo ip lookup helper based on maxmind db files. -pub struct RelayGeoIpLookup; - -/// The processor that normalizes events for store. -pub struct RelayStoreNormalizer; - -/// Chunks the given text based on remarks. -#[no_mangle] -#[relay_ffi::catch_unwind] -pub unsafe extern "C" fn relay_split_chunks( - string: *const RelayStr, - remarks: *const RelayStr, -) -> RelayStr { - let remarks: Vec = serde_json::from_str((*remarks).as_str())?; - let chunks = split_chunks((*string).as_str(), &remarks); - let json = serde_json::to_string(&chunks)?; - json.into() -} - -/// Opens a maxminddb file by path. -#[no_mangle] -#[relay_ffi::catch_unwind] -pub unsafe extern "C" fn relay_geoip_lookup_new(path: *const c_char) -> *mut RelayGeoIpLookup { - let path = CStr::from_ptr(path).to_string_lossy(); - let lookup = GeoIpLookup::open(path.as_ref())?; - Box::into_raw(Box::new(lookup)) as *mut RelayGeoIpLookup -} - -/// Frees a `RelayGeoIpLookup`. -#[no_mangle] -#[relay_ffi::catch_unwind] -pub unsafe extern "C" fn relay_geoip_lookup_free(lookup: *mut RelayGeoIpLookup) { - if !lookup.is_null() { - let lookup = lookup as *mut GeoIpLookup; - let _dropped = Box::from_raw(lookup); - } -} - -/// Returns a list of all valid platform identifiers. -#[no_mangle] -#[relay_ffi::catch_unwind] -pub unsafe extern "C" fn relay_valid_platforms(size_out: *mut usize) -> *const RelayStr { - static VALID_PLATFORM_STRS: OnceLock> = OnceLock::new(); - let platforms = VALID_PLATFORM_STRS - .get_or_init(|| VALID_PLATFORMS.iter().map(|s| RelayStr::new(s)).collect()); - - if let Some(size_out) = size_out.as_mut() { - *size_out = platforms.len(); - } - - platforms.as_ptr() -} - -/// Creates a new normalization config. -#[no_mangle] -#[relay_ffi::catch_unwind] -pub unsafe extern "C" fn relay_store_normalizer_new( - config: *const RelayStr, - _geoip_lookup: *const RelayGeoIpLookup, -) -> *mut RelayStoreNormalizer { - let normalizer: StoreNormalizer = serde_json::from_str((*config).as_str())?; - Box::into_raw(Box::new(normalizer)) as *mut RelayStoreNormalizer -} - -/// Frees a `RelayStoreNormalizer`. -#[no_mangle] -#[relay_ffi::catch_unwind] -pub unsafe extern "C" fn relay_store_normalizer_free(normalizer: *mut RelayStoreNormalizer) { - if !normalizer.is_null() { - let normalizer = normalizer as *mut StoreNormalizer; - let _dropped = Box::from_raw(normalizer); - } -} - -/// Normalizes the event given as JSON. -#[no_mangle] -#[relay_ffi::catch_unwind] -pub unsafe extern "C" fn relay_store_normalizer_normalize_event( - normalizer: *mut RelayStoreNormalizer, - event: *const RelayStr, -) -> RelayStr { - let normalizer = normalizer as *mut StoreNormalizer; - let config = (*normalizer).this(); - let mut event = Annotated::::from_json((*event).as_str())?; - - let event_validation_config = EventValidationConfig { - received_at: config.received_at, - max_secs_in_past: config.max_secs_in_past, - max_secs_in_future: config.max_secs_in_future, - is_validated: config.is_renormalize.unwrap_or(false), - }; - validate_event_timestamps(&mut event, &event_validation_config)?; - - let tx_validation_config = TransactionValidationConfig { - timestamp_range: None, // only supported in relay - is_validated: config.is_renormalize.unwrap_or(false), - }; - validate_transaction(&mut event, &tx_validation_config)?; - - let is_renormalize = config.is_renormalize.unwrap_or(false); - - let normalization_config = NormalizationConfig { - project_id: config.project_id, - client: config.client.clone(), - protocol_version: config.protocol_version.clone(), - key_id: config.key_id.clone(), - grouping_config: config.grouping_config.clone(), - client_ip: config.client_ip.as_ref(), - client_sample_rate: config.client_sample_rate, - user_agent: RawUserAgentInfo { - user_agent: config.user_agent.as_deref(), - client_hints: config.client_hints.as_deref(), - }, - max_name_and_unit_len: None, - breakdowns_config: None, // only supported in relay - normalize_user_agent: config.normalize_user_agent, - transaction_name_config: Default::default(), // only supported in relay - is_renormalize, - remove_other: config.remove_other.unwrap_or(!is_renormalize), - emit_event_errors: !is_renormalize, - device_class_synthesis_config: false, // only supported in relay - enrich_spans: false, - max_tag_value_length: usize::MAX, - span_description_rules: None, - performance_score: None, - geoip_lookup: None, // only supported in relay - ai_model_costs: None, // only supported in relay - enable_trimming: config.enable_trimming.unwrap_or_default(), - measurements: None, - normalize_spans: config.normalize_spans, - replay_id: config.replay_id, - }; - normalize_event(&mut event, &normalization_config); - - RelayStr::from_string(event.to_json()?) -} - -/// Replaces invalid JSON generated by Python. -#[no_mangle] -#[relay_ffi::catch_unwind] -pub unsafe extern "C" fn relay_translate_legacy_python_json(event: *mut RelayStr) -> bool { - let data = slice::from_raw_parts_mut((*event).data as *mut u8, (*event).len); - json_forensics::translate_slice(data); - true -} - -/// Validates a PII selector spec. Used to validate datascrubbing safe fields. -#[no_mangle] -#[relay_ffi::catch_unwind] -pub unsafe extern "C" fn relay_validate_pii_selector(value: *const RelayStr) -> RelayStr { - let value = (*value).as_str(); - match value.parse::() { - Ok(_) => RelayStr::new(""), - Err(err) => match err { - InvalidSelectorError::ParseError(_) => { - // Change the error to something more concise we can show in an UI. - // Error message follows the same format used for fingerprinting rules. - RelayStr::from_string(format!("invalid syntax near {value:?}")) - } - err => RelayStr::from_string(err.to_string()), - }, - } -} - -/// Validate a PII config against the schema. Used in project options UI. -#[no_mangle] -#[relay_ffi::catch_unwind] -pub unsafe extern "C" fn relay_validate_pii_config(value: *const RelayStr) -> RelayStr { - match serde_json::from_str::((*value).as_str()) { - Ok(config) => match config.compiled().force_compile() { - Ok(_) => RelayStr::new(""), - Err(PiiConfigError::RegexError(source)) => RelayStr::from_string(source.to_string()), - }, - Err(e) => RelayStr::from_string(e.to_string()), - } -} - -/// Convert an old datascrubbing config to the new PII config format. -#[no_mangle] -#[relay_ffi::catch_unwind] -pub unsafe extern "C" fn relay_convert_datascrubbing_config(config: *const RelayStr) -> RelayStr { - let config: DataScrubbingConfig = serde_json::from_str((*config).as_str())?; - match config.pii_config() { - Ok(Some(config)) => RelayStr::from_string(serde_json::to_string(config)?), - Ok(None) => RelayStr::new("{}"), - // NOTE: Callers of this function must be able to handle this error. - Err(e) => RelayStr::from_string(e.to_string()), - } -} - -/// Scrub an event using new PII stripping config. -#[no_mangle] -#[relay_ffi::catch_unwind] -pub unsafe extern "C" fn relay_pii_strip_event( - config: *const RelayStr, - event: *const RelayStr, -) -> RelayStr { - let config = serde_json::from_str::((*config).as_str())?; - let mut processor = PiiProcessor::new(config.compiled()); - - let mut event = Annotated::::from_json((*event).as_str())?; - process_value(&mut event, &mut processor, ProcessingState::root())?; - - RelayStr::from_string(event.to_json()?) -} - -/// Walk through the event and collect selectors that can be applied to it in a PII config. This -/// function is used in the UI to provide auto-completion of selectors. -#[no_mangle] -#[relay_ffi::catch_unwind] -pub unsafe extern "C" fn relay_pii_selector_suggestions_from_event( - event: *const RelayStr, -) -> RelayStr { - let mut event = Annotated::::from_json((*event).as_str())?; - let rv = selector_suggestions_from_value(&mut event); - RelayStr::from_string(serde_json::to_string(&rv)?) -} - -/// A test function that always panics. -#[no_mangle] -#[relay_ffi::catch_unwind] -#[allow(clippy::diverging_sub_expression)] -pub unsafe extern "C" fn relay_test_panic() -> () { - panic!("this is a test panic") -} - -/// Controls the globbing behaviors. -#[repr(u32)] -pub enum GlobFlags { - /// When enabled `**` matches over path separators and `*` does not. - DoubleStar = 1, - /// Enables case insensitive path matching. - CaseInsensitive = 2, - /// Enables path normalization. - PathNormalize = 4, - /// Allows newlines. - AllowNewline = 8, -} - -/// Performs a glob operation on bytes. -/// -/// Returns `true` if the glob matches, `false` otherwise. -#[no_mangle] -#[relay_ffi::catch_unwind] -pub unsafe extern "C" fn relay_is_glob_match( - value: *const RelayBuf, - pat: *const RelayStr, - flags: GlobFlags, -) -> bool { - let mut options = GlobOptions::default(); - let flags = flags as u32; - if (flags & GlobFlags::DoubleStar as u32) != 0 { - options.double_star = true; - } - if (flags & GlobFlags::CaseInsensitive as u32) != 0 { - options.case_insensitive = true; - } - if (flags & GlobFlags::PathNormalize as u32) != 0 { - options.path_normalize = true; - } - if (flags & GlobFlags::AllowNewline as u32) != 0 { - options.allow_newline = true; - } - glob_match_bytes((*value).as_bytes(), (*pat).as_str(), options) -} - -/// Parse a sentry release structure from a string. -#[no_mangle] -#[relay_ffi::catch_unwind] -pub unsafe extern "C" fn relay_parse_release(value: *const RelayStr) -> RelayStr { - let release = sentry_release_parser::Release::parse((*value).as_str())?; - RelayStr::from_string(serde_json::to_string(&release)?) -} - -/// Compares two versions. -#[no_mangle] -#[relay_ffi::catch_unwind] -pub unsafe extern "C" fn relay_compare_versions(a: *const RelayStr, b: *const RelayStr) -> i32 { - let ver_a = sentry_release_parser::Version::parse((*a).as_str())?; - let ver_b = sentry_release_parser::Version::parse((*b).as_str())?; - match ver_a.cmp(&ver_b) { - Ordering::Less => -1, - Ordering::Equal => 0, - Ordering::Greater => 1, - } -} - -/// Validate a dynamic rule condition. -/// -/// Used by dynamic sampling, metric extraction, and metric tagging. -#[no_mangle] -#[relay_ffi::catch_unwind] -pub unsafe extern "C" fn relay_validate_rule_condition(value: *const RelayStr) -> RelayStr { - let ret_val = match serde_json::from_str::((*value).as_str()) { - Ok(condition) => { - if condition.supported() { - "".to_string() - } else { - "unsupported condition".to_string() - } - } - Err(e) => e.to_string(), - }; - RelayStr::from_string(ret_val) -} - -/// Validate whole rule ( this will be also implemented in Sentry for better error messages) -/// The implementation in relay is just to make sure that the Sentry implementation doesn't -/// go out of sync. -#[no_mangle] -#[relay_ffi::catch_unwind] -pub unsafe extern "C" fn relay_validate_sampling_configuration(value: *const RelayStr) -> RelayStr { - match serde_json::from_str::((*value).as_str()) { - Ok(config) => { - for rule in config.rules { - if !rule.condition.supported() { - return Ok(RelayStr::new("unsupported sampling rule")); - } - } - RelayStr::default() - } - Err(e) => RelayStr::from_string(e.to_string()), - } -} - -/// Normalize a project config. -#[no_mangle] -#[relay_ffi::catch_unwind] -pub unsafe extern "C" fn relay_normalize_project_config(value: *const RelayStr) -> RelayStr { - let value = (*value).as_str(); - match normalize_json::(value) { - Ok(normalized) => RelayStr::from_string(normalized), - Err(e) => RelayStr::from_string(e.to_string()), - } -} - -/// Normalize a cardinality limit config. -#[no_mangle] -#[relay_ffi::catch_unwind] -pub unsafe extern "C" fn normalize_cardinality_limit_config(value: *const RelayStr) -> RelayStr { - let value = (*value).as_str(); - match normalize_json::(value) { - Ok(normalized) => RelayStr::from_string(normalized), - Err(e) => RelayStr::from_string(e.to_string()), - } -} - -/// Normalize a global config. -#[no_mangle] -#[relay_ffi::catch_unwind] -pub unsafe extern "C" fn relay_normalize_global_config(value: *const RelayStr) -> RelayStr { - let value = (*value).as_str(); - match normalize_json::(value) { - Ok(normalized) => RelayStr::from_string(normalized), - Err(e) => RelayStr::from_string(e.to_string()), - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn pii_config_validation_invalid_regex() { - let config = r#" - { - "rules": { - "strip-fields": { - "type": "redact_pair", - "keyPattern": "(not valid regex", - "redaction": { - "method": "replace", - "text": "[Filtered]" - } - } - }, - "applications": { - "*.everything": ["strip-fields"] - } - } - "#; - assert_eq!( - unsafe { relay_validate_pii_config(&RelayStr::from(config)).as_str() }, - "regex parse error:\n (not valid regex\n ^\nerror: unclosed group" - ); - } - - #[test] - fn pii_config_validation_valid_regex() { - let config = r#" - { - "rules": { - "strip-fields": { - "type": "redact_pair", - "keyPattern": "(\\w+)?+", - "redaction": { - "method": "replace", - "text": "[Filtered]" - } - } - }, - "applications": { - "*.everything": ["strip-fields"] - } - } - "#; - assert_eq!( - unsafe { relay_validate_pii_config(&RelayStr::from(config)).as_str() }, - "" - ); - } -} diff --git a/relay-dynamic-config/Cargo.toml b/relay-dynamic-config/Cargo.toml index e6e1fbd3bed..7fc960dc038 100644 --- a/relay-dynamic-config/Cargo.toml +++ b/relay-dynamic-config/Cargo.toml @@ -31,6 +31,7 @@ relay-sampling = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } smallvec = { workspace = true } +pyo3 = { workspace = true, features = ["serde"] } [dev-dependencies] insta = { workspace = true } diff --git a/relay-dynamic-config/src/global.rs b/relay-dynamic-config/src/global.rs index e25eed2d9e0..44b7c362962 100644 --- a/relay-dynamic-config/src/global.rs +++ b/relay-dynamic-config/src/global.rs @@ -4,6 +4,7 @@ use std::fs::File; use std::io::BufReader; use std::path::Path; +use pyo3::prelude::*; use relay_base_schema::metrics::MetricNamespace; use relay_event_normalization::{MeasurementsConfig, ModelCosts}; use relay_filter::GenericFiltersConfig; @@ -20,6 +21,7 @@ use crate::{defaults, ErrorBoundary, MetricExtractionGroup, MetricExtractionGrou #[derive(Default, Clone, Debug, Serialize, Deserialize)] #[serde(default, rename_all = "camelCase")] #[cfg_attr(feature = "jsonschema", derive(JsonSchema))] +#[pyclass] pub struct GlobalConfig { /// Configuration for measurements normalization. #[serde(skip_serializing_if = "Option::is_none")] diff --git a/relay-event-normalization/Cargo.toml b/relay-event-normalization/Cargo.toml index 215a3bd1b80..e1c01cec3cd 100644 --- a/relay-event-normalization/Cargo.toml +++ b/relay-event-normalization/Cargo.toml @@ -35,6 +35,7 @@ serde_json = { workspace = true } serde_urlencoded = { workspace = true } smallvec = { workspace = true } sqlparser = { workspace = true, features = ["visitor"] } +pyo3 = { workspace = true, features = ["serde"] } thiserror = { workspace = true } url = { workspace = true } diff --git a/relay-event-normalization/src/geo.rs b/relay-event-normalization/src/geo.rs index 94eb4a7c392..161ecf5532b 100644 --- a/relay-event-normalization/src/geo.rs +++ b/relay-event-normalization/src/geo.rs @@ -1,6 +1,10 @@ use std::fmt; +use std::fmt::Formatter; use std::path::Path; +use pyo3::exceptions::PyValueError; +use pyo3::prelude::*; + use relay_event_schema::protocol::Geo; use relay_protocol::Annotated; @@ -11,24 +15,49 @@ type ReaderType = maxminddb::Mmap; type ReaderType = Vec; /// An error in the `GeoIpLookup`. -pub type GeoIpError = maxminddb::MaxMindDBError; +#[derive(Debug)] +pub struct GeoIpError(maxminddb::MaxMindDBError); + +impl fmt::Display for GeoIpError { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } +} + +impl std::error::Error for GeoIpError {} + +impl From for PyErr { + fn from(err: GeoIpError) -> Self { + pyo3::exceptions::PyOSError::new_err(err.0.to_string()) + } +} /// A geo ip lookup helper based on maxmind db files. +#[pyclass] pub struct GeoIpLookup(maxminddb::Reader); +/// Pyo3 implementations +#[pymethods] impl GeoIpLookup { /// Opens a maxminddb file by path. - pub fn open

(path: P) -> Result - where - P: AsRef, - { + #[staticmethod] + #[pyo3(name = "open")] + pub fn open_pyo3(path: &str) -> PyResult { #[cfg(feature = "mmap")] - let reader = maxminddb::Reader::open_mmap(path)?; + let reader = + maxminddb::Reader::open_mmap(path).map_err(|e| PyValueError::new_err(e.to_string()))?; #[cfg(not(feature = "mmap"))] let reader = maxminddb::Reader::open_readfile(path)?; Ok(GeoIpLookup(reader)) } + /// Opens a maxminddb file by path. + /// Used by Python interface. + #[staticmethod] + pub fn from_path(path: &str) -> PyResult { + GeoIpLookup::open_pyo3(path) + } + /// Looks up an IP address. pub fn lookup(&self, ip_address: &str) -> Result, GeoIpError> { // XXX: Why do we parse the IP again after deserializing? @@ -39,8 +68,8 @@ impl GeoIpLookup { let city: maxminddb::geoip2::City = match self.0.lookup(ip_address) { Ok(x) => x, - Err(GeoIpError::AddressNotFoundError(_)) => return Ok(None), - Err(e) => return Err(e), + Err(maxminddb::MaxMindDBError::AddressNotFoundError(_)) => return Ok(None), + Err(e) => return Err(GeoIpError(e)), }; Ok(Some(Geo { @@ -73,6 +102,20 @@ impl GeoIpLookup { } } +impl GeoIpLookup { + /// Opens a maxminddb file by path. + pub fn open

(path: P) -> Result + where + P: AsRef, + { + #[cfg(feature = "mmap")] + let reader = maxminddb::Reader::open_mmap(path).map_err(GeoIpError)?; + #[cfg(not(feature = "mmap"))] + let reader = maxminddb::Reader::open_readfile(path)?; + Ok(GeoIpLookup(reader)) + } +} + impl fmt::Debug for GeoIpLookup { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("GeoIpLookup").finish() diff --git a/relay-event-normalization/src/normalize/breakdowns.rs b/relay-event-normalization/src/normalize/breakdowns.rs index 05dc7d89179..a3e6e45376d 100644 --- a/relay-event-normalization/src/normalize/breakdowns.rs +++ b/relay-event-normalization/src/normalize/breakdowns.rs @@ -7,6 +7,7 @@ use std::collections::HashMap; use std::ops::Deref; use std::time::Duration; +use pyo3::prelude::*; use relay_base_schema::metrics::{DurationUnit, MetricUnit}; use relay_event_schema::protocol::{Breakdowns, Event, Measurement, Measurements, Timestamp}; use relay_protocol::Annotated; @@ -189,6 +190,7 @@ type BreakdownName = String; /// Breakdowns are product-defined numbers that are indirectly reported by the client, and are /// materialized during ingestion. They are usually an aggregation over data present in the event. #[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[pyclass] pub struct BreakdownsConfig(pub HashMap); impl Deref for BreakdownsConfig { diff --git a/relay-event-schema/Cargo.toml b/relay-event-schema/Cargo.toml index fe6200f2ffd..d4331d5af99 100644 --- a/relay-event-schema/Cargo.toml +++ b/relay-event-schema/Cargo.toml @@ -30,6 +30,7 @@ serde_json = { workspace = true } thiserror = { workspace = true } url = { workspace = true } uuid = { workspace = true } +pyo3 = { workspace = true, features = ["serde"] } [dev-dependencies] insta = { workspace = true } diff --git a/relay-event-schema/src/processor/chunks.rs b/relay-event-schema/src/processor/chunks.rs index 58db9f4602f..91872aad4ad 100644 --- a/relay-event-schema/src/processor/chunks.rs +++ b/relay-event-schema/src/processor/chunks.rs @@ -25,6 +25,8 @@ use std::borrow::Cow; use std::fmt; +use pyo3::prelude::*; +use pyo3::types::IntoPyDict; use relay_protocol::{Meta, Remark, RemarkType}; use serde::{Deserialize, Serialize}; @@ -43,12 +45,33 @@ pub enum Chunk<'a> { text: Cow<'a, str>, /// The rule that crated this redaction rule_id: Cow<'a, str>, - /// Type type of remark for this redaction + /// Type of remark for this redaction #[serde(rename = "remark")] ty: RemarkType, }, } +impl<'a> IntoPy for Chunk<'a> { + fn into_py(self, py: Python<'_>) -> PyObject { + match self { + Self::Text { text } => { + let dict = [("type", "text"), ("text", &text)].into_py_dict_bound(py); + dict.to_object(py) + } + Self::Redaction { text, rule_id, ty } => { + let dict = [ + ("type", "redaction"), + ("text", &text), + ("rule_id", &rule_id), + ("ty", &format!("{:?}", ty)), + ] + .into_py_dict_bound(py); + dict.to_object(py) + } + } + } +} + impl<'a> Chunk<'a> { /// The text of this chunk. pub fn as_str(&self) -> &str { diff --git a/relay-event-schema/src/processor/traits.rs b/relay-event-schema/src/processor/traits.rs index ac1a78728d7..1350f312c83 100644 --- a/relay-event-schema/src/processor/traits.rs +++ b/relay-event-schema/src/processor/traits.rs @@ -24,6 +24,12 @@ pub enum ProcessingAction { InvalidTransaction(&'static str), } +impl From for PyErr { + fn from(value: ProcessingAction) -> Self { + PyValueError::new_err(value.to_string()) + } +} + /// The result of running a processor on a value implementing `ProcessValue`. pub type ProcessingResult = Result<(), ProcessingAction>; @@ -136,6 +142,8 @@ pub trait Processor: Sized { #[doc(inline)] pub use enumset::{enum_set, EnumSet}; +use pyo3::exceptions::PyValueError; +use pyo3::PyErr; /// A recursively processable value. pub trait ProcessValue: FromValue + IntoValue + Debug + Clone { diff --git a/relay-event-schema/src/protocol/event.rs b/relay-event-schema/src/protocol/event.rs index eeee238c682..760f2920d16 100644 --- a/relay-event-schema/src/protocol/event.rs +++ b/relay-event-schema/src/protocol/event.rs @@ -1,3 +1,4 @@ +use pyo3::prelude::*; use std::fmt; use std::str::FromStr; @@ -135,6 +136,7 @@ pub struct GroupingConfig { #[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, IntoValue, ProcessValue)] #[cfg_attr(feature = "jsonschema", derive(JsonSchema))] #[metastructure(process_func = "process_event", value_type = "Event")] +#[pyclass] pub struct Event { /// Unique identifier of this event. /// diff --git a/relay-event-schema/src/protocol/types.rs b/relay-event-schema/src/protocol/types.rs index c423a31d5d3..b044b4a6e87 100644 --- a/relay-event-schema/src/protocol/types.rs +++ b/relay-event-schema/src/protocol/types.rs @@ -7,6 +7,7 @@ use std::{fmt, net}; use chrono::{DateTime, Datelike, Duration, LocalResult, NaiveDateTime, TimeZone, Utc}; use enumset::EnumSet; +use pyo3::prelude::*; #[cfg(feature = "jsonschema")] use relay_jsonschema_derive::JsonSchema; use relay_protocol::{ @@ -505,6 +506,7 @@ relay_common::impl_str_serde!(Addr, "an address"); #[derive( Clone, Debug, Eq, PartialEq, PartialOrd, Ord, Hash, Empty, IntoValue, ProcessValue, Serialize, )] +#[pyclass] pub struct IpAddr(pub String); #[cfg(feature = "jsonschema")] diff --git a/relay-event-schema/src/protocol/user.rs b/relay-event-schema/src/protocol/user.rs index 88263fa9c08..f0fbf16b0e2 100644 --- a/relay-event-schema/src/protocol/user.rs +++ b/relay-event-schema/src/protocol/user.rs @@ -4,11 +4,13 @@ use relay_protocol::{Annotated, Empty, FromValue, IntoValue, Object, Value}; use crate::processor::ProcessValue; use crate::protocol::{IpAddr, LenientString}; +use pyo3::prelude::*; /// Geographical location of the end user or device. #[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, IntoValue, ProcessValue)] #[cfg_attr(feature = "jsonschema", derive(JsonSchema))] #[metastructure(process_func = "process_geo")] +#[pyclass] pub struct Geo { /// Two-letter country code (ISO 3166-1 alpha-2). #[metastructure(pii = "true", max_chars = 102, max_chars_allowance = 1004)] diff --git a/relay-ffi-macros/Cargo.toml b/relay-ffi-macros/Cargo.toml deleted file mode 100644 index c75d933eeda..00000000000 --- a/relay-ffi-macros/Cargo.toml +++ /dev/null @@ -1,20 +0,0 @@ -[package] -name = "relay-ffi-macros" -authors = ["Sentry "] -description = "Macros for error handling in FFI bindings" -homepage = "https://getsentry.github.io/relay/" -repository = "https://github.com/getsentry/relay" -version = "24.4.2" -edition = "2021" -license-file = "../LICENSE.md" -publish = false - -[lib] -proc-macro = true - -[lints] -workspace = true - -[dependencies] -syn = { workspace = true, features = ["fold", "full"] } -quote = { workspace = true } diff --git a/relay-ffi-macros/src/lib.rs b/relay-ffi-macros/src/lib.rs deleted file mode 100644 index 14d58037d29..00000000000 --- a/relay-ffi-macros/src/lib.rs +++ /dev/null @@ -1,66 +0,0 @@ -//! Macros for [`relay-ffi`]. -//! -//! [`relay-ffi`]: ../relay_ffi/index.html - -#![warn(missing_docs)] -#![doc( - html_logo_url = "https://raw.githubusercontent.com/getsentry/relay/master/artwork/relay-icon.png", - html_favicon_url = "https://raw.githubusercontent.com/getsentry/relay/master/artwork/relay-icon.png" -)] -#![allow(clippy::derive_partial_eq_without_eq)] - -use proc_macro::TokenStream; -use quote::ToTokens; -use syn::fold::Fold; - -struct CatchUnwind; - -impl CatchUnwind { - fn fold(&mut self, input: TokenStream) -> TokenStream { - let f = syn::parse(input).expect("#[catch_unwind] can only be applied to functions"); - self.fold_item_fn(f).to_token_stream().into() - } -} - -impl Fold for CatchUnwind { - fn fold_item_fn(&mut self, i: syn::ItemFn) -> syn::ItemFn { - if i.sig.unsafety.is_none() { - panic!("#[catch_unwind] requires `unsafe fn`"); - } - - let inner = i.block; - let folded = quote::quote! {{ - ::relay_ffi::__internal::catch_errors(|| { - let __ret = #inner; - - #[allow(unreachable_code)] - Ok(__ret) - }) - }}; - - let block = Box::new(syn::parse2(folded).unwrap()); - syn::ItemFn { block, ..i } - } -} - -/// Captures errors and panics in a thread-local on `unsafe` functions. -/// -/// See [`relay-ffi` documentation] for more information. -/// -/// # Examples -/// -/// ```ignore -/// use relay_ffi::catch_unwind; -/// -/// #[no_mangle] -/// #[catch_unwind] -/// pub unsafe extern "C" fn run_ffi() -> i32 { -/// "invalid".parse()? -/// } -/// ``` -/// -/// [`relay-ffi` documentation]: ../relay_ffi/index.html -#[proc_macro_attribute] -pub fn catch_unwind(_attr: TokenStream, item: TokenStream) -> TokenStream { - CatchUnwind.fold(item) -} diff --git a/relay-ffi/Cargo.toml b/relay-ffi/Cargo.toml deleted file mode 100644 index a7adccb7901..00000000000 --- a/relay-ffi/Cargo.toml +++ /dev/null @@ -1,17 +0,0 @@ -[package] -name = "relay-ffi" -authors = ["Sentry "] -description = "Utilities for error handling in FFI bindings" -homepage = "https://getsentry.github.io/relay/" -repository = "https://github.com/getsentry/relay" -version = "24.4.2" -edition = "2021" -license-file = "../LICENSE.md" -publish = false - -[lints] -workspace = true - -[dependencies] -anyhow = { workspace = true } -relay-ffi-macros = { workspace = true } diff --git a/relay-ffi/src/lib.rs b/relay-ffi/src/lib.rs deleted file mode 100644 index 3a2552a6ac2..00000000000 --- a/relay-ffi/src/lib.rs +++ /dev/null @@ -1,289 +0,0 @@ -//! Utilities for error handling in FFI bindings. -//! -//! This crate facilitates an [`errno`]-like error handling pattern: On success, the result of a -//! function call is returned. On error, a thread-local marker is set that allows to retrieve the -//! error, message, and a backtrace if available. -//! -//! # Catch Errors and Panics -//! -//! The [`catch_unwind`] attribute annotates functions that can internally throw errors. It allows -//! the use of the questionmark operator `?` in a function that does not return `Result`. The error -//! is then available using [`with_last_error`]: -//! -//! ``` -//! use relay_ffi::catch_unwind; -//! -//! #[catch_unwind] -//! unsafe fn parse_number() -> i32 { -//! // use the questionmark operator for errors: -//! let number: i32 = "42".parse()?; -//! -//! // return the value directly, not `Ok`: -//! number * 2 -//! } -//! ``` -//! -//! # Safety -//! -//! Since function calls always need to return a value, this crate has to return -//! `std::mem::zeroed()` as a placeholder in case of an error. This is unsafe for reference types -//! and function pointers. Because of this, functions must be marked `unsafe`. -//! -//! In most cases, FFI functions should return either `repr(C)` structs or pointers, in which case -//! this is safe in principle. The author of the API is responsible for defining the contract, -//! however, and document the behavior of custom structures in case of an error. -//! -//! # Examples -//! -//! Annotate FFI functions with [`catch_unwind`] to capture errors. The error can be inspected via -//! [`with_last_error`]: -//! -//! ``` -//! use relay_ffi::{catch_unwind, with_last_error}; -//! -//! #[catch_unwind] -//! unsafe fn parse_number() -> i32 { -//! "42".parse()? -//! } -//! -//! let parsed = unsafe { parse_number() }; -//! match with_last_error(|e| e.to_string()) { -//! Some(error) => println!("errored with: {error}"), -//! None => println!("result: {parsed}"), -//! } -//! ``` -//! -//! To capture panics, register the panic hook early during library initialization: -//! -//! ``` -//! use relay_ffi::{catch_unwind, with_last_error}; -//! -//! relay_ffi::set_panic_hook(); -//! -//! #[catch_unwind] -//! unsafe fn fail() { -//! panic!("expected panic"); -//! } -//! -//! unsafe { fail() }; -//! -//! if let Some(description) = with_last_error(|e| e.to_string()) { -//! println!("{description}"); -//! } -//! ``` -//! -//! # Creating C-APIs -//! -//! This is an example for exposing an API to C: -//! -//! ``` -//! use std::ffi::CString; -//! use std::os::raw::c_char; -//! -//! #[no_mangle] -//! pub unsafe extern "C" fn init_ffi() { -//! relay_ffi::set_panic_hook(); -//! } -//! -//! #[no_mangle] -//! pub unsafe extern "C" fn last_strerror() -> *mut c_char { -//! let ptr_opt = relay_ffi::with_last_error(|err| { -//! CString::new(err.to_string()) -//! .unwrap_or_default() -//! .into_raw() -//! }); -//! -//! ptr_opt.unwrap_or(std::ptr::null_mut()) -//! } -//! ``` -//! -//! [`errno`]: https://man7.org/linux/man-pages/man3/errno.3.html - -#![warn(missing_docs)] -#![doc( - html_logo_url = "https://raw.githubusercontent.com/getsentry/relay/master/artwork/relay-icon.png", - html_favicon_url = "https://raw.githubusercontent.com/getsentry/relay/master/artwork/relay-icon.png" -)] -#![allow(clippy::derive_partial_eq_without_eq)] - -use std::cell::RefCell; -use std::error::Error; -use std::{fmt, panic, thread}; - -pub use relay_ffi_macros::catch_unwind; - -thread_local! { - static LAST_ERROR: RefCell> = const { RefCell::new(None) }; -} - -fn set_last_error(err: anyhow::Error) { - LAST_ERROR.with(|e| { - *e.borrow_mut() = Some(err); - }); -} - -#[doc(hidden)] -pub mod __internal { - use super::*; - - /// Catches down panics and errors from the given closure. - /// - /// Returns the result of the passed function on success. On error or panic, returns - /// zero-initialized memory and sets the thread-local error. - /// - /// # Safety - /// - /// Returns `std::mem::zeroed` on error, which is unsafe for reference types and function - /// pointers. - #[inline] - pub unsafe fn catch_errors(f: F) -> T - where - F: FnOnce() -> Result + panic::UnwindSafe, - { - match panic::catch_unwind(f) { - Ok(Ok(result)) => result, - Ok(Err(err)) => { - set_last_error(err); - std::mem::zeroed() - } - Err(_) => std::mem::zeroed(), - } - } -} - -/// Acquires a reference to the last error and passes it to the callback, if any. -/// -/// Returns `Some(R)` if there was an error, otherwise `None`. The error resets when it is taken -/// with [`take_last_error`]. -/// -/// # Example -/// -/// ``` -/// use relay_ffi::{catch_unwind, with_last_error}; -/// -/// #[catch_unwind] -/// unsafe fn run_ffi() -> i32 { -/// "invalid".parse()? -/// } -/// -/// let parsed = unsafe { run_ffi() }; -/// match with_last_error(|e| e.to_string()) { -/// Some(error) => println!("errored with: {error}"), -/// None => println!("result: {parsed}"), -/// } -/// ``` -pub fn with_last_error(f: F) -> Option -where - F: FnOnce(&anyhow::Error) -> R, -{ - LAST_ERROR.with(|e| e.borrow().as_ref().map(f)) -} - -/// Takes the last error, leaving `None` in its place. -/// -/// To inspect the error without removing it, use [`with_last_error`]. -/// -/// # Example -/// -/// ``` -/// use relay_ffi::{catch_unwind, take_last_error}; -/// -/// #[catch_unwind] -/// unsafe fn run_ffi() -> i32 { -/// "invalid".parse()? -/// } -/// -/// let parsed = unsafe { run_ffi() }; -/// match take_last_error() { -/// Some(error) => println!("errored with: {error}"), -/// None => println!("result: {parsed}"), -/// } -/// ``` -pub fn take_last_error() -> Option { - LAST_ERROR.with(|e| e.borrow_mut().take()) -} - -/// An error representing a panic carrying the message as payload. -/// -/// To capture panics, register the hook using [`set_panic_hook`]. -/// -/// # Example -/// -/// ``` -/// use relay_ffi::{catch_unwind, with_last_error, Panic}; -/// -/// #[catch_unwind] -/// unsafe fn panics() { -/// panic!("this is fine"); -/// } -/// -/// relay_ffi::set_panic_hook(); -/// -/// unsafe { panics() }; -/// -/// with_last_error(|error| { -/// if let Some(panic) = error.downcast_ref::() { -/// println!("{}", panic.description()); -/// } -/// }); -/// ``` -#[derive(Debug)] -pub struct Panic(String); - -impl Panic { - fn new(info: &panic::PanicInfo) -> Self { - let thread = thread::current(); - let thread = thread.name().unwrap_or("unnamed"); - - let message = match info.payload().downcast_ref::<&str>() { - Some(s) => *s, - None => match info.payload().downcast_ref::() { - Some(s) => &**s, - None => "Box", - }, - }; - - let description = match info.location() { - Some(location) => format!( - "thread '{thread}' panicked with '{message}' at {}:{}", - location.file(), - location.line() - ), - None => format!("thread '{thread}' panicked with '{message}'"), - }; - - Self(description) - } - - /// Returns a description containing the location and message of the panic. - #[inline] - pub fn description(&self) -> &str { - &self.0 - } -} - -impl fmt::Display for Panic { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "panic: {}", self.description()) - } -} - -impl Error for Panic {} - -/// Registers a hook for capturing panics with backtraces. -/// -/// This function must be registered early when the FFI is initialized before any other calls are -/// made. Usually, this would be exported from an initialization function. -/// -/// See the [`Panic`] documentation for more information. -/// -/// # Example -/// -/// ``` -/// pub unsafe extern "C" fn init_ffi() { -/// relay_ffi::set_panic_hook(); -/// } -/// ``` -pub fn set_panic_hook() { - panic::set_hook(Box::new(|info| set_last_error(Panic::new(info).into()))); -} diff --git a/relay-ffi/tests/test_macro.rs b/relay-ffi/tests/test_macro.rs deleted file mode 100644 index 92e839cb073..00000000000 --- a/relay-ffi/tests/test_macro.rs +++ /dev/null @@ -1,51 +0,0 @@ -use relay_ffi::with_last_error; - -#[relay_ffi::catch_unwind] -unsafe fn returns_unit() {} - -#[relay_ffi::catch_unwind] -unsafe fn returns_int() -> i32 { - "42".parse()? -} - -#[relay_ffi::catch_unwind] -unsafe fn returns_error() -> i32 { - "invalid".parse()? -} - -#[relay_ffi::catch_unwind] -#[allow(clippy::diverging_sub_expression)] -unsafe fn panics() { - panic!("this is fine"); -} - -#[test] -fn test_unit() { - unsafe { returns_unit() } - assert!(with_last_error(|_| ()).is_none()) -} - -#[test] -fn test_ok() { - unsafe { returns_int() }; - assert!(with_last_error(|_| ()).is_none()) -} - -#[test] -fn test_error() { - unsafe { returns_error() }; - assert_eq!( - with_last_error(|e| e.to_string()).as_deref(), - Some("invalid digit found in string") - ) -} - -#[test] -fn test_panics() { - relay_ffi::set_panic_hook(); - - unsafe { panics() }; - - let last_error = with_last_error(|e| e.to_string()).expect("returned error"); - assert!(last_error.starts_with("panic: thread \'test_panics\' panicked with \'this is fine\'")); -} diff --git a/relay-pii/Cargo.toml b/relay-pii/Cargo.toml index 24edb7d80cb..16dd65685ba 100644 --- a/relay-pii/Cargo.toml +++ b/relay-pii/Cargo.toml @@ -28,6 +28,7 @@ sha1 = { workspace = true } smallvec = { workspace = true } thiserror = { workspace = true } utf16string = { workspace = true } +pyo3 = { workspace = true, features = ["serde"] } [dev-dependencies] insta = { workspace = true } diff --git a/relay-pii/src/config.rs b/relay-pii/src/config.rs index 3e9d44c6506..85a2b54c4c0 100644 --- a/relay-pii/src/config.rs +++ b/relay-pii/src/config.rs @@ -2,6 +2,7 @@ use std::borrow::Cow; use std::collections::{BTreeMap, BTreeSet}; use std::sync::OnceLock; +use pyo3::prelude::*; use regex::{Regex, RegexBuilder}; use serde::{Deserialize, Deserializer, Serialize, Serializer}; @@ -224,6 +225,7 @@ impl Vars { /// A set of named rule configurations. #[derive(Serialize, Deserialize, Debug, Default, Clone)] +#[pyclass] pub struct PiiConfig { /// A map of custom PII rules. #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] diff --git a/relay-pii/src/generate_selectors.rs b/relay-pii/src/generate_selectors.rs index 7ac5eff0e86..6f993226547 100644 --- a/relay-pii/src/generate_selectors.rs +++ b/relay-pii/src/generate_selectors.rs @@ -1,5 +1,6 @@ use std::collections::BTreeSet; +use pyo3::prelude::*; use relay_event_schema::processor::{ self, Pii, ProcessValue, ProcessingResult, ProcessingState, Processor, ValueType, }; @@ -12,6 +13,7 @@ use crate::utils; /// Metadata about a selector found in the event #[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Serialize)] +#[pyclass] pub struct SelectorSuggestion { /// The selector that users should be able to use to address the underlying value pub path: SelectorSpec, diff --git a/relay-pii/src/legacy.rs b/relay-pii/src/legacy.rs index 1fd08625141..ddc255335da 100644 --- a/relay-pii/src/legacy.rs +++ b/relay-pii/src/legacy.rs @@ -5,6 +5,7 @@ use std::sync::OnceLock; +use pyo3::prelude::*; use serde::{Deserialize, Serialize}; use crate::config::{PiiConfig, PiiConfigError}; @@ -13,6 +14,7 @@ use crate::convert; /// Configuration for Sentry's datascrubbing #[derive(Debug, Default, Clone, Serialize, Deserialize)] #[serde(default, rename_all = "camelCase")] +#[pyclass] pub struct DataScrubbingConfig { /// List with the fields to be excluded. pub exclude_fields: Vec, diff --git a/relay-protocol/Cargo.toml b/relay-protocol/Cargo.toml index 062e3d7ee75..11de3bcb709 100644 --- a/relay-protocol/Cargo.toml +++ b/relay-protocol/Cargo.toml @@ -14,6 +14,7 @@ workspace = true [dependencies] num-traits = { workspace = true } +pyo3 = { workspace = true, features = ["serde"] } relay-common = { workspace = true } relay-protocol-derive = { workspace = true, optional = true } schemars = { workspace = true, optional = true } @@ -32,3 +33,4 @@ default = [] derive = ["dep:relay-protocol-derive"] jsonschema = ["dep:schemars"] test = [] + diff --git a/relay-protocol/src/meta.rs b/relay-protocol/src/meta.rs index 80c17cfa393..406aeb188c1 100644 --- a/relay-protocol/src/meta.rs +++ b/relay-protocol/src/meta.rs @@ -1,6 +1,7 @@ use std::fmt; use std::str::FromStr; +use pyo3::prelude::*; use serde::ser::SerializeSeq; use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; use smallvec::SmallVec; @@ -36,6 +37,7 @@ pub enum RemarkType { /// Information on a modified section in a string. #[derive(Clone, Debug, PartialEq)] +#[pyclass] pub struct Remark { /// The kind of redaction that has been applied on the target value. pub ty: RemarkType, @@ -414,6 +416,7 @@ impl Serialize for Error { /// Meta information for a data field in the event payload. #[derive(Clone, Deserialize, Serialize)] +#[pyclass] struct MetaInner { /// Remarks detailing modifications of this field. #[serde(default, skip_serializing_if = "SmallVec::is_empty", rename = "rem")] @@ -444,6 +447,7 @@ impl MetaInner { /// Meta information for a data field in the event payload. #[derive(Clone, Default, Serialize)] +#[pyclass] pub struct Meta(Option>); impl fmt::Debug for Meta { diff --git a/relay-cabi/Cargo.toml b/relay-pyo3/Cargo.toml similarity index 82% rename from relay-cabi/Cargo.toml rename to relay-pyo3/Cargo.toml index 60fc6229a81..ef79894df0e 100644 --- a/relay-cabi/Cargo.toml +++ b/relay-pyo3/Cargo.toml @@ -1,15 +1,16 @@ [package] -name = "relay-cabi" +name = "relay-pyo3" version = "0.8.64" authors = ["Sentry "] homepage = "https://getsentry.github.io/relay/" repository = "https://github.com/getsentry/relay" description = "Exposes some internals of the relay to C." -edition = "2021" license-file = "../LICENSE.md" publish = false +edition = "2021" [lib] +name = "_relay_pyo3" crate-type = ["cdylib"] [lints] @@ -29,11 +30,19 @@ relay-common = { workspace = true } relay-dynamic-config = { workspace = true } relay-event-normalization = { workspace = true } relay-event-schema = { workspace = true } -relay-ffi = { workspace = true } relay-pii = { workspace = true } relay-protocol = { workspace = true } relay-sampling = { workspace = true } sentry-release-parser = { workspace = true, features = ["serde"] } serde = { workspace = true } serde_json = { workspace = true } +serde-pyobject = { workspace = true } +strum = { workspace = true, features = ["derive"] } uuid = { workspace = true } + +pyo3 = { workspace = true, features = [ + "anyhow", + "chrono", + "extension-module", + "serde", +] } diff --git a/relay-pyo3/src/auth.rs b/relay-pyo3/src/auth.rs new file mode 100644 index 00000000000..d818d825feb --- /dev/null +++ b/relay-pyo3/src/auth.rs @@ -0,0 +1,196 @@ +use std::collections::HashMap; + +use chrono::Duration; +use pyo3::exceptions::PyValueError; +use pyo3::prelude::*; +use pyo3::types::PyBytes; + +use relay_auth::{ + KeyParseError, PublicKey, RegisterRequest, RegisterResponse, RelayVersion, SecretKey, +}; + +use crate::utils::extract_bytes_or_str; + +#[pyclass(name = "PublicKey")] +#[derive(Clone, Eq, PartialEq)] +pub struct PyPublicKey(PublicKey); + +#[pymethods] +impl PyPublicKey { + #[staticmethod] + fn parse(string: &Bound) -> PyResult { + let string = extract_bytes_or_str(string)?; + let inner = string + .parse::() + .map_err(|e| PyValueError::new_err(e.to_string()))?; + Ok(Self(inner)) + } + + fn verify( + &self, + buf: &Bound, + sig: &Bound, + max_age: Option, + ) -> PyResult { + let sig = extract_bytes_or_str(sig)?; + let buf = buf.as_bytes(); + match max_age { + Some(max_age) => Ok(self.0.verify_timestamp(buf, sig, Some(max_age))), + None => Ok(self.0.verify(buf, sig)), + } + } + + fn unpack( + &self, + buf: &Bound, + sig: &Bound, + max_age: Option, + ) -> PyResult<()> { + if !self.verify(buf, sig, max_age)? { + todo!("Return error"); + } + + let _buf = buf.as_bytes(); + todo!("Deserialize into dict"); + } + + fn __str__(&self) -> String { + self.0.to_string() + } + + fn __repr__(slf: &Bound) -> PyResult { + let class_name = slf.get_type().qualname()?; + Ok(format!("<{class_name} '{}'>", slf.borrow().0)) + } +} + +#[pyclass(name = "SecretKey")] +#[derive(Clone, Eq, PartialEq)] +pub struct PySecretKey(SecretKey); + +#[pymethods] +impl PySecretKey { + #[staticmethod] + fn parse(string: &Bound) -> PyResult { + let string = extract_bytes_or_str(string)?; + let inner = string + .parse() + .map_err(|e: KeyParseError| PyValueError::new_err(e.to_string()))?; + Ok(Self(inner)) + } + + fn sign(&self, value: &Bound) -> String { + self.0.sign(value.as_bytes()) + } + + // TODO: implement pack on the Python side + + fn __str__(&self) -> String { + self.0.to_string() + } + + fn __repr__(slf: &Bound) -> PyResult { + let class_name = slf.get_type().qualname()?; + Ok(format!("<{class_name} '{}'>", slf.borrow().0)) + } +} + +#[pyfunction] +fn generate_key_pair() -> (PySecretKey, PyPublicKey) { + let (secret, public) = relay_auth::generate_key_pair(); + (PySecretKey(secret), PyPublicKey(public)) +} + +// TODO: Decode on the Python side +#[pyfunction] +fn generate_relay_id() -> [u8; 16] { + let id = relay_auth::generate_relay_id(); + id.into_bytes() +} + +#[pyfunction] +#[pyo3(signature = (data, signature, secret, max_age = 60))] +fn create_register_challenge( + data: &Bound, + signature: &Bound, + secret: &Bound, + max_age: i64, +) -> PyResult> { + let max_age = Duration::seconds(max_age); + let signature = extract_bytes_or_str(signature)?; + let secret = extract_bytes_or_str(secret)?; + + let req = RegisterRequest::bootstrap_unpack(data.as_bytes(), signature, Some(max_age)) + .map_err(|e| PyValueError::new_err(e.to_string()))?; + + let challenge = req.into_challenge(secret.as_bytes()); + + let mut output = HashMap::new(); + + // TODO: Parse the UUID on the Python side + output.insert("relay_id", challenge.relay_id().to_string()); + output.insert("token", challenge.token().to_owned()); + + Ok(output) +} + +#[pyfunction] +#[pyo3(signature = (data, signature, secret, max_age = 60))] +fn validate_register_response( + data: &Bound, + signature: &Bound, + secret: &Bound, + max_age: i64, +) -> PyResult> { + let max_age = Duration::seconds(max_age); + let signature = extract_bytes_or_str(signature)?; + let secret = extract_bytes_or_str(secret)?; + + let (response, state) = + RegisterResponse::unpack(data.as_bytes(), signature, secret.as_bytes(), Some(max_age)) + .map_err(|e| PyValueError::new_err(e.to_string()))?; + + let mut output = HashMap::new(); + + // TODO: Parse the UUID on the Python side + output.insert("relay_id", response.relay_id().to_string()); + output.insert("token", response.token().to_owned()); + output.insert("public_key", state.public_key().to_string()); + output.insert("version", response.version().to_string()); + + Ok(output) +} + +#[pyfunction] +#[pyo3(signature = (version = None))] +fn is_version_supported(version: Option<&Bound>) -> PyResult { + let version = if let Some(version) = version { + extract_bytes_or_str(version)? + } else { + "" + }; + + // These can be updated when deprecating legacy versions: + if version.is_empty() { + return Ok(true); + } + + let version = version + .parse::() + .map_err(|e| PyValueError::new_err(e.to_string()))?; + + Ok(version.supported()) +} + +#[pymodule] +pub fn auth(m: &Bound) -> PyResult<()> { + m.add_function(wrap_pyfunction!(generate_key_pair, m)?)?; + m.add_function(wrap_pyfunction!(generate_relay_id, m)?)?; + m.add_function(wrap_pyfunction!(create_register_challenge, m)?)?; + m.add_function(wrap_pyfunction!(validate_register_response, m)?)?; + m.add_function(wrap_pyfunction!(is_version_supported, m)?)?; + + m.add_class::()?; + m.add_class::()?; + Ok(()) +} diff --git a/relay-cabi/src/codeowners.rs b/relay-pyo3/src/codeowners.rs similarity index 89% rename from relay-cabi/src/codeowners.rs rename to relay-pyo3/src/codeowners.rs index 908d5311b28..0c1ae7a0adc 100644 --- a/relay-cabi/src/codeowners.rs +++ b/relay-pyo3/src/codeowners.rs @@ -5,13 +5,11 @@ use lru::LruCache; use once_cell::sync::Lazy; use regex::bytes::Regex; -use crate::core::{RelayBuf, RelayStr}; - /// LRU cache for [`Regex`]s in relation to the provided string pattern. -static CODEOWNERS_CACHE: Lazy>> = +pub static CODEOWNERS_CACHE: Lazy>> = Lazy::new(|| Mutex::new(LruCache::new(NonZeroUsize::new(500).unwrap()))); -fn translate_codeowners_pattern(pattern: &str) -> Option { +pub fn translate_codeowners_pattern(pattern: &str) -> Option { let mut regex = String::new(); // Special case backslash can match a backslash file or directory @@ -94,29 +92,6 @@ fn translate_codeowners_pattern(pattern: &str) -> Option { Regex::new(®ex).ok() } -/// Returns `true` if the codeowners path matches the value, `false` otherwise. -#[no_mangle] -#[relay_ffi::catch_unwind] -pub unsafe extern "C" fn relay_is_codeowners_path_match( - value: *const RelayBuf, - pattern: *const RelayStr, -) -> bool { - let value = (*value).as_bytes(); - let pat = (*pattern).as_str(); - - let mut cache = CODEOWNERS_CACHE.lock().unwrap(); - - if let Some(pattern) = cache.get(pat) { - pattern.is_match(value) - } else if let Some(pattern) = translate_codeowners_pattern(pat) { - let result = pattern.is_match(value); - cache.put(pat.to_owned(), pattern); - result - } else { - false - } -} - #[cfg(test)] mod tests { use super::*; diff --git a/relay-pyo3/src/consts.rs b/relay-pyo3/src/consts.rs new file mode 100644 index 00000000000..65d712246c6 --- /dev/null +++ b/relay-pyo3/src/consts.rs @@ -0,0 +1,132 @@ +use pyo3::prelude::*; +use relay_base_schema::data_category::DataCategory; +use std::collections::HashMap; +use strum::IntoEnumIterator; + +/** + * Trace status. + * + * Values from + * Mapping to HTTP from + */ +#[derive(Debug, Copy, Clone, strum::Display, strum::EnumIter)] +#[strum(serialize_all = "snake_case")] +#[repr(u8)] +enum SpanStatus { + /** + * The operation completed successfully. + * + * HTTP status 100..299 + successful redirects from the 3xx range. + */ + Ok = 0, + /** + * The operation was cancelled (typically by the user). + */ + Cancelled = 1, + /** + * Unknown. Any non-standard HTTP status code. + * + * "We do not know whether the transaction failed or succeeded" + */ + Unknown = 2, + /** + * Client specified an invalid argument. 4xx. + * + * Note that this differs from FailedPrecondition. InvalidArgument indicates arguments that + * are problematic regardless of the state of the system. + */ + InvalidArgument = 3, + /** + * Deadline expired before operation could complete. + * + * For operations that change the state of the system, this error may be returned even if the + * operation has been completed successfully. + * + * HTTP redirect loops and 504 Gateway Timeout + */ + DeadlineExceeded = 4, + /** + * 404 Not Found. Some requested entity (file or directory) was not found. + */ + NotFound = 5, + /** + * Already exists (409) + * + * Some entity that we attempted to create already exists. + */ + AlreadyExists = 6, + /** + * 403 Forbidden + * + * The caller does not have permission to execute the specified operation. + */ + PermissionDenied = 7, + /** + * 429 Too Many Requests + * + * Some resource has been exhausted, perhaps a per-user quota or perhaps the entire file + * system is out of space. + */ + ResourceExhausted = 8, + /** + * Operation was rejected because the system is not in a state required for the operation's + * execution + */ + FailedPrecondition = 9, + /** + * The operation was aborted, typically due to a concurrency issue. + */ + Aborted = 10, + /** + * Operation was attempted past the valid range. + */ + OutOfRange = 11, + /** + * 501 Not Implemented + * + * Operation is not implemented or not enabled. + */ + Unimplemented = 12, + /** + * Other/generic 5xx. + */ + InternalError = 13, + /** + * 503 Service Unavailable + */ + Unavailable = 14, + /** + * Unrecoverable data loss or corruption + */ + DataLoss = 15, + /** + * 401 Unauthorized (actually does mean unauthenticated according to RFC 7235) + * + * Prefer PermissionDenied if a user is logged in. + */ + Unauthenticated = 16, +} + +impl SpanStatus { + fn status_code_to_name() -> HashMap { + Self::iter().map(|v| (v as u8, v.to_string())).collect() + } + + fn status_name_to_code() -> HashMap { + Self::iter().map(|v| (v.to_string(), v as u8)).collect() + } +} + +#[pymodule] +pub fn consts(_py: Python, m: &Bound) -> PyResult<()> { + m.add_class::()?; + m.add( + "SPAN_STATUS_CODE_TO_NAME", + SpanStatus::status_code_to_name(), + )?; + m.add( + "SPAN_STATUS_NAME_TO_CODE", + SpanStatus::status_name_to_code(), + )?; + Ok(()) +} diff --git a/relay-pyo3/src/exceptions.rs b/relay-pyo3/src/exceptions.rs new file mode 100644 index 00000000000..eeb9071acb9 --- /dev/null +++ b/relay-pyo3/src/exceptions.rs @@ -0,0 +1,172 @@ +use pyo3::exceptions::PyException; +use pyo3::prelude::*; +use relay_auth::{KeyParseError, UnpackError}; +use relay_event_normalization::GeoIpError; +use relay_event_schema::processor::ProcessingAction; +use sentry_release_parser::InvalidRelease; +use std::collections::HashMap; +use strum::IntoEnumIterator; + +// Represents all possible error codes. +#[repr(u32)] +#[allow(missing_docs)] +#[pyclass(rename_all = "PascalCase")] +#[derive(Debug, Eq, PartialEq, Clone, Copy, strum::Display, strum::EnumIter)] +#[strum(serialize_all = "PascalCase")] +pub enum RelayErrorCode { + Unknown = 2, + + InvalidJsonError = 101, // serde_json::Error + + // relay_auth::KeyParseError + KeyParseErrorBadEncoding = 1000, + KeyParseErrorBadKey = 1001, + + // relay_auth::UnpackError + UnpackErrorBadSignature = 1003, + UnpackErrorBadPayload = 1004, + UnpackErrorSignatureExpired = 1005, + UnpackErrorBadEncoding = 1006, + + // relay_protocol::annotated::ProcessingAction + ProcessingErrorInvalidTransaction = 2001, + ProcessingErrorInvalidGeoIp = 2002, + + // sentry_release_parser::InvalidRelease + InvalidReleaseErrorTooLong = 3001, + InvalidReleaseErrorRestrictedName = 3002, + InvalidReleaseErrorBadCharacters = 3003, +} + +impl From<&'_ anyhow::Error> for RelayErrorCode { + /// This maps all errors that can possibly happen. + fn from(error: &anyhow::Error) -> Self { + for cause in error.chain() { + if cause.downcast_ref::().is_some() { + return Self::InvalidJsonError; + } + if cause.downcast_ref::().is_some() { + return Self::ProcessingErrorInvalidGeoIp; + } + if let Some(err) = cause.downcast_ref::() { + return match err { + KeyParseError::BadEncoding => Self::KeyParseErrorBadEncoding, + KeyParseError::BadKey => Self::KeyParseErrorBadKey, + }; + } + if let Some(err) = cause.downcast_ref::() { + return match err { + UnpackError::BadSignature => Self::UnpackErrorBadSignature, + UnpackError::BadPayload(..) => Self::UnpackErrorBadPayload, + UnpackError::SignatureExpired => Self::UnpackErrorSignatureExpired, + UnpackError::BadEncoding => Self::UnpackErrorBadEncoding, + }; + } + if let Some(err) = cause.downcast_ref::() { + return match err { + ProcessingAction::InvalidTransaction(_) => { + Self::ProcessingErrorInvalidTransaction + } + _ => Self::Unknown, + }; + } + if let Some(err) = cause.downcast_ref::() { + return match err { + InvalidRelease::TooLong => Self::InvalidReleaseErrorTooLong, + InvalidRelease::RestrictedName => Self::InvalidReleaseErrorRestrictedName, + InvalidRelease::BadCharacters => Self::InvalidReleaseErrorBadCharacters, + }; + } + } + Self::Unknown + } +} + +impl RelayErrorCode { + fn exceptions_by_code() -> HashMap { + Self::iter().map(|v| (v as u32, v.to_string())).collect() + } +} + +#[pyclass(extends = PyException, subclass)] +#[derive(Debug, Clone)] +pub struct RelayError { + #[pyo3(get, set)] + code: Option, +} + +impl From<&'_ anyhow::Error> for RelayError { + fn from(error: &'_ anyhow::Error) -> Self { + Self { + code: Some(RelayErrorCode::from(error)), + } + } +} + +impl From for PyErr { + fn from(_value: RelayError) -> Self { + Self::new::(()) + } +} + +macro_rules! make_error { + ($error_name:ident) => { + #[pyclass(extends = PyException)] + #[derive(Debug, Clone)] + pub(crate) struct $error_name { + #[pyo3(get, set)] + code: Option, + } + + #[pymethods] + impl $error_name { + #[new] + pub(crate) fn new() -> Self { + Self { + code: Some(RelayErrorCode::$error_name), + } + } + } + + impl From<$error_name> for PyErr { + fn from(_value: $error_name) -> Self { + Self::new::<$error_name, ()>(()) + } + } + }; +} + +make_error!(Unknown); +make_error!(InvalidJsonError); +make_error!(KeyParseErrorBadEncoding); +make_error!(KeyParseErrorBadKey); +make_error!(UnpackErrorBadSignature); +make_error!(UnpackErrorBadPayload); +make_error!(UnpackErrorSignatureExpired); +make_error!(UnpackErrorBadEncoding); +make_error!(ProcessingErrorInvalidTransaction); +make_error!(ProcessingErrorInvalidGeoIp); +make_error!(InvalidReleaseErrorTooLong); +make_error!(InvalidReleaseErrorRestrictedName); +make_error!(InvalidReleaseErrorBadCharacters); + +#[pymodule] +pub fn exceptions(m: &Bound) -> PyResult<()> { + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add("exceptions_by_code", RelayErrorCode::exceptions_by_code())?; + Ok(()) +} diff --git a/relay-pyo3/src/lib.rs b/relay-pyo3/src/lib.rs new file mode 100644 index 00000000000..33d16b6797a --- /dev/null +++ b/relay-pyo3/src/lib.rs @@ -0,0 +1,33 @@ +use pyo3::prelude::*; +use pyo3::wrap_pymodule; + +mod auth; +mod codeowners; +mod consts; +mod exceptions; +mod processing; +mod utils; + +#[pymodule] +fn _relay_pyo3(py: Python, m: &Bound) -> PyResult<()> { + let modules = py.import_bound("sys")?.getattr("modules")?; + + let auth_module = wrap_pymodule!(auth::auth); + let consts_module = wrap_pymodule!(consts::consts); + let exceptions_module = wrap_pymodule!(exceptions::exceptions); + let processing_module = wrap_pymodule!(processing::processing); + + // Expose them as "from sentry_relay as processing" + m.add_wrapped(auth_module)?; + m.add_wrapped(processing_module)?; + m.add_wrapped(consts_module)?; + m.add_wrapped(exceptions_module)?; + + // Expose them as "import sentry_relay.processing" + modules.set_item("sentry_relay.auth", auth_module(py))?; + modules.set_item("sentry_relay.consts", consts_module(py))?; + modules.set_item("sentry_relay.exceptions", exceptions_module(py))?; + modules.set_item("sentry_relay.processing", processing_module(py))?; + + Ok(()) +} diff --git a/relay-pyo3/src/processing.rs b/relay-pyo3/src/processing.rs new file mode 100644 index 00000000000..7d0586dbf6f --- /dev/null +++ b/relay-pyo3/src/processing.rs @@ -0,0 +1,691 @@ +use std::cmp::Ordering; +use std::collections::{HashMap, HashSet}; + +use chrono::{DateTime, Utc}; +use pyo3::exceptions::PyValueError; +use pyo3::prelude::*; +use pyo3::types::*; +use relay_cardinality::CardinalityLimit; +use sentry_release_parser::{InvalidRelease, Release, Version}; +use serde::{Deserialize, Serialize}; +use serde_pyobject::{from_pyobject, to_pyobject}; +use uuid::Uuid; + +use relay_common::glob::{glob_match_bytes, GlobOptions}; +use relay_dynamic_config::{GlobalConfig, ProjectConfig}; +use relay_event_normalization::{ + validate_event_timestamps, validate_transaction, BreakdownsConfig, ClientHints, + EventValidationConfig, GeoIpLookup, NormalizationConfig, RawUserAgentInfo, + TransactionValidationConfig, +}; +use relay_event_schema::processor::{process_value, ProcessingState}; +use relay_event_schema::protocol::{Event, IpAddr, VALID_PLATFORMS}; +use relay_pii::{ + selector_suggestions_from_value, DataScrubbingConfig, InvalidSelectorError, PiiConfig, + PiiConfigError, PiiProcessor, SelectorSpec, +}; +use relay_protocol::{Annotated, Meta, Remark, RuleCondition}; +use relay_sampling::SamplingConfig; + +use crate::codeowners::{translate_codeowners_pattern, CODEOWNERS_CACHE}; +use crate::exceptions::{ + InvalidReleaseErrorBadCharacters, InvalidReleaseErrorRestrictedName, InvalidReleaseErrorTooLong, +}; +use crate::utils; + +#[derive(Default, Serialize, Deserialize, Debug, Clone)] +#[pyclass] +pub struct PyClientHintsString { + /// The client's OS, e.g. macos, android... + pub sec_ch_ua_platform: Option, + /// The version number of the client's OS. + pub sec_ch_ua_platform_version: Option, + /// Name of the client's web browser and its version. + pub sec_ch_ua: Option, + /// Device model, e.g. samsung galaxy 3. + pub sec_ch_ua_model: Option, +} + +impl From for ClientHints { + fn from(value: PyClientHintsString) -> Self { + Self { + sec_ch_ua_platform: value.sec_ch_ua_platform, + sec_ch_ua_platform_version: value.sec_ch_ua_platform_version, + sec_ch_ua: value.sec_ch_ua, + sec_ch_ua_model: value.sec_ch_ua_model, + } + } +} + +/// Configuration for the store step -- validation and normalization. +#[derive(Serialize, Deserialize, Debug, Default)] +#[serde(default)] +#[pyclass] +pub struct StoreNormalizer { + /// The identifier of the target project, which gets added to the payload. + pub project_id: Option, + + /// The IP address of the SDK that sent the event. + /// + /// When `{{auto}}` is specified and there is no other IP address in the payload, such as in the + /// `request` context, this IP address gets added to the `user` context. + pub client_ip: Option, + + /// The name and version of the SDK that sent the event. + pub client: Option, + + /// The internal identifier of the DSN, which gets added to the payload. + /// + /// Note that this is different from the DSN's public key. The ID is usually numeric. + pub key_id: Option, + + /// The version of the protocol. + /// + /// This is a deprecated field, as there is no more versioning of Relay event payloads. + pub protocol_version: Option, + + /// Configuration for issue grouping. + /// + /// This configuration is persisted into the event payload to achieve idempotency in the + /// processing pipeline and for reprocessing. + pub grouping_config: Option, + + /// The raw user-agent string obtained from the submission request headers. + /// + /// The user agent is used to infer device, operating system, and browser information should the + /// event payload contain no such data. + /// + /// Newer browsers have frozen their user agents and send [`client_hints`](Self::client_hints) + /// instead. If both a user agent and client hints are present, normalization uses client hints. + pub user_agent: Option, + + /// A collection of headers sent by newer browsers about the device and environment. + /// + /// Client hints are the preferred way to infer device, operating system, and browser + /// information should the event payload contain no such data. If no client hints are present, + /// normalization falls back to the user agent. + pub client_hints: PyClientHintsString, + + /// The time at which the event was received in this Relay. + /// + /// This timestamp is persisted into the event payload. + pub received_at: Option>, + + /// The time at which the event was sent by the client. + /// + /// The difference between this and the `received_at` timestamps is used for clock drift + /// correction, should a significant difference be detected. + pub sent_at: Option>, + + /// The maximum amount of seconds an event can be predated into the future. + /// + /// If the event's timestamp lies further into the future, the received timestamp is assumed. + pub max_secs_in_future: Option, + + /// The maximum amount of seconds an event can be dated in the past. + /// + /// If the event's timestamp is older, the received timestamp is assumed. + pub max_secs_in_past: Option, + + /// When `Some(true)`, individual parts of the event payload is trimmed to a maximum size. + /// + /// See the event schema for size declarations. + pub enable_trimming: Option, + + /// When `Some(true)`, it is assumed that the event has been normalized before. + /// + /// This disables certain normalizations, especially all that are not idempotent. The + /// renormalize mode is intended for the use in the processing pipeline, so an event modified + /// during ingestion can be validated against the schema and large data can be trimmed. However, + /// advanced normalizations such as inferring contexts or clock drift correction are disabled. + /// + /// `None` equals to `false`. + pub is_renormalize: Option, + + /// Overrides the default flag for other removal. + pub remove_other: Option, + + /// When `Some(true)`, context information is extracted from the user agent. + pub normalize_user_agent: Option, + + /// Emit breakdowns based on given configuration. + pub breakdowns: Option, + + /// The SDK's sample rate as communicated via envelope headers. + /// + /// It is persisted into the event payload. + pub client_sample_rate: Option, + + /// The identifier of the Replay running while this event was created. + /// + /// It is persisted into the event payload for correlation. + pub replay_id: Option, + + /// Controls whether spans should be normalized (e.g. normalizing the exclusive time). + /// + /// To normalize spans in [`normalize_event`], `is_renormalize` must + /// be disabled _and_ `normalize_spans` enabled. + pub normalize_spans: bool, +} + +#[pymethods] +#[allow(clippy::too_many_arguments)] +impl StoreNormalizer { + #[new] + #[pyo3(signature = ( + client_hints = None, + project_id = None, + client_ip = None, + client = None, + key_id = None, + protocol_version = None, + grouping_config = None, + user_agent = None, + received_at = None, + sent_at = None, + max_secs_in_future = None, + max_secs_in_past = None, + enable_trimming = None, + is_renormalize = None, + remove_other = None, + normalize_user_agent = None, + breakdowns = None, + client_sample_rate = None, + replay_id = None, + normalize_spans = None + ))] + fn new( + client_hints: Option, + project_id: Option, + client_ip: Option, + client: Option, + key_id: Option, + protocol_version: Option, + grouping_config: Option, + user_agent: Option, + received_at: Option>, + sent_at: Option>, + max_secs_in_future: Option, + max_secs_in_past: Option, + enable_trimming: Option, + is_renormalize: Option, + remove_other: Option, + normalize_user_agent: Option, + breakdowns: Option, + client_sample_rate: Option, + replay_id: Option, + normalize_spans: Option, + ) -> Self { + Self { + project_id, + client_ip, + client, + key_id, + protocol_version, + grouping_config, + user_agent, + client_hints: client_hints.unwrap_or_default(), + received_at, + sent_at, + max_secs_in_future, + max_secs_in_past, + enable_trimming, + is_renormalize, + remove_other, + normalize_user_agent, + breakdowns, + client_sample_rate, + replay_id, + normalize_spans: normalize_spans.unwrap_or_default(), + } + } + + #[pyo3(signature = (event = None, raw_event = None))] + pub fn normalize_event<'a>( + &self, + event: Option<&Bound<'a, PyAny>>, + raw_event: Option<&Bound<'a, PyAny>>, // bytes | str + ) -> PyResult { + let raw_event = + raw_event.unwrap_or(event.expect("Event should exist if raw_event is None")); + let event = raw_event.downcast::()?; + let mut data = event.to_string().into_bytes(); + json_forensics::translate_slice(&mut data); + let mut event: Annotated = + Annotated::::from_json(std::str::from_utf8(&data)?) + .map_err(|e| PyValueError::new_err(e.to_string()))?; + + let event_validation_config = EventValidationConfig { + received_at: self.received_at, + max_secs_in_past: self.max_secs_in_past, + max_secs_in_future: self.max_secs_in_future, + is_validated: self.is_renormalize.unwrap_or(false), + }; + validate_event_timestamps(&mut event, &event_validation_config)?; + + let tx_validation_config = TransactionValidationConfig { + timestamp_range: None, // only supported in relay + is_validated: self.is_renormalize.unwrap_or(false), + }; + validate_transaction(&mut event, &tx_validation_config)?; + + let is_renormalize = self.is_renormalize.unwrap_or(false); + + let normalization_config = NormalizationConfig { + project_id: self.project_id, + client: self.client.clone(), + protocol_version: self.protocol_version.clone(), + key_id: self.key_id.clone(), + grouping_config: self + .grouping_config + .as_deref() + .and_then(|gc| serde_json::from_str(gc).ok()), + client_ip: self.client_ip.as_ref(), + client_sample_rate: self.client_sample_rate, + user_agent: RawUserAgentInfo { + user_agent: self.user_agent.as_deref(), + client_hints: ClientHints { + sec_ch_ua: self.client_hints.sec_ch_ua.as_deref(), + sec_ch_ua_model: self.client_hints.sec_ch_ua_model.as_deref(), + sec_ch_ua_platform: self.client_hints.sec_ch_ua_platform.as_deref(), + sec_ch_ua_platform_version: self + .client_hints + .sec_ch_ua_platform_version + .as_deref(), + }, + }, + max_name_and_unit_len: None, + breakdowns_config: None, // only supported in relay + normalize_user_agent: self.normalize_user_agent, + transaction_name_config: Default::default(), // only supported in relay + is_renormalize, + remove_other: self.remove_other.unwrap_or(!is_renormalize), + emit_event_errors: !is_renormalize, + device_class_synthesis_config: false, // only supported in relay + enrich_spans: false, + max_tag_value_length: usize::MAX, + span_description_rules: None, + performance_score: None, + geoip_lookup: None, // only supported in relay + ai_model_costs: None, // only supported in relay + enable_trimming: self.enable_trimming.unwrap_or_default(), + measurements: None, + normalize_spans: self.normalize_spans, + // SAFETY: Unwrap is used instead of `.ok()` since we know it's a valid UUID. + replay_id: self + .replay_id + .as_deref() + .map(|id| Uuid::parse_str(id).unwrap()), + }; + relay_event_normalization::normalize_event(&mut event, &normalization_config); + Python::with_gil(|py| Ok(PyAnnotatedEvent::from(event).into_py(py))) + } +} + +#[pyfunction] +pub fn validate_sampling_configuration(condition: &Bound<'_, PyAny>) -> PyResult<()> { + let input = utils::extract_bytes_or_str(condition)?; + + match serde_json::from_str::(input) { + Ok(config) => { + for rule in config.rules { + if !rule.condition.supported() { + return Err(PyValueError::new_err("unsupported sampling rule")); + } + } + Ok(()) + } + Err(e) => Err(PyValueError::new_err(e.to_string())), + } +} + +#[pyfunction] +pub fn compare_versions<'a>(a: &Bound<'a, PyString>, b: &Bound<'a, PyString>) -> PyResult { + let ver_a = Version::parse(a.to_str()?).map_err(|e| PyValueError::new_err(e.to_string()))?; + let ver_b = Version::parse(b.to_str()?).map_err(|e| PyValueError::new_err(e.to_string()))?; + Ok(match ver_a.cmp(&ver_b) { + Ordering::Less => -1, + Ordering::Equal => 0, + Ordering::Greater => 1, + }) +} + +#[pyfunction] +pub fn validate_pii_selector(selector: &Bound<'_, PyString>) -> PyResult<()> { + let value = selector.to_str()?; + if let Err(err) = value.parse::() { + return match err { + InvalidSelectorError::ParseError(_) => Err(PyValueError::new_err(format!( + "invalid syntax near {value:?}" + ))), + _ => Err(PyValueError::new_err(err.to_string()))?, + }; + } + + Ok(()) +} + +#[pyfunction] +pub fn validate_pii_config(config: &Bound<'_, PyString>) -> PyResult<()> { + match serde_json::from_str::(config.to_str()?) { + Ok(config) => match config.compiled().force_compile() { + Ok(_) => Ok(()), + Err(PiiConfigError::RegexError(source)) => { + Err(PyValueError::new_err(source.to_string())) + } + }, + Err(e) => Err(PyValueError::new_err(e.to_string())), + } +} + +#[pyfunction] +pub fn validate_rule_condition(condition: &Bound<'_, PyString>) -> PyResult<()> { + match serde_json::from_str::(condition.to_str()?) { + Ok(condition) => { + if condition.supported() { + Ok(()) + } else { + Err(PyValueError::new_err("unsupported condition")) + } + } + Err(e) => Err(PyValueError::new_err(e.to_string())), + } +} + +/// @deprecated +#[pyfunction] +pub fn validate_sampling_condition(condition: &Bound<'_, PyString>) -> PyResult<()> { + validate_rule_condition(condition) +} + +#[pyfunction] +pub fn is_codeowners_path_match<'a>( + value: &Bound<'a, PyBytes>, + pattern: &Bound<'a, PyString>, +) -> PyResult { + let value = value.as_bytes(); + let pat = pattern.to_str()?; + + let mut cache = CODEOWNERS_CACHE.lock().unwrap(); + + if let Some(pattern) = cache.get(pat) { + Ok(pattern.is_match(value)) + } else if let Some(pattern) = translate_codeowners_pattern(pat) { + let result = pattern.is_match(value); + cache.put(pat.to_owned(), pattern); + Ok(result) + } else { + Ok(false) + } +} + +#[pyfunction] +pub fn split_chunks<'a>( + input: &Bound<'a, PyString>, + remarks: &Bound<'a, PyList>, +) -> PyResult> { + let py = input.py(); + let remarks = remarks + .iter() + .map(|item| from_pyobject(item).unwrap()) + .collect::>(); + let input = input.to_str()?; + let chunks = relay_event_schema::processor::split_chunks(input, &remarks); + Ok(to_pyobject(py, &chunks)?) +} + +#[derive(Clone, Debug)] +#[pyclass(name = "AnnotatedEvent")] +pub struct PyAnnotatedEvent(pub Option, pub Meta); + +impl From> for PyAnnotatedEvent { + fn from(value: Annotated) -> Self { + Self(value.0, value.1) + } +} + +#[pyfunction] +pub fn pii_strip_event<'a>( + config: &Bound<'a, PiiConfig>, + event: &Bound<'a, PyString>, +) -> PyResult { + let borrowed_config = config.borrow(); + let mut processor = PiiProcessor::new(borrowed_config.compiled()); + + let mut event = Annotated::::from_json(event.to_str()?) + .map_err(|e| PyValueError::new_err(e.to_string()))?; + process_value(&mut event, &mut processor, ProcessingState::root())?; + Python::with_gil(|py| Ok(PyAnnotatedEvent::from(event).into_py(py))) +} + +#[derive(Debug, Clone, Eq, PartialEq)] +#[pyclass(name = "Version")] +pub struct PyVersion { + major: String, + minor: String, + patch: String, + revision: String, + pre: String, + build_code: String, + raw_short: String, + components: u8, + raw_quad: (String, Option, Option, Option), +} + +impl From<&Version<'_>> for PyVersion { + fn from(value: &Version<'_>) -> Self { + let (major, minor, patch, revision) = value.raw_quad(); + Self { + major: value.major().to_string(), + minor: value.minor().to_string(), + patch: value.patch().to_string(), + revision: value.revision().to_string(), + pre: value.pre().map(String::from).unwrap_or_default(), + build_code: value.build_code().map(String::from).unwrap_or_default(), + raw_short: value.raw_short().to_string(), + components: value.components(), + raw_quad: ( + major.to_string(), + minor.map(String::from), + patch.map(String::from), + revision.map(String::from), + ), + } + } +} + +#[derive(Debug, Clone, PartialEq)] +#[pyclass(name = "Release")] +pub struct PyRelease { + raw: String, + package: String, + version_raw: String, + version: Option, +} + +impl From> for PyRelease { + fn from(value: Release<'_>) -> Self { + Self { + raw: value.raw().to_string(), + package: value.package().unwrap_or_default().to_string(), + version_raw: value.version_raw().to_string(), + version: value.version().map(PyVersion::from), + } + } +} + +#[pyfunction] +pub fn parse_release<'a>(release: &Bound<'a, PyString>) -> PyResult> { + let parsed_release = Release::parse(release.to_str()?).map_err(|e| { + let e: PyErr = match e { + InvalidRelease::TooLong => InvalidReleaseErrorTooLong::new().into(), + InvalidRelease::RestrictedName => InvalidReleaseErrorRestrictedName::new().into(), + InvalidRelease::BadCharacters => InvalidReleaseErrorBadCharacters::new().into(), + }; + e + })?; + Ok(to_pyobject(release.py(), &parsed_release)?) +} + +#[pyfunction] +pub fn normalize_global_config<'a>(config: &Bound<'a, PyAny>) -> PyResult> { + let value: GlobalConfig = + from_pyobject(config.to_owned()).map_err(|e| PyValueError::new_err(e.to_string()))?; + Ok(to_pyobject(config.py(), &value)?) +} + +#[pyfunction] +pub fn pii_selector_suggestions_from_event(event: &Bound<'_, PyAny>) -> PyResult { + let event = event.extract::()?; + let mut annotated_event = Annotated::::new(event.0.unwrap_or_default()); + let rv = selector_suggestions_from_value(&mut annotated_event); + Python::with_gil(|py| Ok(rv.into_py(py))) +} + +#[pyfunction] +pub fn convert_datascrubbing_config<'a>(config: &Bound<'a, PyAny>) -> PyResult> { + let py = config.py(); + let dsc: DataScrubbingConfig = + from_pyobject(config.to_owned()).map_err(|e| PyValueError::new_err(e.to_string()))?; + match dsc.pii_config() { + Ok(Some(config)) => Ok(to_pyobject(py, &config)?), + // Return an empty object: "{}" + Ok(None) => Ok(HashMap::::new() + .into_py_dict_bound(py) + .into_any()), + // NOTE: Callers of this function must be able to handle this error. + Err(e) => Err(PyValueError::new_err(e.to_string())), + } +} + +#[pyfunction] +pub fn init_valid_platforms() -> HashSet<&'static str> { + VALID_PLATFORMS.iter().copied().collect() +} + +#[pyfunction] +#[pyo3(signature = (value, pat, double_star=false, case_insensitive=false, path_normalize=false, allow_newline=false))] +pub fn is_glob_match<'a>( + value: &Bound<'a, PyString>, + pat: &Bound<'a, PyString>, + double_star: bool, + case_insensitive: bool, + path_normalize: bool, + allow_newline: bool, +) -> PyResult { + let options = GlobOptions { + double_star, + case_insensitive, + path_normalize, + allow_newline, + }; + Ok(glob_match_bytes( + value.to_str()?.as_bytes(), + pat.to_str()?, + options, + )) +} + +#[pyfunction] +pub fn meta_with_chunks<'py>( + data: &Bound<'py, PyAny>, + meta: &Bound<'py, PyAny>, +) -> PyResult> { + let py = data.py(); + if !meta.is_instance_of::() { + return Ok(meta.clone()); + } + + let meta = meta.downcast::()?; + let mut result: HashMap> = HashMap::new(); + + for (key, item) in meta { + let key = key.downcast::()?; + if key.is_empty()? && item.is_instance_of::() { + let item = item.downcast::()?; + result.insert("".to_string(), item.clone().into_any()); + + if let Some(rem) = item + .get_item("rem")? + .and_then(|r| r.downcast::().ok().cloned()) + { + if let Ok(data) = data.downcast::() { + let chunks = split_chunks(data, &rem)?; + if let Some(empty_item) = result.get_mut("") { + empty_item.set_item("chunks", chunks)?; + } + } + } + let rem = item.get_item("rem")?; + if rem.is_some() && data.is_instance_of::() {} + } else if let Ok(data) = data.downcast::() { + let current_item = data + .get_item(key)? + .expect("Current item should have existed"); + let as_dict = current_item.downcast::()?; + result.insert(key.to_string(), meta_with_chunks(as_dict, &item)?); + } else if let Ok(data) = data.downcast::() { + let int_key = key.to_str()?.parse::()?; + match data.get_item(int_key) { + Ok(val) => { + let val = val.downcast::()?; + result.insert(key.to_string(), meta_with_chunks(val, &item)?); + } + Err(_) => { + result.insert( + key.to_string(), + meta_with_chunks(&PyNone::get_bound(py), &item)?, + ); + } + } + } else { + result.insert(key.to_string(), item.into_any()); + } + } + + Ok(result.into_py_dict_bound(data.py()).into_any()) +} + +#[pyfunction] +fn normalize_cardinality_limit_config<'a>(config: &Bound<'a, PyAny>) -> PyResult> { + let obj: CardinalityLimit = + from_pyobject(config.to_owned()).map_err(|e| PyValueError::new_err(e.to_string()))?; + + Ok(to_pyobject(config.py(), &obj)?) +} + +#[pyfunction] +fn normalize_project_config<'a>(config: &Bound<'a, PyAny>) -> PyResult> { + let obj: ProjectConfig = + from_pyobject(config.to_owned()).map_err(|e| PyValueError::new_err(e.to_string()))?; + + Ok(to_pyobject(config.py(), &obj)?) +} + +#[pymodule] +pub fn processing(_py: Python, m: &Bound) -> PyResult<()> { + m.add_function(wrap_pyfunction!(validate_sampling_configuration, m)?)?; + m.add_function(wrap_pyfunction!(compare_versions, m)?)?; + m.add_function(wrap_pyfunction!(validate_pii_selector, m)?)?; + m.add_function(wrap_pyfunction!(validate_pii_config, m)?)?; + m.add_function(wrap_pyfunction!(validate_rule_condition, m)?)?; + m.add_function(wrap_pyfunction!(validate_sampling_condition, m)?)?; + m.add_function(wrap_pyfunction!(is_codeowners_path_match, m)?)?; + m.add_function(wrap_pyfunction!(split_chunks, m)?)?; + m.add_function(wrap_pyfunction!(pii_strip_event, m)?)?; + m.add_function(wrap_pyfunction!(parse_release, m)?)?; + m.add_function(wrap_pyfunction!(normalize_global_config, m)?)?; + m.add_function(wrap_pyfunction!(pii_selector_suggestions_from_event, m)?)?; + m.add_function(wrap_pyfunction!(convert_datascrubbing_config, m)?)?; + m.add_function(wrap_pyfunction!(init_valid_platforms, m)?)?; + m.add_function(wrap_pyfunction!(is_glob_match, m)?)?; + m.add_function(wrap_pyfunction!(meta_with_chunks, m)?)?; + m.add_function(wrap_pyfunction!(normalize_cardinality_limit_config, m)?)?; + m.add_function(wrap_pyfunction!(normalize_project_config, m)?)?; + + m.add("VALID_PLATFORMS", init_valid_platforms())?; + + m.add_class::()?; + m.add_class::()?; + Ok(()) +} diff --git a/relay-pyo3/src/utils.rs b/relay-pyo3/src/utils.rs new file mode 100644 index 00000000000..1a7e3999f1f --- /dev/null +++ b/relay-pyo3/src/utils.rs @@ -0,0 +1,16 @@ +use pyo3::exceptions::PyValueError; +use pyo3::prelude::*; +use pyo3::types::{PyBytes, PyString}; + +/// Downcast a PyAny which is PyString or PyBytes to &str. +pub fn extract_bytes_or_str<'a>(input: &'a Bound<'a, PyAny>) -> PyResult<&'a str> { + if let Ok(s) = input.downcast::() { + s.to_str() + } else if let Ok(b) = input.downcast::() { + std::str::from_utf8(b.as_bytes()).map_err(PyValueError::new_err) + } else { + Err(PyValueError::new_err( + "Invalid type. Input has to be string or bytes.", + )) + } +}