Edited at

Stackdriver Monitoring `The set of resource labels is incomplete. Missing labels: (container_name namespace_name).` エラーの調査


TL;DR


  • Monitored Resourceのラベルは必須パラメータ

  • GKE, Opencensus-python, stackdriver monitoring環境で Missing labels: (container_name namespace_name)が出たら環境変数にNAMESPACECONTAINER_NAMEを設定する


概要

前回の記事で、OpenCensusを利用してStackdriver Monitoringに測定値データを送信する簡易なアプリケーションを作成しました。

後半となる本記事では、前半のアプリケーションを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}"


GKE

GKE(Google Kubernetes Engine)はGoogleのManaged Kubernetes Serviceです。

GKEノードそのもののmetricもですが、containerのGCP metricも自動でstackdriverに収集されます。

今回はcontainer内のcustom metricをstackdriver monitoringに送信することを目指します。


アプリケーションの修正

前回作成したアプリケーションをGKE上で動かすよう修正します。


認証

アプリケーションにstackdriver monitoringの書き込み権限(roles/monitoring.metricWriter)を与えるために前回はユーザのOAuth2.0認証を利用していましたが、GKE上ではユーザ認証はできません(というよりはするべきではありません)。

ServiceAccountを作成しても良いのですが、今回はもっと簡単な方法が利用できます。

GCP上で動かすアプリケーションには、GCPのサービス同士の連携を楽にするためデフォルトでサービスアカウントが利用できます。ドキュメント上では4通りの認証手段のうちEnvironment-provided service accountとして説明されています。

https://cloud.google.com/docs/authentication/


If your application runs on Compute Engine, Kubernetes Engine, the App Engine flexible environment, or Cloud Functions, you don't need to create your own service account. Compute Engine includes a default service account that is automatically created for you

https://cloud.google.com/docs/authentication/production#obtaining_credentials_on_compute_engine_kubernetes_engine_app_engine_flexible_environment_and_cloud_functions


このため、GCPインスタンス上であればアプリケーションで特に認証行為をしないよう既存のコードを変更します。以下のコード片ではちょうどopencensusのコードに同じ処理があるのでそれを利用しています。


実行環境がGCPかどうかを判定する

from opencensus.common.monitored_resource.gcp_metadata_config import GcpMetadataConfig

gcp_metadata_config = GcpMetadataConfig()
if gcp_metadata_config.is_running:
project_id = gcp_metadata_config.get_gce_metadata()['project/project-id']



Dockerfile

GKEの上で動かすため、前回のコードを次の様にDockerfileでdocker imageに固めます。


Dockerfile


from python:3.7
USER root

ADD main.py /app/main.py
ADD requirements.txt /app/requirements.txt

WORKDIR /app

RUN pip install -r requirements.txt

ENTRYPOINT ["python", "-u", "main.py"]


これをbuildしてimage registryにuploadしておきます。

(この辺は実際にはgithub -> Google Cloud Build -> Google Container Registryの連携で楽をします。)


Deployment

GKEにDeployするためのPod定義を次のようにしました。


deployment.yaml

apiVersion: apps/v1

kind: Deployment
metadata:
name: opencensus-stackdriver-sample
spec:
replicas: 1
selector:
matchLabels:
app: opencensus-stackdriver-sample
template:
metadata:
labels:
app: opencensus-stackdriver-sample
spec:
containers:
- name: opencensus-stackdriver-sample
image: gcr.io/<YOUR IMAGE REPOSITORY>/opencensus-stackdriver-sample:<HASH>


問題の再現

コードの修正が終わったので、GKE上で動かしてみます。

うまく行けばStackdriver Monitoringにデータが入ってくるはずです。

$ kubectl apply -f deployment.yaml

deployment.apps/opencensus-stackdriver-sample created

Deploymentの設定は成功しましたが、期待して待っていてもデータは送信されず、ログを見ると次のようなエラーが出ています。

$ kubectl logs opencensus-stackdriver-sample-fc6449cf4-8mpm5 opencensus-stackdriver-sample

