blob: 36ccd009f4176b4cd4f6e87bf5bb1a4eaa2dc15c [file] [log] [blame]
Tim Windelschmidt6d33a432025-02-04 14:34:25 +01001// Copyright The Monogon Project Authors.
2// SPDX-License-Identifier: Apache-2.0
3
Serge Bazanskib91938f2023-03-29 14:31:22 +02004package main
5
6import (
Serge Bazanskib91938f2023-03-29 14:31:22 +02007 "errors"
8 "fmt"
9 "io"
10
11 "github.com/spf13/cobra"
12
13 "source.monogon.dev/metropolis/cli/metroctl/core"
Serge Bazanskib91938f2023-03-29 14:31:22 +020014 "source.monogon.dev/metropolis/proto/api"
Serge Bazanskida114862023-03-29 17:46:42 +020015 cpb "source.monogon.dev/metropolis/proto/common"
Tim Windelschmidt9f21f532024-05-07 15:14:20 +020016 "source.monogon.dev/osbase/logtree"
17 lpb "source.monogon.dev/osbase/logtree/proto"
Serge Bazanskib91938f2023-03-29 14:31:22 +020018)
19
Serge Bazanskie012b722023-03-29 17:49:04 +020020type 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
33var logFlags metroctlLogFlags
34
Serge Bazanskib91938f2023-03-29 14:31:22 +020035var nodeLogsCmd = &cobra.Command{
36 Short: "Get/stream logs from node",
Serge Bazanskie012b722023-03-29 17:49:04 +020037 Long: `Get or stream logs from node.
38
39Node logs are structured in a 'log tree' structure, in which different subsystems
40log to DNs (distinguished names). For example, service 'foo' might log to
41root.role.foo, while service 'bar' might log to root.role.bar.
42
43To set the DN you want to request logs from, use --dn. The default is to return
44all logs. The default output is also also a good starting point to figure out
45what DNs are active in the system.
46
47When requesting logs for a DN by default all sub-DNs will also be returned (ie.
48with the above example, when requesting DN 'root.role' logs at root.role.foo and
49root.role.bar would also be returned). This behaviour can be disabled by setting
50--exact.
51
52To stream logs, use --follow.
53
54By default, all available logs are returned. To limit the number of historical
55log lines (a.k.a. 'backlog') to return, set --backlog. This similar to requesting
56all lines and then piping the result through 'tail' - but more efficient, as no
57unnecessary lines are fetched.
58`,
59 Use: "logs [node-id]",
Tim Windelschmidtfc6e1cf2024-09-18 17:34:07 +020060 Args: PrintUsageOnWrongArgs(cobra.MinimumNArgs(1)),
Serge Bazanskib91938f2023-03-29 14:31:22 +020061 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 Windelschmidt0b4fb8c2024-09-18 17:34:23 +020066 cc, err := dialAuthenticated(ctx)
67 if err != nil {
68 return fmt.Errorf("while dialing node: %w", err)
69 }
Serge Bazanskib91938f2023-03-29 14:31:22 +020070 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 Bazanskic51d47d2024-02-13 18:40:26 +010087 cacert, err := core.GetClusterCAWithTOFU(ctx, connectOptions())
Serge Bazanskib91938f2023-03-29 14:31:22 +020088 if err != nil {
Serge Bazanskic51d47d2024-02-13 18:40:26 +010089 return fmt.Errorf("could not get CA certificate: %w", err)
Serge Bazanskib91938f2023-03-29 14:31:22 +020090 }
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.
Tim Windelschmidt0b4fb8c2024-09-18 17:34:23 +020094 cl, err := dialAuthenticatedNode(ctx, n.Id, n.Status.ExternalAddress, cacert)
95 if err != nil {
96 return fmt.Errorf("while dialing node: %w", err)
97 }
Serge Bazanskib91938f2023-03-29 14:31:22 +020098 nmgmt := api.NewNodeManagementClient(cl)
99
Tim Windelschmidta10d0cb2025-01-13 14:44:15 +0100100 streamMode := api.GetLogsRequest_STREAM_MODE_DISABLE
Serge Bazanskie012b722023-03-29 17:49:04 +0200101 if logFlags.follow {
Tim Windelschmidta10d0cb2025-01-13 14:44:15 +0100102 streamMode = api.GetLogsRequest_STREAM_MODE_UNBUFFERED
Serge Bazanskie012b722023-03-29 17:49:04 +0200103 }
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 Bazanskib91938f2023-03-29 14:31:22 +0200109 },
Serge Bazanskie012b722023-03-29 17:49:04 +0200110 })
111 }
Tim Windelschmidta10d0cb2025-01-13 14:44:15 +0100112 backlogMode := api.GetLogsRequest_BACKLOG_MODE_ALL
Serge Bazanskie012b722023-03-29 17:49:04 +0200113 var backlogCount int64
114 switch {
115 case logFlags.backlog > 0:
Tim Windelschmidta10d0cb2025-01-13 14:44:15 +0100116 backlogMode = api.GetLogsRequest_BACKLOG_MODE_COUNT
Serge Bazanskie012b722023-03-29 17:49:04 +0200117 backlogCount = int64(logFlags.backlog)
118 case logFlags.backlog == 0:
Tim Windelschmidta10d0cb2025-01-13 14:44:15 +0100119 backlogMode = api.GetLogsRequest_BACKLOG_MODE_DISABLE
Serge Bazanskie012b722023-03-29 17:49:04 +0200120 }
121
122 srv, err := nmgmt.Logs(ctx, &api.GetLogsRequest{
123 Dn: logFlags.dn,
124 BacklogMode: backlogMode,
125 BacklogCount: backlogCount,
126 StreamMode: streamMode,
127 Filters: filters,
Serge Bazanskib91938f2023-03-29 14:31:22 +0200128 })
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 Bazanskie012b722023-03-29 17:49:04 +0200135 fmt.Println("=== Done.")
Serge Bazanskib91938f2023-03-29 14:31:22 +0200136 break
137 }
138 if err != nil {
139 return fmt.Errorf("log stream failed: %w", err)
140 }
141 for _, entry := range res.BacklogEntries {
Serge Bazanskie012b722023-03-29 17:49:04 +0200142 printEntry(entry)
143 }
144 for _, entry := range res.StreamEntries {
145 printEntry(entry)
Serge Bazanskib91938f2023-03-29 14:31:22 +0200146 }
147 }
148
149 return nil
150 },
151}
Serge Bazanskie012b722023-03-29 17:49:04 +0200152
Tim Windelschmidt8814f522024-05-08 00:41:13 +0200153func printEntry(e *lpb.LogEntry) {
Serge Bazanskie012b722023-03-29 17:49:04 +0200154 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
166func 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}