/* Copyright 2018 The Knative 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 metrics import ( "encoding/json" "errors" "fmt" "os" "path" "strconv" "strings" "time" "go.uber.org/zap" ) const ( DomainEnv = "METRICS_DOMAIN" ConfigMapNameEnv = "CONFIG_OBSERVABILITY_NAME" ) // metricsBackend specifies the backend to use for metrics type metricsBackend string const ( // The following keys are used to configure metrics reporting. // See https://github.com/knative/serving/blob/master/config/config-observability.yaml // for details. AllowStackdriverCustomMetricsKey = "metrics.allow-stackdriver-custom-metrics" BackendDestinationKey = "metrics.backend-destination" ReportingPeriodKey = "metrics.reporting-period-seconds" StackdriverProjectIDKey = "metrics.stackdriver-project-id" StackdriverCustomMetricSubDomainKey = "metrics.stackdriver-custom-metrics-subdomain" // Stackdriver is used for Stackdriver backend Stackdriver metricsBackend = "stackdriver" // Prometheus is used for Prometheus backend Prometheus metricsBackend = "prometheus" defaultBackendEnvName = "DEFAULT_METRICS_BACKEND" defaultPrometheusPort = 9090 maxPrometheusPort = 65535 minPrometheusPort = 1024 ) type metricsConfig struct { // The metrics domain. e.g. "serving.knative.dev" or "build.knative.dev". domain string // The component that emits the metrics. e.g. "activator", "autoscaler". component string // The metrics backend destination. backendDestination metricsBackend // reportingPeriod specifies the interval between reporting aggregated views. // If duration is less than or equal to zero, it enables the default behavior. reportingPeriod time.Duration // ---- Prometheus specific below ---- // prometheusPort is the port where metrics are exposed in Prometheus // format. It defaults to 9090. prometheusPort int // ---- Stackdriver specific below ---- // stackdriverProjectID is the stackdriver project ID where the stats data are // uploaded to. This is not the GCP project ID. stackdriverProjectID string // allowStackdriverCustomMetrics indicates whether it is allowed to send metrics to // Stackdriver using "global" resource type and custom metric type if the // metrics are not supported by the registered monitored resource types. Setting this // flag to "true" could cause extra Stackdriver charge. // If backendDestination is not Stackdriver, this is ignored. allowStackdriverCustomMetrics bool // stackdriverCustomMetricsSubDomain is the subdomain to use when sending custom metrics to StackDriver. // If not specified, the default is `knative.dev`. // If backendDestination is not Stackdriver, this is ignored. stackdriverCustomMetricsSubDomain string // True if backendDestination equals to "stackdriver". Store this in a variable // to reduce string comparison operations. isStackdriverBackend bool // stackdriverMetricTypePrefix is the metric domain joins component, e.g. // "knative.dev/serving/activator". Store this in a variable to reduce string // join operations. stackdriverMetricTypePrefix string // stackdriverCustomMetricTypePrefix is "custom.googleapis.com" joined with the subdomain and component. // E.g., "custom.googleapis.com//". // Store this in a variable to reduce string join operations. stackdriverCustomMetricTypePrefix string } func createMetricsConfig(ops ExporterOptions, logger *zap.SugaredLogger) (*metricsConfig, error) { var mc metricsConfig if ops.Domain == "" { return nil, errors.New("metrics domain cannot be empty") } mc.domain = ops.Domain if ops.Component == "" { return nil, errors.New("metrics component name cannot be empty") } mc.component = ops.Component if ops.ConfigMap == nil { return nil, errors.New("metrics config map cannot be empty") } m := ops.ConfigMap // Read backend setting from environment variable first backend := os.Getenv(defaultBackendEnvName) if backend == "" { // Use Prometheus if DEFAULT_METRICS_BACKEND does not exist or is empty backend = string(Prometheus) } // Override backend if it is setting in config map. if backendFromConfig, ok := m[BackendDestinationKey]; ok { backend = backendFromConfig } lb := metricsBackend(strings.ToLower(backend)) switch lb { case Stackdriver, Prometheus: mc.backendDestination = lb default: return nil, fmt.Errorf("unsupported metrics backend value %q", backend) } if mc.backendDestination == Prometheus { pp := ops.PrometheusPort if pp == 0 { pp = defaultPrometheusPort } if pp < minPrometheusPort || pp > maxPrometheusPort { return nil, fmt.Errorf("invalid port %v, should between %v and %v", pp, minPrometheusPort, maxPrometheusPort) } mc.prometheusPort = pp } // If stackdriverProjectIDKey is not provided for stackdriver backend destination, OpenCensus will try to // use the application default credentials. If that is not available, Opencensus would fail to create the // metrics exporter. if mc.backendDestination == Stackdriver { mc.stackdriverProjectID = m[StackdriverProjectIDKey] mc.isStackdriverBackend = true mc.stackdriverMetricTypePrefix = path.Join(mc.domain, mc.component) mc.stackdriverCustomMetricsSubDomain = defaultCustomMetricSubDomain if sdcmd, ok := m[StackdriverCustomMetricSubDomainKey]; ok && sdcmd != "" { mc.stackdriverCustomMetricsSubDomain = sdcmd } mc.stackdriverCustomMetricTypePrefix = path.Join(customMetricTypePrefix, mc.stackdriverCustomMetricsSubDomain, mc.component) if ascmStr, ok := m[AllowStackdriverCustomMetricsKey]; ok && ascmStr != "" { ascmBool, err := strconv.ParseBool(ascmStr) if err != nil { return nil, fmt.Errorf("invalid %s value %q", AllowStackdriverCustomMetricsKey, ascmStr) } mc.allowStackdriverCustomMetrics = ascmBool } } // If reporting period is specified, use the value from the configuration. // If not, set a default value based on the selected backend. // Each exporter makes different promises about what the lowest supported // reporting period is. For Stackdriver, this value is 1 minute. // For Prometheus, we will use a lower value since the exporter doesn't // push anything but just responds to pull requests, and shorter durations // do not really hurt the performance and we rely on the scraping configuration. if repStr, ok := m[ReportingPeriodKey]; ok && repStr != "" { repInt, err := strconv.Atoi(repStr) if err != nil { return nil, fmt.Errorf("invalid %s value %q", ReportingPeriodKey, repStr) } mc.reportingPeriod = time.Duration(repInt) * time.Second } else if mc.backendDestination == Stackdriver { mc.reportingPeriod = 60 * time.Second } else if mc.backendDestination == Prometheus { mc.reportingPeriod = 5 * time.Second } return &mc, nil } // ConfigMapName gets the name of the metrics ConfigMap func ConfigMapName() string { cm := os.Getenv(ConfigMapNameEnv) if cm == "" { return "config-observability" } return cm } // Domain holds the metrics domain to use for surfacing metrics. func Domain() string { if domain := os.Getenv(DomainEnv); domain != "" { return domain } panic(fmt.Sprintf(`The environment variable %q is not set If this is a process running on Kubernetes, then it should be specifying this via: env: - name: %s value: knative.dev/some-repository If this is a Go unit test consuming metric.Domain() then it should add the following import: import ( _ "knative.dev/pkg/metrics/testing" )`, DomainEnv, DomainEnv)) } // JsonToMetricsOptions converts a json string of a // ExporterOptions. Returns a non-nil ExporterOptions always. func JsonToMetricsOptions(jsonOpts string) (*ExporterOptions, error) { var opts ExporterOptions if jsonOpts == "" { return nil, errors.New("json options string is empty") } if err := json.Unmarshal([]byte(jsonOpts), &opts); err != nil { return nil, err } return &opts, nil } // MetricsOptionsToJson converts a ExporterOptions to a json string. func MetricsOptionsToJson(opts *ExporterOptions) (string, error) { if opts == nil { return "", nil } jsonOpts, err := json.Marshal(opts) if err != nil { return "", err } return string(jsonOpts), nil }