Error handling metric export
Traceback (most recent call last):
File "/usr/local/lib/python3.7/site-packages/google/api_core/grpc_helpers.py", line 57, in error_remapped_callable
return callable_(*args, **kwargs)
File "/usr/local/lib/python3.7/site-packages/grpc/_channel.py", line 565, in __call__
return _end_unary_response_blocking(state, call, False, None)
File "/usr/local/lib/python3.7/site-packages/grpc/_channel.py", line 467, in _end_unary_response_blocking
raise _Rendezvous(state, None, None, deadline)
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]"
debug_error_string = "{"created":"@1568876145.710983148","description":"Error received from peer ipv4:216.58.197.138: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]","grpc_status":3}"
>

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
File "/usr/local/lib/python3.7/site-packages/opencensus/metrics/transport.py", line 59, in func
return self.func(*aa, **kw)
File "/usr/local/lib/python3.7/site-packages/opencensus/metrics/transport.py", line 113, in export_all
export(itertools.chain(*all_gets))
File "/usr/local/lib/python3.7/site-packages/opencensus/ext/stackdriver/stats_exporter/__init__.py", line 162, in export_metrics
self.client.project_path(self.options.project_id), ts_batch)
File "/usr/local/lib/python3.7/site-packages/google/cloud/monitoring_v3/gapic/metric_service_client.py", line 1024, in create_time_series
request, retry=retry, timeout=timeout, metadata=metadata
File "/usr/local/lib/python3.7/site-packages/google/api_core/gapic_v1/method.py", line 143, in __call__
return wrapped_func(*args, **kwargs)
File "/usr/local/lib/python3.7/site-packages/google/api_core/retry.py", line 273, in retry_wrapped_func
on_error=on_error,
File "/usr/local/lib/python3.7/site-packages/google/api_core/retry.py", line 182, in retry_target
return target()
File "/usr/local/lib/python3.7/site-packages/google/api_core/timeout.py", line 214, in func_with_timeout
return func(*args, **kwargs)
File "/usr/local/lib/python3.7/site-packages/google/api_core/grpc_helpers.py", line 59, in error_remapped_callable
six.raise_from(exceptions.from_grpc_error(exc), exc)
File "<string>", line 3, in raise_from
google.api_core.exceptions.InvalidArgument: 400 One or more TimeSeries could not be written: The set of resource labels is incomplete. Missing labels: (container_name namespace_name).: timeSeries[0]
Error handling metric export
Traceback (most recent call last):
File "/usr/local/lib/python3.7/site-packages/google/api_core/grpc_helpers.py", line 57, in error_remapped_callable
return callable_(*args, **kwargs)
File "/usr/local/lib/python3.7/site-packages/grpc/_channel.py", line 565, in __call__
return _end_unary_response_blocking(state, call, False, None)
File "/usr/local/lib/python3.7/site-packages/grpc/_channel.py", line 467, in _end_unary_response_blocking
raise _Rendezvous(state, None, None, deadline)
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]"
debug_error_string = "{"created":"@1568876205.539466768","description":"Error received from peer ipv4:216.58.197.138: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]","grpc_status":3}"
>

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
File "/usr/local/lib/python3.7/site-packages/opencensus/metrics/transport.py", line 59, in func
return self.func(*aa, **kw)
File "/usr/local/lib/python3.7/site-packages/opencensus/metrics/transport.py", line 113, in export_all
export(itertools.chain(*all_gets))
File "/usr/local/lib/python3.7/site-packages/opencensus/ext/stackdriver/stats_exporter/__init__.py", line 162, in export_metrics
self.client.project_path(self.options.project_id), ts_batch)
File "/usr/local/lib/python3.7/site-packages/google/cloud/monitoring_v3/gapic/metric_service_client.py", line 1024, in create_time_series
request, retry=retry, timeout=timeout, metadata=metadata
File "/usr/local/lib/python3.7/site-packages/google/api_core/gapic_v1/method.py", line 143, in __call__
return wrapped_func(*args, **kwargs)
File "/usr/local/lib/python3.7/site-packages/google/api_core/retry.py", line 273, in retry_wrapped_func
on_error=on_error,
File "/usr/local/lib/python3.7/site-packages/google/api_core/retry.py", line 182, in retry_target
return target()
File "/usr/local/lib/python3.7/site-packages/google/api_core/timeout.py", line 214, in func_with_timeout
return func(*args, **kwargs)
File "/usr/local/lib/python3.7/site-packages/google/api_core/grpc_helpers.py", line 59, in error_remapped_callable
six.raise_from(exceptions.from_grpc_error(exc), exc)
File "<string>", line 3, in raise_from
google.api_core.exceptions.InvalidArgument: 400 One or more TimeSeries could not be written: The set of resource labels is incomplete. Missing labels: (container_name namespace_name).: timeSeries[0]

