---
# System prepended metadata

title: '[Django] Custom log handler'
tags: [Django]

---

---
tags: Django
---


# [Django] Custom log handler  

我想要在系統出錯的時候，自動發信給管理員/IT人員。

## 需求
Django提供`AdminEmailHandler`以處理Django Application發生異常時，自動寄信通知管理員。  

這裡所謂的管理員是只settings裡面`ADMIN`參數設定的資料，也代表未來要更改管理員就要動到code，當然如果有統一收信窗口就比較沒有影響。  
  
但我的情況不是這樣，我習慣用Admin site當作管理資料的GUI，superuser的角色只會給IT人員使用，所以就打算寫一個能夠從`UserModel`取得`superuser`當作收件人的log handler.  
   
## 研究

**先來瞧瞧AdminEmailHandler的寫法:**

```python=false
class AdminEmailHandler(logging.Handler):
    ...
    
    def emit(self, record):
    
        ...
    
        html_message = reporter.get_traceback_html() if self.include_html else None
        self.send_mail(subject, message, fail_silently=True, html_message=html_message)

    def send_mail(self, subject, message, *args, **kwargs):
        mail.mail_admins(subject, message, *args, connection=self.connection(), **kwargs)
        
    ...
```
  
可以發現發信的method使用內建的`mail_admins`，往下追或是官方文件也寫了，這個method裡面確實讀取的收件人email來自settings的`ADMIN`參數設定。

所以呢，可以繼承這個已經寫好的handler，再另外寫一個method覆蓋`mail_admins`就好。

## 實作

首先要有一個替代mail_admins的method。

```python=false
def mail_superusers(subject='', body='', fail_silently=False, html_message=''):
    """Send a message to the superusers whom is set to be a superuser."""
    # get superusers
    user_model = apps.get_model('base', 'User')
    superusers = user_model.objects.filter(is_superuser=True)
    # get their emails
    email_addrs = [user.email for user in superusers]
    connection = get_connection()
    mail = EmailMultiAlternatives(
                subject=subject,
                body=body,
                to=email_addrs,
                connection=connection
            )
    if html_message:
        mail.attach_alternative(html_message, 'text/html')
    mail.send(fail_silently=fail_silently)
```

這裡踩了一個小坑，User必須是**lazy import**，否則Application無法順利執行。  


再來是custom log handler，非常簡單，就只需要把send_mail覆寫就行:  

```python=false
class SuperUserEmailHandler(AdminEmailHandler):
    
    def send_mail(self, subject, message, *args, **kwargs):
        mail_superusers(subject, message, *args, **kwargs)
```

最後放到LOGGING設定裡面:  

```python=false
'mail_mis': {
    'level': 'ERROR',
    'filters': ['require_debug_false'],
    'class': 'log_handler.SuperUserEmailHandler',
    'include_html': True,
}
```

結束!  
  
但是他預設會傳送一整份degug頁面(就是DEBUG=True會看見的黃頁)
，我也沒有特別去改寫要傳送的內容，所以email內會包含很詳細的bug追蹤，是有點危險的喔。


## 寫在幾年之後

前面是一個人負責所有系統開發與維護，還不容易請到經費租用服務的情況下，土炮的做法。
其實要接 Sentry 這種程式錯誤監控服務，就不用一天到晚收錯誤信了～

## References

[Django docs - AdminEmailHandler](https://docs.djangoproject.com/en/2.2/topics/logging/#django.utils.log.AdminEmailHandler)
[Stackoverflow - How to Lazy Load a model in a managers to stop circular imports?](https://stackoverflow.com/questions/29744016/how-to-lazy-load-a-model-in-a-managers-to-stop-circular-imports)
[寫得更詳盡 - Django 打造 logging handler 將 exception 送到 Slack](https://myapollo.com.tw/zh-tw/django-customize-logging-handler/)