diff --git a/docs/environment-variables.md b/docs/environment-variables.md
index d93b3c5..91b1140 100644
--- a/docs/environment-variables.md
+++ b/docs/environment-variables.md
@@ -135,6 +135,25 @@ If `recording.sampling.mode: adaptive` is enabled in `.tusk/config.yaml`, this e
For more details on sampling rate configuration methods and precedence, see the [Initialization Guide](./initialization.md#3-configure-sampling-rate).
+## TUSK_RECORDING_SAMPLING_LOG_TRANSITIONS
+
+Controls whether adaptive sampling emits transition logs like `Adaptive sampling updated (...)`.
+
+- **Type:** Boolean (`true`/`false`, `1`/`0`, `yes`/`no`, `on`/`off`)
+- **If unset:** Falls back to `recording.sampling.log_transitions` in `.tusk/config.yaml`, then defaults to `true`
+- **Precedence:** Overrides `recording.sampling.log_transitions`
+- **Scope:** Only affects adaptive sampling transition logs. It does not change recording decisions or the global SDK log level
+
+**Examples:**
+
+```bash
+# Keep adaptive sampling active but silence transition logs
+TUSK_RECORDING_SAMPLING_LOG_TRANSITIONS=false npm start
+
+# Explicitly re-enable transition logs
+TUSK_RECORDING_SAMPLING_LOG_TRANSITIONS=true npm start
+```
+
## 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.
diff --git a/docs/initialization.md b/docs/initialization.md
index 160c81b..22fe759 100644
--- a/docs/initialization.md
+++ b/docs/initialization.md
@@ -220,6 +220,20 @@ recording:
sampling_rate: 0.1
```
+#### Adaptive Sampling Logs
+
+When adaptive mode changes state or multiplier, the SDK logs an `Adaptive sampling updated (...)` line at `info` level.
+
+- `state`: controller state such as `healthy`, `warm`, `hot`, or `critical_pause`
+- `multiplier`: factor applied to `base_rate`
+- `effectiveRate`: current root-request recording rate after shedding
+- `pressure`: highest normalized pressure signal (`0..1`) driving the update
+- `queueFill`: smoothed export-queue usage ratio; values near `1.0` mean the exporter is falling behind
+- `eventLoopLagP95Ms`: Node-only p95 event loop lag signal
+- `memoryPressureRatio`: current memory usage relative to its detected limit, when available
+
+Set `recording.sampling.log_transitions: false` in `.tusk/config.yaml`, or set `TUSK_RECORDING_SAMPLING_LOG_TRANSITIONS=false`, if you want to suppress these transition logs without changing the overall SDK log level. Raising `logLevel` to `warn` or higher will also hide them.
+
#### Additional Recording Configuration Options
@@ -250,6 +264,12 @@ recording:
0.001 in adaptive mode |
The minimum steady-state sampling rate for adaptive mode. In critical conditions the SDK can still temporarily pause recording. |
+
+ sampling.log_transitions |
+ boolean |
+ true |
+ Controls whether adaptive sampling emits Adaptive sampling updated (...) transition logs. Can be overridden by TUSK_RECORDING_SAMPLING_LOG_TRANSITIONS. |
+
sampling_rate |
number |
diff --git a/docs/nextjs-initialization.md b/docs/nextjs-initialization.md
index ab8d4c1..68d0a58 100644
--- a/docs/nextjs-initialization.md
+++ b/docs/nextjs-initialization.md
@@ -306,6 +306,12 @@ recording:
0.001 in adaptive mode |
The minimum steady-state sampling rate for adaptive mode. In critical conditions the SDK can still temporarily pause recording. |
+
+ sampling.log_transitions |
+ boolean |
+ true |
+ Controls whether adaptive sampling emits Adaptive sampling updated (...) transition logs. Can be overridden by TUSK_RECORDING_SAMPLING_LOG_TRANSITIONS. |
+
sampling_rate |
number |
diff --git a/src/core/TuskDrift.test.ts b/src/core/TuskDrift.test.ts
index 96d473d..77e9b71 100644
--- a/src/core/TuskDrift.test.ts
+++ b/src/core/TuskDrift.test.ts
@@ -8,6 +8,7 @@ type EnvVars = Record;
type SamplingConfigResult = {
baseRate: number;
minRate: number;
+ logTransitions: boolean;
mode: "fixed" | "adaptive";
};
type TestableTuskDrift = {
@@ -66,3 +67,56 @@ test("falls back to the legacy alias when TUSK_RECORDING_SAMPLING_RATE is invali
t.is(samplingConfig.baseRate, 0.4);
});
+
+test("uses recording.sampling.log_transitions from config when env var is unset", (t) => {
+ const drift = createTestDrift(t, {
+ TUSK_DRIFT_MODE: "DISABLED",
+ });
+ drift.config = {
+ recording: {
+ sampling: {
+ log_transitions: false,
+ },
+ },
+ };
+
+ const samplingConfig = drift.determineSamplingConfig({});
+
+ t.false(samplingConfig.logTransitions);
+});
+
+test("prefers TUSK_RECORDING_SAMPLING_LOG_TRANSITIONS over config", (t) => {
+ const drift = createTestDrift(t, {
+ TUSK_DRIFT_MODE: "DISABLED",
+ TUSK_RECORDING_SAMPLING_LOG_TRANSITIONS: "false",
+ });
+ drift.config = {
+ recording: {
+ sampling: {
+ log_transitions: true,
+ },
+ },
+ };
+
+ const samplingConfig = drift.determineSamplingConfig({});
+
+ t.false(samplingConfig.logTransitions);
+});
+
+test("falls back to config when TUSK_RECORDING_SAMPLING_LOG_TRANSITIONS is invalid", (t) => {
+ const drift = createTestDrift(t, {
+ TUSK_DRIFT_MODE: "DISABLED",
+ TUSK_RECORDING_SAMPLING_LOG_TRANSITIONS: "invalid",
+ });
+ drift.config = {
+ recording: {
+ sampling: {
+ log_transitions: false,
+ },
+ },
+ };
+
+ const samplingConfig = drift.determineSamplingConfig({});
+
+ t.false(samplingConfig.logTransitions);
+});
diff --git a/src/core/TuskDrift.ts b/src/core/TuskDrift.ts
index 614e2b4..3978304 100644
--- a/src/core/TuskDrift.ts
+++ b/src/core/TuskDrift.ts
@@ -86,6 +86,7 @@ export class TuskDriftCore {
private samplingRate = 1;
private samplingMode: SamplingMode = "fixed";
private minSamplingRate = 0;
+ private samplingLogTransitions = true;
private adaptiveSamplingController?: AdaptiveSamplingController;
private adaptiveSamplingInterval: NodeJS.Timeout | null = null;
private eventLoopDelayHistogram: IntervalHistogram | null = null;
@@ -203,7 +204,7 @@ export class TuskDriftCore {
const exportSpans = this.config.recording?.export_spans || false;
logger.info(
- `SDK initialized successfully (version=${SDK_VERSION}, mode=${this.mode}, env=${environment}, service=${serviceName}, serviceId=${serviceId}, exportSpans=${exportSpans}, samplingMode=${this.samplingMode}, samplingBaseRate=${this.samplingRate}, samplingMinRate=${this.minSamplingRate}, logLevel=${logger.getLogLevel()}, runtime=node ${process.version}, platform=${process.platform}/${process.arch}).`,
+ `SDK initialized successfully (version=${SDK_VERSION}, mode=${this.mode}, env=${environment}, service=${serviceName}, serviceId=${serviceId}, exportSpans=${exportSpans}, samplingMode=${this.samplingMode}, samplingBaseRate=${this.samplingRate}, samplingMinRate=${this.minSamplingRate}, samplingLogTransitions=${this.samplingLogTransitions}, logLevel=${logger.getLogLevel()}, runtime=node ${process.version}, platform=${process.platform}/${process.arch}).`,
);
}
@@ -240,6 +241,7 @@ export class TuskDriftCore {
mode: SamplingMode;
baseRate: number;
minRate: number;
+ logTransitions: boolean;
} {
const configSampling = this.config.recording?.sampling;
@@ -302,13 +304,57 @@ export class TuskDriftCore {
minRate = Math.min(baseRate, minRate);
}
+ let logTransitions = true;
+ const envLogTransitions = OriginalGlobalUtils.getOriginalProcessEnvVar(
+ "TUSK_RECORDING_SAMPLING_LOG_TRANSITIONS",
+ );
+ let parsedEnvLogTransitions: boolean | undefined;
+ if (envLogTransitions !== undefined) {
+ const parsed = this.parseBooleanSetting(
+ envLogTransitions,
+ "TUSK_RECORDING_SAMPLING_LOG_TRANSITIONS env var",
+ );
+ if (parsed !== undefined) {
+ logger.debug(
+ `Using adaptive sampling log_transitions from TUSK_RECORDING_SAMPLING_LOG_TRANSITIONS env var: ${parsed}`,
+ );
+ parsedEnvLogTransitions = parsed;
+ }
+ }
+
+ if (parsedEnvLogTransitions !== undefined) {
+ logTransitions = parsedEnvLogTransitions;
+ } else if (configSampling?.log_transitions !== undefined) {
+ if (typeof configSampling.log_transitions === "boolean") {
+ logTransitions = configSampling.log_transitions;
+ } else {
+ logger.warn(
+ `Invalid sampling.log_transitions in config.yaml: expected boolean, got ${typeof configSampling.log_transitions}. Ignoring.`,
+ );
+ }
+ }
+
return {
mode,
baseRate,
minRate,
+ logTransitions,
};
}
+ private parseBooleanSetting(value: string, source: string): boolean | undefined {
+ const normalizedValue = value.trim().toLowerCase();
+ if (["1", "true", "yes", "on"].includes(normalizedValue)) {
+ return true;
+ }
+ if (["0", "false", "no", "off"].includes(normalizedValue)) {
+ return false;
+ }
+
+ logger.warn(`Invalid ${source}: ${value}. Expected one of true/false/1/0/yes/no/on/off.`);
+ return undefined;
+ }
+
private registerDefaultInstrumentations(): void {
const transforms = this.config.transforms ?? this.initParams.transforms;
@@ -469,6 +515,8 @@ export class TuskDriftCore {
mode: this.samplingMode,
baseRate: this.samplingRate,
minRate: this.minSamplingRate,
+ }, {
+ logTransitions: this.samplingLogTransitions,
});
this.effectiveMemoryLimitBytes = this.detectEffectiveMemoryLimitBytes();
@@ -724,6 +772,7 @@ export class TuskDriftCore {
this.samplingMode = samplingConfig.mode;
this.samplingRate = samplingConfig.baseRate;
this.minSamplingRate = samplingConfig.minRate;
+ this.samplingLogTransitions = samplingConfig.logTransitions;
// Need to have observable service id if exporting spans to Tusk backend
if (this.config.recording?.export_spans && !this.config.service?.id) {
diff --git a/src/core/sampling/AdaptiveSamplingController.test.ts b/src/core/sampling/AdaptiveSamplingController.test.ts
index 23e4b7e..60c3080 100644
--- a/src/core/sampling/AdaptiveSamplingController.test.ts
+++ b/src/core/sampling/AdaptiveSamplingController.test.ts
@@ -1,5 +1,6 @@
import test from "ava";
import { AdaptiveSamplingController } from "./AdaptiveSamplingController";
+import { logger } from "../utils/logger";
test("pre-app-start requests bypass sampling and always record", (t) => {
const controller = new AdaptiveSamplingController(
@@ -89,3 +90,39 @@ test("adaptive controller reports load_shed when load shedding underflows effect
t.is(decision.state, "hot");
t.is(decision.reason, "load_shed");
});
+
+test("adaptive controller can suppress transition logs", (t) => {
+ let now = 0;
+ const originalInfo = logger.info;
+ let infoCalls = 0;
+
+ logger.info = (...args: unknown[]) => {
+ infoCalls += 1;
+ };
+
+ t.teardown(() => {
+ logger.info = originalInfo;
+ });
+
+ const controller = new AdaptiveSamplingController(
+ {
+ mode: "adaptive",
+ baseRate: 0.5,
+ minRate: 0.1,
+ },
+ {
+ logTransitions: false,
+ nowFn: () => now,
+ },
+ );
+
+ controller.update({
+ queueFillRatio: 0.9,
+ });
+ now += 1;
+ controller.update({
+ queueFillRatio: 0.1,
+ });
+
+ t.is(infoCalls, 0);
+});
diff --git a/src/core/sampling/AdaptiveSamplingController.ts b/src/core/sampling/AdaptiveSamplingController.ts
index bfe97af..155e448 100644
--- a/src/core/sampling/AdaptiveSamplingController.ts
+++ b/src/core/sampling/AdaptiveSamplingController.ts
@@ -58,6 +58,7 @@ export class AdaptiveSamplingController {
private readonly config: ResolvedSamplingConfig;
private readonly randomFn: () => number;
private readonly nowFn: () => number;
+ private readonly logTransitions: boolean;
private admissionMultiplier = 1;
private state: AdaptiveSamplingState;
@@ -77,14 +78,17 @@ export class AdaptiveSamplingController {
constructor(
config: ResolvedSamplingConfig,
{
+ logTransitions = true,
randomFn = Math.random,
nowFn = Date.now,
}: {
+ logTransitions?: boolean;
randomFn?: () => number;
nowFn?: () => number;
} = {},
) {
this.config = config;
+ this.logTransitions = logTransitions;
this.randomFn = randomFn;
this.nowFn = nowFn;
this.state = config.mode === "fixed" ? "fixed" : "healthy";
@@ -271,6 +275,10 @@ export class AdaptiveSamplingController {
pressure: number,
snapshot: AdaptiveSamplingHealthSnapshot,
): void {
+ if (!this.logTransitions) {
+ return;
+ }
+
if (
previousState === this.state &&
Math.abs(previousMultiplier - this.admissionMultiplier) < 0.05
diff --git a/src/core/utils/configUtils.ts b/src/core/utils/configUtils.ts
index 4aa9a2f..fa0c55a 100644
--- a/src/core/utils/configUtils.ts
+++ b/src/core/utils/configUtils.ts
@@ -37,6 +37,7 @@ export interface TuskConfig {
mode?: "fixed" | "adaptive";
base_rate?: number;
min_rate?: number;
+ log_transitions?: boolean;
};
export_spans?: boolean;
enable_env_var_recording?: boolean;