Skip to content

Commit 694eec0

Browse files
committed
refactor: rename request logging native extension
Signed-off-by: lucarlig <luca.carlig@ibm.com>
1 parent fe56ca8 commit 694eec0

8 files changed

Lines changed: 104 additions & 30 deletions

File tree

mcpgateway/config.py

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

mcpgateway/middleware/request_logging_middleware.py

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@
7676
structured_logger = get_structured_logger("http_gateway")
7777

7878
_RUST_REQUEST_LOGGING_MODULE = None
79+
_RUST_REQUEST_LOGGING_IMPORT_FAILED = False
7980

8081
SENSITIVE_KEYS = frozenset(
8182
{
@@ -187,7 +188,9 @@ def mask_sensitive_data(data, max_depth: int = 10):
187188
{'level': '<nested too deep>'}
188189
"""
189190
if getattr(settings, "experimental_rust_request_logging_masking_enabled", False) is True:
190-
return _load_rust_request_logging_module().mask_sensitive_data(data, max_depth)
191+
rust_module = _load_rust_request_logging_module()
192+
if rust_module is not None:
193+
return rust_module.mask_sensitive_data(data, max_depth)
191194

192195
if max_depth <= 0:
193196
return "<nested too deep>"
@@ -269,7 +272,9 @@ def mask_sensitive_headers(headers):
269272
True
270273
"""
271274
if getattr(settings, "experimental_rust_request_logging_masking_enabled", False) is True:
272-
return _load_rust_request_logging_module().mask_sensitive_headers(headers)
275+
rust_module = _load_rust_request_logging_module()
276+
if rust_module is not None:
277+
return rust_module.mask_sensitive_headers(headers)
273278

274279
masked_headers = {}
275280
for key, value in headers.items():
@@ -285,11 +290,21 @@ def mask_sensitive_headers(headers):
285290

286291

287292
def _load_rust_request_logging_module():
288-
"""Load the experimental Rust masking sidecar on demand."""
289-
global _RUST_REQUEST_LOGGING_MODULE
293+
"""Load the experimental Rust masking native extension on demand."""
294+
global _RUST_REQUEST_LOGGING_IMPORT_FAILED, _RUST_REQUEST_LOGGING_MODULE
290295

291296
if _RUST_REQUEST_LOGGING_MODULE is None:
292-
_RUST_REQUEST_LOGGING_MODULE = importlib.import_module("request_logging_masking_sidecar")
297+
if _RUST_REQUEST_LOGGING_IMPORT_FAILED:
298+
return None
299+
try:
300+
_RUST_REQUEST_LOGGING_MODULE = importlib.import_module("request_logging_masking_native_extension")
301+
except ImportError as exc:
302+
_RUST_REQUEST_LOGGING_IMPORT_FAILED = True
303+
logger.warning(
304+
f"Experimental Rust request logging masking is enabled but the native extension is unavailable; "
305+
f"falling back to Python masking. Install the request logging masking native extension first. ({exc})"
306+
)
307+
return None
293308
return _RUST_REQUEST_LOGGING_MODULE
294309

295310

tests/performance/test_request_logging_masking_sidecar_benchmark.py renamed to tests/performance/test_request_logging_masking_native_extension_benchmark.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
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
@@ -16,12 +16,12 @@
1616
from mcpgateway.middleware.request_logging_middleware import mask_sensitive_data, mask_sensitive_headers
1717

1818
REPO_ROOT = Path(__file__).resolve().parents[2]
19-
SIDECAR_MANIFEST = REPO_ROOT / "tools_rust" / "request_logging_masking_sidecar" / "Cargo.toml"
19+
NATIVE_EXTENSION_MANIFEST = REPO_ROOT / "tools_rust" / "request_logging_masking_native_extension" / "Cargo.toml"
2020

2121

22-
def _ensure_sidecar_installed() -> Any:
23-
subprocess.run(["uv", "run", "maturin", "develop", "--release", "--manifest-path", str(SIDECAR_MANIFEST)], check=True, cwd=REPO_ROOT)
24-
return importlib.import_module("request_logging_masking_sidecar")
22+
def _ensure_native_extension_installed() -> Any:
23+
subprocess.run(["uv", "run", "maturin", "develop", "--release", "--manifest-path", str(NATIVE_EXTENSION_MANIFEST)], check=True, cwd=REPO_ROOT)
24+
return importlib.import_module("request_logging_masking_native_extension")
2525

2626

2727
def _measure(label: str, fn: Callable[[Any], Any], payload: Any, iterations: int) -> tuple[float, float]:
@@ -46,17 +46,17 @@ def _assert_parity(python_fn: Callable[[Any], Any], rust_fn: Callable[[Any], Any
4646

4747

4848
def main() -> None:
49-
sidecar = _ensure_sidecar_installed()
49+
native_extension = _ensure_native_extension_installed()
5050
settings.experimental_rust_request_logging_masking_enabled = False
5151

5252
def python_data(payload: Any) -> Any:
5353
return mask_sensitive_data(payload, 12)
5454

5555
def rust_data(payload: Any) -> Any:
56-
return sidecar.mask_sensitive_data(payload, 12)
56+
return native_extension.mask_sensitive_data(payload, 12)
5757

5858
python_headers = mask_sensitive_headers
59-
rust_headers = sidecar.mask_sensitive_headers
59+
rust_headers = native_extension.mask_sensitive_headers
6060

6161
_assert_parity(
6262
python_data,

tests/unit/mcpgateway/middleware/test_request_logging_middleware.py

Lines changed: 69 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,12 @@ async def _call_next(request):
6565
return _call_next
6666

6767

68+
@pytest.fixture(autouse=True)
69+
def reset_rust_request_logging_module_state(monkeypatch):
70+
monkeypatch.setattr("mcpgateway.middleware.request_logging_middleware._RUST_REQUEST_LOGGING_MODULE", None)
71+
monkeypatch.setattr("mcpgateway.middleware.request_logging_middleware._RUST_REQUEST_LOGGING_IMPORT_FAILED", False)
72+
73+
6874
def make_request(body: bytes = b"{}", headers=None, query_params=None):
6975
scope: Scope = {
7076
"type": "http",
@@ -125,16 +131,42 @@ def test_mask_sensitive_data_ignores_empty_normalized_keys():
125131
assert masked["password"] == "******"
126132

127133

128-
def test_mask_sensitive_data_uses_pyo3_module_when_enabled(monkeypatch):
129-
rust_module = MagicMock()
130-
rust_module.mask_sensitive_data.return_value = {"password": "******", "username": "user"} # pragma: allowlist secret
134+
def test_mask_sensitive_data_uses_native_extension_when_enabled(monkeypatch):
135+
native_extension = MagicMock()
136+
native_extension.mask_sensitive_data.return_value = {"password": "******", "username": "user"} # pragma: allowlist secret
131137
monkeypatch.setattr("mcpgateway.middleware.request_logging_middleware.settings.experimental_rust_request_logging_masking_enabled", True, raising=False)
132138

133-
with patch("mcpgateway.middleware.request_logging_middleware._load_rust_request_logging_module", return_value=rust_module):
139+
with patch("mcpgateway.middleware.request_logging_middleware._load_rust_request_logging_module", return_value=native_extension):
134140
masked = mask_sensitive_data({"password": "secret", "username": "user"}) # pragma: allowlist secret
135141

136142
assert masked == {"password": "******", "username": "user"} # pragma: allowlist secret
137-
rust_module.mask_sensitive_data.assert_called_once_with({"password": "secret", "username": "user"}, 10) # pragma: allowlist secret
143+
native_extension.mask_sensitive_data.assert_called_once_with({"password": "secret", "username": "user"}, 10) # pragma: allowlist secret
144+
145+
146+
def test_mask_sensitive_data_falls_back_to_python_when_native_extension_import_fails(monkeypatch, dummy_logger):
147+
monkeypatch.setattr("mcpgateway.middleware.request_logging_middleware.settings.experimental_rust_request_logging_masking_enabled", True, raising=False)
148+
monkeypatch.setattr("mcpgateway.middleware.request_logging_middleware.importlib.import_module", MagicMock(side_effect=ImportError("boom")))
149+
150+
masked = mask_sensitive_data({"password": "secret", "username": "user"}) # pragma: allowlist secret
151+
152+
assert masked == {"password": "******", "username": "user"} # pragma: allowlist secret
153+
assert len(dummy_logger.warnings) == 1
154+
assert "falling back to Python masking" in dummy_logger.warnings[0]
155+
assert "native extension" in dummy_logger.warnings[0]
156+
157+
158+
def test_mask_sensitive_data_caches_failed_native_extension_import(monkeypatch, dummy_logger):
159+
import_module = MagicMock(side_effect=ImportError("boom"))
160+
monkeypatch.setattr("mcpgateway.middleware.request_logging_middleware.settings.experimental_rust_request_logging_masking_enabled", True, raising=False)
161+
monkeypatch.setattr("mcpgateway.middleware.request_logging_middleware.importlib.import_module", import_module)
162+
163+
first = mask_sensitive_data({"password": "secret"}) # pragma: allowlist secret
164+
second = mask_sensitive_data({"password": "secret"}) # pragma: allowlist secret
165+
166+
assert first == {"password": "******"} # pragma: allowlist secret
167+
assert second == {"password": "******"} # pragma: allowlist secret
168+
assert import_module.call_count == 1
169+
assert len(dummy_logger.warnings) == 1
138170

139171

140172
# --- mask_jwt_in_cookies tests ---
@@ -196,16 +228,26 @@ def test_mask_sensitive_headers_respects_non_sensitive_suffixes():
196228
assert masked["X-JWT_Status_Count"] == "7"
197229

198230

199-
def test_mask_sensitive_headers_uses_pyo3_module_when_enabled(monkeypatch):
200-
rust_module = MagicMock()
201-
rust_module.mask_sensitive_headers.return_value = {"Authorization": "******"}
231+
def test_mask_sensitive_headers_uses_native_extension_when_enabled(monkeypatch):
232+
native_extension = MagicMock()
233+
native_extension.mask_sensitive_headers.return_value = {"Authorization": "******"}
202234
monkeypatch.setattr("mcpgateway.middleware.request_logging_middleware.settings.experimental_rust_request_logging_masking_enabled", True, raising=False)
203235

204-
with patch("mcpgateway.middleware.request_logging_middleware._load_rust_request_logging_module", return_value=rust_module):
236+
with patch("mcpgateway.middleware.request_logging_middleware._load_rust_request_logging_module", return_value=native_extension):
205237
masked = mask_sensitive_headers({"Authorization": "Bearer abc"})
206238

207239
assert masked == {"Authorization": "******"}
208-
rust_module.mask_sensitive_headers.assert_called_once_with({"Authorization": "Bearer abc"})
240+
native_extension.mask_sensitive_headers.assert_called_once_with({"Authorization": "Bearer abc"})
241+
242+
243+
def test_mask_sensitive_headers_fall_back_to_python_when_native_extension_import_fails(monkeypatch, dummy_logger):
244+
monkeypatch.setattr("mcpgateway.middleware.request_logging_middleware.settings.experimental_rust_request_logging_masking_enabled", True, raising=False)
245+
monkeypatch.setattr("mcpgateway.middleware.request_logging_middleware.importlib.import_module", MagicMock(side_effect=ImportError("boom")))
246+
247+
masked = mask_sensitive_headers({"Authorization": "Bearer abc", "X-Trace-Id": "123"})
248+
249+
assert masked == {"Authorization": "******", "X-Trace-Id": "123"}
250+
assert len(dummy_logger.warnings) == 1
209251

210252

211253
# --- RequestLoggingMiddleware tests ---
@@ -222,6 +264,23 @@ async def test_dispatch_logs_json_body(dummy_logger, mock_structured_logger, dum
222264
assert "******" in dummy_logger.logged[0][1]
223265

224266

267+
@pytest.mark.asyncio
268+
async def test_dispatch_falls_back_to_python_masking_when_native_extension_import_fails(dummy_logger, mock_structured_logger, dummy_call_next, monkeypatch):
269+
monkeypatch.setattr("mcpgateway.middleware.request_logging_middleware.settings.experimental_rust_request_logging_masking_enabled", True, raising=False)
270+
monkeypatch.setattr("mcpgateway.middleware.request_logging_middleware.importlib.import_module", MagicMock(side_effect=ImportError("boom")))
271+
272+
middleware = RequestLoggingMiddleware(app=None, enable_gateway_logging=False, log_detailed_requests=True)
273+
body = orjson.dumps({"password": "123", "data": "ok"})
274+
request = make_request(body=body, headers={"Authorization": "Bearer abc"})
275+
276+
response = await middleware.dispatch(request, dummy_call_next)
277+
278+
assert response.status_code == 200
279+
assert any("📩 Incoming request" in msg for _, msg in dummy_logger.logged)
280+
assert "******" in dummy_logger.logged[0][1]
281+
assert any("falling back to Python masking" in warning for warning in dummy_logger.warnings)
282+
283+
225284
@pytest.mark.asyncio
226285
async def test_dispatch_logs_non_json_body(dummy_logger, mock_structured_logger, dummy_call_next):
227286
middleware = RequestLoggingMiddleware(app=None, enable_gateway_logging=False, log_detailed_requests=True)

tools_rust/request_logging_masking_sidecar/Cargo.lock renamed to tools_rust/request_logging_masking_native_extension/Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tools_rust/request_logging_masking_sidecar/Cargo.toml renamed to tools_rust/request_logging_masking_native_extension/Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
[package]
2-
name = "request_logging_masking_sidecar"
2+
name = "request_logging_masking_native_extension"
33
version = "0.1.0"
44
edition = "2021"
55
license = "Apache-2.0"
66

77
[lib]
8-
name = "request_logging_masking_sidecar"
8+
name = "request_logging_masking_native_extension"
99
crate-type = ["cdylib"]
1010

1111
[dependencies]

tools_rust/request_logging_masking_sidecar/pyproject.toml renamed to tools_rust/request_logging_masking_native_extension/pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@ requires = ["maturin>=1.8,<2.0"]
33
build-backend = "maturin"
44

55
[project]
6-
name = "request-logging-masking-sidecar"
6+
name = "request-logging-masking-native-extension"
77
version = "0.1.0"
88
requires-python = ">=3.11"
99

1010
[tool.maturin]
11-
module-name = "request_logging_masking_sidecar"
11+
module-name = "request_logging_masking_native_extension"
1212
bindings = "pyo3"

tools_rust/request_logging_masking_sidecar/src/lib.rs renamed to tools_rust/request_logging_masking_native_extension/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,7 @@ fn mask_sensitive_headers(py: Python<'_>, headers: &Bound<'_, PyAny>) -> PyResul
224224
}
225225

226226
#[pymodule]
227-
fn request_logging_masking_sidecar(module: &Bound<'_, PyModule>) -> PyResult<()> {
227+
fn request_logging_masking_native_extension(module: &Bound<'_, PyModule>) -> PyResult<()> {
228228
module.add_function(wrap_pyfunction!(mask_sensitive_data, module)?)?;
229229
module.add_function(wrap_pyfunction!(mask_sensitive_headers, module)?)?;
230230
Ok(())

0 commit comments

Comments
 (0)