Python's Native Proxy Pattern
Pythonic way to make a proxy
While making cwgrep, I wanted to log every AWS call the tool made without wrapping each method manually.
Turns out, Python lets you do that cleanly.
__getattr__
This is a Python dunder method that’s only called when: The attribute named name is not found in the usual places (__dict__, class, etc.). So if self.describe_log_groups doesn’t exist on the AWSInterceptor instance, Python calls __getattr__(‘describe_log_groups’). This lets you act as a transparent proxy, forwarding unknown method accesses to the real client.
1import time
2import functools
3
4SENSITIVE_KEYS = {"password", "secret", "token", "key"}
5
6def sanitize(params):
7 def _sanitize(obj):
8 if isinstance(obj, dict):
9 return {k: "***" if k.lower() in SENSITIVE_KEYS else _sanitize(v) for k, v in obj.items()}
10 if isinstance(obj, list) and len(obj) > 10:
11 return f"[{len(obj)} items]"
12 return obj
13 return _sanitize(params)
14
15class AWSInterceptor:
16 def __init__(self, client, logger):
17 self._client = client
18 self._logger = logger
19
20 def __getattr__(self, name):
21 method = getattr(self._client, name)
22 if not callable(method):
23 return method
24
25 @functools.wraps(method)
26 def wrapped(*args, **kwargs):
27 start = time.time()
28 try:
29 result = method(*args, **kwargs)
30 duration = time.time() - start
31 self._logger.log_call(name, duration, success=True, params=sanitize(kwargs))
32 return result
33 except Exception as e:
34 duration = time.time() - start
35 self._logger.log_call(name, duration, success=False, error=str(e), params=sanitize(kwargs))
36 raise
37
38 return wrappedNotice the weird decorator ?
1 @functools.wraps(method)
2 def wrapped(*args, **kwargs):This is classic decorator technique, but applied dynamically. It preserves metadata like __name__, __doc__, and signature, so logs and debuggers still refer to the original method, not wrapped.
Now i only had to do
1real_client = boto3.client("logs")
2proxy = AWSInterceptor(real_client, logger)
3proxy.describe_log_groups()That’s it.