Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Tool to customize Telegraf builds #11524

Merged
merged 11 commits into from
Aug 19, 2022
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
/tools/package_lxd_test/package_lxd_test
/tools/license_checker/license_checker*
/tools/readme_config_includer/generator*
/tools/custom_builder/custom_builder*
/vendor
.DS_Store
process.yml
Expand Down
2 changes: 1 addition & 1 deletion .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ issues:
- path: _test\.go
text: "parameter.*seems to be a control flag, avoid control coupling"

- path: (^agent/|^cmd/|^config/|^filter/|^internal/|^logger/|^metric/|^models/|^selfstat/|^testutil/|^plugins/serializers/|^plugins/inputs/zipkin/cmd)
- path: (^agent/|^cmd/|^config/|^filter/|^internal/|^logger/|^metric/|^models/|^selfstat/|^testutil/|^tools|^plugins/serializers/|^plugins/inputs/zipkin/cmd)
text: "imports-blacklist: should not use the following blacklisted import: \"log\""
linters:
- revive
Expand Down
3 changes: 3 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ versioninfo:
go generate cmd/telegraf/telegraf_windows.go; \

build_tools:
$(HOSTGO) build -o ./tools/custom_builder/custom_builder$(EXEEXT) ./tools/custom_builder
$(HOSTGO) build -o ./tools/license_checker/license_checker$(EXEEXT) ./tools/license_checker
$(HOSTGO) build -o ./tools/readme_config_includer/generator$(EXEEXT) ./tools/readme_config_includer/generator.go

Expand Down Expand Up @@ -223,6 +224,8 @@ clean:
rm -f telegraf
rm -f telegraf.exe
rm -rf build
rm -rf tools/custom_builder/custom_builder
rm -rf tools/custom_builder/custom_builder.exe
rm -rf tools/readme_config_includer/generator
rm -rf tools/readme_config_includer/generator.exe
rm -rf tools/package_lxd_test/package_lxd_test
Expand Down
12 changes: 11 additions & 1 deletion cmd/telegraf/telegraf.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,15 @@ import (
"github.com/influxdata/telegraf/internal"
"github.com/influxdata/telegraf/internal/goplugin"
"github.com/influxdata/telegraf/logger"
"github.com/influxdata/telegraf/plugins/aggregators"
_ "github.com/influxdata/telegraf/plugins/aggregators/all"
"github.com/influxdata/telegraf/plugins/inputs"
_ "github.com/influxdata/telegraf/plugins/inputs/all"
"github.com/influxdata/telegraf/plugins/outputs"
_ "github.com/influxdata/telegraf/plugins/outputs/all"
"github.com/influxdata/telegraf/plugins/parsers"
_ "github.com/influxdata/telegraf/plugins/parsers/all"
"github.com/influxdata/telegraf/plugins/processors"
_ "github.com/influxdata/telegraf/plugins/processors/all"
"gopkg.in/tomb.v1"
)
Expand Down Expand Up @@ -271,7 +274,14 @@ func runAgent(ctx context.Context,

logger.SetupLogging(logConfig)

log.Printf("I! Starting Telegraf %s", internal.Version())
log.Printf("I! Starting Telegraf %s%s", internal.Version(), internal.Customized)
log.Printf("I! Available plugins: %d inputs, %d aggregators, %d processors, %d parsers, %d outputs",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I love this additional output, thank you!

I built with a config of just inputs.diskio and outputs.file and I ended up seeing:

2022-08-16T17:43:50Z I! Available plugins: 3 inputs, 0 aggregators, 0 processors, 0 parsers, 2 outputs

Which are:

telegraf-sven on  minify [?] via 🐹 v1.19 
❯ ./telegraf --input-list
Available Input Plugins:
  diskio
  io
  system
telegraf-sven on  minify [?] via 🐹 v1.19 
❯ ./telegraf --output-list
Available Output Plugins: 
  file
  wavefront

Two questions:

  • Why is wavefront pulled in? I understand the other inputs due to dependencies
  • Should we document this in the README, additional plugins may get pulled in due to dependencies. This could be a single sentence added to the readme.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems wavefront is coming through the backyard. ;-) The route is

  • file output imports github.com/influxdata/telegraf/plugins/serializers
  • serializers/registry.go imports github.com/influxdata/telegraf/plugins/serializers/wavefront
  • wavefront serializer imports github.com/influxdata/telegraf/plugins/outputs/wavefront 😨

This happens with the epic comment:

// TODO: this dependency is going the wrong way: Move MetricPoint into the serializer.

🤦‍♂️

I guess this has to be fixed, but is not an issue of custom_builder... What do you think?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we document this in the README, additional plugins may get pulled in due to dependencies. This could be a single sentence added to the readme.

Absolutely. Will do.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lol @ that comment, let's circle around and fix that after this is merged. I can file an issue.

len(inputs.Inputs),
len(aggregators.Aggregators),
len(processors.Processors),
len(parsers.Parsers),
len(outputs.Outputs),
)
log.Printf("I! Loaded inputs: %s", strings.Join(c.InputNames(), " "))
log.Printf("I! Loaded aggregators: %s", strings.Join(c.AggregatorNames(), " "))
log.Printf("I! Loaded processors: %s", strings.Join(c.ProcessorNames(), " "))
Expand Down
11 changes: 5 additions & 6 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -342,10 +342,9 @@ func (c *Config) LoadDirectory(path string) error {
}

