Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -196,3 +196,74 @@ jobs:
docker volume prune -f || true
# Clean up networks
docker network prune -f || true

non-rust-smoke:
name: E2E Non-Rust Smoke - requests
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Install uv
uses: astral-sh/setup-uv@v4
with:
version: "latest"

- name: Setup Python
run: uv python install 3.9

- name: Setup Docker Buildx
uses: docker/setup-buildx-action@v3
with:
driver: docker

- name: Install SDK dependencies
run: uv sync --all-extras

- name: Build SDK
run: uv build

- name: Verify SDK build
run: |
ls -la dist/ || (echo "dist folder not found!" && exit 1)
test -f dist/*.whl || (echo "SDK build incomplete!" && exit 1)

- name: Get latest Tusk CLI version
id: tusk-version
run: |
VERSION=$(curl -s -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \
"https://api.github.com/repos/Use-Tusk/tusk-drift-cli/releases/latest" \
| grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/')
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "Latest Tusk CLI version: $VERSION"

- name: Build base image
env:
DOCKER_DEFAULT_PLATFORM: linux/amd64
run: |
docker build \
--build-arg TUSK_CLI_VERSION=${{ steps.tusk-version.outputs.version }} \
-t python-e2e-base:latest \
-f drift/instrumentation/e2e_common/Dockerfile.base \
.

- name: Run non-rust smoke test
env:
DOCKER_DEFAULT_PLATFORM: linux/amd64
TUSK_CLI_VERSION: ${{ steps.tusk-version.outputs.version }}
TUSK_USE_RUST_CORE: "0"
run: |
chmod +x ./drift/instrumentation/requests/e2e-tests/run.sh
cd ./drift/instrumentation/requests/e2e-tests && ./run.sh 8000

- name: Cleanup Docker resources
if: always()
run: |
# Stop all running containers
docker ps -aq | xargs -r docker stop || true
docker ps -aq | xargs -r docker rm || true
# Clean up volumes
docker volume prune -f || true
# Clean up networks
docker network prune -f || true
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,9 +90,15 @@ Alternatively, you can set up Tusk Drift manually:
1. Install the SDK:

```bash
# Use Rust bindings for better performance
pip install tusk-drift-python-sdk[rust]

# Fallback if no platform-compatible wheel
pip install tusk-drift-python-sdk
```

*For more information about Rust acceleration, refer to [this doc](docs/rust-core-bindings).*

2. Create configuration: Run `tusk init` to create your `.tusk/config.yaml` config file interactively, or create it manually per the [configuration docs](https://github.com/Use-Tusk/tusk-drift-cli/blob/main/docs/configuration.md).

3. Initialize the SDK: Refer to the [initialization guide](docs/initialization.md) to instrument the SDK in your service.
Expand Down
9 changes: 7 additions & 2 deletions docs/environment-variables.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,23 +136,28 @@ These variables control optional Rust-accelerated paths in the SDK.

| Variable | Description | Default |
| --- | --- | --- |
| `TUSK_USE_RUST_CORE` | Enables Rust binding usage when available (`1`, `true`, `yes`) | `0` (disabled) |
| `TUSK_USE_RUST_CORE` | Controls Rust binding usage. Truthy (`1`, `true`, `yes`, `on`) enables, falsy (`0`, `false`, `no`, `off`) disables. | Enabled when unset |
| `TUSK_SKIP_PROTO_VALIDATION` | Skips expensive protobuf validation in hot path (`1`, `true`, `yes`) | `0` (disabled) |

**Notes:**

- The SDK is fail-open: if Rust bindings are unavailable or a Rust call fails, it falls back to Python implementation.
- `TUSK_USE_RUST_CORE` defaults to enabled when unset.
- `TUSK_USE_RUST_CORE` does not install Rust bindings automatically. The `drift-core-python` package still must be installed in your environment.
- If Rust is enabled but bindings cannot be loaded, the SDK logs startup fallback and continues on Python paths.
- `TUSK_SKIP_PROTO_VALIDATION` is performance-focused and should be used with confidence in parity tests and serialization correctness.

See [`rust-core-bindings.md`](./rust-core-bindings.md) for more details.

**Example usage:**

```bash
# Enable Rust path (if drift-core-python is installed)
# Explicitly enable Rust path (also the default when unset)
TUSK_USE_RUST_CORE=1 python app.py

# Explicitly disable Rust path
TUSK_USE_RUST_CORE=0 python app.py

# Enable Rust path and skip proto validation
TUSK_USE_RUST_CORE=1 TUSK_SKIP_PROTO_VALIDATION=1 python app.py
```
Expand Down
36 changes: 18 additions & 18 deletions docs/rust-core-bindings.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,22 @@ At a high level:

## Enablement

Set:
Rust is enabled by default when `TUSK_USE_RUST_CORE` is unset.

Use `TUSK_USE_RUST_CORE` to explicitly override behavior:

- Truthy: `1`, `true`, `yes`, `on`
- Falsy: `0`, `false`, `no`, `off`

Examples:

```bash
# Explicitly enable (same as unset)
TUSK_USE_RUST_CORE=1
```

Truthy values are `1`, `true`, and `yes` (case-insensitive). Any other value is treated as disabled.
# Explicitly disable
TUSK_USE_RUST_CORE=0
```

## Installation Requirements

Expand All @@ -37,23 +46,13 @@ You can install the SDK with Rust bindings via extras:
pip install "tusk-drift-python-sdk[rust]"
```

## Wheel Platform Coverage

Based on the current `drift-core` publish workflow, prebuilt wheels are built for:

- Linux `x86_64-unknown-linux-gnu`
- Linux `aarch64-unknown-linux-gnu`
- macOS Apple Silicon `aarch64-apple-darwin`
- Windows `x86_64-pc-windows-msvc`
## Platform Compatibility

Likely missing prebuilt wheels (source build fallback required) include:
`drift-core` publishes native artifacts across a defined support matrix. See:

- macOS Intel (`x86_64-apple-darwin`)
- Linux musl targets (e.g. Alpine)
- Windows ARM64
- Other uncommon Python/platform combinations not covered by release artifacts
- [`drift-core` compatibility matrix](https://github.com/Use-Tusk/drift-core/blob/main/docs/compatibility-matrix.md)

If no wheel matches the environment, `pip` may attempt a source build of `drift-core-python`, which typically requires a Rust toolchain and native build prerequisites.
If no compatible wheel exists for your environment, `pip` may attempt a source build of `drift-core-python`, which typically requires a Rust toolchain and native build prerequisites.

## Fallback Behavior

Expand All @@ -62,6 +61,7 @@ The bridge module is fail-open:
- Rust calls are guarded.
- On import failures or call exceptions, the corresponding helper returns `None`.
- Calling code then uses the existing Python implementation.
- On startup, the SDK logs whether Rust is enabled/disabled and whether it had to fall back to Python.

This means users do not need Rust installed to run the SDK when Rust acceleration is disabled or unavailable.

Expand All @@ -80,7 +80,7 @@ Use with care:

## Practical Guidance

- Default production-safe posture: leave Rust disabled unless you have tested your deployment matrix.
- Default production-safe posture: keep Rust enabled (default) only on tested deployment matrices.
- Performance posture: enable Rust + benchmark on your workloads before broad rollout.
- Reliability posture: keep parity tests and smoke tests in CI to detect drift between Python and Rust paths.

Expand Down
29 changes: 29 additions & 0 deletions drift/core/drift_sdk.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,34 @@ def _generate_sdk_instance_id(self) -> str:
random_suffix = "".join(random.choices("abcdefghijklmnopqrstuvwxyz0123456789", k=9))
return f"sdk-{timestamp_ms}-{random_suffix}"

@staticmethod
def _log_rust_core_startup_status() -> None:
from .rust_core_binding import get_rust_core_startup_status

status = get_rust_core_startup_status()
env_display = status["raw_env"] if status["raw_env"] is not None else "<unset>"

if status["reason"] == "invalid_env_value_defaulted":
logger.warning(
"Invalid TUSK_USE_RUST_CORE value '%s'; defaulting to enabled rust core path.",
env_display,
)

if not status["enabled"]:
logger.info("Rust core path disabled at startup (env=%s, reason=%s).", env_display, status["reason"])
return

if status["binding_loaded"]:
logger.info("Rust core path enabled at startup (env=%s, reason=%s).", env_display, status["reason"])
return

logger.warning(
"Rust core path requested but binding unavailable; falling back to Python path (env=%s, reason=%s, error=%s).",
env_display,
status["reason"],
status["binding_error"],
)

@classmethod
def initialize(
cls,
Expand Down Expand Up @@ -156,6 +184,7 @@ def initialize(
logger.debug("SDK disabled via environment variable")
return instance

instance._log_rust_core_startup_status()
logger.debug(f"Initializing in {instance.mode} mode")

effective_transforms = transforms
Expand Down
35 changes: 33 additions & 2 deletions drift/core/rust_core_binding.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,27 +15,58 @@

_binding_module = None
_binding_load_attempted = False
_binding_load_error: str | None = None

_RUST_TRUTHY = {"1", "true", "yes", "on"}
_RUST_FALSY = {"0", "false", "no", "off"}


def _rust_env_decision() -> tuple[bool, str, str | None]:
raw = os.getenv("TUSK_USE_RUST_CORE")
if raw is None:
return True, "default_on", None
normalized = raw.strip().lower()
if normalized in _RUST_TRUTHY:
return True, "env_enabled", raw
if normalized in _RUST_FALSY:
return False, "env_disabled", raw
return True, "invalid_env_value_defaulted", raw


def _enabled() -> bool:
return os.getenv("TUSK_USE_RUST_CORE", "0").lower() in {"1", "true", "yes"}
enabled, _, _ = _rust_env_decision()
return enabled


def _load_binding():
global _binding_module, _binding_load_attempted
global _binding_module, _binding_load_attempted, _binding_load_error
if _binding_load_attempted:
return _binding_module
_binding_load_attempted = True
try:
import drift_core as binding

_binding_module = binding
_binding_load_error = None
except Exception as exc: # pragma: no cover - depends on runtime env
logger.debug("Rust core binding not available: %s", exc)
_binding_module = None
_binding_load_error = f"{type(exc).__name__}: {exc}"
return _binding_module


def get_rust_core_startup_status() -> dict[str, Any]:
enabled, reason, raw_env = _rust_env_decision()
loaded = _load_binding() if enabled else None
return {
"enabled": enabled,
"reason": reason,
"raw_env": raw_env,
"binding_loaded": loaded is not None,
"binding_error": _binding_load_error,
}


def normalize_and_hash_jsonable(value: Any) -> tuple[Any, str] | None:
"""Return normalized JSON value and deterministic hash via Rust binding."""
if not _enabled():
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ dependencies = [
flask = ["Flask>=3.1.2"]
fastapi = ["fastapi>=0.115.6", "uvicorn>=0.34.2", "starlette<0.42.0"]
django = ["Django>=4.2"]
rust = ["drift-core-python>=0.1.6"]
rust = ["drift-core-python>=0.1.7"]
dev = [
"Flask>=3.1.2",
"fastapi>=0.115.6",
Expand Down
17 changes: 10 additions & 7 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.