Help us understand the problem. What is going on with this article?

GAE Flexible Environmentでアプリケーションのログを出力するためにやったこと。

More than 3 years have passed since last update.

GoogleAppengine Flexible Environmentでのログ出力

皆さん、アプリケーションを作るときにはログはアプリケーションサーバ上のストレージに吐き出してますか〜??

GAE/go (Standard Environment)ではgoogle.golang.org/appengine/logを利用すればよしなにログビューアーに出力してくれ、ログのフィルタなどもかんたんに行なえ、「なんて素晴らしいんだ!!」と感動したことだと思います。

スクリーンショット_2016-12-06_17_44_49.png

しかし、GAE/FEでは何もせずともログビューアに出力してくれるというわけではありませんでした。

色々苦労して期待通りに出力するところまでできたので、いろいろ調査したことを含め、いつか誰かの役に立つかもしれないので細かく書いてみようと思います。

ログ出力作戦(第一世代)

最初に考えたログ出力方法としてはStackDriverLoggingAPI(v2)を自前で叩くという手法。→こちら

こちらの手法を利用した場合、ローカルの開発環境で叩く場合、ステージング環境で叩く場合、本番環境で叩く場合の出力先(API接続先)の振り分けをしなくてはならないことや、1回のリクエストで複数回APIを叩くのはよろしくないため、リクエストスコープで書き込みをバッファしてLoggingAPIを実行する仕組みを作る必要がありそうです。

そんなところ、FE環境ではfluentdが動いており、適切なフォーマットで吐き出したファイルは適切な形でログビューアに出力されるという情報を得て、早速ログ出力作戦は第二世代へ

ログ出力作戦(第二世代)

FE環境にはエージェントがインストールされ、ログ出力する仕組みが用意されている

使用する App Engine のフレキシブル環境アプリケーションを Stackdriver Error Reporting に接続するには、構造化データをカスタム ログファイルに送信します。

App Engine のフレキシブル環境アプリのデフォルト設定により、Stackdriver Logging エージェントがインストールされ、設定されます。

https://cloud.google.com/error-reporting/docs/setup/app-engine-flexible-environment

なるほど、これはありがたい。
情報通り、決められたファイルに決められた形式で出力する事で、よしなに集めますよ。と。

決められたファイルと決められた形式とは・・・?

出力する仕組みがある、ということは分かったが、出力先と出力するJSONの形がわからない・・・

Using Cloud logging in App Engine apps などのドキュメントを参考に進めたが、微妙にうまく動かない。

この辺からドキュメントは諦めて、ソースコードやら環境を地道に調査したほうが速いのでは?という気がしたのでその手法に切り換え。

まずは、Managed VM Sidecar ContainersというFE環境のコンテナ周りの設定のリポジトリを眺めつつ、環境を把握する。

FE環境のインスタンスはssh接続できる。

ssh接続できることに気づくまでは、コードを書く→デプロイする→死ぬほど待つ。という苦行でしたが接続できてしまえばこっちのもの。いろいろ調べてみます。

appengineの「インスタンス」をクリックし、インスタンスの「SSH」をクリックするとブラウザ上でターミナルが起動します。
スクリーンショット_2016-12-06_18_23_01.png

ターミナルで接続した先はFE環境のインスタンスになります。(多分GCEインスタンスなのかな?)
以下コマンドで動いているコンテナを調べてみます。

