blob: 4aa08722acf5c672a9432c96d8e9c22e55027953 [file] [log] [blame]
Serge Bazanski54e212a2023-06-14 13:45:11 +02001package metrics
2
3import (
4 "context"
5 "crypto/tls"
6 "crypto/x509"
7 "fmt"
8 "net"
9 "net/http"
10 "os/exec"
11
12 "source.monogon.dev/metropolis/node"
13 "source.monogon.dev/metropolis/node/core/identity"
14 "source.monogon.dev/metropolis/pkg/supervisor"
15)
16
17// Service is the Metropolis Metrics Service.
18//
19// Currently, metrics means Prometheus metrics.
20//
21// It runs a forwarding proxy from a public HTTPS listener to a number of
22// locally-running exporters, themselves listening over HTTP. The listener uses
23// the main cluster CA and the node's main certificate, authenticating incoming
24// connections with the same CA.
25//
26// Each exporter is exposed on a separate path, /metrics/<name>, where <name> is
27// the name of the exporter.
28//
29// The HTTPS listener is bound to node.MetricsPort.
30type Service struct {
31 // Credentials used to run the TLS/HTTPS listener and verify incoming
32 // connections.
33 Credentials *identity.NodeCredentials
Tim Windelschmidtf64f1972023-07-28 00:00:50 +000034 Discovery Discovery
Tim Windelschmidtfd49f222023-07-20 14:27:50 +020035
Serge Bazanski54e212a2023-06-14 13:45:11 +020036 // List of Exporters to run and to forward HTTP requests to. If not set, defaults
37 // to DefaultExporters.
Tim Windelschmidtf64f1972023-07-28 00:00:50 +000038 Exporters []*Exporter
Serge Bazanski54e212a2023-06-14 13:45:11 +020039 // enableDynamicAddr enables listening on a dynamically chosen TCP port. This is
40 // used by tests to make sure we don't fail due to the default port being already
41 // in use.
42 enableDynamicAddr bool
Tim Windelschmidtb551b652023-07-17 16:01:42 +020043
Serge Bazanski54e212a2023-06-14 13:45:11 +020044 // dynamicAddr will contain the picked dynamic listen address after the service
45 // starts, if enableDynamicAddr is set.
46 dynamicAddr chan string
47}
48
49// listen starts the public TLS listener for the service.
50func (s *Service) listen() (net.Listener, error) {
51 cert := s.Credentials.TLSCredentials()
52
53 pool := x509.NewCertPool()
54 pool.AddCert(s.Credentials.ClusterCA())
55
56 tlsc := tls.Config{
57 Certificates: []tls.Certificate{
58 cert,
59 },
60 ClientAuth: tls.RequireAndVerifyClientCert,
61 ClientCAs: pool,
62 // TODO(q3k): use VerifyPeerCertificate/VerifyConnection to check that the
63 // incoming client is allowed to access metrics. Currently we allow
64 // anyone/anything with a valid cluster certificate to access them.
65 }
66
67 addr := net.JoinHostPort("", node.MetricsPort.PortString())
68 if s.enableDynamicAddr {
69 addr = ""
70 }
71 return tls.Listen("tcp", addr, &tlsc)
72}
73
74func (s *Service) Run(ctx context.Context) error {
75 lis, err := s.listen()
76 if err != nil {
77 return fmt.Errorf("listen failed: %w", err)
78 }
79 if s.enableDynamicAddr {
80 s.dynamicAddr <- lis.Addr().String()
81 }
82
83 if s.Exporters == nil {
84 s.Exporters = DefaultExporters
85 }
86
87 // First, make sure we don't have duplicate exporters.
88 seenNames := make(map[string]bool)
89 for _, exporter := range s.Exporters {
90 if seenNames[exporter.Name] {
91 return fmt.Errorf("duplicate exporter name: %q", exporter.Name)
92 }
93 seenNames[exporter.Name] = true
94 }
95
96 // Start all exporters as sub-runnables.
97 for _, exporter := range s.Exporters {
Tim Windelschmidte5abee62023-07-19 16:33:36 +020098 if exporter.Executable == "" {
99 continue
100 }
101
Serge Bazanski54e212a2023-06-14 13:45:11 +0200102 cmd := exec.CommandContext(ctx, exporter.Executable, exporter.Arguments...)
103 err := supervisor.Run(ctx, exporter.Name, func(ctx context.Context) error {
104 return supervisor.RunCommand(ctx, cmd)
105 })
106 if err != nil {
107 return fmt.Errorf("running %s failed: %w", exporter.Name, err)
108 }
Serge Bazanski54e212a2023-06-14 13:45:11 +0200109 }
110
111 // And register all exporter forwarding functions on a mux.
112 mux := http.NewServeMux()
113 logger := supervisor.Logger(ctx)
114 for _, exporter := range s.Exporters {
Tim Windelschmidtf64f1972023-07-28 00:00:50 +0000115 mux.HandleFunc(exporter.externalPath(), exporter.ServeHTTP)
Serge Bazanski54e212a2023-06-14 13:45:11 +0200116
117 logger.Infof("Registered exporter %q", exporter.Name)
118 }
119
Tim Windelschmidtb551b652023-07-17 16:01:42 +0200120 // And register a http_sd discovery endpoint.
Tim Windelschmidtf64f1972023-07-28 00:00:50 +0000121 mux.Handle("/discovery", &s.Discovery)
Tim Windelschmidtb551b652023-07-17 16:01:42 +0200122
Serge Bazanski54e212a2023-06-14 13:45:11 +0200123 supervisor.Signal(ctx, supervisor.SignalHealthy)
124
125 // Start forwarding server.
126 srv := http.Server{
127 Handler: mux,
128 BaseContext: func(_ net.Listener) context.Context {
129 return ctx
130 },
131 }
132
133 go func() {
134 <-ctx.Done()
135 srv.Close()
136 }()
137
138 err = srv.Serve(lis)
139 if err != nil && ctx.Err() != nil {
140 return ctx.Err()
141 }
142 return fmt.Errorf("Serve: %w", err)
143}