Skip to content

Commit 760da31

Browse files
committed
feat: add rust request logging masking sidecar
Signed-off-by: lucarlig <luca.carlig@ibm.com>
1 parent 0d8bba9 commit 760da31

7 files changed

Lines changed: 618 additions & 62 deletions

File tree

mcpgateway/config.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -350,6 +350,10 @@ class Settings(BaseSettings):
350350

351351
# Security Validation & Sanitization
352352
experimental_validate_io: bool = Field(default=False, description="Enable experimental input validation and output sanitization")
353+
experimental_rust_request_logging_masking_enabled: bool = Field(
354+
default=False,
355+
description="Enable experimental Rust sidecar for request logging sensitive-data masking",
356+
)
353357
validation_middleware_enabled: bool = Field(default=False, description="Enable validation middleware for all requests")
354358
validation_strict: bool = Field(default=True, description="Strict validation mode - reject on violations")
355359
sanitize_output: bool = Field(default=True, description="Sanitize output to remove control characters")

mcpgateway/middleware/request_logging_middleware.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
"""
4747

4848
# Standard
49+
import importlib
4950
import logging
5051
import re
5152
import secrets
@@ -74,6 +75,8 @@
7475
# Initialize structured logger for gateway boundary logging
7576
structured_logger = get_structured_logger("http_gateway")
7677

78+
_RUST_REQUEST_LOGGING_MODULE = None
79+
7780
SENSITIVE_KEYS = frozenset(
7881
{
7982
"password",
@@ -183,6 +186,9 @@ def mask_sensitive_data(data, max_depth: int = 10):
183186
>>> mask_sensitive_data({"level": {"nested": {}}}, max_depth=1)
184187
{'level': '<nested too deep>'}
185188
"""
189+
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+
186192
if max_depth <= 0:
187193
return "<nested too deep>"
188194

@@ -262,6 +268,9 @@ def mask_sensitive_headers(headers):
262268
>>> "******" in result["Cookie"]
263269
True
264270
"""
271+
if getattr(settings, "experimental_rust_request_logging_masking_enabled", False) is True:
272+
return _load_rust_request_logging_module().mask_sensitive_headers(headers)
273+
265274
masked_headers = {}
266275
for key, value in headers.items():
267276
key_lower = key.lower()
@@ -275,6 +284,15 @@ def mask_sensitive_headers(headers):
275284
return masked_headers
276285

277286

287+
def _load_rust_request_logging_module():
288+
"""Load the experimental Rust masking sidecar on demand."""
289+
global _RUST_REQUEST_LOGGING_MODULE
290+
291+
if _RUST_REQUEST_LOGGING_MODULE is None:
292+
_RUST_REQUEST_LOGGING_MODULE = importlib.import_module("request_logging_masking_sidecar")
293+
return _RUST_REQUEST_LOGGING_MODULE
294+
295+
278296
class RequestLoggingMiddleware(BaseHTTPMiddleware):
279297
"""Middleware for logging HTTP requests with sensitive data masking.
280298
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
# -*- coding: utf-8 -*-
2+
"""Benchmark the request-logging masking Rust sidecar against the Python path."""
3+
4+
# Standard
5+
from __future__ import annotations
6+
7+
import importlib
8+
import statistics
9+
import subprocess
10+
import time
11+
from pathlib import Path
12+
from typing import Any, Callable
13+
14+
# First-Party
15+
from mcpgateway.config import settings
16+
from mcpgateway.middleware.request_logging_middleware import mask_sensitive_data, mask_sensitive_headers
17+
18+
REPO_ROOT = Path(__file__).resolve().parents[2]
19+
SIDECAR_MANIFEST = REPO_ROOT / "tools_rust" / "request_logging_masking_sidecar" / "Cargo.toml"
20+
21+
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")
25+
26+
27+
def _measure(label: str, fn: Callable[[Any], Any], payload: Any, iterations: int) -> tuple[float, float]:
28+
samples = []
29+
for _ in range(iterations):
30+
started = time.perf_counter_ns()
31+
fn(payload)
32+
samples.append(time.perf_counter_ns() - started)
33+
34+
median_ms = statistics.median(samples) / 1_000_000
35+
p95_ms = statistics.quantiles(samples, n=100)[94] / 1_000_000
36+
print(f"{label}: median={median_ms:.3f}ms p95={p95_ms:.3f}ms")
37+
return median_ms, p95_ms
38+
39+
40+
def _assert_parity(python_fn: Callable[[Any], Any], rust_fn: Callable[[Any], Any], payloads: list[Any]) -> None:
41+
for payload in payloads:
42+
python_result = python_fn(payload)
43+
rust_result = rust_fn(payload)
44+
if python_result != rust_result:
45+
raise AssertionError(f"Parity mismatch for payload {payload!r}: python={python_result!r} rust={rust_result!r}")
46+
47+
48+
def main() -> None:
49+
sidecar = _ensure_sidecar_installed()
50+
settings.experimental_rust_request_logging_masking_enabled = False
51+
52+
def python_data(payload: Any) -> Any:
53+
return mask_sensitive_data(payload, 12)
54+
55+
def rust_data(payload: Any) -> Any:
56+
return sidecar.mask_sensitive_data(payload, 12)
57+
58+
python_headers = mask_sensitive_headers
59+
rust_headers = sidecar.mask_sensitive_headers
60+
61+
_assert_parity(
62+
python_data,
63+
rust_data,
64+
[
65+
{"password": "secret", "nested": {"authToken": "abc", "ok": "value"}},
66+
{"token_count": 3, "tokenizer": "ok", "privateKey": "secret"},
67+
[{"jwt_token": "abc"}, {"normal": "value"}],
68+
],
69+
)
70+
_assert_parity(
71+
python_headers,
72+
rust_headers,
73+
[
74+
{"Authorization": "Bearer abc", "Cookie": "jwt_token=abc; theme=dark", "X-Trace-Id": "123"},
75+
{"X-Auth-Count": "5", "X-Api-Key": "secret"},
76+
],
77+
)
78+
79+
scenarios = [
80+
(
81+
"nested_payload_masking",
82+
python_data,
83+
rust_data,
84+
{
85+
"events": [
86+
{
87+
"actor": {"userName": f"user-{index}", "sessionToken": f"token-{index}", "sessionCount": index},
88+
"request": {
89+
"clientSecret": f"secret-{index}",
90+
"payload": {"safeField": "value" * 8, "authDevice": f"device-{index}", "auth_count": index},
91+
},
92+
}
93+
for index in range(1024)
94+
]
95+
},
96+
120,
97+
),
98+
(
99+
"headers_masking",
100+
python_headers,
101+
rust_headers,
102+
{
103+
**{f"X-Custom-{index}": f"value-{index}" for index in range(512)},
104+
**{f"X-Api-Key-{index}": f"secret-{index}" for index in range(256)},
105+
"Cookie": "; ".join([f"jwt_token_{index}=abc{index}" for index in range(128)]),
106+
},
107+
300,
108+
),
109+
]
110+
111+
for name, python_fn, rust_fn, payload, iterations in scenarios:
112+
print(f"\n{name} ({iterations} iterations)")
113+
python_median, _ = _measure("python", python_fn, payload, iterations)
114+
rust_median, _ = _measure("rust", rust_fn, payload, iterations)
115+
print(f"speedup={python_median / rust_median:.2f}x")
116+
117+
118+
if __name__ == "__main__":
119+
main()

0 commit comments

Comments
 (0)