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