Files

153 lines
3.8 KiB
Go

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()
}