HULFT for Linux は転送とかエラーになるとき、状況により転送履歴だけでは何が発生したか完全に判明できないことがあります。いつもならターミナルにログインしてトレースログで詳細状況を確認していましたが、手間かかりますし、Linux の操作も慣れませんので、もし HULFT for Windows のようにログを画面で出せったらと思って、今回は Golang を使用してトレースログをブラウザに表示できるようにチャレンジしました!
Server Sent Events(SSE)でトレースログを転送
Rest API のようなステートレスの HTTP 通信と違って、Server Sent Events(SSE)は常時接続で、サーバー側からデータを不定期に発信することができます。また、WebSocketは双方向通信をサポートしていますが、SSEより複雑なので今回は割愛します。以下の流れでトレースログをSSEで発信することができます:
- Golang サーバーを HULFT for Linux の環境で立ち上げる
- 接続されると、トレースログファイルを開いて末尾に移動し、行単位でログを取得
- 取得したログを SSE に送る
- ソケット切断時、処理を終了(次の接続待ち)
最終的に以下のソースだけで機能を実現可能:
package main
import (
"bufio"
"fmt"
"io"
"log"
"net/http"
"os"
"strings"
"time"
)
func traceLogSSE(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Connection", "keep-alive")
log.Println("Connected!")
flusher, ok := w.(http.Flusher)
if !ok {
http.Error(w, "Streaming unsupported", http.StatusInternalServerError)
return
}
filePath := "/hulft/etc/trace"
f, err := os.Open(filePath)
if err != nil {
http.Error(w, "Cannot open file", http.StatusInternalServerError)
return
}
defer f.Close()
// ファイル末尾へ
f.Seek(0, io.SeekEnd)
reader := bufio.NewReader(f)
ctx := r.Context()
for {
select {
case <-ctx.Done():
log.Println("client disconnected")
return
default:
line, err := reader.ReadString('\n')
if err != nil {
if err == io.EOF {
time.Sleep(200 * time.Millisecond)
continue
}
log.Println(err)
return
}
fmt.Fprintf(w, "data: %s\n\n", strings.TrimRight(line, "\n"))
flusher.Flush()
}
}
}
func main() {
http.HandleFunc("/trace", traceLogSSE)
fmt.Println("Listening on :8080 ...")
log.Fatal(http.ListenAndServe(":8080", nil))
}
Chrome で http://LinuxのAPアドレス:8080/trace に接続したら、新しい転送されると、こんな感じでログを出力できています:

表示内容(列)を少なくしたい
実際出力内容を確認すると、「HULCHARACTER=」が目立つ場所に表示されているが、特に HULCHARACTER を設定していないためもし表示しなければと思います。さっそくフィルタリング関数を作成して出力列を以下のように限定しました:
- ID
- DATE
- CLASS
- HULCHARACTER
- PNAME
- PID
- DTLCODE
- MYHOST
- UID
- 本文(MSG・REQUEST・REPLY)
strings.Fields 関数で簡単にログをスプリットして処理可能ですが、ここでは、「DATE=」の出力はスペースあります、そして本文以降は内容によらず全部出力すべきことを注意できればできます。関数はこんな感じです:
func filterFields(line string) string {
fields := strings.Fields(line)
var id, date, msg string
for i := 0; i < len(fields); i++ {
switch {
case strings.HasPrefix(fields[i], "ID="):
id = fields[i]
case strings.HasPrefix(fields[i], "DATE="):
if i+1 < len(fields) && !strings.Contains(fields[i+1], "=") {
date = fields[i] + " " + fields[i+1]
i++
} else {
date = fields[i]
}
case strings.HasPrefix(fields[i], "MSG=") || strings.HasPrefix(fields[i], "REQUEST=") || strings.HasPrefix(fields[i], "REPLY="):
msg = strings.Join(fields[i:], " ")
i = len(fields)
}
}
var parts []string
if id != "" {
parts = append(parts, id)
}
if date != "" {
parts = append(parts, date)
}
if msg != "" {
parts = append(parts, msg)
}
return strings.Join(parts, " ")
}
エラーログを赤字で表示したい
正直のところ「CLASS=I」と「CLASS=W」のログよりは、「CLASS=E」を強調して表示したいです、例えば赤字、太字で表示。調べましたが、SSE だけ使用してはブラウザ上フォントを実現するのが難しので JavaScript と組み合わせて実現します。
ここでは以下のポイント:
- SSE に data だけではなく、event も発信する
- event はログにより、error・warn・info の三種類
- JavaScript の EventListener を使用して、errorが赤太字、warnはオレンジに表示
- HTML+JavaScript のホームページを用意する
Golang 側は以下の関数を用意して、Event をfmt.Fprintf(w, "event: %s\n", getCategory(line))で発信します。
func getCategory(line string) string {
switch {
case strings.HasPrefix(line, "ID=E"):
return "error"
case strings.HasPrefix(line, "ID=W"):
return "warn"
}
return "info"
}
HTML+JavaScriptはこんな感じ:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>
.error { color: red; font-weight: bold; }
.warn { color: orange; }
.info { color: black; }
</style>
</head>
<body>
<pre id="log"></pre>
<script>
const log = document.getElementById("log");
const es = new EventSource("/trace");
es.addEventListener("error", e => {
const div = document.createElement("div");
div.className = "error";
div.textContent = e.data;
log.appendChild(div);
});
es.addEventListener("warn", e => {
const div = document.createElement("div");
div.className = "warn";
div.textContent = e.data;
log.appendChild(div);
});
es.addEventListener("info", e => {
const div = document.createElement("div");
div.className = "info";
div.textContent = e.data;
log.appendChild(div);
});
</script>
</body>
</html>

