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
61 changes: 61 additions & 0 deletions .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -78,3 +78,64 @@ jobs:
docker volume prune -f || true
# Clean up networks
docker network prune -f || true

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

- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 22

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

- name: Install SDK dependencies
run: npm install

- name: Build SDK
run: npm run build

- name: Verify SDK build
run: |
ls -la dist/ || (echo "dist folder not found!" && exit 1)
test -f dist/index.js || (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: Run non-rust smoke test
env:
DOCKER_DEFAULT_PLATFORM: linux/amd64
TUSK_USE_RUST_CORE: "0"
TUSK_CLI_VERSION: ${{ steps.tusk-version.outputs.version }}
# Required by shared e2e env even if this library does not use them
FIREBASE_PROJECT_ID: ${{ vars.FIREBASE_PROJECT_ID }}
FIREBASE_SERVICE_ACCOUNT: ${{ secrets.FIREBASE_SERVICE_ACCOUNT }}
UPSTASH_REDIS_REST_URL: ${{ vars.UPSTASH_REDIS_REST_URL }}
UPSTASH_REDIS_REST_TOKEN: ${{ secrets.UPSTASH_REDIS_REST_TOKEN }}
run: ./src/instrumentation/libraries/http/e2e-tests/run-all.sh 3000

- 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
19 changes: 18 additions & 1 deletion docs/environment-variables.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,24 @@ TUSK_SAMPLING_RATE=0.1 npm start

For more details on sampling rate configuration methods and precedence, see the [Initialization Guide](./initialization.md#3-configure-sampling-rate).

---
## TUSK_USE_RUST_CORE

Control optional Rust-accelerated paths in the SDK. Truthy (`1`, `true`, `yes`, `on`) enables, falsy (`0`, `false`, `no`, `off`) disables. Enabled when unset.

**Notes:**

- The SDK is fail-open: if Rust bindings are unavailable or a Rust call fails, it falls back to JavaScript implementations.
- If Rust is enabled but bindings cannot be loaded, the SDK logs startup fallback and continues on JavaScript paths.

**Example usage:**

```bash
# Explicitly enable Rust path (also the default when unset)
TUSK_USE_RUST_CORE=1 npm start

# Explicitly disable Rust path
TUSK_USE_RUST_CORE=0 npm start
```

## Related Documentation

Expand Down
35 changes: 21 additions & 14 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` and `true` (case-insensitive). Any other value is treated as disabled.
# Explicitly disable
TUSK_USE_RUST_CORE=0
```

## Installation Requirements

Expand All @@ -29,34 +38,32 @@ The Node SDK currently includes `@use-tusk/drift-core-node` as a regular depende
Notes:

- There is no Node equivalent of Python extras like `[rust]`.
- Rust acceleration is still runtime-gated by `TUSK_USE_RUST_CORE`.
- Rust acceleration is still runtime-gated by `TUSK_USE_RUST_CORE`, now with default-on behavior.
- If the native binding cannot be loaded on a machine, the SDK continues on JavaScript code paths.

## Platform Coverage and Native Binary Concerns

Node native bindings depend on OS/arch/libc compatibility of published prebuilt artifacts.
## Platform Compatibility

Practical implications:
`drift-core` publishes native artifacts across a defined support matrix. See:

- Some platforms may not have a matching native artifact.
- On such platforms, direct use of `@use-tusk/drift-core-node` can fail at runtime.
- Within `drift-node-sdk`, Rust helper loading is guarded and fails open to non-Rust paths.
- [`drift-core` compatibility matrix](https://github.com/Use-Tusk/drift-core/blob/main/docs/compatibility-matrix.md)

Unlike Python wheels, this concern appears as Node native addon compatibility rather than wheel tag compatibility.
Node native bindings depend on OS/arch/libc compatibility of published prebuilt artifacts. On unsupported platforms, `drift-node-sdk` fails open to JavaScript paths.

## Fallback Behavior

The bridge module is fail-open:

- Rust calls are guarded behind a binding loader.
- If `TUSK_USE_RUST_CORE` is unset/falsey, Rust is skipped.
- If `TUSK_USE_RUST_CORE` is falsey, Rust is skipped.
- If loading or a Rust call fails, helper functions return `null`.
- Calling code then uses the existing JavaScript implementation.

On startup, the SDK logs whether Rust is enabled/disabled and whether it had to fall back to JavaScript.

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

## Practical Guidance

- Default production-safe posture: keep Rust disabled unless your deployment matrix is tested.
- Default production-safe posture: keep Rust enabled (default) only on tested deployment matrices.
- Performance posture: enable Rust and benchmark on your workloads before broad rollout.
- Reliability posture: keep parity/smoke tests in CI to detect drift between JS and Rust paths.
8 changes: 4 additions & 4 deletions package-lock.json

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

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@
"typescript": "^5.0.0"
},
"dependencies": {
"@use-tusk/drift-core-node": "0.1.6",
"@use-tusk/drift-core-node": "0.1.7",
"import-in-the-middle": "^1.14.4",
"js-yaml": "^4.1.0",
"jsonpath": "^1.1.1",
Expand Down
27 changes: 27 additions & 0 deletions src/core/TuskDrift.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import {
import { TransformConfigs } from "../instrumentation/libraries/types";
import { ATTR_SERVICE_NAME } from "@opentelemetry/semantic-conventions";
import { Resource } from "@opentelemetry/resources";
import { getRustCoreStartupStatus } from "./rustCoreBinding";

export interface InitParams {
apiKey?: string;
Expand Down Expand Up @@ -156,6 +157,31 @@ export class TuskDriftCore {
}
}

private logRustCoreStartupStatus(): void {
const status = getRustCoreStartupStatus();
const envDisplay = status.rawEnv ?? "<unset>";

if (status.reason === "invalid_env_value_defaulted") {
logger.warn(
`Invalid TUSK_USE_RUST_CORE value '${envDisplay}'; defaulting to enabled rust core path.`,
);
}

if (!status.enabled) {
logger.info(`Rust core path disabled at startup (env=${envDisplay}, reason=${status.reason}).`);
return;
}

if (status.bindingLoaded) {
logger.info(`Rust core path enabled at startup (env=${envDisplay}, reason=${status.reason}).`);
return;
}

logger.warn(
`Rust core path requested but binding unavailable; falling back to JavaScript path (env=${envDisplay}, reason=${status.reason}, error=${status.bindingError}).`,
);
}

private validateSamplingRate(value: number, source: string): boolean {
if (typeof value !== "number" || isNaN(value)) {
logger.warn(`Invalid sampling rate from ${source}: not a number. Ignoring.`);
Expand Down Expand Up @@ -425,6 +451,7 @@ export class TuskDriftCore {
return;
}

this.logRustCoreStartupStatus();
logger.debug(`Initializing in ${this.mode} mode`);

if (!this.initParams.env) {
Expand Down
46 changes: 41 additions & 5 deletions src/core/rustCoreBinding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,13 +48,35 @@ export type BuildSpanProtoBytesInput = {

let bindingLoadAttempted = false;
let binding: RustCoreNodeBinding | null = null;
let bindingLoadError: string | null = null;

function isRustCoreEnabled(): boolean {
type RustCoreEnvDecision = {
enabled: boolean;
reason: "default_on" | "env_enabled" | "env_disabled" | "invalid_env_value_defaulted";
rawEnv: string | null;
};

export type RustCoreStartupStatus = {
enabled: boolean;
reason: RustCoreEnvDecision["reason"];
rawEnv: string | null;
bindingLoaded: boolean;
bindingError: string | null;
};

function getRustCoreEnvDecision(): RustCoreEnvDecision {
const raw = process.env.TUSK_USE_RUST_CORE;
if (!raw) {
return false;
return { enabled: true, reason: "default_on", rawEnv: null };
}
const normalized = raw.trim().toLowerCase();
if (["1", "true", "yes", "on"].includes(normalized)) {
return { enabled: true, reason: "env_enabled", rawEnv: raw };
}
return raw === "1" || raw.toLowerCase() === "true";
if (["0", "false", "no", "off"].includes(normalized)) {
return { enabled: false, reason: "env_disabled", rawEnv: raw };
}
return { enabled: true, reason: "invalid_env_value_defaulted", rawEnv: raw };
}

function loadBinding(): RustCoreNodeBinding | null {
Expand All @@ -63,19 +85,33 @@ function loadBinding(): RustCoreNodeBinding | null {
}
bindingLoadAttempted = true;

if (!isRustCoreEnabled()) {
if (!getRustCoreEnvDecision().enabled) {
return null;
}

try {
// eslint-disable-next-line @typescript-eslint/no-var-requires
binding = require("@use-tusk/drift-core-node") as RustCoreNodeBinding;
} catch {
bindingLoadError = null;
} catch (error) {
binding = null;
bindingLoadError = error instanceof Error ? `${error.name}: ${error.message}` : String(error);
}
return binding;
}

export function getRustCoreStartupStatus(): RustCoreStartupStatus {
const decision = getRustCoreEnvDecision();
const loaded = decision.enabled ? loadBinding() : null;
return {
enabled: decision.enabled,
reason: decision.reason,
rawEnv: decision.rawEnv,
bindingLoaded: loaded !== null,
bindingError: bindingLoadError,
};
}

function toRustSchemaMerges(schemaMerges?: Record<string, any>): Record<string, any> | undefined {
if (!schemaMerges) {
return undefined;
Expand Down