Skip to content

Commit

Permalink
smartagentreceiver: Embedded MonitorCustomConfigs
Browse files Browse the repository at this point in the history
  • Loading branch information
Ryan Fitzpatrick committed Jan 6, 2021
1 parent df94f3a commit 275983f
Show file tree
Hide file tree
Showing 13 changed files with 1,473 additions and 50 deletions.
16 changes: 16 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,14 @@ require (
github.com/ory/go-acc v0.2.6
github.com/pavius/impi v0.0.3
github.com/securego/gosec/v2 v2.5.0
github.com/signalfx/defaults v1.2.2-0.20180531161417-70562fe60657
github.com/signalfx/signalfx-agent v0.0.0-00010101000000-000000000000
github.com/spf13/viper v1.7.1
github.com/stretchr/testify v1.6.1
go.opentelemetry.io/collector v0.15.0
go.uber.org/zap v1.16.0
golang.org/x/sys v0.0.0-20201015000850-e3ed0017c211
gopkg.in/yaml.v2 v2.3.0
honnef.co/go/tools v0.0.1-2020.1.6
)

Expand All @@ -50,3 +54,15 @@ replace (
github.com/open-telemetry/opentelemetry-collector-contrib/receiver/k8sclusterreceiver v0.0.0-00010101000000-000000000000 => github.com/open-telemetry/opentelemetry-collector-contrib/receiver/k8sclusterreceiver v0.15.0
github.com/open-telemetry/opentelemetry-collector-contrib/receiver/redisreceiver v0.0.0-00010101000000-000000000000 => github.com/open-telemetry/opentelemetry-collector-contrib/receiver/redisreceiver v0.15.0
)

// each of these is required for the smartagentreceiver
replace (
code.cloudfoundry.org/go-loggregator => github.com/signalfx/go-loggregator v1.0.1-0.20200205155641-5ba5ca92118d
github.com/dancannon/gorethink => gopkg.in/gorethink/gorethink.v4 v4.0.0
github.com/influxdata/telegraf => github.com/signalfx/telegraf v0.10.2-0.20201211214327-200738592ced
github.com/prometheus/prometheus => github.com/prometheus/prometheus v1.8.2-0.20201105135750-00f16d1ac3a4
github.com/signalfx/signalfx-agent => github.com/signalfx/signalfx-agent v1.0.1-0.20210104180832-f3147854c36c
github.com/signalfx/signalfx-agent/pkg/apm => github.com/signalfx/signalfx-agent/pkg/apm v0.0.0-20210104180832-f3147854c36c
github.com/soheilhy/cmux => github.com/signalfx/signalfx-agent/thirdparty/cmux v0.0.0-20210104180832-f3147854c36c // required for smartagentreceiver to drop google.golang.org/grpc/examples/helloworld/helloworld test dep
google.golang.org/grpc => google.golang.org/grpc v1.29.1 // required for smartagentreceiver's go.etcd.io/etcd dep
)
921 changes: 883 additions & 38 deletions go.sum

Large diffs are not rendered by default.

106 changes: 102 additions & 4 deletions internal/receiver/smartagentreceiver/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,116 @@ package smartagentreceiver

import (
"fmt"
"reflect"
"strings"

"github.com/signalfx/defaults"
_ "github.com/signalfx/signalfx-agent/pkg/core" // required to invoke monitor registration via init() calls
"github.com/signalfx/signalfx-agent/pkg/core/config"
"github.com/signalfx/signalfx-agent/pkg/core/config/validation"
"github.com/signalfx/signalfx-agent/pkg/monitors"
"github.com/spf13/viper"
"go.opentelemetry.io/collector/config/configmodels"
"gopkg.in/yaml.v2"
)

const defaultIntervalSeconds = 10

type Config struct {
configmodels.ReceiverSettings `mapstructure:",squash"`
monitorConfig config.MonitorCustomConfig
}

