🦋

Compute@Edge のデバッグツールとログについて

2021/12/09に公開

Compute@Edge をはじめるにあたって知っておくとよさそうなデバッグツールとログ周りのトピックについて記載します。

ローカルテスト

プロジェクトのルートディレクトリで fastly compute serve を実行するとローカルマシン上でテスト用の HTTP サーバが起動します。Rust の println!() や JS の console.log() の結果がコンソールに表示されるので手軽にプリントデバッグすることができます。

バックエンドサーバの設定

バックエンドリクエストの宛先はバックエンドサーバとして設定されている必要があり、ローカルテストの場合は fastly.toml にサーバの定義を記述します。バックエンドサーバはローカルマシン上で動作する別のサーバでもいいですし、外部のサーバでも構いません。

ローカルテストのバックエンドサーバの定義の例です。

fastly.toml
[local_server]
  [local_server.backends]
    [local_server.backends.backend_a]
      url = "http://127.0.0.1:8080/"
    [local_server.backends.backend_b]
      url = "https://www.example.com/"

ローカルテストではリクエストの Host ヘッダの値は指定しない限り 127.0.0.1:7676 になり、そのままバックエンドに転送するとサーバによってはエラーになります。 override_host を設定するとローカルの HTTP サーバからバックエンドにリクエストを送信するときに Host ヘッダを指定した値で上書きします。

fastly.toml
[local_server]
  [local_server.backends]
    [local_server.backends.backend_a]
      url = "http://127.0.0.1:8080/"
    [local_server.backends.backend_b]
      url = "https://www.example.com/"
+     override_host = "www.example.com"

ディクショナリ

Compute@Edge のコードから Edge Dictionary を参照することができますが、ローカルテストでもローカルのディクショナリを定義して同じコードで参照することができます。

ローカルのディクショナリは JSON ファイルに定義します。

animal_sound.json
{
    "cat": "ニャオ",
    "dog": "ワン"
}

fastly.toml のなかでディクショナリとして JSON ファイルを指定します。

fastly.toml
[local_server]
  [local_server.backends]
    [local_server.backends.backend_a]
      url = "http://127.0.0.1:8080/"
  [local_server.dictionaries]
    [local_server.dictionaries.animal_sound]
      file = "animal_sound.json"
      format = "json"

ディクショナリをルックアップするコードの例です。

Rust

src/main.rs
main.rs
use fastly::{Error, Request, Response, Dictionary};

#[fastly::main]
fn main(_req: Request) -> Result<Response, Error> {
    let dict = Dictionary::open("animal_sound");
    match dict.get("dog") {
        Some(text) => Ok(Response::from_body(text)),
        _ => Ok(Response::from_body("Sound not found"))
    }
}

JavaScript

src/index.js
index.js
addEventListener("fetch", (event) => event.respondWith(handleRequest(event)));

async function handleRequest(event) {
  const dict = new Dictionary("animal_sound");
  const sound = dict.get("cat");
  if (sound) {
    return new Response(sound, {
      status: 200,
    });
  }
  return new Response("Sound not found", {
    status: 200,
  });
}

実行環境によって処理を書き分ける

環境変数 FASTLY_HOSTNAME にはローカルで実行されている場合 localhost がセットされるので実行環境によって処理を書き分けるときに使うことができます。

Rust
let local = std::env::var("FASTLY_HOSTNAME").unwrap_or_default() == "localhost";
if local {
    println!("I'm testing locally!");
}
JavaScript
const local = fastly.env.get("FASTLY_HOSTNAME") === "localhost";
if (local) {
  console.log("I'm testing locally!");
}

ローカルテストの制約

ほとんどの機能をテストすることができるものの、ローカルテストは Fastly で動作するサービスの完全なレプリカではありません。例えばキャッシュの機能はありませんし、Geo ロケーションの情報取得には対応していません。ローカルテストの制約についてはこちらのドキュメントに記載されています。
https://developer.fastly.com/learning/compute/testing/#constraints-and-limitations-1

ライブログテーリング

プロジェクトのルートディレクトリで fastly log-tail を実行すると Fastly にデプロイしたサービスの stdout, stderr へのアウトプットがリアルタイムにコンソールに配信されます。サードパーティのエンドポイントへ送信するように設定されているログはコンソールには配信されません。

ログテーリングは開発中のデバッグツールとして有用ですが、扱えるログのボリュームは限られています。プロダクション環境のログ等、ハイボリュームなログを取得する場合はログストリーミングをご利用ください。

ログテーリングの入力レートやバッファの制限についてはこちらに記載されています。

ログストリーミング

Compute@Edge からサードパーティのログ・ストレージサービスにログを送信することができます。ログの送信先となるエンドポイントの設定と、そのエンドポイントにログを送信するコードの例を紹介します。

ログのエンドポイントの設定

サービスの設定をクローンして編集可能なバージョンを作成し、ログのエンドポイントを設定します。この例では Papertrail を使っています。

fastly service-version clone --version=latest
fastly logging papertrail create --name=my_papertrail --placement=none --address=XXX.papertrailapp.com --port=XXXXX --version=latest

ここでは Fastly CLI を使ってエンドポイントを追加していますが、サービスの管理画面から設定することもできます。

設定に必要な情報は送信先のサービスによって異なります。こちらのリンクから対象のサービスのドキュメントを探してご確認ください。

