Skip to content

Commit 63696d7

Browse files
authored
feat(codersdk): add debug handlers for logs, manifest, and token to agent (#12593)
* feat(codersdk): add debug handlers for logs, manifest, and token to agent * add more logging * use io.LimitReader instead of seeking
1 parent 135381b commit 63696d7

File tree

4 files changed

+148
-1
lines changed

4 files changed

+148
-1
lines changed

agent/agent.go

+50
Original file line numberDiff line numberDiff line change
@@ -1699,11 +1699,61 @@ func (a *agent) HandleHTTPMagicsockDebugLoggingState(w http.ResponseWriter, r *h
16991699
_, _ = fmt.Fprintf(w, "updated magicsock debug logging to %v", stateBool)
17001700
}
17011701

1702+
func (a *agent) HandleHTTPDebugManifest(w http.ResponseWriter, r *http.Request) {
1703+
sdkManifest := a.manifest.Load()
1704+
if sdkManifest == nil {
1705+
a.logger.Error(r.Context(), "no manifest in-memory")
1706+
w.WriteHeader(http.StatusInternalServerError)
1707+
_, _ = fmt.Fprintf(w, "no manifest in-memory")
1708+
return
1709+
}
1710+
1711+
w.WriteHeader(http.StatusOK)
1712+
if err := json.NewEncoder(w).Encode(sdkManifest); err != nil {
1713+
a.logger.Error(a.hardCtx, "write debug manifest", slog.Error(err))
1714+
}
1715+
}
1716+
1717+
func (a *agent) HandleHTTPDebugToken(w http.ResponseWriter, r *http.Request) {
1718+
tok := a.sessionToken.Load()
1719+
if tok == nil {
1720+
a.logger.Error(r.Context(), "no session token in-memory")
1721+
w.WriteHeader(http.StatusInternalServerError)
1722+
_, _ = fmt.Fprintf(w, "no session token in-memory")
1723+
return
1724+
}
1725+
w.WriteHeader(http.StatusOK)
1726+
_, _ = fmt.Fprintf(w, *tok)
1727+
}
1728+
1729+
func (a *agent) HandleHTTPDebugLogs(w http.ResponseWriter, r *http.Request) {
1730+
logPath := filepath.Join(a.logDir, "coder-agent.log")
1731+
f, err := os.Open(logPath)
1732+
if err != nil {
1733+
a.logger.Error(r.Context(), "open agent log file", slog.Error(err), slog.F("path", logPath))
1734+
w.WriteHeader(http.StatusInternalServerError)
1735+
_, _ = fmt.Fprintf(w, "could not open log file: %s", err)
1736+
return
1737+
}
1738+
defer f.Close()
1739+
1740+
// Limit to 10MB.
1741+
w.WriteHeader(http.StatusOK)
1742+
_, err = io.Copy(w, io.LimitReader(f, 10*1024*1024))
1743+
if err != nil && !errors.Is(err, io.EOF) {
1744+
a.logger.Error(r.Context(), "read agent log file", slog.Error(err))
1745+
return
1746+
}
1747+
}
1748+
17021749
func (a *agent) HTTPDebug() http.Handler {
17031750
r := chi.NewRouter()
17041751

1752+
r.Get("/debug/logs", a.HandleHTTPDebugLogs)
17051753
r.Get("/debug/magicsock", a.HandleHTTPDebugMagicsock)
17061754
r.Get("/debug/magicsock/debug-logging/{state}", a.HandleHTTPMagicsockDebugLoggingState)
1755+
r.Get("/debug/manifest", a.HandleHTTPDebugManifest)
1756+
r.Get("/debug/token", a.HandleHTTPDebugToken)
17071757
r.NotFound(func(w http.ResponseWriter, r *http.Request) {
17081758
w.WriteHeader(http.StatusNotFound)
17091759
_, _ = w.Write([]byte("404 not found"))

agent/agent_test.go

+62-1
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ import (
5555
"github.com/coder/coder/v2/agent/proto"
5656
"github.com/coder/coder/v2/codersdk"
5757
"github.com/coder/coder/v2/codersdk/agentsdk"
58+
"github.com/coder/coder/v2/cryptorand"
5859
"github.com/coder/coder/v2/pty/ptytest"
5960
"github.com/coder/coder/v2/tailnet"
6061
"github.com/coder/coder/v2/tailnet/tailnettest"
@@ -1974,11 +1975,21 @@ func TestAgent_WriteVSCodeConfigs(t *testing.T) {
19741975
func TestAgent_DebugServer(t *testing.T) {
19751976
t.Parallel()
19761977

1978+
logDir := t.TempDir()
1979+
logPath := filepath.Join(logDir, "coder-agent.log")
1980+
randLogStr, err := cryptorand.String(32)
1981+
require.NoError(t, err)
1982+
require.NoError(t, os.WriteFile(logPath, []byte(randLogStr), 0o600))
19771983
derpMap, _ := tailnettest.RunDERPAndSTUN(t)
19781984
//nolint:dogsled
19791985
conn, _, _, _, agnt := setupAgent(t, agentsdk.Manifest{
19801986
DERPMap: derpMap,
1981-
}, 0)
1987+
}, 0, func(c *agenttest.Client, o *agent.Options) {
1988+
o.ExchangeToken = func(context.Context) (string, error) {
1989+
return "token", nil
1990+
}
1991+
o.LogDir = logDir
1992+
})
19821993

19831994
awaitReachableCtx := testutil.Context(t, testutil.WaitLong)
19841995
ok := conn.AwaitReachable(awaitReachableCtx)
@@ -2059,6 +2070,56 @@ func TestAgent_DebugServer(t *testing.T) {
20592070
require.Contains(t, string(resBody), `invalid state "blah", must be a boolean`)
20602071
})
20612072
})
2073+
2074+
t.Run("Manifest", func(t *testing.T) {
2075+
t.Parallel()
2076+
2077+
ctx := testutil.Context(t, testutil.WaitLong)
2078+
req, err := http.NewRequestWithContext(ctx, http.MethodGet, srv.URL+"/debug/manifest", nil)
2079+
require.NoError(t, err)
2080+
2081+
res, err := srv.Client().Do(req)
2082+
require.NoError(t, err)
2083+
defer res.Body.Close()
2084+
require.Equal(t, http.StatusOK, res.StatusCode)
2085+
2086+
var v agentsdk.Manifest
2087+
require.NoError(t, json.NewDecoder(res.Body).Decode(&v))
2088+
require.NotNil(t, v)
2089+
})
2090+
2091+
t.Run("Token", func(t *testing.T) {
2092+
t.Parallel()
2093+
2094+
ctx := testutil.Context(t, testutil.WaitLong)
2095+
req, err := http.NewRequestWithContext(ctx, http.MethodGet, srv.URL+"/debug/token", nil)
2096+
require.NoError(t, err)
2097+
2098+
res, err := srv.Client().Do(req)
2099+
require.NoError(t, err)
2100+
require.Equal(t, http.StatusOK, res.StatusCode)
2101+
defer res.Body.Close()
2102+
resBody, err := io.ReadAll(res.Body)
2103+
require.NoError(t, err)
2104+
require.Equal(t, "token", string(resBody))
2105+
})
2106+
2107+
t.Run("Logs", func(t *testing.T) {
2108+
t.Parallel()
2109+
2110+
ctx := testutil.Context(t, testutil.WaitLong)
2111+
req, err := http.NewRequestWithContext(ctx, http.MethodGet, srv.URL+"/debug/logs", nil)
2112+
require.NoError(t, err)
2113+
2114+
res, err := srv.Client().Do(req)
2115+
require.NoError(t, err)
2116+
require.Equal(t, http.StatusOK, res.StatusCode)
2117+
defer res.Body.Close()
2118+
resBody, err := io.ReadAll(res.Body)
2119+
require.NoError(t, err)
2120+
require.NotEmpty(t, string(resBody))
2121+
require.Contains(t, string(resBody), randLogStr)
2122+
})
20622123
}
20632124

20642125
func TestAgent_ScriptLogging(t *testing.T) {

agent/api.go

+3
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,11 @@ func (a *agent) apiHandler() http.Handler {
3636
cacheDuration: cacheDuration,
3737
}
3838
r.Get("/api/v0/listening-ports", lp.handler)
39+
r.Get("/debug/logs", a.HandleHTTPDebugLogs)
3940
r.Get("/debug/magicsock", a.HandleHTTPDebugMagicsock)
4041
r.Get("/debug/magicsock/debug-logging/{state}", a.HandleHTTPMagicsockDebugLoggingState)
42+
r.Get("/debug/manifest", a.HandleHTTPDebugManifest)
43+
r.Get("/debug/token", a.HandleHTTPDebugToken)
4144

4245
return r
4346
}

codersdk/workspaceagentconn.go

+33
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,39 @@ func (c *WorkspaceAgentConn) DebugMagicsock(ctx context.Context) ([]byte, error)
372372
return bs, nil
373373
}
374374

375+
// DebugManifest returns the agent's in-memory manifest. Unfortunately this must
376+
// be returns as a []byte to avoid an import cycle.
377+
func (c *WorkspaceAgentConn) DebugManifest(ctx context.Context) ([]byte, error) {
378+
ctx, span := tracing.StartSpan(ctx)
379+
defer span.End()
380+
res, err := c.apiRequest(ctx, http.MethodGet, "/debug/manifest", nil)
381+
if err != nil {
382+
return nil, xerrors.Errorf("do request: %w", err)
383+
}
384+
defer res.Body.Close()
385+
bs, err := io.ReadAll(res.Body)
386+
if err != nil {
387+
return nil, xerrors.Errorf("read response body: %w", err)
388+
}
389+
return bs, nil
390+
}
391+
392+
// DebugLogs returns up to the last 10MB of `/tmp/coder-agent.log`
393+
func (c *WorkspaceAgentConn) DebugLogs(ctx context.Context) ([]byte, error) {
394+
ctx, span := tracing.StartSpan(ctx)
395+
defer span.End()
396+
res, err := c.apiRequest(ctx, http.MethodGet, "/debug/logs", nil)
397+
if err != nil {
398+
return nil, xerrors.Errorf("do request: %w", err)
399+
}
400+
defer res.Body.Close()
401+
bs, err := io.ReadAll(res.Body)
402+
if err != nil {
403+
return nil, xerrors.Errorf("read response body: %w", err)
404+
}
405+
return bs, nil
406+
}
407+
375408
// apiRequest makes a request to the workspace agent's HTTP API server.
376409
func (c *WorkspaceAgentConn) apiRequest(ctx context.Context, method, path string, body io.Reader) (*http.Response, error) {
377410
ctx, span := tracing.StartSpan(ctx)

0 commit comments

Comments
 (0)