request-log-analyzer 1.7.0 → 1.10.0

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 (88) hide show
  1. data/.gitignore +4 -1
  2. data/DESIGN.rdoc +0 -1
  3. data/Gemfile +2 -0
  4. data/README.rdoc +16 -5
  5. data/bin/request-log-analyzer +4 -2
  6. data/lib/cli/database_console_init.rb +1 -1
  7. data/lib/cli/tools.rb +2 -1
  8. data/lib/mixins/gets_memory_protection.rb +1 -1
  9. data/lib/request_log_analyzer/aggregator/summarizer.rb +2 -2
  10. data/lib/request_log_analyzer/aggregator.rb +3 -3
  11. data/lib/request_log_analyzer/controller.rb +42 -36
  12. data/lib/request_log_analyzer/database.rb +5 -3
  13. data/lib/request_log_analyzer/file_format/apache.rb +19 -28
  14. data/lib/request_log_analyzer/file_format/delayed_job2.rb +38 -0
  15. data/lib/request_log_analyzer/file_format/delayed_job21.rb +39 -0
  16. data/lib/request_log_analyzer/file_format/haproxy.rb +138 -0
  17. data/lib/request_log_analyzer/file_format/oink.rb +96 -0
  18. data/lib/request_log_analyzer/file_format/postgresql.rb +7 -6
  19. data/lib/request_log_analyzer/file_format/rails.rb +4 -4
  20. data/lib/request_log_analyzer/file_format/rails3.rb +34 -11
  21. data/lib/request_log_analyzer/file_format/w3c.rb +56 -0
  22. data/lib/request_log_analyzer/file_format.rb +33 -10
  23. data/lib/request_log_analyzer/filter.rb +3 -5
  24. data/lib/request_log_analyzer/output/fancy_html.rb +0 -5
  25. data/lib/request_log_analyzer/output.rb +6 -7
  26. data/lib/request_log_analyzer/request.rb +17 -3
  27. data/lib/request_log_analyzer/source/log_parser.rb +3 -1
  28. data/lib/request_log_analyzer/source.rb +2 -6
  29. data/lib/request_log_analyzer/tracker/duration.rb +7 -0
  30. data/lib/request_log_analyzer/tracker/numeric_value.rb +123 -12
  31. data/lib/request_log_analyzer/tracker/traffic.rb +7 -0
  32. data/lib/request_log_analyzer/tracker.rb +6 -5
  33. data/lib/request_log_analyzer.rb +36 -17
  34. data/request-log-analyzer.gemspec +9 -6
  35. data/spec/fixtures/oink_22.log +19 -0
  36. data/spec/fixtures/oink_22_failure.log +17 -0
  37. data/spec/fixtures/postgresql.log +29 -2980
  38. data/spec/integration/command_line_usage_spec.rb +4 -4
  39. data/spec/integration/mailer_spec.rb +20 -4
  40. data/spec/integration/munin_plugins_rails_spec.rb +1 -1
  41. data/spec/integration/scout_spec.rb +1 -1
  42. data/spec/lib/helpers.rb +2 -2
  43. data/spec/lib/macros.rb +1 -1
  44. data/spec/lib/matchers.rb +1 -1
  45. data/spec/lib/mocks.rb +1 -1
  46. data/spec/spec_helper.rb +10 -10
  47. data/spec/unit/aggregator/database_inserter_spec.rb +1 -1
  48. data/spec/unit/aggregator/summarizer_spec.rb +1 -1
  49. data/spec/unit/controller/controller_spec.rb +1 -1
  50. data/spec/unit/controller/log_processor_spec.rb +1 -1
  51. data/spec/unit/database/base_class_spec.rb +1 -1
  52. data/spec/unit/database/connection_spec.rb +1 -1
  53. data/spec/unit/database/database_spec.rb +1 -1
  54. data/spec/unit/file_format/amazon_s3_format_spec.rb +1 -1
  55. data/spec/unit/file_format/apache_format_spec.rb +18 -1
  56. data/spec/unit/file_format/common_regular_expressions_spec.rb +1 -1
  57. data/spec/unit/file_format/delayed_job21_format_spec.rb +60 -0
  58. data/spec/unit/file_format/delayed_job2_format_spec.rb +60 -0
  59. data/spec/unit/file_format/delayed_job_format_spec.rb +2 -2
  60. data/spec/unit/file_format/file_format_api_spec.rb +2 -2
  61. data/spec/unit/file_format/format_autodetection_spec.rb +7 -7
  62. data/spec/unit/file_format/haproxy_format_spec.rb +84 -0
  63. data/spec/unit/file_format/line_definition_spec.rb +1 -1
  64. data/spec/unit/file_format/merb_format_spec.rb +1 -1
  65. data/spec/unit/file_format/mysql_format_spec.rb +1 -1
  66. data/spec/unit/file_format/oink_format_spec.rb +105 -0
  67. data/spec/unit/file_format/postgresql_format_spec.rb +24 -46
  68. data/spec/unit/file_format/rack_format_spec.rb +1 -1
  69. data/spec/unit/file_format/rails3_format_spec.rb +41 -14
  70. data/spec/unit/file_format/rails_format_spec.rb +11 -1
  71. data/spec/unit/file_format/w3c_format_spec.rb +46 -0
  72. data/spec/unit/filter/anonymize_filter_spec.rb +1 -1
  73. data/spec/unit/filter/field_filter_spec.rb +1 -1
  74. data/spec/unit/filter/filter_spec.rb +1 -1
  75. data/spec/unit/filter/timespan_filter_spec.rb +1 -1
  76. data/spec/unit/mailer_spec.rb +1 -1
  77. data/spec/unit/request_spec.rb +1 -1
  78. data/spec/unit/source/log_parser_spec.rb +1 -1
  79. data/spec/unit/tracker/duration_tracker_spec.rb +4 -2
  80. data/spec/unit/tracker/frequency_tracker_spec.rb +1 -1
  81. data/spec/unit/tracker/hourly_spread_spec.rb +1 -1
  82. data/spec/unit/tracker/numeric_value_tracker_spec.rb +38 -17
  83. data/spec/unit/tracker/timespan_tracker_spec.rb +1 -1
  84. data/spec/unit/tracker/tracker_api_spec.rb +1 -1
  85. data/spec/unit/tracker/traffic_tracker_spec.rb +1 -1
  86. data/tasks/github-gem.rake +97 -60
  87. data/tasks/request_log_analyzer.rake +102 -0
  88. metadata +198 -153
