Redacting Sensitive Data from Log Output
When polling functions that handle credentials, API keys, or other secrets, you need to ensure those values never leak into your application logs. The sensitive_keys parameter lets you control exactly which argument keys are masked in log output from both TimeoutSampler and @retry.
Prerequisites
timeout-samplerinstalled in your project- Basic familiarity with
TimeoutSampleror the@retrydecorator (see Polling a Function with TimeoutSampler or Retrying Functions with the @retry Decorator)
Quick Example
from timeout_sampler import TimeoutSampler
for sample in TimeoutSampler(
wait_timeout=60,
sleep=5,
func=call_api,
headers={"Authorization": "Bearer my-secret-token", "Content-Type": "application/json"},
):
if sample:
break
The log output automatically masks the Authorization value:
Kwargs: {'headers': {'Authorization': '***', 'Content-Type': 'application/json'}}
No configuration needed — Authorization is one of the built-in sensitive keys.
Built-in Default Sensitive Keys
The following keys are redacted automatically, with no extra configuration:
| Key | Common Use |
|---|---|
authorization |
HTTP Authorization headers |
token |
OAuth/session tokens |
access_token |
OAuth2 access tokens |
password |
User/service credentials |
secret |
Shared secrets |
api_key |
API keys |
apikey |
API keys (alternate spelling) |
Note: Matching is case-insensitive and uses exact key name comparison. A key named
AuthorizationorAUTHORIZATIONwill be redacted, but a key likenextPageTokenortoken_countwill not — only a key named exactlytoken(in any case) triggers redaction.
Adding Custom Sensitive Keys
Pass a set or frozenset of additional key names to the sensitive_keys parameter:
from timeout_sampler import TimeoutSampler
for sample in TimeoutSampler(
wait_timeout=60,
sleep=1,
func=call_api,
sensitive_keys=frozenset({"x-custom-secret"}),
headers={"Authorization": "Bearer token", "x-custom-secret": "value"},
):
if sample:
break
The log output redacts both the built-in key and your custom key:
Kwargs: {'headers': {'Authorization': '***', 'x-custom-secret': '***'}}
Custom keys are merged with the defaults — you never lose the built-in protection. Custom key matching is also case-insensitive: "X-My-Token" will match x-my-token, X-MY-TOKEN, etc.
Using sensitive_keys with @retry
The @retry decorator accepts the same parameter:
from timeout_sampler import retry
@retry(
wait_timeout=30,
sleep=2,
sensitive_keys=frozenset({"x-api-secret", "session_id"}),
)
def fetch_data(headers=None, session_id=None):
# Both headers containing default sensitive keys AND session_id will be redacted
return make_request(headers=headers, session_id=session_id)
fetch_data(
headers={"Authorization": "Bearer abc123"},
session_id="sess-xyz-999",
)
See @retry Decorator API for the full parameter reference.
Passing an Empty Set
Passing an empty frozenset or set still preserves all built-in defaults — it does not disable redaction:
# Built-in keys are still redacted
sampler = TimeoutSampler(
wait_timeout=60,
sleep=1,
func=call_api,
sensitive_keys=frozenset(), # defaults still active
headers={"Authorization": "Bearer still-redacted"},
)
How Recursive Redaction Works
Redaction isn't limited to top-level keyword arguments. It walks through your data recursively, masking sensitive keys inside nested structures:
Nested Dictionaries
TimeoutSampler(
wait_timeout=10,
sleep=1,
func=process,
config={"database": {"password": "hunter2", "host": "db.example.com"}},
)
# Logged as: Kwargs: {'config': {'database': {'password': '***', 'host': 'db.example.com'}}}
Lists Containing Dictionaries
TimeoutSampler(
wait_timeout=10,
sleep=1,
func=process,
args_list=[{"token": "secret-in-list", "id": 42}],
)
# Logged as: Kwargs: {'args_list': [{'token': '***', 'id': 42}]}
Tuples Containing Dictionaries
Tuples are traversed the same way as lists — any dictionary found inside a tuple has its sensitive keys redacted.
Positional Arguments
Dictionaries passed as positional arguments via func_args are also redacted:
TimeoutSampler(
wait_timeout=10,
sleep=1,
func=send_request,
func_args=({"Authorization": "Bearer pos-secret", "safe": "visible"},),
)
# Logged as: Args: ({'Authorization': '***', 'safe': 'visible'},)
Non-String Dictionary Keys
Dictionaries with non-string keys (integers, tuples, etc.) are handled safely. Only string keys are checked for sensitive matches — non-string keys are passed through unchanged:
TimeoutSampler(
wait_timeout=10,
sleep=1,
func=process,
data={1: "int-key-value", "password": "secret123"},
)
# Logged as: Kwargs: {'data': {1: 'int-key-value', 'password': '***'}}
Advanced Usage
Disabling Argument Logging Entirely
If you prefer to suppress all argument output rather than relying on selective redaction, set print_func_args=False:
TimeoutSampler(
wait_timeout=60,
sleep=5,
func=call_api,
print_func_args=False,
headers={"Authorization": "Bearer secret"},
)
# Log output contains no Args/Kwargs section at all
See Controlling Log Output for more logging options.
Depth Limit for Nested Data
Redaction traverses up to 20 levels of nesting. Data nested beyond this depth is replaced with a sentinel value instead of being logged:
<redacted: max depth exceeded>
This protects against stack overflows from extremely deep or circular data structures. In practice, 20 levels is far deeper than any typical API payload.
Type Validation on sensitive_keys
The sensitive_keys parameter must contain only strings. Passing non-string values raises a TypeError immediately at construction time:
# Raises TypeError: sensitive_keys must contain only strings, got int: 123
TimeoutSampler(
wait_timeout=10,
sleep=1,
func=call_api,
sensitive_keys=frozenset({123, "valid_key"}),
)
Tip: Validate your
sensitive_keysvalues early. Errors are raised atTimeoutSampleror@retryinitialization — not when log output is generated — so you'll catch mistakes before any polling begins.
Troubleshooting
| Problem | Cause | Solution |
|---|---|---|
A key like nextPageToken is unexpectedly redacted |
Custom sensitive_keys contains a key that partially matches |
Redaction uses exact key name matching (case-insensitive). Check your sensitive_keys set for overly broad entries. |
| Sensitive values still appear in logs | The key name isn't in the default set or your custom set | Add the key to sensitive_keys. Only exact key-name matches are redacted. |
TypeError when constructing the sampler |
Non-string value in sensitive_keys |
Ensure all elements in the set are strings. |
<redacted: max depth exceeded> appears in logs |
Data structure is nested more than 20 levels deep | This is expected safety behavior. Restructure data or disable argument logging with print_func_args=False. |
| No kwargs visible at all in logs | print_func_args is set to False |
Set print_func_args=True (the default) to see redacted argument output. |