blob: ffae97078b5bb641eb32c98b39615f2d28890ebf [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 Windelschmidt3e756902023-11-24 23:12:06 +010098 exporter := exporter
Tim Windelschmidte5abee62023-07-19 16:33:36 +020099 if exporter.Executable == "" {
100 continue
101 }
102
Serge Bazanski54e212a2023-06-14 13:45:11 +0200103 err := supervisor.Run(ctx, exporter.Name, func(ctx context.Context) error {
Tim Windelschmidt3e756902023-11-24 23:12:06 +0100104 cmd := exec.CommandContext(ctx, exporter.Executable, exporter.Arguments...)
Serge Bazanski54e212a2023-06-14 13:45:11 +0200105 return supervisor.RunCommand(ctx, cmd)
106 })
107 if err != nil {
108 return fmt.Errorf("running %s failed: %w", exporter.Name, err)
109 }
Serge Bazanski54e212a2023-06-14 13:45:11 +0200110 }
111
112 // And register all exporter forwarding functions on a mux.
113 mux := http.NewServeMux()
114 logger := supervisor.Logger(ctx)
115 for _, exporter := range s.Exporters {
Tim Windelschmidtf64f1972023-07-28 00:00:50 +0000116 mux.HandleFunc(exporter.externalPath(), exporter.ServeHTTP)
Serge Bazanski54e212a2023-06-14 13:45:11 +0200117
118 logger.Infof("Registered exporter %q", exporter.Name)
119 }
120
Tim Windelschmidtb551b652023-07-17 16:01:42 +0200121 // And register a http_sd discovery endpoint.
Tim Windelschmidtf64f1972023-07-28 00:00:50 +0000122 mux.Handle("/discovery", &s.Discovery)
Tim Windelschmidtb551b652023-07-17 16:01:42 +0200123
Serge Bazanski54e212a2023-06-14 13:45:11 +0200124 supervisor.Signal(ctx, supervisor.SignalHealthy)
125
126 // Start forwarding server.
127 srv := http.Server{
128 Handler: mux,
129 BaseContext: func(_ net.Listener) context.Context {
130 return ctx
131 },
132 }
133
134 go func() {
135 <-ctx.Done()
136 srv.Close()
137 }()
138
139 err = srv.Serve(lis)
140 if err != nil && ctx.Err() != nil {
141 return ctx.Err()
142 }
143 return fmt.Errorf("Serve: %w", err)
144}