http-log-analyzer 0.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +2 -0
- data/Rakefile +3 -0
- data/TODO.txt +32 -0
- data/bin/http-log-analyzer +47 -0
- data/http-log-analyzer.gemspec +28 -0
- data/lib/http-log-analyzer.rb +24 -0
- data/lib/http-log-analyzer/element.rb +19 -0
- data/lib/http-log-analyzer/element/referer.rb +39 -0
- data/lib/http-log-analyzer/element/request.rb +21 -0
- data/lib/http-log-analyzer/element/source.rb +32 -0
- data/lib/http-log-analyzer/element/status.rb +23 -0
- data/lib/http-log-analyzer/element/user_agent.rb +38 -0
- data/lib/http-log-analyzer/entry.rb +28 -0
- data/lib/http-log-analyzer/importer.rb +90 -0
- data/lib/http-log-analyzer/stats.rb +79 -0
- data/lib/http-log-analyzer/version.rb +5 -0
- metadata +172 -0
checksums.yaml
ADDED
@@ -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
|
data/.gitignore
ADDED
data/Rakefile
ADDED
data/TODO.txt
ADDED
@@ -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
|
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: []
|