Skip to main content

durl - 壊れたURLを検知するCLIツール

· 2 min read

結構前に開発したツールですが、まだ記事にしてなかったので紹介します。

https://github.com/suzuki-shunsuke/durl

ファイル中の URL が壊れていないかチェックするツールです。 ファイル中の URL を抽出し、HTTPリクエストを投げてステータスコードが 2xx でないものがあった場合、異常終了します。

なお、ページ内リンク(アンカー)が壊れているものについては検知できません。

インストール​

Go製で、バイナリを GitHub Releases で公開しています。

https://github.com/suzuki-shunsuke/durl/releases

Docker イメージ​

https://quay.io/repository/suzuki_shunsuke/durl

busybox ベースの Docker イメージも提供しています。 CI で使うのに便利です。

使い方​

durl init で設定ファイル .durl.yml を生成します。

$ durl init

durl check に対象ファイルパスのリストを標準入力として渡してください。 find コマンドなどと組み合わせると良いです。

https://github.com/suzuki-shunsuke/go-errlog/blob/v0.9.0/scripts/durl.sh#L9

find . \
-type d -name node_modules -prune -o \
-type d -name .git -prune -o \
-type d -name vendor -prune -o \
-type f -print | \
grep -v package-lock.json | \
grep -v Gopkg.toml | \
grep -v Gopkg.lock | \
durl check || exit 1

壊れた URL が見つかったら ファイルのパス、URL、HTTPのステータスコードを標準エラー出力します。

$ echo bar.txt | durl check
[bar.txt] https://github.com/suzuki-shunsuke/dead-repository is dead (404)

無視するドメイン​

https://github.com/suzuki-shunsuke/durl/issues/11

これらは無視します。

  • example.com, example.org, example.net
  • localhost

設定​

https://github.com/suzuki-shunsuke/durl#configuration

  • ignore_urls: 無視するURL(完全一致)のリスト
  • ignore_hosts: 無視するドメイン(完全一致)のリスト
  • http_method: HTTPリクエストのメソッド
    • head,get: HEADリクエストに失敗したらGETリクエスト
    • head: HEADリクエスト
    • get: GETリクエスト
  • max_request_count: 同時に並列でリクエストする最大数
  • max_failed_request_count: 最大の失敗URL数。これを超えると即時異常終了する
  • http_request_timeout: HTTPリクエストのタイムアウト

CI に組み込む​

ドキュメントやブログをGitHubなどでホスティングしている場合、CIに組み込むことも出来ます。

毎週30分の技術共有会

· 3 min read
Shunsuke Suzuki
Platform Engineer / OSS Developer / Go / Terraform

自分が最近職場で行っている技術共有の取り組みについて紹介したいと思います。

背景​

これまで自分は積極的に自分にとって新しい技術を取り入れてサービスの品質の向上に繋げてきました。 ただし、それらの技術に関して周りに十分に共有できていなかった側面がありました。

やっていること​

毎週30分決まった時間にスライドを使って発表しています。 対象は同じ部署の希望者です。 枠は30分ですが、実質話しているのは20分くらいな気がします。 k8sのハンズオン的なこともやりました(そのときは30分で終わらないので2回に分けてやりました)。

話したいことはたくさんあるのですが、とりあえず大きなトピックとして以下の3つに絞っています。

  1. k8s(Rancher): オーケストレーション (いまここ)
  2. Drone: CI/CD
  3. Graylog: ログ収集

これまで話したこと・話す予定のこと​

k8s の初心者が k8s を本番運用を視野に入れつつ検証環境で使ってみるところまでを目指して話しています。

  1. なぜ k8s を使うのか(部署のコンテキストに合わせて導入意義を説明)
  2. k8s のリソース(Pod, Service, Deployment, etc) について
  3. k8s, Rancher ハンズオン(2回) 簡単なアプリケーションをデプロイしてみたり
  4. Logging (いまここ)
  5. モニタリング
  6. IP制限のかかった外部サービスへアクセスする方法

毎週30分というペース感について​

