Skip to content
This repository was archived by the owner on Aug 30, 2024. It is now read-only.

Commit 97595a1

Browse files
committed
Add clog package with CLIMessage and CLIError
- show rich log output for Info and Success logs - preserve rich CLI output for errors when possible
1 parent c9043b7 commit 97595a1

18 files changed

+233
-77
lines changed

cmd/coder/main.go

+5-4
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,9 @@ import (
99
"os"
1010
"runtime"
1111

12+
"cdr.dev/coder-cli/internal/clog"
1213
"cdr.dev/coder-cli/internal/cmd"
1314
"cdr.dev/coder-cli/internal/x/xterminal"
14-
15-
"go.coder.com/flog"
1615
)
1716

1817
// Using a global for the version so it can be set at build time using ldflags.
@@ -31,7 +30,8 @@ func main() {
3130

3231
stdoutState, err := xterminal.MakeOutputRaw(os.Stdout.Fd())
3332
if err != nil {
34-
flog.Fatal("set output to raw: %s", err)
33+
clog.Log(clog.Fatal(fmt.Sprintf("set output to raw: %s", err)))
34+
os.Exit(1)
3535
}
3636
defer func() {
3737
// Best effort. Would result in broken terminal on window but nothing we can do about it.
@@ -42,6 +42,7 @@ func main() {
4242
app.Version = fmt.Sprintf("%s %s %s/%s", version, runtime.Version(), runtime.GOOS, runtime.GOARCH)
4343

4444
if err := app.ExecuteContext(ctx); err != nil {
45-
flog.Fatal("%v", err)
45+
clog.Log(err)
46+
os.Exit(1)
4647
}
4748
}

coder-sdk/error.go

+4-4
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ import (
1212
// ErrNotFound describes an error case in which the requested resource could not be found
1313
var ErrNotFound = xerrors.Errorf("resource not found")
1414

15-
// apiError is the expected payload format for our errors.
16-
type apiError struct {
15+
// APIError is the expected payload format for our errors.
16+
type APIError struct {
1717
Err struct {
1818
Msg string `json:"msg"`
1919
} `json:"error"`
@@ -30,15 +30,15 @@ func (e *HTTPError) Error() string {
3030
return fmt.Sprintf("dump response: %+v", err)
3131
}
3232

33-
var msg apiError
33+
var msg APIError
3434
// Try to decode the payload as an error, if it fails or if there is no error message,
3535
// return the response URL with the dump.
3636
if err := json.NewDecoder(e.Response.Body).Decode(&msg); err != nil || msg.Err.Msg == "" {
3737
return fmt.Sprintf("%s\n%s", e.Response.Request.URL, dump)
3838
}
3939

4040
// If the payload was a in the expected error format with a message, include it.
41-
return fmt.Sprintf("%s\n%s%s", e.Response.Request.URL, dump, msg.Err.Msg)
41+
return msg.Err.Msg
4242
}
4343

4444
func bodyError(resp *http.Response) error {

go.mod

-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ require (
1616
github.com/rjeczalik/notify v0.9.2
1717
github.com/spf13/cobra v1.0.0
1818
github.com/stretchr/testify v1.6.1
19-
go.coder.com/flog v0.0.0-20190906214207-47dd47ea0512
2019
golang.org/x/crypto v0.0.0-20200422194213-44a606286825
2120
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a
2221
golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13

internal/activity/pusher.go

+3-3
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,13 @@ package activity
22

33
import (
44
"context"
5+
"fmt"
56
"time"
67

78
"golang.org/x/time/rate"
89

910
"cdr.dev/coder-cli/coder-sdk"
10-
11-
"go.coder.com/flog"
11+
"cdr.dev/coder-cli/internal/clog"
1212
)
1313

1414
const pushInterval = time.Minute
@@ -42,6 +42,6 @@ func (p *Pusher) Push(ctx context.Context) {
4242
}
4343

4444
if err := p.client.PushActivity(ctx, p.source, p.envID); err != nil {
45-
flog.Error("push activity: %s", err)
45+
clog.Log(clog.Error(fmt.Sprintf("push activity: %s", err)))
4646
}
4747
}

internal/clog/error.go

+131
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
package clog
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"os"
7+
"strings"
8+
9+
"github.com/fatih/color"
10+
"golang.org/x/xerrors"
11+
)
12+
13+
// CLIMessage provides a human-readable message for CLI errors and messages.
14+
type CLIMessage struct {
15+
Level string
16+
Color color.Attribute
17+
Header string
18+
Lines []string
19+
}
20+
21+
// CLIError wraps a CLIMessage and allows consumers to treat it as a normal error.
22+
type CLIError struct {
23+
CLIMessage
24+
error
25+
}
26+
27+
// String formats the CLI message for consumption by a human.
28+
func (m CLIMessage) String() string {
29+
var str strings.Builder
30+
str.WriteString(fmt.Sprintf("%s: %s\n",
31+
color.New(m.Color).Sprint(m.Level),
32+
color.New(color.Bold).Sprint(m.Header)),
33+
)
34+
for _, line := range m.Lines {
35+
str.WriteString(fmt.Sprintf(" %s %s\n", color.New(m.Color).Sprint("|"), line))
36+
}
37+
return str.String()
38+
}
39+
40+
// Log logs the given error to stderr, defaulting to "fatal" if the error is not a CLIError.
41+
// If the error is a CLIError, the plain error chain is ignored and the CLIError
42+
// is logged on its own.
43+
func Log(err error) {
44+
var cliErr CLIError
45+
if !xerrors.As(err, &cliErr) {
46+
cliErr = Fatal(err.Error())
47+
}
48+
fmt.Fprintln(os.Stderr, cliErr.String())
49+
}
50+
51+
// LogInfo prints the given info message to stderr.
52+
func LogInfo(header string, lines ...string) {
53+
fmt.Fprint(os.Stderr, CLIMessage{
54+
Level: "info",
55+
Color: color.FgBlue,
56+
Header: header,
57+
Lines: lines,
58+
}.String())
59+
}
60+
61+
// LogSuccess prints the given info message to stderr.
62+
func LogSuccess(header string, lines ...string) {
63+
fmt.Fprint(os.Stderr, CLIMessage{
64+
Level: "success",
65+
Color: color.FgGreen,
66+
Header: header,
67+
Lines: lines,
68+
}.String())
69+
}
70+
71+
// Warn creates an error with the level "warning".
72+
func Warn(header string, lines ...string) CLIError {
73+
return CLIError{
74+
CLIMessage: CLIMessage{
75+
Color: color.FgYellow,
76+
Level: "warning",
77+
Header: header,
78+
Lines: lines,
79+
},
80+
error: errors.New(header),
81+
}
82+
}
83+
84+
// Error creates an error with the level "error".
85+
func Error(header string, lines ...string) CLIError {
86+
return CLIError{
87+
CLIMessage: CLIMessage{
88+
Color: color.FgRed,
89+
Level: "error",
90+
Header: header,
91+
Lines: lines,
92+
},
93+
error: errors.New(header),
94+
}
95+
}
96+
97+
// Fatal creates an error with the level "fatal".
98+
func Fatal(header string, lines ...string) CLIError {
99+
return CLIError{
100+
CLIMessage: CLIMessage{
101+
Color: color.FgRed,
102+
Level: "fatal",
103+
Header: header,
104+
Lines: lines,
105+
},
106+
error: errors.New(header),
107+
}
108+
}
109+
110+
// Bold provides a convenience wrapper around color.New for brevity when logging.
111+
func Bold(a string) string {
112+
return color.New(color.Bold).Sprint(a)
113+
}
114+
115+
// Tip formats according to the given format specifier and prepends a bolded "tip: " header.
116+
func Tip(format string, a ...interface{}) string {
117+
return fmt.Sprintf("%s %s", Bold("tip:"), fmt.Sprintf(format, a...))
118+
}
119+
120+
// Hint formats according to the given format specifier and prepends a bolded "hint: " header.
121+
func Hint(format string, a ...interface{}) string {
122+
return fmt.Sprintf("%s %s", Bold("hint:"), fmt.Sprintf(format, a...))
123+
}
124+
125+
// Cause formats according to the given format specifier and prepends a bolded "cause: " header.
126+
func Cause(format string, a ...interface{}) string {
127+
return fmt.Sprintf("%s %s", Bold("cause:"), fmt.Sprintf(format, a...))
128+
}
129+
130+
// BlankLine is an empty string meant to be used in CLIMessage and CLIError construction.
131+
const BlankLine = ""

internal/cmd/auth.go

+5-1
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,14 @@ import (
88
"golang.org/x/xerrors"
99

1010
"cdr.dev/coder-cli/coder-sdk"
11+
"cdr.dev/coder-cli/internal/clog"
1112
"cdr.dev/coder-cli/internal/config"
1213
)
1314

14-
var errNeedLogin = xerrors.New("failed to read session credentials: did you run \"coder login\"?")
15+
var errNeedLogin = clog.Fatal(
16+
"failed to read session credentials",
17+
clog.Hint(`did you run "coder login [https://coder.domain.com]"?`),
18+
)
1519

1620
func newClient() (*coder.Client, error) {
1721
sessionToken, err := config.Session.Read()

internal/cmd/ceapi.go

+7-4
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"fmt"
66

77
"cdr.dev/coder-cli/coder-sdk"
8+
"cdr.dev/coder-cli/internal/clog"
89
"golang.org/x/xerrors"
910
)
1011

@@ -73,10 +74,12 @@ func findEnv(ctx context.Context, client *coder.Client, envName, userEmail strin
7374
found = append(found, env.Name)
7475
}
7576

76-
return nil, notFoundButDidFind{
77-
needle: envName,
78-
haystack: found,
79-
}
77+
return nil, clog.Fatal(
78+
"failed to find environment",
79+
fmt.Sprintf("environment %q not found in %q", envName, found),
80+
clog.BlankLine,
81+
clog.Tip("run \"coder envs ls\" to view your environments"),
82+
)
8083
}
8184

8285
type notFoundButDidFind struct {

internal/cmd/envs.go

+17-9
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,16 @@ package cmd
22

33
import (
44
"encoding/json"
5+
"fmt"
56
"os"
7+
"sync/atomic"
68

79
"cdr.dev/coder-cli/coder-sdk"
10+
"cdr.dev/coder-cli/internal/clog"
811
"cdr.dev/coder-cli/internal/x/xtabwriter"
912
"github.com/spf13/cobra"
1013
"golang.org/x/sync/errgroup"
1114
"golang.org/x/xerrors"
12-
13-
"go.coder.com/flog"
1415
)
1516

1617
func envsCommand() *cobra.Command {
@@ -37,7 +38,7 @@ func envsCommand() *cobra.Command {
3738
return err
3839
}
3940
if len(envs) < 1 {
40-
flog.Info("no environments found")
41+
clog.LogInfo("no environments found")
4142
return nil
4243
}
4344

@@ -92,26 +93,33 @@ coder envs --user charlie@coder.com ls -o json \
9293
}
9394

9495
var egroup errgroup.Group
96+
var fails int32
9597
for _, envName := range args {
9698
envName := envName
9799
egroup.Go(func() error {
98100
env, err := findEnv(cmd.Context(), client, envName, *user)
99101
if err != nil {
100-
flog.Error("failed to find environment by name \"%s\": %v", envName, err)
101-
return xerrors.Errorf("find environment by name: %w", err)
102+
atomic.AddInt32(&fails, 1)
103+
clog.Log(err)
104+
return xerrors.Errorf("find env by name: %w", err)
102105
}
103106

104107
if err = client.StopEnvironment(cmd.Context(), env.ID); err != nil {
105-
flog.Error("failed to stop environment \"%s\": %v", env.Name, err)
106-
return xerrors.Errorf("stop environment: %w", err)
108+
atomic.AddInt32(&fails, 1)
109+
err = clog.Fatal(fmt.Sprintf("stop environment %q", env.Name),
110+
clog.Cause(err.Error()), clog.BlankLine,
111+
clog.Hint("current environment status is %q", env.LatestStat.ContainerStatus),
112+
)
113+
clog.Log(err)
114+
return err
107115
}
108-
flog.Success("Successfully stopped environment %q", envName)
116+
clog.LogSuccess(fmt.Sprintf("successfully stopped environment %q", envName))
109117
return nil
110118
})
111119
}
112120

113121
if err = egroup.Wait(); err != nil {
114-
return xerrors.Errorf("some stop operations failed")
122+
return clog.Fatal(fmt.Sprintf("%d failure(s) emitted", fails))
115123
}
116124
return nil
117125
},

internal/cmd/login.go

+2-3
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,13 @@ import (
99
"strings"
1010

1111
"cdr.dev/coder-cli/coder-sdk"
12+
"cdr.dev/coder-cli/internal/clog"
1213
"cdr.dev/coder-cli/internal/config"
1314
"cdr.dev/coder-cli/internal/loginsrv"
1415
"github.com/pkg/browser"
1516
"github.com/spf13/cobra"
1617
"golang.org/x/sync/errgroup"
1718
"golang.org/x/xerrors"
18-
19-
"go.coder.com/flog"
2019
)
2120

2221
func makeLoginCmd() *cobra.Command {
@@ -140,7 +139,7 @@ func login(cmd *cobra.Command, envURL *url.URL, urlCfg, sessionCfg config.File)
140139
return xerrors.Errorf("store config: %w", err)
141140
}
142141

143-
flog.Success("Logged in.")
142+
clog.LogSuccess("logged in")
144143

145144
return nil
146145
}

internal/cmd/logout.go

+3-4
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,10 @@ package cmd
33
import (
44
"os"
55

6+
"cdr.dev/coder-cli/internal/clog"
67
"cdr.dev/coder-cli/internal/config"
78
"github.com/spf13/cobra"
89
"golang.org/x/xerrors"
9-
10-
"go.coder.com/flog"
1110
)
1211

1312
func makeLogoutCmd() *cobra.Command {
@@ -22,11 +21,11 @@ func logout(_ *cobra.Command, _ []string) error {
2221
err := config.Session.Delete()
2322
if err != nil {
2423
if os.IsNotExist(err) {
25-
flog.Info("no active session")
24+
clog.LogInfo("no active session")
2625
return nil
2726
}
2827
return xerrors.Errorf("delete session: %w", err)
2928
}
30-
flog.Success("logged out")
29+
clog.LogSuccess("logged out")
3130
return nil
3231
}

0 commit comments

Comments
 (0)