Help us understand the problem. What is going on with this article?

[コピペでOK] GoogleAppsScriptでログを出力する

はじめに

GoogleAppsScript(GAS)でconsoleのスプレッドシートシート出力機能を作成しました。GASではLoggerconsoleなどのログ表示機能が備わっておりますが、ここでは使用しません。コピペですぐに使えるものを目指しています。

機能としては下記の通りです。
- ログレベル
- 出力先指定

使い方

使い方は下記の通りです。

main.gs
function main() {
    try {
        console.error('log: error.');
        console.warn('log: warn.');
        console.info('log: info.');
        console.debug('log: debug.');
        console.log({a:"aaa",b:1,c:{d:2},d:[3,4,5]});
        throw new Error('log: throw error');
    } catch (e) {
        console.error(e.stack);
    }
}

スプレッドシートにログが下記のように出力されます。

timestamp level path message
2020-05-24 21:59:53:916 ERROR src/main(3) log: error.
2020-05-24 21:59:54:203 WARN src/main(4) log: warn.
2020-05-24 21:59:54:540 INFO src/main(5) log: info.
2020-05-24 21:59:54:938 DEBUG src/main(6) log: debug.
2020-05-24 21:59:55:207 DEBUG src/main(7) {a:"aaa",b:1,c:{d:2},d:[3,4,5]}
2020-05-24 21:59:55:452 ERROR src/main(10) Error: log: throw error at main (src/main:8:15)

ログファイル出力の実装

  1. GoogleDriveで「Google スプレッドシート」を新規作成する。スプレッドシードのURLhttps://docs.google.com/spreadsheets/d/*****/edit?*****の部分がIDになる。
  2. GoogleDriveで「Google Apps Script」を新規作成する。*.gsファイルに以下のJavaScriptコードを実装する。

JavaScriptの実装

log.gs
let log = {}; {
    const config = {
        SSID_LOG: '*****', // ※ログ用のスプレッドシートのIDを指定する
        SSN_LOG: '*****', // ※ログ用のスプレッドシートのシート名を指定する
        IS_LOGFILE: true, // ログフラグ true=ログファイル出力あり/false=なし
        LOG_LEVEL: 3, // ログレベル 0=ERROR/1=WARN/2=INFO/3=DEBUG
    };

    const logLevel = { // ログレベル
        ERROR: 0,
        WARN: 1,
        INFO: 2,
        DEBUG: 3,
    };

    let spreadsheet = SpreadsheetApp.openById(config.SSID_LOG);
    let sheet = spreadsheet.getSheetByName(config.SSN_LOG);

    /**
     * 初期化
     */
    log.init = () => {
        if (config.IS_LOGFILE) {
            overrideConsole();
        }
    }

    /**
     * console関数を上書きする
     */
    let overrideConsole = () => {
        console = {};
        console.error = (...args) => {
            if (logLevel.ERROR <= config.LOG_LEVEL) {
                out('ERROR', args);
            }
        }
        console.warn = (...args) => {
            if (logLevel.WARN <= config.LOG_LEVEL) {
                out('WARN', args);
            }
        }
        console.info = (...args) => {
            if (logLevel.INFO <= config.LOG_LEVEL) {
                out('INFO', args);
            }
        }
        console.debug = (...args) => {
            if (logLevel.DEBUG <= config.LOG_LEVEL) {
                out('DEBUG', args);
            }
        }
        console.log = (...args) => {
            if (logLevel.DEBUG <= config.LOG_LEVEL) {
                out('DEBUG', args);
            }
        }
        console.ws = null;
    }

    /**
     * ログを出力する
     * @param {String} level ログレベル
     * @param {String} msg メッセージ
     */
    let out = (level, msg) => {

        let callerInfo = {};
        let tmpPrepareST = Error.prepareStackTrace;
        Error.prepareStackTrace = (e, stack) => {
            let caller = stack[1];
            return {
                file: caller.getFileName(),
                line: caller.getLineNumber(),    
            }
        };
        Error.captureStackTrace(callerInfo, out);
        let file = callerInfo.stack.file;
        let line = callerInfo.stack.line;
        Error.prepareStackTrace = tmpPrepareST;

        let timestamp = Utilities.formatDate(new Date(), 'Asia/Tokyo', 'yyyy-MM-dd HH:mm:ss:SSS');
        let message = convertMsg(msg);

        sheet.appendRow([timestamp, level, `${file}(${line})`, message]);
    }

    /**
     * ログメッセージを変換する
     * @param {Array} arg メッセージ
     */
    let convertMsg = (arg) => {
        let msg = '';
        for (let i = 0; i < arg.length; i++) {
            let t = typeof arg[i];
            switch (t) {
                case 'number':
                case 'boolean':
                case 'string':
                    t = arg[i];
                    break;
                case 'object':
                    if (isArray(arg[i])) {
                        t = '[';
                        let count = 0;
                        for (let p in arg[i]) {
                            t += dumpObject(arg[i][p]);
                            count++;
                            if (count < arg[i].length) {
                                t += ',';
                            }
                        }
                        t += ']';
                    } else {
                        t = '{';
                        let count = 0;
                        let objLen = Object.keys(arg[i]).length;
                        for (let p in arg[i]) {
                            t += p + ':' + dumpObject(arg[i][p]);
                            count++;
                            if (count < objLen) {
                                t += ',';
                            }
                        }
                        t += '}';
                    }
                    break;
            }
            msg += ' ' + t;
        }
        return msg;
    }

    /**
     * オブジェクトを出力する
     * @param {Object} obj 
     */
    let dumpObject = (obj) => {
        let t = typeof obj;
        switch (t) {
            case 'number':
            case 'boolean':
                t = obj;
                break;
            case 'string':
                t = '"' + obj + '"';
                break;
            case 'object':
                if (isArray(obj)) {
                    t = '[';
                    let count = 0;
                    for (let p in obj) {
                        t += dumpObject(obj[p]);
                        count++;
                        if (count < obj.length) {
                            t += ',';
                        }
                    }
                    t += ']';
                } else {
                    t = '{';
                    let count = 0;
                    let objLen = Object.keys(obj).length;
                    for (let p in obj) {
                        t += p + ':' + dumpObject(obj[p]);
                        count++;
                        if (count < objLen) {
                            t += ',';
                        }
                    }
                    t += '}';
                }
                break;
        }
        return t;
    }

    /**
     * 配列を判定する
     * @param {Object} obj 
     */
    let isArray = (obj) => {
        return Object.prototype.toString.call(obj) === '[object Array]';
    }
}
log.init();

参考リンク

さいごに

ソースコードをGitHubに公開しています。
- ソースファイルはこちら

以上です。

yun_bow
サービス志向エンジニアです。プログラミングを使ったモノづくりが好きです。AWS、Python、GO言語を勉強中。 こちらで投稿した記事は、所属会社の公式見解を示すものではないです。
pa-rk
Webアプリ、スマホアプリの開発を手掛ける技術者集団です。
https://www.pa-rk.co.jp
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした