InfluxDB + Grafana를 통해 Proxmox Host CPU 온도 모니터링 시각화 및 알림 구축
PVE node 상태를 모니터링해보고 온도 정보에 대해서도 받아보자.
Influx DB 와 Grafana 를 통해서 Proxmox 를 모니터링 할 수 있도록 구성했습니다. 기본적으로 제공하는 데이터 이외에도, CPU 온도에 대한 패널을 추가하고 telegram을 통해 알림을 설정하는 과정까지를 담았습니다.
Proxmox 모니터링하기
우선, InfluxDB + Grafana 를 통해서 Proxmox 모니터링은 아래 사이트를 따라가시면 쉽게 해결하실 수 있습니다.
이렇게 세팅을 하고 나면 초기에는 다음과 같은 뷰를 얻을 수 있습니다.
CPU 온도 모니터링 추가하기
사실 이 정도의 정보로도 모니터링하는데 충분합니다.
그러나, 제 경우에 CPU 온도 / VM 별 CPU | Memory 상태 / Docker container 별 상태를 한 곳에서 보고 싶었습니다.
특히, CPU 온도의 경우 서버를 위한 장비가 아닌 mini PC 를 통해서 작동하고 있는데, ZBOX 의 경우 팬 조차 존재하지 않는 제품이기에 주기적인 모니터링이 필요했으며 , X300 의 경우 소음으로 인해 팬 속도 제한을 걸어둔 상태인데 예상보다 CPU 온도가 너무 치솟을 경우 이 속도를 추후에 조절하고자 모니터링이 필요했습니다.
또한, 해당 방법을 잘 응용하면 CPU 사용량과 온도 그리고 메모리 사용량을 Router로 부터도 수집하여 제 서버를 구성하는 장치들의 상태를 일괄적으로 Grafana를 통해서 확인할 수 있을 것을 기대하고 작업을 시작했습니다.
다행히 시스템의 장치 온도를 얻어서 Influx DB 로 송신하는 툴은 이미 존재합니다. 아래 레포지토리를 가시면 확인하실 수 있습니다.
위 레포와 함께 아래 가이드를 따라하면, 최종적으로 이렇게 작동하게됩니다.
가이드도 잘 작성된 상태라서 해당 가이드를 쭉 따라가시면 CPU 온도를 얻어내실 수 있습니다. (스크립트 자체도 단순합니다.) 다만, 제 경우에 X300 에서 Ryzen 4750G 를 사용하고 있는데 해당 툴에서 정의해둔 규격과는 다소 다릅니다. AMD 의 경우 CPU Core 에 대한 정보, PCH 에 대한 정보들이 따로 존재하지 않습니다. 때문에 일부분 수정해야합니다.
제 경우에는 Core 에 대한 온도는 크게 의미 없으며, 통합적으로 정보를 얻어내기면 하면 되기에 스크립트를 일부 수정했습니다.
스크립트 고치기
pve_temp_stats_to_influxdb2.py
#!/root/scripts/venv/bin/python3
# Note about python shabang string above: if you're running on a Debian 11
# system use the standard "#!/usr/bin/python3" string while if you are on
# a Debian 12 system you need to create a virtual-env with influxdb_client
# and use the shabang string pointing to the python3 interpreter within
# the virtual-env, such as "#!/root/scripts/venv/bin/python3"
#
# For example, to create the virtual-env in /root/scripts/venv and install
# the required package do the following:
#
# python3 -m venv /root/scripts/venv
# . /root/scripts/venv/bin/activate
# pip3 install influxdb-client
# deactivate
import re
import sys
import json
import argparse
from datetime import datetime
from os import getenv
from subprocess import run,PIPE
from influxdb_client import InfluxDBClient
from influxdb_client.client.write_api import SYNCHRONOUS
from influxdb_client.client.exceptions import InfluxDBError
INFLUX_HOST = getenv("INFLUX_HOST")
INFLUX_PORT = getenv("INFLUX_PORT")
INFLUX_TOKEN = getenv("INFLUX_TOKEN")
INFLUX_ORGANIZATION = getenv("INFLUX_ORGANIZATION")
INFLUX_BUCKET = getenv("INFLUX_BUCKET")
HOST = getenv("HOST_TAG")
CORE_OFFSET=2
CPU_TEMP = getenv("CPU_TEMP").split(':')
if __name__ == '__main__':
parser = argparse.ArgumentParser(usage="PVE Sensors Stats to influxdb2 uploader")
parser.add_argument(
"-t",
"--test",
help="Just print the results without uploading to influxdb2",
action="store_true"
)
args = parser.parse_args()
measurements = []
stats = {}
data = json.loads(run(["/usr/bin/sensors -j"], stdout=PIPE, stderr=None, text=True, shell=True).stdout)
if CPU_TEMP[0]:
stats[CPU_TEMP[0]] = int(data[CPU_TEMP[1]][CPU_TEMP[2]][CPU_TEMP[3]])
measurements.append({
"measurement": "temp",
"tags": {"host": HOST, "service": "lm_sensors"},
"fields": stats
})
if args.test:
print(f"\nMeasurements for host {HOST}")
print(json.dumps(measurements, indent=4))
else:
try:
client = InfluxDBClient(url=f"http://{INFLUX_HOST}:{INFLUX_PORT}", token=INFLUX_TOKEN, org=INFLUX_ORGANIZATION, timeout=30000)
write_api = client.write_api(write_options=SYNCHRONOUS)
write_api.write(
INFLUX_BUCKET,
INFLUX_ORGANIZATION,
measurements
)
except TimeoutError as e:
failure = True
print(e,file=sys.stderr)
print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] TimeoutError: Could not upload data to {INFLUX_HOST}:{INFLUX_PORT} for host {HOST}",file=sys.stderr)
exit(-1)
except InfluxDBError as e:
failure = True
print(e,file=sys.stderr)
print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] InfluxDBError: Could not upload data to {INFLUX_HOST}:{INFLUX_PORT} for host {HOST}",file=sys.stderr)
exit(-1)
except Exception as e:
failure = True
print(e, file=sys.stderr)
print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] Connection Error: Could not upload data to {INFLUX_HOST}:{INFLUX_PORT} for host {HOST}",file=sys.stderr)
exit(-1)
client.close()
pve_temp_stats_to_influxdb2.sh
#!/bin/bash
export INFLUX_HOST="influx_IP_or_DNS"
export INFLUX_PORT="influx_PORT"
export INFLUX_ORGANIZATION="influx_organization"
export INFLUX_BUCKET="influx_bucket"
export INFLUX_TOKEN="influx_token"
export HOST_TAG="measurements_host_tag"
export CPU_CORES="6"
# Execute "sensors -j" and then use the information to set the following environment variables.
# In case some of them, like ACPITZ stuff, are not available, set them to ""
# For example if you want the PCH temperature to be stored in a pch field and you see the following
# within "sensors -j" output
# "pch_cannonlake-virtual-0":{
# "Adapter": "Virtual device",
# "temp1":{
# "temp1_input": 61.000
# }
# }
# AMD Ryzen 4750G output
# "k10temp-pci-00c3":{
# "Adapter": "PCI adapter",
# "Tctl":{
# "temp1_input": 62.875
# }
# },
# Set PCH_INFO as the following, by looking at the chain of values that lead to the temperature of PCH
export CPU_TEMP="cpu_temp:k10temp-pci-00c3:Tctl:temp1_input"
# Debian 11 without Python Virtual Environment
# python3 /home/scripts/pve_temp_stats_to_influxdb2.py $*
# Debian 12 with Python Virtual Environment in /path/to/venv
/path/to/venv/bin/python3 /home/scripts/pve_temp_stats_to_influxdb2.py $*
크게 바뀐 부분은 없고, 제게 존재하지 않는 정보 (PCH_INFO
, ACPITZ_INFO
,CORETEMP_NAME
, NVME_INFO
) 를 삭제하고 python 스크립트에서도 마찬가지로 제거했습니다. (nvme 의 경우 획득은 되나, 딱히 제게는 의미가 없어서 제거해두었습니다. 필요하시다면 가이드대로 정보 획득해서 추가해주시면 됩니다.)
Grafana에서 패널 추가하기
이제 InfluxDB 에 데이터가 수신되고 있으니, Grafana에 추가할 차례입니다. 최종적으로 결과는 이렇게 나올겁니다.
Grafana 대시보드에서 Add 를 통해 패널을 하나 추가합니다.
쿼리의 경우 다음과 같이 작성하면 됩니다. (가이드대로 따라하셨다면, r._field
값에 pch
가 필요할 것입니다.)
from(bucket: "${Bucket}")
|> range(start: v.timeRangeStart, stop:v.timeRangeStop)
|> filter(fn: (r) => r["host"] =~ /${server:regex}/)
|> filter(fn: (r) => r._measurement == "temp" and r._field == "cpu_temp")
저는 시계열 데이터를 그래프로 보여주는 Time series 와 Gauge 를 사용하였습니다. 처음 패널을 생성하게 될 경우 Threshold 값이나, 그래프에 대해서는 기본 값으로 설정되어있는데, 적당히 설정해서 사용하시면 됩니다.
같은 방법을 사용하여 아래와 같이 Summary 탭에서 Host 별 CPU도 보여줄 수 있습니다.
해당 패널의 쿼리는 아래와 같이 설정하시면 됩니다. (내부 패널 디자인은 취향에 맞춰 설정하시면 됩니다.
from(bucket: "${Bucket}")
|> range(start: v.timeRangeStart, stop: v.timeRangeStop)
|> filter(fn: (r) => r._measurement == "temp" and r._field == "cpu_temp")
|> filter(fn: (r) => r.host == "${server}")
Grafana 를 통해 Alert 보내기
단순히 모니터링 시각화를 할 뿐만 아니라, 위험 수치에 도달했을 때 alert 를 보내줬으면 하여, grafana 를 통해 alert 시스템까지 한번 구축해보겠습니다.
Telegram 을 통한 Alert
Grafana 의 경우 다양한 방법을 통해 alert 를 보낼 수 있도록 설정할 수 있습니다. 여기서는 telegram 을 통해 설정해보겠습니다.
우선 Telegram 을 Contact point 로 지정해둬야합니다. Contact point 에서 Add contact point 를 누르고 Telegram bot을 선택합니다.
Telegram bot 의 생성의 경우 아래를 참고해주세요.
입력을 완료하면 Test 버튼을 통해서 정상적으로 메시지가 잘 수신되는지 확인합니다.
정상적으로 수신이되면, Notification Policies 로 갑니다. Default Policy 의 경우 Default email 으로 지정이되어있는데, 이를 방금 만든 Telegram bot 으로 contract point를 지정해줍니다.
이후 다시 대시보드로 돌아갑니다. 현재는 CPU 온도에 대한 알림을 구축하고 있으니, 앞서 만든 패널을 누르고 More -> New alert rule
을 통해 새 alert 규칙을 추가합니다. (이렇게 하면 관련 쿼리를 그대로 들고와서 편합니다.)
Rule 을 생성하는 곳에 도달하면 다음과 같은 쿼리를 볼 수 있습니다. 해당 영역에서는 따로 Grafana Dashboard 의 변수를 사용할 수 없어 명확하게 지정되어있는 모습을 볼 수 있습니다.
그런데, 패널에서 들고오면 보통 Host 에 대한 필터링이 있습니다. 이렇게 필터링이 된 쿼리를 사용할 경우 alert rule 을 host마다 각각 만들어야합니다. 따라서 쿼리에서 host를 따로 필터링하는 부분을 제거해주세요. 제 경우 다음과 같이 쿼리를 작성했습니다.
from(bucket: "proxmox")
|> range(start: v.timeRangeStart, stop: v.timeRangeStop)
|> filter(fn: (r) => r._measurement == "temp" and r._field == "cpu_temp")
그 다음에는 alert 를 위한 표현식을 작성하는 부분입니다. A 라는 결과는 쿼리를 통해서 얻은 결과이며 B는 여기서 값을 정하는 과정(평균값, 최대/최소값, 마지막 값) 그리고 C 에서 Threshold를 설정하는 것입니다. (단순하게 잘 만들어놨네요.)
CPU 온도가 일정 이상 오르면 알림이 가도록, Threshold 의 IS ABOVE
값을 변경합니다. 저는 70도를 기준으로 잡았습니다. (테스트를 위해서라면 당장 울릴만한 수치로 한번 설정해보고 추후에 변경하는 것도 좋습니다.)
Evaluation Behavior 에서는 같은 간격마다 alert 에 대한 평가를 진행할 그룹을 만듭니다. 앞서 cron 을 통해 1분마다 결과를 얻어내도록 하였으니, 저는 매 1분마다 온도를 보고하도록 설정했습니다. 이 또한 각자의 생각에 따라 설정하면 됩니다.
pending period 의 경우 Threshold 로 설정된 값이 일정 시간동안 지속되는지 모니터링 후 fire 하도록 합니다. 예를 들어, 5m 으로 잡으면 5분간 70도 이상의 온도가 감지될 경우 메시지가 발송됩니다.
이렇게 계속 threshold 를 넘는지 체크하여 적합한 상황에 fire될 수 있도록 할 수 있습니다. 만약, 즉각적으로 보내고자 한다면 0을 지정하면 됩니다.
설정이 완료되고 테스트를 해보았습니다.
텔레그램으로 알림이 잘 수신되는 것을 볼 수 있습니다.