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 在虛擬環境中執行,可以避免這個問題
指令碼啟動命令:python3 csv_to_influxdb.py -h "127.0.0.1" -p 8086 -db "database" -f "fileName _failures.csv" -s "fileName _stats_history.csv" -a "application" -m "measurement"

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


@click.command()
@click.option('--host', '-h', default='127.0.0.1', help="influxdb地址")
@click.option('--port', '-p', default=8086, type=int, help="influxdb埠")
@click.option('--database', '-db', required=True, help="influxdb資料庫名")
@click.option('--m', '-m', required=True, help="influxdb表名 measurement")
@click.option('--fp', '-f', required=True, help="locust的failures_csv_file_path失敗結果檔案路徑")
@click.option('--sp', '-s', required=True, help="locust的status_csv_file_path測試結果檔案路徑")
@click.option('--application', '-a', required=True, help="測試標籤,在資料庫中區分不同的測試計劃")
def run(host, port, database, m, fp, sp, application):
    click.echo('host=%s,port=%s, database=%s, m=%s, fp=%s, sp=%s, application=%s' % (host, port, database, m, fp, sp, application))
    client = InfluxDBClient(host=host, port=port, database=database)
    light = threading.Thread(target=stats_history_to_influxdb, args=(client, m, sp, application))
    light.start()
    car1 = threading.Thread(target=failures_to_influxdb, args=(m, fp, application))
    car1.start()


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(client, measurement, status_csv_file_path, application):
    '''
    讀取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.如果transaction是Aggregated,則額外增加一條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''):
        click.echo('m=%s, fp=%s,  application=%s' % (measurement, status_csv_file_path, application))
        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'
                },
                "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(measurement, failures_csv_file_path, application):
    '''
    讀取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:
        click.echo('m=%s, fp=%s, application=%s' % (measurement, failures_csv_file_path, application))
        try:
            answer_failures = csv.reader(open(failures_csv_file_path, 'r', encoding='UTF-8-sig'))
        except FileNotFoundError:
            time.sleep(5)
            continue
        for index, line in enumerate(answer_failures):
            # 判斷是否是首行和無效的Aggregated
            if index:
                # 取出transaction和count
                name = line[1].replace('/', '_')  # name裡面不能有/,如果有這裡會替換成_
                this_time_count = float(line[3])

                # 格式化Error, 將url和responseMsg,拆分開
                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__':
    run()

相關文章