Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@ Commands can use `server` or `servers` interchangeably.
- `-n, --lines <n>` - Number of log lines to retrieve (default: 100)
- `--show-timestamps` - Include timestamps in logs
- `--service <name>` - Docker Compose service name (select one container in multi-service compose apps)
- `--pull-request-id <id>` - Read the logs of that pull request's preview deployment
- `coolify app move <uuid> --environment-uuid <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 <service>=<url>[,<url>]` flags for Docker Compose routing. On update, the supplied entries replace the existing mapping.
Expand Down Expand Up @@ -552,6 +553,9 @@ coolify app logs <uuid>
# View logs for one service in a Docker Compose application
coolify app logs <uuid> --service web

# View logs of the preview deployment for pull request 42
coolify app logs <uuid> --pull-request-id 42

# Environment variables
coolify app env list <uuid>
coolify app env create <uuid> --key API_KEY --value secret123
Expand Down
1 change: 1 addition & 0 deletions cmd/application/application_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"))

Expand Down
16 changes: 12 additions & 4 deletions cmd/application/logs.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,10 @@ func NewLogsCommand() *cobra.Command {

For Docker Compose applications with multiple services, pass --service <name>
(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 <id> to read the logs of that pull request's preview
deployment instead of the main deployment.`,
Args: cli.ExactArgs(1, "<uuid>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
Expand All @@ -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)
}
Expand All @@ -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)
}
Expand All @@ -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
}
Expand All @@ -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
}
6 changes: 5 additions & 1 deletion internal/service/application.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
27 changes: 22 additions & 5 deletions internal/service/application_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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)
}
Expand All @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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")
Expand Down
5 changes: 5 additions & 0 deletions llms-full.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down