mirror of https://github.com/coder/coder.git
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
This commit is contained in:
parent
135381bb4e
commit
63696d762f
|
@ -1699,11 +1699,61 @@ func (a *agent) HandleHTTPMagicsockDebugLoggingState(w http.ResponseWriter, r *h
|
|||
_, _ = fmt.Fprintf(w, "updated magicsock debug logging to %v", stateBool)
|
||||
}
|
||||
|
||||
func (a *agent) HandleHTTPDebugManifest(w http.ResponseWriter, r *http.Request) {
|
||||
sdkManifest := a.manifest.Load()
|
||||
if sdkManifest == nil {
|
||||
a.logger.Error(r.Context(), "no manifest in-memory")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
_, _ = fmt.Fprintf(w, "no manifest in-memory")
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
if err := json.NewEncoder(w).Encode(sdkManifest); err != nil {
|
||||
a.logger.Error(a.hardCtx, "write debug manifest", slog.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
func (a *agent) HandleHTTPDebugToken(w http.ResponseWriter, r *http.Request) {
|
||||
tok := a.sessionToken.Load()
|
||||
if tok == nil {
|
||||
a.logger.Error(r.Context(), "no session token in-memory")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
_, _ = fmt.Fprintf(w, "no session token in-memory")
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = fmt.Fprintf(w, *tok)
|
||||
}
|
||||
|
||||
func (a *agent) HandleHTTPDebugLogs(w http.ResponseWriter, r *http.Request) {
|
||||
logPath := filepath.Join(a.logDir, "coder-agent.log")
|
||||
f, err := os.Open(logPath)
|
||||
if err != nil {
|
||||
a.logger.Error(r.Context(), "open agent log file", slog.Error(err), slog.F("path", logPath))
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
_, _ = fmt.Fprintf(w, "could not open log file: %s", err)
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
// Limit to 10MB.
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, err = io.Copy(w, io.LimitReader(f, 10*1024*1024))
|
||||
if err != nil && !errors.Is(err, io.EOF) {
|
||||
a.logger.Error(r.Context(), "read agent log file", slog.Error(err))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (a *agent) HTTPDebug() http.Handler {
|
||||
r := chi.NewRouter()
|
||||
|
||||
r.Get("/debug/logs", a.HandleHTTPDebugLogs)
|
||||
r.Get("/debug/magicsock", a.HandleHTTPDebugMagicsock)
|
||||
r.Get("/debug/magicsock/debug-logging/{state}", a.HandleHTTPMagicsockDebugLoggingState)
|
||||
r.Get("/debug/manifest", a.HandleHTTPDebugManifest)
|
||||
r.Get("/debug/token", a.HandleHTTPDebugToken)
|
||||
r.NotFound(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
_, _ = w.Write([]byte("404 not found"))
|
||||
|
|
|
@ -55,6 +55,7 @@ import (
|
|||
"github.com/coder/coder/v2/agent/proto"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
"github.com/coder/coder/v2/codersdk/agentsdk"
|
||||
"github.com/coder/coder/v2/cryptorand"
|
||||
"github.com/coder/coder/v2/pty/ptytest"
|
||||
"github.com/coder/coder/v2/tailnet"
|
||||
"github.com/coder/coder/v2/tailnet/tailnettest"
|
||||
|
@ -1974,11 +1975,21 @@ func TestAgent_WriteVSCodeConfigs(t *testing.T) {
|
|||
func TestAgent_DebugServer(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
logDir := t.TempDir()
|
||||
logPath := filepath.Join(logDir, "coder-agent.log")
|
||||
randLogStr, err := cryptorand.String(32)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, os.WriteFile(logPath, []byte(randLogStr), 0o600))
|
||||
derpMap, _ := tailnettest.RunDERPAndSTUN(t)
|
||||
//nolint:dogsled
|
||||
conn, _, _, _, agnt := setupAgent(t, agentsdk.Manifest{
|
||||
DERPMap: derpMap,
|
||||
}, 0)
|
||||
}, 0, func(c *agenttest.Client, o *agent.Options) {
|
||||
o.ExchangeToken = func(context.Context) (string, error) {
|
||||
return "token", nil
|
||||
}
|
||||
o.LogDir = logDir
|
||||
})
|
||||
|
||||
awaitReachableCtx := testutil.Context(t, testutil.WaitLong)
|
||||
ok := conn.AwaitReachable(awaitReachableCtx)
|
||||
|
@ -2059,6 +2070,56 @@ func TestAgent_DebugServer(t *testing.T) {
|
|||
require.Contains(t, string(resBody), `invalid state "blah", must be a boolean`)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("Manifest", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, srv.URL+"/debug/manifest", nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
res, err := srv.Client().Do(req)
|
||||
require.NoError(t, err)
|
||||
defer res.Body.Close()
|
||||
require.Equal(t, http.StatusOK, res.StatusCode)
|
||||
|
||||
var v agentsdk.Manifest
|
||||
require.NoError(t, json.NewDecoder(res.Body).Decode(&v))
|
||||
require.NotNil(t, v)
|
||||
})
|
||||
|
||||
t.Run("Token", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, srv.URL+"/debug/token", nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
res, err := srv.Client().Do(req)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusOK, res.StatusCode)
|
||||
defer res.Body.Close()
|
||||
resBody, err := io.ReadAll(res.Body)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "token", string(resBody))
|
||||
})
|
||||
|
||||
t.Run("Logs", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, srv.URL+"/debug/logs", nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
res, err := srv.Client().Do(req)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusOK, res.StatusCode)
|
||||
defer res.Body.Close()
|
||||
resBody, err := io.ReadAll(res.Body)
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, string(resBody))
|
||||
require.Contains(t, string(resBody), randLogStr)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAgent_ScriptLogging(t *testing.T) {
|
||||
|
|
|
@ -36,8 +36,11 @@ func (a *agent) apiHandler() http.Handler {
|
|||
cacheDuration: cacheDuration,
|
||||
}
|
||||
r.Get("/api/v0/listening-ports", lp.handler)
|
||||
r.Get("/debug/logs", a.HandleHTTPDebugLogs)
|
||||
r.Get("/debug/magicsock", a.HandleHTTPDebugMagicsock)
|
||||
r.Get("/debug/magicsock/debug-logging/{state}", a.HandleHTTPMagicsockDebugLoggingState)
|
||||
r.Get("/debug/manifest", a.HandleHTTPDebugManifest)
|
||||
r.Get("/debug/token", a.HandleHTTPDebugToken)
|
||||
|
||||
return r
|
||||
}
|
||||
|
|
|
@ -372,6 +372,39 @@ func (c *WorkspaceAgentConn) DebugMagicsock(ctx context.Context) ([]byte, error)
|
|||
return bs, nil
|
||||
}
|
||||
|
||||
// DebugManifest returns the agent's in-memory manifest. Unfortunately this must
|
||||
// be returns as a []byte to avoid an import cycle.
|
||||
func (c *WorkspaceAgentConn) DebugManifest(ctx context.Context) ([]byte, error) {
|
||||
ctx, span := tracing.StartSpan(ctx)
|
||||
defer span.End()
|
||||
res, err := c.apiRequest(ctx, http.MethodGet, "/debug/manifest", nil)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("do request: %w", err)
|
||||
}
|
||||
defer res.Body.Close()
|
||||
bs, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("read response body: %w", err)
|
||||
}
|
||||
return bs, nil
|
||||
}
|
||||
|
||||
// DebugLogs returns up to the last 10MB of `/tmp/coder-agent.log`
|
||||
func (c *WorkspaceAgentConn) DebugLogs(ctx context.Context) ([]byte, error) {
|
||||
ctx, span := tracing.StartSpan(ctx)
|
||||
defer span.End()
|
||||
res, err := c.apiRequest(ctx, http.MethodGet, "/debug/logs", nil)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("do request: %w", err)
|
||||
}
|
||||
defer res.Body.Close()
|
||||
bs, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("read response body: %w", err)
|
||||
}
|
||||
return bs, nil
|
||||
}
|
||||
|
||||
// apiRequest makes a request to the workspace agent's HTTP API server.
|
||||
func (c *WorkspaceAgentConn) apiRequest(ctx context.Context, method, path string, body io.Reader) (*http.Response, error) {
|
||||
ctx, span := tracing.StartSpan(ctx)
|
||||
|
|
Loading…
Reference in New Issue