Skip to content

Commit 3306656

Browse files
author
JS5391
committed
First release
0 parents  commit 3306656

14 files changed

Lines changed: 555 additions & 0 deletions

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/.idea/

.pre-commit-config.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
repos:
2+
- repo: https://github.com/psf/black
3+
rev: 20.8b1
4+
hooks:
5+
- id: black

.travis.yml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
language: python
2+
python:
3+
- "3.6"
4+
- "3.7"
5+
- "3.8"
6+
- "3.9-dev"
7+
install:
8+
- pip install .[testing]
9+
script:
10+
- pytest --cov=logging_json --cov-fail-under=100
11+
deploy:
12+
provider: pypi
13+
username: __token__
14+
edge: true
15+
distributions: "sdist bdist_wheel"
16+
skip_existing: true

CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# Changelog
2+
All notable changes to this project will be documented in this file.
3+
4+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
5+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6+
7+
## [Unreleased]
8+
9+
## [0.0.1] - 2020-10-15
10+
### Added
11+
- Public release.
12+
13+
[Unreleased]: https://github.com/Colin-b/healthpy/compare/v0.0.1...HEAD
14+
[0.0.1]: https://github.com/Colin-b/healthpy/releases/tag/v0.0.1

CONTRIBUTING.md

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# How to contribute
2+
3+
Everyone is free to contribute on this project.
4+
5+
There are 2 ways to contribute:
6+
7+
- [Submit an issue.](#submitting-an-issue)
8+
- [Submit a pull request.](#submitting-a-pull-request)
9+
10+
## Submitting an issue
11+
12+
Before creating an issue please make sure that it was not already reported.
13+
14+
### When?
15+
16+
- You encountered an issue.
17+
- You have a change proposal.
18+
- You have a feature request.
19+
20+
### How?
21+
22+
1) Go to the *Issues* tab and click on the *New issue* button.
23+
2) Title should be a small sentence describing the request.
24+
3) The comment should contains as much information as possible
25+
* Actual behavior (including the version you used)
26+
* Expected behavior
27+
* Steps to reproduce
28+
29+
## Submitting a pull request
30+
31+
### When?
32+
33+
- You fixed an issue.
34+
- You changed something.
35+
- You added a new feature.
36+
37+
### How?
38+
39+
#### Code
40+
41+
1) Create a new branch based on `develop` branch.
42+
2) Fetch all dev dependencies.
43+
* Install required python modules using [`pip`](https://pypi.org/project/pip/): **python -m pip install .[testing]**
44+
3) Ensure tests are ok by running them using [`pytest`](http://doc.pytest.org/en/latest/index.html).
45+
4) Follow [Black](https://black.readthedocs.io/en/stable/) code formatting.
46+
* Install [pre-commit](https://pre-commit.com) python module using pip: **python -m pip install pre-commit**
47+
* To add the [pre-commit](https://pre-commit.com) hook, after the installation run: **pre-commit install**
48+
5) Add your changes.
49+
* The commit should only contain small changes and should be atomic.
50+
* The commit message should follow [those rules](https://chris.beams.io/posts/git-commit/).
51+
6) Add or update at least one [`pytest`](http://doc.pytest.org/en/latest/index.html) test case.
52+
* Unless it is an internal refactoring request or a documentation update.
53+
* Each line of code should be covered by the test cases.
54+
7) Add related [changelog entry](https://keepachangelog.com/en/1.1.0/) in the Unreleased section.
55+
* Unless it is a documentation update.
56+
57+
#### Enter pull request
58+
59+
1) Go to the *Pull requests* tab and click on the *New pull request* button.
60+
2) *base* should always be set to `develop` and it should be compared to your branch.
61+
3) Title should be a small sentence describing the request.
62+
4) The comment should contains as much information as possible
63+
* Actual behavior (before the new code)
64+
* Expected behavior (with the new code)
65+
5) A pull request can contain more than one commit, but the entire content should still be [atomic](#what-is-an-atomic-pull-request).
66+
67+
##### What is an atomic pull request
68+
69+
It is important for a Pull Request to be atomic. But with a Pull Request, we measure the "succeed" as the ability to deliver the smallest possible piece of functionality, it can either be composed by one or many atomic commits.
70+
71+
One of the bad practices of a Pull Request is changing things that are not concerned with the functionality that is being addressed, like whitespace changes, typo fixes, variable renaming, etc. If those things are not related to the concern of the Pull Request, it should probably be done in a different one.
72+
73+
One might argue that this practice of not mixing different concerns and small fixes in the same Pull Request violates the Boy Scout Rule because it doesn't allow frequent cleanup. However, cleanup doesn't need to be done in the same Pull Request, the important thing is not leaving the codebase in a bad state after finishing the functionality. If you must, refactor the code in a separate Pull Request, and preferably before the actually concerned functionality is developed, because then if there is a need in the near future to revert the Pull Request, the likelihood of code conflict will be lower. [source](https://medium.com/@fagnerbrack/one-pull-request-one-concern-e84a27dfe9f1)

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2020 Colin Bounouar
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
<h2 align="center">JSON formatter for logging</h2>
2+
3+
<p align="center">
4+
<a href="https://pypi.org/project/logging_json/"><img alt="pypi version" src="https://img.shields.io/pypi/v/logging_json"></a>
5+
<a href="https://travis-ci.com/Colin-b/logging_json"><img alt="Build status" src="https://api.travis-ci.com/Colin-b/logging_json.svg?branch=master"></a>
6+
<a href="https://travis-ci.com/Colin-b/logging_json"><img alt="Coverage" src="https://img.shields.io/badge/coverage-100%25-brightgreen"></a>
7+
<a href="https://github.com/psf/black"><img alt="Code style: black" src="https://img.shields.io/badge/code%20style-black-000000.svg"></a>
8+
<a href="https://travis-ci.com/Colin-b/logging_json"><img alt="Number of tests" src="https://img.shields.io/badge/tests-0 passed-blue"></a>
9+
<a href="https://pypi.org/project/logging_json/"><img alt="Number of downloads" src="https://img.shields.io/pypi/dm/logging_json"></a>
10+
</p>
11+
12+
This module provides a JSON formatter for the python [`logging`](https://docs.python.org/3/library/logging.html) module.
13+
14+
## Features
15+
16+
### Adding additional fields and values
17+
18+
You can add fields to every message that is being logged.
19+
To do so, specify the `fields` parameter to the `logging_json.JSONFormatter` instance.
20+
21+
It must be a dictionary where keys are the keys to be appended to the resulting JSON dictionary (if not already present) and the values can be one of the following:
22+
* An attribute of the logging record (non-exhaustive list can be found on [the python logging documentation](https://docs.python.org/3/library/logging.html#logrecord-attributes)).
23+
* If not found on the record, the value will be linked to the key.
24+
25+
#### Logging exceptions, a specific case
26+
27+
If an exception is loggued, the `exception` key will be appended to the resulting JSON dictionary.
28+
29+
This dictionary will contains 3 keys:
30+
* `type`: The name of the exception class (useful when the message is blank).
31+
* `message`: The str representation of the exception (usually the provided error message).
32+
* `stack`: The stack trace, formatted as a string.
33+
34+
### Logging with a dictionary
35+
36+
This formatter allows you to log dictionary as in the following:
37+
38+
```python
39+
import logging
40+
41+
logging.info({"key": "value", "other key": "other value"})
42+
```
43+
44+
The resulting JSON dictionary will be the one you provided (with the [additional fields](#adding-additional-fields-and-values)).
45+
46+
### Logging with anything else (such as a string)
47+
48+
Anything not logged using a dictionary will be handled by the standard formatter and it can result in one of the 2 output:
49+
* A JSON dictionary, if [additional fields](#adding-additional-fields-and-values) are set, with the message available in the `msg` key of the resulting JSON dictionary.
50+
* The formatted record, if no [additional fields](#adding-additional-fields-and-values) are set.
51+
52+
This handles the usual string logging as in the following:
53+
54+
```python
55+
import logging
56+
57+
logging.info("This is my message")
58+
```
59+
60+
## Configuration
61+
62+
You can create a formatter instance yourself as in the following or you can use a logging configuration.
63+
64+
```python
65+
import logging_json
66+
67+
formatter = logging_json.JSONFormatter(fields={
68+
"level_name": "levelname",
69+
"thread_name": "threadName",
70+
"process_name": "processName"
71+
})
72+
```
73+
74+
### Using logging.config.dictConfig
75+
76+
You can configure your logging as advertise by python, by using the `logging.config.dictConfig` function.
77+
78+
#### dict configuration
79+
80+
```python
81+
import logging.config
82+
import logging_json
83+
import sys
84+
85+
formatter = logging_json.JSONFormatter(fields={
86+
"level_name": "levelname",
87+
"thread_name": "threadName",
88+
"process_name": "processName"
89+
})
90+
handler = logging.StreamHandler(stream=sys.stdout)
91+
handler.setFormatter(formatter)
92+
93+
logging.config.dictConfig({
94+
"version": 1,
95+
"formatters": {
96+
"json": formatter
97+
},
98+
"handlers": {
99+
"standard_output": handler,
100+
},
101+
"loggers": {
102+
"my_app": {"level": "DEBUG"}
103+
},
104+
"root": {
105+
"level": "INFO",
106+
"handlers": ["standard_output"]
107+
}
108+
})
109+
```
110+
111+
#### YAML logging configuration
112+
113+
You can use YAML to store your logging configuration, as in the following sample:
114+
115+
```python
116+
import logging.config
117+
import yaml
118+
119+
with open("path/to/logging_configuration.yaml", "r") as config_file:
120+
logging.config.dictConfig(yaml.load(config_file))
121+
```
122+
123+
Where `logging_configuration.yaml` can be a file containing the following sample:
124+
125+
```yaml
126+
version: 1
127+
formatters:
128+
json:
129+
'()': logging_json.JSONFormatter
130+
fields:
131+
level_name: levelname
132+
thread_name: threadName
133+
process_name: processName
134+
handlers:
135+
standard_output:
136+
class: logging.StreamHandler
137+
formatter: json
138+
stream: ext://sys.stdout
139+
loggers:
140+
my_app:
141+
level: DEBUG
142+
root:
143+
level: INFO
144+
handlers: [standard_output]
145+
```
146+
147+
## How to install
148+
1. [python 3.6+](https://www.python.org/downloads/) must be installed
149+
2. Use pip to install module:
150+
```sh
151+
python -m pip install logging_json
152+
```

_config.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
theme: jekyll-theme-cayman

logging_json/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
from logging_json.version import __version__
2+
from logging_json._formatter import JSONFormatter

logging_json/_formatter.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import collections
2+
import json
3+
import logging
4+
from typing import Any
5+
6+
7+
def _value(record: logging.LogRecord, field_name_or_value: Any) -> Any:
8+
"""
9+
Retrieve value from record if possible. Otherwise use value.
10+
:param record: The record to extract a field named as in field_name_or_value.
11+
:param field_name_or_value: The field name to extract from record or the default value to use if not present.
12+
"""
13+
try:
14+
return getattr(record, field_name_or_value)
15+
except:
16+
return field_name_or_value
17+
18+
19+
class JSONFormatter(logging.Formatter):
20+
def __init__(self, *args, fields=None, **kwargs):
21+
# Allow to provide any formatter setting (useful to provide a custom date format)
22+
super().__init__(*args, **kwargs)
23+
self.fields = fields or {}
24+
self.usesTime = lambda: "asctime" in self.fields.values()
25+
26+
def format(self, record: logging.LogRecord):
27+
# Let python set every additional record field
28+
super().format(record)
29+
30+
message = {
31+
field_name: _value(record, field_value)
32+
for field_name, field_value in self.fields.items()
33+
}
34+
if isinstance(record.msg, collections.abc.Mapping):
35+
message.update(record.msg)
36+
else:
37+
message["msg"] = super().formatMessage(record)
38+
39+
if record.exc_info:
40+
message["exception"] = {
41+
"type": record.exc_info[0].__name__,
42+
"message": str(record.exc_info[1]),
43+
"stack": self.formatException(record.exc_info),
44+
}
45+
46+
return (
47+
super().formatMessage(record)
48+
if (len(message) == 1 and "msg" in message)
49+
else json.dumps(message)
50+
)
51+
52+
def formatMessage(self, record: logging.LogRecord) -> str:
53+
# Speed up this step by doing nothing
54+
return ""

0 commit comments

Comments
 (0)