wackamole 0.0.1

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 (146) hide show
  1. data/.bnsignore +16 -0
  2. data/.gitignore +2 -0
  3. data/History.txt +4 -0
  4. data/README.rdoc +88 -0
  5. data/Rakefile +42 -0
  6. data/aa.rb +25 -0
  7. data/aaa.txt +13 -0
  8. data/bin/wackamole +77 -0
  9. data/lib/app.rb +63 -0
  10. data/lib/controllers/dashboard.rb +30 -0
  11. data/lib/controllers/features.rb +41 -0
  12. data/lib/controllers/logs.rb +49 -0
  13. data/lib/controllers/mission.rb +27 -0
  14. data/lib/controllers/users.rb +41 -0
  15. data/lib/helpers/dashboard_helper.rb +32 -0
  16. data/lib/helpers/features_helper.rb +13 -0
  17. data/lib/helpers/flash_helper.rb +16 -0
  18. data/lib/helpers/logs_helper.rb +116 -0
  19. data/lib/helpers/main_helper.rb +182 -0
  20. data/lib/helpers/mission_helper.rb +59 -0
  21. data/lib/helpers/rails_helper.rb +7 -0
  22. data/lib/wackamole.rb +47 -0
  23. data/lib/wackamole/core_ext/date_time.rb +8 -0
  24. data/lib/wackamole/core_ext/time.rb +9 -0
  25. data/lib/wackamole/models/control.rb +132 -0
  26. data/lib/wackamole/models/feature.rb +74 -0
  27. data/lib/wackamole/models/log.rb +45 -0
  28. data/lib/wackamole/models/mission.rb +164 -0
  29. data/lib/wackamole/models/mole_info.rb +94 -0
  30. data/lib/wackamole/models/search_filter.rb +192 -0
  31. data/lib/wackamole/models/user.rb +46 -0
  32. data/public/favicon.ico +0 -0
  33. data/public/images/.DS_Store +0 -0
  34. data/public/images/browsers/.DS_Store +0 -0
  35. data/public/images/browsers/MSIE 6.png +0 -0
  36. data/public/images/browsers/MSIE 8.0 +0 -0
  37. data/public/images/browsers/MSIE 8.0.png +0 -0
  38. data/public/images/browsers/chrome.png +0 -0
  39. data/public/images/browsers/firefox.png +0 -0
  40. data/public/images/browsers/ie 6.0.png +0 -0
  41. data/public/images/browsers/ie6.jpg +0 -0
  42. data/public/images/browsers/ie7.jpg +0 -0
  43. data/public/images/browsers/ie7.png +0 -0
  44. data/public/images/browsers/msie 6.0.png +0 -0
  45. data/public/images/browsers/msie 7.0.png +0 -0
  46. data/public/images/browsers/opera.jpg +0 -0
  47. data/public/images/browsers/opera.png +0 -0
  48. data/public/images/browsers/safari.jpg +0 -0
  49. data/public/images/browsers/safari.png +0 -0
  50. data/public/images/browsers/unknown_browser.png +0 -0
  51. data/public/images/close.png +0 -0
  52. data/public/images/error.png +0 -0
  53. data/public/images/fade.png +0 -0
  54. data/public/images/fault_big.png +0 -0
  55. data/public/images/fault_small.png +0 -0
  56. data/public/images/feature.png +0 -0
  57. data/public/images/h300.png +0 -0
  58. data/public/images/hori_large.png +0 -0
  59. data/public/images/info_big.png +0 -0
  60. data/public/images/info_small.png +0 -0
  61. data/public/images/loading.gif +0 -0
  62. data/public/images/loading1.gif +0 -0
  63. data/public/images/mole_error.png +0 -0
  64. data/public/images/more.gif +0 -0
  65. data/public/images/next.png +0 -0
  66. data/public/images/perf_big.png +0 -0
  67. data/public/images/perf_small.png +0 -0
  68. data/public/images/powered_by.png +0 -0
  69. data/public/images/prev.png +0 -0
  70. data/public/images/row_fade.png +0 -0
  71. data/public/images/search.png +0 -0
  72. data/public/images/small_logo.png +0 -0
  73. data/public/images/spaceball.gif +0 -0
  74. data/public/images/tick.png +0 -0
  75. data/public/images/users.png +0 -0
  76. data/public/images/wackamole_logo.png +0 -0
  77. data/public/javascripts/.DS_Store +0 -0
  78. data/public/javascripts/application.js +28 -0
  79. data/public/javascripts/g.dot.min.js +7 -0
  80. data/public/javascripts/g.raphael.min.js +7 -0
  81. data/public/javascripts/jit.js +9052 -0
  82. data/public/javascripts/jit.min.js +1 -0
  83. data/public/javascripts/jquery-ui.js +188 -0
  84. data/public/javascripts/jquery.example.js +160 -0
  85. data/public/javascripts/jquery.js +19 -0
  86. data/public/javascripts/jquery.tools.min.js +38 -0
  87. data/public/javascripts/jquery_ba-url.js +9 -0
  88. data/public/javascripts/jquery_min.js +19 -0
  89. data/public/javascripts/jquery_ui_min.js +298 -0
  90. data/public/javascripts/raphael.min.js +7 -0
  91. data/public/stylesheets/tabs-slideshow.css +95 -0
  92. data/public/stylesheets/wackamole.css +684 -0
  93. data/spec/config/bogus_test.yml +3 -0
  94. data/spec/config/test.yml +3 -0
  95. data/spec/core_ext/date_time_spec.rb +8 -0
  96. data/spec/core_ext/time_spec.rb +12 -0
  97. data/spec/data/fixtures.rb +92 -0
  98. data/spec/models/control_spec.rb +98 -0
  99. data/spec/models/feature_spec.rb +52 -0
  100. data/spec/models/log_spec.rb +43 -0
  101. data/spec/models/mission_spec.rb +225 -0
  102. data/spec/models/moled_info_spec.rb +30 -0
  103. data/spec/models/search_filter_spec.rb +151 -0
  104. data/spec/models/user_spec.rb +44 -0
  105. data/spec/spec_helper.rb +14 -0
  106. data/spec/wackamole_spec.rb +20 -0
  107. data/tasks/bones.rake +20 -0
  108. data/tasks/fixtures.rake +13 -0
  109. data/tasks/gem.rake +201 -0
  110. data/tasks/git.rake +40 -0
  111. data/tasks/notes.rake +27 -0
  112. data/tasks/post_load.rake +32 -0
  113. data/tasks/rdoc.rake +54 -0
  114. data/tasks/rubyforge.rake +55 -0
  115. data/tasks/setup.rb +290 -0
  116. data/tasks/spec.rake +54 -0
  117. data/tasks/svn.rake +46 -0
  118. data/views/dashboard/_report.erb +118 -0
  119. data/views/dashboard/index.erb +4 -0
  120. data/views/dashboard/refresh_js.erb +3 -0
  121. data/views/features/_rows.erb +26 -0
  122. data/views/features/filter.js.erb +2 -0
  123. data/views/features/index.erb +24 -0
  124. data/views/features/index.js.erb +2 -0
  125. data/views/layout.erb +47 -0
  126. data/views/logs/_array.erb +12 -0
  127. data/views/logs/_hash.erb +14 -0
  128. data/views/logs/_rows.erb +65 -0
  129. data/views/logs/filter.js.erb +4 -0
  130. data/views/logs/index.erb +13 -0
  131. data/views/logs/index.js.erb +2 -0
  132. data/views/logs/show.erb +66 -0
  133. data/views/mission/_report.erb +49 -0
  134. data/views/mission/index.erb +7 -0
  135. data/views/mission/refresh_js.erb +3 -0
  136. data/views/shared/_filter.erb +63 -0
  137. data/views/shared/_flash.erb +3 -0
  138. data/views/shared/_search.erb +25 -0
  139. data/views/shared/_timestamp.erb +1 -0
  140. data/views/shared/_wait.erb +1 -0
  141. data/views/shared/flash.js.erb +10 -0
  142. data/views/users/_rows.erb +26 -0
  143. data/views/users/filter.js.erb +4 -0
  144. data/views/users/index.erb +24 -0
  145. data/views/users/index.js.erb +2 -0
  146. metadata +347 -0
