sqreen 1.19.1-java → 1.21.0.beta3-java

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 (101) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +34 -0
  3. data/lib/sqreen/actions/block_user.rb +1 -1
  4. data/lib/sqreen/actions/redirect_ip.rb +1 -1
  5. data/lib/sqreen/actions/redirect_user.rb +1 -1
  6. data/lib/sqreen/agent_message.rb +20 -0
  7. data/lib/sqreen/aggregated_metric.rb +25 -0
  8. data/lib/sqreen/attack_detected.html +1 -2
  9. data/lib/sqreen/ca.crt +24 -0
  10. data/lib/sqreen/condition_evaluator.rb +9 -2
  11. data/lib/sqreen/conditionable.rb +24 -6
  12. data/lib/sqreen/configuration.rb +11 -5
  13. data/lib/sqreen/deferred_logger.rb +50 -14
  14. data/lib/sqreen/deliveries/batch.rb +12 -2
  15. data/lib/sqreen/deliveries/simple.rb +4 -0
  16. data/lib/sqreen/deprecation.rb +38 -0
  17. data/lib/sqreen/ecosystem.rb +96 -0
  18. data/lib/sqreen/ecosystem/dispatch_table.rb +43 -0
  19. data/lib/sqreen/ecosystem/exception_reporting.rb +26 -0
  20. data/lib/sqreen/ecosystem/http/net_http.rb +50 -0
  21. data/lib/sqreen/ecosystem/http/rack_request.rb +39 -0
  22. data/lib/sqreen/ecosystem/loggable.rb +13 -0
  23. data/lib/sqreen/ecosystem/module_api.rb +30 -0
  24. data/lib/sqreen/ecosystem/module_api/event_listener.rb +18 -0
  25. data/lib/sqreen/ecosystem/module_api/instrumentation.rb +23 -0
  26. data/lib/sqreen/ecosystem/module_api/message_producer.rb +51 -0
  27. data/lib/sqreen/ecosystem/module_api/signal_producer.rb +24 -0
  28. data/lib/sqreen/ecosystem/module_api/tracing.rb +45 -0
  29. data/lib/sqreen/ecosystem/module_api/tracing/client_data.rb +31 -0
  30. data/lib/sqreen/ecosystem/module_api/tracing/server_data.rb +27 -0
  31. data/lib/sqreen/ecosystem/module_api/tracing_id_generation.rb +16 -0
  32. data/lib/sqreen/ecosystem/module_api/transaction_storage.rb +71 -0
  33. data/lib/sqreen/ecosystem/module_registry.rb +44 -0
  34. data/lib/sqreen/ecosystem/redis/redis_connection.rb +43 -0
  35. data/lib/sqreen/ecosystem/tracing/modules/client.rb +31 -0
  36. data/lib/sqreen/ecosystem/tracing/modules/server.rb +30 -0
  37. data/lib/sqreen/ecosystem/tracing/sampler.rb +160 -0
  38. data/lib/sqreen/ecosystem/tracing/sampling_configuration.rb +150 -0
  39. data/lib/sqreen/ecosystem/tracing/signals/tracing_client.rb +53 -0
  40. data/lib/sqreen/ecosystem/tracing/signals/tracing_server.rb +53 -0
  41. data/lib/sqreen/ecosystem/tracing_broker.rb +101 -0
  42. data/lib/sqreen/ecosystem/tracing_id_setup.rb +34 -0
  43. data/lib/sqreen/ecosystem/transaction_storage.rb +64 -0
  44. data/lib/sqreen/ecosystem/util/call_writers_from_init.rb +13 -0
  45. data/lib/sqreen/ecosystem_integration.rb +87 -0
  46. data/lib/sqreen/ecosystem_integration/around_callbacks.rb +99 -0
  47. data/lib/sqreen/ecosystem_integration/instrumentation_service.rb +42 -0
  48. data/lib/sqreen/ecosystem_integration/request_lifecycle_tracking.rb +58 -0
  49. data/lib/sqreen/ecosystem_integration/signal_consumption.rb +35 -0
  50. data/lib/sqreen/endpoint_testing.rb +184 -0
  51. data/lib/sqreen/event.rb +7 -5
  52. data/lib/sqreen/events/attack.rb +23 -18
  53. data/lib/sqreen/events/remote_exception.rb +0 -22
  54. data/lib/sqreen/events/request_record.rb +15 -71
  55. data/lib/sqreen/frameworks/generic.rb +24 -1
  56. data/lib/sqreen/frameworks/rails.rb +0 -7
  57. data/lib/sqreen/frameworks/request_recorder.rb +15 -2
  58. data/lib/sqreen/graft/call.rb +106 -19
  59. data/lib/sqreen/graft/callback.rb +1 -1
  60. data/lib/sqreen/graft/hook.rb +212 -100
  61. data/lib/sqreen/graft/hook_point.rb +18 -11
  62. data/lib/sqreen/kit/signals/specialized/aggregated_metric.rb +72 -0
  63. data/lib/sqreen/kit/signals/specialized/attack.rb +57 -0
  64. data/lib/sqreen/kit/signals/specialized/binning_metric.rb +76 -0
  65. data/lib/sqreen/kit/signals/specialized/http_trace.rb +26 -0
  66. data/lib/sqreen/kit/signals/specialized/sdk_track_call.rb +50 -0
  67. data/lib/sqreen/kit/signals/specialized/sqreen_exception.rb +57 -0
  68. data/lib/sqreen/legacy/instrumentation.rb +22 -10
  69. data/lib/sqreen/legacy/old_event_submission_strategy.rb +228 -0
  70. data/lib/sqreen/legacy/waf_redactions.rb +49 -0
  71. data/lib/sqreen/log.rb +3 -2
  72. data/lib/sqreen/log/loggable.rb +2 -1
  73. data/lib/sqreen/logger.rb +24 -0
  74. data/lib/sqreen/metrics.rb +1 -0
  75. data/lib/sqreen/metrics/base.rb +3 -0
  76. data/lib/sqreen/metrics/req_detailed.rb +41 -0
  77. data/lib/sqreen/metrics_store.rb +33 -12
  78. data/lib/sqreen/null_logger.rb +22 -0
  79. data/lib/sqreen/performance_notifications/binned_metrics.rb +8 -2
  80. data/lib/sqreen/remote_command.rb +4 -0
  81. data/lib/sqreen/rules.rb +12 -6
  82. data/lib/sqreen/rules/blacklist_ips_cb.rb +2 -2
  83. data/lib/sqreen/rules/custom_error_cb.rb +3 -3
  84. data/lib/sqreen/rules/not_found_cb.rb +2 -0
  85. data/lib/sqreen/rules/rule_cb.rb +6 -2
  86. data/lib/sqreen/rules/waf_cb.rb +16 -13
  87. data/lib/sqreen/runner.rb +138 -16
  88. data/lib/sqreen/sensitive_data_redactor.rb +19 -31
  89. data/lib/sqreen/session.rb +53 -43
  90. data/lib/sqreen/signals/conversions.rb +288 -0
  91. data/lib/sqreen/signals/http_trace_redaction.rb +111 -0
  92. data/lib/sqreen/signals/signals_submission_strategy.rb +78 -0
  93. data/lib/sqreen/version.rb +1 -1
  94. data/lib/sqreen/weave/budget.rb +35 -0
  95. data/lib/sqreen/weave/legacy/instrumentation.rb +277 -135
  96. data/lib/sqreen/worker.rb +6 -2
  97. metadata +86 -10
  98. data/lib/sqreen/backport.rb +0 -9
  99. data/lib/sqreen/backport/clock_gettime.rb +0 -74
  100. data/lib/sqreen/backport/original_name.rb +0 -88
  101. data/lib/sqreen/encoding_sanitizer.rb +0 -27
