mackerel-agent configtest によるチェック強化の実装について
Mackerel Advent Calendar 2022 15日目です。
mackerel-agent configtest
によるチェック機能を強化したので実装を軽く紹介します。
github.com
mackerel.io
何が変わったのか
次のようなmackerel-agent-sample.conf
を作成し、mackerel-agent configtest
を実行する。
apikey = "abcdefg" podfile = "/path/to/pidfile" [foo] command = "bar" [plugin.checks.incorrect1] command = "test command" action = { command = "test command", user = "test user", en = { TEST_KEY = "VALUE_1" } } [plugins.checks.incorrect2] command = "test command" [plugin.check.incorrect3] command = "test command"
強化前は、Syntax OKとなる。
$ mackerel-agent configtest -conf /usr/local/etc/mackerel-agent-sample.conf /usr/local/etc/mackerel-agent-sample.conf Syntax OK
強化後は、各設定項目がtypoで無効になっていたら警告を出してくれる。
$ mackerel-agent configtest -conf /usr/local/etc/mackerel-agent-sample.conf
[WARNING] foo is unexpected key. Did you mean root ?
[WARNING] plugin.check.incorrect3 is unexpected key. Did you mean plugin.checks.incorrect3 ?
[WARNING] plugin.checks.incorrect1.action.en is unexpected key. Did you mean plugin.checks.incorrect1.action.env ?
[WARNING] plugins is unexpected key. Did you mean plugin ?
[WARNING] podfile is unexpected key. Did you mean pidfile ?
実装
候補の列挙
候補とは、Did you mean ***
の***
のことで、各設定項目に該当する。
コードではconfig/config.go
のConfig構造体に列挙されている。
type Config struct { Apibase string ... DisplayName string `toml:"display_name"` ... HostStatus HostStatus `toml:"host_status" conf:"parent"` ... HostIDStorage HostIDStorage `conf:"ignore"` ... }
Apibase
のように、タグがついていない場合、フィールド名の先頭を小文字にして追加する。DisplayName
のように、toml
タグがついている場合、その値を追加する。HostStatus
のように、フィールドの型が構造体の場合、parent
タグをつけて候補を再帰的に取得する(ここだとon_start
,on_stop
)。HostIDStorage
のように、設定ファイルに書き込まれることがない場合、ignore
タグをつけてスキップする。
typoキーの検出
typoキーは、BurntSushi/toml#MetaData.Undecodedを用いて検出できる。
上記の設定ファイルの例では、次のように検出できている。ソートしているのは後の処理のため。
func ValidateConfigFile(file string) ([]UnexpectedKey, error) { config := &Config{} md, err := toml.DecodeFile(file, config) ... undecodedKeys := md.Undecoded() sort.Slice(undecodedKeys, func(i, j int) bool { return undecodedKeys[i].String() < undecodedKeys[j].String() }) // [foo foo.command // plugin.checks.incorrect1.action.en plugin.checks.incorrect1.action.en.TEST_KEY // plugins.checks.incorrect2 plugins.checks.incorrect2.command // podfile] fmt.Printf("%+v", undecodedKeys)
typoキーの判定
foo
の場合、foo
は候補として列挙されていないので、typoキーはfoo
。plugin.checks.incorrect1.action.en
の場合、plugin
が候補として列挙されているので、typoキーはen
。plugins.checks.incorrect2
の場合、plugins
は候補として列挙されていないので、typoキーはplugins
。podfile
の場合、podfile
は候補として列挙されていないので、typoキーはpodfile
。
foo.command
は、foo
が既に判定されているのでスキップ。plugin.checks.incorrect1.action.en.TEST_KEY
は、plugin.checks.incorrect1.action.en
が既に判定されているのでスキップ。plugins.checks.incorrect2.command
は、plugins.checks.incorrect2
が既に判定されているのでスキップ。
スキップしないとContents of suggestion when executing configtest may not be correct · Issue #829 · mackerelio/mackerel-agent · GitHubのような不具合が生じてしまう。
実装: mackerel-agent/validate.go at 5a2668daac02a5e2a406d22ddd23af6cca32e7be · mackerelio/mackerel-agent · GitHub
Config構造体にparent
タグがあるかで判定するより、候補として列挙されているかどうかで判定する方が良かったというPR:
refactor(config/validate): use `candidates` instead of `parentConfKeys` by wafuwafu13 · Pull Request #839 · mackerelio/mackerel-agent · GitHub
plugin.typo.fooの検出
プラグインの設定項目は、checks
, metrics
, metadata
でないといけないが、Config構造体ではstring
であれば良いので、MetaData.Undecoded
では検出できない。
上記の設定ファイルの例だと、plugin.check.incorrect3
が検出されていない。
type Config struct { ... Plugin map[string]map[string]*PluginConfig `conf:"parent"` .... }
そのため、config.Plugin
を別途調べる必要がある。
候補の判定
agext/levenshteinで、レーベンシュタイン距離を用いて判定する。
Terraformでも使われている。
結果の表示
TOMLの形式がおかしいなど致命的なエラーは赤、typoの警告は黄で表示する。
\x1b[31m *** \x1b[0m
のように文字列で指定するとWindowsでうまく表示できないのでfatih/colorを使用した。
Rust学習進捗2022
フロントエンド界隈のOSSで目立ってきたり、Linuxに取り込まれたり、放ってはおけない言語、それがRust。
1年前くらいに入門したこの本に再び目を通した。
gihyo.jp
実践で書いてみたくなったから awesome-alternatives-in-rust に目を通してテストが足りてなかったjqlにコミット。
github.com
github.com
Denoにもちょこっとコミット。
github.com
wafuwafu13.hatenadiary.com
自分でも何か作りたくなったから mkr の Rust実装 mkrust を作成。基本的なことを一通り終えたから開発が止まっている。
github.com
github.com
Cと比較されることが多いのでC版の mkrc を作成。
github.com
メモリ管理の良さを実感できなかったのでガベージコレクションのアルゴリズムと実装を読んだ。
tatsu-zine.com
GCが搭載されてないといかに大変かがわかった本。
book.mynavi.jp
Do'er Advent Calendar 2022 の1日目でした。
deno/std/nodeのfs.DirentでisBlockDeviceの判定ができないのはRustに実装がされていないから...ではない
追記: Rustにis_block_device
があって、Denoにはなかった
github.com
fs.readdir(path, options, callback)のoptions
にwithFileTypes: true
を指定すると、fs.Direntが返ってくる。
fs.Dirent
のdirent.isBlockDevice()
を使った例が以下。
$ node Welcome to Node.js v16.13.1. Type ".help" for more information. > const files = fs.readdirSync(".", {withFileTypes: true}) undefined > files[0].isFile() true > files[0].isBlockDevice() false
しかし、Deno(std/node)では使えない。
$ deno Deno 1.25.4 exit using ctrl+d or close() > import {readdirSync} from "https://deno.land/std@0.157.0/node/fs.ts" undefined > const files = readdirSync(".", {withFileTypes: true}) undefined > files[0].isFile() false > files[0].isBlockDevice() Uncaught Error: Not implemented: Deno does not yet support identification of block devices at notImplemented (https://deno.land/std@0.157.0/node/_utils.ts:23:9) at Dirent.isBlockDevice (https://deno.land/std@0.157.0/node/_fs/_fs_dirent.ts:8:5) at <anonymous>:2:10
deno_std/node/_fs/_fs_dirent.tsを見るとnotImplemented
とされている。
export default class Dirent { constructor(private entry: Deno.DirEntry) {} isBlockDevice(): boolean { notImplemented("Deno does not yet support identification of block devices"); return false; } ...
std/nodeのreaddirSyncは内部的にはDeno.readDirSyncが使われているのでDenoのコードを見にいくとそれっぽい箇所があった。
is_file
があるから上記の例でfiles[0].isFile()
が使えている。
FsStat { is_file: metadata.is_file(), is_directory: metadata.is_dir(), is_symlink: metadata.file_type().is_symlink(), ...
metadata
の参照先はRustなのでRustのコードを見にいくとそれっぽい箇所があった。
impl Metadata { #[must_use] #[stable(feature = "file_type", since = "1.1.0")] pub fn file_type(&self) -> FileType { FileType(self.0.file_type()) } #[must_use] #[stable(feature = "rust1", since = "1.0.0")] pub fn is_dir(&self) -> bool { self.file_type().is_dir() } #[must_use] #[stable(feature = "rust1", since = "1.0.0")] pub fn is_file(&self) -> bool { self.file_type().is_file() } #[must_use] #[stable(feature = "is_symlink", since = "1.58.0")] pub fn is_symlink(&self) -> bool { self.file_type().is_symlink() } ...
ここにis_block_device
を生やせば解決するはず...だけどメタ的な意味でRustわからん
JSのオブジェクトの分割代入で既定値が割り当てられるのはundefinedの場合のみ
「JSのオブジェクトの分割代入で既定値が割り当てられるのはundefinedの場合のみ」ということは、MDNのAssigning to new variable names and providing default valuesにAssigned a default value in case the unpacked value is undefined.
と書いてあることや、13 ECMAScript Language: Expressionsの13.15.5.3 Runtime Semantics: PropertyDestructuringAssignmentEvaluationに4. If Initializeropt is present and v is undefined, then
とあることから分かる。
ここからNode.jsのfs.readを使ってみていく。
node/lib/fs.js#L605~ に以下のコードがある。
function read(fd, buffer, offsetOrOptions, length, position, callback) { ... } else if (arguments.length === 3) { // This is fs.read(fd, bufferOrParams, callback) if (!isArrayBufferView(buffer)) { // This is fs.read(fd, params, callback) params = buffer; ({ buffer = Buffer.alloc(16384) } = params ?? kEmptyObject); } ... ... ({ offset = 0, length = buffer.byteLength - offset, position = null, } = params ?? kEmptyObject); ... validateBuffer(buffer); ...
例えばfs.read(fd, {offset: 1}, callback)
というように呼び出すと、
params = {offset: 1}
({ buffer = Buffer.alloc(16384) } = {offset: 1}
buffer = Buffer.alloc(16384)
というように処理される。
もしfs.read(fd, {buffer: null}, callback)
というように呼び出すと、
params = {buffer: null}
({ buffer = Buffer.alloc(16384) } = {buffer: null}
buffer = null
というように処理される。
この場合、validateBuffer
が呼びされる前にlength = buffer.byteLength - offset
でbuffer = null
が参照されてしまう。
これを解決したのがこのPR。
github.com
ちなみにdeno_stdは現時点でNodeのv18.8.0との互換を保っているため、以下のように意図的にnullを参照するようにしている。
github.com
// @ts-ignore: Intentionally create TypeError for passing test-fs-read.js#L87 length = opt.buffer.byteLength;
RFC8259 の「Implementations MUST NOT add a byte order mark to the beginning of a networked-transmitted JSON text」について
『プログラマのための文字コード技術入門』の p.216 にある、
「JSONでは、データ先頭にBOMをつけないことが求められています(RFC8259)」
についてちょっとだけ詳しく調べる。
RFC8259の該当箇所は8.1. Character Encodingで、
「Implementations MUST NOT add a byte order mark (U+FEFF) to the beginning of a networked-transmitted JSON text」
とある。
続いて
「In the interests of interoperability, implementations that parse JSON texts MAY ignore the presence of a byte order mark rather than treating it as an error」
とあるように、相互運用性の観点から、BOMがあればエラーとして扱うのではなく、無視してもいいらしい。
BOM(Byte Order Mark)とは、ビッグエンディアンかリトルエンディアンのどちらのバイト順を採用しているかを示すために、データの先頭に付ける印のこと。
U+FEFF
の符号位置を用い、ビッグエンディアンではFE FF
、リトルエンディアンではFF FE
という2バイトの列になる。
符号位置は、文字コード表の中の位置のこと。
ビッグエンディアンは、上位8ビットが先頭にくるバイト順、リトルエンディアンは、下位8ビットが先頭にくるバイト順のこと。
U+FEFF
のU+
は、Unicodeの符号位置を表すのに付ける接頭辞。
16ビットのデータを8ビット単位のバイト列にするときには、ビッグエンディアンかリトルエンディアンのどちらを採用するかの問題がある。
Unicodeの符号化方式の中の1つであるUTF-16は、16ビット単位であるためこの問題が発生し、解消するためにBOMを付けることがある。
JSONでBOMを付けないとなると、バイト順の区別はどうするのだろうと思ったところ、同じ RFC8259 8.1. Character Encoding にこう書いてあった。
「JSON text exchanged between systems that are not part of a closed ecosystem MUST be encoded using UTF-8」
UTF-16を扱わないのにRFCなぜBOMに言及しているのだろうと思ったところ、『プログラマのための文字コード技術入門』の p.154 にこう書いてあった。
「バイト順が問題になることのないUTF-8にはBOMは本来関係がありません。ところが、BOMをUTF-8で表現した3バイトの値(EF BB BF)が、UTF-8のデータ列の先頭についていることがあります。バイト順の印という元々の意味を離れて、UTF-8のデータであることの印として利用価値があるという考えもあります。」
ネットワーク転送されるJSONにはその利用価値は必要なく、相互運用性に支障をきたすから、「Implementations MUST NOT add a byte order mark to the beginning of a networked-transmitted JSON text」とされている。
BOMに関する実際の問題の例として、node-fetchのHandling a BOM with .json() #541というissueにある、レスポンスをJSONとして取得する際にエラーが起こるという問題がある。
node-fetchの`.json()`には内部的に`JSON.parse`が用いられており、引数のtextにBOMが含まれていれば、そこでエラーが生じてしまう。JSONの構文はRFC8259に準拠しているからだ。
ユーザー側の解決例としてはここにあるように、textとして取得してから、BOMに当たる0xFEFF
を除去し、JSONにする例がある。
node-fetch側のリリースされている解決法は、fix: handle bom in text and json #1482にあるように、内部でbuffer.toString()
ではなくTextDecoder().decode()
を使うようにしている。
buffer.toString()
では、BOMに当たる0xFEFF
があったとしてもそのまま文字列化してしまい、それをJSON.parse
するためエラーが生じる。
TextDecoder()のパラメータであるutfLabel
のデフォルトは"utf-8"
。
そして重要なのがデフォルトがfalse
のTextDecoder.ignoreBOM。
命名がややこしいが、TextDecoder.prototype.ignoreBOM not working as expectedの回答に
「If on the other hand you want it to be removed from the output, then leave it as false, the parser will treat it specially, and remove it from the output」
とあるように、TextDecoder().decode(buffer)
の結果にはBOMが含まれなくなり、問題なくJSON.parse(text)
ができる。
このように目に見えないBOMと戦ってJSONが生成される例がある。
野良コミッターがOSSの仕様を決める
OSS活動を始めて今まで157PRがCloseされた。翻訳やtypoの修正やテスト、ドキュメントの整備も含まれるけど、野良コミッターでも仕様を決める、もしくはそれに近いことができたのでいくつかまとめておく。
仕様の変更の後継をする
ESLintで最新のecmaVersionを"latest"で指定できるようになったことを受け、TypeScript ESLintでも"latest"指定できるようにするというfiskerさんのPRが詰まっていたのを発見し、実装を進めた
開発の方針を決める
denoland/deno_std/node/_tools/test
に生成されるNodeのバージョンを上げた
github.com
denoland/deno_std/path
とdenoland/deno_std/node/path
の関係を整理して進めていった
github.com
こんな感じでorgに所属してない、コミッター認定されてない野良コミッターでも頑張れば割と大きい部分の仕様に関わることができる。
GitHubでの活動はGitHubの登場はコミッタを特権階級から追い落とす革命だった - Hello, world! - s21gのタイトル通り、何の資格も持っていない文系大学生にとっては飛び道具だった。
日本の方のSocket migration for SO_REUSEPORTという発表を知って、こういうことができるようになりたいと思った。
こういうこととはどういうことかを考えると、世界の仕様を決めるということで、今まで自分が何をできてきたのかをまとめたくなって書いた。
逆に何が足りないかを再認識できてきたので、それを踏まえて活動を進めていく。
機械学習の知識・2022春
- 数IA,IIBはセンター試験8~9割とってた記憶
- 数IIIは以下のように独学した
- 以下の本は何周もして理解を進めてきた
- ディープラーニングの理論と実装の理解を進めてきた
- OSS活動の指標の一つとしてInterested in AI Frameworksを掲げ、まずはGiNZAにコミットした
github.com
wafuwafu13.hatenadiary.com
- 統計検定1級に落ちたことがある
- Kaggleはタイタニックだけしたことがある
- コンペに参加する予定
- 英語: 論文読んだりはまだできていない スピーキングはDMM英会話で修行中
- 以下の本は何度か理解しようと試みてきたが進捗はイマイチ
- その他読んで面白かった本