Skip to content

feat: add pprof and prometheus metrics to coder server #1266

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
May 3, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 37 additions & 2 deletions cli/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"log"
"net"
"net/http"
"net/http/pprof"
"net/url"
"os"
"os/signal"
Expand All @@ -23,6 +24,7 @@ import (
"github.com/google/go-github/v43/github"
"github.com/pion/turn/v2"
"github.com/pion/webrtc/v3"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/spf13/cobra"
"golang.org/x/oauth2"
xgithub "golang.org/x/oauth2/github"
Expand Down Expand Up @@ -55,6 +57,10 @@ func server() *cobra.Command {
var (
accessURL string
address string
promEnabled bool
promAddress string
pprofEnabled bool
pprofAddress string
cacheDir string
dev bool
devUserEmail string
Expand Down Expand Up @@ -245,6 +251,17 @@ func server() *cobra.Command {
}
}

// This prevents the pprof import from being accidentally deleted.
var _ = pprof.Handler
if pprofEnabled {
//nolint:revive
defer serveHandler(cmd.Context(), logger, nil, pprofAddress, "pprof")()
}
if promEnabled {
//nolint:revive
defer serveHandler(cmd.Context(), logger, promhttp.Handler(), promAddress, "prometheus")()
}

errCh := make(chan error, 1)
provisionerDaemons := make([]*provisionerd.Server, 0)
for i := 0; uint8(i) < provisionerDaemonCount; i++ {
Expand Down Expand Up @@ -400,8 +417,12 @@ func server() *cobra.Command {
},
}

cliflag.StringVarP(root.Flags(), &accessURL, "access-url", "", "CODER_ACCESS_URL", "", "Specifies the external URL to access Coder")
cliflag.StringVarP(root.Flags(), &address, "address", "a", "CODER_ADDRESS", "127.0.0.1:3000", "The address to serve the API and dashboard")
cliflag.StringVarP(root.Flags(), &accessURL, "access-url", "", "CODER_ACCESS_URL", "", "Specifies the external URL to access Coder.")
cliflag.StringVarP(root.Flags(), &address, "address", "a", "CODER_ADDRESS", "127.0.0.1:3000", "The address to serve the API and dashboard.")
cliflag.BoolVarP(root.Flags(), &promEnabled, "prometheus-enable", "", "CODER_PROMETHEUS_ENABLE", false, "Enable serving prometheus metrics on the addressdefined by --prometheus-address.")
cliflag.StringVarP(root.Flags(), &promAddress, "prometheus-address", "", "CODER_PROMETHEUS_ADDRESS", "127.0.0.1:2112", "The address to serve prometheus metrics.")
cliflag.BoolVarP(root.Flags(), &promEnabled, "pprof-enable", "", "CODER_PPROF_ENABLE", false, "Enable serving pprof metrics on the address defined by --pprof-address.")
cliflag.StringVarP(root.Flags(), &pprofAddress, "pprof-address", "", "CODER_PPROF_ADDRESS", "127.0.0.1:6060", "The address to serve pprof.")
// systemd uses the CACHE_DIRECTORY environment variable!
cliflag.StringVarP(root.Flags(), &cacheDir, "cache-dir", "", "CACHE_DIRECTORY", filepath.Join(os.TempDir(), "coder-cache"), "Specifies a directory to cache binaries for provision operations.")
cliflag.BoolVarP(root.Flags(), &dev, "dev", "", "CODER_DEV_MODE", false, "Serve Coder in dev mode for tinkering")
Expand Down Expand Up @@ -661,6 +682,20 @@ func configureGithubOAuth2(accessURL *url.URL, clientID, clientSecret string, al
}, nil
}

func serveHandler(ctx context.Context, logger slog.Logger, handler http.Handler, addr, name string) (closeFunc func()) {
logger.Debug(ctx, "http server listening", slog.F("addr", addr), slog.F("name", name))

srv := &http.Server{Addr: addr, Handler: handler}
go func() {
err := srv.ListenAndServe()
if err != nil && !xerrors.Is(err, http.ErrServerClosed) {
logger.Error(ctx, "http server listen", slog.F("name", name), slog.Error(err))
}
}()

return func() { _ = srv.Close() }
}

type datadogLogger struct {
logger slog.Logger
}
Expand Down
12 changes: 11 additions & 1 deletion coderd/coderd.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"time"

"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
"github.com/pion/webrtc/v3"
"google.golang.org/api/idtoken"

Expand Down Expand Up @@ -68,9 +69,18 @@ func New(options *Options) (http.Handler, func()) {
})

r := chi.NewRouter()
r.Use(
func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
next.ServeHTTP(middleware.NewWrapResponseWriter(w, r.ProtoMajor), r)
})
},
httpmw.Prometheus,
chitrace.Middleware(),
)

