Line data Source code
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 0 : void fail(absl::string_view msg) { 30 0 : auto& logger = Logger::Registry::getLog(Logger::Id::tracing); 31 0 : ENVOY_LOG_TO_LOGGER(logger, error, "Failed to parse sampling rules - {}", msg); 32 0 : } 33 : 34 0 : bool isValidRate(double n) { return n >= 0 && n <= 1.0; } 35 0 : bool isValidFixedTarget(double n) { return n >= 0 && static_cast<uint32_t>(n) == n; } 36 : 37 0 : bool validateRule(const ProtobufWkt::Struct& rule) { 38 0 : using ProtobufWkt::Value; 39 : 40 0 : const auto host_it = rule.fields().find(HostJsonKey); 41 0 : if (host_it != rule.fields().end() && 42 0 : host_it->second.kind_case() != Value::KindCase::kStringValue) { 43 0 : fail("host must be a string"); 44 0 : return false; 45 0 : } 46 : 47 0 : const auto http_method_it = rule.fields().find(HttpMethodJsonKey); 48 0 : if (http_method_it != rule.fields().end() && 49 0 : http_method_it->second.kind_case() != Value::KindCase::kStringValue) { 50 0 : fail("HTTP method must be a string"); 51 0 : return false; 52 0 : } 53 : 54 0 : const auto url_path_it = rule.fields().find(UrlPathJsonKey); 55 0 : if (url_path_it != rule.fields().end() && 56 0 : url_path_it->second.kind_case() != Value::KindCase::kStringValue) { 57 0 : fail("URL path must be a string"); 58 0 : return false; 59 0 : } 60 : 61 0 : const auto fixed_target_it = rule.fields().find(FixedTargetJsonKey); 62 0 : if (fixed_target_it == rule.fields().end() || 63 0 : fixed_target_it->second.kind_case() != Value::KindCase::kNumberValue || 64 0 : !isValidFixedTarget(fixed_target_it->second.number_value())) { 65 0 : fail("fixed target is missing or not a valid positive integer"); 66 0 : return false; 67 0 : } 68 : 69 0 : const auto rate_it = rule.fields().find(RateJsonKey); 70 0 : if (rate_it == rule.fields().end() || 71 0 : rate_it->second.kind_case() != Value::KindCase::kNumberValue || 72 0 : !isValidRate(rate_it->second.number_value())) { 73 0 : fail("rate is missing or not a valid positive floating number"); 74 0 : return false; 75 0 : } 76 0 : return true; 77 0 : } 78 : } // namespace 79 : 80 0 : LocalizedSamplingRule LocalizedSamplingRule::createDefault() { 81 0 : return {DefaultFixedTarget, DefaultRate}; 82 0 : } 83 : 84 0 : bool LocalizedSamplingRule::appliesTo(const SamplingRequest& request) const { 85 0 : return (request.host_.empty() || wildcardMatch(host_, request.host_)) && 86 0 : (request.http_method_.empty() || wildcardMatch(http_method_, request.http_method_)) && 87 0 : (request.http_url_.empty() || wildcardMatch(url_path_, request.http_url_)); 88 0 : } 89 : 90 : LocalizedSamplingManifest::LocalizedSamplingManifest(const std::string& rule_json) 91 0 : : default_rule_(LocalizedSamplingRule::createDefault()) { 92 0 : if (rule_json.empty()) { 93 0 : return; 94 0 : } 95 : 96 0 : ProtobufWkt::Struct document; 97 0 : TRY_NEEDS_AUDIT { MessageUtil::loadFromJson(rule_json, document); } 98 0 : END_TRY catch (EnvoyException& e) { 99 0 : fail("invalid JSON format"); 100 0 : return; 101 0 : } 102 : 103 0 : const auto version_it = document.fields().find(VersionJsonKey); 104 0 : if (version_it == document.fields().end()) { 105 0 : fail("missing version number"); 106 0 : return; 107 0 : } 108 : 109 0 : if (version_it->second.kind_case() != ProtobufWkt::Value::KindCase::kNumberValue || 110 0 : version_it->second.number_value() != SamplingFileVersion) { 111 0 : fail("wrong version number"); 112 0 : return; 113 0 : } 114 : 115 0 : const auto default_rule_it = document.fields().find(DefaultRuleJsonKey); 116 0 : if (default_rule_it == document.fields().end() || 117 0 : default_rule_it->second.kind_case() != ProtobufWkt::Value::KindCase::kStructValue) { 118 0 : fail("missing default rule"); 119 0 : return; 120 0 : } 121 : 122 : // extract default rule members 123 0 : auto& default_rule_object = default_rule_it->second.struct_value(); 124 0 : if (!validateRule(default_rule_object)) { 125 0 : return; 126 0 : } 127 : 128 0 : default_rule_.setRate(default_rule_object.fields().find(RateJsonKey)->second.number_value()); 129 0 : default_rule_.setFixedTarget(static_cast<uint32_t>( 130 0 : default_rule_object.fields().find(FixedTargetJsonKey)->second.number_value())); 131 : 132 0 : const auto custom_rules_it = document.fields().find(CustomRulesJsonKey); 133 0 : if (custom_rules_it == document.fields().end()) { 134 0 : return; 135 0 : } 136 : 137 0 : if (custom_rules_it->second.kind_case() != ProtobufWkt::Value::KindCase::kListValue) { 138 0 : fail("rules must be JSON array"); 139 0 : return; 140 0 : } 141 : 142 0 : for (auto& el : custom_rules_it->second.list_value().values()) { 143 0 : if (el.kind_case() != ProtobufWkt::Value::KindCase::kStructValue) { 144 0 : fail("rules array must be objects"); 145 0 : return; 146 0 : } 147 : 148 0 : auto& rule_json = el.struct_value(); 149 0 : if (!validateRule(rule_json)) { 150 0 : return; 151 0 : } 152 : 153 0 : LocalizedSamplingRule rule = LocalizedSamplingRule::createDefault(); 154 0 : const auto host_it = rule_json.fields().find(HostJsonKey); 155 0 : if (host_it != rule_json.fields().end()) { 156 0 : rule.setHost(host_it->second.string_value()); 157 0 : } 158 : 159 0 : const auto http_method_it = rule_json.fields().find(HttpMethodJsonKey); 160 0 : if (http_method_it != rule_json.fields().end()) { 161 0 : rule.setHttpMethod(http_method_it->second.string_value()); 162 0 : } 163 : 164 0 : const auto url_path_it = rule_json.fields().find(UrlPathJsonKey); 165 0 : if (url_path_it != rule_json.fields().end()) { 166 0 : rule.setUrlPath(url_path_it->second.string_value()); 167 0 : } 168 : 169 : // rate and fixed_target must exist because we validated this rule 170 0 : rule.setRate(rule_json.fields().find(RateJsonKey)->second.number_value()); 171 0 : rule.setFixedTarget( 172 0 : static_cast<uint32_t>(rule_json.fields().find(FixedTargetJsonKey)->second.number_value())); 173 : 174 0 : custom_rules_.push_back(std::move(rule)); 175 0 : } 176 0 : } 177 : 178 0 : bool LocalizedSamplingStrategy::shouldTrace(const SamplingRequest& sampling_request) { 179 0 : if (!manifest_.hasCustomRules()) { 180 0 : return shouldTrace(manifest_.defaultRule()); 181 0 : } 182 : 183 0 : for (auto&& rule : manifest_.customRules()) { 184 0 : if (rule.appliesTo(sampling_request)) { 185 0 : return shouldTrace(rule); 186 0 : } 187 0 : } 188 0 : return shouldTrace(manifest_.defaultRule()); 189 0 : } 190 : 191 0 : bool LocalizedSamplingStrategy::shouldTrace(LocalizedSamplingRule& rule) { 192 0 : const auto now = time_source_.monotonicTime(); 193 0 : if (rule.reservoir().take(now)) { 194 0 : return true; 195 0 : } 196 : 197 : // rule.rate() is a rational number between 0 and 1 198 0 : auto toss = random() % 100; 199 0 : if (toss < (100 * rule.rate())) { 200 0 : return true; 201 0 : } 202 : 203 0 : return false; 204 0 : } 205 : 206 : } // namespace XRay 207 : } // namespace Tracers 208 : } // namespace Extensions 209 : } // namespace Envoy