Skip to Content
RiusSDKAdvanced features

Advanced features

Volume and privacy controls for production deployments of the Rius SDK: head sampling to reduce trace volume, and two independent controls over what content leaves your process. All of them are init() arguments; none require code changes in your agent.

Sampling

By default the SDK exports every trace. To reduce volume, set a head-sampling rate:

rius.init(sample_rate=0.25) # keep ~25% of traces

or via the environment:

export RIUS_SAMPLE_RATE=0.25

How it samples

The SDK installs a ParentBased(TraceIdRatioBased(rate)) sampler:

  • Whole traces, not individual spans. The decision is made once per trace, from the trace ID, at the root span. Every child span follows the root’s decision, so you never get fragments of a trace.
  • Parent decisions are respected. If a span arrives with tracing context from an upstream service or another instrumentation layer, its sampled flag is inherited rather than re-rolled. The rate only governs traces that start inside your process.
  • Head sampling only. The decision happens at trace start; there is no tail sampling based on errors or latency in the SDK.

Values outside [0.0, 1.0] are clamped, with a warning log. 0.0 drops everything; 1.0 (the default) keeps everything.

When to sample

Sampling trades visibility for volume. Two rules of thumb:

  • Keep 1.0 in development and staging; you want every trace while iterating.
  • In high-traffic production, prefer lowering the rate over disabling tracing: a 10% sample still shows latency and cost distributions, and error spans in sampled traces remain fully detailed.

If you need to drop content but keep every trace, use the privacy controls below instead of sampling.

Privacy controls

The SDK has two independent controls over what content leaves your process: a switch that strips content entirely, and a masking hook that transforms it. Both run at export time, inside your process, before anything is sent.

capture_content: the content switch

rius.init(capture_content=False) # or: export RIUS_CAPTURE_CONTENT=false

The default is True (content is captured). With False, prompt and response content is stripped from spans at export while metadata still flows: model names, token counts, span taxonomy, finish reasons, latencies. You keep cost and performance analytics and give up content-level debugging.

The same span in the console, with the switch in each position:

Span Input / Output tab with content captured: the input and output panels show the full request and response

Default (capture_content=True): Input / Output carries the full content.

The same span with capture_content off: the input and output panels are empty, while duration, spans, and status remain

capture_content=False: the panels are empty; timings, taxonomy, and status still flow.

Exactly which attributes are covered

The switch (and masking, below) applies to the content attribute families of the SDK itself and of the third-party instrumentation it ships:

FamilyAttributes
Rius spansinput.value, output.value
Rius generationsgen_ai.input.messages, gen_ai.output.messages
Tool definitionsgen_ai.tool.definitions, gen_ai.tool.description (the conventions mark these sensitive: definitions carry prompt engineering, and sometimes credentials in parameter defaults). gen_ai.tool.name is identity, not content, and always flows
GenAI (third-party)gen_ai.prompt, gen_ai.completion, and any gen_ai.prompt.* / gen_ai.completion.*
OpenInferencellm.input_messages, llm.output_messages, and any llm.input_messages.* / llm.output_messages.* / llm.prompts.* / llm.prompt_template.*
Documents and embeddingsany attribute ending .document.content or .embedding.text
MLflow / Traceloopmlflow.spanInputs / mlflow.spanOutputs, traceloop.entity.input / traceloop.entity.output

Indexed keys are matched by prefix, so llm.input_messages.0.message.content is covered. Metadata siblings such as .document.id and .document.score are deliberately not covered, so retrieval analytics survive content stripping.

Attributes outside these families (your own set_attribute() keys, for example) are never touched; treat custom attributes as content you control.

Masking: transform instead of strip

When you need content but not the sensitive parts of it, pass a mask callable. It runs on every covered content attribute:

def mask(value): return my_pii_scrubber(value) rius.init(mask=mask)

If the callable accepts a key keyword argument (or **kwargs), it also receives the attribute name, so you can mask selectively:

def mask(value, *, key: str): if key.startswith("gen_ai.input"): return scrub(value) return value rius.init(mask=mask)

Semantics you can rely on

  • Runs at export, off the hot path. Masking executes in the background batch thread, on copies of span data, so it never mutates what other processors see and never slows your agent’s code.
  • Fail closed. If the mask raises, or returns None, or returns something that cannot be serialized, the affected attribute is dropped entirely and a warning is logged. The original value never leaks on any failure path, and the rest of the batch still exports.
  • Return types. Primitives pass through as-is; other objects are serialized to JSON (truncated at 8192 characters).
  • Composition. capture_content=False is applied first; the mask then runs on whatever content attributes remain.

The TypeScript SDK covers the same attribute families and composes the two controls the same way, and its mask also runs in the exporter chain rather than on your agent’s hot path. It differs on one point: when a mask throws, the affected attribute value becomes the literal string [mask error] instead of being dropped, so a broken mask is visible in the trace and the original value still never leaves the process.

Exceptions, stacktraces, and status messages

Errors are the one place where the two controls do not behave alike, and it matters: a provider error routinely echoes the offending request back in its message, and a stacktrace can carry it in a frame argument.

In TypeScript, with captureContent: false:

  • The exception’s exception.message and exception.stacktrace are stripped from the exception event.
  • The event itself and exception.type are kept, so failures stay visible and countable.
  • The error message that recordException copies onto the span status is cleared, while the ERROR status code remains.

A mask is not a substitute for that. It applies to content attributes, including content attributes carried on span events and on span links, but it does not apply to exception messages, stacktraces, or the status message. If you supply a redactor and leave content capture on, raw provider error text still reaches the backend. Where error text is as sensitive as the prompt itself, set captureContent: false; masking alone will not cover it.

In Python, exception events and the span status are outside the scope of both controls: capture_content=False strips content attributes, and error messages and stacktraces are exported as recorded. Keep that in mind when you rely on either control for a regulated environment.

Choosing between them

  • Regulated environment, content must never leave: capture_content=False.
  • Content is useful but contains PII: a mask with your scrubber.
  • Different rules per attribute family: a key-aware mask.

Next steps

Last updated on