sqreen 1.19.1 → 1.21.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (70) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +18 -0
  3. data/lib/sqreen/agent_message.rb +20 -0
  4. data/lib/sqreen/aggregated_metric.rb +25 -0
  5. data/lib/sqreen/attack_detected.html +1 -2
  6. data/lib/sqreen/ca.crt +24 -0
  7. data/lib/sqreen/configuration.rb +10 -4
  8. data/lib/sqreen/deliveries/batch.rb +12 -2
  9. data/lib/sqreen/deliveries/simple.rb +4 -0
  10. data/lib/sqreen/ecosystem.rb +80 -0
  11. data/lib/sqreen/ecosystem/dispatch_table.rb +43 -0
  12. data/lib/sqreen/ecosystem/http/net_http.rb +51 -0
  13. data/lib/sqreen/ecosystem/http/rack_request.rb +38 -0
  14. data/lib/sqreen/ecosystem/loggable.rb +13 -0
  15. data/lib/sqreen/ecosystem/module_api.rb +30 -0
  16. data/lib/sqreen/ecosystem/module_api/event_listener.rb +18 -0
  17. data/lib/sqreen/ecosystem/module_api/instrumentation.rb +23 -0
  18. data/lib/sqreen/ecosystem/module_api/signal_producer.rb +26 -0
  19. data/lib/sqreen/ecosystem/module_api/tracing_push_down.rb +34 -0
  20. data/lib/sqreen/ecosystem/module_api/transaction_storage.rb +71 -0
  21. data/lib/sqreen/ecosystem/module_registry.rb +39 -0
  22. data/lib/sqreen/ecosystem/redis/redis_connection.rb +35 -0
  23. data/lib/sqreen/ecosystem/tracing/sampler.rb +160 -0
  24. data/lib/sqreen/ecosystem/tracing/sampling_configuration.rb +150 -0
  25. data/lib/sqreen/ecosystem/tracing/signals/tracing_client.rb +53 -0
  26. data/lib/sqreen/ecosystem/tracing/signals/tracing_server.rb +53 -0
  27. data/lib/sqreen/ecosystem/tracing_id_setup.rb +34 -0
  28. data/lib/sqreen/ecosystem/transaction_storage.rb +64 -0
  29. data/lib/sqreen/ecosystem_integration.rb +70 -0
  30. data/lib/sqreen/ecosystem_integration/around_callbacks.rb +89 -0
  31. data/lib/sqreen/ecosystem_integration/instrumentation_service.rb +38 -0
  32. data/lib/sqreen/ecosystem_integration/request_lifecycle_tracking.rb +56 -0
  33. data/lib/sqreen/ecosystem_integration/signal_consumption.rb +35 -0
  34. data/lib/sqreen/endpoint_testing.rb +184 -0
  35. data/lib/sqreen/event.rb +7 -5
  36. data/lib/sqreen/events/attack.rb +23 -18
  37. data/lib/sqreen/events/remote_exception.rb +0 -22
  38. data/lib/sqreen/events/request_record.rb +15 -70
  39. data/lib/sqreen/frameworks/generic.rb +15 -1
  40. data/lib/sqreen/frameworks/request_recorder.rb +13 -2
  41. data/lib/sqreen/graft/call.rb +9 -0
  42. data/lib/sqreen/kit/signals/specialized/aggregated_metric.rb +72 -0
  43. data/lib/sqreen/kit/signals/specialized/attack.rb +57 -0
  44. data/lib/sqreen/kit/signals/specialized/binning_metric.rb +76 -0
  45. data/lib/sqreen/kit/signals/specialized/http_trace.rb +26 -0
  46. data/lib/sqreen/kit/signals/specialized/sdk_track_call.rb +50 -0
  47. data/lib/sqreen/kit/signals/specialized/sqreen_exception.rb +57 -0
  48. data/lib/sqreen/legacy/old_event_submission_strategy.rb +227 -0
  49. data/lib/sqreen/legacy/waf_redactions.rb +49 -0
  50. data/lib/sqreen/log/loggable.rb +1 -1
  51. data/lib/sqreen/metrics/base.rb +3 -0
  52. data/lib/sqreen/metrics_store.rb +22 -12
  53. data/lib/sqreen/performance_notifications/binned_metrics.rb +8 -2
  54. data/lib/sqreen/remote_command.rb +3 -0
  55. data/lib/sqreen/rules.rb +4 -2
  56. data/lib/sqreen/rules/not_found_cb.rb +2 -0
  57. data/lib/sqreen/rules/rule_cb.rb +2 -0
  58. data/lib/sqreen/rules/waf_cb.rb +13 -10
  59. data/lib/sqreen/runner.rb +94 -13
  60. data/lib/sqreen/sensitive_data_redactor.rb +19 -31
  61. data/lib/sqreen/session.rb +53 -43
  62. data/lib/sqreen/signals/conversions.rb +288 -0
  63. data/lib/sqreen/signals/http_trace_redaction.rb +111 -0
  64. data/lib/sqreen/signals/signals_submission_strategy.rb +78 -0
  65. data/lib/sqreen/version.rb +1 -1
  66. data/lib/sqreen/weave/legacy/instrumentation.rb +4 -4
  67. metadata +74 -10
  68. data/lib/sqreen/backport.rb +0 -9
  69. data/lib/sqreen/backport/clock_gettime.rb +0 -74
  70. data/lib/sqreen/backport/original_name.rb +0 -88
