Skip to content

Commit b6fcbf3

Browse files
authored
feat: add support for Prometheus metrics (#58)
This is a port of GoogleCloudPlatform/cloud-sql-proxy#1215
1 parent 05d1cfa commit b6fcbf3

File tree

4 files changed

+181
-7
lines changed

4 files changed

+181
-7
lines changed

cmd/root.go

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,17 @@ import (
2020
"errors"
2121
"fmt"
2222
"net"
23+
"net/http"
2324
"net/url"
2425
"os"
2526
"os/signal"
2627
"strconv"
2728
"strings"
2829
"syscall"
30+
"time"
2931

3032
"cloud.google.com/go/alloydbconn"
33+
"contrib.go.opencensus.io/exporter/prometheus"
3134
"github.com/GoogleCloudPlatform/alloydb-auth-proxy/alloydb"
3235
"github.com/GoogleCloudPlatform/alloydb-auth-proxy/internal/gcloud"
3336
"github.com/GoogleCloudPlatform/alloydb-auth-proxy/internal/proxy"
@@ -75,6 +78,9 @@ func Execute() {
7578
type Command struct {
7679
*cobra.Command
7780
conf *proxy.Config
81+
82+
prometheusNamespace string
83+
httpPort string
7884
}
7985

8086
// Option is a function that configures a Command.
@@ -126,6 +132,10 @@ without having to manage any client SSL certificates.`,
126132
"Path to a service account key to use for authentication.")
127133
cmd.PersistentFlags().BoolVarP(&c.conf.GcloudAuth, "gcloud-auth", "g", false,
128134
"Use gcloud's user configuration to retrieve a token for authentication.")
135+
cmd.PersistentFlags().StringVar(&c.prometheusNamespace, "prometheus-namespace", "",
136+
"Enable Prometheus for metric collection using the provided namespace")
137+
cmd.PersistentFlags().StringVar(&c.httpPort, "http-port", "9090",
138+
"Port for the Prometheus server to use")
129139

130140
// Global and per instance flags
131141
cmd.PersistentFlags().StringVarP(&c.conf.Addr, "address", "a", "127.0.0.1",
@@ -194,6 +204,10 @@ func parseConfig(cmd *cobra.Command, conf *proxy.Config, args []string) error {
194204
}
195205
conf.DialerOpts = opts
196206

207+
if userHasSet("http-port") && !userHasSet("prometheus-namespace") {
208+
return newBadCommandError("cannot specify --http-port without --prometheus-namespace")
209+
}
210+
197211
var ics []proxy.InstanceConnConfig
198212
for _, a := range args {
199213
// Assume no query params initially
@@ -268,14 +282,46 @@ func runSignalWrapper(cmd *Command) error {
268282

269283
shutdownCh := make(chan error)
270284

285+
if cmd.prometheusNamespace != "" {
286+
e, err := prometheus.NewExporter(prometheus.Options{
287+
Namespace: cmd.prometheusNamespace,
288+
})
289+
if err != nil {
290+
return err
291+
}
292+
mux := http.NewServeMux()
293+
mux.Handle("/metrics", e)
294+
addr := fmt.Sprintf("localhost:%s", cmd.httpPort)
295+
server := &http.Server{Addr: addr, Handler: mux}
296+
go func() {
297+
select {
298+
case <-ctx.Done():
299+
// Give the HTTP server a second to shutdown cleanly.
300+
ctx2, _ := context.WithTimeout(context.Background(), time.Second)
301+
if err := server.Shutdown(ctx2); err != nil {
302+
cmd.Printf("failed to shutdown Prometheus HTTP server: %v\n", err)
303+
}
304+
}
305+
}()
306+
go func() {
307+
err := server.ListenAndServe()
308+
if err == http.ErrServerClosed {
309+
return
310+
}
311+
if err != nil {
312+
shutdownCh <- fmt.Errorf("failed to start prometheus HTTP server: %v", err)
313+
}
314+
}()
315+
}
316+
271317
// watch for sigterm / sigint signals
272318
signals := make(chan os.Signal, 1)
273319
signal.Notify(signals, syscall.SIGTERM, syscall.SIGINT)
274320
go func() {
275321
var s os.Signal
276322
select {
277323
case s = <-signals:
278-
case <-cmd.Context().Done():
324+
case <-ctx.Done():
279325
// this should only happen when the context supplied in tests in canceled
280326
s = syscall.SIGINT
281327
}

cmd/root_test.go

Lines changed: 50 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"errors"
2020
"fmt"
2121
"net"
22+
"net/http"
2223
"sync"
2324
"testing"
2425
"time"
@@ -310,6 +311,10 @@ func TestNewCommandWithErrors(t *testing.T) {
310311
desc: "using the unix socket and port query params",
311312
args: []string{"/projects/proj/locations/region/clusters/clust/instances/inst?unix-socket=/path&port=5000"},
312313
},
314+
{
315+
desc: "enabling a Prometheus port without a namespace",
316+
args: []string{"--http-port", "1111", "proj:region:inst"},
317+
},
313318
}
314319

315320
for _, tc := range tcs {
@@ -367,11 +372,7 @@ func TestCommandWithCustomDialer(t *testing.T) {
367372
ctx, cancel := context.WithCancel(context.Background())
368373
defer cancel()
369374

370-
go func() {
371-
if err := c.ExecuteContext(ctx); !errors.As(err, &errSigInt) {
372-
t.Fatalf("want errSigInt, got = %v", err)
373-
}
374-
}()
375+
go c.ExecuteContext(ctx)
375376

376377
// try will run f count times, returning early if f succeeds, or failing
377378
// when count has been exceeded.
@@ -411,3 +412,47 @@ func TestCommandWithCustomDialer(t *testing.T) {
411412
return nil
412413
}, 10)
413414
}
415+
416+
func TestPrometheusMetricsEndpoint(t *testing.T) {
417+
c := NewCommand(WithDialer(&spyDialer{}))
418+
// Keep the test output quiet
419+
c.SilenceUsage = true
420+
c.SilenceErrors = true
421+
c.SetArgs([]string{
422+
"--prometheus-namespace", "prometheus",
423+
"my-project:my-region:my-instance"})
424+
425+
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
426+
defer cancel()
427+
428+
go c.ExecuteContext(ctx)
429+
430+
// try to dial metrics server for a max of ~10s to give the proxy time to
431+
// start up.
432+
tryDial := func(addr string) (*http.Response, error) {
433+
var (
434+
resp *http.Response
435+
attempts int
436+
err error
437+
)
438+
for {
439+
if attempts > 10 {
440+
return resp, err
441+
}
442+
resp, err = http.Get(addr)
443+
if err != nil {
444+
attempts++
445+
time.Sleep(time.Second)
446+
continue
447+
}
448+
return resp, err
449+
}
450+
}
451+
resp, err := tryDial("http://localhost:9090/metrics") // default port set by http-port flag
452+
if err != nil {
453+
t.Fatalf("failed to dial metrics endpoint: %v", err)
454+
}
455+
if resp.StatusCode != http.StatusOK {
456+
t.Fatalf("expected a 200 status, got = %v", resp.StatusCode)
457+
}
458+
}

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ go 1.16
44

55
require (
66
cloud.google.com/go/alloydbconn v0.1.1
7-
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
7+
contrib.go.opencensus.io/exporter/prometheus v0.4.1
88
github.com/google/go-cmp v0.5.8
99
github.com/lib/pq v1.10.5 // indirect
1010
github.com/spf13/cobra v1.5.0

0 commit comments

Comments
 (0)