kubetailrb 0.1.0 → 0.3.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 +18 -1
- data/README.md +49 -14
- data/Rakefile +11 -0
- data/journey_log.md +1 -7
- data/kubetailrb.png +0 -0
- data/lib/kubetailrb/cmd/file.rb +2 -2
- data/lib/kubetailrb/cmd/help.rb +11 -8
- data/lib/kubetailrb/cmd/k8s.rb +77 -23
- data/lib/kubetailrb/filter/log_filter.rb +62 -0
- data/lib/kubetailrb/formatter/json_formatter.rb +93 -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 +24 -3
- data/lib/kubetailrb/painter.rb +36 -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
@@ -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
|
@@ -0,0 +1,115 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'kubetailrb/validated'
|
4
|
+
require 'kubetailrb/filter/log_filter'
|
5
|
+
require 'kubetailrb/formatter/json_formatter'
|
6
|
+
require 'kubetailrb/formatter/no_op_formatter'
|
7
|
+
require 'kubetailrb/formatter/pod_metadata_formatter'
|
8
|
+
require_relative 'with_k8s_client'
|
9
|
+
|
10
|
+
module Kubetailrb
|
11
|
+
module Reader
|
12
|
+
# Read Kubernetes pod logs.
|
13
|
+
class K8sPodReader
|
14
|
+
include Validated
|
15
|
+
include WithK8sClient
|
16
|
+
|
17
|
+
attr_reader :pod_name, :opts
|
18
|
+
|
19
|
+
def initialize(pod_name:, container_name:, opts:, k8s_client: nil)
|
20
|
+
validate(pod_name, container_name, opts)
|
21
|
+
|
22
|
+
@k8s_client = k8s_client
|
23
|
+
@pod_name = pod_name
|
24
|
+
@container_name = container_name
|
25
|
+
@formatter = create_formatter(opts, pod_name, container_name)
|
26
|
+
@filter = Kubetailrb::Filter::LogFilter.create(opts.excludes)
|
27
|
+
@opts = opts
|
28
|
+
end
|
29
|
+
|
30
|
+
def read
|
31
|
+
pod_logs = read_pod_logs
|
32
|
+
unless @opts.follow?
|
33
|
+
print_logs pod_logs
|
34
|
+
return
|
35
|
+
end
|
36
|
+
|
37
|
+
# NOTE: The watch method from kubeclient does not accept `tail_lines`
|
38
|
+
# argument, so I had to resort to some hack... by using the first log to
|
39
|
+
# print out. Not ideal, since it's not really the N last nb lines, and
|
40
|
+
# assume every logs are different, which may not be true.
|
41
|
+
# But it does the job for most cases.
|
42
|
+
first_log_to_display = pod_logs.to_s.split("\n").first
|
43
|
+
should_print_logs = false
|
44
|
+
|
45
|
+
k8s_client.watch_pod_log(@pod_name, @opts.namespace, container: @container_name) do |line|
|
46
|
+
# NOTE: Is it good practice to update a variable that is outside of a
|
47
|
+
# block? Can we do better?
|
48
|
+
should_print_logs = true if line == first_log_to_display
|
49
|
+
|
50
|
+
print_logs(line) if should_print_logs
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
def validate(pod_name, container_name, opts)
|
57
|
+
raise_if_blank pod_name, 'Pod name not set.'
|
58
|
+
raise_if_blank container_name, 'Container name not set.'
|
59
|
+
raise_if_nil opts, 'Opts not set.'
|
60
|
+
end
|
61
|
+
|
62
|
+
def print_logs(logs)
|
63
|
+
if logs.to_s.include?("\n")
|
64
|
+
logs.to_s.split("\n").each { |log| print_logs(log) }
|
65
|
+
return
|
66
|
+
end
|
67
|
+
|
68
|
+
puts @formatter.format(logs) if @filter.test(logs)
|
69
|
+
$stdout.flush
|
70
|
+
end
|
71
|
+
|
72
|
+
def read_pod_logs
|
73
|
+
# The pod may still not up/ready, so small hack to retry 120 times (number
|
74
|
+
# taken randomly) until the pod returns its logs.
|
75
|
+
120.times do
|
76
|
+
pod_logs = k8s_client.get_pod_log(
|
77
|
+
@pod_name,
|
78
|
+
@opts.namespace,
|
79
|
+
container: @container_name,
|
80
|
+
tail_lines: @opts.last_nb_lines
|
81
|
+
)
|
82
|
+
|
83
|
+
if pod_logs.to_s.split("\n").empty?
|
84
|
+
raise PodNotReadyError, "No log returned from #{@pod_name}/#{@container_name}"
|
85
|
+
end
|
86
|
+
|
87
|
+
return pod_logs
|
88
|
+
rescue Kubeclient::HttpError, PodNotReadyError => e
|
89
|
+
puts e.message
|
90
|
+
sleep 1
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def create_formatter(opts, pod_name, container_name)
|
95
|
+
formatter = if opts.raw?
|
96
|
+
Kubetailrb::Formatter::NoOpFormatter.new
|
97
|
+
else
|
98
|
+
Kubetailrb::Formatter::JsonFormatter.new(opts.mdcs)
|
99
|
+
end
|
100
|
+
|
101
|
+
if opts.display_names?
|
102
|
+
formatter = Kubetailrb::Formatter::PodMetadataFormatter.new(
|
103
|
+
pod_name,
|
104
|
+
container_name, formatter
|
105
|
+
)
|
106
|
+
end
|
107
|
+
|
108
|
+
formatter
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
class PodNotReadyError < RuntimeError
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
@@ -0,0 +1,123 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'kubetailrb/validated'
|
4
|
+
require 'kubetailrb/k8s_opts'
|
5
|
+
require_relative 'k8s_pod_reader'
|
6
|
+
require_relative 'with_k8s_client'
|
7
|
+
|
8
|
+
module Kubetailrb
|
9
|
+
module Reader
|
10
|
+
# Read multiple pod logs.
|
11
|
+
class K8sPodsReader
|
12
|
+
include Validated
|
13
|
+
include WithK8sClient
|
14
|
+
include Painter
|
15
|
+
|
16
|
+
attr_reader :pod_query, :container_query, :opts
|
17
|
+
|
18
|
+
def initialize(pod_query:, container_query:, opts:, k8s_client: nil)
|
19
|
+
validate(pod_query, container_query, opts)
|
20
|
+
|
21
|
+
@k8s_client = k8s_client
|
22
|
+
@pod_query = Regexp.new(pod_query)
|
23
|
+
@container_query = Regexp.new(container_query)
|
24
|
+
@opts = opts
|
25
|
+
end
|
26
|
+
|
27
|
+
def read
|
28
|
+
pods = find_pods
|
29
|
+
watch_for_new_pod_events if @opts.follow?
|
30
|
+
|
31
|
+
threads = pods.flat_map do |pod|
|
32
|
+
pod.spec.containers.select { |container| applicable_container?(container.name) }.map do |container|
|
33
|
+
# NOTE: How much memory does a Ruby Thread takes? Can we spawn hundreds
|
34
|
+
# to thoudsands of Threads without issue?
|
35
|
+
start_reading_pod_logs(pod.metadata.name, container.name)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# NOTE: '&:' is a shorthand way of calling 'join' method on each thread.
|
40
|
+
# It's equivalent to: threads.each { |thread| thread.join }
|
41
|
+
threads.each(&:join)
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
def validate(pod_query, container_query, opts)
|
47
|
+
raise_if_blank pod_query, 'Pod query not set.'
|
48
|
+
raise_if_blank container_query, 'Container query not set.'
|
49
|
+
raise_if_nil opts, 'Opts not set.'
|
50
|
+
end
|
51
|
+
|
52
|
+
def find_pods
|
53
|
+
k8s_client
|
54
|
+
.get_pods(namespace: @opts.namespace)
|
55
|
+
.select { |pod| applicable_pod?(pod) }
|
56
|
+
end
|
57
|
+
|
58
|
+
def create_reader(pod_name, container_name)
|
59
|
+
K8sPodReader.new(
|
60
|
+
k8s_client: k8s_client,
|
61
|
+
pod_name: pod_name,
|
62
|
+
container_name: container_name,
|
63
|
+
opts: @opts
|
64
|
+
)
|
65
|
+
end
|
66
|
+
|
67
|
+
#
|
68
|
+
# Watch any pod events, and if there's another pod that validates the pod
|
69
|
+
# query, then let's read the pod logs!
|
70
|
+
#
|
71
|
+
def watch_for_new_pod_events
|
72
|
+
k8s_client.watch_pods(namespace: @opts.namespace) do |notice|
|
73
|
+
next unless applicable_pod?(notice.object)
|
74
|
+
|
75
|
+
on_new_pod_event notice if new_pod_event?(notice)
|
76
|
+
on_deleted_pod_event notice if deleted_pod_event?(notice)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def applicable_pod?(pod)
|
81
|
+
pod.metadata.name.match?(@pod_query)
|
82
|
+
end
|
83
|
+
|
84
|
+
def applicable_container?(container_name)
|
85
|
+
container_name.match?(@container_query)
|
86
|
+
end
|
87
|
+
|
88
|
+
def new_pod_event?(notice)
|
89
|
+
notice.type == 'ADDED' && notice.object.kind == 'Pod'
|
90
|
+
end
|
91
|
+
|
92
|
+
def on_new_pod_event(notice) # rubocop:disable Metrics/AbcSize
|
93
|
+
# NOTE: We are in another thread (are we?), so no sense to use
|
94
|
+
# 'Thread.join' here.
|
95
|
+
|
96
|
+
notice.object.spec.containers.map do |container|
|
97
|
+
next unless applicable_container?(container.name)
|
98
|
+
|
99
|
+
# NOTE: How much memory does a Ruby Thread takes? Can we spawn hundreds
|
100
|
+
# to thoudsands of Threads without issue?
|
101
|
+
puts blue("+ #{notice.object.metadata.name}/#{container.name}")
|
102
|
+
start_reading_pod_logs(notice.object.metadata.name, container.name)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def start_reading_pod_logs(pod_name, container_name)
|
107
|
+
Thread.new { create_reader(pod_name, container_name).read }
|
108
|
+
end
|
109
|
+
|
110
|
+
def deleted_pod_event?(notice)
|
111
|
+
notice.type == 'DELETED' && notice.object.kind == 'Pod'
|
112
|
+
end
|
113
|
+
|
114
|
+
def on_deleted_pod_event(notice)
|
115
|
+
notice.object.spec.containers.map do |container|
|
116
|
+
next unless applicable_container?(container.name)
|
117
|
+
|
118
|
+
puts red("- #{notice.object.metadata.name}/#{container.name}")
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'kubeclient'
|
4
|
+
|
5
|
+
module Kubetailrb
|
6
|
+
module Reader
|
7
|
+
# Add behavior to get a k8s client by using composition.
|
8
|
+
# NOTE: Is it the idiomatic way? Or shall I use a factory? Or is there a
|
9
|
+
# better way?
|
10
|
+
module WithK8sClient
|
11
|
+
def k8s_client
|
12
|
+
@k8s_client ||= create_k8s_client
|
13
|
+
end
|
14
|
+
|
15
|
+
def create_k8s_client
|
16
|
+
config = Kubeclient::Config.read(ENV['KUBECONFIG'] || "#{ENV["HOME"]}/.kube/config")
|
17
|
+
context = config.context
|
18
|
+
Kubeclient::Client.new(
|
19
|
+
context.api_endpoint,
|
20
|
+
'v1',
|
21
|
+
ssl_options: context.ssl_options,
|
22
|
+
auth_options: context.auth_options
|
23
|
+
)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
data/lib/kubetailrb/validated.rb
CHANGED
@@ -7,6 +7,10 @@ module Kubetailrb
|
|
7
7
|
raise ArgumentError, error_message if arg.nil? || arg.strip&.empty?
|
8
8
|
end
|
9
9
|
|
10
|
+
def raise_if_nil(arg, error_message)
|
11
|
+
raise ArgumentError, error_message if arg.nil?
|
12
|
+
end
|
13
|
+
|
10
14
|
def validate_last_nb_lines(last_nb_lines)
|
11
15
|
last_nb_lines_valid = last_nb_lines.is_a?(Integer) && last_nb_lines.positive?
|
12
16
|
raise ArgumentError, "Invalid last_nb_lines: #{last_nb_lines}." unless last_nb_lines_valid
|
data/lib/kubetailrb/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: kubetailrb
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Louis Lin
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-11
|
11
|
+
date: 2024-12-11 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: kubeclient
|
@@ -112,30 +112,30 @@ dependencies:
|
|
112
112
|
name: pry
|
113
113
|
requirement: !ruby/object:Gem::Requirement
|
114
114
|
requirements:
|
115
|
-
- - "
|
115
|
+
- - "~>"
|
116
116
|
- !ruby/object:Gem::Version
|
117
|
-
version:
|
117
|
+
version: 0.14.2
|
118
118
|
type: :development
|
119
119
|
prerelease: false
|
120
120
|
version_requirements: !ruby/object:Gem::Requirement
|
121
121
|
requirements:
|
122
|
-
- - "
|
122
|
+
- - "~>"
|
123
123
|
- !ruby/object:Gem::Version
|
124
|
-
version:
|
124
|
+
version: 0.14.2
|
125
125
|
- !ruby/object:Gem::Dependency
|
126
126
|
name: pry-byebug
|
127
127
|
requirement: !ruby/object:Gem::Requirement
|
128
128
|
requirements:
|
129
|
-
- - "
|
129
|
+
- - "~>"
|
130
130
|
- !ruby/object:Gem::Version
|
131
|
-
version:
|
131
|
+
version: 3.10.1
|
132
132
|
type: :development
|
133
133
|
prerelease: false
|
134
134
|
version_requirements: !ruby/object:Gem::Requirement
|
135
135
|
requirements:
|
136
|
-
- - "
|
136
|
+
- - "~>"
|
137
137
|
- !ruby/object:Gem::Version
|
138
|
-
version:
|
138
|
+
version: 3.10.1
|
139
139
|
- !ruby/object:Gem::Dependency
|
140
140
|
name: rake
|
141
141
|
requirement: !ruby/object:Gem::Requirement
|
@@ -253,16 +253,19 @@ files:
|
|
253
253
|
- lib/kubetailrb/cmd/help.rb
|
254
254
|
- lib/kubetailrb/cmd/k8s.rb
|
255
255
|
- lib/kubetailrb/cmd/version.rb
|
256
|
-
- lib/kubetailrb/
|
257
|
-
- lib/kubetailrb/json_formatter.rb
|
256
|
+
- lib/kubetailrb/filter/log_filter.rb
|
257
|
+
- lib/kubetailrb/formatter/json_formatter.rb
|
258
|
+
- lib/kubetailrb/formatter/no_op_formatter.rb
|
259
|
+
- lib/kubetailrb/formatter/pod_metadata_formatter.rb
|
258
260
|
- lib/kubetailrb/k8s_opts.rb
|
259
|
-
- lib/kubetailrb/k8s_pod_reader.rb
|
260
|
-
- lib/kubetailrb/k8s_pods_reader.rb
|
261
|
-
- lib/kubetailrb/no_op_formatter.rb
|
262
261
|
- lib/kubetailrb/opts_parser.rb
|
262
|
+
- lib/kubetailrb/painter.rb
|
263
|
+
- lib/kubetailrb/reader/file_reader.rb
|
264
|
+
- lib/kubetailrb/reader/k8s_pod_reader.rb
|
265
|
+
- lib/kubetailrb/reader/k8s_pods_reader.rb
|
266
|
+
- lib/kubetailrb/reader/with_k8s_client.rb
|
263
267
|
- lib/kubetailrb/validated.rb
|
264
268
|
- lib/kubetailrb/version.rb
|
265
|
-
- lib/kubetailrb/with_k8s_client.rb
|
266
269
|
- sig/kubetailrb.rbs
|
267
270
|
homepage: https://github.com/l-lin/kubetailrb
|
268
271
|
licenses:
|
@@ -1,122 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require_relative 'validated'
|
4
|
-
|
5
|
-
module Kubetailrb
|
6
|
-
# Read file content
|
7
|
-
class FileReader
|
8
|
-
include Validated
|
9
|
-
|
10
|
-
attr_reader :filepath, :last_nb_lines
|
11
|
-
|
12
|
-
def initialize(filepath:, last_nb_lines:, follow:)
|
13
|
-
@filepath = filepath
|
14
|
-
@last_nb_lines = last_nb_lines
|
15
|
-
@follow = follow
|
16
|
-
|
17
|
-
validate
|
18
|
-
end
|
19
|
-
|
20
|
-
def read
|
21
|
-
# naive_read
|
22
|
-
read_with_fd
|
23
|
-
end
|
24
|
-
|
25
|
-
# NOTE: Is there something like `attr_reader` but for boolean?
|
26
|
-
# Nope, Ruby does not know if a variable is a boolean or not. So it cannot
|
27
|
-
# create a dedicated method for those booleans.
|
28
|
-
def follow?
|
29
|
-
@follow
|
30
|
-
end
|
31
|
-
|
32
|
-
private
|
33
|
-
|
34
|
-
def validate
|
35
|
-
raise NoSuchFileError, "#{@filepath} not found" unless File.exist?(@filepath)
|
36
|
-
|
37
|
-
validate_last_nb_lines @last_nb_lines
|
38
|
-
validate_boolean @follow, "Invalid follow: #{@follow}."
|
39
|
-
end
|
40
|
-
|
41
|
-
#
|
42
|
-
# Naive implementation to read the last N lines of a file.
|
43
|
-
# Does not support `--follow`.
|
44
|
-
# Took ~1.41s to read a 3.1G file (5M lines).
|
45
|
-
#
|
46
|
-
def naive_read
|
47
|
-
# Let's us `wc` optimized to count the number of lines!
|
48
|
-
nb_lines = `wc -l #{@filepath}`.split.first.to_i
|
49
|
-
|
50
|
-
start = nb_lines - @last_nb_lines
|
51
|
-
i = 0
|
52
|
-
|
53
|
-
File.open(@filepath, 'r').each_line do |line|
|
54
|
-
puts line if i >= start
|
55
|
-
i += 1
|
56
|
-
end
|
57
|
-
end
|
58
|
-
|
59
|
-
#
|
60
|
-
# Use `seek` to start from the EOF.
|
61
|
-
# Use `read` to read the content of the file from the given position.
|
62
|
-
# src: https://renehernandez.io/tutorials/implementing-tail-command-in-ruby/
|
63
|
-
# Took ~0.13s to read a 3.1G file (5M lines).
|
64
|
-
#
|
65
|
-
def read_with_fd
|
66
|
-
file = File.open(@filepath)
|
67
|
-
update_stats file
|
68
|
-
read_last_nb_lines file
|
69
|
-
|
70
|
-
if @follow
|
71
|
-
loop do
|
72
|
-
if file_changed?(file)
|
73
|
-
update_stats(file)
|
74
|
-
print file.read
|
75
|
-
end
|
76
|
-
end
|
77
|
-
end
|
78
|
-
ensure
|
79
|
-
file&.close
|
80
|
-
end
|
81
|
-
|
82
|
-
def read_last_nb_lines(file)
|
83
|
-
return if File.empty?(file)
|
84
|
-
|
85
|
-
pos = 0
|
86
|
-
current_line_nb = 0
|
87
|
-
|
88
|
-
loop do
|
89
|
-
pos -= 1
|
90
|
-
# Seek file position from the end.
|
91
|
-
file.seek(pos, IO::SEEK_END)
|
92
|
-
|
93
|
-
# If we have reached the begining of the file, read all the file.
|
94
|
-
# We need to do this check before reading the next byte, otherwise, the
|
95
|
-
# cursor will be moved to 1.
|
96
|
-
break if file.tell.zero?
|
97
|
-
|
98
|
-
# Read only one character (or is it byte?).
|
99
|
-
char = file.read(1)
|
100
|
-
current_line_nb += 1 if char == "\n"
|
101
|
-
|
102
|
-
break if current_line_nb > @last_nb_lines
|
103
|
-
end
|
104
|
-
|
105
|
-
update_stats file
|
106
|
-
puts file.read
|
107
|
-
end
|
108
|
-
|
109
|
-
def update_stats(file)
|
110
|
-
@mtime = file.stat.mtime
|
111
|
-
@size = file.size
|
112
|
-
end
|
113
|
-
|
114
|
-
def file_changed?(file)
|
115
|
-
@mtime != file.stat.mtime || @size != file.size
|
116
|
-
end
|
117
|
-
end
|
118
|
-
|
119
|
-
# NOTE: We can create custom exceptions by extending RuntimeError.
|
120
|
-
class NoSuchFileError < RuntimeError
|
121
|
-
end
|
122
|
-
end
|
@@ -1,66 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Kubetailrb
|
4
|
-
# Format JSON to human readable.
|
5
|
-
class JsonFormatter
|
6
|
-
def format(log)
|
7
|
-
json = JSON.parse(log)
|
8
|
-
|
9
|
-
return format_access_log(json) if access_log?(json)
|
10
|
-
|
11
|
-
format_application_log(json)
|
12
|
-
rescue JSON::ParserError
|
13
|
-
log
|
14
|
-
end
|
15
|
-
|
16
|
-
private
|
17
|
-
|
18
|
-
def access_log?(json)
|
19
|
-
json.include?('http.response.status_code')
|
20
|
-
end
|
21
|
-
|
22
|
-
def format_access_log(json)
|
23
|
-
"#{json["@timestamp"]}#{http_status_code json}#{json["http.request.method"]} #{json["url.path"]}"
|
24
|
-
end
|
25
|
-
|
26
|
-
def format_application_log(json)
|
27
|
-
"#{json["@timestamp"]}#{log_level json}#{json["message"]}"
|
28
|
-
end
|
29
|
-
|
30
|
-
def http_status_code(json)
|
31
|
-
code = json['http.response.status_code']
|
32
|
-
|
33
|
-
return "#{blue(" I ")}[#{code}] " if code >= 200 && code < 400
|
34
|
-
return "#{yellow(" W ")}[#{code}] " if code >= 400 && code < 500
|
35
|
-
return "#{red(" E ")}[#{code}] " if code > 500
|
36
|
-
|
37
|
-
" #{code} "
|
38
|
-
end
|
39
|
-
|
40
|
-
def log_level(json)
|
41
|
-
level = json['log.level']
|
42
|
-
return '' if level.nil? || level.strip.empty?
|
43
|
-
return blue(' I ') if level == 'INFO'
|
44
|
-
return yellow(' W ') if level == 'WARN'
|
45
|
-
return red(' E ') if level == 'ERROR'
|
46
|
-
|
47
|
-
" #{level} "
|
48
|
-
end
|
49
|
-
|
50
|
-
def colorize(text, color_code)
|
51
|
-
" \e[#{color_code}m#{text}\e[0m "
|
52
|
-
end
|
53
|
-
|
54
|
-
def blue(text)
|
55
|
-
colorize(text, '1;30;44')
|
56
|
-
end
|
57
|
-
|
58
|
-
def yellow(text)
|
59
|
-
colorize(text, '1;30;43')
|
60
|
-
end
|
61
|
-
|
62
|
-
def red(text)
|
63
|
-
colorize(text, '1;30;41')
|
64
|
-
end
|
65
|
-
end
|
66
|
-
end
|