sawmill 0.0.2 → 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (33) hide show
  1. data/History.rdoc +17 -0
  2. data/lib/sawmill/entry.rb +17 -1
  3. data/lib/sawmill/entry_processor/build_records.rb +10 -7
  4. data/lib/sawmill/entry_processor/compile_report.rb +115 -0
  5. data/lib/sawmill/entry_processor/conditionals.rb +22 -23
  6. data/lib/sawmill/entry_processor/count_entries.rb +112 -0
  7. data/lib/sawmill/entry_processor/{filter_basic_fields.rb → filter_by_basic_fields.rb} +3 -3
  8. data/lib/sawmill/entry_processor/filter_by_block.rb +96 -0
  9. data/lib/sawmill/entry_processor/format.rb +9 -1
  10. data/lib/sawmill/entry_processor/simple_queue.rb +2 -1
  11. data/lib/sawmill/entry_processor.rb +68 -5
  12. data/lib/sawmill/errors.rb +7 -0
  13. data/lib/sawmill/interface.rb +324 -0
  14. data/lib/sawmill/logger.rb +8 -7
  15. data/lib/sawmill/record.rb +14 -0
  16. data/lib/sawmill/record_processor/compile_report.rb +113 -0
  17. data/lib/sawmill/record_processor/conditionals.rb +12 -13
  18. data/lib/sawmill/record_processor/count_records.rb +84 -0
  19. data/lib/sawmill/record_processor/decompose.rb +2 -2
  20. data/lib/sawmill/record_processor/filter_by_attributes.rb +2 -1
  21. data/lib/sawmill/record_processor/filter_by_block.rb +95 -0
  22. data/lib/sawmill/record_processor/filter_by_record_id.rb +2 -1
  23. data/lib/sawmill/record_processor/format.rb +8 -2
  24. data/lib/sawmill/record_processor/simple_queue.rb +2 -1
  25. data/lib/sawmill/record_processor.rb +69 -5
  26. data/lib/sawmill/rotater/date_based_log_file.rb +8 -8
  27. data/lib/sawmill/rotater/shifting_log_file.rb +7 -6
  28. data/lib/sawmill/util/processor_tools.rb +71 -0
  29. data/lib/sawmill/version.rb +1 -3
  30. data/lib/sawmill.rb +9 -1
  31. data/tests/tc_entry_processors.rb +7 -7
  32. data/tests/tc_reports.rb +101 -0
  33. metadata +13 -3
@@ -103,15 +103,35 @@ module Sawmill
103
103
  end
104
104
 
105
105
 
106
- # Close down the processor. After this is called, the processor should
107
- # ignore any further entries.
108
-
109
- def close
106
+ # Close down the processor, perform any finishing tasks, and return
107
+ # any final calculated value.
108
+ #
109
+ # After this is called, the processor should ignore any further entries.
110
+ #
111
+ # The return value can be used to communicate a final computed value,
112
+ # analysis report, or other data back to the caller. It may also be
113
+ # nil, signalling no finish value.
114
+ #
115
+ # Note that some processors function to multiplex other processors. In
116
+ # such a case, their finish value needs to be an aggregate of the
117
+ # values returned by their descendants. To handle these cases, we
118
+ # define a protocol for finish values. A finish value may be nil, an
119
+ # Array, or another kind of object. Nil means "no value" and thus can
120
+ # be ignored by a processor that aggregates other values. An Array
121
+ # indicates an aggregation; if finish returns an array, it is _always_
122
+ # an aggregation of actual values. Any other kind of object is to be
123
+ # interpreted as a single value. This means that if you want to
124
+ # actually return an array _as_ a value, you must wrap it in another
125
+ # array, indicating "an array of one finish value, and that finish
126
+ # value also happens to be an array itself".
127
+
128
+ def finish
129
+ nil
110
130
  end
111
131
 
112
132
 
113
133
  def self.inherited(subclass_) # :nodoc:
114
- if subclass_.name =~ /^Sawmill::EntryProcessor::(.*)$/
134
+ if subclass_.name =~ /^Sawmill::EntryProcessor::([^:]+)$/
115
135
  name_ = $1
116
136
  Builder.class_eval do
117
137
  define_method(name_) do |*args_|
@@ -122,6 +142,37 @@ module Sawmill
122
142
  end
123
143
 
124
144
 
145
+ # Add a method to the processor building DSL. You may call this method
146
+ # in the DSL to create an instance of this entry processor.
147
+ # You must pass a method name that begins with a lower-case letter or
148
+ # underscore.
149
+ #
150
+ # Processors that subclass Sawmill::EntryProcessor::Base and live in
151
+ # the Sawmill::EntryProcessor namespace will have their class name
152
+ # automatically added to the DSL. This method is primarily for other
153
+ # processors that do not live in that module namespace.
154
+ #
155
+ # See Sawmill::EntryProcessor#build for more information.
156
+ #
157
+ # Raises Sawmill::Errors::DSLMethodError if the given name is already
158
+ # taken.
159
+
160
+ def self.add_dsl_method(name_)
161
+ klass_ = self
162
+ if name_.to_s !~ /^[a-z_]/
163
+ raise ::ArgumentError, "Method name must begin with a lower-case letter or underscore"
164
+ end
165
+ if Builder.method_defined?(name_)
166
+ raise Errors::DSLMethodError, "Method #{name_} already defined"
167
+ end
168
+ Builder.class_eval do
169
+ define_method(name_) do |*args_|
170
+ klass_.new(*args_)
171
+ end
172
+ end
173
+ end
174
+
175
+
125
176
  private
126
177
 
127
178
  def _interpret_processor_array(param_) # :nodoc:
@@ -146,6 +197,18 @@ module Sawmill
146
197
  # A convenience DSL for building sets of processors. This is typically
147
198
  # useful for constructing if-expressions using the boolean operation
148
199
  # processors.
200
+ #
201
+ # Every entry processor that lives in the Sawmill::EntryProcessor
202
+ # module and subclasses Sawmill::EntryProcessor::Base can be
203
+ # instantiated by using its name as a function call. Other processors
204
+ # may also add themselves to the DSL by calling
205
+ # Sawmill::EntryProcessor::Base#add_dsl_method.
206
+ #
207
+ # For example:
208
+ #
209
+ # Sawmill::EntryProcessor.build do
210
+ # If(FilterByBasicFields(:level => :WARN), Format(STDOUT))
211
+ # end
149
212
 
150
213
  def self.build(&block_)
151
214
  ::Blockenspiel.invoke(block_, Builder.new)
@@ -60,6 +60,13 @@ module Sawmill
60
60
  end
61
61
 
62
62
 
63
+ # Tried to register a method with a processor building DSL
64
+ # where the method name was already taken.
65
+
66
+ class DSLMethodError < SawmillError
67
+ end
68
+
69
+
63
70
  end
64
71
 
65
72
 
@@ -0,0 +1,324 @@
1
+ # -----------------------------------------------------------------------------
2
+ #
3
+ # Sawmill convenience interface
4
+ #
5
+ # -----------------------------------------------------------------------------
6
+ # Copyright 2009 Daniel Azuma
7
+ #
8
+ # All rights reserved.
9
+ #
10
+ # Redistribution and use in source and binary forms, with or without
11
+ # modification, are permitted provided that the following conditions are met:
12
+ #
13
+ # * Redistributions of source code must retain the above copyright notice,
14
+ # this list of conditions and the following disclaimer.
15
+ # * Redistributions in binary form must reproduce the above copyright notice,
16
+ # this list of conditions and the following disclaimer in the documentation
17
+ # and/or other materials provided with the distribution.
18
+ # * Neither the name of the copyright holder, nor the names of any other
19
+ # contributors to this software, may be used to endorse or promote products
20
+ # derived from this software without specific prior written permission.
21
+ #
22
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
23
+ # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
24
+ # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
25
+ # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
26
+ # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
27
+ # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
28
+ # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
29
+ # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
30
+ # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
31
+ # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
32
+ # POSSIBILITY OF SUCH DAMAGE.
33
+ # -----------------------------------------------------------------------------
34
+ ;
35
+
36
+
37
+ # This module is a namespace for Sawmill.
38
+ #
39
+ # It also contains some convenience class methods.
40
+
41
+ module Sawmill
42
+
43
+ class << self
44
+
45
+
46
+ # Creates a new logger that writes to a single logfile.
47
+ # You may provide either the path to the logfile, or an IO object to
48
+ # write to, such as STDOUT.
49
+ #
50
+ # You may pass the same options taken by Sawmill::Logger#new and
51
+ # Sawmill::EntryProcessor::Format#new, which are:
52
+ #
53
+ # <tt>:levels</tt>::
54
+ # Use a custom Sawmill::LevelGroup. Normally, you should leave this
55
+ # set to the default, which is Sawmill::STANDARD_LEVELS.
56
+ # <tt>:level</tt>::
57
+ # Default level to use for log messages when no level is explicitly
58
+ # provided. By default, this is set to the level group's default,
59
+ # which in the case of the standard levels is :INFO.
60
+ # <tt>:attribute_level</tt>::
61
+ # Default level to use for attributes when no level is explicitly
62
+ # provided. By default, this is set to the level group's highest,
63
+ # level, which in the case of the standard levels is :ANY.
64
+ # <tt>:progname</tt>::
65
+ # Progname to use in log messages. Default is "sawmill".
66
+ # <tt>:record_progname</tt>::
67
+ # Progname to use in special log entries dealing with log records
68
+ # (i.e. record delimiters and attribute messages). Default is the
69
+ # same as the normal progname setting.
70
+ # <tt>:record_id_generator</tt>::
71
+ # A proc that generates and returns a new record ID if one is not
72
+ # explicitly passed into begin_record. If you do not provide a
73
+ # generator, the default one is used, which generates an ID using the
74
+ # variant 4 (random) UUID standard.
75
+ # <tt>:include_id</tt>::
76
+ # Write the record ID in every log entry. Default is false.
77
+ # <tt>:fractional_second_digits</tt>::
78
+ # Number of digits of fractional seconds to write in timestamps.
79
+ # Default is 2. Accepted values are 0 to 6.
80
+ # <tt>:level_width</tt>::
81
+ # Column width of the level field.
82
+ # <tt>:local_time</tt>::
83
+ # If true, outputs local time with the timezone offset indicator.
84
+ # If false (the default), outputs UTC.
85
+ # <tt>:iso_8601_time</tt>::
86
+ # If true, outputs time in strict ISO 8601 format.
87
+ # If false (the default), outputs a slightly more readable format.
88
+
89
+ def simple_logger(filepath_=::STDOUT, opts_={})
90
+ if filepath_.kind_of?(::String)
91
+ io_ = ::File.open(filepath_)
92
+ elsif filepath_.respond_to?(:write) && filepath_.respond_to?(:close)
93
+ io_ = filepath_
94
+ else
95
+ raise ::ArgumentError, "You must pass a file path or an IO object"
96
+ end
97
+ processor_ = EntryProcessor::Format.new(io_, opts_)
98
+ Logger.new(opts_.merge(:processor => processor_))
99
+ end
100
+
101
+
102
+ # Creates a new logger that writes to a logfile that rotates
103
+ # automatically by "shifting". This is a standard rotation strategy
104
+ # used by many unix tools.
105
+ #
106
+ # You must provide the logfile path, a shifting period, and a maximum
107
+ # file size.
108
+ #
109
+ # The period specifies how often to rotate the logfile. Possible values
110
+ # include <tt>:yearly</tt>, <tt>:monthly</tt>, <tt>:daily</tt>, and
111
+ # <tt>:hourly</tt>. You may also specify an integer value, which is
112
+ # interpreted as a number of seconds. Finally, you may pass nil to
113
+ # disable checking of the file's age. If you do pass nil, you should
114
+ # provide a maximum size.
115
+ #
116
+ # The maximum size is the maximum file size in bytes. You may provide
117
+ # a number, or nil to disable checking of the file size.
118
+ #
119
+ # You may pass the same options taken by Sawmill::Logger#new,
120
+ # Sawmill::EntryProcessor::Format#new, Sawmill::Rotater#new, and
121
+ # Sawmill::Rotater::ShiftingLogFile#new, which are:
122
+ #
123
+ # <tt>:levels</tt>::
124
+ # Use a custom Sawmill::LevelGroup. Normally, you should leave this
125
+ # set to the default, which is Sawmill::STANDARD_LEVELS.
126
+ # <tt>:level</tt>::
127
+ # Default level to use for log messages when no level is explicitly
128
+ # provided. By default, this is set to the level group's default,
129
+ # which in the case of the standard levels is :INFO.
130
+ # <tt>:attribute_level</tt>::
131
+ # Default level to use for attributes when no level is explicitly
132
+ # provided. By default, this is set to the level group's highest,
133
+ # level, which in the case of the standard levels is :ANY.
134
+ # <tt>:progname</tt>::
135
+ # Progname to use in log messages. Default is "sawmill".
136
+ # <tt>:record_progname</tt>::
137
+ # Progname to use in special log entries dealing with log records
138
+ # (i.e. record delimiters and attribute messages). Default is the
139
+ # same as the normal progname setting.
140
+ # <tt>:record_id_generator</tt>::
141
+ # A proc that generates and returns a new record ID if one is not
142
+ # explicitly passed into begin_record. If you do not provide a
143
+ # generator, the default one is used, which generates an ID using the
144
+ # variant 4 (random) UUID standard.
145
+ # <tt>:include_id</tt>::
146
+ # Write the record ID in every log entry. Default is false.
147
+ # <tt>:fractional_second_digits</tt>::
148
+ # Number of digits of fractional seconds to write in timestamps.
149
+ # Default is 2. Accepted values are 0 to 6.
150
+ # <tt>:level_width</tt>::
151
+ # Column width of the level field.
152
+ # <tt>:local_time</tt>::
153
+ # If true, outputs local time with the timezone offset indicator.
154
+ # If false (the default), outputs UTC.
155
+ # <tt>:iso_8601_time</tt>::
156
+ # If true, outputs time in strict ISO 8601 format.
157
+ # If false (the default), outputs a slightly more readable format.
158
+ # <tt>:omit_directives</tt>::
159
+ # If true, omit standard logfile directives. Default is false.
160
+ # <tt>:basedir</tt>::
161
+ # The base directory used if the filepath is a relative path.
162
+ # If not specified, the current working directory is used.
163
+ # <tt>:history_size</tt>::
164
+ # The maximum number of old logfiles (files with indexes) to
165
+ # keep. Files beyond this history size will be automatically
166
+ # deleted. Default is 1. This value must be at least 1.
167
+
168
+ def shifting_logfile(filepath_, period_, max_size_, opts_={})
169
+ rotater_ = Rotater.new(Rotater::ShiftingLogFile, opts_.merge(:filepath => filepath_,
170
+ :max_logfile_size => max_size_, :shift_period => period_))
171
+ processor_ = EntryProcessor::Format.new(rotater_, opts_)
172
+ Logger.new(opts_.merge(:processor => processor_))
173
+ end
174
+
175
+
176
+ # Creates a new logger that writes to a logfile that rotates
177
+ # automatically by tagging filenames with a datestamp.
178
+ #
179
+ # You must provide the file path prefix, and a turnover frequency.
180
+ # Possible values for the turnover frequency are <tt>:yearly</tt>,
181
+ # <tt>:monthly</tt>, <tt>:daily</tt>, <tt>:hourly</tt>, and
182
+ # <tt>:never</tt>.
183
+ #
184
+ # You may pass the same options taken by Sawmill::Logger#new,
185
+ # Sawmill::EntryProcessor::Format#new, Sawmill::Rotater#new, and
186
+ # Sawmill::Rotater::DateBasedLogFile#new, which are:
187
+ #
188
+ # <tt>:levels</tt>::
189
+ # Use a custom Sawmill::LevelGroup. Normally, you should leave this
190
+ # set to the default, which is Sawmill::STANDARD_LEVELS.
191
+ # <tt>:level</tt>::
192
+ # Default level to use for log messages when no level is explicitly
193
+ # provided. By default, this is set to the level group's default,
194
+ # which in the case of the standard levels is :INFO.
195
+ # <tt>:attribute_level</tt>::
196
+ # Default level to use for attributes when no level is explicitly
197
+ # provided. By default, this is set to the level group's highest,
198
+ # level, which in the case of the standard levels is :ANY.
199
+ # <tt>:progname</tt>::
200
+ # Progname to use in log messages. Default is "sawmill".
201
+ # <tt>:record_progname</tt>::
202
+ # Progname to use in special log entries dealing with log records
203
+ # (i.e. record delimiters and attribute messages). Default is the
204
+ # same as the normal progname setting.
205
+ # <tt>:record_id_generator</tt>::
206
+ # A proc that generates and returns a new record ID if one is not
207
+ # explicitly passed into begin_record. If you do not provide a
208
+ # generator, the default one is used, which generates an ID using the
209
+ # variant 4 (random) UUID standard.
210
+ # <tt>:include_id</tt>::
211
+ # Write the record ID in every log entry. Default is false.
212
+ # <tt>:fractional_second_digits</tt>::
213
+ # Number of digits of fractional seconds to write in timestamps.
214
+ # Default is 2. Accepted values are 0 to 6.
215
+ # <tt>:level_width</tt>::
216
+ # Column width of the level field.
217
+ # <tt>:local_time</tt>::
218
+ # If true, outputs local time with the timezone offset indicator.
219
+ # If false (the default), outputs UTC.
220
+ # <tt>:iso_8601_time</tt>::
221
+ # If true, outputs time in strict ISO 8601 format.
222
+ # If false (the default), outputs a slightly more readable format.
223
+ # <tt>:omit_directives</tt>::
224
+ # If true, omit standard logfile directives. Default is false.
225
+ # <tt>:basedir</tt>::
226
+ # The base directory used if the filepath is a relative path.
227
+ # If not specified, the current working directory is used.
228
+ # <tt>:suffix</tt>::
229
+ # The logfile name prefix.
230
+ # In the filename "rails.2009-10-11.log", the suffix is ".log".
231
+ # If not specified, defaults to ".log".
232
+ # <tt>:local_datestamps</tt>::
233
+ # If true, use the local timezone to create datestamps.
234
+ # The default is to use UTC.
235
+
236
+ def date_based_logfile(filepath_, frequency_, opts_={})
237
+ rotater_ = Rotater.new(Rotater::DateBasedLogFile, opts_.merge(:prefix => filepath_,
238
+ :turnover_frequency => frequency_))
239
+ processor_ = EntryProcessor::Format.new(rotater_, opts_)
240
+ Logger.new(opts_.merge(:processor => processor_))
241
+ end
242
+
243
+
244
+ # Open one or more log files and run them through an entry processor.
245
+ # The processor is built on the fly using the EntryProcessor DSL.
246
+ # See EntryProcessor#build for more details.
247
+ #
248
+ # You may pass the same options taken by Sawmill::MultiParser#new,
249
+ # which are:
250
+ #
251
+ # <tt>:levels</tt>::
252
+ # Sawmill::LevelGroup to use to parse log levels.
253
+ # If not specified, Sawmill::STANDARD_LEVELS is used by default.
254
+ # <tt>:emit_incomplete_records_at_eof</tt>::
255
+ # If set to true, causes any incomplete log records to be emitted
256
+ # in their incomplete state when EOF is reached on all streams.
257
+
258
+ def open_entries(globs_, opts_={}, &block_)
259
+ processor_ = EntryProcessor.build(&block_)
260
+ open_files(globs_, processor_, opts_.merge(:finish => true))
261
+ end
262
+
263
+
264
+ # Open one or more log files and run them through a record processor.
265
+ # The processor is built on the fly using the RecordProcessor DSL.
266
+ # See RecordProcessor#build for more details.
267
+ #
268
+ # You may pass the same options taken by Sawmill::MultiParser#new,
269
+ # which are:
270
+ #
271
+ # <tt>:levels</tt>::
272
+ # Sawmill::LevelGroup to use to parse log levels.
273
+ # If not specified, Sawmill::STANDARD_LEVELS is used by default.
274
+ # <tt>:emit_incomplete_records_at_eof</tt>::
275
+ # If set to true, causes any incomplete log records to be emitted
276
+ # in their incomplete state when EOF is reached on all streams.
277
+
278
+ def open_records(globs_, opts_={}, &block_)
279
+ processor_ = RecordProcessor.build(&block_)
280
+ open_files(globs_, processor_, opts_.merge(:finish => true))
281
+ end
282
+
283
+
284
+ # Open one or more log files and run them through the given
285
+ # EntryProcessor or RecordProcessor.
286
+ #
287
+ # You may pass the same options taken by Sawmill::MultiParser#new,
288
+ # which are:
289
+ #
290
+ # <tt>:levels</tt>::
291
+ # Sawmill::LevelGroup to use to parse log levels.
292
+ # If not specified, Sawmill::STANDARD_LEVELS is used by default.
293
+ # <tt>:emit_incomplete_records_at_eof</tt>::
294
+ # If set to true, causes any incomplete log records to be emitted
295
+ # in their incomplete state when EOF is reached on all streams.
296
+ # <tt>:finish</tt>::
297
+ # If set to true, the "finish" method is called on the processor
298
+ # after all files have been parsed, and the return value is returned.
299
+ # Otherwise, the processor is left open and nil is returned.
300
+
301
+ def open_files(globs_, processor_, opts_={})
302
+ io_array_ = []
303
+ globs_ = [globs_] unless globs_.kind_of?(::Array)
304
+ begin
305
+ globs_.each do |glob_|
306
+ ::Dir.glob(glob_).each do |path_|
307
+ io_ = ::File.open(path_)
308
+ io_ = ::Zlib::GzipReader.new(io_) if path_ =~ /\.gz$/
309
+ io_array_ << io_
310
+ end
311
+ end
312
+ MultiParser.new(io_array_, processor_, opts_).parse_all
313
+ ensure
314
+ io_array_.each do |io_|
315
+ io_.close rescue nil
316
+ end
317
+ end
318
+ opts_[:finish] ? processor_.finish : nil
319
+ end
320
+
321
+
322
+ end
323
+
324
+ end
@@ -89,7 +89,7 @@ module Sawmill
89
89
  @attribute_level = @levels.highest
90
90
  end
91
91
  @progname = opts_[:progname] || 'sawmill'
92
- @record_progname = opts_[:record_progname] || @progname
92
+ @record_progname = opts_[:record_progname]
93
93
  @record_id_generator = opts_[:record_id_generator] || Logger._get_default_record_id_generator
94
94
  @processor = opts_[:processor] || Formatter.new(::STDOUT)
95
95
  @current_record_id = nil
@@ -162,7 +162,7 @@ module Sawmill
162
162
  def begin_record(id_=nil)
163
163
  end_record if @current_record_id
164
164
  @current_record_id = (id_ || @record_id_generator.call).to_s
165
- @processor.begin_record(Entry::BeginRecord.new(@levels.highest, ::Time.now, @record_progname, @current_record_id))
165
+ @processor.begin_record(Entry::BeginRecord.new(@levels.highest, ::Time.now, @record_progname || @progname, @current_record_id))
166
166
  @current_record_id
167
167
  end
168
168
 
@@ -181,7 +181,7 @@ module Sawmill
181
181
 
182
182
  def end_record
183
183
  if @current_record_id
184
- @processor.end_record(Entry::EndRecord.new(@levels.highest, ::Time.now, @record_progname, @current_record_id))
184
+ @processor.end_record(Entry::EndRecord.new(@levels.highest, ::Time.now, @record_progname || @progname, @current_record_id))
185
185
  id_ = @current_record_id
