| package main |
| |
| import ( |
| "crypto/x509" |
| "errors" |
| "fmt" |
| "io" |
| |
| "github.com/spf13/cobra" |
| |
| "source.monogon.dev/metropolis/cli/metroctl/core" |
| "source.monogon.dev/metropolis/pkg/logtree" |
| "source.monogon.dev/metropolis/proto/api" |
| |
| cpb "source.monogon.dev/metropolis/proto/common" |
| ) |
| |
| type metroctlLogFlags struct { |
| // follow (ie. stream) logs live. |
| follow bool |
| // dn to query. |
| dn string |
| // exact dn query, i.e. without children/recursion. |
| exact bool |
| // concise logging output format. |
| concise bool |
| // backlog: >0 for a concrete limit, -1 for all, 0 for none |
| backlog int |
| } |
| |
| var logFlags metroctlLogFlags |
| |
| var nodeLogsCmd = &cobra.Command{ |
| Short: "Get/stream logs from node", |
| Long: `Get or stream logs from node. |
| |
| Node logs are structured in a 'log tree' structure, in which different subsystems |
| log to DNs (distinguished names). For example, service 'foo' might log to |
| root.role.foo, while service 'bar' might log to root.role.bar. |
| |
| To set the DN you want to request logs from, use --dn. The default is to return |
| all logs. The default output is also also a good starting point to figure out |
| what DNs are active in the system. |
| |
| When requesting logs for a DN by default all sub-DNs will also be returned (ie. |
| with the above example, when requesting DN 'root.role' logs at root.role.foo and |
| root.role.bar would also be returned). This behaviour can be disabled by setting |
| --exact. |
| |
| To stream logs, use --follow. |
| |
| By default, all available logs are returned. To limit the number of historical |
| log lines (a.k.a. 'backlog') to return, set --backlog. This similar to requesting |
| all lines and then piping the result through 'tail' - but more efficient, as no |
| unnecessary lines are fetched. |
| `, |
| Use: "logs [node-id]", |
| Args: cobra.MinimumNArgs(1), |
| RunE: func(cmd *cobra.Command, args []string) error { |
| ctx := cmd.Context() |
| |
| // First connect to the main management service and figure out the node's IP |
| // address. |
| cc := dialAuthenticated(ctx) |
| mgmt := api.NewManagementClient(cc) |
| nodes, err := core.GetNodes(ctx, mgmt, fmt.Sprintf("node.id == %q", args[0])) |
| if err != nil { |
| return fmt.Errorf("when getting node info: %w", err) |
| } |
| |
| if len(nodes) == 0 { |
| return fmt.Errorf("no such node") |
| } |
| if len(nodes) > 1 { |
| return fmt.Errorf("expression matched more than one node") |
| } |
| n := nodes[0] |
| if n.Status == nil || n.Status.ExternalAddress == "" { |
| return fmt.Errorf("node has no external address") |
| } |
| |
| // TODO(q3k): save CA certificate on takeover |
| info, err := mgmt.GetClusterInfo(ctx, &api.GetClusterInfoRequest{}) |
| if err != nil { |
| return fmt.Errorf("couldn't get cluster info: %w", err) |
| } |
| cacert, err := x509.ParseCertificate(info.CaCertificate) |
| if err != nil { |
| return fmt.Errorf("remote CA certificate invalid: %w", err) |
| } |
| |
| fmt.Printf("=== Logs from %s (%s):\n", n.Id, n.Status.ExternalAddress) |
| // Dial the actual node at its management port. |
| cl := dialAuthenticatedNode(ctx, n.Id, n.Status.ExternalAddress, cacert) |
| nmgmt := api.NewNodeManagementClient(cl) |
| |
| streamMode := api.GetLogsRequest_STREAM_DISABLE |
| if logFlags.follow { |
| streamMode = api.GetLogsRequest_STREAM_UNBUFFERED |
| } |
| var filters []*cpb.LogFilter |
| if !logFlags.exact { |
| filters = append(filters, &cpb.LogFilter{ |
| Filter: &cpb.LogFilter_WithChildren_{ |
| WithChildren: &cpb.LogFilter_WithChildren{}, |
| }, |
| }) |
| } |
| backlogMode := api.GetLogsRequest_BACKLOG_ALL |
| var backlogCount int64 |
| switch { |
| case logFlags.backlog > 0: |
| backlogMode = api.GetLogsRequest_BACKLOG_COUNT |
| backlogCount = int64(logFlags.backlog) |
| case logFlags.backlog == 0: |
| backlogMode = api.GetLogsRequest_BACKLOG_DISABLE |
| } |
| |
| srv, err := nmgmt.Logs(ctx, &api.GetLogsRequest{ |
| Dn: logFlags.dn, |
| BacklogMode: backlogMode, |
| BacklogCount: backlogCount, |
| StreamMode: streamMode, |
| Filters: filters, |
| }) |
| if err != nil { |
| return fmt.Errorf("failed to get logs: %w", err) |
| } |
| for { |
| res, err := srv.Recv() |
| if errors.Is(err, io.EOF) { |
| fmt.Println("=== Done.") |
| break |
| } |
| if err != nil { |
| return fmt.Errorf("log stream failed: %w", err) |
| } |
| for _, entry := range res.BacklogEntries { |
| printEntry(entry) |
| } |
| for _, entry := range res.StreamEntries { |
| printEntry(entry) |
| } |
| } |
| |
| return nil |
| }, |
| } |
| |
| func printEntry(e *cpb.LogEntry) { |
| entry, err := logtree.LogEntryFromProto(e) |
| if err != nil { |
| fmt.Printf("invalid stream entry: %v\n", err) |
| return |
| } |
| if logFlags.concise { |
| fmt.Println(entry.ConciseString(logtree.MetropolisShortenDict, 0)) |
| } else { |
| fmt.Println(entry.String()) |
| } |
| } |
| |
| func init() { |
| nodeLogsCmd.Flags().BoolVarP(&logFlags.follow, "follow", "f", false, "Continue streaming logs after fetching backlog.") |
| nodeLogsCmd.Flags().StringVar(&logFlags.dn, "dn", "", "Distinguished Name to get logs from (and children, if --exact is not set). If not set, defaults to '', which is the top-level DN.") |
| nodeLogsCmd.Flags().BoolVarP(&logFlags.exact, "exact", "e", false, "Only show logs for exactly the DN, do not recurse down the tree.") |
| nodeLogsCmd.Flags().BoolVarP(&logFlags.concise, "concise", "c", false, "Output concise logs.") |
| nodeLogsCmd.Flags().IntVar(&logFlags.backlog, "backlog", -1, "How many lines of historical log data to return. The default (-1) returns all available lines. Zero value means no backlog is returned (useful when using --follow).") |
| nodeCmd.AddCommand(nodeLogsCmd) |
| } |