feat: Add active users prometheus metric (#3406)

This  allows deployments using our Prometheus export t determine
the number of active users in the past hour.

The interval is an hour to align with API key last used refresh times.

SSH connections poll to check shutdown time, so this will be accurate
even on long-running connections without dashboard requests.
This commit is contained in:
Kyle Carberry
2022-08-08 10:09:46 -05:00
committed by GitHub
parent 13a2014d7f
commit 3279504cbe
8 changed files with 307 additions and 55 deletions
+22 -11
View File
@@ -30,6 +30,7 @@ import (
"github.com/google/uuid"
"github.com/pion/turn/v2"
"github.com/pion/webrtc/v3"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/spf13/afero"
"github.com/spf13/cobra"
@@ -53,6 +54,7 @@ import (
"github.com/coder/coder/coderd/database/databasefake"
"github.com/coder/coder/coderd/devtunnel"
"github.com/coder/coder/coderd/gitsshkey"
"github.com/coder/coder/coderd/prometheusmetrics"
"github.com/coder/coder/coderd/telemetry"
"github.com/coder/coder/coderd/tracing"
"github.com/coder/coder/coderd/turnconn"
@@ -392,6 +394,26 @@ func server() *cobra.Command {
defer options.Telemetry.Close()
}
// This prevents the pprof import from being accidentally deleted.
_ = pprof.Handler
if pprofEnabled {
//nolint:revive
defer serveHandler(ctx, logger, nil, pprofAddress, "pprof")()
}
if promEnabled {
options.PrometheusRegistry = prometheus.NewRegistry()
closeFunc, err := prometheusmetrics.ActiveUsers(ctx, options.PrometheusRegistry, options.Database, 0)
if err != nil {
return xerrors.Errorf("register active users prometheus metric: %w", err)
}
defer closeFunc()
//nolint:revive
defer serveHandler(ctx, logger, promhttp.InstrumentMetricHandler(
options.PrometheusRegistry, promhttp.HandlerFor(options.PrometheusRegistry, promhttp.HandlerOpts{}),
), promAddress, "prometheus")()
}
coderAPI := coderd.New(options)
defer coderAPI.Close()
@@ -406,17 +428,6 @@ func server() *cobra.Command {
}
}
// This prevents the pprof import from being accidentally deleted.
_ = pprof.Handler
if pprofEnabled {
//nolint:revive
defer serveHandler(ctx, logger, nil, pprofAddress, "pprof")()
}
if promEnabled {
//nolint:revive
defer serveHandler(ctx, logger, promhttp.Handler(), promAddress, "prometheus")()
}
// Since errCh only has one buffered slot, all routines
// sending on it must be wrapped in a select/default to
// avoid leaving dangling goroutines waiting for the
+55
View File
@@ -1,6 +1,7 @@
package cli_test
import (
"bufio"
"context"
"crypto/ecdsa"
"crypto/elliptic"
@@ -10,6 +11,7 @@ import (
"crypto/x509/pkix"
"encoding/json"
"encoding/pem"
"fmt"
"math/big"
"net"
"net/http"
@@ -17,10 +19,13 @@ import (
"net/url"
"os"
"runtime"
"strconv"
"strings"
"testing"
"time"
"github.com/go-chi/chi"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/goleak"
@@ -374,6 +379,56 @@ func TestServer(t *testing.T) {
cancelFunc()
<-errC
})
t.Run("Prometheus", func(t *testing.T) {
t.Parallel()
ctx, cancelFunc := context.WithCancel(context.Background())
defer cancelFunc()
random, err := net.Listen("tcp", "127.0.0.1:0")
require.NoError(t, err)
_ = random.Close()
tcpAddr, valid := random.Addr().(*net.TCPAddr)
require.True(t, valid)
randomPort := tcpAddr.Port
root, cfg := clitest.New(t,
"server",
"--in-memory",
"--address", ":0",
"--provisioner-daemons", "1",
"--prometheus-enable",
"--prometheus-address", ":"+strconv.Itoa(randomPort),
"--cache-dir", t.TempDir(),
)
serverErr := make(chan error, 1)
go func() {
serverErr <- root.ExecuteContext(ctx)
}()
_ = waitAccessURL(t, cfg)
var res *http.Response
require.Eventually(t, func() bool {
req, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("http://127.0.0.1:%d", randomPort), nil)
assert.NoError(t, err)
res, err = http.DefaultClient.Do(req)
return err == nil
}, testutil.WaitShort, testutil.IntervalFast)
scanner := bufio.NewScanner(res.Body)
hasActiveUsers := false
for scanner.Scan() {
// This metric is manually registered to be tracked in the server. That's
// why we test it's tracked here.
if strings.HasPrefix(scanner.Text(), "coderd_api_active_users_duration_hour") {
hasActiveUsers = true
continue
}
}
require.NoError(t, scanner.Err())
require.True(t, hasActiveUsers)
cancelFunc()
<-serverErr
})
}
func generateTLSCertificate(t testing.TB) (certPath, keyPath string) {