RustでFREETELのギガ数を取得してInfluxDBにいれてGrafanaで見るまで ~ Dockerに添えて ~

# TL;DR

1. Docker を使って Rust, InfluxDB, Grafana の環境を構築する
2. FREETEL のマイページのログインセッションを取得する
3. InfluxDB にデータを投入して、Grafana で見る


# 動機

> もうギガ数に怯える生活は嫌だ！
> せめて、ギガ数が可視化されていれば利用を控えるかもしれない。

# Docker の準備

[influxDB + Grafanaに入門する | Qiita](http://qiita.com/chroju/items/355f3c6da9f8c4867ba5) を参考に Rust, InfluxDB, Grafana の環境を整えます。


```shell-session
$ docker -v
Docker version 17.07.0-ce-rc2, build 36ce605

$ docker-compose -v
docker-compose version 1.15.0, build e12f3b9
```

Rust のソースを置いたり、 InfluxDB や Grafana のデータディレクトリのために `./mount` 配下をコンテナにマウントさせます。


```yaml:docker-compose.yaml
version: "2"

services:
  rust:
    build: .
    volumes:
      - "./mount/rust:/opt/rust"

  influxdb:
    image: influxdb
    ports:
      - "8083:8083"
      - "8086:8086"
    volumes:
      - "./mount/influxdb:/var/lib/influxdb"

  grafana:
    image: grafana/grafana
    ports:
      - "3000:3000"
    volumes:
      - "./mount/grafana:/var/lib/grafana"
```

コンテナ内の Rust のソースは `/opt/rust` に置くことにします。

```text:Dockerfile
FROM rust:1.19.0
RUN apt-get update && apt-get install -y pkg-config libssl-dev
RUN mkdir -p /opt/rust
WORKDIR /opt/rust

CMD ["sh", "-c", "tail -f /dev/null"]
```



```shell-session
$ docker-compose build

$ docker-compose start
Starting influxdb ... done
Starting rust     ... done
Starting grafana  ... done

$ docker-compose ps
          Name                      Command           State                       Ports
------------------------------------------------------------------------------------------------------------
foo_grafana_1    /run.sh                   Up      0.0.0.0:3000->3000/tcp
foo_influxdb_1   /entrypoint.sh influxd    Up      0.0.0.0:8083->8083/tcp, 0.0.0.0:8086->8086/tcp
foo_rust_1       sh -c tail -f /dev/null   Up
```


# Rust のソースを書く

`./mount/rust/src/main.rs`, `./mount/rust/Cargo.toml` のファイルを作成します。

`Cargo.toml` ファイルは、 nodeで言うところの `package.json` と似たようなものです。

```rust:./mount/rust/src/main.rs
fn main() {
  println!("Hello World!");
}
```

```rust:./mount/rust/src/main.rs
[package]

name = "freetel_usage"
version = "0.0.1"
authors = [ "tady <a.dat.jp@gmail.com>" ]
```

```shell-session
docker-compose exec rust cargo run
```
これで、 `rust` コンテナの中で `cargo` が実行され、Rustの実行まで行われます。


# Rust で HTTP リクエストを行う

今回は、 nodeでもお世話になったことがある HTTP リクエストライブラリと同名の [reqwest](https://docs.rs/reqwest/0.7.2/reqwest/) を利用します。
これは [hyper](https://hyper.rs/guides) をクライアント機能のみにしたラッパーです。
非同期処理などを気にせず使えるようにしたもので、今回はこれで十分です。

サンプルのコードは以下のようになります。

```rust
// GET
let client = reqwest::Client::new().unwrap();
let mut resp = client.get("http://example.com/").unwrap()
    .header(...) // hedderの付与
    .send().unwrap();

let mut content = String::new(); // レスポンスの入れ物
resp.read_to_string(&mut content).unwrap();


// POST
let client = reqwest::Client::builder().unwrap()
    .redirect(...) // カスタムリダイレクトポリシーの設定
    .build().unwrap();

let params = [
  ("key", "value")
];

// HTTP Post リクエスト実行
let resp = client.post(LOGIN_FORM_URL).unwrap()
    .header(...) // hedderの付与
    .form(&params).unwrap() // formデータの付与
    .send().unwrap();

resp.status() // スレータスの取得
resp.headers() // レスポンスヘッダの取得
```

今回は、 reqwestのサンプルコードの機能に加え、以下の機能を利用します：

- カスタムリダイレクトポリシーの設定
  - ログインの Post リクエストのレスポンスヘッダの `Set-Cookie` を取得するため
- リクエストにヘッダ(Cookie)を付与
  - ログイン後ページにアクセスするため


# カスタムリダイレクトポリシーの設定

reqwest のデフォルトでは、リダイレクトをフォローするようになっています。
https://docs.rs/crate/reqwest/0.7.2/source/src/redirect.rs

今回は、ログイン Post リクエストのレスポンスに含まれる `Set-Cookie` ヘッダが欲しいため、自動的にフォローしないようにします。

そのために、 HTTP クライアントのビルダーに `redirest()` というメソッドがあるため、これを利用します。

```rust
// カスタムリダイレクトポリシー
// ログインリクエスト後に別のページに遷移するのを防ぐため `stop()` する
let custom = RedirectPolicy::custom(|attempt| {
    // attempt.url() //=> リダイレクトしようとしている次のURL
    // attempt.previous() //=> リダイレクトしてきた過去のURLの配列
    attempt.stop()
});
let client = reqwest::Client::builder().unwrap()
    .redirect(custom)
    .build().unwrap();
```

`attempt` には、「リダイレクトしようとしている次のURL」「リダイレクトしてきた過去のURLの配列」などの情報が含まれています。
デフォルトのリダイレクトポリシーでは、「リダイレクトループの検知」「10回以上のリダイレクトの抑制」などの機能が含まれています。
今回のコードのようにカスタムリダイレクトポリシーを作る際には、同様の検知・抑制の仕組みも自前で実装が必要なことが多いので注意しましょう。


# Set-Cookieヘッダの取得

レスポンスヘッダーは `resp.headers().get` で取得できますが、ヘッダーの型を渡すことで、目的のヘッダを一発で取得することが出来ます。

```rust
if let Some(set_cookies) = resp.headers().get::<header::SetCookie>() {
      let mut set_cookie_value = String::new();
      for set_cookie in &set_cookies.0 {
          let c = Cookie::parse(set_cookie.clone()).expect("Failed to parse cookie.");
          let (name, value) = c.name_value();

          if name == SESSION_COOKIE_NAME {
              set_cookie_value = value.to_string();
          }
      }
      if set_cookie_value != "" {
          return set_cookie_value;
      }
      panic!("Set-Cookie '{}' does not exist!, Set-Cookies: {:?}", SESSION_COOKIE_NAME, set_cookies);
  } else {
      panic!("Set-Cookie '{}' does not exist!", SESSION_COOKIE_NAME);
  }
```

なぜか、FREETELのログインページは、 `Set-Cookie` を２つ返してくるので、有効だと思われる2つめの値を利用しています。


# 環境変数の利用

FREETEL のマイページログインには、メールアドレス、パスワード、電話番号の３つが必要です。
これらは環境変数経由で渡すようにしましょう。

```shell-session
$ docker-compose exec rust env FREETEL_EMAIL=<メールアドレス> FREETEL_PASSWORD=<パスワード> FREETEL_TEL=<ハイフン無し電話番号> cargo run
```

環境変数の取得は以下のようなコードになります：

```rust
let email = env::var("FREETEL_EMAIL").expect("env 'FREETEL_EMAIL' not found");message...
```

# HTML のパース

HTML のパースには、 [select](https://docs.rs/crate/select/) というパーサと、正規表現クレートの `regex `を利用しています。

```rust
use select::document::Document;
use select::predicate::{Predicate, Attr, Class};
use regex::Regex;

let re_usage = Regex::new(r"([\d\.]+)GB").unwrap();

let mut current_usage: f32 = 0.0;
let mut usage_limit: f32 = 0.0;

let document = Document::from(html);

for node in document.find(Class("sim-usage").descendant((Attr("style", "font-size: x-large;")))).take(1) {
    let text = node.text();
    let caps = re_usage.captures(&text).unwrap();
    current_usage = caps.get(1).unwrap().as_str().parse::<f32>().unwrap();
}

for node in document.find(Class("sim-usage").descendant((Attr("style", "font-size: smaller;")))).take(1) {
    let text = node.text();
    let caps = re_usage.captures(&text).unwrap();
    usage_limit = caps.get(1).unwrap().as_str().parse::<f32>().unwrap();
}

(current_usage, usage_limit)
```

このあたりのコードは Rubyと比べても難しくはないですね。


# InfluxDB へのデータ投入

InfluxDB は `curl` で言うと以下のようなリクエストを受け付けます。

```shell-session
$ curl -i -XPOST 'http://INFLUXDB_URL' --data 'freetel_usage value=0.64 1503063534888000000'
```

InfluxDB 用のクレートもあるようですが、今回は `reqwest` をせっかく導入しているので、これで済ませます。

```rust
let timespec = time::get_time();
let current_time_nano = [timespec.sec.to_string(), format!("{:09}", timespec.nsec.to_string())].join("");
let data = [
    format!("freetel_usage value={} {}", current_usage, current_time_nano),
    format!("freetel_limit value={} {}", usage_limit, current_time_nano)
].join("\n");

let client = reqwest::Client::new().unwrap();
let resp = client.post(INFLUXDB_URL).unwrap()
    .body(data.clone())
    .send().unwrap();

println!("resp: {:?}", resp);

if !resp.status().is_success() {
    panic!("influxdb request failed! {}, {:?}, {:?}", INFLUXDB_URL, resp.status(), data);
}
```

単純に Post するだけですね。

# コード

今回のコードは [tadyjp/rust-freetel-usage | Github](https://github.com/tadyjp/rust-freetel-usage) に置いてあります。


# Grafana

このコードを書いていいるうちに、10GBの上限に達してしまった愚かな図はこちらです：

![Screen Shot 2017-08-20 at 9.15.27 PM.png](https://qiita-image-store.s3.amazonaws.com/0/10272/ef17f90e-7c24-2b82-ede8-f3c45b738cb7.png)
 

# オチ

今の生活のネットインフラは `Softbank：FREETEL = 9:1` なので、本当は マイソフトバンクのギガ数を取得したかった。
しかし、マイソフトバンクのログインの仕組みは Capy認証のため、スクレイピングのみで達成するのは困難な上に、Yahoo!ログイン経由でも機械からのアクセスだと文字認証が入るため断念。

次にキャリアを変える時は、ギガ数をプログラムから簡単に扱えるところにします。