Giter Club home page Giter Club logo

sensu-plugin-sdk's Introduction

Sensu Go Plugin Library

GoDoc Go Test

This project is a framework for building Sensu Go plugins. Plugins can be Checks, Handlers, or Mutators. With this library the user only needs to define the plugin arguments, an input validation function and an execution function.

Plugin Configuration

The plugin configuration contains the plugin information.

To define the plugin configuration use the following information.

// Define the plugin configuration
type Config struct {
  sensu.HandlerConfig
  // other configuration
}

// Create the plugin configuration
var config = Config{
  HandlerConfig: sensu.HandlerConfig{
    Name:     "sensu-go-plugin",
    Short:    "Performs my incredible logic",
    Timeout:  10,
    Keyspace: "sensu.io/plugins/my-sensu-go-plugin/config",
  },
}

Plugin Configuration Options

Configuration options are read from the following sources using the following precedence order. Each item takes precedence over the item below it:

  • Sensu event check annotation
  • Sensu event entity annotation
  • Command line argument in short or long form
  • Environment variable
  • Default value
var (
  argumentValue string
  sliceArgument []float64
  mapArgument   map[string]int

  options = []sensu.ConfigOption{
    &sensu.PluginConfigOption[string]{
      Path:      "override-path",
      Env:       "COMMAND_LINE_ENVIRONMENT",
      Argument:  "command-line-argument",
      Shorthand: "c",
      Default:   "Default Value",
      Usage:     "The usage message printed for this option",
      Value:     &argumentValue,
    },
    &sensu.SlicePluginConfigOption[float64]{
      Path:      "override-path",
      Env:       "COMMAND_LINE_ENVIRONMENT",
      Argument:  "command-line-argument",
      Shorthand: "c",
      Default:   []float64{0.1},
      Usage:     "The usage message printed for this option",
      Value:     &sliceArgument,
    },
    &sensu.MapPluginConfigOption[int]{
      Path:      "override-path",
      Env:       "COMMAND_LINE_ENVIRONMENT",
      Argument:  "command-line-argument",
      Shorthand: "c",
      Default:   map[string]int{"hello": 123},
      Usage:     "The usage message printed for this option",
      Value:     &mapArgument,
    },
  }
)

Annotations Configuration Options Override

Configuration options can be overridden using the Sensu event check or entity annotations.

For example, if we have a plugin using the keyspace sensu.io/plugins/my-sensu-go-plugin/config and a configuration option using the path node-name, the following annotation could be configured in an agent configuration file to override whatever value is configuration via the plugin's flags or environment variables:

# /etc/sensu/agent.yml example
annotations:
  sensu.io/plugins/my-sensu-go-plugin/config/node-name: webserver01.example.com

Input Validation Function

The validation function is used to validate the Sensu event and plugin input. It must return an error if there is a problem with the options. If the input is correct nil can be returned and the plugin execution will continue.

To define the validation function use the following signature.

func validateInput(_ *corev2.Event) error {
  // Validate the input here
  return nil
}

Execution function

The execution function executes the plugin's logic. If there is an error while processing the plugin logic the execution function should return an error. If the logic is executed successfully nil should be returned.

To define the execution function use the following signature.

func executeHandler(event *corev2.Event) error {
  // Handler logic
  return nil
}

Putting Everything Together

Create a main function that creates the handler with the previously defined configuration, options, validation function and execution function.

func main() {
  handler := sensu.NewHandler(&config.HandlerConfig, options, validateInput, executeHandler)
  err := handler.Execute()
}

Enterprise plugins

An enterprise plugin requires a valid Sensu license to run. Initialize enterprise handlers with NewEnterpriseHandler. If the license file passed from the handler's environment variables is invalid, it should return an error without executing.

func main() {
  handler := sensu.NewEnterpriseHandler(&config.HandlerConfig, options, validateInput, executeHandler)
  err := handler.Execute()
}

Sensu Go >= 5.21 will add the SENSU_LICENSE_FILE environment variable to the handler execution. To run the plugin independently of Sensu (ex. test/dev), you must set the env var:

SENSU_LICENSE_FILE=$(sensuctl license info --format json)

Templates

The templates package provides a wrapper to the text/template package allowing for the use of templates to expand event attributes. An example of this would be using the following as part of a handler:

--summary-template "{{.Entity.Name}}/{{.Check.Name}}"

Which, if given an event with an entity name of webserver01 and a check name of check-nginx would yield webserver01/check-nginx.

UnixTime template function

A Sensu Go event contains multiple timestamps (e.g. .Check.Issued, .Check.Executed, .Check.LastOk) that are presented in UNIX timestamp format. A function named UnixTime is provided to print these values in a customizable human readable format as part of a template. To customize the output format of the timestamp, use the same format as specified by Go's time.Format. Additional examples can be found here.

Note: the predefined format constants are not available.

The example below demonstrates its use:

[...]
Service: {{.Entity.Name}}/{{.Check.Name}}
Executed: {{(UnixTime .Check.Executed).Format "2 Jan 2006 15:04:05"}}
Last OK: {{(UnixTime .Check.LastOK).Format "2 Jan 2006 15:04:05"}}
[...]

sensu-plugin-sdk's People

Contributors

amdprophet avatar c-kruse avatar ccressent avatar dependabot[bot] avatar echlebek avatar fguimond avatar jspaleta avatar palourde avatar portertech avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

sensu-plugin-sdk's Issues

support downcased annotations

Any annotations read from agent.yml are automatically downcased, so we should figure out if it's possible to load these annotations in plugins which require them. Presently we recommend camelCase annotation keys in docs, and these are silently downcased only in the case of loading from agent.yml. If a camelCase key is already present on the entity, loading the same key regardless of case will cause the camelCase key to be overwritten by a lowercase key.

exitFunction creates a problem for plugin testing

I'm not sure the best way to address this, but here's the problem.

because Execute() eventually calls exitFunction which defaults to os.Exit the main() function pattern as used in the template plugin github projects are very difficult to test using go test
What happens is main() will exit the test entirely unless you set exitFunction, but exitFunction isn't exposed as a tunable.

I'm able to work-around this using reflect and unsafe golang packages but that seems like a lot of work to support some basic testing that we need all derived plugins to be doing.
Example workaround: https://github.com/sensu/sensu-kafka-handler/blob/master/main_test.go

Straw proposal, extend the interface that exposes Execute, with a setter function for exitFunction.

Add a template helper for converting slice of bytes to UUID representation

I want to include event IDs in notifications, e.g. providing the uuid form of the ID in an email for debugging purposes.

Using the email handler template capability, I can create emails using the template Event ID: {{ .ID }} but this is a slice of bytes and is rendered in the resulting email as Event ID: [209 148 75 68 153 63 79 30 190 227 200 57 207 112 180 28]

I believe the UUID representation can be created from a slice of bytes using https://godoc.org/github.com/google/uuid#FromBytes .

The SDK implements some helpers for rendering templates, e.g. UnixTime template function. I propose we add a UUIDFromBytes template function which would make the above linked function available to handlers using the SDK.

Bug: Annotation overrides for StringSlice arguments are not treated as space delimited like cmdline and env var.

Problem

Annotation overrides for StringSlice arguments assume a json encoding instead of using the space delimited behavior used in the arguments and environment variables.

Let's make arguments parse in a more consistent manner.

Straw proposal

  1. Arguments of cobra StringSlice type use CSV styled space delimited in al contexts.
  2. Arguments of cobra StringArray type are parsed in ENV and annotation as a json array of strings, if not parsable as json assume its a single string.

How to keep a state between two handler's executions?

Hi Sensu team,

I see that the handler execution is stateless, but in some use cases it's useful to keep a state between two executions. Is there any existing solution for this use case?

If not, do you think it's something that might be implemented in the future?

Thank you

Required options

Add a Required attribute to PluginConfigOption, which would automatically ensure that the option was provided, so plugins developer don't have manually loop through options to make sure they were indeed provided.

