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

  • 4
    Like
  • 0
    Comment

TL;DR

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

動機

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

Docker の準備

influxDB + Grafanaに入門する | Qiita を参考に Rust, InfluxDB, Grafana の環境を整えます。

$ 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 配下をコンテナにマウントさせます。

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 に置くことにします。

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"]
$ 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 と似たようなものです。

./mount/rust/src/main.rs
fn main() {
  println!("Hello World!");
}
./mount/rust/src/main.rs
[package]

name = "freetel_usage"
version = "0.0.1"
authors = [ "tady <a.dat.jp@gmail.com>" ]
docker-compose exec rust cargo run

これで、 rust コンテナの中で cargo が実行され、Rustの実行まで行われます。

Rust で HTTP リクエストを行う

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

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

// 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() というメソッドがあるため、これを利用します。

// カスタムリダイレクトポリシー
// ログインリクエスト後に別のページに遷移するのを防ぐため `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 で取得できますが、ヘッダーの型を渡すことで、目的のヘッダを一発で取得することが出来ます。

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つ返してくるので、有効だと思われる2つめの値を利用しています。

環境変数の利用

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

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

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

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

HTML のパース

HTML のパースには、 select というパーサと、正規表現クレートの regexを利用しています。

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 で言うと以下のようなリクエストを受け付けます。

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

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

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 に置いてあります。

Grafana

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

Screen Shot 2017-08-20 at 9.15.27 PM.png

オチ

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

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