Skip to content

Commit 5c3efca

Browse files
authored
Merge pull request #37 from geoadmin/feat-BGDIINF_SB-2693-add-option-to-ignore-type-errors
BGDIINF_SB-2693: Added Attribute Type Filter - #major
2 parents b5dcf63 + 887eef2 commit 5c3efca

File tree

6 files changed

+423
-49
lines changed

6 files changed

+423
-49
lines changed

README.md

Lines changed: 84 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ All features can be fully configured from the configuration file.
4444
- [Usage](#usage)
4545
- [Django Request Filter Constructor](#django-request-filter-constructor)
4646
- [Django Request Config Example](#django-request-config-example)
47+
- [Filter out LogRecord attributes based on their types](#filter-out-logrecord-attributes-based-on-their-types)
48+
- [Attribute Type Filter Constructor](#attribute-type-filter-constructor)
49+
- [Attribute Type Filter Config Example](#attribute-type-filter-config-example)
4750
- [ISO Time with Timezone](#iso-time-with-timezone)
4851
- [ISO Time Filter Constructor](#iso-time-filter-constructor)
4952
- [ISO Time Config Example](#iso-time-config-example)
@@ -62,6 +65,7 @@ All features can be fully configured from the configuration file.
6265
- [Case 7. Add all Log Extra as Dictionary to the Standard Formatter (including Django log extra)](#case-7-add-all-log-extra-as-dictionary-to-the-standard-formatter-including-django-log-extra)
6366
- [Case 8. Add Specific Log Extra to the Standard Formatter](#case-8-add-specific-log-extra-to-the-standard-formatter)
6467
- [Breaking Changes](#breaking-changes)
68+
- [Version 4.x.x Breaking Changes](#version-4xx-breaking-changes)
6569
- [Version 3.x.x Breaking Changes](#version-3xx-breaking-changes)
6670
- [Version 2.x.x Breaking Changes](#version-2xx-breaking-changes)
6771
- [Credits](#credits)
@@ -460,42 +464,77 @@ handlers:
460464

461465
## Jsonify Django Request
462466

463-
If you want to log the [Django](https://www.djangoproject.com/) [HttpRequest](https://docs.djangoproject.com/en/3.1/ref/request-response/#httprequest-objects) object using the [JSON Formatter](#json-formatter), this filter is for made for you. It converts the `record.request` attribute to a valid json object or a string if the attribute is not an `HttpRequest` instance. It is also useful when using Django with the JSON Formatter because Django adds in some of its logs either an HttpRequest object to the log extra or a socket object.
467+
If you want to log the [Django](https://www.djangoproject.com/) [HttpRequest](https://docs.djangoproject.com/en/3.1/ref/request-response/#httprequest-objects) object using the [JSON Formatter](#json-formatter), this filter is for made for you. It converts the `record.http_request` attribute (or the attribute specified by `attr_key` in the constructor) to a valid json object if it is of type `HttpRequest`.
464468

465469
The `HttpRequest` attributes that are converted can be configured using the `include_keys` and/or `exclude_keys` filter parameters. This can be useful if you want to limit the log data, for example if you don't want to log Authentication headers.
466470

471+
:warning: The django framework adds sometimes an HttpRequest or socket object under `record.request` when
472+
logging. So if you decide to use the attribute name `request` for this filter, beware that you
473+
will need to handle the case where the attribute is of type `socket` separately, for example by
474+
filtering it out using the attribute type filter. (see example [Filter out LogRecord attributes based on their types](#filter-out-logrecord-attributes-based-on-their-types))
475+
467476
### Usage
468477

469478
Add the filter to the log handler and then add simply the `HttpRequest` to the log extra as follow:
470479

471480
```python
472-
logger.info('My message', extra={'request': request})
481+
logger.info('My message', extra={'http_request': request})
473482
```
474483

475484
### Django Request Filter Constructor
476485

477486
| Parameter | Type | Default | Description |
478487
|----------------|------|---------|------------------------------------------------|
479-
| `include_keys` | list | None | All request attributes that match any of the dotted keys of the list will be jsonify in the `record.request`. When `None` then all attributes are added (default behavior). |
480-
| `exclude_keys` | list | None | All request attributes that match any of the dotted keys of the list will not be added to the jsonify of the `record.request`. **NOTE** this has precedence to `include_keys` which means that if a key is in both list, then it is not added. |
488+
| `include_keys` | list | None | All request attributes that match any of the dotted keys of the list will be added to the jsonifiable object. When `None` then all attributes are added (default behavior). |
489+
| `exclude_keys` | list | None | All request attributes that match any of the dotted keys of the list will not be added to the jsonifiable object. **NOTE** this has precedence to `include_keys` which means that if a key is in both lists, then it is not added. |
490+
| `attr_key` | str | `http_request` | The name of the attribute that stores the HttpRequest object. It will be replaced in place by a jsonifiable dict representing this object. (Note that django sometimes stores an `HttpRequest` under `attr_key: request`. This is however not the default as django also stores other types of objects under this attribute name.)
481491

482492
### Django Request Config Example
483493

484494
```yaml
485495
filters:
486496
django:
487497
(): logging_utilities.filters.django_request.JsonDjangoRequest
498+
attr_key: 'http_request' # This is the default, so it can be omitted
488499
include_keys:
489-
- request.META.REQUEST_METHOD
490-
- request.META.SERVER_NAME
491-
- request.environ
500+
- http_request.META.REQUEST_METHOD
501+
- http_request.META.SERVER_NAME
502+
- http_request.environ
492503
exclude_keys:
493-
- request.META.SERVER_NAME
494-
- request.environ.wsgi
504+
- http_request.META.SERVER_NAME
505+
- http_request.environ.wsgi
495506
```
496507

497508
**NOTE**: `JsonDjangoRequest` only support the special key `'()'` factory in the configuration file (it doesn't work with the normal `'class'` key).
498509

510+
## Filter out LogRecord attributes based on their types
511+
512+
If different libraries or different parts of your code log different object types under the same
513+
logRecord extra attribute, you can use this filter to keep only some of them (whitelist mode) or filter out
514+
some of them (blacklist mode).
515+
516+
### Attribute Type Filter Constructor
517+
518+
| Parameter | Type | Default | Description |
519+
|-----------------|-------------------------------------|---------|-----------------------------|
520+
|`typecheck_list` | dict(key, type\|list of types)| None | A dictionary that maps keys to a type or a list of types. By default, it will only keep a parameter matching a key if the types match or if any of the types in the list match (white list). If in black list mode, it will only keep a parameter if the types don't match. Parameters not appearing in the dict will be ignored and passed though regardless of the mode (whitelist or blacklist).
521+
| `is_blacklist` | bool | false | Whether the list passed should be a blacklist or a whitelist. To use both, simply include this filter two times, one time with this parameter set true and one time with this parameter set false.
522+
523+
### Attribute Type Filter Config Example
524+
525+
```yaml
526+
filters:
527+
type_filter:
528+
(): logging_utilities.filters.attr_type_filter.AttrTypeFilter
529+
is_blacklist: False # Default value is false, so this could be left out
530+
typecheck_list:
531+
# For each attribute listed, one type or a list of types can be specified
532+
request: # can only be a toplevel attribute (no dotted keys allowed)
533+
- django.http.request.HttpRequest # Can be a class name only or the full dotted path
534+
- builtins.dict
535+
my_attr: myClass
536+
```
537+
499538
## ISO Time with Timezone
500539

501540
The standard logging doesn't support the time as ISO with timezone; `YYYY-MM-DDThh:mm:ss.sss±hh:mm`. By default `asctime` uses a ISO like format; `YYYY-MM-DD hh:mm:ss.sss`, but without `T` separator (although this one could be configured by overriding a global variable, this can't be done by config file). You can use the `datefmt` option to specify another date format, however this one don't supports milliseconds, so you could achieve this format: `YYYY-MM-DDThh:mm:ss±hh:mm`.
@@ -869,12 +908,12 @@ filters:
869908
django:
870909
(): logging_utilities.filters.django_request.JsonDjangoRequest
871910
include_keys:
872-
- request.path
873-
- request.method
874-
- request.headers
911+
- http_request.path
912+
- http_request.method
913+
- http_request.headers
875914
exclude_keys:
876-
- request.headers.Authorization
877-
- request.headers.Proxy-Authorization
915+
- http_request.headers.Authorization
916+
- http_request.headers.Proxy-Authorization
878917
879918
formatters:
880919
json:
@@ -887,7 +926,7 @@ formatters:
887926
function: funcName
888927
process: process
889928
thread: thread
890-
request: request
929+
request: http_request
891930
response: response
892931
message: message
893932
@@ -962,6 +1001,12 @@ output:
9621001

9631002
### Case 6. Add parts of Django Request to JSON Output
9641003

1004+
Let's say you want to log parts of the django `HttpRequest` in Json format. Django already logs it
1005+
sometimes under `record.request` so you can use the django request filter to transform it to a jsonisable
1006+
dictionary. However django sometimes also logs an object of type `socket.socket` that you may not
1007+
want to include in the logs. In this case you could use the following configuration. This will only
1008+
keep the request attribute if it is of type `HttpRequest`.
1009+
9651010
config.yaml
9661011

9671012
```yaml
@@ -974,10 +1019,15 @@ root:
9741019
propagate: True
9751020
9761021
filters:
1022+
type_filter:
1023+
(): logging_utilities.filters.attr_type_filter.AttrTypeFilter
1024+
typecheck_list:
1025+
request: django.http.request.HttpRequest
9771026
isotime:
9781027
(): logging_utilities.filters.TimeAttribute
9791028
django:
9801029
(): logging_utilities.filters.django_request.JsonDjangoRequest
1030+
attr_name: request
9811031
include_keys:
9821032
- request.path
9831033
- request.method
@@ -1010,6 +1060,9 @@ handlers:
10101060
stream: ext://sys.stdout
10111061
filters:
10121062
- isotime
1063+
# Typefilter must be before django filter, as the django filter
1064+
# will modify the type of the "HttpRequest" object
1065+
- type_filter
10131066
- django
10141067
```
10151068

@@ -1088,10 +1141,15 @@ root:
10881141
propagate: True
10891142
10901143
filters:
1144+
type_filter:
1145+
(): logging_utilities.filters.attr_type_filter.AttrTypeFilter
1146+
typecheck_list:
1147+
request: django.http.request.HttpRequest
10911148
isotime:
10921149
(): logging_utilities.filters.TimeAttribute
10931150
django:
10941151
(): logging_utilities.filters.django_request.JsonDjangoRequest
1152+
attr_name: request
10951153
include_keys:
10961154
- request.path
10971155
- request.method
@@ -1118,6 +1176,8 @@ handlers:
11181176
stream: ext://sys.stdout
11191177
filters:
11201178
- isotime
1179+
# Type filter must be before django filter
1180+
- type_filter
11211181
- django
11221182
```
11231183
@@ -1221,6 +1281,15 @@ output:
12211281

12221282
## Breaking Changes
12231283

1284+
### Version 4.x.x Breaking Changes
1285+
1286+
From version 3.x.x to version 4.x.x there is the following breaking change:
1287+
1288+
- The django request filter by default now reads the attribute `record.http_request` instead of
1289+
the attribute `record.request`. There is however a new option `attr_name` in the filters constructor
1290+
to manually specify the attribute name. See the example [Add parts of Django Request to JSON Output](#case-6-add-parts-of-django-request-to-json-output) for an example on how to use `attr_name` to be
1291+
backward-compatible with 3.x.x
1292+
12241293
### Version 3.x.x Breaking Changes
12251294

12261295
From version 2.x.x to version 3.x.x there is the following breaking change:

dev_requirements.txt

Lines changed: 57 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,63 @@
1+
asgiref==3.6.0
2+
astroid==2.13.2
3+
bleach==5.0.1
14
build==0.8.0
5+
certifi==2022.12.7
6+
cffi==1.15.1
7+
charset-normalizer==3.0.1
8+
click==8.1.3
9+
coverage==7.0.5
10+
cryptography==39.0.0
11+
Deprecated==1.2.13
12+
dill==0.3.6
13+
Django==3.2.16
14+
docutils==0.19
15+
escape==1.1
216
Flask==2.2.2
17+
idna==3.4
18+
importlib-metadata==6.0.0
19+
importlib-resources==5.10.2
20+
isort==5.11.4
21+
itsdangerous==2.1.2
22+
jaraco.classes==3.2.3
23+
jeepney==0.8.0
24+
Jinja2==3.1.2
25+
keyring==23.13.1
26+
lazy-object-proxy==1.9.0
27+
markdown-it-py==2.1.0
28+
MarkupSafe==2.1.2
29+
mccabe==0.7.0
30+
mdurl==0.1.2
31+
more-itertools==9.0.0
332
nose2==0.12.0
33+
packaging==23.0
34+
pep517==0.13.0
35+
pkginfo==1.9.6
36+
platformdirs==2.6.2
37+
pycparser==2.21
38+
Pygments==2.14.0
439
pylint==2.15.4
5-
yapf==0.32.0
6-
Django==3.2.16
40+
pytoolconfig==1.2.4
41+
pytz==2022.7.1
42+
readme-renderer==37.3
43+
requests==2.28.2
44+
requests-toolbelt==0.10.1
45+
rfc3986==2.0.0
46+
rich==13.2.0
747
rope==1.3.0
48+
SecretStorage==3.3.3
849
setuptools-git-versioning==1.12.0
9-
escape==1.1
10-
Jinja2==3.1.2
11-
twine==4.0.1
50+
six==1.16.0
51+
sqlparse==0.4.3
52+
toml==0.10.2
53+
tomli==2.0.1
54+
tomlkit==0.11.6
55+
twine==4.0.1
56+
typed-ast==1.5.4
57+
typing_extensions==4.4.0
58+
urllib3==1.26.14
59+
webencodings==0.5.1
60+
Werkzeug==2.2.2
61+
wrapt==1.14.1
62+
yapf==0.32.0
63+
zipp==3.11.0
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import logging
2+
3+
4+
def is_instance(attr, of_type):
5+
if isinstance(of_type, str):
6+
attr_class = type(attr)
7+
if '.' in of_type:
8+
return (
9+
'{}.{}'.format(attr_class.__module__, attr_class.__name__) == of_type or
10+
of_type in ['{}.{}'.format(c.__module__, c.__name__) for c in attr_class.__bases__]
11+
)
12+
return attr_class.__name__ == of_type or of_type in [
13+
c.__name__ for c in attr_class.__bases__
14+
]
15+
return isinstance(attr, of_type)
16+
17+
18+
class AttrTypeFilter(logging.Filter):
19+
"""Filter attributes based on their types
20+
21+
This filter can help in case multiple libraries/frameworks etc. use the
22+
same extra properties in the extra parameter of the logging. It filters
23+
these extra properties by type, either with a whitelist (the default) or
24+
with a blacklist.
25+
"""
26+
27+
def __init__(self, typecheck_list, *, is_blacklist=False):
28+
"""Initialize the filter
29+
30+
Args:
31+
typecheck_list: dict(key, type|list of types)
32+
A dictionary that maps keys to a type or a list of types.
33+
By default, it will only keep a parameter matching a key
34+
if the types match or if any of the types in the list match
35+
(white list). If in black list mode, it will only keep a
36+
parameter if the types don't match. Parameters not appearing
37+
in the dict will be ignored and passed though regardless of the
38+
mode (whitelist or blacklist).
39+
is_blacklist: bool (default: false)
40+
Whether the list passed should be a blacklist or a whitelist.
41+
To use both, simply include this filter two times, one time with
42+
this parameter set true and one time with this parameter set false.
43+
"""
44+
self.typecheck_list = typecheck_list
45+
for key in self.typecheck_list:
46+
if not isinstance(self.typecheck_list[key], list):
47+
self.typecheck_list[key] = [self.typecheck_list[key]]
48+
self.is_blacklist = is_blacklist
49+
super().__init__()
50+
51+
def filter(self, record):
52+
for key, whitelisted_types in self.typecheck_list.items():
53+
if not hasattr(record, key):
54+
continue
55+
item = getattr(record, key)
56+
if any(is_instance(item, t) for t in whitelisted_types) is self.is_blacklist:
57+
delattr(record, key)
58+
59+
return True

0 commit comments

Comments
 (0)