log_sense 1.4.1 → 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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6ef1d35932ba8c7fe6e636f6cb507ea99fd6b6026170d62207dfa2606d62bbcb
4
- data.tar.gz: 725dc6e51ace6c9cbd366e3888387ef8f246327c9e1036c66d2eb6351b1f0791
3
+ metadata.gz: '0128717b2ba709bc5dfbb7b755762a757c575ad4307159910cc894aaa3b88f42'
4
+ data.tar.gz: e05054b8eee79a439f5b077e60bc0d95a3e7706c550853333d7d631c458abf91
5
5
  SHA512:
6
- metadata.gz: 592e80cd56f4740cc4003b494c9da960de228025377d630d070a08a495cffac7fd41850d469189510def48f9940700251bd84a141def9b1b15b32fbed967261f
7
- data.tar.gz: 1d802e95fd58c66c4904843478c5ccc8ffc1ce43e87dd199e5b21efc89bcff436b227c646d2e5a386bee22a3d1c67c0341c25fcb4058e48526560d05984f0148
6
+ metadata.gz: 9d9e3dc495f7479292ae96d1bf6298f531258cae74df47ff705aea9613880b0d50aa6a19328f70685484ad1c606bd12a8fb7c87632fe5c9cbefefe4893d9bb4d
7
+ data.tar.gz: b417049bcc119ed82ab4c33d007e15804ba85b2485308ca28448fba512814fc5e8b8b215310bfb5c8108fcdc87292322eefc6826b18718fcbc7fced29eea77cb
data/CHANGELOG.org CHANGED
@@ -2,6 +2,21 @@
2
2
  #+AUTHOR: Adolfo Villafiorita
3
3
  #+STARTUP: showall
4
4
 
5
+ * 1.5.0
6
+
7
+ - [User] Present Unique Visits / day as integer
8
+ - [User] Added Country and Streaks report for rails
9
+ - [User] Changed Streak report in Apache
10
+
11
+ - [Gem] Updated DBIP
12
+ - [Gem] Updated Bundle
13
+
14
+ - [Code] Refactored all reports, so that they are specified
15
+ in the same way
16
+ - [Code] Refactor warning message in textual reports
17
+ - [Code] Build HTML menu for report specification
18
+ - [Code] Various refactoring passes on the code
19
+
5
20
  * 1.4.1
6
21
 
7
22
  - [User] New textual report for Apache
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- log_sense (1.3.1)
4
+ log_sense (1.4.2)
5
5
  browser
6
6
  ipaddr
7
7
  iso_country_codes
@@ -13,9 +13,9 @@ GEM
13
13
  specs:
14
14
  browser (5.3.1)
15
15
  byebug (11.1.3)
16
- ipaddr (1.2.3)
16
+ ipaddr (1.2.4)
17
17
  iso_country_codes (0.7.8)
18
- minitest (5.14.4)
18
+ minitest (5.15.0)
19
19
  rake (12.3.3)
20
20
  sqlite3 (1.4.2)
21
21
  terminal-table (3.0.2)
@@ -32,4 +32,4 @@ DEPENDENCIES
32
32
  rake (~> 12.0)
33
33
 
34
34
  BUNDLED WITH
35
- 2.2.32
35
+ 2.3.3
data/README.org CHANGED
@@ -19,8 +19,6 @@ LogSense reports the following data:
19
19
  - OS, browsers, and devices
20
20
  - IP Country location, thanks to the DPIP lite country DB
21
21
  - Streaks: resources accessed by a given IP over time
22
- - Potential attacks: access to resources which are not meant to be
23
- served by a web server serving static websites
24
22
  - Performance of Rails requests
25
23
 
26
24
  Filters from the command line allow to analyze specific periods and
@@ -33,6 +31,18 @@ And, of course, the compulsory screenshot:
33
31
  #+ATTR_HTML: :width 80%
34
32
  [[file:./apache-screenshot.png]]
35
33
 
34
+
35
+ * An important word of warning
36
+
37
+ [[https://owasp.org/www-community/attacks/Log_Injection][Log poisoning]] is a technique whereby attackers send requests with invalidated
38
+ user input to forge log entries or inject malicious content into the logs.
39
+
40
+ log_sense sanitizes entries of HTML reports, to try and protect from log
41
+ poisoning. *Log entries and URLs in SQLite3, however, are not sanitized*:
42
+ they are stored and read from the log. This is not, in general, an issue,
43
+ unless you use the data from SQLite in environments in which URLs can be
44
+ opened or code executed.
45
+
36
46
  * Motivation
37
47
 
38
48
  LogSense moves along the lines of tools such as [[https://goaccess.io/][GoAccess]] (which
@@ -54,6 +64,7 @@ generated files are then made available on a private area on the web.
54
64
  gem install log_sense
55
65
  #+end_src
56
66
 
67
+
57
68
  * Usage
58
69
 
59
70
  #+begin_src bash :results raw output :wrap example
@@ -73,10 +84,11 @@ generated files are then made available on a private area on the web.
73
84
  -w, --width=WIDTH Maximum width of URL and description columns in text reports
74
85
  -c, --crawlers=POLICY Decide what to do with crawlers (applies to Apache Logs)
75
86
  -n, --no-selfpolls Ignore self poll entries (requests from ::1; applies to Apache Logs)
87
+ --verbose Inform about progress (prints to STDERR)
76
88
  -v, --version Prints version information
77
89
  -h, --help Prints this help
78
90
 
79
- This is version 1.4.1
91
+ This is version 1.5.0
80
92
 
81
93
  Output formats
82
94
  rails parsing can produce the following outputs:
@@ -96,6 +108,7 @@ log_sense -f apache -i access.log -t txt > access-data.txt
96
108
  log_sense -f rails -i production.log -t html -o performance.txt
97
109
  #+end_example
98
110
 
111
+
99
112
  * Change Log
100
113
 
101
114
  See the [[file:CHANGELOG.org][CHANGELOG]] file.
@@ -110,8 +123,8 @@ Concerning the outputs:
110
123
  - HTML reports use [[https://get.foundation/][Zurb Foundation]], [[https://www.datatables.net/][Data Tables]], and [[https://vega.github.io/vega-lite/][Vega Light]], which
111
124
  are all downloaded from a CDN
112
125
  - The textual format is compatible with [[https://orgmode.org/][Org Mode]] and can be further
113
- processed to any format [[https://orgmode.org/][Org Mode]] can be exported to (including HTML
114
- and PDF)
126
+ processed to any format [[https://orgmode.org/][Org Mode]] can be exported to, including HTML
127
+ and PDF, with the word of warning in the section above.
115
128
 
116
129
  * Author and Contributors
117
130
 
@@ -119,8 +132,8 @@ Concerning the outputs:
119
132
 
120
133
  * Known Bugs
121
134
 
122
- No known bugs; an unknown number of unknown bugs.
123
- (See the open issues for the known bugs.)
135
+ No known bugs; an unknown number of unknown bugs. (See the open issues for
136
+ the known bugs.)
124
137
 
125
138
  * License
126
139
 
data/Rakefile CHANGED
@@ -9,7 +9,21 @@ end
9
9
  require_relative './lib/log_sense/ip_locator.rb'
10
10
 
11
11
  desc "Convert Geolocation DB to sqlite"
12
- task :dbip_to_sqlite3, [:filename] do |tasks, args|
13
- filename = args[:filename]
14
- ApacheLogReport::IpLocator::dbip_to_sqlite filename
12
+ task :dbip_to_sqlite3, [:year_month] do |tasks, args|
13
+ filename = "./ip_locations/dbip-country-lite-#{args[:year_month]}.csv"
14
+
15
+ if !File.exist? filename
16
+ puts "Error. Could not find: #{filename}"
17
+ puts
18
+ puts 'I see the following files:'
19
+ puts Dir.glob("ip_locations/dbip-country-lite*").map { |x| "- #{x}\n" }
20
+ puts ''
21
+ puts '1. Download (if necessary) a more recent version from: https://db-ip.com/db/download/ip-to-country-lite'
22
+ puts '2. Save downloaded file to ip_locations/'
23
+ puts '3. Relaunch with YYYY-MM'
24
+
25
+ exit
26
+ else
27
+ LogSense::IpLocator::dbip_to_sqlite filename
28
+ end
15
29
  end
data/exe/log_sense CHANGED
@@ -12,7 +12,7 @@ require 'log_sense.rb'
12
12
  @output_file = @options[:output_file]
13
13
 
14
14
  if ARGV.map { |x| File.exist?(x) }.include?(false)
15
- warn.puts "Error: input file(s) '#{ARGV.reject { |x| File.exist(x) }.join(', ')}' do not exist"
15
+ $stderr.puts "Error: input file(s) '#{ARGV.reject { |x| File.exist(x) }.join(', ')}' do not exist"
16
16
  exit 1
17
17
  end
18
18
  @input_files = ARGV.empty? ? [$stdin] : ARGV.map { |x| File.open(x, 'r') }
@@ -31,35 +31,45 @@ when 'rails'
31
31
  parser_klass = LogSense::RailsLogParser
32
32
  cruncher_klass = LogSense::RailsDataCruncher
33
33
  else
34
- warn.puts "Error: input format #{@options[:input_format]} not understood."
34
+ $stderr.puts "Error: input format #{@options[:input_format]} not understood."
35
35
  exit 1
36
36
  end
37
37
 
38
+ $stderr.puts "Parsing input files..." if @options[:verbose]
38
39
  @db = parser_klass.parse @input_files
39
40
 
40
41
  if @options[:output_format] == 'sqlite'
42
+ $stderr.puts "Saving to SQLite3..." if @options[:verbose]
41
43
  ddb = SQLite3::Database.new(@output_file || 'db.sqlite3')
42
44
  b = SQLite3::Backup.new(ddb, 'main', @db, 'main')
43
45
  b.step(-1) #=> DONE
44
46
  b.finish
45
47
  else
48
+ $stderr.puts "Aggregating data..." if @options[:verbose]
46
49
  @data = cruncher_klass.crunch @db, @options
50
+
51
+ $stderr.puts "Geolocating..." if @options[:verbose]
47
52
  @data = LogSense::IpLocator.geolocate @data
48
53
 
54
+ $stderr.puts "Grouping by country..." if @options[:verbose]
55
+ country_col = @data[:ips][0].size - 1
56
+ @data[:countries] = @data[:ips].group_by { |x| x[country_col] }
57
+
49
58
  @ended_at = Time.now
50
59
  @duration = @ended_at - @started_at
51
60
 
52
61
  @data = @data.merge({
53
62
  command: @command_line,
63
+ filenames: ARGV,
54
64
  log_files: @input_files,
55
65
  started_at: @started_at,
56
66
  ended_at: @ended_at,
57
67
  duration: @duration,
58
68
  width: @options[:width]
59
69
  })
60
-
61
70
  #
62
71
  # Emit Output
63
72
  #
73
+ $stderr.puts "Emitting..." if @options[:verbose]
64
74
  puts LogSense::Emitter.emit @data, @options
65
75
  end
Binary file
@@ -17,15 +17,15 @@ module LogSense
17
17
  @total_days = 0
18
18
  @total_days = (@last_day - @first_day).to_i if @first_day && @last_day
19
19
 
20
- @source_files = db.execute "SELECT distinct(source_file) from LogLine"
20
+ @source_files = db.execute 'SELECT distinct(source_file) from LogLine'
21
21
 
22
- @log_size = db.execute "SELECT count(datetime) from LogLine"
22
+ @log_size = db.execute 'SELECT count(datetime) from LogLine'
23
23
  @log_size = @log_size[0][0]
24
24
 
25
25
  @selfpolls_size = db.execute "SELECT count(datetime) from LogLine where ip == '::1'"
26
26
  @selfpolls_size = @selfpolls_size[0][0]
27
27
 
28
- @crawlers_size = db.execute "SELECT count(datetime) from LogLine where bot == 1"
28
+ @crawlers_size = db.execute 'SELECT count(datetime) from LogLine where bot == 1'
29
29
  @crawlers_size = @crawlers_size[0][0]
30
30
 
31
31
  @first_day_requested = options[:from_date]
@@ -35,7 +35,7 @@ module LogSense
35
35
  @last_day_in_analysis = date_intersect options[:to_date], @last_day, :min
36
36
 
37
37
  @total_days_in_analysis = 0
38
- if @first_day_in_analysis and @last_day_in_analysis
38
+ if @first_day_in_analysis && @last_day_in_analysis
39
39
  @total_days_in_analysis = (@last_day_in_analysis - @first_day_in_analysis).to_i
40
40
  end
41
41
 
@@ -45,24 +45,24 @@ module LogSense
45
45
  filter = [
46
46
  (options[:from_date] ? "date(datetime) >= '#{options[:from_date]}'" : nil),
47
47
  (options[:to_date] ? "date(datetime) <= '#{options[:to_date]}'" : nil),
48
- (options[:only_crawlers] ? "bot == 1" : nil),
49
- (options[:ignore_crawlers] ? "bot == 0" : nil),
48
+ (options[:only_crawlers] ? 'bot == 1' : nil),
49
+ (options[:ignore_crawlers] ? 'bot == 0' : nil),
50
50
  (options[:no_selfpolls] ? "ip != '::1'" : nil),
51
- "true"
51
+ 'true'
52
52
  ].compact.join " and "
53
53
 
54
54
  mega = 1024 * 1024
55
55
  giga = mega * 1024
56
56
  tera = giga * 1024
57
-
57
+
58
58
  # in alternative to sum(size)
59
59
  human_readable_size = <<-EOS
60
- CASE
60
+ CASE
61
61
  WHEN sum(size) < 1024 THEN sum(size) || ' B'
62
62
  WHEN sum(size) >= 1024 AND sum(size) < (#{mega}) THEN ROUND((CAST(sum(size) AS REAL) / 1024), 2) || ' KB'
63
63
  WHEN sum(size) >= (#{mega}) AND sum(size) < (#{giga}) THEN ROUND((CAST(sum(size) AS REAL) / (#{mega})), 2) || ' MB'
64
64
  WHEN sum(size) >= (#{giga}) AND sum(size) < (#{tera}) THEN ROUND((CAST(sum(size) AS REAL) / (#{giga})), 2) || ' GB'
65
- WHEN sum(size) >= (#{tera}) THEN ROUND((CAST(sum(size) AS REAL) / (#{tera})), 2) || ' TB'
65
+ WHEN sum(size) >= (#{tera}) THEN ROUND((CAST(sum(size) AS REAL) / (#{tera})), 2) || ' TB'
66
66
  END AS size
67
67
  EOS
68
68
 
@@ -117,20 +117,19 @@ module LogSense
117
117
 
118
118
  @ips = db.execute "SELECT ip, count(ip), count(distinct(unique_visitor)), #{human_readable_size} from LogLine where #{filter} group by ip order by count(ip) desc limit #{options[:limit]}"
119
119
 
120
- @streaks = db.execute "SELECT ip, substr(datetime, 1, 10), path from LogLine order by ip, datetime"
120
+ @streaks = db.execute 'SELECT ip, substr(datetime, 1, 10), path from LogLine order by ip, datetime'
121
121
  data = {}
122
122
 
123
- self.instance_variables.each do |variable|
124
- var_as_symbol = variable.to_s[1..-1].to_sym
125
- data[var_as_symbol] = eval(variable.to_s)
123
+ instance_variables.each do |variable|
124
+ var_as_symbol = variable.to_s[1..].to_sym
125
+ data[var_as_symbol] = instance_variable_get(variable)
126
126
  end
127
+
127
128
  data
128
129
  end
129
130
 
130
- private
131
-
132
- def self.date_intersect date1, date2, method
133
- if date1 and date2
131
+ def self.date_intersect(date1, date2, method)
132
+ if date1 && date2
134
133
  [date1, date2].send(method)
135
134
  elsif date1
136
135
  date1
@@ -140,4 +139,3 @@ module LogSense
140
139
  end
141
140
  end
142
141
  end
143
-
@@ -44,7 +44,7 @@ module LogSense
44
44
 
45
45
  attr_reader :format
46
46
 
47
- def initialize
47
+ def initialize
48
48
  @format = /#{IP} #{IDENT} #{USERID} \[#{TIMESTAMP}\] "(#{METHOD} #{URL} #{PROTOCOL}|-|.+)" #{RETURN_CODE} #{SIZE} "#{REFERER}" "#{USER_AGENT}"/
49
49
  end
50
50
 
@@ -82,7 +82,7 @@ module LogSense
82
82
  line_number
83
83
  )
84
84
  rescue StandardError => e
85
- warn.puts e.message
85
+ $stderr.puts e.message
86
86
  end
87
87
  end
88
88
  end
@@ -1,8 +1,8 @@
1
+ # coding: utf-8
1
2
  require 'terminal-table'
2
3
  require 'json'
3
4
  require 'erb'
4
5
  require 'ostruct'
5
-
6
6
  module LogSense
7
7
  #
8
8
  # Emit Data
@@ -31,9 +31,7 @@ module LogSense
31
31
  end
32
32
  end
33
33
 
34
- private_class_method
35
-
36
- def self.render(template, vars)
34
+ def self.render(template, vars = {})
37
35
  @template = File.join(File.dirname(__FILE__), 'templates', "_#{template}")
38
36
  erb_template = File.read @template
39
37
  ERB.new(erb_template).result(OpenStruct.new(vars).instance_eval { binding })
@@ -42,11 +40,11 @@ module LogSense
42
40
  def self.escape_javascript(string)
43
41
  js_escape_map = {
44
42
  '<' => '&lt;',
45
- '</' => '&lt;\/',
46
- '\\' => '\\\\',
43
+ '</' => '&lt;/',
47
44
  '\r\n' => '\\r\\n',
48
45
  '\n' => '\\n',
49
46
  '\r' => '\\r',
47
+ '\\' => ' \\\\',
50
48
  '"' => ' \\"',
51
49
  "'" => " \\'",
52
50
  '`' => ' \\`',
@@ -73,7 +71,9 @@ module LogSense
73
71
  # - width width to set
74
72
  def self.shorten(data, heading, width)
75
73
  # indexes of columns which have to be shortened
76
- to_shorten = %w[URL Description Path].map { |x| heading.index x }.compact
74
+ keywords = %w[URL Referers Description Path]
75
+ to_shorten = keywords.map { |x| heading.index x }.compact
76
+
77
77
  if width.nil? || to_shorten.empty? || data[0].nil?
78
78
  data
79
79
  else
@@ -96,7 +96,7 @@ module LogSense
96
96
  # column_alignment: specification of column alignments (works for txt reports)
97
97
  # vega_spec: specifications for Vega output
98
98
  # datatable_options: specific options for datatable
99
- def self.apache_report_specification(data)
99
+ def self.apache_report_specification(data = {})
100
100
  [
101
101
  { title: 'Daily Distribution',
102
102
  header: %w[Day DOW Hits Visits Size],
@@ -326,6 +326,19 @@ module LogSense
326
326
  column_alignment: %i[left right right right left],
327
327
  rows: data[:ips]
328
328
  },
329
+ {
330
+ title: 'Countries',
331
+ header: %w[Country Hits Visits IPs],
332
+ column_alignment: %i[left right right left],
333
+ rows: data[:countries]&.map do |k, v|
334
+ [
335
+ k,
336
+ v.map { |x| x[1] }.inject(&:+),
337
+ v.map { |x| x[2] }.inject(&:+),
338
+ v.map { |x| x[0] }.join(' ')
339
+ ]
340
+ end
341
+ },
329
342
  {
330
343
  title: 'Referers',
331
344
  header: %w[Referers Hits Visits Size],
@@ -333,10 +346,27 @@ module LogSense
333
346
  rows: data[:referers],
334
347
  col: 'small-12 cell'
335
348
  },
349
+ {
350
+ title: 'Streaks',
351
+ report: :html,
352
+ header: ['IP', 'Date', 'Total HTML', 'Total Other', 'HTML', 'Other'],
353
+ column_alignment: %i[left left right right left left],
354
+ rows: data[:streaks]&.group_by { |x| [x[0], x[1]] }&.map do |k, v|
355
+ [
356
+ k[0],
357
+ k[1],
358
+ v.map { |x| x[2] }.compact.select { |x| x.match(/\.html?$/) }.size,
359
+ v.map { |x| x[2] }.compact.reject { |x| x.match(/\.html?$/) }.size,
360
+ v.map { |x| x[2] }.compact.select { |x| x.match(/\.html?$/) }.join(' ■ '),
361
+ v.map { |x| x[2] }.compact.reject { |x| x.match(/\.html?$/) }.join(' ■ ')
362
+ ]
363
+ end,
364
+ col: 'small-12 cell'
365
+ }
336
366
  ]
337
367
  end
338
368
 
339
- def self.rails_report_specification(data)
369
+ def self.rails_report_specification(data = {})
340
370
  [
341
371
  {
342
372
  title: "Daily Distribution",
@@ -469,27 +499,54 @@ module LogSense
469
499
  header: %w[Date IP URL Description Log ID],
470
500
  column_alignment: %i[left left left left left],
471
501
  rows: data[:fatal],
472
- col: "small-12 cell"
502
+ col: 'small-12 cell'
473
503
  },
474
504
  {
475
- title: "Internal Server Errors",
505
+ title: 'Internal Server Errors',
476
506
  header: %w[Date Status IP URL Description Log ID],
477
507
  column_alignment: %i[left left left left left left],
478
508
  rows: data[:internal_server_error],
479
- col: "small-12 cell"
509
+ col: 'small-12 cell'
480
510
  },
481
511
  {
482
- title: "Errors",
512
+ title: 'Errors',
483
513
  header: %w[Log ID Context Description Count],
484
514
  column_alignment: %i[left left left left],
485
515
  rows: data[:error],
486
- col: "small-12 cell"
516
+ col: 'small-12 cell'
487
517
  },
488
518
  {
489
- title: "IPs",
519
+ title: 'IPs',
490
520
  header: %w[IPs Hits Country],
491
521
  column_alignment: %i[left right left],
492
522
  rows: data[:ips]
523
+ },
524
+ {
525
+ title: 'Countries',
526
+ header: %w[Country Hits IPs],
527
+ column_alignment: %i[left right left],
528
+ rows: data[:countries]&.map do |k, v|
529
+ [
530
+ k,
531
+ v.map { |x| x[1] }.inject(&:+),
532
+ v.map { |x| x[0] }.join(' ■ ')
533
+ ]
534
+ end
535
+ },
536
+ {
537
+ title: 'Streaks',
538
+ report: :html,
539
+ header: %w[IP Date Total Resources],
540
+ column_alignment: %i[left left right right left left],
541
+ rows: data[:streaks]&.group_by { |x| [x[0], x[1]] }&.map do |k, v|
542
+ [
543
+ k[0],
544
+ k[1],
545
+ v.size,
546
+ v.map { |x| x[2] }.join(' ■ ')
547
+ ]
548
+ end,
549
+ col: 'small-12 cell'
493
550
  }
494
551
  ]
495
552
  end
@@ -4,19 +4,22 @@ require 'ipaddr'
4
4
  require 'iso_country_codes'
5
5
 
6
6
  module LogSense
7
+ #
8
+ # Populate table of IP Locations from dbip-country-lite
9
+ #
7
10
  module IpLocator
8
- DB_FILE = File.join(File.dirname(__FILE__), "..", "..", "ip_locations", "dbip-country-lite.sqlite3")
11
+ DB_FILE = File.join(File.dirname(__FILE__), '..', '..', 'ip_locations', 'dbip-country-lite.sqlite3')
9
12
 
10
- def self.dbip_to_sqlite db_location
11
- db = SQLite3::Database.new ":memory:"
12
- db.execute "CREATE TABLE ip_location (
13
+ def self.dbip_to_sqlite(db_location)
14
+ db = SQLite3::Database.new ':memory:'
15
+ db.execute 'CREATE TABLE ip_location (
13
16
  from_ip_n INTEGER,
14
17
  from_ip TEXT,
15
18
  to_ip TEXT,
16
19
  country_code TEXT
17
- )"
20
+ )'
18
21
 
19
- ins = db.prepare "INSERT INTO ip_location(from_ip_n, from_ip, to_ip, country_code) values (?, ?, ?, ?)"
22
+ ins = db.prepare 'INSERT INTO ip_location(from_ip_n, from_ip, to_ip, country_code) values (?, ?, ?, ?)'
20
23
  CSV.foreach(db_location) do |row|
21
24
  ip = IPAddr.new row[0]
22
25
  ins.execute(ip.to_i, row[0], row[1], row[2])
@@ -33,29 +36,33 @@ module LogSense
33
36
  SQLite3::Database.new DB_FILE
34
37
  end
35
38
 
36
- def self.locate_ip ip, db
37
- return if not ip
39
+ def self.locate_ip(ip, db)
40
+ return unless ip
38
41
 
39
- ip_n = IPAddr.new(ip).to_i
40
- res = db.execute "SELECT * FROM ip_location where from_ip_n <= #{ip_n} order by from_ip_n desc limit 1"
42
+ query = db.prepare 'SELECT * FROM ip_location where from_ip_n <= ? order by from_ip_n desc limit 1'
41
43
  begin
42
- IsoCountryCodes.find(res[0][3]).name
43
- rescue
44
- res[0][3]
44
+ ip_n = IPAddr.new(ip).to_i
45
+ result_set = query.execute ip_n
46
+ country_code = result_set.map { |x| x[3] }[0]
47
+ IsoCountryCodes.find(country_code).name
48
+ rescue IPAddr::InvalidAddressError
49
+ 'INVALID IP'
50
+ rescue IsoCountryCodes::UnknownCodeError
51
+ country_code
45
52
  end
46
53
  end
47
54
 
48
55
  #
49
56
  # add country code to data[:ips]
50
57
  #
51
- def self.geolocate data
52
- @location_db = IpLocator::load_db
53
- data[:ips].each do |ip|
54
- country_code = IpLocator::locate_ip ip[0], @location_db
55
- ip << country_code
58
+ def self.geolocate(data)
59
+ @location_db = IpLocator.load_db
60
+
61
+ data[:ips].each do |line|
62
+ country_code = IpLocator.locate_ip line[0], @location_db
63
+ line << country_code
56
64
  end
57
65
  data
58
66
  end
59
-
60
67
  end
61
68
  end
@@ -59,6 +59,10 @@ module LogSense
59
59
  args[:no_selfpoll] = true
60
60
  end
61
61
 
62
+ opts.on('--verbose', 'Inform about progress (prints to STDERR)') do
63
+ args[:verbose] = true
64
+ end
65
+
62
66
  opts.on('-v', '--version', 'Prints version information') do
63
67
  puts "log_sense version #{LogSense::VERSION}"
64
68
  puts 'Copyright (C) 2021 Shair.Tech'
@@ -96,6 +100,7 @@ module LogSense
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
105
  args
101
106
  end
@@ -104,6 +104,9 @@ module LogSense
104
104
 
105
105
  @ips = db.execute "SELECT ip, count(ip) from Event where #{filter} group by ip order by count(ip) desc limit #{options[:limit]}"
106
106
 
107
+ @streaks = db.execute 'SELECT ip, substr(started_at, 1, 10), url from Event order by ip, started_at'
108
+ data = {}
109
+
107
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"
108
111
 
109
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'") || [[]]
@@ -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>
@@ -6,8 +6,9 @@ shortened = Emitter::shorten(report[:rows], report[:header], data[:width])
6
6
  table = Terminal::Table.new headings: report[:header], rows: shortened
7
7
  table.style = { border_i: "|" }
8
8
  columns = report[:header].size - 1
9
- (0..columns).map { |i| table.align_column(i, report[:column_alignment][i] || :left) }
10
-
9
+ (0..columns).map do |i|
10
+ table.align_column(i, report[:column_alignment][i] || :left)
11
+ end
11
12
  # return it
12
13
  table
13
14
  %>
@@ -18,7 +18,7 @@
18
18
  </li>
19
19
  <li class="stats-list-negative">
20
20
  <% days = data[:last_day_in_analysis] - data[:first_day_in_analysis] %>
21
- <%= days > 0 ? "%.2f" % (data[:total_unique_visits] / days.to_f) : "N/A" %>
21
+ <%= days > 0 ? "%d" % (data[:total_unique_visits] / days) : "N/A" %>
22
22
  <span class="stats-list-label">Unique Visits / Day</span>
23
23
  </li>
24
24
  <li class="stats-list-negative">
@@ -5,7 +5,7 @@ table = Terminal::Table.new rows: [
5
5
  ["Days", data[:total_days_in_analysis]],
6
6
  ["Hits", data[:total_hits]],
7
7
  ["Unique Visits", data[:total_unique_visits]],
8
- ["Unique Visits / Day", data[:total_days_in_analysis] > 0 ? "%.2f" % (data[:total_unique_visits] / data[:total_days_in_analysis].to_f) : "N/A"],
8
+ ["Unique Visits / Day", data[:total_days_in_analysis] > 0 ? "%d" % (data[:total_unique_visits] / data[:total_days_in_analysis]) : "N/A"],
9
9
  ["Hits/Unique Visitor", data[:total_unique_visits] != 0 ? data[:total_hits] / data[:total_unique_visits] : "N/A"]
10
10
  ]
11
11
  table.style = { border_i: "|" }
@@ -0,0 +1 @@
1
+ >>>> URLs IN THIS FILE ARE NOT SANITIZED AND MIGHT BE UNSAFE TO OPEN <<<<
@@ -1,7 +1,9 @@
1
1
  <!doctype html>
2
2
  <html class="no-js" lang="en">
3
3
  <head>
4
- <title><%= options[:title] || "Log Sense: #{data[:log_file]}" %></title>
4
+ <title>
5
+ <%= options[:title] || "Log Sense: #{data[:filenames].empty? ? "stdin" : data[:filenames].join(", ")}" %>
6
+ </title>
5
7
 
6
8
  <meta charset="utf-8" />
7
9
  <meta http-equiv="x-ua-compatible" content="ie=edge">
@@ -150,42 +152,11 @@
150
152
  <body>
151
153
  <div class="off-canvas-wrapper">
152
154
  <div class="off-canvas position-left" id="offCanvas" data-off-canvas>
153
- <nav>
154
- <h2>Navigation</h2>
155
- <ul class="no-bullet">
156
- <% [ "Summary",
157
- "Log Structure",
158
- "Daily Distribution",
159
- "Time Distribution",
160
- "20_ and 30_ on HTML pages",
161
- "20_ and 30_ on other resources",
162
- "40_ and 50_ on HTML pages",
163
- "40_ and 50_ on other Resources",
164
- "Statuses",
165
- "Daily Statuses",
166
- "Browsers",
167
- "Platforms",
168
- "Referers",
169
- "IPs",
170
- "Geolocation",
171
- "Streaks",
172
- "Command Invocation",
173
- "Performance"
174
- ].each do |item| %>
175
- <li class="nav-item">
176
- <a href="#<%= item.downcase.gsub(' ', '-') %>" data-close><%= item %></a>
177
- </li>
178
- <% end %>
179
- </ul>
180
-
181
- <p>
182
- Generated by
183
- <a href="https://github.com/avillafiorita/log_sense">LogSense</a> <br />
184
- on <%= DateTime.now.strftime("%Y-%m-%d %H:%M") %>.<br />
185
- <a href='https://db-ip.com'>IP Geolocation by DB-IP</a>
186
- </p>
187
- </nav>
155
+ <%= render "navigation.html.erb",
156
+ menus: Emitter::apache_report_specification.map { |x| x[:title] } +
157
+ ["Streaks", "Command Invocation", "Performance"] %>
188
158
  </div>
159
+
189
160
  <div class="off-canvas-content grid-container grid-x fluid" data-off-canvas-content>
190
161
  <div data-sticky-container>
191
162
  <div class="sticky" data-sticky data-margin-top="0">
@@ -196,14 +167,14 @@
196
167
  </div>
197
168
 
198
169
  <section class="main-section">
199
- <h1><%= options[:title] || "Log Sense: #{data[:log_file]}" %></h1>
170
+ <h1><%= options[:title] || "Log Sense Apache Log Report" %></h1>
200
171
 
201
- <p><b>Input File:</b> <%= (data[:log_file] || "stdin") %></p>
172
+ <p><b>Input File(s):</b> <%= data[:filenames].empty? ? "stdin" : data[:filenames].join(", ") %></p>
202
173
 
203
174
  <div class="grid-x grid-margin-x">
204
175
  <article class="card small-12 large-6 cell">
205
176
  <div class="card-divider">
206
- <h2 id="summary">Summary</h2>
177
+ <h2 id="<%= Emitter::slugify "Summary" %>">Summary</h2>
207
178
  </div>
208
179
  <div class="card-section">
209
180
  <%= render "summary.html.erb", data: data %>
@@ -212,7 +183,7 @@
212
183
 
213
184
  <article class="card cell small-12 large-6">
214
185
  <div class="card-divider">
215
- <h2 id="log-structure">Log Structure</h2>
186
+ <h2 id="<%= Emitter::slugify "Summary" %>">Log Structure</h2>
216
187
  </div>
217
188
  <div class="card-section">
218
189
  <%= render "log_structure.html.erb", data: data %>
@@ -224,7 +195,7 @@
224
195
  <% @reports.each_with_index do |report, index| %>
225
196
  <article class="card cell <%= report[:col] || "small-12 large-6" %>" >
226
197
  <div class="card-divider">
227
- <h2 id="<%= report[:title].downcase.gsub(' ', '-') %>">
198
+ <h2 id="<%= Emitter::slugify report[:title] %>">
228
199
  <%= report[:title] %>
229
200
  </h2>
230
201
  </div>
@@ -252,101 +223,10 @@
252
223
  <% end %>
253
224
  </div>
254
225
 
255
- <article class="card">
256
- <div class="card-divider">
257
- <h2 id="geolocation">Geolocation</h2>
258
- </div>
259
- <div class="card-section">
260
- <table id="geolocation-table" class="table unstriped">
261
- <thead>
262
- <tr>
263
- <th>Country Code</th>
264
- <th>Total Hits</th>
265
- <th>Total Visits</th>
266
- <th>IPs</th>
267
- </tr>
268
- </thead>
269
- <tbody>
270
- <%# IP, Hits, Visits Size, Country%>
271
- <% data[:ips].group_by { |x| x[4] }.each do |k, v| %>
272
- <tr>
273
- <td class="country"><%= k %></td>
274
- <td class="total-hits"><%= v.map { |x| x[1] }.inject(&:+) %></td>
275
- <td class="total-visits"><%= v.map { |x| x[2] }.inject(&:+) %></td>
276
- <td class="ips">
277
- <%= v.map { |x| "<a href=\"https://whatismyipaddress.com/ip/#{x[0]}\">#{x[0]}</a>" }.join(", ") %>
278
- </td>
279
- </tr>
280
- <% end %>
281
- </tbody>
282
- </table>
283
- </div>
284
- </article>
285
-
286
- <article class="card">
287
- <div class="card-divider">
288
- <h2 id="streaks">Streaks</h2>
289
- </div>
290
- <div class="card-section">
291
- <table id="streaks-table" class="table data-table streaks">
292
- <thead>
293
- <tr>
294
- <th>IP</th>
295
- <th>
296
- <div class="grid-x grid-margin-x">
297
- <div class="small-2 cell">
298
- Day
299
- </div>
300
- <div class="small-10 cell">
301
- Resources
302
- </div>
303
- </div>
304
- </th>
305
- </tr>
306
- </thead>
307
- <tbody>
308
- <% data[:streaks].group_by(&:first).each do |ip, date_urls| %>
309
- <tr>
310
- <td class="ip">
311
- <a href="https://whatismyipaddress.com/ip/<%= ip %>"><%= ip %></a>
312
- </td>
313
- <td class="streaks">
314
- <div class="grid-x grid-margin-x">
315
- <% date_urls.group_by { |x| x[1] }.each do |date, urls| %>
316
- <div class="small-12 medium-1 cell">
317
- <span class="date"><%= date %></span>
318
- </div>
319
- <div class="small-12 medium-5 cell">
320
- <span class="res-title">HTML:</span>
321
- <% unique_with_count = urls.map { |x| x[2] }.compact.group_by{|e| e}.map{|k, v| [k, v.length]} %>
322
- <ul class="no-bullet">
323
- <% unique_with_count.select { |x| x[0].match /.*\.html?/ }.each do |url| %>
324
- <li>[<%= url[1] %>] <%= Emitter::escape_javascript url[0] %></li>
325
- <% end %>
326
- </ul>
327
- </div>
328
- <div class=" small-12 medium-5 cell">
329
- <span class="res-title">Other Resources:</span>
330
- <ul class="no-bullet">
331
- <% unique_with_count.select { |x| x[0] and ! x[0].match /.*\.html?/ }.each do |url| %>
332
- <li>[<%= url[1] %>] <%= Emitter::escape_javascript url[0] %></li>
333
- <% end %>
334
- </ul>
335
- </div>
336
- <% end %>
337
- </div>
338
- </td>
339
- </tr>
340
- <% end %>
341
- </tbody>
342
- </table>
343
- </div>
344
- </article>
345
-
346
226
  <div class="grid-x grid-margin-x">
347
227
  <div class="cell small-12 large-6">
348
228
  <article>
349
- <h2 id="command-invocation">Command Invocation</h2>
229
+ <h2 id="<%= Emitter::slugify "Command Invocation" %>">Command Invocation</h2>
350
230
 
351
231
  <%= render "command_invocation.html.erb", data: data, options: options %>
352
232
  </article>
@@ -354,7 +234,7 @@
354
234
 
355
235
  <div class="small-12 large-6 cell">
356
236
  <article>
357
- <h2 id="performance">Performance</h2>
237
+ <h2 id="<%= Emitter::slugify "Performance" %>">Performance</h2>
358
238
 
359
239
  <%= render "performance.html.erb", data: data %>
360
240
  </article>
@@ -1,30 +1,17 @@
1
1
  * Apache Log Analysis
2
2
 
3
- >>>> URLs NOT SANITIZED. DO NOT CONVERT TO HTML <<<<
4
- >>>> (USE THE HTML EXPORT FUNCTION INSTEAD) <<<<
3
+ <%= render "warning.txt.erb" %>
5
4
 
6
5
  ** Summary
7
6
 
8
7
  <%= render "summary.txt.erb", data: data %>
9
8
 
10
- <% @reports.each do |report| %>
9
+ <% @reports.reject { |x| x[:report] == :html }.each do |report| %>
11
10
  ** <%= report[:title] %>
12
11
 
13
12
  <%= render "output_table.txt.erb", report: report, data: data %>
14
13
  <% end %>
15
14
 
16
- ** Geolocation
17
-
18
- <%=
19
- ips = data[:ips].group_by { |x| x[4] }.map { |k, v|
20
- [k, v.map { |x| x[1] }.inject(&:+), v.map { |x| x[2] }.inject(&:+) ]
21
- }
22
- table = Terminal::Table.new headings: ["Country", "Hits", "Total Visits"], rows: ips
23
- table.style = { border_i: "|" }
24
- (1..2).map { |i| table.align_column(i, :right) }
25
- table
26
- %>
27
-
28
15
  ** Command Invocation
29
16
 
30
17
  <%= render 'command_invocation.txt.erb', data: data %>
@@ -1,7 +1,9 @@
1
1
  <!doctype html>
2
2
  <html class="no-js" lang="en">
3
3
  <head>
4
- <title><%= options[:title] || "Log Sense: #{data[:log_file]}" %></title>
4
+ <title>
5
+ <%= options[:title] || "Log Sense: #{data[:filenames].empty? ? "stdin" : data[:filenames].join(", ")}" %>
6
+ </title>
5
7
 
6
8
  <meta charset="utf-8" />
7
9
  <meta http-equiv="x-ua-compatible" content="ie=edge">
@@ -150,36 +152,8 @@
150
152
  <body>
151
153
  <div class="off-canvas-wrapper">
152
154
  <div class="off-canvas position-left" id="offCanvas" data-off-canvas>
153
- <nav>
154
- <h2>Navigation</h2>
155
- <ul class="no-bullet">
156
- <% [
157
- "Summary",
158
- "Log Structure",
159
- "Daily Distribution",
160
- "Time Distribution",
161
- "Statuses",
162
- "Rails Performance",
163
- "Fatal Events",
164
- "Internal Server Errrors",
165
- "Errors",
166
- "IPs",
167
- "Command Invocation",
168
- "Performance"
169
- ].each do |item| %>
170
- <li class="nav-item">
171
- <a href="#<%= item.downcase.gsub(' ', '-') %>" data-close><%= item %></a>
172
- </li>
173
- <% end %>
174
- </ul>
175
-
176
- <p>
177
- Generated by
178
- <a href="https://github.com/avillafiorita/log_sense">LogSense</a> <br />
179
- on <%= DateTime.now.strftime("%Y-%m-%d %H:%M") %>.<br />
180
- <a href='https://db-ip.com'>IP Geolocation by DB-IP</a>
181
- </p>
182
- </nav>
155
+ <%= render "navigation.html.erb",
156
+ menus: Emitter::rails_report_specification.map { |x| x[:title] } %>
183
157
  </div>
184
158
  <div class="off-canvas-content grid-container grid-x fluid" data-off-canvas-content>
185
159
  <div data-sticky-container>
@@ -191,14 +165,14 @@
191
165
  </div>
192
166
 
193
167
  <section class="main-section">
194
- <h1><%= options[:title] || "Log Sense: #{data[:log_file]}" %></h1>
168
+ <h1><%= options[:title] || "Log Sense Rails Log Report" %></h1>
195
169
 
196
- <p><b>Input File:</b> <%= (data[:log_file] || "stdin") %></p>
170
+ <p><b>Input File(s):</b> <%= data[:filenames].empty? ? "stdin" : data[:filenames].join(", ") %></p>
197
171
 
198
172
  <div class="grid-x grid-margin-x">
199
173
  <article class="card small-12 large-6 cell">
200
174
  <div class="card-divider">
201
- <h2 id="summary">Summary</h2>
175
+ <h2 id="<%= Emitter::slugify "Summary" %>">Summary</h2>
202
176
  </div>
203
177
  <div class="card-section">
204
178
  <%= render "summary.html.erb", data: data %>
@@ -207,7 +181,7 @@
207
181
 
208
182
  <article class="card cell small-12 large-6">
209
183
  <div class="card-divider">
210
- <h2 id="log-structure">Log Structure</h2>
184
+ <h2 id="<%= Emitter::slugify "Log Structure" %>">Log Structure</h2>
211
185
  </div>
212
186
  <div class="card-section">
213
187
  <%= render "log_structure.html.erb", data: data %>
@@ -249,7 +223,9 @@
249
223
  <div class="grid-x grid-margin-x">
250
224
  <div class="cell small-12 large-6">
251
225
  <article>
252
- <h2 id="command-invocation">Command Invocation</h2>
226
+ <h2 id="<%= Emitter::slugify "Command Invocation" %>">
227
+ Command Invocation
228
+ </h2>
253
229
 
254
230
  <%= render "command_invocation.html.erb", data: data, options: options %>
255
231
  </article>
@@ -257,7 +233,7 @@
257
233
 
258
234
  <div class="small-12 large-6 cell">
259
235
  <article>
260
- <h2 id="performance"> Performance</h2>
236
+ <h2 id="<%= Emitter::slugify "Performance" %>"> Performance</h2>
261
237
 
262
238
  <%= render "performance.html.erb", data: data %>
263
239
  </article>
@@ -1,13 +1,12 @@
1
1
  * Rails Log Analysis
2
2
 
3
- >>>> URLs NOT SANITIZED. DO NOT CONVERT TO HTML <<<<
4
- >>>> (USE THE HTML EXPORT FUNCTION INSTEAD) <<<<
3
+ <%= render "warning.txt.erb" %>
5
4
 
6
5
  ** Summary
7
6
 
8
7
  <%= render "summary.txt.erb", data: data %>
9
8
 
10
- <% @reports.each do |report| %>
9
+ <% @reports.reject { |x| x[:report] == :html }.each do |report| %>
11
10
  ** <%= report[:title] %>
12
11
 
13
12
  <%= render "output_table.txt.erb", report: report, data: data %>
@@ -15,9 +14,9 @@
15
14
 
16
15
  ** Command Invocation
17
16
 
18
- <%= render "command_invocation.txt.erb", data: data %>
17
+ <%= render 'command_invocation.txt.erb', data: data %>
19
18
 
20
- ** Log Sense Performance
19
+ ** Performance
21
20
 
22
- <%= render "performance.txt.erb", data: data %>
21
+ <%= render 'performance.txt.erb', data: data %>
23
22
 
@@ -1,3 +1,3 @@
1
1
  module LogSense
2
- VERSION = "1.4.1"
2
+ VERSION = "1.5.0"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: log_sense
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.4.1
4
+ version: 1.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Adolfo Fibrillation
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2022-03-07 00:00:00.000000000 Z
11
+ date: 2022-03-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: browser
@@ -141,6 +141,7 @@ files:
141
141
  - lib/log_sense/templates/_command_invocation.html.erb
142
142
  - lib/log_sense/templates/_command_invocation.txt.erb
143
143
  - lib/log_sense/templates/_log_structure.html.erb
144
+ - lib/log_sense/templates/_navigation.html.erb
144
145
  - lib/log_sense/templates/_output_table.html.erb
145
146
  - lib/log_sense/templates/_output_table.txt.erb
146
147
  - lib/log_sense/templates/_performance.html.erb
@@ -148,6 +149,7 @@ files:
148
149
  - lib/log_sense/templates/_report_data.html.erb
149
150
  - lib/log_sense/templates/_summary.html.erb
150
151
  - lib/log_sense/templates/_summary.txt.erb
152
+ - lib/log_sense/templates/_warning.txt.erb
151
153
  - lib/log_sense/templates/apache.html.erb
152
154
  - lib/log_sense/templates/apache.txt.erb
153
155
  - lib/log_sense/templates/rails.html.erb