feat: add link for viewing raw build logs in workspace and template build jobs#21727
feat: add link for viewing raw build logs in workspace and template build jobs#21727
Conversation
4b42e92 to
5842d06
Compare
99531d0 to
66a7305
Compare
This adds a `format` query parameter to workspace build logs, workspace
agent logs, template version logs, and template version dry-run logs
endpoints. When `format=text` is specified, logs are returned as plain
text with RFC3339 timestamps instead of JSON.
Each log line is formatted as:
- Provisioner logs: `{timestamp} [{level}] [{source}] {stage}: {output}`
- Agent logs: `{timestamp} [{level}] {output}`
ANSI escape sequences in log output are preserved.
The UI now includes "View raw logs" links:
- Workspace build page (build logs and agent logs tabs)
- Template version editor (output tab)
- Template version page (stats bar)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <[email protected]>
66a7305 to
5387e4a
Compare
Documentation CheckNew Documentation Needed
RationaleThis PR adds a user-facing feature that makes build logs easier to share and consume outside the Coder UI. The PR description specifically mentions this addresses "a long-standing peeve" about not being able to easily link someone to logs. Since users will see "View raw logs" links in the UI and may want to understand how to use the API directly, this deserves documentation in the logs section. The auto-generated API reference docs were already updated, but user-facing documentation explaining the use case and workflow is missing. Automated review via Coder Tasks |
This comment was marked as outdated.
This comment was marked as outdated.
| href={`/api/v2/templateversions/${templateVersion.id}/logs?format=text`} | ||
| target="_blank" | ||
| rel="noopener noreferrer" | ||
| className="flex items-center gap-1 px-3 text-xs text-content-secondary hover:text-content-primary" |
There was a problem hiding this comment.
Someone from frontend would know better, but I think we should give this a specific class; this combo may not age well.
coderd/templateversions_test.go
Outdated
| expectedStatus: http.StatusOK, | ||
| expectedContentType: "application/json", | ||
| checkBody: func(t *testing.T, body string) { | ||
| assert.NotEmpty(t, body) // This is checked more thoroughly in the case above. |
There was a problem hiding this comment.
I meant TestTemplateVersionLogs; updated to clarify.
Documentation CheckPrevious FeedbackNot yet addressed - The suggestion from the previous doc-check review (2026-01-29) has not been implemented. Updates Needed
RationaleThe PR is fully implemented with UI links and backend functionality, but user-facing documentation explaining the feature is still missing. Users seeing "View raw logs" links in the UI will want to understand:
The auto-generated API reference docs document the parameter but don't explain the use case or workflow. Automated review via Coder Tasks |
There was a problem hiding this comment.
Pull request overview
This PR adds a text/plain log output format to several log APIs and exposes “View raw logs” links in the UI, while centralizing log line formatting in the SDK.
Changes:
- Backend: Introduces a
formatquery parameter (json/text) on workspace build, workspace agent, template version, and template dry-run log endpoints, including validation (rejecting invalid values andformat=textwithfollow), text rendering using new SDK helpers, DB→SDK mappers, and updated OpenAPI/markdown docs. - SDK/CLI: Moves log formatting into
codersdk.ProvisionerJobLog.Text()andcodersdk.WorkspaceAgentLog.Text(...), updates the CLIlogscommand to use these helpers, and adds unit tests to verify the log text format. - Frontend: Adds “View raw logs” links on the workspace build page (for builds and agents), template version page, and template editor logs tab, pointing at the new
?format=textAPI behavior.
Reviewed changes
Copilot reviewed 20 out of 20 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
site/src/pages/WorkspaceBuildPage/WorkspaceBuildPageView.tsx |
Adds a header toolbar around the logs tabs and “View raw logs” links for build logs and per-agent logs, wired to the new ?format=text endpoints. |
site/src/pages/TemplateVersionPage/TemplateVersionPageView.tsx |
Extends the stats header to include a “View raw logs” external link for the template version’s provisioner logs. |
site/src/pages/TemplateVersionEditorPage/TemplateVersionEditor.tsx |
Adds a conditional “View raw logs” link in the logs tab when build logs are available and adjusts the close button alignment accordingly. |
docs/reference/api/templates.md |
Documents the new format query parameter and its enum (json, text) for template version and template dry-run logs. |
docs/reference/api/builds.md |
Documents the format query parameter and enum for workspace build logs. |
docs/reference/api/agents.md |
Documents the format query parameter and enum for workspace agent logs. |
codersdk/workspaceagents.go |
Adds WorkspaceAgentLog.Text(agentName, sourceName) to format agent logs as RFC3339 + level + `agent[.name |
codersdk/workspaceagents_test.go |
Adds tests for ProvisionerJobLog.Text() and WorkspaceAgentLog.Text(...), including empty, multiline, and special-character cases. |
codersdk/provisionerdaemons.go |
Adds ProvisionerJobLog.Text() to format provisioner logs as RFC3339 + level + `[provisioner |
coderd/workspacebuilds.go |
Extends the workspace build logs endpoint swagger comments with format while delegating handling to the shared provisioner log helper. |
coderd/workspacebuilds_test.go |
Adds TestWorkspaceBuildLogsFormat to verify JSON vs text responses, content-type, invalid format, and rejection of format=text&follow; uses db2sdk + .Text() for expectations. |
coderd/workspaceagents.go |
Adds format handling to workspace agent logs (validation, disallowing text with follow), a text-rendering code path that uses db2sdk + .Text(...), and updates convertWorkspaceAgentLogs to use db2sdk.WorkspaceAgentLog. |
coderd/workspaceagents_test.go |
Adds TestWorkspaceAgentLogsFormat mirroring the workspace build format tests for agent logs and checking text output via db2sdk + .Text(...). |
coderd/templateversions.go |
Extends swagger comments for template version logs and dry-run logs with the format parameter and delegates actual behavior to the updated shared provisioner logs helper. |
coderd/templateversions_test.go |
Adds TestTemplateVersionLogsFormat and TestTemplateVersionDryRunLogsFormat to cover JSON/text outputs, invalid format, and format=text&follow cases for both regular and dry-run template logs, asserting error messages via codersdk.Error. |
coderd/provisionerjobs.go |
Implements format parsing/validation for provisioner job logs, disallows text with follow, and updates fetchAndWriteLogs to emit either JSON or text/plain using db2sdk + .Text() with the correct content type. |
coderd/database/db2sdk/db2sdk.go |
Adds ProvisionerJobLog and WorkspaceAgentLog converters to map DB log rows into the corresponding codersdk log types used by both JSON and text responses. |
coderd/apidoc/swagger.json |
Updates OpenAPI definitions for the affected log endpoints to include the format query parameter and enumerated values. |
coderd/apidoc/docs.go |
Regenerates the embedded swagger document template to include the new format parameter for the same endpoints. |
cli/logs.go |
Refactors log formatting out of the CLI into SDK Text() methods, updates the internal log line representation to store preformatted text, and uses .Text() for both initial and streaming logs. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| t.Parallel() | ||
| ctx := testutil.Context(t, testutil.WaitLong) | ||
|
|
||
| urlPath := fmt.Sprintf("/api/v2/workspacebuilds/%s/logs%s", r.Build.ID, tt.queryParams) | ||
|
|
||
| res, err := client.Request(ctx, http.MethodGet, urlPath, nil) | ||
| require.NoError(t, err) | ||
| defer res.Body.Close() | ||
|
|
||
| require.Equal(t, tt.expectedStatus, res.StatusCode) | ||
| if tt.expectedContentType != "" { | ||
| require.Contains(t, res.Header.Get("Content-Type"), tt.expectedContentType) | ||
| } | ||
|
|
||
| if assert.NotNil(t, tt.checkBody) { | ||
| body, err := io.ReadAll(res.Body) | ||
| require.NoError(t, err) | ||
| tt.checkBody(t, string(body)) | ||
| } | ||
| }) | ||
| } |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| t.Parallel() | ||
| ctx := testutil.Context(t, testutil.WaitLong) | ||
|
|
||
| urlPath := fmt.Sprintf("/api/v2/workspaceagents/%s/logs%s", workspaceAgent.ID, tt.queryParams) | ||
|
|
||
| res, err := client.Request(ctx, http.MethodGet, urlPath, nil) | ||
| require.NoError(t, err) | ||
| defer res.Body.Close() | ||
|
|
||
| require.Equal(t, tt.expectedStatus, res.StatusCode) | ||
| if tt.expectedContentType != "" { | ||
| require.Contains(t, res.Header.Get("Content-Type"), tt.expectedContentType) | ||
| } | ||
|
|
||
| if assert.NotNil(t, tt.checkBody) { | ||
| body, err := io.ReadAll(res.Body) | ||
| require.NoError(t, err) | ||
| tt.checkBody(string(body)) | ||
| } | ||
| }) | ||
| } |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| t.Parallel() | ||
| ctx := testutil.Context(t, testutil.WaitLong) | ||
|
|
||
| urlPath := fmt.Sprintf("/api/v2/templateversions/%s/logs%s", tv.TemplateVersion.ID, tt.queryParams) | ||
|
|
||
| res, err := client.Request(ctx, http.MethodGet, urlPath, nil) | ||
| require.NoError(t, err) | ||
| defer res.Body.Close() | ||
|
|
||
| require.Equal(t, tt.expectedStatus, res.StatusCode) | ||
| if tt.expectedContentType != "" { | ||
| require.Contains(t, res.Header.Get("Content-Type"), tt.expectedContentType) | ||
| } | ||
| if assert.NotNil(t, tt.checkBody) { | ||
| body, err := io.ReadAll(res.Body) | ||
| require.NoError(t, err) | ||
| tt.checkBody(t, string(body)) | ||
| } | ||
| }) | ||
| } |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| t.Parallel() | ||
| ctx := testutil.Context(t, testutil.WaitLong) | ||
|
|
||
| urlPath := fmt.Sprintf("/api/v2/templateversions/%s/dry-run/%s/logs%s", tv.TemplateVersion.ID, dryRunJob.ID, tt.queryParams) | ||
|
|
||
| res, err := client.Request(ctx, http.MethodGet, urlPath, nil) | ||
| require.NoError(t, err) | ||
| defer res.Body.Close() | ||
|
|
||
| require.Equal(t, tt.expectedStatus, res.StatusCode) | ||
| if tt.expectedContentType != "" { | ||
| require.Contains(t, res.Header.Get("Content-Type"), tt.expectedContentType) | ||
| } | ||
|
|
||
| if assert.NotNil(t, tt.checkBody) { | ||
| body, err := io.ReadAll(res.Body) | ||
| require.NoError(t, err) | ||
| tt.checkBody(t, string(body)) | ||
| } | ||
| }) | ||
| } |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
This addresses a long-standing peeve of mine: it's not easy to link someone to just the logs for a single provisioner job or workspace agent.
You can now add
?format=textto the following API routes:/api/v2/workspaceagents/:id/logs/api/v2/workspacebuilds/:id/logs/api/v2/templateversions/:id/logs/api/v2/templateversions/:id/dry-run/:id/logsNote:
format=textis not supported with websocket streaming (follow). Specifying both will return 400 Bad Request.In addition, added links to the raw logs on the following pages:
As a side-effect, refactored the existing log formatting in
cli/logs.goto live incodersdk.🤖 Generated with Claude Opus 4.5, reviewed by me.