@cryptotaxi247 / netdata-1 / commits / f5799819c

feat(go.d/docker): add docker ps -a function (#21868)

Ilya Mashchenko committed Mar 3, 2026 at 14:47 UTC f5799819c3594483c212394f79713c715bc68424
9 files changed +776 -4
src/go/plugin/go.d/collector/docker/collect.go
-2
@@ -27,8 +27,6 @@ func (c *Collector) collect() (map[string]int64, error) {
27 c.negotiateAPIVersion()
28 }
29
30 - defer func() { _ = c.client.Close() }()
31 -
30 mx := make(map[string]int64)
31
32 if err := c.collectInfo(mx); err != nil {
src/go/plugin/go.d/collector/docker/collector.go
+22 -2
@@ -9,8 +9,10 @@ import (
9 "time"
10
11 "github.com/netdata/netdata/go/plugins/pkg/confopt"
12 + "github.com/netdata/netdata/go/plugins/pkg/funcapi"
13 "github.com/netdata/netdata/go/plugins/pkg/matcher"
14 "github.com/netdata/netdata/go/plugins/plugin/framework/collectorapi"
15 + "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/docker/dockerfunc"
16 "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/dockerhost"
17
18 "github.com/docker/docker/api/types"
@@ -28,11 +30,19 @@ func init() {
30 JobConfigSchema: configSchema,
31 Create: func() collectorapi.CollectorV1 { return New() },
32 Config: func() any { return &Config{} },
33 + Methods: dockerfunc.Methods,
34 + MethodHandler: func(job collectorapi.RuntimeJob) funcapi.MethodHandler {
35 + c, ok := job.Collector().(*Collector)
36 + if !ok {
37 + return nil
38 + }
39 + return c.funcRouter
40 + },
41 })
42 }
43
44 func New() *Collector {
35 - return &Collector{
45 + c := &Collector{
46 Config: Config{
47 Address: docker.DefaultDockerHost,
48 Timeout: confopt.Duration(time.Second * 2),
@@ -47,6 +57,8 @@ func New() *Collector {
57 cntrSr: matcher.TRUE(),
58 containers: make(map[string]bool),
59 }
60 + c.funcRouter = dockerfunc.NewRouter(funcDepsAdapter{collector: c})
61 + return c
62 }
63
64 type Config struct {
@@ -69,6 +81,8 @@ type (
81 client dockerClient
82 newClient func(Config) (dockerClient, error)
83
84 + funcRouter funcapi.MethodHandler
85 +
86 verNegotiated bool
87 containers map[string]bool
88 cntrSr matcher.Matcher
@@ -98,6 +112,9 @@ func (c *Collector) Init(context.Context) error {
112 }
113 c.cntrSr = sr
114 }
115 + if c.funcRouter == nil {
116 + c.funcRouter = dockerfunc.NewRouter(funcDepsAdapter{collector: c})
117 + }
118
119 return nil
120 }
@@ -130,7 +147,10 @@ func (c *Collector) Collect(context.Context) map[string]int64 {
147 return mx
148 }
149
133 -func (c *Collector) Cleanup(context.Context) {
150 +func (c *Collector) Cleanup(ctx context.Context) {
151 + if c.funcRouter != nil {
152 + c.funcRouter.Cleanup(ctx)
153 + }
154 if c.client == nil {
155 return
156 }
src/go/plugin/go.d/collector/docker/dockerfunc/containers.go new
+373
@@ -0,0 +1,373 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +package dockerfunc
4 +
5 +import (
6 + "context"
7 + "errors"
8 + "fmt"
9 + "sort"
10 + "strings"
11 + "time"
12 +
13 + "github.com/docker/docker/api/types"
14 + typesContainer "github.com/docker/docker/api/types/container"
15 + "github.com/docker/go-units"
16 + "github.com/netdata/netdata/go/plugins/pkg/funcapi"
17 +)
18 +
19 +const (
20 + containersMethodID = "container-ls"
21 + containersMethodHelp = "List Docker containers (equivalent to docker ps -a)."
22 +)
23 +
24 +const (
25 + colContainerID = iota
26 + colImage
27 + colCommand
28 + colCreated
29 + colStatus
30 + colState
31 + colPorts
32 + colNames
33 + colContainerIDFull
34 + colCreatedUnix
35 +)
36 +
37 +const (
38 + containersColID = "container_id"
39 + containersColImage = "image"
40 + containersColCommand = "command"
41 + containersColCreated = "created"
42 + containersColStatus = "status"
43 + containersColState = "state"
44 + containersColPorts = "ports"
45 + containersColNames = "names"
46 + containersColIDFull = "container_id_full"
47 + containersColCreatedUnix = "created_unix"
48 +)
49 +
50 +func containersMethodConfig() funcapi.MethodConfig {
51 + return funcapi.MethodConfig{
52 + ID: containersMethodID,
53 + Name: "Containers",
54 + UpdateEvery: 10,
55 + Help: containersMethodHelp,
56 + }
57 +}
58 +
59 +type funcContainers struct {
60 + router *router
61 +}
62 +
63 +func newFuncContainers(r *router) *funcContainers {
64 + return &funcContainers{router: r}
65 +}
66 +
67 +// Compile-time interface check.
68 +var _ funcapi.MethodHandler = (*funcContainers)(nil)
69 +
70 +func (f *funcContainers) MethodParams(_ context.Context, method string) ([]funcapi.ParamConfig, error) {
71 + if method != containersMethodID {
72 + return nil, fmt.Errorf("unknown method: %s", method)
73 + }
74 + return nil, nil
75 +}
76 +
77 +func (f *funcContainers) Handle(ctx context.Context, method string, params funcapi.ResolvedParams) *funcapi.FunctionResponse {
78 + if method != containersMethodID {
79 + return funcapi.NotFoundResponse(method)
80 + }
81 +
82 + client, err := f.router.deps.DockerClient()
83 + if err != nil {
84 + return funcapi.UnavailableResponse("collector is still initializing, please retry in a few seconds")
85 + }
86 +
87 + containers, err := client.ContainerList(ctx, typesContainer.ListOptions{All: true})
88 + if err != nil {
89 + if errors.Is(err, context.DeadlineExceeded) {
90 + return funcapi.ErrorResponse(504, "query timed out")
91 + }
92 + return funcapi.ErrorResponse(500, "failed to list containers: %v", err)
93 + }
94 +
95 + sortContainers(containers)
96 +
97 + now := time.Now()
98 + rows := make([][]any, 0, len(containers))
99 + for _, cntr := range containers {
100 + rows = append(rows, buildContainerRow(cntr, now))
101 + }
102 +
103 + return &funcapi.FunctionResponse{
104 + Status: 200,
105 + Help: containersMethodHelp,
106 + Columns: buildContainerColumns(),
107 + Data: rows,
108 + DefaultSortColumn: containersColCreatedUnix,
109 + }
110 +}
111 +
112 +func (f *funcContainers) Cleanup(ctx context.Context) {}
113 +
114 +func sortContainers(containers []typesContainer.Summary) {
115 + sort.SliceStable(containers, func(i, j int) bool {
116 + if containers[i].Created != containers[j].Created {
117 + return containers[i].Created > containers[j].Created
118 + }
119 + return containers[i].ID < containers[j].ID
120 + })
121 +}
122 +
123 +func buildContainerRow(cntr typesContainer.Summary, now time.Time) []any {
124 + row := make([]any, colCreatedUnix+1)
125 + row[colContainerID] = shortContainerID(cntr.ID)
126 + row[colImage] = cntr.Image
127 + row[colCommand] = strings.TrimSpace(cntr.Command)
128 + row[colCreated] = formatCreated(cntr.Created, now)
129 + row[colStatus] = formatStatus(cntr)
130 + row[colState] = strings.TrimSpace(cntr.State)
131 + row[colPorts] = formatPorts(cntr.Ports)
132 + row[colNames] = formatContainerNames(cntr.Names)
133 + row[colContainerIDFull] = cntr.ID
134 + row[colCreatedUnix] = cntr.Created
135 + return row
136 +}
137 +
138 +func buildContainerColumns() map[string]any {
139 + return map[string]any{
140 + containersColID: funcapi.Column{
141 + Index: colContainerID,
142 + Name: "CONTAINER ID",
143 + Type: funcapi.FieldTypeString,
144 + Visualization: funcapi.FieldVisualValue,
145 + Sort: funcapi.FieldSortAscending,
146 + Sortable: true,
147 + Sticky: true,
148 + Summary: funcapi.FieldSummaryCount,
149 + Filter: funcapi.FieldFilterMultiselect,
150 + Visible: true,
151 + ValueOptions: funcapi.ValueOptions{Transform: funcapi.FieldTransformText},
152 + }.BuildColumn(),
153 + containersColImage: funcapi.Column{
154 + Index: colImage,
155 + Name: "IMAGE",
156 + Type: funcapi.FieldTypeString,
157 + Visualization: funcapi.FieldVisualValue,
158 + Sort: funcapi.FieldSortAscending,
159 + Sortable: true,
160 + Summary: funcapi.FieldSummaryCount,
161 + Filter: funcapi.FieldFilterMultiselect,
162 + Visible: true,
163 + Wrap: true,
164 + ValueOptions: funcapi.ValueOptions{Transform: funcapi.FieldTransformText},
165 + }.BuildColumn(),
166 + containersColCommand: funcapi.Column{
167 + Index: colCommand,
168 + Name: "COMMAND",
169 + Type: funcapi.FieldTypeString,
170 + Visualization: funcapi.FieldVisualValue,
171 + Sort: funcapi.FieldSortAscending,
172 + Sortable: false,
173 + Summary: funcapi.FieldSummaryCount,
174 + Filter: funcapi.FieldFilterNone,
175 + Visible: true,
176 + Wrap: true,
177 + ValueOptions: funcapi.ValueOptions{Transform: funcapi.FieldTransformText},
178 + }.BuildColumn(),
179 + containersColCreated: funcapi.Column{
180 + Index: colCreated,
181 + Name: "CREATED",
182 + Type: funcapi.FieldTypeString,
183 + Visualization: funcapi.FieldVisualValue,
184 + Sort: funcapi.FieldSortDescending,
185 + Sortable: false,
186 + Summary: funcapi.FieldSummaryCount,
187 + Filter: funcapi.FieldFilterMultiselect,
188 + Visible: true,
189 + ValueOptions: funcapi.ValueOptions{Transform: funcapi.FieldTransformText},
190 + }.BuildColumn(),
191 + containersColStatus: funcapi.Column{
192 + Index: colStatus,
193 + Name: "STATUS",
194 + Type: funcapi.FieldTypeString,
195 + Visualization: funcapi.FieldVisualValue,
196 + Sort: funcapi.FieldSortAscending,
197 + Sortable: false,
198 + Summary: funcapi.FieldSummaryCount,
199 + Filter: funcapi.FieldFilterNone,
200 + Visible: true,
201 + Wrap: true,
202 + ValueOptions: funcapi.ValueOptions{Transform: funcapi.FieldTransformText},
203 + }.BuildColumn(),
204 + containersColState: funcapi.Column{
205 + Index: colState,
206 + Name: "State (Raw)",
207 + Type: funcapi.FieldTypeString,
208 + Visualization: funcapi.FieldVisualValue,
209 + Sort: funcapi.FieldSortAscending,
210 + Sortable: true,
211 + Summary: funcapi.FieldSummaryCount,
212 + Filter: funcapi.FieldFilterMultiselect,
213 + Visible: false,
214 + ValueOptions: funcapi.ValueOptions{Transform: funcapi.FieldTransformText},
215 + }.BuildColumn(),
216 + containersColPorts: funcapi.Column{
217 + Index: colPorts,
218 + Name: "PORTS",
219 + Type: funcapi.FieldTypeString,
220 + Visualization: funcapi.FieldVisualValue,
221 + Sort: funcapi.FieldSortAscending,
222 + Sortable: false,
223 + Summary: funcapi.FieldSummaryCount,
224 + Filter: funcapi.FieldFilterNone,
225 + Visible: true,
226 + Wrap: true,
227 + ValueOptions: funcapi.ValueOptions{Transform: funcapi.FieldTransformText},
228 + }.BuildColumn(),
229 + containersColNames: funcapi.Column{
230 + Index: colNames,
231 + Name: "NAMES",
232 + Type: funcapi.FieldTypeString,
233 + Visualization: funcapi.FieldVisualValue,
234 + Sort: funcapi.FieldSortAscending,
235 + Sortable: true,
236 + Summary: funcapi.FieldSummaryCount,
237 + Filter: funcapi.FieldFilterMultiselect,
238 + Visible: true,
239 + Sticky: true,
240 + ValueOptions: funcapi.ValueOptions{Transform: funcapi.FieldTransformText},
241 + }.BuildColumn(),
242 + containersColIDFull: funcapi.Column{
243 + Index: colContainerIDFull,
244 + Name: "Container ID (Full)",
245 + Type: funcapi.FieldTypeString,
246 + Visualization: funcapi.FieldVisualValue,
247 + Sort: funcapi.FieldSortAscending,
248 + Sortable: false,
249 + Summary: funcapi.FieldSummaryCount,
250 + Filter: funcapi.FieldFilterMultiselect,
251 + Visible: false,
252 + UniqueKey: true,
253 + ValueOptions: funcapi.ValueOptions{Transform: funcapi.FieldTransformText},
254 + }.BuildColumn(),
255 + containersColCreatedUnix: funcapi.Column{
256 + Index: colCreatedUnix,
257 + Name: "Created (Unix)",
258 + Type: funcapi.FieldTypeInteger,
259 + Visualization: funcapi.FieldVisualValue,
260 + Sort: funcapi.FieldSortDescending,
261 + Sortable: true,
262 + Summary: funcapi.FieldSummaryMax,
263 + Filter: funcapi.FieldFilterNone,
264 + Visible: false,
265 + ValueOptions: funcapi.ValueOptions{Transform: funcapi.FieldTransformNumber},
266 + }.BuildColumn(),
267 + }
268 +}
269 +
270 +func shortContainerID(id string) string {
271 + if len(id) <= 12 {
272 + return id
273 + }
274 + return id[:12]
275 +}
276 +
277 +func formatContainerNames(names []string) string {
278 + if len(names) == 0 {
279 + return ""
280 + }
281 +
282 + clean := make([]string, 0, len(names))
283 + for _, name := range names {
284 + v := strings.TrimPrefix(strings.TrimSpace(name), "/")
285 + if v != "" {
286 + clean = append(clean, v)
287 + }
288 + }
289 + return strings.Join(clean, ", ")
290 +}
291 +
292 +func formatCreated(created int64, now time.Time) string {
293 + if created <= 0 {
294 + return ""
295 + }
296 + createdAt := time.Unix(created, 0)
297 + if now.Before(createdAt) {
298 + return "0 seconds ago"
299 + }
300 + return units.HumanDuration(now.Sub(createdAt)) + " ago"
301 +}
302 +
303 +func formatStatus(cntr typesContainer.Summary) string {
304 + if cntr.Status != "" {
305 + return cntr.Status
306 + }
307 +
308 + switch cntr.State {
309 + case "running":
310 + return "Up"
311 + case "paused":
312 + return "Paused"
313 + case "restarting":
314 + return "Restarting"
315 + case "removing":
316 + return "Removing"
317 + case "exited":
318 + return "Exited"
319 + case "dead":
320 + return "Dead"
321 + case "created":
322 + return "Created"
323 + default:
324 + return cntr.State
325 + }
326 +}
327 +
328 +func formatPorts(ports []typesContainer.Port) string {
329 + if len(ports) == 0 {
330 + return ""
331 + }
332 +
333 + clone := append([]types.Port(nil), ports...)
334 + sort.Slice(clone, func(i, j int) bool {
335 + if clone[i].PrivatePort != clone[j].PrivatePort {
336 + return clone[i].PrivatePort < clone[j].PrivatePort
337 + }
338 + if clone[i].PublicPort != clone[j].PublicPort {
339 + return clone[i].PublicPort < clone[j].PublicPort
340 + }
341 + if clone[i].IP != clone[j].IP {
342 + return clone[i].IP < clone[j].IP
343 + }
344 + return clone[i].Type < clone[j].Type
345 + })
346 +
347 + out := make([]string, 0, len(clone))
348 + for _, p := range clone {
349 + v := formatPort(p)
350 + if v != "" {
351 + out = append(out, v)
352 + }
353 + }
354 + return strings.Join(out, ", ")
355 +}
356 +
357 +func formatPort(p typesContainer.Port) string {
358 + proto := p.Type
359 + if proto == "" {
360 + proto = "tcp"
361 + }
362 +
363 + switch {
364 + case p.PublicPort > 0 && p.IP != "":
365 + return fmt.Sprintf("%s:%d->%d/%s", p.IP, p.PublicPort, p.PrivatePort, proto)
366 + case p.PublicPort > 0:
367 + return fmt.Sprintf("%d->%d/%s", p.PublicPort, p.PrivatePort, proto)
368 + case p.PrivatePort > 0:
369 + return fmt.Sprintf("%d/%s", p.PrivatePort, proto)
370 + default:
371 + return ""
372 + }
373 +}
src/go/plugin/go.d/collector/docker/dockerfunc/containers_test.go new
+141
@@ -0,0 +1,141 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +package dockerfunc
4 +
5 +import (
6 + "context"
7 + "errors"
8 + "testing"
9 + "time"
10 +
11 + "github.com/docker/docker/api/types"
12 + "github.com/stretchr/testify/assert"
13 + "github.com/stretchr/testify/require"
14 +)
15 +
16 +func TestFuncContainers_HandleUnavailable(t *testing.T) {
17 + r := newRouter(mockDeps{err: errors.New("collector docker client is not ready")})
18 +
19 + resp := r.Handle(context.Background(), containersMethodID, nil)
20 +
21 + require.NotNil(t, resp)
22 + assert.Equal(t, 503, resp.Status)
23 + assert.Contains(t, resp.Message, "initializing")
24 +}
25 +
26 +func TestFuncContainers_HandleTimeout(t *testing.T) {
27 + r := newRouter(mockDeps{
28 + client: mockDockerClient{listErr: context.DeadlineExceeded},
29 + })
30 +
31 + resp := r.Handle(context.Background(), containersMethodID, nil)
32 +
33 + require.NotNil(t, resp)
34 + assert.Equal(t, 504, resp.Status)
35 +}
36 +
37 +func TestFuncContainers_HandleSuccess(t *testing.T) {
38 + now := time.Now()
39 + newerCreated := now.Add(-2 * time.Hour).Unix()
40 + olderCreated := now.Add(-24 * time.Hour).Unix()
41 +
42 + r := newRouter(mockDeps{
43 + client: mockDockerClient{containers: []types.Container{
44 + {
45 + ID: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
46 + Image: "postgres:16",
47 + Command: "docker-entrypoint.sh postgres",
48 + Created: olderCreated,
49 + Status: "Exited (0) 4 days ago",
50 + State: "exited",
51 + Ports: []types.Port{
52 + {IP: "[::]", PrivatePort: 5432, PublicPort: 5432, Type: "tcp"},
53 + {IP: "0.0.0.0", PrivatePort: 5432, PublicPort: 5432, Type: "tcp"},
54 + },
55 + Names: []string{"/postgres-dev"},
56 + },
57 + {
58 + ID: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
59 + Image: "almalinux:8",
60 + Command: "sleep infinity",
61 + Created: newerCreated,
62 + State: "exited",
63 + Names: []string{"/alma8"},
64 + },
65 + }},
66 + })
67 +
68 + resp := r.Handle(context.Background(), containersMethodID, nil)
69 +
70 + require.NotNil(t, resp)
71 + assert.Equal(t, 200, resp.Status)
72 + assert.Equal(t, containersColCreatedUnix, resp.DefaultSortColumn)
73 + assert.Equal(t, containersMethodHelp, resp.Help)
74 +
75 + columns := resp.Columns
76 + require.NotNil(t, columns)
77 + for _, key := range []string{
78 + containersColID,
79 + containersColImage,
80 + containersColCommand,
81 + containersColCreated,
82 + containersColStatus,
83 + containersColState,
84 + containersColPorts,
85 + containersColNames,
86 + containersColIDFull,
87 + containersColCreatedUnix,
88 + } {
89 + _, ok := columns[key]
90 + assert.True(t, ok, "missing column %s", key)
91 + }
92 +
93 + data, ok := resp.Data.([][]any)
94 + require.True(t, ok)
95 + require.Len(t, data, 2)
96 +
97 + // sorted by Created desc: "alma8" first
98 + assert.Equal(t, "alma8", data[0][colNames])
99 + assert.Equal(t, newerCreated, data[0][colCreatedUnix])
100 + assert.Equal(t, "bbbbbbbbbbbb", data[0][colContainerID])
101 + assert.Contains(t, data[0][colCreated].(string), "ago")
102 + assert.Equal(t, "sleep infinity", data[0][colCommand])
103 + assert.Equal(t, "Exited", data[0][colStatus])
104 + assert.Equal(t, "exited", data[0][colState])
105 +
106 + // second row includes formatted ports and truncated command
107 + assert.Equal(t, "postgres-dev", data[1][colNames])
108 + assert.Equal(t, "0.0.0.0:5432->5432/tcp, [::]:5432->5432/tcp", data[1][colPorts])
109 + assert.Equal(t, "aaaaaaaaaaaa", data[1][colContainerID])
110 + assert.Equal(t, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", data[1][colContainerIDFull])
111 + assert.Equal(t, "docker-entrypoint.sh postgres", data[1][colCommand])
112 + assert.Equal(t, "exited", data[1][colState])
113 +}
114 +
115 +func TestFormatHelpers(t *testing.T) {
116 + t.Run("shortContainerID", func(t *testing.T) {
117 + assert.Equal(t, "123456789012", shortContainerID("1234567890123456"))
118 + assert.Equal(t, "abc", shortContainerID("abc"))
119 + })
120 +
121 + t.Run("formatContainerNames", func(t *testing.T) {
122 + assert.Equal(t, "a, b", formatContainerNames([]string{"/a", " /b "}))
123 + assert.Equal(t, "", formatContainerNames(nil))
124 + })
125 +
126 + t.Run("formatCreated", func(t *testing.T) {
127 + now := time.Unix(1_730_000_000, 0)
128 + assert.Equal(t, "", formatCreated(0, now))
129 + assert.Contains(t, formatCreated(now.Add(-48*time.Hour).Unix(), now), "ago")
130 + })
131 +
132 + t.Run("formatPorts", func(t *testing.T) {
133 + ports := []types.Port{
134 + {IP: "[::]", PrivatePort: 5432, PublicPort: 5432, Type: "tcp"},
135 + {IP: "0.0.0.0", PrivatePort: 5432, PublicPort: 5432, Type: "tcp"},
136 + {PrivatePort: 80, Type: "tcp"},
137 + }
138 + assert.Equal(t, "80/tcp, 0.0.0.0:5432->5432/tcp, [::]:5432->5432/tcp", formatPorts(ports))
139 + assert.Equal(t, "", formatPorts(nil))
140 + })
141 +}
src/go/plugin/go.d/collector/docker/dockerfunc/deps.go new
+22
@@ -0,0 +1,22 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +package dockerfunc
4 +
5 +import (
6 + "context"
7 +
8 + "github.com/docker/docker/api/types"
9 + typesContainer "github.com/docker/docker/api/types/container"
10 + typesImage "github.com/docker/docker/api/types/image"
11 +)
12 +
13 +// DockerClient defines the minimal Docker API surface needed by function handlers.
14 +type DockerClient interface {
15 + ContainerList(context.Context, typesContainer.ListOptions) ([]types.Container, error)
16 + ImageList(context.Context, typesImage.ListOptions) ([]typesImage.Summary, error)
17 +}
18 +
19 +// Deps provides runtime dependencies needed by Docker function handlers.
20 +type Deps interface {
21 + DockerClient() (DockerClient, error)
22 +}
src/go/plugin/go.d/collector/docker/dockerfunc/router.go new
+59
@@ -0,0 +1,59 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +package dockerfunc
4 +
5 +import (
6 + "context"
7 + "fmt"
8 +
9 + "github.com/netdata/netdata/go/plugins/pkg/funcapi"
10 +)
11 +
12 +// router routes method calls to appropriate function handlers.
13 +type router struct {
14 + deps Deps
15 +
16 + handlers map[string]funcapi.MethodHandler
17 +}
18 +
19 +func newRouter(deps Deps) *router {
20 + r := &router{
21 + deps: deps,
22 + handlers: make(map[string]funcapi.MethodHandler),
23 + }
24 + r.handlers[containersMethodID] = newFuncContainers(r)
25 + return r
26 +}
27 +
28 +// Compile-time interface check.
29 +var _ funcapi.MethodHandler = (*router)(nil)
30 +
31 +func (r *router) MethodParams(ctx context.Context, method string) ([]funcapi.ParamConfig, error) {
32 + if h, ok := r.handlers[method]; ok {
33 + return h.MethodParams(ctx, method)
34 + }
35 + return nil, fmt.Errorf("unknown method: %s", method)
36 +}
37 +
38 +func (r *router) Handle(ctx context.Context, method string, params funcapi.ResolvedParams) *funcapi.FunctionResponse {
39 + if h, ok := r.handlers[method]; ok {
40 + return h.Handle(ctx, method, params)
41 + }
42 + return funcapi.NotFoundResponse(method)
43 +}
44 +
45 +func (r *router) Cleanup(ctx context.Context) {
46 + for _, h := range r.handlers {
47 + h.Cleanup(ctx)
48 + }
49 +}
50 +
51 +func Methods() []funcapi.MethodConfig {
52 + return []funcapi.MethodConfig{
53 + containersMethodConfig(),
54 + }
55 +}
56 +
57 +func NewRouter(deps Deps) funcapi.MethodHandler {
58 + return newRouter(deps)
59 +}
src/go/plugin/go.d/collector/docker/dockerfunc/router_test.go new
+68
@@ -0,0 +1,68 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +package dockerfunc
4 +
5 +import (
6 + "context"
7 + "errors"
8 + "testing"
9 +
10 + typesContainer "github.com/docker/docker/api/types/container"
11 + typesImage "github.com/docker/docker/api/types/image"
12 + "github.com/stretchr/testify/assert"
13 + "github.com/stretchr/testify/require"
14 +)
15 +
16 +type mockDeps struct {
17 + client DockerClient
18 + err error
19 +}
20 +
21 +func (m mockDeps) DockerClient() (DockerClient, error) {
22 + if m.err != nil {
23 + return nil, m.err
24 + }
25 + if m.client == nil {
26 + return nil, errors.New("collector docker client is not ready")
27 + }
28 + return m.client, nil
29 +}
30 +
31 +type mockDockerClient struct {
32 + containers []typesContainer.Summary
33 + listErr error
34 +}
35 +
36 +func (m mockDockerClient) ContainerList(ctx context.Context, opts typesContainer.ListOptions) ([]typesContainer.Summary, error) {
37 + if m.listErr != nil {
38 + return nil, m.listErr
39 + }
40 + return m.containers, nil
41 +}
42 +
43 +func (m mockDockerClient) ImageList(ctx context.Context, opts typesImage.ListOptions) ([]typesImage.Summary, error) {
44 + return nil, nil
45 +}
46 +
47 +func TestDockerMethods(t *testing.T) {
48 + methods := Methods()
49 +
50 + require.Len(t, methods, 1)
51 + assert.Equal(t, containersMethodID, methods[0].ID)
52 + assert.Equal(t, "Containers", methods[0].Name)
53 + assert.Empty(t, methods[0].RequiredParams)
54 +}
55 +
56 +func TestRouter_NotFound(t *testing.T) {
57 + r := newRouter(mockDeps{client: mockDockerClient{}})
58 + resp := r.Handle(context.Background(), "unknown-method", nil)
59 + require.NotNil(t, resp)
60 + assert.Equal(t, 404, resp.Status)
61 +}
62 +
63 +func TestRouter_MethodParamsUnknownMethod(t *testing.T) {
64 + r := newRouter(mockDeps{client: mockDockerClient{}})
65 + _, err := r.MethodParams(context.Background(), "unknown-method")
66 + require.Error(t, err)
67 + assert.Contains(t, err.Error(), "unknown method")
68 +}
src/go/plugin/go.d/collector/docker/func_deps.go new
+22
@@ -0,0 +1,22 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +package docker
4 +
5 +import (
6 + "errors"
7 +
8 + "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/docker/dockerfunc"
9 +)
10 +
11 +var errDockerClientNotReady = errors.New("collector docker client is not ready")
12 +
13 +type funcDepsAdapter struct {
14 + collector *Collector
15 +}
16 +
17 +func (a funcDepsAdapter) DockerClient() (dockerfunc.DockerClient, error) {
18 + if a.collector.client == nil {
19 + return nil, errDockerClientNotReady
20 + }
21 + return a.collector.client, nil
22 +}
src/go/plugin/go.d/collector/docker/metadata.yaml
+69
@@ -128,6 +128,75 @@ modules:
128 metric: docker.container_health_status
129 info: ${label:container_name} docker container health status is unhealthy
130 link: https://github.com/netdata/netdata/blob/master/src/health/health.d/docker.conf
131 + functions:
132 + description: |
133 + This collector exposes real-time functions for interactive troubleshooting in the Top tab.
134 + list:
135 + - id: container-ls
136 + name: Containers
137 + description: |
138 + Retrieves container list data equivalent to `docker ps -a`.
139 +
140 + This function calls the Docker Container List API with `all=true` and returns both running and non-running containers in a table similar to Docker CLI output.
141 +
142 + Use cases:
143 + - Quickly inspect all containers (running, exited, paused, dead) from Netdata
144 + - Correlate container lifecycle with metric changes and alerts
145 + - Verify exposed ports, image tags, and container names without shell access
146 + parameters: []
147 + returns:
148 + description: Container inventory from Docker Engine. Each row represents one container returned by `docker ps -a`.
149 + columns:
150 + - name: CONTAINER ID
151 + type: string
152 + unit: ""
153 + description: Short container ID (12 characters).
154 + - name: IMAGE
155 + type: string
156 + unit: ""
157 + description: Container image reference.
158 + - name: COMMAND
159 + type: string
160 + unit: ""
161 + description: Container command as reported by Docker API.
162 + - name: CREATED
163 + type: string
164 + unit: ""
165 + description: Human-readable container creation age (for example, '5 days ago').
166 + - name: STATUS
167 + type: string
168 + unit: ""
169 + description: Docker status string (for example, 'Up 3 weeks' or 'Exited (0) 4 weeks ago').
170 + - name: State (Raw)
171 + type: string
172 + unit: ""
173 + visibility: hidden
174 + description: Raw Docker state value (for example, running, exited, paused, restarting, dead).
175 + - name: PORTS
176 + type: string
177 + unit: ""
178 + description: Published or exposed ports summary.
179 + - name: NAMES
180 + type: string
181 + unit: ""
182 + description: Container name.
183 + - name: Container ID (Full)
184 + type: string
185 + unit: ""
186 + visibility: hidden
187 + description: Full 64-character container ID.
188 + - name: Created (Unix)
189 + type: integer
190 + unit: "seconds"
191 + visibility: hidden
192 + description: Container creation timestamp in Unix seconds.
193 + performance: |
194 + Executes a single Docker API request (`ContainerList` with `all=true`):<br/>• No per-container inspect requests are issued<br/>• Response size grows with total container count<br/>• Large histories with many stopped containers may return more rows
195 + security: |
196 + Exposes container metadata that may include sensitive details:<br/>• Container command text may include runtime arguments<br/>• Image names, ports, and container names are visible<br/>• Restrict access to authorized operators
197 + availability: |
198 + Available when:<br/>• Docker collector is initialized and connected<br/>• Docker API list-containers request succeeds<br/>• Returns HTTP 503 while collector is initializing<br/>• Returns HTTP 500 on Docker API errors<br/>• Returns HTTP 504 on timeout
199 + require_cloud: true
200 metrics:
201 folding:
202 title: Metrics