Skip to content

Commit d280bae

Browse files
authored
feature: Move extra fields to userLabels metadata (#1)
* feature: Move extra fields to userLabels metadata * chore: Bump lib version to 0.1.0 * docs: Add usage example to Readme
1 parent 99a8bee commit d280bae

File tree

6 files changed

+135
-40
lines changed

6 files changed

+135
-40
lines changed

Pipfile.lock

Lines changed: 11 additions & 10 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

README.md

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,72 @@
11
# python_google_cloud_logger
22

33
[![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)
45
[![Maintainability](https://api.codeclimate.com/v1/badges/e988f26e1590a6591d96/maintainability)](https://codeclimate.com/github/rai200890/python_google_cloud_logger/maintainability)
56

6-
Python log formatter for Google Cloud
7+
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
8+
9+
Inspired by Elixir's [logger_json](https://github.com/Nebo15/logger_json)
10+
11+
## Instalation
12+
13+
### Pipenv
14+
15+
```
16+
pipenv install google_cloud_logger
17+
```
18+
19+
### Pip
20+
21+
```
22+
pip install google_cloud_logger
23+
```
24+
25+
## Usage
26+
27+
```python
28+
LOG_CONFIG = {
29+
"version": 1,
30+
"formatters": {
31+
"json": {
32+
"()": "google_cloud_logger.GoogleCloudFormatter",
33+
"application_info": {
34+
"type": "python-application",
35+
"name": "Example Application"
36+
},
37+
"format": "[%(asctime)s] %(levelname)s in %(module)s: %(message)s"
38+
}
39+
},
40+
"handlers": {
41+
"json": {
42+
"class": "logging.StreamHandler",
43+
"formatter": "json"
44+
}
45+
},
46+
"loggers": {
47+
"root": {
48+
"level": "INFO",
49+
"handlers": ["json"]
50+
}
51+
}
52+
}
53+
import logging
54+
55+
from logging import config
56+
57+
config.dictConfig(LOG_CONFIG) # load log config from dict
58+
59+
logger = logging.getLogger("root") # get root logger instance
60+
61+
62+
logger.info("farofa", extra={"extra": "extra"}) # log message with extra arguments
63+
```
64+
65+
Example output:
66+
67+
```json
68+
{"timestamp": "2018-11-03T22:05:03.818000Z", "severity": "INFO", "message": "farofa", "labels": {"type": "python-application", "name": "Example Application"}, "metadata": {"userLabels": {"extra": "extra"}}, "sourceLocation": {"file": "<ipython-input-9-8e9384d78e2a>", "line": 1, "function": "<module>"}}
69+
```
770

871
## Credits
972

google_cloud_logger/__init__.py

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from datetime import datetime
2+
import inspect
23

34
from pythonjsonlogger.jsonlogger import JsonFormatter
45

@@ -13,24 +14,30 @@ def __init__(self, *args, **kwargs):
1314
self.application_info = kwargs.pop("application_info", {})
1415
super(GoogleCloudFormatter, self).__init__(*args, **kwargs)
1516

17+
def _get_extra_fields(self, record):
18+
fields = set(field for field in record.__dict__.keys()
19+
if not inspect.ismethod(field)).difference(
20+
set(self.reserved_attrs.keys()))
21+
return {key: getattr(record, key) for key in fields if key}
22+
1623
def add_fields(self, log_record, record, _message_dict):
1724
entry = self.make_entry(record)
1825
for key, value in entry.items():
1926
log_record[key] = value
2027

21-
def make_labels(self, record):
22-
fields = set(record.__dict__.keys()).difference(
23-
set(self.reserved_attrs.keys()))
24-
extra = {key: getattr(record, key) for key in fields}
25-
return {**self.application_info, **extra}
28+
def make_labels(self):
29+
return self.application_info
30+
31+
def make_user_labels(self, record):
32+
return self._get_extra_fields(record)
2633

2734
def make_entry(self, record):
2835
return {
2936
"timestamp": self.format_timestamp(record.asctime),
3037
"severity": self.format_severity(record.levelname),
3138
"message": record.getMessage(),
39+
"labels": self.make_labels(),
3240
"metadata": self.make_metadata(record),
33-
"labels": self.make_labels(record),
3441
"sourceLocation": self.make_source_location(record)
3542
}
3643

@@ -49,8 +56,8 @@ def format_severity(self, level_name):
4956
}
5057
return levels[level_name.upper()]
5158

52-
def make_metadata(self, _record):
53-
return {"userLabels": None}
59+
def make_metadata(self, record):
60+
return {"userLabels": self.make_user_labels(record)}
5461

5562
def make_source_location(self, record):
5663
return {

setup.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
from setuptools import setup
22

3+
__VERSION__ = "0.1.0"
4+
35
setup(
46
name="google_cloud_logger",
5-
version="0.0.2",
7+
version=__VERSION__,
68
description="Google Cloud Logger Formatter",
79
url="http://github.com/rai200890/python_google_cloud_logger",
810
author="Raissa Ferreira",
@@ -14,11 +16,9 @@
1416
"python-json-logger>=v0.1.5",
1517
],
1618
classifiers=[
17-
"Environment :: Web Environment",
18-
"Intended Audience :: Developers",
19+
"Environment :: Web Environment", "Intended Audience :: Developers",
1920
"License :: OSI Approved :: MIT License",
20-
"Natural Language :: English",
21-
"Operating System :: OS Independent",
21+
"Natural Language :: English", "Operating System :: OS Independent",
2222
"Programming Language :: Python :: 3 :: Only",
2323
"Topic :: System :: Logging"
2424
],

test/google_cloud_logger/conftest.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
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

test/google_cloud_logger/test_google_cloud_formatter.py

Lines changed: 24 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,19 @@ def formatter():
1313

1414

1515
@pytest.fixture
16-
def record(mocker):
17-
return mocker.Mock(
18-
asctime="2018-08-30 20:40:57,245",
19-
filename="_internal.py",
20-
funcName="_log",
21-
lineno="88",
22-
levelname="WARNING",
23-
getMessage=lambda: "farofa")
16+
def record(log_record_factory, mocker):
17+
data = {
18+
"asctime": "2018-08-30 20:40:57,245",
19+
"filename": "_internal.py",
20+
"funcName": "_log",
21+
"lineno": "88",
22+
"levelname": "WARNING",
23+
"message": "farofa",
24+
"extra_field": "extra"
25+
}
26+
record = log_record_factory(**data)
27+
record.getMessage = mocker.Mock(return_value=data["message"])
28+
return record
2429

2530

2631
def test_add_fields(formatter, record, mocker):
@@ -31,9 +36,10 @@ def test_add_fields(formatter, record, mocker):
3136
return_value=OrderedDict([("timestamp", "2018-08-30 20:40:57Z"),
3237
("severity", "WARNING"), ("message",
3338
"farofa"),
34-
("metadata", None),
3539
("labels", {
36-
"extra": "extra_args"
40+
"type": "python-application"
41+
}), ("metadata", {
42+
"userLabels": {}
3743
}),
3844
("sourceLocation", {
3945
"file": "_internal.py",
@@ -51,23 +57,25 @@ def test_make_entry(formatter, record):
5157
assert entry["timestamp"] == "2018-08-30T20:40:57.245000Z"
5258
assert entry["severity"] == "WARNING"
5359
assert entry["message"] == "farofa"
54-
assert entry["metadata"] == {"userLabels": None}
55-
assert entry["labels"] is not None
60+
assert entry["metadata"]["userLabels"]["extra_field"] == "extra"
61+
assert entry["labels"] == {"type": "python-application"}
5662
assert entry["sourceLocation"] == {
5763
"file": "_internal.py",
5864
"function": "_log",
5965
"line": "88"
6066
}
6167

6268

63-
def test_make_labels(formatter, record):
64-
labels = formatter.make_labels(record)
69+
def test_make_labels(formatter):
70+
labels = formatter.make_labels()
6571

66-
assert labels["type"] == "python-application"
72+
assert labels == {"type": "python-application"}
6773

6874

6975
def test_make_metadata(formatter, record):
70-
assert formatter.make_metadata(record) == {"userLabels": None}
76+
metadata = formatter.make_metadata(record)
77+
78+
assert metadata["userLabels"]["extra_field"] == "extra"
7179

7280

7381
def test_make_source_location(formatter, record):

0 commit comments

Comments
 (0)