func (cfg *Config) validate() error {
if cfg.NameVal == "" || cfg.TypeVal == "" {
// validation placeholder for coverage
return fmt.Errorf("name and type are required")
func (rCfg *Config) validate() error {
if rCfg.monitorConfig == nil {
return fmt.Errorf("you must supply a valid Smart Agent Monitor config")
}

monitorConfigCore := rCfg.monitorConfig.MonitorConfigCore()
if monitorConfigCore.IntervalSeconds == 0 {
monitorConfigCore.IntervalSeconds = defaultIntervalSeconds
} else if monitorConfigCore.IntervalSeconds < 0 {
return fmt.Errorf("intervalSeconds must be greater than 0s (%d provided)", monitorConfigCore.IntervalSeconds)
}

if err := validation.ValidateStruct(rCfg.monitorConfig); err != nil {
return err
}
return validation.ValidateCustomConfig(rCfg.monitorConfig)
}

// mergeConfigs is used as a custom unmarshaller to dynamically create the desired Smart Agent monitor config
// from the provided receiver config content.
func mergeConfigs(componentViperSection *viper.Viper, intoCfg interface{}) error {
// AllSettings() will include anything not already unmarshalled in the Config instance (*intoCfg).
// This includes all Smart Agent monitor config settings that can be unmarshalled to their
// respective custom monitor config types.
allSettings := componentViperSection.AllSettings()
monitorType, ok := allSettings["type"].(string)
if !ok || monitorType == "" {
return fmt.Errorf("you must specify a \"type\" for a smartagent receiver")
}

// monitors.ConfigTemplates is a map that all monitors use to register their custom configs in the Smart Agent.
// The values are always pointers to an actual custom config.
var customMonitorConfig config.MonitorCustomConfig
if customMonitorConfig, ok = monitors.ConfigTemplates[monitorType]; !ok {
return fmt.Errorf("no known monitor type %q", monitorType)
}
monitorConfigType := reflect.TypeOf(customMonitorConfig).Elem()
monitorConfig := reflect.New(monitorConfigType).Interface()

// Viper is case insensitive and doesn't preserve a record of actual yaml map key cases from the provided config,
// which is a problem when unmarshalling custom agent monitor configs. Here we use a map of lowercase to supported
// case tag key names and update the keys where applicable.
yamlTags := yamlTagsFromStruct(monitorConfigType)
for key, val := range allSettings {
updatedKey := yamlTags[key]
if updatedKey != "" {
delete(allSettings, key)
allSettings[updatedKey] = val
}
}

asBytes, err := yaml.Marshal(allSettings)
if err != nil {
return fmt.Errorf("failed constructing raw Smart Agent Monitor config block: %w", err)
}
err = yaml.UnmarshalStrict(asBytes, monitorConfig)
if err != nil {
return fmt.Errorf("failed creating Smart Agent Monitor custom config: %w", err)
}

err = defaults.Set(monitorConfig)
if err != nil {
return fmt.Errorf("failed setting Smart Agent Monitor config defaults: %w", err)
}
receiverCfg := intoCfg.(*Config)
receiverCfg.monitorConfig = monitorConfig.(config.MonitorCustomConfig)
return nil
}

// Walks through a custom monitor config struct type, creating a map of
// lowercase to supported yaml struct tag name cases.
func yamlTagsFromStruct(s reflect.Type) map[string]string {
yamlTags := map[string]string{}
for i := 0; i < s.NumField(); i++ {
field := s.Field(i)
tag := field.Tag
yamlTag := strings.Split(tag.Get("yaml"), ",")[0]
lowerTag := strings.ToLower(yamlTag)
if yamlTag != lowerTag {
yamlTags[lowerTag] = yamlTag
}

fieldType := field.Type
if fieldType.Kind() == reflect.Struct {
otherFields := yamlTagsFromStruct(fieldType)
for k, v := range otherFields {
yamlTags[k] = v
}
}
}

return yamlTags
}
125 changes: 125 additions & 0 deletions internal/receiver/smartagentreceiver/config_linux_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
// Copyright 2021, OpenTelemetry Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// +build linux

package smartagentreceiver

import (
"path"
"testing"

"github.com/signalfx/signalfx-agent/pkg/core/config"
"github.com/signalfx/signalfx-agent/pkg/monitors/collectd/apache"
"github.com/signalfx/signalfx-agent/pkg/monitors/collectd/genericjmx"
"github.com/signalfx/signalfx-agent/pkg/monitors/collectd/kafka"
"github.com/signalfx/signalfx-agent/pkg/monitors/collectd/memcached"
"github.com/signalfx/signalfx-agent/pkg/monitors/collectd/php"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.opentelemetry.io/collector/component/componenttest"
"go.opentelemetry.io/collector/config/configmodels"
"go.opentelemetry.io/collector/config/configtest"
)

func TestLoadConfigWithLinuxOnlyMonitors(t *testing.T) {
factories, err := componenttest.ExampleComponents()
assert.Nil(t, err)

factory := NewFactory()
factories.Receivers[configmodels.Type(typeStr)] = factory
cfg, err := configtest.LoadConfigFile(
t, path.Join(".", "testdata", "linux_config.yaml"), factories,
)

require.NoError(t, err)
require.NotNil(t, cfg)

assert.Equal(t, len(cfg.Receivers), 4)

apacheCfg := cfg.Receivers["smartagent/apache"].(*Config)
require.Equal(t, &Config{
ReceiverSettings: configmodels.ReceiverSettings{
TypeVal: typeStr,
NameVal: typeStr + "/apache",
},
monitorConfig: &apache.Config{
MonitorConfig: config.MonitorConfig{
Type: "collectd/apache",
IntervalSeconds: 234,
DatapointsToExclude: []config.MetricFilter{},
},
Host: "localhost",
Port: 6379,
URL: "http://{{.Host}}:{{.Port}}/mod_status?auto",
},
}, apacheCfg)
require.NoError(t, apacheCfg.validate())

kafkaCfg := cfg.Receivers["smartagent/kafka"].(*Config)
require.Equal(t, &Config{
ReceiverSettings: configmodels.ReceiverSettings{
TypeVal: typeStr,
NameVal: typeStr + "/kafka",
},
monitorConfig: &kafka.Config{
Config: genericjmx.Config{
MonitorConfig: config.MonitorConfig{
Type: "collectd/kafka",
IntervalSeconds: 345,
DatapointsToExclude: []config.MetricFilter{},
},
Host: "localhost",
Port: 7199,
ServiceURL: "service:jmx:rmi:///jndi/rmi://{{.Host}}:{{.Port}}/jmxrmi",
},
ClusterName: "somecluster",
},
}, kafkaCfg)
require.NoError(t, kafkaCfg.validate())

memcachedCfg := cfg.Receivers["smartagent/memcached"].(*Config)
require.Equal(t, &Config{
ReceiverSettings: configmodels.ReceiverSettings{
TypeVal: typeStr,
NameVal: typeStr + "/memcached",
},
monitorConfig: &memcached.Config{
MonitorConfig: config.MonitorConfig{
Type: "collectd/memcached",
IntervalSeconds: 456,
DatapointsToExclude: []config.MetricFilter{},
},
Host: "localhost",
Port: 5309,
},
}, memcachedCfg)
require.NoError(t, memcachedCfg.validate())

phpCfg := cfg.Receivers["smartagent/php"].(*Config)
require.Equal(t, &Config{
ReceiverSettings: configmodels.ReceiverSettings{
TypeVal: typeStr,
NameVal: typeStr + "/php",
},
monitorConfig: &php.Config{
MonitorConfig: config.MonitorConfig{
Type: "collectd/php-fpm",
IntervalSeconds: 0,
DatapointsToExclude: []config.MetricFilter{},
},
Path: "/status",
},
}, phpCfg)
require.NoError(t, phpCfg.validate())
}
Loading

0 comments on commit 275983f

Please sign in to comment.