Skip to content

Instantly share code, notes, and snippets.

@snichme
Created June 22, 2023 07:00
Show Gist options
  • Save snichme/c468dd2aaff61d6bb4e73ec179a46f7f to your computer and use it in GitHub Desktop.
Save snichme/c468dd2aaff61d6bb4e73ec179a46f7f to your computer and use it in GitHub Desktop.
Parse Prometheus in Crystal
require "json"
HASH = 35
NEWLINE = 10
SPACE = 32
CURLY_BRACKET_LEFT = 123
CURLY_BRACKET_RIGHT = 125
EQUALSIGN = 61
BACKSLASH = 92
QUOTE = 34
class Cursor
@buf : Bytes
def initialize(io)
@buf = io.to_slice
@i = 0
end
def peek
@buf[@i]
end
def next
v = @buf[@i]
@i += 1
v
end
def pos
@i
end
def size
@buf.size
end
end
class Reader
record Metric, name : String, attributes : Hash(String, String) | Nil, value : Float64 do
def to_json(json : JSON::Builder)
json.object do
json.field "name", name
if attrs = attributes
json.field "attributes" do
json.object do
attrs.each do |k, v|
json.field k, v
end
end
end
end
if value.nan?
json.field "value", "NaN"
else
json.field "value", value
end
end
end
end
record Comment, type : String, name : String, desc : String
def initialize(io : IO::Memory)
@buf = Cursor.new io
end
def as_json
b = @buf
size = b.size
res = IO::Memory.new
res.write("[".to_slice)
first = true
until b.pos >= size - 1
case b.peek
when HASH
read_comment(b)
when NEWLINE
b.next
else
res.write(",".to_slice) unless first
read_metric(b).to_json(res)
first = false
end
end
res.write("]".to_slice)
res
end
def read : Array(Metric)
b = @buf
size = b.size
res = Array(Metric).new
until b.pos >= size - 1
case b.peek
when HASH
read_comment(b)
when NEWLINE
b.next
else
res << read_metric(b)
end
end
res
end
def read_comment(buf) : Comment
buf.next
buf.next
typ = read_comment_type(buf)
name = read_metric_name(buf)
desc = read_comment_desc(buf)
Comment.new(typ, name, desc)
end
def read_comment_type(buf) : String
line = String::Builder.new
loop do
v = buf.next
break unless upcase_char?(v)
line.write_byte(v)
end
line.to_s
end
def read_comment_desc(buf) : String
line = String::Builder.new
buf.next
loop do
v = buf.next
break if v == NEWLINE
line.write_byte(v)
end
line.to_s
end
def read_metric_name(buf) : String
line = String::Builder.new
loop do
v = buf.peek
break if v == CURLY_BRACKET_LEFT || v == SPACE
line.write_byte(v)
buf.next
end
line.to_s
end
# "rabbitmq_detailed_queue_messages_ready{vhost=\"gyktijvv\",queue=\"global_{9ff82397-54d9-4ccc-813f-f6c9709ba846}_BaseLayerService_0\"} 0\n" +
def read_metric_attribute_name(buf) : String
line = String::Builder.new
loop do
v = buf.peek
break if v == EQUALSIGN
line.write_byte(v)
buf.next
end
line.to_s
end
def lowcase_char?(v)
v >= 97 && v <= 122
end
def upcase_char?(v)
v >= 65 && v <= 90
end
def char?(v)
upcase_char?(v) || lowcase_char?(v)
end
def read_metric_attribute_value(buf) : String
line = String::Builder.new
buf.next # move beyond first "
escaped = false
loop do
v = buf.next
if escaped
case v
when QUOTE, BACKSLASH
line.write_byte(v)
when 110 # lowercase n
line.write_byte(34)
when 116 # lowercase t
line.write_byte(10)
else
line.write_byte(92)
end
escaped = false
next
end
case v
when QUOTE
break
when NEWLINE
raise "label value '#{line.to_s}' contains unescaped new-line"
when BACKSLASH
escaped = true
else
line.write_byte(v)
end
end
line.to_s
end
def read_metric_attributes(buf) : Hash(String, String)
res = Hash(String, String).new
buf.next
loop do
name = read_metric_attribute_name(buf)
buf.next
value = read_metric_attribute_value(buf)
v = buf.next
res[name] = value unless value.empty?
break if v == CURLY_BRACKET_RIGHT
end
res
end
def read_metric_value(buf) : Float64
line = String::Builder.new
loop do
v = buf.peek
break if v == NEWLINE
line.write_byte(v)
buf.next
end
line.to_s.to_f
end
def read_metric(buf) : Metric
name = read_metric_name(buf)
attrs = nil
if buf.peek == CURLY_BRACKET_LEFT
attrs = read_metric_attributes(buf)
end
value = read_metric_value(buf)
Metric.new(name, attrs, value)
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment