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  }