Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ddtrace/tracer: removed restriction of non-empty fields in sampling rules #1510

Merged
merged 5 commits into from
Oct 12, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 26 additions & 16 deletions ddtrace/tracer/doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,22 +9,25 @@
// tracer, simply call the start method along with an optional set of options.
// By default, the trace agent is considered to be found at "localhost:8126". In a
// setup where this would be different (let's say 127.0.0.1:1234), we could do:
// tracer.Start(tracer.WithAgentAddr("127.0.0.1:1234"))
// defer tracer.Stop()
//
// tracer.Start(tracer.WithAgentAddr("127.0.0.1:1234"))
// defer tracer.Stop()
//
// The tracing client can perform trace sampling. While the trace agent
// already samples traces to reduce bandwidth usage, client sampling reduces
// performance overhead. To make use of it, the package comes with a ready-to-use
// rate sampler that can be passed to the tracer. To use it and keep only 30% of the
// requests, one would do:
// s := tracer.NewRateSampler(0.3)
// tracer.Start(tracer.WithSampler(s))
//
// s := tracer.NewRateSampler(0.3)
// tracer.Start(tracer.WithSampler(s))
//
// More precise control of sampling rates can be configured using sampling rules.
// This can be applied based on span name, service or both, and is used to determine
// the sampling rate to apply. MaxPerSecond specifies max number of spans per second
// that can be sampled per the rule and applies only to sampling rules of type
// tracer.SamplingRuleSpan. If MaxPerSecond is not specified, the default is no limit.
//
// rules := []tracer.SamplingRule{
// // sample 10% of traces with the span name "web.request"
// tracer.NameRule("web.request", 0.1),
Expand All @@ -45,11 +48,14 @@
//
// Sampling rules can also be configured at runtime using the DD_TRACE_SAMPLING_RULES and
// DD_SPAN_SAMPLING_RULES environment variables. When set, it overrides rules set by tracer.WithSamplingRules.
// The value is a JSON array of objects. All rule objects must have a "sample_rate".
// For trace sampling rules the "name" and "service" fields are optional.
// For span sampling rules, at least one of the fields must be specified and must be a valid glob pattern,
// i.e. a string where "*" matches any contiguous substring, even the empty string,
// The value is a JSON array of objects.
// For trace sampling rules, the "sample_rate" field is required, the "name" and "service" fields are optional.
// For span sampling rules, the "name" and "service", if specified, must be a valid glob pattern,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So if neither name nor service are supplied what does the rule do? (And can we add that to the documentation here to clarify it for future readers)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to the RFC, and the implementation in other tracers there shouldn't be solid restrictions like we had before. I am confident that there should be a check to discard a rule like {"service":"*", "name":"*", "sampling_rate": 1} in whatever form it comes. If the customer wants to allow all sampling, it should be done without misusing this API. Wdyt?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I definitely agree that setting it to * for both fields is a clear mis-use of the API so restricting it makes sense to me. I'm still confused what the intended behavior of having an empty name and empty service name since I don't think any spans can have both be empty? So the rule would just never match anything?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On the contrary, empty means it "match all"

// i.e. a string where "*" matches any contiguous substring, even an empty string,
// and "?" character matches exactly one of any character.
// The "sample_rate" field is optional, and if not specified, defaults to "1.0", sampling 100% of the spans.
// The "max_per_second" field is optional, and if not specified, defaults to 0, keeping all the previously sampled spans.
//
// export DD_TRACE_SAMPLING_RULES='[{"name": "web.request", "sample_rate": 1.0}]'
// export DD_SPAN_SAMPLING_RULES='[{"service":"test.?","name": "web.*", "sample_rate": 1.0, "max_per_second":100}]'
//
Expand Down Expand Up @@ -80,15 +86,19 @@
// interfaces. An example alternate implementation is the MDCarrier in our gRPC integration.
//
// As an example, injecting a span's context into an HTTP request would look like this:
// req, err := http.NewRequest("GET", "http://example.com", nil)
// // ...
// err := tracer.Inject(span.Context(), tracer.HTTPHeadersCarrier(req.Header))
// // ...
// http.DefaultClient.Do(req)
//
// req, err := http.NewRequest("GET", "http://example.com", nil)
// // ...
// err := tracer.Inject(span.Context(), tracer.HTTPHeadersCarrier(req.Header))
// // ...
// http.DefaultClient.Do(req)
//
// Then, on the server side, to continue the trace one would do:
// sctx, err := tracer.Extract(tracer.HTTPHeadersCarrier(req.Header))
// // ...
// span := tracer.StartSpan("child.span", tracer.ChildOf(sctx))
//
// sctx, err := tracer.Extract(tracer.HTTPHeadersCarrier(req.Header))
// // ...
// span := tracer.StartSpan("child.span", tracer.ChildOf(sctx))
//
// In the same manner, any means can be used as a carrier to inject a context into a transport. Go's
// context can also be used as a means to transport spans within the same process. The methods
// StartSpanFromContext, ContextWithSpan and SpanFromContext exist for this reason.
Expand Down
26 changes: 20 additions & 6 deletions ddtrace/tracer/rules_sampler.go
Original file line number Diff line number Diff line change
Expand Up @@ -508,8 +508,12 @@ func unmarshalSamplingRules(b []byte, spanType SamplingRuleType) ([]SamplingRule
var errs []string
for i, v := range jsonRules {
if v.Rate == "" {
errs = append(errs, fmt.Sprintf("at index %d: rate not provided", i))
continue
if spanType == SamplingRuleSpan {
v.Rate = "1"
} else {
errs = append(errs, fmt.Sprintf("at index %d: rate not provided", i))
continue
}
}
rate, err := v.Rate.Float64()
if err != nil {
Expand All @@ -522,10 +526,6 @@ func unmarshalSamplingRules(b []byte, spanType SamplingRuleType) ([]SamplingRule
}
switch spanType {
case SamplingRuleSpan:
if v.Service == "" && v.Name == "" {
errs = append(errs, fmt.Sprintf("at index %d: ignoring rule %+v: service name and operation name are not provided", i, v))
continue
}
rules = append(rules, SamplingRule{
Service: globMatch(v.Service),
Name: globMatch(v.Name),
Expand All @@ -535,6 +535,20 @@ func unmarshalSamplingRules(b []byte, spanType SamplingRuleType) ([]SamplingRule
ruleType: SamplingRuleSpan,
})
case SamplingRuleTrace:
if v.Rate == "" {
errs = append(errs, fmt.Sprintf("at index %d: rate not provided", i))
continue
}
rate, err := v.Rate.Float64()
if err != nil {
errs = append(errs, fmt.Sprintf("at index %d: %v", i, err))
continue
}
if rate < 0.0 || rate > 1.0 {
errs = append(errs, fmt.Sprintf("at index %d: ignoring rule %+v: rate is out of [0.0, 1.0] range", i, v))
continue
}

switch {
case v.Service != "" && v.Name != "":
rules = append(rules, NameServiceRule(v.Name, v.Service, rate))
Expand Down
40 changes: 35 additions & 5 deletions ddtrace/tracer/sampler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,15 @@ func TestRuleEnvVars(t *testing.T) {
}, {
value: `[{"service": "abcd", "sample_rate": 1.0}]`,
ruleN: 1,
}, {
value: `[{"sample_rate": 1.0}, {"service": "abcd"}, {"name": "abcd"}, {}]`,
ruleN: 4,
}, {
value: `[{"service": "abcd", "name": "wxyz"}]`,
ruleN: 1,
}, {
value: `[{"sample_rate": 1.0}]`,
ruleN: 1,
}, {
value: `[{"service": "abcd", "sample_rate": 1.0},{"name": "wxyz", "sample_rate": 0.9},{"service": "efgh", "name": "lmnop", "sample_rate": 0.42}]`,
ruleN: 3,
Expand All @@ -298,9 +307,6 @@ func TestRuleEnvVars(t *testing.T) {
}, {
value: `not JSON at all`,
errStr: "\n\terror unmarshalling JSON: invalid character 'o' in literal null (expecting 'u')",
}, {
value: `[{"sample_rate": 1.0}]`,
errStr: "\n\tat index 0: ignoring rule {Service: Name: Rate:1.0 MaxPerSecond:0}: service name and operation name are not provided",
},
} {
t.Run(fmt.Sprintf("%v", i), func(t *testing.T) {
Expand All @@ -315,6 +321,7 @@ func TestRuleEnvVars(t *testing.T) {
})
}
})

t.Run("span-sampling-rules-regex", func(t *testing.T) {
assert := assert.New(t)
defer os.Unsetenv("DD_SPAN_SAMPLING_RULES")
Expand All @@ -323,21 +330,43 @@ func TestRuleEnvVars(t *testing.T) {
rules string
srvRegex string
nameRegex string
rate float64
}{
{
rules: `[{"name": "abcd?", "sample_rate": 1.0}]`,
srvRegex: "^.*$",
nameRegex: "^abcd.$",
rate: 1.0,
},
{
rules: `[{"sample_rate": 0.5}]`,
srvRegex: "^.*$",
nameRegex: "^.*$",
rate: 0.5,
},
{
rules: `[{"max_per_second":100}]`,
srvRegex: "^.*$",
nameRegex: "^.*$",
rate: 1,
},
{
rules: `[{"name": "abcd?"}]`,
srvRegex: "^.*$",
nameRegex: "^abcd.$",
rate: 1.0,
},
{
rules: `[{"service": "*abcd", "sample_rate": 1.0}]`,
rules: `[{"service": "*abcd", "sample_rate":0.5}]`,
nameRegex: "^.*$",
srvRegex: "^.*abcd$",
rate: 0.5,
},
{
rules: `[{"service": "*abcd", "sample_rate": 1.0}]`,
rules: `[{"service": "*abcd", "sample_rate": 0.5}]`,
nameRegex: "^.*$",
srvRegex: "^.*abcd$",
rate: 0.5,
},
} {
t.Run(fmt.Sprintf("%v", i), func(t *testing.T) {
Expand All @@ -346,6 +375,7 @@ func TestRuleEnvVars(t *testing.T) {
assert.NoError(err)
assert.Equal(tt.srvRegex, rules[0].Service.String())
assert.Equal(tt.nameRegex, rules[0].Name.String())
assert.Equal(tt.rate, rules[0].Rate)
})
}
})
Expand Down