@@ -8,17 +8,19 @@
8
8
  module Sqreen
9
9
  # Master interface for point in time events (e.g. Attack, RemoteException)
10
10
  class Event
11
+ # @return [Hash]
11
12
  attr_reader :payload
13
+
14
+ # @return [Time]
15
+ attr_accessor :time # writer used only in tests
16
+
12
17
  def initialize(payload)
13
18
  @payload = payload
14
- end
15
-
16
- def to_hash
17
- payload.to_hash
19
+ @time = Time.now.utc
18
20
  end
19
21
 
20
22
  def to_s
21
- "<#{self.class.name}: #{to_hash}>"
23
+ "<#{self.class.name}: #{payload.to_hash}>"
22
24
  end
23
25
  end
24
26
  end
@@ -11,6 +11,8 @@ module Sqreen
11
11
  # Attack
12
12
  # When creating a new attack, it gets automatically pushed to the event's
13
13
  # queue.
14
+ # XXX: TURNS OUT THIS CLASS IS ACTUALLY NOT USED ANYMORE
15
+ # Framework.observe is used instead with unstructured attack details
14
16
  class Attack < Event
15
17
  def self.record(payload)
16
18
  attack = Attack.new(payload)
@@ -26,11 +28,31 @@ module Sqreen
26
28
  payload['rule']['rulespack_id']
