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.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +31 -1
  3. data/README.md +69 -46
  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 +135 -89
  14. data/lib/zabbix_manager/classes/{httptests.rb → http_tests.rb} +2 -8
  15. data/lib/zabbix_manager/classes/items.rb +91 -87
  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 +5 -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 +42 -45
  27. data/lib/zabbix_manager/classes/user_groups.rb +63 -0
  28. data/lib/zabbix_manager/classes/user_macros.rb +110 -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 +148 -277
  32. data/lib/zabbix_manager/configuration.rb +107 -0
  33. data/lib/zabbix_manager/http_transport.rb +145 -97
  34. data/lib/zabbix_manager/log_sanitizer.rb +9 -19
  35. data/lib/zabbix_manager/monitoring/device.rb +393 -0
  36. data/lib/zabbix_manager/monitoring/expressions.rb +36 -0
  37. data/lib/zabbix_manager/monitoring/line.rb +175 -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 +132 -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 +235 -0
  46. data/lib/zabbix_manager/traffic.rb +238 -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,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
@@ -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.0"
5
5
  end