TwP-logging 0.9.7
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 +169 -0
- data/README.rdoc +102 -0
- data/Rakefile +42 -0
- data/data/bad_logging_1.rb +13 -0
- data/data/bad_logging_2.rb +21 -0
- data/data/logging.rb +42 -0
- data/data/logging.yaml +63 -0
- data/data/simple_logging.rb +13 -0
- data/lib/logging.rb +408 -0
- data/lib/logging/appender.rb +303 -0
- data/lib/logging/appenders/buffering.rb +167 -0
- data/lib/logging/appenders/console.rb +62 -0
- data/lib/logging/appenders/email.rb +75 -0
- data/lib/logging/appenders/file.rb +54 -0
- data/lib/logging/appenders/growl.rb +197 -0
- data/lib/logging/appenders/io.rb +69 -0
- data/lib/logging/appenders/rolling_file.rb +291 -0
- data/lib/logging/appenders/syslog.rb +201 -0
- data/lib/logging/config/configurator.rb +190 -0
- data/lib/logging/config/yaml_configurator.rb +195 -0
- data/lib/logging/layout.rb +119 -0
- data/lib/logging/layouts/basic.rb +34 -0
- data/lib/logging/layouts/pattern.rb +296 -0
- data/lib/logging/log_event.rb +51 -0
- data/lib/logging/logger.rb +490 -0
- data/lib/logging/repository.rb +172 -0
- data/lib/logging/root_logger.rb +61 -0
- data/lib/logging/stats.rb +278 -0
- data/lib/logging/utils.rb +130 -0
- data/logging.gemspec +41 -0
- data/test/appenders/test_buffered_io.rb +183 -0
- data/test/appenders/test_console.rb +66 -0
- data/test/appenders/test_email.rb +171 -0
- data/test/appenders/test_file.rb +93 -0
- data/test/appenders/test_growl.rb +128 -0
- data/test/appenders/test_io.rb +142 -0
- data/test/appenders/test_rolling_file.rb +207 -0
- data/test/appenders/test_syslog.rb +194 -0
- data/test/benchmark.rb +87 -0
- data/test/config/test_configurator.rb +70 -0
- data/test/config/test_yaml_configurator.rb +40 -0
- data/test/layouts/test_basic.rb +43 -0
- data/test/layouts/test_pattern.rb +177 -0
- data/test/setup.rb +74 -0
- data/test/test_appender.rb +166 -0
- data/test/test_layout.rb +110 -0
- data/test/test_log_event.rb +80 -0
- data/test/test_logger.rb +734 -0
- data/test/test_logging.rb +267 -0
- data/test/test_repository.rb +126 -0
- data/test/test_root_logger.rb +81 -0
- data/test/test_stats.rb +274 -0
- data/test/test_utils.rb +116 -0
- metadata +156 -0
@@ -0,0 +1,172 @@
|
|
1
|
+
|
2
|
+
require 'singleton'
|
3
|
+
|
4
|
+
module Logging
|
5
|
+
|
6
|
+
# The Repository is a hash that stores references to all Loggers
|
7
|
+
# that have been created. It provides methods to determine parent/child
|
8
|
+
# relationships between Loggers and to retrieve Loggers from the hash.
|
9
|
+
#
|
10
|
+
class Repository
|
11
|
+
include Singleton
|
12
|
+
|
13
|
+
PATH_DELIMITER = '::' # :nodoc:
|
14
|
+
|
15
|
+
# nodoc:
|
16
|
+
#
|
17
|
+
# This is a singleton class -- use the +instance+ method to obtain the
|
18
|
+
# +Repository+ instance.
|
19
|
+
#
|
20
|
+
def initialize
|
21
|
+
@h = {:root => ::Logging::RootLogger.new}
|
22
|
+
|
23
|
+
# configures the internal logger which is disabled by default
|
24
|
+
logger = ::Logging::Logger.allocate
|
25
|
+
logger._setup(
|
26
|
+
to_key(::Logging),
|
27
|
+
:parent => @h[:root],
|
28
|
+
:additive => false,
|
29
|
+
:level => ::Logging::LEVELS.length # turns this logger off
|
30
|
+
)
|
31
|
+
@h[logger.name] = logger
|
32
|
+
end
|
33
|
+
|
34
|
+
# call-seq:
|
35
|
+
# instance[name]
|
36
|
+
#
|
37
|
+
# Returns the +Logger+ named _name_.
|
38
|
+
#
|
39
|
+
# When _name_ is a +String+ or a +Symbol+ it will be used "as is" to
|
40
|
+
# retrieve the logger. When _name_ is a +Class+ the class name will be
|
41
|
+
# used to retrieve the logger. When _name_ is an object the name of the
|
42
|
+
# object's class will be used to retrieve the logger.
|
43
|
+
#
|
44
|
+
# Example:
|
45
|
+
#
|
46
|
+
# repo = Repository.instance
|
47
|
+
# obj = MyClass.new
|
48
|
+
#
|
49
|
+
# log1 = repo[obj]
|
50
|
+
# log2 = repo[MyClass]
|
51
|
+
# log3 = repo['MyClass']
|
52
|
+
#
|
53
|
+
# log1.object_id == log2.object_id # => true
|
54
|
+
# log2.object_id == log3.object_id # => true
|
55
|
+
#
|
56
|
+
def []( key ) @h[to_key(key)] end
|
57
|
+
|
58
|
+
# call-seq:
|
59
|
+
# instance[name] = logger
|
60
|
+
#
|
61
|
+
# Stores the _logger_ under the given _name_.
|
62
|
+
#
|
63
|
+
# When _name_ is a +String+ or a +Symbol+ it will be used "as is" to
|
64
|
+
# store the logger. When _name_ is a +Class+ the class name will be
|
65
|
+
# used to store the logger. When _name_ is an object the name of the
|
66
|
+
# object's class will be used to store the logger.
|
67
|
+
#
|
68
|
+
def []=( key, val ) @h[to_key(key)] = val end
|
69
|
+
|
70
|
+
# call-seq:
|
71
|
+
# fetch( name )
|
72
|
+
#
|
73
|
+
# Returns the +Logger+ named _name_. An +IndexError+ will be raised if
|
74
|
+
# the logger does not exist.
|
75
|
+
#
|
76
|
+
# When _name_ is a +String+ or a +Symbol+ it will be used "as is" to
|
77
|
+
# retrieve the logger. When _name_ is a +Class+ the class name will be
|
78
|
+
# used to retrieve the logger. When _name_ is an object the name of the
|
79
|
+
# object's class will be used to retrieve the logger.
|
80
|
+
#
|
81
|
+
def fetch( key ) @h.fetch(to_key(key)) end
|
82
|
+
|
83
|
+
# call-seq:
|
84
|
+
# has_logger?( name )
|
85
|
+
#
|
86
|
+
# Returns +true+ if the given logger exists in the repository. Returns
|
87
|
+
# +false+ if this is not the case.
|
88
|
+
#
|
89
|
+
# When _name_ is a +String+ or a +Symbol+ it will be used "as is" to
|
90
|
+
# retrieve the logger. When _name_ is a +Class+ the class name will be
|
91
|
+
# used to retrieve the logger. When _name_ is an object the name of the
|
92
|
+
# object's class will be used to retrieve the logger.
|
93
|
+
#
|
94
|
+
def has_logger?( key ) @h.has_key?(to_key(key)) end
|
95
|
+
|
96
|
+
# call-seq:
|
97
|
+
# parent( key )
|
98
|
+
#
|
99
|
+
# Returns the parent logger for the logger identified by _key_ where
|
100
|
+
# _key_ follows the same identification rules described in
|
101
|
+
# <tt>Repository#[]</tt>. A parent is returned regardless of the
|
102
|
+
# existence of the logger referenced by _key_.
|
103
|
+
#
|
104
|
+
# A note about parents -
|
105
|
+
#
|
106
|
+
# If you have a class A::B::C, then the parent of C is B, and the parent
|
107
|
+
# of B is A. Parents are determined by namespace.
|
108
|
+
#
|
109
|
+
def parent( key )
|
110
|
+
name = _parent_name(to_key(key))
|
111
|
+
return if name.nil?
|
112
|
+
@h[name]
|
113
|
+
end
|
114
|
+
|
115
|
+
# call-seq:
|
116
|
+
# children( key )
|
117
|
+
#
|
118
|
+
# Returns an array of the children loggers for the logger identified by
|
119
|
+
# _key_ where _key_ follows the same identification rules described in
|
120
|
+
# +Repository#[]+. Children are returned regardless of the
|
121
|
+
# existence of the logger referenced by _key_.
|
122
|
+
#
|
123
|
+
def children( parent )
|
124
|
+
ary = []
|
125
|
+
parent = to_key(parent)
|
126
|
+
|
127
|
+
@h.each_pair do |child,logger|
|
128
|
+
next if :root == child
|
129
|
+
ary << logger if parent == _parent_name(child)
|
130
|
+
end
|
131
|
+
return ary.sort
|
132
|
+
end
|
133
|
+
|
134
|
+
# call-seq:
|
135
|
+
# to_key( key )
|
136
|
+
#
|
137
|
+
# Takes the given _key_ and converts it into a form that can be used to
|
138
|
+
# retrieve a logger from the +Repository+ hash.
|
139
|
+
#
|
140
|
+
# When _key_ is a +String+ or a +Symbol+ it will be returned "as is".
|
141
|
+
# When _key_ is a +Class+ the class name will be returned. When _key_ is
|
142
|
+
# an object the name of the object's class will be returned.
|
143
|
+
#
|
144
|
+
def to_key( key )
|
145
|
+
case key
|
146
|
+
when :root, 'root'; :root
|
147
|
+
when String; key
|
148
|
+
when Symbol; key.to_s
|
149
|
+
when Module; key.logger_name
|
150
|
+
when Object; key.class.logger_name
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
# Returns the name of the parent for the logger identified by the given
|
155
|
+
# _key_. If the _key_ is for the root logger, then +nil+ is returned.
|
156
|
+
#
|
157
|
+
def _parent_name( key )
|
158
|
+
return if :root == key
|
159
|
+
|
160
|
+
a = key.split PATH_DELIMITER
|
161
|
+
p = :root
|
162
|
+
while a.slice!(-1) and !a.empty?
|
163
|
+
k = a.join PATH_DELIMITER
|
164
|
+
if @h.has_key? k then p = k; break end
|
165
|
+
end
|
166
|
+
p
|
167
|
+
end
|
168
|
+
|
169
|
+
end # class Repository
|
170
|
+
end # module Logging
|
171
|
+
|
172
|
+
# EOF
|
@@ -0,0 +1,61 @@
|
|
1
|
+
|
2
|
+
module Logging
|
3
|
+
|
4
|
+
# The root logger exists to ensure that all loggers have a parent and a
|
5
|
+
# defined logging level. If a logger is additive, eventually its log
|
6
|
+
# events will propogate up to the root logger.
|
7
|
+
#
|
8
|
+
class RootLogger < Logger
|
9
|
+
|
10
|
+
# undefine the methods that the root logger does not need
|
11
|
+
%w(additive additive= parent parent=).each do |m|
|
12
|
+
undef_method m.intern
|
13
|
+
end
|
14
|
+
|
15
|
+
attr_reader :level
|
16
|
+
|
17
|
+
# call-seq:
|
18
|
+
# RootLogger.new
|
19
|
+
#
|
20
|
+
# Returns a new root logger instance. This method will be called only
|
21
|
+
# once when the +Repository+ singleton instance is created.
|
22
|
+
#
|
23
|
+
def initialize( )
|
24
|
+
::Logging.init unless ::Logging.const_defined? 'MAX_LEVEL_LENGTH'
|
25
|
+
|
26
|
+
@name = 'root'
|
27
|
+
@appenders = []
|
28
|
+
@additive = false
|
29
|
+
@trace = false
|
30
|
+
@level = 0
|
31
|
+
::Logging::Logger.define_log_methods(self)
|
32
|
+
end
|
33
|
+
|
34
|
+
# call-seq:
|
35
|
+
# log <=> other
|
36
|
+
#
|
37
|
+
# Compares this logger by name to another logger. The normal return codes
|
38
|
+
# for +String+ objects apply.
|
39
|
+
#
|
40
|
+
def <=>( other )
|
41
|
+
case other
|
42
|
+
when self; 0
|
43
|
+
when ::Logging::Logger; -1
|
44
|
+
else raise ArgumentError, 'expecting a Logger instance' end
|
45
|
+
end
|
46
|
+
|
47
|
+
# call-seq:
|
48
|
+
# level = :all
|
49
|
+
#
|
50
|
+
# Set the level for the root logger. The functionality of this method is
|
51
|
+
# the same as +Logger#level=+, but setting the level to +nil+ for the
|
52
|
+
# root logger is not allowed. The level is silently set to :all.
|
53
|
+
#
|
54
|
+
def level=( level )
|
55
|
+
super(level || 0)
|
56
|
+
end
|
57
|
+
|
58
|
+
end # class RootLogger
|
59
|
+
end # module Logging
|
60
|
+
|
61
|
+
# EOF
|
@@ -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
|