chook 1.0.1.b2 → 1.1.5

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 (45) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGES.md +56 -0
  3. data/README.md +363 -127
  4. data/bin/chook-server +31 -1
  5. data/data/chook.conf.example +183 -0
  6. data/data/com.pixar.chook-server.plist +20 -0
  7. data/data/sample_handlers/RestAPIOperation.rb +11 -11
  8. data/data/sample_handlers/SmartGroupComputerMembershipChange.rb +3 -6
  9. data/data/sample_jsons/SmartGroupComputerMembershipChange.json +3 -1
  10. data/data/sample_jsons/SmartGroupMobileDeviceMembershipChange.json +3 -1
  11. data/lib/chook/configuration.rb +27 -8
  12. data/lib/chook/event.rb +6 -1
  13. data/lib/chook/event/handled_event.rb +36 -9
  14. data/lib/chook/event/handled_event/handlers.rb +260 -98
  15. data/lib/chook/event/handled_event_logger.rb +86 -0
  16. data/lib/chook/event_handling.rb +1 -0
  17. data/lib/chook/foundation.rb +3 -0
  18. data/lib/chook/procs.rb +17 -1
  19. data/lib/chook/server.rb +73 -72
  20. data/lib/chook/server/auth.rb +164 -0
  21. data/lib/chook/server/log.rb +215 -0
  22. data/lib/chook/server/public/css/chook.css +133 -0
  23. data/lib/chook/server/public/imgs/ChookLogoAlMcWhiggin.png +0 -0
  24. data/lib/chook/server/public/js/chook.js +126 -0
  25. data/lib/chook/server/public/js/logstream.js +101 -0
  26. data/lib/chook/server/routes.rb +28 -0
  27. data/lib/chook/server/routes/handle_by_name.rb +65 -0
  28. data/lib/chook/server/routes/handle_webhook_event.rb +27 -3
  29. data/lib/chook/server/routes/handlers.rb +52 -0
  30. data/lib/chook/server/routes/home.rb +48 -1
  31. data/lib/chook/server/routes/log.rb +105 -0
  32. data/lib/chook/server/routes/login_logout.rb +48 -0
  33. data/lib/chook/server/views/admin.haml +11 -0
  34. data/lib/chook/server/views/bak.haml +48 -0
  35. data/lib/chook/server/views/config.haml +15 -0
  36. data/lib/chook/server/views/handlers.haml +63 -0
  37. data/lib/chook/server/views/layout.haml +64 -0
  38. data/lib/chook/server/views/logstream.haml +33 -0
  39. data/lib/chook/server/views/sketch_admin +44 -0
  40. data/lib/chook/subject.rb +13 -2
  41. data/lib/chook/subject/dep_device.rb +81 -0
  42. data/lib/chook/subject/policy_finished.rb +43 -0
  43. data/lib/chook/subject/smart_group.rb +6 -0
  44. data/lib/chook/version.rb +1 -1
  45. metadata +79 -19
@@ -1,36 +1,45 @@
1
- ### Copyright 2017 Pixar
2
-
3
- ###
4
- ### Licensed under the Apache License, Version 2.0 (the "Apache License")
5
- ### with the following modification; you may not use this file except in
6
- ### compliance with the Apache License and the following modification to it:
7
- ### Section 6. Trademarks. is deleted and replaced with:
8
- ###
9
- ### 6. Trademarks. This License does not grant permission to use the trade
10
- ### names, trademarks, service marks, or product names of the Licensor
11
- ### and its affiliates, except as required to comply with Section 4(c) of
12
- ### the License and to reproduce the content of the NOTICE file.
13
- ###
14
- ### You may obtain a copy of the Apache License at
15
- ###
16
- ### http://www.apache.org/licenses/LICENSE-2.0
17
- ###
18
- ### Unless required by applicable law or agreed to in writing, software
19
- ### distributed under the Apache License with the above modification is
20
- ### distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
21
- ### KIND, either express or implied. See the Apache License for the specific
22
- ### language governing permissions and limitations under the Apache License.
23
- ###
24
- ###
1
+ # Copyright 2017 Pixar
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "Apache License")
4
+ # with the following modification; you may not use this file except in
5
+ # compliance with the Apache License and the following modification to it:
6
+ # Section 6. Trademarks. is deleted and replaced with:
7
+ #
8
+ # 6. Trademarks. This License does not grant permission to use the trade
9
+ # names, trademarks, service marks, or product names of the Licensor
10
+ # and its affiliates, except as required to comply with Section 4(c) of
11
+ # the License and to reproduce the content of the NOTICE file.
12
+ #
13
+ # You may obtain a copy of the Apache License at
14
+ #
15
+ # http://www.apache.org/licenses/LICENSE-2.0
16
+ #
17
+ # Unless required by applicable law or agreed to in writing, software
18
+ # distributed under the Apache License with the above modification is
19
+ # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
20
+ # KIND, either express or implied. See the Apache License for the specific
21
+ # language governing permissions and limitations under the Apache License.
22
+ #
23
+ #
24
+
25
25
  module Chook
26
26
 
27
- # This method is used by the Ruby event-handler files.
27
+ # This method is used by the Ruby 'internal' event-handler files.
28
+ #
29
+ # those handlers are defined by passing a block to this method, like so:
30
+ #
31
+ # Chook.event_handler do |event|
32
+ # # so something with the event
33
+ # end
28
34
  #
29
- # Loading them should call this method and pass in a block
35
+ # Loading them will call this method and pass in a block
30
36
  # with one parameter: a Chook::HandledEvent subclass instance.
31
37
  #
32
- # The block is then converted to a Proc instance in @loaded_event_handler
33
- # and from there can be stored for use by the event identified by the filename.
38
+ # The block is then converted to a #handle method in an anonymous object.
39
+ # The object is stored for use by the event identified by the filename.
40
+ #
41
+ # By storing it as a method in an object, the handlers themselves
42
+ # can use #break or #return to exit (or even #next)
34
43
  #
35
44
  # NOTE: the files should be read with 'load' not 'require', so that they can
36
45
  # be re-loaded as needed
@@ -44,7 +53,13 @@ module Chook
44
53
  # @return [Proc] the block converted to a Proc
45
54
  #
46
55
  def self.event_handler(&block)
47
- HandledEvent::Handlers.loaded_handler = Proc.new(&block)
56
+ obj = Object.new
57
+ obj.define_singleton_method(:handle, &block)
58
+ # Loading the file created the object by calling this method
59
+ # but to access it after loading the file, we need to
60
+ # store it in here:
61
+ HandledEvent::Handlers.loaded_handler = obj
62
+ Chook.logger.debug "Code block for 'Chook.event_handler' loaded into \#handle method of runner-object #{obj.object_id}"
48
63
  end
49
64
 
50
65
  # the server class
@@ -53,56 +68,104 @@ module Chook
53
68
  # The Handlers namespace module
54
69
  module Handlers
55
70
 
56
- # Module Constants
57
- ############################
71
+ # Don't load any handlers whose filenames start with this
72
+ DO_NOT_LOAD_PREFIX = 'Ignore-'.freeze
58
73
 
59
74
  DEFAULT_HANDLER_DIR = '/Library/Application Support/Chook'.freeze
60
75
 
61
- # Module Instance Variables, & accessors
62
- ############################
76
+ # Handlers that are only called by name using the route:
77
+ # post '/handler/:handler_name'
78
+ # are located in this subdirection of the handler directory
79
+ NAMED_HANDLER_SUBDIR = 'NamedHandlers'.freeze
63
80
 
