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
19 changes: 19 additions & 0 deletions docs/environment-variables.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
20 changes: 20 additions & 0 deletions docs/initialization.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

<table>
Expand Down Expand Up @@ -250,6 +264,12 @@ recording:
<td><code>0.001</code> in <code>adaptive</code> mode</td>
<td>The minimum steady-state sampling rate for adaptive mode. In critical conditions the SDK can still temporarily pause recording.</td>
</tr>
<tr>
<td><code>sampling.log_transitions</code></td>
<td><code>boolean</code></td>
<td><code>true</code></td>
<td>Controls whether adaptive sampling emits <code>Adaptive sampling updated (...)</code> transition logs. Can be overridden by <code>TUSK_RECORDING_SAMPLING_LOG_TRANSITIONS</code>.</td>
</tr>
<tr>
<td><code>sampling_rate</code></td>
<td><code>number</code></td>
Expand Down
6 changes: 6 additions & 0 deletions docs/nextjs-initialization.md
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,12 @@ recording:
<td><code>0.001</code> in <code>adaptive</code> mode</td>
<td>The minimum steady-state sampling rate for adaptive mode. In critical conditions the SDK can still temporarily pause recording.</td>
</tr>
<tr>
<td><code>sampling.log_transitions</code></td>
<td><code>boolean</code></td>
<td><code>true</code></td>
<td>Controls whether adaptive sampling emits <code>Adaptive sampling updated (...)</code> transition logs. Can be overridden by <code>TUSK_RECORDING_SAMPLING_LOG_TRANSITIONS</code>.</td>
</tr>
<tr>
<td><code>sampling_rate</code></td>
<td><code>number</code></td>
Expand Down
54 changes: 54 additions & 0 deletions src/core/TuskDrift.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ type EnvVars = Record<string, string | undefined>;
type SamplingConfigResult = {
baseRate: number;
minRate: number;
logTransitions: boolean;
mode: "fixed" | "adaptive";
};
type TestableTuskDrift = {
Expand Down Expand Up @@ -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);
});
51 changes: 50 additions & 1 deletion src/core/TuskDrift.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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}).`,
);
}

Expand Down Expand Up @@ -240,6 +241,7 @@ export class TuskDriftCore {
mode: SamplingMode;
baseRate: number;
minRate: number;
logTransitions: boolean;
} {
const configSampling = this.config.recording?.sampling;

Expand Down Expand Up @@ -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.`,
);
}
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
}

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;

Expand Down Expand Up @@ -469,6 +515,8 @@ export class TuskDriftCore {
mode: this.samplingMode,
baseRate: this.samplingRate,
minRate: this.minSamplingRate,
}, {
logTransitions: this.samplingLogTransitions,
});

this.effectiveMemoryLimitBytes = this.detectEffectiveMemoryLimitBytes();
Expand Down Expand Up @@ -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) {
Expand Down
37 changes: 37 additions & 0 deletions src/core/sampling/AdaptiveSamplingController.test.ts
Original file line number Diff line number Diff line change
@@ -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(
Expand Down Expand Up @@ -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);
});
8 changes: 8 additions & 0 deletions src/core/sampling/AdaptiveSamplingController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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";
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions src/core/utils/configUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading