415bf0cce1
Single Go binary for the ClickHouse log store (logs.raw): chlog with cat/tail/grep subcommands, plus chcat/chtail/chgrep argv[0]-dispatched symlink entrypoints. Every query is time-bounded and fully parameterized; chgrep guards wide unfiltered scans. Ships nfpm RPM with completions and woodpecker PR/tag pipelines mirroring node-lookup.
132 lines
3.4 KiB
Go
132 lines
3.4 KiB
Go
package chlog
|
|
|
|
import (
|
|
"fmt"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
const Table = "logs.raw"
|
|
|
|
// Filter describes one bounded query against logs.raw. Since/Until are
|
|
// mandatory: the table has no text index, so every query must be time-bounded.
|
|
type Filter struct {
|
|
Since time.Time
|
|
Until time.Time
|
|
|
|
Namespace string
|
|
Host string
|
|
Pod string
|
|
Container string
|
|
App string
|
|
Severity string
|
|
Stream string
|
|
Source string
|
|
|
|
Pattern string
|
|
Regex bool
|
|
IgnoreCase bool
|
|
Fields map[string]string
|
|
|
|
Limit uint64
|
|
}
|
|
|
|
// Selective reports whether the filter narrows the scan enough to be cheap:
|
|
// any of namespace, host, or app restricts to a small slice of the table.
|
|
func (f Filter) Selective() bool {
|
|
return f.Namespace != "" || f.Host != "" || f.App != ""
|
|
}
|
|
|
|
type Query struct {
|
|
SQL string
|
|
Params map[string]string
|
|
}
|
|
|
|
const selectColumns = "timestamp, host, source, namespace, pod, container, stream, severity, message, labels, fields"
|
|
|
|
func chTime(t time.Time) string {
|
|
return strconv.FormatInt(t.UnixMilli(), 10)
|
|
}
|
|
|
|
// Build renders a fully parameterized query. All user-supplied values travel
|
|
// as HTTP {name:Type} parameters, never interpolated into the SQL text.
|
|
func Build(f Filter) (Query, error) {
|
|
if f.Since.IsZero() || f.Until.IsZero() {
|
|
return Query{}, fmt.Errorf("query must be time-bounded: since/until missing")
|
|
}
|
|
if !f.Since.Before(f.Until) {
|
|
return Query{}, fmt.Errorf("empty time range: since %s is not before until %s",
|
|
f.Since.UTC().Format(time.RFC3339), f.Until.UTC().Format(time.RFC3339))
|
|
}
|
|
|
|
params := map[string]string{
|
|
"since_ms": chTime(f.Since),
|
|
"until_ms": chTime(f.Until),
|
|
}
|
|
where := []string{
|
|
"timestamp >= fromUnixTimestamp64Milli({since_ms:Int64})",
|
|
"timestamp < fromUnixTimestamp64Milli({until_ms:Int64})",
|
|
}
|
|
|
|
addEq := func(column, name, value string) {
|
|
if value == "" {
|
|
return
|
|
}
|
|
where = append(where, fmt.Sprintf("%s = {%s:String}", column, name))
|
|
params[name] = value
|
|
}
|
|
addEq("namespace", "ns", f.Namespace)
|
|
addEq("host", "host", f.Host)
|
|
addEq("pod", "pod", f.Pod)
|
|
addEq("container", "container", f.Container)
|
|
addEq("stream", "stream", f.Stream)
|
|
addEq("source", "source", f.Source)
|
|
addEq("labels['app']", "app", f.App)
|
|
|
|
if f.Severity != "" {
|
|
where = append(where, "lowerUTF8(severity) = {severity:String}")
|
|
params["severity"] = strings.ToLower(f.Severity)
|
|
}
|
|
|
|
if f.Pattern != "" {
|
|
switch {
|
|
case f.Regex:
|
|
pat := f.Pattern
|
|
if f.IgnoreCase {
|
|
pat = "(?i)" + pat
|
|
}
|
|
where = append(where, "match(message, {pattern:String})")
|
|
params["pattern"] = pat
|
|
case f.IgnoreCase:
|
|
where = append(where, "positionCaseInsensitive(message, {pattern:String}) > 0")
|
|
params["pattern"] = f.Pattern
|
|
default:
|
|
where = append(where, "position(message, {pattern:String}) > 0")
|
|
params["pattern"] = f.Pattern
|
|
}
|
|
}
|
|
|
|
keys := make([]string, 0, len(f.Fields))
|
|
for k := range f.Fields {
|
|
keys = append(keys, k)
|
|
}
|
|
sort.Strings(keys)
|
|
for i, k := range keys {
|
|
kn := fmt.Sprintf("fk%d", i)
|
|
vn := fmt.Sprintf("fv%d", i)
|
|
where = append(where, fmt.Sprintf("fields[{%s:String}] = {%s:String}", kn, vn))
|
|
params[kn] = k
|
|
params[vn] = f.Fields[k]
|
|
}
|
|
|
|
sql := fmt.Sprintf("SELECT %s FROM %s WHERE %s ORDER BY timestamp ASC",
|
|
selectColumns, Table, strings.Join(where, " AND "))
|
|
if f.Limit > 0 {
|
|
sql += " LIMIT {limit:UInt64}"
|
|
params["limit"] = strconv.FormatUint(f.Limit, 10)
|
|
}
|
|
return Query{SQL: sql, Params: params}, nil
|
|
}
|