27
29
  end
28
30
 
29
- def type
31
+ def rule_name
30
32
  return nil unless payload['rule']
31
33
  payload['rule']['name']
32
34
  end
33
35
 
36
+ def test?
37
+ return nil unless payload['rule']
38
+ payload['rule']['test'] ? true : false
39
+ end
40
+
41
+ def beta?
42
+ return nil unless payload['rule']
43
+ payload['rule']['beta'] ? true : false
44
+ end
45
+
46
+ def block?
47
+ return nil unless payload['rule']
48
+ payload['rule']['block'] ? true : false
49
+ end
50
+
51
+ def attack_type
52
+ return nil unless payload['rule']
53
+ payload['rule']['attack_type']
54
+ end
55
+
34
56
  def time
35
57
  return nil unless payload['local']
36
58
  payload['local']['time']
@@ -44,22 +66,5 @@ module Sqreen
44
66
  def enqueue
45
67
  Sqreen.queue.push(self)
46
68
  end
47
-
48
- def to_hash
49
- res = {}
50
- rule_p = payload['rule']
51
- request_p = payload['request']
52
- res[:rule_name] = rule_p['name'] if rule_p && rule_p['name']
53
- res[:rulespack_id] = rule_p['rulespack_id'] if rule_p && rule_p['rulespack_id']
54
- res[:test] = rule_p['test'] if rule_p && rule_p['test']
55
- res[:infos] = payload['infos'] if payload['infos']
56
- res[:time] = time if time
57
- res[:client_ip] = request_p[:addr] if request_p && request_p[:addr]
58
- res[:request] = request_p if request_p
59
- res[:params] = payload['params'] if payload['params']
60
- res[:context] = payload['context'] if payload['context']
61
- res[:headers] = payload['headers'] if payload['headers']
62
- res
63
- end
64
69
  end
65
70
  end
@@ -30,27 +30,5 @@ module Sqreen
30
30
  def klass
31
31
  payload['exception'].class.name
32
32
  end
33
-
34
- def to_hash
35
- exception = payload['exception']
36
- ev = {
37
- :klass => exception.class.name,
38
- :message => exception.message,
39
- :params => payload['request_params'],
40
- :time => payload['time'],
41
- :infos => {
42
- :client_ip => payload['client_ip'],
43
- },
44
- :request => payload['request_infos'],
45
- :headers => payload['headers'],
46
- :rule_name => payload['rule_name'],
47
- :rulespack_id => payload['rulespack_id'],
48
- }
49
-
50
- ev[:infos].merge!(payload['infos']) if payload['infos']
51
- return ev unless exception.backtrace
52
- ev[:context] = { :backtrace => exception.backtrace.map(&:to_s) }
53
- ev
54
- end
55
33
  end
56
34
  end
@@ -8,12 +8,15 @@
8
8
  require 'json'
9
9
  require 'sqreen/log'
10
10
  require 'sqreen/event'
11
- require 'sqreen/encoding_sanitizer'
12
11
  require 'sqreen/sensitive_data_redactor'
13
12
 
14
13
  module Sqreen
15
14
  # When a request is deeemed worthy of being sent to the backend
16
15
  class RequestRecord < Sqreen::Event
16
+ attr_reader :redactor
17
+
18
+ # @param [Hash] payload
19
+ # @param [Sqreen::SensitiveDataRedactor] redactor
17
20
  def initialize(payload, redactor = nil)
18
21
  @redactor = redactor
19
22
  super(payload)
@@ -23,74 +26,18 @@ module Sqreen
23
26
  (payload && payload[:observed]) || {}
24
27
  end
25
28
 
