Skip to content

Commit

Permalink
feat: add support for Prometheus metrics (#1215)
Browse files Browse the repository at this point in the history
This commit adds two flags (prometheus-namespace and http-port)
which enable a Prometheus exporter exposed at localhost:9090/metrics.
The port is configurable with http-port. When enabled, the metrics
endpoint will serve all metrics collected in the Go connector in the
Prometheus format.
  • Loading branch information
enocom authored Jun 13, 2022
1 parent 4b443d6 commit eeb2029
Show file tree
Hide file tree
Showing 4 changed files with 176 additions and 2 deletions.
48 changes: 47 additions & 1 deletion cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,17 @@ import (
"errors"
"fmt"
"net"
"net/http"
"net/url"
"os"
"os/signal"
"strconv"
"strings"
"syscall"
"time"

"cloud.google.com/go/cloudsqlconn"
"contrib.go.opencensus.io/exporter/prometheus"
"github.com/GoogleCloudPlatform/cloudsql-proxy/v2/cloudsql"
"github.com/GoogleCloudPlatform/cloudsql-proxy/v2/internal/gcloud"
"github.com/GoogleCloudPlatform/cloudsql-proxy/v2/internal/proxy"
Expand Down Expand Up @@ -63,6 +66,9 @@ func Execute() {
type Command struct {
*cobra.Command
conf *proxy.Config

prometheusNamespace string
httpPort string
}

// Option is a function that configures a Command.
Expand Down Expand Up @@ -114,6 +120,10 @@ any client SSL certificates.`,
"Path to a service account key to use for authentication.")
cmd.PersistentFlags().BoolVarP(&c.conf.GcloudAuth, "gcloud-auth", "g", false,
"Use gcloud's user configuration to retrieve a token for authentication.")
cmd.PersistentFlags().StringVar(&c.prometheusNamespace, "prometheus-namespace", "",
"Enable Prometheus for metric collection using the provided namespace")
cmd.PersistentFlags().StringVar(&c.httpPort, "http-port", "9090",
"Port for the Prometheus server to use")

// Global and per instance flags
cmd.PersistentFlags().StringVarP(&c.conf.Addr, "address", "a", "127.0.0.1",
Expand Down Expand Up @@ -182,6 +192,10 @@ func parseConfig(cmd *cobra.Command, conf *proxy.Config, args []string) error {
}
conf.DialerOpts = opts

if userHasSet("http-port") && !userHasSet("prometheus-namespace") {
return newBadCommandError("cannot specify --http-port without --prometheus-namespace")
}

var ics []proxy.InstanceConnConfig
for _, a := range args {
// Assume no query params initially
Expand Down Expand Up @@ -256,14 +270,46 @@ func runSignalWrapper(cmd *Command) error {

shutdownCh := make(chan error)

if cmd.prometheusNamespace != "" {
e, err := prometheus.NewExporter(prometheus.Options{
Namespace: cmd.prometheusNamespace,
})
if err != nil {
return err
}
mux := http.NewServeMux()
mux.Handle("/metrics", e)
addr := fmt.Sprintf("localhost:%s", cmd.httpPort)
server := &http.Server{Addr: addr, Handler: mux}
go func() {
select {
case <-ctx.Done():
// Give the HTTP server a second to shutdown cleanly.
ctx2, _ := context.WithTimeout(context.Background(), time.Second)
if err := server.Shutdown(ctx2); err != nil {
cmd.Printf("failed to shutdown Prometheus HTTP server: %v\n", err)
}
}
}()
go func() {
err := server.ListenAndServe()
if err == http.ErrServerClosed {
return
}
if err != nil {
shutdownCh <- fmt.Errorf("failed to start prometheus HTTP server: %v", err)
}
}()
}

// watch for sigterm / sigint signals
signals := make(chan os.Signal, 1)
signal.Notify(signals, syscall.SIGTERM, syscall.SIGINT)
go func() {
var s os.Signal
select {
case s = <-signals:
case <-cmd.Context().Done():
case <-ctx.Done():
// this should only happen when the context supplied in tests in canceled
s = syscall.SIGINT
}
Expand Down
49 changes: 49 additions & 0 deletions cmd/root_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"context"
"errors"
"net"
"net/http"
"sync"
"testing"
"time"
Expand Down Expand Up @@ -280,6 +281,10 @@ func TestNewCommandWithErrors(t *testing.T) {
desc: "using the unix socket and port query params",
args: []string{"proj:region:inst?unix-socket=/path&port=5000"},
},
{
desc: "enabling a Prometheus port without a namespace",
args: []string{"--htto-port", "1111", "proj:region:inst"},
},
}

for _, tc := range tcs {
Expand Down Expand Up @@ -349,3 +354,47 @@ func TestCommandWithCustomDialer(t *testing.T) {
t.Fatalf("want = %v, got = %v", want, got)
}
}

func TestPrometheusMetricsEndpoint(t *testing.T) {
c := NewCommand(WithDialer(&spyDialer{}))
// Keep the test output quiet
c.SilenceUsage = true
c.SilenceErrors = true
c.SetArgs([]string{
"--prometheus-namespace", "prometheus",
"my-project:my-region:my-instance"})

ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()

go c.ExecuteContext(ctx)

// try to dial metrics server for a max of ~10s to give the proxy time to
// start up.
tryDial := func(addr string) (*http.Response, error) {
var (
resp *http.Response
attempts int
err error
)
for {
if attempts > 10 {
return resp, err
}
resp, err = http.Get(addr)
if err != nil {
attempts++
time.Sleep(time.Second)
continue
}
return resp, err
}
}
resp, err := tryDial("http://localhost:9090/metrics") // default port set by http-port flag
if err != nil {
t.Fatalf("failed to dial metrics endpoint: %v", err)
}
if resp.StatusCode != http.StatusOK {
t.Fatalf("expected a 200 status, got = %v", resp.StatusCode)
}
}
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ go 1.16
require (
cloud.google.com/go/cloudsqlconn v0.2.1-0.20220401153611-87e713b37755
cloud.google.com/go/compute v1.6.1
contrib.go.opencensus.io/exporter/prometheus v0.4.1
github.com/GoogleCloudPlatform/cloudsql-proxy v1.29.0
github.com/coreos/go-systemd/v22 v22.3.2
github.com/denisenkom/go-mssqldb v0.12.0
Expand Down
Loading

0 comments on commit eeb2029

Please sign in to comment.