Skip to content

Commit

Permalink
Initial version (#2)
Browse files Browse the repository at this point in the history
  • Loading branch information
multani authored Apr 2, 2023
1 parent 44d5b96 commit 383473a
Show file tree
Hide file tree
Showing 20 changed files with 610 additions and 13 deletions.
2 changes: 2 additions & 0 deletions .flake8
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[flake8]
extend-ignore = E501
35 changes: 35 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
name: Test

on:
push:
branches:
- "**"

workflow_dispatch:

jobs:
test:
runs-on: ubuntu-latest
name: Test Python

strategy:
matrix:
python-version:
- "3.9"
- "3.10"
- "3.11"

steps:
- name: Checkout
uses: actions/checkout@v3

- name: Setup Poetry
run: pipx install hatch

- name: Setup Python
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}

- name: Test
run: hatch run pytest --color=yes
72 changes: 72 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# Google Cloud Logging formatter for structlog

This is an opiniated package that configures [structlog](https://structlog.org/)
to output log compatible with the [Google Cloud Logging log
format](https://cloud.google.com/logging/docs/structured-logging).

The intention of this package is to be used for applications that run in [Google
Kubernetes Engine (GKE)](https://cloud.google.com/kubernetes-engine/) or [Google
Cloud Function](https://cloud.google.com/functions/).

As such, the package is only concerned about **formatting logs**, where logs are
expected to be written on the standard output. Sending the logs to the actual
Google Logging API is supposed to be done by an external agent.


In particular, this package provides the following configuration by default:

* Logs are formatted as JSON using the [Google Cloud Logging log
format](https://cloud.google.com/logging/docs/structured-logging)
* The [Python standard library's
`logging`](https://docs.python.org/3/library/logging.html) log levels are
available and translated to their GCP equivalents.
* Exceptions and `CRITICAL` log messages will be reported into [Google Error
Reporting dashboard](https://cloud.google.com/error-reporting/)
* Additional logger bound arguments will be reported as `labels` in GCP.


## How to use?

Install the package with `pip` or your favorite Python package manager:

```sh
pip install structlog-gcp
```

Then, configure `structlog` as usual, using the Structlog processors the package
provides:

```python
import structlog
import structlog_gcp

gcp_logs = structlog_gcp.StructlogGCP()

structlog.configure(processors=gcp_logs.build_processors())
```

Then, you can use `structlog` as usual:

```python
logger = structlog.get_logger().bind(arg1="something")

logger.info("Hello world")

converted = False
try:
int("foobar")
converted = True
except:
logger.exception("Something bad happens")

if not converted:
logger.critical("This is not supposed to happen", converted=converted)
```



## Reference

* https://cloud.google.com/logging/docs/structured-logging
* https://cloud.google.com/error-reporting/docs/formatting-error-messages
* https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry
Binary file added docs/errors.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/logs.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
16 changes: 16 additions & 0 deletions examples/cloud-function/.gcloudignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# This file specifies files that are *not* uploaded to Google Cloud
# using gcloud. It follows the same syntax as .gitignore, with the addition of
# "#!include" directives (which insert the entries of the given .gitignore-style
# file at that point).
#
# For more information, run:
# $ gcloud topic gcloudignore
#
.gcloudignore
# If you would like to upload your .git directory, .gitignore file or files
# from your .gitignore file, remove the corresponding line
# below:
.git
.gitignore

node_modules
8 changes: 8 additions & 0 deletions examples/cloud-function/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
deploy:
gcloud functions deploy test-log \
--gen2 \
--region europe-west1 \
--runtime python310 \
--source . \
--entry-point test_func1 \
--trigger-http
71 changes: 71 additions & 0 deletions examples/cloud-function/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import logging

import functions_framework
import google.cloud.error_reporting
import google.cloud.logging
import structlog

import structlog_gcp

gcp_logs = structlog_gcp.StructlogGCP()
structlog.configure(processors=gcp_logs.build_processors())


@functions_framework.http
def test_func1(request):
"""Test the logging framework.
* `GET` the deployed URL to trigger the `structlog-gcp` behavior
* `POST` the deployed URL to trigger the official Google logging + error
reporting libraries behavior
* `DELETE` the deployed URL to crash the function and force a cold-restart
"""

if request.method == "GET":
logger = structlog.get_logger("test")

logger.debug("a debug message", foo="bar")
logger.info("an info message", foo="bar")
logger.warning("a warning message", arg="something else")

logger.error("an error message")
logger.critical("a critical message with reported error")

try:
1 / 0
except ZeroDivisionError:
logger.exception("division by zero")

try:
raise TypeError("crash")
except TypeError:
logger.exception("type error")

elif request.method == "POST":
error = google.cloud.error_reporting.Client()
google.cloud.logging.Client().setup_logging()

logging.debug("a debug message")
logging.info("an info message")
logging.warning("a warning message")

logging.error("an error message")
logging.critical("a critical message with reported error")

error.report("a reported error")

try:
1 / 0
except ZeroDivisionError:
error.report_exception()

try:
raise TypeError("crash")
except TypeError:
logging.exception("type error")

elif request.method == "DELETE":
# crash the function to force a cold restart
raise RuntimeError("restart")

return "OK"
3 changes: 3 additions & 0 deletions examples/cloud-function/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
functions-framework==3.*
google-cloud-error-reporting==1.*
structlog-gcp
45 changes: 32 additions & 13 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ requires = ["hatchling"]
build-backend = "hatchling.build"

[project]
name = "structlog-google-cloud-logging"
name = "structlog-gcp"
description = 'A structlog set of processors to output as Google Cloud Logging format'
readme = "README.md"
requires-python = ">=3.7"
Expand All @@ -15,42 +15,58 @@ authors = [
classifiers = [
"Development Status :: 4 - Beta",
"Programming Language :: Python",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: Implementation :: CPython",
"Programming Language :: Python :: Implementation :: PyPy",
]
dependencies = []
dependencies = [
"structlog",
]
dynamic = ["version"]

[project.urls]
Documentation = "https://github.com/multani/structlog-google-cloud-logging#readme"
Issues = "https://github.com/multani/structlog-google-cloud-logging/issues"
Source = "https://github.com/multani/structlog-google-cloud-logging"
Documentation = "https://github.com/multani/structlog-gcp#readme"
Issues = "https://github.com/multani/structlog-gcp/issues"
Source = "https://github.com/multani/structlog-gcp"

[tool.hatch.version]
path = "src/structlog_google_cloud_logging/__about__.py"
path = "structlog_gcp/__about__.py"

[tool.hatch.envs.default]
dependencies = [
"black",
"isort",
"pytest",
"pytest-cov",
"flake8",
]

[tool.hatch.envs.default.scripts]
cov = "pytest --cov-report=term-missing --cov-config=pyproject.toml --cov=structlog_google_cloud_logging --cov=tests {args}"
no-cov = "cov --no-cov {args}"
test = "pytest"

[[tool.hatch.envs.test.matrix]]
python = ["37", "38", "39", "310", "311"]
[[tool.hatch.envs.default.matrix]]
python = ["39", "310", "311"]

[tool.pytest.ini_options]
addopts = [
"--doctest-modules",
"--cov", "structlog_gcp",
"--cov-branch",
"--cov-report", "html",
"--cov-report", "term",
"--tb", "short",
"--verbose",
"--verbose",
]
testpaths = "tests"

[tool.coverage.run]
branch = true
parallel = true
omit = [
"structlog_google_cloud_logging/__about__.py",
"structlog_gcp/__about__.py",
]

[tool.coverage.report]
Expand All @@ -59,3 +75,6 @@ exclude_lines = [
"if __name__ == .__main__.:",
"if TYPE_CHECKING:",
]

[tool.isort]
profile = "black"
Empty file.
1 change: 1 addition & 0 deletions structlog_gcp/__about__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
__version__ = "0.0.1"
1 change: 1 addition & 0 deletions structlog_gcp/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .base import StructlogGCP # noqa: F401
19 changes: 19 additions & 0 deletions structlog_gcp/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from . import errors, processors


class StructlogGCP:
def __init__(self):
pass

def build_processors(self):
procs = []

procs.extend(processors.CoreCloudLogging().setup())
procs.extend(processors.LogSeverity().setup())
procs.extend(processors.CodeLocation().setup())
procs.extend(errors.ReportException().setup())
procs.extend(errors.ReportError(["CRITICAL"]).setup())
procs.append(errors.add_service_context)
procs.extend(processors.FormatAsCloudLogging().setup())

return procs
Loading

0 comments on commit 383473a

Please sign in to comment.