在Python中将Pod资源使用情况数据从Prometheus导出到CSV


一条备忘录,我试图将数据从IBM Cloud Private中包含的Prometheus用Python导出到csv。
假设您要在一个好的旧系统上创建并报告性能信息报告,例如Excel中每个吊舱的CPU使用率。

已通过ICP v3.1.0确认。由于该命令在Mac上执行,因此curldate的行为在Linux上可能会略有不同。

普罗米修斯查询

本文不介绍Prometheus查询,但是最好看一下下面的链接。

  • 普罗米修斯查询道场

例如,以下查询用于获取命名空间中每个Pod的CPU使用率。

1
sum(rate(container_cpu_usage_seconds_total{namespace="default"}[5m])) by (pod_name) * 100

该查询顺序如下。尽管数据类型和rate()函数令人困惑。

  • container_cpu_usage_seconds_total是容器使用的CPU时间的积分值,此查询返回带有多个容器的时间戳数据(即时矢量类型数据)。

  • 可以通过container_cpu_usage_seconds_total{namespace="default"}这样的标签缩小数据范围(即时矢量类型数据)

  • 通过设置container_cpu_usage_seconds_total{namespace="default"}[5m],可以指定数据周期,并返回跨越多个容器的多个时间戳的数据(范围向量类型数据)。

  • 通过设置rate(container_cpu_usage_seconds_total{namespace="default"}[5m]),可以计算指定时间段内每秒的平均增量(即时矢量类型数据)。

    • 如果CPU使用时间每秒增加0.5秒,则CPU使用率将为0.5(50%)。
  • 使用sum(rate(container_cpu_usage_seconds_total{namespace="default"}[5m])) by (pod_name)为每个Pod汇总多个数据(即时向量类型数据)

    • 由于吊舱中可能有多个容器,因此每个吊舱均使用该总数。

从Grafana

导出

从Grafana仪表板,可以使用GUI以面板单位导出csv。在仪表板上,指定数据周期和根据仪表板设置的变量(在下面的示例中,interval和Namespace)在显示数据之后,单击面板的标题部分。与csv。似乎浏览器上的JavaScript已将已经下载到浏览器的json数据转换为csv。

image.png

导出时可能会进行某些设置。

image.png

curl

接下来,尝试使用curl获取数据。

认证

您需要令牌才能访问Prometheus API。

  • 准备执行组件API命令或管理API命令
  • 普罗米修斯API

请注意,您需要ID令牌,但需要访问令牌而不是ID令牌才能访问Prometheus API。

如果

令牌在本地已经是cloudctl login,则可以使用cloudctl tokens获得。

1
ACCESS_TOKEN=$(LANG=C cloudctl tokens | grep "Access token:" | awk '{print ($4)}')

您还可以通过传递

或用户名和密码从API中获取它。

