cronpi × Django Custom Commandで気楽にcronジョブ管理

この記事は[Django Advent Calendar 2019](https://qiita.com/advent-calendar/2019/django) の14日目の記事です。
初投稿だったりします。お手柔らかにお願いします。

# はじめに
毎日ワクワクしながら[本番環境でやらかしちゃった人 Advent Calendar 2019](https://qiita.com/advent-calendar/2019/yarakashi-production)を読んでいるのですが、
crontabで失敗している人が散見されているようですね。
私も日頃crontabには思うところ（毎度ヒヤヒヤしながら`crontab -e`打ったり、毎度日付の設定方法を忘れてググったり、地味に設定ファイルの管理めんどくさい）があり、なにか良い対策ないのかよーと思って探してみました。
ありました、`cronpi`です。
human readableな日付記法とpythonファイルでcronジョブ管理ができるパッケージらしいです。ありがたい...[詳しくはこちら](https://qiita.com/_dakc_/items/17bd79bdfba61f925179)

見つけたcronpiと普段お仕事で使うDjangoCustomCommandを組み合わせたら少しは楽になるのではと思い、執筆しました。

# 概要
## やりたいこと
cronpiとDjango Custom Command(以降、Commandとする)を組み合わせて使用することで、
はじめにで長々と書いた「思うところ」からいくらか開放されることをご紹介したい。
なお、今回はこんなやり方あるかもってことを紹介したいだけなので、細かいProjectの設定等や実行環境については一切言及しない。

## 手順
以下の３点で動作確認を行う。

1. cronpi設定用Commandとログ出力用Commandを作成する
2. cronpi設定用Commandを実行する[^1]
3. crontabで実行されたCommandのログを確認する

# 実践！
## 1.cronpi設定用Commandとログ出力用Commandを作成する
##### cronpi設定用Command
いい感じですね、よく順序忘れてしまう*記法使わなくて済むので。

```python:init_scheduler.py
from pathlib import Path

from django.conf import settings
from django.core.management.base import BaseCommand

import cronpi


class Command(BaseCommand):
    def handle(self, *args, **options):
        python = '/usr/local/bin/python3.7'
        manage_path = Path(settings.BASE_DIR) / 'manage.py'

        # 2019と書いてあるが、毎年12月14日23:40に実行される。
        cronpi.run_by_date(f"{python} {manage_path} hogehoge", isOverwrite=True).on('2019-12-14 23:40')
        # 毎週土曜日、月曜日の23:40に実行される
        cronpi.run_every_week(f"{python} {manage_path} uhouho", isOverwrite=True).on(['sat', 'mon'], time='23:40')
        # 毎月14日の23:40に実行される
        cronpi.run_every_month(f"{python} {manage_path} foo", isOverwrite=True).on(14, time='23:40')

        # 登録されているjob一覧を取得してくれるらしい。
        current = cronpi.get_job_list()
        self.stdout.write('\n'.join(current))
```

##### ログ出力用Command
```python:hogehoge.py
from django.core.management.base import BaseCommand
import logging

class Command(BaseCommand):
    def handle(self, *args, **options):
        logger = logging.getLogger('command')
        logger.info('hoge!hoge!')
```

```python:uhouho.py
# import  省略
class Command(BaseCommand):
    def handle(self, *args, **options):
        logger = logging.getLogger('command')
        logger.info('uhouho!')
```

```python:foo.py
# import  省略
class Command(BaseCommand):
    def handle(self, *args, **options):
        logger = logging.getLogger('command')
        logger.info('foo!bar!')
```
##### settings.pyよりloggingのみ抜粋[^2]
```python:settings.py
LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'formatters': {
        'simple': {
            'format': '%(levelname)s : %(asctime)s : %(message)s',
        }
    },
    'handlers': {
        'file_handler': {
            'level': 'INFO',
            'class': 'logging.FileHandler',
            'filename': BASE_DIR + '/myLog.log', # cronから実行する際は絶対パスでなければならない
            'formatter': 'simple'
        },
    },
    'loggers': {
        'command': {
            'handlers': ['file_handler'],
            'level': 'INFO',
        },
    }
}
```
## 2.cronpi設定用Commandを実行する

```
(my_env) ~/PycharmProjects/django3/mysite $ python manage.py init_schedule
10 23 14 12 * /usr/local/bin/python3.7 /Users/gorilla/PycharmProjects/django3/mysite/manage.py hogehoge
10 23 * * 6,1 /usr/local/bin/python3.7 /Users/gorilla/PycharmProjects/django3/mysite/manage.py uhouho
10 23 14 * * /usr/local/bin/python3.7 /Users/gorilla/PycharmProjects/django3/mysite/manage.py foo
```
## 3.crontabで実行されたCommandのログを確認する
動いてる！

```
INFO : 2019-12-14 23:40:01,215 : hoge!hoge!
INFO : 2019-12-14 23:40:01,215 : banana!
INFO : 2019-12-14 23:40:01,215 : foobar!
```

# 最後に
## まとめ
cronpiを使うと次の利点がありそう。
1. Django内で設定が完結するのでコード管理が楽
2. cronの日付記法を忘れても良さそう
3. `crontab -r`の恐怖を味わう機会が減ってハッピー

## 反省
締め切りギリギリですね、以降はもう少し早めに着手したい。
あと結構勢いで書いてたりするので多少アラがあるかもしれません。その際はご指摘ください。

## 課題・懸念点
1. cronpiにcron設定削除処理がないので、都度都度消す必要がある。もちろん、毎度まっさらな環境にプロジェクトを配置できるのであれば気にしなくて良さそう
2. cronpi.get_job_list()の出力結果はcronの日付記法なのでなんだかんだググる必要はあるかもしれない
3. 実はDjagno向けのcronパッケージが存在することをこの記事を書いているときに知った、比較検討が必須

[^1]: 本番導入時はデプロイ時に何かしらで起動させてあげれば良い気がする
[^2]: cronからの起動時は、logging.FileHandlerにわたすfilenameは絶対パスじゃなきゃいけないの知らなくてちょっとハマった。[参考サイト](https://github.com/kraiz/django-crontab/issues/31) [^3]
[^3]: 実はこの参考サイトでDjango向けのcronパッケージがあることを知った。