1
0
Fork 0

Adding upstream version 1.34.4.

Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
Daniel Baumann 2025-05-24 07:26:29 +02:00
parent e393c3af3f
commit 4978089aab
Signed by: daniel
GPG key ID: FBB4F0E80A80222F
4963 changed files with 677545 additions and 0 deletions

View 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 = ""
```

View 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 = ""

View 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()
})
}

View 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
}