request-log-analyzer 1.0.2 → 1.6.3

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 (144) hide show
  1. data/.gitignore +10 -0
  2. data/DESIGN.rdoc +41 -0
  3. data/LICENSE +4 -4
  4. data/README.rdoc +38 -0
  5. data/Rakefile +6 -3
  6. data/bin/request-log-analyzer +70 -72
  7. data/lib/cli/command_line_arguments.rb +53 -53
  8. data/lib/cli/database_console.rb +26 -0
  9. data/lib/cli/database_console_init.rb +43 -0
  10. data/lib/cli/progressbar.rb +166 -189
  11. data/lib/cli/tools.rb +49 -0
  12. data/lib/request_log_analyzer/aggregator/database_inserter.rb +83 -0
  13. data/lib/request_log_analyzer/aggregator/echo.rb +17 -12
  14. data/lib/request_log_analyzer/aggregator/summarizer.rb +101 -63
  15. data/lib/request_log_analyzer/{aggregator/base.rb → aggregator.rb} +17 -13
  16. data/lib/request_log_analyzer/controller.rb +251 -98
  17. data/lib/request_log_analyzer/database/base.rb +114 -0
  18. data/lib/request_log_analyzer/database/connection.rb +38 -0
  19. data/lib/request_log_analyzer/database/request.rb +22 -0
  20. data/lib/request_log_analyzer/database/source.rb +13 -0
  21. data/lib/request_log_analyzer/database/warning.rb +14 -0
  22. data/lib/request_log_analyzer/database.rb +102 -0
  23. data/lib/request_log_analyzer/file_format/amazon_s3.rb +74 -0
  24. data/lib/request_log_analyzer/file_format/apache.rb +147 -0
  25. data/lib/request_log_analyzer/file_format/delayed_job.rb +55 -0
  26. data/lib/request_log_analyzer/file_format/merb.rb +65 -29
  27. data/lib/request_log_analyzer/file_format/mysql.rb +101 -0
  28. data/lib/request_log_analyzer/file_format/postgresql.rb +68 -0
  29. data/lib/request_log_analyzer/file_format/rack.rb +9 -0
  30. data/lib/request_log_analyzer/file_format/rails.rb +164 -78
  31. data/lib/request_log_analyzer/file_format/rails3.rb +86 -0
  32. data/lib/request_log_analyzer/file_format/rails_development.rb +12 -0
  33. data/lib/request_log_analyzer/file_format.rb +252 -58
  34. data/lib/request_log_analyzer/filter/anonymize.rb +39 -0
  35. data/lib/request_log_analyzer/filter/field.rb +19 -13
  36. data/lib/request_log_analyzer/filter/timespan.rb +25 -12
  37. data/lib/request_log_analyzer/filter.rb +30 -0
  38. data/lib/request_log_analyzer/line_definition.rb +69 -96
  39. data/lib/request_log_analyzer/log_processor.rb +31 -53
  40. data/lib/request_log_analyzer/mailer.rb +65 -0
  41. data/lib/request_log_analyzer/output/fancy_html.rb +49 -0
  42. data/lib/request_log_analyzer/output/fixed_width.rb +220 -0
  43. data/lib/request_log_analyzer/output/html.rb +187 -0
  44. data/lib/request_log_analyzer/output.rb +117 -0
  45. data/lib/request_log_analyzer/request.rb +125 -40
  46. data/lib/request_log_analyzer/source/database_loader.rb +87 -0
  47. data/lib/request_log_analyzer/source/log_parser.rb +297 -0
  48. data/lib/request_log_analyzer/source.rb +72 -0
  49. data/lib/request_log_analyzer/tracker/duration.rb +28 -64
  50. data/lib/request_log_analyzer/tracker/frequency.rb +108 -0
  51. data/lib/request_log_analyzer/tracker/hourly_spread.rb +76 -49
  52. data/lib/request_log_analyzer/tracker/numeric_value.rb +223 -0
  53. data/lib/request_log_analyzer/tracker/timespan.rb +54 -27
  54. data/lib/request_log_analyzer/tracker/traffic.rb +40 -0
  55. data/lib/request_log_analyzer/tracker.rb +101 -0
  56. data/lib/request_log_analyzer.rb +43 -13
  57. data/request-log-analyzer.gemspec +41 -0
  58. data/spec/database.yml +23 -0
  59. data/spec/fixtures/apache_combined.log +5 -0
  60. data/spec/fixtures/apache_common.log +10 -0
  61. data/spec/fixtures/decompression.log +12 -0
  62. data/spec/fixtures/decompression.log.bz2 +0 -0
  63. data/spec/fixtures/decompression.log.gz +0 -0
  64. data/spec/fixtures/decompression.log.zip +0 -0
  65. data/spec/fixtures/decompression.tar.gz +0 -0
  66. data/spec/fixtures/decompression.tgz +0 -0
  67. data/spec/fixtures/header_and_footer.log +6 -0
  68. data/spec/fixtures/merb_prefixed.log +9 -0
  69. data/spec/fixtures/mysql_slow_query.log +110 -0
  70. data/spec/fixtures/postgresql.log +2980 -0
  71. data/spec/fixtures/rails.db +0 -0
  72. data/spec/fixtures/rails_22.log +1 -1
  73. data/spec/fixtures/sinatra.log +99 -0
  74. data/spec/integration/command_line_usage_spec.rb +84 -0
  75. data/spec/integration/mailer_spec.rb +179 -0
  76. data/spec/integration/munin_plugins_rails_spec.rb +58 -0
  77. data/spec/integration/scout_spec.rb +152 -0
  78. data/spec/lib/helpers.rb +72 -0
  79. data/spec/lib/macros.rb +18 -0
  80. data/spec/lib/matchers.rb +77 -0
  81. data/spec/lib/mocks.rb +77 -0
  82. data/spec/lib/testing_format.rb +46 -0
  83. data/spec/spec_helper.rb +16 -59
  84. data/spec/unit/aggregator/database_inserter_spec.rb +93 -0
  85. data/spec/unit/aggregator/summarizer_spec.rb +26 -0
  86. data/spec/unit/controller/controller_spec.rb +41 -0
  87. data/spec/unit/controller/log_processor_spec.rb +18 -0
  88. data/spec/unit/database/base_class_spec.rb +183 -0
  89. data/spec/unit/database/connection_spec.rb +34 -0
  90. data/spec/unit/database/database_spec.rb +133 -0
  91. data/spec/unit/file_format/amazon_s3_format_spec.rb +67 -0
  92. data/spec/unit/file_format/apache_format_spec.rb +203 -0
  93. data/spec/unit/file_format/common_regular_expressions_spec.rb +53 -0
  94. data/spec/unit/file_format/delayed_job_format_spec.rb +83 -0
  95. data/spec/unit/file_format/file_format_api_spec.rb +69 -0
  96. data/spec/unit/file_format/format_autodetection_spec.rb +40 -0
  97. data/spec/unit/file_format/line_definition_spec.rb +75 -0
  98. data/spec/unit/file_format/merb_format_spec.rb +52 -0
  99. data/spec/unit/file_format/mysql_format_spec.rb +154 -0
  100. data/spec/unit/file_format/postgresql_format_spec.rb +65 -0
  101. data/spec/unit/file_format/rack_format_spec.rb +50 -0
  102. data/spec/unit/file_format/rails3_format_spec.rb +120 -0
  103. data/spec/unit/file_format/rails_format_spec.rb +180 -0
  104. data/spec/unit/filter/anonymize_filter_spec.rb +21 -0
  105. data/spec/unit/filter/field_filter_spec.rb +66 -0
  106. data/spec/unit/filter/filter_spec.rb +17 -0
  107. data/spec/unit/filter/timespan_filter_spec.rb +58 -0
  108. data/spec/unit/mailer_spec.rb +42 -0
  109. data/spec/unit/request_spec.rb +111 -0
  110. data/spec/unit/source/log_parser_spec.rb +119 -0
  111. data/spec/unit/tracker/duration_tracker_spec.rb +49 -0
  112. data/spec/unit/tracker/frequency_tracker_spec.rb +88 -0
  113. data/spec/unit/tracker/hourly_spread_spec.rb +79 -0
  114. data/spec/unit/tracker/numeric_value_tracker_spec.rb +166 -0
  115. data/spec/unit/tracker/timespan_tracker_spec.rb +73 -0
  116. data/spec/unit/tracker/tracker_api_spec.rb +125 -0
  117. data/spec/unit/tracker/traffic_tracker_spec.rb +28 -0
  118. data/tasks/github-gem.rake +290 -139
  119. data/tasks/request_log_analyzer.rake +23 -7
  120. metadata +221 -94
  121. data/DESIGN +0 -14
  122. data/HACKING +0 -7
  123. data/README.textile +0 -36
  124. data/lib/cli/bashcolorizer.rb +0 -60
  125. data/lib/request_log_analyzer/aggregator/database.rb +0 -148
  126. data/lib/request_log_analyzer/filter/base.rb +0 -29
  127. data/lib/request_log_analyzer/log_parser.rb +0 -173
  128. data/lib/request_log_analyzer/source/base.rb +0 -42
  129. data/lib/request_log_analyzer/source/log_file.rb +0 -170
  130. data/lib/request_log_analyzer/tracker/base.rb +0 -54
  131. data/lib/request_log_analyzer/tracker/category.rb +0 -71
  132. data/spec/controller_spec.rb +0 -40
  133. data/spec/database_inserter_spec.rb +0 -101
  134. data/spec/file_format_spec.rb +0 -78
  135. data/spec/file_formats/spec_format.rb +0 -26
  136. data/spec/filter_spec.rb +0 -137
  137. data/spec/line_definition_spec.rb +0 -124
  138. data/spec/log_parser_spec.rb +0 -68
  139. data/spec/log_processor_spec.rb +0 -57
  140. data/spec/merb_format_spec.rb +0 -38
  141. data/spec/rails_format_spec.rb +0 -76
  142. data/spec/request_spec.rb +0 -72
  143. data/spec/summarizer_spec.rb +0 -9
  144. data/tasks/rspec.rake +0 -6
@@ -1,30 +1,22 @@
1
1
  module RequestLogAnalyzer
2
-
3
- module Anonymizers
4
- def anonymizer_for_ip(value, capture_definition)
5
- '127.0.0.1'
6
- end
7
2
 
8
- def anonymizer_for_url(value, capture_definition)
9
- value.sub(/^https?\:\/\/[A-z0-9\.-]+\//, "http://example.com/")
10
- end
11
- end
12
-
13
3
  # The line definition class is used to specify what lines should be parsed from the log file.
14
4
  # It contains functionality to match a line against the definition and parse the information
15
5
  # from this line. This is used by the LogParser class when parsing a log file..
16
6
  class LineDefinition
17
7
 
18
- include RequestLogAnalyzer::Anonymizers
19
-
20
8
  class Definer
21
-
9
+
22
10
  attr_accessor :line_definitions
23
-
11
+
24
12
  def initialize
25
13
  @line_definitions = {}
26
14
  end
27
-
15
+
16
+ def initialize_copy(other)
17
+ @line_definitions = other.line_definitions.dup
18
+ end
19
+
28
20
  def method_missing(name, *args, &block)
29
21
  if block_given?
30
22
  @line_definitions[name] = RequestLogAnalyzer::LineDefinition.define(name, &block)
@@ -34,126 +26,107 @@ module RequestLogAnalyzer
34
26
  end
35
27
  end
36
28
 
29
+ class CaptureDefiner
30
+ attr_accessor :capture_hash
31
+
32
+ def initialize(hash)
33
+ @capture_hash = hash
34
+ end
35
+
36
+ def as(type, type_options = {})
37
+ @capture_hash.merge!(type_options.merge(:type => type))
38
+ return self
39
+ end
40
+ end
41
+
37
42
  attr_reader :name
38
43
  attr_accessor :teaser, :regexp, :captures
39
44
  attr_accessor :header, :footer
40
-
45
+
46
+ alias_method :header?, :header
47
+ alias_method :footer?, :footer
48
+
41
49
  # Initializes the LineDefinition instance with a hash containing the different elements of
42
50
  # the definition.
43
51
  def initialize(name, definition = {})
44
52
  @name = name
45
53
  @captures = []
54
+ @teaser = nil
46
55
  definition.each { |key, value| self.send("#{key.to_s}=".to_sym, value) }
47
56
  end
48
-
57
+
49
58
  def self.define(name, &block)
50
59
  definition = self.new(name)
51
60
  yield(definition) if block_given?
52
61
  return definition
53
62
  end
54
63
 
55
- # Converts a parsed value (String) to the desired value using some heuristics.
56
- def convert_value(value, type)
57
- case type
58
- when :integer; value.to_i
59
- when :float; value.to_f
60
- when :decimal; value.to_f
61
- when :symbol; value.to_sym
62
- when :sec; value.to_f
63
- when :msec; value.to_f / 1000
64
- when :timestamp; value.gsub(/[^0-9]/,'')[0..13].to_i # Retrieve with: DateTime.parse(value, '%Y%m%d%H%M%S')
65
- else value
66
- end
64
+ def capture(name)
65
+ new_capture_hash = { :name => name, :type => :string}
66
+ captures << new_capture_hash
67
+ CaptureDefiner.new(new_capture_hash)
67
68
  end
68
-
69
- # Checks whether a given line matches this definition.
69
+
70
+ # Checks whether a given line matches this definition.
70
71
  # It will return false if a line does not match. If the line matches, a hash is returned
71
72
  # with all the fields parsed from that line as content.
72
73
  # If the line definition has a teaser-check, a :teaser_check_failed warning will be emitted
73
74
  # if this teaser-check is passed, but the full regular exprssion does not ,atch.
74
- def matches(line, lineno = nil, parser = nil)
75
+ def matches(line, &warning_handler)
75
76
  if @teaser.nil? || @teaser =~ line
76
77
  if match_data = line.match(@regexp)
77
- request_info = { :line_type => name, :lineno => lineno }
78
-
79
- captures.each_with_index do |capture, index|
80
- next if capture == :ignore
81
-
82
- if match_data.captures[index]
83
- request_info[capture[:name]] = convert_value(match_data.captures[index], capture[:type])
84
- end
85
-
86
- end
87
- return request_info
78
+ return { :line_definition => self, :captures => match_data.captures}
88
79
  else
89
- if @teaser && parser
90
- parser.warn(:teaser_check_failed, "Teaser matched for #{name.inspect}, but full line did not:\n#{line.inspect}")
80
+ if @teaser && warning_handler
81
+ warning_handler.call(:teaser_check_failed, "Teaser matched for #{name.inspect}, but full line did not:\n#{line.inspect}")
91
82
  end
92
83
  return false
93
84
  end
94
85
  else
95
86
  return false
96
87
  end
88
+ rescue
89
+ return false
97
90
  end
98
-
91
+
99
92
  alias :=~ :matches
100
-
101
- def anonymize_value(value, capture_definition)
102
- if capture_definition[:anonymize].respond_to?(:call)
103
- capture_definition[:anonymize].call(value, capture_definition)
93
+
94
+ # matches the line and converts the captured values using the request's
95
+ # convert_value function.
96
+ def match_for(line, request, &warning_handler)
97
+ if match_info = matches(line, &warning_handler)
98
+ convert_captured_values(match_info[:captures], request)
104
99
  else
105
- case capture_definition[:anonymize]
106
- when nil; value
107
- when false; value
108
- when true; '***'
109
- when :slightly; anonymize_slightly(value, capture_definition)
110
- else
111
- method_name = "anonymizer_for_#{capture_definition[:anonymize]}".to_sym
112
- self.respond_to?(method_name) ? self.send(method_name, value, capture_definition) : '***'
113
- end
100
+ false
114
101
  end
115
102
  end
116
-
117
- def anonymize_slightly(value, capture_definition)
118
- case capture_definition[:type]
119
- when :integer
120
- (value.to_i * (0.8 + rand * 0.4)).to_i
121
- when :double
122
- (value.to_f * (0.8 + rand * 0.4)).to_f
123
- when :msec
124
- (value.to_i * (0.8 + rand * 0.4)).to_i
125
- when :sec
126
- (value.to_f * (0.8 + rand * 0.4)).to_f
127
- when :timestamp
128
- (DateTime.parse(value) + (rand(100) - 50)).to_s
129
- else
130
- puts "Cannot anonymize #{capture_definition[:type].inspect} slightly, using ***"
131
- '***'
132
- end
133
- end
134
103
 
135
- # Anonymize a log line
136
- def anonymize(line, options = {})
137
- if self.teaser.nil? || self.teaser =~ line
138
- if self.regexp =~ line
139
- pos_adjustment = 0
140
- captures.each_with_index do |capture, index|
141
- unless $~[index + 1].nil?
142
- anonymized_value = anonymize_value($~[index + 1], capture).to_s
143
- line[($~.begin(index + 1) + pos_adjustment)...($~.end(index + 1) + pos_adjustment)] = anonymized_value
144
- pos_adjustment += anonymized_value.length - $~[index + 1].length
145
- end
104
+ # Updates a captures hash using the converters specified in the request
105
+ # and handle the :provides option in the line definition.
106
+ def convert_captured_values(values, request)
107
+ value_hash = {}
108
+ captures.each_with_index do |capture, index|
109
+
110
+ # convert the value using the request convert_value function
111
+ converted = request.convert_value(values[index], capture)
112
+ value_hash[capture[:name]] ||= converted
113
+
114
+ # Add items directly to the resulting hash from the converted value
115
+ # if it is a hash and they are set in the :provides hash for this line definition
116
+ if converted.kind_of?(Hash) && capture[:provides].kind_of?(Hash)
117
+ capture[:provides].each do |name, type|
118
+ value_hash[name] ||= request.convert_value(converted[name], { :type => type })
146
119
  end
147
- line
148
- elsif self.teaser.nil?
149
- nil
150
- else
151
- options[:discard_teaser_lines] ? "" : line
152
120
  end
153
- else
154
- nil
155
121
  end
122
+ return value_hash
123
+ end
124
+
125
+ # Returns true if this line captures values of the given name
126
+ def captures?(name)
127
+ captures.any? { |c| c[:name] == name }
156
128
  end
129
+
157
130
  end
158
-
131
+
159
132
  end
@@ -1,58 +1,53 @@
1
1
  module RequestLogAnalyzer
2
-
2
+
3
3
  # The Logprocessor class is used to perform simple processing actions over log files.
4
- # It will go over the log file/stream line by line, pass the line to a processor and
5
- # write the result back to the output file or stream. The processor can alter the
4
+ # It will go over the log file/stream line by line, pass the line to a processor and
5
+ # write the result back to the output file or stream. The processor can alter the
6
6
  # contents of the line, remain it intact or remove it altogether, based on the current
7
7
  # file format
8
8
  #
9
- # Currently, two processors are supported, :strip and :anonymize.
10
- # * :strip will remove all irrelevent lines (according to the file format) from the
9
+ # Currently, one processors is supported:
10
+ # * :strip will remove all irrelevent lines (according to the file format) from the
11
11
  # sources. A compact, information packed log will remain/.
12
- # * :anonymize will anonymize sensitive information from the lines according to the
13
- # anonymization rules in the file format. The result can be passed to third parties
14
- # without privacy concerns.
15
12
  #
16
13
  class LogProcessor
17
-
18
- include RequestLogAnalyzer::FileFormat::Awareness
19
-
20
- attr_reader :mode, :options, :sources
14
+
15
+ attr_reader :mode, :options, :sources, :file_format
21
16
  attr_accessor :output_file
22
-
17
+
23
18
  # Builds a logprocessor instance from the arguments given on the command line
24
- # <tt>command</tt> The command hat was used to start the log processor. This can either be
25
- # :strip or :anonymize. This will set the processing mode.
19
+ # <tt>command</tt> The command hat was used to start the log processor. This will set the
20
+ # processing mode. Currently, only :strip is supported.
26
21
  # <tt>arguments</tt> The parsed command line arguments (a CommandLine::Arguments instance)
27
22
  def self.build(command, arguments)
28
-
29
- options = {
30
- :discard_teaser_lines => arguments[:discard_teaser_lines],
31
- :keep_junk_lines => arguments[:keep_junk_lines],
23
+
24
+ options = {
25
+ :discard_teaser_lines => arguments[:discard_teaser_lines],
26
+ :keep_junk_lines => arguments[:keep_junk_lines],
32
27
  }
33
-
28
+
34
29
  log_processor = RequestLogAnalyzer::LogProcessor.new(arguments[:format].to_sym, command, options)
35
30
  log_processor.output_file = arguments[:output] if arguments[:output]
36
31
 
37
32
  arguments.parameters.each do |input|
38
33
  log_processor.sources << input
39
34
  end
40
-
35
+
41
36
  return log_processor
42
37
  end
43
-
38
+
44
39
  # Initializes a new LogProcessor instance.
45
40
  # <tt>format</tt> The file format to use (e.g. :rails).
46
- # <tt>mode</tt> The processing mode (:anonymize or :strip)
41
+ # <tt>mode</tt> The processing mode
47
42
  # <tt>options</tt> A hash with options to take into account
48
43
  def initialize(format, mode, options = {})
49
- @options = options
50
- @mode = mode
51
- @sources = []
44
+ @options = options
45
+ @mode = mode
46
+ @sources = []
47
+ @file_format = format
52
48
  $output_file = nil
53
- self.register_file_format(format)
54
49
  end
55
-
50
+
56
51
  # Processes input files by opening it and sending the filestream to <code>process_io</code>,
57
52
  # in which the actual processing is performed.
58
53
  # <tt>file</tt> The file to process
@@ -61,15 +56,14 @@ module RequestLogAnalyzer
61
56
  end
62
57
 
63
58
  # Processes an input stream by iteration over each line and processing it according to
64
- # the current operation mode (:strip, :anonymize)
59
+ # the current operation mode
65
60
  # <tt>io</tt> The IO instance to process.
66
61
  def process_io(io)
67
62
  case mode
68
63
  when :strip; io.each_line { |line| @output << strip_line(line) }
69
- when :anonymize; io.each_line { |line| @output << anonymize_line(line) }
70
64
  end
71
65
  end
72
-
66
+
73
67
  # Returns the line itself if the string matches any of the line definitions. If no match is
74
68
  # found, an empty line is returned, which will strip the line from the output.
75
69
  # <tt>line</tt> The line to strip
@@ -77,42 +71,26 @@ module RequestLogAnalyzer
77
71
  file_format.line_definitions.any? { |name, definition| definition =~ line } ? line : ""
78
72
  end
79
73
 
80
- # Returns an anonymized version of the provided line. This can be a copy of the line it self,
81
- # an empty string or a string in which some substrings are substituted for anonymized values.
82
- # <tt>line</tt> The line to anonymize
83
- def anonymize_line(line)
84
- anonymized_line = nil
85
- file_format.line_definitions.detect { |name, definition| anonymized_line = definition.anonymize(line, options) }
86
-
87
- if anonymized_line
88
- return anonymized_line
89
- elsif options[:keep_junk_lines]
90
- return line
91
- else
92
- return ""
93
- end
94
- end
95
-
96
74
  # Runs the log processing by setting up the output stream and iterating over all the
97
- # input sources. Input sources can either be filenames (String instances) or IO streams
75
+ # input sources. Input sources can either be filenames (String instances) or IO streams
98
76
  # (IO instances). The strings "-" and "STDIN" will be substituted for the $stdin variable.
99
77
  def run!
100
78
  if @output_file.nil?
101
- @output = $stdout
79
+ @output = $stdout
102
80
  else
103
- @output = File.new(@output_file, 'a')
81
+ @output = File.new(@output_file, 'a')
104
82
  end
105
-
83
+
106
84
  @sources.each do |source|
107
85
  if source.kind_of?(String) && File.exist?(source)
108
86
  process_file(source)
109
87
  elsif source.kind_of?(IO)
110
88
  process_io(source)
111
89
  elsif ['-', 'STDIN'].include?(source)
112
- process_io($stdin)
90
+ process_io($stdin)
113
91
  end
114
92
  end
115
-
93
+
116
94
  ensure
117
95
  @output.close if @output.kind_of?(File)
118
96
  end
@@ -0,0 +1,65 @@
1
+ module RequestLogAnalyzer
2
+
3
+ # Mail report to a specified emailaddress
4
+ class Mailer
5
+
6
+ attr_accessor :data, :to, :host, :port
7
+
8
+ # Initialize a mailer
9
+ # <tt>to</tt> to email address to mail to
10
+ # <tt>host</tt> the mailer host (defaults to localhost)
11
+ # <tt>options</tt> Specific style options
12
+ #
13
+ # Options
14
+ # * <tt>:debug</tt> Do not actually mail
15
+ # * <tt>:from_alias</tt> The from alias
16
+ # * <tt>:to_alias</tt> The to alias
17
+ # * <tt>:subject</tt> The message subject
18
+ def initialize(to, host = 'localhost', options = {})
19
+ require 'net/smtp'
20
+ @to = to
21
+ @host = host
22
+
23
+ @port = 25
24
+ @options = options
25
+ @host, @port = host.split(':') if @host.include?(':')
26
+ @data = []
27
+ end
28
+
29
+ # Send all data in @data to the email address used during initialization.
30
+ # Returns array containg [message_data, from_email_address, to_email_address] of sent email.
31
+ def mail
32
+ from = @options[:from] || 'contact@railsdoctors.com'
33
+ from_alias = @options[:from_alias] || 'Request-log-analyzer reporter'
34
+ to_alias = @options[:to_alias] || to
35
+ subject = @options[:subject] || "Request log analyzer report - generated on #{Time.now.to_s}"
36
+ content_type = ""
37
+ content_type = 'Content-Type: text/html; charset="ISO-8859-1";' if @data.map{|l| l.include?('html')}.include?(true)
38
+ msg = <<END_OF_MESSAGE
39
+ From: #{from_alias} <#{from}>
40
+ To: #{to_alias} <#{@to}>
41
+ Subject: #{subject}
42
+ #{content_type}
43
+
44
+ #{@data.join("\n")}
45
+ END_OF_MESSAGE
46
+
47
+ unless @options[:debug]
48
+ Net::SMTP.start(@host, @port) do |smtp|
49
+ smtp.send_message msg, from, to
50
+ end
51
+ end
52
+
53
+ return [msg, from, to]
54
+ end
55
+
56
+ def << string
57
+ data << string
58
+ end
59
+
60
+ def puts string
61
+ data << string
62
+ end
63
+
64
+ end
65
+ end
@@ -0,0 +1,49 @@
1
+ begin
2
+ require 'rubygems'
3
+ require 'gchart'
4
+ rescue LoadError
5
+ $stderr.puts "The FancyHTML output format requires the googlechart gem:"
6
+ $stderr.puts " (sudo) gem install googlecharts"
7
+ end
8
+
9
+ module RequestLogAnalyzer::Output
10
+
11
+ class FancyHTML < HTML
12
+
13
+ # Load class files if needed
14
+ def self.const_missing(const)
15
+ RequestLogAnalyzer::load_default_class_file(self, const)
16
+ end
17
+
18
+ def report_tracker(tracker)
19
+ case tracker
20
+ when RequestLogAnalyzer::Tracker::HourlySpread then report_hourly_spread(tracker)
21
+ else tracker.report(self)
22
+ end
23
+ end
24
+
25
+ def report_hourly_spread(tracker)
26
+ title tracker.title
27
+ puts tag(:img, nil, :width => '700', :height => '120', :src =>
28
+ Gchart.sparkline(:data => tracker.hour_frequencies, :size => '700x120', :line_colors => '0077CC',
29
+ :axis_with_labels => 'x,y', :axis_labels => [x_axis_labels(tracker),y_axis_labels(tracker)]))
30
+ end
31
+
32
+ def x_axis_labels(tracker)
33
+ frmt = '%H:%M'
34
+ x_axis_labels = []
35
+ num_labels = 5
36
+ start = tracker.first_timestamp
37
+ step = tracker.timespan / (num_labels - 1)
38
+ for i in 0...num_labels do
39
+ x_axis_labels << (start + step * i).strftime(frmt)
40
+ end
41
+ x_axis_labels
42
+ end
43
+
44
+ def y_axis_labels(tracker)
45
+ sorted_frequencies = tracker.hour_frequencies.sort
46
+ [sorted_frequencies.first, sorted_frequencies.last]
47
+ end
48
+ end
49
+ end