blob: 77dd8c223e653b81a7b623a7aefa3f3bfd065d76 [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"
11 "source.monogon.dev/metropolis/pkg/logtree"
Tim Windelschmidt8814f522024-05-08 00:41:13 +020012 lpb "source.monogon.dev/metropolis/pkg/logtree/proto"
Serge Bazanskib91938f2023-03-29 14:31:22 +020013 "source.monogon.dev/metropolis/proto/api"
Serge Bazanskida114862023-03-29 17:46:42 +020014 cpb "source.monogon.dev/metropolis/proto/common"
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]",
57 Args: 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.
63 cc := dialAuthenticated(ctx)
64 mgmt := api.NewManagementClient(cc)
65 nodes, err := core.GetNodes(ctx, mgmt, fmt.Sprintf("node.id == %q", args[0]))
66 if err != nil {
67 return fmt.Errorf("when getting node info: %w", err)
68 }
69
70 if len(nodes) == 0 {
71 return fmt.Errorf("no such node")
72 }
73 if len(nodes) > 1 {
74 return fmt.Errorf("expression matched more than one node")
75 }
76 n := nodes[0]
77 if n.Status == nil || n.Status.ExternalAddress == "" {
78 return fmt.Errorf("node has no external address")
79 }
80
Serge Bazanskic51d47d2024-02-13 18:40:26 +010081 cacert, err := core.GetClusterCAWithTOFU(ctx, connectOptions())
Serge Bazanskib91938f2023-03-29 14:31:22 +020082 if err != nil {
Serge Bazanskic51d47d2024-02-13 18:40:26 +010083 return fmt.Errorf("could not get CA certificate: %w", err)
Serge Bazanskib91938f2023-03-29 14:31:22 +020084 }
85
Serge Bazanskie012b722023-03-29 17:49:04 +020086 fmt.Printf("=== Logs from %s (%s):\n", n.Id, n.Status.ExternalAddress)
Serge Bazanskib91938f2023-03-29 14:31:22 +020087 // Dial the actual node at its management port.
88 cl := dialAuthenticatedNode(ctx, n.Id, n.Status.ExternalAddress, cacert)
89 nmgmt := api.NewNodeManagementClient(cl)
90
Serge Bazanskie012b722023-03-29 17:49:04 +020091 streamMode := api.GetLogsRequest_STREAM_DISABLE
92 if logFlags.follow {
93 streamMode = api.GetLogsRequest_STREAM_UNBUFFERED
94 }
95 var filters []*cpb.LogFilter
96 if !logFlags.exact {
97 filters = append(filters, &cpb.LogFilter{
98 Filter: &cpb.LogFilter_WithChildren_{
99 WithChildren: &cpb.LogFilter_WithChildren{},
Serge Bazanskib91938f2023-03-29 14:31:22 +0200100 },
Serge Bazanskie012b722023-03-29 17:49:04 +0200101 })
102 }
103 backlogMode := api.GetLogsRequest_BACKLOG_ALL
104 var backlogCount int64
105 switch {
106 case logFlags.backlog > 0:
107 backlogMode = api.GetLogsRequest_BACKLOG_COUNT
108 backlogCount = int64(logFlags.backlog)
109 case logFlags.backlog == 0:
110 backlogMode = api.GetLogsRequest_BACKLOG_DISABLE
111 }
112
113 srv, err := nmgmt.Logs(ctx, &api.GetLogsRequest{
114 Dn: logFlags.dn,
115 BacklogMode: backlogMode,
116 BacklogCount: backlogCount,
117 StreamMode: streamMode,
118 Filters: filters,
Serge Bazanskib91938f2023-03-29 14:31:22 +0200119 })
120 if err != nil {
121 return fmt.Errorf("failed to get logs: %w", err)
122 }
123 for {
124 res, err := srv.Recv()
125 if errors.Is(err, io.EOF) {
Serge Bazanskie012b722023-03-29 17:49:04 +0200126 fmt.Println("=== Done.")
Serge Bazanskib91938f2023-03-29 14:31:22 +0200127 break
128 }
129 if err != nil {
130 return fmt.Errorf("log stream failed: %w", err)
131 }
132 for _, entry := range res.BacklogEntries {
Serge Bazanskie012b722023-03-29 17:49:04 +0200133 printEntry(entry)
134 }
135 for _, entry := range res.StreamEntries {
136 printEntry(entry)
Serge Bazanskib91938f2023-03-29 14:31:22 +0200137 }
138 }
139
140 return nil
141 },
142}
Serge Bazanskie012b722023-03-29 17:49:04 +0200143
Tim Windelschmidt8814f522024-05-08 00:41:13 +0200144func printEntry(e *lpb.LogEntry) {
Serge Bazanskie012b722023-03-29 17:49:04 +0200145 entry, err := logtree.LogEntryFromProto(e)
146 if err != nil {
147 fmt.Printf("invalid stream entry: %v\n", err)
148 return
149 }
150 if logFlags.concise {
151 fmt.Println(entry.ConciseString(logtree.MetropolisShortenDict, 0))
152 } else {
153 fmt.Println(entry.String())
154 }
155}
156
157func init() {
158 nodeLogsCmd.Flags().BoolVarP(&logFlags.follow, "follow", "f", false, "Continue streaming logs after fetching backlog.")
159 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.")
160 nodeLogsCmd.Flags().BoolVarP(&logFlags.exact, "exact", "e", false, "Only show logs for exactly the DN, do not recurse down the tree.")
161 nodeLogsCmd.Flags().BoolVarP(&logFlags.concise, "concise", "c", false, "Output concise logs.")
162 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).")
163 nodeCmd.AddCommand(nodeLogsCmd)
164}