structured_log 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (62) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +9 -0
  3. data/.travis.yml +5 -0
  4. data/CODE_OF_CONDUCT.md +74 -0
  5. data/Gemfile +6 -0
  6. data/Gemfile.lock +27 -0
  7. data/LICENSE +21 -0
  8. data/LICENSE.txt +21 -0
  9. data/README.md +620 -0
  10. data/Rakefile +44 -0
  11. data/bin/console +14 -0
  12. data/bin/setup +8 -0
  13. data/images/array.jpg +0 -0
  14. data/images/attributes.png +0 -0
  15. data/images/comment.png +0 -0
  16. data/images/custom.png +0 -0
  17. data/images/data.png +0 -0
  18. data/images/exception.png +0 -0
  19. data/images/hash.png +0 -0
  20. data/images/nesting.jpg +0 -0
  21. data/images/potpourri.png +0 -0
  22. data/images/rescue.jpg +0 -0
  23. data/images/structured.png +0 -0
  24. data/images/text.jpg +0 -0
  25. data/images/time.png +0 -0
  26. data/lib/structured_log.rb +322 -0
  27. data/lib/structured_log/version.rb +3 -0
  28. data/readme_files/README.template.md +167 -0
  29. data/readme_files/logs/array.xml +10 -0
  30. data/readme_files/logs/attributes.xml +7 -0
  31. data/readme_files/logs/cdata.xml +12 -0
  32. data/readme_files/logs/comment.xml +5 -0
  33. data/readme_files/logs/custom_entry.xml +12 -0
  34. data/readme_files/logs/custom_section.xml +14 -0
  35. data/readme_files/logs/data.xml +20 -0
  36. data/readme_files/logs/exception.xml +14 -0
  37. data/readme_files/logs/hash.xml +10 -0
  38. data/readme_files/logs/potpourri.xml +13 -0
  39. data/readme_files/logs/potpourri_other.xml +20 -0
  40. data/readme_files/logs/potpourri_usual.xml +8 -0
  41. data/readme_files/logs/rescue.xml +28 -0
  42. data/readme_files/logs/sections.xml +7 -0
  43. data/readme_files/logs/text.xml +5 -0
  44. data/readme_files/logs/time.xml +17 -0
  45. data/readme_files/scripts/array.rb +6 -0
  46. data/readme_files/scripts/attributes.rb +8 -0
  47. data/readme_files/scripts/cdata.rb +17 -0
  48. data/readme_files/scripts/comment.rb +5 -0
  49. data/readme_files/scripts/custom_entry.rb +10 -0
  50. data/readme_files/scripts/custom_section.rb +15 -0
  51. data/readme_files/scripts/data.rb +16 -0
  52. data/readme_files/scripts/exception.rb +5 -0
  53. data/readme_files/scripts/hash.rb +11 -0
  54. data/readme_files/scripts/potpourri.rb +20 -0
  55. data/readme_files/scripts/potpourri_other.rb +17 -0
  56. data/readme_files/scripts/potpourri_usual.rb +12 -0
  57. data/readme_files/scripts/rescue.rb +12 -0
  58. data/readme_files/scripts/sections.rb +17 -0
  59. data/readme_files/scripts/text.rb +8 -0
  60. data/readme_files/scripts/time.rb +15 -0
  61. data/structured_log.gemspec +38 -0
  62. metadata +176 -0
