| Serge Bazanski | b91938f | 2023-03-29 14:31:22 +0200 | [diff] [blame] | 1 | package main |
| 2 | |
| 3 | import ( |
| Serge Bazanski | b91938f | 2023-03-29 14:31:22 +0200 | [diff] [blame] | 4 | "errors" |
| 5 | "fmt" |
| 6 | "io" |
| 7 | |
| 8 | "github.com/spf13/cobra" |
| 9 | |
| 10 | "source.monogon.dev/metropolis/cli/metroctl/core" |
| Serge Bazanski | b91938f | 2023-03-29 14:31:22 +0200 | [diff] [blame] | 11 | "source.monogon.dev/metropolis/proto/api" |
| Serge Bazanski | da11486 | 2023-03-29 17:46:42 +0200 | [diff] [blame] | 12 | cpb "source.monogon.dev/metropolis/proto/common" |
| Tim Windelschmidt | 9f21f53 | 2024-05-07 15:14:20 +0200 | [diff] [blame] | 13 | "source.monogon.dev/osbase/logtree" |
| 14 | lpb "source.monogon.dev/osbase/logtree/proto" |
| Serge Bazanski | b91938f | 2023-03-29 14:31:22 +0200 | [diff] [blame] | 15 | ) |
| 16 | |
| Serge Bazanski | e012b72 | 2023-03-29 17:49:04 +0200 | [diff] [blame] | 17 | type metroctlLogFlags struct { |
| 18 | // follow (ie. stream) logs live. |
| 19 | follow bool |
| 20 | // dn to query. |
| 21 | dn string |
| 22 | // exact dn query, i.e. without children/recursion. |
| 23 | exact bool |
| 24 | // concise logging output format. |
| 25 | concise bool |
| 26 | // backlog: >0 for a concrete limit, -1 for all, 0 for none |
| 27 | backlog int |
| 28 | } |
| 29 | |
| 30 | var logFlags metroctlLogFlags |
| 31 | |
| Serge Bazanski | b91938f | 2023-03-29 14:31:22 +0200 | [diff] [blame] | 32 | var nodeLogsCmd = &cobra.Command{ |
| 33 | Short: "Get/stream logs from node", |
| Serge Bazanski | e012b72 | 2023-03-29 17:49:04 +0200 | [diff] [blame] | 34 | Long: `Get or stream logs from node. |
| 35 | |
| 36 | Node logs are structured in a 'log tree' structure, in which different subsystems |
| 37 | log to DNs (distinguished names). For example, service 'foo' might log to |
| 38 | root.role.foo, while service 'bar' might log to root.role.bar. |
| 39 | |
| 40 | To set the DN you want to request logs from, use --dn. The default is to return |
| 41 | all logs. The default output is also also a good starting point to figure out |
| 42 | what DNs are active in the system. |
| 43 | |
| 44 | When requesting logs for a DN by default all sub-DNs will also be returned (ie. |
| 45 | with the above example, when requesting DN 'root.role' logs at root.role.foo and |
| 46 | root.role.bar would also be returned). This behaviour can be disabled by setting |
| 47 | --exact. |
| 48 | |
| 49 | To stream logs, use --follow. |
| 50 | |
| 51 | By default, all available logs are returned. To limit the number of historical |
| 52 | log lines (a.k.a. 'backlog') to return, set --backlog. This similar to requesting |
| 53 | all lines and then piping the result through 'tail' - but more efficient, as no |
| 54 | unnecessary lines are fetched. |
| 55 | `, |
| 56 | Use: "logs [node-id]", |
| Tim Windelschmidt | fc6e1cf | 2024-09-18 17:34:07 +0200 | [diff] [blame] | 57 | Args: PrintUsageOnWrongArgs(cobra.MinimumNArgs(1)), |
| Serge Bazanski | b91938f | 2023-03-29 14:31:22 +0200 | [diff] [blame] | 58 | RunE: func(cmd *cobra.Command, args []string) error { |
| 59 | ctx := cmd.Context() |
| 60 | |
| 61 | // First connect to the main management service and figure out the node's IP |
| 62 | // address. |
| Tim Windelschmidt | 0b4fb8c | 2024-09-18 17:34:23 +0200 | [diff] [blame^] | 63 | cc, err := dialAuthenticated(ctx) |
| 64 | if err != nil { |
| 65 | return fmt.Errorf("while dialing node: %w", err) |
| 66 | } |
| Serge Bazanski | b91938f | 2023-03-29 14:31:22 +0200 | [diff] [blame] | 67 | mgmt := api.NewManagementClient(cc) |
| 68 | nodes, err := core.GetNodes(ctx, mgmt, fmt.Sprintf("node.id == %q", args[0])) |
| 69 | if err != nil { |
| 70 | return fmt.Errorf("when getting node info: %w", err) |
| 71 | } |
| 72 | |
| 73 | if len(nodes) == 0 { |
| 74 | return fmt.Errorf("no such node") |
| 75 | } |
| 76 | if len(nodes) > 1 { |
| 77 | return fmt.Errorf("expression matched more than one node") |
| 78 | } |
| 79 | n := nodes[0] |
| 80 | if n.Status == nil || n.Status.ExternalAddress == "" { |
| 81 | return fmt.Errorf("node has no external address") |
| 82 | } |
| 83 | |
| Serge Bazanski | c51d47d | 2024-02-13 18:40:26 +0100 | [diff] [blame] | 84 | cacert, err := core.GetClusterCAWithTOFU(ctx, connectOptions()) |
| Serge Bazanski | b91938f | 2023-03-29 14:31:22 +0200 | [diff] [blame] | 85 | if err != nil { |
| Serge Bazanski | c51d47d | 2024-02-13 18:40:26 +0100 | [diff] [blame] | 86 | return fmt.Errorf("could not get CA certificate: %w", err) |
| Serge Bazanski | b91938f | 2023-03-29 14:31:22 +0200 | [diff] [blame] | 87 | } |
| 88 | |
| Serge Bazanski | e012b72 | 2023-03-29 17:49:04 +0200 | [diff] [blame] | 89 | fmt.Printf("=== Logs from %s (%s):\n", n.Id, n.Status.ExternalAddress) |
| Serge Bazanski | b91938f | 2023-03-29 14:31:22 +0200 | [diff] [blame] | 90 | // Dial the actual node at its management port. |
| Tim Windelschmidt | 0b4fb8c | 2024-09-18 17:34:23 +0200 | [diff] [blame^] | 91 | cl, err := dialAuthenticatedNode(ctx, n.Id, n.Status.ExternalAddress, cacert) |
| 92 | if err != nil { |
| 93 | return fmt.Errorf("while dialing node: %w", err) |
| 94 | } |
| Serge Bazanski | b91938f | 2023-03-29 14:31:22 +0200 | [diff] [blame] | 95 | nmgmt := api.NewNodeManagementClient(cl) |
| 96 | |
| Serge Bazanski | e012b72 | 2023-03-29 17:49:04 +0200 | [diff] [blame] | 97 | streamMode := api.GetLogsRequest_STREAM_DISABLE |
| 98 | if logFlags.follow { |
| 99 | streamMode = api.GetLogsRequest_STREAM_UNBUFFERED |
| 100 | } |
| 101 | var filters []*cpb.LogFilter |
| 102 | if !logFlags.exact { |
| 103 | filters = append(filters, &cpb.LogFilter{ |
| 104 | Filter: &cpb.LogFilter_WithChildren_{ |
| 105 | WithChildren: &cpb.LogFilter_WithChildren{}, |
| Serge Bazanski | b91938f | 2023-03-29 14:31:22 +0200 | [diff] [blame] | 106 | }, |
| Serge Bazanski | e012b72 | 2023-03-29 17:49:04 +0200 | [diff] [blame] | 107 | }) |
| 108 | } |
| 109 | backlogMode := api.GetLogsRequest_BACKLOG_ALL |
| 110 | var backlogCount int64 |
| 111 | switch { |
| 112 | case logFlags.backlog > 0: |
| 113 | backlogMode = api.GetLogsRequest_BACKLOG_COUNT |
| 114 | backlogCount = int64(logFlags.backlog) |
| 115 | case logFlags.backlog == 0: |
| 116 | backlogMode = api.GetLogsRequest_BACKLOG_DISABLE |
| 117 | } |
| 118 | |
| 119 | srv, err := nmgmt.Logs(ctx, &api.GetLogsRequest{ |
| 120 | Dn: logFlags.dn, |
| 121 | BacklogMode: backlogMode, |
| 122 | BacklogCount: backlogCount, |
| 123 | StreamMode: streamMode, |
| 124 | Filters: filters, |
| Serge Bazanski | b91938f | 2023-03-29 14:31:22 +0200 | [diff] [blame] | 125 | }) |
| 126 | if err != nil { |
| 127 | return fmt.Errorf("failed to get logs: %w", err) |
| 128 | } |
| 129 | for { |
| 130 | res, err := srv.Recv() |
| 131 | if errors.Is(err, io.EOF) { |
| Serge Bazanski | e012b72 | 2023-03-29 17:49:04 +0200 | [diff] [blame] | 132 | fmt.Println("=== Done.") |
| Serge Bazanski | b91938f | 2023-03-29 14:31:22 +0200 | [diff] [blame] | 133 | break |
| 134 | } |
| 135 | if err != nil { |
| 136 | return fmt.Errorf("log stream failed: %w", err) |
| 137 | } |
| 138 | for _, entry := range res.BacklogEntries { |
| Serge Bazanski | e012b72 | 2023-03-29 17:49:04 +0200 | [diff] [blame] | 139 | printEntry(entry) |
| 140 | } |
| 141 | for _, entry := range res.StreamEntries { |
| 142 | printEntry(entry) |
| Serge Bazanski | b91938f | 2023-03-29 14:31:22 +0200 | [diff] [blame] | 143 | } |
| 144 | } |
| 145 | |
| 146 | return nil |
| 147 | }, |
| 148 | } |
| Serge Bazanski | e012b72 | 2023-03-29 17:49:04 +0200 | [diff] [blame] | 149 | |
| Tim Windelschmidt | 8814f52 | 2024-05-08 00:41:13 +0200 | [diff] [blame] | 150 | func printEntry(e *lpb.LogEntry) { |
| Serge Bazanski | e012b72 | 2023-03-29 17:49:04 +0200 | [diff] [blame] | 151 | entry, err := logtree.LogEntryFromProto(e) |
| 152 | if err != nil { |
| 153 | fmt.Printf("invalid stream entry: %v\n", err) |
| 154 | return |
| 155 | } |
| 156 | if logFlags.concise { |
| 157 | fmt.Println(entry.ConciseString(logtree.MetropolisShortenDict, 0)) |
| 158 | } else { |
| 159 | fmt.Println(entry.String()) |
| 160 | } |
| 161 | } |
| 162 | |
| 163 | func init() { |
| 164 | nodeLogsCmd.Flags().BoolVarP(&logFlags.follow, "follow", "f", false, "Continue streaming logs after fetching backlog.") |
| 165 | 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.") |
| 166 | nodeLogsCmd.Flags().BoolVarP(&logFlags.exact, "exact", "e", false, "Only show logs for exactly the DN, do not recurse down the tree.") |
| 167 | nodeLogsCmd.Flags().BoolVarP(&logFlags.concise, "concise", "c", false, "Output concise logs.") |
| 168 | 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).") |
| 169 | nodeCmd.AddCommand(nodeLogsCmd) |
| 170 | } |