yast/yast-journal

View on GitHub
src/lib/y2journal/query.rb

Summary

Maintainability
A
0 mins
Test Coverage
# Copyright (c) 2014 SUSE LLC.
#  All Rights Reserved.

#  This program is free software; you can redistribute it and/or
#  modify it under the terms of version 2 or 3 of the GNU General
#  Public License as published by the Free Software Foundation.

#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.   See the
#  GNU General Public License for more details.

#  You should have received a copy of the GNU General Public License
#  along with this program; if not, contact SUSE LLC.

#  To contact Novell about this file by physical or electronic mail,
#  you may find current contact information at www.suse.com

require "y2journal/entry"
require "y2journal/journalctl"

module Y2Journal
  # A more convenient interface to journalctl options
  class Query
    # Valid keys for the filter hash
    VALID_FILTERS = ["unit", "priority", "match"].freeze

    attr_reader :interval, :filters
    # @return [Hash] options in the format expected by Journalctl
    # @see Y2Journal::Journalctl#initialize
    attr_reader :journalctl_options
    # @return [Array] matches in the format expected by Journalctl
    # @see Y2Journal::Journalctl#initialize
    attr_reader :journalctl_matches
    # @return [Array<Entry>] entries read in the last call to #execute
    attr_reader :entries

    # Creates a new query based on the time interval and some additional filters
    #
    # @param interval [Array,Hash,#to_s,nil] Time interval, can take several forms:
    #   * Array of two elements with starting and ending time
    #   * Hash with two possible keys :since and :until
    #   * An scalar value to be passed to the --boot argument of journalctl
    #   * Nil which means no time restriction
    #   In the first two cases, the values can be Time objects or strings of
    #   any format accepted by journalctl for --until and --since
    # @param filters [Hash] The keys must match one of the VALID_FILTERS.
    #   The values are scalars or arrays with the value for the corresponding
    #   journalctl argument. If the value is an Array, the argument will be
    #   repeated as many times as needed.
    # @see Y2Journal::Journalctl#initialize
    def initialize(interval: nil, filters: {})
      unsupported = filters.keys.select { |k| !VALID_FILTERS.include?(k) }
      if !unsupported.empty?
        raise "Unexpected filters for the query: #{unsupported.join(", ")}"
      end
      @filters = filters
      @interval = interval

      @journalctl_matches = if filters["match"].nil?
        []
      else
        [filters["match"]].flatten
      end
      calculate_options

      @entries = []
    end

    # Reads the list of entries from the system
    def execute
      @entries = Entry.all(
        options: journalctl_options,
        matches: journalctl_matches
      )
    end

    # Representation of the query as a string
    def to_s
      "<interval: #{@interval}, filters: #{@filters}, journalctl_options: "\
        "#{@journalctl_options}, journalctl_matches #{@journalctl_matches}>"
    end

    # four parts: day-of-week date time time-zone
    TIMESTAMP_RX = /\S+\s+\S+\s+\S+\s\S+/
    private_constant :TIMESTAMP_RX

    # Array of system's boots registered in the journal.
    #
    # Each boot is represented by a hash with three elements, with all the keys
    # being symbols and all the values being strings.
    #  * :id => 32-character identifier
    #  * :offset => offset relative to the current boot (String)
    #  * :timestamps: Hash with :since, :until => Time
    def self.boots
      lines = Journalctl.new({ "list-boots" => nil, "quiet" => nil }, []).output.lines
      lines.map do |line|
        # The 'journalctl --list-boots' output looks like this
        # (slightly stripped down, see test/data for full-length examples)
        # -1 a07ac0f240 Sun 2014-12-14 16:50:09 CET Mon 2015-01-26 19:18:43 CET
        #  0 24a9a83ecf Mon 2015-01-26 19:55:33 CET Mon 2015-01-26 20:05:16 CET
        if line.strip =~ /^\s*(-*\d+)\s+(\w+)\s+(#{TIMESTAMP_RX})[— ](#{TIMESTAMP_RX})$/
          {
            id:         Regexp.last_match[2],
            offset:     Regexp.last_match[1],
            timestamps: {
              since: ::Time.parse(Regexp.last_match[3]),
              until: ::Time.parse(Regexp.last_match[4])
            }
          }
        else
          raise "Unexpected output for journalctl --list-boots: #{line}"
        end
      end
    end

  private

    def calculate_options
      @journalctl_options = {}

      # If a interval was specified, translate it to journalctl arguments
      if @interval
        case @interval
        when Array
          @journalctl_options["since"] = @interval[0]
          @journalctl_options["until"] = @interval[1]
        when Hash
          @journalctl_options["since"] = @interval[:since]
          @journalctl_options["until"] = @interval[:until]
        else
          @journalctl_options["boot"] = @interval
        end
      end
      # Remove empty time arguments
      @journalctl_options.reject! { |_, v| v.nil? }

      # Add filters...
      @journalctl_options.merge!(@filters)
      # expect 'match' that is not an option but stored at @journalctl_matches
      @journalctl_options.delete("match")
    end
  end
end