log.ioでログをブラウザでリアルタイムモニタリングする

「log.io」を使ってログを集約し、ブラウザからリアルタイムにモニタリングしてみます。

log.io
　http://logio.org/


log.ioは、TCPでログを受信するとともに、ブラウザからログを参照することができます。
Webサーバの機能が付いているので楽ちんなのと、WebSocketを使っているので、リアルタイムにログが出力されてきます。
それから、TCPでログを受信するので、AndroidやArduinoやNode.jsなど、様々なプラットフォームからのログを集約することができそうです。


# log.ioのセットアップ

npmでモジュール化されています。

> npm install -g log.io
> mkdir ~/.log.io
> vi ~/.log.io/server.json
> log.io-server

server.jsonはこんな感じです。

```json:server.json
{
  "messageServer": {
    "port": 6689,
    "host": "【起動させるマシンのIPアドレス】"
  },
  "httpServer": {
    "port": 6688,
    "host": "【起動させるマシンのIPアドレス】"
  },
  "debug": false,
  "basicAuth": {
    "realm": "【適当な名前】",
    "users": {
      "【ログインユーザ名】": "【ログインパスワード】"
    }
  }
}
```

【起動させるマシンのIPアドレス】には、log.ioを起動させたマシンのホスト名またはIPアドレスを指定します。
上記の場合、ポート6688にWebサーバが立ち上がり、ポート6689にログ受信を待ち受けます。
ブラウザから以下を開いてみます。


　http:// 【起動させるマシンのIPアドレス】:6688

ログインパスワードを聞かれますので、server.jsonで指定した【ログインユーザ名】、【ログインパスワード】を入力すればログインできて以下のような画面が表示されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/eecbab11-f9ed-42f0-bc12-33f492ffcbd9.png)

# ログの送信

それではログを送信しています。
送信は、TCPで以下のようなフォーマットの文字列を送ります。

　```+msg|streamName1|sourceName1|this is log message\0```

streamName1やsourceName1で区別されるログを表示したり非表示にしたりできます。

TCPで送信すればよいだけなので、様々なプラットフォームから送信することができます。
今回は以下で送信してみます。

・Android
・Arduino
・Node.js
・Javascript(ブラウザ)

出力後はこんな感じ。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/99f9b34c-5abf-dd4e-25d7-f27249579547.png)

# Android

クラスファイル化してみました。

```java:LogIo.java
package com.example.logiotest;

import android.content.Context;
import android.util.Log;
import java.io.OutputStream;
import java.net.Socket;
import no.nordicsemi.android.log.LogSession;
import no.nordicsemi.android.log.Logger;
import no.nordicsemi.android.log.LogContract.Log.Level;

public class LogIo {
    public String tag = "logio";
    public String default_stream_name = null;
    public String default_source_name = null;
    public String host = null;
    public int port;
    Socket socket = null;
    OutputStream outs = null;
    enum ConnectState {
        Disconnected,
        Connecting,
        Connected
    };
    ConnectState isconnected = ConnectState.Disconnected;
    LogSession logSession = null;

    public LogIo(String host, int port){
        this.default_stream_name = "stream";
        this.default_source_name = "source";
        this.host = host;
        this.port = port;
    }

    public LogIo(Context context, String key, String name){
        logSession = Logger.newSession(context, key, name);
    }

    public void log(String message){
        log3(default_stream_name, default_source_name, message);
    }

    public void log2(String source, String message){
        log3(default_stream_name, source, message);
    }

    synchronized public void log3(String stream, String source, String message){
        if( host != null ) {
            Log.d(tag, "[" + stream + "] [" + source + "] - " + message);
            final String packet = "+msg|" + stream + "|" + source + "|" + message + "\0";
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        byte[] buffer = packet.getBytes("UTF-8");

                        if (isconnected == ConnectState.Connected) {
                            outs.write(buffer);
                            outs.flush();
                        } else if (isconnected == ConnectState.Disconnected) {
                            isconnected = ConnectState.Connecting;
                            socket = new Socket(host, port);
                            outs = socket.getOutputStream();
                            isconnected = ConnectState.Connected;
                            outs.write(buffer);
                            outs.flush();
                        }
                    } catch (Exception ex) {
                        try {
                            if (outs != null) {
                                outs.close();
                                outs = null;
                            }
                            socket.close();
                            socket = null;
                        } catch (Exception ex2) {
                        }
                        isconnected = ConnectState.Disconnected;
                    }
                }
            }).start();
        }
        if( logSession != null ){
            String packet = message;
            if( source != null )
                packet = "[" + source + "] " + packet;
            if( stream != null )
                packet = "[" + stream + "] " + packet;
            Log.d(tag, packet);
            Logger.log(logSession, Level.INFO, packet);
        }
    }
}
```

※LoggerSessionなるものが混在してわかりにくいですが、nRF Loggerというもので、後述します。ということで、盲目的に、appのbuild.gradleのdependenciesに以下を追記しておきます。
　```implementation 'no.nordicsemi.android:log:2.3.0'```

あとは、以下のように呼び出せばよいです。

```java
        LogIo logio = new LogIo("【起動させるマシンのIPアドレス】", 6689);
        logio.log("こんばんは1");
        logio.log2("fromAndroid", "こんばんは2");
```

