將 Jenkins Job 的歷史結果整理出視覺化的 Daily Report mail (二)
前一篇文章說明了 Daily Report 的 Jenkins Job 設定,接下來本篇會說明如何實作 daily_report.py
這個 script 會生成像這樣的 PNG 圖表:
首先要安裝 Pandas 及 matplotlib,我們會使用 Pandas 來讀取 csv 檔案並使用相關便捷的功能來簡化/加速資料分析的步驟,並使用 matplotlib 來產生視覺化的 PNG 圖檔。
python -m pip install Pandas
python -m pip install matplotlib
script 相關的 module import 如下:
我們在 Jenkins Job 內設定預設讀取 config\daily_report_demo.prop,當作 Job 參數。
config\daily_report_demo.prop 的內容如下:
MY_JOB_NAME=daily_report_demo
DAILY_REPORT_MAIL_TO=my_account@gmail.com
MY_JOB_NAME 是指定上游 Jenkins Job 的 Job name,意思就是這個 Daily Report 是統整 daily_report_demo 這個 Job 的建置結果。所以接下來:
接下來讀取之前上游 Job 建置時留下的紀錄 (.csv),csv 檔案的內容如下:
BUILD_NUMBER,BUILD_TIMESTAMP,RESULT,BUILD,DOWNLOAD,BOOTUP,REBOOT
1,06:00:00,SUCCESS,SUCCESS,SUCCESS,SUCCESS,SUCCESS
2,07:00:00,FAILURE,SUCCESS,SUCCESS,FAILURE,FAILURE
3,08:00:00,FAILURE,SUCCESS,SUCCESS,FAILURE,FAILURE
4,09:00:00,FAILURE,SUCCESS,SUCCESS,FAILURE,FAILURE
...
15,20:00:00,SUCCESS,SUCCESS,SUCCESS,SUCCESS,SUCCESS
16,21:00:00,SUCCESS,SUCCESS,SUCCESS,SUCCESS,SUCCESS
17,22:00:00,UNSTABLE,SUCCESS,SUCCESS,SUCCESS,SUCCESS
18,23:00:00,SUCCESS,SUCCESS,SUCCESS,SUCCESS,SUCCESS
daily_report_demo Job 共有四個下游 Job,分別是 BUILD,DOWNLOAD,BOOTUP,REBOOT,csv 檔名會以建置日期作命名,ex: 2018-03-20.csv,內容就是每一次建置的結果。
然後,為了在 mail 裡面表列前一天所有建置的結果,會讀取最新的一筆csv紀錄,並轉成環境變數的格式紀錄在 workspace\daily_report_demo\hourly\2018-03-20.csv.prop,方便從 jelly script 中讀取,會在第三篇說明 jelly script 的文章再詳細說明。
HOURLY_DATA_0=BUILD_NUMBER,BUILD_TIMESTAMP,RESULT,BUILD,DOWNLOAD,BOOTUP,REBOOT
HOURLY_DATA_1=1,06:00:00,SUCCESS,SUCCESS,SUCCESS,SUCCESS,SUCCESS
HOURLY_DATA_2=2,07:00:00,FAILURE,SUCCESS,SUCCESS,FAILURE,FAILURE
HOURLY_DATA_3=3,08:00:00,FAILURE,SUCCESS,SUCCESS,FAILURE,FAILURE
...
接下來,用 Pandas 讀取 csv 檔案,read_csv() 會回傳 dataframe。
定義一個 getStatistic(df, column, result),用來回傳 dataframe 中指定欄位及內容的次數及百分比。以下面這行為例,就是計算 RESULT 欄位裡,SUCCESS 的次數及百分比。
說明一下 getStatistic() 的實作,df[colume] 是指向一個 Series,value_counts() 用來回傳這一欄裡 unique value 的次數,然後用 to_numeric() 轉成浮點數並進行百分比計算。
完整的代碼如下:
為了方便理解,所以把不同欄位的資料給定不同的變數名稱。
date_list 是 x軸資料,代表每一天的建置狀態。
s_rate_list 這些是第一個 y軸的資料,代表 SUCCESS/FAILURE/UNSTABLE 的百分比。
build_f_count_list 這些是第二個 y軸的資料,代表下游 job failure 的次數。
這樣我們需要的資料都已經統計完畢,接下來就是使用 matplotlib 生成 PNG 檔案。
重點說明幾個地方,datestr2num() 會將日期字串轉成數字,顯示的時候再用 DateFormatter() 指定我們想要呈現的字串格式。
plot_date() 可以指定 x 軸或 y 軸為日期,預設是 x 軸為日期。
若是統計資料超過14天,內容會變得雜亂,因此超過14天會使用 WeekdayLocator(),並且不顯示 annotation。百分比若是 0% 或 100%,也一律不在圖表上顯示。
接下來處理第二個 y 軸,因為共用 x 時間軸,所以呼叫 twinx()。
第二個 y 軸用來顯示 Failure 次數,因此用柱狀圖表示。
autolabel() 的實作如下,用來計算顯示 annotation 的位置。
接下來儲存成 PNG 檔案
可以看到我們用 report_tag 當作 PNG 檔名的一部份,因此我們把 report_tag 存在 prop 檔案裏面,讓 Jenkins Job inject 成環境變數。
這樣 daily_report.py 就完成了。
這個 script 會生成像這樣的 PNG 圖表:
首先要安裝 Pandas 及 matplotlib,我們會使用 Pandas 來讀取 csv 檔案並使用相關便捷的功能來簡化/加速資料分析的步驟,並使用 matplotlib 來產生視覺化的 PNG 圖檔。
python -m pip install Pandas
python -m pip install matplotlib
script 相關的 module import 如下:
1 2 3 4 5 6 | import os, time, sys, re import numpy as np import pandas as pd import matplotlib.pyplot as plt import matplotlib.dates as mdates from datetime import datetime, date, timedelta |
我們在 Jenkins Job 內設定預設讀取 config\daily_report_demo.prop,當作 Job 參數。
config\daily_report_demo.prop 的內容如下:
MY_JOB_NAME=daily_report_demo
DAILY_REPORT_MAIL_TO=my_account@gmail.com
MY_JOB_NAME 是指定上游 Jenkins Job 的 Job name,意思就是這個 Daily Report 是統整 daily_report_demo 這個 Job 的建置結果。所以接下來:
- 讀取 WORKSPACE 環境變數,這個環境變數是 Jenkins 設定的,用來取得 Jenkins job 的自訂工作區位置,以我們之前的設定為例,工作區是在 D:\workshop
- 讀取 MY_JOB_NAME 環境變數
1 2 3 4 5 6 7 8 9 10 11 12 13 | if __name__ == '__main__': try: env_workspace = os.environ["WORKSPACE"] env_job_name = os.environ["MY_JOB_NAME"] except Exception as e: print ("Error: can not find environment variables") raise e folder = "%s\\workspace\\%s"%(env_workspace, env_job_name) if not os.path.exists(folder): print ("%s not exist!"%folder) sys.exit(1) |
接下來讀取之前上游 Job 建置時留下的紀錄 (.csv),csv 檔案的內容如下:
BUILD_NUMBER,BUILD_TIMESTAMP,RESULT,BUILD,DOWNLOAD,BOOTUP,REBOOT
1,06:00:00,SUCCESS,SUCCESS,SUCCESS,SUCCESS,SUCCESS
2,07:00:00,FAILURE,SUCCESS,SUCCESS,FAILURE,FAILURE
3,08:00:00,FAILURE,SUCCESS,SUCCESS,FAILURE,FAILURE
4,09:00:00,FAILURE,SUCCESS,SUCCESS,FAILURE,FAILURE
...
15,20:00:00,SUCCESS,SUCCESS,SUCCESS,SUCCESS,SUCCESS
16,21:00:00,SUCCESS,SUCCESS,SUCCESS,SUCCESS,SUCCESS
17,22:00:00,UNSTABLE,SUCCESS,SUCCESS,SUCCESS,SUCCESS
18,23:00:00,SUCCESS,SUCCESS,SUCCESS,SUCCESS,SUCCESS
daily_report_demo Job 共有四個下游 Job,分別是 BUILD,DOWNLOAD,BOOTUP,REBOOT,csv 檔名會以建置日期作命名,ex: 2018-03-20.csv,內容就是每一次建置的結果。
然後,為了在 mail 裡面表列前一天所有建置的結果,會讀取最新的一筆csv紀錄,並轉成環境變數的格式紀錄在 workspace\daily_report_demo\hourly\2018-03-20.csv.prop,方便從 jelly script 中讀取,會在第三篇說明 jelly script 的文章再詳細說明。
HOURLY_DATA_0=BUILD_NUMBER,BUILD_TIMESTAMP,RESULT,BUILD,DOWNLOAD,BOOTUP,REBOOT
HOURLY_DATA_1=1,06:00:00,SUCCESS,SUCCESS,SUCCESS,SUCCESS,SUCCESS
HOURLY_DATA_2=2,07:00:00,FAILURE,SUCCESS,SUCCESS,FAILURE,FAILURE
HOURLY_DATA_3=3,08:00:00,FAILURE,SUCCESS,SUCCESS,FAILURE,FAILURE
...
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 | today = datetime.today() report_tag = "" hourly_folder = "%s\\hourly"%(folder) if not os.path.exists(hourly_folder): os.makedirs(hourly_folder) try: dirs = os.listdir(folder) dirs.sort(key=natural_keys) dirs = reversed(dirs) for file in dirs: if not file.endswith("csv"): continue csv_path = "%s\\%s"%(folder, file) # Read a line then write to .prop file for later environment variable injection prop_path = "%s\\%s.prop"%(hourly_folder, file) with open(prop_path, "w") as prop_file: if prop_file != "": with open(csv_path, "r") as csv_file: report_tag = file.strip(".csv") i = 0 for line in csv_file: prop_file.write("HOURLY_DATA_%s=%s"%(str(i), line)) i += 1 # only processing the latest record break except Exception as e: raise e |
接下來,用 Pandas 讀取 csv 檔案,read_csv() 會回傳 dataframe。
1 | df = pd.read_csv(csv_path) |
定義一個 getStatistic(df, column, result),用來回傳 dataframe 中指定欄位及內容的次數及百分比。以下面這行為例,就是計算 RESULT 欄位裡,SUCCESS 的次數及百分比。
1 | rcount, rate = getStatistic(df, 'RESULT', 'SUCCESS') |
說明一下 getStatistic() 的實作,df[colume] 是指向一個 Series,value_counts() 用來回傳這一欄裡 unique value 的次數,然後用 to_numeric() 轉成浮點數並進行百分比計算。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | def getStatistic(df, column, result): total = len(df[column]) # special case, not return 0 but return None if total == 0: return None, None stat = df[column].value_counts() stat = pd.to_numeric(stat, downcast='float') rcount = 0 rate = 0.0 if result in stat: rcount = stat[result] if rcount != 0: rate = rcount/total*100 return rcount, rate |
完整的代碼如下:
為了方便理解,所以把不同欄位的資料給定不同的變數名稱。
date_list 是 x軸資料,代表每一天的建置狀態。
s_rate_list 這些是第一個 y軸的資料,代表 SUCCESS/FAILURE/UNSTABLE 的百分比。
build_f_count_list 這些是第二個 y軸的資料,代表下游 job failure 的次數。
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 | report_days = 14 pattern = "(.*).csv" date = "" date_list = [] s_rate_list = [] f_rate_list = [] u_rate_list = [] build_f_count_list = [] download_f_count_list = [] bootup_f_count_list = [] reboot_f_count_list = [] try: dirs = os.listdir(folder) dirs.sort(key=natural_keys) dirs = reversed(dirs) for file in dirs: if not file.endswith("csv"): continue if report_days != 0: if len(date_list) >= report_days: break csv_path = "%s\\%s"%(folder, file) df = pd.read_csv(csv_path) ## data for first y scale rcount, rate = getStatistic(df, 'RESULT', 'SUCCESS') # no data in this csv file if rcount == None: continue s_rate_list.append(rate) rcount, rate = getStatistic(df, 'RESULT', 'FAILURE') f_rate_list.append(rate) rcount, rate = getStatistic(df, 'RESULT', 'UNSTABLE') u_rate_list.append(rate) ## data for second y scale rcount, tmp = getStatistic(df, 'BUILD', 'FAILURE') build_f_count_list.append(rcount) rcount, tmp = getStatistic(df, 'DOWNLOAD', 'FAILURE') download_f_count_list.append(rcount) rcount, tmp = getStatistic(df, 'BOOTUP', 'FAILURE') bootup_f_count_list.append(rcount) rcount, tmp = getStatistic(df, 'REBOOT', 'FAILURE') reboot_f_count_list.append(rcount) ## date is x scale date_p = re.search(pattern, file) if date_p != None: rdate = date_p.group(1) date_list.append(rdate) print("\n") except Exception as e: raise e |
這樣我們需要的資料都已經統計完畢,接下來就是使用 matplotlib 生成 PNG 檔案。
重點說明幾個地方,datestr2num() 會將日期字串轉成數字,顯示的時候再用 DateFormatter() 指定我們想要呈現的字串格式。
plot_date() 可以指定 x 軸或 y 軸為日期,預設是 x 軸為日期。
若是統計資料超過14天,內容會變得雜亂,因此超過14天會使用 WeekdayLocator(),並且不顯示 annotation。百分比若是 0% 或 100%,也一律不在圖表上顯示。
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 | # use matplotlib to draw and export PNG fig, ax = plt.subplots() # first y scale is success/failure/unstable rate ax.set_xlabel("Date") ax.set_ylabel("Percentage") ax.set_title("Daily Status", y=1.2) ax.set_ylim(-10, 110) plt.subplots_adjust(top=0.8) dates = mdates.datestr2num(date_list) s_rect = ax.plot_date(dates, s_rate_list, color='tab:green', ls='solid', marker='.', lw=2, label='SUCCESS') f_rect = ax.plot_date(dates, f_rate_list, color='tab:red', ls='solid', marker='.', lw=2, label='FAILURE') u_rect = ax.plot_date(dates, u_rate_list, color='tab:orange', ls='solid', marker='.', lw=2, label='UNSTABLE') box = ax.get_position() ax.set_position([box.x0, box.y0 + box.height * 0.2, box.width, box.height * 0.8]) ax.legend((s_rect[0], f_rect[0], u_rect[0]), ('SUCCESS', 'FAILURE', 'UNSTABLE'), bbox_to_anchor=(0., 1.10, 1., .102), ncol=3, loc='upper center', fontsize=7) ax.xaxis.set_major_formatter(mdates.DateFormatter('%m-%d')) isSkipAnno = False if (today - datetime.strptime(date_list[0], '%Y-%m-%d')) > timedelta(days=14): ax.xaxis.set_major_locator(mdates.WeekdayLocator()) isSkipAnno = True else: ax.xaxis.set_major_locator(mdates.DayLocator()) if not isSkipAnno: for i,j in zip(dates, s_rate_list): if j != 0.0 and j != 100.0: ax.annotate("{:.1f}%".format(j), xy=(i, j), fontsize=7) for i,j in zip(dates, f_rate_list): if j != 0.0 and j != 100.0: ax.annotate("{:.1f}%".format(j), xy=(i, j), fontsize=7) for i,j in zip(dates, u_rate_list): if j != 0.0 and j != 100.0: ax.annotate("{:.1f}%".format(j), xy=(i, j), fontsize=7) |
接下來處理第二個 y 軸,因為共用 x 時間軸,所以呼叫 twinx()。
第二個 y 軸用來顯示 Failure 次數,因此用柱狀圖表示。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | # second y scale is the failure counts of upstream jobs rects = [] width = 0.2 ax2 = ax.twinx() ax2.set_ylabel("Failure Counts") ax2.set_ylim(-2, 24) rects.append(ax2.bar(dates-width, build_f_count_list, width)) rects.append(ax2.bar(dates, download_f_count_list, width)) rects.append(ax2.bar(dates+width, bootup_f_count_list, width)) rects.append(ax2.bar(dates+(width*2), reboot_f_count_list, width)) ax2.legend((rects[0][0], rects[1][0], rects[2][0], rects[3][0]), ('Download', 'Build', 'Bootup', 'Reboot'), bbox_to_anchor=(0, 1.02, 1, .102), ncol=4, loc='upper center', fontsize=7) autolabel(ax2, rects[0]) autolabel(ax2, rects[1]) autolabel(ax2, rects[2]) autolabel(ax2, rects[3]) fig.autofmt_xdate() |
autolabel() 的實作如下,用來計算顯示 annotation 的位置。
1 2 3 4 5 6 7 8 9 10 | def autolabel(ax, rects): ''' Attach a text label above each bar displaying its height ''' for rect in rects: height = rect.get_height() if height != 0: ax.text(rect.get_x() + rect.get_width()/2., 1.05*height, '%d' % int(height), ha='center', va='bottom') |
接下來儲存成 PNG 檔案
1 2 3 4 5 6 7 | # save to PNG png_path = "%s\\png\\"%folder if not os.path.exists(png_path): os.makedirs(png_path) png_path = "%s\\status_%s.png"%(png_path, report_tag) plt.savefig("%s"%png_path, dpi=100, format="png") |
可以看到我們用 report_tag 當作 PNG 檔名的一部份,因此我們把 report_tag 存在 prop 檔案裏面,讓 Jenkins Job inject 成環境變數。
1 2 3 4 5 6 7 8 | try: prop_path = "%s\\daily-report.prop"%folder with open(prop_path, "w") as prop_file: if report_tag != "": prop_file.write("REPORT_TAG=%s\n"%report_tag) except Exception as e: raise e sys.exit(1) |
這樣 daily_report.py 就完成了。
留言
張貼留言