長いですが、重要な部分は次の箇所です。

要するにStackdriver MonitoringのgRPC APIを叩いたが、INVALID_ARGUMENTが返ってきた、エラーメッセージは The set of resource labels is incomplete. Missing labels: (container_name namespace_name) です。

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]"
debug_error_string = "{"created":"@1568876145.710983148","description":"Error received from peer ipv4:216.58.197.138: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]","grpc_status":3}"


調査

エラーコードやエラーメッセージを検索してもあまり有効な情報が得られなかったので、エラーメッセージを頼りに原因を調べていきます。

エラーメッセージはresource labelsが不完全でcontainer_namenamespace_nameのラベルが足りないと言っています。OpenCensusにはTagという用語はありますがlabelという用語はないため、stackdriverに注目します。


Structure of Metric Types

Structure of Metric Typesに、Stackdriver Monitoringの概念の概要が記載されていますが、これを見ても混乱するので読み飛ばします(私にはわからなかった)

一応上記ページでmonitored resourceの例の中に何の説明もなくlabelsが登場するのですが、同ページ中でlabelといえばmetric labelの説明しかないため予め概念が頭に入っていないと混同する(私はした)


Stackdriver Monitoring gRPC API

エラー箇所のAPIを調べて必要なパラメータを確認します。

  File "/usr/local/lib/python3.7/site-packages/google/cloud/monitoring_v3/gapic/metric_service_client.py", line 1024, in create_time_series

request, retry=retry, timeout=timeout, metadata=metadata

stacktraceからこのあたりCreateTimeSeriesRequestを呼んでエラーが出ていることがわかります。

CreateTimeSeriesRequestは測定したTimeSeriesをPOSTする処理です。パラメータにTimeSeriesのリストを取ります。

TimeSeriesのパラメータは次のようになっていて、これを見ると何が起きたかがだんだんわかってきます。

図

TimeSeriesの中でlabelsは2箇所で登場しますが、どうやらMetricのlabelsとMonitoredResourceのlabelsは別物のようです。


Monitored ResourceとMetric

Monitoring ResourceとMetricについて確認します。

唐突ですが、家の庭に植木鉢Aと植木鉢Bがあり、その温度と湿度を監視したいとします。

これは現実的にはあるデータセンターにあるサーバAとサーバBのCPU使用率や同時接続数のことです。


Monitored Resource

Monitored Resourceはtypeとlabelsから成り、ある一意の監視対象リソースを表します。

植木鉢Aと植木鉢Bがそれぞれ個別のMonitored Resourceです。

Monitored Resourceのtypeは監視対象の種類を表しており、代表的なtypeについては予め定義されています。例えばgce-instanceはCompute Engineのインスタンス、aws_lambda_functionはAWSのlambda functionです。

カスタムなMonitored Resourceを追加することはできません。規定のtypeにうまく当てはまらない監視対象があったら、generic_nodeまたはgeneric_taskを選択します。それらにも当てはまらない場合の最終手段としてglobalが用意されています。

植木鉢タイプが予め設定されてあれば良いのですがそんなものはないので、generic_nodeタイプを利用します。植木鉢Aと植木鉢Bは共にgeneric_nodeタイプとしましょう。

labelは同じtypeのリソース同士を一意に区別するためのパラメータです。

既定typeごとにlabelが予め定められています。例えばgeneric_nodeタイプはproject_id, location, namespace, node_idの4つのラベルでリソースを区別します。

