Skip to content

Commit

Permalink
Merge pull request #293 from mackerelio/command-args
Browse files Browse the repository at this point in the history
specify command arguments in mackerel-agent.conf
  • Loading branch information
Songmu authored Dec 20, 2016
2 parents 3ba0f2d + 6b789db commit fe0986c
Show file tree
Hide file tree
Showing 13 changed files with 123 additions and 55 deletions.
9 changes: 2 additions & 7 deletions checks/checker.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import (

"github.com/mackerelio/mackerel-agent/config"
"github.com/mackerelio/mackerel-agent/logging"
"github.com/mackerelio/mackerel-agent/util"
)

var logger = logging.GetLogger("checks")
Expand Down Expand Up @@ -43,7 +42,7 @@ type Checker struct {
// to have the Command field which was used by metrics.pluginGenerator.
// If the configuration of *checks.Checker and/or metrics.pluginGenerator changes,
// we should reconsider using config.PluginConfig.
Config config.PluginConfig
Config *config.PluginConfig
}

// Report is what Checker produces by invoking its command.
Expand All @@ -63,16 +62,12 @@ func (c *Checker) String() string {
// Check invokes the command and transforms its result to a Report.
func (c *Checker) Check() *Report {
now := time.Now()

command := c.Config.Command
logger.Debugf("Checker %q executing command %q", c.Name, command)
message, stderr, exitCode, err := util.RunCommand(command, c.Config.User)
message, stderr, exitCode, err := c.Config.Run()
if stderr != "" {
logger.Warningf("Checker %q output stderr: %s", c.Name, stderr)
}

status := StatusUnknown

if err != nil {
message = err.Error()
} else {
Expand Down
4 changes: 2 additions & 2 deletions checks/checker_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@ import (

func TestChecker_Check(t *testing.T) {
checkerOK := Checker{
Config: config.PluginConfig{
Config: &config.PluginConfig{
Command: "go run testdata/exit.go -code 0 -message OK",
},
}

checkerWarning := Checker{
Config: config.PluginConfig{
Config: &config.PluginConfig{
Command: "go run testdata/exit.go -code 1 -message something_is_going_wrong",
},
}
Expand Down
2 changes: 1 addition & 1 deletion checks/checker_unix_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ func TestChecker_CheckTimeout(t *testing.T) {
util.TimeoutDuration = 1 * time.Second

checkerTimeout := Checker{
Config: config.PluginConfig{
Config: &config.PluginConfig{
Command: "sleep 2",
},
}
Expand Down
8 changes: 4 additions & 4 deletions command/command_unix_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,12 @@ func TestRunOnce(t *testing.T) {

conf := &config.Config{
Plugin: map[string]config.PluginConfigs{
"metrics": map[string]config.PluginConfig{
"metrics": map[string]*config.PluginConfig{
"metric1": {
Command: diceCommand,
},
},
"checks": map[string]config.PluginConfig{
"checks": map[string]*config.PluginConfig{
"check1": {
Command: "echo 1",
},
Expand All @@ -53,12 +53,12 @@ func TestRunOncePayload(t *testing.T) {

conf := &config.Config{
Plugin: map[string]config.PluginConfigs{
"metrics": map[string]config.PluginConfig{
"metrics": map[string]*config.PluginConfig{
"metric1": {
Command: diceCommand,
},
},
"checks": map[string]config.PluginConfig{
"checks": map[string]*config.PluginConfig{
"check1": {
Command: "echo 1",
},
Expand Down
47 changes: 46 additions & 1 deletion config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (

"github.com/BurntSushi/toml"
"github.com/mackerelio/mackerel-agent/logging"
"github.com/mackerelio/mackerel-agent/util"
)

var configLogger = logging.GetLogger("config")
Expand Down Expand Up @@ -63,20 +64,56 @@ type Config struct {

// PluginConfigs represents a set of [plugin.<kind>.<name>] sections in the configuration file
// under a specific <kind>. The key of the map is <name>, for example "mysql" of "plugin.metrics.mysql".
type PluginConfigs map[string]PluginConfig
type PluginConfigs map[string]*PluginConfig

// PluginConfig represents a section of [plugin.*].
// `MaxCheckAttempts`, `NotificationInterval` and `CheckInterval` options are used with check monitoring plugins. Custom metrics plugins ignore these options.
// `User` option is ignore in windows
type PluginConfig struct {
CommandRaw interface{} `toml:"command"`
Command string
CommandArgs []string
User string
NotificationInterval *int32 `toml:"notification_interval"`
CheckInterval *int32 `toml:"check_interval"`
MaxCheckAttempts *int32 `toml:"max_check_attempts"`
CustomIdentifier *string `toml:"custom_identifier"`
}

func (pconf *PluginConfig) prepareCommand() error {
const errFmt = "failed to prepare plugin command. A configuration value of `command` should be string or string slice, but %T"
v := pconf.CommandRaw
switch t := v.(type) {
case string:
pconf.Command = t
case []interface{}:
if len(t) > 0 {
for _, vv := range t {
str, ok := vv.(string)
if !ok {
return fmt.Errorf(errFmt, v)
}
pconf.CommandArgs = append(pconf.CommandArgs, str)
}
} else {
return fmt.Errorf(errFmt, v)
}
case []string:
pconf.CommandArgs = t
default:
return fmt.Errorf(errFmt, v)
}
return nil
}

// Run the plugin
func (pconf *PluginConfig) Run() (string, string, int, error) {
if len(pconf.CommandArgs) > 0 {
return util.RunCommandArgs(pconf.CommandArgs, pconf.User)
}
return util.RunCommand(pconf.Command, pconf.User)
}

const postMetricsDequeueDelaySecondsMax = 59 // max delay seconds for dequeuing from buffer queue
const postMetricsRetryDelaySecondsMax = 3 * 60 // max delay seconds for retrying a request that caused errors

Expand Down Expand Up @@ -179,6 +216,14 @@ func loadConfigFile(file string) (*Config, error) {
return config, err
}
}
for _, pconfs := range config.Plugin {
for _, pconf := range pconfs {
err := pconf.prepareCommand()
if err != nil {
return nil, err
}
}
}
return config, nil
}

Expand Down
28 changes: 28 additions & 0 deletions config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"io/ioutil"
"os"
"path/filepath"
"reflect"
"strings"
"testing"
)
Expand Down Expand Up @@ -337,6 +338,33 @@ silent = true
}
}

func TestLoadConfig_WithCommandArgs(t *testing.T) {
conff, err := newTempFileWithContent(`
apikey = "abcde"
[plugin.metrics.hoge]
command = ["perl", "-E", "say 'Hello'"]
`)
if err != nil {
t.Fatalf("should not raise error: %s", err)
}
defer os.Remove(conff.Name())

config, err := loadConfigFile(conff.Name())
assertNoError(t, err)

expected := []string{"perl", "-E", "say 'Hello'"}
p := config.Plugin["metrics"]["hoge"]
output := p.CommandArgs

if !reflect.DeepEqual(expected, output) {
t.Errorf("command args not expected: %+v", output)
}

if p.Command != "" {
t.Errorf("p.Command should be empty but: %s", p.Command)
}
}

func newTempFileWithContent(content string) (*os.File, error) {
tmpf, err := ioutil.TempFile("", "mackerel-config-test")
if err != nil {
Expand Down
23 changes: 8 additions & 15 deletions metrics/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,12 @@ import (
"github.com/mackerelio/mackerel-agent/config"
"github.com/mackerelio/mackerel-agent/logging"
"github.com/mackerelio/mackerel-agent/mackerel"
"github.com/mackerelio/mackerel-agent/util"
)

// pluginGenerator collects user-defined metrics.
// mackerel-agent runs specified command and parses the result for the metric names and values.
type pluginGenerator struct {
Config config.PluginConfig
Config *config.PluginConfig
Meta *pluginMeta
}

Expand Down Expand Up @@ -46,7 +45,7 @@ const pluginPrefix = "custom."
var pluginConfigurationEnvName = "MACKEREL_AGENT_PLUGIN_META"

// NewPluginGenerator XXX
func NewPluginGenerator(conf config.PluginConfig) PluginGenerator {
func NewPluginGenerator(conf *config.PluginConfig) PluginGenerator {
return &pluginGenerator{Config: conf}
}

Expand Down Expand Up @@ -121,24 +120,21 @@ func (g *pluginGenerator) CustomIdentifier() *string {
// }
// }
func (g *pluginGenerator) loadPluginMeta() error {
command := g.Config.Command
pluginLogger.Debugf("Obtaining plugin configuration: %q", command)

// Set environment variable to make the plugin command generate its configuration
os.Setenv(pluginConfigurationEnvName, "1")
defer os.Setenv(pluginConfigurationEnvName, "")

stdout, stderr, exitCode, err := util.RunCommand(command, g.Config.User)
stdout, stderr, exitCode, err := g.Config.Run()
if err != nil {
return fmt.Errorf("running %q failed: %s, exit=%d stderr=%q", command, err, exitCode, stderr)
return fmt.Errorf("running %T failed: %s, exit=%d stderr=%q", g.Config.CommandRaw, err, exitCode, stderr)
}

outBuffer := bufio.NewReader(strings.NewReader(stdout))
// Read the plugin configuration meta (version etc)

headerLine, err := outBuffer.ReadString('\n')
if err != nil {
return fmt.Errorf("while reading the first line of command %q: %s", command, err)
return fmt.Errorf("while reading the first line of command %T: %s", g.Config.CommandRaw, err)
}

// Parse the header line of format:
Expand Down Expand Up @@ -219,17 +215,14 @@ func (g *pluginGenerator) makeCreateGraphDefsPayload() []mackerel.CreateGraphDef
var delimReg = regexp.MustCompile(`[\s\t]+`)

func (g *pluginGenerator) collectValues() (Values, error) {
command := g.Config.Command
pluginLogger.Debugf("Executing plugin: command = \"%s\"", command)

os.Setenv(pluginConfigurationEnvName, "")
stdout, stderr, _, err := util.RunCommand(command, g.Config.User)
stdout, stderr, _, err := g.Config.Run()

if stderr != "" {
pluginLogger.Infof("command %q outputted to STDERR: %q", command, stderr)
pluginLogger.Infof("command %T outputted to STDERR: %q", g.Config.CommandRaw, stderr)
}
if err != nil {
pluginLogger.Errorf("Failed to execute command %q (skip these metrics):\n", command)
pluginLogger.Errorf("Failed to execute command %T (skip these metrics):\n", g.Config.CommandRaw)
return nil, err
}

Expand Down
14 changes: 7 additions & 7 deletions metrics/plugin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ func containsKeyRegexp(values Values, reg string) bool {
}

func TestPluginGenerate(t *testing.T) {
conf := config.PluginConfig{
conf := &config.PluginConfig{
Command: "ruby ../example/metrics-plugins/dice.rb",
}
g := &pluginGenerator{Config: conf}
Expand All @@ -35,7 +35,7 @@ func TestPluginGenerate(t *testing.T) {
}

func TestPluginCollectValues(t *testing.T) {
g := &pluginGenerator{Config: config.PluginConfig{
g := &pluginGenerator{Config: &config.PluginConfig{
Command: "ruby ../example/metrics-plugins/dice.rb",
},
}
Expand All @@ -49,7 +49,7 @@ func TestPluginCollectValues(t *testing.T) {
}

func TestPluginCollectValuesCommand(t *testing.T) {
g := &pluginGenerator{Config: config.PluginConfig{
g := &pluginGenerator{Config: &config.PluginConfig{
Command: "echo \"just.echo.1\t1\t1397822016\"",
},
}
Expand All @@ -74,7 +74,7 @@ func TestPluginCollectValuesCommand(t *testing.T) {
}

func TestPluginCollectValuesCommandWithSpaces(t *testing.T) {
g := &pluginGenerator{Config: config.PluginConfig{
g := &pluginGenerator{Config: &config.PluginConfig{
Command: `echo "just.echo.2 2 1397822016"`,
}}

Expand All @@ -99,7 +99,7 @@ func TestPluginCollectValuesCommandWithSpaces(t *testing.T) {

func TestPluginLoadPluginMeta(t *testing.T) {
g := &pluginGenerator{
Config: config.PluginConfig{
Config: &config.PluginConfig{
Command: `echo '# mackerel-agent-plugin version=1
{
"graphs": {
Expand Down Expand Up @@ -155,7 +155,7 @@ func TestPluginLoadPluginMeta(t *testing.T) {
}

generatorWithoutConf := &pluginGenerator{
Config: config.PluginConfig{
Config: &config.PluginConfig{
Command: "echo \"just.echo.1\t1\t1397822016\"",
},
}
Expand All @@ -166,7 +166,7 @@ func TestPluginLoadPluginMeta(t *testing.T) {
}

generatorWithBadVersion := &pluginGenerator{
Config: config.PluginConfig{
Config: &config.PluginConfig{
Command: `echo "# mackerel-agent-plugin version=666"`,
},
}
Expand Down
4 changes: 2 additions & 2 deletions metrics/windows/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,15 @@ import (

// PluginGenerator XXX
type PluginGenerator struct {
Config config.PluginConfig
Config *config.PluginConfig
}

var pluginLogger = logging.GetLogger("metrics.plugin")

const pluginPrefix = "custom."

// NewPluginGenerator XXX
func NewPluginGenerator(c config.PluginConfig) (*PluginGenerator, error) {
func NewPluginGenerator(c *config.PluginConfig) (*PluginGenerator, error) {
return &PluginGenerator{c}, nil
}

Expand Down
2 changes: 1 addition & 1 deletion metrics/windows/plugin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ func containsKeyRegexp(values metrics.Values, reg string) bool {
}

func TestPluginGenerate(t *testing.T) {
conf := config.PluginConfig{
conf := &config.PluginConfig{
Command: "ruby ../../example/metrics-plugins/dice.rb",
}
g := &PluginGenerator{conf}
Expand Down
Loading

0 comments on commit fe0986c

Please sign in to comment.