@@ -0,0 +1,44 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rake/testtask'
3
+ require 'markdown_helper'
4
+
5
+ Rake::TestTask.new(:test) do |t|
6
+ t.libs << 'test'
7
+ t.libs << 'lib'
8
+ t.test_files = FileList['test/**/*_test.rb']
9
+ end
10
+
11
+ namespace :build do
12
+
13
+ desc 'Build README.md file from README.template.md'
14
+ task :readme do
15
+ readme_dir_path = File.join(
16
+ File.dirname(__FILE__),
17
+ 'readme_files',
18
+ )
19
+ source_dir_path = File.join(
20
+ readme_dir_path,
21
+ 'scripts',
22
+ )
23
+ target_dir_path = File.join(
24
+ readme_dir_path,
25
+ 'logs',
26
+ )
27
+ source_file_paths = Dir.glob("#{source_dir_path}/*.rb")
28
+ # Run the scripts in the logs directory,
29
+ # because (e.g.) in array.rb, the :file_path
30
+ # to the output log file is the simple 'array.xml',
31
+ # which keeps the file more readable.
32
+ chdir(target_dir_path) do
33
+ source_file_paths.each do |source_file_path|
34
+ command = "ruby #{source_file_path}"
35
+ system(command)
36
+ end
37
+ end
38
+ markdown_helper = MarkdownHelper.new
39
+ markdown_helper.include('readme_files/README.template.md', 'README.md')
40
+ end
41
+
42
+ end
43
+
44
+ task :default => :test
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'bundler/setup'
4
+ require 'structured_log'
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require 'pry'
11
+ # Pry.start
12
+
13
+ require 'irb'
14
+ IRB.start(__FILE__)
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
@@ -0,0 +1,322 @@
1
+ require 'rexml/document'
2
+
3
+ class StructuredLog
4
+
5
+ attr_accessor \
6
+ :file,
7
+ :file_path,
8
+ :backtrace_filter,
9
+ :root_name,
10
+ :xml_indentation
11
+
12
+ include REXML
13
+
14
+ DEFAULT_FILE_NAME = 'log.xml'
15
+ DEFAULT_DIR_PATH = '.'
16
+ DEFAULT_XML_ROOT_TAG_NAME = 'log'
17
+ DEFAULT_XML_INDENTATION = 2
18
+
19
+ # Message for no block error.
20
+ NO_BLOCK_GIVEN_MSG = 'No block given'
21
+ # Message for calling-new error.
22
+ NO_NEW_MSG = format('Please use %s.open, not %s.new.', self.class.name, self.class.name)
23
+
24
+ # Callers should call this method, not method +new+.
25
+ # +file_path+ is the path to the output log file.
26
+ # Options can include:
27
+ # - :root_name => _root-xml-tag-name_.
28
+ # - :xml_indentation => Integer: indentation for nesting XML sub-elements.
29
+ def self.open(file_path = File.join(DEFAULT_DIR_PATH, DEFAULT_FILE_NAME), options=Hash.new)
30
+ raise NO_BLOCK_GIVEN_MSG unless (block_given?)
31
+ default_options = Hash[
32
+ :root_name => DEFAULT_XML_ROOT_TAG_NAME,
33
+ :xml_indentation => DEFAULT_XML_INDENTATION
34
+ ]
35
+ options = default_options.merge(options)
36
+ log = self.new(file_path, options, im_ok_youre_not_ok = true)
37
+ begin
38
+ yield log
39
+ rescue => x
40
+ log.put_element('uncaught_exception', :timestamp, :class => x.class) do
41
+ log.put_element('message', x.message)
42
+ log.put_element('backtrace') do
43
+ backtrace = log.send(:filter_backtrace, x.backtrace)
44
+ log.send(:put_cdata, backtrace)
45
+ end
46
+ end
47
+ end
48
+ log.send(:dispose)
49
+ log.file_path
50
+ end
51
+
52
+ # Start a new section, within the current section.
53
+ # Sections may be nested.
54
+ # - +name+: name for the section.
55
+ # - *+args+: passed to method +put_element+.
56
+ def section(name, *args)
57
+ put_element('section', {:name => name}, *args) do
58
+ yield
59
+ end
60
+ nil
61
+ end
62
+
63
+ def comment(text, *args)
64
+ put_element('comment', text, *args)
65
+ nil
66
+ end
67
+
68
+ # Log an XML element.
69
+ # - +element_name+: Element name for logged element.
70
+ # - *+args+: Anything; processed left to right; for each _arg_:
71
+ # - +Hash+: becomes element attributes.
72
+ # - +String+: appended to PCDATA.
73
+ # - +:timestamp+: causes a timestamp to be added to the element.
74
+ # - +:duration+: causes block's execution duration to be added to the element.
75
+ # - +:rescue+: causes any exception raised during block execution to be rescued and logged.
76
+ # - else: _arg_.inspect is appended to PCDATA.
77
+ def put_element(element_name = 'element', *args)
78
+ attributes = {}
79
+ pcdata = ''
80
+ start_time = nil
81
+ duration_to_be_included = false
82
+ block_to_be_rescued = false
83
+ args.each do |arg|
84
+ case
85
+ when arg.kind_of?(Hash)
86
+ attributes.merge!(arg)
87
+ when arg.kind_of?(String)
88
+ pcdata += arg
89
+ when arg == :timestamp
90
+ attributes[:timestamp] = StructuredLog.timestamp
91
+ when arg == :duration
92
+ duration_to_be_included = true
93
+ when arg == :rescue
94
+ block_to_be_rescued = true
95
+ else
96
+ pcdata = pcdata + arg.inspect
97
+ end
98
+ end
99
+ log_puts("BEGIN\t#{element_name}")
100
+ put_attributes(attributes)
101
+ unless pcdata.empty?
102
+ # Guard against using a terminator that's a substring of pcdata.
103
+ s = 'EOT'
104
+ terminator = s
105
+ while pcdata.match(terminator) do
106
+ terminator += s
107
+ end
108
+ log_puts("PCDATA\t<<#{terminator}")
109
+ log_puts(pcdata)
110
+ log_puts(terminator)
111
+ end
112
+ start_time = Time.new if duration_to_be_included
113
+ if block_given?
114
+ if block_to_be_rescued
115
+ begin
116
+ yield
117
+ rescue Exception => x
118
+ put_element('rescued_exception', :timestamp, :class => x.class) do
119
+ put_element('message', x.message)
120
+ put_element('backtrace') do
121
+ put_cdata(filter_backtrace(x.backtrace))
122
+ end
123
+ end
124
+ end
125
+ else
126
+ yield
127
+ end
128
+ end
129
+ if start_time
130
+ end_time = Time.now
131
+ duration_f = end_time.to_f - start_time.to_f
132
+ duration_s = format('%.3f', duration_f)
133
+ put_attributes({:duration_seconds => duration_s})
134
+ end
135
+ log_puts("END\t#{element_name}")
136
+ nil
137
+ end
138
+
139
+ def put_each_with_index(name, obj)
140
+ lines = ['']
141
+ obj.each_with_index do |item, i|
142
+ lines.push(format('%6d %s', i, item.to_s))
143
+ end
144
+ lines.push('')
145
+ lines.push('')
146
+ put_element('each_with_index', :name => name, :class => obj.class) do
147
+ put_cdata(lines.join("\n"))
148
+ end
149
+ nil
150
+ end
151
+ alias put_array put_each_with_index
152
+ alias put_set put_each_with_index
153
+
154
+ def put_each_pair(name, obj)
155
+ lines = ['']
156
+ obj.each_pair do |key, value|
157
+ lines.push(format('%s => %s', key, value))
158
+ end
159
+ lines.push('')
160
+ lines.push('')
161
+ put_element('each_pair', :name => name, :class => obj.class) do
162
+ put_cdata(lines.join("\n"))
163
+ end
164
+ nil
165
+ end
166
+ alias put_hash put_each_pair
167
+
168
+ def put_data(name, obj)
169
+ put_element('data', :name => name, :class => obj.class) do
170
+ put_cdata(obj.inspect)
171
+ end
172
+ end
173
+
174
+ def put_cdata(text)
175
+ # Guard against using a terminator that's a substring of the cdata.
176
+ s = 'EOT'
177
+ terminator = s
178
+ while text.match(terminator) do
179
+ terminator += s
180
+ end
181
+ log_puts("CDATA\t<<#{terminator}")
182
+ log_puts(text)
183
+ log_puts(terminator)
184
+ nil
185
+ end
186
+
187
+ private
188
+
189
+ def initialize(file_path = File.join(DEFAULT_DIR_PATH, DEFAULT_FILE_NAME), options = Hash.new, im_ok_youre_not_ok = false)
190
+ unless im_ok_youre_not_ok
191
+ # Caller should call StructuredLog.open, not StructuredLog.new.
192
+ raise RuntimeError.new(NO_NEW_MSG)
193
+ end
194
+ self.file_path = file_path
195
+ self.root_name = options[:root_name]
196
+ self.xml_indentation = options[:xml_indentation]
197
+ self.backtrace_filter = options[:backtrace_filter] || /structured_log|ruby/
198
+ self.file = File.open(self.file_path, 'w')
199
+ log_puts("REMARK\tThis text log is the precursor for an XML log.")
200
+ log_puts("REMARK\tIf the logged process completes, this text will be converted to XML.")
201
+ log_puts("BEGIN\t#{self.root_name}")
202
+ nil
203
+ end
204
+
205
+ def dispose
206
+
207
+ # Close the text log.
208
+ log_puts("END\t#{self.root_name}")
209
+ self.file.close
210
+
211
+ # Create the xml log.
212
+ document = REXML::Document.new
213
+ File.open(self.file_path, 'r') do |file|
214
+ element = document
215
+ stack = Array.new
216
+ data_a = nil
217
+ terminator = nil
218
+ file.each_line do |line|
219
+ line.chomp!
220
+ line_type, text = line.split("\t", 2)
221
+ case line_type
222
+ when 'REMARK'
223
+ next
224
+ when 'BEGIN'
225
+ element_name = text
226
+ element = element.add_element(element_name)
227
+ stack.push(element)
228
+ when 'END'
229
+ stack.pop
230
+ element = stack.last
231
+ when 'ATTRIBUTE'
232
+ attr_name, attr_value = text.split("\t", 2)
233
+ element.add_attribute(attr_name, attr_value)
234
+ when 'CDATA'
235
+ stack.push(:cdata)
236
+ data_a = Array.new
237
+ terminator = text.split('<<', 2).last
238
+ when 'PCDATA'
239
+ stack.push(:pcdata)
240
+ data_a = Array.new
241
+ terminator = text.split('<<', 2).last
242
+ when terminator
243
+ data_s = data_a.join("\n")
244
+ data_a = nil
245
+ terminator = nil
246
+ data_type = stack.last
247
+ case data_type
248
+ when :cdata
249
+ cdata = CData.new(data_s)
250
+ element.add(cdata)
251
+ when :pcdata
252
+ element.add_text(data_s)
253
+ else
254
+ # Don't want to raise an exception and spoil the run
255
+ end
256
+ stack.pop
257
+ else
258
+ data_a.push(line) if (terminator)
259
+ end
260
+ end
261
+ document << XMLDecl.default
262
+ end
263
+
264
+ File.open(self.file_path, 'w') do |file|
265
+ document.write(file, self.xml_indentation)
266
+ file.puts('')
267
+ end
268
+ nil
269
+ end
270
+
271
+ def put_attributes(attributes)
272
+ attributes.each_pair do |name, value|
273
+ value = case
274
+ when value.is_a?(String)
275
+ value
276
+ when value.is_a?(Symbol)
277
+ value.to_s
278
+ else
279
+ value.inspect
280
+ end
281
+ log_puts("ATTRIBUTE\t#{name}\t#{value}")
282
+ end
283
+ nil
284
+ end
285
+
286
+ def log_puts(text)
287
+ self.file.puts(text)
288
+ self.file.flush
289
+ nil
290
+ end
291
+
292
+ # Filters lines, to make the backtrace more readable.
293
+ def filter_backtrace(lines)
294
+ filtered = []
295
+ lines.each do |line|
296
+ unless line.match(self.backtrace_filter)
297
+ filtered.push(line)
298
+ end
299
+ end
300
+ filtered = lines if filtered.empty?
301
+ filtered.push('')
302
+ filtered.push('')
303
+ filtered.unshift('')
304
+ filtered.join("\n")
305
+ end
306
+
307
+ # Return a timestamp string.
308
+ # The important property of this string
309
+ # is that it can be incorporated into a legal directory path
310
+ # (i.e., has no colons, etc.).
311
+ def self.timestamp
312
+ now = Time.now
313
+ ts = now.strftime('%Y-%m-%d-%a-%H.%M.%S')
314
+ usec_s = (now.usec / 1000).to_s
315
+ while usec_s.length < 3 do
316
+ usec_s = '0' + usec_s
317
+ end
318
+ # noinspection RubyUnusedLocalVariable
319
+ ts += ".#{usec_s}"
320
+ end
321
+
322
+ end
@@ -0,0 +1,3 @@
1
+ class StructuredLog
2
+ VERSION = '0.9.0'
3
+ end
@@ -0,0 +1,167 @@
1
+ # Structured Log
2
+
3
+ <img src="images/structured.png" height="70" alt="Structured Log">
4
+
5
+ Class <code>StructuredLog</code> offers structured (as opposed to flat) logging. Nested sections (blocks) in Ruby code become nested XML elements in the log.
6
+
7
+ This sectioning allows you to group actions in your program, and that grouping carries over into the log.
8
+
9
+ Optionally, each section may include:
10
+ <ul>
11
+ <li>A timestamp.
12
+ <li>A duration.
13
+ <li>The ability to rescue and log an exception.
14
+ </ul>
15
+
16
+ And of course the class offers many ways to log data.
17
+
18
+ ## About the Examples
19
+
20
+ A working example is worth a thousand words (maybe).
21
+
22
+ Each of the following sections features an example Ruby program, followed by its output log.
23
+
24
+
25
+ ## Sections
26
+
27
+ ### Nested Sections
28
+ <img src="images/nesting.jpg" height="70" alt="Nesting">
29
+
30
+ Use nested sections to give structure to your log.
31
+
32
+ @[ruby](scripts/sections.rb)
33
+
34
+ @[xml](logs/sections.xml)
35
+
36
+ ### Text
37
+ <img src="images/text.jpg" height="70" alt="Text">
38
+
39
+ Add text to a <code>section</code> element by passing a string argument.
40
+
41
+ @[ruby](scripts/text.rb)
42
+
43
+ @[xml](logs/text.xml)
44
+
45
+ ### Attributes
46
+ <img src="images/attributes.png" height="70" alt="Attributes">
47
+
48
+ Add attributes to a <code>section</code> element by passing a hash argument.
49
+
50
+ @[ruby](scripts/attributes.rb)
51
+
52
+ @[xml](logs/attributes.xml)
53
+
54
+ ### Timestamps and Durations
55
+ <img src="images/time.png" height="70" alt="Time">
56
+
57
+ Add a timestamp or duration to a <code>section</code> element by passing a special symbol argument.
58
+
59
+ @[ruby](scripts/time.rb)
60
+
61
+ @[xml](logs/time.xml)
62
+
63
+ ### Rescued Section
64
+ <img src="images/rescue.jpg" height="70" alt="Rescue">
65
+
66
+ Add rescuing to a <code>section</code> element by passing a special symbol argument.
67
+
68
+ For the rescued exception, the class, message, and backtrace are logged.
69
+
70
+ @[ruby](scripts/rescue.rb)
71
+
72
+ @[xml](logs/rescue.xml)
73
+
74
+ ### Potpourri
75
+ <img src="images/potpourri.png" height="70" alt="Potpourri">
76
+
77
+ Pass any mixture of arguments to method <code>section</code>.
78
+
79
+ The section name must be first; after that, anything goes.
80
+
81
+ @[ruby](scripts/potpourri.rb)
82
+
83
+ @[xml](logs/potpourri.xml)
84
+
85
+ ## Data
86
+
87
+ ### Hash-LIke Objects
88
+ <img src="images/hash.png" height="30" alt="Hash">
89
+
90
+ Use method <code>put_each_pair</code>, or its alias <code>put_hash</code>, to log an object that <code>respond_to?(:each_pair)</code>.
91
+
92
+ @[ruby](scripts/hash.rb)
93
+
94
+ @[xml](logs/hash.xml)
95
+
96
+ ### Array-Like Objects
97
+ <img src="images/array.jpg" height="30" alt="Array">
98
+
99
+ Use method <code>put_each_with_index</code>, or its aliases <code>put_array</code> and <code>put_set</code>, to log an object that <code>respond_to?(:each_with_index)</code>.
100
+
101
+ @[ruby](scripts/array.rb)
102
+
103
+ @[xml](logs/array.xml)
104
+
105
+ ### Other Objects
106
+ <img src="images/data.png" height="70" alt="Data">
107
+
108
+ Use method <code>put_data</code> to log any object.
109
+
110
+ @[ruby](scripts/data.rb)
111
+
112
+ @[xml](logs/data.xml)
113
+
114
+ ### Formatted Text
115
+
116
+ Use method <code>put_cdata</code> to log a string (possibly multi-line) as CDATA.
117
+
118
+ @[ruby](scripts/cdata.rb)
119
+
120
+ @[xml](logs/cdata.xml)
121
+
122
+ ### Comment
123
+ <img src="images/comment.png" height="70" alt="Comment">
124
+
125
+ Use method <code>comment</code> to log a comment.
126
+
127
+ @[ruby](scripts/comment.rb)
128
+
129
+ @[xml](logs/comment.xml)
130
+
131
+ ## Custom Logging
132
+ <img src="images/custom.png" width="70" alt="Custom">
133
+
134
+ At the heart of class <code>StructuredLog</code> is method <code>put_element</code>. It logs an element, possibly with children, attributes, and text. Several methods call it, and you can too.
135
+
136
+ Basically, it's just like method <code>section</code>, except that you choose the element name (instead of the fixed name <code>section</code>).
137
+
138
+ Otherwise, it handles a block and all the same arguments as <code>section</code>.
139
+
140
+ ### Section
141
+
142
+ Create a custom section by calling method <code>put_element</code> with a block. The custom section will have children if you call logging methods within the block.
143
+
144
+ @[ruby](scripts/custom_section.rb)
145
+
146
+ @[xml](logs/custom_section.xml)
147
+
148
+ ### Entry
149
+
150
+ Create a custom entry by calling method <code>put_element</code> without a block. The custom entry will not have children.
151
+
152
+ @[ruby](scripts/custom_entry.rb)
153
+
154
+ @[xml](logs/custom_entry.xml)
155
+
156
+ ## Uncaught Exception
157
+ <img src="images/exception.png" width="70" alt="Exception">
158
+
159
+ Finally, what about an uncaught exception, one not rescued by <code>:rescue</code>?
160
+
161
+ When an exception is raised in a section that does not have <code>:rescue</code>, the logger rescues and logs it anyway, just as if there were an invisible "outermost section" with <code>:rescue</code> (which, in fact, there is).
162
+
163
+ Just as for a rescued exception, the log includes the exception's class, message, and backtrace.
164
+
165
+ @[ruby](scripts/exception.rb)
166
+
167
+ @[xml](logs/exception.xml)