master
go 387 lines 10.5 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 package main
4
5 import (
6 "context"
7 "encoding/json"
8 "fmt"
9 "io"
10 "log/slog"
11 "os"
12 "os/user"
13 "strconv"
14 "strings"
15 "time"
16
17 "github.com/netdata/netdata/go/plugins/cmd/internal/agenthost"
18 "github.com/netdata/netdata/go/plugins/cmd/internal/discoveryproviders"
19 "github.com/netdata/netdata/go/plugins/plugin/agent"
20 "github.com/netdata/netdata/go/plugins/plugin/agent/discovery"
21 "github.com/netdata/netdata/go/plugins/plugin/agent/discovery/dummy"
22 "github.com/netdata/netdata/go/plugins/plugin/agent/discovery/file"
23 "github.com/netdata/netdata/go/plugins/plugin/agent/jobmgr"
24 "github.com/netdata/netdata/go/plugins/plugin/agent/policy"
25 "go.uber.org/automaxprocs/maxprocs"
26 "golang.org/x/net/http/httpproxy"
27
28 "github.com/netdata/netdata/go/plugins/logger"
29 "github.com/netdata/netdata/go/plugins/pkg/buildinfo"
30 "github.com/netdata/netdata/go/plugins/pkg/cli"
31 "github.com/netdata/netdata/go/plugins/pkg/executable"
32 "github.com/netdata/netdata/go/plugins/pkg/hostinfo"
33 "github.com/netdata/netdata/go/plugins/pkg/multipath"
34 "github.com/netdata/netdata/go/plugins/pkg/netdataapi"
35 "github.com/netdata/netdata/go/plugins/pkg/pluginconfig"
36 "github.com/netdata/netdata/go/plugins/pkg/terminal"
37 "github.com/netdata/netdata/go/plugins/plugin/framework/collectorapi"
38 "github.com/netdata/netdata/go/plugins/plugin/framework/confgroup"
39 "github.com/netdata/netdata/go/plugins/plugin/framework/dyncfg"
40 "github.com/netdata/netdata/go/plugins/plugin/framework/functions"
41 _ "github.com/netdata/netdata/go/plugins/plugin/go.d/collector"
42 "github.com/netdata/netdata/go/plugins/plugin/go.d/discovery/sdext"
43 )
44
45 func init() {
46 // https://github.com/netdata/netdata/issues/8949#issuecomment-638294959
47 if v := os.Getenv("TZ"); strings.HasPrefix(v, ":") {
48 _ = os.Unsetenv("TZ")
49 }
50 }
51
52 func main() {
53 _, _ = maxprocs.Set(maxprocs.Logger(func(s string, args ...any) {}))
54
55 opts := parseCLI()
56
57 if opts.Version {
58 fmt.Printf("%s.plugin, version: %s\n", executable.Name, buildinfo.Version)
59 return
60 }
61
62 pluginconfig.MustInit(pluginconfig.InitInput{
63 ConfDir: opts.ConfDir,
64 WatchPath: opts.WatchPath,
65 })
66
67 if opts.Function != "" {
68 os.Exit(runFunctionCLI(opts))
69 }
70
71 if lvl := pluginconfig.EnvLogLevel(); lvl != "" {
72 logger.Level.SetByName(lvl)
73 }
74 if opts.Debug {
75 logger.Level.Set(slog.LevelDebug)
76 }
77 isTerminal := terminal.IsTerminal()
78 isInsideK8s := hostinfo.IsInsideK8sCluster()
79 moduleRegistry := moduleRegistryWithSystemdPolicy(collectorapi.DefaultRegistry, hostinfo.SystemdVersion)
80
81 runModePolicy := policy.Agent(isTerminal)
82
83 a := agent.New(agent.Config{
84 Name: executable.Name,
85 PluginConfigDir: pluginconfig.ConfigDir(),
86 CollectorsConfigDir: pluginconfig.CollectorsDir(),
87 ServiceDiscoveryConfigDir: pluginconfig.ServiceDiscoveryDir(),
88 CollectorsConfigWatchPath: pluginconfig.CollectorsConfigWatchPaths(),
89 VarLibDir: pluginconfig.VarLibDir(),
90 ModuleRegistry: moduleRegistry,
91 IsInsideK8s: isInsideK8s,
92 RunModePolicy: runModePolicy,
93 DiscoveryProviders: []discovery.ProviderFactory{
94 discoveryproviders.File(),
95 discoveryproviders.Dummy(),
96 discoveryproviders.SD(sdext.Registry(!isInsideK8s)),
97 },
98 RunModule: opts.Module,
99 RunJob: opts.Job,
100 MinUpdateEvery: opts.UpdateEvery,
101 })
102
103 a.Infof("plugin: name=%s, %s", a.Name, buildinfo.Info())
104 if u, err := user.Current(); err == nil {
105 a.Debugf("current user: name=%s, uid=%s", u.Username, u.Uid)
106 }
107
108 proxyCfg := httpproxy.FromEnvironment()
109 a.Infof("env HTTP_PROXY '%s', HTTPS_PROXY '%s'", proxyCfg.HTTPProxy, proxyCfg.HTTPSProxy)
110
111 a.Infof("directories → config: %s | collectors: %s | sd: %s | varlib: %s",
112 a.ConfigDir, a.CollectorsConfDir, a.ServiceDiscoveryConfigDir, a.VarLibDir)
113
114 agenthost.Run(a)
115 }
116
117 func parseCLI() *cli.Option {
118 opt, err := cli.Parse(os.Args)
119 if err != nil {
120 if cli.IsHelp(err) {
121 os.Exit(0)
122 }
123 os.Exit(1)
124 }
125
126 return opt
127 }
128
129 func moduleRegistryWithSystemdPolicy(base collectorapi.Registry, systemdVersion int) collectorapi.Registry {
130 registry := make(collectorapi.Registry, len(base))
131 for name, creator := range base {
132 if name == "logind" && systemdVersion == 239 {
133 // Known issue: go.d/logind high CPU usage on Alma Linux8.
134 // Keep policy in cmd wiring, not inside generic agent package.
135 creator.Disabled = true
136 }
137 registry[name] = creator
138 }
139 return registry
140 }
141
142 func runFunctionCLI(opts *cli.Option) int {
143 functionName := strings.TrimSpace(opts.Function)
144 if functionName == "" {
145 writeFunctionError(400, "missing function name (expected module:method)")
146 return 1
147 }
148
149 moduleName, methodID, err := functions.SplitFunctionName(functionName)
150 if err != nil {
151 writeFunctionError(400, "%v", err)
152 return 1
153 }
154
155 creator, ok := collectorapi.DefaultRegistry.Lookup(moduleName)
156 if !ok {
157 writeFunctionError(404, "unknown module '%s'", moduleName)
158 return 1
159 }
160 if creator.Methods == nil {
161 writeFunctionError(404, "module '%s' does not expose functions", moduleName)
162 return 1
163 }
164 if methodID == "" {
165 writeFunctionError(400, "missing method name in function '%s'", functionName)
166 return 1
167 }
168
169 payloadBytes, payloadTimeout, err := readFunctionPayload(opts.FunctionPayload)
170 if err != nil {
171 writeFunctionError(400, "%v", err)
172 return 1
173 }
174
175 timeout, err := resolveFunctionTimeout(opts.FunctionTimeout, payloadTimeout)
176 if err != nil {
177 writeFunctionError(400, "%v", err)
178 return 1
179 }
180
181 reg := confgroup.Registry{}
182 reg.Register(moduleName, confgroup.Default{
183 MinUpdateEvery: opts.UpdateEvery,
184 UpdateEvery: creator.UpdateEvery,
185 AutoDetectionRetry: creator.AutoDetectionRetry,
186 Priority: creator.Priority,
187 })
188
189 groups, err := loadConfigGroups(moduleName, reg, pluginconfig.CollectorsDir())
190 if err != nil {
191 writeFunctionError(500, "%v", err)
192 return 1
193 }
194 if len(groups) == 0 {
195 writeFunctionError(404, "no configs found for module '%s'", moduleName)
196 return 1
197 }
198
199 ctx, cancel := context.WithCancel(context.Background())
200 defer cancel()
201
202 jobMgr := jobmgr.New(jobmgr.Config{
203 PluginName: executable.Name,
204 Out: io.Discard,
205 RunModePolicy: policy.FunctionCLI(),
206 VarLibDir: pluginconfig.VarLibDir(),
207 Modules: collectorapi.Registry{moduleName: creator},
208 ConfigDefaults: reg,
209 FnReg: functions.NewManager(),
210 FunctionJSONWriter: func(payload []byte, _ int) {
211 _, _ = os.Stdout.Write(payload)
212 _, _ = os.Stdout.Write([]byte("\n"))
213 },
214 })
215 jobMgr.SetDyncfgResponder(dyncfg.NewResponder(netdataapi.New(io.Discard)))
216
217 in := make(chan []*confgroup.Group, 1)
218 go jobMgr.Run(ctx, in)
219
220 startCtx, startCancel := context.WithTimeout(ctx, time.Second*10)
221 defer startCancel()
222 if ok := jobMgr.WaitStarted(startCtx); !ok {
223 writeFunctionError(503, "job manager failed to start")
224 return 1
225 }
226
227 in <- groups
228
229 if err := waitForJobs(startCtx, jobMgr, moduleName); err != nil {
230 writeFunctionError(503, "%v", err)
231 return 1
232 }
233
234 fn := functions.Function{
235 Name: functionName,
236 Args: opts.FunctionArgs,
237 Payload: payloadBytes,
238 Timeout: timeout,
239 ContentType: "application/json",
240 }
241 jobMgr.ExecuteFunction(functionName, fn)
242
243 return 0
244 }
245
246 func readFunctionPayload(raw string) ([]byte, time.Duration, error) {
247 if raw == "" {
248 return nil, 0, nil
249 }
250
251 var data []byte
252 var err error
253 if after, ok := strings.CutPrefix(raw, "@"); ok {
254 data, err = os.ReadFile(after)
255 } else {
256 data = []byte(raw)
257 }
258 if err != nil {
259 return nil, 0, fmt.Errorf("read payload: %w", err)
260 }
261
262 var payload map[string]any
263 if err := json.Unmarshal(data, &payload); err != nil {
264 return nil, 0, fmt.Errorf("parse payload JSON: %w", err)
265 }
266
267 timeoutMs, ok, err := parsePayloadTimeout(payload)
268 if err != nil {
269 return nil, 0, err
270 }
271 if ok {
272 return data, time.Duration(timeoutMs) * time.Millisecond, nil
273 }
274 return data, 0, nil
275 }
276
277 func parsePayloadTimeout(payload map[string]any) (int64, bool, error) {
278 if payload == nil {
279 return 0, false, nil
280 }
281 raw, ok := payload["timeout"]
282 if !ok {
283 return 0, false, nil
284 }
285 switch v := raw.(type) {
286 case float64:
287 return int64(v), true, nil
288 case int:
289 return int64(v), true, nil
290 case int64:
291 return v, true, nil
292 case string:
293 if v == "" {
294 return 0, false, nil
295 }
296 n, err := strconv.ParseInt(v, 10, 64)
297 if err != nil {
298 return 0, false, fmt.Errorf("invalid payload timeout '%s'", v)
299 }
300 return n, true, nil
301 default:
302 return 0, false, fmt.Errorf("invalid payload timeout type %T", raw)
303 }
304 }
305
306 func resolveFunctionTimeout(flagValue string, payloadTimeout time.Duration) (time.Duration, error) {
307 if flagValue != "" {
308 d, err := time.ParseDuration(flagValue)
309 if err == nil {
310 return d, nil
311 }
312 secs, err2 := strconv.ParseInt(flagValue, 10, 64)
313 if err2 != nil {
314 return 0, fmt.Errorf("invalid function-timeout '%s'", flagValue)
315 }
316 return time.Duration(secs) * time.Second, nil
317 }
318 if payloadTimeout > 0 {
319 return payloadTimeout, nil
320 }
321 return time.Minute, nil
322 }
323
324 func loadConfigGroups(moduleName string, reg confgroup.Registry, collectors multipath.MultiPath) ([]*confgroup.Group, error) {
325 if path, err := collectors.Find(moduleName + ".conf"); err == nil && path != "" {
326 reader := file.NewReader(reg, []string{path})
327 return runDiscoverer(reader)
328 }
329
330 disc, err := dummy.NewDiscovery(dummy.Config{
331 Registry: reg,
332 Names: []string{moduleName},
333 })
334 if err != nil {
335 return nil, err
336 }
337 return runDiscoverer(disc)
338 }
339
340 type discoverer interface {
341 Run(ctx context.Context, in chan<- []*confgroup.Group)
342 }
343
344 func runDiscoverer(d discoverer) ([]*confgroup.Group, error) {
345 ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
346 defer cancel()
347
348 ch := make(chan []*confgroup.Group, 1)
349 go d.Run(ctx, ch)
350
351 select {
352 case groups, ok := <-ch:
353 if !ok {
354 return nil, fmt.Errorf("discoverer returned no groups")
355 }
356 return groups, nil
357 case <-ctx.Done():
358 return nil, fmt.Errorf("discoverer timeout")
359 }
360 }
361
362 func waitForJobs(ctx context.Context, mgr *jobmgr.Manager, moduleName string) error {
363 for {
364 if len(mgr.GetJobNames(moduleName)) > 0 {
365 return nil
366 }
367 select {
368 case <-ctx.Done():
369 return fmt.Errorf("no jobs started for module '%s'", moduleName)
370 case <-time.After(100 * time.Millisecond):
371 }
372 }
373 }
374
375 func writeFunctionError(status int, format string, args ...any) {
376 resp := map[string]any{
377 "status": status,
378 "errorMessage": fmt.Sprintf(format, args...),
379 }
380 data, err := json.Marshal(resp)
381 if err != nil {
382 _, _ = fmt.Fprintf(os.Stdout, "{\"status\":%d,\"errorMessage\":\"%s\"}\n", status, "failed to encode error response")
383 return
384 }
385 _, _ = os.Stdout.Write(data)
386 _, _ = os.Stdout.Write([]byte("\n"))
387 }