Grafana監控圖形拉取

杨梅杨梅發表於2024-12-02

python拉取grafana監控圖形

python透過grafana提供的api介面拉取grafana監控圖形並儲存至word文件生成日報傳送郵件

前置條件:

1.grafana平臺需要安裝grafana-image-renderer 外掛,用於生成靜態圖形

頁面可以檢查是否已安裝

未安裝會進入如下頁面:

從API介面拉取圖片會提示:

安裝方式參考:

https://grafana.com/grafana/plugins/grafana-image-renderer/

該外掛對記憶體大小有一定要求:

如果已安裝該外掛,點選Direct link rendered image後會顯示一個靜態監控圖形頁面

參考: https://cloud.tencent.com/document/product/1437/65674

2.在grafana頁面生成api_keys,用於介面請求認證

不同版本獲取方式不同 此示例為V9.2.6版,入口如下:

點選Add API key新增,可以指定有效期,彈出金鑰的介面記得把金鑰複製下來,否則關閉後就看不到了。

程式碼示例:

  • 該示例是將Grafana獲取的多張圖形儲存到本地的panels目錄下(多執行緒執行),
  • 透過python的Image模組進行圖片拼接(拼接方式可自定義),
  • 讀取日誌模版文件(模版中預留了等字元用於定位),
  • 將拼好的圖片插入到日誌模版文件並替換文件中的日期資訊,
  • 儲存生成新的文件,刪除本地panels目錄下的圖片快取,併傳送郵件。
# -*- coding: UTF-8 -*-
import smtplib
import requests
import os
import shutil
import time
from PIL import Image
from docx import Document
from docx.shared import Inches
from datetime import datetime, timedelta
from concurrent.futures import ThreadPoolExecutor, as_completed
from email.header import Header
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from email.header import make_header


# 配置 Grafana API 引數
GRAFANA_HOST = "http://xxxxxxxxxxx"
# Grafana API 金鑰具有時效性,如過期,請聯絡運維人員生成新的api_keys
API_TOKEN = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
# 儀表板ID
DASHBOARD_UID = "xxxxxxxxxx"
# 圖形ID
PANEL_ID_DATA = {"PANEL_1":[30,31,2,10,12,14,16,18,20,22,24,26,28],
                 "PANEL_2":[37, 39, 41, 43, 45, 47, 49, 51, 53],}
ORG_ID = "1"
#日誌模版檔案
document_path = "xxxxxxxx日誌 - 模板.docx"
# 郵箱接收人
RECEIVERS = ["xxxxxx.com", "xxx.com"]
# 儲存圖片的檔名
folder = "panels"

#郵件傳送
def send_mail(filename):
    sender = 'xxxxxxx.com'
    receiver = ','.join(RECEIVERS)
    smtpserver = 'xxxxxx.com'
    user = 'xxxxxx.com'
    password = 'xxxxxxxx'
    mail_title = filename.split('/')[1]
    mail_title = mail_title.split('.')[0]
    # 建立一個帶附件的例項
    message = MIMEMultipart()
    message['From'] = sender
    message['To'] = receiver
    message['Subject'] = Header(mail_title, 'utf-8')

    # 郵件正文內容
    message.attach(MIMEText(f'hello,附件為 {mail_title},請查收,如有問題請與我聯絡。', 'plain', 'utf-8'))
    # 構造附件
    file_msg = MIMEText(open(filename, 'rb').read(), 'base64', 'UTF-8')
    file_msg["Content-Type"] = 'application/octet-stream;name="%s"' % make_header([(filename, 'UTF-8')]).encode('UTF-8')
    file_msg["Content-Disposition"] = 'attachment;filename= "%s"' % make_header([(filename, 'UTF-8')]).encode('UTF-8')
    message.attach(file_msg)
    try:
       
        smtpObj = smtplib.SMTP(smtpserver)
        smtpObj.starttls()
        smtpObj.login(user, password)
        smtpObj.sendmail(sender, receiver, message.as_string())
        print("郵件傳送成功!!!")
    except Exception as e:
        print(e)
        print("郵件傳送失敗!!!")
    finally:
        smtpObj.quit()




def center_insert_img(doc, img,date_str):
    """插入圖片"""
    date_obj = datetime.strptime(date_str, '%Y-%m-%d')
    # 將datetime物件格式化為指定的中文格式字串
    formatted_date = date_obj.strftime('%Y年%m月%d日')
    for paragraph in doc.paragraphs:
        # 根據文件中的佔位符定點陣圖片插入的位置
        if '<datetime>' in paragraph.text:
            paragraph.text = paragraph.text.replace('<datetime>', formatted_date)
        elif '<img1>' in paragraph.text:
            # 把佔位符去掉
            paragraph.text = paragraph.text.replace('<img1>', '')
            # 新增一個文字塊
            run = paragraph.add_run('')
            # 新增一個’回車換行效果‘
            run.add_break()
            # 新增圖片並指定大小
            run.add_picture(img[0], width=Inches(6.2))
        elif '<img2>' in paragraph.text:
            # 把佔位符去掉
            paragraph.text = paragraph.text.replace('<img2>', '')
            # 新增一個文字塊
            run = paragraph.add_run('')
            # 新增一個’回車換行效果‘
            run.add_break()
            # 新增圖片並指定大小
            run.add_picture(img[1], width=Inches(6.2))

