diff --git a/.chloggen/use-json-iterator.yaml b/.chloggen/use-json-iterator.yaml new file mode 100755 index 0000000000..8eefa84e57 --- /dev/null +++ b/.chloggen/use-json-iterator.yaml @@ -0,0 +1,17 @@ +# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix' +change_type: enhancement + +# The name of the component, or a single word describing the area of concern, (e.g. operator, target allocator, github action) +component: Target Allocator + +# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`). +note: Use json-iter to marshal json + +# One or more tracking issues related to the change +issues: + - 1336 + +# (Optional) One or more lines of additional information to render under the primary note. +# These lines will be padded with 2 spaces and then inserted directly into the document. +# Use pipe (|) for multiline entries. +subtext: diff --git a/cmd/otel-allocator/go.mod b/cmd/otel-allocator/go.mod index ecd00cf117..14d9bfc439 100644 --- a/cmd/otel-allocator/go.mod +++ b/cmd/otel-allocator/go.mod @@ -10,6 +10,7 @@ require ( github.com/go-kit/log v0.2.1 github.com/go-logr/logr v1.2.3 github.com/gorilla/mux v1.8.0 + github.com/json-iterator/go v1.1.12 github.com/mitchellh/hashstructure v1.1.0 github.com/oklog/run v1.1.0 github.com/prometheus-operator/prometheus-operator v0.53.1 @@ -111,7 +112,6 @@ require ( github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/jpillora/backoff v1.0.0 // indirect - github.com/json-iterator/go v1.1.12 // indirect github.com/kolo/xmlrpc v0.0.0-20201022064351-38db28db192b // indirect github.com/linode/linodego v1.8.0 // indirect github.com/mailru/easyjson v0.7.7 // indirect diff --git a/cmd/otel-allocator/server/server.go b/cmd/otel-allocator/server/server.go index 61807b0052..68661bac79 100644 --- a/cmd/otel-allocator/server/server.go +++ b/cmd/otel-allocator/server/server.go @@ -16,20 +16,19 @@ package server import ( "context" - "encoding/json" "fmt" "net/http" "net/url" "time" - yaml2 "github.com/ghodss/yaml" "github.com/go-logr/logr" "github.com/gorilla/mux" + jsoniter "github.com/json-iterator/go" "github.com/mitchellh/hashstructure" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promauto" "github.com/prometheus/client_golang/prometheus/promhttp" - "gopkg.in/yaml.v2" + promconfig "github.com/prometheus/prometheus/config" "github.com/open-telemetry/opentelemetry-operator/cmd/otel-allocator/allocation" "github.com/open-telemetry/opentelemetry-operator/cmd/otel-allocator/target" @@ -42,27 +41,49 @@ var ( }, []string{"path"}) ) +var ( + yamlConfig = jsoniter.Config{ + EscapeHTML: false, + MarshalFloatWith6Digits: true, + ObjectFieldMustBeSimpleString: true, + TagKey: "yaml", + }.Froze() + jsonConfig = jsoniter.Config{ + EscapeHTML: false, + MarshalFloatWith6Digits: true, + ObjectFieldMustBeSimpleString: true, + }.Froze() +) + type collectorJSON struct { Link string `json:"_link"` Jobs []*target.Item `json:"targets"` } +type DiscoveryManager interface { + GetScrapeConfigs() map[string]*promconfig.ScrapeConfig +} + type Server struct { logger logr.Logger allocator allocation.Allocator - discoveryManager *target.Discoverer + discoveryManager DiscoveryManager server *http.Server + yamlMarshaller jsoniter.API // TODO: for testing, it would make a lot of sense to have a simpler interface + jsonMarshaller jsoniter.API // TODO: for testing, it would make a lot of sense to have a simpler interface compareHash uint64 scrapeConfigResponse []byte } -func NewServer(log logr.Logger, allocator allocation.Allocator, discoveryManager *target.Discoverer, listenAddr *string) *Server { +func NewServer(log logr.Logger, allocator allocation.Allocator, discoveryManager DiscoveryManager, listenAddr *string) *Server { s := &Server{ logger: log, allocator: allocator, discoveryManager: discoveryManager, compareHash: uint64(0), + yamlMarshaller: yamlConfig, + jsonMarshaller: jsonConfig, } router := mux.NewRouter().UseEncodedPath() router.Use(s.PrometheusMiddleware) @@ -87,7 +108,7 @@ func (s *Server) Shutdown(ctx context.Context) error { // ScrapeConfigsHandler returns the available scrape configuration discovered by the target allocator. // The target allocator first marshals these configurations such that the underlying prometheus marshaling is used. // After that, the YAML is converted in to a JSON format for consumers to use. -func (s *Server) ScrapeConfigsHandler(w http.ResponseWriter, r *http.Request) { +func (s *Server) ScrapeConfigsHandler(w http.ResponseWriter, _ *http.Request) { configs := s.discoveryManager.GetScrapeConfigs() hash, err := hashstructure.Hash(configs, nil) @@ -98,19 +119,13 @@ func (s *Server) ScrapeConfigsHandler(w http.ResponseWriter, r *http.Request) { } // if the hashes are different, we need to recompute the scrape config if hash != s.compareHash { - var configBytes []byte - configBytes, err = yaml.Marshal(configs) - if err != nil { - s.errorHandler(w, err) - return - } - var jsonConfig []byte - jsonConfig, err = yaml2.YAMLToJSON(configBytes) - if err != nil { - s.errorHandler(w, err) + configBytes, mErr := s.yamlMarshaller.Marshal(configs) + if mErr != nil { + s.errorHandler(w, mErr) return } - s.scrapeConfigResponse = jsonConfig + // Update the response and the hash + s.scrapeConfigResponse = configBytes s.compareHash = hash } // We don't use the jsonHandler method because we don't want our bytes to be re-encoded @@ -121,7 +136,7 @@ func (s *Server) ScrapeConfigsHandler(w http.ResponseWriter, r *http.Request) { } } -func (s *Server) JobHandler(w http.ResponseWriter, r *http.Request) { +func (s *Server) JobHandler(w http.ResponseWriter, _ *http.Request) { displayData := make(map[string]target.LinkJSON) for _, v := range s.allocator.TargetItems() { displayData[v.JobName] = target.LinkJSON{Link: v.Link.Link} @@ -172,7 +187,7 @@ func (s *Server) errorHandler(w http.ResponseWriter, err error) { func (s *Server) jsonHandler(w http.ResponseWriter, data interface{}) { w.Header().Set("Content-Type", "application/json") - err := json.NewEncoder(w).Encode(data) + err := s.jsonMarshaller.NewEncoder(w).Encode(data) if err != nil { s.logger.Error(err, "failed to encode data for http response") } diff --git a/cmd/otel-allocator/server/server_test.go b/cmd/otel-allocator/server/server_test.go index 3f3c7da118..afc6f7e782 100644 --- a/cmd/otel-allocator/server/server_test.go +++ b/cmd/otel-allocator/server/server_test.go @@ -15,15 +15,16 @@ package server import ( - "crypto/rand" "encoding/json" "fmt" "io" - "math/big" + "math/rand" "net/http/httptest" "testing" + "time" "github.com/prometheus/common/model" + promconfig "github.com/prometheus/prometheus/config" "github.com/stretchr/testify/assert" logf "sigs.k8s.io/controller-runtime/pkg/log" @@ -44,6 +45,14 @@ var ( testJobTargetItemTwo = target.NewItem("test-job", "test-url2", testJobLabelSetTwo, "test-collector2") ) +type mockDiscoveryManager struct { + m map[string]*promconfig.ScrapeConfig +} + +func (m *mockDiscoveryManager) GetScrapeConfigs() map[string]*promconfig.ScrapeConfig { + return m.m +} + func TestServer_TargetsHandler(t *testing.T) { leastWeighted, _ := allocation.New("least-weighted", logger) type args struct { @@ -170,12 +179,8 @@ func TestServer_TargetsHandler(t *testing.T) { } } -func randInt(max int64) int64 { - nBig, _ := rand.Int(rand.Reader, big.NewInt(max)) - return nBig.Int64() -} - func BenchmarkServerTargetsHandler(b *testing.B) { + rand.Seed(time.Now().UnixNano()) var table = []struct { numCollectors int numJobs int @@ -202,8 +207,8 @@ func BenchmarkServerTargetsHandler(b *testing.B) { b.Run(fmt.Sprintf("%s_num_cols_%d_num_jobs_%d", allocatorName, v.numCollectors, v.numJobs), func(b *testing.B) { b.ReportAllocs() for i := 0; i < b.N; i++ { - randomJob := randInt(int64(v.numJobs)) - randomCol := randInt(int64(v.numCollectors)) + randomJob := rand.Intn(v.numJobs) //nolint: gosec + randomCol := rand.Intn(v.numCollectors) //nolint: gosec request := httptest.NewRequest("GET", fmt.Sprintf("/jobs/test-job-%d/targets?collector_id=collector-%d", randomJob, randomCol), nil) w := httptest.NewRecorder() s.server.Handler.ServeHTTP(w, request) @@ -212,3 +217,201 @@ func BenchmarkServerTargetsHandler(b *testing.B) { } } } + +func BenchmarkScrapeConfigsHandler(b *testing.B) { + rand.Seed(time.Now().UnixNano()) + s := &Server{ + logger: logger, + yamlMarshaller: yamlConfig, + } + + tests := []int{0, 5, 10, 50, 100, 500} + for _, n := range tests { + data := makeNScrapeConfigs(n) + b.Run(fmt.Sprintf("%d_targets", n), func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + s.compareHash = 0 + s.discoveryManager = &mockDiscoveryManager{m: data} + resp := httptest.NewRecorder() + s.ScrapeConfigsHandler(resp, nil) + } + }) + } +} + +func BenchmarkCollectorMapJSONHandler(b *testing.B) { + rand.Seed(time.Now().UnixNano()) + s := &Server{ + logger: logger, + jsonMarshaller: jsonConfig, + } + + tests := []struct { + numCollectors int + numTargets int + }{ + { + numCollectors: 0, + numTargets: 0, + }, + { + numCollectors: 5, + numTargets: 5, + }, + { + numCollectors: 5, + numTargets: 50, + }, + { + numCollectors: 5, + numTargets: 500, + }, + { + numCollectors: 50, + numTargets: 5, + }, + { + numCollectors: 50, + numTargets: 50, + }, + { + numCollectors: 50, + numTargets: 500, + }, + { + numCollectors: 50, + numTargets: 5000, + }, + } + for _, tc := range tests { + data := makeNCollectorJSON(tc.numCollectors, tc.numTargets) + b.Run(fmt.Sprintf("%d_collectors_%d_targets", tc.numCollectors, tc.numTargets), func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + resp := httptest.NewRecorder() + s.jsonHandler(resp, data) + } + }) + } +} + +func BenchmarkTargetItemsJSONHandler(b *testing.B) { + rand.Seed(time.Now().UnixNano()) + s := &Server{ + logger: logger, + jsonMarshaller: jsonConfig, + } + + tests := []struct { + numTargets int + numLabels int + }{ + { + numTargets: 0, + numLabels: 0, + }, + { + numTargets: 5, + numLabels: 5, + }, + { + numTargets: 5, + numLabels: 50, + }, + { + numTargets: 50, + numLabels: 5, + }, + { + numTargets: 50, + numLabels: 50, + }, + { + numTargets: 500, + numLabels: 50, + }, + { + numTargets: 500, + numLabels: 500, + }, + { + numTargets: 5000, + numLabels: 50, + }, + { + numTargets: 5000, + numLabels: 500, + }, + } + for _, tc := range tests { + data := makeNTargetItems(tc.numTargets, tc.numLabels) + b.Run(fmt.Sprintf("%d_targets_%d_labels", tc.numTargets, tc.numLabels), func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + resp := httptest.NewRecorder() + s.jsonHandler(resp, data) + } + }) + } +} + +var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_/") + +func randSeq(n int) string { + b := make([]rune, n) + for i := range b { + b[i] = letters[rand.Intn(len(letters))] //nolint:gosec + } + return string(b) +} + +func makeNScrapeConfigs(n int) map[string]*promconfig.ScrapeConfig { + items := make(map[string]*promconfig.ScrapeConfig, n) + for i := 0; i < n; i++ { + items[randSeq(20)] = &promconfig.ScrapeConfig{ + JobName: randSeq(20), + ScrapeInterval: model.Duration(30 * time.Second), + ScrapeTimeout: model.Duration(time.Minute), + MetricsPath: randSeq(50), + SampleLimit: 5, + TargetLimit: 200, + LabelLimit: 20, + LabelNameLengthLimit: 50, + LabelValueLengthLimit: 100, + } + } + return items +} + +func makeNCollectorJSON(numCollectors, numItems int) map[string]collectorJSON { + items := make(map[string]collectorJSON, numCollectors) + for i := 0; i < numCollectors; i++ { + items[randSeq(20)] = collectorJSON{ + Link: randSeq(120), + Jobs: makeNTargetItems(numItems, 50), + } + } + return items +} + +func makeNTargetItems(numItems, numLabels int) []*target.Item { + items := make([]*target.Item, 0, numItems) + for i := 0; i < numItems; i++ { + items = append(items, target.NewItem( + randSeq(80), + randSeq(150), + makeNNewLabels(numLabels), + randSeq(30), + )) + } + return items +} + +func makeNNewLabels(n int) model.LabelSet { + labels := make(map[model.LabelName]model.LabelValue, n) + for i := 0; i < n; i++ { + labels[model.LabelName(randSeq(20))] = model.LabelValue(randSeq(20)) + } + return labels +} diff --git a/go.mod b/go.mod index 0860325c33..3f9d7f0cd6 100644 --- a/go.mod +++ b/go.mod @@ -8,12 +8,14 @@ require ( github.com/Masterminds/semver/v3 v3.2.0 github.com/go-logr/logr v1.2.3 github.com/mitchellh/mapstructure v1.5.0 + github.com/openshift/api v3.9.0+incompatible github.com/prometheus/prometheus v1.8.2-0.20210621150501-ff58416a0b02 github.com/spf13/pflag v1.0.5 github.com/stretchr/testify v1.8.1 go.opentelemetry.io/otel v1.11.2 gopkg.in/yaml.v2 v2.4.0 k8s.io/api v0.25.4 + k8s.io/apiextensions-apiserver v0.25.0 k8s.io/apimachinery v0.25.4 k8s.io/client-go v0.25.4 k8s.io/component-base v0.25.4 @@ -97,7 +99,6 @@ require ( github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.0.1 // indirect - github.com/openshift/api v3.9.0+incompatible // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_golang v1.12.2 // indirect @@ -128,7 +129,6 @@ require ( gopkg.in/fsnotify/fsnotify.v1 v1.4.7 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/apiextensions-apiserver v0.25.0 // indirect k8s.io/klog/v2 v2.70.1 // indirect k8s.io/kube-openapi v0.0.0-20220803162953-67bda5d908f1 // indirect k8s.io/utils v0.0.0-20220728103510-ee6ede2d64ed // indirect