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