master
go 296 lines 7.49 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 package funcctl
4
5 import (
6 "context"
7 "fmt"
8
9 "github.com/netdata/netdata/go/plugins/logger"
10 "github.com/netdata/netdata/go/plugins/pkg/funcapi"
11 "github.com/netdata/netdata/go/plugins/pkg/netdataapi"
12 "github.com/netdata/netdata/go/plugins/plugin/framework/collectorapi"
13 "github.com/netdata/netdata/go/plugins/plugin/framework/dyncfg"
14 "github.com/netdata/netdata/go/plugins/plugin/framework/functions"
15 )
16
17 type Options struct {
18 Logger *logger.Logger
19 FnReg functions.Registry
20 API *dyncfg.Responder
21 JSONWriter func([]byte, int)
22 }
23
24 type Controller struct {
25 *logger.Logger
26
27 api *dyncfg.Responder
28 jsonWriter func([]byte, int)
29 fnReg functions.Registry
30 ctx context.Context
31
32 registry *moduleFuncRegistry
33 staticMethodsSeen map[string]struct{}
34 }
35
36 func New(opts Options) *Controller {
37 log := opts.Logger
38 if log == nil {
39 log = logger.New()
40 }
41 reg := opts.FnReg
42 if reg == nil {
43 reg = noopRegistry{}
44 }
45
46 return &Controller{
47 Logger: log,
48 api: opts.API,
49 jsonWriter: opts.JSONWriter,
50 fnReg: reg,
51 registry: newModuleFuncRegistry(),
52 staticMethodsSeen: make(map[string]struct{}),
53 }
54 }
55
56 func (c *Controller) Init(ctx context.Context) {
57 c.ctx = ctx
58 }
59
60 func (c *Controller) SetAPI(api *dyncfg.Responder) {
61 if api == nil {
62 // Nil means "keep the current responder" rather than clearing output wiring.
63 return
64 }
65 c.api = api
66 }
67
68 func (c *Controller) RegisterModules(modules collectorapi.Registry) {
69 for name, creator := range modules {
70 if creator.Methods == nil && creator.JobMethods == nil {
71 continue
72 }
73 c.registry.registerModule(name, creator)
74 }
75 }
76
77 func (c *Controller) GetJobNames(moduleName string) []string {
78 return c.registry.getJobNames(moduleName)
79 }
80
81 func (c *Controller) OnJobStart(job collectorapi.RuntimeJob) {
82 if job == nil {
83 return
84 }
85
86 c.registry.addJob(job.ModuleName(), job.Name(), job)
87 c.registerModuleMethodsOnFirstJobStart(job.ModuleName())
88
89 creator, ok := c.registry.getCreator(job.ModuleName())
90 if !ok || creator.JobMethods == nil {
91 return
92 }
93
94 methods := creator.JobMethods(job)
95 if len(methods) > 0 {
96 c.registerJobMethods(job, methods)
97 }
98 }
99
100 func (c *Controller) OnJobStop(job collectorapi.RuntimeJob) {
101 if job == nil {
102 return
103 }
104
105 c.unregisterJobMethods(job)
106 c.registry.removeJob(job.ModuleName(), job.Name())
107 }
108
109 func (c *Controller) Cleanup() {
110 for name, creator := range c.registry.snapshotCreators() {
111 if creator.Methods == nil {
112 continue
113 }
114 for _, method := range creator.Methods() {
115 if method.ID == "" {
116 continue
117 }
118 for _, funcName := range methodFunctionNames(name, method) {
119 c.fnReg.Unregister(funcName)
120 if c.api != nil {
121 c.api.FunctionRemove(funcName)
122 }
123 }
124 }
125 }
126 }
127
128 func (c *Controller) registerModuleMethodsOnFirstJobStart(moduleName string) {
129 if _, ok := c.staticMethodsSeen[moduleName]; ok {
130 return
131 }
132
133 creator, ok := c.registry.getCreator(moduleName)
134 if !ok || creator.Methods == nil {
135 return
136 }
137
138 for _, method := range creator.Methods() {
139 if method.ID == "" {
140 c.Warningf("skipping function registration for module '%s': empty method ID", moduleName)
141 continue
142 }
143
144 if c.api != nil {
145 help := method.Help
146 if help == "" {
147 help = fmt.Sprintf("%s %s data function", moduleName, method.ID)
148 }
149
150 const cloudAccess = "0x0013"
151 access := "0x0000"
152 if method.RequireCloud {
153 access = cloudAccess
154 }
155
156 for _, funcName := range methodFunctionNames(moduleName, method) {
157 c.fnReg.Register(funcName, c.makeMethodFuncHandler(moduleName, method.ID))
158 c.api.FunctionGlobal(netdataapi.FunctionGlobalOpts{
159 Name: funcName,
160 Timeout: 60,
161 Help: help,
162 Tags: "top",
163 Access: access,
164 Priority: 100,
165 Version: 3,
166 })
167 }
168 continue
169 }
170
171 for _, funcName := range methodFunctionNames(moduleName, method) {
172 c.fnReg.Register(funcName, c.makeMethodFuncHandler(moduleName, method.ID))
173 }
174 }
175
176 c.staticMethodsSeen[moduleName] = struct{}{}
177 }
178
179 func methodFunctionNames(moduleName string, method funcapi.MethodConfig) []string {
180 funcName := fmt.Sprintf("%s:%s", moduleName, method.ID)
181 funcNames := []string{funcName}
182 seen := map[string]struct{}{funcName: {}}
183
184 for _, alias := range method.Aliases {
185 if alias == "" {
186 continue
187 }
188 if _, ok := seen[alias]; ok {
189 continue
190 }
191 seen[alias] = struct{}{}
192 funcNames = append(funcNames, alias)
193 }
194 return funcNames
195 }
196
197 func (c *Controller) registerJobMethods(job collectorapi.RuntimeJob, methods []funcapi.MethodConfig) {
198 planned := make(map[string]struct{}, len(methods))
199
200 for _, method := range methods {
201 if method.ID == "" {
202 c.Warningf("skipping job method registration for %s[%s]: empty method ID", job.ModuleName(), job.Name())
203 continue
204 }
205
206 funcName := fmt.Sprintf("%s:%s", job.ModuleName(), method.ID)
207 if _, exists := planned[method.ID]; exists {
208 c.Errorf("job method registration aborted for %s[%s]: duplicate method ID in batch ('%s')", job.ModuleName(), job.Name(), funcName)
209 return
210 }
211 planned[method.ID] = struct{}{}
212
213 if collision, exists := c.registry.findMethodCollision(job.ModuleName(), job.Name(), method.ID); exists {
214 c.Errorf("job method registration aborted for %s[%s]: collision on '%s' (%s)", job.ModuleName(), job.Name(), funcName, collision)
215 return
216 }
217 }
218
219 // Record methods before publishing handlers so startup-time calls do not race a false 404.
220 c.registry.registerJobMethods(job.ModuleName(), job.Name(), methods)
221
222 for _, method := range methods {
223 if method.ID == "" {
224 continue
225 }
226
227 // FIXME: job methods currently ignore method.Aliases and publish only the
228 // canonical module:method name. Static/module methods use methodFunctionNames().
229 funcName := fmt.Sprintf("%s:%s", job.ModuleName(), method.ID)
230 c.fnReg.Register(funcName, c.makeJobMethodFuncHandler(job.ModuleName(), job.Name(), method.ID))
231
232 if c.api != nil {
233 help := method.Help
234 if help == "" {
235 help = fmt.Sprintf("%s %s data function", job.ModuleName(), method.ID)
236 }
237
238 const cloudAccess = "0x0013"
239 access := "0x0000"
240 if method.RequireCloud {
241 access = cloudAccess
242 }
243
244 c.api.FunctionGlobal(netdataapi.FunctionGlobalOpts{
245 Name: funcName,
246 Timeout: 60,
247 Help: help,
248 Tags: "top",
249 Access: access,
250 Priority: 100,
251 Version: 3,
252 })
253 }
254
255 c.Debugf("registered job method: %s for job %s[%s]", funcName, job.ModuleName(), job.Name())
256 }
257 }
258
259 func (c *Controller) unregisterJobMethods(job collectorapi.RuntimeJob) {
260 methods := c.registry.getJobMethods(job.ModuleName(), job.Name())
261 if len(methods) == 0 {
262 return
263 }
264
265 for _, method := range methods {
266 if method.ID == "" {
267 continue
268 }
269
270 // FIXME: keep this in sync with registerJobMethods() if job-method alias
271 // support is added later; today only the canonical name is removed here.
272 funcName := fmt.Sprintf("%s:%s", job.ModuleName(), method.ID)
273 c.fnReg.Unregister(funcName)
274 if c.api != nil {
275 c.api.FunctionRemove(funcName)
276 }
277 c.Debugf("unregistered job method: %s for job %s[%s]", funcName, job.ModuleName(), job.Name())
278 }
279
280 c.registry.unregisterJobMethods(job.ModuleName(), job.Name())
281 }
282
283 func (c *Controller) baseContext() context.Context {
284 if c.ctx != nil {
285 return c.ctx
286 }
287 return context.Background()
288 }
289
290 type noopRegistry struct{}
291
292 func (noopRegistry) Register(string, func(functions.Function)) {}
293 func (noopRegistry) Unregister(string) {}
294 func (noopRegistry) RegisterPrefix(string, string, func(functions.Function)) {
295 }
296 func (noopRegistry) UnregisterPrefix(string, string) {}