Common Log Format

Common Log File Format

這篇算是一篇軟體的考古文吧,最近對部落格做了些調整,其中一個改變就是把 Google Analytic 拿掉了,一部分是因為現在已經不能用 UA 而要改用 GA4 了,然後其實我也想拿掉很久了,這次就順便把它移除,不過我還是有興趣想知道不同文章大家感興趣的程度差異,所以就又研究起以前那種根據 HTTP server log 來整理網站統計資訊的工具,其實以前一直沒成功拿掉 GA 的原因之一就是找不到好的替代工具,一直以來我比較有印象的就是 AWStats,只是那個介面我實在是受不了,然後搜尋其他替代工具的過程也不太順利,直到這次重新研究之後,發現到一個關鍵字 Common Log Format,這聽起來很一般的詞,在軟體工程界其實已經變成是一個專有名詞了。

Common Log Format 的 Wikipedia 條目 寫著這是一種 HTTP server log 的標準格式,不過我覺得應該只能算是 de facto standard(業界標準),因為沒有任何機構真的定義並作為標準發布過,現在網路上可以找到 W3C 的一份格式說明,但是那其實是 CERN 時代的 httpd 這個軟體的說明文件,趁這個機會,我就想著要來搞清楚幾個我一直很疑惑的幾個和 log 相關的單字,分別就是一開始說到的 common,然後就是 combinedextended,這幾個關鍵字都是我以前在設定 Apache HTTPD 的時後常常會看到的,甚至其它的 web server 也有用到,但是一直沒有搞的很清楚,而且使用的字我也覺得很奇怪,像是 combined 是在 combine 什麼。

結果就是,這些問題的答案,幾乎都在 NCSA(National Center for Supercomputing Applications、美國國家超級電腦應用中心) 開發的 HTTPd 軟體文件中,NCSA HTTPd 也就是最早提出這種 log format 的 HTTP server,而 NCSA HTTPd 的 log,其實預設下是有三個的,分別是:

TransferLog 其實就是現在大家說的 access log,格式就是所謂的 CLF,不過其實當時是寫作 Common Log File(CLF) Format,紀錄的資料格式就是:

host rfc931 authuser [DD/Mon/YYYY:hh:mm:ss] "request" ddd bbbb

