weblog-parser 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +8 -0
- data/.travis.yml +7 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +8 -0
- data/Gemfile.lock +24 -0
- data/LICENSE.txt +21 -0
- data/README.md +197 -0
- data/Rakefile +12 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/bin/wlparser +6 -0
- data/lib/log_parser.rb +31 -0
- data/lib/log_parser/color_text.rb +22 -0
- data/lib/log_parser/constants.rb +101 -0
- data/lib/log_parser/formatter.rb +125 -0
- data/lib/log_parser/ip_validator.rb +17 -0
- data/lib/log_parser/log_reader.rb +100 -0
- data/lib/log_parser/option_handler.rb +144 -0
- data/lib/log_parser/output_processor.rb +90 -0
- data/lib/log_parser/parser.rb +98 -0
- data/lib/log_parser/path_validator.rb +16 -0
- data/lib/log_parser/version.rb +6 -0
- data/lib/log_parser/warning_handler.rb +30 -0
- data/weblog_parser.gemspec +40 -0
- metadata +111 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 213d9f51148d7c7c5201648b8409c9996c8fa6250bc7256766029e2973989c9a
|
4
|
+
data.tar.gz: fb7e70f03efcf6d711133731ee759702a3639dba7d6117eaff28afd8e7c06d20
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: dcd77fe9ae16abbea86aa980d93bc98a6ada1bd9999e84752a45057dfc0eb5da7e9355e224e7ff07c58d3cfb0bff1a0cea62b7aa7c210b851067b739ab7ac56c
|
7
|
+
data.tar.gz: 542e1293a56caeceff20eca7487998a973ac3f69b13dae306197479fb725c907f9bc5b1c85797b7467340dc1a910ab6dcd8702b72dbf4c5d6f66b78c8299f63b
|
data/.gitignore
ADDED
data/.travis.yml
ADDED
data/CODE_OF_CONDUCT.md
ADDED
@@ -0,0 +1,74 @@
|
|
1
|
+
# Contributor Covenant Code of Conduct
|
2
|
+
|
3
|
+
## Our Pledge
|
4
|
+
|
5
|
+
In the interest of fostering an open and welcoming environment, we as
|
6
|
+
contributors and maintainers pledge to making participation in our project and
|
7
|
+
our community a harassment-free experience for everyone, regardless of age, body
|
8
|
+
size, disability, ethnicity, gender identity and expression, level of experience,
|
9
|
+
nationality, personal appearance, race, religion, or sexual identity and
|
10
|
+
orientation.
|
11
|
+
|
12
|
+
## Our Standards
|
13
|
+
|
14
|
+
Examples of behavior that contributes to creating a positive environment
|
15
|
+
include:
|
16
|
+
|
17
|
+
* Using welcoming and inclusive language
|
18
|
+
* Being respectful of differing viewpoints and experiences
|
19
|
+
* Gracefully accepting constructive criticism
|
20
|
+
* Focusing on what is best for the community
|
21
|
+
* Showing empathy towards other community members
|
22
|
+
|
23
|
+
Examples of unacceptable behavior by participants include:
|
24
|
+
|
25
|
+
* The use of sexualized language or imagery and unwelcome sexual attention or
|
26
|
+
advances
|
27
|
+
* Trolling, insulting/derogatory comments, and personal or political attacks
|
28
|
+
* Public or private harassment
|
29
|
+
* Publishing others' private information, such as a physical or electronic
|
30
|
+
address, without explicit permission
|
31
|
+
* Other conduct which could reasonably be considered inappropriate in a
|
32
|
+
professional setting
|
33
|
+
|
34
|
+
## Our Responsibilities
|
35
|
+
|
36
|
+
Project maintainers are responsible for clarifying the standards of acceptable
|
37
|
+
behavior and are expected to take appropriate and fair corrective action in
|
38
|
+
response to any instances of unacceptable behavior.
|
39
|
+
|
40
|
+
Project maintainers have the right and responsibility to remove, edit, or
|
41
|
+
reject comments, commits, code, wiki edits, issues, and other contributions
|
42
|
+
that are not aligned to this Code of Conduct, or to ban temporarily or
|
43
|
+
permanently any contributor for other behaviors that they deem inappropriate,
|
44
|
+
threatening, offensive, or harmful.
|
45
|
+
|
46
|
+
## Scope
|
47
|
+
|
48
|
+
This Code of Conduct applies both within project spaces and in public spaces
|
49
|
+
when an individual is representing the project or its community. Examples of
|
50
|
+
representing a project or community include using an official project e-mail
|
51
|
+
address, posting via an official social media account, or acting as an appointed
|
52
|
+
representative at an online or offline event. Representation of a project may be
|
53
|
+
further defined and clarified by project maintainers.
|
54
|
+
|
55
|
+
## Enforcement
|
56
|
+
|
57
|
+
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
58
|
+
reported by contacting the project team at davidmorton0@gmail.com. All
|
59
|
+
complaints will be reviewed and investigated and will result in a response that
|
60
|
+
is deemed necessary and appropriate to the circumstances. The project team is
|
61
|
+
obligated to maintain confidentiality with regard to the reporter of an incident.
|
62
|
+
Further details of specific enforcement policies may be posted separately.
|
63
|
+
|
64
|
+
Project maintainers who do not follow or enforce the Code of Conduct in good
|
65
|
+
faith may face temporary or permanent repercussions as determined by other
|
66
|
+
members of the project's leadership.
|
67
|
+
|
68
|
+
## Attribution
|
69
|
+
|
70
|
+
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
|
71
|
+
available at [http://contributor-covenant.org/version/1/4][version]
|
72
|
+
|
73
|
+
[homepage]: http://contributor-covenant.org
|
74
|
+
[version]: http://contributor-covenant.org/version/1/4/
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
LogParser (0.1.0)
|
5
|
+
|
6
|
+
GEM
|
7
|
+
remote: https://rubygems.org/
|
8
|
+
specs:
|
9
|
+
byebug (11.1.1)
|
10
|
+
minitest (5.14.0)
|
11
|
+
rake (10.5.0)
|
12
|
+
|
13
|
+
PLATFORMS
|
14
|
+
x64-mingw32
|
15
|
+
|
16
|
+
DEPENDENCIES
|
17
|
+
LogParser!
|
18
|
+
bundler
|
19
|
+
byebug (~> 11.1)
|
20
|
+
minitest (~> 5.0)
|
21
|
+
rake (~> 10.0)
|
22
|
+
|
23
|
+
BUNDLED WITH
|
24
|
+
2.1.4
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2020 David Morton
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,197 @@
|
|
1
|
+
# Webserver Log Parser - Readme
|
2
|
+
|
3
|
+
## Installing
|
4
|
+
|
5
|
+
`gem install weblog-parser`
|
6
|
+
|
7
|
+
## Usage
|
8
|
+
|
9
|
+
wlparser reads a webserver logfile and counts page visits and unique page views.
|
10
|
+
It uses a command-line interface.
|
11
|
+
|
12
|
+
`wlparser -h`
|
13
|
+
`wlparser --help`
|
14
|
+
|
15
|
+
Shows a list of options
|
16
|
+
|
17
|
+
`wlparser -f logfile.log`
|
18
|
+
`wlparser --file logfile.log`
|
19
|
+
|
20
|
+
Reads a log file and display results:
|
21
|
+
|
22
|
+
`wlparser -m 'logfile1.log logfile2.log'`
|
23
|
+
`wlparser --multiple_files 'logfile1.log logfile2.log'`
|
24
|
+
|
25
|
+
Reads a list of log files in quotes and displays results. All files give
|
26
|
+
using -f or -m options will be read and the output combined.
|
27
|
+
|
28
|
+
If no files are specified, the default file 'webserver.log' will be read.
|
29
|
+
|
30
|
+
`wlparser -c`
|
31
|
+
`wlparser --color`
|
32
|
+
|
33
|
+
Displays colored text output. Colors can be change in Constants.rb.
|
34
|
+
|
35
|
+
`wlparser -C`
|
36
|
+
`wlparser --no_color`
|
37
|
+
|
38
|
+
Disables colored text output.
|
39
|
+
|
40
|
+
`wlparser -v`
|
41
|
+
`wlparser --verbose`
|
42
|
+
|
43
|
+
Shows extra information, including all validation warnings.
|
44
|
+
|
45
|
+
`wlparser -q`
|
46
|
+
`wlparser --quiet`
|
47
|
+
|
48
|
+
Displays minimal information i.e. only important warnings. Will still write
|
49
|
+
information to a file if this option is selected. Disables verbose.
|
50
|
+
|
51
|
+
`wlparser -o`
|
52
|
+
`wlparser --output_file info.txt`
|
53
|
+
|
54
|
+
Writes output to file. Default is 'log_info.txt' if no file chosen, although
|
55
|
+
this will only work if this is the last argument given.
|
56
|
+
|
57
|
+
`wlparser -t`
|
58
|
+
`wlparser --timestamp`
|
59
|
+
|
60
|
+
Adds a timestamp to the output file. If an output file is given that already
|
61
|
+
exists, this is turned on automatically.
|
62
|
+
|
63
|
+
`wlparser -x`
|
64
|
+
`wlparser --text`
|
65
|
+
|
66
|
+
Sets file output format to text, similar to that displayed (default).
|
67
|
+
|
68
|
+
`wlparser -j`
|
69
|
+
`wlparser --json`
|
70
|
+
|
71
|
+
Sets file output format to json.
|
72
|
+
|
73
|
+
`wlparser -4`
|
74
|
+
`wlparser --ip4_validation`
|
75
|
+
|
76
|
+
Validates ip addresses using ip4 format (default).
|
77
|
+
|
78
|
+
`wlparser -6`
|
79
|
+
`wlparser --ip6_validation`
|
80
|
+
|
81
|
+
Validates ip addresses using ip6 format.
|
82
|
+
|
83
|
+
`wlparser -6`
|
84
|
+
`wlparser --ip4ip6_validation`
|
85
|
+
|
86
|
+
Validates ip addresses if it matches either ip4 or ip6 format.
|
87
|
+
|
88
|
+
`wlparser -I`
|
89
|
+
`wlparser --no_ip_validation`
|
90
|
+
|
91
|
+
Does not validate ip addresses, assumes they are all valid.
|
92
|
+
|
93
|
+
`wlparser -p`
|
94
|
+
`wlparser --path_validation`
|
95
|
+
|
96
|
+
Validates webpage path (default).
|
97
|
+
|
98
|
+
`wlparser -P`
|
99
|
+
`wlparser --no_path_validation`
|
100
|
+
|
101
|
+
Does not validate webpage path, assumes they are all valid.
|
102
|
+
|
103
|
+
`wlparser -r`
|
104
|
+
`wlparser --remove_invalid`
|
105
|
+
|
106
|
+
Ignore logs in files if either ip address or path is invalid.
|
107
|
+
|
108
|
+
`wlparser -R`
|
109
|
+
`wlparser --warn_invalid`
|
110
|
+
|
111
|
+
Warns about logs with invalid ip addresss or path, but still reads them
|
112
|
+
(default)
|
113
|
+
|
114
|
+
`wlparser -g`
|
115
|
+
`wlparser --page_visits`
|
116
|
+
|
117
|
+
Displays page visits in results and in text file output (default).
|
118
|
+
|
119
|
+
`wlparser -g`
|
120
|
+
`wlparser --page_visits`
|
121
|
+
|
122
|
+
Does not display page visits in results or text file output.
|
123
|
+
|
124
|
+
`wlparser -u`
|
125
|
+
`wlparser --unique_page_views`
|
126
|
+
|
127
|
+
Displays unique page views in results and in text file output (default).
|
128
|
+
|
129
|
+
`wlparser -U`
|
130
|
+
`wlparser --no_unique_page_views`
|
131
|
+
|
132
|
+
Displays page visits in results and in text file output (default).
|
133
|
+
|
134
|
+
##Log Format
|
135
|
+
|
136
|
+
Logs should be on separate lines.
|
137
|
+
There should be a space separator between the webpage path and the ip address.
|
138
|
+
|
139
|
+
Example log with ip4 address:
|
140
|
+
`\webpage\index 123.123.123.123`
|
141
|
+
|
142
|
+
Logs can use either using ip4 addresses or ip6 addresses.
|
143
|
+
|
144
|
+
ip4 addresses should be valid i.e. between 0.0.0.0 and 255.255.255.255, although
|
145
|
+
you can skip this check.
|
146
|
+
|
147
|
+
Example log with ip6 address:
|
148
|
+
`\webpage\index 1234:1234:1234:1234:1234:1234:1234:1234`
|
149
|
+
|
150
|
+
ip6 addresses can be compressed e.g.
|
151
|
+
|
152
|
+
`\webpage\index 1234:1234::1234`
|
153
|
+
|
154
|
+
## Testing
|
155
|
+
|
156
|
+
Tests can be run using:
|
157
|
+
|
158
|
+
`rake test`
|
159
|
+
|
160
|
+
Tests have been separated into
|
161
|
+
* unit tests - test methods in each class
|
162
|
+
* integration tests - test the whole app
|
163
|
+
* performance - parses a log file with 10,000 logs and a log with 100 logs
|
164
|
+
100 times. Calculates the time taken and logs parsed/second. The log files
|
165
|
+
are a mixture of ip4 and ip6 addresses.
|
166
|
+
|
167
|
+
## App structure
|
168
|
+
|
169
|
+
A class diagram can be found here: https://tinyurl.com/tky2f74
|
170
|
+
Note that the dependencies to Constants are not shown.
|
171
|
+
|
172
|
+
## Executables
|
173
|
+
|
174
|
+
* wlparser - Starts the app
|
175
|
+
|
176
|
+
### Classes
|
177
|
+
|
178
|
+
* Parser - Holds the log information and changes the format
|
179
|
+
* LogReader - Loads files then reads logs. Validates logs, ip addresses and paths
|
180
|
+
* ipValidator - Validates ip addresses
|
181
|
+
* PathValidator - Validates the path for the webpage
|
182
|
+
* OptionHandler - Sets the options from the command line arguments given
|
183
|
+
* Formatter - Formats information for text or display output
|
184
|
+
* OutputProcessor - Assembles the information for output
|
185
|
+
* WarningHandler - Handles the warnings found when parsing the logs
|
186
|
+
|
187
|
+
### Modules
|
188
|
+
|
189
|
+
* LogParser - Calls the methods in order
|
190
|
+
* Constants - Contains default options and other constants used in the app
|
191
|
+
* TestData - Contains the data used in the tests
|
192
|
+
* ColorText - Adds color to text
|
193
|
+
* Version - Gives the current version number
|
194
|
+
|
195
|
+
### Logs
|
196
|
+
|
197
|
+
* test_logs contains log files used in testing
|
data/Rakefile
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
ENV['APP_ENV'] = 'test'
|
2
|
+
require "bundler/gem_tasks"
|
3
|
+
require "rake/testtask"
|
4
|
+
|
5
|
+
Rake::TestTask.new(:test) do |t|
|
6
|
+
t.libs << "test"
|
7
|
+
t.libs << "lib"
|
8
|
+
t.test_files = FileList["test/**/*_test.rb"]
|
9
|
+
end
|
10
|
+
|
11
|
+
ENV['RACK_ENV'] = 'test'
|
12
|
+
task :default => :test
|
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "LogParser"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
data/bin/wlparser
ADDED
data/lib/log_parser.rb
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
Dir.glob(File.join(__dir__, 'log_parser', '*.rb'))
|
4
|
+
.sort
|
5
|
+
.each { |file| require file }
|
6
|
+
|
7
|
+
# main file for app
|
8
|
+
module LogParser
|
9
|
+
def self.parse
|
10
|
+
@options = OptionHandler.new.options
|
11
|
+
|
12
|
+
log_reader = LogReader.new(
|
13
|
+
options: { file_list: @options[:file_list],
|
14
|
+
path_validation: @options[:path_validation],
|
15
|
+
ip_validation: @options[:ip_validation],
|
16
|
+
log_remove: @options[:log_remove] }
|
17
|
+
).load_logs
|
18
|
+
|
19
|
+
parser = Parser.new(log_reader: log_reader,
|
20
|
+
quiet: @options[:quiet],
|
21
|
+
verbose: @options[:verbose])
|
22
|
+
parser.count_views
|
23
|
+
|
24
|
+
output_processor = OutputProcessor.new(parser: parser, options: @options)
|
25
|
+
puts output_processor.output_to_display
|
26
|
+
|
27
|
+
return unless @options[:output_file]
|
28
|
+
|
29
|
+
output_processor.write_to_file(format: @options[:output_format])
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Changes text color
|
4
|
+
module ColorText
|
5
|
+
COLOR_CODE = { black: 30,
|
6
|
+
red: 31,
|
7
|
+
green: 32,
|
8
|
+
yellow: 33,
|
9
|
+
blue: 34,
|
10
|
+
magenta: 35,
|
11
|
+
cyan: 36,
|
12
|
+
gray: 37,
|
13
|
+
white: 38 }.freeze
|
14
|
+
|
15
|
+
def colorize(text, color)
|
16
|
+
"\e[#{COLOR_CODE[color]}m#{text}\e[0m"
|
17
|
+
end
|
18
|
+
|
19
|
+
def colorize_if(text, color, change_color = false)
|
20
|
+
change_color ? colorize(text, color) : text
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Constants file
|
4
|
+
module Constants
|
5
|
+
# Options that can be changed ------------------------------------------------
|
6
|
+
DEFAULT_OPTIONS = { ip_validation: :ip4,
|
7
|
+
path_validation: true,
|
8
|
+
page_visits: true,
|
9
|
+
unique_page_views: true,
|
10
|
+
output_format: :text,
|
11
|
+
file_list: [] }
|
12
|
+
|
13
|
+
DEFAULT_LOG = 'webserver.log'
|
14
|
+
|
15
|
+
OUTPUT_COLORS = { title: :cyan, # can be :black, :red, :green, :yellow,
|
16
|
+
line_break: :yellow, # :blue, :magenta, :cyan, :gray, :white
|
17
|
+
columns: %i[gray green red], # if more columns,
|
18
|
+
log: :cyan, # final color will be used
|
19
|
+
options: :magenta }.freeze
|
20
|
+
|
21
|
+
WARNING_COLORS = { true => :red, # important warnings color
|
22
|
+
false => :yellow, # non-important warnings color
|
23
|
+
:none => :green }.freeze
|
24
|
+
# ---------------------------------------------------------------------------
|
25
|
+
|
26
|
+
OPTION_DESCRIPTIONS = {
|
27
|
+
verbose: ->(value) { "verbose: #{(value || false)}" },
|
28
|
+
quiet: ->(value) { "quiet: #{(value || false)}" },
|
29
|
+
highlighting: ->(value) { "highlighting: #{(value || false)}" },
|
30
|
+
file_list: ->(files) { "file list: #{files ? files.join(', ') : '- '}" },
|
31
|
+
output_file: ->(output_file) { "output file: #{output_file || '- '}" },
|
32
|
+
timestamp: ->(value) { "timestamp: #{(value || false)}" },
|
33
|
+
output_format: ->(format) { "output format: #{format}" },
|
34
|
+
ip_validation: lambda { |validation|
|
35
|
+
"ip validation: #{VALIDATION_NAMES[validation]}"
|
36
|
+
},
|
37
|
+
log_remove: ->(value) { "ignore invalid logs: #{(value || false)}" },
|
38
|
+
path_validation: ->(value) { "path validation: #{(value || false)}" },
|
39
|
+
page_visits: ->(value) { "show page visits: #{(value || false)}" },
|
40
|
+
unique_page_views: lambda { |value|
|
41
|
+
"show unique page views: #{(value || false)}"
|
42
|
+
}
|
43
|
+
}.freeze
|
44
|
+
|
45
|
+
VALIDATION_NAMES = { log: 'log',
|
46
|
+
ip4: 'ip4 address',
|
47
|
+
ip6: 'ip6 address',
|
48
|
+
ip4_ip6: 'ip4/ip6 address',
|
49
|
+
none: 'none',
|
50
|
+
path: 'path' }.freeze
|
51
|
+
|
52
|
+
LOG_WARNINGS = { file: { name: 'File Error', important: true },
|
53
|
+
log: { name: 'Log Format Error', important: true },
|
54
|
+
ip4: { name: 'Ip4 Address Format Error', important: false },
|
55
|
+
ip6: { name: 'Ip6 Address Format Error', important: false },
|
56
|
+
ip4_ip6: { name: 'Ip Address Format Error',
|
57
|
+
important: false },
|
58
|
+
path: { name: 'Path Format Error',
|
59
|
+
important: false } }.freeze
|
60
|
+
|
61
|
+
WARNINGS_JSON = { file: :fileError,
|
62
|
+
log: :logFormatError,
|
63
|
+
ip4: :ip4AddressFormatError,
|
64
|
+
ip6: :ip6AddressFormatError,
|
65
|
+
ip4_ip6: :ipAddressFormatError,
|
66
|
+
path: :pathFormatError }.freeze
|
67
|
+
|
68
|
+
INFO_TITLES = { visits: 'Page Visits',
|
69
|
+
unique_views: 'Unique Page Views' }.freeze
|
70
|
+
|
71
|
+
DESCRIPTORS = { visits: ['', 'visit'],
|
72
|
+
unique_views: ['', 'unique view'] }.freeze
|
73
|
+
|
74
|
+
VALID_ADDRESS = {
|
75
|
+
none: proc { true },
|
76
|
+
ip4: proc { |address| address.match(VALID_IP4) },
|
77
|
+
ip6: proc { |address| address.match(VALID_IP6) },
|
78
|
+
ip4_ip6: proc { |address|
|
79
|
+
address.match(VALID_IP4) || address.match(VALID_IP6)
|
80
|
+
}
|
81
|
+
}.freeze
|
82
|
+
|
83
|
+
VALID_LOG = '\S+\s([a-zA-Z0-9:.]+)'
|
84
|
+
|
85
|
+
VALID_PATH = '^(/[a-zA-Z0-9.$_+\\-!*(),\']+/?)*/?$'
|
86
|
+
|
87
|
+
VALID_IP4 = '^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2' \
|
88
|
+
'[0-4][0-9]|[01]?[0-9][0-9]?)$'
|
89
|
+
|
90
|
+
VALID_IP6 = '^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:)'\
|
91
|
+
'{1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]'\
|
92
|
+
'{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}'\
|
93
|
+
'(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]'\
|
94
|
+
'{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|'\
|
95
|
+
'[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]'\
|
96
|
+
'{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::'\
|
97
|
+
'(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}'\
|
98
|
+
'[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-f'\
|
99
|
+
'A-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.)'\
|
100
|
+
'{3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$'
|
101
|
+
end
|
@@ -0,0 +1,125 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Formats text for output
|
4
|
+
class Formatter
|
5
|
+
include ColorText
|
6
|
+
include Constants
|
7
|
+
|
8
|
+
attr_accessor :output
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
@output = []
|
12
|
+
end
|
13
|
+
|
14
|
+
def add_title(title:, add_color: false)
|
15
|
+
output.push(colorize_if(title, OUTPUT_COLORS[:title], add_color))
|
16
|
+
line_break = colorize_if(('-' * title.length),
|
17
|
+
OUTPUT_COLORS[:line_break], add_color)
|
18
|
+
output.unshift(line_break)
|
19
|
+
output.push(line_break)
|
20
|
+
end
|
21
|
+
|
22
|
+
def add_row(row:, descriptor:, add_color: false)
|
23
|
+
output.push(row.map.with_index { |item, i|
|
24
|
+
colorize_if(add_descriptor(item, descriptor[i]),
|
25
|
+
OUTPUT_COLORS[:columns][i], add_color)
|
26
|
+
}.join(' '))
|
27
|
+
end
|
28
|
+
|
29
|
+
def add_descriptor(item, descriptor)
|
30
|
+
if item.is_a?(Integer) && (descriptor != '')
|
31
|
+
format('%<item>s %<descriptor>s', item: item,
|
32
|
+
descriptor: pluralise(descriptor, item))
|
33
|
+
else
|
34
|
+
item
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def format_info(view_info:, add_color: false)
|
39
|
+
@output = []
|
40
|
+
add_title(title: view_info[:title], add_color: add_color)
|
41
|
+
view_info[:info].sort_by { |page, views| [-views, page] }
|
42
|
+
.each do |row|
|
43
|
+
add_row(row: row,
|
44
|
+
descriptor: view_info[:descriptor],
|
45
|
+
add_color: add_color)
|
46
|
+
end
|
47
|
+
@output
|
48
|
+
end
|
49
|
+
|
50
|
+
def format_log_info(log_info:, add_color: false)
|
51
|
+
files = log_info[:files_read].map { |file| File.absolute_path(file) }
|
52
|
+
.join(",\n")
|
53
|
+
format("\n%<file_info>s\n%<logs_read>s\n%<logs_added>s",
|
54
|
+
file_info: colorize_if("Files read: #{files}",
|
55
|
+
OUTPUT_COLORS[:log], add_color),
|
56
|
+
logs_read: colorize_if("Logs read: #{log_info[:logs_read]}",
|
57
|
+
OUTPUT_COLORS[:log], add_color),
|
58
|
+
logs_added: colorize_if("Logs added: #{log_info[:logs_added]}",
|
59
|
+
OUTPUT_COLORS[:log], add_color))
|
60
|
+
end
|
61
|
+
|
62
|
+
def format_minimal_warnings(warnings:, add_color: false)
|
63
|
+
warnings_list = warnings.filter { |_type, info|
|
64
|
+
info[:important] && !info[:warnings].empty?
|
65
|
+
}.map { |_type, info|
|
66
|
+
color = WARNING_COLORS[true]
|
67
|
+
text = [warning_summary(info[:name], info[:warnings].length),
|
68
|
+
info[:warnings]].join("\n")
|
69
|
+
colorize_if(text, color, add_color)
|
70
|
+
}
|
71
|
+
warnings_list.flatten.join("\n")
|
72
|
+
end
|
73
|
+
|
74
|
+
def format_full_warnings(warnings:, add_color: false)
|
75
|
+
warnings_list = warnings.map do |_type, info|
|
76
|
+
if info[:warnings].empty?
|
77
|
+
color = WARNING_COLORS[:none]
|
78
|
+
text = [warning_summary(info[:name], info[:warnings].length)]
|
79
|
+
else
|
80
|
+
color = WARNING_COLORS[info[:important]]
|
81
|
+
text = [warning_summary(info[:name], info[:warnings].length),
|
82
|
+
info[:warnings]]
|
83
|
+
end
|
84
|
+
colorize_if(text.join("\n"), color, add_color)
|
85
|
+
end
|
86
|
+
warnings_list.flatten.join("\n")
|
87
|
+
end
|
88
|
+
|
89
|
+
def format_normal_warnings(warnings:, add_color: false)
|
90
|
+
warnings_list = warnings.map do |_type, info|
|
91
|
+
if !info[:warnings].empty? && info[:important]
|
92
|
+
color = WARNING_COLORS[true]
|
93
|
+
text = [warning_summary(info[:name], info[:warnings].length),
|
94
|
+
info[:warnings]].join("\n")
|
95
|
+
elsif info[:important]
|
96
|
+
color = WARNING_COLORS[:none]
|
97
|
+
text = [warning_summary(info[:name], info[:warnings].length)].join("\n")
|
98
|
+
else
|
99
|
+
color_key = info[:warnings].empty? ? :none : false
|
100
|
+
color = WARNING_COLORS[color_key]
|
101
|
+
text = [warning_summary(info[:name], info[:warnings].length)].join("\n")
|
102
|
+
end
|
103
|
+
colorize_if(text, color, add_color)
|
104
|
+
end
|
105
|
+
warnings_list.flatten.join("\n")
|
106
|
+
end
|
107
|
+
|
108
|
+
def pluralise(item, number)
|
109
|
+
item + (number != 1 ? 's' : '')
|
110
|
+
end
|
111
|
+
|
112
|
+
def warning_summary(name, number)
|
113
|
+
format('%<name>ss: %<number>d %<warnings>s',
|
114
|
+
name: name, number: number, warnings: pluralise('warning', number))
|
115
|
+
end
|
116
|
+
|
117
|
+
def format_options(options:, add_color: false)
|
118
|
+
options = OPTION_DESCRIPTIONS.map do |k, _v|
|
119
|
+
OPTION_DESCRIPTIONS[k].call(options[k])
|
120
|
+
end
|
121
|
+
text = format("\nOptions selected:\n\n%<options>s\n",
|
122
|
+
options: options.join(",\n"))
|
123
|
+
colorize_if(text, OUTPUT_COLORS[:options], add_color)
|
124
|
+
end
|
125
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Validates ip addresses
|
4
|
+
class IpValidator
|
5
|
+
include Constants
|
6
|
+
|
7
|
+
attr_reader :ip_address, :validation
|
8
|
+
|
9
|
+
def initialize(ip_address:, validation:)
|
10
|
+
@ip_address = ip_address
|
11
|
+
@validation = VALID_ADDRESS[validation || :none]
|
12
|
+
end
|
13
|
+
|
14
|
+
def valid?
|
15
|
+
@validation.call(ip_address)
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,100 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Reads logs
|
4
|
+
class LogReader
|
5
|
+
include Constants
|
6
|
+
|
7
|
+
attr_accessor :logs_read, :logs_added
|
8
|
+
attr_reader :read_log, :warnings, :files_read, :options
|
9
|
+
|
10
|
+
def initialize(options: {})
|
11
|
+
@read_log = Hash.new { |h, k| h[k] = [] }
|
12
|
+
@options = options
|
13
|
+
@warnings = []
|
14
|
+
@logs_read = 0
|
15
|
+
@logs_added = 0
|
16
|
+
@files_read = []
|
17
|
+
@options[:file_list] = [DEFAULT_LOG] if options[:file_list] == []
|
18
|
+
end
|
19
|
+
|
20
|
+
def load_logs
|
21
|
+
options[:file_list].each { |file| load_log(file: file) }
|
22
|
+
self
|
23
|
+
end
|
24
|
+
|
25
|
+
def load_log(file:)
|
26
|
+
begin
|
27
|
+
File.open(file, 'r').each.with_index do |line, i|
|
28
|
+
add_log(log: line, line_number: i + 1, file: file)
|
29
|
+
end
|
30
|
+
@files_read.push(file)
|
31
|
+
rescue
|
32
|
+
warnings.push(type: :file,
|
33
|
+
message: format(' - File not found: %<file>s', file: file))
|
34
|
+
end
|
35
|
+
self
|
36
|
+
end
|
37
|
+
|
38
|
+
def add_log(log:, line_number: 1, file:)
|
39
|
+
@logs_read += 1
|
40
|
+
|
41
|
+
log_valid = valid_log?(log: log)
|
42
|
+
add_warning_if(type: :log, line_number: line_number, file: file,
|
43
|
+
add_if: !log_valid)
|
44
|
+
return self unless log_valid
|
45
|
+
|
46
|
+
path, ip_address = log.split(' ')
|
47
|
+
ip_valid = valid_ip?(ip_address: ip_address)
|
48
|
+
path_valid = !options[:path_validation] || valid_path?(path: path)
|
49
|
+
|
50
|
+
add_warning_if(type: options[:ip_validation], line_number: line_number,
|
51
|
+
file: file, add_if: !ip_valid)
|
52
|
+
add_warning_if(type: :path, line_number: line_number, file: file,
|
53
|
+
add_if: !path_valid)
|
54
|
+
|
55
|
+
if (ip_valid && path_valid) || !options[:log_remove]
|
56
|
+
@logs_added += 1
|
57
|
+
read_log[path].push(ip_address)
|
58
|
+
end
|
59
|
+
self
|
60
|
+
end
|
61
|
+
|
62
|
+
def add_warning_if(type:, line_number:, file:, add_if:)
|
63
|
+
return unless add_if
|
64
|
+
|
65
|
+
warnings.push(type: type,
|
66
|
+
message: log_warning_message(
|
67
|
+
name: VALIDATION_NAMES[type],
|
68
|
+
line_number: line_number,
|
69
|
+
file: file
|
70
|
+
))
|
71
|
+
end
|
72
|
+
|
73
|
+
def log_warning_message(name:, line_number:, file:)
|
74
|
+
format(' - Invalid %<name>s%<line_file>s',
|
75
|
+
name: name,
|
76
|
+
line_file: add_line_and_file_name(line_number, file))
|
77
|
+
end
|
78
|
+
|
79
|
+
def add_line_and_file_name(line_number, file)
|
80
|
+
format('%<file>s - line %<line>d', file: add_file_name(file),
|
81
|
+
line: line_number)
|
82
|
+
end
|
83
|
+
|
84
|
+
def add_file_name(file)
|
85
|
+
format(' - File: %<file>s', file: file)
|
86
|
+
end
|
87
|
+
|
88
|
+
def valid_ip?(ip_address:)
|
89
|
+
IpValidator.new(ip_address: ip_address,
|
90
|
+
validation: options[:ip_validation]).valid?
|
91
|
+
end
|
92
|
+
|
93
|
+
def valid_log?(log:)
|
94
|
+
log.match(VALID_LOG)
|
95
|
+
end
|
96
|
+
|
97
|
+
def valid_path?(path:)
|
98
|
+
PathValidator.new(path: path).valid?
|
99
|
+
end
|
100
|
+
end
|
@@ -0,0 +1,144 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'optparse'
|
4
|
+
|
5
|
+
# Parses command line options
|
6
|
+
class OptionHandler
|
7
|
+
include Constants
|
8
|
+
|
9
|
+
attr_reader :options
|
10
|
+
|
11
|
+
def initialize
|
12
|
+
@options = DEFAULT_OPTIONS.clone
|
13
|
+
@options[:file_list] = []
|
14
|
+
|
15
|
+
begin
|
16
|
+
OptionParser.new do |opts|
|
17
|
+
opts.on('-v', '--verbose', 'Show extra information') do
|
18
|
+
options[:verbose] = true unless options[:quiet]
|
19
|
+
end
|
20
|
+
|
21
|
+
opts.on('-q', '--quiet',
|
22
|
+
'No display except important warnings. Disables verbose') do
|
23
|
+
options[:quiet] = true
|
24
|
+
options[:verbose] = false
|
25
|
+
end
|
26
|
+
|
27
|
+
opts.on('-c', '--color', 'Enables colored display text') do
|
28
|
+
options[:highlighting] = true
|
29
|
+
end
|
30
|
+
|
31
|
+
opts.on('-C', '--no_color',
|
32
|
+
'Disables colored display text (default)') do
|
33
|
+
options[:highlighting] = false
|
34
|
+
end
|
35
|
+
|
36
|
+
opts.on('-f', '--file [FILE]',
|
37
|
+
'Log file to read. Default is webserver.log') do |file|
|
38
|
+
if file
|
39
|
+
options[:file_list].push(file)
|
40
|
+
else
|
41
|
+
unless ENV['APP_ENV'] == 'test'
|
42
|
+
puts 'Missing input file name. Exiting.'
|
43
|
+
end
|
44
|
+
exit 50
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
opts.on('-m', "--multiple_files ['FILE_LIST']",
|
49
|
+
'Read a list of log files in quotes') do |file_list|
|
50
|
+
if file_list
|
51
|
+
options[:file_list] += file_list.split(' ')
|
52
|
+
else
|
53
|
+
unless ENV['APP_ENV'] == 'test'
|
54
|
+
puts 'Missing input file list. Exiting.'
|
55
|
+
end
|
56
|
+
exit 50
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
opts.on('-o', '--output_file [FILE]', 'Write output to file') do |file|
|
61
|
+
options[:output_file] = file || 'log_info.txt'
|
62
|
+
end
|
63
|
+
|
64
|
+
opts.on('-t', '--timestamp', 'Add timestamp to output file') do
|
65
|
+
options[:timestamp] = true
|
66
|
+
end
|
67
|
+
|
68
|
+
opts.on('-x', '--text', 'Sets file output format to text (default)') do
|
69
|
+
options[:output_format] = :text
|
70
|
+
end
|
71
|
+
|
72
|
+
opts.on('-j', '--json', 'Sets file output format to json') do
|
73
|
+
options[:output_format] = :json
|
74
|
+
end
|
75
|
+
|
76
|
+
opts.on('-h', '--help', 'Shows help') do
|
77
|
+
puts opts if ENV['APP_ENV'] != 'test'
|
78
|
+
exit 51
|
79
|
+
end
|
80
|
+
|
81
|
+
opts.on('-4', '--ip4_validation',
|
82
|
+
'Validate ip addresses using ip4 format (default)') do
|
83
|
+
options[:ip_validation] = :ip4
|
84
|
+
end
|
85
|
+
|
86
|
+
opts.on('-6', '--ip6_validation',
|
87
|
+
'Validate ip addresses using ip6 format') do
|
88
|
+
options[:ip_validation] = :ip6
|
89
|
+
end
|
90
|
+
|
91
|
+
opts.on('-i', '--ip4ip6_validation',
|
92
|
+
'Validate ip addresses using either ip4 or ip6 format') do
|
93
|
+
options[:ip_validation] = :ip4_ip6
|
94
|
+
end
|
95
|
+
|
96
|
+
opts.on('-I', '--no_ip_validation',
|
97
|
+
'No validatation of ip addresses') do
|
98
|
+
options[:ip_validation] = :none
|
99
|
+
end
|
100
|
+
|
101
|
+
opts.on('-r', '--remove_invalid',
|
102
|
+
'Ignore log if invalid ip addresss or path') do
|
103
|
+
options[:log_remove] = true
|
104
|
+
end
|
105
|
+
|
106
|
+
opts.on('-R', '--warn_invalid',
|
107
|
+
'Warn but not ignore log if invalid ip address or path
|
108
|
+
(default)') do
|
109
|
+
options[:log_remove] = false
|
110
|
+
end
|
111
|
+
|
112
|
+
opts.on('-p', '--path_validation', 'Validate webpage path (default)') do
|
113
|
+
options[:path_validation] = true
|
114
|
+
end
|
115
|
+
|
116
|
+
opts.on('-P', '--no_path_validation',
|
117
|
+
'Does not validate webpage path') do
|
118
|
+
options[:path_validation] = false
|
119
|
+
end
|
120
|
+
|
121
|
+
opts.on('-g', '--page_visits', 'Show page visits (default)') do
|
122
|
+
options[:page_visits] = true
|
123
|
+
end
|
124
|
+
|
125
|
+
opts.on('-G', '--no_page_visits', 'Do not show page visits') do
|
126
|
+
options[:page_visits] = false
|
127
|
+
end
|
128
|
+
|
129
|
+
opts.on('-u', '--unique_page_views',
|
130
|
+
'Show unique page views (default)') do
|
131
|
+
options[:unique_page_views] = true
|
132
|
+
end
|
133
|
+
|
134
|
+
opts.on('-U', '--no_unique_page_views',
|
135
|
+
'Do not show unique page views') do
|
136
|
+
options[:unique_page_views] = false
|
137
|
+
end
|
138
|
+
end.parse!
|
139
|
+
rescue OptionParser::InvalidOption => e
|
140
|
+
puts e if ENV['APP_ENV'] != 'test'
|
141
|
+
exit
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
# Processes output for display or write to file
|
5
|
+
class OutputProcessor
|
6
|
+
include Constants
|
7
|
+
|
8
|
+
attr_reader :add_color, :parser
|
9
|
+
|
10
|
+
def initialize(parser:, options:)
|
11
|
+
@parser = parser
|
12
|
+
@options = options
|
13
|
+
@add_color = options ? @options[:highlighting] : false
|
14
|
+
end
|
15
|
+
|
16
|
+
def name_output_file
|
17
|
+
if File.exist?(@options[:output_file]) || @options[:timestamp]
|
18
|
+
timestamp_filename(@options[:output_file])
|
19
|
+
else
|
20
|
+
@options[:output_file]
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def timestamp_filename(file)
|
25
|
+
dir = File.dirname(file)
|
26
|
+
base = File.basename(file, '.*')
|
27
|
+
time = Time.now.strftime('%d-%m-%y_%H-%M-%S')
|
28
|
+
ext = File.extname(file)
|
29
|
+
File.join(dir, "#{base}_#{time}#{ext}")
|
30
|
+
end
|
31
|
+
|
32
|
+
def output_to_display
|
33
|
+
output = []
|
34
|
+
if @options[:quiet]
|
35
|
+
output.push(parser.formatted_minimal_warnings(add_color: add_color))
|
36
|
+
else
|
37
|
+
if @options[:page_visits]
|
38
|
+
output.push(parser.formatted_page_views(view_type: :visits,
|
39
|
+
add_color: add_color))
|
40
|
+
end
|
41
|
+
if @options[:unique_page_views]
|
42
|
+
output.push(parser.formatted_page_views(view_type: :unique_views,
|
43
|
+
add_color: add_color))
|
44
|
+
end
|
45
|
+
output.push(parser.formatted_log_info(add_color: add_color))
|
46
|
+
if @options[:verbose]
|
47
|
+
output.unshift(Formatter.new.format_options(options: @options,
|
48
|
+
add_color: add_color))
|
49
|
+
output.push('', parser.formatted_full_warnings(add_color: add_color))
|
50
|
+
else
|
51
|
+
output.push('', parser.formatted_normal_warnings(add_color: add_color))
|
52
|
+
end
|
53
|
+
end
|
54
|
+
output.join("\n")
|
55
|
+
end
|
56
|
+
|
57
|
+
def output_to_file_text
|
58
|
+
output = []
|
59
|
+
if @options[:page_visits]
|
60
|
+
output.push(parser.formatted_page_views(view_type: :visits).join("\n"))
|
61
|
+
end
|
62
|
+
if @options[:unique_page_views]
|
63
|
+
output.push(parser.formatted_page_views(
|
64
|
+
view_type: :unique_views
|
65
|
+
).join("\n"))
|
66
|
+
end
|
67
|
+
output.push(parser.formatted_log_info)
|
68
|
+
if @options[:verbose]
|
69
|
+
output.push(parser.formatted_full_warnings)
|
70
|
+
else
|
71
|
+
output.push(parser.formatted_normal_warnings)
|
72
|
+
end
|
73
|
+
output.join("\n")
|
74
|
+
end
|
75
|
+
|
76
|
+
def output_to_file_json
|
77
|
+
JSON.pretty_generate parser.hash_format(verbose: @options[:verbose])
|
78
|
+
end
|
79
|
+
|
80
|
+
def write_to_file(format:)
|
81
|
+
file = name_output_file
|
82
|
+
f = File.new(file, 'w')
|
83
|
+
format_select = { text: -> { output_to_file_text },
|
84
|
+
json: -> { output_to_file_json } }
|
85
|
+
f.write format_select[format].call
|
86
|
+
f.close
|
87
|
+
|
88
|
+
puts format('Output written to: %<file>s', file: file)
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Parses log information
|
4
|
+
class Parser
|
5
|
+
include Constants
|
6
|
+
|
7
|
+
attr_reader :page_views, :formatter, :log_reader
|
8
|
+
|
9
|
+
def initialize(log_reader: {}, quiet: false, verbose: false)
|
10
|
+
@page_views = {}
|
11
|
+
@log_reader = log_reader
|
12
|
+
@formatter = Formatter.new
|
13
|
+
@warning_handler = nil
|
14
|
+
end
|
15
|
+
|
16
|
+
def warnings
|
17
|
+
WarningHandler.new(warnings: log_info[:warnings])
|
18
|
+
.store_warning_info(warning_info: LOG_WARNINGS)
|
19
|
+
.warnings_summary
|
20
|
+
end
|
21
|
+
|
22
|
+
def count_views(logs: log_reader.read_log)
|
23
|
+
logs.each do |page, ip_addresses|
|
24
|
+
@page_views[page] = { visits: ip_addresses.length,
|
25
|
+
unique_views: ip_addresses.uniq.length }
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def view_info(view_type:)
|
30
|
+
{ title: INFO_TITLES[view_type],
|
31
|
+
descriptor: DESCRIPTORS[view_type],
|
32
|
+
info: @page_views.map { |page, views| [page, views[view_type]] } }
|
33
|
+
end
|
34
|
+
|
35
|
+
def log_info
|
36
|
+
{ files_read: (log_reader == {} ? [] : log_reader.files_read),
|
37
|
+
logs_read: (log_reader == {} ? 0 : log_reader.logs_read),
|
38
|
+
logs_added: (log_reader == {} ? 0 : log_reader.logs_added),
|
39
|
+
warnings: (log_reader == {} ? [] : log_reader.warnings) }
|
40
|
+
end
|
41
|
+
|
42
|
+
def formatted_log_info(add_color: false)
|
43
|
+
formatter.format_log_info(log_info: log_info, add_color: add_color)
|
44
|
+
end
|
45
|
+
|
46
|
+
def formatted_page_views(view_type:, add_color: false)
|
47
|
+
formatter.format_info(view_info: view_info(view_type: view_type),
|
48
|
+
add_color: add_color)
|
49
|
+
end
|
50
|
+
|
51
|
+
def formatted_full_warnings(add_color: false)
|
52
|
+
formatter.format_full_warnings(warnings: warnings, add_color: add_color)
|
53
|
+
end
|
54
|
+
|
55
|
+
def formatted_minimal_warnings(add_color: false)
|
56
|
+
formatter.format_minimal_warnings(warnings: warnings, add_color: add_color)
|
57
|
+
end
|
58
|
+
|
59
|
+
def formatted_normal_warnings(add_color: false)
|
60
|
+
formatter.format_normal_warnings(warnings: warnings, add_color: add_color)
|
61
|
+
end
|
62
|
+
|
63
|
+
def hash_format(verbose:)
|
64
|
+
output = {}
|
65
|
+
output['filesRead'] = log_info[:files_read]
|
66
|
+
output['logsRead'] = log_info[:logs_read]
|
67
|
+
output['logsAdded'] = log_info[:logs_added]
|
68
|
+
output['pageVisits'] = {}
|
69
|
+
output['uniquePageViews'] = {}
|
70
|
+
page_views.each do |page, views|
|
71
|
+
output['pageVisits'][page] = views[:visits]
|
72
|
+
output['uniquePageViews'][page] = views[:unique_views]
|
73
|
+
end
|
74
|
+
if verbose
|
75
|
+
warning_summary = warnings.map do |type, info|
|
76
|
+
{ WARNINGS_JSON[type] => {
|
77
|
+
'numberWarnings': info[:warnings].length,
|
78
|
+
'warnings': info[:warnings]
|
79
|
+
} }
|
80
|
+
end
|
81
|
+
else
|
82
|
+
warning_summary = warnings.map do |type, info|
|
83
|
+
if info[:important]
|
84
|
+
{ WARNINGS_JSON[type] => {
|
85
|
+
'numberWarnings': info[:warnings].length,
|
86
|
+
'messages': info[:warnings]
|
87
|
+
} }
|
88
|
+
else
|
89
|
+
{ WARNINGS_JSON[type] => {
|
90
|
+
'numberWarnings': info[:warnings].length
|
91
|
+
} }
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
output['warnings'] = warning_summary
|
96
|
+
output
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Handles warnings found when reading logs
|
4
|
+
class WarningHandler
|
5
|
+
include Constants
|
6
|
+
|
7
|
+
attr_reader :warnings, :warning_info
|
8
|
+
|
9
|
+
def initialize(warnings: [])
|
10
|
+
@warnings = warnings
|
11
|
+
end
|
12
|
+
|
13
|
+
def store_warning_info(warning_info: {})
|
14
|
+
@warning_info = warning_info
|
15
|
+
self
|
16
|
+
end
|
17
|
+
|
18
|
+
def warnings_summary
|
19
|
+
summary = {}
|
20
|
+
warning_info.each do |type, info|
|
21
|
+
summary[type] = {}
|
22
|
+
summary[type][:name] = info[:name]
|
23
|
+
summary[type][:important] = info[:important]
|
24
|
+
summary[type][:warnings] = @warnings
|
25
|
+
.filter { |warning| warning[:type] == type }
|
26
|
+
.map { |warning| warning[:message] }
|
27
|
+
end
|
28
|
+
summary
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
lib = File.expand_path("../lib", __FILE__)
|
2
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
3
|
+
require "log_parser/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = "weblog-parser"
|
7
|
+
spec.version = LogParser::VERSION
|
8
|
+
spec.authors = ["David Morton"]
|
9
|
+
spec.email = ["davidmorton0@gmail.com"]
|
10
|
+
|
11
|
+
spec.summary = %q{A command line parser for web server logs}
|
12
|
+
spec.homepage = "https://github.com/davidmorton0/LogParser"
|
13
|
+
spec.license = "MIT"
|
14
|
+
|
15
|
+
# Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
|
16
|
+
# to allow pushing to a single host or delete this section to allow pushing to any host.
|
17
|
+
if spec.respond_to?(:metadata)
|
18
|
+
#spec.metadata["allowed_push_host"] = "TODO: Set to 'http://mygemserver.com'"
|
19
|
+
|
20
|
+
#spec.metadata["homepage_uri"] = spec.homepage
|
21
|
+
spec.metadata["source_code_uri"] = "https://github.com/davidmorton0/LogParser"
|
22
|
+
#spec.metadata["changelog_uri"] = "TODO: Put your gem's CHANGELOG.md URL here."
|
23
|
+
else
|
24
|
+
raise "RubyGems 2.0 or newer is required to protect against " \
|
25
|
+
"public gem pushes."
|
26
|
+
end
|
27
|
+
|
28
|
+
# Specify which files should be added to the gem when it is released.
|
29
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
30
|
+
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
31
|
+
f.match(%r{^(test|spec|features)/})
|
32
|
+
end
|
33
|
+
spec.bindir = "bin"
|
34
|
+
spec.executables = ["wlparser"]
|
35
|
+
spec.require_paths = ["lib"]
|
36
|
+
|
37
|
+
spec.add_development_dependency "bundler", "~> 2.1.4"
|
38
|
+
spec.add_development_dependency "minitest", "~> 5.0"
|
39
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
40
|
+
end
|
metadata
ADDED
@@ -0,0 +1,111 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: weblog-parser
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- David Morton
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2020-02-14 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 2.1.4
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 2.1.4
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: minitest
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '5.0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '5.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rake
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '10.0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '10.0'
|
55
|
+
description:
|
56
|
+
email:
|
57
|
+
- davidmorton0@gmail.com
|
58
|
+
executables:
|
59
|
+
- wlparser
|
60
|
+
extensions: []
|
61
|
+
extra_rdoc_files: []
|
62
|
+
files:
|
63
|
+
- ".gitignore"
|
64
|
+
- ".travis.yml"
|
65
|
+
- CODE_OF_CONDUCT.md
|
66
|
+
- Gemfile
|
67
|
+
- Gemfile.lock
|
68
|
+
- LICENSE.txt
|
69
|
+
- README.md
|
70
|
+
- Rakefile
|
71
|
+
- bin/console
|
72
|
+
- bin/setup
|
73
|
+
- bin/wlparser
|
74
|
+
- lib/log_parser.rb
|
75
|
+
- lib/log_parser/color_text.rb
|
76
|
+
- lib/log_parser/constants.rb
|
77
|
+
- lib/log_parser/formatter.rb
|
78
|
+
- lib/log_parser/ip_validator.rb
|
79
|
+
- lib/log_parser/log_reader.rb
|
80
|
+
- lib/log_parser/option_handler.rb
|
81
|
+
- lib/log_parser/output_processor.rb
|
82
|
+
- lib/log_parser/parser.rb
|
83
|
+
- lib/log_parser/path_validator.rb
|
84
|
+
- lib/log_parser/version.rb
|
85
|
+
- lib/log_parser/warning_handler.rb
|
86
|
+
- weblog_parser.gemspec
|
87
|
+
homepage: https://github.com/davidmorton0/LogParser
|
88
|
+
licenses:
|
89
|
+
- MIT
|
90
|
+
metadata:
|
91
|
+
source_code_uri: https://github.com/davidmorton0/LogParser
|
92
|
+
post_install_message:
|
93
|
+
rdoc_options: []
|
94
|
+
require_paths:
|
95
|
+
- lib
|
96
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
97
|
+
requirements:
|
98
|
+
- - ">="
|
99
|
+
- !ruby/object:Gem::Version
|
100
|
+
version: '0'
|
101
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
102
|
+
requirements:
|
103
|
+
- - ">="
|
104
|
+
- !ruby/object:Gem::Version
|
105
|
+
version: '0'
|
106
|
+
requirements: []
|
107
|
+
rubygems_version: 3.0.3
|
108
|
+
signing_key:
|
109
|
+
specification_version: 4
|
110
|
+
summary: A command line parser for web server logs
|
111
|
+
test_files: []
|