http-log-analyzer 0.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 5b4140aef82a3b635f50830132f5725b21a2f753d17f78cab80ef918807d3410
4
+ data.tar.gz: 91d4884ba4b346211cfdb5707aca91229f5df5dea3c8e6e96657e500b2025ee5
5
+ SHA512:
6
+ metadata.gz: e3ed61db57fb6c32a382695c4d518429e936972c3ec954e0199d3eba54199ec6c47d1b226335a2e3aad59242f790c19cfa8746f0436ea5fce46b57585ba6878f
7
+ data.tar.gz: 8df22a7d0d7344c119caf3cbf05f7d3a678f794094713223179dd7e4634f4d75532303d623d1d44fff1fd05c7a038e0deed99e868fbe91f2e33e7b4ba2590569
@@ -0,0 +1,2 @@
1
+ .DS_Store
2
+ *.gem
@@ -0,0 +1,3 @@
1
+ require 'rubygems/tasks'
2
+
3
+ Gem::Tasks.new
@@ -0,0 +1,32 @@
1
+ collapse referers by domain of referer
2
+
3
+ ensure domain is not in referers list
4
+
5
+ remove 'android-app://com.google.android.gm' from referers list
6
+ or count under Google?
7
+
8
+ cache log files (as Marshalled data) to reduce parsing time
9
+
10
+ move ignore-data to external files
11
+
12
+ use external list for user-agent strings
13
+ http://www.useragentstring.com/pages/useragentstring.php?name=All
14
+ https://www.webmasterworld.com/search_engine_spiders/4849056.htm
15
+
16
+ detect users & sessions
17
+
18
+ store parsed data
19
+ store cache in files
20
+
21
+ generate monthly reports
22
+
23
+ detect users & sessions
24
+ first parse, then process
25
+
26
+ add configuration file (in Ruby)
27
+ site/report name
28
+ domain names
29
+ ignored IPs
30
+
31
+ use more recent geo-IP databases?
32
+ https://dev.maxmind.com/geoip/geoip2/downloadable/
@@ -0,0 +1,47 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'http-log-analyzer'
4
+ require 'simple_option_parser'
5
+
6
+ options = SimpleOptionParser.parse(ARGV,
7
+ domain: nil,
8
+ ignored_cities: nil,
9
+ ignored_browsers: nil,
10
+ ignored_referers: nil,
11
+ ignored_paths: nil,
12
+ period: nil,
13
+ )
14
+
15
+ %i{ignored_cities ignored_browsers ignored_referers ignored_paths}.each do |key|
16
+ if (file = options[key])
17
+ options[key] = HTTPLogAnalyzer::Importer.parse_list(File.read(file))
18
+ end
19
+ end
20
+
21
+ importer = HTTPLogAnalyzer::Importer.new(options)
22
+
23
+ last_filename = nil
24
+ file_size = nil
25
+ ARGF.each_line do |line|
26
+ if ARGF.filename != last_filename
27
+ last_filename = ARGF.filename
28
+ if ARGF.filename == '-'
29
+ file_size = 1
30
+ else
31
+ file_size = File.size(ARGF.filename)
32
+ end
33
+ end
34
+ STDERR.print "%-20s %6d %3d%%\r" % [
35
+ ARGF.filename,
36
+ ARGF.lineno,
37
+ ARGF.filename == '-' ? 100 : ((ARGF.pos.to_f / file_size) * 100).ceil,
38
+ ]
39
+ begin
40
+ importer.process_line(ARGF.filename, ARGF.lineno, line)
41
+ rescue HTTPLogAnalyzer::ParseError => e
42
+ warn "#{e}: #{ARGF.filename}:#{ARGF.lineno}: #{line}"
43
+ end
44
+ end
45
+ STDERR.puts ' ' * ENV['COLUMNS'].to_i
46
+
47
+ importer.report
@@ -0,0 +1,28 @@
1
+ require_relative 'lib/http-log-analyzer/version'
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = 'http-log-analyzer'
5
+ s.version = HTTPLogAnalyzer::VERSION
6
+ s.author = 'John Labovitz'
7
+ s.email = 'johnl@johnlabovitz.com'
8
+
9
+ s.summary = %q{Analyze HTTP log files.}
10
+ # s.description = %q{}
11
+ s.homepage = 'http://github.com/jslabovitz/http-log-analyzer'
12
+ s.license = 'MIT'
13
+
14
+ s.files = `git ls-files`.split("\n")
15
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
16
+ s.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) }
17
+ s.require_path = 'lib'
18
+
19
+ s.add_dependency 'addressable', '~> 2.5'
20
+ s.add_dependency 'geoip', '~> 1.6'
21
+ s.add_dependency 'http-log-parser', '~> 0'
22
+ s.add_dependency 'mime-types', '~> 3.1'
23
+ s.add_dependency 'simple_option_parser', '~> 0.3'
24
+ s.add_dependency 'user_agent_parser', '~> 2.4'
25
+
26
+ s.add_development_dependency 'rake', '~> 12.3'
27
+ s.add_development_dependency 'rubygems-tasks', '~> 0.2'
28
+ end
@@ -0,0 +1,24 @@
1
+ require 'net/http/status'
2
+ require 'date'
3
+
4
+ require 'http_log_parser'
5
+ require 'user_agent_parser'
6
+ require 'addressable/uri'
7
+ require 'mime/types'
8
+ require 'geoip'
9
+
10
+ module HTTPLogAnalyzer
11
+
12
+ class ParseError < Exception; end
13
+
14
+ end
15
+
16
+ require 'http-log-analyzer/entry'
17
+ require 'http-log-analyzer/importer'
18
+ require 'http-log-analyzer/stats'
19
+ require 'http-log-analyzer/element'
20
+ require 'http-log-analyzer/element/referer'
21
+ require 'http-log-analyzer/element/request'
22
+ require 'http-log-analyzer/element/source'
23
+ require 'http-log-analyzer/element/status'
24
+ require 'http-log-analyzer/element/user_agent'
@@ -0,0 +1,19 @@
1
+ module HTTPLogAnalyzer
2
+
3
+ class Element
4
+
5
+ def self.parse(string)
6
+ $cache ||= {}
7
+ $cache[self] ||= {}
8
+ $cache[self][string] ||= new.tap { |e| e.parse(string) }
9
+ end
10
+
11
+ def normalize_uri!(uri)
12
+ uri.normalize!
13
+ uri.scheme = 'http' if uri.scheme == 'https'
14
+ uri.host = uri.host.downcase.sub(/^(www|m)\./, '').sub(/\.$/, '') if uri.host
15
+ end
16
+
17
+ end
18
+
19
+ end
@@ -0,0 +1,39 @@
1
+ module HTTPLogAnalyzer
2
+
3
+ class Element
4
+
5
+ class Referer < Element
6
+
7
+ QueryKeys = %w{q p searchfor wd}
8
+
9
+ attr_accessor :uri
10
+ attr_accessor :query
11
+
12
+ def parse(string)
13
+ if string != '-'
14
+ @uri = Addressable::URI.parse(string) or raise ParseError, "Can't parse URI: #{string}"
15
+ normalize_uri!(@uri)
16
+ if @uri.host
17
+ # normalize Facebook link-shims
18
+ if @uri.host =~ /^(l|lm)\.facebook\.com$/ || @uri.path.start_with?('/l.php')
19
+ @uri.host = 'facebook.com'
20
+ @uri.path = '/'
21
+ @uri.query = nil
22
+ end
23
+ if (values = @uri.query_values)
24
+ QueryKeys.each do |key|
25
+ if (value = values[key]) && !value.empty?
26
+ @query = values[key]
27
+ break
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+
35
+ end
36
+
37
+ end
38
+
39
+ end
@@ -0,0 +1,21 @@
1
+ module HTTPLogAnalyzer
2
+
3
+ class Element
4
+
5
+ class Request < Element
6
+
7
+ attr_accessor :uri
8
+ attr_accessor :mime_types
9
+
10
+ def parse(string)
11
+ @method, uri_string, @version = string.split(/\s+/)
12
+ @uri = Addressable::URI.parse(uri_string) or raise ParseError, "Can't parse URI: #{uri_string}"
13
+ normalize_uri!(@uri)
14
+ @mime_types = MIME::Types.type_for(@uri.path)
15
+ end
16
+
17
+ end
18
+
19
+ end
20
+
21
+ end
@@ -0,0 +1,32 @@
1
+ module HTTPLogAnalyzer
2
+
3
+ class Element
4
+
5
+ class Source < Element
6
+
7
+ attr_accessor :address
8
+ attr_accessor :country
9
+ attr_accessor :region
10
+ attr_accessor :city
11
+
12
+ def parse(string)
13
+ result = $geo_ip.city(string)
14
+ if result
15
+ city, region, country = %i{city_name real_region_name country_name}.map do |key|
16
+ value = result.send(key)
17
+ value.to_s.empty? ? nil : value
18
+ end
19
+ @address = result.ip
20
+ @country = country
21
+ @region = [region, country].join(', ') if region
22
+ @city = [city, region, country].join(', ') if city
23
+ else
24
+ @ip = string
25
+ end
26
+ end
27
+
28
+ end
29
+
30
+ end
31
+
32
+ end
@@ -0,0 +1,23 @@
1
+ module HTTPLogAnalyzer
2
+
3
+ class Element
4
+
5
+ class Status < Element
6
+
7
+ attr_accessor :code
8
+ attr_accessor :message
9
+
10
+ def parse(string)
11
+ @code = string.to_i
12
+ @message = Net::HTTP::STATUS_CODES[@code]
13
+ end
14
+
15
+ def class
16
+ @code / 100
17
+ end
18
+
19
+ end
20
+
21
+ end
22
+
23
+ end
@@ -0,0 +1,38 @@
1
+ module HTTPLogAnalyzer
2
+
3
+ class Element
4
+
5
+ class UserAgent < Element
6
+
7
+ attr_accessor :browser
8
+ attr_accessor :system
9
+
10
+ def parse(string)
11
+ if string != '-'
12
+ user_agent = $user_agent_parser.parse(string)
13
+ @browser = case (family = user_agent.family.strip)
14
+ when nil, 'Other'
15
+ 'other'
16
+ else
17
+ family
18
+ end
19
+ @system = case (name = user_agent.os.name.strip)
20
+ when /^Windows\s/
21
+ 'Windows'
22
+ when /^Mac OS/i
23
+ 'macOS'
24
+ when 'Other', nil
25
+ 'other'
26
+ when 'Ubuntu', 'Fedora', 'SUSE'
27
+ 'Linux'
28
+ else
29
+ name
30
+ end
31
+ end
32
+ end
33
+
34
+ end
35
+
36
+ end
37
+
38
+ end
@@ -0,0 +1,28 @@
1
+ module HTTPLogAnalyzer
2
+
3
+ class Entry
4
+
5
+ attr_accessor :timestamp
6
+ attr_accessor :source
7
+ attr_accessor :request
8
+ attr_accessor :status
9
+ attr_accessor :referer
10
+ attr_accessor :user_agent
11
+
12
+ def initialize(info)
13
+ # 12/Feb/2016:09:59:04 +0000
14
+ @timestamp = DateTime.strptime(info[:datetime], '%d/%b/%Y:%H:%M:%S %Z')
15
+ @source = Element::Source.parse(info[:ip])
16
+ @request = Element::Request.parse(info[:request])
17
+ @status = Element::Status.parse(info[:status])
18
+ @referer = Element::Referer.parse(info[:referer])
19
+ @user_agent = Element::UserAgent.parse(info[:user_agent])
20
+ end
21
+
22
+ def calendar_week
23
+ '%s - %s' % [1, 7].map { |d| DateTime.commercial(@timestamp.year, @timestamp.cweek, d).to_date }
24
+ end
25
+
26
+ end
27
+
28
+ end
@@ -0,0 +1,90 @@
1
+ module HTTPLogAnalyzer
2
+
3
+ class Importer
4
+
5
+ StatusKeys = {
6
+ 4 => :client_error_statuses,
7
+ 5 => :server_error_statuses,
8
+ }
9
+
10
+ def initialize(
11
+ domain:,
12
+ ignored_ips: nil,
13
+ ignored_cities: nil,
14
+ ignored_browsers: nil,
15
+ ignored_referers: nil,
16
+ ignored_paths: nil,
17
+ period: nil
18
+ )
19
+ @domain = domain
20
+ @ignored_ips = ignored_ips || {}
21
+ @ignored_cities = ignored_cities || {}
22
+ @ignored_browsers = ignored_browsers || {}
23
+ @ignored_referers = ignored_referers || {}
24
+ @ignored_paths = ignored_paths || {}
25
+ @period = period ? parse_period(period) : nil
26
+ @log_parser = HttpLogParser.new
27
+ $user_agent_parser = UserAgentParser::Parser.new
28
+ $geo_ip = GeoIP.new(File.expand_path('/usr/local/var/GeoIP/GeoLiteCity.dat'))
29
+ @stats = Stats.new
30
+ end
31
+
32
+ def parse_period(period)
33
+ Range.new(*period.split(' - ', 2).map { |d| DateTime.parse(d) })
34
+ end
35
+
36
+ def process_line(file, line_num, line)
37
+ begin
38
+ data = @log_parser.parse_line(line)
39
+ rescue => e
40
+ raise ParseError, "Can't parse line: #{line}"
41
+ end
42
+ entry = Entry.new(data)
43
+ if @period && !@period.cover?(entry.timestamp)
44
+ # ignore timestamp out of specified period
45
+ elsif ignore?(entry)
46
+ @ignored_ips[entry.source.address] = true
47
+ elsif page?(entry)
48
+ if (statuses_key = StatusKeys[entry.status.class])
49
+ @stats.add(statuses_key, "#{entry.status.code}: #{entry.request.uri.path}")
50
+ end
51
+ @stats.add(:source_country, entry.source.country)
52
+ @stats.add(:source_region, entry.source.region)
53
+ @stats.add(:source_city, entry.source.city)
54
+ @stats.add(:wv_source_city, entry.source.city) if entry.source.region == 'West Virginia, United States'
55
+ @stats.add(:pages, entry.request.uri.path)
56
+ @stats.add(:via, entry.referer.uri) unless entry.referer&.uri&.host == @domain
57
+ @stats.add(:searches, entry.referer.query) if entry.referer&.query
58
+ @stats.add(:browsers, entry.user_agent.browser)
59
+ @stats.add(:systems, entry.user_agent.system)
60
+ @stats.add(:dates, entry.calendar_week)
61
+ end
62
+ end
63
+
64
+ def ignore?(entry)
65
+ @ignored_ips[entry.source.address] ||
66
+ @ignored_browsers[entry.user_agent.browser] ||
67
+ @ignored_cities[entry.source.city] ||
68
+ (entry.referer&.uri && @ignored_referers[entry.referer&.uri&.host]) ||
69
+ @ignored_paths[entry.request.uri.path]
70
+ end
71
+
72
+ def page?(entry)
73
+ entry.request.uri.path !~ %r{/.*?https?://} && # bad URL construction (from bot)
74
+ !entry.request.uri.path.empty? &&
75
+ entry.request.mime_types.empty? # pages have empty MIME types
76
+ end
77
+
78
+ def report
79
+ @stats.report
80
+ end
81
+
82
+ def self.parse_list(text)
83
+ Hash[
84
+ text.split(/\n/).map { |s| s.sub(/#.*/, '').strip }.reject(&:empty?).map { |s| [s, true] }
85
+ ]
86
+ end
87
+
88
+ end
89
+
90
+ end
@@ -0,0 +1,79 @@
1
+ module HTTPLogAnalyzer
2
+
3
+ class Stats
4
+
5
+ Sections = {
6
+ via: %q{Top 20 sites people came from (aka 'referers')},
7
+ searches: %q{Searches},
8
+ source_country: %q{Top 20 countries visitors came from},
9
+ source_region: %q{Top 20 regions visitors came from},
10
+ source_city: %q{Top 20 cities visitors came from},
11
+ wv_source_city: %q{Top 20 WV cities visitors came from},
12
+ browsers: %q{Top 20 browsers of visitors},
13
+ systems: %q{Top 20 operating systems of visitors},
14
+ pages: %q{Top 20 pages visited at our site},
15
+ informational_statuses: %q{Informational: request received, continuing process},
16
+ success_statuses: %q{Success: action successfully received, understood, accepted},
17
+ redirection_statuses: %q{Redirection: further action must be taken in order to complete the request},
18
+ client_error_statuses: %q{Client error: request contains bad syntax or cannot be fulfilled},
19
+ server_error_statuses: %q{Server error: server failed to fulfill an apparently valid request},
20
+ dates: %q{Visitors per week},
21
+ }
22
+
23
+ def initialize
24
+ @stats = Hash[ Sections.keys.map { |k| [k, {}] } ]
25
+ end
26
+
27
+ def add(key, value)
28
+ if value
29
+ table = @stats[key] or raise "No stats for key #{key.inspect}"
30
+ table[value] ||= 0
31
+ table[value] += 1
32
+ end
33
+ end
34
+
35
+ def report
36
+ @stats.each do |field, counts|
37
+ next if counts.empty?
38
+ puts; puts "#{Sections[field]}:"
39
+ total = counts.values.sum
40
+ if field == :dates
41
+ counts = counts.sort_by { |k, v| k }
42
+ else
43
+ counts = counts.sort_by { |k, v| v }.reverse[0..19]
44
+ end
45
+ counts.each do |label, count|
46
+ puts ' %s' % BarGraphItem.new(label: label, count: count, total: total)
47
+ end
48
+ end
49
+ end
50
+
51
+ class BarGraphItem
52
+
53
+ def initialize(label:, count:, total:, width: 50)
54
+ @label = label
55
+ @count = count
56
+ @total = total
57
+ @width = width
58
+ @fraction = @count.to_f / @total
59
+ end
60
+
61
+ def bar
62
+ n = (@fraction * @width).ceil.to_i
63
+ ('.' * (@width - n)) + ('#' * n)
64
+ end
65
+
66
+ def to_s
67
+ '%s %3d%% %6d %s' % [
68
+ bar,
69
+ (@fraction * 100).ceil.to_i,
70
+ @count,
71
+ @label,
72
+ ]
73
+ end
74
+
75
+ end
76
+
77
+ end
78
+
79
+ end
@@ -0,0 +1,5 @@
1
+ module HTTPLogAnalyzer
2
+
3
+ VERSION = '0.3'
4
+
5
+ end
metadata ADDED
@@ -0,0 +1,172 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: http-log-analyzer
3
+ version: !ruby/object:Gem::Version
4
+ version: '0.3'
5
+ platform: ruby
6
+ authors:
7
+ - John Labovitz
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2018-03-22 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: addressable
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '2.5'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '2.5'
27
+ - !ruby/object:Gem::Dependency
28
+ name: geoip
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.6'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.6'
41
+ - !ruby/object:Gem::Dependency
42
+ name: http-log-parser
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: mime-types
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '3.1'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '3.1'
69
+ - !ruby/object:Gem::Dependency
70
+ name: simple_option_parser
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '0.3'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '0.3'
83
+ - !ruby/object:Gem::Dependency
84
+ name: user_agent_parser
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '2.4'
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '2.4'
97
+ - !ruby/object:Gem::Dependency
98
+ name: rake
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '12.3'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '12.3'
111
+ - !ruby/object:Gem::Dependency
112
+ name: rubygems-tasks
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: '0.2'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: '0.2'
125
+ description:
126
+ email: johnl@johnlabovitz.com
127
+ executables:
128
+ - http-log-analyzer
129
+ extensions: []
130
+ extra_rdoc_files: []
131
+ files:
132
+ - ".gitignore"
133
+ - Rakefile
134
+ - TODO.txt
135
+ - bin/http-log-analyzer
136
+ - http-log-analyzer.gemspec
137
+ - lib/http-log-analyzer.rb
138
+ - lib/http-log-analyzer/element.rb
139
+ - lib/http-log-analyzer/element/referer.rb
140
+ - lib/http-log-analyzer/element/request.rb
141
+ - lib/http-log-analyzer/element/source.rb
142
+ - lib/http-log-analyzer/element/status.rb
143
+ - lib/http-log-analyzer/element/user_agent.rb
144
+ - lib/http-log-analyzer/entry.rb
145
+ - lib/http-log-analyzer/importer.rb
146
+ - lib/http-log-analyzer/stats.rb
147
+ - lib/http-log-analyzer/version.rb
148
+ homepage: http://github.com/jslabovitz/http-log-analyzer
149
+ licenses:
150
+ - MIT
151
+ metadata: {}
152
+ post_install_message:
153
+ rdoc_options: []
154
+ require_paths:
155
+ - lib
156
+ required_ruby_version: !ruby/object:Gem::Requirement
157
+ requirements:
158
+ - - ">="
159
+ - !ruby/object:Gem::Version
160
+ version: '0'
161
+ required_rubygems_version: !ruby/object:Gem::Requirement
162
+ requirements:
163
+ - - ">="
164
+ - !ruby/object:Gem::Version
165
+ version: '0'
166
+ requirements: []
167
+ rubyforge_project:
168
+ rubygems_version: 2.7.6
169
+ signing_key:
170
+ specification_version: 4
171
+ summary: Analyze HTTP log files.
172
+ test_files: []