1
#include "source/extensions/tracers/xray/localized_sampling.h"
2

            
3
#include "source/common/http/exception.h"
4
#include "source/common/protobuf/utility.h"
5
#include "source/extensions/tracers/xray/util.h"
6

            
7
namespace Envoy {
8
namespace Extensions {
9
namespace Tracers {
10
namespace XRay {
11

            
12
// Corresponds to 5% sampling rate when no custom rules are applied.
13
constexpr double DefaultRate = 0.05;
14
// Determines how many requests to sample per second before default
15
// sampling rate kicks in when no custom rules are applied.
16
constexpr int DefaultFixedTarget = 1;
17
// The required 'version' of sampling manifest file when localized sampling is applied.
18
constexpr int SamplingFileVersion = 2;
19
constexpr auto VersionJsonKey = "version";
20
constexpr auto DefaultRuleJsonKey = "default";
21
constexpr auto FixedTargetJsonKey = "fixed_target";
22
constexpr auto RateJsonKey = "rate";
23
constexpr auto CustomRulesJsonKey = "rules";
24
constexpr auto HostJsonKey = "host";
25
constexpr auto HttpMethodJsonKey = "http_method";
26
constexpr auto UrlPathJsonKey = "url_path";
27

            
28
namespace {
29
19
void fail(absl::string_view msg) {
30
19
  auto& logger = Logger::Registry::getLog(Logger::Id::tracing);
31
19
  ENVOY_LOG_TO_LOGGER(logger, error, "Failed to parse sampling rules - {}", msg);
32
19
}
33

            
34
29
bool isValidRate(double n) { return n >= 0 && n <= 1.0; }
35
34
bool isValidFixedTarget(double n) { return n >= 0 && static_cast<uint32_t>(n) == n; }
36

            
37
39
bool validateRule(const Protobuf::Struct& rule) {
38
39
  using Protobuf::Value;
39

            
40
39
  const auto host_it = rule.fields().find(HostJsonKey);
41
39
  if (host_it != rule.fields().end() &&
42
39
      host_it->second.kind_case() != Value::KindCase::kStringValue) {
43
1
    fail("host must be a string");
44
1
    return false;
45
1
  }
46

            
47
38
  const auto http_method_it = rule.fields().find(HttpMethodJsonKey);
48
38
  if (http_method_it != rule.fields().end() &&
49
38
      http_method_it->second.kind_case() != Value::KindCase::kStringValue) {
50
1
    fail("HTTP method must be a string");
51
1
    return false;
52
1
  }
53

            
54
37
  const auto url_path_it = rule.fields().find(UrlPathJsonKey);
55
37
  if (url_path_it != rule.fields().end() &&
56
37
      url_path_it->second.kind_case() != Value::KindCase::kStringValue) {
57
1
    fail("URL path must be a string");
58
1
    return false;
59
1
  }
60

            
61
36
  const auto fixed_target_it = rule.fields().find(FixedTargetJsonKey);
62
36
  if (fixed_target_it == rule.fields().end() ||
63
36
      fixed_target_it->second.kind_case() != Value::KindCase::kNumberValue ||
64
36
      !isValidFixedTarget(fixed_target_it->second.number_value())) {
65
4
    fail("fixed target is missing or not a valid positive integer");
66
4
    return false;
67
4
  }
68

            
69
32
  const auto rate_it = rule.fields().find(RateJsonKey);
70
32
  if (rate_it == rule.fields().end() ||
71
32
      rate_it->second.kind_case() != Value::KindCase::kNumberValue ||
72
32
      !isValidRate(rate_it->second.number_value())) {
73
7
    fail("rate is missing or not a valid positive floating number");
74
7
    return false;
75
7
  }
76
25
  return true;
77
32
}
78
} // namespace
79

            
80
44
LocalizedSamplingRule LocalizedSamplingRule::createDefault() {
81
44
  return {DefaultFixedTarget, DefaultRate};
82
44
}
83

            
84
51
bool LocalizedSamplingRule::appliesTo(const SamplingRequest& request) const {
85
51
  return (request.host_.empty() || wildcardMatch(host_, request.host_)) &&
86
51
         (request.http_method_.empty() || wildcardMatch(http_method_, request.http_method_)) &&
87
51
         (request.http_url_.empty() || wildcardMatch(url_path_, request.http_url_));
88
51
}
89

            
90
LocalizedSamplingManifest::LocalizedSamplingManifest(const std::string& rule_json)
91
38
    : default_rule_(LocalizedSamplingRule::createDefault()) {
92
38
  if (rule_json.empty()) {
93
10
    return;
94
10
  }
95

            
96
28
  Protobuf::Struct document;
97
28
  TRY_NEEDS_AUDIT { MessageUtil::loadFromJson(rule_json, document); }
98
28
  END_TRY catch (EnvoyException& e) {
99
2
    fail("invalid JSON format");
100
2
    return;
101
2
  }
102

            
103
26
  const auto version_it = document.fields().find(VersionJsonKey);
104
26
  if (version_it == document.fields().end()) {
105
1
    fail("missing version number");
106
1
    return;
107
1
  }
108

            
109
25
  if (version_it->second.kind_case() != Protobuf::Value::KindCase::kNumberValue ||
110
25
      version_it->second.number_value() != SamplingFileVersion) {
111
1
    fail("wrong version number");
112
1
    return;
113
1
  }
114

            
115
24
  const auto default_rule_it = document.fields().find(DefaultRuleJsonKey);
116
24
  if (default_rule_it == document.fields().end() ||
117
24
      default_rule_it->second.kind_case() != Protobuf::Value::KindCase::kStructValue) {
118
1
    fail("missing default rule");
119
1
    return;
120
1
  }
121

            
122
  // extract default rule members
123
23
  auto& default_rule_object = default_rule_it->second.struct_value();
124
23
  if (!validateRule(default_rule_object)) {
125
4
    return;
126
4
  }
127

            
128
19
  default_rule_.setRate(default_rule_object.fields().find(RateJsonKey)->second.number_value());
129
19
  default_rule_.setFixedTarget(static_cast<uint32_t>(
130
19
      default_rule_object.fields().find(FixedTargetJsonKey)->second.number_value()));
131

            
132
19
  const auto custom_rules_it = document.fields().find(CustomRulesJsonKey);
133
19
  if (custom_rules_it == document.fields().end()) {
134
2
    return;
135
2
  }
136

            
137
17
  if (custom_rules_it->second.kind_case() != Protobuf::Value::KindCase::kListValue) {
138
    fail("rules must be JSON array");
139
    return;
140
  }
141

            
142
17
  for (auto& el : custom_rules_it->second.list_value().values()) {
143
16
    if (el.kind_case() != Protobuf::Value::KindCase::kStructValue) {
144
      fail("rules array must be objects");
145
      return;
146
    }
147

            
148
16
    auto& rule_json = el.struct_value();
149
16
    if (!validateRule(rule_json)) {
150
10
      return;
151
10
    }
152

            
153
6
    LocalizedSamplingRule rule = LocalizedSamplingRule::createDefault();
154
6
    const auto host_it = rule_json.fields().find(HostJsonKey);
155
6
    if (host_it != rule_json.fields().end()) {
156
6
      rule.setHost(host_it->second.string_value());
157
6
    }
158

            
159
6
    const auto http_method_it = rule_json.fields().find(HttpMethodJsonKey);
160
6
    if (http_method_it != rule_json.fields().end()) {
161
6
      rule.setHttpMethod(http_method_it->second.string_value());
162
6
    }
163

            
164
6
    const auto url_path_it = rule_json.fields().find(UrlPathJsonKey);
165
6
    if (url_path_it != rule_json.fields().end()) {
166
6
      rule.setUrlPath(url_path_it->second.string_value());
167
6
    }
168

            
169
    // rate and fixed_target must exist because we validated this rule
170
6
    rule.setRate(rule_json.fields().find(RateJsonKey)->second.number_value());
171
6
    rule.setFixedTarget(
172
6
        static_cast<uint32_t>(rule_json.fields().find(FixedTargetJsonKey)->second.number_value()));
173

            
174
6
    custom_rules_.push_back(std::move(rule));
175
6
  }
176
17
}
177

            
178
77
bool LocalizedSamplingStrategy::shouldTrace(const SamplingRequest& sampling_request) {
179
77
  if (!manifest_.hasCustomRules()) {
180
26
    return shouldTrace(manifest_.defaultRule());
181
26
  }
182

            
183
51
  for (auto&& rule : manifest_.customRules()) {
184
51
    if (rule.appliesTo(sampling_request)) {
185
21
      return shouldTrace(rule);
186
21
    }
187
51
  }
188
30
  return shouldTrace(manifest_.defaultRule());
189
51
}
190

            
191
77
bool LocalizedSamplingStrategy::shouldTrace(LocalizedSamplingRule& rule) {
192
77
  const auto now = time_source_.monotonicTime();
193
77
  if (rule.reservoir().take(now)) {
194
7
    return true;
195
7
  }
196

            
197
  // rule.rate() is a rational number between 0 and 1
198
70
  auto toss = random() % 100;
199
70
  if (toss < (100 * rule.rate())) {
200
9
    return true;
201
9
  }
202

            
203
61
  return false;
204
70
}
205

            
206
} // namespace XRay
207
} // namespace Tracers
208
} // namespace Extensions
209
} // namespace Envoy