v0.1.0 initial commit
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
package filter
|
||||
|
||||
import "github.com/lixenwraith/vif-log/internal/logfile"
|
||||
|
||||
func init() {
|
||||
Register(Desc{Kind: "fields", Help: "regexp over the parsed fields, smart-case", New: newFields})
|
||||
}
|
||||
|
||||
// Fields applies a regex to the fields column. This forces disk reads for
|
||||
// surviving rows to evaluate the parsed JSON, so it evaluates last.
|
||||
type Fields struct {
|
||||
Query string
|
||||
pat Pattern
|
||||
}
|
||||
|
||||
func newFields(arg string) (Filter, error) {
|
||||
p, err := NewPattern(arg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Fields{Query: arg, pat: p}, nil
|
||||
}
|
||||
|
||||
func (f *Fields) Kind() string { return "fields" }
|
||||
|
||||
func (f *Fields) Needs() Need {
|
||||
if f.pat.Empty() {
|
||||
return 0
|
||||
}
|
||||
return NeedFields
|
||||
}
|
||||
|
||||
func (f *Fields) Match(c *Ctx) bool {
|
||||
if f.pat.Empty() {
|
||||
return true
|
||||
}
|
||||
// Scopes the regex exactly to the rendered "key=value" string,
|
||||
// excluding the discriminator (msg).
|
||||
return f.pat.MatchBytes(c.Record().ColumnBytes(logfile.ColFields))
|
||||
}
|
||||
|
||||
func (f *Fields) Label() string {
|
||||
if f.pat.Empty() {
|
||||
return ""
|
||||
}
|
||||
return "fields:" + f.Query
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
package filter
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
"github.com/lixenwraith/vif-log/internal/logfile"
|
||||
)
|
||||
|
||||
// Need declares what a filter must touch beyond the index row.
|
||||
type Need uint8
|
||||
|
||||
const (
|
||||
NeedRaw Need = 1 << iota // raw line bytes
|
||||
NeedFields // parsed fields
|
||||
)
|
||||
|
||||
// Ctx is the per-record evaluation context. Raw bytes and parsed fields are
|
||||
// fetched lazily, so index-only predicates never touch the file.
|
||||
type Ctx struct {
|
||||
Idx *logfile.Index
|
||||
Meta logfile.Meta
|
||||
I int
|
||||
|
||||
rd *logfile.Reader
|
||||
raw []byte
|
||||
rec logfile.Record
|
||||
rawOK bool
|
||||
recOK bool
|
||||
}
|
||||
|
||||
// Bind attaches the index and the line reader used for raw access.
|
||||
func (c *Ctx) Bind(idx *logfile.Index, rd *logfile.Reader) { c.Idx, c.rd = idx, rd }
|
||||
|
||||
// Reset points the context at record i.
|
||||
func (c *Ctx) Reset(i int, m logfile.Meta) {
|
||||
c.I, c.Meta = i, m
|
||||
c.raw, c.rawOK, c.recOK = nil, false, false
|
||||
}
|
||||
|
||||
// Raw returns the record's line bytes, nil when unavailable.
|
||||
func (c *Ctx) Raw() []byte {
|
||||
if !c.rawOK {
|
||||
c.rawOK = true
|
||||
if c.rd != nil {
|
||||
c.raw, _ = c.rd.Line(c.Meta)
|
||||
}
|
||||
}
|
||||
return c.raw
|
||||
}
|
||||
|
||||
// Record returns the parsed record.
|
||||
func (c *Ctx) Record() *logfile.Record {
|
||||
if !c.recOK {
|
||||
c.recOK = true
|
||||
c.rec.Parse(c.Meta, c.Raw())
|
||||
}
|
||||
return &c.rec
|
||||
}
|
||||
|
||||
// Filter is one predicate in the stack.
|
||||
type Filter interface {
|
||||
Kind() string
|
||||
Label() string
|
||||
Needs() Need
|
||||
Match(*Ctx) bool
|
||||
}
|
||||
|
||||
// Entry wraps a filter with the stack-level toggles, so negation and disabling
|
||||
// need no per-filter code.
|
||||
type Entry struct {
|
||||
F Filter
|
||||
Enabled bool
|
||||
Negate bool
|
||||
}
|
||||
|
||||
// Stack is the composed filter chain, evaluated cheapest-first.
|
||||
type Stack struct {
|
||||
Entries []Entry
|
||||
order []int
|
||||
needs Need
|
||||
}
|
||||
|
||||
// Compile recomputes the evaluation order; call after mutating Entries.
|
||||
func (s *Stack) Compile() {
|
||||
s.order = s.order[:0]
|
||||
s.needs = 0
|
||||
for i, e := range s.Entries {
|
||||
if !e.Enabled {
|
||||
continue
|
||||
}
|
||||
s.order = append(s.order, i)
|
||||
s.needs |= e.F.Needs()
|
||||
}
|
||||
// Index-only predicates first: raw readers then only see survivors.
|
||||
sort.SliceStable(s.order, func(a, b int) bool {
|
||||
return s.Entries[s.order[a]].F.Needs() < s.Entries[s.order[b]].F.Needs()
|
||||
})
|
||||
}
|
||||
|
||||
// Needs reports the union of the enabled filters' requirements.
|
||||
func (s *Stack) Needs() Need { return s.needs }
|
||||
|
||||
// Match evaluates the enabled filters in cost order.
|
||||
func (s *Stack) Match(c *Ctx) bool {
|
||||
for _, i := range s.order {
|
||||
e := s.Entries[i]
|
||||
if e.F.Match(c) == e.Negate {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Add appends an enabled filter and recompiles.
|
||||
func (s *Stack) Add(f Filter) {
|
||||
s.Entries = append(s.Entries, Entry{F: f, Enabled: true})
|
||||
s.Compile()
|
||||
}
|
||||
|
||||
// Find returns the first entry of the given kind.
|
||||
func (s *Stack) Find(kind string) (*Entry, bool) {
|
||||
for i := range s.Entries {
|
||||
if s.Entries[i].F.Kind() == kind {
|
||||
return &s.Entries[i], true
|
||||
}
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// Set replaces the entry of f's kind, appending when absent.
|
||||
func (s *Stack) Set(f Filter) {
|
||||
for i := range s.Entries {
|
||||
if s.Entries[i].F.Kind() == f.Kind() {
|
||||
s.Entries[i].F, s.Entries[i].Enabled = f, true
|
||||
s.Compile()
|
||||
return
|
||||
}
|
||||
}
|
||||
s.Add(f)
|
||||
}
|
||||
|
||||
// Remove drops the entry of the given kind.
|
||||
func (s *Stack) Remove(kind string) bool {
|
||||
for i := range s.Entries {
|
||||
if s.Entries[i].F.Kind() == kind {
|
||||
s.Entries = append(s.Entries[:i], s.Entries[i+1:]...)
|
||||
s.Compile()
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Summary renders the stack for the status bar: (disabled) and !negated.
|
||||
func (s *Stack) Summary() []string {
|
||||
out := make([]string, 0, len(s.Entries))
|
||||
for _, e := range s.Entries {
|
||||
l := e.F.Label()
|
||||
if l == "" {
|
||||
continue // filter is inert; the header strip or nothing reports it
|
||||
}
|
||||
if e.Negate {
|
||||
l = "!" + l
|
||||
}
|
||||
if !e.Enabled {
|
||||
l = "(" + l + ")"
|
||||
}
|
||||
out = append(out, l)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// Desc describes a registered filter kind for menus and help.
|
||||
type Desc struct {
|
||||
Kind string
|
||||
Help string
|
||||
New func(arg string) (Filter, error)
|
||||
}
|
||||
|
||||
var registry []Desc
|
||||
|
||||
// Register makes a filter kind constructible by name. A new kind is a new file
|
||||
// plus one Register call; no render-path switch changes.
|
||||
func Register(d Desc) { registry = append(registry, d) }
|
||||
|
||||
// Kinds returns the registered filter kinds.
|
||||
func Kinds() []Desc { return registry }
|
||||
|
||||
// New constructs a registered filter.
|
||||
func New(kind, arg string) (Filter, error) {
|
||||
for _, d := range registry {
|
||||
if d.Kind == kind {
|
||||
return d.New(arg)
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("filter: unknown kind %q", kind)
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package filter
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/lixenwraith/vif-log/internal/logfile"
|
||||
)
|
||||
|
||||
func init() {
|
||||
Register(Desc{Kind: "find", Help: "regexp over the focused column, smart-case", New: newFind})
|
||||
}
|
||||
|
||||
// Find keeps records whose focused column matches the pattern. Time, tick, sub
|
||||
// and msg resolve from the index row, so only fields and all read the file.
|
||||
type Find struct {
|
||||
Query string
|
||||
Col logfile.Column
|
||||
pat Pattern
|
||||
scratch []byte
|
||||
}
|
||||
|
||||
// NewFind returns an inert find filter.
|
||||
func NewFind() *Find { return &Find{} }
|
||||
|
||||
func newFind(arg string) (Filter, error) {
|
||||
f := NewFind()
|
||||
if err := f.Set(arg, logfile.ColAll); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return f, nil
|
||||
}
|
||||
|
||||
// Set replaces the pattern and its column scope.
|
||||
func (f *Find) Set(q string, col logfile.Column) error {
|
||||
p, err := NewPattern(q)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
f.Query, f.Col, f.pat = q, col, p
|
||||
return nil
|
||||
}
|
||||
|
||||
// Active reports whether the filter constrains anything.
|
||||
func (f *Find) Active() bool { return !f.pat.Empty() }
|
||||
|
||||
func (f *Find) Kind() string { return "find" }
|
||||
|
||||
func (f *Find) Needs() Need {
|
||||
if !f.Active() {
|
||||
return 0
|
||||
}
|
||||
switch f.Col {
|
||||
case logfile.ColFields, logfile.ColAll:
|
||||
return NeedFields
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (f *Find) Match(c *Ctx) bool {
|
||||
if !f.Active() {
|
||||
return true
|
||||
}
|
||||
switch f.Col {
|
||||
case logfile.ColTime:
|
||||
f.scratch = append(f.scratch[:0], logfile.StampText(c.Meta)...)
|
||||
case logfile.ColTick:
|
||||
f.scratch = strconv.AppendUint(f.scratch[:0], uint64(c.Meta.Tick), 10)
|
||||
case logfile.ColSub:
|
||||
f.scratch = append(f.scratch[:0], c.Idx.SubName(c.Meta.Sub)...)
|
||||
case logfile.ColMsg:
|
||||
f.scratch = append(f.scratch[:0], c.Idx.MsgName(c.Meta.Msg)...)
|
||||
default:
|
||||
return f.pat.MatchBytes(c.Record().ColumnBytes(f.Col))
|
||||
}
|
||||
return f.pat.MatchBytes(f.scratch)
|
||||
}
|
||||
|
||||
func (f *Find) Label() string {
|
||||
if !f.Active() {
|
||||
return ""
|
||||
}
|
||||
return "/" + f.Col.String() + ":" + f.Query
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
package filter
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/lixenwraith/vif-log/internal/logfile"
|
||||
)
|
||||
|
||||
func init() {
|
||||
Register(Desc{Kind: "level", Help: "level set (IWE) or threshold (>=WARN)", New: newLevel})
|
||||
}
|
||||
|
||||
// Level filters by a bitmask over logfile.Level. Threshold mode is a mutation
|
||||
// of the mask, not a second representation.
|
||||
type Level struct{ Mask uint16 }
|
||||
|
||||
// NewLevel returns a level filter with every level enabled.
|
||||
func NewLevel() *Level { return &Level{Mask: (1 << uint(logfile.LevelCount)) - 1} }
|
||||
|
||||
func newLevel(arg string) (Filter, error) {
|
||||
f := NewLevel()
|
||||
arg = strings.TrimSpace(arg)
|
||||
if arg == "" {
|
||||
return f, nil
|
||||
}
|
||||
if t, ok := strings.CutPrefix(arg, ">="); ok {
|
||||
l, found := levelByName(strings.TrimSpace(t))
|
||||
if !found {
|
||||
return nil, fmt.Errorf("filter/level: unknown level %q", t)
|
||||
}
|
||||
f.Threshold(l)
|
||||
return f, nil
|
||||
}
|
||||
f.Mask = 0
|
||||
for i := 0; i < len(arg); i++ {
|
||||
l, ok := logfile.LevelByInitial(arg[i] &^ 0x20)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("filter/level: unknown level initial %q", arg[i])
|
||||
}
|
||||
f.Mask |= 1 << uint(l)
|
||||
}
|
||||
return f, nil
|
||||
}
|
||||
|
||||
func levelByName(s string) (logfile.Level, bool) {
|
||||
s = strings.ToUpper(s)
|
||||
for i := logfile.Level(0); i < logfile.LevelCount; i++ {
|
||||
if i.String() == s {
|
||||
return i, true
|
||||
}
|
||||
}
|
||||
return logfile.LevelBad, false
|
||||
}
|
||||
|
||||
func (f *Level) Kind() string { return "level" }
|
||||
func (f *Level) Needs() Need { return 0 }
|
||||
func (f *Level) Match(c *Ctx) bool { return f.Mask&(1<<uint(c.Meta.Lvl)) != 0 }
|
||||
|
||||
// Has reports whether level l passes.
|
||||
func (f *Level) Has(l logfile.Level) bool { return f.Mask&(1<<uint(l)) != 0 }
|
||||
|
||||
// Toggle flips one level.
|
||||
func (f *Level) Toggle(l logfile.Level) { f.Mask ^= 1 << uint(l) }
|
||||
|
||||
// SetAll enables or disables every level.
|
||||
func (f *Level) SetAll(on bool) {
|
||||
if on {
|
||||
f.Mask = (1 << uint(logfile.LevelCount)) - 1
|
||||
} else {
|
||||
f.Mask = 0
|
||||
}
|
||||
}
|
||||
|
||||
// Threshold enables the ordered levels at or above l, leaving PROC and BAD alone.
|
||||
func (f *Level) Threshold(l logfile.Level) {
|
||||
for i := range logfile.Level(logfile.OrderedCount) {
|
||||
if i >= l {
|
||||
f.Mask |= 1 << uint(i)
|
||||
} else {
|
||||
f.Mask &^= 1 << uint(i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ThresholdToggle applies the threshold at l, or restores every ordered level
|
||||
// when the mask already has exactly that shape. Digit keys use this: 2 hides
|
||||
// TRACE, 2 again brings it back.
|
||||
func (f *Level) ThresholdToggle(l logfile.Level) {
|
||||
if f.isThreshold(l) {
|
||||
for i := range logfile.Level(logfile.OrderedCount) {
|
||||
f.Mask |= 1 << uint(i)
|
||||
}
|
||||
return
|
||||
}
|
||||
f.Threshold(l)
|
||||
}
|
||||
|
||||
// isThreshold reports whether the ordered levels are exactly those at or above l.
|
||||
func (f *Level) isThreshold(l logfile.Level) bool {
|
||||
for i := range logfile.Level(logfile.OrderedCount) {
|
||||
if f.Has(i) != (i >= l) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// AllOn reports whether no level is filtered out.
|
||||
func (f *Level) AllOn() bool {
|
||||
return f.Mask&((1<<uint(logfile.LevelCount))-1) == (1<<uint(logfile.LevelCount))-1
|
||||
}
|
||||
|
||||
// Shift moves the threshold by d, clamped to the ordered range.
|
||||
func (f *Level) Shift(d int) {
|
||||
t := int(f.LowestOrdered())
|
||||
t += d
|
||||
if t < 0 {
|
||||
t = 0
|
||||
}
|
||||
if t >= logfile.OrderedCount {
|
||||
t = logfile.OrderedCount - 1
|
||||
}
|
||||
f.Threshold(logfile.Level(t))
|
||||
}
|
||||
|
||||
// LowestOrdered returns the lowest enabled ordered level, ERROR if none.
|
||||
func (f *Level) LowestOrdered() logfile.Level {
|
||||
for i := logfile.Level(0); i < logfile.Level(logfile.OrderedCount); i++ {
|
||||
if f.Has(i) {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return logfile.LevelError
|
||||
}
|
||||
|
||||
// Label reports the level filter only when it hides something; the header
|
||||
// strip shows the per-level state.
|
||||
func (f *Level) Label() string {
|
||||
if f.AllOn() {
|
||||
return ""
|
||||
}
|
||||
if l, ok := f.thresholdShape(); ok {
|
||||
return "lvl>=" + l.String()
|
||||
}
|
||||
var b strings.Builder
|
||||
b.WriteString("lvl:")
|
||||
for i := range logfile.LevelCount {
|
||||
if f.Has(i) {
|
||||
b.WriteByte(i.Initial())
|
||||
}
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// thresholdShape reports whether the mask is "ordered >= t, plus PROC and BAD".
|
||||
func (f *Level) thresholdShape() (logfile.Level, bool) {
|
||||
if !f.Has(logfile.LevelProc) || !f.Has(logfile.LevelBad) {
|
||||
return 0, false
|
||||
}
|
||||
t := f.LowestOrdered()
|
||||
if t == 0 || !f.isThreshold(t) {
|
||||
return 0, false
|
||||
}
|
||||
return t, true
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
package filter
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Pattern is a smart-case matcher: an all-lowercase pattern matches
|
||||
// case-insensitively, any upper-case character makes it case-sensitive.
|
||||
// A pattern free of regexp metacharacters takes an allocation-free
|
||||
// substring path; everything else compiles to stdlib RE2.
|
||||
type Pattern struct {
|
||||
Src string
|
||||
re *regexp.Regexp
|
||||
lit []byte
|
||||
fold bool
|
||||
}
|
||||
|
||||
// NewPattern compiles s; an empty s yields an inert pattern.
|
||||
func NewPattern(s string) (Pattern, error) {
|
||||
p := Pattern{Src: s, fold: s == strings.ToLower(s)}
|
||||
if s == "" {
|
||||
return p, nil
|
||||
}
|
||||
if regexp.QuoteMeta(s) == s {
|
||||
if p.fold {
|
||||
p.lit = []byte(strings.ToLower(s))
|
||||
} else {
|
||||
p.lit = []byte(s)
|
||||
}
|
||||
return p, nil
|
||||
}
|
||||
expr := s
|
||||
if p.fold {
|
||||
expr = "(?i)" + s
|
||||
}
|
||||
re, err := regexp.Compile(expr)
|
||||
if err != nil {
|
||||
return Pattern{}, err
|
||||
}
|
||||
p.re = re
|
||||
return p, nil
|
||||
}
|
||||
|
||||
// Empty reports whether the pattern constrains nothing.
|
||||
func (p Pattern) Empty() bool { return p.Src == "" }
|
||||
|
||||
// MatchBytes reports whether b contains a match.
|
||||
func (p Pattern) MatchBytes(b []byte) bool {
|
||||
switch {
|
||||
case p.Src == "":
|
||||
return true
|
||||
case p.lit != nil && p.fold:
|
||||
return foldContains(b, p.lit)
|
||||
case p.lit != nil:
|
||||
return bytes.Contains(b, p.lit)
|
||||
default:
|
||||
return p.re.Match(b)
|
||||
}
|
||||
}
|
||||
|
||||
// MatchString reports whether s contains a match.
|
||||
func (p Pattern) MatchString(s string) bool {
|
||||
switch {
|
||||
case p.Src == "":
|
||||
return true
|
||||
case p.lit != nil && p.fold:
|
||||
return foldContains([]byte(s), p.lit)
|
||||
case p.lit != nil:
|
||||
return strings.Contains(s, string(p.lit))
|
||||
default:
|
||||
return p.re.MatchString(s)
|
||||
}
|
||||
}
|
||||
|
||||
func lowerASCII(c byte) byte {
|
||||
if c >= 'A' && c <= 'Z' {
|
||||
return c + 'a' - 'A'
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// foldContains reports whether hay contains needle, ASCII case-insensitively.
|
||||
// needle must already be lowercased.
|
||||
func foldContains(hay, needle []byte) bool {
|
||||
if len(needle) == 0 {
|
||||
return true
|
||||
}
|
||||
if len(hay) < len(needle) {
|
||||
return false
|
||||
}
|
||||
first := needle[0]
|
||||
for i := 0; i+len(needle) <= len(hay); i++ {
|
||||
if lowerASCII(hay[i]) != first {
|
||||
continue
|
||||
}
|
||||
match := true
|
||||
for j := 1; j < len(needle); j++ {
|
||||
if lowerASCII(hay[i+j]) != needle[j] {
|
||||
match = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if match {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package filter
|
||||
|
||||
import "slices"
|
||||
|
||||
// PinSet is the pin buffer: record indices the user marked. It is owned by the
|
||||
// app and outlives every filter change, which is the point — pins are
|
||||
// assembled across successive filters. Not registered: it has no meaningful
|
||||
// construction from a string argument.
|
||||
type PinSet struct{ m map[int32]struct{} }
|
||||
|
||||
// NewPinSet returns an empty pin buffer.
|
||||
func NewPinSet() *PinSet { return &PinSet{m: make(map[int32]struct{})} }
|
||||
|
||||
// Has reports whether record i is pinned.
|
||||
func (p *PinSet) Has(i int32) bool { _, ok := p.m[i]; return ok }
|
||||
|
||||
// Toggle flips record i, returning its new state.
|
||||
func (p *PinSet) Toggle(i int32) bool {
|
||||
if _, ok := p.m[i]; ok {
|
||||
delete(p.m, i)
|
||||
return false
|
||||
}
|
||||
p.m[i] = struct{}{}
|
||||
return true
|
||||
}
|
||||
|
||||
// Clear empties the buffer.
|
||||
func (p *PinSet) Clear() { clear(p.m) }
|
||||
|
||||
// Len returns the pin count.
|
||||
func (p *PinSet) Len() int { return len(p.m) }
|
||||
|
||||
// Sorted returns the pinned record indices in file order.
|
||||
func (p *PinSet) Sorted() []int32 {
|
||||
out := make([]int32, 0, len(p.m))
|
||||
for i := range p.m {
|
||||
out = append(out, i)
|
||||
}
|
||||
slices.Sort(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// Pinned restricts the view to the pin buffer.
|
||||
type Pinned struct {
|
||||
Set *PinSet
|
||||
On bool
|
||||
}
|
||||
|
||||
// NewPinned wraps a pin buffer as a filter.
|
||||
func NewPinned(s *PinSet) *Pinned { return &Pinned{Set: s} }
|
||||
|
||||
func (f *Pinned) Kind() string { return "pin" }
|
||||
func (f *Pinned) Needs() Need { return 0 }
|
||||
|
||||
func (f *Pinned) Match(c *Ctx) bool {
|
||||
return !f.On || f.Set.Has(int32(c.I))
|
||||
}
|
||||
|
||||
func (f *Pinned) Label() string {
|
||||
if !f.On {
|
||||
return ""
|
||||
}
|
||||
return "pinned-only"
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package filter
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/lixenwraith/vif-log/internal/logfile"
|
||||
)
|
||||
|
||||
func init() {
|
||||
Register(Desc{Kind: "snap", Help: "collapse stat snapshots to one line (arg: off)", New: newCollapse})
|
||||
}
|
||||
|
||||
// Collapse hides stat-snapshot members, turning a ~40-record snapshot into one
|
||||
// navigable line. On is the global state; exc holds the per-group exceptions
|
||||
// toggled with Enter. Index-only, so it costs nothing to evaluate.
|
||||
type Collapse struct {
|
||||
On bool
|
||||
exc map[uint32]bool
|
||||
}
|
||||
|
||||
// NewCollapse returns a collapse filter in the given global state.
|
||||
func NewCollapse(on bool) *Collapse {
|
||||
return &Collapse{On: on, exc: make(map[uint32]bool)}
|
||||
}
|
||||
|
||||
func newCollapse(arg string) (Filter, error) {
|
||||
return NewCollapse(strings.TrimSpace(arg) != "off"), nil
|
||||
}
|
||||
|
||||
func (f *Collapse) Kind() string { return "snap" }
|
||||
func (f *Collapse) Needs() Need { return 0 }
|
||||
|
||||
func (f *Collapse) Match(c *Ctx) bool {
|
||||
if c.Meta.Snap == 0 || c.Meta.Flags&logfile.FlagSnapHead != 0 {
|
||||
return true
|
||||
}
|
||||
return f.Expanded(c.Meta.Snap)
|
||||
}
|
||||
|
||||
// Expanded reports whether group id shows its members.
|
||||
func (f *Collapse) Expanded(id uint32) bool { return f.On == f.exc[id] }
|
||||
|
||||
// ToggleGroup flips one group against the global state.
|
||||
func (f *Collapse) ToggleGroup(id uint32) { f.exc[id] = !f.exc[id] }
|
||||
|
||||
// ResetGroups drops the per-group exceptions; group ids are file-scoped.
|
||||
func (f *Collapse) ResetGroups() { clear(f.exc) }
|
||||
|
||||
// Toggle flips the global state and drops all per-group exceptions.
|
||||
func (f *Collapse) Toggle() {
|
||||
f.On = !f.On
|
||||
clear(f.exc)
|
||||
}
|
||||
|
||||
func (f *Collapse) Label() string {
|
||||
s := "snap:expanded"
|
||||
if f.On {
|
||||
s = "snap:collapsed"
|
||||
}
|
||||
if len(f.exc) > 0 {
|
||||
s += "*"
|
||||
}
|
||||
return s
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package filter
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func init() {
|
||||
Register(Desc{Kind: "tick", Help: "tick range: N | N-M | N- | -M", New: newTickSpan})
|
||||
Register(Desc{Kind: "run", Help: "run range: N | N-M | N- | -M", New: newRunSpan})
|
||||
}
|
||||
|
||||
// Span is an inclusive numeric range over an index-resident counter.
|
||||
type Span struct {
|
||||
field string
|
||||
Lo, Hi uint32
|
||||
Query string
|
||||
}
|
||||
|
||||
func newTickSpan(arg string) (Filter, error) { return newSpan("tick", arg) }
|
||||
func newRunSpan(arg string) (Filter, error) { return newSpan("run", arg) }
|
||||
|
||||
func newSpan(field, arg string) (Filter, error) {
|
||||
lo, hi, err := parseSpan(arg)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("filter/%s: %w", field, err)
|
||||
}
|
||||
return &Span{field: field, Lo: lo, Hi: hi, Query: arg}, nil
|
||||
}
|
||||
|
||||
// parseSpan accepts N, N-M, N- and -M; an empty spec is the full range.
|
||||
func parseSpan(arg string) (uint32, uint32, error) {
|
||||
arg = strings.TrimSpace(arg)
|
||||
if arg == "" {
|
||||
return 0, ^uint32(0), nil
|
||||
}
|
||||
lo, hi := arg, arg
|
||||
if i := strings.IndexByte(arg, '-'); i >= 0 {
|
||||
lo, hi = arg[:i], arg[i+1:]
|
||||
}
|
||||
var l uint32
|
||||
h := ^uint32(0)
|
||||
if lo != "" {
|
||||
v, err := strconv.ParseUint(lo, 10, 32)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
l = uint32(v)
|
||||
}
|
||||
if hi != "" {
|
||||
v, err := strconv.ParseUint(hi, 10, 32)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
h = uint32(v)
|
||||
}
|
||||
if l > h {
|
||||
l, h = h, l
|
||||
}
|
||||
return l, h, nil
|
||||
}
|
||||
|
||||
func (f *Span) Kind() string { return f.field }
|
||||
func (f *Span) Needs() Need { return 0 }
|
||||
|
||||
func (f *Span) Match(c *Ctx) bool {
|
||||
v := c.Meta.Tick
|
||||
if f.field == "run" {
|
||||
v = c.Meta.Run
|
||||
}
|
||||
return v >= f.Lo && v <= f.Hi
|
||||
}
|
||||
|
||||
func (f *Span) Label() string {
|
||||
if f.Lo == 0 && f.Hi == ^uint32(0) {
|
||||
return ""
|
||||
}
|
||||
return f.field + ":" + f.Query
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package filter
|
||||
|
||||
import "github.com/lixenwraith/vif-log/internal/logfile"
|
||||
|
||||
func init() {
|
||||
Register(Desc{Kind: "sub", Help: "subsystem regexp, e.g. sub:^(fsm|rec)$", New: newSub})
|
||||
Register(Desc{Kind: "msg", Help: "message regexp, e.g. msg:transition", New: newMsg})
|
||||
}
|
||||
|
||||
// Text matches one of the interned vocabulary columns. The pattern is applied
|
||||
// once per interned id into a bitset, so the per-record cost is a bit probe
|
||||
// and no line is ever read.
|
||||
type Text struct {
|
||||
kind string
|
||||
Query string
|
||||
pat Pattern
|
||||
mask []uint64
|
||||
n int
|
||||
}
|
||||
|
||||
func newSub(arg string) (Filter, error) { return newText("sub", arg) }
|
||||
func newMsg(arg string) (Filter, error) { return newText("msg", arg) }
|
||||
|
||||
func newText(kind, arg string) (Filter, error) {
|
||||
p, err := NewPattern(arg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Text{kind: kind, Query: arg, pat: p}, nil
|
||||
}
|
||||
|
||||
func (f *Text) Kind() string { return f.kind }
|
||||
func (f *Text) Needs() Need { return 0 }
|
||||
|
||||
func (f *Text) Match(c *Ctx) bool {
|
||||
if f.pat.Empty() {
|
||||
return true
|
||||
}
|
||||
var id int
|
||||
var tab []string
|
||||
if f.kind == "sub" {
|
||||
id, tab = int(c.Meta.Sub), c.Idx.Subs()
|
||||
} else {
|
||||
id, tab = int(c.Meta.Msg), c.Idx.Msgs()
|
||||
}
|
||||
f.ensure(tab)
|
||||
if id < 0 || id >= f.n {
|
||||
return false
|
||||
}
|
||||
return f.mask[id>>6]&(1<<uint(id&63)) != 0
|
||||
}
|
||||
|
||||
// ensure rebuilds the bitset when the interned table has grown.
|
||||
func (f *Text) ensure(tab []string) {
|
||||
if f.mask != nil && f.n == len(tab) {
|
||||
return
|
||||
}
|
||||
need := (len(tab) + 63) / 64
|
||||
if cap(f.mask) < need {
|
||||
f.mask = make([]uint64, need)
|
||||
} else {
|
||||
f.mask = f.mask[:need]
|
||||
clear(f.mask)
|
||||
}
|
||||
for i, s := range tab {
|
||||
if f.pat.MatchString(s) {
|
||||
f.mask[i>>6] |= 1 << uint(i&63)
|
||||
}
|
||||
}
|
||||
f.n = len(tab)
|
||||
}
|
||||
|
||||
func (f *Text) Label() string {
|
||||
if f.pat.Empty() {
|
||||
return ""
|
||||
}
|
||||
return f.kind + ":" + f.Query
|
||||
}
|
||||
|
||||
// SubOf resolves the column a text filter binds to; used by the help overlay.
|
||||
func (f *Text) Column() logfile.Column {
|
||||
if f.kind == "sub" {
|
||||
return logfile.ColSub
|
||||
}
|
||||
return logfile.ColMsg
|
||||
}
|
||||
Reference in New Issue
Block a user