blob: e547c5db3d0a3e8ca27269dd17f40ddfe135e1af [file] [log] [blame]
Tim Windelschmidt6d33a432025-02-04 14:34:25 +01001// Copyright The Monogon Project Authors.
2// SPDX-License-Identifier: Apache-2.0
3
Serge Bazanski54e212a2023-06-14 13:45:11 +02004package metrics
5
6import (
7 "context"
8 "crypto/tls"
9 "crypto/x509"
10 "fmt"
11 "net"
12 "net/http"
13 "os/exec"
14
Jan Schär0f8ce4c2025-09-04 13:27:50 +020015 "source.monogon.dev/metropolis/node/allocs"
Serge Bazanski54e212a2023-06-14 13:45:11 +020016 "source.monogon.dev/metropolis/node/core/identity"
Tim Windelschmidt9f21f532024-05-07 15:14:20 +020017 "source.monogon.dev/osbase/supervisor"
Serge Bazanski54e212a2023-06-14 13:45:11 +020018)
19
20// Service is the Metropolis Metrics Service.
21//
22// Currently, metrics means Prometheus metrics.
23//
24// It runs a forwarding proxy from a public HTTPS listener to a number of
25// locally-running exporters, themselves listening over HTTP. The listener uses
26// the main cluster CA and the node's main certificate, authenticating incoming
27// connections with the same CA.
28//
29// Each exporter is exposed on a separate path, /metrics/<name>, where <name> is
30// the name of the exporter.
31//
Jan Schär0f8ce4c2025-09-04 13:27:50 +020032// The HTTPS listener is bound to allocs.PortMetrics.
Serge Bazanski54e212a2023-06-14 13:45:11 +020033type Service struct {
34 // Credentials used to run the TLS/HTTPS listener and verify incoming
35 // connections.
36 Credentials *identity.NodeCredentials
Tim Windelschmidtf64f1972023-07-28 00:00:50 +000037 Discovery Discovery
Tim Windelschmidtfd49f222023-07-20 14:27:50 +020038
Serge Bazanski54e212a2023-06-14 13:45:11 +020039 // List of Exporters to run and to forward HTTP requests to. If not set, defaults
40 // to DefaultExporters.
Tim Windelschmidtf64f1972023-07-28 00:00:50 +000041 Exporters []*Exporter
Serge Bazanski54e212a2023-06-14 13:45:11 +020042 // enableDynamicAddr enables listening on a dynamically chosen TCP port. This is
43 // used by tests to make sure we don't fail due to the default port being already
44 // in use.
45 enableDynamicAddr bool
Tim Windelschmidtb551b652023-07-17 16:01:42 +020046
Serge Bazanski54e212a2023-06-14 13:45:11 +020047 // dynamicAddr will contain the picked dynamic listen address after the service
48 // starts, if enableDynamicAddr is set.
49 dynamicAddr chan string
50}
51
52// listen starts the public TLS listener for the service.
53func (s *Service) listen() (net.Listener, error) {
54 cert := s.Credentials.TLSCredentials()
55
56 pool := x509.NewCertPool()
57 pool.AddCert(s.Credentials.ClusterCA())
58
59 tlsc := tls.Config{
60 Certificates: []tls.Certificate{
61 cert,
62 },
63 ClientAuth: tls.RequireAndVerifyClientCert,
64 ClientCAs: pool,
65 // TODO(q3k): use VerifyPeerCertificate/VerifyConnection to check that the
66 // incoming client is allowed to access metrics. Currently we allow
67 // anyone/anything with a valid cluster certificate to access them.
68 }
69
Jan Schär0f8ce4c2025-09-04 13:27:50 +020070 addr := net.JoinHostPort("", allocs.PortMetrics.PortString())
Serge Bazanski54e212a2023-06-14 13:45:11 +020071 if s.enableDynamicAddr {
72 addr = ""
73 }
74 return tls.Listen("tcp", addr, &tlsc)
75}
76
77func (s *Service) Run(ctx context.Context) error {
78 lis, err := s.listen()
79 if err != nil {
80 return fmt.Errorf("listen failed: %w", err)
81 }
82 if s.enableDynamicAddr {
83 s.dynamicAddr <- lis.Addr().String()
84 }
85
86 if s.Exporters == nil {
87 s.Exporters = DefaultExporters
88 }
89
90 // First, make sure we don't have duplicate exporters.
91 seenNames := make(map[string]bool)
92 for _, exporter := range s.Exporters {
93 if seenNames[exporter.Name] {
94 return fmt.Errorf("duplicate exporter name: %q", exporter.Name)
95 }
96 seenNames[exporter.Name] = true
97 }
98
99 // Start all exporters as sub-runnables.
100 for _, exporter := range s.Exporters {
Tim Windelschmidt3e756902023-11-24 23:12:06 +0100101 exporter := exporter
Tim Windelschmidte5abee62023-07-19 16:33:36 +0200102 if exporter.Executable == "" {
103 continue
104 }
105
Serge Bazanski54e212a2023-06-14 13:45:11 +0200106 err := supervisor.Run(ctx, exporter.Name, func(ctx context.Context) error {
Tim Windelschmidt3e756902023-11-24 23:12:06 +0100107 cmd := exec.CommandContext(ctx, exporter.Executable, exporter.Arguments...)
Serge Bazanski54e212a2023-06-14 13:45:11 +0200108 return supervisor.RunCommand(ctx, cmd)
109 })
110 if err != nil {
111 return fmt.Errorf("running %s failed: %w", exporter.Name, err)
112 }
Serge Bazanski54e212a2023-06-14 13:45:11 +0200113 }
114
115 // And register all exporter forwarding functions on a mux.
116 mux := http.NewServeMux()
117 logger := supervisor.Logger(ctx)
118 for _, exporter := range s.Exporters {
Tim Windelschmidtf64f1972023-07-28 00:00:50 +0000119 mux.HandleFunc(exporter.externalPath(), exporter.ServeHTTP)
Serge Bazanski54e212a2023-06-14 13:45:11 +0200120
121 logger.Infof("Registered exporter %q", exporter.Name)
122 }
123
Tim Windelschmidtb551b652023-07-17 16:01:42 +0200124 // And register a http_sd discovery endpoint.
Tim Windelschmidtf64f1972023-07-28 00:00:50 +0000125 mux.Handle("/discovery", &s.Discovery)
Tim Windelschmidtb551b652023-07-17 16:01:42 +0200126
Serge Bazanski54e212a2023-06-14 13:45:11 +0200127 supervisor.Signal(ctx, supervisor.SignalHealthy)
128
129 // Start forwarding server.
130 srv := http.Server{
131 Handler: mux,
132 BaseContext: func(_ net.Listener) context.Context {
133 return ctx
134 },
135 }
136
137 go func() {
138 <-ctx.Done()
139 srv.Close()
140 }()
141
142 err = srv.Serve(lis)
143 if err != nil && ctx.Err() != nil {
144 return ctx.Err()
145 }
Tim Windelschmidt73e98822024-04-18 23:13:49 +0200146 return fmt.Errorf("Serve(): %w", err)
Serge Bazanski54e212a2023-06-14 13:45:11 +0200147}