otelcollector/prom-config-validator-builder/main.go (311 lines of code) (raw):

package main import ( "context" "flag" "fmt" "io/ioutil" "log" "os" "regexp" "strings" "go.opentelemetry.io/collector/confmap" "go.opentelemetry.io/collector/confmap/provider/envprovider" "go.opentelemetry.io/collector/confmap/provider/fileprovider" "go.opentelemetry.io/collector/otelcol" yaml "gopkg.in/yaml.v2" ) type OtelConfig struct { Exporters interface{} `yaml:"exporters"` Processors interface{} `yaml:"processors"` Extensions interface{} `yaml:"extensions"` Receivers struct { Prometheus struct { Config interface{} `yaml:"config"` TargetAllocator interface{} `yaml:"target_allocator"` } `yaml:"prometheus"` } `yaml:"receivers"` Service struct { Extensions interface{} `yaml:"extensions"` Pipelines struct { Metrics struct { Exporters interface{} `yaml:"exporters"` Processors interface{} `yaml:"processors"` Receivers interface{} `yaml:"receivers"` } `yaml:"metrics"` MetricsTelemetry struct { Exporters interface{} `yaml:"exporters,omitempty"` Processors interface{} `yaml:"processors,omitempty"` Receivers interface{} `yaml:"receivers,omitempty"` } `yaml:"metrics/telemetry,omitempty"` } `yaml:"pipelines"` Telemetry struct { Logs struct { Level interface{} `yaml:"level"` Encoding interface{} `yaml:"encoding"` OutputPaths []string `yaml:"output_paths"` } `yaml:"logs"` } `yaml:"telemetry"` } `yaml:"service"` } var RESET = "\033[0m" var RED = "\033[31m" func logFatalError(message string) { // Do not set env var if customer is running outside of agent to just validate config if os.Getenv("CONFIG_VALIDATOR_RUNNING_IN_AGENT") == "true" { setFatalErrorMessageAsEnvVar(message) } // Always log the full message log.Fatalf("%s%s%s", RED, message, RESET) } func setFatalErrorMessageAsEnvVar(message string) { // Truncate to use as a dimension in the invalid config metric for prometheus-collector-health job truncatedMessage := message if len(message) > 1023 { truncatedMessage = message[:1023] } // Replace newlines for env var to be set correctly re := regexp.MustCompile("\\n") truncatedMessage = re.ReplaceAllString(truncatedMessage, "") // Write env var to a file so it can be used by other processes file, err := os.OpenFile("/opt/microsoft/prom_config_validator_env_var", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) if err != nil { log.Println("prom-config-validator::Unable to open file - prom_config_validator_env_var") } setEnvVarString := fmt.Sprintf("export INVALID_CONFIG_FATAL_ERROR=\"%s\"\n", truncatedMessage) if os.Getenv("OS_TYPE") != "linux" { setEnvVarString = fmt.Sprintf("INVALID_CONFIG_FATAL_ERROR=%s\n", truncatedMessage) } _, err = file.Write([]byte(setEnvVarString)) if err != nil { log.Println("prom-config-validator::Unable to write to the file prom_config_validator_env_var") } file.Close() } func generateOtelConfig(promFilePath string, outputFilePath string, otelConfigTemplatePath string) error { var otelConfig OtelConfig otelConfigFileContents, err := ioutil.ReadFile(otelConfigTemplatePath) if err != nil { return err } err = yaml.Unmarshal([]byte(otelConfigFileContents), &otelConfig) if err != nil { return err } promConfigFileContents, err := ioutil.ReadFile(promFilePath) if err != nil { return err } var prometheusConfig map[string]interface{} err = yaml.Unmarshal([]byte(promConfigFileContents), &prometheusConfig) if err != nil { return err } scrapeConfigs := prometheusConfig["scrape_configs"] if scrapeConfigs != nil { var sc = scrapeConfigs.([]interface{}) for _, scrapeConfig := range sc { scrapeConfig := scrapeConfig.(map[interface{}]interface{}) if scrapeConfig["relabel_configs"] != nil { relabelConfigs := scrapeConfig["relabel_configs"].([]interface{}) for _, relabelConfig := range relabelConfigs { relabelConfig := relabelConfig.(map[interface{}]interface{}) //replace $ with $$ for regex field if relabelConfig["regex"] != nil { // Adding this check here since regex can be boolean and the conversion will fail if _, isString := relabelConfig["regex"].(string); isString { regexString := relabelConfig["regex"].(string) modifiedRegexString := strings.ReplaceAll(regexString, "$$", "$") modifiedRegexString = strings.ReplaceAll(modifiedRegexString, "$", "$$") // Doing the below since we dont want to substitute $ with $$ for env variables NODE_NAME and NODE_IP. modifiedRegexString = strings.ReplaceAll(modifiedRegexString, "$$NODE_NAME", "${env:NODE_NAME}") modifiedRegexString = strings.ReplaceAll(modifiedRegexString, "$$NODE_IP", "${env:NODE_IP}") relabelConfig["regex"] = modifiedRegexString } } //replace $ with $$ for replacement field if relabelConfig["replacement"] != nil { replacement := relabelConfig["replacement"].(string) modifiedReplacementString := strings.ReplaceAll(replacement, "$$", "$") modifiedReplacementString = strings.ReplaceAll(modifiedReplacementString, "$", "$$") modifiedReplacementString = strings.ReplaceAll(modifiedReplacementString, "$$NODE_NAME", "${env:NODE_NAME}") modifiedReplacementString = strings.ReplaceAll(modifiedReplacementString, "$$NODE_IP", "${env:NODE_IP}") relabelConfig["replacement"] = modifiedReplacementString } } } if scrapeConfig["metric_relabel_configs"] != nil { metricRelabelConfigs := scrapeConfig["metric_relabel_configs"].([]interface{}) for _, metricRelabelConfig := range metricRelabelConfigs { metricRelabelConfig := metricRelabelConfig.(map[interface{}]interface{}) //replace $ with $$ for regex field if metricRelabelConfig["regex"] != nil { // Adding this check here since regex can be boolean and the conversion will fail if _, isString := metricRelabelConfig["regex"].(string); isString { regexString := metricRelabelConfig["regex"].(string) modifiedRegexString := strings.ReplaceAll(regexString, "$$", "$") modifiedRegexString = strings.ReplaceAll(modifiedRegexString, "$", "$$") modifiedRegexString = strings.ReplaceAll(modifiedRegexString, "$$NODE_NAME", "${env:NODE_NAME}") modifiedRegexString = strings.ReplaceAll(modifiedRegexString, "$$NODE_IP", "${env:NODE_IP}") metricRelabelConfig["regex"] = modifiedRegexString } } //replace $ with $$ for replacement field if metricRelabelConfig["replacement"] != nil { replacement := metricRelabelConfig["replacement"].(string) modifiedReplacementString := strings.ReplaceAll(replacement, "$$", "$") modifiedReplacementString = strings.ReplaceAll(modifiedReplacementString, "$", "$$") modifiedReplacementString = strings.ReplaceAll(modifiedReplacementString, "$$NODE_NAME", "${env:NODE_NAME}") modifiedReplacementString = strings.ReplaceAll(modifiedReplacementString, "$$NODE_IP", "${env:NODE_IP}") metricRelabelConfig["replacement"] = modifiedReplacementString } } } if scrapeConfig["static_configs"] != nil { staticConfigs := scrapeConfig["static_configs"].([]interface{}) for _, staticConfig := range staticConfigs { staticConfig := staticConfig.(map[interface{}]interface{}) if staticConfig["targets"] != nil { targets := staticConfig["targets"].([]interface{}) for i, target := range targets { if _, isString := target.(string); isString { targetValue := target.(string) modifiedtargetValue := strings.ReplaceAll(targetValue, "$$NODE_NAME", "$NODE_NAME") modifiedtargetValue = strings.ReplaceAll(modifiedtargetValue, "$$NODE_IP", "$NODE_IP") modifiedtargetValue = strings.ReplaceAll(modifiedtargetValue, "$NODE_NAME", "${env:NODE_NAME}") modifiedtargetValue = strings.ReplaceAll(modifiedtargetValue, "$NODE_IP", "${env:NODE_IP}") staticConfig["targets"].([]interface{})[i] = modifiedtargetValue } } } } } } } // Need this here even though it is present in the receiver's config validate method since we only do the $ manipulation for regex and replacement fields // in scrape configs sections and the load method which is called before the validate method fails to unmarshal due to single $. // Either approach will fail but the receiver's config load wont return the right error message unsupportedFeatures := make([]string, 0, 4) if prometheusConfig["remote_write"] != nil { unsupportedFeatures = append(unsupportedFeatures, "remote_write") } if prometheusConfig["remote_read"] != nil { unsupportedFeatures = append(unsupportedFeatures, "remote_read") } if prometheusConfig["rule_files"] != nil { unsupportedFeatures = append(unsupportedFeatures, "rule_files") } if prometheusConfig["alerting"] != nil { unsupportedFeatures = append(unsupportedFeatures, "alerting") } if len(unsupportedFeatures) != 0 { return fmt.Errorf("unsupported features:\n\t%s", strings.Join(unsupportedFeatures, "\n\t")) } globalSettingsFromMergedOtelConfig := prometheusConfig["global"] if globalSettingsFromMergedOtelConfig != nil { globalSettings := globalSettingsFromMergedOtelConfig.(map[interface{}]interface{}) scrapeInterval := globalSettings["scrape_interval"] if (len(globalSettings) > 1) || (len(globalSettings) == 1 && scrapeInterval != "15s") { setEnvVarString := fmt.Sprintf("AZMON_GLOBAL_SETTINGS_CONFIGURED=true\n") file, err := os.OpenFile("/opt/microsoft/prom_config_validator_env_var", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) if err != nil { log.Println("prom-config-validator::Unable to open file - prom_config_validator_env_var") } _, err = file.Write([]byte(setEnvVarString)) if err != nil { log.Println("prom-config-validator::Unable to write to the file prom_config_validator_env_var") } file.Close() if err != nil { log.Println("prom-config-validator::Unable to close file prom_config_validator_env_var", err) } else { log.Printf("prom-config-validator::Successfully set env variables for global config in file prom_config_validator_env_var\n") } } } otelConfig.Receivers.Prometheus.Config = prometheusConfig if os.Getenv("DEBUG_MODE_ENABLED") == "true" { otelConfig.Service.Pipelines.Metrics.Exporters = []interface{}{"otlp", "prometheus"} if os.Getenv("CCP_METRICS_ENABLED") != "true" { otelConfig.Service.Pipelines.MetricsTelemetry.Receivers = []interface{}{"prometheus"} otelConfig.Service.Pipelines.MetricsTelemetry.Exporters = []interface{}{"prometheus/telemetry"} otelConfig.Service.Pipelines.MetricsTelemetry.Processors = []interface{}{"filter/telemetry"} } } mergedConfig, err := yaml.Marshal(otelConfig) if err != nil { return err } if err := ioutil.WriteFile(outputFilePath, mergedConfig, 0644); err != nil { return err } fmt.Printf("prom-config-validator::Successfully generated otel config\n") return nil } type stringArrayValue struct { values []string } func (s *stringArrayValue) Set(val string) error { s.values = append(s.values, val) return nil } func (s *stringArrayValue) String() string { return "[" + strings.Join(s.values, ", ") + "]" } func main() { log.SetFlags(0) configFilePtr := flag.String("config", "", "Config file to validate") outFilePtr := flag.String("output", "", "Output file path for writing collector config") otelTemplatePathPtr := flag.String("otelTemplate", "", "OTel Collector config template file path") flag.Parse() promFilePath := *configFilePtr otelConfigTemplatePath := *otelTemplatePathPtr if otelConfigTemplatePath == "" { logFatalError("prom-config-validator::Please provide otel config template path\n") os.Exit(1) } if promFilePath != "" { fmt.Printf("prom-config-validator::Config file provided - %s\n", promFilePath) outputFilePath := *outFilePtr if outputFilePath == "" { outputFilePath = "merged-otel-config.yaml" } err := generateOtelConfig(promFilePath, outputFilePath, otelConfigTemplatePath) if err != nil { logFatalError(fmt.Sprintf("Generating otel config failed: %v\n", err)) os.Exit(1) } flags := new(flag.FlagSet) //parserProvider.Flags(flags) configFlagEx := new(stringArrayValue) flags.Var(configFlagEx, "config", "Locations to the config file(s), note that only a"+ " single location can be set per flag entry e.g. `-config=file:/path/to/first --config=file:path/to/second`.") configFlag := fmt.Sprintf("--config=%s", outputFilePath) err = flags.Parse([]string{ configFlag, }) if err != nil { logFatalError(fmt.Sprintf("prom-config-validator::Error parsing flags - %v\n", err)) os.Exit(1) } factories, err := components() if err != nil { logFatalError(fmt.Sprintf("prom-config-validator::Failed to build components: %v\n", err)) os.Exit(1) } fmp := fileprovider.NewFactory() envp := envprovider.NewFactory() providers := []confmap.ProviderFactory{fmp, envp} cp, err := otelcol.NewConfigProvider( otelcol.ConfigProviderSettings{ ResolverSettings: confmap.ResolverSettings{ URIs: []string{fmt.Sprintf("file:%s", outputFilePath)}, ProviderFactories: providers, }, }, ) if err != nil { logFatalError(fmt.Errorf("prom-config-validator::Cannot load configuration's parser: %w\n", err).Error()) os.Exit(1) } fmt.Printf("prom-config-validator::Loading configuration...\n") cfg, err := cp.Get(context.Background(), factories) if err != nil { logFatalError(fmt.Sprintf("prom-config-validator::Cannot load configuration: %v", err)) os.Exit(1) } err = cfg.Validate() if err != nil { logFatalError(fmt.Errorf("prom-config-validator::Invalid configuration: %w\n", err).Error()) os.Exit(1) } } else { logFatalError("prom-config-validator::Please provide a config file using the --config flag to validate\n") os.Exit(1) } fmt.Printf("prom-config-validator::Successfully loaded and validated prometheus config\n") os.Exit(0) }