From efb6d2f53a067f1fea07c357c01f2aa0f58894d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yannis=20G=C3=BCdel?= Date: Mon, 21 Sep 2026 11:36:51 +0000 Subject: [PATCH] feat(application): read preview deployment logs with --pull-request-id `coolify app logs` could only read the main deployment's container. Coolify exposes preview runtime logs at /applications/{uuid}/previews/{pull_request_id}/logs, so add a --pull-request-id flag (same name as on `coolify deploy`) that switches to that endpoint. --lines, --show-timestamps, --service and --follow work unchanged. --- README.md | 4 ++++ cmd/application/application_test.go | 1 + cmd/application/logs.go | 16 ++++++++++++---- internal/service/application.go | 6 +++++- internal/service/application_test.go | 27 ++++++++++++++++++++++----- llms-full.txt | 5 +++++ 6 files changed, 49 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 039fe98..5faf7be 100644 --- a/README.md +++ b/README.md @@ -174,6 +174,7 @@ Commands can use `server` or `servers` interchangeably. - `-n, --lines ` - Number of log lines to retrieve (default: 100) - `--show-timestamps` - Include timestamps in logs - `--service ` - Docker Compose service name (select one container in multi-service compose apps) + - `--pull-request-id ` - Read the logs of that pull request's preview deployment - `coolify app move --environment-uuid ` - Move an application to another environment - `coolify app tag list|add|remove` - Manage application tags - Git-backed application create variants (`public`, `github`, and `deploy-key`) and `app update` support repeatable `--compose-domain =[,]` flags for Docker Compose routing. On update, the supplied entries replace the existing mapping. @@ -552,6 +553,9 @@ coolify app logs # View logs for one service in a Docker Compose application coolify app logs --service web +# View logs of the preview deployment for pull request 42 +coolify app logs --pull-request-id 42 + # Environment variables coolify app env list coolify app env create --key API_KEY --value secret123 diff --git a/cmd/application/application_test.go b/cmd/application/application_test.go index 051d680..d94011b 100644 --- a/cmd/application/application_test.go +++ b/cmd/application/application_test.go @@ -29,6 +29,7 @@ func TestNewAppCommand_RegistersMoveAndTagCommands(t *testing.T) { func TestNewApplicationCommands_ExposeParityFlags(t *testing.T) { assert.NotNil(t, NewLogsCommand().Flags().Lookup("show-timestamps")) assert.NotNil(t, NewLogsCommand().Flags().Lookup("service")) + assert.NotNil(t, NewLogsCommand().Flags().Lookup("pull-request-id")) assert.NotNil(t, NewMoveCommand().Flags().Lookup("environment-uuid")) assert.NotNil(t, NewUpdateCommand().Flags().Lookup("compose-domain")) diff --git a/cmd/application/logs.go b/cmd/application/logs.go index 20e2edf..6c3754a 100644 --- a/cmd/application/logs.go +++ b/cmd/application/logs.go @@ -22,7 +22,10 @@ func NewLogsCommand() *cobra.Command { For Docker Compose applications with multiple services, pass --service (the compose service key, e.g. web or db) to select which container's logs to return. -Without --service, the API returns logs from the first running container.`, +Without --service, the API returns logs from the first running container. + +Pass --pull-request-id to read the logs of that pull request's preview +deployment instead of the main deployment.`, Args: cli.ExactArgs(1, ""), RunE: func(cmd *cobra.Command, args []string) error { ctx := cmd.Context() @@ -37,10 +40,14 @@ Without --service, the API returns logs from the first running container.`, follow, _ := cmd.Flags().GetBool("follow") showTimestamps, _ := cmd.Flags().GetBool("show-timestamps") serviceName, _ := cmd.Flags().GetString("service") + pullRequestID, _ := cmd.Flags().GetInt("pull-request-id") + if pullRequestID < 0 { + return fmt.Errorf("--pull-request-id cannot be negative") + } appSvc := service.NewApplicationService(client) if !follow { - resp, err := appSvc.Logs(ctx, uuid, lines, showTimestamps, serviceName) + resp, err := appSvc.Logs(ctx, uuid, lines, showTimestamps, serviceName, pullRequestID) if err != nil { return fmt.Errorf("failed to get logs: %w", err) } @@ -56,7 +63,7 @@ Without --service, the API returns logs from the first running container.`, lastLogs := "" - resp, err := appSvc.Logs(ctx, uuid, lines, showTimestamps, serviceName) + resp, err := appSvc.Logs(ctx, uuid, lines, showTimestamps, serviceName, pullRequestID) if err != nil { return fmt.Errorf("failed to get logs: %w", err) } @@ -69,7 +76,7 @@ Without --service, the API returns logs from the first running container.`, fmt.Println("\nStopping log follow...") return nil case <-ticker.C: - resp, err := appSvc.Logs(ctx, uuid, lines, showTimestamps, serviceName) + resp, err := appSvc.Logs(ctx, uuid, lines, showTimestamps, serviceName, pullRequestID) if err != nil { continue } @@ -90,5 +97,6 @@ Without --service, the API returns logs from the first running container.`, cmd.Flags().BoolP("follow", "f", false, "Follow log output (like tail -f)") cmd.Flags().Bool("show-timestamps", false, "Show timestamps in log output") cmd.Flags().String("service", "", "Docker Compose service name (selects one container in multi-service apps)") + cmd.Flags().Int("pull-request-id", 0, "Pull request ID to read the logs of its preview deployment") return cmd } diff --git a/internal/service/application.go b/internal/service/application.go index 2795674..d598004 100644 --- a/internal/service/application.go +++ b/internal/service/application.go @@ -119,8 +119,12 @@ func (s *ApplicationService) Restart(ctx context.Context, uuid string) (*models. // Logs retrieves logs for an application. // serviceName selects a docker-compose service container (API query service_name). // Empty serviceName keeps legacy behavior (first/only container). -func (s *ApplicationService) Logs(ctx context.Context, uuid string, lines int, showTimestamps bool, serviceName string) (*models.ApplicationLogsResponse, error) { +// pullRequestID > 0 reads the preview deployment of that pull request instead. +func (s *ApplicationService) Logs(ctx context.Context, uuid string, lines int, showTimestamps bool, serviceName string, pullRequestID int) (*models.ApplicationLogsResponse, error) { endpoint := fmt.Sprintf("applications/%s/logs", uuid) + if pullRequestID > 0 { + endpoint = fmt.Sprintf("applications/%s/previews/%d/logs", uuid, pullRequestID) + } query := url.Values{} if lines > 0 { query.Set("lines", strconv.Itoa(lines)) diff --git a/internal/service/application_test.go b/internal/service/application_test.go index 0ad7279..6de6699 100644 --- a/internal/service/application_test.go +++ b/internal/service/application_test.go @@ -630,7 +630,7 @@ func TestApplicationService_Logs(t *testing.T) { client := api.NewClient(server.URL, "test-token") svc := NewApplicationService(client) - result, err := svc.Logs(context.Background(), "app-uuid-123", 0, false, "") + result, err := svc.Logs(context.Background(), "app-uuid-123", 0, false, "", 0) require.NoError(t, err) assert.NotNil(t, result) assert.Contains(t, result.Logs, "Application started") @@ -653,7 +653,7 @@ func TestApplicationService_Logs_WithLines(t *testing.T) { client := api.NewClient(server.URL, "test-token") svc := NewApplicationService(client) - result, err := svc.Logs(context.Background(), "app-uuid-123", 50, false, "") + result, err := svc.Logs(context.Background(), "app-uuid-123", 50, false, "", 0) require.NoError(t, err) assert.NotNil(t, result) } @@ -668,7 +668,7 @@ func TestApplicationService_Logs_WithLinesAndTimestamps(t *testing.T) { defer server.Close() client := api.NewClient(server.URL, "test-token") - result, err := NewApplicationService(client).Logs(context.Background(), "app-uuid-123", 25, true, "") + result, err := NewApplicationService(client).Logs(context.Background(), "app-uuid-123", 25, true, "", 0) require.NoError(t, err) assert.Equal(t, "timestamped", result.Logs) @@ -685,12 +685,29 @@ func TestApplicationService_Logs_WithServiceName(t *testing.T) { defer server.Close() client := api.NewClient(server.URL, "test-token") - result, err := NewApplicationService(client).Logs(context.Background(), "app-uuid-123", 50, true, "web") + result, err := NewApplicationService(client).Logs(context.Background(), "app-uuid-123", 50, true, "web", 0) require.NoError(t, err) assert.Equal(t, "web service logs", result.Logs) } +func TestApplicationService_Logs_WithPullRequestID(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "/api/v1/applications/app-uuid-123/previews/42/logs", r.URL.Path) + assert.Equal(t, "web", r.URL.Query().Get("service_name")) + assert.Equal(t, "50", r.URL.Query().Get("lines")) + assert.Empty(t, r.URL.Query().Get("pull_request_id")) + _ = json.NewEncoder(w).Encode(models.ApplicationLogsResponse{Logs: "preview logs"}) + })) + defer server.Close() + + client := api.NewClient(server.URL, "test-token") + result, err := NewApplicationService(client).Logs(context.Background(), "app-uuid-123", 50, false, "web", 42) + + require.NoError(t, err) + assert.Equal(t, "preview logs", result.Logs) +} + func TestApplicationService_Move(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, "/api/v1/applications/app-uuid-123/move", r.URL.Path) @@ -727,7 +744,7 @@ func TestApplicationService_Logs_Error(t *testing.T) { client := api.NewClient(server.URL, "test-token") svc := NewApplicationService(client) - result, err := svc.Logs(context.Background(), "app-uuid-123", 0, false, "") + result, err := svc.Logs(context.Background(), "app-uuid-123", 0, false, "", 0) require.Error(t, err) assert.Nil(t, result) assert.Contains(t, err.Error(), "failed to get logs for application") diff --git a/llms-full.txt b/llms-full.txt index 0d34f48..278aec1 100644 --- a/llms-full.txt +++ b/llms-full.txt @@ -1558,6 +1558,11 @@ Parameters: description: Number of log lines to retrieve required: false default: 100 + - name: --pull-request-id + type: integer + description: Pull request ID to read the logs of its preview deployment + required: false + default: 0 - name: --service type: string description: Docker Compose service name (selects one container in multi-service apps)