以下のようなことを配慮しました。

  • 集中力が続くこと
    • 60分は長すぎる
  • 持続可能であること
    • 1, 2 回やっただけでは意味がない
    • 30分だけなら参加しやすい
    • 準備のコストも現実的な範囲
    • 30分と短めなので毎週やる。隔週とかだと頻度が少なすぎるし、1回飛ぶと1ヶ月空いてしまう

これまでの結果​

特に大きな成果があるわけではないですが、 k8sに興味を持ちk8sを検証環境で使ってくれる人が出てきました。 共有会がk8s を触るきっかけになったのだとしたらそれだけでもやってよかったと思います。

また、自分自身学ぶこともありました。 Logging に関して自分は今まで Sidecar pattern を使っていたのですが、Cluster Level Logging への移行を検討するきっかけになりました。

今後もこの取組を(無理のない範囲で)続けていきたいと思います。

Rancherでusername が重複してログインできなくなった場合の解消方法

· 2 min read
Shunsuke Suzuki
Platform Engineer / OSS Developer / Go / Terraform

先日起こった Rancher のトラブルの解消方法について紹介したいと思います。 Rancher のバージョンは v2.1.6 です。 admin ユーザーでログインしようとしたところ、エラーが起こりました。 最初パスワードが間違っているのかと思い、パスワードリセットしたものの、解消しませんでした。

https://rancher.com/docs/rancher/v2.x/en/faq/technical/#how-can-i-reset-the-admin-password

エラーメッセージをよく見ると 500 エラーでした。そこで rancher のコンテナのログを見ました。

[ERROR] API error response 500 for POST /v3-public/localProviders/local?action=login. Cause: found more than one users with username admin

username が admin のユーザーが複数人いるからログインに失敗しているようです。 であれば、ユーザーを rename ないし delete すれば解消しそうです。 しかし Admin 権限を持っているのが admin しかいないため、ユーザーを rename したり delete するのが難しいです。

どうすればよいかと思って調べてたところ rancher のコンテナ内で kubectl コマンドを使うことで Rancher の Custom Resource を操作できそうなことを知りました。

https://qiita.com/yamamoto-febc/items/498b911611dd25351ad7

そこで 2 人いる admin の片方を rename することで解消しました。

# rancher のコンテナに入る
$ docker exec -ti rancher bash
# CRDの一覧
$ kubectl get crd
# ユーザー一覧
$ kubectl get users.management.cattle.io -o yaml
# rename する user をファイルに書き出す
$ kubectl get users.management.cattle.io/<id> -o yaml > /tmp/user.yaml
# vi がコンテナにないため sed で対応
$ sed -e "s/username: admin/username: admin2/" /tmp/user.yaml | kubectl apply -f -

Goの設定管理で viper の代わりに confita を使う

· 2 min read
Shunsuke Suzuki
Platform Engineer / OSS Developer / Go / Terraform

Golang の設定管理のライブラリといえば viper が有名ですが、 confita も良さそうだったので紹介したいと思います。

confita の機能としては以下のようなものがあります。

  • 構造体に設定をマッピング
  • flag や環境変数、設定ファイルに対応
  • 複数の設定ファイルに対応

構造体に設定をマッピングすることで、https://github.com/go-playground/validator のようなライブラリを使って設定のバリデーションが出来ます。

また viper は v1.3.1 の時点で複数の設定ファイルを扱いにくいです。

Viper can search multiple paths, but currently a single Viper instance only supports a single configuration file.

k8s で ConfigMap と Secret を設定ファイルとして扱う場合、複数のファイルを扱えないと不便です。 その点 confita は複数の設定ファイルを問題なく扱えます。

以下はフラグで指定した複数の設定ファイルから設定を読み込む簡単なサンプルです。

import (
"context"

"gopkg.in/go-playground/validator.v9"

"github.com/heetch/confita"
"github.com/heetch/confita/backend"
"github.com/heetch/confita/backend/file"
flag "github.com/spf13/pflag"
)

