-
Notifications
You must be signed in to change notification settings - Fork 4.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
gcp/observability: implement public preview config syntax, logging schema, and exposed metrics #5704
Merged
Merged
gcp/observability: implement public preview config syntax, logging schema, and exposed metrics #5704
Changes from 7 commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
9b7e76d
Observability changes for public preview
zasweq 6a49085
Responded to Doug's comments
zasweq 77d6555
Responded to Doug's comments
zasweq 1c60786
Responded to Doug's comments
zasweq b18bf95
get rid of non determinism in expectation in logging test for truncat…
zasweq 7e8c2f9
Responded to Doug's comments
zasweq fb87d70
Offline discussion with Doug
zasweq e820693
Add check from offline discussion to make check more robust
zasweq 7479221
Added conditional to warning log
zasweq File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -21,86 +21,23 @@ package observability | |
import ( | ||
"context" | ||
"encoding/json" | ||
"errors" | ||
"fmt" | ||
"io/ioutil" | ||
"os" | ||
"regexp" | ||
|
||
gcplogging "cloud.google.com/go/logging" | ||
"golang.org/x/oauth2/google" | ||
"google.golang.org/grpc/internal/envconfig" | ||
) | ||
|
||
const ( | ||
envObservabilityConfig = "GRPC_CONFIG_OBSERVABILITY" | ||
envObservabilityConfigJSON = "GRPC_CONFIG_OBSERVABILITY_JSON" | ||
envProjectID = "GOOGLE_CLOUD_PROJECT" | ||
logFilterPatternRegexpStr = `^([\w./]+)/((?:\w+)|[*])$` | ||
envProjectID = "GOOGLE_CLOUD_PROJECT" | ||
methodStringRegexpStr = `^([\w./]+)/((?:\w+)|[*])$` | ||
) | ||
|
||
var logFilterPatternRegexp = regexp.MustCompile(logFilterPatternRegexpStr) | ||
|
||
// logFilter represents a method logging configuration. | ||
type logFilter struct { | ||
// Pattern is a string which can select a group of method names. By | ||
// default, the Pattern is an empty string, matching no methods. | ||
// | ||
// Only "*" Wildcard is accepted for Pattern. A Pattern is in the form | ||
// of <service>/<method> or just a character "*" . | ||
// | ||
// If the Pattern is "*", it specifies the defaults for all the | ||
// services; If the Pattern is <service>/*, it specifies the defaults | ||
// for all methods in the specified service <service>; If the Pattern is | ||
// */<method>, this is not supported. | ||
// | ||
// Examples: | ||
// - "Foo/Bar" selects only the method "Bar" from service "Foo" | ||
// - "Foo/*" selects all methods from service "Foo" | ||
// - "*" selects all methods from all services. | ||
Pattern string `json:"pattern,omitempty"` | ||
// HeaderBytes is the number of bytes of each header to log. If the size of | ||
// the header is greater than the defined limit, content past the limit will | ||
// be truncated. The default value is 0. | ||
HeaderBytes int32 `json:"header_bytes,omitempty"` | ||
// MessageBytes is the number of bytes of each message to log. If the size | ||
// of the message is greater than the defined limit, content pass the limit | ||
// will be truncated. The default value is 0. | ||
MessageBytes int32 `json:"message_bytes,omitempty"` | ||
} | ||
|
||
// config is configuration for observability behaviors. By default, no | ||
// configuration is required for tracing/metrics/logging to function. This | ||
// config captures the most common knobs for gRPC users. It's always possible to | ||
// override with explicit config in code. | ||
type config struct { | ||
// EnableCloudTrace represents whether the tracing data upload to | ||
// CloudTrace should be enabled or not. | ||
EnableCloudTrace bool `json:"enable_cloud_trace,omitempty"` | ||
// EnableCloudMonitoring represents whether the metrics data upload to | ||
// CloudMonitoring should be enabled or not. | ||
EnableCloudMonitoring bool `json:"enable_cloud_monitoring,omitempty"` | ||
// EnableCloudLogging represents Whether the logging data upload to | ||
// CloudLogging should be enabled or not. | ||
EnableCloudLogging bool `json:"enable_cloud_logging,omitempty"` | ||
// DestinationProjectID is the destination GCP project identifier for the | ||
// 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. | ||
DestinationProjectID string `json:"destination_project_id,omitempty"` | ||
// LogFilters is a list of method config. The order matters here - the first | ||
// Pattern which matches the current method will apply the associated config | ||
// options in the logFilter. Any other logFilter that also matches that | ||
// comes later will be ignored. So a logFilter of "*/*" should appear last | ||
// in this list. | ||
LogFilters []logFilter `json:"log_filters,omitempty"` | ||
// GlobalTraceSamplingRate is the global setting that controls the | ||
// probability of a RPC being traced. For example, 0.05 means there is a 5% | ||
// chance for a RPC to be traced, 1.0 means trace every call, 0 means don’t | ||
// start new traces. | ||
GlobalTraceSamplingRate float64 `json:"global_trace_sampling_rate,omitempty"` | ||
// CustomTags a list of custom tags that will be attached to every log | ||
// entry. | ||
CustomTags map[string]string `json:"custom_tags,omitempty"` | ||
} | ||
var methodStringRegexp = regexp.MustCompile(methodStringRegexpStr) | ||
|
||
// fetchDefaultProjectID fetches the default GCP project id from environment. | ||
func fetchDefaultProjectID(ctx context.Context) string { | ||
|
@@ -123,14 +60,34 @@ func fetchDefaultProjectID(ctx context.Context) string { | |
return credentials.ProjectID | ||
} | ||
|
||
func validateFilters(config *config) error { | ||
for _, filter := range config.LogFilters { | ||
if filter.Pattern == "*" { | ||
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 | ||
} | ||
match := logFilterPatternRegexp.FindStringSubmatch(filter.Pattern) | ||
match := methodStringRegexp.FindStringSubmatch(method) | ||
if match == nil { | ||
return fmt.Errorf("invalid log filter Pattern: %v", filter.Pattern) | ||
return fmt.Errorf("invalid method string: %v", method) | ||
} | ||
} | ||
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 | ||
|
@@ -144,38 +101,159 @@ func unmarshalAndVerifyConfig(rawJSON json.RawMessage) (*config, error) { | |
if err := json.Unmarshal(rawJSON, &config); err != nil { | ||
return nil, fmt.Errorf("error parsing observability config: %v", err) | ||
} | ||
if err := validateFilters(&config); err != nil { | ||
if err := validateLoggingEvents(&config); err != nil { | ||
return nil, fmt.Errorf("error parsing observability config: %v", err) | ||
} | ||
if config.GlobalTraceSamplingRate > 1 || config.GlobalTraceSamplingRate < 0 { | ||
return nil, fmt.Errorf("error parsing observability config: invalid global trace sampling rate %v", config.GlobalTraceSamplingRate) | ||
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 fileSystemPath := os.Getenv(envObservabilityConfigJSON); fileSystemPath != "" { | ||
content, err := ioutil.ReadFile(fileSystemPath) // TODO: Switch to os.ReadFile once dropped support for go 1.15 | ||
if f := envconfig.ObservabilityConfigFile; f != "" { | ||
logger.Warning("Ignoring GRPC_GCP_OBSERVABILITY_CONFIG and using GRPC_GCP_OBSERVABILITY_CONFIG_FILE contents.") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You can't log this warning unconditionally; only if the other var is set. |
||
content, err := ioutil.ReadFile(f) // TODO: Switch to os.ReadFile once dropped support for go 1.15 | ||
if err != nil { | ||
return nil, fmt.Errorf("error reading observability configuration file %q: %v", fileSystemPath, err) | ||
return nil, fmt.Errorf("error reading observability configuration file %q: %v", f, err) | ||
} | ||
return unmarshalAndVerifyConfig(content) | ||
} else if content := os.Getenv(envObservabilityConfig); content != "" { | ||
return unmarshalAndVerifyConfig([]byte(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.DestinationProjectID == "" { | ||
if config.ProjectID == "" { | ||
// Try to fetch the GCP project id | ||
projectID := fetchDefaultProjectID(ctx) | ||
if projectID == "" { | ||
return fmt.Errorf("empty destination project ID") | ||
} | ||
config.DestinationProjectID = projectID | ||
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:"method,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 { | ||
dfawley marked this conversation as resolved.
Show resolved
Hide resolved
|
||
// 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:"method,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 a RPC | ||
// being traced. For example, 0.05 means there is a 5% chance for a 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"` | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Was there also supposed to be an error if
"*"
isn't last in the list?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ping on this one.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh sorry forgot to respond to this earlier (I swear I typed something out at least - it must've gotten lost). No, there was not supposed to be an error here. Previously, yes this was speced out somewhere (was a docstring in the config type by Lidi). However, now we decided not to do any preprocessing (me and you had a discussion about this) and left it up to the user to know if he specifies a * somewhere in method[] for an event, no further event will hit, and entrusted the user with more responsibility in that regard, and trusted the user.