@@ -61,7 +61,7 @@ module Sqreen
61
61
  obj.each do |k, v|
62
62
  ck = k.is_a?(String) ? k.downcase : k
63
63
  if @keys.include?(ck)
64
- redacted << v
64
+ redacted += SensitiveDataRedactor.all_strings(v)
65
65
  v = MASK
66
66
  else
67
67
  v, r = redact(v)
@@ -74,39 +74,27 @@ module Sqreen
74
74
  [result, redacted]
75
75
  end
76
76
 
77
- def redact_attacks!(attacks, values)
78
- return attacks if values.empty?
79
-
80
- values = values.map { |v| v.downcase if v.is_a?(String) }
81
-
82
- attacks.each do |e|
83
- next(e) unless e[:infos]
84
- next(e) unless e[:infos][:waf_data]
85
-
86
- parsed = JSON.parse(e[:infos][:waf_data])
87
- redacted = parsed.each do |w|
88
- next unless (filters = w['filter'])
89
-
90
- filters.each do |f|
91
- next unless (v = f['resolved_value'])
92
- next unless values.include?(v.downcase)
77
+ class << self
78
+ def all_strings(v)
79
+ accum = []
80
+ all_strings_impl(v, accum)
81
+ accum
82
+ end
93
83
 
94
- f['match_status'] = MASK
95
- f['resolved_value'] = MASK
84
+ private
85
+
86
+ def all_strings_impl(obj, accum)
87
+ case obj
88
+ when String
89
+ accum << obj
90
+ when Array
91
+ obj.each { |el| all_strings_impl(el, accum) }
92
+ when Hash
93
+ obj.each do |k, v|
94
+ all_strings_impl(k, accum)
95
+ all_strings_impl(v, accum)
96
96
  end
97
97
  end
98
- e[:infos][:waf_data] = JSON.dump(redacted)
99
- end
100
- end
101
-
102
- def redact_exceptions!(exceptions, values)
103
- return exceptions if values.empty?
104
-
105
- exceptions.each do |e|
106
- next(e) unless e[:infos]
107
- next(e) unless e[:infos][:waf]
108
-
109
- e[:infos][:waf].delete(:args)
110
98
  end
111
99
  end
112
100
  end
@@ -11,6 +11,10 @@ require 'sqreen/events/attack'
11
11
  require 'sqreen/events/request_record'
12
12
  require 'sqreen/exception'
13
13
  require 'sqreen/safe_json'
