OpenCensusでStackdriver Monitoringにカスタム指標を記録する

# TL;DR

- OpenCensus(Stats)でStackdriver monitoringにCustom Metricを送信した

# 概要

エラー調査の記事を書こうとしたのですが、前提とするシステムの構成要素が多いため前半と後半に分割しました。この記事はその前半部分です。
前半は、OpenCensusを利用してGCPのStackdriver Monitoringにpythonアプリケーションの測定値を収集する方法を説明します。
後半では、前半のアプリケーションをGKEに乗せたときに出た次のエラーについて原因と解決方法を記載します。

```
grpc._channel._Rendezvous: <_Rendezvous of RPC that terminated with:
        status = StatusCode.INVALID_ARGUMENT
        details = "One or more TimeSeries could not be written: The set of resource labels is incomplete. Missing labels: (container_name namespace_name).: timeSeries[0-5]"
        debug_error_string = "{"created":"@1568274913.190983719","description":"Error received from peer ipv4:172.217.25.106:443","file":"src/core/lib/surface/call.cc","file_line":1052,"grpc_message":"One or more TimeSeries could not be written: The set of resource labels is incomplete. Missing labels: (container_name namespace_name).: timeSeries[0-5]","grpc_status":3}"
```


# やりたいこと

アプリケーション固有の測定情報を活用したい。そのために以下の技術要素を利用する。

## Monitoring

面倒くさくて後回しにされがちな気がしますが、堅牢なシステムを作る上で監視は重要な要素です。
基本的にはCPUやメモリ, Disk使用量、ネットワークトラフィックの時系列データを蓄積し、グラフ表示したりしきい値を設定してアラートを飛ばしたりします。また従来の用途の他には、SRE的なアプローチを取る場合にもまずはシステムの状態が監視できていないとお話になりませんし、ABテストの結果も監視のデータを元に行います。
Monitoringの関心事は大きく収集・集約・蓄積・利活用に分けられます。

## OpenCensus

OpenCensus is a set of libraries for various languages that allow you to collect application metrics and distributed traces, then transfer the data to a backend of your choice in real time. 
https://opencensus.io/

OpenCensusは測定値をPrometheusなどのバックエンドに送信する処理を様々な言語で実装しているライブラリです。Monitoringのうちの収集・集約を担います。

2019/9現在次の言語がサポートされています。
https://opencensus.io/language-support/

- Go
- Java
- C#
- C++
- Node.js
- Ruby
- Erlang/Elixir
- Python
- PHP

バックエンドとデータをやり取りする部分はExporterとして言語ごとバックエンドごとに実装されます(大変そう)。

ちなみにOpenCensus + OpenTracing => OpenTelemetryになることが発表されていますが、OpenTelemetryとしてのソフトウェアがまだないためしばらくはOpenCensus, OpenTracingを利用することになるようです。

> OpenTelemetry will offer backwards compatibility with existing OpenCensus integrations, and we will continue to make security patches to existing OpenCensus libraries for two years.
https://opencensus.io/


## Stackdriver Monitoring

Stackdriver MonitoringではMonitoringのうち収集・集約(・可視化)する基盤を提供しています。
GCP上のリソースの基本的なMetricは自動で収集されます。
https://cloud.google.com/monitoring/api/metrics_gcp

また、Metric収集のためのAPIを提供しており、GCP metric以外の測定値もstackdriverに収集することができます。
このときmetricは何でもよくて、例えば自分の心拍データとかAndroidからセンサーデータを飛ばすとかテストの点数とか本当に何でもいいです。
https://cloud.google.com/monitoring/custom-metrics/


# 環境

## mac

```
$ sw_vers
ProductName:    Mac OS X
ProductVersion: 10.14.5
BuildVersion:   18F132
```

## python

```
$ python --version
Python 3.7.3
```

# 実装

OpenCensus-pythonのexampleを参考に小さなアプリケーションを作成します。
https://github.com/census-instrumentation/opencensus-python/blob/master/contrib/opencensus-ext-stackdriver/examples/stackdriver.py

```python:main.py
import os
import time
import json
import random
import logging
import google.auth
from opencensus.stats import view as view_module
from opencensus.stats import stats as stats_module
from opencensus.stats import measure as measure_module
from opencensus.stats import aggregation as aggregation_module
from opencensus.ext.stackdriver import stats_exporter as stackdriver


INTERVAL_SECONDS = os.getenv('INTERVAL_SECONDS', 60)
logger = logging.getLogger(__name__)
DEBUG = os.getenv('DEBUG', False)
if DEBUG:
    logging.basicConfig(level=logging.INFO)

def verify_credential():
    if 'GOOGLE_APPLICATION_CREDENTIALS' in os.environ:
        # You can specify the path of a service account key.
        with open(os.getenv('GOOGLE_APPLICATION_CREDENTIALS'), 'r') as f:
            credential = json.load(f)
            project_id = credential.project_id
    else:
        # If there is no clue, get credential by OAuth.
        try:
            _, project_id = google.auth.default()
        except google.auth.exceptions.DefaultCredentialsError:
            raise ValueError("Couldn't find Google Cloud credentials, set the "
                             "project ID with 'gcloud set project'")

    return project_id


def init(project_id):
    # measure defines metricKind and ValueType in stackdriver
    # measurement is data Point in TimeSeries in stackdriver
    measure = measure_module.MeasureInt('metric_name', 'measure description', 's')

    # The stats recorder
    view_manager = stats_module.stats.view_manager
    stats_recorder = stats_module.stats.stats_recorder

    # view is MetricDescriptor in stackdriver
    view = view_module.View(
            'metric_name',
            'metric description',
            [],
            measure,
            aggregation_module.LastValueAggregation())

    # Enable metrics
    # you need credential. use google.auth or set environment variable instead.
    exporter = stackdriver.new_stats_exporter(
        stackdriver.Options(project_id=project_id))
    view_manager.register_exporter(exporter)

    view_manager.register_view(view)
    mmap = stats_recorder.new_measurement_map()

    return mmap, measure


def main():
    project_id = verify_credential()
    mmap, measure = init(project_id)

    while True:
        time.sleep(INTERVAL_SECONDS)

        metric = random.randint(1, 100)

        mmap.measure_int_put(measure, metric)
        mmap.record()

        logger.info('put value {}'.format(str(metric)))


if __name__ == '__main__':
    main()

```

