Class: Tap::Support::Audit

Inherits:
Object show all
Defined in:
lib/tap/support/audit.rb

Overview

Audit provides a way to track the values passed among tasks or, more generally, any Executable. Audits collectively build a directed acyclic graph of task execution and have great utility in debugging and record keeping.

Audits record a key, a current value, and the previous audit(s) in the trail. Keys are arbitrary identifiers of where the value comes from. To illustrate, lets use symbols as keys.

# initialize a new audit
_a = Audit.new(:one, 1)
_a.key                              # => :one
_a.value                            # => 1

# build a short trail
_b = Audit.new(:two, 2, _a)
_c = Audit.new(:three, 3, _b)

_a.sources                          # => []
_b.sources                          # => [_a]
_c.sources                          # => [_b]

Audits allow you track back through the sources of each audit to build a trail describing how a particular value was produced.

_c.trail                            # => [_a,_b,_c]
_c.trail {|audit| audit.key }       # => [:one, :two, :three]
_c.trail {|audit| audit.value }     # => [1,2,3]

Any number of audits may share the same source, so forks are naturally supported.

_d = Audit.new(:four, 4, _b)
_d.trail                            # => [_a,_b,_d]

_e = Audit.new(:five, 5, _b)
_e.trail                            # => [_a,_b,_e]

Merges are supported by specifying more than one source. Merges have the effect of nesting audit trails within an array:

_f = Audit.new(:six, 6)
_g = Audit.new(:seven, 7, _f)
_h = Audit.new(:eight, 8, [_c,_d,_g])
_h.trail                            # => [[[_a,_b,_c], [_a,_b,_d], [_f,_g]], _h]

Nesting can get quite ugly after a couple merges so Audit provides a scalable pretty-print dump that helps visualize the audit trail.

"\n" + _h.dump
# => %q{
# o-[one] 1
# o-[two] 2
# |
# |-o-[three] 3
# | |
# `---o-[four] 4
#   | |
#   | | o-[six] 6
#   | | o-[seven] 7
#   | | |
#   `-`-`-o-[eight] 8
# }

In practice, tasks are recorded as keys. Thus audit trails can be used to access task configurations and other information that may be useful when creating reports or making workflow decisions. Note that by convention audits and non-audit methods that return audits are prefixed with an underscore.

– Note Audit could easily be expanded to track sinks as well as sources. In initialize:

@sinks = []
sources.each do |source|
  source.sinks << self
end

The downside is that this may not circumvent cleanly if you want light or no auditing. It also adds additonal references which will prevent garbage collection. On the plus side, sinks will make it easier to truly use Audits as a DAG

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(key = nil, value = nil, sources = nil) ⇒ Audit

Initializes a new Audit. Sources may be an array, a single value (which is turned into an array), or nil (indicating no sources).

_a = Audit.new(nil, nil, nil)
_a.sources                        # => []

_b = Audit.new(nil, nil, _a)
_b.sources                        # => [_a]

_c = Audit.new(nil, nil, [_a,_b])
_c.sources                        # => [_a,_b]


227
228
229
230
231
# File 'lib/tap/support/audit.rb', line 227

def initialize(key=nil, value=nil, sources=nil)
  @key = key
  @value = value
  @source = singularize(sources)
end

Instance Attribute Details

#keyObject (readonly)

A key for self (typically the task producing value, or nil if the value has an unknown origin)



210
211
212
# File 'lib/tap/support/audit.rb', line 210

def key
  @key
end

#valueObject (readonly)

The current value



213
214
215
# File 'lib/tap/support/audit.rb', line 213

def value
  @value
end

Class Method Details

.dump(audits, target = $stdout) ⇒ Object

Produces a pretty-print dump of the specified audits to target. A block may be provided to format the trailer of each line.



92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
# File 'lib/tap/support/audit.rb', line 92

