この記事は、ラクス Advent Calendar 2024 シリーズ2の19日目の記事です。裏番組なので気楽にいきます。
いよいよ2024年も終わりが見えてきました。この記事では、今年をふりかえって、私の日々の開発業務をほんの少しだけ楽にしてくれた小さな改善たちを紹介したいと思います。2024年に新しく始めたものしばりで思い出そうとしたのですが、すでに全然思い出せないです…
logfmt: JSONログの整形
今年は、担当プロダクトのアプリログをJSON化して本番投入しました。今までは人間の読みやすさ重視だったのですが、メトリクスやログをGrafanaに一元化できるよう機械の扱いやすさ重視に変わりました。何気に本番環境のログをJSONにするのは初めてです。
…結果、人間としてはめちゃくちゃ読みづらくなりました。特に、複数行のスタックトレースが \n
や \t
で1行にまとめられてしまい、横スクロールが破綻するのでお手上げです。
{"datetime": "2024-12-17T15:37:04.548","message": "localhost:5432 への接続が拒絶されました。ホスト名とポート番号が正しいことと、postmaster がTCP/IP接続を受け付けていることを確認してください。","logger_name": "com.zaxxer.hikari.pool.HikariPool","thread_name": "pool-4-thread-2","level": "ERROR","stack_trace": "org.postgresql.util.PSQLException: localhost:5432 への接続が拒絶されました。ホスト名とポート番号が正しいことと、postmaster がTCP/IP接続を受け付けていることを確認してください。\n\tat org.postgresql.core.v3.ConnectionFactoryImpl.openConnectionImpl(ConnectionFactoryImpl.java:342)\n\tat org.postgresql.core.ConnectionFactory.openConnection(ConnectionFactory.java:54)\n\tat org.postgresql.jdbc.PgConnection.<init>(PgConnection.java:263)\n\tat org.postgresql.Driver.makeConnection(Driver.java:443)\n\tat org.postgresql.Driver.connect(Driver.java:297)\n\t...\n\tat java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1136)\n\tat java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:635)\n\tat java.base/java.lang.Thread.run(Thread.java:840)\nCaused by: java.net.ConnectException: Connection refused\n\t ...\n\tat org.postgresql.core.v3.ConnectionFactoryImpl.tryConnect(ConnectionFactoryImpl.java:132)\n\tat org.postgresql.core.v3.ConnectionFactoryImpl.openConnectionImpl(ConnectionFactoryImpl.java:258)\n\t... 61 common frames omitted\n","trace_id": "a3e528e8b59f835967a3cf024de43885","version": "1.0.0"}
というわけで、こうです1。
$ alias logfmt='jq . | sed "s/\\\\n/\\n/g; s/\\\\t/\\t/g"'
$ pbpaste | logfmt
{
"datetime": "2024-12-17T15:37:04.548",
"message": "localhost:5432 への接続が拒絶されました。ホスト名とポート番号が正しいことと、postmaster がTCP/IP接続を受け付けていることを確認してください。",
"logger_name": "com.zaxxer.hikari.pool.HikariPool",
"thread_name": "pool-4-thread-2",
"level": "ERROR",
"stack_trace": "org.postgresql.util.PSQLException: localhost:5432 への接続が拒絶されました。ホスト名とポート番号が正しいことと、postmaster がTCP/IP接続を受け付けていることを確認してください。
at org.postgresql.core.v3.ConnectionFactoryImpl.openConnectionImpl(ConnectionFactoryImpl.java:342)
at org.postgresql.core.ConnectionFactory.openConnection(ConnectionFactory.java:54)
at org.postgresql.jdbc.PgConnection.<init>(PgConnection.java:263)
at org.postgresql.Driver.makeConnection(Driver.java:443)
at org.postgresql.Driver.connect(Driver.java:297)
...
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1136)
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:635)
at java.base/java.lang.Thread.run(Thread.java:840)
Caused by: java.net.ConnectException: Connection refused
...
at org.postgresql.core.v3.ConnectionFactoryImpl.tryConnect(ConnectionFactoryImpl.java:132)
at org.postgresql.core.v3.ConnectionFactoryImpl.openConnectionImpl(ConnectionFactoryImpl.java:258)
... 61 common frames omitted
",
"trace_id": "a3e528e8b59f835967a3cf024de43885",
"version": "1.0.0"
}
多少くずれるけど十分過ぎます。実際には、GitHub Issueに貼ったり、チャットに貼ったりしてチームメンバーに共有することがほとんどなので、こうです2。
$ pbpaste | logfmt | pbcopy
gh-check: GitHubのあれやこれやのチェック
フロントエンドとバックエンドのリポジトリが分かれたり、運用や補助ツールのためのリポジトリが分かれたりと、開発が進んでいくとマイクロサービスでなくてもリポジトリが増えていきます。そうなってくると、GitHubの1つ1つのリポジトリをブラウザで見にいって、特定のラベルをもつIssueが増えていないかとか、ワークフローが失敗していないかとか、確認するのはいちいち面倒です。
というわけで、こうです。GitHub CLI を使って、シェルスクリプトで一括チェックします。
#!/bin/bash
REPO_BACKEND=awesome-backend
REPO_FRONTEND=awesome-frontend
REPO_OPERATION=awesome-operation
echo "======================================================================"
echo "Closed Issue/PR without milestone"
echo "======================================================================"
for repo in "${REPO_BACKEND}"; do
gh issue list --repo $repo --search "is:closed no:milestone";
gh pr list --repo $repo --search "is:closed no:milestone";
done
echo
echo "======================================================================"
echo "Vulnerability Issue"
echo "======================================================================"
for repo in "${REPO_BACKEND}" "${REPO_FRONTEND}" "${REPO_OPERATION}"; do
gh issue list --repo $repo --label "security"
done
echo
echo "======================================================================"
echo "Vulnerability scan actions"
echo "======================================================================"
for repo in "${REPO_BACKEND}" "${REPO_FRONTEND}" "${REPO_OPERATION}"; do
echo $repo
gh run list --repo $repo --workflow "Scan container image daily" --limit 1
done
結果はこんなイメージ。
$ gh-check
======================================================================
Closed Issue/PR without milestone
======================================================================
Showing 1 of 1 issue in awesome-backend that matches your search
ID TITLE LABELS UPDATED
#3644 bug: エラーメッセージに統一性が無い discard about 21 hours ago
no pull requests match your search in awesome-backend
======================================================================
Vulnerability Issue
======================================================================
Showing 2 of 2 issues in awesome-backend that match your search
ID TITLE LABELS UPDATED
#3616 awesome-backend:0.9.0 security about 6 hours ago
#3598 awesome-backend:0.8.0 security about 6 hours ago
Showing 1 of 1 issues in awesome-frontend that match your search
ID TITLE LABELS UPDATED
#3336 awesome-frontend:0.8.0 security about 6 hours ago
no pull requests match your search in awesome-operation
======================================================================
Vulnerability scan actions
======================================================================
awesome-backend
STATUS TITLE WORKFLOW BRANCH EVENT ID ELAPSED AGE
✓ Scan container image daily Scan container image daily develop schedule 12382195765 1m28s about 6 hours ago
awesome-frontend
STATUS TITLE WORKFLOW BRANCH EVENT ID ELAPSED AGE
✓ Scan container image daily Scan container image daily develop schedule 12382156609 1m20s about 6 hours ago
awesome-operation
STATUS TITLE WORKFLOW BRANCH EVENT ID ELAPSED AGE
✓ Scan container image daily Scan container image daily main schedule 12382173232 1m4s about 6 hours ago
本当はこれ自体をGitHub Actionsに組み込んで成否を自動判定させる方がよいと思いますが、複数リポジトリにアクセスさせるときの認証が面倒なので、いったん自分のトークンでローカル実行する簡易対応にとどめています。
copy-markdown-url: 完全に自分好みURLコピー
世の中にはURLをコピーするための数多のブラウザ拡張がありますが、タイトル文字列とURLをWebページのどこから抽出するか柔軟に選べるものはありません(嘘です、探してないです)。大抵は、HTMLのtitle要素から抽出すると思いますが、サイト名などの付属情報がいっぱい付いてしまい、チャットなどで連携するとノイズになりがちです。
そしてWebデータベースともなると、データ項目を自由に設定できるため、どこをタイトルとして抽出すべきかは個々のデータベースごとに変わります。Webデータベース側でいい感じに設定できるといいのですが、私が使っているものはできません。
というわけで、user.jsを使って力技で何とかします。
この手のスクリプトを書くときに、トリガーとなるボタンなり何なりをWebサイト上に埋め込むのが面倒だなといつも思っていたのですが、実はコンテキストメニューに追加できることを知りました。結構便利です。
// ==UserScript==
// @name Copy Markdown URL
// ...
// @match *://*/*
// @run-at context-menu
// @grant GM_setClipboard
// ==/UserScript==
(function() {
'use strict';
// (URL, タイトル文字列) のペアを抽出
function extractURL() {
const url = window.location.href;
const title = document.title;
if (url.match(/https:\/\/github.com\/.+\/pull\/.+/)) {
// GitHub Pull Request
const match = title.match(/^(.+) by (.+) · Pull Request #(\d+) · (.+)\/(.+)$/);
if (match) {
return [ url, `${match[5]}#${match[3]}: ${match[1]}` ];
}
} else if (url.match(/^https:\/\/webdb.example.com\/.+/)) {
// Webデータベース
const mainDocument = parent.document.querySelector('iframe#main').contentDocument;
const pageTitle = mainDocument.querySelector('.fw-pankuzu-box').innerText;
const link = mainDocument.querySelector('#js-copy-page-url').getAttribute('data-url');
if (pageTitle.includes("調査依頼")) {
// このWebデータベースからハードコーディングで特定項目の情報を抽出
const id = mainDocument.querySelectorAll('.table-box td')[0].innerText.trim();
const summary = mainDocument.querySelectorAll('.table-box td')[8].innerText.trim();
if (id && summary) {
return [ link, `調査依頼${id}: ${summary}` ];
}
} else if (pageTitle.includes(...)) {
...
}
} else if (...) {
...
} else if (...) {
...
}
return [ url, title ];
}
// URLを抽出してMarkdown形式でクリップボードにコピー
const [ url, title ] = extractURL();
const text = `[${title}](${url})`
GM_setClipboard(text, "text");
})();
私は気づきました。Webデータベースごとにこの分岐を足していくのはつらい、つらすぎる。これは完成しないと。
broken-ci: 不安定なCIのチャット通知
REST APIを開発するときに、「ワンパスはDBも含めた自動テストを書く」をチーム方針としています。テストコードを頑張って書くのはよいのですが、やはりDBを含めたテストにかかる時間が伸び続け、とうとうPRで全テストが通るのを待てなくなりました。
苦渋の決断として、PRではDBなしのテストコードだけを実行し、DBありのテストコードはマージ後に統合ブランチで実行することにしました。が、案の定マージ後にテストがコケます。しかも、CIエラーがマージ者にしか通知されないため、どうしても気付くのが遅れたり、「CIコケてるよ」連絡を個別にしたりと、いろいろ億劫でした。
というわけで、よくある話ですが、チャットに自動通知します。
今回面倒なのは、GitHubはインターネット側、チャットのMattermostはイントラネット側で稼働しているため、GitHub Actionsから普通には通知を送れないという点です。仕方がないので、テスト実行と通知するジョブを分離し、通知ジョブだけはイントラネット上の self-hosted runner を使って実行することで、疎通の問題を回避します。
jobs:
...
test:
...
notify:
runs-on: [ self-hosted, awesome-runner ]
timeout-minutes: 1
needs: [ test ]
if: ${{ failure() && (github.ref_name == 'main' || startsWith(github.ref_name, 'develop')) }}
steps:
- name: "Notify to Mattermost"
run: |
url="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}"
label="${GITHUB_REPOSITORY} > ${GITHUB_REF_NAME} > actions/runs/${GITHUB_RUN_ID}"
msg="${{ github.event.head_commit.message }}"
curl -s -i -k -X POST -H 'Content-Type: application/json' \
-d "{\"channel\": \"awesome-channel\", \"username\": \"不安定なCI\", \"icon_emoji\": \":fire:\", \
\"text\": \"@実装チーム CIが失敗しました (triggered by \`@${GITHUB_ACTOR}\`) #broken-ci\n* [${label}](${url})\n\`\`\`\n${msg}\n\`\`\`\"}" \
${{ secrets.MATTERMOST_WEBHOOK_URL }}
まとめ
2024年に取り組んだちょっとした改善を紹介しました。日々の開発で不便に思ったことをどう楽にできるか考えているときほど楽しい瞬間はないですよね。来年もやっていき。