blob: b06a9f77947ba0cbf93ca562ccf765425542c4d5 [file] [log] [blame]
Serge Bazanskib91938f2023-03-29 14:31:22 +02001package main
2
3import (
Serge Bazanskib91938f2023-03-29 14:31:22 +02004 "errors"
5 "fmt"
6 "io"
7
8 "github.com/spf13/cobra"
9
10 "source.monogon.dev/metropolis/cli/metroctl/core"
Serge Bazanskib91938f2023-03-29 14:31:22 +020011 "source.monogon.dev/metropolis/proto/api"
Serge Bazanskida114862023-03-29 17:46:42 +020012 cpb "source.monogon.dev/metropolis/proto/common"
Tim Windelschmidt9f21f532024-05-07 15:14:20 +020013 "source.monogon.dev/osbase/logtree"
14 lpb "source.monogon.dev/osbase/logtree/proto"
Serge Bazanskib91938f2023-03-29 14:31:22 +020015)
16
Serge Bazanskie012b722023-03-29 17:49:04 +020017type 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
30var logFlags metroctlLogFlags
31
Serge Bazanskib91938f2023-03-29 14:31:22 +020032var nodeLogsCmd = &cobra.Command{
33 Short: "Get/stream logs from node",
Serge Bazanskie012b722023-03-29 17:49:04 +020034 Long: `Get or stream logs from node.
35
36Node logs are structured in a 'log tree' structure, in which different subsystems
37log to DNs (distinguished names). For example, service 'foo' might log to
38root.role.foo, while service 'bar' might log to root.role.bar.
39
40To set the DN you want to request logs from, use --dn. The default is to return
41all logs. The default output is also also a good starting point to figure out
42what DNs are active in the system.
43
44When requesting logs for a DN by default all sub-DNs will also be returned (ie.
45with the above example, when requesting DN 'root.role' logs at root.role.foo and
46root.role.bar would also be returned). This behaviour can be disabled by setting
47--exact.
48
49To stream logs, use --follow.
50
51By default, all available logs are returned. To limit the number of historical
52log lines (a.k.a. 'backlog') to return, set --backlog. This similar to requesting
53all lines and then piping the result through 'tail' - but more efficient, as no
54unnecessary lines are fetched.
55`,
56 Use: "logs [node-id]",
Tim Windelschmidtfc6e1cf2024-09-18 17:34:07 +020057 Args: PrintUsageOnWrongArgs(cobra.MinimumNArgs(1)),
Serge Bazanskib91938f2023-03-29 14:31:22 +020058 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 Windelschmidt0b4fb8c2024-09-18 17:34:23 +020063 cc, err := dialAuthenticated(ctx)
64 if err != nil {
65 return fmt.Errorf("while dialing node: %w", err)
66 }
Serge Bazanskib91938f2023-03-29 14:31:22 +020067 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 Bazanskic51d47d2024-02-13 18:40:26 +010084 cacert, err := core.GetClusterCAWithTOFU(ctx, connectOptions())
Serge Bazanskib91938f2023-03-29 14:31:22 +020085 if err != nil {
Serge Bazanskic51d47d2024-02-13 18:40:26 +010086 return fmt.Errorf("could not get CA certificate: %w", err)
Serge Bazanskib91938f2023-03-29 14:31:22 +020087 }
88
Serge Bazanskie012b722023-03-29 17:49:04 +020089 fmt.Printf("=== Logs from %s (%s):\n", n.Id, n.Status.ExternalAddress)
Serge Bazanskib91938f2023-03-29 14:31:22 +020090 // Dial the actual node at its management port.
Tim Windelschmidt0b4fb8c2024-09-18 17:34:23 +020091 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 Bazanskib91938f2023-03-29 14:31:22 +020095 nmgmt := api.NewNodeManagementClient(cl)
96
Tim Windelschmidta10d0cb2025-01-13 14:44:15 +010097 streamMode := api.GetLogsRequest_STREAM_MODE_DISABLE
Serge Bazanskie012b722023-03-29 17:49:04 +020098 if logFlags.follow {
Tim Windelschmidta10d0cb2025-01-13 14:44:15 +010099 streamMode = api.GetLogsRequest_STREAM_MODE_UNBUFFERED
Serge Bazanskie012b722023-03-29 17:49:04 +0200100 }
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 Bazanskib91938f2023-03-29 14:31:22 +0200106 },
Serge Bazanskie012b722023-03-29 17:49:04 +0200107 })
108 }
Tim Windelschmidta10d0cb2025-01-13 14:44:15 +0100109 backlogMode := api.GetLogsRequest_BACKLOG_MODE_ALL
Serge Bazanskie012b722023-03-29 17:49:04 +0200110 var backlogCount int64
111 switch {
112 case logFlags.backlog > 0:
Tim Windelschmidta10d0cb2025-01-13 14:44:15 +0100113 backlogMode = api.GetLogsRequest_BACKLOG_MODE_COUNT
Serge Bazanskie012b722023-03-29 17:49:04 +0200114 backlogCount = int64(logFlags.backlog)
115 case logFlags.backlog == 0:
Tim Windelschmidta10d0cb2025-01-13 14:44:15 +0100116 backlogMode = api.GetLogsRequest_BACKLOG_MODE_DISABLE
Serge Bazanskie012b722023-03-29 17:49:04 +0200117 }
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 Bazanskib91938f2023-03-29 14:31:22 +0200125 })
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 Bazanskie012b722023-03-29 17:49:04 +0200132 fmt.Println("=== Done.")
Serge Bazanskib91938f2023-03-29 14:31:22 +0200133 break
134 }
135 if err != nil {
136 return fmt.Errorf("log stream failed: %w", err)
137 }
138 for _, entry := range res.BacklogEntries {
Serge Bazanskie012b722023-03-29 17:49:04 +0200139 printEntry(entry)
140 }
141 for _, entry := range res.StreamEntries {
142 printEntry(entry)
Serge Bazanskib91938f2023-03-29 14:31:22 +0200143 }
144 }
145
146 return nil
147 },
148}
Serge Bazanskie012b722023-03-29 17:49:04 +0200149
Tim Windelschmidt8814f522024-05-08 00:41:13 +0200150func printEntry(e *lpb.LogEntry) {
Serge Bazanskie012b722023-03-29 17:49:04 +0200151 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
163func 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}