Skip to content

Commit f6606ac

Browse files
[APP-6612] Support downloading logs (viamrobotics#4660)
1 parent 8b23150 commit f6606ac

File tree

2 files changed

+135
-20
lines changed

2 files changed

+135
-20
lines changed

cli/app.go

+13-3
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,11 @@ const (
2323
// TODO: RSDK-6683.
2424
quietFlag = "quiet"
2525

26-
logsFlagErrors = "errors"
27-
logsFlagTail = "tail"
28-
logsFlagCount = "count"
26+
logsFlagFormat = "format"
27+
logsFlagOutputFile = "output"
28+
logsFlagErrors = "errors"
29+
logsFlagTail = "tail"
30+
logsFlagCount = "count"
2931

3032
runFlagData = "data"
3133
runFlagStream = "stream"
@@ -1587,6 +1589,14 @@ var app = &cli.App{
15871589
Required: true,
15881590
},
15891591
},
1592+
&cli.StringFlag{
1593+
Name: logsFlagOutputFile,
1594+
Usage: "path to output file",
1595+
},
1596+
&cli.StringFlag{
1597+
Name: logsFlagFormat,
1598+
Usage: "file format (text or json)",
1599+
},
15901600
&cli.BoolFlag{
15911601
Name: logsFlagErrors,
15921602
Usage: "show only errors",

cli/client.go

+122-17
Original file line numberDiff line numberDiff line change
@@ -609,6 +609,8 @@ type robotsLogsArgs struct {
609609
Organization string
610610
Location string
611611
Machine string
612+
Output string
613+
Format string
612614
Errors bool
613615
Count int
614616
}
@@ -633,35 +635,138 @@ func RobotsLogsAction(c *cli.Context, args robotsLogsArgs) error {
633635
return errors.Wrap(err, "could not get machine parts")
634636
}
635637

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 {
636658
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+
}
639673
}
640674

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)
646678
}
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+
})
648698
if err != nil {
649-
return err
699+
return errors.Wrap(err, "failed to fetch logs")
650700
}
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+
}
659725
}
726+
727+
logsFetched += len(resp.Logs)
660728
}
661729

662730
return nil
663731
}
664732

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+
665770
type robotsPartStatusArgs struct {
666771
Organization string
667772
Location string

0 commit comments

Comments
 (0)