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

# はじめに

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

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

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

```javaScript: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 スプレッドシート」を新規作成する。スプレッドシードのURL`https://docs.google.com/spreadsheets/d/*****/edit?`の`*****`の部分がIDになる。
2. GoogleDriveで「Google Apps Script」を新規作成する。*.gsファイルに以下のJavaScriptコードを実装する。

## 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++) {
            msg += ' ' + dumpObject(arg[i]);
        }
        return msg;
    }

    /**
     * オブジェクトを出力する
     * @param {Object} obj 
     */
    let dumpObject = (obj) => {
        let v = '';
        let t = typeof obj;
        switch (t) {
            case 'number':
            case 'boolean':
                v = obj;
                break;
            case 'string':
                v = '"' + obj + '"';
                break;
            case 'object':
                if (isArray(obj)) {
                    v = '[';
                    let count = 0;
                    for (let value of obj) {
                        v += dumpObject(value);
                        count++;
                        if (count < obj.length) {
                            v += ',';
                        }
                    }
                    v += ']';
                } else if (isObject(obj) || isError(obj)) {
                    v = '{';
                    let count = 0;
                    let nameList = Object.getOwnPropertyNames(obj);
                    for (let key of nameList) {
                        let ret = dumpObject(obj[key]);
                        if (ret) {
                            v += key + ':' + ret;
                            count++;
                            if (count < nameList.length) {
                                v += ',';
                            }
                        }
                    }
                    v += '}';
                } else if (isString(obj) || isDate(obj)) {
                    v = '"' + obj.toString() + '"';
                } else if (isNumber(obj) || isBoolean(obj)) {
                    v = obj.valueOf();
                } else if (isFunction(obj)) {
                    v = '<fuction>';
                } else if (isNull(obj)) {
                    v = '<null>';
                } else if (isUndefined(obj)) {
                    v = '<undefined>';
                }
                break;
            case 'function':
                v = '<function>';
                break;
            case 'symbol':
                v = '<symbol>';
                break;
            case 'undefined':
                v = '<undefined>';
                break;
        }
        return v;
    }

    let isArray = (obj) => {
        return Object.prototype.toString.call(obj) === '[object Array]';
    }

    let isBoolean = (obj) => {
        return Object.prototype.toString.call(obj) === '[object Boolean]';
    }

    let isDate = (obj) => {
        return Object.prototype.toString.call(obj) === '[object Date]';
    }

    let isError = (obj) => {
        return Object.prototype.toString.call(obj) === '[object Error]';
    }

    let isNumber = (obj) => {
        return Object.prototype.toString.call(obj) === '[object Number]';
    }

    let isObject = (obj) => {
        return Object.prototype.toString.call(obj) === '[object Object]';
    }

    let isString = (obj) => {
        return Object.prototype.toString.call(obj) === '[object String]';
    }

    let isFunction = (obj) => {
        return Object.prototype.toString.call(obj) === '[object Function]';
    }

    let isNull = (obj) => {
        return Object.prototype.toString.call(obj) === '[object Null]';
    }

    let isUndefined = (obj) => {
        return Object.prototype.toString.call(obj) === '[object Undefined]';
    }
}
log.init();
```

# 参考リンク
- https://qiita.com/84zume/items/547f392eb1351b21e77e

# さいごに
ソースコードをGitHubに公開しています。
- [ソースファイルはこちら](https://github.com/yunbow/GoogleAppsScript-logger)

以上です。