class AwsLogs::Tail

Public Class Methods

new(options={}) click to toggle source
# File lib/aws_logs/tail.rb, line 7
def initialize(options={})
  @options = options
  @log_group_name = options[:log_group_name]
  # Setting to ensure matches default CLI option
  @follow = @options[:follow].nil? ? true : @options[:follow]

  @loop_count = 0
  @output = [] # for specs
  reset
  set_trap
end
stop_follow!() click to toggle source
# File lib/aws_logs/tail.rb, line 131
def self.stop_follow!
  @@end_loop_signal = true
end

Public Instance Methods

check_follow_until!() click to toggle source
# File lib/aws_logs/tail.rb, line 104
def check_follow_until!
  follow_until = @options[:follow_until]
  return unless follow_until

  messages = @events.map(&:message)
  @@end_loop_signal = messages.detect { |m| m.include?(follow_until) }
end
display() click to toggle source

Events canduplicated as events can be written to the exact same timestamp. So also track the last_shown_event_id and prevent duplicate log lines from re-appearing.

# File lib/aws_logs/tail.rb, line 86
def display
  new_events = @events
  shown_index = new_events.find_index { |e| e.event_id == @last_shown_event_id }
  if shown_index
    new_events = @events[shown_index+1..-1] || []
  end

  new_events.each do |e|
    time = Time.at(e.timestamp/1000).utc
    line = [time.to_s.color(:green), e.message]
    format = @options[:format] || "detailed"
    line.insert(1, e.log_stream_name.color(:purple)) if format == "detailed"
    say line.join(' ')
  end
  @last_shown_event_id = @events.last&.event_id
  check_follow_until!
end
output() click to toggle source
# File lib/aws_logs/tail.rb, line 116
def output
  @output.join("\n") + "\n"
end
refresh_events(start_time, end_time) click to toggle source
# File lib/aws_logs/tail.rb, line 58
def refresh_events(start_time, end_time)
  @events = []
  next_token = :start

  # TODO: can hit throttle limit if there are lots of pages
  while next_token
    options = {
      log_group_name: @log_group_name, # required
      start_time: start_time,
      end_time: end_time,
      # limit: 1000,
      # interleaved: true,
    }
    options[:log_stream_names] = @options[:log_stream_names] if @options[:log_stream_names]
    options[:log_stream_name_prefix] = @options[:log_stream_name_prefix] if @options[:log_stream_name_prefix]
    options[:filter_pattern] = @options[:filter_pattern] if @options[:filter_pattern]
    options[:next_token] = next_token if next_token != :start && !next_token.nil?
    resp = cloudwatchlogs.filter_log_events(options)

    @events += resp.events
    next_token = resp.next_token
  end

  @events
end
reset() click to toggle source
# File lib/aws_logs/tail.rb, line 19
def reset
  @events = [] # constantly replaced with recent events
  @last_shown_event_id = nil
  @completed = nil
end
run() click to toggle source

The start and end time is useful to limit results and make the API fast. We'll leverage it like so:

1. load all events from an initial since time
2. after that load events pass that first window

It's a sliding window of time we're using.

# File lib/aws_logs/tail.rb, line 32
def run
  if ENV['AWS_LOGS_NOOP']
    puts "Noop test"
    return
  end

  # We overlap the sliding window because CloudWatch logs can receive or send the logs out of order.
  # For example, a bunch of logs can all come in at the same second, but they haven't registered to CloudWatch logs
  # yet. If we don't overlap the sliding window then we'll miss the logs that were delayed in registering.
  overlap = 60*1000 # overlap the sliding window by a minute
  since, now = initial_since, current_now
  until end_loop?
    refresh_events(since, now)
    display
    since, now = now-overlap, current_now
    loop_count!
    sleep 5 if @follow && !ENV["AWS_LOGS_TEST"]
  end
  # Refresh and display a final time in case the end_loop gets interrupted by stop_follow!
  refresh_events(since, now)
  display
rescue Aws::CloudWatchLogs::Errors::ResourceNotFoundException => e
  puts "ERROR: #{e.class}: #{e.message}".color(:red)
  puts "Log group #{@log_group_name} not found."
end
say(text) click to toggle source
# File lib/aws_logs/tail.rb, line 112
def say(text)
  ENV["AWS_LOGS_TEST"] ? @output << text : puts(text)
end
set_trap() click to toggle source
# File lib/aws_logs/tail.rb, line 120
def set_trap
  Signal.trap("INT") {
    puts "\nCtrl-C detected. Exiting..."
    exit # immediate exit
  }
end

Private Instance Methods

current_now() click to toggle source
# File lib/aws_logs/tail.rb, line 142
def current_now
  (Time.now.to_i) * 1000 # now in milliseconds
end
end_loop?() click to toggle source
# File lib/aws_logs/tail.rb, line 146
def end_loop?
  return true if @@end_loop_signal
  max_loop_count && @loop_count >= max_loop_count
end
initial_since() click to toggle source
# File lib/aws_logs/tail.rb, line 136
def initial_since
  since = @options[:since]
  seconds = since ? Since.new(since).to_i : Since::DEFAULT
  (Time.now.to_i - seconds) * 1000 # past 10 minutes in milliseconds
end
loop_count!() click to toggle source
# File lib/aws_logs/tail.rb, line 151
def loop_count!
  @loop_count += 1
end
max_loop_count() click to toggle source

Useful for specs

# File lib/aws_logs/tail.rb, line 156
def max_loop_count
  @follow ? nil : 1
end