feat: tag chat-originating agent logs with chat_id (#25019)

Workspace-agent logs emitted while serving chatd-driven requests were
not correlated with the originating chat, making agent logs hard to
attribute to the corresponding/originating chat.

This adds agent-side chat context middleware that parses `Coder-Chat-Id`
once, enriches agent access logs and structured handler/background logs,
and adds a chatd bridge log when chat headers are attached to an agent
connection.

Closes CODAGT-324
This commit is contained in:
Ethan
2026-05-08 13:25:30 +10:00
committed by GitHub
parent e9f0385198
commit 3a9080fff6
15 changed files with 380 additions and 95 deletions
+12 -8
View File
@@ -18,7 +18,7 @@ import (
"golang.org/x/xerrors"
"cdr.dev/slog/v3"
"github.com/coder/coder/v2/agent/agentgit"
"github.com/coder/coder/v2/agent/agentchat"
"github.com/coder/coder/v2/coderd/httpapi"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/codersdk/workspacesdk"
@@ -86,6 +86,8 @@ func (api *API) HandleReadFile(rw http.ResponseWriter, r *http.Request) {
}
func (api *API) streamFile(ctx context.Context, rw http.ResponseWriter, path string, offset, limit int64) (HTTPResponseCode, error) {
logger := api.logger.With(agentchat.Fields(ctx)...)
if !filepath.IsAbs(path) {
return http.StatusBadRequest, xerrors.Errorf("file path must be absolute: %q", path)
}
@@ -131,7 +133,7 @@ func (api *API) streamFile(ctx context.Context, rw http.ResponseWriter, path str
reader := io.NewSectionReader(f, offset, bytesToRead)
_, err = io.Copy(rw, reader)
if err != nil && !errors.Is(err, io.EOF) && ctx.Err() == nil {
api.logger.Error(ctx, "workspace agent read file", slog.Error(err))
logger.Error(ctx, "workspace agent read file", slog.Error(err))
}
return 0, nil
@@ -322,8 +324,8 @@ func (api *API) HandleWriteFile(rw http.ResponseWriter, r *http.Request) {
// Track edited path for git watch.
if api.pathStore != nil {
if chatID, ancestorIDs, ok := agentgit.ExtractChatContext(r); ok {
api.pathStore.AddPaths(append([]uuid.UUID{chatID}, ancestorIDs...), []string{path})
if chatContext, ok := agentchat.FromContext(ctx); ok {
api.pathStore.AddPaths(append([]uuid.UUID{chatContext.ID}, chatContext.AncestorIDs...), []string{path})
}
}
@@ -458,12 +460,12 @@ func (api *API) HandleEditFiles(rw http.ResponseWriter, r *http.Request) {
// Track edited paths for git watch.
if api.pathStore != nil {
if chatID, ancestorIDs, ok := agentgit.ExtractChatContext(r); ok {
if chatContext, ok := agentchat.FromContext(ctx); ok {
filePaths := make([]string, 0, len(req.Files))
for _, f := range req.Files {
filePaths = append(filePaths, f.Path)
}
api.pathStore.AddPaths(append([]uuid.UUID{chatID}, ancestorIDs...), filePaths)
api.pathStore.AddPaths(append([]uuid.UUID{chatContext.ID}, chatContext.AncestorIDs...), filePaths)
}
}
@@ -565,6 +567,8 @@ func (api *API) prepareFileEdit(path string, edits []workspacesdk.FileEdit) (int
// On failure the temp file is cleaned up and the original is
// untouched.
func (api *API) atomicWrite(ctx context.Context, path string, mode *os.FileMode, r io.Reader) (int, error) {
logger := api.logger.With(agentchat.Fields(ctx)...)
dir := filepath.Dir(path)
tmpName := filepath.Join(dir, fmt.Sprintf(".%s.tmp.%s", filepath.Base(path), uuid.New().String()[:8]))
@@ -579,7 +583,7 @@ func (api *API) atomicWrite(ctx context.Context, path string, mode *os.FileMode,
cleanup := func() {
if err := api.filesystem.Remove(tmpName); err != nil {
api.logger.Warn(ctx, "unable to clean up temp file", slog.Error(err))
logger.Warn(ctx, "unable to clean up temp file", slog.Error(err))
}
}
@@ -601,7 +605,7 @@ func (api *API) atomicWrite(ctx context.Context, path string, mode *os.FileMode,
// no window where the target has wrong permissions.
if mode != nil {
if err := api.filesystem.Chmod(tmpName, *mode); err != nil {
api.logger.Warn(ctx, "unable to set file permissions",
logger.Warn(ctx, "unable to set file permissions",
slog.F("path", path),
slog.Error(err),
)
+6 -5
View File
@@ -24,6 +24,7 @@ import (
"cdr.dev/slog/v3"
"cdr.dev/slog/v3/sloggers/slogtest"
"github.com/coder/coder/v2/agent/agentchat"
"github.com/coder/coder/v2/agent/agentfiles"
"github.com/coder/coder/v2/agent/agentgit"
"github.com/coder/coder/v2/codersdk"
@@ -1157,7 +1158,7 @@ func TestHandleWriteFile_ChatHeaders_UpdatesPathStore(t *testing.T) {
rr := httptest.NewRecorder()
r := chi.NewRouter()
r.Post("/write-file", api.HandleWriteFile)
r.ServeHTTP(rr, req)
agentchat.Middleware(r).ServeHTTP(rr, req)
require.Equal(t, http.StatusOK, rr.Code)
@@ -1185,7 +1186,7 @@ func TestHandleWriteFile_NoChatHeaders_NoPathStoreUpdate(t *testing.T) {
rr := httptest.NewRecorder()
r := chi.NewRouter()
r.Post("/write-file", api.HandleWriteFile)
r.ServeHTTP(rr, req)
agentchat.Middleware(r).ServeHTTP(rr, req)
require.Equal(t, http.StatusOK, rr.Code)
@@ -1211,7 +1212,7 @@ func TestHandleWriteFile_Failure_NoPathStoreUpdate(t *testing.T) {
rr := httptest.NewRecorder()
r := chi.NewRouter()
r.Post("/write-file", api.HandleWriteFile)
r.ServeHTTP(rr, req)
agentchat.Middleware(r).ServeHTTP(rr, req)
require.Equal(t, http.StatusBadRequest, rr.Code)
@@ -1252,7 +1253,7 @@ func TestHandleEditFiles_ChatHeaders_UpdatesPathStore(t *testing.T) {
rr := httptest.NewRecorder()
r := chi.NewRouter()
r.Post("/edit-files", api.HandleEditFiles)
r.ServeHTTP(rr, req)
agentchat.Middleware(r).ServeHTTP(rr, req)
require.Equal(t, http.StatusOK, rr.Code)
@@ -1289,7 +1290,7 @@ func TestHandleEditFiles_Failure_NoPathStoreUpdate(t *testing.T) {
rr := httptest.NewRecorder()
r := chi.NewRouter()
r.Post("/edit-files", api.HandleEditFiles)
r.ServeHTTP(rr, req)
agentchat.Middleware(r).ServeHTTP(rr, req)
require.NotEqual(t, http.StatusOK, rr.Code)