From 01998a7c080818a4c1998422fe5fd8917b7a8013 Mon Sep 17 00:00:00 2001 From: Antoine Toulme Date: Fri, 28 Feb 2025 08:39:34 -0800 Subject: [PATCH] [builder] add doc generation --- .chloggen/generate_docs.yaml | 25 ++++ Makefile | 2 +- cmd/builder/internal/builder/config.go | 1 + cmd/builder/internal/builder/main.go | 17 +++ cmd/builder/internal/builder/templates.go | 7 ++ .../internal/builder/templates/doc.go.tmpl | 113 ++++++++++++++++++ .../internal/builder/templates/md.tmpl | 40 +++++++ cmd/builder/internal/command.go | 5 + cmd/otelcorecol/internal/doc/doc.go | 97 +++++++++++++++ cmd/otelcorecol/internal/doc/md.tmpl | 40 +++++++ 10 files changed, 346 insertions(+), 1 deletion(-) create mode 100644 .chloggen/generate_docs.yaml create mode 100644 cmd/builder/internal/builder/templates/doc.go.tmpl create mode 100644 cmd/builder/internal/builder/templates/md.tmpl create mode 100644 cmd/otelcorecol/internal/doc/doc.go create mode 100644 cmd/otelcorecol/internal/doc/md.tmpl diff --git a/.chloggen/generate_docs.yaml b/.chloggen/generate_docs.yaml new file mode 100644 index 00000000000..698cf56af6d --- /dev/null +++ b/.chloggen/generate_docs.yaml @@ -0,0 +1,25 @@ +# Use this changelog template to create an entry for release notes. + +# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix' +change_type: enhancement + +# The name of the component, or a single word describing the area of concern, (e.g. otlpreceiver) +component: builder + +# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`). +note: Add an option to the builder to generate an executable that can execute a text template of distribution documentation + +# One or more tracking issues or pull requests related to the change +issues: [12528] + +# (Optional) One or more lines of additional information to render under the primary note. +# These lines will be padded with 2 spaces and then inserted directly into the document. +# Use pipe (|) for multiline entries. +subtext: + +# Optional: The change log or logs in which this entry should be included. +# e.g. '[user]' or '[user, api]' +# Include 'user' if the change is relevant to end users. +# Include 'api' if there is a change to a library API. +# Default: '[user]' +change_logs: [] diff --git a/Makefile b/Makefile index aff77344719..425180352d1 100644 --- a/Makefile +++ b/Makefile @@ -161,7 +161,7 @@ otelcorecol: .PHONY: genotelcorecol genotelcorecol: install-tools - pushd cmd/builder/ && $(GOCMD) run ./ --skip-compilation --config ../otelcorecol/builder-config.yaml --output-path ../otelcorecol && popd + pushd cmd/builder/ && $(GOCMD) run ./ --skip-compilation --generate-doc-cmd --config ../otelcorecol/builder-config.yaml --output-path ../otelcorecol && popd $(MAKE) -C cmd/otelcorecol fmt .PHONY: ocb diff --git a/cmd/builder/internal/builder/config.go b/cmd/builder/internal/builder/config.go index 1b5e46435e9..2f801113175 100644 --- a/cmd/builder/internal/builder/config.go +++ b/cmd/builder/internal/builder/config.go @@ -39,6 +39,7 @@ type Config struct { GCFlags string `mapstructure:"-"` GCSet bool `mapstructure:"-"` // only used to override GCFlags Verbose bool `mapstructure:"-"` + GenerateDocCmd bool `mapstructure:"-"` Distribution Distribution `mapstructure:"dist"` Exporters []Module `mapstructure:"exporters"` diff --git a/cmd/builder/internal/builder/main.go b/cmd/builder/internal/builder/main.go index b6b1ddecc1d..197bbbbc282 100644 --- a/cmd/builder/internal/builder/main.go +++ b/cmd/builder/internal/builder/main.go @@ -84,6 +84,10 @@ func Generate(cfg *Config) error { return fmt.Errorf("failed to create output path: %w", err) } + if err := os.MkdirAll(filepath.Clean(filepath.Join(cfg.Distribution.OutputPath, "internal")), 0o750); err != nil { + return fmt.Errorf("failed to create source internal folder: %w", err) + } + for _, tmpl := range []*template.Template{ mainTemplate, mainOthersTemplate, @@ -96,6 +100,19 @@ func Generate(cfg *Config) error { } } + if cfg.GenerateDocCmd { + docFolder := filepath.Clean(filepath.Join(cfg.Distribution.OutputPath, "internal", "doc")) + if err := os.MkdirAll(docFolder, 0o750); err != nil { + return fmt.Errorf("failed to create source folder: %w", err) + } + if err := os.WriteFile(filepath.Join(docFolder, "md.tmpl"), mdBytes, 0o644); err != nil { + return fmt.Errorf("failed to write markdown doc template: %w", err) + } + if err := processAndWrite(cfg, docBytesTemplate, filepath.Join(docFolder, docBytesTemplate.Name()), cfg); err != nil { + return fmt.Errorf("failed to generate docs source file %q: %w", docBytesTemplate.Name(), err) + } + } + cfg.Logger.Info("Sources created", zap.String("path", cfg.Distribution.OutputPath)) return nil } diff --git a/cmd/builder/internal/builder/templates.go b/cmd/builder/internal/builder/templates.go index a916508d049..4752e364d79 100644 --- a/cmd/builder/internal/builder/templates.go +++ b/cmd/builder/internal/builder/templates.go @@ -28,6 +28,13 @@ var ( //go:embed templates/go.mod.tmpl goModBytes []byte goModTemplate = parseTemplate("go.mod", goModBytes) + + //go:embed templates/doc.go.tmpl + docBytes []byte + docBytesTemplate = parseTemplate("doc.go", docBytes) + + //go:embed templates/md.tmpl + mdBytes []byte ) func parseTemplate(name string, bytes []byte) *template.Template { diff --git a/cmd/builder/internal/builder/templates/doc.go.tmpl b/cmd/builder/internal/builder/templates/doc.go.tmpl new file mode 100644 index 00000000000..bfc5f30b65d --- /dev/null +++ b/cmd/builder/internal/builder/templates/doc.go.tmpl @@ -0,0 +1,113 @@ +// Code generated by "go.opentelemetry.io/collector/cmd/builder". DO NOT EDIT. + +// Program {{ .Distribution.Name }} is an OpenTelemetry Collector utility to generate docs. +package main + +import ( + _ "embed" + "text/template" + "go.opentelemetry.io/collector/component" + "go.opentelemetry.io/collector/confmap" + {{- range.ConfmapConverters }} + {{.Name}} "{{.Import}}" + {{- end }} + {{- range.ConfmapProviders }} + {{.Name}} "{{.Import}}" + {{- end }} + "{{.Distribution.Module}}/internal" + "go.opentelemetry.io/collector/otelcol" + {{- range .Connectors}} + {{.Name}} "{{.Import}}" + {{- end}} + {{- range .Exporters}} + {{.Name}} "{{.Import}}" + {{- end}} + {{- range .Extensions}} + {{.Name}} "{{.Import}}" + {{- end}} + {{- range .Processors}} + {{.Name}} "{{.Import}}" + {{- end}} + {{- range .Receivers}} + {{.Name}} "{{.Import}}" + {{- end}} +) + +//go:embed md.tmpl +var mdBytes []byte + +type ComponentInfo struct { + Name string + GoMod string + Factory component.Factory +} + +func (c ComponentInfo) URL() string { + return strings.Split(c.GoMod, " ")[0] +} + +func (c ComponentInfo) Version() string { + return strings.Split(c.GoMod, " ")[1] +} + +func main() { + buildInfo := component.BuildInfo{ + Command: "{{ .Distribution.Name }}", + Description: "{{ .Distribution.Description }}", + Version: "{{ .Distribution.Version }}", + } + + + + info := struct{ + BuildInfo component.BuildInfo + Connectors []ComponentInfo + Exporters []ComponentInfo + Extensions []ComponentInfo + Processors []ComponentInfo + Receivers []ComponentInfo + }{ + BuildInfo: buildInfo, + Connectors: []ComponentInfo{ + {{- range .Connectors}} + ComponentInfo{ Name: "{{.Name}}", GoMod: "{{.GoMod}}", Factory: {{.Name}}.NewFactory() }, + {{- end}} + }, + Exporters: []ComponentInfo{ + {{- range .Exporters}} + ComponentInfo{ Name: "{{.Name}}", GoMod: "{{.GoMod}}", Factory: {{.Name}}.NewFactory() }, + {{- end}} + }, + Extensions: []ComponentInfo{ + {{- range .Extensions}} + ComponentInfo{ Name: "{{.Name}}", GoMod: "{{.GoMod}}", Factory: {{.Name}}.NewFactory() }, + {{- end}} + }, + Processors: []ComponentInfo{ + {{- range .Processors}} + ComponentInfo{ Name: "{{.Name}}", GoMod: "{{.GoMod}}", Factory: {{.Name}}.NewFactory() }, + {{- end}} + }, + Receivers: []ComponentInfo{ + {{- range .Receivers}} + ComponentInfo{ Name: "{{.Name}}", GoMod: "{{.GoMod}}", Factory: {{.Name}}.NewFactory() }, + {{- end}} + }, + } + + templateBytes := mdBytes + if len(os.Args) > 1 { + templateFile := os.Args[1] + b, err := os.ReadFile(templateFile) + if err != nil { + panic(err) + } + templateBytes = b + } + + tmpl := template.Must(template.New("").Parse(string(templateBytes))) + err := tmpl.Execute(os.Stdout, info) + if err != nil { + panic(err) + } +} diff --git a/cmd/builder/internal/builder/templates/md.tmpl b/cmd/builder/internal/builder/templates/md.tmpl new file mode 100644 index 00000000000..889f91f3ad6 --- /dev/null +++ b/cmd/builder/internal/builder/templates/md.tmpl @@ -0,0 +1,40 @@ +# {{ .BuildInfo.Command }} {{ .BuildInfo.Version }} + +{{ .BuildInfo.Description }} + +| Connectors | Version | Traces To Traces | Traces To Metrics | Traces To Logs | Metrics To Traces | Metrics To Logs | Logs To Traces | Logs To Metrics | Logs To Logs | +|:---------------------------------------------------------------|:-----------------|:------------------|:---------------|:------------------|:-----------------|:---------------|:----------------|:-------------| +{{- range .Connectors }} +| [{{ .Factory.Type }}]({{ .URL }}) | {{.Version}} | {{ if .Factory.TracesToTracesStability }}[{{.Factory.TracesToTracesStability}}]{{ end }} | {{ if .Factory.TracesToMetricsStability }}[{{.Factory.TracesToMetricsStability}}]{{ end }} | {{ if .Factory.TracesToLogsStability }}[{{.Factory.TracesToLogsStability}}]{{ end }} | {{ if .Factory.MetricsToTracesStability }}[{{.Factory.MetricsToTracesStability}}]{{ end }} | {{ if .Factory.MetricsToMetricsStability }}[{{.Factory.MetricsToMetricsStability}}]{{ end }} | {{ if .Factory.MetricsToLogsStability }}[{{.Factory.MetricsToLogsStability}}]{{ end }} | {{ if .Factory.LogsToTracesStability }}[{{.Factory.LogsToTracesStability}}]{{ end }} | {{ if .Factory.LogsToMetricsStability }}[{{.Factory.LogsToMetricsStability}}]{{ end }} | {{ if .Factory.LogsToLogsStability }}[{{.Factory.LogsToLogsStability}}]{{ end }} | +{{- end }} + +| Exporters | Version | Logs | Metrics | Traces | +|:---------------------------------------------------------------|:---------------|:---------------|:---------------| +{{- range .Exporters }} +| [{{ .Factory.Type }}]({{ .URL }}) | {{.Version}} | {{ if .Factory.LogsStability }}[{{.Factory.LogsStability}}]{{ end }} | {{ if .Factory.MetricsStability }}[{{.Factory.MetricsStability}}]{{ end }} | {{ if .Factory.TracesStability }}[{{.Factory.TracesStability}}]{{ end }} | +{{- end }} + +| Extensions | Version | Stability | +|:---------------------------------------------------------------|:---------------| +{{- range .Extensions }} +| [{{ .Factory.Type }}]({{ .URL }}) | {{.Version}} | {{ if .Factory.Stability }}[{{.Factory.Stability}}]{{ end }} | +{{- end }} + +| Processors | Version | Logs | Metrics | Traces | +|:---------------------------------------------------------------|:---------------|:---------------|:---------------| +{{- range .Processors }} +| [{{ .Factory.Type }}]({{ .URL }}) | {{.Version}} | {{ if .Factory.LogsStability }}[{{.Factory.LogsStability}}]{{ end }} | {{ if .Factory.MetricsStability }}[{{.Factory.MetricsStability}}]{{ end }} | {{ if .Factory.TracesStability }}[{{.Factory.TracesStability}}]{{ end }} | +{{- end }} + +| Receivers | Version | Logs | Metrics | Traces | +|:---------------------------------------------------------------|:---------------|:---------------|:---------------| +{{- range .Receivers }} +| [{{ .Factory.Type }}]({{ .URL }}) | {{.Version}} | {{ if .Factory.LogsStability }}[{{.Factory.LogsStability}}]{{ end }} | {{ if .Factory.MetricsStability }}[{{.Factory.MetricsStability}}]{{ end }} | {{ if .Factory.TracesStability }}[{{.Factory.TracesStability}}]{{ end }} | +{{- end }} + +[Development]: https://github.com/open-telemetry/opentelemetry-collector#development +[Alpha]: https://github.com/open-telemetry/opentelemetry-collector#alpha +[Beta]: https://github.com/open-telemetry/opentelemetry-collector#beta +[Stable]: https://github.com/open-telemetry/opentelemetry-collector#stable +[Deprecated]: https://github.com/open-telemetry/opentelemetry-collector#deprecated +[Unmaintained]: https://github.com/open-telemetry/opentelemetry-collector#unmaintained \ No newline at end of file diff --git a/cmd/builder/internal/command.go b/cmd/builder/internal/command.go index aa90b7d66f2..fa688ca7f3d 100644 --- a/cmd/builder/internal/command.go +++ b/cmd/builder/internal/command.go @@ -30,6 +30,7 @@ const ( gcflagsFlag = "gcflags" distributionOutputPathFlag = "output-path" verboseFlag = "verbose" + generateDocCmdFlag = "generate-doc-cmd" ) // Command is the main entrypoint for this application @@ -84,6 +85,7 @@ func initFlags(flags *flag.FlagSet) error { flags.Bool(skipGetModulesFlag, false, "Whether builder should skip updating go.mod and retrieve Go module list (default false)") flags.Bool(skipStrictVersioningFlag, true, "Whether builder should skip strictly checking the calculated versions following dependency resolution") flags.Bool(verboseFlag, false, "Whether builder should print verbose output (default false)") + flags.Bool(generateDocCmdFlag, false, "Whether builder should generate an additional command to output docs (default false)") flags.String(ldflagsFlag, "", `ldflags to include in the "go build" command`) flags.String(gcflagsFlag, "", `gcflags to include in the "go build" command`) flags.String(distributionOutputPathFlag, "", "Where to write the resulting files") @@ -161,6 +163,9 @@ func applyFlags(flags *flag.FlagSet, cfg *builder.Config) error { cfg.Verbose, err = flags.GetBool(verboseFlag) errs = multierr.Append(errs, err) + cfg.GenerateDocCmd, err = flags.GetBool(generateDocCmdFlag) + errs = multierr.Append(errs, err) + if flags.Changed(distributionOutputPathFlag) { cfg.Distribution.OutputPath, err = flags.GetString(distributionOutputPathFlag) errs = multierr.Append(errs, err) diff --git a/cmd/otelcorecol/internal/doc/doc.go b/cmd/otelcorecol/internal/doc/doc.go new file mode 100644 index 00000000000..7e58ae33b83 --- /dev/null +++ b/cmd/otelcorecol/internal/doc/doc.go @@ -0,0 +1,97 @@ +// Code generated by "go.opentelemetry.io/collector/cmd/builder". DO NOT EDIT. + +// Program otelcorecol is an OpenTelemetry Collector utility to generate docs. +package main + +import ( + _ "embed" + "os" + "strings" + "text/template" + + "go.opentelemetry.io/collector/component" + forwardconnector "go.opentelemetry.io/collector/connector/forwardconnector" + debugexporter "go.opentelemetry.io/collector/exporter/debugexporter" + nopexporter "go.opentelemetry.io/collector/exporter/nopexporter" + otlpexporter "go.opentelemetry.io/collector/exporter/otlpexporter" + otlphttpexporter "go.opentelemetry.io/collector/exporter/otlphttpexporter" + memorylimiterextension "go.opentelemetry.io/collector/extension/memorylimiterextension" + zpagesextension "go.opentelemetry.io/collector/extension/zpagesextension" + batchprocessor "go.opentelemetry.io/collector/processor/batchprocessor" + memorylimiterprocessor "go.opentelemetry.io/collector/processor/memorylimiterprocessor" + nopreceiver "go.opentelemetry.io/collector/receiver/nopreceiver" + otlpreceiver "go.opentelemetry.io/collector/receiver/otlpreceiver" +) + +//go:embed md.tmpl +var mdBytes []byte + +type ComponentInfo struct { + Name string + GoMod string + Factory component.Factory +} + +func (c ComponentInfo) URL() string { + return strings.Split(c.GoMod, " ")[0] +} + +func (c ComponentInfo) Version() string { + return strings.Split(c.GoMod, " ")[1] +} + +func main() { + buildInfo := component.BuildInfo{ + Command: "otelcorecol", + Description: "Local OpenTelemetry Collector binary, testing only.", + Version: "0.120.0-dev", + } + + info := struct { + BuildInfo component.BuildInfo + Connectors []ComponentInfo + Exporters []ComponentInfo + Extensions []ComponentInfo + Processors []ComponentInfo + Receivers []ComponentInfo + }{ + BuildInfo: buildInfo, + Connectors: []ComponentInfo{ + {Name: "forwardconnector", GoMod: "go.opentelemetry.io/collector/connector/forwardconnector v0.120.0", Factory: forwardconnector.NewFactory()}, + }, + Exporters: []ComponentInfo{ + {Name: "debugexporter", GoMod: "go.opentelemetry.io/collector/exporter/debugexporter v0.120.0", Factory: debugexporter.NewFactory()}, + {Name: "nopexporter", GoMod: "go.opentelemetry.io/collector/exporter/nopexporter v0.120.0", Factory: nopexporter.NewFactory()}, + {Name: "otlpexporter", GoMod: "go.opentelemetry.io/collector/exporter/otlpexporter v0.120.0", Factory: otlpexporter.NewFactory()}, + {Name: "otlphttpexporter", GoMod: "go.opentelemetry.io/collector/exporter/otlphttpexporter v0.120.0", Factory: otlphttpexporter.NewFactory()}, + }, + Extensions: []ComponentInfo{ + {Name: "memorylimiterextension", GoMod: "go.opentelemetry.io/collector/extension/memorylimiterextension v0.120.0", Factory: memorylimiterextension.NewFactory()}, + {Name: "zpagesextension", GoMod: "go.opentelemetry.io/collector/extension/zpagesextension v0.120.0", Factory: zpagesextension.NewFactory()}, + }, + Processors: []ComponentInfo{ + {Name: "batchprocessor", GoMod: "go.opentelemetry.io/collector/processor/batchprocessor v0.120.0", Factory: batchprocessor.NewFactory()}, + {Name: "memorylimiterprocessor", GoMod: "go.opentelemetry.io/collector/processor/memorylimiterprocessor v0.120.0", Factory: memorylimiterprocessor.NewFactory()}, + }, + Receivers: []ComponentInfo{ + {Name: "nopreceiver", GoMod: "go.opentelemetry.io/collector/receiver/nopreceiver v0.120.0", Factory: nopreceiver.NewFactory()}, + {Name: "otlpreceiver", GoMod: "go.opentelemetry.io/collector/receiver/otlpreceiver v0.120.0", Factory: otlpreceiver.NewFactory()}, + }, + } + + templateBytes := mdBytes + if len(os.Args) > 1 { + templateFile := os.Args[1] + b, err := os.ReadFile(templateFile) + if err != nil { + panic(err) + } + templateBytes = b + } + + tmpl := template.Must(template.New("").Parse(string(templateBytes))) + err := tmpl.Execute(os.Stdout, info) + if err != nil { + panic(err) + } +} diff --git a/cmd/otelcorecol/internal/doc/md.tmpl b/cmd/otelcorecol/internal/doc/md.tmpl new file mode 100644 index 00000000000..889f91f3ad6 --- /dev/null +++ b/cmd/otelcorecol/internal/doc/md.tmpl @@ -0,0 +1,40 @@ +# {{ .BuildInfo.Command }} {{ .BuildInfo.Version }} + +{{ .BuildInfo.Description }} + +| Connectors | Version | Traces To Traces | Traces To Metrics | Traces To Logs | Metrics To Traces | Metrics To Logs | Logs To Traces | Logs To Metrics | Logs To Logs | +|:---------------------------------------------------------------|:-----------------|:------------------|:---------------|:------------------|:-----------------|:---------------|:----------------|:-------------| +{{- range .Connectors }} +| [{{ .Factory.Type }}]({{ .URL }}) | {{.Version}} | {{ if .Factory.TracesToTracesStability }}[{{.Factory.TracesToTracesStability}}]{{ end }} | {{ if .Factory.TracesToMetricsStability }}[{{.Factory.TracesToMetricsStability}}]{{ end }} | {{ if .Factory.TracesToLogsStability }}[{{.Factory.TracesToLogsStability}}]{{ end }} | {{ if .Factory.MetricsToTracesStability }}[{{.Factory.MetricsToTracesStability}}]{{ end }} | {{ if .Factory.MetricsToMetricsStability }}[{{.Factory.MetricsToMetricsStability}}]{{ end }} | {{ if .Factory.MetricsToLogsStability }}[{{.Factory.MetricsToLogsStability}}]{{ end }} | {{ if .Factory.LogsToTracesStability }}[{{.Factory.LogsToTracesStability}}]{{ end }} | {{ if .Factory.LogsToMetricsStability }}[{{.Factory.LogsToMetricsStability}}]{{ end }} | {{ if .Factory.LogsToLogsStability }}[{{.Factory.LogsToLogsStability}}]{{ end }} | +{{- end }} + +| Exporters | Version | Logs | Metrics | Traces | +|:---------------------------------------------------------------|:---------------|:---------------|:---------------| +{{- range .Exporters }} +| [{{ .Factory.Type }}]({{ .URL }}) | {{.Version}} | {{ if .Factory.LogsStability }}[{{.Factory.LogsStability}}]{{ end }} | {{ if .Factory.MetricsStability }}[{{.Factory.MetricsStability}}]{{ end }} | {{ if .Factory.TracesStability }}[{{.Factory.TracesStability}}]{{ end }} | +{{- end }} + +| Extensions | Version | Stability | +|:---------------------------------------------------------------|:---------------| +{{- range .Extensions }} +| [{{ .Factory.Type }}]({{ .URL }}) | {{.Version}} | {{ if .Factory.Stability }}[{{.Factory.Stability}}]{{ end }} | +{{- end }} + +| Processors | Version | Logs | Metrics | Traces | +|:---------------------------------------------------------------|:---------------|:---------------|:---------------| +{{- range .Processors }} +| [{{ .Factory.Type }}]({{ .URL }}) | {{.Version}} | {{ if .Factory.LogsStability }}[{{.Factory.LogsStability}}]{{ end }} | {{ if .Factory.MetricsStability }}[{{.Factory.MetricsStability}}]{{ end }} | {{ if .Factory.TracesStability }}[{{.Factory.TracesStability}}]{{ end }} | +{{- end }} + +| Receivers | Version | Logs | Metrics | Traces | +|:---------------------------------------------------------------|:---------------|:---------------|:---------------| +{{- range .Receivers }} +| [{{ .Factory.Type }}]({{ .URL }}) | {{.Version}} | {{ if .Factory.LogsStability }}[{{.Factory.LogsStability}}]{{ end }} | {{ if .Factory.MetricsStability }}[{{.Factory.MetricsStability}}]{{ end }} | {{ if .Factory.TracesStability }}[{{.Factory.TracesStability}}]{{ end }} | +{{- end }} + +[Development]: https://github.com/open-telemetry/opentelemetry-collector#development +[Alpha]: https://github.com/open-telemetry/opentelemetry-collector#alpha +[Beta]: https://github.com/open-telemetry/opentelemetry-collector#beta +[Stable]: https://github.com/open-telemetry/opentelemetry-collector#stable +[Deprecated]: https://github.com/open-telemetry/opentelemetry-collector#deprecated +[Unmaintained]: https://github.com/open-telemetry/opentelemetry-collector#unmaintained \ No newline at end of file