LoginSignup
0
0

More than 1 year has passed since last update.

ブラウザからLinuxサーバのログを気軽に参照するWebサーバ

Last updated at Posted at 2021-09-14

UbuntuなどのLinuxサーバにあるログを参照するためにわざわざSSHでログインするのが面倒なので、ブラウザから参照できるようにした。
適当につくったものなので、セキュリティも何もない。自己責任でお願いします。

画面はこんな感じ。一応行番号も付くようにしています。

image.png

ログの先頭や末尾をサクッと見れたり、前ページ・後ページに移動できたりします。
なんならファイルダウンロードもできます。
一応、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();
        });
}

以上

0
0
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
0
0