lib/log_parser/log_parser.rb
# frozen_string_literal: true require 'log_parser/logger'require 'log_parser/buffer'require 'log_parser/message'require 'log_parser/pattern' # Parses a log, extracting messages according to a set of {Pattern}.## Instances are single-use; create a new one for every log and parsing run.module LogParser # @return [Array<Message>] # the messages this parser found in the given log. attr_reader :messages # The parser keeps a record of the scope changes it detects. # # **Note:** Only available in debug mode; see {Logger}. # # The keys are line indices. # The values are arrays of strings, with one string per scope change, # in the same order as in the original line. # * Entering a new scope is denoted by # # ``` # push filename # ``` # and # * leaving a scope by # # ``` # pop filename # ``` # Note the extra space after `pop` here; it's there for quaint cosmetic reasons. # # @return [Hash<Integer, Array<String>>] # the scope changes this parser detected in the given log. attr_reader :scope_changes_by_line if Logger.debug? # Parses the given log lines and extracts all messages (of known form). # @return [Array<Message>] def parse skip_empty_lines until empty? parse_next_lines skip_empty_lines end # TODO: Remove duplicates? @messages end protected # Creates a new instance. # # This parser will read lines one by one from the given `log`. # If it is an `IO` or `StringIO`, only those lines currently under investigation will be kept in memory. # # @param [Array<String>,IO,StringIO] log # A set of log lines that will be parsed. def initialize(log) @files = [] @messages = [] @log_line_number = 1 @lines = LogParser::Buffer.new(log) Missing space after `#`. #Logger.debug "Parsing from '#{log}'" @scope_changes_by_line = {} if Logger.debug? end # @abstract # @return [Array<Pattern>] # The set of patterns this parser utilizes to extract messages. def patterns raise NotImplementedError end # Extracts scope changes in the form of stack operations from the given line. # # @abstract # @param [String] _line # @return [Array<String,:pop>] # A list of new scopes this line enters (filename strings) and leaves (`:pop`). # Read stack operations from left to right. def scope_changes(_line) raise NotImplementedError end # @return [true,false] # `true` if (and only if) there are no more lines to consume. def empty? @lines.empty? end private # Forwards the internal buffer up to the next line that contains anything but whitespace. # # @return [void] def skip_empty_lines @lines.first first_nonempty_line = @lines.find_index { |line| /[^\s]/ =~ line } remove_consumed_lines(first_nonempty_line || @lines.buffer_size) end # Reads the log until the next full message, consuming the lines. # Assumes that empty lines have already been skipped. # # @return [Message,nil] # The next message that could be extracted, or `nil` if none could be found. # @raise If parsing already finished.Assignment Branch Condition size for parse_next_lines is too high. [19.82/15]
Method `parse_next_lines` has a Cognitive Complexity of 7 (exceeds 5 allowed). Consider refactoring. def parse_next_lines raise 'Parse already done!' if @lines.empty? line = @lines.first Logger.debug "\nLine #{@log_line_number}: '#{line.strip}'" msg = nil # Use the first pattern that matches. Let's hope that's a good heuristic. # If not, we'll have to let all competitors consume and see who wins -- # which we'd decide how? matching_pattern = patterns.detect { |p| p.begins_at?(line) } if matching_pattern.nil? Logger.debug '- No pattern matches' apply_scope_changes else Logger.debug "- Matched pattern: '#{matching_pattern.class}'" msg = consume_pattern(matching_pattern) @messages.push(msg) unless msg.nil? end if @lines.empty? @lines.close Logger.debug "\nFiles that did not close: #{@files}" end msg end # After reading `i` lines, remove them from the internal buffer using this method. # # @return [void] def remove_consumed_lines(i) @lines.forward(i) @log_line_number += i @scope_changes_by_line[@log_line_number] = [] if Logger.debug? && i.positive? end # Consume as many lines as the given pattern will match. # Assumes that `pattern.begins_at?(@lines.first)` is `true`. # # If applying `pattern` is not successful, this method consumes a single line. # # @param [Pattern] pattern # The pattern to use for matching. # @return [Message,nil] # The message `pattern` produced, if any. def consume_pattern(pattern) # Apply the pattern, i.e. read the next message! # @type [Message] message message, consumed_lines = pattern.read(@lines) message.log_lines = { from: @log_line_number, to: @log_line_number + consumed_lines - 1 } message.source_file ||= @files.last Logger.debug message remove_consumed_lines consumed_lines return message rescue StandardError => e Logger.debug e.to_s remove_consumed_lines 1 return nil end # Extracts the scope changes from the current line and applies them to the file stack `@files`. # # @return [void]Assignment Branch Condition size for apply_scope_changes is too high. [16.52/15]
Method `apply_scope_changes` has a Cognitive Complexity of 13 (exceeds 5 allowed). Consider refactoring. def apply_scope_changes # In the hope that scope changes happen not on the same # line as messages. Gulp. scope_changes(@lines.first).each do |op| if op == :pop left = @files.pop Logger.debug "- Finished source file: '#{left.nil? ? 'nil' : left}'" @scope_changes_by_line[@log_line_number].push "pop #{left}" if Logger.debug? else # op is file name Logger.debug "- Entered source file: '#{op}'" @scope_changes_by_line[@log_line_number].push "push #{op}" if Logger.debug? @files.push(op) end end remove_consumed_lines 1 endend