aws-logs 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,6 @@
1
+ class AwsLogs::Completer::Script
2
+ def self.generate
3
+ bash_script = File.expand_path("script.sh", File.dirname(__FILE__))
4
+ puts "source #{bash_script}"
5
+ end
6
+ end
@@ -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,9 @@
1
+ module AwsLogs::Help
2
+ class << self
3
+ def text(namespaced_command)
4
+ path = namespaced_command.to_s.gsub(':','/')
5
+ path = File.expand_path("../help/#{path}.md", __FILE__)
6
+ IO.read(path) if File.exist?(path)
7
+ end
8
+ end
9
+ end
@@ -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,3 @@
1
+ To use, add the following to your `~/.bashrc` or `~/.profile`
2
+
3
+ eval $(aws-logs completion_script)
@@ -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 current 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 patter 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] || true
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
+ while true && !end_loop?
40
+ refresh_events(since, now)
41
+ display
42
+ since, now = now, current_now
43
+ loop_count!
44
+ sleep 5 if @follow && !@@end_loop_signal && !ENV["AWS_LOGS_TEST"]
45
+ end
46
+ rescue Aws::CloudWatchLogs::Errors::ResourceNotFoundException => e
47
+ puts "ERROR: #{e.class}: #{e.message}".color(:red)
48
+ puts "Log group #{@log_group_name} not found."
49
+ end
50
+
51
+ def refresh_events(start_time, end_time)
52
+ @events = []
53
+ next_token = :start
54
+
55
+ # TODO: can hit throttle limit if there are lots of pages
56
+ while next_token
57
+ options = {
58
+ log_group_name: @log_group_name, # required
59
+ start_time: start_time,
60
+ end_time: end_time,
61
+ # limit: 10,
62
+ }
63
+ options[:log_stream_names] = @options[:log_stream_names] if @options[:log_stream_names]
64
+ options[:log_stream_name_prefix] = @options[:log_stream_name_prefix] if @options[:log_stream_name_prefix]
65
+ options[:filter_pattern] = @options[:filter_pattern] if @options[:filter_pattern]
66
+ resp = cloudwatchlogs.filter_log_events(options)
67
+
68
+ @events += resp.events
69
+ next_token = resp.next_token
70
+ end
71
+
72
+ @events
73
+ end
74
+
75
+ # Events canduplicated as events can be written to the exact same timestamp.
76
+ # So also track the last_shown_event_id and prevent duplicate log lines from re-appearing.
77
+ def display
78
+ new_events = @events
79
+ shown_index = new_events.find_index { |e| e.event_id == @last_shown_event_id }
80
+ if shown_index
81
+ new_events = @events[shown_index+1..-1] || []
82
+ end
83
+
84
+ new_events.each do |e|
85
+ time = Time.at(e.timestamp/1000).utc
86
+ line = [time.to_s.color(:green), e.message]
87
+ format = @options[:format] || "detailed"
88
+ line.insert(1, e.log_stream_name.color(:purple)) if format == "detailed"
89
+ say line.join(' ')
90
+ end
91
+ @last_shown_event_id = @events.last&.event_id
92
+ check_follow_until!
93
+ end
94
+
95
+ def check_follow_until!
96
+ follow_until = @options[:follow_until]
97
+ return unless follow_until
98
+
99
+ messages = @events.map(&:message)
100
+ if messages.detect { |m| m.include?(follow_until) }
101
+ @@end_loop_signal = true
102
+ end
103
+ end
104
+
105
+ def say(text)
106
+ ENV["AWS_LOGS_TEST"] ? @output << text : puts(text)
107
+ end
108
+
109
+ def output
110
+ @output.join("\n") + "\n"
111
+ end
112
+
113
+ @@end_loop_signal = false
114
+ def set_trap
115
+ Signal.trap("INT") {
116
+ puts "\nCtrl-C detected. Exiting..."
117
+ @@end_loop_signal = true # useful to control loop
118
+ exit # immediate exit
119
+ }
120
+ end
121
+
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,3 @@
1
+ module AwsLogs
2
+ VERSION = "0.2.0"
3
+ end
@@ -0,0 +1,11 @@
1
+ {
2
+ "events": [
3
+ {
4
+ "log_stream_name": "stream-name",
5
+ "timestamp": 1574888810000,
6
+ "message": "message1",
7
+ "event_id": "event1"
8
+ }
9
+ ],
10
+ "next_token": 2
11
+ }
@@ -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,8 @@
1
+ describe AwsLogs::CLI do
2
+ describe "aws-logs" do
3
+ it "tail" do
4
+ out = execute("AWS_LOGS_NOOP=1 exe/aws-logs tail LOG_GROUP")
5
+ expect(out).to include("Noop test")
6
+ end
7
+ end
8
+ end