aws-logs 0.3.2
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 +17 -0
- data/.rspec +3 -0
- data/CHANGELOG.md +21 -0
- data/Gemfile +6 -0
- data/Guardfile +19 -0
- data/LICENSE.txt +22 -0
- data/README.md +38 -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 +12 -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 +57 -0
- data/lib/aws_logs/since.rb +82 -0
- data/lib/aws_logs/tail.rb +151 -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,57 @@
|
|
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 support combining the formats. IE: 5m30s.
|
45
|
+
|
46
|
+
## Filter Pattern
|
47
|
+
|
48
|
+
The `--filter-pattern` option is quite powerful as CloudWatch supports a full
|
49
|
+
[Filter and Pattern Syntax](https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/FilterAndPatternSyntax.html).
|
50
|
+
|
51
|
+
To match terms with spaces in it, you'll need quotes around it. Otherise, the match will be an OR of the terms. Example:
|
52
|
+
|
53
|
+
aws-logs tail /aws/codebuild/demo --filter-pattern '"Wed Nov 27 23"' --since 3h --no-follow
|
54
|
+
|
55
|
+
Here's an example of matching with an exclude pattern using the `-` (minus sign).
|
56
|
+
|
57
|
+
aws-logs tail /aws/codebuild/demo --filter-pattern '"ERROR" - "Exiting"' --since 3h --no-follow
|
@@ -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,151 @@
|
|
1
|
+
require "json"
|
2
|
+
|
3
|
+
module AwsLogs
|
4
|
+
class Tail
|
5
|
+
include AwsServices
|
6
|
+
|
7
|
+
def initialize(options={})
|
8
|
+
@options = options
|
9
|
+
@log_group_name = options[:log_group_name]
|
10
|
+
# Setting to ensure matches default CLI option
|
11
|
+
@follow = @options[:follow].nil? ? true : @options[:follow]
|
12
|
+
|
13
|
+
@loop_count = 0
|
14
|
+
@output = [] # for specs
|
15
|
+
reset
|
16
|
+
set_trap
|
17
|
+
end
|
18
|
+
|
19
|
+
def reset
|
20
|
+
@events = [] # constantly replaced with recent events
|
21
|
+
@last_shown_event_id = nil
|
22
|
+
@completed = nil
|
23
|
+
end
|
24
|
+
|
25
|
+
# The start and end time is useful to limit results and make the API fast. We'll leverage it like so:
|
26
|
+
#
|
27
|
+
# 1. load all events from an initial since time
|
28
|
+
# 2. after that load events pass that first window
|
29
|
+
#
|
30
|
+
# It's a sliding window of time we're using.
|
31
|
+
#
|
32
|
+
def run
|
33
|
+
if ENV['AWS_LOGS_NOOP']
|
34
|
+
puts "Noop test"
|
35
|
+
return
|
36
|
+
end
|
37
|
+
|
38
|
+
since, now = initial_since, current_now
|
39
|
+
until end_loop?
|
40
|
+
refresh_events(since, now)
|
41
|
+
display
|
42
|
+
since, now = now, current_now
|
43
|
+
loop_count!
|
44
|
+
sleep 5 if @follow && !ENV["AWS_LOGS_TEST"]
|
45
|
+
end
|
46
|
+
# Refresh and display a final time in case the end_loop gets interrupted by stop_follow!
|
47
|
+
refresh_events(since, now)
|
48
|
+
display
|
49
|
+
rescue Aws::CloudWatchLogs::Errors::ResourceNotFoundException => e
|
50
|
+
puts "ERROR: #{e.class}: #{e.message}".color(:red)
|
51
|
+
puts "Log group #{@log_group_name} not found."
|
52
|
+
end
|
53
|
+
|
54
|
+
def refresh_events(start_time, end_time)
|
55
|
+
@events = []
|
56
|
+
next_token = :start
|
57
|
+
|
58
|
+
# TODO: can hit throttle limit if there are lots of pages
|
59
|
+
while next_token
|
60
|
+
options = {
|
61
|
+
log_group_name: @log_group_name, # required
|
62
|
+
start_time: start_time,
|
63
|
+
end_time: end_time,
|
64
|
+
# limit: 10,
|
65
|
+
}
|
66
|
+
options[:log_stream_names] = @options[:log_stream_names] if @options[:log_stream_names]
|
67
|
+
options[:log_stream_name_prefix] = @options[:log_stream_name_prefix] if @options[:log_stream_name_prefix]
|
68
|
+
options[:filter_pattern] = @options[:filter_pattern] if @options[:filter_pattern]
|
69
|
+
resp = cloudwatchlogs.filter_log_events(options)
|
70
|
+
|
71
|
+
@events += resp.events
|
72
|
+
next_token = resp.next_token
|
73
|
+
end
|
74
|
+
|
75
|
+
@events
|
76
|
+
end
|
77
|
+
|
78
|
+
# Events canduplicated as events can be written to the exact same timestamp.
|
79
|
+
# So also track the last_shown_event_id and prevent duplicate log lines from re-appearing.
|
80
|
+
def display
|
81
|
+
new_events = @events
|
82
|
+
shown_index = new_events.find_index { |e| e.event_id == @last_shown_event_id }
|
83
|
+
if shown_index
|
84
|
+
new_events = @events[shown_index+1..-1] || []
|
85
|
+
end
|
86
|
+
|
87
|
+
new_events.each do |e|
|
88
|
+
time = Time.at(e.timestamp/1000).utc
|
89
|
+
line = [time.to_s.color(:green), e.message]
|
90
|
+
format = @options[:format] || "detailed"
|
91
|
+
line.insert(1, e.log_stream_name.color(:purple)) if format == "detailed"
|
92
|
+
say line.join(' ')
|
93
|
+
end
|
94
|
+
@last_shown_event_id = @events.last&.event_id
|
95
|
+
check_follow_until!
|
96
|
+
end
|
97
|
+
|
98
|
+
def check_follow_until!
|
99
|
+
follow_until = @options[:follow_until]
|
100
|
+
return unless follow_until
|
101
|
+
|
102
|
+
messages = @events.map(&:message)
|
103
|
+
@@end_loop_signal = messages.detect { |m| m.include?(follow_until) }
|
104
|
+
end
|
105
|
+
|
106
|
+
def say(text)
|
107
|
+
ENV["AWS_LOGS_TEST"] ? @output << text : puts(text)
|
108
|
+
end
|
109
|
+
|
110
|
+
def output
|
111
|
+
@output.join("\n") + "\n"
|
112
|
+
end
|
113
|
+
|
114
|
+
def set_trap
|
115
|
+
Signal.trap("INT") {
|
116
|
+
puts "\nCtrl-C detected. Exiting..."
|
117
|
+
exit # immediate exit
|
118
|
+
}
|
119
|
+
end
|
120
|
+
|
121
|
+
@@end_loop_signal = false
|
122
|
+
def self.stop_follow!
|
123
|
+
@@end_loop_signal = true
|
124
|
+
end
|
125
|
+
|
126
|
+
private
|
127
|
+
def initial_since
|
128
|
+
since = @options[:since]
|
129
|
+
seconds = since ? Since.new(since).to_i : Since::DEFAULT
|
130
|
+
(Time.now.to_i - seconds) * 1000 # past 10 minutes in milliseconds
|
131
|
+
end
|
132
|
+
|
133
|
+
def current_now
|
134
|
+
(Time.now.to_i) * 1000 # now in milliseconds
|
135
|
+
end
|
136
|
+
|
137
|
+
def end_loop?
|
138
|
+
return true if @@end_loop_signal
|
139
|
+
max_loop_count && @loop_count >= max_loop_count
|
140
|
+
end
|
141
|
+
|
142
|
+
def loop_count!
|
143
|
+
@loop_count += 1
|
144
|
+
end
|
145
|
+
|
146
|
+
# Useful for specs
|
147
|
+
def max_loop_count
|
148
|
+
@follow ? nil : 1
|
149
|
+
end
|
150
|
+
end
|
151
|
+
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
|
+
}
|