@@ -2,12 +2,16 @@ package cmd
22
33import (
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+
1115var 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+
282342var 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+
305440func 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