1 /* 2 * Copyright 2022 The gRPC Authors 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package io.grpc.gcp.observability; 18 19 import static com.google.common.base.Preconditions.checkArgument; 20 21 import com.google.cloud.ServiceOptions; 22 import com.google.common.base.Charsets; 23 import com.google.common.collect.ImmutableList; 24 import com.google.common.collect.ImmutableMap; 25 import com.google.common.collect.ImmutableSet; 26 import io.grpc.internal.JsonParser; 27 import io.grpc.internal.JsonUtil; 28 import io.opencensus.trace.Sampler; 29 import io.opencensus.trace.samplers.Samplers; 30 import java.io.IOException; 31 import java.nio.file.Files; 32 import java.nio.file.Paths; 33 import java.util.Collections; 34 import java.util.List; 35 import java.util.Map; 36 import java.util.logging.Level; 37 import java.util.logging.Logger; 38 import java.util.regex.Matcher; 39 import java.util.regex.Pattern; 40 41 /** 42 * gRPC GcpObservability configuration processor. 43 */ 44 final class ObservabilityConfigImpl implements ObservabilityConfig { 45 private static final Logger logger = Logger 46 .getLogger(ObservabilityConfigImpl.class.getName()); 47 private static final String CONFIG_ENV_VAR_NAME = "GRPC_GCP_OBSERVABILITY_CONFIG"; 48 private static final String CONFIG_FILE_ENV_VAR_NAME = "GRPC_GCP_OBSERVABILITY_CONFIG_FILE"; 49 // Tolerance for floating-point comparisons. 50 private static final double EPSILON = 1e-6; 51 52 private static final Pattern METHOD_NAME_REGEX = 53 Pattern.compile("^([*])|((([\\w.]+)/((?:\\w+)|[*])))$"); 54 55 private boolean enableCloudLogging = false; 56 private boolean enableCloudMonitoring = false; 57 private boolean enableCloudTracing = false; 58 private String projectId = null; 59 60 private List<LogFilter> clientLogFilters; 61 private List<LogFilter> serverLogFilters; 62 private Sampler sampler; 63 private Map<String, String> customTags; 64 getInstance()65 static ObservabilityConfigImpl getInstance() throws IOException { 66 ObservabilityConfigImpl config = new ObservabilityConfigImpl(); 67 String configFile = System.getenv(CONFIG_FILE_ENV_VAR_NAME); 68 if (configFile != null) { 69 config.parseFile(configFile); 70 } else { 71 config.parse(System.getenv(CONFIG_ENV_VAR_NAME)); 72 } 73 return config; 74 } 75 parseFile(String configFile)76 void parseFile(String configFile) throws IOException { 77 String configFileContent = 78 new String(Files.readAllBytes(Paths.get(configFile)), Charsets.UTF_8); 79 checkArgument(!configFileContent.isEmpty(), CONFIG_FILE_ENV_VAR_NAME + " is empty!"); 80 parse(configFileContent); 81 } 82 83 @SuppressWarnings("unchecked") parse(String config)84 void parse(String config) throws IOException { 85 checkArgument(config != null, CONFIG_ENV_VAR_NAME + " value is null!"); 86 parseConfig((Map<String, ?>) JsonParser.parse(config)); 87 } 88 parseConfig(Map<String, ?> config)89 private void parseConfig(Map<String, ?> config) { 90 checkArgument(config != null, "Invalid configuration"); 91 if (config.isEmpty()) { 92 clientLogFilters = Collections.emptyList(); 93 serverLogFilters = Collections.emptyList(); 94 customTags = Collections.emptyMap(); 95 return; 96 } 97 projectId = fetchProjectId(JsonUtil.getString(config, "project_id")); 98 99 Map<String, ?> rawCloudLoggingObject = JsonUtil.getObject(config, "cloud_logging"); 100 if (rawCloudLoggingObject != null) { 101 enableCloudLogging = true; 102 ImmutableList.Builder<LogFilter> clientFiltersBuilder = new ImmutableList.Builder<>(); 103 ImmutableList.Builder<LogFilter> serverFiltersBuilder = new ImmutableList.Builder<>(); 104 parseLoggingObject(rawCloudLoggingObject, clientFiltersBuilder, serverFiltersBuilder); 105 clientLogFilters = clientFiltersBuilder.build(); 106 serverLogFilters = serverFiltersBuilder.build(); 107 } 108 109 Map<String, ?> rawCloudMonitoringObject = JsonUtil.getObject(config, "cloud_monitoring"); 110 if (rawCloudMonitoringObject != null) { 111 enableCloudMonitoring = true; 112 } 113 114 Map<String, ?> rawCloudTracingObject = JsonUtil.getObject(config, "cloud_trace"); 115 if (rawCloudTracingObject != null) { 116 enableCloudTracing = true; 117 sampler = parseTracingObject(rawCloudTracingObject); 118 } 119 120 Map<String, ?> rawCustomTagsObject = JsonUtil.getObject(config, "labels"); 121 if (rawCustomTagsObject != null) { 122 customTags = parseCustomTags(rawCustomTagsObject); 123 } 124 125 if (clientLogFilters == null) { 126 clientLogFilters = Collections.emptyList(); 127 } 128 if (serverLogFilters == null) { 129 serverLogFilters = Collections.emptyList(); 130 } 131 if (customTags == null) { 132 customTags = Collections.emptyMap(); 133 } 134 } 135 fetchProjectId(String configProjectId)136 private static String fetchProjectId(String configProjectId) { 137 // If project_id is not specified in config, get default GCP project id from the environment 138 String projectId = configProjectId != null ? configProjectId : getDefaultGcpProjectId(); 139 checkArgument(projectId != null, "Unable to detect project_id"); 140 logger.log(Level.FINEST, "Found project ID : ", projectId); 141 return projectId; 142 } 143 getDefaultGcpProjectId()144 private static String getDefaultGcpProjectId() { 145 return ServiceOptions.getDefaultProjectId(); 146 } 147 parseLoggingObject( Map<String, ?> rawLoggingConfig, ImmutableList.Builder<LogFilter> clientFilters, ImmutableList.Builder<LogFilter> serverFilters)148 private static void parseLoggingObject( 149 Map<String, ?> rawLoggingConfig, 150 ImmutableList.Builder<LogFilter> clientFilters, 151 ImmutableList.Builder<LogFilter> serverFilters) { 152 parseRpcEvents(JsonUtil.getList(rawLoggingConfig, "client_rpc_events"), clientFilters); 153 parseRpcEvents(JsonUtil.getList(rawLoggingConfig, "server_rpc_events"), serverFilters); 154 } 155 parseTracingObject(Map<String, ?> rawCloudTracingConfig)156 private static Sampler parseTracingObject(Map<String, ?> rawCloudTracingConfig) { 157 Sampler defaultSampler = Samplers.probabilitySampler(0.0); 158 Double samplingRate = JsonUtil.getNumberAsDouble(rawCloudTracingConfig, "sampling_rate"); 159 if (samplingRate == null) { 160 return defaultSampler; 161 } 162 checkArgument(samplingRate >= 0.0 && samplingRate <= 1.0, 163 "'sampling_rate' needs to be between [0.0, 1.0]"); 164 // Using alwaysSample() instead of probabilitySampler() because according to 165 // {@link io.opencensus.trace.samplers.ProbabilitySampler#shouldSample} 166 // there is a (very) small chance of *not* sampling if probability = 1.00. 167 return 1 - samplingRate < EPSILON ? Samplers.alwaysSample() 168 : Samplers.probabilitySampler(samplingRate); 169 } 170 parseCustomTags(Map<String, ?> rawCustomTags)171 private static Map<String, String> parseCustomTags(Map<String, ?> rawCustomTags) { 172 ImmutableMap.Builder<String, String> builder = new ImmutableMap.Builder<>(); 173 for (Map.Entry<String, ?> entry: rawCustomTags.entrySet()) { 174 checkArgument( 175 entry.getValue() instanceof String, 176 "'labels' needs to be a map of <string, string>"); 177 builder.put(entry.getKey(), (String) entry.getValue()); 178 } 179 return builder.build(); 180 } 181 parseRpcEvents(List<?> rpcEvents, ImmutableList.Builder<LogFilter> filters)182 private static void parseRpcEvents(List<?> rpcEvents, ImmutableList.Builder<LogFilter> filters) { 183 if (rpcEvents == null) { 184 return; 185 } 186 List<Map<String, ?>> jsonRpcEvents = JsonUtil.checkObjectList(rpcEvents); 187 for (Map<String, ?> jsonClientRpcEvent : jsonRpcEvents) { 188 filters.add(parseJsonLogFilter(jsonClientRpcEvent)); 189 } 190 } 191 parseJsonLogFilter(Map<String, ?> logFilterMap)192 private static LogFilter parseJsonLogFilter(Map<String, ?> logFilterMap) { 193 ImmutableSet.Builder<String> servicesSetBuilder = new ImmutableSet.Builder<>(); 194 ImmutableSet.Builder<String> methodsSetBuilder = new ImmutableSet.Builder<>(); 195 boolean wildCardFilter = false; 196 197 boolean excludeFilter = 198 Boolean.TRUE.equals(JsonUtil.getBoolean(logFilterMap, "exclude")); 199 List<String> methodsList = JsonUtil.getListOfStrings(logFilterMap, "methods"); 200 if (methodsList != null) { 201 wildCardFilter = extractMethodOrServicePattern( 202 methodsList, excludeFilter, servicesSetBuilder, methodsSetBuilder); 203 } 204 Integer maxHeaderBytes = JsonUtil.getNumberAsInteger(logFilterMap, "max_metadata_bytes"); 205 Integer maxMessageBytes = JsonUtil.getNumberAsInteger(logFilterMap, "max_message_bytes"); 206 207 return new LogFilter( 208 servicesSetBuilder.build(), 209 methodsSetBuilder.build(), 210 wildCardFilter, 211 maxHeaderBytes != null ? maxHeaderBytes.intValue() : 0, 212 maxMessageBytes != null ? maxMessageBytes.intValue() : 0, 213 excludeFilter); 214 } 215 extractMethodOrServicePattern(List<String> patternList, boolean exclude, ImmutableSet.Builder<String> servicesSetBuilder, ImmutableSet.Builder<String> methodsSetBuilder)216 private static boolean extractMethodOrServicePattern(List<String> patternList, boolean exclude, 217 ImmutableSet.Builder<String> servicesSetBuilder, 218 ImmutableSet.Builder<String> methodsSetBuilder) { 219 boolean globalFilter = false; 220 for (String methodOrServicePattern : patternList) { 221 Matcher matcher = METHOD_NAME_REGEX.matcher(methodOrServicePattern); 222 checkArgument( 223 matcher.matches(), "invalid service or method filter : " + methodOrServicePattern); 224 if ("*".equals(methodOrServicePattern)) { 225 checkArgument(!exclude, "cannot have 'exclude' and '*' wildcard in the same filter"); 226 globalFilter = true; 227 } else if ("*".equals(matcher.group(5))) { 228 String service = matcher.group(4); 229 servicesSetBuilder.add(service); 230 } else { 231 methodsSetBuilder.add(methodOrServicePattern); 232 } 233 } 234 return globalFilter; 235 } 236 237 @Override isEnableCloudLogging()238 public boolean isEnableCloudLogging() { 239 return enableCloudLogging; 240 } 241 242 @Override isEnableCloudMonitoring()243 public boolean isEnableCloudMonitoring() { 244 return enableCloudMonitoring; 245 } 246 247 @Override isEnableCloudTracing()248 public boolean isEnableCloudTracing() { 249 return enableCloudTracing; 250 } 251 252 @Override getProjectId()253 public String getProjectId() { 254 return projectId; 255 } 256 257 @Override getClientLogFilters()258 public List<LogFilter> getClientLogFilters() { 259 return clientLogFilters; 260 } 261 262 @Override getServerLogFilters()263 public List<LogFilter> getServerLogFilters() { 264 return serverLogFilters; 265 } 266 267 @Override getSampler()268 public Sampler getSampler() { 269 return sampler; 270 } 271 272 @Override getCustomTags()273 public Map<String, String> getCustomTags() { 274 return customTags; 275 } 276 } 277