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.
@@ -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 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,3 @@
1
+ module AwsLogs
2
+ VERSION = "0.3.2"
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