14
+ require 'sqreen/kit'
15
+ require 'sqreen/kit/configuration'
16
+ require 'sqreen/signals/signals_submission_strategy'
17
+ require 'sqreen/legacy/old_event_submission_strategy'
14
18
 
15
19
  require 'net/https'
16
20
  require 'uri'
@@ -41,13 +45,12 @@ module Sqreen
41
45
  RETRY_MANY = 301
42
46
 
43
47
  MUTEX = Mutex.new
44
- METRICS_KEY = 'metrics'.freeze
45
48
 
46
49
  @@path_prefix = '/sqreen/v0/'
47
50
 
48
51
  attr_accessor :request_compression
49
52
 
50
- def initialize(server_url, token, app_name = nil)
53
+ def initialize(server_url, cert_store, token, app_name = nil, proxy_url = nil)
51
54
  @token = token
52
55
  @app_name = app_name
53
56
  @session_id = nil
@@ -59,15 +62,29 @@ module Sqreen
59
62
  uri = parse_uri(server_url)
60
63
  use_ssl = (uri.scheme == 'https')
61
64
 
65
+ proxy_params = []
66
+ if proxy_url
67
+ proxy_uri = parse_uri(proxy_url)
68
+ proxy_params = [proxy_uri.host, proxy_uri.port, proxy_uri.user, proxy_uri.password]
69
+ end
70
+
62
71
  @req_nb = 0
63
72
 
64
- @http = Net::HTTP.new(uri.host, uri.port)
73
+ @http = Net::HTTP.new(uri.host, uri.port, *proxy_params)
65
74
  @http.use_ssl = use_ssl
66
- if use_ssl
67
- cert_file = File.join(File.dirname(__FILE__), 'ca.crt')
68
- cert_store = OpenSSL::X509::Store.new
69
- cert_store.add_file cert_file
70
- @http.cert_store = cert_store
75
+ @http.verify_mode = OpenSSL::SSL::VERIFY_NONE if ENV['SQREEN_SSL_NO_VERIFY'] # for testing
76
+ @http.cert_store = cert_store if use_ssl
77
+ self.use_signals = false
78
+ end
79
+
80
+ def use_signals=(do_use)
81
+ return if do_use == @use_signals
82
+
83
+ @use_signals = do_use
84
+ if do_use
85
+ @evt_sub_strategy = Sqreen::Signals::SignalsSubmissionStrategy.new
86
+ else
87
+ @evt_sub_strategy = Sqreen::Legacy::OldEventSubmissionStrategy.new(method(:post))
71
88
  end
72
89
  end
73
90
 
@@ -218,10 +235,7 @@ module Sqreen
218
235
  end
219
236
 
220
237
  def login(framework)
221
- headers = {
222
- 'x-api-key' => @token,
223
- 'x-app-name' => @app_name || framework.application_name,
224
- }.reject { |k, v| v == nil }
238
+ headers = prelogin_auth_headers(framework)
225
239
 
226
240
  Sqreen.log.warn "Using app name: #{headers['x-app-name']}"
227
241
 
@@ -235,6 +249,10 @@ module Sqreen
235
249
  end
236
250
  Sqreen.log.info 'Login success.'
237
251
  @session_id = res['session_id']
252
+
253
+ Kit::Configuration.session_key = @session_id
254
+ Kit.reset
255
+
238
256
  Sqreen.log.debug { "received session_id #{@session_id}" }
239
257
  Sqreen.logged_in = true
240
258
  res
@@ -246,20 +264,24 @@ module Sqreen
246
264
 
247
265
  def heartbeat(cmd_res = {}, metrics = [])
248
266
  payload = {}
249
- payload['metrics'] = metrics unless metrics.nil? || metrics.empty?
267
+ unless metrics.nil? || metrics.empty?
268
+ # never reached with signals
269
+ payload['metrics'] = metrics.map do |m|
270
+ Sqreen::Legacy::EventToHash.convert_agg_metric(m)
271
+ end
272
+ end
250
273
  payload['command_results'] = cmd_res unless cmd_res.nil? || cmd_res.empty?
251
274
 
252
275
  post('app-beat', payload.empty? ? nil : payload, {}, RETRY_MANY)
253
276
  end
254
277
 
255
278
  def post_metrics(metrics)
256
- return if metrics.nil? || metrics.empty?
257
- payload = { METRICS_KEY => metrics }
258
- post(METRICS_KEY, payload, {}, RETRY_MANY)
279
+ @evt_sub_strategy.post_metrics(metrics)
259
280
  end
260
281
 
282
+ # XXX never called
261
283
  def post_attack(attack)
262
- post('attack', attack.to_hash, {}, RETRY_MANY)
284
+ @evt_sub_strategy.post_attack(attack)
263
285
  end
264
286
 
265
287
  def post_bundle(bundle_sig, dependencies)
@@ -271,33 +293,22 @@ module Sqreen
271
293
  end
272
294
 
273
295
  def post_request_record(request_record)
274
- post('request_record', request_record.to_hash, {}, RETRY_MANY)
296
+ @evt_sub_strategy.post_request_record(request_record)
275
297
  end
276
298
 
277
299
  # Post an exception to Sqreen for analysis
278
300
  # @param exception [RemoteException] Exception and context to be sent over
279
301
  def post_sqreen_exception(exception)
280
- post('sqreen_exception', exception.to_hash, {}, 5)
281
- rescue StandardError => e
282
- Sqreen.log.warn(format('Could not post exception (network down? %s) %s',
283
- e.inspect,
284
- exception.to_hash.inspect))
285
- nil
302
+ @evt_sub_strategy.post_sqreen_exception(exception)
286
303
  end
287
304
 
288
- BATCH_KEY = 'batch'.freeze
289
- EVENT_TYPE_KEY = 'event_type'.freeze
290
305
  def post_batch(events)
291
- batch = events.map do |event|
292
- h = event.to_hash
293
- h[EVENT_TYPE_KEY] = event_kind(event)
294
- h
295
- end
296
- Sqreen.log.debug do
297
- tally = Hash[events.group_by(&:class).map{ |k,v| [k, v.count] }]
298
- "Doing batch with the following tally of event types: #{tally}"
299
- end
300
- post(BATCH_KEY, { BATCH_KEY => batch }, {}, RETRY_MANY)
306
+ @evt_sub_strategy.post_batch(events)
307
+ end
308
+
309
+ def post_agent_message(framework, agent_message)
310
+ headers = prelogin_auth_headers(framework)
311
+ post('app_agent_message', agent_message.to_h, headers, 0)
301
312
  end
302
313
 
303
314
  # Perform agent logout
@@ -314,14 +325,13 @@ module Sqreen
314
325
  disconnect
315
326
  end
316
327
 
317
- protected
328
+ private
318
329
 
319
- def event_kind(event)
320
- case event
321
- when Sqreen::RemoteException then 'sqreen_exception'
322
- when Sqreen::Attack then 'attack'
323
- when Sqreen::RequestRecord then 'request_record'
324
- end
330
+ def prelogin_auth_headers(framework)
331
+ {
332
+ 'x-api-key' => @token,
333
+ 'x-app-name' => @app_name || framework.application_name,
334
+ }.reject { |_k, v| v == nil }
325
335
  end
326
336
  end
327
337
  end