26
- def to_hash
27
- res = { :version => '20171208' }
28
- if payload[:observed]
29
- res[:observed] = payload[:observed].dup
30
- rulespack = nil
31
- if observed[:attacks]
32
- res[:observed][:attacks] = observed[:attacks].map do |att|
33
- natt = att.dup
34
- rulespack = natt.delete(:rulespack_id) || rulespack
35
- natt
36
- end
37
- end
38
- if observed[:sqreen_exceptions]
39
- res[:observed][:sqreen_exceptions] = observed[:sqreen_exceptions].map do |exc|
40
- nex = exc.dup
41
- excp = nex.delete(:exception)
42
- if excp
43
- nex[:message] = excp.message
44
- nex[:klass] = excp.class.name
45
- end
46
- rulespack = nex.delete(:rulespack_id) || rulespack
47
- nex
48
- end
49
- end
50
- res[:rulespack_id] = rulespack unless rulespack.nil?
51
- if observed[:observations]
52
- res[:observed][:observations] = observed[:observations].map do |cat, key, value, time|
53
- { :category => cat, :key => key, :value => value, :time => time }
54
- end
55
- end
56
- if observed[:sdk]
57
- res[:observed][:sdk] = processed_sdk_calls
58
- end
59
- end
60
- res[:local] = payload['local'] if payload['local']
61
- if payload['request']
62
- res[:request] = payload['request'].dup
63
- res[:client_ip] = res[:request].delete(:client_ip) if res[:request][:client_ip]
64
- else
65
- res[:request] = {}
66
- end
67
- if payload['response']
68
- res[:response] = payload['response'].dup
69
- else
70
- res[:response] = {}
71
- end
72
-
73
- res[:request][:parameters] = payload['params'] if payload['params']
74
- res[:request][:headers] = payload['headers'] if payload['headers']
75
-
76
- res = Sqreen::EncodingSanitizer.sanitize(res)
29
+ def last_identify_args
30
+ return nil unless observed[:sdk]
77
31
 
78
- if @redactor
79
- res[:request], redacted = @redactor.redact(res[:request])
80
- if redacted.any? && res[:observed] && res[:observed][:attacks]
81
- res[:observed][:attacks] = @redactor.redact_attacks!(res[:observed][:attacks], redacted)
82
- end
83
- if redacted.any? && res[:observed] && res[:observed][:sqreen_exceptions]
84
- res[:observed][:sqreen_exceptions] = @redactor.redact_exceptions!(res[:observed][:sqreen_exceptions], redacted)
85
- end
32
+ observed[:sdk].reverse_each do |meth, _time, *args|
33
+ next unless meth == :identify
34
+ return args
86
35
  end
87
-
88
- res
36
+ nil
89
37
  end
90
38
 
91
- private
92
-
93
39
  def processed_sdk_calls
40
+ return [] unless observed[:sdk]
94
41
  auth_keys = last_identify_id
95
42
 
96
43
  observed[:sdk].map do |meth, time, *args|
@@ -102,6 +49,8 @@ module Sqreen
102
49
  end
103
50
  end
104
51
 
52
+ private
53
+
105
54
  def inject_identifiers(args, meth, auth_keys)
106
55
  return args unless meth == :track && auth_keys
107
56
 
@@ -118,13 +67,8 @@ module Sqreen
118
67
  end
119
68
 
120
69
  def last_identify_id
121
- return nil unless observed[:sdk]
122
-
123
- observed[:sdk].reverse_each do |meth, _time, *args|
124
- next unless meth == :identify
125
- return args.first if args.respond_to? :first
126
- end
127
- nil
70
+ args = last_identify_args
71
+ args.first if args.respond_to? :first
128
72
  end
129
73
  end
130
74
  end
@@ -22,8 +22,17 @@ module Sqreen
22
22
  include RequestRecorder
23
23
  attr_accessor :sqreen_configuration
24
24
 
25
+ attr_writer :req_start_cb, :req_end_cb
26
+
25
27
  def initialize
26
28
  clean_request_record
29
+
30
+ # for notifying the ecosystem of request boundaries
31
+ # XXX: this should be refactored. It shouldn't be
32
+ # the framework doing these notifications to the ecosystem
33
+ # Probably the rule callback should do it itself
34
+ @req_start_cb = Proc.new {}
35
+ @req_end_cb = Proc.new {}
27
36
  end
28
37
 
29
38
  # What kind of database is this
@@ -209,7 +218,16 @@ module Sqreen
209
218
 
210
219
  # Should the agent not be starting up?
211
220
  def prevent_startup
221
+ # SQREEN-880 - prevent Sqreen startup on Sidekiq workers
222
+ return :sidekiq_cli if defined?(Sidekiq::CLI)
223
+ return :delayed_job if defined?(Delayed::Command)
224
+
225
+ # Prevent Sqreen startup on rake tasks - unless this is a Sqreen test
226
+ run_in_test = sqreen_configuration.get(:run_in_test)
227
+ return :rake if !run_in_test && $0.end_with?('rake')
228
+
212
229
  return :irb if $0 == 'irb'
230
+
213
231
  return if sqreen_configuration.nil?
214
232
  disable = sqreen_configuration.get(:disable)
215
233
  return :config_disable if disable == true || disable.to_s.to_i == 1
@@ -251,8 +269,12 @@ module Sqreen
251
269
  # Nota: cleanup should be performed at end of request (see clean_request)
252
270
  def store_request(object)
253
271
  return unless ensure_rack_loaded
272
+
273
+ rack_req = Rack::Request.new(object)
274
+ @req_start_cb.call(rack_req)
275
+
254
276
  self.remaining_perf_budget = Sqreen.performance_budget
255
- SharedStorage.set(:request, Rack::Request.new(object))
277
+ SharedStorage.set(:request, rack_req)
256
278
  SharedStorage.set(:xss_params, nil)
257
279
  SharedStorage.set(:whitelisted, nil)
258
280
  SharedStorage.set(:request_overtime, nil)
@@ -281,6 +303,7 @@ module Sqreen
281
303
  SharedStorage.set(:xss_params, nil)
282
304
  SharedStorage.set(:whitelisted, nil)
283
305
  SharedStorage.set(:request_overtime, nil)
306
+ @req_end_cb.call
284
307
  end
285
308
 
286
309
  def remaining_perf_budget
@@ -103,13 +103,6 @@ module Sqreen
103
103
  run_in_test = sqreen_configuration.get(:run_in_test)
104
104
  return :rails_test if !run_in_test && (Rails.env.test? || Rails.env.cucumber?)
105
105
 
106
- # SQREEN-880 - prevent Sqreen startup on Sidekiq workers
107
- return :sidekiq_cli if defined?(Sidekiq::CLI)
108
- return :delayed_job if defined?(Delayed::Command)
109
-
110
- # Prevent Sqreen startup on rake tasks - unless this is a Sqreen test
111
- return :rake if !run_in_test && $0.end_with?('rake')
112
-
113
106
  return nil unless defined?(Rails::CommandsTasks)
114
107
  return nil if defined?(Rails::Server)
115
108
  return :rails_console if defined?(Rails::Console)
@@ -58,12 +58,22 @@ module Sqreen
58
58
  Sqreen.log.debug { "close_request_record called. observed_items: #{observed_items}" }
59
59
 
60
60
  clean_request_record if observed_items.nil?
61
- if only_metric_observation
61
+ if Sqreen.features['use_signals'] || only_metric_observation
62
62
  push_metrics(observations_queue, queue)
63
- return clean_request_record
64
63
  end
64
+
65
+ if only_metric_observation
66
+ clean_request_record
67
+ return
68
+ end
69
+
70
+ # signals require request section to be present
71
+ payload_requests << 'request'
72
+ # for signals, response is optional, but the backend team wants them
73
+ payload_requests << 'response'
65
74
  payload = payload_creator.payload(payload_requests)
66
75
  payload[:observed] = observed_items
76
+
67
77
  queue.push create_request_record(payload)
68
78
  clean_request_record
69
79
  end
@@ -79,10 +89,13 @@ module Sqreen
79
89
  @redactor ||= SensitiveDataRedactor.from_config
80
90
  end
81
91
 
92
+ # pushes metric observations to the observations queue
93
+ # and clears the list for the request record
82
94
  def push_metrics(observations_queue, event_queue)
83
95
  observed_items[:observations].each do |obs|
84
96
  observations_queue.push obs
85
97
  end
98
+ observed_items[:observations] = []
86
99
  return unless observations_queue.size > MAX_OBS_QUEUE_LENGTH / 2
87
100
  event_queue.push Sqreen::METRICS_EVENT
88
101
  end
@@ -27,6 +27,10 @@ module Sqreen
27
27
  def raise(value)
28
28
  Flow.raise(value)
29
29
  end
30
+
31
+ def noop
32
+ Flow.noop
33
+ end
30
34
  end
31
35
 
32
36
  class Flow
@@ -46,12 +50,17 @@ module Sqreen
46
50
  def raise(value)
47
51
  new(:raise, value)
48
52
  end
53
+
54
+ def noop
55
+ new(:noop, nil)
56
+ end
49
57
  end
50
58
 
51
59
  def initialize(action, value, brk = false)
52
60
  @action = action
53
61
  @value = value
54
62
  @break = brk
63
+ @passed_conditions = false
55
64
  end
56
65
 
57
66
  def return?
