Chromeデベロッパツールがないブラウザでconsole.logを確認する方法

ブラウザのJavascriptでconsole.logでデバッグ出力しているけれども、Chromeデベロッパツールを使えない場合があります。たとえば、

* AndroidやiPhoneのブラウザ上で動作させている場合
* 埋め込みブラウザ(WebViewを使ったCordovaやLIFFなど)上で動作させている場合
* 別の人や別の場所のブラウザで動作させている場合

など

その場合に、なんとかconsole.logのデバッグ出力を確認する方法をいくつか挙げておきます。

* まずは普通にChromeデベロッパツールが使える場合(【方法①】)
　　F12キーを押して表示されるChromeデベロッパツールでデバッグします。
　　これができれば何も問題ないですが、スマホ上では無理。
* VConsoleを使う場合(【方法②】)
　　ブラウザのWebページ内で、コンソール出力をダイアログ表示します。(お手軽)
* AndroidのUSB/ワイヤレス デバッグを使う場合(【方法③】)
　　Androidスマホであれば、USB/ワイヤレス デバッグを使うことで、リモートデバッグできます。(一度繋いでしまえば多機能)
* リモートコンソール(今回作成)を使う場合(【方法④】)
　　外部に立ち上げたサーバを介して、他のPCのブラウザからコンソール出力を確認します。(サーバを立ち上げてしまえばお手軽)


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/2d407347-f1a5-e87f-5b3c-dd73af7c9128.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/92ed619a-5582-c6bb-ffd3-e448c5aea395.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/10a0db92-b971-766f-6e0b-6cbbe3780eb0.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/369ce251-77cf-1803-fd36-de5fad4cfe22.png)

以下、それぞれの特徴をまとめてみました。

||【方法①】Chromeデベロッパツール|【方法②】VConsole|【方法③】USB/ワイヤレス デバッグ|【方法④】リモートコンソール(自作)|
|--:|:-:|:-:|:-:|:-:|
|デバッグ出力表示|〇|〇|〇|〇|
|トレース実行|〇||〇||
|接続方法|ローカル(ブラウザ内)|ローカル(ブラウザ内)|有線(USB)、ネットワーク(WiFi)|ネットワーク|
|対象OS|Win/Mac|Win/Mac/Android/iOS|Android|Win/Mac/Android/iOS|
|対象ブラウザ|Chromeブラウザ|Chromeブラウザ/Chrome以外のブラウザ/組み込みブラウザ|Chromeブラウザ|Chromeブラウザ/Chrome以外のブラウザ/組み込みブラウザ|
|別機器の要否|不要|不要|Win/Mac|サーバ(自作)|

# 【方法①】Chromeデベロッパツール

Chromeブラウザで、F12を押せば、Chromeデベロッパツールが立ち上がり、デバッグ出力の確認だけでなく、ブレークポイントなどトレース実行ができます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/4086ebc6-aadc-d213-b5f9-99212068db1f.png)

これが一番手軽で多機能ですが、可能なのはWindowsやMac上のChromeブラウザで表示されたWebページだけです。
スマホで表示させたWebページやCordova・LIFFなどの埋め込みブラウザはできません。

# 【方法②】VConsole

以下のモジュールを使います。

Tencent/VConsole

https://github.com/Tencent/vConsole

CDNがあるので、以下をHTMLに記載すればよいです。

```
<script src="https://cdn.jsdelivr.net/npm/vconsole/dist/vconsole.min.js"></script>
```

そして、どこかのJavascriptに以下を記載すれば、

```js
const vConsole = new VConsole();
```

あとはいつも通り、console.logを使うだけです。

スマホのWebページの方を見ると、右下に、「vConsole」という緑色のボタンが増えているのがわかります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/0d17af07-2e5d-9220-7f35-162385f3a72a.png)

そのボタンを押すと、ダイアログが表示されて、console.logで出力した文字列が出力されていることが確認できます。ただし、トレース実行はできません。デバッグ出力だけです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/8b1ec40c-c242-7ad6-2694-4fc299331bea.png)

