Skip to content
Merged
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
10 changes: 10 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,16 @@
bin
.DS_Store

# Binaries generated by make
vcr
vcr_linux_amd64
vcr_linux_arm64
vcr_windows_amd64.exe
vcr_macos_amd64
vcr_macos_arm64
tests/integration/bin/vcr-cli
tests/integration/bin/mockserver

# Lefthook local overrides
lefthook-local.yml

Expand Down
41 changes: 31 additions & 10 deletions docs/vcr_instance_log.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
## vcr instance log

This command will output the log of an instance.
Fetch logs from a deployed VCR instance.

By default, the command retrieves the last N log entries (controlled by `--history`) and exits.
Use the `--follow` (`-f`) flag to continuously stream new log entries until you press Ctrl+C.

```
vcr instance log --project-name <project-name> --instance-name <instance-name> [flags]
Expand All @@ -9,23 +12,41 @@ vcr instance log --project-name <project-name> --instance-name <instance-name> [
### Examples

```
# Output instance log by instance id:
# Print the last logs by instance id (default, no follow):
$ vcr instance log --id <instance-id>

# Output instance log by project and instance name:
# Print the last logs by project and instance name:
$ vcr instance log --project-name <project-name> --instance-name <instance-name>

# Continuously stream new logs (follow mode):
$ vcr instance log --project-name <project-name> --instance-name <instance-name> --follow

# Follow logs using the short flag:
$ vcr instance log -p <project-name> -n <instance-name> -f

# Print the last 500 log entries and exit:
$ vcr instance log --id <instance-id> --history 500

# Filter to show only errors and above:
$ vcr instance log -p <project-name> -n <instance-name> --log-level error

# Show only application logs (exclude provider logs):
$ vcr instance log -p <project-name> -n <instance-name> --source-type application

# Combine filters with follow:
$ vcr instance log -p <project-name> -n <instance-name> -l warn -s application -f
```

### Options

```
--history int Prints the last N number of records (default 300)
-i, --id string Instance ID
-n, --instance-name string Instance name (must be used with project-name flag)
-l, --log-level string Filter for log level, e.g.trace, debug, info, warn, error, fatal
-p, --project-name string Project name (must be used with instance-name flag)
-s, --source-type string Filter for source type e.g. application, provider
-f, --follow Continuously stream new log entries (press Ctrl+C to stop)
--history int Number of historical log entries to fetch initially (default 300)
-i, --id string Instance UUID (alternative to project-name + instance-name)
-n, --instance-name string Instance name (requires --project-name)
-l, --log-level string Minimum log level: trace, debug, info, warn, error, fatal
-p, --project-name string Project name (requires --instance-name)
-s, --source-type string Filter by source: application, provider
```

### Options inherited from parent commands
Expand All @@ -44,4 +65,4 @@ $ vcr instance log --project-name <project-name> --instance-name <instance-name>

* [vcr instance](vcr_instance.md) - Used for instance management

###### Auto generated by spf13/cobra on 26-Nov-2024
###### Auto generated by spf13/cobra on 13-Apr-2026
37 changes: 24 additions & 13 deletions vcr/instance/log/log.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ type Options struct {
LogLevel string
SourceType string
Limit int
Follow bool
}

func NewCmdInstanceLog(f cmdutil.Factory) *cobra.Command {
Expand All @@ -62,11 +63,12 @@ func NewCmdInstanceLog(f cmdutil.Factory) *cobra.Command {
cmd := &cobra.Command{
Use: "log",
Aliases: []string{"logs"},
Short: "Stream real-time logs from a deployed VCR instance",
Long: heredoc.Doc(`Stream real-time logs from a deployed VCR instance.
Short: "Fetch logs from a deployed VCR instance",
Long: heredoc.Doc(`Fetch logs from a deployed VCR instance.

This command connects to a running instance and streams its logs in real-time
to your terminal. Logs are continuously fetched until you press Ctrl+C.
By default, the command retrieves the last N log entries (controlled by --history)
and exits. Use --follow (-f) to continuously stream new log entries until you
press Ctrl+C.

IDENTIFYING THE INSTANCE
You can identify the instance using either:
Expand All @@ -93,27 +95,29 @@ func NewCmdInstanceLog(f cmdutil.Factory) *cobra.Command {
`),
Args: cobra.MaximumNArgs(0),
Example: heredoc.Doc(`
# Stream logs by project and instance name
# Print the last logs by project and instance name (default, exits after output)
$ vcr instance log --project-name my-app --instance-name dev
2024-01-15T10:30:00Z [application] Server started on port 3000
2024-01-15T10:30:01Z [application] Connected to database
^C
Interrupt received, stopping...

# Stream logs by instance ID
# Print the last logs by instance ID
$ vcr instance log --id 12345678-1234-1234-1234-123456789abc

# Continuously stream new logs (press Ctrl+C to stop)
$ vcr instance log -p my-app -n dev --follow
$ vcr instance log -p my-app -n dev -f

# Print the last 500 log entries and exit
$ vcr instance log -p my-app -n dev --history 500

# Filter to show only errors and above
$ vcr instance log -p my-app -n dev --log-level error

# Show only application logs (exclude provider logs)
$ vcr instance log -p my-app -n dev --source-type application

# Increase history to last 500 log entries
$ vcr instance log -p my-app -n dev --history 500

# Combine filters
$ vcr instance log -p my-app -n dev -l warn -s application
# Combine filters with follow
$ vcr instance log -p my-app -n dev -l warn -s application -f
`),
RunE: func(_ *cobra.Command, _ []string) error {
ctx, cancel := context.WithDeadline(context.Background(), opts.Deadline())
Expand All @@ -129,6 +133,7 @@ func NewCmdInstanceLog(f cmdutil.Factory) *cobra.Command {
cmd.Flags().StringVarP(&opts.InstanceName, "instance-name", "n", "", "Instance name (requires --project-name)")
cmd.Flags().StringVarP(&opts.LogLevel, "log-level", "l", "", "Minimum log level: trace, debug, info, warn, error, fatal")
cmd.Flags().StringVarP(&opts.SourceType, "source-type", "s", "", "Filter by source: application, provider")
cmd.Flags().BoolVarP(&opts.Follow, "follow", "f", false, "Continuously stream new log entries (press Ctrl+C to stop)")

return cmd
}
Expand All @@ -146,6 +151,12 @@ func runLog(ctx context.Context, opts *Options) error {

opts.InstanceID = inst.ID

// Without --follow just print the historical logs and exit.
if !opts.Follow {
fetchLogs(io, opts, time.Time{})
return nil
}
Comment thread
gorkarevilla marked this conversation as resolved.
Comment thread
gorkarevilla marked this conversation as resolved.

ticker := time.NewTicker(TickerInterval)
defer ticker.Stop()
lastTimestamp := time.Time{}
Expand Down
97 changes: 95 additions & 2 deletions vcr/instance/log/log_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"bytes"
"errors"
"io"
"os"
"testing"
"time"

Expand All @@ -21,8 +22,10 @@ func TestLog(t *testing.T) {
type mock struct {
LogListLogsByInstanceIDTimes int
LogGetInstByProjAndInstNameTimes int
LogGetInstanceByIDTimes int
LogListLogsByInstanceIDReturnErr error
LogGetInstByProjAndInstNameReturnErr error
LogGetInstanceByIDReturnErr error
LogReturnLogs []api.Log
LogReturnInstance api.Instance
LogProjectName string
Expand Down Expand Up @@ -69,6 +72,41 @@ func TestLog(t *testing.T) {
errMsg: "failed to validate flags: must provide either 'id' flag or 'project-name' and 'instance-name' flags",
},
},
{
name: "default-no-follow-fetches-once-by-instance-id",
cli: "--id=abc-123",
mock: mock{
LogListLogsByInstanceIDTimes: 1,
LogGetInstByProjAndInstNameTimes: 0,
LogGetInstanceByIDTimes: 1,
LogReturnInstance: api.Instance{ID: "abc-123"},
LogInstanceID: "abc-123",
LogReturnLogs: []api.Log{{Timestamp: time.Now(), SourceType: "application", Message: "hello"}},
LogListLogsByInstanceIDReturnErr: nil,
LogGetInstByProjAndInstNameReturnErr: nil,
LogGetInstanceByIDReturnErr: nil,
},
want: want{
stdout: "[application] hello",
},
},
{
name: "default-no-follow-get-instance-error",
cli: "--id=bad-id",
mock: mock{
LogListLogsByInstanceIDTimes: 0,
LogGetInstByProjAndInstNameTimes: 0,
LogGetInstanceByIDTimes: 1,
LogReturnInstance: api.Instance{},
LogInstanceID: "bad-id",
LogListLogsByInstanceIDReturnErr: nil,
LogGetInstByProjAndInstNameReturnErr: nil,
LogGetInstanceByIDReturnErr: errors.New("datastore error"),
},
want: want{
errMsg: "failed to get instance",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
Expand All @@ -83,6 +121,10 @@ func TestLog(t *testing.T) {
GetInstanceByProjectAndInstanceName(gomock.Any(), tt.mock.LogProjectName, tt.mock.LogInstanceName).
Times(tt.mock.LogGetInstByProjAndInstNameTimes).
Return(tt.mock.LogReturnInstance, tt.mock.LogGetInstByProjAndInstNameReturnErr)
datastoreMock.EXPECT().
GetInstanceByID(gomock.Any(), tt.mock.LogInstanceID).
Times(tt.mock.LogGetInstanceByIDTimes).
Return(tt.mock.LogReturnInstance, tt.mock.LogGetInstanceByIDReturnErr)
datastoreMock.EXPECT().ListLogsByInstanceID(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
Times(tt.mock.LogListLogsByInstanceIDTimes).
Return(tt.mock.LogReturnLogs, tt.mock.LogListLogsByInstanceIDReturnErr)
Expand All @@ -104,7 +146,7 @@ func TestLog(t *testing.T) {

if _, err := cmd.ExecuteC(); err != nil && tt.want.errMsg != "" {
require.Error(t, err, "should throw error")
require.Equal(t, tt.want.errMsg, err.Error())
require.Contains(t, err.Error(), tt.want.errMsg)
return
}
Comment thread
gorkarevilla marked this conversation as resolved.
cmdOut := &testutil.CmdOut{
Expand All @@ -116,7 +158,11 @@ func TestLog(t *testing.T) {
return
}
require.NoError(t, err, "should not throw error")
require.Equal(t, tt.want.stdout, cmdOut.String())
if tt.want.stdout != "" {
require.Contains(t, cmdOut.String(), tt.want.stdout)
} else {
require.Equal(t, tt.want.stdout, cmdOut.String())
}
})
}
}
Expand Down Expand Up @@ -249,3 +295,50 @@ func Test_printLogs(t *testing.T) {
})
}
}

func TestLog_Follow(t *testing.T) {
ctrl := gomock.NewController(t)

datastoreMock := mocks.NewMockDatastoreInterface(ctrl)
deploymentMock := mocks.NewMockDeploymentInterface(ctrl)

datastoreMock.EXPECT().
GetInstanceByID(gomock.Any(), "abc-123").
Times(1).
Return(api.Instance{ID: "abc-123"}, nil)

// Track how many times ListLogsByInstanceID is called and send SIGTERM
// after the second tick so the follow loop exits cleanly.
callCount := 0
datastoreMock.EXPECT().
ListLogsByInstanceID(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
MinTimes(2).
DoAndReturn(func(_ interface{}, _ interface{}, _ interface{}, _ interface{}) ([]api.Log, error) {
callCount++
if callCount >= 2 {
// Send an interrupt to the current process so runLog's signal
// handler fires and the follow loop exits.
p, _ := os.FindProcess(os.Getpid())
_ = p.Signal(os.Interrupt)
}
return []api.Log{{Timestamp: time.Now(), SourceType: "application", Message: "streaming"}}, nil
})

ios, _, stdout, _ := iostreams.Test()

argv, err := shlex.Split("--id=abc-123 --follow")
require.NoError(t, err)

f := testutil.DefaultFactoryMock(t, ios, nil, nil, datastoreMock, deploymentMock, nil, nil)

cmd := NewCmdInstanceLog(f)
cmd.SetArgs(argv)
cmd.SetIn(&bytes.Buffer{})
cmd.SetOut(io.Discard)
cmd.SetErr(io.Discard)

_, err = cmd.ExecuteC()
require.NoError(t, err, "follow should exit cleanly on interrupt")
require.GreaterOrEqual(t, callCount, 2, "logs should have been fetched at least twice")
require.Contains(t, stdout.String(), "[application] streaming")
}
Loading