Adding upstream version 1.34.4.
Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
parent
e393c3af3f
commit
4978089aab
4963 changed files with 677545 additions and 0 deletions
79
plugins/outputs/sumologic/README.md
Normal file
79
plugins/outputs/sumologic/README.md
Normal file
|
@ -0,0 +1,79 @@
|
|||
# Sumo Logic Output Plugin
|
||||
|
||||
This plugin writes metrics to a [Sumo Logic HTTP Source][sumologic] using one
|
||||
of the following data formats:
|
||||
|
||||
- `graphite` for Content-Type of `application/vnd.sumologic.graphite`
|
||||
- `carbon2` for Content-Type of `application/vnd.sumologic.carbon2`
|
||||
- `prometheus` for Content-Type of `application/vnd.sumologic.prometheus`
|
||||
|
||||
⭐ Telegraf v1.16.0
|
||||
🏷️ logging
|
||||
💻 all
|
||||
|
||||
[sumologic]: https://help.sumologic.com/03Send-Data/Sources/02Sources-for-Hosted-Collectors/HTTP-Source/Upload-Metrics-to-an-HTTP-Source
|
||||
|
||||
## Global configuration options <!-- @/docs/includes/plugin_config.md -->
|
||||
|
||||
In addition to the plugin-specific configuration settings, plugins support
|
||||
additional global and plugin configuration settings. These settings are used to
|
||||
modify metrics, tags, and field or create aliases and configure ordering, etc.
|
||||
See the [CONFIGURATION.md][CONFIGURATION.md] for more details.
|
||||
|
||||
[CONFIGURATION.md]: ../../../docs/CONFIGURATION.md#plugins
|
||||
|
||||
## Configuration
|
||||
|
||||
```toml @sample.conf
|
||||
# A plugin that can send metrics to Sumo Logic HTTP metric collector.
|
||||
[[outputs.sumologic]]
|
||||
## Unique URL generated for your HTTP Metrics Source.
|
||||
## This is the address to send metrics to.
|
||||
# url = "https://events.sumologic.net/receiver/v1/http/<UniqueHTTPCollectorCode>"
|
||||
|
||||
## Data format to be used for sending metrics.
|
||||
## This will set the "Content-Type" header accordingly.
|
||||
## Currently supported formats:
|
||||
## * graphite - for Content-Type of application/vnd.sumologic.graphite
|
||||
## * carbon2 - for Content-Type of application/vnd.sumologic.carbon2
|
||||
## * prometheus - for Content-Type of application/vnd.sumologic.prometheus
|
||||
##
|
||||
## More information can be found at:
|
||||
## https://help.sumologic.com/03Send-Data/Sources/02Sources-for-Hosted-Collectors/HTTP-Source/Upload-Metrics-to-an-HTTP-Source#content-type-headers-for-metrics
|
||||
##
|
||||
## NOTE:
|
||||
## When unset, telegraf will by default use the influx serializer which is currently unsupported
|
||||
## in HTTP Source.
|
||||
data_format = "carbon2"
|
||||
|
||||
## Timeout used for HTTP request
|
||||
# timeout = "5s"
|
||||
|
||||
## Max HTTP request body size in bytes before compression (if applied).
|
||||
## By default 1MB is recommended.
|
||||
## NOTE:
|
||||
## Bear in mind that in some serializer a metric even though serialized to multiple
|
||||
## lines cannot be split any further so setting this very low might not work
|
||||
## as expected.
|
||||
# max_request_body_size = 1000000
|
||||
|
||||
## Additional, Sumo specific options.
|
||||
## Full list can be found here:
|
||||
## https://help.sumologic.com/03Send-Data/Sources/02Sources-for-Hosted-Collectors/HTTP-Source/Upload-Metrics-to-an-HTTP-Source#supported-http-headers
|
||||
|
||||
## Desired source name.
|
||||
## Useful if you want to override the source name configured for the source.
|
||||
# source_name = ""
|
||||
|
||||
## Desired host name.
|
||||
## Useful if you want to override the source host configured for the source.
|
||||
# source_host = ""
|
||||
|
||||
## Desired source category.
|
||||
## Useful if you want to override the source category configured for the source.
|
||||
# source_category = ""
|
||||
|
||||
## Comma-separated key=value list of dimensions to apply to every metric.
|
||||
## Custom dimensions will allow you to query your metrics at a more granular level.
|
||||
# dimensions = ""
|
||||
```
|
51
plugins/outputs/sumologic/sample.conf
Normal file
51
plugins/outputs/sumologic/sample.conf
Normal file
|
@ -0,0 +1,51 @@
|
|||
# A plugin that can send metrics to Sumo Logic HTTP metric collector.
|
||||
[[outputs.sumologic]]
|
||||
## Unique URL generated for your HTTP Metrics Source.
|
||||
## This is the address to send metrics to.
|
||||
# url = "https://events.sumologic.net/receiver/v1/http/<UniqueHTTPCollectorCode>"
|
||||
|
||||
## Data format to be used for sending metrics.
|
||||
## This will set the "Content-Type" header accordingly.
|
||||
## Currently supported formats:
|
||||
## * graphite - for Content-Type of application/vnd.sumologic.graphite
|
||||
## * carbon2 - for Content-Type of application/vnd.sumologic.carbon2
|
||||
## * prometheus - for Content-Type of application/vnd.sumologic.prometheus
|
||||
##
|
||||
## More information can be found at:
|
||||
## https://help.sumologic.com/03Send-Data/Sources/02Sources-for-Hosted-Collectors/HTTP-Source/Upload-Metrics-to-an-HTTP-Source#content-type-headers-for-metrics
|
||||
##
|
||||
## NOTE:
|
||||
## When unset, telegraf will by default use the influx serializer which is currently unsupported
|
||||
## in HTTP Source.
|
||||
data_format = "carbon2"
|
||||
|
||||
## Timeout used for HTTP request
|
||||
# timeout = "5s"
|
||||
|
||||
## Max HTTP request body size in bytes before compression (if applied).
|
||||
## By default 1MB is recommended.
|
||||
## NOTE:
|
||||
## Bear in mind that in some serializer a metric even though serialized to multiple
|
||||
## lines cannot be split any further so setting this very low might not work
|
||||
## as expected.
|
||||
# max_request_body_size = 1000000
|
||||
|
||||
## Additional, Sumo specific options.
|
||||
## Full list can be found here:
|
||||
## https://help.sumologic.com/03Send-Data/Sources/02Sources-for-Hosted-Collectors/HTTP-Source/Upload-Metrics-to-an-HTTP-Source#supported-http-headers
|
||||
|
||||
## Desired source name.
|
||||
## Useful if you want to override the source name configured for the source.
|
||||
# source_name = ""
|
||||
|
||||
## Desired host name.
|
||||
## Useful if you want to override the source host configured for the source.
|
||||
# source_host = ""
|
||||
|
||||
## Desired source category.
|
||||
## Useful if you want to override the source category configured for the source.
|
||||
# source_category = ""
|
||||
|
||||
## Comma-separated key=value list of dimensions to apply to every metric.
|
||||
## Custom dimensions will allow you to query your metrics at a more granular level.
|
||||
# dimensions = ""
|
275
plugins/outputs/sumologic/sumologic.go
Normal file
275
plugins/outputs/sumologic/sumologic.go
Normal file
|
@ -0,0 +1,275 @@
|
|||
//go:generate ../../../tools/readme_config_includer/generator
|
||||
package sumologic
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
_ "embed"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/influxdata/telegraf"
|
||||
"github.com/influxdata/telegraf/config"
|
||||
"github.com/influxdata/telegraf/internal"
|
||||
"github.com/influxdata/telegraf/models"
|
||||
"github.com/influxdata/telegraf/plugins/outputs"
|
||||
"github.com/influxdata/telegraf/plugins/serializers/carbon2"
|
||||
"github.com/influxdata/telegraf/plugins/serializers/graphite"
|
||||
"github.com/influxdata/telegraf/plugins/serializers/prometheus"
|
||||
)
|
||||
|
||||
//go:embed sample.conf
|
||||
var sampleConfig string
|
||||
|
||||
const (
|
||||
defaultClientTimeout = 5 * time.Second
|
||||
defaultMethod = http.MethodPost
|
||||
defaultMaxRequestBodySize = 1000000
|
||||
|
||||
contentTypeHeader = "Content-Type"
|
||||
carbon2ContentType = "application/vnd.sumologic.carbon2"
|
||||
graphiteContentType = "application/vnd.sumologic.graphite"
|
||||
prometheusContentType = "application/vnd.sumologic.prometheus"
|
||||
)
|
||||
|
||||
type header string
|
||||
|
||||
const (
|
||||
sourceNameHeader header = `X-Sumo-Name`
|
||||
sourceHostHeader header = `X-Sumo-Host`
|
||||
sourceCategoryHeader header = `X-Sumo-Category`
|
||||
dimensionsHeader header = `X-Sumo-Dimensions`
|
||||
)
|
||||
|
||||
type SumoLogic struct {
|
||||
URL string `toml:"url"`
|
||||
Timeout config.Duration `toml:"timeout"`
|
||||
MaxRequestBodySize config.Size `toml:"max_request_body_size"`
|
||||
|
||||
SourceName string `toml:"source_name"`
|
||||
SourceHost string `toml:"source_host"`
|
||||
SourceCategory string `toml:"source_category"`
|
||||
Dimensions string `toml:"dimensions"`
|
||||
|
||||
Log telegraf.Logger `toml:"-"`
|
||||
|
||||
client *http.Client
|
||||
serializer telegraf.Serializer
|
||||
|
||||
headers map[string]string
|
||||
}
|
||||
|
||||
func (*SumoLogic) SampleConfig() string {
|
||||
return sampleConfig
|
||||
}
|
||||
|
||||
func (s *SumoLogic) SetSerializer(serializer telegraf.Serializer) {
|
||||
s.serializer = serializer
|
||||
}
|
||||
|
||||
func (s *SumoLogic) createClient() *http.Client {
|
||||
return &http.Client{
|
||||
Transport: &http.Transport{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
},
|
||||
Timeout: time.Duration(s.Timeout),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SumoLogic) Connect() error {
|
||||
s.headers = make(map[string]string)
|
||||
|
||||
var serializer telegraf.Serializer
|
||||
if unwrapped, ok := s.serializer.(*models.RunningSerializer); ok {
|
||||
serializer = unwrapped.Serializer
|
||||
} else {
|
||||
serializer = s.serializer
|
||||
}
|
||||
|
||||
switch serializer.(type) {
|
||||
case *carbon2.Serializer:
|
||||
s.headers[contentTypeHeader] = carbon2ContentType
|
||||
case *graphite.GraphiteSerializer:
|
||||
s.headers[contentTypeHeader] = graphiteContentType
|
||||
case *prometheus.Serializer:
|
||||
s.headers[contentTypeHeader] = prometheusContentType
|
||||
default:
|
||||
return fmt.Errorf("unsupported serializer %T", serializer)
|
||||
}
|
||||
|
||||
if s.Timeout == 0 {
|
||||
s.Timeout = config.Duration(defaultClientTimeout)
|
||||
}
|
||||
|
||||
s.client = s.createClient()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (*SumoLogic) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SumoLogic) Write(metrics []telegraf.Metric) error {
|
||||
if s.serializer == nil {
|
||||
return errors.New("sumologic: serializer unset")
|
||||
}
|
||||
if len(metrics) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
reqBody, err := s.serializer.SerializeBatch(metrics)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if l := len(reqBody); l > int(s.MaxRequestBodySize) {
|
||||
chunks, err := s.splitIntoChunks(metrics)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.writeRequestChunks(chunks)
|
||||
}
|
||||
|
||||
return s.writeRequestChunk(reqBody)
|
||||
}
|
||||
|
||||
func (s *SumoLogic) writeRequestChunks(chunks [][]byte) error {
|
||||
for _, reqChunk := range chunks {
|
||||
if err := s.writeRequestChunk(reqChunk); err != nil {
|
||||
s.Log.Errorf("Error sending chunk: %v", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SumoLogic) writeRequestChunk(reqBody []byte) error {
|
||||
var (
|
||||
err error
|
||||
buff bytes.Buffer
|
||||
gz = gzip.NewWriter(&buff)
|
||||
)
|
||||
|
||||
if _, err = gz.Write(reqBody); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := gz.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(defaultMethod, s.URL, &buff)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Encoding", "gzip")
|
||||
req.Header.Set("User-Agent", internal.ProductToken())
|
||||
|
||||
// Set headers coming from the configuration.
|
||||
for k, v := range s.headers {
|
||||
req.Header.Set(k, v)
|
||||
}
|
||||
|
||||
setHeaderIfSetInConfig(req, sourceNameHeader, s.SourceName)
|
||||
setHeaderIfSetInConfig(req, sourceHostHeader, s.SourceHost)
|
||||
setHeaderIfSetInConfig(req, sourceCategoryHeader, s.SourceCategory)
|
||||
setHeaderIfSetInConfig(req, dimensionsHeader, s.Dimensions)
|
||||
|
||||
resp, err := s.client.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("sumologic: failed sending request to %q: %w", s.URL, err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
return fmt.Errorf("sumologic: when writing to %q received status code: %d", s.URL, resp.StatusCode)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// splitIntoChunks splits metrics to be sent into chunks so that every request
|
||||
// is smaller than s.MaxRequestBodySize unless it was configured so small so that
|
||||
// even a single metric cannot fit.
|
||||
// In such a situation metrics will be sent one by one with a warning being logged
|
||||
// for every request sent even though they don't fit in s.MaxRequestBodySize bytes.
|
||||
func (s *SumoLogic) splitIntoChunks(metrics []telegraf.Metric) ([][]byte, error) {
|
||||
var (
|
||||
numMetrics = len(metrics)
|
||||
chunks = make([][]byte, 0)
|
||||
)
|
||||
|
||||
for i := 0; i < numMetrics; {
|
||||
var toAppend []byte
|
||||
for i < numMetrics {
|
||||
chunkBody, err := s.serializer.Serialize(metrics[i])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
la := len(toAppend)
|
||||
if la != 0 {
|
||||
// We already have something to append ...
|
||||
if la+len(chunkBody) > int(s.MaxRequestBodySize) {
|
||||
// ... and it's just the right size, without currently processed chunk.
|
||||
break
|
||||
}
|
||||
// ... we can try appending more.
|
||||
i++
|
||||
toAppend = append(toAppend, chunkBody...)
|
||||
continue
|
||||
}
|
||||
|
||||
// la == 0
|
||||
i++
|
||||
toAppend = chunkBody
|
||||
|
||||
if len(chunkBody) > int(s.MaxRequestBodySize) {
|
||||
s.Log.Warnf(
|
||||
"max_request_body_size set to %d which is too small even for a single metric (len: %d), sending without split",
|
||||
s.MaxRequestBodySize, len(chunkBody),
|
||||
)
|
||||
|
||||
// The serialized metric is too big, but we have no choice
|
||||
// but to send it.
|
||||
// max_request_body_size was set so small that it wouldn't
|
||||
// even accommodate a single metric.
|
||||
break
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if toAppend == nil {
|
||||
break
|
||||
}
|
||||
|
||||
chunks = append(chunks, toAppend)
|
||||
}
|
||||
|
||||
return chunks, nil
|
||||
}
|
||||
|
||||
func setHeaderIfSetInConfig(r *http.Request, h header, value string) {
|
||||
if value != "" {
|
||||
r.Header.Set(string(h), value)
|
||||
}
|
||||
}
|
||||
|
||||
func Default() *SumoLogic {
|
||||
return &SumoLogic{
|
||||
Timeout: config.Duration(defaultClientTimeout),
|
||||
MaxRequestBodySize: defaultMaxRequestBodySize,
|
||||
headers: make(map[string]string),
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
outputs.Add("sumologic", func() telegraf.Output {
|
||||
return Default()
|
||||
})
|
||||
}
|
709
plugins/outputs/sumologic/sumologic_test.go
Normal file
709
plugins/outputs/sumologic/sumologic_test.go
Normal file
|
@ -0,0 +1,709 @@
|
|||
package sumologic
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/influxdata/telegraf"
|
||||
"github.com/influxdata/telegraf/config"
|
||||
"github.com/influxdata/telegraf/internal"
|
||||
"github.com/influxdata/telegraf/metric"
|
||||
"github.com/influxdata/telegraf/plugins/serializers/carbon2"
|
||||
"github.com/influxdata/telegraf/plugins/serializers/graphite"
|
||||
"github.com/influxdata/telegraf/plugins/serializers/prometheus"
|
||||
"github.com/influxdata/telegraf/testutil"
|
||||
)
|
||||
|
||||
func getMetric() telegraf.Metric {
|
||||
m := metric.New(
|
||||
"cpu",
|
||||
map[string]string{},
|
||||
map[string]interface{}{
|
||||
"value": 42.0,
|
||||
},
|
||||
time.Unix(0, 0),
|
||||
)
|
||||
return m
|
||||
}
|
||||
|
||||
func getMetrics() []telegraf.Metric {
|
||||
const count = 100
|
||||
var metrics = make([]telegraf.Metric, count)
|
||||
|
||||
for i := 0; i < count; i++ {
|
||||
m := metric.New(
|
||||
fmt.Sprintf("cpu-%d", i),
|
||||
map[string]string{
|
||||
"ec2_instance": "aws-129038123",
|
||||
"image": "aws-ami-1234567890",
|
||||
},
|
||||
map[string]interface{}{
|
||||
"idle": 5876876,
|
||||
"steal": 5876876,
|
||||
"system": 5876876,
|
||||
"user": 5876876,
|
||||
"temp": 70.0,
|
||||
},
|
||||
time.Unix(0, 0),
|
||||
)
|
||||
metrics[i] = m
|
||||
}
|
||||
return metrics
|
||||
}
|
||||
|
||||
func TestMethod(t *testing.T) {
|
||||
ts := httptest.NewServer(http.NotFoundHandler())
|
||||
defer ts.Close()
|
||||
|
||||
u, err := url.Parse("http://" + ts.Listener.Addr().String())
|
||||
require.NoError(t, err)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
plugin func() *SumoLogic
|
||||
expectedMethod string
|
||||
connectError bool
|
||||
}{
|
||||
{
|
||||
name: "default method is POST",
|
||||
plugin: func() *SumoLogic {
|
||||
s := Default()
|
||||
s.URL = u.String()
|
||||
return s
|
||||
},
|
||||
expectedMethod: http.MethodPost,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ts.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != tt.expectedMethod {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
t.Errorf("Not equal, expected: %q, actual: %q", tt.expectedMethod, r.Method)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
})
|
||||
|
||||
serializer := &carbon2.Serializer{
|
||||
Format: "field_separate",
|
||||
}
|
||||
require.NoError(t, serializer.Init())
|
||||
|
||||
plugin := tt.plugin()
|
||||
plugin.SetSerializer(serializer)
|
||||
err = plugin.Connect()
|
||||
if tt.connectError {
|
||||
require.Error(t, err)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
|
||||
err = plugin.Write([]telegraf.Metric{getMetric()})
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestStatusCode(t *testing.T) {
|
||||
ts := httptest.NewServer(http.NotFoundHandler())
|
||||
defer ts.Close()
|
||||
|
||||
u, err := url.Parse("http://" + ts.Listener.Addr().String())
|
||||
require.NoError(t, err)
|
||||
|
||||
pluginFn := func() *SumoLogic {
|
||||
s := Default()
|
||||
s.URL = u.String()
|
||||
return s
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
plugin *SumoLogic
|
||||
statusCode int
|
||||
errFunc func(t *testing.T, err error)
|
||||
}{
|
||||
{
|
||||
name: "success",
|
||||
plugin: pluginFn(),
|
||||
statusCode: http.StatusOK,
|
||||
errFunc: func(t *testing.T, err error) {
|
||||
require.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "1xx status is an error",
|
||||
plugin: pluginFn(),
|
||||
statusCode: http.StatusSwitchingProtocols,
|
||||
errFunc: func(t *testing.T, err error) {
|
||||
require.Error(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "3xx status is an error",
|
||||
plugin: pluginFn(),
|
||||
statusCode: http.StatusMultipleChoices,
|
||||
errFunc: func(t *testing.T, err error) {
|
||||
require.Error(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "4xx status is an error",
|
||||
plugin: pluginFn(),
|
||||
statusCode: http.StatusBadRequest,
|
||||
errFunc: func(t *testing.T, err error) {
|
||||
require.Error(t, err)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ts.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.WriteHeader(tt.statusCode)
|
||||
})
|
||||
|
||||
serializer := &carbon2.Serializer{
|
||||
Format: "field_separate",
|
||||
}
|
||||
require.NoError(t, serializer.Init())
|
||||
|
||||
tt.plugin.SetSerializer(serializer)
|
||||
err = tt.plugin.Connect()
|
||||
require.NoError(t, err)
|
||||
|
||||
err = tt.plugin.Write([]telegraf.Metric{getMetric()})
|
||||
tt.errFunc(t, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestContentType(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
plugin func() *SumoLogic
|
||||
expectedBody []byte
|
||||
}{
|
||||
{
|
||||
name: "carbon2 (data format = field separate) is supported",
|
||||
plugin: func() *SumoLogic {
|
||||
s := Default()
|
||||
s.headers = map[string]string{
|
||||
contentTypeHeader: carbon2ContentType,
|
||||
}
|
||||
serializer := &carbon2.Serializer{
|
||||
Format: "field_separate",
|
||||
}
|
||||
require.NoError(t, serializer.Init())
|
||||
s.SetSerializer(serializer)
|
||||
return s
|
||||
},
|
||||
expectedBody: []byte("metric=cpu field=value 42 0\n"),
|
||||
},
|
||||
{
|
||||
name: "carbon2 (data format = metric includes field) is supported",
|
||||
plugin: func() *SumoLogic {
|
||||
s := Default()
|
||||
s.headers = map[string]string{
|
||||
contentTypeHeader: carbon2ContentType,
|
||||
}
|
||||
serializer := &carbon2.Serializer{
|
||||
Format: "metric_includes_field",
|
||||
}
|
||||
require.NoError(t, serializer.Init())
|
||||
s.SetSerializer(serializer)
|
||||
return s
|
||||
},
|
||||
expectedBody: []byte("metric=cpu_value 42 0\n"),
|
||||
},
|
||||
{
|
||||
name: "graphite is supported",
|
||||
plugin: func() *SumoLogic {
|
||||
s := Default()
|
||||
s.headers = map[string]string{
|
||||
contentTypeHeader: graphiteContentType,
|
||||
}
|
||||
serializer := &graphite.GraphiteSerializer{}
|
||||
require.NoError(t, serializer.Init())
|
||||
s.SetSerializer(serializer)
|
||||
return s
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "prometheus is supported",
|
||||
plugin: func() *SumoLogic {
|
||||
s := Default()
|
||||
s.headers = map[string]string{
|
||||
contentTypeHeader: prometheusContentType,
|
||||
}
|
||||
s.SetSerializer(&prometheus.Serializer{})
|
||||
return s
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var body bytes.Buffer
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
gz, err := gzip.NewReader(r.Body)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
var maxDecompressionSize int64 = 500 * 1024 * 1024
|
||||
n, err := io.CopyN(&body, gz, maxDecompressionSize)
|
||||
if errors.Is(err, io.EOF) {
|
||||
err = nil
|
||||
}
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
if n > maxDecompressionSize {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
t.Errorf("Size of decoded data exceeds (%v) allowed size (%v)", n, maxDecompressionSize)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
u, err := url.Parse("http://" + ts.Listener.Addr().String())
|
||||
require.NoError(t, err)
|
||||
|
||||
plugin := tt.plugin()
|
||||
plugin.URL = u.String()
|
||||
|
||||
require.NoError(t, plugin.Connect())
|
||||
|
||||
err = plugin.Write([]telegraf.Metric{getMetric()})
|
||||
require.NoError(t, err)
|
||||
|
||||
if tt.expectedBody != nil {
|
||||
require.Equal(t, string(tt.expectedBody), body.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestContentEncodingGzip(t *testing.T) {
|
||||
ts := httptest.NewServer(http.NotFoundHandler())
|
||||
defer ts.Close()
|
||||
|
||||
u, err := url.Parse("http://" + ts.Listener.Addr().String())
|
||||
require.NoError(t, err)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
plugin func() *SumoLogic
|
||||
}{
|
||||
{
|
||||
name: "default content_encoding=gzip works",
|
||||
plugin: func() *SumoLogic {
|
||||
s := Default()
|
||||
s.URL = u.String()
|
||||
return s
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ts.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Header.Get("Content-Encoding") != "gzip" {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
t.Errorf("Not equal, expected: %q, actual: %q", "gzip", r.Header.Get("Content-Encoding"))
|
||||
return
|
||||
}
|
||||
|
||||
body, err := gzip.NewReader(r.Body)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
payload, err := io.ReadAll(body)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
if string(payload) != "metric=cpu field=value 42 0\n" {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
t.Errorf("Not equal, expected: %q, actual: %q", "metric=cpu field=value 42 0\n", string(payload))
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
})
|
||||
|
||||
serializer := &carbon2.Serializer{
|
||||
Format: "field_separate",
|
||||
}
|
||||
require.NoError(t, serializer.Init())
|
||||
|
||||
plugin := tt.plugin()
|
||||
|
||||
plugin.SetSerializer(serializer)
|
||||
err = plugin.Connect()
|
||||
require.NoError(t, err)
|
||||
|
||||
err = plugin.Write([]telegraf.Metric{getMetric()})
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultUserAgent(t *testing.T) {
|
||||
ts := httptest.NewServer(http.NotFoundHandler())
|
||||
defer ts.Close()
|
||||
|
||||
u, err := url.Parse("http://" + ts.Listener.Addr().String())
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("default-user-agent", func(t *testing.T) {
|
||||
ts.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Header.Get("User-Agent") != internal.ProductToken() {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
t.Errorf("Not equal, expected: %q, actual: %q", internal.ProductToken(), r.Header.Get("User-Agent"))
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
})
|
||||
|
||||
plugin := &SumoLogic{
|
||||
URL: u.String(),
|
||||
MaxRequestBodySize: Default().MaxRequestBodySize,
|
||||
}
|
||||
|
||||
serializer := &carbon2.Serializer{
|
||||
Format: "field_separate",
|
||||
}
|
||||
require.NoError(t, serializer.Init())
|
||||
|
||||
plugin.SetSerializer(serializer)
|
||||
err = plugin.Connect()
|
||||
require.NoError(t, err)
|
||||
|
||||
err = plugin.Write([]telegraf.Metric{getMetric()})
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestTOMLConfig(t *testing.T) {
|
||||
testcases := []struct {
|
||||
name string
|
||||
configBytes []byte
|
||||
expectedError bool
|
||||
}{
|
||||
{
|
||||
name: "carbon2 content type is supported",
|
||||
configBytes: []byte(`
|
||||
[[outputs.sumologic]]
|
||||
url = "https://localhost:3000"
|
||||
data_format = "carbon2"
|
||||
`),
|
||||
expectedError: false,
|
||||
},
|
||||
{
|
||||
name: "graphite content type is supported",
|
||||
configBytes: []byte(`
|
||||
[[outputs.sumologic]]
|
||||
url = "https://localhost:3000"
|
||||
data_format = "graphite"
|
||||
`),
|
||||
expectedError: false,
|
||||
},
|
||||
{
|
||||
name: "prometheus content type is supported",
|
||||
configBytes: []byte(`
|
||||
[[outputs.sumologic]]
|
||||
url = "https://localhost:3000"
|
||||
data_format = "prometheus"
|
||||
`),
|
||||
expectedError: false,
|
||||
},
|
||||
{
|
||||
name: "setting extra headers is not possible",
|
||||
configBytes: []byte(`
|
||||
[[outputs.sumologic]]
|
||||
url = "https://localhost:3000"
|
||||
data_format = "carbon2"
|
||||
[outputs.sumologic.headers]
|
||||
X-Sumo-Name = "dummy"
|
||||
X-Sumo-Host = "dummy"
|
||||
X-Sumo-Category = "dummy"
|
||||
X-Sumo-Dimensions = "dummy"
|
||||
`),
|
||||
expectedError: true,
|
||||
},
|
||||
{
|
||||
name: "full example from sample config is correct",
|
||||
configBytes: []byte(`
|
||||
[[outputs.sumologic]]
|
||||
url = "https://localhost:3000"
|
||||
data_format = "carbon2"
|
||||
timeout = "5s"
|
||||
source_name = "name"
|
||||
source_host = "hosta"
|
||||
source_category = "category"
|
||||
dimensions = "dimensions"
|
||||
`),
|
||||
expectedError: false,
|
||||
},
|
||||
{
|
||||
name: "unknown key - sumo_metadata - in config fails",
|
||||
configBytes: []byte(`
|
||||
[[outputs.sumologic]]
|
||||
url = "https://localhost:3000"
|
||||
data_format = "carbon2"
|
||||
timeout = "5s"
|
||||
source_name = "name"
|
||||
sumo_metadata = "metadata"
|
||||
`),
|
||||
expectedError: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range testcases {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := config.NewConfig()
|
||||
|
||||
if tt.expectedError {
|
||||
require.Error(t, c.LoadConfigData(tt.configBytes, config.EmptySourcePath))
|
||||
} else {
|
||||
require.NoError(t, c.LoadConfigData(tt.configBytes, config.EmptySourcePath))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMaxRequestBodySize(t *testing.T) {
|
||||
ts := httptest.NewServer(http.NotFoundHandler())
|
||||
defer ts.Close()
|
||||
|
||||
u, err := url.Parse("http://" + ts.Listener.Addr().String())
|
||||
require.NoError(t, err)
|
||||
|
||||
testcases := []struct {
|
||||
name string
|
||||
plugin func() *SumoLogic
|
||||
metrics []telegraf.Metric
|
||||
expectedError bool
|
||||
expectedRequestCount int32
|
||||
expectedMetricLinesCount int32
|
||||
}{
|
||||
{
|
||||
name: "default max request body size is 1MB and doesn't split small enough metric slices",
|
||||
plugin: func() *SumoLogic {
|
||||
s := Default()
|
||||
s.URL = u.String()
|
||||
return s
|
||||
},
|
||||
metrics: []telegraf.Metric{getMetric()},
|
||||
expectedError: false,
|
||||
expectedRequestCount: 1,
|
||||
expectedMetricLinesCount: 1,
|
||||
},
|
||||
{
|
||||
name: "default max request body size is 1MB and doesn't split small even medium sized metrics",
|
||||
plugin: func() *SumoLogic {
|
||||
s := Default()
|
||||
s.URL = u.String()
|
||||
return s
|
||||
},
|
||||
metrics: getMetrics(),
|
||||
expectedError: false,
|
||||
expectedRequestCount: 1,
|
||||
expectedMetricLinesCount: 500, // count (100) metrics, 5 lines per each (steal, idle, system, user, temp) = 500
|
||||
},
|
||||
{
|
||||
name: "when short by at least 1B the request is split",
|
||||
plugin: func() *SumoLogic {
|
||||
s := Default()
|
||||
s.URL = u.String()
|
||||
// getMetrics returns metrics that serialized (using carbon2),
|
||||
// uncompressed size is 43750B
|
||||
s.MaxRequestBodySize = 43_749
|
||||
return s
|
||||
},
|
||||
metrics: getMetrics(),
|
||||
expectedError: false,
|
||||
expectedRequestCount: 2,
|
||||
expectedMetricLinesCount: 500, // count (100) metrics, 5 lines per each (steal, idle, system, user, temp) = 500
|
||||
},
|
||||
{
|
||||
name: "max request body size properly splits requests - max 10_000",
|
||||
plugin: func() *SumoLogic {
|
||||
s := Default()
|
||||
s.URL = u.String()
|
||||
s.MaxRequestBodySize = 10_000
|
||||
return s
|
||||
},
|
||||
metrics: getMetrics(),
|
||||
expectedError: false,
|
||||
expectedRequestCount: 5,
|
||||
expectedMetricLinesCount: 500, // count (100) metrics, 5 lines per each (steal, idle, system, user, temp) = 500
|
||||
},
|
||||
{
|
||||
name: "max request body size properly splits requests - max 5_000",
|
||||
plugin: func() *SumoLogic {
|
||||
s := Default()
|
||||
s.URL = u.String()
|
||||
s.MaxRequestBodySize = 5_000
|
||||
return s
|
||||
},
|
||||
metrics: getMetrics(),
|
||||
expectedError: false,
|
||||
expectedRequestCount: 10,
|
||||
expectedMetricLinesCount: 500, // count (100) metrics, 5 lines per each (steal, idle, system, user, temp) = 500
|
||||
},
|
||||
{
|
||||
name: "max request body size properly splits requests - max 2_500",
|
||||
plugin: func() *SumoLogic {
|
||||
s := Default()
|
||||
s.URL = u.String()
|
||||
s.MaxRequestBodySize = 2_500
|
||||
return s
|
||||
},
|
||||
metrics: getMetrics(),
|
||||
expectedError: false,
|
||||
expectedRequestCount: 20,
|
||||
expectedMetricLinesCount: 500, // count (100) metrics, 5 lines per each (steal, idle, system, user, temp) = 500
|
||||
},
|
||||
{
|
||||
name: "max request body size properly splits requests - max 1_000",
|
||||
plugin: func() *SumoLogic {
|
||||
s := Default()
|
||||
s.URL = u.String()
|
||||
s.MaxRequestBodySize = 1_000
|
||||
return s
|
||||
},
|
||||
metrics: getMetrics(),
|
||||
expectedError: false,
|
||||
expectedRequestCount: 50,
|
||||
expectedMetricLinesCount: 500, // count (100) metrics, 5 lines per each (steal, idle, system, user, temp) = 500
|
||||
},
|
||||
{
|
||||
name: "max request body size properly splits requests - max 500",
|
||||
plugin: func() *SumoLogic {
|
||||
s := Default()
|
||||
s.URL = u.String()
|
||||
s.MaxRequestBodySize = 500
|
||||
return s
|
||||
},
|
||||
metrics: getMetrics(),
|
||||
expectedError: false,
|
||||
expectedRequestCount: 100,
|
||||
expectedMetricLinesCount: 500, // count (100) metrics, 5 lines per each (steal, idle, system, user, temp) = 500
|
||||
},
|
||||
{
|
||||
name: "max request body size properly splits requests - max 300",
|
||||
plugin: func() *SumoLogic {
|
||||
s := Default()
|
||||
s.URL = u.String()
|
||||
s.MaxRequestBodySize = 300
|
||||
return s
|
||||
},
|
||||
metrics: getMetrics(),
|
||||
expectedError: false,
|
||||
expectedRequestCount: 100,
|
||||
expectedMetricLinesCount: 500, // count (100) metrics, 5 lines per each (steal, idle, system, user, temp) = 500
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range testcases {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var (
|
||||
requestCount int32
|
||||
linesCount int32
|
||||
)
|
||||
ts.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
atomic.AddInt32(&requestCount, 1)
|
||||
|
||||
if tt.expectedMetricLinesCount != 0 {
|
||||
atomic.AddInt32(&linesCount, int32(countLines(t, r.Body)))
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
})
|
||||
|
||||
serializer := &carbon2.Serializer{
|
||||
Format: "field_separate",
|
||||
}
|
||||
require.NoError(t, serializer.Init())
|
||||
|
||||
plugin := tt.plugin()
|
||||
plugin.SetSerializer(serializer)
|
||||
plugin.Log = testutil.Logger{}
|
||||
|
||||
err = plugin.Connect()
|
||||
require.NoError(t, err)
|
||||
|
||||
err = plugin.Write(tt.metrics)
|
||||
if tt.expectedError {
|
||||
require.Error(t, err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tt.expectedRequestCount, atomic.LoadInt32(&requestCount))
|
||||
require.Equal(t, tt.expectedMetricLinesCount, atomic.LoadInt32(&linesCount))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTryingToSendEmptyMetricsDoesntFail(t *testing.T) {
|
||||
ts := httptest.NewServer(http.NotFoundHandler())
|
||||
defer ts.Close()
|
||||
|
||||
u, err := url.Parse("http://" + ts.Listener.Addr().String())
|
||||
require.NoError(t, err)
|
||||
|
||||
metrics := make([]telegraf.Metric, 0)
|
||||
plugin := Default()
|
||||
plugin.URL = u.String()
|
||||
|
||||
serializer := &carbon2.Serializer{
|
||||
Format: "field_separate",
|
||||
}
|
||||
require.NoError(t, serializer.Init())
|
||||
plugin.SetSerializer(serializer)
|
||||
|
||||
err = plugin.Connect()
|
||||
require.NoError(t, err)
|
||||
|
||||
err = plugin.Write(metrics)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func countLines(t *testing.T, body io.Reader) int {
|
||||
// All requests coming from Sumo Logic output plugin are gzipped.
|
||||
gz, err := gzip.NewReader(body)
|
||||
require.NoError(t, err)
|
||||
|
||||
var linesCount int
|
||||
for s := bufio.NewScanner(gz); s.Scan(); {
|
||||
linesCount++
|
||||
}
|
||||
|
||||
return linesCount
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue