| /* |
| * |
| * Copyright 2022 gRPC authors. |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| * |
| */ |
| |
| package observability |
| |
| import ( |
| "context" |
| "encoding/json" |
| "errors" |
| "fmt" |
| "os" |
| "strings" |
| |
| gcplogging "cloud.google.com/go/logging" |
| "golang.org/x/oauth2/google" |
| "google.golang.org/grpc/internal/envconfig" |
| ) |
| |
| const envProjectID = "GOOGLE_CLOUD_PROJECT" |
| |
| // fetchDefaultProjectID fetches the default GCP project id from environment. |
| func fetchDefaultProjectID(ctx context.Context) string { |
| // Step 1: Check ENV var |
| if s := os.Getenv(envProjectID); s != "" { |
| logger.Infof("Found project ID from env %v: %v", envProjectID, s) |
| return s |
| } |
| // Step 2: Check default credential |
| credentials, err := google.FindDefaultCredentials(ctx, gcplogging.WriteScope) |
| if err != nil { |
| logger.Infof("Failed to locate Google Default Credential: %v", err) |
| return "" |
| } |
| if credentials.ProjectID == "" { |
| logger.Infof("Failed to find project ID in default credential: %v", err) |
| return "" |
| } |
| logger.Infof("Found project ID from Google Default Credential: %v", credentials.ProjectID) |
| return credentials.ProjectID |
| } |
| |
| // validateMethodString validates whether the string passed in is a valid |
| // pattern. |
| func validateMethodString(method string) error { |
| if strings.HasPrefix(method, "/") { |
| return errors.New("cannot have a leading slash") |
| } |
| serviceMethod := strings.Split(method, "/") |
| if len(serviceMethod) != 2 { |
| return errors.New("/ must come in between service and method, only one /") |
| } |
| if serviceMethod[1] == "" { |
| return errors.New("method name must be non empty") |
| } |
| if serviceMethod[0] == "*" { |
| return errors.New("cannot have service wildcard * i.e. (*/m)") |
| } |
| return nil |
| } |
| |
| func validateLogEventMethod(methods []string, exclude bool) error { |
| for _, method := range methods { |
| if method == "*" { |
| if exclude { |
| return errors.New("cannot have exclude and a '*' wildcard") |
| } |
| continue |
| } |
| if err := validateMethodString(method); err != nil { |
| return fmt.Errorf("invalid method string: %v, err: %v", method, err) |
| } |
| } |
| return nil |
| } |
| |
| func validateLoggingEvents(config *config) error { |
| if config.CloudLogging == nil { |
| return nil |
| } |
| for _, clientRPCEvent := range config.CloudLogging.ClientRPCEvents { |
| if err := validateLogEventMethod(clientRPCEvent.Methods, clientRPCEvent.Exclude); err != nil { |
| return fmt.Errorf("error in clientRPCEvent method: %v", err) |
| } |
| } |
| for _, serverRPCEvent := range config.CloudLogging.ServerRPCEvents { |
| if err := validateLogEventMethod(serverRPCEvent.Methods, serverRPCEvent.Exclude); err != nil { |
| return fmt.Errorf("error in serverRPCEvent method: %v", err) |
| } |
| } |
| return nil |
| } |
| |
| // unmarshalAndVerifyConfig unmarshals a json string representing an |
| // observability config into its internal go format, and also verifies the |
| // configuration's fields for validity. |
| func unmarshalAndVerifyConfig(rawJSON json.RawMessage) (*config, error) { |
| var config config |
| if err := json.Unmarshal(rawJSON, &config); err != nil { |
| return nil, fmt.Errorf("error parsing observability config: %v", err) |
| } |
| if err := validateLoggingEvents(&config); err != nil { |
| return nil, fmt.Errorf("error parsing observability config: %v", err) |
| } |
| if config.CloudTrace != nil && (config.CloudTrace.SamplingRate > 1 || config.CloudTrace.SamplingRate < 0) { |
| return nil, fmt.Errorf("error parsing observability config: invalid cloud trace sampling rate %v", config.CloudTrace.SamplingRate) |
| } |
| logger.Infof("Parsed ObservabilityConfig: %+v", &config) |
| return &config, nil |
| } |
| |
| func parseObservabilityConfig() (*config, error) { |
| if f := envconfig.ObservabilityConfigFile; f != "" { |
| if envconfig.ObservabilityConfig != "" { |
| logger.Warning("Ignoring GRPC_GCP_OBSERVABILITY_CONFIG and using GRPC_GCP_OBSERVABILITY_CONFIG_FILE contents.") |
| } |
| content, err := os.ReadFile(f) |
| if err != nil { |
| return nil, fmt.Errorf("error reading observability configuration file %q: %v", f, err) |
| } |
| return unmarshalAndVerifyConfig(content) |
| } else if envconfig.ObservabilityConfig != "" { |
| return unmarshalAndVerifyConfig([]byte(envconfig.ObservabilityConfig)) |
| } |
| // If the ENV var doesn't exist, do nothing |
| return nil, nil |
| } |
| |
| func ensureProjectIDInObservabilityConfig(ctx context.Context, config *config) error { |
| if config.ProjectID == "" { |
| // Try to fetch the GCP project id |
| projectID := fetchDefaultProjectID(ctx) |
| if projectID == "" { |
| return fmt.Errorf("empty destination project ID") |
| } |
| config.ProjectID = projectID |
| } |
| return nil |
| } |
| |
| type clientRPCEvents struct { |
| // Methods is a list of strings which can select a group of methods. By |
| // default, the list is empty, matching no methods. |
| // |
| // The value of the method is in the form of <service>/<method>. |
| // |
| // "*" is accepted as a wildcard for: |
| // 1. The method name. If the value is <service>/*, it matches all |
| // methods in the specified service. |
| // 2. The whole value of the field which matches any <service>/<method>. |
| // It’s not supported when Exclude is true. |
| // 3. The * wildcard cannot be used on the service name independently, |
| // */<method> is not supported. |
| // |
| // The service name, when specified, must be the fully qualified service |
| // name, including the package name. |
| // |
| // Examples: |
| // 1."goo.Foo/Bar" selects only the method "Bar" from service "goo.Foo", |
| // here “goo” is the package name. |
| // 2."goo.Foo/*" selects all methods from service "goo.Foo" |
| // 3. "*" selects all methods from all services. |
| Methods []string `json:"methods,omitempty"` |
| // Exclude represents whether the methods denoted by Methods should be |
| // excluded from logging. The default value is false, meaning the methods |
| // denoted by Methods are included in the logging. If Exclude is true, the |
| // wildcard `*` cannot be used as value of an entry in Methods. |
| Exclude bool `json:"exclude,omitempty"` |
| // MaxMetadataBytes is the maximum number of bytes of each header to log. If |
| // the size of the metadata is greater than the defined limit, content past |
| // the limit will be truncated. The default value is 0. |
| MaxMetadataBytes int `json:"max_metadata_bytes"` |
| // MaxMessageBytes is the maximum number of bytes of each message to log. If |
| // the size of the message is greater than the defined limit, content past |
| // the limit will be truncated. The default value is 0. |
| MaxMessageBytes int `json:"max_message_bytes"` |
| } |
| |
| type serverRPCEvents struct { |
| // Methods is a list of strings which can select a group of methods. By |
| // default, the list is empty, matching no methods. |
| // |
| // The value of the method is in the form of <service>/<method>. |
| // |
| // "*" is accepted as a wildcard for: |
| // 1. The method name. If the value is <service>/*, it matches all |
| // methods in the specified service. |
| // 2. The whole value of the field which matches any <service>/<method>. |
| // It’s not supported when Exclude is true. |
| // 3. The * wildcard cannot be used on the service name independently, |
| // */<method> is not supported. |
| // |
| // The service name, when specified, must be the fully qualified service |
| // name, including the package name. |
| // |
| // Examples: |
| // 1."goo.Foo/Bar" selects only the method "Bar" from service "goo.Foo", |
| // here “goo” is the package name. |
| // 2."goo.Foo/*" selects all methods from service "goo.Foo" |
| // 3. "*" selects all methods from all services. |
| Methods []string `json:"methods,omitempty"` |
| // Exclude represents whether the methods denoted by Methods should be |
| // excluded from logging. The default value is false, meaning the methods |
| // denoted by Methods are included in the logging. If Exclude is true, the |
| // wildcard `*` cannot be used as value of an entry in Methods. |
| Exclude bool `json:"exclude,omitempty"` |
| // MaxMetadataBytes is the maximum number of bytes of each header to log. If |
| // the size of the metadata is greater than the defined limit, content past |
| // the limit will be truncated. The default value is 0. |
| MaxMetadataBytes int `json:"max_metadata_bytes"` |
| // MaxMessageBytes is the maximum number of bytes of each message to log. If |
| // the size of the message is greater than the defined limit, content past |
| // the limit will be truncated. The default value is 0. |
| MaxMessageBytes int `json:"max_message_bytes"` |
| } |
| |
| type cloudLogging struct { |
| // ClientRPCEvents represents the configuration for outgoing RPC's from the |
| // binary. The client_rpc_events configs are evaluated in text order, the |
| // first one matched is used. If an RPC doesn't match an entry, it will |
| // continue on to the next entry in the list. |
| ClientRPCEvents []clientRPCEvents `json:"client_rpc_events,omitempty"` |
| |
| // ServerRPCEvents represents the configuration for incoming RPC's to the |
| // binary. The server_rpc_events configs are evaluated in text order, the |
| // first one matched is used. If an RPC doesn't match an entry, it will |
| // continue on to the next entry in the list. |
| ServerRPCEvents []serverRPCEvents `json:"server_rpc_events,omitempty"` |
| } |
| |
| type cloudMonitoring struct{} |
| |
| type cloudTrace struct { |
| // SamplingRate is the global setting that controls the probability of an RPC |
| // being traced. For example, 0.05 means there is a 5% chance for an RPC to |
| // be traced, 1.0 means trace every call, 0 means don’t start new traces. By |
| // default, the sampling_rate is 0. |
| SamplingRate float64 `json:"sampling_rate,omitempty"` |
| } |
| |
| type config struct { |
| // ProjectID is the destination GCP project identifier for uploading log |
| // entries. If empty, the gRPC Observability plugin will attempt to fetch |
| // the project_id from the GCP environment variables, or from the default |
| // credentials. If not found, the observability init functions will return |
| // an error. |
| ProjectID string `json:"project_id,omitempty"` |
| // CloudLogging defines the logging options. If not present, logging is disabled. |
| CloudLogging *cloudLogging `json:"cloud_logging,omitempty"` |
| // CloudMonitoring determines whether or not metrics are enabled based on |
| // whether it is present or not. If present, monitoring will be enabled, if |
| // not present, monitoring is disabled. |
| CloudMonitoring *cloudMonitoring `json:"cloud_monitoring,omitempty"` |
| // CloudTrace defines the tracing options. When present, tracing is enabled |
| // with default configurations. When absent, the tracing is disabled. |
| CloudTrace *cloudTrace `json:"cloud_trace,omitempty"` |
| // Labels are applied to cloud logging, monitoring, and trace. |
| Labels map[string]string `json:"labels,omitempty"` |
| } |