From: Petri Ovaska Date: Thu, 25 Nov 2021 13:07:17 +0000 (+0200) Subject: New 'statistics' SDL CLI command X-Git-Tag: v0.9.0^0 X-Git-Url: https://gerrit.o-ran-sc.org/r/gitweb?a=commitdiff_plain;ds=inline;h=74b5e054dc40ed47cbbbb28f1fc7c80b50efcae6;p=ric-plt%2Fsdlgo.git New 'statistics' SDL CLI command Display SDL resource usage statistics. Usage: sdlcli statistics [flags] Update also the release-notes. Version: 0.9.0 Issue-Id: RIC-113 Change-Id: I653d5325a8ff4a989f3cb7d9ab35c0fe9987392e Signed-off-by: Petri Ovaska --- diff --git a/docs/release-notes.rst b/docs/release-notes.rst index 319c497..28c15cc 100644 --- a/docs/release-notes.rst +++ b/docs/release-notes.rst @@ -29,6 +29,9 @@ This document provides the release notes of the sdlgo. Version history --------------- +[0.9.0] - 2021-12-16 + +* SDL CLI tool [0.8.0] - 2021-09-22 diff --git a/internal/cli/cli_private_fn_test.go b/internal/cli/cli_private_fn_test.go index d37cb9e..2a20a51 100644 --- a/internal/cli/cli_private_fn_test.go +++ b/internal/cli/cli_private_fn_test.go @@ -30,4 +30,5 @@ var ( NewSetCmdForTest = newSetCmd NewRemoveCmdForTest = newRemoveCmd NewNamespacesCmdForTest = newNamespacesCmd + NewStatisticsCmd = newStatisticsCmd ) diff --git a/internal/cli/statistics.go b/internal/cli/statistics.go new file mode 100644 index 0000000..05608e2 --- /dev/null +++ b/internal/cli/statistics.go @@ -0,0 +1,164 @@ +/* + Copyright (c) 2021 AT&T Intellectual Property. + Copyright (c) 2018-2021 Nokia. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +/* + * This source code is part of the near-RT RIC (RAN Intelligent Controller) + * platform project (RICP). + */ + +package cli + +import ( + "fmt" + "gerrit.o-ran-sc.org/r/ric-plt/sdlgo/internal/sdlgoredis" + "github.com/spf13/cobra" + "os" + "reflect" + "text/tabwriter" +) + +func init() { + rootCmd.AddCommand(newStatisticsCmd(newDatabase)) +} + +var ( + statsLong = `Display SDL resource usage statistics` + + statsExample = ` # Show statistics per node + sdlcli statistics` +) + +func newStatisticsCmd(dbCreateCb DbCreateCb) *cobra.Command { + return &cobra.Command{ + Use: "statistics", + Short: "Display statistics.", + Long: statsLong, + Example: statsExample, + Args: cobra.ExactArgs(0), + RunE: func(cmd *cobra.Command, args []string) error { + statistics, err := runStats(dbCreateCb) + if err != nil { + fmt.Fprintf(os.Stderr, "%s", buf.String()) + return err + } + printStatistics(cmd, statistics) + return nil + }, + } +} + +func runStats(dbCreateCb DbCreateCb) ([]*sdlgoredis.DbStatistics, error) { + var statistics []*sdlgoredis.DbStatistics + for _, dbInst := range dbCreateCb().Instances { + dbStatistics, err := dbInst.Statistics() + if err != nil { + return nil, err + } + statistics = append(statistics, dbStatistics) + } + return statistics, nil +} + +func writeClientsInfo(w *tabwriter.Writer, clients sdlgoredis.ClientsInfoFields) { + fmt.Fprintf(w, "\t\t\tConnectedClients:%d\n", clients.ConnectedClients) + fmt.Fprintf(w, "\t\t\tClientRecentMaxInputBuffer:%d\n", clients.ClientRecentMaxInputBuffer) + fmt.Fprintf(w, "\t\t\tClientRecentMaxOutputBuffer:%d\n", clients.ClientRecentMaxOutputBuffer) +} + +func writeMemoryInfo(w *tabwriter.Writer, memory sdlgoredis.MeroryInfoFields) { + fmt.Fprintf(w, "\t\t\tUsedMemory:%d\n", memory.UsedMemory) + fmt.Fprintf(w, "\t\t\tUsedMemoryHuman:%s\n", memory.UsedMemoryHuman) + fmt.Fprintf(w, "\t\t\tUsedMemoryRss:%d\n", memory.UsedMemoryRss) + fmt.Fprintf(w, "\t\t\tUsedMemoryRssHuman:%s\n", memory.UsedMemoryRssHuman) + fmt.Fprintf(w, "\t\t\tUsedMemoryPeak:%d\n", memory.UsedMemoryPeak) + fmt.Fprintf(w, "\t\t\tUsedMemoryPeakHuman:%s\n", memory.UsedMemoryPeakHuman) + fmt.Fprintf(w, "\t\t\tUsedMemoryPeakPerc:%s\n", memory.UsedMemoryPeakPerc) + fmt.Fprintf(w, "\t\t\tMemFragmentationRatio:%.2f\n", memory.MemFragmentationRatio) + fmt.Fprintf(w, "\t\t\tMemFragmentationBytes:%d\n", memory.MemFragmentationBytes) +} + +func writeStatsInfo(w *tabwriter.Writer, stats sdlgoredis.StatsInfoFields) { + fmt.Fprintf(w, "\t\t\tTotalConnectionsReceived:%d\n", stats.TotalConnectionsReceived) + fmt.Fprintf(w, "\t\t\tTotalCommandsProcessed:%d\n", stats.TotalCommandsProcessed) + fmt.Fprintf(w, "\t\t\tSyncFull:%d\n", stats.SyncFull) + fmt.Fprintf(w, "\t\t\tSyncPartialOk:%d\n", stats.SyncPartialOk) + fmt.Fprintf(w, "\t\t\tSyncPartialErr:%d\n", stats.SyncPartialErr) + fmt.Fprintf(w, "\t\t\tPubsubChannels:%d\n", stats.PubsubChannels) +} + +func writeCpuInfo(w *tabwriter.Writer, cpu sdlgoredis.CpuInfoFields) { + fmt.Fprintf(w, "\t\t\tUsedCpuSys:%v\n", cpu.UsedCpuSys) + fmt.Fprintf(w, "\t\t\tUsedCpuUser:%v\n", cpu.UsedCpuUser) +} + +func fillCommandstatsInfo(w *tabwriter.Writer, i interface{}, cmdstat string) { + stype := reflect.ValueOf(i).Elem() + callsField := stype.FieldByName("Calls").Interface() + usecField := stype.FieldByName("Usec").Interface() + usecPerCallField := stype.FieldByName("UsecPerCall").Interface() + + if callsField.(uint32) > 0 { + fmt.Fprintf(w, "\t\t\t%s:calls=%d usec:%d usecPerCall:%.2f\n", + cmdstat, callsField, usecField, usecPerCallField) + } +} + +func writeCommandstatsInfo(w *tabwriter.Writer, commandstats sdlgoredis.CommandstatsInfoFields) { + fillCommandstatsInfo(w, &commandstats.CmdstatReplconf, "CmdstatReplconf") + fillCommandstatsInfo(w, &commandstats.CmdstatKeys, "CmdstatKeys") + fillCommandstatsInfo(w, &commandstats.CmdstatRole, "CmdstatRole") + fillCommandstatsInfo(w, &commandstats.CmdstatPsync, "CmdstatPsync") + fillCommandstatsInfo(w, &commandstats.CmdstatMset, "CmdstatMset") + fillCommandstatsInfo(w, &commandstats.CmdstatPublish, "CmdstatPublish") + fillCommandstatsInfo(w, &commandstats.CmdstatInfo, "CmdstatInfo") + fillCommandstatsInfo(w, &commandstats.CmdstatPing, "CmdstatPing") + fillCommandstatsInfo(w, &commandstats.CmdstatClient, "CmdstatClient") + fillCommandstatsInfo(w, &commandstats.CmdstatCommand, "CmdstatCommand") + fillCommandstatsInfo(w, &commandstats.CmdstatSubscribe, "CmdstatSubscribe") + fillCommandstatsInfo(w, &commandstats.CmdstatMonitor, "CmdstatMonitor") + fillCommandstatsInfo(w, &commandstats.CmdstatConfig, "CmdstatConfig") + fillCommandstatsInfo(w, &commandstats.CmdstatSlaveof, "CmdstatSlaveof") +} + +func writeKeyspaceInfo(w *tabwriter.Writer, keyspace sdlgoredis.KeyspaceInfoFields) { + fmt.Fprintf(w, "\t\t\tDb0:keys=%d\n", keyspace.Db.Keys) +} + +func printStatistics(cmd *cobra.Command, statistics []*sdlgoredis.DbStatistics) { + w := tabwriter.NewWriter(cmd.OutOrStdout(), 6, 4, 2, ' ', 0) + fmt.Fprintln(w, "CLUSTER\tROLE\tADDRESS\tSTATISTICS") + + for i, s := range statistics { + for _, stats := range s.Stats { + fmt.Fprintf(w, "%d", i) + if stats.Info.Fields.PrimaryRole { + fmt.Fprintf(w, "\tPrimary") + } else { + fmt.Fprintf(w, "\tReplica") + } + fmt.Fprintf(w, "\t%s:%s", stats.IPAddr, stats.Port) + fmt.Fprintf(w, "\tUptimeInDays:%d\n", stats.Info.Fields.Server.UptimeInDays) + writeClientsInfo(w, stats.Info.Fields.Clients) + writeMemoryInfo(w, stats.Info.Fields.Memory) + writeStatsInfo(w, stats.Info.Fields.Stats) + writeCpuInfo(w, stats.Info.Fields.Cpu) + writeCommandstatsInfo(w, stats.Info.Fields.Commandstats) + writeKeyspaceInfo(w, stats.Info.Fields.Keyspace) + } + } + w.Flush() +} diff --git a/internal/cli/statistics_test.go b/internal/cli/statistics_test.go new file mode 100644 index 0000000..7ce7736 --- /dev/null +++ b/internal/cli/statistics_test.go @@ -0,0 +1,448 @@ +/* + Copyright (c) 2021 AT&T Intellectual Property. + Copyright (c) 2018-2021 Nokia. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +/* + * This source code is part of the near-RT RIC (RAN Intelligent Controller) + * platform project (RICP). + */ + +package cli_test + +import ( + "bytes" + "errors" + "gerrit.o-ran-sc.org/r/ric-plt/sdlgo/internal/cli" + "gerrit.o-ran-sc.org/r/ric-plt/sdlgo/internal/mocks" + "gerrit.o-ran-sc.org/r/ric-plt/sdlgo/internal/sdlgoredis" + "github.com/stretchr/testify/assert" + "testing" +) + +var columnNamesOut = `CLUSTER ROLE ADDRESS STATISTICS +` +var primaryOut = `0 Primary 192.168.10.1:6379 UptimeInDays:1 + ConnectedClients:1 + ClientRecentMaxInputBuffer:2 + ClientRecentMaxOutputBuffer:0 + UsedMemory:2073528 + UsedMemoryHuman:1.98M + UsedMemoryRss:13721600 + UsedMemoryRssHuman:13.09M + UsedMemoryPeak:6706008 + UsedMemoryPeakHuman:6.40M + UsedMemoryPeakPerc:30.92% + MemFragmentationRatio:6.66 + MemFragmentationBytes:11379232 + TotalConnectionsReceived:566 + TotalCommandsProcessed:3635063 + SyncFull:2 + SyncPartialOk:21 + SyncPartialErr:0 + PubsubChannels:1 + UsedCpuSys:1457.472805 + UsedCpuUser:861.060018 + CmdstatReplconf:calls=1090183 usec:2790911 usecPerCall:2.56 + CmdstatKeys:calls=20 usec:233 usecPerCall:11.65 + CmdstatRole:calls=2 usec:84 usecPerCall:42.00 + CmdstatPsync:calls=23 usec:3582 usecPerCall:155.74 + CmdstatMset:calls=4 usec:30 usecPerCall:7.50 + CmdstatPublish:calls=804867 usec:12965499 usecPerCall:16.11 + CmdstatInfo:calls=164255 usec:57935065 usecPerCall:352.71 + CmdstatPing:calls=1582493 usec:3466844 usecPerCall:2.19 + CmdstatClient:calls=6 usec:11 usecPerCall:2.17 + CmdstatCommand:calls=482 usec:1462633 usecPerCall:3034.51 + CmdstatSubscribe:calls=3 usec:11 usecPerCall:3.67 + CmdstatMonitor:calls=3 usec:6 usecPerCall:2.00 + Db0:keys=123 +` +var replicaOut = `0 Replica 192.168.10.2:6379 UptimeInDays:2 + ConnectedClients:8 + ClientRecentMaxInputBuffer:3 + ClientRecentMaxOutputBuffer:1 + UsedMemory:2135768 + UsedMemoryHuman:2.04M + UsedMemoryRss:13721720 + UsedMemoryRssHuman:12.48M + UsedMemoryPeak:6706009 + UsedMemoryPeakHuman:6.41M + UsedMemoryPeakPerc:30.93% + MemFragmentationRatio:6.67 + MemFragmentationBytes:11379233 + TotalConnectionsReceived:567 + TotalCommandsProcessed:3635064 + SyncFull:0 + SyncPartialOk:0 + SyncPartialErr:0 + PubsubChannels:1 + UsedCpuSys:1457.472806 + UsedCpuUser:861.060019 + CmdstatRole:calls=1 usec:3 usecPerCall:32.00 + CmdstatMset:calls=3 usec:14 usecPerCall:7.51 + CmdstatPublish:calls=804868 usec:12965498 usecPerCall:16.15 + CmdstatInfo:calls=164256 usec:57935066 usecPerCall:352.72 + CmdstatPing:calls=1582494 usec:3466845 usecPerCall:2.20 + CmdstatCommand:calls=483 usec:1462634 usecPerCall:3034.52 + CmdstatSubscribe:calls=3 usec:12 usecPerCall:3.68 + CmdstatMonitor:calls=6 usec:11 usecPerCall:2.11 + CmdstatConfig:calls=2 usec:1462 usecPerCall:146.74 + CmdstatSlaveof:calls=1 usec:412 usecPerCall:412.00 + Db0:keys=123 +` + +var statsMock *statisticsMock + +type statisticsMock struct { + dbIface *mocks.MockDB + dbErr error + dbStatistics sdlgoredis.DbStatistics +} + +func newStatisticsDatabaseMock() *cli.Database { + db := &cli.Database{} + statsMock.dbIface = new(mocks.MockDB) + statsMock.dbIface.On("Statistics").Return(&statsMock.dbStatistics, statsMock.dbErr) + db.Instances = append(db.Instances, statsMock.dbIface) + return db +} + +func setupStatisticsMockDb() { + if statsMock == nil { + statsMock = new(statisticsMock) + } + statsMock.dbStatistics.Stats = []*sdlgoredis.DbStatisticsInfo{} +} + +func setupStatisticsTestcase(tb testing.TB) func(tb testing.TB) { + setupStatisticsMockDb() + + return func(tb testing.TB) { + statsMock.dbStatistics.Stats = nil + } +} + +func setupStatisticsMockDbWithPrimaryInfo() { + statsMock.dbStatistics.Stats = append(statsMock.dbStatistics.Stats, + &sdlgoredis.DbStatisticsInfo{ + IPAddr: "192.168.10.1", + Port: "6379", + Info: &sdlgoredis.DbInfo{ + Fields: sdlgoredis.DbInfoFields{ + PrimaryRole: true, + Server: sdlgoredis.ServerInfoFields{ + UptimeInDays: 1, + }, + Clients: sdlgoredis.ClientsInfoFields{ + ConnectedClients: 1, + ClientRecentMaxInputBuffer: 2, + ClientRecentMaxOutputBuffer: 0, + }, + Memory: sdlgoredis.MeroryInfoFields{ + UsedMemory: 2073528, + UsedMemoryHuman: "1.98M", + UsedMemoryRss: 13721600, + UsedMemoryRssHuman: "13.09M", + UsedMemoryPeak: 6706008, + UsedMemoryPeakHuman: "6.40M", + UsedMemoryPeakPerc: "30.92%", + MemFragmentationRatio: 6.66, + MemFragmentationBytes: 11379232, + }, + Stats: sdlgoredis.StatsInfoFields{ + TotalConnectionsReceived: 566, + TotalCommandsProcessed: 3635063, + SyncFull: 2, + SyncPartialOk: 21, + SyncPartialErr: 0, + PubsubChannels: 1, + }, + Cpu: sdlgoredis.CpuInfoFields{ + UsedCpuSys: 1457.472805, + UsedCpuUser: 861.060018, + }, + Commandstats: sdlgoredis.CommandstatsInfoFields{ + CmdstatReplconf: sdlgoredis.CommandstatsValues{ + Calls: 1090183, + Usec: 2790911, + UsecPerCall: 2.56, + }, + CmdstatKeys: sdlgoredis.CommandstatsValues{ + Calls: 20, + Usec: 233, + UsecPerCall: 11.65, + }, + CmdstatRole: sdlgoredis.CommandstatsValues{ + Calls: 2, + Usec: 84, + UsecPerCall: 42.00, + }, + CmdstatConfig: sdlgoredis.CommandstatsValues{}, + CmdstatPsync: sdlgoredis.CommandstatsValues{ + Calls: 23, + Usec: 3582, + UsecPerCall: 155.74, + }, + CmdstatMset: sdlgoredis.CommandstatsValues{ + Calls: 4, + Usec: 30, + UsecPerCall: 7.50, + }, + CmdstatPublish: sdlgoredis.CommandstatsValues{ + Calls: 804867, + Usec: 12965499, + UsecPerCall: 16.11, + }, + CmdstatInfo: sdlgoredis.CommandstatsValues{ + Calls: 164255, + Usec: 57935065, + UsecPerCall: 352.71, + }, + CmdstatPing: sdlgoredis.CommandstatsValues{ + Calls: 1582493, + Usec: 3466844, + UsecPerCall: 2.19, + }, + CmdstatClient: sdlgoredis.CommandstatsValues{ + Calls: 6, + Usec: 11, + UsecPerCall: 2.17, + }, + CmdstatCommand: sdlgoredis.CommandstatsValues{ + Calls: 482, + Usec: 1462633, + UsecPerCall: 3034.51, + }, + CmdstatSubscribe: sdlgoredis.CommandstatsValues{ + Calls: 3, + Usec: 11, + UsecPerCall: 3.67, + }, + CmdstatMonitor: sdlgoredis.CommandstatsValues{ + Calls: 3, + Usec: 6, + UsecPerCall: 2.00, + }, + CmdstatSlaveof: sdlgoredis.CommandstatsValues{}, + }, + Keyspace: sdlgoredis.KeyspaceInfoFields{ + Db: sdlgoredis.KeyspaceValues{ + Keys: 123, + }, + }, + }, + }, + }, + ) +} + +func setupStatisticsMockDbWithReplicaInfo() { + statsMock.dbStatistics.Stats = append(statsMock.dbStatistics.Stats, + &sdlgoredis.DbStatisticsInfo{ + IPAddr: "192.168.10.2", + Port: "6379", + Info: &sdlgoredis.DbInfo{ + Fields: sdlgoredis.DbInfoFields{ + PrimaryRole: false, + Server: sdlgoredis.ServerInfoFields{ + UptimeInDays: 2, + }, + Clients: sdlgoredis.ClientsInfoFields{ + ConnectedClients: 8, + ClientRecentMaxInputBuffer: 3, + ClientRecentMaxOutputBuffer: 1, + }, + Memory: sdlgoredis.MeroryInfoFields{ + UsedMemory: 2135768, + UsedMemoryHuman: "2.04M", + UsedMemoryRss: 13721720, + UsedMemoryRssHuman: "12.48M", + UsedMemoryPeak: 6706009, + UsedMemoryPeakHuman: "6.41M", + UsedMemoryPeakPerc: "30.93%", + MemFragmentationRatio: 6.67, + MemFragmentationBytes: 11379233, + }, + Stats: sdlgoredis.StatsInfoFields{ + TotalConnectionsReceived: 567, + TotalCommandsProcessed: 3635064, + SyncFull: 0, + SyncPartialOk: 0, + SyncPartialErr: 0, + PubsubChannels: 1, + }, + Cpu: sdlgoredis.CpuInfoFields{ + UsedCpuSys: 1457.472806, + UsedCpuUser: 861.060019, + }, + Commandstats: sdlgoredis.CommandstatsInfoFields{ + CmdstatReplconf: sdlgoredis.CommandstatsValues{}, + CmdstatKeys: sdlgoredis.CommandstatsValues{}, + CmdstatRole: sdlgoredis.CommandstatsValues{ + Calls: 1, + Usec: 3, + UsecPerCall: 32.00, + }, + CmdstatConfig: sdlgoredis.CommandstatsValues{ + Calls: 2, + Usec: 1462, + UsecPerCall: 146.74, + }, + CmdstatPsync: sdlgoredis.CommandstatsValues{}, + CmdstatMset: sdlgoredis.CommandstatsValues{ + Calls: 3, + Usec: 14, + UsecPerCall: 7.51, + }, + CmdstatPublish: sdlgoredis.CommandstatsValues{ + Calls: 804868, + Usec: 12965498, + UsecPerCall: 16.15, + }, + CmdstatInfo: sdlgoredis.CommandstatsValues{ + Calls: 164256, + Usec: 57935066, + UsecPerCall: 352.72, + }, + CmdstatPing: sdlgoredis.CommandstatsValues{ + Calls: 1582494, + Usec: 3466845, + UsecPerCall: 2.20, + }, + CmdstatClient: sdlgoredis.CommandstatsValues{}, + CmdstatCommand: sdlgoredis.CommandstatsValues{ + Calls: 483, + Usec: 1462634, + UsecPerCall: 3034.52, + }, + CmdstatSubscribe: sdlgoredis.CommandstatsValues{ + Calls: 3, + Usec: 12, + UsecPerCall: 3.68, + }, + CmdstatMonitor: sdlgoredis.CommandstatsValues{ + Calls: 6, + Usec: 11, + UsecPerCall: 2.11, + }, + CmdstatSlaveof: sdlgoredis.CommandstatsValues{ + Calls: 1, + Usec: 412, + UsecPerCall: 412.00, + }, + }, + Keyspace: sdlgoredis.KeyspaceInfoFields{ + Db: sdlgoredis.KeyspaceValues{ + Keys: 123, + }, + }, + }, + }, + }, + ) +} + +func runStatisticsCli() (string, error) { + buf := new(bytes.Buffer) + cmd := cli.NewStatisticsCmd(newStatisticsDatabaseMock) + cmd.SetOut(buf) + + err := cmd.Execute() + + return buf.String(), err +} + +func TestCliStatisticsCanShowHelp(t *testing.T) { + var expOkErr error + expHelp := "Usage:\n " + "statistics [flags]" + expNokErr := errors.New("unknown flag: --some-unknown-flag") + expArgCntErr := errors.New("accepts 0 arg(s), received 1") + tests := []struct { + args string + expErr error + expOutput string + }{ + {args: "-h", expErr: expOkErr, expOutput: expHelp}, + {args: "--help", expErr: expOkErr, expOutput: expHelp}, + {args: "--some-unknown-flag", expErr: expNokErr, expOutput: expHelp}, + {args: "some-extra-argument", expErr: expArgCntErr, expOutput: expHelp}, + } + + for _, test := range tests { + buf := new(bytes.Buffer) + cmd := cli.NewStatisticsCmd(newStatisticsDatabaseMock) + cmd.SetOut(buf) + cmd.SetArgs([]string{test.args}) + + err := cmd.Execute() + + stdout := buf.String() + assert.Equal(t, test.expErr, err) + assert.Contains(t, stdout, test.expOutput) + } +} + +func TestCliStatisticsShowOnlyColumnsNames(t *testing.T) { + teardownTest := setupStatisticsTestcase(t) + defer teardownTest(t) + + expOut := "CLUSTER ROLE ADDRESS STATISTICS\n" + + stdout, err := runStatisticsCli() + + assert.Nil(t, err) + assert.Equal(t, expOut, stdout) +} + +func TestCliStatisticsShowPrimaryInfo(t *testing.T) { + teardownTest := setupStatisticsTestcase(t) + defer teardownTest(t) + + setupStatisticsMockDbWithPrimaryInfo() + expOut := columnNamesOut + primaryOut + + stdout, err := runStatisticsCli() + + assert.Nil(t, err) + assert.Equal(t, expOut, stdout) +} + +func TestCliStatisticsShowPrimaryAndReplicaInfo(t *testing.T) { + teardownTest := setupStatisticsTestcase(t) + defer teardownTest(t) + + setupStatisticsMockDbWithPrimaryInfo() + setupStatisticsMockDbWithReplicaInfo() + expOut := columnNamesOut + primaryOut + replicaOut + + stdout, err := runStatisticsCli() + + assert.Nil(t, err) + assert.Equal(t, expOut, stdout) +} + +func TestCliStatisticsRaisesError(t *testing.T) { + teardownTest := setupStatisticsTestcase(t) + defer teardownTest(t) + + expErr := errors.New("Boom!") + expOut := "Usage:\n statistics [flags]" + statsMock.dbErr = errors.New("Boom!") + + stdout, err := runStatisticsCli() + + assert.Equal(t, expErr, err) + assert.Contains(t, stdout, expOut) +} diff --git a/internal/cli/types.go b/internal/cli/types.go index eb86905..6cfe725 100644 --- a/internal/cli/types.go +++ b/internal/cli/types.go @@ -30,6 +30,7 @@ type iDatabase interface { Info() (*sdlgoredis.DbInfo, error) State() (*sdlgoredis.DbState, error) Keys(pattern string) ([]string, error) + Statistics() (*sdlgoredis.DbStatistics, error) } //Database struct is a holder for the internal database instances. diff --git a/internal/mocks/db_mocks_private_testing.go b/internal/mocks/db_mocks_private_testing.go index 42bb061..1a4e29b 100644 --- a/internal/mocks/db_mocks_private_testing.go +++ b/internal/mocks/db_mocks_private_testing.go @@ -46,6 +46,11 @@ func (m *MockDB) Keys(pattern string) ([]string, error) { return a.Get(0).([]string), a.Error(1) } +func (m *MockDB) Statistics() (*sdlgoredis.DbStatistics, error) { + a := m.Called() + return a.Get(0).(*sdlgoredis.DbStatistics), a.Error(1) +} + type MockSdlApi struct { mock.Mock } diff --git a/internal/sdlgoredis/dbinfo.go b/internal/sdlgoredis/dbinfo.go index 931cfb7..6f8a638 100644 --- a/internal/sdlgoredis/dbinfo.go +++ b/internal/sdlgoredis/dbinfo.go @@ -33,4 +33,80 @@ type DbInfo struct { type DbInfoFields struct { PrimaryRole bool ConnectedReplicaCnt uint32 + Server ServerInfoFields + Clients ClientsInfoFields + Memory MeroryInfoFields + Stats StatsInfoFields + Cpu CpuInfoFields + Commandstats CommandstatsInfoFields + Keyspace KeyspaceInfoFields +} + +type ServerInfoFields struct { + UptimeInDays uint32 +} + +type ClientsInfoFields struct { + ConnectedClients uint32 + ClientRecentMaxInputBuffer uint32 + ClientRecentMaxOutputBuffer uint32 +} + +type MeroryInfoFields struct { + UsedMemory uint64 + UsedMemoryHuman string + UsedMemoryRss uint64 + UsedMemoryRssHuman string + UsedMemoryPeak uint64 + UsedMemoryPeakHuman string + UsedMemoryPeakPerc string + MemFragmentationRatio float32 + MemFragmentationBytes uint32 +} + +type StatsInfoFields struct { + TotalConnectionsReceived uint32 + TotalCommandsProcessed uint32 + SyncFull uint32 + SyncPartialOk uint32 + SyncPartialErr uint32 + PubsubChannels uint32 +} + +type CpuInfoFields struct { + UsedCpuSys float64 + UsedCpuUser float64 +} + +type CommandstatsValues struct { + Calls uint32 + Usec uint32 + UsecPerCall float32 +} + +type CommandstatsInfoFields struct { + CmdstatReplconf CommandstatsValues + CmdstatKeys CommandstatsValues + CmdstatRole CommandstatsValues + CmdstatConfig CommandstatsValues + CmdstatPsync CommandstatsValues + CmdstatMset CommandstatsValues + CmdstatPublish CommandstatsValues + CmdstatInfo CommandstatsValues + CmdstatPing CommandstatsValues + CmdstatClient CommandstatsValues + CmdstatCommand CommandstatsValues + CmdstatSubscribe CommandstatsValues + CmdstatMonitor CommandstatsValues + CmdstatSlaveof CommandstatsValues +} + +type KeyspaceValues struct { + Keys uint32 + Expires uint32 + AvgTtl uint32 +} + +type KeyspaceInfoFields struct { + Db KeyspaceValues } diff --git a/internal/sdlgoredis/dbstatistics.go b/internal/sdlgoredis/dbstatistics.go new file mode 100644 index 0000000..c4b3d8a --- /dev/null +++ b/internal/sdlgoredis/dbstatistics.go @@ -0,0 +1,35 @@ +/* + Copyright (c) 2021 AT&T Intellectual Property. + Copyright (c) 2018-2021 Nokia. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +/* + * This source code is part of the near-RT RIC (RAN Intelligent Controller) + * platform project (RICP). + */ + +package sdlgoredis + +//DbStatistics struct contains list of DB's Statistics information. +type DbStatistics struct { + Stats []*DbStatisticsInfo +} + +//DbStatisticsInfo struct contains fields for DB Statistics information. +type DbStatisticsInfo struct { + IPAddr string + Port string + Info *DbInfo +} diff --git a/internal/sdlgoredis/sdlgoredis.go b/internal/sdlgoredis/sdlgoredis.go index 77dce09..0b93239 100644 --- a/internal/sdlgoredis/sdlgoredis.go +++ b/internal/sdlgoredis/sdlgoredis.go @@ -28,7 +28,9 @@ import ( "github.com/go-redis/redis/v7" "io" "log" + "net" "os" + "reflect" "strconv" "strings" "sync" @@ -499,20 +501,181 @@ func (db *DB) Info() (*DbInfo, error) { return &info, err } +func lineContains(line, substr string) bool { + return strings.Contains(line, substr) +} + +func getFieldValueStr(line, substr string) string { + if idx := strings.Index(line, substr); idx != -1 { + return line[idx+len(substr):] + } + return "" +} + +func getUint32FromString(s string) uint32 { + if val, err := strconv.ParseUint(s, 10, 32); err == nil { + return uint32(val) + } + return 0 +} + +func getUint64FromString(s string) uint64 { + if val, err := strconv.ParseUint(s, 10, 64); err == nil { + return uint64(val) + } + return 0 +} + +func getFloatFromString(s string, bitSize int) float64 { + if val, err := strconv.ParseFloat(s, bitSize); err == nil { + return val + } + return 0 +} + +func getFloat64FromString(s string) float64 { + return getFloatFromString(s, 64) +} + +func getFloat32FromString(s string) float32 { + return float32(getFloatFromString(s, 32)) +} + +func getValueString(values string, key string) string { + slice := strings.Split(values, ",") + for _, s := range slice { + if lineContains(s, key) { + return getFieldValueStr(s, key) + } + } + return "" +} + +func getCommandstatsValues(values string) (string, string, string) { + calls := getValueString(values, "calls=") + usec := getValueString(values, "usec=") + usecPerCall := getValueString(values, "usec_per_call=") + return calls, usec, usecPerCall +} + +func updateCommandstatsValues(i interface{}, line, cmdstat string) { + stype := reflect.ValueOf(i).Elem() + values := getFieldValueStr(line, cmdstat) + callsStr, usecStr, usecPerCallStr := getCommandstatsValues(values) + + callsField := stype.FieldByName("Calls") + callsField.Set(reflect.ValueOf(getUint32FromString(callsStr))) + + usecField := stype.FieldByName("Usec") + usecField.Set(reflect.ValueOf(getUint32FromString(usecStr))) + + usecPerCallField := stype.FieldByName("UsecPerCall") + usecPerCallField.Set(reflect.ValueOf(getFloat32FromString(usecPerCallStr))) +} + +func getKeyspaceValues(values string) (string, string, string) { + keys := getValueString(values, "keys=") + expires := getValueString(values, "expires=") + avgttl := getValueString(values, "avg_ttl=") + return keys, expires, avgttl +} + +func updateKeyspaceValues(i interface{}, line, keyspace string) { + stype := reflect.ValueOf(i).Elem() + values := getFieldValueStr(line, keyspace) + keysStr, expiresStr, avgttlStr := getKeyspaceValues(values) + + keysField := stype.FieldByName("Keys") + keysField.Set(reflect.ValueOf(getUint32FromString(keysStr))) + + expiresField := stype.FieldByName("Expires") + expiresField.Set(reflect.ValueOf(getUint32FromString(expiresStr))) + + avgttlField := stype.FieldByName("AvgTtl") + avgttlField.Set(reflect.ValueOf(getUint32FromString(avgttlStr))) +} + func readRedisInfoReplyFields(input []string, info *DbInfo) error { for _, line := range input { - if idx := strings.Index(line, "role:"); idx != -1 { - roleStr := line[idx+len("role:"):] - if roleStr == "master" { + switch { + case lineContains(line, "role:") && !lineContains(line, "_role:"): + if "master" == getFieldValueStr(line, "role:") { info.Fields.PrimaryRole = true } - } else if idx := strings.Index(line, "connected_slaves:"); idx != -1 { - cntStr := line[idx+len("connected_slaves:"):] - cnt, err := strconv.ParseUint(cntStr, 10, 32) - if err != nil { - return fmt.Errorf("Info reply error: %s", err.Error()) - } - info.Fields.ConnectedReplicaCnt = uint32(cnt) + case lineContains(line, "connected_slaves:"): + info.Fields.ConnectedReplicaCnt = getUint32FromString(getFieldValueStr(line, "connected_slaves:")) + case lineContains(line, "uptime_in_days:"): + info.Fields.Server.UptimeInDays = getUint32FromString(getFieldValueStr(line, "uptime_in_days:")) + case lineContains(line, "connected_clients:"): + info.Fields.Clients.ConnectedClients = getUint32FromString(getFieldValueStr(line, "connected_clients:")) + case lineContains(line, "client_recent_max_input_buffer:"): + info.Fields.Clients.ClientRecentMaxInputBuffer = getUint32FromString(getFieldValueStr(line, "client_recent_max_input_buffer:")) + case lineContains(line, "client_recent_max_output_buffer:"): + info.Fields.Clients.ClientRecentMaxOutputBuffer = getUint32FromString(getFieldValueStr(line, "client_recent_max_output_buffer:")) + case lineContains(line, "used_memory:"): + info.Fields.Memory.UsedMemory = getUint64FromString(getFieldValueStr(line, "used_memory:")) + case lineContains(line, "used_memory_human:"): + info.Fields.Memory.UsedMemoryHuman = getFieldValueStr(line, "used_memory_human:") + case lineContains(line, "used_memory_rss:"): + info.Fields.Memory.UsedMemoryRss = getUint64FromString(getFieldValueStr(line, "used_memory_rss:")) + case lineContains(line, "used_memory_rss_human:"): + info.Fields.Memory.UsedMemoryRssHuman = getFieldValueStr(line, "used_memory_rss_human:") + case lineContains(line, "used_memory_peak:"): + info.Fields.Memory.UsedMemoryPeak = getUint64FromString(getFieldValueStr(line, "used_memory_peak:")) + case lineContains(line, "used_memory_peak_human:"): + info.Fields.Memory.UsedMemoryPeakHuman = getFieldValueStr(line, "used_memory_peak_human:") + case lineContains(line, "used_memory_peak_perc:"): + info.Fields.Memory.UsedMemoryPeakPerc = getFieldValueStr(line, "used_memory_peak_perc:") + case lineContains(line, "mem_fragmentation_ratio:"): + info.Fields.Memory.MemFragmentationRatio = getFloat32FromString(getFieldValueStr(line, "mem_fragmentation_ratio:")) + case lineContains(line, "mem_fragmentation_bytes:"): + info.Fields.Memory.MemFragmentationBytes = getUint32FromString(getFieldValueStr(line, "mem_fragmentation_bytes:")) + case lineContains(line, "total_connections_received:"): + info.Fields.Stats.TotalConnectionsReceived = getUint32FromString(getFieldValueStr(line, "total_connections_received:")) + case lineContains(line, "total_commands_processed:"): + info.Fields.Stats.TotalCommandsProcessed = getUint32FromString(getFieldValueStr(line, "total_commands_processed:")) + case lineContains(line, "sync_full:"): + info.Fields.Stats.SyncFull = getUint32FromString(getFieldValueStr(line, "sync_full:")) + case lineContains(line, "sync_partial_ok:"): + info.Fields.Stats.SyncPartialOk = getUint32FromString(getFieldValueStr(line, "sync_partial_ok:")) + case lineContains(line, "sync_partial_err:"): + info.Fields.Stats.SyncPartialErr = getUint32FromString(getFieldValueStr(line, "sync_partial_err:")) + case lineContains(line, "pubsub_channels:"): + info.Fields.Stats.PubsubChannels = getUint32FromString(getFieldValueStr(line, "pubsub_channels:")) + case lineContains(line, "used_cpu_sys:"): + info.Fields.Cpu.UsedCpuSys = getFloat64FromString(getFieldValueStr(line, "used_cpu_sys:")) + case lineContains(line, "used_cpu_user:"): + info.Fields.Cpu.UsedCpuUser = getFloat64FromString(getFieldValueStr(line, "used_cpu_user:")) + case lineContains(line, "cmdstat_replconf:"): + updateCommandstatsValues(&info.Fields.Commandstats.CmdstatReplconf, line, "cmdstat_replconf:") + case lineContains(line, "cmdstat_keys:"): + updateCommandstatsValues(&info.Fields.Commandstats.CmdstatKeys, line, "cmdstat_keys:") + case lineContains(line, "cmdstat_role:"): + updateCommandstatsValues(&info.Fields.Commandstats.CmdstatRole, line, "cmdstat_role:") + case lineContains(line, "cmdstat_psync:"): + updateCommandstatsValues(&info.Fields.Commandstats.CmdstatPsync, line, "cmdstat_psync:") + case lineContains(line, "cmdstat_mset:"): + updateCommandstatsValues(&info.Fields.Commandstats.CmdstatMset, line, "cmdstat_mset:") + case lineContains(line, "cmdstat_publish:"): + updateCommandstatsValues(&info.Fields.Commandstats.CmdstatPublish, line, "cmdstat_publish:") + case lineContains(line, "cmdstat_info:"): + updateCommandstatsValues(&info.Fields.Commandstats.CmdstatInfo, line, "cmdstat_info:") + case lineContains(line, "cmdstat_ping:"): + updateCommandstatsValues(&info.Fields.Commandstats.CmdstatPing, line, "cmdstat_ping:") + case lineContains(line, "cmdstat_client:"): + updateCommandstatsValues(&info.Fields.Commandstats.CmdstatClient, line, "cmdstat_client:") + case lineContains(line, "cmdstat_command:"): + updateCommandstatsValues(&info.Fields.Commandstats.CmdstatCommand, line, "cmdstat_command:") + case lineContains(line, "cmdstat_subscribe:"): + updateCommandstatsValues(&info.Fields.Commandstats.CmdstatSubscribe, line, "cmdstat_subscribe:") + case lineContains(line, "cmdstat_monitor:"): + updateCommandstatsValues(&info.Fields.Commandstats.CmdstatMonitor, line, "cmdstat_monitor:") + case lineContains(line, "cmdstat_config:"): + updateCommandstatsValues(&info.Fields.Commandstats.CmdstatConfig, line, "cmdstat_config:") + case lineContains(line, "cmdstat_slaveof:"): + updateCommandstatsValues(&info.Fields.Commandstats.CmdstatSlaveof, line, "cmdstat_slaveof:") + case lineContains(line, "db0:"): + updateKeyspaceValues(&info.Fields.Keyspace.Db, line, "db0:") } } return nil @@ -546,6 +709,8 @@ func (db *DB) fillDbStateFromDbInfo(info *DbInfo) (*DbState, error) { PrimaryDbState: PrimaryDbState{ Fields: PrimaryDbStateFields{ Role: "master", + Ip: db.cfg.hostname, + Port: db.cfg.port, Flags: "master", }, }, @@ -562,6 +727,72 @@ func (db *DB) fillDbStateFromDbInfo(info *DbInfo) (*DbState, error) { return &dbState, dbState.Err } +func createReplicaDbClient(host string) *DB { + cfg := readConfig(osImpl{}) + cfg.sentinelPort = "" + cfg.clusterAddrList, cfg.port, _ = net.SplitHostPort(host) + return createDbClient(cfg, cfg.clusterAddrList, newRedisClient, subscribeNotifications, nil) +} + +func getStatisticsInfo(db *DB, host string) (*DbStatisticsInfo, error) { + dbStatisticsInfo := new(DbStatisticsInfo) + dbStatisticsInfo.IPAddr, dbStatisticsInfo.Port, _ = net.SplitHostPort(host) + + info, err := db.Info() + if err != nil { + return nil, err + } + dbStatisticsInfo.Info = info + + return dbStatisticsInfo, nil +} + +func sentinelStatistics(db *DB) (*DbStatistics, error) { + dbState := new(DbState) + dbStatistics := new(DbStatistics) + dbStatisticsInfo := new(DbStatisticsInfo) + var err error + + dbState, err = db.State() + if err != nil { + return nil, err + } + + dbStatisticsInfo, err = getStatisticsInfo(db, dbState.PrimaryDbState.GetAddress()) + dbStatistics.Stats = append(dbStatistics.Stats, dbStatisticsInfo) + + if dbState.ReplicasDbState != nil { + for _, r := range dbState.ReplicasDbState.States { + replicaDb := createReplicaDbClient(r.GetAddress()) + dbStatisticsInfo, err = getStatisticsInfo(replicaDb, r.GetAddress()) + replicaDb.CloseDB() + if err != nil { + return nil, err + } + dbStatistics.Stats = append(dbStatistics.Stats, dbStatisticsInfo) + } + } + + return dbStatistics, nil +} + +func standaloneStatistics(db *DB) (*DbStatistics, error) { + dbStatistics := new(DbStatistics) + + dbStatisticsInfo, err := getStatisticsInfo(db, net.JoinHostPort(db.cfg.hostname, db.cfg.port)) + dbStatistics.Stats = append(dbStatistics.Stats, dbStatisticsInfo) + + return dbStatistics, err +} + +func (db *DB) Statistics() (*DbStatistics, error) { + if db.cfg.sentinelPort != "" { + return sentinelStatistics(db) + } + + return standaloneStatistics(db) +} + var luaRefresh = redis.NewScript(`if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("pexpire", KEYS[1], ARGV[2]) else return 0 end`) func (db *DB) PExpireIE(key string, data interface{}, expiration time.Duration) error { diff --git a/internal/sdlgoredis/sdlgoredis_test.go b/internal/sdlgoredis/sdlgoredis_test.go index 34e057f..112d9cc 100644 --- a/internal/sdlgoredis/sdlgoredis_test.go +++ b/internal/sdlgoredis/sdlgoredis_test.go @@ -1207,7 +1207,6 @@ func TestInfoOfStandalonePrimaryRedisSuccessfully(t *testing.T) { } func TestInfoOfStandalonePrimaryRedisFailureWhenIntConversionFails(t *testing.T) { - expErr := errors.New("Info reply error: strconv.ParseUint: parsing \"not-int\": invalid syntax") _, r, db := setupHaEnv(true) redisInfo := "# Replication\r\n" + "role:master\r\n" + @@ -1222,7 +1221,7 @@ func TestInfoOfStandalonePrimaryRedisFailureWhenIntConversionFails(t *testing.T) r.On("Info", []string{"all"}).Return(redis.NewStringResult(redisInfo, nil)) info, err := db.Info() - assert.Equal(t, expErr, err) + assert.Nil(t, err) assert.Equal(t, expInfo, info) r.AssertExpectations(t) } @@ -1255,6 +1254,71 @@ func TestInfoWithEmptyContentSuccessfully(t *testing.T) { r.AssertExpectations(t) } +func TestInfoWithSomeStatisticsOfStandalonePrimaryRedis(t *testing.T) { + _, r, db := setupHaEnv(true) + redisInfo := "# Replication\r\n" + + "role:master\r\n" + + "connected_slaves:0\r\n" + + "min_slaves_good_slaves:0\r\n" + + "# Server\r\n" + + "uptime_in_days:23\r\n" + + "# Clients\r\n" + + "connected_clients:2\r\n" + + "# Memory\r\n" + + "used_memory:2093808\r\n" + + "used_memory_human:2.00M\r\n" + + "mem_fragmentation_ratio:6.44\r\n" + + "# Stats\r\n" + + "total_connections_received:278\r\n" + + "# CPU\r\n" + + "used_cpu_sys:1775.254919\r\n" + + "# Commandstats\r\n" + + "# cmdstat_role:calls=1,usec=3,usec_per_call=3.00\r\n" + + "# Keyspace\r\n" + + "db0:keys=4,expires=0,avg_ttl=0" + expInfo := &sdlgoredis.DbInfo{ + Fields: sdlgoredis.DbInfoFields{ + PrimaryRole: true, + ConnectedReplicaCnt: 0, + Server: sdlgoredis.ServerInfoFields{ + UptimeInDays: 23, + }, + Clients: sdlgoredis.ClientsInfoFields{ + ConnectedClients: 2, + }, + Memory: sdlgoredis.MeroryInfoFields{ + UsedMemory: 2093808, + UsedMemoryHuman: "2.00M", + MemFragmentationRatio: 6.44, + }, + Stats: sdlgoredis.StatsInfoFields{ + TotalConnectionsReceived: 278, + }, + Cpu: sdlgoredis.CpuInfoFields{ + UsedCpuSys: 1775.254919, + }, + Commandstats: sdlgoredis.CommandstatsInfoFields{ + CmdstatRole: sdlgoredis.CommandstatsValues{ + Calls: 1, + Usec: 3, + UsecPerCall: 3.00, + }, + }, + Keyspace: sdlgoredis.KeyspaceInfoFields{ + Db: sdlgoredis.KeyspaceValues{ + Keys: 4, + }, + }, + }, + } + + r.On("Info", []string{"all"}).Return(redis.NewStringResult(redisInfo, nil)) + info, err := db.Info() + assert.Nil(t, err) + assert.Equal(t, expInfo, info) + r.AssertExpectations(t) +} + func TestStateWithPrimaryAndTwoReplicaRedisSuccessfully(t *testing.T) { _, r, s, db := setupHaEnvWithSentinels(true, "3") @@ -1402,6 +1466,8 @@ func TestStateWithSinglePrimaryRedisSuccessfully(t *testing.T) { PrimaryDbState: sdlgoredis.PrimaryDbState{ Fields: sdlgoredis.PrimaryDbStateFields{ Role: "master", + Ip: "service-ricplt-dbaas-tcp-cluster-0.ricplt", + Port: "6376", Flags: "master", }, }, @@ -1428,6 +1494,8 @@ func TestStateWithSinglePrimaryRedisFailureWhenIntConversionFails(t *testing.T) PrimaryDbState: sdlgoredis.PrimaryDbState{ Fields: sdlgoredis.PrimaryDbStateFields{ Role: "master", + Ip: "service-ricplt-dbaas-tcp-cluster-0.ricplt", + Port: "6376", Flags: "master", }, }, @@ -1456,3 +1524,36 @@ func TestStateWithSinglePrimaryRedisFailureInInfoCall(t *testing.T) { assert.Equal(t, expState, ret) r.AssertExpectations(t) } + +func TestStatisticsWithSinglePrimaryRedisSuccessfully(t *testing.T) { + _, r, db := setupSingleEnv(true, "1") + redisInfo := "# Replication\r\n" + + "role:master\r\n" + + "connected_slaves:0\r\n" + + "min_slaves_good_slaves:0\r\n" + + "# Server\r\n" + + "uptime_in_days:12\r\n" + + expStatistics := &sdlgoredis.DbStatistics{ + Stats: []*sdlgoredis.DbStatisticsInfo{ + { + IPAddr: "service-ricplt-dbaas-tcp-cluster-0.ricplt", + Port: "6376", + Info: &sdlgoredis.DbInfo{ + Fields: sdlgoredis.DbInfoFields{ + PrimaryRole: true, + Server: sdlgoredis.ServerInfoFields{ + UptimeInDays: 12, + }, + }, + }, + }, + }, + } + + r.On("Info", []string{"all"}).Return(redis.NewStringResult(redisInfo, nil)) + ret, err := db.Statistics() + assert.Nil(t, err) + assert.Equal(t, expStatistics, ret) + r.AssertExpectations(t) +}