目录
三、通过home assistant配置Prometheus数采集
为了追求办公效率与体验,提升公司整体数字化与智能化水平,将会对办公区设备等进行改造。
一、需求实例
改造需求一:夏日提前开启会议室空调
1.技术实现
(1)新建一个wifi网络,供IOT设备使用
(2)调用企业微信会议室预订接口获取到会议室预订信息,拿到meetingroom_id等参数
(3)0.5版本硬编码,包含cmdb_code、cmdb_name、home_assistant_switch_entity_id、home_assistant_climate_entity_id、home_assistant_sensor_entity_id、meetingroom_id、meeting_name等参数
(4)封装获取空调开关状态函数,通过home assistant控制空调的开关
(5)封装获取当前温度函数
(6)封装根据当前温度调整到合适的温度函数
(7)会议室提前3分钟开启空调,3分钟内判断booking_id,如果存在则不会继续轮询
(8)每晚10点固定时间执行一次关闭空调操作
2.项目设计
3.项目代码
在确定了上述设计方案后,小组正式立项,采购相关设备,码代码。
项目代码结构如上,具体代码如下:
(1).env文件内容:
CORP_ID = '企业ID'
SECRET = '应用的secret'
HA_API_URL = "你的HomeAssistant地址:8123"
# 长寿命访问令牌
HA_ACCESS_TOKEN = "your long token"
(2)cmdb_dict.py文件内容:
from logger import logger
cmdb_data = [{
"cmdb_code": "HZZGC_AC_15F_ROOMB_1",
"cmdb_name": "15-东区-南-B会议室",
"Home_Assistant_SwitchEntityID": ["switch.qdhkl_ac_1111_switch_status"],
"Home_Assistant_ClimateEntityID": ["climate.qdhkl_ac_1111_air_conditioner"],
"Home_Assistant_SensorEntityID": [""],
"meetingroom_id": 1,
"meetingroom_name": "F15-B"
},
{
"cmdb_code": "HZZGC_AC_15F_ROOMC_1",
"cmdb_name": "15-东区-C会议室",
"Home_Assistant_SwitchEntityID": ["switch.qdhkl_ac_2222_switch_status"],
"Home_Assistant_ClimateEntityID": ["climate.qdhkl_ac_2222_air_conditioner"],
"Home_Assistant_SensorEntityID": [""],
"meetingroom_id": 2,
"meetingroom_name": "F15-C"
},
]
# 创建一个空字典来存储转换后的数据
cmdb_dict = {}
# 清理数据,移除缺少'meetingroom_id'的数据项,并尝试将cmdb_data转换为字典形式
for room in cmdb_data:
if "meetingroom_id" in room:
try:
cmdb_dict[room["meetingroom_id"]] = room
except Exception as e:
logger.warning(f"Error processing room: {room}. Error: {e}")
else:
logger.warning(f"Skipped room without 'meetingroom_id': {room}")
# 定义通用查找函数
def find_by_meetingroom_id(meetingroom_id, key):
"""根据会议室ID查找指定键的值"""
room_data = cmdb_dict.get(meetingroom_id)
if room_data:
return room_data.get(key)
return None
def find_switch_entity_id_by_meetingroom_id(meetingroom_id):
return find_by_meetingroom_id(meetingroom_id, "Home_Assistant_SwitchEntityID") or []
def find_climate_entity_id_by_meetingroom_id(meetingroom_id):
return find_by_meetingroom_id(meetingroom_id, "Home_Assistant_ClimateEntityID") or []
def find_sensor_entity_id_by_meetingroom_id(meetingroom_id):
return find_by_meetingroom_id(meetingroom_id, "Home_Assistant_SensorEntityID") or []
def find_meetingroom_name_by_meetingroom_id(meetingroom_id):
return find_by_meetingroom_id(meetingroom_id, "meetingroom_name")
(3)home_assistant.py文件内容:
#pip install pytest-asyncio
#pip install colorlog
import asyncio
import os
import datetime
from dotenv import load_dotenv
from meeting_rooms import get_meeting_room_bookings,get_access_token
from cmdb_dict import cmdb_dict,find_meetingroom_name_by_meetingroom_id,find_switch_entity_id_by_meetingroom_id,find_climate_entity_id_by_meetingroom_id, find_sensor_entity_id_by_meetingroom_id
import requests
from logger import logger
from work_dict import work_data
load_dotenv() # 加载环境变量
# Home Assistant 配置
HA_ACCESS_TOKEN = os.getenv('HA_ACCESS_TOKEN')
HA_API_URL = os.getenv('HA_API_URL')
# 设置认证信息
headers = {
"Authorization": f"Bearer {HA_ACCESS_TOKEN}",
"Content-Type": "application/json"
}
# 初始化标志字典
booking_flags = {}
# 获取空调开关状态
async def get_entity_state_http(switch_entity_id):
try:
url = f"{HA_API_URL}/api/states/{switch_entity_id}"
response = requests.get(url, headers=headers)
if response.status_code == 200:
state = response.json()
if state is not None:
return state.get("state")
else:
logger.error(f"Failed to get state of entity {switch_entity_id}. Response: {state}")
return None
else:
logger.error(f"Failed to get state of entity {switch_entity_id}. Status code: {response.status_code}")
return None
except Exception as e:
logger.error(f"An error occurred while getting entity state via HTTP: {e}")
return None
await asyncio.sleep(1)
# 控制设备开关
async def control_device(switch_entity_id, action):
# 根据实体ID分割出 domain
domain = switch_entity_id.split('.')[0]
# 根据动作选择服务名
service = "turn_on" if action == "on" else "turn_off"
url = f"{HA_API_URL}/api/services/{domain}/{service}"
payload = {
"entity_id": switch_entity_id
}
try:
response = requests.post(url, json=payload, headers=headers)
if response.status_code == 200:
logger.info(f"Controlled device {switch_entity_id} to {action}.")
else:
logger.error(f"Failed to control device {switch_entity_id}. Status code: {response.status_code}")
except Exception as e:
logger.error(f"An error occurred while controlling device {switch_entity_id}: {e}")
await asyncio.sleep(1)
# 通过Home assistant获取到当前温度
async def get_current_temperature(climate_entity_id):
url = f"{HA_API_URL}/api/states/{climate_entity_id}"
try:
response = requests.get(url, headers=headers)
response.raise_for_status()
data = response.json()
current_temperature = data.get('attributes', {}).get('current_temperature')
if current_temperature is not None:
logger.info(f"Current temperature for {climate_entity_id}: {current_temperature}")
return current_temperature
else:
logger.warning(f"Current temperature not found for {climate_entity_id}")
return None
except requests.exceptions.RequestException as e:
logger.error(f"Error fetching current temperature for {climate_entity_id}: {e}")
return None
await asyncio.sleep(1)
# 调整空调温度
async def adjust_ac_temperature(climate_entity_id, current_temperature):
current_state = await get_entity_state_http(climate_entity_id) # 使用await
if current_state is None:
logger.error(f"Failed to get state of AC {climate_entity_id}.")
return
if current_temperature < 15:
target_temperature = 25 # 如果室外温度较低,将室内温度设置为25度
elif current_temperature > 30:
target_temperature = 25 # 如果室外温度较高,将室内温度设置为25度
else:
target_temperature = 25 # 在适中温度范围内,将室内温度设置为25度
url = f"{HA_API_URL}/api/services/climate/set_temperature"
data = {
"entity_id": climate_entity_id,
"temperature": target_temperature
}
try:
response = requests.post(url, headers=headers, json=data)
if response.status_code == 200:
logger.info(f"Adjusted AC {climate_entity_id} to {target_temperature}°C in cool mode.")
return target_temperature
else:
logger.error(f"Failed to adjust AC {climate_entity_id}. Status code: {response.status_code}")
except Exception as e:
logger.error(f"An error occurred while adjusting AC {climate_entity_id}: {e}")
# 遍历所有会议室的信息,并调用控制逻辑
async def iterate_meeting_rooms(access_token: str):
bookings = get_meeting_room_bookings(access_token)
logger.info("Bookings: %s", bookings)
for booking in bookings:
meetingroom_id = booking.get('meetingroom_id')
meeting_name = find_meetingroom_name_by_meetingroom_id(meetingroom_id)
if meetingroom_id not in cmdb_dict:
logger.warning(f"Meeting Room ID {meetingroom_id} not found in CMDB data.")
continue
schedules = booking.get('schedule', [])
if not schedules:
logger.info(f"No schedules found for Meeting Room ID {meetingroom_id}, {meeting_name}.")
continue
for schedule in schedules:
yield (meetingroom_id, meeting_name, schedule)
async def control_logic(meetingroom_id: str, meeting_name: str, schedule: dict, switch_entity_ids: list[str], climate_entity_ids: list[str], current_timestamp: int, previous_end_time: int) -> int:
start_time = schedule['start_time']
end_time = schedule['end_time']
three_minutes_before = start_time - 180
booking_id = schedule['booking_id']
# 打印预订信息
logger.info(f"Meeting Room ID: {meetingroom_id}, Meeting Name: {meeting_name}, Start Time: {datetime.datetime.fromtimestamp(start_time)}, End Time: {datetime.datetime.fromtimestamp(end_time)}")
tasks = []
for switch_entity_id, climate_entity_id in zip(switch_entity_ids, climate_entity_ids):
# 如果实体 ID 为空,则跳过该实体
if not switch_entity_id or not climate_entity_id:
logger.warning(f"Skipping invalid entity IDs: Switch ID={switch_entity_id}, Climate ID={climate_entity_id}")
continue
ac_state = await get_entity_state_http(switch_entity_id)
if ac_state is None:
logger.error(f"Failed to get state of AC in Room {meetingroom_id}, {meeting_name}.")
continue
# 获取外部温度
current_temperature = await get_current_temperature(climate_entity_id)
tasks.append(asyncio.create_task(control_ac(switch_entity_id, climate_entity_id, ac_state, current_temperature, start_time, end_time, three_minutes_before, current_timestamp, previous_end_time, meetingroom_id, meeting_name, booking_id )))
if tasks:
await asyncio.gather(*tasks)
return end_time
# 控制空调逻辑
async def control_ac(switch_entity_id, climate_entity_id, ac_state, current_temperature, start_time, end_time, three_minutes_before, current_timestamp, previous_end_time, meetingroom_id, meeting_name, booking_id):
global booking_flags
# 清理过期的标志
now = datetime.datetime.now().timestamp()
expired_flags = [booking_id for booking_id, timestamp in booking_flags.items() if now - timestamp > 3 * 60] # 3分钟过期
for booking_id in expired_flags:
del booking_flags[booking_id]
if three_minutes_before <= current_timestamp <= start_time:
# 在会议开始前3分钟内,如果空调关闭,则开启空调
if ac_state == 'off' and booking_id not in booking_flags:
await control_device(switch_entity_id, "on")
logger.info(f"Turned on AC in Room {meetingroom_id, meeting_name} before the meeting starts at {datetime.datetime.fromtimestamp(start_time)}.")
await adjust_ac_temperature(climate_entity_id, current_temperature)
# 设置标志
booking_flags[booking_id] = datetime.datetime.now().timestamp()
else:
logger.info(f"AC in Room {meetingroom_id, meeting_name} is already on.")
elif start_time < current_timestamp < end_time:
# 如果当前时间在会议期间
if ac_state == 'on':
logger.info(f"Meeting ongoing in Room {meetingroom_id}, {meeting_name}. Air conditioner is already on.")
else:
logger.warning(f"Meeting ongoing in Room {meetingroom_id}, {meeting_name}, but Air conditioner is off or not found.")
elif previous_end_time is not None and previous_end_time < current_timestamp < start_time:
# 如果当前时间在上一个预订时间段结束和下一个预订时间段开始之间
# 并且下一个会议马上就要开始,则不需要关闭空调
if three_minutes_before <= current_timestamp:
logger.info(f"Meeting is about to start in Room {meetingroom_id}, {meeting_name}. No need to turn off AC.")
#await adjust_ac_temperature(climate_entity_id, current_temperature)
# 关闭空调的逻辑后续靠人在传感器
# else:
# # 在关闭空调之前检查当前空调的状态
# if ac_state == 'on':
# await control_device(switch_entity_id, "off")
# logger.info(f"Turned off AC in Room {meetingroom_id, meeting_name} after the previous meeting ended.")
# else:
# logger.info(f"AC in Room {meetingroom_id, meeting_name} is already off.")
elif current_timestamp >= end_time:
# 如果当前时间已经超过预订结束时间
logger.info(f"Meeting in Room {meetingroom_id}, {meeting_name} has ended.")
# 检查和控制空调
async def check_and_control_air_conditioner():
previous_end_time = None # 初始化上一个预订时间段的结束时间
while True:
access_token = get_access_token()
if not access_token:
logger.error("Failed to get access token, skipping...")
return
current_timestamp = int(datetime.datetime.now().timestamp())
# 检查当前时间是否为晚上10点
if datetime.datetime.now().hour == 22 and datetime.datetime.now().minute == 0:
# 关闭所有会议室的空调
for meetingroom_id in cmdb_dict.keys():
switch_entity_id = find_switch_entity_id_by_meetingroom_id(meetingroom_id)
if switch_entity_id:
for single_entity_id in switch_entity_id:
await control_device(single_entity_id, 'off')
logger.info(f"Turned off AC in Room {meetingroom_id} at {datetime.datetime.now()}.")
# 关闭所有会议室的空调,排除机房
for work in work_data:
if work["work_name"] != "15-机房":
for entity_id in work["Home_Assistant_SwitchEntityID"]:
await control_device(entity_id, 'off')
logger.info(f"Turned off AC in Room {work['work_name']} at {datetime.datetime.now()}.")
async for meetingroom_id, meeting_name, schedule in iterate_meeting_rooms(access_token):
switch_entity_ids = find_switch_entity_id_by_meetingroom_id(meetingroom_id)
climate_entity_ids = find_climate_entity_id_by_meetingroom_id(meetingroom_id)
#sensor_entity_ids = find_sensor_entity_id_by_meetingroom_id(meetingroom_id)
if not (switch_entity_ids and climate_entity_ids):
logger.warning(
f"No valid Home Assistant Entity IDs found for Room {meetingroom_id}, {meeting_name}. Skipping...")
continue
# 调用控制逻辑函数,并更新上一个预订时间段的结束时间
try:
previous_end_time = await control_logic(meetingroom_id, meeting_name, schedule, switch_entity_ids, climate_entity_ids, current_timestamp, previous_end_time)
except Exception as e:
logger.error(f"Error during control logic for Room {meetingroom_id}: {e}")
await asyncio.sleep(5) # 休眠5秒后再次检查
(4)logger.py文件内容:
import logging
import colorlog
import sys
from logging.handlers import TimedRotatingFileHandler
from datetime import datetime
import os
# 配置日志
logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)
# 创建 TimedRotatingFileHandler 处理器
file_handler = TimedRotatingFileHandler(
filename="run_ac.log",
when="midnight",
interval=1,
backupCount=7 # 保留最近7个备份
)
file_handler.setLevel(logging.INFO)
file_formatter = logging.Formatter('%(asctime)s [%(levelname)s]: %(message)s', datefmt='%Y-%m-%d %H:%M:%S')
file_handler.setFormatter(file_formatter)
# 移除默认的 StreamHandler
for hdlr in logger.handlers[:]:
if isinstance(hdlr, logging.StreamHandler):
logger.removeHandler(hdlr)
# 为控制台输出添加颜色
console_handler = logging.StreamHandler(sys.stdout)
console_handler.setLevel(logging.DEBUG)
console_formatter = colorlog.ColoredFormatter(
"%(log_color)s%(asctime)s [%(levelname)s]: %(message)s",
datefmt=None,
reset=True,
log_colors={
'DEBUG': 'cyan',
'INFO': 'green',
'WARNING': 'yellow',
'ERROR': 'red',
'CRITICAL': 'red,bg_white',
},
secondary_log_colors={},
style='%'
)
console_handler.setFormatter(console_formatter)
# 将 handler 添加到 logger
logger.addHandler(file_handler)
logger.addHandler(console_handler)
# 清理过期的日志文件
def clean_old_logs(log_file, max_days=7):
log_dir = os.path.dirname(log_file)
if not os.path.exists(log_dir):
return
now = datetime.now()
for file_name in os.listdir(log_dir):
if file_name.startswith("run_ac.log.") and not file_name.endswith(".gz"):
try:
file_date_str = file_name.split(".", 1)[1]
file_date = datetime.strptime(file_date_str, "%Y%m%d")
if (now - file_date).days > max_days:
file_path = os.path.join(log_dir, file_name)
os.remove(file_path)
except ValueError:
continue
# 清理旧日志
clean_old_logs("run_ac.log")
(5)meeting_rooms.py文件内容:
# __*__coding:utf-8__*__
# Author: Alice Li
import os
from dotenv import load_dotenv
import requests
from logger import logger
load_dotenv() # 加载环境变量
# 企业微信配置
CORP_ID = os.getenv('CORP_ID')
SECRET = os.getenv('SECRET')
WECHAT_API_URL = "https://qyapi.weixin.qq.com/cgi-bin/"
# 获取Access Token
def get_access_token():
# 获取Access Token的URL
access_token_url = f"{WECHAT_API_URL}gettoken?corpid={CORP_ID}&corpsecret={SECRET}"
try:
# 发送请求获取Access Token
response = requests.get(access_token_url)
if response.status_code == 200:
return response.json().get('access_token')
else:
logger.error(f"Failed to get access token: {response.text}")
return None
except Exception as e:
logger.error(f"Error occurred while fetching access token: {e}")
return None
# 获取会议室预定情况
def get_meeting_room_bookings(access_token):
# 获取会议室预定情况的URL
booking_info_url = f"{WECHAT_API_URL}oa/meetingroom/get_booking_info?access_token={access_token}"
try:
# 发送请求获取会议室预定情况
response = requests.post(booking_info_url)
if response.status_code == 200:
data = response.json()
logger.info("get_meeting_room_bookings 的返回值: %s", data)
if 'errcode' in data and data['errcode'] != 0:
logger.error(f"Failed to fetch booking info: {data['errcode']} - {data['errmsg']}")
return []
# 返回会议室预订列表
return data.get('booking_list', [])
else:
logger.error(f"Failed to fetch booking info: {response.text}")
return []
except Exception as e:
logger.error(f"Error occurred while fetching booking info: {e}")
return []
(6)work_dict.py文件内容:
# __*__coding:utf-8__*__
# Author: Alice Li
# 工作区数据
work_data = [{
"work_name": "15-茶水间-西-1排-东",
"Home_Assistant_SwitchEntityID": ["switch.qdhkl_ac_3333_switch_status"],
},
{
"work_name": "15-机房",
"Home_Assistant_SwitchEntityID": ["switch.qdhkl_ac_4444_switch_status"],
},
]
(7)main.py文件内容:
# __*__coding:utf-8__*__
# Author: Alice Li
from home_assistant import check_and_control_air_conditioner
import asyncio
async def main():
# 启动会议室逻辑任务
asyncio.create_task(check_and_control_air_conditioner())
# 运行主循环,保持任务持续运行
while True:
await asyncio.sleep(1)
if __name__ == "__main__":
asyncio.run(main())
(8)requirements.txt文件内容:
aiohappyeyeballs==2.3.5
aiohttp==3.10.3
aiosignal==1.3.1
allure-pytest==2.13.2
allure-python-commons==2.13.2
amqp==5.2.0
APScheduler==3.10.4
async==0.6.2
async-timeout==4.0.3
asyncio==3.4.3
attrs==23.1.0
bidict==0.23.1
billiard==4.2.0
blinker==1.8.2
celery==5.4.0
certifi==2023.7.22
cffi==1.16.0
charset-normalizer==3.3.0
click==8.1.7
click-didyoumean==0.3.1
click-plugins==1.1.1
click-repl==0.3.0
colorama==0.4.6
contourpy==1.2.1
cryptography==41.0.5
cycler==0.12.1
exceptiongroup==1.1.3
Flask==3.0.3
Flask-SocketIO==5.3.6
Flask-SQLAlchemy==3.1.1
fonttools==4.53.1
frozenlist==1.4.1
gevent==24.2.1
greenlet==3.0.3
gTTS==2.5.2
h11==0.14.0
holidays==0.54
idna==3.4
iniconfig==2.0.0
itsdangerous==2.2.0
Jinja2==3.1.4
kiwisolver==1.4.5
kombu==5.4.0
logger==1.4
loguru==0.7.2
MarkupSafe==2.1.5
multidict==6.0.5
numpy==2.0.1
outcome==1.2.0
packaging==23.2
pillow==10.4.0
pluggy==1.3.0
prometheus_client==0.20.0
prompt_toolkit==3.0.47
pycparser==2.21
pygame==2.6.0
PyMySQL==1.1.0
pyOpenSSL==23.3.0
pyparsing==3.1.2
PySocks==1.7.1
pytest==7.4.2
pytest-rerunfailures==13.0
python-dateutil==2.9.0.post0
python-dotenv==1.0.1
python-engineio==4.9.1
python-socketio==5.11.3
pytz==2024.1
PyYAML==6.0.1
redis==5.0.8
requests==2.31.0
requests-toolbelt==1.0.0
selenium==4.13.0
simple-websocket==1.0.0
six==1.16.0
sniffio==1.3.0
sortedcontainers==2.4.0
SQLAlchemy==2.0.32
tomli==2.0.1
trio==0.22.2
trio-websocket==0.11.1
typing_extensions==4.12.2
tzdata==2024.1
tzlocal==5.2
urllib3==2.0.6
vine==5.1.0
wcwidth==0.2.13
websocket==0.2.1
websocket-client==1.8.0
websockets==12.0
Werkzeug==3.0.3
win32-setctime==1.1.0
wsproto==1.2.0
xlrd==2.0.1
yarl==1.9.4
zope.event==5.0
zope.interface==7.0.1
二、镜像管理与部署
本地代码运行通过后,逻辑没有问题情况下,通过Dockerfile打成镜像文件,推送到镜像库,通过rancher管理和运行K8S
Dockerfile文件内容:
# 使用官方 Python 运行时作为父镜像
FROM python:3.10-slim
# 设置工作目录
WORKDIR /terra-iot/first_ac_project
# 复制当前目录下的所有内容到容器中的 /terra-iot/first_ac_project 目录下
COPY first_ac_project/* ./
# 设置镜像源
ENV PIP_INDEX_URL https://pypi.tuna.tsinghua.edu.cn/simple/
# 安装依赖
RUN pip install --no-cache-dir -r /terra-iot/first_ac_project/requirements.txt
# 设置环境变量
COPY first_ac_project/.env ./.env
# 设置时区
RUN ln -snf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && echo 'Asia/Shanghai' > /etc/timezone
# 暴露端口
EXPOSE 8080
# 运行应用
CMD ["python", "./main.py"]
打镜像:
docker build -t 公司镜像库地址/terra_iot:0.5-20240911 .
推镜像:
docker push 公司镜像地址/terra_iot:0.5-20240911
可直接登录rancher的可视化界面创建deployments
可查看日志以及打开终端等,由上我们的项目已经部署到k8s上并运行了。
三、通过home assistant配置Prometheus数采集
步骤一:home assistant所在服务器配置文件中开启普罗米修斯数据采集,修改configuration.yaml 文件内容
# Loads default set of integrations. Do not remove.
default_config:
# Load frontend themes from the themes folder
frontend:
themes: !include_dir_merge_named themes
automation: !include automations.yaml
script: !include scripts.yaml
scene: !include scenes.yaml
prometheus:
namespace: homeassistant
filter:
include_domains:
- switch
- sensor
- climate
修改后重启服务
docker-compose down
docker-compose up -d
步骤二:prometheus所在服务器配置文件中配置home assistant相关信息,修改prometheus.yml文件内容
scrape_configs:
- job_name: "homeassistant" # 任务名称
scrape_interval: 60s # 设置此任务的抓取间隔时间
metrics_path: /api/prometheus # 指定Home Assistant的Prometheus端点路径
authorization:
credentials: "your long token"
scheme: http
static_configs:
- targets: ['home assistant所在的服务器IP地址:8123']
同样的,修改后需要重启服务
docker-compose down
docker-compose up -d
重启服务后,可以在n9e(采集Prometheus数据)上搜到普罗米修斯数据哦~
上述采集的数据有2个不正常的。
1.metric乱码
解决方法:检查配置没问题后,怀疑是编码格式不一致导致的
果然从上述结果来看,果然是字符编码格式不统一导致的,统一为utf-8
如何修改编码格式为utf-8呢,最简单的方式用notepad++,修改编码方式为:UTF-8-BOM
然后重启服务就可以了~
2. 时间戳展示数据不正确
修改展示Unit为原数据格式就好了
四、Grafana大盘展示
上述采集了空调数据,考虑采用之前做的普罗米修斯数据采集方式进行监控并通过grafana展示。
grafana上增加仪表板和配置相应的promQL语句。
第一层:
第二层:
具体的grafana参数实现和设置等这里就不再详细讲解了,可以关注之前的文章了解学习哦~
同时采集告警数据如下: