Skip to content

Commit 7b03890

Browse files
committed
feat: Add simple request logging to CLI
1 parent d181731 commit 7b03890

File tree

4 files changed

+104
-2
lines changed

4 files changed

+104
-2
lines changed

cli/requestlogging.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package cli
2+
3+
import (
4+
"fmt"
5+
"io"
6+
"net/http"
7+
"strconv"
8+
)
9+
10+
type loggingRoundTripper struct {
11+
http.RoundTripper
12+
io.Writer
13+
}
14+
15+
func newLoggingRoundTripper(writer io.Writer) http.RoundTripper {
16+
return &loggingRoundTripper{
17+
Writer: writer,
18+
}
19+
}
20+
21+
func (lrt loggingRoundTripper) RoundTrip(request *http.Request) (*http.Response, error) {
22+
inner := lrt.RoundTripper
23+
if inner == nil {
24+
inner = http.DefaultTransport
25+
}
26+
27+
response, err := inner.RoundTrip(request)
28+
29+
var displayedStatusCode string
30+
if err != nil {
31+
displayedStatusCode = "(err)"
32+
} else {
33+
displayedStatusCode = strconv.Itoa(response.StatusCode)
34+
}
35+
36+
_, _ = fmt.Fprintf(lrt.Writer, "%s %s %s\n", request.Method, request.URL.String(), displayedStatusCode)
37+
return response, err
38+
}

cli/requestlogging_test.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package cli_test
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/require"
7+
8+
"github.com/coder/coder/cli/clitest"
9+
"github.com/coder/coder/coderd/coderdtest"
10+
"github.com/coder/coder/pty/ptytest"
11+
)
12+
13+
func TestRequestLogging(t *testing.T) {
14+
t.Parallel()
15+
t.Run("List", func(t *testing.T) {
16+
t.Parallel()
17+
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true})
18+
user := coderdtest.CreateFirstUser(t, client)
19+
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
20+
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
21+
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
22+
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
23+
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
24+
cmd, root := clitest.New(t, "ls", "--log-requests")
25+
clitest.SetupConfig(t, client, root)
26+
doneChan := make(chan struct{})
27+
pty := ptytest.New(t)
28+
cmd.SetIn(pty.Input())
29+
cmd.SetOut(pty.Output())
30+
go func() {
31+
defer close(doneChan)
32+
err := cmd.Execute()
33+
require.NoError(t, err)
34+
}()
35+
pty.ExpectMatch("GET " + client.URL.String() + "/api/v2/workspaces 200")
36+
pty.ExpectMatch(workspace.Name)
37+
pty.ExpectMatch("Running")
38+
<-doneChan
39+
})
40+
}

cli/root.go

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ const (
3535
varGlobalConfig = "global-config"
3636
varNoOpen = "no-open"
3737
varForceTty = "force-tty"
38+
varLogRequests = "log-requests"
3839
)
3940

4041
func init() {
@@ -91,11 +92,14 @@ func Root() *cobra.Command {
9192

9293
cmd.PersistentFlags().String(varURL, "", "Specify the URL to your deployment.")
9394
cmd.PersistentFlags().String(varToken, "", "Specify an authentication token.")
95+
cliflag.String(cmd.PersistentFlags(), varGlobalConfig, "", "CODER_CONFIG_DIR", configdir.LocalConfig("coderv2"), "Specify the path to the global `coder` config directory.")
96+
cmd.PersistentFlags().Bool(varLogRequests, false, "Log requests made to remote API endpoints.")
97+
98+
// Hidden flags for internal use.
9499
cliflag.String(cmd.PersistentFlags(), varAgentToken, "", "CODER_AGENT_TOKEN", "", "Specify an agent authentication token.")
95100
_ = cmd.PersistentFlags().MarkHidden(varAgentToken)
96101
cliflag.String(cmd.PersistentFlags(), varAgentURL, "", "CODER_AGENT_URL", "", "Specify the URL for an agent to access your deployment.")
97102
_ = cmd.PersistentFlags().MarkHidden(varAgentURL)
98-
cliflag.String(cmd.PersistentFlags(), varGlobalConfig, "", "CODER_CONFIG_DIR", configdir.LocalConfig("coderv2"), "Specify the path to the global `coder` config directory.")
99103
cmd.PersistentFlags().Bool(varForceTty, false, "Force the `coder` command to run as if connected to a TTY.")
100104
_ = cmd.PersistentFlags().MarkHidden(varForceTty)
101105
cmd.PersistentFlags().Bool(varNoOpen, false, "Block automatically opening URLs in the browser.")
@@ -126,7 +130,17 @@ func createClient(cmd *cobra.Command) (*codersdk.Client, error) {
126130
return nil, err
127131
}
128132
}
129-
client := codersdk.New(serverURL)
133+
logRequests, err := cmd.Flags().GetBool(varLogRequests)
134+
if err != nil {
135+
return nil, err
136+
}
137+
138+
var client *codersdk.Client
139+
if logRequests {
140+
client = codersdk.NewWithRoundTripper(serverURL, newLoggingRoundTripper(cmd.OutOrStderr()))
141+
} else {
142+
client = codersdk.New(serverURL)
143+
}
130144
client.SessionToken = token
131145
return client, nil
132146
}

codersdk/client.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,16 @@ func New(serverURL *url.URL) *Client {
2626
}
2727
}
2828

29+
// NewWithRoundTripper behaves like New, but allows specifying a custom implementation of http.RoundTripper.
30+
func NewWithRoundTripper(serverURL *url.URL, roundTripper http.RoundTripper) *Client {
31+
return &Client{
32+
URL: serverURL,
33+
HTTPClient: &http.Client{
34+
Transport: roundTripper,
35+
},
36+
}
37+
}
38+
2939
// Client is an HTTP caller for methods to the Coder API.
3040
// @typescript-ignore Client
3141
type Client struct {

0 commit comments

Comments
 (0)