log_sense 1.0.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.
- checksums.yaml +7 -0
- data/.gitignore +10 -0
- data/CHANGELOG.org +27 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +37 -0
- data/LICENSE.txt +21 -0
- data/README.org +114 -0
- data/Rakefile +15 -0
- data/alr-styles.css +61 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/exe/log_sense +66 -0
- data/ip_locations/dbip-country-lite.sqlite3 +0 -0
- data/lib/log_sense/apache_data_cruncher.rb +131 -0
- data/lib/log_sense/apache_log_parser.rb +87 -0
- data/lib/log_sense/emitter.rb +49 -0
- data/lib/log_sense/ip_locator.rb +55 -0
- data/lib/log_sense/options_parser.rb +86 -0
- data/lib/log_sense/rails_data_cruncher.rb +117 -0
- data/lib/log_sense/rails_log_parser.rb +176 -0
- data/lib/log_sense/templates/#apache.org.erb# +266 -0
- data/lib/log_sense/templates/.#apache.org.erb +1 -0
- data/lib/log_sense/templates/_output_table.html.erb +25 -0
- data/lib/log_sense/templates/apache.html.erb +328 -0
- data/lib/log_sense/templates/apache.org.erb +266 -0
- data/lib/log_sense/templates/rails.txt.erb +39 -0
- data/lib/log_sense/version.rb +3 -0
- data/lib/log_sense.rb +8 -0
- data/log_sense.gemspec +39 -0
- metadata +189 -0
@@ -0,0 +1,55 @@
|
|
1
|
+
require 'csv'
|
2
|
+
require 'sqlite3'
|
3
|
+
require 'ipaddr'
|
4
|
+
require 'iso_country_codes'
|
5
|
+
|
6
|
+
module LogSense
|
7
|
+
module IpLocator
|
8
|
+
DB_FILE = "ip_locations/dbip-country-lite.sqlite3"
|
9
|
+
|
10
|
+
def self.dbip_to_sqlite db_location
|
11
|
+
db = SQLite3::Database.new ":memory:"
|
12
|
+
db.execute "CREATE TABLE ip_location (
|
13
|
+
from_ip_n INTEGER,
|
14
|
+
from_ip TEXT,
|
15
|
+
to_ip TEXT,
|
16
|
+
country_code TEXT
|
17
|
+
)"
|
18
|
+
|
19
|
+
ins = db.prepare "INSERT INTO ip_location(from_ip_n, from_ip, to_ip, country_code) values (?, ?, ?, ?)"
|
20
|
+
CSV.foreach(db_location) do |row|
|
21
|
+
ip = IPAddr.new row[0]
|
22
|
+
ins.execute(ip.to_i, row[0], row[1], row[2])
|
23
|
+
end
|
24
|
+
|
25
|
+
# persist to file
|
26
|
+
ddb = SQLite3::Database.new(DB_FILE)
|
27
|
+
b = SQLite3::Backup.new(ddb, 'main', db, 'main')
|
28
|
+
b.step(-1) #=> DONE
|
29
|
+
b.finish
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.load_db
|
33
|
+
SQLite3::Database.new DB_FILE
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.locate_ip ip, db
|
37
|
+
ip_n = IPAddr.new(ip).to_i
|
38
|
+
res = db.execute "SELECT * FROM ip_location where from_ip_n <= #{ip_n} order by from_ip_n desc limit 1"
|
39
|
+
IsoCountryCodes.find(res[0][3]).name
|
40
|
+
end
|
41
|
+
|
42
|
+
#
|
43
|
+
# add country code to data[:ips]
|
44
|
+
#
|
45
|
+
def self.geolocate data
|
46
|
+
@location_db = IpLocator::load_db
|
47
|
+
data[:ips].each do |ip|
|
48
|
+
country_code = IpLocator::locate_ip ip[0], @location_db
|
49
|
+
ip << country_code
|
50
|
+
end
|
51
|
+
data
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
require 'optparse/date'
|
3
|
+
require 'apache_log_report/version'
|
4
|
+
|
5
|
+
module LogSense
|
6
|
+
module OptionsParser
|
7
|
+
#
|
8
|
+
# parse command line options
|
9
|
+
#
|
10
|
+
def self.parse options
|
11
|
+
limit = 30
|
12
|
+
args = {}
|
13
|
+
|
14
|
+
opt_parser = OptionParser.new do |opts|
|
15
|
+
opts.banner = "Usage: log_sense [options] [logfile]"
|
16
|
+
|
17
|
+
opts.on("-fFORMAT", "--from=FORMAT", String, "Input format (either rails or apache)") do |n|
|
18
|
+
args[:input_format] = n
|
19
|
+
end
|
20
|
+
|
21
|
+
opts.on("-iINPUT_FILE", "--input=INPUT_FILE", String, "Input file") do |n|
|
22
|
+
args[:input_file] = n
|
23
|
+
end
|
24
|
+
|
25
|
+
opts.on("-tFORMAT", "--to=FORMAT", String, "Output format: html, org, txt, sqlite. Defaults to org mode") do |n|
|
26
|
+
args[:output_format] = n
|
27
|
+
end
|
28
|
+
|
29
|
+
opts.on("-oOUTPUT_FILE", "--output=OUTPUT_FILE", String, "Output file") do |n|
|
30
|
+
args[:output_file] = n
|
31
|
+
end
|
32
|
+
|
33
|
+
opts.on("-bDATE", "--begin=DATE", Date, "Consider entries after or on DATE") do |n|
|
34
|
+
args[:from_date] = n
|
35
|
+
end
|
36
|
+
|
37
|
+
opts.on("-eDATE", "--end=DATE", Date, "Consider entries before or on DATE") do |n|
|
38
|
+
args[:to_date] = n
|
39
|
+
end
|
40
|
+
|
41
|
+
opts.on("-lN", "--limit=N", Integer, "Number of entries to show (defaults to #{limit})") do |n|
|
42
|
+
args[:limit] = n
|
43
|
+
end
|
44
|
+
|
45
|
+
opts.on("-cPOLICY", "--crawlers=POLICY", String, "Decide what to do with crawlers (applies to Apache Logs)") do |n|
|
46
|
+
case n
|
47
|
+
when 'only'
|
48
|
+
args[:only_crawlers] = true
|
49
|
+
when 'ignore'
|
50
|
+
args[:ignore_crawlers] = true
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
opts.on("-np", "--ignore-selfpoll", "Ignore self poll entries (requests from ::1; applies to Apache Logs)") do
|
55
|
+
args[:no_selfpoll] = true
|
56
|
+
end
|
57
|
+
|
58
|
+
opts.on("-v", "--version", "Prints version information") do
|
59
|
+
puts "log_sense version #{LogSense::VERSION}"
|
60
|
+
puts "Copyright (C) 2020 Adolfo Villafiorita"
|
61
|
+
puts "Distributed under the terms of the MIT license"
|
62
|
+
puts ""
|
63
|
+
puts "Written by Adolfo Villafiorita"
|
64
|
+
exit
|
65
|
+
end
|
66
|
+
|
67
|
+
opts.on("-h", "--help", "Prints this help") do
|
68
|
+
puts opts
|
69
|
+
puts "This is version #{LogSense::VERSION}"
|
70
|
+
exit
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
opt_parser.parse!(options)
|
75
|
+
|
76
|
+
args[:limit] ||= limit
|
77
|
+
args[:input_format] ||= "apache"
|
78
|
+
args[:output_format] ||= "html"
|
79
|
+
args[:ignore_crawlers] ||= false
|
80
|
+
args[:only_crawlers] ||= false
|
81
|
+
args[:no_selfpoll] ||= false
|
82
|
+
|
83
|
+
return args
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,117 @@
|
|
1
|
+
module LogSense
|
2
|
+
module RailsDataCruncher
|
3
|
+
|
4
|
+
#
|
5
|
+
# take a sqlite3 database and analyze data
|
6
|
+
#
|
7
|
+
# @ variables are automatically put in the returned data
|
8
|
+
#
|
9
|
+
|
10
|
+
def self.crunch db, options = { limit: 30 }
|
11
|
+
first_day_s = db.execute "SELECT started_at from Event order by started_at limit 1"
|
12
|
+
# we could use ended_at to cover the full activity period, but I prefer started_at
|
13
|
+
# with the meaning that the monitor event initiation
|
14
|
+
last_day_s = db.execute "SELECT started_at from Event order by started_at desc limit 1"
|
15
|
+
|
16
|
+
# make first and last day into dates or nil
|
17
|
+
# TODO: bug possible value here: [[nil]], which is not empty
|
18
|
+
@first_day = first_day_s.empty? ? nil : Date.parse(first_day_s[0][0])
|
19
|
+
@last_day = last_day_s.empty? ? nil : Date.parse(last_day_s[0][0])
|
20
|
+
|
21
|
+
@total_days = 0
|
22
|
+
if @first_day and @last_day
|
23
|
+
@total_days = (@last_day - @first_day).to_i
|
24
|
+
end
|
25
|
+
|
26
|
+
@events = db.execute "SELECT count(started_at) from Event"
|
27
|
+
|
28
|
+
@first_day_requested = options[:from_date]
|
29
|
+
@last_day_requested = options[:to_date]
|
30
|
+
|
31
|
+
@first_day_in_analysis = date_intersect options[:from_date], @first_day, :max
|
32
|
+
@last_day_in_analysis = date_intersect options[:to_date], @last_day, :min
|
33
|
+
|
34
|
+
@total_days_in_analysis = 0
|
35
|
+
if @first_day_in_analysis and @last_day_in_analysis
|
36
|
+
@total_days_in_analysis = (@last_day_in_analysis - @first_day_in_analysis).to_i
|
37
|
+
end
|
38
|
+
|
39
|
+
#
|
40
|
+
# generate the where clause corresponding to the command line options to filter data
|
41
|
+
#
|
42
|
+
filter = [
|
43
|
+
(options[:from_date] ? "date(started_at) >= '#{options[:from_date]}'" : nil),
|
44
|
+
(options[:to_date] ? "date(started_at) <= '#{options[:to_date]}'" : nil),
|
45
|
+
"true"
|
46
|
+
].compact.join " and "
|
47
|
+
|
48
|
+
mega = 1024 * 1024
|
49
|
+
giga = mega * 1024
|
50
|
+
tera = giga * 1024
|
51
|
+
|
52
|
+
# in alternative to sum(size)
|
53
|
+
human_readable_size = <<-EOS
|
54
|
+
CASE
|
55
|
+
WHEN sum(size) < 1024 THEN sum(size) || ' B'
|
56
|
+
WHEN sum(size) >= 1024 AND sum(size) < (#{mega}) THEN ROUND((CAST(sum(size) AS REAL) / 1024), 2) || ' KB'
|
57
|
+
WHEN sum(size) >= (#{mega}) AND sum(size) < (#{giga}) THEN ROUND((CAST(sum(size) AS REAL) / (#{mega})), 2) || ' MB'
|
58
|
+
WHEN sum(size) >= (#{giga}) AND sum(size) < (#{tera}) THEN ROUND((CAST(sum(size) AS REAL) / (#{giga})), 2) || ' GB'
|
59
|
+
WHEN sum(size) >= (#{tera}) THEN ROUND((CAST(sum(size) AS REAL) / (#{tera})), 2) || ' TB'
|
60
|
+
END AS size
|
61
|
+
EOS
|
62
|
+
|
63
|
+
human_readable_day = <<-EOS
|
64
|
+
case cast (strftime('%w', started_at) as integer)
|
65
|
+
when 0 then 'Sunday'
|
66
|
+
when 1 then 'Monday'
|
67
|
+
when 2 then 'Tuesday'
|
68
|
+
when 3 then 'Wednesday'
|
69
|
+
when 4 then 'Thursday'
|
70
|
+
when 5 then 'Friday'
|
71
|
+
else 'Saturday'
|
72
|
+
end as dow
|
73
|
+
EOS
|
74
|
+
|
75
|
+
@total_events = db.execute "SELECT count(started_at) from Event where #{filter}"
|
76
|
+
|
77
|
+
@daily_distribution = db.execute "SELECT date(started_at), #{human_readable_day}, count(started_at) from Event where #{filter} group by date(started_at)"
|
78
|
+
@time_distribution = db.execute "SELECT strftime('%H', started_at), count(started_at) from Event where #{filter} group by strftime('%H', started_at)"
|
79
|
+
|
80
|
+
@statuses = db.execute "SELECT status, count(status) from Event where #{filter} group by status order by status"
|
81
|
+
|
82
|
+
@by_day_4xx = db.execute "SELECT date(started_at), count(started_at) from Event where substr(status, 1,1) == '4' and #{filter} group by date(started_at)"
|
83
|
+
@by_day_3xx = db.execute "SELECT date(started_at), count(started_at) from Event where substr(status, 1,1) == '3' and #{filter} group by date(started_at)"
|
84
|
+
@by_day_2xx = db.execute "SELECT date(started_at), count(started_at) from Event where substr(status, 1,1) == '2' and #{filter} group by date(started_at)"
|
85
|
+
|
86
|
+
@statuses_by_day = (@by_day_2xx + @by_day_3xx + @by_day_4xx).group_by { |x| x[0] }.to_a.map { |x|
|
87
|
+
[x[0], x[1].map { |y| y[1] }].flatten
|
88
|
+
}
|
89
|
+
|
90
|
+
@ips = db.execute "SELECT ip, count(ip) from Event where #{filter} group by ip order by count(ip) desc limit #{options[:limit]}"
|
91
|
+
|
92
|
+
@performance = db.execute "SELECT distinct(controller), count(controller), printf(\"%.2f\", min(duration_total_ms)), printf(\"%.2f\", avg(duration_total_ms)), printf(\"%.2f\", max(duration_total_ms)) from Event group by controller order by controller"
|
93
|
+
|
94
|
+
data = {}
|
95
|
+
self.instance_variables.each do |variable|
|
96
|
+
var_as_symbol = variable.to_s[1..-1].to_sym
|
97
|
+
data[var_as_symbol] = eval(variable.to_s)
|
98
|
+
end
|
99
|
+
data
|
100
|
+
end
|
101
|
+
|
102
|
+
private
|
103
|
+
|
104
|
+
def self.date_intersect date1, date2, method
|
105
|
+
if date1 and date2
|
106
|
+
[date1, date2].send(method)
|
107
|
+
elsif date1
|
108
|
+
date1
|
109
|
+
else
|
110
|
+
date2
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
@@ -0,0 +1,176 @@
|
|
1
|
+
require 'sqlite3'
|
2
|
+
require 'byebug'
|
3
|
+
|
4
|
+
module LogSense
|
5
|
+
module RailsLogParser
|
6
|
+
def self.parse filename, options = {}
|
7
|
+
content = filename ? File.readlines(filename) : ARGF.readlines
|
8
|
+
|
9
|
+
db = SQLite3::Database.new ":memory:"
|
10
|
+
db.execute 'CREATE TABLE IF NOT EXISTS Event(
|
11
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
12
|
+
started_at TEXT,
|
13
|
+
ended_at TEXT,
|
14
|
+
log_id TEXT,
|
15
|
+
ip TEXT,
|
16
|
+
url TEXT,
|
17
|
+
controller TEXT,
|
18
|
+
html_verb TEXT,
|
19
|
+
status INTEGER,
|
20
|
+
duration_total_ms FLOAT,
|
21
|
+
duration_views_ms FLOAT,
|
22
|
+
duration_ar_ms FLOAT,
|
23
|
+
allocations INTEGER
|
24
|
+
)'
|
25
|
+
|
26
|
+
ins = db.prepare("insert into Event(
|
27
|
+
started_at,
|
28
|
+
ended_at,
|
29
|
+
log_id,
|
30
|
+
ip,
|
31
|
+
url,
|
32
|
+
controller,
|
33
|
+
html_verb,
|
34
|
+
status,
|
35
|
+
duration_total_ms,
|
36
|
+
duration_views_ms,
|
37
|
+
duration_ar_ms,
|
38
|
+
allocations)
|
39
|
+
values (#{Array.new(12, '?').join(', ')})")
|
40
|
+
|
41
|
+
# requests in the log might be interleaved.
|
42
|
+
#
|
43
|
+
# We use the 'pending' variable to progressively store data
|
44
|
+
# about requests till they are completed; whey they are
|
45
|
+
# complete, we enter the entry in the DB and remove it from the
|
46
|
+
# hash
|
47
|
+
pending = {}
|
48
|
+
|
49
|
+
# Log lines are either one of:
|
50
|
+
#
|
51
|
+
# LOG_LEVEL, [ZULU_TIMESTAMP #NUMBER] INFO --: [ID] Started VERB "URL" for IP at TIMESTAMP
|
52
|
+
# LOG_LEVEL, [ZULU_TIMESTAMP #NUMBER] INFO --: [ID] Processing by CONTROLLER as FORMAT
|
53
|
+
# LOG_LEVEL, [ZULU_TIMESTAMP #NUMBER] INFO --: [ID] Parameters: JSON
|
54
|
+
# LOG_LEVEL, [ZULU_TIMESTAMP #NUMBER] INFO --: [ID] Rendered VIEW within LAYOUT (Duration: DURATION | Allocations: ALLOCATIONS)
|
55
|
+
# LOG_LEVEL, [ZULU_TIMESTAMP #NUMBER] INFO --: [ID] Completed STATUS STATUS_STRING in DURATION (Views: DURATION | ActiveRecord: DURATION | Allocations: NUMBER)
|
56
|
+
#
|
57
|
+
# and they appears in the order shown above: started, processing, ...
|
58
|
+
#
|
59
|
+
# Different requests might be interleaved, of course
|
60
|
+
|
61
|
+
File.readlines(filename).each do |line|
|
62
|
+
# We discard LOG_LEVEL != 'I'
|
63
|
+
next if line[0] != 'I'
|
64
|
+
|
65
|
+
data = self.match_and_process_start line
|
66
|
+
if data
|
67
|
+
id = data[:log_id]
|
68
|
+
pending[id] = data.merge (pending[id] || {})
|
69
|
+
next
|
70
|
+
end
|
71
|
+
|
72
|
+
data = self.match_and_process_processing_by line
|
73
|
+
if data
|
74
|
+
id = data[:log_id]
|
75
|
+
pending[id] = data.merge (pending[id] || {})
|
76
|
+
next
|
77
|
+
end
|
78
|
+
|
79
|
+
data = self.match_and_process_completed line
|
80
|
+
if data
|
81
|
+
id = data[:log_id]
|
82
|
+
|
83
|
+
# it might as well be that the first event started before
|
84
|
+
# the log. With this, we make sure we add only events whose
|
85
|
+
# start was logged and parsed
|
86
|
+
if pending[id]
|
87
|
+
event = data.merge (pending[id] || {})
|
88
|
+
|
89
|
+
ins.execute(
|
90
|
+
event[:started_at],
|
91
|
+
event[:ended_at],
|
92
|
+
event[:log_id],
|
93
|
+
event[:ip],
|
94
|
+
event[:url],
|
95
|
+
event[:controller],
|
96
|
+
event[:html_verb],
|
97
|
+
event[:status],
|
98
|
+
event[:duration_total_ms],
|
99
|
+
event[:duration_views_ms],
|
100
|
+
event[:duration_ar_ms],
|
101
|
+
event[:allocations]
|
102
|
+
)
|
103
|
+
|
104
|
+
pending.delete(id)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
db
|
110
|
+
end
|
111
|
+
|
112
|
+
TIMESTAMP = /(?<timestamp>[^ ]+)/
|
113
|
+
ID = /(?<id>[a-z0-9-]+)/
|
114
|
+
VERB = /(?<verb>GET|POST|PATCH|PUT|DELETE)/
|
115
|
+
URL = /(?<url>[^"]+)/
|
116
|
+
IP = /(?<ip>[0-9.]+)/
|
117
|
+
STATUS = /(?<status>[0-9]+)/
|
118
|
+
MSECS = /[0-9.]+/
|
119
|
+
|
120
|
+
# I, [2021-10-19T08:16:34.343858 #10477] INFO -- : [67103c0d-455d-4fe8-951e-87e97628cb66] Started GET "/grow/people/471" for 217.77.80.35 at 2021-10-19 08:16:34 +0000
|
121
|
+
STARTED_REGEXP = /I, \[#{TIMESTAMP} #[0-9]+\] INFO -- : \[#{ID}\] Started #{VERB} "#{URL}" for #{IP} at/
|
122
|
+
|
123
|
+
def self.match_and_process_start line
|
124
|
+
matchdata = STARTED_REGEXP.match line
|
125
|
+
if matchdata
|
126
|
+
{
|
127
|
+
started_at: matchdata[:timestamp],
|
128
|
+
log_id: matchdata[:id],
|
129
|
+
html_verb: matchdata[:verb],
|
130
|
+
url: matchdata[:url],
|
131
|
+
ip: matchdata[:ip]
|
132
|
+
}
|
133
|
+
else
|
134
|
+
nil
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
# I, [2021-10-19T08:16:34.712331 #10477] INFO -- : [67103c0d-455d-4fe8-951e-87e97628cb66] Completed 200 OK in 367ms (Views: 216.7ms | ActiveRecord: 141.3ms | Allocations: 168792)
|
139
|
+
COMPLETED_REGEXP = /I, \[#{TIMESTAMP} #[0-9]+\] INFO -- : \[#{ID}\] Completed #{STATUS} [^ ]+ in (?<total>#{MSECS})ms \(Views: (?<views>#{MSECS})ms \| ActiveRecord: (?<arec>#{MSECS})ms \| Allocations: (?<alloc>[0-9]+)\)/
|
140
|
+
|
141
|
+
def self.match_and_process_completed line
|
142
|
+
matchdata = COMPLETED_REGEXP.match line
|
143
|
+
if matchdata
|
144
|
+
{
|
145
|
+
ended_at: matchdata[:timestamp],
|
146
|
+
log_id: matchdata[:id],
|
147
|
+
status: matchdata[:status],
|
148
|
+
duration_total_ms: matchdata[:total],
|
149
|
+
duration_views_ms: matchdata[:views],
|
150
|
+
duration_ar_ms: matchdata[:arec],
|
151
|
+
allocations: matchdata[:alloc],
|
152
|
+
}
|
153
|
+
else
|
154
|
+
nil
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
# I, [2021-10-19T08:16:34.345162 #10477] INFO -- : [67103c0d-455d-4fe8-951e-87e97628cb66] Processing by PeopleController#show as HTML
|
159
|
+
PROCESSING_REGEXP = /I, \[#{TIMESTAMP} #[0-9]+\] INFO -- : \[#{ID}\] Processing by (?<controller>[^ ]+) as/
|
160
|
+
|
161
|
+
def self.match_and_process_processing_by line
|
162
|
+
matchdata = PROCESSING_REGEXP.match line
|
163
|
+
if matchdata
|
164
|
+
{
|
165
|
+
log_id: matchdata[:id],
|
166
|
+
controller: matchdata[:controller]
|
167
|
+
}
|
168
|
+
else
|
169
|
+
nil
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
end
|
174
|
+
|
175
|
+
end
|
176
|
+
|
@@ -0,0 +1,266 @@
|
|
1
|
+
#+TITLE: Apache Log Analysis: <%= data[:log_file] %>
|
2
|
+
#+DATE: <<%= Date.today %>>
|
3
|
+
#+STARTUP: showall
|
4
|
+
#+OPTIONS: ^:{}
|
5
|
+
#+HTML_HEAD: <link rel="stylesheet" type="text/css" href="ala-style.css" />
|
6
|
+
#+OPTIONS: html-style:nil
|
7
|
+
|
8
|
+
* Summary
|
9
|
+
|
10
|
+
| Hits | <%= "%10d" % data[:total_hits][0][0] %> |
|
11
|
+
| Unique Visitors | <%= "%10d" % data[:total_unique_visitors][0][0] %> |
|
12
|
+
| Tx | <%= "%10s" % data[:total_size][0][0] %> |
|
13
|
+
| Logged Period | <%= data[:first_day] %> -- <%= data[:last_day] %> |
|
14
|
+
| Days | <%= "%10d" % data[:total_days] %> |
|
15
|
+
| Period Requested | <%= data[:first_day_requested] %> -- <%= data[:last_day_requested] %> |
|
16
|
+
| Period Analyzed | <%= data[:first_day_in_analysis] %> -- <%= data[:last_day_in_analysis] %> |
|
17
|
+
| Days in Analysis | <%= data[:total_days_in_analysis] %> |
|
18
|
+
|
19
|
+
* Daily Distribution
|
20
|
+
|
21
|
+
<%= self.output_txt_table "daily_distribution", ["Day", "Hits", "Visits", "Size"], data[:daily_distribution] %>
|
22
|
+
|
23
|
+
#+BEGIN_SRC gnuplot :var data = daily_distribution :results output :exports <%= @export %> :file <%= @prefix %>daily<%= @suffix %>.svg
|
24
|
+
reset
|
25
|
+
set grid ytics linestyle 0
|
26
|
+
set grid xtics linestyle 0
|
27
|
+
set terminal svg size 1200,800 fname 'Arial'
|
28
|
+
|
29
|
+
set xdata time
|
30
|
+
set timefmt "%Y-%m-%d"
|
31
|
+
set format x "%a, %b %d"
|
32
|
+
set xtics rotate by 60 right
|
33
|
+
|
34
|
+
set title "Hits and Visitors"
|
35
|
+
set xlabel "Date"
|
36
|
+
set ylabel "Hits"
|
37
|
+
set y2label "Visits"
|
38
|
+
set y2tics
|
39
|
+
|
40
|
+
set style fill transparent solid 0.2 noborder
|
41
|
+
|
42
|
+
plot data using 1:2 with linespoints lw 3 lc rgb "#0000AA" pointtype 5 title "Hits" axes x1y2, \\
|
43
|
+
data using 1:2 with filledcurves below x1 linecolor rgb "#0000AA" notitle axes x1y2, \\
|
44
|
+
data using 1:3 with linespoints lw 3 lc rgb "#AA0000" pointtype 7 title "Visitors", \\
|
45
|
+
data using 1:3 with filledcurves below x1 notitle linecolor rgb "#AA0000", \\
|
46
|
+
data using 1:($3+0.1*$3):3 with labels notitle textcolor rgb "#AA0000", \\
|
47
|
+
data using 1:($2+0.1*$2):2 with labels notitle textcolor rgb "#0000AA" axes x1y2
|
48
|
+
#+END_SRC
|
49
|
+
|
50
|
+
|
51
|
+
* Time Distribution
|
52
|
+
|
53
|
+
<%= self.output_txt_table "time_distribution", ["Hour", "Hits", "Visits", "Size"], data[:time_distribution] %>
|
54
|
+
|
55
|
+
|
56
|
+
#+BEGIN_SRC gnuplot :var data = time_distribution :results output :exports <%= @export %> :file <%= @prefix %>time<%= @suffix %>.svg
|
57
|
+
reset
|
58
|
+
set terminal svg size 1200,800 fname 'Arial' fsize 10
|
59
|
+
|
60
|
+
set grid ytics linestyle 0
|
61
|
+
|
62
|
+
set title "Hits and Visitors"
|
63
|
+
set xlabel "Date"
|
64
|
+
set ylabel "Hits"
|
65
|
+
set y2label "Visitors"
|
66
|
+
set y2tics
|
67
|
+
|
68
|
+
set style fill solid 0.25
|
69
|
+
set boxwidth 0.6
|
70
|
+
|
71
|
+
set style data histograms
|
72
|
+
set style histogram clustered gap 1
|
73
|
+
|
74
|
+
plot data using 2:xtic(1) lc rgb "#0000AA" title "Hits", \\
|
75
|
+
data using 3 lc rgb "#AA0000" title "Visitors" axes x1y2, \\
|
76
|
+
data using ($0 - 0.2):($2 + 0.1*$2):2 with labels title "" textcolor rgb("#0000AA"), \\
|
77
|
+
data using ($0 + 0.2):($3 + 0.1*$3):3 with labels title "" textcolor rgb("#AA0000") axes x1y2
|
78
|
+
#+END_SRC
|
79
|
+
|
80
|
+
#+BEGIN_SRC gnuplot :var data = time_distribution :results output :exports <%= @export %> :file <%= @prefix %>time-traffic<%= @suffix %>.svg
|
81
|
+
reset
|
82
|
+
set terminal svg size 1200,800 fname 'Arial' fsize 10
|
83
|
+
|
84
|
+
set grid ytics linestyle 0
|
85
|
+
|
86
|
+
set title "Traffic"
|
87
|
+
set xlabel "Date"
|
88
|
+
set ylabel "Traffic"
|
89
|
+
|
90
|
+
set style fill solid 0.50
|
91
|
+
set boxwidth 0.6
|
92
|
+
|
93
|
+
set style data histograms
|
94
|
+
set style histogram clustered gap 1
|
95
|
+
|
96
|
+
plot data using 2:xtic(1) lc rgb "#00AA00" title "Traffic", \\
|
97
|
+
data using ($0):($2 + 0.1*$2):2 with labels title "" textcolor rgb("#00AA00")
|
98
|
+
#+END_SRC
|
99
|
+
|
100
|
+
* Most Requested Pages
|
101
|
+
|
102
|
+
<%= self.output_txt_table "most_requested_pages", ["Path", "Hits", "Visits", "Size"], data[:most_requested_pages] %>
|
103
|
+
|
104
|
+
* Most Requested URIs
|
105
|
+
|
106
|
+
<%= self.output_txt_table "most_requested_resources", ["Path", "Hits", "Visits", "Size"], data[:most_requested_resources] %>
|
107
|
+
|
108
|
+
* 404s on HTML files
|
109
|
+
|
110
|
+
<%= self.output_txt_table "pages_404", ["Path", "Hits", "Visitors"], data[:missed_pages] %>
|
111
|
+
|
112
|
+
* 404s on other resources
|
113
|
+
|
114
|
+
<%= self.output_txt_table "resources_404", ["Path", "Hits", "Visitors"], data[:missed_resources] %>
|
115
|
+
|
116
|
+
* Possible Attacks
|
117
|
+
|
118
|
+
<%= self.output_txt_table "attacks", ["Path", "Hits", "Visitors"], data[:attacks] %>
|
119
|
+
|
120
|
+
* Statuses
|
121
|
+
|
122
|
+
<%= self.output_txt_table "statuses", ["Status", "Count"], data[:statuses] %>
|
123
|
+
|
124
|
+
#+BEGIN_SRC gnuplot :var data = statuses :results output :exports <%= @export %> :file <%= @prefix %>statuses<%= @suffix %>.svg
|
125
|
+
reset
|
126
|
+
set grid ytics linestyle 0
|
127
|
+
set terminal svg size 1200,800 fname 'Arial' fsize 10
|
128
|
+
|
129
|
+
set style fill solid 0.25
|
130
|
+
set boxwidth 0.6
|
131
|
+
|
132
|
+
plot data using 2:xtic(1) with boxes lc rgb "#0000AA" title "Hits", \\
|
133
|
+
data using ($0):($2+0.1*$2):2 with labels textcolor rgb "#0000AA"
|
134
|
+
#+END_SRC
|
135
|
+
|
136
|
+
* Daily Statuses
|
137
|
+
|
138
|
+
<%= self.output_txt_table "daily_statuses", ["Status", "2xx", "3xx", "4xx"], data[:statuses_by_day] %>
|
139
|
+
|
140
|
+
#+BEGIN_SRC gnuplot :var data = daily_statuses :results output :exports <%= @export %> :file <%= @prefix %>daily-statuses<%= @suffix %>.svg
|
141
|
+
reset
|
142
|
+
set terminal svg size 1200,800 fname 'Arial' fsize 10
|
143
|
+
|
144
|
+
set grid ytics linestyle 0
|
145
|
+
|
146
|
+
set title "Daily Statuses"
|
147
|
+
set xlabel "Date"
|
148
|
+
set ylabel "Number of Hits"
|
149
|
+
set xtics rotate by 60 right
|
150
|
+
|
151
|
+
set style fill solid 0.25
|
152
|
+
set boxwidth 0.6
|
153
|
+
|
154
|
+
set style data histograms
|
155
|
+
set style histogram clustered gap 1
|
156
|
+
|
157
|
+
plot data using 2:xtic(1) lc rgb "#00AA00" title "2xx", \\
|
158
|
+
data using 3 lc rgb "#0000CC" title "3xx", \\
|
159
|
+
data using 4 lc rgb "#AA0000" title "4xx", \\
|
160
|
+
data using ($0 - 1. / 4):($2 + 0.1*$2):2 with labels title "" textcolor rgb("#00AA00"), \\
|
161
|
+
data using ($0):($3 + 0.1*$3):3 with labels title "" textcolor rgb("#0000CC"), \\
|
162
|
+
data using ($0 + 1. / 4):($4 + 0.1*$4):4 with labels title "" textcolor rgb("#AA0000")
|
163
|
+
#+END_SRC
|
164
|
+
|
165
|
+
* Browsers
|
166
|
+
|
167
|
+
<%= self.output_txt_table "browsers", ["Browser", "Hits", "Visitors", "Size"], data[:browsers] %>
|
168
|
+
|
169
|
+
#+BEGIN_SRC gnuplot :var data = browsers :results output :exports <%= @export %> :file <%= @prefix %>browser<%= @suffix %>.svg
|
170
|
+
reset
|
171
|
+
set grid ytics linestyle 0
|
172
|
+
set terminal svg size 1200,800 fname 'Arial' fsize 10
|
173
|
+
|
174
|
+
set style fill solid 0.25
|
175
|
+
set boxwidth 0.6
|
176
|
+
|
177
|
+
plot data using 2:xtic(1) with boxes lc rgb "#0000AA" title "Hits", \\
|
178
|
+
data using ($0):($2+0.1*$2):2 with labels textcolor rgb "#0000AA"
|
179
|
+
#+END_SRC
|
180
|
+
|
181
|
+
* Platforms
|
182
|
+
|
183
|
+
<%= self.output_txt_table "platforms", ["Platform", "Hits", "Visitors", "Size"], data[:platforms] %>
|
184
|
+
|
185
|
+
#+BEGIN_SRC gnuplot :var data = platforms :results output :exports <%= @export %> :file <%= @prefix %>platforms<%= @suffix %>.svg
|
186
|
+
reset
|
187
|
+
set grid ytics linestyle 0
|
188
|
+
set terminal svg size 1200,800 fname 'Arial' fsize 10
|
189
|
+
|
190
|
+
set style fill solid 0.25
|
191
|
+
set boxwidth 0.6
|
192
|
+
|
193
|
+
plot data using 2:xtic(1) with boxes lc rgb "#0000AA" title "Hits", \\
|
194
|
+
data using ($0):($2+0.1*$2):2 with labels textcolor rgb "#0000AA"
|
195
|
+
#+END_SRC
|
196
|
+
|
197
|
+
* IPs
|
198
|
+
|
199
|
+
<%= self.output_txt_table "ips", ["IPs", "Hits", "Visitors", "Size"], data[:ips] %>
|
200
|
+
|
201
|
+
|
202
|
+
* Referers
|
203
|
+
|
204
|
+
<%= self.output_txt_tabl e"referers", ["Referers", "Hits", "Visitors", "Size"], data[:referers] %>
|
205
|
+
|
206
|
+
#+BEGIN_SRC gnuplot :var data = referers :results output :exports <%= @export %> :file <%= @prefix %>referers<%= @suffix %>.svg
|
207
|
+
reset
|
208
|
+
set terminal svg size 1200,800 fname 'Arial' fsize 10
|
209
|
+
|
210
|
+
set grid ytics linestyle 0
|
211
|
+
set grid xtics linestyle 0
|
212
|
+
|
213
|
+
set title "Referers"
|
214
|
+
set xlabel "Date"
|
215
|
+
set xtics rotate by 60 right
|
216
|
+
set ylabel "Hits and Visits"
|
217
|
+
|
218
|
+
set style fill solid 0.45
|
219
|
+
set boxwidth 0.7
|
220
|
+
|
221
|
+
set style data histograms
|
222
|
+
set style histogram clustered gap 1
|
223
|
+
|
224
|
+
plot data using 2:xtic(1) lc rgb "#AA00AA" title "Hits", \\
|
225
|
+
data using 3 lc rgb "#0AAAA0" title "Visits", \\
|
226
|
+
data using ($0 - 1. / 3):($2 + 0.1*$2):2 with labels title "" textcolor rgb("#AA00AA"), \\
|
227
|
+
data using ($0 + 1. / 3):($3 + 0.1*$3):3 with labels title "" textcolor rgb("#0AAAA0")
|
228
|
+
#+END_SRC
|
229
|
+
|
230
|
+
* Command Invocation and Performance
|
231
|
+
|
232
|
+
** Command Invocation
|
233
|
+
|
234
|
+
#+BEGIN_EXAMPLE shell
|
235
|
+
<%= data[:command] %>
|
236
|
+
#+END_EXAMPLE
|
237
|
+
|
238
|
+
| Input file | <%= "%-50s" % (data[:log_file] || "stdin") %> |
|
239
|
+
| Ignore crawlers | <%= "%-50s" % options[:ignore_crawlers] %> |
|
240
|
+
| Only crawlers | <%= "%-50s" % options[:only_crawlers] %> |
|
241
|
+
| No selfpoll | <%= "%-50s" % options[:no_selfpoll] %> |
|
242
|
+
| Filter by date | <%= "%-50s" % (options[:from_date] != nil or options[:to_date] != nil) %> |
|
243
|
+
| Prefix | <%= "%-50s" % @prefix %> |
|
244
|
+
| Suffix | <%= "%-50s" % @suffix %> |
|
245
|
+
|
246
|
+
** Log Structure
|
247
|
+
|
248
|
+
| Log size | <%= "%10d" % data[:log_size][0][0] %> |
|
249
|
+
| Self poll entries | <%= "%10d" % data[:selfpolls_size][0][0] %> |
|
250
|
+
| Crawlers | <%= "%10d" % data[:crawlers_size][0][0] %> |
|
251
|
+
| Entries considered | <%= "%10d" % data[:total_hits][0][0] %> |
|
252
|
+
|
253
|
+
** Performance
|
254
|
+
|
255
|
+
| Analysis started at | <%= data[:started_at].to_s %> |
|
256
|
+
| Analysis ended at | <%= data[:ended_at].to_s %> |
|
257
|
+
| Duration (sec) | <%= "%5.3d" % data[:duration] %> |
|
258
|
+
| Duration (min) | <%= "%5.3d" % (data[:duration] / 60 ) %> |
|
259
|
+
| Log size | <%= "%9d" % data[:log_size][0][0] %> |
|
260
|
+
| Lines/sec | <%= "%6.2f" % (data[:log_size][0][0] / data[:duration]) %> |
|
261
|
+
|
262
|
+
* Local Variables :noexport:
|
263
|
+
# Local Variables:
|
264
|
+
# org-confirm-babel-evaluate: nil
|
265
|
+
# org-display-inline-images: t
|
266
|
+
# end:
|