Structured Log
Class StructuredLog offers structured (as opposed to flat) logging. Nested sections (blocks) in Ruby code become nested XML elements in the log.
This sectioning allows you to group actions in your program, and that grouping carries over into the log.
Optionally, each section may include:
- A timestamp.
- A duration.
- The ability to rescue and log an exception.
And of course the class offers many ways to log data.
About the Examples
A working example is worth a thousand words (maybe).
Each of the following sections features an example Ruby program, followed by its output log.
Sections
Nested Sections
Use nested sections to give structure to your log.
sections.rb:
require 'structured_log'
StructuredLog.open('sections.xml') do |log|
# Any code can be here.
log.section('Outer') do
# Any code can be here.
log.section('Mid') do
# Any code can be here.
log.section('Inner') do
# Any code can be here.
end
# Any code can be here.
end
# Any code can be here.
end
# Any code can be here.
endsections.xml:
<log>
<section name='Outer'>
<section name='Mid'>
<section name='Inner'/>
</section>
</section>
</log>Text
Add text to a section element by passing a string argument.
text.rb:
require 'structured_log'
text = 'This section has text.'
StructuredLog.open('text.xml') do |log|
log.section('with_text', text) do
# Any code can be here.
end
endtext.xml:
<log>
<section name='with_text'>
This section has text.
</section>
</log>Attributes
Add attributes to a section element by passing a hash argument.
attributes.rb:
require 'structured_log'
attributes = {:a => 0, :b => 1}
StructuredLog.open('attributes.xml') do |log|
log.section('with_attributes', attributes) do
log.comment('This section has attributes a and b.')
end
endattributes.xml:
<log>
<section name='with_attributes' a='0' b='1'>
<comment>
This section has attributes a and b.
</comment>
</section>
</log>Timestamps and Durations
Add a timestamp or duration to a section element by passing a special symbol argument.
time.rb:
require 'structured_log'
StructuredLog.open('time.xml') do |log|
log.section('Section with timestamp', :timestamp) do
log.comment('This section has a timestamp.')
end
log.section('Section with duration', :duration) do
log.comment('This section has a duration.')
sleep 1
end
log.section('Section with both', :duration, :timestamp) do
log.comment('This section has both.')
sleep 1
end
endtime.xml:
<log>
<section name='Section with timestamp' timestamp='2019-03-19-Tue-13.50.02.358'>
<comment>
This section has a timestamp.
</comment>
</section>
<section name='Section with duration' duration_seconds='1.000'>
<comment>
This section has a duration.
</comment>
</section>
<section name='Section with both' timestamp='2019-03-19-Tue-13.50.03.359' duration_seconds='1.000'>
<comment>
This section has both.
</comment>
</section>
</log>Rescued Section
Add rescuing to a section element by passing a special symbol argument.
For the rescued exception, the class, message, and backtrace are logged.
rescue.rb:
require 'structured_log'
StructuredLog.open('rescue.xml') do |log|
log.section('Section with rescue', :rescue) do
log.comment('This section will terminate because of the failure.')
fail 'This exception will be rescued and logged.'
# This comment will not be reached.
end
log.section('Another section') do
log.comment('This comment will be reached and logged, because of rescue above.')
end
endrescue.xml:
<log>
<section name='Section with rescue'>
<comment>
This section will terminate because of the failure.
</comment>
<rescued_exception timestamp='2019-03-19-Tue-13.50.01.509' class='RuntimeError'>
<message>
This exception will be rescued and logged.
</message>
<backtrace>
<![CDATA[
C:/structured_log/readme_files/scripts/rescue.rb:6:in `block (2 levels) in <main>'
C:/Ruby25-x64/lib/ruby/gems/2.5.0/gems/structured_log-0.9.0/lib/structured_log.rb:58:in `block in section'
C:/Ruby25-x64/lib/ruby/gems/2.5.0/gems/structured_log-0.9.0/lib/structured_log.rb:116:in `put_element'
C:/Ruby25-x64/lib/ruby/gems/2.5.0/gems/structured_log-0.9.0/lib/structured_log.rb:57:in `section'
C:/structured_log/readme_files/scripts/rescue.rb:4:in `block in <main>'
C:/Ruby25-x64/lib/ruby/gems/2.5.0/gems/structured_log-0.9.0/lib/structured_log.rb:38:in `open'
C:/structured_log/readme_files/scripts/rescue.rb:3:in `<main>'
]]>
</backtrace>
</rescued_exception>
</section>
<section name='Another section'>
<comment>
This comment will be reached and logged, because of rescue above.
</comment>
</section>
</log>Potpourri
Pass any mixture of arguments to method section.
The section name must be first; after that, anything goes.
potpourri.rb:
require 'structured_log'
attributes = {:a => 0, :b => 1}
more_attributes = {:c => 2, :d => 3}
text = 'This section has a potpourri.'
float = 3.14159
boolean = false
fixnum = 1066
time = Time.new
exception = RuntimeError.new('Oops!')
StructuredLog.open('potpourri.xml') do |log|
log.section('All together now', 'Order does not matter except in aggregating text and attributes.') do
args = [attributes, :rescue, text, float, :duration, more_attributes, boolean, :timestamp, fixnum, time, exception]
log.section('Potpourri', *args) do
end
log.section('Reverse potpourri', *args.reverse) do
end
end
endpotpourri.xml:
<log>
<section name='All together now'>
Order does not matter except in aggregating text and attributes.
<section name='Potpourri' a='0' b='1' c='2' d='3' timestamp='2019-03-19-Tue-13.49.59.631' duration_seconds='0.000'>
This section has a potpourri.3.14159false10662019-03-19 13:49:59
-0500#<RuntimeError: Oops!>
</section>
<section name='Reverse potpourri' timestamp='2019-03-19-Tue-13.49.59.631' c='2' d='3' a='0' b='1' duration_seconds='0.000'>
#<RuntimeError: Oops!>2019-03-19 13:49:59 -05001066false3.14159This
section has a potpourri.
</section>
</section>
</log>Data
Hash-LIke Objects
Use method put_each_pair, or its alias put_hash, to log an object that respond_to?(:each_pair).
hash.rb:
require 'structured_log'
hash = {
:a => 'z',
:aa => 'zz',
:aaa => 'zzz',
:aaaa => 'zzzz',
}
StructuredLog.open('hash.xml') do |log|
log.put_hash('my_hash', hash)
endhash.xml:
<log>
<each_pair name='my_hash' class='Hash'>
<![CDATA[
a => z
aa => zz
aaa => zzz
aaaa => zzzz
]]>
</each_pair>
</log>Array-Like Objects
Use method put_each_with_index, or its aliases put_array and put_set, to log an object that respond_to?(:each_with_index).
array.rb:
require 'structured_log'
array = %w/foo bar baz bat/
StructuredLog.open('array.xml') do |log|
log.put_array('my_array', array)
endarray.xml:
<log>
<each_with_index name='my_array' class='Array'>
<![CDATA[
0 foo
1 bar
2 baz
3 bat
]]>
</each_with_index>
</log>Other Objects
Use method put_data to log any object.
data.rb:
require 'structured_log'
data = {
:float => 3.14,
:integer => 1066,
:false => false,
:time => Time.new,
:exception => RuntimeError.new('Oops!'),
:nil => nil,
}
StructuredLog.open('data.xml') do |log|
data.each_pair do |type, datum|
name = "my_#{type}"
log.put_data(name, datum)
end
enddata.xml:
<log>
<data name='my_float' class='Float'>
3.14
</data>
<data name='my_integer' class='Integer'>
1066
</data>
<data name='my_false' class='FalseClass'>
false
</data>
<data name='my_time' class='Time'>
2019-03-19 13:49:58 -0500
</data>
<data name='my_exception' class='RuntimeError'>
Oops!
</data>
<data name='my_nil' class='NilClass'/>
</log>Formatted Text
Use method put_cdata to log a string (possibly multi-line) as CDATA.
cdata.rb:
require 'structured_log'
text = <<EOT
Method put_cdata puts the data verbatim:
* Nothing is added or subtracted.
* Not even whitespace.
So you can use the method to log a formatted string.
(You'll need to add your own leading and trailing newlines, if desired.)
EOT
StructuredLog.open('cdata.xml') do |log|
log.put_cdata(text)
endcdata.xml:
<log>
<![CDATA[
Method put_cdata puts the data verbatim:
* Nothing is added or subtracted.
* Not even whitespace.
So you can use the method to log a formatted string.
(You'll need to add your own leading and trailing newlines, if desired.)
]]>
</log>Comment
Use method comment to log a comment.
comment.rb:
require 'structured_log'
StructuredLog.open('comment.xml') do |log|
log.comment('My comment can be any text.')
endcomment.xml:
<log>
<comment>
My comment can be any text.
</comment>
</log>Custom Logging
At the heart of class StructuredLog is method put_element. It logs an element, possibly with children, attributes, and text. Several methods call it, and you can too.
Basically, it's just like method section, except that you choose the element name (instead of the fixed name section).
Otherwise, it handles a block and all the same arguments as section.
Section
Create a custom section by calling method put_element with a block. The custom section will have children if you call logging methods within the block.
custom_section.rb:
require 'structured_log'
StructuredLog.open('custom_section.xml') do |log|
log.section('With blocks') do
log.put_element('section_with_children') do
log.put_element('child', :rank => 'Older')
log.put_element('child', :rank => 'Younger')
end
log.put_element('section_with_duration', :duration, 'Block contains timed code to be timed.') do
sleep 1
end
log.put_element('section_with_rescue', :rescue, 'Block contains code to be rescued if necessary.') do
end
end
endcustom_section.xml:
<log>
<section name='With blocks'>
<section_with_children>
<child rank='Older'/>
<child rank='Younger'/>
</section_with_children>
<section_with_duration duration_seconds='1.000'>
Block contains timed code to be timed.
</section_with_duration>
<section_with_rescue>
Block contains code to be rescued if necessary.
</section_with_rescue>
</section>
</log>Entry
Create a custom entry by calling method put_element without a block. The custom entry will not have children.
custom_entry.rb:
require 'structured_log'
StructuredLog.open('custom_entry.xml') do |log|
log.section('Without blocks') do
log.put_element('element_with_text', 'No child elements, just this text.')
log.put_element('element_with_attributes', {:a => 0, :b => 1})
log.put_element('element_with_timestamp', :timestamp)
log.put_element('element_with_data', 3.14159)
end
endcustom_entry.xml:
<log>
<section name='Without blocks'>
<element_with_text>
No child elements, just this text.
</element_with_text>
<element_with_attributes a='0' b='1'/>
<element_with_timestamp timestamp='2019-03-19-Tue-13.49.57.173'/>
<element_with_data>
3.14159
</element_with_data>
</section>
</log>Uncaught Exception
Finally, what about an uncaught exception, one not rescued by :rescue?
When an exception is raised in a section that does not have :rescue, the logger rescues and logs it anyway, just as if there were an invisible "outermost section" with :rescue (which, in fact, there is).
Just as for a rescued exception, the log includes the exception's class, message, and backtrace.
exception.rb:
require 'structured_log'
StructuredLog.open('exception.xml') do |_|
fail('Oops!')
endexception.xml:
<log>
<uncaught_exception timestamp='2019-03-19-Tue-13.49.59.081' class='RuntimeError'>
<message>
Oops!
</message>
<backtrace>
<![CDATA[
C:/structured_log/readme_files/scripts/exception.rb:4:in `block in <main>'
C:/Ruby25-x64/lib/ruby/gems/2.5.0/gems/structured_log-0.9.0/lib/structured_log.rb:38:in `open'
C:/structured_log/readme_files/scripts/exception.rb:3:in `<main>'
]]>
</backtrace>
</uncaught_exception>
</log>









