chook 1.0.1.b2 → 1.1.5

Sign up to get free protection for your applications and to get access to all the features.
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