diff --git a/api.go b/api.go index e3f90852f..d3fe62322 100644 --- a/api.go +++ b/api.go @@ -21,6 +21,7 @@ import ( "fmt" "reflect" "strconv" + "strings" "time" "github.com/sourcegraph/zoekt/query" @@ -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. diff --git a/api_test.go b/api_test.go index fe860d58c..ab13f145d 100644 --- a/api_test.go +++ b/api_test.go @@ -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. @@ -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) + } + } +}