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

Commit b361110

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 b361110

File tree

16 files changed

+224
-56
lines changed

16 files changed

+224
-56
lines changed

cmd/coder/main.go

Lines changed: 5 additions & 4 deletions
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

Lines changed: 4 additions & 4 deletions
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 {

internal/activity/pusher.go

Lines changed: 3 additions & 3 deletions
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

Lines changed: 131 additions & 0 deletions
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.Fprintln(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.Fprintln(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

Lines changed: 5 additions & 1 deletion
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

Lines changed: 7 additions & 4 deletions
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

Lines changed: 17 additions & 9 deletions
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

Lines changed: 2 additions & 3 deletions
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

Lines changed: 3 additions & 4 deletions
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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy