kubetailrb 0.1.0 → 0.2.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 +4 -4
- data/CHANGELOG.md +13 -1
- data/README.md +30 -4
- data/Rakefile +11 -0
- data/journey_log.md +1 -7
- data/lib/kubetailrb/cmd/file.rb +2 -2
- data/lib/kubetailrb/cmd/help.rb +9 -8
- data/lib/kubetailrb/cmd/k8s.rb +55 -10
- data/lib/kubetailrb/filter/log_filter.rb +62 -0
- data/lib/kubetailrb/formatter/json_formatter.rb +72 -0
- data/lib/kubetailrb/formatter/no_op_formatter.rb +12 -0
- data/lib/kubetailrb/formatter/pod_metadata_formatter.rb +32 -0
- data/lib/kubetailrb/k8s_opts.rb +10 -2
- data/lib/kubetailrb/painter.rb +32 -0
- data/lib/kubetailrb/reader/file_reader.rb +124 -0
- data/lib/kubetailrb/reader/k8s_pod_reader.rb +115 -0
- data/lib/kubetailrb/reader/k8s_pods_reader.rb +123 -0
- data/lib/kubetailrb/reader/with_k8s_client.rb +27 -0
- data/lib/kubetailrb/validated.rb +4 -0
- data/lib/kubetailrb/version.rb +1 -1
- metadata +19 -16
- data/lib/kubetailrb/file_reader.rb +0 -122
- data/lib/kubetailrb/json_formatter.rb +0 -66
- data/lib/kubetailrb/k8s_pod_reader.rb +0 -83
- data/lib/kubetailrb/k8s_pods_reader.rb +0 -86
- data/lib/kubetailrb/no_op_formatter.rb +0 -10
- data/lib/kubetailrb/with_k8s_client.rb +0 -25
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 427b49a8499da9f1c69481130560ba2b783dc122b1a89146e4b2e6ce2f516ec9
|
4
|
+
data.tar.gz: 6be87b2d3529725ef21c2acaee397ddf1989dec3e91bd98f9b99494c6531b9e2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a63a5917935cda345eab9b35f918efdd84778bc58ababf1ede52425cbb71760ac40738cf24ae11596b0c71b373c7aee00b25ec4fc343d0b58deb226e072919f6
|
7
|
+
data.tar.gz: '08b4cb3695fb7704010a0bf868fa3465fe6ee7f0a1b5dafc5e90de3fd83295e8b266aec4555ab2f2840becffcb9f5137564781dbfa8edd69698dff091ebb5d98'
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,17 @@
|
|
1
|
-
## [
|
1
|
+
## [0.2.0] - 2024-12-05
|
2
|
+
|
3
|
+
- remove `--pretty` flag
|
4
|
+
- now by default, it's displayed in pretty mode
|
5
|
+
- to display the log without formatting, use `--raw` flag
|
6
|
+
- add possibility to filter out the access and datadog logs
|
7
|
+
- support displaying stack trace in pretty mode
|
8
|
+
- support formatting rails logs
|
9
|
+
- fix color when receiving HTTP 500 in access logs
|
10
|
+
- add `--display-names` flag to display the pod and container names
|
11
|
+
- colorize pod events (blue for new pod events, red for deleted pod events)
|
2
12
|
|
3
13
|
## [0.1.0] - 2024-10-27
|
4
14
|
|
5
15
|
- Initial release
|
16
|
+
|
17
|
+
## [Unreleased]
|
data/README.md
CHANGED
@@ -1,5 +1,8 @@
|
|
1
1
|
# Kubetailrb
|
2
2
|
|
3
|
+
[](https://github.com/HazAT/badge/blob/master/LICENSE)
|
4
|
+
[](http://rubygems.org/gems/kubetailrb)
|
5
|
+
|
3
6
|
> Tail your Kubernetes pod logs at the same time.
|
4
7
|
|
5
8
|
> [!NOTE]
|
@@ -16,6 +19,12 @@
|
|
16
19
|
|
17
20
|
## Installation
|
18
21
|
|
22
|
+
```sh
|
23
|
+
gem install kubetailrb
|
24
|
+
```
|
25
|
+
|
26
|
+
If you want to install directly from the repository instead:
|
27
|
+
|
19
28
|
```sh
|
20
29
|
# Install dependencies.
|
21
30
|
./bin/setup
|
@@ -31,16 +40,18 @@ bundle exec rake install
|
|
31
40
|
kubetailrb -h
|
32
41
|
|
33
42
|
# follow pod logs
|
34
|
-
kubetailrb 'clock' --namespace sandbox
|
43
|
+
kubetailrb 'clock' --namespace sandbox --raw
|
35
44
|
|
36
45
|
# follow pod structured JSON logs and display in human friendly way
|
37
|
-
kubetailrb 'clock-json' --namespace sandbox --
|
46
|
+
kubetailrb 'clock-json' --namespace sandbox --follow
|
38
47
|
# or with shorter flags
|
39
|
-
kubetailrb 'clock-json' -n sandbox -
|
48
|
+
kubetailrb 'clock-json' -n sandbox -f
|
40
49
|
|
41
50
|
# you can filter the pods using regex on the pod names
|
42
|
-
kubetailrb '^clock(?!-json)' -n sandbox -
|
51
|
+
kubetailrb '^clock(?!-json)' -n sandbox -f
|
43
52
|
|
53
|
+
# you can also filter the containers using regex on the container names
|
54
|
+
kubetailrb 'clock' -n sandbox -f -c 'json'
|
44
55
|
```
|
45
56
|
|
46
57
|
## Development
|
@@ -64,6 +75,21 @@ bundle exec rake test:watch
|
|
64
75
|
bundle exec rake
|
65
76
|
```
|
66
77
|
|
78
|
+
## Release a new version
|
79
|
+
|
80
|
+
Update the version in
|
81
|
+
[`lib/kubetailrb/version.rb`](./lib/kubetailrb/version.rb).
|
82
|
+
|
83
|
+
> [!WARNING]
|
84
|
+
> You may have to update the tests...
|
85
|
+
> Too lazy to update the script to also update the tests...
|
86
|
+
|
87
|
+
Then execute the script:
|
88
|
+
|
89
|
+
```sh
|
90
|
+
./bin/release
|
91
|
+
```
|
92
|
+
|
67
93
|
## Contributing
|
68
94
|
|
69
95
|
Bug reports and pull requests are welcome on GitHub at https://github.com/l-lin/kubetailrb.
|
data/Rakefile
CHANGED
@@ -66,6 +66,17 @@ namespace :k8s do
|
|
66
66
|
puts `kubectl run #{app_name} --image #{docker_image}`
|
67
67
|
end
|
68
68
|
|
69
|
+
desc 'Delete application from k8s.'
|
70
|
+
task :delete, [:app_name] do |_, args|
|
71
|
+
next unless k8s_up?
|
72
|
+
|
73
|
+
app_name = args[:app_name]
|
74
|
+
|
75
|
+
next if app_name.nil? || app_name.strip.empty?
|
76
|
+
|
77
|
+
puts `kubectl delete po #{app_name} --force true` if pod_up?(app_name)
|
78
|
+
end
|
79
|
+
|
69
80
|
desc 'Deploy all applications to k8s.'
|
70
81
|
task :deploy_all do
|
71
82
|
Rake::Task['k8s:deploy'].invoke('clock')
|
data/journey_log.md
CHANGED
@@ -4,12 +4,6 @@
|
|
4
4
|
> tribulations, and triumphs as I navigated the world of this dynamic
|
5
5
|
> programming language.
|
6
6
|
|
7
|
-
|
8
|
-
## 🤔 Things that I'm curious about
|
9
|
-
|
10
|
-
|
11
|
-
---
|
12
|
-
|
13
7
|
## 2024-10-27
|
14
8
|
### Context
|
15
9
|
|
@@ -90,7 +84,7 @@ It seems the convention is:
|
|
90
84
|
- `features` contains the cucumber scenarios, i.e. integration tests.
|
91
85
|
- `bin/` contains some scripts that can help the developer experience,
|
92
86
|
- Rails projects also have scripts in this `bin/` directory.
|
93
|
-
- `
|
87
|
+
- `exe/` contains the executables that will be installed to the user system if
|
94
88
|
the latter is installing the gem
|
95
89
|
- It seems to be a convention from Bundler, but that is configurable in the
|
96
90
|
`gemspec` file.
|
data/lib/kubetailrb/cmd/file.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require 'kubetailrb/file_reader'
|
3
|
+
require 'kubetailrb/reader/file_reader'
|
4
4
|
|
5
5
|
module Kubetailrb
|
6
6
|
module Cmd
|
@@ -13,7 +13,7 @@ module Kubetailrb
|
|
13
13
|
attr_reader :reader
|
14
14
|
|
15
15
|
def initialize(filepath:, last_nb_lines: DEFAULT_NB_LINES, follow: DEFAULT_FOLLOW)
|
16
|
-
@reader = Kubetailrb::FileReader.new(filepath: filepath, last_nb_lines: last_nb_lines, follow: follow)
|
16
|
+
@reader = Kubetailrb::Reader::FileReader.new(filepath: filepath, last_nb_lines: last_nb_lines, follow: follow)
|
17
17
|
end
|
18
18
|
|
19
19
|
def execute
|
data/lib/kubetailrb/cmd/help.rb
CHANGED
@@ -14,14 +14,15 @@ module Kubetailrb
|
|
14
14
|
kubetailrb pod-query [flags]
|
15
15
|
|
16
16
|
Flags:
|
17
|
-
-v, --version
|
18
|
-
-h, --help
|
19
|
-
--tail
|
20
|
-
-f, --follow
|
21
|
-
--file
|
22
|
-
-
|
23
|
-
|
24
|
-
-n, --namespace
|
17
|
+
-v, --version Display version.
|
18
|
+
-h, --help Display help.
|
19
|
+
--tail The number of lines from the end of the logs to show. Defaults to 10.
|
20
|
+
-f, --follow Output appended data as the file grows.
|
21
|
+
--file Display file content.
|
22
|
+
-r, --raw Only display pod logs, without any special formatting.
|
23
|
+
--display-names Display pod and container names.
|
24
|
+
-n, --namespace Kubernetes namespace to use.
|
25
|
+
-c, --container Container name when multiple containers in pod. Default to '.'.
|
25
26
|
HELP
|
26
27
|
end
|
27
28
|
|
data/lib/kubetailrb/cmd/k8s.rb
CHANGED
@@ -1,8 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require 'kubetailrb/k8s_pods_reader'
|
4
|
-
require 'kubetailrb/json_formatter'
|
5
|
-
require 'kubetailrb/no_op_formatter'
|
3
|
+
require 'kubetailrb/reader/k8s_pods_reader'
|
6
4
|
|
7
5
|
module Kubetailrb
|
8
6
|
module Cmd
|
@@ -10,17 +8,25 @@ module Kubetailrb
|
|
10
8
|
class K8s
|
11
9
|
DEFAULT_NB_LINES = 10
|
12
10
|
DEFAULT_NAMESPACE = 'default'
|
11
|
+
DEFAULT_CONTAINER_QUERY = '.*'
|
13
12
|
|
14
13
|
NAMESPACE_FLAGS = %w[-n --namespace].freeze
|
15
14
|
TAIL_FLAG = '--tail'
|
16
15
|
FOLLOW_FLAGS = %w[-f --follow].freeze
|
17
16
|
RAW_FLAGS = %w[-r --raw].freeze
|
18
|
-
|
17
|
+
DISPLAY_NAMES_FLAG = '--display-names'
|
18
|
+
|
19
|
+
CONTAINER_FLAGS = %w[-c --container].freeze
|
20
|
+
EXCLUDE_FLAGS = %w[-e --exclude].freeze
|
19
21
|
|
20
22
|
attr_reader :reader
|
21
23
|
|
22
|
-
def initialize(pod_query:,
|
23
|
-
@reader = Kubetailrb::K8sPodsReader.new(
|
24
|
+
def initialize(pod_query:, container_query:, opts:)
|
25
|
+
@reader = Kubetailrb::Reader::K8sPodsReader.new(
|
26
|
+
pod_query: pod_query,
|
27
|
+
container_query: container_query,
|
28
|
+
opts: opts
|
29
|
+
)
|
24
30
|
end
|
25
31
|
|
26
32
|
def execute
|
@@ -31,12 +37,14 @@ module Kubetailrb
|
|
31
37
|
def create(*args)
|
32
38
|
new(
|
33
39
|
pod_query: parse_pod_query(*args),
|
34
|
-
|
40
|
+
container_query: parse_container_query(*args),
|
35
41
|
opts: K8sOpts.new(
|
36
42
|
namespace: parse_namespace(*args),
|
37
43
|
last_nb_lines: parse_nb_lines(*args),
|
38
44
|
follow: parse_follow(*args),
|
39
|
-
raw: parse_raw(*args)
|
45
|
+
raw: parse_raw(*args),
|
46
|
+
display_names: parse_display_names(*args),
|
47
|
+
exclude: parse_exclude(*args)
|
40
48
|
)
|
41
49
|
)
|
42
50
|
end
|
@@ -109,8 +117,39 @@ module Kubetailrb
|
|
109
117
|
args.any? { |arg| RAW_FLAGS.include?(arg) }
|
110
118
|
end
|
111
119
|
|
112
|
-
def
|
113
|
-
args.any? { |arg|
|
120
|
+
def parse_container_query(*args)
|
121
|
+
return DEFAULT_CONTAINER_QUERY unless args.any? { |arg| CONTAINER_FLAGS.include?(arg) }
|
122
|
+
|
123
|
+
index = args.find_index { |arg| CONTAINER_FLAGS.include?(arg) }.to_i
|
124
|
+
|
125
|
+
raise MissingContainerQueryValueError, "Missing #{CONTAINER_FLAGS} value." if args[index + 1].nil?
|
126
|
+
|
127
|
+
args[index + 1]
|
128
|
+
end
|
129
|
+
|
130
|
+
def parse_display_names(*args)
|
131
|
+
args.include?(DISPLAY_NAMES_FLAG)
|
132
|
+
end
|
133
|
+
|
134
|
+
#
|
135
|
+
# Parse log exclusion from arguments provided in the CLI, e.g.
|
136
|
+
#
|
137
|
+
# kubetailrb some-pod --exclude access-logs,dd-logs
|
138
|
+
#
|
139
|
+
# will return [access-logs, dd-logs].
|
140
|
+
#
|
141
|
+
# Will raise `MissingExcludeValueError` if the value is not provided:
|
142
|
+
#
|
143
|
+
# kubetailrb some-pod --exclude
|
144
|
+
#
|
145
|
+
def parse_exclude(*args)
|
146
|
+
return [] unless args.any? { |arg| EXCLUDE_FLAGS.include?(arg) }
|
147
|
+
|
148
|
+
index = args.find_index { |arg| EXCLUDE_FLAGS.include?(arg) }.to_i
|
149
|
+
|
150
|
+
raise MissingExcludeValueError, "Missing #{EXCLUDE_FLAGS} value." if args[index + 1].nil?
|
151
|
+
|
152
|
+
args[index + 1].split(',')
|
114
153
|
end
|
115
154
|
end
|
116
155
|
end
|
@@ -121,6 +160,12 @@ module Kubetailrb
|
|
121
160
|
class MissingNamespaceValueError < RuntimeError
|
122
161
|
end
|
123
162
|
|
163
|
+
class MissingContainerQueryValueError < RuntimeError
|
164
|
+
end
|
165
|
+
|
166
|
+
class MissingExcludeValueError < RuntimeError
|
167
|
+
end
|
168
|
+
|
124
169
|
class InvalidNbLinesValueError < RuntimeError
|
125
170
|
end
|
126
171
|
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'kubetailrb/validated'
|
4
|
+
|
5
|
+
module Kubetailrb
|
6
|
+
module Filter
|
7
|
+
# Filter the logs that we do not want to see.
|
8
|
+
# Currently only supporting excluding access logs and datadog logs.
|
9
|
+
class LogFilter
|
10
|
+
include Validated
|
11
|
+
|
12
|
+
def self.create(exclude)
|
13
|
+
new(exclude.include?('access-logs'), exclude.include?('dd-logs'))
|
14
|
+
end
|
15
|
+
|
16
|
+
def initialize(exclude_access_logs, exclude_dd_logs)
|
17
|
+
@exclude_access_logs = exclude_access_logs
|
18
|
+
@exclude_dd_logs = exclude_dd_logs
|
19
|
+
|
20
|
+
validate
|
21
|
+
end
|
22
|
+
|
23
|
+
# Returns true if the log should be print, false otherwise.
|
24
|
+
def test(log)
|
25
|
+
return false if @exclude_access_logs && access_log?(log)
|
26
|
+
return false if @exclude_dd_logs && dd_log?(log)
|
27
|
+
|
28
|
+
true
|
29
|
+
end
|
30
|
+
|
31
|
+
def exclude_access_logs?
|
32
|
+
@exclude_access_logs
|
33
|
+
end
|
34
|
+
|
35
|
+
def exclude_dd_logs?
|
36
|
+
@exclude_dd_logs
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def validate
|
42
|
+
validate_boolean @exclude_access_logs, "Invalid exclude_access_logs: #{@exclude_access_logs}."
|
43
|
+
validate_boolean @exclude_dd_logs, "Invalid exclude_dd_logs: #{@exclude_dd_logs}."
|
44
|
+
end
|
45
|
+
|
46
|
+
def access_log?(log)
|
47
|
+
json = JSON.parse(log)
|
48
|
+
# NOTE: Shall I mutualize this function, as it's also used in
|
49
|
+
# JsonFormatter? It's only implemented in 2 places... Maybe I shall wait
|
50
|
+
# until there's a third one before applying DRY.
|
51
|
+
json.include?('http.response.status_code') || json.include?('http_status')
|
52
|
+
rescue JSON::ParserError
|
53
|
+
false
|
54
|
+
end
|
55
|
+
|
56
|
+
def dd_log?(log)
|
57
|
+
# NOTE: Is there's a better way to detect if it's a datadog log?
|
58
|
+
log.include?('[dd') || log.include?('[datadog]')
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'kubetailrb/painter'
|
4
|
+
|
5
|
+
module Kubetailrb
|
6
|
+
module Formatter
|
7
|
+
# Format JSON to human readable.
|
8
|
+
class JsonFormatter
|
9
|
+
include Painter
|
10
|
+
|
11
|
+
def format(log)
|
12
|
+
json = JSON.parse(log)
|
13
|
+
|
14
|
+
return format_access_log(json) if access_log?(json)
|
15
|
+
|
16
|
+
format_application_log(json)
|
17
|
+
rescue JSON::ParserError
|
18
|
+
log
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def access_log?(json)
|
24
|
+
json.include?('http.response.status_code') || json.include?('http_status')
|
25
|
+
end
|
26
|
+
|
27
|
+
def format_access_log(json)
|
28
|
+
"#{json["@timestamp"]}#{http_status_code json}#{http_method json} #{url_path json}"
|
29
|
+
end
|
30
|
+
|
31
|
+
def format_application_log(json)
|
32
|
+
"#{json["@timestamp"]}#{log_level json}#{json["message"]}#{format_stack_trace json}"
|
33
|
+
end
|
34
|
+
|
35
|
+
def format_stack_trace(json)
|
36
|
+
stack_trace = json['error.stack_trace']
|
37
|
+
|
38
|
+
return '' if stack_trace.nil? || stack_trace.strip&.empty?
|
39
|
+
|
40
|
+
"\n#{stack_trace}"
|
41
|
+
end
|
42
|
+
|
43
|
+
def http_status_code(json)
|
44
|
+
code = json['http.response.status_code'] || json['http_status']
|
45
|
+
|
46
|
+
return " #{highlight_blue(" I ")} [#{code}] " if code >= 200 && code < 400
|
47
|
+
return " #{highlight_yellow(" W ")} [#{code}] " if code >= 400 && code < 500
|
48
|
+
return " #{highlight_red(" E ")} [#{code}] " if code >= 500
|
49
|
+
|
50
|
+
" #{code} "
|
51
|
+
end
|
52
|
+
|
53
|
+
def log_level(json)
|
54
|
+
level = json['log.level'] || json.dig('log', 'level')
|
55
|
+
return ' ' if level.nil? || level.strip.empty?
|
56
|
+
return " #{highlight_blue(" I ")} " if level == 'INFO'
|
57
|
+
return " #{highlight_yellow(" W ")} " if level == 'WARN'
|
58
|
+
return " #{highlight_red(" E ")} " if level == 'ERROR'
|
59
|
+
|
60
|
+
" #{level} "
|
61
|
+
end
|
62
|
+
|
63
|
+
def http_method(json)
|
64
|
+
json['http.request.method'] || json['http_method']
|
65
|
+
end
|
66
|
+
|
67
|
+
def url_path(json)
|
68
|
+
json['url.path'] || json['http_path']
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'kubetailrb/validated'
|
4
|
+
|
5
|
+
module Kubetailrb
|
6
|
+
module Formatter
|
7
|
+
# Display the pod and container name.
|
8
|
+
class PodMetadataFormatter
|
9
|
+
include Validated
|
10
|
+
|
11
|
+
def initialize(pod_name, container_name, formatter)
|
12
|
+
@pod_name = pod_name
|
13
|
+
@container_name = container_name
|
14
|
+
@formatter = formatter
|
15
|
+
|
16
|
+
validate
|
17
|
+
end
|
18
|
+
|
19
|
+
def format(log)
|
20
|
+
"#{@pod_name}/#{@container_name} | #{@formatter.format(log)}"
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def validate
|
26
|
+
raise_if_blank @pod_name, 'Pod name not set.'
|
27
|
+
raise_if_blank @container_name, 'Container name not set.'
|
28
|
+
raise_if_nil @formatter, 'Formatter not set.'
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
data/lib/kubetailrb/k8s_opts.rb
CHANGED
@@ -7,13 +7,15 @@ module Kubetailrb
|
|
7
7
|
class K8sOpts
|
8
8
|
include Validated
|
9
9
|
|
10
|
-
attr_reader :namespace, :last_nb_lines
|
10
|
+
attr_reader :namespace, :last_nb_lines, :exclude
|
11
11
|
|
12
|
-
def initialize(namespace:, last_nb_lines:, follow:, raw:)
|
12
|
+
def initialize(namespace:, last_nb_lines:, follow:, raw:, display_names:, exclude:) # rubocop:disable Metrics/ParameterLists
|
13
13
|
@namespace = namespace
|
14
14
|
@last_nb_lines = last_nb_lines
|
15
15
|
@follow = follow
|
16
16
|
@raw = raw
|
17
|
+
@display_names = display_names
|
18
|
+
@exclude = exclude
|
17
19
|
|
18
20
|
validate
|
19
21
|
end
|
@@ -26,6 +28,10 @@ module Kubetailrb
|
|
26
28
|
@raw
|
27
29
|
end
|
28
30
|
|
31
|
+
def display_names?
|
32
|
+
@display_names
|
33
|
+
end
|
34
|
+
|
29
35
|
private
|
30
36
|
|
31
37
|
def validate
|
@@ -33,6 +39,8 @@ module Kubetailrb
|
|
33
39
|
validate_last_nb_lines @last_nb_lines
|
34
40
|
validate_boolean @follow, "Invalid follow: #{@follow}."
|
35
41
|
validate_boolean @raw, "Invalid raw: #{@raw}."
|
42
|
+
validate_boolean @display_names, "Invalid display names: #{@display_names}."
|
43
|
+
raise_if_nil @exclude, 'Exclude not set'
|
36
44
|
end
|
37
45
|
end
|
38
46
|
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Kubetailrb
|
4
|
+
# Add behaviors to colorize console output.
|
5
|
+
module Painter
|
6
|
+
def blue(text)
|
7
|
+
colorize(text, '34')
|
8
|
+
end
|
9
|
+
|
10
|
+
def red(text)
|
11
|
+
colorize(text, '31')
|
12
|
+
end
|
13
|
+
|
14
|
+
def highlight_blue(text)
|
15
|
+
colorize(text, '1;30;44')
|
16
|
+
end
|
17
|
+
|
18
|
+
def highlight_yellow(text)
|
19
|
+
colorize(text, '1;30;43')
|
20
|
+
end
|
21
|
+
|
22
|
+
def highlight_red(text)
|
23
|
+
colorize(text, '1;30;41')
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def colorize(text, color_code)
|
29
|
+
"\e[#{color_code}m#{text}\e[0m"
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,124 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'kubetailrb/validated'
|
4
|
+
|
5
|
+
module Kubetailrb
|
6
|
+
module Reader
|
7
|
+
# Read file content
|
8
|
+
class FileReader
|
9
|
+
include Validated
|
10
|
+
|
11
|
+
attr_reader :filepath, :last_nb_lines
|
12
|
+
|
13
|
+
def initialize(filepath:, last_nb_lines:, follow:)
|
14
|
+
@filepath = filepath
|
15
|
+
@last_nb_lines = last_nb_lines
|
16
|
+
@follow = follow
|
17
|
+
|
18
|
+
validate
|
19
|
+
end
|
20
|
+
|
21
|
+
def read
|
22
|
+
# naive_read
|
23
|
+
read_with_fd
|
24
|
+
end
|
25
|
+
|
26
|
+
# NOTE: Is there something like `attr_reader` but for boolean?
|
27
|
+
# Nope, Ruby does not know if a variable is a boolean or not. So it cannot
|
28
|
+
# create a dedicated method for those booleans.
|
29
|
+
def follow?
|
30
|
+
@follow
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def validate
|
36
|
+
raise NoSuchFileError, "#{@filepath} not found" unless File.exist?(@filepath)
|
37
|
+
|
38
|
+
validate_last_nb_lines @last_nb_lines
|
39
|
+
validate_boolean @follow, "Invalid follow: #{@follow}."
|
40
|
+
end
|
41
|
+
|
42
|
+
#
|
43
|
+
# Naive implementation to read the last N lines of a file.
|
44
|
+
# Does not support `--follow`.
|
45
|
+
# Took ~1.41s to read a 3.1G file (5M lines).
|
46
|
+
#
|
47
|
+
def naive_read
|
48
|
+
# Let's us `wc` optimized to count the number of lines!
|
49
|
+
nb_lines = `wc -l #{@filepath}`.split.first.to_i
|
50
|
+
|
51
|
+
start = nb_lines - @last_nb_lines
|
52
|
+
i = 0
|
53
|
+
|
54
|
+
File.open(@filepath, 'r').each_line do |line|
|
55
|
+
puts line if i >= start
|
56
|
+
i += 1
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
#
|
61
|
+
# Use `seek` to start from the EOF.
|
62
|
+
# Use `read` to read the content of the file from the given position.
|
63
|
+
# src: https://renehernandez.io/tutorials/implementing-tail-command-in-ruby/
|
64
|
+
# Took ~0.13s to read a 3.1G file (5M lines).
|
65
|
+
#
|
66
|
+
def read_with_fd
|
67
|
+
file = File.open(@filepath)
|
68
|
+
update_stats file
|
69
|
+
read_last_nb_lines file
|
70
|
+
|
71
|
+
if @follow
|
72
|
+
loop do
|
73
|
+
if file_changed?(file)
|
74
|
+
update_stats(file)
|
75
|
+
print file.read
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
ensure
|
80
|
+
file&.close
|
81
|
+
end
|
82
|
+
|
83
|
+
def read_last_nb_lines(file)
|
84
|
+
return if File.empty?(file)
|
85
|
+
|
86
|
+
pos = 0
|
87
|
+
current_line_nb = 0
|
88
|
+
|
89
|
+
loop do
|
90
|
+
pos -= 1
|
91
|
+
# Seek file position from the end.
|
92
|
+
file.seek(pos, IO::SEEK_END)
|
93
|
+
|
94
|
+
# If we have reached the begining of the file, read all the file.
|
95
|
+
# We need to do this check before reading the next byte, otherwise, the
|
96
|
+
# cursor will be moved to 1.
|
97
|
+
break if file.tell.zero?
|
98
|
+
|
99
|
+
# Read only one character (or is it byte?).
|
100
|
+
char = file.read(1)
|
101
|
+
current_line_nb += 1 if char == "\n"
|
102
|
+
|
103
|
+
break if current_line_nb > @last_nb_lines
|
104
|
+
end
|
105
|
+
|
106
|
+
update_stats file
|
107
|
+
puts file.read
|
108
|
+
end
|
109
|
+
|
110
|
+
def update_stats(file)
|
111
|
+
@mtime = file.stat.mtime
|
112
|
+
@size = file.size
|
113
|
+
end
|
114
|
+
|
115
|
+
def file_changed?(file)
|
116
|
+
@mtime != file.stat.mtime || @size != file.size
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
# NOTE: We can create custom exceptions by extending RuntimeError.
|
121
|
+
class NoSuchFileError < RuntimeError
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|