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