aws-logs 0.1.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,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