@@ -0,0 +1,288 @@
1
+ require 'sqreen/version'
2
+ require 'sqreen/rules/rule_cb'
3
+ require 'sqreen/metrics/base'
4
+ require 'sqreen/metrics/binning'
5
+ require 'sqreen/signals/http_trace_redaction'
6
+ require 'sqreen/kit/signals/signal_attributes'
7
+ require 'sqreen/kit/signals/specialized/aggregated_metric'
8
+ require 'sqreen/kit/signals/specialized/attack'
9
+ require 'sqreen/kit/signals/specialized/binning_metric'
10
+ require 'sqreen/kit/signals/specialized/sqreen_exception'
11
+ require 'sqreen/kit/signals/specialized/http_trace'
12
+ require 'sqreen/kit/signals/specialized/sdk_track_call'
13
+
14
+ module Sqreen
15
+ module Signals
16
+ module Conversions # rubocop:disable Metrics/ModuleLength
17
+ class << self
18
+ # @param [Sqreen::AggregatedMetric] agg
19
+ # @return [Sqreen::Kit::Signals::Metric]
20
+ def convert_metric_sample(agg)
21
+ attrs = {
22
+ signal_name: "sq.agent.metric.#{agg.name}",
23
+ source: if agg.rule
24
+ "sqreen:rules:#{agg.rule.rulespack_id}:#{agg.rule.rule_name}"
25
+ else
26
+ agent_gen_source
27
+ end,
28
+ time: agg.finish,
29
+ }
30
+
31
+ if agg.metric.is_a?(Sqreen::Metric::Binning)
32
+ conv_binning_metric(agg, attrs)
33
+ else
34
+ conv_generic_metric(agg, attrs)
35
+ end
36
+ end
37
+
38
+ # @param [Sqreen::Attack] attack
39
+ # XXX: not used because we don't use Sqreen::Attack
40
+ def convert_attack(attack)
41
+ # no need to set actor/context as we only include them in request records/traces
42
+ Kit::Signals::Specialized::Attack.new(
43
+ signal_name: "sq.agent.attack.#{attack.attack_type}",
44
+ source: "sqreen:rule:#{attack.rulespack_id}:#{attack.rule_name}",
45
+ time: attack.time,
46
+ location: Kit::Signals::Location.new(stack_trace: attack.backtrace),
47
+ payload: Kit::Signals::Specialized::Attack::Payload.new(
48
+ test: attack.test?,
49
+ block: attack.block?,
50
+ infos: attack.infos
51
+ )
52
+ )
53
+ end
54
+
55
+ # see Sqreen::Rules::RuleCB.record_event
56
+ def convert_unstructured_attack(payload)
57
+ Kit::Signals::Specialized::Attack.new(
58
+ signal_name: "sq.agent.attack.#{payload[:attack_type]}",
59
+ source: "sqreen:rule:#{payload[:rulespack_id]}:#{payload[:rule_name]}",
60
+ time: payload[:time],
61
+ location: (Kit::Signals::Location.new(stack_trace: payload[:backtrace]) if payload[:backtrace]),
62
+ payload: Kit::Signals::Specialized::Attack::Payload.new(
63
+ test: payload[:test],
64
+ block: payload[:block],
65
+ infos: payload[:infos]
66
+ )
67
+ )
68
+ end
69
+
70
+ # @param [Sqreen::RemoteException] exception
71
+ # @return [Sqreen::Kit::Signals::Specialized::SqreenException]
72
+ def convert_exception(exception)
73
+ payload = exception.payload
74
+
75
+ infos = payload['client_ip'] ? { client_ip: payload['client_ip'] } : {}
76
+ infos.merge!(payload['infos'] || {})
77
+
78
+ Kit::Signals::Specialized::SqreenException.new(
79
+ source: if payload['rule_name']
80
+ "sqreen:rule:#{payload['rulespack_id']}:#{payload['rule_name']}"
81
+ else
82
+ agent_gen_source
83
+ end,
84
+ time: exception.time,
85
+ ruby_exception: payload['exception'],
86
+ infos: infos
87
+ )
88
+ end
89
+
90
+ # see Sqreen::Rules::RuleCB.record_exception
91
+ # @param [Hash] payload
92
+ # @return [Sqreen::Kit::Signals::Specialized::SqreenException]
93
+ def convert_unstructured_exception(payload)
94
+ Kit::Signals::Specialized::SqreenException.new(
95
+ source: "sqreen:rule:#{payload[:rulespack_id]}:#{payload[:rule_name]}",
96
+ time: payload[:time],
97
+ ruby_exception: payload[:exception],
98
+ infos: payload[:infos]
99
+ )
100
+ end
101
+
102
+ # @param [Sqreen::RequestRecord] req_rec
103
+ # @return [Sqreen::Kit::Signals::Specialized::HttpTrace]
104
+ def convert_req_record(req_rec)
105
+ payload = req_rec.payload
106
+
107
+ request_p = payload['request']
108
+ id_args = req_rec.last_identify_args
109
+ identifiers = id_args[0] if id_args
110
+ traits = id_args[1] if id_args
111
+
112
+ observed = payload[:observed] || {}
113
+ signals = []
114
+ signals += (observed[:attacks] || [])
115
+ .map { |att| convert_unstructured_attack(att) }
116
+ signals += (observed[:sqreen_exceptions] || [])
117
+ .map { |sq_exc| convert_unstructured_exception(sq_exc) }
118
+ signals += req_rec.processed_sdk_calls
119
+ .select { |h| h[:name] == :track }
120
+ .map { |h| convert_track(h) }
121
+ signals += (observed[:signals] || [])
122
+
123
+ trace = Kit::Signals::Specialized::HttpTrace.new(
124
+ actor: Kit::Signals::Actor.new(
125
+ ip_addresses: [request_p[:client_ip]].compact,
126
+ user_agent: request_p[:user_agent],
127
+ identifiers: identifiers,
128
+ traits: traits,
129
+ ),
130
+ location_infra: location_infra,
131
+ context: convert_request(request_p,
132
+ payload['response'],
133
+ payload['headers'],
134
+ payload['params']),
135
+ data: signals
136
+ )
137
+ HttpTraceRedaction.redact_trace!(trace, req_rec.redactor)
138
+ trace
139
+ end
140
+
141
+ # @return [Array<Sqreen::Kit::Signals::Signal|Sqreen::Kit::Signals::Trace>]
142
+ def convert_batch(batch)
143
+ batch.map do |evt|
144
+ case evt
145
+ when RemoteException
146
+ convert_exception(evt)
147
+ when AggregatedMetric
148
+ convert_metric_sample(evt)
149
+ when RequestRecord
150
+ convert_req_record(evt)
151
+ when Sqreen::Kit::Signals::Signal
152
+ evt
153
+ when Sqreen::Kit::Signals::Trace
154
+ evt
155
+ else
156
+ raise NotImplementedError, "Unknown type of event in batch: #{evt}"
157
+ end
158
+ end
159
+ end
160
+
161
+ private
162
+
163
+ def agent_gen_source
164
+ "sqreen:agent:ruby:#{Sqreen::VERSION}"
165
+ end
166
+
167
+ def location_infra
168
+ @location_infra ||= begin
169
+ Kit::Signals::LocationInfra.new(
170
+ agent_version: Sqreen::VERSION,
171
+ os_type: RuntimeInfos.os[:os_type],
172
+ hostname: RuntimeInfos.hostname,
173
+ runtime_type: RuntimeInfos.runtime[:runtime_type],
174
+ runtime_version: RuntimeInfos.runtime[:runtime_version],
175
+ libsqreen_version: RuntimeInfos.libsqreen_version,
176
+ )
177
+ end
178
+ end
179
+
180
+ # see Sqreen::RequestRecord.processed_sdk_calls
181
+ def convert_track(call_info)
182
+ options = call_info[:args][1] || {}
183
+ Kit::Signals::Specialized::SdkTrackCall.new(
184
+ signal_name: "sq.sdk.#{call_info[:args][0]}",
185
+ time: call_info[:time],
186
+ payload: Kit::Signals::Specialized::SdkTrackCall::Payload.new(
187
+ properties: options[:properties],
188
+ user_identifiers: options[:user_identifiers]
189
+ )
190
+ )
191
+ end
192
+
193
+ # @param [Hash] req_payload
194
+ # @param [Hash] headers_payload
195
+ # @param [Hash] params_payload
196
+ # see the PayloadCreator abomination for reference
197
+ # TODO: do not convert from the old payload to the new payload
198
+ # Have an intermediate object that gets the data from the framework.
199
+ # (Or convert directly from the framework, but this needs to be
200
+ # done during the request, not just before event is transmitted)
201
+ def convert_request(req_payload, resp_payload, headers_payload, params_payload)
202
+ req_payload ||= {}
203
+ headers_payload ||= {}
204
+ resp_payload ||= {}
205
+ params_payload ||= {}
206
+
207
+ other = params_payload['other']
208
+ other = merge_hash_append(other, params_payload['rack'])
209
+ other = merge_hash_append(other, params_payload['grape_params'])
210
+ other = merge_hash_append(other, params_payload['rack_routing'])
211
+
212
+ Sqreen::Kit::Signals::Context::HttpContext.new(
213
+ {
214
+ rid: req_payload[:rid],
215
+ headers: headers_payload,
216
+ user_agent: req_payload[:user_agent],
217
+ scheme: req_payload[:scheme],
218
+ verb: req_payload[:verb],
219
+ host: req_payload[:host],
220
+ port: req_payload[:port],
221
+ remote_ip: req_payload[:remote_ip],
222
+ remote_port: req_payload[:remote_port] || 0,
223
+ path: req_payload[:path],
224
+ referer: req_payload[:referer],
225
+ params_query: params_payload['query'],
226
+ params_form: params_payload['form'],
227
+ params_other: other,
228
+ # endpoint, is_reveal_replayed not set
229
+ status: resp_payload[:status],
230
+ content_length: resp_payload[:content_length],
231
+ content_type: resp_payload[:content_type],
232
+ }
233
+ )
234
+ end
235
+
236
+ def merge_hash_append(hash1, hash2)
237
+ return nil if hash1.nil? && hash2.nil?
238
+ return hash1 if hash2.nil? || hash2.empty?
239
+ return hash2 if hash1.nil? || hash1.empty?
240
+
241
+ pairs = (hash1.keys + hash2.keys).map do |key|
242
+ values1 = hash1[key]
243
+ values2 = hash2[key]
244
+ values = [values1, values2].compact
245
+ values = values.first if values.size == 1
246
+ [key, values]
247
+ end
248
+ Hash[pairs]
249
+ end
250
+
251
+ # @param [Sqreen::AggregatedMetric] agg
252
+ # @param [Hash] attrs
253
+ def conv_generic_metric(agg, attrs)
254
+ attrs[:payload] = Kit::Signals::Specialized::AggregatedMetric::Payload.new(
255
+ kind: metric_kind(agg.metric),
256
+ capture_interval_s: agg.metric.period,
257
+ date_started: agg.start,
258
+ date_ended: agg.finish,
259
+ values: agg.data
260
+ )
261
+
262
+ Kit::Signals::Specialized::AggregatedMetric.new(attrs)
263
+ end
264
+
265
+ # @param [Sqreen::AggregatedMetric] agg
266
+ # @param [Hash] attrs
267
+ def conv_binning_metric(agg, attrs)
268
+ attrs[:payload] = Kit::Signals::Specialized::BinningMetric::Payload.new(
269
+ capture_interval_s: agg.metric.period,
270
+ date_started: agg.start,
271
+ date_ended: agg.finish,
272
+ base: agg.data['b'],
273
+ unit: agg.data['u'],
274
+ max: agg.data['v']['max'],
275
+ bins: agg.data['v'].reject { |k, _v| k == 'max' }
276
+ )
277
+
278
+ Kit::Signals::Specialized::BinningMetric.new(attrs)
279
+ end
280
+
281
+ # @param [Sqreen::Metric::Base] metric
282
+ def metric_kind(metric)
283
+ metric.class.name.sub(/.*::/, '').sub(/Metric$/, '')
284
+ end
285
+ end
286
+ end
287
+ end
288
+ end