@@ -91,60 +100,138 @@ module Sqreen
91
100
  def break?
92
101
  @break ? true : false
93
102
  end
103
+
104
+ def passed_conditions!
105
+ @passed_conditions = true
106
+
107
+ self
108
+ end
109
+
110
+ def passed_conditions?
111
+ @passed_conditions
112
+ end
94
113
  end
95
114
 
115
+ class TimerError < StandardError; end
116
+
96
117
  class Timer
97
118
  def self.read
98
119
  Process.clock_gettime(Process::CLOCK_MONOTONIC)
99
120
  end
100
121
 
101
- attr_reader :tag
122
+ attr_reader :tag, :size
123
+ attr_accessor :conditions_passed
102
124
 
103
125
  def initialize(tag, &block)
104
126
  @tag = tag
105
- @blips = []
106
127
  @block = block
128
+ @tally = 0
129
+ @size = 0
107
130
  end
108
131
 
109
- def duration
110
- @blips.each_with_index.reduce(0) { |a, (e, i)| i.even? ? a - e : a + e }
132
+ def elapsed
133
+ raise(TimerError, 'Timer#elapsed when paused') if @size.even?
134
+
135
+ @tally + Timer.read
111
136
  end
112
137
 
113
- def elapsed
114
- @blips.each_with_index.reduce(0) { |a, (e, i)| i.even? ? a - e : a + e } + Timer.read
138
+ def duration
139
+ raise(TimerError, 'Timer#duration when running') if @size.odd?
140
+
141
+ @tally
115
142
  end
116
143
 
117
144
  def ignore
118
- @blips << Timer.read
145
+ raise(TimerError, 'Timer#ignore when paused') if @size.even?
146
+
147
+ @size += 1
148
+ @tally += Timer.read
119
149
  yield(self)
120
150
  ensure
121
- @blips << Timer.read
151
+ @size += 1
152
+ @tally -= Timer.read
122
153
  end
123
154
 
124
- def measure
125
- @blips << Timer.read
155
+ def measure(opts = nil)
156
+ raise(TimerError, 'Timer#measure when running') if @size.odd?
157
+
158
+ now = Timer.read
159
+
160
+ ignore = opts[:ignore] if opts
161
+ if ignore
162
+ ignore.size += 1
163
+ ignore.tally += now
164
+ end
165
+
166
+ @size += 1
167
+ @tally -= now
168
+
126
169
  yield(self)
127
170
  ensure
128
- @blips << Timer.read
171
+ now = Timer.read
172
+
173
+ if ignore
174
+ ignore.size += 1
175
+ ignore.tally -= now
176
+ end
177
+
178
+ @size += 1
179
+ @tally += now
180
+
129
181
  @block.call(self) if @block
130
- Sqreen::Graft.logger.debug { "#{@tag}: time=%.03fus" % (duration * 1_000_000) }
131
182
  end
132
183
 
133
- def start
134
- @blips << Timer.read
184
+ def start(at = Timer.read)
185
+ raise(TimerError, 'Timer#start when started') unless @size.even?
186
+
187
+ @size += 1
188
+ @tally -= at
189
+
190
+ at
191
+ end
192
+
193
+ def stop(at = Timer.read)
194
+ raise(TimerError, 'Timer#stop when unstarted') unless @size.odd?
195
+
196
+ @size += 1
197
+ @tally += at
198
+
199
+ at
135
200
  end
136
201
 
137
- def stop
138
- @blips << Timer.read
202
+ def started?
203
+ @size != 0 && @size.odd?
139
204
  end
140
205
 
141
- def size
142
- @blips.size
206
+ def stopped?
207
+ @size != 0 && @size.even?
208
+ end
209
+
210
+ def running?
211
+ @size.odd?
212
+ end
213
+
214
+ def paused?
215
+ @size.even?
216
+ end
217
+
218
+ def include_measurements(another_timer)
219
+ @blips += another_timer.instance_variable_get(:@blips)
220
+ end
221
+
222
+ def start_and_end
223
+ raise 'Not exactly two measurements recorded' unless size == 2
224
+ @blips
143
225
  end
144
226
 
145
227
  def to_s
146
- "#{@tag}: time=%.03fus" % (duration * 1_000_000)
228
+ "#{@tag}: time=%.03fus" % (@tally * 1_000_000)
147
229
  end
230
+
231
+ protected
232
+
233
+ attr_reader :tally
234
+ attr_writer :size, :tally
148
235
  end
149
236
  end
150
237
  end