r.Route("/api/v2", func(r chi.Router) {
r.Use(
chitrace.Middleware(),
// Specific routes can specify smaller limits.
httpmw.RateLimitPerMinute(options.APIRateLimit),
debugLogRequest(api.Logger),
Expand Down
110 changes: 110 additions & 0 deletions coderd/httpmw/prometheus.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package httpmw

import (
"net/http"
"strconv"
"time"

"github.com/go-chi/chi/v5"
chimw "github.com/go-chi/chi/v5/middleware"

"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
)

var (
requestsProcessed = promauto.NewCounterVec(prometheus.CounterOpts{
Namespace: "coderd",
Subsystem: "api",
Name: "requests_processed_total",
Help: "The total number of processed API requests",
}, []string{"code", "method", "path"})
requestsConcurrent = promauto.NewGauge(prometheus.GaugeOpts{
Namespace: "coderd",
Subsystem: "api",
Name: "concurrent_requests",
Help: "The number of concurrent API requests",
})
websocketsConcurrent = promauto.NewGauge(prometheus.GaugeOpts{
Namespace: "coderd",
Subsystem: "api",
Name: "concurrent_websockets",
Help: "The total number of concurrent API websockets",
})
websocketsDist = promauto.NewHistogramVec(prometheus.HistogramOpts{
Namespace: "coderd",
Subsystem: "api",
Name: "websocket_durations_ms",
Help: "Websocket duration distribution of requests in milliseconds",
Buckets: []float64{
durationToFloatMs(01 * time.Millisecond),
durationToFloatMs(01 * time.Second),
durationToFloatMs(01 * time.Minute),
durationToFloatMs(01 * time.Hour),
durationToFloatMs(15 * time.Hour),
durationToFloatMs(30 * time.Hour),
},
}, []string{"path"})
requestsDist = promauto.NewHistogramVec(prometheus.HistogramOpts{
Namespace: "coderd",
Subsystem: "api",
Name: "request_latencies_ms",
Help: "Latency distribution of requests in milliseconds",
Buckets: []float64{1, 5, 10, 25, 50, 100, 500, 1000, 5000, 10000, 30000},
}, []string{"method", "path"})
)

func durationToFloatMs(d time.Duration) float64 {
return float64(d.Milliseconds())
}

func Prometheus(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var (
start = time.Now()
method = r.Method
rctx = chi.RouteContext(r.Context())
)
sw, ok := w.(chimw.WrapResponseWriter)
if !ok {
panic("dev error: http.ResponseWriter is not chimw.WrapResponseWriter")
}

var (
dist *prometheus.HistogramVec
distOpts []string
)
// We want to count websockets separately.
if isWebsocketUpgrade(r) {
websocketsConcurrent.Inc()
defer websocketsConcurrent.Dec()

dist = websocketsDist
} else {
requestsConcurrent.Inc()
defer requestsConcurrent.Dec()

dist = requestsDist
distOpts = []string{method}
}

next.ServeHTTP(w, r)

path := rctx.RoutePattern()
distOpts = append(distOpts, path)
statusStr := strconv.Itoa(sw.Status())

requestsProcessed.WithLabelValues(statusStr, method, path).Inc()
dist.WithLabelValues(distOpts...).Observe(float64(time.Since(start)) / 1e6)
})
}

func isWebsocketUpgrade(r *http.Request) bool {
vs := r.Header.Values("Upgrade")
for _, v := range vs {
if v == "websocket" {
return true
}
}
return false
}
6 changes: 6 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ require (
github.com/pion/webrtc/v3 v3.1.34
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8
github.com/pkg/sftp v1.13.4
github.com/prometheus/client_golang v1.12.1
github.com/quasilyte/go-ruleguard/dsl v0.3.19
github.com/robfig/cron/v3 v3.0.1
github.com/spf13/cobra v1.4.0
Expand Down Expand Up @@ -135,6 +136,7 @@ require (
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect
github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cenkalti/backoff/v4 v4.1.2 // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/charmbracelet/bubbles v0.10.3 // indirect
Expand Down Expand Up @@ -184,6 +186,7 @@ require (
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-colorable v0.1.12 // indirect
github.com/mattn/go-runewidth v0.0.13 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect
github.com/miekg/dns v1.1.45 // indirect
github.com/mitchellh/go-wordwrap v1.0.1 // indirect
Expand Down Expand Up @@ -213,6 +216,9 @@ require (
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/pquerna/cachecontrol v0.1.0 // indirect
github.com/prometheus/client_model v0.2.0 // indirect
github.com/prometheus/common v0.32.1 // indirect
github.com/prometheus/procfs v0.7.3 // indirect
github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/sirupsen/logrus v1.8.1 // indirect
Expand Down
7 changes: 4 additions & 3 deletions site/src/api/typesGenerated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ export interface CreateWorkspaceBuildRequest {
// This is likely an enum in an external package ("github.com/coder/coder/coderd/database.WorkspaceTransition")
readonly transition: string
readonly dry_run: boolean
readonly state: string
}

// From codersdk/organizations.go:52:6
Expand Down Expand Up @@ -265,12 +266,12 @@ export interface UpdateUserProfileRequest {
readonly username: string
}

// From codersdk/workspaces.go:94:6
// From codersdk/workspaces.go:95:6
export interface UpdateWorkspaceAutostartRequest {
readonly schedule: string
}

// From codersdk/workspaces.go:114:6
// From codersdk/workspaces.go:115:6
export interface UpdateWorkspaceAutostopRequest {
readonly schedule: string
}
Expand Down Expand Up @@ -366,7 +367,7 @@ export interface WorkspaceAgentResourceMetadata {
readonly cpu_mhz: number
}

// From codersdk/workspacebuilds.go:17:6
// From codersdk/workspacebuilds.go:18:6
export interface WorkspaceBuild {
readonly id: string
readonly created_at: string
Expand Down