sawmill 0.0.2 → 0.0.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.
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