diff --git a/pkg/kubedog/display.go b/pkg/kubedog/display.go index bcd234e7..56af03fc 100644 --- a/pkg/kubedog/display.go +++ b/pkg/kubedog/display.go @@ -7,6 +7,8 @@ import ( "sort" "strings" + "golang.org/x/term" + "github.com/werf/kubedog/pkg/tracker/daemonset" "github.com/werf/kubedog/pkg/tracker/deployment" "github.com/werf/kubedog/pkg/tracker/indicators" @@ -209,15 +211,16 @@ func displayChildPodsStatusProgress(t *utils.Table, prevPods, pods map[string]po sort.Strings(podsNames) var podRows [][]interface{} + + newPodSet := make(map[string]struct{}, len(newPodsNames)) + for _, name := range newPodsNames { + newPodSet[name] = struct{}{} + } + for _, podName := range podsNames { var podRow []interface{} - isPodNew := false - for _, newPodName := range newPodsNames { - if newPodName == podName { - isPodNew = true - } - } + _, isPodNew := newPodSet[podName] prevPodStatus := prevPods[podName] podStatus := pods[podName] @@ -278,6 +281,9 @@ func formatResourceWarning(reason string) string { } func termWidth() int { + if w, _, err := term.GetSize(int(os.Stderr.Fd())); err == nil && w > 0 { + return w + } return 140 } diff --git a/pkg/kubedog/display_test.go b/pkg/kubedog/display_test.go new file mode 100644 index 00000000..f8084b9c --- /dev/null +++ b/pkg/kubedog/display_test.go @@ -0,0 +1,445 @@ +package kubedog + +import ( + "bytes" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/werf/kubedog/pkg/tracker/daemonset" + "github.com/werf/kubedog/pkg/tracker/deployment" + "github.com/werf/kubedog/pkg/tracker/job" + "github.com/werf/kubedog/pkg/tracker/pod" + "github.com/werf/kubedog/pkg/tracker/statefulset" +) + +// --- formatResourceCaption --- + +func TestFormatResourceCaption_Ready(t *testing.T) { + result := formatResourceCaption("deploy/myapp", true, false) + assert.Contains(t, result, "deploy/myapp") + // Green ANSI escape should be present + assert.Contains(t, result, "\033[") +} + +func TestFormatResourceCaption_Failed(t *testing.T) { + result := formatResourceCaption("deploy/myapp", false, true) + assert.Contains(t, result, "deploy/myapp") + assert.Contains(t, result, "\033[") +} + +func TestFormatResourceCaption_InProgress(t *testing.T) { + result := formatResourceCaption("deploy/myapp", false, false) + assert.Contains(t, result, "deploy/myapp") + // Yellow for in-progress + assert.Contains(t, result, "\033[") +} + +func TestFormatResourceCaption_ReadyTakesPrecedence(t *testing.T) { + // isReady=true should win over isFailed=true + resultReady := formatResourceCaption("x", true, false) + resultFailed := formatResourceCaption("x", false, true) + // Colors should differ + assert.NotEqual(t, resultReady, resultFailed) +} + +// --- formatPodResourceCaption --- + +func TestFormatPodResourceCaption_NotNew(t *testing.T) { + result := formatPodResourceCaption("my-pod-abc", true, false, false) + // Not a new pod: no coloring applied, just the plain name + assert.Equal(t, "my-pod-abc", result) +} + +func TestFormatPodResourceCaption_NewAndReady(t *testing.T) { + result := formatPodResourceCaption("my-pod-abc", true, false, true) + assert.Contains(t, result, "my-pod-abc") + assert.Contains(t, result, "\033[") +} + +func TestFormatPodResourceCaption_NewAndFailed(t *testing.T) { + result := formatPodResourceCaption("my-pod-abc", false, true, true) + assert.Contains(t, result, "my-pod-abc") + assert.Contains(t, result, "\033[") +} + +func TestFormatPodResourceCaption_NewInProgress(t *testing.T) { + result := formatPodResourceCaption("my-pod-abc", false, false, true) + assert.Contains(t, result, "my-pod-abc") + assert.Contains(t, result, "\033[") +} + +// --- formatResourceError / formatResourceWarning --- + +func TestFormatResourceError(t *testing.T) { + result := formatResourceError("CrashLoopBackOff") + assert.Contains(t, result, "error:") + assert.Contains(t, result, "CrashLoopBackOff") +} + +func TestFormatResourceWarning(t *testing.T) { + result := formatResourceWarning("PodNotScheduled") + assert.Contains(t, result, "warning:") + assert.Contains(t, result, "PodNotScheduled") +} + +// --- termWidth --- + +func TestTermWidth_ReturnsPositive(t *testing.T) { + w := termWidth() + assert.Greater(t, w, 0) +} + +// --- displayDeploymentStatusProgress --- + +func TestDisplayDeploymentStatusProgress_ZeroStatus(t *testing.T) { + var buf bytes.Buffer + caption := formatResourceCaption("deploy/myapp", false, false) + var prev deployment.DeploymentStatus + status := deployment.DeploymentStatus{} + + // Must not panic and must produce some output + assert.NotPanics(t, func() { + displayDeploymentStatusProgress(&buf, caption, status, &prev) + }) + out := buf.String() + assert.NotEmpty(t, out) + assert.Contains(t, out, "DEPLOYMENT") +} + +func TestDisplayDeploymentStatusProgress_Failed(t *testing.T) { + var buf bytes.Buffer + caption := formatResourceCaption("deploy/myapp", false, true) + var prev deployment.DeploymentStatus + status := deployment.DeploymentStatus{ + IsFailed: true, + FailedReason: "ImagePullBackOff", + } + + assert.NotPanics(t, func() { + displayDeploymentStatusProgress(&buf, caption, status, &prev) + }) + out := buf.String() + assert.Contains(t, out, "error:") + assert.Contains(t, out, "ImagePullBackOff") +} + +func TestDisplayDeploymentStatusProgress_WithWaitingMessage(t *testing.T) { + var buf bytes.Buffer + caption := formatResourceCaption("deploy/myapp", false, false) + var prev deployment.DeploymentStatus + // WaitingForMessages is only rendered when there are pods + status := deployment.DeploymentStatus{ + StatusGeneration: 1, + WaitingForMessages: []string{"up-to-date 1->3"}, + Pods: map[string]pod.PodStatus{ + "myapp-pod-abc": {ReadyContainers: 1, TotalContainers: 1}, + }, + } + + assert.NotPanics(t, func() { + displayDeploymentStatusProgress(&buf, caption, status, &prev) + }) + out := buf.String() + assert.Contains(t, out, "Waiting for:") + assert.Contains(t, out, "up-to-date 1->3") +} + +func TestDisplayDeploymentStatusProgress_WithPods(t *testing.T) { + var buf bytes.Buffer + caption := formatResourceCaption("deploy/myapp", false, false) + prev := deployment.DeploymentStatus{} + status := deployment.DeploymentStatus{ + StatusGeneration: 1, + Pods: map[string]pod.PodStatus{ + "myapp-abc-123": {ReadyContainers: 1, TotalContainers: 1}, + }, + NewPodsNames: []string{"myapp-abc-123"}, + } + + assert.NotPanics(t, func() { + displayDeploymentStatusProgress(&buf, caption, status, &prev) + }) + out := buf.String() + assert.Contains(t, out, "POD") + assert.Contains(t, out, "myapp-abc-123") +} + +// --- displayStatefulSetStatusProgress --- + +func TestDisplayStatefulSetStatusProgress_ZeroStatus(t *testing.T) { + var buf bytes.Buffer + caption := formatResourceCaption("sts/myapp", false, false) + var prev statefulset.StatefulSetStatus + status := statefulset.StatefulSetStatus{} + + assert.NotPanics(t, func() { + displayStatefulSetStatusProgress(&buf, caption, status, &prev) + }) + out := buf.String() + assert.Contains(t, out, "STATEFULSET") +} + +func TestDisplayStatefulSetStatusProgress_WithWarnings(t *testing.T) { + var buf bytes.Buffer + caption := formatResourceCaption("sts/myapp", false, false) + var prev statefulset.StatefulSetStatus + status := statefulset.StatefulSetStatus{ + WarningMessages: []string{"PodNotScheduled: insufficient resources"}, + } + + assert.NotPanics(t, func() { + displayStatefulSetStatusProgress(&buf, caption, status, &prev) + }) + out := buf.String() + assert.Contains(t, out, "warning:") + assert.Contains(t, out, "PodNotScheduled") +} + +func TestDisplayStatefulSetStatusProgress_Failed(t *testing.T) { + var buf bytes.Buffer + caption := formatResourceCaption("sts/myapp", false, true) + var prev statefulset.StatefulSetStatus + status := statefulset.StatefulSetStatus{ + IsFailed: true, + FailedReason: "timeout waiting for ready", + } + + assert.NotPanics(t, func() { + displayStatefulSetStatusProgress(&buf, caption, status, &prev) + }) + out := buf.String() + assert.Contains(t, out, "error:") + assert.Contains(t, out, "timeout waiting for ready") +} + +// --- displayDaemonSetStatusProgress --- + +func TestDisplayDaemonSetStatusProgress_ZeroStatus(t *testing.T) { + var buf bytes.Buffer + caption := formatResourceCaption("ds/myapp", false, false) + var prev daemonset.DaemonSetStatus + status := daemonset.DaemonSetStatus{} + + assert.NotPanics(t, func() { + displayDaemonSetStatusProgress(&buf, caption, status, &prev) + }) + out := buf.String() + assert.Contains(t, out, "DAEMONSET") +} + +func TestDisplayDaemonSetStatusProgress_Failed(t *testing.T) { + var buf bytes.Buffer + caption := formatResourceCaption("ds/myapp", false, true) + var prev daemonset.DaemonSetStatus + status := daemonset.DaemonSetStatus{ + IsFailed: true, + FailedReason: "node not ready", + } + + assert.NotPanics(t, func() { + displayDaemonSetStatusProgress(&buf, caption, status, &prev) + }) + out := buf.String() + assert.Contains(t, out, "error:") + assert.Contains(t, out, "node not ready") +} + +// --- displayJobStatusProgress --- + +func TestDisplayJobStatusProgress_ZeroStatus(t *testing.T) { + var buf bytes.Buffer + caption := formatResourceCaption("job/myjob", false, false) + var prev job.JobStatus + status := job.JobStatus{} + + assert.NotPanics(t, func() { + displayJobStatusProgress(&buf, caption, status, &prev) + }) + out := buf.String() + assert.Contains(t, out, "JOB") +} + +func TestDisplayJobStatusProgress_Active(t *testing.T) { + var buf bytes.Buffer + caption := formatResourceCaption("job/myjob", false, false) + var prev job.JobStatus + status := job.JobStatus{ + StatusGeneration: 1, + } + + assert.NotPanics(t, func() { + displayJobStatusProgress(&buf, caption, status, &prev) + }) + out := buf.String() + assert.Contains(t, out, "ACTIVE") +} + +func TestDisplayJobStatusProgress_Failed(t *testing.T) { + var buf bytes.Buffer + caption := formatResourceCaption("job/myjob", false, true) + var prev job.JobStatus + status := job.JobStatus{ + IsFailed: true, + FailedReason: "BackoffLimitExceeded", + } + + assert.NotPanics(t, func() { + displayJobStatusProgress(&buf, caption, status, &prev) + }) + out := buf.String() + assert.Contains(t, out, "error:") + assert.Contains(t, out, "BackoffLimitExceeded") +} + +func TestDisplayJobStatusProgress_WithWaitingMessage(t *testing.T) { + var buf bytes.Buffer + caption := formatResourceCaption("job/myjob", false, false) + var prev job.JobStatus + status := job.JobStatus{ + WaitingForMessages: []string{"succeeded 0->1"}, + Pods: map[string]pod.PodStatus{ + "myjob-abc": {ReadyContainers: 0, TotalContainers: 1}, + }, + } + + assert.NotPanics(t, func() { + displayJobStatusProgress(&buf, caption, status, &prev) + }) + out := buf.String() + assert.Contains(t, out, "Waiting for:") + assert.Contains(t, out, "succeeded 0->1") +} + +// --- displayChildPodsStatusProgress --- + +func TestDisplayChildPodsStatusProgress_Empty(t *testing.T) { + var buf bytes.Buffer + caption := formatResourceCaption("deploy/myapp", false, false) + // With no pods, only the header should be rendered + prev := deployment.DeploymentStatus{} + status := deployment.DeploymentStatus{ + Pods: map[string]pod.PodStatus{}, + } + assert.NotPanics(t, func() { + displayDeploymentStatusProgress(&buf, caption, status, &prev) + }) + // No POD sub-table header when pods is empty + out := buf.String() + assert.NotContains(t, out, "POD") +} + +func TestDisplayChildPodsStatusProgress_NewPodSet(t *testing.T) { + var buf bytes.Buffer + caption := formatResourceCaption("deploy/myapp", false, false) + prev := deployment.DeploymentStatus{} + // Two pods: one new, one old + status := deployment.DeploymentStatus{ + StatusGeneration: 1, + Pods: map[string]pod.PodStatus{ + "pod-new-abc": {ReadyContainers: 0, TotalContainers: 1}, + "pod-old-xyz": {ReadyContainers: 1, TotalContainers: 1}, + }, + NewPodsNames: []string{"pod-new-abc"}, + } + + assert.NotPanics(t, func() { + displayDeploymentStatusProgress(&buf, caption, status, &prev) + }) + out := buf.String() + assert.Contains(t, out, "pod-new-abc") + assert.Contains(t, out, "pod-old-xyz") +} + +func TestDisplayChildPodsStatusProgress_ManyPodsONCheck(t *testing.T) { + // Verifies O(1) new-pod detection works correctly for many pods + var buf bytes.Buffer + caption := formatResourceCaption("deploy/myapp", false, false) + prev := deployment.DeploymentStatus{} + + pods := make(map[string]pod.PodStatus) + newNames := make([]string, 0, 10) + for i := 0; i < 20; i++ { + name := strings.Repeat("a", i+1) + pods[name] = pod.PodStatus{ReadyContainers: 1, TotalContainers: 1} + if i%2 == 0 { + newNames = append(newNames, name) + } + } + status := deployment.DeploymentStatus{ + StatusGeneration: 1, + Pods: pods, + NewPodsNames: newNames, + } + + assert.NotPanics(t, func() { + displayDeploymentStatusProgress(&buf, caption, status, &prev) + }) + assert.NotEmpty(t, buf.String()) +} + +// --- displayCanaryStatus --- + +func TestDisplayCanaryStatus_Normal(t *testing.T) { + var buf bytes.Buffer + caption := formatResourceCaption("canary/myapp", false, false) + view := CanaryStatusView{Phase: "Progressing", Age: "1m"} + + assert.NotPanics(t, func() { + displayCanaryStatus(&buf, caption, view) + }) + out := buf.String() + assert.Contains(t, out, "Progressing") + assert.Contains(t, out, "1m") +} + +func TestDisplayCanaryStatus_Failed(t *testing.T) { + var buf bytes.Buffer + caption := formatResourceCaption("canary/myapp", false, true) + view := CanaryStatusView{Phase: "Failed", IsFailed: true} + + assert.NotPanics(t, func() { + displayCanaryStatus(&buf, caption, view) + }) + out := buf.String() + assert.Contains(t, out, "Failed") +} + +func TestDisplayCanaryStatus_Succeeded(t *testing.T) { + var buf bytes.Buffer + caption := formatResourceCaption("canary/myapp", true, false) + view := CanaryStatusView{Phase: "Succeeded"} + + assert.NotPanics(t, func() { + displayCanaryStatus(&buf, caption, view) + }) + out := buf.String() + assert.Contains(t, out, "Succeeded") +} + +func TestDisplayCanaryStatus_EmptyPhaseAndAge(t *testing.T) { + var buf bytes.Buffer + caption := formatResourceCaption("canary/myapp", false, false) + view := CanaryStatusView{} + + assert.NotPanics(t, func() { + displayCanaryStatus(&buf, caption, view) + }) + // Should still produce output (at least the caption + newline) + assert.NotEmpty(t, buf.String()) +} + +// --- writeOut --- + +func TestWriteOut(t *testing.T) { + var buf bytes.Buffer + writeOut(&buf, "hello world") + assert.Equal(t, "hello world", buf.String()) +} + +func TestWriteOut_Empty(t *testing.T) { + var buf bytes.Buffer + writeOut(&buf, "") + assert.Equal(t, "", buf.String()) +} diff --git a/pkg/kubedog/tracker.go b/pkg/kubedog/tracker.go index 679be994..095fd598 100644 --- a/pkg/kubedog/tracker.go +++ b/pkg/kubedog/tracker.go @@ -316,21 +316,23 @@ func (t *Tracker) runDeploymentTracker(ctx context.Context, tr *deployment.Track func (t *Tracker) waitDeploymentTracker(ctx context.Context, tr *deployment.Tracker, trackErrCh <-chan error, doneCh <-chan struct{}) error { var prevStatus deployment.DeploymentStatus out := statusOutput() - caption := formatResourceCaption(fmt.Sprintf("deploy/%s", tr.ResourceName), false, false) + resourceName := fmt.Sprintf("deploy/%s", tr.ResourceName) for { select { case status := <-tr.Added: - displayDeploymentStatusProgress(out, caption, status, &prevStatus) + displayDeploymentStatusProgress(out, formatResourceCaption(resourceName, false, false), status, &prevStatus) prevStatus = status case <-tr.Ready: + displayDeploymentStatusProgress(out, formatResourceCaption(resourceName, true, false), prevStatus, &prevStatus) t.logger.Infof("Deployment %s/%s is ready", tr.Namespace, tr.ResourceName) return nil case status := <-tr.Failed: + displayDeploymentStatusProgress(out, formatResourceCaption(resourceName, false, true), status, &prevStatus) return fmt.Errorf("deployment %s/%s failed: %s", tr.Namespace, tr.ResourceName, status.FailedReason) case status := <-tr.Status: if status.StatusGeneration > prevStatus.StatusGeneration { - displayDeploymentStatusProgress(out, caption, status, &prevStatus) + displayDeploymentStatusProgress(out, formatResourceCaption(resourceName, false, false), status, &prevStatus) prevStatus = status } case msg := <-tr.EventMsg: @@ -362,21 +364,23 @@ func (t *Tracker) runStatefulSetTracker(ctx context.Context, tr *statefulset.Tra func (t *Tracker) waitStatefulSetTracker(ctx context.Context, tr *statefulset.Tracker, trackErrCh <-chan error, doneCh <-chan struct{}) error { var prevStatus statefulset.StatefulSetStatus out := statusOutput() - caption := formatResourceCaption(fmt.Sprintf("sts/%s", tr.ResourceName), false, false) + resourceName := fmt.Sprintf("sts/%s", tr.ResourceName) for { select { case status := <-tr.Added: - displayStatefulSetStatusProgress(out, caption, status, &prevStatus) + displayStatefulSetStatusProgress(out, formatResourceCaption(resourceName, false, false), status, &prevStatus) prevStatus = status case <-tr.Ready: + displayStatefulSetStatusProgress(out, formatResourceCaption(resourceName, true, false), prevStatus, &prevStatus) t.logger.Infof("StatefulSet %s/%s is ready", tr.Namespace, tr.ResourceName) return nil case status := <-tr.Failed: + displayStatefulSetStatusProgress(out, formatResourceCaption(resourceName, false, true), status, &prevStatus) return fmt.Errorf("statefulset %s/%s failed: %s", tr.Namespace, tr.ResourceName, status.FailedReason) case status := <-tr.Status: if status.StatusGeneration > prevStatus.StatusGeneration { - displayStatefulSetStatusProgress(out, caption, status, &prevStatus) + displayStatefulSetStatusProgress(out, formatResourceCaption(resourceName, false, false), status, &prevStatus) prevStatus = status } case msg := <-tr.EventMsg: @@ -407,21 +411,23 @@ func (t *Tracker) runDaemonSetTracker(ctx context.Context, tr *daemonset.Tracker func (t *Tracker) waitDaemonSetTracker(ctx context.Context, tr *daemonset.Tracker, trackErrCh <-chan error, doneCh <-chan struct{}) error { var prevStatus daemonset.DaemonSetStatus out := statusOutput() - caption := formatResourceCaption(fmt.Sprintf("ds/%s", tr.ResourceName), false, false) + resourceName := fmt.Sprintf("ds/%s", tr.ResourceName) for { select { case status := <-tr.Added: - displayDaemonSetStatusProgress(out, caption, status, &prevStatus) + displayDaemonSetStatusProgress(out, formatResourceCaption(resourceName, false, false), status, &prevStatus) prevStatus = status case <-tr.Ready: + displayDaemonSetStatusProgress(out, formatResourceCaption(resourceName, true, false), prevStatus, &prevStatus) t.logger.Infof("DaemonSet %s/%s is ready", tr.Namespace, tr.ResourceName) return nil case status := <-tr.Failed: + displayDaemonSetStatusProgress(out, formatResourceCaption(resourceName, false, true), status, &prevStatus) return fmt.Errorf("daemonset %s/%s failed: %s", tr.Namespace, tr.ResourceName, status.FailedReason) case status := <-tr.Status: if status.StatusGeneration > prevStatus.StatusGeneration { - displayDaemonSetStatusProgress(out, caption, status, &prevStatus) + displayDaemonSetStatusProgress(out, formatResourceCaption(resourceName, false, false), status, &prevStatus) prevStatus = status } case msg := <-tr.EventMsg: @@ -452,21 +458,23 @@ func (t *Tracker) runJobTracker(ctx context.Context, tr *job.Tracker, errCh chan func (t *Tracker) waitJobTracker(ctx context.Context, tr *job.Tracker, trackErrCh <-chan error, doneCh <-chan struct{}) error { var prevStatus job.JobStatus out := statusOutput() - caption := formatResourceCaption(fmt.Sprintf("job/%s", tr.ResourceName), false, false) + resourceName := fmt.Sprintf("job/%s", tr.ResourceName) for { select { case status := <-tr.Added: - displayJobStatusProgress(out, caption, status, &prevStatus) + displayJobStatusProgress(out, formatResourceCaption(resourceName, false, false), status, &prevStatus) prevStatus = status case <-tr.Succeeded: + displayJobStatusProgress(out, formatResourceCaption(resourceName, true, false), prevStatus, &prevStatus) t.logger.Infof("Job %s/%s succeeded", tr.Namespace, tr.ResourceName) return nil case status := <-tr.Failed: + displayJobStatusProgress(out, formatResourceCaption(resourceName, false, true), status, &prevStatus) return fmt.Errorf("job %s/%s failed: %s", tr.Namespace, tr.ResourceName, status.FailedReason) case status := <-tr.Status: if status.StatusGeneration > prevStatus.StatusGeneration { - displayJobStatusProgress(out, caption, status, &prevStatus) + displayJobStatusProgress(out, formatResourceCaption(resourceName, false, false), status, &prevStatus) prevStatus = status } case msg := <-tr.EventMsg: @@ -496,22 +504,30 @@ func (t *Tracker) runCanaryTracker(ctx context.Context, tr *canary.Tracker, errC func (t *Tracker) waitCanaryTracker(ctx context.Context, tr *canary.Tracker, trackErrCh <-chan error, doneCh <-chan struct{}) error { out := statusOutput() - caption := formatResourceCaption(fmt.Sprintf("canary/%s", tr.ResourceName), false, false) + resourceName := fmt.Sprintf("canary/%s", tr.ResourceName) + var lastView CanaryStatusView for { select { case status := <-tr.Added: - displayCanaryStatus(out, caption, CanaryStatusView{ + view := CanaryStatusView{ Phase: string(status.CanaryStatus.Phase), IsFailed: status.IsFailed, - }) + } + displayCanaryStatus(out, formatResourceCaption(resourceName, false, false), view) + lastView = view case <-tr.Succeeded: + displayCanaryStatus(out, formatResourceCaption(resourceName, true, false), CanaryStatusView{Phase: lastView.Phase}) t.logger.Infof("Canary %s/%s succeeded", tr.Namespace, tr.ResourceName) return nil case status := <-tr.Failed: + displayCanaryStatus(out, formatResourceCaption(resourceName, false, true), CanaryStatusView{ + Phase: lastView.Phase, + IsFailed: true, + }) return fmt.Errorf("canary %s/%s failed: %s", tr.Namespace, tr.ResourceName, status.FailedReason) case status := <-tr.Status: - displayCanaryStatus(out, caption, CanaryStatusView{ + view := CanaryStatusView{ Phase: func() string { if status.StatusIndicator != nil { return status.StatusIndicator.Value @@ -520,7 +536,9 @@ func (t *Tracker) waitCanaryTracker(ctx context.Context, tr *canary.Tracker, tra }(), Age: status.Age, IsFailed: status.IsFailed, - }) + } + displayCanaryStatus(out, formatResourceCaption(resourceName, false, false), view) + lastView = view case msg := <-tr.EventMsg: t.logger.Infof("canary/%s: %s", tr.ResourceName, msg) case err := <-trackErrCh: