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.
@@ -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: