Skip to content

Commit 701f7d4

Browse files
committed
perf: add native json request logging fast path
Signed-off-by: lucarlig <luca.carlig@ibm.com>
1 parent 694eec0 commit 701f7d4

6 files changed

Lines changed: 361 additions & 69 deletions

File tree

mcpgateway/middleware/request_logging_middleware.py

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,24 @@ def mask_sensitive_headers(headers):
289289
return masked_headers
290290

291291

292+
def _mask_json_payload_for_logging(payload: bytes, max_depth: int = 10) -> str:
293+
"""Mask a JSON payload for logging, preferring the native bytes fast path."""
294+
if getattr(settings, "experimental_rust_request_logging_masking_enabled", False) is True:
295+
rust_module = _load_rust_request_logging_module()
296+
if rust_module is not None and hasattr(rust_module, "mask_sensitive_json_bytes"):
297+
try:
298+
masked_payload = rust_module.mask_sensitive_json_bytes(payload, max_depth)
299+
if isinstance(masked_payload, bytes):
300+
return masked_payload.decode("utf-8", errors="ignore")
301+
return str(masked_payload)
302+
except Exception:
303+
pass
304+
305+
json_payload = orjson.loads(payload)
306+
payload_to_log = mask_sensitive_data(json_payload, max_depth)
307+
return orjson.dumps(payload_to_log).decode()
308+
309+
292310
def _load_rust_request_logging_module():
293311
"""Load the experimental Rust masking native extension on demand."""
294312
global _RUST_REQUEST_LOGGING_IMPORT_FAILED, _RUST_REQUEST_LOGGING_MODULE
@@ -630,16 +648,13 @@ async def dispatch(self, request: Request, call_next: Callable):
630648
truncated = False
631649
body_to_log = body
632650

633-
payload = body_to_log.decode("utf-8", errors="ignore").strip()
651+
payload = body_to_log.strip()
634652
if payload:
635653
try:
636-
json_payload = orjson.loads(payload)
637-
payload_to_log = mask_sensitive_data(json_payload)
638-
# Use orjson without indent for performance (compact output)
639-
payload_str = orjson.dumps(payload_to_log).decode()
654+
payload_str = _mask_json_payload_for_logging(payload)
640655
except orjson.JSONDecodeError:
641656
# For non-JSON payloads, still mask potential sensitive data
642-
payload_str = payload
657+
payload_str = payload.decode("utf-8", errors="ignore")
643658
for sensitive_key in SENSITIVE_KEYS:
644659
if sensitive_key in payload_str.lower():
645660
payload_str = "<contains sensitive data - masked>"

tests/performance/test_request_logging_masking_native_extension_benchmark.py

Lines changed: 128 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,125 @@
88
import statistics
99
import subprocess
1010
import time
11+
from dataclasses import dataclass
1112
from pathlib import Path
1213
from typing import Any, Callable
1314

1415
# First-Party
1516
from mcpgateway.config import settings
17+
from mcpgateway.middleware import request_logging_middleware
1618
from mcpgateway.middleware.request_logging_middleware import mask_sensitive_data, mask_sensitive_headers
1719

1820
REPO_ROOT = Path(__file__).resolve().parents[2]
1921
NATIVE_EXTENSION_MANIFEST = REPO_ROOT / "tools_rust" / "request_logging_masking_native_extension" / "Cargo.toml"
2022

2123

