📃

GitLab ログ調査の小技たち

2023/12/01に公開

これは GitLab Advent Calendar 2023 の 1日目の記事です。

はじめに

今日も GitLab の運用おつかれさまです。
運用につきものなのがトラブルシューティング。トラブルシューティングに欠かせないのがログの調査です。

GitLab はたくさんのログを吐きます。構成や利用機能の有無で増減しますが、全部で40個近くあります。そんな膨大なログから必要な情報を抜き出すための小技たちを紹介します。

なお以下の説明は、GitLab を、最も一般的な Linux パッケージ (a.k.a Omnibus パッケージ) でインストールしている前提になっています。ソースからインストールした場合には当てはまらない情報も含まれていますので、ご注意ください。

流れるログを眺める

何かが起きるのを待っているとき、流れるログをリアルタイムで眺めたいときがあります。
下記コマンドで、前述の全部のログをリアルタイムで表示します。

sudo gitlab-ctl tail

しかしこれだとどんどん表示が流れてしまって目で追うことは不可能なので、こうすると /var/log/gitlab のサブディレクトリ単位で表示するログを絞ることができます。

sudo gitlab-ctl </var/log/gitlabのサブディレクトリ名>

例:

sudo gitlab-ctl gitlab-rails

複数のサブディレクトリのログを見たいとか、サブディレクトリ内をさらに絞りたい場合は、こういう小技が使えます。<...パス名の一部>| (OR) で繋いで複数指定できます。

gitlab-ctl tail |
  sed -nre'/^==> .*(<除きたいログのパス名の一部>).* <==/,/^$/d;/^==> .*(<含めたいログのパス名の一部>).* <==/,/^$/p'

例:

gitlab-ctl tail |
  sed -nre'/^==> .*(db-migrate|exporter).* <==/,/^$/d;/^==> .*(gitlab-rails|sidekiq).* <==/,/^$/p'

次のは GitLab に限らずわりと知られた小技だと思いますが、
いくら表示を絞っても、流れるログを目で追うのは至難の技なので、注目したい文字列をハイライトしてみます。上の各コマンドの出力を下記の grep にパイプで渡します。

| grep -E '<ハイライトしたいキーワード>|$'

gitlab-ctl tail |
  sed -nre'/^==> .*(db-migrate|exporter).* <==/,/^$/d;/^==> .*(gitlab-rails|sidekiq).* <==/,/^$/p' |
  grep -Ei 'error|$'

ローテートされたログも見たい

GitLab のログは、一定の期間または一定のサイズごとにローテートされます。
GitLab のログには大きく2種類あって、それぞれでローテートの設定や見方が微妙に違うので分けて説明します。

logrotate 配下のログ

デフォルトでは、24時間 ごとにローテートされます。
ローテートされたログも含めて全部見るには下記のコマンドを実行します。

sudo zcat -f $(ls -vr <ログファイルのパス名>*)

例:

sudo zcat -f $(ls -vr /var/log/gitlab/gitlab-rails/production_json.log*)

runit 配下のコンポーネントのログ

デフォルトでは、24時間 または 200MB の早く来た方でローテートされます。
ローテートされたログも含めて全部見るには下記のコマンドを実行します。

sudo zcat -f $(ls <ログディレクトリのパス>/*.[su] <ログディレクトリのパス>/current)

例:

sudo zcat -f $(ls /var/log/gitlab/sidekiq/*.[su] /var/log/gitlab/sidekiq/current)

JSON ログを扱う

GitLab のログは現在 JSON 形式の構造化ログへの移行を進めています (厳密には NDJSON / 改行区切り JSON)。5年越しでまだ継続中ですが、主だったログは移行が完了しています。

JSON を扱うときの強い味方が Jq です。
Jq を使ってGitLab のログを扱う例をいくつか紹介します。
(以下の例は、sudo -i してルートになって、cd /var/log/gitlab した後、という前提で書いてあります。)

全ログを横断的に検索する

Grep で -R を付けて検索していますが、-H でどのログファイルなのかも知りたい、でもそうすると出力が JSON じゃなくなってしまう、というわけで、ファイル名を sed で JSON に埋め込んでいます。
3行目は、もともと JSON 形式じゃないログ ─ runit 配下のコンポーネントのログは全部 current という名前のためファイル名で見分けがつかないので ─ をエラーにせず素通しする方法です。

grep -RHEi --include '*_json.log' --include current 'error' |
  sed -re's/^([^:]+): ?\{/{"logfile":"\1",/' | 
  jq -Rr '. as $line | fromjson? // $line'

調べたい現象がどのログに出ているのかまだわからないときに便利です。

全ログを横断的に検索してタイムスタンプでソートする

前項の応用編で、Jq 側でキーワード検索して、さらにタイムスタンプでソートしています。
Jq 側で処理するため、JSON 形式のログだけに絞っています。
Jq でソートするには、入力が array でないといけないので -s オプションを付けて、ソートした後に .[] で改行区切りに展開しています。

grep -RHEi --include '*_json.log' --include current '^{' |
  sed -re's/^([^:]+): ?\{/\{"logfile":"\1",/' |
  jq -s 'sort_by(.time)|.[]|select(.severity=="ERROR" or .status>200)'

調べたい現象が複数のログファイルにわたって出るときに便利です。

その他 Jq スクリプトいろいろ

correlation_id でグループ化

jq -s 'sort_by(.time)|group_by(.correlation_id)|.[]'

uri に特定の文字列が入っているログを抽出

jq 'select(.uri and (.uri|contains("git-upload-pack")))'

sidekiq のログから、created_at から completed_at まで 60 秒以上かかっている Ci ジョブのログを抽出する

jq 'select(.message[0:4]=="Ci::" and .completed_at and (.completed_at[0:19]+"Z"|fromdate)-(.created_at[0:19]+"Z"|fromdate) > 60)

おわりに

書き出してみると、意外にバリエーションがなくて、こじんまりした記事になってしまいました。基本的なパターンをいろんな違うシーンで使い回してたんだなー、ということを実感しています。
GitLab を運用する方々にちょっとでも参考になれば幸いです。

Discussion