Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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