├── .circleci └── config.yml ├── .coveragerc ├── .gitignore ├── LICENSE ├── Makefile ├── Pipfile ├── Pipfile.lock ├── README.md ├── google_cloud_logger └── __init__.py ├── pytest.ini ├── setup.py └── test ├── __init__.py └── google_cloud_logger ├── __init__.py ├── conftest.py └── test_google_cloud_formatter.py /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | docker: 5 | - image: circleci/python:3.7.1 6 | environment: 7 | CC_TEST_REPORTER_ID: 9f8a5da723397006023534dc6c5e32fc95d38bf7a92ae0e43530dba2a726722e 8 | steps: 9 | - checkout 10 | - run: 11 | name: Install OS dependencies 12 | command: make setup-os 13 | - run: 14 | name: Install Code Climate Test Reporter Tool 15 | command: | 16 | curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter 17 | chmod +x ./cc-test-reporter 18 | - run: 19 | name: Install package dependencies 20 | command: make setup 21 | - run: 22 | command: | 23 | ./cc-test-reporter before-build 24 | - run: 25 | name: Run unit tests 26 | command: make test 27 | - run: 28 | name: Upload test coverage to Code Climate 29 | command: | 30 | ./cc-test-reporter format-coverage coverage.xml -t coverage.py 31 | ./cc-test-reporter upload-coverage 32 | - run: 33 | command: | 34 | ./cc-test-reporter after-build -t coverage.py 35 | - run: 36 | command: make check 37 | - store_artifacts: 38 | destination: htmlcov 39 | path: htmlcov 40 | deploy: 41 | docker: 42 | - image: circleci/python:3.7.1 43 | steps: 44 | - checkout 45 | - run: 46 | name: Install OS dependencies 47 | command: make setup-os 48 | - run: 49 | name: Install package dependencies 50 | command: make setup 51 | - run: 52 | name: init .pypirc 53 | command: | 54 | echo -e "[pypi]" >> ~/.pypirc 55 | echo -e "repository = https://upload.pypi.org/legacy/" 56 | - run: 57 | name: Publish Package on Pypi 58 | command: make release 59 | workflows: 60 | version: 2 61 | build_and_deploy: 62 | jobs: 63 | - build: 64 | filters: 65 | tags: 66 | only: /.*/ 67 | - deploy: 68 | requires: 69 | - build 70 | filters: 71 | tags: 72 | only: /[0-9]+(\.[0-9]+)*/ 73 | branches: 74 | ignore: /.*/ -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | omit = test/* -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | coverage/ 40 | htmlcov/ 41 | .tox/ 42 | .coverage 43 | .coverage.* 44 | .cache 45 | nosetests.xml 46 | coverage.xml 47 | *.cover 48 | .hypothesis/ 49 | .pytest_cache/ 50 | 51 | # Translations 52 | *.mo 53 | *.pot 54 | 55 | # Django stuff: 56 | *.log 57 | local_settings.py 58 | db.sqlite3 59 | 60 | # Flask stuff: 61 | instance/ 62 | .webassets-cache 63 | 64 | # Scrapy stuff: 65 | .scrapy 66 | 67 | # Sphinx documentation 68 | docs/_build/ 69 | 70 | # PyBuilder 71 | target/ 72 | 73 | # Jupyter Notebook 74 | .ipynb_checkpoints 75 | 76 | # pyenv 77 | .python-version 78 | 79 | # celery beat schedule file 80 | celerybeat-schedule 81 | 82 | # SageMath parsed files 83 | *.sage.py 84 | 85 | # Environments 86 | .env 87 | .venv 88 | env/ 89 | venv/ 90 | ENV/ 91 | env.bak/ 92 | venv.bak/ 93 | 94 | # Spyder project settings 95 | .spyderproject 96 | .spyproject 97 | 98 | # Rope project settings 99 | .ropeproject 100 | 101 | # mkdocs documentation 102 | /site 103 | 104 | # mypy 105 | .mypy_cache/ 106 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Raissa Ferreira 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. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: setup-os 2 | setup-os: 3 | sudo apt install python-pip 4 | sudo pip install -U pip pipenv 5 | 6 | .PHONY: setup 7 | setup: 8 | pipenv --rm || true 9 | pipenv --python python3.7 10 | pipenv install --dev 11 | 12 | .PHONY: test 13 | test: 14 | pipenv run pytest 15 | 16 | .PHONY: check 17 | check: 18 | pipenv run flake8 19 | pipenv run safety check 20 | 21 | .PHONY: build 22 | build: 23 | rm -rf dist 24 | pipenv run python setup.py sdist bdist_wheel 25 | 26 | .PHONY: release 27 | release: build 28 | pipenv run twine upload dist/* || true -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [packages] 7 | google_cloud_logger = {editable = true, path = "."} 8 | 9 | [dev-packages] 10 | pytest = "~=3.9.3" 11 | pytest_mock = "~=1.10.0" 12 | pytest-cov = "~=2.6.0" 13 | flake8 = "~=3.6.0" 14 | yapf = "~=0.24.0" 15 | ipdb = "~=0.11" 16 | safety = "~=1.8.4" 17 | twine = "~=1.12.1" 18 | pyyaml = ">=4.2b4" 19 | 20 | [requires] 21 | python_version = "3.7" 22 | -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "743418b60c63bcbc9ff3e34c13f1d9452abac79f2b7be30949a0dd632eb65985" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.7" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "google-cloud-logger": { 20 | "editable": true, 21 | "path": "." 22 | }, 23 | "python-json-logger": { 24 | "hashes": [ 25 | "sha256:3e000053837500f9eb28d6228d7cb99fabfc1874d34b40c08289207292abaf2e", 26 | "sha256:cf2caaf34bd2eff394915b6242de4d0245de79971712439380ece6f149748cde" 27 | ], 28 | "version": "==0.1.10" 29 | } 30 | }, 31 | "develop": { 32 | "atomicwrites": { 33 | "hashes": [ 34 | "sha256:0312ad34fcad8fac3704d441f7b317e50af620823353ec657a53e981f92920c0", 35 | "sha256:ec9ae8adaae229e4f8446952d204a3e4b5fdd2d099f9be3aaf556120135fb3ee" 36 | ], 37 | "version": "==1.2.1" 38 | }, 39 | "attrs": { 40 | "hashes": [ 41 | "sha256:10cbf6e27dbce8c30807caf056c8eb50917e0eaafe86347671b57254006c3e69", 42 | "sha256:ca4be454458f9dec299268d472aaa5a11f67a4ff70093396e1ceae9c76cf4bbb" 43 | ], 44 | "version": "==18.2.0" 45 | }, 46 | "backcall": { 47 | "hashes": [ 48 | "sha256:38ecd85be2c1e78f77fd91700c76e14667dc21e2713b63876c0eb901196e01e4", 49 | "sha256:bbbf4b1e5cd2bdb08f915895b51081c041bac22394fdfcfdfbe9f14b77c08bf2" 50 | ], 51 | "version": "==0.1.0" 52 | }, 53 | "bleach": { 54 | "hashes": [ 55 | "sha256:48d39675b80a75f6d1c3bdbffec791cf0bbbab665cf01e20da701c77de278718", 56 | "sha256:73d26f018af5d5adcdabf5c1c974add4361a9c76af215fe32fdec8a6fc5fb9b9" 57 | ], 58 | "version": "==3.0.2" 59 | }, 60 | "certifi": { 61 | "hashes": [ 62 | "sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7", 63 | "sha256:993f830721089fef441cdfeb4b2c8c9df86f0c63239f06bd025a76a7daddb033" 64 | ], 65 | "version": "==2018.11.29" 66 | }, 67 | "chardet": { 68 | "hashes": [ 69 | "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", 70 | "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" 71 | ], 72 | "version": "==3.0.4" 73 | }, 74 | "click": { 75 | "hashes": [ 76 | "sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13", 77 | "sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7" 78 | ], 79 | "version": "==7.0" 80 | }, 81 | "coverage": { 82 | "hashes": [ 83 | "sha256:029c69deaeeeae1b15bc6c59f0ffa28aa8473721c614a23f2c2976dec245cd12", 84 | "sha256:02abbbebc6e9d5abe13cd28b5e963dedb6ffb51c146c916d17b18f141acd9947", 85 | "sha256:1bbfe5b82a3921d285e999c6d256c1e16b31c554c29da62d326f86c173d30337", 86 | "sha256:210c02f923df33a8d0e461c86fdcbbb17228ff4f6d92609fc06370a98d283c2d", 87 | "sha256:2d0807ba935f540d20b49d5bf1c0237b90ce81e133402feda906e540003f2f7a", 88 | "sha256:35d7a013874a7c927ce997350d314144ffc5465faf787bb4e46e6c4f381ef562", 89 | "sha256:3636f9d0dcb01aed4180ef2e57a4e34bb4cac3ecd203c2a23db8526d86ab2fb4", 90 | "sha256:42f4be770af2455a75e4640f033a82c62f3fb0d7a074123266e143269d7010ef", 91 | "sha256:48440b25ba6cda72d4c638f3a9efa827b5b87b489c96ab5f4ff597d976413156", 92 | "sha256:4dac8dfd1acf6a3ac657475dfdc66c621f291b1b7422a939cc33c13ac5356473", 93 | "sha256:4e8474771c69c2991d5eab65764289a7dd450bbea050bc0ebb42b678d8222b42", 94 | "sha256:551f10ddfeff56a1325e5a34eff304c5892aa981fd810babb98bfee77ee2fb17", 95 | "sha256:5b104982f1809c1577912519eb249f17d9d7e66304ad026666cb60a5ef73309c", 96 | "sha256:5c62aef73dfc87bfcca32cee149a1a7a602bc74bac72223236b0023543511c88", 97 | "sha256:633151f8d1ad9467b9f7e90854a7f46ed8f2919e8bc7d98d737833e8938fc081", 98 | "sha256:772207b9e2d5bf3f9d283b88915723e4e92d9a62c83f44ec92b9bd0cd685541b", 99 | "sha256:7d5e02f647cd727afc2659ec14d4d1cc0508c47e6cfb07aea33d7aa9ca94d288", 100 | "sha256:a9798a4111abb0f94584000ba2a2c74841f2cfe5f9254709756367aabbae0541", 101 | "sha256:b38ea741ab9e35bfa7015c93c93bbd6a1623428f97a67083fc8ebd366238b91f", 102 | "sha256:b6a5478c904236543c0347db8a05fac6fc0bd574c870e7970faa88e1d9890044", 103 | "sha256:c6248bfc1de36a3844685a2e10ba17c18119ba6252547f921062a323fb31bff1", 104 | "sha256:c705ab445936457359b1424ef25ccc0098b0491b26064677c39f1d14a539f056", 105 | "sha256:d95a363d663ceee647291131dbd213af258df24f41350246842481ec3709bd33", 106 | "sha256:e27265eb80cdc5dab55a40ef6f890e04ecc618649ad3da5265f128b141f93f78", 107 | "sha256:ebc276c9cb5d917bd2ae959f84ffc279acafa9c9b50b0fa436ebb70bbe2166ea", 108 | "sha256:f4d229866d030863d0fe3bf297d6d11e6133ca15bbb41ed2534a8b9a3d6bd061", 109 | "sha256:f95675bd88b51474d4fe5165f3266f419ce754ffadfb97f10323931fa9ac95e5", 110 | "sha256:f95bc54fb6d61b9f9ff09c4ae8ff6a3f5edc937cda3ca36fc937302a7c152bf1", 111 | "sha256:fd0f6be53de40683584e5331c341e65a679dbe5ec489a0697cec7c2ef1a48cda" 112 | ], 113 | "version": "==5.0a4" 114 | }, 115 | "decorator": { 116 | "hashes": [ 117 | "sha256:2c51dff8ef3c447388fe5e4453d24a2bf128d3a4c32af3fabef1f01c6851ab82", 118 | "sha256:c39efa13fbdeb4506c476c9b3babf6a718da943dab7811c206005a4a956c080c" 119 | ], 120 | "version": "==4.3.0" 121 | }, 122 | "docutils": { 123 | "hashes": [ 124 | "sha256:02aec4bd92ab067f6ff27a38a38a41173bf01bed8f89157768c1573f53e474a6", 125 | "sha256:51e64ef2ebfb29cae1faa133b3710143496eca21c530f3f71424d77687764274", 126 | "sha256:7a4bd47eaf6596e1295ecb11361139febe29b084a87bf005bf899f9a42edc3c6" 127 | ], 128 | "version": "==0.14" 129 | }, 130 | "dparse": { 131 | "hashes": [ 132 | "sha256:00a5fdfa900629e5159bf3600d44905b333f4059a3366f28e0dbd13eeab17b19", 133 | "sha256:cef95156fa0adedaf042cd42f9990974bec76f25dfeca4dc01f381a243d5aa5b" 134 | ], 135 | "version": "==0.4.1" 136 | }, 137 | "flake8": { 138 | "hashes": [ 139 | "sha256:6a35f5b8761f45c5513e3405f110a86bea57982c3b75b766ce7b65217abe1670", 140 | "sha256:c01f8a3963b3571a8e6bd7a4063359aff90749e160778e03817cd9b71c9e07d2" 141 | ], 142 | "index": "pypi", 143 | "version": "==3.6.0" 144 | }, 145 | "idna": { 146 | "hashes": [ 147 | "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", 148 | "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" 149 | ], 150 | "version": "==2.8" 151 | }, 152 | "ipdb": { 153 | "hashes": [ 154 | "sha256:7081c65ed7bfe7737f83fa4213ca8afd9617b42ff6b3f1daf9a3419839a2a00a" 155 | ], 156 | "index": "pypi", 157 | "version": "==0.11" 158 | }, 159 | "ipython": { 160 | "hashes": [ 161 | "sha256:6a9496209b76463f1dec126ab928919aaf1f55b38beb9219af3fe202f6bbdd12", 162 | "sha256:f69932b1e806b38a7818d9a1e918e5821b685715040b48e59c657b3c7961b742" 163 | ], 164 | "markers": "python_version >= '3.3'", 165 | "version": "==7.2.0" 166 | }, 167 | "ipython-genutils": { 168 | "hashes": [ 169 | "sha256:72dd37233799e619666c9f639a9da83c34013a73e8bbc79a7a6348d93c61fab8", 170 | "sha256:eb2e116e75ecef9d4d228fdc66af54269afa26ab4463042e33785b887c628ba8" 171 | ], 172 | "version": "==0.2.0" 173 | }, 174 | "jedi": { 175 | "hashes": [ 176 | "sha256:571702b5bd167911fe9036e5039ba67f820d6502832285cde8c881ab2b2149fd", 177 | "sha256:c8481b5e59d34a5c7c42e98f6625e633f6ef59353abea6437472c7ec2093f191" 178 | ], 179 | "version": "==0.13.2" 180 | }, 181 | "mccabe": { 182 | "hashes": [ 183 | "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", 184 | "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" 185 | ], 186 | "version": "==0.6.1" 187 | }, 188 | "more-itertools": { 189 | "hashes": [ 190 | "sha256:38a936c0a6d98a38bcc2d03fdaaedaba9f412879461dd2ceff8d37564d6522e4", 191 | "sha256:c0a5785b1109a6bd7fac76d6837fd1feca158e54e521ccd2ae8bfe393cc9d4fc", 192 | "sha256:fe7a7cae1ccb57d33952113ff4fa1bc5f879963600ed74918f1236e212ee50b9" 193 | ], 194 | "version": "==5.0.0" 195 | }, 196 | "packaging": { 197 | "hashes": [ 198 | "sha256:0886227f54515e592aaa2e5a553332c73962917f2831f1b0f9b9f4380a4b9807", 199 | "sha256:f95a1e147590f204328170981833854229bb2912ac3d5f89e2a8ccd2834800c9" 200 | ], 201 | "version": "==18.0" 202 | }, 203 | "parso": { 204 | "hashes": [ 205 | "sha256:35704a43a3c113cce4de228ddb39aab374b8004f4f2407d070b6a2ca784ce8a2", 206 | "sha256:895c63e93b94ac1e1690f5fdd40b65f07c8171e3e53cbd7793b5b96c0e0a7f24" 207 | ], 208 | "version": "==0.3.1" 209 | }, 210 | "pexpect": { 211 | "hashes": [ 212 | "sha256:2a8e88259839571d1251d278476f3eec5db26deb73a70be5ed5dc5435e418aba", 213 | "sha256:3fbd41d4caf27fa4a377bfd16fef87271099463e6fa73e92a52f92dfee5d425b" 214 | ], 215 | "markers": "sys_platform != 'win32'", 216 | "version": "==4.6.0" 217 | }, 218 | "pickleshare": { 219 | "hashes": [ 220 | "sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca", 221 | "sha256:9649af414d74d4df115d5d718f82acb59c9d418196b7b4290ed47a12ce62df56" 222 | ], 223 | "version": "==0.7.5" 224 | }, 225 | "pkginfo": { 226 | "hashes": [ 227 | "sha256:7424f2c8511c186cd5424bbf31045b77435b37a8d604990b79d4e70d741148bb", 228 | "sha256:a6d9e40ca61ad3ebd0b72fbadd4fba16e4c0e4df0428c041e01e06eb6ee71f32" 229 | ], 230 | "version": "==1.5.0.1" 231 | }, 232 | "pluggy": { 233 | "hashes": [ 234 | "sha256:447ba94990e8014ee25ec853339faf7b0fc8050cdc3289d4d71f7f410fb90095", 235 | "sha256:bde19360a8ec4dfd8a20dcb811780a30998101f078fc7ded6162f0076f50508f" 236 | ], 237 | "version": "==0.8.0" 238 | }, 239 | "prompt-toolkit": { 240 | "hashes": [ 241 | "sha256:c1d6aff5252ab2ef391c2fe498ed8c088066f66bc64a8d5c095bbf795d9fec34", 242 | "sha256:d4c47f79b635a0e70b84fdb97ebd9a274203706b1ee5ed44c10da62755cf3ec9", 243 | "sha256:fd17048d8335c1e6d5ee403c3569953ba3eb8555d710bfc548faf0712666ea39" 244 | ], 245 | "version": "==2.0.7" 246 | }, 247 | "ptyprocess": { 248 | "hashes": [ 249 | "sha256:923f299cc5ad920c68f2bc0bc98b75b9f838b93b599941a6b63ddbc2476394c0", 250 | "sha256:d7cc528d76e76342423ca640335bd3633420dc1366f258cb31d05e865ef5ca1f" 251 | ], 252 | "version": "==0.6.0" 253 | }, 254 | "py": { 255 | "hashes": [ 256 | "sha256:bf92637198836372b520efcba9e020c330123be8ce527e535d185ed4b6f45694", 257 | "sha256:e76826342cefe3c3d5f7e8ee4316b80d1dd8a300781612ddbc765c17ba25a6c6" 258 | ], 259 | "version": "==1.7.0" 260 | }, 261 | "pycodestyle": { 262 | "hashes": [ 263 | "sha256:cbc619d09254895b0d12c2c691e237b2e91e9b2ecf5e84c26b35400f93dcfb83", 264 | "sha256:cbfca99bd594a10f674d0cd97a3d802a1fdef635d4361e1a2658de47ed261e3a" 265 | ], 266 | "version": "==2.4.0" 267 | }, 268 | "pyflakes": { 269 | "hashes": [ 270 | "sha256:9a7662ec724d0120012f6e29d6248ae3727d821bba522a0e6b356eff19126a49", 271 | "sha256:f661252913bc1dbe7fcfcbf0af0db3f42ab65aabd1a6ca68fe5d466bace94dae" 272 | ], 273 | "version": "==2.0.0" 274 | }, 275 | "pygments": { 276 | "hashes": [ 277 | "sha256:5ffada19f6203563680669ee7f53b64dabbeb100eb51b61996085e99c03b284a", 278 | "sha256:e8218dd399a61674745138520d0d4cf2621d7e032439341bc3f647bff125818d" 279 | ], 280 | "version": "==2.3.1" 281 | }, 282 | "pyparsing": { 283 | "hashes": [ 284 | "sha256:40856e74d4987de5d01761a22d1621ae1c7f8774585acae358aa5c5936c6c90b", 285 | "sha256:f353aab21fd474459d97b709e527b5571314ee5f067441dc9f88e33eecd96592" 286 | ], 287 | "version": "==2.3.0" 288 | }, 289 | "pytest": { 290 | "hashes": [ 291 | "sha256:a9e5e8d7ab9d5b0747f37740276eb362e6a76275d76cebbb52c6049d93b475db", 292 | "sha256:bf47e8ed20d03764f963f0070ff1c8fda6e2671fc5dd562a4d3b7148ad60f5ca" 293 | ], 294 | "index": "pypi", 295 | "version": "==3.9.3" 296 | }, 297 | "pytest-cov": { 298 | "hashes": [ 299 | "sha256:0ab664b25c6aa9716cbf203b17ddb301932383046082c081b9848a0edf5add33", 300 | "sha256:230ef817450ab0699c6cc3c9c8f7a829c34674456f2ed8df1fe1d39780f7c87f" 301 | ], 302 | "index": "pypi", 303 | "version": "==2.6.1" 304 | }, 305 | "pytest-mock": { 306 | "hashes": [ 307 | "sha256:53801e621223d34724926a5c98bd90e8e417ce35264365d39d6c896388dcc928", 308 | "sha256:d89a8209d722b8307b5e351496830d5cc5e192336003a485443ae9adeb7dd4c0" 309 | ], 310 | "index": "pypi", 311 | "version": "==1.10.0" 312 | }, 313 | "pyyaml": { 314 | "hashes": [ 315 | "sha256:254bf6fda2b7c651837acb2c718e213df29d531eebf00edb54743d10bcb694eb", 316 | "sha256:3108529b78577327d15eec243f0ff348a0640b0c3478d67ad7f5648f93bac3e2", 317 | "sha256:3c17fb92c8ba2f525e4b5f7941d850e7a48c3a59b32d331e2502a3cdc6648e76", 318 | "sha256:8d6d96001aa7f0a6a4a95e8143225b5d06e41b1131044913fecb8f85a125714b", 319 | "sha256:c8a88edd93ee29ede719080b2be6cb2333dfee1dccba213b422a9c8e97f2967b" 320 | ], 321 | "index": "pypi", 322 | "version": "==4.2b4" 323 | }, 324 | "readme-renderer": { 325 | "hashes": [ 326 | "sha256:bb16f55b259f27f75f640acf5e00cf897845a8b3e4731b5c1a436e4b8529202f", 327 | "sha256:c8532b79afc0375a85f10433eca157d6b50f7d6990f337fa498c96cd4bfc203d" 328 | ], 329 | "version": "==24.0" 330 | }, 331 | "requests": { 332 | "hashes": [ 333 | "sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e", 334 | "sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b" 335 | ], 336 | "version": "==2.21.0" 337 | }, 338 | "requests-toolbelt": { 339 | "hashes": [ 340 | "sha256:42c9c170abc2cacb78b8ab23ac957945c7716249206f90874651971a4acff237", 341 | "sha256:f6a531936c6fa4c6cfce1b9c10d5c4f498d16528d2a54a22ca00011205a187b5" 342 | ], 343 | "version": "==0.8.0" 344 | }, 345 | "safety": { 346 | "hashes": [ 347 | "sha256:399511524f47230d5867f1eb75548f9feefb7a2711a4985cb5be0e034f87040f", 348 | "sha256:69b970918324865dcd7b92337e07152a0ea1ceecaf92f4d3b38529ee0ca83441" 349 | ], 350 | "index": "pypi", 351 | "version": "==1.8.4" 352 | }, 353 | "six": { 354 | "hashes": [ 355 | "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", 356 | "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" 357 | ], 358 | "version": "==1.12.0" 359 | }, 360 | "tqdm": { 361 | "hashes": [ 362 | "sha256:79420109a762f82e20e8ecdc3b3bc1bc6c6536884a8de5fa86a50eb99386376a", 363 | "sha256:7fa801cf60e318bf7d2ac7498254df0f3d7a3ad23c622ae63666257d5f833967" 364 | ], 365 | "version": "==4.29.0" 366 | }, 367 | "traitlets": { 368 | "hashes": [ 369 | "sha256:9c4bd2d267b7153df9152698efb1050a5d84982d3384a37b2c1f7723ba3e7835", 370 | "sha256:c6cb5e6f57c5a9bdaa40fa71ce7b4af30298fbab9ece9815b5d995ab6217c7d9" 371 | ], 372 | "version": "==4.3.2" 373 | }, 374 | "twine": { 375 | "hashes": [ 376 | "sha256:7d89bc6acafb31d124e6e5b295ef26ac77030bf098960c2a4c4e058335827c5c", 377 | "sha256:fad6f1251195f7ddd1460cb76d6ea106c93adb4e56c41e0da79658e56e547d2c" 378 | ], 379 | "index": "pypi", 380 | "version": "==1.12.1" 381 | }, 382 | "urllib3": { 383 | "hashes": [ 384 | "sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39", 385 | "sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22" 386 | ], 387 | "version": "==1.24.1" 388 | }, 389 | "wcwidth": { 390 | "hashes": [ 391 | "sha256:3df37372226d6e63e1b1e1eda15c594bca98a22d33a23832a90998faa96bc65e", 392 | "sha256:f4ebe71925af7b40a864553f761ed559b43544f8f71746c2d756c7fe788ade7c" 393 | ], 394 | "version": "==0.1.7" 395 | }, 396 | "webencodings": { 397 | "hashes": [ 398 | "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78", 399 | "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923" 400 | ], 401 | "version": "==0.5.1" 402 | }, 403 | "yapf": { 404 | "hashes": [ 405 | "sha256:b96815bd0bbd2ab290f2ae9e610756940b17a0523ef2f6b2d31da749fc395137", 406 | "sha256:cebb6faf35c9027c08996c07831b8971f3d67c0eb615269f66dfd7e6815fdc2a" 407 | ], 408 | "index": "pypi", 409 | "version": "==0.24.0" 410 | } 411 | } 412 | } 413 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # python_google_cloud_logger 2 | 3 | [![CircleCI](https://circleci.com/gh/rai200890/python_google_cloud_logger.svg?style=svg&circle-token=cdb4c95268aa18f240f607082833c94a700f96e9)](https://circleci.com/gh/rai200890/python_google_cloud_logger) 4 | [![PyPI version](https://badge.fury.io/py/google-cloud-logger.svg)](https://badge.fury.io/py/google-cloud-logger) 5 | [![Maintainability](https://api.codeclimate.com/v1/badges/e988f26e1590a6591d96/maintainability)](https://codeclimate.com/github/rai200890/python_google_cloud_logger/maintainability) 6 | [![Test Coverage](https://api.codeclimate.com/v1/badges/e988f26e1590a6591d96/test_coverage)](https://codeclimate.com/github/rai200890/python_google_cloud_logger/test_coverage) 7 | 8 | Python log formatter for Google Cloud according to [v2 specification](https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry) using [python-json-logger](https://github.com/madzak/python-json-logger) formatter 9 | 10 | Inspired by Elixir's [logger_json](https://github.com/Nebo15/logger_json) 11 | 12 | ## Instalation 13 | 14 | ### Pipenv 15 | 16 | ``` 17 | pipenv install google_cloud_logger 18 | ``` 19 | 20 | ### Pip 21 | 22 | ``` 23 | pip install google_cloud_logger 24 | ``` 25 | 26 | ## Usage 27 | 28 | ```python 29 | LOG_CONFIG = { 30 | "version": 1, 31 | "formatters": { 32 | "json": { 33 | "()": "google_cloud_logger.GoogleCloudFormatter", 34 | "application_info": { 35 | "type": "python-application", 36 | "name": "Example Application" 37 | }, 38 | "format": "[%(asctime)s] %(levelname)s in %(module)s: %(message)s" 39 | } 40 | }, 41 | "handlers": { 42 | "json": { 43 | "class": "logging.StreamHandler", 44 | "formatter": "json" 45 | } 46 | }, 47 | "loggers": { 48 | "root": { 49 | "level": "INFO", 50 | "handlers": ["json"] 51 | } 52 | } 53 | } 54 | import logging 55 | 56 | from logging import config 57 | 58 | config.dictConfig(LOG_CONFIG) # load log config from dict 59 | 60 | logger = logging.getLogger("root") # get root logger instance 61 | 62 | 63 | logger.info("farofa", extra={"extra": "extra"}) # log message with extra arguments 64 | ``` 65 | 66 | Example output: 67 | 68 | ```json 69 | {"timestamp": "2018-11-03T22:05:03.818000Z", "severity": "INFO", "message": "farofa", "labels": {"type": "python-application", "name": "Example Application"}, "metadata": {"userLabels": {"extra": "extra"}}, "sourceLocation": {"file": "", "line": 1, "function": ""}} 70 | ``` 71 | 72 | ## Credits 73 | 74 | Thanks [@thulio](https://github.com/thulio), [@robsonpeixoto](https://github.com/robsonpeixoto), [@ramondelemos](https://github.com/ramondelemos) -------------------------------------------------------------------------------- /google_cloud_logger/__init__.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | import traceback 3 | from datetime import datetime 4 | from io import StringIO 5 | 6 | from pythonjsonlogger.jsonlogger import JsonFormatter 7 | 8 | 9 | class GoogleCloudFormatter(JsonFormatter): 10 | """ 11 | Log Formatter according to Google Cloud v2 Specification: 12 | https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry 13 | """ 14 | 15 | def __init__(self, *args, **kwargs): 16 | self.application_info = kwargs.pop("application_info", {}) 17 | super(GoogleCloudFormatter, self).__init__(*args, **kwargs) 18 | 19 | def _get_extra_fields(self, record): 20 | if hasattr(record, "extra"): 21 | return record.extra 22 | attributes = (field for field in record.__dict__.keys() 23 | if not inspect.ismethod(field)) 24 | 25 | fields = set(attributes).difference(set(self.reserved_attrs.keys())) 26 | return {key: getattr(record, key) for key in fields if key} 27 | 28 | def add_fields(self, log_record, record, _message_dict): 29 | entry = self.make_entry(record) 30 | for key, value in entry.items(): 31 | log_record[key] = value 32 | 33 | def make_labels(self): 34 | return self.application_info 35 | 36 | def make_user_labels(self, record): 37 | return self._get_extra_fields(record) 38 | 39 | def make_entry(self, record): 40 | return { 41 | "timestamp": self.format_timestamp(record.asctime), 42 | "severity": self.format_severity(record.levelname), 43 | "message": record.getMessage(), 44 | "labels": self.make_labels(), 45 | "metadata": self.make_metadata(record), 46 | "sourceLocation": self.make_source_location(record), 47 | } 48 | 49 | def format_timestamp(self, asctime): 50 | datetime_format = "%Y-%m-%d %H:%M:%S,%f" 51 | return datetime.strptime(asctime, datetime_format).isoformat("T") + "Z" 52 | 53 | def format_severity(self, level_name): 54 | levels = { 55 | "DEFAULT": "NOTSET", 56 | "CRITICAL": "CRITICAL", 57 | "ERROR": "ERROR", 58 | "WARNING": "WARNING", 59 | "INFO": "INFO", 60 | "DEBUG": "DEBUG", 61 | } 62 | return levels[level_name.upper()] 63 | 64 | def make_exception(self, record): 65 | with StringIO() as buf: 66 | exception_info = record.exc_info 67 | traceback.print_tb(exception_info[2], file=buf) 68 | return { 69 | "class": record.exc_info[0], 70 | "message": record.exc_info[1], 71 | "traceback": buf.getvalue(), 72 | } 73 | 74 | def make_metadata(self, record): 75 | if getattr(record, "exc_info", None): 76 | return { 77 | "userLabels": self.make_user_labels(record), 78 | "exception": self.make_exception(record), 79 | } 80 | return {"userLabels": self.make_user_labels(record)} 81 | 82 | def make_source_location(self, record): 83 | return { 84 | "file": record.filename, 85 | "line": record.lineno, 86 | "function": record.funcName, 87 | } 88 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | addopts = -v -s --cov=google_cloud_logger --cov-report html --cov-report term --cov-report=xml -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | with open("README.md", "r") as output: 4 | long_description = output.read() 5 | 6 | __VERSION__ = "0.2.1" 7 | 8 | setup( 9 | name="google_cloud_logger", 10 | version=__VERSION__, 11 | description="Google Cloud Logger Formatter", 12 | long_description=long_description, 13 | long_description_content_type="text/markdown", 14 | url="http://github.com/rai200890/python_google_cloud_logger", 15 | author="Raissa Ferreira", 16 | author_email="rai200890@gmail.com", 17 | license="MIT", 18 | packages=find_packages(), 19 | python_requires=">=3.4.*", 20 | install_requires=["python-json-logger>=0.1.10"], 21 | classifiers=[ 22 | "Environment :: Web Environment", "Intended Audience :: Developers", 23 | "License :: OSI Approved :: MIT License", 24 | "Natural Language :: English", "Operating System :: OS Independent", 25 | "Programming Language :: Python :: 3 :: Only", 26 | "Topic :: System :: Logging" 27 | ], 28 | zip_safe=False) 29 | -------------------------------------------------------------------------------- /test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rai200890/python_google_cloud_logger/8431da979cffbec6df9c94ee7a10da608a3703aa/test/__init__.py -------------------------------------------------------------------------------- /test/google_cloud_logger/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rai200890/python_google_cloud_logger/8431da979cffbec6df9c94ee7a10da608a3703aa/test/google_cloud_logger/__init__.py -------------------------------------------------------------------------------- /test/google_cloud_logger/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | class MockLogRecord(object): 5 | def __init__(self, args={}, **kwargs): 6 | merged = {**args, **kwargs} 7 | for field, value in merged.items(): 8 | setattr(self, field, value) 9 | 10 | 11 | @pytest.fixture 12 | def log_record_factory(): 13 | def build_log_record(**args): 14 | return MockLogRecord(**args) 15 | 16 | return build_log_record 17 | -------------------------------------------------------------------------------- /test/google_cloud_logger/test_google_cloud_formatter.py: -------------------------------------------------------------------------------- 1 | from collections import OrderedDict 2 | 3 | import pytest 4 | from google_cloud_logger import GoogleCloudFormatter 5 | 6 | 7 | # from https://stackoverflow.com/a/19258720 8 | class FakeCode(object): 9 | def __init__(self, co_filename, co_name): 10 | self.co_filename = co_filename 11 | self.co_name = co_name 12 | 13 | 14 | class FakeFrame(object): 15 | def __init__(self, f_code, f_globals): 16 | self.f_code = f_code 17 | self.f_globals = f_globals 18 | 19 | 20 | class FakeTraceback(object): 21 | def __init__(self, frames, line_nums): 22 | if len(frames) != len(line_nums): 23 | raise ValueError("Ya messed up!") 24 | self._frames = frames 25 | self._line_nums = line_nums 26 | self.tb_frame = frames[0] 27 | self.tb_lineno = line_nums[0] 28 | 29 | @property 30 | def tb_next(self): 31 | if len(self._frames) > 1: 32 | return FakeTraceback(self._frames[1:], self._line_nums[1:]) 33 | 34 | 35 | @pytest.fixture 36 | def formatter(): 37 | return GoogleCloudFormatter( 38 | "[%(asctime)s] %(levelname)s in %(module)s: %(message)s", 39 | application_info={"type": "python-application"}, 40 | ) 41 | 42 | 43 | @pytest.fixture 44 | def record(log_record_factory, mocker): 45 | data = { 46 | "asctime": "2018-08-30 20:40:57,245", 47 | "filename": "_internal.py", 48 | "funcName": "_log", 49 | "lineno": "88", 50 | "levelname": "WARNING", 51 | "message": "farofa", 52 | "extra_field": "extra", 53 | } 54 | record = log_record_factory(**data) 55 | record.getMessage = mocker.Mock(return_value=data["message"]) 56 | return record 57 | 58 | 59 | @pytest.fixture 60 | def record_with_extra_attribute(log_record_factory, mocker): 61 | data = { 62 | "asctime": "2018-08-30 20:40:57,245", 63 | "filename": "_internal.py", 64 | "funcName": "_log", 65 | "lineno": "88", 66 | "levelname": "WARNING", 67 | "message": "farofa", 68 | "extra": { 69 | "extra_field": "extra" 70 | }, 71 | } 72 | record = log_record_factory(**data) 73 | record.getMessage = mocker.Mock(return_value=data["message"]) 74 | return record 75 | 76 | 77 | @pytest.fixture 78 | def record_with_exception(log_record_factory, mocker): 79 | code = FakeCode("module.py", "function") 80 | frame = FakeFrame(code, {}) 81 | traceback = FakeTraceback([frame], [1]) 82 | data = { 83 | "asctime": "2018-08-30 20:40:57,245", 84 | "filename": "_internal.py", 85 | "funcName": "_log", 86 | "lineno": "88", 87 | "levelname": "WARNING", 88 | "message": "farofa", 89 | "exc_info": (Exception, "ERROR", traceback), 90 | } 91 | record = log_record_factory(**data) 92 | record.getMessage = mocker.Mock(return_value=data["message"]) 93 | return record 94 | 95 | 96 | def test_add_fields(formatter, record, mocker): 97 | log_record = OrderedDict({}) 98 | mocker.patch.object( 99 | formatter, 100 | "make_entry", 101 | return_value=OrderedDict([ 102 | ("timestamp", "2018-08-30 20:40:57Z"), 103 | ("severity", "WARNING"), 104 | ("message", "farofa"), 105 | ("labels", { 106 | "type": "python-application" 107 | }), 108 | ("metadata", { 109 | "userLabels": {} 110 | }), 111 | ( 112 | "sourceLocation", 113 | { 114 | "file": "_internal.py", 115 | "function": "_log", 116 | "line": "88" 117 | }, 118 | ), 119 | ]), 120 | ) 121 | formatter.add_fields(log_record, record, {}) 122 | 123 | assert log_record == formatter.make_entry.return_value 124 | 125 | 126 | def test_make_entry(formatter, record): 127 | entry = formatter.make_entry(record) 128 | 129 | assert entry["timestamp"] == "2018-08-30T20:40:57.245000Z" 130 | assert entry["severity"] == "WARNING" 131 | assert entry["message"] == "farofa" 132 | assert entry["metadata"]["userLabels"]["extra_field"] == "extra" 133 | assert entry["labels"] == {"type": "python-application"} 134 | assert entry["sourceLocation"] == { 135 | "file": "_internal.py", 136 | "function": "_log", 137 | "line": "88", 138 | } 139 | 140 | 141 | def test_make_labels(formatter): 142 | labels = formatter.make_labels() 143 | 144 | assert labels == {"type": "python-application"} 145 | 146 | 147 | def test_make_metadata(formatter, record): 148 | metadata = formatter.make_metadata(record) 149 | 150 | assert metadata["userLabels"]["extra_field"] == "extra" 151 | 152 | 153 | def test_make_metadata_with_extra_attribute(formatter, 154 | record_with_extra_attribute): 155 | metadata = formatter.make_metadata(record_with_extra_attribute) 156 | 157 | assert metadata["userLabels"]["extra_field"] == "extra" 158 | 159 | 160 | def test_make_metadata_with_exception(formatter, record_with_exception): 161 | metadata = formatter.make_metadata(record_with_exception) 162 | 163 | assert metadata["exception"] == { 164 | "class": Exception().__class__, 165 | "message": "ERROR", 166 | "traceback": ' File "module.py", line 1, in function\n', 167 | } 168 | 169 | 170 | def test_make_source_location(formatter, record): 171 | assert formatter.make_source_location(record) == { 172 | "file": "_internal.py", 173 | "function": "_log", 174 | "line": "88", 175 | } 176 | 177 | 178 | def test_format_timestamp(formatter): 179 | assert (formatter.format_timestamp("2018-08-30 20:40:57,245") == 180 | "2018-08-30T20:40:57.245000Z") 181 | 182 | 183 | @pytest.mark.parametrize( 184 | "python_level, expected_level", 185 | [ 186 | ("DEFAULT", "NOTSET"), 187 | ("CRITICAL", "CRITICAL"), 188 | ("ERROR", "ERROR"), 189 | ("WARNING", "WARNING"), 190 | ("INFO", "INFO"), 191 | ("DEBUG", "DEBUG"), 192 | ], 193 | ) 194 | def test_format_severity(formatter, python_level, expected_level): 195 | assert formatter.format_severity(python_level) == expected_level 196 | --------------------------------------------------------------------------------