node_exporterでルーティングテーブルを監視する

先日リリースされたnode_exporter 1.1.0network_routeコレクタが追加され、Linux上のルーティングテーブルを監視することが出来るようになりました。

このPRは私が昨年出したもので、指摘を頂きながらマージしてもらったものです。

動作の概要

PR説明文からの引用ですが、 network_route コレクタを有効にすると以下のようなメトリクスを出力します。 こちらの内容をPrometheusが収集・Grafana等で可視化するという形です。

# HELP node_network_route network routing table
# TYPE node_network_route gauge
node_network_route{dest="10.0.0.0/24",gw="100.0.0.1",if="eth1",priority="20",proto="zebra",src="",weight="1"} 1
node_network_route{dest="10.0.0.0/24",gw="100.0.0.2",if="eth1",priority="20",proto="zebra",src="",weight="1"} 1
node_network_route{dest="100.0.0.0/24",gw="",if="eth1",priority="0",proto="kernel",src="100.0.0.3",weight=""} 1
node_network_route{dest="192.168.10.0/24",gw="",if="eth0",priority="0",proto="kernel",src="192.168.10.2",weight=""} 1
node_network_route{dest="default",gw="192.168.10.1",if="eth0",priority="0",proto="static",src="",weight=""} 1
# HELP node_network_routes_total network routing table
# TYPE node_network_routes_total gauge
node_network_routes_total{if="eth0"} 2
node_network_routes_total{if="eth1"} 3

背景

Linuxマシンにzebra,ospfd,bgpd等の経路交換を行うソフトウェアを利用しており、これらが実際に設定した経路を監視したいと言ったことが実装のモチベーションです。 最初は社内のGitHub Enterpriseにのみ route_exporter という名前の別ソフトウェアとして公開していましたが、他の人にも需要が有るだろうということで node_exporter へPRを提出しました。

kubernetes環境を自分で構築する場合、CalicoでBGPネットワークの構築などを行うケースも多いと思われます。また、ノードの台数を動的に変更するケースも少なくないと思いますので、このコレクタの適用範囲はある程度広いと考えています。

導入方法

network_route コレクタはデフォルトで有効になっていないので、起動引数に --collector.network_route を与えることで有効化されます。

続きを読む

TypeScriptでのバリデーションにsuperstructが最高

TypeScriptに関わらずバリデーションを書く場面というのは非常に多いと思います。例えば、フォーム・APIにPOSTされたHTTP Bodyの検証等、場面は変わってもやることはあまり変わらないと思います。愚直にif文を書く場面も多いかと思うのですが、TypeScriptのライブラリsuperstructを利用すると記述をシンプルに行なえます。

サンプルリポジトリ: https://github.com/kamijin-fanta/react-form-schemavalidation-example

↑雑なスライドです。この記事も同様の内容を記述しています。

Data Structure Validation

  • フィールド型・データの範囲などを定義・検証する仕組み
  • 人間が検証コードを書く必要が無くなって良い
  • 仕様例

    • JSON Schema
    • Swagger
  • 実装例: go, python等様々な言語にてライブラリが存在

JSON Schemaで、2~3文字のstringというルールを書いた例

{
  "type": "string",
  "minLength": 2,
  "maxLength": 3
}

superstruct

色々仕様・実装が有るわけですが、TypeScriptでのみ利用するのであればsuperstructがかなり使いやすいかなと感じます。

まずは簡単な例から。 assert関数はルールに一致しなければ例外を吐きます。 TypeScriptのassertion functionとして利用が可能なので、言語との親和性も高いです。

it("string & assert", () => {
  const strSchema = string(); // 文字列というスキーマを作成

  {
    const input: any = "1234";
    assert(input, strSchema);
    // ここまで到達すればstringであることが保証される
    expect(typeof input).toEqual("string");
  }

  expect(() => {
    const input: any = 1234;
    assert(input, strSchema);
  }).toThrow("Expected a string, but received: 1234");
});

例外を投げてほしくない場合、以下のようにも検証が可能です。 assert 関数の代わりに validate 関数を利用しています。 タプルでエラーと結果が返ります。

it("number & validation", () => {
  const numSchema = number();

  {
    const input: any = 1234;
    const [error, result] = validate(input, numSchema); // numSchema.validate(input)とも書ける
    expect(error).toBeUndefined();
    expect(result).toEqual(1234);
  }

  {
    const input: any = "1234";
    const [error, result] = validate(input, numSchema);
    expect(error?.message).toEqual('Expected a number, but received: "1234"');
    expect(result).toBeUndefined();
  }
});

次はオブジェクトの構造について検証します。 数値のid、文字列のname、文字列を含む配列のtagsを定義しています。 ここでは検証にassertvalidateの代わりにisを利用しているので、型のガードが行われます。

it("object & is", () => {
  const userSchema = object({
    id: number(),
    name: string(),
    tags: array(string()),
  });

  {
    const input: any = { id: 123, name: "mike", tags: ["hoge", "fuga"] };
    expect(is(input, userSchema)).toEqual(true);
    if (is(input, userSchema)) {
      // inputがuserSchemaを満たす場合にのみifに入るので、inputはanyでなくなる
      // input.id, input.name...
    }
  }

  {
    const input: any = { name: "mike" };
    expect(is(input, userSchema)).toEqual(false);
  }
});

今まではデータ型について注目していましたが、内容についてバリデーションを書きます。 name フィールドは長さが1~20の文字列であることが必要です。 また、Infer型を用いてuserSchemaからuserTypeの導出も行ってみました。 size以外にもいくつかの制約条件がデフォルトで定義されており、自分で定義することも可能です。

it("refinements", () => {
  const userSchema = object({
    id: number(),
    name: size(string(), 1, 20),
  });
  type userType = Infer<typeof userSchema>;

  {
    const input: userType = { id: 123, name: "" };
    const [error] = validate(input, userSchema);
    expect(error?.message).toEqual(
      "At path: name -- Expected a string with a length between `1` and `20` but received one with a length of `0`"
    );
  }
});

フィールド内容によってその値を書き換える機能も提供されており、例えばからの場合にデフォルト値を設定することが可能です。 validate関数のオプションに{ coerce: true }を指定することで利用が可能です。 ここではデフォルト値として、IDの値を実行するたびに増加する整数を設定しています。

it("coercions", () => {
  let index = 0;
  const userSchema = object({
    id: defaulted(number(), () => index++),
    name: trimmed(size(string(), 1, 20)),
  });

  {
    const input: any = { name: " mike " };

    // 1回目の実行
    const [, res1] = validate(input, userSchema, { coerce: true });
    expect(res1).toEqual({
      id: 0,
      name: "mike",
    });

    // 2回目の実行
    const [, res2] = validate(input, userSchema, { coerce: true });
    expect(res2).toEqual({
      id: 1,
      name: "mike",
    });
  }
});

今までは新たにスキーマから作成していましたが、既存の型に対してのスキーマを作成することも可能です。 Describe<T>型を用いることで、スキーマと既存型が一致していることを検証できます。 残念ながら自分で内容は埋めなければならないのですが、抜け漏れを防ぐことが出来ます。

it("use exist type", () => {
  type User = {
    id: number;
    name: string;
  };

  const userSchema: Describe<User> = object({
    id: number(),
    name: string(),
  });

  const input: any = { id: 123, name: "mike" };
  const [, res] = validate(input, userSchema);
  expect(res).toEqual({
    id: 123,
    name: "mike",
  });
});

react-hook-formと組み合わせる

react-hook-formはパフォーマンスが良く使いやすいReactのフォームライブラリです。 バリデーション・繰り返しフィールド等の機能が一通り揃っていて実用的です。

通常react-hook-formでは次のようにフォームを記述します。 バリデーションのルールはregisterの中に記述されています。

export function SimpleForm(): React.ReactElement {
  const { register, handleSubmit, errors } = useForm();
  const onSubmit = (data: any) => console.log(data);

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <div className="group">
        <label>
          name
          <input name="name" ref={register({ required: true, minLength: 1, maxLength: 20 })} />
          {errors.name && "name is required"}
        </label>
      </div>
      <div className="group">
        <label>
          age
          <input name="age" type="number" ref={register({ required: true, valueAsNumber: true, min: 20 })} />
          {errors.age && "age must be upper 20"}
        </label>
      </div>
      <input type="submit" />
    </form>
  );
}

こちらの書き方でも良いのですが、エラー文章を自分で書く必要があり、errors onSubmitが型安全とはなりません。 (もちろん自分で型注釈すれば改善できます) こういった問題をsuperstructを組み合わせることで改善できます。

superstructを利用した例は以下のようになります。

const parsableInt = coerce(number(), string(), (value) => parseInt(value));

const userSchema = object({
  name: size(string(), 1, 20),
  age: min(parsableInt, 20),
});
type userType = Infer<typeof userSchema>;

export function SchemaValidationForm(): React.ReactElement {
  const { register, handleSubmit, errors } = useForm({
    resolver: superstructResolver(userSchema, { coerce: true }),
  });
  const onSubmit = (data: userType) => console.log(data);

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <div className="group">
        <label>
          name
          <input name="name" ref={register()} />
          {errors.name?.message}
        </label>
      </div>
      <div className="group">
        <label>
          age
          <input name="age" type="number" ref={register()} />
          {errors.age?.message}
        </label>
      </div>
      <input type="submit" />
    </form>
  );
}

register内のルール記述が不要となり、エラーメッセージはsuperstructが自動生成します。 errors等もスキーマから自動的に型の推測が行われるので、typo等の心配もなくなります。

react-hook-form+superstructはreactでフォーム作る人にオススメな構成だと言えるでしょう。

続きを読む