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