package libprometheus import ( "fmt" "net/url" "os" "path/filepath" "sort" "strings" ) const configTemplate = `# Prometheus configuration. # Generated automatically — do not edit. global: scrape_interval: %s evaluation_interval: %s scrape_configs: - job_name: '%s' scheme: '%s' metrics_path: '%s' %s static_configs: - targets: ['%s'] %s ` const alertingTemplate = `alerting: alertmanagers: - scheme: '%s' path_prefix: '%s' api_version: 'v2' static_configs: - targets: ['%s'] %s ` type PrometheusConfigOptions struct { ScrapeInterval string JobName string ScrapeTarget string AlertmanagerTarget string AlertmanagerHeaders map[string]string InsecureSkipVerify bool } // WritePrometheusConfig writes a Prometheus config.yaml to dataDir/config.yaml. // ScrapeTarget is a full scrape URL (e.g. "http://localhost:9001/api/metrics"). // AlertmanagerTarget is an optional base URL used by Prometheus notifier // (e.g. "https://portainer.example/api/endpoints/1/edge/alerts"). func WritePrometheusConfig(dataDir string, options PrometheusConfigOptions) error { if err := os.MkdirAll(dataDir, 0o750); err != nil { return fmt.Errorf("create prometheus config dir: %w", err) } scheme, metricsPath, hostPort, err := parseTarget(options.ScrapeTarget, "/metrics") if err != nil { return fmt.Errorf("parse scrape target: %w", err) } escape := func(s string) string { return strings.ReplaceAll(s, "'", "''") } alertingSection, err := renderAlertingSection(options.AlertmanagerTarget, options.AlertmanagerHeaders, options.InsecureSkipVerify, escape) if err != nil { return err } scrapeTLSBlock := "" if scheme == "https" && options.InsecureSkipVerify { scrapeTLSBlock = " tls_config:\n insecure_skip_verify: true\n" } content := fmt.Sprintf(configTemplate, options.ScrapeInterval, options.ScrapeInterval, escape(options.JobName), escape(scheme), escape(metricsPath), scrapeTLSBlock, escape(hostPort), alertingSection, ) return os.WriteFile(filepath.Join(dataDir, "config.yaml"), []byte(content), 0o600) } // parseTarget splits a full URL into its scheme, path, and host:port components // for use in a Prometheus static_configs entry. func parseTarget(target string, defaultPath string) (scheme, parsedPath, hostPort string, err error) { u, err := url.Parse(target) if err != nil { return "", "", "", err } scheme = u.Scheme if scheme == "" { scheme = "http" } parsedPath = u.Path if parsedPath == "" { parsedPath = defaultPath } hostPort = u.Host if hostPort == "" { hostPort = target // fallback: treat the whole string as host:port } return scheme, parsedPath, hostPort, nil } func renderAlertingSection(target string, headers map[string]string, insecureSkipVerify bool, escape func(string) string) (string, error) { if target == "" { return "", nil } scheme, pathPrefix, hostPort, err := parseTarget(target, "") if err != nil { return "", fmt.Errorf("parse alertmanager target: %w", err) } tlsBlock := "" if insecureSkipVerify { tlsBlock = " tls_config:\n insecure_skip_verify: true\n" } headersBlock := renderHTTPHeaders(headers, escape, " ") return fmt.Sprintf(alertingTemplate, escape(scheme), escape(pathPrefix), escape(hostPort), tlsBlock+headersBlock, ), nil } func renderHTTPHeaders(headers map[string]string, escape func(string) string, indent string) string { if len(headers) == 0 { return "" } keys := make([]string, 0, len(headers)) for name := range headers { keys = append(keys, name) } sort.Strings(keys) var builder strings.Builder _, _ = fmt.Fprintf(&builder, "%shttp_headers:\n", indent) for _, name := range keys { _, _ = fmt.Fprintf(&builder, "%s %s:\n", indent, name) _, _ = fmt.Fprintf(&builder, "%s values: ['%s']\n", indent, escape(headers[name])) } return builder.String() }