64
- # This holds the most recently loaded Proc handler
65
- # until it can be stored in the @handlers Hash
66
- @loaded_handler = nil
81
+ # internal handler files must match this regex somewhere
82
+ INTERNAL_HANDLER_BLOCK_START_RE = /Chook.event_handler( ?\{| do) *\|/
67
83
 
68
- # Getter for @loaded_handler
84
+ # self loaded_handler=
69
85
  #
70
- # @return [Proc,nil] the most recent Proc loaded from a handler file.
86
+ # @return [Obj,nil] the most recent Proc loaded from a handler file.
71
87
  # destined for storage in @handlers
72
88
  #
73
89
  def self.loaded_handler
74
90
  @loaded_handler
75
91
  end
76
92
 
77
- # Setter for @loaded_event_handler
93
+ # A holding place for internal handlers as they are loaded
94
+ # before being added to the @handlers Hash
95
+ # see Chook.event_handler(&block)
78
96
  #
79
- # @param a_proc [Proc] a Proc object for storage in @handlers
97
+ # @param a_proc [Object] An object instance with a #handle method
80
98
  #
81
- def self.loaded_handler=(a_proc)
82
- @loaded_handler = a_proc
99
+ def self.loaded_handler=(anon_obj)
100
+ @loaded_handler = anon_obj
83
101
  end
84
102
 
85
- # A hash of loaded handlers.
86
- # Keys are Strings - the name of the events handled
87
- # Values are Arrays of either Procs, or Pathnames to executable files.
88
- # See the .handlers getter Methods
89
- @handlers = {}
90
-
91
- # Getter for @event_handlers
103
+ # Getter for @handlers
104
+ #
105
+ # @return [Hash{String => Array}] a mapping of Event Names as they
106
+ # come from the JSS to an Array of handlers for the event.
107
+ # The handlers are either Pathnames to executable external handlers
108
+ # or Objcts with a #handle method, for internal handlers
109
+ # (The objects also have a #handler_file attribute that is the Pathname)
92
110
  #
93
- # @return [Hash{String => Array}] a mapping of Event Names as the come from
94
- # the JSS to an Array of handlers for the event. The handlers are either
95
- # Proc objects to call from within ruby, or Pathnames to executable files
96
- # which will take raw JSON on stdin.
97
111
  def self.handlers
98
- @handlers
112
+ @handlers ||= {}
99
113
  end
100
114
 
101
- # Module Methods
102
- ############################
115
+ # Handlers can check Chook::HandledEvent::Handlers.reloading?
116
+ # and do stuff if desired.
117
+ def self.reloading?
118
+ @reloading
119
+ end
120
+
121
+ # getter for @named_handlers
122
+ # These handlers are called by name via the route
123
+ # " post '/handler/:handler_name'"
124
+ #
125
+ # They are not tied to any event type by their filenames
126
+ # its up to the writers of the handlers to make sure
127
+ # the webhook that calls them is sending the correct event
128
+ # type.
129
+ #
130
+ # The data structure of @named_handlers is a
131
+ # Hash of Strings to Pathnames or Anon Objects:
132
+ # {
133
+ # handler_filename => Pathname or Obj,
134
+ # handler_filename => Pathname or Obj,
135
+ # handler_filename => Pathname or Obj
136
+ # }
137
+ #
138
+ # @return [Hash {String => Pathname, Proc}]
139
+ def self.named_handlers
140
+ @named_handlers ||= {}
141
+ end
142
+
143
+ # the Pathname objects for all loaded handlers
144
+ #
145
+ # @return [Array<Pathname>]
146
+ #
147
+ def self.all_handler_paths
148
+ hndlrs = named_handlers.values
149
+ hndlrs += handlers.values.flatten
150
+ hndlrs.map do |hndlr|
151
+ hndlr.is_a?(Pathname) ? hndlr : hndlr.handler_file
152
+ end
153
+ end
103
154
 
104
155
  # Load all the event handlers from the handler_dir or an arbitrary dir.
105
156
  #
157
+ #
158
+ # Handler files must be either:
159
+ # - An executable file, which will have the raw JSON from the JSS piped
160
+ # to it's stdin when executed
161
+ # or
162
+ # - A non-executable file of ruby code like this:
163
+ # Chook.event_handler do |event|
164
+ # # your code goes here.
165
+ # end
166
+ #
167
+ # (see the Chook README for details about writing the ruby handlers)
168
+ #
106
169
  # @param from_dir [String, Pathname] directory from which to load the
107
170
  # handlers. Defaults to CONFIG.handler_dir or DEFAULT_HANDLER_DIR if
108
171
  # config is unset
@@ -112,27 +175,55 @@ module Chook
112
175
  #
113
176
  # @return [void]
114
177
  #
115
- def self.load_handlers(from_dir: Chook::CONFIG.handler_dir, reload: false)
178
+ def self.load_handlers(from_dir: Chook.config.handler_dir, reload: false)
179
+ # use default if needed
116
180
  from_dir ||= DEFAULT_HANDLER_DIR
181
+ handler_dir = Pathname.new(from_dir)
182
+ named_handler_dir = handler_dir + NAMED_HANDLER_SUBDIR
183
+ load_type = 'Loading'
184
+
117
185
  if reload
118
- @handlers_loaded_from = nil
186
+ @reloading = true
119
187
  @handlers = {}
188
+ @named_handlers = {}
120
189
  @loaded_handler = nil
190
+ load_type = 'Re-loading'
121
191
  end
122
192
 
123
- handler_dir = Pathname.new(from_dir)
124
- return unless handler_dir.directory? && handler_dir.readable?
193
+ # General Handlers
194
+ Chook.logger.info "#{load_type} general handlers from directory: #{handler_dir}"
195
+ if handler_dir.directory? && handler_dir.readable?
196
+ handler_dir.children.each do |handler_file|
197
+ # ignore if marked to
198
+ next if handler_file.basename.to_s.start_with? DO_NOT_LOAD_PREFIX
199
+
200
+ load_general_handler(handler_file) if handler_file.file? && handler_file.readable?
201
+ end
202
+ Chook.logger.info handlers.empty? ? 'No general handlers found' : "Loaded #{handlers.values.flatten.size} general handlers for #{handlers.keys.size} event triggers"
203
+ else
204
+ Chook.logger.error "General handler directory '#{from_dir}' not a readable directory. No general handlers loaded. "
205
+ end
206
+
207
+ # Named Handlers
208
+ Chook.logger.info "#{load_type} named handlers from directory: #{named_handler_dir}"
209
+ if named_handler_dir.directory? && named_handler_dir.readable?
210
+ named_handler_dir.children.each do |handler_file|
211
+ # ignore if marked to
212
+ next if handler_file.basename.to_s.start_with? DO_NOT_LOAD_PREFIX
125
213
 
126
- handler_dir.children.each do |handler_file|
127
- load_handler(handler_file) if handler_file.file? && handler_file.readable?
214
+ load_named_handler(handler_file) if handler_file.file? && handler_file.readable?
215
+ end
216
+ Chook.logger.info "Loaded #{named_handlers.size} named handlers"
217
+ else
218
+ Chook.logger.error "Named handler directory '#{named_handler_dir}' not a readable directory. No named handlers loaded. "
128
219
  end
129
220
 
130
- @handlers_loaded_from = handler_dir
131
- @handlers.values.flatten.size
221
+ @reloading = false
132
222
  end # load handlers
133
223
 
134
- # Load an event handler from a file.
135
- # Handler files must begin with the name of the event they handle,
224
+ # Load a general event handler from a file.
225
+ #
226
+ # General Handler files must begin with the name of the event they handle,
136
227
  # e.g. ComputerAdded, followed by: nothing, a dot, a dash, or
137
228
  # and underscore. Case doesn't matter.
138
229
  # So all of these are OK:
@@ -142,49 +233,113 @@ module Chook
142
233
  # Computeradded-update-ldap
143
234
  # There can be as many as desired for each event.
144
235
  #
145
- # Each must be either:
146
- # - An executable file, which will have the raw JSON from the JSS piped
147
- # to it's stdin when executed
148
- # or
149
- # - A non-executable file of ruby code like this:
150
- # Chook.event_handler do |event|
151
- # # your code goes here.
152
- # end
153
- #
154
- # (see the Chook README for details about writing the ruby handlers)
155
- #
156
- # @param from_file [Pathname] the file from which to load the handler
236
+ # @param handler_file [Pathname] the file from which to load the handler
157
237
  #
158
238
  # @return [void]
159
239
  #
160
- def self.load_handler(from_file)
161
- handler_file = Pathname.new from_file
240
+ def self.load_general_handler(handler_file)
241
+ Chook.logger.debug "Starting load of general handler file '#{handler_file.basename}'"
242
+
162
243
  event_name = event_name_from_handler_filename(handler_file)
163
- return unless event_name
244
+ unless event_name
245
+ Chook.logger.debug "Ignoring general handler file '#{handler_file.basename}': Filename doesn't start with event name"
246
+ return
247
+ end
164
248
 
165
249
  # create an array for this event's handlers, if needed
166
- @handlers[event_name] ||= []
250
+ handlers[event_name] ||= []
167
251
 
252
+ # external? if so, its executable and we only care about its pathname
168
253
  if handler_file.executable?
169
- # store as a Pathname, we'll pipe JSON to it
170
- unless @handlers[event_name].include? handler_file
171
- @handlers[event_name] << handler_file
172
- puts "===> Loaded executable handler file '#{handler_file.basename}'"
173
- end
254
+ Chook.logger.info "Loading external general handler file '#{handler_file.basename}' for #{event_name} events"
255
+ handlers[event_name] << handler_file
174
256
  return
175
257
  end
176
258
 
177
- # load the file. If written correctly, it will
178
- # put a Proc into @loaded_handler
179
- load handler_file.to_s
180
- if @loaded_handler
181
- @handlers[event_name] << @loaded_handler
182
- puts "===> Loaded internal handler file '#{handler_file.basename}'"
183
- @loaded_handler = nil
259
+ # Internal, we store an object with a .handle method
260
+ Chook.logger.info "Loading internal general handler file '#{handler_file.basename}' for #{event_name} events"
261
+ load_internal_handler handler_file
262
+ handlers[event_name] << @loaded_handler if @loaded_handler
263
+
264
+ end # self.load_general_handler(handler_file)
265
+
266
+ # Load a named event handler from a file.
267
+ #
268
+ # Named Handler files can have any name, as they are called directly
269
+ # from a Jamf webhook via URL.
270
+ #
271
+ # @param handler_file [Pathname] the file from which to load the handler
272
+ #
273
+ # @return [void]
274
+ #
275
+ def self.load_named_handler(handler_file)
276
+ Chook.logger.debug "Starting load of named handler file '#{handler_file.basename}'"
277
+
278
+ # external? if so, its executable and we only care about its pathname
279
+ if handler_file.executable?
280
+ Chook.logger.info "Loading external named handler file '#{handler_file.basename}'"
281
+ named_handlers[handler_file.basename.to_s] = handler_file
282
+ return
283
+ end
284
+
285
+ # Internal, we store an object with a .handle method
286
+ Chook.logger.info "Loading internal named handler file '#{handler_file.basename}'"
287
+ load_internal_handler handler_file
288
+ named_handlers[handler_file.basename.to_s] = @loaded_handler if @loaded_handler
289
+ end # self.load_general_handler(handler_file)
290
+
291
+ # if the given file is executable, store it's path as a handler for the event
292
+ #
293
+ # @return [Boolean] did we load an external handler?
294
+ #
295
+ def self.load_external_handler(handler_file, event_name, named)
296
+ return false unless handler_file.executable?
297
+
298
+ say_named = named ? 'named ' : ''
299
+ Chook.logger.info "Loading #{say_named}external handler file '#{handler_file.basename}' for #{event_name} events"
300
+
301
+ if named
302
+ named_handlers[event_name][handler_file.basename.to_s] = handler_file
184
303
  else
185
- puts "===> FAILED loading internal handler file '#{handler_file.basename}'"
304
+ # store the Pathname, we'll pipe JSON to it
305
+ handlers[event_name] << handler_file
306
+ end
307
+
308
+ true
309
+ end
310
+
311
+ # if a given path is not executable, try to load it as an internal handler
312
+ #
313
+ # @param handler_file[Pathname] the handler file
314
+ #
315
+ # @return [Object] and anonymous object that has a .handle method
316
+ #
317
+ def self.load_internal_handler(handler_file)
318
+ # load the file. If written correctly, it will
319
+ # put an anon. Object with a #handle method into @loaded_handler
320
+ unless handler_file.read =~ INTERNAL_HANDLER_BLOCK_START_RE
321
+ Chook.logger.error "Internal handler file '#{handler_file}' missing event_handler block"
322
+ return nil
323
+ end
324
+
325
+ # reset @loaded_handler - the `load` call will refill it
326
+ # see Chook.event_handler
327
+ @loaded_handler = nil
328
+
329
+ begin
330
+ load handler_file.to_s
331
+ raise '@loaded handler nil after loading file' unless @loaded_handler
332
+ rescue => e
333
+ Chook.logger.error "FAILED loading internal handler file '#{handler_file}': #{e}"
334
+ return
186
335
  end
187
- end # self.load_handler(handler_file)
336
+
337
+ # add a method to the object to get its Pathname
338
+ @loaded_handler.define_singleton_method(:handler_file) { handler_file }
339
+
340
+ # return it
341
+ @loaded_handler
342
+ end
188
343
 
189
344
  # Given a handler filename, return the event name it wants to handle
190
345
  #
@@ -194,9 +349,16 @@ module Chook
194
349
  # @return [String,nil] The matching event name or nil if no match
195
350
  #
196
351
  def self.event_name_from_handler_filename(filename)
352
+ filename = filename.basename
197
353
  @event_names ||= Chook::Event::EVENTS.keys
198
- desired_event_name = filename.basename.to_s.split(/\.|-|_/).first
199
- @event_names.select { |n| desired_event_name.casecmp(n).zero? }.first
354
+ desired_event_name = filename.to_s.split(/\.|-|_/).first
355
+ ename = @event_names.select { |n| desired_event_name.casecmp(n).zero? }.first
356
+ if ename
357
+ Chook.logger.debug "Found event name '#{ename}' at start of filename '#{filename}'"
358
+ else
359
+ Chook.logger.debug "No known event name at start of filename '#{filename}'"
360
+ end
361
+ ename
200
362
  end
201
363
 
202
364
  end # module Handler
@@ -0,0 +1,86 @@
1
+ ### Copyright 2017 Pixar
2
+
3
+ ###
4
+ ### Licensed under the Apache License, Version 2.0 (the "Apache License")
5
+ ### with the following modification; you may not use this file except in
6
+ ### compliance with the Apache License and the following modification to it:
7
+ ### Section 6. Trademarks. is deleted and replaced with:
8
+ ###
9
+ ### 6. Trademarks. This License does not grant permission to use the trade
10
+ ### names, trademarks, service marks, or product names of the Licensor
11
+ ### and its affiliates, except as required to comply with Section 4(c) of
12
+ ### the License and to reproduce the content of the NOTICE file.
13
+ ###
14
+ ### You may obtain a copy of the Apache License at
15
+ ###
16
+ ### http://www.apache.org/licenses/LICENSE-2.0
17
+ ###
18
+ ### Unless required by applicable law or agreed to in writing, software
19
+ ### distributed under the Apache License with the above modification is
20
+ ### distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
21
+ ### KIND, either express or implied. See the Apache License for the specific
22
+ ### language governing permissions and limitations under the Apache License.
23
+ ###
24
+ ###
25
+
26
+ module Chook
27
+
28
+ # a simple object embedded in a Handled Event that
29
+ # allows a standardize way to note event-related log entries
30
+ # with the event object_id.
31
+ #
32
+ # Every Handled Event has one of these instances exposed in it's
33
+ # #logger attribute, and usable from within 'internal' handlers
34
+ #
35
+ # Here's an example.
36
+ #
37
+ # Say you have a ComputerSmartGroupMembershipChanged event
38
+ #
39
+ # calling `event.logger.info "foobar"` will generate the log message:
40
+ #
41
+ # Event 1234567: foobar
42
+ #
43
+ class HandledEventLogger
44
+
45
+ def initialize(event)
46
+ @event = event
47
+ end
48
+
49
+ def event_message(msg)
50
+ "Event #{@event.id}: #{msg}"
51
+ end
52
+
53
+ def debug(msg)
54
+ Chook::Server::Log.logger.debug event_message(msg)
55
+ end
56
+
57
+ def info(msg)
58
+ Chook::Server::Log.logger.info event_message(msg)
59
+ end
60
+
61
+ def warn(msg)
62
+ Chook::Server::Log.logger.warn event_message(msg)
63
+ end
64
+
65
+ def error(msg)
66
+ Chook::Server::Log.logger.error event_message(msg)
67
+ end
68
+
69
+ def fatal(msg)
70
+ Chook::Server::Log.logger.fatal event_message(msg)
71
+ end
72
+
73
+ def unknown(msg)
74
+ Chook::Server::Log.logger.unknown event_message(msg)
75
+ end
76
+
77
+ # log an exception - multiple log lines
78
+ # the first being the error message the rest being indented backtrace
79
+ def log_exception(exception)
80
+ error "#{exception.class}: #{exception}"
81
+ exception.backtrace.each { |l| error "..#{l}" }
82
+ end
83
+
84
+ end # class HandledEventLogger
85
+
86
+ end # module