任意の文字列をマスクするmaskedioパッケージを作ったが実装に不満がある

runnトークンやパスワードといった機密データをマスクする機能を作りたかったので、そのためのパッケージを作りました。

github.com

機能は単純で、maskedio.NewWriter(w io.Writer) で、任意の io.Writer を内包したインスタンス *maskedio.Writer を作成します。*maskedio.Writerio.Writer を満たすのでそれを代わりに使うというものです。 そして、任意のキーワードを登録すると、そのキーワードが指定の文字列(デフォルトは ***** )で置換されて、元の io.Writer に渡されます。

一見するとシンプルなのですが、1つだけ実装に不満があります。

厳密にキーワードをマスクしようとすると、どうしてもフラッシュ処理(末尾処理)が必要になる

io.Writer は Writeメソッドだけを持つインターフェイスです。

type Writer interface {
    Write(p []byte) (n int, err error)
}

例えば、passw0rd というキーワードをマスクしようとした時、単純な実装だと

Write([]byte("xxxxxpassw0rdxxxx")

ならマスクできるのですが、

Write([]byte("xxxxxpass"))
Write([]byte("w0rdxxxx"))

となると、マスクに失敗してしまいます。

そこで、マスク失敗を回避するために maskedio では内部にバッファを持って、入力の末尾がキーワードに合致しそうだったら、バッファに溜め込んで次のWriteを待つようにしました。

しかし、そのようなバッファを持ってしまうと、次に問題となるのが次のような末尾が中途半端にキーワードに合致してしまうケースです。

Write([]byte("xxxxxpass"))
// 終わり

バッファに溜まっている値(今回のケースだと []byte("xxxxxpass") を何かをトリガーに内部のio.Writer に書き出す必要があります。

ここまでで、気づいた方はいるかと思いますが、これは bufio.Writer と同じです。

bufio.WriterFlush() メソッドで末尾の書き出しをサポートしています。

maskedio.Writer も同様に Flash() メソッドを持たせていますが。maskedioのユースケースの性質上、どうにかして io.Writer の interface のメソッドだけで機能を実現したいと考えました。

時間差でフラッシュを自動実行

そこで苦肉の策として実装したのが、時間差での自動Flushです。

maskedio/maskedio.go at e9228162f440ff3c778fe2565fab4aa2534dea1f · k1LoW/maskedio · GitHub

// Auto flush
go func() {
    time.Sleep(100 * time.Microsecond)
    _ = w.Flush()
}()

この実装に不満があります

100 * time.Microsecond に強い根拠はないし、全てケースをカバーできるわけではない実装です。

かと言って、io.Writer + Flush() をライブラリとして強いるもの違う気がしています。

結局

解決できずに今に至ります。

厳しい。。

エラーをスタックトレースも含めて構造化ログで取得できる github.com/k1LoW/errors を作った

エラーパッケージを作った話です。

www.m3tech.blog

私は、上記のエントリの「ぼくがかんがえるエラー処理の要件」に完全同意*1で、パフォーマンスをある程度犠牲にしてでもスタックトレースを「カジュアルに」受け取りたいと考えています。

また、

tech.kanmu.co.jp

にもあるように「移行のしやすさ」も欲しいです。

というのも、

methane.hatenablog.jp

にあるように、将来的にGoの標準パッケージにスタックトレースを取得できる機能が登場する可能性があるからです。標準パッケージ最高。

で、

speakerdeck.com

に「独自に実装するのもおすすめ」とあったので、既存のエラーパッケージを参考に独自に実装してみました。

github.com/k1LoW/errors

github.com

特徴としては次のとおりです。

結局、標準パッケージである errors パッケージに追加した機能は

errors.WithStack(err error)スタックトレースを保持して、errors.StackTraces(err error)スタックトレースを取得する

だけです。

それだけのパッケージです。現時点では %+v%#v といったverbにも対応していません*2

シンプルな分、「 github.com/k1LoW/errors の導入」も「 github.com/k1LoW/errors からの移行」も楽だと思っています。

*1:少なくとも現在関わっているプロジェクトにおいては

*2:対応しようとしてみたが errors.Join 対応と併用が難しかったのでやめた

パッケージのgo.modのgoディレクティブのバージョンを最新の1つ前のマイナーバージョン(oldstable)に強制するoldstableを作った

確かに、あまり気にせずgovluncheckに引っ掛かったら即、最新バージョンに上げていたので良くないなあと反省。

というわけで、再発防止ということで作りました*1

github.com

これは何?

指定したgo.modのgoディレクティブのバージョンが最新の1つ前のマイナーバージョンかどうかを確認するツール、もしくはGitHub Actionです。

例えば最新のGoのバージョンが1.22.4だった場合、普通に使用するとgoディレクティブがマイナーバージョン1.21の最新のパッチバージョン(2024年7月1日時点では1.21.11)でないとエラーになります。

$ oldstable
Error: version of go directive in go.mod is not latest oldstable (oldstable: 1.21.11, current: 1.22.4)

1.22.4という最新バージョンでもダメで、1.21.10という最新ではないパッチバージョンでもダメです。govulncheckとの併用を想定しているので、厳しめなルールです*2

ちなみにGitHub Actionとしても提供されていて、次のように書くことでチェックできます。

# .github/workflows/oldstable.yml
[...]
    steps:
      -
        name: Check if version of go directive in go.mod is oldstable
        uses: k1LoW/oldstable@v1
        # with:
        #   go-mod-file: go.mod
        #   lax: false

laxモード

「最新バージョンの1つ前のマイナーバージョンの最新のパッチバージョン」が厳しいという人のためにlaxモードも提供しています。

これは、マイナーバージョンだけチェックする形になっています。

チェックルールとしては次のような感じです。

oldstable go directive lax mode (--lax) check
1.21.11 1.21.11 false ok
1.21.11 1.21.6 false ng
1.21.11 1.22.4 false ng
1.21.11 1.21 false ng
1.21.11 1.21.6 true ok
1.21.11 1.22.4 true ng
1.21.11 1.21 true ok

oldstable って?

actions/setup-go で使われていたaliasです。stable が最新バージョンで、oldstable がその前のマイナーバージョンの最新です。良さげな命名だったので使わせてもらっています。

というわけで

私が管理していて「主にツールではなくライブラリとして使うことを想定しているパッケージ」には随時入れていこうと思います。

こんな感じですね。

github.com

すでにgoディレクティブを1.22に上げているものが多いので、1.23のリリースまでお待ちくださいませ。

*1:というのも気をつけないといけないパッケージが多すぎるので...

*2:どうせgovulncheckでパッチバージョンを上げることを求められる

PHPカンファレンス福岡2024に参加した #phpconfuk

参加してきました。

phpcon.fukuoka.jp

今回は雨ということもあり、ちょっと子供のイベントごとのお手伝いをして、午後から参加しました(晴れだったら朝からベローチェの予定だった)。

とはいえ、いつものFFB。入った途端に一瞬でPHPカンファレンス福岡でした。いやあ最初から参加したかった!

参加者としてのカンファレンス参加

いろいろなプライベートな理由がありカンファレンス参加は、遠方だと難しい時期なのですが、そこは地元福岡、カジュアルに参加できて最高です。

しかも、全国からお話ししたい人、質問したい人が来てくれるわけで、「東京、いつもズルい!」と思うと共に「PHPカンファレンス福岡ありがとう!!!!!!!!!!!」となりました。PHPカンファレンス福岡ありがとう!

見かけた知っている人知っている人に(意を決して)話しかけて、近況を聞いたり昔話をしたり最近の技術トピックについて教えてもらったり、なんというか「カンファレンスの廊下」をいつも以上に意識して楽しみました。

とはいえずっと「廊下」ができるほどコミュニケーション能力があるわけではないので、元から聞きたいと思っていた発表を聞かせてもらったり、Ask the Speaker なら話しかけていいだろうと話しかけたり、していました。

「スポンサーブースも回るぞ!」と思っていたのですが結局「コミュニケーション能力の難」を発揮して回れたのは一部でした。いつかちゃんと全部回るという実績を解除したいし、ツアーに参加すればよかった。。。。

🍺

懇親会も2次会もひたすらに話していました。たまたまテーブルとご一緒させていただいた方と面白い話をしたり、話しかけたいと思っていた方( #EM問題集 な方や、 ななうぇぶ な方とか。ファンなんですよね。 )に、(多少失礼もあったかと思いつつ)聞きたいことを聞いたり質問したり、また、たまたま席が一緒だった方といろいろ話したり。

しっかり酔っ払いました。

これ↓もあまり覚えていなんですよね。。。

PHPerKaigiでも同じ体験をしたのですが、「k1LoWさんから聴くP山さんの凄さ・強さ」的なのが2次会で発生し、本当に本当に身が引き締まる思い。 がんばろうがんばろう。

daisuki.nichiyoubi.land

まあ、ここら辺はシラフでも話せる気がするので、大丈夫な範囲で話している気がする。たぶん。GMOペパボに来たらもれなく体感できます。皆さん是非!

2次会を企画運営してくださった cakephper さんこと市川さんいつもありがとうございます!!!!!!!

runn開発者会議

katzchum さんとも本当にわずかな時間でしたが時間をいただいてrunn開発者会議できました。

私が持っていった直近の課題はこれで、ポロポロと相談したら、シュッといい示唆をもらいました。最高!!

他にもいろいろタフな課題が残っていますが、ぼちぼち解決していきます。

それはそれとしてPHPカンファレンス福岡2024内でrunn developers conference 2024があったらしく、さらにキーノートスピーチがあったらしい...

https://k2tzumi.github.io/runn-developers-conference-fukuoka-2024-keynote-speech/1

まとめ

今回もPHPカンファレンス福岡でした。大変によかったです。最初から参加したかったけど、それはそれ、しょうがない。

ずーっと楽しかったです。

ありがとうございました!!

Go Conference 2024に参加した #gocon

Go Conference 2024に参加/登壇してきました。

gocon.jp

前日のUnofficial Pre Partyから参加させていただき、2日間Goの話題に浸かってしました。

moneyforward.connpass.com

2日に渡って理解した range over func

いろいろ楽しかったのですが、印象に残っていることを1つだけを書いておこうかと思います。

Unofficial Pre Party で MakKi さんがrange over funcのエラー処理という具体的なお話をしてもらい、感想戦で大盛り上がりしたあと、Go Conference 2024のトップバッターで tenntenn さんがさらに解像度を上げて紹介してもらえたのことで「range over func完全に理解した」になることができました。

speakerdeck.com

docs.google.com

iterパッケージに並んでいる関数を見て「全くわからん」となっていたところも説明してもらって「!!!!」となりました。

良い体験でした。

MakKi さんといえば、お昼をご一緒させてもらったのですがその時に紹介してもらったゲームサーバのアーキテクチャの話が最高に面白かったです。 依存の逆転というか、発想の転換というか、「すげえ!」と感心しきりでした。

github.com

発表と発音

そして、私も「Cleanup handling in Go」というタイトルで発表をしました。

speakerdeck.com

が、

セッション内でdonegroupというパッケージを紹介したのですが、ずっと発音を間違って「dˈɔːn grúːp」と読んでいたという...

正しくは「dʌn gruːp」です!!!!!!

正しくは「dʌn gruːp」です!!!!!!

正しくは「dʌn gruːp」です!!!!!!

この後、懇親会で散々いじられるわけですが、私はずっと頭を抱えていました...完全にやらかした...

というわけで

最後のアレは置いておいて、他にも多くのイベントが発生して、最高に楽しい2日間でした。 会場で初めて会えた方だったり、いつもカンファレンスで会える方だったり、残念ながらタイミングが会わずに会えなかった(お話できなかった)方だったり、本当に一期一会なカンファレンスにりました。

スタッフの皆さん、Goコミュニティのみなさんありがとうございました!

最近、参照したり共有したりすることが多いURL

soudai.hatenablog.com

gihyo.jp

scrapbox.io

scrapbox.io

よく参照するならまとめておけばいいじゃない。

ラインナップからフェーズ的なものについては察してください。

ガッチガチに「こうあるべき」というような話というよりも、モデリングやテーブル設計においての固定観念*1を崩すためのキッカケに使っていることが多い気がします。

ちなみに私は、十数年、散々失敗した挙句にこのような有益なURLを有益だと理解して参照したりしているわけで、最初から(もしくは短期間に)ここに行き着ける人ってすごいなと尊敬しています。

追記

そーだいさんから有益情報を教えてもらったので並べておきます。

*1:「(Webフレームワークの)モデルとデータベーステーブルを1:1にして終わり」とか

APIテスティングツールに必要なのはテストケースごとのIDなのではないか #cicd_test_night

先日、機会をいただきましてCI/CD Test Night #7でAPIテスティングツール*1を開発している中で考えていることをお話しさせていただきました。

testnight.connpass.com

詳細はスライドをご覧ください。

speakerdeck.com

APIテストはスモールテストと比べて効果は大きいがコストも大きい

私はAPIテストの一番の課題はこれだと思っています。

APIテストは、「APIに対してリクエストを投げてそのままテストする」というわかりやすさがあります。

また、テストあたりにおけるカバレッジの広さというメリットもあります。

なので、できればデメリットであるコストの大きさをなんとかしたいわけです。

この課題の解決には2つの方法があります。

  1. コストを小さくする
  2. 範囲を広げて効果を大きくする

先に、範囲を広げる方法について考えると

  • 作ったテストをそのまま負荷テストに使えるように
  • 作ったテストをそのまま本番環境のリグレッションテストにも使えるように
  • APIテストしていたらAPIスキーマとの合致も検証するように

というような方向性、言ってみれば一石N鳥にすることによって効果を大きくするというものになります。効果が大きければコストが気にならなくなるだろうという寸法です。

では、コストを小さくするにはどのようにするかというと「テスト自体を書きやすくする(実装コストを小さくする)」などがあるのですが、結局は実行コストの大きさ、特に実行時間の長さが際立ってきてこれを解決しないといけなくなります。

まず最初に解決する手段として挙げられるのは「テストの並列実行」だと思います。

しかし、並列実行にも限界があります。

その次に取れる手段は何でしょう?

インテリジェントにテストを実行する

もう全てを愚直に実行するのはあきらめて、効率よく実行していく必要がありそうです。

「効率よく」というのは、例えば、

  • 失敗しやすいテストから先に実行する
  • 失敗しないテストは通常は実行しない
  • 実行時間が平均的になるように分けて並列実行する

などです。

ちなみに、このようなアプローチは私が考えたわけではありません。

私は、Launchable社の方たちの発表やエントリを見て知りました。

そして、APIテストという実行時間の長いテストに向き合うようになって、このアプローチの効果に期待しはじめるようになりました。

APIテスティングツールに必要なのはテストケースごとのIDなのではないか

そして、タイトル回収です。

先述した「インテリジェントなテスト実行」を実現するためにはまず何が必要か?というと、「テストケースの一意な識別」です。

それぞれのテストケースを一意に識別できなければ「どのテストが失敗したのか」がわかりません。 識別できなければ特定のテストだけを実行することもできないし、指定の順番に実行するということもできません。

テストケースに識別子=IDがあれば、特定テストの実行や、任意の順番でのテスト実行も可能になります。

インテリジェントなテストを実現するための第一歩が「ID」だと考えます。

APIテスティングツールに必要なのは(まず)テストケースごとのIDなのではないでしょうか*2

IDによる識別や、IDを使った柔軟なテストの実行ができることで、コストを抑えたインテリジェントなテスト実行への道が開けます。

ちなみに、runnにおけるIDの利用については次のエントリをご覧ください。

zenn.dev

この考えに至るまでの話

実は、きっかけはこのXのポストです。

ここからテスト実行時間について意識して考えるようになり、今に至っています。

今回の発表やこのエントリは @zoncoen さんのポストへの私なりの回答となります。

考えるきっかけをくれた @zoncoen さん、発表する機会をくれた @ponkio_o さんありがとうございました。

*1:ここでいう「API」とはWeb APIやgRPCを指します

*2:多くのテスティングツールにおいては備えているものでもあります。ただ、私が開発しているrunnには最初なかったのです。ちなみに提案してくださったのは同じrunn開発者の @katzchum さんです