## verify_credential

```python:抜粋
    if 'GOOGLE_APPLICATION_CREDENTIALS' in os.environ:
        # You can specify the path of a service account key.
        with open(os.getenv('GOOGLE_APPLICATION_CREDENTIALS'), 'r') as f:
            credential = json.load(f)
            project_id = credential.project_id
    else:
        # If there is no clue, get credential by OAuth.
        try:
            _, project_id = google.auth.default()
        except google.auth.exceptions.DefaultCredentialsError:
            raise ValueError("Couldn't find Google Cloud credentials, set the "
                             "project ID with 'gcloud set project'")

```

GCPのStackdriver monitoringに書き込みをするため、`roles/monitoring.metricWriter`の権限が必要です。
https://cloud.google.com/monitoring/access-control

アプリケーションでこの権限を利用する方法は4通りありますが、今回はService Account, OAuth 2.0のどちらかを利用することにします。
https://cloud.google.com/docs/authentication/

環境変数`GOOGLE_APPLICATION_CREDENTIALS`にサービスアカウント鍵(jsonファイル)のPathを設定していればサービスアカウントを利用、そうでなければOAuth2.0を利用してユーザ権限でアクセスします。

## init

OpenCensusとStackdriverは異なるので、利用する際にはそれぞれどの概念がどの概念に対応しているのかを意識すると良いです。
https://cloud.google.com/monitoring/custom-metrics/open-census#mapping-models

厳密に1:1の対応にはなっていないですが、およそ次の通りの対応になっています。

| Stackdriver Monitoring | OpenCensus | Description |
|:--:|:--:|:--|
| MetricDescriptor | view | how to collect and aggregate individual measurements |
| LabelDescriptor | tag | contextual information that can be used to filter and group metrics |
| ValueType | measure | metric data to be recorded |
| MetricKind | aggregation | a function applied to data used to summarize it |
| (data) Point | measurement | a data point collected for measure |
| TimeSeries | view data | aggregated data |

## メインループ

```python:抜粋
    while True:
        time.sleep(INTERVAL_SECONDS)

        metric = random.randint(1, 100)

        mmap.measure_int_put(measure, metric)
        mmap.record()
```

メインループで測定値(この場合はランダムな整数)を記録しています。
`mmap.record`で値を記録していますがこのタイミングでStackdriver APIが呼ばれるわけではなく、裏でThreadで一定期間ごとに送信しています。

# 実行

参考実装をこちらのrepositoryに上げています。
https://github.com/ymaki/opencensus-stackdriver-sample

こんな感じで実行してください。

```console
$ git clone https://github.com/ymaki/opencensus-stackdriver-sample
$ cd opencensus-stackdriver-sample
$ pip install -r requirements.txt
$ python main.py
/Users/hoge/playground/opencensus-stackdriver-sample/lib/python3.7/site-packages/google/auth/_default.py:66: UserWarning: Your application has authenticated using end user credentials from Google Cloud SDK. We recommend that most server applications use service accounts instead. If your application continues to use end user credentials from Cloud SDK, you might receive a "quota exceeded" or "API not enabled" error. For more information about service accounts, see https://cloud.google.com/docs/authentication/
  warnings.warn(_CLOUD_SDK_CREDENTIALS_WARNING)
```

OAuth2.0でユーザ認証していることを警告されています。気になる場合はService Accountを作成して鍵情報を適切に渡してください。
また、ログに何も出ないのでちゃんと動いているかわからないという場合は次のようにプログラムを起動してINFOレベルを出力しましょう。

```console
$ DEBUG=True python main.py
```

## 確認

GCP consoleのStackdriver monitoringから値が入っているかを確認します。

<img width="688" alt="スクリーンショット 2019-09-19 14.23.32.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266939/65cb30af-3948-c305-753d-5078677f7caa.png">

良さそう🎉
metric情報は以下のようになり、`custom.googleapis.com/opencensus/`のprefixがつくことがわかります。

```
Metric: custom.googleapis.com/opencensus/metric_name
Description: metric description
Resource type: global
Unit: s     Kind: Gauge     Value type: Int64
```

# 活用

Stackdriver Monitoring自体にデータをグラフで表示する機能やアラートなど基本的な機能が揃っています。
API経由でgrafanaなどの外部ツールで可視化することも可能です。
収集したデータは6週間で消えてしまうため、必要であればBigQueryなどに蓄積すると良いでしょう。 https://cloud.google.com/monitoring/quotas#data_retention_policy

# まとめ

アプリケーション固有の時系列データを収集するために、OpenCensusを利用してStackdriver Monitoringに測定データを送ることができました。