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