Imports with old name on v0.13.0 bug

Problem:

go get: github.com/sensu-community/sensu-plugin-sdk@none updating to
	github.com/sensu-community/[email protected]: parsing go.mod:
	module declares its path as: github.com/sensu/sensu-plugin-sdk
	        but was required as: github.com/sensu-community/sensu-plugin-sdk

One of the places:

"github.com/sensu-community/sensu-plugin-sdk/version"

"github.com/sensu-community/sensu-plugin-sdk/httpclient"

https://github.com/sensu/sensu-plugin-sdk/search?q=sensu-community

Add version command

Add a version command that will output version information that can be set via ldflags. By default, the following are used by goreleaser:

-X main.version={{.Version}}
-X main.commit={{.Commit}}
-X main.date={{.Date}}
-X main.builtBy=goreleaser

Feature Request: Extend argument parsing support for pflags.StringArray

Currently:
currently string slice arguments are handled as pflags.StringSlice and this treats strings as CSV comma delimited strings making it possible to use a single argument call and extract a multiple value string slice from a single string.
Problem:
Automatic use of commas as separates is not always appropriate

Solution:
optionally allow arguments to be treated as pflags.StringArray, which does not automatically used the comma delimited string splitting.

Straw Implementation:
Extend the option struct with a new boolean to treat string slice as a pflags.StringArray in setupFlag() function

Feature Enhancement: Include helper functions for handling of prometheus exposition formatted metrics

Feature Description

Check plugin use case

Prometheus exposition format is desirable across several metric check plugins, it would be useful to have a set of SDK functions that check plugins could make use of for consistent well formed Prometheus exposition output instead of having each plugin roll its own process.

Handler plugin use case

Sensu handlers based on this SDK would also benefit from helper function that know how to take Sensu internal metrics format and convert them into Prometheus exposition in a consistent manner.

Considerations

  1. Prometheus metrics exposition format makes use of specially formatted unique comment strings to provide type information decorating a family of related metrics. These comment strings can only show up once per metrics family group.
  2. Sensu agent ingests Prometheus exposition format and converts the comments into specially named Sensu metrics tags that appear on all metrics. To support the handler use case, SDK metrics helper function would need to set/comprehend specially named tags consistent with how sensu-agent currently does the task.

Straw Implementation

Helper function to populate Sensu Metrics object programmatically

Helper function must be able to store optional TYPE and HELP comments consistent with how Sensu Agent ingests prom exposition metrics.

Check plugin use case

Check plugin executable would use this function to populate a Sensu Metrics structure as part of metrics collection business logic.

Handler plugin use case

Handlers are not expected to need this function

Helper function to output Sensu Metrics in prom exposition format

Helper function must be able to take Sensu Metrics object and export it in prometheus exposition formatted string building TYPE and HELP comment strings from Metrics tag information consistent with how sensu-agent currently ingests prometheus comment strings.

Check plugin use case

Check plugin executable would use this function to export metrics collected into Sensu Metrics object as Prometheus exposition formatted string.

Handler plugin use case

Handler plugin executable would use this function to export Sensu Metrics object as Prometheus exposition formatted string.

Minimum Viable Goal

  1. Need to be able to refactor system-check plugin to use these helper functions to create prom exp output:
./system-check 
# HELP system_cpu_cores [GAUGE] Number of cpu cores on the system
# TYPE system_cpu_cores GAUGE
system_cpu_cores{} 8 1636582440402
# HELP system_cpu_idle [GAUGE] Percent of time all cpus were idle
# TYPE system_cpu_idle GAUGE
system_cpu_idle{cpu="cpu0"} 74.49664429582639 1636582440402
system_cpu_idle{cpu="cpu1"} 78.26086956470425 1636582440402
system_cpu_idle{cpu="cpu2"} 77.07641196137524 1636582440402
system_cpu_idle{cpu="cpu3"} 81.27090300950525 1636582440402
system_cpu_idle{cpu="cpu4"} 76.8456375843412 1636582440402
system_cpu_idle{cpu="cpu5"} 78.1144781152644 1636582440402
system_cpu_idle{cpu="cpu6"} 81.72757475139935 1636582440402
system_cpu_idle{cpu="cpu7"} 81.33333333413934 1636582440402
system_cpu_idle{cpu="cpu-total"} 78.56247388180051 1636582440402
# HELP system_cpu_used [GAUGE] Percent of time all cpus were used
# TYPE system_cpu_used GAUGE
system_cpu_used{cpu="cpu0"} 25.503355704173615 1636582440402
system_cpu_used{cpu="cpu1"} 21.73913043529575 1636582440402
system_cpu_used{cpu="cpu2"} 22.923588038624757 1636582440402
system_cpu_used{cpu="cpu3"} 18.729096990494753 1636582440402
system_cpu_used{cpu="cpu4"} 23.154362415658795 1636582440402
system_cpu_used{cpu="cpu5"} 21.885521884735596 1636582440402
system_cpu_used{cpu="cpu6"} 18.272425248600655 1636582440402
system_cpu_used{cpu="cpu7"} 18.66666666586066 1636582440402
system_cpu_used{cpu="cpu-total"} 21.437526118199486 1636582440402
...
  1. Need to be able to refactor sumologic handler plugin using the same helper functions to take event generated from system-check and ingested by sensu-agent and faithfully reproduce original prom exp format string.

Rename PluginConfigOption

I find PluginConfigOption kind of confusing, especially with PluginConfig around; its name led me to believe it had a direct link with the plugin configuration.

Not sure what's the best solution from that point, here's some ideas:

  • Rename PluginConfig to PluginCommand
  • Rename PluginConfigOption to...
    • PluginInput
    • PluginOption
    • PluginFlag
    • PluginArg
    • etc.

Add support for more argument types

New argument types need to be supported to provide more flexibility to plugin developers. The following argument types should be supported.

  • uint16
  • uint32
  • int16
  • int32
  • int64
  • float32
  • float64

Support Sensu Go License Validation

Extract Sensu Go license validation logic from the commercial distribution of Sensu and place it in a new project repository, sensu/sensu-licensing. Update the SDK to import the licensing project and support optional license enforcement for Handlers. Update the commercial distribution of Sensu to import and use the licensing project. Handler plugin authors can then choose to require a valid license for plugin execution or partial functionality (good signature and valid until timestamp). The commercial distribution of Sensu Go will soon expose a cluster's active license file to handler plugins via an environment variable, SENSU_LICENSE_FILE. The license file is JSON encoded. This work will result in a new release of the SDK.

Proposal document: https://docs.google.com/document/d/1tHKIyx7kCetxwfpWfjUUlJCK9cpOiNrk2NWGgwLw8rQ/edit#

Feature Request: Impliment state file functionality in SDK

Rationale

Some check patterns require a small amount of state for correct operation. Lets bake that functionality into the SDK.

Check State Use Cases

  • log checking -> to keep up with file seek location to optimize log file reading
  • rate threshold checks -> many existing check threshold patterns currently require checks to internally wait and call an operation twice in order to calculate a rate. This behavior could be better optimized if the sdk kept a state file to be used.

Implementation requirements derived from current sensu-log-check implementation

Operational requirements/assumptions

  • Must assume the same plugin executable may be run with different settings on the same agent as different checks or check hooks, and each will need a different state file.
  • State file is a single access at a time.
  • State file contents must be able to represent a json-like golang struct with nested maps and slices.

Basic functionality needed

  1. Function to read state filepath into corresponding golang struct pointer
  • return nil if state file is missing or empty
  • return error if read permission error or similar
  1. Function to write golang struct pointer into state file
  • silently create file if missing.
  • return error if write permission error or similar

Advanced Functionality

If possible, implement state file with client access locking aware so that check can hold a lock on the file while the check is operating preventing a second check from access the file. This will prevent operators from accidentally using the same state file for multiple independently running checks or check hooks.

