Skip to content

Commit

Permalink
Merge pull request #242 from riemann/improve-parse-error
Browse files Browse the repository at this point in the history
Improve error reporting on parse error
  • Loading branch information
jamtur01 authored Sep 8, 2022
2 parents e541108 + 7411243 commit b19f60b
Show file tree
Hide file tree
Showing 7 changed files with 245 additions and 71 deletions.
11 changes: 11 additions & 0 deletions lib/riemann/tools/health.rb
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,8 @@ def initialize
@swap_enabled = true
end
end

invalidate_cache
end

def alert(service, state, metric, description)
Expand Down Expand Up @@ -139,6 +141,8 @@ def report_int(service, value, report)
end

def report_uptime(uptime)
return unless uptime

description = uptime_to_human(uptime)

if uptime < @limits[:uptime][:critical]
Expand Down Expand Up @@ -262,6 +266,13 @@ def uptime_parser

def uptime
@cached_data[:uptime] ||= uptime_parser.parse(`uptime`)
rescue Racc::ParseError => e
report(
service: 'uptime',
description: "Error parsing uptime: #{e.message}",
state: 'critical',
)
@cached_data[:uptime] = {}
end

def bsd_load
Expand Down
6 changes: 6 additions & 0 deletions lib/riemann/tools/md.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@ def tick
description: status,
state: state,
)
rescue Racc::ParseError => e
report(
service: 'mdstat',
description: "Error parsing mdstat: #{e.message}",
state: 'critical',
)
rescue Errno::ENOENT => e
report(
service: 'mdstat',
Expand Down
95 changes: 51 additions & 44 deletions lib/riemann/tools/mdstat_parser.y
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ rule
devices: devices device { result = val[0].merge(val[1]) }
| { result = {} }

device: IDENTIFIER ':' IDENTIFIER PERSONALITY list_of_devices INTEGER BLOCKS super level '[' INTEGER '/' INTEGER ']' '[' IDENTIFIER ']' progress bitmap { result = { val[0] => val[15] } }
device: IDENTIFIER ':' IDENTIFIER PERSONALITY list_of_devices INTEGER BLOCKS super level '[' INTEGER '/' INTEGER ']' '[' IDENTIFIER ']' progress bitmap { result = { val[0][:value] => val[15][:value] } }

list_of_devices: list_of_devices device
| device
Expand Down Expand Up @@ -43,62 +43,69 @@ end

require 'strscan'

require 'riemann/tools/utils'

---- inner

def parse(text)
s = StringScanner.new(text)
@tokens = []
s = Utils::StringTokenizer.new(text)

until s.eos? do
case
when s.scan(/\n/) then # ignore
when s.scan(/\s+/) then # ignore

when s.scan(/\[=*>.*\]/) then @tokens << [:PROGRESS, s.matched]
when s.scan(/%/) then @tokens << ['%', s.matched]
when s.scan(/,/) then @tokens << [',', s.matched]
when s.scan(/:/) then @tokens << [':', s.matched]
when s.scan(/</) then @tokens << ['<', s.matched]
when s.scan(/=/) then @tokens << ['=', s.matched]
when s.scan(/>/) then @tokens << ['>', s.matched]
when s.scan(/\(/) then @tokens << ['(', s.matched]
when s.scan(/\)/) then @tokens << [')', s.matched]
when s.scan(/\./) then @tokens << ['.', s.matched]
when s.scan(/\//) then @tokens << ['/', s.matched]
when s.scan(/\[/) then @tokens << ['[', s.matched]
when s.scan(/]/) then @tokens << [']', s.matched]
when s.scan(/algorithm/) then @tokens << [:ALGORITHM, s.matched]
when s.scan(/bitmap/) then @tokens << [:BITMAP, s.matched]
when s.scan(/blocks/) then @tokens << [:BLOCKS, s.matched]
when s.scan(/check/) then @tokens << [:CHECK, s.matched]
when s.scan(/chunk/) then @tokens << [:CHUNK, s.matched]
when s.scan(/finish/) then @tokens << [:FINISH, s.matched]
when s.scan(/level/) then @tokens << [:LEVEL, s.matched]
when s.scan(/min/) then @tokens << [:MIN, s.matched]
when s.scan(/pages/) then @tokens << [:PAGES, s.matched]
when s.scan(/(raid([014-6]|10)|linear|multipath|faulty)\b/) then @tokens << [:PERSONALITY, s.matched]
when s.scan(/Personalities/) then @tokens << [:PERSONALITIES, s.matched]
when s.scan(/recovery/) then @tokens << [:RECOVERY, s.matched]
when s.scan(/reshape/) then @tokens << [:RESHAPE, s.matched]
when s.scan(/resync/) then @tokens << [:RESYNC, s.matched]
when s.scan(/speed/) then @tokens << [:SPEED, s.matched]
when s.scan(/super/) then @tokens << [:SUPER, s.matched]
when s.scan(/unused devices/) then @tokens << [:UNUSED_DEVICES, s.matched]
when s.scan(/K\/sec/) then @tokens << [:SPEED_UNIT, s.matched.to_i]
when s.scan(/KB/) then @tokens << [:BYTE_UNIT, s.matched.to_i]
when s.scan(/k/) then @tokens << [:UNIT, s.matched.to_i]
when s.scan(/\d+\.\d+/) then @tokens << [:FLOAT, s.matched.to_i]
when s.scan(/\d+/) then @tokens << [:INTEGER, s.matched.to_i]
when s.scan(/F\b/) then @tokens << [:FAILED, s.matched.to_i]
when s.scan(/\w+/) then @tokens << [:IDENTIFIER, s.matched]
when s.scan(/\n/) then s.push_token(nil)
when s.scan(/\s+/) then s.push_token(nil)

when s.scan(/\[=*>.*\]/) then s.push_token(:PROGRESS)
when s.scan(/%/) then s.push_token('%')
when s.scan(/,/) then s.push_token(',')
when s.scan(/:/) then s.push_token(':')
when s.scan(/</) then s.push_token('<')
when s.scan(/=/) then s.push_token('=')
when s.scan(/>/) then s.push_token('>')
when s.scan(/\(/) then s.push_token('(')
when s.scan(/\)/) then s.push_token(')')
when s.scan(/\./) then s.push_token('.')
when s.scan(/\//) then s.push_token('/')
when s.scan(/\[/) then s.push_token('[')
when s.scan(/]/) then s.push_token(']')
when s.scan(/algorithm/) then s.push_token(:ALGORITHM)
when s.scan(/bitmap/) then s.push_token(:BITMAP)
when s.scan(/blocks/) then s.push_token(:BLOCKS)
when s.scan(/check/) then s.push_token(:CHECK)
when s.scan(/chunk/) then s.push_token(:CHUNK)
when s.scan(/finish/) then s.push_token(:FINISH)
when s.scan(/level/) then s.push_token(:LEVEL)
when s.scan(/min/) then s.push_token(:MIN)
when s.scan(/pages/) then s.push_token(:PAGES)
when s.scan(/(raid([014-6]|10)|linear|multipath|faulty)\b/) then s.push_token(:PERSONALITY)
when s.scan(/Personalities/) then s.push_token(:PERSONALITIES)
when s.scan(/recovery/) then s.push_token(:RECOVERY)
when s.scan(/reshape/) then s.push_token(:RESHAPE)
when s.scan(/resync/) then s.push_token(:RESYNC)
when s.scan(/speed/) then s.push_token(:SPEED)
when s.scan(/super/) then s.push_token(:SUPER)
when s.scan(/unused devices/) then s.push_token(:UNUSED_DEVICES)
when s.scan(/K\/sec/) then s.push_token(:SPEED_UNIT)
when s.scan(/KB/) then s.push_token(:BYTE_UNIT)
when s.scan(/k/) then s.push_token(:UNIT)
when s.scan(/\d+\.\d+/) then s.push_token(:FLOAT, s.matched.to_f)
when s.scan(/\d+/) then s.push_token(:INTEGER, s.matched.to_i)
when s.scan(/F\b/) then s.push_token(:FAILED)
when s.scan(/\w+/) then s.push_token(:IDENTIFIER)
else
raise s.rest
s.unexpected_token
end
end

@tokens = s.tokens

do_parse
end

def next_token
@tokens.shift
end

def on_error(error_token_id, error_value, value_stack)
raise(Racc::ParseError, "parse error on value \"#{error_value[:value]}\" (#{token_to_str(error_token_id)}) on line #{error_value[:lineno]}:\n#{error_value[:line]}\n#{' ' * error_value[:pos]}^#{'~' * (error_value[:value].to_s.length - 1)}")
end
61 changes: 34 additions & 27 deletions lib/riemann/tools/uptime_parser.y
Original file line number Diff line number Diff line change
Expand Up @@ -18,59 +18,66 @@ rule
| UP uptime_min { result = val[1] }
| UP uptime_sec { result = val[1] }

uptime_days: INTEGER DAYS ',' { result = val[0] * 86400 }
uptime_days: INTEGER DAYS ',' { result = val[0][:value] * 86400 }

uptime_hr_min: INTEGER ':' INTEGER { result = val[0] * 3600 + val[2] * 60 }
uptime_hr_min: INTEGER ':' INTEGER { result = val[0][:value] * 3600 + val[2][:value] * 60 }

uptime_hr: INTEGER HRS { result = val[0] * 3600 }
uptime_hr: INTEGER HRS { result = val[0][:value] * 3600 }

uptime_min: INTEGER MINS { result = val[0] * 60 }
uptime_min: INTEGER MINS { result = val[0][:value] * 60 }

uptime_sec: INTEGER SECS { result = val[0] }
uptime_sec: INTEGER SECS { result = val[0][:value] }

users: INTEGER USERS
users: INTEGER USERS { result = val[0][:value] }

load_averages: LOAD_AVERAGES FLOAT FLOAT FLOAT { result = { 1 => val[1], 5 => val[2], 15 => val[3] } }
| LOAD_AVERAGES FLOAT ',' FLOAT ',' FLOAT { result = { 1 => val[1], 5 => val[3], 15 => val[5] } }
load_averages: LOAD_AVERAGES FLOAT FLOAT FLOAT { result = { 1 => val[1][:value], 5 => val[2][:value], 15 => val[3][:value] } }
| LOAD_AVERAGES FLOAT ',' FLOAT ',' FLOAT { result = { 1 => val[1][:value], 5 => val[3][:value], 15 => val[5][:value] } }
end


---- header

require 'strscan'

require 'riemann/tools/utils'

---- inner

def parse(text)
s = StringScanner.new(text)
@tokens = []
s = Utils::StringTokenizer.new(text)

until s.eos? do
case
when s.scan(/\n/) then # ignore
when s.scan(/\s+/) then # ignore

when s.scan(/:/) then @tokens << [':', s.matched]
when s.scan(/,/) then @tokens << [',', s.matched]
when s.scan(/\d+[,.]\d+/) then @tokens << [:FLOAT, s.matched.sub(',', '.').to_f]
when s.scan(/\d+/) then @tokens << [:INTEGER, s.matched.to_i]
when s.scan(/AM/) then @tokens << [:AM, s.matched]
when s.scan(/PM/) then @tokens << [:PM, s.matched]
when s.scan(/up/) then @tokens << [:UP, s.matched]
when s.scan(/days?/) then @tokens << [:DAYS, s.matched]
when s.scan(/hrs?/) then @tokens << [:HRS, s.matched]
when s.scan(/mins?/) then @tokens << [:MINS, s.matched]
when s.scan(/secs?/) then @tokens << [:SECS, s.matched]
when s.scan(/users?/) then @tokens << [:USERS, s.matched]
when s.scan(/load averages?:/) then @tokens << [:LOAD_AVERAGES, s.matched]
when s.scan(/\n/) then s.push_token(nil)
when s.scan(/\s+/) then s.push_token(nil)

when s.scan(/:/) then s.push_token(':')
when s.scan(/,/) then s.push_token(',')
when s.scan(/\d+[,.]\d+/) then s.push_token(:FLOAT, s.matched.sub(',', '.').to_f)
when s.scan(/\d+/) then s.push_token(:INTEGER, s.matched.to_i)
when s.scan(/AM/) then s.push_token(:AM)
when s.scan(/PM/) then s.push_token(:PM)
when s.scan(/up/) then s.push_token(:UP)
when s.scan(/days?/) then s.push_token(:DAYS)
when s.scan(/hrs?/) then s.push_token(:HRS)
when s.scan(/mins?/) then s.push_token(:MINS)
when s.scan(/secs?/) then s.push_token(:SECS)
when s.scan(/users?/) then s.push_token(:USERS)
when s.scan(/load averages?:/) then s.push_token(:LOAD_AVERAGES)
else
raise s.rest
raise s.unexpected_token
end
end

@tokens = s.tokens

do_parse
end

def next_token
@tokens.shift
end

def on_error(error_token_id, error_value, value_stack)
raise(Racc::ParseError, "parse error on value \"#{error_value[:value]}\" (#{token_to_str(error_token_id)}) on line #{error_value[:lineno]}:\n#{error_value[:line]}\n#{' ' * error_value[:pos]}^#{'~' * (error_value[:value].to_s.length - 1)}")
end
48 changes: 48 additions & 0 deletions lib/riemann/tools/utils.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,54 @@
module Riemann
module Tools
module Utils # :nodoc:
class StringTokenizer
attr_reader :tokens

def initialize(text)
@scanner = StringScanner.new(text)

@lineno = 1
@pos = 0
@line = next_line
@tokens = []
end

def scan(expression)
@scanner.scan(expression)
end

def eos?
@scanner.eos?
end

def matched
@scanner.matched
end

def next_line
(@scanner.check_until(/\n/) || @scanner.rest).chomp
end

def push_token(token, value = nil)
value ||= @scanner.matched

if value == "\n"
@lineno += 1
@line = next_line
@pos = pos = 0
else
pos = @pos
@pos += @scanner.matched.length
end

@tokens << [token, { value: value, line: @line, lineno: @lineno, pos: pos }] if token
end

def unexpected_token
raise(Racc::ParseError, "unexpected data on line #{@lineno}:\n#{@line}\n#{' ' * @pos}^")
end
end

def reverse_numeric_sort_with_header(data, header: 1, count: 10)
lines = data.chomp.split("\n")
header = lines.shift(header)
Expand Down
38 changes: 38 additions & 0 deletions spec/riemann/tools/health_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -111,4 +111,42 @@
end
end
end

context '#bsd_uptime' do
context 'when given unexpected data' do
before do
allow(subject).to receive(:`).with('uptime').and_return(<<~DOCUMENT)
10:27:42 up 20:05, load averages: 0.79, 0.50, 0.44
DOCUMENT
end

it 'reports critical state' do
allow(subject).to receive(:report)
subject.bsd_uptime
expect(subject).to have_received(:report).with(service: 'uptime', description: <<~DESCRIPTION.chomp, state: 'critical')
Error parsing uptime: parse error on value "load averages:" (LOAD_AVERAGES) on line 1:
10:27:42 up 20:05, load averages: 0.79, 0.50, 0.44
^~~~~~~~~~~~~~
DESCRIPTION
end
end

context 'when given malformed data' do
before do
allow(subject).to receive(:`).with('uptime').and_return(<<~DOCUMENT)
10:27:42 up 20:05, 1 user, load average: 0.79, 0.50, 0.44 [IO: 0.15, 0.12, 0.08 CPU: 0.64, 0.38, 0.35]
DOCUMENT
end

it 'reports critical state' do
allow(subject).to receive(:report)
subject.bsd_uptime
expect(subject).to have_received(:report).with(service: 'uptime', description: <<~DESCRIPTION.chomp, state: 'critical')
Error parsing uptime: unexpected data on line 1:
10:27:42 up 20:05, 1 user, load average: 0.79, 0.50, 0.44 [IO: 0.15, 0.12, 0.08 CPU: 0.64, 0.38, 0.35]
^
DESCRIPTION
end
end
end
end
Loading

0 comments on commit b19f60b

Please sign in to comment.