用 NXLog 收應用程式的文字記錄檔送到 Graylog 時,常會發現 Graylog 上的時間和記錄裡寫的時間對不起來。原因是 NXLog 預設用「讀到這一行的時間」當作訊息時間,而不是記錄本身發生的時間。
平常讀取即時寫入的檔案,誤差只有幾秒,看不太出來;但只要 NXLog 重新啟動、補讀一批舊檔案,或是網路中斷一段時間,所有訊息的時間就會擠在同一刻,在 Graylog 依時間查詢、做告警就全部失準。這篇整理兩種把記錄裡的時間(以下稱 LogEventTime)蓋回 timestamp 的做法。
時間是怎麼決定的
我去看了 NXLog CE 的原始碼,xm_gelf 產生 GELF 的 timestamp 時,依序取:
| 順序 | 欄位 | 說明 |
|---|---|---|
| 1 | $EventTime | 必須是 datetime 型別 |
| 2 | $EventReceivedTime | NXLog 讀到這筆的時間 |
| 3 | 當下時間 | 都沒有的話 |
用 im_file 讀文字檔時,NXLog 不會自己解析每一行的時間,所以 $EventTime 是空的,最後送出去的就是 $EventReceivedTime。
另外有一個細節:NXLog CE 送出的 GELF timestamp 只到秒,原始碼裡會把微秒除掉。如果記錄的時間有毫秒,而你需要依毫秒排序,就要用後面 Graylog 端的做法。
範例記錄
以下面這種常見格式為例:
2026-01-19 10:15:32.1234 INFO [OrderService] order created, id=10023
2026-01-19 10:15:32.5678 WARN [OrderService] payment timeout, retry=1
做法一:在 NXLog 解析時間
在 NXLog 端用正規表示式取出時間,轉成 datetime 指給 $EventTime,GELF 的 timestamp 就會直接用它:
<Input in_app>
Module im_file
File "D:\\logs\\app\\*.log"
SavePos TRUE
ReadFromLast TRUE
Exec if $raw_event =~ /^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\.(\d+)/ \
{ \
$LogEventTime = $1 + "." + $2; \
$EventTime = strptime($1, "%Y-%m-%d %H:%M:%S"); \
}
Exec $ShortMessage = substr($raw_event, 0, 250);
</Input>
strptime()
依指定格式把字串轉成 datetime,NXLog CE 內建$LogEventTime
保留含毫秒的原始時間字串,一起送到 Graylog 備查- 時區
strptime()以 NXLog 主機的本地時區解讀,主機時區要設對
優點是 Graylog 不用另外設定;缺點是只到秒,而且每種記錄格式都要在 NXLog 寫一次。
做法二:在 Graylog 用 Pipeline 覆寫
NXLog 只負責把時間字串取出來放進 LogEventTime 欄位(做法一的 $LogEventTime = ... 那行即可,不用設 $EventTime),由 Graylog 的 Pipeline Rule 轉成 timestamp:
rule "LogEventTime_Override_Timestamp"
when
has_field("LogEventTime")
then
let ts = parse_date(
value: to_string($message.LogEventTime),
pattern: "yyyy-MM-dd HH:mm:ss.SSSS",
locale: "en",
timezone: "Asia/Taipei"
);
set_field("received_timestamp", $message.timestamp);
set_field("timestamp", ts);
end
pattern
Joda 格式,SSSS對應 4 位數的小數秒,要和記錄的格式一致timezone
記錄裡的時間是哪個時區就填哪個,台灣是Asia/Taipei;沒填會被當成 UTC,時間會差 8 小時received_timestamp
覆寫前先把原本的時間存起來,日後要查「延遲了多久才收到」時也可以使用
這個做法保留了毫秒,也可以集中管理,所有 NXLog 只要負責送字串就好。
格式不符的保護
記錄裡偶爾會有不是時間開頭的行(例如例外的堆疊追蹤),parse_date() 解析失敗會讓這筆訊息處理出錯。可以在條件裡先用正規表示式確認格式:
rule "LogEventTime_Override_Timestamp"
when
has_field("LogEventTime")
&& regex("^\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}\\.\\d{4}$", to_string($message.LogEventTime)).matches == true
then
let ts = parse_date(to_string($message.LogEventTime), "yyyy-MM-dd HH:mm:ss.SSSS", "en", "Asia/Taipei");
set_field("received_timestamp", $message.timestamp);
set_field("timestamp", ts);
end
套用到串流
Pipeline 建立後要連結到這些記錄會進入的串流,並在「System → Configurations → Message Processors」確認 Pipeline Processor 有啟用。設定前可以用 Pipeline 頁面的「Simulate processing」貼一筆訊息試看看結果。
兩種做法比較
| 項目 | NXLog 端(EventTime) | Graylog 端(Pipeline) |
|---|---|---|
| 精確度 | 秒 | 毫秒 |
| 設定位置 | 每台 NXLog | Graylog 集中管理 |
| 時區 | 依 NXLog 主機 | 規則裡明確指定 |
| 解析失敗 | 不設 EventTime,退回接收時間 | 要自己加條件保護 |
| Graylog 負擔 | 無 | 每筆多一次解析 |
我自己的習慣是用做法二:NXLog 的設定越簡單越好,格式、時區這些規則集中在 Graylog,改起來也方便。
注意事項
- 時間差太多的訊息
覆寫成很久以前的時間後,訊息仍然寫進目前的 index,但依時間範圍查詢時只會看到落在範圍內的。補讀大量舊記錄時,查詢要記得把時間範圍拉大 - 伺服器時間要同步
產生記錄的主機時間不準,覆寫後反而更亂,所有主機都要設好 NTP - Windows 事件記錄不用這樣做
im_msvistalog讀到的事件本身就帶有EventTime,NXLog 會直接使用
結論
記錄的時間是事後追查的基準,用「收到的時間」代替「發生的時間」,平常看不出差別,真的出事要對時間軸時才會發現對不上。用 NXLog 收文字記錄檔的話,建議一開始就把時間解析做好。
參考資料
- Graylog 文件:Pipeline Functions(parse_date)
https://go2docs.graylog.org/current/making_sense_of_your_log_data/functions.html - Graylog 文件:Pipelines
https://go2docs.graylog.org/current/making_sense_of_your_log_data/pipelines.html - Joda-Time:DateTimeFormat
https://www.joda.org/joda-time/apidocs/org/joda/time/format/DateTimeFormat.html - NXLog Community Edition
https://nxlog.co/products/nxlog-community-edition



