Skip to content

Commit 7d3a3f4

Browse files
committed
fix: benchmark rust masking via public api
Signed-off-by: lucarlig <luca.carlig@ibm.com>
1 parent 6a4cfa5 commit 7d3a3f4

5 files changed

Lines changed: 95 additions & 15 deletions

File tree

.secrets.baseline

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"files": null,
44
"lines": null
55
},
6-
"generated_at": "2026-04-03T13:21:10Z",
6+
"generated_at": "2026-04-03T14:47:08Z",
77
"plugins_used": [
88
{
99
"name": "AWSKeyDetector"
@@ -22032,7 +22032,7 @@
2203222032
"hashed_secret": "4ea8d2335b430796cf3f500368c5b0f5b1dc90f5",
2203322033
"is_secret": false,
2203422034
"is_verified": false,
22035-
"line_number": 187,
22035+
"line_number": 207,
2203622036
"type": "Secret Keyword",
2203722037
"verified_result": null
2203822038
}

mcpgateway/config.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -351,7 +351,7 @@ class Settings(BaseSettings):
351351
experimental_validate_io: bool = Field(default=False, description="Enable experimental input validation and output sanitization")
352352
experimental_rust_request_logging_masking_enabled: bool = Field(
353353
default=False,
354-
description="Enable experimental Rust sidecar for request logging sensitive-data masking",
354+
description="Enable experimental Rust native extension for request logging sensitive-data masking",
355355
)
356356
validation_middleware_enabled: bool = Field(default=False, description="Enable validation middleware for all requests")
357357
validation_strict: bool = Field(default=True, description="Strict validation mode - reject on violations")

mcpgateway/middleware/request_logging_middleware.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -285,7 +285,7 @@ def mask_sensitive_headers(headers):
285285

286286

287287
def _load_rust_request_logging_module():
288-
"""Load the experimental Rust masking sidecar on demand."""
288+
"""Load the experimental Rust masking native extension on demand."""
289289
global _RUST_REQUEST_LOGGING_MODULE
290290

291291
if _RUST_REQUEST_LOGGING_MODULE is None:

tests/performance/test_request_logging_masking_sidecar_benchmark.py

Lines changed: 71 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
# -*- coding: utf-8 -*-
2-
"""Benchmark the request-logging masking Rust sidecar against the Python path."""
2+
"""Benchmark the request-logging masking Rust native extension against the Python path."""
33

44
# Standard
55
from __future__ import annotations
66

7+
from contextlib import contextmanager
78
import importlib
89
import statistics
910
import subprocess
@@ -12,6 +13,7 @@
1213
from typing import Any, Callable
1314

1415
# First-Party
16+
import mcpgateway.middleware.request_logging_middleware as request_logging_middleware
1517
from mcpgateway.config import settings
1618
from mcpgateway.middleware.request_logging_middleware import mask_sensitive_data, mask_sensitive_headers
1719

@@ -24,7 +26,36 @@ def _ensure_sidecar_installed() -> Any:
2426
return importlib.import_module("request_logging_masking_sidecar")
2527

2628

29+
def test_reset_rust_module_handle_keeps_flagged_masking_reloadable() -> None:
30+
_ensure_sidecar_installed()
31+
request_logging_middleware._RUST_REQUEST_LOGGING_MODULE = object()
32+
33+
_reset_rust_module_handle()
34+
35+
with _rust_masking_enabled(True):
36+
masked = mask_sensitive_data({"password": "secret"}, 12) # pragma: allowlist secret
37+
38+
assert masked == {"password": "******"} # pragma: allowlist secret
39+
40+
41+
@contextmanager
42+
def _rust_masking_enabled(enabled: bool):
43+
original = settings.experimental_rust_request_logging_masking_enabled
44+
settings.experimental_rust_request_logging_masking_enabled = enabled
45+
try:
46+
yield
47+
finally:
48+
settings.experimental_rust_request_logging_masking_enabled = original
49+
50+
51+
def _reset_rust_module_handle() -> None:
52+
request_logging_middleware._RUST_REQUEST_LOGGING_MODULE = None
53+
54+
2755
def _measure(label: str, fn: Callable[[Any], Any], payload: Any, iterations: int) -> tuple[float, float]:
56+
for _ in range(5):
57+
fn(payload)
58+
2859
samples = []
2960
for _ in range(iterations):
3061
started = time.perf_counter_ns()
@@ -37,6 +68,24 @@ def _measure(label: str, fn: Callable[[Any], Any], payload: Any, iterations: int
3768
return median_ms, p95_ms
3869

3970

71+
def _measure_once(label: str, fn: Callable[[Any], Any], payload: Any) -> float:
72+
started = time.perf_counter_ns()
73+
fn(payload)
74+
elapsed_ms = (time.perf_counter_ns() - started) / 1_000_000
75+
print(f"{label}: elapsed={elapsed_ms:.3f}ms")
76+
return elapsed_ms
77+
78+
79+
def _run_with_rust_masking_enabled(enabled: bool, fn: Callable[[Any], Any], payload: Any) -> Any:
80+
with _rust_masking_enabled(enabled):
81+
return fn(payload)
82+
83+
84+
def _measure_with_rust_masking_enabled(enabled: bool, label: str, fn: Callable[[Any], Any], payload: Any, iterations: int) -> tuple[float, float]:
85+
with _rust_masking_enabled(enabled):
86+
return _measure(label, fn, payload, iterations)
87+
88+
4089
def _assert_parity(python_fn: Callable[[Any], Any], rust_fn: Callable[[Any], Any], payloads: list[Any]) -> None:
4190
for payload in payloads:
4291
python_result = python_fn(payload)
@@ -47,29 +96,36 @@ def _assert_parity(python_fn: Callable[[Any], Any], rust_fn: Callable[[Any], Any
4796

4897
def main() -> None:
4998
sidecar = _ensure_sidecar_installed()
50-
settings.experimental_rust_request_logging_masking_enabled = False
99+
request_logging_middleware._RUST_REQUEST_LOGGING_MODULE = sidecar
51100

52101
def python_data(payload: Any) -> Any:
53102
return mask_sensitive_data(payload, 12)
54103

55104
def rust_data(payload: Any) -> Any:
56-
return sidecar.mask_sensitive_data(payload, 12)
105+
return mask_sensitive_data(payload, 12)
106+
107+
def python_headers(payload: Any) -> Any:
108+
return mask_sensitive_headers(payload)
109+
110+
def rust_headers(payload: Any) -> Any:
111+
return mask_sensitive_headers(payload)
57112

58-
python_headers = mask_sensitive_headers
59-
rust_headers = sidecar.mask_sensitive_headers
113+
# Warm the lazy import path once so the benchmark measures steady-state masking work.
114+
_run_with_rust_masking_enabled(True, rust_data, {"warmupToken": "warmup"})
115+
_run_with_rust_masking_enabled(True, rust_headers, {"Authorization": "Bearer warmup"})
60116

61117
_assert_parity(
62-
python_data,
63-
rust_data,
118+
lambda payload: _run_with_rust_masking_enabled(False, python_data, payload),
119+
lambda payload: _run_with_rust_masking_enabled(True, rust_data, payload),
64120
[
65121
{"password": "secret", "nested": {"authToken": "abc", "ok": "value"}},
66122
{"token_count": 3, "tokenizer": "ok", "privateKey": "secret"},
67123
[{"jwt_token": "abc"}, {"normal": "value"}],
68124
],
69125
)
70126
_assert_parity(
71-
python_headers,
72-
rust_headers,
127+
lambda payload: _run_with_rust_masking_enabled(False, python_headers, payload),
128+
lambda payload: _run_with_rust_masking_enabled(True, rust_headers, payload),
73129
[
74130
{"Authorization": "Bearer abc", "Cookie": "jwt_token=abc; theme=dark", "X-Trace-Id": "123"},
75131
{"X-Auth-Count": "5", "X-Api-Key": "secret"},
@@ -110,8 +166,12 @@ def rust_data(payload: Any) -> Any:
110166

111167
for name, python_fn, rust_fn, payload, iterations in scenarios:
112168
print(f"\n{name} ({iterations} iterations)")
113-
python_median, _ = _measure("python", python_fn, payload, iterations)
114-
rust_median, _ = _measure("rust", rust_fn, payload, iterations)
169+
_reset_rust_module_handle()
170+
with _rust_masking_enabled(True):
171+
_measure_once("rust first_public_call", rust_fn, payload)
172+
_measure_once("rust warmup", rust_fn, payload)
173+
python_median, _ = _measure_with_rust_masking_enabled(False, "python", python_fn, payload, iterations)
174+
rust_median, _ = _measure_with_rust_masking_enabled(True, "rust-public-api", rust_fn, payload, iterations)
115175
print(f"speedup={python_median / rust_median:.2f}x")
116176

117177

tests/unit/mcpgateway/middleware/test_request_logging_middleware.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,26 @@ def test_mask_sensitive_data_uses_rust_sidecar_when_enabled(monkeypatch):
137137
rust_module.mask_sensitive_data.assert_called_once_with({"password": "secret", "username": "user"}, 10) # pragma: allowlist secret
138138

139139

140+
def test_mask_sensitive_data_missing_sidecar_is_hard_failure_when_enabled(monkeypatch):
141+
monkeypatch.setattr("mcpgateway.middleware.request_logging_middleware.settings.experimental_rust_request_logging_masking_enabled", True, raising=False)
142+
143+
with patch("mcpgateway.middleware.request_logging_middleware._load_rust_request_logging_module", side_effect=ModuleNotFoundError("missing sidecar")):
144+
with pytest.raises(ModuleNotFoundError, match="missing sidecar"):
145+
mask_sensitive_data({"password": "secret"}) # pragma: allowlist secret
146+
147+
148+
def test_mask_sensitive_headers_uses_rust_sidecar_when_enabled(monkeypatch):
149+
rust_module = MagicMock()
150+
rust_module.mask_sensitive_headers.return_value = {"Authorization": "******", "X-Custom": "ok"}
151+
monkeypatch.setattr("mcpgateway.middleware.request_logging_middleware.settings.experimental_rust_request_logging_masking_enabled", True, raising=False)
152+
153+
with patch("mcpgateway.middleware.request_logging_middleware._load_rust_request_logging_module", return_value=rust_module):
154+
masked = mask_sensitive_headers({"Authorization": "Bearer abc", "X-Custom": "ok"})
155+
156+
assert masked == {"Authorization": "******", "X-Custom": "ok"}
157+
rust_module.mask_sensitive_headers.assert_called_once_with({"Authorization": "Bearer abc", "X-Custom": "ok"})
158+
159+
140160
def test_mask_sensitive_headers_missing_sidecar_is_hard_failure_when_enabled(monkeypatch):
141161
monkeypatch.setattr("mcpgateway.middleware.request_logging_middleware.settings.experimental_rust_request_logging_masking_enabled", True, raising=False)
142162

0 commit comments

Comments
 (0)