exporter/collector/config.go (226 lines of code) (raw):

// Copyright 2021 OpenTelemetry 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 collector import ( "context" "errors" "fmt" "regexp" "runtime" "strings" "time" "go.opentelemetry.io/collector/component" "go.opentelemetry.io/collector/pdata/pcommon" "go.opentelemetry.io/collector/pdata/pmetric" "go.opentelemetry.io/otel/metric" "golang.org/x/oauth2/google" "google.golang.org/api/impersonate" "google.golang.org/api/option" monitoredrespb "google.golang.org/genproto/googleapis/api/monitoredres" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" "google.golang.org/grpc/encoding/gzip" otelgrpc "google.golang.org/grpc/stats/opentelemetry" ) const ( DefaultTimeout = 12 * time.Second // Consistent with Cloud Monitoring's timeout ) // Config defines configuration for Google Cloud exporter. type Config struct { // ProjectID is the project telemetry is sent to if the gcp.project.id // resource attribute is not set. If unspecified, this is determined using // application default credentials. ProjectID string `mapstructure:"project"` UserAgent string `mapstructure:"user_agent"` ImpersonateConfig ImpersonateConfig `mapstructure:"impersonate"` TraceConfig TraceConfig `mapstructure:"trace"` LogConfig LogConfig `mapstructure:"log"` MetricConfig MetricConfig `mapstructure:"metric"` DestinationProjectQuota bool `mapstructure:"destination_project_quota"` } type ClientConfig struct { // GetClientOptions returns additional options to be passed // to the underlying Google Cloud API client. // Must be set programmatically (no support via declarative config). // If GetClientOptions returns any options, the exporter will not add the // default credentials, as those could conflict with options provided via // GetClientOptions. // Optional. GetClientOptions func() []option.ClientOption `mapstructure:"-"` Endpoint string `mapstructure:"endpoint"` // Compression specifies the compression format for Metrics and Logging gRPC requests. // Supported values: gzip. Compression string `mapstructure:"compression"` // Only has effect if Endpoint is not "" UseInsecure bool `mapstructure:"use_insecure"` // GRPCPoolSize sets the size of the connection pool in the GCP client GRPCPoolSize int `mapstructure:"grpc_pool_size"` } type TraceConfig struct { // AttributeMappings determines how to map from OpenTelemetry attribute // keys to Google Cloud Trace keys. By default, it changes http and // service keys so that they appear more prominently in the UI. AttributeMappings []AttributeMapping `mapstructure:"attribute_mappings"` ClientConfig ClientConfig `mapstructure:",squash"` } // AttributeMapping maps from an OpenTelemetry key to a Google Cloud Trace key. type AttributeMapping struct { // Key is the OpenTelemetry attribute key Key string `mapstructure:"key"` // Replacement is the attribute sent to Google Cloud Trace Replacement string `mapstructure:"replacement"` } type MetricConfig struct { // MapMonitoredResource is not exposed as an option in the configuration, but // can be used by other exporters to extend the functionality of this // exporter. It allows overriding the function used to map otel resource to // monitored resource. MapMonitoredResource func(pcommon.Resource) *monitoredrespb.MonitoredResource `mapstructure:"-"` // ExtraMetrics is an extension point for exporters to modify the metrics // before they are sent by the exporter. ExtraMetrics func(pmetric.Metrics) `mapstructure:"-"` // GetMetricName is not settable in config files, but can be used by other // exporters which extend the functionality of this exporter. It allows // customizing the naming of metrics. baseName already includes type // suffixes for summary metrics, but does not (yet) include the domain prefix GetMetricName func(baseName string, metric pmetric.Metric) (string, error) `mapstructure:"-"` // WALConfig holds configuration settings for the write ahead log. WALConfig *WALConfig `mapstructure:"experimental_wal_config"` Prefix string `mapstructure:"prefix"` // KnownDomains contains a list of prefixes. If a metric already has one // of these prefixes, the prefix is not added. KnownDomains []string `mapstructure:"known_domains"` // ResourceFilters, if provided, provides a list of resource filters. // Resource attributes matching any filter will be included in metric labels. // Defaults to empty, which won't include any additional resource labels. Note that the // service_resource_labels option operates independently from resource_filters. ResourceFilters []ResourceFilter `mapstructure:"resource_filters"` ClientConfig ClientConfig `mapstructure:",squash"` // CreateMetricDescriptorBufferSize is the buffer size for the channel // which asynchronously calls CreateMetricDescriptor. Default is 10. CreateMetricDescriptorBufferSize int `mapstructure:"create_metric_descriptor_buffer_size"` SkipCreateMetricDescriptor bool `mapstructure:"skip_create_descriptor"` // CreateServiceTimeSeries, if true, this will send all timeseries using `CreateServiceTimeSeries`. // Implicitly, this sets `SkipMetricDescriptor` to true. CreateServiceTimeSeries bool `mapstructure:"create_service_timeseries"` // InstrumentationLibraryLabels, if true, set the instrumentation_source // and instrumentation_version labels. Defaults to true. InstrumentationLibraryLabels bool `mapstructure:"instrumentation_library_labels"` // ServiceResourceLabels, if true, causes the exporter to copy OTel's // service.name, service.namespace, and service.instance.id resource attributes into the GCM timeseries metric labels. This // option is recommended to avoid writing duplicate timeseries against the same monitored // resource. Disabling this option does not prevent resource_filters from adding those // labels. Default is true. ServiceResourceLabels bool `mapstructure:"service_resource_labels"` // CumulativeNormalization normalizes cumulative metrics without start times or with // explicit reset points by subtracting subsequent points from the initial point. // It is enabled by default. Since it caches starting points, it may result in // increased memory usage. CumulativeNormalization bool `mapstructure:"cumulative_normalization"` // EnableSumOfSquaredDeviation enables calculation of an estimated sum of squared // deviation. It isn't correct, so we don't send it by default, and don't expose // it to users. For some uses, it is expected, however. EnableSumOfSquaredDeviation bool `mapstructure:"sum_of_squared_deviation"` } // WALConfig defines settings for the write ahead log. WAL buffering writes data // points in-order to disk before reading and exporting them. This allows for // better retry logic when exporting fails (such as a network outage), because // it preserves both the data on disk and the order of the data points. type WALConfig struct { // Directory is the location to store WAL files. Directory string `mapstructure:"directory"` // MaxBackoff sets the length of time to exponentially re-try failed exports. MaxBackoff time.Duration `mapstructure:"max_backoff"` } // ImpersonateConfig defines configuration for service account impersonation. type ImpersonateConfig struct { TargetPrincipal string `mapstructure:"target_principal"` Subject string `mapstructure:"subject"` Delegates []string `mapstructure:"delegates"` } type ResourceFilter struct { // Match resource keys by prefix Prefix string `mapstructure:"prefix"` // Match resource keys by regex Regex string `mapstructure:"regex"` } type LogConfig struct { // MapMonitoredResource is not exposed as an option in the configuration, but // can be used by other exporters to extend the functionality of this // exporter. It allows overriding the function used to map otel resource to // monitored resource. MapMonitoredResource func(pcommon.Resource) *monitoredrespb.MonitoredResource `mapstructure:"-"` // DefaultLogName sets the fallback log name to use when one isn't explicitly set // for a log entry. If unset, logs without a log name will raise an error. DefaultLogName string `mapstructure:"default_log_name"` // ResourceFilters, if provided, provides a list of resource filters. // Resource attributes matching any filter will be included in LogEntry labels. // Defaults to empty, which won't include any additional resource labels. ResourceFilters []ResourceFilter `mapstructure:"resource_filters"` ClientConfig ClientConfig `mapstructure:",squash"` // ServiceResourceLabels, if true, causes the exporter to copy OTel's // service.name, service.namespace, and service.instance.id resource attributes into the Cloud Logging LogEntry labels. // Disabling this option does not prevent resource_filters from adding those labels. Default is true. ServiceResourceLabels bool `mapstructure:"service_resource_labels"` // ErrorReportingType enables automatically parsing error logs to a json payload containing the // type value for GCP Error Reporting. See https://cloud.google.com/error-reporting/docs/formatting-error-messages#log-text. ErrorReportingType bool `mapstructure:"error_reporting_type"` } // Known metric domains. Note: This is now configurable for advanced usages. var domains = []string{"googleapis.com", "kubernetes.io", "istio.io", "knative.dev"} // DefaultConfig creates the default configuration for exporter. func DefaultConfig() Config { return Config{ LogConfig: LogConfig{ ServiceResourceLabels: true, MapMonitoredResource: defaultResourceToLoggingMonitoredResource, }, MetricConfig: MetricConfig{ KnownDomains: domains, Prefix: "workload.googleapis.com", CreateMetricDescriptorBufferSize: 10, InstrumentationLibraryLabels: true, ServiceResourceLabels: true, CumulativeNormalization: true, GetMetricName: defaultGetMetricName, MapMonitoredResource: defaultResourceToMonitoringMonitoredResource, }, } } // ValidateConfig returns an error if the provided configuration is invalid. func ValidateConfig(cfg Config) error { seenKeys := make(map[string]struct{}, len(cfg.TraceConfig.AttributeMappings)) seenReplacements := make(map[string]struct{}, len(cfg.TraceConfig.AttributeMappings)) for _, mapping := range cfg.TraceConfig.AttributeMappings { if _, ok := seenKeys[mapping.Key]; ok { return fmt.Errorf("duplicate key in traces.attribute_mappings: %q", mapping.Key) } seenKeys[mapping.Key] = struct{}{} if _, ok := seenReplacements[mapping.Replacement]; ok { return fmt.Errorf("duplicate replacement in traces.attribute_mappings: %q", mapping.Replacement) } seenReplacements[mapping.Replacement] = struct{}{} } for _, resourceFilter := range cfg.MetricConfig.ResourceFilters { if len(resourceFilter.Regex) == 0 { continue } if _, err := regexp.Compile(resourceFilter.Regex); err != nil { return fmt.Errorf("unable to parse resource filter regex: %s", err.Error()) } } if len(cfg.LogConfig.ClientConfig.Compression) > 0 && cfg.LogConfig.ClientConfig.Compression != gzip.Name { return fmt.Errorf("unknown compression option '%s', allowed values: '', 'gzip'", cfg.LogConfig.ClientConfig.Compression) } if len(cfg.MetricConfig.ClientConfig.Compression) > 0 && cfg.MetricConfig.ClientConfig.Compression != gzip.Name { return fmt.Errorf("unknown compression option '%s', allowed values: '', 'gzip'", cfg.MetricConfig.ClientConfig.Compression) } if len(cfg.TraceConfig.ClientConfig.Compression) > 0 { return fmt.Errorf("traces.compression invalid: compression is only available for logs and metrics") } return nil } func SetUserAgent(cfg *Config, buildInfo component.BuildInfo) { if cfg.UserAgent == "" { cfg.UserAgent = fmt.Sprintf( "%s/%s (%s/%s)", buildInfo.Description, buildInfo.Version, runtime.GOOS, runtime.GOARCH, ) return } if strings.Contains(cfg.UserAgent, "{{version}}") { cfg.UserAgent = strings.ReplaceAll(cfg.UserAgent, "{{version}}", buildInfo.Version) } } func generateClientOptions(ctx context.Context, clientCfg *ClientConfig, cfg *Config, scopes []string, meterProvider metric.MeterProvider) ([]option.ClientOption, error) { // Disable the built-in telemetry so we have full control over the telemetry produced. copts := []option.ClientOption{ option.WithTelemetryDisabled(), option.WithGRPCDialOption(otelgrpc.DialOption(otelgrpc.Options{ MetricsOptions: otelgrpc.MetricsOptions{ MeterProvider: meterProvider, }, })), } // grpc.WithUserAgent is used by the Trace exporter, but not the Metric exporter (see comment below) if cfg.UserAgent != "" { copts = append(copts, option.WithGRPCDialOption(grpc.WithUserAgent(cfg.UserAgent))) } if clientCfg.Endpoint != "" { if clientCfg.UseInsecure { // option.WithGRPCConn option takes precedent over all other supplied options so the // following user agent will be used by both exporters if we reach this branch dialOpts := []grpc.DialOption{ otelgrpc.DialOption(otelgrpc.Options{ MetricsOptions: otelgrpc.MetricsOptions{ MeterProvider: meterProvider, }, }), grpc.WithTransportCredentials(insecure.NewCredentials()), } if cfg.UserAgent != "" { dialOpts = append(dialOpts, grpc.WithUserAgent(cfg.UserAgent)) } conn, err := grpc.NewClient(clientCfg.Endpoint, dialOpts...) if err != nil { return nil, fmt.Errorf("cannot configure grpc conn: %w", err) } copts = append(copts, option.WithGRPCConn(conn)) } else { copts = append(copts, option.WithEndpoint(clientCfg.Endpoint)) } } if cfg.ImpersonateConfig.TargetPrincipal != "" { if cfg.ProjectID == "" { creds, err := google.FindDefaultCredentials(ctx, scopes...) if err != nil { return nil, fmt.Errorf("error finding default application credentials: %v", err) } cfg.ProjectID = creds.ProjectID } tokenSource, err := impersonate.CredentialsTokenSource(ctx, impersonate.CredentialsConfig{ TargetPrincipal: cfg.ImpersonateConfig.TargetPrincipal, Delegates: cfg.ImpersonateConfig.Delegates, Subject: cfg.ImpersonateConfig.Subject, Scopes: scopes, }) if err != nil { return nil, err } copts = append(copts, option.WithTokenSource(tokenSource)) } else if !clientCfg.UseInsecure && (clientCfg.GetClientOptions == nil || len(clientCfg.GetClientOptions()) == 0) { // Only add default credentials if GetClientOptions does not // provide additional options since GetClientOptions could pass // credentials which conflict with the default creds. creds, err := google.FindDefaultCredentials(ctx, scopes...) if err != nil { return nil, fmt.Errorf("error finding default application credentials: %v", err) } copts = append(copts, option.WithCredentials(creds)) if cfg.ProjectID == "" { cfg.ProjectID = creds.ProjectID } } if clientCfg.GRPCPoolSize > 0 { copts = append(copts, option.WithGRPCConnectionPool(clientCfg.GRPCPoolSize)) } if clientCfg.GetClientOptions != nil { copts = append(copts, clientCfg.GetClientOptions()...) } if cfg.ProjectID == "" { return nil, errors.New("no project set in config, or found with application default credentials") } return copts, nil }