Skip to content

Strawman: Have FTDC try calling DoCommand with {"command": "stats"} to get modular component stats. #5078

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions components/motor/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -172,3 +172,12 @@ func (c *client) IsMoving(ctx context.Context) (bool, error) {
}
return resp.IsMoving, nil
}

func (c *client) Stats() any {
resp, err := c.DoCommand(context.Background(), map[string]any{"command": "stats"})
if err != nil {
return nil
}

return resp
}
7 changes: 5 additions & 2 deletions ftdc/ftdc.go
Original file line number Diff line number Diff line change
Expand Up @@ -316,7 +316,9 @@ func (ftdc *FTDC) constructDatum() datum {

for idx := range statsers {
namedStatser := &statsers[idx]
datum.Data[namedStatser.name] = namedStatser.statser.Stats()
if stats := namedStatser.statser.Stats(); stats != nil {
datum.Data[namedStatser.name] = stats
}
}

return datum
Expand Down Expand Up @@ -521,6 +523,7 @@ func (ftdc *FTDC) getWriter() (io.Writer, error) {
// to return an error.
os.O_RDWR|os.O_CREATE|os.O_EXCL, 0o644)
if err == nil {
ftdc.logger.Debugw("FTDC created new output file", "path", ftdc.currOutputFile.Name())
break
}
ftdc.logger.Warnw("FTDC failed to open file", "err", err)
Expand Down Expand Up @@ -627,7 +630,7 @@ func (ftdc *FTDC) checkAndDeleteOldFiles() error {
// deletion testing. Filename generation uses padding such that we can rely on there before 2/4
// digits for every numeric value.
//
//nolint
// nolint
// Example filename: countingBytesTest1228324349/viam-server-2024-11-18T20-37-01Z.ftdc
var filenameTimeRe = regexp.MustCompile(`viam-server-(\d{4})-(\d{2})-(\d{2})T(\d{2})-(\d{2})-(\d{2})Z.ftdc`)

Expand Down
13 changes: 9 additions & 4 deletions module/testmodule/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -336,10 +336,15 @@ func (tm *testMotor) IsPowered(_ context.Context, _ map[string]interface{}) (boo
return false, 0.0, nil
}

// DoCommand trivially implements motor.Motor.
func (tm *testMotor) DoCommand(_ context.Context, _ map[string]interface{}) (map[string]interface{}, error) {
//nolint:nilnil
return nil, nil
func (tm *testMotor) DoCommand(ctx context.Context, req map[string]interface{}) (map[string]interface{}, error) {
if req["command"] == "stats" {
return map[string]any{
"BadSetPowers": 5,
"BadStat": "boo",
}, nil
}

return nil, fmt.Errorf("Unknown command: %v", req["command"])
}

// IsMoving trivially implements motor.Motor.
Expand Down
8 changes: 6 additions & 2 deletions robot/impl/local_robot.go
Original file line number Diff line number Diff line change
Expand Up @@ -370,8 +370,12 @@ func newWithResources(
// - Guarantee that the `rpcServer` is initialized (enough) when the web service is
// constructed to get a valid copy of its stats object (for the schema's sake). Even if
// the web service has not been "started".
ftdcDir := ftdc.DefaultDirectory(utils.ViamDotDir, partID)
ftdcWorker = ftdc.NewWithUploader(ftdcDir, conn, partID, logger.Sublogger("ftdc"))
if rOpts.ftdcWriter != nil {
ftdcWorker = ftdc.NewWithWriter(rOpts.ftdcWriter, logger.Sublogger("ftdc"))
} else {
ftdcDir := ftdc.DefaultDirectory(utils.ViamDotDir, partID)
ftdcWorker = ftdc.NewWithUploader(ftdcDir, conn, partID, logger.Sublogger("ftdc"))
}
if statser, err := sys.NewSelfSysUsageStatser(); err == nil {
ftdcWorker.Add("proc.viam-server", statser)
}
Expand Down
76 changes: 76 additions & 0 deletions robot/impl/local_robot_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package robotimpl

import (
"bytes"
"context"
"crypto/tls"
"crypto/x509"
Expand Down Expand Up @@ -44,6 +45,7 @@ import (
"go.viam.com/rdk/config"
"go.viam.com/rdk/examples/customresources/apis/gizmoapi"
"go.viam.com/rdk/examples/customresources/apis/summationapi"
"go.viam.com/rdk/ftdc"
rgrpc "go.viam.com/rdk/grpc"
internalcloud "go.viam.com/rdk/internal/cloud"
"go.viam.com/rdk/logging"
Expand Down Expand Up @@ -4653,3 +4655,77 @@ func TestListTunnels(t *testing.T) {
test.That(t, err, test.ShouldBeNil)
test.That(t, ttes, test.ShouldResemble, trafficTunnelEndpoints)
}

// TestModuleStats asserts that modular resources that respond to a `DoCommand({"command":
// "stats"})` will be captured into FTDC.
func TestModuleStats(t *testing.T) {
ctx := context.Background()
logger := logging.NewTestLogger(t)

testMotor := resource.NewModel("rdk", "test", "motor")
testPath := rtestutils.BuildTempModule(t, "module/testmodule")
cfg := &config.Config{
Modules: []config.Module{
{
Name: "mod",
ExePath: testPath,
},
},
Components: []resource.Config{
{
Name: "h",
Model: testMotor,
API: motor.API,
Attributes: rutils.AttributeMap{},
},
},
}

// We will capture FTDC data to an in-memory buffer.
serializedFTDCData := bytes.NewBuffer(nil)
robot, err := New(ctx, cfg, nil, logger, WithFTDCWriter(serializedFTDCData))
test.That(t, err, test.ShouldBeNil)
defer robot.Close(ctx)

options, _, addr := robottestutils.CreateBaseOptionsAndListener(t)
err = robot.StartWeb(ctx, options)
test.That(t, err, test.ShouldBeNil)

// Create a client connection to the robot.
robotConn, err := rgrpc.Dial(ctx, addr, logger)
test.That(t, err, test.ShouldBeNil)
defer utils.UncheckedErrorFunc(robotConn.Close)

// Wrap the client connection as a motor connection.
motorRes, err := motor.NewClientFromConn(ctx, robotConn, "some-remote", motor.Named("h"), logger)
test.That(t, err, test.ShouldBeNil)

// Assert that a call to `motor.client.Stats` results in a valid response.
resp := motorRes.(ftdc.Statser).Stats()
test.That(t, resp.(map[string]any)["BadSetPowers"], test.ShouldEqual, 5)

// Wait for a few seconds, hoping data gets written to FTDC and stop the robot.
time.Sleep(3 * time.Second)
robot.Close(ctx)

// Parse the FTDC data.
parsed, err := ftdc.Parse(serializedFTDCData)
test.That(t, err, test.ShouldBeNil)

// Assert we have some datapoints
test.That(t, len(parsed), test.ShouldBeGreaterThan, 0)

// Iterate over the FTDC data. Count the number of `BadSetPowers` occurrences.
badSetPowerCnt := 0
for _, datum := range parsed {
for _, reading := range datum.Readings {
if reading.MetricName == "rdk:component:motor/h.ResStats.BadSetPowers" {
badSetPowerCnt++
}
}
}

// We cannot assert that `badSetPowerCnt` ought to be exactly `len(parsed)`. FTDC may accrue
// data points before the module and resource are registered/created.
test.That(t, badSetPowerCnt, test.ShouldBeGreaterThan, 0)
}
11 changes: 11 additions & 0 deletions robot/impl/robot_options.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package robotimpl

import (
"io"

"go.viam.com/rdk/robot/web"
)

Expand All @@ -22,6 +24,8 @@ type options struct {
// whether or not to run FTDC
enableFTDC bool

ftdcWriter io.Writer

// disableCompleteConfigWorker starts the robot without the complete config worker - should only be used for tests.
disableCompleteConfigWorker bool
}
Expand Down Expand Up @@ -55,6 +59,13 @@ func WithFTDC() Option {
})
}

func WithFTDCWriter(writer io.Writer) Option {
return newFuncOption(func(o *options) {
o.enableFTDC = true
o.ftdcWriter = writer
})
}

// WithWebOptions returns a Option which sets the streamConfig
// used to enable audio/video streaming over WebRTC.
func WithWebOptions(opts ...web.Option) Option {
Expand Down
Loading