diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 810ef9b..d1854ac 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -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 diff --git a/docs/environment-variables.md b/docs/environment-variables.md index b2ba358..fd6f48f 100644 --- a/docs/environment-variables.md +++ b/docs/environment-variables.md @@ -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 diff --git a/docs/rust-core-bindings.md b/docs/rust-core-bindings.md index d06629b..182d7b5 100644 --- a/docs/rust-core-bindings.md +++ b/docs/rust-core-bindings.md @@ -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 @@ -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. diff --git a/package-lock.json b/package-lock.json index 13f8c1a..4ed565b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "0.1.30", "license": "Apache-2.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", @@ -2940,9 +2940,9 @@ "license": "ISC" }, "node_modules/@use-tusk/drift-core-node": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/@use-tusk/drift-core-node/-/drift-core-node-0.1.6.tgz", - "integrity": "sha512-xFvaZAbyykIoqVIEBfmuYWYquyC7C8QlWQyseQTfBK1l97YkXSZZEN3re9cgsIiqd/SNrrMZld/L9eIYmSOktg==" + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/@use-tusk/drift-core-node/-/drift-core-node-0.1.7.tgz", + "integrity": "sha512-wZS8Zmt3t2IfC5QfdDRQBe4FKQn7xJQjfBTlY46Vkk3ag3s8lZpGCJNhn0V+VqP3UnbB5La4wJc46gJ2yM31aA==" }, "node_modules/@use-tusk/drift-schemas": { "version": "0.1.30", diff --git a/package.json b/package.json index c117296..ca9da70 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/core/TuskDrift.ts b/src/core/TuskDrift.ts index 372b67c..a908481 100644 --- a/src/core/TuskDrift.ts +++ b/src/core/TuskDrift.ts @@ -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; @@ -156,6 +157,31 @@ export class TuskDriftCore { } } + private logRustCoreStartupStatus(): void { + const status = getRustCoreStartupStatus(); + const envDisplay = status.rawEnv ?? ""; + + 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.`); @@ -425,6 +451,7 @@ export class TuskDriftCore { return; } + this.logRustCoreStartupStatus(); logger.debug(`Initializing in ${this.mode} mode`); if (!this.initParams.env) { diff --git a/src/core/rustCoreBinding.ts b/src/core/rustCoreBinding.ts index b82c47a..fe6ee90 100644 --- a/src/core/rustCoreBinding.ts +++ b/src/core/rustCoreBinding.ts @@ -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 { @@ -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): Record | undefined { if (!schemaMerges) { return undefined;