OpenStack计量服务-Ceilometer
Ceilometer(计量服务)简介
Ceilometer属于OpenStack项目中的一个组件,是在Telemetry服务基础上发展而来,主要用作收集、监控和处理OpenStack资源的指标数据。它可以收集虚拟机、卷、网络等资源的性能指标,如CPU利用率、内存使用量、网络流量等。这些指标数据可以用于监控和分析OpenStack环境的性能、资源利用情况以及故障排除。
Ceilometer通过与OpenStack其他组件集成,如Nova(计算服务)、Cinder(块存储服务)、Neutron(网络服务)等,能够获取各个组件的指标数据,并将其存储在数据库中供后续查询和分析。
Ceilometer还提供了报警和事件处理机制,可以根据设定的阈值或规则触发报警并采取相应的操作。这样,管理员可以根据指标数据的变化情况及时采取措施,维护和优化OpenStack环境。
Ceilometer架构
数据采集
Ceilometer有两个核心服务,也是它采集数据的两种方式:
polling agent:
Compute agent (ceilometer-agent-compute)运行在每个 compute 节点上,以轮询的方式通过调用 Image 的 driver 来获取资源使用统计数据。
Central agent (ceilometer-agent-central)运行在 management server 上,以轮询的方式通过调用 OpenStack 各个组件(包括 Nova、Cinder、Glance、Neutron、Swift 等)的 API 收集资源使用统计数据。
notification agent: 运行在一个或者多个management server上的数据收集程序,监听消息队列上的通知,将他们转换成时间和样本、应用管道操作。核心是通知守护进程(agent-notification),它监视消息队列中其他 OpenStack 组件(例如 Nova、Glance、Cinder、Neutron、Swift、Keystone 和 Heat)发送的数据以及 Ceilometer 内部通信。

