Skip to content

Commit e57961d

Browse files
authored
Slack logger (#29)
* 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
1 parent 4015834 commit e57961d

File tree

8 files changed

+168
-15
lines changed

8 files changed

+168
-15
lines changed

.github/workflows/pythonpackage.yml

+2-2
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ jobs:
1010
runs-on: ubuntu-latest
1111
strategy:
1212
matrix:
13-
python-version: [3.6, 3.7, 3.8]
14-
django-version: ['<3', '>=3']
13+
python-version: [3.8, 3.9, 3.10.5]
14+
django-version: ['<4', '>=4']
1515

1616
steps:
1717
- uses: actions/checkout@v2

Dockerfile

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
FROM python:3.9.2
1+
FROM python:3.10.5
22

33
# Spatial packages and such
44
RUN apt-get update && apt-get install -y libgdal-dev libsqlite3-mod-spatialite

README.md

+53
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,59 @@ class WhateverTest(TestCase):
107107
```
108108
109109
110+
#### Slack logging
111+
112+
Get a Slack webhook URL and set `SLACK_WEBHOOK_URL` env var. You can also set `DJANGO_SLACK_LOG_LEVEL`
113+
with info, warning, etc.
114+
115+
Modify your Celery settings:
116+
```py
117+
# Let our slack logger handle celery stuff
118+
CELERY_WORKER_HIJACK_ROOT_LOGGER = False
119+
```
120+
121+
Example `LOGGING` configuration that turns on Slack logging if `SLACK_WEBHOOK_URL` env var is found:
122+
```py
123+
LOGGING = {
124+
'version': 1,
125+
'disable_existing_loggers': False,
126+
'formatters': {
127+
'colored': {
128+
'()': 'colorlog.ColoredFormatter',
129+
'format': "%(log_color)s%(levelname)-8s%(reset)s %(white)s%(message)s",
130+
}
131+
},
132+
'handlers': {
133+
'console': {
134+
'class': 'logging.StreamHandler',
135+
'formatter': 'colored',
136+
},
137+
},
138+
'loggers': {
139+
'': {
140+
'handlers': ['console'],
141+
'level': os.getenv('DJANGO_LOG_LEVEL', 'INFO'),
142+
},
143+
'django': {
144+
'handlers': ['console'],
145+
'level': os.getenv('DJANGO_LOG_LEVEL', 'INFO'),
146+
'propagate': False,
147+
}
148+
},
149+
}
150+
151+
SLACK_WEBHOOK_URL = os.getenv('SLACK_WEBHOOK_URL', '')
152+
if SLACK_WEBHOOK_URL:
153+
LOGGING['handlers']['slack'] = {
154+
'class': 'ckc.logging.CkcSlackHandler',
155+
'level': os.getenv('DJANGO_SLACK_LOG_LEVEL', 'ERROR'),
156+
}
157+
158+
LOGGING['loggers']['django']['handlers'] = ['console', 'slack']
159+
LOGGING['loggers']['']['handlers'] = ['console', 'slack']
160+
```
161+
162+
110163
#### `./manage.py` commands
111164
112165
| command | description|

ckc/fields.py

+2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from collections import OrderedDict
2+
13
from rest_framework import serializers
24

35

ckc/logging.py

+98
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
"""
2+
Most of this Slack logging is pulled from here:
3+
https://github.com/junhwi/python-slack-logger/
4+
"""
5+
import os
6+
import logging
7+
import json
8+
9+
from logging import LogRecord
10+
from urllib.parse import urlparse
11+
from logging.handlers import HTTPHandler
12+
13+
14+
class SlackHandler(HTTPHandler):
15+
def __init__(self, url, username=None, icon_url=None, icon_emoji=None, channel=None, mention=None):
16+
o = urlparse(url)
17+
is_secure = o.scheme == 'https'
18+
HTTPHandler.__init__(self, o.netloc, o.path, method="POST", secure=is_secure)
19+
self.username = username
20+
self.icon_url = icon_url
21+
self.icon_emoji = icon_emoji
22+
self.channel = channel
23+
self.mention = mention and mention.lstrip('@')
24+
25+
def mapLogRecord(self, record):
26+
text = self.format(record)
27+
28+
if isinstance(self.formatter, SlackFormatter):
29+
payload = {
30+
'attachments': [
31+
text,
32+
],
33+
}
34+
if self.mention:
35+
payload['text'] = '<@{0}>'.format(self.mention)
36+
else:
37+
if self.mention:
38+
text = '<@{0}> {1}'.format(self.mention, text)
39+
payload = {
40+
'text': text,
41+
}
42+
43+
if self.username:
44+
payload['username'] = self.username
45+
if self.icon_url:
46+
payload['icon_url'] = self.icon_url
47+
if self.icon_emoji:
48+
payload['icon_emoji'] = self.icon_emoji
49+
if self.channel:
50+
payload['channel'] = self.channel
51+
52+
ret = {
53+
'payload': json.dumps(payload),
54+
}
55+
return ret
56+
57+
58+
class SlackFormatter(logging.Formatter):
59+
def format(self, record):
60+
ret = {}
61+
if record.levelname == 'INFO':
62+
ret['color'] = 'good'
63+
elif record.levelname == 'WARNING':
64+
ret['color'] = 'warning'
65+
elif record.levelname == 'ERROR':
66+
ret['color'] = '#E91E63'
67+
elif record.levelname == 'CRITICAL':
68+
ret['color'] = 'danger'
69+
70+
ret['author_name'] = record.levelname
71+
ret['title'] = record.name
72+
ret['ts'] = record.created
73+
ret['text'] = super(SlackFormatter, self).format(record)
74+
return ret
75+
76+
77+
class SlackLogFilter(logging.Filter):
78+
"""
79+
Logging filter to decide when logging to Slack is requested, using
80+
the `extra` kwargs:
81+
`logger.info("...", extra={'notify_slack': True})`
82+
"""
83+
84+
def filter(self, record):
85+
return getattr(record, 'notify_slack', False)
86+
87+
88+
class CkcSlackHandler(SlackHandler):
89+
"""
90+
Override the default handler to insert our own URL
91+
"""
92+
def __init__(self, **kwargs):
93+
url = os.getenv('SLACK_WEBHOOK_URL')
94+
super().__init__(url, **kwargs)
95+
96+
def format(self, record: LogRecord) -> str:
97+
"""Surround our log message in a "code block" for styling."""
98+
return f"```{super().format(record)}```"

requirements.txt

+8-8
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
11
# These requirements are for local development and testing of the module
22

33
# python packaging
4-
twine==3.1.1
4+
twine==4.0.1
55

66
# django stuff
7-
Django==3.1.12
8-
djangorestframework==3.12.4
7+
Django==4.0.6
8+
djangorestframework==3.13.1
9+
pytz==2022.1
910

1011
# factories
11-
factory-boy==3.2.0
12+
factory-boy==3.2.1
1213

1314
# tests
14-
pytest==5.4.1
15-
pytest-django==3.9.0
16-
pytest-pythonpath==0.7.3
17-
flake8==3.7.9
15+
pytest==7.1.2
16+
pytest-django==4.5.2
17+
flake8==4.0.1

setup.cfg

+2-4
Original file line numberDiff line numberDiff line change
@@ -27,19 +27,17 @@ per-file-ignores =
2727
[tool:pytest]
2828
addopts = --reuse-db
2929
DJANGO_SETTINGS_MODULE = testproject.settings
30-
python_paths = testproject
30+
pythonpath = . testproject
3131
python_files =
3232
tests/integration/*.py
3333
tests/functional/*.py
34-
test_paths =
35-
tests/
3634

3735
[metadata]
3836
name = django-ckc
3937
author = Eric Carmichael
4038
author_email = [email protected]
4139
description = tools, utilities, etc. we use across projects @ ckc
42-
version = 0.0.7
40+
version = 0.0.8
4341
url = https://github.com/ckcollab/django-ckc
4442
keywords =
4543
django

testproject/settings.py

+2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33

44
DEBUG = True
55

6+
USE_TZ = True
7+
68
BASE_DIR = os.path.dirname(__file__)
79

810
# NOTE: We're using Geospatial sqlite jazz

0 commit comments

Comments
 (0)