「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はこんな感じです。
{
"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で指定した【ログインユーザ名】、【ログインパスワード】を入力すればログインできて以下のような画面が表示されます。
ログの送信
それではログを送信しています。
送信は、TCPで以下のようなフォーマットの文字列を送ります。
+msg|streamName1|sourceName1|this is log message\0
streamName1やsourceName1で区別されるログを表示したり非表示にしたりできます。
TCPで送信すればよいだけなので、様々なプラットフォームから送信することができます。
今回は以下で送信してみます。
・Android
・Arduino
・Node.js
・Javascript(ブラウザ)
出力後はこんな感じ。
Android
クラスファイル化してみました。
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
モジュール化しました。
'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サーバに転送してもらいましょう。
サーバ側の実装です。
'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';
};
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で以下のように呼び出せばよいです。
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アプリからログをモニタリングできます。
以上