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
@@ -0,0 +1,102 @@
1
+ require 'rubygems'
2
+ require 'active_record'
3
+
4
+ class RequestLogAnalyzer::Database
5
+
6
+ def self.const_missing(const) # :nodoc:
7
+ RequestLogAnalyzer::load_default_class_file(self, const)
8
+ end
9
+
10
+ include RequestLogAnalyzer::Database::Connection
11
+
12
+ attr_accessor :file_format
13
+ attr_reader :line_classes
14
+
15
+ def initialize(connection_identifier = nil)
16
+ @line_classes = []
17
+ RequestLogAnalyzer::Database::Base.database = self
18
+ connect(connection_identifier)
19
+ end
20
+
21
+ # Returns the ORM class for the provided line type
22
+ def get_class(line_type)
23
+ line_type = line_type.name if line_type.respond_to?(:name)
24
+ Object.const_get("#{line_type}_line".camelize)
25
+ end
26
+
27
+ def default_classes
28
+ [RequestLogAnalyzer::Database::Request, RequestLogAnalyzer::Database::Source, RequestLogAnalyzer::Database::Warning]
29
+ end
30
+
31
+ # Loads the ORM classes by inspecting the tables in the current database
32
+ def load_database_schema!
33
+ connection.tables.map do |table|
34
+ case table.to_sym
35
+ when :warnings then RequestLogAnalyzer::Database::Warning
36
+ when :sources then RequestLogAnalyzer::Database::Source
37
+ when :requests then RequestLogAnalyzer::Database::Request
38
+ else load_activerecord_class(table)
39
+ end
40
+ end
41
+ end
42
+
43
+ # Returns an array of all the ActiveRecord-bases ORM classes for this database
44
+ def orm_classes
45
+ default_classes + line_classes
46
+ end
47
+
48
+ # Loads an ActiveRecord-based class that correspond to the given parameter, which can either be
49
+ # a table name or a LineDefinition instance.
50
+ def load_activerecord_class(linedefinition_or_table)
51
+
52
+ case linedefinition_or_table
53
+ when String, Symbol
54
+ klass_name = linedefinition_or_table.to_s.singularize.camelize
55
+ klass = RequestLogAnalyzer::Database::Base.subclass_from_table(linedefinition_or_table)
56
+ when RequestLogAnalyzer::LineDefinition
57
+ klass_name = "#{linedefinition_or_table.name}_line".camelize
58
+ klass = RequestLogAnalyzer::Database::Base.subclass_from_line_definition(linedefinition_or_table)
59
+ end
60
+
61
+ Object.const_set(klass_name, klass)
62
+ klass = Object.const_get(klass_name)
63
+ @line_classes << klass
64
+ return klass
65
+ end
66
+
67
+ def fileformat_classes
68
+ raise "No file_format provided!" unless file_format
69
+ line_classes = file_format.line_definitions.map { |(name, definition)| load_activerecord_class(definition) }
70
+ return default_classes + line_classes
71
+ end
72
+
73
+ # Creates the database schema and related ActiveRecord::Base subclasses that correspond to the
74
+ # file format definition. These ORM classes will later be used to create records in the database.
75
+ def create_database_schema!
76
+ fileformat_classes.each { |klass| klass.create_table! }
77
+ end
78
+
79
+ # Drops the table of all the ORM classes, and unregisters the classes
80
+ def drop_database_schema!
81
+ file_format ? fileformat_classes.map(&:drop_table!) : orm_classes.map(&:drop_table!)
82
+ remove_orm_classes!
83
+ end
84
+
85
+ # Registers the default ORM classes in the default namespace
86
+ def register_default_orm_classes!
87
+ Object.const_set('Request', RequestLogAnalyzer::Database::Request)
88
+ Object.const_set('Source', RequestLogAnalyzer::Database::Source)
89
+ Object.const_set('Warning', RequestLogAnalyzer::Database::Warning)
90
+ end
91
+
92
+ # Unregisters every ORM class constant
93
+ def remove_orm_classes!
94
+ orm_classes.each do |klass|
95
+ if klass.respond_to?(:name) && !klass.name.blank?
96
+ klass_name = klass.name.split('::').last
97
+ Object.send(:remove_const, klass_name) if Object.const_defined?(klass_name)
98
+ end
99
+ end
100
+ end
101
+
102
+ end
@@ -0,0 +1,74 @@
1
+ module RequestLogAnalyzer::FileFormat
2
+
3
+ # FileFormat for Amazon S3 access logs.
4
+ #
5
+ # Access logs are disabled by default on Amazon S3. To enable logging, see
6
+ # http://docs.amazonwebservices.com/AmazonS3/latest/index.html?ServerLogs.html
7
+ class AmazonS3 < Base
8
+
9
+ extend CommonRegularExpressions
10
+
11
+ line_definition :access do |line|
12
+ line.header = true
13
+ line.footer = true
14
+ line.regexp = /^([^\ ]+) ([^\ ]+) \[(#{timestamp('%d/%b/%Y:%H:%M:%S %z')})?\] (#{ip_address}) ([^\ ]+) ([^\ ]+) (\w+(?:\.\w+)*) ([^\ ]+) "([^"]+)" (\d+) ([^\ ]+) (\d+) (\d+) (\d+) (\d+) "([^"]*)" "([^"]*)"/
15
+
16
+ line.capture(:bucket_owner)
17
+ line.capture(:bucket)
18
+ line.capture(:timestamp).as(:timestamp)
19
+ line.capture(:remote_ip)
20
+ line.capture(:requester)
21
+ line.capture(:request_id)
22
+ line.capture(:operation)
23
+ line.capture(:key).as(:nillable_string)
24
+ line.capture(:request_uri)
25
+ line.capture(:http_status).as(:integer)
26
+ line.capture(:error_code).as(:nillable_string)
27
+ line.capture(:bytes_sent).as(:traffic, :unit => :byte)
28
+ line.capture(:object_size).as(:traffic, :unit => :byte)
29
+ line.capture(:total_time).as(:duration, :unit => :msec)
30
+ line.capture(:turnaround_time).as(:duration, :unit => :msec)
31
+ line.capture(:referer).as(:referer)
32
+ line.capture(:user_agent).as(:user_agent)
33
+ end
34
+
35
+ report do |analyze|
36
+ analyze.timespan
37
+ analyze.hourly_spread
38
+
39
+ analyze.frequency :category => lambda { |r| "#{r[:bucket]}/#{r[:key]}"}, :title => "Most popular items"
40
+ analyze.duration :duration => :total_time, :category => lambda { |r| "#{r[:bucket]}/#{r[:key]}"}, :title => "Request duration"
41
+ analyze.traffic :traffic => :bytes_sent, :category => lambda { |r| "#{r[:bucket]}/#{r[:key]}"}, :title => "Traffic"
42
+ analyze.frequency :category => :http_status, :title => 'HTTP status codes'
43
+ analyze.frequency :category => :error_code, :title => 'Error codes'
44
+ end
45
+
46
+ class Request < RequestLogAnalyzer::Request
47
+
48
+ MONTHS = {'Jan' => '01', 'Feb' => '02', 'Mar' => '03', 'Apr' => '04', 'May' => '05', 'Jun' => '06',
49
+ 'Jul' => '07', 'Aug' => '08', 'Sep' => '09', 'Oct' => '10', 'Nov' => '11', 'Dec' => '12' }
50
+
51
+ # Do not use DateTime.parse, but parse the timestamp ourselves to return a integer
52
+ # to speed up parsing.
53
+ def convert_timestamp(value, definition)
54
+ "#{value[7,4]}#{MONTHS[value[3,3]]}#{value[0,2]}#{value[12,2]}#{value[15,2]}#{value[18,2]}".to_i
55
+ end
56
+
57
+ # Make sure that the string '-' is parsed as a nil value.
58
+ def convert_nillable_string(value, definition)
59
+ value == '-' ? nil : value
60
+ end
61
+
62
+ # Can be implemented in subclasses for improved categorizations
63
+ def convert_referer(value, definition)
64
+ value == '-' ? nil : value
65
+ end
66
+
67
+ # Can be implemented in subclasses for improved categorizations
68
+ def convert_user_agent(value, definition)
69
+ value == '-' ? nil : value
70
+ end
71
+ end
72
+
73
+ end
74
+ end
@@ -0,0 +1,147 @@
1
+ module RequestLogAnalyzer::FileFormat
2
+
3
+ # The Apache file format is able to log Apache access.log files.
4
+ #
5
+ # The access.log can be configured in Apache to have many different formats. In theory, this
6
+ # FileFormat can handle any format, but it must be aware of the log formatting that is used
7
+ # by sending the formatting string as parameter to the create method, e.g.:
8
+ #
9
+ # RequestLogAnalyzer::FileFormat::Apache.create('%h %l %u %t "%r" %>s %b')
10
+ #
11
+ # It also supports the predefined Apache log formats "common" and "combined". The line
12
+ # definition and the report definition will be constructed using this file format string.
13
+ # From the command line, you can provide the format string using the <tt>--apache-format</tt>
14
+ # command line option.
15
+ class Apache < Base
16
+
17
+ extend CommonRegularExpressions
18
+
19
+ # A hash of predefined Apache log formats
20
+ LOG_FORMAT_DEFAULTS = {
21
+ :common => '%h %l %u %t "%r" %>s %b',
22
+ :combined => '%h %l %u %t "%r" %>s %b "%{Referer}i" "%{User-agent}i"',
23
+ :rack => '%h %l %u %t "%r" %>s %b %T',
24
+ :referer => '%{Referer}i -> %U',
25
+ :agent => '%{User-agent}i'
26
+ }
27
+
28
+ # I have encountered two timestamp types, with timezone and without. Parse both.
29
+ APACHE_TIMESTAMP = Regexp.union(timestamp('%d/%b/%Y:%H:%M:%S %z'), timestamp('%d/%b/%Y %H:%M:%S'))
30
+
31
+ # A hash that defines how the log format directives should be parsed.
32
+ LOG_DIRECTIVES = {
33
+ '%' => { :regexp => '%', :captures => [] },
34
+ 'h' => { :regexp => '([A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)+)', :captures => [{:name => :remote_host, :type => :string}] },
35
+ 'a' => { :regexp => "(#{ip_address})", :captures => [{:name => :remote_ip, :type => :string}] },
36
+ 'b' => { :regexp => '(\d+|-)', :captures => [{:name => :bytes_sent, :type => :traffic}] },
37
+ 'c' => { :regexp => '(\+|\-|\X)', :captures => [{:name => :connection_status, :type => :integer}] },
38
+ 'D' => { :regexp => '(\d+|-)', :captures => [{:name => :duration, :type => :duration, :unit => :musec}] },
39
+ 'l' => { :regexp => '([\w-]+)', :captures => [{:name => :remote_logname, :type => :nillable_string}] },
40
+ 'T' => { :regexp => '((?:\d+(?:\.\d+))|-)', :captures => [{:name => :duration, :type => :duration, :unit => :sec}] },
41
+ 't' => { :regexp => "\\[(#{APACHE_TIMESTAMP})?\\]", :captures => [{:name => :timestamp, :type => :timestamp}] },
42
+ 's' => { :regexp => '(\d{3})', :captures => [{:name => :http_status, :type => :integer}] },
43
+ 'u' => { :regexp => '(\w+|-)', :captures => [{:name => :user, :type => :nillable_string}] },
44
+ 'U' => { :regexp => '(\/\S*)', :captures => [{:name => :path, :type => :string}] },
45
+ 'r' => { :regexp => '([A-Z]+) (\S+) HTTP\/(\d+(?:\.\d+)*)', :captures => [{:name => :http_method, :type => :string},
46
+ {:name => :path, :type => :path}, {:name => :http_version, :type => :string}]},
47
+ 'i' => { 'Referer' => { :regexp => '(\S+)', :captures => [{:name => :referer, :type => :nillable_string}] },
48
+ 'User-agent' => { :regexp => '(.*)', :captures => [{:name => :user_agent, :type => :user_agent}] }
49
+ }
50
+ }
51
+
52
+ # Creates the Apache log format language based on a Apache log format string.
53
+ # It will set up the line definition and the report trackers according to the Apache access log format,
54
+ # which should be passed as first argument. By default, is uses the 'combined' log format.
55
+ def self.create(*args)
56
+ access_line = access_line_definition(args.first)
57
+ trackers = report_trackers(access_line) + report_definer.trackers
58
+ self.new(line_definer.line_definitions.merge(:access => access_line), trackers)
59
+ end
60
+
61
+ # Creates the access log line definition based on the Apache log format string
62
+ def self.access_line_definition(format_string)
63
+ format_string ||= :common
64
+ format_string = LOG_FORMAT_DEFAULTS[format_string.to_sym] || format_string
65
+
66
+
67
+ line_regexp = ''
68
+ captures = []
69
+ format_string.scan(/([^%]*)(?:%(?:\{([^\}]+)\})?>?([A-Za-z%]))?/) do |literal, arg, variable|
70
+
71
+ line_regexp << Regexp.quote(literal) # Make sure to parse the literal before the directive
72
+
73
+ if variable
74
+ # Check if we recognize the log directive
75
+ directive = LOG_DIRECTIVES[variable]
76
+ directive = directive[arg] if directive && arg
77
+
78
+ if directive
79
+ line_regexp << directive[:regexp] # Parse the value of the directive
80
+ captures += directive[:captures] # Add the directive's information to the captures
81
+ else
82
+ puts "%#{directive} log directiven not yet supported, field is ignored."
83
+ line_regexp << '.*' # Just accept any input for this literal
84
+ end
85
+ end
86
+ end
87
+
88
+ # Return a new line definition object
89
+ return RequestLogAnalyzer::LineDefinition.new(:access, :regexp => Regexp.new(line_regexp),
90
+ :captures => captures, :header => true, :footer => true)
91
+ end
92
+
93
+ # Sets up the report trackers according to the fields captured by the access line definition.
94
+ def self.report_trackers(line_definition)
95
+ analyze = RequestLogAnalyzer::Aggregator::Summarizer::Definer.new
96
+
97
+ analyze.timespan if line_definition.captures?(:timestamp)
98
+ analyze.hourly_spread if line_definition.captures?(:timestamp)
99
+
100
+ analyze.frequency :category => :http_method, :title => "HTTP methods" if line_definition.captures?(:http_method)
101
+ analyze.frequency :category => :http_status, :title => "HTTP statuses" if line_definition.captures?(:http_status)
102
+ analyze.frequency :category => lambda { |r| r.category }, :title => "Most popular URIs" if line_definition.captures?(:path)
103
+
104
+ analyze.frequency :category => :user_agent, :title => "User agents" if line_definition.captures?(:user_agent)
105
+ analyze.frequency :category => :referer, :title => "Referers" if line_definition.captures?(:referer)
106
+
107
+ analyze.duration :duration => :duration, :category => lambda { |r| r.category }, :title => 'Request duration' if line_definition.captures?(:duration)
108
+ analyze.traffic :traffic => :bytes_sent, :category => lambda { |r| r.category }, :title => 'Traffic' if line_definition.captures?(:bytes_sent)
109
+
110
+ return analyze.trackers
111
+ end
112
+
113
+ # Define a custom Request class for the Apache file format to speed up timestamp handling.
114
+ class Request < RequestLogAnalyzer::Request
115
+
116
+ def category
117
+ first(:path)
118
+ end
119
+
120
+ MONTHS = {'Jan' => '01', 'Feb' => '02', 'Mar' => '03', 'Apr' => '04', 'May' => '05', 'Jun' => '06',
121
+ 'Jul' => '07', 'Aug' => '08', 'Sep' => '09', 'Oct' => '10', 'Nov' => '11', 'Dec' => '12' }
122
+
123
+ # Do not use DateTime.parse, but parse the timestamp ourselves to return a integer
124
+ # to speed up parsing.
125
+ def convert_timestamp(value, definition)
126
+ "#{value[7,4]}#{MONTHS[value[3,3]]}#{value[0,2]}#{value[12,2]}#{value[15,2]}#{value[18,2]}".to_i
127
+ end
128
+
129
+ # This function can be overridden to rewrite the path for better categorization in the
130
+ # reports.
131
+ def convert_path(value, definition)
132
+ value
133
+ end
134
+
135
+ # This function can be overridden to simplify the user agent string for better
136
+ # categorization in the reports
137
+ def convert_user_agent(value, definition)
138
+ value # TODO
139
+ end
140
+
141
+ # Make sure that the string '-' is parsed as a nil value.
142
+ def convert_nillable_string(value, definition)
143
+ value == '-' ? nil : value
144
+ end
145
+ end
146
+ end
147
+ end
@@ -0,0 +1,55 @@
1
+ module RequestLogAnalyzer::FileFormat
2
+
3
+ # The DelayedJob file format parsed log files that are created by DelayedJob.
4
+ # By default, the log file can be found in RAILS_ROOT/log/delayed_job.log
5
+ class DelayedJob < Base
6
+
7
+ line_definition :job_lock do |line|
8
+ line.header = true
9
+ line.regexp = /\* \[JOB\] acquiring lock on (\S+)/
10
+
11
+ line.capture(:job)
12
+ end
13
+
14
+ line_definition :job_completed do |line|
15
+ line.footer = true
16
+ line.regexp = /\* \[JOB\] (\S+) completed after (\d+\.\d+)/
17
+
18
+ line.capture(:completed_job)
19
+ line.capture(:duration).as(:duration, :unit => :sec)
20
+ end
21
+
22
+ line_definition :job_failed do |line|
23
+ line.footer = true
24
+ line.regexp = /\* \[JOB\] (\S+) failed with (\S+)\: .* - (\d+) failed attempts/
25
+
26
+ line.capture(:failed_job)
27
+ line.capture(:exception)
28
+ line.capture(:attempts).as(:integer)
29
+ end
30
+
31
+ line_definition :job_lock_failed do |line|
32
+ line.footer = true
33
+ line.regexp = /\* \[JOB\] failed to acquire exclusive lock for (\S+)/
34
+
35
+ line.capture(:locked_job)
36
+ end
37
+
38
+ # line_definition :batch_completed do |line|
39
+ # line.header = true
40
+ # line.footer = true
41
+ # line.regexp = /(\d+) jobs processed at (\d+\.\d+) j\/s, (\d+) failed .../
42
+ #
43
+ # line.capture(:total_amount).as(:integer)
44
+ # line.capture(:mean_duration).as(:duration, :unit => :sec)
45
+ # line.capture(:failed_amount).as(:integer)
46
+ # end
47
+
48
+ report do |analyze|
49
+ analyze.frequency :job, :line_type => :job_completed, :title => "Completed jobs"
50
+ analyze.frequency :job, :if => lambda { |request| request[:attempts] == 1 }, :title => "Failed jobs"
51
+
52
+ analyze.duration :duration, :category => :job, :line_type => :job_completed, :title => "Job duration"
53
+ end
54
+ end
55
+ end
@@ -1,33 +1,69 @@
1
- module RequestLogAnalyzer::FileFormat::Merb
1
+ module RequestLogAnalyzer::FileFormat
2
+
3
+ # The Merb file format parses the request header with the timestamp, the params line
4
+ # with the most important request information and the durations line which contains
5
+ # the different request durations that can be used for analysis.
6
+ class Merb < Base
7
+
8
+ extend CommonRegularExpressions
2
9
 
3
- LINE_DEFINITIONS = {
4
-
5
10
  # ~ Started request handling: Fri Aug 29 11:10:23 +0200 2008
6
- :started => {
7
- :header => true,
8
- :teaser => /Started/,
9
- :regexp => /Started request handling\:\ (.+)/,
10
- :captures => [{ :name => :timestamp, :type => :timestamp, :anonymize => :slightly }]
11
- },
12
-
11
+ line_definition :started do |line|
12
+ line.header = true
13
+ line.teaser = /Started request handling\:/
14
+ line.regexp = /Started request handling\:\ (#{timestamp('%a %b %d %H:%M:%S %z %Y')})/
15
+ line.captures << { :name => :timestamp, :type => :timestamp }
16
+ end
17
+
13
18
  # ~ Params: {"action"=>"create", "controller"=>"session"}
14
- # ~ Params: {"_method"=>"delete", "authenticity_token"=>"[FILTERED]", "action"=>"d}
15
- :params => {
16
- :teaser => /Params/,
17
- :regexp => /Params\:\ \{(.+)\}/,
18
- :captures => [{ :name => :raw_hash, :type => :string}]
19
- },
20
-
19
+ # ~ Params: {"_method"=>"delete", "authenticity_token"=>"[FILTERED]", "action"=>"destroy"}
20
+ line_definition :params do |line|
21
+ line.teaser = /Params\:\ /
22
+ line.regexp = /Params\:\ (\{.+\})/
23
+ line.captures << { :name => :params, :type => :eval, :provides => {
24
+ :namespace => :string, :controller => :string, :action => :string, :format => :string, :method => :string } }
25
+ end
26
+
21
27
  # ~ {:dispatch_time=>0.006117, :after_filters_time=>6.1e-05, :before_filters_time=>0.000712, :action_time=>0.005833}
22
- :completed => {
23
- :footer => true,
24
- :teaser => /\{:dispatch_time/,
25
- :regexp => /\{\:dispatch_time=>(\d+\.\d+(?:e-?\d+)?), (?:\:after_filters_time=>(\d+\.\d+(?:e-?\d+)?), )?(?:\:before_filters_time=>(\d+\.\d+(?:e-?\d+)?), )?\:action_time=>(\d+\.\d+(?:e-?\d+)?)\}/,
26
- :captures => [{ :name => :dispatch_time, :type => :sec, :anonymize => :slightly },
27
- { :name => :after_filters_time, :type => :sec, :anonymize => :slightly },
28
- { :name => :before_filters_time, :type => :sec, :anonymize => :slightly },
29
- { :name => :action_time, :type => :sec, :anonymize => :slightly }]
30
- }
31
- }
32
-
33
- end
28
+ line_definition :completed do |line|
29
+ line.footer = true
30
+ # line.teaser = Regexp.new(Regexp.quote('~ {:'))
31
+ line.regexp = /(\{.*\:dispatch_time\s*=>\s*\d+\.\d+.*\})/
32
+ line.captures << { :name => :times_hash, :type => :eval, :provides => {
33
+ :dispatch_time => :duration, :after_filters_time => :duration,
34
+ :before_filters_time => :duration, :action_time => :duration } }
35
+ end
36
+
37
+ REQUEST_CATEGORIZER = Proc.new do |request|
38
+ category = "#{request[:controller]}##{request[:action]}"
39
+ category = "#{request[:namespace]}::#{category}" if request[:namespace]
40
+ category = "#{category}.#{request[:format]}" if request[:format]
41
+ category
42
+ end
43
+
44
+ report do |analyze|
45
+
46
+ analyze.timespan
47
+ analyze.hourly_spread
48
+
49
+ analyze.frequency :category => REQUEST_CATEGORIZER, :title => "Top 20 by hits"
50
+ analyze.duration :dispatch_time, :category => REQUEST_CATEGORIZER, :title => 'Request dispatch duration'
51
+
52
+ # analyze.duration :action_time, :category => REQUEST_CATEGORIZER, :title => 'Request action duration'
53
+ # analyze.duration :after_filters_time, :category => REQUEST_CATEGORIZER, :title => 'Request after_filter duration'
54
+ # analyze.duration :before_filters_time, :category => REQUEST_CATEGORIZER, :title => 'Request before_filter duration'
55
+ end
56
+
57
+ class Request < RequestLogAnalyzer::Request
58
+
59
+ MONTHS = {'Jan' => '01', 'Feb' => '02', 'Mar' => '03', 'Apr' => '04', 'May' => '05', 'Jun' => '06',
60
+ 'Jul' => '07', 'Aug' => '08', 'Sep' => '09', 'Oct' => '10', 'Nov' => '11', 'Dec' => '12' }
61
+
62
+ # Speed up timestamp conversion
63
+ def convert_timestamp(value, definition)
64
+ "#{value[26,4]}#{MONTHS[value[4,3]]}#{value[8,2]}#{value[11,2]}#{value[14,2]}#{value[17,2]}".to_i
65
+ end
66
+ end
67
+ end
68
+
69
+ end
@@ -0,0 +1,101 @@
1
+ module RequestLogAnalyzer::FileFormat
2
+
3
+ class Mysql < Base
4
+
5
+ extend CommonRegularExpressions
6
+
7
+ line_definition :time do |line|
8
+ line.header = :alternative
9
+ line.teaser = /\# Time: /
10
+ line.regexp = /\# Time: (#{timestamp('%y%m%d %k:%M:%S')})/
11
+
12
+ line.capture(:timestamp).as(:timestamp)
13
+ end
14
+
15
+ line_definition :user_host do |line|
16
+ line.header = :alternative
17
+ line.teaser = /\# User\@Host\: /
18
+ line.regexp = /\# User\@Host\: ([\w-]+)\[[\w-]+\] \@ ([\w\.-]*) \[(#{ip_address(true)})\]/
19
+
20
+ line.capture(:user)
21
+ line.capture(:host)
22
+ line.capture(:ip)
23
+ end
24
+
25
+ line_definition :query_statistics do |line|
26
+ line.header = :alternative
27
+ line.teaser = /\# Query_time: /
28
+ line.regexp = /\# Query_time: (\d+(?:\.\d+)?)\s+Lock_time: (\d+(?:\.\d+)?)\s+Rows_sent: (\d+)\s+Rows_examined: (\d+)/
29
+
30
+ line.capture(:query_time).as(:duration, :unit => :sec)
31
+ line.capture(:lock_time).as(:duration, :unit => :sec)
32
+ line.capture(:rows_sent).as(:integer)
33
+ line.capture(:rows_examined).as(:integer)
34
+ end
35
+
36
+ line_definition :use_database do |line|
37
+ line.regexp = /^use (\w+);\s*$/
38
+ line.capture(:database)
39
+ end
40
+
41
+ line_definition :query_part do |line|
42
+ line.regexp = /^(?!(?:use |\# |SET ))(.*[^;\s])\s*$/
43
+ line.capture(:query_fragment)
44
+ end
45
+
46
+ line_definition :query do |line|
47
+ line.footer = true
48
+ line.regexp = /^(?!(?:use |\# |SET ))(.*);\s*$/
49
+ line.capture(:query).as(:sql)
50
+ end
51
+
52
+ PER_USER = :user
53
+ PER_QUERY = :query
54
+ PER_USER_QUERY = Proc.new { |request| "#{request[:user]}@#{request.host}: #{request[:query]}" }
55
+
56
+ report do |analyze|
57
+ analyze.timespan :line_type => :time
58
+ analyze.frequency :user, :title => "Users with most queries"
59
+ analyze.duration :query_time, :category => PER_USER, :title => 'Query time per user'
60
+ analyze.duration :query_time, :category => PER_USER_QUERY, :title => 'Query time'
61
+
62
+ analyze.duration :lock_time, :category => PER_USER_QUERY, :title => 'Lock time',
63
+ :if => lambda { |request| request[:lock_time] > 0.0 }
64
+
65
+ analyze.numeric_value :rows_examined, :category => PER_USER_QUERY, :title => "Rows examined"
66
+ analyze.numeric_value :rows_sent, :category => PER_USER_QUERY, :title => "Rows sent"
67
+ end
68
+
69
+ class Request < RequestLogAnalyzer::Request
70
+
71
+ def convert_sql(value, definition)
72
+
73
+ # Recreate the full SQL query by joining all the previous parts and this last line
74
+ sql = every(:query_fragment).join("\n") + value
75
+
76
+ # Sanitize an SQL query so that it can be used as a category field.
77
+ # sql.gsub!(/\/\*.*\*\//, '') # remove comments
78
+ sql.gsub!(/\s+/, ' ') # remove excessive whitespace
79
+ sql.gsub!(/`([^`]+)`/, '\1') # remove quotes from field names
80
+ sql.gsub!(/'\d{4}-\d{2}-\d{2}'/, ':date') # replace dates
81
+ sql.gsub!(/'\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}'/, ':datetime') # replace timestamps
82
+ sql.gsub!(/'[^']*'/, ':string') # replace strings
83
+ sql.gsub!(/\b\d+\b/, ':int') # replace integers
84
+ sql.gsub!(/(:int,)+:int/, ':ints') # replace multiple ints by a list
85
+ sql.gsub!(/(:string,)+:string/, ':strings') # replace multiple strings by a list
86
+
87
+ return sql.rstrip
88
+ end
89
+
90
+ def host
91
+ self[:host] == '' || self[:host].nil? ? self[:ip] : self[:host]
92
+ end
93
+
94
+ # Convert the timestamp to an integer
95
+ def convert_timestamp(value, definition)
96
+ all,y,m,d,h,i,s = value.split(/(\d\d)(\d\d)(\d\d)\s+(\d?\d):(\d\d):(\d\d)/)
97
+ ('20%s%s%s%s%s%s' % [y,m,d,h.rjust(2, '0'),i,s]).to_i
98
+ end
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,68 @@
1
+ module RequestLogAnalyzer::FileFormat
2
+
3
+ class Postgresql < Base
4
+
5
+ extend CommonRegularExpressions
6
+
7
+ line_definition :query do |line|
8
+ line.header = true
9
+ line.teaser = /LOG\: query\:/
10
+ line.regexp = /(#{timestamp('%y-%m-%d %k:%M:%S')})\ LOG: query:\s+(.*)/
11
+
12
+ line.capture(:timestamp).as(:timestamp)
13
+ line.capture(:query_fragment)
14
+ end
15
+
16
+ line_definition :duration do |line|
17
+ line.footer = true
18
+ line.teaser = /duration:/
19
+ line.regexp = /#{timestamp('%y-%m-%d %k:%M:%S')}\ LOG\: duration\: (.*)(\ )sec/
20
+
21
+ line.capture(:query_time).as(:duration, :unit => :sec)
22
+ line.capture(:query).as(:sql) # Hack to gather up fragments
23
+ end
24
+
25
+ line_definition :query_fragment do |line|
26
+ line.regexp = /^(?!.*LOG)\s*(.*)\s*/
27
+ line.capture(:query_fragment)
28
+ end
29
+
30
+ report do |analyze|
31
+ analyze.timespan
32
+ analyze.hourly_spread
33
+ analyze.duration :query_time, :category => :query, :title => 'Query time'
34
+ end
35
+
36
+ class Request < RequestLogAnalyzer::Request
37
+
38
+ def convert_sql(value, definition)
39
+
40
+ # Recreate the full SQL query by joining all the previous parts and this last line
41
+ sql = every(:query_fragment).join("\n") + value
42
+
43
+ # Sanitize an SQL query so that it can be used as a category field.
44
+ # sql.gsub!(/\/\*.*\*\//, '') # remove comments
45
+ sql.gsub!(/\s+/, ' ') # remove excessive whitespace
46
+ sql.gsub!(/"([^"]+)"/, '\1') # remove quotes from field names
47
+ sql.gsub!(/'\d{4}-\d{2}-\d{2}'/, ':date') # replace dates
48
+ sql.gsub!(/'\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}'/, ':datetime') # replace timestamps
49
+ sql.gsub!(/'[^']*'/, ':string') # replace strings
50
+ sql.gsub!(/\b\d+\b/, ':int') # replace integers
51
+ sql.gsub!(/(:int,)+:int/, ':ints') # replace multiple ints by a list
52
+ sql.gsub!(/(:string,)+:string/, ':strings') # replace multiple strings by a list
53
+
54
+ return sql.lstrip.rstrip
55
+ end
56
+
57
+ def host
58
+ self[:host] == '' || self[:host].nil? ? self[:ip] : self[:host]
59
+ end
60
+
61
+ # Convert the timestamp to an integer
62
+ def convert_timestamp(value, definition)
63
+ all,y,m,d,h,i,s = value.split(/(\d\d)-(\d\d)-(\d\d)\s+(\d?\d):(\d\d):(\d\d)/)
64
+ ('20%s%s%s%s%s%s' % [y,m,d,h.rjust(2, '0'),i,s]).to_i
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,9 @@
1
+ module RequestLogAnalyzer::FileFormat
2
+
3
+ class Rack < Apache
4
+
5
+ def self.create(*args)
6
+ super(:rack, *args)
7
+ end
8
+ end
9
+ end