(参考) fastly logging papertrail コマンドのオプションについて

Flag Example Description
--name my_papertrail エンドポイント名。コードの中でログの送信先を指定するときに使う
--address XXX.papertrailapp.com Papertrail アカウントの Settings > Log Destinations から確認
--port 12345 Papertrail アカウントの Settings > Log Destinations から確認
--placement none Compute@Edge ログエンドポイントの場合は "none"

設定したエンドポイントにログを送信する

設定したエンドポイントにログを送信するコードの例です。

Rust

src/main.rs

log, log-fastlyCargo.toml に追加します。

cargo add log log-fastly
    Updating 'https://github.com/rust-lang/crates.io-index' index
      Adding log v0.4.14 to dependencies
      Adding log-fastly v0.8.0 to dependencies

エンドポイント名を使ってログの送信先を指定します。

main.rs
use fastly::{Error, Request, Response, http::StatusCode};

#[fastly::main]
fn main(req: Request) -> Result<Response, Error> {
    log_fastly::init_simple("my_papertrail", log::LevelFilter::Warn);
    log::warn!("This will be written to my_papertrail...");
    log::info!("...but this won't");

    match req.get_path() {
        "/panic" => panic!("oh no!"),
        _ => Ok(Response::from_status(StatusCode::OK))
    }
}

パニックしたときのログを同じエンドポイントに送信したい場合はこのようにします。

main.rs
use fastly::{Error, Request, Response, http::StatusCode};

#[fastly::main]
fn main(req: Request) -> Result<Response, Error> {
    log_fastly::init_simple("my_papertrail", log::LevelFilter::Warn);
    log::warn!("This will be written to my_papertrail...");
    log::info!("...but this won't");
+   fastly::log::set_panic_endpoint("my_papertrail").unwrap();

    match req.get_path() {
        "/panic" => panic!("oh no!"),
        _ => Ok(Response::from_status(StatusCode::OK))
    }
}

JavaScript

src/index.js

エンドポイント名を使ってログの送信先を指定します。

index.js
addEventListener("fetch", (event) => event.respondWith(handleRequest(event)));

async function handleRequest(event) {
  const logger = fastly.getLogger("my_papertrail");
  logger.log("Hello, Papertrail");

  return new Response("", {
    status: 200,
  });
}

ログすると有用かもしれない情報

環境変数

Name Example Description
FASTLY_CACHE_GENERATION 1 Purge all を実行するとカウントアップする
FASTLY_CUSTOMER_ID 1mXdot9pM8uPxdKyAHF4BG
FASTLY_HOSTNAME cache-hnd18735 リクエストを処理したサーバのホスト名。ローカルテスト時は "localhost"
FASTLY_POP HND
FASTLY_REGION Asia
FASTLY_SERVICE_ID 59cnYbyxkqxmqwdpxfj7Te
FASTLY_SERVICE_VERSION 3
FASTLY_TRACE_ID f840590299c64e8485a2747b7f12fdf2 クライアントリクエスト毎に割り振られる UUID

環境変数の値を取得するコードの例。

Rust
let hostname = std::env::var("FASTLY_HOSTNAME").unwrap_or_default();
JavaScript
const hostname = fastly.env.get("FASTLY_HOSTNAME");

IPアドレス

クライアント IP を取得するコードの例

Rust
let ip = req.get_client_ip_addr().unwrap();
JavaScript
const ip = event.client.address;

IP をマスクして匿名化するコードの例。
https://developer.fastly.com/solutions/examples/anonymize-client-ip-for-logging

Geolocation

取得できる情報は SDK のドキュメントから確認できます。

Geo 情報を取得するコードの例。ローカル実行時は Geo 情報をルックアップしないように条件をつけています。

Rust

この例では JSON 形式でログを出すことにします。 serde_jsonCargo.toml に追加します。

cargo add serde_json
    Updating 'https://github.com/rust-lang/crates.io-index' index
      Adding serde_json v1.0.72 to dependencies
main.rs
use fastly::{Error, Request, Response, http::StatusCode};

#[fastly::main]
fn main(req: Request) -> Result<Response, Error> {
    let local = std::env::var("FASTLY_HOSTNAME").unwrap_or_default() == "localhost";
    if !local {
        let client_ip = req.get_client_ip_addr().unwrap();
        let geo = fastly::geo::geo_lookup(client_ip).unwrap();

        let client_data = serde_json::json!({
            "ip": client_ip,
            "as": geo.as_number(),
            "city": geo.city(),
            "country_code": geo.country_code(),
            "conn_type": geo.conn_type(),
        });

        println!("{}", client_data);
    }

    Ok(Response::from_status(StatusCode::OK))
}
JavaScript
index.js
addEventListener("fetch", (event) => event.respondWith(handleRequest(event)));

async function handleRequest(event) {
  const local = fastly.env.get("FASTLY_HOSTNAME") === "localhost";
  if (!local) {
    const ip = event.client.address;
    const geo = event.client.geo;

    const clientData = JSON.stringify({
      ip,
      "as": geo.as_number,
      "city": geo.city,
      "country_code": geo.country_code,
      "conn_type": geo.conn_type,
    });

    console.log(clientData);
  }

  return new Response("", {
    status: 200,
  });
}

Discussion