端っこプログラマーの手帳

主にプログラムに関する手記です

【Python】コンソールでのログ監視をFTP経由で行いたい

レンタルサーバーなどで、FTP接続しかできない開発者泣かせの環境があったりします。 こんな環境ではログファイルをみるのも大変々々。
SSHを使えればなぁ」と夢想しつつ、FTPクライアントからファイルをダウンロードして開いて末尾を確認という非生産的な行為を繰り返すうちに 「誰かの陰謀ではないか?」と世の中疑り出す始末。。。

こんなときこそプログラムでしょ♪

こんなときこそ、手動で行っている作業をプログラムで自動化すべきです。
開発スイッチが入りました。
手順を整理するとこんな感じ!!
(Linxコマンドでいう tail -f [file] のようなことをやりたいのです)

1. FTP接続(指定した環境にPythonからFTP接続)
2. ログファイルのDWと出力(ログファイルをダウンロードして、末尾の何行かをコンソールに出力)
3. 更新有無の判定(ログファイルが追記されているか確認)
4. 更新分の出力(ログファイルが追記されていれば、再びダウンロードして追記分を出力)  
(以後3,4を繰り返す)

1. FTP接続

# -*- coding: utf-8 -*-
import ftplib
import time

host = ''     # 接続先のホスト名
user = ''     # 接続先のユーザ名 
password = '' # 接続先のパスワード

ftp = ftplib.FTP(host, user, password)

pythonのftplibライブラリを使用します。
21.13. ftplib — FTPプロトコルクライアント — Python 3.5.1 ドキュメント

ホスト、ユーザ名、パスワードを引数に渡してインスタンスを生成します。
以後は、このFTPクラスを通して接続先とファイルの状態のチェックやダウンロードなどのやり取りを行います。
FTP接続先に用事があったらこいつに問い合わせる感じでしょうか...
一度FTPに接続したら、Pythonの関数(os.path.exists)などでアクセスできるのかと思いましたが、Pythonから直接FTP先にアクセスは無理なのでそんなことはできないです。

2. ログファイルのDWと出力

logpath = '/var/www/log/'
logfile = 'test.log'

# ダウンロードデータの受け皿
content = bytearray()

# ダウンロードしたバイトデータを引数に呼ばれる
def download(data):
    for b in data:
        content.append(b)

# 該当ディレクトリに移動してダウンロード
ftp.cwd(logpath)
ftp.retrbinary("RETR %s" % logfile, download)
logdata = content.decode('utf-8')

目的のログファイルをダウンロードします。
まずは、ftp.cwd() で目的のパスへ移動します。(Linxでいうと cd ですね) パスの指定は、FTPのルートディレクトリからの絶対パスでの指定となります。
当然といえば当然なのですが、サーバのルートディレクトリとは異なる環境もあります。(FTPクライアントで接続するとリモートサイトのパスが表示されるのでそれを参考にするとよいです。)

ftp.retrbinary() が対象ファイルのダウンロード処理で、第1引数はFTPコマンドでファイルを指定。
FTPコマンドをPythonから使えるようにラップしたものが、fitlib と思うのですが、ここでは素のFTPコマンドを指定します。
(つまり、ダウンロード以外のFTPコマンドも指定できるということでもあります。マニュアルをみて理解できない点でも、FTPコマンドを調べてみると納得いくことが多々ありました。)

FTPコマンド一覧
FTPコマンドの一覧 - Wikipedia

第2引数は、コールバック関数を指定。(ここでは、download関数を指定)
引数の dataにダウンロードした内容が、バイナリで格納されます。
ログファイルのサイズが大きいときは、download() が何回かに分けて呼ばれます。
その影響で、バイナリ → UTF-8へのデコード処理を、download() 関数内で行うのでは、上手くいきませんでした。
(数バイトの固まりで意味を成す、マルチバイトのデータが途中でちょん切れてしまってうまくできないようです)
そこで「分かれてきたデータをバイナリのまま結合して、最後にデコードする」という手段をとりました。

logdata = logdata.strip().split("\n")
# 末尾の行数を覚えておく
index = len(logdata)
for oneRow in logdata[-3:]:
    print(oneRow )

# ダウンロードファイルの受け皿として後でも使うので初期化
content = bytearray()

ダウンロードしたデータを配列に変換して末尾の3行をコンソールに出力します。
この配列の要素数が、ファイルの行数になります。追加分の出力のとき必要なため、末尾の行数を覚えておきます。
前後の改行は、strip() メソッドで無視します。

3. 更新有無の判定

# ファイルサイズを取得
# 失敗する場合は、FTP接続先がバイナリモードでない可能性がある。ftp.sendcmd("TYPE I") でバイナリモードしてから実行すると解決するかも
size = ftp.size(logfile)

ログファイルが追記されているかの判定は、ファイルサイズで行います。
ftp.size() でファイルのサイズ取得を行います。

4. 更新分の出力

前回の値と比較して、大きい場合のみ再度、2. と同じようにダウンロードして
追加分(前回の末尾の行数から今回の末尾まで)を出力します。
このままだと無限ループになるため、ctrl+c で終了できるようにしました。
クラッシュしないように、time.sleepでウェイトをいれています。

# ウェイトを置かないと処理が止まる
time.sleep(2.0)

try:
    while True:
        old_size = size
        size = ftp.size(logfile)
    # 前回のサイズと比較
        if old_size < size:
            ftp.retrbinary("RETR %s" % logfile, download)
            logdata = content.decode('utf-8')
            logdata = logdata.strip().split("\n")
      # 前回との差分を出力
            for oneRow in logdata[index:]:
                print(oneRow)

            index = len(logdata)

        time.sleep(2.0)

except KeyboardInterrupt:
    ftp.close()
except:
    ftp.close()

おわり

こんな感じでFTP経由でログファイル監視が実現できました。
整理改良したものをGitHubに置いておきます。

Python FTP接続でログファイルを監視する · GitHub

次の野望はこいつをGUIのアプリケーション化することです。 PyQtがなかなか素晴らしいそうなのでやってみたいです。 久しぶりの投稿でした。