Package / arch (push) Canceled after 0s
Package / user (push) Canceled after 0s
Package / deb (push) Canceled after 0s
Package / freebsd (push) Canceled after 0s
Package / image (push) Canceled after 0s
Test / test (push) Canceled after 0s
Test / e2e (push) Canceled after 0s
266 lines
4.9 KiB
Go
266 lines
4.9 KiB
Go
package logline
|
|
|
|
import (
|
|
"encoding/json"
|
|
"strconv"
|
|
)
|
|
|
|
// JSON token kinds, and kText for a logfmt value, unquoted already
|
|
const (
|
|
kNone byte = 0
|
|
kStr byte = 's'
|
|
kNum byte = 'n'
|
|
kBool byte = 'b'
|
|
kNull byte = 'z'
|
|
kObj byte = 'o'
|
|
kArr byte = 'a'
|
|
kText byte = 't'
|
|
)
|
|
|
|
func skipSpace(s string, i int) int {
|
|
for i < len(s) && (s[i] == ' ' || s[i] == '\t' || s[i] == '\r' || s[i] == '\n') {
|
|
i++
|
|
}
|
|
return i
|
|
}
|
|
|
|
func isNumByte(c byte) bool {
|
|
return c == '-' || c == '+' || c == '.' || c == 'e' || c == 'E' || (c >= '0' && c <= '9')
|
|
}
|
|
|
|
// scanString returns the index past the closing quote; i indexes the opener.
|
|
func scanString(s string, i int) (int, bool) {
|
|
for i++; i < len(s); i++ {
|
|
switch s[i] {
|
|
case '\\':
|
|
i++
|
|
case '"':
|
|
return i + 1, true
|
|
}
|
|
}
|
|
return i, false
|
|
}
|
|
|
|
// scanValue returns the end index and kind of the value starting at i.
|
|
func scanValue(s string, i int) (int, byte, bool) {
|
|
if i >= len(s) {
|
|
return i, kNone, false
|
|
}
|
|
switch c := s[i]; c {
|
|
case '"':
|
|
e, ok := scanString(s, i)
|
|
return e, kStr, ok
|
|
|
|
case '{', '[':
|
|
opener, closer, kind := byte('{'), byte('}'), kObj
|
|
if c == '[' {
|
|
opener, closer, kind = '[', ']', kArr
|
|
}
|
|
depth := 0
|
|
for i < len(s) {
|
|
ch := s[i]
|
|
if ch == '"' {
|
|
e, ok := scanString(s, i)
|
|
if !ok {
|
|
return e, kind, false
|
|
}
|
|
i = e
|
|
continue
|
|
}
|
|
if ch == opener {
|
|
depth++
|
|
} else if ch == closer {
|
|
depth--
|
|
if depth == 0 {
|
|
return i + 1, kind, true
|
|
}
|
|
}
|
|
i++
|
|
}
|
|
return i, kind, false
|
|
|
|
case 't', 'f', 'n':
|
|
kind := kBool
|
|
if c == 'n' {
|
|
kind = kNull
|
|
}
|
|
for i < len(s) && s[i] >= 'a' && s[i] <= 'z' {
|
|
i++
|
|
}
|
|
return i, kind, true
|
|
|
|
default:
|
|
e := i
|
|
for e < len(s) && isNumByte(s[e]) {
|
|
e++
|
|
}
|
|
return e, kNum, e > i
|
|
}
|
|
}
|
|
|
|
// eachField calls fn for each member of the object at i, which must index '{',
|
|
// until fn returns false; it reports whether the object is well formed.
|
|
func eachField(s string, i int, fn func(key, val string, kind byte) bool) bool {
|
|
if i >= len(s) || s[i] != '{' {
|
|
return false
|
|
}
|
|
i = skipSpace(s, i+1)
|
|
if i < len(s) && s[i] == '}' {
|
|
return true
|
|
}
|
|
for i < len(s) {
|
|
if s[i] != '"' {
|
|
return false
|
|
}
|
|
ke, ok := scanString(s, i)
|
|
if !ok {
|
|
return false
|
|
}
|
|
key := s[i+1 : ke-1]
|
|
|
|
i = skipSpace(s, ke)
|
|
if i >= len(s) || s[i] != ':' {
|
|
return false
|
|
}
|
|
i = skipSpace(s, i+1)
|
|
|
|
vs := i
|
|
ve, kind, ok := scanValue(s, i)
|
|
if !ok {
|
|
return false
|
|
}
|
|
if !fn(key, s[vs:ve], kind) {
|
|
return true
|
|
}
|
|
|
|
i = skipSpace(s, ve)
|
|
if i >= len(s) {
|
|
return false
|
|
}
|
|
switch s[i] {
|
|
case ',':
|
|
i = skipSpace(s, i+1)
|
|
case '}':
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// eachItem calls fn for each element of the array at i, which must index '[';
|
|
// it reports whether the array is well formed.
|
|
func eachItem(s string, i int, fn func(val string, kind byte)) bool {
|
|
if i >= len(s) || s[i] != '[' {
|
|
return false
|
|
}
|
|
i = skipSpace(s, i+1)
|
|
if i < len(s) && s[i] == ']' {
|
|
return true
|
|
}
|
|
for i < len(s) {
|
|
ve, kind, ok := scanValue(s, i)
|
|
if !ok {
|
|
return false
|
|
}
|
|
fn(s[i:ve], kind)
|
|
i = skipSpace(s, ve)
|
|
if i >= len(s) {
|
|
return false
|
|
}
|
|
switch s[i] {
|
|
case ',':
|
|
i = skipSpace(s, i+1)
|
|
case ']':
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// eachPair calls fn for each pair of a logfmt line, key=value with the value
|
|
// bare or quoted as Go quotes it; it reports whether s is such pairs alone.
|
|
func eachPair(s string, fn func(key, val string)) bool {
|
|
bare := func(i int) int {
|
|
for i < len(s) && s[i] > ' ' && s[i] != '=' && s[i] != '"' {
|
|
i++
|
|
}
|
|
return i
|
|
}
|
|
for i := skipSpace(s, 0); i < len(s); i = skipSpace(s, i) {
|
|
k := i
|
|
if i = bare(k); i == k || i == len(s) || s[i] != '=' {
|
|
return false
|
|
}
|
|
key, v := s[k:i], i+1
|
|
i = bare(v)
|
|
val := s[v:i]
|
|
if i == v && i < len(s) && s[i] == '"' {
|
|
var ok bool
|
|
var err error
|
|
if i, ok = scanString(s, v); !ok {
|
|
return false
|
|
}
|
|
if val, err = strconv.Unquote(s[v:i]); err != nil {
|
|
return false
|
|
}
|
|
}
|
|
if i < len(s) && s[i] > ' ' {
|
|
return false
|
|
}
|
|
fn(key, val)
|
|
}
|
|
return true
|
|
}
|
|
|
|
// unquote returns the content of a string token, decoding only when it holds
|
|
// an escape; any other token, a logfmt value too, is returned as it is.
|
|
func unquote(tok string, kind byte) string {
|
|
if kind != kStr || len(tok) < 2 {
|
|
return tok
|
|
}
|
|
in := tok[1 : len(tok)-1]
|
|
for i := 0; i < len(in); i++ {
|
|
if in[i] == '\\' {
|
|
var s string
|
|
if json.Unmarshal([]byte(tok), &s) == nil {
|
|
return s
|
|
}
|
|
break
|
|
}
|
|
}
|
|
return in
|
|
}
|
|
|
|
// parseInt64 parses a complete decimal integer token without allocating.
|
|
func parseInt64(s string) (int64, bool) {
|
|
if len(s) == 0 {
|
|
return 0, false
|
|
}
|
|
i, neg := 0, false
|
|
if s[0] == '-' || s[0] == '+' {
|
|
neg = s[0] == '-'
|
|
i++
|
|
}
|
|
if i >= len(s) {
|
|
return 0, false
|
|
}
|
|
var v int64
|
|
for ; i < len(s); i++ {
|
|
if s[i] < '0' || s[i] > '9' {
|
|
return 0, false
|
|
}
|
|
v = v*10 + int64(s[i]-'0')
|
|
if v < 0 {
|
|
return 0, false
|
|
}
|
|
}
|
|
if neg {
|
|
v = -v
|
|
}
|
|
return v, true
|
|
}
|