log_sense 1.5.2 → 1.6.1

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.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.org +31 -0
  3. data/Gemfile.lock +6 -4
  4. data/README.org +108 -34
  5. data/Rakefile +30 -10
  6. data/exe/log_sense +110 -39
  7. data/ip_locations/dbip-country-lite.sqlite3 +0 -0
  8. data/lib/log_sense/aggregator.rb +191 -0
  9. data/lib/log_sense/apache_aggregator.rb +122 -0
  10. data/lib/log_sense/apache_log_line_parser.rb +23 -21
  11. data/lib/log_sense/apache_log_parser.rb +15 -12
  12. data/lib/log_sense/apache_report_shaper.rb +309 -0
  13. data/lib/log_sense/emitter.rb +55 -553
  14. data/lib/log_sense/ip_locator.rb +35 -18
  15. data/lib/log_sense/options_checker.rb +24 -0
  16. data/lib/log_sense/options_parser.rb +81 -51
  17. data/lib/log_sense/rails_aggregator.rb +69 -0
  18. data/lib/log_sense/rails_log_parser.rb +82 -68
  19. data/lib/log_sense/rails_report_shaper.rb +183 -0
  20. data/lib/log_sense/report_shaper.rb +105 -0
  21. data/lib/log_sense/templates/_cdn_links.html.erb +11 -0
  22. data/lib/log_sense/templates/_command_invocation.html.erb +4 -0
  23. data/lib/log_sense/templates/_log_structure.html.erb +7 -1
  24. data/lib/log_sense/templates/_output_table.html.erb +6 -2
  25. data/lib/log_sense/templates/_rails.css.erb +7 -0
  26. data/lib/log_sense/templates/_summary.html.erb +9 -7
  27. data/lib/log_sense/templates/_summary.txt.erb +2 -2
  28. data/lib/log_sense/templates/{rails.html.erb → report_html.erb} +19 -37
  29. data/lib/log_sense/templates/{apache.txt.erb → report_txt.erb} +1 -1
  30. data/lib/log_sense/version.rb +1 -1
  31. data/lib/log_sense.rb +19 -9
  32. data/log_sense.gemspec +1 -1
  33. data/screenshots/rails-screenshot.png +0 -0
  34. metadata +18 -12
  35. data/lib/log_sense/apache_data_cruncher.rb +0 -147
  36. data/lib/log_sense/rails_data_cruncher.rb +0 -141
  37. data/lib/log_sense/templates/apache.html.erb +0 -115
  38. data/lib/log_sense/templates/rails.txt.erb +0 -22
  39. /data/{apache-screenshot.png → screenshots/apache-screenshot.png} +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5f37b2247014af9bccfb8bdd205b54eb51cbef35495904444765648a96e0b9ac
4
- data.tar.gz: a9cd03afb6770bf854791b8ec93e87d09532828d894b28f5dde670f1af1b1b1d
3
+ metadata.gz: 0a915af429fe2492f17bc767c86767e38d37a674af6206fb3b2c0a76e4cad620
4
+ data.tar.gz: bf4cc3a1e438b752c9c99fee5f91c85abd38de95dac947c2382e2f9825b51467
5
5
  SHA512:
6
- metadata.gz: 62981216e38b92e1c3ea227726dccd592c441de32519a4df6f39bac127f55da32e82b4a1a14897b09b7032c7b3f9506a14e80e49dc43bfe8c9d86bfaa340da82
7
- data.tar.gz: e5e6180008a12561d688668cffb2ef3d885d7733cf877aafaed0397b9fa0e91dd12a2998b19006dad4fe6b202bd203a00757f86d5d37efee777dbc7be43e5ab1
6
+ metadata.gz: 5612ef5474aa397132527588d289d9d1eba4f8954b92553e32fd5b856f2bd5441ba09d89d1bf12a0f220c602dcd2e73de2d6e360b6177b162d57adc2726442c9
7
+ data.tar.gz: c3b546cc177a3364b1b513f4274c1521aa1669bb17b142eb453ba73251f1e3458e7b9d1703b692ef275a474821ce7375e387155f63e3e7d5d7c9c42e1c50a150
data/CHANGELOG.org CHANGED
@@ -2,6 +2,37 @@
2
2
  #+AUTHOR: Adolfo Villafiorita
3
3
  #+STARTUP: showall
4
4
 
5
+ * 1.6.1
6
+
7
+ - Country DB now stores country name.
8
+
9
+ * 1.6.0
10
+
11
+ - [User] New output format =ufw= generates directives to blacklist IPs
12
+ requesting URLs matching a pattern. For users of the Uncomplicated
13
+ Firewall.
14
+ - [User] new option =--no-geo= skips geolocation, which is terribly
15
+ costly in the current implementation.
16
+ - [User] Updated DB-IP country file to Dec 2022 version.
17
+ - [User] Changed name of SQLite output format to sqlite3
18
+ - [User] It is now possible to start analysis from a sqlite3 DB
19
+ generated by log_sense, breaking parsing and generation in two
20
+ steps.
21
+ - [User] Check for correctness of I/O formats before launching
22
+ analysis
23
+ - [User] Streak report has been renames Session. Limited the number
24
+ of URLs shown in each session, to avoid buffer?/memory overflows
25
+ when an IP requests a massive amount of URLs.
26
+ - [User] Added an IP-per-hour visits report.
27
+ - [Code] A rather extensive refactoring of the source code to
28
+ remove code duplications and improve code structure.
29
+ - [Code] Rubocop-ped various files
30
+ - [Code] Added text renderer to DataTable, which sanitizes input and
31
+ further reduces risks of XSS and log poisoning attacks
32
+ - [Code] CDN links have been ported into the Emitter module and used
33
+ in the Embedded Ruby Templates (erbs). This simplifies version
34
+ updates of Javascript libraries used in reports.
35
+
5
36
  * 1.5.2
6
37
 
7
38
  - [User] Updated DB-IP country file.
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- log_sense (1.5.2)
4
+ log_sense (1.5.3)
5
5
  browser
6
6
  ipaddr
7
7
  iso_country_codes
@@ -16,18 +16,20 @@ GEM
16
16
  irb (>= 1.3.6)
17
17
  reline (>= 0.3.1)
18
18
  io-console (0.5.11)
19
- ipaddr (1.2.4)
19
+ ipaddr (1.2.5)
20
20
  irb (1.4.1)
21
21
  reline (>= 0.3.0)
22
22
  iso_country_codes (0.7.8)
23
+ mini_portile2 (2.8.0)
23
24
  minitest (5.15.0)
24
25
  rake (12.3.3)
25
26
  reline (0.3.1)
26
27
  io-console (~> 0.5)
27
- sqlite3 (1.4.4)
28
+ sqlite3 (1.5.4)
29
+ mini_portile2 (~> 2.8.0)
28
30
  terminal-table (3.0.2)
29
31
  unicode-display_width (>= 1.1.1, < 3)
30
- unicode-display_width (2.2.0)
32
+ unicode-display_width (2.3.0)
31
33
 
32
34
  PLATFORMS
33
35
  ruby
data/README.org CHANGED
@@ -9,7 +9,7 @@ Rails logs. Written in Ruby, it runs from the command line, it is
9
9
  fast, and it can be installed on any system with a relatively recent
10
10
  version of Ruby. We tested on Ruby 2.6.9, Ruby 3.0.x and later.
11
11
 
12
- LogSense reports the following data:
12
+ When generating reports, LogSense reports the following data:
13
13
 
14
14
  - Visitors, hits, unique visitors, bandwidth used
15
15
  - Most accessed HTML pages
@@ -22,18 +22,49 @@ LogSense reports the following data:
22
22
  - IP Country location, thanks to the DP-IP lite country DB
23
23
  - Streaks: resources accessed by a given IP over time
24
24
  - Performance of Rails requests
25
+
26
+ A special output format =ufw= generates rules for the [[https://launchpad.net/ufw][Uncomplicated
27
+ Firewall]] to blacklist IPs requesting URLs matching a specific pattern.
25
28
 
26
29
  Filters from the command line allow to analyze specific periods and
27
30
  distinguish traffic generated by self polls and crawlers.
28
31
 
29
- LogSense generates HTML, txt, and SQLite outputs.
32
+ LogSense generates HTML, txt, ufw, and SQLite outputs.
30
33
 
31
- And, of course, the compulsory screenshot:
34
+ ** Apache Report Structure
32
35
 
33
36
  #+ATTR_HTML: :width 80%
34
- [[file:./apache-screenshot.png]]
37
+ [[file:./screenshots/apache-screenshot.png]]
38
+
39
+
40
+ ** Rails Report Structure
41
+
42
+ #+ATTR_HTML: :width 80%
43
+ [[file:./screenshots/rails-screenshot.png]]
44
+
45
+
46
+ ** UFW Report
35
47
 
48
+ The output format =ufw= generates directives for Uncomplicated
49
+ Firewall blacklisting IPs requesting URLs matching a given pattern.
36
50
 
51
+ We use it to blacklist IPs requesting WordPress login pages on our
52
+ websites... since we don't use WordPress for our websites.
53
+
54
+ *Example*
55
+
56
+ #+begin_src
57
+ $ log_sense -f apache -t ufw -i apache.log
58
+ # /users/sign_in/xmlrpc.php?rsd
59
+ ufw deny from 20.212.3.206
60
+
61
+ # /wp-login.php /wordpress/wp-login.php /blog/wp-login.php /wp/wp-login.php
62
+ ufw deny from 185.255.134.18
63
+
64
+ ...
65
+ #+end_src
66
+
67
+
37
68
  * An important word of warning
38
69
 
39
70
  [[https://owasp.org/www-community/attacks/Log_Injection][Log poisoning]] is a technique whereby attackers send requests with invalidated
@@ -48,9 +79,10 @@ opened or code executed.
48
79
  * Motivation
49
80
 
50
81
  LogSense moves along the lines of tools such as [[https://goaccess.io/][GoAccess]] (which
51
- strongly inspired the development of Log Sense) and [[https://umami.is/][Umami]], focusing on
52
- *privacy* and *data-ownership*: the data generated by LogSense is
53
- stored on your computer and owned by you (like it should be)[fn:1].
82
+ strongly inspired the development of Log Sense) and [[https://umami.is/][Umami]], both
83
+ focusing on *privacy* and *data-ownership*: the data generated by
84
+ LogSense is stored on your computer and owned by you (like it should
85
+ be)[fn:1].
54
86
 
55
87
  LogSense is also inspired by *static websites generators*: statistics
56
88
  are generated from the command line and accessed as static HTML files.
@@ -76,33 +108,30 @@ generated files are then made available on a private area on the web.
76
108
  #+RESULTS:
77
109
  #+begin_example
78
110
  Usage: log_sense [options] [logfile ...]
79
- --title=TITLE Title to use in the report
80
- -f, --input-format=FORMAT Input format (either rails or apache)
81
- -i, --input-files=file,file, Input files (can also be passed directly)
82
- -t, --output-format=FORMAT Output format: html, org, txt, sqlite. See below for available formats
83
- -o, --output-file=OUTPUT_FILE Output file
84
- -b, --begin=DATE Consider entries after or on DATE
85
- -e, --end=DATE Consider entries before or on DATE
86
- -l, --limit=N Limit to the N most requested resources (defaults to 100)
87
- -w, --width=WIDTH Maximum width of long columns in textual reports
88
- -r, --rows=ROWS Maximum number of rows for columns with multiple entries in textual reports
89
- -c, --crawlers=POLICY Decide what to do with crawlers (applies to Apache Logs)
90
- -n, --no-selfpolls Ignore self poll entries (requests from ::1; applies to Apache Logs)
91
- --verbose Inform about progress (prints to STDERR)
92
- -v, --version Prints version information
93
- -h, --help Prints this help
94
-
95
- This is version 1.5.2
96
-
97
- Output formats
98
- apache parsing can produce the following outputs:
99
- - sqlite
100
- - html
101
- - txt
102
- rails parsing can produce the following outputs:
103
- - sqlite
104
- - html
105
- - txt
111
+ --title=TITLE Title to use in the report
112
+ -f, --input-format=FORMAT Input format (either rails or apache)
113
+ -i, --input-files=file,file, Input files (can also be passed directly)
114
+ -t, --output-format=FORMAT Output format: html, org, txt, sqlite.
115
+ -o, --output-file=OUTPUT_FILE Output file
116
+ -b, --begin=DATE Consider entries after or on DATE
117
+ -e, --end=DATE Consider entries before or on DATE
118
+ -l, --limit=N Limit to the N most requested resources (defaults to 100)
119
+ -w, --width=WIDTH Maximum width of long columns in textual reports
120
+ -r, --rows=ROWS Maximum number of rows for columns with multiple entries in textual reports
121
+ -p, --pattern=PATTERN Pattern to use with ufw report to decide IP to blacklist
122
+ -c, --crawlers=POLICY Decide what to do with crawlers (applies to Apache Logs)
123
+ --no-selfpolls Ignore self poll entries (requests from ::1; applies to Apache Logs)
124
+ -n, --no-geog Do not geolocate entries
125
+ --verbose Inform about progress (output to STDERR)
126
+ -v, --version Prints version information
127
+ -h, --help Prints this help
128
+
129
+ This is version 1.6.0
130
+
131
+ Output formats:
132
+
133
+ - rails: txt, html, sqlite3, ufw
134
+ - apache: txt, html, sqlite3, ufw
106
135
  #+end_example
107
136
 
108
137
  Examples:
@@ -112,6 +141,51 @@ log_sense -f apache -i access.log -t txt > access-data.txt
112
141
  log_sense -f rails -i production.log -t html -o performance.html
113
142
  #+end_example
114
143
 
144
+ * Code Structure
145
+
146
+ The code implements a pipeline, with the following steps:
147
+
148
+ 1. *Parser:* parses a log to a SQLite3 database. The database
149
+ contains a table with a list of events, and, in the case of Rails
150
+ report, a table with the errors.
151
+ 2. *Aggregator:* takes as input a SQLite DB and aggregates data,
152
+ typically performing "group by", which are simpler to generate in
153
+ Ruby, rather than in SQL. The module outputs a Hash, with
154
+ different reporting data.
155
+ 3. *GeoLocator:* add country information to all the reporting data
156
+ which has an IP as one the fields.
157
+ 4. *Shaper:* makes (geolocated) aggregated data (e.g. Hashes and
158
+ such), into Array of Arrays, simplifying the structure of the code
159
+ building the reports.
160
+ 5. *Emitter* generates reports from shaped data using ERB.
161
+
162
+ The architecture and the structure of the code is far from being nice,
163
+ for historical reason and for a bunch of small differences existing
164
+ between the input and the outputs to be generated. This usually ends
165
+ up with modifications to the code that have to be replicated in
166
+ different parts of the code and in interferences.
167
+
168
+ Among the points I would like to address:
169
+
170
+ - The execution pipeline in the main script has a few exceptions to
171
+ manage SQLite reading/dumping and ufw report. A linear structure
172
+ would be a lot nicer.
173
+ - Two different classes are defined for steps 1, 2, and 4, to manage,
174
+ respectively, Apache and Rails logs. These classes inherit from a
175
+ common ancestor (e.g. ApacheParser and RailsParser both inherit from
176
+ Parser), but there is still too little code shared. A nicer
177
+ approach would be that of identifying a common DB structure and
178
+ unify the pipeline up to (or including) the generation of
179
+ reports. There are a bunch of small different things to highlight in
180
+ reports, which still make this difficult. For instance, the country
181
+ report for Apache reports size of TX data, which is not available
182
+ for Rail reports.
183
+ - Geolocation could become a lot more efficient if performed in
184
+ SQLite, rather than in Ruby
185
+ - The distinction between Aggregation, Shaping, and Emission is a too
186
+ fine-grained and it would be nice to be able to cleanly remove one
187
+ of the steps.
188
+
115
189
 
116
190
  * Change Log
117
191
 
data/Rakefile CHANGED
@@ -9,18 +9,38 @@ 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, [:year_month] do |tasks, args|
13
- filename = "./ip_locations/dbip-country-lite-#{args[:year_month]}.csv"
12
+ task :dbip, [:filename] do |tasks, args|
13
+ filename_or_yyyy_mm = args[:filename]
14
+
15
+ filename = if /\d{4}-\d{2}/.match(filename_or_yyyy_mm)
16
+ "ip_locations/dbip-country-lite-#{filename_or_yyyy_mm}.csv"
17
+ else
18
+ filename_or_yyyy_mm
19
+ end
20
+
21
+ # if the filename has a .gz extension or a gzipped version of the file
22
+ # exists, gunzip it
23
+ if File.extname(filename) == ".gz" || File.exist?("#{filename}.gz")
24
+ system "gunzip #{filename}.gz"
25
+ end
14
26
 
15
27
  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'
28
+ puts <<-EOS
29
+ Error. Could not find: #{filename}
30
+
31
+ I see the following files:
32
+
33
+ #{Dir.glob("ip_locations/dbip-country-lite*").map { |x| "- #{x}" }.join("\n")}
34
+
35
+ 1. Download (if necessary) a more recent version from: https://db-ip.com/db/download/ip-to-country-lite
36
+ 2. Save downloaded file to ip_locations/
37
+ 3. Relaunch with YYYY-MM (will build: dbip-country-lite-YYYY-MM.csv)
38
+ or with filename.
39
+
40
+ Remark. If the filename has the extension .gz or if the
41
+ filename does not exist, but a file with the same name and .gz extension
42
+ exists, it is gunzipped first
43
+ EOS
24
44
 
25
45
  exit
26
46
  else
data/exe/log_sense CHANGED
@@ -1,82 +1,153 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
- require 'log_sense.rb'
3
+ require "log_sense"
4
+ require "sqlite3"
4
5
 
5
6
  #
6
7
  # Parse Command Line Arguments
7
8
  #
8
9
 
9
10
  # this better be here... OptionsParser consumes ARGV
10
- @command_line = ARGV.join(' ')
11
- @options = LogSense::OptionsParser.parse ARGV
12
- @output_file = @options[:output_file]
11
+ @command_line = ARGV.join(" ")
12
+ @options = LogSense::OptionsParser.parse ARGV
13
+ @input_filenames = @options[:input_filenames] + ARGV
14
+ @output_filename = @options[:output_filename]
13
15
 
14
16
  #
15
- # Input files can be gotten from an option and from what remains in
16
- # ARGV
17
+ # Check correctness of input data.
18
+ #
19
+
20
+ #
21
+ # Check input files
17
22
  #
18
- @input_filenames = @options[:input_filenames] + ARGV
19
23
  @non_existing = @input_filenames.reject { |x| File.exist?(x) }
20
24
 
21
- unless @non_existing.empty?
22
- $stderr.puts "Error: input file(s) '#{@non_existing.join(', ')}' do not exist"
25
+ if @non_existing.any?
26
+ warn "Error: some input file(s) \"#{@non_existing.join(", ")}\" do not exist"
27
+ exit 1
28
+ end
29
+
30
+ #
31
+ # Special condition: sqlite3 requires a single file as input
32
+ #
33
+ if @input_filenames.size > 0 &&
34
+ File.extname(@input_filenames.first) == "sqlite3" &&
35
+ @input_filenames.size > 1
36
+ warn "Error: you can pass only one sqlite3 file as input"
37
+ exit 1
38
+ end
39
+
40
+ #
41
+ # Supported input/output chains
42
+ #
43
+ iformat = @options[:input_format]
44
+ oformat = @options[:output_format]
45
+
46
+ if !LogSense::OptionsChecker::compatible?(iformat, oformat)
47
+ warn "Error: don't know how to make #{iformat} into #{oformat}."
48
+ warn "Possible transformation chains:"
49
+ warn LogSense::OptionsChecker.chains_to_s
23
50
  exit 1
24
51
  end
25
- @input_files = @input_filenames.empty? ? [$stdin] : @input_filenames.map { |x| File.open(x, 'r') }
26
52
 
27
53
  #
28
- # Parse Log and Track Statistics
54
+ # Do the work
29
55
  #
30
56
 
31
57
  @started_at = Time.now
32
58
 
33
- case @options[:input_format]
34
- when 'apache'
35
- parser_klass = LogSense::ApacheLogParser
36
- cruncher_klass = LogSense::ApacheDataCruncher
37
- when 'rails'
38
- parser_klass = LogSense::RailsLogParser
39
- cruncher_klass = LogSense::RailsDataCruncher
59
+ if @input_filenames.size > 0 &&
60
+ File.extname(@input_filenames.first) == ".sqlite3"
61
+ warn "Reading SQLite3 DB ..." if @options[:verbose]
62
+ @db = SQLite3::Database.open @input_filenames.first
40
63
  else
41
- $stderr.puts "Error: input format #{@options[:input_format]} not understood."
42
- exit 1
64
+ warn "Parsing ..." if @options[:verbose]
65
+ @input_files = if @input_filenames.empty?
66
+ [$stdin]
67
+ else
68
+ @input_filenames.map { |fname| File.open(fname, "r") }
69
+ end
70
+ class_name = "LogSense::#{@options[:input_format].capitalize}LogParser"
71
+ parser_class = Object.const_get class_name
72
+ parser = parser_class.new
73
+ @db = parser.parse @input_files
43
74
  end
44
75
 
45
- $stderr.puts "Parsing input files..." if @options[:verbose]
46
- @db = parser_klass.parse @input_files
76
+ if @options[:output_format] == "sqlite3"
77
+ warn "Saving SQLite3 DB ..." if @options[:verbose]
47
78
 
48
- if @options[:output_format] == 'sqlite'
49
- $stderr.puts "Saving to SQLite3..." if @options[:verbose]
50
- ddb = SQLite3::Database.new(@output_file || 'db.sqlite3')
51
- b = SQLite3::Backup.new(ddb, 'main', @db, 'main')
79
+ ddb = SQLite3::Database.new(@output_filename || "db.sqlite3")
80
+ b = SQLite3::Backup.new(ddb, "main", @db, "main")
52
81
  b.step(-1) #=> DONE
53
82
  b.finish
83
+
84
+ exit 0
85
+ elsif @options[:output_format] == "ufw"
86
+ pattern = @options[:pattern] || "php"
87
+
88
+ if @options[:input_format] == "rails"
89
+ query = "select distinct event.ip,event.url
90
+ from error join event
91
+ where event.log_id = error.log_id and
92
+ event.url like '%#{pattern}%'"
93
+ else
94
+ query = "select distinct ip,path from logline
95
+ where path like '%#{pattern}%'"
96
+ end
97
+
98
+ ips = @db.execute query
99
+ ips_and_urls = ips.group_by { |x| x[0] }.transform_values { |x|
100
+ x.map { |y| y[1..-1] }.flatten
101
+ }
102
+ ips_and_urls.each do |ip, urls|
103
+ puts "# #{urls[0..10].uniq.join(' ')}"
104
+ puts "ufw deny from #{ip}"
105
+ puts
106
+ end
107
+
108
+ exit 0
54
109
  else
55
- $stderr.puts "Aggregating data..." if @options[:verbose]
56
- @data = cruncher_klass.crunch @db, @options
110
+ warn "Aggregating data ..." if @options[:verbose]
111
+ class_name = "LogSense::#{@options[:input_format].capitalize}Aggregator"
112
+ aggr_class = Object.const_get class_name
113
+ aggr = aggr_class.new(@db, @options)
114
+ @data = aggr.aggregate
57
115
 
58
- $stderr.puts "Geolocating..." if @options[:verbose]
59
- @data = LogSense::IpLocator.geolocate @data
116
+ if @options[:geolocation]
117
+ warn "Geolocating ..." if @options[:verbose]
118
+ @data = LogSense::IpLocator.geolocate @data
60
119
 
61
- $stderr.puts "Grouping by country..." if @options[:verbose]
62
- country_col = @data[:ips][0].size - 1
63
- @data[:countries] = @data[:ips].group_by { |x| x[country_col] }
120
+ warn "Grouping IPs by country ..." if @options[:verbose]
121
+ country_col = @data[:ips][0].size - 1
122
+ @data[:countries] = @data[:ips].group_by { |x| x[country_col] }
123
+ else
124
+ @data[:countries] = {}
125
+ end
64
126
 
65
127
  @ended_at = Time.now
66
128
  @duration = @ended_at - @started_at
67
129
 
68
130
  @data = @data.merge({
69
131
  command: @command_line,
70
- filenames: ARGV,
132
+ filenames: @input_filenames,
71
133
  log_files: @input_files,
72
134
  started_at: @started_at,
73
135
  ended_at: @ended_at,
74
136
  duration: @duration,
75
137
  width: @options[:width]
76
138
  })
77
- #
78
- # Emit Output
79
- #
80
- $stderr.puts "Emitting..." if @options[:verbose]
81
- puts LogSense::Emitter.emit @data, @options
139
+
140
+ if @options[:verbose]
141
+ warn "I have the following keys in data: "
142
+ warn @data.keys.sort.map { |key| "#{key}: #{@data[key].class}" }.join("\n")
143
+ end
144
+
145
+ warn "Shaping data for output ..." if @options[:verbose]
146
+ class_name = "LogSense::#{@options[:input_format].capitalize}ReportShaper"
147
+ shaper_class = Object.const_get class_name
148
+ shaper = shaper_class.new
149
+ @reports = shaper.shape @data
150
+
151
+ warn "Emitting..." if @options[:verbose]
152
+ puts LogSense::Emitter.emit @reports, @data, @options
82
153
  end
Binary file