Skip to content

Commit

Permalink
feature: Move extra fields to userLabels metadata (#1)
Browse files Browse the repository at this point in the history
* feature: Move extra fields to userLabels metadata

* chore: Bump lib version to 0.1.0

* docs: Add usage example to Readme
  • Loading branch information
rai200890 authored Nov 4, 2018
1 parent 99a8bee commit d280bae
Show file tree
Hide file tree
Showing 6 changed files with 135 additions and 40 deletions.
21 changes: 11 additions & 10 deletions Pipfile.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

65 changes: 64 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,72 @@
# python_google_cloud_logger

[![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)
[![PyPI version](https://badge.fury.io/py/google-cloud-logger.svg)](https://badge.fury.io/py/google-cloud-logger)
[![Maintainability](https://api.codeclimate.com/v1/badges/e988f26e1590a6591d96/maintainability)](https://codeclimate.com/github/rai200890/python_google_cloud_logger/maintainability)

Python log formatter for Google Cloud
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

Inspired by Elixir's [logger_json](https://github.com/Nebo15/logger_json)

## Instalation

### Pipenv

```
pipenv install google_cloud_logger
```

### Pip

```
pip install google_cloud_logger
```

## Usage

```python
LOG_CONFIG = {
"version": 1,
"formatters": {
"json": {
"()": "google_cloud_logger.GoogleCloudFormatter",
"application_info": {
"type": "python-application",
"name": "Example Application"
},
"format": "[%(asctime)s] %(levelname)s in %(module)s: %(message)s"
}
},
"handlers": {
"json": {
"class": "logging.StreamHandler",
"formatter": "json"
}
},
"loggers": {
"root": {
"level": "INFO",
"handlers": ["json"]
}
}
}
import logging

from logging import config

config.dictConfig(LOG_CONFIG) # load log config from dict

logger = logging.getLogger("root") # get root logger instance


logger.info("farofa", extra={"extra": "extra"}) # log message with extra arguments
```

Example output:

```json
{"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>"}}
```

## Credits

Expand Down
23 changes: 15 additions & 8 deletions google_cloud_logger/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from datetime import datetime
import inspect

from pythonjsonlogger.jsonlogger import JsonFormatter

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

def _get_extra_fields(self, record):
fields = set(field for field in record.__dict__.keys()
if not inspect.ismethod(field)).difference(
set(self.reserved_attrs.keys()))
return {key: getattr(record, key) for key in fields if key}

def add_fields(self, log_record, record, _message_dict):
entry = self.make_entry(record)
for key, value in entry.items():
log_record[key] = value

def make_labels(self, record):
fields = set(record.__dict__.keys()).difference(
set(self.reserved_attrs.keys()))
extra = {key: getattr(record, key) for key in fields}
return {**self.application_info, **extra}
def make_labels(self):
return self.application_info

def make_user_labels(self, record):
return self._get_extra_fields(record)

def make_entry(self, record):
return {
"timestamp": self.format_timestamp(record.asctime),
"severity": self.format_severity(record.levelname),
"message": record.getMessage(),
"labels": self.make_labels(),
"metadata": self.make_metadata(record),
"labels": self.make_labels(record),
"sourceLocation": self.make_source_location(record)
}

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

def make_metadata(self, _record):
return {"userLabels": None}
def make_metadata(self, record):
return {"userLabels": self.make_user_labels(record)}

def make_source_location(self, record):
return {
Expand Down
10 changes: 5 additions & 5 deletions setup.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
from setuptools import setup

__VERSION__ = "0.1.0"

setup(
name="google_cloud_logger",
version="0.0.2",
version=__VERSION__,
description="Google Cloud Logger Formatter",
url="http://github.com/rai200890/python_google_cloud_logger",
author="Raissa Ferreira",
Expand All @@ -14,11 +16,9 @@
"python-json-logger>=v0.1.5",
],
classifiers=[
"Environment :: Web Environment",
"Intended Audience :: Developers",
"Environment :: Web Environment", "Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Natural Language :: English",
"Operating System :: OS Independent",
"Natural Language :: English", "Operating System :: OS Independent",
"Programming Language :: Python :: 3 :: Only",
"Topic :: System :: Logging"
],
Expand Down
16 changes: 16 additions & 0 deletions test/google_cloud_logger/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import pytest


class MockLogRecord(object):
def __init__(self, args={}, **kwargs):
merged = {**args, **kwargs}
for field, value in merged.items():
setattr(self, field, value)


@pytest.fixture
def log_record_factory():
def build_log_record(**args):
return MockLogRecord(**args)

return build_log_record
40 changes: 24 additions & 16 deletions test/google_cloud_logger/test_google_cloud_formatter.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,19 @@ def formatter():


@pytest.fixture
def record(mocker):
return mocker.Mock(
asctime="2018-08-30 20:40:57,245",
filename="_internal.py",
funcName="_log",
lineno="88",
levelname="WARNING",
getMessage=lambda: "farofa")
def record(log_record_factory, mocker):
data = {
"asctime": "2018-08-30 20:40:57,245",
"filename": "_internal.py",
"funcName": "_log",
"lineno": "88",
"levelname": "WARNING",
"message": "farofa",
"extra_field": "extra"
}
record = log_record_factory(**data)
record.getMessage = mocker.Mock(return_value=data["message"])
return record


def test_add_fields(formatter, record, mocker):
Expand All @@ -31,9 +36,10 @@ def test_add_fields(formatter, record, mocker):
return_value=OrderedDict([("timestamp", "2018-08-30 20:40:57Z"),
("severity", "WARNING"), ("message",
"farofa"),
("metadata", None),
("labels", {
"extra": "extra_args"
"type": "python-application"
}), ("metadata", {
"userLabels": {}
}),
("sourceLocation", {
"file": "_internal.py",
Expand All @@ -51,23 +57,25 @@ def test_make_entry(formatter, record):
assert entry["timestamp"] == "2018-08-30T20:40:57.245000Z"
assert entry["severity"] == "WARNING"
assert entry["message"] == "farofa"
assert entry["metadata"] == {"userLabels": None}
assert entry["labels"] is not None
assert entry["metadata"]["userLabels"]["extra_field"] == "extra"
assert entry["labels"] == {"type": "python-application"}
assert entry["sourceLocation"] == {
"file": "_internal.py",
"function": "_log",
"line": "88"
}


def test_make_labels(formatter, record):
labels = formatter.make_labels(record)
def test_make_labels(formatter):
labels = formatter.make_labels()

assert labels["type"] == "python-application"
assert labels == {"type": "python-application"}


def test_make_metadata(formatter, record):
assert formatter.make_metadata(record) == {"userLabels": None}
metadata = formatter.make_metadata(record)

assert metadata["userLabels"]["extra_field"] == "extra"


def test_make_source_location(formatter, record):
Expand Down

0 comments on commit d280bae

Please sign in to comment.