Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 74a7616

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

File tree

10 files changed

+123
-39
lines changed

10 files changed

+123
-39
lines changed

docs/admin/integrations/data-cloud.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ Visit the [users list](../../reference/cli/users_list.md) documentation for more
3636

3737
### API
3838

39-
Use [get users](../../reference/api/users#get-users):
39+
Use [get users](../../reference/api/users.md#get-users):
4040

4141
```bash
4242
curl -X GET http://coder-server:8080/api/v2/users \

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)