mackerel-plugin-accesslogを中心にMackerelのコードを読んだ
グラフのラベル名をいい感じにする簡単なPRを投げた時のメモ。
公式ブログを読めばなんとなく機能的な部分はわかった。
mackerel.io
mackerel.io
mackerel-plugin-accesslog/main.go
のmpaccesslog.Do()
呼び出されているmp.NewMackerelPlugin(...).Run()
は、go-mackerel-plugin/mackerel-plugin.go
に定義されている。
mp.NewMackerelPlugin(&AccesslogPlugin{
prefix: *optPrefix,
file: flag.Args()[0],
posFile: *optPosFile,
noPosFile: *optNoPosFile,
parser: parser,
}).Run()
// Run the plugin func (mp *MackerelPlugin) Run() { if os.Getenv("MACKEREL_AGENT_PLUGIN_META") != "" { mp.OutputDefinitions() } else { mp.OutputValues() } }
このgo-mackerel-plugin
のmp.OutputValues()
でmackerel-plugin-accesslog
のFetchMetrics
が呼び出されている。
// OutputValues output the metrics func (mp *MackerelPlugin) OutputValues() { now := time.Now() stat, err := mp.FetchMetrics()
mackerel-plugin-accesslog
のFetchMetrics
では、まずgetReadCloser()
でp.file
つまり flag.Args()[0]
で指定されたアクセスログのファイルが開かれる。
// FetchMetrics interface for mackerelplugin func (p *AccesslogPlugin) FetchMetrics() (map[string]float64, error) { rc, takeMetrics, err := p.getReadCloser()
func (p *AccesslogPlugin) getReadCloser() (io.ReadCloser, bool, error) { if p.noPosFile { rc, err := os.Open(p.file) return rc, true, err }
それをbufio.NewReader, Reader.Readlineで読み込んでいくという流れ。
r := bufio.NewReader(rc) ... buf, isPrefix, err := r.ReadLine() ...
ちなみにアクセスログのパーサーはoptFormat = flag.String("format", "", "Access Log format ('ltsv' or 'apache')")
でltsv
かapache
が指定されてパースされている。
switch *optFormat { case "": parser = nil // guess format by log (default) case "ltsv": parser = parsers.LTSV case "apache": parser = parsers.Apache
if p.parser == nil { p.parser, l, err = parsers.GuessParser(line) } else { l, err = p.parser.Parse(line) }
パーサーの実態はGitHub - Songmu/axslogparser: Fairly accurate access Log Parserで、ltsv
もapache
も指定されていなかった場合は推測してくれるらしい。
あとはret := make(map[string]float64)
のマップに結果を集計していってそれを返しておしまい。
ret[string(fmt.Sprintf("%d", l.Status)[0])+"xx_count"]++ ret["total_count"]++ ... ret[v+"_percentage"] = ret[v+"_count"] * 100 / ret["total_count"]
はじめてOSSに新機能追加した
owという、Lots of built-in validations
なOSSに、BigIntのバリデーションを追加した。
owに関しては2月頃に3回、型整備のPRを出してマージされていたので、コードには少し馴染みがあった。
https://wafuwafu13.hateblo.jp/#sindresorhusow
OSSの型整備より一歩踏み込んだことがしたいと思い、issueを消化することを目標に最近やっていた。
実際、null
, undefined
に起因するバグがあったのでそれを消化できた。
そして今回、BigInt
のサポートをするという、約3年前のissueがあったので拾ってやってみた。
Support BigInt validator · Issue #54 · sindresorhus/ow · GitHub
owの構造と、何をどう追加したのかを雑にメモしておく。
まず、README.md
にも載っている以下のコードでArgumentError
がどのようにして起こるのかをみていく。
import ow from 'ow'; const unicorn = input => { ow(input, ow.string.minLength(5)); // … }; unicorn('yo'); //=> ArgumentError: Expected string `input` to have a minimum length of `5`, got `yo`
source/predicate/string.ts
に、ow.string
に関するコードが定義されており、minLength
の定義は以下のようになっている。
ow/string.ts at 49841a260cd431985b91d319b091dd59096775a3 · sindresorhus/ow · GitHub
minLength(length: number): this { return this.addValidator({ message: (value, label) => `Expected ${label} to have a minimum length of \`${length}\`, got \`${value}\``, validator: value => value.length >= length, negatedMessage: (value, label) => `Expected ${label} to have a maximum length of \`${length - 1}\`, got \`${value}\`` }); }
このvalue => value.length >= length
のバリデーターは、source/predicates/predicate.ts
で発火される。
message
の発火も同じ部分である。
ow/predicate.ts at 49841a260cd431985b91d319b091dd59096775a3 · sindresorhus/ow · GitHub
ow/predicate.ts at 49841a260cd431985b91d319b091dd59096775a3 · sindresorhus/ow · GitHub
...
result = validator(value);
...
const errorMessage = message(value, label_, result);
このvalidator
の実行結果がtrue
でなければ、最終的に独自定義のArgumentError
が投げられる。
ow/predicate.ts at 49841a260cd431985b91d319b091dd59096775a3 · sindresorhus/ow · GitHub
throw new ArgumentError(message, main, errors);
ArgumentError
が投げられるのはこれ以外にも、ow(input, ow.type.foo)
においてinput
とtype
の型が合っていない場合がある。
例えばtest/string.ts
でテストされているような場合がある。
ow/string.ts at 49841a260cd431985b91d319b091dd59096775a3 · sindresorhus/ow · GitHub
t.throws(() => { ow(12 as any, ow.string); }, 'Expected argument to be of type `string` but received type `number`');
このバリデーションは、Predicate
クラスのコンストラクタで定義されている。
ow/predicate.ts at 49841a260cd431985b91d319b091dd59096775a3 · sindresorhus/ow · GitHub
this.addValidator({ message: (value, label) => { // We do not include type in this label as we do for other messages, because it would be redundant. const label_ = label?.slice(this.type.length + 1); // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing return `Expected ${label_ || 'argument'} to be of type \`${this.type}\` but received type \`${is(value)}\``; }, validator: value => (is as any)[typeString](value) });
ここのバリデーションで使われているis
は、owと同じ作者が作ったライブラリである。
github.com
今回やったissueに立ち返ってみると、is
でBigInt detection
がサポートされたので、それをow
にも取り込もう、というものだった。
Support BigInt detection · Issue #39 · sindresorhus/is · GitHub
最初は、source/predicates/number.ts
にbigint
メソッドを足して、ow(BigInt(9007199254740991), ow.number.bigint)
で判定させようと思ったが、number
とbigint
で型が違うため、コンストラクタのバリデーションで意図せず弾かれてしまった。
新たにBigIntPredicate
クラスを定義し、意図してコンストラクタのバリデーションで弾かれる方針で実装を進めた。
NodeのBigInt
のサポートが10.4.0からなのでテストが思うように動かなかったりいろいろしたが、無事マージされた。
このOSSの作者はFull-Time Open-Sourcererでレスポンスもレビューも手早くて時差があるものの開発体験がとても良かった。
ソースコードの核となる部分の7割くらいを理解できれば、issueのバグを潰したり新機能開発ができるという肌感がある。
技術書積読消化購読メモ
4月5月
読んだ
- Docker Kubernetes実践コンテナ開発入門(Kubernetesから)
- Amazon Web Services基礎からのネットワーク&サーバー構築
- Kubernetes on AWS
- TCP/IPネットワーク入門
- Real World HTTP
- 入門監視
- Mackerelサーバ監視入門
- DNSがよくわかる教科書
- マイクロサービスアーキテクチャ
- オブジェクト指向でなぜ作るのか
- ドメイン駆動設計入門
- SREサイトリライアビリティエンジニアリング
- サイトリライアビリティワークブック
6月中
手動かす
読む
- Infrastructure as Code
- nginx実践入門
- 大規模サービス技術入門
- Go言語による並行処理
- コンピュータシステムの理論と実装
- 情報処理概論
- Linuxのしくみ
- SQLアンチパターン
- Elasticsearch実践ガイド
- Webフロントエンドハイパフォーマンスチューニング
- DMM.comを支えるデータ駆動戦略
- 関数型プログラミングの基礎 JavaScriptを使って学ぶ
7月以降
買って手動かす
GraphQLどれか- 実践 パケット解析 第3版
- 現代暗号への招待
- 言語論どれか
GoならわかるシステムプログラミングRust本どれか- ガベージコレクション 自動的メモリ管理を構成する理論と実装
- 関数プログラミング実践入門
- 型システム入門 プログラミング言語と型の理論
8月までマストでやらないといけないっぽい
理論系はインターンの波が去ったらでいいか
axiosの内部構造を理解してadapterの仕組みを解明する
axiosのadapterを使う機会があり、仕組みが気になったのでメモしておく。
github.com
adapter
で検索をかけると、lib/core/dispatchRequest.js
の51行目の記述が目についた。
axios/dispatchRequest.js at 7821ed20892f478ca6aea929559bd02ffcc8b063 · axios/axios · GitHub
var adapter = config.adapter || defaults.adapter; return adapter(config).then(function onAdapterResolution(response) { ...
dispatchRequest.js
は名前からも、lib/core/Axios.js
で使われていることからも、実際にリクエストを送る部分であろう。
まずはdefaults.adapter
がなんなのかを調べると、lib/defaults.js
に以下の記述があった。
axios/defaults.js at 7821ed20892f478ca6aea929559bd02ffcc8b063 · axios/axios · GitHub
axios/defaults.js at 7821ed20892f478ca6aea929559bd02ffcc8b063 · axios/axios · GitHub
function getDefaultAdapter() { var adapter; if (typeof XMLHttpRequest !== 'undefined') { // For browsers use XHR adapter adapter = require('./adapters/xhr'); } else if (typeof process !== 'undefined' && Object.prototype.toString.call(process) === '[object process]') { // For node use HTTP adapter adapter = require('./adapters/http'); } return adapter; } ... var defaults = { adapter: getDefaultAdapter(), ....
typeof XMLHttpRequest
をchromeのコンソールで調べると以下のようになったので、adapter = require('./adapters/xhr');
の元を見にいった。
typeof XMLHttpRequest "function"
lib/adapters/xhr.js
では、XMLHttpRequestを使ってリクエストが送られていた。
axios/xhr.js at 7821ed20892f478ca6aea929559bd02ffcc8b063 · axios/axios · GitHub
axios/xhr.js at 7821ed20892f478ca6aea929559bd02ffcc8b063 · axios/axios · GitHub
axios/xhr.js at 7821ed20892f478ca6aea929559bd02ffcc8b063 · axios/axios · GitHub
var request = new XMLHttpRequest(); ... request.open(config.method.toUpperCase(), buildURL(fullPath, config.params, config.paramsSerializer), true); ... // Send the request request.send(requestData);
つまり、axiosをadapter
オプションを指定せずに使うと、内部的にはXMLHttpRequest
を使った通信が行われ、adapter
オプションを指定すると、通信そのものをラップできることが分かる。
オプションの反映はlib/core/mergeConfig.js
を使って行われていることが分かったが、深追いはせずテストコードを眺めるだけに留めた。
axios/mergeConfig.spec.js at main · axios/axios · GitHub
実際にadapter
オプションを書いて使ってみた。
この場合リクエストは発生せず、実装したPromiseが返っている。
const axios = require("axios"); const sampleAdapter = () => { return new Promise((resolve, reject) => { resolve({ data: "sample data" }); }); }; axios({ url: "https://example.com/", adapter: sampleAdapter, }).then((res) => console.log(res.data));
$ node index.js sample data
リクエストを送る部分はlib/core
配下にあるものだと思っていたので、adapter
オプションを指定しない場合にlib/adapters
配下のコードが使われているのが意外だった。
追記:
axiosにPR投げてみたらlib/adapters
配下のコードが使われているという理解はあっていた。
github.com
AWA株式会社でのインターンでインフラに強くなりました
株式会社サイバーエージェントの全職種向け 実践就業型インターン【オンライン参加可】に3月2日~3月31日の1ヶ月間参加し、インフラに強くなりました(過去の自分比)。
参加するまで
サイバーエージェントは魅力的な会社ですし、人生で1回は行っておきたいという気持ちがまずありました。
また、大規模サービスに触れる機会はもともとあったのですが、インフラがほぼブラックボックスの状態で開発をしていたので、インフラに詳しくなりたいという目的がありました。
個人ではJS/TSを重点的にやることが多く、また、インフラの実務経験がほぼない状態でインフラを志望するのは勇気が要りましたが、春は挑戦の季節ということで応募してみました。
とは言っても、エントリー締め切りがインターンシップ開始月の3ヶ月前の月末だったので、11月あたりにエントリーした記憶があります。
1次面接(人事)でABEMAかAWAに行きたいと伝え、2次面接(エンジニア)でインフラをやりたいと伝えたところ、希望が通ってAWAでインフラをさせてもらうことになりました。
面接とはいっても雑談のような感じで、エントリーから参加するまでは長かったですが、合否の結果は早かったのでストレスなく暮らせました。
(EC2とECSという単語が出てくると少し混乱するというようなレベル感だったので、ゴリゴリの技術面接ではなくてよかったです。)
一応オフラインを希望したのですが、フルリモートでの勤務となりました。
出社できなかったのは残念ですが、メンターさんや人事の方とのコミュニケーションに苦労はしませんでした。
働いてみて、AWAは少数精鋭という印象を受けました。
僕が所属したサーバーチームは自分を含めて4名で、Androidをやりながらサーバーもやるといったようなマルチな方の集団といった感じでした。
やったこと
ここからの内容は以下のスライドにも載せてあります。
- Terrafromのバージョンアップ(v0.12 -> v0.13)
まずはレポジトリ全体の構成をしっかり把握してから始めました。
ただCIを回してバージョンをあげればいいというわけではなく、運用上、HCLの記述と実際の設定に差分が生じるので、それを解消する必要がありました。
- 本番LOUNGEサーバーのリソース増強(Terraform/Datadog)
ECSの設定をいじって、タスクが切り替わる様子やDatadogのグラフが遷移する様子をみました。
- LOUNGE検索の追加(MongoDB/Mongo Connector/Elasticsearch/Jenkins/Ansible)
登場してくるツールやレポジトリが多くなったので、自分がしたことがどこにどう反映されるのかを理解しながら進めることを意識しました。
初めての概念が多かったのでドキュメントをしっかり読みました。
github.com
- ドキュメントの作成(DocBase)
検索データの同期周りは、1年前のドキュメントしかなかったので、新たに作成しました。
ドキュメントとかテストとかしっかりしたいタイプなのでこういう仕事もよかったです。
- LOUNGEの検索処理
gRPCのproto定義から始めてGoを書いていきました。
クリーンアーキテクチャが採用されているレポジトリに項目を足す作業だったので、難しい部分はありましたが、見通しは立てやすかったです。
これでつまったりしていました。
github.com
感想
- 夕会すごい
毎日30分、金曜1時間の夕会に参加させていただきました。
エンジニアの方だけではなく、40名くらいの方々が集まり、念密に施策のすり合わせをしていらっしゃいました。
AWAは毎日聴いているものの、今回はインフラでの参加だったので、開発者としてサービスに対する理解が薄くなるのではないかという心配をしていましたが、異なる職種の方が何をしているのかまで知れました。
また、メンターさんにも毎日1on1で30分以上時間をいただき、質問の対応や進捗の管理をしていただきました。
- Go読みやすい
普段はGoでの開発はしていなかったので不安だったのですが、インターフェイスから辿っていったり、VSCodeの機能を使ったりして初見でも自力で読める部分は多かったです。
Goでの運用を知れたのもよかったです。
- インフラ周り楽しい
普段インフラはあまり触っておらず、何をしているか分からなくて怖い印象を持っていたのですが、1つの作業でアプリケーションを大きく動かすことができることに特に楽しみを感じました。
- 触るレポジトリ多かった
マイクロサービスで構成されているため、自分が関連するところだけでもTerraformのレポジトリを合わせると8つありました。
普段はいわゆるモノレポしか基本触らないので、最初は頭が混乱しましたが、慣れるとやりやすさを感じました。
- サービスの歴史の理解が重要
どのサービスにも少なからずクセのようなものがあり、なんでこのようなコードになっているのか、どうしてこのツールを用いているのか、に対する理解が働く上で重要だと感じました。
- 意外と自走できる部分はあった
Goもインフラもほぼ未経験の状態で飛び込んだので、1ヶ月で自分ができることはかなり少ないと思っていましたが、ひとつひとつキャッチアップしていくことでなんとか1エンジニアとして働くことができました。
gomock
を使ったりしながらテストまで一貫して書けたり、コード書いてる途中で自分が生んだN+1問題に気づけて修正できたりしたのもよかったです。
どこでもインフラのタスクに手をあげていくぞというやっていきや、独学でももっとインフラの知識を深めていきたいという感情が芽生えたのも大きな成功体験でした。
AWAはこれからも使い続けるサービスですし、今回得た経験もこれから活き続けるので、とても濃い1ヶ月でした。
課題としては、iOS/Android の知識がもっとあれば、得るものもより多かったと思います。
どんな質問にも答えてくださったメンターさん、進捗や目標を管理してくださった人事の方、インターンという場に関わる方々に感謝です、ありがとうございました!
DefinitelyTyped で Ace の型を大幅に改善した
前のこの記事では、Definitely Typed
から型をインストールして適用する際に、実際にレポジトリまで見に行かないといけなかった、ということを書いた。
今回は、PRを出してマージされたので、そのことを書く。
PRタイトルにある通り、この型は全て ajaxorg/ace/ace.d.ts
から取ってきた。
コミットログを見ると、ace.d.ts
のファーストコミットが2018年3月、DefinitelyTyped/types/ace/index.d.ts
のファーストコミットが2017年3月 となっており、DefinitelyTyped
の方が先に作成されているが、以下のコメントに示す通り、
ace.d.ts
の型の方が正確であり、以下の差分に示す通り、
DefinitelyTyped
の型にはany
が多く、その役割をきちんと果たしてはいなかった。
DefinitelyTyped
のレポジトリは巨大で、clone
するにも検索するにも時間がかかって大変だったが、PRを出した。
ace.d.ts
を参考にしてるとはいえ、変更がかなり多いので、レビュワーが大変だなーと思っていたが、2週間経ってもレビュワーは反応しなかった。
botが反応し、違う人にレビュー依頼を出してからは、すぐにマージされてリリースされた。
[ace] Improve by referring to ace.d.ts by wafuwafu13 · Pull Request #51346 · DefinitelyTyped/DefinitelyTyped · GitHub
any
だらけで長い間放置されていたことも、なかなかレビューがされなかったことも、違う人にレビューが回ると一瞬でマージされたことにも、DefinitelyTyped
に対する今までの信頼性(主観)が少し損なわれたが、改善できたのはよかった。
参考にしていた、ace.d.ts
にも、まだ足りない型定義があったので少し追加しておいた。
github.com
こちらの反応は早かった。
Jest のモック関数を整理する
npq
という、npmやyarnでインストールする際に安全確認をするライブラリがあり、そこで使われているaxios
をnode-fetch
で代替するというissueがあったのでやってみた。
置き換えるだけの簡単な作業かと思っていたが、テストを通すのが難しかった。
jest.mock や mockImplementation や jest.spyOnに対する理解が曖昧だったので、整理したいと思う。
コードはこのレポジトリにまとめた。
github.com
まずは、単純な関数のモックから整理していきたい。
下のように、第2引数とランダムな値を足した値を返す関数があるとする。
function random(a, b, c) { return b + Math.random(); } module.exports = random;
この関数のテストでは、便宜上、ランダムな値は無視したい。
なぜなら、無視しないと返り値が予測できず、テストが通らないからだ。
const random = require("./random"); it("random function", () => { expect(random(1, 2, 3)).toBe(2); // Expected: 2 // Received: 2.119340184508286 });
モックする1つの方法として、jest.fn を用い、random
に新しい、未使用の mock functionを代入してしまう方法がある。
しかし、この方法では、後続のテストが壊れてしまう恐れがある。
let random = require("./random"); random = jest.fn(() => 2) it("random function", () => { expect(random(1, 2, 3)).toBe(2); // pass expect(random(1, 3, 5)).toBe(3); // Expected: 3 // Received: 2 });
そこで、random
モジュールをモックし、random
関数が第2引数を返すようにモックすれば、うまくいく。
const random = require("./random"); jest.mock('./random') random.mockImplementation((a, b, c) => b); it("random function", () => { expect(random(1, 2, 3)).toBe(2); // pass expect(random(1, 3, 5)).toBe(3); // pass });
これをnode-fetch
に対するモックでも応用してみる。
下のように、Promise
を返す関数があるとする。
const fetch = require("node-fetch"); function getByFetch() { const data = fetch("http://example.com/"); return data; } module.exports = getByFetch;
これに対するテストは、node-fetch
モジュールをモックし、fetch
関数がfoo
を返すようにモックすれば、うまくいく。
const fetch = require("node-fetch"); const getByFetch = require("./fetch"); jest.mock("node-fetch"); fetch.mockImplementation(() => { return "foo"; }); it("fetch", () => { const data = getByFetch(); expect(data).toBe("foo"); // pass });
次に、オブジェクト内の関数のモックについて整理していく。
このようなオブジェクトがあるとする。
const obj = { random: function () { return Math.random(); }, randomPlusOne: function () { return Math.random() + 1; }, foo: function () { return "foo"; }, }; module.exports = obj;
これに対するテストでモックする1つの方法としては、obj
モジュールごとモックしてしまう方法がある。
const obj = require("./object"); jest.mock("./object", () => { return { random: () => 1, randomPlusOne: () => 2, foo: () => 'foo', }; }); it("random object", () => { expect(obj.random()).toBe(1); // pass expect(obj.randomPlusOne()).toBe(2); // pass expect(obj.foo()).toBe('foo') // pass });
これは、次のようにも書ける。
const obj = require("./object"); jest.mock("./object"); obj.random.mockImplementation(() => 1); obj.randomPlusOne.mockImplementation(() => 2); obj.foo.mockImplementation(() => 'foo'); it("random object", () => { expect(obj.random()).toBe(1); // pass expect(obj.randomPlusOne()).toBe(2); // pass expect(obj.foo()).toBe('foo') // pass });
しかしこれらは、obj
モジュールごとモックしているため、例えばfoo
をモックし忘れたら、存在していないことになってしまう。
const obj = require("./object"); jest.mock("./object"); obj.random.mockImplementation(() => 1); obj.randomPlusOne.mockImplementation(() => 2); it("random object", () => { expect(obj.random()).toBe(1); // pass expect(obj.randomPlusOne()).toBe(2); // pass expect(obj.foo()).toBe('foo') // Expected: "foo" // Received: undefined });
jest.spyOnを用いれば、その心配はなくなる。
const obj = require("./object"); jest.spyOn(obj, 'random').mockImplementation(() => 1); jest.spyOn(obj, 'randomPlusOne').mockImplementation(() => 2); it("random object", () => { expect(obj.random()).toBe(1); // pass expect(obj.randomPlusOne()).toBe(2); // pass expect(obj.foo()).toBe('foo') // pass });
これをaxios
に対するモックでも応用してみる。
下のように、Promise
を返す関数があるとする。
const axios = require("axios"); function getByAxios() { const data = axios.get("http://example.com/"); return data; } module.exports = getByAxios;
これに対するテストは、以下のように書けばうまくいく。
const axios = require("axios"); const getByAxios = require("./axios"); jest.spyOn(axios, 'get').mockImplementation(() => Promise.resolve('foo')); it("axios", async () => { const data = await getByAxios(); expect(data).toBe("foo"); });
理解が少し難しいjest.mock
/mockImplementation
/jest.spyOn
あたりを整理した。