LoginSignup
16
14

More than 5 years have passed since last update.

複数サーバのログを1つのコンソールでまとめてtailする

Last updated at Posted at 2016-02-09

はじめに

アプリケーションサーバの再起動・配置時などにはロードバランサや各サーバ用のコンソールで煩雑になるため、
各サーバのログをtailするためだけに新たな面積を割くことが困難だったりします。

そこで、複数サーバにあるログを1つのコンソールでtailできるようにしてみました。
流れが速すぎて読めない可能性もありますが、
適宜grepなども組み合わせつつ使ってみると便利かもしれません。

機能は限定的ですが、概要は下記の通りです。

  • 指定された複数のサーバにSSHログインし、対象のファイルをtailし、標準出力に出力する
  • 対象のファイルが日別・時間別にローテートされている場合などに対応するため、ファイル名に日時のパターンを指定できる
  • 日時の経過により、対象のファイルを変更する必要がある場合は、自動で切り替える
  • 1つのサーバで問題が生じたら、すべてのtailを停止し、Exceptionをthrowしてアプリケーションを終了する
  • tail開始後、標準入力で'quit'(+エンター)を入力されたら、すべてのtailを停止し、アプリケーションを終了する

事前に以下のライブラリを用意します。

実装例

サンプルでは、動作確認しやすいようにmainメソッドで実行できるようにしてあります。

MultiTail.java
import com.jcraft.jsch.ChannelExec;
import com.jcraft.jsch.JSch;
import com.jcraft.jsch.JSchException;
import com.jcraft.jsch.Session;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.text.SimpleDateFormat;
import java.util.Date;

/**
 *
 * @author tool-taro.com
 */
public class MultiTail implements Runnable {

    public static void main(String[] args) throws IOException, InterruptedException, Exception {

        //MultiTailの1インスタンスあたり1台のサーバのtailを担当する
        //複数のサーバをまとめてtailする場合は、相当数のインスタンスを生成してstartする
        MultiTail[] multiTailList = new MultiTail[2];
        //コンストラクタ: 表示名, ホスト, ポート, ユーザ, パスワード, tail対象ファイル(日時のパターン指定にも対応), tail時の文字コード
        multiTailList[0] = new MultiTail("serv1", "ホスト", 22, "ユーザ", "パスワード", "'/var/log/httpd/access_log.'yyyyMMdd", "UTF-8");
        multiTailList[1] = new MultiTail("serv2", "ホスト", 22, "ユーザ", "パスワード", "'/var/log/httpd/access_log.'yyyyMMdd", "UTF-8");
        //tailを開始する
        for (MultiTail multiTail : multiTailList) {
            multiTail.start();
        }

        BufferedReader reader = null;

        try {
            //標準入力で終了用のコマンドを受け付ける
            reader = new BufferedReader(new InputStreamReader(System.in));
            while (true) {
                //いずれかのサーバでExceptionが発生したらアプリケーションを終了する
                for (MultiTail multiTail : multiTailList) {
                    if (multiTail.getException() != null) {
                        MultiTail.terminateAll(multiTailList);
                        throw multiTail.getException();
                    }
                }

                if (!reader.ready()) {
                    Thread.sleep(50);
                    continue;
                }

                //"quit"+エンターを入力されたらアプリケーションを終了する
                if ("quit".equals(reader.readLine())) {
                    MultiTail.terminateAll(multiTailList);
                    break;
                }
            }
        }
        finally {
            if (reader != null) {
                try {
                    reader.close();
                }
                catch (IOException e) {
                }
            }
        }
    }

    //すべての接続を切断する
    private static void terminateAll(MultiTail[] multiTailList) throws InterruptedException {
        for (MultiTail multiTail : multiTailList) {
            multiTail.terminate();
        }
    }

    private String name = null;
    private String host = null;
    private int port = -1;
    private String user = null;
    private String password = null;
    private String filePath = null;
    private String encoding = null;
    private Thread thread = null;
    private boolean terminated = false;
    private Exception exception = null;

    public MultiTail(String name, String host, int port, String user, String password, String filePath, String encoding) {
        this.name = name;
        this.host = host;
        this.port = port;
        this.user = user;
        this.password = password;
        this.filePath = filePath;
        this.encoding = encoding;
    }

    //SSHログインしてtailするThreadを開始する
    public void start() {
        this.thread = new Thread(this);
        this.thread.start();
        System.out.println("started [" + this.name + "]");
    }

    //Threadの停止を指示して終了まで待機する
    public void terminate() throws InterruptedException {
        this.terminated = true;
        this.thread.join();
        System.out.println("terminated [" + this.name + "]");
    }

