Skip to content

Commit

Permalink
Slack logger (#29)
Browse files Browse the repository at this point in the history
* add slack logger

* bump version to 0.0.8

* check against more modern versions of python

* also change django versions to test against

* tests passing locally

* bump versions

* fix test paths

* fix py3.1 version in github tests ?? 3.10.5 to be explicit

* add pytz since it was removed in django 4
  • Loading branch information
ckcollab authored Jul 16, 2022
1 parent 4015834 commit e57961d
Show file tree
Hide file tree
Showing 8 changed files with 168 additions and 15 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/pythonpackage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [3.6, 3.7, 3.8]
django-version: ['<3', '>=3']
python-version: [3.8, 3.9, 3.10.5]
django-version: ['<4', '>=4']

steps:
- uses: actions/checkout@v2
Expand Down
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM python:3.9.2
FROM python:3.10.5

# Spatial packages and such
RUN apt-get update && apt-get install -y libgdal-dev libsqlite3-mod-spatialite
Expand Down
53 changes: 53 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,59 @@ class WhateverTest(TestCase):
```
#### Slack logging
Get a Slack webhook URL and set `SLACK_WEBHOOK_URL` env var. You can also set `DJANGO_SLACK_LOG_LEVEL`
with info, warning, etc.
Modify your Celery settings:
```py
# Let our slack logger handle celery stuff
CELERY_WORKER_HIJACK_ROOT_LOGGER = False
```
Example `LOGGING` configuration that turns on Slack logging if `SLACK_WEBHOOK_URL` env var is found:
```py
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'formatters': {
'colored': {
'()': 'colorlog.ColoredFormatter',
'format': "%(log_color)s%(levelname)-8s%(reset)s %(white)s%(message)s",
}
},
'handlers': {
'console': {
'class': 'logging.StreamHandler',
'formatter': 'colored',
},
},
'loggers': {
'': {
'handlers': ['console'],
'level': os.getenv('DJANGO_LOG_LEVEL', 'INFO'),
},
'django': {
'handlers': ['console'],
'level': os.getenv('DJANGO_LOG_LEVEL', 'INFO'),
'propagate': False,
}
},
}
SLACK_WEBHOOK_URL = os.getenv('SLACK_WEBHOOK_URL', '')
if SLACK_WEBHOOK_URL:
LOGGING['handlers']['slack'] = {
'class': 'ckc.logging.CkcSlackHandler',
'level': os.getenv('DJANGO_SLACK_LOG_LEVEL', 'ERROR'),
}
LOGGING['loggers']['django']['handlers'] = ['console', 'slack']
LOGGING['loggers']['']['handlers'] = ['console', 'slack']
```
#### `./manage.py` commands
| command | description|
Expand Down
2 changes: 2 additions & 0 deletions ckc/fields.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from collections import OrderedDict

from rest_framework import serializers


Expand Down
98 changes: 98 additions & 0 deletions ckc/logging.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
"""
Most of this Slack logging is pulled from here:
https://github.com/junhwi/python-slack-logger/
"""
import os
import logging
import json

from logging import LogRecord
from urllib.parse import urlparse
from logging.handlers import HTTPHandler


class SlackHandler(HTTPHandler):
def __init__(self, url, username=None, icon_url=None, icon_emoji=None, channel=None, mention=None):
o = urlparse(url)
is_secure = o.scheme == 'https'
HTTPHandler.__init__(self, o.netloc, o.path, method="POST", secure=is_secure)
self.username = username
self.icon_url = icon_url
self.icon_emoji = icon_emoji
self.channel = channel
self.mention = mention and mention.lstrip('@')

def mapLogRecord(self, record):
text = self.format(record)

if isinstance(self.formatter, SlackFormatter):
payload = {
'attachments': [
text,
],
}
if self.mention:
payload['text'] = '<@{0}>'.format(self.mention)
else:
if self.mention:
text = '<@{0}> {1}'.format(self.mention, text)
payload = {
'text': text,
}

if self.username:
payload['username'] = self.username
if self.icon_url:
payload['icon_url'] = self.icon_url
if self.icon_emoji:
payload['icon_emoji'] = self.icon_emoji
if self.channel:
payload['channel'] = self.channel

ret = {
'payload': json.dumps(payload),
}
return ret


class SlackFormatter(logging.Formatter):
def format(self, record):
ret = {}
if record.levelname == 'INFO':
ret['color'] = 'good'
elif record.levelname == 'WARNING':
ret['color'] = 'warning'
elif record.levelname == 'ERROR':
ret['color'] = '#E91E63'
elif record.levelname == 'CRITICAL':
ret['color'] = 'danger'

ret['author_name'] = record.levelname
ret['title'] = record.name
ret['ts'] = record.created
ret['text'] = super(SlackFormatter, self).format(record)
return ret


class SlackLogFilter(logging.Filter):
"""
Logging filter to decide when logging to Slack is requested, using
the `extra` kwargs:
`logger.info("...", extra={'notify_slack': True})`
"""

def filter(self, record):
return getattr(record, 'notify_slack', False)


class CkcSlackHandler(SlackHandler):
"""
Override the default handler to insert our own URL
"""
def __init__(self, **kwargs):
url = os.getenv('SLACK_WEBHOOK_URL')
super().__init__(url, **kwargs)

def format(self, record: LogRecord) -> str:
"""Surround our log message in a "code block" for styling."""
return f"```{super().format(record)}```"
16 changes: 8 additions & 8 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
# These requirements are for local development and testing of the module

# python packaging
twine==3.1.1
twine==4.0.1

# django stuff
Django==3.1.12
djangorestframework==3.12.4
Django==4.0.6
djangorestframework==3.13.1
pytz==2022.1

# factories
factory-boy==3.2.0
factory-boy==3.2.1

# tests
pytest==5.4.1
pytest-django==3.9.0
pytest-pythonpath==0.7.3
flake8==3.7.9
pytest==7.1.2
pytest-django==4.5.2
flake8==4.0.1
6 changes: 2 additions & 4 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -27,19 +27,17 @@ per-file-ignores =
[tool:pytest]
addopts = --reuse-db
DJANGO_SETTINGS_MODULE = testproject.settings
python_paths = testproject
pythonpath = . testproject
python_files =
tests/integration/*.py
tests/functional/*.py
test_paths =
tests/

[metadata]
name = django-ckc
author = Eric Carmichael
author_email = [email protected]
description = tools, utilities, etc. we use across projects @ ckc
version = 0.0.7
version = 0.0.8
url = https://github.com/ckcollab/django-ckc
keywords =
django
Expand Down
2 changes: 2 additions & 0 deletions testproject/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

DEBUG = True

USE_TZ = True

BASE_DIR = os.path.dirname(__file__)

# NOTE: We're using Geospatial sqlite jazz
Expand Down

0 comments on commit e57961d

Please sign in to comment.