zabbix_manager 5.1.6 → 6.0.1

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.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +44 -1
  3. data/README.md +73 -45
  4. data/lib/zabbix_manager/classes/actions.rb +3 -11
  5. data/lib/zabbix_manager/classes/applications.rb +2 -8
  6. data/lib/zabbix_manager/classes/configurations.rb +4 -9
  7. data/lib/zabbix_manager/classes/{drules.rb → discovery_rules.rb} +2 -8
  8. data/lib/zabbix_manager/classes/errors.rb +3 -0
  9. data/lib/zabbix_manager/classes/events.rb +2 -1
  10. data/lib/zabbix_manager/classes/graphs.rb +19 -15
  11. data/lib/zabbix_manager/classes/{hostgroups.rb → host_groups.rb} +5 -11
  12. data/lib/zabbix_manager/classes/host_interfaces.rb +261 -0
  13. data/lib/zabbix_manager/classes/hosts.rb +139 -89
  14. data/lib/zabbix_manager/classes/{httptests.rb → http_tests.rb} +2 -8
  15. data/lib/zabbix_manager/classes/items.rb +139 -92
  16. data/lib/zabbix_manager/classes/maintenance.rb +2 -1
  17. data/lib/zabbix_manager/classes/{mediatypes.rb → media_types.rb} +2 -7
  18. data/lib/zabbix_manager/classes/problems.rb +11 -40
  19. data/lib/zabbix_manager/classes/proxies.rb +7 -15
  20. data/lib/zabbix_manager/classes/proxy_groups.rb +16 -0
  21. data/lib/zabbix_manager/classes/roles.rb +13 -19
  22. data/lib/zabbix_manager/classes/screens.rb +11 -30
  23. data/lib/zabbix_manager/classes/scripts.rb +2 -8
  24. data/lib/zabbix_manager/classes/server.rb +1 -0
  25. data/lib/zabbix_manager/classes/templates.rb +2 -65
  26. data/lib/zabbix_manager/classes/triggers.rb +62 -53
  27. data/lib/zabbix_manager/classes/user_groups.rb +63 -0
  28. data/lib/zabbix_manager/classes/user_macros.rb +111 -0
  29. data/lib/zabbix_manager/classes/users.rb +15 -54
  30. data/lib/zabbix_manager/classes/value_maps.rb +52 -0
  31. data/lib/zabbix_manager/client.rb +162 -279
  32. data/lib/zabbix_manager/configuration.rb +115 -0
  33. data/lib/zabbix_manager/http_transport.rb +145 -97
  34. data/lib/zabbix_manager/log_sanitizer.rb +18 -29
  35. data/lib/zabbix_manager/monitoring/device.rb +394 -0
  36. data/lib/zabbix_manager/monitoring/expressions.rb +36 -0
  37. data/lib/zabbix_manager/monitoring/line.rb +176 -0
  38. data/lib/zabbix_manager/monitoring/line_items.rb +110 -0
  39. data/lib/zabbix_manager/monitoring/line_lifecycle.rb +169 -0
  40. data/lib/zabbix_manager/monitoring/line_plan.rb +113 -0
  41. data/lib/zabbix_manager/monitoring/thresholds.rb +182 -0
  42. data/lib/zabbix_manager/monitoring/traffic_items.rb +133 -0
  43. data/lib/zabbix_manager/monitoring/validation.rb +92 -0
  44. data/lib/zabbix_manager/monitoring.rb +216 -642
  45. data/lib/zabbix_manager/resource.rb +262 -0
  46. data/lib/zabbix_manager/traffic.rb +239 -0
  47. data/lib/zabbix_manager/version.rb +1 -1
  48. data/lib/zabbix_manager.rb +66 -179
  49. data/zabbix_manager.gemspec +6 -22
  50. metadata +41 -190
  51. data/lib/zabbix_manager/basic/basic_alias.rb +0 -42
  52. data/lib/zabbix_manager/basic/basic_func.rb +0 -55
  53. data/lib/zabbix_manager/basic/basic_init.rb +0 -50
  54. data/lib/zabbix_manager/basic/basic_logic.rb +0 -239
  55. data/lib/zabbix_manager/classes/hostinterfaces.rb +0 -189
  56. data/lib/zabbix_manager/classes/usergroups.rb +0 -75
  57. data/lib/zabbix_manager/classes/usermacros.rb +0 -222
  58. data/lib/zabbix_manager/classes/valuemaps.rb +0 -26
