経験は何よりも饒舌

10年後に真価を発揮するかもしれないブログ 

Mackerel アンバサダーになりました


Mackerel アンバサダー に選ばれました。

Ambassador プランになりました


前職は Mackerel のエンジニアとしてアルバイトをしていた (例) のですが、再び好きなサービスに関われて非常に光栄です。

これを機にまたいろいろ遊ぼうと思います。


合わせて読みたい
wafuwafu13.hatenadiary.com

本ブログの掲載内容は私自身の見解であり、必ずしも所属する企業や組織の立場、戦略、意見を代表するものではありません。

Mackerel で為替を監視する

欧州で働いているため、給料がユーロで振り込まれる。
永住する気はさらさらないので、いづれは全て円にしなければならない。
いつすればいいの?損をしたくない!
気づけば FX をしている状態になっていた。

とはいえ、自分でコントロールできない値に一喜一憂するほど人生は長くない。
ならばせめて監視をしよう。
そうだ、Mackerel プラグインを作ろう。

というわけで完成したのがこれ。(自分以外が使用することを想定していません)

github.com


まずは API がないか調べる。
[ 為替 API 無料 ] で以下の記事がヒットした。

有料になった exchangeratesapi.io を無料の範囲内で使い倒すnpmパッケージを作ってみた #Node.js - Qiita

どうやら Exchange Rates API に無料プランがあるらしい。
他をざっとみたら有料だったので exchangeratesapi を使用する。

無料プランの注意点として、リクエストが月に 1000回 までに制限されている。
API の更新頻度が 1日 1回 なので 30回程度 叩ければ十分だが、なんとなく定期的にメトリクスを投稿したい。
なのでとりあえず余裕を持って 2時間 に 1回 リクエストを投げるようにした。
具体的には、メトリクスを投稿した時点のタイムスタンプをファイルに書き込んで、エージェントが実行する際には、ファイルの最終行を読み取り、2時間 以上経っていなければ意図的にエラーを返すようにした。

mackerel-plugin-exchange/lib/exchange.go at f4659da9d6b0afaaab125214594cac8851b90147 · wafuwafu13/mackerel-plugin-exchange · GitHub

作りたてなので面白いグラフにはなってない


あとはよしなに式監視 alias(scale(host(4EuzHqHvEEU, custom.exchange.USD), 1), USD) などを仕込む。

テストで Warning > 140 Critical > 145 にしてみた

この式監視では、円高に気付けない、アラートの闘値を逐一変更する必要がある、などの問題点があるが、linearRegression() とかで本格的に監視できる可能性を感じた。

スナップショットテストのテストにスナップショットを使う

github.com

testing/snapshot.tsgreen(bold(`\n > ${updated} snapshots updated.`))の出力に影響する部分のリファクタリングをする際、異なるディレクトリにあるスナップショットテストの実行結果をテストで保証したかった。

既存のコードを読んだら、スナップショットテストのテストにスナップショットを使っていたのでおもしろかったので、追加したテストについてメモしておく。


まず、一時的なディレクトリを作成し、受け取った関数を実行し、実行後に一時的なディレクトリを削除する関数を用意する。

https://github.com/denoland/deno_std/blob/b7e6dc0914aeaa93b6af3f9db666eaaf7615f456/testing/snapshot_test.ts#L45-L62

function testFnWithDifferentTempDir(
  fn: (
    t: Deno.TestContext,
    tempDir1: string,
    tempDir2: string,
  ) => Promise<void>,
) {
  return async (t: Deno.TestContext) => {
    const tempDir1 = await Deno.makeTempDir();
    const tempDir2 = await Deno.makeTempDir();
    try {
      await fn(t, tempDir1, tempDir2);
    } finally {
      await Deno.remove(tempDir1, { recursive: true });
      await Deno.remove(tempDir2, { recursive: true });
    }
  };
}


実行される関数についてみていく。

引数に、スナップショットテストを記述した文字列を渡して関数を実行している。

https://github.com/denoland/deno_std/blob/b7e6dc0914aeaa93b6af3f9db666eaaf7615f456/testing/snapshot_test.ts#L629-L660

testFnWithDifferentTempDir(async (t, tempDir1, tempDir2) => {
...
    const result1 = await runTestWithUpdateFlag(
      `
      import { assertSnapshot } from "${SNAPSHOT_MODULE_URL}";

      Deno.test("Snapshot Test - First", async (t) => {
        await assertSnapshot(t, [
          1,
          2,
        ]);
      });
      Deno.test("Snapshot Test - Second", async (t) => {
        await assertSnapshot(t, [
          3,
          4,
        ]);
      });`,
      `
      import { assertSnapshot } from "${SNAPSHOT_MODULE_URL}";

      Deno.test("Snapshot Test - First", async (t) => {
        await assertSnapshot(t, [
          1,
          2,
        ]);
      });
      Deno.test("Snapshot Test - Second", async (t) => {
        await assertSnapshot(t, [
          3,
          4,
        ]);
      });`,
    );
...


実行される関数は、スナップショットテストを記述した文字列を一時的なディレクトリにあるファイルに書き込んで、スナップショットテストをコマンドで実行している。
その結果が返り値となっている。

https://github.com/denoland/deno_std/blob/b7e6dc0914aeaa93b6af3f9db666eaaf7615f456/testing/snapshot_test.ts#L594-L618

    const tempTestFileName = "test.ts";
    const tempTestFilePath1 = join(tempDir1, tempTestFileName);
    const tempTestFilePath2 = join(tempDir2, tempTestFileName);

    async function runTestWithUpdateFlag(test1: string, test2: string) {
      await Deno.writeTextFile(tempTestFilePath1, test1);
      await Deno.writeTextFile(tempTestFilePath2, test2);

      const command = new Deno.Command(Deno.execPath(), {
        args: [
          "test",
          "--allow-all",
          tempTestFilePath1,
          tempTestFilePath2,
          "--",
          "-u",
        ],
      });
      const { stdout, stderr } = await command.output();

      return {
        output: new TextDecoder().decode(stdout),
        error: new TextDecoder().decode(stderr),
      };
    }


その結果を、スナップショットテストする。

https://github.com/denoland/deno_std/blob/b7e6dc0914aeaa93b6af3f9db666eaaf7615f456/testing/snapshot_test.ts#L663-L665

    await assertSnapshot(t, formatTestOutput(result1.output), {
      name: "Snapshot Test - Different Dir - New snapshot",
    });

スナップショットが生成される。
これをみて、green(bold(`\n > ${updated} snapshots updated.`))の出力に影響がないか確認する。

https://github.com/denoland/deno_std/blob/b7e6dc0914aeaa93b6af3f9db666eaaf7615f456/testing/__snapshots__/snapshot_test.ts.snap#L517-L550

snapshot[`Snapshot Test - Different Dir - New snapshot 1`] = `
"running 2 tests from <tempDir>/test.ts
" +
  "Snapshot Test - First ... ok (--ms)
" +
  "Snapshot Test - Second ... ok (--ms)
" +
  "------- output -------
" +
  "
" +
  " > 2 snapshots updated.
" +
  "running 2 tests from <tempDir>/test.ts
" +
  "Snapshot Test - First ...----- output end -----
" +
  "Snapshot Test - First ... ok (--ms)
" +
  "Snapshot Test - Second ... ok (--ms)
" +
  "------- output -------
" +
  "
" +
  " > 2 snapshots updated.
" +
  "
" +
  "ok | 4 passed | 0 failed (--ms)
" +
  "
"
`;

それぞれの実行結果が2 snapshots updatedになっているのでテスト成功。

About Deno 1.34 API changes - Deno.FileInfo

Deno 1.34 was released in May 26, 2023

deno.com

I implemented Deno API changes - Deno.FileInfo

https://deno.com/blog/v1.34#denofileinfo

The Deno.FileInfo interface now includes the following new fields:

  • Deno.FileInfo.isBlockDevice
  • Deno.FileInfo.isCharDevice
  • Deno.FileInfo.isFifo
  • Deno.FileInfo.isSocket


The fields are available on Linux and macOS. On Windows, they are always null.

Thank you to Hirotaka Tagawa for the contribution.

PR is #19008
github.com



Versions prior to 1.34 don't have isBlockDevice, isCharDevice, isFifo, isSocket information in FileInfo


Example =>

$ touch file_test

$ deno --version
deno 1.33.2 (release, aarch64-apple-darwin)
v8 11.4.183.1
typescript 5.0.3

> Deno.statSync('/Users/wafuwafu13/desktop/file_test')
{
  isFile: true,
  isDirectory: false,
  isSymlink: false,
  size: 0,
  mtime: 2023-05-25T20:38:52.075Z,
  atime: 2023-05-25T20:38:52.075Z,
  birthtime: 2023-05-25T20:38:52.075Z,
  dev: 16777232,
  ino: 56604379,
  mode: 33188,
  nlink: 1,
  uid: 501,
  gid: 20,
  rdev: 0,
  blksize: 4096,
  blocks: 0
}


In version 1.34 have isBlockDevice, isCharDevice, isFifo, isSocket information in FileInfo


Example =>

$ deno --version
deno 1.34.0 (release, aarch64-apple-darwin)
v8 11.5.150.1
typescript 5.0.4

> Deno.statSync('/Users/wafuwafu13/desktop/file_test')
{
  isFile: true,
  isDirectory: false,
  isSymlink: false,
  size: 0,
  mtime: 2023-05-25T20:38:52.075Z,
  atime: 2023-05-25T20:38:52.075Z,
  birthtime: 2023-05-25T20:38:52.075Z,
  dev: 16777232,
  ino: 56604379,
  mode: 33188,
  nlink: 1,
  uid: 501,
  gid: 20,
  rdev: 0,
  blksize: 4096,
  blocks: 0,
  isBlockDevice: false,
  isCharDevice: false,
  isFifo: false,
  isSocket: false
}


Example of isBlockDevice: true =>

> Deno.statSync('/dev/disk0')
{
  isFile: false,
  isDirectory: false,
  isSymlink: false,
  size: 0,
  mtime: 2023-05-24T20:27:08.000Z,
  atime: 2023-05-24T20:27:08.000Z,
  birthtime: 1970-01-01T00:00:00.000Z,
  dev: 89290492,
  ino: 623,
  mode: 24992,
  nlink: 1,
  uid: 0,
  gid: 5,
  rdev: 16777216,
  blksize: 2048,
  blocks: 0,
  isBlockDevice: true,
  isCharDevice: false,
  isFifo: false,
  isSocket: false
}


Example of isCharDevice: true =>

> Deno.statSync('/dev/tty')
{
  isFile: false,
  isDirectory: false,
  isSymlink: false,
  size: 0,
  mtime: 2023-05-24T20:27:08.000Z,
  atime: 2023-05-24T20:27:08.000Z,
  birthtime: 1970-01-01T00:00:00.000Z,
  dev: 89290492,
  ino: 334,
  mode: 8630,
  nlink: 1,
  uid: 0,
  gid: 0,
  rdev: 33554432,
  blksize: 65536,
  blocks: 0,
  isBlockDevice: false,
  isCharDevice: true,
  isFifo: false,
  isSocket: false
}


Example of isFifo: true =>

$ mkfifo fifo_test

> Deno.statSync('/Users/wafuwafu13/desktop/fifo_test')
{
  isFile: false,
  isDirectory: false,
  isSymlink: false,
  size: 0,
  mtime: 2023-05-25T20:37:24.714Z,
  atime: 2023-05-25T20:37:24.714Z,
  birthtime: 2023-05-25T20:37:24.714Z,
  dev: 16777232,
  ino: 56604333,
  mode: 4516,
  nlink: 1,
  uid: 501,
  gid: 20,
  rdev: 0,
  blksize: 4096,
  blocks: 0,
  isBlockDevice: false,
  isCharDevice: false,
  isFifo: true,
  isSocket: false
}


Example of isSocket: true =>

> Deno.statSync('/var/run/filesystemui.socket')
{
  isFile: false,
  isDirectory: false,
  isSymlink: false,
  size: 0,
  mtime: 2023-05-24T20:27:25.710Z,
  atime: 2023-05-24T20:27:25.710Z,
  birthtime: 2023-05-24T20:27:25.710Z,
  dev: 16777232,
  ino: 56514866,
  mode: 49572,
  nlink: 1,
  uid: 501,
  gid: 0,
  rdev: 0,
  blksize: 4096,
  blocks: 0,
  isBlockDevice: false,
  isCharDevice: false,
  isFifo: false,
  isSocket: true
}

🦕

株式会社はてなを退職します

From: 株式会社はてな
To: Amazon Web Services EMEA SARL

2020年8月から2023年2月末まで、2年半くらいエンジニアとしてアルバイトをし、3つのチームでお世話になりました。

はてなブログ(2020年8月~2021年7月)

面接を受けたのは学部2年の夏。
その頃にはコロナが猛威を奮っていて、大学は動画を提供する施設になっていました。
1浪までして入った社会学部ですが、社会に興味が持てず、情報学の独学に励み、評価されず、楽しみや評価されたことは忘れ、理想のスケールとの乖離に陰鬱となり、後戻りはできぬと吐きそうになっていた頃に、憧れていたはてなに入れることになりました。
家では集中できなかったのと、三条あたりを歩くのが好きだったので、週3回フルタイムでオフィスに出社していました。
キャンパスの喧騒より、御池ビルの静閑が心地よく、オフィスに行った回数がキャンパスのそれに迫ろうとしていて、その事実が、自分はエンジニアであるという矜持をより強固にしたのでしょう。
ある時期までかなり美味しいお弁当が提供されていて、体制の変更で提供されなくなってからは、高級感漂う松屋ホテルユニゾ京都烏丸御池店に通っていました。

まずは普通の学生では触れないであろう言語、Perlの入門から始まりました。
もともとJavaScript,Rubyあたりを触っていたのですが、Perlは衝撃的な言語でした。
はてなブログのコードはそれまでの経験からするととてつもなく精巧かつ巨大で、最初の頃はメンターさんにかなり面倒を見てもらいました。
特にコードレビューは深く正確で、これ以上のレビューを受けることはそうそうないだろうと今でも思っています。
フロントエンドとバックエンドの境なくいろいろやっていて、多かったのが機能修正・改善でした。
表に出ているのだと、例えばこの中のいくつかをやっていました。
staff.hatenablog.com
半年ほど経てば、荒削りだった思考がましになったと自覚するまでになり、数多くのタスクに手をつけていました。
アルバイトは手数が勝負だと思っていたので、多いときは1日に10件くらいのissueをちょっとづつ進めてまたレビューに投げるというような状態でした。
そのうち数ヶ月単位のプロジェクトにも携わるようになり、企画やデザイナーの方と密に関わったり、デプロイやらキャッシュやらを考慮するエンジニアにもなれました。
staff.hatenablog.com
日々自分が開発しているブログを読み、書き、反応も貰える充足感。
その一方で、エンジニアとしての実力にまだ飢えていました。
時間があって余裕がなかったからか、他人にできて自分にできないことが許せなくて、興味ないことは認めないことで排除するスタンスをとっていた。

シスプラ(2021年11月~2022年4月)

他社のインターン等の事情で8月から10月末まではてなを離れていました。
OSS活動に熱中していたようです。
wafuwafu13.hatenadiary.com
はてなブログのチームではインフラを触る機会があまりなかったので、異動したいと申し出ました。
心残りがあったので、引き続きはてなブログにも顔を出していました。
developer.hatenastaff.com
インフラ寄りのタスクは必要な思考が異なり、タスクがあっても前提知識が足りずアプローチの仕方が分からない状態で、ここでもメンターさんに面倒を見てもらいました。
CI/CDの整備や構築、そして普通の学生では叩かないであろうAkamaiAPIであれこれするタスクなどをこなし、アプリケーションの開発と異なる面白さが発見できました。
その頃には家が集中できる環境になっていたので、リモートしか受け付けない体になりました。
3月にオフィスが移転されたのですが、最終出社以外1度も訪れることはありませんでした。
余裕と惰性を感じさせない日々を貫く緊張感と共に過ごすことは特別でも日常でもなかった。

Mackerel(2022年4月~2023年2月)

AWSなど手段を問わず関わってみたかったサービスがあり、Mackerelもその1つでした。
そのため、就活は終了していましたが、時給交渉が成功したことも相まって、はてなを離れずMackerelチームに異動することにしました。
興味はインフラにあったのですが、AngularJSからReactへの移行プロジェクトが進んでいたので、それに加わることになりました。
フロントは1番得意な領域だったのですが、大規模な移行プロジェクトに継続的に関わったことはなかったので、学びがいろいろありました。
それがひと段落したのが9月頃で、そこからはチームが変わってOSSを7割、残りは内部の改善や構築をしていました。
この働き方が1番性に合っていて、かなり楽しかったです。
https://github.com/pulls?page=1&q=author%3Awafuwafu13+org%3Amackerelio+org%3Amackerelio-labs
急いで繕った夢が叶った。



振り返れば大学生の半分を超える期間をはてなで過ごしてきました。
はてなに関わった方が積み上げた土台の上でひたすら自分のやりたいことだけやらせていただきました。インターンもバイトもずっと続いてほしいです。
大変お世話になりました。ありがとうございました。

Rustで『Database Design and Implementation』の3章を実装する

約1年前にDenoでチャレンジをしていたが、4章でbufferを扱えきれなくてScalaに乗り換えた。

wafuwafu13.hatenadiary.com
github.com

scala-simpledbのログによると12章まで進んでいたようだが、CIが落ちているので、どこかしらで進めなくなったらしい。
また、Scalaでも結局import java.***していたのでほとんど元のSimpleDBの写経になっていたような記憶がある。
というわけで、心機一転Rustで挑戦してみた結果、3章までなんとか実装できたのでメモしておく。

github.com

1番の難所はbufferの扱いだが、bytebufferという神Crateのおかげで乗り越えられた。
set_rposset_wposをサポートしているので、直感的に思った位置に書き込める。
欲を言えばその位置もデバッグ出来れば楽だが、get_rposget_wposで取得できるので問題ない。

github.com
追記: 素早くマージしてくださった。感謝。


注意点として、set_wposの説明にSets the writing cursor to min(newPosition, self.len()) to prevent overflowとあるように、書き込みで指定するoffsetがbufferの長さを超えているとき、自動で確保されるのではなく切り詰められるので、事前にresizeで確保しておく必要がある。

pub fn set_i32(&mut self, offset: usize, n: i32) {
    if self.buffer.len() < offset {
        self.buffer.resize(offset + n.to_be_bytes().len())
    }
    self.buffer.set_wpos(offset);
    self.buffer.write_i32(n);
}

ファイルに書き込んだbufferはVSCodeHex Editorを使えばデバッグしやすい。

abcdefghijklmと345(=101011001=(00000001, 01011001=(Y)))

CIはQuickstart CI workflowをそのまま使った。

手応え的に4章も実装できそうな気がしている。

ArduinoでDHT11を使って室温を取得し、Mackerelに投稿する

ELEGOO Arduino用UNO R3スターターキットに同梱されている、気温と湿度を測れるやつ(DHT11 Temperature and Humidity module)で取得した気温をMackerelに投稿してみた。

まずは説明書通り簡単な回路を組んでArdunio IDEで実行する。

Elegoo Uno R3 + DHT11 + F-M wires

スターターキットに同梱されているサンプルコードの出力を、Serial.println(temperature)だけにしてシンプルにしておいた。

~/desktop/mackerel-plugin-dht 
$ git diff DHT11.ino 
diff --git a/DHT11.ino b/DHT11.ino
index c4c54ac..f80e18a 100644
--- a/DHT11.ino
+++ b/DHT11.ino
@@ -48,6 +48,10 @@ void loop( )
      true, then a measurement is available. */
   if( measure_environment( &temperature, &humidity ) == true )
   {
+    Serial.println(temperature);
-    Serial.print( "T = " );
-    Serial.print( temperature, 1 );
-    Serial.print( " deg. C, H = " );
-    Serial.print( humidity, 1 );
-    Serial.println( "%" );
   }
 }


Mackerelへの投稿は、go-mackerel-pluginを利用してカスタムメトリックプラグインを作成して行う。
シリアル通信の受信は、go.bug.st/serialを利用するとできる。
他の人が使うことを考慮していないため、ポートのパスはベタ書きした。
scanner.Scan()した1行目の値の表記がおかしかったので、2行目の値を投稿することにした。
注意点として、killしないと受信できない。

~/desktop 
$ lsof | grep usbmodem 
serial-mo 22338 wafuwafu13    3u      CHR                9,5         0t7                 723 /dev/cu.usbmodem1101
~/desktop 
$ kill 22338
func (u DhtPlugin) FetchMetrics() (map[string]float64, error) {
	mode := &serial.Mode{
		BaudRate: 9600,
	}
	port, err := serial.Open("/dev/cu.usbmodem1101", mode)
	if err != nil {
		return nil, fmt.Errorf("Failed to open port: %s", err)
	}
	scanner := bufio.NewScanner(port)
	scanner.Split(bufio.ScanLines)
	var degrees []string
	for scanner.Scan() {
		degrees = append(degrees, scanner.Text())
		if len(degrees) == 2 {
			break
		}
	}
	degree, _ := strconv.ParseFloat(degrees[1], 64)
	return map[string]float64{"degrees": degree}, nil
}

ビルドして、パスを設定ファイルに追加する。

[plugin.metrics.dht]
command = "/Users/wafuwafu13/Desktop/mackerel-plugin-dht/mackerel-plugin-dht"

室温は18℃だった。ストーブに近づけたら47℃まで上がった。

使用したコードはこちら。
github.com