24+
@dataclass(frozen=True)
25+
class BenchmarkScenario:
26+
name: str
27+
python_fn: Callable[[Any], Any]
28+
public_native_fn: Callable[[Any], Any]
29+
direct_native_fn: Callable[[Any], Any]
30+
payload: Any
31+
iterations: int
32+
33+
34+
def _build_scenarios(native_extension: Any) -> list[BenchmarkScenario]:
35+
def python_data(payload: Any) -> Any:
36+
settings.experimental_rust_request_logging_masking_enabled = False
37+
return mask_sensitive_data(payload, 12)
38+
39+
def public_native_data(payload: Any) -> Any:
40+
settings.experimental_rust_request_logging_masking_enabled = True
41+
return mask_sensitive_data(payload, 12)
42+
43+
def direct_native_data(payload: Any) -> Any:
44+
return native_extension.mask_sensitive_data(payload, 12)
45+
46+
def python_headers(payload: Any) -> Any:
47+
settings.experimental_rust_request_logging_masking_enabled = False
48+
return mask_sensitive_headers(payload)
49+
50+
def public_native_headers(payload: Any) -> Any:
51+
settings.experimental_rust_request_logging_masking_enabled = True
52+
return mask_sensitive_headers(payload)
53+
54+
def direct_native_headers(payload: Any) -> Any:
55+
return native_extension.mask_sensitive_headers(payload)
56+
57+
return [
58+
BenchmarkScenario(
59+
name="nested_payload_masking",
60+
python_fn=python_data,
61+
public_native_fn=public_native_data,
62+
direct_native_fn=direct_native_data,
63+
payload={
64+
"events": [
65+
{
66+
"actor": {"userName": f"user-{index}", "sessionToken": f"token-{index}", "sessionCount": index},
67+
"request": {
68+
"clientSecret": f"secret-{index}",
69+
"payload": {"safeField": "value" * 8, "authDevice": f"device-{index}", "auth_count": index},
70+
},
71+
}
72+
for index in range(1024)
73+
]
74+
},
75+
iterations=120,
76+
),
77+
BenchmarkScenario(
78+
name="headers_masking",
79+
python_fn=python_headers,
80+
public_native_fn=public_native_headers,
81+
direct_native_fn=direct_native_headers,
82+
payload={
83+
**{f"X-Custom-{index}": f"value-{index}" for index in range(512)},
84+
**{f"X-Api-Key-{index}": f"secret-{index}" for index in range(256)},
85+
"Cookie": "; ".join([f"jwt_token_{index}=abc{index}" for index in range(128)]),
86+
},
87+
iterations=300,
88+
),
89+
]
90+
91+
92+
def test_build_scenarios_exposes_public_and_direct_native_paths(monkeypatch):
93+
native_extension = type(
94+
"NativeExtension",
95+
(),
96+
{
97+
"mask_sensitive_data": staticmethod(lambda payload, max_depth: {"path": "direct", "payload": payload, "max_depth": max_depth}),
98+
"mask_sensitive_headers": staticmethod(lambda payload: {"path": "direct", "payload": payload}),
99+
},
100+
)()
101+
observed_flags: list[bool] = []
102+
103+
def fake_mask_sensitive_data(payload: Any, max_depth: int = 10) -> Any:
104+
observed_flags.append(settings.experimental_rust_request_logging_masking_enabled)
105+
return {"path": "public" if settings.experimental_rust_request_logging_masking_enabled else "python", "max_depth": max_depth}
106+
107+
def fake_mask_sensitive_headers(payload: Any) -> Any:
108+
observed_flags.append(settings.experimental_rust_request_logging_masking_enabled)
109+
return {"path": "public" if settings.experimental_rust_request_logging_masking_enabled else "python"}
110+
111+
monkeypatch.setattr("tests.performance.test_request_logging_masking_native_extension_benchmark.mask_sensitive_data", fake_mask_sensitive_data)
112+
monkeypatch.setattr("tests.performance.test_request_logging_masking_native_extension_benchmark.mask_sensitive_headers", fake_mask_sensitive_headers)
113+
114+
scenarios = {scenario.name: scenario for scenario in _build_scenarios(native_extension)}
115+
116+
public_data = scenarios["nested_payload_masking"].public_native_fn({"k": "v"})
117+
direct_data = scenarios["nested_payload_masking"].direct_native_fn({"k": "v"})
118+
python_data = scenarios["nested_payload_masking"].python_fn({"k": "v"})
119+
public_headers = scenarios["headers_masking"].public_native_fn({"Authorization": "Bearer x"})
120+
direct_headers = scenarios["headers_masking"].direct_native_fn({"Authorization": "Bearer x"})
121+
122+
assert public_data["path"] == "public"
123+
assert direct_data["path"] == "direct"
124+
assert python_data["path"] == "python"
125+
assert public_headers["path"] == "public"
126+
assert direct_headers["path"] == "direct"
127+
assert observed_flags == [True, False, True]
128+
129+
22130
def _ensure_native_extension_installed() -> Any:
23131
subprocess.run(["uv", "run", "maturin", "develop", "--release", "--manifest-path", str(NATIVE_EXTENSION_MANIFEST)], check=True, cwd=REPO_ROOT)
24132
return importlib.import_module("request_logging_masking_native_extension")
@@ -45,74 +153,44 @@ def _assert_parity(python_fn: Callable[[Any], Any], rust_fn: Callable[[Any], Any
45153
raise AssertionError(f"Parity mismatch for payload {payload!r}: python={python_result!r} rust={rust_result!r}")
46154

47155

48-
def main() -> None:
49-
native_extension = _ensure_native_extension_installed()
50-
settings.experimental_rust_request_logging_masking_enabled = False
156+
def _prepare_public_native_path(native_extension: Any) -> None:
157+
settings.experimental_rust_request_logging_masking_enabled = True
158+
request_logging_middleware._RUST_REQUEST_LOGGING_MODULE = native_extension
159+
request_logging_middleware._RUST_REQUEST_LOGGING_IMPORT_FAILED = False
51160

52-
def python_data(payload: Any) -> Any:
53-
return mask_sensitive_data(payload, 12)
54161

55-
def rust_data(payload: Any) -> Any:
56-
return native_extension.mask_sensitive_data(payload, 12)
162+
def main() -> None:
163+
native_extension = _ensure_native_extension_installed()
164+
_prepare_public_native_path(native_extension)
57165

58-
python_headers = mask_sensitive_headers
59-
rust_headers = native_extension.mask_sensitive_headers
166+
scenarios = _build_scenarios(native_extension)
60167

61168
_assert_parity(
62-
python_data,
63-
rust_data,
169+
scenarios[0].python_fn,
170+
scenarios[0].direct_native_fn,
64171
[
65172
{"password": "secret", "nested": {"authToken": "abc", "ok": "value"}},
66173
{"token_count": 3, "tokenizer": "ok", "privateKey": "secret"},
67174
[{"jwt_token": "abc"}, {"normal": "value"}],
68175
],
69176
)
70177
_assert_parity(
71-
python_headers,
72-
rust_headers,
178+
scenarios[1].python_fn,
179+
scenarios[1].direct_native_fn,
73180
[
74181
{"Authorization": "Bearer abc", "Cookie": "jwt_token=abc; theme=dark", "X-Trace-Id": "123"},
75182
{"X-Auth-Count": "5", "X-Api-Key": "secret"},
76183
],
77184
)
78185

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")
186+
for scenario in scenarios:
187+
print(f"\n{scenario.name} ({scenario.iterations} iterations)")
188+
python_median, _ = _measure("python", scenario.python_fn, scenario.payload, scenario.iterations)
189+
public_native_median, _ = _measure("public_native", scenario.public_native_fn, scenario.payload, scenario.iterations)
190+
direct_native_median, _ = _measure("direct_native", scenario.direct_native_fn, scenario.payload, scenario.iterations)
191+
print(f"public_speedup={python_median / public_native_median:.2f}x")
192+
print(f"direct_speedup={python_median / direct_native_median:.2f}x")
193+
print(f"public_overhead={(public_native_median / direct_native_median - 1) * 100:.1f}%")
116194

117195

118196
if __name__ == "__main__":

tests/unit/mcpgateway/middleware/test_request_logging_middleware.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,25 @@ async def test_dispatch_logs_json_body(dummy_logger, mock_structured_logger, dum
264264
assert "******" in dummy_logger.logged[0][1]
265265

266266

267+
@pytest.mark.asyncio
268+
async def test_dispatch_uses_native_json_bytes_fast_path_when_available(dummy_logger, mock_structured_logger, dummy_call_next, monkeypatch):
269+
native_extension = MagicMock()
270+
native_extension.mask_sensitive_json_bytes.return_value = b'{"password":"******","data":"ok"}' # pragma: allowlist secret
271+
monkeypatch.setattr("mcpgateway.middleware.request_logging_middleware.settings.experimental_rust_request_logging_masking_enabled", True, raising=False)
272+
273+
middleware = RequestLoggingMiddleware(app=None, enable_gateway_logging=False, log_detailed_requests=True)
274+
body = orjson.dumps({"password": "123", "data": "ok"})
275+
request = make_request(body=body, headers={"Authorization": "Bearer abc"})
276+
277+
with patch("mcpgateway.middleware.request_logging_middleware._load_rust_request_logging_module", return_value=native_extension):
278+
response = await middleware.dispatch(request, dummy_call_next)
279+
280+
assert response.status_code == 200
281+
native_extension.mask_sensitive_json_bytes.assert_called_once_with(body, 10)
282+
native_extension.mask_sensitive_data.assert_not_called()
283+
assert any('"password":"******"' in msg for _, msg in dummy_logger.logged) # pragma: allowlist secret
284+
285+
267286
@pytest.mark.asyncio
268287
async def test_dispatch_falls_back_to_python_masking_when_native_extension_import_fails(dummy_logger, mock_structured_logger, dummy_call_next, monkeypatch):
269288
monkeypatch.setattr("mcpgateway.middleware.request_logging_middleware.settings.experimental_rust_request_logging_masking_enabled", True, raising=False)

0 commit comments

Comments
 (0)