Hypothetical plugin check operation with locking

  1. Pass state filepath as cmdline argument and save into plugin level attribute
  2. Establish state file session enabling file access lock automatically at start of check execution function, defer session close as part of check execution function as part of normal SDK operation.
  3. Read state filepath contents and hold in golang struct pointer as part of check execution business logic.
  4. Perform additional check operation business logic
  5. Write golang state struct pointer into state filepath as part of check execution business logic
  6. Perform remaining chec operation business logic
  7. run deferred close state file session dropping the client access lock at check execution end

Convention over configuration

I believe we could decrease the number of decisions a plugin developer needs to do when using this SDK, by enforcing some conventions, and therefore simplify the process of creating your own plugin.

For example, the various NewGo... (e.g. sensu.NewGoHandler) require you to pass functions as arguments:

func main() {
	handler := sensu.NewGoHandler(&handler.PluginConfig, options, checkArgs, executeHandler)
	handler.Execute()
}

func checkArgs(_ *types.Event) error {}

func executeHandler(event *types.Event) error {}

I believe by adopting some conventions on the name of these functions, we could reduce the number of arguments required and therefore the complexity. For example, if we assumed that a Plugin must adhere to a given interface, we could simply do the following instead:

func main() {
	handler := sensu.NewGoHandler(&plugin)
	handler.Execute()
}

func (p *Plugin) Validate(_ *types.Event) error {}

func (p *Plugin) Execute(event *types.Event) error {}

Similarly, I don't believe declaring a PluginConfigOption should require that much attributes, e.g.

&sensu.PluginConfigOption{
			Path:      "example",
			Env:       "HANDLER_EXAMPLE",
			Argument:  "example",
			Shorthand: "e",
			Default:   "",
			Usage:     "An example configuration option",
			Value:     &handler.endpoint,
		},
	}

Instead, maybe we could simply assume sane default values (e.g. I assume the Path and the Argument could be similar) but allow those values to be overridden for edge cases.

Create abstraction for Sensu Checks

The first version of this repository contains an abstraction for the common Sensu handler logic. The abstraction is responsible to

  • Read and validate the Sensu event from stdin
  • Read the arguments from the command line, environment variable or Sensu event
  • Execute the user provided validation logic
  • Execute the user provided handler logic

