Skip to content

Commit 5d0af5e

Browse files
committed
Merge branch 'main' into tag-coder-users-dx
2 parents a5c1949 + b2a1de9 commit 5d0af5e

File tree

9 files changed

+122
-38
lines changed

9 files changed

+122
-38
lines changed

enterprise/coderd/prebuilds/metricscollector.go

Lines changed: 86 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,17 @@ package prebuilds
22

33
import (
44
"context"
5+
"fmt"
6+
"sync/atomic"
57
"time"
68

7-
"cdr.dev/slog"
8-
99
"github.com/prometheus/client_golang/prometheus"
10+
"golang.org/x/xerrors"
11+
12+
"cdr.dev/slog"
1013

1114
"github.com/coder/coder/v2/coderd/database"
12-
"github.com/coder/coder/v2/coderd/database/dbauthz"
15+
"github.com/coder/coder/v2/coderd/database/dbtime"
1316
"github.com/coder/coder/v2/coderd/prebuilds"
1417
)
1518

@@ -55,20 +58,34 @@ var (
5558
labels,
5659
nil,
5760
)
61+
lastUpdateDesc = prometheus.NewDesc(
62+
"coderd_prebuilt_workspaces_metrics_last_updated",
63+
"The unix timestamp when the metrics related to prebuilt workspaces were last updated; these metrics are cached.",
64+
[]string{},
65+
nil,
66+
)
67+
)
68+
69+
const (
70+
metricsUpdateInterval = time.Second * 15
71+
metricsUpdateTimeout = time.Second * 10
5872
)
5973

6074
type MetricsCollector struct {
6175
database database.Store
6276
logger slog.Logger
6377
snapshotter prebuilds.StateSnapshotter
78+
79+
latestState atomic.Pointer[metricsState]
6480
}
6581

6682
var _ prometheus.Collector = new(MetricsCollector)
6783

6884
func NewMetricsCollector(db database.Store, logger slog.Logger, snapshotter prebuilds.StateSnapshotter) *MetricsCollector {
85+
log := logger.Named("prebuilds_metrics_collector")
6986
return &MetricsCollector{
7087
database: db,
71-
logger: logger.Named("prebuilds_metrics_collector"),
88+
logger: log,
7289
snapshotter: snapshotter,
7390
}
7491
}
@@ -80,38 +97,34 @@ func (*MetricsCollector) Describe(descCh chan<- *prometheus.Desc) {
8097
descCh <- desiredPrebuildsDesc
8198
descCh <- runningPrebuildsDesc
8299
descCh <- eligiblePrebuildsDesc
100+
descCh <- lastUpdateDesc
83101
}
84102

