feat: expose app insights as Prometheus metrics (#10346)

This commit is contained in:
Marcin Tojek
2023-11-07 17:14:59 +01:00
committed by GitHub
parent 8441c36dfb
commit 0a550815e9
12 changed files with 421 additions and 20 deletions
@@ -13,9 +13,13 @@ import (
"cdr.dev/slog"
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/codersdk"
)
var templatesActiveUsersDesc = prometheus.NewDesc("coderd_insights_templates_active_users", "The number of active users of the template.", []string{"template_name"}, nil)
var (
templatesActiveUsersDesc = prometheus.NewDesc("coderd_insights_templates_active_users", "The number of active users of the template.", []string{"template_name"}, nil)
applicationsUsageSecondsDesc = prometheus.NewDesc("coderd_insights_applications_usage_seconds", "The application usage per template.", []string{"template_name", "application_name", "slug"}, nil)
)
type MetricsCollector struct {
database database.Store
@@ -28,6 +32,7 @@ type MetricsCollector struct {
type insightsData struct {
templates []database.GetTemplateInsightsByTemplateRow
apps []database.GetTemplateAppInsightsByTemplateRow
templateNames map[uuid.UUID]string
}
@@ -70,9 +75,10 @@ func (mc *MetricsCollector) Run(ctx context.Context) (func(), error) {
// Phase 1: Fetch insights from database
// FIXME errorGroup will be used to fetch insights for apps and parameters
eg, egCtx := errgroup.WithContext(ctx)
eg.SetLimit(1)
eg.SetLimit(2)
var templateInsights []database.GetTemplateInsightsByTemplateRow
var appInsights []database.GetTemplateAppInsightsByTemplateRow
eg.Go(func() error {
var err error
@@ -85,13 +91,24 @@ func (mc *MetricsCollector) Run(ctx context.Context) (func(), error) {
}
return err
})
eg.Go(func() error {
var err error
appInsights, err = mc.database.GetTemplateAppInsightsByTemplate(egCtx, database.GetTemplateAppInsightsByTemplateParams{
StartTime: startTime,
EndTime: endTime,
})
if err != nil {
mc.logger.Error(ctx, "unable to fetch application insights from database", slog.Error(err))
}
return err
})
err := eg.Wait()
if err != nil {
return
}
// Phase 2: Collect template IDs, and fetch relevant details
templateIDs := uniqueTemplateIDs(templateInsights)
templateIDs := uniqueTemplateIDs(templateInsights, appInsights)
templateNames := make(map[uuid.UUID]string, len(templateIDs))
if len(templateIDs) > 0 {
@@ -107,7 +124,9 @@ func (mc *MetricsCollector) Run(ctx context.Context) (func(), error) {
// Refresh the collector state
mc.data.Store(&insightsData{
templates: templateInsights,
templates: templateInsights,
apps: appInsights,
templateNames: templateNames,
})
}
@@ -133,6 +152,7 @@ func (mc *MetricsCollector) Run(ctx context.Context) (func(), error) {
func (*MetricsCollector) Describe(descCh chan<- *prometheus.Desc) {
descCh <- templatesActiveUsersDesc
descCh <- applicationsUsageSecondsDesc
}
func (mc *MetricsCollector) Collect(metricsCh chan<- prometheus.Metric) {
@@ -143,6 +163,40 @@ func (mc *MetricsCollector) Collect(metricsCh chan<- prometheus.Metric) {
return // insights data not loaded yet
}
// Custom apps
for _, appRow := range data.apps {
metricsCh <- prometheus.MustNewConstMetric(applicationsUsageSecondsDesc, prometheus.GaugeValue, float64(appRow.UsageSeconds), data.templateNames[appRow.TemplateID],
appRow.DisplayName.String, appRow.SlugOrPort)
}
// Built-in apps
for _, templateRow := range data.templates {
metricsCh <- prometheus.MustNewConstMetric(applicationsUsageSecondsDesc, prometheus.GaugeValue,
float64(templateRow.UsageVscodeSeconds),
data.templateNames[templateRow.TemplateID],
codersdk.TemplateBuiltinAppDisplayNameVSCode,
"")
metricsCh <- prometheus.MustNewConstMetric(applicationsUsageSecondsDesc, prometheus.GaugeValue,
float64(templateRow.UsageJetbrainsSeconds),
data.templateNames[templateRow.TemplateID],
codersdk.TemplateBuiltinAppDisplayNameJetBrains,
"")
metricsCh <- prometheus.MustNewConstMetric(applicationsUsageSecondsDesc, prometheus.GaugeValue,
float64(templateRow.UsageReconnectingPtySeconds),
data.templateNames[templateRow.TemplateID],
codersdk.TemplateBuiltinAppDisplayNameWebTerminal,
"")
metricsCh <- prometheus.MustNewConstMetric(applicationsUsageSecondsDesc, prometheus.GaugeValue,
float64(templateRow.UsageSshSeconds),
data.templateNames[templateRow.TemplateID],
codersdk.TemplateBuiltinAppDisplayNameSSH,
"")
}
// Templates
for _, templateRow := range data.templates {
metricsCh <- prometheus.MustNewConstMetric(templatesActiveUsersDesc, prometheus.GaugeValue, float64(templateRow.ActiveUsers), data.templateNames[templateRow.TemplateID])
}
@@ -150,11 +204,14 @@ func (mc *MetricsCollector) Collect(metricsCh chan<- prometheus.Metric) {
// Helper functions below.
func uniqueTemplateIDs(templateInsights []database.GetTemplateInsightsByTemplateRow) []uuid.UUID {
func uniqueTemplateIDs(templateInsights []database.GetTemplateInsightsByTemplateRow, appInsights []database.GetTemplateAppInsightsByTemplateRow) []uuid.UUID {
tids := map[uuid.UUID]bool{}
for _, t := range templateInsights {
tids[t.TemplateID] = true
}
for _, t := range appInsights {
tids[t.TemplateID] = true
}
uniqueUUIDs := make([]uuid.UUID, len(tids))
var i int
@@ -5,25 +5,30 @@ import (
"encoding/json"
"io"
"os"
"strings"
"testing"
"time"
"github.com/google/uuid"
"github.com/prometheus/client_golang/prometheus"
io_prometheus_client "github.com/prometheus/client_model/go"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"cdr.dev/slog/sloggers/slogtest"
"github.com/coder/coder/v2/agent/agenttest"
"github.com/coder/coder/v2/coderd/coderdtest"
"github.com/coder/coder/v2/coderd/database/dbauthz"
"github.com/coder/coder/v2/coderd/database/dbtestutil"
"github.com/coder/coder/v2/coderd/prometheusmetrics/insights"
"github.com/coder/coder/v2/coderd/workspaceapps"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/provisioner/echo"
"github.com/coder/coder/v2/provisionersdk/proto"
"github.com/coder/coder/v2/testutil"
)
func TestCollect_TemplateInsights(t *testing.T) {
func TestCollectInsights(t *testing.T) {
t.Parallel()
logger := slogtest.Make(t, &slogtest.Options{IgnoreErrors: true})
@@ -53,9 +58,11 @@ func TestCollect_TemplateInsights(t *testing.T) {
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionPlan: echo.PlanComplete,
ProvisionApply: echo.ProvisionApplyWithAgent(authToken),
ProvisionApply: provisionApplyWithAgentAndApp(authToken),
})
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) {
ctr.Name = "golden-template"
})
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
require.Empty(t, template.BuildTimeStats[codersdk.WorkspaceTransitionStart])
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
@@ -66,6 +73,24 @@ func TestCollect_TemplateInsights(t *testing.T) {
_ = agenttest.New(t, client.URL, authToken)
resources := coderdtest.AwaitWorkspaceAgents(t, client, workspace.ID)
// Fake app usage
reporter := workspaceapps.NewStatsDBReporter(db, workspaceapps.DefaultStatsDBReporterBatchSize)
//nolint:gocritic // This is a test.
err = reporter.Report(dbauthz.AsSystemRestricted(context.Background()), []workspaceapps.StatsReport{
{
UserID: user.UserID,
WorkspaceID: workspace.ID,
AgentID: resources[0].Agents[0].ID,
AccessMethod: "terminal",
SlugOrPort: "golden-slug",
SessionID: uuid.New(),
SessionStartedAt: time.Now().Add(-3 * time.Minute),
SessionEndedAt: time.Now().Add(-time.Minute).Add(-time.Second),
Requests: 1,
},
})
require.NoError(t, err, "want no error inserting app stats")
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
@@ -97,6 +122,11 @@ func TestCollect_TemplateInsights(t *testing.T) {
err = sess.Start("cat")
require.NoError(t, err)
defer func() {
_ = sess.Close()
_ = sshConn.Close()
}()
goldenFile, err := os.ReadFile("testdata/insights-metrics.json")
require.NoError(t, err)
golden := map[string]int{}
@@ -112,9 +142,13 @@ func TestCollect_TemplateInsights(t *testing.T) {
// Then
for _, metric := range metrics {
switch metric.GetName() {
case "coderd_insights_templates_active_users":
case "coderd_insights_applications_usage_seconds", "coderd_insights_templates_active_users":
for _, m := range metric.Metric {
collected[metric.GetName()] = int(m.Gauge.GetValue())
key := metric.GetName()
if len(m.Label) > 0 {
key = key + "[" + metricLabelAsString(m) + "]"
}
collected[key] = int(m.Gauge.GetValue())
}
default:
require.FailNowf(t, "unexpected metric collected", "metric: %s", metric.GetName())
@@ -122,11 +156,41 @@ func TestCollect_TemplateInsights(t *testing.T) {
}
return assert.ObjectsAreEqualValues(golden, collected)
}, testutil.WaitMedium, testutil.IntervalFast, "template insights are missing")
}, testutil.WaitMedium, testutil.IntervalFast, "template insights are inconsistent with golden files, got: %v", collected)
}
// We got our latency metrics, close the connection.
_ = sess.Close()
_ = sshConn.Close()
func metricLabelAsString(m *io_prometheus_client.Metric) string {
var labels []string
for _, labelPair := range m.Label {
labels = append(labels, labelPair.GetName()+"="+labelPair.GetValue())
}
return strings.Join(labels, ",")
}
require.EqualValues(t, golden, collected)
func provisionApplyWithAgentAndApp(authToken string) []*proto.Response {
return []*proto.Response{{
Type: &proto.Response_Apply{
Apply: &proto.ApplyComplete{
Resources: []*proto.Resource{{
Name: "example",
Type: "aws_instance",
Agents: []*proto.Agent{{
Id: uuid.NewString(),
Name: "example",
Auth: &proto.Agent_Token{
Token: authToken,
},
Apps: []*proto.App{
{
Slug: "golden-slug",
DisplayName: "Golden Slug",
SharingLevel: proto.AppSharingLevel_OWNER,
Url: "http://localhost:1234",
},
},
}},
}},
},
},
}}
}
@@ -1,3 +1,8 @@
{
"coderd_insights_templates_active_users": 1
"coderd_insights_applications_usage_seconds[application_name=JetBrains,slug=,template_name=golden-template]": 0,
"coderd_insights_applications_usage_seconds[application_name=Visual Studio Code,slug=,template_name=golden-template]": 0,
"coderd_insights_applications_usage_seconds[application_name=Web Terminal,slug=,template_name=golden-template]": 0,
"coderd_insights_applications_usage_seconds[application_name=SSH,slug=,template_name=golden-template]": 60,
"coderd_insights_applications_usage_seconds[application_name=Golden Slug,slug=golden-slug,template_name=golden-template]": 180,
"coderd_insights_templates_active_users[template_name=golden-template]": 1
}