log_sense 1.3.5 → 1.5.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.
@@ -7,46 +7,46 @@ module LogSense
7
7
  #
8
8
  # parse command line options
9
9
  #
10
- def self.parse options
11
- limit = 30
10
+ def self.parse(options)
11
+ limit = 900
12
12
  args = {}
13
13
 
14
14
  opt_parser = OptionParser.new do |opts|
15
- opts.banner = "Usage: log_sense [options] [logfile]"
15
+ opts.banner = 'Usage: log_sense [options] [logfile ...]'
16
16
 
17
- opts.on("-tTITLE", "--title=TITLE", String, "Title to use in the report") do |n|
17
+ opts.on('-tTITLE', '--title=TITLE', String, 'Title to use in the report') do |n|
18
18
  args[:title] = n
19
19
  end
20
20
 
21
- opts.on("-fFORMAT", "--input-format=FORMAT", String, "Input format (either rails or apache)") do |n|
21
+ opts.on('-fFORMAT', '--input-format=FORMAT', String, 'Input format (either rails or apache)') do |n|
22
22
  args[:input_format] = n
23
23
  end
24
24
 
25
- opts.on("-iINPUT_FILE", "--input-file=INPUT_FILE", String, "Input file") do |n|
26
- args[:input_file] = n
27
- end
28
-
29
- opts.on("-tFORMAT", "--output-format=FORMAT", String, "Output format: html, org, txt, sqlite. See below for available formats") do |n|
25
+ opts.on('-tFORMAT', '--output-format=FORMAT', String, 'Output format: html, org, txt, sqlite. See below for available formats') do |n|
30
26
  args[:output_format] = n
31
27
  end
32
28
 
33
- opts.on("-oOUTPUT_FILE", "--output-file=OUTPUT_FILE", String, "Output file") do |n|
29
+ opts.on('-oOUTPUT_FILE', '--output-file=OUTPUT_FILE', String, 'Output file') do |n|
34
30
  args[:output_file] = n
35
31
  end
36
32
 
37
- opts.on("-bDATE", "--begin=DATE", Date, "Consider entries after or on DATE") do |n|
33
+ opts.on('-bDATE', '--begin=DATE', Date, 'Consider entries after or on DATE') do |n|
38
34
  args[:from_date] = n
39
35
  end
40
36
 
41
- opts.on("-eDATE", "--end=DATE", Date, "Consider entries before or on DATE") do |n|
37
+ opts.on('-eDATE', '--end=DATE', Date, 'Consider entries before or on DATE') do |n|
42
38
  args[:to_date] = n
43
39
  end
44
40
 
45
- opts.on("-lN", "--limit=N", Integer, "Number of entries to show (defaults to #{limit})") do |n|
41
+ opts.on('-lN', '--limit=N', Integer, "Limit to the N most requested resources (defaults to #{limit})") do |n|
46
42
  args[:limit] = n
47
43
  end
48
44
 
49
- opts.on("-cPOLICY", "--crawlers=POLICY", String, "Decide what to do with crawlers (applies to Apache Logs)") do |n|
45
+ opts.on('-wWIDTH', '--width=WIDTH', Integer, 'Maximum width of URL and description columns in text reports') do |n|
46
+ args[:width] = n
47
+ end
48
+
49
+ opts.on('-cPOLICY', '--crawlers=POLICY', String, 'Decide what to do with crawlers (applies to Apache Logs)') do |n|
50
50
  case n
51
51
  when 'only'
52
52
  args[:only_crawlers] = true
@@ -55,30 +55,34 @@ module LogSense
55
55
  end
56
56
  end
57
57
 
58
- opts.on("-ns", "--no-selfpoll", "Ignore self poll entries (requests from ::1; applies to Apache Logs)") do
58
+ opts.on('-ns', '--no-selfpoll', 'Ignore self poll entries (requests from ::1; applies to Apache Logs)') do
59
59
  args[:no_selfpoll] = true
60
60
  end
61
61
 
62
- opts.on("-v", "--version", "Prints version information") do
62
+ opts.on('--verbose', 'Inform about progress (prints to STDERR)') do
63
+ args[:verbose] = true
64
+ end
65
+
66
+ opts.on('-v', '--version', 'Prints version information') do
63
67
  puts "log_sense version #{LogSense::VERSION}"
64
- puts "Copyright (C) 2021 Shair.Tech"
65
- puts "Distributed under the terms of the MIT license"
68
+ puts 'Copyright (C) 2021 Shair.Tech'
69
+ puts 'Distributed under the terms of the MIT license'
66
70
  exit