@@ -0,0 +1,27 @@
1
+ require 'chronic'
2
+
3
+ module Mission
4
+
5
+ # ---------------------------------------------------------------------------
6
+ get '/mission' do
7
+ # reset app info
8
+ session[:app_info] = @app_info = nil
9
+
10
+ load_report
11
+ erb :'mission/index'
12
+ end
13
+
14
+ # ---------------------------------------------------------------------------
15
+ get '/mission/refresh' do
16
+ load_report
17
+ erb :'/mission/refresh_js', :layout => false
18
+ end
19
+
20
+ # ---------------------------------------------------------------------------
21
+ get '/mission/fixed/:app/:env/:type' do
22
+ Wackamole::Mission.reset!( params[:app], params[:env], params[:type] )
23
+ load_report
24
+ erb :'/mission/refresh_js', :layout => false
25
+ end
26
+
27
+ end
@@ -0,0 +1,41 @@
1
+ module Users
2
+
3
+ # ---------------------------------------------------------------------------
4
+ # Paginate top users
5
+ get "/users/:page" do
6
+ page = params[:page] ? params[:page].to_i : 1
7
+
8
+ @users = Wackamole::User.paginate_tops( @filter.to_conds, page )
9
+ @search_path = "/users/search"
10
+ @filter_path = "/users/filter"
11
+
12
+ if request.xhr?
13
+ erb :'users/index.js', :layout => false
14
+ else
15
+ erb :'users/index'
16
+ end
17
+ end
18
+
19
+ # ---------------------------------------------------------------------------
20
+ # search users
21
+ post "/users/search" do
22
+ begin
23
+ @filter.search_terms = params[:search_filter][:search_terms]
24
+ @users = Wackamole::User.paginate_tops( @filter.to_conds )
25
+ rescue => boom
26
+ logger.error boom
27
+ flash[:error] = boom
28
+ @users = [].paginate
29
+ end
30
+
31
+ erb :"users/filter.js", :layout => false
32
+ end
33
+
34
+ # ---------------------------------------------------------------------------
35
+ # Filter
36
+ post "/users/filter" do
37
+ @filter.from_options( params[:filter] )
38
+ @users = Wackamole::User.paginate_tops( @filter.to_conds )
39
+ erb :"users/filter.js", :layout => false
40
+ end
41
+ end
@@ -0,0 +1,32 @@
1
+ module DashboardHelper
2
+
3
+ helpers do
4
+
5
+ # -------------------------------------------------------------------------
6
+ # Make sure all indexes are set
7
+ def ensure_indexes!
8
+ Wackamole::Log.ensure_indexes!
9
+ Wackamole::User.ensure_indexes!
10
+ Wackamole::Feature.ensure_indexes!
11
+ end
12
+
13
+ # -------------------------------------------------------------------------
14
+ # Retrieve moled app info...
15
+ def load_app_info
16
+ @app_info = Wackamole::Feature.get_app_info
17
+ session[:app_info] = @app_info
18
+ end
19
+
20
+ # -------------------------------------------------------------------------
21
+ # Loads the application details
22
+ def load_app_details
23
+ @info = Wackamole::MoledInfo.collect_dashboard_info( @updated_on.clone.utc )
24
+ end
25
+
26
+ # -------------------------------------------------------------------------
27
+ # Check for zeroed out series...
28
+ def zeroed_series?( series )
29
+ series.inject(0) { |res, s| res + s } == 0
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,13 @@
1
+ module FeaturesHelper
2
+
3
+ helpers do
4
+ # ---------------------------------------------------------------------------
5
+ # Find feature context for log entry
6
+ def display_context( feature )
7
+ if feature['ctl']
8
+ return "#{feature['ctl']}##{feature['act']}"
9
+ end
10
+ feature['ctx']
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,16 @@
1
+ module FlashHelper
2
+ helpers do
3
+
4
+ # clear out flash object
5
+ def clear_it!
6
+ @flash = session[:flash] || OrderedHash.new
7
+ @flash.clear
8
+ end
9
+
10
+ # add flash message
11
+ def flash_it!( type, msg )
12
+ @flash = session[:flash] || OrderedHash.new
13
+ @flash[type] = msg
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,116 @@
1
+ module LogsHelper
2
+
3
+ helpers do
4
+ # ---------------------------------------------------------------------------
5
+ def user_name_for( user_id )
6
+ user = Wackamole::User.users_cltn.find_one( user_id )
7
+ user['una']
8
+ end
9
+
10
+ # ---------------------------------------------------------------------------
11
+ def feature_name_for( feature_id )
12
+ feature = Wackamole::Feature.features_cltn.find_one( feature_id )
13
+ if feature['ctx']
14
+ return feature['ctx']
15
+ end
16
+ "#{feature['ctrl']}##{feature['act']}"
17
+ end
18
+
19
+ # ---------------------------------------------------------------------------
20
+ def human_type( type )
21
+ case type
22
+ when Rackamole.perf
23
+ "Performance"
24
+ when Rackamole.fault
25
+ "Exception"
26
+ else
27
+ "Feature"
28
+ end
29
+ end
30
+
31
+ # ---------------------------------------------------------------------------
32
+ # Change ids to time
33
+ def timestamp_for( log )
34
+ begin
35
+ date_tokens = log['did'].match( /(\d{4})(\d{2})(\d{2})/ ).captures
36
+ time_tokens = log['tid'].match( /(\d{2})(\d{2})(\d{2})/ ).captures
37
+ time = Time.utc( date_tokens[0], date_tokens[1], date_tokens[2], time_tokens[0], time_tokens[1], time_tokens[2] )
38
+ return time.getlocal.strftime( "%m/%d/%y %H:%M:%S")
39
+ rescue
40
+ ;
41
+ end
42
+ "N/A"
43
+ end
44
+
45
+ # ---------------------------------------------------------------------------
46
+ # Find feature context for log entry
47
+ def context_for( feature_id )
48
+ feature = Wackamole::Feature.features_cltn.find_one( feature_id )
49
+ if feature['ctl']
50
+ return "#{feature['ctl']}##{feature['act']}"
51
+ end
52
+ feature['ctx']
53
+ end
54
+
55
+ # ---------------------------------------------------------------------------
56
+ # Converts mole type to big icon
57
+ def mole_type_icon( type )
58
+ case type
59
+ when Rackamole.perf
60
+ "perf_big.png"
61
+ when Rackamole.fault
62
+ "fault_big.png"
63
+ else
64
+ "info_big.png"
65
+ end
66
+ end
67
+
68
+ # ---------------------------------------------------------------------------
69
+ # Converts hash to string
70
+ def dump_hash( hash )
71
+ content = []
72
+ hash.each_pair do |k,v|
73
+ content << content_tag( :span, "#{k} -> #{v}" )
74
+ end
75
+ content.join( "" )
76
+ end
77
+
78
+ # ---------------------------------------------------------------------------
79
+ # Trim out extra host info if any
80
+ def format_host( host )
81
+ return host.split( "." ).first if host.index( /\./ )
82
+ host
83
+ end
84
+
85
+ # ---------------------------------------------------------------------------
86
+ # Check if request time is available
87
+ def request_time( req_time )
88
+ begin
89
+ "%4.2f" % req_time
90
+ rescue
91
+ "N/A"
92
+ end
93
+ end
94
+
95
+ # ---------------------------------------------------------------------------
96
+ # Change log color depending on type
97
+ def row_class_for( type )
98
+ case type
99
+ when Rackamole.feature
100
+ "feature"
101
+ when Rackamole.perf
102
+ "perf"
103
+ when Rackamole.fault
104
+ "fault"
105
+ end
106
+ end
107
+
108
+ # ---------------------------------------------------------------------------
109
+ # Setup browser icon indicator
110
+ def browser_icon( browser )
111
+ img_name = browser
112
+ img_name = "unknown_browser" if img_name.nil? or img_name == "N/A"
113
+ image_tag "browsers/#{img_name.to_s.downcase.gsub( /\\/, '')}.png", :size => "20x20"
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,182 @@
1
+ require 'json'
2
+ require 'will_paginate/view_helpers/base'
3
+ require 'will_paginate/view_helpers/link_renderer'
4
+
5
+ # BOZO !! Refact helpers...
6
+ module WillPaginate::ViewHelpers
7
+ class LinkRenderer
8
+ def url( page )
9
+ "#{@options[:params][:url]}/#{page}"
10
+ end
11
+ end
12
+ end
13
+
14
+ module MainHelper
15
+ helpers do
16
+ include WillPaginate::ViewHelpers::Base
17
+
18
+ def pluralize( count, name )
19
+ count.to_s + " " + name + (count.to_i > 1 ? "s" : "")
20
+ end
21
+
22
+ def javascript_tag( code )
23
+ content_tag( :script, code )
24
+ end
25
+
26
+ def remote_function(options)
27
+ jq_options = {}
28
+ jq_options[:beforeSend] = options[:before] if options.has_key?( :before )
29
+ jq_options[:complete] = options[:complete] if options.has_key?( :complete )
30
+ jq_options[:success] = options[:success] if options.has_key?( :success )
31
+ jq_options[:error] = options[:error] if options.has_key?( :error )
32
+ jq_options[:url] = options[:url] if options.has_key?( :url )
33
+ jq_options[:data] = options[:data] if options.has_key?( :data )
34
+ jq_options[:dataType] = options[:data_type] || 'script'
35
+ jq_options[:type] = options[:type] || "GET"
36
+
37
+ buff = []
38
+ jq_options.each_pair { |k,v| buff << "#{k}:'#{v}'" }
39
+
40
+ "$.ajax({#{buff.join( "," )}})"
41
+ end
42
+
43
+ def periodically_call_remote(options = {})
44
+ frequency = options[:frequency] || 10 # every ten seconds by default
45
+ code = "setInterval(function() {#{remote_function(options)}}, #{frequency} * 1000)"
46
+ javascript_tag(code)
47
+ end
48
+
49
+ def content_tag( tag, content, opts={} )
50
+ "<#{tag} #{html_options( opts )}>#{content}</#{tag}>"
51
+ end
52
+
53
+ def options_for_select( list, selection )
54
+ buff = []
55
+ list.each do |option|
56
+ opts = {}
57
+ if option.is_a? Array
58
+ opts[:value] = option.last
59
+ content = option.first
60
+ opts[:selected] = "selected" if option.last == selection
61
+ else
62
+ content = option
63
+ opts[:value] = option
64
+ opts[:selected] = "selected" if option == selection
65
+ end
66
+ buff << content_tag( :option, content, opts )
67
+ end
68
+ buff.join( "\n" )
69
+ end
70
+
71
+ def image_tag( name, opts={} )
72
+ "<img src=\"/images/#{name}\" #{html_options( opts )}/>"
73
+ end
74
+
75
+ def link_to_remote( name, url, opts={} )
76
+ "<a href=\"#\" onclick=\"#{remote_function({:url=>url})};return false;\" #{html_options(opts)}>#{name}</a>"
77
+ end
78
+
79
+ # def form_remote_tag( name, opts={}, html_opts={} )
80
+ # "<form onsubmit=\"#{remote_function(opts)};return false;\" #{html_options(html_opts)}></form>"
81
+ # end
82
+
83
+ def link_to( name, url, opts={} )
84
+ "<a href=\"#{url}\" #{html_options(opts)}>#{name}</a>"
85
+ end
86
+
87
+ def html_options( opts )
88
+ html = []
89
+ html << "id=\"#{opts[:id]}\"" if opts.has_key?( :id )
90
+ html << "border=\"#{opts[:border]}\"" if opts.has_key?( :border )
91
+ html << "class=\"#{opts[:class]}\"" if opts.has_key?( :class )
92
+ if opts.has_key?( :size )
93
+ size = opts[:size]
94
+ width, height = size.split( 'x' )
95
+ style = "width:#{width}px;height:#{height}px;"
96
+ if opts[:style]
97
+ opts[:style] += ";#{style}"
98
+ else
99
+ opts[:style] = style
100
+ end
101
+ end
102
+ html << "style=\"#{opts[:style]}\"" if opts.has_key?( :style )
103
+ html << "selected=\"selected\"" if opts.has_key?( :selected )
104
+ html << "value=\"#{opts[:value]}\"" if opts.has_key?( :value )
105
+ html.empty? ? "" : html.join( " " )
106
+ end
107
+
108
+ def stylesheets( styles )
109
+ buff = []
110
+ styles.each do |style|
111
+ buff << "<link rel=\"stylesheet\" href=\"#{v_styles( style )}\" type=\"text/css\" media=\"screen\" />"
112
+ end
113
+ buff.join( "\n" )
114
+ end
115
+
116
+ def javascripts( scripts )
117
+ buff = []
118
+ scripts.each do |script|
119
+ buff << "<script src=\"#{v_js( script )}\" type=\"text/javascript\"></script>"
120
+ end
121
+ buff.join( "\n" )
122
+ end
123
+
124
+ def v_styles(stylesheet)
125
+ "/stylesheets/#{stylesheet}.css?" + File.mtime(File.join(Sinatra::Application.public, "stylesheets", "#{stylesheet}.css")).to_i.to_s
126
+ end
127
+
128
+ def v_js(js)
129
+ "/javascripts/#{js}.js?" + File.mtime(File.join(Sinatra::Application.public, "javascripts", "#{js}.js")).to_i.to_s
130
+ end
131
+
132
+ # truncate a string - won't work for multibyte
133
+ def truncate(text, length = 30, truncate_string = "...")
134
+ return "" if text.nil?
135
+ l = length - truncate_string.size
136
+ text.size > length ? (text[0...l] + truncate_string).to_s : text
137
+ end
138
+
139
+ def align_for( value )
140
+ return "right" if value.is_a?(Fixnum)
141
+ "left"
142
+ end
143
+
144
+ # Add thousand markers
145
+ def format_number( value )
146
+ return value.to_s.gsub(/(\d)(?=\d{3}+(\.\d*)?$)/, '\1,') if value.instance_of?(Fixnum)
147
+ value
148
+ end
149
+
150
+ def partial( page, options={} )
151
+ if object = options.delete(:object)
152
+ template = page.to_s.split("/").last
153
+ options.merge!( :locals => { template.to_sym => object } )
154
+ end
155
+
156
+ if page.to_s.index( /\// )
157
+ page = page.to_s.gsub( /\//, '/_' )
158
+ else
159
+ page = "_" + page.to_s
160
+ end
161
+ erb page.to_sym, options.merge!( :layout => false )
162
+ end
163
+
164
+ JS_ESCAPE_MAP =
165
+ {
166
+ '\\' => '\\\\',
167
+ '</' => '<\/',
168
+ "\r\n" => '\n',
169
+ "\n" => '\n',
170
+ "\r" => '\n',
171
+ '"' => '\\"',
172
+ "'" => "\\'"
173
+ }
174
+ def escape_javascript(javascript)
175
+ if javascript
176
+ javascript.gsub(/(\\|<\/|\r\n|[\n\r"'])/) { JS_ESCAPE_MAP[$1] }
177
+ else
178
+ ''
179
+ end
180
+ end
181
+ end
182
+ end
@@ -0,0 +1,59 @@
1
+ module MissionHelper
2
+ helpers do
3
+
4
+ # -------------------------------------------------------------------------
5
+ def load_report
6
+ @old_reports = Wackamole::Mission.find( {}, :sort => [ [:app, Mongo::ASCENDING], [:env, Mongo::ASCENDING] ] ).to_a
7
+
8
+ last_tick = session[:last_tick]
9
+ reset = last_tick.nil?
10
+ last_tick = last_tick || Chronic.parse( '1 minute ago' )
11
+ session[:last_tick] = Time.now
12
+ @reports = Wackamole::Mission.rollups( last_tick.utc, reset )
13
+ end
14
+
15
+ # -------------------------------------------------------------------------
16
+ # compute diff delta
17
+ def delta?( diff )
18
+ diff == 0 ? content_tag( :span, diff, :class => "no_diff") : content_tag( :span, "+#{diff}", :class => "diff")
19
+ end
20
+
21
+ # -------------------------------------------------------------------------
22
+ # Assign status fg for application
23
+ def assign_class( type, count, diff )
24
+ clazz = case type
25
+ when "faults" : (diff > 0 ? "fault" : "")
26
+ when "perfs" : (diff > 0 ? "perf" : "")
27
+ when "features" : (diff > 0 ? "active" : "inactive")
28
+ else ""
29
+ end
30
+ clazz
31
+ end
32
+
33
+ # -------------------------------------------------------------------------
34
+ # Computes count diff since last check point
35
+ def compute_diff( app_name, env, type_name, count )
36
+ old_count = find_report_count( app_name, env, type_name )
37
+ return (count - old_count) if( old_count )
38
+ count
39
+ end
40
+
41
+ # =========================================================================
42
+ private
43
+
44
+ # -----------------------------------------------------------------------
45
+ # Find report count for a given report
46
+ def find_report_count( app, env, type )
47
+ @old_reports.each do |report|
48
+ next unless report['app'] == app
49
+ report['envs'].each_pair do |e, details|
50
+ next unless e == env
51
+ details.each_pair do |type_name, count|
52
+ return count if type_name == type
53
+ end
54
+ end
55
+ end
56
+ nil
57
+ end
58
+ end
59
+ end