$ sudo docker ps
CONTAINER ID        IMAGE                                                                                                                         COMMAND                  CREATED             STATUS              PORTS                                        NAMES
61096f77d1a5        gcr.io/google_appengine/nginx-proxy                                                                                           "/var/lib/nginx/bin/s"   About an hour ago   Up About an hour    8080/tcp, 8090/tcp, 0.0.0.0:8443->8443/tcp   thirsty_brattain
3e176f20673a        us.gcr.io/fe-stud/appengine/default.20161206t152342@sha256:330d6b553033c80ad0313fe3c2fd713bdaec873197d00193b6f8a32beda0ca73   "/docker-entrypoint.b"   About an hour ago   Up About an hour    8080/tcp                                     gaeapp
73675d907c83        gcr.io/google_appengine/api-proxy                                                                                             "/proxy"                 About an hour ago   Up About an hour                                                 api
5430f456d28c        gcr.io/google_appengine/fluentd-logger                                                                                        "/opt/google-fluentd/"   About an hour ago   Up About an hour                                                 stupefied_heisenberg

FE環境のインスタンスはDocker上で4つのコンテナが起動しており、共有しているホストインスタンスのディレクトリにgaeapp(アプリケーション)からログを吐き出すとfluentdのコンテナが拾ってログを送るという構成のようです。
スクリーンショット 2016-12-06 18.12.06.png

fluentdなので収集するログファイルは Github内のファイル に定義されており、

/fluentd_logger/managed_vms.conf

以下のようになっています。

ファイルパターン
/var/log/app_engine/app/app*.log
/var/log/app_engine/app/app*.json
/var/log/app_engine/app/request*.log
/var/log/app_engine/app/STDOUT*.log
/var/log/app_engine/app/STDERR*.log
/var/log/app_engine/app/custom_logs/*.log
/var/log/app_engine/app/custom_logs/*.json
/var/log/app_engine/monitoring/*.log
/var/log/syslog

標準で対象になっているのはこのあたりかと。
注意点としてはfluentdのコンテナ内のパスであること!(私はgaeappコンテナ内でも同一のパスになるのではという思い込みのせいで少しハマってました。。。)

どうハマったのか下記します。
ディレクトリパスの対応表

gaeappコンテナがマウントしているパス host instance内のログディレクトリ fluentdコンテナがマウントしているパス
/var/log/app_engine /var/log/app_engine/app /var/log/app_engine/app

気づきましたか?
そうです、ホストインスタンスとfluentdのコンテナのログディレクトリのパスは一致してるのですが、何故かgaeappのログディレクトリのパスが異なっています。。。

なので、fluentdの設定ファイルを見て、そのディレクトリに対してgaeapp内でログファイルを出力してもfluentdが拾ってくれません・・・(涙。

この辺はfluentdのコンテナに入ってログをtailしながら試してみると良い気がします。

fluentdコンテナに入るにはホストインスタンス上で

$ sudo docker exec -it stupefied_heisenberg bash

でコンテナに入り、

# tail -f /var/log/google-fluentd/google-fluentd.log

で眺めることができます。
ちなみにログが正しくfluentdに拾われている場合には

2016-12-06 07:47:25 +0000 [info]: following tail of /var/log/app_engine/app/app.json
2016-12-06 07:54:25 +0000 [info]: following tail of /var/log/app_engine/app/app.log.json

のようなログが出力されます。(ファイルを見つけた最初にのみ出力されるのでそれも注意。)

出力する際に必要なJSON形式とは??

まずはJSON形式じゃないログはどのようにログビューアでどのように出力されるか。

テキストのログは*.logという拡張子のファイルに出力するとテキストとして出力されます。
JSON形式のログと異なるところは、ログレベルなどの情報の付与が行えないという点でしょうか。

app.logを出力した例
スクリーンショット 2016-12-06 18.46.25.png
細長い画像ですみません。。
テキストで出力した場合には(例は中身がJSON形式ですが、)ログレベル(左側のアイコン)もつかず出力されます。

アプリケーションから出力する場合にはやはりログレベルは欲しいです。
その為、出力に必要なJSON形式を下記します。

JSONフォーマットのログを出力し、ログビューアにきれいに出力するには。(完結編?)

JSONのフォーマットには appengine-java-vm-runtime 内の JsonFormatter.javaLogContext.java を利用します。

他の言語で利用する場合にも JsonFormatter.java 内のformatメソッドはそれほど難しい処理をしていないので、そちらを参考にして実装しても良いと思います。

で、結論となるJSON形式のサンプルですが以下のようになります。

{
  "timestamp":{"seconds":1481005920,"nanos":927000000},
  "severity":"ERROR",
  "thread":"qtp1347137144-13",
  "message":"TestLogger: === TEST LOG ==="
}

ちなみに先程のJsonFormatterを利用する場合にはjava.util.logging.LogRecordを利用することになりますが、注意点としてLogRecord#setLoggerNameを利用してロガー名を入れる必要があります。
入れなくてもエラーにはならないのですが、JSONのmessageが"null: === TEST LOG ==="のようになってしまいカッコ悪いので。

ざっくりとしたサンプルコード

Level level = Level.SEVERE; // ログレベル、Error相当
LogRecord record = new LogRecord(level, "=== TEST LOG ===");
record.setLoggerName("TestLogger");
JsonFormatter formatter = new JsonFormatter();
String logString = formatter.format(record); // このStringをjsonファイルの行として出力すれば良い。

ここまで準備できればJSONをファイルに吐き出してきれいにログビューアーに出力することができるようになります。
スクリーンショット_2016-12-06_18_57_25.png
例はエラーレベルで出力してますので、ただ事じゃなさそうなアイコンがついてますね[!!]

Javaでの出力方法 (Appendix)

Scala(Java)で利用する場合にはslf4j/logbackなどを利用して出力したいと思いますが、以下のような方法で出力ができました。

プロジェクトの pom.xmlappengine-java-loggingappengine-managed-runtime を追加する

pom.xml
<!-- https://mvnrepository.com/artifact/com.google.appengine/appengine-java-logging -->
<dependency>
    <groupId>com.google.appengine</groupId>
    <artifactId>appengine-java-logging</artifactId>
    <version>1.9.31</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.google.appengine/appengine-managed-runtime -->
<dependency>
    <groupId>com.google.appengine</groupId>
    <artifactId>appengine-managed-runtime</artifactId>
    <version>1.9.31</version>
</dependency>

アプリケーションの何処かで VmRuntimeFileLogHandler#init を呼ぶ。

これは内部的にログ出力設定を指定しています。

VmRuntimeFileLogHandler.java
// ~~ snip ~~
    private static String fileLogPattern() {
        String pattern = System.getenv("APP_ENGINE_LOG_CONFIG_PATTERN");
        if(pattern != null) {
            return pattern;
        } else {
            String directory = System.getProperty("com.google.apphosting.logs", "/var/log/app_engine");
            pattern = System.getProperty("com.google.apphosting.vmruntime.VmRuntimeFileLogHandler.pattern");
            return pattern != null?(pattern.startsWith("/")?pattern:directory + "/" + pattern):directory + "/" + "app.%g.log.json";
        }
    }
// ~~ snip ~~

指定内容はこのクラスを参考にして、logback.xml などに転記してもいいと思います。その場合には VmRuntimeFileLogHandler#init は呼ばなくて良いです。

ロガーを生成して、ログを出力する

LogSample.java
Logger logger = Logger.getLogger("com");  // ロガー生成

logger.warning("= WARNING TEST ="); // ログ出力

必要に応じて logging.properties を設定します

.level=ALL

com.level=SEVERE

謝辞

FE環境でのログ出力周りに関して、いろいろと情報を頂いたりご相談させていただいた @soundTricker さんに感謝いたします。

fringe81
Fringeは、最新のテクノロジーとプロフェッショナルによるサービスにより、社会課題に仮説を立てて市場に広げていくことで、数十年という長期的なスパンで価値を生み出し続け、より良い世界を創る集団です。 既存の領域に限らず、時流を読み、仮説を生み出し、テクノロジーの力で優れたサービスを生み出し続けます。
https://www.fringe81.com/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away