We want to implement a similar pattern for Sensu checks. While handlers and checks are similar there are some differences (please correct me if I'm wrong).

  • Handlers read a Sensu Event from stdin while Checks don't
  • Handlers always read a Sensu Event from stdiin while checks optionally read one
  • Handlers can read input values from the event, while checks can't since they don't read an event
  • Checks write the return the result data to stdout or stderr, handlers don't
  • Checks exit status code indicates the state... 0=OK, 1=WARNING, 2=CRITICAL, other=UNKNOWN

You can look at the gohandler.go file, as some code can probably be reused. What I had in mind is to invoke the checks the same way the handlers are with some subtle differences.

  1. See the way options are defined at https://github.com/sensu-skunkworks/sensu-aws-ec2-deregistration-handler/blob/sensu-auth/main.go#L31. Very similar approach but no need to have the Path property since it defines where to look in the Sensu Event.
  2. Create a new sensu.NewGoCheck function instead of sensu.NewGoHandler https://github.com/sensu-skunkworks/sensu-aws-ec2-deregistration-handler/blob/sensu-auth/main.go#L31
  3. The sensu.NewGoCheck function should have an extra readSensuEvent argument which would tell whether or not the Sensu Check expects an event to be sent through stdin
  4. The execution method needs to have a way to return the exit status code, result and error. The result and error should automatically be written to stdout and stderr respectively. The exit status code should be used automatically when the check process exits. Signature might be something like this: func(event *types.Event) (int, string, error) - (exit status code, result, error) where event will be nil if the check doesn't read from stdin

Unit tests are required.

Once this is done the EC2 plugin can be converted to use the abstraction.

Add exit status logic to plugins

Checks

  • 0 indicates “OK”
  • 1 indicates “WARNING”
  • 2 indicates “CRITICAL”

Mutators

  • 0 indicates OK status
  • exit codes other than 0 indicate failure

Handlers

No mention, assuming the same as mutators

Feature Enhancement: Do not trigger usage message for errors in all cases

Problem Statement

base plugin pluginWorkflowFunction will always trigger the usage message if a non nil error is returned.

But in practice objects built on top of base plugin such as GoHandler and GoCheck have separate validation and execute functions.

Example GoHandler

Typically you want errors from GoHandler's validationFunction to trigger a usage message as the validationFunction is used for ensuring correct argument usage, and the usage message as output for failures there make sense as a breadcrumb for users to help them find the solution to the problem when using plugin as an asset.

But, when GoHandler's executeFunction returns an error you typically just want the error output, not the usage message, as the usage message in this context just adds noise.

Straw Enhancement Proposal

Extend base plugin's pluginWorkflowFunction to return additional boolean to indicate if usage message needs to be displayed.
Refactor derived plugin types to use new usage message boolean logic.

Avoid secrets leakage when displaying help output

When displaying --help output whether intentionally or when an error has occurred, if a secret is present as an environment variable assignable in PluginConfigOption, it could possibly be leaked.

One possible solution would be to add a PluginConfigOption boolean called 'secret' that when set to true would not print the default values in those cases.

Create abstraction for Sensu mutators

Create an abstraction to execute Sensu Mutators (https://docs.sensu.io/sensu-go/5.5/reference/mutators/). Mutators accept a Sensu event, transform the event and returns the output JSON.

The first version of this repository contains an abstraction for the common Sensu handles logic. The abstraction is responsible to

  • Read and validate the Sensu event from stdin
  • Read the arguments from the command line, environment variable or Sensu event
  • Execute the user provided validation logic
  • Execute the user provided handler logic

We want to implement something similar for mutators.

The similarities and differences are:

  • Mutators and handlers both read a Sensu event from stdin
  • Mutators and handlers both read command line and env variable arguments
  • Do we want to support configuration overrides in the mutators? @calebhailey
  • Mutators return the transformed event's JSON to stdout or stderr
  • Mutators return a 0 exit status code upon success or any other in case of failure

How to implement

  1. See the way options are defined at https://github.com/sensu-skunkworks/sensu-aws-ec2-deregistration-handler/blob/sensu-auth/main.go#L31. Very similar approach but no need to have the Path property since it defines where to look in the Sensu Event. Dependent on the support of configuration overrides
  2. Create a new sensu.NewGoMutator method instead of sensu.NewGoHandler https://github.com/sensu-skunkworks/sensu-aws-ec2-deregistration-handler/blob/sensu-auth/main.go#L31
  3. The validation method signature should be the same - https://github.com/sensu-skunkworks/sensu-aws-ec2-deregistration-handler/blob/sensu-auth/main.go#L31
  4. The execution method should return an interface object with JSON annotations, or an error - https://github.com/sensu-skunkworks/sensu-aws-ec2-deregistration-handler/blob/sensu-auth/main.go#L31 Signature might be something like func(event *types.Event) (*types.Event, error)
  5. If no error is present the exit status code is automatically set to 0, otherwise it is set to 1

Unit tests are required.

bad argument parsing not throwing an error with SDK v0.16

This appears to be a regression in v0.16 from previous releases, not sure how far back this goes.

When passing a bad argument for example "--gross icky" the output reads "Error executing blah-blah-plugin: unknown flag: --gross" but the return status is 0.

The return status should definitely be non-zero to help plugin users diagnose the problem.

This isn't solvable inside the plugins using the sdk. The sdk actually forces exits prior to any plugin specific business logic runs such as a checkArgs function

HTTP Client POST not working as intended

Recently, a community user attempted to use the HTTP client, but was unable to, because the client sends POST requests to the incorrect URL.

The issue is that the library uses a URL generated in part by URIPath, which includes the namespace and name of the resource. However, when targeting routes with POST, this results in incorrect paths.

The library should handle these situations gracefully and come up with the correct URL when instructed to use POST.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.