植木鉢Aと植木鉢Bはproject_id, location, namespaceまでは同じで、node_idをAとBとして区別すると良さそうです。

リソース同士を一意に区別する必要があるため、labelは必須パラメータになっています。


Metric

Metricはcpu usagehttp response latencyなどの監視項目(または指標)を表します。

Metricのtypeは監視項目の種類を表します。

植木鉢監視の場合、温度と湿度です。

代表的な項目はGCP Metricsとして予め用意されていますが、任意のカスタム指標を追加することができます。

Metricのlabelは監視項目をフィルタリングするために使うようです。

例えば、湿度Metricに天気というラベルを用意しておくと、晴れの日の湿度と雨の日の湿度のグラフを比較することができます。

Metricのラベルは補助的な情報であるため任意パラメータです。


Stackdriver調査まとめ

今回はエラーメッセージからMonitoredResourceの必須ラベルが足りないものと推測します。

Monitored Resource Typeは、GKE上のコンテナなのでおそらく gke_containerk8s_containerです。エラーメッセージではnamespace_idではなくnamespace_nameが足りないと言っているため k8s_containerの方でしょう。

このタイプのResourceは次のlabelを設定する必要があります。


k8s_container

- project_id: The identifier of the GCP project associated with this resource, such as "my-project".

- location: The physical location of the cluster that contains the container.

- cluster_name: The name of the cluster that the container is running in.

- namespace_name: The name of the namespace that the container is running in.

- pod_name: The name of the pod that the container is running in.

- container_name: The name of the container.

https://cloud.google.com/monitoring/api/resources#tag_k8s_container


この内、container_namenamespace_nameが足りないようです。

逆に言うと、project_id, location, cluster_name, pod_name はAPI callに含まれているようです。誰の仕業でしょう。


OpenCensus-Python

そもそも、サンプルコードではMonitored Resource Typeもラベルも登場しませんでした。

gke_containerと指定した覚えもcluster_namepod_idを設定した覚えもありません。

APIを呼ぶ際にopencensusかgoogle cloudクライアントライブラリあたりでラベルを設定しているのだと思います。気になる箇所の処理を読めるのがOSSの良い点なので追っていきましょう。


Monitored Resource Type判定

Monitored Resourceについてはopencensusのstackdriver拡張のset_monitored_resourceを見れば何をしているかがわかります。

monitored_resource.get_instanceでMonitored Resource typeの判定が行われており、gce_instance, aws_ec2_instance, k8s_containerの判定をして、typeが判明したらそれに対応するlabelを取得します。



k8s_container_typeのmonitored_resourceを設定するところ

if k8s_utils.is_k8s_environment():

resources.append(resource.Resource(
_K8S_CONTAINER, k8s_utils.get_k8s_metadata()))

https://github.com/census-instrumentation/opencensus-python/blob/3f01007a05dcae84cc912a28588d3074fff4f587/opencensus/common/monitored_resource/monitored_resource.py#L51



Monitored Reosurce label取得

更に追っていくとk8s_containerのラベルは2つの観点で集められています。


  1. GCE metadata serverから次のパラメータを取得する


  • PROJECT_ID_KEY: 'project/project-id'

  • INSTANCE_ID_KEY: 'instance/id'

  • ZONE_KEY: 'instance/zone'

  • CLUSTER_NAME_KEY = 'instance/attributes/cluster-name'

https://github.com/census-instrumentation/opencensus-python/blob/3f01007a05dcae84cc912a28588d3074fff4f587/opencensus/common/monitored_resource/gcp_metadata_config.py#L95

GCPにはメタデータサーバという仕組みが用意されており、インスタンスの中からメタデータを取得できるようです。

project/project-id, instance/id, instance/zone, instance/attributes/cluster-nameについてはdefault metadata keys で確認できます。

2.もし次の環境変数が設定されていればその値を取得する


  • CONTAINER_NAME_KEY: 'CONTAINER_NAME'

  • NAMESPACE_NAME_KEY: 'NAMESPACE'

  • POD_NAME_KEY: 'HOSTNAME'

特に上記の環境変数を設定した覚えはないです。

実際に動いているPodを見てみましょう。

$ kubectl exec opencensus-stackdriver-sample-fc6449cf4-nrspv env

PATH=/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
HOSTNAME=opencensus-stackdriver-sample-fc6449cf4-nrspv
(省略)

HOSTNAMEは設定されていますが、NAMESPACECONTAINER_NAMEは設定されていませんでした。

kubernetesのドキュメントには、このあたりの環境変数については次のように書かれています。


The hostname of a Container is the name of the Pod in which the Container is running. It is available through the hostname command or the gethostname function call in libc.

The Pod name and namespace are available as environment variables through the downward API.


環境変数HOSTNAMEを設定しているのはkubernetesではなくbashがgethostname関数を利用して自動で設定しているものと思うので、ベースのイメージによってはHOSTNAMEも無い場合がありそうです。これは別途追ってみるのも面白いかもしれません。


API parameterの設定

取得したMonitored Resourceのtypeとlabelを設定します。

ここでAPI callに利用するkeyと先ほど取得したlabelのkeyを対応させます。


     if resource_type == 'k8s_container':

series.resource.type = 'k8s_container'
set_attribute_label(gcp_metadata_config.PROJECT_ID_KEY, 'project_id')
set_attribute_label(k8s_utils.CLUSTER_NAME_KEY, 'cluster_name')
set_attribute_label(k8s_utils.CONTAINER_NAME_KEY, 'container_name')
set_attribute_label(k8s_utils.NAMESPACE_NAME_KEY, 'namespace_name')
set_attribute_label(k8s_utils.POD_NAME_KEY, 'pod_name')
set_attribute_label(gcp_metadata_config.ZONE_KEY, 'location')

https://github.com/census-instrumentation/opencensus-python/blob/af284a92b80bcbaf5db53e7e0813f96691b4c696/contrib/opencensus-ext-stackdriver/opencensus/ext/stackdriver/stats_exporter/__init__.py#L339



OpenCensus-Python調査まとめ

随分とわかってきました。

APIパラメータのうち、project_id, location, cluster_nameはGCP metadata serverから取得します。GCPがこれらのパラメータはデフォルトで提供してくれています。

container_name, namespace_name, pod_name は環境変数から取得します。pod_nameに対応するHOSTNAMEはkubernetesが勝手に挿入してくれるようですが、CONTAINER_NAMENAMESPACEは勝手には挿入してくれないようです。

この調査結果はエラーメッセージ The set of resource labels is incomplete. Missing labels: (container_name namespace_name) と一致します。


対策


Kubernetes downward API

どうも環境変数を足してあげればうまく動きそうです。

CONTAINER_NAMENAMESPACE

PodやContainerに関するこのあたりのパラメータをcontainerに環境変数として渡せます。

deployment.yamlに次の行を足して、NAMESPACECONTAINER_NAMEを入れてやります。

コンテナ名を動的に環境変数に取得する方法がわからなかった(できない?)ためCONTAINER_NAMEは静的に設定しています。


deployment.yaml

          env:

- name: NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
- name: CONTAINER_NAME
value: "opencensus-stackdriver-sample"


確認

動かしてみましょう。

$ kubectl apply -f deployment.yaml

deployment.apps/opencensus-stackdriver-sample configured

スクリーンショット 2019-10-08 1.31.54.png

Podを起動して暫く待つと、Stackdriver Monitoring Consoleでpodからのmetricが流れてきているのが確認できます。成功しました🎉

monitored resource labelもちゃんと設定できているようです。


まとめ

GKE上のコンテナからOpenCensus-pythonでstackdriverにmetricを送る場合、gRPC APIでエラーが返されます。このエラーを解消するためには環境変数NAMESPACECONTAINER_NAMEを明に設定する必要があります。

流石にそれはライブラリ側で解決してくれーと思ってissueをあげようとしたらこの記事を書いている間にこの事象のissueが上がっていました。🥳

https://github.com/census-instrumentation/opencensus-python/issues/796