knative.dev/pkg@v0.0.0-20260602142205-ac97e43f6622/observability/metrics/prometheus/server.go (about) 1 /* 2 Copyright 2025 The Knative Authors 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package prometheus 18 19 import ( 20 "context" 21 "crypto/tls" 22 "crypto/x509" 23 "fmt" 24 "net" 25 "net/http" 26 "os" 27 "strconv" 28 "strings" 29 "time" 30 31 "github.com/prometheus/client_golang/prometheus/promhttp" 32 knativetls "knative.dev/pkg/network/tls" 33 ) 34 35 const ( 36 defaultPrometheusPort = "9090" 37 maxPrometheusPort = 65535 38 minPrometheusPort = 1024 39 defaultPrometheusHost = "" // IPv4 and IPv6 40 prometheusPortEnvName = "METRICS_PROMETHEUS_PORT" 41 prometheusHostEnvName = "METRICS_PROMETHEUS_HOST" 42 prometheusTLSCertEnvName = "METRICS_PROMETHEUS_TLS_CERT" 43 prometheusTLSKeyEnvName = "METRICS_PROMETHEUS_TLS_KEY" 44 prometheusTLSClientAuthEnvName = "METRICS_PROMETHEUS_TLS_CLIENT_AUTH" 45 prometheusTLSClientCAFileEnvName = "METRICS_PROMETHEUS_TLS_CLIENT_CA_FILE" 46 // used with network/tls.DefaultConfigFromEnv. E.g. METRICS_PROMETHEUS_TLS_MIN_VERSION. 47 prometheusTLSEnvPrefix = "METRICS_PROMETHEUS_" 48 ) 49 50 type ServerOption func(*options) 51 52 type Server struct { 53 http *http.Server 54 certFile string 55 keyFile string 56 } 57 58 func NewServer(opts ...ServerOption) (*Server, error) { 59 o := options{ 60 host: defaultPrometheusHost, 61 port: defaultPrometheusPort, 62 } 63 64 for _, opt := range opts { 65 opt(&o) 66 } 67 68 envOverride(&o.host, prometheusHostEnvName) 69 envOverride(&o.port, prometheusPortEnvName) 70 envOverride(&o.certFile, prometheusTLSCertEnvName) 71 envOverride(&o.keyFile, prometheusTLSKeyEnvName) 72 envOverride(&o.clientAuth, prometheusTLSClientAuthEnvName) 73 envOverride(&o.clientCAFile, prometheusTLSClientCAFileEnvName) 74 75 if err := validate(&o); err != nil { 76 return nil, err 77 } 78 79 var tlsConfig *tls.Config 80 if o.certFile != "" && o.keyFile != "" { 81 cfg, err := knativetls.DefaultConfigFromEnv(prometheusTLSEnvPrefix) 82 if err != nil { 83 return nil, err 84 } 85 if err := applyPrometheusClientAuth(cfg, &o); err != nil { 86 return nil, err 87 } 88 tlsConfig = cfg 89 } 90 91 mux := http.NewServeMux() 92 mux.Handle("GET /metrics", promhttp.Handler()) 93 94 addr := net.JoinHostPort(o.host, o.port) 95 96 return &Server{ 97 http: &http.Server{ 98 Addr: addr, 99 Handler: mux, 100 TLSConfig: tlsConfig, 101 // https://medium.com/a-journey-with-go/go-understand-and-mitigate-slowloris-attack-711c1b1403f6 102 ReadHeaderTimeout: 5 * time.Second, 103 }, 104 certFile: o.certFile, 105 keyFile: o.keyFile, 106 }, nil 107 } 108 109 // ListenAndServe starts the metrics server on plain HTTP. 110 func (s *Server) ListenAndServe() error { 111 return s.http.ListenAndServe() 112 } 113 114 // ListenAndServeTLS starts the metrics server on TLS (HTTPS) using the given certificate and key files. 115 func (s *Server) ListenAndServeTLS(certFile, keyFile string) error { 116 return s.http.ListenAndServeTLS(certFile, keyFile) 117 } 118 119 // Serve starts the metrics server, choosing TLS or plain HTTP based on the server configuration. 120 // If both METRICS_PROMETHEUS_TLS_CERT and METRICS_PROMETHEUS_TLS_KEY are set, it calls ListenAndServeTLS 121 func (s *Server) Serve() error { 122 if s.certFile != "" && s.keyFile != "" { 123 return s.http.ListenAndServeTLS(s.certFile, s.keyFile) 124 } 125 return s.http.ListenAndServe() 126 } 127 128 func (s *Server) Shutdown(ctx context.Context) error { 129 return s.http.Shutdown(ctx) 130 } 131 132 type options struct { 133 host string 134 port string 135 certFile string 136 keyFile string 137 clientAuth string 138 clientCAFile string 139 } 140 141 func WithHost(host string) ServerOption { 142 return func(o *options) { 143 o.host = host 144 } 145 } 146 147 func WithPort(port string) ServerOption { 148 return func(o *options) { 149 o.port = port 150 } 151 } 152 153 func validate(o *options) error { 154 port, err := strconv.ParseUint(o.port, 10, 16) 155 if err != nil { 156 return fmt.Errorf("prometheus port %q could not be parsed as a port number: %w", 157 o.port, err) 158 } 159 160 if port < minPrometheusPort || port > maxPrometheusPort { 161 return fmt.Errorf("prometheus port %d, should be between %d and %d", 162 port, minPrometheusPort, maxPrometheusPort) 163 } 164 165 if (o.certFile != "" && o.keyFile == "") || (o.certFile == "" && o.keyFile != "") { 166 return fmt.Errorf("both %s and %s must be set or neither", prometheusTLSCertEnvName, prometheusTLSKeyEnvName) 167 } 168 169 tlsEnabled := o.certFile != "" && o.keyFile != "" 170 auth := strings.TrimSpace(strings.ToLower(o.clientAuth)) 171 172 if auth != "" && auth != "none" && auth != "optional" && auth != "require" { 173 return fmt.Errorf("invalid %s %q: must be %q, %q, or %q", 174 prometheusTLSClientAuthEnvName, o.clientAuth, "none", "optional", "require") 175 } 176 177 if !tlsEnabled && ((auth != "" && auth != "none") || o.clientCAFile != "") { 178 return fmt.Errorf("%s and %s require TLS to be enabled (%s and %s must be set)", 179 prometheusTLSClientAuthEnvName, prometheusTLSClientCAFileEnvName, prometheusTLSCertEnvName, prometheusTLSKeyEnvName) 180 } 181 182 if tlsEnabled && (auth == "optional" || auth == "require") && strings.TrimSpace(o.clientCAFile) == "" { 183 return fmt.Errorf("%s must be set when %s is %q (client certs cannot be validated without a CA)", 184 prometheusTLSClientCAFileEnvName, prometheusTLSClientAuthEnvName, auth) 185 } 186 187 if tlsEnabled && (auth == "" || auth == "none") && strings.TrimSpace(o.clientCAFile) != "" { 188 return fmt.Errorf("%s is set but %s is %q; set %s to %q or %q to use client certificate verification", 189 prometheusTLSClientCAFileEnvName, prometheusTLSClientAuthEnvName, auth, prometheusTLSClientAuthEnvName, "optional", "require") 190 } 191 192 return nil 193 } 194 195 func envOverride(target *string, envName string) { 196 val := os.Getenv(envName) 197 if val != "" { 198 *target = val 199 } 200 } 201 202 // applyPrometheusClientAuth configures mTLS (client certificate verification) on cfg. 203 // o.clientAuth and o.clientCAFile are populated from env vars; validate() has already checked them. 204 func applyPrometheusClientAuth(cfg *tls.Config, o *options) error { 205 v := strings.TrimSpace(strings.ToLower(o.clientAuth)) 206 if v == "" || v == "none" { 207 return nil 208 } 209 210 var clientAuth tls.ClientAuthType 211 switch v { 212 case "optional": 213 clientAuth = tls.VerifyClientCertIfGiven 214 case "require": 215 clientAuth = tls.RequireAndVerifyClientCert 216 } 217 218 caFile := strings.TrimSpace(o.clientCAFile) 219 if caFile != "" { 220 pem, err := os.ReadFile(caFile) 221 if err != nil { 222 return fmt.Errorf("reading %s: %w", prometheusTLSClientCAFileEnvName, err) 223 } 224 pool := x509.NewCertPool() 225 if !pool.AppendCertsFromPEM(pem) { 226 return fmt.Errorf("no valid CA certificates found in %s", prometheusTLSClientCAFileEnvName) 227 } 228 cfg.ClientCAs = pool 229 } 230 231 cfg.ClientAuth = clientAuth 232 return nil 233 }