def save_img_to_doc(img,day_time):
    """把圖片儲存到doc檔案中的指定位置"""
    tpl_doc = document_path
    current_year = datetime.now().year
    if not os.path.exists(f"{current_year}_doc"):
        os.makedirs(f"{current_year}_doc")
    res_doc = f'{current_year}_doc/日誌報告_{day_time}.docx'
    # 開啟模板檔案
    document = Document(tpl_doc)
    # 插入圖片居中
    center_insert_img(document, img,day_time)
    # 儲存結果檔案
    document.save(res_doc)
    return res_doc

def download_image(render_url, output_path, headers):
    """下載單張圖片的函式"""
    try:
        response = requests.get(render_url, headers=headers, stream=True)
        if response.status_code == 200 and "image/png" in response.headers.get("Content-Type", ""):
            with open(output_path, "wb") as f:
                for chunk in response.iter_content(chunk_size=8192):
                    f.write(chunk)
            return output_path
        else:
            print(f"獲取圖形失敗: {render_url}")
            print(f"狀態碼: {response.status_code}, 返回內容: {response.text}")
            return None
    except Exception as e:
        print(f"下載圖片出錯: {e}")
        return None


def merge_images(image_paths, output_path, layout):
    """合併圖片"""

    images = [Image.open(path) for path in image_paths]
    width, height = images[0].size
    new_image = object
    if layout == "grid_3xN":
        num_rows = (len(images) + 2) // 3
        new_image = Image.new('RGB', (3 * width, num_rows * height))
        for idx, img in enumerate(images):
            x, y = (idx % 3) * width, (idx // 3) * height
            new_image.paste(img, (x, y))
    elif layout == "grid_3x3":
        new_image = Image.new('RGB', (3 * width, 3 * height))
        for idx, img in enumerate(images[:9]):
            x, y = (idx % 3) * width, (idx // 3) * height
            new_image.paste(img, (x, y))

    new_image.save(output_path)
    new_image.close()


def download_grafana_panel():
    # start = time.time()
    time_tuple = generate_daily_timestamps_and_dates()
    image_paths = []
    save_img_to_doc_paths = []
    headers = {"Authorization": f"Bearer {API_TOKEN}"}
    tasks = []

    # 建立下載目錄
    daily_folder = f"{folder}/{time_tuple[0]}"
    os.makedirs(daily_folder, exist_ok=True)

    # 使用執行緒池並行下載圖片 
    # 此處的的URL僅供參考,I6xasdas要替換為你頁面的實際值
    # 也就是你點選Direct link rendered image後顯示的靜態監控圖形頁面的URL
    with ThreadPoolExecutor(max_workers=5) as executor:
        for PANEL_NAME, PANEL_ID_list in PANEL_ID_DATA.items():
            for PANEL_ID in PANEL_ID_list:
                render_url = f'{GRAFANA_HOST}/render/d-solo/I6xasdas/{DASHBOARD_UID}?orgId=1&from={time_tuple[1]}&to={time_tuple[2]}&panelId={PANEL_ID}&width=1000&height=500&tz=Asia%2FShanghai'
                output_path = f"{daily_folder}/{DASHBOARD_UID}_panel_{PANEL_ID}.png"
                tasks.append(executor.submit(download_image, render_url, output_path, headers))

        # 收集任務結果
        for future in as_completed(tasks):
            result = future.result()
            if result:
                image_paths.append(result)

    # 合併圖片 (第一組 3xN)
    if len(image_paths) > 0:
        output_path1 = f"{daily_folder}/panel_01.jpg"
        merge_images(image_paths[:13], output_path1, layout="grid_3xN")
        save_img_to_doc_paths.append(output_path1)

    # 合併圖片 (第二組 3x3)
    if len(image_paths) > 13:
        output_path2 = f"{daily_folder}/panel_02.jpg"
        merge_images(image_paths[12:21], output_path2, layout="grid_3x3")
        save_img_to_doc_paths.append(output_path2)

    # 插入文件儲存
    doc_file_path = save_img_to_doc(save_img_to_doc_paths, time_tuple[0])
    # 刪除快取圖片
    remove_dir(f"{folder}/{time_tuple[0]}")
    #傳送郵件
    send_mail(doc_file_path)
    # end_time = time.time() - start
    # print(f"巡檢報告成功儲存並刪除圖片快取!耗時{round(end_time,2)}秒")



def generate_daily_timestamps_and_dates():
    current_date_time = datetime.now()
    # 提取日期部分並格式化為年-月-日的字串形式
    formatted_date = current_date_time.strftime('%Y-%m-%d')
    # 將輸入的日期字串轉換為datetime物件
    start_date = datetime.strptime(formatted_date, '%Y-%m-%d')
    last_day_start_date = start_date - timedelta(days=1)

    # 獲取當天的起始時間(將時間設定為00:00:00)
    start_of_day = last_day_start_date.replace(hour=0, minute=0, second=0, microsecond=0)
    # 將datetime物件轉換為時間戳(單位:秒)
    timestamp = int(start_of_day.timestamp()*1000)
    date_str = start_of_day.strftime('%Y-%m-%d')

    return (date_str, timestamp, timestamp + 86399*1000)


def remove_dir(directory):
    try:
        shutil.rmtree(directory)
    except OSError as e:
        print(f"刪除目錄時出錯: {e}")
        if e.errno == 32:  # 檔案正在被佔用
            print("檔案被佔用,等待釋放後重試...")
            time.sleep(0.1)  # 等待 0.1 秒後再次嘗試
            try:
                shutil.rmtree(directory)
                print(f"第二次嘗試刪除成功: {directory}")
            except Exception as e2:
                print(f"仍然無法刪除目錄: {e2}")


if __name__ == "__main__":
    download_grafana_panel()

相關文章