【起動させるマシンのIPアドレス】のところに、log.ioを立ち上げたマシンのホスト名またはIPアドレスを指定します。

それと、ネットワークを使うので、AndroidManifest.xmlに以下を追記します。
　```<uses-permission android:name="android.permission.INTERNET" />```

# Arduino

Arduinoもログ送信可能です。

```c
#include <WiFiClient.h>
WiFiClient logio_client;
String logio_host;
int logio_port = 6689;
String default_stream_name = "stream";
String default_source_name = "source";

void logio_setup(String host, int port) {
  logio_host = host;
  logio_port = port;
}

void logio_log(String message) {
  logio_log3(default_stream_name, default_source_name, message);
}

void logio_log2(String source, String message) {
  logio_log3(default_stream_name, source, message);
}

void logio_log3(String stream, String source, String message) {
  Serial.println("[" + stream + "] [" + source + "] - " + message);
  
  if( !logio_client.connected() ){
    if( !logio_client.connect(logio_host.c_str(), logio_port) ){
      Serial.println("connection failed");
      return;
    }
  }
  if( logio_client.connected() ){
    String packet = "+msg|" + stream + "|" + source + "|" + message;
    logio_client.write(packet.c_str(), strlen(packet.c_str()) + 1);
    logio_client.flush();
  }
}
```

あとは、setup()でWiFiをAPに接続したのち、以下を呼び出し、

　``` logio_setup("【起動させるマシンのIPアドレス】", 6689);```

loop()などの適当なところで以下を呼び出します。

```c
logio_log("Test Message");
logio_log2("fromArduino”, "Test Message");
```

# Node.js

モジュール化しました。

```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;
```

あとは、呼び出し側で以下を呼び出せばよいです。

```js
var logio = new LogIo('【起動させるマシンのIPアドレス】', 6689)
logio.log('test message');
logio.log2('fromNodejs', 'test message');
```

# javascript

Javascriptには残念ながらTCP通信する機能がないため、TCP通信するNode.jsサーバを立ち上げて、そこからlog.ioサーバに転送してもらいましょう。

サーバ側の実装です。

```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 || '【起動させるマシンのIPアドレス】';
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 '0.0.0.0';
};

```

```js:response.js
class Response{
    constructor(context){
        this.statusCode = 200;
        this.headers = {'Access-Control-Allow-Origin' : '*'};
        if( context )
            this.set_body(context);
        else
            this.body = "{}";
    }

    set_error(error){
        this.body = JSON.stringify({"err": error});
        return this;
    }

    set_body(content){
        this.body = JSON.stringify(content);        
        return this;
    }
    
    get_body(){
        return JSON.parse(this.body);
    }
}

module.exports = Response;
```

Node.jsのところで作成したlogio.jsを流用しています。
エンドポイント「/logio-post」に、JSONでPOSTすれば、log.ioサーバに転送します。

POSTするJSONのフォーマットは以下の感じです。

```
{
  "stream": "【任意のStream名】", // オプション
  "source": "【任意のSource名】", // オプション
  "message": "【任意のメッセージ】", // 必須
}
```

あとは、ブラウザのJavascriptで以下のように呼び出せばよいです。

```js:start.js
var url = "http:// 【起動させるマシンのIPアドレス】:6689/logio-post";
var param = {
	source: "fromHttp",
	message: "こんにちは"
};
do_post(url, param);


function do_post(url, body) {
  const headers = new Headers({ "Content-Type": "application/json; charset=utf-8" });

  return fetch(new URL(url).toString(), {
      method: 'POST',
      body: JSON.stringify(body),
      headers: headers
    })
    .then((response) => {
      if (!response.ok)
        throw 'status is not 200';
      return response.json();
    });
}
```

# log.ioもうちょっと

触ってみて気づいたけど、

・StreamやSourceを作らないと初回のメッセージが表示されないのが面倒だなあ。。。
・日付を自動的に入れてくれるとありがたいのに。。。
・ブラウザだけで、StreamやSourceが削除できないなあ。。。

# (おまけ) Androidでローカルログ保存

Androidでは、log.ioで集約する方法のほかに、Android内にインストールした別のアプリに集約し、そのアプリからモニタリングする方法もあります。
その便利なアプリが「nRF Logger」です。

Google Play：nRF Logger
　https://play.google.com/store/apps/details?id=no.nordicsemi.android.log&hl=ja

もともと、BLEを使ったアプリのLoggerとして作られたようです。
とりあえず、このアプリをAndroidにインストールしておきます。

ログを送信する側のために、ライブラリを用意してくれています。

NordicSemiconductor/nRF-Logger-API
　https://github.com/NordicSemiconductor/nRF-Logger-API

ソースはすでに、LogIo.javaの中に実装しています。
appのbuild.gradleのdependenciesに
```implementation 'no.nordicsemi.android:log:2.3.0'```
の追加もお忘れずに。

で、使うときには以下の感じです。

```java
        LogIo logio = new LogIo(this, "testKey", "testName");
        logio.log("こんばんは1");
```

（ちなみに、ログレベルはINFO固定にしてます）

こんな感じで、nRF Loggerアプリからログをモニタリングできます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/d9d5344d-448d-b3b7-a963-3830cb8eb296.png)

以上