Skip to content

Commit

Permalink
api: implement succinct output for SearchOptions.String (#719)
Browse files Browse the repository at this point in the history
I am often reading the output of String in traces and logs, and it is
really hard to parse since there are many fields and most are unset.
This is a quality of life improvement so it is much easier to scan the
output.

For example the default zoekt-webserver struct's string output goes from
a 456 byte string to

  zoekt.SearchOptions{ ShardMaxMatchCount=100000 TotalMaxMatchCount=1000000 MaxWallTime=10s }

Test Plan: go test. The unit tests ensure I cover every field now and in
the future when fields are added.
  • Loading branch information
keegancsmith authored Jan 17, 2024
1 parent b35d8a2 commit 7dcc797
Show file tree
Hide file tree
Showing 2 changed files with 139 additions and 3 deletions.
66 changes: 65 additions & 1 deletion api.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"fmt"
"reflect"
"strconv"
"strings"
"time"

"github.com/sourcegraph/zoekt/query"
Expand Down Expand Up @@ -945,8 +946,71 @@ type SearchOptions struct {
SpanContext map[string]string
}

// String returns a succinct representation of the options. This is meant for
// human consumption in logs and traces.
//
// Note: some tracing systems have limits on length of values, so we take care
// to try and make this small, and include the important information near the
// front incase of truncation.
func (s *SearchOptions) String() string {
return fmt.Sprintf("%#v", s)
var b strings.Builder

add := func(name, value string) {
b.WriteString(name)
b.WriteByte('=')
b.WriteString(value)
b.WriteByte(' ')
}
addInt := func(name string, value int) {
if value != 0 {
add(name, strconv.Itoa(value))
}
}
addDuration := func(name string, value time.Duration) {
if value != 0 {
add(name, value.String())
}
}
addBool := func(name string, value bool) {
if !value {
return
}
b.WriteString(name)
b.WriteByte(' ')
}

b.WriteString("zoekt.SearchOptions{ ")

addInt("ShardMaxMatchCount", s.ShardMaxMatchCount)
addInt("TotalMaxMatchCount", s.TotalMaxMatchCount)
addInt("ShardRepoMaxMatchCount", s.ShardRepoMaxMatchCount)
addInt("ShardMaxImportantMatch", s.ShardMaxImportantMatch)
addInt("TotalMaxImportantMatch", s.TotalMaxImportantMatch)
addInt("MaxDocDisplayCount", s.MaxDocDisplayCount)
addInt("MaxMatchDisplayCount", s.MaxMatchDisplayCount)
addInt("NumContextLines", s.NumContextLines)

addDuration("MaxWallTime", s.MaxWallTime)
addDuration("FlushWallTime", s.FlushWallTime)

if s.DocumentRanksWeight > 0 {
add("DocumentRanksWeight", strconv.FormatFloat(s.DocumentRanksWeight, 'g', -1, 64))
}

addBool("EstimateDocCount", s.EstimateDocCount)
addBool("Whole", s.Whole)
addBool("ChunkMatches", s.ChunkMatches)
addBool("UseDocumentRanks", s.UseDocumentRanks)
addBool("UseKeywordScoring", s.UseKeywordScoring)
addBool("Trace", s.Trace)
addBool("DebugScore", s.DebugScore)

for k, v := range s.SpanContext {
add("SpanContext."+k, strconv.Quote(v))
}

b.WriteByte('}')
return b.String()
}

// Sender is the interface that wraps the basic Send method.
Expand Down
76 changes: 74 additions & 2 deletions api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,13 @@ import (
"strings"
"testing"
"time"

"github.com/grafana/regexp"
)

/*
BenchmarkMinimalRepoListEncodings/slice-8 570 2145665 ns/op 753790 bytes 3981 B/op 0 allocs/op
BenchmarkMinimalRepoListEncodings/map-8 360 3337522 ns/op 740778 bytes 377777 B/op 13002 allocs/op
BenchmarkMinimalRepoListEncodings/slice-8 570 2145665 ns/op 753790 bytes 3981 B/op 0 allocs/op
BenchmarkMinimalRepoListEncodings/map-8 360 3337522 ns/op 740778 bytes 377777 B/op 13002 allocs/op
*/
func BenchmarkMinimalRepoListEncodings(b *testing.B) {
size := uint32(13000) // 2021-06-24 rough estimate of number of repos on a replica.
Expand Down Expand Up @@ -165,3 +167,73 @@ tool fieldalignment then update this test.`, c.v, c.size, got)
}
}
}

func TestSearchOptions_String(t *testing.T) {
// To make sure we don't forget to update the string implementation we use
// reflection to generate a SearchOptions with every field being non
// default. We then check that the field name is present in the output.
opts := SearchOptions{}
var fieldNames []string
rv := reflect.ValueOf(&opts).Elem()
for i := 0; i < rv.NumField(); i++ {
f := rv.Field(i)
name := rv.Type().Field(i).Name
fieldNames = append(fieldNames, name)
switch f.Kind() {
case reflect.Bool:
f.SetBool(true)
case reflect.Int:
f.SetInt(1)
case reflect.Int64:
f.SetInt(1)
case reflect.Float64:
f.SetFloat(1)
case reflect.Map:
// Only map is SpanContext
f.Set(reflect.ValueOf(map[string]string{"key": "value"}))
default:
t.Fatalf("add support for %s field (%s)", f.Kind(), name)
}
}

s := opts.String()
for _, name := range fieldNames {
found, err := regexp.MatchString("\\b"+regexp.QuoteMeta(name)+"\\b", s)
if err != nil {
t.Fatal(err)
}
if !found {
t.Errorf("could not find field %q in string output of SearchOptions:\n%s", name, s)
}
}

webDefaults := SearchOptions{
MaxWallTime: 10 * time.Second,
}
webDefaults.SetDefaults()

// Now we hand craft a few corner and common cases
cases := []struct {
Opts SearchOptions
Want string
}{{
// Empty
Opts: SearchOptions{},
Want: "zoekt.SearchOptions{ }",
}, {
// healthz options
Opts: SearchOptions{ShardMaxMatchCount: 1, TotalMaxMatchCount: 1, MaxDocDisplayCount: 1},
Want: "zoekt.SearchOptions{ ShardMaxMatchCount=1 TotalMaxMatchCount=1 MaxDocDisplayCount=1 }",
}, {
// zoekt-webserver defaults
Opts: webDefaults,
Want: "zoekt.SearchOptions{ ShardMaxMatchCount=100000 TotalMaxMatchCount=1000000 MaxWallTime=10s }",
}}

for _, tc := range cases {
got := tc.Opts.String()
if got != tc.Want {
t.Errorf("unexpected String for %#v:\ngot: %s\nwant: %s", tc.Opts, got, tc.Want)
}
}
}

0 comments on commit 7dcc797

Please sign in to comment.