Skip to content

Commit dda20f7

Browse files
Logs in CLI
1 parent 570f52d commit dda20f7

2 files changed

Lines changed: 144 additions & 1 deletion

File tree

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,12 +84,15 @@ cipi-cli apps edit <name> [flags] Edit an application
8484
cipi-cli apps delete <name> [-y] Delete an application
8585
cipi-cli apps suspend <name> Suspend an application (HTTP 503)
8686
cipi-cli apps unsuspend <name> Bring a suspended application back online
87+
cipi-cli apps logs <name> [flags] Read application logs
8788
```
8889

8990
**Create flags:** `--user`, `--domain`, `--php`, `--repository`, `--branch`, `--custom`, `--docroot`
9091

9192
**Edit flags:** `--php`, `--repository`, `--branch`, `--domain` (rename primary domain; requires Cipi 4.6.2+ / API 1.9.0+)
9293

94+
**Logs flags:** `--type` (default `all`), `--page` (default `1`), `--per-page` (default `50`, max `1000`; requires API 1.11.9+)
95+
9396
### Domains
9497

9598
```
@@ -214,6 +217,7 @@ See the [Cipi API documentation](https://cipi.sh/docs/advanced#cipi-api) for det
214217
| --- | --- | --- |
215218
| Suspend / unsuspend | 4.5.8 | 1.8.1 |
216219
| Rename primary domain (`apps edit --domain`) | 4.6.2 | 1.9.0 |
220+
| App logs (`apps logs`) || 1.11.9 |
217221
| Global domain map (`domains`) | 4.5.5 | — (built from `/api/apps`) |
218222

219223
New app PHP versions must be **8.3**, **8.4**, or **8.5** (Cipi 4.5.4+).

cmd/apps.go

Lines changed: 140 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,16 @@ package cmd
22

33
import (
44
"fmt"
5+
"net/url"
6+
"strings"
57

68
"github.com/cipi-sh/cli/internal/api"
79
"github.com/cipi-sh/cli/internal/output"
810
"github.com/spf13/cobra"
911
)
1012

13+
var appLogTypes = []string{"all", "nginx", "php", "worker", "deploy", "laravel"}
14+
1115
var appsCmd = &cobra.Command{
1216
Use: "apps",
1317
Aliases: []string{"app"},
@@ -279,6 +283,62 @@ var appsSuspendCmd = &cobra.Command{
279283
},
280284
}
281285

286+
var appsLogsCmd = &cobra.Command{
287+
Use: "logs [name]",
288+
Short: "Read application logs",
289+
Long: "Read paginated log snapshots (nginx, PHP-FPM, Laravel, worker, deploy). Page 1 returns the most recent lines.",
290+
Args: cobra.ExactArgs(1),
291+
RunE: func(cmd *cobra.Command, args []string) error {
292+
logType, _ := cmd.Flags().GetString("type")
293+
page, _ := cmd.Flags().GetInt("page")
294+
perPage, _ := cmd.Flags().GetInt("per-page")
295+
296+
logType = strings.ToLower(strings.TrimSpace(logType))
297+
if !isAppLogType(logType) {
298+
output.Error("Invalid log type %q — use one of: %s", logType, strings.Join(appLogTypes, ", "))
299+
return fmt.Errorf("invalid log type")
300+
}
301+
if page < 1 {
302+
output.Error("Page must be at least 1")
303+
return fmt.Errorf("invalid page")
304+
}
305+
if perPage < 1 || perPage > 1000 {
306+
output.Error("per-page must be between 1 and 1000")
307+
return fmt.Errorf("invalid per-page")
308+
}
309+
310+
client, err := api.NewClient()
311+
if err != nil {
312+
output.Error("%s", err)
313+
return err
314+
}
315+
316+
query := url.Values{}
317+
query.Set("type", logType)
318+
query.Set("page", fmt.Sprintf("%d", page))
319+
query.Set("per_page", fmt.Sprintf("%d", perPage))
320+
321+
path := fmt.Sprintf("/api/apps/%s/logs?%s", url.PathEscape(args[0]), query.Encode())
322+
323+
var result struct {
324+
Data map[string]interface{} `json:"data"`
325+
}
326+
327+
if err := client.Get(path, &result); err != nil {
328+
output.Error("Failed to read logs: %s", err)
329+
return err
330+
}
331+
332+
if jsonFlag {
333+
output.PrintJSON(result)
334+
return nil
335+
}
336+
337+
printAppLogs(result.Data)
338+
return nil
339+
},
340+
}
341+
282342
var appsUnsuspendCmd = &cobra.Command{
283343
Use: "unsuspend [name]",
284344
Short: "Unsuspend an application (bring it back online)",
@@ -302,6 +362,81 @@ var appsUnsuspendCmd = &cobra.Command{
302362
},
303363
}
304364

365+
func isAppLogType(t string) bool {
366+
for _, v := range appLogTypes {
367+
if t == v {
368+
return true
369+
}
370+
}
371+
return false
372+
}
373+
374+
func printAppLogs(data map[string]interface{}) {
375+
if len(data) == 0 {
376+
output.Warn("No log data returned")
377+
return
378+
}
379+
380+
app := str(data, "app")
381+
logType := str(data, "type")
382+
page := str(data, "page")
383+
perPage := str(data, "per_page")
384+
385+
output.Header(fmt.Sprintf("App logs: %s", app))
386+
output.KeyValue(nil, "Type", logType)
387+
output.KeyValue(nil, "Page", page)
388+
output.KeyValue(nil, "Per page", perPage)
389+
390+
if types, ok := data["available_types"].([]interface{}); ok && len(types) > 0 {
391+
parts := make([]string, 0, len(types))
392+
for _, t := range types {
393+
parts = append(parts, fmt.Sprintf("%v", t))
394+
}
395+
output.KeyValue(nil, "Available", strings.Join(parts, ", "))
396+
}
397+
398+
if warnings, ok := data["warnings"].([]interface{}); ok {
399+
for _, w := range warnings {
400+
output.Warn("%v", w)
401+
}
402+
}
403+
404+
files, _ := data["files"].([]interface{})
405+
if len(files) == 0 {
406+
output.Warn("No log files matched this filter")
407+
fmt.Println()
408+
return
409+
}
410+
411+
for _, item := range files {
412+
file, ok := item.(map[string]interface{})
413+
if !ok {
414+
continue
415+
}
416+
417+
path := str(file, "path")
418+
filePage := str(file, "page")
419+
totalPages := str(file, "total_pages")
420+
totalLines := str(file, "total_lines")
421+
422+
fmt.Println()
423+
output.Dim.Printf(" %s\n", path)
424+
output.Dim.Printf(" page %s/%s (%s lines total)\n", filePage, totalPages, totalLines)
425+
output.Dim.Println(" " + strings.Repeat("─", 60))
426+
427+
lines, _ := file["lines"].([]interface{})
428+
if len(lines) == 0 {
429+
output.Dim.Println(" (empty)")
430+
continue
431+
}
432+
for _, line := range lines {
433+
fmt.Printf(" %v\n", line)
434+
}
435+
}
436+
437+
fmt.Println()
438+
}
439+
305440
func str(m map[string]interface{}, key string) string {
306441
if v, ok := m[key]; ok && v != nil {
307442
switch val := v.(type) {
@@ -347,6 +482,10 @@ func init() {
347482

348483
appsDeleteCmd.Flags().BoolP("yes", "y", false, "Skip confirmation")
349484

350-
appsCmd.AddCommand(appsListCmd, appsShowCmd, appsCreateCmd, appsEditCmd, appsDeleteCmd, appsSuspendCmd, appsUnsuspendCmd)
485+
appsLogsCmd.Flags().StringP("type", "t", "all", "Log type (all, nginx, php, worker, deploy, laravel)")
486+
appsLogsCmd.Flags().IntP("page", "p", 1, "Page number (1 = most recent lines)")
487+
appsLogsCmd.Flags().Int("per-page", 50, "Lines per log file per page (max 1000)")
488+
489+
appsCmd.AddCommand(appsListCmd, appsShowCmd, appsCreateCmd, appsEditCmd, appsDeleteCmd, appsLogsCmd, appsSuspendCmd, appsUnsuspendCmd)
351490
rootCmd.AddCommand(appsCmd)
352491
}

0 commit comments

Comments
 (0)