- host: Either the DNS name or the IP number of the remote client
- rfc931: Any information returned by identd for this person, - otherwise.
- authuser: If user sent a userid for authentication, the user name, - otherwise.
- DD: Day
- Mon: Month (calendar name)
- YYYY: Year
- hh: hour (24-hour format, the machine's timezone)
- mm: minutes
- ss: seconds
- request: The first line of the HTTP request as sent by the client.
- ddd: the status code returned by the server, - if not available.
- bbbb: the total number of bytes sent, *not including the HTTP/1.0 header*, - if not available

然後文件還有定義了一個可擴充版的 Extended CLF Format,允許在這些 log 的末端加上其他的資料,如果 LogOptions 設定為Combined的話,三種 log 會 combine 在一起,使用 Extended CLF Format,多加上了 referrer 和 user-agent 資訊,這也就是 Combined 這個格式名稱的由來,而這邊有另外一個容易混淆的東西,就是 W3C 有一份很古老的 Extended Log File Format 的 Working Draft,這份文件定義的格式和 CLF 其實是沒關係的,所以看文件時,有比較仔細的文件就會寫到是 W3C 的 extended 還是 NCSA 的。

雖然我沒仔細查先後關係,不過 CERN 版的 HTTPd 應該是後來才實作了 NCSA 版的 log format,文件內則是稱為 Common Logfile Format,簡稱也是 CLF,不過單字有一點點不一樣就是,當然格式是一樣的,然後其實它也保留了 CERN HTTPd 的舊版 log,格式是:

time remotehost request

實作是:

fprintf(log, "%24.24s %s %s\n",
		asctime(gorl), HTClientHost, n_noth(HTReqLine));

其中的%24.24s我還是研究了好一陣子才看懂第一個 24 是最短長度,資料不夠長時會加上空白,然後.後面的是精確度,遇到字串時則會變成最長長度,超過的就會不輸出,asctime 則是一個內建函數可以把時間轉成字串,格式是:

Www Mmm dd hh:mm:ss yyyy

長度剛好是 24 個字元,至於那個變數名稱gorl則是我花最多時間才參透的,它的意思是:「GMT time or Local time」,但是它不是 indicator 那種二元值,而是變數本身就是那個時間,而那個時間可能是 GMT 時區時間也可能是本地時間。

這樣,其實很多細節的小疑問都有了解答,像是以前看 log 常常看到兩個-接連出現,其實代表的是連續兩個沒有值的欄位,其中一個是現在已經幾乎沒用到的 Identification Protocol(RFC1413),也是個古早的東西,我稍微看了一下好像 IRC 有用到;然後因為其實沒有標準,所以以前和現在的日期格式用的已經不一樣了,現在是普遍有加上時區,當時 NCSA 的和後來 CERN 實做的都沒有時區資訊;另外就是 Apache HTTPD 文件 的範例也有提到 RefererLog 和 AgentLog,我當時看到時就想說怎麼會有人只要這兩種資訊的 log;發現 CLF 這個專有名詞後,我也循線找到更多的 web log 分析工具了,目前我是先挑了 goaccess 來用。

最後整理一下,這三個關鍵字在 web log 的情境下時:

  • Common 格式,通常指的是 Common Log File(CLF) Format;
  • Extended,不考慮 W3C 的版本的話,這邊指的是 NCSA Extended CLF Foramt,如果 CLF Format 定義的欄位不夠用,需要更多資訊時,就可以使用這種格式,多的資訊是加在 log 尾端;
  • Combined 格式,多加了 referrer 和 user-agent 的 web log,使用 NCSA 版 Extended CLF Foramt 的格式,combine 指的是合併 TransferLog、RefererLog 和 AgentLog 三種 log。

實際上 NCSA HTTPd 就不只 Common 和 Combined 兩種,還有 ServerName 可以加上,當然也是使用 Extended 格式,不過目前流傳下來,最常見的就是這兩種了。


Archlinux 修復紀錄

Universal Studio Singapore

之前我在推特上有說過我不小心把我放 blog 的主機搞壞,當時就是用 pacman 更新過後,出現一些錯誤,我快速的重跑pacman -Syu然後就開始一直出現錯誤了,當時想說是因為我太久沒更新,然後有相依性錯誤造成系統幾乎爛掉,一度要放棄,不過因為網站相關的 instance 都還跑著,所以我就想說暫時放著,等有空把資料弄出來再重建系統,然後十一月中去了一趟新加坡,這趟行程要邊顧小孩其實很累,然後就在回來當天晚上就收到 Linode 的緊急維護,已經把我的 Linode 主機重開了,網站當然也死了,真的是晴天霹靂,不過實在太累了我也只能先放著不管。

過了幾天終於比較有力氣來看看看問題,我當時的狀況是,無法使用 pacman,然後更進一步發現是 curl 就死掉,curl 死掉會造成很多東西一起掛掉,像是 git、wget 也都掛了,結果我能使用的工具和手段就變的很少,總之先來看看錯誤訊息吧:

/usr/lib/libcurl.so.4: undefined symbol: BrotliDecoderCreateInstance

由此可知基本上問題就是動態連結 Brotli 的 library 時出錯,我還記得我當初裝機器時,Archlinux 還沒有正式 Brotli 的套件,所以我還自己編譯了一版給 nginx 用,而我的 nginx 也是自己編譯的,沒想到不知不覺 Archlinux 已經有正式的 Brotli 套件,而且 curl 還相依於它。

接著我就開始各種嘗試,想辦法重新裝 Brotli 套件,curl 雖然不能動,但是我還可以用 scp 傳檔案上去,不過就算傳上去 pacman 也還是完全無法跑起來,即使我只是想要他安裝本地的檔案,而不是要連網路,然後我也去看了/use/local/裡面 brotli 套件的 header 檔案,查看內容,發現真的沒有BrotliDecoderCreateInstance,不過這個 symbol 在 Brotli 的 repo 內是有的,而且已經存在了有四年之久,所以顯然,我系統內安裝的版本很有問題,雖然確定問題在哪,但是還是一直沒有解決方法,重裝套件需要 pacman,但是 pacman 需要修好 brotli 才能動,陷入死結當中,更糟的是,我在網路上搜尋就是找不到有一樣問題的人。

然後我就開始研究 pacman 掛掉要怎麼辦,找了許久終於找到有一個 pacman-static 的工具,是預先編譯好,並且是靜態連結的 pacman 執行檔,抓下來後發現真的可以用,真的是感動的痛哭流涕,然後我立馬執行pacman -Syu,一切執行順利,感動QQ,然後我執行了curl想確認有沒有修好,結果我再次看到了那個一樣的,熟悉的錯誤訊息...

這時我百思不得其解,我用 pacman 看安裝的套件版本確實是新的,我去解開套件來看也是新的,但是我去系統的/usr/local/下看裡面的檔案卻是舊的,重新裝了很多次也都是一樣狀況,就這樣鬼打牆很久之後,我突然察覺,/usr/local/下的東西,其實是我們手動編譯安裝的,也是路徑中優先權較高的,然後我在前面有提過,我很久以前有手動編譯安裝 Brotli 套件,終於,一切真相大白,我手動裝的時間點是五年前,所以該版本沒有BrotliDecoderCreateInstance,然後 Archlinux 用的是四年前版本,所以系統中其他需要 Brotli 的東西都會因此而掛掉,解決方法就是把手動裝的全部砍光光就好了。

不過砍掉我手動編譯的 Brotli,也同時造成我的 nginx 再起不能,因為在設定檔內它是需要我手動編譯安裝的那那個套件,解決方法是很簡單,就把需要的 module 路徑改到 pacman 安裝的套件那邊,然後我的 nginx 就可以起來了,不過我的 blog 還是死的,非 blog 的部分倒是活著,我一開始想說是 php-fpm 的問題,看錯誤訊息發現有 permission 問題,就去改 socket file permisson 成 666,然後網站還是起不來,我研究了很久,想要看看 PHP 的錯誤訊息,但是一直看不到東西,也去看 nginx error log,journalctl 也是看沒錯誤,還以為 php-fpm 是死的,還用了

<? echo phpinfo(); ?>

然後開瀏覽器看到原始碼直接回回來,搞了一陣子想起要改用<?php,改下去結果又發現一切正常,最後才發現,問題是因為我在用 pacman 更新整個系統時,把 PHP 7 升級到 8,然後我的程式碼裡面有個地方寫死大版號不對的話會回錯誤訊息,但是我沒有把錯誤訊息寫到 log 中,結果就造成我一直找不到問題點。

PHP 的問題解決後,我的 blog 就回到線上了,不過其實,這時候我的 mariadb 還是死的,透過 journalctl 看 log

sudo journalctl -xeu mariadb.service

有一行寫著:

Plugin 'InnoDB' registration as a STORAGE ENGINE failed.

一開始想說是安裝失敗,後來往上找發現還有另外一行:

InnoDB: Upgrade after a crash is not supported. The redo log was created with MariaDB 10.4.8

意思就是如果你的 DB crash 後,沒有正常關閉的狀態下,去更新 mariadb,就會有這個錯誤,不過一開始我不以為意,因為我認為我只有 upgrade,但是不知道是何時 crash 過,後來回想,應該是 brotli 爛掉時,mariadb 就跟著起不來了,總之,這個問題的解決方法,就是退回舊的 10.4.8 然後重新啟動 DB,所以就研究了一下 Archlinux 怎樣安裝舊版的套件,基本上 pacman 是不能指定版本的,有兩個方法可以裝舊版,一個是透過系統內的 pacman cache,不過我之前在修理的時候已經清掉了,所以就只能從 Arch Linux Package Archive 那邊下載特定版本的 package tar 檔案,下來用pacman -U安裝,然後因為有相依性問題,所以要把幾個需要的套件都抓下來,一起安裝:

pacman -U mariadb-10.4.8-2-x86_64.pkg.tar.xz \
  mariadb-clients-10.4.8-2-x86_64.pkg.tar.xz \
  mariadb-libs-10.4.8-2-x86_64.pkg.tar.xz

反正如果啟動失敗,也會有訊息提示你要看 log,結果把 mariadb 三個都裝下去後還真的有問題,說找不到 openssl 1.1 的檔案,所以也去抓下來手動安裝:

pacman -S openssl-1.1

至此,總算是修好了,接下來就是有時間要把我的 blog 系統容器化吧,有太多不是很好安裝的東西了。

PS. 這篇發的出去表示真的修好了。


My First Contribution to Nginx

nginx conf

因為工作上的需要,所以其實我還蠻常會編輯 nginx configuration file 的,理所當然的編輯器是用 vim,然後就會對 nginx 設定檔的支援有意見,一般人用的 nginx 設定檔的 vim script 其實是 nginx repository 的 contrib 目錄裡面的那份,這份 vim script 其實本來也是獨立的,不過原作者好像把他捐進去 nginx 裡面,之後就一直都在裡面了,也因此之後更新就很不頻繁。

然後因為檔案都放在 nginx repository 裡面,Vim 要使用其實不太方便,所以 Github 上還看的到不少人單獨抽出來,我一開始也是 fork mosky 的來用,後來就直接在自己的 repository 上面修改了,改一陣子之後就開始想要推回 upstream,也就是 nginx 的程式庫,然後就開始了這段協工旅程(?)。

要發修改上 upstream,第一步自然是看一下如何貢獻,節錄這邊幾個重點:

  1. nginx-devel 這個 mailing list 做討論
  2. 發 patch 前有一些注意事項,不過我改 vim script 比較沒關係
  3. Patch 也是用 email 發到 nginx-devel,有範例
  4. 推薦用 patchbomb
  5. 要先簽 CLA(不過目前這條已經不見了,改成最後說發 patch 等於同意用他們專案的 LCIENSE)

總之我就照這份,先去訂閱了 nginx-devel 觀察一陣子,然後就直接把我的修改整個丟上去了,一開始是直接用 Gmail 發,把 patch 檔內容直接複製貼過去,產生 patch 檔的方法是:

hg export > something.patch

hg export會直接輸出最後一個 commit 的 patch 內容到 STDOUT,然後就直接用 Gmail 發過去,結果 review 的 Maxim Dounin 說他沒辦法 apply patch,可能是因為我的 mail client 的關係,建議我用 patchbomb 發,所以就研究搜尋了一下,發現他是直接發 email 的機制,所以要把帳號密碼都寫到設定內,找了一篇 Gmail 的設定範例,搭配 Google account 的應用程式密碼,設定範例如下:

[extensions]
hgext.patchbomb =

[email]
from=othree <othree@gmail.com>
to=nginx-devel@nginx.org
cc=othree@gmail.com
method=smtp

[smtp]
host=smtp.gmail.com
port=587
username=othree@gmail.com
password=[gmail_password]
tls=True

把這些資訊填入.hg/hgrc這個檔案內,然後就可以用hg email -o --test來測試看看,這個指令會把完整的原始信件內容,包括 header 等都顯示出來(丟到 STDOUT),如果正式要發就把--test拿掉就好了。

確認一切沒問題後,我就改用 patchbomb 發 patch 到 nginx-devel 了,結果還是被拒絕了,問題主要是這個 patch 一次修改太多,理想上應該是不同目的的修改放到不同 patch 內,當然這和我一開始的預想不一樣就是了,我一開始的想法是因為 contrib 這邊的東西,相對於 nginx 本體的原始碼來說比較次要,所以盡量減少 commit 數,其實如果我有先去問過應該是可以少繞這段路,總之,為了一個一個修改送出,我又開了一個 github repository,叫做 nginx-contrib-vim-patch,想要慢慢把我的 nginx-contrib-vim 內的更動搬過去,接著開始的,就是漫長的 review 和溝通了。

其實我完全沒想到 Maxim Dounin 會這麼認真的 review,不止會看我這樣改是要達到什麼目的,還有認真測試,結果被抓出一堆問題,雖然都是奇妙的 conf 寫法,合語法,但是應該不會有人這樣寫的 case,這些 case 我也開始慢慢收集到 github 上的 nginx-conf-test,方便之後測試用,總之來回許久,終於有一部分比較簡單的東西先進去 nginx repository 了,然後我發現外部貢獻者都會在 change log 那邊被感謝,我貢獻進去的目前應該都在 1.11.11 那版,其實只有把新的 directive 補上(core modules, 3rd party modules)和幾個 protocol 參數的 highlight,至於其他的修改還進不去,目前看起來會是一場長期抗戰,主要是因為 reviewer 對於期望的目標和我不一樣,目前大概會維持兩個版本吧,一邊弄自己希望的,一邊抽東西送回去 upstream,不得不說主事者控制太緊會讓貢獻者動力被削減不少。

貢獻 nginx 的過程讓我體會到以前的開源協做的模式(應該是吧?),用 mailing list 溝通,發 Patch、code review、做討論,這些點來看,nginx 的流程其實是非常老派,和現在用 Github 做溝通、協做 的流程差很大,門檻也高不少,當然這不一定是壞事,還是要看專案性質,在 Github 這類平台上做這些協做流程的話,門檻降低了,其實可能隨之而來的問題就是太多人進來造成貢獻品質落差很大,反而會吃掉主力人員的時間,剛好今天也看到知乎上有一篇「維護一個大型開源專案是怎樣的體驗?」,裡面就有提到 VSCode 的狀況,變成還需要排人專門處理 issue 和 PR,感覺就很可怕。

順帶一題,nginx 的固定貢獻者當中不少中國人啊。


Docker 雜談

最近使用的一些感想和疑問

  • --name自動會變 network 內的 hostname,蠻方便
  • Image build 失敗,還是會產生一個東西在那邊,要 rm 掉才能重新 build...
  • Service 還不支援 update 修改 network
  • DB container 的資料要開 volume 掛比較保險(免得不小心砍到 container)
  • 在多個 node 上開 replica,掛 volume 的話都是在該 node 上找,目前似乎沒有透過網路共享 volume container 這種事
  • 很多服務的官方 docker 都是用環境變數做設定,不知道是不是本來就有的慣例
  • 一直更新 registry 上的 image 好像會越來越多垃圾,不知道是不是有清理機制
  • 整體而言,我覺得 docker service 介面是個很容易讓 fat finger 造成服務爆炸設計,不過架構規劃的好的話,應該不會太難修復就是

Ubuntu 14.04 與 MovableType 4.x

前一篇文章提到我為 SSL 安全性升級到 Ubuntu 14.04,其實升級之後遇到一些 MovableType 的相容性問題,Ubuntu 在 12.04 的時候還是用 Perl 5.14,不過到 14.04 時,Perl 升級到 5.18 了,順便提一下現在最新的穩定版是 5.20,不過 5.18 和 5.20 是同時都有在維護的。

然後我的 MovableType 是用很久以前的 4.38,用新的 Perl 會跑不起來,不過我也不太想升級,一來是 License 問題,二來是新的 MT 一個很大的架構改變是他變成是多 blog 系統,我也不太喜歡這點。總之剩下的方法就是想辦法修 bug,或是用舊的 Perl 跑,顯然後者容易許多,然後我也找到 gugod 開發的 perlbrew 這工具,類似於Ruby 的 RVM,c9s 也有寫一篇文章介紹

比較有趣的是我用了 perlbrew 安裝好 Perl 5.14 後,用which perl找到 Perl 5.14 執行檔位置然後手動修改 mt.cgi 等檔案,用 mt-check 檢查發現還缺 DBI 的套件,就用升級前就已經裝好的 cpanminus 裝了,不過怎麼裝都顯示正常但是 MT 就是一直抓不到,看了一下 cpanm 檔案原始碼才發現它用的 perl 是:

#!/usr/bin/perl

心想 gugod 怎麼可能不處理這個問題,於是搜尋一下發現可以用 perlbrew 安裝 cpanm

perlbrew install-cpanm

看了安裝出來的 cpanm 用的 perl 來源是不一樣的:

#!/usr/bin/env perl

這樣用 cpanm 裝的 Perl 套件總算可以用了,之後還有一個 5.14.2 和 5.14.4 的差異造成的錯誤,就照網路上找到的文章修正了。


[

最近因為 shellshock 的關係,跑去看了/bin/下面的東西,結果突然發現有個執行檔叫做[

ls /bin/

執行了也完全沒反應,結果和同事討論就上去 stack overflow 來問問看,然後馬上就被回說看看man [。想不到,原來 shell script 裡面的 if else condition 的[ ],其實就是這個執行檔啊,以前一直以為是個語法的 syntax...


mozjpeg 2.1

Mozilla 最近發佈了 mozjpeg 2.1,同時還有一篇 用 mozjpeg 產生高效率的 JPEG 教大家使用裡面的工具,所以我之前誤會以為現在只有 c lib 可以用,這篇文章有一些範例指令讓大家可以把cjpeg把圖片重新壓縮,小缺憾是安裝講的比較不清楚,所以我另外測試過,提供一下 Mac OSX 的指令:

cd mozjpeg
autoreconf -fiv
mkdir build && cd build
sh ../configure
make
sudo make install

關鍵的就是 BUILDING.txt 沒說build目錄是要自己建立的,不過實際測試的效果要等週一才能測試看看,目前只有編譯過確定有指令工具可以用而已。

另外一個比較容易漏掉的是要編譯需要 NASM, homebrew 可以直接brew install nasm


OSX 裝 ruby 1.9.3p392 編譯問題

最近因故要在 Mac 上重新建立工作環境,結果在裝 ruby 時遇到 compile error,看起來也不是今天才有的問題,狀況是 clang 4.1 之後才開始有的,而 OSX 10.9 要用的 XCode 5.0.1 已經是 clang 5.0 了,所以不避開這個問題會無法在新的 OSX 上裝 Ruby,包括用其他 Ruby 管理工具也是一樣,像我其實就是用 rvm 要裝,還好有解法:

CFLAGS=-Wno-error=shorten-64-to-32 rvm install ruby-1.9.3-p392

fasd, 命令列加速工具

以前曾經介紹過 autojump 這個很好用的快速切換目錄的指令,後來 大貓 跟我說有個叫 z 的,一樣用途,原理也差不多,不過 z 的位置比較好按些,最近在看 Vim Scripts 時,意外發現到有個 fasd,也是一樣的原理,不過他的功能比較強大,配合一些 alias 就可以做到和 z 或是 v 一樣的功能,作者對相關的領域很熟悉,對於常用 shell 的整合很好,像是 zsh 和 bash 的 指令補完 就都有支援,目前正在改用他,Mac 安裝很方便:

brew install fasd

然後在.bashrc或是.zshrc加上

eval "$(fasd --init auto)"

就可以了,其他環境有包好的就比較少了,可以看看他的 Wiki: Installing via Package Managers,其他的環境我測試過 Ubuntu 編譯安裝都很順利,基本上只是拷拷檔案而已。


Build v8

平常是使用 Google V8 引擎的 command line 來做自己工作機的 js console,最主要的用途是跑 jslint,不過這需要自己來編譯,以前是用 scons 來編譯,剛剛想要編譯新版發現 Google 又換了自動化工具,從 scons 換成 gyp,安裝流程其實比較簡單,先安裝好 svn,然後執行:

make dependencies
make console=readline native

產生的 d8 執行檔會在 out/native/ 下面,官方說明有提到可以用 clang 編譯,不過我嘗試後會有錯誤,還在看要怎樣處理。


此類別所有文章