67
71
  end
68
72
 
69
- opts.on("-h", "--help", "Prints this help") do
73
+ opts.on('-h', '--help', 'Prints this help') do
70
74
  puts opts
71
- puts ""
75
+ puts ''
72
76
  puts "This is version #{LogSense::VERSION}"
73
77
 
74
- puts ""
75
- puts "Output formats"
76
- pathname = File.join(File.dirname(__FILE__), "templates", "*")
77
- templates = Dir.glob(pathname).select { |x| ! File.basename(x).start_with? /_|#/ and ! File.basename(x).end_with? "~" }
78
- components = templates.map { |x| File.basename(x).split "." }.group_by { |x| x[0] }
78
+ puts ''
79
+ puts 'Output formats'
80
+ pathname = File.join(File.dirname(__FILE__), 'templates', '*')
81
+ templates = Dir.glob(pathname).select { |x| !File.basename(x).start_with?(/_|#/) && !File.basename(x).end_with?('~') }
82
+ components = templates.map { |x| File.basename(x).split '.' }.group_by { |x| x[0] }
79
83
  components.each do |k, vs|
80
84
  puts "#{k} parsing can produce the following outputs:"
81
- puts " - sqlite"
85
+ puts ' - sqlite'
82
86
  vs.each do |v|
83
87
  puts " - #{v[1]}"
84
88
  end
@@ -91,13 +95,14 @@ module LogSense
91
95
  opt_parser.parse!(options)
92
96
 
93
97
  args[:limit] ||= limit
94
- args[:input_format] ||= "apache"
95
- args[:output_format] ||= "html"
98
+ args[:input_format] ||= 'apache'
99
+ args[:output_format] ||= 'html'
96
100
  args[:ignore_crawlers] ||= false
97
101
  args[:only_crawlers] ||= false
98
102
  args[:no_selfpoll] ||= false
103
+ args[:verbose] ||= false
99
104
 
100
- return args
105
+ args
101
106
  end
102
107
  end
103
108
  end
@@ -7,7 +7,7 @@ module LogSense
7
7
  # @ variables are automatically put in the returned data
8
8
  #
9
9
 
10
- def self.crunch db, options = { limit: 30 }
10
+ def self.crunch db, options = { limit: 900 }
11
11
  first_day_s = db.execute "SELECT started_at from Event where started_at not NULL order by started_at limit 1"
12
12
  # we could use ended_at to cover the full activity period, but I prefer started_at
13
13
  # with the meaning that the monitor event initiation
@@ -19,9 +19,10 @@ module LogSense
19
19
  @last_day = last_day_s&.first&.first ? Date.parse(last_day_s[0][0]) : nil
20
20
 
21
21
  @total_days = 0
22
- if @first_day and @last_day
23
- @total_days = (@last_day - @first_day).to_i
24
- end
22
+ @total_days = (@last_day - @first_day).to_i if @first_day && @last_day
23
+
24
+ # TODO should also look into Error
25
+ @source_files = db.execute "SELECT distinct(source_file) from Event"
25
26
 
26
27
  @log_size = db.execute "SELECT count(started_at) from Event"
27
28
  @log_size = @log_size[0][0]
@@ -103,6 +104,9 @@ module LogSense
103
104
 
104
105
  @ips = db.execute "SELECT ip, count(ip) from Event where #{filter} group by ip order by count(ip) desc limit #{options[:limit]}"
105
106
 
107
+ @streaks = db.execute 'SELECT ip, substr(started_at, 1, 10), url from Event order by ip, started_at'
108
+ data = {}
109
+
106
110
  @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"
107
111
 
108
112
  @fatal = db.execute ("SELECT strftime(\"%Y-%m-%d %H:%M\", started_at), ip, url, error.description, event.log_id FROM Event JOIN Error ON event.log_id == error.log_id WHERE exit_status == 'F'") || [[]]
@@ -2,10 +2,8 @@ require 'sqlite3'
2
2
 
3
3
  module LogSense
4
4
  module RailsLogParser
5
- def self.parse filename, options = {}
6
- content = filename ? File.readlines(filename) : ARGF.readlines
7
-
8
- db = SQLite3::Database.new ":memory:"
5
+ def self.parse(streams, options = {})
6
+ db = SQLite3::Database.new ':memory:'
9
7
  db.execute 'CREATE TABLE IF NOT EXISTS Event(
10
8
  id INTEGER PRIMARY KEY AUTOINCREMENT,
11
9
  exit_status TEXT,
@@ -17,12 +15,14 @@ module LogSense
17
15
  url TEXT,
18
16
  controller TEXT,
19
17
  html_verb TEXT,
20
- status INTEGER,
18
+ status INTEGER,
21
19
  duration_total_ms FLOAT,
22
20
  duration_views_ms FLOAT,
23
21
  duration_ar_ms FLOAT,
24
22
  allocations INTEGER,
25
- comment TEXT
23
+ comment TEXT,
24
+ source_file TEXT,
25
+ line_number INTEGER
26
26
  )'
27
27
 
28
28
  ins = db.prepare("insert into Event(
@@ -35,30 +35,34 @@ module LogSense
35
35
  url,
36
36
  controller,
37
37
  html_verb,
38
- status,
38
+ status,
39
39
  duration_total_ms,
40
40
  duration_views_ms,
41
41
  duration_ar_ms,
42
42
  allocations,
43
- comment
43
+ comment,
44
+ source_file,
45
+ line_number
44
46
  )
45
- values (#{Array.new(15, '?').join(', ')})")
47
+ values (#{Array.new(17, '?').join(', ')})")
46
48
 
47
-
48
49
  db.execute 'CREATE TABLE IF NOT EXISTS Error(
49
50
  id INTEGER PRIMARY KEY AUTOINCREMENT,
50
51
  log_id TEXT,
51
52
  context TEXT,
52
- description TEXT
53
+ description TEXT,
54
+ filename TEXT,
55
+ line_number INTEGER
53
56
  )'
54
57
 
55
58
  ins_error = db.prepare("insert into Error(
56
59
  log_id,
57
60
  context,
58
- description
61
+ description,
62
+ filename,
63
+ line_number
59
64
  )
60
- values (?, ?, ?)")
61
-
65
+ values (?, ?, ?, ?, ?)")
62
66
 
63
67
  # requests in the log might be interleaved.
64
68
  #
@@ -79,94 +83,101 @@ module LogSense
79
83
  # and they appears in the order shown above: started, processing, ...
80
84
  #
81
85
  # Different requests might be interleaved, of course
82
-
83
- File.readlines(filename).each do |line|
84
- # I and F for completed requests, [ is for error messages
85
- next if line[0] != 'I' and line[0] != 'F' and line[0] != '['
86
-
87
- data = self.match_and_process_error line
88
- if data
89
- ins_error.execute(data[:log_id], data[:context], data[:description])
90
- next
91
- end
92
-
93
- data = self.match_and_process_start line
94
- if data
95
- id = data[:log_id]
96
- pending[id] = data.merge (pending[id] || {})
97
- next
98
- end
99
-
100
- data = self.match_and_process_processing_by line
101
- if data
102
- id = data[:log_id]
103
- pending[id] = data.merge (pending[id] || {})
104
- next
105
- end
106
-
107
- data = self.match_and_process_fatal line
108
- if data
109
- id = data[:log_id]
110
- # it might as well be that the first event started before
111
- # the log. With this, we make sure we add only events whose
112
- # start was logged and parsed
113
- if pending[id]
114
- event = data.merge (pending[id] || {})
86
+ #
87
+ streams.each do |stream|
88
+ stream.readlines.each_with_index do |line, line_number|
89
+ filename = stream == $stdin ? "stdin" : stream.path
115
90
 
116
- ins.execute(
117
- event[:exit_status],
118
- event[:started_at],
119
- event[:ended_at],
120
- event[:log_id],
121
- event[:ip],
122
- unique_visitor_id(event),
123
- event[:url],
124
- event[:controller],
125
- event[:html_verb],
126
- event[:status],
127
- event[:duration_total_ms],
128
- event[:duration_views_ms],
129
- event[:duration_ar_ms],
130
- event[:allocations],
131
- event[:comment]
132
- )
91
+ # I and F for completed requests, [ is for error messages
92
+ next if line[0] != 'I' and line[0] != 'F' and line[0] != '['
133
93
 
134
- pending.delete(id)
94
+ data = match_and_process_error line
95
+ if data
96
+ ins_error.execute(data[:log_id], data[:context], data[:description], filename, line_number)
97
+ next
98
+ end
99
+
100
+ data = match_and_process_start line
101
+ if data
102
+ id = data[:log_id]
103
+ pending[id] = data.merge(pending[id] || {})
104
+ next
135
105
  end
136
- end
137
-
138
- data = self.match_and_process_completed line
139
- if data
140
- id = data[:log_id]
141
106
 
142
- # it might as well be that the first event started before
143
- # the log. With this, we make sure we add only events whose
144
- # start was logged and parsed
145
- if pending[id]
146
- event = data.merge (pending[id] || {})
107
+ data = match_and_process_processing_by line
108
+ if data
109
+ id = data[:log_id]
110
+ pending[id] = data.merge(pending[id] || {})
111
+ next
112
+ end
147
113
 
148
- ins.execute(
149
- event[:exit_status],
150
- event[:started_at],
151
- event[:ended_at],
152
- event[:log_id],
153
- event[:ip],
154
- unique_visitor_id(event),
155
- event[:url],
156
- event[:controller],
157
- event[:html_verb],
158
- event[:status],
159
- event[:duration_total_ms],
160
- event[:duration_views_ms],
161
- event[:duration_ar_ms],
162
- event[:allocations],
163
- event[:comment]
164
- )
114
+ data = match_and_process_fatal line
115
+ if data
116
+ id = data[:log_id]
117
+ # it might as well be that the first event started before
118
+ # the log. With this, we make sure we add only events whose
119
+ # start was logged and parsed
120
+ if pending[id]
121
+ event = data.merge(pending[id] || {})
122
+
123
+ ins.execute(
124
+ event[:exit_status],
125
+ event[:started_at],
126
+ event[:ended_at],
127
+ event[:log_id],
128
+ event[:ip],
129
+ unique_visitor_id(event),
130
+ event[:url],
131
+ event[:controller],
132
+ event[:html_verb],
133
+ event[:status],
134
+ event[:duration_total_ms],
135
+ event[:duration_views_ms],
136
+ event[:duration_ar_ms],
137
+ event[:allocations],
138
+ event[:comment],
139
+ filename,
140
+ line_number
141
+ )
142
+
143
+ pending.delete(id)
144
+ end
145
+ end
165
146
 
166
- pending.delete(id)
147
+ data = self.match_and_process_completed line
148
+ if data
149
+ id = data[:log_id]
150
+
151
+ # it might as well be that the first event started before
152
+ # the log. With this, we make sure we add only events whose
153
+ # start was logged and parsed
154
+ if pending[id]
155
+ event = data.merge (pending[id] || {})
156
+
157
+ ins.execute(
158
+ event[:exit_status],
159
+ event[:started_at],
160
+ event[:ended_at],
161
+ event[:log_id],
162
+ event[:ip],
163
+ unique_visitor_id(event),
164
+ event[:url],
165
+ event[:controller],
166
+ event[:html_verb],
167
+ event[:status],
168
+ event[:duration_total_ms],
169
+ event[:duration_views_ms],
170
+ event[:duration_ar_ms],
171
+ event[:allocations],
172
+ event[:comment],
173
+ filename,
174
+ line_number
175
+ )
176
+
177
+ pending.delete(id)
178
+ end
167
179
  end
168
180
  end
169
-
170
181
  end
171
182
 
172
183
  db
@@ -226,7 +237,7 @@ module LogSense
226
237
  # I, [2021-12-06T14:28:19.736545 #2804090] INFO -- : [34091cb5-3e7b-4042-aaf8-6c6510d3f14c] Completed 500 Internal Server Error in 66ms (ActiveRecord: 8.0ms | Allocations: 24885)
227
238
  COMPLETED_REGEXP = /I, \[#{TIMESTAMP} #[0-9]+\] INFO -- : \[#{ID}\] Completed #{STATUS} #{STATUS_IN_WORDS} in (?<total>#{MSECS})ms \((Views: (?<views>#{MSECS})ms \| )?ActiveRecord: (?<arec>#{MSECS})ms( \| Allocations: (?<alloc>[0-9]+))?\)/
228
239
 
229
- def self.match_and_process_completed line
240
+ def self.match_and_process_completed(line)
230
241
  matchdata = (COMPLETED_REGEXP.match line)
231
242
  # exit_status = matchdata[:status].to_i == 500 ? "E" : "I"
232
243
  if matchdata
@@ -267,7 +278,7 @@ module LogSense
267
278
  # F, [2021-12-04T00:34:05.839269 #2735058] FATAL -- : [3a16162e-a6a5-435e-a9d8-c4df5dc0f728] actionpack (5.2.4.4) lib/action_dispatch/middleware/debug_exceptions.rb:65:in `call'
268
279
  FATAL_REGEXP = /F, \[#{TIMESTAMP} #[0-9]+\] FATAL -- : \[#{ID}\] (?<comment>.*)$/
269
280
 
270
- def self.match_and_process_fatal line
281
+ def self.match_and_process_fatal(line)
271
282
  matchdata = FATAL_REGEXP.match line
272
283
  if matchdata
273
284
  {
@@ -281,11 +292,8 @@ module LogSense
281
292
  end
282
293
 
283
294
  # generate a unique visitor id from an event
284
- def self.unique_visitor_id event
295
+ def self.unique_visitor_id(event)
285
296
  "#{DateTime.parse(event[:started_at] || event[:ended_at] || "1970-01-01").strftime("%Y-%m-%d")} #{event[:ip]}"
286
297
  end
287
-
288
298
  end
289
-
290
299
  end
291
-
@@ -4,10 +4,6 @@
4
4
  <th>CLI Command</th>
5
5
  <td><code><%= data[:command] %></code></td>
6
6
  </tr>
7
- <tr>
8
- <th>Input file</th>
9
- <td><code><%= (data[:log_file] || "stdin") %></code></td>
10
- </tr>
11
7
  <tr>
12
8
  <th>Ignore crawlers</th>
13
9
  <td><code><%= options[:ignore_crawlers] %></code></td></tr>
@@ -1,6 +1,7 @@
1
1
  <%=
2
- table = Terminal::Table.new rows: [ ["Command", data[:command] ],
3
- ["Input file", data[:log_file] || "stdin" ]
4
- ]
2
+ table = Terminal::Table.new rows: [
3
+ ["Command", data[:command] ],
4
+ ]
5
+ table.style = { border_i: "|" }
5
6
  table
6
7
  %>
@@ -0,0 +1,21 @@
1
+ <nav>
2
+ <h2>Navigation</h2>
3
+ <ul class="no-bullet">
4
+ <% (["Summary", "Log Structure"] +
5
+ menus +
6
+ ["Command Invocation", "Performance"]).each do |item| %>
7
+ <li class="nav-item">
8
+ <a href="#<%= Emitter::slugify item %>" data-close>
9
+ <%= item %>
10
+ </a>
11
+ </li>
12
+ <% end %>
13
+ </ul>
14
+
15
+ <p>
16
+ Generated by
17
+ <a href="https://github.com/avillafiorita/log_sense">LogSense</a> <br />
18
+ on <%= DateTime.now.strftime("%Y-%m-%d %H:%M") %>.<br />
19
+ <a href='https://db-ip.com'>IP Geolocation by DB-IP</a>
20
+ </p>
21
+ </nav>
@@ -1,9 +1,3 @@
1
- <%
2
- def slugify string
3
- string.downcase.gsub(/ +/, '-')
4
- end
5
- %>
6
-
7
1
  <table id="table-<%= index %>" class="table unstriped">
8
2
  <thead>
9
3
  <tr>
@@ -19,9 +13,10 @@ end
19
13
  $(document).ready(function(){
20
14
  $('#table-<%= index %>').dataTable({
21
15
  data: data_<%= index %>,
16
+ <%= report[:datatable_options] + "," if report[:datatable_options] %>
22
17
  columns: [
23
18
  <% report[:header].each do |header| %>
24
- { data: '<%= header %>', className: '<%= slugify(header) %>' },
19
+ { data: '<%= header %>', className: '<%= Emitter::slugify(header) %>' },
25
20
  <% end %>
26
21
  ]
27
22
  });
@@ -0,0 +1,14 @@
1
+ <%=
2
+ # shortens long URLs and long descriptions
3
+ shortened = Emitter::shorten(report[:rows], report[:header], data[:width])
4
+
5
+ # build and style the table
6
+ table = Terminal::Table.new headings: report[:header], rows: shortened
7
+ table.style = { border_i: "|" }
8
+ columns = report[:header].size - 1
9
+ (0..columns).map do |i|
10
+ table.align_column(i, report[:column_alignment][i] || :left)
11
+ end
12
+ # return it
13
+ table
14
+ %>
@@ -15,7 +15,7 @@
15
15
  <%= data[:log_size] %> <span class="stats-list-label">Events</span>
16
16
  </li>
17
17
  <li class="stats-list-positive">
18
- <td><%= "%.2f" % (data[:log_size] / data[:duration]) %>
18
+ <%= "%.2f" % (data[:log_size] / data[:duration]) %>
19
19
  <span class="stats-list-label">Parsed Events/sec</span>
20
20
  </li>
21
21
  </ul>
@@ -1,9 +1,12 @@
1
1
  <%=
2
- table = Terminal::Table.new rows: [ ["Analysis started at", data[:started_at].to_s ],
3
- ["Analysis ended at", data[:ended_at].to_s ],
4
- ["Duration", "%02d:%02d" % [data[:duration] / 60, data[:duration] % 60] ],
5
- ["Events", "%9d" % data[:log_size] ],
6
- ["Parsed events/sec", "%.2f" % (data[:log_size] / data[:duration]) ] ]
2
+ table = Terminal::Table.new rows: [
3
+ ["Analysis started at", data[:started_at].to_s ],
4
+ ["Analysis ended at", data[:ended_at].to_s ],
5
+ ["Duration", "%02d:%02d" % [data[:duration] / 60, data[:duration] % 60] ],
6
+ ["Events", "%9d" % data[:log_size] ],
7
+ ["Parsed events/sec", "%.2f" % (data[:log_size] / data[:duration]) ]
8
+ ]
9
+ table.style = { border_i: "|" }
7
10
  table.align_column(2, :right)
8
11
  table
9
12
  %>
@@ -1,10 +1,10 @@
1
1
  <script>
2
- /* this is used both by Vega and DataTable */
2
+ /* this is used both by Vega and DataTable for <%= report[:title] %>*/
3
3
  data_<%= index %> = [
4
4
  <% report[:rows].each do |row| %>
5
5
  {
6
6
  <% report[:header].each_with_index do |h, i| %>
7
- "<%= h %>": <%= (row[i].class == Integer or row[i].class == Float) ? row[i] : "\"#{Emitter::escape_javascript(row[i] || '')}\"" %>,
7
+ "<%= h %>": "<%= Emitter::process row[i] %>",
8
8
  <% end %>
9
9
  },
10
10
  <% end %>
@@ -17,7 +17,12 @@
17
17
  <%= data[:total_unique_visits] %> <span class="stats-list-label">Unique Visits</span>
18
18
  </li>
19
19
  <li class="stats-list-negative">
20
- <%= data[:total_unique_visits] != 0 ? data[:total_hits] / data[:total_unique_visits] : "N/A" %>
20
+ <% days = data[:last_day_in_analysis] - data[:first_day_in_analysis] %>
21
+ <%= days > 0 ? "%d" % (data[:total_unique_visits] / days) : "N/A" %>
21
22
  <span class="stats-list-label">Unique Visits / Day</span>
22
23
  </li>
24
+ <li class="stats-list-negative">
25
+ <%= data[:total_unique_visits] != 0 ? data[:total_hits] / data[:total_unique_visits] : "N/A" %>
26
+ <span class="stats-list-label">Hits / Unique Visitor</span>
27
+ </li>
23
28
  </ul>
@@ -1,10 +1,13 @@
1
1
  <%=
2
- table = Terminal::Table.new rows: [ ["Input File", data[:log_file] || "stdin" ],
3
- ["Period Analyzed", "#{data[:first_day_in_analysis]} -- #{data[:last_day_in_analysis]}" ],
4
- ["Days", data[:total_days_in_analysis] ],
5
- ["Events", data[:events] ],
6
- ["Unique Visits", data[:total_unique_visits] ],
7
- ["Avg. Events per Visit", data[:total_unique_visits] != 0 ? data[:events] / data[:total_unique_visits] : "N/A" ]
8
- ]
9
- table
2
+ table = Terminal::Table.new rows: [
3
+ ["From", data[:first_day_in_analysis]],
4
+ ["To", data[:last_day_in_analysis]],
5
+ ["Days", data[:total_days_in_analysis]],
6
+ ["Hits", data[:total_hits]],
7
+ ["Unique Visits", data[:total_unique_visits]],
8
+ ["Unique Visits / Day", data[:total_days_in_analysis] > 0 ? "%d" % (data[:total_unique_visits] / data[:total_days_in_analysis]) : "N/A"],
9
+ ["Hits/Unique Visitor", data[:total_unique_visits] != 0 ? data[:total_hits] / data[:total_unique_visits] : "N/A"]
10
+ ]
11
+ table.style = { border_i: "|" }
12
+ table
10
13
  %>
@@ -0,0 +1 @@
1
+ >>>> URLs IN THIS FILE ARE NOT SANITIZED AND MIGHT BE UNSAFE TO OPEN <<<<