locust+influxdb+grafana 實現自定義測試報告

y發表於2020-10-29

locust本身提供的測試報告勉強可以用,但是也有很多不方便的地方,於是自己搭建一個測試報告,下面是測試報告的效果圖


下面是locust輸出的測試結果,可以看到和圖形報告展示的資料是一致的

下面是搭建過程,(省去了locust和grafana安裝過程,安裝比較簡單,可自行百度)

一 locust測試指令碼,注意事項

a.配置寫入csv檔案時間間隔,在測試類中增加
locust.stats.CSV_STATS_INTERVAL_SEC = 5  # 配置寫入csv檔案頻率為5秒,預設是2
b.啟動命令
locust -f test_answer.py -u 50 --csv=fileName --csv-full-history --headless -t10m
--csv=fileName中的fileName是生成檔名的字首

二 influxDb配置

a.啟動influxdb

後臺啟動:nohup influxd -config /etc/influxdb/influxdb.conf &

b. 進入influxdb資料庫

進入終端:influx -host 127.0.0.1 -port 8096 可以知道埠和ip,不不指定預設是本機8086埠

如果需要修改埠,在/etc/influxdb/influxdb.conf修改埠

c.建立資料庫
create database databaseName
show databases
d.進入資料庫
use databaseName
e.檢視錶
show measurements

三 grafana配置

a.配置資料來源

按圖配置就可以了,最下面還有個Min time interval 填5 不要忘了

b. 配置儀表模板

官方模板地址:https://grafana.com/grafana/dashboards
我們選5496模板,點選lode下載
然後選擇資料來源等配置點選 import按鈕

四 將locust測試資料寫入influxDB

需要注意的是influxdb的包和locust的包不相容,建議influxdb在虛擬環境中執行,可以避免這個問題
下面是寫入指令碼只需要填寫自己的influxdb地址和locust生成的csv檔名就可以了
client = InfluxDBClient(host='127.0.0.1', port=8086, database='database_name')

measurement = 'measurement_name'

failures_csv_file_path = '_failures.csv'

status_csv_file_path = '
_stats_history.csv'

application = 'application_name'

import subprocess, copy, time, threading, csv
from datetime import datetime
from influxdb import InfluxDBClient

client = InfluxDBClient(host='127.0.0.1', port=8086, database='database_name')

measurement = 'measurement_name'
failures_csv_file_path = '*_failures.csv'
status_csv_file_path = '*_stats_history.csv'
application = 'application_name'

lock = threading.Lock()
body = [] # 測試資料容器
stats_history_count_dict = {} # 請求總數的容器
stats_history_failures_count_dict = {} # 請求明細中的失敗總數容器
failures_count_dict = {}
'''
failures_count_dict使用來存放失敗總數的容器,由於locust寫入的*_failures.csv檔案是覆蓋寫入的,所以這個檔案使用csv庫讀取,每隔5秒讀取一次
locust同一個請求的同一個失敗的失敗數是累計的,所以這裡將每個錯誤資訊分別儲存,後面計算本5秒鐘每一個api的每一個失敗的失敗數
failures_count_dict = {
'request_name': { # 介面層
'response_msg': { # responsemsg
'url': 1.0
}

}
}
'''


def body_append(value):
'''
body裡面寫值
:param value:
:return:
'''
lock.acquire() # 加鎖
global body
body.append(value)
lock.release() # 釋放鎖


def body_clear():
'''
清空body
:return:
'''
lock.acquire() # 加鎖
global body
body.clear()
lock.release() # 釋放鎖


def stats_history_to_influxdb():
'''
讀取locust測試結果並寫入influxdb
1.使用tail讀取stats_history.csv檔案,正因為使用了tail命令,所以該指令碼只能在linux環境下執行
2.判斷是否是首行,是則跳過
3.csv檔案取出請求總數,減去容器中存放的上次請求總數,然後將新值賦值給stats_history_count_dict容器,如果count是負數則說明容器裡面有上一次的測試資料,那麼直接存csv檔案裡面的count,並將容器清空
4.csv檔案取出失敗數,減去容器中存放的上次失敗數,然後將新值賦值給stats_history_failures_count_dict容器,如果count是負數則說明容器裡面有上一次的測試資料,那麼直接存csv檔案裡面的count,並將容器清空
5.將處理好的資料存入body
a.如果transactionAggregated,則額外增加一條transaction等於internal的資料
b.如果failures_count > 0,則額外增加一條statut等於ko的資料
6.最後將body存入influxdb
:return:
'''
p = subprocess.Popen('tail -Fn 0 %s' % status_csv_file_path, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
for line in iter(p.stdout.readline, b''):
line = line.rstrip().decode('utf8')
data_list = line.split(',')
# 判斷是否是首行和無效的Aggregated
if data_list[0][1:-1].isnumeric() and data_list[7].isnumeric():
name = data_list[3][1:-1].replace('/', '_')
# 判斷容器裡面有沒有count,有就用本次的count減去上次的count
this_time_count = float(data_list[18])
if stats_history_count_dict.get(name):
count = this_time_count - stats_history_count_dict.get(name)
# count小於0說明是重新開始測試,需要清空count_dict
if count < 0:
count = this_time_count
stats_history_count_dict.clear()
else:
count = this_time_count
# count容器賦值
stats_history_count_dict.update({name: this_time_count})

# 判斷容器裡面有沒有failures_count,有就用本次的count減去上次的count
this_time_failures_count = float(data_list[19])
failures_count = 0.0
if this_time_failures_count > 0: # 如果有error資料就走下面的處理邏輯
if stats_history_failures_count_dict.get(name):
failures_count = this_time_failures_count - stats_history_failures_count_dict.get(name)
# failures_count小於0說明重新開始測試,需要清空stats_history_failures_count_dict
if failures_count < 0:
failures_count = this_time_failures_count
stats_history_failures_count_dict.clear()
else:
failures_count = this_time_failures_count
# stats_history_failures_count_dict容器賦值
stats_history_failures_count_dict.update({name: this_time_failures_count})

test_data = {
"measurement": measurement,
"time": int(data_list[0][1:-1]) * 1000000000,
"tags": {
"application": application,
"responseCode": None,
"responseMessage": None,
"statut": 'ok' if name != 'Aggregated' else 'all',
"transaction": name if name != 'Aggregated' else 'all' # transaction 就是locust Apiname
},
"fields": {
'avg': float(data_list[21]),
'count': count,
'countError': failures_count,
'endedT': float(data_list[1][1:-1]),
'hit': float(data_list[18]),
'max': float(data_list[23]),
'maxAT': float(data_list[1][1:-1]),
'meanAT': None,
'min': float(data_list[22]),
'minAT': None,
'pct90.0': float(data_list[10]),
'pct95.0': float(data_list[11]),
'pct99.0': float(data_list[13]),
'rb': None,
'sb': float(data_list[24]),
'startedT': float(data_list[1][1:-1])
}
}
# 封裝測試結果
body.append(test_data)

test_data_detailed = copy.deepcopy(body)[len(body) - 1]

# 判斷是否是Aggregated合計資料,如果是就插入一條internal資料,然後寫入influxdb
if name == 'Aggregated':
# 封裝internal,一組資料的最後一個,直接寫入influxdb,並清空body
test_data_detailed['tags']['transaction'] = 'internal'
test_data_detailed['tags']['statut'] = None
test_data_detailed['fields']['avg'] = None
test_data_detailed['fields']['count'] = None
test_data_detailed['fields']['hit'] = None
test_data_detailed['fields']['max'] = None
test_data_detailed['fields']['pct90.0'] = None
test_data_detailed['fields']['pct95.0'] = None
test_data_detailed['fields']['pct99.0'] = None
test_data_detailed['fields']['sb'] = None
body.append(test_data_detailed)
client.write_points(body)
body.clear()
else: # 封裝單個請求的5秒彙總資料
test_data_detailed['tags']['statut'] = 'all'
body.append(test_data_detailed)

if name != 'Aggregated' and failures_count > 0:
# 寫入error資料
test_data_error = copy.deepcopy(test_data_detailed)
test_data_error['tags']['statut'] = 'ko'
test_data_error['fields']['count'] = failures_count
body.append(test_data_error)


def failures_to_influxdb():
'''
讀取locust斷言失敗的結果,並寫入influxdb
1.清除*_failures.csv中的資料,不清楚的話一開始測試就會直接把上次測試的結果寫入資料庫
2.開始讀取*_failures.csv,第一行是表頭,直接if index跳過
3.csv檔案取出失敗數,減去容器中存放的上次失敗數,然後將新值賦值給容器,如果count是負數則說明容器裡面有上一次的測試資料,那麼直接存csv檔案裡面的count,並將容器清空
4.將處理好的資料存入body,等待stats_history_to_influxdb方法中寫入influxdb
:return:
'''
try:
open('answer_failures.csv', "r+").truncate() # 清空檔案
except FileNotFoundError:
print("沒有檔案!")
while True:
answer_failures = csv.reader(open(failures_csv_file_path, 'r', encoding='UTF-8-sig'))
for index, line in enumerate(answer_failures):
# 判斷是否是首行和無效的Aggregated
if index:
# 取出transactioncount
name = line[1].replace('/', '_') # name裡面不能有/,如果有這裡會替換成_
this_time_count = float(line[3])

# 格式化Error, urlresponseMsg,拆分開
error_list = line[2].split(' response ')

# 算出每一次的count
if failures_count_dict.get(name, {}).get(error_list[1], {}).get(error_list[0], None):
count = this_time_count - failures_count_dict.get(name, {}).get(error_list[1], {}).get(error_list[0], None)
else:
count = this_time_count

# 判斷是否有新增的error
if count > 0:
# count容器賦值
# failures_count_dict.update({name: {error_list[1]: {error_list[0]: this_time_count, 'count': count}}})
if failures_count_dict.get(name):
if failures_count_dict.get(name, {}).get(error_list[1], {}):
if failures_count_dict.get(name, {}).get(error_list[1], {}).get(error_list[0], None):
failures_count_dict[name][error_list[1]][error_list[0]] = this_time_count
else:
failures_count_dict[name][error_list[1]].update({error_list[0]: this_time_count})
else:
failures_count_dict[name].update({error_list[1]: {error_list[0]: this_time_count}})
else:
failures_count_dict.update({name: {error_list[1]: {error_list[0]: this_time_count}}})

test_data = {
"measurement": measurement,
"time": datetime.utcnow().isoformat("T"),
"tags": {
"statut": error_list[0], # url存放在status欄位
"application": application,
"responseCode": "Assertion failed",
"responseMessage": error_list[1],
"transaction": name
},
"fields": {
'count': count,
}
}
# 封裝測試結果
body.append(test_data)
elif count < 0:
failures_count_dict.clear()
break
time.sleep(5)


if __name__ == '__main__':
light = threading.Thread(target=stats_history_to_influxdb, )
light.start()

car1 = threading.Thread(target=failures_to_influxdb, )
car1.start()
分享就到這裡,你學會了嗎?

相關文章