forked from knative/pkg
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathconfig.go
293 lines (253 loc) · 10.1 KB
/
config.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
/*
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 (
"errors"
"fmt"
"os"
"path"
"strconv"
"strings"
"time"
"go.uber.org/zap"
corev1 "k8s.io/api/core/v1"
)
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"
// 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
)
// ExporterOptions contains options for configuring the exporter.
type ExporterOptions struct {
// Domain is the metrics domain. e.g. "knative.dev". Must be present.
Domain string
// Component is the name of the component that emits the metrics. e.g.
// "activator", "queue_proxy". Should only contains alphabets and underscore.
// Must be present.
Component string
// PrometheusPort is the port to expose metrics if metrics backend is Prometheus.
// It should be between maxPrometheusPort and maxPrometheusPort. 0 value means
// using the default 9090 value. If is ignored if metrics backend is not
// Prometheus.
PrometheusPort int
// ConfigMap is the data from config map config-observability. Must be present.
// See https://github.com/knative/serving/blob/master/config/config-observability.yaml
// for details.
ConfigMap map[string]string
}
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 "knative_revision" resource type. Setting this
// flag to "true" could cause extra Stackdriver charge.
// If backendDestination is not Stackdriver, this is ignored.
allowStackdriverCustomMetrics bool
// 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/knative.dev" joins
// component, e.g. "custom.googleapis.com/knative.dev/serving/activator".
// Store this in a variable to reduce string join operations.
stackdriverCustomMetricTypePrefix string
}
func getMetricsConfig(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.stackdriverCustomMetricTypePrefix = path.Join(customMetricTypePrefix, 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
}
// UpdateExporterFromConfigMap returns a helper func that can be used to update the exporter
// when a config map is updated.
func UpdateExporterFromConfigMap(component string, logger *zap.SugaredLogger) func(configMap *corev1.ConfigMap) {
domain := Domain()
return func(configMap *corev1.ConfigMap) {
UpdateExporter(ExporterOptions{
Domain: domain,
Component: component,
ConfigMap: configMap.Data,
}, logger)
}
}
// UpdateExporter updates the exporter based on the given ExporterOptions.
func UpdateExporter(ops ExporterOptions, logger *zap.SugaredLogger) error {
newConfig, err := getMetricsConfig(ops, logger)
if err != nil {
if ce := getCurMetricsExporter(); ce == nil {
// Fail the process if there doesn't exist an exporter.
logger.Errorw("Failed to get a valid metrics config", zap.Error(err))
} else {
logger.Errorw("Failed to get a valid metrics config; Skip updating the metrics exporter", zap.Error(err))
}
return err
}
if isNewExporterRequired(newConfig) {
logger.Info("Flushing the existing exporter before setting up the new exporter.")
FlushExporter()
e, err := newMetricsExporter(newConfig, logger)
if err != nil {
logger.Errorf("Failed to update a new metrics exporter based on metric config %v. error: %v", newConfig, err)
return err
}
existingConfig := getCurMetricsConfig()
setCurMetricsExporter(e)
logger.Infof("Successfully updated the metrics exporter; old config: %v; new config %v", existingConfig, newConfig)
}
setCurMetricsConfig(newConfig)
return nil
}
// isNewExporterRequired compares the non-nil newConfig against curMetricsConfig. When backend changes,
// or stackdriver project ID changes for stackdriver backend, we need to update the metrics exporter.
func isNewExporterRequired(newConfig *metricsConfig) bool {
cc := getCurMetricsConfig()
if cc == nil || newConfig.backendDestination != cc.backendDestination {
return true
} else if newConfig.backendDestination == Stackdriver && newConfig.stackdriverProjectID != cc.stackdriverProjectID {
return true
}
return false
}
// 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))
}