对于采集的数据类型也有两种:
Sample(样本):Sample是指标数据的基本单位,用于表示一次指标的测量值。每个Sample包含了指标的名称、值、时间戳、资源标识符等信息。Ceilometer通过收集来自不同OpenStack组件的指标数据,生成一系列的Sample。这些Sample可以表示CPU利用率、内存使用量、网络流量等不同类型的指标数据。Sample通常与时间序列数据库(如Gnocchi)一起使用,用于存储和查询指标数据。
Event(事件):Event用于表示OpenStack环境中的发生的事件。事件可以是资源的创建、修改、删除,或者其他与资源状态变化相关的操作。每个Event包含了事件的类型、资源标识符、时间戳、事件数据等信息。Ceilometer的Panko组件负责收集和存储这些事件数据,并与指标数据进行关联。通过事件数据,管理员可以了解资源的创建、状态变化等操作,以便进行监控、审计和故障排除等工作。
数据处理
Pipelines是Ceilometer中数据处理的机制,描述了一组sources(数据来源)和sinks(数据分发)之间的关系,Metes 数据依次经过(零个或者多个) Transformer 和 (一个或者多个)Publisher 处理,最后达到(一个或者多个)Receiver。其中Recivers 包括 Ceilometer Collector 和 外部系统。
对于采集到的数据Ceilometer可以通过多个pipelines以多个形式进行发布,该功能由notification agents完成。
以下是常见的可以用来发布数据的方式:
gnocchi:发布samples/events到Gnocchi Api中。
notifier:基于通知的发布器,将sample推送到可由外部系统使用的消息队列。
http/https:通过REST API发布数据。
file:发布数据到指定地址和名字的文件中。
Gnocchi是一个开源的时间序列数据库项目,专门用于存储和分析大规模的时间序列数据。它最初作为OpenStack项目的一部分,用于存储和查询Ceilometer(Telemetry)收集的指标数据,但现在已经成为一个独立的项目,可以与多个数据源和应用程序集成。
Ceilometer命令
Ceilometer实验
作业与实验环境
超星网址
虚拟机openstack-allinone,账户root,密码000000
VmWare需要修改网络设置,在编辑->虚拟网络编辑器中将Vmnet1网卡的子网由原来的
192.168.10.0改为192.168.100.0。
后续实验需要启动实例,Horizon登录地址
192.168.100.10/dashboard,使用域xiandian、用户名admin、密码000000登录之后上传本地D盘的cirros镜像到OpenStack平台。
执行脚本完成身份认证。
# 加载管理员环境变量,后续 openstack/ceilometer/curl 命令无需手动传凭据 [root@controller ~]# source /etc/keystone/admin-openrc.sh使用上节课学习的heat编排模板创建实例。
将以下内容保存到模板文件
instance.yaml中:heat_template_version: 2015-04-30 description: resources: # 1. 创建私有网络 my_network: type: OS::Neutron::Net properties: name: private-iso-net # 2. 创建私有子网 my_subnet: type: OS::Neutron::Subnet properties: name: private-iso-net-subnet network_id: { get_resource: my_network } # 网段 cidr: 192.168.200.0/24 gateway_ip: 192.168.200.1 # DNS服务器地址 dns_nameservers: ["114.114.114.114", "8.8.8.8"] ip_version: 4 enable_dhcp: true # 创建云主机 my_instance: type: OS::Nova::Server properties: name: my-test-instance # 镜像和规格名称 image: cirros flavor: m1.small networks: - network: { get_resource: my_network }创建堆栈
# 创建堆栈:-t 指定模板文件,test 为堆栈名 [root@controller ~]# openstack stack create -t instance.yaml test +---------------------+--------------------------------------+ | Field | Value | +---------------------+--------------------------------------+ | id | 77aa77ea-1571-46eb-9a34-52c0d41f4172 | | stack_name | test | | description | No description | | creation_time | 2025-12-11T12:30:24 | | updated_time | None | | stack_status | CREATE_IN_PROGRESS | | stack_status_reason | | +---------------------+--------------------------------------+ # 查看堆栈资源状态,确认网络/子网/实例创建进度 [root@controller ~]# openstack stack resource list test +---------------+-------------------------------------+---------------------+--------------------+---------------------+ | resource_name | physical_resource_id | resource_type | resource_status | updated_time | +---------------+-------------------------------------+---------------------+--------------------+---------------------+ | my_instance | ad42c104-0d1c-457d-a8ef- | OS::Nova::Server | CREATE_IN_PROGRESS | 2025-12-11T12:30:24 | | | c171e8a7a70f | | | | | my_subnet | cf3d0052-587d- | OS::Neutron::Subnet | CREATE_IN_PROGRESS | 2025-12-11T12:30:24 | | | 46ab-b784-054f011995ca | | | | | my_network | ab75c798-d8c8-4832-94af- | OS::Neutron::Net | CREATE_COMPLETE | 2025-12-11T12:30:24 | | | e36cc0e54d47 | | | | +---------------+-------------------------------------+---------------------+--------------------+---------------------+
作业1:查看堆栈资源清单后截图上传。
meters.yaml查看
关于Ceilmoeter需要采集的数据(指标)都定义在/etc/ceilometer/meters.yaml中,管理员可以对其进行修改。
示例(不需要修改):
- name: "image.size" # 指标名称:镜像大小
event_type: # 触发该指标的事件类型列表
- "image.upload" # 事件类型1:镜像上传
- "image.delete" # 事件类型2:镜像删除
- "image.update" # 事件类型3:镜像更新
type: "gauge" # 指标类型:gauge(可变度量值)
unit: B # 指标单位:字节(Bytes)
volume: $.payload.size # 指标值提取:从事件payload的size字段获取
resource_id: $.payload.id # 资源标识:从事件payload的id字段获取
project_id: $.payload.owner # 所属项目:从事件payload的owner字段获取
pipeline.yaml修改
可使用配置文件
/etc/ceilometer/pipeline.yaml配置对meters的处理,示例:修改cpu meter的采样间隔为1分钟,并且添加
- file:///var/log/ceilometer/ceilometer-file-publisher到publishers字段,增加一个发布方式。# sources:定义采集来源(轮询间隔、meter 列表、输出到哪个 sink) - name: cpu_source interval: 60 # 采样间隔 60 秒 meters: - "cpu" # 采集 cpu meter sinks: - cpu_sink # 发送到名为 cpu_sink 的下游处理链 # sinks:定义处理链(transformer + publisher) - name: cpu_sink transformers: - name: "rate_of_change" # 将累计值转为速率 parameters: target: name: "cpu_util" # 输出指标名 unit: "%" # 单位 type: "gauge" scale: "100.0 / (10**9 * (resource_metadata.cpu_number or 1))" # 将纳秒级 cpu 时间换算为百分比,除以 vCPU 数 publishers: - notifier:// # 默认通过消息队列发布 - file:///var/log/ceilometer/ceilometer-file-publisher # 追加:写入文件 - udp://192.168.100.10:9000 # 追加:UDP 推送到外部监听端口重启Ceilometer相关服务后查看发布的内容。
[root@controller]# systemctl restart openstack-ceilometer*查看文件内接收的内容:
[root@controller ~]# tail -f /var/log/ceilometer/ceilometer-file-publisher {'user_id': u'0befa70f767848e39df8224107b71858', 'name': 'cpu_util', 'resource_id': u'dcfb5539-454c-4d1a-9dc2-f39a3a64cb34', 'timestamp': u'2023-12-10T23:43:22.566393', 'resource_metadata': {u'status': u'active', u'cpu_number': 1, u'state': u'active', u'ramdisk_id': None, u'display_name': u'te', u'name': u'instance-00000009', u'disk_gb': 1, u'instance_host': u'controller', u'kernel_id': None, u'instance_id': u'dcfb5539-454c-4d1a-9dc2-f39a3a64cb34', u'image': {u'id': u'c65fc953-344c-46dc-8e3a-4a452fac4ffd', u'links': [{u'href': u'http://controller:8774/c88f5a1b7619420dadb4309743e53f1a/images/c65fc953-344c-46dc-8e3a-4a452fac4ffd', u'rel': u'bookmark'}], u'name': u'cirros'}, u'ephemeral_gb': 0, u'vcpus': 1, u'memory_mb': 512, u'instance_type': u'm1.tiny', u'host': u'e3d6b1115b110b721fd922d3e5c497f2c86621b692ddccdbee24320b', u'root_gb': 1, u'image_ref': u'c65fc953-344c-46dc-8e3a-4a452fac4ffd', u'flavor': {u'name': u'm1.tiny', u'links': [{u'href': u'http://controller:8774/c88f5a1b7619420dadb4309743e53f1a/flavors/1', u'rel': u'bookmark'}], u'ram': 512, u'ephemeral': 0, u'vcpus': 1, u'disk': 1, u'id': u'1'}, u'OS-EXT-AZ:availability_zone': u'nova', u'image_ref_url': u'http://controller:8774/c88f5a1b7619420dadb4309743e53f1a/images/c65fc953-344c-46dc-8e3a-4a452fac4ffd'}, 'volume': 0.0, 'source': 'openstack', 'project_id': u'f9ff39ba9daa4e5a8fee1fc50e2d2b34', 'type': 'gauge', 'id': 'e79bd3d4-97b5-11ee-b203-000c29d6d36c', 'unit': '%'}新建文件
udp_receiver.py,将以下代码保存到该文件,然后使用python udp_receiver.py运行:# -*- coding: utf-8 -*- import BaseHTTPServer import socket import threading import json import datetime import sys import errno # 引入错误码模块 # 尝试导入 msgpack try: import msgpack HAS_MSGPACK = True except ImportError: HAS_MSGPACK = False # --- 配置 --- UDP_PORT = 9000 WEB_PORT = 9001 # 端口保持和你刚才运行的一致 DATA_STORE = [] # --- 线程1:UDP 接收 --- def udp_server_thread(): sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock.bind(('0.0.0.0', UDP_PORT)) print "[UDP] 监听服务已启动 (Port: %s)" % UDP_PORT while True: try: data, addr = sock.recvfrom(65535) payload = None if HAS_MSGPACK: try: payload = msgpack.unpackb(data, encoding='utf-8') except: try: payload = msgpack.unpackb(data) except: pass if payload is None: try: payload = json.loads(data) except: pass now = datetime.datetime.now().strftime("%H:%M:%S") if payload: if isinstance(payload, list): for item in payload: item['recv_time'] = now DATA_STORE.insert(0, item) elif isinstance(payload, dict): payload['recv_time'] = now DATA_STORE.insert(0, payload) if len(DATA_STORE) > 50: del DATA_STORE[50:] # 简化日志,不打印具体内容,只打印计数 sys.stdout.write("[UDP] 收到数据! 当前缓存: %d\r" % len(DATA_STORE)) sys.stdout.flush() except Exception as e: print "\nUDP Error:", e # --- 线程2:Web 展示 (修复 Broken Pipe) --- class MonitorHandler(BaseHTTPServer.BaseHTTPRequestHandler): def do_GET(self): # 【修复1】忽略 favicon.ico 请求,防止浏览器乱断开 if self.path == '/favicon.ico': self.send_response(204) # No Content return try: self.send_response(200) self.send_header('Content-type', 'text/html; charset=utf-8') self.end_headers() html = u""" <html> <head> <title>Ceilometer 监控</title> <!-- 每 5 秒刷新一次,不要太快 --> <meta http-equiv="refresh" content="5"> <style> body { font-family: sans-serif; margin: 20px; } table { border-collapse: collapse; width: 100%%; } th, td { border: 1px solid #ddd; padding: 10px; text-align: left; } th { background-color: #007bff; color: white; } tr:nth-child(even) { background-color: #f2f2f2; } .high { color: red; font-weight: bold; } </style> </head> <body> <h2>Ceilometer UDP 实时数据</h2> <p>UDP端口: %d | Web端口: %d | 记录数: %d</p> <table> <tr> <th>接收时间</th> <th>Name</th> <th>Volume</th> <th>Resource ID</th> </tr> """ % (UDP_PORT, WEB_PORT, len(DATA_STORE)) for item in DATA_STORE: name = item.get('counter_name') or item.get('name', 'N/A') vol = item.get('counter_volume') or item.get('volume', 0) rid = item.get('resource_id', 'N/A') time = item.get('recv_time', '') style = u"" try: if float(vol) > 80: style = u'class="high"' except: pass html += u""" <tr> <td>%s</td> <td>%s</td> <td %s>%s</td> <td>%s</td> </tr> """ % (time, name, style, vol, rid) html += u"</table></body></html>" # 【修复2】捕获写入时的 Broken Pipe 错误 self.wfile.write(html.encode('utf-8')) except socket.error as e: # 如果是 Broken pipe (Errno 32),直接忽略,不打印报错 if e.errno == errno.EPIPE: pass else: print "Socket Error:", e except Exception as e: # 其他错误才打印 print "Web Error:", e # 禁止打印烦人的访问日志 def log_message(self, format, *args): return if __name__ == '__main__': t = threading.Thread(target=udp_server_thread) t.setDaemon(True) t.start() # 允许地址重用,防止重启脚本时报 Address already in use BaseHTTPServer.HTTPServer.allow_reuse_address = True server = BaseHTTPServer.HTTPServer(('', WEB_PORT), MonitorHandler) print "\n===========================================" print "Web 面板地址: http://<Controller_IP>:%d" % WEB_PORT print "UDP 监听端口: %d" % UDP_PORT print "===========================================\n" try: server.serve_forever() except KeyboardInterrupt: print "\nExit."使用浏览器打开
http://192.168.100.10:9001,查看网页端接收到的数据
作业2:使用网页端接收到数据后截图上传。
数据读取
通过命令读取计量和样本。
[root@controller ~]# ceilometer meter-list |head -10 # 列出计量项并截取前10行便于快速查看字段 +---------------------------------+------------+-----------+-----------------------------------------------------------------------+----------------------------------+----------------------------------+ | Name | Type | Unit | Resource ID | User ID | Project ID | +---------------------------------+------------+-----------+-----------------------------------------------------------------------+----------------------------------+----------------------------------+ | compute.instance.booting.time | gauge | sec | 2324c631-cf3e-49eb-a8e2-be384341e4ac | 0befa70f767848e39df8224107b71858 | f9ff39ba9daa4e5a8fee1fc50e2d2b34 | | compute.instance.booting.time | gauge | sec | 26b3b242-3704-4d37-88bc-d9cf8a5b0080 | 0befa70f767848e39df8224107b71858 | f9ff39ba9daa4e5a8fee1fc50e2d2b34 | | compute.instance.booting.time | gauge | sec | e1e52ae3-1f3e-4d62-bc21-d8b19055ab5c | 0befa70f767848e39df8224107b71858 | f9ff39ba9daa4e5a8fee1fc50e2d2b34 | | cpu | cumulative | ns | 2324c631-cf3e-49eb-a8e2-be384341e4ac | 0befa70f767848e39df8224107b71858 | f9ff39ba9daa4e5a8fee1fc50e2d2b34 | | cpu | cumulative | ns | e1e52ae3-1f3e-4d62-bc21-d8b19055ab5c | 0befa70f767848e39df8224107b71858 | f9ff39ba9daa4e5a8fee1fc50e2d2b34 | | cpu.delta | delta | ns | e1e52ae3-1f3e-4d62-bc21-d8b19055ab5c | 0befa70f767848e39df8224107b71858 | f9ff39ba9daa4e5a8fee1fc50e2d2b34 | | cpu_util | gauge | % | e1e52ae3-1f3e-4d62-bc21-d8b19055ab5c | 0befa70f767848e39df8224107b71858 | f9ff39ba9daa4e5a8fee1fc50e2d2b34 |[root@controller ~]# ceilometer sample-list -m cpu_util | head -10 # 指定 meter(cpu_util) 查看最近样本 +--------------------------------------+----------+-------+----------------+------+----------------------------+ | Resource ID | Name | Type | Volume | Unit | Timestamp | +--------------------------------------+----------+-------+----------------+------+----------------------------+ | dcfb5539-454c-4d1a-9dc2-f39a3a64cb34 | cpu_util | gauge | 3.34969394963 | % | 2023-12-11T10:49:36.856000 | | dcfb5539-454c-4d1a-9dc2-f39a3a64cb34 | cpu_util | gauge | 3.41829637437 | % | 2023-12-11T10:48:36.851000 | | dcfb5539-454c-4d1a-9dc2-f39a3a64cb34 | cpu_util | gauge | 3.50940950027 | % | 2023-12-11T10:47:37.172000 | | dcfb5539-454c-4d1a-9dc2-f39a3a64cb34 | cpu_util | gauge | 3.53780765348 | % | 2023-12-11T10:46:37.048000 | | dcfb5539-454c-4d1a-9dc2-f39a3a64cb34 | cpu_util | gauge | 3.50009759439 | % | 2023-12-11T10:45:36.841000 | | dcfb5539-454c-4d1a-9dc2-f39a3a64cb34 | cpu_util | gauge | 3.46708312113 | % | 2023-12-11T10:44:36.843000 | | dcfb5539-454c-4d1a-9dc2-f39a3a64cb34 | cpu_util | gauge | 3.31595080815 | % | 2023-12-11T10:43:36.850000 |通过api读取计量和样本。
# 获取令牌Token(供 curl 直接使用) [root@controller ~]# openstack token issue # 复制令牌、导出令牌到环境变量 OS_TOKEN [root@controller ~]# export OS_TOKEN=gAAAAABlduo9h-PcRjEUSOgCRqzg06x6JzWSEMWWm2hFZqbbvh6zdR12vTzYP9HNRhk99fKbK0hDsYz6nVCZmDuqXJHGMaPWUutYxZ38t0jKwbvYDiVtzhAUK12ws6mVzXbjAIVThIv8QZZ_kuvaWhRf0EIp9JNZE0XTAHF_htTb1-v9LqX6aCg # 读取计量列表(8777 为 ceilometer-api 默认端口),json.tool 美化输出,less 分页 [root@controller ~]# curl -X GET -H "X-Auth-Token: $OS_TOKEN" "http://localhost:8777/v2/meters" |python -m json.tool |less# 发起请求读取样本列表(同样使用 OS_TOKEN) [root@controller ~]# curl -X GET -H "X-Auth-Token: $OS_TOKEN" "http://localhost:8777/v2/samples" |python -m json.tool |less作业3:通过api读取到样本后截图上传。
Alarms告警规则
使用 CLI 创建一个名为 “cpu_high” 的 Threshold Alarm “当连续 2 个 1 分钟内 某 instance 的 cpu_util 值超过 70 的时候产生告警,并其内容被写入日志文件”:
# 创建阈值告警规则:当 CPU 利用率连续 3 个采样周期(3×60秒)超过 70% 时触发告警
# --name: 告警名称
# --meter-name: 监控指标名称(cpu_util)
# --threshold: 告警阈值(70%)
# --comparison-operator: 比较操作符(gt 表示大于)
# --statistic: 统计方式(avg 表示平均值)
# --period: 采样周期(60 秒)
# --evaluation-periods: 连续评估周期数(3 个周期都超过阈值才触发)
# --alarm-action: 告警触发时的动作(HTTP POST 到接收端点)
# --query: 查询条件(指定特定实例的 resource_id)
[root@controller ~]# ceilometer alarm-threshold-create \
--name cpu_high \
--description 'Alert when CPU exceeds 70% for 3 consecutive periods' \
--meter-name cpu_util \
--threshold 70.0 \
--comparison-operator gt \
--statistic avg \
--period 60 \
--evaluation-periods 3 \
--alarm-action 'http://192.168.100.10:9001' \
--query resource_id=INSTANCE_ID
提示:上述命令中的INSTANCE_ID需要替换成实际的实例ID
# 查看告警规则清单
[root@controller ~]# ceilometer alarm-list
+--------------------------------------+--------------+-------+----------+---------+------------+-------------------------------------+------------------+
| Alarm ID | Name | State | Severity | Enabled | Continuous | Alarm condition | Time constraints |
+--------------------------------------+--------------+-------+----------+---------+------------+-------------------------------------+------------------+
| 3047034c-462a-461a-86ea-2c7446b3b024 | cpu_high_web | ok | low | True | False | avg(cpu_util) > 70.0 during 3 x 10s | None |
+--------------------------------------+--------------+-------+----------+---------+------------+-------------------------------------+------------------+
CTRL-C关掉上一个python脚本,新建文件alarm_receiver.py,将以下代码保存到该文件,然后使用python alarm_receiver.py运行:
# -*- coding: utf-8 -*-
import BaseHTTPServer
import socket
import threading
import json
import datetime
import sys
import errno
# 尝试导入 msgpack
try:
import msgpack
HAS_MSGPACK = True
except ImportError:
HAS_MSGPACK = False
# --- 配置 ---
UDP_PORT = 9000
WEB_PORT = 9001
DATA_STORE = []
ALARM_STORE = []
# --- 自定义静默服务器类 (用于屏蔽 Broken pipe 报错) ---
class QuietHTTPServer(BaseHTTPServer.HTTPServer):
def handle_error(self, request, client_address):
"""重写错误处理方法,过滤掉 Broken pipe"""
exc_type, exc_value, _ = sys.exc_info()
# 如果是 socket 错误且错误码是 32 (Broken pipe),直接忽略
if issubclass(exc_type, socket.error) and exc_value.errno == errno.EPIPE:
return
# 其他错误照常打印
BaseHTTPServer.HTTPServer.handle_error(self, request, client_address)
# --- 线程1:UDP 接收 ---
def udp_server_thread():
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.bind(('0.0.0.0', UDP_PORT))
print "[UDP] 监听服务已启动 (Port: %s)" % UDP_PORT
while True:
try:
data, addr = sock.recvfrom(65535)
payload = None
if HAS_MSGPACK:
try: payload = msgpack.unpackb(data, encoding='utf-8')
except:
try: payload = msgpack.unpackb(data)
except: pass
if payload is None:
try: payload = json.loads(data)
except: pass
now = datetime.datetime.now().strftime("%H:%M:%S")
if payload:
if isinstance(payload, list):
for item in payload:
item['recv_time'] = now
DATA_STORE.insert(0, item)
elif isinstance(payload, dict):
payload['recv_time'] = now
DATA_STORE.insert(0, payload)
if len(DATA_STORE) > 50: del DATA_STORE[50:]
except Exception as e:
print "\nUDP Error:", e
# --- 线程2:Web 展示 + 告警接收 ---
class MonitorHandler(BaseHTTPServer.BaseHTTPRequestHandler):
def do_GET(self):
if self.path == '/favicon.ico':
self.send_response(204)
return
try:
self.send_response(200)
self.send_header('Content-type', 'text/html; charset=utf-8')
self.end_headers()
html = u"""
<html>
<head>
<title>OpenStack 全栈监控</title>
<meta http-equiv="refresh" content="3">
<style>
body { font-family: "Microsoft YaHei", sans-serif; margin: 20px; }
table { border-collapse: collapse; width: 100%%; margin-bottom: 20px; }
th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
.realtime-th { background-color: #007bff; color: white; }
.alarm-th { background-color: #dc3545; color: white; }
tr:nth-child(even) { background-color: #f2f2f2; }
.high { color: red; font-weight: bold; }
h2 { border-bottom: 2px solid #ccc; padding-bottom: 5px; }
</style>
</head>
<body>
<h2 style="color: #dc3545;"> 告警历史 (Alarm History)</h2>
<table>
<tr>
<th class="alarm-th">触发时间</th>
<th class="alarm-th">告警名称</th>
<th class="alarm-th">原因 (Reason)</th>
<th class="alarm-th">状态</th>
</tr>
"""
if not ALARM_STORE:
html += u"<tr><td colspan='4' style='text-align:center'>暂无告警</td></tr>"
else:
for alarm in ALARM_STORE:
reason = alarm.get('reason', u'N/A')
if not isinstance(reason, unicode):
reason = str(reason).decode('utf-8', 'ignore')
html += u"""
<tr><td>%s</td><td>%s</td><td>%s</td><td>%s</td></tr>
""" % (alarm.get('time'), alarm.get('name'), reason, alarm.get('state'))
html += u"""
</table>
<h2 style="color: #007bff;"> 实时采样 (Real-time Samples)</h2>
<table>
<tr>
<th class="realtime-th">接收时间</th>
<th class="realtime-th">监控项</th>
<th class="realtime-th">数值</th>
<th class="realtime-th">Resource ID</th>
</tr>
"""
for item in DATA_STORE:
name = item.get('counter_name') or item.get('name', 'N/A')
vol = item.get('counter_volume') or item.get('volume', 0)
rid = item.get('resource_id', 'N/A')
time = item.get('recv_time', '')
style = u'class="high"' if float(vol) > 80 else u""
html += u"""
<tr><td>%s</td><td>%s</td><td %s>%s</td><td>%s</td></tr>
""" % (time, name, style, vol, rid)
html += u"</table></body></html>"
self.wfile.write(html.encode('utf-8'))
except socket.error:
# 这里虽然捕获了,但 finish() 阶段可能还会抛出,会被 QuietHTTPServer 拦截
pass
def do_POST(self):
try:
content_length = int(self.headers['Content-Length'])
post_data = self.rfile.read(content_length)
post_data_str = post_data.decode('utf-8', 'ignore')
payload = json.loads(post_data_str)
now = datetime.datetime.now().strftime("%H:%M:%S")
reason = payload.get('reason', u'N/A')
if not isinstance(reason, unicode):
reason = str(reason).decode('utf-8', 'ignore')
alarm_info = {
'time': now,
'name': payload.get('alarm_name', u'Unknown'),
'state': payload.get('current', u'alarm'),
'reason': reason
}
ALARM_STORE.insert(0, alarm_info)
if len(ALARM_STORE) > 10: del ALARM_STORE[10:]
# 打印到终端,证明收到了
print_msg = u"[ALARM] 收到告警: %s" % reason
print print_msg.encode('utf-8')
self.send_response(200)
self.end_headers()
except Exception as e:
print "POST Error:", str(e)
self.send_response(500)
self.end_headers()
def log_message(self, format, *args): return
if __name__ == '__main__':
t = threading.Thread(target=udp_server_thread)
t.setDaemon(True)
t.start()
# 使用 QuietHTTPServer 而不是 BaseHTTPServer.HTTPServer
QuietHTTPServer.allow_reuse_address = True
server = QuietHTTPServer(('', WEB_PORT), MonitorHandler)
print "\n==========================================="
print "全栈监控面板: http://<Controller_IP>:%d" % WEB_PORT
print "UDP 监听端口: %d" % UDP_PORT
print "HTTP 监听端口: %d" % WEB_PORT
print "===========================================\n"
try:
server.serve_forever()
except KeyboardInterrupt:
print "Exit."
网站运行起来之后使用浏览器访问
192.168.100.10:9001查看监控页面。
出发告警规则
告警规则要求是云主机的占用率持续超过70%,这里我们需要在云主机内执行一个while循环,在云主机的控制台内运行以下命令:
while true; do :;done
运行循环后云主机的CPU占用会持续超过70%,等待3分钟后浏览器上会产生一个告警记录。
作业4:等待数据产生,观察浏览器上是否出现告警记录,截图上传。