非常にお手軽ですね。CordovaアプリやLIFFアプリをデバッグするときにはかなり重宝します。
ただ、デバッグ出力を見たい時に都度「Console」ボタンを押す必要がありますので、リアルタイムに見たいとか流れているデバッグ出力を眺めたい場合はつらいところです。

# 【方法③】USB/ワイヤレス リモートデバッグ

Androidに限定されますが、ADB通信機能を利用して、他のPCからリモートデバッグできます。

PC上のChromeブラウザを立ち上げて、アドレスバーに以下を入力します。

　```chrome://inspect/#devices```

左側のナビゲーションメニューからDevicesが選択されていることを確認します。
次に、Android側で、USB接続して「USBデバッグ」を有効にするか、「ワイヤレスデバッグ」を有効にします。いずれも、開発者オプションから変更できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/bb6b9e03-607d-37ba-1327-5f727af9c71f.png)

そうすると、PC上のブラウザ上にAndroid機器が表示され、まさに今Android上で表示されているWebページ一覧が表示されます。デバッグしたいWebページの「Inspect」をクリックすると、そのページをデバッグできるようになります。

リモートから、Androidのデバッグ出力確認だけでなく、トレース実行もできるのは、優れものです。
ただし、Androidスマホ限定です。

# 【方法④】リモートコンソール(自作)

最後が、今回自作したリモートコンソールです。
方法②のVConsoleに見ていますが、デバッグ出力が表示される場所が、別のWebページであることが違います。
スマホのブラウザ内でのconsole.logの出力文字列を別途立ち上げるNode.jsサーバに転送し、それをWebページとして参照できるようにしているわけです。
コンソールとして表示させるために、以下の投稿で説明したlog.ioを使っています。

　[log.ioでログをブラウザでリアルタイムモニタリングする](https://qiita.com/poruruba/items/7579243d9725b1cb23a5)

log.io
　http://logio.org/

使い方です。HTMLに以下を記載します。

```html
<script src="js/remoteconsole.js"></script>
```

次に、Javascriptのいづれかの場所に以下を記述します。

```js
const remoteConsole = new RemoteConsole("http://[立ち上げたNode.jsサーバのホスト名]/logio-post");
```

あとは、Javascriptでconsole.logを呼び出せば、そのままサーバに転送され、log.ioが提供するページで確認できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/8b971621-7a7c-4e36-dfc0-684916d31e40.png)

このお陰でたくさん流れるコンソールログを確認できますし、複数のスマホからのコンソールログを1ヵ所に集約できます。

〇仕組み

少し、仕組みについて説明します。
クライアント側では、remoteconsole.jsの中で処理しています。
既存のconsole.log関数を横取りし、デバッグ出力文字列をNode.jsサーバに転送しています。Node.jsサーバとは、HTTP Post(Json)で通信しています。

```js:js/remoteconsole.js
class RemoteConsole{
    constructor(base_url, stream, force = true){
        this.debuglog_base_url = base_url;
        this.debuglog_stream = stream;

        if( !window ){
            console.log('window not found');
            return;
        }

        window.console_rm = {
            log: async (...messages) => {
                if( this.oldLog )
                    this.oldLog(...messages);
                for (const message of messages)
                    await this.debuglog_logToServer(message, "log");
            },
            error: async (...messages) => {
                if( this.oldError )
                    this.oldError(...messages);
                for (const message of messages)
                    await this.debuglog_logToServer(message, "error");
            }
        };
        if( force ){
            this.oldLog = window.console.log;
            window.console.log = window.console_rm.log;
            this.oldError = window.console.error;
            window.console.error = window.console_rm.error;
        }

        // intecept errors
        if (!window.onerror) {
            window.onerror = async (errorMsg, url, lineNumber, column, errorObj) => {
                await this.debuglog_logToServer(errorMsg, "error");
                await this.debuglog_logToServer(errorObj, "error");
                return false;
            }
        }
    }
    
    // this is optional, but it avoids 'converting circular structure' errors
    debuglog_customStringify(inp) {
        let cache = [];
        return JSON.stringify(inp, function (key, value) {
            if (typeof value === 'object' && value !== null) {
                if (cache.indexOf(value) !== -1) {
                    // Circular reference found, discard key
                    return;
                }
                // Store value in our collection
                cache.push(value);
            }
            return value;
        });
    }

    async debuglog_logToServer(consoleMsg, source) {
        const jsonTxt = this.debuglog_customStringify(consoleMsg);
        return this.do_post(this.debuglog_base_url, {
            source: source,
            stream: this.debuglog_stream,
            message: jsonTxt
        });
    };

    async do_post(url, body) {
        const headers = new Headers({ "Content-Type": "application/json" });
      
        return fetch(url, {
          method: 'POST',
          body: JSON.stringify(body),
          headers: headers
        })
        .then((response) => {
          if (!response.ok)
            throw new Error('status is not 200');
          return response.json();
        });
      }
}
```

一方、Node.jsサーバでは、受信した文字列をLog.ioサーバに転送しています。なぜ転送しているかというと、Log.ioは、TCP通信で受け付けており、クライアント側のブラウザからのHTTP Post(Json)からTCP通信への変換が必要であるためです。

```js:index.js
'use strict';

const HELPER_BASE = process.env.HELPER_BASE || '../../helpers/';
const Response = require(HELPER_BASE + 'response');

const LOGIO_HOST = process.env.LOGIO_HOST || '【log.ioサーバのホスト名】';
const LOGIO_PORT = process.env.LOGIO_PORT || 6689;

var LogIo = require('./logio');
var logio = new LogIo(LOGIO_HOST, LOGIO_PORT)

exports.handler = async (event, context, callback) => {
  if( event.path == '/logio-post'){
    var body = JSON.parse(event.body);

    var stream = body.stream || getRemoteIpAddress(context.req);
    
    if( stream && body.source )
      logio.log3(stream, body.source, body.message);
    else if( !stream && body.source )
      logio.log2(body.source, body.message);
    else if( !stream && !body.source )
      logio.log(body.message);
    else
      throw "invalid param";

    return new Response({"status": "OK"});
  }
}

function getRemoteIpAddress(req) {
  if( req ){
    if (req.headers['x-forwarded-for']) {
      return req.headers['x-forwarded-for'];
    }
    if (req.connection && req.connection.remoteAddress) {
      return req.connection.remoteAddress;
    }
    if (req.connection.socket && req.connection.socket.remoteAddress) {
      return req.connection.socket.remoteAddress;
    }
    if (req.socket && req.socket.remoteAddress) {
      return req.socket.remoteAddress;
    }
  }
  return null;
};
```

```js:logio.js
'use strict';

const net = require('net');

var ConnectState = {
  disconnected : 0,
  connecting: 1,
  connected: 2
};

class LogIo{
  constructor(host, port){
    this.host = host;
    this.port = port;
    this.default_stream_name = 'stream';
    this.default_source_name = 'source';

    this.isconnected = ConnectState.disconnected;
    this.client = new net.Socket();
    this.client.on('close', () =>{
      this.isconnected = ConnectState.disconnected;
      console.log('[LogIo] disconnected');
    });
  }

  log(message){
    this.log3(this.default_stream_name, this.default_source_name, message);
  }
  
  log2(source_name, message){
    this.log3(this.default_stream_name, source_name, message);
  }
  
  log3(stream_name, source_name, message){
    console.log(`[${stream_name}] [${source_name}] - ${message}`);
    var packet = `+msg|${stream_name}|${source_name}|${message}\0`;
    if( this.isconnected == ConnectState.connected ){
      this.client.write(packet);
    }else if( this.isconnected == ConnectState.disconnected ){
      try{
        this.isconnected = ConnectState.connecting;
        this.client.connect(this.port, this.host, () =>{
          this.isconnected = ConnectState.connected;
          this.client.write(packet);
          console.log('[LogIo] connected to ' + this.host + ':' + this.port);
        });
      }catch(error){
        console.error(error);
      }
    }else{
      console.log('[LogIo] connecting');
    }
  }
}

module.exports = LogIo;
```

# 終わりに

ほかにもあるかもしれませんが、、、、一応これで事足りてます。

以下を参考にさせていただきました。

https://stackoverflow.com/questions/43575126/how-to-remote-debug-console-log

以上