From 5d26be1312a9daed730602d8bbf7ef35202fcd49 Mon Sep 17 00:00:00 2001 From: bhautikpip Date: Tue, 22 Feb 2022 10:02:49 -0800 Subject: [PATCH 01/38] added dependabot for sampler module and util dir --- .github/dependabot.yml | 10 ++++++++++ samplers/aws/xray/go.mod | 3 +++ 2 files changed, 13 insertions(+) create mode 100644 samplers/aws/xray/go.mod diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 121cd2726ab..e519d0a23f0 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -723,3 +723,13 @@ updates: schedule: interval: "weekly" day: "sunday" + - + package-ecosystem: "gomod" + directory: "/samplers/aws/xray" + labels: + - dependencies + - go + - "Skip Changelog" + schedule: + interval: "weekly" + day: "sunday" diff --git a/samplers/aws/xray/go.mod b/samplers/aws/xray/go.mod new file mode 100644 index 00000000000..15522919346 --- /dev/null +++ b/samplers/aws/xray/go.mod @@ -0,0 +1,3 @@ +module go.opentelemetry.io/contrib/samplers/aws/xray + +go 1.16 From cfcf1a73e306c4680cab52104ee75436d1540362 Mon Sep 17 00:00:00 2001 From: bhautikpip Date: Tue, 22 Feb 2022 18:16:29 -0800 Subject: [PATCH 02/38] initial code commit: getSamplingRules, getSamplingTargets and business logic implementation --- samplers/aws/xray/fallback_sampler.go | 69 +++++ samplers/aws/xray/go.mod | 11 + samplers/aws/xray/go.sum | 23 ++ samplers/aws/xray/internal/client.go | 191 ++++++++++++ samplers/aws/xray/internal/manifest.go | 341 +++++++++++++++++++++ samplers/aws/xray/internal/match.go | 70 +++++ samplers/aws/xray/internal/reservoir.go | 79 +++++ samplers/aws/xray/internal/rule.go | 155 ++++++++++ samplers/aws/xray/internal/util/clock.go | 32 ++ samplers/aws/xray/internal/util/rand.go | 114 +++++++ samplers/aws/xray/internal/util/timer.go | 45 +++ samplers/aws/xray/remote_sampler.go | 180 +++++++++++ samplers/aws/xray/remote_sampler_config.go | 111 +++++++ 13 files changed, 1421 insertions(+) create mode 100644 samplers/aws/xray/fallback_sampler.go create mode 100644 samplers/aws/xray/go.sum create mode 100644 samplers/aws/xray/internal/client.go create mode 100644 samplers/aws/xray/internal/manifest.go create mode 100644 samplers/aws/xray/internal/match.go create mode 100644 samplers/aws/xray/internal/reservoir.go create mode 100644 samplers/aws/xray/internal/rule.go create mode 100644 samplers/aws/xray/internal/util/clock.go create mode 100644 samplers/aws/xray/internal/util/rand.go create mode 100644 samplers/aws/xray/internal/util/timer.go create mode 100644 samplers/aws/xray/remote_sampler.go create mode 100644 samplers/aws/xray/remote_sampler_config.go diff --git a/samplers/aws/xray/fallback_sampler.go b/samplers/aws/xray/fallback_sampler.go new file mode 100644 index 00000000000..ecd9ef732e2 --- /dev/null +++ b/samplers/aws/xray/fallback_sampler.go @@ -0,0 +1,69 @@ +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + sdktrace "go.opentelemetry.io/otel/sdk/trace" + "go.opentelemetry.io/otel/trace" + "sync/atomic" + "time" +) + +type FallbackSampler struct { + currentEpoch int64 + defaultSampler sdktrace.Sampler +} + +// Compile time assertion that remoteSampler implements the Sampler interface. +var _ sdktrace.Sampler = (*FallbackSampler)(nil) + +// NewFallbackSampler returns a FallbackSampler which samples 1 req/sec and additional 5% of requests using traceIDRatioBasedSampler. +func NewFallbackSampler() *FallbackSampler { + return &FallbackSampler{ + defaultSampler: sdktrace.TraceIDRatioBased(0.05), + } +} + +// ShouldSample implements the logic of borrowing 1 req/sec and then use traceIDRatioBasedSampler to sample 5% of additional requests. +func (fs *FallbackSampler) ShouldSample(parameters sdktrace.SamplingParameters) sdktrace.SamplingResult { + // borrowing one request every second + if fs.borrow(time.Now().Unix()) { + return sdktrace.SamplingResult{ + Tracestate: trace.SpanContextFromContext(parameters.ParentContext).TraceState(), + Decision: sdktrace.RecordAndSample, + } + } + + // traceIDRatioBasedSampler to sample 5% of additional requests every second + return fs.defaultSampler.ShouldSample(parameters) +} + +// Description returns description of the sampler being used +func (fs *FallbackSampler) Description() string { + return "FallbackSampler{" + fs.getDescription() + "}" +} + +func (fs *FallbackSampler) getDescription() string { + return "fallback sampling with sampling config of 1 req/sec and 5% of additional requests" +} + +func (fs *FallbackSampler) borrow(now int64) bool { + cur := atomic.LoadInt64(&fs.currentEpoch) + if cur >= now { + return false + } + return atomic.CompareAndSwapInt64(&fs.currentEpoch, cur, now) +} + diff --git a/samplers/aws/xray/go.mod b/samplers/aws/xray/go.mod index 15522919346..bd6b0a16899 100644 --- a/samplers/aws/xray/go.mod +++ b/samplers/aws/xray/go.mod @@ -1,3 +1,14 @@ module go.opentelemetry.io/contrib/samplers/aws/xray go 1.16 + +replace go.opentelemetry.io/contrib/samplers/aws/xray/internal => ../internal + +require ( + github.com/go-logr/logr v1.2.2 + github.com/go-logr/stdr v1.2.2 + github.com/jinzhu/copier v0.3.5 + go.opentelemetry.io/otel/sdk v1.4.0 + go.opentelemetry.io/otel/trace v1.4.0 + golang.org/x/sys v0.0.0-20210510120138-977fb7262007 // indirect +) \ No newline at end of file diff --git a/samplers/aws/xray/go.sum b/samplers/aws/xray/go.sum new file mode 100644 index 00000000000..fae9d0e1338 --- /dev/null +++ b/samplers/aws/xray/go.sum @@ -0,0 +1,23 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-logr/logr v1.2.2 h1:ahHml/yUpnlb96Rp8HCvtYVPY8ZYpxq3g7UYchIYwbs= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= +github.com/jinzhu/copier v0.3.5 h1:GlvfUwHk62RokgqVNvYsku0TATCF7bAHVwEXoBh3iJg= +github.com/jinzhu/copier v0.3.5/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +go.opentelemetry.io/otel v1.4.0 h1:7ESuKPq6zpjRaY5nvVDGiuwK7VAJ8MwkKnmNJ9whNZ4= +go.opentelemetry.io/otel v1.4.0/go.mod h1:jeAqMFKy2uLIxCtKxoFj0FAL5zAPKQagc3+GtBWakzk= +go.opentelemetry.io/otel/sdk v1.4.0 h1:LJE4SW3jd4lQTESnlpQZcBhQ3oci0U2MLR5uhicfTHQ= +go.opentelemetry.io/otel/sdk v1.4.0/go.mod h1:71GJPNJh4Qju6zJuYl1CrYtXbrgfau/M9UAggqiy1UE= +go.opentelemetry.io/otel/trace v1.4.0 h1:4OOUrPZdVFQkbzl/JSdvGCWIdw5ONXXxzHlaLlWppmo= +go.opentelemetry.io/otel/trace v1.4.0/go.mod h1:uc3eRsqDfWs9R7b92xbQbU42/eTNz4N+gLP8qJCi4aE= +golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007 h1:gG67DSER+11cZvqIMb8S8bt0vZtiN6xWYARwirrOSfE= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/samplers/aws/xray/internal/client.go b/samplers/aws/xray/internal/client.go new file mode 100644 index 00000000000..62461512945 --- /dev/null +++ b/samplers/aws/xray/internal/client.go @@ -0,0 +1,191 @@ +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package internal + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "net/http" + "net/url" +) + +type getSamplingRulesInput struct { + NextToken *string `json:"NextToken"` +} + +// getSamplingRulesOutput is used to store parsed json sampling rules. +type getSamplingRulesOutput struct { + SamplingRuleRecords []*samplingRuleRecords `json:"SamplingRuleRecords"` +} + +type samplingRuleRecords struct { + SamplingRule *ruleProperties `json:"SamplingRule"` +} + +// ruleProperties is the base set of properties that define a sampling rule. +type ruleProperties struct { + RuleName string `json:"RuleName"` + ServiceType string `json:"ServiceType"` + ResourceARN string `json:"ResourceARN"` + Attributes map[string]string `json:"Attributes"` + ServiceName string `json:"ServiceName"` + Host string `json:"Host"` + HTTPMethod string `json:"HTTPMethod"` + URLPath string `json:"URLPath"` + ReservoirSize int64 `json:"ReservoirSize"` + FixedRate float64 `json:"FixedRate"` + Priority int64 `json:"Priority"` + Version int64 `json:"Version"` +} + +type getSamplingTargetsInput struct { + SamplingStatisticsDocuments []*samplingStatisticsDocument +} + +// samplingStatisticsDocument is used to store current state of sampling data. +type samplingStatisticsDocument struct { + // a unique identifier for the service in hexadecimal + ClientID *string + + // the name of the sampling rule + RuleName *string + + // the number of requests that matched the rule + RequestCount *int64 + + // the number of requests borrowed + BorrowCount *int64 + + // the number of requests sampled using the rule + SampledCount *int64 + + // the current time + Timestamp *int64 +} + +// getSamplingTargetsOutput is used to store parsed json sampling targets +type getSamplingTargetsOutput struct { + LastRuleModification *float64 `json:"LastRuleModification,omitempty"` + SamplingTargetDocuments []*samplingTargetDocument `json:"SamplingTargetDocuments,omitempty"` + UnprocessedStatistics []*unprocessedStatistic `json:"UnprocessedStatistics,omitempty"` +} + +// samplingTargetDocument contains updated targeted information retrieved from X-Ray service +type samplingTargetDocument struct { + // the percentage of matching requests to instrument, after the reservoir is + // exhausted + FixedRate *float64 `json:"FixedRate,omitempty"` + + // the number of seconds for the service to wait before getting sampling targets + // again + Interval *int64 `json:"Interval,omitempty"` + + // the number of requests per second that X-Ray allocated this service + ReservoirQuota *int64 `json:"ReservoirQuota,omitempty"` + + // when the reservoir quota expires + ReservoirQuotaTTL *float64 `json:"ReservoirQuotaTTL,omitempty"` + + // the name of the sampling rule + RuleName *string `json:"RuleName,omitempty"` +} + +type unprocessedStatistic struct { + ErrorCode *string `json:"ErrorCode,omitempty"` + Message *string `json:"Message,omitempty"` + RuleName *string `json:"RuleName,omitempty"` +} + +type xrayClient struct { + // http client for sending sampling requests to the collector + httpClient *http.Client + + endpoint *url.URL +} + +// newClient returns an HTTP client with proxy endpoint +func newClient(addr string) (client *xrayClient, err error) { + endpoint := "http://" + addr + + endpointURL, err := url.Parse(endpoint) + if err != nil { + return nil, err + } + + return &xrayClient{ + httpClient: &http.Client{}, + endpoint: endpointURL, + }, nil +} + +// getSamplingRules calls the collector(aws proxy enabled) for sampling rules +func (c *xrayClient) getSamplingRules(ctx context.Context) (*getSamplingRulesOutput, error) { + samplingRulesInput, err := json.Marshal(getSamplingRulesInput{}) + if err != nil { + return nil, err + } + body := bytes.NewReader(samplingRulesInput) + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.endpoint.String()+"/GetSamplingRules", body) + if err != nil { + return nil, fmt.Errorf("xray client: failed to create http request: %w", err) + } + + output, err := c.httpClient.Do(req) + if err != nil { + return nil, fmt.Errorf("xray client: unable to retrieve sampling settings: %w", err) + } + defer output.Body.Close() + + var samplingRulesOutput *getSamplingRulesOutput + if err := json.NewDecoder(output.Body).Decode(&samplingRulesOutput); err != nil { + return nil, fmt.Errorf("xray client: unable to unmarshal the response body: %w", err) + } + + return samplingRulesOutput, nil +} + +// getSamplingTargets calls the collector(aws proxy enabled) for sampling targets +func (c *xrayClient) getSamplingTargets(ctx context.Context, s []*samplingStatisticsDocument) (*getSamplingTargetsOutput, error) { + statistics := getSamplingTargetsInput{ + SamplingStatisticsDocuments: s, + } + + statisticsByte, err := json.Marshal(statistics) + if err != nil { + return nil, err + } + body := bytes.NewReader(statisticsByte) + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.endpoint.String()+"/SamplingTargets", body) + if err != nil { + return nil, fmt.Errorf("xray client: failed to create http request: %w", err) + } + + output, err := c.httpClient.Do(req) + if err != nil { + return nil, fmt.Errorf("xray client: unable to retrieve sampling settings: %w", err) + } + defer output.Body.Close() + + var samplingTargetsOutput *getSamplingTargetsOutput + if err := json.NewDecoder(output.Body).Decode(&samplingTargetsOutput); err != nil { + return nil, fmt.Errorf("xray client: unable to unmarshal the response body: %w", err) + } + + return samplingTargetsOutput, nil +} diff --git a/samplers/aws/xray/internal/manifest.go b/samplers/aws/xray/internal/manifest.go new file mode 100644 index 00000000000..aef5625ece7 --- /dev/null +++ b/samplers/aws/xray/internal/manifest.go @@ -0,0 +1,341 @@ +package internal + +import ( + "context" + crypto "crypto/rand" + "fmt" + "github.com/go-logr/logr" + "github.com/jinzhu/copier" + "go.opentelemetry.io/contrib/samplers/aws/xray/internal/util" + sdktrace "go.opentelemetry.io/otel/sdk/trace" + "sort" + "strings" + "sync" + "time" +) + +const defaultInterval = int64(10) + +const manifestTTL = 3600 + +// Manifest represents a full sampling ruleset and provides +// option for configuring Logger, Clock and xrayClient. +type Manifest struct { + Rules []Rule + SamplingTargetsPollingInterval time.Duration + refreshedAt int64 + xrayClient *xrayClient + logger logr.Logger + clock util.Clock + mu sync.RWMutex +} + +// NewManifest return manifest object configured with logging, clock and xrayClient. +func NewManifest(addr string, logger logr.Logger) (*Manifest, error) { + client, err := newClient(addr); if err != nil { + return nil, err + } + + return &Manifest{ + xrayClient: client, + clock: &util.DefaultClock{}, + logger: logger, + SamplingTargetsPollingInterval: 10 * time.Second, + }, nil +} + +// Expired returns true if the manifest has not been successfully refreshed in +// manifestTTL seconds. +func (m *Manifest) Expired() bool { + m.mu.RLock() + defer m.mu.RUnlock() + + return m.refreshedAt < m.clock.Now().Unix()-manifestTTL +} + +// MatchAgainstManifestRules returns a Rule and boolean flag set as true if rule has been match against span attributes, otherwise nil and false +func (m *Manifest) MatchAgainstManifestRules(parameters sdktrace.SamplingParameters, serviceName string, cloudPlatform string) (*Rule, bool) { + m.mu.RLock() + defer m.mu.RUnlock() + + matched := false + + for index, r := range m.Rules { + isRuleMatch := r.appliesTo(parameters, serviceName, cloudPlatform) + + if isRuleMatch { + matched = true + return &m.Rules[index], matched + } + } + + return nil, matched +} + +// RefreshManifestRules writes sampling rule properties to the manifest object. +func (m *Manifest) RefreshManifestRules(ctx context.Context) (err error) { + // get sampling rules from AWS X-Ray console + rules, err := m.xrayClient.getSamplingRules(ctx) + if err != nil { + return err + } + + // update the retrieved sampling rules to manifest object + m.updateRules(rules) + + return +} + +// RefreshManifestTargets updates sampling targets (statistics) for each rule +func (m *Manifest) RefreshManifestTargets(ctx context.Context) (err error) { + var manifest Manifest + + // deep copy centralized manifest object to temporary manifest to avoid thread safety issue + m.mu.RLock() + err = copier.CopyWithOption(&manifest, m, copier.Option{IgnoreEmpty: false, DeepCopy: true}); if err != nil { + return err + } + m.mu.RUnlock() + + // generate sampling statistics based on the data in temporary manifest + statistics, err := manifest.snapshots(); if err != nil { return err } + + // return if no statistics to report + if len(statistics) == 0 { + m.logger.V(5).Info("no statistics to report and not refreshing sampling targets") + return nil + } + + // get sampling targets (statistics) for every expired rule from AWS X-Ray + targets, err := m.xrayClient.getSamplingTargets(ctx, statistics) + if err != nil { + return fmt.Errorf("refreshTargets: error occurred while getting sampling targets: %w", err) + } else { + m.logger.V(5).Info("successfully fetched sampling targets") + } + + // update temporary manifest with retrieved targets (statistics) for each rule + refresh, err := manifest.updateTargets(targets); if err != nil { + return err + } + + // find next polling interval for targets + minPoll := manifest.minimumPollingInterval(targets) + if minPoll > 0 { + m.SamplingTargetsPollingInterval = time.Duration(minPoll) * time.Second + } + + // update centralized manifest object + m.mu.Lock() + m.Rules = manifest.Rules + m.mu.Unlock() + + // perform out-of-band async manifest refresh if refresh is set to true + if refresh { + m.logger.V(5).Info("refreshing sampling rules out-of-band") + + go func() { + if err := m.RefreshManifestRules(ctx); err != nil { + m.logger.Error(err, "error occurred refreshing sampling rules out-of-band") + } + }() + } + + return +} + +func (m *Manifest) updateRules(rules *getSamplingRulesOutput) { + tempManifest := Manifest{ + Rules: []Rule{}, + } + + for _, records := range rules.SamplingRuleRecords { + if records.SamplingRule.RuleName == "" { + m.logger.V(5).Info("Sampling rule without rule name is not supported") + continue + } + + if records.SamplingRule.Version != int64(1) { + m.logger.V(5).Info("Sampling rule without Version 1 is not supported", "RuleName", records.SamplingRule.RuleName) + continue + } + + // create rule and store it in temporary manifest to avoid thread safety issues + tempManifest.createRule(*records.SamplingRule) + } + + // Re-sort to fix matching priorities. + tempManifest.sort() + + m.mu.Lock() + m.Rules = tempManifest.Rules + m.refreshedAt = m.clock.Now().Unix() + m.mu.Unlock() + + return +} + +func (m *Manifest) createRule(ruleProp ruleProperties) { + cr := reservoir { + capacity: ruleProp.ReservoirSize, + interval: defaultInterval, + } + + csr := Rule { + reservoir: cr, + ruleProperties: ruleProp, + } + + m.Rules = append(m.Rules, csr) + + return +} + +func (m *Manifest) updateTargets(targets *getSamplingTargetsOutput) (refresh bool, err error) { + // update sampling targets for each rule + for _, t := range targets.SamplingTargetDocuments { + if t.RuleName != nil && t.ReservoirQuota != nil { + fmt.Println("rule name") + fmt.Println(*t.RuleName) + fmt.Println("assigned quota") + fmt.Println(*t.ReservoirQuota) + } + + if err := m.updateReservoir(t); err != nil { + return false, err + } + } + + // consume unprocessed statistics messages + for _, s := range targets.UnprocessedStatistics { + m.logger.V(5).Info( + "error occurred updating sampling target for rule, code and message", "RuleName", *s.RuleName, "ErrorCode", + *s.ErrorCode, + "Message", *s.Message, + ) + + // do not set any flags if error is unknown + if s.ErrorCode == nil || s.RuleName == nil { + continue + } + + // set batch failure if any sampling statistics returned 5xx + if strings.HasPrefix(*s.ErrorCode, "5") { + return false, fmt.Errorf("sampling statistics returned 5xx") + } + + // set refresh flag if any sampling statistics returned 4xx + if strings.HasPrefix(*s.ErrorCode, "4") { + refresh = true + } + } + + // set refresh flag if modifiedAt timestamp from remote is greater than ours + if remote := targets.LastRuleModification; remote != nil { + if int64(*remote) >= m.refreshedAt { + refresh = true + } + } + + return +} + +func (m *Manifest) updateReservoir(t *samplingTargetDocument) (err error) { + if t.RuleName == nil { + return fmt.Errorf("invalid sampling target. Missing rule name") + } + + if t.FixedRate == nil { + return fmt.Errorf("invalid sampling target for rule %s. Missing fixed rate", *t.RuleName) + } + + for index, rule := range m.Rules { + if rule.ruleProperties.RuleName == *t.RuleName { + m.Rules[index].reservoir.refreshedAt = m.clock.Now().Unix() + + // Update non-optional attributes from response + m.Rules[index].ruleProperties.FixedRate = *t.FixedRate + + // Update optional attributes from response + if t.ReservoirQuota != nil { + m.Rules[index].reservoir.quota = *t.ReservoirQuota + } + if t.ReservoirQuotaTTL != nil { + m.Rules[index].reservoir.expiresAt = int64(*t.ReservoirQuotaTTL) + } + if t.Interval != nil { + m.Rules[index].reservoir.interval = *t.Interval + } + } + } + + return +} + +// snapshots takes a snapshot of sampling statistics from all rules, resetting +// statistics counters in the process. +func (m *Manifest) snapshots() ([]*samplingStatisticsDocument, error) { + statistics := make([]*samplingStatisticsDocument, 0, len(m.Rules)+1) + + // Generate sampling statistics for user-defined rules + for _, r := range m.Rules { +// if r.stale(m.clock.Now().Unix()) { + s := r.snapshot() + clientID, err := generateClientId(); if err != nil { + return nil, err + } + s.ClientID = clientID + + statistics = append(statistics, s) +// } + } + + return statistics, nil +} + +// sort sorts the rule array first by priority and then by rule name. +func (m *Manifest) sort() { + // comparison function + less := func(i, j int) bool { + if m.Rules[i].ruleProperties.Priority == m.Rules[j].ruleProperties.Priority { + return strings.Compare(m.Rules[i].ruleProperties.RuleName, m.Rules[j].ruleProperties.RuleName) < 0 + } + return m.Rules[i].ruleProperties.Priority < m.Rules[j].ruleProperties.Priority + } + + sort.Slice(m.Rules, less) +} + +// minimumPollingInterval finds the minimum interval amongst all the targets +func (m *Manifest) minimumPollingInterval(targets *getSamplingTargetsOutput) (minPoll int64) { + minPoll = 0 + for _, t := range targets.SamplingTargetDocuments { + if t.Interval != nil { + if minPoll == 0 { + minPoll = *t.Interval + } else { + if minPoll > *t.Interval { + minPoll = *t.Interval + } + } + } + } + + return minPoll +} + +// generateClientId generates random client ID +func generateClientId() (*string, error) { + var r [12]byte + + _, err := crypto.Read(r[:]) + if err != nil { + return nil, fmt.Errorf("unable to generate client ID: %w", err) + } + + id := fmt.Sprintf("%02x", r) + + return &id, err +} + + diff --git a/samplers/aws/xray/internal/match.go b/samplers/aws/xray/internal/match.go new file mode 100644 index 00000000000..ecdee7580c0 --- /dev/null +++ b/samplers/aws/xray/internal/match.go @@ -0,0 +1,70 @@ +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package internal + +import "strings" + +// wildcardMatch returns true if text matches pattern at the given case-sensitivity; returns false otherwise. +func wildcardMatch(pattern, text string) bool { + patternLen := len(pattern) + textLen := len(text) + if patternLen == 0 { + return textLen == 0 + } + + if pattern == "*" { + return true + } + + pattern = strings.ToLower(pattern) + text = strings.ToLower(text) + + i := 0 + p := 0 + iStar := textLen + pStar := 0 + + for i < textLen { + if p < patternLen { + switch pattern[p] { + case text[i]: + i++ + p++ + continue + case '?': + i++ + p++ + continue + case '*': + iStar = i + pStar = p + p++ + continue + } + } + if iStar == textLen { + return false + } + iStar++ + i = iStar + p = pStar + 1 + } + + for p < patternLen && pattern[p] == '*' { + p++ + } + + return p == patternLen && i == textLen +} \ No newline at end of file diff --git a/samplers/aws/xray/internal/reservoir.go b/samplers/aws/xray/internal/reservoir.go new file mode 100644 index 00000000000..0d6038531cc --- /dev/null +++ b/samplers/aws/xray/internal/reservoir.go @@ -0,0 +1,79 @@ +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package internal + +import ( + "sync/atomic" +) + +// reservoir represents a sampling statistics for a given rule and populate it's value from +// the response getSamplingTargets API which sends information on sampling statistics real-time +type reservoir struct { + // quota assigned to client + quota int64 + + // quota refresh timestamp + refreshedAt int64 + + // quota expiration timestamp + expiresAt int64 + + // polling interval for quota + interval int64 + + // total size of reservoir + capacity int64 + + // reservoir consumption for current epoch + used int64 + + // reservoir usage is reset every second + currentEpoch int64 +} + +// expired returns true if current time is past expiration timestamp. False otherwise. +func (r *reservoir) expired(now int64) bool { + expire := atomic.LoadInt64(&r.expiresAt) + + return now > expire +} + +// borrow returns true if the reservoir has not been borrowed from this epoch. +func (r *reservoir) borrow(now int64) bool { + cur := atomic.LoadInt64(&r.currentEpoch) + if cur >= now { + return false + } + return atomic.CompareAndSwapInt64(&r.currentEpoch, cur, now) +} + +// Take consumes quota from reservoir, if any remains, then returns true. False otherwise. +func (r *reservoir) take(now int64) bool { + cur := atomic.LoadInt64(&r.currentEpoch) + quota := atomic.LoadInt64(&r.quota) + used := atomic.LoadInt64(&r.used) + + if cur != now { + atomic.CompareAndSwapInt64(&r.currentEpoch, cur, now) + atomic.CompareAndSwapInt64(&r.used, used, int64(0)) + used = 0 + } + + if quota > used { + atomic.AddInt64(&r.used, 1) + return true + } + return false +} diff --git a/samplers/aws/xray/internal/rule.go b/samplers/aws/xray/internal/rule.go new file mode 100644 index 00000000000..f5f5722fcc8 --- /dev/null +++ b/samplers/aws/xray/internal/rule.go @@ -0,0 +1,155 @@ +package internal + +import ( + "fmt" + "go.opentelemetry.io/contrib/samplers/aws/xray/internal/util" + sdktrace "go.opentelemetry.io/otel/sdk/trace" + "go.opentelemetry.io/otel/trace" + "sync/atomic" +) + +// Rule represents a sampling rule which contains rule properties and reservoir which keeps tracks of sampling statistics of a rule +type Rule struct { + // reservoir has equivalent fields to store what we receive from service API getSamplingTargets + // https://docs.aws.amazon.com/xray/latest/api/API_GetSamplingTargets.html + reservoir reservoir + + // equivalent to what we receive from service API getSamplingRules + // https://docs.aws.amazon.com/cli/latest/reference/xray/get-sampling-rules.html + ruleProperties ruleProperties + + // number of requests matched against specific rule + matchedRequests int64 + + // number of requests sampled using specific rule + sampledRequests int64 + + // number of requests borrowed using specific rule + borrowedRequests int64 +} + +// stale checks if targets (sampling stats) for a given rule is expired or not +func (r *Rule) stale(now int64) bool { + return r.matchedRequests != 0 && now >= r.reservoir.refreshedAt+r.reservoir.interval +} + +// snapshot takes a snapshot of the sampling statistics counters, returning +// samplingStatisticsDocument. It also resets statistics counters. +func (r *Rule) snapshot() *samplingStatisticsDocument { + clock := &util.DefaultClock{} + now := clock.Now().Unix() + + name := r.ruleProperties.RuleName + requests, sampled, borrowed := r.matchedRequests, r.sampledRequests, r.borrowedRequests + + // reset counters + r.matchedRequests, r.sampledRequests, r.borrowedRequests = 0, 0, 0 + requests, sampled, borrowed = 4, 4, 4 + + return &samplingStatisticsDocument{ + RequestCount: &requests, + SampledCount: &sampled, + BorrowCount: &borrowed, + RuleName: &name, + Timestamp: &now, + } +} + +// Sample uses sampling targets of a given rule to decide which sampling should be done and returns a SamplingResult. +func (r *Rule) Sample(parameters sdktrace.SamplingParameters) sdktrace.SamplingResult { + sd := sdktrace.SamplingResult{ + Tracestate: trace.SpanContextFromContext(parameters.ParentContext).TraceState(), + } + + clock := &util.DefaultClock{} + now := clock.Now().Unix() + + atomic.AddInt64(&r.matchedRequests, int64(1)) + + // fallback sampling logic if quota for a given rule is expired + if r.reservoir.expired(now) { + // borrowing one request every second + if r.reservoir.borrow(now) { + fmt.Println("inside expired reservoir") + atomic.AddInt64(&r.borrowedRequests, int64(1)) + + sd.Decision = sdktrace.RecordAndSample + return sd + } + + fmt.Println("inside expired traceIDRatio") + // using traceIDRatioBased sampler to sample using fixed rate + sd = sdktrace.TraceIDRatioBased(r.ruleProperties.FixedRate).ShouldSample(parameters) + + if sd.Decision == sdktrace.RecordAndSample { + atomic.AddInt64(&r.sampledRequests, int64(1)) + } + + return sd + } + + // Take from reservoir quota, if quota is available for that second + if r.reservoir.take(now) { + fmt.Println("inside non expired reservoir") + atomic.AddInt64(&r.sampledRequests, int64(1)) + sd.Decision = sdktrace.RecordAndSample + + return sd + } + + fmt.Println("inside non expired traceIDRatio") + // using traceIDRatioBased sampler to sample using fixed rate + sd = sdktrace.TraceIDRatioBased(r.ruleProperties.FixedRate).ShouldSample(parameters) + + if sd.Decision == sdktrace.RecordAndSample { + atomic.AddInt64(&r.sampledRequests, int64(1)) + } + + return sd +} + +// appliesTo performs a matching against rule properties to see if a given rule does match with any of the rule set on AWS X-Ray console +func (r *Rule) appliesTo(parameters sdktrace.SamplingParameters, serviceName string, cloudPlatform string) bool { + var httpTarget string + var httpURL string + var httpHost string + var httpMethod string + + if parameters.Attributes != nil { + for _, attrs := range parameters.Attributes { + if attrs.Key == "http.target" { + httpTarget = attrs.Value.AsString() + } + if attrs.Key == "http.url" { + httpURL = attrs.Value.AsString() + } + if attrs.Key == "http.host" { + httpHost = attrs.Value.AsString() + } + if attrs.Key == "http.method" { + httpMethod = attrs.Value.AsString() + } + } + } + + return (r.attributeMatching(parameters)) && (wildcardMatch(r.ruleProperties.ServiceName, serviceName)) && + (wildcardMatch(r.ruleProperties.ServiceType, cloudPlatform)) && + (wildcardMatch(r.ruleProperties.Host, httpHost)) && + (wildcardMatch(r.ruleProperties.HTTPMethod, httpMethod)) && + (wildcardMatch(r.ruleProperties.URLPath, httpURL) || wildcardMatch(r.ruleProperties.URLPath, httpTarget)) +} + +// attributeMatching performs a match on attributes set by users on AWS X-Ray console +func (r *Rule) attributeMatching(parameters sdktrace.SamplingParameters) bool { + match := false + if len(r.ruleProperties.Attributes) > 0 { + for _, attrs := range parameters.Attributes { + if r.ruleProperties.Attributes[string(attrs.Key)] != "" { + match = wildcardMatch(r.ruleProperties.Attributes[string(attrs.Key)], attrs.Value.AsString()) + } + } + return match + } + + return true +} \ No newline at end of file diff --git a/samplers/aws/xray/internal/util/clock.go b/samplers/aws/xray/internal/util/clock.go new file mode 100644 index 00000000000..65ab07c1bc3 --- /dev/null +++ b/samplers/aws/xray/internal/util/clock.go @@ -0,0 +1,32 @@ +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package util + +import ( + "time" +) + +// Clock provides an interface to implement method for getting current time. +type Clock interface { + Now() time.Time +} + +// DefaultClock is an implementation of Clock interface. +type DefaultClock struct{} + +// Now returns current time. +func (t *DefaultClock) Now() time.Time { + return time.Now() +} diff --git a/samplers/aws/xray/internal/util/rand.go b/samplers/aws/xray/internal/util/rand.go new file mode 100644 index 00000000000..6792a14eaa9 --- /dev/null +++ b/samplers/aws/xray/internal/util/rand.go @@ -0,0 +1,114 @@ +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package util + +import ( + crand "crypto/rand" + "encoding/binary" + "math/rand" + "sync" + "time" +) + +var _ rand.Source = (*lockedSource)(nil) +var _ rand.Source64 = (*lockedSource64)(nil) + +type lockedSource struct { + mu sync.Mutex + src rand.Source +} + +func (src *lockedSource) Int63() int64 { + src.mu.Lock() + defer src.mu.Unlock() + return src.src.Int63() +} + +func (src *lockedSource) Seed(seed int64) { + src.mu.Lock() + defer src.mu.Unlock() + src.src.Seed(seed) +} + +type lockedSource64 struct { + mu sync.Mutex + src rand.Source64 +} + +func (src *lockedSource64) Int63() int64 { + src.mu.Lock() + defer src.mu.Unlock() + return src.src.Int63() +} + +func (src *lockedSource64) Uint64() uint64 { + src.mu.Lock() + defer src.mu.Unlock() + return src.src.Uint64() +} + +func (src *lockedSource64) Seed(seed int64) { + src.mu.Lock() + defer src.mu.Unlock() + src.src.Seed(seed) +} + +func newSeed() int64 { + var seed int64 + if err := binary.Read(crand.Reader, binary.BigEndian, &seed); err != nil { + // fallback to timestamp + seed = time.Now().UnixNano() + } + return seed +} + +func newGlobalRand() *rand.Rand { + src := rand.NewSource(newSeed()) + if src64, ok := src.(rand.Source64); ok { + return rand.New(&lockedSource64{src: src64}) + } + return rand.New(&lockedSource{src: src}) +} + +// Rand is an interface for a set of methods that return random value. +type Rand interface { + Int63n(n int64) int64 + Intn(n int) int + Float64() float64 +} + +// defaultRand is an implementation of Rand interface. +// It is safe for concurrent use by multiple goroutines. +type defaultRand struct{} + +var globalRand = newGlobalRand() + +// Int63n returns, as an int64, a non-negative pseudo-random number in [0,n) +// from the default Source. +func (r *defaultRand) Int63n(n int64) int64 { + return globalRand.Int63n(n) +} + +// Intn returns, as an int, a non-negative pseudo-random number in [0,n) +// from the default Source. +func (r *defaultRand) Intn(n int) int { + return globalRand.Intn(n) +} + +// Float64 returns, as a float64, a pseudo-random number in [0.0,1.0) +// from the default Source. +func (r *defaultRand) Float64() float64 { + return globalRand.Float64() +} \ No newline at end of file diff --git a/samplers/aws/xray/internal/util/timer.go b/samplers/aws/xray/internal/util/timer.go new file mode 100644 index 00000000000..f758fdf24e8 --- /dev/null +++ b/samplers/aws/xray/internal/util/timer.go @@ -0,0 +1,45 @@ +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package util + +import ( + "time" +) + +// Ticker is the same as time.Ticker except that it has jitters. +// A Ticker must be created with NewTicker. +type ticker struct { + t *time.Ticker + d time.Duration + jitter time.Duration +} + +// NewTicker creates a new Ticker that will send the current time on its channel. +func NewTicker(d, jitter time.Duration) *ticker { + t := time.NewTicker(d - time.Duration(globalRand.Int63n(int64(jitter)))) + + jitteredTicker := ticker{ + t: t, + d: d, + jitter: jitter, + } + + return &jitteredTicker +} + +// C is channel. +func (j *ticker) C() <-chan time.Time { + return j.t.C +} \ No newline at end of file diff --git a/samplers/aws/xray/remote_sampler.go b/samplers/aws/xray/remote_sampler.go new file mode 100644 index 00000000000..7c88f019574 --- /dev/null +++ b/samplers/aws/xray/remote_sampler.go @@ -0,0 +1,180 @@ +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "context" + "go.opentelemetry.io/contrib/samplers/aws/xray/internal" + "go.opentelemetry.io/contrib/samplers/aws/xray/internal/util" + "sync" + "time" + + "github.com/go-logr/logr" + + sdktrace "go.opentelemetry.io/otel/sdk/trace" +) + +// remoteSampler is a sampler for AWS X-Ray which polls sampling rules and sampling targets +// to make a sampling decision based on rules set by users on AWS X-Ray console +type remoteSampler struct { + // manifest is the list of known centralized sampling rules. + manifest *internal.Manifest + + // pollerStarted, if true represents rule and target pollers are started. + pollerStarted bool + + // samplingRulesPollingInterval, default is 300 seconds. + samplingRulesPollingInterval time.Duration + + // matching attribute + serviceName string + + // matching attribute + cloudPlatform string + + // fallback sampler + fallbackSampler *FallbackSampler + + // logger for logging + logger logr.Logger + + mu sync.RWMutex +} + +// Compile time assertion that remoteSampler implements the Sampler interface. +var _ sdktrace.Sampler = (*remoteSampler)(nil) + +// NewRemoteSampler returns a sampler which decides to sample a given request or not +// based on the sampling rules set by users on AWS X-Ray console. Sampler also periodically polls +// sampling rules and sampling targets. +func NewRemoteSampler(ctx context.Context, serviceName string, cloudPlatform string, opts ...Option) (sdktrace.Sampler, error) { + // create new config based on options or set to default values + cfg := newConfig(opts...) + + // validate config + err := validateConfig(cfg) + if err != nil { + return nil, err + } + + // create manifest with config + m, err := internal.NewManifest(cfg.endpoint, cfg.logger); if err != nil { + return nil, err + } + + remoteSampler := &remoteSampler { + manifest: m, + samplingRulesPollingInterval: cfg.samplingRulesPollingInterval, + fallbackSampler: NewFallbackSampler(), + serviceName: serviceName, + cloudPlatform: cloudPlatform, + logger: cfg.logger, + } + + // starts the rule and target poller + remoteSampler.start(ctx) + + return remoteSampler, nil +} + +// ShouldSample matches span attributes with retrieved sampling rules and perform sampling, +// if rules does not match or manifest is expired then use fallback sampling. +func (rs *remoteSampler) ShouldSample(parameters sdktrace.SamplingParameters) sdktrace.SamplingResult { + if !rs.manifest.Expired() { + // match against known rules + r, match := rs.manifest.MatchAgainstManifestRules(parameters, rs.serviceName, rs.cloudPlatform); if match { + // remote sampling based on rule match + return r.Sample(parameters) + } + } + + // Use fallback sampler if manifest is expired or sampling rules does not match against manifest + rs.logger.V(5).Info("span attributes does not match to the sampling rules or manifest is expired so using fallback sampling strategy") + return rs.fallbackSampler.ShouldSample(parameters) +} + +// Description returns description of the sampler being used +func (rs *remoteSampler) Description() string { + return "AwsXrayRemoteSampler{" + rs.getDescription() + "}" +} + +func (rs *remoteSampler) getDescription() string { + return "remote sampling with AWS X-Ray" +} + +func (rs *remoteSampler) start(ctx context.Context) { + if !rs.pollerStarted { + rs.pollerStarted = true + rs.startPoller(ctx) + } +} + +// startPoller starts the rule and target poller in a single go routine which runs periodically +// to refresh manifest and targets +func (rs *remoteSampler) startPoller(ctx context.Context) { +// go func() { + // jitter = 5s, default 300 seconds + rulesTicker := util.NewTicker(rs.samplingRulesPollingInterval, 5*time.Second) + + // jitter = 100ms, default 10 seconds + targetTicker := util.NewTicker(rs.manifest.SamplingTargetsPollingInterval, 100*time.Millisecond) + + // fetch sampling rules to kick start the remote sampling + if err := rs.manifest.RefreshManifestRules(ctx); err != nil { + rs.logger.Error(err, "Error occurred while refreshing sampling rules") + } else { + rs.logger.V(5).Info("Successfully fetched sampling rules") + } + + for { + select { + case _, more := <-rulesTicker.C(): + if !more { + return + } + + // fetch sampling rules and updates manifest + if err := rs.manifest.RefreshManifestRules(ctx); err != nil { + rs.logger.Error(err, "error occurred while refreshing sampling rules") + } else { + rs.logger.V(5).Info("successfully fetched sampling rules") + } + continue + case _, more := <-targetTicker.C(): + if !more { + return + } + + // fetch sampling targets and updates manifest + if err := rs.manifest.RefreshManifestTargets(ctx); err != nil { + rs.logger.Error(err, "error occurred while refreshing sampling rule targets") + } + continue + case <-ctx.Done(): + return + } + } +// }() +} + +func main() { + ctx := context.Background() + rs, _ := NewRemoteSampler(ctx, "test", "test-platform") + + for i := 0; i < 1000; i++ { + rs.ShouldSample(sdktrace.SamplingParameters{}) + time.Sleep(250 * time.Millisecond) + } +} diff --git a/samplers/aws/xray/remote_sampler_config.go b/samplers/aws/xray/remote_sampler_config.go new file mode 100644 index 00000000000..f0b4c085b03 --- /dev/null +++ b/samplers/aws/xray/remote_sampler_config.go @@ -0,0 +1,111 @@ +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "fmt" + "log" + "math" + "os" + "regexp" + "strconv" + "strings" + "time" + + "github.com/go-logr/logr" + "github.com/go-logr/stdr" +) + +const ( + defaultProxyEndpoint = "127.0.0.1:2000" + defaultPollingInterval = 300 +) + +// SamplerOption is a function that sets config on the sampler +type Option func(options *config) + +type config struct { + endpoint string + samplingRulesPollingInterval time.Duration + logger logr.Logger +} + +// sets custom proxy endpoint +func WithEndpoint(endpoint string) Option { + return func(o *config) { + o.endpoint = endpoint + } +} + +// sets polling interval for sampling rules +func WithSamplingRulesPollingInterval(polingInterval time.Duration) Option { + return func(o *config) { + o.samplingRulesPollingInterval = polingInterval + } +} + +// sets custom logging for remote sampling implementation +func WithLogger(l logr.Logger) Option { + return func(o *config) { + o.logger = l + } +} + +func newConfig(opts ...Option) *config { + cfg := &config{ + endpoint: defaultProxyEndpoint, + samplingRulesPollingInterval: defaultPollingInterval * time.Second, + logger: stdr.NewWithOptions(log.New(os.Stderr, "", log.LstdFlags|log.Lshortfile), stdr.Options{LogCaller: stdr.Error}), + } + + for _, option := range opts { + option(cfg) + } + + stdr.SetVerbosity(5) + + return cfg +} + +func validateConfig(cfg *config) (err error) { + // check endpoint follows certain format + split := strings.Split(cfg.endpoint, ":") + + if len(split) > 2 { + return fmt.Errorf("endpoint validation error: expected format is 127.0.0.1:8080") + } + + // validate host name + r, err := regexp.Compile("[^A-Za-z0-9.]") + if err != nil { + return err + } + + if r.MatchString(split[0]) { + return fmt.Errorf("endpoint validation error: expected format is 127.0.0.1:8080") + } + + // validate port + if _, err := strconv.Atoi(split[1]); err != nil { + return fmt.Errorf("endpoint validation error: expected format is 127.0.0.1:8080") + } + + // validate polling interval is positive + if math.Signbit(float64(cfg.samplingRulesPollingInterval)) { + return fmt.Errorf("endpoint validation error: samplingRulesPollingInterval should be positive number") + } + + return +} From 762ee2cec7dae93e48a10ebe7a0bc2f671e197e8 Mon Sep 17 00:00:00 2001 From: bhautikpip Date: Tue, 22 Feb 2022 18:50:47 -0800 Subject: [PATCH 03/38] remove debugging logic --- samplers/aws/xray/internal/manifest.go | 4 ++-- samplers/aws/xray/internal/rule.go | 1 - samplers/aws/xray/remote_sampler.go | 4 ++-- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/samplers/aws/xray/internal/manifest.go b/samplers/aws/xray/internal/manifest.go index aef5625ece7..4f0d27ed387 100644 --- a/samplers/aws/xray/internal/manifest.go +++ b/samplers/aws/xray/internal/manifest.go @@ -279,7 +279,7 @@ func (m *Manifest) snapshots() ([]*samplingStatisticsDocument, error) { // Generate sampling statistics for user-defined rules for _, r := range m.Rules { -// if r.stale(m.clock.Now().Unix()) { + if r.stale(m.clock.Now().Unix()) { s := r.snapshot() clientID, err := generateClientId(); if err != nil { return nil, err @@ -287,7 +287,7 @@ func (m *Manifest) snapshots() ([]*samplingStatisticsDocument, error) { s.ClientID = clientID statistics = append(statistics, s) -// } + } } return statistics, nil diff --git a/samplers/aws/xray/internal/rule.go b/samplers/aws/xray/internal/rule.go index f5f5722fcc8..b25329cb333 100644 --- a/samplers/aws/xray/internal/rule.go +++ b/samplers/aws/xray/internal/rule.go @@ -44,7 +44,6 @@ func (r *Rule) snapshot() *samplingStatisticsDocument { // reset counters r.matchedRequests, r.sampledRequests, r.borrowedRequests = 0, 0, 0 - requests, sampled, borrowed = 4, 4, 4 return &samplingStatisticsDocument{ RequestCount: &requests, diff --git a/samplers/aws/xray/remote_sampler.go b/samplers/aws/xray/remote_sampler.go index 7c88f019574..e5f578b241f 100644 --- a/samplers/aws/xray/remote_sampler.go +++ b/samplers/aws/xray/remote_sampler.go @@ -124,7 +124,7 @@ func (rs *remoteSampler) start(ctx context.Context) { // startPoller starts the rule and target poller in a single go routine which runs periodically // to refresh manifest and targets func (rs *remoteSampler) startPoller(ctx context.Context) { -// go func() { + go func() { // jitter = 5s, default 300 seconds rulesTicker := util.NewTicker(rs.samplingRulesPollingInterval, 5*time.Second) @@ -166,7 +166,7 @@ func (rs *remoteSampler) startPoller(ctx context.Context) { return } } -// }() + }() } func main() { From 43d432b96a12741e15b100b6f15864c707d3dbd6 Mon Sep 17 00:00:00 2001 From: bhautikpip Date: Tue, 22 Feb 2022 19:12:57 -0800 Subject: [PATCH 04/38] fix clientID generation bug --- samplers/aws/xray/internal/manifest.go | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/samplers/aws/xray/internal/manifest.go b/samplers/aws/xray/internal/manifest.go index 4f0d27ed387..271e1c135b1 100644 --- a/samplers/aws/xray/internal/manifest.go +++ b/samplers/aws/xray/internal/manifest.go @@ -25,6 +25,7 @@ type Manifest struct { SamplingTargetsPollingInterval time.Duration refreshedAt int64 xrayClient *xrayClient + clientID *string logger logr.Logger clock util.Clock mu sync.RWMutex @@ -32,15 +33,22 @@ type Manifest struct { // NewManifest return manifest object configured with logging, clock and xrayClient. func NewManifest(addr string, logger logr.Logger) (*Manifest, error) { + // generate client for getSamplingRules and getSamplingTargets API call client, err := newClient(addr); if err != nil { return nil, err } + // generate clientID for sampling statistics + clientID, err := generateClientId(); if err != nil { + return nil, err + } + return &Manifest{ xrayClient: client, clock: &util.DefaultClock{}, logger: logger, SamplingTargetsPollingInterval: 10 * time.Second, + clientID: clientID, }, nil } @@ -281,10 +289,7 @@ func (m *Manifest) snapshots() ([]*samplingStatisticsDocument, error) { for _, r := range m.Rules { if r.stale(m.clock.Now().Unix()) { s := r.snapshot() - clientID, err := generateClientId(); if err != nil { - return nil, err - } - s.ClientID = clientID + s.ClientID = m.clientID statistics = append(statistics, s) } From 3565ccf521a8daa44eb3056850cfb4cb7a7e5c67 Mon Sep 17 00:00:00 2001 From: bhautikpip Date: Thu, 24 Feb 2022 18:00:49 -0800 Subject: [PATCH 05/38] added unit testing for remote sampling --- samplers/aws/xray/fallback_sampler_test.go | 51 + samplers/aws/xray/go.mod | 4 +- samplers/aws/xray/go.sum | 4 + samplers/aws/xray/internal/client_test.go | 283 ++++ samplers/aws/xray/internal/manifest.go | 27 +- samplers/aws/xray/internal/manifest_test.go | 1186 +++++++++++++++++ samplers/aws/xray/internal/match_test.go | 180 +++ samplers/aws/xray/internal/reservoir.go | 7 +- samplers/aws/xray/internal/reservoir_test.go | 151 +++ samplers/aws/xray/internal/rule.go | 22 +- samplers/aws/xray/internal/rule_test.go | 395 ++++++ samplers/aws/xray/internal/util/clock.go | 20 + samplers/aws/xray/remote_sampler.go | 7 +- .../aws/xray/remote_sampler_config_test.go | 101 ++ samplers/aws/xray/remote_sampler_test.go | 57 + 15 files changed, 2464 insertions(+), 31 deletions(-) create mode 100644 samplers/aws/xray/fallback_sampler_test.go create mode 100644 samplers/aws/xray/internal/client_test.go create mode 100644 samplers/aws/xray/internal/manifest_test.go create mode 100644 samplers/aws/xray/internal/match_test.go create mode 100644 samplers/aws/xray/internal/reservoir_test.go create mode 100644 samplers/aws/xray/internal/rule_test.go create mode 100644 samplers/aws/xray/remote_sampler_config_test.go create mode 100644 samplers/aws/xray/remote_sampler_test.go diff --git a/samplers/aws/xray/fallback_sampler_test.go b/samplers/aws/xray/fallback_sampler_test.go new file mode 100644 index 00000000000..b6375f0c17a --- /dev/null +++ b/samplers/aws/xray/fallback_sampler_test.go @@ -0,0 +1,51 @@ +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "github.com/stretchr/testify/assert" + "go.opentelemetry.io/otel/sdk/trace" + "testing" +) + +// assert sampling using fallback sampler. +func TestSampleUsingFallbackSampler(t *testing.T) { + fs := NewFallbackSampler() + assert.NotEmpty(t, fs.defaultSampler) + + sd := fs.ShouldSample(trace.SamplingParameters{}) + assert.Equal(t, trace.RecordAndSample, sd.Decision) +} + +// assert that we only borrow 1 req/sec. +func TestBorrowOnePerSecond(t *testing.T) { + fs := NewFallbackSampler() + borrowed := fs.borrow(1500000000) + + // assert that borrowing one per second + assert.True(t, borrowed) + + borrowed = fs.borrow(1500000000) + + // assert that borrowing again is false during that second + assert.False(t, borrowed) +} + +// assert fallback sampling description. +func TestFallbackSamplerDescription(t *testing.T) { + fs := NewFallbackSampler() + s := fs.Description() + assert.Equal(t, s, "FallbackSampler{fallback sampling with sampling config of 1 req/sec and 5% of additional requests}") +} \ No newline at end of file diff --git a/samplers/aws/xray/go.mod b/samplers/aws/xray/go.mod index bd6b0a16899..e99f18b94ff 100644 --- a/samplers/aws/xray/go.mod +++ b/samplers/aws/xray/go.mod @@ -8,7 +8,9 @@ require ( github.com/go-logr/logr v1.2.2 github.com/go-logr/stdr v1.2.2 github.com/jinzhu/copier v0.3.5 + github.com/stretchr/testify v1.7.0 + go.opentelemetry.io/otel v1.4.0 // indirect go.opentelemetry.io/otel/sdk v1.4.0 go.opentelemetry.io/otel/trace v1.4.0 golang.org/x/sys v0.0.0-20210510120138-977fb7262007 // indirect -) \ No newline at end of file +) diff --git a/samplers/aws/xray/go.sum b/samplers/aws/xray/go.sum index fae9d0e1338..ce697ceae91 100644 --- a/samplers/aws/xray/go.sum +++ b/samplers/aws/xray/go.sum @@ -1,3 +1,4 @@ +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/go-logr/logr v1.2.2 h1:ahHml/yUpnlb96Rp8HCvtYVPY8ZYpxq3g7UYchIYwbs= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -6,8 +7,10 @@ github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= github.com/jinzhu/copier v0.3.5 h1:GlvfUwHk62RokgqVNvYsku0TATCF7bAHVwEXoBh3iJg= github.com/jinzhu/copier v0.3.5/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= go.opentelemetry.io/otel v1.4.0 h1:7ESuKPq6zpjRaY5nvVDGiuwK7VAJ8MwkKnmNJ9whNZ4= go.opentelemetry.io/otel v1.4.0/go.mod h1:jeAqMFKy2uLIxCtKxoFj0FAL5zAPKQagc3+GtBWakzk= @@ -20,4 +23,5 @@ golang.org/x/sys v0.0.0-20210510120138-977fb7262007 h1:gG67DSER+11cZvqIMb8S8bt0v golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/samplers/aws/xray/internal/client_test.go b/samplers/aws/xray/internal/client_test.go new file mode 100644 index 00000000000..1d1c25c2e69 --- /dev/null +++ b/samplers/aws/xray/internal/client_test.go @@ -0,0 +1,283 @@ +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package internal + +import ( + "context" + "net/http" + "net/http/httptest" + "net/url" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGetSamplingRules(t *testing.T) { + body := []byte(`{ + "NextToken": null, + "SamplingRuleRecords": [ + { + "CreatedAt": 0, + "ModifiedAt": 1639517389, + "SamplingRule": { + "Attributes": {}, + "FixedRate": 0.5, + "HTTPMethod": "*", + "Host": "*", + "Priority": 10000, + "ReservoirSize": 60, + "ResourceARN": "*", + "RuleARN": "arn:aws:xray:us-west-2:xxxxxxx:sampling-rule/Default", + "RuleName": "Default", + "ServiceName": "*", + "ServiceType": "*", + "URLPath": "*", + "Version": 1 + } + }, + { + "CreatedAt": 1637691613, + "ModifiedAt": 1643748669, + "SamplingRule": { + "Attributes": {}, + "FixedRate": 0.09, + "HTTPMethod": "GET", + "Host": "*", + "Priority": 1, + "ReservoirSize": 3, + "ResourceARN": "*", + "RuleARN": "arn:aws:xray:us-west-2:xxxxxxx:sampling-rule/test-rule", + "RuleName": "test-rule", + "ServiceName": "test-rule", + "ServiceType": "local", + "URLPath": "/aws-sdk-call", + "Version": 1 + } + }, + { + "CreatedAt": 1639446197, + "ModifiedAt": 1639446197, + "SamplingRule": { + "Attributes": {}, + "FixedRate": 0.09, + "HTTPMethod": "*", + "Host": "*", + "Priority": 100, + "ReservoirSize": 100, + "ResourceARN": "*", + "RuleARN": "arn:aws:xray:us-west-2:xxxxxxx:sampling-rule/test-rule-1", + "RuleName": "test-rule-1", + "ServiceName": "*", + "ServiceType": "*", + "URLPath": "*", + "Version": 1 + } + } + ] +}`) + ctx := context.Background() + + // generate a test server so we can capture and inspect the request + testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { + _, err := res.Write([]byte(body)) + require.NoError(t, err) + })) + + u, err := url.Parse(testServer.URL) + require.NoError(t, err) + + client, err := newClient(u.Host) + require.NoError(t, err) + + samplingRules, err := client.getSamplingRules(ctx) + require.NoError(t, err) + + assert.Equal(t, samplingRules.SamplingRuleRecords[0].SamplingRule.RuleName, "Default") + assert.Equal(t, samplingRules.SamplingRuleRecords[0].SamplingRule.ServiceType, "*") + assert.Equal(t, samplingRules.SamplingRuleRecords[0].SamplingRule.Host, "*") + assert.Equal(t, samplingRules.SamplingRuleRecords[0].SamplingRule.URLPath, "*") + assert.Equal(t, samplingRules.SamplingRuleRecords[0].SamplingRule.ReservoirSize, int64(60)) + assert.Equal(t, samplingRules.SamplingRuleRecords[0].SamplingRule.FixedRate, 0.5) + + assert.Equal(t, samplingRules.SamplingRuleRecords[1].SamplingRule.RuleName, "test-rule") + assert.Equal(t, samplingRules.SamplingRuleRecords[1].SamplingRule.ServiceType, "local") + assert.Equal(t, samplingRules.SamplingRuleRecords[1].SamplingRule.Host, "*") + assert.Equal(t, samplingRules.SamplingRuleRecords[1].SamplingRule.URLPath, "/aws-sdk-call") + assert.Equal(t, samplingRules.SamplingRuleRecords[1].SamplingRule.ReservoirSize, int64(3)) + assert.Equal(t, samplingRules.SamplingRuleRecords[1].SamplingRule.FixedRate, 0.09) + + assert.Equal(t, samplingRules.SamplingRuleRecords[2].SamplingRule.RuleName, "test-rule-1") + assert.Equal(t, samplingRules.SamplingRuleRecords[2].SamplingRule.ServiceType, "*") + assert.Equal(t, samplingRules.SamplingRuleRecords[2].SamplingRule.Host, "*") + assert.Equal(t, samplingRules.SamplingRuleRecords[2].SamplingRule.URLPath, "*") + assert.Equal(t, samplingRules.SamplingRuleRecords[2].SamplingRule.ReservoirSize, int64(100)) + assert.Equal(t, samplingRules.SamplingRuleRecords[2].SamplingRule.FixedRate, 0.09) +} + +func TestGetSamplingRulesWithMissingValues(t *testing.T) { + body := []byte(`{ + "NextToken": null, + "SamplingRuleRecords": [ + { + "CreatedAt": 0, + "ModifiedAt": 1639517389, + "SamplingRule": { + "Attributes": {}, + "FixedRate": 0.5, + "HTTPMethod": "*", + "Host": "*", + "ResourceARN": "*", + "RuleARN": "arn:aws:xray:us-west-2:xxxxxxx:sampling-rule/Default", + "RuleName": "Default", + "ServiceName": "*", + "ServiceType": "*", + "URLPath": "*", + "Version": 1 + } + } + ] +}`) + ctx := context.Background() + + // generate a test server so we can capture and inspect the request + testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { + _, err := res.Write([]byte(body)) + require.NoError(t, err) + })) + defer testServer.Close() + + u, err := url.Parse(testServer.URL) + require.NoError(t, err) + + client, err := newClient(u.Host) + require.NoError(t, err) + + samplingRules, err := client.getSamplingRules(ctx) + require.NoError(t, err) + + // Priority and ReservoirSize are missing in API response so they are assigned as nil + assert.Equal(t, samplingRules.SamplingRuleRecords[0].SamplingRule.Priority, int64(0)) + assert.Equal(t, samplingRules.SamplingRuleRecords[0].SamplingRule.ReservoirSize, int64(0)) + + // other values are stored as expected + assert.Equal(t, samplingRules.SamplingRuleRecords[0].SamplingRule.RuleName, "Default") +} + +func TestGetSamplingTargets(t *testing.T) { + body := []byte(`{ + "LastRuleModification": 123456, + "SamplingTargetDocuments": [ + { + "FixedRate": 5, + "Interval": 5, + "ReservoirQuota": 3, + "ReservoirQuotaTTL": 456789, + "RuleName": "r1" + } + ], + "UnprocessedStatistics": [ + { + "ErrorCode": "200", + "Message": "ok", + "RuleName": "r1" + } + ] +}`) + + ctx := context.Background() + + // generate a test server so we can capture and inspect the request + testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { + _, err := res.Write([]byte(body)) + require.NoError(t, err) + })) + defer testServer.Close() + + u, err := url.Parse(testServer.URL) + require.NoError(t, err) + + client, err := newClient(u.Host) + require.NoError(t, err) + + samplingTragets, err := client.getSamplingTargets(ctx, nil) + require.NoError(t, err) + + assert.Equal(t, *samplingTragets.LastRuleModification, float64(123456)) + assert.Equal(t, *samplingTragets.SamplingTargetDocuments[0].FixedRate, float64(5)) + assert.Equal(t, *samplingTragets.SamplingTargetDocuments[0].Interval, int64(5)) + assert.Equal(t, *samplingTragets.SamplingTargetDocuments[0].ReservoirQuota, int64(3)) + assert.Equal(t, *samplingTragets.SamplingTargetDocuments[0].ReservoirQuotaTTL, float64(456789)) + assert.Equal(t, *samplingTragets.SamplingTargetDocuments[0].RuleName, "r1") + assert.Equal(t, *samplingTragets.UnprocessedStatistics[0].RuleName, "r1") + assert.Equal(t, *samplingTragets.UnprocessedStatistics[0].ErrorCode, "200") + assert.Equal(t, *samplingTragets.UnprocessedStatistics[0].Message, "ok") +} + +func TestGetSamplingTargetsMissingValues(t *testing.T) { + body := []byte(`{ + "LastRuleModification": 123456, + "SamplingTargetDocuments": [ + { + "FixedRate": 5, + "ReservoirQuotaTTL": 456789, + "RuleName": "r1" + } + ], + "UnprocessedStatistics": [ + { + "ErrorCode": "200", + "Message": "ok", + "RuleName": "r1" + } + ] +}`) + + ctx := context.Background() + + // generate a test server so we can capture and inspect the request + testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { + _, err := res.Write([]byte(body)) + require.NoError(t, err) + })) + defer testServer.Close() + + u, err := url.Parse(testServer.URL) + require.NoError(t, err) + + client, err := newClient(u.Host) + require.NoError(t, err) + + samplingTragets, err := client.getSamplingTargets(ctx, nil) + require.NoError(t, err) + + assert.Nil(t, samplingTragets.SamplingTargetDocuments[0].Interval) + assert.Nil(t, samplingTragets.SamplingTargetDocuments[0].ReservoirQuota) +} + +func TestNewClient(t *testing.T) { + xrayClient, err := newClient("127.0.0.1:2020") + require.NoError(t, err) + + assert.Equal(t, xrayClient.endpoint.String(), "http://127.0.0.1:2020") +} + +func TestEndpointIsNotReachable(t *testing.T) { + client, err := newClient("127.0.0.1:2020") + require.NoError(t, err) + + _, err = client.getSamplingRules(context.Background()) + assert.Error(t, err) +} \ No newline at end of file diff --git a/samplers/aws/xray/internal/manifest.go b/samplers/aws/xray/internal/manifest.go index 271e1c135b1..5df9115c561 100644 --- a/samplers/aws/xray/internal/manifest.go +++ b/samplers/aws/xray/internal/manifest.go @@ -27,7 +27,7 @@ type Manifest struct { xrayClient *xrayClient clientID *string logger logr.Logger - clock util.Clock + Clock util.Clock mu sync.RWMutex } @@ -45,7 +45,7 @@ func NewManifest(addr string, logger logr.Logger) (*Manifest, error) { return &Manifest{ xrayClient: client, - clock: &util.DefaultClock{}, + Clock: &util.DefaultClock{}, logger: logger, SamplingTargetsPollingInterval: 10 * time.Second, clientID: clientID, @@ -58,7 +58,7 @@ func (m *Manifest) Expired() bool { m.mu.RLock() defer m.mu.RUnlock() - return m.refreshedAt < m.clock.Now().Unix()-manifestTTL + return m.refreshedAt < m.Clock.Now().Unix()-manifestTTL } // MatchAgainstManifestRules returns a Rule and boolean flag set as true if rule has been match against span attributes, otherwise nil and false @@ -177,7 +177,7 @@ func (m *Manifest) updateRules(rules *getSamplingRulesOutput) { m.mu.Lock() m.Rules = tempManifest.Rules - m.refreshedAt = m.clock.Now().Unix() + m.refreshedAt = m.Clock.Now().Unix() m.mu.Unlock() return @@ -202,13 +202,6 @@ func (m *Manifest) createRule(ruleProp ruleProperties) { func (m *Manifest) updateTargets(targets *getSamplingTargetsOutput) (refresh bool, err error) { // update sampling targets for each rule for _, t := range targets.SamplingTargetDocuments { - if t.RuleName != nil && t.ReservoirQuota != nil { - fmt.Println("rule name") - fmt.Println(*t.RuleName) - fmt.Println("assigned quota") - fmt.Println(*t.ReservoirQuota) - } - if err := m.updateReservoir(t); err != nil { return false, err } @@ -217,9 +210,9 @@ func (m *Manifest) updateTargets(targets *getSamplingTargetsOutput) (refresh boo // consume unprocessed statistics messages for _, s := range targets.UnprocessedStatistics { m.logger.V(5).Info( - "error occurred updating sampling target for rule, code and message", "RuleName", *s.RuleName, "ErrorCode", - *s.ErrorCode, - "Message", *s.Message, + "error occurred updating sampling target for rule, code and message", "RuleName", s.RuleName, "ErrorCode", + s.ErrorCode, + "Message", s.Message, ) // do not set any flags if error is unknown @@ -259,7 +252,7 @@ func (m *Manifest) updateReservoir(t *samplingTargetDocument) (err error) { for index, rule := range m.Rules { if rule.ruleProperties.RuleName == *t.RuleName { - m.Rules[index].reservoir.refreshedAt = m.clock.Now().Unix() + m.Rules[index].reservoir.refreshedAt = m.Clock.Now().Unix() // Update non-optional attributes from response m.Rules[index].ruleProperties.FixedRate = *t.FixedRate @@ -287,8 +280,8 @@ func (m *Manifest) snapshots() ([]*samplingStatisticsDocument, error) { // Generate sampling statistics for user-defined rules for _, r := range m.Rules { - if r.stale(m.clock.Now().Unix()) { - s := r.snapshot() + if r.stale(m.Clock.Now().Unix()) { + s := r.snapshot(m.Clock.Now().Unix()) s.ClientID = m.clientID statistics = append(statistics, s) diff --git a/samplers/aws/xray/internal/manifest_test.go b/samplers/aws/xray/internal/manifest_test.go new file mode 100644 index 00000000000..5fd28da3081 --- /dev/null +++ b/samplers/aws/xray/internal/manifest_test.go @@ -0,0 +1,1186 @@ +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package internal + +import ( + "context" + "github.com/go-logr/stdr" + "github.com/stretchr/testify/require" + "go.opentelemetry.io/contrib/samplers/aws/xray/internal/util" + sdktrace "go.opentelemetry.io/otel/sdk/trace" + "log" + "net/http" + "net/http/httptest" + "net/url" + "os" + "testing" + + "github.com/stretchr/testify/assert" +) + +// assert that new manifest has certain non-nil attributes +func TestNewManifest(t *testing.T) { + logger := stdr.NewWithOptions(log.New(os.Stderr, "", log.LstdFlags|log.Lshortfile), stdr.Options{LogCaller: stdr.Error}) + m, err := NewManifest("127.0.0.1:2000", logger) + require.NoError(t, err) + + assert.NotEmpty(t, m.logger) + assert.NotEmpty(t, m.clientID) + assert.NotEmpty(t, m.SamplingTargetsPollingInterval) + + assert.NotNil(t, m.Clock) + assert.NotNil(t, m.xrayClient) +} + +// assert that manifest rule r2 is a match for sampling +func TestMatchAgainstManifestRules(t *testing.T) { + r1 := Rule{ + ruleProperties: ruleProperties{ + RuleName: "r1", + Priority: 10000, + Host: "*", + HTTPMethod: "*", + URLPath: "*", + ReservoirSize: 60, + FixedRate: 0.5, + Version: 1, + ServiceName: "helios", + ResourceARN: "*", + ServiceType: "*", + }, + reservoir: reservoir{ + expiresAt: 14050, + }, + } + + r2 := Rule{ + ruleProperties: ruleProperties{ + RuleName: "r2", + Priority: 100, + Host: "*", + HTTPMethod: "*", + URLPath: "*", + ReservoirSize: 6, + FixedRate: 0.5, + Version: 1, + ServiceName: "test", + ResourceARN: "*", + ServiceType: "local", + }, + reservoir: reservoir{ + expiresAt: 14050, + }, + } + + rules := []Rule{r1, r2} + + m := &Manifest{ + Rules: rules, + } + + exp, err := m.MatchAgainstManifestRules(sdktrace.SamplingParameters{}, "test", "local") + require.NotEmpty(t, err) + + // assert that manifest rule r2 is a match + assert.Equal(t, *exp, r2) +} + +func TestRefreshManifestRules(t *testing.T) { + ctx := context.Background() + + body := []byte(`{ + "NextToken": null, + "SamplingRuleRecords": [ + { + "CreatedAt": 0, + "ModifiedAt": 1639517389, + "SamplingRule": { + "Attributes": {}, + "FixedRate": 0.5, + "HTTPMethod": "*", + "Host": "*", + "Priority": 10000, + "ReservoirSize": 60, + "ResourceARN": "*", + "RuleARN": "arn:aws:xray:us-west-2:xxxxxxx:sampling-rule/r1", + "RuleName": "r1", + "ServiceName": "*", + "ServiceType": "*", + "URLPath": "*", + "Version": 1 + } + }, + { + "CreatedAt": 1637691613, + "ModifiedAt": 1643748669, + "SamplingRule": { + "Attributes": {}, + "FixedRate": 0.09, + "HTTPMethod": "GET", + "Host": "*", + "Priority": 1, + "ReservoirSize": 3, + "ResourceARN": "*", + "RuleARN": "arn:aws:xray:us-west-2:xxxxxxx:sampling-rule/r2", + "RuleName": "r2", + "ServiceName": "test-rule", + "ServiceType": "*", + "URLPath": "/aws-sdk-call", + "Version": 1 + } + }, + { + "CreatedAt": 1639446197, + "ModifiedAt": 1639446197, + "SamplingRule": { + "Attributes": {}, + "FixedRate": 0.09, + "HTTPMethod": "*", + "Host": "*", + "Priority": 100, + "ReservoirSize": 100, + "ResourceARN": "*", + "RuleARN": "arn:aws:xray:us-west-2:xxxxxxx:sampling-rule/r3", + "RuleName": "r3", + "ServiceName": "*", + "ServiceType": "local", + "URLPath": "*", + "Version": 1 + } + } + ] +}`) + + // generate a test server so we can capture and inspect the request + testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { + _, err := res.Write([]byte(body)) + require.NoError(t, err) + })) + defer testServer.Close() + + u, err := url.Parse(testServer.URL) + require.NoError(t, err) + + client, err := newClient(u.Host) + require.NoError(t, err) + + m := &Manifest{ + Rules: []Rule{}, + xrayClient: client, + Clock: &util.DefaultClock{}, + } + + err = m.RefreshManifestRules(ctx) + require.NoError(t, err) + + r1 := Rule{ + ruleProperties: ruleProperties{ + RuleName: "r1", + Priority: 10000, + Host: "*", + HTTPMethod: "*", + URLPath: "*", + ReservoirSize: 60, + Version: 1, + FixedRate: 0.5, + ServiceName: "*", + ResourceARN: "*", + ServiceType: "*", + Attributes: map[string]string{}, + }, + reservoir: reservoir{ + interval: defaultInterval, + capacity: int64(60), + }, + } + + r2 := Rule{ + ruleProperties: ruleProperties{ + RuleName: "r2", + Priority: 1, + Host: "*", + HTTPMethod: "GET", + URLPath: "/aws-sdk-call", + ReservoirSize: 3, + FixedRate: 0.09, + Version: 1, + ServiceName: "test-rule", + ResourceARN: "*", + ServiceType: "*", + Attributes: map[string]string{}, + }, + reservoir: reservoir{ + interval: defaultInterval, + capacity: int64(3), + }, + } + + r3 := Rule{ + ruleProperties: ruleProperties{ + RuleName: "r3", + Priority: 100, + Host: "*", + HTTPMethod: "*", + URLPath: "*", + ReservoirSize: 100, + FixedRate: 0.09, + Version: 1, + ServiceName: "*", + ResourceARN: "*", + ServiceType: "local", + Attributes: map[string]string{}, + }, + reservoir: reservoir{ + interval: defaultInterval, + capacity: int64(100), + }, + } + + // Assert on sorting order + assert.Equal(t, r2, m.Rules[0]) + assert.Equal(t, r3, m.Rules[1]) + assert.Equal(t, r1, m.Rules[2]) + + // Assert on size of manifest + assert.Equal(t, 3, len(m.Rules)) +} + +// assert that rule with no ServiceName updates manifest successfully with empty values +func TestRefreshManifestMissingServiceName(t *testing.T) { + ctx := context.Background() + + // rule with no ServiceName + body := []byte(`{ + "NextToken": null, + "SamplingRuleRecords": [ + { + "CreatedAt": 0, + "ModifiedAt": 1639517389, + "SamplingRule": { + "Attributes": {}, + "FixedRate": 0.5, + "HTTPMethod": "*", + "Host": "*", + "Priority": 10000, + "ReservoirSize": 60, + "ResourceARN": "XYZ", + "RuleARN": "arn:aws:xray:us-west-2:xxxxxxx:sampling-rule/r1", + "RuleName": "r1", + "ServiceType": "*", + "URLPath": "*", + "Version": 1 + } + } + ] +}`) + + // generate a test server so we can capture and inspect the request + testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { + _, err := res.Write([]byte(body)) + require.NoError(t, err) + + })) + defer testServer.Close() + + u, err := url.Parse(testServer.URL) + require.NoError(t, err) + + client, err := newClient(u.Host) + require.NoError(t, err) + + m := &Manifest{ + Rules: []Rule{}, + xrayClient: client, + Clock: &util.DefaultClock{}, + } + + err = m.RefreshManifestRules(ctx) + require.NoError(t, err) + + // assert on rule gets added + assert.Equal(t, 1, len(m.Rules)) +} + +// assert that rule with no RuleName does not update to the manifest +func TestRefreshManifestMissingRuleName(t *testing.T) { + ctx := context.Background() + + // rule with no RuleName + body := []byte(`{ + "NextToken": null, + "SamplingRuleRecords": [ + { + "CreatedAt": 0, + "ModifiedAt": 1639517389, + "SamplingRule": { + "Attributes": {}, + "FixedRate": 0.5, + "HTTPMethod": "*", + "Host": "*", + "Priority": 10000, + "ReservoirSize": 60, + "ResourceARN": "XYZ", + "RuleARN": "arn:aws:xray:us-west-2:xxxxxxx:sampling-rule/r1", + "ServiceName": "test", + "URLPath": "*", + "Version": 1 + } + } + ] +}`) + + // generate a test server so we can capture and inspect the request + testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { + _, err := res.Write([]byte(body)) + require.NoError(t, err) + + })) + defer testServer.Close() + + u, err := url.Parse(testServer.URL) + require.NoError(t, err) + + client, err := newClient(u.Host) + require.NoError(t, err) + + m := &Manifest{ + Rules: []Rule{}, + xrayClient: client, + Clock: &util.DefaultClock{}, + logger: stdr.NewWithOptions(log.New(os.Stderr, "", log.LstdFlags|log.Lshortfile), stdr.Options{LogCaller: stdr.Error}), + } + + err = m.RefreshManifestRules(ctx) + require.NoError(t, err) + + // assert on rule not added + assert.Equal(t, 0, len(m.Rules)) +} + +// assert that rule with version greater than one does not update to the manifest +func TestRefreshManifestIncorrectVersion(t *testing.T) { + ctx := context.Background() + + // rule with Version 5 + body := []byte(`{ + "NextToken": null, + "SamplingRuleRecords": [ + { + "CreatedAt": 0, + "ModifiedAt": 1639517389, + "SamplingRule": { + "Attributes": {}, + "FixedRate": 0.5, + "HTTPMethod": "*", + "Host": "*", + "Priority": 10000, + "ReservoirSize": 60, + "ResourceARN": "XYZ", + "RuleARN": "arn:aws:xray:us-west-2:xxxxxxx:sampling-rule/r1", + "RuleName": "r1", + "ServiceName": "test", + "ServiceType": "*", + "URLPath": "*", + "Version": 5 + } + } + ] +}`) + + // generate a test server so we can capture and inspect the request + testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { + _, err := res.Write([]byte(body)) + require.NoError(t, err) + + })) + defer testServer.Close() + + u, err := url.Parse(testServer.URL) + require.NoError(t, err) + + client, err := newClient(u.Host) + require.NoError(t, err) + + m := &Manifest{ + Rules: []Rule{}, + xrayClient: client, + Clock: &util.DefaultClock{}, + logger: stdr.NewWithOptions(log.New(os.Stderr, "", log.LstdFlags|log.Lshortfile), stdr.Options{LogCaller: stdr.Error}), + } + + err = m.RefreshManifestRules(ctx) + require.NoError(t, err) + + // assert on rule not added + assert.Equal(t, 0, len(m.Rules)) +} + +// assert that 1 valid and 1 invalid rule update only valid rule gets stored to the manifest +func TestRefreshManifestAddOneInvalidRule(t *testing.T) { + ctx := context.Background() + + // RuleName is missing from r2 + body := []byte(`{ + "NextToken": null, + "SamplingRuleRecords": [ + { + "CreatedAt": 0, + "ModifiedAt": 1639517389, + "SamplingRule": { + "Attributes": {}, + "FixedRate": 0.5, + "HTTPMethod": "*", + "Host": "*", + "Priority": 10000, + "ReservoirSize": 60, + "ResourceARN": "*", + "RuleARN": "arn:aws:xray:us-west-2:xxxxxxx:sampling-rule/r1", + "RuleName": "r1", + "ServiceName": "*", + "ServiceType": "*", + "URLPath": "*", + "Version": 1 + } + }, + { + "CreatedAt": 0, + "ModifiedAt": 1639517389, + "SamplingRule": { + "Attributes": {"a":"b"}, + "FixedRate": 0.5, + "HTTPMethod": "*", + "Host": "*", + "Priority": 10000, + "ReservoirSize": 60, + "ResourceARN": "*", + "RuleARN": "arn:aws:xray:us-west-2:xxxxxxx:sampling-rule/r2", + "ServiceName": "*", + "ServiceType": "*", + "URLPath": "*", + "Version": 1 + } + } + ] +}`) + + r1 := Rule{ + ruleProperties: ruleProperties{ + RuleName: "r1", + Priority: 10000, + Host: "*", + HTTPMethod: "*", + URLPath: "*", + ReservoirSize: 60, + FixedRate: 0.5, + Version: 1, + ServiceName: "*", + ResourceARN: "*", + ServiceType: "*", + Attributes: map[string]string{}, + }, + reservoir: reservoir{ + interval: defaultInterval, + capacity: int64(60), + }, + } + + // generate a test server so we can capture and inspect the request + testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { + _, err := res.Write([]byte(body)) + require.NoError(t, err) + })) + defer testServer.Close() + + u, err := url.Parse(testServer.URL) + require.NoError(t, err) + + client, err := newClient(u.Host) + require.NoError(t, err) + + m := &Manifest{ + Rules: []Rule{}, + xrayClient: client, + Clock: &util.DefaultClock{}, + logger: stdr.NewWithOptions(log.New(os.Stderr, "", log.LstdFlags|log.Lshortfile), stdr.Options{LogCaller: stdr.Error}), + } + + err = m.RefreshManifestRules(ctx) + require.NoError(t, err) + + assert.Equal(t, 1, len(m.Rules)) + + // assert on r1 + assert.Equal(t, r1, m.Rules[0]) +} + +// assert that a valid sampling target updates its rule +func TestUpdateTargets(t *testing.T) { + clock := &util.MockClock{ + NowTime: 1500000000, + } + + // sampling target received from centralized sampling backend + rate := 0.05 + quota := int64(10) + ttl := float64(1500000060) + name := "r1" + + st := samplingTargetDocument{ + FixedRate: &rate, + ReservoirQuota: "a, + ReservoirQuotaTTL: &ttl, + RuleName: &name, + } + + targets := &getSamplingTargetsOutput{ + SamplingTargetDocuments: []*samplingTargetDocument{&st}, + } + + // sampling rule about to be updated with new target + r1 := Rule{ + ruleProperties: ruleProperties{ + RuleName: "r1", + FixedRate: 0.10, + }, + reservoir: reservoir{ + quota: 8, + refreshedAt: 1499999990, + expiresAt: 1500000010, + capacity: 50, + used: 7, + currentEpoch: 1500000000, + }, + } + + rules := []Rule{r1} + + m := &Manifest{ + Rules: rules, + Clock: clock, + } + + refresh, err := m.updateTargets(targets) + require.NoError(t, err) + + // assert refresh is false + assert.False(t, refresh) + + // Updated sampling rule + exp := Rule{ + ruleProperties: ruleProperties{ + RuleName: "r1", + FixedRate: 0.05, + }, + reservoir: reservoir{ + quota: 10, + refreshedAt: 1500000000, + expiresAt: 1500000060, + capacity: 50, + used: 7, + currentEpoch: 1500000000, + }, + } + + // assert that updated the rule targets of rule r1 + assert.Equal(t, exp, m.Rules[0]) +} + +// assert that when last rule modification time is greater than manifest refresh time we need to update manifest +// out of band (async) +func TestUpdateTargetsRefreshFlagTest(t *testing.T) { + clock := &util.MockClock{ + NowTime: 1500000000, + } + + // sampling target received from centralized sampling backend + rate := 0.05 + quota := int64(10) + ttl := float64(1500000060) + name := "r1" + + st := samplingTargetDocument{ + FixedRate: &rate, + ReservoirQuota: "a, + ReservoirQuotaTTL: &ttl, + RuleName: &name, + } + + targetLastRuleModifiedTime := float64(1500000020) + targets := &getSamplingTargetsOutput{ + SamplingTargetDocuments: []*samplingTargetDocument{&st}, + LastRuleModification: &targetLastRuleModifiedTime, + } + + // sampling rule about to be updated with new target + r1 := Rule{ + ruleProperties: ruleProperties{ + RuleName: "r1", + FixedRate: 0.10, + }, + reservoir: reservoir{ + quota: 8, + refreshedAt: 1499999990, + expiresAt: 1500000010, + capacity: 50, + used: 7, + currentEpoch: 1500000000, + }, + } + + rules := []Rule{r1} + + m := &Manifest{ + Rules: rules, + refreshedAt: clock.Now().Unix(), + Clock: clock, + } + + refresh, err := m.updateTargets(targets) + require.NoError(t, err) + + // assert refresh is false + assert.True(t, refresh) + + // Updated sampling rule + exp := Rule{ + ruleProperties: ruleProperties{ + RuleName: "r1", + FixedRate: 0.05, + }, + reservoir: reservoir{ + quota: 10, + refreshedAt: 1500000000, + expiresAt: 1500000060, + capacity: 50, + used: 7, + currentEpoch: 1500000000, + }, + } + + // assert that updated the rule targets of rule r1 + assert.Equal(t, exp, m.Rules[0]) +} + +// unprocessed statistics error code is 5xx then updateTargets returns an error, if 4xx refresh flag set to true +func TestUpdateTargetsUnprocessedStatistics(t *testing.T) { + clock := &util.MockClock{ + NowTime: 1500000000, + } + + // sampling target received from centralized sampling backend + rate := 0.05 + quota := int64(10) + ttl := float64(1500000060) + name := "r1" + + st := samplingTargetDocument{ + FixedRate: &rate, + ReservoirQuota: "a, + ReservoirQuotaTTL: &ttl, + RuleName: &name, + } + + errorCode500 := "500" + unprocessedStats5xx := unprocessedStatistic{ + ErrorCode: &errorCode500, + RuleName: &name, + } + + errorCode400 := "400" + unprocessedStats4xx := unprocessedStatistic{ + ErrorCode: &errorCode400, + RuleName: &name, + } + + targets := &getSamplingTargetsOutput{ + SamplingTargetDocuments: []*samplingTargetDocument{&st}, + UnprocessedStatistics: []*unprocessedStatistic{&unprocessedStats5xx, &unprocessedStats4xx}, + } + + + m := &Manifest{ + Clock: clock, + logger: stdr.NewWithOptions(log.New(os.Stderr, "", log.LstdFlags|log.Lshortfile), stdr.Options{LogCaller: stdr.Error}), + + } + + refresh, err := m.updateTargets(targets) + // assert error happened since unprocessed stats has 5xx error code + require.Error(t, err) + + // assert refresh is false + assert.False(t, refresh) +} + +// assert that a missing sampling rule in manifest does not update it's reservoir values +func TestUpdateReservoir(t *testing.T) { + // Sampling target received from centralized sampling backend + rate := 0.05 + quota := int64(10) + ttl := float64(1500000060) + name := "r1" + st := &samplingTargetDocument{ + FixedRate: &rate, + ReservoirQuota: "a, + ReservoirQuotaTTL: &ttl, + RuleName: &name, + } + + // manifest only has rule r2 but not rule with r1 which targets just received + r1 := Rule{ + ruleProperties: ruleProperties{ + RuleName: "r2", + FixedRate: 0.10, + }, + reservoir: reservoir{ + quota: 8, + refreshedAt: 1499999990, + expiresAt: 1500000010, + capacity: 50, + used: 7, + currentEpoch: 1500000000, + }, + } + + rules := []Rule{r1} + + m := &Manifest{ + Rules: rules, + } + + err := m.updateReservoir(st) + require.NoError(t, err) + + // assert that rule reservoir value does not get updated and still same as r1 + assert.Equal(t, m.Rules[0], r1) +} + +// assert that a sampling target with missing Fixed Rate returns an error +func TestUpdateReservoirMissingFixedRate(t *testing.T) { + // Sampling target received from centralized sampling backend + quota := int64(10) + ttl := float64(1500000060) + name := "r1" + st := &samplingTargetDocument{ + ReservoirQuota: "a, + ReservoirQuotaTTL: &ttl, + RuleName: &name, + } + + // manifest rule which we're trying to update with above target st + r1 := Rule{ + ruleProperties: ruleProperties{ + RuleName: "r2", + FixedRate: 0.10, + }, + reservoir: reservoir{ + quota: 8, + refreshedAt: 1499999990, + expiresAt: 1500000010, + capacity: 50, + used: 7, + currentEpoch: 1500000000, + }, + } + + rules := []Rule{r1} + + m := &Manifest{ + Rules: rules, + } + + err := m.updateReservoir(st) + require.Error(t, err) +} + +// assert that a sampling target with missing Rule Name returns an error +func TestUpdateReservoirMissingRuleName(t *testing.T) { + // Sampling target received from centralized sampling backend + rate := 0.05 + quota := int64(10) + ttl := float64(1500000060) + st := &samplingTargetDocument{ + ReservoirQuota: "a, + ReservoirQuotaTTL: &ttl, + FixedRate: &rate, + } + + // manifest rule which we're trying to update with above target st + r1 := Rule{ + ruleProperties: ruleProperties{ + RuleName: "r2", + FixedRate: 0.10, + }, + reservoir: reservoir{ + quota: 8, + refreshedAt: 1499999990, + expiresAt: 1500000010, + capacity: 50, + used: 7, + currentEpoch: 1500000000, + }, + } + + rules := []Rule{r1} + + m := &Manifest{ + Rules: rules, + } + + err := m.updateReservoir(st) + require.Error(t, err) +} + +// assert that snapshots returns an array of valid sampling statistics +func TestSnapshots(t *testing.T) { + clock := &util.MockClock{ + NowTime: 1500000000, + } + + time := clock.Now().Unix() + + name1 := "r1" + requests1 := int64(1000) + sampled1 := int64(100) + borrowed1 := int64(5) + r1 := Rule{ + ruleProperties: ruleProperties{ + RuleName: name1, + }, + reservoir: reservoir{ + interval: 10, + }, + matchedRequests: requests1, + sampledRequests: sampled1, + borrowedRequests: borrowed1, + } + + name2 := "r2" + requests2 := int64(500) + sampled2 := int64(10) + borrowed2 := int64(0) + r2 := Rule{ + ruleProperties: ruleProperties{ + RuleName: name2, + }, + reservoir: reservoir{ + interval: 10, + }, + matchedRequests: requests2, + sampledRequests: sampled2, + borrowedRequests: borrowed2, + } + + rules := []Rule{r1, r2} + + id := "c1" + m := &Manifest{ + Rules: rules, + clientID: &id, + Clock: clock, + } + + // Expected SamplingStatistics structs + ss1 := samplingStatisticsDocument{ + ClientID: &id, + RequestCount: &requests1, + RuleName: &name1, + SampledCount: &sampled1, + BorrowCount: &borrowed1, + Timestamp: &time, + } + + ss2 := samplingStatisticsDocument{ + ClientID: &id, + RequestCount: &requests2, + RuleName: &name2, + SampledCount: &sampled2, + BorrowCount: &borrowed2, + Timestamp: &time, + } + + statistics, err := m.snapshots() + require.NoError(t, err) + + // match time + *statistics[0].Timestamp = 1500000000 + *statistics[1].Timestamp = 1500000000 + + assert.Equal(t, ss1, *statistics[0]) + assert.Equal(t, ss2, *statistics[1]) +} + +// assert that fresh and inactive rules are not included in a snapshot +func TestMixedSnapshots(t *testing.T) { + clock := &util.MockClock{ + NowTime: 1500000000, + } + + id := "c1" + time := clock.Now().Unix() + + // stale and active rule + name1 := "r1" + requests1 := int64(1000) + sampled1 := int64(100) + borrows1 := int64(5) + + r1 := Rule{ + ruleProperties: ruleProperties{ + RuleName: name1, + }, + reservoir: reservoir{ + interval: 20, + refreshedAt: 1499999980, + }, + matchedRequests: requests1, + sampledRequests: sampled1, + borrowedRequests: borrows1, + } + + // fresh and inactive rule + name2 := "r2" + requests2 := int64(0) + sampled2 := int64(0) + borrows2 := int64(0) + + r2 := Rule{ + ruleProperties: ruleProperties{ + RuleName: name2, + }, + reservoir: reservoir{ + interval: 20, + refreshedAt: 1499999990, + }, + matchedRequests: requests2, + sampledRequests: sampled2, + borrowedRequests: borrows2, + } + + // fresh rule + name3 := "r3" + requests3 := int64(1000) + sampled3 := int64(100) + borrows3 := int64(5) + + r3 := Rule{ + ruleProperties: ruleProperties{ + RuleName: name3, + }, + reservoir: reservoir{ + interval: 20, + refreshedAt: 1499999990, + }, + matchedRequests: requests3, + sampledRequests: sampled3, + borrowedRequests: borrows3, + } + + rules := []Rule{r1, r2, r3} + + m := &Manifest{ + clientID: &id, + Clock: clock, + Rules: rules, + } + + ss1 := samplingStatisticsDocument{ + ClientID: &id, + RequestCount: &requests1, + RuleName: &name1, + SampledCount: &sampled1, + BorrowCount: &borrows1, + Timestamp: &time, + } + + statistics, err := m.snapshots() + require.NoError(t, err) + + // match time + *statistics[0].Timestamp = 1500000000 + + // assert that only inactive rules are added to the statistics + assert.Equal(t, 1, len(statistics)) + assert.Equal(t, ss1, *statistics[0]) +} + +// Assert that sorting an unsorted array results in a sorted array - check priority +func TestSortBasedOnPriority(t *testing.T) { + r1 := Rule{ + ruleProperties: ruleProperties{ + RuleName: "r1", + Priority: 5, + }, + } + + r2 := Rule{ + ruleProperties: ruleProperties{ + RuleName: "r2", + Priority: 6, + }, + } + + r3 := Rule{ + ruleProperties: ruleProperties{ + RuleName: "r3", + Priority: 7, + }, + } + + // Unsorted rules array + rules := []Rule{r2, r1, r3} + + m := &Manifest{ + Rules: rules, + } + + // Sort array + m.sort() + + // Assert on order + assert.Equal(t, r1, m.Rules[0]) + assert.Equal(t, r2, m.Rules[1]) + assert.Equal(t, r3, m.Rules[2]) +} + +// Assert that sorting an unsorted array results in a sorted array - check priority and rule name +func TestSortBasedOnRuleName(t *testing.T) { + r1 := Rule{ + ruleProperties: ruleProperties{ + RuleName: "r1", + Priority: 5, + }, + } + + r2 := Rule{ + ruleProperties: ruleProperties{ + RuleName: "r2", + Priority: 5, + }, + } + + r3 := Rule{ + ruleProperties: ruleProperties{ + RuleName: "r3", + Priority: 7, + }, + } + + // Unsorted rules array + rules := []Rule{r2, r1, r3} + + m := &Manifest{ + Rules: rules, + } + + // Sort array + m.sort() + + // Assert on order + assert.Equal(t, r1, m.Rules[0]) + assert.Equal(t, r2, m.Rules[1]) + assert.Equal(t, r3, m.Rules[2]) +} + +// asserts the minimum value of all the targets +func TestMinPollInterval(t *testing.T) { + interval1 := int64(10) + t1 := &samplingTargetDocument{ + Interval: &interval1, + } + + interval2 := int64(15) + t2 := &samplingTargetDocument{ + Interval: &interval2, + } + + interval3 := int64(25) + t3 := &samplingTargetDocument{ + Interval: &interval3, + } + + targets := &getSamplingTargetsOutput{ + SamplingTargetDocuments: []*samplingTargetDocument{t1, t2, t3}, + } + + m := &Manifest{} + + minPoll := m.minimumPollingInterval(targets) + + assert.Equal(t, int64(10), minPoll) +} + +// asserts the minimum value of all the targets when some targets has 0 interval +func TestMinPollIntervalZeroCase(t *testing.T) { + interval1 := int64(0) + t1 := &samplingTargetDocument{ + Interval: &interval1, + } + + interval2 := int64(0) + t2 := &samplingTargetDocument{ + Interval: &interval2, + } + + interval3 := int64(5) + t3 := &samplingTargetDocument{ + Interval: &interval3, + } + + targets := &getSamplingTargetsOutput{ + SamplingTargetDocuments: []*samplingTargetDocument{t1, t2, t3}, + } + + m := &Manifest{} + + minPoll := m.minimumPollingInterval(targets) + + assert.Equal(t, int64(5), minPoll) +} + +// asserts the minimum value of all the targets when some targets has negative interval +func TestMinPollIntervalNegativeCase(t *testing.T) { + interval1 := int64(-5) + t1 := &samplingTargetDocument{ + Interval: &interval1, + } + + interval2 := int64(0) + t2 := &samplingTargetDocument{ + Interval: &interval2, + } + + interval3 := int64(0) + t3 := &samplingTargetDocument{ + Interval: &interval3, + } + + targets := &getSamplingTargetsOutput{ + SamplingTargetDocuments: []*samplingTargetDocument{t1, t2, t3}, + } + + m := &Manifest{} + + minPoll := m.minimumPollingInterval(targets) + + assert.Equal(t, int64(-5), minPoll) +} + +// assert that able to successfully generate the client ID +func TestGenerateClientID(t *testing.T) { + clientID, err := generateClientId() + require.NoError(t, err) + assert.NotEmpty(t, clientID) +} diff --git a/samplers/aws/xray/internal/match_test.go b/samplers/aws/xray/internal/match_test.go new file mode 100644 index 00000000000..f57d333c4f5 --- /dev/null +++ b/samplers/aws/xray/internal/match_test.go @@ -0,0 +1,180 @@ +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package internal + +import ( + "bytes" + "math/rand" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestInvalidArgs(t *testing.T) { + assert.False(t, wildcardMatch("", "whatever")) +} + +func TestInvalidArgs1(t *testing.T) { + assert.True(t, wildcardMatch("*", "")) +} + +func TestMatchExactPositive(t *testing.T) { + assert.True(t, wildcardMatch("foo", "foo")) +} + +func TestMatchExactNegative(t *testing.T) { + assert.False(t, wildcardMatch("foo", "bar")) +} + +func TestSingleWildcardPositive(t *testing.T) { + assert.True(t, wildcardMatch("fo?", "foo")) +} + +func TestSingleWildcardNegative(t *testing.T) { + assert.False(t, wildcardMatch("f?o", "boo")) +} + +func TestMultipleWildcardPositive(t *testing.T) { + assert.True(t, wildcardMatch("?o?", "foo")) +} + +func TestMultipleWildcardNegative(t *testing.T) { + assert.False(t, wildcardMatch("f??", "boo")) +} + +func TestGlobPositive(t *testing.T) { + assert.True(t, wildcardMatch("*oo", "foo")) +} + +func TestGlobPositiveZeroOrMore(t *testing.T) { + assert.True(t, wildcardMatch("foo*", "foo")) +} + +func TestGlobNegativeZeroOrMore(t *testing.T) { + assert.False(t, wildcardMatch("foo*", "fo0")) +} + +func TestGlobNegative(t *testing.T) { + assert.False(t, wildcardMatch("fo*", "boo")) +} + +func TestGlobAndSinglePositive(t *testing.T) { + assert.True(t, wildcardMatch("*o?", "foo")) +} + +func TestGlobAndSingleNegative(t *testing.T) { + assert.False(t, wildcardMatch("f?*", "boo")) +} + +func TestPureWildcard(t *testing.T) { + assert.True(t, wildcardMatch("*", "boo")) +} + +func TestMisc(t *testing.T) { + animal1 := "?at" + animal2 := "?o?se" + animal3 := "*s" + + vehicle1 := "J*" + vehicle2 := "????" + + assert.True(t, wildcardMatch(animal1, "bat")) + assert.True(t, wildcardMatch(animal1, "cat")) + assert.True(t, wildcardMatch(animal2, "horse")) + assert.True(t, wildcardMatch(animal2, "mouse")) + assert.True(t, wildcardMatch(animal3, "dogs")) + assert.True(t, wildcardMatch(animal3, "horses")) + + assert.True(t, wildcardMatch(vehicle1, "Jeep")) + assert.True(t, wildcardMatch(vehicle2, "ford")) + assert.False(t, wildcardMatch(vehicle2, "chevy")) + assert.True(t, wildcardMatch("*", "cAr")) + + assert.True(t, wildcardMatch("*/foo", "/bar/foo")) +} + +func TestCaseInsensitivity(t *testing.T) { + assert.True(t, wildcardMatch("Foo", "Foo")) + assert.True(t, wildcardMatch("Foo", "FOO")) + assert.True(t, wildcardMatch("Fo*", "Foo0")) + assert.True(t, wildcardMatch("Fo*", "FOO0")) + assert.True(t, wildcardMatch("Fo?", "Foo")) + assert.True(t, wildcardMatch("Fo?", "FOo")) + assert.True(t, wildcardMatch("Fo?", "FoO")) + assert.True(t, wildcardMatch("Fo?", "FOO")) +} + +func TestLongStrings(t *testing.T) { + chars := []byte{'a', 'b', 'c', 'd'} + text := bytes.NewBufferString("a") + for i := 0; i < 8192; i++ { + text.WriteString(string(chars[rand.Intn(len(chars))])) + } + text.WriteString("b") + + assert.True(t, wildcardMatch("a*b", text.String())) +} + +func TestNoGlobs(t *testing.T) { + assert.False(t, wildcardMatch("abcd", "abc")) +} + +func TestEdgeCaseGlobs(t *testing.T) { + assert.True(t, wildcardMatch("", "")) + assert.True(t, wildcardMatch("a", "a")) + assert.True(t, wildcardMatch("*a", "a")) + assert.True(t, wildcardMatch("*a", "ba")) + assert.True(t, wildcardMatch("a*", "a")) + assert.True(t, wildcardMatch("a*", "ab")) + assert.True(t, wildcardMatch("a*a", "aa")) + assert.True(t, wildcardMatch("a*a", "aba")) + assert.True(t, wildcardMatch("a*a", "aaa")) + assert.True(t, wildcardMatch("a*a*", "aa")) + assert.True(t, wildcardMatch("a*a*", "aba")) + assert.True(t, wildcardMatch("a*a*", "aaa")) + assert.True(t, wildcardMatch("a*a*", "aaaaaaaaaaaaaaaaaaaaaaa")) + assert.True(t, wildcardMatch("a*b*a*b*a*b*a*b*a*", + "akljd9gsdfbkjhaabajkhbbyiaahkjbjhbuykjakjhabkjhbabjhkaabbabbaaakljdfsjklababkjbsdabab")) + assert.False(t, wildcardMatch("a*na*ha", "anananahahanahana")) +} + +func TestMultiGlobs(t *testing.T) { + assert.True(t, wildcardMatch("*a", "a")) + assert.True(t, wildcardMatch("**a", "a")) + assert.True(t, wildcardMatch("***a", "a")) + assert.True(t, wildcardMatch("**a*", "a")) + assert.True(t, wildcardMatch("**a**", "a")) + + assert.True(t, wildcardMatch("a**b", "ab")) + assert.True(t, wildcardMatch("a**b", "abb")) + + assert.True(t, wildcardMatch("*?", "a")) + assert.True(t, wildcardMatch("*?", "aa")) + assert.True(t, wildcardMatch("*??", "aa")) + assert.False(t, wildcardMatch("*???", "aa")) + assert.True(t, wildcardMatch("*?", "aaa")) + + assert.True(t, wildcardMatch("?", "a")) + assert.False(t, wildcardMatch("??", "a")) + + assert.True(t, wildcardMatch("?*", "a")) + assert.True(t, wildcardMatch("*?", "a")) + assert.False(t, wildcardMatch("?*?", "a")) + assert.True(t, wildcardMatch("?*?", "aa")) + assert.True(t, wildcardMatch("*?*", "a")) + + assert.False(t, wildcardMatch("*?*a", "a")) + assert.True(t, wildcardMatch("*?*a*", "ba")) +} \ No newline at end of file diff --git a/samplers/aws/xray/internal/reservoir.go b/samplers/aws/xray/internal/reservoir.go index 0d6038531cc..77bdd8a4c89 100644 --- a/samplers/aws/xray/internal/reservoir.go +++ b/samplers/aws/xray/internal/reservoir.go @@ -15,6 +15,7 @@ package internal import ( + "fmt" "sync/atomic" ) @@ -47,7 +48,7 @@ type reservoir struct { func (r *reservoir) expired(now int64) bool { expire := atomic.LoadInt64(&r.expiresAt) - return now > expire + return now >= expire } // borrow returns true if the reservoir has not been borrowed from this epoch. @@ -65,6 +66,10 @@ func (r *reservoir) take(now int64) bool { quota := atomic.LoadInt64(&r.quota) used := atomic.LoadInt64(&r.used) + fmt.Println("cur", cur) + fmt.Println("quota", quota) + fmt.Println("used", used) + if cur != now { atomic.CompareAndSwapInt64(&r.currentEpoch, cur, now) atomic.CompareAndSwapInt64(&r.used, used, int64(0)) diff --git a/samplers/aws/xray/internal/reservoir_test.go b/samplers/aws/xray/internal/reservoir_test.go new file mode 100644 index 00000000000..803e3d1c273 --- /dev/null +++ b/samplers/aws/xray/internal/reservoir_test.go @@ -0,0 +1,151 @@ +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package internal + +import ( + "go.opentelemetry.io/contrib/samplers/aws/xray/internal/util" + "testing" + + "github.com/stretchr/testify/assert" +) + +// assert that reservoir quota is expired. +func TestExpiredReservoir(t *testing.T) { + clock := &util.MockClock{ + NowTime: 1500000001, + } + + r := &reservoir{ + expiresAt: 1500000000, + } + + expired := r.expired(clock.Now().Unix()) + + assert.True(t, expired) +} + +// assert that reservoir quota is still expired since now time is equal to expiresAt time. +func TestExpiredReservoirSameAsClockTime(t *testing.T) { + clock := &util.MockClock{ + NowTime: 1500000000, + } + + r := &reservoir{ + expiresAt: 1500000000, + } + + expired := r.expired(clock.Now().Unix()) + + assert.True(t, expired) +} + +// Assert that borrow only 1 req/sec +func TestBorrowEverySecond(t *testing.T) { + clock := &util.MockClock{ + NowTime: 1500000000, + } + + r := &reservoir{ + capacity: 10, + } + + s := r.borrow(clock.Now().Unix()) + assert.True(t, s) + + s = r.borrow(clock.Now().Unix()) + assert.False(t, s) + + // Increment clock by 1 + clock = &util.MockClock{ + NowTime: 1500000001, + } + + s = r.borrow(clock.Now().Unix()) + assert.True(t, s) +} + +// assert that we can still borrowing from reservoir is possible since assigned quota is available to consume +// and it will increase used count. +func TestConsumeAvailableQuota(t *testing.T) { + clock := &util.MockClock{ + NowTime: 1500000000, + } + + r := &reservoir{ + quota: int64(9), + capacity: int64(100), + used: int64(0), + currentEpoch: clock.Now().Unix(), + } + + s := r.take(clock.Now().Unix()) + assert.True(t, s) + assert.Equal(t, int64(1), r.used) +} + +// assert that we can not borrow from reservoir since assigned quota is not available to consume +// and it will not increase used count. +func TestConsumeUnavailableQuota(t *testing.T) { + clock := &util.MockClock{ + NowTime: 1500000000, + } + + r := &reservoir{ + quota: int64(9), + capacity: int64(100), + used: int64(9), + currentEpoch: clock.Now().Unix(), + } + + s := r.take(clock.Now().Unix()) + assert.False(t, s) + assert.Equal(t, int64(9), r.used) +} + +func TestResetQuotaUsageRotation(t *testing.T) { + clock := &util.MockClock{ + NowTime: 1500000000, + } + + r := &reservoir{ + quota: int64(5), + capacity: int64(100), + used: int64(0), + currentEpoch: clock.Now().Unix(), + } + + // consume quota for second + for i := 0; i < 5; i++ { + taken := r.take(clock.Now().Unix()) + assert.Equal(t, true, taken) + assert.Equal(t, int64(i+1), r.used) + } + + // take() should be false since no unused quota left + taken := r.take(clock.Now().Unix()) + assert.Equal(t, false, taken) + assert.Equal(t, int64(5), r.used) + + // increment epoch to reset unused quota + clock = &util.MockClock{ + NowTime: 1500000001, + } + + // take() should be true since ununsed quota is available + taken = r.take(clock.Now().Unix()) + assert.Equal(t, int64(1500000001), r.currentEpoch) + assert.Equal(t, true, taken) + assert.Equal(t, int64(1), r.used) +} \ No newline at end of file diff --git a/samplers/aws/xray/internal/rule.go b/samplers/aws/xray/internal/rule.go index b25329cb333..453b1b03c8b 100644 --- a/samplers/aws/xray/internal/rule.go +++ b/samplers/aws/xray/internal/rule.go @@ -26,6 +26,8 @@ type Rule struct { // number of requests borrowed using specific rule borrowedRequests int64 + + clock util.Clock } // stale checks if targets (sampling stats) for a given rule is expired or not @@ -35,10 +37,7 @@ func (r *Rule) stale(now int64) bool { // snapshot takes a snapshot of the sampling statistics counters, returning // samplingStatisticsDocument. It also resets statistics counters. -func (r *Rule) snapshot() *samplingStatisticsDocument { - clock := &util.DefaultClock{} - now := clock.Now().Unix() - +func (r *Rule) snapshot(now int64) *samplingStatisticsDocument { name := r.ruleProperties.RuleName requests, sampled, borrowed := r.matchedRequests, r.sampledRequests, r.borrowedRequests @@ -55,14 +54,11 @@ func (r *Rule) snapshot() *samplingStatisticsDocument { } // Sample uses sampling targets of a given rule to decide which sampling should be done and returns a SamplingResult. -func (r *Rule) Sample(parameters sdktrace.SamplingParameters) sdktrace.SamplingResult { +func (r *Rule) Sample(parameters sdktrace.SamplingParameters, now int64) sdktrace.SamplingResult { sd := sdktrace.SamplingResult{ Tracestate: trace.SpanContextFromContext(parameters.ParentContext).TraceState(), } - clock := &util.DefaultClock{} - now := clock.Now().Unix() - atomic.AddInt64(&r.matchedRequests, int64(1)) // fallback sampling logic if quota for a given rule is expired @@ -142,9 +138,13 @@ func (r *Rule) appliesTo(parameters sdktrace.SamplingParameters, serviceName str func (r *Rule) attributeMatching(parameters sdktrace.SamplingParameters) bool { match := false if len(r.ruleProperties.Attributes) > 0 { - for _, attrs := range parameters.Attributes { - if r.ruleProperties.Attributes[string(attrs.Key)] != "" { - match = wildcardMatch(r.ruleProperties.Attributes[string(attrs.Key)], attrs.Value.AsString()) + for key, value := range r.ruleProperties.Attributes { + for _, attrs := range parameters.Attributes { + if key == string(attrs.Key) { + match = wildcardMatch(value, attrs.Value.AsString()) + } else { + match = false + } } } return match diff --git a/samplers/aws/xray/internal/rule_test.go b/samplers/aws/xray/internal/rule_test.go new file mode 100644 index 00000000000..0442fcd3391 --- /dev/null +++ b/samplers/aws/xray/internal/rule_test.go @@ -0,0 +1,395 @@ +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package internal + +import ( + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/sdk/trace" + "testing" + + "github.com/stretchr/testify/assert" +) + +// assert that rule is active but stale due to quota is expired. +func TestStaleRule(t *testing.T) { + r1 := Rule{ + matchedRequests: 5, + reservoir: reservoir{ + refreshedAt: 1500000000, + interval: 10, + }, + } + + s := r1.stale(1500000010) + assert.True(t, s) +} + +// assert that rule is active and not stale. +func TestFreshRule(t *testing.T) { + r1 := Rule{ + matchedRequests: 5, + reservoir: reservoir{ + refreshedAt: 1500000000, + interval: 10, + }, + } + + s := r1.stale(1500000009) + assert.False(t, s) +} + +// assert that rule is inactive but not stale. +func TestInactiveRule(t *testing.T) { + r1 := Rule{ + matchedRequests: 0, + reservoir: reservoir{ + refreshedAt: 1500000000, + interval: 10, + }, + } + + s := r1.stale(1500000011) + assert.False(t, s) +} + +// assert on snapshot of sampling statistics counters. +func TestSnapshot(t *testing.T) { + r1 := Rule{ + ruleProperties: ruleProperties{ + RuleName: "r1", + }, + matchedRequests: 100, + sampledRequests: 12, + borrowedRequests: 2, + } + + ss := r1.snapshot(1500000000) + + // assert counters were reset + assert.Equal(t, int64(0), r1.matchedRequests) + assert.Equal(t, int64(0), r1.sampledRequests) + assert.Equal(t, int64(0), r1.borrowedRequests) + + // assert on SamplingStatistics counters + assert.Equal(t, int64(100), *ss.RequestCount) + assert.Equal(t, int64(12), *ss.SampledCount) + assert.Equal(t, int64(2), *ss.BorrowCount) + assert.Equal(t, "r1", *ss.RuleName) +} + +// assert that reservoir is expired, borrowing 1 req during that second. +func TestExpiredReservoirBorrowSample(t *testing.T) { + r1 := Rule{ + reservoir: reservoir{ + expiresAt: 1500000060, + used: 0, + capacity: 10, + currentEpoch: 1500000061, + }, + ruleProperties: ruleProperties{ + RuleName: "r1", + FixedRate: 0.06, + }, + } + + sd := r1.Sample(trace.SamplingParameters{}, 1500000062) + + assert.Equal(t, trace.RecordAndSample, sd.Decision) + assert.Equal(t, int64(1), r1.borrowedRequests) + assert.Equal(t, int64(0), r1.sampledRequests) + assert.Equal(t, int64(1), r1.matchedRequests) + assert.Equal(t, int64(0), r1.reservoir.used) +} + +// assert that reservoir is expired, borrowed 1 req during that second so now using traceIDRatioBased sampler. +func TestExpiredReservoirTraceIDRationBasedSample(t *testing.T) { + r1 := Rule{ + reservoir: reservoir{ + expiresAt: 1500000060, + used: 0, + capacity: 10, + currentEpoch: 1500000061, + }, + ruleProperties: ruleProperties{ + RuleName: "r1", + FixedRate: 0.06, + }, + } + + r1.Sample(trace.SamplingParameters{}, 1500000061) + + assert.Equal(t, int64(0), r1.borrowedRequests) + assert.Equal(t, int64(1), r1.sampledRequests) + assert.Equal(t, int64(1), r1.matchedRequests) + assert.Equal(t, int64(0), r1.reservoir.used) +} + +// assert that reservoir is not expired, quota is available so consuming from quota. +func TestConsumeFromQuotaSample(t *testing.T) { + r1 := Rule{ + ruleProperties: ruleProperties{ + RuleName: "r1", + }, + reservoir: reservoir{ + quota: 10, + expiresAt: 1500000060, + currentEpoch: 1500000000, + used: 0, + }, + } + + sd := r1.Sample(trace.SamplingParameters{}, 1500000000) + + assert.Equal(t, trace.RecordAndSample, sd.Decision) + assert.Equal(t, int64(1), r1.sampledRequests) + assert.Equal(t, int64(0), r1.borrowedRequests) + assert.Equal(t, int64(1), r1.matchedRequests) + assert.Equal(t, int64(1), r1.reservoir.used) +} + +// assert that sampling using traceIDRationBasedSampler. +func TestTraceIDRatioBasedSampler(t *testing.T) { + r1 := Rule{ + reservoir: reservoir{ + quota: 10, + expiresAt: 1500000060, + currentEpoch: 1500000000, + used: 10, + }, + ruleProperties: ruleProperties{ + FixedRate: 0.05, + RuleName: "r1", + }, + } + + sd := r1.Sample(trace.SamplingParameters{}, 1500000000) + + assert.NotEmpty(t, sd.Decision) + assert.Equal(t, int64(1), r1.sampledRequests) + assert.Equal(t, int64(0), r1.borrowedRequests) + assert.Equal(t, int64(1), r1.matchedRequests) + assert.Equal(t, int64(10), r1.reservoir.used) +} + +// assert that when fixed rate is 0 traceIDRatioBased sampler will not sample the trace. +func TestTraceIDRatioBasedSamplerFixedRateZero(t *testing.T) { + r1 := Rule{ + reservoir: reservoir{ + quota: 10, + expiresAt: 1500000060, + currentEpoch: 1500000000, + used: 10, + }, + ruleProperties: ruleProperties{ + FixedRate: 0, + RuleName: "r1", + }, + } + + sd := r1.Sample(trace.SamplingParameters{}, 1500000000) + + assert.Equal(t, sd.Decision, trace.Drop) +} + +func TestAppliesToMatchingWithAllAttrs(t *testing.T) { + r1 := Rule{ + ruleProperties: ruleProperties{ + RuleName: "r1", + ServiceName: "test-service", + ServiceType: "EC2", + Host: "localhost", + HTTPMethod: "GET", + URLPath: "http://127.0.0.1:2000", + }, + } + + httpAttrs := []attribute.KeyValue{ + attribute.String("http.host", "localhost"), + attribute.String("http.method", "GET"), + attribute.String("http.url", "http://127.0.0.1:2000"), + } + + assert.True(t, r1.appliesTo(trace.SamplingParameters{Attributes: httpAttrs}, "test-service", "EC2")) +} + +// assert that matching will happen when rules has all the HTTP attrs set as '*' and +// span has any attribute values. +func TestAppliesToMatchingWithStarHTTPAttrs(t *testing.T) { + r1 := Rule{ + ruleProperties: ruleProperties{ + RuleName: "r1", + ServiceName: "test-service", + ServiceType: "EC2", + Host: "*", + HTTPMethod: "*", + URLPath: "*", + }, + } + + httpAttrs := []attribute.KeyValue{ + attribute.String("http.host", "localhost"), + attribute.String("http.method", "GET"), + attribute.String("http.url", "http://127.0.0.1:2000"), + } + + assert.True(t, r1.appliesTo(trace.SamplingParameters{Attributes: httpAttrs}, "test-service", "EC2")) +} + +// assert that matching will not happen when rules has all the HTTP attrs set as non '*' values and +// span has no HTTP attributes. +func TestAppliesToMatchingWithHTTPAttrs_NoSpanAttrs(t *testing.T) { + r1 := Rule{ + ruleProperties: ruleProperties{ + RuleName: "r1", + ServiceName: "test-service", + ServiceType: "EC2", + Host: "localhost", + HTTPMethod: "GET", + URLPath: "http://127.0.0.1:2000", + }, + } + + assert.False(t, r1.appliesTo(trace.SamplingParameters{}, "test-service", "EC2")) +} + +// assert that matching will happen when rules has all the HTTP attrs set as '*' values and +// span has no HTTP attributes. +func TestAppliesToMatchingWithStarHTTPAttrs_NoSpanAttrs(t *testing.T) { + r1 := Rule{ + ruleProperties: ruleProperties{ + RuleName: "r1", + ServiceName: "test-service", + ServiceType: "EC2", + Host: "*", + HTTPMethod: "*", + URLPath: "*", + }, + } + + assert.True(t, r1.appliesTo(trace.SamplingParameters{}, "test-service", "EC2")) +} + +// assert that matching will not happen when rules has some HTTP attrs set as non '*' values and +// span has no HTTP attributes. +func TestAppliesToMatchingWithPartialHTTPAttrs_NoSpanAttrs(t *testing.T) { + r1 := Rule{ + ruleProperties: ruleProperties{ + RuleName: "r1", + ServiceName: "test-service", + ServiceType: "EC2", + Host: "*", + HTTPMethod: "GET", + URLPath: "*", + }, + } + + assert.False(t, r1.appliesTo(trace.SamplingParameters{}, "test-service", "EC2")) +} + +// assert that matching will not happen when rule and span ServiceType attr value is different. +func TestAppliesToNoMatching(t *testing.T) { + r1 := Rule{ + ruleProperties: ruleProperties{ + RuleName: "r1", + ServiceName: "test-service", + ServiceType: "EC2", + Host: "*", + HTTPMethod: "*", + URLPath: "*", + }, + } + + assert.False(t, r1.appliesTo(trace.SamplingParameters{}, "test-service", "ECS")) +} + +// assert that if rules has attribute and span has those attribute with same value then matching will happen. +func TestAttributeMatching(t *testing.T) { + commonLabels := []attribute.KeyValue{ + attribute.String("labelA", "chocolate"), + attribute.String("labelB", "raspberry"), + } + + r1 := Rule{ + ruleProperties: ruleProperties{ + Attributes: map[string]string{ + "labelA": "chocolate", + "labelB": "raspberry", + }, + }, + } + + assert.True(t, r1.attributeMatching(trace.SamplingParameters{Attributes: commonLabels})) +} + +// assert that if some of the rules attributes are not present in span attributes then matching +// will not happen. +func TestNoAttributeMatching(t *testing.T) { + commonLabels := []attribute.KeyValue{ + attribute.String("labelA", "chocolate"), + attribute.String("labelB", "raspberry"), + } + + r1 := Rule{ + ruleProperties: ruleProperties{ + Attributes: map[string]string{ + "labelA": "chocolate", + "labelC": "fudge", + }, + }, + } + + assert.False(t, r1.attributeMatching(trace.SamplingParameters{Attributes: commonLabels})) +} + +// assert that wildcard attributes will match. +func TestAttributeWildCardMatching(t *testing.T) { + commonLabels := []attribute.KeyValue{ + attribute.String("labelA", "chocolate"), + attribute.String("labelB", "raspberry"), + } + + r1 := Rule{ + ruleProperties: ruleProperties{ + Attributes: map[string]string{ + "labelA": "choco*", + "labelB": "rasp*", + }, + }, + } + + assert.True(t, r1.attributeMatching(trace.SamplingParameters{Attributes: commonLabels})) +} + +// assert that if rules has no attributes then matching will happen. +func TestAttributeMatching_NoRuleAttrs(t *testing.T) { + commonLabels := []attribute.KeyValue{ + attribute.String("labelA", "chocolate"), + attribute.String("labelB", "raspberry"), + } + + r1 := Rule{ + ruleProperties: ruleProperties{ + Attributes: map[string]string{}, + }, + } + + assert.True(t, r1.attributeMatching(trace.SamplingParameters{Attributes: commonLabels})) +} + + + + + + diff --git a/samplers/aws/xray/internal/util/clock.go b/samplers/aws/xray/internal/util/clock.go index 65ab07c1bc3..69a626b9c29 100644 --- a/samplers/aws/xray/internal/util/clock.go +++ b/samplers/aws/xray/internal/util/clock.go @@ -15,6 +15,7 @@ package util import ( + "sync/atomic" "time" ) @@ -30,3 +31,22 @@ type DefaultClock struct{} func (t *DefaultClock) Now() time.Time { return time.Now() } + +// MockClock is a struct to record current time. +type MockClock struct { + NowTime int64 + NowNanos int64 +} + +// Now function returns NowTime value. +func (c *MockClock) Now() time.Time { + return time.Unix(c.NowTime, c.NowNanos) +} + +// Increment is a method to increase current time. +func (c *MockClock) Increment(s int64, ns int64) time.Time { + sec := atomic.AddInt64(&c.NowTime, s) + nSec := atomic.AddInt64(&c.NowNanos, ns) + + return time.Unix(sec, nSec) +} diff --git a/samplers/aws/xray/remote_sampler.go b/samplers/aws/xray/remote_sampler.go index e5f578b241f..df28b128a94 100644 --- a/samplers/aws/xray/remote_sampler.go +++ b/samplers/aws/xray/remote_sampler.go @@ -96,7 +96,7 @@ func (rs *remoteSampler) ShouldSample(parameters sdktrace.SamplingParameters) sd // match against known rules r, match := rs.manifest.MatchAgainstManifestRules(parameters, rs.serviceName, rs.cloudPlatform); if match { // remote sampling based on rule match - return r.Sample(parameters) + return r.Sample(parameters, rs.manifest.Clock.Now().Unix()) } } @@ -172,6 +172,11 @@ func (rs *remoteSampler) startPoller(ctx context.Context) { func main() { ctx := context.Background() rs, _ := NewRemoteSampler(ctx, "test", "test-platform") + // + //commonLabels := []attribute.KeyValue{ + // attribute.String("labelA", "chocolate"), + // attribute.String("labelB", "raspberry"), + //} for i := 0; i < 1000; i++ { rs.ShouldSample(sdktrace.SamplingParameters{}) diff --git a/samplers/aws/xray/remote_sampler_config_test.go b/samplers/aws/xray/remote_sampler_config_test.go new file mode 100644 index 00000000000..54cdda959fb --- /dev/null +++ b/samplers/aws/xray/remote_sampler_config_test.go @@ -0,0 +1,101 @@ +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "log" + "os" + "testing" + "time" + + "github.com/go-logr/logr" + "github.com/go-logr/stdr" + "github.com/stretchr/testify/assert" +) + +// assert that user provided values are tied to config. +func TestNewConfig(t *testing.T) { + cfg := newConfig(WithSamplingRulesPollingInterval(400*time.Second), WithEndpoint("127.0.0.1:5000"), WithLogger(logr.Logger{})) + + assert.Equal(t, cfg.samplingRulesPollingInterval, 400*time.Second) + assert.Equal(t, cfg.endpoint, "127.0.0.1:5000") + assert.Equal(t, cfg.logger, logr.Logger{}) +} + +// assert that when user did not provide values are then config would be picked up from default values. +func TestDefaultConfig(t *testing.T) { + cfg := newConfig() + + assert.Equal(t, cfg.samplingRulesPollingInterval, 300*time.Second) + assert.Equal(t, cfg.endpoint, "127.0.0.1:2000") + assert.Equal(t, cfg.logger, stdr.NewWithOptions(log.New(os.Stderr, "", log.LstdFlags|log.Lshortfile), stdr.Options{LogCaller: stdr.Error})) +} + +// assert when some config is provided by user then other config will be picked up from default config. +func TestPartialUserProvidedConfig(t *testing.T) { + cfg := newConfig(WithSamplingRulesPollingInterval(500 * time.Second)) + + assert.Equal(t, cfg.samplingRulesPollingInterval, 500*time.Second) + assert.Equal(t, cfg.endpoint, "127.0.0.1:2000") + assert.Equal(t, cfg.logger, stdr.NewWithOptions(log.New(os.Stderr, "", log.LstdFlags|log.Lshortfile), stdr.Options{LogCaller: stdr.Error})) +} + +// assert not expected endpoint format leads to an error (expected format: "127.0.0.1:2020"). +func TestValidateConfigIncorrectEndpoint(t *testing.T) { + cfg := newConfig(WithEndpoint("http://127.0.0.1:2000")) + + err := validateConfig(cfg) + assert.Error(t, err) +} + +// assert not expected endpoint format leads to an error (expected format: "127.0.0.1:2020"). +func TestValidateConfigSpecialCharacterEndpoint(t *testing.T) { + cfg := newConfig(WithEndpoint("@127.0.0.1:2000")) + + err := validateConfig(cfg) + assert.Error(t, err) +} + +// assert not expected endpoint format leads to an error (expected format: "127.0.0.1:2020"). +func TestValidateConfigLocalHost(t *testing.T) { + cfg := newConfig(WithEndpoint("localhost:2000")) + + err := validateConfig(cfg) + assert.NoError(t, err) +} + +// assert not expected endpoint format leads to an error (expected format: "127.0.0.1:2020"). +func TestValidateConfigInvalidPort(t *testing.T) { + cfg := newConfig(WithEndpoint("127.0.0.1:abcd")) + + err := validateConfig(cfg) + assert.Error(t, err) +} + +// assert negative sampling rules interval leads to an error. +func TestValidateConfigNegativeDuration(t *testing.T) { + cfg := newConfig(WithSamplingRulesPollingInterval(-300 * time.Second)) + + err := validateConfig(cfg) + assert.Error(t, err) +} + +// assert positive sampling rules interval. +func TestValidateConfigPositiveDuration(t *testing.T) { + cfg := newConfig(WithSamplingRulesPollingInterval(300 * time.Second)) + + err := validateConfig(cfg) + assert.NoError(t, err) +} \ No newline at end of file diff --git a/samplers/aws/xray/remote_sampler_test.go b/samplers/aws/xray/remote_sampler_test.go new file mode 100644 index 00000000000..913cc87613b --- /dev/null +++ b/samplers/aws/xray/remote_sampler_test.go @@ -0,0 +1,57 @@ +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "github.com/stretchr/testify/assert" + "go.opentelemetry.io/contrib/samplers/aws/xray/internal" + "go.opentelemetry.io/contrib/samplers/aws/xray/internal/util" + sdktrace "go.opentelemetry.io/otel/sdk/trace" + "testing" +) + +// assert that when manifest is not expired sampling happens with 1 req/sec. +func TestShouldSample(t *testing.T) { + clock := &util.MockClock{ + NowTime: 100, + } + + r1 := internal.Rule{} + + rules := []internal.Rule{r1} + + m := &internal.Manifest{ + Rules: rules, + Clock: clock, + } + + rs := &remoteSampler{ + manifest: m, + } + + sd := rs.ShouldSample(sdktrace.SamplingParameters{}) + assert.Equal(t, sd.Decision, sdktrace.RecordAndSample) +} + +// assert remote sampling description. +func TestRemoteSamplerDescription(t *testing.T) { + rs := &remoteSampler{} + + s := rs.Description() + assert.Equal(t, s, "AwsXrayRemoteSampler{remote sampling with AWS X-Ray}") +} + + + From 7108aca0612cf33a41d195e40cf413fe279cbcdc Mon Sep 17 00:00:00 2001 From: bhautikpip Date: Fri, 25 Feb 2022 17:55:33 -0800 Subject: [PATCH 06/38] fixed minor issues and ran precommit --- samplers/aws/xray/fallback_sampler.go | 8 +- samplers/aws/xray/fallback_sampler_test.go | 8 +- samplers/aws/xray/go.mod | 2 +- samplers/aws/xray/go.sum | 2 + samplers/aws/xray/internal/client.go | 6 +- samplers/aws/xray/internal/client_test.go | 2 +- samplers/aws/xray/internal/manifest.go | 96 ++++-- samplers/aws/xray/internal/manifest_test.go | 324 +++++++++++++----- samplers/aws/xray/internal/match.go | 73 ++-- samplers/aws/xray/internal/match_test.go | 216 ++++-------- samplers/aws/xray/internal/reservoir.go | 5 - samplers/aws/xray/internal/reservoir_test.go | 5 +- samplers/aws/xray/internal/rule.go | 88 +++-- samplers/aws/xray/internal/rule_test.go | 55 ++- samplers/aws/xray/internal/util/rand.go | 22 -- samplers/aws/xray/internal/util/timer.go | 10 +- samplers/aws/xray/remote_sampler.go | 36 +- samplers/aws/xray/remote_sampler_config.go | 4 +- .../aws/xray/remote_sampler_config_test.go | 4 +- samplers/aws/xray/remote_sampler_test.go | 9 +- 20 files changed, 561 insertions(+), 414 deletions(-) diff --git a/samplers/aws/xray/fallback_sampler.go b/samplers/aws/xray/fallback_sampler.go index ecd9ef732e2..a62ad37497f 100644 --- a/samplers/aws/xray/fallback_sampler.go +++ b/samplers/aws/xray/fallback_sampler.go @@ -12,13 +12,14 @@ // See the License for the specific language governing permissions and // limitations under the License. -package main +package xray import ( - sdktrace "go.opentelemetry.io/otel/sdk/trace" - "go.opentelemetry.io/otel/trace" "sync/atomic" "time" + + sdktrace "go.opentelemetry.io/otel/sdk/trace" + "go.opentelemetry.io/otel/trace" ) type FallbackSampler struct { @@ -66,4 +67,3 @@ func (fs *FallbackSampler) borrow(now int64) bool { } return atomic.CompareAndSwapInt64(&fs.currentEpoch, cur, now) } - diff --git a/samplers/aws/xray/fallback_sampler_test.go b/samplers/aws/xray/fallback_sampler_test.go index b6375f0c17a..cf2e8c4e9de 100644 --- a/samplers/aws/xray/fallback_sampler_test.go +++ b/samplers/aws/xray/fallback_sampler_test.go @@ -12,12 +12,14 @@ // See the License for the specific language governing permissions and // limitations under the License. -package main +package xray import ( + "testing" + "github.com/stretchr/testify/assert" + "go.opentelemetry.io/otel/sdk/trace" - "testing" ) // assert sampling using fallback sampler. @@ -48,4 +50,4 @@ func TestFallbackSamplerDescription(t *testing.T) { fs := NewFallbackSampler() s := fs.Description() assert.Equal(t, s, "FallbackSampler{fallback sampling with sampling config of 1 req/sec and 5% of additional requests}") -} \ No newline at end of file +} diff --git a/samplers/aws/xray/go.mod b/samplers/aws/xray/go.mod index e99f18b94ff..3dde1140556 100644 --- a/samplers/aws/xray/go.mod +++ b/samplers/aws/xray/go.mod @@ -9,7 +9,7 @@ require ( github.com/go-logr/stdr v1.2.2 github.com/jinzhu/copier v0.3.5 github.com/stretchr/testify v1.7.0 - go.opentelemetry.io/otel v1.4.0 // indirect + go.opentelemetry.io/otel v1.4.0 go.opentelemetry.io/otel/sdk v1.4.0 go.opentelemetry.io/otel/trace v1.4.0 golang.org/x/sys v0.0.0-20210510120138-977fb7262007 // indirect diff --git a/samplers/aws/xray/go.sum b/samplers/aws/xray/go.sum index ce697ceae91..46a4c9b0e35 100644 --- a/samplers/aws/xray/go.sum +++ b/samplers/aws/xray/go.sum @@ -4,6 +4,7 @@ github.com/go-logr/logr v1.2.2 h1:ahHml/yUpnlb96Rp8HCvtYVPY8ZYpxq3g7UYchIYwbs= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o= github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= github.com/jinzhu/copier v0.3.5 h1:GlvfUwHk62RokgqVNvYsku0TATCF7bAHVwEXoBh3iJg= github.com/jinzhu/copier v0.3.5/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg= @@ -22,6 +23,7 @@ golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210510120138-977fb7262007 h1:gG67DSER+11cZvqIMb8S8bt0vZtiN6xWYARwirrOSfE= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/samplers/aws/xray/internal/client.go b/samplers/aws/xray/internal/client.go index 62461512945..74a418215de 100644 --- a/samplers/aws/xray/internal/client.go +++ b/samplers/aws/xray/internal/client.go @@ -79,9 +79,9 @@ type samplingStatisticsDocument struct { // getSamplingTargetsOutput is used to store parsed json sampling targets type getSamplingTargetsOutput struct { - LastRuleModification *float64 `json:"LastRuleModification,omitempty"` - SamplingTargetDocuments []*samplingTargetDocument `json:"SamplingTargetDocuments,omitempty"` - UnprocessedStatistics []*unprocessedStatistic `json:"UnprocessedStatistics,omitempty"` + LastRuleModification *float64 `json:"LastRuleModification,omitempty"` + SamplingTargetDocuments []*samplingTargetDocument `json:"SamplingTargetDocuments,omitempty"` + UnprocessedStatistics []*unprocessedStatistic `json:"UnprocessedStatistics,omitempty"` } // samplingTargetDocument contains updated targeted information retrieved from X-Ray service diff --git a/samplers/aws/xray/internal/client_test.go b/samplers/aws/xray/internal/client_test.go index 1d1c25c2e69..f6742347253 100644 --- a/samplers/aws/xray/internal/client_test.go +++ b/samplers/aws/xray/internal/client_test.go @@ -280,4 +280,4 @@ func TestEndpointIsNotReachable(t *testing.T) { _, err = client.getSamplingRules(context.Background()) assert.Error(t, err) -} \ No newline at end of file +} diff --git a/samplers/aws/xray/internal/manifest.go b/samplers/aws/xray/internal/manifest.go index 5df9115c561..a4daa68e69e 100644 --- a/samplers/aws/xray/internal/manifest.go +++ b/samplers/aws/xray/internal/manifest.go @@ -1,17 +1,33 @@ +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package internal import ( "context" crypto "crypto/rand" "fmt" - "github.com/go-logr/logr" - "github.com/jinzhu/copier" - "go.opentelemetry.io/contrib/samplers/aws/xray/internal/util" - sdktrace "go.opentelemetry.io/otel/sdk/trace" "sort" "strings" "sync" "time" + + "github.com/go-logr/logr" + "github.com/jinzhu/copier" + + "go.opentelemetry.io/contrib/samplers/aws/xray/internal/util" + sdktrace "go.opentelemetry.io/otel/sdk/trace" ) const defaultInterval = int64(10) @@ -21,34 +37,36 @@ const manifestTTL = 3600 // Manifest represents a full sampling ruleset and provides // option for configuring Logger, Clock and xrayClient. type Manifest struct { - Rules []Rule - SamplingTargetsPollingInterval time.Duration - refreshedAt int64 - xrayClient *xrayClient - clientID *string - logger logr.Logger - Clock util.Clock - mu sync.RWMutex + Rules []Rule + SamplingTargetsPollingInterval time.Duration + refreshedAt int64 + xrayClient *xrayClient + clientID *string + logger logr.Logger + Clock util.Clock + mu sync.RWMutex } // NewManifest return manifest object configured with logging, clock and xrayClient. func NewManifest(addr string, logger logr.Logger) (*Manifest, error) { // generate client for getSamplingRules and getSamplingTargets API call - client, err := newClient(addr); if err != nil { + client, err := newClient(addr) + if err != nil { return nil, err } // generate clientID for sampling statistics - clientID, err := generateClientId(); if err != nil { + clientID, err := generateClientID() + if err != nil { return nil, err } return &Manifest{ - xrayClient: client, - Clock: &util.DefaultClock{}, - logger: logger, + xrayClient: client, + Clock: &util.DefaultClock{}, + logger: logger, SamplingTargetsPollingInterval: 10 * time.Second, - clientID: clientID, + clientID: clientID, }, nil } @@ -62,22 +80,25 @@ func (m *Manifest) Expired() bool { } // MatchAgainstManifestRules returns a Rule and boolean flag set as true if rule has been match against span attributes, otherwise nil and false -func (m *Manifest) MatchAgainstManifestRules(parameters sdktrace.SamplingParameters, serviceName string, cloudPlatform string) (*Rule, bool) { +func (m *Manifest) MatchAgainstManifestRules(parameters sdktrace.SamplingParameters, serviceName string, cloudPlatform string) (*Rule, bool, error) { m.mu.RLock() defer m.mu.RUnlock() matched := false for index, r := range m.Rules { - isRuleMatch := r.appliesTo(parameters, serviceName, cloudPlatform) + isRuleMatch, err := r.appliesTo(parameters, serviceName, cloudPlatform) + if err != nil { + return nil, isRuleMatch, err + } if isRuleMatch { matched = true - return &m.Rules[index], matched + return &m.Rules[index], matched, nil } } - return nil, matched + return nil, matched, nil } // RefreshManifestRules writes sampling rule properties to the manifest object. @@ -100,13 +121,17 @@ func (m *Manifest) RefreshManifestTargets(ctx context.Context) (err error) { // deep copy centralized manifest object to temporary manifest to avoid thread safety issue m.mu.RLock() - err = copier.CopyWithOption(&manifest, m, copier.Option{IgnoreEmpty: false, DeepCopy: true}); if err != nil { + err = copier.CopyWithOption(&manifest, m, copier.Option{IgnoreEmpty: false, DeepCopy: true}) + if err != nil { return err } m.mu.RUnlock() // generate sampling statistics based on the data in temporary manifest - statistics, err := manifest.snapshots(); if err != nil { return err } + statistics, err := manifest.snapshots() + if err != nil { + return err + } // return if no statistics to report if len(statistics) == 0 { @@ -118,12 +143,13 @@ func (m *Manifest) RefreshManifestTargets(ctx context.Context) (err error) { targets, err := m.xrayClient.getSamplingTargets(ctx, statistics) if err != nil { return fmt.Errorf("refreshTargets: error occurred while getting sampling targets: %w", err) - } else { - m.logger.V(5).Info("successfully fetched sampling targets") } + m.logger.V(5).Info("successfully fetched sampling targets") + // update temporary manifest with retrieved targets (statistics) for each rule - refresh, err := manifest.updateTargets(targets); if err != nil { + refresh, err := manifest.updateTargets(targets) + if err != nil { return err } @@ -159,12 +185,12 @@ func (m *Manifest) updateRules(rules *getSamplingRulesOutput) { for _, records := range rules.SamplingRuleRecords { if records.SamplingRule.RuleName == "" { - m.logger.V(5).Info("Sampling rule without rule name is not supported") + m.logger.V(5).Info("sampling rule without rule name is not supported") continue } if records.SamplingRule.Version != int64(1) { - m.logger.V(5).Info("Sampling rule without Version 1 is not supported", "RuleName", records.SamplingRule.RuleName) + m.logger.V(5).Info("sampling rule without Version 1 is not supported", "RuleName", records.SamplingRule.RuleName) continue } @@ -179,24 +205,20 @@ func (m *Manifest) updateRules(rules *getSamplingRulesOutput) { m.Rules = tempManifest.Rules m.refreshedAt = m.Clock.Now().Unix() m.mu.Unlock() - - return } func (m *Manifest) createRule(ruleProp ruleProperties) { - cr := reservoir { + cr := reservoir{ capacity: ruleProp.ReservoirSize, interval: defaultInterval, } - csr := Rule { + csr := Rule{ reservoir: cr, ruleProperties: ruleProp, } m.Rules = append(m.Rules, csr) - - return } func (m *Manifest) updateTargets(targets *getSamplingTargetsOutput) (refresh bool, err error) { @@ -323,7 +345,7 @@ func (m *Manifest) minimumPollingInterval(targets *getSamplingTargetsOutput) (mi } // generateClientId generates random client ID -func generateClientId() (*string, error) { +func generateClientID() (*string, error) { var r [12]byte _, err := crypto.Read(r[:]) @@ -335,5 +357,3 @@ func generateClientId() (*string, error) { return &id, err } - - diff --git a/samplers/aws/xray/internal/manifest_test.go b/samplers/aws/xray/internal/manifest_test.go index 5fd28da3081..31f37ef4f59 100644 --- a/samplers/aws/xray/internal/manifest_test.go +++ b/samplers/aws/xray/internal/manifest_test.go @@ -16,10 +16,6 @@ package internal import ( "context" - "github.com/go-logr/stdr" - "github.com/stretchr/testify/require" - "go.opentelemetry.io/contrib/samplers/aws/xray/internal/util" - sdktrace "go.opentelemetry.io/otel/sdk/trace" "log" "net/http" "net/http/httptest" @@ -27,10 +23,16 @@ import ( "os" "testing" + "github.com/go-logr/stdr" + "github.com/stretchr/testify/require" + + "go.opentelemetry.io/contrib/samplers/aws/xray/internal/util" + sdktrace "go.opentelemetry.io/otel/sdk/trace" + "github.com/stretchr/testify/assert" ) -// assert that new manifest has certain non-nil attributes +// assert that new manifest has certain non-nil attributes. func TestNewManifest(t *testing.T) { logger := stdr.NewWithOptions(log.New(os.Stderr, "", log.LstdFlags|log.Lshortfile), stdr.Options{LogCaller: stdr.Error}) m, err := NewManifest("127.0.0.1:2000", logger) @@ -44,11 +46,39 @@ func TestNewManifest(t *testing.T) { assert.NotNil(t, m.xrayClient) } -// assert that manifest rule r2 is a match for sampling +// assert that manifest is expired. +func TestExpiredManifest(t *testing.T) { + clock := &util.MockClock{ + NowTime: 10000, + } + + m := &Manifest{ + Clock: clock, + refreshedAt: 3700, + } + + assert.True(t, m.Expired()) +} + +// assert that if collector is not enabled at specified endpoint, returns an error +func TestRefreshManifestError(t *testing.T) { + // collector is not running at port 2020 so expect error + client, err := newClient("127.0.0.1:2020") + require.NoError(t, err) + + m := &Manifest{ + xrayClient: client, + } + + err = m.RefreshManifestRules(context.Background()) + assert.Error(t, err) +} + +// assert that manifest rule r2 is a match for sampling. func TestMatchAgainstManifestRules(t *testing.T) { r1 := Rule{ ruleProperties: ruleProperties{ - RuleName: "r1", + RuleName: "r1", Priority: 10000, Host: "*", HTTPMethod: "*", @@ -90,8 +120,8 @@ func TestMatchAgainstManifestRules(t *testing.T) { Rules: rules, } - exp, err := m.MatchAgainstManifestRules(sdktrace.SamplingParameters{}, "test", "local") - require.NotEmpty(t, err) + exp, _, err := m.MatchAgainstManifestRules(sdktrace.SamplingParameters{}, "test", "local") + require.NoError(t, err) // assert that manifest rule r2 is a match assert.Equal(t, *exp, r2) @@ -177,7 +207,7 @@ func TestRefreshManifestRules(t *testing.T) { require.NoError(t, err) m := &Manifest{ - Rules: []Rule{}, + Rules: []Rule{}, xrayClient: client, Clock: &util.DefaultClock{}, } @@ -198,7 +228,7 @@ func TestRefreshManifestRules(t *testing.T) { ServiceName: "*", ResourceARN: "*", ServiceType: "*", - Attributes: map[string]string{}, + Attributes: map[string]string{}, }, reservoir: reservoir{ interval: defaultInterval, @@ -219,7 +249,7 @@ func TestRefreshManifestRules(t *testing.T) { ServiceName: "test-rule", ResourceARN: "*", ServiceType: "*", - Attributes: map[string]string{}, + Attributes: map[string]string{}, }, reservoir: reservoir{ interval: defaultInterval, @@ -240,7 +270,7 @@ func TestRefreshManifestRules(t *testing.T) { ServiceName: "*", ResourceARN: "*", ServiceType: "local", - Attributes: map[string]string{}, + Attributes: map[string]string{}, }, reservoir: reservoir{ interval: defaultInterval, @@ -257,7 +287,7 @@ func TestRefreshManifestRules(t *testing.T) { assert.Equal(t, 3, len(m.Rules)) } -// assert that rule with no ServiceName updates manifest successfully with empty values +// assert that rule with no ServiceName updates manifest successfully with empty values. func TestRefreshManifestMissingServiceName(t *testing.T) { ctx := context.Background() @@ -301,9 +331,9 @@ func TestRefreshManifestMissingServiceName(t *testing.T) { require.NoError(t, err) m := &Manifest{ - Rules: []Rule{}, + Rules: []Rule{}, xrayClient: client, - Clock: &util.DefaultClock{}, + Clock: &util.DefaultClock{}, } err = m.RefreshManifestRules(ctx) @@ -313,7 +343,7 @@ func TestRefreshManifestMissingServiceName(t *testing.T) { assert.Equal(t, 1, len(m.Rules)) } -// assert that rule with no RuleName does not update to the manifest +// assert that rule with no RuleName does not update to the manifest. func TestRefreshManifestMissingRuleName(t *testing.T) { ctx := context.Background() @@ -356,10 +386,10 @@ func TestRefreshManifestMissingRuleName(t *testing.T) { require.NoError(t, err) m := &Manifest{ - Rules: []Rule{}, + Rules: []Rule{}, xrayClient: client, - Clock: &util.DefaultClock{}, - logger: stdr.NewWithOptions(log.New(os.Stderr, "", log.LstdFlags|log.Lshortfile), stdr.Options{LogCaller: stdr.Error}), + Clock: &util.DefaultClock{}, + logger: stdr.NewWithOptions(log.New(os.Stderr, "", log.LstdFlags|log.Lshortfile), stdr.Options{LogCaller: stdr.Error}), } err = m.RefreshManifestRules(ctx) @@ -369,7 +399,7 @@ func TestRefreshManifestMissingRuleName(t *testing.T) { assert.Equal(t, 0, len(m.Rules)) } -// assert that rule with version greater than one does not update to the manifest +// assert that rule with version greater than one does not update to the manifest. func TestRefreshManifestIncorrectVersion(t *testing.T) { ctx := context.Background() @@ -414,10 +444,10 @@ func TestRefreshManifestIncorrectVersion(t *testing.T) { require.NoError(t, err) m := &Manifest{ - Rules: []Rule{}, + Rules: []Rule{}, xrayClient: client, - Clock: &util.DefaultClock{}, - logger: stdr.NewWithOptions(log.New(os.Stderr, "", log.LstdFlags|log.Lshortfile), stdr.Options{LogCaller: stdr.Error}), + Clock: &util.DefaultClock{}, + logger: stdr.NewWithOptions(log.New(os.Stderr, "", log.LstdFlags|log.Lshortfile), stdr.Options{LogCaller: stdr.Error}), } err = m.RefreshManifestRules(ctx) @@ -427,7 +457,7 @@ func TestRefreshManifestIncorrectVersion(t *testing.T) { assert.Equal(t, 0, len(m.Rules)) } -// assert that 1 valid and 1 invalid rule update only valid rule gets stored to the manifest +// assert that 1 valid and 1 invalid rule update only valid rule gets stored to the manifest. func TestRefreshManifestAddOneInvalidRule(t *testing.T) { ctx := context.Background() @@ -488,7 +518,7 @@ func TestRefreshManifestAddOneInvalidRule(t *testing.T) { ServiceName: "*", ResourceARN: "*", ServiceType: "*", - Attributes: map[string]string{}, + Attributes: map[string]string{}, }, reservoir: reservoir{ interval: defaultInterval, @@ -510,10 +540,10 @@ func TestRefreshManifestAddOneInvalidRule(t *testing.T) { require.NoError(t, err) m := &Manifest{ - Rules: []Rule{}, + Rules: []Rule{}, xrayClient: client, - Clock: &util.DefaultClock{}, - logger: stdr.NewWithOptions(log.New(os.Stderr, "", log.LstdFlags|log.Lshortfile), stdr.Options{LogCaller: stdr.Error}), + Clock: &util.DefaultClock{}, + logger: stdr.NewWithOptions(log.New(os.Stderr, "", log.LstdFlags|log.Lshortfile), stdr.Options{LogCaller: stdr.Error}), } err = m.RefreshManifestRules(ctx) @@ -525,7 +555,129 @@ func TestRefreshManifestAddOneInvalidRule(t *testing.T) { assert.Equal(t, r1, m.Rules[0]) } -// assert that a valid sampling target updates its rule +// assert that inactive rule so return early without doing getSamplingTargets call +func TestRefreshManifestTarget_NoSnapShot(t *testing.T) { + clock := &util.MockClock{ + NowTime: 15000000, + } + + r1 := Rule{ + ruleProperties: ruleProperties{ + RuleName: "r3", + Priority: 100, + Host: "*", + HTTPMethod: "*", + URLPath: "*", + ReservoirSize: 100, + FixedRate: 0.09, + Version: 1, + ServiceName: "*", + ResourceARN: "*", + ServiceType: "local", + Attributes: map[string]string{}, + }, + reservoir: reservoir{ + interval: defaultInterval, + capacity: int64(100), + }, + matchedRequests: int64(0), + } + + rules := []Rule{r1} + + m := &Manifest{ + Rules: rules, + Clock: clock, + logger: stdr.NewWithOptions(log.New(os.Stderr, "", log.LstdFlags|log.Lshortfile), stdr.Options{LogCaller: stdr.Error}), + } + + err := m.RefreshManifestTargets(context.Background()) + assert.Nil(t, err) +} + +// assert that refresh manifest targets successfully updates reservoir value for a rule. +func TestRefreshManifestTargets(t *testing.T) { + // RuleName is missing from r2 + body := []byte(`{ + "LastRuleModification": 17000000, + "SamplingTargetDocuments": [ + { + "FixedRate": 0.06, + "Interval": 25, + "ReservoirQuota": 23, + "ReservoirQuotaTTL": 15000000, + "RuleName": "r1" + } + ], + "UnprocessedStatistics": [ + { + "ErrorCode": "200", + "Message": "Ok", + "RuleName": "r1" + } + ] +}`) + + clock := &util.MockClock{ + NowTime: 150, + } + + r1 := Rule{ + ruleProperties: ruleProperties{ + RuleName: "r1", + Priority: 100, + Host: "*", + HTTPMethod: "*", + URLPath: "*", + ReservoirSize: 100, + FixedRate: 0.09, + Version: 1, + ServiceName: "*", + ResourceARN: "*", + ServiceType: "local", + Attributes: map[string]string{}, + }, + reservoir: reservoir{ + interval: defaultInterval, + capacity: int64(100), + }, + matchedRequests: int64(5), + } + + rules := []Rule{r1} + + // generate a test server so we can capture and inspect the request + testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { + _, err := res.Write([]byte(body)) + require.NoError(t, err) + })) + defer testServer.Close() + + u, err := url.Parse(testServer.URL) + require.NoError(t, err) + + client, err := newClient(u.Host) + require.NoError(t, err) + + m := &Manifest{ + Rules: rules, + Clock: clock, + logger: stdr.NewWithOptions(log.New(os.Stderr, "", log.LstdFlags|log.Lshortfile), stdr.Options{LogCaller: stdr.Error}), + xrayClient: client, + refreshedAt: 18000000, + } + + err = m.RefreshManifestTargets(context.Background()) + require.NoError(t, err) + + // assert target updates + assert.Equal(t, m.Rules[0].ruleProperties.FixedRate, 0.06) + assert.Equal(t, m.Rules[0].reservoir.quota, int64(23)) + assert.Equal(t, m.Rules[0].reservoir.expiresAt, int64(15000000)) + assert.Equal(t, m.Rules[0].reservoir.interval, int64(25)) +} + +// assert that a valid sampling target updates its rule. func TestUpdateTargets(t *testing.T) { clock := &util.MockClock{ NowTime: 1500000000, @@ -598,7 +750,7 @@ func TestUpdateTargets(t *testing.T) { } // assert that when last rule modification time is greater than manifest refresh time we need to update manifest -// out of band (async) +// out of band (async). func TestUpdateTargetsRefreshFlagTest(t *testing.T) { clock := &util.MockClock{ NowTime: 1500000000, @@ -620,7 +772,7 @@ func TestUpdateTargetsRefreshFlagTest(t *testing.T) { targetLastRuleModifiedTime := float64(1500000020) targets := &getSamplingTargetsOutput{ SamplingTargetDocuments: []*samplingTargetDocument{&st}, - LastRuleModification: &targetLastRuleModifiedTime, + LastRuleModification: &targetLastRuleModifiedTime, } // sampling rule about to be updated with new target @@ -642,9 +794,9 @@ func TestUpdateTargetsRefreshFlagTest(t *testing.T) { rules := []Rule{r1} m := &Manifest{ - Rules: rules, + Rules: rules, refreshedAt: clock.Now().Unix(), - Clock: clock, + Clock: clock, } refresh, err := m.updateTargets(targets) @@ -673,7 +825,7 @@ func TestUpdateTargetsRefreshFlagTest(t *testing.T) { assert.Equal(t, exp, m.Rules[0]) } -// unprocessed statistics error code is 5xx then updateTargets returns an error, if 4xx refresh flag set to true +// unprocessed statistics error code is 5xx then updateTargets returns an error, if 4xx refresh flag set to true. func TestUpdateTargetsUnprocessedStatistics(t *testing.T) { clock := &util.MockClock{ NowTime: 1500000000, @@ -692,39 +844,51 @@ func TestUpdateTargetsUnprocessedStatistics(t *testing.T) { RuleName: &name, } + // case for 4xx errorCode500 := "500" unprocessedStats5xx := unprocessedStatistic{ ErrorCode: &errorCode500, - RuleName: &name, + RuleName: &name, } - errorCode400 := "400" - unprocessedStats4xx := unprocessedStatistic{ - ErrorCode: &errorCode400, - RuleName: &name, - } - - targets := &getSamplingTargetsOutput{ + targets5xx := &getSamplingTargetsOutput{ SamplingTargetDocuments: []*samplingTargetDocument{&st}, - UnprocessedStatistics: []*unprocessedStatistic{&unprocessedStats5xx, &unprocessedStats4xx}, + UnprocessedStatistics: []*unprocessedStatistic{&unprocessedStats5xx}, } - m := &Manifest{ - Clock: clock, + Clock: clock, logger: stdr.NewWithOptions(log.New(os.Stderr, "", log.LstdFlags|log.Lshortfile), stdr.Options{LogCaller: stdr.Error}), - } - refresh, err := m.updateTargets(targets) - // assert error happened since unprocessed stats has 5xx error code + refresh, err := m.updateTargets(targets5xx) + // assert error happened since unprocessed stats has returned 5xx error code require.Error(t, err) // assert refresh is false assert.False(t, refresh) + + // case for 4xx + errorCode400 := "400" + unprocessedStats4xx := unprocessedStatistic{ + ErrorCode: &errorCode400, + RuleName: &name, + } + + targets4xx := &getSamplingTargetsOutput{ + SamplingTargetDocuments: []*samplingTargetDocument{&st}, + UnprocessedStatistics: []*unprocessedStatistic{&unprocessedStats4xx}, + } + + refresh, err = m.updateTargets(targets4xx) + // assert that no error happened since unprocessed stats has returned 4xx error code + require.NoError(t, err) + + // assert refresh is true + assert.True(t, refresh) } -// assert that a missing sampling rule in manifest does not update it's reservoir values +// assert that a missing sampling rule in manifest does not update it's reservoir values. func TestUpdateReservoir(t *testing.T) { // Sampling target received from centralized sampling backend rate := 0.05 @@ -767,7 +931,7 @@ func TestUpdateReservoir(t *testing.T) { assert.Equal(t, m.Rules[0], r1) } -// assert that a sampling target with missing Fixed Rate returns an error +// assert that a sampling target with missing Fixed Rate returns an error. func TestUpdateReservoirMissingFixedRate(t *testing.T) { // Sampling target received from centralized sampling backend quota := int64(10) @@ -805,7 +969,7 @@ func TestUpdateReservoirMissingFixedRate(t *testing.T) { require.Error(t, err) } -// assert that a sampling target with missing Rule Name returns an error +// assert that a sampling target with missing Rule Name returns an error. func TestUpdateReservoirMissingRuleName(t *testing.T) { // Sampling target received from centralized sampling backend rate := 0.05 @@ -843,7 +1007,7 @@ func TestUpdateReservoirMissingRuleName(t *testing.T) { require.Error(t, err) } -// assert that snapshots returns an array of valid sampling statistics +// assert that snapshots returns an array of valid sampling statistics. func TestSnapshots(t *testing.T) { clock := &util.MockClock{ NowTime: 1500000000, @@ -857,7 +1021,7 @@ func TestSnapshots(t *testing.T) { borrowed1 := int64(5) r1 := Rule{ ruleProperties: ruleProperties{ - RuleName: name1, + RuleName: name1, }, reservoir: reservoir{ interval: 10, @@ -875,7 +1039,7 @@ func TestSnapshots(t *testing.T) { ruleProperties: ruleProperties{ RuleName: name2, }, - reservoir: reservoir{ + reservoir: reservoir{ interval: 10, }, matchedRequests: requests2, @@ -887,9 +1051,9 @@ func TestSnapshots(t *testing.T) { id := "c1" m := &Manifest{ - Rules: rules, + Rules: rules, clientID: &id, - Clock: clock, + Clock: clock, } // Expected SamplingStatistics structs @@ -922,7 +1086,7 @@ func TestSnapshots(t *testing.T) { assert.Equal(t, ss2, *statistics[1]) } -// assert that fresh and inactive rules are not included in a snapshot +// assert that fresh and inactive rules are not included in a snapshot. func TestMixedSnapshots(t *testing.T) { clock := &util.MockClock{ NowTime: 1500000000, @@ -960,7 +1124,7 @@ func TestMixedSnapshots(t *testing.T) { ruleProperties: ruleProperties{ RuleName: name2, }, - reservoir: reservoir{ + reservoir: reservoir{ interval: 20, refreshedAt: 1499999990, }, @@ -979,7 +1143,7 @@ func TestMixedSnapshots(t *testing.T) { ruleProperties: ruleProperties{ RuleName: name3, }, - reservoir: reservoir{ + reservoir: reservoir{ interval: 20, refreshedAt: 1499999990, }, @@ -992,8 +1156,8 @@ func TestMixedSnapshots(t *testing.T) { m := &Manifest{ clientID: &id, - Clock: clock, - Rules: rules, + Clock: clock, + Rules: rules, } ss1 := samplingStatisticsDocument{ @@ -1016,7 +1180,7 @@ func TestMixedSnapshots(t *testing.T) { assert.Equal(t, ss1, *statistics[0]) } -// Assert that sorting an unsorted array results in a sorted array - check priority +// Assert that sorting an unsorted array results in a sorted array - check priority. func TestSortBasedOnPriority(t *testing.T) { r1 := Rule{ ruleProperties: ruleProperties{ @@ -1055,7 +1219,7 @@ func TestSortBasedOnPriority(t *testing.T) { assert.Equal(t, r3, m.Rules[2]) } -// Assert that sorting an unsorted array results in a sorted array - check priority and rule name +// Assert that sorting an unsorted array results in a sorted array - check priority and rule name. func TestSortBasedOnRuleName(t *testing.T) { r1 := Rule{ ruleProperties: ruleProperties{ @@ -1094,21 +1258,21 @@ func TestSortBasedOnRuleName(t *testing.T) { assert.Equal(t, r3, m.Rules[2]) } -// asserts the minimum value of all the targets +// asserts the minimum value of all the targets. func TestMinPollInterval(t *testing.T) { interval1 := int64(10) t1 := &samplingTargetDocument{ - Interval: &interval1, + Interval: &interval1, } - interval2 := int64(15) + interval2 := int64(5) t2 := &samplingTargetDocument{ - Interval: &interval2, + Interval: &interval2, } interval3 := int64(25) t3 := &samplingTargetDocument{ - Interval: &interval3, + Interval: &interval3, } targets := &getSamplingTargetsOutput{ @@ -1119,24 +1283,24 @@ func TestMinPollInterval(t *testing.T) { minPoll := m.minimumPollingInterval(targets) - assert.Equal(t, int64(10), minPoll) + assert.Equal(t, int64(5), minPoll) } -// asserts the minimum value of all the targets when some targets has 0 interval +// asserts the minimum value of all the targets when some targets has 0 interval. func TestMinPollIntervalZeroCase(t *testing.T) { interval1 := int64(0) t1 := &samplingTargetDocument{ - Interval: &interval1, + Interval: &interval1, } interval2 := int64(0) t2 := &samplingTargetDocument{ - Interval: &interval2, + Interval: &interval2, } interval3 := int64(5) t3 := &samplingTargetDocument{ - Interval: &interval3, + Interval: &interval3, } targets := &getSamplingTargetsOutput{ @@ -1150,21 +1314,21 @@ func TestMinPollIntervalZeroCase(t *testing.T) { assert.Equal(t, int64(5), minPoll) } -// asserts the minimum value of all the targets when some targets has negative interval +// asserts the minimum value of all the targets when some targets has negative interval. func TestMinPollIntervalNegativeCase(t *testing.T) { interval1 := int64(-5) t1 := &samplingTargetDocument{ - Interval: &interval1, + Interval: &interval1, } interval2 := int64(0) t2 := &samplingTargetDocument{ - Interval: &interval2, + Interval: &interval2, } interval3 := int64(0) t3 := &samplingTargetDocument{ - Interval: &interval3, + Interval: &interval3, } targets := &getSamplingTargetsOutput{ @@ -1178,9 +1342,9 @@ func TestMinPollIntervalNegativeCase(t *testing.T) { assert.Equal(t, int64(-5), minPoll) } -// assert that able to successfully generate the client ID +// assert that able to successfully generate the client ID. func TestGenerateClientID(t *testing.T) { - clientID, err := generateClientId() + clientID, err := generateClientID() require.NoError(t, err) assert.NotEmpty(t, clientID) } diff --git a/samplers/aws/xray/internal/match.go b/samplers/aws/xray/internal/match.go index ecdee7580c0..5f1dbe6ee88 100644 --- a/samplers/aws/xray/internal/match.go +++ b/samplers/aws/xray/internal/match.go @@ -14,57 +14,58 @@ package internal -import "strings" +import ( + "fmt" + "regexp" + "strings" +) // wildcardMatch returns true if text matches pattern at the given case-sensitivity; returns false otherwise. -func wildcardMatch(pattern, text string) bool { +func wildcardMatch(pattern, text string) (bool, error) { patternLen := len(pattern) textLen := len(text) if patternLen == 0 { - return textLen == 0 + return textLen == 0, nil } if pattern == "*" { - return true + return true, nil } pattern = strings.ToLower(pattern) text = strings.ToLower(text) - i := 0 - p := 0 - iStar := textLen - pStar := 0 + match, err := regexp.MatchString(toRegexPattern(pattern), text) + if err != nil { + return false, fmt.Errorf("wildcardMatch: unable to perform regex matching: %w", err) + } + + return match, nil +} - for i < textLen { - if p < patternLen { - switch pattern[p] { - case text[i]: - i++ - p++ - continue - case '?': - i++ - p++ - continue - case '*': - iStar = i - pStar = p - p++ - continue +func toRegexPattern(pattern string) string { + tokenStart := -1 + var result strings.Builder + for i, char := range pattern { + if string(char) == "*" || string(char) == "?" { + if tokenStart != -1 { + result.WriteString(regexp.QuoteMeta(pattern[tokenStart:i])) + tokenStart = -1 + } + + if string(char) == "*" { + result.WriteString(".*") + } else { + result.WriteString(".") + } + } else { + if tokenStart == -1 { + tokenStart = i } } - if iStar == textLen { - return false - } - iStar++ - i = iStar - p = pStar + 1 } - - for p < patternLen && pattern[p] == '*' { - p++ + if tokenStart != -1 { + result.WriteString(regexp.QuoteMeta(pattern[tokenStart:])) } - - return p == patternLen && i == textLen -} \ No newline at end of file + return result.String() +} diff --git a/samplers/aws/xray/internal/match_test.go b/samplers/aws/xray/internal/match_test.go index f57d333c4f5..39cebaeebed 100644 --- a/samplers/aws/xray/internal/match_test.go +++ b/samplers/aws/xray/internal/match_test.go @@ -19,101 +19,85 @@ import ( "math/rand" "testing" + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/assert" ) -func TestInvalidArgs(t *testing.T) { - assert.False(t, wildcardMatch("", "whatever")) -} - -func TestInvalidArgs1(t *testing.T) { - assert.True(t, wildcardMatch("*", "")) -} - -func TestMatchExactPositive(t *testing.T) { - assert.True(t, wildcardMatch("foo", "foo")) -} - -func TestMatchExactNegative(t *testing.T) { - assert.False(t, wildcardMatch("foo", "bar")) -} - -func TestSingleWildcardPositive(t *testing.T) { - assert.True(t, wildcardMatch("fo?", "foo")) -} - -func TestSingleWildcardNegative(t *testing.T) { - assert.False(t, wildcardMatch("f?o", "boo")) -} - -func TestMultipleWildcardPositive(t *testing.T) { - assert.True(t, wildcardMatch("?o?", "foo")) -} - -func TestMultipleWildcardNegative(t *testing.T) { - assert.False(t, wildcardMatch("f??", "boo")) -} - -func TestGlobPositive(t *testing.T) { - assert.True(t, wildcardMatch("*oo", "foo")) -} - -func TestGlobPositiveZeroOrMore(t *testing.T) { - assert.True(t, wildcardMatch("foo*", "foo")) -} - -func TestGlobNegativeZeroOrMore(t *testing.T) { - assert.False(t, wildcardMatch("foo*", "fo0")) -} - -func TestGlobNegative(t *testing.T) { - assert.False(t, wildcardMatch("fo*", "boo")) -} - -func TestGlobAndSinglePositive(t *testing.T) { - assert.True(t, wildcardMatch("*o?", "foo")) -} - -func TestGlobAndSingleNegative(t *testing.T) { - assert.False(t, wildcardMatch("f?*", "boo")) -} +// assert wildcard match is positive. +func TestWildCardMatchPositive(t *testing.T) { + tests := []struct { + pattern string + text string + }{ + // wildcard positive test set + {"*", ""}, + {"foo", "foo"}, + {"foo*bar*?", "foodbaris"}, + {"?o?", "foo"}, + {"*oo", "foo"}, + {"foo*", "foo"}, + {"*o?", "foo"}, + {"*", "boo"}, + {"", ""}, + {"a", "a"}, + {"*a", "a"}, + {"*a", "ba"}, + {"a*", "a"}, + {"a*", "ab"}, + {"a*a", "aa"}, + {"a*a", "aba"}, + {"a*a*", "aaaaaaaaaaaaaaaaaaaaaaa"}, + {"a*b*a*b*a*b*a*b*a*", + "akljd9gsdfbkjhaabajkhbbyiaahkjbjhbuykjakjhabkjhbabjhkaabbabbaaakljdfsjklababkjbsdabab"}, + {"a*na*ha", "anananahahanahana"}, + {"***a", "a"}, + {"**a**", "a"}, + {"a**b", "ab"}, + {"*?", "a"}, + {"*??", "aa"}, + {"*?", "a"}, + {"*?*a*", "ba"}, + {"?at", "bat"}, + {"?at", "cat"}, + {"?o?se", "horse"}, + {"?o?se", "mouse"}, + {"*s", "horse"}, + {"J*", "Jeep"}, + {"*/foo", "/bar/foo"}, + } -func TestPureWildcard(t *testing.T) { - assert.True(t, wildcardMatch("*", "boo")) + for _, test := range tests { + match, err := wildcardMatch(test.pattern, test.text) + require.NoError(t, err) + assert.True(t, match) + } } -func TestMisc(t *testing.T) { - animal1 := "?at" - animal2 := "?o?se" - animal3 := "*s" - - vehicle1 := "J*" - vehicle2 := "????" - - assert.True(t, wildcardMatch(animal1, "bat")) - assert.True(t, wildcardMatch(animal1, "cat")) - assert.True(t, wildcardMatch(animal2, "horse")) - assert.True(t, wildcardMatch(animal2, "mouse")) - assert.True(t, wildcardMatch(animal3, "dogs")) - assert.True(t, wildcardMatch(animal3, "horses")) - - assert.True(t, wildcardMatch(vehicle1, "Jeep")) - assert.True(t, wildcardMatch(vehicle2, "ford")) - assert.False(t, wildcardMatch(vehicle2, "chevy")) - assert.True(t, wildcardMatch("*", "cAr")) - - assert.True(t, wildcardMatch("*/foo", "/bar/foo")) -} +// assert wildcard match is negative. +func TestWildCardMatchNegative(t *testing.T) { + tests := []struct { + pattern string + text string + }{ + // wildcard negative test set + {"", "whatever"}, + {"foo", "bar"}, + {"f?o", "boo"}, + {"f??", "boo"}, + {"fo*", "boo"}, + {"f?*", "boo"}, + {"abcd", "abc"}, + {"??", "a"}, + {"??", "a"}, + {"*?*a", "a"}, + } -func TestCaseInsensitivity(t *testing.T) { - assert.True(t, wildcardMatch("Foo", "Foo")) - assert.True(t, wildcardMatch("Foo", "FOO")) - assert.True(t, wildcardMatch("Fo*", "Foo0")) - assert.True(t, wildcardMatch("Fo*", "FOO0")) - assert.True(t, wildcardMatch("Fo?", "Foo")) - assert.True(t, wildcardMatch("Fo?", "FOo")) - assert.True(t, wildcardMatch("Fo?", "FoO")) - assert.True(t, wildcardMatch("Fo?", "FOO")) + for _, test := range tests { + match, err := wildcardMatch(test.pattern, test.text) + require.NoError(t, err) + assert.False(t, match) + } } func TestLongStrings(t *testing.T) { @@ -124,57 +108,7 @@ func TestLongStrings(t *testing.T) { } text.WriteString("b") - assert.True(t, wildcardMatch("a*b", text.String())) -} - -func TestNoGlobs(t *testing.T) { - assert.False(t, wildcardMatch("abcd", "abc")) + match, err := wildcardMatch("a*b", text.String()) + require.NoError(t, err) + assert.True(t, match) } - -func TestEdgeCaseGlobs(t *testing.T) { - assert.True(t, wildcardMatch("", "")) - assert.True(t, wildcardMatch("a", "a")) - assert.True(t, wildcardMatch("*a", "a")) - assert.True(t, wildcardMatch("*a", "ba")) - assert.True(t, wildcardMatch("a*", "a")) - assert.True(t, wildcardMatch("a*", "ab")) - assert.True(t, wildcardMatch("a*a", "aa")) - assert.True(t, wildcardMatch("a*a", "aba")) - assert.True(t, wildcardMatch("a*a", "aaa")) - assert.True(t, wildcardMatch("a*a*", "aa")) - assert.True(t, wildcardMatch("a*a*", "aba")) - assert.True(t, wildcardMatch("a*a*", "aaa")) - assert.True(t, wildcardMatch("a*a*", "aaaaaaaaaaaaaaaaaaaaaaa")) - assert.True(t, wildcardMatch("a*b*a*b*a*b*a*b*a*", - "akljd9gsdfbkjhaabajkhbbyiaahkjbjhbuykjakjhabkjhbabjhkaabbabbaaakljdfsjklababkjbsdabab")) - assert.False(t, wildcardMatch("a*na*ha", "anananahahanahana")) -} - -func TestMultiGlobs(t *testing.T) { - assert.True(t, wildcardMatch("*a", "a")) - assert.True(t, wildcardMatch("**a", "a")) - assert.True(t, wildcardMatch("***a", "a")) - assert.True(t, wildcardMatch("**a*", "a")) - assert.True(t, wildcardMatch("**a**", "a")) - - assert.True(t, wildcardMatch("a**b", "ab")) - assert.True(t, wildcardMatch("a**b", "abb")) - - assert.True(t, wildcardMatch("*?", "a")) - assert.True(t, wildcardMatch("*?", "aa")) - assert.True(t, wildcardMatch("*??", "aa")) - assert.False(t, wildcardMatch("*???", "aa")) - assert.True(t, wildcardMatch("*?", "aaa")) - - assert.True(t, wildcardMatch("?", "a")) - assert.False(t, wildcardMatch("??", "a")) - - assert.True(t, wildcardMatch("?*", "a")) - assert.True(t, wildcardMatch("*?", "a")) - assert.False(t, wildcardMatch("?*?", "a")) - assert.True(t, wildcardMatch("?*?", "aa")) - assert.True(t, wildcardMatch("*?*", "a")) - - assert.False(t, wildcardMatch("*?*a", "a")) - assert.True(t, wildcardMatch("*?*a*", "ba")) -} \ No newline at end of file diff --git a/samplers/aws/xray/internal/reservoir.go b/samplers/aws/xray/internal/reservoir.go index 77bdd8a4c89..b38995e4918 100644 --- a/samplers/aws/xray/internal/reservoir.go +++ b/samplers/aws/xray/internal/reservoir.go @@ -15,7 +15,6 @@ package internal import ( - "fmt" "sync/atomic" ) @@ -66,10 +65,6 @@ func (r *reservoir) take(now int64) bool { quota := atomic.LoadInt64(&r.quota) used := atomic.LoadInt64(&r.used) - fmt.Println("cur", cur) - fmt.Println("quota", quota) - fmt.Println("used", used) - if cur != now { atomic.CompareAndSwapInt64(&r.currentEpoch, cur, now) atomic.CompareAndSwapInt64(&r.used, used, int64(0)) diff --git a/samplers/aws/xray/internal/reservoir_test.go b/samplers/aws/xray/internal/reservoir_test.go index 803e3d1c273..858b7513181 100644 --- a/samplers/aws/xray/internal/reservoir_test.go +++ b/samplers/aws/xray/internal/reservoir_test.go @@ -15,9 +15,10 @@ package internal import ( - "go.opentelemetry.io/contrib/samplers/aws/xray/internal/util" "testing" + "go.opentelemetry.io/contrib/samplers/aws/xray/internal/util" + "github.com/stretchr/testify/assert" ) @@ -148,4 +149,4 @@ func TestResetQuotaUsageRotation(t *testing.T) { assert.Equal(t, int64(1500000001), r.currentEpoch) assert.Equal(t, true, taken) assert.Equal(t, int64(1), r.used) -} \ No newline at end of file +} diff --git a/samplers/aws/xray/internal/rule.go b/samplers/aws/xray/internal/rule.go index 453b1b03c8b..10162dcaefd 100644 --- a/samplers/aws/xray/internal/rule.go +++ b/samplers/aws/xray/internal/rule.go @@ -1,11 +1,24 @@ +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package internal import ( - "fmt" - "go.opentelemetry.io/contrib/samplers/aws/xray/internal/util" + "sync/atomic" + sdktrace "go.opentelemetry.io/otel/sdk/trace" "go.opentelemetry.io/otel/trace" - "sync/atomic" ) // Rule represents a sampling rule which contains rule properties and reservoir which keeps tracks of sampling statistics of a rule @@ -26,8 +39,6 @@ type Rule struct { // number of requests borrowed using specific rule borrowedRequests int64 - - clock util.Clock } // stale checks if targets (sampling stats) for a given rule is expired or not @@ -65,14 +76,12 @@ func (r *Rule) Sample(parameters sdktrace.SamplingParameters, now int64) sdktrac if r.reservoir.expired(now) { // borrowing one request every second if r.reservoir.borrow(now) { - fmt.Println("inside expired reservoir") atomic.AddInt64(&r.borrowedRequests, int64(1)) sd.Decision = sdktrace.RecordAndSample return sd } - fmt.Println("inside expired traceIDRatio") // using traceIDRatioBased sampler to sample using fixed rate sd = sdktrace.TraceIDRatioBased(r.ruleProperties.FixedRate).ShouldSample(parameters) @@ -85,14 +94,12 @@ func (r *Rule) Sample(parameters sdktrace.SamplingParameters, now int64) sdktrac // Take from reservoir quota, if quota is available for that second if r.reservoir.take(now) { - fmt.Println("inside non expired reservoir") atomic.AddInt64(&r.sampledRequests, int64(1)) sd.Decision = sdktrace.RecordAndSample return sd } - fmt.Println("inside non expired traceIDRatio") // using traceIDRatioBased sampler to sample using fixed rate sd = sdktrace.TraceIDRatioBased(r.ruleProperties.FixedRate).ShouldSample(parameters) @@ -104,11 +111,12 @@ func (r *Rule) Sample(parameters sdktrace.SamplingParameters, now int64) sdktrac } // appliesTo performs a matching against rule properties to see if a given rule does match with any of the rule set on AWS X-Ray console -func (r *Rule) appliesTo(parameters sdktrace.SamplingParameters, serviceName string, cloudPlatform string) bool { +func (r *Rule) appliesTo(parameters sdktrace.SamplingParameters, serviceName string, cloudPlatform string) (bool, error) { var httpTarget string var httpURL string var httpHost string var httpMethod string + var HTTPURLPathMatcher bool if parameters.Attributes != nil { for _, attrs := range parameters.Attributes { @@ -127,28 +135,66 @@ func (r *Rule) appliesTo(parameters sdktrace.SamplingParameters, serviceName str } } - return (r.attributeMatching(parameters)) && (wildcardMatch(r.ruleProperties.ServiceName, serviceName)) && - (wildcardMatch(r.ruleProperties.ServiceType, cloudPlatform)) && - (wildcardMatch(r.ruleProperties.Host, httpHost)) && - (wildcardMatch(r.ruleProperties.HTTPMethod, httpMethod)) && - (wildcardMatch(r.ruleProperties.URLPath, httpURL) || wildcardMatch(r.ruleProperties.URLPath, httpTarget)) + // attributes and other HTTP span attributes matching + attributeMatcher, err := r.attributeMatching(parameters) + if err != nil { + return attributeMatcher, err + } + serviceNameMatcher, err := wildcardMatch(r.ruleProperties.ServiceName, serviceName) + if err != nil { + return serviceNameMatcher, err + } + serviceTypeMatcher, err := wildcardMatch(r.ruleProperties.ServiceType, cloudPlatform) + if err != nil { + return serviceTypeMatcher, err + } + HTTPMethodMatcher, err := wildcardMatch(r.ruleProperties.HTTPMethod, httpMethod) + if err != nil { + return HTTPMethodMatcher, err + } + HTTPHostMatcher, err := wildcardMatch(r.ruleProperties.Host, httpHost) + if err != nil { + return HTTPHostMatcher, err + } + + if httpURL != "" { + HTTPURLPathMatcher, err = wildcardMatch(r.ruleProperties.URLPath, httpURL) + if err != nil { + return HTTPURLPathMatcher, err + } + } else { + HTTPURLPathMatcher, err = wildcardMatch(r.ruleProperties.URLPath, httpTarget) + if err != nil { + return HTTPURLPathMatcher, err + } + } + + return attributeMatcher && + serviceNameMatcher && + serviceTypeMatcher && + HTTPMethodMatcher && + HTTPHostMatcher && + HTTPURLPathMatcher, nil } // attributeMatching performs a match on attributes set by users on AWS X-Ray console -func (r *Rule) attributeMatching(parameters sdktrace.SamplingParameters) bool { - match := false +func (r *Rule) attributeMatching(parameters sdktrace.SamplingParameters) (match bool, err error) { + match = false if len(r.ruleProperties.Attributes) > 0 { for key, value := range r.ruleProperties.Attributes { for _, attrs := range parameters.Attributes { if key == string(attrs.Key) { - match = wildcardMatch(value, attrs.Value.AsString()) + match, err = wildcardMatch(value, attrs.Value.AsString()) + if err != nil { + return false, err + } } else { match = false } } } - return match + return match, nil } - return true -} \ No newline at end of file + return true, nil +} diff --git a/samplers/aws/xray/internal/rule_test.go b/samplers/aws/xray/internal/rule_test.go index 0442fcd3391..4c9b8b4dc28 100644 --- a/samplers/aws/xray/internal/rule_test.go +++ b/samplers/aws/xray/internal/rule_test.go @@ -15,9 +15,12 @@ package internal import ( + "testing" + + "github.com/stretchr/testify/require" + "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/sdk/trace" - "testing" "github.com/stretchr/testify/assert" ) @@ -92,7 +95,7 @@ func TestSnapshot(t *testing.T) { // assert that reservoir is expired, borrowing 1 req during that second. func TestExpiredReservoirBorrowSample(t *testing.T) { r1 := Rule{ - reservoir: reservoir{ + reservoir: reservoir{ expiresAt: 1500000060, used: 0, capacity: 10, @@ -162,7 +165,7 @@ func TestConsumeFromQuotaSample(t *testing.T) { // assert that sampling using traceIDRationBasedSampler. func TestTraceIDRatioBasedSampler(t *testing.T) { r1 := Rule{ - reservoir: reservoir{ + reservoir: reservoir{ quota: 10, expiresAt: 1500000060, currentEpoch: 1500000000, @@ -221,7 +224,9 @@ func TestAppliesToMatchingWithAllAttrs(t *testing.T) { attribute.String("http.url", "http://127.0.0.1:2000"), } - assert.True(t, r1.appliesTo(trace.SamplingParameters{Attributes: httpAttrs}, "test-service", "EC2")) + match, err := r1.appliesTo(trace.SamplingParameters{Attributes: httpAttrs}, "test-service", "EC2") + require.NoError(t, err) + assert.True(t, match) } // assert that matching will happen when rules has all the HTTP attrs set as '*' and @@ -244,7 +249,9 @@ func TestAppliesToMatchingWithStarHTTPAttrs(t *testing.T) { attribute.String("http.url", "http://127.0.0.1:2000"), } - assert.True(t, r1.appliesTo(trace.SamplingParameters{Attributes: httpAttrs}, "test-service", "EC2")) + match, err := r1.appliesTo(trace.SamplingParameters{Attributes: httpAttrs}, "test-service", "EC2") + require.NoError(t, err) + assert.True(t, match) } // assert that matching will not happen when rules has all the HTTP attrs set as non '*' values and @@ -261,7 +268,9 @@ func TestAppliesToMatchingWithHTTPAttrs_NoSpanAttrs(t *testing.T) { }, } - assert.False(t, r1.appliesTo(trace.SamplingParameters{}, "test-service", "EC2")) + match, err := r1.appliesTo(trace.SamplingParameters{}, "test-service", "EC2") + require.NoError(t, err) + assert.False(t, match) } // assert that matching will happen when rules has all the HTTP attrs set as '*' values and @@ -278,7 +287,9 @@ func TestAppliesToMatchingWithStarHTTPAttrs_NoSpanAttrs(t *testing.T) { }, } - assert.True(t, r1.appliesTo(trace.SamplingParameters{}, "test-service", "EC2")) + match, err := r1.appliesTo(trace.SamplingParameters{}, "test-service", "EC2") + require.NoError(t, err) + assert.True(t, match) } // assert that matching will not happen when rules has some HTTP attrs set as non '*' values and @@ -295,7 +306,9 @@ func TestAppliesToMatchingWithPartialHTTPAttrs_NoSpanAttrs(t *testing.T) { }, } - assert.False(t, r1.appliesTo(trace.SamplingParameters{}, "test-service", "EC2")) + match, err := r1.appliesTo(trace.SamplingParameters{}, "test-service", "EC2") + require.NoError(t, err) + assert.False(t, match) } // assert that matching will not happen when rule and span ServiceType attr value is different. @@ -311,7 +324,9 @@ func TestAppliesToNoMatching(t *testing.T) { }, } - assert.False(t, r1.appliesTo(trace.SamplingParameters{}, "test-service", "ECS")) + match, err := r1.appliesTo(trace.SamplingParameters{}, "test-service", "ECS") + require.NoError(t, err) + assert.False(t, match) } // assert that if rules has attribute and span has those attribute with same value then matching will happen. @@ -330,7 +345,9 @@ func TestAttributeMatching(t *testing.T) { }, } - assert.True(t, r1.attributeMatching(trace.SamplingParameters{Attributes: commonLabels})) + match, err := r1.attributeMatching(trace.SamplingParameters{Attributes: commonLabels}) + require.NoError(t, err) + assert.True(t, match) } // assert that if some of the rules attributes are not present in span attributes then matching @@ -350,7 +367,9 @@ func TestNoAttributeMatching(t *testing.T) { }, } - assert.False(t, r1.attributeMatching(trace.SamplingParameters{Attributes: commonLabels})) + match, err := r1.attributeMatching(trace.SamplingParameters{Attributes: commonLabels}) + require.NoError(t, err) + assert.False(t, match) } // assert that wildcard attributes will match. @@ -369,7 +388,9 @@ func TestAttributeWildCardMatching(t *testing.T) { }, } - assert.True(t, r1.attributeMatching(trace.SamplingParameters{Attributes: commonLabels})) + match, err := r1.attributeMatching(trace.SamplingParameters{Attributes: commonLabels}) + require.NoError(t, err) + assert.True(t, match) } // assert that if rules has no attributes then matching will happen. @@ -385,11 +406,7 @@ func TestAttributeMatching_NoRuleAttrs(t *testing.T) { }, } - assert.True(t, r1.attributeMatching(trace.SamplingParameters{Attributes: commonLabels})) + match, err := r1.attributeMatching(trace.SamplingParameters{Attributes: commonLabels}) + require.NoError(t, err) + assert.True(t, match) } - - - - - - diff --git a/samplers/aws/xray/internal/util/rand.go b/samplers/aws/xray/internal/util/rand.go index 6792a14eaa9..d53fbc1adf7 100644 --- a/samplers/aws/xray/internal/util/rand.go +++ b/samplers/aws/xray/internal/util/rand.go @@ -89,26 +89,4 @@ type Rand interface { Float64() float64 } -// defaultRand is an implementation of Rand interface. -// It is safe for concurrent use by multiple goroutines. -type defaultRand struct{} - var globalRand = newGlobalRand() - -// Int63n returns, as an int64, a non-negative pseudo-random number in [0,n) -// from the default Source. -func (r *defaultRand) Int63n(n int64) int64 { - return globalRand.Int63n(n) -} - -// Intn returns, as an int, a non-negative pseudo-random number in [0,n) -// from the default Source. -func (r *defaultRand) Intn(n int) int { - return globalRand.Intn(n) -} - -// Float64 returns, as a float64, a pseudo-random number in [0.0,1.0) -// from the default Source. -func (r *defaultRand) Float64() float64 { - return globalRand.Float64() -} \ No newline at end of file diff --git a/samplers/aws/xray/internal/util/timer.go b/samplers/aws/xray/internal/util/timer.go index f758fdf24e8..0c5ff83081c 100644 --- a/samplers/aws/xray/internal/util/timer.go +++ b/samplers/aws/xray/internal/util/timer.go @@ -20,17 +20,17 @@ import ( // Ticker is the same as time.Ticker except that it has jitters. // A Ticker must be created with NewTicker. -type ticker struct { +type Ticker struct { t *time.Ticker d time.Duration jitter time.Duration } // NewTicker creates a new Ticker that will send the current time on its channel. -func NewTicker(d, jitter time.Duration) *ticker { +func NewTicker(d, jitter time.Duration) *Ticker { t := time.NewTicker(d - time.Duration(globalRand.Int63n(int64(jitter)))) - jitteredTicker := ticker{ + jitteredTicker := Ticker{ t: t, d: d, jitter: jitter, @@ -40,6 +40,6 @@ func NewTicker(d, jitter time.Duration) *ticker { } // C is channel. -func (j *ticker) C() <-chan time.Time { +func (j *Ticker) C() <-chan time.Time { return j.t.C -} \ No newline at end of file +} diff --git a/samplers/aws/xray/remote_sampler.go b/samplers/aws/xray/remote_sampler.go index df28b128a94..943ea38aea5 100644 --- a/samplers/aws/xray/remote_sampler.go +++ b/samplers/aws/xray/remote_sampler.go @@ -12,14 +12,14 @@ // See the License for the specific language governing permissions and // limitations under the License. -package main +package xray import ( "context" + "time" + "go.opentelemetry.io/contrib/samplers/aws/xray/internal" "go.opentelemetry.io/contrib/samplers/aws/xray/internal/util" - "sync" - "time" "github.com/go-logr/logr" @@ -49,8 +49,6 @@ type remoteSampler struct { // logger for logging logger logr.Logger - - mu sync.RWMutex } // Compile time assertion that remoteSampler implements the Sampler interface. @@ -70,11 +68,12 @@ func NewRemoteSampler(ctx context.Context, serviceName string, cloudPlatform str } // create manifest with config - m, err := internal.NewManifest(cfg.endpoint, cfg.logger); if err != nil { + m, err := internal.NewManifest(cfg.endpoint, cfg.logger) + if err != nil { return nil, err } - remoteSampler := &remoteSampler { + remoteSampler := &remoteSampler{ manifest: m, samplingRulesPollingInterval: cfg.samplingRulesPollingInterval, fallbackSampler: NewFallbackSampler(), @@ -94,7 +93,13 @@ func NewRemoteSampler(ctx context.Context, serviceName string, cloudPlatform str func (rs *remoteSampler) ShouldSample(parameters sdktrace.SamplingParameters) sdktrace.SamplingResult { if !rs.manifest.Expired() { // match against known rules - r, match := rs.manifest.MatchAgainstManifestRules(parameters, rs.serviceName, rs.cloudPlatform); if match { + r, match, err := rs.manifest.MatchAgainstManifestRules(parameters, rs.serviceName, rs.cloudPlatform) + if err != nil { + rs.logger.Error(err, "regexp matching error while matching span and resource attributes") + return sdktrace.SamplingResult{} + } + + if match { // remote sampling based on rule match return r.Sample(parameters, rs.manifest.Clock.Now().Unix()) } @@ -168,18 +173,3 @@ func (rs *remoteSampler) startPoller(ctx context.Context) { } }() } - -func main() { - ctx := context.Background() - rs, _ := NewRemoteSampler(ctx, "test", "test-platform") - // - //commonLabels := []attribute.KeyValue{ - // attribute.String("labelA", "chocolate"), - // attribute.String("labelB", "raspberry"), - //} - - for i := 0; i < 1000; i++ { - rs.ShouldSample(sdktrace.SamplingParameters{}) - time.Sleep(250 * time.Millisecond) - } -} diff --git a/samplers/aws/xray/remote_sampler_config.go b/samplers/aws/xray/remote_sampler_config.go index f0b4c085b03..0be2581443e 100644 --- a/samplers/aws/xray/remote_sampler_config.go +++ b/samplers/aws/xray/remote_sampler_config.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package main +package xray import ( "fmt" @@ -74,8 +74,6 @@ func newConfig(opts ...Option) *config { option(cfg) } - stdr.SetVerbosity(5) - return cfg } diff --git a/samplers/aws/xray/remote_sampler_config_test.go b/samplers/aws/xray/remote_sampler_config_test.go index 54cdda959fb..457e9fa850d 100644 --- a/samplers/aws/xray/remote_sampler_config_test.go +++ b/samplers/aws/xray/remote_sampler_config_test.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package main +package xray import ( "log" @@ -98,4 +98,4 @@ func TestValidateConfigPositiveDuration(t *testing.T) { err := validateConfig(cfg) assert.NoError(t, err) -} \ No newline at end of file +} diff --git a/samplers/aws/xray/remote_sampler_test.go b/samplers/aws/xray/remote_sampler_test.go index 913cc87613b..cea0b44dce8 100644 --- a/samplers/aws/xray/remote_sampler_test.go +++ b/samplers/aws/xray/remote_sampler_test.go @@ -12,14 +12,16 @@ // See the License for the specific language governing permissions and // limitations under the License. -package main +package xray import ( + "testing" + "github.com/stretchr/testify/assert" + "go.opentelemetry.io/contrib/samplers/aws/xray/internal" "go.opentelemetry.io/contrib/samplers/aws/xray/internal/util" sdktrace "go.opentelemetry.io/otel/sdk/trace" - "testing" ) // assert that when manifest is not expired sampling happens with 1 req/sec. @@ -52,6 +54,3 @@ func TestRemoteSamplerDescription(t *testing.T) { s := rs.Description() assert.Equal(t, s, "AwsXrayRemoteSampler{remote sampling with AWS X-Ray}") } - - - From 27567e05486f693316bc3af86bf36ccdf99d3618 Mon Sep 17 00:00:00 2001 From: bhautikpip Date: Mon, 28 Feb 2022 11:40:07 -0800 Subject: [PATCH 07/38] Relayout reservoir and rule struct to ensure int64s are 8-byte aligned --- samplers/aws/xray/internal/reservoir.go | 12 ++++++------ samplers/aws/xray/internal/rule.go | 16 ++++++++-------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/samplers/aws/xray/internal/reservoir.go b/samplers/aws/xray/internal/reservoir.go index b38995e4918..07342137675 100644 --- a/samplers/aws/xray/internal/reservoir.go +++ b/samplers/aws/xray/internal/reservoir.go @@ -21,6 +21,12 @@ import ( // reservoir represents a sampling statistics for a given rule and populate it's value from // the response getSamplingTargets API which sends information on sampling statistics real-time type reservoir struct { + // reservoir consumption for current epoch + used int64 + + // reservoir usage is reset every second + currentEpoch int64 + // quota assigned to client quota int64 @@ -35,12 +41,6 @@ type reservoir struct { // total size of reservoir capacity int64 - - // reservoir consumption for current epoch - used int64 - - // reservoir usage is reset every second - currentEpoch int64 } // expired returns true if current time is past expiration timestamp. False otherwise. diff --git a/samplers/aws/xray/internal/rule.go b/samplers/aws/xray/internal/rule.go index 10162dcaefd..94b36debbeb 100644 --- a/samplers/aws/xray/internal/rule.go +++ b/samplers/aws/xray/internal/rule.go @@ -23,14 +23,6 @@ import ( // Rule represents a sampling rule which contains rule properties and reservoir which keeps tracks of sampling statistics of a rule type Rule struct { - // reservoir has equivalent fields to store what we receive from service API getSamplingTargets - // https://docs.aws.amazon.com/xray/latest/api/API_GetSamplingTargets.html - reservoir reservoir - - // equivalent to what we receive from service API getSamplingRules - // https://docs.aws.amazon.com/cli/latest/reference/xray/get-sampling-rules.html - ruleProperties ruleProperties - // number of requests matched against specific rule matchedRequests int64 @@ -39,6 +31,14 @@ type Rule struct { // number of requests borrowed using specific rule borrowedRequests int64 + + // reservoir has equivalent fields to store what we receive from service API getSamplingTargets + // https://docs.aws.amazon.com/xray/latest/api/API_GetSamplingTargets.html + reservoir reservoir + + // equivalent to what we receive from service API getSamplingRules + // https://docs.aws.amazon.com/cli/latest/reference/xray/get-sampling-rules.html + ruleProperties ruleProperties } // stale checks if targets (sampling stats) for a given rule is expired or not From d4ee60ea3ef44585a16164f6600819200af36fda Mon Sep 17 00:00:00 2001 From: bhautikpip Date: Mon, 28 Feb 2022 12:07:03 -0800 Subject: [PATCH 08/38] removed flaky test --- samplers/aws/xray/internal/rule_test.go | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/samplers/aws/xray/internal/rule_test.go b/samplers/aws/xray/internal/rule_test.go index 4c9b8b4dc28..b07f5f3ea73 100644 --- a/samplers/aws/xray/internal/rule_test.go +++ b/samplers/aws/xray/internal/rule_test.go @@ -372,27 +372,6 @@ func TestNoAttributeMatching(t *testing.T) { assert.False(t, match) } -// assert that wildcard attributes will match. -func TestAttributeWildCardMatching(t *testing.T) { - commonLabels := []attribute.KeyValue{ - attribute.String("labelA", "chocolate"), - attribute.String("labelB", "raspberry"), - } - - r1 := Rule{ - ruleProperties: ruleProperties{ - Attributes: map[string]string{ - "labelA": "choco*", - "labelB": "rasp*", - }, - }, - } - - match, err := r1.attributeMatching(trace.SamplingParameters{Attributes: commonLabels}) - require.NoError(t, err) - assert.True(t, match) -} - // assert that if rules has no attributes then matching will happen. func TestAttributeMatching_NoRuleAttrs(t *testing.T) { commonLabels := []attribute.KeyValue{ From 02eb8a923a7c71d3048eb99de9979b5cdd4f3b0e Mon Sep 17 00:00:00 2001 From: bhautikpip Date: Mon, 28 Feb 2022 13:09:50 -0800 Subject: [PATCH 09/38] debugging upstream test failure --- samplers/aws/xray/internal/rule_test.go | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/samplers/aws/xray/internal/rule_test.go b/samplers/aws/xray/internal/rule_test.go index b07f5f3ea73..1442fb3f8c1 100644 --- a/samplers/aws/xray/internal/rule_test.go +++ b/samplers/aws/xray/internal/rule_test.go @@ -15,6 +15,7 @@ package internal import ( + "fmt" "testing" "github.com/stretchr/testify/require" @@ -347,6 +348,7 @@ func TestAttributeMatching(t *testing.T) { match, err := r1.attributeMatching(trace.SamplingParameters{Attributes: commonLabels}) require.NoError(t, err) + fmt.Println(match) assert.True(t, match) } @@ -372,6 +374,28 @@ func TestNoAttributeMatching(t *testing.T) { assert.False(t, match) } +// assert that wildcard attributes will match. +func TestAttributeWildCardMatching(t *testing.T) { + commonLabels := []attribute.KeyValue{ + attribute.String("labelA", "chocolate"), + attribute.String("labelB", "raspberry"), + } + + r1 := Rule{ + ruleProperties: ruleProperties{ + Attributes: map[string]string{ + "labelA": "choco*", + "labelB": "rasp*", + }, + }, + } + + match, err := r1.attributeMatching(trace.SamplingParameters{Attributes: commonLabels}) + require.NoError(t, err) + fmt.Println(match) + assert.True(t, match) +} + // assert that if rules has no attributes then matching will happen. func TestAttributeMatching_NoRuleAttrs(t *testing.T) { commonLabels := []attribute.KeyValue{ From edc424a66b0a2e21964d5cceace612477a089d91 Mon Sep 17 00:00:00 2001 From: bhautikpip Date: Mon, 28 Feb 2022 14:18:06 -0800 Subject: [PATCH 10/38] fix data race failures in some tests --- samplers/aws/xray/internal/manifest_test.go | 88 +++++++++++++++++++++ samplers/aws/xray/internal/rule_test.go | 53 +------------ 2 files changed, 92 insertions(+), 49 deletions(-) diff --git a/samplers/aws/xray/internal/manifest_test.go b/samplers/aws/xray/internal/manifest_test.go index 31f37ef4f59..1215724390d 100644 --- a/samplers/aws/xray/internal/manifest_test.go +++ b/samplers/aws/xray/internal/manifest_test.go @@ -23,6 +23,8 @@ import ( "os" "testing" + "go.opentelemetry.io/otel/attribute" + "github.com/go-logr/stdr" "github.com/stretchr/testify/require" @@ -127,6 +129,92 @@ func TestMatchAgainstManifestRules(t *testing.T) { assert.Equal(t, *exp, r2) } +// assert that if rules has attribute and span has those attribute with same value then matching will happen. +func TestMatchAgainstManifestRules_AttributeMatch(t *testing.T) { + commonLabels := []attribute.KeyValue{ + attribute.String("labelA", "chocolate"), + attribute.String("labelB", "raspberry"), + } + + r1 := Rule{ + ruleProperties: ruleProperties{ + RuleName: "r1", + Priority: 10000, + Host: "*", + HTTPMethod: "*", + URLPath: "*", + ReservoirSize: 60, + FixedRate: 0.5, + Version: 1, + ServiceName: "*", + ResourceARN: "*", + ServiceType: "*", + Attributes: map[string]string{ + "labelA": "chocolate", + "labelB": "raspberry", + }, + }, + reservoir: reservoir{ + expiresAt: 14050, + }, + } + + rules := []Rule{r1} + + m := &Manifest{ + Rules: rules, + } + + exp, _, err := m.MatchAgainstManifestRules(sdktrace.SamplingParameters{Attributes: commonLabels}, "test", "local") + require.NoError(t, err) + + // assert that manifest rule r1 is a match + assert.Equal(t, *exp, r1) +} + +// assert that wildcard attributes will match. +func TestMatchAgainstManifestRules_AttributeWildCardMatch(t *testing.T) { + commonLabels := []attribute.KeyValue{ + attribute.String("labelA", "chocolate"), + attribute.String("labelB", "raspberry"), + } + + r1 := Rule{ + ruleProperties: ruleProperties{ + RuleName: "r1", + Priority: 10000, + Host: "*", + HTTPMethod: "*", + URLPath: "*", + ReservoirSize: 60, + FixedRate: 0.5, + Version: 1, + ServiceName: "*", + ResourceARN: "*", + ServiceType: "*", + Attributes: map[string]string{ + "labelA": "choco*", + "labelB": "rasp*", + }, + }, + reservoir: reservoir{ + expiresAt: 14050, + }, + } + + rules := []Rule{r1} + + m := &Manifest{ + Rules: rules, + } + + exp, _, err := m.MatchAgainstManifestRules(sdktrace.SamplingParameters{Attributes: commonLabels}, "test", "local") + require.NoError(t, err) + + // assert that manifest rule r1 is a match + assert.Equal(t, *exp, r1) +} + func TestRefreshManifestRules(t *testing.T) { ctx := context.Background() diff --git a/samplers/aws/xray/internal/rule_test.go b/samplers/aws/xray/internal/rule_test.go index 1442fb3f8c1..c1f28ee15ba 100644 --- a/samplers/aws/xray/internal/rule_test.go +++ b/samplers/aws/xray/internal/rule_test.go @@ -15,7 +15,6 @@ package internal import ( - "fmt" "testing" "github.com/stretchr/testify/require" @@ -330,8 +329,8 @@ func TestAppliesToNoMatching(t *testing.T) { assert.False(t, match) } -// assert that if rules has attribute and span has those attribute with same value then matching will happen. -func TestAttributeMatching(t *testing.T) { +// assert that if rules has no attributes then matching will happen. +func TestAttributeMatching_NoRuleAttrs(t *testing.T) { commonLabels := []attribute.KeyValue{ attribute.String("labelA", "chocolate"), attribute.String("labelB", "raspberry"), @@ -339,22 +338,18 @@ func TestAttributeMatching(t *testing.T) { r1 := Rule{ ruleProperties: ruleProperties{ - Attributes: map[string]string{ - "labelA": "chocolate", - "labelB": "raspberry", - }, + Attributes: map[string]string{}, }, } match, err := r1.attributeMatching(trace.SamplingParameters{Attributes: commonLabels}) require.NoError(t, err) - fmt.Println(match) assert.True(t, match) } // assert that if some of the rules attributes are not present in span attributes then matching // will not happen. -func TestNoAttributeMatching(t *testing.T) { +func TestMatchAgainstManifestRules_NoAttributeMatch(t *testing.T) { commonLabels := []attribute.KeyValue{ attribute.String("labelA", "chocolate"), attribute.String("labelB", "raspberry"), @@ -373,43 +368,3 @@ func TestNoAttributeMatching(t *testing.T) { require.NoError(t, err) assert.False(t, match) } - -// assert that wildcard attributes will match. -func TestAttributeWildCardMatching(t *testing.T) { - commonLabels := []attribute.KeyValue{ - attribute.String("labelA", "chocolate"), - attribute.String("labelB", "raspberry"), - } - - r1 := Rule{ - ruleProperties: ruleProperties{ - Attributes: map[string]string{ - "labelA": "choco*", - "labelB": "rasp*", - }, - }, - } - - match, err := r1.attributeMatching(trace.SamplingParameters{Attributes: commonLabels}) - require.NoError(t, err) - fmt.Println(match) - assert.True(t, match) -} - -// assert that if rules has no attributes then matching will happen. -func TestAttributeMatching_NoRuleAttrs(t *testing.T) { - commonLabels := []attribute.KeyValue{ - attribute.String("labelA", "chocolate"), - attribute.String("labelB", "raspberry"), - } - - r1 := Rule{ - ruleProperties: ruleProperties{ - Attributes: map[string]string{}, - }, - } - - match, err := r1.attributeMatching(trace.SamplingParameters{Attributes: commonLabels}) - require.NoError(t, err) - assert.True(t, match) -} From 855156829d690b5e50bcdc2a6e8b95cef59a3b51 Mon Sep 17 00:00:00 2001 From: bhautikpip Date: Mon, 28 Feb 2022 16:57:03 -0800 Subject: [PATCH 11/38] fix bug in attribute matching and minor changes --- samplers/aws/xray/internal/manifest.go | 4 +- samplers/aws/xray/internal/manifest_test.go | 2 + samplers/aws/xray/internal/rule.go | 7 +++- samplers/aws/xray/internal/rule_test.go | 42 +++++++++++++++++++++ 4 files changed, 52 insertions(+), 3 deletions(-) diff --git a/samplers/aws/xray/internal/manifest.go b/samplers/aws/xray/internal/manifest.go index a4daa68e69e..f350238fa78 100644 --- a/samplers/aws/xray/internal/manifest.go +++ b/samplers/aws/xray/internal/manifest.go @@ -86,8 +86,8 @@ func (m *Manifest) MatchAgainstManifestRules(parameters sdktrace.SamplingParamet matched := false - for index, r := range m.Rules { - isRuleMatch, err := r.appliesTo(parameters, serviceName, cloudPlatform) + for index := range m.Rules { + isRuleMatch, err := m.Rules[index].appliesTo(parameters, serviceName, cloudPlatform) if err != nil { return nil, isRuleMatch, err } diff --git a/samplers/aws/xray/internal/manifest_test.go b/samplers/aws/xray/internal/manifest_test.go index 1215724390d..4e551b84d25 100644 --- a/samplers/aws/xray/internal/manifest_test.go +++ b/samplers/aws/xray/internal/manifest_test.go @@ -169,6 +169,7 @@ func TestMatchAgainstManifestRules_AttributeMatch(t *testing.T) { require.NoError(t, err) // assert that manifest rule r1 is a match + assert.Nil(t, err) assert.Equal(t, *exp, r1) } @@ -212,6 +213,7 @@ func TestMatchAgainstManifestRules_AttributeWildCardMatch(t *testing.T) { require.NoError(t, err) // assert that manifest rule r1 is a match + assert.Nil(t, err) assert.Equal(t, *exp, r1) } diff --git a/samplers/aws/xray/internal/rule.go b/samplers/aws/xray/internal/rule.go index 94b36debbeb..2c85bed019a 100644 --- a/samplers/aws/xray/internal/rule.go +++ b/samplers/aws/xray/internal/rule.go @@ -180,6 +180,7 @@ func (r *Rule) appliesTo(parameters sdktrace.SamplingParameters, serviceName str // attributeMatching performs a match on attributes set by users on AWS X-Ray console func (r *Rule) attributeMatching(parameters sdktrace.SamplingParameters) (match bool, err error) { match = false + unmatchedCounter := 0 if len(r.ruleProperties.Attributes) > 0 { for key, value := range r.ruleProperties.Attributes { for _, attrs := range parameters.Attributes { @@ -189,9 +190,13 @@ func (r *Rule) attributeMatching(parameters sdktrace.SamplingParameters) (match return false, err } } else { - match = false + unmatchedCounter++ } } + if unmatchedCounter == len(parameters.Attributes) { + return false, nil + } + unmatchedCounter = 0 } return match, nil } diff --git a/samplers/aws/xray/internal/rule_test.go b/samplers/aws/xray/internal/rule_test.go index c1f28ee15ba..93e2b49eecb 100644 --- a/samplers/aws/xray/internal/rule_test.go +++ b/samplers/aws/xray/internal/rule_test.go @@ -329,6 +329,27 @@ func TestAppliesToNoMatching(t *testing.T) { assert.False(t, match) } +// assert that if rules has attribute and span has those attribute with same value then matching will happen. +func TestAttributeMatching(t *testing.T) { + commonLabels := []attribute.KeyValue{ + attribute.String("labelA", "chocolate"), + attribute.String("labelB", "raspberry"), + } + + r1 := Rule{ + ruleProperties: ruleProperties{ + Attributes: map[string]string{ + "labelA": "chocolate", + "labelB": "raspberry", + }, + }, + } + + match, err := r1.attributeMatching(trace.SamplingParameters{Attributes: commonLabels}) + require.NoError(t, err) + assert.True(t, match) +} + // assert that if rules has no attributes then matching will happen. func TestAttributeMatching_NoRuleAttrs(t *testing.T) { commonLabels := []attribute.KeyValue{ @@ -347,6 +368,27 @@ func TestAttributeMatching_NoRuleAttrs(t *testing.T) { assert.True(t, match) } +// assert that wildcard attributes will match. +func TestAttributeWildCardMatching(t *testing.T) { + commonLabels := []attribute.KeyValue{ + attribute.String("labelA", "chocolate"), + attribute.String("labelB", "raspberry"), + } + + r1 := Rule{ + ruleProperties: ruleProperties{ + Attributes: map[string]string{ + "labelA": "choco*", + "labelB": "rasp*", + }, + }, + } + + match, err := r1.attributeMatching(trace.SamplingParameters{Attributes: commonLabels}) + require.NoError(t, err) + assert.True(t, match) +} + // assert that if some of the rules attributes are not present in span attributes then matching // will not happen. func TestMatchAgainstManifestRules_NoAttributeMatch(t *testing.T) { From 85a7548c7e239cba5be06a04e353c0ceef5635e3 Mon Sep 17 00:00:00 2001 From: bhautikpip Date: Tue, 1 Mar 2022 17:34:42 -0800 Subject: [PATCH 12/38] addressed review comments: 1 --- samplers/aws/xray/fallback_sampler.go | 2 +- samplers/aws/xray/fallback_sampler_test.go | 5 ++ samplers/aws/xray/internal/client.go | 25 ++++++--- samplers/aws/xray/internal/client_test.go | 3 +- samplers/aws/xray/internal/manifest.go | 42 ++++++--------- samplers/aws/xray/internal/manifest_test.go | 18 ++++--- samplers/aws/xray/internal/match.go | 2 +- samplers/aws/xray/internal/match_test.go | 3 +- samplers/aws/xray/internal/reservoir.go | 21 ++++---- samplers/aws/xray/internal/rule.go | 8 +-- samplers/aws/xray/internal/util/clock.go | 2 +- samplers/aws/xray/internal/util/rand.go | 59 ++------------------- samplers/aws/xray/internal/util/timer.go | 4 +- samplers/aws/xray/remote_sampler.go | 50 ++++++++++------- samplers/aws/xray/remote_sampler_config.go | 10 ++-- samplers/aws/xray/remote_sampler_test.go | 4 +- 16 files changed, 114 insertions(+), 144 deletions(-) diff --git a/samplers/aws/xray/fallback_sampler.go b/samplers/aws/xray/fallback_sampler.go index a62ad37497f..04e97ba6f0a 100644 --- a/samplers/aws/xray/fallback_sampler.go +++ b/samplers/aws/xray/fallback_sampler.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package xray +package xray // import "go.opentelemetry.io/contrib/samplers/aws/xray" import ( "sync/atomic" diff --git a/samplers/aws/xray/fallback_sampler_test.go b/samplers/aws/xray/fallback_sampler_test.go index cf2e8c4e9de..52fef3c73bf 100644 --- a/samplers/aws/xray/fallback_sampler_test.go +++ b/samplers/aws/xray/fallback_sampler_test.go @@ -43,6 +43,11 @@ func TestBorrowOnePerSecond(t *testing.T) { // assert that borrowing again is false during that second assert.False(t, borrowed) + + borrowed = fs.borrow(1500000001) + + // assert that borrowing again in next second + assert.True(t, borrowed) } // assert fallback sampling description. diff --git a/samplers/aws/xray/internal/client.go b/samplers/aws/xray/internal/client.go index 74a418215de..886337f2806 100644 --- a/samplers/aws/xray/internal/client.go +++ b/samplers/aws/xray/internal/client.go @@ -114,10 +114,14 @@ type xrayClient struct { // http client for sending sampling requests to the collector httpClient *http.Client - endpoint *url.URL + // resolved URL to call getSamplingRules API + samplingRulesURL string + + // resolved URL to call getSamplingTargets API + samplingTargetsURL string } -// newClient returns an HTTP client with proxy endpoint +// newClient returns an HTTP client with proxy endpoint. func newClient(addr string) (client *xrayClient, err error) { endpoint := "http://" + addr @@ -126,13 +130,18 @@ func newClient(addr string) (client *xrayClient, err error) { return nil, err } + // construct resolved URL for getSamplingRules and getSamplingTargets API calls + samplingRulesURL := endpointURL.String() + "/GetSamplingRules" + samplingTargetsURL := endpointURL.String() + "/SamplingTargets" + return &xrayClient{ - httpClient: &http.Client{}, - endpoint: endpointURL, + httpClient: &http.Client{}, + samplingRulesURL: samplingRulesURL, + samplingTargetsURL: samplingTargetsURL, }, nil } -// getSamplingRules calls the collector(aws proxy enabled) for sampling rules +// getSamplingRules calls the collector(aws proxy enabled) for sampling rules. func (c *xrayClient) getSamplingRules(ctx context.Context) (*getSamplingRulesOutput, error) { samplingRulesInput, err := json.Marshal(getSamplingRulesInput{}) if err != nil { @@ -140,7 +149,7 @@ func (c *xrayClient) getSamplingRules(ctx context.Context) (*getSamplingRulesOut } body := bytes.NewReader(samplingRulesInput) - req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.endpoint.String()+"/GetSamplingRules", body) + req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.samplingRulesURL, body) if err != nil { return nil, fmt.Errorf("xray client: failed to create http request: %w", err) } @@ -159,7 +168,7 @@ func (c *xrayClient) getSamplingRules(ctx context.Context) (*getSamplingRulesOut return samplingRulesOutput, nil } -// getSamplingTargets calls the collector(aws proxy enabled) for sampling targets +// getSamplingTargets calls the collector(aws proxy enabled) for sampling targets. func (c *xrayClient) getSamplingTargets(ctx context.Context, s []*samplingStatisticsDocument) (*getSamplingTargetsOutput, error) { statistics := getSamplingTargetsInput{ SamplingStatisticsDocuments: s, @@ -171,7 +180,7 @@ func (c *xrayClient) getSamplingTargets(ctx context.Context, s []*samplingStatis } body := bytes.NewReader(statisticsByte) - req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.endpoint.String()+"/SamplingTargets", body) + req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.samplingTargetsURL, body) if err != nil { return nil, fmt.Errorf("xray client: failed to create http request: %w", err) } diff --git a/samplers/aws/xray/internal/client_test.go b/samplers/aws/xray/internal/client_test.go index f6742347253..4c7a6dd224f 100644 --- a/samplers/aws/xray/internal/client_test.go +++ b/samplers/aws/xray/internal/client_test.go @@ -271,7 +271,8 @@ func TestNewClient(t *testing.T) { xrayClient, err := newClient("127.0.0.1:2020") require.NoError(t, err) - assert.Equal(t, xrayClient.endpoint.String(), "http://127.0.0.1:2020") + assert.Equal(t, xrayClient.samplingRulesURL, "http://127.0.0.1:2020/GetSamplingRules") + assert.Equal(t, xrayClient.samplingTargetsURL, "http://127.0.0.1:2020/SamplingTargets") } func TestEndpointIsNotReachable(t *testing.T) { diff --git a/samplers/aws/xray/internal/manifest.go b/samplers/aws/xray/internal/manifest.go index f350238fa78..a82df8f0853 100644 --- a/samplers/aws/xray/internal/manifest.go +++ b/samplers/aws/xray/internal/manifest.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package internal +package internal // import "go.opentelemetry.io/contrib/samplers/aws/xray/internal" import ( "context" @@ -79,7 +79,8 @@ func (m *Manifest) Expired() bool { return m.refreshedAt < m.Clock.Now().Unix()-manifestTTL } -// MatchAgainstManifestRules returns a Rule and boolean flag set as true if rule has been match against span attributes, otherwise nil and false +// MatchAgainstManifestRules returns a Rule and boolean flag set as true +// if rule has been match against span attributes, otherwise nil and false func (m *Manifest) MatchAgainstManifestRules(parameters sdktrace.SamplingParameters, serviceName string, cloudPlatform string) (*Rule, bool, error) { m.mu.RLock() defer m.mu.RUnlock() @@ -116,41 +117,41 @@ func (m *Manifest) RefreshManifestRules(ctx context.Context) (err error) { } // RefreshManifestTargets updates sampling targets (statistics) for each rule -func (m *Manifest) RefreshManifestTargets(ctx context.Context) (err error) { +func (m *Manifest) RefreshManifestTargets(ctx context.Context) (refresh bool, err error) { var manifest Manifest // deep copy centralized manifest object to temporary manifest to avoid thread safety issue m.mu.RLock() err = copier.CopyWithOption(&manifest, m, copier.Option{IgnoreEmpty: false, DeepCopy: true}) if err != nil { - return err + return false, err } m.mu.RUnlock() // generate sampling statistics based on the data in temporary manifest statistics, err := manifest.snapshots() if err != nil { - return err + return false, err } // return if no statistics to report if len(statistics) == 0 { m.logger.V(5).Info("no statistics to report and not refreshing sampling targets") - return nil + return false, nil } // get sampling targets (statistics) for every expired rule from AWS X-Ray targets, err := m.xrayClient.getSamplingTargets(ctx, statistics) if err != nil { - return fmt.Errorf("refreshTargets: error occurred while getting sampling targets: %w", err) + return false, fmt.Errorf("refreshTargets: error occurred while getting sampling targets: %w", err) } m.logger.V(5).Info("successfully fetched sampling targets") // update temporary manifest with retrieved targets (statistics) for each rule - refresh, err := manifest.updateTargets(targets) + refresh, err = manifest.updateTargets(targets) if err != nil { - return err + return refresh, err } // find next polling interval for targets @@ -164,17 +165,6 @@ func (m *Manifest) RefreshManifestTargets(ctx context.Context) (err error) { m.Rules = manifest.Rules m.mu.Unlock() - // perform out-of-band async manifest refresh if refresh is set to true - if refresh { - m.logger.V(5).Info("refreshing sampling rules out-of-band") - - go func() { - if err := m.RefreshManifestRules(ctx); err != nil { - m.logger.Error(err, "error occurred refreshing sampling rules out-of-band") - } - }() - } - return } @@ -272,8 +262,8 @@ func (m *Manifest) updateReservoir(t *samplingTargetDocument) (err error) { return fmt.Errorf("invalid sampling target for rule %s. Missing fixed rate", *t.RuleName) } - for index, rule := range m.Rules { - if rule.ruleProperties.RuleName == *t.RuleName { + for index := range m.Rules { + if m.Rules[index].ruleProperties.RuleName == *t.RuleName { m.Rules[index].reservoir.refreshedAt = m.Clock.Now().Unix() // Update non-optional attributes from response @@ -301,9 +291,9 @@ func (m *Manifest) snapshots() ([]*samplingStatisticsDocument, error) { statistics := make([]*samplingStatisticsDocument, 0, len(m.Rules)+1) // Generate sampling statistics for user-defined rules - for _, r := range m.Rules { - if r.stale(m.Clock.Now().Unix()) { - s := r.snapshot(m.Clock.Now().Unix()) + for index := range m.Rules { + if m.Rules[index].stale(m.Clock.Now().Unix()) { + s := m.Rules[index].snapshot(m.Clock.Now().Unix()) s.ClientID = m.clientID statistics = append(statistics, s) @@ -344,7 +334,7 @@ func (m *Manifest) minimumPollingInterval(targets *getSamplingTargetsOutput) (mi return minPoll } -// generateClientId generates random client ID +// generateClientID generates random client ID func generateClientID() (*string, error) { var r [12]byte diff --git a/samplers/aws/xray/internal/manifest_test.go b/samplers/aws/xray/internal/manifest_test.go index 4e551b84d25..bbfb2d04a6b 100644 --- a/samplers/aws/xray/internal/manifest_test.go +++ b/samplers/aws/xray/internal/manifest_test.go @@ -285,7 +285,7 @@ func TestRefreshManifestRules(t *testing.T) { // generate a test server so we can capture and inspect the request testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { - _, err := res.Write([]byte(body)) + _, err := res.Write(body) require.NoError(t, err) })) defer testServer.Close() @@ -408,7 +408,7 @@ func TestRefreshManifestMissingServiceName(t *testing.T) { // generate a test server so we can capture and inspect the request testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { - _, err := res.Write([]byte(body)) + _, err := res.Write(body) require.NoError(t, err) })) @@ -463,7 +463,7 @@ func TestRefreshManifestMissingRuleName(t *testing.T) { // generate a test server so we can capture and inspect the request testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { - _, err := res.Write([]byte(body)) + _, err := res.Write(body) require.NoError(t, err) })) @@ -521,7 +521,7 @@ func TestRefreshManifestIncorrectVersion(t *testing.T) { // generate a test server so we can capture and inspect the request testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { - _, err := res.Write([]byte(body)) + _, err := res.Write(body) require.NoError(t, err) })) @@ -618,7 +618,7 @@ func TestRefreshManifestAddOneInvalidRule(t *testing.T) { // generate a test server so we can capture and inspect the request testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { - _, err := res.Write([]byte(body)) + _, err := res.Write(body) require.NoError(t, err) })) defer testServer.Close() @@ -681,7 +681,8 @@ func TestRefreshManifestTarget_NoSnapShot(t *testing.T) { logger: stdr.NewWithOptions(log.New(os.Stderr, "", log.LstdFlags|log.Lshortfile), stdr.Options{LogCaller: stdr.Error}), } - err := m.RefreshManifestTargets(context.Background()) + refresh, err := m.RefreshManifestTargets(context.Background()) + assert.False(t, refresh) assert.Nil(t, err) } @@ -738,7 +739,7 @@ func TestRefreshManifestTargets(t *testing.T) { // generate a test server so we can capture and inspect the request testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { - _, err := res.Write([]byte(body)) + _, err := res.Write(body) require.NoError(t, err) })) defer testServer.Close() @@ -757,7 +758,8 @@ func TestRefreshManifestTargets(t *testing.T) { refreshedAt: 18000000, } - err = m.RefreshManifestTargets(context.Background()) + refresh, err := m.RefreshManifestTargets(context.Background()) + assert.False(t, refresh) require.NoError(t, err) // assert target updates diff --git a/samplers/aws/xray/internal/match.go b/samplers/aws/xray/internal/match.go index 5f1dbe6ee88..111f1f3efd9 100644 --- a/samplers/aws/xray/internal/match.go +++ b/samplers/aws/xray/internal/match.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package internal +package internal // import "go.opentelemetry.io/contrib/samplers/aws/xray/internal" import ( "fmt" diff --git a/samplers/aws/xray/internal/match_test.go b/samplers/aws/xray/internal/match_test.go index 39cebaeebed..66824bd19c4 100644 --- a/samplers/aws/xray/internal/match_test.go +++ b/samplers/aws/xray/internal/match_test.go @@ -19,9 +19,8 @@ import ( "math/rand" "testing" - "github.com/stretchr/testify/require" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) // assert wildcard match is positive. diff --git a/samplers/aws/xray/internal/reservoir.go b/samplers/aws/xray/internal/reservoir.go index 07342137675..0fd511c6ffe 100644 --- a/samplers/aws/xray/internal/reservoir.go +++ b/samplers/aws/xray/internal/reservoir.go @@ -12,9 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. -package internal +package internal // import "go.opentelemetry.io/contrib/samplers/aws/xray/internal" import ( + "sync" "sync/atomic" ) @@ -61,19 +62,19 @@ func (r *reservoir) borrow(now int64) bool { // Take consumes quota from reservoir, if any remains, then returns true. False otherwise. func (r *reservoir) take(now int64) bool { - cur := atomic.LoadInt64(&r.currentEpoch) - quota := atomic.LoadInt64(&r.quota) - used := atomic.LoadInt64(&r.used) + var mu sync.RWMutex + mu.Lock() + defer mu.Unlock() - if cur != now { - atomic.CompareAndSwapInt64(&r.currentEpoch, cur, now) - atomic.CompareAndSwapInt64(&r.used, used, int64(0)) - used = 0 + if r.currentEpoch != now { + r.used = 0 + r.currentEpoch = now } - if quota > used { - atomic.AddInt64(&r.used, 1) + if r.quota > r.used { + r.used++ return true } + return false } diff --git a/samplers/aws/xray/internal/rule.go b/samplers/aws/xray/internal/rule.go index 2c85bed019a..f69b563e9a7 100644 --- a/samplers/aws/xray/internal/rule.go +++ b/samplers/aws/xray/internal/rule.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package internal +package internal // import "go.opentelemetry.io/contrib/samplers/aws/xray/internal" import ( "sync/atomic" @@ -64,7 +64,8 @@ func (r *Rule) snapshot(now int64) *samplingStatisticsDocument { } } -// Sample uses sampling targets of a given rule to decide which sampling should be done and returns a SamplingResult. +// Sample uses sampling targets of a given rule to decide +// which sampling should be done and returns a SamplingResult. func (r *Rule) Sample(parameters sdktrace.SamplingParameters, now int64) sdktrace.SamplingResult { sd := sdktrace.SamplingResult{ Tracestate: trace.SpanContextFromContext(parameters.ParentContext).TraceState(), @@ -110,7 +111,8 @@ func (r *Rule) Sample(parameters sdktrace.SamplingParameters, now int64) sdktrac return sd } -// appliesTo performs a matching against rule properties to see if a given rule does match with any of the rule set on AWS X-Ray console +// appliesTo performs a matching against rule properties to see +// if a given rule does match with any of the rule set on AWS X-Ray console func (r *Rule) appliesTo(parameters sdktrace.SamplingParameters, serviceName string, cloudPlatform string) (bool, error) { var httpTarget string var httpURL string diff --git a/samplers/aws/xray/internal/util/clock.go b/samplers/aws/xray/internal/util/clock.go index 69a626b9c29..ecddcebb2d8 100644 --- a/samplers/aws/xray/internal/util/clock.go +++ b/samplers/aws/xray/internal/util/clock.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package util +package util // import "go.opentelemetry.io/contrib/samplers/aws/xray/internal/util" import ( "sync/atomic" diff --git a/samplers/aws/xray/internal/util/rand.go b/samplers/aws/xray/internal/util/rand.go index d53fbc1adf7..287006278b7 100644 --- a/samplers/aws/xray/internal/util/rand.go +++ b/samplers/aws/xray/internal/util/rand.go @@ -12,59 +12,15 @@ // See the License for the specific language governing permissions and // limitations under the License. -package util +package util // import "go.opentelemetry.io/contrib/samplers/aws/xray/internal/util" import ( crand "crypto/rand" "encoding/binary" "math/rand" - "sync" "time" ) -var _ rand.Source = (*lockedSource)(nil) -var _ rand.Source64 = (*lockedSource64)(nil) - -type lockedSource struct { - mu sync.Mutex - src rand.Source -} - -func (src *lockedSource) Int63() int64 { - src.mu.Lock() - defer src.mu.Unlock() - return src.src.Int63() -} - -func (src *lockedSource) Seed(seed int64) { - src.mu.Lock() - defer src.mu.Unlock() - src.src.Seed(seed) -} - -type lockedSource64 struct { - mu sync.Mutex - src rand.Source64 -} - -func (src *lockedSource64) Int63() int64 { - src.mu.Lock() - defer src.mu.Unlock() - return src.src.Int63() -} - -func (src *lockedSource64) Uint64() uint64 { - src.mu.Lock() - defer src.mu.Unlock() - return src.src.Uint64() -} - -func (src *lockedSource64) Seed(seed int64) { - src.mu.Lock() - defer src.mu.Unlock() - src.src.Seed(seed) -} - func newSeed() int64 { var seed int64 if err := binary.Read(crand.Reader, binary.BigEndian, &seed); err != nil { @@ -77,16 +33,7 @@ func newSeed() int64 { func newGlobalRand() *rand.Rand { src := rand.NewSource(newSeed()) if src64, ok := src.(rand.Source64); ok { - return rand.New(&lockedSource64{src: src64}) + return rand.New(src64) } - return rand.New(&lockedSource{src: src}) -} - -// Rand is an interface for a set of methods that return random value. -type Rand interface { - Int63n(n int64) int64 - Intn(n int) int - Float64() float64 + return rand.New(src) } - -var globalRand = newGlobalRand() diff --git a/samplers/aws/xray/internal/util/timer.go b/samplers/aws/xray/internal/util/timer.go index 0c5ff83081c..c6fc76ef667 100644 --- a/samplers/aws/xray/internal/util/timer.go +++ b/samplers/aws/xray/internal/util/timer.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package util +package util // import "go.opentelemetry.io/contrib/samplers/aws/xray/internal/util" import ( "time" @@ -28,7 +28,7 @@ type Ticker struct { // NewTicker creates a new Ticker that will send the current time on its channel. func NewTicker(d, jitter time.Duration) *Ticker { - t := time.NewTicker(d - time.Duration(globalRand.Int63n(int64(jitter)))) + t := time.NewTicker(d - time.Duration(newGlobalRand().Int63n(int64(jitter)))) jitteredTicker := Ticker{ t: t, diff --git a/samplers/aws/xray/remote_sampler.go b/samplers/aws/xray/remote_sampler.go index 943ea38aea5..54117efb5ea 100644 --- a/samplers/aws/xray/remote_sampler.go +++ b/samplers/aws/xray/remote_sampler.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package xray +package xray // import "go.opentelemetry.io/contrib/samplers/aws/xray" import ( "context" @@ -20,10 +20,9 @@ import ( "go.opentelemetry.io/contrib/samplers/aws/xray/internal" "go.opentelemetry.io/contrib/samplers/aws/xray/internal/util" + sdktrace "go.opentelemetry.io/otel/sdk/trace" "github.com/go-logr/logr" - - sdktrace "go.opentelemetry.io/otel/sdk/trace" ) // remoteSampler is a sampler for AWS X-Ray which polls sampling rules and sampling targets @@ -105,12 +104,13 @@ func (rs *remoteSampler) ShouldSample(parameters sdktrace.SamplingParameters) sd } } - // Use fallback sampler if manifest is expired or sampling rules does not match against manifest + // Use fallback sampler if manifest is expired + // or sampling rules does not match against manifest rs.logger.V(5).Info("span attributes does not match to the sampling rules or manifest is expired so using fallback sampling strategy") return rs.fallbackSampler.ShouldSample(parameters) } -// Description returns description of the sampler being used +// Description returns description of the sampler being used. func (rs *remoteSampler) Description() string { return "AwsXrayRemoteSampler{" + rs.getDescription() + "}" } @@ -127,7 +127,7 @@ func (rs *remoteSampler) start(ctx context.Context) { } // startPoller starts the rule and target poller in a single go routine which runs periodically -// to refresh manifest and targets +// to refresh manifest and targets. func (rs *remoteSampler) startPoller(ctx context.Context) { go func() { // jitter = 5s, default 300 seconds @@ -137,11 +137,7 @@ func (rs *remoteSampler) startPoller(ctx context.Context) { targetTicker := util.NewTicker(rs.manifest.SamplingTargetsPollingInterval, 100*time.Millisecond) // fetch sampling rules to kick start the remote sampling - if err := rs.manifest.RefreshManifestRules(ctx); err != nil { - rs.logger.Error(err, "Error occurred while refreshing sampling rules") - } else { - rs.logger.V(5).Info("Successfully fetched sampling rules") - } + rs.refreshManifest(ctx) for { select { @@ -151,11 +147,7 @@ func (rs *remoteSampler) startPoller(ctx context.Context) { } // fetch sampling rules and updates manifest - if err := rs.manifest.RefreshManifestRules(ctx); err != nil { - rs.logger.Error(err, "error occurred while refreshing sampling rules") - } else { - rs.logger.V(5).Info("successfully fetched sampling rules") - } + rs.refreshManifest(ctx) continue case _, more := <-targetTicker.C(): if !more { @@ -163,8 +155,11 @@ func (rs *remoteSampler) startPoller(ctx context.Context) { } // fetch sampling targets and updates manifest - if err := rs.manifest.RefreshManifestTargets(ctx); err != nil { - rs.logger.Error(err, "error occurred while refreshing sampling rule targets") + refresh := rs.refreshTargets(ctx) + + // out of band manifest refresh if it manifest is not updated + if refresh { + rs.refreshManifest(ctx) } continue case <-ctx.Done(): @@ -173,3 +168,22 @@ func (rs *remoteSampler) startPoller(ctx context.Context) { } }() } + +// refreshManifest refreshes the manifest retrieved via getSamplingRules API. +func (rs *remoteSampler) refreshManifest(ctx context.Context) { + if err := rs.manifest.RefreshManifestRules(ctx); err != nil { + rs.logger.Error(err, "Error occurred while refreshing sampling rules") + } else { + rs.logger.V(5).Info("Successfully fetched sampling rules") + } +} + +// refreshTarget refreshes the sampling targets in manifest retrieved via getSamplingTargets API. +func (rs *remoteSampler) refreshTargets(ctx context.Context) bool { + refresh := false + var err error + if refresh, err = rs.manifest.RefreshManifestTargets(ctx); err != nil { + rs.logger.Error(err, "error occurred while refreshing sampling rule targets") + } + return refresh +} diff --git a/samplers/aws/xray/remote_sampler_config.go b/samplers/aws/xray/remote_sampler_config.go index 0be2581443e..cb279d3fbd9 100644 --- a/samplers/aws/xray/remote_sampler_config.go +++ b/samplers/aws/xray/remote_sampler_config.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package xray +package xray // import "go.opentelemetry.io/contrib/samplers/aws/xray" import ( "fmt" @@ -33,7 +33,7 @@ const ( defaultPollingInterval = 300 ) -// SamplerOption is a function that sets config on the sampler +// Option is a function that sets config on the sampler. type Option func(options *config) type config struct { @@ -42,21 +42,21 @@ type config struct { logger logr.Logger } -// sets custom proxy endpoint +// WithEndpoint sets custom proxy endpoint. func WithEndpoint(endpoint string) Option { return func(o *config) { o.endpoint = endpoint } } -// sets polling interval for sampling rules +// WithSamplingRulesPollingInterval sets polling interval for sampling rules. func WithSamplingRulesPollingInterval(polingInterval time.Duration) Option { return func(o *config) { o.samplingRulesPollingInterval = polingInterval } } -// sets custom logging for remote sampling implementation +// WithLogger sets custom logging for remote sampling implementation. func WithLogger(l logr.Logger) Option { return func(o *config) { o.logger = l diff --git a/samplers/aws/xray/remote_sampler_test.go b/samplers/aws/xray/remote_sampler_test.go index cea0b44dce8..236b89e86f2 100644 --- a/samplers/aws/xray/remote_sampler_test.go +++ b/samplers/aws/xray/remote_sampler_test.go @@ -24,7 +24,7 @@ import ( sdktrace "go.opentelemetry.io/otel/sdk/trace" ) -// assert that when manifest is not expired sampling happens with 1 req/sec. +// TestShouldSample assert that when manifest is not expired sampling happens with 1 req/sec. func TestShouldSample(t *testing.T) { clock := &util.MockClock{ NowTime: 100, @@ -47,7 +47,7 @@ func TestShouldSample(t *testing.T) { assert.Equal(t, sd.Decision, sdktrace.RecordAndSample) } -// assert remote sampling description. +// TestRemoteSamplerDescription assert remote sampling description. func TestRemoteSamplerDescription(t *testing.T) { rs := &remoteSampler{} From 828627c843ae1a4e241df3196b89218dd494d341 Mon Sep 17 00:00:00 2001 From: bhautikpip Date: Wed, 2 Mar 2022 14:11:02 -0800 Subject: [PATCH 13/38] added use of montonic time and minor changes --- samplers/aws/xray/internal/manifest.go | 56 +++--- samplers/aws/xray/internal/manifest_test.go | 201 +++++++++----------- samplers/aws/xray/internal/reservoir.go | 5 +- samplers/aws/xray/internal/rule.go | 54 +++--- samplers/aws/xray/internal/rule_test.go | 87 +++++---- samplers/aws/xray/remote_sampler.go | 2 +- 6 files changed, 204 insertions(+), 201 deletions(-) diff --git a/samplers/aws/xray/internal/manifest.go b/samplers/aws/xray/internal/manifest.go index a82df8f0853..80873a3bd9f 100644 --- a/samplers/aws/xray/internal/manifest.go +++ b/samplers/aws/xray/internal/manifest.go @@ -30,8 +30,6 @@ import ( sdktrace "go.opentelemetry.io/otel/sdk/trace" ) -const defaultInterval = int64(10) - const manifestTTL = 3600 // Manifest represents a full sampling ruleset and provides @@ -39,7 +37,7 @@ const manifestTTL = 3600 type Manifest struct { Rules []Rule SamplingTargetsPollingInterval time.Duration - refreshedAt int64 + refreshedAt time.Time xrayClient *xrayClient clientID *string logger logr.Logger @@ -76,26 +74,28 @@ func (m *Manifest) Expired() bool { m.mu.RLock() defer m.mu.RUnlock() - return m.refreshedAt < m.Clock.Now().Unix()-manifestTTL + now := time.Unix(m.Clock.Now().Unix()-manifestTTL, 0) + return now.After(m.refreshedAt) } // MatchAgainstManifestRules returns a Rule and boolean flag set as true // if rule has been match against span attributes, otherwise nil and false func (m *Manifest) MatchAgainstManifestRules(parameters sdktrace.SamplingParameters, serviceName string, cloudPlatform string) (*Rule, bool, error) { m.mu.RLock() - defer m.mu.RUnlock() + rules := m.Rules + m.mu.RUnlock() matched := false - for index := range m.Rules { - isRuleMatch, err := m.Rules[index].appliesTo(parameters, serviceName, cloudPlatform) + for index := range rules { + isRuleMatch, err := rules[index].appliesTo(parameters, serviceName, cloudPlatform) if err != nil { return nil, isRuleMatch, err } if isRuleMatch { matched = true - return &m.Rules[index], matched, nil + return &rules[index], matched, nil } } @@ -155,9 +155,9 @@ func (m *Manifest) RefreshManifestTargets(ctx context.Context) (refresh bool, er } // find next polling interval for targets - minPoll := manifest.minimumPollingInterval(targets) + minPoll := manifest.minimumPollingInterval() if minPoll > 0 { - m.SamplingTargetsPollingInterval = time.Duration(minPoll) * time.Second + m.SamplingTargetsPollingInterval = minPoll } // update centralized manifest object @@ -193,14 +193,13 @@ func (m *Manifest) updateRules(rules *getSamplingRulesOutput) { m.mu.Lock() m.Rules = tempManifest.Rules - m.refreshedAt = m.Clock.Now().Unix() + m.refreshedAt = m.Clock.Now() m.mu.Unlock() } func (m *Manifest) createRule(ruleProp ruleProperties) { cr := reservoir{ capacity: ruleProp.ReservoirSize, - interval: defaultInterval, } csr := Rule{ @@ -245,7 +244,10 @@ func (m *Manifest) updateTargets(targets *getSamplingTargetsOutput) (refresh boo // set refresh flag if modifiedAt timestamp from remote is greater than ours if remote := targets.LastRuleModification; remote != nil { - if int64(*remote) >= m.refreshedAt { + // convert unix timestamp to time.Time + lastRuleModification := time.Unix(int64(*targets.LastRuleModification), 0) + + if lastRuleModification.After(m.refreshedAt) { refresh = true } } @@ -264,7 +266,7 @@ func (m *Manifest) updateReservoir(t *samplingTargetDocument) (err error) { for index := range m.Rules { if m.Rules[index].ruleProperties.RuleName == *t.RuleName { - m.Rules[index].reservoir.refreshedAt = m.Clock.Now().Unix() + m.Rules[index].reservoir.refreshedAt = m.Clock.Now() // Update non-optional attributes from response m.Rules[index].ruleProperties.FixedRate = *t.FixedRate @@ -277,7 +279,7 @@ func (m *Manifest) updateReservoir(t *samplingTargetDocument) (err error) { m.Rules[index].reservoir.expiresAt = int64(*t.ReservoirQuotaTTL) } if t.Interval != nil { - m.Rules[index].reservoir.interval = *t.Interval + m.Rules[index].reservoir.interval = time.Duration(*t.Interval) } } } @@ -292,8 +294,8 @@ func (m *Manifest) snapshots() ([]*samplingStatisticsDocument, error) { // Generate sampling statistics for user-defined rules for index := range m.Rules { - if m.Rules[index].stale(m.Clock.Now().Unix()) { - s := m.Rules[index].snapshot(m.Clock.Now().Unix()) + if m.Rules[index].stale(m.Clock.Now()) { + s := m.Rules[index].snapshot(m.Clock.Now()) s.ClientID = m.clientID statistics = append(statistics, s) @@ -317,21 +319,19 @@ func (m *Manifest) sort() { } // minimumPollingInterval finds the minimum interval amongst all the targets -func (m *Manifest) minimumPollingInterval(targets *getSamplingTargetsOutput) (minPoll int64) { - minPoll = 0 - for _, t := range targets.SamplingTargetDocuments { - if t.Interval != nil { - if minPoll == 0 { - minPoll = *t.Interval - } else { - if minPoll > *t.Interval { - minPoll = *t.Interval - } +func (m *Manifest) minimumPollingInterval() (minPoll time.Duration) { + minPoll = time.Duration(0) + for _, rules := range m.Rules { + if minPoll == 0 { + minPoll = rules.reservoir.interval + } else { + if minPoll >= rules.reservoir.interval { + minPoll = rules.reservoir.interval } } } - return minPoll + return minPoll * time.Second } // generateClientID generates random client ID diff --git a/samplers/aws/xray/internal/manifest_test.go b/samplers/aws/xray/internal/manifest_test.go index bbfb2d04a6b..75cad108a0e 100644 --- a/samplers/aws/xray/internal/manifest_test.go +++ b/samplers/aws/xray/internal/manifest_test.go @@ -22,6 +22,7 @@ import ( "net/url" "os" "testing" + "time" "go.opentelemetry.io/otel/attribute" @@ -54,9 +55,10 @@ func TestExpiredManifest(t *testing.T) { NowTime: 10000, } + refreshedAt := time.Unix(3700, 0) m := &Manifest{ Clock: clock, - refreshedAt: 3700, + refreshedAt: refreshedAt, } assert.True(t, m.Expired()) @@ -321,7 +323,6 @@ func TestRefreshManifestRules(t *testing.T) { Attributes: map[string]string{}, }, reservoir: reservoir{ - interval: defaultInterval, capacity: int64(60), }, } @@ -342,7 +343,6 @@ func TestRefreshManifestRules(t *testing.T) { Attributes: map[string]string{}, }, reservoir: reservoir{ - interval: defaultInterval, capacity: int64(3), }, } @@ -363,7 +363,6 @@ func TestRefreshManifestRules(t *testing.T) { Attributes: map[string]string{}, }, reservoir: reservoir{ - interval: defaultInterval, capacity: int64(100), }, } @@ -611,7 +610,6 @@ func TestRefreshManifestAddOneInvalidRule(t *testing.T) { Attributes: map[string]string{}, }, reservoir: reservoir{ - interval: defaultInterval, capacity: int64(60), }, } @@ -667,10 +665,11 @@ func TestRefreshManifestTarget_NoSnapShot(t *testing.T) { Attributes: map[string]string{}, }, reservoir: reservoir{ - interval: defaultInterval, capacity: int64(100), }, - matchedRequests: int64(0), + samplingStatistics: samplingStatistics{ + matchedRequests: int64(0), + }, } rules := []Rule{r1} @@ -729,10 +728,11 @@ func TestRefreshManifestTargets(t *testing.T) { Attributes: map[string]string{}, }, reservoir: reservoir{ - interval: defaultInterval, capacity: int64(100), }, - matchedRequests: int64(5), + samplingStatistics: samplingStatistics{ + matchedRequests: int64(5), + }, } rules := []Rule{r1} @@ -749,13 +749,13 @@ func TestRefreshManifestTargets(t *testing.T) { client, err := newClient(u.Host) require.NoError(t, err) - + refreshedAt := time.Unix(18000000, 0) m := &Manifest{ Rules: rules, Clock: clock, logger: stdr.NewWithOptions(log.New(os.Stderr, "", log.LstdFlags|log.Lshortfile), stdr.Options{LogCaller: stdr.Error}), xrayClient: client, - refreshedAt: 18000000, + refreshedAt: refreshedAt, } refresh, err := m.RefreshManifestTargets(context.Background()) @@ -766,7 +766,7 @@ func TestRefreshManifestTargets(t *testing.T) { assert.Equal(t, m.Rules[0].ruleProperties.FixedRate, 0.06) assert.Equal(t, m.Rules[0].reservoir.quota, int64(23)) assert.Equal(t, m.Rules[0].reservoir.expiresAt, int64(15000000)) - assert.Equal(t, m.Rules[0].reservoir.interval, int64(25)) + assert.Equal(t, m.Rules[0].reservoir.interval, time.Duration(25)) } // assert that a valid sampling target updates its rule. @@ -792,6 +792,7 @@ func TestUpdateTargets(t *testing.T) { SamplingTargetDocuments: []*samplingTargetDocument{&st}, } + refreshedAt1 := time.Unix(1499999990, 0) // sampling rule about to be updated with new target r1 := Rule{ ruleProperties: ruleProperties{ @@ -800,7 +801,7 @@ func TestUpdateTargets(t *testing.T) { }, reservoir: reservoir{ quota: 8, - refreshedAt: 1499999990, + refreshedAt: refreshedAt1, expiresAt: 1500000010, capacity: 50, used: 7, @@ -821,6 +822,7 @@ func TestUpdateTargets(t *testing.T) { // assert refresh is false assert.False(t, refresh) + refreshedAt2 := time.Unix(1500000000, 0) // Updated sampling rule exp := Rule{ ruleProperties: ruleProperties{ @@ -829,7 +831,7 @@ func TestUpdateTargets(t *testing.T) { }, reservoir: reservoir{ quota: 10, - refreshedAt: 1500000000, + refreshedAt: refreshedAt2, expiresAt: 1500000060, capacity: 50, used: 7, @@ -867,6 +869,7 @@ func TestUpdateTargetsRefreshFlagTest(t *testing.T) { LastRuleModification: &targetLastRuleModifiedTime, } + refreshedAt1 := time.Unix(1499999990, 0) // sampling rule about to be updated with new target r1 := Rule{ ruleProperties: ruleProperties{ @@ -875,7 +878,7 @@ func TestUpdateTargetsRefreshFlagTest(t *testing.T) { }, reservoir: reservoir{ quota: 8, - refreshedAt: 1499999990, + refreshedAt: refreshedAt1, expiresAt: 1500000010, capacity: 50, used: 7, @@ -887,7 +890,7 @@ func TestUpdateTargetsRefreshFlagTest(t *testing.T) { m := &Manifest{ Rules: rules, - refreshedAt: clock.Now().Unix(), + refreshedAt: clock.Now(), Clock: clock, } @@ -897,6 +900,7 @@ func TestUpdateTargetsRefreshFlagTest(t *testing.T) { // assert refresh is false assert.True(t, refresh) + refreshedAt2 := time.Unix(1500000000, 0) // Updated sampling rule exp := Rule{ ruleProperties: ruleProperties{ @@ -905,7 +909,7 @@ func TestUpdateTargetsRefreshFlagTest(t *testing.T) { }, reservoir: reservoir{ quota: 10, - refreshedAt: 1500000000, + refreshedAt: refreshedAt2, expiresAt: 1500000060, capacity: 50, used: 7, @@ -994,6 +998,7 @@ func TestUpdateReservoir(t *testing.T) { RuleName: &name, } + refreshedAt1 := time.Unix(1499999990, 0) // manifest only has rule r2 but not rule with r1 which targets just received r1 := Rule{ ruleProperties: ruleProperties{ @@ -1002,7 +1007,7 @@ func TestUpdateReservoir(t *testing.T) { }, reservoir: reservoir{ quota: 8, - refreshedAt: 1499999990, + refreshedAt: refreshedAt1, expiresAt: 1500000010, capacity: 50, used: 7, @@ -1035,6 +1040,7 @@ func TestUpdateReservoirMissingFixedRate(t *testing.T) { RuleName: &name, } + refreshedAt1 := time.Unix(1499999990, 0) // manifest rule which we're trying to update with above target st r1 := Rule{ ruleProperties: ruleProperties{ @@ -1043,7 +1049,7 @@ func TestUpdateReservoirMissingFixedRate(t *testing.T) { }, reservoir: reservoir{ quota: 8, - refreshedAt: 1499999990, + refreshedAt: refreshedAt1, expiresAt: 1500000010, capacity: 50, used: 7, @@ -1073,6 +1079,7 @@ func TestUpdateReservoirMissingRuleName(t *testing.T) { FixedRate: &rate, } + refreshedAt1 := time.Unix(1499999990, 0) // manifest rule which we're trying to update with above target st r1 := Rule{ ruleProperties: ruleProperties{ @@ -1081,7 +1088,7 @@ func TestUpdateReservoirMissingRuleName(t *testing.T) { }, reservoir: reservoir{ quota: 8, - refreshedAt: 1499999990, + refreshedAt: refreshedAt1, expiresAt: 1500000010, capacity: 50, used: 7, @@ -1105,7 +1112,7 @@ func TestSnapshots(t *testing.T) { NowTime: 1500000000, } - time := clock.Now().Unix() + time1 := clock.Now().Unix() name1 := "r1" requests1 := int64(1000) @@ -1118,9 +1125,11 @@ func TestSnapshots(t *testing.T) { reservoir: reservoir{ interval: 10, }, - matchedRequests: requests1, - sampledRequests: sampled1, - borrowedRequests: borrowed1, + samplingStatistics: samplingStatistics{ + matchedRequests: requests1, + sampledRequests: sampled1, + borrowedRequests: borrowed1, + }, } name2 := "r2" @@ -1134,9 +1143,11 @@ func TestSnapshots(t *testing.T) { reservoir: reservoir{ interval: 10, }, - matchedRequests: requests2, - sampledRequests: sampled2, - borrowedRequests: borrowed2, + samplingStatistics: samplingStatistics{ + matchedRequests: requests2, + sampledRequests: sampled2, + borrowedRequests: borrowed2, + }, } rules := []Rule{r1, r2} @@ -1155,7 +1166,7 @@ func TestSnapshots(t *testing.T) { RuleName: &name1, SampledCount: &sampled1, BorrowCount: &borrowed1, - Timestamp: &time, + Timestamp: &time1, } ss2 := samplingStatisticsDocument{ @@ -1164,7 +1175,7 @@ func TestSnapshots(t *testing.T) { RuleName: &name2, SampledCount: &sampled2, BorrowCount: &borrowed2, - Timestamp: &time, + Timestamp: &time1, } statistics, err := m.snapshots() @@ -1185,32 +1196,36 @@ func TestMixedSnapshots(t *testing.T) { } id := "c1" - time := clock.Now().Unix() + time1 := clock.Now().Unix() // stale and active rule name1 := "r1" requests1 := int64(1000) sampled1 := int64(100) - borrows1 := int64(5) + borrowed1 := int64(5) + refreshedAt1 := time.Unix(1499999970, 0) r1 := Rule{ ruleProperties: ruleProperties{ RuleName: name1, }, reservoir: reservoir{ interval: 20, - refreshedAt: 1499999980, + refreshedAt: refreshedAt1, + }, + samplingStatistics: samplingStatistics{ + matchedRequests: requests1, + sampledRequests: sampled1, + borrowedRequests: borrowed1, }, - matchedRequests: requests1, - sampledRequests: sampled1, - borrowedRequests: borrows1, } + refreshedAt2 := time.Unix(1499999990, 0) // fresh and inactive rule name2 := "r2" requests2 := int64(0) sampled2 := int64(0) - borrows2 := int64(0) + borrowed2 := int64(0) r2 := Rule{ ruleProperties: ruleProperties{ @@ -1218,18 +1233,21 @@ func TestMixedSnapshots(t *testing.T) { }, reservoir: reservoir{ interval: 20, - refreshedAt: 1499999990, + refreshedAt: refreshedAt2, + }, + samplingStatistics: samplingStatistics{ + matchedRequests: requests2, + sampledRequests: sampled2, + borrowedRequests: borrowed2, }, - matchedRequests: requests2, - sampledRequests: sampled2, - borrowedRequests: borrows2, } + refreshedAt3 := time.Unix(1499999990, 0) // fresh rule name3 := "r3" requests3 := int64(1000) sampled3 := int64(100) - borrows3 := int64(5) + borrowed3 := int64(5) r3 := Rule{ ruleProperties: ruleProperties{ @@ -1237,11 +1255,13 @@ func TestMixedSnapshots(t *testing.T) { }, reservoir: reservoir{ interval: 20, - refreshedAt: 1499999990, + refreshedAt: refreshedAt3, + }, + samplingStatistics: samplingStatistics{ + matchedRequests: requests3, + sampledRequests: sampled3, + borrowedRequests: borrowed3, }, - matchedRequests: requests3, - sampledRequests: sampled3, - borrowedRequests: borrows3, } rules := []Rule{r1, r2, r3} @@ -1257,16 +1277,13 @@ func TestMixedSnapshots(t *testing.T) { RequestCount: &requests1, RuleName: &name1, SampledCount: &sampled1, - BorrowCount: &borrows1, - Timestamp: &time, + BorrowCount: &borrowed1, + Timestamp: &time1, } statistics, err := m.snapshots() require.NoError(t, err) - // match time - *statistics[0].Timestamp = 1500000000 - // assert that only inactive rules are added to the statistics assert.Equal(t, 1, len(statistics)) assert.Equal(t, ss1, *statistics[0]) @@ -1352,86 +1369,44 @@ func TestSortBasedOnRuleName(t *testing.T) { // asserts the minimum value of all the targets. func TestMinPollInterval(t *testing.T) { - interval1 := int64(10) - t1 := &samplingTargetDocument{ - Interval: &interval1, - } - - interval2 := int64(5) - t2 := &samplingTargetDocument{ - Interval: &interval2, - } - - interval3 := int64(25) - t3 := &samplingTargetDocument{ - Interval: &interval3, - } + r1 := Rule{reservoir: reservoir{interval: 10}} + r2 := Rule{reservoir: reservoir{interval: 5}} + r3 := Rule{reservoir: reservoir{interval: 25}} - targets := &getSamplingTargetsOutput{ - SamplingTargetDocuments: []*samplingTargetDocument{t1, t2, t3}, - } - - m := &Manifest{} + rules := []Rule{r1, r2, r3} + m := &Manifest{Rules: rules} - minPoll := m.minimumPollingInterval(targets) + minPoll := m.minimumPollingInterval() - assert.Equal(t, int64(5), minPoll) + assert.Equal(t, 5*time.Second, minPoll) } // asserts the minimum value of all the targets when some targets has 0 interval. func TestMinPollIntervalZeroCase(t *testing.T) { - interval1 := int64(0) - t1 := &samplingTargetDocument{ - Interval: &interval1, - } - - interval2 := int64(0) - t2 := &samplingTargetDocument{ - Interval: &interval2, - } - - interval3 := int64(5) - t3 := &samplingTargetDocument{ - Interval: &interval3, - } - - targets := &getSamplingTargetsOutput{ - SamplingTargetDocuments: []*samplingTargetDocument{t1, t2, t3}, - } + r1 := Rule{reservoir: reservoir{interval: 0}} + r2 := Rule{reservoir: reservoir{interval: 0}} + r3 := Rule{reservoir: reservoir{interval: 5}} - m := &Manifest{} + rules := []Rule{r1, r2, r3} + m := &Manifest{Rules: rules} - minPoll := m.minimumPollingInterval(targets) + minPoll := m.minimumPollingInterval() - assert.Equal(t, int64(5), minPoll) + assert.Equal(t, 5*time.Second, minPoll) } // asserts the minimum value of all the targets when some targets has negative interval. func TestMinPollIntervalNegativeCase(t *testing.T) { - interval1 := int64(-5) - t1 := &samplingTargetDocument{ - Interval: &interval1, - } - - interval2 := int64(0) - t2 := &samplingTargetDocument{ - Interval: &interval2, - } - - interval3 := int64(0) - t3 := &samplingTargetDocument{ - Interval: &interval3, - } + r1 := Rule{reservoir: reservoir{interval: -5}} + r2 := Rule{reservoir: reservoir{interval: 0}} + r3 := Rule{reservoir: reservoir{interval: 0}} - targets := &getSamplingTargetsOutput{ - SamplingTargetDocuments: []*samplingTargetDocument{t1, t2, t3}, - } - - m := &Manifest{} + rules := []Rule{r1, r2, r3} + m := &Manifest{Rules: rules} - minPoll := m.minimumPollingInterval(targets) + minPoll := m.minimumPollingInterval() - assert.Equal(t, int64(-5), minPoll) + assert.Equal(t, -5*time.Second, minPoll) } // assert that able to successfully generate the client ID. diff --git a/samplers/aws/xray/internal/reservoir.go b/samplers/aws/xray/internal/reservoir.go index 0fd511c6ffe..586dd4a3ce4 100644 --- a/samplers/aws/xray/internal/reservoir.go +++ b/samplers/aws/xray/internal/reservoir.go @@ -17,6 +17,7 @@ package internal // import "go.opentelemetry.io/contrib/samplers/aws/xray/intern import ( "sync" "sync/atomic" + "time" ) // reservoir represents a sampling statistics for a given rule and populate it's value from @@ -32,13 +33,13 @@ type reservoir struct { quota int64 // quota refresh timestamp - refreshedAt int64 + refreshedAt time.Time // quota expiration timestamp expiresAt int64 // polling interval for quota - interval int64 + interval time.Duration // total size of reservoir capacity int64 diff --git a/samplers/aws/xray/internal/rule.go b/samplers/aws/xray/internal/rule.go index f69b563e9a7..8dda53f418c 100644 --- a/samplers/aws/xray/internal/rule.go +++ b/samplers/aws/xray/internal/rule.go @@ -16,6 +16,7 @@ package internal // import "go.opentelemetry.io/contrib/samplers/aws/xray/intern import ( "sync/atomic" + "time" sdktrace "go.opentelemetry.io/otel/sdk/trace" "go.opentelemetry.io/otel/trace" @@ -23,14 +24,7 @@ import ( // Rule represents a sampling rule which contains rule properties and reservoir which keeps tracks of sampling statistics of a rule type Rule struct { - // number of requests matched against specific rule - matchedRequests int64 - - // number of requests sampled using specific rule - sampledRequests int64 - - // number of requests borrowed using specific rule - borrowedRequests int64 + samplingStatistics samplingStatistics // reservoir has equivalent fields to store what we receive from service API getSamplingTargets // https://docs.aws.amazon.com/xray/latest/api/API_GetSamplingTargets.html @@ -41,43 +35,55 @@ type Rule struct { ruleProperties ruleProperties } +type samplingStatistics struct { + // number of requests matched against specific rule + matchedRequests int64 + + // number of requests sampled using specific rule + sampledRequests int64 + + // number of requests borrowed using specific rule + borrowedRequests int64 +} + // stale checks if targets (sampling stats) for a given rule is expired or not -func (r *Rule) stale(now int64) bool { - return r.matchedRequests != 0 && now >= r.reservoir.refreshedAt+r.reservoir.interval +func (r *Rule) stale(now time.Time) bool { + reservoirRefreshTime := r.reservoir.refreshedAt.Add(time.Duration(r.reservoir.interval) * time.Second) + return r.samplingStatistics.matchedRequests != 0 && now.After(reservoirRefreshTime) } // snapshot takes a snapshot of the sampling statistics counters, returning // samplingStatisticsDocument. It also resets statistics counters. -func (r *Rule) snapshot(now int64) *samplingStatisticsDocument { +func (r *Rule) snapshot(now time.Time) *samplingStatisticsDocument { name := r.ruleProperties.RuleName - requests, sampled, borrowed := r.matchedRequests, r.sampledRequests, r.borrowedRequests + requests, sampled, borrowed := r.samplingStatistics.matchedRequests, r.samplingStatistics.sampledRequests, r.samplingStatistics.borrowedRequests // reset counters - r.matchedRequests, r.sampledRequests, r.borrowedRequests = 0, 0, 0 - + r.samplingStatistics.matchedRequests, r.samplingStatistics.sampledRequests, r.samplingStatistics.borrowedRequests = 0, 0, 0 + timeStamp := now.Unix() return &samplingStatisticsDocument{ RequestCount: &requests, SampledCount: &sampled, BorrowCount: &borrowed, RuleName: &name, - Timestamp: &now, + Timestamp: &timeStamp, } } // Sample uses sampling targets of a given rule to decide // which sampling should be done and returns a SamplingResult. -func (r *Rule) Sample(parameters sdktrace.SamplingParameters, now int64) sdktrace.SamplingResult { +func (r *Rule) Sample(parameters sdktrace.SamplingParameters, now time.Time) sdktrace.SamplingResult { sd := sdktrace.SamplingResult{ Tracestate: trace.SpanContextFromContext(parameters.ParentContext).TraceState(), } - atomic.AddInt64(&r.matchedRequests, int64(1)) + atomic.AddInt64(&r.samplingStatistics.matchedRequests, int64(1)) // fallback sampling logic if quota for a given rule is expired - if r.reservoir.expired(now) { + if r.reservoir.expired(now.Unix()) { // borrowing one request every second - if r.reservoir.borrow(now) { - atomic.AddInt64(&r.borrowedRequests, int64(1)) + if r.reservoir.borrow(now.Unix()) { + atomic.AddInt64(&r.samplingStatistics.borrowedRequests, int64(1)) sd.Decision = sdktrace.RecordAndSample return sd @@ -87,15 +93,15 @@ func (r *Rule) Sample(parameters sdktrace.SamplingParameters, now int64) sdktrac sd = sdktrace.TraceIDRatioBased(r.ruleProperties.FixedRate).ShouldSample(parameters) if sd.Decision == sdktrace.RecordAndSample { - atomic.AddInt64(&r.sampledRequests, int64(1)) + atomic.AddInt64(&r.samplingStatistics.sampledRequests, int64(1)) } return sd } // Take from reservoir quota, if quota is available for that second - if r.reservoir.take(now) { - atomic.AddInt64(&r.sampledRequests, int64(1)) + if r.reservoir.take(now.Unix()) { + atomic.AddInt64(&r.samplingStatistics.sampledRequests, int64(1)) sd.Decision = sdktrace.RecordAndSample return sd @@ -105,7 +111,7 @@ func (r *Rule) Sample(parameters sdktrace.SamplingParameters, now int64) sdktrac sd = sdktrace.TraceIDRatioBased(r.ruleProperties.FixedRate).ShouldSample(parameters) if sd.Decision == sdktrace.RecordAndSample { - atomic.AddInt64(&r.sampledRequests, int64(1)) + atomic.AddInt64(&r.samplingStatistics.sampledRequests, int64(1)) } return sd diff --git a/samplers/aws/xray/internal/rule_test.go b/samplers/aws/xray/internal/rule_test.go index 93e2b49eecb..ad60dcf0773 100644 --- a/samplers/aws/xray/internal/rule_test.go +++ b/samplers/aws/xray/internal/rule_test.go @@ -16,6 +16,7 @@ package internal import ( "testing" + "time" "github.com/stretchr/testify/require" @@ -27,43 +28,55 @@ import ( // assert that rule is active but stale due to quota is expired. func TestStaleRule(t *testing.T) { + refreshedAt := time.Unix(1500000000, 0) r1 := Rule{ - matchedRequests: 5, + samplingStatistics: samplingStatistics{ + matchedRequests: 5, + }, reservoir: reservoir{ - refreshedAt: 1500000000, + refreshedAt: refreshedAt, interval: 10, }, } - s := r1.stale(1500000010) + now := time.Unix(1500000020, 0) + s := r1.stale(now) assert.True(t, s) } // assert that rule is active and not stale. func TestFreshRule(t *testing.T) { + refreshedAt := time.Unix(1500000000, 0) r1 := Rule{ - matchedRequests: 5, + samplingStatistics: samplingStatistics{ + matchedRequests: 5, + }, reservoir: reservoir{ - refreshedAt: 1500000000, + refreshedAt: refreshedAt, interval: 10, }, } - s := r1.stale(1500000009) + now := time.Unix(1500000009, 0) + s := r1.stale(now) assert.False(t, s) } // assert that rule is inactive but not stale. func TestInactiveRule(t *testing.T) { + refreshedAt := time.Unix(1500000000, 0) r1 := Rule{ - matchedRequests: 0, + samplingStatistics: samplingStatistics{ + matchedRequests: 0, + }, reservoir: reservoir{ - refreshedAt: 1500000000, + refreshedAt: refreshedAt, interval: 10, }, } - s := r1.stale(1500000011) + now := time.Unix(1500000011, 0) + s := r1.stale(now) assert.False(t, s) } @@ -73,17 +86,20 @@ func TestSnapshot(t *testing.T) { ruleProperties: ruleProperties{ RuleName: "r1", }, - matchedRequests: 100, - sampledRequests: 12, - borrowedRequests: 2, + samplingStatistics: samplingStatistics{ + matchedRequests: 100, + sampledRequests: 12, + borrowedRequests: 2, + }, } - ss := r1.snapshot(1500000000) + now := time.Unix(1500000000, 0) + ss := r1.snapshot(now) // assert counters were reset - assert.Equal(t, int64(0), r1.matchedRequests) - assert.Equal(t, int64(0), r1.sampledRequests) - assert.Equal(t, int64(0), r1.borrowedRequests) + assert.Equal(t, int64(0), r1.samplingStatistics.matchedRequests) + assert.Equal(t, int64(0), r1.samplingStatistics.sampledRequests) + assert.Equal(t, int64(0), r1.samplingStatistics.borrowedRequests) // assert on SamplingStatistics counters assert.Equal(t, int64(100), *ss.RequestCount) @@ -107,12 +123,13 @@ func TestExpiredReservoirBorrowSample(t *testing.T) { }, } - sd := r1.Sample(trace.SamplingParameters{}, 1500000062) + now := time.Unix(1500000062, 0) + sd := r1.Sample(trace.SamplingParameters{}, now) assert.Equal(t, trace.RecordAndSample, sd.Decision) - assert.Equal(t, int64(1), r1.borrowedRequests) - assert.Equal(t, int64(0), r1.sampledRequests) - assert.Equal(t, int64(1), r1.matchedRequests) + assert.Equal(t, int64(1), r1.samplingStatistics.borrowedRequests) + assert.Equal(t, int64(0), r1.samplingStatistics.sampledRequests) + assert.Equal(t, int64(1), r1.samplingStatistics.matchedRequests) assert.Equal(t, int64(0), r1.reservoir.used) } @@ -131,11 +148,12 @@ func TestExpiredReservoirTraceIDRationBasedSample(t *testing.T) { }, } - r1.Sample(trace.SamplingParameters{}, 1500000061) + now := time.Unix(1500000061, 0) + r1.Sample(trace.SamplingParameters{}, now) - assert.Equal(t, int64(0), r1.borrowedRequests) - assert.Equal(t, int64(1), r1.sampledRequests) - assert.Equal(t, int64(1), r1.matchedRequests) + assert.Equal(t, int64(0), r1.samplingStatistics.borrowedRequests) + assert.Equal(t, int64(1), r1.samplingStatistics.sampledRequests) + assert.Equal(t, int64(1), r1.samplingStatistics.matchedRequests) assert.Equal(t, int64(0), r1.reservoir.used) } @@ -153,12 +171,13 @@ func TestConsumeFromQuotaSample(t *testing.T) { }, } - sd := r1.Sample(trace.SamplingParameters{}, 1500000000) + now := time.Unix(1500000000, 0) + sd := r1.Sample(trace.SamplingParameters{}, now) assert.Equal(t, trace.RecordAndSample, sd.Decision) - assert.Equal(t, int64(1), r1.sampledRequests) - assert.Equal(t, int64(0), r1.borrowedRequests) - assert.Equal(t, int64(1), r1.matchedRequests) + assert.Equal(t, int64(1), r1.samplingStatistics.sampledRequests) + assert.Equal(t, int64(0), r1.samplingStatistics.borrowedRequests) + assert.Equal(t, int64(1), r1.samplingStatistics.matchedRequests) assert.Equal(t, int64(1), r1.reservoir.used) } @@ -177,12 +196,13 @@ func TestTraceIDRatioBasedSampler(t *testing.T) { }, } - sd := r1.Sample(trace.SamplingParameters{}, 1500000000) + now := time.Unix(1500000000, 0) + sd := r1.Sample(trace.SamplingParameters{}, now) assert.NotEmpty(t, sd.Decision) - assert.Equal(t, int64(1), r1.sampledRequests) - assert.Equal(t, int64(0), r1.borrowedRequests) - assert.Equal(t, int64(1), r1.matchedRequests) + assert.Equal(t, int64(1), r1.samplingStatistics.sampledRequests) + assert.Equal(t, int64(0), r1.samplingStatistics.borrowedRequests) + assert.Equal(t, int64(1), r1.samplingStatistics.matchedRequests) assert.Equal(t, int64(10), r1.reservoir.used) } @@ -201,7 +221,8 @@ func TestTraceIDRatioBasedSamplerFixedRateZero(t *testing.T) { }, } - sd := r1.Sample(trace.SamplingParameters{}, 1500000000) + now := time.Unix(1500000000, 0) + sd := r1.Sample(trace.SamplingParameters{}, now) assert.Equal(t, sd.Decision, trace.Drop) } diff --git a/samplers/aws/xray/remote_sampler.go b/samplers/aws/xray/remote_sampler.go index 54117efb5ea..9a830ac3ff2 100644 --- a/samplers/aws/xray/remote_sampler.go +++ b/samplers/aws/xray/remote_sampler.go @@ -100,7 +100,7 @@ func (rs *remoteSampler) ShouldSample(parameters sdktrace.SamplingParameters) sd if match { // remote sampling based on rule match - return r.Sample(parameters, rs.manifest.Clock.Now().Unix()) + return r.Sample(parameters, rs.manifest.Clock.Now()) } } From 073609790d51d54c01c03206cd1ff87e30cd0592 Mon Sep 17 00:00:00 2001 From: bhautikpip Date: Wed, 2 Mar 2022 15:03:22 -0800 Subject: [PATCH 14/38] fix flaky remote sampling test --- samplers/aws/xray/remote_sampler_test.go | 37 ++++++++++++++++++++++-- 1 file changed, 34 insertions(+), 3 deletions(-) diff --git a/samplers/aws/xray/remote_sampler_test.go b/samplers/aws/xray/remote_sampler_test.go index 236b89e86f2..23a81a45f99 100644 --- a/samplers/aws/xray/remote_sampler_test.go +++ b/samplers/aws/xray/remote_sampler_test.go @@ -15,8 +15,12 @@ package xray import ( + "log" + "os" "testing" + "github.com/go-logr/stdr" + "github.com/stretchr/testify/assert" "go.opentelemetry.io/contrib/samplers/aws/xray/internal" @@ -24,8 +28,33 @@ import ( sdktrace "go.opentelemetry.io/otel/sdk/trace" ) -// TestShouldSample assert that when manifest is not expired sampling happens with 1 req/sec. -func TestShouldSample(t *testing.T) { +// TestShouldSample assert that when manifest is not expired sd is dropped since FixedRate is 0 +func TestShouldSample_NonExpiredManifest(t *testing.T) { + clock := &util.MockClock{ + NowTime: -62135593200, + } + + r1 := internal.Rule{} + + rules := []internal.Rule{r1} + + m := &internal.Manifest{ + Rules: rules, + Clock: clock, + } + + rs := &remoteSampler{ + manifest: m, + logger: stdr.NewWithOptions(log.New(os.Stderr, "", log.LstdFlags|log.Lshortfile), stdr.Options{LogCaller: stdr.Error}), + fallbackSampler: NewFallbackSampler(), + } + + sd := rs.ShouldSample(sdktrace.SamplingParameters{}) + assert.Equal(t, sd.Decision, sdktrace.Drop) +} + +// TestShouldSample assert that when manifest is expired sampling happens with 1 req/sec. +func TestShouldSample_ExpiredManifest(t *testing.T) { clock := &util.MockClock{ NowTime: 100, } @@ -40,7 +69,9 @@ func TestShouldSample(t *testing.T) { } rs := &remoteSampler{ - manifest: m, + manifest: m, + logger: stdr.NewWithOptions(log.New(os.Stderr, "", log.LstdFlags|log.Lshortfile), stdr.Options{LogCaller: stdr.Error}), + fallbackSampler: NewFallbackSampler(), } sd := rs.ShouldSample(sdktrace.SamplingParameters{}) From bc0f85f06360a57534c3bb8cf4a4e0e20a54289f Mon Sep 17 00:00:00 2001 From: bhautikpip Date: Wed, 2 Mar 2022 15:16:51 -0800 Subject: [PATCH 15/38] relayout the struct to fix unaligned atomic op issue --- samplers/aws/xray/internal/reservoir.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/samplers/aws/xray/internal/reservoir.go b/samplers/aws/xray/internal/reservoir.go index 586dd4a3ce4..6d39cbdb8af 100644 --- a/samplers/aws/xray/internal/reservoir.go +++ b/samplers/aws/xray/internal/reservoir.go @@ -23,21 +23,21 @@ import ( // reservoir represents a sampling statistics for a given rule and populate it's value from // the response getSamplingTargets API which sends information on sampling statistics real-time type reservoir struct { - // reservoir consumption for current epoch - used int64 + // quota expiration timestamp + expiresAt int64 // reservoir usage is reset every second currentEpoch int64 + // reservoir consumption for current epoch + used int64 + // quota assigned to client quota int64 // quota refresh timestamp refreshedAt time.Time - // quota expiration timestamp - expiresAt int64 - // polling interval for quota interval time.Duration From ff14fae69b97255a46bc4f9705889a6d01bb3f29 Mon Sep 17 00:00:00 2001 From: bhautikpip Date: Mon, 7 Mar 2022 16:52:48 -0800 Subject: [PATCH 16/38] added leaky bucket logic for reservoir --- samplers/aws/xray/internal/client.go | 4 +- samplers/aws/xray/internal/client_test.go | 10 +- samplers/aws/xray/internal/manifest.go | 3 +- samplers/aws/xray/internal/manifest_test.go | 112 ++++++++--------- samplers/aws/xray/internal/reservoir.go | 89 +++++++++----- samplers/aws/xray/internal/reservoir_test.go | 121 ++++++++++++------- samplers/aws/xray/internal/rule.go | 6 +- samplers/aws/xray/internal/rule_test.go | 52 ++++---- samplers/aws/xray/remote_sampler_test.go | 25 ---- 9 files changed, 225 insertions(+), 197 deletions(-) diff --git a/samplers/aws/xray/internal/client.go b/samplers/aws/xray/internal/client.go index 886337f2806..e9e6cde7869 100644 --- a/samplers/aws/xray/internal/client.go +++ b/samplers/aws/xray/internal/client.go @@ -46,7 +46,7 @@ type ruleProperties struct { Host string `json:"Host"` HTTPMethod string `json:"HTTPMethod"` URLPath string `json:"URLPath"` - ReservoirSize int64 `json:"ReservoirSize"` + ReservoirSize float64 `json:"ReservoirSize"` FixedRate float64 `json:"FixedRate"` Priority int64 `json:"Priority"` Version int64 `json:"Version"` @@ -95,7 +95,7 @@ type samplingTargetDocument struct { Interval *int64 `json:"Interval,omitempty"` // the number of requests per second that X-Ray allocated this service - ReservoirQuota *int64 `json:"ReservoirQuota,omitempty"` + ReservoirQuota *float64 `json:"ReservoirQuota,omitempty"` // when the reservoir quota expires ReservoirQuotaTTL *float64 `json:"ReservoirQuotaTTL,omitempty"` diff --git a/samplers/aws/xray/internal/client_test.go b/samplers/aws/xray/internal/client_test.go index 4c7a6dd224f..bf1e31f1e44 100644 --- a/samplers/aws/xray/internal/client_test.go +++ b/samplers/aws/xray/internal/client_test.go @@ -109,21 +109,21 @@ func TestGetSamplingRules(t *testing.T) { assert.Equal(t, samplingRules.SamplingRuleRecords[0].SamplingRule.ServiceType, "*") assert.Equal(t, samplingRules.SamplingRuleRecords[0].SamplingRule.Host, "*") assert.Equal(t, samplingRules.SamplingRuleRecords[0].SamplingRule.URLPath, "*") - assert.Equal(t, samplingRules.SamplingRuleRecords[0].SamplingRule.ReservoirSize, int64(60)) + assert.Equal(t, samplingRules.SamplingRuleRecords[0].SamplingRule.ReservoirSize, 60.0) assert.Equal(t, samplingRules.SamplingRuleRecords[0].SamplingRule.FixedRate, 0.5) assert.Equal(t, samplingRules.SamplingRuleRecords[1].SamplingRule.RuleName, "test-rule") assert.Equal(t, samplingRules.SamplingRuleRecords[1].SamplingRule.ServiceType, "local") assert.Equal(t, samplingRules.SamplingRuleRecords[1].SamplingRule.Host, "*") assert.Equal(t, samplingRules.SamplingRuleRecords[1].SamplingRule.URLPath, "/aws-sdk-call") - assert.Equal(t, samplingRules.SamplingRuleRecords[1].SamplingRule.ReservoirSize, int64(3)) + assert.Equal(t, samplingRules.SamplingRuleRecords[1].SamplingRule.ReservoirSize, 3.0) assert.Equal(t, samplingRules.SamplingRuleRecords[1].SamplingRule.FixedRate, 0.09) assert.Equal(t, samplingRules.SamplingRuleRecords[2].SamplingRule.RuleName, "test-rule-1") assert.Equal(t, samplingRules.SamplingRuleRecords[2].SamplingRule.ServiceType, "*") assert.Equal(t, samplingRules.SamplingRuleRecords[2].SamplingRule.Host, "*") assert.Equal(t, samplingRules.SamplingRuleRecords[2].SamplingRule.URLPath, "*") - assert.Equal(t, samplingRules.SamplingRuleRecords[2].SamplingRule.ReservoirSize, int64(100)) + assert.Equal(t, samplingRules.SamplingRuleRecords[2].SamplingRule.ReservoirSize, 100.0) assert.Equal(t, samplingRules.SamplingRuleRecords[2].SamplingRule.FixedRate, 0.09) } @@ -170,7 +170,7 @@ func TestGetSamplingRulesWithMissingValues(t *testing.T) { // Priority and ReservoirSize are missing in API response so they are assigned as nil assert.Equal(t, samplingRules.SamplingRuleRecords[0].SamplingRule.Priority, int64(0)) - assert.Equal(t, samplingRules.SamplingRuleRecords[0].SamplingRule.ReservoirSize, int64(0)) + assert.Equal(t, samplingRules.SamplingRuleRecords[0].SamplingRule.ReservoirSize, 0.0) // other values are stored as expected assert.Equal(t, samplingRules.SamplingRuleRecords[0].SamplingRule.RuleName, "Default") @@ -218,7 +218,7 @@ func TestGetSamplingTargets(t *testing.T) { assert.Equal(t, *samplingTragets.LastRuleModification, float64(123456)) assert.Equal(t, *samplingTragets.SamplingTargetDocuments[0].FixedRate, float64(5)) assert.Equal(t, *samplingTragets.SamplingTargetDocuments[0].Interval, int64(5)) - assert.Equal(t, *samplingTragets.SamplingTargetDocuments[0].ReservoirQuota, int64(3)) + assert.Equal(t, *samplingTragets.SamplingTargetDocuments[0].ReservoirQuota, 3.0) assert.Equal(t, *samplingTragets.SamplingTargetDocuments[0].ReservoirQuotaTTL, float64(456789)) assert.Equal(t, *samplingTragets.SamplingTargetDocuments[0].RuleName, "r1") assert.Equal(t, *samplingTragets.UnprocessedStatistics[0].RuleName, "r1") diff --git a/samplers/aws/xray/internal/manifest.go b/samplers/aws/xray/internal/manifest.go index 80873a3bd9f..66878b17200 100644 --- a/samplers/aws/xray/internal/manifest.go +++ b/samplers/aws/xray/internal/manifest.go @@ -200,6 +200,7 @@ func (m *Manifest) updateRules(rules *getSamplingRulesOutput) { func (m *Manifest) createRule(ruleProp ruleProperties) { cr := reservoir{ capacity: ruleProp.ReservoirSize, + mu: &sync.RWMutex{}, } csr := Rule{ @@ -276,7 +277,7 @@ func (m *Manifest) updateReservoir(t *samplingTargetDocument) (err error) { m.Rules[index].reservoir.quota = *t.ReservoirQuota } if t.ReservoirQuotaTTL != nil { - m.Rules[index].reservoir.expiresAt = int64(*t.ReservoirQuotaTTL) + m.Rules[index].reservoir.expiresAt = time.Unix(int64(*t.ReservoirQuotaTTL), 0) } if t.Interval != nil { m.Rules[index].reservoir.interval = time.Duration(*t.Interval) diff --git a/samplers/aws/xray/internal/manifest_test.go b/samplers/aws/xray/internal/manifest_test.go index 75cad108a0e..537b21219c1 100644 --- a/samplers/aws/xray/internal/manifest_test.go +++ b/samplers/aws/xray/internal/manifest_test.go @@ -21,6 +21,7 @@ import ( "net/http/httptest" "net/url" "os" + "sync" "testing" "time" @@ -95,7 +96,7 @@ func TestMatchAgainstManifestRules(t *testing.T) { ServiceType: "*", }, reservoir: reservoir{ - expiresAt: 14050, + expiresAt: time.Unix(14050, 0), }, } @@ -114,7 +115,7 @@ func TestMatchAgainstManifestRules(t *testing.T) { ServiceType: "local", }, reservoir: reservoir{ - expiresAt: 14050, + expiresAt: time.Unix(14050, 0), }, } @@ -157,7 +158,7 @@ func TestMatchAgainstManifestRules_AttributeMatch(t *testing.T) { }, }, reservoir: reservoir{ - expiresAt: 14050, + expiresAt: time.Unix(14050, 0), }, } @@ -201,7 +202,7 @@ func TestMatchAgainstManifestRules_AttributeWildCardMatch(t *testing.T) { }, }, reservoir: reservoir{ - expiresAt: 14050, + expiresAt: time.Unix(14050, 0), }, } @@ -323,7 +324,8 @@ func TestRefreshManifestRules(t *testing.T) { Attributes: map[string]string{}, }, reservoir: reservoir{ - capacity: int64(60), + capacity: 60, + mu: &sync.RWMutex{}, }, } @@ -343,7 +345,8 @@ func TestRefreshManifestRules(t *testing.T) { Attributes: map[string]string{}, }, reservoir: reservoir{ - capacity: int64(3), + capacity: 3, + mu: &sync.RWMutex{}, }, } @@ -363,7 +366,8 @@ func TestRefreshManifestRules(t *testing.T) { Attributes: map[string]string{}, }, reservoir: reservoir{ - capacity: int64(100), + capacity: 100, + mu: &sync.RWMutex{}, }, } @@ -610,7 +614,8 @@ func TestRefreshManifestAddOneInvalidRule(t *testing.T) { Attributes: map[string]string{}, }, reservoir: reservoir{ - capacity: int64(60), + capacity: 60, + mu: &sync.RWMutex{}, }, } @@ -665,7 +670,7 @@ func TestRefreshManifestTarget_NoSnapShot(t *testing.T) { Attributes: map[string]string{}, }, reservoir: reservoir{ - capacity: int64(100), + capacity: 100, }, samplingStatistics: samplingStatistics{ matchedRequests: int64(0), @@ -728,7 +733,8 @@ func TestRefreshManifestTargets(t *testing.T) { Attributes: map[string]string{}, }, reservoir: reservoir{ - capacity: int64(100), + capacity: 100, + mu: &sync.RWMutex{}, }, samplingStatistics: samplingStatistics{ matchedRequests: int64(5), @@ -764,8 +770,8 @@ func TestRefreshManifestTargets(t *testing.T) { // assert target updates assert.Equal(t, m.Rules[0].ruleProperties.FixedRate, 0.06) - assert.Equal(t, m.Rules[0].reservoir.quota, int64(23)) - assert.Equal(t, m.Rules[0].reservoir.expiresAt, int64(15000000)) + assert.Equal(t, m.Rules[0].reservoir.quota, 23.0) + assert.Equal(t, m.Rules[0].reservoir.expiresAt, time.Unix(15000000, 0)) assert.Equal(t, m.Rules[0].reservoir.interval, time.Duration(25)) } @@ -777,7 +783,7 @@ func TestUpdateTargets(t *testing.T) { // sampling target received from centralized sampling backend rate := 0.05 - quota := int64(10) + quota := float64(10) ttl := float64(1500000060) name := "r1" @@ -800,12 +806,10 @@ func TestUpdateTargets(t *testing.T) { FixedRate: 0.10, }, reservoir: reservoir{ - quota: 8, - refreshedAt: refreshedAt1, - expiresAt: 1500000010, - capacity: 50, - used: 7, - currentEpoch: 1500000000, + quota: 8, + refreshedAt: refreshedAt1, + expiresAt: time.Unix(1500000010, 0), + capacity: 50, }, } @@ -830,12 +834,10 @@ func TestUpdateTargets(t *testing.T) { FixedRate: 0.05, }, reservoir: reservoir{ - quota: 10, - refreshedAt: refreshedAt2, - expiresAt: 1500000060, - capacity: 50, - used: 7, - currentEpoch: 1500000000, + quota: 10, + refreshedAt: refreshedAt2, + expiresAt: time.Unix(1500000060, 0), + capacity: 50, }, } @@ -852,7 +854,7 @@ func TestUpdateTargetsRefreshFlagTest(t *testing.T) { // sampling target received from centralized sampling backend rate := 0.05 - quota := int64(10) + quota := float64(10) ttl := float64(1500000060) name := "r1" @@ -877,12 +879,10 @@ func TestUpdateTargetsRefreshFlagTest(t *testing.T) { FixedRate: 0.10, }, reservoir: reservoir{ - quota: 8, - refreshedAt: refreshedAt1, - expiresAt: 1500000010, - capacity: 50, - used: 7, - currentEpoch: 1500000000, + quota: 8, + refreshedAt: refreshedAt1, + expiresAt: time.Unix(1500000010, 0), + capacity: 50, }, } @@ -908,12 +908,10 @@ func TestUpdateTargetsRefreshFlagTest(t *testing.T) { FixedRate: 0.05, }, reservoir: reservoir{ - quota: 10, - refreshedAt: refreshedAt2, - expiresAt: 1500000060, - capacity: 50, - used: 7, - currentEpoch: 1500000000, + quota: 10, + refreshedAt: refreshedAt2, + expiresAt: time.Unix(1500000060, 0), + capacity: 50, }, } @@ -929,7 +927,7 @@ func TestUpdateTargetsUnprocessedStatistics(t *testing.T) { // sampling target received from centralized sampling backend rate := 0.05 - quota := int64(10) + quota := float64(10) ttl := float64(1500000060) name := "r1" @@ -988,7 +986,7 @@ func TestUpdateTargetsUnprocessedStatistics(t *testing.T) { func TestUpdateReservoir(t *testing.T) { // Sampling target received from centralized sampling backend rate := 0.05 - quota := int64(10) + quota := float64(10) ttl := float64(1500000060) name := "r1" st := &samplingTargetDocument{ @@ -1006,12 +1004,10 @@ func TestUpdateReservoir(t *testing.T) { FixedRate: 0.10, }, reservoir: reservoir{ - quota: 8, - refreshedAt: refreshedAt1, - expiresAt: 1500000010, - capacity: 50, - used: 7, - currentEpoch: 1500000000, + quota: 8, + refreshedAt: refreshedAt1, + expiresAt: time.Unix(1500000010, 0), + capacity: 50, }, } @@ -1031,7 +1027,7 @@ func TestUpdateReservoir(t *testing.T) { // assert that a sampling target with missing Fixed Rate returns an error. func TestUpdateReservoirMissingFixedRate(t *testing.T) { // Sampling target received from centralized sampling backend - quota := int64(10) + quota := float64(10) ttl := float64(1500000060) name := "r1" st := &samplingTargetDocument{ @@ -1048,12 +1044,10 @@ func TestUpdateReservoirMissingFixedRate(t *testing.T) { FixedRate: 0.10, }, reservoir: reservoir{ - quota: 8, - refreshedAt: refreshedAt1, - expiresAt: 1500000010, - capacity: 50, - used: 7, - currentEpoch: 1500000000, + quota: 8, + refreshedAt: refreshedAt1, + expiresAt: time.Unix(1500000010, 0), + capacity: 50, }, } @@ -1071,7 +1065,7 @@ func TestUpdateReservoirMissingFixedRate(t *testing.T) { func TestUpdateReservoirMissingRuleName(t *testing.T) { // Sampling target received from centralized sampling backend rate := 0.05 - quota := int64(10) + quota := float64(10) ttl := float64(1500000060) st := &samplingTargetDocument{ ReservoirQuota: "a, @@ -1087,12 +1081,10 @@ func TestUpdateReservoirMissingRuleName(t *testing.T) { FixedRate: 0.10, }, reservoir: reservoir{ - quota: 8, - refreshedAt: refreshedAt1, - expiresAt: 1500000010, - capacity: 50, - used: 7, - currentEpoch: 1500000000, + quota: 8, + refreshedAt: refreshedAt1, + expiresAt: time.Unix(1500000010, 0), + capacity: 50, }, } diff --git a/samplers/aws/xray/internal/reservoir.go b/samplers/aws/xray/internal/reservoir.go index 6d39cbdb8af..138bc0d7f7f 100644 --- a/samplers/aws/xray/internal/reservoir.go +++ b/samplers/aws/xray/internal/reservoir.go @@ -16,7 +16,6 @@ package internal // import "go.opentelemetry.io/contrib/samplers/aws/xray/intern import ( "sync" - "sync/atomic" "time" ) @@ -24,16 +23,16 @@ import ( // the response getSamplingTargets API which sends information on sampling statistics real-time type reservoir struct { // quota expiration timestamp - expiresAt int64 + expiresAt time.Time - // reservoir usage is reset every second - currentEpoch int64 + // quota assigned to client to consume per second + quota float64 - // reservoir consumption for current epoch - used int64 + // current balance of quota + quotaBalance float64 - // quota assigned to client - quota int64 + // total size of reservoir consumption per second + capacity float64 // quota refresh timestamp refreshedAt time.Time @@ -41,41 +40,77 @@ type reservoir struct { // polling interval for quota interval time.Duration - // total size of reservoir - capacity int64 + // stores reservoir ticks + lastTick time.Time + + // stores borrow ticks + borrowTick time.Time + + mu *sync.RWMutex } // expired returns true if current time is past expiration timestamp. False otherwise. -func (r *reservoir) expired(now int64) bool { - expire := atomic.LoadInt64(&r.expiresAt) +func (r *reservoir) expired(now time.Time) bool { + r.mu.RLock() + defer r.mu.RUnlock() - return now >= expire + return now.After(r.expiresAt) } // borrow returns true if the reservoir has not been borrowed from this epoch. -func (r *reservoir) borrow(now int64) bool { - cur := atomic.LoadInt64(&r.currentEpoch) - if cur >= now { +func (r *reservoir) borrow(now time.Time) bool { + r.mu.Lock() + defer r.mu.Unlock() + + currentTime := now + + if currentTime.Equal(r.borrowTick) { return false } - return atomic.CompareAndSwapInt64(&r.currentEpoch, cur, now) + + if currentTime.After(r.borrowTick) { + r.borrowTick = currentTime + return true + } + + return false } -// Take consumes quota from reservoir, if any remains, then returns true. False otherwise. -func (r *reservoir) take(now int64) bool { - var mu sync.RWMutex - mu.Lock() - defer mu.Unlock() +// take consumes quota from reservoir, if any remains, then returns true. False otherwise. +func (r *reservoir) take(now time.Time, itemCost float64) bool { + r.mu.Lock() + defer r.mu.Unlock() - if r.currentEpoch != now { - r.used = 0 - r.currentEpoch = now + if r.lastTick.IsZero() { + r.lastTick = now + r.quotaBalance = r.quota } - if r.quota > r.used { - r.used++ + if r.quotaBalance >= itemCost { + r.quotaBalance -= itemCost + return true + } + + // update quota balance based on elapsed time + r.refreshQuotaBalance(now) + + if r.quotaBalance >= itemCost { + r.quotaBalance -= itemCost return true } return false } + +func (r *reservoir) refreshQuotaBalance(now time.Time) { + currentTime := now + elapsedTime := currentTime.Sub(r.lastTick) + r.lastTick = currentTime + + // calculate how much credit have we accumulated since the last tick + totalQuotaBalanceCapacity := elapsedTime.Seconds() * r.capacity + r.quotaBalance += elapsedTime.Seconds() * r.quota + if r.quotaBalance > totalQuotaBalanceCapacity { + r.quotaBalance = totalQuotaBalanceCapacity + } +} diff --git a/samplers/aws/xray/internal/reservoir_test.go b/samplers/aws/xray/internal/reservoir_test.go index 858b7513181..2b62b3a2db2 100644 --- a/samplers/aws/xray/internal/reservoir_test.go +++ b/samplers/aws/xray/internal/reservoir_test.go @@ -15,7 +15,9 @@ package internal import ( + "sync" "testing" + "time" "go.opentelemetry.io/contrib/samplers/aws/xray/internal/util" @@ -28,11 +30,13 @@ func TestExpiredReservoir(t *testing.T) { NowTime: 1500000001, } + expiresAt := time.Unix(1500000000, 0) r := &reservoir{ - expiresAt: 1500000000, + expiresAt: expiresAt, + mu: &sync.RWMutex{}, } - expired := r.expired(clock.Now().Unix()) + expired := r.expired(clock.Now()) assert.True(t, expired) } @@ -43,13 +47,16 @@ func TestExpiredReservoirSameAsClockTime(t *testing.T) { NowTime: 1500000000, } + expiresAt := time.Unix(1500000000, 0) + r := &reservoir{ - expiresAt: 1500000000, + expiresAt: expiresAt, + mu: &sync.RWMutex{}, } - expired := r.expired(clock.Now().Unix()) + expired := r.expired(clock.Now()) - assert.True(t, expired) + assert.False(t, expired) } // Assert that borrow only 1 req/sec @@ -60,12 +67,13 @@ func TestBorrowEverySecond(t *testing.T) { r := &reservoir{ capacity: 10, + mu: &sync.RWMutex{}, } - s := r.borrow(clock.Now().Unix()) + s := r.borrow(clock.Now()) assert.True(t, s) - s = r.borrow(clock.Now().Unix()) + s = r.borrow(clock.Now()) assert.False(t, s) // Increment clock by 1 @@ -73,46 +81,52 @@ func TestBorrowEverySecond(t *testing.T) { NowTime: 1500000001, } - s = r.borrow(clock.Now().Unix()) + s = r.borrow(clock.Now()) assert.True(t, s) } // assert that we can still borrowing from reservoir is possible since assigned quota is available to consume // and it will increase used count. -func TestConsumeAvailableQuota(t *testing.T) { +func TestConsumeFromReservoir(t *testing.T) { clock := &util.MockClock{ NowTime: 1500000000, } r := &reservoir{ - quota: int64(9), - capacity: int64(100), - used: int64(0), - currentEpoch: clock.Now().Unix(), + quota: 2, + capacity: 100, + mu: &sync.RWMutex{}, } - s := r.take(clock.Now().Unix()) - assert.True(t, s) - assert.Equal(t, int64(1), r.used) -} - -// assert that we can not borrow from reservoir since assigned quota is not available to consume -// and it will not increase used count. -func TestConsumeUnavailableQuota(t *testing.T) { - clock := &util.MockClock{ - NowTime: 1500000000, - } - - r := &reservoir{ - quota: int64(9), - capacity: int64(100), - used: int64(9), - currentEpoch: clock.Now().Unix(), - } - - s := r.take(clock.Now().Unix()) - assert.False(t, s) - assert.Equal(t, int64(9), r.used) + // reservoir updates the quotaBalance for new second and allows to consume + // quota balance is 0 because we are consuming from reservoir for the first time + assert.Equal(t, r.quotaBalance, 0.0) + assert.True(t, r.take(clock.Now(), 1.0)) + assert.Equal(t, r.quotaBalance, 1.0) + assert.True(t, r.take(clock.Now(), 1.0)) + assert.Equal(t, r.quotaBalance, 0.0) + // once assigned quota is consumed reservoir does not allow to consume in that second + assert.False(t, r.take(clock.Now(), 1.0)) + + // increase the clock by 1 + clock.NowTime = 1500000001 + + // reservoir updates the quotaBalance for new second and allows to consume + assert.Equal(t, r.quotaBalance, 0.0) + assert.True(t, r.take(clock.Now(), 1.0)) + assert.Equal(t, r.quotaBalance, 1.0) + assert.True(t, r.take(clock.Now(), 1.0)) + assert.Equal(t, r.quotaBalance, 0.0) + // once assigned quota is consumed reservoir does not allow to consume in that second + assert.False(t, r.take(clock.Now(), 1.0)) + + // increase the clock by 5 + clock.NowTime = 1500000005 + + // elapsedTime is 4 seconds so quota balance should be elapsedTime * quota = 8 and below take would consume 1 so + // ultimately 7 + assert.True(t, r.take(clock.Now(), 1.0)) + assert.Equal(t, r.quotaBalance, 7.0) } func TestResetQuotaUsageRotation(t *testing.T) { @@ -121,23 +135,20 @@ func TestResetQuotaUsageRotation(t *testing.T) { } r := &reservoir{ - quota: int64(5), - capacity: int64(100), - used: int64(0), - currentEpoch: clock.Now().Unix(), + quota: 5, + capacity: 100, + mu: &sync.RWMutex{}, } // consume quota for second for i := 0; i < 5; i++ { - taken := r.take(clock.Now().Unix()) + taken := r.take(clock.Now(), 1.0) assert.Equal(t, true, taken) - assert.Equal(t, int64(i+1), r.used) } // take() should be false since no unused quota left - taken := r.take(clock.Now().Unix()) + taken := r.take(clock.Now(), 1.0) assert.Equal(t, false, taken) - assert.Equal(t, int64(5), r.used) // increment epoch to reset unused quota clock = &util.MockClock{ @@ -145,8 +156,26 @@ func TestResetQuotaUsageRotation(t *testing.T) { } // take() should be true since ununsed quota is available - taken = r.take(clock.Now().Unix()) - assert.Equal(t, int64(1500000001), r.currentEpoch) + taken = r.take(clock.Now(), 1.0) assert.Equal(t, true, taken) - assert.Equal(t, int64(1), r.used) +} + +// assert that when quotaBalance exceeds totalQuotaBalanceCapacity then totalQuotaBalanceCapacity +// gets assigned to quotaBalance +func TestQuotaBalanceExceedsCapacity(t *testing.T) { + clock := &util.MockClock{ + NowTime: 1500000001, + } + + r := &reservoir{ + quota: 6, + capacity: 5, + mu: &sync.RWMutex{}, + lastTick: time.Unix(1500000000, 0), + } + + r.refreshQuotaBalance(clock.Now()) + + // assert that if quotaBalance exceeds capacity then total capacity would be new quotaBalance + assert.Equal(t, r.quotaBalance, 1*r.capacity) } diff --git a/samplers/aws/xray/internal/rule.go b/samplers/aws/xray/internal/rule.go index 8dda53f418c..517456179ca 100644 --- a/samplers/aws/xray/internal/rule.go +++ b/samplers/aws/xray/internal/rule.go @@ -80,9 +80,9 @@ func (r *Rule) Sample(parameters sdktrace.SamplingParameters, now time.Time) sdk atomic.AddInt64(&r.samplingStatistics.matchedRequests, int64(1)) // fallback sampling logic if quota for a given rule is expired - if r.reservoir.expired(now.Unix()) { + if r.reservoir.expired(now) { // borrowing one request every second - if r.reservoir.borrow(now.Unix()) { + if r.reservoir.borrow(now) { atomic.AddInt64(&r.samplingStatistics.borrowedRequests, int64(1)) sd.Decision = sdktrace.RecordAndSample @@ -100,7 +100,7 @@ func (r *Rule) Sample(parameters sdktrace.SamplingParameters, now time.Time) sdk } // Take from reservoir quota, if quota is available for that second - if r.reservoir.take(now.Unix()) { + if r.reservoir.take(now, 1.0) { atomic.AddInt64(&r.samplingStatistics.sampledRequests, int64(1)) sd.Decision = sdktrace.RecordAndSample diff --git a/samplers/aws/xray/internal/rule_test.go b/samplers/aws/xray/internal/rule_test.go index ad60dcf0773..a849abf1403 100644 --- a/samplers/aws/xray/internal/rule_test.go +++ b/samplers/aws/xray/internal/rule_test.go @@ -15,6 +15,7 @@ package internal import ( + "sync" "testing" "time" @@ -112,10 +113,9 @@ func TestSnapshot(t *testing.T) { func TestExpiredReservoirBorrowSample(t *testing.T) { r1 := Rule{ reservoir: reservoir{ - expiresAt: 1500000060, - used: 0, - capacity: 10, - currentEpoch: 1500000061, + expiresAt: time.Unix(1500000060, 0), + capacity: 10, + mu: &sync.RWMutex{}, }, ruleProperties: ruleProperties{ RuleName: "r1", @@ -130,17 +130,16 @@ func TestExpiredReservoirBorrowSample(t *testing.T) { assert.Equal(t, int64(1), r1.samplingStatistics.borrowedRequests) assert.Equal(t, int64(0), r1.samplingStatistics.sampledRequests) assert.Equal(t, int64(1), r1.samplingStatistics.matchedRequests) - assert.Equal(t, int64(0), r1.reservoir.used) } // assert that reservoir is expired, borrowed 1 req during that second so now using traceIDRatioBased sampler. func TestExpiredReservoirTraceIDRationBasedSample(t *testing.T) { r1 := Rule{ reservoir: reservoir{ - expiresAt: 1500000060, - used: 0, - capacity: 10, - currentEpoch: 1500000061, + expiresAt: time.Unix(1500000060, 0), + capacity: 10, + mu: &sync.RWMutex{}, + borrowTick: time.Unix(1500000061, 0), }, ruleProperties: ruleProperties{ RuleName: "r1", @@ -149,25 +148,24 @@ func TestExpiredReservoirTraceIDRationBasedSample(t *testing.T) { } now := time.Unix(1500000061, 0) - r1.Sample(trace.SamplingParameters{}, now) + sd := r1.Sample(trace.SamplingParameters{}, now) + assert.NotEmpty(t, sd.Decision) assert.Equal(t, int64(0), r1.samplingStatistics.borrowedRequests) assert.Equal(t, int64(1), r1.samplingStatistics.sampledRequests) assert.Equal(t, int64(1), r1.samplingStatistics.matchedRequests) - assert.Equal(t, int64(0), r1.reservoir.used) } // assert that reservoir is not expired, quota is available so consuming from quota. -func TestConsumeFromQuotaSample(t *testing.T) { +func TestConsumeFromReservoirSample(t *testing.T) { r1 := Rule{ ruleProperties: ruleProperties{ RuleName: "r1", }, reservoir: reservoir{ - quota: 10, - expiresAt: 1500000060, - currentEpoch: 1500000000, - used: 0, + quota: 10, + expiresAt: time.Unix(1500000060, 0), + mu: &sync.RWMutex{}, }, } @@ -178,17 +176,16 @@ func TestConsumeFromQuotaSample(t *testing.T) { assert.Equal(t, int64(1), r1.samplingStatistics.sampledRequests) assert.Equal(t, int64(0), r1.samplingStatistics.borrowedRequests) assert.Equal(t, int64(1), r1.samplingStatistics.matchedRequests) - assert.Equal(t, int64(1), r1.reservoir.used) } -// assert that sampling using traceIDRationBasedSampler. -func TestTraceIDRatioBasedSampler(t *testing.T) { +// assert that sampling using traceIDRationBasedSampler when reservoir quota is consumed. +func TestTraceIDRatioBasedSampler_ReservoirIsConsumedSample(t *testing.T) { r1 := Rule{ reservoir: reservoir{ - quota: 10, - expiresAt: 1500000060, - currentEpoch: 1500000000, - used: 10, + quota: 10, + expiresAt: time.Unix(1500000060, 0), + mu: &sync.RWMutex{}, + lastTick: time.Unix(1500000000, 0), }, ruleProperties: ruleProperties{ FixedRate: 0.05, @@ -203,17 +200,16 @@ func TestTraceIDRatioBasedSampler(t *testing.T) { assert.Equal(t, int64(1), r1.samplingStatistics.sampledRequests) assert.Equal(t, int64(0), r1.samplingStatistics.borrowedRequests) assert.Equal(t, int64(1), r1.samplingStatistics.matchedRequests) - assert.Equal(t, int64(10), r1.reservoir.used) } // assert that when fixed rate is 0 traceIDRatioBased sampler will not sample the trace. func TestTraceIDRatioBasedSamplerFixedRateZero(t *testing.T) { r1 := Rule{ reservoir: reservoir{ - quota: 10, - expiresAt: 1500000060, - currentEpoch: 1500000000, - used: 10, + quota: 10, + expiresAt: time.Unix(1500000060, 0), + mu: &sync.RWMutex{}, + lastTick: time.Unix(1500000000, 0), }, ruleProperties: ruleProperties{ FixedRate: 0, diff --git a/samplers/aws/xray/remote_sampler_test.go b/samplers/aws/xray/remote_sampler_test.go index 23a81a45f99..3d7aa57c25a 100644 --- a/samplers/aws/xray/remote_sampler_test.go +++ b/samplers/aws/xray/remote_sampler_test.go @@ -28,31 +28,6 @@ import ( sdktrace "go.opentelemetry.io/otel/sdk/trace" ) -// TestShouldSample assert that when manifest is not expired sd is dropped since FixedRate is 0 -func TestShouldSample_NonExpiredManifest(t *testing.T) { - clock := &util.MockClock{ - NowTime: -62135593200, - } - - r1 := internal.Rule{} - - rules := []internal.Rule{r1} - - m := &internal.Manifest{ - Rules: rules, - Clock: clock, - } - - rs := &remoteSampler{ - manifest: m, - logger: stdr.NewWithOptions(log.New(os.Stderr, "", log.LstdFlags|log.Lshortfile), stdr.Options{LogCaller: stdr.Error}), - fallbackSampler: NewFallbackSampler(), - } - - sd := rs.ShouldSample(sdktrace.SamplingParameters{}) - assert.Equal(t, sd.Decision, sdktrace.Drop) -} - // TestShouldSample assert that when manifest is expired sampling happens with 1 req/sec. func TestShouldSample_ExpiredManifest(t *testing.T) { clock := &util.MockClock{ From 3f7d44dd02c0d40c2727074ba9c4561a0ecab13a Mon Sep 17 00:00:00 2001 From: bhautikpip Date: Tue, 8 Mar 2022 15:35:41 -0800 Subject: [PATCH 17/38] minor changes to fix some issues --- samplers/aws/xray/internal/client.go | 2 +- samplers/aws/xray/internal/manifest.go | 5 +++-- samplers/aws/xray/internal/manifest_test.go | 14 +++++++------- samplers/aws/xray/internal/reservoir.go | 2 +- samplers/aws/xray/internal/rule.go | 18 ++++++++++++------ samplers/aws/xray/internal/rule_test.go | 13 +++++++++---- 6 files changed, 33 insertions(+), 21 deletions(-) diff --git a/samplers/aws/xray/internal/client.go b/samplers/aws/xray/internal/client.go index e9e6cde7869..5efbbb0987b 100644 --- a/samplers/aws/xray/internal/client.go +++ b/samplers/aws/xray/internal/client.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package internal +package internal // import "go.opentelemetry.io/contrib/samplers/aws/xray/internal" import ( "bytes" diff --git a/samplers/aws/xray/internal/manifest.go b/samplers/aws/xray/internal/manifest.go index 66878b17200..fa6fd774aa2 100644 --- a/samplers/aws/xray/internal/manifest.go +++ b/samplers/aws/xray/internal/manifest.go @@ -204,8 +204,9 @@ func (m *Manifest) createRule(ruleProp ruleProperties) { } csr := Rule{ - reservoir: cr, - ruleProperties: ruleProp, + reservoir: cr, + ruleProperties: ruleProp, + samplingStatistics: &samplingStatistics{}, } m.Rules = append(m.Rules, csr) diff --git a/samplers/aws/xray/internal/manifest_test.go b/samplers/aws/xray/internal/manifest_test.go index 537b21219c1..c683927bd38 100644 --- a/samplers/aws/xray/internal/manifest_test.go +++ b/samplers/aws/xray/internal/manifest_test.go @@ -672,7 +672,7 @@ func TestRefreshManifestTarget_NoSnapShot(t *testing.T) { reservoir: reservoir{ capacity: 100, }, - samplingStatistics: samplingStatistics{ + samplingStatistics: &samplingStatistics{ matchedRequests: int64(0), }, } @@ -736,7 +736,7 @@ func TestRefreshManifestTargets(t *testing.T) { capacity: 100, mu: &sync.RWMutex{}, }, - samplingStatistics: samplingStatistics{ + samplingStatistics: &samplingStatistics{ matchedRequests: int64(5), }, } @@ -1117,7 +1117,7 @@ func TestSnapshots(t *testing.T) { reservoir: reservoir{ interval: 10, }, - samplingStatistics: samplingStatistics{ + samplingStatistics: &samplingStatistics{ matchedRequests: requests1, sampledRequests: sampled1, borrowedRequests: borrowed1, @@ -1135,7 +1135,7 @@ func TestSnapshots(t *testing.T) { reservoir: reservoir{ interval: 10, }, - samplingStatistics: samplingStatistics{ + samplingStatistics: &samplingStatistics{ matchedRequests: requests2, sampledRequests: sampled2, borrowedRequests: borrowed2, @@ -1205,7 +1205,7 @@ func TestMixedSnapshots(t *testing.T) { interval: 20, refreshedAt: refreshedAt1, }, - samplingStatistics: samplingStatistics{ + samplingStatistics: &samplingStatistics{ matchedRequests: requests1, sampledRequests: sampled1, borrowedRequests: borrowed1, @@ -1227,7 +1227,7 @@ func TestMixedSnapshots(t *testing.T) { interval: 20, refreshedAt: refreshedAt2, }, - samplingStatistics: samplingStatistics{ + samplingStatistics: &samplingStatistics{ matchedRequests: requests2, sampledRequests: sampled2, borrowedRequests: borrowed2, @@ -1249,7 +1249,7 @@ func TestMixedSnapshots(t *testing.T) { interval: 20, refreshedAt: refreshedAt3, }, - samplingStatistics: samplingStatistics{ + samplingStatistics: &samplingStatistics{ matchedRequests: requests3, sampledRequests: sampled3, borrowedRequests: borrowed3, diff --git a/samplers/aws/xray/internal/reservoir.go b/samplers/aws/xray/internal/reservoir.go index 138bc0d7f7f..a22f0aea7f1 100644 --- a/samplers/aws/xray/internal/reservoir.go +++ b/samplers/aws/xray/internal/reservoir.go @@ -64,7 +64,7 @@ func (r *reservoir) borrow(now time.Time) bool { currentTime := now - if currentTime.Equal(r.borrowTick) { + if currentTime.Unix() == r.borrowTick.Unix() { return false } diff --git a/samplers/aws/xray/internal/rule.go b/samplers/aws/xray/internal/rule.go index 517456179ca..d614b5d47fb 100644 --- a/samplers/aws/xray/internal/rule.go +++ b/samplers/aws/xray/internal/rule.go @@ -24,7 +24,7 @@ import ( // Rule represents a sampling rule which contains rule properties and reservoir which keeps tracks of sampling statistics of a rule type Rule struct { - samplingStatistics samplingStatistics + samplingStatistics *samplingStatistics // reservoir has equivalent fields to store what we receive from service API getSamplingTargets // https://docs.aws.amazon.com/xray/latest/api/API_GetSamplingTargets.html @@ -56,15 +56,21 @@ func (r *Rule) stale(now time.Time) bool { // samplingStatisticsDocument. It also resets statistics counters. func (r *Rule) snapshot(now time.Time) *samplingStatisticsDocument { name := r.ruleProperties.RuleName - requests, sampled, borrowed := r.samplingStatistics.matchedRequests, r.samplingStatistics.sampledRequests, r.samplingStatistics.borrowedRequests + + matchedRequests := atomic.LoadInt64(&r.samplingStatistics.matchedRequests) + sampledRequests := atomic.LoadInt64(&r.samplingStatistics.sampledRequests) + borrowedRequest := atomic.LoadInt64(&r.samplingStatistics.borrowedRequests) // reset counters - r.samplingStatistics.matchedRequests, r.samplingStatistics.sampledRequests, r.samplingStatistics.borrowedRequests = 0, 0, 0 + atomic.CompareAndSwapInt64(&r.samplingStatistics.matchedRequests, matchedRequests, int64(0)) + atomic.CompareAndSwapInt64(&r.samplingStatistics.sampledRequests, sampledRequests, int64(0)) + atomic.CompareAndSwapInt64(&r.samplingStatistics.borrowedRequests, borrowedRequest, int64(0)) + timeStamp := now.Unix() return &samplingStatisticsDocument{ - RequestCount: &requests, - SampledCount: &sampled, - BorrowCount: &borrowed, + RequestCount: &matchedRequests, + SampledCount: &sampledRequests, + BorrowCount: &borrowedRequest, RuleName: &name, Timestamp: &timeStamp, } diff --git a/samplers/aws/xray/internal/rule_test.go b/samplers/aws/xray/internal/rule_test.go index a849abf1403..b0165b32703 100644 --- a/samplers/aws/xray/internal/rule_test.go +++ b/samplers/aws/xray/internal/rule_test.go @@ -31,7 +31,7 @@ import ( func TestStaleRule(t *testing.T) { refreshedAt := time.Unix(1500000000, 0) r1 := Rule{ - samplingStatistics: samplingStatistics{ + samplingStatistics: &samplingStatistics{ matchedRequests: 5, }, reservoir: reservoir{ @@ -49,7 +49,7 @@ func TestStaleRule(t *testing.T) { func TestFreshRule(t *testing.T) { refreshedAt := time.Unix(1500000000, 0) r1 := Rule{ - samplingStatistics: samplingStatistics{ + samplingStatistics: &samplingStatistics{ matchedRequests: 5, }, reservoir: reservoir{ @@ -67,7 +67,7 @@ func TestFreshRule(t *testing.T) { func TestInactiveRule(t *testing.T) { refreshedAt := time.Unix(1500000000, 0) r1 := Rule{ - samplingStatistics: samplingStatistics{ + samplingStatistics: &samplingStatistics{ matchedRequests: 0, }, reservoir: reservoir{ @@ -87,7 +87,7 @@ func TestSnapshot(t *testing.T) { ruleProperties: ruleProperties{ RuleName: "r1", }, - samplingStatistics: samplingStatistics{ + samplingStatistics: &samplingStatistics{ matchedRequests: 100, sampledRequests: 12, borrowedRequests: 2, @@ -121,6 +121,7 @@ func TestExpiredReservoirBorrowSample(t *testing.T) { RuleName: "r1", FixedRate: 0.06, }, + samplingStatistics: &samplingStatistics{}, } now := time.Unix(1500000062, 0) @@ -145,6 +146,7 @@ func TestExpiredReservoirTraceIDRationBasedSample(t *testing.T) { RuleName: "r1", FixedRate: 0.06, }, + samplingStatistics: &samplingStatistics{}, } now := time.Unix(1500000061, 0) @@ -167,6 +169,7 @@ func TestConsumeFromReservoirSample(t *testing.T) { expiresAt: time.Unix(1500000060, 0), mu: &sync.RWMutex{}, }, + samplingStatistics: &samplingStatistics{}, } now := time.Unix(1500000000, 0) @@ -191,6 +194,7 @@ func TestTraceIDRatioBasedSampler_ReservoirIsConsumedSample(t *testing.T) { FixedRate: 0.05, RuleName: "r1", }, + samplingStatistics: &samplingStatistics{}, } now := time.Unix(1500000000, 0) @@ -215,6 +219,7 @@ func TestTraceIDRatioBasedSamplerFixedRateZero(t *testing.T) { FixedRate: 0, RuleName: "r1", }, + samplingStatistics: &samplingStatistics{}, } now := time.Unix(1500000000, 0) From 84730a1f12e2f7c1ebdcb8f7fcef93ee64e8959d Mon Sep 17 00:00:00 2001 From: bhautikpip Date: Tue, 8 Mar 2022 15:42:01 -0800 Subject: [PATCH 18/38] fix test failures --- samplers/aws/xray/internal/manifest_test.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/samplers/aws/xray/internal/manifest_test.go b/samplers/aws/xray/internal/manifest_test.go index c683927bd38..27ee1e27684 100644 --- a/samplers/aws/xray/internal/manifest_test.go +++ b/samplers/aws/xray/internal/manifest_test.go @@ -327,6 +327,7 @@ func TestRefreshManifestRules(t *testing.T) { capacity: 60, mu: &sync.RWMutex{}, }, + samplingStatistics: &samplingStatistics{}, } r2 := Rule{ @@ -348,6 +349,7 @@ func TestRefreshManifestRules(t *testing.T) { capacity: 3, mu: &sync.RWMutex{}, }, + samplingStatistics: &samplingStatistics{}, } r3 := Rule{ @@ -369,6 +371,7 @@ func TestRefreshManifestRules(t *testing.T) { capacity: 100, mu: &sync.RWMutex{}, }, + samplingStatistics: &samplingStatistics{}, } // Assert on sorting order @@ -617,6 +620,7 @@ func TestRefreshManifestAddOneInvalidRule(t *testing.T) { capacity: 60, mu: &sync.RWMutex{}, }, + samplingStatistics: &samplingStatistics{}, } // generate a test server so we can capture and inspect the request From 98df8a44ef0ae8a036b3a3e9a6f1b01955dc52ec Mon Sep 17 00:00:00 2001 From: bhautikpip Date: Wed, 9 Mar 2022 14:38:43 -0800 Subject: [PATCH 19/38] removed epoch based logic when borrowing --- samplers/aws/xray/fallback_sampler.go | 50 +++++++++-- samplers/aws/xray/fallback_sampler_test.go | 22 ++++- samplers/aws/xray/internal/manifest.go | 4 +- samplers/aws/xray/internal/reservoir.go | 56 ++++++------ samplers/aws/xray/internal/reservoir_test.go | 93 ++++++++++++++++---- samplers/aws/xray/internal/rule.go | 15 ++-- samplers/aws/xray/internal/rule_test.go | 8 +- 7 files changed, 175 insertions(+), 73 deletions(-) diff --git a/samplers/aws/xray/fallback_sampler.go b/samplers/aws/xray/fallback_sampler.go index 04e97ba6f0a..3df69c4fece 100644 --- a/samplers/aws/xray/fallback_sampler.go +++ b/samplers/aws/xray/fallback_sampler.go @@ -15,7 +15,7 @@ package xray // import "go.opentelemetry.io/contrib/samplers/aws/xray" import ( - "sync/atomic" + "fmt" "time" sdktrace "go.opentelemetry.io/otel/sdk/trace" @@ -23,7 +23,8 @@ import ( ) type FallbackSampler struct { - currentEpoch int64 + lastTick time.Time + quotaBalance float64 defaultSampler sdktrace.Sampler } @@ -34,19 +35,22 @@ var _ sdktrace.Sampler = (*FallbackSampler)(nil) func NewFallbackSampler() *FallbackSampler { return &FallbackSampler{ defaultSampler: sdktrace.TraceIDRatioBased(0.05), + quotaBalance: 1.0, } } // ShouldSample implements the logic of borrowing 1 req/sec and then use traceIDRatioBasedSampler to sample 5% of additional requests. func (fs *FallbackSampler) ShouldSample(parameters sdktrace.SamplingParameters) sdktrace.SamplingResult { // borrowing one request every second - if fs.borrow(time.Now().Unix()) { + if fs.take(time.Now(), 1.0) { + fmt.Println("inside borrow") return sdktrace.SamplingResult{ Tracestate: trace.SpanContextFromContext(parameters.ParentContext).TraceState(), Decision: sdktrace.RecordAndSample, } } + fmt.Println("outside borrow") // traceIDRatioBasedSampler to sample 5% of additional requests every second return fs.defaultSampler.ShouldSample(parameters) } @@ -60,10 +64,40 @@ func (fs *FallbackSampler) getDescription() string { return "fallback sampling with sampling config of 1 req/sec and 5% of additional requests" } -func (fs *FallbackSampler) borrow(now int64) bool { - cur := atomic.LoadInt64(&fs.currentEpoch) - if cur >= now { - return false +// take consumes quota from reservoir, if any remains, then returns true. False otherwise. +func (fs *FallbackSampler) take(now time.Time, itemCost float64) bool { + if fs.lastTick.IsZero() { + fs.lastTick = now + } + + if fs.quotaBalance >= itemCost { + fs.quotaBalance -= itemCost + return true + } + + // update quota balance based on elapsed time + fs.refreshQuotaBalance(now) + + if fs.quotaBalance >= itemCost { + fs.quotaBalance -= itemCost + return true + } + + return false +} + +// refreshQuotaBalance refreshes the quotaBalance considering elapsedTime. +func (fs *FallbackSampler) refreshQuotaBalance(now time.Time) { + currentTime := now + elapsedTime := currentTime.Sub(fs.lastTick) + fs.lastTick = currentTime + + // when elapsedTime is higher than 1 even then we need to keep quotaBalance + // near to 1 so making elapsedTime to 1 for only borrowing 1 per second case + if elapsedTime.Seconds() > 1.0 { + fs.quotaBalance += 1.0 + } else { + // calculate how much credit have we accumulated since the last tick + fs.quotaBalance += elapsedTime.Seconds() * 1 } - return atomic.CompareAndSwapInt64(&fs.currentEpoch, cur, now) } diff --git a/samplers/aws/xray/fallback_sampler_test.go b/samplers/aws/xray/fallback_sampler_test.go index 52fef3c73bf..cb68f551807 100644 --- a/samplers/aws/xray/fallback_sampler_test.go +++ b/samplers/aws/xray/fallback_sampler_test.go @@ -16,6 +16,7 @@ package xray import ( "testing" + "time" "github.com/stretchr/testify/assert" @@ -26,6 +27,7 @@ import ( func TestSampleUsingFallbackSampler(t *testing.T) { fs := NewFallbackSampler() assert.NotEmpty(t, fs.defaultSampler) + assert.Equal(t, fs.quotaBalance, 1.0) sd := fs.ShouldSample(trace.SamplingParameters{}) assert.Equal(t, trace.RecordAndSample, sd.Decision) @@ -34,22 +36,36 @@ func TestSampleUsingFallbackSampler(t *testing.T) { // assert that we only borrow 1 req/sec. func TestBorrowOnePerSecond(t *testing.T) { fs := NewFallbackSampler() - borrowed := fs.borrow(1500000000) + borrowed := fs.take(time.Unix(1500000000, 0), 1.0) // assert that borrowing one per second assert.True(t, borrowed) - borrowed = fs.borrow(1500000000) + borrowed = fs.take(time.Unix(1500000000, 0), 1.0) // assert that borrowing again is false during that second assert.False(t, borrowed) - borrowed = fs.borrow(1500000001) + borrowed = fs.take(time.Unix(1500000001, 0), 1.0) // assert that borrowing again in next second assert.True(t, borrowed) } +// assert that when elapsedTime is high quotaBalance should still be close to 1. +func TestBorrowWithLargeElapsedTime(t *testing.T) { + fs := NewFallbackSampler() + borrowed := fs.take(time.Unix(1500000000, 0), 1.0) + + // assert that borrowing one per second + assert.True(t, borrowed) + + // Increase the time by 9 seconds + borrowed = fs.take(time.Unix(1500000009, 0), 1.0) + assert.True(t, borrowed) + assert.Equal(t, fs.quotaBalance, 0.0) +} + // assert fallback sampling description. func TestFallbackSamplerDescription(t *testing.T) { fs := NewFallbackSampler() diff --git a/samplers/aws/xray/internal/manifest.go b/samplers/aws/xray/internal/manifest.go index fa6fd774aa2..e24e967665d 100644 --- a/samplers/aws/xray/internal/manifest.go +++ b/samplers/aws/xray/internal/manifest.go @@ -74,8 +74,8 @@ func (m *Manifest) Expired() bool { m.mu.RLock() defer m.mu.RUnlock() - now := time.Unix(m.Clock.Now().Unix()-manifestTTL, 0) - return now.After(m.refreshedAt) + manifestLiveTime := m.refreshedAt.Add(time.Second * manifestTTL) + return m.Clock.Now().After(manifestLiveTime) } // MatchAgainstManifestRules returns a Rule and boolean flag set as true diff --git a/samplers/aws/xray/internal/reservoir.go b/samplers/aws/xray/internal/reservoir.go index a22f0aea7f1..c4b98a5f74d 100644 --- a/samplers/aws/xray/internal/reservoir.go +++ b/samplers/aws/xray/internal/reservoir.go @@ -43,9 +43,6 @@ type reservoir struct { // stores reservoir ticks lastTick time.Time - // stores borrow ticks - borrowTick time.Time - mu *sync.RWMutex } @@ -57,33 +54,19 @@ func (r *reservoir) expired(now time.Time) bool { return now.After(r.expiresAt) } -// borrow returns true if the reservoir has not been borrowed from this epoch. -func (r *reservoir) borrow(now time.Time) bool { - r.mu.Lock() - defer r.mu.Unlock() - - currentTime := now - - if currentTime.Unix() == r.borrowTick.Unix() { - return false - } - - if currentTime.After(r.borrowTick) { - r.borrowTick = currentTime - return true - } - - return false -} - // take consumes quota from reservoir, if any remains, then returns true. False otherwise. -func (r *reservoir) take(now time.Time, itemCost float64) bool { +func (r *reservoir) take(now time.Time, borrowed bool, itemCost float64) bool { r.mu.Lock() defer r.mu.Unlock() if r.lastTick.IsZero() { r.lastTick = now - r.quotaBalance = r.quota + + if borrowed { + r.quotaBalance = 1.0 + } else { + r.quotaBalance = r.quota + } } if r.quotaBalance >= itemCost { @@ -92,7 +75,7 @@ func (r *reservoir) take(now time.Time, itemCost float64) bool { } // update quota balance based on elapsed time - r.refreshQuotaBalance(now) + r.refreshQuotaBalance(now, borrowed) if r.quotaBalance >= itemCost { r.quotaBalance -= itemCost @@ -102,15 +85,28 @@ func (r *reservoir) take(now time.Time, itemCost float64) bool { return false } -func (r *reservoir) refreshQuotaBalance(now time.Time) { +// refreshQuotaBalance refreshes the quotaBalance. if borrowed true then add quota balance 1 by every second +// otherwise add quota balance based on assigned quota by X-Ray service +func (r *reservoir) refreshQuotaBalance(now time.Time, borrowed bool) { currentTime := now elapsedTime := currentTime.Sub(r.lastTick) r.lastTick = currentTime // calculate how much credit have we accumulated since the last tick - totalQuotaBalanceCapacity := elapsedTime.Seconds() * r.capacity - r.quotaBalance += elapsedTime.Seconds() * r.quota - if r.quotaBalance > totalQuotaBalanceCapacity { - r.quotaBalance = totalQuotaBalanceCapacity + if borrowed { + // when elapsedTime is higher than 1 even then we need to keep quotaBalance + // near to 1 so making elapsedTime to 1 for only borrowing 1 per second case + if elapsedTime.Seconds() > 1.0 { + r.quotaBalance += 1.0 + } else { + r.quotaBalance += elapsedTime.Seconds() * 1 + } + } else { + totalQuotaBalanceCapacity := elapsedTime.Seconds() * r.capacity + r.quotaBalance += elapsedTime.Seconds() * r.quota + + if r.quotaBalance > totalQuotaBalanceCapacity { + r.quotaBalance = totalQuotaBalanceCapacity + } } } diff --git a/samplers/aws/xray/internal/reservoir_test.go b/samplers/aws/xray/internal/reservoir_test.go index 2b62b3a2db2..9a602ac5325 100644 --- a/samplers/aws/xray/internal/reservoir_test.go +++ b/samplers/aws/xray/internal/reservoir_test.go @@ -59,7 +59,7 @@ func TestExpiredReservoirSameAsClockTime(t *testing.T) { assert.False(t, expired) } -// Assert that borrow only 1 req/sec +// assert that borrow only 1 req/sec func TestBorrowEverySecond(t *testing.T) { clock := &util.MockClock{ NowTime: 1500000000, @@ -70,10 +70,10 @@ func TestBorrowEverySecond(t *testing.T) { mu: &sync.RWMutex{}, } - s := r.borrow(clock.Now()) + s := r.take(clock.Now(), true, 1.0) assert.True(t, s) - s = r.borrow(clock.Now()) + s = r.take(clock.Now(), true, 1.0) assert.False(t, s) // Increment clock by 1 @@ -81,10 +81,52 @@ func TestBorrowEverySecond(t *testing.T) { NowTime: 1500000001, } - s = r.borrow(clock.Now()) + s = r.take(clock.Now(), true, 1.0) assert.True(t, s) } +// assert that when reservoir is expired we consume from quota is 1 and then +// when reservoir is not expired consume from assigned quota by X-Ray service +func TestConsumeFromBorrow_ConsumeFromQuota(t *testing.T) { + clock := &util.MockClock{ + NowTime: 1500000000, + } + + r := &reservoir{ + quota: 2, + capacity: 10, + mu: &sync.RWMutex{}, + } + + s := r.take(clock.Now(), true, 1.0) + assert.True(t, s) + + s = r.take(clock.Now(), true, 1.0) + assert.False(t, s) + + // Increment clock by 1 + clock = &util.MockClock{ + NowTime: 1500000001, + } + + s = r.take(clock.Now(), true, 1.0) + assert.True(t, s) + + // Increment clock by 1 + clock = &util.MockClock{ + NowTime: 1500000002, + } + + s = r.take(clock.Now(), false, 1.0) + assert.True(t, s) + + s = r.take(clock.Now(), false, 1.0) + assert.True(t, s) + + s = r.take(clock.Now(), false, 1.0) + assert.False(t, s) +} + // assert that we can still borrowing from reservoir is possible since assigned quota is available to consume // and it will increase used count. func TestConsumeFromReservoir(t *testing.T) { @@ -101,31 +143,31 @@ func TestConsumeFromReservoir(t *testing.T) { // reservoir updates the quotaBalance for new second and allows to consume // quota balance is 0 because we are consuming from reservoir for the first time assert.Equal(t, r.quotaBalance, 0.0) - assert.True(t, r.take(clock.Now(), 1.0)) + assert.True(t, r.take(clock.Now(), false, 1.0)) assert.Equal(t, r.quotaBalance, 1.0) - assert.True(t, r.take(clock.Now(), 1.0)) + assert.True(t, r.take(clock.Now(), false, 1.0)) assert.Equal(t, r.quotaBalance, 0.0) // once assigned quota is consumed reservoir does not allow to consume in that second - assert.False(t, r.take(clock.Now(), 1.0)) + assert.False(t, r.take(clock.Now(), false, 1.0)) // increase the clock by 1 clock.NowTime = 1500000001 // reservoir updates the quotaBalance for new second and allows to consume assert.Equal(t, r.quotaBalance, 0.0) - assert.True(t, r.take(clock.Now(), 1.0)) + assert.True(t, r.take(clock.Now(), false, 1.0)) assert.Equal(t, r.quotaBalance, 1.0) - assert.True(t, r.take(clock.Now(), 1.0)) + assert.True(t, r.take(clock.Now(), false, 1.0)) assert.Equal(t, r.quotaBalance, 0.0) // once assigned quota is consumed reservoir does not allow to consume in that second - assert.False(t, r.take(clock.Now(), 1.0)) + assert.False(t, r.take(clock.Now(), false, 1.0)) // increase the clock by 5 clock.NowTime = 1500000005 // elapsedTime is 4 seconds so quota balance should be elapsedTime * quota = 8 and below take would consume 1 so // ultimately 7 - assert.True(t, r.take(clock.Now(), 1.0)) + assert.True(t, r.take(clock.Now(), false, 1.0)) assert.Equal(t, r.quotaBalance, 7.0) } @@ -142,12 +184,12 @@ func TestResetQuotaUsageRotation(t *testing.T) { // consume quota for second for i := 0; i < 5; i++ { - taken := r.take(clock.Now(), 1.0) + taken := r.take(clock.Now(), false, 1.0) assert.Equal(t, true, taken) } // take() should be false since no unused quota left - taken := r.take(clock.Now(), 1.0) + taken := r.take(clock.Now(), false, 1.0) assert.Equal(t, false, taken) // increment epoch to reset unused quota @@ -156,13 +198,13 @@ func TestResetQuotaUsageRotation(t *testing.T) { } // take() should be true since ununsed quota is available - taken = r.take(clock.Now(), 1.0) + taken = r.take(clock.Now(), false, 1.0) assert.Equal(t, true, taken) } // assert that when quotaBalance exceeds totalQuotaBalanceCapacity then totalQuotaBalanceCapacity // gets assigned to quotaBalance -func TestQuotaBalanceExceedsCapacity(t *testing.T) { +func TestQuotaBalanceNonBorrow_ExceedsCapacity(t *testing.T) { clock := &util.MockClock{ NowTime: 1500000001, } @@ -174,8 +216,27 @@ func TestQuotaBalanceExceedsCapacity(t *testing.T) { lastTick: time.Unix(1500000000, 0), } - r.refreshQuotaBalance(clock.Now()) + r.refreshQuotaBalance(clock.Now(), false) // assert that if quotaBalance exceeds capacity then total capacity would be new quotaBalance assert.Equal(t, r.quotaBalance, 1*r.capacity) } + +// assert quotaBalance and capacity of borrowing case +func TestQuotaBalanceBorrow(t *testing.T) { + clock := &util.MockClock{ + NowTime: 1500000001, + } + + r := &reservoir{ + quota: 6, + capacity: 5, + mu: &sync.RWMutex{}, + lastTick: time.Unix(1500000000, 0), + } + + r.refreshQuotaBalance(clock.Now(), true) + + // assert that if quotaBalance exceeds capacity then total capacity would be new quotaBalance + assert.Equal(t, r.quotaBalance, 1.0) +} diff --git a/samplers/aws/xray/internal/rule.go b/samplers/aws/xray/internal/rule.go index d614b5d47fb..3c951d3c372 100644 --- a/samplers/aws/xray/internal/rule.go +++ b/samplers/aws/xray/internal/rule.go @@ -57,14 +57,9 @@ func (r *Rule) stale(now time.Time) bool { func (r *Rule) snapshot(now time.Time) *samplingStatisticsDocument { name := r.ruleProperties.RuleName - matchedRequests := atomic.LoadInt64(&r.samplingStatistics.matchedRequests) - sampledRequests := atomic.LoadInt64(&r.samplingStatistics.sampledRequests) - borrowedRequest := atomic.LoadInt64(&r.samplingStatistics.borrowedRequests) - - // reset counters - atomic.CompareAndSwapInt64(&r.samplingStatistics.matchedRequests, matchedRequests, int64(0)) - atomic.CompareAndSwapInt64(&r.samplingStatistics.sampledRequests, sampledRequests, int64(0)) - atomic.CompareAndSwapInt64(&r.samplingStatistics.borrowedRequests, borrowedRequest, int64(0)) + matchedRequests := atomic.SwapInt64(&r.samplingStatistics.matchedRequests, int64(0)) + sampledRequests := atomic.SwapInt64(&r.samplingStatistics.sampledRequests, int64(0)) + borrowedRequest := atomic.SwapInt64(&r.samplingStatistics.borrowedRequests, int64(0)) timeStamp := now.Unix() return &samplingStatisticsDocument{ @@ -88,7 +83,7 @@ func (r *Rule) Sample(parameters sdktrace.SamplingParameters, now time.Time) sdk // fallback sampling logic if quota for a given rule is expired if r.reservoir.expired(now) { // borrowing one request every second - if r.reservoir.borrow(now) { + if r.reservoir.take(now, true, 1.0) { atomic.AddInt64(&r.samplingStatistics.borrowedRequests, int64(1)) sd.Decision = sdktrace.RecordAndSample @@ -106,7 +101,7 @@ func (r *Rule) Sample(parameters sdktrace.SamplingParameters, now time.Time) sdk } // Take from reservoir quota, if quota is available for that second - if r.reservoir.take(now, 1.0) { + if r.reservoir.take(now, false, 1.0) { atomic.AddInt64(&r.samplingStatistics.sampledRequests, int64(1)) sd.Decision = sdktrace.RecordAndSample diff --git a/samplers/aws/xray/internal/rule_test.go b/samplers/aws/xray/internal/rule_test.go index b0165b32703..fdaede4cacf 100644 --- a/samplers/aws/xray/internal/rule_test.go +++ b/samplers/aws/xray/internal/rule_test.go @@ -137,10 +137,10 @@ func TestExpiredReservoirBorrowSample(t *testing.T) { func TestExpiredReservoirTraceIDRationBasedSample(t *testing.T) { r1 := Rule{ reservoir: reservoir{ - expiresAt: time.Unix(1500000060, 0), - capacity: 10, - mu: &sync.RWMutex{}, - borrowTick: time.Unix(1500000061, 0), + expiresAt: time.Unix(1500000060, 0), + capacity: 10, + mu: &sync.RWMutex{}, + lastTick: time.Unix(1500000061, 0), }, ruleProperties: ruleProperties{ RuleName: "r1", From 0737cbd703ad4ff0dc8d45702b5392788f46a0a5 Mon Sep 17 00:00:00 2001 From: bhautikpip Date: Wed, 9 Mar 2022 14:41:40 -0800 Subject: [PATCH 20/38] remove debugging lines --- samplers/aws/xray/fallback_sampler.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/samplers/aws/xray/fallback_sampler.go b/samplers/aws/xray/fallback_sampler.go index 3df69c4fece..5feb623271b 100644 --- a/samplers/aws/xray/fallback_sampler.go +++ b/samplers/aws/xray/fallback_sampler.go @@ -15,7 +15,6 @@ package xray // import "go.opentelemetry.io/contrib/samplers/aws/xray" import ( - "fmt" "time" sdktrace "go.opentelemetry.io/otel/sdk/trace" @@ -43,14 +42,12 @@ func NewFallbackSampler() *FallbackSampler { func (fs *FallbackSampler) ShouldSample(parameters sdktrace.SamplingParameters) sdktrace.SamplingResult { // borrowing one request every second if fs.take(time.Now(), 1.0) { - fmt.Println("inside borrow") return sdktrace.SamplingResult{ Tracestate: trace.SpanContextFromContext(parameters.ParentContext).TraceState(), Decision: sdktrace.RecordAndSample, } } - fmt.Println("outside borrow") // traceIDRatioBasedSampler to sample 5% of additional requests every second return fs.defaultSampler.ShouldSample(parameters) } From 7500f1ea8ab91c61b793322cfc747860312b6b0e Mon Sep 17 00:00:00 2001 From: bhautikpip Date: Sun, 13 Mar 2022 14:41:54 -0700 Subject: [PATCH 21/38] address review comments --- samplers/aws/xray/fallback_sampler.go | 13 +- samplers/aws/xray/internal/client.go | 23 ++- samplers/aws/xray/internal/client_test.go | 4 +- samplers/aws/xray/internal/manifest.go | 29 ++-- samplers/aws/xray/internal/manifest_test.go | 153 +++++++++++++++++--- samplers/aws/xray/internal/reservoir.go | 2 +- samplers/aws/xray/internal/rule.go | 15 +- samplers/aws/xray/remote_sampler.go | 102 ++++++------- samplers/aws/xray/remote_sampler_config.go | 37 +++-- samplers/aws/xray/remote_sampler_test.go | 33 ----- 10 files changed, 247 insertions(+), 164 deletions(-) diff --git a/samplers/aws/xray/fallback_sampler.go b/samplers/aws/xray/fallback_sampler.go index 5feb623271b..c10ac27ee85 100644 --- a/samplers/aws/xray/fallback_sampler.go +++ b/samplers/aws/xray/fallback_sampler.go @@ -15,6 +15,7 @@ package xray // import "go.opentelemetry.io/contrib/samplers/aws/xray" import ( + "sync" "time" sdktrace "go.opentelemetry.io/otel/sdk/trace" @@ -25,6 +26,7 @@ type FallbackSampler struct { lastTick time.Time quotaBalance float64 defaultSampler sdktrace.Sampler + mu sync.RWMutex } // Compile time assertion that remoteSampler implements the Sampler interface. @@ -54,15 +56,14 @@ func (fs *FallbackSampler) ShouldSample(parameters sdktrace.SamplingParameters) // Description returns description of the sampler being used func (fs *FallbackSampler) Description() string { - return "FallbackSampler{" + fs.getDescription() + "}" -} - -func (fs *FallbackSampler) getDescription() string { - return "fallback sampling with sampling config of 1 req/sec and 5% of additional requests" + return "FallbackSampler{fallback sampling with sampling config of 1 req/sec and 5% of additional requests}" } // take consumes quota from reservoir, if any remains, then returns true. False otherwise. func (fs *FallbackSampler) take(now time.Time, itemCost float64) bool { + fs.mu.Lock() + defer fs.mu.Unlock() + if fs.lastTick.IsZero() { fs.lastTick = now } @@ -95,6 +96,6 @@ func (fs *FallbackSampler) refreshQuotaBalance(now time.Time) { fs.quotaBalance += 1.0 } else { // calculate how much credit have we accumulated since the last tick - fs.quotaBalance += elapsedTime.Seconds() * 1 + fs.quotaBalance += elapsedTime.Seconds() } } diff --git a/samplers/aws/xray/internal/client.go b/samplers/aws/xray/internal/client.go index 5efbbb0987b..92aa722ede0 100644 --- a/samplers/aws/xray/internal/client.go +++ b/samplers/aws/xray/internal/client.go @@ -23,10 +23,6 @@ import ( "net/url" ) -type getSamplingRulesInput struct { - NextToken *string `json:"NextToken"` -} - // getSamplingRulesOutput is used to store parsed json sampling rules. type getSamplingRulesOutput struct { SamplingRuleRecords []*samplingRuleRecords `json:"SamplingRuleRecords"` @@ -131,23 +127,24 @@ func newClient(addr string) (client *xrayClient, err error) { } // construct resolved URL for getSamplingRules and getSamplingTargets API calls - samplingRulesURL := endpointURL.String() + "/GetSamplingRules" - samplingTargetsURL := endpointURL.String() + "/SamplingTargets" + endpointURL.Path = "/GetSamplingRules" + samplingRulesURL := *endpointURL + + endpointURL.Path = "/SamplingTargets" + samplingTargetsURL := *endpointURL return &xrayClient{ httpClient: &http.Client{}, - samplingRulesURL: samplingRulesURL, - samplingTargetsURL: samplingTargetsURL, + samplingRulesURL: samplingRulesURL.String(), + samplingTargetsURL: samplingTargetsURL.String(), }, nil } // getSamplingRules calls the collector(aws proxy enabled) for sampling rules. func (c *xrayClient) getSamplingRules(ctx context.Context) (*getSamplingRulesOutput, error) { - samplingRulesInput, err := json.Marshal(getSamplingRulesInput{}) - if err != nil { - return nil, err - } - body := bytes.NewReader(samplingRulesInput) + emptySamplingRulesInputJSON := []byte(`{"NextToken": null}`) + + body := bytes.NewReader(emptySamplingRulesInputJSON) req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.samplingRulesURL, body) if err != nil { diff --git a/samplers/aws/xray/internal/client_test.go b/samplers/aws/xray/internal/client_test.go index bf1e31f1e44..12abf569d0e 100644 --- a/samplers/aws/xray/internal/client_test.go +++ b/samplers/aws/xray/internal/client_test.go @@ -271,8 +271,8 @@ func TestNewClient(t *testing.T) { xrayClient, err := newClient("127.0.0.1:2020") require.NoError(t, err) - assert.Equal(t, xrayClient.samplingRulesURL, "http://127.0.0.1:2020/GetSamplingRules") - assert.Equal(t, xrayClient.samplingTargetsURL, "http://127.0.0.1:2020/SamplingTargets") + assert.Equal(t, "http://127.0.0.1:2020/GetSamplingRules", xrayClient.samplingRulesURL) + assert.Equal(t, "http://127.0.0.1:2020/SamplingTargets", xrayClient.samplingTargetsURL) } func TestEndpointIsNotReachable(t *testing.T) { diff --git a/samplers/aws/xray/internal/manifest.go b/samplers/aws/xray/internal/manifest.go index e24e967665d..05131d6fdc4 100644 --- a/samplers/aws/xray/internal/manifest.go +++ b/samplers/aws/xray/internal/manifest.go @@ -18,6 +18,7 @@ import ( "context" crypto "crypto/rand" "fmt" + "math" "sort" "strings" "sync" @@ -41,7 +42,7 @@ type Manifest struct { xrayClient *xrayClient clientID *string logger logr.Logger - Clock util.Clock + clock util.Clock mu sync.RWMutex } @@ -61,7 +62,7 @@ func NewManifest(addr string, logger logr.Logger) (*Manifest, error) { return &Manifest{ xrayClient: client, - Clock: &util.DefaultClock{}, + clock: &util.DefaultClock{}, logger: logger, SamplingTargetsPollingInterval: 10 * time.Second, clientID: clientID, @@ -75,7 +76,7 @@ func (m *Manifest) Expired() bool { defer m.mu.RUnlock() manifestLiveTime := m.refreshedAt.Add(time.Second * manifestTTL) - return m.Clock.Now().After(manifestLiveTime) + return m.clock.Now().After(manifestLiveTime) } // MatchAgainstManifestRules returns a Rule and boolean flag set as true @@ -193,7 +194,7 @@ func (m *Manifest) updateRules(rules *getSamplingRulesOutput) { m.mu.Lock() m.Rules = tempManifest.Rules - m.refreshedAt = m.Clock.Now() + m.refreshedAt = m.clock.Now() m.mu.Unlock() } @@ -268,7 +269,7 @@ func (m *Manifest) updateReservoir(t *samplingTargetDocument) (err error) { for index := range m.Rules { if m.Rules[index].ruleProperties.RuleName == *t.RuleName { - m.Rules[index].reservoir.refreshedAt = m.Clock.Now() + m.Rules[index].reservoir.refreshedAt = m.clock.Now() // Update non-optional attributes from response m.Rules[index].ruleProperties.FixedRate = *t.FixedRate @@ -296,8 +297,8 @@ func (m *Manifest) snapshots() ([]*samplingStatisticsDocument, error) { // Generate sampling statistics for user-defined rules for index := range m.Rules { - if m.Rules[index].stale(m.Clock.Now()) { - s := m.Rules[index].snapshot(m.Clock.Now()) + if m.Rules[index].stale(m.clock.Now()) { + s := m.Rules[index].snapshot(m.clock.Now()) s.ClientID = m.clientID statistics = append(statistics, s) @@ -321,15 +322,15 @@ func (m *Manifest) sort() { } // minimumPollingInterval finds the minimum interval amongst all the targets -func (m *Manifest) minimumPollingInterval() (minPoll time.Duration) { - minPoll = time.Duration(0) +func (m *Manifest) minimumPollingInterval() time.Duration { + if len(m.Rules) == 0 { + return time.Duration(0) + } + + minPoll := time.Duration(math.MaxInt64) for _, rules := range m.Rules { - if minPoll == 0 { + if minPoll >= rules.reservoir.interval { minPoll = rules.reservoir.interval - } else { - if minPoll >= rules.reservoir.interval { - minPoll = rules.reservoir.interval - } } } diff --git a/samplers/aws/xray/internal/manifest_test.go b/samplers/aws/xray/internal/manifest_test.go index 27ee1e27684..2928d1b25af 100644 --- a/samplers/aws/xray/internal/manifest_test.go +++ b/samplers/aws/xray/internal/manifest_test.go @@ -46,7 +46,6 @@ func TestNewManifest(t *testing.T) { assert.NotEmpty(t, m.clientID) assert.NotEmpty(t, m.SamplingTargetsPollingInterval) - assert.NotNil(t, m.Clock) assert.NotNil(t, m.xrayClient) } @@ -58,7 +57,7 @@ func TestExpiredManifest(t *testing.T) { refreshedAt := time.Unix(3700, 0) m := &Manifest{ - Clock: clock, + clock: clock, refreshedAt: refreshedAt, } @@ -302,7 +301,7 @@ func TestRefreshManifestRules(t *testing.T) { m := &Manifest{ Rules: []Rule{}, xrayClient: client, - Clock: &util.DefaultClock{}, + clock: &util.DefaultClock{}, } err = m.RefreshManifestRules(ctx) @@ -429,7 +428,7 @@ func TestRefreshManifestMissingServiceName(t *testing.T) { m := &Manifest{ Rules: []Rule{}, xrayClient: client, - Clock: &util.DefaultClock{}, + clock: &util.DefaultClock{}, } err = m.RefreshManifestRules(ctx) @@ -484,7 +483,7 @@ func TestRefreshManifestMissingRuleName(t *testing.T) { m := &Manifest{ Rules: []Rule{}, xrayClient: client, - Clock: &util.DefaultClock{}, + clock: &util.DefaultClock{}, logger: stdr.NewWithOptions(log.New(os.Stderr, "", log.LstdFlags|log.Lshortfile), stdr.Options{LogCaller: stdr.Error}), } @@ -542,7 +541,7 @@ func TestRefreshManifestIncorrectVersion(t *testing.T) { m := &Manifest{ Rules: []Rule{}, xrayClient: client, - Clock: &util.DefaultClock{}, + clock: &util.DefaultClock{}, logger: stdr.NewWithOptions(log.New(os.Stderr, "", log.LstdFlags|log.Lshortfile), stdr.Options{LogCaller: stdr.Error}), } @@ -639,7 +638,7 @@ func TestRefreshManifestAddOneInvalidRule(t *testing.T) { m := &Manifest{ Rules: []Rule{}, xrayClient: client, - Clock: &util.DefaultClock{}, + clock: &util.DefaultClock{}, logger: stdr.NewWithOptions(log.New(os.Stderr, "", log.LstdFlags|log.Lshortfile), stdr.Options{LogCaller: stdr.Error}), } @@ -685,7 +684,7 @@ func TestRefreshManifestTarget_NoSnapShot(t *testing.T) { m := &Manifest{ Rules: rules, - Clock: clock, + clock: clock, logger: stdr.NewWithOptions(log.New(os.Stderr, "", log.LstdFlags|log.Lshortfile), stdr.Options{LogCaller: stdr.Error}), } @@ -762,7 +761,7 @@ func TestRefreshManifestTargets(t *testing.T) { refreshedAt := time.Unix(18000000, 0) m := &Manifest{ Rules: rules, - Clock: clock, + clock: clock, logger: stdr.NewWithOptions(log.New(os.Stderr, "", log.LstdFlags|log.Lshortfile), stdr.Options{LogCaller: stdr.Error}), xrayClient: client, refreshedAt: refreshedAt, @@ -779,6 +778,112 @@ func TestRefreshManifestTargets(t *testing.T) { assert.Equal(t, m.Rules[0].reservoir.interval, time.Duration(25)) } +// assert that refresh manifest targets successfully updates samplingTargetsPollingInterval. +func TestRefreshManifestTargets_PollIntervalUpdateTest(t *testing.T) { + // RuleName is missing from r2 + body := []byte(`{ + "LastRuleModification": 17000000, + "SamplingTargetDocuments": [ + { + "FixedRate": 0.06, + "Interval": 15, + "ReservoirQuota": 23, + "ReservoirQuotaTTL": 15000000, + "RuleName": "r1" + }, + { + "FixedRate": 0.06, + "Interval": 5, + "ReservoirQuota": 23, + "ReservoirQuotaTTL": 15000000, + "RuleName": "r2" + }, + { + "FixedRate": 0.06, + "Interval": 25, + "ReservoirQuota": 23, + "ReservoirQuotaTTL": 15000000, + "RuleName": "r3" + } + ], + "UnprocessedStatistics": [ + { + "ErrorCode": "200", + "Message": "Ok", + "RuleName": "r3" + } + ] +}`) + + clock := &util.MockClock{ + NowTime: 150, + } + + r1 := Rule{ + ruleProperties: ruleProperties{ + RuleName: "r1", + }, + reservoir: reservoir{ + mu: &sync.RWMutex{}, + }, + samplingStatistics: &samplingStatistics{ + matchedRequests: int64(5), + }, + } + + r2 := Rule{ + ruleProperties: ruleProperties{ + RuleName: "r2", + }, + reservoir: reservoir{ + mu: &sync.RWMutex{}, + }, + samplingStatistics: &samplingStatistics{}, + } + + r3 := Rule{ + ruleProperties: ruleProperties{ + RuleName: "r3", + }, + reservoir: reservoir{ + mu: &sync.RWMutex{}, + }, + samplingStatistics: &samplingStatistics{}, + } + + rules := []Rule{r1, r2, r3} + + // generate a test server so we can capture and inspect the request + testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { + _, err := res.Write(body) + require.NoError(t, err) + })) + defer testServer.Close() + + u, err := url.Parse(testServer.URL) + require.NoError(t, err) + + client, err := newClient(u.Host) + require.NoError(t, err) + + client.samplingTargetsURL = "http://" + u.Host + "/SamplingTargets" + refreshedAt := time.Unix(18000000, 0) + + m := &Manifest{ + Rules: rules, + clock: clock, + logger: stdr.NewWithOptions(log.New(os.Stderr, "", log.LstdFlags|log.Lshortfile), stdr.Options{LogCaller: stdr.Error}), + xrayClient: client, + refreshedAt: refreshedAt, + } + + _, err = m.RefreshManifestTargets(context.Background()) + require.NoError(t, err) + + // assert that sampling rules polling interval is minimum of all target intervals min(15, 5, 25) + assert.Equal(t, m.SamplingTargetsPollingInterval, 5*time.Second) +} + // assert that a valid sampling target updates its rule. func TestUpdateTargets(t *testing.T) { clock := &util.MockClock{ @@ -821,7 +926,7 @@ func TestUpdateTargets(t *testing.T) { m := &Manifest{ Rules: rules, - Clock: clock, + clock: clock, } refresh, err := m.updateTargets(targets) @@ -895,7 +1000,7 @@ func TestUpdateTargetsRefreshFlagTest(t *testing.T) { m := &Manifest{ Rules: rules, refreshedAt: clock.Now(), - Clock: clock, + clock: clock, } refresh, err := m.updateTargets(targets) @@ -955,7 +1060,7 @@ func TestUpdateTargetsUnprocessedStatistics(t *testing.T) { } m := &Manifest{ - Clock: clock, + clock: clock, logger: stdr.NewWithOptions(log.New(os.Stderr, "", log.LstdFlags|log.Lshortfile), stdr.Options{LogCaller: stdr.Error}), } @@ -1152,7 +1257,7 @@ func TestSnapshots(t *testing.T) { m := &Manifest{ Rules: rules, clientID: &id, - Clock: clock, + clock: clock, } // Expected SamplingStatistics structs @@ -1264,7 +1369,7 @@ func TestMixedSnapshots(t *testing.T) { m := &Manifest{ clientID: &id, - Clock: clock, + clock: clock, Rules: rules, } @@ -1365,9 +1470,9 @@ func TestSortBasedOnRuleName(t *testing.T) { // asserts the minimum value of all the targets. func TestMinPollInterval(t *testing.T) { - r1 := Rule{reservoir: reservoir{interval: 10}} - r2 := Rule{reservoir: reservoir{interval: 5}} - r3 := Rule{reservoir: reservoir{interval: 25}} + r1 := Rule{reservoir: reservoir{interval: time.Duration(10)}} + r2 := Rule{reservoir: reservoir{interval: time.Duration(5)}} + r3 := Rule{reservoir: reservoir{interval: time.Duration(25)}} rules := []Rule{r1, r2, r3} m := &Manifest{Rules: rules} @@ -1379,23 +1484,23 @@ func TestMinPollInterval(t *testing.T) { // asserts the minimum value of all the targets when some targets has 0 interval. func TestMinPollIntervalZeroCase(t *testing.T) { - r1 := Rule{reservoir: reservoir{interval: 0}} - r2 := Rule{reservoir: reservoir{interval: 0}} - r3 := Rule{reservoir: reservoir{interval: 5}} + r1 := Rule{reservoir: reservoir{interval: time.Duration(0)}} + r2 := Rule{reservoir: reservoir{interval: time.Duration(0)}} + r3 := Rule{reservoir: reservoir{interval: time.Duration(5)}} rules := []Rule{r1, r2, r3} m := &Manifest{Rules: rules} minPoll := m.minimumPollingInterval() - assert.Equal(t, 5*time.Second, minPoll) + assert.Equal(t, 0*time.Second, minPoll) } // asserts the minimum value of all the targets when some targets has negative interval. func TestMinPollIntervalNegativeCase(t *testing.T) { - r1 := Rule{reservoir: reservoir{interval: -5}} - r2 := Rule{reservoir: reservoir{interval: 0}} - r3 := Rule{reservoir: reservoir{interval: 0}} + r1 := Rule{reservoir: reservoir{interval: time.Duration(-5)}} + r2 := Rule{reservoir: reservoir{interval: time.Duration(0)}} + r3 := Rule{reservoir: reservoir{interval: time.Duration(0)}} rules := []Rule{r1, r2, r3} m := &Manifest{Rules: rules} diff --git a/samplers/aws/xray/internal/reservoir.go b/samplers/aws/xray/internal/reservoir.go index c4b98a5f74d..d065bfda006 100644 --- a/samplers/aws/xray/internal/reservoir.go +++ b/samplers/aws/xray/internal/reservoir.go @@ -99,7 +99,7 @@ func (r *reservoir) refreshQuotaBalance(now time.Time, borrowed bool) { if elapsedTime.Seconds() > 1.0 { r.quotaBalance += 1.0 } else { - r.quotaBalance += elapsedTime.Seconds() * 1 + r.quotaBalance += elapsedTime.Seconds() } } else { totalQuotaBalanceCapacity := elapsedTime.Seconds() * r.capacity diff --git a/samplers/aws/xray/internal/rule.go b/samplers/aws/xray/internal/rule.go index 3c951d3c372..8935f44d6bd 100644 --- a/samplers/aws/xray/internal/rule.go +++ b/samplers/aws/xray/internal/rule.go @@ -187,11 +187,12 @@ func (r *Rule) appliesTo(parameters sdktrace.SamplingParameters, serviceName str } // attributeMatching performs a match on attributes set by users on AWS X-Ray console -func (r *Rule) attributeMatching(parameters sdktrace.SamplingParameters) (match bool, err error) { - match = false - unmatchedCounter := 0 +func (r *Rule) attributeMatching(parameters sdktrace.SamplingParameters) (bool, error) { + match := false + var err error if len(r.ruleProperties.Attributes) > 0 { for key, value := range r.ruleProperties.Attributes { + unmatchedCounter := 0 for _, attrs := range parameters.Attributes { if key == string(attrs.Key) { match, err = wildcardMatch(value, attrs.Value.AsString()) @@ -201,11 +202,11 @@ func (r *Rule) attributeMatching(parameters sdktrace.SamplingParameters) (match } else { unmatchedCounter++ } + + if unmatchedCounter == len(parameters.Attributes) { + return false, nil + } } - if unmatchedCounter == len(parameters.Attributes) { - return false, nil - } - unmatchedCounter = 0 } return match, nil } diff --git a/samplers/aws/xray/remote_sampler.go b/samplers/aws/xray/remote_sampler.go index 9a830ac3ff2..4b03b5b4d77 100644 --- a/samplers/aws/xray/remote_sampler.go +++ b/samplers/aws/xray/remote_sampler.go @@ -18,8 +18,9 @@ import ( "context" "time" - "go.opentelemetry.io/contrib/samplers/aws/xray/internal" "go.opentelemetry.io/contrib/samplers/aws/xray/internal/util" + + "go.opentelemetry.io/contrib/samplers/aws/xray/internal" sdktrace "go.opentelemetry.io/otel/sdk/trace" "github.com/go-logr/logr" @@ -90,83 +91,82 @@ func NewRemoteSampler(ctx context.Context, serviceName string, cloudPlatform str // ShouldSample matches span attributes with retrieved sampling rules and perform sampling, // if rules does not match or manifest is expired then use fallback sampling. func (rs *remoteSampler) ShouldSample(parameters sdktrace.SamplingParameters) sdktrace.SamplingResult { - if !rs.manifest.Expired() { - // match against known rules - r, match, err := rs.manifest.MatchAgainstManifestRules(parameters, rs.serviceName, rs.cloudPlatform) - if err != nil { - rs.logger.Error(err, "regexp matching error while matching span and resource attributes") - return sdktrace.SamplingResult{} - } + if rs.manifest.Expired() { + // Use fallback sampler if manifest is expired + rs.logger.V(5).Info("manifest is expired so using fallback sampling strategy") - if match { - // remote sampling based on rule match - return r.Sample(parameters, rs.manifest.Clock.Now()) - } + return rs.fallbackSampler.ShouldSample(parameters) + } + + // match against known rules + r, match, err := rs.manifest.MatchAgainstManifestRules(parameters, rs.serviceName, rs.cloudPlatform) + if err != nil { + rs.logger.Error(err, "regexp matching error while matching span and resource attributes") + return sdktrace.SamplingResult{} + } + + if match { + // remote sampling based on rule match + return r.Sample(parameters, time.Now()) } - // Use fallback sampler if manifest is expired - // or sampling rules does not match against manifest + // Use fallback sampler if + // sampling rules does not match against manifest rs.logger.V(5).Info("span attributes does not match to the sampling rules or manifest is expired so using fallback sampling strategy") return rs.fallbackSampler.ShouldSample(parameters) } // Description returns description of the sampler being used. func (rs *remoteSampler) Description() string { - return "AwsXrayRemoteSampler{" + rs.getDescription() + "}" -} - -func (rs *remoteSampler) getDescription() string { - return "remote sampling with AWS X-Ray" + return "AwsXrayRemoteSampler{remote sampling with AWS X-Ray}" } func (rs *remoteSampler) start(ctx context.Context) { if !rs.pollerStarted { rs.pollerStarted = true - rs.startPoller(ctx) + go rs.startPoller(ctx) } } // startPoller starts the rule and target poller in a single go routine which runs periodically // to refresh manifest and targets. func (rs *remoteSampler) startPoller(ctx context.Context) { - go func() { - // jitter = 5s, default 300 seconds - rulesTicker := util.NewTicker(rs.samplingRulesPollingInterval, 5*time.Second) + // jitter = 5s, default 300 seconds + rulesTicker := util.NewTicker(rs.samplingRulesPollingInterval, 5*time.Second) - // jitter = 100ms, default 10 seconds - targetTicker := util.NewTicker(rs.manifest.SamplingTargetsPollingInterval, 100*time.Millisecond) + // jitter = 100ms, default 10 seconds + targetTicker := util.NewTicker(rs.manifest.SamplingTargetsPollingInterval, 100*time.Millisecond) - // fetch sampling rules to kick start the remote sampling - rs.refreshManifest(ctx) + // fetch sampling rules to kick start the remote sampling + rs.refreshManifest(ctx) - for { - select { - case _, more := <-rulesTicker.C(): - if !more { - return - } + for { + select { + case _, more := <-rulesTicker.C(): + if !more { + return + } - // fetch sampling rules and updates manifest - rs.refreshManifest(ctx) - continue - case _, more := <-targetTicker.C(): - if !more { - return - } - - // fetch sampling targets and updates manifest - refresh := rs.refreshTargets(ctx) - - // out of band manifest refresh if it manifest is not updated - if refresh { - rs.refreshManifest(ctx) - } - continue - case <-ctx.Done(): + // fetch sampling rules and updates manifest + rs.refreshManifest(ctx) + continue + case _, more := <-targetTicker.C(): + if !more { return } + + // fetch sampling targets and updates manifest + refresh := rs.refreshTargets(ctx) + + // out of band manifest refresh if it manifest is not updated + if refresh { + rs.refreshManifest(ctx) + } + continue + case <-ctx.Done(): + return } - }() + } } // refreshManifest refreshes the manifest retrieved via getSamplingRules API. diff --git a/samplers/aws/xray/remote_sampler_config.go b/samplers/aws/xray/remote_sampler_config.go index cb279d3fbd9..5f1503599f5 100644 --- a/samplers/aws/xray/remote_sampler_config.go +++ b/samplers/aws/xray/remote_sampler_config.go @@ -33,34 +33,45 @@ const ( defaultPollingInterval = 300 ) -// Option is a function that sets config on the sampler. -type Option func(options *config) - type config struct { endpoint string samplingRulesPollingInterval time.Duration logger logr.Logger } +// Option sets configuration on the sampler. +type Option interface { + apply(*config) *config +} + +type optionFunc func(*config) *config + +func (f optionFunc) apply(cfg *config) *config { + return f(cfg) +} + // WithEndpoint sets custom proxy endpoint. func WithEndpoint(endpoint string) Option { - return func(o *config) { - o.endpoint = endpoint - } + return optionFunc(func(cfg *config) *config { + cfg.endpoint = endpoint + return cfg + }) } // WithSamplingRulesPollingInterval sets polling interval for sampling rules. func WithSamplingRulesPollingInterval(polingInterval time.Duration) Option { - return func(o *config) { - o.samplingRulesPollingInterval = polingInterval - } + return optionFunc(func(cfg *config) *config { + cfg.samplingRulesPollingInterval = polingInterval + return cfg + }) } // WithLogger sets custom logging for remote sampling implementation. func WithLogger(l logr.Logger) Option { - return func(o *config) { - o.logger = l - } + return optionFunc(func(cfg *config) *config { + cfg.logger = l + return cfg + }) } func newConfig(opts ...Option) *config { @@ -71,7 +82,7 @@ func newConfig(opts ...Option) *config { } for _, option := range opts { - option(cfg) + option.apply(cfg) } return cfg diff --git a/samplers/aws/xray/remote_sampler_test.go b/samplers/aws/xray/remote_sampler_test.go index 3d7aa57c25a..80cf88785d6 100644 --- a/samplers/aws/xray/remote_sampler_test.go +++ b/samplers/aws/xray/remote_sampler_test.go @@ -15,44 +15,11 @@ package xray import ( - "log" - "os" "testing" - "github.com/go-logr/stdr" - "github.com/stretchr/testify/assert" - - "go.opentelemetry.io/contrib/samplers/aws/xray/internal" - "go.opentelemetry.io/contrib/samplers/aws/xray/internal/util" - sdktrace "go.opentelemetry.io/otel/sdk/trace" ) -// TestShouldSample assert that when manifest is expired sampling happens with 1 req/sec. -func TestShouldSample_ExpiredManifest(t *testing.T) { - clock := &util.MockClock{ - NowTime: 100, - } - - r1 := internal.Rule{} - - rules := []internal.Rule{r1} - - m := &internal.Manifest{ - Rules: rules, - Clock: clock, - } - - rs := &remoteSampler{ - manifest: m, - logger: stdr.NewWithOptions(log.New(os.Stderr, "", log.LstdFlags|log.Lshortfile), stdr.Options{LogCaller: stdr.Error}), - fallbackSampler: NewFallbackSampler(), - } - - sd := rs.ShouldSample(sdktrace.SamplingParameters{}) - assert.Equal(t, sd.Decision, sdktrace.RecordAndSample) -} - // TestRemoteSamplerDescription assert remote sampling description. func TestRemoteSamplerDescription(t *testing.T) { rs := &remoteSampler{} From 8a4c1dd7c4fcddd9eb1078f6aacf58f8cb651d83 Mon Sep 17 00:00:00 2001 From: bhautikpip Date: Mon, 14 Mar 2022 13:59:25 -0700 Subject: [PATCH 22/38] changed endpoint to be url.URL instead of string --- samplers/aws/xray/internal/client.go | 17 ++-- samplers/aws/xray/internal/client_test.go | 20 +++-- samplers/aws/xray/internal/manifest.go | 3 +- samplers/aws/xray/internal/manifest_test.go | 26 +++--- samplers/aws/xray/remote_sampler.go | 7 +- samplers/aws/xray/remote_sampler_config.go | 37 ++++----- .../aws/xray/remote_sampler_config_test.go | 81 ++++++++++++------- 7 files changed, 111 insertions(+), 80 deletions(-) diff --git a/samplers/aws/xray/internal/client.go b/samplers/aws/xray/internal/client.go index 92aa722ede0..5c5cad07c8b 100644 --- a/samplers/aws/xray/internal/client.go +++ b/samplers/aws/xray/internal/client.go @@ -118,20 +118,13 @@ type xrayClient struct { } // newClient returns an HTTP client with proxy endpoint. -func newClient(addr string) (client *xrayClient, err error) { - endpoint := "http://" + addr - - endpointURL, err := url.Parse(endpoint) - if err != nil { - return nil, err - } - +func newClient(endpoint url.URL) (client *xrayClient, err error) { // construct resolved URL for getSamplingRules and getSamplingTargets API calls - endpointURL.Path = "/GetSamplingRules" - samplingRulesURL := *endpointURL + endpoint.Path = "/GetSamplingRules" + samplingRulesURL := endpoint - endpointURL.Path = "/SamplingTargets" - samplingTargetsURL := *endpointURL + endpoint.Path = "/SamplingTargets" + samplingTargetsURL := endpoint return &xrayClient{ httpClient: &http.Client{}, diff --git a/samplers/aws/xray/internal/client_test.go b/samplers/aws/xray/internal/client_test.go index 12abf569d0e..2aeb381d9fb 100644 --- a/samplers/aws/xray/internal/client_test.go +++ b/samplers/aws/xray/internal/client_test.go @@ -99,7 +99,7 @@ func TestGetSamplingRules(t *testing.T) { u, err := url.Parse(testServer.URL) require.NoError(t, err) - client, err := newClient(u.Host) + client, err := newClient(*u) require.NoError(t, err) samplingRules, err := client.getSamplingRules(ctx) @@ -162,7 +162,7 @@ func TestGetSamplingRulesWithMissingValues(t *testing.T) { u, err := url.Parse(testServer.URL) require.NoError(t, err) - client, err := newClient(u.Host) + client, err := newClient(*u) require.NoError(t, err) samplingRules, err := client.getSamplingRules(ctx) @@ -209,7 +209,7 @@ func TestGetSamplingTargets(t *testing.T) { u, err := url.Parse(testServer.URL) require.NoError(t, err) - client, err := newClient(u.Host) + client, err := newClient(*u) require.NoError(t, err) samplingTragets, err := client.getSamplingTargets(ctx, nil) @@ -254,10 +254,10 @@ func TestGetSamplingTargetsMissingValues(t *testing.T) { })) defer testServer.Close() - u, err := url.Parse(testServer.URL) + endpoint, err := url.Parse(testServer.URL) require.NoError(t, err) - client, err := newClient(u.Host) + client, err := newClient(*endpoint) require.NoError(t, err) samplingTragets, err := client.getSamplingTargets(ctx, nil) @@ -268,7 +268,10 @@ func TestGetSamplingTargetsMissingValues(t *testing.T) { } func TestNewClient(t *testing.T) { - xrayClient, err := newClient("127.0.0.1:2020") + endpoint, err := url.Parse("http://127.0.0.1:2020") + require.NoError(t, err) + + xrayClient, err := newClient(*endpoint) require.NoError(t, err) assert.Equal(t, "http://127.0.0.1:2020/GetSamplingRules", xrayClient.samplingRulesURL) @@ -276,7 +279,10 @@ func TestNewClient(t *testing.T) { } func TestEndpointIsNotReachable(t *testing.T) { - client, err := newClient("127.0.0.1:2020") + endpoint, err := url.Parse("http://127.0.0.1:2020") + require.NoError(t, err) + + client, err := newClient(*endpoint) require.NoError(t, err) _, err = client.getSamplingRules(context.Background()) diff --git a/samplers/aws/xray/internal/manifest.go b/samplers/aws/xray/internal/manifest.go index 05131d6fdc4..760511d6739 100644 --- a/samplers/aws/xray/internal/manifest.go +++ b/samplers/aws/xray/internal/manifest.go @@ -19,6 +19,7 @@ import ( crypto "crypto/rand" "fmt" "math" + "net/url" "sort" "strings" "sync" @@ -47,7 +48,7 @@ type Manifest struct { } // NewManifest return manifest object configured with logging, clock and xrayClient. -func NewManifest(addr string, logger logr.Logger) (*Manifest, error) { +func NewManifest(addr url.URL, logger logr.Logger) (*Manifest, error) { // generate client for getSamplingRules and getSamplingTargets API call client, err := newClient(addr) if err != nil { diff --git a/samplers/aws/xray/internal/manifest_test.go b/samplers/aws/xray/internal/manifest_test.go index 2928d1b25af..f0023d0f7d8 100644 --- a/samplers/aws/xray/internal/manifest_test.go +++ b/samplers/aws/xray/internal/manifest_test.go @@ -39,7 +39,11 @@ import ( // assert that new manifest has certain non-nil attributes. func TestNewManifest(t *testing.T) { logger := stdr.NewWithOptions(log.New(os.Stderr, "", log.LstdFlags|log.Lshortfile), stdr.Options{LogCaller: stdr.Error}) - m, err := NewManifest("127.0.0.1:2000", logger) + + endpoint, err := url.Parse("http://127.0.0.1:2020") + require.NoError(t, err) + + m, err := NewManifest(*endpoint, logger) require.NoError(t, err) assert.NotEmpty(t, m.logger) @@ -67,7 +71,10 @@ func TestExpiredManifest(t *testing.T) { // assert that if collector is not enabled at specified endpoint, returns an error func TestRefreshManifestError(t *testing.T) { // collector is not running at port 2020 so expect error - client, err := newClient("127.0.0.1:2020") + endpoint, err := url.Parse("http://127.0.0.1:2020") + require.NoError(t, err) + + client, err := newClient(*endpoint) require.NoError(t, err) m := &Manifest{ @@ -295,7 +302,7 @@ func TestRefreshManifestRules(t *testing.T) { u, err := url.Parse(testServer.URL) require.NoError(t, err) - client, err := newClient(u.Host) + client, err := newClient(*u) require.NoError(t, err) m := &Manifest{ @@ -422,7 +429,7 @@ func TestRefreshManifestMissingServiceName(t *testing.T) { u, err := url.Parse(testServer.URL) require.NoError(t, err) - client, err := newClient(u.Host) + client, err := newClient(*u) require.NoError(t, err) m := &Manifest{ @@ -477,7 +484,7 @@ func TestRefreshManifestMissingRuleName(t *testing.T) { u, err := url.Parse(testServer.URL) require.NoError(t, err) - client, err := newClient(u.Host) + client, err := newClient(*u) require.NoError(t, err) m := &Manifest{ @@ -535,7 +542,7 @@ func TestRefreshManifestIncorrectVersion(t *testing.T) { u, err := url.Parse(testServer.URL) require.NoError(t, err) - client, err := newClient(u.Host) + client, err := newClient(*u) require.NoError(t, err) m := &Manifest{ @@ -632,7 +639,7 @@ func TestRefreshManifestAddOneInvalidRule(t *testing.T) { u, err := url.Parse(testServer.URL) require.NoError(t, err) - client, err := newClient(u.Host) + client, err := newClient(*u) require.NoError(t, err) m := &Manifest{ @@ -756,7 +763,7 @@ func TestRefreshManifestTargets(t *testing.T) { u, err := url.Parse(testServer.URL) require.NoError(t, err) - client, err := newClient(u.Host) + client, err := newClient(*u) require.NoError(t, err) refreshedAt := time.Unix(18000000, 0) m := &Manifest{ @@ -863,10 +870,9 @@ func TestRefreshManifestTargets_PollIntervalUpdateTest(t *testing.T) { u, err := url.Parse(testServer.URL) require.NoError(t, err) - client, err := newClient(u.Host) + client, err := newClient(*u) require.NoError(t, err) - client.samplingTargetsURL = "http://" + u.Host + "/SamplingTargets" refreshedAt := time.Unix(18000000, 0) m := &Manifest{ diff --git a/samplers/aws/xray/remote_sampler.go b/samplers/aws/xray/remote_sampler.go index 4b03b5b4d77..6a3995a4e1e 100644 --- a/samplers/aws/xray/remote_sampler.go +++ b/samplers/aws/xray/remote_sampler.go @@ -59,10 +59,13 @@ var _ sdktrace.Sampler = (*remoteSampler)(nil) // sampling rules and sampling targets. func NewRemoteSampler(ctx context.Context, serviceName string, cloudPlatform string, opts ...Option) (sdktrace.Sampler, error) { // create new config based on options or set to default values - cfg := newConfig(opts...) + cfg, err := newConfig(opts...) + if err != nil { + return nil, err + } // validate config - err := validateConfig(cfg) + err = validateConfig(cfg) if err != nil { return nil, err } diff --git a/samplers/aws/xray/remote_sampler_config.go b/samplers/aws/xray/remote_sampler_config.go index 5f1503599f5..d0f38805b0e 100644 --- a/samplers/aws/xray/remote_sampler_config.go +++ b/samplers/aws/xray/remote_sampler_config.go @@ -18,9 +18,9 @@ import ( "fmt" "log" "math" + "net/url" "os" "regexp" - "strconv" "strings" "time" @@ -29,12 +29,11 @@ import ( ) const ( - defaultProxyEndpoint = "127.0.0.1:2000" defaultPollingInterval = 300 ) type config struct { - endpoint string + endpoint url.URL samplingRulesPollingInterval time.Duration logger logr.Logger } @@ -51,7 +50,7 @@ func (f optionFunc) apply(cfg *config) *config { } // WithEndpoint sets custom proxy endpoint. -func WithEndpoint(endpoint string) Option { +func WithEndpoint(endpoint url.URL) Option { return optionFunc(func(cfg *config) *config { cfg.endpoint = endpoint return cfg @@ -74,9 +73,14 @@ func WithLogger(l logr.Logger) Option { }) } -func newConfig(opts ...Option) *config { +func newConfig(opts ...Option) (*config, error) { + defaultProxyEndpoint, err := url.Parse("http://127.0.0.1:2000") + if err != nil { + return nil, err + } + cfg := &config{ - endpoint: defaultProxyEndpoint, + endpoint: *defaultProxyEndpoint, samplingRulesPollingInterval: defaultPollingInterval * time.Second, logger: stdr.NewWithOptions(log.New(os.Stderr, "", log.LstdFlags|log.Lshortfile), stdr.Options{LogCaller: stdr.Error}), } @@ -85,35 +89,32 @@ func newConfig(opts ...Option) *config { option.apply(cfg) } - return cfg + return cfg, nil } func validateConfig(cfg *config) (err error) { // check endpoint follows certain format - split := strings.Split(cfg.endpoint, ":") + endpointHostSplit := strings.Split(cfg.endpoint.Host, ":") - if len(split) > 2 { - return fmt.Errorf("endpoint validation error: expected format is 127.0.0.1:8080") + if len(endpointHostSplit) > 2 { + return fmt.Errorf("config validation error: expected endpoint host format is hostname:port") } + hostName := endpointHostSplit[0] + // validate host name r, err := regexp.Compile("[^A-Za-z0-9.]") if err != nil { return err } - if r.MatchString(split[0]) { - return fmt.Errorf("endpoint validation error: expected format is 127.0.0.1:8080") - } - - // validate port - if _, err := strconv.Atoi(split[1]); err != nil { - return fmt.Errorf("endpoint validation error: expected format is 127.0.0.1:8080") + if r.MatchString(hostName) || hostName == "" { + return fmt.Errorf("config validation error: host name should not contain special characters or empty") } // validate polling interval is positive if math.Signbit(float64(cfg.samplingRulesPollingInterval)) { - return fmt.Errorf("endpoint validation error: samplingRulesPollingInterval should be positive number") + return fmt.Errorf("config validation error: samplingRulesPollingInterval should be positive number") } return diff --git a/samplers/aws/xray/remote_sampler_config_test.go b/samplers/aws/xray/remote_sampler_config_test.go index 457e9fa850d..dcf2bbce619 100644 --- a/samplers/aws/xray/remote_sampler_config_test.go +++ b/samplers/aws/xray/remote_sampler_config_test.go @@ -16,10 +16,13 @@ package xray import ( "log" + "net/url" "os" "testing" "time" + "github.com/stretchr/testify/require" + "github.com/go-logr/logr" "github.com/go-logr/stdr" "github.com/stretchr/testify/assert" @@ -27,75 +30,93 @@ import ( // assert that user provided values are tied to config. func TestNewConfig(t *testing.T) { - cfg := newConfig(WithSamplingRulesPollingInterval(400*time.Second), WithEndpoint("127.0.0.1:5000"), WithLogger(logr.Logger{})) + endpoint, err := url.Parse("https://127.0.0.1:5000") + require.NoError(t, err) + + cfg, err := newConfig(WithSamplingRulesPollingInterval(400*time.Second), WithEndpoint(*endpoint), WithLogger(logr.Logger{})) + require.NoError(t, err) assert.Equal(t, cfg.samplingRulesPollingInterval, 400*time.Second) - assert.Equal(t, cfg.endpoint, "127.0.0.1:5000") + assert.Equal(t, cfg.endpoint, *endpoint) assert.Equal(t, cfg.logger, logr.Logger{}) } // assert that when user did not provide values are then config would be picked up from default values. func TestDefaultConfig(t *testing.T) { - cfg := newConfig() + endpoint, err := url.Parse("http://127.0.0.1:2000") + require.NoError(t, err) + + cfg, err := newConfig() + require.NoError(t, err) assert.Equal(t, cfg.samplingRulesPollingInterval, 300*time.Second) - assert.Equal(t, cfg.endpoint, "127.0.0.1:2000") + assert.Equal(t, cfg.endpoint, *endpoint) assert.Equal(t, cfg.logger, stdr.NewWithOptions(log.New(os.Stderr, "", log.LstdFlags|log.Lshortfile), stdr.Options{LogCaller: stdr.Error})) } // assert when some config is provided by user then other config will be picked up from default config. func TestPartialUserProvidedConfig(t *testing.T) { - cfg := newConfig(WithSamplingRulesPollingInterval(500 * time.Second)) + endpoint, err := url.Parse("http://127.0.0.1:2000") + require.NoError(t, err) + + cfg, err := newConfig(WithSamplingRulesPollingInterval(500 * time.Second)) + require.NoError(t, err) assert.Equal(t, cfg.samplingRulesPollingInterval, 500*time.Second) - assert.Equal(t, cfg.endpoint, "127.0.0.1:2000") + assert.Equal(t, cfg.endpoint, *endpoint) assert.Equal(t, cfg.logger, stdr.NewWithOptions(log.New(os.Stderr, "", log.LstdFlags|log.Lshortfile), stdr.Options{LogCaller: stdr.Error})) } -// assert not expected endpoint format leads to an error (expected format: "127.0.0.1:2020"). -func TestValidateConfigIncorrectEndpoint(t *testing.T) { - cfg := newConfig(WithEndpoint("http://127.0.0.1:2000")) +// assert that valid endpoint would not result in an error. +func TestValidEndpoint(t *testing.T) { + endpoint, err := url.Parse("http://127.0.0.1:2000") + require.NoError(t, err) - err := validateConfig(cfg) - assert.Error(t, err) + cfg, err := newConfig(WithEndpoint(*endpoint)) + require.NoError(t, err) + + err = validateConfig(cfg) + assert.NoError(t, err) } -// assert not expected endpoint format leads to an error (expected format: "127.0.0.1:2020"). -func TestValidateConfigSpecialCharacterEndpoint(t *testing.T) { - cfg := newConfig(WithEndpoint("@127.0.0.1:2000")) +// assert that host name with special character would result in an error. +func TestValidateHostNameWithSpecialCharacterEndpoint(t *testing.T) { + endpoint, err := url.Parse("http://127.0.0.1@:2000") + require.NoError(t, err) + + cfg, err := newConfig(WithEndpoint(*endpoint)) + require.NoError(t, err) - err := validateConfig(cfg) + err = validateConfig(cfg) assert.Error(t, err) } -// assert not expected endpoint format leads to an error (expected format: "127.0.0.1:2020"). -func TestValidateConfigLocalHost(t *testing.T) { - cfg := newConfig(WithEndpoint("localhost:2000")) - - err := validateConfig(cfg) - assert.NoError(t, err) -} +// assert that endpoint without host name would result in an error. +func TestValidateInvalidEndpoint(t *testing.T) { + endpoint, err := url.Parse("https://") + require.NoError(t, err) -// assert not expected endpoint format leads to an error (expected format: "127.0.0.1:2020"). -func TestValidateConfigInvalidPort(t *testing.T) { - cfg := newConfig(WithEndpoint("127.0.0.1:abcd")) + cfg, err := newConfig(WithEndpoint(*endpoint)) + require.NoError(t, err) - err := validateConfig(cfg) + err = validateConfig(cfg) assert.Error(t, err) } // assert negative sampling rules interval leads to an error. func TestValidateConfigNegativeDuration(t *testing.T) { - cfg := newConfig(WithSamplingRulesPollingInterval(-300 * time.Second)) + cfg, err := newConfig(WithSamplingRulesPollingInterval(-300 * time.Second)) + require.NoError(t, err) - err := validateConfig(cfg) + err = validateConfig(cfg) assert.Error(t, err) } // assert positive sampling rules interval. func TestValidateConfigPositiveDuration(t *testing.T) { - cfg := newConfig(WithSamplingRulesPollingInterval(300 * time.Second)) + cfg, err := newConfig(WithSamplingRulesPollingInterval(300 * time.Second)) + require.NoError(t, err) - err := validateConfig(cfg) + err = validateConfig(cfg) assert.NoError(t, err) } From 0cf3224c740202e79bd33649f307941f9954ee55 Mon Sep 17 00:00:00 2001 From: bhautikpip Date: Thu, 17 Mar 2022 11:01:33 -0700 Subject: [PATCH 23/38] added validate config inside new config and minor change --- samplers/aws/xray/internal/rule.go | 7 +++---- samplers/aws/xray/remote_sampler.go | 6 ------ samplers/aws/xray/remote_sampler_config.go | 6 ++++++ .../aws/xray/remote_sampler_config_test.go | 20 ++++--------------- 4 files changed, 13 insertions(+), 26 deletions(-) diff --git a/samplers/aws/xray/internal/rule.go b/samplers/aws/xray/internal/rule.go index 8935f44d6bd..88433e676de 100644 --- a/samplers/aws/xray/internal/rule.go +++ b/samplers/aws/xray/internal/rule.go @@ -202,10 +202,9 @@ func (r *Rule) attributeMatching(parameters sdktrace.SamplingParameters) (bool, } else { unmatchedCounter++ } - - if unmatchedCounter == len(parameters.Attributes) { - return false, nil - } + } + if unmatchedCounter == len(parameters.Attributes) { + return false, nil } } return match, nil diff --git a/samplers/aws/xray/remote_sampler.go b/samplers/aws/xray/remote_sampler.go index 6a3995a4e1e..be7e7aff623 100644 --- a/samplers/aws/xray/remote_sampler.go +++ b/samplers/aws/xray/remote_sampler.go @@ -64,12 +64,6 @@ func NewRemoteSampler(ctx context.Context, serviceName string, cloudPlatform str return nil, err } - // validate config - err = validateConfig(cfg) - if err != nil { - return nil, err - } - // create manifest with config m, err := internal.NewManifest(cfg.endpoint, cfg.logger) if err != nil { diff --git a/samplers/aws/xray/remote_sampler_config.go b/samplers/aws/xray/remote_sampler_config.go index d0f38805b0e..ca3c5920e1d 100644 --- a/samplers/aws/xray/remote_sampler_config.go +++ b/samplers/aws/xray/remote_sampler_config.go @@ -89,6 +89,12 @@ func newConfig(opts ...Option) (*config, error) { option.apply(cfg) } + // validate config + err = validateConfig(cfg) + if err != nil { + return nil, err + } + return cfg, nil } diff --git a/samplers/aws/xray/remote_sampler_config_test.go b/samplers/aws/xray/remote_sampler_config_test.go index dcf2bbce619..358eab7a6db 100644 --- a/samplers/aws/xray/remote_sampler_config_test.go +++ b/samplers/aws/xray/remote_sampler_config_test.go @@ -84,10 +84,7 @@ func TestValidateHostNameWithSpecialCharacterEndpoint(t *testing.T) { endpoint, err := url.Parse("http://127.0.0.1@:2000") require.NoError(t, err) - cfg, err := newConfig(WithEndpoint(*endpoint)) - require.NoError(t, err) - - err = validateConfig(cfg) + _, err = newConfig(WithEndpoint(*endpoint)) assert.Error(t, err) } @@ -96,27 +93,18 @@ func TestValidateInvalidEndpoint(t *testing.T) { endpoint, err := url.Parse("https://") require.NoError(t, err) - cfg, err := newConfig(WithEndpoint(*endpoint)) - require.NoError(t, err) - - err = validateConfig(cfg) + _, err = newConfig(WithEndpoint(*endpoint)) assert.Error(t, err) } // assert negative sampling rules interval leads to an error. func TestValidateConfigNegativeDuration(t *testing.T) { - cfg, err := newConfig(WithSamplingRulesPollingInterval(-300 * time.Second)) - require.NoError(t, err) - - err = validateConfig(cfg) + _, err := newConfig(WithSamplingRulesPollingInterval(-300 * time.Second)) assert.Error(t, err) } // assert positive sampling rules interval. func TestValidateConfigPositiveDuration(t *testing.T) { - cfg, err := newConfig(WithSamplingRulesPollingInterval(300 * time.Second)) - require.NoError(t, err) - - err = validateConfig(cfg) + _, err := newConfig(WithSamplingRulesPollingInterval(300 * time.Second)) assert.NoError(t, err) } From 24bc28601a7b378507ebfbf96ac92ac407f4d6fe Mon Sep 17 00:00:00 2001 From: bhautikpip Date: Fri, 18 Mar 2022 10:54:23 -0700 Subject: [PATCH 24/38] removed util package and address minor comments --- samplers/aws/xray/go.mod | 2 -- .../aws/xray/internal/{util => }/clock.go | 2 +- samplers/aws/xray/internal/manifest.go | 5 ++-- samplers/aws/xray/internal/manifest_test.go | 29 +++++++++---------- samplers/aws/xray/internal/{util => }/rand.go | 2 +- samplers/aws/xray/internal/reservoir_test.go | 26 ++++++++--------- .../aws/xray/internal/{util => }/timer.go | 20 ++++++------- samplers/aws/xray/remote_sampler.go | 8 ++--- 8 files changed, 44 insertions(+), 50 deletions(-) rename samplers/aws/xray/internal/{util => }/clock.go (94%) rename samplers/aws/xray/internal/{util => }/rand.go (92%) rename samplers/aws/xray/internal/{util => }/timer.go (71%) diff --git a/samplers/aws/xray/go.mod b/samplers/aws/xray/go.mod index 3dde1140556..4ced10348be 100644 --- a/samplers/aws/xray/go.mod +++ b/samplers/aws/xray/go.mod @@ -2,8 +2,6 @@ module go.opentelemetry.io/contrib/samplers/aws/xray go 1.16 -replace go.opentelemetry.io/contrib/samplers/aws/xray/internal => ../internal - require ( github.com/go-logr/logr v1.2.2 github.com/go-logr/stdr v1.2.2 diff --git a/samplers/aws/xray/internal/util/clock.go b/samplers/aws/xray/internal/clock.go similarity index 94% rename from samplers/aws/xray/internal/util/clock.go rename to samplers/aws/xray/internal/clock.go index ecddcebb2d8..7c37ea7aaa6 100644 --- a/samplers/aws/xray/internal/util/clock.go +++ b/samplers/aws/xray/internal/clock.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package util // import "go.opentelemetry.io/contrib/samplers/aws/xray/internal/util" +package internal // import "go.opentelemetry.io/contrib/samplers/aws/xray/internal" import ( "sync/atomic" diff --git a/samplers/aws/xray/internal/manifest.go b/samplers/aws/xray/internal/manifest.go index 760511d6739..6e1f4d218f8 100644 --- a/samplers/aws/xray/internal/manifest.go +++ b/samplers/aws/xray/internal/manifest.go @@ -28,7 +28,6 @@ import ( "github.com/go-logr/logr" "github.com/jinzhu/copier" - "go.opentelemetry.io/contrib/samplers/aws/xray/internal/util" sdktrace "go.opentelemetry.io/otel/sdk/trace" ) @@ -43,7 +42,7 @@ type Manifest struct { xrayClient *xrayClient clientID *string logger logr.Logger - clock util.Clock + clock Clock mu sync.RWMutex } @@ -63,7 +62,7 @@ func NewManifest(addr url.URL, logger logr.Logger) (*Manifest, error) { return &Manifest{ xrayClient: client, - clock: &util.DefaultClock{}, + clock: &DefaultClock{}, logger: logger, SamplingTargetsPollingInterval: 10 * time.Second, clientID: clientID, diff --git a/samplers/aws/xray/internal/manifest_test.go b/samplers/aws/xray/internal/manifest_test.go index f0023d0f7d8..782e820499c 100644 --- a/samplers/aws/xray/internal/manifest_test.go +++ b/samplers/aws/xray/internal/manifest_test.go @@ -30,7 +30,6 @@ import ( "github.com/go-logr/stdr" "github.com/stretchr/testify/require" - "go.opentelemetry.io/contrib/samplers/aws/xray/internal/util" sdktrace "go.opentelemetry.io/otel/sdk/trace" "github.com/stretchr/testify/assert" @@ -55,7 +54,7 @@ func TestNewManifest(t *testing.T) { // assert that manifest is expired. func TestExpiredManifest(t *testing.T) { - clock := &util.MockClock{ + clock := &MockClock{ NowTime: 10000, } @@ -308,7 +307,7 @@ func TestRefreshManifestRules(t *testing.T) { m := &Manifest{ Rules: []Rule{}, xrayClient: client, - clock: &util.DefaultClock{}, + clock: &DefaultClock{}, } err = m.RefreshManifestRules(ctx) @@ -435,7 +434,7 @@ func TestRefreshManifestMissingServiceName(t *testing.T) { m := &Manifest{ Rules: []Rule{}, xrayClient: client, - clock: &util.DefaultClock{}, + clock: &DefaultClock{}, } err = m.RefreshManifestRules(ctx) @@ -490,7 +489,7 @@ func TestRefreshManifestMissingRuleName(t *testing.T) { m := &Manifest{ Rules: []Rule{}, xrayClient: client, - clock: &util.DefaultClock{}, + clock: &DefaultClock{}, logger: stdr.NewWithOptions(log.New(os.Stderr, "", log.LstdFlags|log.Lshortfile), stdr.Options{LogCaller: stdr.Error}), } @@ -548,7 +547,7 @@ func TestRefreshManifestIncorrectVersion(t *testing.T) { m := &Manifest{ Rules: []Rule{}, xrayClient: client, - clock: &util.DefaultClock{}, + clock: &DefaultClock{}, logger: stdr.NewWithOptions(log.New(os.Stderr, "", log.LstdFlags|log.Lshortfile), stdr.Options{LogCaller: stdr.Error}), } @@ -645,7 +644,7 @@ func TestRefreshManifestAddOneInvalidRule(t *testing.T) { m := &Manifest{ Rules: []Rule{}, xrayClient: client, - clock: &util.DefaultClock{}, + clock: &DefaultClock{}, logger: stdr.NewWithOptions(log.New(os.Stderr, "", log.LstdFlags|log.Lshortfile), stdr.Options{LogCaller: stdr.Error}), } @@ -660,7 +659,7 @@ func TestRefreshManifestAddOneInvalidRule(t *testing.T) { // assert that inactive rule so return early without doing getSamplingTargets call func TestRefreshManifestTarget_NoSnapShot(t *testing.T) { - clock := &util.MockClock{ + clock := &MockClock{ NowTime: 15000000, } @@ -723,7 +722,7 @@ func TestRefreshManifestTargets(t *testing.T) { ] }`) - clock := &util.MockClock{ + clock := &MockClock{ NowTime: 150, } @@ -822,7 +821,7 @@ func TestRefreshManifestTargets_PollIntervalUpdateTest(t *testing.T) { ] }`) - clock := &util.MockClock{ + clock := &MockClock{ NowTime: 150, } @@ -892,7 +891,7 @@ func TestRefreshManifestTargets_PollIntervalUpdateTest(t *testing.T) { // assert that a valid sampling target updates its rule. func TestUpdateTargets(t *testing.T) { - clock := &util.MockClock{ + clock := &MockClock{ NowTime: 1500000000, } @@ -963,7 +962,7 @@ func TestUpdateTargets(t *testing.T) { // assert that when last rule modification time is greater than manifest refresh time we need to update manifest // out of band (async). func TestUpdateTargetsRefreshFlagTest(t *testing.T) { - clock := &util.MockClock{ + clock := &MockClock{ NowTime: 1500000000, } @@ -1036,7 +1035,7 @@ func TestUpdateTargetsRefreshFlagTest(t *testing.T) { // unprocessed statistics error code is 5xx then updateTargets returns an error, if 4xx refresh flag set to true. func TestUpdateTargetsUnprocessedStatistics(t *testing.T) { - clock := &util.MockClock{ + clock := &MockClock{ NowTime: 1500000000, } @@ -1215,7 +1214,7 @@ func TestUpdateReservoirMissingRuleName(t *testing.T) { // assert that snapshots returns an array of valid sampling statistics. func TestSnapshots(t *testing.T) { - clock := &util.MockClock{ + clock := &MockClock{ NowTime: 1500000000, } @@ -1298,7 +1297,7 @@ func TestSnapshots(t *testing.T) { // assert that fresh and inactive rules are not included in a snapshot. func TestMixedSnapshots(t *testing.T) { - clock := &util.MockClock{ + clock := &MockClock{ NowTime: 1500000000, } diff --git a/samplers/aws/xray/internal/util/rand.go b/samplers/aws/xray/internal/rand.go similarity index 92% rename from samplers/aws/xray/internal/util/rand.go rename to samplers/aws/xray/internal/rand.go index 287006278b7..0b286f8ef3d 100644 --- a/samplers/aws/xray/internal/util/rand.go +++ b/samplers/aws/xray/internal/rand.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package util // import "go.opentelemetry.io/contrib/samplers/aws/xray/internal/util" +package internal // import "go.opentelemetry.io/contrib/samplers/aws/xray/internal" import ( crand "crypto/rand" diff --git a/samplers/aws/xray/internal/reservoir_test.go b/samplers/aws/xray/internal/reservoir_test.go index 9a602ac5325..33d97e43983 100644 --- a/samplers/aws/xray/internal/reservoir_test.go +++ b/samplers/aws/xray/internal/reservoir_test.go @@ -19,14 +19,12 @@ import ( "testing" "time" - "go.opentelemetry.io/contrib/samplers/aws/xray/internal/util" - "github.com/stretchr/testify/assert" ) // assert that reservoir quota is expired. func TestExpiredReservoir(t *testing.T) { - clock := &util.MockClock{ + clock := &MockClock{ NowTime: 1500000001, } @@ -43,7 +41,7 @@ func TestExpiredReservoir(t *testing.T) { // assert that reservoir quota is still expired since now time is equal to expiresAt time. func TestExpiredReservoirSameAsClockTime(t *testing.T) { - clock := &util.MockClock{ + clock := &MockClock{ NowTime: 1500000000, } @@ -61,7 +59,7 @@ func TestExpiredReservoirSameAsClockTime(t *testing.T) { // assert that borrow only 1 req/sec func TestBorrowEverySecond(t *testing.T) { - clock := &util.MockClock{ + clock := &MockClock{ NowTime: 1500000000, } @@ -77,7 +75,7 @@ func TestBorrowEverySecond(t *testing.T) { assert.False(t, s) // Increment clock by 1 - clock = &util.MockClock{ + clock = &MockClock{ NowTime: 1500000001, } @@ -88,7 +86,7 @@ func TestBorrowEverySecond(t *testing.T) { // assert that when reservoir is expired we consume from quota is 1 and then // when reservoir is not expired consume from assigned quota by X-Ray service func TestConsumeFromBorrow_ConsumeFromQuota(t *testing.T) { - clock := &util.MockClock{ + clock := &MockClock{ NowTime: 1500000000, } @@ -105,7 +103,7 @@ func TestConsumeFromBorrow_ConsumeFromQuota(t *testing.T) { assert.False(t, s) // Increment clock by 1 - clock = &util.MockClock{ + clock = &MockClock{ NowTime: 1500000001, } @@ -113,7 +111,7 @@ func TestConsumeFromBorrow_ConsumeFromQuota(t *testing.T) { assert.True(t, s) // Increment clock by 1 - clock = &util.MockClock{ + clock = &MockClock{ NowTime: 1500000002, } @@ -130,7 +128,7 @@ func TestConsumeFromBorrow_ConsumeFromQuota(t *testing.T) { // assert that we can still borrowing from reservoir is possible since assigned quota is available to consume // and it will increase used count. func TestConsumeFromReservoir(t *testing.T) { - clock := &util.MockClock{ + clock := &MockClock{ NowTime: 1500000000, } @@ -172,7 +170,7 @@ func TestConsumeFromReservoir(t *testing.T) { } func TestResetQuotaUsageRotation(t *testing.T) { - clock := &util.MockClock{ + clock := &MockClock{ NowTime: 1500000000, } @@ -193,7 +191,7 @@ func TestResetQuotaUsageRotation(t *testing.T) { assert.Equal(t, false, taken) // increment epoch to reset unused quota - clock = &util.MockClock{ + clock = &MockClock{ NowTime: 1500000001, } @@ -205,7 +203,7 @@ func TestResetQuotaUsageRotation(t *testing.T) { // assert that when quotaBalance exceeds totalQuotaBalanceCapacity then totalQuotaBalanceCapacity // gets assigned to quotaBalance func TestQuotaBalanceNonBorrow_ExceedsCapacity(t *testing.T) { - clock := &util.MockClock{ + clock := &MockClock{ NowTime: 1500000001, } @@ -224,7 +222,7 @@ func TestQuotaBalanceNonBorrow_ExceedsCapacity(t *testing.T) { // assert quotaBalance and capacity of borrowing case func TestQuotaBalanceBorrow(t *testing.T) { - clock := &util.MockClock{ + clock := &MockClock{ NowTime: 1500000001, } diff --git a/samplers/aws/xray/internal/util/timer.go b/samplers/aws/xray/internal/timer.go similarity index 71% rename from samplers/aws/xray/internal/util/timer.go rename to samplers/aws/xray/internal/timer.go index c6fc76ef667..1fe310e2e69 100644 --- a/samplers/aws/xray/internal/util/timer.go +++ b/samplers/aws/xray/internal/timer.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package util // import "go.opentelemetry.io/contrib/samplers/aws/xray/internal/util" +package internal // import "go.opentelemetry.io/contrib/samplers/aws/xray/internal" import ( "time" @@ -21,19 +21,19 @@ import ( // Ticker is the same as time.Ticker except that it has jitters. // A Ticker must be created with NewTicker. type Ticker struct { - t *time.Ticker - d time.Duration - jitter time.Duration + Tick *time.Ticker + duration time.Duration + jitter time.Duration } // NewTicker creates a new Ticker that will send the current time on its channel. -func NewTicker(d, jitter time.Duration) *Ticker { - t := time.NewTicker(d - time.Duration(newGlobalRand().Int63n(int64(jitter)))) +func NewTicker(duration, jitter time.Duration) *Ticker { + t := time.NewTicker(duration - time.Duration(newGlobalRand().Int63n(int64(jitter)))) jitteredTicker := Ticker{ - t: t, - d: d, - jitter: jitter, + Tick: t, + duration: duration, + jitter: jitter, } return &jitteredTicker @@ -41,5 +41,5 @@ func NewTicker(d, jitter time.Duration) *Ticker { // C is channel. func (j *Ticker) C() <-chan time.Time { - return j.t.C + return j.Tick.C } diff --git a/samplers/aws/xray/remote_sampler.go b/samplers/aws/xray/remote_sampler.go index be7e7aff623..3dc9824c538 100644 --- a/samplers/aws/xray/remote_sampler.go +++ b/samplers/aws/xray/remote_sampler.go @@ -18,8 +18,6 @@ import ( "context" "time" - "go.opentelemetry.io/contrib/samplers/aws/xray/internal/util" - "go.opentelemetry.io/contrib/samplers/aws/xray/internal" sdktrace "go.opentelemetry.io/otel/sdk/trace" @@ -129,10 +127,12 @@ func (rs *remoteSampler) start(ctx context.Context) { // to refresh manifest and targets. func (rs *remoteSampler) startPoller(ctx context.Context) { // jitter = 5s, default 300 seconds - rulesTicker := util.NewTicker(rs.samplingRulesPollingInterval, 5*time.Second) + rulesTicker := internal.NewTicker(rs.samplingRulesPollingInterval, 5*time.Second) + defer rulesTicker.Tick.Stop() // jitter = 100ms, default 10 seconds - targetTicker := util.NewTicker(rs.manifest.SamplingTargetsPollingInterval, 100*time.Millisecond) + targetTicker := internal.NewTicker(rs.manifest.SamplingTargetsPollingInterval, 100*time.Millisecond) + defer targetTicker.Tick.Stop() // fetch sampling rules to kick start the remote sampling rs.refreshManifest(ctx) From 78874a9383267292e46a2cae2c58f5ba8b5d31fa Mon Sep 17 00:00:00 2001 From: bhautikpip Date: Fri, 18 Mar 2022 12:48:47 -0700 Subject: [PATCH 25/38] moved timer and rand to xray package, minor changes --- samplers/aws/xray/internal/clock.go | 27 ++---- samplers/aws/xray/internal/manifest.go | 14 +-- samplers/aws/xray/internal/manifest_test.go | 52 +++++------ samplers/aws/xray/internal/reservoir_test.go | 98 ++++++++++---------- samplers/aws/xray/{internal => }/rand.go | 2 +- samplers/aws/xray/remote_sampler.go | 12 +-- samplers/aws/xray/{internal => }/timer.go | 22 ++--- 7 files changed, 109 insertions(+), 118 deletions(-) rename samplers/aws/xray/{internal => }/rand.go (92%) rename samplers/aws/xray/{internal => }/timer.go (67%) diff --git a/samplers/aws/xray/internal/clock.go b/samplers/aws/xray/internal/clock.go index 7c37ea7aaa6..5fb98674570 100644 --- a/samplers/aws/xray/internal/clock.go +++ b/samplers/aws/xray/internal/clock.go @@ -15,38 +15,29 @@ package internal // import "go.opentelemetry.io/contrib/samplers/aws/xray/internal" import ( - "sync/atomic" "time" ) // Clock provides an interface to implement method for getting current time. -type Clock interface { - Now() time.Time +type clock interface { + now() time.Time } // DefaultClock is an implementation of Clock interface. -type DefaultClock struct{} +type defaultClock struct{} // Now returns current time. -func (t *DefaultClock) Now() time.Time { +func (t *defaultClock) now() time.Time { return time.Now() } // MockClock is a struct to record current time. -type MockClock struct { - NowTime int64 - NowNanos int64 +type mockClock struct { + nowTime int64 + nowNanos int64 } // Now function returns NowTime value. -func (c *MockClock) Now() time.Time { - return time.Unix(c.NowTime, c.NowNanos) -} - -// Increment is a method to increase current time. -func (c *MockClock) Increment(s int64, ns int64) time.Time { - sec := atomic.AddInt64(&c.NowTime, s) - nSec := atomic.AddInt64(&c.NowNanos, ns) - - return time.Unix(sec, nSec) +func (c *mockClock) now() time.Time { + return time.Unix(c.nowTime, c.nowNanos) } diff --git a/samplers/aws/xray/internal/manifest.go b/samplers/aws/xray/internal/manifest.go index 6e1f4d218f8..cfde8dd7c48 100644 --- a/samplers/aws/xray/internal/manifest.go +++ b/samplers/aws/xray/internal/manifest.go @@ -42,7 +42,7 @@ type Manifest struct { xrayClient *xrayClient clientID *string logger logr.Logger - clock Clock + clock clock mu sync.RWMutex } @@ -62,7 +62,7 @@ func NewManifest(addr url.URL, logger logr.Logger) (*Manifest, error) { return &Manifest{ xrayClient: client, - clock: &DefaultClock{}, + clock: &defaultClock{}, logger: logger, SamplingTargetsPollingInterval: 10 * time.Second, clientID: clientID, @@ -76,7 +76,7 @@ func (m *Manifest) Expired() bool { defer m.mu.RUnlock() manifestLiveTime := m.refreshedAt.Add(time.Second * manifestTTL) - return m.clock.Now().After(manifestLiveTime) + return m.clock.now().After(manifestLiveTime) } // MatchAgainstManifestRules returns a Rule and boolean flag set as true @@ -194,7 +194,7 @@ func (m *Manifest) updateRules(rules *getSamplingRulesOutput) { m.mu.Lock() m.Rules = tempManifest.Rules - m.refreshedAt = m.clock.Now() + m.refreshedAt = m.clock.now() m.mu.Unlock() } @@ -269,7 +269,7 @@ func (m *Manifest) updateReservoir(t *samplingTargetDocument) (err error) { for index := range m.Rules { if m.Rules[index].ruleProperties.RuleName == *t.RuleName { - m.Rules[index].reservoir.refreshedAt = m.clock.Now() + m.Rules[index].reservoir.refreshedAt = m.clock.now() // Update non-optional attributes from response m.Rules[index].ruleProperties.FixedRate = *t.FixedRate @@ -297,8 +297,8 @@ func (m *Manifest) snapshots() ([]*samplingStatisticsDocument, error) { // Generate sampling statistics for user-defined rules for index := range m.Rules { - if m.Rules[index].stale(m.clock.Now()) { - s := m.Rules[index].snapshot(m.clock.Now()) + if m.Rules[index].stale(m.clock.now()) { + s := m.Rules[index].snapshot(m.clock.now()) s.ClientID = m.clientID statistics = append(statistics, s) diff --git a/samplers/aws/xray/internal/manifest_test.go b/samplers/aws/xray/internal/manifest_test.go index 782e820499c..8319e499b2a 100644 --- a/samplers/aws/xray/internal/manifest_test.go +++ b/samplers/aws/xray/internal/manifest_test.go @@ -54,8 +54,8 @@ func TestNewManifest(t *testing.T) { // assert that manifest is expired. func TestExpiredManifest(t *testing.T) { - clock := &MockClock{ - NowTime: 10000, + clock := &mockClock{ + nowTime: 10000, } refreshedAt := time.Unix(3700, 0) @@ -307,7 +307,7 @@ func TestRefreshManifestRules(t *testing.T) { m := &Manifest{ Rules: []Rule{}, xrayClient: client, - clock: &DefaultClock{}, + clock: &defaultClock{}, } err = m.RefreshManifestRules(ctx) @@ -434,7 +434,7 @@ func TestRefreshManifestMissingServiceName(t *testing.T) { m := &Manifest{ Rules: []Rule{}, xrayClient: client, - clock: &DefaultClock{}, + clock: &defaultClock{}, } err = m.RefreshManifestRules(ctx) @@ -489,7 +489,7 @@ func TestRefreshManifestMissingRuleName(t *testing.T) { m := &Manifest{ Rules: []Rule{}, xrayClient: client, - clock: &DefaultClock{}, + clock: &defaultClock{}, logger: stdr.NewWithOptions(log.New(os.Stderr, "", log.LstdFlags|log.Lshortfile), stdr.Options{LogCaller: stdr.Error}), } @@ -547,7 +547,7 @@ func TestRefreshManifestIncorrectVersion(t *testing.T) { m := &Manifest{ Rules: []Rule{}, xrayClient: client, - clock: &DefaultClock{}, + clock: &defaultClock{}, logger: stdr.NewWithOptions(log.New(os.Stderr, "", log.LstdFlags|log.Lshortfile), stdr.Options{LogCaller: stdr.Error}), } @@ -644,7 +644,7 @@ func TestRefreshManifestAddOneInvalidRule(t *testing.T) { m := &Manifest{ Rules: []Rule{}, xrayClient: client, - clock: &DefaultClock{}, + clock: &defaultClock{}, logger: stdr.NewWithOptions(log.New(os.Stderr, "", log.LstdFlags|log.Lshortfile), stdr.Options{LogCaller: stdr.Error}), } @@ -659,8 +659,8 @@ func TestRefreshManifestAddOneInvalidRule(t *testing.T) { // assert that inactive rule so return early without doing getSamplingTargets call func TestRefreshManifestTarget_NoSnapShot(t *testing.T) { - clock := &MockClock{ - NowTime: 15000000, + clock := &mockClock{ + nowTime: 15000000, } r1 := Rule{ @@ -722,8 +722,8 @@ func TestRefreshManifestTargets(t *testing.T) { ] }`) - clock := &MockClock{ - NowTime: 150, + clock := &mockClock{ + nowTime: 150, } r1 := Rule{ @@ -821,8 +821,8 @@ func TestRefreshManifestTargets_PollIntervalUpdateTest(t *testing.T) { ] }`) - clock := &MockClock{ - NowTime: 150, + clock := &mockClock{ + nowTime: 150, } r1 := Rule{ @@ -891,8 +891,8 @@ func TestRefreshManifestTargets_PollIntervalUpdateTest(t *testing.T) { // assert that a valid sampling target updates its rule. func TestUpdateTargets(t *testing.T) { - clock := &MockClock{ - NowTime: 1500000000, + clock := &mockClock{ + nowTime: 1500000000, } // sampling target received from centralized sampling backend @@ -962,8 +962,8 @@ func TestUpdateTargets(t *testing.T) { // assert that when last rule modification time is greater than manifest refresh time we need to update manifest // out of band (async). func TestUpdateTargetsRefreshFlagTest(t *testing.T) { - clock := &MockClock{ - NowTime: 1500000000, + clock := &mockClock{ + nowTime: 1500000000, } // sampling target received from centralized sampling backend @@ -1004,7 +1004,7 @@ func TestUpdateTargetsRefreshFlagTest(t *testing.T) { m := &Manifest{ Rules: rules, - refreshedAt: clock.Now(), + refreshedAt: clock.now(), clock: clock, } @@ -1035,8 +1035,8 @@ func TestUpdateTargetsRefreshFlagTest(t *testing.T) { // unprocessed statistics error code is 5xx then updateTargets returns an error, if 4xx refresh flag set to true. func TestUpdateTargetsUnprocessedStatistics(t *testing.T) { - clock := &MockClock{ - NowTime: 1500000000, + clock := &mockClock{ + nowTime: 1500000000, } // sampling target received from centralized sampling backend @@ -1214,11 +1214,11 @@ func TestUpdateReservoirMissingRuleName(t *testing.T) { // assert that snapshots returns an array of valid sampling statistics. func TestSnapshots(t *testing.T) { - clock := &MockClock{ - NowTime: 1500000000, + clock := &mockClock{ + nowTime: 1500000000, } - time1 := clock.Now().Unix() + time1 := clock.now().Unix() name1 := "r1" requests1 := int64(1000) @@ -1297,12 +1297,12 @@ func TestSnapshots(t *testing.T) { // assert that fresh and inactive rules are not included in a snapshot. func TestMixedSnapshots(t *testing.T) { - clock := &MockClock{ - NowTime: 1500000000, + clock := &mockClock{ + nowTime: 1500000000, } id := "c1" - time1 := clock.Now().Unix() + time1 := clock.now().Unix() // stale and active rule name1 := "r1" diff --git a/samplers/aws/xray/internal/reservoir_test.go b/samplers/aws/xray/internal/reservoir_test.go index 33d97e43983..ab70c25dfaf 100644 --- a/samplers/aws/xray/internal/reservoir_test.go +++ b/samplers/aws/xray/internal/reservoir_test.go @@ -24,8 +24,8 @@ import ( // assert that reservoir quota is expired. func TestExpiredReservoir(t *testing.T) { - clock := &MockClock{ - NowTime: 1500000001, + clock := &mockClock{ + nowTime: 1500000001, } expiresAt := time.Unix(1500000000, 0) @@ -34,15 +34,15 @@ func TestExpiredReservoir(t *testing.T) { mu: &sync.RWMutex{}, } - expired := r.expired(clock.Now()) + expired := r.expired(clock.now()) assert.True(t, expired) } // assert that reservoir quota is still expired since now time is equal to expiresAt time. func TestExpiredReservoirSameAsClockTime(t *testing.T) { - clock := &MockClock{ - NowTime: 1500000000, + clock := &mockClock{ + nowTime: 1500000000, } expiresAt := time.Unix(1500000000, 0) @@ -52,15 +52,15 @@ func TestExpiredReservoirSameAsClockTime(t *testing.T) { mu: &sync.RWMutex{}, } - expired := r.expired(clock.Now()) + expired := r.expired(clock.now()) assert.False(t, expired) } // assert that borrow only 1 req/sec func TestBorrowEverySecond(t *testing.T) { - clock := &MockClock{ - NowTime: 1500000000, + clock := &mockClock{ + nowTime: 1500000000, } r := &reservoir{ @@ -68,26 +68,26 @@ func TestBorrowEverySecond(t *testing.T) { mu: &sync.RWMutex{}, } - s := r.take(clock.Now(), true, 1.0) + s := r.take(clock.now(), true, 1.0) assert.True(t, s) - s = r.take(clock.Now(), true, 1.0) + s = r.take(clock.now(), true, 1.0) assert.False(t, s) // Increment clock by 1 - clock = &MockClock{ - NowTime: 1500000001, + clock = &mockClock{ + nowTime: 1500000001, } - s = r.take(clock.Now(), true, 1.0) + s = r.take(clock.now(), true, 1.0) assert.True(t, s) } // assert that when reservoir is expired we consume from quota is 1 and then // when reservoir is not expired consume from assigned quota by X-Ray service func TestConsumeFromBorrow_ConsumeFromQuota(t *testing.T) { - clock := &MockClock{ - NowTime: 1500000000, + clock := &mockClock{ + nowTime: 1500000000, } r := &reservoir{ @@ -96,40 +96,40 @@ func TestConsumeFromBorrow_ConsumeFromQuota(t *testing.T) { mu: &sync.RWMutex{}, } - s := r.take(clock.Now(), true, 1.0) + s := r.take(clock.now(), true, 1.0) assert.True(t, s) - s = r.take(clock.Now(), true, 1.0) + s = r.take(clock.now(), true, 1.0) assert.False(t, s) // Increment clock by 1 - clock = &MockClock{ - NowTime: 1500000001, + clock = &mockClock{ + nowTime: 1500000001, } - s = r.take(clock.Now(), true, 1.0) + s = r.take(clock.now(), true, 1.0) assert.True(t, s) // Increment clock by 1 - clock = &MockClock{ - NowTime: 1500000002, + clock = &mockClock{ + nowTime: 1500000002, } - s = r.take(clock.Now(), false, 1.0) + s = r.take(clock.now(), false, 1.0) assert.True(t, s) - s = r.take(clock.Now(), false, 1.0) + s = r.take(clock.now(), false, 1.0) assert.True(t, s) - s = r.take(clock.Now(), false, 1.0) + s = r.take(clock.now(), false, 1.0) assert.False(t, s) } // assert that we can still borrowing from reservoir is possible since assigned quota is available to consume // and it will increase used count. func TestConsumeFromReservoir(t *testing.T) { - clock := &MockClock{ - NowTime: 1500000000, + clock := &mockClock{ + nowTime: 1500000000, } r := &reservoir{ @@ -141,37 +141,37 @@ func TestConsumeFromReservoir(t *testing.T) { // reservoir updates the quotaBalance for new second and allows to consume // quota balance is 0 because we are consuming from reservoir for the first time assert.Equal(t, r.quotaBalance, 0.0) - assert.True(t, r.take(clock.Now(), false, 1.0)) + assert.True(t, r.take(clock.now(), false, 1.0)) assert.Equal(t, r.quotaBalance, 1.0) - assert.True(t, r.take(clock.Now(), false, 1.0)) + assert.True(t, r.take(clock.now(), false, 1.0)) assert.Equal(t, r.quotaBalance, 0.0) // once assigned quota is consumed reservoir does not allow to consume in that second - assert.False(t, r.take(clock.Now(), false, 1.0)) + assert.False(t, r.take(clock.now(), false, 1.0)) // increase the clock by 1 - clock.NowTime = 1500000001 + clock.nowTime = 1500000001 // reservoir updates the quotaBalance for new second and allows to consume assert.Equal(t, r.quotaBalance, 0.0) - assert.True(t, r.take(clock.Now(), false, 1.0)) + assert.True(t, r.take(clock.now(), false, 1.0)) assert.Equal(t, r.quotaBalance, 1.0) - assert.True(t, r.take(clock.Now(), false, 1.0)) + assert.True(t, r.take(clock.now(), false, 1.0)) assert.Equal(t, r.quotaBalance, 0.0) // once assigned quota is consumed reservoir does not allow to consume in that second - assert.False(t, r.take(clock.Now(), false, 1.0)) + assert.False(t, r.take(clock.now(), false, 1.0)) // increase the clock by 5 - clock.NowTime = 1500000005 + clock.nowTime = 1500000005 // elapsedTime is 4 seconds so quota balance should be elapsedTime * quota = 8 and below take would consume 1 so // ultimately 7 - assert.True(t, r.take(clock.Now(), false, 1.0)) + assert.True(t, r.take(clock.now(), false, 1.0)) assert.Equal(t, r.quotaBalance, 7.0) } func TestResetQuotaUsageRotation(t *testing.T) { - clock := &MockClock{ - NowTime: 1500000000, + clock := &mockClock{ + nowTime: 1500000000, } r := &reservoir{ @@ -182,29 +182,29 @@ func TestResetQuotaUsageRotation(t *testing.T) { // consume quota for second for i := 0; i < 5; i++ { - taken := r.take(clock.Now(), false, 1.0) + taken := r.take(clock.now(), false, 1.0) assert.Equal(t, true, taken) } // take() should be false since no unused quota left - taken := r.take(clock.Now(), false, 1.0) + taken := r.take(clock.now(), false, 1.0) assert.Equal(t, false, taken) // increment epoch to reset unused quota - clock = &MockClock{ - NowTime: 1500000001, + clock = &mockClock{ + nowTime: 1500000001, } // take() should be true since ununsed quota is available - taken = r.take(clock.Now(), false, 1.0) + taken = r.take(clock.now(), false, 1.0) assert.Equal(t, true, taken) } // assert that when quotaBalance exceeds totalQuotaBalanceCapacity then totalQuotaBalanceCapacity // gets assigned to quotaBalance func TestQuotaBalanceNonBorrow_ExceedsCapacity(t *testing.T) { - clock := &MockClock{ - NowTime: 1500000001, + clock := &mockClock{ + nowTime: 1500000001, } r := &reservoir{ @@ -214,7 +214,7 @@ func TestQuotaBalanceNonBorrow_ExceedsCapacity(t *testing.T) { lastTick: time.Unix(1500000000, 0), } - r.refreshQuotaBalance(clock.Now(), false) + r.refreshQuotaBalance(clock.now(), false) // assert that if quotaBalance exceeds capacity then total capacity would be new quotaBalance assert.Equal(t, r.quotaBalance, 1*r.capacity) @@ -222,8 +222,8 @@ func TestQuotaBalanceNonBorrow_ExceedsCapacity(t *testing.T) { // assert quotaBalance and capacity of borrowing case func TestQuotaBalanceBorrow(t *testing.T) { - clock := &MockClock{ - NowTime: 1500000001, + clock := &mockClock{ + nowTime: 1500000001, } r := &reservoir{ @@ -233,7 +233,7 @@ func TestQuotaBalanceBorrow(t *testing.T) { lastTick: time.Unix(1500000000, 0), } - r.refreshQuotaBalance(clock.Now(), true) + r.refreshQuotaBalance(clock.now(), true) // assert that if quotaBalance exceeds capacity then total capacity would be new quotaBalance assert.Equal(t, r.quotaBalance, 1.0) diff --git a/samplers/aws/xray/internal/rand.go b/samplers/aws/xray/rand.go similarity index 92% rename from samplers/aws/xray/internal/rand.go rename to samplers/aws/xray/rand.go index 0b286f8ef3d..2ba8df59193 100644 --- a/samplers/aws/xray/internal/rand.go +++ b/samplers/aws/xray/rand.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package internal // import "go.opentelemetry.io/contrib/samplers/aws/xray/internal" +package xray // import "go.opentelemetry.io/contrib/samplers/aws/xray" import ( crand "crypto/rand" diff --git a/samplers/aws/xray/remote_sampler.go b/samplers/aws/xray/remote_sampler.go index 3dc9824c538..097f743a930 100644 --- a/samplers/aws/xray/remote_sampler.go +++ b/samplers/aws/xray/remote_sampler.go @@ -127,19 +127,19 @@ func (rs *remoteSampler) start(ctx context.Context) { // to refresh manifest and targets. func (rs *remoteSampler) startPoller(ctx context.Context) { // jitter = 5s, default 300 seconds - rulesTicker := internal.NewTicker(rs.samplingRulesPollingInterval, 5*time.Second) - defer rulesTicker.Tick.Stop() + rulesTicker := newTicker(rs.samplingRulesPollingInterval, 5*time.Second) + defer rulesTicker.tick.Stop() // jitter = 100ms, default 10 seconds - targetTicker := internal.NewTicker(rs.manifest.SamplingTargetsPollingInterval, 100*time.Millisecond) - defer targetTicker.Tick.Stop() + targetTicker := newTicker(rs.manifest.SamplingTargetsPollingInterval, 100*time.Millisecond) + defer targetTicker.tick.Stop() // fetch sampling rules to kick start the remote sampling rs.refreshManifest(ctx) for { select { - case _, more := <-rulesTicker.C(): + case _, more := <-rulesTicker.c(): if !more { return } @@ -147,7 +147,7 @@ func (rs *remoteSampler) startPoller(ctx context.Context) { // fetch sampling rules and updates manifest rs.refreshManifest(ctx) continue - case _, more := <-targetTicker.C(): + case _, more := <-targetTicker.c(): if !more { return } diff --git a/samplers/aws/xray/internal/timer.go b/samplers/aws/xray/timer.go similarity index 67% rename from samplers/aws/xray/internal/timer.go rename to samplers/aws/xray/timer.go index 1fe310e2e69..2fdb3e9ac08 100644 --- a/samplers/aws/xray/internal/timer.go +++ b/samplers/aws/xray/timer.go @@ -12,26 +12,26 @@ // See the License for the specific language governing permissions and // limitations under the License. -package internal // import "go.opentelemetry.io/contrib/samplers/aws/xray/internal" +package xray // import "go.opentelemetry.io/contrib/samplers/aws/xray" import ( "time" ) -// Ticker is the same as time.Ticker except that it has jitters. +// ticker is the same as time.Ticker except that it has jitters. // A Ticker must be created with NewTicker. -type Ticker struct { - Tick *time.Ticker +type ticker struct { + tick *time.Ticker duration time.Duration jitter time.Duration } -// NewTicker creates a new Ticker that will send the current time on its channel. -func NewTicker(duration, jitter time.Duration) *Ticker { +// newTicker creates a new Ticker that will send the current time on its channel. +func newTicker(duration, jitter time.Duration) *ticker { t := time.NewTicker(duration - time.Duration(newGlobalRand().Int63n(int64(jitter)))) - jitteredTicker := Ticker{ - Tick: t, + jitteredTicker := ticker{ + tick: t, duration: duration, jitter: jitter, } @@ -39,7 +39,7 @@ func NewTicker(duration, jitter time.Duration) *Ticker { return &jitteredTicker } -// C is channel. -func (j *Ticker) C() <-chan time.Time { - return j.Tick.C +// c is channel. +func (j *ticker) c() <-chan time.Time { + return j.tick.C } From 765f300276d15104eaa911d5738f6ce830ffd21e Mon Sep 17 00:00:00 2001 From: bhautikpip Date: Fri, 18 Mar 2022 16:27:01 -0700 Subject: [PATCH 26/38] added more tests --- samplers/aws/xray/internal/manifest_test.go | 10 +++++++++ samplers/aws/xray/internal/reservoir_test.go | 19 +++++++++++++++++ samplers/aws/xray/internal/rule_test.go | 22 ++++++++++++++++++++ 3 files changed, 51 insertions(+) diff --git a/samplers/aws/xray/internal/manifest_test.go b/samplers/aws/xray/internal/manifest_test.go index 8319e499b2a..0b9fc905e53 100644 --- a/samplers/aws/xray/internal/manifest_test.go +++ b/samplers/aws/xray/internal/manifest_test.go @@ -1515,6 +1515,16 @@ func TestMinPollIntervalNegativeCase(t *testing.T) { assert.Equal(t, -5*time.Second, minPoll) } +// asserts that manifest with empty rules return 0 +func TestMinPollIntervalNoRules(t *testing.T) { + var rules []Rule + m := &Manifest{Rules: rules} + + minPoll := m.minimumPollingInterval() + + assert.Equal(t, 0*time.Second, minPoll) +} + // assert that able to successfully generate the client ID. func TestGenerateClientID(t *testing.T) { clientID, err := generateClientID() diff --git a/samplers/aws/xray/internal/reservoir_test.go b/samplers/aws/xray/internal/reservoir_test.go index ab70c25dfaf..96f9f0c0412 100644 --- a/samplers/aws/xray/internal/reservoir_test.go +++ b/samplers/aws/xray/internal/reservoir_test.go @@ -238,3 +238,22 @@ func TestQuotaBalanceBorrow(t *testing.T) { // assert that if quotaBalance exceeds capacity then total capacity would be new quotaBalance assert.Equal(t, r.quotaBalance, 1.0) } + +// assert that when borrow is true and elapsedTime is greater than 1, then we only increase the quota balance by 1 +func TestQuotaBalanceIncreaseByOne_BorrowCase(t *testing.T) { + clock := &mockClock{ + nowTime: 1500000002, + } + + r := &reservoir{ + quota: 6, + capacity: 5, + mu: &sync.RWMutex{}, + quotaBalance: 0.25, + lastTick: time.Unix(1500000000, 0), + } + + r.refreshQuotaBalance(clock.now(), true) + + assert.Equal(t, r.quotaBalance, 1.25) +} diff --git a/samplers/aws/xray/internal/rule_test.go b/samplers/aws/xray/internal/rule_test.go index fdaede4cacf..83afe0df324 100644 --- a/samplers/aws/xray/internal/rule_test.go +++ b/samplers/aws/xray/internal/rule_test.go @@ -351,6 +351,28 @@ func TestAppliesToNoMatching(t *testing.T) { assert.False(t, match) } +// assert that when attribute has http.url is empty, uses http.target wildcard matching. +func TestAppliesToHTTPTargetMatching(t *testing.T) { + commonLabels := []attribute.KeyValue{ + attribute.String("http.target", "target"), + } + + r1 := Rule{ + ruleProperties: ruleProperties{ + RuleName: "r1", + ServiceName: "test-service", + ServiceType: "ECS", + Host: "*", + HTTPMethod: "*", + URLPath: "*", + }, + } + + match, err := r1.appliesTo(trace.SamplingParameters{Attributes: commonLabels}, "test-service", "ECS") + require.NoError(t, err) + assert.True(t, match) +} + // assert that if rules has attribute and span has those attribute with same value then matching will happen. func TestAttributeMatching(t *testing.T) { commonLabels := []attribute.KeyValue{ From cc384b84b0bca4919337eb50b8d5ddcccc7f019d Mon Sep 17 00:00:00 2001 From: bhautikpip Date: Mon, 21 Mar 2022 13:16:33 -0700 Subject: [PATCH 27/38] improved testing from codecov/patch annotations --- samplers/aws/xray/internal/manifest_test.go | 61 ++++++++++++++++++++- 1 file changed, 60 insertions(+), 1 deletion(-) diff --git a/samplers/aws/xray/internal/manifest_test.go b/samplers/aws/xray/internal/manifest_test.go index 0b9fc905e53..3af87506f14 100644 --- a/samplers/aws/xray/internal/manifest_test.go +++ b/samplers/aws/xray/internal/manifest_test.go @@ -225,6 +225,42 @@ func TestMatchAgainstManifestRules_AttributeWildCardMatch(t *testing.T) { assert.Equal(t, *exp, r1) } +// assert that when no known rule is match then returned rule is nil, +// matched flag is false +func TestMatchAgainstManifestRules_NoMatch(t *testing.T) { + r1 := Rule{ + ruleProperties: ruleProperties{ + RuleName: "r1", + Priority: 10000, + Host: "*", + HTTPMethod: "*", + URLPath: "*", + ReservoirSize: 60, + FixedRate: 0.5, + Version: 1, + ServiceName: "test-no-match", + ResourceARN: "*", + ServiceType: "local", + }, + reservoir: reservoir{ + expiresAt: time.Unix(14050, 0), + }, + } + + rules := []Rule{r1} + + m := &Manifest{ + Rules: rules, + } + + rule, isMatch, err := m.MatchAgainstManifestRules(sdktrace.SamplingParameters{}, "test", "local") + + // assert that when no known rule is match then returned rule is nil + require.NoError(t, err) + assert.False(t, isMatch) + assert.Nil(t, rule) +} + func TestRefreshManifestRules(t *testing.T) { ctx := context.Background() @@ -1052,7 +1088,7 @@ func TestUpdateTargetsUnprocessedStatistics(t *testing.T) { RuleName: &name, } - // case for 4xx + // case for 5xx errorCode500 := "500" unprocessedStats5xx := unprocessedStatistic{ ErrorCode: &errorCode500, @@ -1094,6 +1130,29 @@ func TestUpdateTargetsUnprocessedStatistics(t *testing.T) { // assert refresh is true assert.True(t, refresh) + + // case when rule error code is unknown do not set any flag + unprocessedStats := unprocessedStatistic{ + ErrorCode: nil, + RuleName: nil, + } + + targets := &getSamplingTargetsOutput{ + SamplingTargetDocuments: []*samplingTargetDocument{&st}, + UnprocessedStatistics: []*unprocessedStatistic{&unprocessedStats}, + } + + m = &Manifest{ + clock: clock, + logger: stdr.NewWithOptions(log.New(os.Stderr, "", log.LstdFlags|log.Lshortfile), stdr.Options{LogCaller: stdr.Error}), + } + + refresh, err = m.updateTargets(targets) + require.NoError(t, err) + + // assert refresh is false + assert.False(t, refresh) + } // assert that a missing sampling rule in manifest does not update it's reservoir values. From cc7901cf22f7297718957fc2a72b7aae85557a45 Mon Sep 17 00:00:00 2001 From: bhautikpip Date: Wed, 23 Mar 2022 16:01:17 -0700 Subject: [PATCH 28/38] addressed review comment --- samplers/aws/xray/fallback_sampler.go | 15 +-- samplers/aws/xray/go.mod | 2 +- samplers/aws/xray/go.sum | 3 +- samplers/aws/xray/internal/client.go | 38 +++---- samplers/aws/xray/internal/client_test.go | 43 ++++---- samplers/aws/xray/internal/clock.go | 10 +- samplers/aws/xray/internal/manifest.go | 85 ++++++++------- samplers/aws/xray/internal/manifest_test.go | 104 ++++++++----------- samplers/aws/xray/internal/match_test.go | 3 +- samplers/aws/xray/internal/reservoir.go | 38 +++---- samplers/aws/xray/internal/reservoir_test.go | 28 +++-- samplers/aws/xray/internal/rule.go | 62 +++++------ samplers/aws/xray/internal/rule_test.go | 12 +-- samplers/aws/xray/rand.go | 4 +- samplers/aws/xray/remote_sampler.go | 43 ++++---- samplers/aws/xray/remote_sampler_config.go | 4 +- samplers/aws/xray/timer.go | 6 +- 17 files changed, 245 insertions(+), 255 deletions(-) diff --git a/samplers/aws/xray/fallback_sampler.go b/samplers/aws/xray/fallback_sampler.go index c10ac27ee85..1a7a5ea1bc9 100644 --- a/samplers/aws/xray/fallback_sampler.go +++ b/samplers/aws/xray/fallback_sampler.go @@ -22,6 +22,7 @@ import ( "go.opentelemetry.io/otel/trace" ) +// FallbackSampler does the sampling at a rate of 1 req/sec and 5% of additional requests. type FallbackSampler struct { lastTick time.Time quotaBalance float64 @@ -54,7 +55,7 @@ func (fs *FallbackSampler) ShouldSample(parameters sdktrace.SamplingParameters) return fs.defaultSampler.ShouldSample(parameters) } -// Description returns description of the sampler being used +// Description returns description of the sampler being used. func (fs *FallbackSampler) Description() string { return "FallbackSampler{fallback sampling with sampling config of 1 req/sec and 5% of additional requests}" } @@ -74,7 +75,7 @@ func (fs *FallbackSampler) take(now time.Time, itemCost float64) bool { } // update quota balance based on elapsed time - fs.refreshQuotaBalance(now) + fs.refreshQuotaBalanceLocked(now) if fs.quotaBalance >= itemCost { fs.quotaBalance -= itemCost @@ -84,11 +85,11 @@ func (fs *FallbackSampler) take(now time.Time, itemCost float64) bool { return false } -// refreshQuotaBalance refreshes the quotaBalance considering elapsedTime. -func (fs *FallbackSampler) refreshQuotaBalance(now time.Time) { - currentTime := now - elapsedTime := currentTime.Sub(fs.lastTick) - fs.lastTick = currentTime +// refreshQuotaBalanceLocked refreshes the quotaBalance considering elapsedTime. +// It is assumed the lock is held when calling this. +func (fs *FallbackSampler) refreshQuotaBalanceLocked(now time.Time) { + elapsedTime := now.Sub(fs.lastTick) + fs.lastTick = now // when elapsedTime is higher than 1 even then we need to keep quotaBalance // near to 1 so making elapsedTime to 1 for only borrowing 1 per second case diff --git a/samplers/aws/xray/go.mod b/samplers/aws/xray/go.mod index 4ced10348be..918da3cb3c9 100644 --- a/samplers/aws/xray/go.mod +++ b/samplers/aws/xray/go.mod @@ -3,7 +3,7 @@ module go.opentelemetry.io/contrib/samplers/aws/xray go 1.16 require ( - github.com/go-logr/logr v1.2.2 + github.com/go-logr/logr v1.2.3 github.com/go-logr/stdr v1.2.2 github.com/jinzhu/copier v0.3.5 github.com/stretchr/testify v1.7.0 diff --git a/samplers/aws/xray/go.sum b/samplers/aws/xray/go.sum index 46a4c9b0e35..df4dc141237 100644 --- a/samplers/aws/xray/go.sum +++ b/samplers/aws/xray/go.sum @@ -1,7 +1,8 @@ github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/go-logr/logr v1.2.2 h1:ahHml/yUpnlb96Rp8HCvtYVPY8ZYpxq3g7UYchIYwbs= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= +github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o= diff --git a/samplers/aws/xray/internal/client.go b/samplers/aws/xray/internal/client.go index 5c5cad07c8b..e98901f6fe4 100644 --- a/samplers/aws/xray/internal/client.go +++ b/samplers/aws/xray/internal/client.go @@ -54,49 +54,49 @@ type getSamplingTargetsInput struct { // samplingStatisticsDocument is used to store current state of sampling data. type samplingStatisticsDocument struct { - // a unique identifier for the service in hexadecimal + // A unique identifier for the service in hexadecimal. ClientID *string - // the name of the sampling rule + // The name of the sampling rule. RuleName *string - // the number of requests that matched the rule + // The number of requests that matched the rule. RequestCount *int64 - // the number of requests borrowed + // The number of requests borrowed. BorrowCount *int64 - // the number of requests sampled using the rule + // The number of requests sampled using the rule. SampledCount *int64 - // the current time + // The current time. Timestamp *int64 } -// getSamplingTargetsOutput is used to store parsed json sampling targets +// getSamplingTargetsOutput is used to store parsed json sampling targets. type getSamplingTargetsOutput struct { LastRuleModification *float64 `json:"LastRuleModification,omitempty"` SamplingTargetDocuments []*samplingTargetDocument `json:"SamplingTargetDocuments,omitempty"` UnprocessedStatistics []*unprocessedStatistic `json:"UnprocessedStatistics,omitempty"` } -// samplingTargetDocument contains updated targeted information retrieved from X-Ray service +// samplingTargetDocument contains updated targeted information retrieved from X-Ray service. type samplingTargetDocument struct { - // the percentage of matching requests to instrument, after the reservoir is - // exhausted + // The percentage of matching requests to instrument, after the reservoir is + // exhausted. FixedRate *float64 `json:"FixedRate,omitempty"` - // the number of seconds for the service to wait before getting sampling targets - // again + // The number of seconds for the service to wait before getting sampling targets + // again. Interval *int64 `json:"Interval,omitempty"` - // the number of requests per second that X-Ray allocated this service + // The number of requests per second that X-Ray allocated this service. ReservoirQuota *float64 `json:"ReservoirQuota,omitempty"` - // when the reservoir quota expires + // The reservoir quota expires. ReservoirQuotaTTL *float64 `json:"ReservoirQuotaTTL,omitempty"` - // the name of the sampling rule + // The name of the sampling rule. RuleName *string `json:"RuleName,omitempty"` } @@ -107,19 +107,19 @@ type unprocessedStatistic struct { } type xrayClient struct { - // http client for sending sampling requests to the collector + // HTTP client for sending sampling requests to the collector. httpClient *http.Client - // resolved URL to call getSamplingRules API + // Resolved URL to call getSamplingRules API. samplingRulesURL string - // resolved URL to call getSamplingTargets API + // Resolved URL to call getSamplingTargets API. samplingTargetsURL string } // newClient returns an HTTP client with proxy endpoint. func newClient(endpoint url.URL) (client *xrayClient, err error) { - // construct resolved URL for getSamplingRules and getSamplingTargets API calls + // Construct resolved URLs for getSamplingRules and getSamplingTargets API calls. endpoint.Path = "/GetSamplingRules" samplingRulesURL := endpoint diff --git a/samplers/aws/xray/internal/client_test.go b/samplers/aws/xray/internal/client_test.go index 2aeb381d9fb..060f46e653f 100644 --- a/samplers/aws/xray/internal/client_test.go +++ b/samplers/aws/xray/internal/client_test.go @@ -25,6 +25,20 @@ import ( "github.com/stretchr/testify/require" ) +func createTestClient(testServerURL string) (*xrayClient, error) { + u, err := url.Parse(testServerURL) + if err != nil { + return nil, err + } + + client, err := newClient(*u) + if err != nil { + return nil, err + } + + return client, nil +} + func TestGetSamplingRules(t *testing.T) { body := []byte(`{ "NextToken": null, @@ -92,14 +106,12 @@ func TestGetSamplingRules(t *testing.T) { // generate a test server so we can capture and inspect the request testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { - _, err := res.Write([]byte(body)) + _, err := res.Write(body) require.NoError(t, err) })) + t.Cleanup(testServer.Close) - u, err := url.Parse(testServer.URL) - require.NoError(t, err) - - client, err := newClient(*u) + client, err := createTestClient(testServer.URL) require.NoError(t, err) samplingRules, err := client.getSamplingRules(ctx) @@ -157,12 +169,9 @@ func TestGetSamplingRulesWithMissingValues(t *testing.T) { _, err := res.Write([]byte(body)) require.NoError(t, err) })) - defer testServer.Close() + t.Cleanup(testServer.Close) - u, err := url.Parse(testServer.URL) - require.NoError(t, err) - - client, err := newClient(*u) + client, err := createTestClient(testServer.URL) require.NoError(t, err) samplingRules, err := client.getSamplingRules(ctx) @@ -204,12 +213,9 @@ func TestGetSamplingTargets(t *testing.T) { _, err := res.Write([]byte(body)) require.NoError(t, err) })) - defer testServer.Close() + t.Cleanup(testServer.Close) - u, err := url.Parse(testServer.URL) - require.NoError(t, err) - - client, err := newClient(*u) + client, err := createTestClient(testServer.URL) require.NoError(t, err) samplingTragets, err := client.getSamplingTargets(ctx, nil) @@ -252,12 +258,9 @@ func TestGetSamplingTargetsMissingValues(t *testing.T) { _, err := res.Write([]byte(body)) require.NoError(t, err) })) - defer testServer.Close() - - endpoint, err := url.Parse(testServer.URL) - require.NoError(t, err) + t.Cleanup(testServer.Close) - client, err := newClient(*endpoint) + client, err := createTestClient(testServer.URL) require.NoError(t, err) samplingTragets, err := client.getSamplingTargets(ctx, nil) diff --git a/samplers/aws/xray/internal/clock.go b/samplers/aws/xray/internal/clock.go index 5fb98674570..4482cb0f97a 100644 --- a/samplers/aws/xray/internal/clock.go +++ b/samplers/aws/xray/internal/clock.go @@ -18,26 +18,26 @@ import ( "time" ) -// Clock provides an interface to implement method for getting current time. +// clock represents a time keeper that returns its version of the current time. type clock interface { now() time.Time } -// DefaultClock is an implementation of Clock interface. +// defaultClock wraps the standard time package. type defaultClock struct{} -// Now returns current time. +// now returns current time according to the standard time package. func (t *defaultClock) now() time.Time { return time.Now() } -// MockClock is a struct to record current time. +// mockClock is a time keeper that returns a fixed time. type mockClock struct { nowTime int64 nowNanos int64 } -// Now function returns NowTime value. +// now function returns the fixed time value stored in c. func (c *mockClock) now() time.Time { return time.Unix(c.nowTime, c.nowNanos) } diff --git a/samplers/aws/xray/internal/manifest.go b/samplers/aws/xray/internal/manifest.go index cfde8dd7c48..e80f1a7378b 100644 --- a/samplers/aws/xray/internal/manifest.go +++ b/samplers/aws/xray/internal/manifest.go @@ -25,16 +25,18 @@ import ( "sync" "time" - "github.com/go-logr/logr" "github.com/jinzhu/copier" + "github.com/go-logr/logr" + sdktrace "go.opentelemetry.io/otel/sdk/trace" ) const manifestTTL = 3600 +const version = 1 // Manifest represents a full sampling ruleset and provides -// option for configuring Logger, Clock and xrayClient. +// options for configuring Logger, Clock and xrayClient. type Manifest struct { Rules []Rule SamplingTargetsPollingInterval time.Duration @@ -46,15 +48,16 @@ type Manifest struct { mu sync.RWMutex } -// NewManifest return manifest object configured with logging, clock and xrayClient. +// NewManifest return manifest object configured the passed with logging and an xrayClient +// configured to address addr. func NewManifest(addr url.URL, logger logr.Logger) (*Manifest, error) { - // generate client for getSamplingRules and getSamplingTargets API call + // Generate client for getSamplingRules and getSamplingTargets API call. client, err := newClient(addr) if err != nil { return nil, err } - // generate clientID for sampling statistics + // Generate clientID for sampling statistics. clientID, err := generateClientID() if err != nil { return nil, err @@ -80,14 +83,12 @@ func (m *Manifest) Expired() bool { } // MatchAgainstManifestRules returns a Rule and boolean flag set as true -// if rule has been match against span attributes, otherwise nil and false +// if rule has been match against span attributes, otherwise nil and false. func (m *Manifest) MatchAgainstManifestRules(parameters sdktrace.SamplingParameters, serviceName string, cloudPlatform string) (*Rule, bool, error) { m.mu.RLock() rules := m.Rules m.mu.RUnlock() - matched := false - for index := range rules { isRuleMatch, err := rules[index].appliesTo(parameters, serviceName, cloudPlatform) if err != nil { @@ -95,39 +96,46 @@ func (m *Manifest) MatchAgainstManifestRules(parameters sdktrace.SamplingParamet } if isRuleMatch { - matched = true - return &rules[index], matched, nil + return &rules[index], true, nil } } - return nil, matched, nil + return nil, false, nil } // RefreshManifestRules writes sampling rule properties to the manifest object. func (m *Manifest) RefreshManifestRules(ctx context.Context) (err error) { - // get sampling rules from AWS X-Ray console + // Get sampling rules from AWS X-Ray console. rules, err := m.xrayClient.getSamplingRules(ctx) if err != nil { return err } - // update the retrieved sampling rules to manifest object + // Update the retrieved sampling rules to manifest object. m.updateRules(rules) return } -// RefreshManifestTargets updates sampling targets (statistics) for each rule +// RefreshManifestTargets updates sampling targets (statistics) for each rule. func (m *Manifest) RefreshManifestTargets(ctx context.Context) (refresh bool, err error) { var manifest Manifest // deep copy centralized manifest object to temporary manifest to avoid thread safety issue - m.mu.RLock() - err = copier.CopyWithOption(&manifest, m, copier.Option{IgnoreEmpty: false, DeepCopy: true}) + err = func() error { + m.mu.RLock() + defer m.mu.RUnlock() + + err = copier.CopyWithOption(&manifest, m, copier.Option{IgnoreEmpty: false, DeepCopy: true}) + if err != nil { + return err + } + + return nil + }() if err != nil { return false, err } - m.mu.RUnlock() // generate sampling statistics based on the data in temporary manifest statistics, err := manifest.snapshots() @@ -135,13 +143,13 @@ func (m *Manifest) RefreshManifestTargets(ctx context.Context) (refresh bool, er return false, err } - // return if no statistics to report + // Return if no statistics to report. if len(statistics) == 0 { m.logger.V(5).Info("no statistics to report and not refreshing sampling targets") return false, nil } - // get sampling targets (statistics) for every expired rule from AWS X-Ray + // Get sampling targets (statistics) for every expired rule from AWS X-Ray. targets, err := m.xrayClient.getSamplingTargets(ctx, statistics) if err != nil { return false, fmt.Errorf("refreshTargets: error occurred while getting sampling targets: %w", err) @@ -149,19 +157,19 @@ func (m *Manifest) RefreshManifestTargets(ctx context.Context) (refresh bool, er m.logger.V(5).Info("successfully fetched sampling targets") - // update temporary manifest with retrieved targets (statistics) for each rule + // Update temporary manifest with retrieved targets (statistics) for each rule. refresh, err = manifest.updateTargets(targets) if err != nil { return refresh, err } - // find next polling interval for targets + // Find next polling interval for targets. minPoll := manifest.minimumPollingInterval() if minPoll > 0 { m.SamplingTargetsPollingInterval = minPoll } - // update centralized manifest object + // Update centralized manifest object. m.mu.Lock() m.Rules = manifest.Rules m.mu.Unlock() @@ -180,12 +188,12 @@ func (m *Manifest) updateRules(rules *getSamplingRulesOutput) { continue } - if records.SamplingRule.Version != int64(1) { + if records.SamplingRule.Version != version { m.logger.V(5).Info("sampling rule without Version 1 is not supported", "RuleName", records.SamplingRule.RuleName) continue } - // create rule and store it in temporary manifest to avoid thread safety issues + // Create the rule and store it in temporary manifest to avoid thread safety issues. tempManifest.createRule(*records.SamplingRule) } @@ -214,14 +222,14 @@ func (m *Manifest) createRule(ruleProp ruleProperties) { } func (m *Manifest) updateTargets(targets *getSamplingTargetsOutput) (refresh bool, err error) { - // update sampling targets for each rule + // Update sampling targets for each rule. for _, t := range targets.SamplingTargetDocuments { if err := m.updateReservoir(t); err != nil { return false, err } } - // consume unprocessed statistics messages + // Consume unprocessed statistics messages. for _, s := range targets.UnprocessedStatistics { m.logger.V(5).Info( "error occurred updating sampling target for rule, code and message", "RuleName", s.RuleName, "ErrorCode", @@ -229,25 +237,25 @@ func (m *Manifest) updateTargets(targets *getSamplingTargetsOutput) (refresh boo "Message", s.Message, ) - // do not set any flags if error is unknown + // Do not set any flags if error is unknown. if s.ErrorCode == nil || s.RuleName == nil { continue } - // set batch failure if any sampling statistics returned 5xx + // Set batch failure if any sampling statistics returned 5xx. if strings.HasPrefix(*s.ErrorCode, "5") { return false, fmt.Errorf("sampling statistics returned 5xx") } - // set refresh flag if any sampling statistics returned 4xx + // Set refresh flag if any sampling statistics returned 4xx. if strings.HasPrefix(*s.ErrorCode, "4") { refresh = true } } - // set refresh flag if modifiedAt timestamp from remote is greater than ours + // Set refresh flag if modifiedAt timestamp from remote is greater than ours. if remote := targets.LastRuleModification; remote != nil { - // convert unix timestamp to time.Time + // Convert unix timestamp to time.Time. lastRuleModification := time.Unix(int64(*targets.LastRuleModification), 0) if lastRuleModification.After(m.refreshedAt) { @@ -260,11 +268,11 @@ func (m *Manifest) updateTargets(targets *getSamplingTargetsOutput) (refresh boo func (m *Manifest) updateReservoir(t *samplingTargetDocument) (err error) { if t.RuleName == nil { - return fmt.Errorf("invalid sampling target. Missing rule name") + return fmt.Errorf("invalid sampling targe: missing rule name") } if t.FixedRate == nil { - return fmt.Errorf("invalid sampling target for rule %s. Missing fixed rate", *t.RuleName) + return fmt.Errorf("invalid sampling target for rule %s: missing fixed rate", *t.RuleName) } for index := range m.Rules { @@ -293,9 +301,9 @@ func (m *Manifest) updateReservoir(t *samplingTargetDocument) (err error) { // snapshots takes a snapshot of sampling statistics from all rules, resetting // statistics counters in the process. func (m *Manifest) snapshots() ([]*samplingStatisticsDocument, error) { - statistics := make([]*samplingStatisticsDocument, 0, len(m.Rules)+1) + statistics := make([]*samplingStatisticsDocument, 0, len(m.Rules)) - // Generate sampling statistics for user-defined rules + // Generate sampling statistics for user-defined rules. for index := range m.Rules { if m.Rules[index].stale(m.clock.now()) { s := m.Rules[index].snapshot(m.clock.now()) @@ -308,9 +316,8 @@ func (m *Manifest) snapshots() ([]*samplingStatisticsDocument, error) { return statistics, nil } -// sort sorts the rule array first by priority and then by rule name. +// sort sorts the Rules of m array first by priority and then by name. func (m *Manifest) sort() { - // comparison function less := func(i, j int) bool { if m.Rules[i].ruleProperties.Priority == m.Rules[j].ruleProperties.Priority { return strings.Compare(m.Rules[i].ruleProperties.RuleName, m.Rules[j].ruleProperties.RuleName) < 0 @@ -321,7 +328,7 @@ func (m *Manifest) sort() { sort.Slice(m.Rules, less) } -// minimumPollingInterval finds the minimum interval amongst all the targets +// minimumPollingInterval finds the minimum polling interval for all the targets of m's Rules. func (m *Manifest) minimumPollingInterval() time.Duration { if len(m.Rules) == 0 { return time.Duration(0) @@ -337,7 +344,7 @@ func (m *Manifest) minimumPollingInterval() time.Duration { return minPoll * time.Second } -// generateClientID generates random client ID +// generateClientID generates random client ID. func generateClientID() (*string, error) { var r [12]byte diff --git a/samplers/aws/xray/internal/manifest_test.go b/samplers/aws/xray/internal/manifest_test.go index 3af87506f14..967073b9092 100644 --- a/samplers/aws/xray/internal/manifest_test.go +++ b/samplers/aws/xray/internal/manifest_test.go @@ -16,18 +16,16 @@ package internal import ( "context" - "log" "net/http" "net/http/httptest" "net/url" - "os" "sync" "testing" "time" "go.opentelemetry.io/otel/attribute" - "github.com/go-logr/stdr" + "github.com/go-logr/logr/testr" "github.com/stretchr/testify/require" sdktrace "go.opentelemetry.io/otel/sdk/trace" @@ -37,7 +35,7 @@ import ( // assert that new manifest has certain non-nil attributes. func TestNewManifest(t *testing.T) { - logger := stdr.NewWithOptions(log.New(os.Stderr, "", log.LstdFlags|log.Lshortfile), stdr.Options{LogCaller: stdr.Error}) + logger := testr.New(t) endpoint, err := url.Parse("http://127.0.0.1:2020") require.NoError(t, err) @@ -130,7 +128,8 @@ func TestMatchAgainstManifestRules(t *testing.T) { Rules: rules, } - exp, _, err := m.MatchAgainstManifestRules(sdktrace.SamplingParameters{}, "test", "local") + exp, match, err := m.MatchAgainstManifestRules(sdktrace.SamplingParameters{}, "test", "local") + require.True(t, match) require.NoError(t, err) // assert that manifest rule r2 is a match @@ -138,7 +137,7 @@ func TestMatchAgainstManifestRules(t *testing.T) { } // assert that if rules has attribute and span has those attribute with same value then matching will happen. -func TestMatchAgainstManifestRules_AttributeMatch(t *testing.T) { +func TestMatchAgainstManifestRulesAttributeMatch(t *testing.T) { commonLabels := []attribute.KeyValue{ attribute.String("labelA", "chocolate"), attribute.String("labelB", "raspberry"), @@ -173,16 +172,16 @@ func TestMatchAgainstManifestRules_AttributeMatch(t *testing.T) { Rules: rules, } - exp, _, err := m.MatchAgainstManifestRules(sdktrace.SamplingParameters{Attributes: commonLabels}, "test", "local") + exp, match, err := m.MatchAgainstManifestRules(sdktrace.SamplingParameters{Attributes: commonLabels}, "test", "local") + require.True(t, match) require.NoError(t, err) // assert that manifest rule r1 is a match - assert.Nil(t, err) assert.Equal(t, *exp, r1) } // assert that wildcard attributes will match. -func TestMatchAgainstManifestRules_AttributeWildCardMatch(t *testing.T) { +func TestMatchAgainstManifestRulesAttributeWildCardMatch(t *testing.T) { commonLabels := []attribute.KeyValue{ attribute.String("labelA", "chocolate"), attribute.String("labelB", "raspberry"), @@ -217,7 +216,8 @@ func TestMatchAgainstManifestRules_AttributeWildCardMatch(t *testing.T) { Rules: rules, } - exp, _, err := m.MatchAgainstManifestRules(sdktrace.SamplingParameters{Attributes: commonLabels}, "test", "local") + exp, match, err := m.MatchAgainstManifestRules(sdktrace.SamplingParameters{Attributes: commonLabels}, "test", "local") + require.True(t, match) require.NoError(t, err) // assert that manifest rule r1 is a match @@ -227,7 +227,7 @@ func TestMatchAgainstManifestRules_AttributeWildCardMatch(t *testing.T) { // assert that when no known rule is match then returned rule is nil, // matched flag is false -func TestMatchAgainstManifestRules_NoMatch(t *testing.T) { +func TestMatchAgainstManifestRulesNoMatch(t *testing.T) { r1 := Rule{ ruleProperties: ruleProperties{ RuleName: "r1", @@ -332,12 +332,9 @@ func TestRefreshManifestRules(t *testing.T) { _, err := res.Write(body) require.NoError(t, err) })) - defer testServer.Close() + t.Cleanup(testServer.Close) - u, err := url.Parse(testServer.URL) - require.NoError(t, err) - - client, err := newClient(*u) + client, err := createTestClient(testServer.URL) require.NoError(t, err) m := &Manifest{ @@ -415,13 +412,12 @@ func TestRefreshManifestRules(t *testing.T) { samplingStatistics: &samplingStatistics{}, } + require.Len(t, m.Rules, 3) + // Assert on sorting order assert.Equal(t, r2, m.Rules[0]) assert.Equal(t, r3, m.Rules[1]) assert.Equal(t, r1, m.Rules[2]) - - // Assert on size of manifest - assert.Equal(t, 3, len(m.Rules)) } // assert that rule with no ServiceName updates manifest successfully with empty values. @@ -459,12 +455,9 @@ func TestRefreshManifestMissingServiceName(t *testing.T) { require.NoError(t, err) })) - defer testServer.Close() + t.Cleanup(testServer.Close) - u, err := url.Parse(testServer.URL) - require.NoError(t, err) - - client, err := newClient(*u) + client, err := createTestClient(testServer.URL) require.NoError(t, err) m := &Manifest{ @@ -477,7 +470,7 @@ func TestRefreshManifestMissingServiceName(t *testing.T) { require.NoError(t, err) // assert on rule gets added - assert.Equal(t, 1, len(m.Rules)) + assert.Len(t, m.Rules, 1) } // assert that rule with no RuleName does not update to the manifest. @@ -514,26 +507,23 @@ func TestRefreshManifestMissingRuleName(t *testing.T) { require.NoError(t, err) })) - defer testServer.Close() - - u, err := url.Parse(testServer.URL) - require.NoError(t, err) + t.Cleanup(testServer.Close) - client, err := newClient(*u) + client, err := createTestClient(testServer.URL) require.NoError(t, err) m := &Manifest{ Rules: []Rule{}, xrayClient: client, clock: &defaultClock{}, - logger: stdr.NewWithOptions(log.New(os.Stderr, "", log.LstdFlags|log.Lshortfile), stdr.Options{LogCaller: stdr.Error}), + logger: testr.New(t), } err = m.RefreshManifestRules(ctx) require.NoError(t, err) // assert on rule not added - assert.Equal(t, 0, len(m.Rules)) + assert.Len(t, m.Rules, 0) } // assert that rule with version greater than one does not update to the manifest. @@ -572,26 +562,23 @@ func TestRefreshManifestIncorrectVersion(t *testing.T) { require.NoError(t, err) })) - defer testServer.Close() + t.Cleanup(testServer.Close) - u, err := url.Parse(testServer.URL) - require.NoError(t, err) - - client, err := newClient(*u) + client, err := createTestClient(testServer.URL) require.NoError(t, err) m := &Manifest{ Rules: []Rule{}, xrayClient: client, clock: &defaultClock{}, - logger: stdr.NewWithOptions(log.New(os.Stderr, "", log.LstdFlags|log.Lshortfile), stdr.Options{LogCaller: stdr.Error}), + logger: testr.New(t), } err = m.RefreshManifestRules(ctx) require.NoError(t, err) // assert on rule not added - assert.Equal(t, 0, len(m.Rules)) + assert.Len(t, m.Rules, 0) } // assert that 1 valid and 1 invalid rule update only valid rule gets stored to the manifest. @@ -681,20 +668,20 @@ func TestRefreshManifestAddOneInvalidRule(t *testing.T) { Rules: []Rule{}, xrayClient: client, clock: &defaultClock{}, - logger: stdr.NewWithOptions(log.New(os.Stderr, "", log.LstdFlags|log.Lshortfile), stdr.Options{LogCaller: stdr.Error}), + logger: testr.New(t), } err = m.RefreshManifestRules(ctx) require.NoError(t, err) - assert.Equal(t, 1, len(m.Rules)) + assert.Len(t, m.Rules, 1) // assert on r1 assert.Equal(t, r1, m.Rules[0]) } // assert that inactive rule so return early without doing getSamplingTargets call -func TestRefreshManifestTarget_NoSnapShot(t *testing.T) { +func TestRefreshManifestTargetNoSnapShot(t *testing.T) { clock := &mockClock{ nowTime: 15000000, } @@ -727,12 +714,12 @@ func TestRefreshManifestTarget_NoSnapShot(t *testing.T) { m := &Manifest{ Rules: rules, clock: clock, - logger: stdr.NewWithOptions(log.New(os.Stderr, "", log.LstdFlags|log.Lshortfile), stdr.Options{LogCaller: stdr.Error}), + logger: testr.New(t), } refresh, err := m.RefreshManifestTargets(context.Background()) assert.False(t, refresh) - assert.Nil(t, err) + assert.NoError(t, err) } // assert that refresh manifest targets successfully updates reservoir value for a rule. @@ -793,18 +780,16 @@ func TestRefreshManifestTargets(t *testing.T) { _, err := res.Write(body) require.NoError(t, err) })) - defer testServer.Close() + t.Cleanup(testServer.Close) - u, err := url.Parse(testServer.URL) + client, err := createTestClient(testServer.URL) require.NoError(t, err) - client, err := newClient(*u) - require.NoError(t, err) refreshedAt := time.Unix(18000000, 0) m := &Manifest{ Rules: rules, clock: clock, - logger: stdr.NewWithOptions(log.New(os.Stderr, "", log.LstdFlags|log.Lshortfile), stdr.Options{LogCaller: stdr.Error}), + logger: testr.New(t), xrayClient: client, refreshedAt: refreshedAt, } @@ -814,6 +799,7 @@ func TestRefreshManifestTargets(t *testing.T) { require.NoError(t, err) // assert target updates + require.Len(t, m.Rules, 1) assert.Equal(t, m.Rules[0].ruleProperties.FixedRate, 0.06) assert.Equal(t, m.Rules[0].reservoir.quota, 23.0) assert.Equal(t, m.Rules[0].reservoir.expiresAt, time.Unix(15000000, 0)) @@ -821,8 +807,7 @@ func TestRefreshManifestTargets(t *testing.T) { } // assert that refresh manifest targets successfully updates samplingTargetsPollingInterval. -func TestRefreshManifestTargets_PollIntervalUpdateTest(t *testing.T) { - // RuleName is missing from r2 +func TestRefreshManifestTargetsPollIntervalUpdateTest(t *testing.T) { body := []byte(`{ "LastRuleModification": 17000000, "SamplingTargetDocuments": [ @@ -900,12 +885,9 @@ func TestRefreshManifestTargets_PollIntervalUpdateTest(t *testing.T) { _, err := res.Write(body) require.NoError(t, err) })) - defer testServer.Close() + t.Cleanup(testServer.Close) - u, err := url.Parse(testServer.URL) - require.NoError(t, err) - - client, err := newClient(*u) + client, err := createTestClient(testServer.URL) require.NoError(t, err) refreshedAt := time.Unix(18000000, 0) @@ -913,7 +895,7 @@ func TestRefreshManifestTargets_PollIntervalUpdateTest(t *testing.T) { m := &Manifest{ Rules: rules, clock: clock, - logger: stdr.NewWithOptions(log.New(os.Stderr, "", log.LstdFlags|log.Lshortfile), stdr.Options{LogCaller: stdr.Error}), + logger: testr.New(t), xrayClient: client, refreshedAt: refreshedAt, } @@ -922,7 +904,7 @@ func TestRefreshManifestTargets_PollIntervalUpdateTest(t *testing.T) { require.NoError(t, err) // assert that sampling rules polling interval is minimum of all target intervals min(15, 5, 25) - assert.Equal(t, m.SamplingTargetsPollingInterval, 5*time.Second) + assert.Equal(t, 5*time.Second, m.SamplingTargetsPollingInterval) } // assert that a valid sampling target updates its rule. @@ -1102,7 +1084,7 @@ func TestUpdateTargetsUnprocessedStatistics(t *testing.T) { m := &Manifest{ clock: clock, - logger: stdr.NewWithOptions(log.New(os.Stderr, "", log.LstdFlags|log.Lshortfile), stdr.Options{LogCaller: stdr.Error}), + logger: testr.New(t), } refresh, err := m.updateTargets(targets5xx) @@ -1144,7 +1126,7 @@ func TestUpdateTargetsUnprocessedStatistics(t *testing.T) { m = &Manifest{ clock: clock, - logger: stdr.NewWithOptions(log.New(os.Stderr, "", log.LstdFlags|log.Lshortfile), stdr.Options{LogCaller: stdr.Error}), + logger: testr.New(t), } refresh, err = m.updateTargets(targets) @@ -1450,7 +1432,7 @@ func TestMixedSnapshots(t *testing.T) { require.NoError(t, err) // assert that only inactive rules are added to the statistics - assert.Equal(t, 1, len(statistics)) + require.Len(t, statistics, 1) assert.Equal(t, ss1, *statistics[0]) } diff --git a/samplers/aws/xray/internal/match_test.go b/samplers/aws/xray/internal/match_test.go index 66824bd19c4..5dff0e0a801 100644 --- a/samplers/aws/xray/internal/match_test.go +++ b/samplers/aws/xray/internal/match_test.go @@ -63,13 +63,14 @@ func TestWildCardMatchPositive(t *testing.T) { {"?o?se", "mouse"}, {"*s", "horse"}, {"J*", "Jeep"}, + {"J*", "jeep"}, {"*/foo", "/bar/foo"}, } for _, test := range tests { match, err := wildcardMatch(test.pattern, test.text) require.NoError(t, err) - assert.True(t, match) + assert.True(t, match, test.text) } } diff --git a/samplers/aws/xray/internal/reservoir.go b/samplers/aws/xray/internal/reservoir.go index d065bfda006..bb0ac51f882 100644 --- a/samplers/aws/xray/internal/reservoir.go +++ b/samplers/aws/xray/internal/reservoir.go @@ -20,33 +20,33 @@ import ( ) // reservoir represents a sampling statistics for a given rule and populate it's value from -// the response getSamplingTargets API which sends information on sampling statistics real-time +// the response getSamplingTargets API which sends information on sampling statistics in real-time. type reservoir struct { - // quota expiration timestamp + // Quota expiration timestamp. expiresAt time.Time - // quota assigned to client to consume per second + // Quota assigned to client to consume per second. quota float64 - // current balance of quota + // Current balance of quota. quotaBalance float64 - // total size of reservoir consumption per second + // Total size of reservoir consumption per second. capacity float64 - // quota refresh timestamp + // Quota refresh timestamp. refreshedAt time.Time - // polling interval for quota + // Polling interval for quota. interval time.Duration - // stores reservoir ticks + // Stores reservoir ticks. lastTick time.Time mu *sync.RWMutex } -// expired returns true if current time is past expiration timestamp. False otherwise. +// expired returns true if current time is past expiration timestamp. Otherwise, false is returned if no quota remains. func (r *reservoir) expired(now time.Time) bool { r.mu.RLock() defer r.mu.RUnlock() @@ -75,7 +75,7 @@ func (r *reservoir) take(now time.Time, borrowed bool, itemCost float64) bool { } // update quota balance based on elapsed time - r.refreshQuotaBalance(now, borrowed) + r.refreshQuotaBalanceLocked(now, borrowed) if r.quotaBalance >= itemCost { r.quotaBalance -= itemCost @@ -85,17 +85,17 @@ func (r *reservoir) take(now time.Time, borrowed bool, itemCost float64) bool { return false } -// refreshQuotaBalance refreshes the quotaBalance. if borrowed true then add quota balance 1 by every second -// otherwise add quota balance based on assigned quota by X-Ray service -func (r *reservoir) refreshQuotaBalance(now time.Time, borrowed bool) { - currentTime := now - elapsedTime := currentTime.Sub(r.lastTick) - r.lastTick = currentTime +// refreshQuotaBalanceLocked refreshes the quotaBalance. If borrowed is true then add to the quota balance 1 by every second, +// otherwise add to the quota balance based on assigned quota by X-Ray service. +// It is assumed the lock is held when calling this. +func (r *reservoir) refreshQuotaBalanceLocked(now time.Time, borrowed bool) { + elapsedTime := now.Sub(r.lastTick) + r.lastTick = now - // calculate how much credit have we accumulated since the last tick + // Calculate how much credit have we accumulated since the last tick. if borrowed { - // when elapsedTime is higher than 1 even then we need to keep quotaBalance - // near to 1 so making elapsedTime to 1 for only borrowing 1 per second case + // In borrowing case since we want to enforce sample one req every second, no need to accumulate + // quotaBalance based on elapsedTime when elapsedTime is greater than 1. if elapsedTime.Seconds() > 1.0 { r.quotaBalance += 1.0 } else { diff --git a/samplers/aws/xray/internal/reservoir_test.go b/samplers/aws/xray/internal/reservoir_test.go index 96f9f0c0412..7a93b479a39 100644 --- a/samplers/aws/xray/internal/reservoir_test.go +++ b/samplers/aws/xray/internal/reservoir_test.go @@ -52,9 +52,7 @@ func TestExpiredReservoirSameAsClockTime(t *testing.T) { mu: &sync.RWMutex{}, } - expired := r.expired(clock.now()) - - assert.False(t, expired) + assert.False(t, r.expired(clock.now())) } // assert that borrow only 1 req/sec @@ -85,7 +83,7 @@ func TestBorrowEverySecond(t *testing.T) { // assert that when reservoir is expired we consume from quota is 1 and then // when reservoir is not expired consume from assigned quota by X-Ray service -func TestConsumeFromBorrow_ConsumeFromQuota(t *testing.T) { +func TestConsumeFromBorrowConsumeFromQuota(t *testing.T) { clock := &mockClock{ nowTime: 1500000000, } @@ -182,8 +180,7 @@ func TestResetQuotaUsageRotation(t *testing.T) { // consume quota for second for i := 0; i < 5; i++ { - taken := r.take(clock.now(), false, 1.0) - assert.Equal(t, true, taken) + assert.True(t, r.take(clock.now(), false, 1.0)) } // take() should be false since no unused quota left @@ -196,13 +193,12 @@ func TestResetQuotaUsageRotation(t *testing.T) { } // take() should be true since ununsed quota is available - taken = r.take(clock.now(), false, 1.0) - assert.Equal(t, true, taken) + assert.True(t, r.take(clock.now(), false, 1.0)) } // assert that when quotaBalance exceeds totalQuotaBalanceCapacity then totalQuotaBalanceCapacity // gets assigned to quotaBalance -func TestQuotaBalanceNonBorrow_ExceedsCapacity(t *testing.T) { +func TestQuotaBalanceNonBorrowExceedsCapacity(t *testing.T) { clock := &mockClock{ nowTime: 1500000001, } @@ -214,10 +210,10 @@ func TestQuotaBalanceNonBorrow_ExceedsCapacity(t *testing.T) { lastTick: time.Unix(1500000000, 0), } - r.refreshQuotaBalance(clock.now(), false) + r.refreshQuotaBalanceLocked(clock.now(), false) // assert that if quotaBalance exceeds capacity then total capacity would be new quotaBalance - assert.Equal(t, r.quotaBalance, 1*r.capacity) + assert.Equal(t, r.quotaBalance, r.capacity) } // assert quotaBalance and capacity of borrowing case @@ -233,14 +229,14 @@ func TestQuotaBalanceBorrow(t *testing.T) { lastTick: time.Unix(1500000000, 0), } - r.refreshQuotaBalance(clock.now(), true) + r.refreshQuotaBalanceLocked(clock.now(), true) // assert that if quotaBalance exceeds capacity then total capacity would be new quotaBalance - assert.Equal(t, r.quotaBalance, 1.0) + assert.Equal(t, 1.0, r.quotaBalance) } // assert that when borrow is true and elapsedTime is greater than 1, then we only increase the quota balance by 1 -func TestQuotaBalanceIncreaseByOne_BorrowCase(t *testing.T) { +func TestQuotaBalanceIncreaseByOneBorrowCase(t *testing.T) { clock := &mockClock{ nowTime: 1500000002, } @@ -253,7 +249,7 @@ func TestQuotaBalanceIncreaseByOne_BorrowCase(t *testing.T) { lastTick: time.Unix(1500000000, 0), } - r.refreshQuotaBalance(clock.now(), true) + r.refreshQuotaBalanceLocked(clock.now(), true) - assert.Equal(t, r.quotaBalance, 1.25) + assert.Equal(t, 1.25, r.quotaBalance) } diff --git a/samplers/aws/xray/internal/rule.go b/samplers/aws/xray/internal/rule.go index 88433e676de..4ced79b37fc 100644 --- a/samplers/aws/xray/internal/rule.go +++ b/samplers/aws/xray/internal/rule.go @@ -22,31 +22,31 @@ import ( "go.opentelemetry.io/otel/trace" ) -// Rule represents a sampling rule which contains rule properties and reservoir which keeps tracks of sampling statistics of a rule +// Rule represents a sampling rule which contains rule properties and reservoir which keeps tracks of sampling statistics of a rule. type Rule struct { samplingStatistics *samplingStatistics - // reservoir has equivalent fields to store what we receive from service API getSamplingTargets + // reservoir has equivalent fields to store what we receive from service API getSamplingTargets. // https://docs.aws.amazon.com/xray/latest/api/API_GetSamplingTargets.html reservoir reservoir - // equivalent to what we receive from service API getSamplingRules + // ruleProperty is equivalent to what we receive from service API getSamplingRules. // https://docs.aws.amazon.com/cli/latest/reference/xray/get-sampling-rules.html ruleProperties ruleProperties } type samplingStatistics struct { - // number of requests matched against specific rule + // matchedRequests is the number of requests matched against specific rule. matchedRequests int64 - // number of requests sampled using specific rule + // sampledRequests is the number of requests sampled using specific rule. sampledRequests int64 - // number of requests borrowed using specific rule + // borrowedRequests is the number of requests borrowed using specific rule. borrowedRequests int64 } -// stale checks if targets (sampling stats) for a given rule is expired or not +// stale checks if targets (sampling stats) for a given rule is expired or not. func (r *Rule) stale(now time.Time) bool { reservoirRefreshTime := r.reservoir.refreshedAt.Add(time.Duration(r.reservoir.interval) * time.Second) return r.samplingStatistics.matchedRequests != 0 && now.After(reservoirRefreshTime) @@ -80,9 +80,9 @@ func (r *Rule) Sample(parameters sdktrace.SamplingParameters, now time.Time) sdk atomic.AddInt64(&r.samplingStatistics.matchedRequests, int64(1)) - // fallback sampling logic if quota for a given rule is expired + // Fallback sampling logic if quota for a given rule is expired. if r.reservoir.expired(now) { - // borrowing one request every second + // Borrowing one request every second. if r.reservoir.take(now, true, 1.0) { atomic.AddInt64(&r.samplingStatistics.borrowedRequests, int64(1)) @@ -90,7 +90,7 @@ func (r *Rule) Sample(parameters sdktrace.SamplingParameters, now time.Time) sdk return sd } - // using traceIDRatioBased sampler to sample using fixed rate + // Using traceIDRatioBased sampler to sample using fixed rate. sd = sdktrace.TraceIDRatioBased(r.ruleProperties.FixedRate).ShouldSample(parameters) if sd.Decision == sdktrace.RecordAndSample { @@ -100,7 +100,7 @@ func (r *Rule) Sample(parameters sdktrace.SamplingParameters, now time.Time) sdk return sd } - // Take from reservoir quota, if quota is available for that second + // Take from reservoir quota, if quota is available for that second. if r.reservoir.take(now, false, 1.0) { atomic.AddInt64(&r.samplingStatistics.sampledRequests, int64(1)) sd.Decision = sdktrace.RecordAndSample @@ -119,7 +119,7 @@ func (r *Rule) Sample(parameters sdktrace.SamplingParameters, now time.Time) sdk } // appliesTo performs a matching against rule properties to see -// if a given rule does match with any of the rule set on AWS X-Ray console +// if a given rule does match with any of the rule set on AWS X-Ray console. func (r *Rule) appliesTo(parameters sdktrace.SamplingParameters, serviceName string, cloudPlatform string) (bool, error) { var httpTarget string var httpURL string @@ -144,7 +144,7 @@ func (r *Rule) appliesTo(parameters sdktrace.SamplingParameters, serviceName str } } - // attributes and other HTTP span attributes matching + // Attributes and other HTTP span attributes matching. attributeMatcher, err := r.attributeMatching(parameters) if err != nil { return attributeMatcher, err @@ -186,29 +186,31 @@ func (r *Rule) appliesTo(parameters sdktrace.SamplingParameters, serviceName str HTTPURLPathMatcher, nil } -// attributeMatching performs a match on attributes set by users on AWS X-Ray console +// attributeMatching performs a match on attributes set by users on AWS X-Ray console. func (r *Rule) attributeMatching(parameters sdktrace.SamplingParameters) (bool, error) { match := false var err error - if len(r.ruleProperties.Attributes) > 0 { - for key, value := range r.ruleProperties.Attributes { - unmatchedCounter := 0 - for _, attrs := range parameters.Attributes { - if key == string(attrs.Key) { - match, err = wildcardMatch(value, attrs.Value.AsString()) - if err != nil { - return false, err - } - } else { - unmatchedCounter++ + + if len(r.ruleProperties.Attributes) == 0 { + return true, nil + } + + for key, value := range r.ruleProperties.Attributes { + unmatchedCounter := 0 + for _, attrs := range parameters.Attributes { + if key == string(attrs.Key) { + match, err = wildcardMatch(value, attrs.Value.AsString()) + if err != nil { + return false, err } + } else { + unmatchedCounter++ } - if unmatchedCounter == len(parameters.Attributes) { - return false, nil - } } - return match, nil + if unmatchedCounter == len(parameters.Attributes) { + return false, nil + } } - return true, nil + return match, nil } diff --git a/samplers/aws/xray/internal/rule_test.go b/samplers/aws/xray/internal/rule_test.go index 83afe0df324..5cf23ca549e 100644 --- a/samplers/aws/xray/internal/rule_test.go +++ b/samplers/aws/xray/internal/rule_test.go @@ -182,7 +182,7 @@ func TestConsumeFromReservoirSample(t *testing.T) { } // assert that sampling using traceIDRationBasedSampler when reservoir quota is consumed. -func TestTraceIDRatioBasedSampler_ReservoirIsConsumedSample(t *testing.T) { +func TestTraceIDRatioBasedSamplerReservoirIsConsumedSample(t *testing.T) { r1 := Rule{ reservoir: reservoir{ quota: 10, @@ -278,7 +278,7 @@ func TestAppliesToMatchingWithStarHTTPAttrs(t *testing.T) { // assert that matching will not happen when rules has all the HTTP attrs set as non '*' values and // span has no HTTP attributes. -func TestAppliesToMatchingWithHTTPAttrs_NoSpanAttrs(t *testing.T) { +func TestAppliesToMatchingWithHTTPAttrsNoSpanAttrs(t *testing.T) { r1 := Rule{ ruleProperties: ruleProperties{ RuleName: "r1", @@ -297,7 +297,7 @@ func TestAppliesToMatchingWithHTTPAttrs_NoSpanAttrs(t *testing.T) { // assert that matching will happen when rules has all the HTTP attrs set as '*' values and // span has no HTTP attributes. -func TestAppliesToMatchingWithStarHTTPAttrs_NoSpanAttrs(t *testing.T) { +func TestAppliesToMatchingWithStarHTTPAttrsNoSpanAttrs(t *testing.T) { r1 := Rule{ ruleProperties: ruleProperties{ RuleName: "r1", @@ -316,7 +316,7 @@ func TestAppliesToMatchingWithStarHTTPAttrs_NoSpanAttrs(t *testing.T) { // assert that matching will not happen when rules has some HTTP attrs set as non '*' values and // span has no HTTP attributes. -func TestAppliesToMatchingWithPartialHTTPAttrs_NoSpanAttrs(t *testing.T) { +func TestAppliesToMatchingWithPartialHTTPAttrsNoSpanAttrs(t *testing.T) { r1 := Rule{ ruleProperties: ruleProperties{ RuleName: "r1", @@ -395,7 +395,7 @@ func TestAttributeMatching(t *testing.T) { } // assert that if rules has no attributes then matching will happen. -func TestAttributeMatching_NoRuleAttrs(t *testing.T) { +func TestAttributeMatchingNoRuleAttrs(t *testing.T) { commonLabels := []attribute.KeyValue{ attribute.String("labelA", "chocolate"), attribute.String("labelB", "raspberry"), @@ -435,7 +435,7 @@ func TestAttributeWildCardMatching(t *testing.T) { // assert that if some of the rules attributes are not present in span attributes then matching // will not happen. -func TestMatchAgainstManifestRules_NoAttributeMatch(t *testing.T) { +func TestMatchAgainstManifestRulesNoAttributeMatch(t *testing.T) { commonLabels := []attribute.KeyValue{ attribute.String("labelA", "chocolate"), attribute.String("labelB", "raspberry"), diff --git a/samplers/aws/xray/rand.go b/samplers/aws/xray/rand.go index 2ba8df59193..59681bf3c6e 100644 --- a/samplers/aws/xray/rand.go +++ b/samplers/aws/xray/rand.go @@ -30,8 +30,10 @@ func newSeed() int64 { return seed } +var seed = newSeed() + func newGlobalRand() *rand.Rand { - src := rand.NewSource(newSeed()) + src := rand.NewSource(seed) if src64, ok := src.(rand.Source64); ok { return rand.New(src64) } diff --git a/samplers/aws/xray/remote_sampler.go b/samplers/aws/xray/remote_sampler.go index 097f743a930..13f021e3680 100644 --- a/samplers/aws/xray/remote_sampler.go +++ b/samplers/aws/xray/remote_sampler.go @@ -25,7 +25,7 @@ import ( ) // remoteSampler is a sampler for AWS X-Ray which polls sampling rules and sampling targets -// to make a sampling decision based on rules set by users on AWS X-Ray console +// to make a sampling decision based on rules set by users on AWS X-Ray console. type remoteSampler struct { // manifest is the list of known centralized sampling rules. manifest *internal.Manifest @@ -36,16 +36,13 @@ type remoteSampler struct { // samplingRulesPollingInterval, default is 300 seconds. samplingRulesPollingInterval time.Duration - // matching attribute serviceName string - // matching attribute cloudPlatform string - // fallback sampler fallbackSampler *FallbackSampler - // logger for logging + // logger for logging. logger logr.Logger } @@ -56,7 +53,7 @@ var _ sdktrace.Sampler = (*remoteSampler)(nil) // based on the sampling rules set by users on AWS X-Ray console. Sampler also periodically polls // sampling rules and sampling targets. func NewRemoteSampler(ctx context.Context, serviceName string, cloudPlatform string, opts ...Option) (sdktrace.Sampler, error) { - // create new config based on options or set to default values + // Create new config based on options or set to default values. cfg, err := newConfig(opts...) if err != nil { return nil, err @@ -77,37 +74,34 @@ func NewRemoteSampler(ctx context.Context, serviceName string, cloudPlatform str logger: cfg.logger, } - // starts the rule and target poller remoteSampler.start(ctx) return remoteSampler, nil } -// ShouldSample matches span attributes with retrieved sampling rules and perform sampling, -// if rules does not match or manifest is expired then use fallback sampling. +// ShouldSample matches span attributes with retrieved sampling rules and returns a sampling result. +// If the sampling parameter to not match or the manifest is expired then the fallback sampler is used. func (rs *remoteSampler) ShouldSample(parameters sdktrace.SamplingParameters) sdktrace.SamplingResult { if rs.manifest.Expired() { - // Use fallback sampler if manifest is expired + // Use fallback sampler if manifest is expired. rs.logger.V(5).Info("manifest is expired so using fallback sampling strategy") return rs.fallbackSampler.ShouldSample(parameters) } - // match against known rules r, match, err := rs.manifest.MatchAgainstManifestRules(parameters, rs.serviceName, rs.cloudPlatform) if err != nil { - rs.logger.Error(err, "regexp matching error while matching span and resource attributes") + rs.logger.Error(err, "rule matching error") return sdktrace.SamplingResult{} } if match { - // remote sampling based on rule match + // Remote sampling based on rule match. return r.Sample(parameters, time.Now()) } - // Use fallback sampler if - // sampling rules does not match against manifest - rs.logger.V(5).Info("span attributes does not match to the sampling rules or manifest is expired so using fallback sampling strategy") + // Use fallback sampler if sampling rules does not match against manifest. + rs.logger.V(5).Info("span does not match rules from manifest(or it is expired), using fallback sampler") return rs.fallbackSampler.ShouldSample(parameters) } @@ -124,17 +118,17 @@ func (rs *remoteSampler) start(ctx context.Context) { } // startPoller starts the rule and target poller in a single go routine which runs periodically -// to refresh manifest and targets. +// to the refresh manifest and targets. func (rs *remoteSampler) startPoller(ctx context.Context) { - // jitter = 5s, default 300 seconds + // jitter = 5s, default duration 300 seconds. rulesTicker := newTicker(rs.samplingRulesPollingInterval, 5*time.Second) defer rulesTicker.tick.Stop() - // jitter = 100ms, default 10 seconds + // jitter = 100ms, default duration 10 seconds. targetTicker := newTicker(rs.manifest.SamplingTargetsPollingInterval, 100*time.Millisecond) defer targetTicker.tick.Stop() - // fetch sampling rules to kick start the remote sampling + // Fetch sampling rules to kick start the remote sampling. rs.refreshManifest(ctx) for { @@ -144,7 +138,6 @@ func (rs *remoteSampler) startPoller(ctx context.Context) { return } - // fetch sampling rules and updates manifest rs.refreshManifest(ctx) continue case _, more := <-targetTicker.c(): @@ -152,10 +145,10 @@ func (rs *remoteSampler) startPoller(ctx context.Context) { return } - // fetch sampling targets and updates manifest refresh := rs.refreshTargets(ctx) - // out of band manifest refresh if it manifest is not updated + // If LastRuleModification time is more recent than manifest refresh time, + // then we explicitly perform refreshing the manifest. if refresh { rs.refreshManifest(ctx) } @@ -169,9 +162,9 @@ func (rs *remoteSampler) startPoller(ctx context.Context) { // refreshManifest refreshes the manifest retrieved via getSamplingRules API. func (rs *remoteSampler) refreshManifest(ctx context.Context) { if err := rs.manifest.RefreshManifestRules(ctx); err != nil { - rs.logger.Error(err, "Error occurred while refreshing sampling rules") + rs.logger.Error(err, "error occurred while refreshing sampling rules") } else { - rs.logger.V(5).Info("Successfully fetched sampling rules") + rs.logger.V(5).Info("successfully fetched sampling rules") } } diff --git a/samplers/aws/xray/remote_sampler_config.go b/samplers/aws/xray/remote_sampler_config.go index ca3c5920e1d..9c6eac8dede 100644 --- a/samplers/aws/xray/remote_sampler_config.go +++ b/samplers/aws/xray/remote_sampler_config.go @@ -50,6 +50,7 @@ func (f optionFunc) apply(cfg *config) *config { } // WithEndpoint sets custom proxy endpoint. +// default endpoint being used is http://127.0.0.1:2000. func WithEndpoint(endpoint url.URL) Option { return optionFunc(func(cfg *config) *config { cfg.endpoint = endpoint @@ -58,6 +59,7 @@ func WithEndpoint(endpoint url.URL) Option { } // WithSamplingRulesPollingInterval sets polling interval for sampling rules. +// default samplingRulesPollingInterval being used is 300 seconds. func WithSamplingRulesPollingInterval(polingInterval time.Duration) Option { return optionFunc(func(cfg *config) *config { cfg.samplingRulesPollingInterval = polingInterval @@ -66,6 +68,7 @@ func WithSamplingRulesPollingInterval(polingInterval time.Duration) Option { } // WithLogger sets custom logging for remote sampling implementation. +// default logger being used is go-logr/stdr (https://github.com/go-logr/stdr). func WithLogger(l logr.Logger) Option { return optionFunc(func(cfg *config) *config { cfg.logger = l @@ -118,7 +121,6 @@ func validateConfig(cfg *config) (err error) { return fmt.Errorf("config validation error: host name should not contain special characters or empty") } - // validate polling interval is positive if math.Signbit(float64(cfg.samplingRulesPollingInterval)) { return fmt.Errorf("config validation error: samplingRulesPollingInterval should be positive number") } diff --git a/samplers/aws/xray/timer.go b/samplers/aws/xray/timer.go index 2fdb3e9ac08..59290871937 100644 --- a/samplers/aws/xray/timer.go +++ b/samplers/aws/xray/timer.go @@ -19,14 +19,14 @@ import ( ) // ticker is the same as time.Ticker except that it has jitters. -// A Ticker must be created with NewTicker. +// A Ticker must be created with newTicker. type ticker struct { tick *time.Ticker duration time.Duration jitter time.Duration } -// newTicker creates a new Ticker that will send the current time on its channel. +// newTicker creates a new Ticker that will send the current time on its channel with the passed jitter. func newTicker(duration, jitter time.Duration) *ticker { t := time.NewTicker(duration - time.Duration(newGlobalRand().Int63n(int64(jitter)))) @@ -39,7 +39,7 @@ func newTicker(duration, jitter time.Duration) *ticker { return &jitteredTicker } -// c is channel. +// c returns a channel that receives when the ticker fires. func (j *ticker) c() <-chan time.Time { return j.tick.C } From 1a07f6d5825dec89dced90965dede3493b46c3d2 Mon Sep 17 00:00:00 2001 From: bhautikpip Date: Wed, 23 Mar 2022 21:52:47 -0700 Subject: [PATCH 29/38] remove unnecessary config validation code and minor changes --- samplers/aws/xray/internal/rule.go | 39 +++++++++-- samplers/aws/xray/internal/rule_test.go | 70 +++++++++++++++++++ samplers/aws/xray/remote_sampler.go | 2 + samplers/aws/xray/remote_sampler_config.go | 35 +--------- .../aws/xray/remote_sampler_config_test.go | 19 ++--- 5 files changed, 118 insertions(+), 47 deletions(-) diff --git a/samplers/aws/xray/internal/rule.go b/samplers/aws/xray/internal/rule.go index 4ced79b37fc..033b2b80483 100644 --- a/samplers/aws/xray/internal/rule.go +++ b/samplers/aws/xray/internal/rule.go @@ -149,41 +149,68 @@ func (r *Rule) appliesTo(parameters sdktrace.SamplingParameters, serviceName str if err != nil { return attributeMatcher, err } + + if !attributeMatcher { + return attributeMatcher, nil + } + serviceNameMatcher, err := wildcardMatch(r.ruleProperties.ServiceName, serviceName) if err != nil { return serviceNameMatcher, err } + + if !serviceNameMatcher { + return serviceNameMatcher, nil + } + serviceTypeMatcher, err := wildcardMatch(r.ruleProperties.ServiceType, cloudPlatform) if err != nil { return serviceTypeMatcher, err } + + if !serviceTypeMatcher { + return serviceTypeMatcher, nil + } + HTTPMethodMatcher, err := wildcardMatch(r.ruleProperties.HTTPMethod, httpMethod) if err != nil { return HTTPMethodMatcher, err } + + if !HTTPMethodMatcher { + return HTTPMethodMatcher, nil + } + HTTPHostMatcher, err := wildcardMatch(r.ruleProperties.Host, httpHost) if err != nil { return HTTPHostMatcher, err } + if !HTTPHostMatcher { + return HTTPHostMatcher, nil + } + if httpURL != "" { HTTPURLPathMatcher, err = wildcardMatch(r.ruleProperties.URLPath, httpURL) if err != nil { return HTTPURLPathMatcher, err } + + if !HTTPURLPathMatcher { + return HTTPURLPathMatcher, nil + } } else { HTTPURLPathMatcher, err = wildcardMatch(r.ruleProperties.URLPath, httpTarget) if err != nil { return HTTPURLPathMatcher, err } + + if !HTTPURLPathMatcher { + return HTTPURLPathMatcher, nil + } } - return attributeMatcher && - serviceNameMatcher && - serviceTypeMatcher && - HTTPMethodMatcher && - HTTPHostMatcher && - HTTPURLPathMatcher, nil + return true, nil } // attributeMatching performs a match on attributes set by users on AWS X-Ray console. diff --git a/samplers/aws/xray/internal/rule_test.go b/samplers/aws/xray/internal/rule_test.go index 5cf23ca549e..f3aca97db78 100644 --- a/samplers/aws/xray/internal/rule_test.go +++ b/samplers/aws/xray/internal/rule_test.go @@ -373,6 +373,76 @@ func TestAppliesToHTTPTargetMatching(t *testing.T) { assert.True(t, match) } +// assert early exit when attribute matcher is false +func TestAppliesToExitEarlyNoAttributesMatch(t *testing.T) { + commonLabels := []attribute.KeyValue{ + attribute.String("http.target", "target"), + } + + r1 := Rule{ + ruleProperties: ruleProperties{ + RuleName: "r1", + ServiceName: "test-service", + ServiceType: "ECS", + Host: "*", + HTTPMethod: "*", + URLPath: "*", + Attributes: map[string]string{ + "labelA": "chocolate", + "labelC": "fudge", + }, + }, + } + + match, err := r1.appliesTo(trace.SamplingParameters{Attributes: commonLabels}, "test-service", "ECS") + require.NoError(t, err) + assert.False(t, match) +} + +// assert early exit when service name matcher is false +func TestAppliesToExitEarlyNoServiceNameMatch(t *testing.T) { + r1 := Rule{ + ruleProperties: ruleProperties{ + RuleName: "r1", + ServiceName: "test-service", + ServiceType: "ECS", + Host: "*", + HTTPMethod: "*", + URLPath: "*", + Attributes: map[string]string{ + "labelA": "chocolate", + "labelC": "fudge", + }, + }, + } + + match, err := r1.appliesTo(trace.SamplingParameters{}, "test-service11", "ECS") + require.NoError(t, err) + assert.False(t, match) +} + +// assert early exit when service type matcher is false +func TestAppliesToExitEarlyNoServiceTypeMatch(t *testing.T) { + r1 := Rule{ + ruleProperties: ruleProperties{ + RuleName: "r1", + ServiceName: "test-service", + ServiceType: "EC2", + Host: "*", + HTTPMethod: "*", + URLPath: "*", + Attributes: map[string]string{ + "labelA": "chocolate", + "labelC": "fudge", + }, + }, + } + + match, err := r1.appliesTo(trace.SamplingParameters{}, "test-service", "ECS") + require.NoError(t, err) + assert.False(t, match) +} + // assert that if rules has attribute and span has those attribute with same value then matching will happen. func TestAttributeMatching(t *testing.T) { commonLabels := []attribute.KeyValue{ diff --git a/samplers/aws/xray/remote_sampler.go b/samplers/aws/xray/remote_sampler.go index 13f021e3680..644cd1f06ba 100644 --- a/samplers/aws/xray/remote_sampler.go +++ b/samplers/aws/xray/remote_sampler.go @@ -52,6 +52,8 @@ var _ sdktrace.Sampler = (*remoteSampler)(nil) // NewRemoteSampler returns a sampler which decides to sample a given request or not // based on the sampling rules set by users on AWS X-Ray console. Sampler also periodically polls // sampling rules and sampling targets. +// NOTE: ctx passed in NewRemoteSampler API is being used in background go routine. Cancellation to this context can kill the background go routine. +// Guide on AWS X-Ray remote sampling implementation (https://aws-otel.github.io/docs/getting-started/remote-sampling#otel-remote-sampling-implementation-caveats) func NewRemoteSampler(ctx context.Context, serviceName string, cloudPlatform string, opts ...Option) (sdktrace.Sampler, error) { // Create new config based on options or set to default values. cfg, err := newConfig(opts...) diff --git a/samplers/aws/xray/remote_sampler_config.go b/samplers/aws/xray/remote_sampler_config.go index 9c6eac8dede..fa747661cc2 100644 --- a/samplers/aws/xray/remote_sampler_config.go +++ b/samplers/aws/xray/remote_sampler_config.go @@ -20,8 +20,6 @@ import ( "math" "net/url" "os" - "regexp" - "strings" "time" "github.com/go-logr/logr" @@ -92,38 +90,9 @@ func newConfig(opts ...Option) (*config, error) { option.apply(cfg) } - // validate config - err = validateConfig(cfg) - if err != nil { - return nil, err - } - - return cfg, nil -} - -func validateConfig(cfg *config) (err error) { - // check endpoint follows certain format - endpointHostSplit := strings.Split(cfg.endpoint.Host, ":") - - if len(endpointHostSplit) > 2 { - return fmt.Errorf("config validation error: expected endpoint host format is hostname:port") - } - - hostName := endpointHostSplit[0] - - // validate host name - r, err := regexp.Compile("[^A-Za-z0-9.]") - if err != nil { - return err - } - - if r.MatchString(hostName) || hostName == "" { - return fmt.Errorf("config validation error: host name should not contain special characters or empty") - } - if math.Signbit(float64(cfg.samplingRulesPollingInterval)) { - return fmt.Errorf("config validation error: samplingRulesPollingInterval should be positive number") + return nil, fmt.Errorf("config validation error: samplingRulesPollingInterval should be positive number") } - return + return cfg, nil } diff --git a/samplers/aws/xray/remote_sampler_config_test.go b/samplers/aws/xray/remote_sampler_config_test.go index 358eab7a6db..1c636806566 100644 --- a/samplers/aws/xray/remote_sampler_config_test.go +++ b/samplers/aws/xray/remote_sampler_config_test.go @@ -75,26 +75,29 @@ func TestValidEndpoint(t *testing.T) { cfg, err := newConfig(WithEndpoint(*endpoint)) require.NoError(t, err) - err = validateConfig(cfg) - assert.NoError(t, err) + assert.Equal(t, cfg.endpoint, *endpoint) } -// assert that host name with special character would result in an error. +// assert that host name with special character would not result in an error. func TestValidateHostNameWithSpecialCharacterEndpoint(t *testing.T) { endpoint, err := url.Parse("http://127.0.0.1@:2000") require.NoError(t, err) - _, err = newConfig(WithEndpoint(*endpoint)) - assert.Error(t, err) + cfg, err := newConfig(WithEndpoint(*endpoint)) + require.NoError(t, err) + + assert.Equal(t, cfg.endpoint, *endpoint) } -// assert that endpoint without host name would result in an error. +// assert that endpoint without host name would not result in an error. func TestValidateInvalidEndpoint(t *testing.T) { endpoint, err := url.Parse("https://") require.NoError(t, err) - _, err = newConfig(WithEndpoint(*endpoint)) - assert.Error(t, err) + cfg, err := newConfig(WithEndpoint(*endpoint)) + require.NoError(t, err) + + assert.Equal(t, cfg.endpoint, *endpoint) } // assert negative sampling rules interval leads to an error. From 50c1f3dab29564907af0252a934394797a49b4f5 Mon Sep 17 00:00:00 2001 From: bhautikpip Date: Thu, 24 Mar 2022 20:21:17 -0700 Subject: [PATCH 30/38] aaded race test and review comments changes --- samplers/aws/xray/go.mod | 8 +- samplers/aws/xray/go.sum | 16 +- samplers/aws/xray/internal/client_test.go | 60 +- samplers/aws/xray/internal/manifest.go | 3 +- samplers/aws/xray/internal/manifest_test.go | 567 +++++++++++-------- samplers/aws/xray/internal/reservoir_test.go | 2 +- samplers/aws/xray/internal/rule.go | 12 +- samplers/aws/xray/internal/rule_test.go | 211 ++++++- samplers/aws/xray/remote_sampler.go | 2 +- 9 files changed, 540 insertions(+), 341 deletions(-) diff --git a/samplers/aws/xray/go.mod b/samplers/aws/xray/go.mod index 918da3cb3c9..49291646857 100644 --- a/samplers/aws/xray/go.mod +++ b/samplers/aws/xray/go.mod @@ -6,9 +6,9 @@ require ( github.com/go-logr/logr v1.2.3 github.com/go-logr/stdr v1.2.2 github.com/jinzhu/copier v0.3.5 - github.com/stretchr/testify v1.7.0 - go.opentelemetry.io/otel v1.4.0 - go.opentelemetry.io/otel/sdk v1.4.0 - go.opentelemetry.io/otel/trace v1.4.0 + github.com/stretchr/testify v1.7.1 + go.opentelemetry.io/otel v1.6.0 + go.opentelemetry.io/otel/sdk v1.6.0 + go.opentelemetry.io/otel/trace v1.6.0 golang.org/x/sys v0.0.0-20210510120138-977fb7262007 // indirect ) diff --git a/samplers/aws/xray/go.sum b/samplers/aws/xray/go.sum index df4dc141237..f71c9c92645 100644 --- a/samplers/aws/xray/go.sum +++ b/samplers/aws/xray/go.sum @@ -12,14 +12,14 @@ github.com/jinzhu/copier v0.3.5/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -go.opentelemetry.io/otel v1.4.0 h1:7ESuKPq6zpjRaY5nvVDGiuwK7VAJ8MwkKnmNJ9whNZ4= -go.opentelemetry.io/otel v1.4.0/go.mod h1:jeAqMFKy2uLIxCtKxoFj0FAL5zAPKQagc3+GtBWakzk= -go.opentelemetry.io/otel/sdk v1.4.0 h1:LJE4SW3jd4lQTESnlpQZcBhQ3oci0U2MLR5uhicfTHQ= -go.opentelemetry.io/otel/sdk v1.4.0/go.mod h1:71GJPNJh4Qju6zJuYl1CrYtXbrgfau/M9UAggqiy1UE= -go.opentelemetry.io/otel/trace v1.4.0 h1:4OOUrPZdVFQkbzl/JSdvGCWIdw5ONXXxzHlaLlWppmo= -go.opentelemetry.io/otel/trace v1.4.0/go.mod h1:uc3eRsqDfWs9R7b92xbQbU42/eTNz4N+gLP8qJCi4aE= +github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +go.opentelemetry.io/otel v1.6.0 h1:YV6GkGe/Ag2PKsm4rjlqdSNs0w0A5ZzxeGkxhx1T+t4= +go.opentelemetry.io/otel v1.6.0/go.mod h1:bfJD2DZVw0LBxghOTlgnlI0CV3hLDu9XF/QKOUXMTQQ= +go.opentelemetry.io/otel/sdk v1.6.0 h1:JoriAoiNENuxxIQApR1O0k2h1Md5QegZhbentcRJpWk= +go.opentelemetry.io/otel/sdk v1.6.0/go.mod h1:PjLRUfDsoPy0zl7yrDGSUqjj43tL7rEtFdCEiGlxXRM= +go.opentelemetry.io/otel/trace v1.6.0 h1:NDzPermp9ISkhxIaJXjBTi2O60xOSHDHP/EezjOL2wo= +go.opentelemetry.io/otel/trace v1.6.0/go.mod h1:qs7BrU5cZ8dXQHBGxHMOxwME/27YH2qEp4/+tZLLwJE= golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007 h1:gG67DSER+11cZvqIMb8S8bt0vZtiN6xWYARwirrOSfE= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/samplers/aws/xray/internal/client_test.go b/samplers/aws/xray/internal/client_test.go index 060f46e653f..08a18752db4 100644 --- a/samplers/aws/xray/internal/client_test.go +++ b/samplers/aws/xray/internal/client_test.go @@ -25,18 +25,20 @@ import ( "github.com/stretchr/testify/require" ) -func createTestClient(testServerURL string) (*xrayClient, error) { - u, err := url.Parse(testServerURL) - if err != nil { - return nil, err - } +func createTestClient(t *testing.T, body []byte) *xrayClient { + testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, _ *http.Request) { + _, err := res.Write(body) + require.NoError(t, err) + })) + t.Cleanup(testServer.Close) + + u, err := url.Parse(testServer.URL) + require.NoError(t, err) client, err := newClient(*u) - if err != nil { - return nil, err - } + require.NoError(t, err) - return client, nil + return client } func TestGetSamplingRules(t *testing.T) { @@ -104,15 +106,7 @@ func TestGetSamplingRules(t *testing.T) { }`) ctx := context.Background() - // generate a test server so we can capture and inspect the request - testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { - _, err := res.Write(body) - require.NoError(t, err) - })) - t.Cleanup(testServer.Close) - - client, err := createTestClient(testServer.URL) - require.NoError(t, err) + client := createTestClient(t, body) samplingRules, err := client.getSamplingRules(ctx) require.NoError(t, err) @@ -164,15 +158,7 @@ func TestGetSamplingRulesWithMissingValues(t *testing.T) { }`) ctx := context.Background() - // generate a test server so we can capture and inspect the request - testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { - _, err := res.Write([]byte(body)) - require.NoError(t, err) - })) - t.Cleanup(testServer.Close) - - client, err := createTestClient(testServer.URL) - require.NoError(t, err) + client := createTestClient(t, body) samplingRules, err := client.getSamplingRules(ctx) require.NoError(t, err) @@ -208,15 +194,7 @@ func TestGetSamplingTargets(t *testing.T) { ctx := context.Background() - // generate a test server so we can capture and inspect the request - testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { - _, err := res.Write([]byte(body)) - require.NoError(t, err) - })) - t.Cleanup(testServer.Close) - - client, err := createTestClient(testServer.URL) - require.NoError(t, err) + client := createTestClient(t, body) samplingTragets, err := client.getSamplingTargets(ctx, nil) require.NoError(t, err) @@ -253,15 +231,7 @@ func TestGetSamplingTargetsMissingValues(t *testing.T) { ctx := context.Background() - // generate a test server so we can capture and inspect the request - testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { - _, err := res.Write([]byte(body)) - require.NoError(t, err) - })) - t.Cleanup(testServer.Close) - - client, err := createTestClient(testServer.URL) - require.NoError(t, err) + client := createTestClient(t, body) samplingTragets, err := client.getSamplingTargets(ctx, nil) require.NoError(t, err) diff --git a/samplers/aws/xray/internal/manifest.go b/samplers/aws/xray/internal/manifest.go index e80f1a7378b..a040ab5aa81 100644 --- a/samplers/aws/xray/internal/manifest.go +++ b/samplers/aws/xray/internal/manifest.go @@ -121,7 +121,6 @@ func (m *Manifest) RefreshManifestRules(ctx context.Context) (err error) { func (m *Manifest) RefreshManifestTargets(ctx context.Context) (refresh bool, err error) { var manifest Manifest - // deep copy centralized manifest object to temporary manifest to avoid thread safety issue err = func() error { m.mu.RLock() defer m.mu.RUnlock() @@ -316,7 +315,7 @@ func (m *Manifest) snapshots() ([]*samplingStatisticsDocument, error) { return statistics, nil } -// sort sorts the Rules of m array first by priority and then by name. +// sort sorts the Rules of m first by priority and then by name. func (m *Manifest) sort() { less := func(i, j int) bool { if m.Rules[i].ruleProperties.Priority == m.Rules[j].ruleProperties.Priority { diff --git a/samplers/aws/xray/internal/manifest_test.go b/samplers/aws/xray/internal/manifest_test.go index 967073b9092..2188b60436e 100644 --- a/samplers/aws/xray/internal/manifest_test.go +++ b/samplers/aws/xray/internal/manifest_test.go @@ -16,13 +16,13 @@ package internal import ( "context" - "net/http" - "net/http/httptest" "net/url" "sync" "testing" "time" + "github.com/jinzhu/copier" + "go.opentelemetry.io/otel/attribute" "github.com/go-logr/logr/testr" @@ -33,6 +33,16 @@ import ( "github.com/stretchr/testify/assert" ) +func createSamplingTargetDocument(name string, interval int64, rate, quota, ttl float64) *samplingTargetDocument { + return &samplingTargetDocument{ + FixedRate: &rate, + Interval: &interval, + ReservoirQuota: "a, + ReservoirQuotaTTL: &ttl, + RuleName: &name, + } +} + // assert that new manifest has certain non-nil attributes. func TestNewManifest(t *testing.T) { logger := testr.New(t) @@ -56,10 +66,9 @@ func TestExpiredManifest(t *testing.T) { nowTime: 10000, } - refreshedAt := time.Unix(3700, 0) m := &Manifest{ clock: clock, - refreshedAt: refreshedAt, + refreshedAt: time.Unix(3700, 0), } assert.True(t, m.Expired()) @@ -122,10 +131,8 @@ func TestMatchAgainstManifestRules(t *testing.T) { }, } - rules := []Rule{r1, r2} - m := &Manifest{ - Rules: rules, + Rules: []Rule{r1, r2}, } exp, match, err := m.MatchAgainstManifestRules(sdktrace.SamplingParameters{}, "test", "local") @@ -166,10 +173,8 @@ func TestMatchAgainstManifestRulesAttributeMatch(t *testing.T) { }, } - rules := []Rule{r1} - m := &Manifest{ - Rules: rules, + Rules: []Rule{r1}, } exp, match, err := m.MatchAgainstManifestRules(sdktrace.SamplingParameters{Attributes: commonLabels}, "test", "local") @@ -210,10 +215,8 @@ func TestMatchAgainstManifestRulesAttributeWildCardMatch(t *testing.T) { }, } - rules := []Rule{r1} - m := &Manifest{ - Rules: rules, + Rules: []Rule{r1}, } exp, match, err := m.MatchAgainstManifestRules(sdktrace.SamplingParameters{Attributes: commonLabels}, "test", "local") @@ -247,10 +250,8 @@ func TestMatchAgainstManifestRulesNoMatch(t *testing.T) { }, } - rules := []Rule{r1} - m := &Manifest{ - Rules: rules, + Rules: []Rule{r1}, } rule, isMatch, err := m.MatchAgainstManifestRules(sdktrace.SamplingParameters{}, "test", "local") @@ -327,23 +328,13 @@ func TestRefreshManifestRules(t *testing.T) { ] }`) - // generate a test server so we can capture and inspect the request - testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { - _, err := res.Write(body) - require.NoError(t, err) - })) - t.Cleanup(testServer.Close) - - client, err := createTestClient(testServer.URL) - require.NoError(t, err) - m := &Manifest{ Rules: []Rule{}, - xrayClient: client, + xrayClient: createTestClient(t, body), clock: &defaultClock{}, } - err = m.RefreshManifestRules(ctx) + err := m.RefreshManifestRules(ctx) require.NoError(t, err) r1 := Rule{ @@ -449,28 +440,17 @@ func TestRefreshManifestMissingServiceName(t *testing.T) { ] }`) - // generate a test server so we can capture and inspect the request - testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { - _, err := res.Write(body) - require.NoError(t, err) - - })) - t.Cleanup(testServer.Close) - - client, err := createTestClient(testServer.URL) - require.NoError(t, err) - m := &Manifest{ Rules: []Rule{}, - xrayClient: client, + xrayClient: createTestClient(t, body), clock: &defaultClock{}, } - err = m.RefreshManifestRules(ctx) + err := m.RefreshManifestRules(ctx) require.NoError(t, err) // assert on rule gets added - assert.Len(t, m.Rules, 1) + require.Len(t, m.Rules, 1) } // assert that rule with no RuleName does not update to the manifest. @@ -501,29 +481,18 @@ func TestRefreshManifestMissingRuleName(t *testing.T) { ] }`) - // generate a test server so we can capture and inspect the request - testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { - _, err := res.Write(body) - require.NoError(t, err) - - })) - t.Cleanup(testServer.Close) - - client, err := createTestClient(testServer.URL) - require.NoError(t, err) - m := &Manifest{ Rules: []Rule{}, - xrayClient: client, + xrayClient: createTestClient(t, body), clock: &defaultClock{}, logger: testr.New(t), } - err = m.RefreshManifestRules(ctx) + err := m.RefreshManifestRules(ctx) require.NoError(t, err) // assert on rule not added - assert.Len(t, m.Rules, 0) + require.Len(t, m.Rules, 0) } // assert that rule with version greater than one does not update to the manifest. @@ -556,29 +525,18 @@ func TestRefreshManifestIncorrectVersion(t *testing.T) { ] }`) - // generate a test server so we can capture and inspect the request - testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { - _, err := res.Write(body) - require.NoError(t, err) - - })) - t.Cleanup(testServer.Close) - - client, err := createTestClient(testServer.URL) - require.NoError(t, err) - m := &Manifest{ Rules: []Rule{}, - xrayClient: client, + xrayClient: createTestClient(t, body), clock: &defaultClock{}, logger: testr.New(t), } - err = m.RefreshManifestRules(ctx) + err := m.RefreshManifestRules(ctx) require.NoError(t, err) // assert on rule not added - assert.Len(t, m.Rules, 0) + require.Len(t, m.Rules, 0) } // assert that 1 valid and 1 invalid rule update only valid rule gets stored to the manifest. @@ -651,30 +609,17 @@ func TestRefreshManifestAddOneInvalidRule(t *testing.T) { samplingStatistics: &samplingStatistics{}, } - // generate a test server so we can capture and inspect the request - testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { - _, err := res.Write(body) - require.NoError(t, err) - })) - defer testServer.Close() - - u, err := url.Parse(testServer.URL) - require.NoError(t, err) - - client, err := newClient(*u) - require.NoError(t, err) - m := &Manifest{ Rules: []Rule{}, - xrayClient: client, + xrayClient: createTestClient(t, body), clock: &defaultClock{}, logger: testr.New(t), } - err = m.RefreshManifestRules(ctx) + err := m.RefreshManifestRules(ctx) require.NoError(t, err) - assert.Len(t, m.Rules, 1) + require.Len(t, m.Rules, 1) // assert on r1 assert.Equal(t, r1, m.Rules[0]) @@ -709,10 +654,8 @@ func TestRefreshManifestTargetNoSnapShot(t *testing.T) { }, } - rules := []Rule{r1} - m := &Manifest{ - Rules: rules, + Rules: []Rule{r1}, clock: clock, logger: testr.New(t), } @@ -773,25 +716,12 @@ func TestRefreshManifestTargets(t *testing.T) { }, } - rules := []Rule{r1} - - // generate a test server so we can capture and inspect the request - testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { - _, err := res.Write(body) - require.NoError(t, err) - })) - t.Cleanup(testServer.Close) - - client, err := createTestClient(testServer.URL) - require.NoError(t, err) - - refreshedAt := time.Unix(18000000, 0) m := &Manifest{ - Rules: rules, + Rules: []Rule{r1}, clock: clock, logger: testr.New(t), - xrayClient: client, - refreshedAt: refreshedAt, + xrayClient: createTestClient(t, body), + refreshedAt: time.Unix(18000000, 0), } refresh, err := m.RefreshManifestTargets(context.Background()) @@ -878,29 +808,15 @@ func TestRefreshManifestTargetsPollIntervalUpdateTest(t *testing.T) { samplingStatistics: &samplingStatistics{}, } - rules := []Rule{r1, r2, r3} - - // generate a test server so we can capture and inspect the request - testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { - _, err := res.Write(body) - require.NoError(t, err) - })) - t.Cleanup(testServer.Close) - - client, err := createTestClient(testServer.URL) - require.NoError(t, err) - - refreshedAt := time.Unix(18000000, 0) - m := &Manifest{ - Rules: rules, + Rules: []Rule{r1, r2, r3}, clock: clock, logger: testr.New(t), - xrayClient: client, - refreshedAt: refreshedAt, + xrayClient: createTestClient(t, body), + refreshedAt: time.Unix(18000000, 0), } - _, err = m.RefreshManifestTargets(context.Background()) + _, err := m.RefreshManifestTargets(context.Background()) require.NoError(t, err) // assert that sampling rules polling interval is minimum of all target intervals min(15, 5, 25) @@ -909,28 +825,10 @@ func TestRefreshManifestTargetsPollIntervalUpdateTest(t *testing.T) { // assert that a valid sampling target updates its rule. func TestUpdateTargets(t *testing.T) { - clock := &mockClock{ - nowTime: 1500000000, - } - - // sampling target received from centralized sampling backend - rate := 0.05 - quota := float64(10) - ttl := float64(1500000060) - name := "r1" - - st := samplingTargetDocument{ - FixedRate: &rate, - ReservoirQuota: "a, - ReservoirQuotaTTL: &ttl, - RuleName: &name, - } - targets := &getSamplingTargetsOutput{ - SamplingTargetDocuments: []*samplingTargetDocument{&st}, + SamplingTargetDocuments: []*samplingTargetDocument{createSamplingTargetDocument("r1", 0, 0.05, 10, 1500000060)}, } - refreshedAt1 := time.Unix(1499999990, 0) // sampling rule about to be updated with new target r1 := Rule{ ruleProperties: ruleProperties{ @@ -939,16 +837,18 @@ func TestUpdateTargets(t *testing.T) { }, reservoir: reservoir{ quota: 8, - refreshedAt: refreshedAt1, + refreshedAt: time.Unix(1499999990, 0), expiresAt: time.Unix(1500000010, 0), capacity: 50, }, } - rules := []Rule{r1} + clock := &mockClock{ + nowTime: 1500000000, + } m := &Manifest{ - Rules: rules, + Rules: []Rule{r1}, clock: clock, } @@ -958,7 +858,6 @@ func TestUpdateTargets(t *testing.T) { // assert refresh is false assert.False(t, refresh) - refreshedAt2 := time.Unix(1500000000, 0) // Updated sampling rule exp := Rule{ ruleProperties: ruleProperties{ @@ -967,7 +866,7 @@ func TestUpdateTargets(t *testing.T) { }, reservoir: reservoir{ quota: 10, - refreshedAt: refreshedAt2, + refreshedAt: time.Unix(1500000000, 0), expiresAt: time.Unix(1500000060, 0), capacity: 50, }, @@ -980,30 +879,12 @@ func TestUpdateTargets(t *testing.T) { // assert that when last rule modification time is greater than manifest refresh time we need to update manifest // out of band (async). func TestUpdateTargetsRefreshFlagTest(t *testing.T) { - clock := &mockClock{ - nowTime: 1500000000, - } - - // sampling target received from centralized sampling backend - rate := 0.05 - quota := float64(10) - ttl := float64(1500000060) - name := "r1" - - st := samplingTargetDocument{ - FixedRate: &rate, - ReservoirQuota: "a, - ReservoirQuotaTTL: &ttl, - RuleName: &name, - } - targetLastRuleModifiedTime := float64(1500000020) targets := &getSamplingTargetsOutput{ - SamplingTargetDocuments: []*samplingTargetDocument{&st}, + SamplingTargetDocuments: []*samplingTargetDocument{createSamplingTargetDocument("r1", 0, 0.05, 10, 1500000060)}, LastRuleModification: &targetLastRuleModifiedTime, } - refreshedAt1 := time.Unix(1499999990, 0) // sampling rule about to be updated with new target r1 := Rule{ ruleProperties: ruleProperties{ @@ -1012,16 +893,18 @@ func TestUpdateTargetsRefreshFlagTest(t *testing.T) { }, reservoir: reservoir{ quota: 8, - refreshedAt: refreshedAt1, + refreshedAt: time.Unix(1499999990, 0), expiresAt: time.Unix(1500000010, 0), capacity: 50, }, } - rules := []Rule{r1} + clock := &mockClock{ + nowTime: 1500000000, + } m := &Manifest{ - Rules: rules, + Rules: []Rule{r1}, refreshedAt: clock.now(), clock: clock, } @@ -1032,7 +915,6 @@ func TestUpdateTargetsRefreshFlagTest(t *testing.T) { // assert refresh is false assert.True(t, refresh) - refreshedAt2 := time.Unix(1500000000, 0) // Updated sampling rule exp := Rule{ ruleProperties: ruleProperties{ @@ -1041,7 +923,7 @@ func TestUpdateTargetsRefreshFlagTest(t *testing.T) { }, reservoir: reservoir{ quota: 10, - refreshedAt: refreshedAt2, + refreshedAt: time.Unix(1500000000, 0), expiresAt: time.Unix(1500000060, 0), capacity: 50, }, @@ -1053,35 +935,23 @@ func TestUpdateTargetsRefreshFlagTest(t *testing.T) { // unprocessed statistics error code is 5xx then updateTargets returns an error, if 4xx refresh flag set to true. func TestUpdateTargetsUnprocessedStatistics(t *testing.T) { - clock := &mockClock{ - nowTime: 1500000000, - } - - // sampling target received from centralized sampling backend - rate := 0.05 - quota := float64(10) - ttl := float64(1500000060) - name := "r1" - - st := samplingTargetDocument{ - FixedRate: &rate, - ReservoirQuota: "a, - ReservoirQuotaTTL: &ttl, - RuleName: &name, - } - // case for 5xx + ruleName := "r1" errorCode500 := "500" unprocessedStats5xx := unprocessedStatistic{ ErrorCode: &errorCode500, - RuleName: &name, + RuleName: &ruleName, } targets5xx := &getSamplingTargetsOutput{ - SamplingTargetDocuments: []*samplingTargetDocument{&st}, + SamplingTargetDocuments: []*samplingTargetDocument{createSamplingTargetDocument(ruleName, 0, 0.05, 10, 1500000060)}, UnprocessedStatistics: []*unprocessedStatistic{&unprocessedStats5xx}, } + clock := &mockClock{ + nowTime: 1500000000, + } + m := &Manifest{ clock: clock, logger: testr.New(t), @@ -1098,11 +968,11 @@ func TestUpdateTargetsUnprocessedStatistics(t *testing.T) { errorCode400 := "400" unprocessedStats4xx := unprocessedStatistic{ ErrorCode: &errorCode400, - RuleName: &name, + RuleName: &ruleName, } targets4xx := &getSamplingTargetsOutput{ - SamplingTargetDocuments: []*samplingTargetDocument{&st}, + SamplingTargetDocuments: []*samplingTargetDocument{createSamplingTargetDocument(ruleName, 0, 0.05, 10, 1500000060)}, UnprocessedStatistics: []*unprocessedStatistic{&unprocessedStats4xx}, } @@ -1120,7 +990,7 @@ func TestUpdateTargetsUnprocessedStatistics(t *testing.T) { } targets := &getSamplingTargetsOutput{ - SamplingTargetDocuments: []*samplingTargetDocument{&st}, + SamplingTargetDocuments: []*samplingTargetDocument{createSamplingTargetDocument(ruleName, 0, 0.05, 10, 1500000060)}, UnprocessedStatistics: []*unprocessedStatistic{&unprocessedStats}, } @@ -1139,19 +1009,6 @@ func TestUpdateTargetsUnprocessedStatistics(t *testing.T) { // assert that a missing sampling rule in manifest does not update it's reservoir values. func TestUpdateReservoir(t *testing.T) { - // Sampling target received from centralized sampling backend - rate := 0.05 - quota := float64(10) - ttl := float64(1500000060) - name := "r1" - st := &samplingTargetDocument{ - FixedRate: &rate, - ReservoirQuota: "a, - ReservoirQuotaTTL: &ttl, - RuleName: &name, - } - - refreshedAt1 := time.Unix(1499999990, 0) // manifest only has rule r2 but not rule with r1 which targets just received r1 := Rule{ ruleProperties: ruleProperties{ @@ -1160,19 +1017,17 @@ func TestUpdateReservoir(t *testing.T) { }, reservoir: reservoir{ quota: 8, - refreshedAt: refreshedAt1, + refreshedAt: time.Unix(1499999990, 0), expiresAt: time.Unix(1500000010, 0), capacity: 50, }, } - rules := []Rule{r1} - m := &Manifest{ - Rules: rules, + Rules: []Rule{r1}, } - err := m.updateReservoir(st) + err := m.updateReservoir(createSamplingTargetDocument("r1", 0, 0.05, 10, 1500000060)) require.NoError(t, err) // assert that rule reservoir value does not get updated and still same as r1 @@ -1181,17 +1036,6 @@ func TestUpdateReservoir(t *testing.T) { // assert that a sampling target with missing Fixed Rate returns an error. func TestUpdateReservoirMissingFixedRate(t *testing.T) { - // Sampling target received from centralized sampling backend - quota := float64(10) - ttl := float64(1500000060) - name := "r1" - st := &samplingTargetDocument{ - ReservoirQuota: "a, - ReservoirQuotaTTL: &ttl, - RuleName: &name, - } - - refreshedAt1 := time.Unix(1499999990, 0) // manifest rule which we're trying to update with above target st r1 := Rule{ ruleProperties: ruleProperties{ @@ -1200,35 +1044,24 @@ func TestUpdateReservoirMissingFixedRate(t *testing.T) { }, reservoir: reservoir{ quota: 8, - refreshedAt: refreshedAt1, + refreshedAt: time.Unix(1499999990, 0), expiresAt: time.Unix(1500000010, 0), capacity: 50, }, } - rules := []Rule{r1} - m := &Manifest{ - Rules: rules, + Rules: []Rule{r1}, } + st := createSamplingTargetDocument("r1", 0, 0, 10, 1500000060) + st.FixedRate = nil err := m.updateReservoir(st) require.Error(t, err) } // assert that a sampling target with missing Rule Name returns an error. func TestUpdateReservoirMissingRuleName(t *testing.T) { - // Sampling target received from centralized sampling backend - rate := 0.05 - quota := float64(10) - ttl := float64(1500000060) - st := &samplingTargetDocument{ - ReservoirQuota: "a, - ReservoirQuotaTTL: &ttl, - FixedRate: &rate, - } - - refreshedAt1 := time.Unix(1499999990, 0) // manifest rule which we're trying to update with above target st r1 := Rule{ ruleProperties: ruleProperties{ @@ -1237,18 +1070,18 @@ func TestUpdateReservoirMissingRuleName(t *testing.T) { }, reservoir: reservoir{ quota: 8, - refreshedAt: refreshedAt1, + refreshedAt: time.Unix(1499999990, 0), expiresAt: time.Unix(1500000010, 0), capacity: 50, }, } - rules := []Rule{r1} - m := &Manifest{ - Rules: rules, + Rules: []Rule{r1}, } + st := createSamplingTargetDocument("r1", 0, 0, 10, 1500000060) + st.RuleName = nil err := m.updateReservoir(st) require.Error(t, err) } @@ -1297,11 +1130,9 @@ func TestSnapshots(t *testing.T) { }, } - rules := []Rule{r1, r2} - id := "c1" m := &Manifest{ - Rules: rules, + Rules: []Rule{r1, r2}, clientID: &id, clock: clock, } @@ -1351,14 +1182,13 @@ func TestMixedSnapshots(t *testing.T) { sampled1 := int64(100) borrowed1 := int64(5) - refreshedAt1 := time.Unix(1499999970, 0) r1 := Rule{ ruleProperties: ruleProperties{ RuleName: name1, }, reservoir: reservoir{ interval: 20, - refreshedAt: refreshedAt1, + refreshedAt: time.Unix(1499999970, 0), }, samplingStatistics: &samplingStatistics{ matchedRequests: requests1, @@ -1367,7 +1197,6 @@ func TestMixedSnapshots(t *testing.T) { }, } - refreshedAt2 := time.Unix(1499999990, 0) // fresh and inactive rule name2 := "r2" requests2 := int64(0) @@ -1380,7 +1209,7 @@ func TestMixedSnapshots(t *testing.T) { }, reservoir: reservoir{ interval: 20, - refreshedAt: refreshedAt2, + refreshedAt: time.Unix(1499999990, 0), }, samplingStatistics: &samplingStatistics{ matchedRequests: requests2, @@ -1389,7 +1218,6 @@ func TestMixedSnapshots(t *testing.T) { }, } - refreshedAt3 := time.Unix(1499999990, 0) // fresh rule name3 := "r3" requests3 := int64(1000) @@ -1402,7 +1230,7 @@ func TestMixedSnapshots(t *testing.T) { }, reservoir: reservoir{ interval: 20, - refreshedAt: refreshedAt3, + refreshedAt: time.Unix(1499999990, 0), }, samplingStatistics: &samplingStatistics{ matchedRequests: requests3, @@ -1572,3 +1400,242 @@ func TestGenerateClientID(t *testing.T) { require.NoError(t, err) assert.NotEmpty(t, clientID) } + +// validate no data race is happening when updating rule properties in manifest while matching +func TestRaceUpdatingRulesWhileMatching(t *testing.T) { + // getSamplingRules response + ruleRecords := samplingRuleRecords{ + SamplingRule: &ruleProperties{ + RuleName: "r1", + Priority: 10000, + Host: "localhost", + HTTPMethod: "*", + URLPath: "/test/path", + ReservoirSize: 40, + FixedRate: 0.9, + Version: 1, + ServiceName: "helios", + ResourceARN: "*", + ServiceType: "*", + }, + } + + s := &getSamplingRulesOutput{ + SamplingRuleRecords: []*samplingRuleRecords{&ruleRecords}, + } + + // existing rule in manifest + r1 := Rule{ + ruleProperties: ruleProperties{ + RuleName: "r1", + Priority: 10000, + Host: "*", + HTTPMethod: "*", + URLPath: "*", + ReservoirSize: 60, + FixedRate: 0.5, + Version: 1, + ServiceName: "test", + ResourceARN: "*", + ServiceType: "*", + }, + reservoir: reservoir{ + expiresAt: time.Unix(14050, 0), + }, + } + + rules := []Rule{r1} + + clock := &mockClock{ + nowTime: 1500000000, + } + + m := &Manifest{ + Rules: rules, + clock: clock, + } + + // async rule updates + go func() { + for i := 0; i < 100; i++ { + m.updateRules(s) + time.Sleep(time.Millisecond) + } + }() + + // matching logic + for i := 0; i < 100; i++ { + _, match, err := m.MatchAgainstManifestRules(sdktrace.SamplingParameters{}, "helios", "macos") + require.NoError(t, err) + require.False(t, match) + } +} + +// validate no data race is happening when updating rule properties and rule targets in manifest while matching +func TestRaceUpdatingRulesAndTargetsWhileMatching(t *testing.T) { + // getSamplingRules response to update existing manifest rule + ruleRecords := samplingRuleRecords{ + SamplingRule: &ruleProperties{ + RuleName: "r1", + Priority: 10000, + Host: "localhost", + HTTPMethod: "*", + URLPath: "/test/path", + ReservoirSize: 40, + FixedRate: 0.9, + Version: 1, + ServiceName: "helios", + ResourceARN: "*", + ServiceType: "*", + }, + } + + // existing rule already present in manifest + r1 := Rule{ + ruleProperties: ruleProperties{ + RuleName: "r1", + Priority: 10000, + Host: "*", + HTTPMethod: "*", + URLPath: "*", + ReservoirSize: 60, + FixedRate: 0.5, + Version: 1, + ServiceName: "test", + ResourceARN: "*", + ServiceType: "*", + }, + reservoir: reservoir{ + refreshedAt: time.Unix(13000000, 0), + }, + } + clock := &mockClock{ + nowTime: 1500000000, + } + + rules := []Rule{r1} + + m := &Manifest{ + Rules: rules, + clock: clock, + } + + // async rule updates + go func() { + for i := 0; i < 100; i++ { + m.updateRules(&getSamplingRulesOutput{ + SamplingRuleRecords: []*samplingRuleRecords{&ruleRecords}, + }) + time.Sleep(time.Millisecond) + } + }() + + // async target updates + go func() { + for i := 0; i < 100; i++ { + var manifest Manifest + + err := func() error { + m.mu.RLock() + defer m.mu.RUnlock() + + err := copier.CopyWithOption(&manifest, m, copier.Option{IgnoreEmpty: false, DeepCopy: true}) + if err != nil { + return err + } + + return nil + }() + require.NoError(t, err) + + err = manifest.updateReservoir(createSamplingTargetDocument("r1", 0, 0.05, 10, 13000000)) + require.NoError(t, err) + time.Sleep(time.Millisecond) + + m.mu.Lock() + m.Rules = manifest.Rules + m.mu.Unlock() + } + }() + + // matching logic + for i := 0; i < 100; i++ { + _, match, err := m.MatchAgainstManifestRules(sdktrace.SamplingParameters{}, "helios", "macos") + require.NoError(t, err) + require.False(t, match) + time.Sleep(time.Millisecond) + } +} + +// validate no data race is when capturing sampling statistics in manifest while sampling +func TestRaceUpdatingSamplingStatisticsWhenSampling(t *testing.T) { + // existing rule already present in manifest + r1 := Rule{ + ruleProperties: ruleProperties{ + RuleName: "r1", + Priority: 10000, + Host: "*", + HTTPMethod: "*", + URLPath: "*", + ReservoirSize: 60, + FixedRate: 0.5, + Version: 1, + ServiceName: "test", + ResourceARN: "*", + ServiceType: "*", + }, + reservoir: reservoir{ + refreshedAt: time.Unix(15000000, 0), + mu: &sync.RWMutex{}, + }, + samplingStatistics: &samplingStatistics{ + matchedRequests: 5, + borrowedRequests: 0, + sampledRequests: 0, + }, + } + clock := &mockClock{ + nowTime: 18000000, + } + + rules := []Rule{r1} + + m := &Manifest{ + Rules: rules, + clock: clock, + } + + // async snapshot updates + go func() { + for i := 0; i < 100; i++ { + var manifest Manifest + + err := func() error { + m.mu.RLock() + defer m.mu.RUnlock() + + err := copier.CopyWithOption(&manifest, m, copier.Option{IgnoreEmpty: false, DeepCopy: true}) + if err != nil { + return err + } + + return nil + }() + require.NoError(t, err) + + _, err = manifest.snapshots() + require.NoError(t, err) + + m.mu.Lock() + m.Rules = manifest.Rules + m.mu.Unlock() + time.Sleep(time.Millisecond) + } + }() + + // sampling logic + for i := 0; i < 100; i++ { + _ = r1.Sample(sdktrace.SamplingParameters{}, time.Unix(clock.nowTime+int64(i), 0)) + time.Sleep(time.Millisecond) + } +} diff --git a/samplers/aws/xray/internal/reservoir_test.go b/samplers/aws/xray/internal/reservoir_test.go index 7a93b479a39..3b7b705b401 100644 --- a/samplers/aws/xray/internal/reservoir_test.go +++ b/samplers/aws/xray/internal/reservoir_test.go @@ -185,7 +185,7 @@ func TestResetQuotaUsageRotation(t *testing.T) { // take() should be false since no unused quota left taken := r.take(clock.now(), false, 1.0) - assert.Equal(t, false, taken) + assert.False(t, taken) // increment epoch to reset unused quota clock = &mockClock{ diff --git a/samplers/aws/xray/internal/rule.go b/samplers/aws/xray/internal/rule.go index 033b2b80483..0ba680c9c59 100644 --- a/samplers/aws/xray/internal/rule.go +++ b/samplers/aws/xray/internal/rule.go @@ -48,8 +48,10 @@ type samplingStatistics struct { // stale checks if targets (sampling stats) for a given rule is expired or not. func (r *Rule) stale(now time.Time) bool { - reservoirRefreshTime := r.reservoir.refreshedAt.Add(time.Duration(r.reservoir.interval) * time.Second) - return r.samplingStatistics.matchedRequests != 0 && now.After(reservoirRefreshTime) + matchedRequests := atomic.LoadInt64(&r.samplingStatistics.matchedRequests) + + reservoirRefreshTime := r.reservoir.refreshedAt.Add(r.reservoir.interval * time.Second) + return matchedRequests != 0 && now.After(reservoirRefreshTime) } // snapshot takes a snapshot of the sampling statistics counters, returning @@ -210,7 +212,7 @@ func (r *Rule) appliesTo(parameters sdktrace.SamplingParameters, serviceName str } } - return true, nil + return attributeMatcher && serviceNameMatcher && serviceTypeMatcher && HTTPMethodMatcher && HTTPHostMatcher && HTTPURLPathMatcher, nil } // attributeMatching performs a match on attributes set by users on AWS X-Ray console. @@ -230,6 +232,10 @@ func (r *Rule) attributeMatching(parameters sdktrace.SamplingParameters) (bool, if err != nil { return false, err } + + if !match { + return false, nil + } } else { unmatchedCounter++ } diff --git a/samplers/aws/xray/internal/rule_test.go b/samplers/aws/xray/internal/rule_test.go index f3aca97db78..d9d89b4298b 100644 --- a/samplers/aws/xray/internal/rule_test.go +++ b/samplers/aws/xray/internal/rule_test.go @@ -19,6 +19,8 @@ import ( "testing" "time" + "github.com/jinzhu/copier" + "github.com/stretchr/testify/require" "go.opentelemetry.io/otel/attribute" @@ -373,17 +375,18 @@ func TestAppliesToHTTPTargetMatching(t *testing.T) { assert.True(t, match) } -// assert early exit when attribute matcher is false -func TestAppliesToExitEarlyNoAttributesMatch(t *testing.T) { +// assert early exit when rule properties retrieved from AWS X-Ray console does not match with span attributes +func TestAppliesToExitEarlyNoMatch(t *testing.T) { commonLabels := []attribute.KeyValue{ - attribute.String("http.target", "target"), + attribute.String("labelA", "chocolate"), + attribute.String("labelC", "fudge"), } - r1 := Rule{ + noServiceNameMatch := &Rule{ ruleProperties: ruleProperties{ RuleName: "r1", - ServiceName: "test-service", - ServiceType: "ECS", + ServiceName: "local", + ServiceType: "*", Host: "*", HTTPMethod: "*", URLPath: "*", @@ -394,17 +397,10 @@ func TestAppliesToExitEarlyNoAttributesMatch(t *testing.T) { }, } - match, err := r1.appliesTo(trace.SamplingParameters{Attributes: commonLabels}, "test-service", "ECS") - require.NoError(t, err) - assert.False(t, match) -} - -// assert early exit when service name matcher is false -func TestAppliesToExitEarlyNoServiceNameMatch(t *testing.T) { - r1 := Rule{ + noServiceTypeMatch := &Rule{ ruleProperties: ruleProperties{ RuleName: "r1", - ServiceName: "test-service", + ServiceName: "*", ServiceType: "ECS", Host: "*", HTTPMethod: "*", @@ -416,31 +412,79 @@ func TestAppliesToExitEarlyNoServiceNameMatch(t *testing.T) { }, } - match, err := r1.appliesTo(trace.SamplingParameters{}, "test-service11", "ECS") - require.NoError(t, err) - assert.False(t, match) -} + noHTTPMethodMatcher := &Rule{ + ruleProperties: ruleProperties{ + RuleName: "r1", + ServiceName: "*", + ServiceType: "*", + Host: "*", + HTTPMethod: "GET", + URLPath: "*", + Attributes: map[string]string{ + "labelA": "chocolate", + "labelC": "fudge", + }, + }, + } -// assert early exit when service type matcher is false -func TestAppliesToExitEarlyNoServiceTypeMatch(t *testing.T) { - r1 := Rule{ + noHTTPHostMatcher := &Rule{ + ruleProperties: ruleProperties{ + RuleName: "r1", + ServiceName: "*", + ServiceType: "*", + Host: "http://localhost:2022", + HTTPMethod: "*", + URLPath: "*", + Attributes: map[string]string{ + "labelA": "chocolate", + "labelC": "fudge", + }, + }, + } + + noHTTPURLPathMatcher := &Rule{ + ruleProperties: ruleProperties{ + RuleName: "r1", + ServiceName: "*", + ServiceType: "*", + Host: "*", + HTTPMethod: "*", + URLPath: "/test/path", + Attributes: map[string]string{}, + }, + } + + noAttributeMatcher := &Rule{ ruleProperties: ruleProperties{ RuleName: "r1", ServiceName: "test-service", - ServiceType: "EC2", + ServiceType: "*", Host: "*", HTTPMethod: "*", URLPath: "*", Attributes: map[string]string{ "labelA": "chocolate", - "labelC": "fudge", + "labelC": "vanilla", }, }, } - match, err := r1.appliesTo(trace.SamplingParameters{}, "test-service", "ECS") - require.NoError(t, err) - assert.False(t, match) + tests := []struct { + rules *Rule + }{ + {noServiceNameMatch}, + {noServiceTypeMatch}, + {noHTTPMethodMatcher}, + {noHTTPHostMatcher}, + {noHTTPURLPathMatcher}, + {noAttributeMatcher}, + } + + for _, test := range tests { + match, err := test.rules.appliesTo(trace.SamplingParameters{Attributes: commonLabels}, "test-service", "local") + require.NoError(t, err) + require.False(t, match) + } } // assert that if rules has attribute and span has those attribute with same value then matching will happen. @@ -524,3 +568,116 @@ func TestMatchAgainstManifestRulesNoAttributeMatch(t *testing.T) { require.NoError(t, err) assert.False(t, match) } + +// validate no data race is happening when updating rule properties and rule targets in manifest while sampling +func TestRaceUpdatingRulesAndTargetsWhileSampling(t *testing.T) { + // getSamplingRules response to update existing manifest rule + ruleRecords := samplingRuleRecords{ + SamplingRule: &ruleProperties{ + RuleName: "r1", + Priority: 10000, + Host: "localhost", + HTTPMethod: "*", + URLPath: "/test/path", + ReservoirSize: 40, + FixedRate: 0.9, + Version: 1, + ServiceName: "helios", + ResourceARN: "*", + ServiceType: "*", + }, + } + + // sampling target document to update existing manifest rule + rate := 0.05 + quota := float64(10) + ttl := float64(18000000) + name := "r1" + + st := samplingTargetDocument{ + FixedRate: &rate, + ReservoirQuota: "a, + ReservoirQuotaTTL: &ttl, + RuleName: &name, + } + + // existing rule already present in manifest + r1 := Rule{ + ruleProperties: ruleProperties{ + RuleName: "r1", + Priority: 10000, + Host: "*", + HTTPMethod: "*", + URLPath: "*", + ReservoirSize: 60, + FixedRate: 0.5, + Version: 1, + ServiceName: "test", + ResourceARN: "*", + ServiceType: "*", + }, + reservoir: reservoir{ + refreshedAt: time.Unix(18000000, 0), + mu: &sync.RWMutex{}, + }, + samplingStatistics: &samplingStatistics{ + matchedRequests: 0, + borrowedRequests: 0, + sampledRequests: 0, + }, + } + clock := &mockClock{ + nowTime: 1500000000, + } + + rules := []Rule{r1} + + m := &Manifest{ + Rules: rules, + clock: clock, + } + + // async rule updates + go func() { + for i := 0; i < 100; i++ { + m.updateRules(&getSamplingRulesOutput{ + SamplingRuleRecords: []*samplingRuleRecords{&ruleRecords}, + }) + time.Sleep(time.Millisecond) + } + }() + + // async target updates + go func() { + for i := 0; i < 100; i++ { + var manifest Manifest + + err := func() error { + m.mu.RLock() + defer m.mu.RUnlock() + + err := copier.CopyWithOption(&manifest, m, copier.Option{IgnoreEmpty: false, DeepCopy: true}) + if err != nil { + return err + } + + return nil + }() + require.NoError(t, err) + + err = manifest.updateReservoir(&st) + require.NoError(t, err) + time.Sleep(time.Millisecond) + + m.mu.Lock() + m.Rules = manifest.Rules + m.mu.Unlock() + } + }() + + // sampling logic + for i := 0; i < 100; i++ { + _ = r1.Sample(trace.SamplingParameters{}, time.Unix(clock.nowTime+int64(i), 0)) + time.Sleep(time.Millisecond) + } +} diff --git a/samplers/aws/xray/remote_sampler.go b/samplers/aws/xray/remote_sampler.go index 644cd1f06ba..c83a1dd9b73 100644 --- a/samplers/aws/xray/remote_sampler.go +++ b/samplers/aws/xray/remote_sampler.go @@ -53,7 +53,7 @@ var _ sdktrace.Sampler = (*remoteSampler)(nil) // based on the sampling rules set by users on AWS X-Ray console. Sampler also periodically polls // sampling rules and sampling targets. // NOTE: ctx passed in NewRemoteSampler API is being used in background go routine. Cancellation to this context can kill the background go routine. -// Guide on AWS X-Ray remote sampling implementation (https://aws-otel.github.io/docs/getting-started/remote-sampling#otel-remote-sampling-implementation-caveats) +// Guide on AWS X-Ray remote sampling implementation (https://aws-otel.github.io/docs/getting-started/remote-sampling#otel-remote-sampling-implementation-caveats). func NewRemoteSampler(ctx context.Context, serviceName string, cloudPlatform string, opts ...Option) (sdktrace.Sampler, error) { // Create new config based on options or set to default values. cfg, err := newConfig(opts...) From b0edd369d210e70ce2a4af1056eb4844d91a35aa Mon Sep 17 00:00:00 2001 From: bhautikpip Date: Fri, 25 Mar 2022 16:52:26 -0700 Subject: [PATCH 31/38] added custom deep copy implementation --- samplers/aws/xray/go.mod | 1 - samplers/aws/xray/go.sum | 2 - samplers/aws/xray/internal/client_test.go | 11 + samplers/aws/xray/internal/manifest.go | 71 ++++-- samplers/aws/xray/internal/manifest_test.go | 234 ++++++++++++------- samplers/aws/xray/internal/reservoir.go | 2 +- samplers/aws/xray/internal/reservoir_test.go | 10 - samplers/aws/xray/internal/rule.go | 2 +- samplers/aws/xray/internal/rule_test.go | 44 +--- samplers/aws/xray/remote_sampler.go | 4 +- samplers/aws/xray/remote_sampler_config.go | 6 +- 11 files changed, 228 insertions(+), 159 deletions(-) diff --git a/samplers/aws/xray/go.mod b/samplers/aws/xray/go.mod index 49291646857..4a07f421d36 100644 --- a/samplers/aws/xray/go.mod +++ b/samplers/aws/xray/go.mod @@ -5,7 +5,6 @@ go 1.16 require ( github.com/go-logr/logr v1.2.3 github.com/go-logr/stdr v1.2.2 - github.com/jinzhu/copier v0.3.5 github.com/stretchr/testify v1.7.1 go.opentelemetry.io/otel v1.6.0 go.opentelemetry.io/otel/sdk v1.6.0 diff --git a/samplers/aws/xray/go.sum b/samplers/aws/xray/go.sum index f71c9c92645..8c5508c9615 100644 --- a/samplers/aws/xray/go.sum +++ b/samplers/aws/xray/go.sum @@ -7,8 +7,6 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o= github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= -github.com/jinzhu/copier v0.3.5 h1:GlvfUwHk62RokgqVNvYsku0TATCF7bAHVwEXoBh3iJg= -github.com/jinzhu/copier v0.3.5/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= diff --git a/samplers/aws/xray/internal/client_test.go b/samplers/aws/xray/internal/client_test.go index 08a18752db4..40e881cf91b 100644 --- a/samplers/aws/xray/internal/client_test.go +++ b/samplers/aws/xray/internal/client_test.go @@ -240,6 +240,17 @@ func TestGetSamplingTargetsMissingValues(t *testing.T) { assert.Nil(t, samplingTragets.SamplingTargetDocuments[0].ReservoirQuota) } +func TestNilContext(t *testing.T) { + client := createTestClient(t, []byte(``)) + samplingRulesOutput, err := client.getSamplingRules(context.TODO()) + require.Error(t, err) + require.Nil(t, samplingRulesOutput) + + samplingTargetsOutput, err := client.getSamplingTargets(context.TODO(), nil) + require.Error(t, err) + require.Nil(t, samplingTargetsOutput) +} + func TestNewClient(t *testing.T) { endpoint, err := url.Parse("http://127.0.0.1:2020") require.NoError(t, err) diff --git a/samplers/aws/xray/internal/manifest.go b/samplers/aws/xray/internal/manifest.go index a040ab5aa81..ec3b49aae71 100644 --- a/samplers/aws/xray/internal/manifest.go +++ b/samplers/aws/xray/internal/manifest.go @@ -25,8 +25,6 @@ import ( "sync" "time" - "github.com/jinzhu/copier" - "github.com/go-logr/logr" sdktrace "go.opentelemetry.io/otel/sdk/trace" @@ -119,24 +117,10 @@ func (m *Manifest) RefreshManifestRules(ctx context.Context) (err error) { // RefreshManifestTargets updates sampling targets (statistics) for each rule. func (m *Manifest) RefreshManifestTargets(ctx context.Context) (refresh bool, err error) { - var manifest Manifest - - err = func() error { - m.mu.RLock() - defer m.mu.RUnlock() + // Deep copy manifest object. + manifest := m.deepCopy() - err = copier.CopyWithOption(&manifest, m, copier.Option{IgnoreEmpty: false, DeepCopy: true}) - if err != nil { - return err - } - - return nil - }() - if err != nil { - return false, err - } - - // generate sampling statistics based on the data in temporary manifest + // Generate sampling statistics based on the data in temporary manifest statistics, err := manifest.snapshots() if err != nil { return false, err @@ -208,11 +192,10 @@ func (m *Manifest) updateRules(rules *getSamplingRulesOutput) { func (m *Manifest) createRule(ruleProp ruleProperties) { cr := reservoir{ capacity: ruleProp.ReservoirSize, - mu: &sync.RWMutex{}, } csr := Rule{ - reservoir: cr, + reservoir: &cr, ruleProperties: ruleProp, samplingStatistics: &samplingStatistics{}, } @@ -315,6 +298,52 @@ func (m *Manifest) snapshots() ([]*samplingStatisticsDocument, error) { return statistics, nil } +// deepCopy copies the m to another manifest object. +func (m *Manifest) deepCopy() *Manifest { + m.mu.RLock() + defer m.mu.RUnlock() + + manifest := Manifest{ + Rules: []Rule{}, + } + + for _, rule := range m.Rules { + // Deep copying rules. + var tempRule Rule + tempRule.ruleProperties = rule.ruleProperties + + // Deep copying reservoir (copying each fields of reservoir because we want to initialize new mutex values for each rule). + var tempRes reservoir + + rule.reservoir.mu.RLock() + tempRes.expiresAt = rule.reservoir.expiresAt + tempRes.quota = rule.reservoir.quota + tempRes.quotaBalance = rule.reservoir.quotaBalance + tempRes.capacity = rule.reservoir.capacity + tempRes.refreshedAt = rule.reservoir.refreshedAt + tempRes.interval = rule.reservoir.interval + tempRes.lastTick = rule.reservoir.lastTick + rule.reservoir.mu.RUnlock() + + tempRule.reservoir = &tempRes + + // Shallow copying sampling statistics. + tempRule.samplingStatistics = rule.samplingStatistics + + manifest.Rules = append(manifest.Rules, tempRule) + } + + // Copying other manifest fields. + manifest.SamplingTargetsPollingInterval = m.SamplingTargetsPollingInterval + manifest.refreshedAt = m.refreshedAt + manifest.xrayClient = m.xrayClient + manifest.clientID = m.clientID + manifest.logger = m.logger + manifest.clock = m.clock + + return &manifest +} + // sort sorts the Rules of m first by priority and then by name. func (m *Manifest) sort() { less := func(i, j int) bool { diff --git a/samplers/aws/xray/internal/manifest_test.go b/samplers/aws/xray/internal/manifest_test.go index 2188b60436e..4ff24c8504e 100644 --- a/samplers/aws/xray/internal/manifest_test.go +++ b/samplers/aws/xray/internal/manifest_test.go @@ -17,12 +17,9 @@ package internal import ( "context" "net/url" - "sync" "testing" "time" - "github.com/jinzhu/copier" - "go.opentelemetry.io/otel/attribute" "github.com/go-logr/logr/testr" @@ -107,7 +104,7 @@ func TestMatchAgainstManifestRules(t *testing.T) { ResourceARN: "*", ServiceType: "*", }, - reservoir: reservoir{ + reservoir: &reservoir{ expiresAt: time.Unix(14050, 0), }, } @@ -126,7 +123,7 @@ func TestMatchAgainstManifestRules(t *testing.T) { ResourceARN: "*", ServiceType: "local", }, - reservoir: reservoir{ + reservoir: &reservoir{ expiresAt: time.Unix(14050, 0), }, } @@ -168,7 +165,7 @@ func TestMatchAgainstManifestRulesAttributeMatch(t *testing.T) { "labelB": "raspberry", }, }, - reservoir: reservoir{ + reservoir: &reservoir{ expiresAt: time.Unix(14050, 0), }, } @@ -210,7 +207,7 @@ func TestMatchAgainstManifestRulesAttributeWildCardMatch(t *testing.T) { "labelB": "rasp*", }, }, - reservoir: reservoir{ + reservoir: &reservoir{ expiresAt: time.Unix(14050, 0), }, } @@ -245,7 +242,7 @@ func TestMatchAgainstManifestRulesNoMatch(t *testing.T) { ResourceARN: "*", ServiceType: "local", }, - reservoir: reservoir{ + reservoir: &reservoir{ expiresAt: time.Unix(14050, 0), }, } @@ -352,9 +349,8 @@ func TestRefreshManifestRules(t *testing.T) { ServiceType: "*", Attributes: map[string]string{}, }, - reservoir: reservoir{ + reservoir: &reservoir{ capacity: 60, - mu: &sync.RWMutex{}, }, samplingStatistics: &samplingStatistics{}, } @@ -374,9 +370,8 @@ func TestRefreshManifestRules(t *testing.T) { ServiceType: "*", Attributes: map[string]string{}, }, - reservoir: reservoir{ + reservoir: &reservoir{ capacity: 3, - mu: &sync.RWMutex{}, }, samplingStatistics: &samplingStatistics{}, } @@ -396,9 +391,8 @@ func TestRefreshManifestRules(t *testing.T) { ServiceType: "local", Attributes: map[string]string{}, }, - reservoir: reservoir{ + reservoir: &reservoir{ capacity: 100, - mu: &sync.RWMutex{}, }, samplingStatistics: &samplingStatistics{}, } @@ -602,9 +596,8 @@ func TestRefreshManifestAddOneInvalidRule(t *testing.T) { ServiceType: "*", Attributes: map[string]string{}, }, - reservoir: reservoir{ + reservoir: &reservoir{ capacity: 60, - mu: &sync.RWMutex{}, }, samplingStatistics: &samplingStatistics{}, } @@ -646,7 +639,7 @@ func TestRefreshManifestTargetNoSnapShot(t *testing.T) { ServiceType: "local", Attributes: map[string]string{}, }, - reservoir: reservoir{ + reservoir: &reservoir{ capacity: 100, }, samplingStatistics: &samplingStatistics{ @@ -707,9 +700,8 @@ func TestRefreshManifestTargets(t *testing.T) { ServiceType: "local", Attributes: map[string]string{}, }, - reservoir: reservoir{ + reservoir: &reservoir{ capacity: 100, - mu: &sync.RWMutex{}, }, samplingStatistics: &samplingStatistics{ matchedRequests: int64(5), @@ -780,32 +772,26 @@ func TestRefreshManifestTargetsPollIntervalUpdateTest(t *testing.T) { ruleProperties: ruleProperties{ RuleName: "r1", }, - reservoir: reservoir{ - mu: &sync.RWMutex{}, - }, samplingStatistics: &samplingStatistics{ matchedRequests: int64(5), }, + reservoir: &reservoir{}, } r2 := Rule{ ruleProperties: ruleProperties{ RuleName: "r2", }, - reservoir: reservoir{ - mu: &sync.RWMutex{}, - }, samplingStatistics: &samplingStatistics{}, + reservoir: &reservoir{}, } r3 := Rule{ ruleProperties: ruleProperties{ RuleName: "r3", }, - reservoir: reservoir{ - mu: &sync.RWMutex{}, - }, samplingStatistics: &samplingStatistics{}, + reservoir: &reservoir{}, } m := &Manifest{ @@ -835,7 +821,7 @@ func TestUpdateTargets(t *testing.T) { RuleName: "r1", FixedRate: 0.10, }, - reservoir: reservoir{ + reservoir: &reservoir{ quota: 8, refreshedAt: time.Unix(1499999990, 0), expiresAt: time.Unix(1500000010, 0), @@ -864,7 +850,7 @@ func TestUpdateTargets(t *testing.T) { RuleName: "r1", FixedRate: 0.05, }, - reservoir: reservoir{ + reservoir: &reservoir{ quota: 10, refreshedAt: time.Unix(1500000000, 0), expiresAt: time.Unix(1500000060, 0), @@ -891,7 +877,7 @@ func TestUpdateTargetsRefreshFlagTest(t *testing.T) { RuleName: "r1", FixedRate: 0.10, }, - reservoir: reservoir{ + reservoir: &reservoir{ quota: 8, refreshedAt: time.Unix(1499999990, 0), expiresAt: time.Unix(1500000010, 0), @@ -921,7 +907,7 @@ func TestUpdateTargetsRefreshFlagTest(t *testing.T) { RuleName: "r1", FixedRate: 0.05, }, - reservoir: reservoir{ + reservoir: &reservoir{ quota: 10, refreshedAt: time.Unix(1500000000, 0), expiresAt: time.Unix(1500000060, 0), @@ -1015,7 +1001,7 @@ func TestUpdateReservoir(t *testing.T) { RuleName: "r2", FixedRate: 0.10, }, - reservoir: reservoir{ + reservoir: &reservoir{ quota: 8, refreshedAt: time.Unix(1499999990, 0), expiresAt: time.Unix(1500000010, 0), @@ -1042,7 +1028,7 @@ func TestUpdateReservoirMissingFixedRate(t *testing.T) { RuleName: "r2", FixedRate: 0.10, }, - reservoir: reservoir{ + reservoir: &reservoir{ quota: 8, refreshedAt: time.Unix(1499999990, 0), expiresAt: time.Unix(1500000010, 0), @@ -1068,7 +1054,7 @@ func TestUpdateReservoirMissingRuleName(t *testing.T) { RuleName: "r2", FixedRate: 0.10, }, - reservoir: reservoir{ + reservoir: &reservoir{ quota: 8, refreshedAt: time.Unix(1499999990, 0), expiresAt: time.Unix(1500000010, 0), @@ -1102,7 +1088,7 @@ func TestSnapshots(t *testing.T) { ruleProperties: ruleProperties{ RuleName: name1, }, - reservoir: reservoir{ + reservoir: &reservoir{ interval: 10, }, samplingStatistics: &samplingStatistics{ @@ -1120,7 +1106,7 @@ func TestSnapshots(t *testing.T) { ruleProperties: ruleProperties{ RuleName: name2, }, - reservoir: reservoir{ + reservoir: &reservoir{ interval: 10, }, samplingStatistics: &samplingStatistics{ @@ -1186,7 +1172,7 @@ func TestMixedSnapshots(t *testing.T) { ruleProperties: ruleProperties{ RuleName: name1, }, - reservoir: reservoir{ + reservoir: &reservoir{ interval: 20, refreshedAt: time.Unix(1499999970, 0), }, @@ -1207,7 +1193,7 @@ func TestMixedSnapshots(t *testing.T) { ruleProperties: ruleProperties{ RuleName: name2, }, - reservoir: reservoir{ + reservoir: &reservoir{ interval: 20, refreshedAt: time.Unix(1499999990, 0), }, @@ -1228,7 +1214,7 @@ func TestMixedSnapshots(t *testing.T) { ruleProperties: ruleProperties{ RuleName: name3, }, - reservoir: reservoir{ + reservoir: &reservoir{ interval: 20, refreshedAt: time.Unix(1499999990, 0), }, @@ -1264,7 +1250,112 @@ func TestMixedSnapshots(t *testing.T) { assert.Equal(t, ss1, *statistics[0]) } -// Assert that sorting an unsorted array results in a sorted array - check priority. +// assert that deep copy creates a new manifest object with new address space. +func TestDeepCopy(t *testing.T) { + r1 := Rule{ + ruleProperties: ruleProperties{ + RuleName: "r1", + Priority: 100, + Host: "http://127.0.0.0.1:2020", + HTTPMethod: "POST", + URLPath: "/test", + ReservoirSize: 100, + FixedRate: 0.09, + Version: 1, + ServiceName: "openTelemetry", + ResourceARN: "*", + ServiceType: "local", + Attributes: map[string]string{}, + }, + reservoir: &reservoir{ + capacity: 100, + }, + samplingStatistics: &samplingStatistics{ + matchedRequests: int64(5), + borrowedRequests: int64(1), + sampledRequests: int64(3), + }, + } + + r2 := Rule{ + ruleProperties: ruleProperties{ + RuleName: "r2", + Priority: 10, + Host: "http://127.0.0.0.1:2020", + HTTPMethod: "GET", + URLPath: "/test/path", + ReservoirSize: 100, + FixedRate: 0.09, + Version: 1, + ServiceName: "x-ray", + ResourceARN: "*", + ServiceType: "local", + Attributes: map[string]string{}, + }, + reservoir: &reservoir{ + capacity: 100, + }, + samplingStatistics: &samplingStatistics{ + matchedRequests: int64(5), + borrowedRequests: int64(1), + sampledRequests: int64(3), + }, + } + + clock := &mockClock{ + nowTime: 1500000000, + } + + m := &Manifest{ + Rules: []Rule{r1, r2}, + SamplingTargetsPollingInterval: 10 * time.Second, + refreshedAt: time.Unix(1500000, 0), + xrayClient: createTestClient(t, []byte(`hello world!`)), + logger: testr.New(t), + clock: clock, + } + + manifest := m.deepCopy() + + require.Len(t, m.Rules, 2) + require.Len(t, manifest.Rules, 2) + + assert.Equal(t, &m.xrayClient, &manifest.xrayClient) + + assert.NotSame(t, &m.clock, &manifest.clock) + assert.NotSame(t, &m.refreshedAt, &manifest.refreshedAt) + assert.NotSame(t, &m.SamplingTargetsPollingInterval, &manifest.SamplingTargetsPollingInterval) + assert.NotSame(t, &m.logger, &manifest.logger) + assert.NotSame(t, &m.mu, &manifest.mu) + + // rule properties has different address space in m and manifest + assert.NotSame(t, &m.Rules[0].ruleProperties.RuleName, &manifest.Rules[0].ruleProperties.RuleName) + assert.NotSame(t, &m.Rules[0].ruleProperties.ServiceName, &manifest.Rules[0].ruleProperties.RuleName) + assert.NotSame(t, &m.Rules[0].ruleProperties.ServiceType, &manifest.Rules[0].ruleProperties.RuleName) + assert.NotSame(t, &m.Rules[0].ruleProperties.Host, &manifest.Rules[0].ruleProperties.RuleName) + assert.NotSame(t, &m.Rules[0].ruleProperties.HTTPMethod, &manifest.Rules[0].ruleProperties.RuleName) + assert.NotSame(t, &m.Rules[0].ruleProperties.URLPath, &manifest.Rules[0].ruleProperties.RuleName) + assert.NotSame(t, &m.Rules[0].ruleProperties.FixedRate, &manifest.Rules[0].ruleProperties.RuleName) + assert.NotSame(t, &m.Rules[0].ruleProperties.ReservoirSize, &manifest.Rules[0].ruleProperties.RuleName) + assert.NotSame(t, &m.Rules[0].ruleProperties.ResourceARN, &manifest.Rules[0].ruleProperties.RuleName) + assert.NotSame(t, &m.Rules[0].ruleProperties.Priority, &manifest.Rules[0].ruleProperties.Priority) + assert.NotSame(t, &m.Rules[0].ruleProperties.Version, &manifest.Rules[0].ruleProperties.Version) + assert.NotSame(t, &m.Rules[0].ruleProperties.Attributes, &manifest.Rules[0].ruleProperties.Attributes) + + // reservoir has different address space in m and manifest + assert.NotSame(t, &m.Rules[0].reservoir.refreshedAt, &manifest.Rules[0].reservoir.refreshedAt) + assert.NotSame(t, &m.Rules[0].reservoir.expiresAt, &manifest.Rules[0].reservoir.expiresAt) + assert.NotSame(t, &m.Rules[0].reservoir.lastTick, &manifest.Rules[0].reservoir.lastTick) + assert.NotSame(t, &m.Rules[0].reservoir.interval, &manifest.Rules[0].reservoir.interval) + assert.NotSame(t, &m.Rules[0].reservoir.capacity, &manifest.Rules[0].reservoir.capacity) + assert.NotSame(t, &m.Rules[0].reservoir.quota, &manifest.Rules[0].reservoir.quota) + assert.NotSame(t, &m.Rules[0].reservoir.quotaBalance, &manifest.Rules[0].reservoir.quotaBalance) + + // samplings statistics has same address space since it is a pointer + assert.Equal(t, &m.Rules[0].samplingStatistics, &manifest.Rules[0].samplingStatistics) +} + +// assert that sorting an unsorted array results in a sorted array - check priority. func TestSortBasedOnPriority(t *testing.T) { r1 := Rule{ ruleProperties: ruleProperties{ @@ -1303,7 +1394,7 @@ func TestSortBasedOnPriority(t *testing.T) { assert.Equal(t, r3, m.Rules[2]) } -// Assert that sorting an unsorted array results in a sorted array - check priority and rule name. +// assert that sorting an unsorted array results in a sorted array - check priority and rule name. func TestSortBasedOnRuleName(t *testing.T) { r1 := Rule{ ruleProperties: ruleProperties{ @@ -1344,9 +1435,9 @@ func TestSortBasedOnRuleName(t *testing.T) { // asserts the minimum value of all the targets. func TestMinPollInterval(t *testing.T) { - r1 := Rule{reservoir: reservoir{interval: time.Duration(10)}} - r2 := Rule{reservoir: reservoir{interval: time.Duration(5)}} - r3 := Rule{reservoir: reservoir{interval: time.Duration(25)}} + r1 := Rule{reservoir: &reservoir{interval: time.Duration(10)}} + r2 := Rule{reservoir: &reservoir{interval: time.Duration(5)}} + r3 := Rule{reservoir: &reservoir{interval: time.Duration(25)}} rules := []Rule{r1, r2, r3} m := &Manifest{Rules: rules} @@ -1358,9 +1449,9 @@ func TestMinPollInterval(t *testing.T) { // asserts the minimum value of all the targets when some targets has 0 interval. func TestMinPollIntervalZeroCase(t *testing.T) { - r1 := Rule{reservoir: reservoir{interval: time.Duration(0)}} - r2 := Rule{reservoir: reservoir{interval: time.Duration(0)}} - r3 := Rule{reservoir: reservoir{interval: time.Duration(5)}} + r1 := Rule{reservoir: &reservoir{interval: time.Duration(0)}} + r2 := Rule{reservoir: &reservoir{interval: time.Duration(0)}} + r3 := Rule{reservoir: &reservoir{interval: time.Duration(5)}} rules := []Rule{r1, r2, r3} m := &Manifest{Rules: rules} @@ -1372,9 +1463,9 @@ func TestMinPollIntervalZeroCase(t *testing.T) { // asserts the minimum value of all the targets when some targets has negative interval. func TestMinPollIntervalNegativeCase(t *testing.T) { - r1 := Rule{reservoir: reservoir{interval: time.Duration(-5)}} - r2 := Rule{reservoir: reservoir{interval: time.Duration(0)}} - r3 := Rule{reservoir: reservoir{interval: time.Duration(0)}} + r1 := Rule{reservoir: &reservoir{interval: time.Duration(-5)}} + r2 := Rule{reservoir: &reservoir{interval: time.Duration(0)}} + r3 := Rule{reservoir: &reservoir{interval: time.Duration(0)}} rules := []Rule{r1, r2, r3} m := &Manifest{Rules: rules} @@ -1439,7 +1530,7 @@ func TestRaceUpdatingRulesWhileMatching(t *testing.T) { ResourceARN: "*", ServiceType: "*", }, - reservoir: reservoir{ + reservoir: &reservoir{ expiresAt: time.Unix(14050, 0), }, } @@ -1505,7 +1596,7 @@ func TestRaceUpdatingRulesAndTargetsWhileMatching(t *testing.T) { ResourceARN: "*", ServiceType: "*", }, - reservoir: reservoir{ + reservoir: &reservoir{ refreshedAt: time.Unix(13000000, 0), }, } @@ -1533,22 +1624,9 @@ func TestRaceUpdatingRulesAndTargetsWhileMatching(t *testing.T) { // async target updates go func() { for i := 0; i < 100; i++ { - var manifest Manifest - - err := func() error { - m.mu.RLock() - defer m.mu.RUnlock() - - err := copier.CopyWithOption(&manifest, m, copier.Option{IgnoreEmpty: false, DeepCopy: true}) - if err != nil { - return err - } + manifest := m.deepCopy() - return nil - }() - require.NoError(t, err) - - err = manifest.updateReservoir(createSamplingTargetDocument("r1", 0, 0.05, 10, 13000000)) + err := manifest.updateReservoir(createSamplingTargetDocument("r1", 0, 0.05, 10, 13000000)) require.NoError(t, err) time.Sleep(time.Millisecond) @@ -1584,9 +1662,8 @@ func TestRaceUpdatingSamplingStatisticsWhenSampling(t *testing.T) { ResourceARN: "*", ServiceType: "*", }, - reservoir: reservoir{ + reservoir: &reservoir{ refreshedAt: time.Unix(15000000, 0), - mu: &sync.RWMutex{}, }, samplingStatistics: &samplingStatistics{ matchedRequests: 5, @@ -1608,22 +1685,9 @@ func TestRaceUpdatingSamplingStatisticsWhenSampling(t *testing.T) { // async snapshot updates go func() { for i := 0; i < 100; i++ { - var manifest Manifest - - err := func() error { - m.mu.RLock() - defer m.mu.RUnlock() - - err := copier.CopyWithOption(&manifest, m, copier.Option{IgnoreEmpty: false, DeepCopy: true}) - if err != nil { - return err - } - - return nil - }() - require.NoError(t, err) + manifest := m.deepCopy() - _, err = manifest.snapshots() + _, err := manifest.snapshots() require.NoError(t, err) m.mu.Lock() diff --git a/samplers/aws/xray/internal/reservoir.go b/samplers/aws/xray/internal/reservoir.go index bb0ac51f882..3c6ddd57a49 100644 --- a/samplers/aws/xray/internal/reservoir.go +++ b/samplers/aws/xray/internal/reservoir.go @@ -43,7 +43,7 @@ type reservoir struct { // Stores reservoir ticks. lastTick time.Time - mu *sync.RWMutex + mu sync.RWMutex } // expired returns true if current time is past expiration timestamp. Otherwise, false is returned if no quota remains. diff --git a/samplers/aws/xray/internal/reservoir_test.go b/samplers/aws/xray/internal/reservoir_test.go index 3b7b705b401..723b766ec33 100644 --- a/samplers/aws/xray/internal/reservoir_test.go +++ b/samplers/aws/xray/internal/reservoir_test.go @@ -15,7 +15,6 @@ package internal import ( - "sync" "testing" "time" @@ -31,7 +30,6 @@ func TestExpiredReservoir(t *testing.T) { expiresAt := time.Unix(1500000000, 0) r := &reservoir{ expiresAt: expiresAt, - mu: &sync.RWMutex{}, } expired := r.expired(clock.now()) @@ -49,7 +47,6 @@ func TestExpiredReservoirSameAsClockTime(t *testing.T) { r := &reservoir{ expiresAt: expiresAt, - mu: &sync.RWMutex{}, } assert.False(t, r.expired(clock.now())) @@ -63,7 +60,6 @@ func TestBorrowEverySecond(t *testing.T) { r := &reservoir{ capacity: 10, - mu: &sync.RWMutex{}, } s := r.take(clock.now(), true, 1.0) @@ -91,7 +87,6 @@ func TestConsumeFromBorrowConsumeFromQuota(t *testing.T) { r := &reservoir{ quota: 2, capacity: 10, - mu: &sync.RWMutex{}, } s := r.take(clock.now(), true, 1.0) @@ -133,7 +128,6 @@ func TestConsumeFromReservoir(t *testing.T) { r := &reservoir{ quota: 2, capacity: 100, - mu: &sync.RWMutex{}, } // reservoir updates the quotaBalance for new second and allows to consume @@ -175,7 +169,6 @@ func TestResetQuotaUsageRotation(t *testing.T) { r := &reservoir{ quota: 5, capacity: 100, - mu: &sync.RWMutex{}, } // consume quota for second @@ -206,7 +199,6 @@ func TestQuotaBalanceNonBorrowExceedsCapacity(t *testing.T) { r := &reservoir{ quota: 6, capacity: 5, - mu: &sync.RWMutex{}, lastTick: time.Unix(1500000000, 0), } @@ -225,7 +217,6 @@ func TestQuotaBalanceBorrow(t *testing.T) { r := &reservoir{ quota: 6, capacity: 5, - mu: &sync.RWMutex{}, lastTick: time.Unix(1500000000, 0), } @@ -244,7 +235,6 @@ func TestQuotaBalanceIncreaseByOneBorrowCase(t *testing.T) { r := &reservoir{ quota: 6, capacity: 5, - mu: &sync.RWMutex{}, quotaBalance: 0.25, lastTick: time.Unix(1500000000, 0), } diff --git a/samplers/aws/xray/internal/rule.go b/samplers/aws/xray/internal/rule.go index 0ba680c9c59..afc022bf048 100644 --- a/samplers/aws/xray/internal/rule.go +++ b/samplers/aws/xray/internal/rule.go @@ -28,7 +28,7 @@ type Rule struct { // reservoir has equivalent fields to store what we receive from service API getSamplingTargets. // https://docs.aws.amazon.com/xray/latest/api/API_GetSamplingTargets.html - reservoir reservoir + reservoir *reservoir // ruleProperty is equivalent to what we receive from service API getSamplingRules. // https://docs.aws.amazon.com/cli/latest/reference/xray/get-sampling-rules.html diff --git a/samplers/aws/xray/internal/rule_test.go b/samplers/aws/xray/internal/rule_test.go index d9d89b4298b..fd34aef5f53 100644 --- a/samplers/aws/xray/internal/rule_test.go +++ b/samplers/aws/xray/internal/rule_test.go @@ -15,12 +15,9 @@ package internal import ( - "sync" "testing" "time" - "github.com/jinzhu/copier" - "github.com/stretchr/testify/require" "go.opentelemetry.io/otel/attribute" @@ -36,7 +33,7 @@ func TestStaleRule(t *testing.T) { samplingStatistics: &samplingStatistics{ matchedRequests: 5, }, - reservoir: reservoir{ + reservoir: &reservoir{ refreshedAt: refreshedAt, interval: 10, }, @@ -54,7 +51,7 @@ func TestFreshRule(t *testing.T) { samplingStatistics: &samplingStatistics{ matchedRequests: 5, }, - reservoir: reservoir{ + reservoir: &reservoir{ refreshedAt: refreshedAt, interval: 10, }, @@ -72,7 +69,7 @@ func TestInactiveRule(t *testing.T) { samplingStatistics: &samplingStatistics{ matchedRequests: 0, }, - reservoir: reservoir{ + reservoir: &reservoir{ refreshedAt: refreshedAt, interval: 10, }, @@ -114,10 +111,9 @@ func TestSnapshot(t *testing.T) { // assert that reservoir is expired, borrowing 1 req during that second. func TestExpiredReservoirBorrowSample(t *testing.T) { r1 := Rule{ - reservoir: reservoir{ + reservoir: &reservoir{ expiresAt: time.Unix(1500000060, 0), capacity: 10, - mu: &sync.RWMutex{}, }, ruleProperties: ruleProperties{ RuleName: "r1", @@ -138,10 +134,9 @@ func TestExpiredReservoirBorrowSample(t *testing.T) { // assert that reservoir is expired, borrowed 1 req during that second so now using traceIDRatioBased sampler. func TestExpiredReservoirTraceIDRationBasedSample(t *testing.T) { r1 := Rule{ - reservoir: reservoir{ + reservoir: &reservoir{ expiresAt: time.Unix(1500000060, 0), capacity: 10, - mu: &sync.RWMutex{}, lastTick: time.Unix(1500000061, 0), }, ruleProperties: ruleProperties{ @@ -166,10 +161,9 @@ func TestConsumeFromReservoirSample(t *testing.T) { ruleProperties: ruleProperties{ RuleName: "r1", }, - reservoir: reservoir{ + reservoir: &reservoir{ quota: 10, expiresAt: time.Unix(1500000060, 0), - mu: &sync.RWMutex{}, }, samplingStatistics: &samplingStatistics{}, } @@ -186,10 +180,9 @@ func TestConsumeFromReservoirSample(t *testing.T) { // assert that sampling using traceIDRationBasedSampler when reservoir quota is consumed. func TestTraceIDRatioBasedSamplerReservoirIsConsumedSample(t *testing.T) { r1 := Rule{ - reservoir: reservoir{ + reservoir: &reservoir{ quota: 10, expiresAt: time.Unix(1500000060, 0), - mu: &sync.RWMutex{}, lastTick: time.Unix(1500000000, 0), }, ruleProperties: ruleProperties{ @@ -211,10 +204,9 @@ func TestTraceIDRatioBasedSamplerReservoirIsConsumedSample(t *testing.T) { // assert that when fixed rate is 0 traceIDRatioBased sampler will not sample the trace. func TestTraceIDRatioBasedSamplerFixedRateZero(t *testing.T) { r1 := Rule{ - reservoir: reservoir{ + reservoir: &reservoir{ quota: 10, expiresAt: time.Unix(1500000060, 0), - mu: &sync.RWMutex{}, lastTick: time.Unix(1500000000, 0), }, ruleProperties: ruleProperties{ @@ -616,9 +608,8 @@ func TestRaceUpdatingRulesAndTargetsWhileSampling(t *testing.T) { ResourceARN: "*", ServiceType: "*", }, - reservoir: reservoir{ + reservoir: &reservoir{ refreshedAt: time.Unix(18000000, 0), - mu: &sync.RWMutex{}, }, samplingStatistics: &samplingStatistics{ matchedRequests: 0, @@ -650,22 +641,9 @@ func TestRaceUpdatingRulesAndTargetsWhileSampling(t *testing.T) { // async target updates go func() { for i := 0; i < 100; i++ { - var manifest Manifest - - err := func() error { - m.mu.RLock() - defer m.mu.RUnlock() - - err := copier.CopyWithOption(&manifest, m, copier.Option{IgnoreEmpty: false, DeepCopy: true}) - if err != nil { - return err - } - - return nil - }() - require.NoError(t, err) + manifest := m.deepCopy() - err = manifest.updateReservoir(&st) + err := manifest.updateReservoir(&st) require.NoError(t, err) time.Sleep(time.Millisecond) diff --git a/samplers/aws/xray/remote_sampler.go b/samplers/aws/xray/remote_sampler.go index c83a1dd9b73..2244cbb891d 100644 --- a/samplers/aws/xray/remote_sampler.go +++ b/samplers/aws/xray/remote_sampler.go @@ -82,7 +82,7 @@ func NewRemoteSampler(ctx context.Context, serviceName string, cloudPlatform str } // ShouldSample matches span attributes with retrieved sampling rules and returns a sampling result. -// If the sampling parameter to not match or the manifest is expired then the fallback sampler is used. +// If the sampling parameters do not match or the manifest is expired then the fallback sampler is used. func (rs *remoteSampler) ShouldSample(parameters sdktrace.SamplingParameters) sdktrace.SamplingResult { if rs.manifest.Expired() { // Use fallback sampler if manifest is expired. @@ -120,7 +120,7 @@ func (rs *remoteSampler) start(ctx context.Context) { } // startPoller starts the rule and target poller in a single go routine which runs periodically -// to the refresh manifest and targets. +// to refresh the manifest and targets. func (rs *remoteSampler) startPoller(ctx context.Context) { // jitter = 5s, default duration 300 seconds. rulesTicker := newTicker(rs.samplingRulesPollingInterval, 5*time.Second) diff --git a/samplers/aws/xray/remote_sampler_config.go b/samplers/aws/xray/remote_sampler_config.go index fa747661cc2..b12a96fb6cd 100644 --- a/samplers/aws/xray/remote_sampler_config.go +++ b/samplers/aws/xray/remote_sampler_config.go @@ -48,7 +48,7 @@ func (f optionFunc) apply(cfg *config) *config { } // WithEndpoint sets custom proxy endpoint. -// default endpoint being used is http://127.0.0.1:2000. +// If this option is not provided the default endpoint used will be http://127.0.0.1:2000. func WithEndpoint(endpoint url.URL) Option { return optionFunc(func(cfg *config) *config { cfg.endpoint = endpoint @@ -57,7 +57,7 @@ func WithEndpoint(endpoint url.URL) Option { } // WithSamplingRulesPollingInterval sets polling interval for sampling rules. -// default samplingRulesPollingInterval being used is 300 seconds. +// If this option is not provided the default samplingRulesPollingInterval used will be 300 seconds. func WithSamplingRulesPollingInterval(polingInterval time.Duration) Option { return optionFunc(func(cfg *config) *config { cfg.samplingRulesPollingInterval = polingInterval @@ -66,7 +66,7 @@ func WithSamplingRulesPollingInterval(polingInterval time.Duration) Option { } // WithLogger sets custom logging for remote sampling implementation. -// default logger being used is go-logr/stdr (https://github.com/go-logr/stdr). +// If this option is not provided the default logger used will be go-logr/stdr (https://github.com/go-logr/stdr). func WithLogger(l logr.Logger) Option { return optionFunc(func(cfg *config) *config { cfg.logger = l From 0bad9a6ac0047302a0857d10111622bfa132e75b Mon Sep 17 00:00:00 2001 From: bhautikpip Date: Sat, 26 Mar 2022 09:36:21 -0700 Subject: [PATCH 32/38] update depandabot config --- .github/dependabot.yml | 49 ------------------------------------------ 1 file changed, 49 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 7fb9c21ea60..b7a37286102 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -622,55 +622,6 @@ updates: schedule: interval: "weekly" day: "sunday" - - package-ecosystem: "gomod" - directory: "/tools" - labels: - - dependencies - - go - - "Skip Changelog" - schedule: - interval: "weekly" - day: "sunday" - - - package-ecosystem: "gomod" - directory: "/instrumentation/github.com/go-kit/kit/otelkit" - labels: - - dependencies - - go - - "Skip Changelog" - schedule: - interval: "weekly" - day: "sunday" - - - package-ecosystem: "gomod" - directory: "/instrumentation/github.com/go-kit/kit/otelkit/example" - labels: - - dependencies - - go - - "Skip Changelog" - schedule: - interval: "weekly" - day: "sunday" - - - package-ecosystem: "gomod" - directory: "/instrumentation/github.com/go-kit/kit/otelkit/test" - labels: - - dependencies - - go - - "Skip Changelog" - schedule: - interval: "weekly" - day: "sunday" - - - package-ecosystem: "gomod" - directory: "/zpages" - labels: - - dependencies - - go - - "Skip Changelog" - schedule: - interval: "weekly" - day: "sunday" - package-ecosystem: "gomod" directory: "/samplers/aws/xray" From 94cb0b76a836503ccbb32e8757182659da1f4c94 Mon Sep 17 00:00:00 2001 From: bhautikpip Date: Mon, 28 Mar 2022 13:00:40 -0700 Subject: [PATCH 33/38] added fallback sampler in case of rule match error and minor changes --- .github/dependabot.yml | 23 +++++++++++------------ samplers/aws/xray/remote_sampler.go | 6 +++--- tools/go.mod | 3 +++ tools/go.sum | 4 ++-- 4 files changed, 19 insertions(+), 17 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index b7a37286102..c9036468267 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -587,7 +587,7 @@ updates: interval: weekly day: sunday - package-ecosystem: gomod - directory: /samplers/jaegerremote + directory: /samplers/aws/xray labels: - dependencies - go @@ -596,7 +596,7 @@ updates: interval: weekly day: sunday - package-ecosystem: gomod - directory: /samplers/jaegerremote/example + directory: /samplers/jaegerremote labels: - dependencies - go @@ -605,7 +605,7 @@ updates: interval: weekly day: sunday - package-ecosystem: gomod - directory: /tools + directory: /samplers/jaegerremote/example labels: - dependencies - go @@ -614,21 +614,20 @@ updates: interval: weekly day: sunday - package-ecosystem: gomod - directory: /zpages + directory: /tools labels: - dependencies - go - Skip Changelog schedule: - interval: "weekly" - day: "sunday" - - - package-ecosystem: "gomod" - directory: "/samplers/aws/xray" + interval: weekly + day: sunday + - package-ecosystem: gomod + directory: /zpages labels: - dependencies - go - - "Skip Changelog" + - Skip Changelog schedule: - interval: "weekly" - day: "sunday" + interval: weekly + day: sunday diff --git a/samplers/aws/xray/remote_sampler.go b/samplers/aws/xray/remote_sampler.go index 2244cbb891d..31b18344a6c 100644 --- a/samplers/aws/xray/remote_sampler.go +++ b/samplers/aws/xray/remote_sampler.go @@ -93,8 +93,8 @@ func (rs *remoteSampler) ShouldSample(parameters sdktrace.SamplingParameters) sd r, match, err := rs.manifest.MatchAgainstManifestRules(parameters, rs.serviceName, rs.cloudPlatform) if err != nil { - rs.logger.Error(err, "rule matching error") - return sdktrace.SamplingResult{} + rs.logger.Error(err, "rule matching error, using fallback sampler") + return rs.fallbackSampler.ShouldSample(parameters) } if match { @@ -109,7 +109,7 @@ func (rs *remoteSampler) ShouldSample(parameters sdktrace.SamplingParameters) sd // Description returns description of the sampler being used. func (rs *remoteSampler) Description() string { - return "AwsXrayRemoteSampler{remote sampling with AWS X-Ray}" + return "AWSXRayRemoteSampler{remote sampling with AWS X-Ray}" } func (rs *remoteSampler) start(ctx context.Context) { diff --git a/tools/go.mod b/tools/go.mod index 1837062b43b..7894a74f130 100644 --- a/tools/go.mod +++ b/tools/go.mod @@ -2,7 +2,10 @@ module go.opentelemetry.io/contrib/tools go 1.13 +exclude github.com/blizzy78/varnamelen v0.6.1 + require ( + github.com/blizzy78/varnamelen v0.7.0 // indirect github.com/client9/misspell v0.3.4 github.com/golangci/golangci-lint v1.45.2 github.com/jcchavezs/porto v0.4.0 diff --git a/tools/go.sum b/tools/go.sum index 38a5e094609..8569097b782 100644 --- a/tools/go.sum +++ b/tools/go.sum @@ -118,8 +118,8 @@ github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kB github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM= github.com/bkielbasa/cyclop v1.2.0 h1:7Jmnh0yL2DjKfw28p86YTd/B4lRGcNuu12sKE35sM7A= github.com/bkielbasa/cyclop v1.2.0/go.mod h1:qOI0yy6A7dYC4Zgsa72Ppm9kONl0RoIlPbzot9mhmeI= -github.com/blizzy78/varnamelen v0.6.1 h1:kttPCLzXFa+0nt++Cw9fb7GrSSM4KkyIAoX/vXsbuqA= -github.com/blizzy78/varnamelen v0.6.1/go.mod h1:zy2Eic4qWqjrxa60jG34cfL0VXcSwzUrIx68eJPb4Q8= +github.com/blizzy78/varnamelen v0.7.0 h1:Y7LzPAysnU3YAabhqXkP9/G9/r7Ks9fUEvOjXoXStlY= +github.com/blizzy78/varnamelen v0.7.0/go.mod h1:V9TzQZ4fLJ1DSrjVDfl89H7aMnTvKkApdHeyESmyR7k= github.com/bombsimon/wsl/v3 v3.3.0 h1:Mka/+kRLoQJq7g2rggtgQsjuI/K5Efd87WX96EWFxjM= github.com/bombsimon/wsl/v3 v3.3.0/go.mod h1:st10JtZYLE4D5sC7b8xV4zTKZwAQjCH/Hy2Pm1FNZIc= github.com/breml/bidichk v0.2.2 h1:w7QXnpH0eCBJm55zGCTJveZEkQBt6Fs5zThIdA6qQ9Y= From 6bfaa57472f78a4144fd9a568fb65afc5f6c9f15 Mon Sep 17 00:00:00 2001 From: bhautikpip Date: Mon, 28 Mar 2022 13:08:01 -0700 Subject: [PATCH 34/38] fix test failure --- samplers/aws/xray/remote_sampler_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samplers/aws/xray/remote_sampler_test.go b/samplers/aws/xray/remote_sampler_test.go index 80cf88785d6..f7da63d1eb9 100644 --- a/samplers/aws/xray/remote_sampler_test.go +++ b/samplers/aws/xray/remote_sampler_test.go @@ -25,5 +25,5 @@ func TestRemoteSamplerDescription(t *testing.T) { rs := &remoteSampler{} s := rs.Description() - assert.Equal(t, s, "AwsXrayRemoteSampler{remote sampling with AWS X-Ray}") + assert.Equal(t, s, "AWSXRayRemoteSampler{remote sampling with AWS X-Ray}") } From 9adbf5af2553871666884a744a200a7e1557cb87 Mon Sep 17 00:00:00 2001 From: Bhautik Pipaliya <56270044+bhautikpip@users.noreply.github.com> Date: Mon, 28 Mar 2022 13:54:05 -0700 Subject: [PATCH 35/38] Update samplers/aws/xray/internal/manifest.go Co-authored-by: Tyler Yahn --- samplers/aws/xray/internal/manifest.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samplers/aws/xray/internal/manifest.go b/samplers/aws/xray/internal/manifest.go index ec3b49aae71..ed6fb063d92 100644 --- a/samplers/aws/xray/internal/manifest.go +++ b/samplers/aws/xray/internal/manifest.go @@ -120,7 +120,7 @@ func (m *Manifest) RefreshManifestTargets(ctx context.Context) (refresh bool, er // Deep copy manifest object. manifest := m.deepCopy() - // Generate sampling statistics based on the data in temporary manifest + // Generate sampling statistics based on the data in temporary manifest. statistics, err := manifest.snapshots() if err != nil { return false, err From dfd027285bfd09043be20d55eb8b6580fabd1884 Mon Sep 17 00:00:00 2001 From: bhautikpip Date: Mon, 28 Mar 2022 18:02:27 -0700 Subject: [PATCH 36/38] added API description: minor change --- samplers/aws/xray/remote_sampler.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/samplers/aws/xray/remote_sampler.go b/samplers/aws/xray/remote_sampler.go index 31b18344a6c..4f50aa3316f 100644 --- a/samplers/aws/xray/remote_sampler.go +++ b/samplers/aws/xray/remote_sampler.go @@ -53,6 +53,8 @@ var _ sdktrace.Sampler = (*remoteSampler)(nil) // based on the sampling rules set by users on AWS X-Ray console. Sampler also periodically polls // sampling rules and sampling targets. // NOTE: ctx passed in NewRemoteSampler API is being used in background go routine. Cancellation to this context can kill the background go routine. +// serviceName refers to the name of the service equivalent to set by customers on AWS X-Ray console when adding sampling rules and +// cloudPlatform refers to the environment service is running on (EC2, EKS etc). // Guide on AWS X-Ray remote sampling implementation (https://aws-otel.github.io/docs/getting-started/remote-sampling#otel-remote-sampling-implementation-caveats). func NewRemoteSampler(ctx context.Context, serviceName string, cloudPlatform string, opts ...Option) (sdktrace.Sampler, error) { // Create new config based on options or set to default values. From 1c77778b01ba196c35ebe7187d78b7c12783a08e Mon Sep 17 00:00:00 2001 From: Bhautik Pipaliya <56270044+bhautikpip@users.noreply.github.com> Date: Tue, 29 Mar 2022 10:38:59 -0700 Subject: [PATCH 37/38] Update samplers/aws/xray/remote_sampler.go Co-authored-by: Tyler Yahn --- samplers/aws/xray/remote_sampler.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/samplers/aws/xray/remote_sampler.go b/samplers/aws/xray/remote_sampler.go index 4f50aa3316f..8b9898f8341 100644 --- a/samplers/aws/xray/remote_sampler.go +++ b/samplers/aws/xray/remote_sampler.go @@ -53,8 +53,8 @@ var _ sdktrace.Sampler = (*remoteSampler)(nil) // based on the sampling rules set by users on AWS X-Ray console. Sampler also periodically polls // sampling rules and sampling targets. // NOTE: ctx passed in NewRemoteSampler API is being used in background go routine. Cancellation to this context can kill the background go routine. -// serviceName refers to the name of the service equivalent to set by customers on AWS X-Ray console when adding sampling rules and -// cloudPlatform refers to the environment service is running on (EC2, EKS etc). +// serviceName refers to the name of the service equivalent to the one set in the AWS X-Ray console when adding sampling rules and +// cloudPlatform refers to the cloud platform the service is running on ("ec2", "ecs", "eks", "lambda", etc). // Guide on AWS X-Ray remote sampling implementation (https://aws-otel.github.io/docs/getting-started/remote-sampling#otel-remote-sampling-implementation-caveats). func NewRemoteSampler(ctx context.Context, serviceName string, cloudPlatform string, opts ...Option) (sdktrace.Sampler, error) { // Create new config based on options or set to default values. From 1fc8df8299194200c436c63549bcc35361ba0303 Mon Sep 17 00:00:00 2001 From: bhautikpip Date: Tue, 29 Mar 2022 11:12:13 -0700 Subject: [PATCH 38/38] minor change: modified matchers to return true directly --- samplers/aws/xray/internal/rule.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samplers/aws/xray/internal/rule.go b/samplers/aws/xray/internal/rule.go index afc022bf048..358a26b14e7 100644 --- a/samplers/aws/xray/internal/rule.go +++ b/samplers/aws/xray/internal/rule.go @@ -212,7 +212,7 @@ func (r *Rule) appliesTo(parameters sdktrace.SamplingParameters, serviceName str } } - return attributeMatcher && serviceNameMatcher && serviceTypeMatcher && HTTPMethodMatcher && HTTPHostMatcher && HTTPURLPathMatcher, nil + return true, nil } // attributeMatching performs a match on attributes set by users on AWS X-Ray console.