func loadConfig(ctx context.Context) (Config, error) {
cps := flag.StringSliceP("config", "c", nil, "configuration file path")
flag.Parse()
fileBackends := []backend.Backend{}
for _, p := range *cps {
fileBackends = append(fileBackends, file.NewBackend(p))
}

loader := confita.NewLoader(fileBackends...)
cfg := Config{LogLevel: "info"} // default value
if err := loader.Load(ctx, &cfg); err != nil {
return cfg, err
}
validate := validator.New()
if err := validate.Struct(cfg); err != nil {
return cfg, err
}
return cfg, nil
}

以上、簡単な紹介でした。 viper 以外のライブラリを探している人は試してみてください。

JS以外でのnpmの活用

· 5 min read
Shunsuke Suzuki
Platform Engineer / OSS Developer / Go / Terraform

npm は Node.js のパッケージマネージャーですが、自分はJS以外のプロジェクトでも使えると思っています。 実際、Goのアプリケーション、OSS、ansible role, playbook など種類を問わず、自分が管理している多くのリポジトリで使っています。 ただ、GoのOSSで npm 使っているのは自分以外で見たことはないですし、 正直あまり賛同はされないかなと思いますが、こういう考え方もあると思っていただけたらと思います。

npm を使う理由は

  • Node製のツールを使うため
  • npm scripts を使うため (今回書きたいのはこっち)

の2つあります。

Node製のツール​

などを使っています。 Nodeはバージョンの変化が速く、互換性が壊れたりとかも多い印象ですが、 グローバルにインストールしなくてもリポジトリごとに install 出来る(package.jsonで管理できる)のでその点は(特にチーム開発では)良いと思います。

npm scripts​

npm scripts によってそのリポジトリの開発に使うコマンド群を管理するということを自分はしています。

https://github.com/suzuki-shunsuke/gomic/blob/v0.5.7/package.json

なにもツールを使わない場合に比べ、こうすることでチーム全体でコマンドを統一できますし、一連のコマンドをスクリプト化して npm scripts で実行できるようにするなど、自動化も促進されます。

ごく簡単な自動化の例ですが、tag を打つと同時にソースコード中のバージョン番号を更新するのを npm run tag v1.1.0 といったコマンドで出来るようにしています。 こうすることで tag とversionコマンドで出力されるバージョンが違うなんてことを防ぐことが出来ます。

https://github.com/suzuki-shunsuke/gomic/blob/v0.5.7/scripts/tag.sh

また、オプションによって動作が変わるようなコマンドは npm scripts によって実行することでオプションを統一できます。 例えば gofmt は -s オプションの有無で結果が変わります。

Make でいいのでは​

npm scripts ではなくて Make のほうがいいんじゃないのという意見もあるかと思います。

  • npm が特定の言語のパッケージマネージャーなのに対し、Makeはより汎用的なツールなので、Node以外ではMakeのほうが適切では
  • Makeのほうがnpm scriptsより多機能
    • 依存関係のあるタスクを管理する場合はMakeのほうが良い
  • package.jsonはjsonなのでコメントを書けないのが不便

ただ、Makeよりnpm scriptsのほうが良いと感じている部分が少なからずあります。

  • Makeには派生がある(BSD, GNU, etc)
  • Makeは複雑すぎる(1冊の本になるくらい)
    • npm scripts は機能が少ない分、とてもシンプル
  • npm scripts はカレントディレクトリに依存しない(package.jsonがあるディレクトリから実行される)
    • MakeではカレントディレクトリにMakefile がない場合、Makefileのパスを指定する必要がある
  • Makeは引数が渡しづらい(make FOO=foo みたいに引数名を指定する必要がある)

そのため、自分は npm scripts を使っています(huskyとかを使うからというのもありますが)。

それ以外のツールとの比較はしていません。 npm はかなり一般的なツールであり、そのへんのツールよりは導入障壁が低いと思います。

npm scripts の関数・エイリアス​

npm scripts をよく使う場合、関数やエイリアスを設定すると便利です。

nx() {
npm --silent run $1 -- ${@:2}
}
alias npm="npm --silent"

--silent オプションは、ログレベルの設定です。 https://docs.npmjs.com/misc/config#loglevel ログレベルを特に指定しないと、Nodeに関するログも出力されてしまい、見通しが悪くなります。