186
186
  @current_record_id = nil
187
187
  id_
@@ -210,7 +210,7 @@ module Sawmill
210
210
  end
211
211
  end
212
212
  return true if level_obj_ < @level
213
- @processor.attribute(Entry::Attribute.new(level_obj_, ::Time.now, progname_ || @record_progname, @current_record_id, key_, value_, operation_))
213
+ @processor.attribute(Entry::Attribute.new(level_obj_, ::Time.now, progname_ || @record_progname || @progname, @current_record_id, key_, value_, operation_))
214
214
  true
215
215
  end
216
216
 
@@ -233,11 +233,12 @@ module Sawmill
233
233
  end
234
234
 
235
235
 
236
- # Close the logger by closing the log entry processor to which it is
237
- # emitting log entries.
236
+ # Close the logger by finishing the log entry processor to which it is
237
+ # emitting log entries. Returns the value returned by the processor's
238
+ # finish method.
238
239
 
239
240
  def close
240
- @processor.close
241
+ @processor.finish
241
242
  end
242
243
 
243
244
 
@@ -72,6 +72,7 @@ module Sawmill
72
72
  @message_count = 0
73
73
  @entries = []
74
74
  @attributes = {}
75
+ @computations = {}
75
76
  if entries_ && entries_.size > 0
76
77
  entries_.each do |entry_|
77
78
  add_entry(entry_)
@@ -259,6 +260,19 @@ module Sawmill
259
260
  end
260
261
 
261
262
 
263
+ # Compute and cache a value.
264
+ # This is a convenient way for RecordProcessor objects to share
265
+ # computed information about a record.
266
+ #
267
+ # Returns the computed value with the given key.
268
+ # If the given key has not been computed yet, computes it by
269
+ # calling the given block and passing self.
270
+
271
+ def compute(key_)
272
+ @computations[key_] ||= yield self
273
+ end
274
+
275
+
262
276
  end
263
277
 
264
278
 
@@ -0,0 +1,113 @@
1
+ # -----------------------------------------------------------------------------
2
+ #
3
+ # Sawmill record processor that generates reports
4
+ #
5
+ # -----------------------------------------------------------------------------
6
+ # Copyright 2009 Daniel Azuma
7
+ #
8
+ # All rights reserved.
9
+ #
10
+ # Redistribution and use in source and binary forms, with or without
11
+ # modification, are permitted provided that the following conditions are met:
12
+ #
13
+ # * Redistributions of source code must retain the above copyright notice,
14
+ # this list of conditions and the following disclaimer.
15
+ # * Redistributions in binary form must reproduce the above copyright notice,
16
+ # this list of conditions and the following disclaimer in the documentation
17
+ # and/or other materials provided with the distribution.
18
+ # * Neither the name of the copyright holder, nor the names of any other
19
+ # contributors to this software, may be used to endorse or promote products
20
+ # derived from this software without specific prior written permission.
21
+ #
22
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
23
+ # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
24
+ # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
25
+ # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
26
+ # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
27
+ # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
28
+ # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
29
+ # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
30
+ # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
31
+ # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
32
+ # POSSIBILITY OF SUCH DAMAGE.
33
+ # -----------------------------------------------------------------------------
34
+ ;
35
+
36
+
37
+ module Sawmill
38
+
39
+
40
+ module RecordProcessor
41
+
42
+
43
+ # This processor collects and formats reports from descendant
44
+ # record processors.
45
+
46
+ class CompileReport < All
47
+
48
+
49
+ # Create a report collection.
50
+ #
51
+ # Recognized options include:
52
+ #
53
+ # <tt>:postprocessor</tt>::
54
+ # Postprocessor proc for individual reports.
55
+ # <tt>:separator</tt>::
56
+ # Separator string for reports. Default is a single newline.
57
+ # <tt>:header</tt>::
58
+ # Header string for the final compiled report.
59
+ # Default is the empty string.
60
+ # <tt>:footer</tt>::
61
+ # Footer string for the final compiled report.
62
+ # Default is the empty string.
63
+
64
+ def initialize(*children_)
65
+ opts_ = children_.last.kind_of?(::Hash) ? children_.pop : {}
66
+ @postprocessor = opts_[:postprocessor]
67
+ @separator = opts_[:separator] || "\n"
68
+ @header = opts_[:header] || ''
69
+ @footer = opts_[:footer] || ''
70
+ super(*children_)
71
+ end
72
+
73
+
74
+ # Separator string to be inserted between individual reports.
75
+ attr_accessor :separator
76
+
77
+ # Header string for the final compiled report.
78
+ attr_accessor :header
79
+
80
+ # Footer string for the final compiled report.
81
+ attr_accessor :footer
82
+
83
+
84
+ # Provide a postprocessor block for individual report values.
85
+ # This block should take a single parameter and return a string
86
+ # that should be included in the compiled report. It may also
87
+ # return nil to indicate that the data should not be included.
88
+
89
+ def to_postprocess_value(&block_)
90
+ @postprocessor = block_
91
+ end
92
+
93
+
94
+ # On finish, this processor calls finish on its descendants, converts
95
+ # their values into strings and compiles them into a report. It then
96
+ # returns that report as a string.
97
+
98
+ def finish
99
+ values_ = super || []
100
+ values_ = [values_] unless values_.kind_of?(::Array)
101
+ values_.map!{ |val_| @postprocessor.call(val_) } if @postprocessor
102
+ values_.compact!
103
+ "#{@header}#{values_.join(@separator)}#{@footer}"
104
+ end
105
+
106
+
107
+ end
108
+
109
+
110
+ end
111
+
112
+
113
+ end