def dump(audits, target=$stdout) # :yields: audit
  return dump(audits, target) do |audit| 
    "o-[#{audit.key}] #{audit.value.inspect}"
  end unless block_given?
  
  # arrayify audits
  audits = [audits].flatten
  
  # the order of audits
  order = []
  
  # (audit, sinks) hash preventing double iteration over 
  # audits, and identifying sinks for a particular audit
  sinks = {}
  
  # iterate over all audits, collecting in order
  audits.each do |audit|
    traverse(audit, order, sinks)
  end
  
  # visit each audit, collecting audits into indent groups
  groups = []
  group = nil
  order.each do |audit|
    sources = audit.sources
    unless sources.length == 1 && sinks[sources[0]].length <= 1
      group = []
      groups << group
    end
    
    group << audit
  end
  
  # identify nodes at which a fork occurs... these are audits
  # that have more than one sink, and they cause a fork-style
  # leader to be printed
  forks = {}
  sinks.each_pair do |audit, sinks|
    n = sinks.length
    forks[audit] = [0, n] if n > 1
  end
  
  # setup print
  index = 0
  leader = ""
  
  # print each group
  groups.each do |group|
    sources = group[0].sources
    complete = audits.include?(group[-1])
    
    case 
    when sources.length > 1
      # print a merge
      # `-`-`-o-[merge]
      
      leader =~ /^(.*)((\| *){#{sources.length}})$/
      leader = "#{$1}#{' ' * $2.length} "
      target << "#{$1}#{$2.gsub('|', '`').gsub(' ', '-')}-#{yield(group.shift)}\n"
      
    when fork = forks[sources[0]]
      # print a fork
      # |-o-[a]
      # |
      # `---o-[b]
      
      n = fork[0] += 1
      base = leader[0, leader.length - (2 * n - 1)]
      target << "#{base}#{fork[0] == fork[1] ? '`-' : '|-'}#{'--' * (n-1)}#{yield(group.shift)}\n"
      leader  = "#{base}#{fork[0] == fork[1] ? '  ' : '| '}#{'| ' * (n-1)}"
      
    when index > 0
      # simply get ready to print the next series of audits
      # o-[a]
      # o-[b]
      
      leader = "#{leader} "
      leader = "" if leader.strip.empty?
    end
    
    # print the next series of audits
    group.each do |audit|
      target << "#{leader}#{yield(audit)}\n"
    end
    
    # add a continuation line, if necessary
    unless group == groups.last
      if complete
        leader = "#{leader} "
      else
        leader = "#{leader}|"
      end
      target << "#{leader}\n"
    end
    
    index += 1
  end
  
  target
end

Instance Method Details

#dump(&block) ⇒ Object

A kind of pretty-print for Audits.



306
307
308
# File 'lib/tap/support/audit.rb', line 306

def dump(&block)
  Audit.dump(self, "", &block)
end

#sourcesObject

An array of source audits for self. Sources may be empty.



234
235
236
# File 'lib/tap/support/audit.rb', line 234

def sources
  arrayify(@source)
end

#splatObject

Produces a fork of self for each item in value, using the index of the item as a key. Splat is useful for developing each item of an array value along different paths.

_a = Audit.new(nil, [:x, :y, :z])
_b,_c,_d = _a.splat

_b.key                            # => 0
_b.value                          # => :x

_c.key                            # => 1
_c.value                          # => :y

_d.key                            # => 2
_d.value                          # => :z
_d.trail                          # => [_a,_d]

If value does not respond to ‘each’, an array with self as the only member will be returned. This ensures that the result of splat is an array of audits ready for further development.

_a = Audit.new(nil, :value)
_a.splat                          # => [_a]


262
263
264
265
266
267
268
269
270
271
272
# File 'lib/tap/support/audit.rb', line 262

def splat
  return [self] unless value.respond_to?(:each)
  
  collection = []
  index = 0
  value.each do |obj|
    collection << Audit.new(index, obj, self)
    index += 1
  end
  collection
end

#trail(trail = [], &block) ⇒ Object

Recursively collects an audit trail leading to self. Single sources are collected into the trail directly, while multiple sources are collected into arrays.

_a = Audit.new(:one, 1)
_b = Audit.new(:two, 2, _a)
_b.trail                          # => [_a,_b]

_a = Audit.new(:one, 1)
_b = Audit.new(:two, 2)
_c = Audit.new(:three, 3, [_a, _b])
_c.trail                          # => [[[_a],[_b]],_c]

A block may be provided to collect a specific audit attribute instead of the audit itself.

_c.trail {|audit| audit.value }   # => [[[1],[2]],3]


292
293
294
295
296
297
298
299
300
301
302
303
# File 'lib/tap/support/audit.rb', line 292

def trail(trail=[], &block)
  trail.unshift(block_given? ? block.call(self) : self)
  
  case @source
  when Audit
    @source.trail(trail, &block)
  when Array
    trail.unshift @source.collect {|audit| audit.trail(&block) }
  end
  
  trail
end