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.
- data/.gitignore +10 -0
- data/DESIGN.rdoc +41 -0
- data/LICENSE +4 -4
- data/README.rdoc +38 -0
- data/Rakefile +6 -3
- data/bin/request-log-analyzer +70 -72
- data/lib/cli/command_line_arguments.rb +53 -53
- data/lib/cli/database_console.rb +26 -0
- data/lib/cli/database_console_init.rb +43 -0
- data/lib/cli/progressbar.rb +166 -189
- data/lib/cli/tools.rb +49 -0
- data/lib/request_log_analyzer/aggregator/database_inserter.rb +83 -0
- data/lib/request_log_analyzer/aggregator/echo.rb +17 -12
- data/lib/request_log_analyzer/aggregator/summarizer.rb +101 -63
- data/lib/request_log_analyzer/{aggregator/base.rb → aggregator.rb} +17 -13
- data/lib/request_log_analyzer/controller.rb +251 -98
- data/lib/request_log_analyzer/database/base.rb +114 -0
- data/lib/request_log_analyzer/database/connection.rb +38 -0
- data/lib/request_log_analyzer/database/request.rb +22 -0
- data/lib/request_log_analyzer/database/source.rb +13 -0
- data/lib/request_log_analyzer/database/warning.rb +14 -0
- data/lib/request_log_analyzer/database.rb +102 -0
- data/lib/request_log_analyzer/file_format/amazon_s3.rb +74 -0
- data/lib/request_log_analyzer/file_format/apache.rb +147 -0
- data/lib/request_log_analyzer/file_format/delayed_job.rb +55 -0
- data/lib/request_log_analyzer/file_format/merb.rb +65 -29
- data/lib/request_log_analyzer/file_format/mysql.rb +101 -0
- data/lib/request_log_analyzer/file_format/postgresql.rb +68 -0
- data/lib/request_log_analyzer/file_format/rack.rb +9 -0
- data/lib/request_log_analyzer/file_format/rails.rb +164 -78
- data/lib/request_log_analyzer/file_format/rails3.rb +86 -0
- data/lib/request_log_analyzer/file_format/rails_development.rb +12 -0
- data/lib/request_log_analyzer/file_format.rb +252 -58
- data/lib/request_log_analyzer/filter/anonymize.rb +39 -0
- data/lib/request_log_analyzer/filter/field.rb +19 -13
- data/lib/request_log_analyzer/filter/timespan.rb +25 -12
- data/lib/request_log_analyzer/filter.rb +30 -0
- data/lib/request_log_analyzer/line_definition.rb +69 -96
- data/lib/request_log_analyzer/log_processor.rb +31 -53
- data/lib/request_log_analyzer/mailer.rb +65 -0
- data/lib/request_log_analyzer/output/fancy_html.rb +49 -0
- data/lib/request_log_analyzer/output/fixed_width.rb +220 -0
- data/lib/request_log_analyzer/output/html.rb +187 -0
- data/lib/request_log_analyzer/output.rb +117 -0
- data/lib/request_log_analyzer/request.rb +125 -40
- data/lib/request_log_analyzer/source/database_loader.rb +87 -0
- data/lib/request_log_analyzer/source/log_parser.rb +297 -0
- data/lib/request_log_analyzer/source.rb +72 -0
- data/lib/request_log_analyzer/tracker/duration.rb +28 -64
- data/lib/request_log_analyzer/tracker/frequency.rb +108 -0
- data/lib/request_log_analyzer/tracker/hourly_spread.rb +76 -49
- data/lib/request_log_analyzer/tracker/numeric_value.rb +223 -0
- data/lib/request_log_analyzer/tracker/timespan.rb +54 -27
- data/lib/request_log_analyzer/tracker/traffic.rb +40 -0
- data/lib/request_log_analyzer/tracker.rb +101 -0
- data/lib/request_log_analyzer.rb +43 -13
- data/request-log-analyzer.gemspec +41 -0
- data/spec/database.yml +23 -0
- data/spec/fixtures/apache_combined.log +5 -0
- data/spec/fixtures/apache_common.log +10 -0
- data/spec/fixtures/decompression.log +12 -0
- data/spec/fixtures/decompression.log.bz2 +0 -0
- data/spec/fixtures/decompression.log.gz +0 -0
- data/spec/fixtures/decompression.log.zip +0 -0
- data/spec/fixtures/decompression.tar.gz +0 -0
- data/spec/fixtures/decompression.tgz +0 -0
- data/spec/fixtures/header_and_footer.log +6 -0
- data/spec/fixtures/merb_prefixed.log +9 -0
- data/spec/fixtures/mysql_slow_query.log +110 -0
- data/spec/fixtures/postgresql.log +2980 -0
- data/spec/fixtures/rails.db +0 -0
- data/spec/fixtures/rails_22.log +1 -1
- data/spec/fixtures/sinatra.log +99 -0
- data/spec/integration/command_line_usage_spec.rb +84 -0
- data/spec/integration/mailer_spec.rb +179 -0
- data/spec/integration/munin_plugins_rails_spec.rb +58 -0
- data/spec/integration/scout_spec.rb +152 -0
- data/spec/lib/helpers.rb +72 -0
- data/spec/lib/macros.rb +18 -0
- data/spec/lib/matchers.rb +77 -0
- data/spec/lib/mocks.rb +77 -0
- data/spec/lib/testing_format.rb +46 -0
- data/spec/spec_helper.rb +16 -59
- data/spec/unit/aggregator/database_inserter_spec.rb +93 -0
- data/spec/unit/aggregator/summarizer_spec.rb +26 -0
- data/spec/unit/controller/controller_spec.rb +41 -0
- data/spec/unit/controller/log_processor_spec.rb +18 -0
- data/spec/unit/database/base_class_spec.rb +183 -0
- data/spec/unit/database/connection_spec.rb +34 -0
- data/spec/unit/database/database_spec.rb +133 -0
- data/spec/unit/file_format/amazon_s3_format_spec.rb +67 -0
- data/spec/unit/file_format/apache_format_spec.rb +203 -0
- data/spec/unit/file_format/common_regular_expressions_spec.rb +53 -0
- data/spec/unit/file_format/delayed_job_format_spec.rb +83 -0
- data/spec/unit/file_format/file_format_api_spec.rb +69 -0
- data/spec/unit/file_format/format_autodetection_spec.rb +40 -0
- data/spec/unit/file_format/line_definition_spec.rb +75 -0
- data/spec/unit/file_format/merb_format_spec.rb +52 -0
- data/spec/unit/file_format/mysql_format_spec.rb +154 -0
- data/spec/unit/file_format/postgresql_format_spec.rb +65 -0
- data/spec/unit/file_format/rack_format_spec.rb +50 -0
- data/spec/unit/file_format/rails3_format_spec.rb +120 -0
- data/spec/unit/file_format/rails_format_spec.rb +180 -0
- data/spec/unit/filter/anonymize_filter_spec.rb +21 -0
- data/spec/unit/filter/field_filter_spec.rb +66 -0
- data/spec/unit/filter/filter_spec.rb +17 -0
- data/spec/unit/filter/timespan_filter_spec.rb +58 -0
- data/spec/unit/mailer_spec.rb +42 -0
- data/spec/unit/request_spec.rb +111 -0
- data/spec/unit/source/log_parser_spec.rb +119 -0
- data/spec/unit/tracker/duration_tracker_spec.rb +49 -0
- data/spec/unit/tracker/frequency_tracker_spec.rb +88 -0
- data/spec/unit/tracker/hourly_spread_spec.rb +79 -0
- data/spec/unit/tracker/numeric_value_tracker_spec.rb +166 -0
- data/spec/unit/tracker/timespan_tracker_spec.rb +73 -0
- data/spec/unit/tracker/tracker_api_spec.rb +125 -0
- data/spec/unit/tracker/traffic_tracker_spec.rb +28 -0
- data/tasks/github-gem.rake +290 -139
- data/tasks/request_log_analyzer.rake +23 -7
- metadata +221 -94
- data/DESIGN +0 -14
- data/HACKING +0 -7
- data/README.textile +0 -36
- data/lib/cli/bashcolorizer.rb +0 -60
- data/lib/request_log_analyzer/aggregator/database.rb +0 -148
- data/lib/request_log_analyzer/filter/base.rb +0 -29
- data/lib/request_log_analyzer/log_parser.rb +0 -173
- data/lib/request_log_analyzer/source/base.rb +0 -42
- data/lib/request_log_analyzer/source/log_file.rb +0 -170
- data/lib/request_log_analyzer/tracker/base.rb +0 -54
- data/lib/request_log_analyzer/tracker/category.rb +0 -71
- data/spec/controller_spec.rb +0 -40
- data/spec/database_inserter_spec.rb +0 -101
- data/spec/file_format_spec.rb +0 -78
- data/spec/file_formats/spec_format.rb +0 -26
- data/spec/filter_spec.rb +0 -137
- data/spec/line_definition_spec.rb +0 -124
- data/spec/log_parser_spec.rb +0 -68
- data/spec/log_processor_spec.rb +0 -57
- data/spec/merb_format_spec.rb +0 -38
- data/spec/rails_format_spec.rb +0 -76
- data/spec/request_spec.rb +0 -72
- data/spec/summarizer_spec.rb +0 -9
- 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
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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"=>"
|
|
15
|
-
:params
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|