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 <petri.ovaska@nokia.com>
Version history
---------------
+[0.9.0] - 2021-12-16
+
+* SDL CLI tool
[0.8.0] - 2021-09-22
NewSetCmdForTest = newSetCmd
NewRemoveCmdForTest = newRemoveCmd
NewNamespacesCmdForTest = newNamespacesCmd
+ NewStatisticsCmd = newStatisticsCmd
)
--- /dev/null
+/*
+ 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()
+}
--- /dev/null
+/*
+ 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)
+}
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.
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
}
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
}
--- /dev/null
+/*
+ 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
+}
"github.com/go-redis/redis/v7"
"io"
"log"
+ "net"
"os"
+ "reflect"
"strconv"
"strings"
"sync"
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
PrimaryDbState: PrimaryDbState{
Fields: PrimaryDbStateFields{
Role: "master",
+ Ip: db.cfg.hostname,
+ Port: db.cfg.port,
Flags: "master",
},
},
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 {
}
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" +
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)
}
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")
PrimaryDbState: sdlgoredis.PrimaryDbState{
Fields: sdlgoredis.PrimaryDbStateFields{
Role: "master",
+ Ip: "service-ricplt-dbaas-tcp-cluster-0.ricplt",
+ Port: "6376",
Flags: "master",
},
},
PrimaryDbState: sdlgoredis.PrimaryDbState{
Fields: sdlgoredis.PrimaryDbStateFields{
Role: "master",
+ Ip: "service-ricplt-dbaas-tcp-cluster-0.ricplt",
+ Port: "6376",
Flags: "master",
},
},
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)
+}