1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

「バッチってどう書くの?」から始めて、ログ取得を自動化してみた話

1
Posted at

はじめに

インターン中に「定期的なログ取得などの繰り返し作業は、毎回手動でSQLを叩くのではなく、バッチを書いて自動化するか、プロダクト側に管理機能を追加するのが望ましい」という話を聞きました。

頭ではなんとなく理解できたものの、「バッチって実際どうやって書くんだろう?」という疑問が残りました。

聞いただけで終わらせるのはもったいない。せっかくなら自分で手を動かして体験してみよう、と思ったのがこの記事のきっかけです。


なぜ手動作業をバッチ化するべきなのか

まず「バッチを書くべき理由」を整理しておきます。

手動で実行する場合 バッチで自動化する場合
毎回人が実行する必要がある スケジュール設定で自動実行できる
誰がいつ何をしたか残りにくい 実行ログ・出力ファイルで追跡できる
本番DBに直接アクセスするリスクがある 処理を切り出してリスクを局所化できる
操作ミスが起きやすい 同じ処理を毎回正確に再現できる

セキュリティ・運用の両面で、バッチ化することに大きな意味があることがわかりました。


作ったもの

毎朝9時に、前日分の操作ログをCSVに自動出力するバッチです。

$ python batch/export_logs.py
ログ取得開始...
出力完了: ./output/logs_20260505_090000.csv (2件)

実行するたびにタイムスタンプ付きのCSVが生成されるため、「いつ実行したか」が自然に記録として残ります。


環境構成

  • Docker + PostgreSQL 15(本番に近い環境をローカルで再現)
  • Python 3.12 + psycopg2(DB接続)
  • cron(定期実行)
  • venv(仮想環境)
log-batch/
├── docker-compose.yml
├── init.sql          # テーブル作成・ダミーデータ投入
├── batch/
│   ├── export_logs.py
│   └── requirements.txt
└── output/           # CSV出力先

実装の流れ

Step 1: DockerでDBを立ち上げる

docker-compose.yml でPostgreSQLを起動します。

docker-compose.yml

version: '3.8'
services:
  db:
    image: postgres:15
    environment:
      POSTGRES_USER: admin
      POSTGRES_PASSWORD: password
      POSTGRES_DB: appdb
    ports:
      - "5432:5432"
    volumes:
      - ./init.sql:/docker-entrypoint-initdb.d/init.sql

ここで学んだこと: /docker-entrypoint-initdb.d/ にファイルを置くと、コンテナ起動時に自動でSQLが実行されます。テーブル作成とダミーデータ投入を init.sql 一本で済ませられるのが便利でした。

init.sql

CREATE TABLE operations_log (
  id SERIAL PRIMARY KEY,
  user_id VARCHAR(50),
  action VARCHAR(100),
  target_resource VARCHAR(100),
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

INSERT INTO operations_log (user_id, action, target_resource, created_at) VALUES
  ('user_001', 'LOGIN', 'auth_service', NOW() - INTERVAL '2 days'),
  ('user_002', 'VIEW_REPORT', 'report_dashboard', NOW() - INTERVAL '1 day'),
  ('user_001', 'EXPORT_DATA', 'customer_table', NOW() - INTERVAL '1 day'),
  ('user_003', 'DELETE_RECORD', 'orders_table', NOW() - INTERVAL '3 hours'),
  ('user_002', 'LOGIN', 'auth_service', NOW() - INTERVAL '1 hour'),
  ('user_001', 'VIEW_REPORT', 'report_dashboard', NOW() - INTERVAL '30 minutes');

Step 2: Pythonでバッチスクリプトを書く

batch/export_logs.py

import psycopg2
import csv
import os
from datetime import datetime, timedelta

DB_CONFIG = {
    "host": "localhost",
    "port": 5432,
    "dbname": "appdb",
    "user": "admin",
    "password": "password",
}

OUTPUT_DIR = "./output"

def fetch_logs():
    conn = psycopg2.connect(**DB_CONFIG)
    cursor = conn.cursor() #cursorはDBとのやり取りをする窓口のようなもの

    # 昨日分のみ取得
    today = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
    yesterday_start = today - timedelta(days=1)
    yesterday_end = today
    
    cursor.execute("""
        SELECT id, user_id, action, target_resource, created_at
        FROM operations_log
        WHERE created_at >= %s AND created_at < %s
        ORDER BY created_at DESC
    """, (yesterday_start, yesterday_end))

    rows = cursor.fetchall()
    cursor.close()
    conn.close()
    return rows

def export_to_csv(rows):
    os.makedirs(OUTPUT_DIR, exist_ok=True)

    # 実行日時をファイル名に含める(上書きされないようにするため)
    filename = f"{OUTPUT_DIR}/logs_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv"

    with open(filename, "w", newline="") as f: # ブロックを抜けると自動でファイルをclose()してくれる
        writer = csv.writer(f)
        writer.writerow(["id", "user_id", "action", "target_resource", "created_at"])
        writer.writerows(rows)

    print(f"出力完了: {filename} ({len(rows)}件)")

if __name__ == "__main__":
    print("ログ取得開始...")
    rows = fetch_logs()
    export_to_csv(rows)

Step 3: cronで毎朝9時に自動実行する

crontab -e

以下を追加します。
/Users/username の部分はご自身の環境に合わせて書き換えてください。
※ 著者は実験のため仮想環境で実行しています。

0 9 * * * /Users/username/dev/log-batch/.venv/bin/python /Users/username/dev/log-batch/batch/export_logs.py

cronの書き方は左から「分・時・日・月・曜日 実行コマンド」の順です。

0  9  *  *  *   [何で実行するか]   [何を実行するか(スクリプトのパス)]
│  │  │  │  │   └ pythonはvenv内のフルパスを指定した
│  │  │  │  └── 曜日(* = 毎日)
│  │  │  └───── 月(* = 毎月)
│  │  └───────── 日(* = 毎日)
│  └─────────── 時(9時)
└─────────────── 分(0分)

設定確認:

crontab -l

ハマったポイント

ローカルのPostgreSQLと競合した

ポートが競合してDockerが起動できないエラーが出ました。

Bind for 0.0.0.0:5432 failed: port is already allocated

原因はHomebrewでインストールしたPostgreSQLがすでに5432番ポートを使っていたことでした。

brew services stop postgresql@14

で止めることで解決しました。Dockerだけを使うなら、ローカルのDBサービスは止めておくとすっきりします。


おわりに

「バッチを書く」という言葉は知っていたものの、これまでは抽象的な理解にとどまっていました。
今回実際に手を動かしたことで、「なぜ自動化するのか」「どう実装するのか」を具体的にイメージできるようになりました。

特に、定期実行・ログの追跡・操作ミスの防止といった観点で、バッチ化が運用の安定性につながる理由を理解できたのは大きな収穫です。

実務ではさらに複雑になるはずですが、小さく試すことで理解の解像度は確実に上がると感じました。
今後も「なんとなく知っている」で終わらせず、手を動かしながら理解を深めていきたいと思います。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?