aws-logs 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/.rspec +3 -0
- data/CHANGELOG.md +7 -0
- data/Gemfile +6 -0
- data/Guardfile +19 -0
- data/LICENSE.txt +22 -0
- data/README.md +46 -0
- data/Rakefile +14 -0
- data/aws-logs.gemspec +33 -0
- data/exe/aws-logs +14 -0
- data/lib/aws-logs.rb +1 -0
- data/lib/aws_logs.rb +10 -0
- data/lib/aws_logs/autoloader.rb +22 -0
- data/lib/aws_logs/aws_services.rb +11 -0
- data/lib/aws_logs/cli.rb +33 -0
- data/lib/aws_logs/command.rb +82 -0
- data/lib/aws_logs/completer.rb +159 -0
- data/lib/aws_logs/completer/script.rb +6 -0
- data/lib/aws_logs/completer/script.sh +10 -0
- data/lib/aws_logs/help.rb +9 -0
- data/lib/aws_logs/help/completion.md +20 -0
- data/lib/aws_logs/help/completion_script.md +3 -0
- data/lib/aws_logs/help/tail.md +44 -0
- data/lib/aws_logs/since.rb +82 -0
- data/lib/aws_logs/tail.rb +142 -0
- data/lib/aws_logs/version.rb +3 -0
- data/spec/fixtures/typical/events-1.json +11 -0
- data/spec/fixtures/typical/events-2.json +17 -0
- data/spec/fixtures/typical/events-3.json +23 -0
- data/spec/fixtures/typical/events-4.json +28 -0
- data/spec/lib/cli_spec.rb +8 -0
- data/spec/lib/since_spec.rb +35 -0
- data/spec/lib/tail_spec.rb +88 -0
- data/spec/spec_helper.rb +45 -0
- metadata +240 -0
@@ -0,0 +1,10 @@
|
|
1
|
+
_aws-logs() {
|
2
|
+
COMPREPLY=()
|
3
|
+
local word="${COMP_WORDS[COMP_CWORD]}"
|
4
|
+
local words=("${COMP_WORDS[@]}")
|
5
|
+
unset words[0]
|
6
|
+
local completion=$(aws-logs completion ${words[@]})
|
7
|
+
COMPREPLY=( $(compgen -W "$completion" -- "$word") )
|
8
|
+
}
|
9
|
+
|
10
|
+
complete -F _aws-logs aws-logs
|
@@ -0,0 +1,20 @@
|
|
1
|
+
## Examples
|
2
|
+
|
3
|
+
aws-logs completion
|
4
|
+
|
5
|
+
Prints words for TAB auto-completion.
|
6
|
+
|
7
|
+
aws-logs completion
|
8
|
+
aws-logs completion hello
|
9
|
+
aws-logs completion hello name
|
10
|
+
|
11
|
+
To enable, TAB auto-completion add the following to your profile:
|
12
|
+
|
13
|
+
eval $(aws-logs completion_script)
|
14
|
+
|
15
|
+
Auto-completion example usage:
|
16
|
+
|
17
|
+
aws-logs [TAB]
|
18
|
+
aws-logs hello [TAB]
|
19
|
+
aws-logs hello name [TAB]
|
20
|
+
aws-logs hello name --[TAB]
|
@@ -0,0 +1,44 @@
|
|
1
|
+
## Examples
|
2
|
+
|
3
|
+
aws-logs tail /aws/codebuild/demo --since 60m
|
4
|
+
aws-logs tail /aws/codebuild/demo --since "2018-08-08 08:00:00"
|
5
|
+
aws-logs tail /aws/codebuild/demo --no-follow
|
6
|
+
aws-logs tail /aws/codebuild/demo --format simple
|
7
|
+
aws-logs tail /aws/codebuild/demo --filter-pattern Wed
|
8
|
+
|
9
|
+
## Examples with Output
|
10
|
+
|
11
|
+
Using `--since`
|
12
|
+
|
13
|
+
$ aws-logs tail /aws/codebuild/demo --since 60m --no-follow
|
14
|
+
2019-11-27 22:56:05 UTC 8cb8b7fd-3662-4120-95bc-efff637c7220 Wed Nov 27 22:56:04 UTC 2019
|
15
|
+
2019-11-27 22:56:16 UTC 8cb8b7fd-3662-4120-95bc-efff637c7220
|
16
|
+
2019-11-27 22:56:16 UTC 8cb8b7fd-3662-4120-95bc-efff637c7220 [Container] 2019/11/27 22:56:14 Phase complete: BUILD State: SUCCEEDED
|
17
|
+
2019-11-27 22:56:16 UTC 8cb8b7fd-3662-4120-95bc-efff637c7220 [Container] 2019/11/27 22:56:14 Phase context status code: Message:
|
18
|
+
2019-11-27 22:56:16 UTC 8cb8b7fd-3662-4120-95bc-efff637c7220 [Container] 2019/11/27 22:56:14 Entering phase POST_BUILD
|
19
|
+
2019-11-27 22:56:16 UTC 8cb8b7fd-3662-4120-95bc-efff637c7220 [Container] 2019/11/27 22:56:14 Phase complete: POST_BUILD State: SUCCEEDED
|
20
|
+
2019-11-27 22:56:16 UTC 8cb8b7fd-3662-4120-95bc-efff637c7220 [Container] 2019/11/27 22:56:14 Phase context status code: Message:
|
21
|
+
$
|
22
|
+
|
23
|
+
Using `--filter-pattern`.
|
24
|
+
|
25
|
+
$ aws-logs tail /aws/codebuild/demo --filter-pattern Wed --since 60m
|
26
|
+
2019-11-27 22:19:41 UTC 0d933e8f-c15b-41af-a5c7-36b54530cb17 Wed Nov 27 22:19:37 UTC 2019
|
27
|
+
2019-11-27 22:19:49 UTC 0d933e8f-c15b-41af-a5c7-36b54530cb17 Wed Nov 27 22:19:47 UTC 2019
|
28
|
+
2019-11-27 22:19:59 UTC 0d933e8f-c15b-41af-a5c7-36b54530cb17 Wed Nov 27 22:19:57 UTC 2019
|
29
|
+
2019-11-27 22:20:09 UTC 0d933e8f-c15b-41af-a5c7-36b54530cb17 Wed Nov 27 22:20:07 UTC 2019
|
30
|
+
2019-11-27 22:20:19 UTC 0d933e8f-c15b-41af-a5c7-36b54530cb17 Wed Nov 27 22:20:17 UTC 2019
|
31
|
+
2019-11-27 22:20:29 UTC 0d933e8f-c15b-41af-a5c7-36b54530cb17 Wed Nov 27 22:20:27 UTC 2019
|
32
|
+
2019-11-27 22:20:39 UTC 0d933e8f-c15b-41af-a5c7-36b54530cb17 Wed Nov 27 22:20:37 UTC 2019
|
33
|
+
|
34
|
+
## Since Formats
|
35
|
+
|
36
|
+
Since supports these formats:
|
37
|
+
|
38
|
+
* s - seconds
|
39
|
+
* m - minutes
|
40
|
+
* h - hours
|
41
|
+
* d - days
|
42
|
+
* w - weeks
|
43
|
+
|
44
|
+
Since does not current support combining the formats. IE: 5m30s.
|
@@ -0,0 +1,82 @@
|
|
1
|
+
require "active_support/core_ext/integer"
|
2
|
+
require "time"
|
3
|
+
|
4
|
+
module AwsLogs
|
5
|
+
class Since
|
6
|
+
DEFAULT = 10.minutes.to_i
|
7
|
+
|
8
|
+
def initialize(str)
|
9
|
+
@str = str
|
10
|
+
end
|
11
|
+
|
12
|
+
def to_i
|
13
|
+
if iso8601_format?
|
14
|
+
iso8601_seconds
|
15
|
+
elsif friendly_format?
|
16
|
+
friendly_seconds
|
17
|
+
else
|
18
|
+
puts warning
|
19
|
+
return DEFAULT
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
ISO8601_REGEXP = /\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/
|
24
|
+
def iso8601_format?
|
25
|
+
!!@str.match(ISO8601_REGEXP)
|
26
|
+
end
|
27
|
+
|
28
|
+
def iso8601_seconds
|
29
|
+
# https://stackoverflow.com/questions/3775544/how-do-you-convert-an-iso-8601-date-to-a-unix-timestamp-in-ruby
|
30
|
+
Time.iso8601(@str.sub(/ /,'T')).to_i
|
31
|
+
end
|
32
|
+
|
33
|
+
FRIENDLY_REGEXP = /(\d+)(\w+)/
|
34
|
+
def friendly_format?
|
35
|
+
!!@str.match(FRIENDLY_REGEXP)
|
36
|
+
end
|
37
|
+
|
38
|
+
def friendly_seconds
|
39
|
+
number, unit = find_match(FRIENDLY_REGEXP)
|
40
|
+
unless number && unit
|
41
|
+
puts warning
|
42
|
+
return DEFAULT
|
43
|
+
end
|
44
|
+
|
45
|
+
meth = shorthand(unit)
|
46
|
+
if number.respond_to?(meth)
|
47
|
+
number.send(meth).to_i
|
48
|
+
else
|
49
|
+
puts warning
|
50
|
+
return DEFAULT
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def find_match(regexp)
|
55
|
+
md = @str.match(regexp)
|
56
|
+
if md
|
57
|
+
number, unit = md[1].to_i, md[2]
|
58
|
+
end
|
59
|
+
[number, unit]
|
60
|
+
end
|
61
|
+
|
62
|
+
def warning
|
63
|
+
"WARN: since is not in a supported format. Falling back to 10m".color(:yellow)
|
64
|
+
end
|
65
|
+
|
66
|
+
# s - seconds
|
67
|
+
# m - minutes
|
68
|
+
# h - hours
|
69
|
+
# d - days
|
70
|
+
# w - weeks
|
71
|
+
def shorthand(k)
|
72
|
+
map = {
|
73
|
+
s: :seconds,
|
74
|
+
m: :minutes,
|
75
|
+
h: :hours,
|
76
|
+
d: :days,
|
77
|
+
w: :weeks,
|
78
|
+
}
|
79
|
+
map[k.to_sym] || k
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,142 @@
|
|
1
|
+
require "json"
|
2
|
+
|
3
|
+
module AwsLogs
|
4
|
+
class Tail
|
5
|
+
include AwsServices
|
6
|
+
|
7
|
+
def initialize(options={})
|
8
|
+
@options = options
|
9
|
+
@log_group = options[:log_group]
|
10
|
+
|
11
|
+
@loop_count = 0
|
12
|
+
@output = [] # for specs
|
13
|
+
reset
|
14
|
+
set_trap
|
15
|
+
end
|
16
|
+
|
17
|
+
@@end_loop_signal = false
|
18
|
+
def set_trap
|
19
|
+
Signal.trap("INT") {
|
20
|
+
puts "\nCtrl-C detected. Exiting..."
|
21
|
+
@@end_loop_signal = true # delayed exit, usefu control loop flow though
|
22
|
+
exit # immediate exit
|
23
|
+
}
|
24
|
+
end
|
25
|
+
|
26
|
+
def reset
|
27
|
+
@events = [] # constantly replaced with recent events
|
28
|
+
@last_shown_event_id = nil
|
29
|
+
@completed = nil
|
30
|
+
end
|
31
|
+
|
32
|
+
# The start and end time is useful to limit results and make the API fast. We'll leverage it like so:
|
33
|
+
#
|
34
|
+
# 1. load all events from an initial since time
|
35
|
+
# 2. after that load events pass that first window
|
36
|
+
#
|
37
|
+
# It's a sliding window of time we're using.
|
38
|
+
#
|
39
|
+
def run
|
40
|
+
if ENV['AWS_LOGS_NOOP']
|
41
|
+
puts "Noop test"
|
42
|
+
return
|
43
|
+
end
|
44
|
+
|
45
|
+
since, now = initial_since, current_now
|
46
|
+
while true && !end_loop?
|
47
|
+
refresh_events(since, now)
|
48
|
+
display
|
49
|
+
since, now = now, current_now
|
50
|
+
loop_count!
|
51
|
+
sleep 5 if @options[:follow] && !@@end_loop_signal && !ENV["AWS_LOGS_TEST"]
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def refresh_events(start_time, end_time)
|
56
|
+
@events = []
|
57
|
+
next_token = :start
|
58
|
+
|
59
|
+
# TODO: can hit throttle limit if there are lots of pages
|
60
|
+
while next_token
|
61
|
+
options = {
|
62
|
+
log_group_name: @log_group, # required
|
63
|
+
start_time: start_time,
|
64
|
+
end_time: end_time,
|
65
|
+
# limit: 10,
|
66
|
+
}
|
67
|
+
options[:log_stream_names] = @options[:log_stream_names] if @options[:log_stream_names]
|
68
|
+
options[:log_stream_name_prefix] = @options[:log_stream_name_prefix] if @options[:log_stream_name_prefix]
|
69
|
+
options[:filter_pattern] = @options[:filter_pattern] if @options[:filter_pattern]
|
70
|
+
resp = cloudwatchlogs.filter_log_events(options)
|
71
|
+
|
72
|
+
@events += resp.events
|
73
|
+
next_token = resp.next_token
|
74
|
+
end
|
75
|
+
|
76
|
+
@events
|
77
|
+
end
|
78
|
+
|
79
|
+
# Events canduplicated as events can be written to the exact same timestamp.
|
80
|
+
# So also track the last_shown_event_id and prevent duplicate log lines from re-appearing.
|
81
|
+
def display
|
82
|
+
new_events = @events
|
83
|
+
shown_index = new_events.find_index { |e| e.event_id == @last_shown_event_id }
|
84
|
+
if shown_index
|
85
|
+
new_events = @events[shown_index+1..-1] || []
|
86
|
+
end
|
87
|
+
|
88
|
+
new_events.each do |e|
|
89
|
+
time = Time.at(e.timestamp/1000).utc
|
90
|
+
line = [time.to_s.color(:green), e.message]
|
91
|
+
format = @options[:format] || "detailed"
|
92
|
+
line.insert(1, e.log_stream_name.color(:purple)) if format == "detailed"
|
93
|
+
say line.join(' ')
|
94
|
+
end
|
95
|
+
@last_shown_event_id = @events.last&.event_id
|
96
|
+
check_follow_until!
|
97
|
+
end
|
98
|
+
|
99
|
+
def check_follow_until!
|
100
|
+
follow_until = @options[:follow_until]
|
101
|
+
return unless follow_until
|
102
|
+
|
103
|
+
messages = @events.map(&:message)
|
104
|
+
if messages.detect { |m| m.include?(follow_until) }
|
105
|
+
@@end_loop_signal = true
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def say(text)
|
110
|
+
ENV["AWS_LOGS_TEST"] ? @output << text : puts(text)
|
111
|
+
end
|
112
|
+
|
113
|
+
def output
|
114
|
+
@output.join("\n") + "\n"
|
115
|
+
end
|
116
|
+
|
117
|
+
private
|
118
|
+
def initial_since
|
119
|
+
since = @options[:since]
|
120
|
+
seconds = since ? Since.new(since).to_i : Since::DEFAULT
|
121
|
+
(Time.now.to_i - seconds) * 1000 # past 10 minutes in milliseconds
|
122
|
+
end
|
123
|
+
|
124
|
+
def current_now
|
125
|
+
(Time.now.to_i) * 1000 # now in milliseconds
|
126
|
+
end
|
127
|
+
|
128
|
+
def end_loop?
|
129
|
+
return true if @@end_loop_signal
|
130
|
+
max_loop_count && @loop_count >= max_loop_count
|
131
|
+
end
|
132
|
+
|
133
|
+
def loop_count!
|
134
|
+
@loop_count += 1
|
135
|
+
end
|
136
|
+
|
137
|
+
# Useful for specs
|
138
|
+
def max_loop_count
|
139
|
+
@options[:follow] ? nil : 1
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
{
|
2
|
+
"events": [
|
3
|
+
{
|
4
|
+
"log_stream_name": "stream-name",
|
5
|
+
"timestamp": 1574888810000,
|
6
|
+
"message": "message1",
|
7
|
+
"event_id": "event1"
|
8
|
+
},
|
9
|
+
{
|
10
|
+
"log_stream_name": "stream-name",
|
11
|
+
"timestamp": 1574888820000,
|
12
|
+
"message": "message2",
|
13
|
+
"event_id": "event2"
|
14
|
+
}
|
15
|
+
],
|
16
|
+
"next_token": 3
|
17
|
+
}
|
@@ -0,0 +1,23 @@
|
|
1
|
+
{
|
2
|
+
"events": [
|
3
|
+
{
|
4
|
+
"log_stream_name": "stream-name",
|
5
|
+
"timestamp": 1574888810000,
|
6
|
+
"message": "message1",
|
7
|
+
"event_id": "event1"
|
8
|
+
},
|
9
|
+
{
|
10
|
+
"log_stream_name": "stream-name",
|
11
|
+
"timestamp": 1574888820000,
|
12
|
+
"message": "message2",
|
13
|
+
"event_id": "event2"
|
14
|
+
},
|
15
|
+
{
|
16
|
+
"log_stream_name": "stream-name",
|
17
|
+
"timestamp": 1574888830000,
|
18
|
+
"message": "message3",
|
19
|
+
"event_id": "event3"
|
20
|
+
}
|
21
|
+
],
|
22
|
+
"next_token": 4
|
23
|
+
}
|
@@ -0,0 +1,28 @@
|
|
1
|
+
{
|
2
|
+
"events": [
|
3
|
+
{
|
4
|
+
"log_stream_name": "stream-name",
|
5
|
+
"timestamp": 1574888810000,
|
6
|
+
"message": "message1",
|
7
|
+
"event_id": "event1"
|
8
|
+
},
|
9
|
+
{
|
10
|
+
"log_stream_name": "stream-name",
|
11
|
+
"timestamp": 1574888820000,
|
12
|
+
"message": "message2",
|
13
|
+
"event_id": "event2"
|
14
|
+
},
|
15
|
+
{
|
16
|
+
"log_stream_name": "stream-name",
|
17
|
+
"timestamp": 1574888830000,
|
18
|
+
"message": "message3",
|
19
|
+
"event_id": "event3"
|
20
|
+
},
|
21
|
+
{
|
22
|
+
"log_stream_name": "stream-name",
|
23
|
+
"timestamp": 1574888840000,
|
24
|
+
"message": "message4",
|
25
|
+
"event_id": "event4"
|
26
|
+
}
|
27
|
+
]
|
28
|
+
}
|
@@ -0,0 +1,35 @@
|
|
1
|
+
describe AwsLogs::Since do
|
2
|
+
let(:since) { AwsLogs::Since.new(str) }
|
3
|
+
|
4
|
+
context "friendly format" do
|
5
|
+
context "5m" do
|
6
|
+
let(:str) { "5m" }
|
7
|
+
it "5m" do
|
8
|
+
expect(since.to_i).to eq 300
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
context "1hr" do
|
13
|
+
let(:str) { "1h" }
|
14
|
+
it "1h" do
|
15
|
+
expect(since.to_i).to eq 3600
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
context "junk" do
|
20
|
+
let(:str) { "junk" }
|
21
|
+
it "junk" do
|
22
|
+
expect(since.to_i).to eq 600 # fallback
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
context "iso8601 format" do
|
28
|
+
context "2018-08-08 08:08:08" do
|
29
|
+
let(:str) { "2018-08-08 08:08:08" }
|
30
|
+
it "2018-08-08 08:08:08" do
|
31
|
+
expect(since.to_i).to be_a(Integer)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|