master
go 797 lines 21.6 KB
Raw
1 // docgen - Automated documentation generator for ibm.d modules
2 package main
3
4 import (
5 "encoding/json"
6 "flag"
7 "fmt"
8 "log"
9 "os"
10 "path/filepath"
11 "strings"
12 "text/template"
13
14 "gopkg.in/yaml.v3"
15 )
16
17 // ModuleInfo contains metadata about a module
18 type ModuleInfo struct {
19 Name string
20 DisplayName string
21 Description string
22 Icon string
23 Categories []string
24 Link string
25 }
26
27 // Config represents the YAML structure from contexts.yaml
28 type Config struct {
29 Classes map[string]Class `yaml:",inline"`
30 }
31
32 type Class struct {
33 Labels []string `yaml:"labels"`
34 Contexts []Context `yaml:"contexts"`
35 }
36
37 type Context struct {
38 Name string `yaml:"name"`
39 Context string `yaml:"context"`
40 Family string `yaml:"family"`
41 Title string `yaml:"title"`
42 Units string `yaml:"units"`
43 Type string `yaml:"type"`
44 Priority int `yaml:"priority"`
45 UpdateEvery int `yaml:"update_every"`
46 Dimensions []Dimension `yaml:"dimensions"`
47 }
48
49 type Dimension struct {
50 Name string `yaml:"name"`
51 Algorithm string `yaml:"algo"`
52 Mul int `yaml:"mul"`
53 Div int `yaml:"div"`
54 Precision int `yaml:"precision"`
55 }
56
57 // ConfigField represents a configuration field for schema generation
58 type ConfigField struct {
59 Name string
60 JSONName string
61 Type string
62 Title string
63 ItemsType string
64 Required bool
65 Default any
66 Description string
67 Format string
68 Minimum *int
69 Maximum *int
70 Examples []string
71 Pointer bool
72 Enum []string
73 GoType string
74 UIGroup string
75 UIWidget string
76 UIHelp string
77 UIPlaceholder string
78 }
79
80 func main() {
81 var (
82 module = flag.String("module", "", "Module name (required)")
83 contextFile = flag.String("contexts", "contexts/contexts.yaml", "Path to contexts.yaml")
84 configFile = flag.String("config", "config.go", "Path to config.go")
85 outputDir = flag.String("output", ".", "Output directory")
86 moduleInfo = flag.String("module-info", "module.yaml", "Module info file")
87 )
88 flag.Parse()
89
90 if *module == "" {
91 log.Fatal("module name is required")
92 }
93
94 generator := &DocGenerator{
95 ModuleName: *module,
96 ContextFile: *contextFile,
97 ConfigFile: *configFile,
98 OutputDir: *outputDir,
99 ModuleInfo: *moduleInfo,
100 }
101
102 if err := generator.Generate(); err != nil {
103 log.Fatalf("failed to generate documentation: %v", err)
104 }
105
106 log.Printf("Generated documentation for module '%s'", *module)
107 }
108
109 type DocGenerator struct {
110 ModuleName string
111 ContextFile string
112 ConfigFile string
113 OutputDir string
114 ModuleInfo string
115 consts map[string]any
116 hasHTTPConfig bool
117 }
118
119 func (g *DocGenerator) Generate() error {
120 // Parse contexts.yaml
121 contexts, err := g.parseContexts()
122 if err != nil {
123 return fmt.Errorf("failed to parse contexts: %w", err)
124 }
125
126 // Parse module info
127 moduleInfo, err := g.parseModuleInfo()
128 if err != nil {
129 return fmt.Errorf("failed to parse module info: %w", err)
130 }
131
132 // Parse config.go for schema generation
133 configFields, err := g.parseConfig()
134 if err != nil {
135 return fmt.Errorf("failed to parse config: %w", err)
136 }
137
138 // Generate metadata.yaml
139 if err := g.generateMetadata(contexts, moduleInfo); err != nil {
140 return fmt.Errorf("failed to generate metadata.yaml: %w", err)
141 }
142
143 // Generate config_schema.json
144 if err := g.generateConfigSchema(configFields); err != nil {
145 return fmt.Errorf("failed to generate config_schema.json: %w", err)
146 }
147
148 // Generate README.md
149 if err := g.generateReadme(contexts, moduleInfo, configFields); err != nil {
150 return fmt.Errorf("failed to generate README.md: %w", err)
151 }
152
153 return nil
154 }
155
156 func (g *DocGenerator) parseContexts() (*Config, error) {
157 data, err := os.ReadFile(g.ContextFile)
158 if err != nil {
159 return nil, err
160 }
161
162 var config Config
163 if err := yaml.Unmarshal(data, &config.Classes); err != nil {
164 return nil, err
165 }
166
167 return &config, nil
168 }
169
170 func (g *DocGenerator) parseModuleInfo() (*ModuleInfo, error) {
171 // Try to read module.yaml file with module-specific info
172 if _, err := os.Stat(g.ModuleInfo); os.IsNotExist(err) {
173 // Create default module info
174 return &ModuleInfo{
175 Name: g.ModuleName,
176 DisplayName: strings.Title(g.ModuleName),
177 Description: fmt.Sprintf("Monitor %s metrics", strings.Title(g.ModuleName)),
178 Icon: "icon.svg",
179 Categories: []string{"data-collection.generic"},
180 Link: "https://example.com",
181 }, nil
182 }
183
184 data, err := os.ReadFile(g.ModuleInfo)
185 if err != nil {
186 return nil, err
187 }
188
189 var info struct {
190 Name string `yaml:"name"`
191 DisplayName string `yaml:"display_name"`
192 Description string `yaml:"description"`
193 Icon string `yaml:"icon"`
194 Categories []string `yaml:"categories"`
195 Link string `yaml:"link"`
196 }
197 if err := yaml.Unmarshal(data, &info); err != nil {
198 return nil, err
199 }
200
201 return &ModuleInfo{
202 Name: info.Name,
203 DisplayName: info.DisplayName,
204 Description: info.Description,
205 Icon: info.Icon,
206 Categories: info.Categories,
207 Link: info.Link,
208 }, nil
209 }
210
211 func (g *DocGenerator) parseConfig() ([]ConfigField, error) {
212 // Try to parse the actual Go file - this MUST succeed
213 fields, defaults, err := g.parseConfigFromGoFile()
214 if err != nil {
215 // FAIL HARD - no fallback for validation errors
216 return nil, fmt.Errorf("config validation failed: %w", err)
217 }
218
219 // If we got no fields from parsing, this is also an error
220 if len(fields) == 0 {
221 return nil, fmt.Errorf("no configuration fields found in config.go")
222 }
223
224 // Ensure standard fields are present even if they come from embedded structs
225 ensureField := func(name string, desc string, defaultKey string, fallback any, fieldType string, uiGroup string, uiWidget string, min *int, max *int) {
226 for _, f := range fields {
227 if f.JSONName == name {
228 return
229 }
230 }
231
232 defaultVal := fallback
233 if defaultKey != "" && defaults != nil {
234 if val, ok := defaults[defaultKey]; ok {
235 defaultVal = val
236 }
237 }
238 fields = append([]ConfigField{ConfigField{
239 Name: name,
240 JSONName: name,
241 Type: fieldType,
242 Title: formatTitleFromJSONName(name),
243 Required: false,
244 Default: defaultVal,
245 Description: desc,
246 Minimum: min,
247 Maximum: max,
248 GoType: fieldType,
249 UIGroup: uiGroup,
250 UIWidget: uiWidget,
251 }}, fields...)
252 }
253
254 ensureField("update_every", "Data collection frequency", "UpdateEvery", 10, "integer", "Connection", "", new(1), nil)
255
256 if g.hasHTTPConfig {
257 ensureField("url", "Target URL", "", "", "string", "Connection", "", nil, nil)
258 ensureField("username", "Username for authentication", "", "", "string", "Connection", "", nil, nil)
259 ensureField("password", "Password for authentication", "", "", "string", "Connection", "password", nil, nil)
260 ensureField("timeout", "Request timeout in seconds", "HTTPConfig.ClientConfig.Timeout", 0, "integer", "Connection", "", nil, nil)
261 ensureField("not_follow_redirects", "Disable HTTP redirects", "", false, "boolean", "HTTP", "", nil, nil)
262 ensureField("proxy_url", "Proxy URL", "", "", "string", "HTTP", "", nil, nil)
263 ensureField("proxy_username", "Proxy username", "", "", "string", "HTTP", "", nil, nil)
264 ensureField("proxy_password", "Proxy password", "", "", "string", "HTTP", "password", nil, nil)
265 ensureField("headers", "Custom headers", "", nil, "object", "HTTP", "", nil, nil)
266 ensureField("tls_skip_verify", "Skip TLS certificate verification", "", false, "boolean", "HTTP", "", nil, nil)
267 ensureField("tls_ca", "Custom CA bundle path", "", "", "string", "HTTP", "", nil, nil)
268 ensureField("tls_cert", "Client certificate path", "", "", "string", "HTTP", "", nil, nil)
269 ensureField("tls_key", "Client key path", "", "", "string", "HTTP", "", nil, nil)
270 }
271
272 return fields, nil
273 }
274
275 func (g *DocGenerator) getFallbackConfigFields() []ConfigField {
276 return []ConfigField{
277 {
278 Name: "update_every",
279 JSONName: "update_every",
280 Type: "integer",
281 Title: formatTitleFromJSONName("update_every"),
282 Required: false,
283 Default: 10,
284 Description: "Data collection frequency",
285 Minimum: new(1),
286 },
287 {
288 Name: "reset_statistics",
289 JSONName: "reset_statistics",
290 Type: "boolean",
291 Title: formatTitleFromJSONName("reset_statistics"),
292 Required: false,
293 Default: false,
294 Description: "ResetStatistics enables SQL calls that reset IBM i system statistics on each run.",
295 },
296 {
297 Name: "endpoint",
298 JSONName: "endpoint",
299 Type: "string",
300 Title: formatTitleFromJSONName("endpoint"),
301 Required: false,
302 Default: "dummy://localhost",
303 Description: "Connection endpoint",
304 Examples: []string{"dummy://localhost", "tcp://server:1414"},
305 },
306 {
307 Name: "connect_timeout",
308 JSONName: "connect_timeout",
309 Type: "integer",
310 Title: formatTitleFromJSONName("connect_timeout"),
311 Required: false,
312 Default: 5,
313 Description: "Connection timeout in seconds",
314 Minimum: new(1),
315 Maximum: new(300),
316 },
317 {
318 Name: "collect_items",
319 JSONName: "collect_items",
320 Type: "boolean",
321 Title: formatTitleFromJSONName("collect_items"),
322 Required: false,
323 Default: true,
324 Description: "Enable collection of item metrics",
325 },
326 {
327 Name: "max_items",
328 JSONName: "max_items",
329 Type: "integer",
330 Title: formatTitleFromJSONName("max_items"),
331 Required: false,
332 Default: 10,
333 Description: "Maximum number of items to collect",
334 Minimum: new(1),
335 Maximum: new(1000),
336 },
337 }
338 }
339
340 //go:fix inline
341 func intPtr(i int) *int {
342 return new(i)
343 }
344
345 func indent(spaces int, text string) string {
346 if text == "" {
347 return ""
348 }
349 prefix := strings.Repeat(" ", spaces)
350 lines := strings.Split(text, "\n")
351 for i, line := range lines {
352 if line == "" {
353 lines[i] = ""
354 } else {
355 lines[i] = prefix + line
356 }
357 }
358 return strings.Join(lines, "\n")
359 }
360
361 func (g *DocGenerator) generateMetadata(contexts *Config, moduleInfo *ModuleInfo) error {
362 tmpl := template.Must(template.New("metadata").Funcs(template.FuncMap{
363 "lower": strings.ToLower,
364 "title": strings.Title,
365 "indent": indent,
366 }).Parse(metadataTemplate))
367
368 // Prepare template data
369 data := struct {
370 ModuleInfo *ModuleInfo
371 Contexts *Config
372 ModuleName string
373 }{
374 ModuleInfo: moduleInfo,
375 Contexts: contexts,
376 ModuleName: g.ModuleName,
377 }
378
379 // Create output file
380 outFile := filepath.Join(g.OutputDir, "metadata.yaml")
381 file, err := os.Create(outFile)
382 if err != nil {
383 return err
384 }
385 defer file.Close()
386
387 return tmpl.Execute(file, data)
388 }
389
390 func (g *DocGenerator) generateConfigSchema(fields []ConfigField) error {
391 // Create schema manually to avoid template issues
392 schema := map[string]any{
393 "jsonSchema": map[string]any{
394 "$schema": "http://json-schema.org/draft-07/schema#",
395 "title": fmt.Sprintf("%s collector configuration", g.ModuleName),
396 "type": "object",
397 },
398 }
399
400 properties := make(map[string]any)
401 var required []string
402 groupOrder := make([]string, 0)
403 groupFields := make(map[string][]string)
404 fieldUIOptions := make(map[string]map[string]any)
405
406 for _, field := range fields {
407 prop := map[string]any{
408 "title": field.Title,
409 "type": field.Type,
410 }
411 if field.Description != "" {
412 prop["description"] = field.Description
413 }
414
415 if field.Type == "array" {
416 itemsType := field.ItemsType
417 if itemsType == "" {
418 itemsType = "string"
419 }
420 prop["items"] = map[string]any{
421 "type": itemsType,
422 }
423 }
424
425 if field.Default != nil {
426 prop["default"] = field.Default
427 }
428
429 if field.Format != "" {
430 prop["format"] = field.Format
431 }
432
433 if field.Minimum != nil {
434 prop["minimum"] = *field.Minimum
435 }
436
437 if field.Maximum != nil {
438 prop["maximum"] = *field.Maximum
439 }
440
441 if len(field.Examples) > 0 {
442 prop["examples"] = field.Examples
443 }
444
445 if len(field.Enum) > 0 {
446 prop["enum"] = field.Enum
447 }
448
449 properties[field.JSONName] = prop
450
451 if field.Required {
452 required = append(required, field.JSONName)
453 }
454
455 group := field.UIGroup
456 if group == "" {
457 group = "Advanced"
458 }
459 if _, exists := groupFields[group]; !exists {
460 groupOrder = append(groupOrder, group)
461 }
462 groupFields[group] = append(groupFields[group], field.JSONName)
463
464 opts, exists := fieldUIOptions[field.JSONName]
465 if !exists {
466 opts = make(map[string]any)
467 }
468 if field.UIWidget != "" {
469 opts["ui:widget"] = field.UIWidget
470 }
471 if field.UIHelp != "" {
472 opts["ui:help"] = field.UIHelp
473 }
474 if field.UIPlaceholder != "" {
475 opts["ui:placeholder"] = field.UIPlaceholder
476 }
477 if field.Type == "array" {
478 opts["ui:listFlavour"] = "list"
479 }
480 if len(opts) > 0 {
481 fieldUIOptions[field.JSONName] = opts
482 }
483 }
484
485 schema["jsonSchema"].(map[string]any)["properties"] = properties
486 if len(required) > 0 {
487 schema["jsonSchema"].(map[string]any)["required"] = required
488 }
489
490 uiSchema := map[string]any{
491 "uiOptions": map[string]any{
492 "fullPage": true,
493 },
494 }
495 uiSchema["ui:flavour"] = "tabs"
496
497 if len(groupOrder) > 0 {
498 tabs := make([]map[string]any, 0, len(groupOrder))
499 for _, group := range groupOrder {
500 fieldsForGroup := groupFields[group]
501 if len(fieldsForGroup) == 0 {
502 continue
503 }
504 tabs = append(tabs, map[string]any{
505 "title": group,
506 "fields": fieldsForGroup,
507 })
508 }
509 if len(tabs) > 0 {
510 uiSchema["ui:options"] = map[string]any{
511 "tabs": tabs,
512 }
513 }
514 }
515
516 for fieldName, opts := range fieldUIOptions {
517 uiSchema[fieldName] = opts
518 }
519
520 schema["uiSchema"] = uiSchema
521
522 // Marshal to JSON with proper formatting
523 data, err := json.MarshalIndent(schema, "", " ")
524 if err != nil {
525 return err
526 }
527
528 // Create output file
529 outFile := filepath.Join(g.OutputDir, "config_schema.json")
530 return os.WriteFile(outFile, data, 0644)
531 }
532
533 func (g *DocGenerator) generateReadme(contexts *Config, moduleInfo *ModuleInfo, fields []ConfigField) error {
534 tmpl := template.Must(template.New("readme").Funcs(template.FuncMap{
535 "lower": strings.ToLower,
536 "title": strings.Title,
537 }).Parse(readmeTemplate))
538
539 // Prepare template data
540 data := struct {
541 ModuleInfo *ModuleInfo
542 Contexts *Config
543 Fields []ConfigField
544 ModuleName string
545 }{
546 ModuleInfo: moduleInfo,
547 Contexts: contexts,
548 Fields: fields,
549 ModuleName: g.ModuleName,
550 }
551
552 // Create output file
553 outFile := filepath.Join(g.OutputDir, "README.md")
554 file, err := os.Create(outFile)
555 if err != nil {
556 return err
557 }
558 defer file.Close()
559
560 return tmpl.Execute(file, data)
561 }
562
563 const metadataTemplate = `# Generated metadata.yaml for {{.ModuleName}} module
564 plugin_name: ibm.d.plugin
565 modules:
566 - meta:
567 plugin_name: ibm.d.plugin
568 module_name: {{.ModuleName}}
569 monitored_instance:
570 name: {{.ModuleInfo.DisplayName}}
571 link: {{.ModuleInfo.Link}}
572 categories:{{range .ModuleInfo.Categories}}
573 - {{.}}{{end}}
574 icon_filename: "{{.ModuleInfo.Icon}}"
575 related_resources:
576 integrations:
577 list: []
578 info_provided_to_referring_integrations:
579 description: ""
580 keywords:
581 - {{.ModuleName}}
582 most_popular: false
583 overview:
584 data_collection:
585 metrics_description: |
586 {{ indent 10 .ModuleInfo.Description }}
587 method_description: |
588 {{ indent 10 (printf "The collector connects to %s and collects metrics via its monitoring interface." .ModuleInfo.DisplayName) }}
589 supported_platforms:
590 include: []
591 exclude: []
592 multi_instance: true
593 additional_permissions:
594 description: ""
595 default_behavior:
596 auto_detection:
597 description: ""
598 limits:
599 description: ""
600 performance_impact:
601 description: ""
602 setup:
603 prerequisites:
604 list:
605 - title: Enable monitoring interface
606 description: |
607 Ensure the {{.ModuleInfo.DisplayName}} monitoring interface is accessible.
608 configuration:
609 file:
610 name: ibm.d/{{.ModuleName}}.conf
611 options:
612 description: |
613 Configuration options for the {{.ModuleName}} collector.
614 folding:
615 title: Config options
616 enabled: true
617 list:
618 - name: update_every
619 description: Data collection frequency.
620 default_value: 1
621 required: false
622 - name: endpoint
623 description: Connection endpoint.
624 default_value: "dummy://localhost"
625 required: false
626 examples:
627 folding:
628 enabled: true
629 title: Config
630 list:
631 - name: Basic
632 description: Basic configuration example.
633 config: |
634 jobs:
635 - name: local
636 endpoint: dummy://localhost
637 troubleshooting:
638 problems:
639 list: []
640 alerts: []
641 metrics:
642 folding:
643 title: Metrics
644 enabled: false
645 description: ""
646 availability: []
647 scopes:{{range $className, $class := .Contexts.Classes}}{{if not $class.Labels}}
648 - name: global
649 description: These metrics refer to the entire monitored instance.
650 labels: []
651 metrics:{{range $class.Contexts}}
652 - name: {{.Context}}
653 description: {{.Title}}
654 unit: {{.Units}}
655 chart_type: {{.Type}}
656 dimensions:{{range .Dimensions}}
657 - name: {{.Name}}{{end}}{{end}}{{else}}
658 - name: {{lower $className}}
659 description: These metrics refer to {{lower $className}} instances.
660 labels:{{range $class.Labels}}
661 - name: {{.}}
662 description: {{title .}} identifier{{end}}
663 metrics:{{range $class.Contexts}}
664 - name: {{.Context}}
665 description: {{.Title}}
666 unit: {{.Units}}
667 chart_type: {{.Type}}
668 dimensions:{{range .Dimensions}}
669 - name: {{.Name}}{{end}}{{end}}{{end}}{{end}}
670 `
671
672 const readmeTemplate = `# {{.ModuleInfo.DisplayName}} collector
673
674 ## Overview
675
676 {{.ModuleInfo.Description}}
677
678 This collector is part of the [Netdata](https://github.com/netdata/netdata) monitoring solution.
679
680 ## Collected metrics
681
682 Metrics grouped by scope.
683
684 The scope defines the instance that the metric belongs to. An instance is uniquely identified by a set of labels.
685
686 ### Per {{.ModuleInfo.DisplayName}} instance
687
688 {{range $className, $class := .Contexts.Classes}}{{if not $class.Labels}}
689 These metrics refer to the entire monitored {{$.ModuleInfo.DisplayName}} instance.
690
691 This scope has no labels.
692
693 Metrics:
694
695 | Metric | Dimensions | Unit |
696 |:-------|:-----------|:-----|
697 {{range $class.Contexts}}| {{.Context}} | {{range $i, $dim := .Dimensions}}{{if $i}}, {{end}}{{$dim.Name}}{{end}} | {{.Units}} |
698 {{end}}{{end}}{{end}}
699
700 {{range $className, $class := .Contexts.Classes}}{{if $class.Labels}}
701 ### Per {{lower $className}}
702
703 These metrics refer to individual {{lower $className}} instances.
704
705 Labels:
706
707 | Label | Description |
708 |:------|:------------|{{range $class.Labels}}
709 | {{.}} | {{title .}} identifier |{{end}}
710
711 Metrics:
712
713 | Metric | Dimensions | Unit |
714 |:-------|:-----------|:-----|
715 {{range $class.Contexts}}| {{.Context}} | {{range $i, $dim := .Dimensions}}{{if $i}}, {{end}}{{$dim.Name}}{{end}} | {{.Units}} |
716 {{end}}{{end}}{{end}}
717
718 ## Configuration
719
720 ### File
721
722 The configuration file name for this integration is ` + "`" + `ibm.d/{{.ModuleName}}.conf` + "`" + `.
723
724 You can edit the configuration file using the ` + "`" + `edit-config` + "`" + ` script from the
725 Netdata [config directory](https://github.com/netdata/netdata/blob/master/docs/netdata-agent/configuration.md#the-netdata-config-directory).
726
727 ` + "```" + `bash
728 cd /etc/netdata 2>/dev/null || cd /opt/netdata/etc/netdata
729 sudo ./edit-config ibm.d/{{.ModuleName}}.conf
730 ` + "```" + `
731
732 ### Options
733
734 The following options can be defined globally or per job.
735
736 | Name | Description | Default | Required | Min | Max |
737 |:-----|:------------|:--------|:---------|:----|:----|{{range .Fields}}
738 | {{.Name}} | {{.Description}} | ` + "`" + `{{.Default}}` + "`" + ` | {{if .Required}}yes{{else}}no{{end}} | {{if .Minimum}}{{.Minimum}}{{else}}-{{end}} | {{if .Maximum}}{{.Maximum}}{{else}}-{{end}} |{{end}}
739
740 ### Examples
741
742 #### Basic configuration
743
744 {{$.ModuleInfo.DisplayName}} monitoring with default settings.
745
746 <details>
747 <summary>Config</summary>
748
749 ` + "```" + `yaml
750 jobs:
751 - name: local
752 endpoint: dummy://localhost
753 ` + "```" + `
754
755 </details>
756
757 ## Troubleshooting
758
759 ### Debug Mode
760
761 To troubleshoot issues with the ` + "`" + `{{.ModuleName}}` + "`" + ` collector, run the ` + "`" + `ibm.d.plugin` + "`" + ` with the debug option enabled.
762 The output should give you clues as to why the collector isn't working.
763
764 - Navigate to the ` + "`" + `plugins.d` + "`" + ` directory, usually at ` + "`" + `/usr/libexec/netdata/plugins.d/` + "`" + `
765 - Switch to the ` + "`" + `netdata` + "`" + ` user
766 - Run the ` + "`" + `ibm.d.plugin` + "`" + ` to debug the collector:
767
768 ` + "```" + `bash
769 sudo -u netdata ./ibm.d.plugin -d -m {{.ModuleName}}
770 ` + "```" + `
771
772 ## Getting Logs
773
774 If you're encountering problems with the ` + "`" + `{{.ModuleName}}` + "`" + ` collector, follow these steps to retrieve logs and identify potential issues:
775
776 - **Run the command** specific to your system (systemd, non-systemd, or Docker container).
777 - **Examine the output** for any warnings or error messages that might indicate issues. These messages will typically provide clues about the root cause of the problem.
778
779 ### For systemd systems (most Linux distributions)
780
781 ` + "```" + `bash
782 sudo journalctl -u netdata --reverse | grep {{.ModuleName}}
783 ` + "```" + `
784
785 ### For non-systemd systems
786
787 ` + "```" + `bash
788 sudo grep {{.ModuleName}} /var/log/netdata/error.log
789 sudo grep {{.ModuleName}} /var/log/netdata/collector.log
790 ` + "```" + `
791
792 ### For Docker containers
793
794 ` + "```" + `bash
795 sudo docker logs netdata 2>&1 | grep {{.ModuleName}}
796 ` + "```" + `
797 `