logging 0.9.2 → 0.9.3

Sign up to get free protection for your applications and to get access to all the features.
data/History.txt CHANGED
@@ -1,3 +1,9 @@
1
+ == 0.9.3 / 2008-09-12
2
+
3
+ 2 minor enhancement
4
+ - Added a class for tracking basic statistics
5
+ - Will use the 'fastthread' gem if availble
6
+
1
7
  == 0.9.2 / 2008-09-03
2
8
 
3
9
  2 bug fixes
data/Manifest.txt CHANGED
@@ -25,6 +25,7 @@ lib/logging/log_event.rb
25
25
  lib/logging/logger.rb
26
26
  lib/logging/repository.rb
27
27
  lib/logging/root_logger.rb
28
+ lib/logging/stats.rb
28
29
  lib/logging/utils.rb
29
30
  tasks/ann.rake
30
31
  tasks/bones.rake
@@ -57,4 +58,5 @@ test/test_logger.rb
57
58
  test/test_logging.rb
58
59
  test/test_repository.rb
59
60
  test/test_root_logger.rb
61
+ test/test_stats.rb
60
62
  test/test_utils.rb
data/Rakefile CHANGED
@@ -16,7 +16,7 @@ PROJ.rdoc.dir = 'doc/rdoc'
16
16
  #PROJ.rdoc.remote_dir = 'rdoc'
17
17
  PROJ.rdoc.remote_dir = ''
18
18
  PROJ.version = Logging::VERSION
19
- PROJ.release_name = %q{Meta Class Madness}
19
+ PROJ.release_name = %q{Log-O-Stat}
20
20
 
21
21
  PROJ.exclude << %w[^tags$ ^tasks/archive ^coverage]
22
22
  PROJ.rdoc.exclude << '^data'
data/lib/logging.rb CHANGED
@@ -3,6 +3,9 @@
3
3
  # Used to prevent the class/module from being loaded more than once
4
4
  unless defined? Logging
5
5
 
6
+ require 'thread'
7
+ begin require 'fastthread'; rescue LoadError; end
8
+
6
9
  # TODO: Windows Log Service appender
7
10
 
8
11
  #
@@ -10,7 +13,7 @@ unless defined? Logging
10
13
  module Logging
11
14
 
12
15
  # :stopdoc:
13
- VERSION = '0.9.2'
16
+ VERSION = '0.9.3'
14
17
  LIBPATH = ::File.expand_path(::File.dirname(__FILE__)) + ::File::SEPARATOR
15
18
  PATH = ::File.dirname(LIBPATH) + ::File::SEPARATOR
16
19
  WIN32 = %r/djgpp|(cyg|ms|bcc)win|mingw/ =~ RUBY_PLATFORM
@@ -1,6 +1,4 @@
1
1
 
2
- require 'thread'
3
-
4
2
  module Logging
5
3
 
6
4
  # The +Appender+ class is provides methods for appending log events to a
@@ -1,6 +1,4 @@
1
1
 
2
- require 'thread'
3
-
4
2
  module Logging
5
3
 
6
4
  # The +Logger+ class is the primary interface to the +Logging+ framework.
