UbuntuなどのLinuxサーバにあるログを参照するためにわざわざSSHでログインするのが面倒なので、ブラウザから参照できるようにした。
適当につくったものなので、セキュリティも何もない。自己責任でお願いします。
画面はこんな感じ。一応行番号も付くようにしています。
ログの先頭や末尾をサクッと見れたり、前ページ・後ページに移動できたりします。
なんならファイルダウンロードもできます。
一応、APIKeyで守ってますが、適当実装なので、ローカルネットワークでのみ使てください。
ソースコードもろもろはGitHubに上げておきました。
poruruba/SimpleLogViewer
ソースコード(サーバ)
そのままソースコード載せておきます。
api/controllers/tail-file/index.js
'use strict';
const HELPER_BASE = process.env.HELPER_BASE || '../../helpers/';
const Response = require(HELPER_BASE + 'response');
const BinResponse = require(HELPER_BASE + 'binresponse');
const TextResponse = require(HELPER_BASE + 'textresponse');
const APIKEY = "【お好きなAPIKey】";
const logfile_list = [
// 参照したいログファイル名の配列
];
const { exec } = require('child_process');
const streamBuffers = require('stream-buffers');
const archiver = require('archiver');
const path = require('path');
exports.handler = async (event, context, callback) => {
if( event.path == '/tail-view-file'){
if (!event.requestContext.apikeyAuth || event.requestContext.apikeyAuth.apikey != APIKEY )
throw "wrong apikey";
var body = JSON.parse(event.body);
console.log(body);
if( logfile_list.indexOf(body.fname) < 0 )
throw 'not allowed';
var num = Number(body.num);
var start = Number(body.start);
return new Promise((resolve, reject) =>{
var exec_batch;
if (body.order == 'head'){
exec_batch = `cat -n ${body.fname} | head -n ${start - 1 + num} | tail -n ${num} | sed -r "s/\\x1B\\[([0-9]{1,2}(;[0-9]{1,2})*)?m//g" | col -bx`;
} else if (body.order == 'tail'){
exec_batch = `cat -n ${body.fname} | tail -n ${start - 1 + num} | head -n ${num} | sed -r "s/\\x1B\\[([0-9]{1,2}(;[0-9]{1,2})*)?m//g" | col -bx`;
}else{
reject('unknown order');
}
exec(exec_batch, (err, stdout, stderr) => {
if (err) {
reject(err);
return;
}
resolve(new TextResponse("text/plain", stdout));
});
});
}else
if( event.path == '/tail-get-file'){
if (!event.requestContext.apikeyAuth || event.requestContext.apikeyAuth.apikey != APIKEY )
throw "wrong apikey";
var body = JSON.parse(event.body);
console.log(body);
if( logfile_list.indexOf(body.fname) < 0 )
throw 'not allowed';
return new Promise((resolve, reject) =>{
var dest_stream = new streamBuffers.WritableStreamBuffer();
const archive = archiver('zip', {
zlib: { level: 9 }
});
dest_stream.on('finish', () => {
console.log('stream finish');
var response = new BinResponse('application/zip', dest_stream.getContents());
response.set_filename(path.basename(body.fname) + '.zip');
resolve(response);
});
archive.pipe(dest_stream);
archive.on('error', (err) => {
reject(err);
});
archive.file(body.fname, { name: path.basename(body.fname) });
archive.finalize();
});
}else
if( event.path == '/tail-list' ){
return new Response({ list: logfile_list });
}
};
要は、以下のようなコマンドを実行しているだけです。
行番号付けたり、画面制御コードを省いたりしています。
cat -n ${body.fname} | tail -n ${start - 1 + num} | head -n ${num} | sed -r "s/\\x1B\\[([0-9]{1,2}(;[0-9]{1,2})*)?m//g" | col -bx
ソースコード(クライアント)
クライアント側ソースコード。
特に難しいことはしていません。Vueのおかげで。
public/log_viwer/js/start.js
'use strict';
//const vConsole = new VConsole();
//window.datgui = new dat.GUI();
const base_url = "http://【本サーバのURL】";
var vue_options = {
el: "#top",
mixins: [mixins_bootstrap],
data: {
apikey: '',
file_list: [],
num_of_col: 1,
select_file: ["", ""],
log_data: ['', ''],
select_order: ["tail", "tail"],
start_line: [1, 1],
get_line: [30, 30],
top_line: [-1, -1]
},
computed: {
class_row: function () {
return "col-sm-" + Math.floor(12 / this.num_of_col);
}
},
methods: {
check_top_line: function (index, log_data) {
try {
var i = 0;
for (; ; i++)
if (log_data.charAt(i) != ' ' || !log_data.charAt(i))
break;
var j = i;
for (; ; j++)
if (log_data.charAt(j) == ' ' || !log_data.charAt(j))
break;
if (i < j)
this.top_line[index] = Number(log_data.substring(i, j));
else
this.top_line[index] = -1;
} catch (error) {
console.log(error);
}
},
add_num: function (index, target, num) {
if (target == 'start_line') {
var line = this.start_line[index] + num;
if (line < 1) line = 1;
this.$set(this.start_line, index, line);
} else
if (target == 'get_line') {
var line = this.get_line[index] + num;
if (line < 1) line = 1;
this.$set(this.get_line, index, line);
}
},
log_get_file: async function (index) {
try {
this.progress_open();
var param = {
fname: this.select_file[index],
};
var blob = await do_post_blob_with_apikey(base_url + "/tail-get-file", param, this.apikey);
Cookies.set('tail-apikey', this.apikey, { expires: 3650 });
var url = window.URL.createObjectURL(blob);
var a = document.createElement('a');
a.href = url;
a.target = '_blank';
a.download = "download.zip";
a.click();
window.URL.revokeObjectURL(url);
} catch (error) {
console.error(error);
alert(error);
} finally {
this.progress_close();
}
},
log_view_file: async function (index) {
var param = {
fname: this.select_file[index],
order: this.select_order[index],
start: this.start_line[index],
num: this.get_line[index],
};
try {
var log_data = await do_post_text_with_apikey(base_url + "/tail-view-file", param, this.apikey);
if (!log_data)
return;
this.check_top_line(index, log_data);
this.$set(this.log_data, index, log_data);
Cookies.set('tail-apikey', this.apikey, { expires: 3650 });
} catch (error) {
console.error(error);
alert(error);
}
},
log_next: async function (index) {
if (this.top_line[index] < 0)
return;
var start;
if (this.select_order[index] == 'tail') {
start = this.top_line[index] - this.get_line[index];
if (start < 1) start = 1;
} else if (this.select_order[index] == 'head') {
start = this.top_line[index] + this.get_line[index];
}
var param = {
fname: this.select_file[index],
order: 'head',
start: start,
num: this.get_line[index],
};
try {
var log_data = await do_post_text_with_apikey(base_url + "/tail-view-file", param, this.apikey);
if (!log_data)
return;
this.check_top_line(index, log_data);
this.$set(this.log_data, index, log_data);
} catch (error) {
console.error(error);
alert(error);
}
},
log_prev: async function (index) {
if (this.top_line[index] < 0)
return;
var start;
if (this.select_order[index] == 'tail') {
start = this.top_line[index] + this.get_line[index];
} else if (this.select_order[index] == 'head') {
start = this.top_line[index] - this.get_line[index];
if (start < 1) start = 1;
}
var param = {
fname: this.select_file[index],
order: 'head',
start: start,
num: this.get_line[index],
};
try {
var log_data = await do_post_text_with_apikey(base_url + "/tail-view-file", param, this.apikey);
if (!log_data)
return;
this.check_top_line(index, log_data);
this.$set(this.log_data, index, log_data);
} catch (error) {
console.error(error);
alert(error);
}
},
},
created: function () {
},
mounted: async function () {
proc_load();
this.apikey = Cookies.get('tail-apikey');
try {
var result = await do_post(base_url + "/tail-list", {});
this.file_list = result.list;
this.select_file[0] = this.file_list[0];
this.select_file[1] = this.file_list[0];
} catch (error) {
console.error(error);
alert(error);
}
}
};
vue_add_data(vue_options, { progress_title: '' }); // for progress-dialog
vue_add_global_components(components_bootstrap);
vue_add_global_components(components_utils);
/* add additional components */
window.vue = new Vue(vue_options);
function do_post_text_with_apikey(url, body, apikey) {
const headers = new Headers({ "Content-Type": "application/json; charset=utf-8", "X-API-KEY": apikey });
return fetch(url, {
method: 'POST',
body: JSON.stringify(body),
headers: headers
})
.then((response) => {
if (!response.ok)
throw 'status is not 200';
// return response.json();
return response.text();
// return response.blob();
// return response.arrayBuffer();
});
}
function do_post_blob_with_apikey(url, body, apikey) {
const headers = new Headers({ "Content-Type": "application/json; charset=utf-8", "X-API-KEY": apikey });
return fetch(url, {
method: 'POST',
body: JSON.stringify(body),
headers: headers
})
.then((response) => {
if (!response.ok)
throw 'status is not 200';
// return response.json();
// return response.text();
return response.blob();
// return response.arrayBuffer();
});
}
以上