1
2
3
4
5
USERNAME="admin"
PASSWORD="admin"
ACCESS_TOKEN=$(curl -s -k -H "Content-Type: application/x-www-form-urlencoded;charset=UTF-8" \
  -d "grant_type=password&username=${USERNAME}&password=${PASSWORD}&scope=openid" \
  https://mycluster.icp:8443/idprovider/v1/auth/identitytoken | jq -r '.access_token')

检查查询

通过单击

Grafana面板标题中的"编辑",可以查看正在使用的Prometheus查询。 $interval$namespace从仪表板中定义的可变部分传递。

image.png

您还可以通过打开查询检查器来检查您拥有哪种HTTP请求。

image.png

您需要传递querystartendstep作为GET请求的参数。 startend是纪元时间,step是数据点之间的间隔。

运行卷曲

如下向

curl发出请求。使用-H选项添加用于身份验证的标头。用--data-urlencolde编码数据并添加-G选项以获取数据,而不是POST。

1
2
3
4
5
6
curl -k -s -G -H "Authorization:Bearer $ACCESS_TOKEN" \
  https://mycluster.icp:8443/prometheus/api/v1/query_range \
  --data-urlencode 'query=sum(rate(container_cpu_usage_seconds_total{namespace="default"}[5m])) by (pod_name) * 100' \
  --data-urlencode "start=1547517120" \
  --data-urlencode "end=1547527950" \
  --data-urlencode "step=30" | jq .

如果

参数设置为变量并放在前面,则如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
NAMESPACE="default"
INTERVAL="5m"
QUERY="sum(rate(container_cpu_usage_seconds_total{namespace="${NAMESPACE}"}[${INTERVAL}])) by (pod_name) * 100"
START=$(date -v -1d +%s)  # 1日前の時刻をエポックタイムで取得
END=$(date +%s)           # 今の時刻をエポックタイムで取得
STEP=30
curl -k -s -G -H "Authorization:Bearer $ACCESS_TOKEN" \
  https://mycluster.icp:8443/prometheus/api/v1/query_range \
  --data-urlencode "query=${QUERY}" \
  --data-urlencode "start=${START}" \
  --data-urlencode "end=${END}" \
  --data-urlencode "step=${STEP}" | jq .

执行后,将以json形式返回,如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
$ curl -k -s -G -H "Authorization:Bearer $ACCESS_TOKEN" \
>   https://mycluster.icp:8443/prometheus/api/v1/query_range \
>   --data-urlencode "query=${QUERY}" \
>   --data-urlencode "start=${START}" \
>   --data-urlencode "end=${END}" \
>   --data-urlencode "step=${STEP}" | jq .
{
  "status": "success",
  "data": {
    "resultType": "matrix",
    "result": [
      {
        "metric": {
          "pod_name": "infra-test-nodeport-cust-0"
        },
        "values": [
          [
            1547537972,
            "2.5491993487228775"
          ],
          [
            1547538002,
            "2.300661640484626"
          ],
(省略)

的Python

使用shell脚本使

json csv有点困难,所以让我们尝试使用Python。

代码创建

创建以下过程代码。我仍然不知道如何编写好的代码,所以这个想法是让Python做无聊的事情。

pod_cpu_exporter.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
import argparse
import collections
import csv
import datetime
import logging
import re
import subprocess

import pprint
import requests
import urllib3


formatter = '%(asctime)s %(name)-12s %(levelname)-8s %(message)s'
logging.basicConfig(level=logging.WARNING, format=formatter)
logger = logging.getLogger(__name__)


# 警告を非表示にする
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)


# コマンド引数の処理
parser = argparse.ArgumentParser(description='PodのCPU使用率をcsvに出力します。')
parser.add_argument('-f', '--filename',
                    action='store',
                    type=str,
                    help='出力先のファイル名を指定します')
parser.add_argument('-n', '--namespace',
                    action='store',
                    type=str,
                    default='default',
                    help='Namespaceを指定します')
parser.add_argument('--interval',
                    action='store',
                    type=str,
                    default='5m',
                    help='CPU使用率計算に使用するデータの間隔を指定します(例)1h、5m')
parser.add_argument('--start',
                    action='store',
                    type=str,
                    help='データの開始時間を指定します(例)20190101-1000')
parser.add_argument('--end',
                    action='store',
                    type=str,
                    help='データの終了時間を指定します(例)20190102-1000')
parser.add_argument('--step',
                    action='store',
                    type=int,
                    help='データポイントの間隔(秒)を指定します')
args = parser.parse_args()

filepath = args.filename
namespace = args.namespace
interval = args.interval
step = args.step
logger.debug('filepath: {}'.format(filepath))
logger.debug('interval: {}'.format(interval))
logger.debug('step: {}'.format(step))

# 引数の開始時間と終了時間をUNIX時刻に変換
start_str = args.start
end_str = args.end
start_dt = datetime.datetime.strptime(start_str, '%Y%m%d-%H%M')
end_dt = datetime.datetime.strptime(end_str, '%Y%m%d-%H%M')
start_unix = start_dt.timestamp()
end_unix = end_dt.timestamp()
logger.debug('start_dt: {}'.format(start_dt))
logger.debug('end_dt: {}'.format(end_dt))
logger.debug('start_unix: {}'.format(start_unix))
logger.debug('end_unix: {}'.format(end_unix))

# サブプロセスでコマンドを実行し、結果からアクセストークンを抽出
completed_process = subprocess.run(['cloudctl', 'tokens'], stdout=subprocess.PIPE)
result_str = completed_process.stdout.decode('utf-8')
match = re.search(r'(.*)\s+Bearer\s+(.*)', result_str)
access_token = (match.group(2))
logger.debug('access_token: {}'.format(access_token))

# Prometheusクエリー
# 指定のNamespaceの、指定のintervalで算出したPod毎のCPU使用率を取得する
# sum(rate(container_cpu_usage_seconds_total{namespace="$namespace"}[$interval])) by (pod_name) * 100
query = 'sum(rate(container_cpu_usage_seconds_total{{namespace="{}"}}[{}])) ' \
        'by (pod_name) * 100'.format(namespace, interval)
logger.debug('query: {}'.format(query))

# リクエスト
url = 'https://mycluster.icp:8443/prometheus/api/v1/query_range'
headers = {'Authorization': 'Bearer {}'.format(access_token)}
params = {'query': query,
          'start': start_unix,
          'end': end_unix,
          'step': step}
logger.debug('url: {}'.format(url))
logger.debug('headers: {}'.format(headers))
logger.debug('params: {}'.format(params))


# リクエストを実行
response = requests.get(url, verify=False, headers=headers, params=params)
response.raise_for_status()
logger.debug('response: {}'.format(response))

# レスポンスは以下のようなデータ
# pprint.pprint(response.json())
# {'data': {'result': [{'metric': {'pod_name': 'infra-test-nodeport-cust-0'},
#                       'values': [[1547528400, '2.64939279124293'],
#                                  [1547532000, '2.5820633706497045'],
#                                  [1547535600, '2.562417181158173'],
#                                  [1547539200, '2.4563804665536724'],

# 意味のない部分を取り除いて中のリストを取り出す
results = response.json()['data']['result']

# 取り出したのは以下のようなデータ
# pprint.pprint(results)
# [{'metric': {'pod_name': 'infra-test-nodeport-cust-0'},
#   'values': [[1547528400, '2.64939279124293'],
#              [1547532000, '2.5820633706497045'],
#              [1547535600, '2.562417181158173'],
#              [1547539200, '2.4563804665536724'],
#
# このデータを時刻をキーにして以下のような辞書にまとめる
#
# {1547464889.632: {'infra-test-nodeport-cust-0': '3.1518179124293577',
#                   'infra-test-nodeport-cust-1': '1.530811175762711',
#                   'infra-test-nodeport2-cus-0': '3.0063879859887037',
#                   'infra-test-nodeport2-cus-1': '1.5241500936723127'},
#  1547468489.632: {'infra-test-nodeport-cust-0': '3.161739384943495',
#                   'infra-test-nodeport-cust-1': '1.5393470943785368',
#                   'infra-test-nodeport2-cus-0': '2.8831145322598943',
#                   'infra-test-nodeport2-cus-1': '1.578976048757047'},

# 時刻毎のデータの辞書を用意する
time_series = collections.defaultdict(dict)

# Pod名のSetを用意する
pod_names = set()

for result in results:
    # Pod名を取り出してSetに入れておく
    pod_name = result['metric']['pod_name']
    pod_names.add(pod_name)
    for value in result['values']:
        # timestampを辞書のキーにすることで同じtimestampのデータをまとめる
        # defaultdictを使うことでキーがなくてもKeyErrorにならない
        time_series[value[0]][pod_name] = value[1]

# pprint.pprint(time_series)

# csvのヘッダーは時刻とPod名にする
fieldnames = ['timestamp']
fieldnames.extend(pod_names)

# csvファイルに保存する
with open(filepath, 'w') as csv_file:

    writer = csv.DictWriter(csv_file, fieldnames=fieldnames)
    writer.writeheader()

    # 辞書から時間毎のデータを取り出してループする
    for timestamp, values in time_series.items():
        # 行に時間の列を追加
        row = {'timestamp': datetime.datetime.fromtimestamp(timestamp)}
        # valuesは以下のような辞書
        # {'infra-test-nodeport-cust-0': '2.467225521553685',
        #  'infra-test-nodeport-cust-1': '1.5932590068361583',
        #  'infra-test-nodeport2-cus-0': '2.2811341803954917',
        #  'infra-test-nodeport2-cus-1': '1.6517850743220521'},
        # 事前に格納したPod名のリストの方でループする
        for pod_name in pod_names:
            try:
                row[pod_name] = values[pod_name]
            except KeyError:
                # valuesにこのPodのデータがないときはKeyErrorが発生するので空データを入れる
                row[pod_name] = ''
        writer.writerow(row)

执行示例

request需要模块。

1
pip install requests

由于令牌是通过在

子进程中执行cloudctl token而获得的,因此需要cloudctl命令,并且必须提前登录。

如果执行

,则可以按以下方式获取csv。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
$ python export_pod_cpu.py --help
usage: export_pod_cpu.py [-h] [-f FILENAME] [-n NAMESPACE]
                         [--interval INTERVAL] [--start START] [--end END]
                         [--step STEP]

PodのCPU使用率をcsvに出力します。

optional arguments:
  -h, --help            show this help message and exit
  -f FILENAME, --filename FILENAME
                        出力先のファイル名を指定します
  -n NAMESPACE, --namespace NAMESPACE
                        Namespaceを指定します
  --interval INTERVAL   CPU使用率計算に使用するデータの間隔を指定します(例)1h、5m
  --start START         データの開始時間を指定します(例)20190101-1000
  --end END             データの終了時間を指定します(例)20190102-1000
  --step STEP           データポイントの間隔(秒)を指定します
$ python export_pod_cpu.py -f test.csv -n default --interval 5m --start 20190115-1600 --end 20190116-1600 --step 600
$ cat test.csv
timestamp,infra-test-nodeport-cust-1,infra-test-nodeport2-cus-1,infra-test-nodeport2-cus-0,infra-test-nodeport-cust-0
2019-01-15 16:00:00,1.7765650066665255,1.5227466104164478,2.4642478804166026,2.7171226995831903
2019-01-15 16:10:00,1.4900129837500724,1.4548672362502657,2.3264455262498513,2.7998607379167124
2019-01-15 16:20:00,1.7629794254168016,1.6523557370834396,2.231762218333415,2.040916205000182
2019-01-15 16:30:00,1.87828389291667,1.8159218674998103,1.7364743683333472,2.4042730716670726
2019-01-15 16:40:00,1.6692312762499266,1.7964510670833533,2.264148609108464,2.300661640484626
2019-01-15 16:50:00,1.7293341049999826,1.7319731500000066,1.8862790512499334,2.091235875833111

(省略)

2019-01-16 15:00:00,1.4205530700000204,1.450829464166835,2.229421241249838,2.6200906879167483
2019-01-16 15:10:00,1.207286078750182,1.4032802437501837,1.8004293182339135,2.120460568511992
2019-01-16 15:20:00,1.2547518466667875,1.5286131908334255,1.8340893729164995,2.258688518749826
2019-01-16 15:30:00,1.2122690649999868,1.3929129920832868,2.2814286763049063,2.8766292608645045
2019-01-16 15:40:00,1.4557575473232511,1.2478161367336864,1.8957547083334705,2.535229864166316
2019-01-16 15:50:00,1.0475173487499962,1.2800040254167773,1.750940527916403,2.7946370483334704
2019-01-16 16:00:00,1.4323884024997824,1.3961431675001752,2.2708808758333516,2.626497514166885
$

待办事项

  • 日期和时间是在当地时间指定的,但是使用UTC进行指定可能更方便。
  • 由于我只使用CPU,因此也需要使用内存
  • openpyxl看来您可以使用模块等将其执行到Excel。