Skip to content

Commit f54471a

Browse files
author
Bob Bui
committed
feat: add capability to customize extraction of request, response info
#68
1 parent 8c48387 commit f54471a

File tree

11 files changed

+73
-48
lines changed

11 files changed

+73
-48
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ All notable changes to this project will be documented in this file.
44
This project adheres to [Semantic Versioning](http://semver.org/).
55
The format is based on [Keep a Changelog](http://keepachangelog.com/).
66

7+
## 1.4.0rc - 2021-04-09
8+
- add capability to customize extraction of request, response info #68
9+
710
## 1.3.0 - 2021-03-25
811
- add fastapi support #65
912

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,7 @@ json_logging.config_root_logger()
214214
Customer JSON log formatter can be passed to init method. see examples for more detail: [non web](https://github.com/thangbn/json-logging-python/blob/master/example/custom_log_format.py),
215215
[web](https://github.com/thangbn/json-logging-python/blob/master/example/custom_log_format_request.py)
216216

217+
Custom extraction of request and response can be done by subclass **json_logging.RequestResponseDataExtractionBase**
217218

218219

219220
## 2.7 Exclude certain URl from request instrumentation

example/custom_log_format_request.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ class CustomRequestJSONLog(json_logging.JSONLogWebFormatter):
1414

1515
def format(self, record):
1616
json_customized_log_object = ({
17+
"type": "request",
1718
"customized_prop": "customized value",
1819
"correlation_id": json_logging.get_correlation_id(),
1920
})

json_logging/__init__.py

Lines changed: 48 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,46 @@ def init_non_web(*args, **kw):
9898
__init(*args, **kw)
9999

100100

101+
class RequestResponseDataExtractorBase(dict):
102+
"""
103+
class that keep HTTP request information for request instrumentation logging
104+
"""
105+
106+
def __init__(self, request, **kwargs):
107+
super(RequestResponseDataExtractorBase, self).__init__(**kwargs)
108+
109+
def update_response_status(self, response):
110+
pass
111+
112+
113+
class RequestResponseDataExtractor(RequestResponseDataExtractorBase):
114+
"""
115+
default implementation
116+
"""
117+
118+
def __init__(self, request, **kwargs):
119+
super(RequestResponseDataExtractor, self).__init__(request, **kwargs)
120+
utcnow = datetime.utcnow()
121+
self.request_start = utcnow
122+
self.request = request
123+
self.request_received_at = util.iso_time_format(utcnow)
124+
125+
# noinspection PyAttributeOutsideInit
126+
def update_response_status(self, response):
127+
"""
128+
update response information into this object, must be called before invoke request logging statement
129+
:param response:
130+
"""
131+
response_adapter = _request_util.response_adapter
132+
utcnow = datetime.utcnow()
133+
time_delta = utcnow - self.request_start
134+
self.response_time_ms = int(time_delta.total_seconds()) * 1000 + int(time_delta.microseconds / 1000)
135+
self.response_status = response_adapter.get_status_code(response)
136+
self.response_size_b = response_adapter.get_response_size(response)
137+
self.response_content_type = response_adapter.get_content_type(response)
138+
self.response_sent_at = util.iso_time_format(utcnow)
139+
140+
101141
def __init(framework_name=None, custom_formatter=None, enable_json=False):
102142
"""
103143
Initialize JSON logging support, if no **framework_name** passed, logging will be initialized in non-web context.
@@ -153,14 +193,16 @@ def __init(framework_name=None, custom_formatter=None, enable_json=False):
153193
util.update_formatter_for_loggers(existing_loggers, _default_formatter)
154194

155195

156-
def init_request_instrument(app=None, custom_formatter=None, exclude_url_patterns=[]):
196+
def init_request_instrument(app=None, custom_formatter=None, exclude_url_patterns=[],
197+
request_response_data_extractor_class=RequestResponseDataExtractor):
157198
"""
158199
Configure the request instrumentation logging configuration for given web app. Must be called after init method
159200
160201
If **custom_formatter** is passed, it will use this formatter over the default.
161202
162203
:param app: current web application instance
163204
:param custom_formatter: formatter to override default JSONRequestLogFormatter.
205+
:param request_response_data_extractor_class: requestinfo to override default json_logging.RequestInfo.
164206
"""
165207

166208
if _current_framework is None or _current_framework == '-':
@@ -170,8 +212,11 @@ def init_request_instrument(app=None, custom_formatter=None, exclude_url_pattern
170212
if not issubclass(custom_formatter, logging.Formatter):
171213
raise ValueError('custom_formatter is not subclass of logging.Formatter', custom_formatter)
172214

215+
if not issubclass(request_response_data_extractor_class, RequestResponseDataExtractorBase):
216+
raise ValueError('request_response_data_extractor_class is not subclass of json_logging.RequestInfoBase', custom_formatter)
217+
173218
configurator = _current_framework['app_request_instrumentation_configurator']()
174-
configurator.config(app, exclude_url_patterns=exclude_url_patterns)
219+
configurator.config(app, request_response_data_extractor_class, exclude_url_patterns=exclude_url_patterns)
175220

176221
formatter = custom_formatter if custom_formatter else JSONRequestLogFormatter
177222
request_logger = configurator.request_logger
@@ -182,8 +227,6 @@ def init_request_instrument(app=None, custom_formatter=None, exclude_url_pattern
182227

183228

184229
def get_request_logger():
185-
global _request_logger
186-
187230
if _current_framework is None or _current_framework == '-':
188231
raise RuntimeError(
189232
"request_logger is only available if json_logging is inited with a web app, "
@@ -196,34 +239,6 @@ def get_request_logger():
196239
return instance.request_logger
197240

198241

199-
class RequestInfo(dict):
200-
"""
201-
class that keep HTTP request information for request instrumentation logging
202-
"""
203-
204-
def __init__(self, request, **kwargs):
205-
super(RequestInfo, self).__init__(**kwargs)
206-
utcnow = datetime.utcnow()
207-
self.request_start = utcnow
208-
self.request = request
209-
self.request_received_at = util.iso_time_format(utcnow)
210-
211-
# noinspection PyAttributeOutsideInit
212-
def update_response_status(self, response):
213-
"""
214-
update response information into this object, must be called before invoke request logging statement
215-
:param response:
216-
"""
217-
response_adapter = _request_util.response_adapter
218-
utcnow = datetime.utcnow()
219-
time_delta = utcnow - self.request_start
220-
self.response_time_ms = int(time_delta.total_seconds()) * 1000 + int(time_delta.microseconds / 1000)
221-
self.response_status = response_adapter.get_status_code(response)
222-
self.response_size_b = response_adapter.get_response_size(response)
223-
self.response_content_type = response_adapter.get_content_type(response)
224-
self.response_sent_at = util.iso_time_format(utcnow)
225-
226-
227242
class BaseJSONFormatter(logging.Formatter):
228243
"""
229244
Base class for JSON formatters
@@ -406,5 +421,6 @@ def init_connexion(custom_formatter=None, enable_json=False):
406421
request_adapter_class=fastapi_support.FastAPIRequestAdapter,
407422
response_adapter_class=fastapi_support.FastAPIResponseAdapter)
408423

424+
409425
def init_fastapi(custom_formatter=None, enable_json=False):
410426
__init(framework_name='fastapi', custom_formatter=custom_formatter, enable_json=enable_json)

json_logging/framework/connexion/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ def is_connexion_present():
3131

3232

3333
class ConnexionAppRequestInstrumentationConfigurator(AppRequestInstrumentationConfigurator):
34-
def config(self, app, exclude_url_patterns=[]):
34+
def config(self, app, request_response_data_extractor_class, exclude_url_patterns=[]):
3535
if not is_connexion_present():
3636
raise RuntimeError("connexion is not available in system runtime")
3737
from flask.app import Flask
@@ -51,7 +51,7 @@ def config(self, app, exclude_url_patterns=[]):
5151
@app.app.before_request
5252
def before_request():
5353
if is_not_match_any_pattern(_current_request.path, exclude_url_patterns):
54-
g.request_info = json_logging.RequestInfo(_current_request)
54+
g.request_info = request_response_data_extractor_class(_current_request)
5555

5656
@app.app.after_request
5757
def after_request(response):

json_logging/framework/fastapi/implementation.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
from starlette.responses import Response
1616
from starlette.types import ASGIApp
1717

18+
_request_config_class = None
19+
1820

1921
class JSONLoggingASGIMiddleware(BaseHTTPMiddleware):
2022
def __init__(self, app: ASGIApp, exclude_url_patterns=tuple()) -> None:
@@ -29,7 +31,7 @@ async def dispatch(self, request: Request, call_next: RequestResponseEndpoint) -
2931
if not log_request:
3032
return await call_next(request)
3133

32-
request_info = json_logging.RequestInfo(request)
34+
request_info = _request_config_class(request)
3335
response = await call_next(request)
3436
request_info.update_response_status(response)
3537
self.request_logger.info(
@@ -39,10 +41,13 @@ async def dispatch(self, request: Request, call_next: RequestResponseEndpoint) -
3941

4042

4143
class FastAPIAppRequestInstrumentationConfigurator(AppRequestInstrumentationConfigurator):
42-
def config(self, app, exclude_url_patterns=tuple()):
44+
def config(self, app, request_response_data_extractor_class, exclude_url_patterns=[]):
4345
if not isinstance(app, fastapi.FastAPI):
4446
raise RuntimeError("app is not a valid fastapi.FastAPI instance")
4547

48+
global _request_config_class
49+
_request_config_class = request_response_data_extractor_class
50+
4651
# Disable standard logging
4752
logging.getLogger('uvicorn.access').disabled = True
4853

json_logging/framework/flask/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ def is_flask_present():
2626

2727

2828
class FlaskAppRequestInstrumentationConfigurator(AppRequestInstrumentationConfigurator):
29-
def config(self, app, exclude_url_patterns=[]):
29+
def config(self, app, request_response_data_extractor_class, exclude_url_patterns=[]):
3030
if not is_flask_present():
3131
raise RuntimeError("flask is not available in system runtime")
3232
from flask.app import Flask
@@ -47,7 +47,7 @@ def config(self, app, exclude_url_patterns=[]):
4747
@app.before_request
4848
def before_request():
4949
if is_not_match_any_pattern(_current_request.path, exclude_url_patterns):
50-
g.request_info = json_logging.RequestInfo(_current_request)
50+
g.request_info = request_response_data_extractor_class(_current_request)
5151

5252
@app.after_request
5353
def after_request(response):

json_logging/framework/quart/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ def is_quart_present():
2727

2828

2929
class QuartAppRequestInstrumentationConfigurator(AppRequestInstrumentationConfigurator):
30-
def config(self, app, exclude_url_patterns=[]):
30+
def config(self, app, request_response_data_extractor_class, exclude_url_patterns=[]):
3131
if not is_quart_present():
3232
raise RuntimeError("quart is not available in system runtime")
3333
from quart.app import Quart
@@ -51,7 +51,7 @@ def config(self, app, exclude_url_patterns=[]):
5151
@app.before_request
5252
def before_request():
5353
if is_not_match_any_pattern(_current_request.path, exclude_url_patterns):
54-
g.request_info = json_logging.RequestInfo(_current_request)
54+
g.request_info = request_response_data_extractor_class(_current_request)
5555

5656
@app.after_request
5757
def after_request(response):

json_logging/framework/sanic/__init__.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ def config(self):
5050

5151

5252
class SanicAppRequestInstrumentationConfigurator(AppRequestInstrumentationConfigurator):
53-
def config(self, app, exclude_url_patterns=[]):
53+
def config(self, app, request_response_data_extractor_class, exclude_url_patterns=[]):
5454
if not is_sanic_present():
5555
raise RuntimeError("Sanic is not available in system runtime")
5656
# noinspection PyPackageRequirements
@@ -67,7 +67,7 @@ def config(self, app, exclude_url_patterns=[]):
6767
@app.middleware("request")
6868
def before_request(request):
6969
if is_not_match_any_pattern(request.path, exclude_url_patterns):
70-
request.ctx.request_info = json_logging.RequestInfo(request)
70+
request.ctx.request_info = request_response_data_extractor_class(request)
7171

7272
@app.middleware("response")
7373
def after_request(request, response):
@@ -78,9 +78,6 @@ def after_request(request, response):
7878
"", extra={"request_info": request_info, "type": "request"}
7979
)
8080

81-
def get_request_logger(self):
82-
return self.request_logger
83-
8481

8582
class SanicRequestAdapter(RequestAdapter):
8683
@staticmethod

json_logging/framework_base.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -189,11 +189,13 @@ def __new__(cls, *args, **kw):
189189
cls._instance.request_logger = None
190190
return cls._instance
191191

192-
def config(self, app, exclude_url_patterns=None):
192+
def config(self, app, request_response_data_extractor_class, exclude_url_patterns=[]):
193193
"""
194194
configuration logic
195195
196-
:param app:
196+
:param app: web application object instance
197+
:param request_response_data_extractor_class: request info class
198+
:param exclude_url_patterns: list of URL regex pattern to be excluded from request logging
197199
"""
198200
raise NotImplementedError
199201

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
setup(
1414
name="json-logging",
15-
version='1.3.0',
15+
version='1.4.0-rc',
1616
packages=find_packages(exclude=['contrib', 'docs', 'tests*', 'example', 'dist', 'build']),
1717
license='Apache License 2.0',
1818
description="JSON Python Logging",

0 commit comments

Comments
 (0)