103+
// Collect uses the cached state to set configured metrics.
104+
// The state is cached because this function can be called multiple times per second and retrieving the current state
105+
// is an expensive operation.
85106
func (mc *MetricsCollector) Collect(metricsCh chan<- prometheus.Metric) {
86-
// nolint:gocritic // We need to set an authz context to read metrics from the db.
87-
ctx, cancel := context.WithTimeout(dbauthz.AsPrebuildsOrchestrator(context.Background()), 10*time.Second)
88-
defer cancel()
89-
prebuildMetrics, err := mc.database.GetPrebuildMetrics(ctx)
90-
if err != nil {
91-
mc.logger.Error(ctx, "failed to get prebuild metrics", slog.Error(err))
107+
currentState := mc.latestState.Load() // Grab a copy; it's ok if it goes stale during the course of this func.
108+
if currentState == nil {
109+
mc.logger.Warn(context.Background(), "failed to set prebuilds metrics; state not set")
110+
metricsCh <- prometheus.MustNewConstMetric(lastUpdateDesc, prometheus.GaugeValue, 0)
92111
return
93112
}
94113

95-
for _, metric := range prebuildMetrics {
114+
for _, metric := range currentState.prebuildMetrics {
96115
metricsCh <- prometheus.MustNewConstMetric(createdPrebuildsDesc, prometheus.CounterValue, float64(metric.CreatedCount), metric.TemplateName, metric.PresetName, metric.OrganizationName)
97116
metricsCh <- prometheus.MustNewConstMetric(failedPrebuildsDesc, prometheus.CounterValue, float64(metric.FailedCount), metric.TemplateName, metric.PresetName, metric.OrganizationName)
98117
metricsCh <- prometheus.MustNewConstMetric(claimedPrebuildsDesc, prometheus.CounterValue, float64(metric.ClaimedCount), metric.TemplateName, metric.PresetName, metric.OrganizationName)
99118
}
100119

101-
snapshot, err := mc.snapshotter.SnapshotState(ctx, mc.database)
102-
if err != nil {
103-
mc.logger.Error(ctx, "failed to get latest prebuild state", slog.Error(err))
104-
return
105-
}
106-
107-
for _, preset := range snapshot.Presets {
120+
for _, preset := range currentState.snapshot.Presets {
108121
if !preset.UsingActiveVersion {
109122
continue
110123
}
111124

112-
presetSnapshot, err := snapshot.FilterByPreset(preset.ID)
125+
presetSnapshot, err := currentState.snapshot.FilterByPreset(preset.ID)
113126
if err != nil {
114-
mc.logger.Error(ctx, "failed to filter by preset", slog.Error(err))
127+
mc.logger.Error(context.Background(), "failed to filter by preset", slog.Error(err))
115128
continue
116129
}
117130
state := presetSnapshot.CalculateState()
@@ -120,4 +133,57 @@ func (mc *MetricsCollector) Collect(metricsCh chan<- prometheus.Metric) {
120133
metricsCh <- prometheus.MustNewConstMetric(runningPrebuildsDesc, prometheus.GaugeValue, float64(state.Actual), preset.TemplateName, preset.Name, preset.OrganizationName)
121134
metricsCh <- prometheus.MustNewConstMetric(eligiblePrebuildsDesc, prometheus.GaugeValue, float64(state.Eligible), preset.TemplateName, preset.Name, preset.OrganizationName)
122135
}
136+
137+
metricsCh <- prometheus.MustNewConstMetric(lastUpdateDesc, prometheus.GaugeValue, float64(currentState.createdAt.Unix()))
138+
}
139+
140+
type metricsState struct {
141+
prebuildMetrics []database.GetPrebuildMetricsRow
142+
snapshot *prebuilds.GlobalSnapshot
143+
createdAt time.Time
144+
}
145+
146+
// BackgroundFetch updates the metrics state every given interval.
147+
func (mc *MetricsCollector) BackgroundFetch(ctx context.Context, updateInterval, updateTimeout time.Duration) {
148+
tick := time.NewTicker(time.Nanosecond)
149+
defer tick.Stop()
150+
151+
for {
152+
select {
153+
case <-ctx.Done():
154+
return
155+
case <-tick.C:
156+
// Tick immediately, then set regular interval.
157+
tick.Reset(updateInterval)
158+
159+
if err := mc.UpdateState(ctx, updateTimeout); err != nil {
160+
mc.logger.Error(ctx, "failed to update prebuilds metrics state", slog.Error(err))
161+
}
162+
}
163+
}
164+
}
165+
166+
// UpdateState builds the current metrics state.
167+
func (mc *MetricsCollector) UpdateState(ctx context.Context, timeout time.Duration) error {
168+
start := time.Now()
169+
fetchCtx, fetchCancel := context.WithTimeout(ctx, timeout)
170+
defer fetchCancel()
171+
172+
prebuildMetrics, err := mc.database.GetPrebuildMetrics(fetchCtx)
173+
if err != nil {
174+
return xerrors.Errorf("fetch prebuild metrics: %w", err)
175+
}
176+
177+
snapshot, err := mc.snapshotter.SnapshotState(fetchCtx, mc.database)
178+
if err != nil {
179+
return xerrors.Errorf("snapshot state: %w", err)
180+
}
181+
mc.logger.Debug(ctx, "fetched prebuilds metrics state", slog.F("duration_secs", fmt.Sprintf("%.2f", time.Since(start).Seconds())))
182+
183+
mc.latestState.Store(&metricsState{
184+
prebuildMetrics: prebuildMetrics,
185+
snapshot: snapshot,
186+
createdAt: dbtime.Now(),
187+
})
188+
return nil
123189
}

enterprise/coderd/prebuilds/metricscollector_test.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
"github.com/coder/quartz"
1717

1818
"github.com/coder/coder/v2/coderd/database"
19+
"github.com/coder/coder/v2/coderd/database/dbauthz"
1920
"github.com/coder/coder/v2/coderd/database/dbgen"
2021
"github.com/coder/coder/v2/coderd/database/dbtestutil"
2122
agplprebuilds "github.com/coder/coder/v2/coderd/prebuilds"
@@ -248,6 +249,10 @@ func TestMetricsCollector(t *testing.T) {
248249
setupTestDBWorkspaceAgent(t, db, workspace.ID, eligible)
249250
}
250251

252+
// Force an update to the metrics state to allow the collector to collect fresh metrics.
253+
// nolint:gocritic // Authz context needed to retrieve state.
254+
require.NoError(t, collector.UpdateState(dbauthz.AsPrebuildsOrchestrator(ctx), testutil.WaitLong))
255+
251256
metricsFamilies, err := registry.Gather()
252257
require.NoError(t, err)
253258

enterprise/coderd/prebuilds/reconcile.go

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"database/sql"
66
"fmt"
77
"math"
8+
"sync"
89
"sync/atomic"
910
"time"
1011

@@ -67,10 +68,12 @@ func NewStoreReconciler(store database.Store,
6768
provisionNotifyCh: make(chan database.ProvisionerJob, 10),
6869
}
6970

70-
reconciler.metrics = NewMetricsCollector(store, logger, reconciler)
71-
if err := registerer.Register(reconciler.metrics); err != nil {
72-
// If the registerer fails to register the metrics collector, it's not fatal.
73-
logger.Error(context.Background(), "failed to register prometheus metrics", slog.Error(err))
71+
if registerer != nil {
72+
reconciler.metrics = NewMetricsCollector(store, logger, reconciler)
73+
if err := registerer.Register(reconciler.metrics); err != nil {
74+
// If the registerer fails to register the metrics collector, it's not fatal.
75+
logger.Error(context.Background(), "failed to register prometheus metrics", slog.Error(err))
76+
}
7477
}
7578

7679
return reconciler
@@ -87,16 +90,27 @@ func (c *StoreReconciler) Run(ctx context.Context) {
8790
slog.F("backoff_interval", c.cfg.ReconciliationBackoffInterval.String()),
8891
slog.F("backoff_lookback", c.cfg.ReconciliationBackoffLookback.String()))
8992

93+
var wg sync.WaitGroup
9094
ticker := c.clock.NewTicker(reconciliationInterval)
9195
defer ticker.Stop()
9296
defer func() {
97+
wg.Wait()
9398
c.done <- struct{}{}
9499
}()
95100

96101
// nolint:gocritic // Reconciliation Loop needs Prebuilds Orchestrator permissions.
97102
ctx, cancel := context.WithCancelCause(dbauthz.AsPrebuildsOrchestrator(ctx))
98103
c.cancelFn = cancel
99104

105+
// Start updating metrics in the background.
106+
if c.metrics != nil {
107+
wg.Add(1)
108+
go func() {
109+
defer wg.Done()
110+
c.metrics.BackgroundFetch(ctx, metricsUpdateInterval, metricsUpdateTimeout)
111+
}()
112+
}
113+
100114
// Everything is in place, reconciler can now be considered as running.
101115
//
102116
// NOTE: without this atomic bool, Stop might race with Run for the c.cancelFn above.

site/src/components/HelpTooltip/HelpTooltip.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import {
55
css,
66
useTheme,
77
} from "@emotion/react";
8-
import OpenInNewIcon from "@mui/icons-material/OpenInNew";
98
import Link from "@mui/material/Link";
109
import { Stack } from "components/Stack/Stack";
1110
import {
@@ -16,6 +15,7 @@ import {
1615
PopoverTrigger,
1716
usePopover,
1817
} from "components/deprecated/Popover/Popover";
18+
import { ExternalLinkIcon } from "lucide-react";
1919
import { CircleHelpIcon } from "lucide-react";
2020
import {
2121
type FC,
@@ -137,7 +137,7 @@ interface HelpTooltipLink {
137137
export const HelpTooltipLink: FC<HelpTooltipLink> = ({ children, href }) => {
138138
return (
139139
<Link href={href} target="_blank" rel="noreferrer" css={styles.link}>
140-
<OpenInNewIcon css={styles.linkIcon} />
140+
<ExternalLinkIcon className="size-icon-xs" css={styles.linkIcon} />
141141
{children}
142142
</Link>
143143
);

site/src/components/PaginationWidget/PaginationWidgetBase.tsx

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { useTheme } from "@emotion/react";
2-
import KeyboardArrowLeft from "@mui/icons-material/KeyboardArrowLeft";
3-
import KeyboardArrowRight from "@mui/icons-material/KeyboardArrowRight";
42
import useMediaQuery from "@mui/material/useMediaQuery";
3+
import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react";
54
import type { FC } from "react";
65
import { NumberedPageButton, PlaceholderPageButton } from "./PageButtons";
76
import { PaginationNavButton } from "./PaginationNavButton";
@@ -60,7 +59,7 @@ export const PaginationWidgetBase: FC<PaginationWidgetBaseProps> = ({
6059
}
6160
}}
6261
>
63-
<KeyboardArrowLeft />
62+
<ChevronLeftIcon className="size-icon-sm" />
6463
</PaginationNavButton>
6564

6665
{isMobile ? (
@@ -87,7 +86,7 @@ export const PaginationWidgetBase: FC<PaginationWidgetBaseProps> = ({
8786
}
8887
}}
8988
>
90-
<KeyboardArrowRight />
89+
<ChevronRightIcon className="size-icon-sm" />
9190
</PaginationNavButton>
9291
</div>
9392
);

site/src/components/RichParameterInput/RichParameterInput.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import type { Interpolation, Theme } from "@emotion/react";
2-
import SettingsIcon from "@mui/icons-material/Settings";
32
import Button from "@mui/material/Button";
43
import FormControlLabel from "@mui/material/FormControlLabel";
54
import FormHelperText from "@mui/material/FormHelperText";
@@ -13,6 +12,7 @@ import { ExternalImage } from "components/ExternalImage/ExternalImage";
1312
import { MemoizedMarkdown } from "components/Markdown/Markdown";
1413
import { Pill } from "components/Pill/Pill";
1514
import { Stack } from "components/Stack/Stack";
15+
import { SettingsIcon } from "lucide-react";
1616
import { CircleAlertIcon } from "lucide-react";
1717
import { type FC, type ReactNode, useState } from "react";
1818
import type {
@@ -153,7 +153,7 @@ const ParameterLabel: FC<ParameterLabelProps> = ({ parameter, isPreset }) => {
153153
)}
154154
{isPreset && (
155155
<Tooltip title="This value was set by a preset">
156-
<Pill type="info" icon={<SettingsIcon />}>
156+
<Pill type="info" icon={<SettingsIcon className="size-icon-xs" />}>
157157
Preset
158158
</Pill>
159159
</Tooltip>

site/src/pages/GroupsPage/GroupPage.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import type { Interpolation, Theme } from "@emotion/react";
22
import PersonAdd from "@mui/icons-material/PersonAdd";
3-
import SettingsOutlined from "@mui/icons-material/SettingsOutlined";
43
import LoadingButton from "@mui/lab/LoadingButton";
54
import Button from "@mui/material/Button";
65
import { getErrorMessage } from "api/errors";
@@ -50,6 +49,7 @@ import {
5049
TableToolbar,
5150
} from "components/TableToolbar/TableToolbar";
5251
import { MemberAutocomplete } from "components/UserAutocomplete/UserAutocomplete";
52+
import { SettingsIcon } from "lucide-react";
5353
import { EllipsisVertical, TrashIcon } from "lucide-react";
5454
import { type FC, useState } from "react";
5555
import { Helmet } from "react-helmet-async";
@@ -123,7 +123,7 @@ const GroupPage: FC = () => {
123123
<Stack direction="row" spacing={2}>
124124
<Button
125125
component={RouterLink}
126-
startIcon={<SettingsOutlined />}
126+
startIcon={<SettingsIcon className="size-icon-sm" />}
127127
to="settings"
128128
>
129129
Settings

site/src/pages/TemplateSettingsPage/Sidebar.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import VariablesIcon from "@mui/icons-material/CodeOutlined";
2-
import GeneralIcon from "@mui/icons-material/SettingsOutlined";
32
import ScheduleIcon from "@mui/icons-material/TimerOutlined";
43
import type { Template } from "api/typesGenerated";
54
import { Avatar } from "components/Avatar/Avatar";
@@ -8,6 +7,7 @@ import {
87
SidebarHeader,
98
SidebarNavItem,
109
} from "components/Sidebar/Sidebar";
10+
import { SettingsIcon } from "lucide-react";
1111
import { LockIcon } from "lucide-react";
1212
import { linkToTemplate, useLinks } from "modules/navigation";
1313
import type { FC } from "react";
@@ -32,7 +32,7 @@ export const Sidebar: FC<SidebarProps> = ({ template }) => {
3232
subtitle={template.name}
3333
/>
3434

35-
<SidebarNavItem href="" icon={GeneralIcon}>
35+
<SidebarNavItem href="" icon={SettingsIcon}>
3636
General
3737
</SidebarNavItem>
3838
<SidebarNavItem href="permissions" icon={LockIcon}>

site/src/pages/WorkspacePage/WorkspaceActions/WorkspaceActions.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import DownloadOutlined from "@mui/icons-material/DownloadOutlined";
22
import DuplicateIcon from "@mui/icons-material/FileCopyOutlined";
33
import HistoryIcon from "@mui/icons-material/HistoryOutlined";
4-
import SettingsIcon from "@mui/icons-material/SettingsOutlined";
54
import type { Workspace, WorkspaceBuildParameter } from "api/typesGenerated";
65
import { Button } from "components/Button/Button";
76
import {
@@ -12,6 +11,7 @@ import {
1211
DropdownMenuTrigger,
1312
} from "components/DropdownMenu/DropdownMenu";
1413
import { useAuthenticated } from "hooks/useAuthenticated";
14+
import { SettingsIcon } from "lucide-react";
1515
import { TrashIcon } from "lucide-react";
1616
import { EllipsisVertical } from "lucide-react";
1717
import {
@@ -196,7 +196,7 @@ export const WorkspaceActions: FC<WorkspaceActionsProps> = ({
196196

197197
<DropdownMenuContent id="workspace-options" align="end">
198198
<DropdownMenuItem onClick={handleSettings}>
199-
<SettingsIcon />
199+
<SettingsIcon className="size-icon-sm" />
200200
Settings
201201
</DropdownMenuItem>
202202

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy