blob: a04558cbdaf910aae0167eec72b4c1274b119ae1 [file] [log] [blame]
Serge Bazanskib91938f2023-03-29 14:31:22 +02001package main
2
3import (
4 "crypto/x509"
5 "errors"
6 "fmt"
7 "io"
8
9 "github.com/spf13/cobra"
10
11 "source.monogon.dev/metropolis/cli/metroctl/core"
12 "source.monogon.dev/metropolis/pkg/logtree"
13 "source.monogon.dev/metropolis/proto/api"
Serge Bazanskie012b722023-03-29 17:49:04 +020014
Serge Bazanskida114862023-03-29 17:46:42 +020015 cpb "source.monogon.dev/metropolis/proto/common"
Serge Bazanskib91938f2023-03-29 14:31:22 +020016)
17
Serge Bazanskie012b722023-03-29 17:49:04 +020018type metroctlLogFlags struct {
19 // follow (ie. stream) logs live.
20 follow bool
21 // dn to query.
22 dn string
23 // exact dn query, i.e. without children/recursion.
24 exact bool
25 // concise logging output format.
26 concise bool
27 // backlog: >0 for a concrete limit, -1 for all, 0 for none
28 backlog int
29}
30
31var logFlags metroctlLogFlags
32
Serge Bazanskib91938f2023-03-29 14:31:22 +020033var nodeLogsCmd = &cobra.Command{
34 Short: "Get/stream logs from node",
Serge Bazanskie012b722023-03-29 17:49:04 +020035 Long: `Get or stream logs from node.
36
37Node logs are structured in a 'log tree' structure, in which different subsystems
38log to DNs (distinguished names). For example, service 'foo' might log to
39root.role.foo, while service 'bar' might log to root.role.bar.
40
41To set the DN you want to request logs from, use --dn. The default is to return
42all logs. The default output is also also a good starting point to figure out
43what DNs are active in the system.
44
45When requesting logs for a DN by default all sub-DNs will also be returned (ie.
46with the above example, when requesting DN 'root.role' logs at root.role.foo and
47root.role.bar would also be returned). This behaviour can be disabled by setting
48--exact.
49
50To stream logs, use --follow.
51
52By default, all available logs are returned. To limit the number of historical
53log lines (a.k.a. 'backlog') to return, set --backlog. This similar to requesting
54all lines and then piping the result through 'tail' - but more efficient, as no
55unnecessary lines are fetched.
56`,
57 Use: "logs [node-id]",
58 Args: cobra.MinimumNArgs(1),
Serge Bazanskib91938f2023-03-29 14:31:22 +020059 RunE: func(cmd *cobra.Command, args []string) error {
60 ctx := cmd.Context()
61
62 // First connect to the main management service and figure out the node's IP
63 // address.
64 cc := dialAuthenticated(ctx)
65 mgmt := api.NewManagementClient(cc)
66 nodes, err := core.GetNodes(ctx, mgmt, fmt.Sprintf("node.id == %q", args[0]))
67 if err != nil {
68 return fmt.Errorf("when getting node info: %w", err)
69 }
70
71 if len(nodes) == 0 {
72 return fmt.Errorf("no such node")
73 }
74 if len(nodes) > 1 {
75 return fmt.Errorf("expression matched more than one node")
76 }
77 n := nodes[0]
78 if n.Status == nil || n.Status.ExternalAddress == "" {
79 return fmt.Errorf("node has no external address")
80 }
81
82 // TODO(q3k): save CA certificate on takeover
83 info, err := mgmt.GetClusterInfo(ctx, &api.GetClusterInfoRequest{})
84 if err != nil {
85 return fmt.Errorf("couldn't get cluster info: %w", err)
86 }
87 cacert, err := x509.ParseCertificate(info.CaCertificate)
88 if err != nil {
89 return fmt.Errorf("remote CA certificate invalid: %w", err)
90 }
91
Serge Bazanskie012b722023-03-29 17:49:04 +020092 fmt.Printf("=== Logs from %s (%s):\n", n.Id, n.Status.ExternalAddress)
Serge Bazanskib91938f2023-03-29 14:31:22 +020093 // Dial the actual node at its management port.
94 cl := dialAuthenticatedNode(ctx, n.Id, n.Status.ExternalAddress, cacert)
95 nmgmt := api.NewNodeManagementClient(cl)
96
Serge Bazanskie012b722023-03-29 17:49:04 +020097 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 Bazanskib91938f2023-03-29 14:31:22 +0200106 },
Serge Bazanskie012b722023-03-29 17:49:04 +0200107 })
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 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
150func printEntry(e *cpb.LogEntry) {
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
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}