Skip to content

Commit 40df8fb

Browse files
authored
Add up on the postgresql.database metrics (#171)
1 parent 73e8877 commit 40df8fb

File tree

4 files changed

+116
-39
lines changed

4 files changed

+116
-39
lines changed

app/services/metrics/metrics.go

Lines changed: 23 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import (
66
"os"
77
"time"
88

9+
"github.com/labstack/gommon/log"
10+
911
"hostlink/app/services/agentstate"
1012
"hostlink/config/appconf"
1113
"hostlink/domain/credential"
@@ -27,14 +29,14 @@ type Pusher interface {
2729
}
2830

2931
type metricspusher struct {
30-
apiserver apiserver.MetricsOperations
31-
agentstate agentstate.Operations
32-
metricscollector pgmetrics.Collector
33-
syscollector sysmetrics.Collector
34-
netcollector networkmetrics.Collector
35-
storagecollector storagemetrics.Collector
36-
crypto crypto.Service
37-
privateKeyPath string
32+
apiserver apiserver.MetricsOperations
33+
agentstate agentstate.Operations
34+
metricscollector pgmetrics.Collector
35+
syscollector sysmetrics.Collector
36+
netcollector networkmetrics.Collector
37+
storagecollector storagemetrics.Collector
38+
crypto crypto.Service
39+
privateKeyPath string
3840
}
3941

4042
func NewWithConf() (*metricspusher, error) {
@@ -130,11 +132,10 @@ func (mp *metricspusher) Push(cred credential.Credential) error {
130132

131133
ctx := context.Background()
132134
var metricSets []domainmetrics.MetricSet
133-
var collectionErrors []error
134135

135136
sysMetrics, err := mp.syscollector.Collect(ctx)
136137
if err != nil {
137-
collectionErrors = append(collectionErrors, fmt.Errorf("system metrics: %w", err))
138+
log.Warnf("system metrics collection failed: %v", err)
138139
} else {
139140
metricSets = append(metricSets, domainmetrics.MetricSet{
140141
Type: domainmetrics.MetricTypeSystem,
@@ -144,7 +145,7 @@ func (mp *metricspusher) Push(cred credential.Credential) error {
144145

145146
netMetrics, err := mp.netcollector.Collect(ctx)
146147
if err != nil {
147-
collectionErrors = append(collectionErrors, fmt.Errorf("network metrics: %w", err))
148+
log.Warnf("network metrics collection failed: %v", err)
148149
} else {
149150
metricSets = append(metricSets, domainmetrics.MetricSet{
150151
Type: domainmetrics.MetricTypeNetwork,
@@ -154,17 +155,19 @@ func (mp *metricspusher) Push(cred credential.Credential) error {
154155

155156
dbMetrics, err := mp.metricscollector.Collect(cred)
156157
if err != nil {
157-
collectionErrors = append(collectionErrors, fmt.Errorf("database metrics: %w", err))
158+
log.Warnf("database metrics collection failed: %v", err)
159+
dbMetrics = domainmetrics.PostgreSQLDatabaseMetrics{Up: false}
158160
} else {
159-
metricSets = append(metricSets, domainmetrics.MetricSet{
160-
Type: domainmetrics.MetricTypePostgreSQLDatabase,
161-
Metrics: dbMetrics,
162-
})
161+
dbMetrics.Up = true
163162
}
163+
metricSets = append(metricSets, domainmetrics.MetricSet{
164+
Type: domainmetrics.MetricTypePostgreSQLDatabase,
165+
Metrics: dbMetrics,
166+
})
164167

165168
storageMetrics, err := mp.storagecollector.Collect(ctx)
166169
if err != nil {
167-
collectionErrors = append(collectionErrors, fmt.Errorf("storage metrics: %w", err))
170+
log.Warnf("storage metrics collection failed: %v", err)
168171
} else {
169172
for _, sm := range storageMetrics {
170173
metricSets = append(metricSets, domainmetrics.MetricSet{
@@ -180,9 +183,9 @@ func (mp *metricspusher) Push(cred credential.Credential) error {
180183
}
181184
}
182185

183-
if len(metricSets) == 0 {
184-
return fmt.Errorf("all metrics collection failed: %v", collectionErrors)
185-
}
186+
// If only the postgresql.database metric set exists (with up=false) and
187+
// all other collectors failed, we still push so the server knows the agent
188+
// is alive and PostgreSQL status is reported.
186189

187190
hostname, _ := os.Hostname()
188191

app/services/metrics/metrics_test.go

Lines changed: 89 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -459,7 +459,7 @@ func TestPush_SystemMetricsFailure_StillPushesDbMetrics(t *testing.T) {
459459
setupNetCollectorMocks(mocks.netcollector)
460460
setupStorageCollectorMocks(mocks.storagecollector)
461461
mocks.collector.On("Collect", testCred).
462-
Return(domainmetrics.PostgreSQLDatabaseMetrics{ConnectionsTotal: 5}, nil)
462+
Return(domainmetrics.PostgreSQLDatabaseMetrics{Up: true, ConnectionsTotal: 5}, nil)
463463
mocks.apiserver.On("PushMetrics", mock.Anything, mock.MatchedBy(func(p domainmetrics.MetricPayload) bool {
464464
hasNetwork := false
465465
hasDb := false
@@ -469,7 +469,8 @@ func TestPush_SystemMetricsFailure_StillPushesDbMetrics(t *testing.T) {
469469
hasNetwork = true
470470
}
471471
if ms.Type == domainmetrics.MetricTypePostgreSQLDatabase {
472-
hasDb = true
472+
dbMetrics := ms.Metrics.(domainmetrics.PostgreSQLDatabaseMetrics)
473+
hasDb = dbMetrics.Up
473474
}
474475
if ms.Type == domainmetrics.MetricTypeStorage {
475476
hasStorage = true
@@ -486,7 +487,7 @@ func TestPush_SystemMetricsFailure_StillPushesDbMetrics(t *testing.T) {
486487
}
487488

488489
// Push Tests - Partial Collection (system succeeds, db fails)
489-
func TestPush_DatabaseMetricsFailure_StillPushesSystemMetrics(t *testing.T) {
490+
func TestPush_DatabaseMetricsFailure_StillPushesSystemMetricsAndDbWithUpFalse(t *testing.T) {
490491
mp, mocks := setupTestMetricsPusher()
491492
testCred := credential.Credential{Host: "localhost", DataDirectory: "/var/lib/postgresql"}
492493
collectErr := errors.New("connection refused")
@@ -501,6 +502,7 @@ func TestPush_DatabaseMetricsFailure_StillPushesSystemMetrics(t *testing.T) {
501502
hasSys := false
502503
hasNetwork := false
503504
hasStorage := false
505+
hasDbWithUpFalse := false
504506
for _, ms := range p.MetricSets {
505507
if ms.Type == domainmetrics.MetricTypeSystem {
506508
hasSys = true
@@ -511,8 +513,12 @@ func TestPush_DatabaseMetricsFailure_StillPushesSystemMetrics(t *testing.T) {
511513
if ms.Type == domainmetrics.MetricTypeStorage {
512514
hasStorage = true
513515
}
516+
if ms.Type == domainmetrics.MetricTypePostgreSQLDatabase {
517+
dbMetrics := ms.Metrics.(domainmetrics.PostgreSQLDatabaseMetrics)
518+
hasDbWithUpFalse = !dbMetrics.Up
519+
}
514520
}
515-
return hasSys && hasNetwork && hasStorage
521+
return hasSys && hasNetwork && hasStorage && hasDbWithUpFalse
516522
})).Return(nil)
517523

518524
err := mp.Push(testCred)
@@ -522,8 +528,8 @@ func TestPush_DatabaseMetricsFailure_StillPushesSystemMetrics(t *testing.T) {
522528
mocks.apiserver.AssertExpectations(t)
523529
}
524530

525-
// Push Tests - All Collections Fail
526-
func TestPush_AllCollectionsFail(t *testing.T) {
531+
// Push Tests - All Collections Fail — still pushes postgresql.database with up=false
532+
func TestPush_AllCollectionsFail_StillPushesDbWithUpFalse(t *testing.T) {
527533
mp, mocks := setupTestMetricsPusher()
528534
testCred := credential.Credential{Host: "localhost", DataDirectory: "/var/lib/postgresql"}
529535

@@ -536,12 +542,22 @@ func TestPush_AllCollectionsFail(t *testing.T) {
536542
Return(nil, errors.New("storage failed"))
537543
mocks.collector.On("Collect", testCred).
538544
Return(domainmetrics.PostgreSQLDatabaseMetrics{}, errors.New("connection refused"))
545+
mocks.apiserver.On("PushMetrics", mock.Anything, mock.MatchedBy(func(p domainmetrics.MetricPayload) bool {
546+
if len(p.MetricSets) != 1 {
547+
return false
548+
}
549+
ms := p.MetricSets[0]
550+
if ms.Type != domainmetrics.MetricTypePostgreSQLDatabase {
551+
return false
552+
}
553+
dbMetrics := ms.Metrics.(domainmetrics.PostgreSQLDatabaseMetrics)
554+
return !dbMetrics.Up
555+
})).Return(nil)
539556

540557
err := mp.Push(testCred)
541558

542-
assert.Error(t, err)
543-
assert.Contains(t, err.Error(), "all metrics collection failed")
544-
mocks.apiserver.AssertNotCalled(t, "PushMetrics")
559+
assert.NoError(t, err)
560+
mocks.apiserver.AssertExpectations(t)
545561
}
546562

547563
func TestPush_APIServerPushFailure(t *testing.T) {
@@ -554,7 +570,7 @@ func TestPush_APIServerPushFailure(t *testing.T) {
554570
setupNetCollectorMocks(mocks.netcollector)
555571
setupStorageCollectorMocks(mocks.storagecollector)
556572
mocks.collector.On("Collect", testCred).
557-
Return(domainmetrics.PostgreSQLDatabaseMetrics{ConnectionsTotal: 5}, nil)
573+
Return(domainmetrics.PostgreSQLDatabaseMetrics{Up: true, ConnectionsTotal: 5}, nil)
558574
mocks.apiserver.On("PushMetrics", mock.Anything, mock.Anything).
559575
Return(pushErr)
560576

@@ -579,6 +595,7 @@ func TestPush_Success_ValidatesPayloadSchema(t *testing.T) {
579595
setupStorageCollectorMocks(mocks.storagecollector)
580596
mocks.collector.On("Collect", testCred).
581597
Return(domainmetrics.PostgreSQLDatabaseMetrics{
598+
Up: true,
582599
ConnectionsTotal: 10,
583600
MaxConnections: 100,
584601
CacheHitRatio: 95.5,
@@ -653,6 +670,9 @@ func TestPush_Success_ValidatesPayloadSchema(t *testing.T) {
653670
}
654671

655672
// Check database metrics values
673+
if !dbMetrics.Up {
674+
return false
675+
}
656676
if dbMetrics.ConnectionsTotal != 10 {
657677
return false
658678
}
@@ -695,7 +715,7 @@ func TestPush_ContextPropagation(t *testing.T) {
695715
setupNetCollectorMocks(mocks.netcollector)
696716
setupStorageCollectorMocks(mocks.storagecollector)
697717
mocks.collector.On("Collect", testCred).
698-
Return(domainmetrics.PostgreSQLDatabaseMetrics{}, nil)
718+
Return(domainmetrics.PostgreSQLDatabaseMetrics{Up: true}, nil)
699719
mocks.apiserver.On("PushMetrics", mock.MatchedBy(func(ctx context.Context) bool {
700720
return ctx != nil
701721
}), mock.Anything).
@@ -742,7 +762,7 @@ func TestPush_CredentialPassedCorrectly(t *testing.T) {
742762
c.Port == testCred.Port &&
743763
c.Username == testCred.Username &&
744764
c.Dialect == testCred.Dialect
745-
})).Return(domainmetrics.PostgreSQLDatabaseMetrics{}, nil)
765+
})).Return(domainmetrics.PostgreSQLDatabaseMetrics{Up: true}, nil)
746766
mocks.apiserver.On("PushMetrics", mock.Anything, mock.Anything).
747767
Return(nil)
748768

@@ -793,6 +813,59 @@ func setupStorageCollectorMocks(collector *MockStorageCollector) {
793813
}, nil)
794814
}
795815

816+
// Verifies database down sends up=false with zero metrics
817+
func TestPush_DatabaseDown_SendsUpFalseWithZeroMetrics(t *testing.T) {
818+
mp, mocks := setupTestMetricsPusher()
819+
testCred := credential.Credential{Host: "localhost", DataDirectory: "/var/lib/postgresql"}
820+
821+
mocks.agentstate.On("GetAgentID").Return("agent-123")
822+
setupSysCollectorMocks(mocks.syscollector)
823+
setupNetCollectorMocks(mocks.netcollector)
824+
setupStorageCollectorMocks(mocks.storagecollector)
825+
mocks.collector.On("Collect", testCred).
826+
Return(domainmetrics.PostgreSQLDatabaseMetrics{}, errors.New("connection refused"))
827+
mocks.apiserver.On("PushMetrics", mock.Anything, mock.MatchedBy(func(p domainmetrics.MetricPayload) bool {
828+
for _, ms := range p.MetricSets {
829+
if ms.Type == domainmetrics.MetricTypePostgreSQLDatabase {
830+
dbMetrics := ms.Metrics.(domainmetrics.PostgreSQLDatabaseMetrics)
831+
// up must be false
832+
if dbMetrics.Up {
833+
return false
834+
}
835+
// all other fields must be zero
836+
if dbMetrics.ConnectionsTotal != 0 {
837+
return false
838+
}
839+
if dbMetrics.MaxConnections != 0 {
840+
return false
841+
}
842+
if dbMetrics.CacheHitRatio != 0 {
843+
return false
844+
}
845+
if dbMetrics.TransactionsPerSecond != 0 {
846+
return false
847+
}
848+
if dbMetrics.CommittedTxPerSecond != 0 {
849+
return false
850+
}
851+
if dbMetrics.BlocksReadPerSecond != 0 {
852+
return false
853+
}
854+
if dbMetrics.ReplicationLagSeconds != 0 {
855+
return false
856+
}
857+
return true
858+
}
859+
}
860+
return false // postgresql.database metric set must be present
861+
})).Return(nil)
862+
863+
err := mp.Push(testCred)
864+
865+
assert.NoError(t, err)
866+
mocks.apiserver.AssertExpectations(t)
867+
}
868+
796869
// Verifies storage metrics are included in the payload
797870
func TestPush_IncludesStorageMetrics(t *testing.T) {
798871
mp, mocks := setupTestMetricsPusher()
@@ -803,7 +876,7 @@ func TestPush_IncludesStorageMetrics(t *testing.T) {
803876
setupNetCollectorMocks(mocks.netcollector)
804877
setupStorageCollectorMocks(mocks.storagecollector)
805878
mocks.collector.On("Collect", testCred).
806-
Return(domainmetrics.PostgreSQLDatabaseMetrics{}, nil)
879+
Return(domainmetrics.PostgreSQLDatabaseMetrics{Up: true}, nil)
807880
mocks.apiserver.On("PushMetrics", mock.Anything, mock.MatchedBy(func(p domainmetrics.MetricPayload) bool {
808881
hasStorage := false
809882
for _, ms := range p.MetricSets {
@@ -832,7 +905,7 @@ func TestPush_StorageMetricsFailure_StillPushesOtherMetrics(t *testing.T) {
832905
mocks.storagecollector.On("Collect", mock.Anything).
833906
Return(nil, errors.New("storage collection failed"))
834907
mocks.collector.On("Collect", testCred).
835-
Return(domainmetrics.PostgreSQLDatabaseMetrics{}, nil)
908+
Return(domainmetrics.PostgreSQLDatabaseMetrics{Up: true}, nil)
836909
mocks.apiserver.On("PushMetrics", mock.Anything, mock.MatchedBy(func(p domainmetrics.MetricPayload) bool {
837910
hasSys := false
838911
hasNet := false
@@ -876,7 +949,7 @@ func TestPush_StorageMetricsMultipleMounts(t *testing.T) {
876949
},
877950
}, nil)
878951
mocks.collector.On("Collect", testCred).
879-
Return(domainmetrics.PostgreSQLDatabaseMetrics{}, nil)
952+
Return(domainmetrics.PostgreSQLDatabaseMetrics{Up: true}, nil)
880953
mocks.apiserver.On("PushMetrics", mock.Anything, mock.MatchedBy(func(p domainmetrics.MetricPayload) bool {
881954
storageCount := 0
882955
for _, ms := range p.MetricSets {
@@ -913,7 +986,7 @@ func TestPush_StorageMetricsWithAttributes(t *testing.T) {
913986
},
914987
}, nil)
915988
mocks.collector.On("Collect", testCred).
916-
Return(domainmetrics.PostgreSQLDatabaseMetrics{}, nil)
989+
Return(domainmetrics.PostgreSQLDatabaseMetrics{Up: true}, nil)
917990
mocks.apiserver.On("PushMetrics", mock.Anything, mock.MatchedBy(func(p domainmetrics.MetricPayload) bool {
918991
for _, ms := range p.MetricSets {
919992
if ms.Type == domainmetrics.MetricTypeStorage {

domain/metrics/metrics.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ type NetworkMetrics struct {
4343
}
4444

4545
type PostgreSQLDatabaseMetrics struct {
46+
Up bool `json:"up"`
4647
ConnectionsTotal int `json:"connections_total"`
4748
MaxConnections int `json:"max_connections"`
4849
CacheHitRatio float64 `json:"cache_hit_ratio"`

go.mod

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
module hostlink
22

3-
go 1.24.4
3+
go 1.26.0
44

55
require (
66
github.com/glebarez/sqlite v1.11.0
@@ -14,10 +14,12 @@ require (
1414
github.com/mattn/go-shellwords v1.0.12
1515
github.com/oklog/ulid/v2 v2.1.1
1616
github.com/shirou/gopsutil/v4 v4.25.11
17+
github.com/sirupsen/logrus v1.9.3
1718
github.com/stretchr/testify v1.11.1
1819
github.com/testcontainers/testcontainers-go v0.39.0
1920
github.com/testcontainers/testcontainers-go/modules/postgres v0.39.0
2021
github.com/urfave/cli/v3 v3.4.1
22+
golang.org/x/sys v0.39.0
2123
gorm.io/gorm v1.31.0
2224
)
2325

@@ -71,7 +73,6 @@ require (
7173
github.com/pmezard/go-difflib v1.0.0 // indirect
7274
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
7375
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
74-
github.com/sirupsen/logrus v1.9.3 // indirect
7576
github.com/stretchr/objx v0.5.2 // indirect
7677
github.com/tklauser/go-sysconf v0.3.16 // indirect
7778
github.com/tklauser/numcpus v0.11.0 // indirect
@@ -87,7 +88,6 @@ require (
8788
golang.org/x/crypto v0.46.0 // indirect
8889
golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b // indirect
8990
golang.org/x/net v0.47.0 // indirect
90-
golang.org/x/sys v0.39.0 // indirect
9191
golang.org/x/text v0.32.0 // indirect
9292
golang.org/x/time v0.11.0 // indirect
9393
google.golang.org/grpc v1.75.1 // indirect

0 commit comments

Comments
 (0)