1package command
2
3import (
4	"fmt"
5	"io"
6	"os"
7	"os/signal"
8	"strconv"
9	"strings"
10	"syscall"
11	"time"
12
13	"github.com/hashicorp/nomad/api"
14	"github.com/mitchellh/cli"
15)
16
17type MonitorCommand struct {
18	Meta
19}
20
21func (c *MonitorCommand) Help() string {
22	helpText := `
23Usage: nomad monitor [options]
24
25	Stream log messages of a nomad agent. The monitor command lets you
26	listen for log levels that may be filtered out of the Nomad agent. For
27	example your agent may only be logging at INFO level, but with the monitor
28	command you can set -log-level DEBUG
29
30General Options:
31
32	` + generalOptionsUsage() + `
33
34Monitor Specific Options:
35
36  -log-level <level>
37    Sets the log level to monitor (default: INFO)
38
39  -node-id <node-id>
40    Sets the specific node to monitor
41
42  -server-id <server-id>
43    Sets the specific server to monitor
44
45  -json
46    Sets log output to JSON format
47  `
48	return strings.TrimSpace(helpText)
49}
50
51func (c *MonitorCommand) Synopsis() string {
52	return "stream logs from a Nomad agent"
53}
54
55func (c *MonitorCommand) Name() string { return "monitor" }
56
57func (c *MonitorCommand) Run(args []string) int {
58	c.Ui = &cli.PrefixedUi{
59		OutputPrefix: "    ",
60		InfoPrefix:   "    ",
61		ErrorPrefix:  "==> ",
62		Ui:           c.Ui,
63	}
64
65	var logLevel string
66	var nodeID string
67	var serverID string
68	var logJSON bool
69
70	flags := c.Meta.FlagSet(c.Name(), FlagSetClient)
71	flags.Usage = func() { c.Ui.Output(c.Help()) }
72	flags.StringVar(&logLevel, "log-level", "", "")
73	flags.StringVar(&nodeID, "node-id", "", "")
74	flags.StringVar(&serverID, "server-id", "", "")
75	flags.BoolVar(&logJSON, "json", false, "")
76
77	if err := flags.Parse(args); err != nil {
78		return 1
79	}
80
81	args = flags.Args()
82	if l := len(args); l != 0 {
83		c.Ui.Error("This command takes no arguments")
84		c.Ui.Error(commandErrorText(c))
85		return 1
86	}
87
88	client, err := c.Meta.Client()
89	if err != nil {
90		c.Ui.Error(fmt.Sprintf("Error initializing client: %s", err))
91		c.Ui.Error(commandErrorText(c))
92		return 1
93	}
94
95	// Query the node info and lookup prefix
96	if len(nodeID) == 1 {
97		c.Ui.Error(fmt.Sprintf("Node identifier must contain at least two characters."))
98		return 1
99	}
100
101	if nodeID != "" {
102		nodeID = sanitizeUUIDPrefix(nodeID)
103		nodes, _, err := client.Nodes().PrefixList(nodeID)
104		if err != nil {
105			c.Ui.Error(fmt.Sprintf("Error querying node: %v", err))
106			return 1
107		}
108
109		if len(nodes) == 0 {
110			c.Ui.Error(fmt.Sprintf("No node(s) with prefix or id %q found", nodeID))
111			return 1
112		}
113
114		if len(nodes) > 1 {
115			out := formatNodeStubList(nodes, false)
116			c.Ui.Output(fmt.Sprintf("Prefix matched multiple nodes\n\n%s", out))
117			return 1
118		}
119		nodeID = nodes[0].ID
120	}
121
122	params := map[string]string{
123		"log_level": logLevel,
124		"node_id":   nodeID,
125		"server_id": serverID,
126		"log_json":  strconv.FormatBool(logJSON),
127	}
128
129	query := &api.QueryOptions{
130		Params: params,
131	}
132
133	eventDoneCh := make(chan struct{})
134	frames, errCh := client.Agent().Monitor(eventDoneCh, query)
135	select {
136	case err := <-errCh:
137		c.Ui.Error(fmt.Sprintf("Error starting monitor: %s", err))
138		c.Ui.Error(commandErrorText(c))
139		return 1
140	default:
141	}
142
143	// Create a reader
144	var r io.ReadCloser
145	frameReader := api.NewFrameReader(frames, errCh, eventDoneCh)
146	frameReader.SetUnblockTime(500 * time.Millisecond)
147	r = frameReader
148
149	defer r.Close()
150
151	signalCh := make(chan os.Signal, 1)
152	signal.Notify(signalCh, os.Interrupt, syscall.SIGTERM)
153
154	go func() {
155		<-signalCh
156		// End the streaming
157		r.Close()
158	}()
159
160	_, err = io.Copy(os.Stdout, r)
161	if err != nil {
162		c.Ui.Error(fmt.Sprintf("error monitoring logs: %s", err))
163		return 1
164	}
165
166	return 0
167}
168