zabbix_manager 5.1.6 → 6.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +31 -1
- data/README.md +69 -46
- data/lib/zabbix_manager/classes/actions.rb +3 -11
- data/lib/zabbix_manager/classes/applications.rb +2 -8
- data/lib/zabbix_manager/classes/configurations.rb +4 -9
- data/lib/zabbix_manager/classes/{drules.rb → discovery_rules.rb} +2 -8
- data/lib/zabbix_manager/classes/errors.rb +3 -0
- data/lib/zabbix_manager/classes/events.rb +2 -1
- data/lib/zabbix_manager/classes/graphs.rb +19 -15
- data/lib/zabbix_manager/classes/{hostgroups.rb → host_groups.rb} +5 -11
- data/lib/zabbix_manager/classes/host_interfaces.rb +261 -0
- data/lib/zabbix_manager/classes/hosts.rb +135 -89
- data/lib/zabbix_manager/classes/{httptests.rb → http_tests.rb} +2 -8
- data/lib/zabbix_manager/classes/items.rb +91 -87
- data/lib/zabbix_manager/classes/maintenance.rb +2 -1
- data/lib/zabbix_manager/classes/{mediatypes.rb → media_types.rb} +2 -7
- data/lib/zabbix_manager/classes/problems.rb +11 -40
- data/lib/zabbix_manager/classes/proxies.rb +5 -15
- data/lib/zabbix_manager/classes/proxy_groups.rb +16 -0
- data/lib/zabbix_manager/classes/roles.rb +13 -19
- data/lib/zabbix_manager/classes/screens.rb +11 -30
- data/lib/zabbix_manager/classes/scripts.rb +2 -8
- data/lib/zabbix_manager/classes/server.rb +1 -0
- data/lib/zabbix_manager/classes/templates.rb +2 -65
- data/lib/zabbix_manager/classes/triggers.rb +42 -45
- data/lib/zabbix_manager/classes/user_groups.rb +63 -0
- data/lib/zabbix_manager/classes/user_macros.rb +110 -0
- data/lib/zabbix_manager/classes/users.rb +15 -54
- data/lib/zabbix_manager/classes/value_maps.rb +52 -0
- data/lib/zabbix_manager/client.rb +148 -277
- data/lib/zabbix_manager/configuration.rb +107 -0
- data/lib/zabbix_manager/http_transport.rb +145 -97
- data/lib/zabbix_manager/log_sanitizer.rb +9 -19
- data/lib/zabbix_manager/monitoring/device.rb +393 -0
- data/lib/zabbix_manager/monitoring/expressions.rb +36 -0
- data/lib/zabbix_manager/monitoring/line.rb +175 -0
- data/lib/zabbix_manager/monitoring/line_items.rb +110 -0
- data/lib/zabbix_manager/monitoring/line_lifecycle.rb +169 -0
- data/lib/zabbix_manager/monitoring/line_plan.rb +113 -0
- data/lib/zabbix_manager/monitoring/thresholds.rb +182 -0
- data/lib/zabbix_manager/monitoring/traffic_items.rb +132 -0
- data/lib/zabbix_manager/monitoring/validation.rb +92 -0
- data/lib/zabbix_manager/monitoring.rb +216 -642
- data/lib/zabbix_manager/resource.rb +235 -0
- data/lib/zabbix_manager/traffic.rb +238 -0
- data/lib/zabbix_manager/version.rb +1 -1
- data/lib/zabbix_manager.rb +66 -179
- data/zabbix_manager.gemspec +6 -22
- metadata +41 -190
- data/lib/zabbix_manager/basic/basic_alias.rb +0 -42
- data/lib/zabbix_manager/basic/basic_func.rb +0 -55
- data/lib/zabbix_manager/basic/basic_init.rb +0 -50
- data/lib/zabbix_manager/basic/basic_logic.rb +0 -239
- data/lib/zabbix_manager/classes/hostinterfaces.rb +0 -189
- data/lib/zabbix_manager/classes/usergroups.rb +0 -75
- data/lib/zabbix_manager/classes/usermacros.rb +0 -222
- data/lib/zabbix_manager/classes/valuemaps.rb +0 -26
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class ZabbixManager
|
|
4
|
+
# 共享资源身份、精确查询和单对象写入行为。
|
|
5
|
+
# 子类声明 API 前缀、身份和回执字段;原生透传仍由调用方负责授权及版本适配。
|
|
6
|
+
# 回执校验只确认响应中的 ID,不提供事务、跨进程排他或失败后的自动重放。
|
|
7
|
+
class Resource
|
|
8
|
+
# @param client [ZabbixManager::Client] 共享客户端
|
|
9
|
+
def initialize(client)
|
|
10
|
+
@client = client
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# @return [String] Zabbix API 模块名
|
|
14
|
+
def method_name
|
|
15
|
+
raise Invalid, "resource API method is not defined"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# @return [String] 业务标识字段
|
|
19
|
+
def identify
|
|
20
|
+
"name"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# @return [String] 对象 ID 字段
|
|
24
|
+
def key
|
|
25
|
+
"#{method_name}id"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# @return [String] 写入响应的 ID 集合字段
|
|
29
|
+
def keys
|
|
30
|
+
"#{key}s"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# @return [Hash] 创建默认属性
|
|
34
|
+
def default_options
|
|
35
|
+
{}
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# 合并创建默认属性,不修改调用方输入。
|
|
39
|
+
# @param data [Hash] 对应资源的原生 API 属性
|
|
40
|
+
# @return [Integer] 创建对象的 ID
|
|
41
|
+
# @raise [ProtocolError] 响应没有确认唯一的有效 ID
|
|
42
|
+
def create(data)
|
|
43
|
+
response_id(create_raw([default_options.merge(data.deep_symbolize_keys)]))
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# 删除明确指定的对象 ID。
|
|
47
|
+
# 此操作不可撤销;批量回执必须确认所有请求 ID。
|
|
48
|
+
# @param ids [Integer, String, Array<Integer, String>] 一个或多个正整数 ID
|
|
49
|
+
# @return [Integer] 首个删除对象的 ID
|
|
50
|
+
def delete(ids)
|
|
51
|
+
requested = normalized_ids(ids)
|
|
52
|
+
response_ids(delete_raw(requested), expected: requested).first
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# 按稳定业务身份创建或更新一个对象。
|
|
56
|
+
# 通用 CRUD 不提供跨进程原子性;监控业务使用对应的 reconcile/upsert 方法。
|
|
57
|
+
# @param data [Hash] 包含 identify 字段的原生属性
|
|
58
|
+
# @return [Integer]
|
|
59
|
+
def create_or_update(data)
|
|
60
|
+
attributes = data.deep_symbolize_keys
|
|
61
|
+
id = get_id(**identity_filter(attributes))
|
|
62
|
+
id ? update(attributes.merge(key.to_sym => id)) : create(attributes)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# 仅在请求属性有变化时更新;无条件写入使用 update_raw。
|
|
66
|
+
# @param data [Hash] 包含资源 ID 的原生属性,省略字段不参与比较
|
|
67
|
+
# @return [Integer]
|
|
68
|
+
# @raise [ProtocolError] 写入回执未确认请求 ID
|
|
69
|
+
def update(data)
|
|
70
|
+
attributes = data.deep_symbolize_keys
|
|
71
|
+
id = attributes[key.to_sym]
|
|
72
|
+
raise Invalid, "#{key} is required" if id.blank?
|
|
73
|
+
|
|
74
|
+
normalized_ids([id])
|
|
75
|
+
|
|
76
|
+
current = dump_by_id(key.to_sym => id).find { |item| item.fetch(key).to_s == id.to_s }
|
|
77
|
+
return id.to_i if current && attributes_match?(current, attributes)
|
|
78
|
+
|
|
79
|
+
response_id(update_raw([attributes]), expected: [id])
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# 按业务标识查询完整对象。
|
|
83
|
+
# @param data [Hash] 包含 identify 指定的业务标识
|
|
84
|
+
# @return [Array<Hash>]
|
|
85
|
+
def get_full_data(data)
|
|
86
|
+
get_raw(filter: identity_filter(data), output: "extend")
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# 按对象 ID 查询完整对象。
|
|
90
|
+
# @param data [Hash] 包含 key 指定的 ID 字段
|
|
91
|
+
# @return [Array<Hash>]
|
|
92
|
+
def dump_by_id(data)
|
|
93
|
+
attributes = data.deep_symbolize_keys
|
|
94
|
+
id = attributes[key.to_sym]
|
|
95
|
+
raise Invalid, "#{key} is required" if id.blank?
|
|
96
|
+
|
|
97
|
+
get_raw(filter: { key.to_sym => id }, output: "extend")
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# @return [Hash] 业务标识到原生字符串 ID 的映射
|
|
101
|
+
# @raise [Conflict] 多个资源具有相同业务标识
|
|
102
|
+
def all
|
|
103
|
+
get_raw(output: "extend").each_with_object({}) do |item, result|
|
|
104
|
+
name = item.fetch(identify)
|
|
105
|
+
raise Conflict, "multiple #{method_name} objects share the same identity" if result.key?(name)
|
|
106
|
+
|
|
107
|
+
result[name] = item.fetch(key)
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# 精确查找唯一资源,空结果返回 nil,多个结果拒绝任意选择。
|
|
112
|
+
# @param data [Hash] 包含 identify 字段的精确过滤条件
|
|
113
|
+
# @return [Integer, nil]
|
|
114
|
+
def get_id(data)
|
|
115
|
+
attributes = data.deep_symbolize_keys
|
|
116
|
+
name = attributes[identify.to_sym]
|
|
117
|
+
raise Invalid, "#{identify} not supplied in call to get_id" if name.nil?
|
|
118
|
+
|
|
119
|
+
matches = get_raw(filter: attributes, output: [key, identify]).select do |item|
|
|
120
|
+
item[identify].to_s == name.to_s
|
|
121
|
+
end
|
|
122
|
+
raise Conflict, "multiple #{method_name} objects match the requested identity" if matches.length > 1
|
|
123
|
+
|
|
124
|
+
response_identifier(matches.first[key]) if matches.first
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# 查找或创建单个对象,不隐含更新已有对象。
|
|
128
|
+
# @param data [Hash] 创建属性及 identify 指定的业务标识
|
|
129
|
+
# @return [Integer] 已存在或新创建对象的 ID
|
|
130
|
+
def get_or_create(data)
|
|
131
|
+
get_id(**identity_filter(data)) || create(data)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# @return [Hash] 精确查询条件,复合身份由资源类覆盖
|
|
135
|
+
def identity_filter(data)
|
|
136
|
+
attributes = data.deep_symbolize_keys
|
|
137
|
+
{ identify.to_sym => attributes.fetch(identify.to_sym) }
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# @param data [Hash] 原生 *.get 参数;调用方负责授权和资源边界
|
|
141
|
+
# @return [Object] 原始查询响应
|
|
142
|
+
def get_raw(data)
|
|
143
|
+
@client.api_request(method: "#{method_name}.get", params: data)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# @param data [Hash, Array<Hash>] 原生 *.create 参数,不执行高层业务校验
|
|
147
|
+
# @return [Object] 原始创建响应,调用方负责核对回执
|
|
148
|
+
def create_raw(data)
|
|
149
|
+
@client.api_request(method: "#{method_name}.create", params: data)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# @param data [Hash, Array<Hash>] 原生 *.update 参数,不执行高层业务校验
|
|
153
|
+
# @return [Object] 原始更新响应,调用方负责核对回执
|
|
154
|
+
def update_raw(data)
|
|
155
|
+
@client.api_request(method: "#{method_name}.update", params: data)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# @param data [Array<Integer, String>] 原生 *.delete ID 集合
|
|
159
|
+
# @return [Object] 原始删除响应,调用方负责核对回执
|
|
160
|
+
def delete_raw(data)
|
|
161
|
+
@client.api_request(method: "#{method_name}.delete", params: data)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
private
|
|
165
|
+
|
|
166
|
+
def validate_boolean!(value, name)
|
|
167
|
+
raise Invalid, "#{name} must be true or false" unless value == true || value == false
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def integer_attribute(value, name)
|
|
171
|
+
unless value.is_a?(Integer) || (value.is_a?(String) && value.match?(/\A[+-]?\d+\z/))
|
|
172
|
+
raise Invalid, "#{name} must be an integer"
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
value.is_a?(Integer) ? value : Integer(value, 10)
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def normalized_ids(values, allow_empty: false)
|
|
179
|
+
ids = Array.wrap(values).map do |value|
|
|
180
|
+
id = integer_attribute(value, keys)
|
|
181
|
+
raise Invalid, "#{keys} must be positive integers" unless id.positive?
|
|
182
|
+
|
|
183
|
+
id.to_s
|
|
184
|
+
end.uniq
|
|
185
|
+
raise Invalid, "#{keys} are required" if ids.empty? && !allow_empty
|
|
186
|
+
|
|
187
|
+
ids
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# API 数字字符串可与整数比较,nil 和布尔值保留各自语义。
|
|
191
|
+
def normalized_attributes(attributes)
|
|
192
|
+
attributes.deep_symbolize_keys.deep_transform_values do |value|
|
|
193
|
+
value.is_a?(Numeric) ? value.to_s : value
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def attributes_match?(current, desired)
|
|
198
|
+
actual = normalized_attributes(current)
|
|
199
|
+
expected = normalized_attributes(desired)
|
|
200
|
+
actual.slice(*expected.keys) == expected
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def response_identifier(value, id_field = key)
|
|
204
|
+
unless value.is_a?(Integer) || (value.is_a?(String) && value.match?(/\A[0-9]+\z/))
|
|
205
|
+
raise ProtocolError, "invalid #{method_name} response: #{id_field} must be a positive integer"
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
id = value.to_i
|
|
209
|
+
raise ProtocolError, "invalid #{method_name} response: #{id_field} must be positive" unless id.positive?
|
|
210
|
+
|
|
211
|
+
id
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def response_ids(result, id_field = keys, expected: nil)
|
|
215
|
+
values = result[id_field] if result.is_a?(Hash)
|
|
216
|
+
unless values.is_a?(Array) && values.any?
|
|
217
|
+
raise ProtocolError, "invalid #{method_name} response: #{id_field} must be a nonempty array"
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
ids = values.map { |value| response_identifier(value, id_field) }
|
|
221
|
+
if ids.uniq != ids || (expected && ids.sort != expected.map { |id| response_identifier(id) }.sort)
|
|
222
|
+
raise ProtocolError, "invalid #{method_name} response: #{id_field} do not confirm the requested objects"
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
ids
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def response_id(result, id_field = keys, expected: nil)
|
|
229
|
+
ids = response_ids(result, id_field, expected: expected)
|
|
230
|
+
raise ProtocolError, "invalid #{method_name} response: expected one #{key}" unless ids.one?
|
|
231
|
+
|
|
232
|
+
ids.first
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
end
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "bigdecimal"
|
|
4
|
+
|
|
5
|
+
class ZabbixManager
|
|
6
|
+
# 通过官方 API 读取有界流量序列;历史样本与小时趋势始终明确区分。
|
|
7
|
+
class Traffic
|
|
8
|
+
DEFAULT_LIMIT = 12_000
|
|
9
|
+
DEFAULT_MAX_POINTS = 720
|
|
10
|
+
DECIMAL = /\A[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?\z/
|
|
11
|
+
|
|
12
|
+
# @param manager [ZabbixManager] 共享连接及资源对象的管理器
|
|
13
|
+
def initialize(manager)
|
|
14
|
+
@manager = manager
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# 返回每个请求监控项的序列,包括不可见、不支持和无数据的状态。
|
|
18
|
+
# 时段两端均包含;Time 按秒取整。数值保留 Integer 或 BigDecimal 精度。
|
|
19
|
+
# points 包含 clock/ns/count/value/min/max;压缩后时间指向桶内最后一条记录。
|
|
20
|
+
# current_value 为已返回记录中最新值(趋势为小时均值);截断趋势不保证包含最新小时。
|
|
21
|
+
# @param hostid [Integer, String] 所属主机 ID
|
|
22
|
+
# @param itemids [Array<Integer, String>] 需要查询的监控项 ID
|
|
23
|
+
# @param time_from [Time, Integer] 起始时间或 Unix 秒
|
|
24
|
+
# @param time_till [Time, Integer] 结束时间或 Unix 秒
|
|
25
|
+
# @param source [Symbol] :history 原始样本,或 :trends 小时统计
|
|
26
|
+
# @param limit [Integer] 每项最多保留的 API 记录数,额外查询一条用于识别截断
|
|
27
|
+
# @param max_points [Integer] 每项最多返回的图表点数
|
|
28
|
+
# @return [Hash] 查询范围、来源及包含状态、统计和 points 的 series 数组
|
|
29
|
+
# @raise [Invalid] 标识、时间范围或数量无效
|
|
30
|
+
# @raise [ProtocolError] 响应形状、归属或样本数值无效
|
|
31
|
+
# @raise [ApiError, TransportError] 官方 API 查询失败;不会转换为空序列
|
|
32
|
+
def series(hostid:, itemids:, time_from:, time_till:, source: :history,
|
|
33
|
+
limit: DEFAULT_LIMIT, max_points: DEFAULT_MAX_POINTS)
|
|
34
|
+
hostid = identifier!(hostid, "hostid")
|
|
35
|
+
ids = item_ids!(itemids)
|
|
36
|
+
options = query_options(time_from, time_till, source, limit, max_points)
|
|
37
|
+
items = items_for_host(hostid, ids)
|
|
38
|
+
results = ids.map { |id| item_series(id, items[id], options) }
|
|
39
|
+
{ hostid: hostid, **options.slice(:time_from, :time_till, :source), series: results }
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# 精确发现接口入/出方向的 bps 监控项,再读取相同时间范围的序列。
|
|
43
|
+
# @param host [String, Array<String>, Hash] 主机候选名或明确的 hostid + host
|
|
44
|
+
# @param interface_name [String] 接口名称,复用现有方向及单位校验
|
|
45
|
+
# @param time_from [Time, Integer] 起始时间或 Unix 秒
|
|
46
|
+
# @param time_till [Time, Integer] 结束时间或 Unix 秒
|
|
47
|
+
# @param source [Symbol] :history 或 :trends
|
|
48
|
+
# @param limit [Integer] 每项 API 记录上限
|
|
49
|
+
# @param max_points [Integer] 每项图表点上限
|
|
50
|
+
# @return [Hash] series 结果,附带 host、interface_name 和各项 direction
|
|
51
|
+
# @raise [Invalid, Conflict] 接口输入、bps 语义或主机/监控项身份无法确认
|
|
52
|
+
# @raise [ApiError, TransportError] 主机、监控项或采样查询失败
|
|
53
|
+
def for_interface(host:, interface_name:, time_from:, time_till:, source: :history,
|
|
54
|
+
limit: DEFAULT_LIMIT, max_points: DEFAULT_MAX_POINTS)
|
|
55
|
+
Monitoring::Validation.text!(interface_name, "interface_name")
|
|
56
|
+
query_options(time_from, time_till, source, limit, max_points)
|
|
57
|
+
resolved = @manager.hosts.resolve(host)
|
|
58
|
+
candidates = @manager.items.monitored_traffic_candidates(resolved.fetch(:hostid))
|
|
59
|
+
response_rows!(candidates, "item.get")
|
|
60
|
+
traffic = Monitoring::TrafficItems.new(candidates).for_interface(interface_name)
|
|
61
|
+
directions = traffic.to_h { |direction, item| [item.fetch(:itemid).to_s, direction] }
|
|
62
|
+
result = series(hostid: resolved.fetch(:hostid), itemids: directions.keys,
|
|
63
|
+
time_from: time_from, time_till: time_till, source: source, limit: limit, max_points: max_points)
|
|
64
|
+
result[:series].each do |item|
|
|
65
|
+
if item[:status] != :unavailable && item[:units] != "bps"
|
|
66
|
+
raise Conflict, "interface traffic units changed after bps discovery"
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
item[:direction] = directions.fetch(item[:itemid])
|
|
70
|
+
end
|
|
71
|
+
result.merge(host: resolved.fetch(:host), interface_name: interface_name)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
private
|
|
75
|
+
|
|
76
|
+
def identifier!(value, name)
|
|
77
|
+
Monitoring::Validation.positive_id!(value, name).to_s
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def item_ids!(values)
|
|
81
|
+
raise Invalid, "itemids must be a nonempty array" unless values.is_a?(Array) && values.any?
|
|
82
|
+
|
|
83
|
+
values.map { |value| identifier!(value, "itemid") }.uniq
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def query_options(time_from, time_till, source, limit, max_points)
|
|
87
|
+
time_from = epoch!(time_from, "time_from")
|
|
88
|
+
time_till = epoch!(time_till, "time_till")
|
|
89
|
+
raise Invalid, "time_till must not precede time_from" if time_till < time_from
|
|
90
|
+
raise Invalid, "source must be :history or :trends" unless %i[history trends].include?(source)
|
|
91
|
+
|
|
92
|
+
{ time_from: time_from, time_till: time_till, source: source,
|
|
93
|
+
limit: positive_integer!(limit, "limit"), max_points: positive_integer!(max_points, "max_points") }
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def epoch!(value, name)
|
|
97
|
+
value = value.to_i if value.is_a?(Time)
|
|
98
|
+
raise Invalid, "#{name} must be Time or nonnegative Unix seconds" unless value.is_a?(Integer) && value >= 0
|
|
99
|
+
|
|
100
|
+
value
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def positive_integer!(value, name)
|
|
104
|
+
raise Invalid, "#{name} must be a positive integer" unless value.is_a?(Integer) && value.positive?
|
|
105
|
+
|
|
106
|
+
value
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# 显式主机过滤和响应归属检查保证外部 itemid 不能越过查询边界。
|
|
110
|
+
def items_for_host(hostid, ids)
|
|
111
|
+
rows = @manager.query(method: "item.get", params: {
|
|
112
|
+
hostids: [hostid], itemids: ids, monitored: true,
|
|
113
|
+
output: %w[itemid hostid name units value_type]
|
|
114
|
+
})
|
|
115
|
+
response_rows!(rows, "item.get").each_with_object({}) do |item, result|
|
|
116
|
+
id = response_integer!(item["itemid"], "itemid", positive: true).to_s
|
|
117
|
+
owner = response_integer!(item["hostid"], "hostid", positive: true).to_s
|
|
118
|
+
unless ids.include?(id) && owner == hostid && !result.key?(id)
|
|
119
|
+
raise ProtocolError, "item.get returned unexpected or duplicate item ownership"
|
|
120
|
+
end
|
|
121
|
+
unless item["name"].is_a?(String) && item["units"].is_a?(String)
|
|
122
|
+
raise ProtocolError, "item.get returned invalid name or units"
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
result[id] = { itemid: id, name: item["name"], units: item["units"],
|
|
126
|
+
value_type: response_integer!(item["value_type"], "value_type") }
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def item_series(id, metadata, options)
|
|
131
|
+
result = { itemid: id, name: nil, units: nil, value_type: nil, status: :unavailable,
|
|
132
|
+
truncated: false, record_count: 0, sample_count: 0,
|
|
133
|
+
current_value: nil, peak_value: nil, points: [] }
|
|
134
|
+
return result unless metadata
|
|
135
|
+
|
|
136
|
+
result.merge!(metadata)
|
|
137
|
+
return result.merge(status: :unsupported) unless [0, 3].include?(metadata[:value_type])
|
|
138
|
+
|
|
139
|
+
points = read_points(metadata, options)
|
|
140
|
+
truncated = points.length > options[:limit]
|
|
141
|
+
points = points.sort_by { |point| [point[:clock], point[:ns]] }.last(options[:limit])
|
|
142
|
+
result.merge(status: points.empty? ? :empty : :ok, truncated: truncated,
|
|
143
|
+
record_count: points.length, sample_count: points.sum { |point| point[:count] },
|
|
144
|
+
current_value: points.last&.fetch(:value), peak_value: points.map { |point| point[:max] }.max,
|
|
145
|
+
points: reduce_points(points, options[:max_points]))
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def read_points(item, options)
|
|
149
|
+
history = options[:source] == :history
|
|
150
|
+
method = history ? "history.get" : "trend.get"
|
|
151
|
+
params = options.slice(:time_from, :time_till).merge(itemids: [item[:itemid]], limit: options[:limit] + 1)
|
|
152
|
+
if history
|
|
153
|
+
params.merge!(history: item[:value_type], output: %w[itemid clock ns value],
|
|
154
|
+
sortfield: %w[clock ns], sortorder: "DESC")
|
|
155
|
+
else
|
|
156
|
+
# trend.get 没有排序参数;截断的趋势集合不承诺覆盖整段或包含最新小时。
|
|
157
|
+
params[:output] = %w[itemid clock num value_min value_avg value_max]
|
|
158
|
+
end
|
|
159
|
+
rows = response_rows!(@manager.query(method: method, params: params), method)
|
|
160
|
+
raise ProtocolError, "#{method} exceeded the requested record limit" if rows.length > params[:limit]
|
|
161
|
+
|
|
162
|
+
points = rows.map { |row| parse_point(row, item, options) }
|
|
163
|
+
identities = points.map { |point| [point[:clock], point[:ns]] }
|
|
164
|
+
raise ProtocolError, "#{method} returned duplicate sample timestamps" unless identities.uniq == identities
|
|
165
|
+
|
|
166
|
+
points
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def parse_point(row, item, options)
|
|
170
|
+
id = response_integer!(row["itemid"], "itemid", positive: true).to_s
|
|
171
|
+
clock = response_integer!(row["clock"], "clock")
|
|
172
|
+
unless id == item[:itemid] && clock.between?(options[:time_from], options[:time_till])
|
|
173
|
+
raise ProtocolError, "traffic sample is outside the requested item or time range"
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
if options[:source] == :history
|
|
177
|
+
ns = response_integer!(row["ns"], "ns")
|
|
178
|
+
raise ProtocolError, "history.get returned invalid nanoseconds" unless ns < 1_000_000_000
|
|
179
|
+
|
|
180
|
+
value = item[:value_type] == 3 ? response_integer!(row["value"], "value") : decimal!(row["value"])
|
|
181
|
+
{ clock: clock, ns: ns, count: 1, value: value, min: value, max: value }
|
|
182
|
+
else
|
|
183
|
+
count = response_integer!(row["num"], "num", positive: true)
|
|
184
|
+
minimum, average, maximum = %w[value_min value_avg value_max].map { |field| decimal!(row[field]) }
|
|
185
|
+
unless minimum <= average && average <= maximum && (item[:value_type] != 3 || minimum >= 0)
|
|
186
|
+
raise ProtocolError, "trend.get returned inconsistent numeric statistics"
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
{ clock: clock, ns: 0, count: count, value: average, min: minimum, max: maximum }
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def response_rows!(rows, method)
|
|
194
|
+
unless rows.is_a?(Array) && rows.all? { |row| row.is_a?(Hash) }
|
|
195
|
+
raise ProtocolError, "#{method} must return an array of objects"
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
rows
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def response_integer!(value, field, positive: false)
|
|
202
|
+
unless value.is_a?(Integer) || (value.is_a?(String) && value.match?(/\A\d+\z/))
|
|
203
|
+
raise ProtocolError, "traffic response #{field} must be an integer"
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
integer = value.to_i
|
|
207
|
+
if integer.negative? || (positive && integer.zero?)
|
|
208
|
+
raise ProtocolError, "traffic response #{field} is outside its valid range"
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
integer
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def decimal!(value)
|
|
215
|
+
unless (value.is_a?(String) || value.is_a?(Numeric)) && DECIMAL.match?(value.to_s)
|
|
216
|
+
raise ProtocolError, "traffic response value must be a finite decimal"
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
number = BigDecimal(value.to_s)
|
|
220
|
+
raise ProtocolError, "traffic response value must be finite" unless number.finite?
|
|
221
|
+
|
|
222
|
+
number
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# 合并只影响展示密度;保留样本权重和极值,clock/ns 标示桶内最后一条记录。
|
|
226
|
+
def reduce_points(points, max_points)
|
|
227
|
+
return points if points.length <= max_points
|
|
228
|
+
|
|
229
|
+
size = (points.length.to_f / max_points).ceil
|
|
230
|
+
points.each_slice(size).map do |bucket|
|
|
231
|
+
count = bucket.sum { |point| point[:count] }
|
|
232
|
+
total = bucket.sum { |point| BigDecimal(point[:value].to_s) * point[:count] }
|
|
233
|
+
{ clock: bucket.last[:clock], ns: bucket.last[:ns], count: count, value: total / count,
|
|
234
|
+
min: bucket.map { |point| point[:min] }.min, max: bucket.map { |point| point[:max] }.max }
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
end
|