Skip to content

Commit fa5cd06

Browse files
authored
Merge pull request bobbui#3 from Darkless012/quart-framework-support
added support for quart framework
2 parents 73de414 + 99d7417 commit fa5cd06

File tree

5 files changed

+234
-16
lines changed

5 files changed

+234
-16
lines changed

README.md

Lines changed: 36 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,19 +17,19 @@ If you're using Cloud Foundry, it worth to check out the library [SAP/cf-python-
1717
7. [References](#7-references)
1818

1919
# 1. Features
20-
1. Emit JSON logs ([format detail](#0-full-logging-format-references))
20+
1. Emit JSON logs ([format detail](#0-full-logging-format-references))
2121
2. Support **correlation-id** [\[1\]](#1-what-is-correlation-idrequest-id)
2222
3. Lightweight, no dependencies, minimal configuration needed (1 LoC to get it working)
2323
4. Fully compatible with Python **logging** module. Support both Python 2.7.x and 3.x
24-
5. Support HTTP request instrumentation. Built in support for [Flask](http://flask.pocoo.org/) & [Sanic](https://github.com/channelcat/sanic). Extensible to support other web frameworks. PR welcome :smiley: .
24+
5. Support HTTP request instrumentation. Built in support for [Flask](http://flask.pocoo.org/) & [Sanic](https://github.com/channelcat/sanic) & [Quart](https://gitlab.com/pgjones/quart). Extensible to support other web frameworks. PR welcome :smiley: .
2525
6. Support inject arbitrary extra properties to JSON log message.
2626

2727
# 2. Usage
2828
Install by running this command:
2929
> pip install json-logging
3030
3131
By default log will be emitted in normal format to ease the local development. To enable it on production set either **json_logging.ENABLE_JSON_LOGGING** or **ENABLE_JSON_LOGGING environment variable** to true.
32-
32+
3333
To configure, call **json_logging.init(framework_name)**. Once configured library will try to configure all loggers (existing and newly created) to emit log in JSON format.
3434
See following use cases for more detail.
3535

@@ -99,6 +99,32 @@ if __name__ == "__main__":
9999
app.run(host="0.0.0.0", port=8000)
100100
```
101101

102+
### Quart
103+
104+
```python
105+
import asyncio, logging, sys, json_logging, quart
106+
107+
app = quart.Quart(__name__)
108+
json_logging.ENABLE_JSON_LOGGING = True
109+
json_logging.init(framework_name='quart')
110+
json_logging.init_request_instrument(app)
111+
112+
# init the logger as usual
113+
logger = logging.getLogger("test logger")
114+
logger.setLevel(logging.DEBUG)
115+
logger.addHandler(logging.StreamHandler(sys.stdout))
116+
117+
@app.route('/')
118+
async def home():
119+
logger.info("test log statement")
120+
logger.info("test log statement", extra={'props': {"extra_property": 'extra_value'}})
121+
return "Hello world"
122+
123+
if __name__ == "__main__":
124+
loop = asyncio.get_event_loop()
125+
app.run(host='0.0.0.0', port=int(5000), use_reloader=False, loop=loop)
126+
```
127+
102128
## 2.3 Get current correlation-id
103129
Current request correlation-id can be retrieved and pass to downstream services call as follow:
104130

@@ -223,16 +249,16 @@ e.g.:
223249
}
224250
```
225251
See following tables for detail format explanation:
226-
- Common field
252+
- Common field
227253

228254
Field | Description | Format | Example
229255
--- | --- | --- | ---
230-
written_at | The date when this log message was written. | ISO 8601 YYYY-MM-DDTHH:MM:SS.milliZ | 2017-12-23T15:14:02.208Z
256+
written_at | The date when this log message was written. | ISO 8601 YYYY-MM-DDTHH:MM:SS.milliZ | 2017-12-23T15:14:02.208Z
231257
written_ts | The timestamp in nano-second precision when this request metric message was written. | long number | 1456820553816849408
232258
correlation_id | The timestamp in nano-second precision when this request metric message was written. | string | db2d002e-2702-41ec-66f5-c002a80a3d3f
233-
type | Type of logging. "logs" or "request" | string |
259+
type | Type of logging. "logs" or "request" | string |
234260
component_id | Uniquely identifies the software component that has processed the current request | string | 9e6f3ecf-def0-4baf-8fac-9339e61d5645
235-
component_name | A human-friendly name representing the software component | string | my-fancy-component
261+
component_name | A human-friendly name representing the software component | string | my-fancy-component
236262
component_instance | Instance's index of horizontally scaled service | string | 0
237263

238264
- application logs
@@ -245,7 +271,7 @@ thread | Identifies the execution thread in which this log message has been writ
245271
logger | The logger name that emits the log message.
246272
| string | requests-logger
247273

248-
- request logs:
274+
- request logs:
249275

250276
Field | Description | Format | Example
251277
--- | --- | --- | ---
@@ -267,7 +293,7 @@ referer | For HTTP requests, identifies the address of the webpage (i.e. the URI
267293
x_forwarded_for | Comma-separated list of IP addresses, the left-most being the original client, followed by proxy server addresses that forwarded the client request. | string | 192.0.2.60,10.12.9.23
268294

269295
## [1] What is correlation-id/request id
270-
https://stackoverflow.com/questions/25433258/what-is-the-x-request-id-http-header
296+
https://stackoverflow.com/questions/25433258/what-is-the-x-request-id-http-header
271297
## [2] Python logging propagate
272298
https://docs.python.org/3/library/logging.html#logging.Logger.propagate
273299
https://docs.python.org/2/library/logging.html#logging.Logger.propagate
@@ -303,7 +329,7 @@ python -m pip install --index-url https://test.pypi.org/simple/ json_logging
303329
304330
python setup.py sdist upload -r pypitest
305331
python setup.py bdist_wheel --universal upload -r pypitest
306-
pip3 install json_logging --index-url https://test.pypi.org/simple/
332+
pip3 install json_logging --index-url https://test.pypi.org/simple/
307333
```
308334
pypi
309335
```

README.rst

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ searchable by logging infrastructure such as
77

88
| If you’re using Cloud Foundry, it worth to check out the library
99
`SAP/cf-python-logging-support <https://github.com/SAP/cf-python-logging-support>`_
10-
which I’m also original author and contributor.
10+
which I’m also original author and contributor.
1111
1212
1. Features
1313
===========
@@ -22,14 +22,15 @@ searchable by logging infrastructure such as
2222
2.7.x and 3.x
2323
5. Support HTTP request instrumentation. Built in support for
2424
`Flask <http://flask.pocoo.org/>`_ &
25-
`Sanic <https://github.com/channelcat/sanic>`_. Extensible to support other web
25+
`Sanic <https://github.com/channelcat/sanic>`_ &
26+
`Quart <https://gitlab.com/pgjones/quart>`_. Extensible to support other web
2627
frameworks. PR welcome :smiley: .
2728
6. Support inject arbitrary extra properties to JSON log message.
2829

2930
2. Usage
3031
========
3132

32-
Install by running this command:
33+
Install by running this command:
3334

3435
.. code:: python
3536
@@ -123,6 +124,33 @@ Sanic
123124
if __name__ == "__main__":
124125
app.run(host="0.0.0.0", port=8000)
125126
127+
Quart
128+
~~~~~
129+
130+
.. code:: python
131+
132+
import asyncio, logging, sys, json_logging, quart
133+
134+
app = quart.Quart(__name__)
135+
json_logging.ENABLE_JSON_LOGGING = True
136+
json_logging.init(framework_name='quart')
137+
json_logging.init_request_instrument(app)
138+
139+
# init the logger as usual
140+
logger = logging.getLogger("test logger")
141+
logger.setLevel(logging.DEBUG)
142+
logger.addHandler(logging.StreamHandler(sys.stdout))
143+
144+
@app.route('/')
145+
async def home():
146+
logger.info("test log statement")
147+
logger.info("test log statement", extra={'props': {"extra_property": 'extra_value'}})
148+
return "Hello world"
149+
150+
if __name__ == "__main__":
151+
loop = asyncio.get_event_loop()
152+
app.run(host='0.0.0.0', port=int(5000), use_reloader=False, loop=loop)
153+
126154
127155
2.3 Get current correlation-id
128156
------------------------------
@@ -317,7 +345,7 @@ you can install Sanic on windows by running these commands:
317345
[0] Full logging format references
318346
----------------------------------
319347

320-
2 types of logging statement will be emitted by this library:
348+
2 types of logging statement will be emitted by this library:
321349
- Application log: normal logging statement e.g.:
322350

323351
::
@@ -368,7 +396,7 @@ you can install Sanic on windows by running these commands:
368396
"response_sent_at": "2017-12-23T16:55:37.280Z"
369397
}
370398

371-
See following tables for detail format explanation:
399+
See following tables for detail format explanation:
372400

373401
- Common field
374402

@@ -589,4 +617,4 @@ https://docs.python.org/2/library/logging.html#logging.Logger.propagate
589617
[3] more on flask use_reloader
590618
------------------------------
591619

592-
http://flask.pocoo.org/docs/0.12/errorhandling/#working-with-debuggers
620+
http://flask.pocoo.org/docs/0.12/errorhandling/#working-with-debuggers

example/quart_sample_app.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import asyncio
2+
import logging
3+
import sys
4+
5+
import quart
6+
7+
import json_logging
8+
9+
app = quart.Quart(__name__)
10+
json_logging.ENABLE_JSON_LOGGING = True
11+
json_logging.init(framework_name='quart')
12+
json_logging.init_request_instrument(app)
13+
14+
# init the logger as usual
15+
logger = logging.getLogger("test logger")
16+
logger.setLevel(logging.DEBUG)
17+
logger.addHandler(logging.StreamHandler(sys.stdout))
18+
19+
20+
@app.route('/')
21+
async def home():
22+
logger.info("test log statement")
23+
logger.info("test log statement", extra={'props': {"extra_property": 'extra_value'}})
24+
return "Hello world"
25+
26+
27+
if __name__ == "__main__":
28+
loop = asyncio.get_event_loop()
29+
app.run(host='0.0.0.0', port=int(5000), use_reloader=False, loop=loop)

json_logging/__init__.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,3 +292,11 @@ def format(self, record):
292292
SanicAppRequestInstrumentationConfigurator,
293293
SanicRequestAdapter,
294294
SanicResponseAdapter)
295+
296+
# register quart support
297+
# noinspection PyPep8
298+
import json_logging.framework.quart as quart_support
299+
300+
register_framework_support('quart', None, quart_support.QuartAppRequestInstrumentationConfigurator,
301+
quart_support.QuartRequestAdapter,
302+
quart_support.QuartResponseAdapter)
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
# coding=utf-8
2+
import logging
3+
import sys
4+
5+
import json_logging
6+
import json_logging.framework
7+
from json_logging import JSONLogWebFormatter
8+
from json_logging.framework_base import AppRequestInstrumentationConfigurator, RequestAdapter, ResponseAdapter
9+
10+
11+
def is_quart_present():
12+
# noinspection PyPep8,PyBroadException
13+
try:
14+
import quart
15+
return True
16+
except:
17+
return False
18+
19+
20+
if is_quart_present():
21+
from quart import request as request_obj
22+
import quart as quart
23+
24+
_current_request = request_obj
25+
_quart = quart
26+
27+
28+
class QuartAppRequestInstrumentationConfigurator(AppRequestInstrumentationConfigurator):
29+
def config(self, app):
30+
if not is_quart_present():
31+
raise RuntimeError("quart is not available in system runtime")
32+
from quart.app import Quart
33+
if not isinstance(app, Quart):
34+
raise RuntimeError("app is not a valid quart.app.Quart app instance")
35+
36+
# Remove quart logging handlers
37+
from quart.logging import default_handler, serving_handler
38+
logging.getLogger('quart.app').removeHandler(default_handler)
39+
logging.getLogger('quart.serving').removeHandler(serving_handler)
40+
41+
42+
json_logging.util.use_cf_logging_formatter([
43+
# logging.getLogger('quart.app'),
44+
# logging.getLogger('quart.serving'),
45+
], JSONLogWebFormatter)
46+
47+
# noinspection PyAttributeOutsideInit
48+
self.request_logger = logging.getLogger('quart.app')
49+
self.request_logger.setLevel(logging.DEBUG)
50+
self.request_logger.addHandler(logging.StreamHandler(sys.stdout))
51+
52+
from quart import g
53+
54+
@app.before_request
55+
def before_request():
56+
g.request_info = json_logging.RequestInfo(_current_request)
57+
58+
@app.after_request
59+
def after_request(response):
60+
request_info = g.request_info
61+
request_info.update_response_status(response)
62+
# TODO:handle to print out request instrumentation in non-JSON mode
63+
self.request_logger.info("", extra={'request_info': request_info})
64+
return response
65+
66+
67+
class QuartRequestAdapter(RequestAdapter):
68+
@staticmethod
69+
def get_request_class_type():
70+
raise NotImplementedError
71+
72+
@staticmethod
73+
def support_global_request_object():
74+
return True
75+
76+
@staticmethod
77+
def get_current_request():
78+
return _current_request
79+
80+
def get_remote_user(self, request):
81+
if request.authorization is not None:
82+
return request.authorization.username
83+
else:
84+
return json_logging.EMPTY_VALUE
85+
86+
def is_in_request_context(self, request_):
87+
return _quart.has_request_context()
88+
89+
def get_http_header(self, request, header_name, default=None):
90+
if header_name in request.headers:
91+
return request.headers.get(header_name)
92+
return default
93+
94+
def set_correlation_id(self, request_, value):
95+
_quart.g.correlation_id = value
96+
97+
def get_correlation_id_in_request_context(self, request):
98+
return _quart.g.get('correlation_id', None)
99+
100+
def get_protocol(self, request):
101+
return request.scheme
102+
103+
def get_path(self, request):
104+
return request.path
105+
106+
def get_content_length(self, request):
107+
return request.content_length
108+
109+
def get_method(self, request):
110+
return request.method
111+
112+
def get_remote_ip(self, request):
113+
return request.remote_addr
114+
115+
def get_remote_port(self, request):
116+
return request.host.split(":", 2)[1]
117+
118+
119+
class QuartResponseAdapter(ResponseAdapter):
120+
def get_status_code(self, response):
121+
return response.status_code
122+
123+
def get_response_size(self, response):
124+
return response.content_length
125+
126+
def get_content_type(self, response):
127+
return response.content_type

0 commit comments

Comments
 (0)