@@ -0,0 +1,262 @@
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 = response_objects(dump_by_id(key.to_sym => id)).find do |item|
77
+ response_identifier(item[key]) == id.to_i
78
+ end
79
+ return id.to_i if current && attributes_match?(current, attributes)
80
+
81
+ response_id(update_raw([attributes]), expected: [id])
82
+ end
83
+
84
+ # 按业务标识查询完整对象。
85
+ # @param data [Hash] 包含 identify 指定的业务标识
86
+ # @return [Array<Hash>]
87
+ def get_full_data(data)
88
+ response_objects(get_raw(filter: identity_filter(data), output: "extend"))
89
+ end
90
+
91
+ # 按对象 ID 查询完整对象。
92
+ # @param data [Hash] 包含 key 指定的 ID 字段
93
+ # @return [Array<Hash>]
94
+ def dump_by_id(data)
95
+ attributes = data.deep_symbolize_keys
96
+ id = attributes[key.to_sym]
97
+ raise Invalid, "#{key} is required" if id.blank?
98
+
99
+ response_objects(get_raw(filter: { key.to_sym => id }, output: "extend"))
100
+ end
101
+
102
+ # @return [Hash] 业务标识到原生字符串 ID 的映射
103
+ # @raise [Conflict] 多个资源具有相同业务标识
104
+ def all
105
+ response_objects(get_raw(output: "extend")).each_with_object({}) do |item, result|
106
+ response_identifier(item[key])
107
+ raise ProtocolError, "invalid #{method_name} response: #{identify} is missing" unless item.key?(identify)
108
+
109
+ name = item.fetch(identify)
110
+ raise Conflict, "multiple #{method_name} objects share the same identity" if result.key?(name)
111
+
112
+ result[name] = item.fetch(key)
113
+ end
114
+ end
115
+
116
+ # 精确查找唯一资源,空结果返回 nil,多个结果拒绝任意选择。
117
+ # @param data [Hash] 包含 identify 字段的精确过滤条件
118
+ # @return [Integer, nil]
119
+ def get_id(data)
120
+ attributes = data.deep_symbolize_keys
121
+ name = attributes[identify.to_sym]
122
+ raise Invalid, "#{identify} not supplied in call to get_id" if name.nil?
123
+
124
+ rows = response_objects(get_raw(filter: attributes, output: [key, identify]))
125
+ rows.each do |item|
126
+ response_identifier(item[key])
127
+ raise ProtocolError, "invalid #{method_name} response: #{identify} is missing" unless item.key?(identify)
128
+ end
129
+ matches = rows.select do |item|
130
+ item[identify].to_s == name.to_s
131
+ end
132
+ raise Conflict, "multiple #{method_name} objects match the requested identity" if matches.length > 1
133
+
134
+ response_identifier(matches.first[key]) if matches.first
135
+ end
136
+
137
+ # 查找或创建单个对象,不隐含更新已有对象。
138
+ # @param data [Hash] 创建属性及 identify 指定的业务标识
139
+ # @return [Integer] 已存在或新创建对象的 ID
140
+ def get_or_create(data)
141
+ get_id(**identity_filter(data)) || create(data)
142
+ end
143
+
144
+ # @return [Hash] 精确查询条件,复合身份由资源类覆盖
145
+ def identity_filter(data)
146
+ attributes = data.deep_symbolize_keys
147
+ { identify.to_sym => attributes.fetch(identify.to_sym) }
148
+ end
149
+
150
+ # @param data [Hash] 原生 *.get 参数;调用方负责授权和资源边界
151
+ # @return [Object] 原始查询响应
152
+ def get_raw(data)
153
+ @client.api_request(method: "#{method_name}.get", params: data)
154
+ end
155
+
156
+ # @param data [Hash, Array<Hash>] 原生 *.create 参数,不执行高层业务校验
157
+ # @return [Object] 原始创建响应,调用方负责核对回执
158
+ def create_raw(data)
159
+ @client.api_request(method: "#{method_name}.create", params: data)
160
+ end
161
+
162
+ # @param data [Hash, Array<Hash>] 原生 *.update 参数,不执行高层业务校验
163
+ # @return [Object] 原始更新响应,调用方负责核对回执
164
+ def update_raw(data)
165
+ @client.api_request(method: "#{method_name}.update", params: data)
166
+ end
167
+
168
+ # @param data [Array<Integer, String>] 原生 *.delete ID 集合
169
+ # @return [Object] 原始删除响应,调用方负责核对回执
170
+ def delete_raw(data)
171
+ @client.api_request(method: "#{method_name}.delete", params: data)
172
+ end
173
+
174
+ private
175
+
176
+ # 高层查询必须区分空结果与损坏的响应;原生 get_raw 保持透传。
177
+ def response_objects(result)
178
+ unless result.is_a?(Array) && result.all? { |object| object.is_a?(Hash) }
179
+ raise ProtocolError, "invalid #{method_name} response: expected an array of objects"
180
+ end
181
+
182
+ result
183
+ end
184
+
185
+ # 单对象查询边界不能接受 ID 集合、布尔值或浮点数。
186
+ def positive_id_attribute(value, name)
187
+ id = integer_attribute(value, name)
188
+ raise Invalid, "#{name} must be a positive integer" unless id.positive?
189
+
190
+ id
191
+ end
192
+
193
+ def validate_boolean!(value, name)
194
+ raise Invalid, "#{name} must be true or false" unless value == true || value == false
195
+ end
196
+
197
+ def integer_attribute(value, name)
198
+ unless value.is_a?(Integer) || (value.is_a?(String) && value.match?(/\A[+-]?\d+\z/))
199
+ raise Invalid, "#{name} must be an integer"
200
+ end
201
+
202
+ value.is_a?(Integer) ? value : Integer(value, 10)
203
+ end
204
+
205
+ def normalized_ids(values, allow_empty: false)
206
+ ids = Array.wrap(values).map do |value|
207
+ id = integer_attribute(value, keys)
208
+ raise Invalid, "#{keys} must be positive integers" unless id.positive?
209
+
210
+ id.to_s
211
+ end.uniq
212
+ raise Invalid, "#{keys} are required" if ids.empty? && !allow_empty
213
+
214
+ ids
215
+ end
216
+
217
+ # API 数字字符串可与整数比较,nil 和布尔值保留各自语义。
218
+ def normalized_attributes(attributes)
219
+ attributes.deep_symbolize_keys.deep_transform_values do |value|
220
+ value.is_a?(Numeric) ? value.to_s : value
221
+ end
222
+ end
223
+
224
+ def attributes_match?(current, desired)
225
+ actual = normalized_attributes(current)
226
+ expected = normalized_attributes(desired)
227
+ actual.slice(*expected.keys) == expected
228
+ end
229
+
230
+ def response_identifier(value, id_field = key)
231
+ unless value.is_a?(Integer) || (value.is_a?(String) && value.match?(/\A[0-9]+\z/))
232
+ raise ProtocolError, "invalid #{method_name} response: #{id_field} must be a positive integer"
233
+ end
234
+
235
+ id = value.to_i
236
+ raise ProtocolError, "invalid #{method_name} response: #{id_field} must be positive" unless id.positive?
237
+
238
+ id
239
+ end
240
+
241
+ def response_ids(result, id_field = keys, expected: nil)
242
+ values = result[id_field] if result.is_a?(Hash)
243
+ unless values.is_a?(Array) && values.any?
244
+ raise ProtocolError, "invalid #{method_name} response: #{id_field} must be a nonempty array"
245
+ end
246
+
247
+ ids = values.map { |value| response_identifier(value, id_field) }
248
+ if ids.uniq != ids || (expected && ids.sort != expected.map { |id| response_identifier(id) }.sort)
249
+ raise ProtocolError, "invalid #{method_name} response: #{id_field} do not confirm the requested objects"
250
+ end
251
+
252
+ ids
253
+ end
254
+
255
+ def response_id(result, id_field = keys, expected: nil)
256
+ ids = response_ids(result, id_field, expected: expected)
257
+ raise ProtocolError, "invalid #{method_name} response: expected one #{key}" unless ids.one?
258
+
259
+ ids.first
260
+ end
261
+ end
262
+ end
@@ -0,0 +1,239 @@
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
+ requested_ids = ids.to_h { |id| [id, true] }
112
+ rows = @manager.query(method: "item.get", params: {
113
+ hostids: [hostid], itemids: ids, monitored: true,
114
+ output: %w[itemid hostid name units value_type]
115
+ })
116
+ response_rows!(rows, "item.get").each_with_object({}) do |item, result|
117
+ id = response_integer!(item["itemid"], "itemid", positive: true).to_s
118
+ owner = response_integer!(item["hostid"], "hostid", positive: true).to_s
119
+ unless requested_ids.key?(id) && owner == hostid && !result.key?(id)
120
+ raise ProtocolError, "item.get returned unexpected or duplicate item ownership"
121
+ end
122
+ unless item["name"].is_a?(String) && item["units"].is_a?(String)
123
+ raise ProtocolError, "item.get returned invalid name or units"
124
+ end
125
+
126
+ result[id] = { itemid: id, name: item["name"], units: item["units"],
127
+ value_type: response_integer!(item["value_type"], "value_type") }
128
+ end
129
+ end
130
+
131
+ def item_series(id, metadata, options)
132
+ result = { itemid: id, name: nil, units: nil, value_type: nil, status: :unavailable,
133
+ truncated: false, record_count: 0, sample_count: 0,
134
+ current_value: nil, peak_value: nil, points: [] }
135
+ return result unless metadata
136
+
137
+ result.merge!(metadata)
138
+ return result.merge(status: :unsupported) unless [0, 3].include?(metadata[:value_type])
139
+
140
+ points = read_points(metadata, options)
141
+ truncated = points.length > options[:limit]
142
+ points = points.sort_by { |point| [point[:clock], point[:ns]] }.last(options[:limit])
143
+ result.merge(status: points.empty? ? :empty : :ok, truncated: truncated,
144
+ record_count: points.length, sample_count: points.sum { |point| point[:count] },
145
+ current_value: points.last&.fetch(:value), peak_value: points.map { |point| point[:max] }.max,
146
+ points: reduce_points(points, options[:max_points]))
147
+ end
148
+
149
+ def read_points(item, options)
150
+ history = options[:source] == :history
151
+ method = history ? "history.get" : "trend.get"
152
+ params = options.slice(:time_from, :time_till).merge(itemids: [item[:itemid]], limit: options[:limit] + 1)
153
+ if history
154
+ params.merge!(history: item[:value_type], output: %w[itemid clock ns value],
155
+ sortfield: %w[clock ns], sortorder: "DESC")
156
+ else
157
+ # trend.get 没有排序参数;截断的趋势集合不承诺覆盖整段或包含最新小时。
158
+ params[:output] = %w[itemid clock num value_min value_avg value_max]
159
+ end
160
+ rows = response_rows!(@manager.query(method: method, params: params), method)
161
+ raise ProtocolError, "#{method} exceeded the requested record limit" if rows.length > params[:limit]
162
+
163
+ points = rows.map { |row| parse_point(row, item, options) }
164
+ identities = points.map { |point| [point[:clock], point[:ns]] }
165
+ raise ProtocolError, "#{method} returned duplicate sample timestamps" unless identities.uniq == identities
166
+
167
+ points
168
+ end
169
+
170
+ def parse_point(row, item, options)
171
+ id = response_integer!(row["itemid"], "itemid", positive: true).to_s
172
+ clock = response_integer!(row["clock"], "clock")
173
+ unless id == item[:itemid] && clock.between?(options[:time_from], options[:time_till])
174
+ raise ProtocolError, "traffic sample is outside the requested item or time range"
175
+ end
176
+
177
+ if options[:source] == :history
178
+ ns = response_integer!(row["ns"], "ns")
179
+ raise ProtocolError, "history.get returned invalid nanoseconds" unless ns < 1_000_000_000
180
+
181
+ value = item[:value_type] == 3 ? response_integer!(row["value"], "value") : decimal!(row["value"])
182
+ { clock: clock, ns: ns, count: 1, value: value, min: value, max: value }
183
+ else
184
+ count = response_integer!(row["num"], "num", positive: true)
185
+ minimum, average, maximum = %w[value_min value_avg value_max].map { |field| decimal!(row[field]) }
186
+ unless minimum <= average && average <= maximum && (item[:value_type] != 3 || minimum >= 0)
187
+ raise ProtocolError, "trend.get returned inconsistent numeric statistics"
188
+ end
189
+
190
+ { clock: clock, ns: 0, count: count, value: average, min: minimum, max: maximum }
191
+ end
192
+ end
193
+
194
+ def response_rows!(rows, method)
195
+ unless rows.is_a?(Array) && rows.all? { |row| row.is_a?(Hash) }
196
+ raise ProtocolError, "#{method} must return an array of objects"
197
+ end
198
+
199
+ rows
200
+ end
201
+
202
+ def response_integer!(value, field, positive: false)
203
+ unless value.is_a?(Integer) || (value.is_a?(String) && value.match?(/\A\d+\z/))
204
+ raise ProtocolError, "traffic response #{field} must be an integer"
205
+ end
206
+
207
+ integer = value.to_i
208
+ if integer.negative? || (positive && integer.zero?)
209
+ raise ProtocolError, "traffic response #{field} is outside its valid range"
210
+ end
211
+
212
+ integer
213
+ end
214
+
215
+ def decimal!(value)
216
+ unless (value.is_a?(String) || value.is_a?(Numeric)) && DECIMAL.match?(value.to_s)
217
+ raise ProtocolError, "traffic response value must be a finite decimal"
218
+ end
219
+
220
+ number = BigDecimal(value.to_s)
221
+ raise ProtocolError, "traffic response value must be finite" unless number.finite?
222
+
223
+ number
224
+ end
225
+
226
+ # 合并只影响展示密度;保留样本权重和极值,clock/ns 标示桶内最后一条记录。
227
+ def reduce_points(points, max_points)
228
+ return points if points.length <= max_points
229
+
230
+ size = (points.length.to_f / max_points).ceil
231
+ points.each_slice(size).map do |bucket|
232
+ count = bucket.sum { |point| point[:count] }
233
+ total = bucket.sum { |point| BigDecimal(point[:value].to_s) * point[:count] }
234
+ { clock: bucket.last[:clock], ns: bucket.last[:ns], count: count, value: total / count,
235
+ min: bucket.map { |point| point[:min] }.min, max: bucket.map { |point| point[:max] }.max }
236
+ end
237
+ end
238
+ end
239
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class ZabbixManager
4
- VERSION = "5.1.6"
4
+ VERSION = "6.0.1"
5
5
  end