Skip to content

Commit c1f47b5

Browse files
feat: add metric for sync durations
add syncDuration metric add syncDuration metric unit test Signed-off-by: Jack-R-lantern <[email protected]>
1 parent dc1d148 commit c1f47b5

File tree

3 files changed

+115
-0
lines changed

3 files changed

+115
-0
lines changed

controller/appcontroller.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1587,6 +1587,7 @@ func (ctrl *ApplicationController) setOperationState(app *appv1.Application, sta
15871587
destServer = destCluster.Server
15881588
}
15891589
ctrl.metricsServer.IncSync(app, destServer, state)
1590+
ctrl.metricsServer.IncAppSyncDuration(app, destServer, state)
15901591
}
15911592
}
15921593

controller/metrics/metrics.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import (
3333
type MetricsServer struct {
3434
*http.Server
3535
syncCounter *prometheus.CounterVec
36+
syncDuration *prometheus.CounterVec
3637
kubectlExecCounter *prometheus.CounterVec
3738
kubectlExecPendingGauge *prometheus.GaugeVec
3839
orphanedResourcesGauge *prometheus.GaugeVec
@@ -76,6 +77,14 @@ var (
7677
append(descAppDefaultLabels, "dest_server", "phase", "dry_run"),
7778
)
7879

80+
syncDuration = prometheus.NewCounterVec(
81+
prometheus.CounterOpts{
82+
Name: "argocd_app_sync_duration_seconds_total",
83+
Help: "Application sync performance in seconds total.",
84+
},
85+
append(descAppDefaultLabels, "dest_server"),
86+
)
87+
7988
k8sRequestCounter = prometheus.NewCounterVec(
8089
prometheus.CounterOpts{
8190
Name: "argocd_app_k8s_request_total",
@@ -188,6 +197,7 @@ func NewMetricsServer(addr string, appLister applister.ApplicationLister, appFil
188197
healthz.ServeHealthCheck(mux, healthCheck)
189198

190199
registry.MustRegister(syncCounter)
200+
registry.MustRegister(syncDuration)
191201
registry.MustRegister(k8sRequestCounter)
192202
registry.MustRegister(kubectlExecCounter)
193203
registry.MustRegister(kubectlExecPendingGauge)
@@ -209,6 +219,7 @@ func NewMetricsServer(addr string, appLister applister.ApplicationLister, appFil
209219
Handler: mux,
210220
},
211221
syncCounter: syncCounter,
222+
syncDuration: syncDuration,
212223
k8sRequestCounter: k8sRequestCounter,
213224
kubectlExecCounter: kubectlExecCounter,
214225
kubectlExecPendingGauge: kubectlExecPendingGauge,
@@ -243,6 +254,14 @@ func (m *MetricsServer) IncSync(app *argoappv1.Application, destServer string, s
243254
m.syncCounter.WithLabelValues(app.Namespace, app.Name, app.Spec.GetProject(), destServer, string(state.Phase), strconv.FormatBool(isDryRun)).Inc()
244255
}
245256

257+
// IncAppSyncDuration observes app sync duration
258+
func (m *MetricsServer) IncAppSyncDuration(app *argoappv1.Application, destServer string, state *argoappv1.OperationState) {
259+
if state.FinishedAt != nil {
260+
m.syncDuration.WithLabelValues(app.Namespace, app.Name, app.Spec.GetProject(), destServer).
261+
Add(float64(time.Duration(state.FinishedAt.Unix() - state.StartedAt.Unix())))
262+
}
263+
}
264+
246265
func (m *MetricsServer) IncKubectlExec(command string) {
247266
m.kubectlExecCounter.WithLabelValues(m.hostname, command).Inc()
248267
}
@@ -314,6 +333,7 @@ func (m *MetricsServer) SetExpiration(cacheExpiration time.Duration) error {
314333
_, err := m.cron.AddFunc(fmt.Sprintf("@every %s", cacheExpiration), func() {
315334
log.Infof("Reset Prometheus metrics based on existing expiration '%v'", cacheExpiration)
316335
m.syncCounter.Reset()
336+
m.syncDuration.Reset()
317337
m.kubectlExecCounter.Reset()
318338
m.kubectlExecPendingGauge.Reset()
319339
m.orphanedResourcesGauge.Reset()

controller/metrics/metrics_test.go

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,63 @@ status:
178178
status: Healthy
179179
`
180180

181+
const fakeAppOperationRunning = `
182+
apiVersion: argoproj.io/v1alpha1
183+
kind: Application
184+
metadata:
185+
name: my-app
186+
namespace: argocd
187+
labels:
188+
team-name: my-team
189+
team-bu: bu-id
190+
argoproj.io/cluster: test-cluster
191+
spec:
192+
destination:
193+
namespace: dummy-namespace
194+
name: cluster1
195+
project: important-project
196+
source:
197+
path: some/path
198+
repoURL: https://github.com/argoproj/argocd-example-apps.git
199+
status:
200+
sync:
201+
status: OutOfSync
202+
health:
203+
status: Progressing
204+
operationState:
205+
phase: Running
206+
startedAt: "2025-01-29T08:42:34Z"
207+
`
208+
209+
const fakeAppOperationFinished = `
210+
apiVersion: argoproj.io/v1alpha1
211+
kind: Application
212+
metadata:
213+
name: my-app
214+
namespace: argocd
215+
labels:
216+
team-name: my-team
217+
team-bu: bu-id
218+
argoproj.io/cluster: test-cluster
219+
spec:
220+
destination:
221+
namespace: dummy-namespace
222+
name: cluster1
223+
project: important-project
224+
source:
225+
path: some/path
226+
repoURL: https://github.com/argoproj/argocd-example-apps.git
227+
status:
228+
sync:
229+
status: Synced
230+
health:
231+
status: Healthy
232+
operationState:
233+
phase: Succeeded
234+
startedAt: "2025-01-29T08:42:34Z"
235+
finishedAt: "2025-01-29T08:42:35Z"
236+
`
237+
181238
var noOpHealthCheck = func(_ *http.Request) error {
182239
return nil
183240
}
@@ -468,6 +525,43 @@ func assertMetricsNotPrinted(t *testing.T, expectedLines, body string) {
468525
}
469526
}
470527

528+
func TestMetricsSyncDuration(t *testing.T) {
529+
cancel, appLister := newFakeLister()
530+
defer cancel()
531+
mockDB := mocks.NewArgoDB(t)
532+
metricsServ, err := NewMetricsServer("localhost:8082", appLister, appFilter, noOpHealthCheck, []string{}, []string{}, mockDB)
533+
require.NoError(t, err)
534+
535+
fakeAppOperationRunning := newFakeApp(fakeAppOperationRunning)
536+
metricsServ.IncAppSyncDuration(fakeAppOperationRunning, "https://localhost:6443", fakeAppOperationRunning.Status.OperationState)
537+
538+
req, err := http.NewRequest(http.MethodGet, "/metrics", nil)
539+
require.NoError(t, err)
540+
rr := httptest.NewRecorder()
541+
metricsServ.Handler.ServeHTTP(rr, req)
542+
assert.Equal(t, http.StatusOK, rr.Code)
543+
body := rr.Body.String()
544+
log.Println(body)
545+
assert.NotContains(t, body, "argocd_app_sync_duration_seconds_total")
546+
547+
fakeAppOperationFinished := newFakeApp(fakeAppOperationFinished)
548+
metricsServ.IncAppSyncDuration(fakeAppOperationFinished, "https://localhost:6443", fakeAppOperationFinished.Status.OperationState)
549+
550+
req, err = http.NewRequest(http.MethodGet, "/metrics", nil)
551+
require.NoError(t, err)
552+
rr = httptest.NewRecorder()
553+
metricsServ.Handler.ServeHTTP(rr, req)
554+
assert.Equal(t, http.StatusOK, rr.Code)
555+
body = rr.Body.String()
556+
appSyncDurationTotal := `
557+
# HELP argocd_app_sync_duration_seconds_total Application sync performance in seconds total.
558+
# TYPE argocd_app_sync_duration_seconds_total counter
559+
argocd_app_sync_duration_seconds_total{dest_server="https://localhost:6443",name="my-app",namespace="argocd",project="important-project"} 1
560+
`
561+
log.Println(body)
562+
assertMetricsPrinted(t, appSyncDurationTotal, body)
563+
}
564+
471565
func TestReconcileMetrics(t *testing.T) {
472566
cancel, appLister := newFakeLister()
473567
defer cancel()

0 commit comments

Comments
 (0)