$ /usr/local/bin/npm run vet

> @ vet /Users/suzuki-shunsuke/repos/src/github.com/suzuki-shunsuke/gomic
> go vet ./...

# github.com/suzuki-shunsuke/gomic/internal/usecase/gencmd/output
internal/usecase/gencmd/output/output.go:9:14: undefined: imports
npm ERR! code ELIFECYCLE
npm ERR! errno 2
npm ERR! @ vet: `go vet ./...`
npm ERR! Exit status 2
npm ERR!
npm ERR! Failed at the @ vet script.
npm ERR! This is probably not a problem with npm. There is likely additional logging output above.

npm ERR! A complete log of this run can be found in:
npm ERR! /Users/suzuki-shunsuke/.npm/_logs/2019-02-14T22_44_50_065Z-debug.log

--silent をつけるとすっきりします。

$ /usr/local/bin/npm --silent run vet
# github.com/suzuki-shunsuke/gomic/internal/usecase/gencmd/output
internal/usecase/gencmd/output/output.go:9:14: undefined: imports

ただこのオプションつけると、npm scripts で間違ったコマンドを指定してもなんのエラーも出力してくれなくなるので注意してください。

Go の好きなところ

· 2 min read
Shunsuke Suzuki
Platform Engineer / OSS Developer / Go / Terraform

自分は 2017/8頃(曖昧)からメインで書く言語を Python から Go に変更しました。 Goを書き始めて割と早い段階でGoが一番好きになりました。 そこでなんで Go が好きなのかということを頑張って言語化しようと思います。

若干他の言語と比較する部分もありますが、決して他の言語をディスったり、 他の言語より優れているということが言いたいわけではないのでご了承ください。

  • 依存するものが小さく、バイナリ1つインストールするだけで良い
    • Prometheus の exporter とかインストールするの簡単
    • Docker Imageも最小限になる
  • 静的型付け
    • ビルド出来ている時点で一定の信頼性が担保されている
    • よく知らないコードを読んだり修正するときとかだいぶ有り難い
  • GoDocが素晴らしい
    • 何もしなくてもライブラリのドキュメントが出来上がっている
  • ライブラリの公開が容易
    • GitHubに公開するだけ
    • npm や pypi のようなレジストリがないので楽
  • go test とか go vet, gofmt みたいに標準ツールが揃っている
  • コーディング規約で悩む必要がない
  • lintツールが充実している
    • gometalinter とか使っておけば OK
    • lintできる環境を構築するのにそこまで頑張らなくて良い
  • エラーハンドリングが暗黙的に省略できないので信頼性が高い
    • Goのエラーハンドリング嫌いって人もいるし、v2で改善されるって話も聞くけど、自分はむしろ好き(面倒なのは理解できるけど)
  • 言語仕様がシンプル(客観的な根拠はないし、難しい部分もあるけど、そんな気がする)
    • メタプログラミング使った、魔術的なコードになりにくい
  • interface 使ってコードを疎結合にするのが書いてて気持ちいい
  • 並列処理が書きやすい

go-error-handling-logging-practice v0.2

· 2 min read
Shunsuke Suzuki
Platform Engineer / OSS Developer / Go / Terraform

以前 Golang のロギング・エラーハンドリングについて書きました。

それを少し v0.1 から v0.2 に互換性を壊す形でアップデートしようかと思います。 本記事ではその変更点について書きます。

変更点​

関数のエラーに情報を付与する責務を関数に割り当てていたものを、呼び出し元に割り当てるようにします。

具体的には元々

func createUser(name string, age int) error {
return errlog.Wrap(checkName(name), logrus.Fields{"age": age}, "failed to create a user")
}

だったものが

func createUser(name string, age int) error {
return errlog.Wrap(checkName(name), nil, "user name is invalid")
}

になります。

変更理由​

メタ情報のフィールド名はコンテキストに依存します。 上記の例だとユーザー名というメタ情報のフィールド名は name より user_name や admin_name, owner_name としたほうが適切かもしれません。それは関数内部では分からず、呼び出し元でないと分かりません。呼び出し元でないとフィールド名の衝突が避けられないこともあるでしょう。

メッセージに関しても同様のことが言えます。 また、元々 v0.1 ではユーザーが定義した関数と

  • 標準関数やサードパーティのライブラリなど、プロジェクト外部で定義された関数
  • interface の関数やメソッド

を区別し、前者では関数側でエラーに情報を付与させる一方、後者では呼び出し元で情報を付与させるというふうにしていました。

v0.2 では両者を区別せず、どちらの場合でも呼び出し元に付与させるというふうにすることでよりルールがシンプルになります。

Terraform Providerで import を実装する方法

· 2 min read
Shunsuke Suzuki
Platform Engineer / OSS Developer / Go / Terraform

terraform provider graylog で alert condition と stream rule の import を実装しました。

そこで import を実装する方法を紹介したいと思います。

terraform でリソースをimportするにはリソースがimportをサポートしている必要があります。 schema.Resource の Importer フィールドですね。リソースがIDだけでGet出来る場合、schema.ImportStatePassthroughを使えば終わりです。 一方、Graylogのalert condition や stream rule はIDだけでなく、stream id も必要になります。 terraform import コマンドは1つの引数しか取らないため、サポートできないのでは?と以前まで思っていました。 そういった場合、次のようにStateFuncを実装すればサポートできます。

https://github.com/suzuki-shunsuke/go-graylog/pull/59/commits/baee1165f49d2bc21b6ea7551ceff6b7daf01543#diff-f41be2a3640efd12ad4e808d77c5c8d5

# "/" で区切って stream id と ID を渡す
$ terraform import graylog_alarm_callback.test 5bb1b4b5c9e77bbbbbbbbbbb/5c4acaefc9e77bbbbbbbbbbb

区切り文字は何でも良いのでしょうが、公式のprovider が "/" で区切っていたのでそれに従うことにしました。

https://www.terraform.io/docs/providers/google/r/spanner_database.html#import

https://godoc.org/github.com/hashicorp/terraform/helper/schema#ImportStatePassthrough の実装を見てみれば分かりますが、 StateFunc の中では GET API を叩いてリソースを取得したりはしません。 terraform import コマンドの標準出力を見ると分かりますが refresh を実行しているのでそこでGETしているようです。 StateFunc は *schema.ResourceData の配列を返しますが、これは1度のGETで複数のリソースを取得するようなAPIをサポートするためのようです。

以上、terraform import の実装方法について紹介しました。

GithubをFree Planにダウングレードした

· 3 min read
Shunsuke Suzuki
Platform Engineer / OSS Developer / Go / Terraform

GitHub のプラン体系が変わり、無料プランでも無制限でprivate repositoryが作れるようになりました。

https://github.blog/2019-01-07-new-year-new-github/

そこで無料プランにダウングレードすることにしました。

無料プランではwikiはpublic repositoryでしか使えないので、 private repository の wiki を 移行することにしました。

private なソースコード(サービス)のためのwikiではなく、 個人的なメモが書いてあるだけだったので移行することに特に問題はありませんでした。

全 private repository の wiki を clone​

そこでまずはそういった wiki を clone して一つのリポジトリにまとめることにしました。

https://github.com/suzuki-shunsuke/foo の wiki は https://github.com/suzuki-shunsuke/foo.wiki で clone できます。

次のようなコマンドを実行し、private repositoryのwikiを全部cloneしました。

https://developer.github.com/v3/repos/#list-your-repositories

curl "https://api.github.com/user/repos?access_token=$GITHUB_TOKEN&visibility=private" | jq -r '.[].html_url' | xargs -I{} -n 1 git clone {}.wiki

wikiが存在しないものに関しては clone に失敗します。 API で wiki のリストが取得できると良かったんですが、 wikiに関するAPIはなさそうです。

また /user/repos API のレスポンスの has_wiki はwikiが存在しなくても、wikiが無効化されてなければ true なようです。

wiki を notable に移植​

clone した wiki を notable に移植します。

notable は moon gift で最近紹介されていて良さそうだったので使ってみます。

Hugo に移植しても良かったのですが notable が気になったので notable にしました。 notable を使ってみてダメそうだったら Hugo も検討します。

Hugo に比べてnotableが良いところは

  • シンタクスハイライト
  • tagによる絞り込み
  • 検索

がデフォルトで(特に手を加えたりしなくても)いい感じに使えることです。

notable の data directory を private repository で管理します(Dropbox などのクラウドストレージで管理しても良いと思います)。

専用の private repository を1つ作成し、data directory を作成し、notable の設定で作成したdata directory をnotable の data directory にします。

notable の data directory にはバイナリではなく、plain text として wiki が作られるようなので移植も header の部分を notable に合わせて data directory に放り込むだけで良いので簡単でした。

header はこんな感じです。

---
title: Ansible action plugin について
tags: [ansible]
---

意外と移行対象のwikiが少なかったので1, 2時間で終わりました。 終わったあとプランを無料プランに変更して作業は終了しました。

Go におけるエラーハンドリングとロギングのプラクティス

· 8 min read
Shunsuke Suzuki
Platform Engineer / OSS Developer / Go / Terraform

2018-12-30 追記​

この記事を元にドキュメントを書いてみました。

https://github.com/suzuki-shunsuke/go-error-handling-logging-practice

追記ここまで


Go でエラーハンドリングとロギングをしてきて自分の中で固まりつつあるプラクティスを明文化します。 明文化することで以下のことを目指します。

  • 迷いをなくす
  • コードの一貫性を保つ
  • コーディング規約とすることでレビューの品質を上げる(自動化は出来ないけど)
  • コードの品質を上げる(コードがゴチャつかなくなる)
  • 適切にエラーをロギングする(必要十分な情報をログとして残す)

またエラーハンドリングとロギングのためのライブラリを自作しているのでそれも紹介します。

https://github.com/suzuki-shunsuke/go-errlog

ロギングに関する関連記事​

この記事を書く前に軽くググってみただけでちゃんと読んでないのですが、 興味のある人は読んでみてください。

ログレベルは分ける​

ログレベルでwarningとかいらないという意見もありますが、自分は必要だと思っています。 自分は以下のログレベルを使い分けます。

  • debug: あまり使わない。調査目的で一時的に埋め込むログ。調査が終わったら出力しないようにする。一時的でないものはinfoにする
  • info: エラーでないログ。イベント、処理の開始時や終了を記録するのに使うことが多い
  • warn: 4xx系のエラー。それが起こっただけではアラートを飛ばさないが、数が通常時より多い場合はバグかUIに問題があってユーザーが間違えやすくなっている可能性があるのでアラートを飛ばす
  • error: 5xx系のエラー。アラートを飛ばす(閾値は調整)
  • fatal: 処理継続が不可能な致命的なエラー。システムを止める

書いてから思いましたが、これに関しては標準的な使い分けのルールがありそうですね(要調査)。。

logrus を使ってログを構造化する​

前提としてwebシステムやバッチシステムなどを想定しています。CLIツールならば話は変わるでしょう。 JSONフォーマットで出力してfluentdでElasticsearchにフォワードするのが個人的によくあるパターンです。

go-errlogもlogrusの使用を前提としています。

ロギングのライブラリは他にも色々あるので、logrusで満足できない人は以下から探してみるとよいでしょう。

https://github.com/avelino/awesome-go#logging

エラーログは中央集権的に main に近い所で出力する​

エラーログをどこで出力するかですが、原則中央集権的に main に近い所で出力します。 因みに中央集権的という表現は echo の centralized error handling からもじっています。

https://echo.labstack.com/guide/error-handling

error が発生してもすぐログを吐くのではなく、error を関数の戻り値として返し、ロギングする責務を親に委譲します。 Goでは以下のようなイディオムがよく見られますね。

if err != nil {
return err
}

ロギングに必要な情報を戻り値のerrorに含める​

上記のコードで問題なのは、エラーに関する情報が欠損することがあることです。

これに関しては以下の記事が参考になります。

https://deeeet.com/writing/2016/04/25/go-pkg-errors/

エラーに関する情報には2種類あると個人的に考えていて「メッセージ」と「メタ情報」なんて風に脳内で呼んでたりします。

  • メッセージ: エラーの原因を示すhuman readable なテキスト(pkg/errorsはこれに対応している)
    • リストになる
  • メタ情報: エラーに関する構造化されたデータ
    • ハッシュになる

例えば foo というユーザー名が既に使われていてユーザーの作成に失敗した場合

  • メッセージ
    • username is already used
    • invalid username
    • failed to create a user
  • メタ情報
    • username: foo

と言った感じになります。 メッセージにメタ情報を含めて "foo" is invalid username といった風にも出来ますが、そうすると検索・集計しづらかったり、メッセージの生成に一手間かかったりするのでメッセージにはメタ情報を含めません。

pkg/errors だとメタ情報には対応できないので自分でライブラリを作りました。

https://github.com/suzuki-shunsuke/go-errlog

こんな感じになります。

return errlog.Wrap(err, logrus.Fields{"username": "foo"}, "failed to create a user")

error に含める情報の責務​

上記のように error に情報を含める場合、どこまで含めるかというのが問題になります。 ここでプラクティスとして、 関数がerrorを返す場合、その関数がもっている情報は全て含める責務があり、 逆に子関数から返ってきたerrorには子関数に渡っている情報が含まれているので呼び出し元で付与する必要はないというふうにしています。

func createUser(name string, age int) error {
if err := checkName(name); err != nil {
return errlog.Wrap(err, logrus.Fields{"age": age}, "failed to create a user")
}
}

つまり上のコードでは子関数に渡っているメタ情報nameや、invalid username のようなメッセージを errlog.Wrap に渡す必要はありません。 上記の例だとエラーに関係ない age も渡す必要はないのではないかとも考えられますが、原則ログに残すこととします。

ただし、子関数が標準関数やサードパーティのライブラリなど、プロジェクト外部で定義された関数であれば話は別です。 それらがどのようなエラーを返すかは保証がありません。

if f, err := os.Open(filename); err != nil {
return errlog.Wrap(err, logrus.Fields{"filename": filename}, "failed to open a file", "failed to create a user")
}

上記の例だと、os.Openに渡したメタ情報 filename や os.Openに失敗したことを示す failed to open a file といったメッセージもerrlog.Wrapに渡しています。

errlog.Wrap は複数のメッセージを渡せるようになっています。 メッセージの順番は左からイベントが発生した順になるようにします。 上記の例だと「ファイルのオープンに失敗」した結果、「ユーザの作成に失敗」するという順序になります。

エラーのロギングはシンプルに​

go-errlogではシンプルにロギングを記述できます。

logger := errlog.NewLogger(nil)
// err != nil なら logging する
// err がメタ情報を持ってたら logrusで構造化してロギングする
// メッセージも pkg/errors のように一つのテキストに連結してロギング
logger.Fatal(createUser("foo", 10))

その他 go-errlog の機能​

メタ情報やメッセージによって条件分岐したり出来るようにヘルパー関数を幾つか提供しています。

  • CheckField
  • HasField
  • HasMsg

詳細はGoDocやソースコードを見てください。

最後に​

色々書いてしまいましたが、一番言いたかったことは

関数がerrorを返す場合、その関数がもっている情報は全て含める責務があり、 逆に子関数から返ってきたerrorには子関数に渡っている情報が含まれているので呼び出し元で付与する必要はないというふうにしています。

ただし、子関数が標準関数やサードパーティのライブラリなど、プロジェクト外部で定義された関数であれば話は別です。

の部分です。この辺は元々自分の中でルールが決まってなくてずっとモヤモヤしてて、 コードを書くたびにぶれてたのですが、「こうすればいけるんじゃないか」と思いつき、その実装を補助するライブラリを開発し、 実践したところ今の所そこそこうまく行っています。 ただまだ日が浅いので少しずつブラッシュアップされていく部分もあると思いますが、 その場合でも「なんとなく」ではなく、可能な限り明文化していくことで、迷いをなくし、コードとログの品質を上げていきたいと思います。