// Try to find a default config file at these locations (in order):
// 1. $TELEGRAF_CONFIG_PATH
// 2. $HOME/.telegraf/telegraf.conf
// 3. /etc/telegraf/telegraf.conf
//
// 1. $TELEGRAF_CONFIG_PATH
// 2. $HOME/.telegraf/telegraf.conf
// 3. /etc/telegraf/telegraf.conf
func getDefaultConfigPath() (string, error) {
envfile := os.Getenv("TELEGRAF_CONFIG_PATH")
homefile := os.ExpandEnv("${HOME}/.telegraf/telegraf.conf")
Expand Down Expand Up @@ -387,7 +386,7 @@ func (c *Config) LoadConfig(path string) error {
return err
}
}
data, err := loadConfig(path)
data, err := LoadConfigFile(path)
if err != nil {
return fmt.Errorf("Error loading config file %s: %w", path, err)
}
Expand Down Expand Up @@ -566,7 +565,7 @@ func escapeEnv(value string) string {
return envVarEscaper.Replace(value)
}

func loadConfig(config string) ([]byte, error) {
func LoadConfigFile(config string) ([]byte, error) {
if fetchURLRe.MatchString(config) {
u, err := url.Parse(config)
if err != nil {
Expand Down
8 changes: 4 additions & 4 deletions docs/CUSTOMIZATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,13 @@ build as otherwise _all_ plugins will be selected regardless of other tags.
## Via make

When using the project's makefile, the build can be customized via the
`BUILDTAGS` environment variable containing a __space-separated__ list of the
`BUILDTAGS` environment variable containing a __comma-separated__ list of the
selected plugins (or categories) __and__ the `custom` tag.

For example

```shell
BUILDTAGS="custom inputs outputs.influxdb_v2 parsers.json" make
BUILDTAGS="custom,inputs,outputs.influxdb_v2,parsers.json" make
```

will build a customized Telegraf including _all_ `inputs`, the InfluxDB v2
Expand All @@ -32,13 +32,13 @@ will build a customized Telegraf including _all_ `inputs`, the InfluxDB v2
## Via `go build`

If you wish to build Telegraf using native go tools, you can use the `go build`
command with the `-tags` option. Specify a __space-separated__ list of the
command with the `-tags` option. Specify a __comma-separated__ list of the
selected plugins (or categories) __and__ the `custom` tag as argument.

For example

```shell
go build -tags "custom inputs outputs.influxdb_v2 parsers.json" ./cmd/telegraf
go build -tags "custom,inputs,outputs.influxdb_v2,parsers.json" ./cmd/telegraf
```

will build a customized Telegraf including _all_ `inputs`, the InfluxDB v2
Expand Down
8 changes: 8 additions & 0 deletions filter/filter.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,14 @@ func Compile(filters []string) (Filter, error) {
}
}

func MustCompile(filters []string) Filter {
f, err := Compile(filters)
if err != nil {
panic(err)
}
return f
}

// hasMeta reports whether path contains any magic glob characters.
func hasMeta(s string) bool {
return strings.ContainsAny(s, "*?[")
Expand Down
5 changes: 5 additions & 0 deletions internal/customized_no.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
//go:build !custom

package internal

const Customized = ""
5 changes: 5 additions & 0 deletions internal/customized_yes.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
//go:build custom

package internal

const Customized = " (customized)"
81 changes: 81 additions & 0 deletions tools/custom_builder/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# Telegraf customization tool

Telegraf's `custom_builder` is a tool to select the plugins compiled into the
Telegraf binary. By doing so, Telegraf can become smaller, saving both disk
space and memory if only a sub-set of plugins is selected.

## Building

To build `custom_builder` run the following command:

```shell
# make build_tools
```

The resulting binary is located in the `tools/custom_builder` folder.

## Running

The easiest way of building a customized Telegraf is to use your
Telegraf configuration file(s). Assuming your configuration is
in `/etc/telegraf/telegraf.conf` you can run

```shell
# ./tools/custom_builder/custom_builder --config /etc/telegraf/telegraf.conf
```

to build a Telegraf binary tailored to your configuration.
You can also specify a configuration directory similar to
Telegraf itself. To additionally use the configurations in
`/etc/telegraf/telegraf.d` run

```shell
# ./tools/custom_builder/custom_builder \
--config /etc/telegraf/telegraf.conf \
--config-dir /etc/telegraf/telegraf.d
```

Configurations can also be retrieved from remote locations just
like for Telegraf.

```shell
# ./tools/custom_builder/custom_builder --config http://myserver/telegraf.conf
```

will download the configuration from `myserver`.

The `--config` and `--config-dir` option can be used multiple times.
In case you want to deploy Telegraf to multiple systems with
different configurations, simply specify the super-set of all
configurations you have. `custom_builder` will figure out the list
for you

```shell
# ./tools/custom_builder/custom_builder \
--config system1/telegraf.conf \
--config system2/telegraf.conf \
--config ... \
--config systemN/telegraf.conf \
--config-dir system1/telegraf.d \
--config-dir system2/telegraf.d \
--config-dir ... \
--config-dir systemN/telegraf.d
```

The Telegraf customization uses
[Golang's build-tags](https://pkg.go.dev/go/build#hdr-Build_Constraints) to
select the set of plugins. To see which tags are set use the `--tags` flag.

To get more help run

```shell
# ./tools/custom_builder/custom_builder --help
```

## Notes

Please make sure to include all `parsers` you intend to use and check the
enabled-plugins list.

Additional plugins can potentially be enabled automatically due to
dependencies without being shown in the enabled-plugins list.
153 changes: 153 additions & 0 deletions tools/custom_builder/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
package main

import (
"bytes"
"errors"
"fmt"
"os"
"path/filepath"

"github.com/influxdata/telegraf/config"
"github.com/influxdata/toml"
"github.com/influxdata/toml/ast"
)

type pluginState map[string]bool
type selection map[string]pluginState

func ImportConfigurations(files, dirs []string) (*selection, int, error) {
sel := selection(make(map[string]pluginState))

// Initialize the categories
for _, category := range categories {
sel[category] = make(map[string]bool)
}

// Gather all configuration files
var filenames []string
filenames = append(filenames, files...)

for _, dir := range dirs {
// Walk the directory and get the packages
elements, err := os.ReadDir(dir)
if err != nil {
return nil, 0, fmt.Errorf("reading directory %q failed: %w", dir, err)
}

for _, element := range elements {
if element.IsDir() || filepath.Ext(element.Name()) != ".conf" {
continue
}

filenames = append(filenames, filepath.Join(dir, element.Name()))
}
}
if len(filenames) == 0 {
return &sel, 0, errors.New("no configuration files given or found")
}

// Do the actual import
err := sel.importFiles(filenames)
return &sel, len(filenames), err
}

func (s *selection) Filter(p packageCollection) (*packageCollection, error) {
enabled := packageCollection{
packages: map[string][]packageInfo{},
}

for category, pkgs := range p.packages {
var categoryEnabledPackages []packageInfo
settings := (*s)[category]
for _, pkg := range pkgs {
if _, found := settings[pkg.Plugin]; found {
categoryEnabledPackages = append(categoryEnabledPackages, pkg)
}
}
enabled.packages[category] = categoryEnabledPackages
}

// Make sure we update the list of default parsers used by
// the remaining packages
enabled.FillDefaultParsers()

// If the user did not configure any parser, we want to include
// the default parsers if any to preserve a functional set of
// plugins.
if len(enabled.packages["parsers"]) == 0 && len(enabled.defaultParsers) > 0 {
var parsers []packageInfo
for _, pkg := range p.packages["parsers"] {
for _, name := range enabled.defaultParsers {
if pkg.Plugin == name {
parsers = append(parsers, pkg)
break
}
}
}
enabled.packages["parsers"] = parsers
}

return &enabled, nil
}

func (s *selection) importFiles(configurations []string) error {
for _, cfg := range configurations {
buf, err := config.LoadConfigFile(cfg)
if err != nil {
return fmt.Errorf("reading %q failed: %v", cfg, err)
}

if err := s.extractPluginsFromConfig(buf); err != nil {
return fmt.Errorf("extracting plugins from %q failed: %v", cfg, err)
}
}

return nil
}

func (s *selection) extractPluginsFromConfig(buf []byte) error {
table, err := toml.Parse(trimBOM(buf))
if err != nil {
return fmt.Errorf("parsing TOML failed: %w", err)
}

for category, subtbl := range table.Fields {
categoryTbl, ok := subtbl.(*ast.Table)
if !ok {
continue
}

if _, found := (*s)[category]; !found {
continue
}

for name, data := range categoryTbl.Fields {
(*s)[category][name] = true

// We need to check the data_format field to get all required parsers
switch category {
case "inputs", "processors":
pluginTables, ok := data.([]*ast.Table)
if !ok {
continue
}
for _, subsubtbl := range pluginTables {
for field, fieldData := range subsubtbl.Fields {
if field != "data_format" {
continue
}
kv := fieldData.(*ast.KeyValue)
name := kv.Value.(*ast.String)
(*s)["parsers"][name.Value] = true
}
}
}
}
}

return nil
}

func trimBOM(f []byte) []byte {
return bytes.TrimPrefix(f, []byte("\xef\xbb\xbf"))
}
Loading