logging 0.9.2 → 0.9.3

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.
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