2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Last updated at Posted at 2020-06-20

「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はこんな感じです。

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

ログの送信

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

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

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

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

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

出力後はこんな感じ。

image.png

Android

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

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'

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

        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もログ送信可能です。

#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()などの適当なところで以下を呼び出します。

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

Node.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;

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

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

javascript

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

サーバ側の実装です。

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';
};

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で以下のように呼び出せばよいです。

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'
の追加もお忘れずに。

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

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

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

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

image.png

以上

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?