@@ -0,0 +1,138 @@
1
+ module RequestLogAnalyzer::FileFormat
2
+
3
+ class Haproxy < RequestLogAnalyzer::FileFormat::Base
4
+
5
+ extend CommonRegularExpressions
6
+
7
+ # Define line types
8
+ line_definition :haproxy do |line|
9
+ line.header = true
10
+ line.footer = true
11
+
12
+ line.regexp = %r{
13
+ (#{ip_address}):\d+\s # client_ip ':' client_port
14
+ \[(#{timestamp('%d/%b/%Y:%H:%M:%S')})\.\d{3}\]\s # '[' accept_date ']'
15
+ (\S+)\s # frontend_name
16
+ (\S+)\/(\S+)\s # backend_name '/' server_name
17
+ (\d+|-1)\/(\d+|-1)\/(\d+|-1)\/(\d+|-1)\/\+?(\d+)\s # Tq '/' Tw '/' Tc '/' Tr '/' Tt
18
+ (\d+)\s # status_code
19
+ \+?(\d+)\s # bytes_read
20
+ (\S+)\s # captured_request_cookie
21
+ (\S+)\s # captured_response_cookie
22
+ (\w|-)(\w|-)(\w|-)(\w|-)\s # termination_state
23
+ (\d+)\/(\d+)\/(\d+)\/(\d+)\/\+?(\d+)\s # actconn '/' feconn '/' beconn '/' srv_conn '/' retries
24
+ (\d+)\/(\d+)\s # srv_queue '/' backend_queue
25
+ (\S*)\s? # captured_request_headers
26
+ (\S*)\s? # captured_response_headers
27
+ "([^"]*)" # '"' http_request '"'
28
+ }x
29
+
30
+ line.capture(:client_ip).as(:string)
31
+ line.capture(:accept_date).as(:timestamp)
32
+ line.capture(:frontend_name).as(:string)
33
+ line.capture(:backend_name).as(:string)
34
+ line.capture(:server_name).as(:string)
35
+ line.capture(:tq).as(:nillable_duration, :unit => :msec)
36
+ line.capture(:tw).as(:nillable_duration, :unit => :msec)
37
+ line.capture(:tc).as(:nillable_duration, :unit => :msec)
38
+ line.capture(:tr).as(:nillable_duration, :unit => :msec)
39
+ line.capture(:tt).as(:duration, :unit => :msec)
40
+ line.capture(:status_code).as(:integer)
41
+ line.capture(:bytes_read).as(:traffic, :unit => :byte)
42
+ line.capture(:captured_request_cookie).as(:nillable_string)
43
+ line.capture(:captured_response_cookie).as(:nillable_string)
44
+ line.capture(:termination_event_code).as(:nillable_string)
45
+ line.capture(:terminated_session_state).as(:nillable_string)
46
+ line.capture(:clientside_persistence_cookie).as(:nillable_string)
47
+ line.capture(:serverside_persistence_cookie).as(:nillable_string)
48
+ line.capture(:actconn).as(:integer)
49
+ line.capture(:feconn).as(:integer)
50
+ line.capture(:beconn).as(:integer)
51
+ line.capture(:srv_conn).as(:integer)
52
+ line.capture(:retries).as(:integer)
53
+ line.capture(:srv_queue).as(:integer)
54
+ line.capture(:backend_queue).as(:integer)
55
+ line.capture(:captured_request_headers).as(:nillable_string)
56
+ line.capture(:captured_response_headers).as(:nillable_string)
57
+ line.capture(:http_request).as(:nillable_string)
58
+ end
59
+
60
+ # Define the summary report
61
+ report do |analyze|
62
+ analyze.hourly_spread :field => :accept_date
63
+
64
+ analyze.frequency :client_ip,
65
+ :title => "Hits per IP"
66
+
67
+ analyze.frequency :frontend_name,
68
+ :title => "Hits per frontend service"
69
+
70
+ analyze.frequency :backend_name,
71
+ :title => "Hits per backend service"
72
+
73
+ analyze.frequency :server_name,
74
+ :title => "Hits per backend server"
75
+
76
+ analyze.frequency :status_code,
77
+ :title => "HTTP response code frequency"
78
+
79
+ analyze.frequency :http_request,
80
+ :title => "Most popular requests"
81
+
82
+ analyze.frequency :http_request,
83
+ :title => "Most frequent HTTP 40x errors",
84
+ :category => lambda { |r| "#{r[:http_request]}"},
85
+ :if => lambda { |r| r[:status_code] >= 400 and r[:status_code] <= 417 }
86
+
87
+ analyze.frequency :http_request,
88
+ :title => "Most frequent HTTP 50x errors",
89
+ :category => lambda { |r| "#{r[:http_request]}"},
90
+ :if => lambda { |r| r[:status_code] >= 500 and r[:status_code] <= 505 }
91
+
92
+ analyze.traffic :bytes_read,
93
+ :title => "Traffic per frontend service",
94
+ :category => lambda { |r| "#{r[:frontend_name]}"}
95
+
96
+ analyze.traffic :bytes_read,
97
+ :title => "Traffic per backend service",
98
+ :category => lambda { |r| "#{r[:backend_name]}"}
99
+
100
+ analyze.traffic :bytes_read,
101
+ :title => "Traffic per backend server",
102
+ :category => lambda { |r| "#{r[:server_name]}"}
103
+
104
+ analyze.duration :tr,
105
+ :title => "Time waiting for backend response",
106
+ :category => lambda { |r| "#{r[:http_request]}"}
107
+
108
+ analyze.duration :tt,
109
+ :title => "Total time spent on request",
110
+ :category => lambda { |r| "#{r[:http_request]}"}
111
+ end
112
+
113
+ # Define a custom Request class for the HAProxy file format to speed up
114
+ # timestamp handling. Shamelessly copied from apache.rb
115
+ class Request < RequestLogAnalyzer::Request
116
+
117
+ MONTHS = {'Jan' => '01', 'Feb' => '02', 'Mar' => '03', 'Apr' => '04', 'May' => '05', 'Jun' => '06',
118
+ 'Jul' => '07', 'Aug' => '08', 'Sep' => '09', 'Oct' => '10', 'Nov' => '11', 'Dec' => '12' }
119
+
120
+ # Do not use DateTime.parse, but parse the timestamp ourselves to return
121
+ # a integer to speed up parsing.
122
+ def convert_timestamp(value, definition)
123
+ "#{value[7,4]}#{MONTHS[value[3,3]]}#{value[0,2]}#{value[12,2]}#{value[15,2]}#{value[18,2]}".to_i
124
+ end
125
+
126
+ # Make sure that the strings '-' or '{}' or '' are parsed as a nil value.
127
+ def convert_nillable_string(value, definition)
128
+ value =~ /-|\{\}|^$/ ? nil : value
129
+ end
130
+
131
+ # Make sure that -1 is parsed as a nil value.
132
+ def convert_nillable_duration(value, definition)
133
+ value == '-1' ? nil : convert_duration(value, definition)
134
+ end
135
+
136
+ end
137
+ end
138
+ end
@@ -0,0 +1,96 @@
1
+ class RequestLogAnalyzer::FileFormat::Oink < RequestLogAnalyzer::FileFormat::Rails
2
+ # capture the PID
3
+ HODEL3000_PROCESSING = /\[(\d+)\]: Processing ((?:\w+::)*\w+)#(\w+)(?: to (\w+))? \(for (#{ip_address}) at (#{timestamp('%Y-%m-%d %H:%M:%S')})\) \[([A-Z]+)\]/
4
+
5
+ # TODO: fix me!
6
+ line_definition :processing do |line|
7
+ line.header = true
8
+ line.regexp = Regexp.union(LINE_DEFINITIONS[:processing].regexp,HODEL3000_PROCESSING)
9
+ line.captures = [ { :name => :controller, :type => :string }, # Default Rails Processing
10
+ { :name => :action, :type => :string },
11
+ { :name => :format, :type => :string, :default => 'html' },
12
+ { :name => :ip, :type => :string },
13
+ { :name => :timestamp, :type => :timestamp },
14
+ { :name => :method, :type => :string },
15
+ { :name => :pid, :type => :integer }, # Hodel 3000 Processing w/PID
16
+ { :name => :controller, :type => :string },
17
+ { :name => :action, :type => :string },
18
+ { :name => :format, :type => :string, :default => 'html' },
19
+ { :name => :ip, :type => :string },
20
+ { :name => :timestamp, :type => :timestamp },
21
+ { :name => :method, :type => :string }]
22
+ end
23
+
24
+ line_definition :memory_usage do |line|
25
+ line.regexp = /\[(\d+)\]: Memory usage: (\d+)/
26
+ line.capture(:pid).as(:integer)
27
+ line.capture(:memory).as(:traffic)
28
+ end
29
+
30
+ line_definition :instance_type_counter do |line|
31
+ line.regexp = /\[(\d+)\]: Instantiation Breakdown: (.*)$/
32
+ line.capture(:pid).as(:integer)
33
+ line.capture(:instance_counts).as(:pipe_separated_counts)
34
+ end
35
+
36
+ report(:append) do |analyze|
37
+ analyze.traffic :memory_diff, :category => REQUEST_CATEGORIZER, :title => "Largest Memory Increases", :line_type => :memory_usage
38
+ end
39
+
40
+ # Keep a record of PIDs and their memory usage when validating requests.
41
+ def pids
42
+ @pids ||= {}
43
+ end
44
+
45
+ class Request < RequestLogAnalyzer::FileFormat::Rails::Request
46
+ # Overrides the #validate method to handle PID updating.
47
+ def validate
48
+ update_pids
49
+ super
50
+ end
51
+
52
+ # Accessor for memory information associated with the specified request PID. If no memory exists
53
+ # for this request's :pid, the memory tracking is initialized.
54
+ def pid_memory
55
+ file_format.pids[self[:pid]] ||= { :last_memory_reading => -1, :current_memory_reading => -1 }
56
+ end
57
+
58
+ # Calculates :memory_diff for each request based on the last completed request that was not a failure.
59
+ def update_pids
60
+ # memory isn't recorded with exceptions. need to set #last_memory_reading+ to -1 as
61
+ # the memory used could have changed. for the next request the memory change will not be recorded.
62
+ #
63
+ # NOTE - the failure regex was not matching with a Rails Development log file.
64
+ if has_line_type?(:failure) and processing = has_line_type?(:processing)
65
+ pid_memory[:last_memory_reading] = -1
66
+ elsif mem_line = has_line_type?(:memory_usage)
67
+ memory_reading = mem_line[:memory]
68
+ pid_memory[:current_memory_reading] = memory_reading
69
+ # calcuate the change in memory
70
+ unless pid_memory[:current_memory_reading] == -1 || pid_memory[:last_memory_reading] == -1
71
+ # logged as kB, need to convert to bytes for the :traffic Tracker
72
+ memory_diff = (pid_memory[:current_memory_reading] - pid_memory[:last_memory_reading])*1024
73
+ if memory_diff > 0
74
+ self.attributes[:memory_diff] = memory_diff
75
+ end # if memory_diff > 0
76
+ end # unless
77
+
78
+ pid_memory[:last_memory_reading] = pid_memory[:current_memory_reading]
79
+ pid_memory[:current_memory_reading] = -1
80
+ end # if mem_line
81
+ return true
82
+ end # def update_pids
83
+
84
+ def convert_pipe_separated_counts(value, capture_definition)
85
+ count_strings = value.split(' | ')
86
+ count_arrays = count_strings.map do |count_string|
87
+ if count_string =~ /^(\w+): (\d+)/
88
+ [$1, $2.to_i]
89
+ end
90
+ end
91
+
92
+ Hash[count_arrays]
93
+ end
94
+
95
+ end # class Request
96
+ end
@@ -1,24 +1,25 @@
1
1
  module RequestLogAnalyzer::FileFormat
2
2
 
3
+ # PostgresQL spec 8.3.7
3
4
  class Postgresql < Base
4
5
 
5
6
  extend CommonRegularExpressions
6
7
 
7
8
  line_definition :query do |line|
8
9
  line.header = true
9
- line.teaser = /LOG\: query\:/
10
- line.regexp = /(#{timestamp('%y-%m-%d %k:%M:%S')})\ LOG: query:\s+(.*)/
10
+ line.teaser = /.*LOG\:/
11
+ line.regexp = /(#{timestamp('%Y-%m-%d %k:%M:%S')})\ \S+ \[\d+\]\:\ \[.*\]\ LOG\:\ \ \d+\:\ duration\: (.*)\ ms\ \ statement:\ (.*)/
11
12
 
12
13
  line.capture(:timestamp).as(:timestamp)
14
+ line.capture(:query_time).as(:duration, :unit => :sec)
13
15
  line.capture(:query_fragment)
14
16
  end
15
17
 
16
- line_definition :duration do |line|
18
+ line_definition :location do |line|
17
19
  line.footer = true
18
- line.teaser = /duration:/
19
- line.regexp = /#{timestamp('%y-%m-%d %k:%M:%S')}\ LOG\: duration\: (.*)(\ )sec/
20
+ line.teaser = /.*LOCATION:/
21
+ line.regexp = /.*(\ )LOCATION:/
20
22
 
21
- line.capture(:query_time).as(:duration, :unit => :sec)
22
23
  line.capture(:query).as(:sql) # Hack to gather up fragments
23
24
  end
24
25
 
@@ -25,9 +25,9 @@ module RequestLogAnalyzer::FileFormat
25
25
  lines.each do |line|
26
26
  line = line.to_sym
27
27
  if LINE_COLLECTIONS.has_key?(line)
28
- LINE_COLLECTIONS[line].each { |l| definitions_hash[l] = LINE_DEFINITIONS[l] }
28
+ LINE_COLLECTIONS[line].each { |l| definitions_hash[l] ||= LINE_DEFINITIONS[l] }
29
29
  elsif LINE_DEFINITIONS.has_key?(line)
30
- definitions_hash[line] = LINE_DEFINITIONS[line]
30
+ definitions_hash[line] ||= LINE_DEFINITIONS[line]
31
31
  else
32
32
  raise "Unrecognized Rails log line name: #{line.inspect}!"
33
33
  end
@@ -82,8 +82,8 @@ module RequestLogAnalyzer::FileFormat
82
82
 
83
83
  # Rails > 2.1 completed line example
84
84
  # Completed in 614ms (View: 120, DB: 31) | 200 OK [http://floorplanner.local/demo]
85
- RAILS_22_COMPLETED = /Completed in (\d+)ms \((?:View: (\d+), )?DB: (\d+)\) \| (\d\d\d).+\[(http.+)\]/
86
-
85
+ RAILS_22_COMPLETED = /Completed in (\d+)ms \((?:View: (\d+))?,?(?:.?DB: (\d+))?\)? \| (\d{3}).+\[(http.+)\]/
86
+
87
87
  # A hash of definitions for all common lines in Rails logs.
88
88
  LINE_DEFINITIONS = {
89
89
  :processing => RequestLogAnalyzer::LineDefinition.new(:processing, :header => true,
@@ -8,11 +8,11 @@ module RequestLogAnalyzer::FileFormat
8
8
 
9
9
  extend CommonRegularExpressions
10
10
 
11
- # Started GET "/queries" for 127.0.0.1 at 2010-02-25 16:15:18
11
+ # beta4: Started GET "/" for 127.0.0.1 at Wed Jul 07 09:13:27 -0700 2010 (different time format)
12
12
  line_definition :started do |line|
13
13
  line.header = true
14
14
  line.teaser = /Started /
15
- line.regexp = /Started ([A-Z]+) "([^"]+)" for (#{ip_address}) at (#{timestamp('%Y-%m-%d %H:%M:%S')})/
15
+ line.regexp = /Started ([A-Z]+) "([^"]+)" for (#{ip_address}) at (#{timestamp('%a %b %d %H:%M:%S %z %Y')}|#{timestamp('%Y-%m-%d %H:%M:%S %z')})/
16
16
 
17
17
  line.capture(:method)
18
18
  line.capture(:path)
@@ -23,21 +23,25 @@ module RequestLogAnalyzer::FileFormat
23
23
  # Processing by QueriesController#index as HTML
24
24
  line_definition :processing do |line|
25
25
  line.teaser = /Processing by /
26
- line.regexp = /Processing by (\w+)\#(\w+) as (\w+)/
26
+ line.regexp = /Processing by ([A-Za-z0-9\-:]+)\#(\w+) as ([\w\/\*]+)/
27
27
 
28
28
  line.capture(:controller)
29
29
  line.capture(:action)
30
30
  line.capture(:format)
31
31
  end
32
32
 
33
- # Completed in 9ms (Views: 4.9ms | ActiveRecord: 0.5ms) with 200
33
+ # Completed 200 OK in 224ms (Views: 200.2ms | ActiveRecord: 3.4ms)
34
+ # Completed 302 Found in 23ms
35
+ # Completed in 189ms
34
36
  line_definition :completed do |line|
35
37
  line.footer = true
36
38
  line.teaser = /Completed /
37
- line.regexp = /Completed (\d+) .* in (\d+)ms \([^\)]*\)/
38
-
39
+ line.regexp = /Completed (\d+)? .*in (\d+(?:\.\d+)?)ms(?:[^\(]*\(Views: (\d+(?:\.\d+)?)ms .* ActiveRecord: (\d+(?:\.\d+)?)ms.*\))?/
40
+
39
41
  line.capture(:status).as(:integer)
40
42
  line.capture(:duration).as(:duration, :unit => :msec)
43
+ line.capture(:view).as(:duration, :unit => :msec)
44
+ line.capture(:db).as(:duration, :unit => :msec)
41
45
  end
42
46
 
43
47
  # ActionView::Template::Error (undefined local variable or method `field' for #<Class>) on line #3 of /Users/willem/Code/warehouse/app/views/queries/execute.csv.erb:
@@ -69,18 +73,37 @@ module RequestLogAnalyzer::FileFormat
69
73
  analyze.frequency :status, :title => 'HTTP statuses returned'
70
74
 
71
75
  analyze.duration :duration, :category => REQUEST_CATEGORIZER, :title => "Request duration", :line_type => :completed
72
- # analyze.duration :view, :category => REQUEST_CATEGORIZER, :title => "View rendering time", :line_type => :completed
73
- # analyze.duration :db, :category => REQUEST_CATEGORIZER, :title => "Database time", :line_type => :completed
76
+ analyze.duration :view, :category => REQUEST_CATEGORIZER, :title => "View rendering time", :line_type => :completed
77
+ analyze.duration :db, :category => REQUEST_CATEGORIZER, :title => "Database time", :line_type => :completed
74
78
 
75
79
  analyze.frequency :category => REQUEST_CATEGORIZER, :title => 'Process blockers (> 1 sec duration)',
76
80
  :if => lambda { |request| request[:duration] && request[:duration] > 1.0 }
77
81
  end
78
82
 
79
83
  class Request < RequestLogAnalyzer::Request
80
- def convert_timestamp(value, defintion)
81
- value.gsub(/[^0-9]/, '')[0...14].to_i
84
+ # Used to handle conversion of abbrev. month name to a digit
85
+ MONTHS = %w(Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec)
86
+
87
+ def convert_timestamp(value, definition)
88
+ # the time value can be in 2 formats:
89
+ # - 2010-10-26 02:27:15 +0000 (ruby 1.9.2)
90
+ # - Thu Oct 25 16:15:18 -0800 2010
91
+ if value =~ /^#{CommonRegularExpressions::TIMESTAMP_PARTS['Y']}/
92
+ value.gsub!(/\W/,'')
93
+ value[0..13].to_i
94
+ else
95
+ value.gsub!(/\W/,'')
96
+ time_as_str = value[-4..-1] # year
97
+ # convert the month to a 2-digit representation
98
+ month = MONTHS.index(value[3..5])+1
99
+ month < 10 ? time_as_str << "0#{month}" : time_as_str << month.to_s
100
+
101
+ time_as_str << value[6..13] # day of month + time
102
+ time_as_str.to_i
103
+ end
104
+
82
105
  end
83
106
  end
84
107
 
85
108
  end
86
- end
109
+ end
@@ -0,0 +1,56 @@
1
+ module RequestLogAnalyzer::FileFormat
2
+
3
+ # FileFormat for W3C access logs.
4
+ class W3c < Base
5
+
6
+ extend CommonRegularExpressions
7
+
8
+ line_definition :access do |line|
9
+ line.header = true
10
+ line.footer = true
11
+ line.regexp = /^(#{timestamp('%Y-%m-%d %H:%M:%S')}) (#{ip_address}) (.*) (#{ip_address}) (\d+) (\w+) ([\w|\/|.]+) \- (\d+) (\d+) (\d+) (\d+) (.*) (\S+)/
12
+
13
+ line.capture(:timestamp).as(:timestamp)
14
+ line.capture(:remote_ip)
15
+ line.capture(:username).as(:nillable_string)
16
+ line.capture(:local_ip)
17
+ line.capture(:port).as(:integer)
18
+ line.capture(:method)
19
+ line.capture(:path).as(:path)
20
+ line.capture(:http_status).as(:integer)
21
+ line.capture(:bytes_sent).as(:traffic, :unit => :byte)
22
+ line.capture(:bytes_received).as(:traffic, :unit => :byte)
23
+ line.capture(:duration).as(:duration, :unit => :msec)
24
+ line.capture(:user_agent)
25
+ line.capture(:referer)
26
+ end
27
+
28
+ report do |analyze|
29
+ analyze.timespan
30
+ analyze.hourly_spread
31
+
32
+ analyze.frequency :category => :http_method, :title => "HTTP methods"
33
+ analyze.frequency :category => :http_status, :title => "HTTP statuses"
34
+
35
+ analyze.frequency :category => :path, :title => "Most popular URIs"
36
+
37
+ analyze.frequency :category => :user_agent, :title => "User agents"
38
+ analyze.frequency :category => :referer, :title => "Referers"
39
+
40
+ analyze.duration :duration => :duration, :category => :path, :title => 'Request duration'
41
+ analyze.traffic :traffic => :bytes_sent, :category => :path, :title => 'Traffic out'
42
+ analyze.traffic :traffic => :bytes_received, :category => :path, :title => 'Traffic in'
43
+ end
44
+
45
+ class Request < RequestLogAnalyzer::Request
46
+ # Do not use DateTime.parse, but parse the timestamp ourselves to return a integer
47
+ # to speed up parsing.
48
+ def convert_timestamp(value, definition)
49
+ "#{value[0,4]}#{value[5,2]}#{value[8,2]}#{value[11,2]}#{value[14,2]}#{value[17,2]}".to_i
50
+ end
51
+ end
52
+
53
+
54
+ end
55
+
56
+ end
@@ -1,8 +1,20 @@
1
1
  module RequestLogAnalyzer::FileFormat
2
2
 
3
- def self.const_missing(const) # :nodoc:
4
- RequestLogAnalyzer::load_default_class_file(self, const)
5
- end
3
+ autoload :Rails, 'request_log_analyzer/file_format/rails'
4
+ autoload :Rails3, 'request_log_analyzer/file_format/rails3'
5
+ autoload :RailsDevelopment, 'request_log_analyzer/file_format/rails_development'
6
+ autoload :Oink, 'request_log_analyzer/file_format/oink'
7
+ autoload :Rack, 'request_log_analyzer/file_format/rack'
8
+ autoload :Merb, 'request_log_analyzer/file_format/merb'
9
+ autoload :Mysql, 'request_log_analyzer/file_format/mysql'
10
+ autoload :Postgresql, 'request_log_analyzer/file_format/postgresql'
11
+ autoload :DelayedJob, 'request_log_analyzer/file_format/delayed_job'
12
+ autoload :DelayedJob2, 'request_log_analyzer/file_format/delayed_job2'
13
+ autoload :DelayedJob21, 'request_log_analyzer/file_format/delayed_job21'
14
+ autoload :Apache, 'request_log_analyzer/file_format/apache'
15
+ autoload :AmazonS3, 'request_log_analyzer/file_format/amazon_s3'
16
+ autoload :W3c, 'request_log_analyzer/file_format/w3c'
17
+ autoload :Haproxy, 'request_log_analyzer/file_format/haproxy'
6
18
 
7
19
  # Loads a FileFormat::Base subclass instance.
8
20
  # You can provide:
@@ -20,10 +32,11 @@ module RequestLogAnalyzer::FileFormat
20
32
  # a usable class is provided. Use this format class.
21
33
  klass = file_format
22
34
 
23
- elsif file_format.kind_of?(String) && File.exist?(file_format)
35
+ elsif file_format.kind_of?(String) && File.exist?(file_format) && File.file?(file_format)
24
36
  # load a format from a ruby file
25
37
  require file_format
26
- const = RequestLogAnalyzer::to_camelcase(File.basename(file_format, '.rb'))
38
+
39
+ const = RequestLogAnalyzer.to_camelcase(File.basename(file_format, '.rb'))
27
40
  if RequestLogAnalyzer::FileFormat.const_defined?(const)
28
41
  klass = RequestLogAnalyzer::FileFormat.const_get(const)
29
42
  elsif Object.const_defined?(const)
@@ -34,7 +47,7 @@ module RequestLogAnalyzer::FileFormat
34
47
 
35
48
  else
36
49
  # load a provided file format
37
- klass = RequestLogAnalyzer::FileFormat.const_get(RequestLogAnalyzer::to_camelcase(file_format))
50
+ klass = RequestLogAnalyzer::FileFormat.const_get(RequestLogAnalyzer.to_camelcase(file_format))
38
51
  end
39
52
 
40
53
  # check the returned klass to see if it can be used
@@ -46,7 +59,7 @@ module RequestLogAnalyzer::FileFormat
46
59
 
47
60
  # Returns an array of all FileFormat instances that are shipped with request-log-analyzer by default.
48
61
  def self.all_formats
49
- @all_formats ||= Dir[File.dirname(__FILE__) + '/file_format/*.rb'].map do |file|
62
+ @all_formats ||= Dir[File.expand_path('file_format/*.rb', File.dirname(__FILE__))].map do |file|
50
63
  self.load(File.basename(file, '.rb'))
51
64
  end
52
65
  end
@@ -246,11 +259,21 @@ module RequestLogAnalyzer::FileFormat
246
259
  request_class.create(self, *hashes)
247
260
  end
248
261
 
249
- # Checks whether the line definitions form a valid language.
250
- # A file format should have at least a header and a footer line type
262
+ # Checks whether the file format is valid so it can be safely used with RLA.
251
263
  def valid?
264
+ valid_line_definitions? && valid_request_class?
265
+ end
266
+
267
+ # Checks whether the line definitions form a valid language.
268
+ # A file format should have at least a header and a footer line type
269
+ def valid_line_definitions?
252
270
  line_definitions.any? { |(name, ld)| ld.header } && line_definitions.any? { |(name, ld)| ld.footer }
253
271
  end
272
+
273
+ # Checks whether the request class inherits from the base Request class.
274
+ def valid_request_class?
275
+ request_class.ancestors.include?(RequestLogAnalyzer::Request)
276
+ end
254
277
 
255
278
  # Returns true if this language captures the given symbol in one of its line definitions
256
279
  def captures?(name)
@@ -272,4 +295,4 @@ module RequestLogAnalyzer::FileFormat
272
295
  return nil
273
296
  end
274
297
  end
275
- end
298
+ end
@@ -1,10 +1,8 @@
1
1
  module RequestLogAnalyzer::Filter
2
2
 
3
- # Filter class loader using const_missing
4
- # This function will automatically load the class file based on the name of the class
5
- def self.const_missing(const)
6
- RequestLogAnalyzer::load_default_class_file(self, const)
7
- end
3
+ autoload :Field, 'request_log_analyzer/filter/field'
4
+ autoload :Timespan, 'request_log_analyzer/filter/timespan'
5
+ autoload :Anonymize, 'request_log_analyzer/filter/anonymize'
8
6
 
9
7
  # Base filter class used to filter input requests.
10
8
  # All filters should interit from this base.
@@ -10,11 +10,6 @@ module RequestLogAnalyzer::Output
10
10
 
11
11
  class FancyHTML < HTML
12
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
13
  def report_tracker(tracker)
19
14
  case tracker
20
15
  when RequestLogAnalyzer::Tracker::HourlySpread then report_hourly_spread(tracker)
@@ -1,11 +1,10 @@
1
1
  # Module for generating output
2
2
  module RequestLogAnalyzer::Output
3
3
 
4
- # Load class files if needed
5
- def self.const_missing(const)
6
- RequestLogAnalyzer::load_default_class_file(self, const)
7
- end
8
-
4
+ autoload :FixedWidth, 'request_log_analyzer/output/fixed_width'
5
+ autoload :HTML, 'request_log_analyzer/output/html'
6
+ autoload :FancyHTML, 'request_log_analyzer/output/fancy_html'
7
+
9
8
  # Loads a Output::Base subclass instance.
10
9
  def self.load(file_format, *args)
11
10
 
@@ -21,7 +20,7 @@ module RequestLogAnalyzer::Output
21
20
  elsif file_format.kind_of?(String) && File.exist?(file_format)
22
21
  # load a format from a ruby file
23
22
  require file_format
24
- const = RequestLogAnalyzer::to_camelcase(File.basename(file_format, '.rb'))
23
+ const = RequestLogAnalyzer.to_camelcase(File.basename(file_format, '.rb'))
25
24
  if RequestLogAnalyzer::FileFormat.const_defined?(const)
26
25
  klass = RequestLogAnalyzer::Output.const_get(const)
27
26
  elsif Object.const_defined?(const)
@@ -32,7 +31,7 @@ module RequestLogAnalyzer::Output
32
31
 
33
32
  else
34
33
  # load a provided file format
35
- klass = RequestLogAnalyzer::Output.const_get(RequestLogAnalyzer::to_camelcase(file_format))
34
+ klass = RequestLogAnalyzer::Output.const_get(RequestLogAnalyzer.to_camelcase(file_format))
36
35
  end
37
36
 
38
37
  # check the returned klass to see if it can be used
@@ -27,14 +27,28 @@ module RequestLogAnalyzer
27
27
  def convert_integer(value, capture_definition); value.to_i; end
28
28
  def convert_sym(value, capture_definition); value.to_sym; end
29
29
  def convert_symbol(value, capture_definition); value.to_sym; end
30
+ def convert_nillable_string(value, definition); value == '-' ? nil : value ; end
31
+
32
+ # This function can be overridden to rewrite the path for better categorization in the
33
+ # reports.
34
+ def convert_path(value, definition)
35
+ value
36
+ end
30
37
 
31
38
  # Converts :eval field, which should evaluate to a hash.
32
39
  def convert_eval(value, capture_definition)
33
- eval(value).inject({}) { |h, (k, v)| h[k.to_sym] = v; h}
34
- rescue SyntaxError
40
+ eval(sanitize_parameters(value)).inject({}) { |h, (k, v)| h[k.to_sym] = v; h}
41
+ # Wide range of errors possible with wild eval. ATM we choose to crash on this.
42
+ rescue SyntaxError
35
43
  nil
36
44
  end
37
45
 
46
+ # Removes certain string sequences which would be problematic for eval.
47
+ # TODO remove all characters not valid in ruby symbols
48
+ def sanitize_parameters(parameter_string)
49
+ parameter_string.gsub(/#</, '"').gsub(/>,/, '", ').gsub(/\\0/, '')
50
+ end
51
+
38
52
  # Slow default method to parse timestamps.
39
53
  # Reimplement this function in a file format specific Request class
40
54
  # to improve the timestamp parsing speed.
@@ -177,4 +191,4 @@ module RequestLogAnalyzer
177
191
  @lines.map { |line| line[:lineno] }.reject { |v| v.nil? }.max
178
192
  end
179
193
  end
180
- end
194
+ end
@@ -43,7 +43,9 @@ module RequestLogAnalyzer::Source
43
43
  @progress_handler = nil
44
44
 
45
45
  @options[:parse_strategy] ||= DEFAULT_PARSE_STRATEGY
46
- raise "Unknown parse strategy" unless PARSE_STRATEGIES.include?(@options[:parse_strategy])
46
+ unless PARSE_STRATEGIES.include?(@options[:parse_strategy])
47
+ raise "Unknown parse strategy: #{@options[@parse_strategy]}"
48
+ end
47
49
  end
48
50
 
49
51
  # Reads the input, which can either be a file, sequence of files or STDIN to parse
@@ -8,12 +8,8 @@
8
8
  # - Currently, RequestLogAnalyzer::Source::LogParser is the only implemented source.
9
9
  module RequestLogAnalyzer::Source
10
10
 
11
- # Loads constants that reside in the RequestLogAnalyzer::Source namespace. This function uses
12
- # RequestLogAnalyzer::load_default_class_file to load the file in which the constant is declared.
13
- # <tt>const</tt>:: The constant to load in the RequestLogAnalyzer::Source namespace.
14
- def self.const_missing(const)
15
- RequestLogAnalyzer::load_default_class_file(self, const)
16
- end
11
+ autoload :LogParser, 'request_log_analyzer/source/log_parser'
12
+ autoload :DatabaseLoader, 'request_log_analyzer/source/database_loader'
17
13
 
18
14
  # The base Source class. All other sources should inherit from this class.
19
15
  #
@@ -25,6 +25,13 @@ module RequestLogAnalyzer::Tracker
25
25
  def prepare
26
26
  options[:value] = options[:duration] if options[:duration]
27
27
  super
28
+
29
+ @number_of_buckets = options[:number_of_buckets] || 1000
30
+ @min_bucket_value = options[:min_bucket_value] ? options[:min_bucket_value].to_f : 0.0001
31
+ @max_bucket_value = options[:max_bucket_value] ? options[:max_bucket_value].to_f : 1000
32
+
33
+ # precalculate the bucket size
34
+ @bucket_size = (Math.log(@max_bucket_value) - Math.log(@min_bucket_value)) / @number_of_buckets.to_f
28
35
  end
29
36
 
30
37
  # Display a duration