    @Override
    public void run() {
        JSch jsch;
        Session session = null;
        ChannelExec channel = null;
        BufferedWriter writer = null;
        BufferedReader reader = null;

        String currentFilePath = null;
        String newFilePath;

        try {
            SimpleDateFormat formatter = new SimpleDateFormat(this.filePath);

            jsch = new JSch();
            session = jsch.getSession(this.user, this.host, this.port);
            //known_hostsのチェックをスキップ
            session.setConfig("StrictHostKeyChecking", "no");
            session.setPassword(this.password);
            session.connect();

            while (true) {
                //日付がかわった場合、tail対象のファイルを変更する必要があるかを判定する
                //変更する必要がある場合、現在のコマンドを破棄し、新たに発行する
                newFilePath = formatter.format(new Date());
                if (!newFilePath.equals(currentFilePath)) {
                    if (writer != null) {
                        try {
                            writer.close();
                        }
                        catch (IOException e) {
                        }
                    }
                    if (reader != null) {
                        try {
                            reader.close();
                        }
                        catch (IOException e) {
                        }
                    }
                    if (channel != null) {
                        try {
                            channel.disconnect();
                        }
                        catch (Exception e) {
                        }
                    }

                    currentFilePath = newFilePath;
                    channel = (ChannelExec) session.openChannel("exec");
                    channel.setCommand("tail -f " + currentFilePath);
                    channel.connect();
                    System.out.println("tail [" + this.name + ":" + currentFilePath + "]");

                    writer = new BufferedWriter(new OutputStreamWriter(channel.getOutputStream(), this.encoding));
                    reader = new BufferedReader(new InputStreamReader(channel.getInputStream(), this.encoding), 128);
                }

                //コマンドが予期せず終了している場合
                if (channel.isClosed()) {
                    throw new IOException("closed [" + this.name + "]");
                }
                //終了の指示を受けている場合
                if (this.terminated) {
                    throw new InterruptedException("terminated [" + this.name + "]");
                }

                if (!reader.ready()) {
                    Thread.sleep(50);
                    continue;
                }
                //双方のやり取りが一定時間なくなって切断されるケースに(一応)備えてダミーのデータをwriteする
                writer.write(" ");
                writer.flush();

                //readした行を標準出力に出力する
                System.out.println(this.name + ":" + reader.readLine());
            }
        }
        catch (JSchException | IOException | InterruptedException e) {
            this.exception = e;
        }
        finally {
            if (writer != null) {
                try {
                    writer.close();
                }
                catch (IOException e) {
                }
            }
            if (reader != null) {
                try {
                    reader.close();
                }
                catch (IOException e) {
                }
            }
            if (channel != null) {
                try {
                    channel.disconnect();
                }
                catch (Exception e) {
                }
            }
            if (session != null) {
                try {
                    session.disconnect();
                }
                catch (Exception e) {
                }
            }
        }
    }

    public Exception getException() {
        return this.exception;
    }
}

動作確認

$ MultiTail.java
$ java MultiTail
$ started [serv1]
started [serv2]
tail [serv1:/var/log/httpd/access_log.20160209]
tail [serv2:/var/log/httpd/access_log.20160209]
serv1:XXX.XXX.XXX.XXX - - [09/Feb/2016:13:03:58 +0900] "GET /hogehoge HTTP/1.1" 404 206 "-" "Mozilla/5.0 (Windows NT 10.0; WOW64; rv:43.0) Gecko/20100101 Firefox/43.0" 466
serv2:XXX.XXX.XXX.XXX - - [09/Feb/2016:13:03:59 +0900] "GET /hogehoge HTTP/1.1" 404 206 "-" "Mozilla/5.0 (Windows NT 10.0; WOW64; rv:43.0) Gecko/20100101 Firefox/43.0" 400
serv1:XXX.XXX.XXX.XXX - - [09/Feb/2016:13:04:00 +0900] "GET /hogehoge HTTP/1.1" 404 206 "-" "Mozilla/5.0 (Windows NT 10.0; WOW64; rv:43.0) Gecko/20100101 Firefox/43.0" 3704
serv2:XXX.XXX.XXX.XXX - - [09/Feb/2016:13:04:01 +0900] "GET /hogehoge HTTP/1.1" 404 206 "-" "Mozilla/5.0 (Windows NT 10.0; WOW64; rv:43.0) Gecko/20100101 Firefox/43.0" 522
quit
terminated [serv1]
terminated [serv2]

環境

  • 開発

    • Windows 10 Pro
    • JDK 1.8.0_121
    • NetBeans IDE 8.2
  • 動作検証

    • CentOS Linux release 7.3
    • JDK 1.8.0_121

Webツールも公開しています。
Web便利ツール@ツールタロウ

16
14
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
16
14