はじめに
アプリケーションサーバの再起動・配置時などにはロードバランサや各サーバ用のコンソールで煩雑になるため、
各サーバのログをtailするためだけに新たな面積を割くことが困難だったりします。
そこで、複数サーバにあるログを1つのコンソールでtailできるようにしてみました。
流れが速すぎて読めない可能性もありますが、
適宜grepなども組み合わせつつ使ってみると便利かもしれません。
機能は限定的ですが、概要は下記の通りです。
- 指定された複数のサーバにSSHログインし、対象のファイルをtailし、標準出力に出力する
- 対象のファイルが日別・時間別にローテートされている場合などに対応するため、ファイル名に日時のパターンを指定できる
- 日時の経過により、対象のファイルを変更する必要がある場合は、自動で切り替える
- 1つのサーバで問題が生じたら、すべてのtailを停止し、Exceptionをthrowしてアプリケーションを終了する
- tail開始後、標準入力で’quit'(+エンター)を入力されたら、すべてのtailを停止し、アプリケーションを終了する
事前に以下のライブラリを用意します。
- JSch
- http://www.jcraft.com/jsch/
- ※”jsch-0.1.53.jar”のリンクからダウンロード
実装例
サンプルでは、動作確認しやすいように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 (Exception 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 (Exception e) { } } if (reader != null) { try { reader.close(); } catch (Exception 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 (Exception e) { } } if (reader != null) { try { reader.close(); } catch (Exception 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_74
- NetBeans IDE 8.1
- 動作検証
- CentOS Linux release 7.2
- JDK 1.8.0_74
Webツールも公開しています。
Web便利ツール@ツールタロウ