@@ -609,6 +609,8 @@ type robotsLogsArgs struct {
609
609
Organization string
610
610
Location string
611
611
Machine string
612
+ Output string
613
+ Format string
612
614
Errors bool
613
615
Count int
614
616
}
@@ -633,35 +635,138 @@ func RobotsLogsAction(c *cli.Context, args robotsLogsArgs) error {
633
635
return errors .Wrap (err , "could not get machine parts" )
634
636
}
635
637
638
+ // Determine the output destination
639
+ var writer io.Writer
640
+ if args .Output != "" {
641
+ file , err := os .OpenFile (args .Output , os .O_CREATE | os .O_WRONLY | os .O_TRUNC , 0o600 )
642
+ if err != nil {
643
+ return errors .Wrap (err , "could not open file for writing" )
644
+ }
645
+ //nolint:errcheck
646
+ defer file .Close ()
647
+ writer = file
648
+ } else {
649
+ // Output to console
650
+ writer = c .App .Writer
651
+ }
652
+
653
+ return client .fetchAndSaveLogs (robot , parts , args , writer )
654
+ }
655
+
656
+ // fetchLogs fetches logs for all parts and writes them to the provided writer.
657
+ func (c * viamClient ) fetchAndSaveLogs (robot * apppb.Robot , parts []* apppb.RobotPart , args robotsLogsArgs , writer io.Writer ) error {
636
658
for i , part := range parts {
637
- if i != 0 {
638
- printf (c .App .Writer , "" )
659
+ // Write a header for text format
660
+ if args .Format == "text" || args .Format == "" {
661
+ // Add robot information as a header for context
662
+ if i == 0 {
663
+ header := fmt .Sprintf ("Robot: %s -> Location: %s -> Organization: %s -> Machine: %s\n " ,
664
+ robot .Name , args .Location , args .Organization , args .Machine )
665
+ if _ , err := fmt .Fprintln (writer , header ); err != nil {
666
+ return errors .Wrap (err , "failed to write robot header" )
667
+ }
668
+ }
669
+
670
+ if _ , err := fmt .Fprintf (writer , "===== Logs for Part: %s =====\n " , part .Name ); err != nil {
671
+ return errors .Wrap (err , "failed to write header to writer" )
672
+ }
639
673
}
640
674
641
- var header string
642
- if orgStr == "" || locStr == "" || robotStr == "" {
643
- header = fmt .Sprintf ("%s -> %s -> %s -> %s" , client .selectedOrg .Name , client .selectedLoc .Name , robot .Name , part .Name )
644
- } else {
645
- header = part .Name
675
+ // Stream logs for the part
676
+ if err := c .streamLogsForPart (part , args , writer ); err != nil {
677
+ return errors .Wrapf (err , "could not stream logs for part %s" , part .Name )
646
678
}
647
- numLogs , err := getNumLogs (c , args .Count )
679
+ }
680
+ return nil
681
+ }
682
+
683
+ // streamLogsForPart streams logs for a specific part directly to a file.
684
+ func (c * viamClient ) streamLogsForPart (part * apppb.RobotPart , args robotsLogsArgs , writer io.Writer ) error {
685
+ numLogs , err := getNumLogs (c .c , args .Count )
686
+ if err != nil {
687
+ return err
688
+ }
689
+
690
+ // Write logs for this part
691
+ var pageToken string
692
+ for logsFetched := 0 ; logsFetched < numLogs ; {
693
+ resp , err := c .client .GetRobotPartLogs (c .c .Context , & apppb.GetRobotPartLogsRequest {
694
+ Id : part .Id ,
695
+ ErrorsOnly : args .Errors ,
696
+ PageToken : & pageToken ,
697
+ })
648
698
if err != nil {
649
- return err
699
+ return errors . Wrap ( err , "failed to fetch logs" )
650
700
}
651
- if err := client .printRobotPartLogs (
652
- orgStr , locStr , robotStr , part .Id ,
653
- args .Errors ,
654
- "\t " ,
655
- header ,
656
- numLogs ,
657
- ); err != nil {
658
- return errors .Wrap (err , "could not print machine logs" )
701
+
702
+ pageToken = resp .NextPageToken
703
+ // Break in the event of no logs in GetRobotPartLogsResponse or when
704
+ // page token is empty (no more pages).
705
+ if resp .Logs == nil || pageToken == "" {
706
+ break
707
+ }
708
+
709
+ // Truncate this intermediate slice of resp.Logs based on how many logs
710
+ // are still required by numLogs.
711
+ remainingLogsNeeded := numLogs - logsFetched
712
+ if remainingLogsNeeded < len (resp .Logs ) {
713
+ resp .Logs = resp .Logs [:remainingLogsNeeded ]
714
+ }
715
+
716
+ for _ , log := range resp .Logs {
717
+ formattedLog , err := formatLog (log , part .Name , args .Format )
718
+ if err != nil {
719
+ return errors .Wrap (err , "failed to format log" )
720
+ }
721
+
722
+ if _ , err := fmt .Fprintln (writer , formattedLog ); err != nil {
723
+ return errors .Wrap (err , "failed to write log to writer" )
724
+ }
659
725
}
726
+
727
+ logsFetched += len (resp .Logs )
660
728
}
661
729
662
730
return nil
663
731
}
664
732
733
+ // formatLog formats a single log entry based on the specified format.
734
+ func formatLog (log * commonpb.LogEntry , partName , format string ) (string , error ) {
735
+ fieldsString , err := logEntryFieldsToString (log .Fields )
736
+ if err != nil {
737
+ fieldsString = fmt .Sprintf ("error formatting fields: %v" , err )
738
+ }
739
+
740
+ switch format {
741
+ case "json" :
742
+ logMap := map [string ]interface {}{
743
+ "part" : partName ,
744
+ "ts" : log .Time .AsTime ().Unix (),
745
+ "time" : log .Time .AsTime ().Format (logging .DefaultTimeFormatStr ),
746
+ "message" : log .Message ,
747
+ "level" : log .Level ,
748
+ "logger" : log .LoggerName ,
749
+ "fields" : fieldsString ,
750
+ }
751
+ logJSON , err := json .Marshal (logMap )
752
+ if err != nil {
753
+ return "" , errors .Wrap (err , "failed to marshal log to JSON" )
754
+ }
755
+ return string (logJSON ), nil
756
+ case "text" , "" :
757
+ return fmt .Sprintf (
758
+ "%s\t %s\t %s\t %s\t %s" ,
759
+ log .Time .AsTime ().Format (logging .DefaultTimeFormatStr ),
760
+ log .Level ,
761
+ log .LoggerName ,
762
+ log .Message ,
763
+ fieldsString ,
764
+ ), nil
765
+ default :
766
+ return "" , fmt .Errorf ("invalid format: %s" , format )
767
+ }
768
+ }
769
+
665
770
type robotsPartStatusArgs struct {
666
771
Organization string
667
772
Location string
0 commit comments