Skip to content

Commit

Permalink
Merge pull request #40 from ethpandaops/feat/cmd-logs
Browse files Browse the repository at this point in the history
feat(logs): adds new logs cmd
  • Loading branch information
mattevans authored Jan 15, 2025
2 parents 1f445c7 + fa8e0b4 commit 74a1eeb
Show file tree
Hide file tree
Showing 13 changed files with 450 additions and 3 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ contributoor status # Check service status
contributoor restart # Restart the service
contributoor config # View/edit configuration
contributoor update # Update to latest version
contributoor logs # Show logs
```

## 🔨 Development
Expand Down
2 changes: 1 addition & 1 deletion cmd/cli/commands/install/display.go
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ func (d *InstallDisplay) OnComplete() error {

fmt.Printf("\n%sInstallation complete%s\n", tui.TerminalColorGreen, tui.TerminalColorReset)
fmt.Printf("You can now manage contributoor using the following command(s):\n")
fmt.Printf(" contributoor [start|stop|status|update|config]\n")
fmt.Printf(" contributoor [start|stop|restart|status|update|config|logs]\n")

return nil
}
101 changes: 101 additions & 0 deletions cmd/cli/commands/logs/logs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package logs

import (
"fmt"

"github.com/ethpandaops/contributoor-installer/cmd/cli/options"
"github.com/ethpandaops/contributoor-installer/internal/sidecar"
"github.com/ethpandaops/contributoor-installer/internal/tui"
"github.com/ethpandaops/contributoor/pkg/config/v1"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
)

func RegisterCommands(app *cli.App, opts *options.CommandOpts) {
app.Commands = append(app.Commands, cli.Command{
Name: "logs",
Usage: "View Contributoor logs",
UsageText: "contributoor logs [options]",
Flags: []cli.Flag{
cli.IntFlag{
Name: "tail",
Usage: "Number of lines to show from the end of logs",
Value: 100,
},
cli.BoolFlag{
Name: "follow, f",
Usage: "Follow log output",
},
},
Action: func(c *cli.Context) error {
var (
log = opts.Logger()
installerCfg = opts.InstallerConfig()
)

sidecarCfg, err := sidecar.NewConfigService(log, c.GlobalString("config-path"))
if err != nil {
return fmt.Errorf("%s%v%s", tui.TerminalColorRed, err, tui.TerminalColorReset)
}

dockerSidecar, err := sidecar.NewDockerSidecar(log, sidecarCfg, installerCfg)
if err != nil {
return fmt.Errorf("error creating docker sidecar service: %w", err)
}

systemdSidecar, err := sidecar.NewSystemdSidecar(log, sidecarCfg, installerCfg)
if err != nil {
return fmt.Errorf("error creating systemd sidecar service: %w", err)
}

binarySidecar, err := sidecar.NewBinarySidecar(log, sidecarCfg, installerCfg)
if err != nil {
return fmt.Errorf("error creating binary sidecar service: %w", err)
}

return showLogs(c, log, sidecarCfg, dockerSidecar, systemdSidecar, binarySidecar)
},
})
}

func showLogs(
c *cli.Context,
log *logrus.Logger,
sidecarCfg sidecar.ConfigManager,
docker sidecar.DockerSidecar,
systemd sidecar.SystemdSidecar,
binary sidecar.BinarySidecar,
) error {
var (
runner sidecar.SidecarRunner
cfg = sidecarCfg.Get()
)

// Get the appropriate runner based on run method.
switch cfg.RunMethod {
case config.RunMethod_RUN_METHOD_DOCKER:
runner = docker
case config.RunMethod_RUN_METHOD_SYSTEMD:
runner = systemd
case config.RunMethod_RUN_METHOD_BINARY:
runner = binary
default:
return fmt.Errorf("invalid sidecar run method: %s", cfg.RunMethod)
}

// Check if service is running.
running, err := runner.IsRunning()
if err != nil {
log.Errorf("could not check sidecar status: %v", err)

return err
}

if !running {
fmt.Printf("%sContributoor is not running%s\n", tui.TerminalColorYellow, tui.TerminalColorReset)

return nil
}

return runner.Logs(c.Int("tail"), c.Bool("follow"))
}
232 changes: 232 additions & 0 deletions cmd/cli/commands/logs/logs_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
package logs

import (
"errors"
"flag"
"testing"

"github.com/ethpandaops/contributoor-installer/cmd/cli/options"
sidecarmock "github.com/ethpandaops/contributoor-installer/internal/sidecar/mock"
"github.com/ethpandaops/contributoor/pkg/config/v1"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/urfave/cli"
"go.uber.org/mock/gomock"
)

func TestShowLogs(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()

tests := []struct {
name string
runMethod config.RunMethod
tailLines int
follow bool
setupMocks func(*sidecarmock.MockConfigManager, *sidecarmock.MockDockerSidecar, *sidecarmock.MockBinarySidecar, *sidecarmock.MockSystemdSidecar)
expectedError string
}{
{
name: "docker - shows logs successfully",
runMethod: config.RunMethod_RUN_METHOD_DOCKER,
tailLines: 100,
follow: false,
setupMocks: func(cfg *sidecarmock.MockConfigManager, d *sidecarmock.MockDockerSidecar, b *sidecarmock.MockBinarySidecar, s *sidecarmock.MockSystemdSidecar) {
cfg.EXPECT().Get().Return(&config.Config{
RunMethod: config.RunMethod_RUN_METHOD_DOCKER,
}).Times(1)
d.EXPECT().IsRunning().Return(true, nil)
d.EXPECT().Logs(100, false).Return(nil)
},
},
{
name: "docker - service not running",
runMethod: config.RunMethod_RUN_METHOD_DOCKER,
tailLines: 100,
follow: false,
setupMocks: func(cfg *sidecarmock.MockConfigManager, d *sidecarmock.MockDockerSidecar, b *sidecarmock.MockBinarySidecar, s *sidecarmock.MockSystemdSidecar) {
cfg.EXPECT().Get().Return(&config.Config{
RunMethod: config.RunMethod_RUN_METHOD_DOCKER,
}).Times(1)
d.EXPECT().IsRunning().Return(false, nil)
},
},
{
name: "docker - logs fail",
runMethod: config.RunMethod_RUN_METHOD_DOCKER,
tailLines: 100,
follow: false,
setupMocks: func(cfg *sidecarmock.MockConfigManager, d *sidecarmock.MockDockerSidecar, b *sidecarmock.MockBinarySidecar, s *sidecarmock.MockSystemdSidecar) {
cfg.EXPECT().Get().Return(&config.Config{
RunMethod: config.RunMethod_RUN_METHOD_DOCKER,
}).Times(1)
d.EXPECT().IsRunning().Return(true, nil)
d.EXPECT().Logs(100, false).Return(errors.New("logs failed"))
},
expectedError: "logs failed",
},
{
name: "binary - shows logs successfully",
runMethod: config.RunMethod_RUN_METHOD_BINARY,
tailLines: 50,
follow: true,
setupMocks: func(cfg *sidecarmock.MockConfigManager, d *sidecarmock.MockDockerSidecar, b *sidecarmock.MockBinarySidecar, s *sidecarmock.MockSystemdSidecar) {
cfg.EXPECT().Get().Return(&config.Config{
RunMethod: config.RunMethod_RUN_METHOD_BINARY,
}).Times(1)
b.EXPECT().IsRunning().Return(true, nil)
b.EXPECT().Logs(50, true).Return(nil)
},
},
{
name: "systemd - shows logs successfully",
runMethod: config.RunMethod_RUN_METHOD_SYSTEMD,
tailLines: 200,
follow: false,
setupMocks: func(cfg *sidecarmock.MockConfigManager, d *sidecarmock.MockDockerSidecar, b *sidecarmock.MockBinarySidecar, s *sidecarmock.MockSystemdSidecar) {
cfg.EXPECT().Get().Return(&config.Config{
RunMethod: config.RunMethod_RUN_METHOD_SYSTEMD,
}).Times(1)
s.EXPECT().IsRunning().Return(true, nil)
s.EXPECT().Logs(200, false).Return(nil)
},
},
{
name: "invalid sidecar run method",
runMethod: config.RunMethod_RUN_METHOD_UNSPECIFIED,
tailLines: 100,
follow: false,
setupMocks: func(cfg *sidecarmock.MockConfigManager, d *sidecarmock.MockDockerSidecar, b *sidecarmock.MockBinarySidecar, s *sidecarmock.MockSystemdSidecar) {
cfg.EXPECT().Get().Return(&config.Config{
RunMethod: config.RunMethod_RUN_METHOD_UNSPECIFIED,
}).Times(1)
},
expectedError: "invalid sidecar run method",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var (
mockConfig = sidecarmock.NewMockConfigManager(ctrl)
mockDocker = sidecarmock.NewMockDockerSidecar(ctrl)
mockBinary = sidecarmock.NewMockBinarySidecar(ctrl)
mockSystemd = sidecarmock.NewMockSystemdSidecar(ctrl)
)

tt.setupMocks(mockConfig, mockDocker, mockBinary, mockSystemd)

var (
app = cli.NewApp()
set = flag.NewFlagSet("test", flag.ContinueOnError)
)

set.Int("tail", tt.tailLines, "")
set.Bool("follow", tt.follow, "")
ctx := cli.NewContext(app, set, nil)

err := showLogs(ctx, logrus.New(), mockConfig, mockDocker, mockSystemd, mockBinary)

if tt.expectedError != "" {
assert.ErrorContains(t, err, tt.expectedError)

return
}

assert.NoError(t, err)
})
}
}

func TestRegisterCommands(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()

tests := []struct {
name string
configPath string
expectedError string
}{
{
name: "successfully registers command",
configPath: "testdata/valid",
},
{
name: "fails when config service fails",
configPath: "/invalid/path/that/doesnt/exist",
expectedError: "directory [/invalid/path/that/doesnt/exist] does not exist",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Create dummy CLI app, with the config flag.
app := cli.NewApp()
app.Flags = []cli.Flag{
cli.StringFlag{
Name: "config-path",
},
}

// Ensure we set the config path flag.
globalSet := flag.NewFlagSet("test", flag.ContinueOnError)
globalSet.String("config-path", "", "")

err := globalSet.Set("config-path", tt.configPath)
require.NoError(t, err)

// Create the cmd context.
globalCtx := cli.NewContext(app, globalSet, nil)
app.Metadata = map[string]interface{}{
"flagContext": globalCtx,
}

RegisterCommands(
app,
options.NewCommandOpts(
options.WithName("logs"),
options.WithLogger(logrus.New()),
),
)

if tt.expectedError != "" {
// Ensure the command registration succeeded
assert.NoError(t, err)

// Assert that the action execution fails as expected.
cmd := app.Commands[0]
ctx := cli.NewContext(app, nil, globalCtx)

// Assert that the action is the func we expect.
action, ok := cmd.Action.(func(*cli.Context) error)
require.True(t, ok, "expected action to be func(*cli.Context) error")

// Execute the action and assert the error.
actionErr := action(ctx)
assert.Error(t, actionErr)
assert.ErrorContains(t, actionErr, tt.expectedError)
} else {
// Ensure the command registration succeeded.
assert.NoError(t, err)
assert.Len(t, app.Commands, 1)

// Ensure the command is registered as expected.
cmd := app.Commands[0]
assert.Equal(t, "logs", cmd.Name)
assert.Equal(t, "View Contributoor logs", cmd.Usage)
assert.Equal(t, "contributoor logs [options]", cmd.UsageText)
assert.NotNil(t, cmd.Action)

// Verify flags.
assert.Len(t, cmd.Flags, 2)
tailFlag, _ := cmd.Flags[0].(cli.IntFlag)
followFlag, _ := cmd.Flags[1].(cli.BoolFlag)

assert.Equal(t, "tail", tailFlag.Name)
assert.Equal(t, 100, tailFlag.Value)
assert.Equal(t, "follow, f", followFlag.Name)
}
})
}
}
6 changes: 6 additions & 0 deletions cmd/cli/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (

"github.com/ethpandaops/contributoor-installer/cmd/cli/commands/config"
"github.com/ethpandaops/contributoor-installer/cmd/cli/commands/install"
"github.com/ethpandaops/contributoor-installer/cmd/cli/commands/logs"
"github.com/ethpandaops/contributoor-installer/cmd/cli/commands/restart"
"github.com/ethpandaops/contributoor-installer/cmd/cli/commands/start"
"github.com/ethpandaops/contributoor-installer/cmd/cli/commands/status"
Expand Down Expand Up @@ -107,6 +108,11 @@ func main() {
options.WithLogger(log),
))

logs.RegisterCommands(app, options.NewCommandOpts(
options.WithName("logs"),
options.WithLogger(log),
))

// Handle normal exit.
app.After = func(c *cli.Context) error {
return nil
Expand Down
4 changes: 2 additions & 2 deletions install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -361,9 +361,9 @@ setup_macos_launchd() {
<key>WorkingDirectory</key>
<string>$CONTRIBUTOOR_PATH</string>
<key>StandardOutPath</key>
<string>$CONTRIBUTOOR_PATH/logs/service.log</string>
<string>$CONTRIBUTOOR_PATH/logs/debug.log</string>
<key>StandardErrorPath</key>
<string>$CONTRIBUTOOR_PATH/logs/error.log</string>
<string>$CONTRIBUTOOR_PATH/logs/service.log</string>
<key>EnvironmentVariables</key>
<dict>
<key>PATH</key>
Expand Down
Loading

0 comments on commit 74a1eeb

Please sign in to comment.