master
go 239 lines 7.67 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 package beanstalk
4
5 import (
6 "errors"
7 "fmt"
8 "strconv"
9 "strings"
10
11 "github.com/netdata/netdata/go/plugins/logger"
12 "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/socket"
13
14 "gopkg.in/yaml.v2"
15 )
16
17 type beanstalkConn interface {
18 connect() error
19 disconnect() error
20 queryStats() (*beanstalkdStats, error)
21 queryListTubes() ([]string, error)
22 queryStatsTube(string) (*tubeStats, error)
23 }
24
25 // https://github.com/beanstalkd/beanstalkd/blob/91c54fc05dc759ef27459ce4383934e1a4f2fb4b/doc/protocol.txt#L553
26 type beanstalkdStats struct {
27 CurrentJobsUrgent int64 `yaml:"current-jobs-urgent" stm:"current-jobs-urgent"`
28 CurrentJobsReady int64 `yaml:"current-jobs-ready" stm:"current-jobs-ready"`
29 CurrentJobsReserved int64 `yaml:"current-jobs-reserved" stm:"current-jobs-reserved"`
30 CurrentJobsDelayed int64 `yaml:"current-jobs-delayed" stm:"current-jobs-delayed"`
31 CurrentJobsBuried int64 `yaml:"current-jobs-buried" stm:"current-jobs-buried"`
32 CmdPut int64 `yaml:"cmd-put" stm:"cmd-put"`
33 CmdPeek int64 `yaml:"cmd-peek" stm:"cmd-peek"`
34 CmdPeekReady int64 `yaml:"cmd-peek-ready" stm:"cmd-peek-ready"`
35 CmdPeekDelayed int64 `yaml:"cmd-peek-delayed" stm:"cmd-peek-delayed"`
36 CmdPeekBuried int64 `yaml:"cmd-peek-buried" stm:"cmd-peek-buried"`
37 CmdReserve int64 `yaml:"cmd-reserve" stm:"cmd-reserve"`
38 CmdReserveWithTimeout int64 `yaml:"cmd-reserve-with-timeout" stm:"cmd-reserve-with-timeout"`
39 CmdTouch int64 `yaml:"cmd-touch" stm:"cmd-touch"`
40 CmdUse int64 `yaml:"cmd-use" stm:"cmd-use"`
41 CmdWatch int64 `yaml:"cmd-watch" stm:"cmd-watch"`
42 CmdIgnore int64 `yaml:"cmd-ignore" stm:"cmd-ignore"`
43 CmdDelete int64 `yaml:"cmd-delete" stm:"cmd-delete"`
44 CmdRelease int64 `yaml:"cmd-release" stm:"cmd-release"`
45 CmdBury int64 `yaml:"cmd-bury" stm:"cmd-bury"`
46 CmdKick int64 `yaml:"cmd-kick" stm:"cmd-kick"`
47 CmdStats int64 `yaml:"cmd-stats" stm:"cmd-stats"`
48 CmdStatsJob int64 `yaml:"cmd-stats-job" stm:"cmd-stats-job"`
49 CmdStatsTube int64 `yaml:"cmd-stats-tube" stm:"cmd-stats-tube"`
50 CmdListTubes int64 `yaml:"cmd-list-tubes" stm:"cmd-list-tubes"`
51 CmdListTubeUsed int64 `yaml:"cmd-list-tube-used" stm:"cmd-list-tube-used"`
52 CmdListTubesWatched int64 `yaml:"cmd-list-tubes-watched" stm:"cmd-list-tubes-watched"`
53 CmdPauseTube int64 `yaml:"cmd-pause-tube" stm:"cmd-pause-tube"`
54 JobTimeouts int64 `yaml:"job-timeouts" stm:"job-timeouts"`
55 TotalJobs int64 `yaml:"total-jobs" stm:"total-jobs"`
56 CurrentTubes int64 `yaml:"current-tubes" stm:"current-tubes"`
57 CurrentConnections int64 `yaml:"current-connections" stm:"current-connections"`
58 CurrentProducers int64 `yaml:"current-producers" stm:"current-producers"`
59 CurrentWorkers int64 `yaml:"current-workers" stm:"current-workers"`
60 CurrentWaiting int64 `yaml:"current-waiting" stm:"current-waiting"`
61 TotalConnections int64 `yaml:"total-connections" stm:"total-connections"`
62 RusageUtime float64 `yaml:"rusage-utime" stm:"rusage-utime,1000,1"`
63 RusageStime float64 `yaml:"rusage-stime" stm:"rusage-stime,1000,1"`
64 Uptime int64 `yaml:"uptime" stm:"uptime"`
65 BinlogRecordsWritten int64 `yaml:"binlog-records-written" stm:"binlog-records-written"`
66 BinlogRecordsMigrated int64 `yaml:"binlog-records-migrated" stm:"binlog-records-migrated"`
67 }
68
69 // https://github.com/beanstalkd/beanstalkd/blob/91c54fc05dc759ef27459ce4383934e1a4f2fb4b/doc/protocol.txt#L497
70 type tubeStats struct {
71 Name string `yaml:"name"`
72 CurrentJobsUrgent int64 `yaml:"current-jobs-urgent" stm:"current-jobs-urgent"`
73 CurrentJobsReady int64 `yaml:"current-jobs-ready" stm:"current-jobs-ready"`
74 CurrentJobsReserved int64 `yaml:"current-jobs-reserved" stm:"current-jobs-reserved"`
75 CurrentJobsDelayed int64 `yaml:"current-jobs-delayed" stm:"current-jobs-delayed"`
76 CurrentJobsBuried int64 `yaml:"current-jobs-buried" stm:"current-jobs-buried"`
77 TotalJobs int64 `yaml:"total-jobs" stm:"total-jobs"`
78 CurrentUsing int64 `yaml:"current-using" stm:"current-using"`
79 CurrentWaiting int64 `yaml:"current-waiting" stm:"current-waiting"`
80 CurrentWatching int64 `yaml:"current-watching" stm:"current-watching"`
81 Pause float64 `yaml:"pause" stm:"pause"`
82 CmdDelete int64 `yaml:"cmd-delete" stm:"cmd-delete"`
83 CmdPauseTube int64 `yaml:"cmd-pause-tube" stm:"cmd-pause-tube"`
84 PauseTimeLeft float64 `yaml:"pause-time-left" stm:"pause-time-left"`
85 }
86
87 func newBeanstalkConn(conf Config, log *logger.Logger) beanstalkConn {
88 return &beanstalkClient{
89 Logger: log,
90 client: socket.New(socket.Config{
91 Address: conf.Address,
92 Timeout: conf.Timeout.Duration(),
93 MaxReadLines: 2000,
94 TLSConf: nil,
95 }),
96 }
97 }
98
99 const (
100 cmdQuit = "quit"
101 cmdStats = "stats"
102 cmdListTubes = "list-tubes"
103 cmdStatsTube = "stats-tube"
104 )
105
106 type beanstalkClient struct {
107 *logger.Logger
108
109 client socket.Client
110 }
111
112 func (c *beanstalkClient) connect() error {
113 return c.client.Connect()
114 }
115
116 func (c *beanstalkClient) disconnect() error {
117 _, _, _ = c.query(cmdQuit)
118 return c.client.Disconnect()
119 }
120
121 func (c *beanstalkClient) queryStats() (*beanstalkdStats, error) {
122 cmd := cmdStats
123
124 resp, data, err := c.query(cmd)
125 if err != nil {
126 return nil, err
127 }
128 if resp != "OK" {
129 return nil, fmt.Errorf("command '%s' bad response: %s", cmd, resp)
130 }
131
132 var stats beanstalkdStats
133
134 if err := yaml.Unmarshal(data, &stats); err != nil {
135 return nil, err
136 }
137
138 return &stats, nil
139 }
140
141 func (c *beanstalkClient) queryListTubes() ([]string, error) {
142 cmd := cmdListTubes
143
144 resp, data, err := c.query(cmd)
145 if err != nil {
146 return nil, err
147 }
148 if resp != "OK" {
149 return nil, fmt.Errorf("command '%s' bad response: %s", cmd, resp)
150 }
151
152 var tubes []string
153
154 if err := yaml.Unmarshal(data, &tubes); err != nil {
155 return nil, err
156 }
157
158 return tubes, nil
159 }
160
161 func (c *beanstalkClient) queryStatsTube(tubeName string) (*tubeStats, error) {
162 cmd := fmt.Sprintf("%s %s", cmdStatsTube, tubeName)
163
164 resp, data, err := c.query(cmd)
165 if err != nil {
166 return nil, err
167 }
168 if resp == "NOT_FOUND" {
169 return nil, nil
170 }
171 if resp != "OK" {
172 return nil, fmt.Errorf("command '%s' bad response: %s", cmd, resp)
173 }
174
175 var stats tubeStats
176 if err := yaml.Unmarshal(data, &stats); err != nil {
177 return nil, err
178 }
179
180 return &stats, nil
181 }
182
183 func (c *beanstalkClient) query(command string) (string, []byte, error) {
184 c.Debugf("executing command: %s", command)
185
186 var (
187 resp string
188 body []byte
189 length int
190 err error
191 )
192
193 if err := c.client.Command(command+"\r\n", func(line []byte) (bool, error) {
194 if resp == "" {
195 s := string(line)
196 c.Debugf("command '%s' response: '%s'", command, s)
197
198 resp, length, err = parseResponseLine(s)
199 if err != nil {
200 return false, fmt.Errorf("command '%s' line '%s': %v", command, s, err)
201 }
202
203 return resp == "OK", nil
204 }
205
206 body = append(body, line...)
207 body = append(body, '\n')
208
209 return len(body) < length, nil
210 }); err != nil {
211 return "", nil, fmt.Errorf("command '%s': %v", command, err)
212 }
213
214 return resp, body, nil
215 }
216
217 func parseResponseLine(line string) (string, int, error) {
218 parts := strings.Fields(line)
219 if len(parts) == 0 {
220 return "", 0, errors.New("empty response")
221 }
222
223 resp := parts[0]
224
225 if resp != "OK" {
226 return resp, 0, nil
227 }
228
229 if len(parts) < 2 {
230 return "", 0, errors.New("missing bytes count")
231 }
232
233 length, err := strconv.Atoi(parts[1])
234 if err != nil {
235 return "", 0, errors.New("invalid bytes count")
236 }
237
238 return resp, length, nil
239 }