@@ -0,0 +1,278 @@
1
+ # Simple statistics collection and logging module.
2
+ #
3
+ module Logging::Stats
4
+
5
+ # A very simple little class for doing some basic fast statistics
6
+ # sampling. You feed it either samples of numeric data you want measured
7
+ # or you call Sampler#tick to get it to add a time delta between the last
8
+ # time you called it. When you're done either call sum, sumsq, num, min,
9
+ # max, mean or sd to get the information. The other option is to just
10
+ # call to_s and see everything.
11
+ #
12
+ # It does all of this very fast and doesn't take up any memory since the
13
+ # samples are not stored but instead all the values are calculated on the
14
+ # fly.
15
+ #
16
+ class Sampler
17
+
18
+ attr_reader :name, :sum, :sumsq, :num, :min, :max, :last
19
+
20
+ # Create a new sampler.
21
+ #
22
+ def initialize( name )
23
+ @name = name
24
+ reset
25
+ end
26
+
27
+ # Resets the internal counters so you can start sampling again.
28
+ #
29
+ def reset
30
+ @sum = 0.0
31
+ @sumsq = 0.0
32
+ @num = 0
33
+ @min = 0.0
34
+ @max = 0.0
35
+ @last = nil
36
+ @last_time = Time.now.to_f
37
+ self
38
+ end
39
+
40
+ # Coalesce the statistics from the _other_ sampler into this one. The
41
+ # _other_ sampler is not modified by this method.
42
+ #
43
+ # Coalescing the same two samplers mutliple times should only be done if
44
+ # one of the samplers is reset between calls to this method. Otherwise
45
+ # statistics will be counted multiple times.
46
+ #
47
+ def coalesce( other )
48
+ @sum += other.sum
49
+ @sumsq += other.sumsq
50
+ if other.num > 0
51
+ @min = other.min if @min > other.min
52
+ @max = other.max if @max < other.max
53
+ @last = other.last
54
+ end
55
+ @num += other.num
56
+ end
57
+
58
+ # Adds a sampling to the calculations.
59
+ #
60
+ def sample( s )
61
+ @sum += s
62
+ @sumsq += s * s
63
+ if @num == 0
64
+ @min = @max = s
65
+ else
66
+ @min = s if @min > s
67
+ @max = s if @max < s
68
+ end
69
+ @num += 1
70
+ @last = s
71
+ end
72
+
73
+ # Returns statistics in a common format.
74
+ #
75
+ def to_s
76
+ "[%s]: SUM=%0.6f, SUMSQ=%0.6f, NUM=%d, MEAN=%0.6f, SD=%0.6f, MIN=%0.6f, MAX=%0.6f" % to_a
77
+ end
78
+
79
+ # An array of the values: [name,sum,sumsq,num,mean,sd,min,max]
80
+ #
81
+ def to_a
82
+ [name, sum, sumsq, num, mean, sd, min, max]
83
+ end
84
+
85
+ # Class method that returns the headers that a CSV file would have for the
86
+ # values that this stats object is using.
87
+ #
88
+ def self.keys
89
+ %w[name sum sumsq num mean sd min max]
90
+ end
91
+
92
+ def to_hash
93
+ {:name => name, :sum => sum, :sumsq => sumsq, :num => num,
94
+ :mean => mean, :sd => sd, :min => min, :max => max}
95
+ end
96
+
97
+ # Calculates and returns the mean for the data passed so far.
98
+ #
99
+ def mean
100
+ return 0.0 if num < 1
101
+ sum / num
102
+ end
103
+
104
+ # Calculates the standard deviation of the data so far.
105
+ #
106
+ def sd
107
+ return 0.0 if num < 2
108
+
109
+ # (sqrt( ((s).sumsq - ( (s).sum * (s).sum / (s).num)) / ((s).num-1) ))
110
+ begin
111
+ return Math.sqrt( (sumsq - ( sum * sum / num)) / (num-1) )
112
+ rescue Errno::EDOM
113
+ return 0.0
114
+ end
115
+ end
116
+
117
+ # You can just call tick repeatedly if you need the delta times
118
+ # between a set of sample periods, but many times you actually want
119
+ # to sample how long something takes between a start/end period.
120
+ # Call mark at the beginning and then tick at the end you'll get this
121
+ # kind of measurement. Don't mix mark/tick and tick sampling together
122
+ # or the measurement will be meaningless.
123
+ #
124
+ def mark
125
+ @last_time = Time.now.to_f
126
+ end
127
+
128
+ # Adds a time delta between now and the last time you called this. This
129
+ # will give you the average time between two activities.
130
+ #
131
+ # An example is:
132
+ #
133
+ # t = Sampler.new("do_stuff")
134
+ # 10000.times { do_stuff(); t.tick }
135
+ # t.dump("time")
136
+ #
137
+ def tick
138
+ now = Time.now.to_f
139
+ sample(now - @last_time)
140
+ @last_time = now
141
+ end
142
+ end # class Sampler
143
+
144
+ # The Tracker class provides synchronized access to a collection of
145
+ # related samplers.
146
+ #
147
+ class Tracker
148
+
149
+ attr_reader :stats
150
+
151
+ # Create a new Tracker instance. An optional boolean can be bassed in to
152
+ # change the "threadsafe" value of the tracker. By default all trackers
153
+ # are created to be threadsafe.
154
+ #
155
+ def initialize( threadsafe = true )
156
+ @stats = Hash.new do |h,name|
157
+ h[name] = ::Logging::Stats::Sampler.new(name)
158
+ end
159
+ @mutex = threadsafe ? ReentrantMutex.new : nil
160
+ @runner = nil
161
+ end
162
+
163
+ # Coalesce the samplers from the _other_ tracker into this one. The
164
+ # _other_ tracker is not modified by this method.
165
+ #
166
+ # Coalescing the same two trackers mutliple times should only be done if
167
+ # one of the trackers is reset between calls to this method. Otherwise
168
+ # statistics will be counted multiple times.
169
+ #
170
+ # Only this tracker is locked when the coalescing is happening. It is
171
+ # left to the user to lock the other tracker if that is the desired
172
+ # behavior. This is a deliberate choice in order to prevent deadlock
173
+ # situations where two threads are contending on the same mutex.
174
+ #
175
+ def coalesce( other )
176
+ sync {
177
+ other.stats.each do |name,sampler|
178
+ stats[name].coalesce(sampler)
179
+ end
180
+ }
181
+ end
182
+
183
+ # Add the given _value_ to the named _event_ sampler. The sampler will
184
+ # be created if it does not exist.
185
+ #
186
+ def sample( event, value )
187
+ sync {stats[event].sample(value)}
188
+ end
189
+
190
+ # Mark the named _event_ sampler. The sampler will be created if it does
191
+ # not exist.
192
+ #
193
+ def mark( event )
194
+ sync {stats[event].mark}
195
+ end
196
+
197
+ # Tick the named _event_ sampler. The sampler will be created if it does
198
+ # not exist.
199
+ #
200
+ def tick( event )
201
+ sync {stats[event].tick}
202
+ end
203
+
204
+ # Time the execution of the given block and store the results in the
205
+ # named _event_ sampler. The sampler will be created if it does not
206
+ # exist.
207
+ #
208
+ def time( event )
209
+ sync {stats[event].mark}
210
+ yield
211
+ ensure
212
+ sync {stats[event].tick}
213
+ end
214
+
215
+ # Reset all the samplers managed by this tracker.
216
+ #
217
+ def reset
218
+ sync {stats.each_value {|sampler| sampler.reset}}
219
+ self
220
+ end
221
+
222
+ # Periodically execute the given _block_ at the given _period_. The
223
+ # tracker will be locked while the block is executing.
224
+ #
225
+ # This method is useful for logging statistics at given interval.
226
+ #
227
+ # Example
228
+ #
229
+ # periodically_run( 300 ) {
230
+ # logger = Logging::Logger['stats']
231
+ # tracker.each {|sampler| logger << sampler.to_s}
232
+ # tracker.reset
233
+ # }
234
+ #
235
+ def periodically_run( period, &block )
236
+ raise ArgumentError, 'a runner already exists' unless @runner.nil?
237
+
238
+ @runner = Thread.new do
239
+ start = stop = Time.now.to_f
240
+ loop do
241
+ seconds = period - (stop-start)
242
+ seconds = period if seconds <= 0
243
+ sleep seconds
244
+
245
+ start = Time.now.to_f
246
+ break if Thread.current[:stop] == true
247
+ if @mutex then @mutex.synchronize(&block)
248
+ else block.call end
249
+ stop = Time.now.to_f
250
+ end
251
+ end
252
+ end
253
+
254
+ # Stop the current periodic runner if present.
255
+ #
256
+ def stop
257
+ return if @runner.nil?
258
+ @runner[:stop] = true
259
+ @runner.wakeup if @runner.status
260
+ @runner = nil
261
+ end
262
+
263
+ # call-seq:
264
+ # sync { block }
265
+ #
266
+ # Obtains an exclusive lock, runs the block, and releases the lock when
267
+ # the block completes. This method is re-entrant so that a single thread
268
+ # can call +sync+ multiple times without hanging the thread.
269
+ #
270
+ def sync
271
+ return yield if @mutex.nil?
272
+ @mutex.synchronize {yield}
273
+ end
274
+ end # class Tracker
275
+
276
+ end # module Logging::Stats
277
+
278
+ # EOF
data/lib/logging/utils.rb CHANGED
@@ -102,4 +102,29 @@ class Module
102
102
  end
103
103
  end
104
104
 
105
+ class ReentrantMutex < Mutex
106
+
107
+ def initialize
108
+ super
109
+ @locker = nil
110
+ end
111
+
112
+ alias :original_synchronize :synchronize
113
+
114
+ def synchronize
115
+ if @locker == Thread.current
116
+ yield
117
+ else
118
+ original_synchronize {
119
+ begin
120
+ @locker = Thread.current
121
+ yield
122
+ ensure
123
+ @locker = nil
124
+ end
125
+ }
126
+ end
127
+ end
128
+ end # class ReentrantMutex
129
+
105
130
  # EOF
@@ -0,0 +1,274 @@
1
+
2
+ require File.join(File.dirname(__FILE__), %w[setup])
3
+
4
+ module TestLogging
5
+ module TestStats
6
+
7
+ class TestSampler < Test::Unit::TestCase
8
+ include LoggingTestCase
9
+
10
+ def setup
11
+ super
12
+ @sampler = ::Logging::Stats::Sampler.new('test')
13
+ end
14
+
15
+ def test_reset
16
+ (1..10).each {|n| @sampler.sample n}
17
+
18
+ assert_equal 55, @sampler.sum
19
+ assert_equal 385, @sampler.sumsq
20
+ assert_equal 10, @sampler.num
21
+ assert_equal 1, @sampler.min
22
+ assert_equal 10, @sampler.max
23
+ assert_equal 10, @sampler.last
24
+
25
+ @sampler.reset
26
+
27
+ assert_equal 0, @sampler.sum
28
+ assert_equal 0, @sampler.sumsq
29
+ assert_equal 0, @sampler.num
30
+ assert_equal 0, @sampler.min
31
+ assert_equal 0, @sampler.max
32
+ assert_nil @sampler.last
33
+ end
34
+
35
+ def test_coalesce
36
+ other = ::Logging::Stats::Sampler.new('other')
37
+ (1..5).each {|n| other.sample n}
38
+ (6..10).each {|n| @sampler.sample n}
39
+
40
+ assert_equal 5, @sampler.num
41
+
42
+ @sampler.coalesce other
43
+
44
+ assert_equal 55, @sampler.sum
45
+ assert_equal 385, @sampler.sumsq
46
+ assert_equal 10, @sampler.num
47
+ assert_equal 1, @sampler.min
48
+ assert_equal 10, @sampler.max
49
+ assert_equal 5, @sampler.last
50
+
51
+ @sampler.coalesce ::Logging::Stats::Sampler.new('tmp')
52
+
53
+ assert_equal 55, @sampler.sum
54
+ assert_equal 385, @sampler.sumsq
55
+ assert_equal 10, @sampler.num
56
+ assert_equal 1, @sampler.min
57
+ assert_equal 10, @sampler.max
58
+ assert_equal 5, @sampler.last
59
+ end
60
+
61
+ def test_sample
62
+ @sampler.sample 1
63
+
64
+ assert_equal 1, @sampler.sum
65
+ assert_equal 1, @sampler.sumsq
66
+ assert_equal 1, @sampler.num
67
+ assert_equal 1, @sampler.min
68
+ assert_equal 1, @sampler.max
69
+ assert_equal 1, @sampler.last
70
+
71
+ @sampler.sample 2
72
+
73
+ assert_equal 3, @sampler.sum
74
+ assert_equal 5, @sampler.sumsq
75
+ assert_equal 2, @sampler.num
76
+ assert_equal 1, @sampler.min
77
+ assert_equal 2, @sampler.max
78
+ assert_equal 2, @sampler.last
79
+ end
80
+
81
+ def test_to_s
82
+ (1..10).each {|n| @sampler.sample n}
83
+ assert_equal(
84
+ "[test]: SUM=55.000000, SUMSQ=385.000000, NUM=10, MEAN=5.500000, SD=3.027650, MIN=1.000000, MAX=10.000000",
85
+ @sampler.to_s
86
+ )
87
+ end
88
+
89
+ def test_to_a
90
+ (1..10).each {|n| @sampler.sample n}
91
+ assert_equal(
92
+ ['test', 55, 385, 10, 5.5, @sampler.sd, 1, 10],
93
+ @sampler.to_a
94
+ )
95
+ end
96
+
97
+ def test_to_hash
98
+ (1..10).each {|n| @sampler.sample n}
99
+ assert_equal(
100
+ {:name => 'test', :sum => 55, :sumsq => 385, :num => 10, :mean => 5.5, :sd => @sampler.sd, :min => 1, :max => 10},
101
+ @sampler.to_hash
102
+ )
103
+ end
104
+
105
+ def test_mean
106
+ assert_equal 0, @sampler.mean
107
+
108
+ @sampler.sample 10
109
+ assert_equal 10, @sampler.mean
110
+
111
+ @sampler.sample 20
112
+ assert_equal 15, @sampler.mean
113
+ end
114
+
115
+ def test_sd
116
+ assert_equal 0, @sampler.sd
117
+
118
+ @sampler.sample 1
119
+ assert_equal 0, @sampler.sd
120
+
121
+ @sampler.sample 2
122
+ assert_in_delta 0.707106781186548, @sampler.sd, 1e-10
123
+
124
+ @sampler.sample 3
125
+ assert_in_delta 1.0, @sampler.sd, 1e-10
126
+
127
+ @sampler.sample 4
128
+ assert_in_delta 1.29099444873581, @sampler.sd, 1e-10
129
+ end
130
+
131
+ def test_mark_and_tick
132
+ 10.times do
133
+ @sampler.mark
134
+ sleep 0.01
135
+ @sampler.tick
136
+ end
137
+
138
+ assert_equal 10, @sampler.num
139
+ assert_in_delta 0.01, @sampler.mean, 1e-3
140
+ end
141
+ end # class TestSampler
142
+
143
+ class TestTracker < Test::Unit::TestCase
144
+ include LoggingTestCase
145
+
146
+ def setup
147
+ super
148
+ @tracker = ::Logging::Stats::Tracker.new
149
+ @stats = @tracker.stats
150
+ end
151
+
152
+ def test_coalesce
153
+ 1.times {|n| @tracker.sample('foo', n)}
154
+ 2.times {|n| @tracker.sample('bar', n)}
155
+ 3.times {|n| @tracker.sample('baz', n)}
156
+
157
+ assert_equal %w[bar baz foo], @stats.keys.sort
158
+ assert_equal 1, @stats['foo'].num
159
+ assert_equal 2, @stats['bar'].num
160
+ assert_equal 3, @stats['baz'].num
161
+
162
+ # when other is empty, nothing should change in our tracker
163
+ other = ::Logging::Stats::Tracker.new
164
+ @tracker.coalesce other
165
+
166
+ assert_equal %w[bar baz foo], @stats.keys.sort
167
+ assert_equal 1, @stats['foo'].num
168
+ assert_equal 2, @stats['bar'].num
169
+ assert_equal 3, @stats['baz'].num
170
+
171
+ # now add some samples to other
172
+ 4.times {|n| other.sample('buz', n)}
173
+ 5.times {|n| other.sample('bar', n)}
174
+ @tracker.coalesce other
175
+
176
+ assert_equal %w[bar baz buz foo], @stats.keys.sort
177
+ assert_equal 1, @stats['foo'].num
178
+ assert_equal 7, @stats['bar'].num
179
+ assert_equal 3, @stats['baz'].num
180
+ assert_equal 4, @stats['buz'].num
181
+ end
182
+
183
+ def test_mark
184
+ assert @stats.empty?
185
+ @tracker.mark 'foo'
186
+ assert !@stats.empty?
187
+
188
+ sampler = @stats['foo']
189
+ assert_equal 0, sampler.num
190
+ end
191
+
192
+ def test_tick
193
+ assert @stats.empty?
194
+ @tracker.tick 'foo'
195
+ assert !@stats.empty?
196
+
197
+ sampler = @stats['foo']
198
+ assert_equal 1, sampler.num
199
+ end
200
+
201
+ def test_sample
202
+ assert @stats.empty?
203
+ @tracker.sample 'foo', 1
204
+ assert !@stats.empty?
205
+
206
+ sampler = @stats['foo']
207
+ assert_equal 1, sampler.num
208
+ assert_equal 1, sampler.last
209
+
210
+ @tracker.sample 'foo', 2
211
+ assert_equal 2, sampler.num
212
+ assert_equal 2, sampler.last
213
+ assert_equal 3, sampler.sum
214
+ end
215
+
216
+ def test_time
217
+ assert @stats.empty?
218
+ @tracker.time('foo') {sleep 0.05}
219
+ assert !@stats.empty?
220
+
221
+ sampler = @stats['foo']
222
+ assert_equal 1, sampler.num
223
+ assert_in_delta 0.05, sampler.sum, 1e-3
224
+
225
+ @tracker.time('foo') {sleep 0.05}
226
+ assert_equal 2, sampler.num
227
+ assert_in_delta 0.10, sampler.sum, 1e-2
228
+
229
+ assert_raise(RuntimeError) do
230
+ @tracker.time('foo') {raise 'Uh Oh!'}
231
+ end
232
+ assert_equal 3, sampler.num
233
+ end
234
+
235
+ def test_reset
236
+ 1.times {|n| @tracker.sample('foo', n)}
237
+ 2.times {|n| @tracker.sample('bar', n)}
238
+ 3.times {|n| @tracker.sample('baz', n)}
239
+
240
+ assert_equal 1, @stats['foo'].num
241
+ assert_equal 2, @stats['bar'].num
242
+ assert_equal 3, @stats['baz'].num
243
+
244
+ @tracker.reset
245
+
246
+ assert_equal 0, @stats['foo'].num
247
+ assert_equal 0, @stats['bar'].num
248
+ assert_equal 0, @stats['baz'].num
249
+ end
250
+
251
+ def test_reentrant_synchronization
252
+ assert_nothing_raised do
253
+ @tracker.sync {
254
+ @tracker.sample('foo', Math::PI)
255
+ @tracker.reset
256
+ }
257
+ end
258
+ end
259
+
260
+ def test_periodically_run
261
+ @tracker.periodically_run(0.1) {
262
+ @tracker.tick 'foo'
263
+ }
264
+ sleep 0.5
265
+ @tracker.stop
266
+
267
+ assert(@stats['foo'].num > 1)
268
+ end
269
+ end # class TestTracker
270
+
271
+ end # module TestStats
272
+ end # module TestLogging
273
+
274
+ # EOF
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: logging
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.2
4
+ version: 0.9.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tim Pease
@@ -9,7 +9,7 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2008-09-03 00:00:00 -06:00
12
+ date: 2008-09-13 00:00:00 -06:00
13
13
  default_executable:
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
@@ -69,6 +69,7 @@ files:
69
69
  - lib/logging/logger.rb
70
70
  - lib/logging/repository.rb
71
71
  - lib/logging/root_logger.rb
72
+ - lib/logging/stats.rb
72
73
  - lib/logging/utils.rb
73
74
  - tasks/ann.rake
74
75
  - tasks/bones.rake
@@ -101,6 +102,7 @@ files:
101
102
  - test/test_logging.rb
102
103
  - test/test_repository.rb
103
104
  - test/test_root_logger.rb
105
+ - test/test_stats.rb
104
106
  - test/test_utils.rb
105
107
  has_rdoc: true
106
108
  homepage: http://logging.rubyforge.org/
@@ -148,4 +150,5 @@ test_files:
148
150
  - test/test_logging.rb
149
151
  - test/test_repository.rb
150
152
  - test/test_root_logger.rb
153
+ - test/test_stats.rb
151
154
  - test/test_utils.rb